pax_global_header00006660000000000000000000000064152737025500014520gustar00rootroot0000000000000052 comment=9450b29b8ea7c2f8b2d6db705c413db25ac111c3 pr0m1th3as-datatypes-9450b29/000077500000000000000000000000001527370255000156765ustar00rootroot00000000000000pr0m1th3as-datatypes-9450b29/.gitattributes000066400000000000000000000001251527370255000205670ustar00rootroot00000000000000docs export-ignore logos export-ignore README.md export-ignore .github export-ignore pr0m1th3as-datatypes-9450b29/COPYING000066400000000000000000001045131527370255000167350ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. 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. Copyright (C) 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 . 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: Copyright (C) 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 . 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 . pr0m1th3as-datatypes-9450b29/DESCRIPTION000066400000000000000000000006321527370255000174050ustar00rootroot00000000000000Name: datatypes Version: 1.6.2 Date: 2026-11-07 Author: Andreas Bertsatos Maintainer: Andreas Bertsatos Title: Data Types for GNU Octave Description: A package for providing extra data types and related functionality for GNU Octave. Categories: Datatypes Depends: octave (>= 11.1.0) Url: https://github.com/pr0m1th3as/datatypes Tracker: https://github.com/pr0m1th3as/datatypes/issues pr0m1th3as-datatypes-9450b29/INDEX000066400000000000000000000026531527370255000164760ustar00rootroot00000000000000datatypes >> datatypes categorical arrays categorical iscategorical calendarDuration arrays calendarDuration caldays calweeks calmonths calquarters calyears iscalendarduration datetime arrays datetime isdatetime isnat NaT duration arrays duration days years hours minutes seconds milliseconds isduration string arrays string strings convertCharsToStrings convertStringsToChars table arrays table array2table cell2table csv2table ods2table ods2struct readtable struct2ods struct2xlsx struct2table xlsx2struct istable timetable arrays timetable array2timetable csv2timetable ods2timetable readtimetable istimetable eventtable timeseries objects timeseries istimeseries tscollection istscollection timeseries2timetable tsdata.timemetadata tsdata.datametadata tsdata.qualmetadata tsdata.interpolation tsdata.event dictionary objects dictionary configureDictionary isdictionary readdictionary supplementary classes tabular missing vartype timerange withtol rowfilter eventfilter @cell/disp @cell/display supplementary functions findgroups splitapply groupcounts groupfilter groupsummary grouptransform discretize histcounts keyHash keyMatch parsePairedArguments istabular compiled functions __ckeyHash__ __nkeyHash__ __datetime__ __ldml__ __csv2table__ __table2csv__ __ods2table__ __table2ods__ __xlsx2table__ __table2xlsx__ __json2dictionary__ __dictionary2json__ pr0m1th3as-datatypes-9450b29/LICENSE.txt000066400000000000000000000003761527370255000175270ustar00rootroot00000000000000All software in the 'datatypes' package is published under the GNU General Public License version 3 (GPLv3). The package's logo ('doc/datatypes.png') is licensed under a Creative Commons Attribution-ShareAlike 4.0 International license (CC BY-SA 4.0). pr0m1th3as-datatypes-9450b29/NEWS000066400000000000000000000034321527370255000163770ustar00rootroot00000000000000Summary of important user-visible changes for datatypes 1.6.2: -------------------------------------------------------------- This patch release makes `ismissing` read its indicators as MATLAB does, and fixes `string` on big-endian machines and for text outside ASCII. ** `ismissing` on a `table` or `timetable` matches a `logical`, `single` or integer indicator only in variables of its own class and in `double` variables; it used to mark every numeric and logical variable, and a cell indicator such as `{int32(1), 2.4}` also marked 2. ** `missing` as an indicator marks the standard missing values of every class and of every table variable; it used to be refused or ignored. ** `NaN`, `NaT` and `` indicators now mark the missing elements of `duration`, `datetime` and `string` arrays. ** Text indicators on categorical data follow MATLAB: empty or blank text marks ``, and in tables a `` or `""` indicator no longer does. A blank indicator such as `' '` marks only cellstr elements holding that text, and a cell indicator no longer joins its texts into one. ** `calendarDuration.ismissing` takes an indicator, and tables take `calendarDuration` indicators, an Octave extension. ** Comparing strings with `<`, `<=`, `>` or `>=` no longer raises an index error when they hold characters outside ASCII. ** On big-endian machines, `strip` with no characters given removes whitespace again, strings with characters above U+00FF compare in the right order, and the test suite passes. Backwards incompatible improvements: ** A `NaN` indicator in `ismissing` on a table no longer marks `NaN` in `duration` and `calendarDuration` variables, as in MATLAB; use `missing`, or a `NaN` of the variable's own class. pr0m1th3as-datatypes-9450b29/doc/000077500000000000000000000000001527370255000164435ustar00rootroot00000000000000pr0m1th3as-datatypes-9450b29/doc/ODS_FORMAT.md000066400000000000000000000260171527370255000204700ustar00rootroot00000000000000# The `datatypes` OpenDocument spreadsheet format How the `datatypes` package for GNU Octave stores a `table` or a `timetable` in an OpenDocument spreadsheet, so that the file round-trips without losing variable types, formats, units, descriptions, row labels or an attached event table. **This is a description of what the package writes, not a standard.** It is versioned with the package and may change between releases. It is published so that a reader in another language can be written against it, and so that a file can be understood by someone holding only the file. Written for `datatypes` 1.6.0. ## Why a spreadsheet CSV carries values and nothing else: a column of dates comes back as text, and a variable's unit, description and display format are gone. Binary columnar formats such as Parquet carry types faithfully but cannot be opened by a person without tooling. OpenDocument is an OASIS standard, it is what LibreOffice writes natively, and a sheet of it can be read by hand. The cost is that a spreadsheet has no place to put a table's metadata. This format puts it on a hidden sheet, so a reader that knows nothing of the convention still sees the data, and one that does gets the types back. Both packaged (`.ods`) and flat (`.fods`) OpenDocument files are written and read. ## File layout A file holds one or more **data sheets** and exactly one **metadata sheet** named `__datatypes_meta__`. A data sheet is ordinary. Its first row holds the column headers and each following row holds one row of the table. A `datetime` is written in ISO 8601 form, a `duration` as a clock, and everything else as its natural representation. Nothing on a data sheet is specific to this format, which is the point: opened in a spreadsheet application it looks like a table. A missing number (`NaN`) is an empty cell. An infinite number is the text `Inf` or `-Inf`: the format allows the numbers `INF` and `-INF`, but a spreadsheet application loads a non-finite number as 0. A reader takes the text `Inf`, `-Inf` and `NaN` in a numeric column back as those numbers, and an error cell as a missing value. For a `timetable` the first column is the row times, headed by the row dimension name. For a `table` with row names the first column is those names, headed likewise. The metadata sheet is excluded from every sheet enumeration the package performs, so a caller asking for "the first sheet" gets the first sheet of data. ## The metadata sheet A grid of text cells, read top to bottom. It has two parts: an optional **preamble** of keyword lines, then one **section** per data sheet. ### Sections A section opens with a marker row whose first cell is `## Sheet: `, naming the data sheet it describes. A file holding a single table may omit the markers, in which case the whole grid is one unnamed section describing the first data sheet alone; any other sheet in the file has no metadata. The marker is followed by a comment row carrying four counts: ``` # varTypes 1 rows; varNames 1 rows; varDescriptions 1 rows; varUnits 1 rows. ``` and then that many rows of each block, in that order, one column per variable: * **types**, always present; * **descriptions**, omitted when no variable has one; * **units**, omitted when no variable has one. Variable **names** live on the data sheet, in its header row. The `varNames` count records how many rows of that sheet they occupy, which is more than one only for nested variables. ### The type block One cell per variable, holding the Octave class name. A class that carries a display format writes it after a vertical bar: ``` datetime|dd-MMM-uuuu HH:mm:ss duration|h double cell ``` The first cell of the type row describes the label column rather than a variable: * `RowNames` for a table's row names; * `RowTimes||` for a timetable's row times; * empty when the sheet has neither. A nested variable writes one type row per level of nesting, reading down the column, which is why `varTypes` may count more than one row: ``` # varTypes 2 rows; varNames 2 rows; varDescriptions 0 rows; varUnits 0 rows. double | table | table | double | double ``` ### Worked example A timetable of three rows and two variables, one of them carrying a unit and a description, with an event table attached: ``` ## Events crossref: | Sheet1 | Sheet1_Events | EventLabels | | ## Sheet: Sheet1 # varTypes 1 rows; varNames 1 rows; varDescriptions 1 rows; varUnits 1 rows. RowTimes|datetime|dd-MMM-uuuu HH:mm:ss | double | cell | a value | | kg | ## Sheet: Sheet1_Events # varTypes 1 rows; varNames 1 rows; varDescriptions 0 rows; varUnits 0 rows. RowTimes|datetime|dd-MMM-uuuu HH:mm:ss | cell ``` ## Event tables An event table is a timetable of events that attaches to another timetable. It is written as **an ordinary data sheet with a section of its own**, and a line in the preamble records the attachment. ``` ## Events crossref: | Sales 2024 | Sales 2024 Events | Labels | Lengths | Ends ``` The cells are, in order: the keyword, the name of the sheet that carries the timetable, the name of the sheet that carries its event table, and the three variable designations of the event table. A designation that is not set is written as an empty cell. The names occupy separate cells rather than one string because a sheet name may contain spaces. The designations are on this line because no other part of the format records them: the metadata sheet holds types, names, descriptions and units, and an event table's three designations are none of those. Without them the event table returns with its designations cleared, which is a silent loss. One line per timetable that carries events. A timetable without events writes no line, so a file with no events has no preamble at all. An attached event table with zero rows is a real state, distinct both from no event table and from a detached timetable, and it survives the file: its sheet and its preamble line are written like any other. ### Rules a reader must follow * **A referenced sheet is consumed, not returned on its own.** It attaches to the timetable that names it and produces no separate result. Returning it twice would put the same event table in two places, and a caller editing one copy would write back a file whose halves disagree. * **One event sheet may be referenced by more than one timetable.** The writer emits one sheet per timetable, but a hand-written file may share one, and it is read as written. Each referring timetable gets its own copy. * **The event table is validated against every timetable it attaches to**, not once per sheet. A sheet valid for one referrer may be invalid for another. The row times of the two must be of the same class. * **A file carrying a preamble must be sectioned.** An unsectioned grid is taken whole, so a preamble line would be read as metadata. Events imply at least two sheets, so this is automatic in practice. * **An unrecognised `## :` preamble line must be skipped**, so that a later addition does not break a reader written against this document. A writer that adds or replaces a data sheet without writing its metadata, as `writetable` does, must leave the metadata true to the sheets: a replaced sheet loses its section and every preamble line naming it, and a sheet added to a file whose grid is unsectioned first gives that grid the marker of the sheet it describes. A reader should refuse a reference naming a sheet the file does not hold, a preamble line whose referring sheet has no section, a referenced sheet whose row times are of a different class than the referring timetable's, and a referenced sheet that carries a reference of its own. An event table cannot carry an event table. ### Choosing a sheet Asked for no particular sheet, a table reader returns the first data sheet and a timetable reader returns the first data sheet that is not named as an event table by a preamble line, so that a file holding one timetable and its events reads with no argument. A sheet asked for by number indexes every data sheet, event sheets included, so that one number means one sheet whichever reader is used and matches what a spreadsheet application shows. Only the default does the excluding. An event sheet asked for by name comes back as an event table, which is what it is. ## Workbooks of several tables A file may hold any number of tables and timetables, each on its own data sheet with its own section. Nothing in the file marks it as a workbook rather than a single table: the sheets, their names and their order are the whole of it. The package writes and reads such a file in two forms. * **A scalar structure**, through `struct2ods` and `ods2struct`: one field per sheet, in sheet order. A sheet name that is not a valid field name, such as `Sales 2024`, is held on the field's table as the custom property `ActualSheetName`, which `struct2ods` writes as the sheet name. The property itself is not stored in the file. * **A `dictionary`**, through `writedictionary` and `readdictionary` with `'FileType', 'ods'` or a `.ods` or `.fods` extension: one entry per sheet, the key the sheet name exactly as written and the value its table or timetable, in sheet order. The keys must be text and every value a table or a timetable. A file of tables alone, or of timetables alone, reads back with that value type; a mix of the two reads back as `cell` values. Both forms go through the same writer and reader, so everything above holds for each sheet: it keeps its types, formats, units and descriptions, and a timetable's event table goes on a sheet of its own, named `_Events`, which is consumed on reading and becomes no field or key of its own. A sheet name must be non-empty, unique, and free of the characters `[ ] * ? : \ /`. A field or key that breaks this is refused with `struct2ods`'s error, before anything is written. ## Why the preamble is at the top Anything appended to the metadata grid is absorbed into the last section and corrupts it, silently in some shapes and with an error in others, because a section runs from its marker to the next marker or to the end of the grid. A row above the first marker belongs to no section and is invisible to a reader that predates the feature. That is the whole reason for the position. Four alternatives were tried and rejected: extra rows at the end of the referring sheet's own section, which is the same corruption; a fifth field in the counts row, which changes the header of every file the package writes and breaks old readers on files that carry no events; a second hidden sheet, which readers that predate it count as data; and a naming convention such as `Foo` and `Foo_Events`, which breaks the moment a user renames a sheet, and being renameable in a spreadsheet application is half the point of the format. ## Stability The format is versioned with the package. The preamble mechanism is designed so that a new keyword can be added without breaking a reader written against this document, provided that reader skips keywords it does not recognise. Questions and corrections: https://github.com/pr0m1th3as/datatypes/issues pr0m1th3as-datatypes-9450b29/doc/datatypes.png000066400000000000000000001103051527370255000211470ustar00rootroot00000000000000‰PNG  IHDRÈÁŠWÿVØzTXtRaw profile type exifxœmPÛ Ã ügŠŽ€‡„D꿘桞„}`s~¸ýó>Ü«�Õ; KŠ9FhÖÌ$ù�µ[òÚmÇ6ctw›Ïxx×Ì–?ßé'0\ ¡lh½ÊÔO!+ ­£Æ« R›*Zg)ñõÃfbNËm´:gžHçYê[Rûü¼ë‚­Ö€:ÂŽw!ñ°"it&í)ð K¢Hô‡ Ê^9d®A”’2ç"±ÅË�p_ÝeÈ{0X"„iCCPICC profilexœ}‘=HÃ@Å_ÓŠ"U;ˆ8d¨NDEÄI«P„ ¡VhÕÁäÒ/hÒ�¤¸8 ®?«.κ:¸ ‚àˆ»à¤è"%þ/)´ˆñà¸ïî=îÞB½Ì4+4hºm¦q1“];_B/‚cFf–1'IIøŽ¯{øzãYþçþ=jÎb@@$že†ioOmÚç}â+Ê*ñ9ñ¨I$~äºâñç‚ËÏŒ˜éÔþA×/‘K!W Œ ¨@ƒìúÁÿàw·V~bÜK Ç�ŽÇù:w�FÍq¾�§qŸ�+½å¯Ô�éOÒk--zôm×-MÙ.w€�'C6eW ÒòyàýŒ¾) ôßÝk^oÍ}œ>iê*y#Ê^÷ywW{oÿžiö÷YÉr�×»hÔðiTXtXML:com.adobe.xmp 2.0 Linux 1744600475516750 2.10.38 image/png Andreas Bertsatos 1 GIMP 2.10 2025:04:14T06:14:33+03:00 2025:04:14T06:14:33+03:00 gimp:docid:gimp:c36f85fe-7e32-4501-97e3-7d47d2499f44 saved / xmp.iid:3f8dfdc4-0e75-4b69-9016-0456f95b7341 Gimp 2.10 (Linux) 2025-04-14T06:14:35+03:00 xmp.iid:a80de4dc-0b88-42bf-96d3-794dccb8d9ca xmp.did:fef46d76-114e-4380-bbc6-ea93d6100512 ð1é‚bKGDùC» pHYsÃÃÇo¨dtIMEé#ÖbÓ¼tEXtAuthorAndreas Bertsatos"ìÇPtEXtCopyrightCC BY-SA 4.0ωޔ IDATxÚì�w˜\Õ‘öç¦ÎÝÓ“•³„r"H�B$!Àì…õÚÆ6Æa½k{?Xg{mï.Ø»ÞuÄØ$“ Æœ„E"(#PΚÑh4©§s¸á|Üîži�$Öžzž~¤¹Ý}ûÞsO�ªz«ê=BJ)é—“§�ÁÎ¥±óyl˧ç� UA5tTËê "4£Ðþ�ˆÖ?ïB¤ÄÊ&HîXIrÏf¶ì>@g{#V²;+¢ôYž0Z  %PÇÌQCð NÅ„Y†NBQu¢Lߣ"ú-ȉ‹•é$Û´ŸC«ŸaÿK?ÇN§Ø‘«B ‰À!êÅPôŠÁ!°ÓdÚ¶Óšu@‚D%¤Z övàr.þ¡‰sðÖ Gé·,ý òÒ`X&GÖü�MK~ÍÁ}›Éçs˜Ž@Õ¼¨þ¦M˜ÂЋã8 ÕãGU54]GÓ4¤#1͹\–\ìÍ/ÿžUk^Åɶbå“4‰Öpá9s²øSxëG!µÐûäÿBpa‘Ú³‘=ü)ëWü‘N'àÚ )™¾è³Œœr.uã&¡"ض�ã8¨ªŠª(HÀqlÛv\T±=[Ù¹} kùÒ±�ÒaX(ÉôE_bÀ…ïÃ[7²üûä=k3pòYöÿæ?XùÂïéH&°¥@óD¨w—ßz;ÁêzPÛFJ‰ Åu«¤«H‰ * ªªÇqp)%0sVþßxB•ýϧ_AάÄ6,eí/¾Â–æö"J+¹øÓ?fÜysѼ~„H)Ëq†;ɲ¦Ic{’l.G…O'èó¢k*¦#‰§³$²&Õ‘µ!?ºª TµË”^B÷üŽM㎭<óÍë‹q�Å”³/â¢O݉Q5°ÿ!�fQú‡Àµñ7_fËÝÿØM9`öGîdì싺EQÊ–ƒbà]Š=’9“L.OU8@mE˜°ßKÈï£:d`u”°ßCG2M:_@Bù\B(å˜Äq×å2< ›8•«¿õ[TO%ªª±mÃk¬¿ûŸ0“ýϪ_AÎ@Ìqxõ Öµšeå˜ÿÙŸ2íÒ«QT!Wú.ÓR€iÙ¤ry|�hÀ‹¡i(Ь+Bà34*C>l[’5)�Hœ£Ï×Í¢ ;‘E_¹ÏUF o¬_Eã£ßÆÎ&úX¿‚œ>1“lº÷ÖíÞ_>6xúuŒ;o.ª¦Wz�] ®-Û!(89Û"g™X¦…¦i蚊ª©¨ª lÓÆ±-á¾¶”Ø8¤ó9’¹,¦c!MÓPUµÀÛ¶�¢( ;ž9ÿoòè<ÿÒ34¿ò»þYÛ¯ §É³rlÚ_y‚76oF)–{ÕËüOÞ�(*GiòJGbÙ6¶c“³-Ú²IšÒ�$ 9$`Ù7€W5Í=æØØ¶ƒe»q‹®*HÙ‡S´f“˜8(Ý‚þR.E�¦ëL�9¡Áç�.ÀŸüwbÛWöÏÜ~9õ’lØÆò'¾EÞî º¯¼ã~ü¡ð±®�U“.ähIÇ(8.T««*>C#—ϓΛXÒé p\LJ’9¯®¡ …ˆá#døÈ˜y:2I,ÇæxH‰nx¸ñß~^þ;�³ØùÌ/±²ÉþÙÛ¯ §NœB†æßÿˆC oùXíäE ›4µ½Ú–UÌs¸+z^Z´æ’¨ŠJm B½?BÔë£2àÇP‡;â´'³¤ò&yÛ!oIÚ“yb‰!¯ŽGWP„$dx©ñ…¨ò…H›yâ¹ –e¹r7wË,ºt^ŸŸs?|g)ôaûKéØôRÿìíW�Sh=ölæ¥Õ¯”]+€iW~Ýp]+)ò–MÖ´° $Í4סj*š®ãÑt$’¶LœL!�¢¨hÝÝ-˱m„¢pÞ×¢ttö?õÌt¬÷+È©ˆ=v.˜l6W>扎bääéØÒ!•+p$ž¢±#ESGŠÃ±é,¦!Ý‹Q,&,!Uš¢Pô1(¦Â¯£Hi™ø4…ºh�AÕ<†[õ+]; $hŠ‚H!ˆgó´ÄS4w¦ˆ¥säM§ˆj©šÆ5_{ ø»°fO+ñ�Ëúgð)–¿É~�Ì‘½ìxõI$¾ò±‹ný¨© -�iòyCUŠB&gÓ‘Êàó„ÃAE)[ÇqŠ“Xøx LÛq-…ª ªn-Vi-¸E�Hð¨*uþé¼IG2G*›Ç2m·†KUðè!Ñ€�„jêË×k9°gíËDg^Žêñ÷Ïä~ Òw_ÿZÒžòߪQ�?!kÚîHaLj#~†ÖV0¬6Â�ê0AC#“ΑÌäA(hª çºÈS šUñz<„ü>ü^](R š¦¢k®ûdÛ¶ K‰ŠF*kÓ™Ì`¨ yõQò!‡#±$íéyÓ 3ášÛË×ݸù�Xéþ`½_AúPì|ší[¶"”®[šÈÀ±èLç°,‡êH€ªp€�ßGÈç#ôS_Äï1ˆ§²dMŠ¥!]%¤$Bq­Ž‚îûn¥o÷á–8Ä3yé •A?ƒ*#TG‚T†BÔU„\Å£iÄâò¦ƒåâI€B*NlK°Þ¯ }(f*NÓ�­ˆ1‰MÁ4Ifrø}A·fJJ»XwåóT˜¶M&gb{<„®ËuTCJPUu•Ŷmìbi¼¢EÅ™º¢PàóºMVš¢¡ª C§2èǶ2E*/_ws>@óÏõÏâ~éC ’Mc'ö86cñ‡±L×íñêCG –eaZ&¶c£j¾b m™6¶i•D/v˜¦‰iºy]×Ë¥ñ–eaYŽã;UP¦í «ºê&5]GÕT¤ãÆ7†®�˜¶�ª)L<ÿ"„R´"BÁ<¸'ŸéŸÉý ÒG¤uª´z>ål$jqÂ:Òér�÷_)%9Ó,Z‰,~¦T…«tsÙºWç–ܰîuVB^xþZ�´»Ù%�ü …r—û›o¾É÷¿û vïÜ� GÓ;èv’|¬©&÷£X}…`À4 «ï[ÕtMÅï5Hfò$2y"^O1Îp!ضŽNî¹ÿ®¾þø Eåò÷£'IJeñ%…)½/¥dÈÐ!ø}^̼$•Ï“5- MGÏåV 5š…W,±\åì‘qЙÌb&cøêß»cîH·Z9“ϰ/ÞÀþ# ´'ÛºEc¿'@M¤–IÇPå«,»­ѯ §SMd¬žƒnè‰tŠ'¸—-;vñ™/ÜΆկ²kÛV&NšÈEó/åûßûë__G>“æŽ/~žgžy†­[·qÙe—1~üxî¾ûn¢Ñ(UUU,X°€ûî»Û¶ùð‡?L.—ãg?ûuuuL�:˲x衇ø·û7¢Ñ*vìÚÅ/~ü¿D>î¸ãvþã;ßÁq$ç^p!£&LF�ª`™–[Yœ48’u°2©÷äX·¤;ØÓ´ŸæÆ ¨Mh?ø:ª®0Ùœ­{bŠ÷‘’ë¤É ÿ† 9‡ô˜Y ¯Æèš¡hgˆÈâo/ÒKÑ“‚W_}•óçñÑ�}‚#ñѺA È™<ô𣌘4ƒ›n¹•¡Ã†ñÙOÝJsÓ!~õ«‡X¼x1�=öW_}5ãÇ�g„ ¬^½šÕ«W³`ÁÇaÅŠL›6 ÇqøÜç>WîBlmmEJ‡€ÇàO<ʵ÷! ÌáXŠé³ærðà~ûäoù—GEÀ‡®*ÇX¨÷¢XŽE[ª“Ç_{ŒàÎW™çÄ©w,t£˜wR‹À_·Gâ*ºŸ‚Bö‡îq'ºa¸f¾ºº Ó#0 94Ã…^ëë”›š"‘0—NJžþyü~?sçÎEAmmmúBPUUUžµµµ]D ¦‰# ¨GU52–I¤¶’¨ÇOHó”aä�–aäîÞbÔ£ zÏ\©ICì0÷/û7ì_ÎL)�@ܲhv93Î[Œ¿~8^ÝèÕ­-)J÷…­ø nüš‹6ì õÚÃXGÖb8سŒ-M›È^ò9.wÁ)u;ÿæ`^íPt½ç€>}×í]Ì‘8ŽD Ýëæ(4 Ñ£M¶;„«ª*7ß|3‹/¦¢¢â¸ÐîÑÇK(—@)¾Üv\[ÚåL}w¢ˆÞŒ}ØïE Uœ·T²¥iÛÿÝ÷2!)°¤$9‹C…K®ÿ"UCÇõªG+ÉñÞ+ݯwÈ8ªnüU7ý¡‡p„¨\ ë÷_å�+#oú-H_‰Q7§ÿµd LË*? ŸîA+dÉšª�Zœ°.’Ôå–h|EA/v–8³Jç*Y“R�Þý¸lé`Ú6©|U(xuMÓÅß(}gëÊ—Ëî„À[yzÙN$’—w­D}ñ‡LH·"‹aóê�—rÉUŸd~ T¼W§œÏy+ëñVÊQþ¶¢ × ¦êï~Lv÷zÒ+þ—骎¹ê–ÆÛ¸|á§ÐU½_ANVT ÙC]“Ü2Ý.À"ËHi²{t��á%–M¡â–ºëªŠ‚ª¨îTéF7ª(*JQyJYs)A+õ©QGJ(Ç)B`éB�t!GÚÌ6¼xU½ØÛ.{Pš¶Úßc¢*ƒÆ¢zC§mü)ym×Zü:Ó¥;}¥bÐrö'¸nÖ¢V!—Í’J§ ‡BoëóvÖ©$¦i�Çñû|øý´h¡s®@²¾Žly�[žá)ÛbÑÂOã7¼ý.ÖIY�@˜#Çõ@{SM›Ø¿íÍÁ"€**|B‰BŽX.K,“§%‘æH"E,“£`Û(ºG½¸U ]Ç[J ¶CÖ”dM›¼åö¹§òYZ3q E%døP=ÎUž<Ý8¹iL�ZÇoÏ‘}ˆçþ‹Å˲¤$µàKL:J9R©4ÉdŠD"Ò9qä¨Ûyò¹œ{ŽxÓÌw¹–sÞGåõ_CñD Á¬�ÏòòªßÐטÓßœ‚¨¾ѱ“{L4igé<Ò„”Ò-,±Ú6Š”DŒÚyµÅin朗5ÆþÃí4ÅRd,EHé`YvÙÒTMÕ@€i™¤òZš:R4´Ç8ØÖIc{’ŽTE¨D½"Þ†ª»…’VW½—¦iäRIÞ|ê?»)»�ªi NÛØµ$ÛY÷çgD®­Ø÷¥�›ÿF�Ÿå–È7é'DiËYÆÑ¥E£çv(Ýÿ.}W9�KÑ²£VúW1¼Ô|ôçhµg£H‡Âª{øý†çºJ…ú]¬w'‘ Pøw$�ò±÷~•‰sæ£û|eWÈ,ÒŠ¦râ‰,š¢ ùÐU�mKÒ“D:‹íHU…ñª®‹&qM)ŶE2W 5ž!™ÉâÑT¼šK”H¥ˆ§•AjÕxu QŒSJ½ð%v•=o¬îq¡Ñ—¡O%iÎÌsÿ‹ð�ؾṟ{ú-œ?y®kí�òr¹v‘#_(Ý$‹Xgg�X¤»’ô s)ÈÊïçrù¢Ëê�J¥J¡(ø|>4MUÇ9«e3TûVÜË‘áS­ëW�w õÏÜ‹®âµW–aš]èdçºL¾ ËeÅX"–Ì¢)‚�U^õ>ŽCÄ‘tdr´Å3ÄÒyjƒÞž&¹X;eÛ’öd–d:KUÀGeć¡ë8ŽM*“§5‘¡£3EÐ0ðjJ¯A­cÛ¬|ð;ÝâÁŒ9óOÄ»eïë,Þó,ZñÚ”³9Öb!p¤C,ÖI"‘èÕÅÉLró¤~ß²mbñD·1é$ RYYAø¼E 2«¨l¿^ò>}ã·ðhž~ë݈ Ã/ù~owR°ñO�’K§º¸©AÞ’dó&Á¯‹diŲt¯Ç ôãó$Ó9LÇqWûb§¡e¹¼X™¼I2“'òS âóèhŠ‚¡iTÔWEÐT…X*C¾`â8v’lÛ¶Yù§ßae[Êù�óGTž:¿›Ãqê¤-ÕÁò—J¸”³P ¸ôSˆ"„ÛÙ'�»�¬¡ôûø}eäN×4>/~Ÿ—@éå÷–�õö*½gè.2¥*Jù=oñ¹%“Ib±Ò±ÏZ„gÜu8BpMóz–nyÙoAÞ½„GOfÆ”sX¶zJq޵ïZÆÞ×W2ê¼ ]þ\MÅ.Øn¥­ƒS$tsc‰á8xu�x6çÇi]h•mÙHYËÂv$/C+ö¤ÛÅí4ªB2[ ‘É’/˜®–ë¤$�L²öÁ;º®]Ë3øêÛ1"µ§|œléðô†ç¹1Ñ\žpúìÏP­-Þ§I2á®ì!Ÿ‡PØÍ– �“JYx �h4 B9¦ê¹T¬èv}+Ñ ÍÊd2tÄ:Ýq Ñ Èeó´··“J¥ ø|xýÓ.#¿ã)téý·dÏ:¿Ç×oAÞÕÊà 3lñ§={Cžûám$Ú[Žñ•�x¾ìI:ÅÒ‘’¼]¤ ’à ‘¢çF: ¥4ŠçQMUp‰”=ë)Í|ž_õc=®±nìÔθô´ŒS*›dä›Ï”ÿÎÔÌÄ7ñ‚bÓ�/)1t ¯×‹áó¡{¼ds9î»ï>~ò“Ÿ`[6ª¦£é†Ç‹nxÐtÃý»øÝ(¾W<^z-]º”¦¦¦b®Ê=‡¦ƒA CGJÈçò€À¨ŽZ5LiÛÌŠ¯õ»X'#Ñq³¹ôÆ/ •7|éîÿÀÎeqlME�Ι8H¤-±-·1kÙ¤sUОMДè eæ‹�U*%ŠéHrËe^TE™Ûrlò“lÁD×\÷LUœb ¾eÅK¤½^¾¶qÁçßpúiÚ/dÝÎU L6vÅãô‹4eZ"hºînŒ"%áp„Ûnû$ûöíC(¢Ü�Œ‘ò˜üG÷÷á7#êÞ”v¼sD.ý4Õª‡%«#× îw±Þi,¢( œwµ+–Ò¸w[yÀ›¶>˦çÏgÊÂëð(*A¯Ng*�ahT<¨Ò]5³¶Ek"‹eÙÔDCäÈ‘Êçq�<>¡�-äÉÛš&hO¦ñ*aŸE(Å >-b™,‰t–šH¯×@Q¶eÓܰŸå?ùt×ÃR#ÝNå”ÓS˜h:kV>Á?(zÙª��z!RJ¶nÝʲe˘8q"Ç'“Íðð¡pá…s9÷Üóðn  ʉÎT*ÅSO=…eY\½x1Á@€_Þ{/U••ø®Y¼)%Ë—/gç®]jjâ’pˆ]»vrÏ/–SYUÅâ«3`ÀR©4<ø #†%‘Nñ‘�|”hÝ0ô!ó0^âÓ‡Ùа�Y#§õ[�w+žÊA\þ�ßcX­¿GÀ¾ò‘o°ö÷�¢J›ª Ÿ®ÑÒgK' �)b)Zãd29jÂj‚~ª}AªüA žMË&±¤EØã¥2à%Ÿ/p¸=A["G<[ 3[àH"ÑŽU¡"àAWU‚æý{xúk7õX]/ž=�ÑW~ì´�Í›M»¸6×{¤¦Ý‚G7hmmåûßÿ>7ÝtC† ¦P( éwóÍÜpà üüž_ðæ›oÒ|¤™B¡@K[;MMM466ò_ÿõ_TUU1~üx~üãqðàAyä&O™Ì¯{ŒmÛ¶±eËzøaæ^p �3løH>ò‘�2sÆL~ú³ŸÑÐØ@&›åÕW_eì˜ÑÜzë'ñzÝ,zdþÇŠðz��·bŸD^¤¿ϼ[¾Ç‹?ýG²w¼þäàõx™8+ÃÄ’i’ù©‚‰"À£*„ƒ¢¡†¦ *º�#%f‘�ZW4¡bj6 БÊr¨µEˆòÄóë:Õ?^]öLïÛÃï¾tU·ULrÁ”ÉŒ¼é+h�èi‰ä`KSdW!à�qç–­ÀðáÉF£ø¼š�´�ÉdX²d `Ó¦MtÄbø×U´m„b“Ëå8pð øü~’É$…B�1cÆ ëÁPˆl.G[[Ç Ã0 ÆŽ‹eÙìØ¹“eË–ÑÒÒ‚ªª n ree%C† &èÊii·gFF7¬Ä¶ß�ú.7CÕNõ0#{µ=£ß÷Fwœ ѳ/cÞmÿÍò{n§!ÞUd¸âÑo²íÕ?³øËÿCM$H…íÅA **º¦ ŠÛvzì@¥Û¶c—tŸ×ƒ®kø½’é,¦e¡**>¯AØïÅ0tlÓdéý?d÷+¿îq}ó&ŒeôÇïÂw·†¶m›šC[ÑŠþ}Ö;¯?Œ·ù«©©‰\.G"‘À¶mvïÚI6�âŽù"_K&üTFÂTWVâ7<„Â!,¿�Apíµ×0nÌÉ$ªªâóx¨ ‡ðÑpˆ€×Ãó‰A¿�xg ¿Çà©gžáÒ ¨¨¨àW=D4¶­n«îEŽ„Ò"Ö±“D6Eõ»ŒÙÔo~ó›ßìS•°³�l@t샖­Ä÷¯§iÏb7;¸™ØÁÍd›¶Jí‡B'2× †ŽP}g\I|Ç0`à(:¶ü™x¶+ÌűùÙǨ<ŽêÁà ø}ø<®;Ô=HìÚZÍ-;)•¨—ª|UEEWU¼†›Ü—ÇÐI´æéï™ÆµO íb`)æž=‹³n½ ÿ q§u|†Íð 5—Ú&—Ë�µav¶ñÚ÷�lmÄ.$°²í=~Óïó±àü‹©¿öñÛ³¢ï4K&gv÷‚Bñö€§àèf§cÿ>þÂ$Þq©º×çÇçó¹ÖX„ìªþí~ÿ¥Úã IDATUÉ“Øø�)ˆt áÚ×?Î »<Ø„zä–pþ”Q¨#/``Eáñ€áo´Û ã@6†,äȦ³´Ç;)ì\Ά] ˜R-[˜˜©óçM0vï2~Ú,üÓ¯CøjN¯5Ñ ¢ç,fö¸ó÷Æ‹ìüÃ�¬ßÛŠ#´â6vª 3üÙí\ó_ô¢×L!�V�ÖÆ äz”Ç®²Ýô/ŒXð„qæ·3È[yTº�¶dÍhêz]Íeù%{YõOtÒ¿ÝçD7t�nÄ?·Æ^”+G«‰UÈŸ)$°¶=Îó/m&^ð•BP5`8s§ŒE;żÍʧ€¯ áü�³f2¶سr ûöï§£ù€ûI»ÓQö­ØÎÂö;Íù2Jt¤k�N£èájê.º�ʳrÖú?³uõ îX�LµÐž×\JÐ"*'�æTRëzy¼o ø¡È÷„rôvÍŠm3™¥”'to'bŽÿ·¿PpKé-Ë*Ç!†aàõzÐ4½Xµpœ«? K|B "SGزôšÑYôxoþ?Kuý@וz÷Ž?Ц3fîbFž—§ýð–ýæeþ[[ þ´-ʰæÿåüy—#F_qÚ•z ‚š¹71÷ÜkÉw¶’kÜÆÁuKY·ü’ù“¬*‰�«àò[x/J°}ïÛ9WîzÞËD?^,Ñ[üÑý½T*E>Ÿ#›ÍbšÖqË0 B¡ Á`°ÏN”·]Ö:°÷�_cóžNÚº)Ljɳùàg¿Nõà'§GãΆ‡Úac¹ñŸ¾ÍÔÙ]yŠ€†Nƒuy{ûo‘váŒMÅãÇW7ŒèÌË÷¾/`„û 9Gœƒ+±sé÷„B(ŠJî8èC‡±téRV¬XA:�.æ\E9Kq´B477³zõê.+ì8òYn¼ñFÞxc–e¡© >�A8è' úñy=(Š P(ÐÖÖN[[¶e•-y÷{9% ât6²åO?`mCW¬¡éW\û÷œ{ÙûÀ9uëµîeü—3{ñG{ß�­fÅ‹/Àö'ßQŸó)„½} `Ë¡�»Ö¼7,†/ÂænãÛ}BwÞy'–åBÚÙl¶\¢^‚ƒ3™Lù�ÏçI§Ód2™2[Ëž={xâ‰'H¥R8ŽM>Ÿcÿ�¾ó�ï0zÔH‚~•‘0¡`�×O$¥ªº–h4J8DWÛ&•JÓÒÚŠeºÍ I0øîi‘´ã»U‡Ù½ä»l<(÷KxCUœ{ÕÍä=>Z[ÛúÞ××u‚Á`6nù…`ýÒÇËéP&Àë/-a†D½è ¸[§FA~ÛäĹ]{€œ!© W³ÓËÅâ-3‹i™hªV&³›0aMMM´¶´àóyùɯÍ—¾òUn»í6î¾ûnŽ9ÂÇ?þqî¸ãLÓäšk®¡±±‘¦¦&ššš¨¯¯güøñlÛ¶�dz~ýz¦L™L<ž`çÎ�|ï®»øïïßÅÐÁÓÙ¼e/¿ú*~Ÿ�qãÆ1gÎ^}m¿ÿýï9{æ ’ÉW/¾š\.O{kò·—4›ºêwO‹Ôû̲r¤Wþ”�‡üeåP /g_y3Šî)&»úþ•ËåˆÅbež©’Œ�z.ç-ú0F·Í*wgjyõÅçqZwžÑ‰$mÇì» lÖ­ø#f¢ãŒ+kÄÆðtñ;Ík°Û³  áöž—ÊÝ…å¢A!—\r .dñâÅ <˜©S§2uêTV¬XAee%3gÎdذa\pÁ …¶m3súT†J(Å*^²„Ë^Êå—-äá‡"�ìDÁ”)S¹ð¢yìÙ»‡ŠHUUÈt۽ؒ�ïK "±·?ÉšÍÍØ2ìj‘¦3÷úOR7hÈ©›hRÒÙÙI>ŸÇ4Íäl#ÆOaØÈѼúÌÃ4Øáú �#/ßEý•w"ü5gd"Ù–…�‹÷ÙùÚ›÷qdÅ“ ¹òÓgTA,ÛÆðU@úˆ;Ù¥…UÈÓ=Ú, q¢ùvÏ„„ÃárVܲ,~õ«_a‘H„|¾'üjš&étÆ%íóz]2ðbÀ^(Ðuƒ\>O*�&™H‚”†^L\‚áñ�FéÜ´¢|ÎUºÁùÇõ�‘­›øËò7h±ÃåcÕÃ&PY[ßcפ¾~uÏ</ÐS<~†N™]n¾Àkû#6<Ò:3 ÒyÅÊõÙùâ¦ÁŸ»‡lÓ¶3¦m©¿üÓ÷˜Ð¶£Çñí[W–ŸO"‘`ýúõŒ7Žšš<†Áá¦f:DCCC��…ºS7551|øð25@0$ŸË±wÏ^,ËÂÐU …™l–xœ5“ÚÙ×pªÛé�È$ù󚧘ºé·\me{�PH¿ô• >BeeeqQËÑÙÇFŽ…×(_m t)‘r !¨®®¦¦ºÆÍuI°TMeôÈx½^~ŸŸ™çœ,3�ãHlÛdØÐ!üò…ˆDÂÅvÁ¨#Q„ ßÚ·öJ*‘¡S‹4±'ƒH3Elý�¢«À \3˜Ššzr¹Üi™o¥-I.—£³Ó ÆÂá°[w/cfÎcË+(vÛ�ç7¬B:ç´éGrïìÙòú)©™ÚŸò`Ý÷M.«NxÔ;kÝÜ´ƒ#ñVf�šNÀ_‘lmÞÃ+ÛV1pûÎKÂó6èY~ÿk˜©kуáRúèºN¡P žH¢ÙXJçïc»å¥Rán‘�êî•ÚÍUR»ñn>‚eÙÇ\G*“#•é C§¾¶–øï(ÿÞËžJ®6þ¤žC×h´ïf]CWá À¥wñD¢×•ÞeгOêÇ ÃÀ0º:½J P²%"²RoÛ6�@!“gÍcûÊ¿`©ï¥ƒ¤w­$<ø\PôS®Ž™çЋOÒ’rÞ¡u(Ö.I—úÇåK9*#"T@¥±=ÎKw�…_zo剕¼I´Ò¹äû ˆíåB•|X3Šªšá Tc7°iO·ñÚæg™ÛâÒö'-§Ò¶ë L¿Øe@T5*£´¶¶‘ÉfÉÊžP6[A´"L8RqL(\Þðá²å;²‡µs÷ ï¾ã¯Dulbüßò‘Œcá™r‘“$ö.qòÓ²géB×ONºðš²«u ÂaYÄãñ“& Öu�ªªª^´·ý0J-š%ŸvÎuãåßü¬ËŠìßËy¹$øO=ãGrß&Þxå‰þ¼ UAcFÅ;x2¾AãñF¢ù{&[í|š\Ç!²‡w�=¸‘†ûXú¿_dá§¿‹·nÄ[þFÖÌñ«—~Áu±Ýx…ÊGR1Hu@Ë›ä�èÈòT€àFq,‹ã Ü ¼ò= ðÚÁ ^Ÿ�êšjâ�q×ý9‘y!%¹l–`0Œ¢º}A¢ÈÒhK�ªBmmM™Ê$­­íä ^ƒpEE8ŽÄI´Ñyð…òéõ,˜våI?gÍÅòóä÷=�-‡¹ /õCGw%(mv²D/âæG¯°Ùl�ÇCee%���=�|>OGGUUUTÕ $­#s¡Èl¢@¡ù Œ‘—œZ4“àõ‡¾NsÊîuŒ¤”H;ÏÐacŽžÃ䉓 …·nzè� 3Ò1rGö`vÆ[;ü¸îœ”’•ë—ð�Ý/¡µ3¸Ià9vš¿kæA!Œ—ÂZü4à ~�¡SÈç]VÊ£¯ƒ¸t¬Å�‡7p—¸ bš¦’ÏÛL ÇvªŠz¹ÓA@·E[b. lu¶ÒñØ­=î®qÆ�Ì DúFAœæulhé2áž@„êoÝ¢Ø=�îKÉd2$ TU-o«\Ê�”/ºˆpy|~ꇟŞ¢‚4™²»þpJD:M/üšÍÛvöªŠæåœñ£ºøŸ¨3ÍãE1Þ};±ˆ¢�<ûm?·fÿ«~�ršÊo”CËYúÒ®\ø÷åI©j¼šQT„rÐÑ-ÄïV¤(e÷G„.Ü›ï “Í›hªRVâRs— t�}zóòeG äCÓ¾käê±½AÊ2ÊYsÇ>39…|>O¢óضM{{;‘H„ÊÊJ’É$©T Ã0‡Ã=|Òî+‡žÞ…,ÄÆ©aÿˆïÙÀ²ß|)Õ«w$`ì‚�3éìó¨8ë¼ÓZþr ­‘½º“‹¬§«:MÓ·?ÊJ�Ys®é7Г®�Þ+{…P�¢gÎ+�'0-‹T:MD×ËñIésšÖÝœ@zË ²ëY>G£îãœËþ… 'Ø'÷©áXˆŽ†2º°à¦Ï¼ƒ€ó]šé£V_Û¶éìì<æü%«T£u´[6sÞUìÛøª»c“�¼xp,Wu6@m´Ï§DjÏžúþˆ§»”ÃðF™1e2Ó>ý?xüÁò.®§S|^M•£(I 9§¯ÊYH‡aëïa‰YàŠy7×%ﵞž«nžD#‰ÐÑÑAg<‰nøý]01@´¢MQÐ5…ô¦—I¾tgù¦ì™ò>8dbŸÝ£&Í4š<²¦l »o—Õ+‚ã8$‰2)Á;•5kÖ°víZ¾öµ¯•ϋŎ{¾TÊe\�D"Ç<Mï‰Xål2í}¿bÚí¯«ÞLzCûª«ª]Vût†ÖÖvR© ‘H]ב¹í¿º¬®¾™¼”Ü]7�/]| ~£oé£4a›ÎzNh½I$tvv–Q,EQhnnfß¾}üèG?bΜ9ø|>êëë) 477ãóùÈf³ø|>ªªªhnnfÒ¤I̘1£lZZZˆÇãhš»5@uu5†a`Û6---,Ë"—ËQUUõöÞqþÔ¸¡‘¸àý_dä%ï#TYÍ{Q Ýàª9‹Ø=d ΋?GißtZ~WhÛÀÁ_}sÖ§©˜q)�£6Ô<šµäx0²¢(TWW¡ëÉdŠl6K<™À·í9´ƒÏ£ØÙÊñØÀü¿E·S} vÛR€Vޝ}ík´··“ËåØ¿?†a�F‰F£x½^jjjˆF£†A"‘ફ®bÉ’%ÚÚÚ0 ƒÊÊJ6lØÀw¾ór¹étšmÛ¶ñ™Ï|†D"Áï~÷;þð7C¾dÉ–,Y‚išÜ|óÍ,]ºôÌN¾HSßÿ‰÷¬rt—ÑCÇQõÁo£Õž}Zw°�¨«Jòž²ví³´w¶a9ö1V¤7eåŠ`· °¢"‚Ï£ 4o'²ä‹ûþÐC9²H} ·]óeêç¦ï„#J˲صkË–-cîܹLš4©\‡QUµ\ŸÓ ^´hõõõ àÂÈ¥Ï;ŽSN6 !1bƒ büøñ¼öÚk\sÍ5Ü{ï½üâ¿`ìØ±LŸ>½Ïû�ß%ºÀÿ xø?ð-¶üá'D÷ÿyÚ~Y‚�cèŠа:ÈŽ!²¿z(Œ9—¡µƒŽ ô”þ¨ý0Ë6¿Æe¹vÚ÷¼@…}¬«h)*[ι…>÷j‚§�èB;ˆë]*++¹÷Þ{yñÅùáÈäÉ“ùüç?�¢Ì"hšfy[–U¦á´,«ë`©9ª{PÞ}ï‡Ò ÓµRÞQàõ.'q! Ù¤¿¡ûùkax˜ø¾Ï³ö¹j¾ùhŸ·ÀFÒ¨è¶É@º�A½�†ýK¼ÏÁ»îçüÐñ¬A]ÍHFÔ�AW f�mû^g÷¾W8G³™¨+,Àå¬8ê·š“†ºiTœ{מuÁ)?M*µÞ­Iã-§U"‘ ½½�÷¿ÿýÔ×׳dÉÚÛÛB”›œ8@ @Qb±¦i‹Å—Ïëõ–•ã펚š®»î:V­ZE0dË–-œwÞy'ðÀ�ÇEy e)ÂŒÒÆ‰íÀÞ»œÔÁ�¼Þb3Ú`8û³=ðס$Bpö%7ÑÒ´«}cŸžûM$»‡_Ĺ&—MñÀó?äCñ}Ç$, ¡àP²�Ø‚Ll�=]ï_€`®�nÅ0Ç<-ö ƒƒs?ÉâiWöOÏØ9¹˜´ŸûOl­-*^ÿOÿîîÄb1r¹•••477³k×.TUeĈåÊÛB¡ÀæÍ›ikk£ººš™3g’J¥X³f ³gÏÆï÷‡1M“^x�T*…Çã!�N3zôh<`æÌ™:tˆT*Å¥—^J.—cõjwëãx€E‹qýõ×s¿þþ¿ôHNÕ+1FW©4ƒðÀ± Õ Õu�,¡äp:öpøðA¬æ Èl‚U±±HánhÑrÌ?o žóÿ™¿&‘ŽCËo¾ƒÙørŸ Wû£#Èžÿ1æ�Ÿ]ÎlgÌ/ï^‹oË2r_a¬ca—aò�$\žäF!±+ÇÒ1êb&LœÃȪ!§uÌ4¡ûA©é±ÐÚ¶…~TõŽ‚aÆ1lذ^WþRŒQ’ªªªògKß×u�E‹ÑÒÒBee%ªª–ËHJß;vl9–ihh`РAe²°‰'öúÐ�–f'Jó^ðîߎGÛêîû'÷!:¶Ë‚ê@ÊÒq pK–ŠÎfÜö‘z5þºD( µ×‰†‡ã('aIlà�šÉÜö¾¯Sªêa£ýº—ËÇÏ%7ú\b™ÛX¾ù9:¶¼Èáì>!­b5®ÃÑõÖ`ãnÕ–?–“ÇÍfî¤Ë]? qÇ®Ó>fRJYXñž|µ±\jâ WqÍ'¿ÜÂTUUõ(M׃[,!©ªª*×ý' Òé4š¦QUUÕƒ!½¥¥…|>O(êâ]ýÜÓìÛðJŸÊ€ÑS¹èºà¯U¤mÑú»»(|ñ]L‡�µS˜¶øË ¬8±]vMÇâP¼™G‘ÈÇiloáB+Ý¥XRÒ®z8à‹0°²ŽŠ`”Q5CNºT½Ï‚tuÐ<Ê~Lé‚ZÊ),—è°—â‘\.‡ªªåÜJ[WÕc,Ó1¦¸�WÃb΢ò×,BÕðŽžE¡á¥wÌ-v(:г¯ý*µá‡ºuEcxt0パHw7ߣ !ÌâŒX‰·Ueà,έÿ1¯vw0*dSÄÚŽ­>>c`¡PèQ†þ–“Î0ʸ¦i–³ç¹\MÓ‡Ãx<žw ãrYÚíë³ÁðWÔpéÍŸEÓ�ã*÷Ñn§¢('tÝ%j£’â÷VæÿNÚzëÙ·m» ›¿@xú|‚Ä ß=áßܤ©TÌ»í)Go Šªüß�ÊÝD¡æG|a9H7siZ÷¿¥ˆÅbÄãñz …EJ—�¸ªªŠÊÊJ¼^o�ú�-‚ŒÇÚ‰·ì³Á¨4 Ÿ¿wt$—Ë•ag]×™:u*·Þz+>ø ;w¾=7×Ò¥KËßݾ}û1ïoß¾½ÇùßêõÁöná>ÿùÏ£ë:ŸûÜçÈf³o{Máiã›ò¡Œ;$Τ™=jK¢—B޹€ÀƇIÜCo<÷c&÷ž…UU•P(t¤;S¢ßï'ôºÂ R©~¿ÿ„úM–ýúÇ}6ãÎ[Èô /{ËÏŒ9’T*ÅôéÓq‡]»v±téRjkkùÖ·¾Å•W^Ùk¾Æ²,6mÚDm­ë³¯]»–ñãÇcæÍ›Wn;.�ïÞ½Ç2äX$§­­�Çœ±cÇòì³Ïò�o|£G óñ$4c!ÙÍ�¼¥«%€U¾Ì�yõ1�P Ȫ1Œ«×Y{°« 3ÑÑÚ+1µ¿ÿÝ%ÒŽGçn?H>Ÿ?¡{`ÛäIv4–^30âÄØ[.\ȃ>X6oÞÌ·¿ým¾ò•¯pÖYg1zôèc¾“ÍfYµjÿ÷�išlÚ´‰\.×c3f Ë–-ëárÝyç�Üÿý<ñÄÔÔ¼u)ÅæÍ›¸ë®»¸öÚkyóÍ7™?þÛ»¿UÐÍÅl\þ–�yrâå'”ÿ5Iy¹Þ †Í˜Û£A~ÅÓ÷!s©÷ÜE›…<_}¶G‡ÙɈ7¥~Ȉþ¼ã8(ŠBEEsçÎå«_ý*›6mâá‡îõóMMM<õÔS\}õÕLš4‰—_~™T*õ¶h_)fy»¶Û¶yã�7˜;w.sæÌaÖ¬Y<õÔS'|?µ×ÿ+úà‹ŽûþjO€ëÏ^Äߢôð´‘W1o¬ƒZŒE�íìÛ¹í=wÑ­‡’élí³ó]ñá“KΚ5‹sÎ9‡Ç{ŒxüXÒåËÝÕyÊ”)̘1ƒµkמPÜr¢’Ífyã�7=z4‘H„}èC<ýôÓ´¶žØ MÇ7¶wº$ ÐϺš ¤_A„ª1pÖßá5ºüÌ]kþ‚hÙýž¹à†ÝÛxå·÷ôÙùΚ}ªvr4A†apÙe—‘ɸ;¾WÝÿý|⟠�2nœË»dÉ’¾[0Z[yøá‡™>}:º®söÙg�ÇÙºuë Ÿ#<½ww,§zÇòßÝO[cßY3M÷R;hXŸœ«Teн3RJÉÖ­[) L˜0¡||Þ¼yÜu×]lß¾�Ù³gŸÔïڶ̓>ÈŒ31Â�£***3f +W®ä–[n9a@%8m¹O—+~’ƺñ'EþüWgA„dܼ`bMGº«k!gݒǰ­Â)cx÷xò‘�”Â@ ÀĉyôÑG�qùÞÒ‚ �>`VR(M&�˜Êß²ßDG1þÊ/býé‡losa×tÇaÖJ}}=uu]þúæÍ›Ù¹s';wîä·¿ým¯ßýÿì½w|\Õ™ÿÿ¾eºfFÒ¨Ër‘-÷.L 5à–JpI`I/$»›ý¦ò# � d!Ô,¡„°Mï8¸bƒÁM²U-É#i4½Ýr~Œf¤±$7,' >/îËèÞ™{gΜÏyúçùùÏNuuõa=7SX¶lÙ°¯yì±Çøüç??¬Ú:Ü(9çZöìx „@µz©ô”È~¶s(›ÅÔÏþ˜øªÿÁï�6lD¢1ž]ñwj'naÞé— ±>†¡íÙËοew` [©Š­)daй|Ô[R�ª~t’kÓ4Yµj~¿Ÿ_üâyú~¶¶þÖ[oÅëõæIÉmÛ¶qã�7²fÍšak\V­[¹r%'œpBžŠ•+V¬`Û¶m´¶¶2qâăº§ðAÝ94º<|zö)XË1€ÐP)ž@ý…ßC{ï^\Ó™©£Ðи›xç Lš:÷”Å8+¦v“!í[× 7þƒÍ�ÍDLkž7å´±{ð-ù²¦õ�?³ºglýÑÙÒâ,kËÊ•+ùÃþÀ9çœÃ…^˜{] `õêÕœvÚi\{íµCJöìÙÃ�7ÞÈC=Ä\pˆ%Å™±sçNÖ¬YÃo~ó¾ò•¯ ¹îóùX¶l;wî&�Q� @$gÖÅßç3E+Ø»éÞj/Â@¦#¦Ð±ñ¼›70m¬iâT�›’jGR”Ž Ûq IDAT<"/˜†�¦¥Z=dÏŽ D>|–æx)`ÍífÕö(õÓkp,ú²'ãq;f1ã¶=Ì›ÿØB{ÔΡ&‡ÊŠzÀ¼«‘Æ_ÿúWþú׿æ�»âŠ+¸îºë()PGšššhiiá¿øÅ°êMUU×^{-o¼ñmmmyÅe;V­ZÀÙgŸ=¼bÚ4&L˜À¦M›8óÌ3÷›æ“§ZÇ¡$³²¬ÈS/¤²æÎÚò(»Þ}—†˜„ ;ï4¥°5?ÇŠá,g�7„ÅaÃðÔ ¬V�@DèÑ.´HkCEÈq?BÈDͲ<‰�‹&¤˜pÜUH•sA°$›æ|•Ç4ÒûÚ/yµÙw4þ‡>fÍš• ôUVV2uêTN>ùdfΜ‰ËåÊ“2---Ì›7�úúúIÔ.¸àš››iii n·›3f ûþH$Bcc#Ë—/Ï}¦}Guu5'Ÿ|2ï¾û.š¦4@2YvF¥O4@$q˜» wzã›lÞö!Z4Ds¼$‰,'·Ìì-öYý ”¸ @‰2k�š njE“ŽCÔœˆ¤ì?«W„[H¬½�ßK“§/ËŠÊ%ß»ñØö8ÂÆûx§éTYAÑÀaqb·¹ðºJ°**‹Ç¤¶f_ 2Y¾)XŠ'±pnÃÿ³v­ Ù¸•õ±Zº ˜˜BÂRÞF$!P%$ I‚1ö+L= kÍ™HÎbPŽíQòŒÃyâq¶ùKþþ¾8*’äã> ìn:!"é0š–@’ú‹Ã$ÅÔ¹¢þx]¾c98¤(`s£Ôœˆ»æD –Xl�hRÜOO_ˆ®ˆYƒZ€Ë*1¡ØžjL‹¹|8*ÿã8|ØNüõ]?`½¿ì€ùgµwø?³(d¥Ÿ,ÚbÉ�Ñ$ô}»�ä°ñb+†òb(„I‰0)ÙW‹Ë´ IF>B;¾äª`ÂÉWÓø÷G jû�oœùÅïCÁa›Õ˪-ÿËÔ1 >ÅS£ë®�äLcHÅ’È–þF•Gv‚åª9L«±0F’åü:6o¤m|زöñ]?Vþ<Éæ¡jîé8díØ*Åá°{yg÷ ÒZò@þ¯ ËøÓ˜36}l�îVDH±ñážwŽžÔÒDBq?¡¸Ÿ`ÜO(ÞM*ƒQ¤åV?v¿�beìô¥li{…¸1¼Û7Ò׋§Èwl�”…£:xaçÃŒ-ž‚ÏSuDï�ÒãÄQö†šhðo¤µw'þPB-¢¤|â ˜JøÛ7àqyS4�‰%s¨-�C�³»¥àˆÔùvä_zôìà™‡ÿL<9|a•$É\zÝÍCw©ƒíñ ¹?†a ªêˆ)6Y0UUsé$ƒ;öf>�”ãØ‰íeŸK–åa£õÙºöì{³„}“ÖrÿÆ?L¸ k‚«ŒÏÎZ†"ô½6ßË»�¯²³{3I‹“X:�ÝæFQ¬ŒÜ¸:sÞÉd‹¬âQlTÚJ©¯;•êâÉé3},s Da gU· ›B€)¤¼ž}Ò0 $‘HpÍ5×`·ÛêˆD"¬]»»ÝÎc�=6üN˜Jñïÿþï,\¸0W—!„r/›ÍÆyç�Çí·ßζmÛ†€aùòåûý,¿üå/‡´°knnæOú]t6›�ÓN;�o}ë[Ü{ï½ôôôìwþé8šypvÜÝ[yêÝ»ˆ§¯Ý›À¤;ÔÎÊwÿÌÍ/ü�÷c{H:J@uârúúÁÁ~Ô¨~ðK NG1›‡„jc·æ±mOñÐ;¿fOï. óðìRõãIu¢Ì¹šsËÖ¢wmCaмÛSGwÚ1l’£,ËÔÕÕqÅWäÎ…Ãažzê)æÏŸÏ¬Yù�:EañâÅ|ãßàšk®áÔSOÍÕ`dÇk¯½Æ½÷ÞËO<1„BÕívç’MÓ$ ò«_ýŠG}”ûï¿?/}$[/rùå—+)*++óÔ‰††–-[Foo/³fÍbùòå†AWW·Ür '�tR^ÞØ¾£5ØJL;86—³„ötŒ×ü–óf�Ê¢ºƒþ�é(ol„Áv4TJËgæXú%IB‘„œ=~)E%C²¯% ÒîfKÛ&‚–(&f^«é½†Ácþ…RÙÊgæ^IqÁ¡©ƒOk¨¢ƒÞQ?Äú0ãaÔ9ð]---Œ?ž[o½•oûÛ#¾æ´ÓNã«_ý*×]w].ß©¯¯�åË—SUUÅm·Ý–[äBdYfòäÉìØ±#ŒÏ<ó Ë—/ç;ßù·ÜrKîÚ•W^É<@<?`q•¦iÜxã�<õÔSüîw¿£¾¾ž‚‚ à ™LÒÓÓCqq1n÷ÈÄÐ;ý;XÕøÌ¡-$I"ØÉÙS.bZU=çÈB°³ã]V~ø rÁ˜!×ë+Qã®a|ií¡òZŠ–î&Z"-lëÚ‚aÉë3M<ÔÀÙu—1kÜñXÔƒsõBÒ6-ÈžñÈU‹�ëÎB�ó¹ƒzW–�j{Ș1cøáÈßþö7Z[hPß~ûmÞ{ï=®ºêªõàûz<.ºè"¦OŸÎ­·Þ:,oÖÁ´ÝÖ4�·ß~›O<‘N8!×ÃEQ\.ãÆ�Û/8[­oQÿèz—‡7ßÇÛÛŸ$™ŽaŠü’bMOñêÖGy¾ù%$×Àn®H 'TžÄ5 ¿Á‰µ'�ŽÁDCìB‹�ºª©œ6ùL¾´èÊœ¨YÛHœž‰¼Ñ¹†§Þ½ý UÂOh^ó‘ P*ŠÂùçŸO0äž{îÉI�Ÿýìg|îsŸcÁ‚ƒo¢™íœ5Ìw¡ÆãñCâ8Þ÷ýM�]‡?³²JXhä¶7~Äßß½‹uÛ_¥¡ë=Z»·ñôÆ;X·w+BR�úÓêU.áÂi—°pÂq8,ŽýFèS©�@`¿Üò$SàpsùÜå|nÚe¹´��d•Öd‚'ß»Ÿ@´ë“iƒíQ^^Îïÿ{>ûÙÏòoÿöolذ�`0Èw¿ûÝCJ1÷ûý4551f̘¼ôùÁjÌ�ݯ*K–,á駟泟ý,óçÏÇëõtÉ­)L¶><[�„Í5†ædˆæÄ:”@¿JSÇÑß÷Ã"[˜Q2›%ãO8¨ï–H$رcétEQ˜2eJNBŽ$�Ë=\½àëlÞó.k:V#¨ª�=ų[å‚YËñ8Š�d´ÇÉ'ŸÌ—¿üe~ðƒÐÛÛËøÃÖš†�,ˆAkk+wÜqíííÜvÛmyì(Ùñ“ŸüdØ&FK—.夓N2¬*W\q+V¬à¬³ÎâœsΡ¶¶–9sæpÊ)§P[[»ßÅx¤ûsH’Rû5!2…¶bΚ¸”ʪ\뇬ûy¤Ï�Çs.o]׉D"#d°:fU­,wN«“WšZ‰ôYý+¾xÂâvøŽd4‡ÛífùòåœrÊ)ÌŸ?ŸóÏ?¿åÇ;wîvWÿêW¿:b{ƒßþö·ÃžßW�›>}:/¿ü2/¿ü26làÉ'ŸäöÛoÏÝã«_ýêˆvÈ[õ¹òZ ùÂÜ+PÃ0èë룷·— &ìWÒ ŽédÿÎò$7t]§±±‘ÒÒRŠ‹‹™U5‡¤–dõž7sR.íð±jóý\¸ðÃîÇrÇÌ™3˜;wn»ÉHãÌ3Ï2-!f̘ÁgœÁâÅ‹G$zëììVõÉõ»lÙ2.¹ä~ñ‹_ÐÐÐÀÝwßÍ÷¿ÿ}R©ÿõ_ÿ5¼T;B¬•#n$ŸŸ³Y’I¥R´µµBÐÑÑÁرc‡• ÃÙSYFÎÁ¯Ïþ¿iš´µµ‡‰F£ƒAjjj¨¯YˆÃêäå¦çs®M‹³v×óœ8å¼cÉŸa ¤#ÇÚ‘ýq†¸bòäɼøâ‹‡,¥†Ȉê’,c³Ù°Ùl,X° ×õ�7ÞàꫯÒ󱽯�ÎØžQ�ò+æUQÑ4�†††<ªÖP(”Sµ„hšF<'‡‡æ���ƒAÜn7�§Ó‰ÕjE’$ à �#IB�Éd’)S¦0³rñtŒìy«ßnsðÒÎ'[4…±eS�"ôzè5¶[¿Â¬Š÷l¶]ÚÑ6›�ùóç³mÛ6’É¡™¸oµ¼>ªÏ_\uB®TWQŠŠŠòbétÃ0èììÌu'Égš&±XŒX,FWWV«·ÛMUUUζ<ÊÊÊrN“EãŽÃaqòJó € È7“·›žçÒ’Iyi3ŸH€ˆ¶;y¹GâÉÀñœQÑÃŒò2äO{y*•bݺu#vðÚïµg;U'KÆŸ'm+**Ð4-'²RE×ut]ÏíþY@Y­Ö\.YÖ°Ï 2®ñÞÞ^�v»=—óf³Ùðù|”””ämHÕÞjlŠ�”‘y]c¨…Z×0gü‰Ÿp€˜Qžè]ˆE¼Þµ—¥uu8�@‹ëÑ~¿XûDQŠ‹‹‘åŒ^ß}÷1nÜ8ÆŽKAA‹…ŽŽî¼óNî½÷^n¸á†\/úìxs×k£úÙ/šqÙuTQÆŽK2™dÛ¶m躞[ÔÙN�ÇCII ¨ªšqëz®jV ëéé!ç ù¬:&Ë2“&MÂår ‘ÖÅ._˜ó%îy÷Oç�»š—·>ÊÔêzlý¥ÆŸ@€˜`ËO\ûͺõüäÄþå?ymíðÑåË.»Œ{gÃ�‚M›6qíµ×ûÚŸþô§|ýë_Ï[,)=Ek¸uÔ>÷ñÕ'ᡆ]’$Òét^WY–ñù|”••åöàëYš$UUQU‡ÃAqq1ñx¿ßOOO†aäÞ—J¥F´Ý\6½uì 5 „@ñTÑâo`rõìO(@ÌZ,Ìàͤ7�fÛ^?ÓÊó{ð9�Nn¹å–¼Öû‹…Ûn»m¿‰€’$qÛm·ågœA}}ýˆ×3âÛívn¾ùf®½öZš››éëë#•JQRRÂôéÓ™2e(ûå_6ÝGT‹ŒÊt[d õ5 G¼‰DصkW®7ŒÝngüøñ9cû`’$aµZ©­­¥¨¨(³/ê:ÍÍÍCêf²cRÉ@š¾PT¶ïÉ�R|òbD‘F(ži‹Åéݧ…ÚÇy¼ÞøÊɻگ†�QAoo/áp&£VUÕœZ%ËrNµªªª:¨œ´lÚØ±csÀ‘eUU7n\.m'‘Hà÷û‡ÍŠV•é¾L ·Ä7�]{?üdD‹m&) ¯ÿ5�Ÿ­YKp?™¢—±¾u-ïuoÕg8T'ê0Ñ4�öööÜb?~üˆÞ¹ƒ±ÕöW®l³Ù˜0aB8]]]ÃÆVdI¦ÔUžEQdÒzò“AgW#;ÒûoiüôöïMÂHóa÷£þœÉÅSqZó%ÀÝwßÍÛo¿�3ʳªTgg'†aðQMâžž~÷»ßåêñ{zzPU5çÖÖu�ÎÎÎ>GW ’éè' ‰&º Òtî��ï¶ïùXNAJKð?kþH08ˆWK _;3Òù!ºÔ�Sk×®¥³³3÷weeeNÚµk×°þ‘ì§–––œš6Xꔕ•¡iÍÍÍÄãq, eee9Õ-k‹Ì3»š±�ZœT:õIˆ€À«<Ø·ð Ê¥îÛ±ƒ·šš>~{D:‚–l8ðl™�oâo[‹l–ðc�ïÅßK¢{+íoašËI• à™¦™3¨m6n·!åå幨F6K7!ÏFÍKMÓxðÁillÌ«üôx<¹–sÕÕÕ9»Än·ãt:B�L&‰Çã9)3’”Õ ýÑC¬ï6óØM4ÛÝ„ÃbaÁ˜1›i(t•ñ¥Å?åÉÍ÷Þ'M<igLÑxªœ5øl…Tϸ˜2ï¸a)}’Z„ÿvvïÝI{ª�@*ŽÍê0===Ü|óÍÔ×׳k×.–,YB<ç+_ù ›7o榛nbÞ¼y,]º”W^y…»îº‹sÏ=—h4ʾðvíÚÅŽ;سg‹-bÁ‚<ûì³lÞ¼Y–immåœsÎaË–-¬ZµŠ{ï½—�;wb·ÛéîîæþûïÇçó …¨¯¯§··—+¯¼’믿ž—^z‰ë¯¿ž²²²¡ŽIü‹D L0"˜‰ˆxfÒÆ 2TIA²£ºj1³P,^�Ô~Îß¡#x‡MáCï÷ýàŽ�¤uƒãÇ�û? Sèô†_£Ø}"Šì ° œË}ƒ§6ÝÁ^ÝÄÐ’craý×(÷Ö`QmÈÒþ�c»ÅÍ”ê…L®ª'©Åy¿åmVm}œ¢Ò¡Í|Þÿ}¦N�Ê…^ÈC=”[€Ùš”lÚHö|mm-—_~9¦i¢( uuuL�:•¶¶6~üãóðÃsþùç³cÇ>ó™Ï0aÂvìØ�Åbá‚ .àµ×^ËI”×_�ÚÚZ.ºè"¾ó�ïPSSƒªªx½^.¸àV®\I{{û€˜4aü‹D÷C¼ØØaE$‚ôâ¥]÷±E›KD¨ýËnIg†ÚÇLÇf6ÄBœàncJ)¸=³ÁY ¶A :Õź–&6kÓPÑç/€‡wíB‘%�óÎñ§› ºÿñ4ú†Mô^©RVxjf�[]\¼à;li}›ò±TM<¬ûK’ŒÃZÀ⺳¼Òò®}JXc±^¯7¯üxp�oßQSS“KJL&“üñ�Ì5Aݺu+Éd2—BÒÚÚŠ¢(”——ãóùòú !‡ÃŒ;6#ÉvÚ²Ûíy‰�C­, ‹¤ü DˆtɾgØØ%ñR°IL¡×´R,¥±Ê€lÁg³ðï¶ÍLP¶’NI|¿(浄„ÆÃ¡Ø"&Ò|Êó2Ç•ú±ø>�Å^ŠÞõ(OEf28�‡!ÚÈ¢)ǰëÕ¿ 8ŒÍÿ¸�¢;eRK%ÜÎiy×Uż §±çY;B í½RWWÇwÜÁüùós¤uYn{{;mmmCxDz#�³zõjn¼ñFš›› ‡ÃlÙ²EQÐ4�h4Jmm-N§3—ìhš&©T ›ÍÆÌ™3Y¹r%“'O¦©©‰O<‘P(t`R ‘É?7Õ$Ùñçxy»à¹Ä¢"ãa:×ÑDM‘‹I%“qÚe°�ZŠg� Œ‚E¤"�èek A¼o+cSéÂ�°ÈÖ÷ÂûA…]ÆG§º1�XV’à„ºàü×V¹úÛéyý)Šï±‚*è½ÅC]Å—s5á£1Z{y|û¹ºö9¥ó8µî t]gË–-¤Ói¶lÙBEEH’DSS¦iR[[K}}}® 0[� "¼¶¶6<›6mböìÙ¹„ÆææfEᤓNbóæÍôôô Ë2º®3sæLëׯ'°páÂ\æïîÝ»¹êª«X»v-'NÌU€þiÝHê TapÙÌåÿ$€ˆ4„_ãýí›øKl !3IŽrº·�ée*rõeEêP„@ƒÐÞ ììhäµP!mÂyÄ[¼H€E‚ŸÎ*Äã›ò¿V¯ÃLÒÓý”oBŠš É´~ïmÆÏþ)E®¹£úlpw¯»WAFŸŸU2—Ó'Ÿ™§ò¼ÿþû$“ITUeúôé9ïH‰‰ƒßkš&tttP^^ž+Ï=Ðûrž)McëÖ­¤R), sçÎRÏžH�båÒÙ_ü'¨XF7¡¶'xu¯ƒ£‹‘€Ån…ó+ÊÑ]Çч �Ðáßß:™Úq“keOÏ. ‘>‚vƒÒnÚÒÍU¥�P;a>Ìú—G8¹‹ô“«ë[‘Âö�X…RH¡sô?£Ãê¢ÀîÌ)²»ƒ�,IŸ€ÓêÌ� k‡dݸZàƒ‡,ËTVVÒÛÛKUUÕA¿7[x•ugU¼‘I¼%D,ŽÃê:ÊI¶êø=?iZЉ„Ïbá‡eÛ°O¼rØ�zd.ïýïðÀ;‹¢ªY|sÛ*VõYÙ¦Ù�>$,Üê¯aN`;çŒÝNEõ©`õñÏh›œ2‚w¿Eìå7ñ¾QŽ…É,NS^úéQU­r6ˆjAIÅÑÕ OL�b˜ù6I6S×4MÒéô~i{†¸]÷ù÷`Á‘ý7[}(„6µesû»$õ Qæ*DU¬G "¶•`ë�ü®ëL$dIâ ëñ8Nöõ‰D»Ý6d’ÉvûÈ‹=‹ãr |ùñ³ÏãêD€—ßú3+”…Œ¢ª4Ù¤ûØ´v­åâê0ÎÊ3ÁRr”KÁÃ=(ï&ñjåC𙘢¸jÉQù<6‹ƒê‚:ZÄ€ÐîoaÚØéåõzsž¦p8œWÙ(Wäßf˜{e+%I˜Ù•5röì¯d¤ÖÑ‘Íô¶ÝÍÏ;Î'hZ™å¶ó‹ã—àÝs$Å0Ì!_:‹�è}zû‚CΫŽb>}Ú·9¿X=â‰ÝÙ¤‹ q×5TðäÚçhm~R£Sßm˜qB‰Fº¶>IËC?ÁûÍ=(ëRHº4¬ð’½X;7FqH’LmÅd’©�ßàÕŽWòÕ0‡UUBÐ×ׇ®ë9Õ'‰är´>ò<±X,§R†AoooNU+((ÈçØ&�xæºCÈLª˜}”’l#Þú+né< E’¸rÞ¼ö£ë*•U§ÎZÌ9¶ÑËÔU¼šªæ‘­û‘; ÓŒãÁ05"ñm4vÜCÇcw‘üåßPnj pEÕþkB„ÏH¡ÊΣ6Ï“«æ‘ŒìhƒÝ{v˜ˆV+9•' iMMMôöör$}F===455‘J¥‡Ã¹ø‰ÍfÃn·çIÝÐÙÒ³ÃH3¿j1Š’)¨]ËLèz†_¶_LJȸU•¯Í˜�Õb=øíùH.`ÕÊìq3°ú›x"ÈGŠ‹Œ¸™ïŒ}¼?f½štì}Óª`·£È–}"ÖBè¦FBë£`W’Xh;ÑØœ›‹)Ú\œùÅdËAÏ�­ E:z„›—…cÎ`[¬U±abÒÜÉøÊ‰ÈrÆ0.**"ʨa~¿·Û�ÏçËø ‡ã€jX–"«Vùýþœ¡^TT4„3¹9�É»³(¦UŸ#ÐU€èþGøõîɤ„ŒM–ùA}=*``—�$ �@˜Sy*–ôï°:Ö × ‹7ÅΧf/�WoçiµþˆçË ¶b©¸†Y”AúÍ­8ߌ#d½¿8hˆ»Ú4± ³×‚Ó�p2©û= UChÄQu,¬=‹†-÷äþÞü�y¡”e ÓÕÕE2™$‹ )//Ö«´/w˜¢(Lš4)ïµûþÖƒ x�ÇCOOÁ`0'Áöe½B°ºõM@š RY<ã=€Äv°¹3@TÔ  øâÔ)8-ú"ŒH I’H¤5"‰Š:°Ã S�H¥'Ãï ’šF8ß�o¢ZìÌ^r5¯lzŸˆ~äZFÛ$“iÕc�ìãFˆ±²0ÞNBû›GNóM}¡/Vv»Ë¨²—Ò¡Åú¬Â_ÖÞÉ·Nÿo,+ªªRSSCcc#BÚÛÛq8æ-Ú¬}b±Xò~oy˜�%›™»/ k<§­­-œêêê}Pö9Ïm_A($•pÞÌåX”��htla°£õþ˜�‚`¦×ˬþd¦iÒÓÝ“;ºýÝyGOwº6`¨éZ&}@ïÏßÏJ‡T*M*•8’)ÌÁ¼²b„£”z¼|YÙˆqˆ{«Œ \IR£ÆÛŒQâ Æ¢QY6{ħ„‚Z[†6MåèÄgÕ6�!ŽnßxU±r|í除ÓþC.p²fÇ[9‰àñxr,Ó4inn&Öï€Émmm¤R©<0Œd£hšFkkk.­>³FRìÞ½;¯8ËçóåIŸx:ÎξíÌðÍ`\I~²å¨HZÏ;½v²ËõKóææ–¡,Ë”–—æ&&‰!-lI’�ä� 1MC7@•ÈUT 0ucÙ€EÏîø%ßdÉÚ7Xw»]nIãË…k‘m…Ô¸Â8ìP 3–…b[ÄË”¢b°�ß¿;æËDÿ_‰-o’^݈ë­ÒýH“�îbó®vcÌIcQŽ*Fã›ÂlÏ$6w¢($YfM÷[ŒéĪ)¨ªš#�‹Åb$“I?~<�‡`0ˆ$I¹ŽÁ;¾ßï§´´4¯ÄÖn·cµZ ø|>b±MMM9~Þ,�Ðà÷ôÅûxdËÿ"I‘@3‹g\޼O‰ð‘ˆ™"Üû&kS Q\Z[‹ußZáþõ K2�{¿Æ¹ÕjÍõÄ"£KK²„Ãé8<Ÿì>†Ü¼±Ù°£qØ:‘BYc¼-ÁUµÛžÏ!ÛOéO©ß‡[ ˜q�š¾,YðØ&ãY0‰ðŒM4Ÿs•7ŽE«£cLka 3É?c,št›^¼�Åæâ¥Ö—¸ÀQHIa‹… &ÐØØH2™$•JÑÐÐ@MM *<œ IDAT^¯·Û=¬]�Fñù|CjЫªªrô£­­­9úR«ÕÊøñã‡Ð­v†;HI‰Ξr¥Þ¡ý\Ž<@R{ø[W*§¢0»²b�t‡ÕŠËa?à:Ž[Tœv[ž·A˜‚T"�kÝ`ñkUU ìö¡F¼ÈÜ8ºÏ„O/+ó»…à µN3Ô>.)Û�oüÅà8 å€è;TeÍã¨gÚ„ týçSÏíÀýfÙWz%«I0¹—íü£�G!Ëÿ½Š5£5ÄÍ�¬¿›åÇ}�BO1N§“I“&ÑÔÔD,Ã4MZZZp¹\”——S\\œ!Ä°ä ¦i‡Ù»w/‘H$§ŠÙívjkks®åìx·mo¶½êËf2âiÃ{Ã~ö³ŸýìHNJkç:÷g’­²Ì9“&åR†n …‰Çã¹CA,’aéN&’8vR‰6«•@ �x7݆sÌÕýQñÑ )²oñ|SDœ[°oñÑÇYÛ\¨«zi©~#Ä,T‘�PdG#-Æåðâ–\4>@Vú;ýZ-¼¿ûj}Sp92œ»………¤Óé\Mº¦iôõõÑÓÓƒ¦iye¸n·;—ª�Çéíí¥¹¹¿ßŸÇíëõz™8q"‡#OmëÚÊ«-/b˜¥’Á9s®Â¢ Ÿ�qd³y…AÛ‡?å×{OC®›?�qýÄ]�aßNk.§ó€n^¿¿‡ââ"%µôúð�¸‹·íé`옪!ùþÙ êè좪2¿Ÿy$å¿ßY‡ \\ðŸš< Š>ÅÑ-Ž2é‰n ÷í»)¹oÊ‘_»& R³¢(…ç ª‹>‹Ç1uÔ¿™&ïµ¼Îs»Vâp ̽SvpZÕ©L;#·�†B!Z[[s =+5²ÅMƒ]¸YÐ컄- 555C¤O4åÅ�ÏÑiZ„K}·£hd‡Ã�ˆøšRe¹ßÖ7 $>{S’2YíëâËÅI½æ`Ÿã²(œB+¯2–ˆé=Äѯ”))X'™„�Qø¿Õ‹8’·!a{?S7^þ¢DÛ×þ?ìSê)¯<�ëøQûÎ’$3gÜ)¤µ¯¶¼†ÝYâf‚gšždq¸“…“OÄjµSXXˆÇãɵ1ˆÅb躞ûýGòh)Š‚Ó館¨ˆÒÒÒ!�À`<Èïý¹?­E£T† }g¿à8â‘ÂðhpæˆÓ,$ÒišZšé XP¿ Ïc5Fi]'žJåv€ßÞ|3Ï?ÿ<�=þ±þ”€t*ÍW¾|O<þ8wÿù^.ÿÂ2FJç0ÌaˆŠ {Ù$+ã9;¹ U˜‡V�r„F‰k1úi!:Å£Tžßö=º†ÕâDU­$€Gv<Äd×D¦•Íb\ù¤\Y®ÝnÇçó Q§“Zï;ÂÉ0-�æþR™/›Š¶ó©qg±xòÒƒÖ.Ž@Œ¨@&µy¸t’žÞ^xé%vlßF8æ¥^@UUw¯—D"AãÎBá º¦3¿¾žÊþ£ÕnCUü~?ëÖ®¡¼¼œé3f ¨ v»�t:‰¢ |Ç õ.‘H°~Ý:‚Á .§“ªŠ²!"ØÖ2€nÄÁHu€˜B£µçQïiƳy˜¶p¹¬€ä·gRQ¶Ó*¡È\À¾|-Vk%gA P v1Î2èï@ùãäŽ$"’pZÁ{{!É «H}ÛOiá©HÒè$ZTOäò…ßbõΧYÛ¾§§’l^ôÎØnv55aks²¸´žÚŠiXUª¬ IÃÓ‹j††ifÔl´›W¢›I#ÿ…™¦PÒ8ÿøQè*;¤tú#7"M,¾ÿˆm"‘`Í;kؽkÉD’�6`³YY¸h!ª¢ðÎêÕ¼ðÜsx ½ìíÚËM¿þϬ\•ëëÐÖÖÆ-¿½EQyêÉ'øûŠ,X°°?=EÎ3l³;K4å‡ßÿ>áPˆ15cyoó&¶oý�oûÛyi õ–>ž–)!�žóèFŸ 3ISÛØžëÄñnùs@9g Žú騦°�(‡´{«`W)·Ï†ŸÏ&ÝÞIüÃ�˜÷l@ôç»I)u›ŒvË?hýF5¥ŸC>‚MN»ÕÅé3—1­r îÝÈš–pº'¢È q=Æk�oòZç›8 •qÅ“pÚ °[‡’Yï ì¤+Þ™oéJÐldBá–Lø +g¢*‡žÌvb°#:i¿Î—š15\à <öè#tìÙÃM7ß<!Epú§söÒ³q8ìÞÕÄå—]Âû›7sêi§!K;¶o硇¡fl †¡óà}÷±há¢~@dâ’D^¿ÁõkÖÒÜÔÄ3ډynÅ*~yÃõ\vÙeŒD§Ži{ÆP2õAiG)¶·þï_UïçƒCª(@™\�wù¹©¢ej+²}þ²tYu$Û•I­*Ç[]Aûl'w|€ÙЛ“&–�.äZhùχWqy\£4ª|µT�gñ„ÓxýƒÇhèk@²�¬"÷ÖÅ�m¡í'…�z¼‡"ÙÃÒ9_aRU}ŽDâpÆýæ¦�è•rÙlXûë�KNdš¦É¦mÛyàÁ¸ï¾û\sá;EeÞ¼yŒ«©ÁWTÄ’ÅÇqÓM7âê·sœV+ª¢`QTÜý­ÈLÓ$ìã«W3ov~sœH$¿Â0…Fcïñ<*ãØìÍ—ïO/G)ò0Òé±X&.”H¥ÑÒ) ]Ç4’”i3`±Ø°Ù­8\.ÕåõH?žKè¦G1>(òRöªØoßMûwŸalé…£:’$ãqøø·…×Mé vÒÙMCÏfÚ‚»HI¥˜Bï¸<×K"ÙG<ÖŒ‚L™«š)U ¨ñL¢ÔSA‘«<£U|ÄqÔI Ó$‰¢é:{ýÝ(ª‚¦é„C!N9õ¾öµ¯³sgÁPˆÏ_v)Á`ˆH4F4eÓ¦M$úy]˜2e*{»»3@Šf­hšÖ_ˆì&iœp ÜxãMLž\G×^?åe¥Cj$S–ly”MrºÃ/à|t/ÎM¥yàPO¬ÅùÅO1uÔh‡Ã1b+]׉D"ƒ!bI Í”¦ŽÐ5$YéÏu“ÑR ]FNK„"Iœö…^�ï/#¶{©V@<“U`Ý]€rK½?ÞˆÏQTf¤À^HAE!ã+¦±¤î3†AOdÁH'‘T_N…쪓â‚r*ŠkQ$KБ u€(²Ì¤IiÚ½›÷ÞÛDII cÆÔPTTÈ”)SÐ �ööv^ã víÚ…×ëÁã.ÀãqSVVÆ÷ßÏ”Éuüþ÷¿ç‰'ž ²¿Ç¹»À…×ëå•W^frÝDŠ‹‹™>}:Ÿþô™Üÿ}¼ñÆë†ÎžŽÞ ¸øâ‹)--Í}®‚ 4‘ï”íˆoÁ|p#Î7Kó ×�ÎC™6žh4š«ß.,,Äëõb³ÙòX Óé4½�}Ái¬ z3*b:‚dsgHî´(’ê@´(VtÅF8� ¾·—x"A‰Ï‡«v"òO/$ñ� Ø.��›ÿ„³þzÖŠ£ºV$$TE¥¢p…ÿ²#—j"Ò8£Oój¸ ˜ìõ0ÉçÐ3MAMÍÆŒÃÊ•+Ù°agžy&UU•œzê©464°}ûvÎ8ýL¬6+Ó§OgÚ´i|ðÁsæ§?Í3ÿ;ßüæ79ÿüós†¶Õjeîܹlظ‘7^�D"ÁqÇGQQK—.¥±±‘U«V ùô™gR[[›Ÿ~±-˜©r«²¤˜[=T÷¨M¼nÄhÛòÜOy�©¦ö¯ŸNª® ¿ßO  ­§Ñщ'â$≠%f¿jšN§ñû» „bèªÉæÉ\¥Ã X�ì…™XŽžDRÈÖ‚Œ4ÑãHª ÉêÆ”’ñ8Z:‰ÍfÅQ⃹•èk@3‘t Ï›¥tžº–b×’£ZWò¯0Ž\ª‰Gìùo¾Ùp.àQUþ{ÉqyýÇSé4áh LjɊRö?�><^O^œD…Âx =ûÒ2 ®®n*+ˆjKÙûö¨©®òäWxº5SX³ÀåŠúãÀZ6jßÜñŽ›ÚP»Í…U�[¾ˆ¿{/i3 JÆ#)¦.0S&v‹�ªªª syO½¡8¦Å‹dq€i`& ’£I±"Œ"Ñ—‘&˜:f²LÉéC’-=‰”Rè²PQ^ŽÝn§ïÿÝ‡ÙØ›“¦Ñ¸®¼”÷œO@ŽœŠ%Û�¤�…Öõ¡±ºnÐ{ ‹Å‚Ó•©76LA"‘$•JçÞìr¹0…I2™"�ç€a³ÙH§ÒØvLa¢›&º¦�F ‹ªâ*på@6Ô§£EÜ„¦R£È¿Oï!µe#îöqÐ_ "•¹°üÇìéí&-Ò J=SvŒ,!!át8)ôbµZ ‡Ãô…b˜ªIµƒ¡ÅÁH!Ù‹�dK®9s±ÿ�$‹ ‘ìƒTì^$ņ°xEƒØ¬AJKK(úù÷7Œ�m AÁ›Åô.~€â¿F–­Çrè²H)_GMC]¥’,áõzö#@dƒÅjÉËáÏZ,–|#[bH®¿¢*xúŸs Dô¤iòr_ P0�¸¬d#È'�šaÞ]MѪÊ8<¿ü"þH˜”–B²H ƒM±£™22^¯—¢Â"ìv;Éd’¾¾>´t ÌB‹e¿žÌ‘¥ûÏeüž#•ù7{N04L#ˆd$3jXŽR__§Ó�ÇãAûâ<¤í{�b&(Pô@)þɯRQxö' G4ÙHx¦²Ôµ;gßþzÝúÜzÒðÇ�…¼ïõî‘M€”ò ’N§‰F#†¤iúÕ+L#޼Δ²š™qS'[“,I ¦¤’Je"Ë2Ѫ°<’j«ƒ¶Žç¨¬¾ð@kØÇqnÉs¼¿gA޹ðÅÆF>;ujVèç¹’Ä>õZ3ï÷nq¿à÷}È‚�xfOWÎI<Á×É£6Ù)#DùCÓr…P¶K õÇqE¡¨¨(Ç •ýü©TŠh4J,EK'1Œ4.›‰,G"ŠnJè†@¡Ç²‚¤X©Rƒ½XæPESêÈ k™´¦çÔÔÊÒÉôJ `d:/Yý¤Ò{±YË�ä�‡â¦²x Sü!µŒé…=Œóz™âó!ØÀË+I™tyC˜Hf¾Ša �±OeÙàëÂ̼f×mGç[vä–KJÈ|flØ«Gm²“¡.Dl FE©*9Ÿ-ç8èo/õaè ¬ª�Ç)¡*R�„LS"­C<!‘N¢aC ,~=•ÙxŒÔ>“a‚‘B˜ÂÔûÕ5@ObXÔ¼9ìýît|7n,í>‚(?�Ã4EJÎ⬢;hô•V653±¨ˆx*…¾OojÑ¿8äAöAJÓˆÆ9µKôï²É´F,™Ä4Êk +�UZ׈&ýÉŠ"§·eŸÑÿžm»éÔ´þ %[‘K¾Àh¤â 8Œ›}©TŠ@ —x4ˆÃfàuK˜B&™6‰ÅMtƒþÄ W˜U•°Û$J¼ i]'IKLÙ6 våv)ÿïÁ§Ä`gzþ3¡zai H‚ò¿N'º¤÷˜Šuø±1µæ8–ƶ±26ÁžD‚ÿÙü—ÖÖô ˆÝfÃæ°gjw¬V`fŽåÀUàÌpö H¥S¤SZî6› [?Ý�ÒéL©&Hýúý@ÚÈnÿ^þÐØ>`xJ‚‰ÅS�¬¥£:ÙÉ@Îxl¦¢Çq3ó®?þøãÔÕÕáó“J„(,E‘‰Ä ¢q�!äþp ]×2]’„L"mŠØ­&Þ…²"… Å 71e BÉx±4¡°êïã3ç_‚ÕbC˜BHª#“¦Ò¿‰dEÙ§þÛ*£ Ô> ! "É^D¦ÓË1€Öð.âôŠ×xn×øÜ©¶xœŽh”1ϰö£¡™˜UÿEUUò‚ŠƒE¾ÃîȰæmàú`¹Áco<Σ íyN�U¼Œ¯ê?]� ÇG& xå•W�e ·k En0MðtÒºŒj±a‘d"ápŽP-[’š‰¯¤´4þ> o�„d&°Ë2IÝÀ²;!$zºý1¨ÿ£)“õv  «EÍSùt‡Bz¼ µ/�dHT¼W•âŸÑPëÿ°›7Ï�P°�ù?³«”Qk4!x ©‰®d‡Ó1äp:3‹^Ê‹ÃPë F¾¶¯±¿ÏÑ ò`[kÎkpiñ6ŠÇ]‘É‚í‘Ui†s›&š¦‘LD),È�Kt÷i$Ó26›«ÕF*™ä'?ù1½½½†ŽÑ_fšíýg³Û‘d+��Á#{Ž] [ñ8R?]¸Ífç+_ÿ6›�~þÖÌ¿ƒÔ0! d l6Û>¬ê,öÂÜäªq•£’òü±• j!•Õçquâ)îè=1GïùÀÎ�œ[SÉñã'b¦ÆAQ n»=ŸÝ]´d’ÇÈÌŠ¡þ¸Jž‘.í�^îݾ#ïõcÔ$Ç��ŠäšòOû"‘wÝu‹…]»9çì0…É+¯oâí¬G–e¾øÅ+�$‰ßÿþ6¶mÛÆm·ÝÊŒéÓ9ÿ‚ ðïõ³ê¹UÓäÔÓN£ººšÇþö$¯¼ò2ÿxÇˬ™ïrñ¥_dO{ �=ökW¿Á½�>‡ÝnçåUO²§µ‰hRGV,wâ©Ì™=“Þ]Çã;¶QRR²eËðù|XU;ö‚B ü|’Æè׃̦nBœ Òy*2 Ð…à©ÖÖîí劺ÉC¹{¥ ÿª$�"+y*T4í/ï â±xÆÆ hò7ó`g~çÜéj€¯N(ÅŸæhé Ãí·~ø!º®sá…òòKÏc·Ê„#Õ5µ|ë[Çó /ðâ‹/òùÏžë®û}}A®¹újJŠ‹H'üá·sõÕ×àóù¸þúë¹á†¸âŠ+±ÙìŒWÅgÎ\H2e" W_}5[Þ]ƒ™` ;Éh�O�r ûÛã\tÉå¬[³šêb;/>¿Š;3uëÖqï½÷òýï˜ IÈÓ㼋9eRŒM$Ø¥9r˱#•âÎí[¹nΜ¾AµÃºnPXèE݇—U(*ôŽ‰Ç“"L“x¬—ß½·ƒ�žÏ»;N�sM]¹|ù¨å\ £s¢(¶!XôûýL˜0 �×ãÄ0¡¨Æ«¯½FÇž˜5kVÎÎȪc2�Åijjâî?ß�ÓáD×u¢±(%¾EA7 7)pÊXc)¢is æ^‹ÕjÇép Ê`¤ôõv±~ýz~üã“J¥¨®®FÓ4,3›|*�áCH¼�ä#,ŠOç³6Ò´g·í=¹ êÿ½q+§——0¿²’q¾Œ'©¸¸«e(£FQ¡weÏàQQ^‚”ðóJË^ÞÛ»‡^s æb 1ÝæêÉ2rÉòLºÆQO¦Ó@Ž€¾¢¢‚+V0mjm&J´¶v°vÍZnþÍoX¹b{ö´çâ$’ºa`N—‹‰'rÕU_¦®®ŽŽŽJ|%9'…¦é$R·Sà°J¨V’bE±{AÄ‘T'²Ã‡du"Y]HL®«ãôÓOç†nÀjµ²gÏl6±D„hO.2Du=ó¢ø>æ,òêý>ÌQ…ÏSÂbV°>2–4r®KlS,Κ.?ÛvSšŽˆ&Т!BÑ$áH4wÄbñ¼¿Ã‘(áP€x •&7÷oßΓ­Ýl�& 5'5œüWõ*N�0 ÕwV~|à(�TÂ�´¦9© Xqœ½¯×ˆ ؾíCâ±'Mc⤩ôõذ~©t »ÍÎüùó±X­è†Î³Ï>KwO3fÌ`μy¼ö꫼õöÛ”””PQ‘)9p:�¬\¹’M›6±xál6lÜÌ}÷?D"™âÃ÷Öc³;Ð%+å•5´6ídÒøjB}Ý,]º”ÊÊJ~øa¶lÙÂÔ©Sñz½¤õ±wÖcó�΋×1ùc�#K^}°C륻ëy;»x(2�á´[·d0ÓmC²"—y6ax`¢6‘J'yZ[„™Ž"¢-¬7ʆµ&Ns´rBy€²ê‹À>îŸ6Ù=ÝÐoxK— ©¢€â[ÿ!{»ºÐ’=¸]ÝŠêÀbµû]La�ÅÐ4-Ç,™%+°Z­yíÊLÓ$™Œás d :ƒ †lCËÄ={¦W�âé¯&©Af"ÒGï wáØ¥²÷š÷¨Xò]Š]óŽ©X££kø(­Yöÿ·w®1vUUÿíó¸çžÇ½÷Ì�Ûé ÐG(¥Ð¡µ…6H’4hŒDb¢c41bÔh0$¢‰ô»ñ£!•¨ACRh„Š‚¬<,` ´ÐÁ™Ît¦3s_sÏ{o?œ;Ó™>„ÒÒ¹ëÛÙÎ=wïó?k¯µÿë¿XQú+W7ä'#7òjRF#oq&€¶Òy¦™Çxjj5‚£‹@4ÔÍSOuGŽƒCÁe…¼”›WùPûÂYŸì‚W%\cŽ[¨‰6ÁÞ—°®&“B(¤d™DªM×—Ä_‹ÓÁJ) ÃDÊŒ4�s]jY–¢”MÁ² +è�ß M dŒÊ$ÂpsBRÇ •ŠC­¿†}šì @89�}8ž¸¿M¥xþ{�³�yó¯E+oá+•C´'Ãs�5<ÖZGªÄI9í„‘SK^ƒ�ä¦Òv_Äü¶%èåsb²m³ŸC;ÇXóT—°Ø}ç©4NY¶ô? ×à ŽÏÕ‰ôø…û(‘Ðȃ»hÒç÷S.—Oj”y¢½öú“¬W „¢(6¢ën ï‹i.–¿™‚¿™íç¹±y€ýÓ Óí�}ÑEü;uO�ˆîâé×ߢ\4Ø^MÐýMP¹“ÿ«oò™tœFÓ\i¾�OßÇÚ~9š®“Å¢›Öè¦qZ ,MÓ°,‹4Mòd…]NZ®vbF>U"¿Î²]“d2�v,»XÄu<ÏÃê ïÍgÈòmÛÒ߃/Ì Ðè\7Eõ’�,3ΕÞ•˜Þ®Œ ¹>8DÚx :‡ œ�,]ü¦@¡îôÒ0¶{ } ÏX=Ç{a«vrìK?Æ`ñþ‰{ëG0Œ¡Ò(LR©P§ó’B`vc�îÀ‚w8Ñä=%ºqŽç200ˆeY9�KÓ�RÒjµh6›H%± Õju Mç­#/Óÿr^Å)L�²µ±�³èR@³A³)šÛ ¼í¼šð²½‘º³za—ÔøÑ¯°îù4M¥#UŠ]4Ú)Jæ�aOû1Y †S� Aîe4….I nÉÃqœ%+¥�N‡ÙÆ ÀL olIDAT…eX”J¥€dYŠÿä("RÁÜí6þ²ˆFÏÞ_O)4œí7‘\ät¨Ã³h‡'Ð ‹0V8E !ráéw“^”YF–&8VNzL•‰ãºK¼Ì|À/•�\M…¥¤ÐV{†úø   ~Ë(CîÇÎX[„@z@Ÿ»™#Ÿ=˜w~ÊÙÞ8¶Kæ¢nQ�¦1i’¼c H)IÒ„$‰1ô ¯(b…i9»Ý�³,£Õj155E»Ý&ŽbæÅ7¥‰â˜™Ÿ?Šý>H°¶\NÙ^VkÕÈÙØ×êk¿Jãæ1’=¯áJ aØ´IÙÓ°ŒŒ ˜#ŠÂ…°ÿeм1èt�iDÕH ‘,P*çôœ ˜ššbtl”‰£Œ��ÓÚ¹6°Ì¿óñÍáƒÿ ´o…"Ü1‡ÙÇ—ÝZõr–¬ZÚ†ºi=ÊÊýCøƒ_ãKƒ 6HR¨Vtl«[f,³“éüJ�<&%Rfš¤ê�©C£#°]Ÿb±H½^gllŒÉ™Ib¡tI¤"¤.A* üJNå™:6AðÇGÑbpǼ3XŽÜHÏNˆEt†V߯Ľ¯ "�š ‘ß}�R¹Js.ÿò×|·(‰ã�8 I³t¡ïIÇdi~�u¯Ã°ƒNL­¬aõˆB‰¾n+îF£A{®�QÐÐ B º4¨úU<Ï#N#^y짬þ›�²ÍïI.(ßÁy_Õȹe¶5ÄÚ ¾OðÑãT|çÅ7qKUês‚$UÔ|ƒj4"â°C̆QE!aØ! æ�i€g¥ T¦³s€Qb` £¶,‹��úü>te CIJôTdzJ ­¢V«¡i¯<ü[®xÔFe0ùÍ7Ü|ç{ÒŒæù!;+\¬ž-‰FëO ïÛƒûl?hŠâ×wÐY?D³>�i$x¶èže(¢X‘¤¹r‹ ?X´L°ÌüL=ˆ`.Ö)ºúûk �ùü¶l¾ÙÎìì,QQ«Õð}Ã0H²„ý=ĺßAЉ»^fÕ–{(;—/_OßÈ9•1>ó8òþ§±÷奿Úç·"®ÝJ«Ù&ç0´„¢ ¦‘‹ïåúbÝ—>Sĉ Ît„aS®øKÎ2N•éÊ·d!ŽãP(èt:ìø—\²«‰Haìîç¸è²»©ºW.ï­p çH&Z{È~ñ$ÎãUTA‘~qöõ× b“N§CÈ,AÉ,O9uE©5ÝÄ0‹Ø¶�ã8KºP½áЮûÙ´ÛC Åä7F¸`ë—©Ø›{±b çÒfKÒjäàø½¬þá6D¬Á€ß¹‘òÊKQ’Sv®ÕuÃÈUHÞ 0¢(äïÏî"�|„M¿ÛˆÐG¿=ÊÚáoáV-Ë ¼�€…É(£o<„¹û¥? ½Œä“¢Ý°™•�hÚ»ãšÍÍžÎ3ûÚÛÄ}ËcööÊW]Oex¦Vê-B ç¾79:ûͧ÷Pù“‡1f¡<Áø'Féßú*+×c™.ºfþ× “Ró§ëAÐæÀ¿þBçØ>ôàÅ gÈ)G¿v„‹WÞ…[ÜpÞ ÁõržY”Ns¬ñÁþÝd�:+î&¹(¤yE¯8Lã:ÍñÉ’eo ÝE� N;'JÛ¤Ù Ñ›¯³âU�â”À{µBsçñF Ë­ô;›Ð…×›ì@>È%a¶³—ÑéÝ >ÐO7Ñ;:ÆHñ8›W qƒšïI¨M· ѱ¬~f?Së»�гeÙžoôrÞ%%L¦IÒ6aûu‘4:[MÖýl;Ê8.=9qÛ!ôµ|o±n¶~1E³LA¯öð·iÿk¦S„÷IEND®B`‚pr0m1th3as-datatypes-9450b29/doc/datatypes.qch000066400000000000000000017700001527370255000211430ustar00rootroot00000000000000SQLite format 3@ .zq  o–«A ž $ § › 3 ½ ? É oX''qtableMetaDataTableMetaDataTableCREATE TABLE MetaDataTable(Name Text, Value BLOB )t ##�/tableFolderTableFolderTableCREATE TABLE FolderTable(Id INTEGER PRIMARY KEY, Name Text, NamespaceID INTEGER )| ''�7tableFileNameTableFileNameTable CREATE TABLE FileNameTable (FolderId INTEGER, Name TEXT, FileId INTEGER, Title TEXT )t ++�tableFileFilterTableFileFilterTable CREATE TABLE FileFilterTable (FilterAttributeId INTEGER, FileId INTEGER )f ''� tableFileDataTableFileDataTable CREATE TABLE FileDataTable (Id INTEGER PRIMARY KEY, Data BLOB )� 77�#tableFileAttributeSetTableFileAttributeSetTable CREATE TABLE FileAttributeSetTable (Id INTEGER, FilterAttributeId INTEGER )�33�/tableContentsFilterTableContentsFilterTable CREATE TABLE ContentsFilterTable (FilterAttributeId INTEGER, ContentsId INTEGER ){''�5tableContentsTableContentsTableCREATE TABLE ContentsTable (Id INTEGER PRIMARY KEY, NamespaceId INTEGER, Data BLOB )x--�#tableIndexFilterTableIndexFilterTableCREATE TABLE IndexFilterTable (FilterAttributeId INTEGER, IndexId INTEGER )� !!‚ tableIndexTableIndexTableCREATE TABLE IndexTable (Id INTEGER PRIMARY KEY, Name TEXT, Identifier TEXT, NamespaceId INTEGER, FileId INTEGER, Anchor TEXT )h##�tableFilterTableFilterTableCREATE TABLE FilterTable (NameId INTEGER, FilterAttributeId INTEGER )l++�tableFilterNameTableFilterNameTableCREATE TABLE FilterNameTable (Id INTEGER PRIMARY KEY, Name TEXT ){55�tableFilterAttributeTableFilterAttributeTableCREATE TABLE FilterAttributeTable (Id INTEGER PRIMARY KEY, Name TEXT )h))� tableNamespaceTableNamespaceTableCREATE TABLE NamespaceTable (Id INTEGER PRIMARY KEY,Name TEXT ) ááAoctave.community.datatypes    ÅûõïéãÝ×ÑËÅ~†\}†|…D{„yz„#yƒDx‚bw‚v�1uf  ÷T �ï(index.htmldatatypes.categorical_arrays.html$categorical arrays categorical.htmlcategoricallcategorical_Create_and_convert__categorical__type.htmlJCreate and convert 'categorical' typeHcategorical_Summary_Information.html&Summary InformationBcategorical_Query_Operations.html Query OperationsHcategorical_Category_Operations.html&Category OperationsLcategorical_Relational_Operations.html*Relational OperationsLcategorical_Arithmetic_Operations.html*Arithmetic Operationsbcategorical_Sort__Filter__and_Set_Operations.html@Sort, Filter, and Set OperationsBcategorical_Array_Operations.html Array Operations,categorical_Other.html Other8calendarDuration_arrays.html.calendarDuration arrays*calendarDuration.html calendarDuration@calendarDuration_properties.htmlProperties€calendarDuration_Create_and_convert__calendarDuration__type.htmlTCreate and convert 'calendarDuration' typeRcalendarDuration_Summary_Information.html&Summary InformationLcalendarDuration_Query_Operations.html Query OperationsZcalendarDuration_Mathematical_Operations.html.Mathematical OperationshcalendarDuration_Equality_and_Filter_Operations.html<Equality and Filter OperationsLcalendarDuration_Array_Operations.html Array Operations6calendarDuration_Other.html Other(datetime_arrays.htmldatetime arraysdatetime.htmldatetime0datetime_properties.htmlProperties`datetime_Create_and_convert__datetime__type.htmlDCreate and convert 'datetr  NunûöñìçâÝØÓÎÉÄ¿ºµ°«¦¡œ—’�ˆƒ~ytoje`[VQLGB=83.)$ ü÷òíèãÞÙÔÏÊÅÀ»¶±¬§¢�˜“�‡�{um�l� k�j�i�h~g|f{ewdrcnbkai`g_e^c]b\`[_Z]Y[XZSYRVQUPSORNPMMLLKKJJHIGHDGCEBDAC@B?A=?<=;;:98765534231201.0-/,.*-)+'*&)%&$%"$ #!      öqûöpzo= ÷÷ doc îî!qchVersion1.0 …ûÛ…‚S…*�xœm’ÁjÃ0 †ïy ͧ šš c8¾´Ûu;ô°ÕDi Nl§¥{úÉq I)ä(¿>ýQ¤ž¶_›Ýï÷4¡µ:S·@Xqh) ” :O¡C¨ó7!9L°¤K tèœ)Ñ:‡¯dz“)y%ì»êy¯ºÂ€áÒ“‡G…¬ÈÂ[*„ñ‰ÐJbd¬ô,Ï5+N÷PZô¾ÍjÏ'çÇÀEeWÑ´«:¡Ó»O%c„Ôþž·×ð<ª~’èEÉ¢diع�ÀÔ`ŽýÀx@Hmf˜1“>l5ZOÐ…†ÜÙxZf V™ÓÍwëóµHü»7:sùØ! =óÁQÜ1ú²Ýa”s­E·ÈÀ8G¾§2˜û©��7ã}м9”l1M}}’¿Î{ˆûa�æŸÕ8ª 11·Œë#æÓ@Ë8VÊëFÈ´iÿÛLÓ¸˜°>? xœµ[Í’·¾ë)ú`»J»´öà¤*+ë•-;¥¿Š6NrÚgÀåxg€ÙŸœô¹&/§'Iw˜Á�CÒi €þÐhôÀÙ^½¿ºþׇÙÊTåüÙÌÿ<‡?•0œe+®´0/'�Yžýi2…vS˜RÌsn¸yZ =›Ú†g³©¸�ùNó¢ëÄ^œ~=^À‡õü⻋ïÏ^¼8ûî�³éšZ.ÙšgwüV°¥Tl­ä}‘õ-�Fq†ó0;ãuΔ(¹9[6uf Yó²0O4òõ»¿³÷™á÷âÜN½º˜_ÉÚˆÚR¨<›5¸Æ²˜Ï8[)±|9ù*ƒÉn¥*2^Þp¥ø“žÌƒ6fÛfS>ŸMaàÖèRÔ9W¯ÅL0EÿÞy`‘•hÇû†}ã6éæiô´QÀáv”­îcø¢ìRm߈å¨7Ê·$ŒÔBBßÈÅo"3n¬mc®m�+…• õÔ�îÚö�ÖÍz]Š ¤'ÈJ®µ@…ÍÌ5§ÍáEuk–öà|Éj]”"§ðmÑÑS’p�u˜£æ•x9‰ 8ˆ 9�Úü«æ3“Ïg™ÌEi{ºsT_:ø2™÷ªŸ�gŠS¿kÕV,YQ¯à Í8³”zã¨ÅÂ{Ζ¼Ô‚I³ê¡ÐxÈq®)àLkQö0îAx‰TAݬˆ]mð€„ eRÝ-¤¼;¢Ûí°¶[5tøN¢�+íÁ*#NFŸ'=…vüq…QÎ�AåC|:ËTÂáP b¸ô® ôhpÙtÖÜð¢ÆÃH³†Ã˜É²©jèá�gŠIܺ ³¸u1�b7¦ê™Gÿ­5‘¾a„™ôCRøÅBVªCÒ«îP…™¬*~¦Å< ¾2eØ7`Ͼí«É£’5ëöªÇ¸£á‘Ýèàõª©¦Ä÷NÙ�•`o/¯ß\þp†÷(`BPæ–RUc„ÓÜ! *ãŽw7êð#Þ²ÛóÕNLñãþ°’@^ÉÀê¹ä ðâDÎOÄÑ8 [kàéê“yWŽr©U€Šºjôt¦ŠꟇ7ì�k¶‚0@�J‚sŒ‚íÙôŸŸ±Jp Ú:OÓJ›Ž^/m^:&j¦þt¡¬Ø/^Xlm´´øa6Ö·T•š¨ô{)2Ÿ”ëãàöþ^€»ö‹ ÑßlÝe(ávü %fÇnBšÌwou{5¯Öè¥u.G�·�ËŽ|o÷mßUûµÍ×xɇ–Zئ{è"Uº!ÅAÈ@ª8T[ÜVcTÄV†ïD^ÛЩHG‰ÿEQ†Z”aãJwb”Ÿ^FQ†Z”aãJìƒÏgN L¯PkYòP·¿´P{­CX{�ñJæ'ÂK6¦‡“ZZ|T‹ÛÚÚ§tñ “ȃº3‰ÇB¤I2ÙÔèÇ•xÔ_(-U7ÕDSÁ㤕KP§>RÚ�ÐËzP‰ï2~!ˆ” `Øc&äÌ"läIð馪ÈQ kqVàðA¸j»0m FЦÈ4´Ð–ÖŸ:÷À€Ö‡_[�{¦î#±Ñ¦Íûœ´(=Üc‚+‘)aÀ­@7Ï—cÈ^Ýî�, ¦Ú§ÎÇØl…? ]9†ågø*o¯³ýŽ¥ŽëÏ\¯&sWˆò@ÔBÙèf]~=©Àí·hH •âñ€i¨"Å, *±¡ŒMÁ¢äh° è‹ h�¿ìùÀ %òKuKÑìN¬5ôöcrm~ÃÖ40º!6ŒiÝ(ßÎJØÞc�ÓÕ™õoÛâÁ÷hc”€�¿õ�üXƒžùG~°‘òÂk:aÜdN–on&ó°ÕÞò¶/ŸÿóN–%üùUZǰ�DxÝûpÿö¬©sAÚêÍ«·oœ@ý ŠþO`s¸ùÚ üšØèó¼CúüË'~]5Î3%èöøVJN?ñ ÞèØç^­Ô‘ð\2 1¨¦CÔüÞ"Ü|¾ä…°}EdÕt„˜ ²—%ï_}dßXçKzËBåo�kùS†ì¤êì´Qq’`G!£p”Ðô¦ïö8616¼Zz7ìMX?dszï®–Ë"žýŽª_™}ÎcÏè”u�U»Þq�ˆöí%Ò 7ÓÖÓW÷  z–u²%1vÉ0ã¢á¢i"b`Vr'fàOÌíU–pJü{oó;Xü3EÕ¶|í:fptñËzš¾½7µ/zÜñþfYˆ2×®#õÖhÇÂ7š€^þc)·Îþk«îiQ·Bð¹ Œ9jcÓ²¾’Ó´»[�ÃV…’ÎFäKn¹ÝoÐüÚdÉk¼�…=3˳”;Å�у“ûH (*nÌí¬ü+'V¨%v�g[®�N–º¸…lq4± ³^!K7¯` Þy¶z ÛæñCHàªÄª››$ª>~yÚ/ýÍéÕ¾Àÿ¤“î5r‹À{ÕµŠïv– •o^XÔæší¥Ø¶ k3ÿ4 Àª$¿ m£¥9ú5¯¸žð–°hòêŸFÙ›ð?ši‘<évºÂG2¬Áp[Îò/WVÔhn¨:Xç½ô0Ø•Ê àõ°Â¥…’›é<=Ý=2ÊëN<"ô ÎûykÒ=÷�&ÿ’=ãˆå£[":øÞ9¡ +Zí`‹…UšAO»“™»€·&ì ÙÔ¨EJC¥Ö‚zƒƒº1Öís Ü­­„ÔµBZ\£¾¸c qÍ­Ø P¨FZ�äµJÜÃR¡/5Ö»R\¯W¼à¥Ýca3@‡5Ùt‰uÈûØ!ïC‡øt"{  ÄÞ u�gNTB¯jaŒ? A.£Úª7Û´ ¢^Q:ÜA†Ô*”k[vd\ì°¢ B«*‰Ô!̧¦¤ôœ×wõǰʇ2‡$ Rþ�¥ÑmtÚRr�C‚VEü”Æ%F9”Wíx¸vêÇð<‹8ܼH`,ûý+�Ö]�k_«V/¼…iãÁ¤DO��T(I)°÷n>¨o£ñ4¹ѰúÜeàæ||÷"Îÿ38ïrºc…ËÙÛPÐðòs‚!_BAÄ!÷®Ï±å“ÆýQ‰|ÊZ×L–’™ñòiü½! ?aü Ø|°W‡Ž,}xœíYM�Ó0½çW áÒ¶…"„ÒHh—½‚ÄàT¹‰ÓXʶÓeùõLlÇuJÛ-)E[iµ‰¿™—ɼW· ŸÝ|º¾ûöù#d²È#/ì”$x(¨$g„ *g~#ÓÑ;?ÀyÉdN£¤áD²ªÂ9ya §½00ðE•<´Á^E ‘D>ÔT¼‡ßPxÙ ”¤ 3?Á)? Òâ&Q¢WLpTCœ!f~6…Tà߇ׯUBÝáŠðè& ÚÌ \˜(‹^¨k_õµ—a  aP·á#¸e?h2’¬ k’¬„=öôª„­:…M} Ô6ò¶°í �SÙðR1È.[w]UWÕœ ZJV.=H·�S)è÷†­HŽë@V 3 8j¨h©»Ù¯à>cqE#$,(ʦ œÅ:_w‹ö~tÕ4#†dsQY–“EŽð–>öGV%�V|ß�ˆ+äâ™ô1˜„šr„B1®êºLRˆ«rE¹À.!yU.£/”*øˆ3å`ëdœ¦Ø:&÷¸íáçÝÈw8,Â6Ä\ÓÔÀŠíîGêp $«š¢B V6’"Èœ+” Ìœœ-ϙź£=®9%’ÎI™Ìõc‘ó¹½8oÅÖ¯ôXk·7ÔBV2@í8B7e¶J7õ>Vê&ÌP­+ø±+Ü1j÷zrߪv�ã`¹{›zß�;À“ƒ-e¦â·BÇ $oƒ¿~;OÞL¦^Ïm L©ŸœA˜67Ý}±‡CíÁ(¾?Þm¦jÖ LùŽ5f¨A(øƒP¸„Îñ× bSï¦rONï6lÿ£Þ�€ûãÝz·%°Š·µ8Vó6ÐPÕ›to�'V~—ådÚ·¼¨ÿ<6g­|+åÍ™Ýê·œ¬ú-¯cÕo U¿ 0@ýybõwYN¦~[Á‹úÏCý· ÿFý6ÁæÌ¾Ï~—˜³p¿ p£ ß ¬£ Ú¬á'ß8©öxƒ·ÿgÂGwnU�2Ïqˆ‹Aœùï6®Æ·Nï6&ÖOÈÚÃzr�9Üݺîà{Ôîxƒm™bŸÖ�Tí¾O«W�b#bßbóò{¼Ùƒ}á¸æ·Ã6H~ß¾X»G^-YLrø�püÐg £¦±d+$“*- öS�»!Çýg€¥½nIÚobMû†(gÑv-õ%¸C)L`^ú-Ó/Ž„ ƒˆ+�Z FxœµWÛŽÛ6}×WL] À^!�, Ø$@^Ü5�¶o´4²P¤CRÞxŸò!íÏåK2¼H–dmw»M�ÅÊ¢È3‡3‡3Ã쇷¿\oÿøõÔ¶y’ud%=´ Ššiƒv½hmµúi‘Ò¸åV`^2‹–7Lkv2Y†“,�Ëwª<9°n*³§šŸáb}N2’5¸^pÓ}_äYÊÜê—ùy�f¿¤±‚³^Ô¯ 2ô·¢WK+ UâÐXvd:ß¾ÏR÷„5d»Ø.‡g~ÊïaÆó,õYzpVrØê�WÀ塵À 0èmD ÿ6³„Š ƒ l�ú†¼JRÉ�ãÆ¬^-x@šR<ó›çLÜ�ß@£mµtŒ„Úó‚ 0ô�ée\k4,,?™ ˆ~ë!;z)ñÅ@2;r?½Ï{œÞéÛ»œŽÆB¥4l”]½Ym�+P`ƒÒŠÄDA�ó11™g1ãWof²¡àÕ y,ø–âA¡¤e\r¹�$,i©3âv‡¬¨i–�•’%MLºmö�êÀlͼüyP7lAŸ“e´åuÇ“�¦«ªÓ~jù‘‰ÎxœµégI�mƒšI<¸�ßÉÉ&l R*KGja£&î–€Z+}‡&‰ð@‘žþŒïÔáP†~q/B9+ÂÿŠjn_„�KxR£üðz ñËæ²ø½aÄýª§ï•n˜} ÄýT�½ßð½yâaœîÿT'ìüÖsøúå/w`˜?0_¿ü}×I’n ¸)¾ÅrZ“žRzÞDz^PPD“$ð ›�Ùz‘ož8«æ5I›üLƒ¾jþn¸­)Ý‹ò²¥ôPCŽï¡ çŽò?¥Ä=Qá’(0pš±©þR–Î¥«”JÙJ ¡n{s¢Jñ9¹ß¯cç=("�<ÄçÀ$“È «Ã4,>f¾Ù æƒWÜ�,n‚VUÞìã(?ðØ?ŽsŒ†Ÿ|KFÜw§±¡ ö—Ut¾9üçÚJâûàNâ�§-*°]~—´”—�ï\�‘xDMýDs`Ú—s’®U$ú½O@Úõréãß('�Âr'øB´þ0ùnáÂŒ!Óo•oR˜¦8ÓÕ€1C]rr�ˆXÞíд"tp—+†¾5V+¹ÏC Ÿ0ŠjD‡Ú>ƒZcµ^tN¾rW–Ï·†³ó]³0ì%¨a¸vÙMlZwÕ<ŸÇCe)M$(¿&�7›4\—¾pß`¾†D� Nxœí™ÉnÛ0†ïzŠ©{i�ÄB(Y§½¶@shO-�-¢ÚBRܧïpÑâx©­Ún‚`Šäü³�óÁV‚7÷_Æ?¾~‚Deièõ²˜>2T ¢„ ‰j4¨ÔìúãÀ§yÅUŠaÄRÌc&î+Á/r`B°¥ |»ì¾“™ñR‹~c¦˜Z–(ï`«5mó9Ëp4 ]1ÍÂÀgZâ&t3´ï†&JˆR&åh�ÜÂLÒß5=*Ú1vÝ &´í}àëŒ ˜¶ZÓÞ™ßíòûÀ7�_j'!Œ]´×áò´íг{b¾¨CÉäõíÀšÙ0Ö};Ç›�‚@U‰\söÏKåöy¶d´½(1W<Ÿ7e5Á>V|¡g¨T‚@OJ}×õ<%Š™(œ£p uÞM’M-mœÂOelÁxʦ)Iê„èF%E ³Bì•›¼Ò‘šØ(3‰À”(È<“&�¢, ÉBTä ’º‘I%Š|~C4ÑÐ5ðÝ”@—,83—l%†¡î‚·ÏgbeT¹]J›E#¸Dê%#dF‡ %L¬ƒóÒÄÕó‚“ÿ'Mc×}|˜´Û¼´'uÎ]žÔÉ(µZ?¢XëÞH±æ§dŠóp^¨Ô5½PååSåðo/€)nYÛI•¶t]®´5<YZ½~l©í{Ó¥8%_{ÆsMüW„ik{aÌ óO~Á¼¾¬°uëêNƸÚwãá8tqbýÐbŒ{sÅXŸ*ÖÁy¿³¸z^pò~ǼJ¨´4Þ¼´'\ÖFqsœ XÖ{!æás0›DÿˆšQÑ×ÍVVÊôÐî6±]r3ºÝÔ Ôâ‰KÜ›K��×ãÝÂ'º1ú•m‹©´˜s²õ@+0A-+ˆH%FŠ/P£C7ªä¿Ì¸+9\=+*ùX‹5á+ýŠ?åáîFÝÜ—�O†$m4|÷fß·ÿ6ø xKpwúÛÝÖ¨¹|>`;ðÅà èã�M9FꥠӛT�GÄÍϾ«&²Õ›¬ü¥*õ±ÚDF¾Eé$å-\þ<$þstØñu”ebZÈÍ}.Æ1}–ÇÊ”$+ÛJé"ãp£0¬7rκÄ[1T½ÉI³lÔBí_!ÖPr™sâ‚·úÖÁÀGÒŽÃ3Ť%Ôûm5¦=Þᔕ¿g¿Æ´Ç!zI6qçÞHÙYÒHêÔR&â h'JS©þVþDïY ä“ã©"ïXfúUÞ|| x>ÓJLæ&´æsA·�…i�ÿ°G-z‡‡èëÝ@…â¼`ªÅGn�ü÷#XåÎ)S[OÞúlX}'ò“n óY‚åh�G³�÷«íÊ¡8uˆL‰3°¸Ó—VǤï�Ž�`¸ l¨[ì¶ö];JìúGJ0%µ Ù2PaV»�Ñr–¼GƒZ¬xñáoº®YÜXÑ_•ø® =xTÚ!'ÈÄRhø߈„{î<üâQhzô&²ùq²ÞæÍ©‡ <'ØŽÌ�ÙðÇ]t§*Ù~" ‰¥4·% Ç h·Ú%·V4b«l|ñ9xËÅ SÜ Ý MTÖë“é."åáâ‹gåo¶ðÒ¶ÿìÓûd„ëO[äYæû‹ß½ó0õ²üÝ;ŸÇìlˆ�]ìp0:�Ö9·¸“Ä�ÍÊúàŸL'B6‹¸Z$ô`�ç¢u`ÏúÖ8Ƀ9€nb7Î_ðK�,QÎoµ3ÑèíØÓ§sy?¶Wç¤XT†‚L@œ³œÎÓâ˜.žgŠøUv¢×êu; öZ½õŠªäŽ²Ì°·ê �OzœÆ×P³‰°mÓT‚v ¯‚Ä:³tÉe·+c†s à?±r óo`4±U!Ê ðâøÖ ߢÀ®�’œ¨ n`á“V>øç2ûëOi)ˆ‹2jްL•ÜÕ«¾®ÕˆÙÔUcÇXB[�iʾji:©„KùR;/Í4¦…X¤Œ,}ÎMW`§–óyä¢&€8ÿ€­qxXðr°8Q'R¦½½h�ÞøÝbP�3 *¶Z7b(ºtuëæ|ðŸ“B+ÅYÊq¡å£7u ‡ÃCÞ<ÞîóÖJœ&£"®Ú–l dý`ì× BÖÔõ¶‚X’²ŸönL`>ºa&v$�B?s䜮µUC/멚½�tÃÑTïéÌÆ”h©$¾�|�ÕJ‹B¸ÁîQ��$x�—sÐË8N¤´'é¶aS•eŸ8¼QyÁc%8š2T„°±jäÔĵÍ‘J “­˜ÔÔòº®Ö+¼Ÿ’ØK|S�UYO1¿¥¹DGk5ž«j¢�öxdrÎï¤ÚNŸì½CsË´‰zÇâ€ÌñHâØ™ÝnæØöšÚS{í¤-ŸÝË»ùZö�ë!£w×ð»ipD÷šVR¶�eãiÖ Ì‹¦.€´v©òYÈ—¦TÍ¨Š±”êžÕž\$ßÒJF:#Ô“�Üö ÓNº±vKJÚTL¶øˑ̜Mʹ0Ó�‚ß9?g’ì?¿KzŽÎäF–̽r�бŒ�[E7ÅÆÙx—Šë®“÷(Z.S¬0†a“—Ž4Ì5±cíõÐÒ%3–¾¾fUd�Pf©á¨C[3·©ê÷Óªz�œ Nö°©Œké²SÙœz€½=(‚ÙEn�A ]öE%Ð Ÿœ s…Tz`}æËϸMvQÓ€Ä>‹` ÚçÑí¹²†,³Ô¾z´Ú”°Ò¼œi'‡œÉêÄ5›|’¡‚;åÈe9Å{(«YµÂ#Ú¹Q×µ¦€Ä¿=�[¹žRŠulžUXßs�yVp†ß(jí´Þ8EðÖ! |ÐåÔéïsÛÉ5wظøbÕj¾§¨}%�—²Ç ƒ–³$èÆœÜnU¢ Í�� œ†Å�?)Xù%‚TßgXÆ 8¢8ÂOw¸T7UD‰¡0Ó´#nÑ.û‘åó9��v(#<ϱ›;l%â5b {Z’¯jxÈP)u^>j?¹¨‚±¸ŽpÆÍžƒÄE×xØ’va芀s÷< ŸG!9Œ¹�ê[˜óA2ÿöÚ[T‹ô¬ÿ¹7°±Þ5Õ»—tf#ÔàN|Pm«q'Q”½�Ü–ÿÏáz^t·w¡Ñc‡×ó¢A>þ›Š¶OvÈ›Š¸ªÖ­�MÜ�˜ø�„ÂǧHª±ù¥ìZc#òœs½Þù¨[Bë©]ôÔÌD�Gwè3/|7@OÔâËX[ž4�§í¥¸S%`Ì“,BéÖÐSÓœ&p‹>†?þ£ÇmÄ£ ÿRíÈxáÖìi×"7hOSY¿a×O�4Á….Á¨¸ƒñ;–å —‡kWY` ÑS¼uå¬HèFqïGI.Ù´Èd(’é §&t¹ú¨ÓÂñ{(7ž}µC¹ñì°—:~²÷muéäíϽÈì V.Ì})dïûó†'ß½ˆT¼âòÖyh? ʉ¸x¼n“”°v'okémŒ²4Ùf‘N‹®¸¼°+a/¬U©G©zIØÉY š“÷ît¬Áã#Ùc:+XZ´²¥Xò!¾yD=lmE¹šéÎUPhÍ·�½Ì;wN•'½Í�‡“‡{rc·W¹K^w_5bÞç«(±ìÛ«J»µíÔH¼¯�ý}ûŠ|]r¼Å'|*¬Í¹xœ]éÜý½ 1ÅüN‚ˆf/n÷&‚Añ¦�îJæxWRö&Šû±.÷,.öôI0ôÞ¥ûÁ6?ø•<‹v8ý´³=Á×^í¹ˆb¯žó+^d�hïnuþ�tÀܵ¦ï%?‘kÊ?PîXù~àVmR­i«Æôö�·²§c]Âæ�+Ýàaß«ëÐï^m`ÅíÆ‚€¾9–K{åC.©Ì⦜¨ìì«»þÕ” ìðájÇ‘¶Õz0Ey�œ“OTn@ª:Q=î ‘ø}jø¢ˆâÖ-…SwØIÀ•‹€í@ÕÉ »æƒÎë\®Jÿž~ÙgjÁZ¾7Ân«iQÍÞ§�%yiîF Íê1‰töƬU…I?Ìç˜pD)Ë¿~ÿ*‰Ê\ãúɇÂ|hÇ�pm黨}�w“+ÎeSbwL=ˆEÁß‹76À)`GÍnå”ë­.¯Æíé&ÇËÓ8¤¤#Â.â9ÇÝ$Gˆ ºi{V€ðÔ‡¦­“H¡íÄe‘âF”íi¤èÖQ¹èð Xú)δùüSœrÛ³„3MÏ™�´ÏÏȧ¾Rê9º_=f­J*ì’ ïA­k"¢D�ÏóÓ6t‡/=ÝI�ȾD, ß°�VíÞºOoj 6Ó7d&= Édt„]Rmw�O1µÊ7ç픵û2Fš­¦ò«Öxê©¢¦ j¥ %óCäþ©�Õ•†ÒþŒ�V†¸†J/lEU�-ãŸl=ĤdŠp& �ETy¡Ê�rm<ª«ã©2†?W4ç\UºnÎß}ƒÃ ôð�Ÿ}&ùW9«+c`ç> hßv<ÅTrÇz@âr©ê÷àEÝb6ðoÞÀâCS]sK,ØìÒ8‘-.” ÀvDž'Ó&¿.•{[ÌGèÈ÷6z"’înŸ>íkº¹Æš>fÛæ9ÆýÍ-ú3ÿ®9_T–3щÁ  -=u“¸>åô…ÓÇVOF»ìx‹øºåp½²£ÍI‹÷˜=ÞÅù8 �”ùè@§]Ä�HßhçU–áuïŸ×†v¯r/áPí½ì±ƒÕ8"LØ¢›Âî�ñºÈ«lð t-�¨éƒÂW¿{húF¡?{|öG:GÜCĪiþ5$€Â�ý1=¾J¯ µof‰�îú�—ÿ�ÿ)ÿ[>“ÿ+O“-ÊMÔÚTêT:£ýÐÊ=´âN‰X~ÕÂÔ½ÔÆ¶ÁšQ˜�¨¬‹3« 4øé_i|óa¦ ¿é÷ÀHè¿`´@¾·[[jW]DR3IÐÂ"öè+cíŠ/âSX¹q�,…Ø Øå+U7cR¾Êß;«ØiŸÐÁËm›³Ç· YupO’…ïºÕ!Ðt/(®Í·¶�Þúoòh.ô}d³ì@šQ+2¼å5äݼE§æ K°‚·Òì¼[Vpõé“È Oë팷ØÛzŸÛônñÑ,w$…©Hv>¼cÃG‹$÷kHHO/·mõ�ÛÒq¼Ù~sï- ãÝíK–ÁòÞËç,cRûù>hÙ�<¾§ïYv= ¿�% ì?‚Õ!­áÎvÿŽÂ—|ø··©ðB/=T³xpùúâT//yjü7öåŒ,ØæÖyä{~›pÒ¦ì¹çsŠÏŸð½èœÚ·<±·Øá<¡¶9v'zãåçf”û cҕ綸ŸïYºŸºŸ³þ{–ч,uôíÛ‚ˆ‰;CG¹â³[˜Vßþ±Ëa�ýô¹Ëßàç.¯Ì*ú–/"îöæUúTØõlP¾Ï`DêñYMïzî"ìŽ/Òxù�[DDî|Þ">jab1ß1 ãÇ2:•!ºÇ2Ú øÜfé œæç©�Ò(â·%Ø2åÄ–`û›õ´ìG½Ib¦›îüíôì³árÍúç¶¿{§~Ò¨ßm·wç÷7ç‹ÁîüX‡Úóï»;Ÿ“y ‡Ç¢¿j@<œ�í·¸¹iGÃöÊA–öíËØÔú‘¼™ýë€}}[³æ2ÞÊûߨ4‘ÔÚÞÕ½³a�Ȳ4õÓº²Áã„Áx¥¨-T>Éè“–€é<‚°C¶Ž/Ÿ#…Ú•�×ö;¯=¾2ò’ôYC„î>�VÙ-þoºÿ̆y ¦¦ºOõ"�‰xœí]{oÇ‘ÿ¿?EŸŒXÔaIJTì3DšF²`²XŠ�K.zwz¹ÍÎl¦g¹¢?ýÕ£ŸóÚ)ÇgH`qv¦»ºªºêW�ž¹ø�?<û?þF.šeq).Ü´Êà?KÝ(9[¨Úèæëëf~üÕƒS¸ÞäM¡/5-´Tu­nÍÅ)_§öÙi•ÝâHO.3Õ¨æv¥Í3™>¿‰ %KµÔ_? ‹gtÃˋS…Ïž]FWáþ3¸¸’³BóõƒÅrnàÖY•éxŽ‹U_6Óââÿ!¿–Ót˜é¥<¢›®ø–G§4ÆÅéª3>üïþlîcž ?ò–›\øIkÝKÊ¥|^•7ºn¤*™k²©¤b6ž¾+ËoÉKsüÅ~�ÉíÐØÏ9ã�lZž¿�KÕÔùß Tú�Jg™ÈÍB×Zj5[ÀhÅzYÊjž>?Õ³j© ,þÌi�¼­�Üq5ñÃ3`Æ´¨n€(ppæ�Y¯VUÝèLNo-¬(èߘcV¬³¼¼†‰gº(?8‘ÊÈ¢‚˰¦[¸¦‘ ¦©U^6Èñ3™åK]š®žHùmµÑÀ#�¸0S©vTK �hs8«`åeÕHý¦˜5ϵ²Z5 UÈ•ªáÑF×°c«š×‘³@{•”Yˆ¿Î«¢¨6p¯�8û1Íãå5葪¯×K\xÏf£a�A>+ЊüFãÆã©âxöcP¦|ò_ÓªÎtíÿ4Kz g:øªÎ—ª¾ãÓÔðÿP�5‹¯œ=ýÃËÃÀ/â{þë wOÄC¾é´á±¡¦¡³thbô߬rá$æ¡7MM?@ó\ÅŠ�º>4[×òFÏš ¤RÕ¸=� ¨¦t_¦çyé„ä½�ñ#)ß.ܯ˵i@ã�°O ìûý�°$¹”„äÿnfnº(Ì]» ƒxp4Ï ]z[uŸPlëd‡²W˜b%Ÿ¿ùIâ`„.î†Æ™£ü°Iã³ ­q¦·˜•Q™ß]ˆp`sx:�kä4T/6‹Qç±tŽØCtö5lë°«ÍLª†=ö%`Âd(FÝðd”š‰_g°b·*pÌ‹*³�)oh ;g°á«%6ð²²ÈKí2[ó¼6��ºÐôSÕšMC­9W¶žYks¼&âƒÏ»‰`4H�&-#2¡!W•1ù ,¦øJmç¤ÑÔIžk]£Q�u,ÁÚYàÈì)+iÖÀ^šP‰[Wލd]F,hPÐÆ-IØ5áùr…‚p•ªYÂHXQ!Ϻ¨Å™= …CO¿ÖÆ1ÅGSÙˆeC¢q‰4¬ ‘0ÈXÃÖ” Û䳬2Þ00ƒ‹f lnÑõµ[¤1$ I#A–­à$ú Øõã:J¢ÃjD÷.m@„N®ûˆ-ˆ­èÒ2»¥¡ Ú„gÖÞŸq:7 ®™rè6nvApï·ý!ÝTTÐèl£ N©1N#fëZQ„Ç6ÙteïnéýŒ&&˜Ë_aLw£‘¼e¡?€*Í À(GµÁ*ÜaX£¯�FbY³P ù‘Z¯`t4s‰¢±ÞÆê"öÓ+è·À°·0¡S§/ì‚ãßÜOvõÉOaë?D¿ô)È)ZÏÃÝùˆC„hö‚Yù…ÿ7ÁìÓ‚Ù/ fŸFaÏëõò[2ž¯Àv†=_rØ0 ÇÍâUžA+µ“Ù­“ ç„Ј@ˆgP�«¥…9”žq¥¬È£,%Bîi¡Ma/cQH@¹ 1°B©V«ÕËeñªUÁìž#ï<³N̹$ ÉG(¯uI® ‹�–ŽQb%Où…Ë ½7½ Ãåëb>à>Ý7mñåÁñm[ø.®�ýº¿�²Q Fù»ùh"4^®0æÀ8]] 3[[¾Þ–z#ì•@«E޲Ցi…†¤rZˆ6Ó Û=3–fˆ¥Ðqó[$QT×èVÝN \˜bÆ8 CÑÅ‘WñRØTöñ ÜKío²*‹Û°åбO¤Ñ�ÜF6ÎÎ÷Ì!þ÷ÞÃTä }™;„£�Á†\#+»âIyçy AE©ÉO©79<>mC6’Œ²þ?v­¦¹åh"«JÌõw$¬Kàš‰ÒÏIðpÿ~êd›Èq4�¥Dsa§®×º/Õ²Ó.¶0pÛV bEW­‚Vuñ¸YTë"CŽw`±°¸¸áy ªÎ¯sôáí�#ÕŸ~â�lÄ¥§>ÿ׺jÎiÎ[þw7\áën¸ä®46¨’vMl‹I?÷È$Z̬� ¨¬ÊR_«¿*¬º´L+9?ë,ZA¡KdD æ…Ì«º/dY`ñw¾³tù(@[“È÷­ÜFÝ WÖã`_�¡”Qh9€Ñ0å7V³¨«õ5([ca“ �7uÞ4ºl�q5«nµ«ßYMáô ö&6éå{”0•Z|øÉY›`pr�² ¯°µ²-•ë°Ò¢Þ©Ê´Ú+0 ³�éx ÓÝõ®fØeœïoz—?ofuÕÛ$Óö¿�1ýV¡¶Ù‡ˆh\ˆG˜" qŒñ“ /‡œÈ3–ØG󎢈“üQ¾Öâ !T­–�H…dŠ6$¢ÁVlŒ`\88àDÓYn—òØbÌI;€÷†Ð|’dz¥Ë ½Uì ÚûËrcW•ˆŠ;S%Ú·ƒU"-Å v€�«�c‡òß®JØ‚Þݵ¡?ëLe÷P,å|ø68eÀ04c�76‚ÔIÂJ dVÒÁ°5DÖ�f­Z�Cé'b‹ Ž¢T0�s-?F�ÊBl¶*“îRdÎ�3^�IÁ¹5ˆ:¶µMóʵ¾[ ##—Z ¯8/ÌxœÝZmoÛÈþÎ_±Uл Y‰®ÆõbG€ÏI€Ú¤ˆ�´×¢0VäÊ$L‘,w)Y÷ë;/»Ë%EÙ:¿½N$íëÌì33Ï,yú‡wŸÎ/ùÛ{‘šU>�N݇’ |¬”‘"Ne­•y;jÌròçÑÚMfr57ö/r%d]Ë­>�r{t:µóe²ÅÕ^Ïi¤ÙVJ¿»Ó ?:•¢�+õvD�3?h4?�J\c6ïõÀ¼tT"Î¥ÖoGé±Xj—‰ ÷;]ËznÌé?Å[qºØ]i1ßѸ36¼Ì·ŸËÍ% ÓßžN©aÌËÕ¶™G¿´�§ÓêÙÂm/Œªz%æë‹r!WU®>K£zÂ,° üMà§y„Pߤ*ϳêÄîúàã%Ä�/2oÔ $sq^kU! F¡0¥�-4�"™dk'õJOŽG<™%î‹Ù“ñP@Ew!JÄ,§&Ub6yg¥ —î‰>JÆi3ófUˆ…ŠËUV\ܱßÉЦ"— ´( ÆÝ S qÉ_#ÚA‹U£ ,*®³µ*Æb±e¡D¹¼Ãa†�+Ê:º I:+b*&bY%m¾h²ÜˆMfÒ²1(ìʜΧ&Æ SÂÈUˆe]®H3ì ‘C+âjÔk W4«…eU©"Q HT¶pÁ%¬¹v 4ŠvJaûëð˜Ûæ™7¬['¸vSðº¼Gg5Ðêukºb#ëDJã)-Ë„‚è°íR6#¯yÑ@1}.¡Øk šã1YM+�íw–Á?!9Q7©Bz”ª­HåZa–ã^QgÖˆ‡Ò´V¾h@`É&HÁ¦ PSH–®=û 8šÔ‘7°7.§Xqo”ª:âþŠ™Õ]Ò®BÝ™åÛV,šœ¸LH hØJ“ Ã3+ ÙÁøºW¦ r()kFX d™ÚW�|œ§üâC¼ð­P‘–ÔºŽ\ äsþ$‰�[t´¼z< °VÎÁ`7݉”˜xèE)tƒÔßïÕ2f§ö€oƒ©ÞØzaeÒ�*¢¬P£–ìïXìë&×B!¬4î `HJÖ»†cf˜nJ8_œ€™ ý× -–à ¶Uuž“´CjÔtÙh•¼é”TÙ0Jˆ¥€cgD§ ?ÀJ)Ú)&úÖ®i©UŸØ8¡ ÑSÆm=)È.i H¨ž›†…û–ߨ8ÝàSe}}XÿÈk™Õ�÷-«H­®ˆfa!å}6r½%ï'.‚¢ ´•%¬gà=6·gà WÓHù2»P«ÊliŒ5E/@´!Dø‘‚YÁÿO"g¸cËDïšgýL€%1’>¬“%mž0}{\ý|õi?a‚£¦K”îç=:£Eûù]8·£ü¨o‹ÃÖ@,kt~>˜ì ¶ž¸ž°õä~0aë­óì„í°ýNØ ñ©RÅ»2n¨ŠÓ¥§T)ótt­£Ã!t XIà|~¯ˆŽÊ=€É ³¿]&÷"DzÝÁä‚(s“Ã)ˆ�qô,.¼f{Ç“¢»YÛã~çL=žÆµÜç7Ñ8–t‡Ã€ïäp{(\ôT®Gá¢ßÌá,&×çpѳ‘¸óý?‘8kØCY\ß¿;ºMHÝPÛTåH®Âø�¦„86nCµt¦xñB0Çq]j òÆ)–g…rê¤Y’¨6òÒÖñ v’ÆÈ8Å‹árˆúq¸Ì €æïüîV×ñ� ‡ œmø–¯ÝËŸ£óñ\ivLêÖ¼QZæxϾRøDÒù_dé¡!óÇ& cÁKPx¢�4ÖoAÛúš³ ì(óÜ!½Éѹ8È"œZÞa(è 3a$Óè¨=äv"ž•-�ûغÆ›¯Ð9Á$Æê%Éc(q¡Ù$&‘ü8Ä1|9Ó!ò¤Aš†¾&»žÉ:‘0*G0:Kü¼Ø¶7q•çp Mmƒ%ø­U9bÊaX%™a·¥’ç­‚KÃUÑ©û« �ÅQËÃæ�èMlx"?´öéÝ ÷.`Ÿ´öé•Y¾öéÕ[®}zë<{ísØ~�¸¬¬pÆ.ñüõìò/g?M UÀÑüH4Ru9¤*Mðš“—§å†é¨'ƒ6ª�jÖ¿í�0(ˆ¾Œ  WPXÇ®Š‡XÝÔ…¥ öÖåžËñKuk"ÚYƒšÜjn]EàÔµ7Ù¾)ñuÃK~bÝVy ·[«-œüÄå@Ûm®oÚVíÓtSUe Ñk…ë0á³öâi�A:ÁŠ c&Z´´ä[£ŸwŸ‘?ßenxÔ8ÝZ@µ%Ž»øÏ¬è hÎttòÔº¢9rÏãlP;—_(’ÏÍX¼aæŠy2ïÎK>í¶ÉЩ8b,þâ9º³h¹¿f9sÕ¯SÍz·;¦ÏÎÅ�ÇÇ?´'…^î_¦˜½šýiòêûÉ«/g³7¯^ÁßäÕ1üÿ¯³•ª³XN?ªÍÕ/@²ÿíG¤#.¯‹Ls�Ãd&ò“T»Näq8 RÒW¹øC‰T[ r›gש™h¹F§_"…*˜^ª‘ÆŸü]gI­µŽh³¤EÙ‰Ð�¾µÎ›Þfïk‘9F@,ÊÄžP TDè6éPáOÁbpS—¦ó°Ä¶·‘Œxò ´¢ ¡¶Û…vl ’vìêþ�âññ…ëN ý½®VÈ'xúÐ7ÅÿBÝ öüXò4  ,ºgÈE´ó|!Èg•B©¤*æ$x"0xåÏï|Hó4·ë›!Ïí{íý„ÕŸÕ(<¶¯wÝÿ)o¦‡oÐ:Hw—BÚYÍ“Ðì¡–—u� б¢jø1Õº4K@Œâ›Ç ¤Šƒ™e ^ Û°\î)Y­˜ÌI‘—×�¶r{q ‰.«ñ�<›líßtÒÙ¯íû‹vIp·ŸI#–{I·àV·P¯¨¥èäûô·78v °1U“Ö¸-xãœlˆ`F†ý² >$�ü›¬·WK÷FS0/½ìøõ)ßM ^ÊÍ÷Ð{ŽVÁ7h É ¾<Ÿgó=û÷v=�ÂÐîøöÅNoTüŒ)í3µ¯×Où¥ýÿ§$L|  9 9�D › 5xœÕXkoÛ6ý®_ÁyÀÒJŒ(0$Ž�®t@³uh:lŸ Z¢c®’¨’”]÷×ï\>$JqÜîQ`+ ;¦î½¼�sÏ%µøæÙÏOo~ýœml]-³Eü¼ÄW-,gņk#ìÕ¬³ëÓïgs¬[i+±´²Fh) S«?DaÍbîŸd‹y°°Råžì=Z–Ür»o…¹`‡!‘-8kx-®fÒ "³åbÎÉÄù2]†Æ9V[VTܘ«Ùæ1[ü?ÅO �B•"Ýs±åzyób1§ovÅ«‰¹Õ’=pB¿y™‡‹¹³±˜·´Ï’ÝèN0¹f²i;ˤaœù]R+n!D•³5¯Œ`Ên„ÞI#Î2o¬”ÛèvmNϼ}olêgêäa¯v|o˜¶Ó ¹U©[YðŠ|p�gLj-L —äî¬bF~r§&sf“.8KÑ®•f¼a\k¾‡lö™€¡¥Ôc×On^=ù�m¸a�b¦+6lÝ5Ø]5—Á|«ÕV–¢Ì„¼UiÊNsêkÊþíiùªýCíýC,Rût 3Lé4V«ævùFdÈ(@`–2Ƶ�ë«ÙËaÿÛxiœÉɤáaƒ9j8¬)TU ä²Éƒ´#ƒÿ¶#;_¸ÉÿèÞôÿ¼ïBÄcþóÜq ’ç½Ó œ=>ê{Á|“‚ù°¹Ò4`úklô(¢è»�¨*Ù^æ,<ùéó>|¶ƒÿ’/Å=�üT5[¡­›ž, «Ð3½•/oÓÁ¡CÞÜ“þ¤MÑ�´ôƒ t kÁÑ5ÁEÙ¸ž½orøí—SïdÇß/®"S�ÈW#¯x�ÇÆ(Çc#9Ûm$´êÎXÒk¼‘Z575·U4:yr„xFõf5ò`,цç®}p«Î—Ü‚ÍÁDCL‚¾jQ¯„69e\éRèËÈe¼(DkÍýã›WÈGB?Ï´}L‡ sÃA¿†×m%Œ«t*Ÿ4ð†¯JkMKëx•;€ ‚ÞƒˆÿجÕÙ˵}Æm$ÆKV”Çóaã§Pæ· º±ÕÞ}‘…ªW“ýÆïé·2û¦Ø€812ÂIœ7PÓjl)GCl‹R)}ÑOË;ôîpK5ðžfnS…Ë©Šˆ, rVJÓV|/ʘ—– â�¸Kã�¿FD:ÈA)òÁA�[^šž$MË q´nÁ‰ˆ%h— ”´5©ã{@¨4·aom�BŒ‡T죷x,ùà!ç] ‡ôs�Oé ù¡IÇvëµü(âçÝ£¾D§Õžà Ù,æèΠW‘éñ´Ç–ëÝ·DUe+ÐUÝc&V+ñÌ…qºoÛVUŽZ1Á€úr¢‰c�•M'í>¨_DªÀ“\÷”#{R9)¼ŽêBŽã'ŸÔ&QŠ«ÆŠö¤C£>�òÔÕx%Hª M}öVÒ”ÆÇí§OÃPpëOxDMtÁ�`2¹ÄV4wF‰OBy‰7Ï�Âx·l€ÌqœWr¦G7/�ËJõ‘$°BMÿúô»m^ñ•¨L’ýÜÏ:×8ë.(£ˆ|¼™D9IŠ»c `¼$ƒ¾¨J: üÔteô àm T´ 1L�Y0~ú´M)¢¿t¼ÌíëO¤ÕŠr*›»`ÁfÞøW~é�ê%‡È 9zí0>dÞÑîï ý¦á~—ÜC¦J¼Öð{6B ]Ó %î„Oýµ6Ü;z¯XÉå}I˜F¾˜Cx¢1 ÿnĵˆŸÏÈ:Eé~Då;OŽÛ �Ã6Ò'Çm|ÊÛHŸ·1⢱‘Ñ£ãV\ùÆÚn)Ñš»ªÍÃ[ܹ;ü'Ymš³ ÛáÛ„ ˆ ŒxœÍTÁnÛ0 ½û+8Ÿ6`©°†BÖe»mè½ìT(6S “%C¢Ýe_?ÊR‚´�±a§dÒ�äÓ#MùêÓíæîû·ÏÐQoU%ê–�ICÓé‘ÖõH»ÕûZ°Ÿ YTq‹=:Òa�Õ1b”"¬¤(I¶¾Ý§”ïT«IÓ~Àx ± ª¤§{\×÷ Z{ßš8ÔJ �r\ªÉ)’“Ñ—ìrôºî®`ÙøUB]ƒÜªŒÜ*x-'ÔFŠt¼‘b†I1¼HÁÏŠMz–*…E 9Ö�[ÁfŒä{ó [HVïÁï ¥‚ÞÇ‹*c[3˜ôquUçð»Î0§Ñ5d¼?a°^·nÒBãBfšù̯ðh¨ƒ€qÀ†€üi¹ ´;R‰@šÀiq7"_oî¾Ü|k~ ôÚ9 †L'—z!GJw^fAcps! üI\"ÏFRaÆ‘q`è-óúÓ­Z�Gµx¦³Ó·:;0ì?;3 Ïý�ƒþ¯Ï”–»\•’ÿÞå¢,Kµ9þ˜I¶1­kkÞÜ­kÒÛÑêp‘¶F­Š•”—‚aO±½‰‘]°ÅZÀ²ÆiOl±°dz Ú=ÐG{Ÿ$#o ºX ØàwÆnx´ð8±¸O"N<'1bVR”Í(òÒý i Ù¯– ¬<"xœÅZmoܸþ®_Áºhœ k¯í¼ÜÁ»1à³/¸Üåì v®í}¨Á•¸Ï’¨#);›¢ÿ½3ÃQ»râäZp`‹/£™ápžgF™ÿéôüäòo¿g¥­«£l~ ^À¯ZXÎò’k#ìË­Î.w¾ÝšÂ¸•¶G…Ì­T ×+¦¿‰ÜšùÔÍdó©—°PÅ åíÜr»j…9dcaE6ç¬áµx¹•«f)¯;-NãÊ­£ù”£¤ƒ£‘YØ“-Ë+nÌË­ò9[øÙ�G [sUˆTƒù-×GÅ|Š¿ØK6_Œ ]±Ç´ôF¬.a§Û0qÛoyÕ‰~ôÉ|Jo™O[Ôäˆ�hÁ­`<5÷NÚ’�0Æ›‚‘FÑs£˜h¬–ÂìfNJ!oƒEµÙy¾å;kÖLÑÿËuϘ¶Ó èÓ ,ü ‹Ä14à¬éµž°»R�ö�-˜ZfN}¶©YìÖ“2aGºaMË d<[ó±{? —¹«TœS—Ãlt§±æÈ ÂÊZ¬wš£;Ö†ÁÍ¢)¸>NgIå ðZi<? aøóñå›ãïÏsÑZ3p£Ð†~³åpÂQ(™ë'$&ä~’ñZAdÁöÚ±ËÎHYF5äœoKC·tEÑÉ‹Bi$‚S@ÐÑ…tF�§~(c ²r ‚_nõ `aâÏE’œÓäÀÁSãû®\N¼]¯ ar-âê*™_¼KÙ1ïFþ»/‘æ$&»Á»Ò‰ÍWš®m+Qƒ_Qj¸;Ɖ÷ѸՇ%¢y| i=;iŠ1”K‡¿ Þ._¥ø6�íï÷€×¥h’KH’mGWœß‹Â'l ¡·ƒùNñ` KôL•Wâó޼ô8åŸÏ¯�Ë–.AôðE)‹ÁñôCÉkx%oD;!é ¤aÖQþLgL’Z­neỳ@šá…Œ&à¶�Àg}–ˆà#©�Wz°ÿ]DiøxÜÁ-)F#o81{¢TërbÐ-%$P^�§¯§p|Ÿ§@gë¿ ßsÞ�è!“[jUÃÈ�çg>ÀÎ[Ñœª¼Ã„ÀL‹Ú˜RËðõ_Áè†æ|Òu´Ö¥}§]Ib88†›Ø¨'Ä 0^ð �¸RxÕHž3Úi^ZÛÆt “Á>ô‹B cL¹Ë — Röi‚9‚ÛŽb"!(Q‘´æLuŒ/Kø5ÞݹfãÎ W:Ö U•ß ãêZ`þ:ô§Õa-TÉ#ÖtõBhCŒ0hKDÌ+ ¢ÄÇ% ±R˜@\8hnþùüé|*jG+ƒÍì{ñ,Œè¼tØ!¸jÈŒF\é-Б¨ûRZçUð£S3Т%,84¡ J‹Ô0èƒ�d«ì`ÝÀõ3†d‰œÿúâœ}ûbosZM/U€@í ñ±� oXëÁÞÁ³�½}øY£RýÄåþÞáþøŽiFÚk~äÍ®OÙù}ü ýAùw âðiò‚Áþ!“›y# ·×mð&ô\ßD«~]3æ¯{©þ°‚½™z×39Ý�`!èˆzâFÎÊÍ7]ššöb÷j(¼Õ²v4ÉjøW‚©…-_n<ÿËÖÑšÏæS[¦k¾‰k7ºESëþrÝ2] E»Êüœ–ÓÛ‘™Ø"]ûM²–wVÅÆc<�B,yWÙ'àÞªr<ƒ2ëÑÜ.Àuª ·û›Ù(|wUa’ë—ýè,vp÷öÆÚmXG�°Hv�)èˆO9à—Ð(ùƒà1›!Çœ—ö3½’Aßç3}ÌNiç_r1×(a½cE4ÛÞDˆí€Ta¹^b·ðÊ!ŽU96p¸ž$S<ØÃ³!Ø™ 9Ýî�«‘Ýçˆ^«ÍÒ7=ú½Sv†³î¯$ög.e0Û÷€�) 9uÀ‹ƒÔøº|ð~ é†­wQpQŸ�o„h¥‡;Âxp@7,8 ÷þò„D-—ÃH´äQìøÇ�”æ¡„D®€¿?R‚v*ŽEÃs[y³$1+N 2‰ž!½QAƒÍ3(€xiN»¶’Ø‹úI¬ÞuÕ—ßB+?Qóñ¾‘¿w½_µðÝ<-Zì¹Ôurëy”Â�_í¯Ç:¿:HƒÜ(†ÈÜÈ–ÎÄá8¤Ãõz@eeõ­xuávÄ"ÀL=±²šË ƒ‡xš[ÓôïF^7Ë‘û£²‘6Ôâ³úoi˹~ éǨí‰WuXiöÒ«BÚoB$- �›ì‰Ê°7ƒf qŽA7PªÇ:uæ?"ÐZý÷�‘Ø3%Ûh®KФZVA ¯¨”H.€¡p©éãÇBÄvÍÇ–ã#mg}8õTgÂ6±ÌµzxR~ô™±‡÷PÈà hn“ë·$‘à 5è?ßßÅ çˆhPg“ØM:°2ž—B(Fm1ÊáKqÿX^zª*ûÆ÷�P0¹iÐáÀ}ÙŠ¸JØ B ¥�´àÊØ�–ˆo/Äf#Ê•| žß ý.8Ácv§ôÍB©𵪠µ…BÀA5MìCøÇ{ÊK_ð€<ì3‚ŒŒ�Ÿ^P׽陹˜؅5øµ ¦´P¦†µ«ÀØ¢³1ŽÖk†ä ß‰ZY—ã\áæÊcðÐXK¯OƒžÞããš¹aOáb„j”}W©»€¼ úMا¯.ž pÕˆÙè¢PšŒ¶ÿ&ç®vL.¹«ù‡ŸË&T"4àg¥íÿé :Õl|AZ ›ÁG¤��Z—NÁqåè°“ÜGòÖ ª×?!•G'î#‚ïÒ‡†å}¯›ImƒÌõ æSÿŸ5¦î?�ü΂ɧM£Ú¾¨nìW”F…©Lavò_TàÙú‹{yµ µ¡7¶â<Éx7(-øÃ�×ý;<ì|zõ§M]õ{;K̯ô×iàûo‡9 Ï|† ÇpØíŒÒéþv|V Œ/0v£äX¬ª¬7ÊN}o­!GT£âÖÈåéƒka*t Ø&= ’µO@¹¶—°Øÿ�æ%:·…¥�4î (õA¦mB$­SSÂRŠà�J0(å‡ñg°îÂêP?ªB_›WZ�^ôŸWÃŒÏÕº®2·W¬€ƒz =y«‚Š€$¿«À=ÉŒ> `ÌaâpP¯¥}@^‚Jä=x g{pþí_<»1ª(+çÇàj¸ä3ïÖ@>€¹4o Ëap­’[Ï<EjäÁïèì18""|£‰ìR ž4>MÇðy§½… }’Ý(Tu‡š S»º—„7Š6a+ß› R.,v-Ñ·1d„ÐÐà*2µSªKs��Êå‹ZbÄçlUç¸{­åŒÃ~ô¢aƒ÷n)!Í&-1ñ‡«¡iˆî•ÐsŸí×[fû8@`Çc¦�ÎH‘…? lpÒ—ñ›yï³Ï‚âZà™¯q sÛzf¶²~�% a<[ÒŒ§Ç�IØy0"Í4’4±e’cO°]DÅúrð¦ EWÇãÕ@G¹C§Li‚9d"<§-f±]šZ½qxaÅÚ€�=¤ •Ÿ�±Ñ$Y|`×”o")qç’›¯(XqBøÏÅ+Ì6ê f3„ñ�a‹_Äl9� Œ$'¬¶E½�@ÅMÄœ1&b¤[€ÿØŽIÍêFÃw<5?š}&æêši2 9t­M�¶A…Ž&Žm‚�RðxhÏéƒÔq5XïeŠ´`hxx²ß„[À¶¶ÖÜÿ>ŒL œ_ .ÅÚi*VvåbaŸ{Ä*nÝr.l£88ĉݹq«é>Öä îuÙØÝá©ñÝÊn7­Ê ô<* 0Ùé¬ç÷ìGÄuò59h8ƒvWX€DÇwc8AØúo»äUªAàâ�»�¸Úçeƒ5‰7¿¥\UX4jª­�ÀRfq¥R®×eU/ÓvDaSÒåŒýlLè¡òn¹h4x€Ó_gèk¬¨MüknËõÍÍ’ÞÅãóãÆWMn¶¾µ ü·ä¨ÐË£OŸÿ÷‘ax» oüoÆã�s|ï¼5óS#YÔÈ8¯á•,|”š½¦k9 d=‰È\غnÀ¬ù=Qݺ4b_�r åwŠo¢´?ɤŽäœGhº{‰â6¾k³MÚ »ôê†�=¥ÑÕèq¤�Ô·�ŠŠí<„?æ%XÛ-chê3«þ¬� 4vQ» ú{™T(ÁH]i V\»ã]aX6ôÖ qç|b>�gŽ|Œ²aÐùÁÆuKðK ±KÛÍê”j¡`€˜~yâxKX°"·© É{µþZêe11Wö —À;¡×ïšØ'Z²ÁÍÞ« ô¥žª±ÿŠòC-Æ|a£ÌÔaÛÆCš A.ö�éS»rñ÷¿uq[6ìhЄßh ¿Ü)àAêŽ>QÁ!Û±ЏøõŒ7·Ák›.m“á^X3h«ðg�Ƈµ"Ĥüþ·gó(üòö‡³ sÔµÿîÌ—õC*þàýÙ59—i³^Q c3»mï�äߨ,§èø�5¬•š—•U!æ8ilPq>¸"”w²›ù�7èQ©:%Š-ŒŽÑhÐY&JZÓ[\D@uã—&n@D@ÿ"Øtl*êí.sÞ%à�΢5¾­}ËÄxSðÈÉŸ» ÊNhüÉ=kNw>ÔfJµ0~êJ:§øÁÄp‡n_<ú‘ðþD12�FØá´ê~ÿ<›Ô'}Ìöi· �ÚtmQUtÅDÒ 4N žá>ð¤®ÖeÝi×'àK,ÄMÖ‡‡û•ÙÌK�Šž“‹eɸ×МÊÌ;4d6LÖs­š{ s7uw·ô³ý-ö±1)¿u-O*„+Ü.¯^:íÜ]ß@büûz±ÐªMàFîk–6!  ·ËÒ |#>ƒ5¦X¼²Oãÿ/fªÚ W ŸJ¸íjCz+æÖò>ÿ¥º#ûqÍ™âl?âœÓy3PëM àn¸nf½]裿Zïewä- §³¾ñ�:­Z7>t´l%Jô•Ÿ`ê Äp�Q.çZq^•Ë Ç�2 öêSè¼Z½¤âé¨ÕŠyýºÅ+£}áueìÂø¸¸0�Õp 5õ"qŸgâé¹³çöRšüe%¾õÜa6ä9Ÿv0À\ö-ÚþóôÐÚu_ÍÈÛ`–ÃÔûöĪ4$“â½ã"vÞÅ”3ªÚyHF•süؘ¤¯ýòÖ£3ïcuFsþ~,çǘ?þ‡>7¡Ä6íŸÄçþÉ`€&b´t1„¡äŒ×V1@£Éstš7N5x$E‰•æ°4’JÀQÙól\àÝ1µj†ÓƒXFÃ’LÄÃ{ ™5GõÓ® ]3ô�>œŽe ½ñ‡ºyo¶0%ߥ-.pï4¡I‘|me•g(2Hš#T¦Å•LßãqÙ¸@ðå¬�,Æ¢¤¸ÿ¸ K¨y2¹xöÙ³ ãŒö›\昣äM5^ƒ Ñ+-…¾šÕ1‚_·aÌ7_ö ¦ijb§ë« €ù'.&Á}ø-ŠYÓè¡)Þj¤Yºv¡É—|ôO[¾dp°­d†d;詇»îcŠò.á’À�¥ �"H¾D*ÙÆÖ¤•…¿I,šÅAq£µ°Þ‚\@†‡dâý^‚öhI‚Iü8hfD+²â­Œ’<"Z�H|Ä~£äf¿ÑëËâ0& ñ.kãÇš÷/†N4˜â?$'64*XŒh9L2Q,™ ÌŽQ½¥Ýf7v:L~ƒîIM¬ ød&V*ëp+‚Ð1Ër¡ë`Ö—^¥Ùâ<�{&¡åà *¸. t˜†Ž¯‚Um™;ïk¸ï=fçj̾âžAy�o!¡¸UrÍ€WÁâr1íêÅÄWú�Qÿ‚O1á]¾¶oßܙևºCgHBn��^�3Þf5" ´4«š¦U}€9aÈkcBM¯ð»ÀßK„‚7f'XX£1v{×ðÇ�ëU+›»wS52·ã/ÌŽ÷ê4½y'¨3�™˜à»°ÐÜÂ0(ô; ö‘¹Ø¨ò\Õu˜×É÷أ߸¢¾Ý@“[¼·{ÏÝÖ@lqk8> ^:tÏøT“ý»4� O½�sÁGù¥½�m!½£ AHWM7ì¾e‹Ë“>™ë2Éq;q™VˆívP^î>íh‡3>wÁWM]½cô…§Æ"¶zj@ý­ÿ#C޳ƒ«+÷!-3›Ï湋¼?Dq„û ®ÜýÐ6Rô{]�3­óÄXW”\JN2&a� " ¡ %`ã4ÑÁUYÀ|¶|¬ÝN-Pñ¨Ô¸4%@âÓ{o|“¾5—¢§ïýMŽÆt³Ds�0™uün  TÍ @c_ +¼ÉSõ…àïŠUþ܉RpDÂ,'KIds×ÑD¶õ@'°ÉNJ%ä]_›„æL8$a$ÖC‘�ÛD ÿ™…k,P±ù¦¾÷1ßÔnŸð7›4Îlʺ‹ióþÄ;Œd„*èÑ›JÁIkÃIpu/“ý*4›Q#O%Ù½u3�ô øú!’WQ‹¯bÔûêûÈÜhš°·;{ûþ¶ö 9·ý¼€¾Ð ˜ƒ¼Ævh5 ½tZ½•¢EV�Jiäzg{ï'+ zµ1ViiꇡÓÖîå�p>]€�ãYêGOÈÊMRÄPÔiî¢äPPsØ~)Á!ÿ*¡ÖÊtF¡Žq� H’`–˜}دM …]ú®\ß± úAn\2Ó–uA¶Aº€ݦ„҆Ƶd¶&õCå,¸éZ.Zà1®®ÎL‹b¨–C²óR‰$b`EBGWôÛê¹ñ$ S>íÌ£Û;\= ¹Ëï±-ÿÚ'�þ¹«/uB²uh”¢f¾SÖ¬·u+ ôš—ª#’ƒü*ÔP€¢ñФ� µ�}L3s‡ÈSv RhJ#ʒň¼‘Á€TÕïˆhB9FGc†ýgt½B!„®ÇÔ«Çxm"à�Æ.–“¦ÎQñdwhØqá âž~czTƒ„ÐlЋ֟ u ©.OäŠ ¶h°ó¥çÊé%O ЀÁc·¤æ –ª"Nû ™°ãÂ140¯&ÆÙcí\-å}^6Á²Îhg^…™àä7UZt™ú–×$«Žcn<ÉÔBô²Ð¤é”÷Tâ¾(Wí:~¿ÿú£_ð‚Šɾ€éšzý¸P‹ÖSq9¾¦qNí\ŸŠ¾õ:ï/@ÂyƒÓÍ>ÁÆÀŠ_“[ëüˆ€KLeWuXo§ÑMÎ+ÊŽ€¡rH VbÞWØñ(žM�&>´¹ÛâR�qd*@ð‹î#û­Ðv¤k1¹b'õŸ­bÞue ÷Ž®Ã¿†øIPôlj�5‹¼-чâ|õ‘PÜ6² ,‡²BWyÊŸ³DFÏÔìIÜÖxÚ8ry;AõdíKz(±7#†•A`'EO¥êP@Í=r&�ZÉø;Ó<ÂÇLúh§"‹1µ.:6 h,.6ŠðÅ´›@8Pƒ­×‹µ š&ÏP¹¬ÛäwyEê;SÍ31Jý(ø‡L@IŸ„bûu£ó�ÿO0{ ÙÅV̼c5>d+„1RRäïab«5h¢‰ã·(k£/÷#š®¹hÂfÖEžF6‘@ƒµ{‚ñÓr�'9Q9Ñ$möð—,dcíÓ{¥VšØš5µqÁ³Û9ûîóEn–y7î"èL8YM¢ÀÕ7ô>ðå½Za¼²X3$§æœ¸X�P%omD ͶuûìØÇŸzÛרÿI (³èiLf ¡6,�žw#&Üö:†yRlÏìàˆÇy8=Gü{{8ø”×6E+ÂD‡˜ –CáÌT+óB›Ú�M¸±ùr‚â¶Ö"Â÷`!DbðP/p wé:º `†6«Î²§Ï¹1L^ŒXÕ��qd"ß8ûñôÈ8ÆâßGaà(ÌøkØŽ[1Âvl{Äv­|dŒ}dÛ?_1w4ŠIÐýº.Wèg›§�4Ýæ©FÈÊh•Ýc#ضýUÄÍÚÇ‚îi€=ŽÈȬDˆ™ÑddÞ½2Ó® ÍÍ'+Ê¥äÒHÎ&Ù úˆ�Ø£øZLÆÔ ©‹ÝðùŽ!u1S ©�ápªNeIAefWÇ •>¾f®•JVî¢ðW³<¸î.Ãÿ¡×Á5Ýf#�æ#-ʃk�¬î†ÝTÕ/Ã&ÖŒTaâ]žBsà¢â!¼|Í܃çê¦ÿJÍ ®ÀfœÑ™úŽÊSE+¶–Ù¦Õ‡qìä8.‡¤"\H@mòÚÔ­ƒ%¹f䇾“ƒã ´àÐþæ˜4^�¾i;DZ/òÊa\ž1•½Lyp€ÊÝ’?Q¡¢f½wN š?V´ÿ?™¡×“™¡‘Üá0P»ÅWO” r™ ñt© ^â_&ô"9ÌQ±–ÅÙCžaî&Ê =&-´¿Ó…¥K#i!1žúOZè?i¡ñ2'Ò¹VT*‡ ôלú7p4[À<aßÕt7élí|dwóPJçp>"›Ó#øŸQ¦5ÑÅ!•ZolS}Üæ’KìåZw÷PבüHa²eB�òq§âtOáàŠŽ×‘Q‰�ås”³ÈÃeÏ´WüƒMr²©ŸjÚà �zÂâQ®p/ÕôT¾¬)Ç&¯:`�Í-í“ZšriÑ)À¡³=�yµ²ÂÊ,5tXÁán¶Q÷x‘۱"¯”l¢{ÌÞž×æüÄ­ S¢9C½ë°¦$ôÕÔ R@‚®l‹+Üq—%™ºÏ ’^Ž�Y$”ŒR:Z3ɧgøÐå4?@æ0Uin3ÂÇ.ŧó·3üä½[�îù,¹xë<Ú ž¢_»*d mDÒ¦=l  ñãp0P€÷.𳌠WuÁpÞ¬sßÅI¡$•øÁO^8;À’¼À¿zÍÂL/q¸Ù)Ìò—U†nþÈÞ~U„{Ø=K)6IûN9>>IùÄ~z•\Æ�)V£ß8ÓÚ�¸<ª œ7µÌR°^§(ÏCæÉ;,XŽ}²‘bÔi·Û0šñ„‰HŠðÝ}œg±Ù{>Ìy6²þ'^#dû½û±“׳ɡìgÔ&ÙÉø9³‹t4d 0S0¿†¾ßŒ⪪ŒåZ*M>qc„@ì"Ø–J{äR·xõ#UÜÛr©ÃTjßÉŒ�áM ÔíùSá¨ãnsìK¶t<À—t üš½ ‡‡”v©p,ðxœÕZïr·ÿΧ@õÁvf(1QcOÇ¢8c[V£N"z*ÕuÚI5àŽD„®Žó©ïÐ7ì“t±ŽwÇ£DJbÒÎÈ–ˆöïo»ËþîlüáúÇOÉÌæbÔÆ_Œ¦ð+g–’dFµaöô ´Ùá°n¹l”¨¼à‚¥$+eb¹’f8ðOzÃA 0QéÒÑûf”RKí²`æ-é:;zCJ$ÍÙéÁÍMrË–ßQ3»¹9 Ô‘8Õ—áÄ1¬$Ô˜ÓƒÙk’Ø›¨”�Þ’áœêQÉ¥}óípàþ&§d8iQ˜ŒÈ+Üh¬ö»¾�ÀpP<ñd+êðsí#¸¼˜1!xqÒ÷GÎ/?óîÍ·7ïß]}ìä;:W ÁôþõïK%üú¬È è K2¥‰÷9M,ÓdÎ K~øª5]•;cùéy¢)ŸGursøúùü‘Ï™lœë»S„ –3i ¬1ä PXp;s¹&‚É©�b,P‹G&ª”)Õœ2avÁ€®ã� ªmU 2%Î.Iy–1 ¼ˆ)·–Ë©‰[ @m¥¨! •€L°dÊ*µŠÑõŒ?”€• 0HÄ,ZÊû*ø#X™@ÔÌTŠ|^óÏR�TØŸ_hüå-ÆÌ9Sär|MJÃ@4àV1I¹ˆe%Ñ,݈Ù)òÉ‘"»À|÷¬H¾�Å3¢Y–9Ó< X¾³ÿóþÚÌòœµü½Z¾×ßµ,üw´ï� kÿ0n|Î2hPZ²¨,ë®ÞS¸u·zdàE|¨>`Ýl] W´«™§P÷_cB½Åí_®?@uàv»"�Ñdk`Òã’–(™š>YÌ8<‚z`�u/¨ …–·Ž˜+h¹4–béL4+¥3…òº—•Üb••VÇ ÃÄy„%œáðÏo›Ðäöˆ� ‹¥N’°Âš,ñX¯$תR1‡ÿ Üàn&ÔâN(¬b@`* d×Ðø'¨ÂÍ•O`ßž£ŠRzîT8‘3(â9J •E£É•ív‰PšŸ„?œýS;ŠÖ–xYyžÁ†tôi|uñ%Ú´moh@è-ðÍ òÑÁ- º)'Öõ€Š¾oG ‰è9\¢ …R·eAPOqD€aÚ”ÈÑ,”áwë29ÔÀÔ hI»Ì‹´ Ö“±WˆÒcªDæÞÄÙZB& #‚䪴àñ.!5û¥·[I¡Êî )AýZ°ï½Âu`¬€6 €íçÕªI„ßIfÇC5‚­)÷Šfe «ˆª‹¨“3S"í&‰hŒ„(,@ÕXä}Œaݾb×ǶӉø¦•£”SvÁ 냛ÔÔS×Jc:™h6_ïŒ.ŸÎì¡¡sgèLÐ)2G”úSœz»è«uÜ¡×.$ä+@„“^-™xÛalÒÊ iÏ À £^Gs"Ò\´¿ÔÙ”¬wоà?ûÜ( Æçç±L7B¤‰·hdO 7£ò†SÆjÓ “å¶ñ¹àsÕ\Õ©è¾½FŸ¼)xUy&”2 ‚©¾?ûáûè¾ H>W:§õëÁÂÝ ï¿]Z’ûF´9vi�fL[½J˜Àq]Ýx›¦| ¨^2ª >ö ^³È ¨œ–t Çl¦„¬±[å½›ó?Wz¾bya—¤j–_š%\U™…�ü(§p‰Ì¾‚+è#Æ–Z’9%C•%¹<œ,ß ½Ã Œ©ùÝ*Î|†i£…ÜP»uW—¬OiÞ{$«º‘¯ƒ=ãL¤}rI/W�7J±ÚpÈkè�¶Ì£ÓNIDhD'<œ(aø¬ü‘ÌàÓ$ AÂ9çö6„Ad¥=ôÀ²6É"8µÀóºŒW™àð„ ¨)î !å�‘n=å²UÍ+¥(28Ô6 ?‡É-@œý„±GbæÇ–ND{îQ[ß*ÇÔ*M4é‡ÖlµN®j2¾)-¼§šMœùЦ_”|¸úLÜi¸„‘|´Ó êb¼É!•¼Ux…}+k'îŽ ?lc$t $[6®­ïlãIû|‡Qû Wlgâpù5²#Ú8Zü¹,\ ß÷EÐ×65‚â1öW©éÄxm}·áž[h˜xn«Ö6¬¸¯xZ·kƒÝÃ1ð[Šïúcöi]çîB†¬›…ñù$ЏY— ãÅ €w��G<|–ŒÏ®â\ì(Òq"ö¬Ø¯D~¾ì$;³ ®o‹îZV ç6cã1>u~œ~=ã´£°æQé…ô™qÕ‰)8àúÂÐýÅ-õã‚É3•”¹ÿ®Í�ƒr©xÅv�©øN˜»Î\\°Çd¼ž\ª<Ò”`Çü¼ï¸½”¦îó 8j÷q–ñ„! Éè}"F� ²ZÌ+äÖÀú\8uŸ9m:=:ó¦ð„²,xR‚Ú5ý,4·èÄçò!�Ë<,ÜpF»6'ÌúZš8’-UNüÔ£¥OÕúÑ ’¯24’ebCýù »®Ø;¶æ:žc)ÐXsïÎ0]éII^ ˽b ¥o'JÝÖ5tc68ó4üzÞŽ$k€G^uTö¸æ™Çúg£äqÊ‘1Õ˪מî mŸÆ¬fM„6¿î IªƒY®SnP€ßZY[¤Í%.3I»¿¶ S¤?]�/}'Î%Þ¼NZ?BwSI?¸åa|�»q6bîuð;IÔäg0¾÷²�PZjV½aC(½eK³îiÏ®‚¥ûæ!Q¢Ìe­Ý± ö¶8ˆè“´,O¨Å·p üW`\Ê~–Õ¦Ò÷†¨¿r¤Õ¢ïßÚ˜¸ùFŒU®@ÙÇ’dxÞ,ñJTH.Ë™?kl T'lé†GÇÿxý{ox—¢fÊÙ…ÝQàŒ`¿ŽPȨ0Õ²PSÐ[“›~mŸ,…¨N“¯ÝTëëšO¼eâ— Þ1`³íð¶ÖÕ!ç¾�P Oƒ}5å§]x¶šŸI%3.!܃©ç1N Xœ›�äèg{ßzP…±8¤TÇMÿ�+ÃÍÞëG›ÇM÷ߥ«ÝáBÝøþLEìØ…û5šöÓG •v‰ßb®S^åšv¶Z»ƒáª³vÙ^2LÏ7 ¦þêòxL-Í,ªý‡ ó¶ûg4Œ&•΃s:3hc˜ù«ûJK³�j\ éGÀùˆôNœ>Ôêó¢“ÎÔs²J=mB•ø.=Å#³ð"¢ŒDŸ%øëþ†¤•âr÷B[ Æ+hÂL' °$`ÑŠ—ôr݈2«œDÝWS.Êu‘£( 0Ç€ 8~ÆÂˆÓåÑD•"Å/t',Ö'� |Td=ò}Ë·uÜ ÈÿbóŽâ ××’&¤P<0xœÍ›moÛ8€¿ûWð|öhb4¹-Šƒk h»ÛâzéÞ¶wÀ} h‰¶‰H¤JRIœ_3Cê­¦cG±å‹ßG£áˆšþåý—wßþ÷û¶ry6M«ƒà)rá8KVÜXáÞŒK·8{=žÀu']&f wb©�Lx6�øK£é$T�ët� ½œ¥Üq·.„ýëÔ€¬Ñ”3ÅsñfÜÊϦŽU/º=ÀùhZ°$ãÖ¾¯~a gpê J¢SÑîj:ïÖžC«Tf:)°�{k _3# #¬PNªe›�a[ç#_:•·U¿¹=ûe`¾×N?t…ñnÛ–q®é»åY),s+îX¢ )´JGÌiÆÙB*é�cÑTÚÄ8¯úö»[ÉdW› &¤[ ôI¥ê¿­ø-Œd-årà‡FùÂ@ÆÏ�bJçXøœ±OŽI‚‹…L$ ²;€’T‰±¥Ñea‘Åè;Ë$Œƒ9>Ï6å4uOB÷c3sm%:�Ó¸;De�6$Á2iiìÍ�‡?–ˆ,ó¥G” jÉ�ߊjã0R(¦ÊFš4âö=—R¹—¯ªN‘[•*•‰°${ì»é„ó­ueè[ÌáŸgV{d&2‘Ó ëň•Ê‹-íÔóó]Í[­Ô!ŸC2ÈÉÙRÞ 5ªê®i@�‚TŒ~"9Éæ”ÉŽg­“ùo`W±HÈ ú`aÂY¨@7ƒ@kîj[nØ€a†…öÀ`2|":Au6Í=L“µ˜�º…AÀo�\œ½oqÃty?�Vi0eŠŽ=Ð}Å!5í)"ûcd_qHdx*¿úûxF‡ÀTo`ÞË â½¼èÇ{y10ïËWÄ‹ÎOÞ—¯æ}M¸¯ûѾ¶ Ú[öUßrpý-ƒ—}5¸\…Ë Ãe_%.×âÒ«qyX=žT>à6ÿók™çܬ¯?)ïÚ€G¼Í�ÃZ9½|Ëm=tC¾<‡|"&�÷Á E~tûÌÕ^lÍ qrEÔSü¼¹ …Åï0�ºT. c•Œ«Q‰>j’”°F„åŒÝä}¾÷ºq&ÔÒ­Æ3ŒŠ“r6jÉÊò…¢žÐW¸~÷~/•ʦ“1œ«2ŸcC>°ò°ÐÈ�ó‡�0tˆá}ƒz†á…YG=†C¼ë�܂ޅD|u‡‘Z™® Ã\ZÔíbÜiZÿ] üR`ó87Á®ÒeÖ\îeT£mw½"[Å„À7ªÓщ`µpć6{Aê¬Ì!R*ø‡p¥QÌ™R01ú�d¾~?ã°©È G²¤D_Nª| ºï%aB¢�Å £êÇâS5`¯ynRØÔøp¨‡  P †ÇÛÕÚäsd>À€#ó©þŒ�‹cÝg¹@;L””ŠQþ*U7 Ý5ÙV¸c€ùÐ2‘ùäV´* ÝÆÂF…%ˆ’[Íè¶É õŽW토¯>y2`]óˆFß!úßPùx¦�§›Ã§úcúúÇ@Ô&̱OõEôµ�ˆ/üH<{"¨·¡þÅ!Zí£ñÖO Ä­Oõ�"–�,�×|$õ©þ ¹‡v:²ï‚ó¸éËV9Ïug·õÐOS¿À ‰˜Þ¦iÛ�uúùoböAË…Y W'cxÿÂ̇ö4"×·�°IÇÕsÛŒ £ó¡(qgGEY¥ã”˜{IÒ¾ˆ ²>‰SRö)0ÁYóˆ!�gw 4's ¥Ñ!¢Êç`e#þÿÓÌÖ"#Hn®&﹦k{/�A‹ïã™øî\—H·î,Á·Œù`8K1ž-£A®ß(|�K+®p7ˆ_n�!¥]aËÐ`˜0ÝNÌS áKÓø ÓÏÂÚ?‡ø2__Ãx .²SQÙ]iD¶ÇݰÓ¼5Ò­rád²iš¼çZ€í½t}©À;�ÑÈÇלgº™mÇò`³²/ ¿@ `|æà¸œšO¤’£ éw¯0'ìRÃ@ý#�Gb„€~£|W7 U†kÝ“o%­£÷CðnÒ1Ö��«—†çlŽŽ•ÛçåÇî7Ž߈þ*30ß×ôšü�Íû‹½`¾Ø Úó†îÊ3oÕ½úþñ¦¸¡»âfkXŠ6 Fî�j_n©nÈÜ<Îmc†ç¤à¸à¯~£†®úMR/Dè¨R[Q› ÐÃc:]ÜxÌ*MW°†0‹Ç=Ä{õ'ÖJ‚UÏü1úÊ©vŸH¦RAQ+GŽ|rÛÒª.€¯&@¨î.�xê³²‡¶ —ÊÅ‚V‚˜Ø†‹yÂT;ËO{��.܆*´òVœisBXÐU­HeuÜ«TÊÜ“qêÄ\Y ï=Û‹�´ýã>Ú³ewUÜí\·e¥Íá„DÔ��²Zá&„¤¦ “1"nÖ"Ä�ˆ!þ¿wHNhD‘#Ÿ?ÆÃW… ï� )öòý„…ÏwâÂÄ^`Xµ�p,>[n‰a…OCê�;}EÜí÷[F¬vÅ �Ô”ø³‰5‘&±+¹@S%£‚…ÌÒîCeÈ‹=¬C¸²òÒ�0Cbû¾¢L>à'E†+[hÛXtÕó¥ÿnJYcÊG8?á¶RëŸù Êåɱë^Áo­’Qǵ¦ë’õúàcmq%‡ÛùÄÿ‚Ÿ®…Ç<¥{=Û›Vº1i¿£ië~¦ú{3¿kiß…ÿ$|Ç;ñÿ”I“X 4�4ŽX�4Šxœ­Y[�7~Ÿ_áîà ¤„” ŠÍFÚBi‘ ±¥ªBÎÌIâ2cOmφì¯ï9¾Ì%™i² ±›Ø>—ï\¾cïô§×^]ýýñ7¶¶E>K¦ñð `9K×\°'•]Ž9™à÷VØf)·°RZ¤6ká*Æ@pà(‹>"¶äÑÐd‰K·ìÒæ¦399÷Ÿ1C¾~;Fúý‡Î»h{Úýx?Œ‹ÇWÉ'P«�éhðíÊìªiœÿ&kû¿î:08Ã~ Ù}fj¡æŠåÐŒ‚®u¨"VYÂíÓÕc÷�úuSsÄ'‡‹®¿Ãî„yßàÁÛÓo†BÂ7¥’±Nc¦)!K�°ØëÀ áÕßÊ‹$‹’eOf¶ˆ8öƒûMÞ5'&5wMÄ៷¬k8‰±÷Ä{e­ö„T‹ôé8~­cس ”œXÑ_U.\-Ðÿ"3P�ã>¦*[V¶Þ8Šw—¢Ã‚&‘œU]#Áå£NOn°ž0^€Ç›¶ÍÉ€ �.4æ;yOÙ³„ ¥Y×& -÷'}6Q[ná°Tyf|È­æ"ïD›æ;ÄÕ±§»€„Cȼ›_0 é×�oÉ@…(ÏÆ‹íØÿ¬ˆ°œ¹à>;Gàk? …·¯’“Yqê2åoÁƒàd#¶VBr´3z(’E…^„ËbgkËe�ëȇ�Á:˜zÅÐ Á­Ýqž@ËÛM;ˆ:8PÌûü¹çˆÞ6ÅÙq«½eGÝYŽjpÿ:3�Öt/ê èWÊ¢'ý�<Ùr·›ÑqO)GÀþ¶p34>‡Õ[ÎYxª y-äaoÃÇj ,¹Àñt Ö¿ƒÏ ÔG麕�ëY÷ tÛ¼`Û@¨WA=щPaƒ~ñ,®TÎuâßÅ4”H��óÀÓÂ^äYUÃâeUÉàŒÏžÏÆ aÙ›ùçñS¤j®—6rõµÉA�ç*ÏÇŸ•»¦z[ZµÒ¼Ä‰ßa•°e%S;xK<ˆÂ`t<�·±ñÖ&­UÄØ»ˆíÉ~µ\° „©ç°àª7�çZ».­!ŽS0ü+,Iz=ñA (¡:³Å«ýš^ðV”ò ú£�tWÄ)F-þÁ1Â[ê/T8a…‰¤ñXx&º!2ü²§àNÔ†q*Ê�óUÎ�›QüZiœ³x.ìv”8Ö>•j;÷ù/ãzJ7v¨·(?�àß�H^óŠ„ò¼hɆR×-«ç0aܰœæ¸9÷K¢´DÙ$RáÅ÷×ï9$u³%_Æ*šÆc‰<·ï;r2ÿ×÷¿<üö¿_ k[ä 2¯>8Ká£à–ÑdÍ´áöþÊÙìúîj Ï­°9_$Ìò•Ò"a9}оQ&Sš(¹áÚÒç­÷ϩݖ|> É|,UºEu7‹”Y†kÌ[ú¹ €Ì•¬à÷W­÷¯Z¿_-æS†Êf‹� g Jšä̘û«õ𨖍´ãí[:ß0½x˜OñƒÞÓùrѳ\Ð~É·aÉËùÔ˘OË¿Dþ$lذÜq€êoTÂFÄ·þ]ÃWûT‹ž­yž‹ò]Tü´v,ùÍë5cQ“‹JþH™ÖlKUÖ¡_pî [R±©l/Ìõ›« %ؽcj[H/hâ•ÐÞ^ë­ ]Ç3­ jל Y: í"ñ¸ÉIºäTº‚ƒ´ ÍÕ …N…ìâV|BS§™JN¨±ZÈÕ„*Mˆa+�ð,±\Ó O¬Òæ¥?¡jÒÑêÕ±Ü(Ôɤ õ¾/°÷70½z Ùq�x׌Җ§ÔIñÑñö㎃ðÿ®¹lÖ�¸H˜èÞ ÞMÀø-Í¡ …õ`�f"Ç/Và‡)Y‚@iN5/Ô†§ ÷ga .‰¦ ¹k(�‚¥57¥‚ÚeU¤É³Ü¾s2å™�<}¶²ï*:ðœ\ÚZR GÚ(©#úÝ–‚ær;ÁÅ`8,UµxK3p»VÓµ‘PålmeEí³y|°ä4ä&ûŒ°†Huøô(ì:øÞ�Å”<™'„$ý'p¿pÆz"FXê›"±Ïø¦Apű|ÃéÑV«ñœe˜PNæÜ|°%ž#}Ä3ÌHªÔ5^è6&1¡Îx§a9ì“+Ζ9BÿA"g:ß"§˜&£Á”¬×a�ªë9T*FåÓÁnB•ÐsHF:,ë�ÒMH)ËÐ[/ NïN }6ï²´QÈ…¯›¬æ]í³çxeΨ*„µˆá^ ­òÈdB¡E�Q�æ%8EÇ7”ßç:Ð  Œ¯Ì&M©ñ^‚=^w¡3àìfÄyYt*'ªvþ­ÙÒÓ•¹@ÙAîÄG^Ø É¯¨2ÛÜ’û<¯k'3½5â,R>a  ™ÓˆbC$0%M†D–LÒ\_—=k1û¤mÛUípÏEeú³�NÙw¿h Ë×ʊ¦ÆÆO ü`:ZµE�vÍlèÃ9Ä™Us01å‘0¹§ŠY‹²Û…b¥"ؘÁTÀ+«|%âe“ÁRÉk¼ª:“¯×5¶¾ž{Sº]±*°X7•„º¸ÄÔ, è°.ÃâÇ?:– ‹Õó[à_T*_@€*9ÌDˆ “HV€Å‹-·ˆ9ûºÁ[íj×!» 8>¾}% KRì�ø"ßÒ]ëÆU£°RÕhd ØÂt« Wø;KÇÚd«�C…MéÖ•Ai/K|• 3'NÞ/ÑO"¶)È�Ý.·3çU¶ÕÝ ú’—†Bxì‡@Ù)ߟI- BêÑç]ƒ8©!‡Ã�]¬b.Nd÷Šð3_òæ ®|ô%LgäGwäÌŸ ,¿q¿OQΠ¶Å–Œ¼êik'_–tlmÚo$”oë´ôÃ~a뱘ô�¬vTTøš™f‚4ÀÄ냶;}ÓØ|£YC׌áíH¸ÖÒŽbúoM÷ÇŸïÇÎO³èKŒB^=‡Ÿ;zA—¢7»þ¾2ÈW->�~YÙ„2Ž£µ£¨ëŒyqÆ G½ªÉã!"’­ÃpàË |k6·'•pÆôõgÿÜÝ*Fûg»o_¶&ªcõY¹e·J³ùD蕳�¢e]Š~�sfÕmÛ¾­Ò+…YÈ”gISZfw�qÕÅd 0 ž$jÄ;–Š<ïÙûÓñÃK…AüÂËqð«d]Š_�s~QÀ1üÚz>cü`½ýz>ÿnô¢¨KÁób.À.ì?]ØMŽ÷)á� m/„‘tpì�ñßg�øz6 âëÙh ¢¨@|=» DØ2ˆhòâÍí0ˆ7·£�ˆ¢Fñæö2aÿÉ ¢É?ˆ”¾W–×·:ÍÒJ€·Kà_y ³ø7_Þ\‹€ûD_<÷î<Y_[„ NúzöÍí7§1ån˜(w£ñänšÜ]Æ’»ÓIr÷�âÈ]Enf§ÄêênĶîFêëîÒÆîÎéìî jíîPow#6w7Rww—¶wwNw_Pƒw‡:¼±Å»‘z¼»´É»sº¼ûlÚüiPµ`7^vŸEvgta÷ÏjÃn¨ÏÞ¼Ù¡É4þ¯ÚiøÏ¼ÿ•Là ŒŒ˜q±fO2xœí\mo¹þÎ_ÁúCr”¨pÀ!±8¾î€æ¥mŠkqèzEIÛì.uK®mݯïÌ�Ü÷ÕRÒÆq‹‡sd’Ãá¼ñ™áÈúñÃõ§}|Ë7&MìÂÿ�b ?Ri�6"×Ò\žfõü‡³9üÞÄ&‘‹H¹Vy‰„ÿµ�ùŽØÊ\˜Xeúbnç°‹¹£u£–;¤|¾X #Ìn+õ+¾ŸÌe‚g"•—gµ©/bí>íÎs�d_.ú'•—0¾åQ"´¾<Û|ÏWþ{ ¬ŽÔ²q’WüâVä‹O?]Ìñ'¿ä7‹:¹›ÿަ\Û3»& ŸÚþòÙÅœ_Ì·¸ù‚’Úð•ÊËKÍ㌋†Dž‹Ý f-ã[ÏsªŸféX~Û,VüÀÏ¥)òL‰Z#ÌrÐ’€Zq³‘\®ã?$º�,�TfDœÅÙÚqiòBº½�]ŠhÓò\ê­Ê–8S&2•™ÁMúhš�0pºJN;�ë �×OÍE¶t,¬D¢=\Á1ò»XK/â9ÈxØÂTR¤Ù°}ÑðTÖ刵m«cF#•q”,�W=r°²"rüVFFåGÚ“%ÑËM×p8Ø_"òæáfŒßmbÐ8ðÔ1ˆ²�f5¢ÊÚy˜;Ðl\¿œ_5…`é¼|þ£Û¬Ñsg ‘aÛÓþsà´vâo4ç½÷¯óûiw±ÙðLeÏ3¹†v+¯O ²9™nÍpH£Ñ‰,ÎÑšÄàˆÖqfFK…•'dZìôØ!/D2¬GíÕãú³4*ý�7"þõË^}žj4a›Îø“�L’xûú`smç’Ó>G. “É �±–…e`îŽô<8êóá/Ìêv½I!uP|{ãÖÚDå­-R°t ¡ø \Û\›ï\âvái¬5þBÝü¢¤û=ãò‰Èȼ ¹@~ Zuî÷u{’ö‚º‹aÚ Ä¿m®Reä’Õ‡t`<º0©t+òX«ÌkTüK¯}t¤Ú:1Ú�Ê[ˆd†’Ýñ´èµ�+# ­…™’d¢V5dò…Í3I2îPd!¨‚k ÙlEœÓ‹i¬-Û£É%ÐüYf‘tL-܆Á&V<È_“½&G•XN°ü®cöaÀÞ(;æTgÛ­h]TÇ'®Ð¦¦€dDl’Ùá)köU"kÀ®Ó„V^d`ödnÀt7ä[„µ ­â ¬²aƧ†äì;&cük&.]9…€¸ŠÛ,,p‡Äm¸Ãâ6û¸' Ü#þ±/r³ýøøˆÈÍj¡{šÈ� °àûÁÈm‡'Jm<±‰’é2i<.úYnEŠqƒ˜ TWn6­Ì*Ø'²Çêð$séÉ@PLu+�æ——ü¥'�f„5 Ã’nÖȺnøÆ¯öÓpÎ닪ˆóë y*Ó™Û- Ÿˆ8<‘Ò^¯Ç|3Ž7¦Þkæ¤þ4Wwú©Ûø`~kÅ!úõyï×ÇÔ¼  'íZ<ŠvG_ðŸ€`ÃáÊ›ƒjºp ,ˆ§3è î‚,Þ^TmË¢-©Úzb¾\ R¨3=p‡°F¼±ãOóº„VOÖæµg¥q7à ì�,9=]âÊ&ĸ•HîqXõllaBƒó2:”,×Àká‘J¤Ctïý©oÐÁý¾½�6ÂÏùGÜ8§�‰¸‡½�Õ w1ÚÊT•Â`A¸°v8¬ü\—kã™; Û@\wûÚ+>™?Sï}ˆ’Ä î½í�·ÇFj!¥Ö…¬u»UŠÔ“˜x›´O^ÁT¸¬ÄÖh3rÐÐ6î©{ƒ(𜀯Ší6‰‹5&èâ�à÷dÝ¥±eí‰vFµmÀíàht¡"´ÕÖ.}8p…ï&ëNj=8¢z ~I潞MŠ4§åäèÂ�½6jº¼3\«Ûa¨#ñ�0~P¼$ÆKTú'b¬ Ž’VÆwøžgIàƒTÃ�KIŽ…IdÒ«´/ÿÜ �¿X{lŠ+ÊßÉx-€GÚ…–Ë2˜€åÛ£ã×�‡¢–µõ©‰‹ «ì¨G°f5 ¶W{Ѽ¨KQ°†,ôVFñ*® „oâõ¦â¶§‹6õ²G¬½Ô(Öý#K¤ÖU”±ó5—÷±6³QžÐ0ôs¹Zaή,~ƒŽ�‡ÁS[Ƨvü@€ª ÓB�žÌHJu`â¾Oåÿ°Lu ¡#Xæk#uH6Ñ#»'šÌõ¼¦³áXsÝ�5Ç#²ëº‡-•´77�akZ|•«ÔÞ�+ÖÈ eà}Q?TxûÚ¡$±A�±‰yçö²òòh1l?�h{ªªmMy\›Šü3Fª‚â ëß×!›�Lë’ï›I •�Wi9˜å½2ȪÒ` Ùü*{ÞÒW]¢¶šŸ5» ì*Jâ¿î 9)} UÓ21Ÿ¦5d›+J.ÕYΘH¡5z§©´$tœRËåªu’§¿¶ZYuš‰ž8 ¿T)ŒM¤L¢4ÑËfd§ôâúÇà¡õ“„´àÛFµ¢§5Ð×Tm <Ÿ5Zþ&m ´´0;<‘‘ybÙ™%wœ�9»yffK_v‹P;s Y9vJd•yØÇûZQÏký§aF¤ò}7�>ñ9Ë9¤Vp:uçyË8}ÐÝrIåù¯qÂÃy8"Ôx鼑½³ÙÀÇDDdÐOK—"Zï>ž4ìÆG† Zû(B†cÓ¡#i+ôì:<é�#<4¼*�Þ>9ÄQåÊèÀ¤Í†:¦ŒµC¥sâ4“uCŽËˆ5ÌÃIÃ1UÕ«ùÍ®—á°޾•À>ÿ¹ºí»Røžúa®T­_µ‹6Îã­&+?/¥@ìË•(3ã�¶GŸ)ïm¼Ùö8A·Âðt){”ÄVmbÜÍÜG,U™2*‹£1r®–Ò2V²‡}¢¡\·$`Qn.]�/‹m‚¥zÓì/Y8^r2q¸Ä›FÑá-”µnÙÊUVDøÍÆú¯¶ÒaµmŠê¾A|·¿»á§×é-n¸¤t�õz¨wÐZ„ð•iÏÓ ïF�y.í¯»Ü#h°ßà«t…³M]qî¾ÚXÓŽö Í-Mch;šÕ³Ù]Œ“)®Y#ÀÇÎXŒS&�ƖЗ„Çý;ø/ƒªä�wü‚09줕+~.»MûRaçtÔL¤¾|y޶q°+/¬Ôz“«×s‡N¥»Çrš¯EQ=z�-C´Ôv5!#ÔÛjŽŒË5öQÞã­›dW~E ZÈ\ð/s¼ý”†‹úFW¦üî„m±°�Ⱥç¹õÈü‚ñ�JÇÔ¹[†sºfìA믷5mk Ô-¨à»€ UÝ`¬|§YÎPªQá"Ûžãö÷9zâ¾Ñü «+©ÖåÜåNS¬·–2 bl)t ר Ûɧãuæb'«®±6øº¿Î”ý’Xk¬l(ïÓhû[m‰ÌÖf3ã7…¡ LBn£E»>‘VûoåfÚõŸLb �1þ–?L\jc0 .gùÈØéö¬ ëKXõ^TöwVÄ¿uxvs¨±oè87‚»*†í=«4ö^¼/geÈ$èÊ}18èáÒÞ̃¶i‡'zýöÄ&zý>¥ÃÂ%q_ì)ë� ÇKØWGGZ+jQ�U¯@6¡°PGñsþ�oq”i)3a6g‹ó'Öè×`T2µß8jOy列ӔgÀÔ›‚}°ÚØæJ¢þ6oŸö›ªr9w¸nnÿ4Þ£™Â¯ KÁKˆs‘j&äxœíš_‹ã6Àßõ)æòp´�]såê®×k{Pº¥Ü=ôQ±'‰@‘²’œ­ûé;’ì8ÞØ×„dÓÛÔ°�µ,ÍÿÑ�!I_üx÷þÓŸ¿€¥[É)Këä9}¬ÐqÈ–ÜXt“Qáæ7ß�ZwÂIœfÜáB‘q  äNhEÿÞ­Ñ„ÿmšÄ�,M*�3�—^ü«iÎwåí[8@`)ÅW8íì¿ÅûÑ4M¸—ùzÚ~A§^Óú2É­�Œ–o`néï†�ÊtÞòá-¤n¦Ÿ~Jÿ HgS/f6…o«wñÍ8nü!>}›&AR𬽶)|¸/¸®„¹6-߸1¼´·,îÌŦ¶leoÞŒâáhÕcCðþ @XpK¤Ý…Øp‰Ê�ž‡[*Çÿb=Òw%ÃdÒ’]‹æ*ƒ®0Ê©�G•–²Vü�À£!’›ZB­ Â-A8 (qEÆY ª§+«œ)°ÒÆàa‰ƒŒLƒv­U.Ô¢9J:[f{ûví&ÓB ¼‘ôŠ4±FÕœK[ëjT•áŒÒîþEöª §fÑWF®ÖT§3‰c&²%8Õ¥:FëuÑWþDHF¦1h…ÕŠ”Z3¡ppƒ¦„\�»–z`ì� Ó, vkJtUdÆŒ¬¶o_ÕEGöqÞQ”SÊ—ãèO�¼ä2¼ñ‰Òé�ÒN4>[ºQ…–¢JªR Ù“Èê�ehò�`äË5'¯ýC�y2„Ü`N2A•aeT®™Ï)ÁÜGÞÛ×6¯mQ”»mЪb¥Ê-¢>Ÿªíã/Cž9ò`ƒ™Ó”/ZC)­3u }9ЂAµD !5=V�¡C „o¥½\Ü2ø¨h—ðª-Ž{Úm¿Ûö$1¯!6-ÅC¨ª:ceUñ wõ(mUWß]³² 29ÁB×nóúØ-¹»ÆLÌ�¥íÁ¦èt¼�V9Î…·£I¤|ð• ìèÌ1yG¹mµfÕm†S·ù sUVÁÜõž.¿‚JˆNË"\qKQÛÀ¶FÔ&t÷f�=„YàYãÅI˜Ÿ r_‚”uÖ|8| «®:yx1ò¼\¸ïŸ)|hû¢''ìÑMat6±ŒN`Û�Ñi,b[=‹fÔÐ@b7þ�ôY…Wt+í˜Áj;(ž&³†7!}™¦J«¯Ï�,{déï�&l-ê veÔq}Ôq硎;�:O@wQÒ\h³…�.Í�~"\j¶°“ár�Açë�Ë”C�ÒDŠ�‘ëbˆì›\äy&yüäò+Z{‰±E^pl‘Ïyl‘] f–kÃJgš¾|‰/=�1L+]¤é›Väy¦yü´Ò�æ èrÁQE>ãQ¥i¡aN¹Z  9"ÄÒÁ Õ7¡¨óL(êø å7*öØ»çûú^]nyñLG‘® NmSq1™�×øÿSèôŒ(üç2øH“T¿2Kâ�ÙþC\BŽ<œ|% xœÕZK�Û6¾óW°{hÒbw�P¤^ëm=´)ÚE�´D¯ H¢"RÞ¸¿¾3$E‘zY~,ЉÔÌÇáÌ73ä.¿ùùãÓ§þø…îtž­È²ùá,…ŸœkF“«×7µÞÞýx³€÷Z茯¦ù³¬DÂ2údÿ K^1-d¡– ;�,NÜF¦þn•2Íô¡äê=*¦“%£ËùÃM0ûž¥)<ª›ÕrÁPì«�Qøþ,i’1¥nvïé?Id­à]îYµZ/øCèr³ò"6+úÖ ?Úá[;¹à/v¾[.ŒÌ墼оow<ËDùè2ß¾yÜj^½qZ˜Žv9ŽþÝÁ£¾ ’5ßÊŠŸeEÓÔï4WTËhßYU±Ã=±³S±o çêîý�`Uw�:˜ó7‡²²äEª(ŒxU%OÄVð”Šbà{¬w¼š†š)Ófh rK(+”J‹â9ÐvOé'˜%k]ÖzLd³ÈT‚¹ © Md¡ ãÏy«Ö;зá™é€Îhš»±ìQ3žïs(ÌÚp3üÜš¤c†C`ñÍa@ðù˜ç{ç0è�ùþrÔ½Aš× 7ŒrÂ+j©•%h£=O´¬nñ,GéŠ*pVQ óÀ=u…0ì;ðŸMX�²Y3×øR#Ðy:eW§º5:ÀU‘�aŠ›k¼Ë ×ÛóJêÁϬDâEö%ÒX 1 #»¤ÆêÛºH�Ûœ¢Æ{ì‚ÐÞ×½«#ª´.3�ö7@!ØŠoyÅ‹žz;D|?ö¶äï/pÏ�y¨‚ŸÌïüaˆ6ÌúY¦$¡Ýz€Íú†l-€ÀF2VΫg>‘³üø%Y+2’·d–ÎË['f�5GÉ¿¡È0:aw¯‘9<Ôù²ßí¾HtÂH‚„ÈúÔ‰ä&x:nçc‚üÅ8 :X€:óö]ƒhàW4†2A++pk0H`¢à†Ë f‚LÃlH õOj-ö€p¤žG`Ÿ‚@q”YU\•²01ÖIš¡f2¤šíVüN)ÿb¹¦•‡OÇmÒJ^ÏK#'øÁ„»:×襒ICÇþ$ã)A7è;U¨¿›n±kg›¼K _M®AÇ-¬aÙƒ"•&ùhÊôTñ\¤3h¿â¹ÜOñ~;áâ¥tù÷Š,?¥fF‚YÑ?�€0N¶•̯Bë-¶áå» �¿ÁOë¢VQ�lp�#EþIÕûŽ©€Û•øý)¥{–ÕBÑØx�XJ Ô*à�nù xúüB~lù“i­1‰Á9?Ÿ9;ôRÁ:ˆ&Ò!p S«.2;FS ¤¿EO § òΑ*w€xÈYÌ3 ‘\À<ÃÄC.dž¦²5–�ªmû¦¼bm ˜áÝ$É5.#¹VÊø±Œ±ö+0ßQÝ`â@÷@ȇ¦è»~‘Ûâ=ÁNî+K‘]NèôV·&$òàŽÏÆ÷ .tZ–»mãÄeßÁ³–IÐßo}ɹªã2Ÿ¦y€Ì'‚y<@Û]ìv]npe�o@$È÷„ê†ÃSªñÖöœý?ÁW‰÷ŠN…Ûmú{k Ú+o†3¬%|2$ R)™r•¯(KDγà{îýƒ 8HP˜‡k7e¿µê-Po׿;—K¦ºŸR@ôHJÙLlËЮ�ñŒÓÛˆYùÆt¤“ Çϸ,ãb^µ®žÐÓF§™4šIÌ諤n´²63Ž1„!1–•�/jèšEâŽRÙˆþIÃD Èo±$1'(þÐÂr°ýa¼ìD²³Qg+Xˆ•’Wy­Í–Ñm?±à@3hj9­±[ÛÄ5]xœÑð¤Ç–%cì4Á ÀuŒàFÙàÄhõ¢Î¸\Ñ¿¸~… u˜N¸ƒ/TH)#mm/ZÂ.ŸE�ͱù’t ;Á˜!ÆÎ¸Ôˆ6(©d–É|SÕWœ©j¼ºÎÄÊ4\R/Ò­—û¶T9oú«î e E€µd)lXD&ñ ÂZígÊ`ÜM@æÑMP6��ƒÃŽ= t7‚ƒP¼‰£ÃØÑF�á9vÔ~7@�.I§A³:d´½h{oÅǺ½�l¤¬Ã³�¦›`íþ)Ê?×`E»õ +¡Uƒw�›ß= ½´ü? Lýñ©£ò ÖE‚ì¤+«sºz- ~aS3v&—>…\ê�0éz„CŸd¾ÁtÛã*5›:[ÂõH†ñ>×"Û1Œ üC¡Ù2,?Š‚ûï#Ñþ6â¶âº® Õ2R@Á/;©â*†¹{ê'¨ì|YÉ´N<¦ñ\4O!opÅLC�BÑà.†Åw¶îMie6ÃW<ýp‚�:ÈHÛ@ \âtsI´‚æÊCOæ9`1G®ÒßoŒûŠ‹�…û¥…ýC¨ÿ$$¥ß ÁÁ”<¨|Bïxœí\ßoܸ~ç_Áú!i���p@‘l äÒ-Ðä ÜEqÈWâz Hâ–¤loþúÎ�”DiÉ•ä•í<H°öJä|œŸßŒ”¬ÿô·_?ýþßÿ�îLY\“uóÁY%7Œf;¦47.j³}ó׋+øÞSðëŒ~#•ÈXA?*avp»Èè¯{®˜²Òë+w#Y_ù 72?àöo¯sf˜9ì¹~G'ì Țъ•üÃEpÿe)ª‹ëõÃMº\�u?Á…=Í ¦õ‡‹ÝÏt«áöLæ=ôïèú–©ëOë+ü èzsm—o®éŸí¥�îÒ_ÖWvíúj?mß?Â�WNŒ¨r~ï¾ù¶¤¬‰gvíß¼+åó¾åٳȶŸ¯YQ¼öß=¥uÓ€ Qq¦~L¯v¼(ÄþýR覛 ?~w øó~5q]4ð‡§Ž…J¡µ¨n¶»‰J¼¦¿•à'\Ê ^òÊh***U.*ÈYaþbJ±ƒ¾$ne.n´¥~óó…ÛÌ!¨°ÅãŸ*njUijvœêŽéဠ2RÑpÃKJÿ¹í}C…¦Œ–Ì(qßX8‰ƒ´@Uò®‘pù:m«dI9Ëv4“E]V ÿ³T„–uaD<¯4$÷Vg£¨´�€BV7VèV(m­dõ-Xp#+Úîݘ¡Õù„àH‹g…–=cÀJ‘¹%öwX*ʺ¤·¬¨¹õ�¡€½Û PÈŽysB=£Z|ç¾±]úŸ¯ìMîØpºî` ÑÊ08J©8ÜÅÀ *�4Úxé 7 Ô"˜�ÊìÝ`}wžijÌ;'W‘ˆÕHp½ç™Ø žÓÍa°Ãb˜b¦ƒ Àq�ÛèöúX¡E¬#�CÞÄèæ æ X–VuÉ!*›ˆ; ¿*¾çά{©…·èPÀ\iˆ”�ÕÁûi)ãlÚs M ¦`9ºƒ± Yfê&¾z�Oà;wS�÷,cûd1=Vt˜Uùhz륰%úD™ëÅ:éAsaÓ‹¤ñxCþŽÇâàO9¡²6ûÚ`Üɪ°ö9¸ Äy›ú¢Z£.·�\nÍ â7n¬û ”hu¦XÐz‡p×ý)ŒtÁ˜I¥¸ÞBÜ-q¨”&øJ¤’w¥$ZEIè²ÍæÉB£]¥é6t¬Pp• »4¡™.àhâL–{ÐMáR´@Ú&`UcåÀ.Þ :\ãwâßúÐ’3kˆÎ!XÒC ÛTˆ¯W"V6gû+ 2_)º…¿ü–«CX+›Í„ ó¬=Aspþ?Ì ˜‹`KçÍe{ñíq”…<Êšx*�j“¥¦;à ‚ jU©Á·6Æ”ŠAÚóѵWüVÈòírñ=h”$„©{UÈz�›d«0|Œµ�Sš3#œ´Q…¼Ã;œÓ¿ó篱.D£…ײ¦…ýºM®¤ ap•¿Yùäª]Ê‹v˜úš¸ó˜/¡þ ¦fèlìÑò GÀß ¶L ¢Â�0öŽp�ׇ­XÕæt<Ë€@ϵ† *ø‘�a¦†lrJMèS ÊP�¢ÊŠ:çG·žH…Õ�⮄¡¿Ä”{J1èG=#ÇìŸr¸8yÒ+tøz€Ôì‚ݧfìþ¬Ù.ªÙÅ"²&žáIfs/=»˜*ûÉfó=Íìb"¦'˜]$�<Ê좑5{vñ/ì utÁî'Œ.Š>ŒG˜\¤`ŒM.†È:¸HªáiIñムvÿ܃ Dð(ƒ‹¡V~„ÁÅTL/ƒ‹óSô¼Ìà"’ÜΞ[L@ÿÀ¹Åh¸Ï�[`mzî¹EüPSçe/=·(†uøelñ”c ´ðËØbñ±…»ôع9nÑH%3ç%Äcº¡'Ï1·Xzfqúˆ Ï,x.Xò• {qæäâ—6 c—ãwXf 0iëeFsD�?4˜*mÖ˜àd;zZììžô‹]ìæŽx=m(ÃÄŽ´ƒç°MhJý�>{)�DñÌïM›Ð›„(Þžú¥I´§£Ýé $Ù¡&TÚ÷<²¥ž�Öz¤Ãq HLÏhûÆN�Ÿ#¨°íØpe»ÓiÙqåk�k …9ç8¦˜zý˜=¿ƒté� eš;îûUs'ñ:–ùV®TwSméÅ;ƒ³øMGöŠsÐÓN;£9íÔAæ7§“PLiG5iGc=F\³ëH£:Ñ‘ö³Æì~Èc}!Ì-Ë ˜ñ1&‘Ò’ Æ ^L@Œã¼˜<Œ‡¤ñ\^œj5:Ü‘N£åË3è2 ëÓB—áöY†K3©ò—ÿ²ëzÊ÷¥—è?÷Ÿ·<º Uob²°Øás”è¶g7³ÄLmæ= JIk€�K�O= 0‚’ÁÎ`ã�³"²)\<…ÂMû–¡áãP$<îÁ\<‰b1&N9¶Ý€•�²q{Œ}ºþÍôÇ+&;ŒðôÞà §<ü¢eã–qÉ,«•n×GFM>úIe'lß{BÖM«42HUc¿GBÕ¹}à…w��=¥�Ùçp�…íFMrÛÛxα�ët%d¼ˆ´çiÀž«�e�n©¥2A/´Ü6-D3Æ�º üÍÓ«‡inØúéæÚ¯†—æë¼æ+¦×Çj½R…,Ö�� �;¡M&k0e‚Bv7Ì$’_C"î²ÌÜuòöÞ·aýhòŽ^ú*UÉ ñÝþÛ®I±;�/•ø¯Ïåf!¬£C~; ôˆžýn”7Š•t#ð ŠÚ¦¶g±³NeŒQb4ÈáF¤KÕm‘ ¦hM�k ý0h¿Ê»Ç…$vB7-ž«ÚàÃi[^ åÝr[^0 ) ^ÅÁvtí³hÇÓüâYc\7I¿íå�ØÓ>@ßú©b@À0úóV-ÝÆ«&wÛ*þtpyÏ-°-r£½n•íqÝ d¨¿U7'q¿7;Árßix8_%c–³§ÁÝ#Ùº�æ0×{Ååm°D%M'þЖâ€ÇÙ;*Ê•’ê�£JVÓß¹’—„~•ÆïæLóª0ïÛNüÕ�yߨ¼ (îöÅà� D8P° x ¹‡U!­ðÑ3eû½{­ çÊsœbbr!§²ËÑìŒV~÷(}׆¿'E &Cä.ƒÙZ7ñ’{3¨ûÍÚ>¨ÔLÌxêD'åü ²—"C$+'ÇÈ6‡nô0M$™QHßw Óä�ÈA˜/�ß’°=ÒþäÞ(•¿�`Ù+¹aQ„P¢G<:!é_ñ‚Y^ä^[�ãK‡ü`ˆ‡« ne7< §»|ô< ­Ð ?W[ä1l²Ï·#¦ˆh*0Úƒ,a�¬.{Fï.Ö^ÕQí’A Â|† hçÏÁK0Ñ�Iꘫ�Q_ñóÈAgxІÄrYò½Ñá+Wôaz·o÷¸–¶¸o›a� –Ðã5hÐ úž“D“·§ÕWþ?œ¸rÿ¯ÅÿY÷ŸÉ QQ�,º\pPxœí][oÜ6~ç¯àú!‰�±�(°pœâ´ÅÝ4Á6Å¢Xä�ÖpfÔh¤‰(Ù3ûë÷ÞDQÔm.N² hàHä9<žï;¤Ü›¿ýôîõ‡?ßÿL—Å*™’óg3økÅ F£%Ë/^ž•ÅüâïgWðïE\$|±‚/²<ŽXBÏòbB‰“‚çÊÒý�ôݚ笈³TÜ\©1äæJÏ}—Ͷ(é‡éŒ¬Ø®¹¸¦ã¦„±ä†Ñ”­øË3gèåŠm>�Mo® x>õÁÈçðdM£„ ñòlù#� x?ÊfµU]Ó›{–Ooo®ð/ú’ÞÜMÕø»)}&Ÿ½RÏ&êÍ_ÕOç7Wrª›«õ1ÅèŸfñª_,ü¹€‹ øÿ¸òõ¬q:ãõ/»Uz²äI¯_xâ§à:ðY±ä4aù‚‹‚ò„¯xZ§àOšå³8G»Ngyζ—DÍ1‹ïÍ"VââÇ35­Z€g2TnˆKh΋2O…ÔË}Á(I*-³¹|«¡uÅàZf\D<…Å,pM<¿¤µW]•°ú;nV<¡"N#.g·ÒXÎiÎÒO|Fï¶ò‘œKkçÄ*Âx3÷Ô€è=�Š ’¤õ rÓ‡¸X’úâ�m¯X‘Ç3±´¤ eÔL&%„²$+p-i”%å*Eýkóa"×T[2QW&ÏP‘_²-—1D8Oä¼ñ€˜t(¡u@ Îã=šféX|‘ð"o™ÙŒ›Œ"Ÿ8_‹šË¹ðÕŸpx””ÒÙÒ/ÚåĆýNñÙ–Ë�U‘jT¬yÏc3õ\�\ëšXdh–”/`3½ç°(X1D›€(g¹¿Va 3$XJ¤™Òru§"´Šbk{¥8šÉª«ã‡%‰ŽêQa/s’Ï•߯=VMüL™õIR¼(akšÇðþ“EñBÛé\N”f…Î!XÁ‡%ßÊeë5d'Ÿ�°y!`3ÍÝ‘Œ7ã…Ü•¤[UÝ¥U¶ni–&["gd9ª¹ùÁ¦�Í«MÄùLxÖÒê[é¤Ûl°–?Ò$þÄuh (ŒzÙ¡<À|J3¢Ÿ<Õ–|Ÿ°HJ{ZEV!}¢ö"kÔúŽÄ’H7B×8|F±„€:?›W¢lµf9j Bùçì†Y´e#E”�PÚøõt™Ý×16å$z¯Ž²´`qªÓÅ·{»‰¼Ú¦_ß»»]£×t¥ª ‹Äi+�G{a9þøXd€˜ÇÆ"-*õc±‚íçØ`´ÛŒ-wA#ì{#2¾4 (ñu‚‘žø<�‘ÙŒ8yðFió��ôV QŽÜŒ¨ñ~I>üO<pFP ¬üÈëzñ&†'}YªuyûþQ0^›}Z16 ë°î hõµ|$†Á2Àe�Â�òΊHÄ Yì‚&Hmò>˜TíÓ�«p`iठ¬LÔ ‰ÌŽ¿‚ÿtŠõðQ+<ê�4Í嵂_®•A†"˜‘ 3W #P(,…“¨-]�™@�Ñ/ÀP&qËPµuš*�Øú j&wœSĪúÝš4/ [žqÖ0*w‘©¢- ¾ §HU(,evkjèóµ:w’Ö¸7´6X_ÙjÆŠåÙôí“v5ñâ·›+¾šêR Øn¥†ÝÛ@‘l¸£�È@ !;ê«NóÈœPªý…Ú\ÿfô îÙõä¯ó>ëÉ—à�gDç=YNÆÇûbe5Ç‘ðrxr�ÆPäOØq€ë°õ=’ _1~n³S7†ÆQ‡ÅÏrÆ> -_Ò»Èó‹Ÿ\ádp¯s8¦&uP]ÇÔpRsMmù¥×Xï´§šãèw†Š:%)z;é$‡³Á ŦôC¶¦¿šÐ5Øý„Áè=ú  lÞ“J*Ûz1|‰E‰ºÈ³rmêâÞ‡ØÄ=Åö ’á`uøGÖÞ‰õWzoàðûßPÝ`):¢‰ ŠS§Øøº>Ñ­ŽÄ­4¹õá+Z’.düžçÄDØxˆ>Ç&ìÓLm¼Æ­2t*–#�b~QܤR åE@OxEw†0 4YKÎ#OÀÿ…Îi7“çÍ;Ä|+½ôr€4“`|ø–€œšI´-«,ÑÜSzEÓÏÞóZ?Á°(…¼K¡Iã'¾µmY•· �’ª›…$½™ ˆ� õ#ÎA±G >×-„Zd9›A™&bÃX0ž!ý{ˆ? ÇÛøˆãwÞüöó{˜Qv^ŸÌ/;Þ£Þ]6”îp#ýµvt¸‘�˜rQtg0eŸåâ^ÑZãY®î4œåVî=ËÝ…Ü:eœH:ë|½ÊÉqx®Kwã¹R´ ŠƒóZ—šwê¦(xê±pâÑpVë)ŸÈ~¶½÷¡H€6˸h’åØ(Rg‰ï¢œo u+âš¾}õ៯nõU>EH•·¨ TÅÜÌCÈ'*áuþnD�½w×¶•= A¯cêÉãññ2�?—¼…�«‡ûpq3à ۦÖÞ¼x‡Ðì±äeQTB‘H#þeVøÅÔð%>¾ýµ ÓØì‡=þܼò~¾­6OÇÖÃÏ?äKNkü0 JvËñ§‹©Êš|U"{�4Ûqß ^µhÃ[öçIÐDÞµ¹@gs„®'úcƒ÷qp×mýB„ÈwKãÍ‹–\iC¯ÖMi¹¼Ö¥BîéL•€ÏHÝiC¾ÒI«Æ’˨Ix Q®†R:´Á$fÖ‚¶Ö¼½ÚÄ4¢`w ÚØ~š¦“ìÔU›V[K¸ûÚ¶ÜýضÉØœ%а"û9²=Ã~Tذ’+©CvêúXë%(‰@2~·Î„‚-"ò?‰é: q�àµ)¿.žTò—ñb9X�*$þÌJiLÍåCÉSZ6ÜÀòE©ÛŒÙ‚£/vÛ,Õ¹z»FOdXs-º°¯–sõu‡4î�Ø€¦êf4•ãb«ZU$$ÓtgªÁ•3€0U»ÞYÝÅ©ü]Š—I½v4 Fz@ %ß~±ƒ­U�l:©¦vÍËTr®'¹ø\f/„�V6Ꜯpœ®Ë"tÄÿøžËjÅClìo°ŒÈ¾/„[,™}~áÜV×� ¥ n˜éá'.9Þ´•Éí̺éá�çuÎïã¬4‹Er5`š“j½Zo[ó?‡„Ý ƒ†¥²ò’ØÏÆ<ª»R�~U}[}[�‡ pía¯î‹éŠ�úZûft¸îoÛMûQ¼;â¹€¨h¡~öùHö÷Ú…ìÎ$-äÁ3ü8z°›¬Ñ,¡ƒ¼Å<[Âp†b&èœÝ€”V.6•¿ðÈŽÆ 3ÿ!ëØµziIå;ñÿ‡à�~5Óƒœzöü‚S!¸¯Éã7CåÞïTì”Õæ÷û¬“…:˜Cù¬yŒô8�Ãuž©3C<�h.ÊYÜðTxRlçvËž�U¢Ëlð{©Š ã@Nty4ˆ´ò 7�ƒú¾R€Û_ nm7œTíp4•Q-Ù‚K,ÝîlÎR1ç¹²²»n¼º6§©ÆÆ¾X3ÛVn�ÂâÊ5‹½þz.Ï,ñŒd<“Ï2Ý£WRë�|T½¦íyèL'À’_;r¾�,ÉR?+¸¨zŠ­Ä #7¸™v!Q•͵c7FäÝ$Äã�ªOŒ½'%Õˆ>ó€d¹ÝV-z{ Ô½iÚì¯MÝõI÷ˆJàöÀØ-ü�S&wNÞènà41'×ã�“>=7y Ò`ì *øCüO�­¡ Ÿ¬‹�Bp^{… ^ívÅIuU³Ö»°„ÞaGº©ýŠ„»-ác))Ë]=·Q䝸p�h§8_¶ËP¿‚éVJßoàÅ,žÏÛ>zRO÷�œvŠ£Î�’n~ (t`pœ{ ´Ä�ܶg,µJ'X¹¬¬œs•'PytP9 iO�²R~tÍØý!¼jf†$µÃº Cm£ð¢A|¤òõ"¾ñ udkëØF¶M–·ã5x¸'\“3<Z èËõ]ý�ÝS„n|%¥€\½ÈòCƒ7Ðé„ÝöÂn®w¾z3qu‚nß tëIÙrß 4V=u¿µN ï¹ªŽ*å&Õ~é¿„ØSê Ž)ÉùrˆÒQïÀ€20ó@‚¦¦.„ܨ*>$¥V> -²ŽVÓßYÎÿ“¡‹R®�7¥ªá‰4z™§‚�ù(°tÕ&Êý,*EŠ6-†[x‡Ç!XÕà";¨Ï°ii“2WQæº8«1÷šK-”µ a‘éX&”®:rx ²W¾x”y<`rùžØ²€ï?d>°eÞ~±ò Û„üìþMù“¾àº&'‰fw_WëÚ´w'. ¸÷Èj , JGÓRÞ«ÍL$leú�ç&Lo²DO~O,žù{ CŸ; „³Å ¼hSjhº@ UÏC ¨‘×@€³‡aƒq<™çŽ¥ÈÁ½ã^ÿ@} X›c¤��GõàÄǽüÚû7a¾÷Ü:ªñÖü�ua~Ñ'/ö>Ðè=Œô (×ŠØøA¥ë,±Ÿ0tÊ([Å®¸豤è0Íž´±k0 ‡Ÿ‘bFNÊã’Áq±sÞìxŒ�WžÞÄWψ»R“ ¦6xœí[m�Û¸þÎ_Áî‡kìÆH€ŠÄ1°›KÛ|¹;A�"¸´D[D%J%)ïz}‡o)‰¶¼ö^¯ÅI66¥™‡óòÌ ¥]þé§_>}û篟q¡ªr…–þ%9ü¨¨"8+ˆ�T}¼jÕææ/W ø^1UÒUFÝÖ‚e¤Ä·B�=þ¥¡‚(Vs¹\ØkÐrád­ë|¯%¿]åDµo¨|�‹€kÑ’`N*úñ*¸ô üÿjµ\-ïÝj°÷½ƒ…g%‘òãUñ#ÞHøsõmY�GØßã厈էåBÿÀñr½2bÖ+üÊ,嬲‹×öÒÛèÓ�ÿôCAË’5^/FÇrÑh+ü©æZ‡¿ÑŽ‰Þ±|ƒì…9ÛyÈ•¼ùñÊÞká‚�ó á¬‡$ѨYÒ0)k¾Å€�r .Ã1¤7ß–%f¼i•[µRá‚ì(VÅ<‹%{¤˜>d´QN ^ªmð-JËÇø–ïq½17Äj ’H)k¼¦&~I¦¨@xG3U y�¥ Ú_] œÁŽº�›þ‹ù²®€´.)2 ¯ñ}QKØ }PÒDigH :@jŤÔzv¤lõW÷Ë PZžÓ ã4‡]|®µ÷Úa³-¯…YºÅ¼­(xF‹+ëmï"ÌÀq‚nZIsØãÅD_½ÔYÝ–¹…D<¨½Á©Žta_6˜€û�hø .Ïu£è¸Öç``ç\ëV¤��¼Gâ{¦ŠÀë”ñ\ß@�/ê Ì[ÿÂáj@’I¯ãÕø´w�DÇôm0¯9�â …^qªÜîáŸ=Îk¸GaNiŽU= lÐxdW#Ôñ>!Ka\™-‡@^/oD]õÎð¬‚ñ¯¢V½mì � ŒÇ˽I‘ž rk¶t¿yU�QÒŽö†× Û#£RÁJTý-Æð>îÀv r/jñ˜&x·z ’ïDuD*±ÿ„?j•� 5Q±uÑ;ˆóàyng6^è¿[ÄC¹õ”ñð>„ªî:Ußç¨øÍëЙTt»/!ÛŠEè‰EE“ž�etrÙ�pä}ÉèjšY$ŽÔ4¿H$jš]$Ö4·H®è¥H¼‰ÿÁ"±£B¥‹„[½D‘èD=¹Hü$£>O‰p÷ñ!RñaºDìÜÞÿ8bÃ\ùR^êÃK}ø?®‚6U²<ØÅÉê�® waUðE�ÛOƒRð Šò·Cvô ?‡p¼ž…U@s“ZWø+m(¸?«ídˆ>2§Ùµ¯Ge!Íq\ªZI0¡Å›uù¢³;”€ï+E"õäê…2¸ޱ@DxY@£§îr¶ÿûýs"�Øt¿ÏPaPB»�H,š± £P'Ö{³X2©“K0 �RÁÁ@ºšâ5´«qȉßäyÕêK kEríL TǾ¾ŸaÛ‰€ÖE#hš€­aÏ7‚îXÝJß=ö ô…¢º>ê¨|'5�îhI«4ßéÕ3 ÏŠx:ã�N75žJ}$4Î^­;¿�"Iß}Ù.1ÍFÂà‡âêQÆ\ëµ]¢i‹×¡Sšl»qœî‰\ê^ÛLrºF)gOŸ H†‚Ü“]@?t£Ø’*à¾4 j4hŽ{ ]çT—ßÙ‚ §àG#ºMÒ·{¶²­�ksµ bHf»!»ñ¬‚ʦæ¹y‘°Ã ~IFÅÉä�t¯:AÞ†nz«óÉ2Ó�n;î IÛ÷~ÛÖÚ4¤°c`=V�ÅÖÓºx~³¡B£ è˜{Þ¾‡Ë*©Û¼ r̓À™‡Ðg¦Ì4¦6ÎÁªu?¡Åí>�ü>7¯F{ ÅÖrV?§[˜øvtX ]uc>O|<ÚÄRÌ<ÕÛ�\Û'VÏür!× ùÄó“)—Iÿš`ëO°y%ÚLu M1æ@;¸\ª�4Xçàüãpxˆ® q‚—À:~ßDW«w?žûÙ@+¸’(ÁlÖ›°„¢‡?seÆ07rÙÃÝ—'”¯Ý–|ì9"áÔ4|9}ð£¸9còe<ÎeV¼A`#ßF1M#8ÈŸîOò­¢V)|¡o-†ù3+;dAšL³z^3åD<ÇTw>†@©‘ôÝ7¢ÉCÎóâˆ1Nš+/ÜÒh—š¼&Þ³˜¨ãÝHÎY)•h<8éÓľ)‚ÁH‘Q&*ÛŠß3X5Œ9y‹†…U§d#ê\Ӱϸ™uÆ$«Õ¨Z‘ýVŠŽ7S“þ8!^ƒNj–3ôQ^_è[Þ;Çr33g`eÖ–¶»jU ¼jO¼±¬í¡—Š�¸ÕïÿΟÀÍYáúSc¶¤­Õ¤î˜4ÇÊn"íôM”9öÆÔœ»B`m÷êûoᬫ†›’lÝℼ¤š1‘É‚m’/nùõsˆ5ò»Í©³uö|–êx@’С£ŽÊçeºœð¬Q5€’6¾†íËჰxìõ$§>hœ©¨»žÙ¥þØ+üR¹„# âñäpŸe¢€'}ЄoNzS"^±±cf&óðmp¼~‰ãs¦ÏÎÍ£³kó„Í<²«yF]‹g®wÝ›*Pú�̬ôãŠ4 íF¿1õÜEtâŸÄ'âίBå¯çy³ÃIÃÎËšÛ0kØ8mîæ¦Í¾ƒÑº Äí³eQ�˜Åit j”F¬ƒ‹à¹²T©/³8:3Y¹›–›êaƒ z†‡ªVpî×�™øÉ= h`íyž?qXxÉT¼››ŠwóSq¢ó%“I³Ù¹4™³h�óÇŸNHÅ.0¹Ø­_¢„†El”|ߺˆ�³ËŽ·Úfêj*3•§²|Z•Òê"ß–JZ%džŠi‘ߪҊ{íE÷„ݬÔå˜I$—c·Õ¤H7¸ñWW©´ט‡âƒvRÉ¥¹\ªÇgíEíή.Ǹß9Wg-оKWbšIcŸ-ˆ™Ó"íJ塸¼•%~üôrŒ?‰ÇârÒYv9ž\‰Ïié¼î‹Ë1¼¯>âf?òº¿øÝ÷�_ŸÞ5NÜoÿl.}ÑøõÕeúõ¯É.½¿Cøs¿Ú“lüÙBe™^=‚ó±{Ï‹r)í=·¿Ãóïúqœ[äj-dYÊ�(fDê63 ¿’ê[˜¥9pÆTxï.Û,éu6Q*[•¹ˆ ,$=<ˆYY,E^-U©§NYê÷"&8ë…ž.IJ2V,ä­JÛ…*a¥R¢(…�W¦EV-s3‚}W¥2*·:Ÿ‹�ȉe‘Ûü�Ê üwQTô¡Î+«Ì(!4 p0…�"‚)„V)xœ}’MkÃ0 †ïùZÎmÍ …QÜ\Ö vÚ»ì¨ÚJkplc«…üû)MrX[ÙâÑ«×úiÿùúýóõ'î|Sé9ZI1‚9a.Ä»úÌíò¥VRgÇžƒž‚ż?gd¤evT´‘J«Iêm??7¹OT¶ð¯‚ •FØÑ®¾%Wï1wÈu£²ë;7!:k�¥ìêÓÚ"±”íÐn¢½?Év6Òƒ>4³ÐA†]q­Ò ÙÀޕ䱇öJTcݺË<¬+ËMý]@Id\ëÈ/ S† Ž$ gŽPÄæÀG[ݶýAà\ÃE¼L°‚Ä,KD< ÷½tV&®&ûJüiz05þˆ_Vá¼^�ž.ËxœÍšÝoÛ6ÀßýWpêC_êkQ`E’®Ö�­Ý€=´tޏJ¤BRq�¿~w¤$Ë�©þˆ´ÍÏßOÇ#©éO—Ÿ.¾þóùŠ¥6Ïf£iýžà#ËYœrmÀžG¥]Œ‰&˜o…Í`ó dÂõe©¹JN'>4�Tíç*YQo?Ïn¹]`~eÝfX>šr&yçÑvq4›N8uò20 fަ‹3nÌy”¾f ƒÿÆøÓb»X%Ðy:t1Çþ]Å餠Îfì�Ö|Å4 H+ä Kªê†©³"VÊ_dð]Ì3c¯76md1&¾¦Öœ�|÷‰¸«is3~ù¯xœ2�ÛtÙ 2Ȇ«‹ iAKže+fS`²Ìç  n™ª F,WÒ¦æE¨ÑVXÂeÂx5h²5˜šÿ ±ÝPÁˆ¥ªÔØ.²´Pu` V21gŒ½·(¼dsÒ $Ì*fD^db±"¥ÄeVëOŽX31@ ªå¤v’ìNew¤ÜZ›Du=¬©ªŸŠ&Ö€ã PªUy“’B„fHm¬.cT&›¯Fø;Ÿ Iƒ¢²@‹¸îeà „ÄéIɳ5[­:ÉN}k(¶(eìo Iýë‹uîà[ ÛÏc�?ZÜ–\£ ¬›Ðt4¥+à­¢e*о¼&ºä¤r„�;ëæŒ,; Tt"âTMQ#-ɸÆ3£FŒßq‘q|7 �Gª×g¯ C“�ïVóØŽúhGØœ«¶�àä*y3ûž _ýI•5b ]MªaqÕxF^ïYý+jY&Gím´¨ª�‹Z¿@äªÆ%ðnëç-ÈTî*}5kúÛVÉu¡UÚ 0nˆhö¹É ¾±‹WäIÇôÔ³©M*G×»ÓgÛÎÞ*�sô˜þéª|"u�ÿ/…)2ôŠ‹ªeOpx”³†yP® g|×h¤×øîÝ!Íõu§Ò5ùéJnßÀYuÕ€=ßnðœQƒýõòƒP]½uת®�wuY‹Æ$,½íÒ;Ò}/ÏÚj~,‘œh|UÐ4ëõ�� ¿�eɆa�§­PZØê¥Ë9—íG^ƒÈC–!-ΚO'«2¾íÆÕÄ¡ÓB™°yk"1^B'†+îÐ3Ì< †÷–ª‘'V‹ïØ#6G5zŸŒ‰ý„¡ÆƒÎ†_Wê5e[€?Á–Zú¸²PÚ±·<¦<ƒðã;.p}p��_¨`×`¬ªÖk5:ôÍ•|˜×ÃÇaÑ: ØU'òÒÏC»1jE‹=™]í¡ }µƒÙžØ¾úPÜu€mFÛ=ÙëCÑ» @ÔÚ ôävµ7 ûÅw_Ê<çzuý^ú@ Ž‘£YUÂZ%ûGl»† x,q�—þýæ‡ã‘ý�l6ÜÜä89î¢ûØlرHSmÒ†¢EÏ�óè!Ú¯Êâ6m}ÈP…{CË{ä•÷ëV*yZmðAë�Ž¢™†˜÷‡Ka{õaÍi¿Áê7ÈX%› ÚïkR¬Â¨ÔízÙ@?/óG Èû©€z‡ä]ŒËfëìýýKp€®>„‰UVæ¸ß«S…uº&v…sLÒ�ë䀹ì yaWÄía»N ‹Ëhæ`)±7¬?¢r}œW6¼ûE cäý�%rcJT¹Û4Û”[– ãN¡ö8b.†í@búÔÿ‰IN pêèD bßËI‘…\/>…Å.N�ë7ÜDìSz——ãËý²þÐÞµO†°ß ÜÖ†Kw3ÇzóJî^U|j•¿ûÈâ¤È¸1pî�ž!è7ÙÒ_Z„Ù·œÌiQÕÒ‘ªå�Æ‹=<ƺh0�NÌêÔ�Ô¾““{¥±OHÐq¿ ìÇʼnÂG,éÆfíÒ£„h ×UÝVâVË=‚ÁrHG~U~ ´¬HË�¨IÚF»âaQ‹Œ@éoÐ%‰xêô�å.J¯L*–“Î|�?r^ùÐn<_�ëk”¼Ì¬(2ìx5ç¾S€.€ÙºŸº¢�WØ•;2{+2 ºëˆêJîÚW:Š?ê1xWopÍà6xšÆú«³šwv °ëlb¯IÞ‹YbÈ/ƒÁ~Ã,ä“£.¥¸-‘Ü?î€JØÏJèhÕÏ2ݧD][ô_Ãú‚„N„-ïn.”Ä�í�CŽ0[=ÉR¥ï]•¾SZÜ+IÇŒq+º$;%1È;â*"þ›>·ˆŸ¯†Â}ÜáŸá(±ÜÊĪ`Ž},ÞŸ’8‡I‰^œ;âÙú<äô¸¦á _±ÔŸ™5ñ5Qk'$ës›…Vù0J7)/€„p‰°Ò]ÑãÃÅBÇ& ò\u2¨d,,q³•­˜«ãÎÊNu Ñ“Ëó’ÎȪÄîóLÜÓ׉šKS(³óÄüã!G8=¡EC-À~O7’ÆÝž‘¢ožš FÕu2´4°Ðý¿xékÜ-Ìø¨œý“O8wºŠF\zÿdÝU Îõ· ;üüeÂ^“êëò‰ÿfý?g ïÕåtZ”)¼œm.„x’ebRYÿ˜¹˜¨z• i€Å"SI8•¨rm�˜l„†sUŠ[™U*üpé‚.^ôI,û½¢§…FìÜ •´<’•"q¬@DÂm©£xYò¸FM­¾U$Ü× �™Ñ¿(ä¯- m@Á (/�NX…yŽu¾ÍÈr¿çYÊ$,7·Üû`*°0‡šu?;2³*Ÿ²T@�2Á¨h¬Ìm-À&-ÉìhŠLâ#k·…ØÉÈîsÂ}†� ²„b ª?fÎÙL²Ûhz]µá©I�9oMIÇœŽ²¦¤×œî 5%½±ôƒqö’ì…Ž£�#q–€ Q‘Åi5.€-3g·1$;¬sæ,!9Xú-!l ×-ìû0O JñÞšð!�I+uÞŠ^‘®0¶‚º(ìÕöAQï"ix;< e¦@Éyº³R:™Àù�?oï=)Zmǰçªs‡Bd;¯ñ¥VF`(÷ªñ6Ÿ%ýÙ™X•Å ê© ˆ#SƸ4lF‹Ðò³ôL«ô¨ïË “-ÉaØ–Í˳“»“öàeªÍ*“ä³E3x×´ K´@bðø#…�,,Äù�Ó:Ù *aVdY±Æ_3eáÕ ú +æL{qÜÛ™.À*x/x¼«d‰¤PÓ’–wW/ÃjÆ—îŠuX±Vê¦gA *uŸ×VЀ(·pŒ$}?“}Av^žÁ4D‘g`µùT¡ŠB¬€ÄV3Bl P¯VŠp†VÈIÉq¦�jyÔ9�óþßIÍ•Kve¶FŒ™ 'Ó¬J�9·¡³!Çô”K|!®‹Ä¡W}@0'È«ÕÊ6­ì¤ÈçW×…U/Çî7ñÝ“×ÿzòw‘`«yaÁ—ß‚m�Z݆vÿ)–è–¾«t©Òˆqàá…kÉq  NdAž¦š«-¦ôæJ⛎ìHF!éà×›e­î €Ô^«‘!ä#THê4òsÀ=U–¤ 5B²2X:¬KMÕÉzA§Q>FµÞ03b¡Sç¦v³òLÑË {;X€„º,vB¡8\£Á»§u||F�‚—i[Ä,\«–WK ä`ûr Œ�ÙRA½£Þ¯ <ãG™Ë ¡(åœ6Cµ±ÓPô:—Ù¬Enü©ò´˜RÆ 3’R¦z $øŒŸ•æ]U<·_çÂ/'¢Â�}˜Æ!1v!|%­$‚}B“úØòÌÓj àù€¹ÿ€™ûX‰‹D×-Ì…m ÈÔrÉôTƒ…ú=qÍ�ëÎÚz •2,¥ð�²ÿE•!йSßj¹²›máq„ö)úûâóŸ~ þ÷‹È!¶Ç%0 ’ŸCΞ¾‘5€î(¿“ìßÅ»¥â`®öÆ/Â3¯XÀP’A�¬3š°×ãÎu‚ÕP>(3p+NəيE–ÂÈd°¡M�_¾Rмð¡Ι6K±(Õ¬ÛK}ÃùÜöŽÿ )ê�]ùŸ°¯:‡ðQ‘hø_'Ãá’ˆð�‡“ xJè§Ã  ÕÒûøÃþ×Ã{­Þt·áv#ÚøÇi Ùý¬KÌHaFÀTüogWþ§ž7©w‹÷p7Nõí®=&rœ‘™]ÍùhYo_~h‹hÄ-ÎÙF[uK÷òÊúÜÓù%G‚â£ÂìIe Áž¾.b�Ö†78c¦#Ž·r��Ãò‰[I±µEãÑi!M'€ê»í€ÜâÊÕ»Ì˪÷¶nQr0 °kV–*’qh}bàB.v8‰¯„£b´ÃY Šÿë)‰�>P˜ˆQ¸ªV6õ{øwžSb™£AÌi9¦$0×�…¬ êQ´zyqeÄöaàƒéÂÓZQÝÈDm¨é°p©ŸÑAƈ0.÷qøJýå˯Ó(á{ é Rð jª7‘7uh£@‘«[̇ADX€AZÄò…µ.m×6´Å4¶“7nëß\|ÿ„"ιóÌ}ý¥ ª+ÁÄ•c¾\•Ü)çþÙÅ@¼p½ó"�—uׇ@j?6œŽvm·ÿbÅ]•ni#° P‡ ôÎÚPp$yŒù�ᨠ;mV]­ïÈp D�fL”°ˆê�C…³M�Þ/ê–H׳Ô˜öŸÂŸ½§)Ò•8Š©GÐÊ"ŽKgjY¼8b a³°¬�D¨tö4�\Më›—T�[Õ(lƒ ð«ÔjðebëLTûa=ÈíI2 AuÄ%[wŠ6 «�PÏ�ÂOÃñ‰ÎG™¡{�aê¾]—‡”˜M¤2OZ»Q M(t•ÅZ€îC*ÖõÞ‘°òFawÄ]О/å[ê¦Ô§á@ j®®’Y° ¶°FÔKžP»„n Ø}6MçM†x¯wòŽ˜£T¦ w5½,ÖF˜E�G[¤¶Æ¾�¥œf%Ó”ÚxœG©åU¦fÖ5K¨Ñ¤’®h!ô—z¾°çoK¨H…q(JY'š]�¡9ñýkT6kõmÖP„Ní¬ xÉ1~øô¿�jÀ:зoòÓŽÛ�Ÿ‹M›W.w€…æ­Ï�c¨uaü�üíGìñÙ¼q?œë!X�äN#‡sì €ù1*Ð…*âìêú3ìL˜G_jÕ÷äŒU˜2BÑÖó/wW¬ðÖð àÖ‰¾�àÌ%<4ÅÝ4šSÌÄE¨ò]Æ +0 e¾ ]¥bdÇHWT¶u% ìBIMòºÁê?Ý' r�ô§Ó œ\ËI¦öÝœ·Èö¤EóÂ69J’í!‹9w;AXF�\qÐ=Ojm¯%ö‡Øgà›¨¢öÅ3ÑÔó<\ï°ÖÅL…n£]-Ÿ«¹Dž{ªŒ 1¿&V›]”E5�X¾ÖqV›p›Ø] Ìt®A­X-åEãmpY…‚,Å®•´“¢fæA. '‰èœ¨lFxcÊy-¯[ÍkjÀ¾üº¶mÏÅ~˜¢înzhÌçöãö€ÏMÉ㻀¯*2±!�ø’NÅ—§+È®P]ÑJ!#bÍ �ôî˜-qù/ÝÎElnO)wöÅöé‰t¾KQÎ(ŽÒÔO~˜ÀE/Gk{ÝB=»-ª{…/o;=u¼Â$Î6&%‡h²Á,qz—ü‚i(¨3®Ù˜ˆo£V�§[Õ•ŒÛ¥ !ý˜,‚©ÕwùžnÄW{¤ˆGZ]÷&ÙÑ>jÌWð²h¼Bæõ¨K=eÑæ ø�:eª¸ƒÉ¥˜S@ÑžÀÀpfºóîJiû 4ÛŸÓmÐöÇŸ¶?Å3µžŽ ?bA‘>4/š2hŒát­†n)ýÌRO;BC”G4ØhPî×¼KtâQªP­† Îl¹&gìãŽ?fQ#ÞEv¯Žâ,~ªH1LU–˜JÕ [î4A“|¶IŠ”Æv{ËzÊ‘2`WùN>8< ”ÙS´§ DHjiðL?×5§àÕ„Z¶S9µÂ –nŒ‹ÐÐ ©ÏÝ"ËɤT·ZZn õgJ’DÁc­Ÿ6ÈiK¼Ø7”½N.Ƀ(ÍeI¯sBJoO…kæAÄAÑ< í<{òòUýB>íI*¡\Í*5@¸Ë‰žWEÅ™$f ½Öc�ušAcê�êÙ Rm° ?/…C†$|%mäªSl¨XMSj7,áà!S‰„ú\­1?oå·L�ë„42ª§ÆPdÔ?äxè…{ø›¶ëGÈ >AvˆÛh>f¢Åcxô€Ì‡ç€ ÕÊÙ³9Kiü¤ �ú[FHŸy©žÌÇL½€]* ^äBfúfømŠ¿»Þùµ(¾Õ>>ßú¡Ñ~ôôU¡)h¾œ^Õ{»�‡dXÍi$²uƺ`dQ›ÆW-vÞg�àFO@¯©6r^*_“í¸Äΰ¹äáµöEZsC2j¼•:Þ‹j?¾ÞF}T¥*±ëînÏŠÙ•rß'5l>Å_e{z|ã¾§úæMgÑšì�ø¹hŒQõ?0t 6ÃX[ŸþJÌD#][žýJŒÔƒaý�N0)6¥(?r÷™®… Þo î;˜^¹õ…gíGÆœ$�O1çSÌ9–‘!¶”a'ns¼Tv/žÓEOñè°ÃŽ‹;®B9IàñÅù…bñ �þPht‚Äó.bQïw5†£Q­¢�xTëêtˆTÓ<“<‰ãP)|‡ô(\JZó�Úá9qRwuåÄœñr6×›»Ž¥>$l7N>aá%ú�âàÖ¯¾ ÇBg);�ЙÌéPÐ<ùÎé(ü£WO“”1©�ß Š|ÈOì5Z¿_Ì»S‰×'äû¸È×ó­á&æ�Ýÿ"rÌÿ�Êÿ³x–ˆ O óôO‡"ŽHxœíXMO1½ûWLs‚’°©R…–H8"*µ=ô8Y;ÄÒ®½õ�þúŽíMB² ù„K‘�–øcüf<ï½MòO7÷×?¿…‘«Ê>Ë'�œ•pÅ�î²ãݰ÷µ“Ѹ“®ýK¡8šoÐI­àÝHTô?ÍÀ}-Ұͳ´žåYw ù8œrÖçèÐ�ka/`óp´�å +qÙYÜvZIåm§ŸgŽ8oÁL (Ê9Í×P”híegô†–þzôÑÑîBóv†�?¢é_çYxÀ%äƒ~mЇ£8y•&»ié·ôé8ÏbÀ<«Ã¡}øáÎ`ójÓN�Á±=ei—� •í}é¤ ä¤ˆg, -P�Aüñò1ï@ãˆ+‡Ïlyü—�¡7y#œ7*�`„õeŒn'i«§ mh¶ÖŠÓˆRT„Æ¥/ÃÐèŠÖk+¦S „S˜¿iàŠrNó£Æ+ÿ ÀPªÚ;ZóàÓñ8tÂ$€JÂZ4c8’CÊ„Še?ñ\£²¡�`¾$!ù9ì•·é@F W5A”¢ O#YŒ€ ¢l‰NÎ|vÌaí‚V¢™a`)C44fbûˆGA¹$øS7l•f6#Ø)Ê‚(¥)Å3U¥šÎžMºŽZìVÒ”™ÏP›…©²XZö"ðIÉiq”*jdÑÔÿ‰¢¥²R¾TbªgM×O5·?ÝÉ©óc"ŒKEK–%F+ÊÔ J…l¶e?†O�ãMû}èU$Ü$½ŒXõš�øµJâ+%~…–´Ôã— ™˜~XõðKäcOÁ轌µL"æé¨G¬{ÀØÒŸ'²ÐæúVTgó±aÔåë=æÖ)Ø–^rŹ|# XÞËGN¶ôäÑ/h�­³�Uº¼¬™ØAŒ#Áú0Žÿ×8Öi†?¨høåª±Â5"­lm©ØSNÖyé¾®Çë_·äô.áˆ+¯Þv\p°Ûn¢mé·I{ƒq¯QHbméd]Ò·«70�ˆò½ÜãôóFö!Z5`‹E¨�澈¢´/¾œ´=d¹Î±åJ¾òm%¦W�(òÓëiö�XiO²Ÿ–¾�Ø}Þ…fîÅ–Ù×Nîµ"éxâ.îÅZöµ›{±–}½î^ë_­e|uXÊW»qþ�‘ÏoËój¢³†é}3žW1ïnp{%·³æÈ,ýÞùj¶V‹|—|%IxœÝZmoÛ6þÎ_ÁùCÑv (04¶�tnÑk² ºaØZ¢cn¥ˆ”÷×ïø&Û’õæA; ˆ#ãøð^ž;O™ý°¼ûéþÏ_?à�Œ£š¹JBøˆ©$8Ø�LP9är=úq0†ï%“]$¢<$Ù2ψd Ç¿å4Ûã»”šg1A4[ÀUîüdIä>¥âî€ ÐŒ`Nb:”寘’(�ù`1?­èVÈÖDRDDˆù`ó¯üŒàQ@�„UËÞáÙ–d‹û�³±úÄs<[-ˆ«~­`ÝÒˆ¼™�5ÒlœªÝøw*óŒc™å³uÕf’ed�™Ààp ‹·4�Iv… DȶNåXŒÞ ªQ·¬œÓ¬V+œimÔfQòÀ@ øE2tjèï6,Ø(µÌNJ}‡VÈ -,Áá©)¥­ÑÛ0á¡^“HÈ àf;&èÆ7¥%e:Z"»Í¾3J Á¾Rk÷5FÁ¿´Ì­3tò·Û1¹A˜'|ÄéX´¥øXÔd i&%�S¹oæ¤ñII è•‘ó2ê¥ýiXb!òGÃÂÔN¾î�~ÌIÔh%r6Ð}l€NëM§Zÿ’ï—šôÂ&ÔëçÁ ¥{‹9=.{¿9ž¼·lm±{¿ß4¾lf ‘ñIB‡è™‡ö2ZR}k÷Äz gX7ZYã8“"ŠÇÒê`è3Þ·OŽ^²w'™I‹f’Ÿ$sˆžIöœBgë÷F²^u®µÆAkï¬�ŦËöþfî<üÚ�Æh\¸�ÈÍ`1y%a�¸†BFc;)+ËÜZ™‰–yZ½ßã�êI#ì5<$Ì!ƒN£f);¶ÿÊ46ÿ1õR½�vŒ ˜^xœÅXaoÛ6ý®_ÁåÃ�rÜdi7´Ž�lY·­; Y‡¡( Z:Y\(R#©¸ö¯ß)ɲb9Κa@C"y÷îÞãñ¨É7Wï~ºþó·ŸYî 9�&Íð pœ%97ÜÅQå²ÑGc|ï„“0M¸•rsUî„Vì}UܬØk•iSøw“q˜MƵ͹NWäátšrÇݪû‚f ×DÎ/à⨿äÄŠ5M'cNÖÏî óãhã ‡K–HníÅQþŒe%:½Ï 6¹åfjד1ý² 6™Oƒ•ù”ûA\s†ŸLÆÞÊd\>ÔC*ŠÏz‰Ûÿ‡×ÓídzæñÛ¤åË}hðo„�nÕGoÕè¥Ýr“hYÊö}m2ñYµq|ê2€lÊÞã¦3Æïj�ÃW'Q˜šŠÛ&ŒÂŽž…Õ!„¾<Èí .˜WeÑ#†Èn!qÚ°¥p9s9øµ;VU1CÀ@BÊÙ'þ�'9ÃPAYBˆ{Â/j ³t {Äzš`¶‘÷°ý�ÞÚhìˆ2 ˜´1`K­R¡̶„DdR&TÏè c¯³Þ;&0SQ�¦˜,«²¥IM*1M V†iY`b¹JC†)Q“ÒD§ÑÆ]‹Q½­¤¥„ÍKË Ê/—V³9t–qË,”i$aQyî:yÿƒ` ôˆötåÊʵãš– Ûa¹ $²¡«[0\ÆÝôwr=£Ä!ß4mFj‡'»´7=I)ƒ%inQËÃJ/*–cˆ�7„é¡ÜtÐD[Ì\=1j½—µ~LRÈÚöŠÒj Fo‘ò˜”¨õC ©E[�þƒ-ÎÜ}ä„_ÃϬËNcî‚Þ„†`wÃW®‡ò±Q·ÒœÒþLÔj�ÇDÔ9�zelÿf‰Y¥$XlÔŠV g£n‘¦£Ú«0Œ‰ñe.°‡L¸…:–{ÀG-ú§‡*àV¿r»Wõ”sÃ¥]ö[K¼‹¨´x«{ä9¦v@}¿€‚Ð5ÈŽúvå«Ëuf�qXŽ‹5•ÁF…mÚóófÜân¨js‡ì—Ø$×�  ‚Bs¸«@°ÚZ›¤`®rØ®Á¯�ØóóÑ\8öjöatŠ=;7‚c+^Ëþ•^J0£™–rôASÉ%fU:½0¼Dáú´E,«Tâe´ó>sH.¹ -}'C-â cuï}ÔùS:Ë,8†¶´mlp¹Ð›Ò‚Ö3â›Ë9 ƒ°ÀjN_K¢š&4ò;îe—£;»RŽ¡› îï„§°›TWÑÜhôü/¬zX6j¨ ííd±-­0›Žs~9T¦p:ºÄ+uØÔ2kår³”¯¬oÖÖ¨Mxù"iùKA{UJ„k |Ô&ÁKÈøaÙœÞrrñ"bíî NØñéYË%@ÚͬpC“š91áéXYÜtÇ·{üÇß7c”úm¨Jc1¿ƒ×aJRa�@ÕÆ;o|mA Þ¾{ºÆ Ò)ç:oØ}忌5“J£K0Γ€NKI÷T­äÊ'žî;ÚáÅÈ´{ÓHæ–:�WGÓ ç-wIÞ9 JmœmÄî/Ù8 5½¤¥µ¾Å td2Ó‚Q<­pˆ½½¼~sùc4+Š2ô#A@©†±BQÓ±f¶.誋Úcx]¥›0XÔo¼[ÝK!eƒ§ÙÇ,|á‰ó�ë�HÀ}õE7”‘T¸¡š—m)…Y†¾,µ¹ôû.qü–¶ ¾tÒöNÆqýqs>£þJ:>‹ j új‘ ¢/MxœíZß�Û6~ç_ÁÛ‡^ìÆH€‡Ä1°›´wyi‹ 8 ú@K´MœD©$å]ï_ßáO‘²dÉ^mš‡’8¶¨™�Ãù>ÎPZþã㯾üþÛOx§Êb…–þƒ’>JªÎvDHªÞ_5jsó¯«ü®˜*è*#å9A«8¾‚ð¯5µßåra¢åÂ\WùA›½Ê‰"êPSùO°7 %Áœ”ôýUwü«Œ¨«ÕrA´å7G°ôe°ð®Ö8+ˆ”ï¯v?â�„?7ðUß›Uùñ|Þâ垈ՇåBà÷x¹^[ë~a.嬴¯íÐÛäÛ�ÿöÃŽ«ß½\.Œ£å¢Ö`VøCÅÁ åð÷8 DGA¾BvtÎö|)o~¼²,ðL0ù4|8kqI4„l’ILŠŠo1¡\½§À^a|[˜ñºQÞnÙH…wdO±ÚQ,aͱd�Ó‡ŒÖÊY„KW6Eb=öùWc+u9F Yá5X’Jh3îR%pøÃ×�!ÉxO3U ûcUÖà~]X€¯¾Ís¦CDŠâp� w3øßšbHত‚eàü=\ãûËvð/å¸Ö,˱ªìÔ` S£}á{XU�€Ø,G/›: È�SôÙUâq„BnÈl4 ö•Î¥Î*Á+®He«Ž†ŽÃlTr8§aôìaÒ$ý³a{�D&dsù€bWwÁÕ×).þð>Ï58‚â€ÎgO¡­¥%ºŒ—XR“·œÔÄ'áÑ4â1¢É,&á�(>�…{*Ô Ý�ÙXì]ÌÂÿ‚–=/ÊoÎÁw‰‹wýÜ»|g Ü0#ÿOÀs(h]žæŸÑK¿ÚÝÅ´ófXÇí·מË[þº›yþÂ/ã8&ŠÎYxX ¹Õëz…?Óš‚dUͨɽ¡jh²Ì´Ð,®)+â’PöxG. Û™Þ£×ÔŠÍEÓhYyÒ CåxNA{Qá vâQ>ÌwrN´‘�y;Ùž©èù‡‰H,kš± £9Â냹X0©Kˆ+E·Z…HÑP9´“äÌ ~º]Â&(\ÏõÒ`J@ "=½<¶=ù­w«h§ò-@-èžU�ô[Ö=S»Ø_l*l^£aðÛײH ZŽè¢2‡0Z;—+ã…ŠtÒí¹Ù£S›•v +>½*JRLßýd½²ù m…™ lÈj*W©sÇ/S¥˜v÷hÚÖ”Ÿ6©2�βûÚ’Í9DG´„¡UbC=e¤(¼â7&QP®c¡D©RÀ±`¯1S}…W‘¨Ð [ÑdDR}»4[Bõ³ñÒ�4ˆ�ïvBf⺎‚ÊZ(½ÎÏò>˜gë;ÒE\�¾EŠWëh±ÂJ‘ ¦e&äå-Öu_’nÓXåÆÀz¬}Á_V~³¡B£‹N‰Ì=¯ß°R¦A “·™ž”àݼ™†00‚B¼`åmžCT«¶�6¡ É´ÙÂwâúˆdNÉW'?§[ Ýžv÷K·2ÏŸ�È,—2µ»!z²´-±ðœ¹£È [Š|J¯Û'òrPå?hùß‚EБ9ÖÅ�¨Ê™ëQx ØïQácœÑ KÐ$¢vW«7?|\¡åʵ„ÈŠ‚ÉZ Æ~âð+d.(kÛÞ ¨ß¼Þ|ó©t¦`œšš1§zÂnzaºŒ§$R¦öaç_ìyl"Ê)wàÕÈ›z˜µ©m5V*c~Mg�Ü‘šž&�2C=æìŸ9ñËŸt`¹õIE 8ªÙm¦¢´�œªŸ5e£NÖ«nÈl½^°7\ Éíú7åT�‚=ê=DØ8t•²�þ©G´~¹ùx±p9Œñ…úL§Â¶Åˆ"�g$ d‚ úw¾ÕtcJ¢˜¼Ý³6nð¢´b¼‹:&LzÅ&•iÅ [‹îÁÿ§2æøMŸêÃ(È×FU¿âu5¢ïª?µCÃQ ÖJ7–¤®Û§’ÇÂt—ˆ�r‘žÕ»¼ˆ�¿<ó0žM`›�N·1�Ø1Ÿî¦òéßC’sû¼ôja³”_cx~±€� wiuU³8B¬¬ÝMÃÍ~ã�èC û\tÙ�¨ƒs@É$*Á€Û¤èk”æ¤çÝTzÞM§gO­rD0C½Éüêå1êèÀø³Ô1z†ì8ÅÏ0h¶ý.²ØÝñŽù%$pãl[uÁnÖÒ¢_´û^Ç‰ÞÆAýV“%øgüÚMœéݰç°À·‹,i6iM³ù5ûûW5{þe�{U1þY¿·ÔÿâÕµÇ6Ì„å0ó2›îèÌÏÝa�¤Y¸×Ùö­ù¿_ @Œ˜ ¿xœíYKoÛF¾óWL|ˆ@¶£A G`7 zHâ\AÐÊ\š‹R»ôîR2ûë;³»|в'µs X¢Èýæ›÷Œ<{öæò·«O¼…Ì®òE4«ß8KðmÅ-ƒ8cÚp;?(mzüëÁ¿·Âæ|³œË„é7¥fV( ooJ– [“ ¼¹å. îïšÙÔ‹fÓ¿TIEÂ^.f™­ nÎà‹Qñx4c يφ§OøÍÁb6e$æt‹1ÞÅó§x³€8gÆÌ²W�|ã¥Å£±J¶5=ƒÙšéÅÕ»Ù”Þa³å‚°– 8r·Îý�‰ðÂ_½˜MÜlZ�È\qc!Ux­ã’Û çrÛ LkV™“ÈŸMĺ&¼2ǯ<œ';¤Æoöá€Í81knA¥îSIËn£è]d˜Ï{Ø549Ns[ji€A®®ªç5‚�°Y<ç+i ¬ ²¬.y�±É¸æ»ÌÒg�â¢|Š{O@¡Jz# ï‹JYnjY'„1B^Z¢ä†ÎG •õ tг8óXøôï\Æ|‚r«€ö‘}¬y!û•PÔÜ”ùB­yj˜Úu<�ó&èïJŒ´%*-þE+ªU�æ[æHz“ ÔÄj&MÎ,2E¨V…¦–�p!€¹åŽ¢ý$yA:,ôÓø•ö1¼æº‚D Sƒ®™ÐI�j6ß8ÓPñ¶ÄƒécmÕÜ|‰f½Ê_3UÚ¢´ƒÜ që(²ð9’!ÂR*�¿-˜çÄâXé„|퇥V,‰1ùé;]æÜtŒüáüêýù$<U‘ªÍ\2ÁŽðœ€!ldŠ/2ëelÙ-pk½eHÙ�C¤\°:iž¸hÌÆeÕ^·E„·É˜ ¼žksSª×ÔF†0Â@'‚u %Š×lг(’]kÎëø"š\<ƒ¦öaEúÇÀÑË”àx��=�~é–Ž­„�ÀÓ]‰fŒ0×ÖXÔg ¤žó܃5Ø}ûý4ÿ:ýy¹�ÛR»©‚YHb\`%•¶:NU©!Ã?X]„un’Â2M±¤ Ö�YOO±¾ÞÕT$¿«©Hþíš a=´©`ˆþ�m“ö±Úʳ︭4]áa­%ꃑ;žg<ÏEñzíP«�9 bïÿõ¹Ë#¨'nÏ×áþßÛDkj[sÕŸî¡N×e;ªòÞÓTk,O`<›‡*kÙ#Q·¯qýÆ�„�Ò+¿"Owûû¾Œî –šQŸç¾4qp©éš‚Ç"ÈzYEãéäçŒ09pAƒS-0Uy®6í´u”.é7®\ÔªzÃÔñGØ…ŽL/î0}°%Õ{fbT‹¤4–Eì¾e8ÞïÐáÌ ´ÇŒH£6wkSRäA>ÜU~¾Ò‹v™7ƒâžªÀ8¡¯sh*´±÷øÓ=ÓaB2höŽz>™€8A’t%ÓjU(àÑ�èûDÄþס̓çS¹u=ݑߵò3q�íM  �OªtÆô©T�%Q;Õo¹�éëÒoV]s·•<¨rìUñ»æiÜ@’mzegV«9s3—3>Eõ‚-Wh¤Œ³³Ÿõ£1™  ½!ÎJƒÙÌr·1h_…ÜX¼ÒG9m”ž× H‘>BÃË� ÈYL¬“Œ>N,2qÁZʘ®›A¼†…\`H4+ˆßL¢‘ÊJË$0®R¿1øœ†]%óŠÂM$n×8=n+–ïj.f+ >×à zo+拞ÀU"DCD�ÎoaÍc«´À¶¦o�Ñ™dÇP7 Í×B•µÒ´yí³b…;½ã×€þăUŽ‚‡I·ºãbRÒöK+Ôh–tÁŽúTZ#¸_ÚÓçýÓýÓƒ_^ÐßeÜGÏîð'7¡$:ýÖ-Ùò‹¹OÎv²ïô†9 be C‹2Ø›¦á_FSÿªÿÜzÒ aa™,"²\JxœÅ\msÛ6þ®_�cg.íIJ¦ítææêhÆ�Ó6w�“Öîõz_4 ‰hH‚@Ûʯ¿Ýø"’LÉ’< ¹ÂëƒÅ»‹^üíêýëÛ??¼a©Í³ñà¢~ žÀ+–³8åÚû*ªìlø�háVÚLŒn…•¹¸¹ßƒ‹‘Ï7UÉKùÓp»(…ù'k“Cøà‚³‚çâUTGã‹ÇLßt †ƒ‹’Å7æU”~Çfþ á§…ô±JD·†‹i'ëÊ££ ³K­ù‚iQjaDae1g¥’…5L 3±Ê` MûI‹¹Ò’,æ™(®Ï® DÞÕxr3ü.òe³�¯Ÿ~2Nu« N¨Æ ]ð,[PuÚ7,QÕ4—Í,c½OU&ØB@oœ±\6…wÉÎXª* äEÂrYTVàÃîE– �0"VE‚â¬J|¥f3Íc+@b%×öœ±[ÀU‰ÐÙ“ɼÌD8& LBCTA pM~�í8O_ÔM~ýò%ËäTs½ðB#bi ¦f�QšÁJÁL"F˸ü±Vï´YÆ)ÅVSc90 Î”fÐãzf@ýfê¾ÙÚP´Ê2AíF<˜pDÝ "¬VÚ” §ƒ:Xœã0;b·¢Í雹©bžÅR-w©n!ÅÿTѦ‚®s1?*�s[‡—Z•B[d:ˆœŠ%”ž8|Òzþ@—%Ê”‚j²¦$Õ‹t8ôG* hF"M™²P­À}Ëe�̦ ?R®l&E– Ø—.×gŸaä>ú]|uFP¨‰r�" áq¬´H?!0('Ã{™5 x”‹éb…¹çˆèJ»Á4¾ß ‰˜ñ*³g5«_À(æµtž‘ì}lù�š$s&c&¬(HDm RhRuoòÛËëK`ðtªÅ�tŠò'î„^0âË—â|~îëzóææ¶nò÷uýÀØŒO›ñ#š¦ƒ{wyûËå׿S¥¾7l*R~'a(£X«�-ª€Ž½Vúû2d¾ Z I Â$¡°¢¢h_çï·¯_~ë+:>ªÙ Y)2#îS`>pë’á|—‰  ä|1` š©±/�¾s|¿� 6ý\w4û¸‹ë¿bJ:9ÖdYæÖWóF¬Ž­p÷·"~a¬VÅ|Œ9qä�jù c zR-f¯¢zj¿ª4õì9j¿/VC£ñj*ª3Ö-)Y*!ir&�ÞˆSs¯a”á@õj.ývÜ–ç:iÇ?�?4X&dýõ1Ÿ’þµz|a¯·öEÃN×óQ#¼Õ�Xü¿ò³Å̧ÀàT·{�õ uæÂÇõb |·ÊQÝÞ Ë^käÆÇæ1`;™4‘4<+]BH>!{Q'|�Fôgñ+oÙК?­âx̆*+Ľ×(^u‘æºãY îÑ-ýq‚À‚ùÂÔöGíO0Ï$KƒJËgpÓ'èÖFµBKxÛ5ý ”óAÛƒ]h``;"Èq/ .ÇuTS ³µ½€ö1˜QB³;®ôaûkàðì�ºÁ ¢åÃAa.ò$Ã#òO0aÏØ;´`�ÍzѪ‚aŽvKW²�ñrH¤i �ÒŸAR2©Ô²LWÐòcâEX+€¼mÑܧ Üø<‚ø«‹:!Jrž¢1½BI4Oˆœ:T‹ "£#CGF <× —Sv+�SèWz;–bNÑ9¶ÑؽCo¼«~:Œ÷B|ŒÆø áûÂOjF¤!C0ßd¼4`t,ðrÀ¿ÈeRÈyzÌŸ�ï�=µÑÒ­=µm07[Á7Užs½˜¼-œV;'ÑØÇ°NL;w]ñQ—Ÿa,â3(æ¾oGlÇP$µ;½B(®«|*È݆Îÿ§e“£Wú�^AIP–gèÐ{lÝ�Ã"7wŽšÑ½CØ~¡˜•žsvïóBù(?sX<ö3 ¡�_”B†±ä^ìƒëå~l4ή¬©µ¨6™‘‡íVêÕ5û|2¯òc#â4‚ˆøÃ YU~t½XSáý†’ýÛû§èɪ�Ÿ*°ÎÜ;„ðwŠ©½¾ãŽKò* Ìà j2¼qkY“ >ë|¯VMàÃòÏÒkBŠ<>4#l"g3Üø&b<ŒZàví)0> ²wïuÅCœUFÞ‰¡ÒÇǯÀ•T2}‰Úhc6À–u´ðЦüÚ—òëÂ÷…6[®¿æÀ¯ùs£u*–‰OÑX| žö[SG„—àÂ×ßÞbHÛì#È2�p½z?«(PpWYXTAY}­ ˆ·b¿9v#‚TéÏ„Âá; Z~ÆëÏ(•5¨ÎUñà Ãs=„Ì!dÿÁk”ñqqiQÒÅP÷o”0¢SIØÏKÛŽ�‚ƒÄ“ð¬lÈìf]<–ip GqcuÛfCÑi‚ v³B3Ó*?,MÊK�`‰3‘¢"–:ö†NC™‘õA)²yh§æ™6h¶`„ðœîybý ô |ë„�[n•2îzØõ.›à[ÀÉ�Üï-4n-7?Ú¦²hÜ�Á…ËTPÿÛªÛ„¯'~<›î{~RÅVGÝÛc‹›?·¡ü¦òÈÿtã6¡'‰kwOWššÆt÷~Jxð„L€¶JÇ£Eïq}�%Bm†]„6Loó¤‘ì˜}ßéËÀ÷Þ@PÔîU‘u^ήشîgª ¶LβSÖÞ¯o·á™Z²£^Àù¿‘;ïK{Þ-¸%ÈEð¿z L5³¡ üÖ¦ÒÐö�Aã ó`hHÎFÎÔÒ† QDhØ;Ø�wý4o+3Pì4ÐÓ¨\v!Õ—}“Bx/Mˆ«†.Qg¯©F˹ߗS„‘>ÌÀÒJć׉*×[Sa›YP¶Žù$ Tq´iXƒu¬�]�±`¤¶xy™€‘“š¹dp<è…œô¸Œ×_‘®Ö?ûØW ÔdÔÏ$�žžixœÕ}KsG¶æ¾~E�&¦eE$HŠ´EÊŒ�%ÙV_Óvéë¶;:T‚¨V¡ ]Y í&bæ_ÌòîoÌ/˜Åì»ÿÒœgfÖRT»íèhI@UæÉ“çñ�G&žÿ—Wß½¼úéû×ñ¬žççÑsýÃ$)ü17uOfIeMýù£e=~öh>¯³:7çiR›:››øeeà¯qR¤ñ¤,nLUÇ�õËÇq½Z˜çûüJô|_†—é ':Àa|ÆžÆw^�ž'q‘ÌÍç�ôË=ýË£óçû NpxÞùÞ=„¯ñ$O¬ýüÑì8žZxaR¦~U§ñó›¤:¿z¾�ÄŸÇÏÇç~€ñyü ?ÿ¸(o?ß§¿?‘?Ÿï/nüºL“ÕGža^VÕG^ÆÊØÚTo)øÅ+øè²®²âÚò“ÿŒ9±¬ð"{gÒ/ËjžÔV9�A¿üèyS,–5ÓÓ"çÍ·_^\ý)º AŽüï³›²þÉ$UëÙïßüûwÿòÄSN’Ü´üæ»—/¾yýQIÿw3©ËêãIÿOn�øÇÅw�¾úU§•}Ý|ôM㟗¿ õË�GðŸZû’ýì—U9o/ÀOh?ÆÈ¿›™<Ïgnq½z»“ „ÿ áŸõq_ý\m«põs/çÅ…¹�“ªJVq9õé&É—ÆîEü|šÝ(Ýs;<~ÄCðÌ-JÝL\™zYf·`É’Ê¿E”D€ËªÊØEY¤`+㺌뙉'Kø°¨éaBo„»6“m¤#³4°Mpf»mAúÚ^¿~71‹:ž–UwÇøQíª¨“wñ$)ⱉ—Ö¤ñmVÏbŽÌ2ˆâ5˜‰V»ê9W3cM\.ꬮ2w‘†(F.‚ÕÎn ³m¼¬™Z ï8©ãy–Ùõ¬ÖíÞao7¢xBÂ…ûÛÞØ)XâNmÞÕqVÄý#Ý v÷QfEmñiÞñøGXA¹¬#àËF<ú€{ŸáV§¦VÀ8"¦Yeëx‘™ po1QõŒ/Ê:çIñv@ì7`À@GàµJÞ`é)€ÖDv•^Å}�dÚSò¶Êêž„K²éÔ� #y2Ñ5ì~)Ô‚*f°âò¶ˆhËÆfRÎ�Jè·É•[¢-yV¥Ê|9/T‹-ŽL”%"äÀÉ«gÈÒŒ–87 �ê5F®^!©E©4ˆÌ6¹†3å9¼‹¢5%y†�¨Pz˜M@ò…V=+m“ÇÖFDšŸÂÍŒO õV¼‡/ó¼¼…uÕ@ë Šx 4Þ©²wk<óc6eCñ¿Æe•Ânè?íœÞ­ÎŸ×¨;i=ûüÑÑÓÿ¦Žðñþ^\ Óô±ó\u>}rO³É‰\‰xÀcÝÞÇ_}:Ÿw­+}|jƒ0 6ßd¤Ù¤ÆS,vgNȲE÷ŽM«XЈaÍÓ‚;[$hJª€ˆ«¦�æ×æ°e0‰øÎW/#Øÿ)cçÍÜ8òû¯¿îûìâÂm-Ìê??õ_ P¨3­À Ð2“B¦q7yªê[™¿-3Øãh´€…ˆN±¸�Ú‘ÆÓ’м¸¡÷xÛöa§7mwšÂf_ q×7î÷¼,€pÌʤ¨Ó%Ø"Xw2Wæ&Ã}€Õ®Š„Y«OÆå�Ù‘2 ë"3´•´„”¾•TÀðƒãAŒJ¨/Øó×`‡3;#C4ØLìÎ4î§éþV ½€ÀÓðNø ø b2-·%˜°ìš|ZÞeµ…Ò3Ð@>ÞyË÷..ö¶ @G¬1ÐÅ>8§ð7$(ê¡h‡ëŹV2‰üƒðԳʖÛZyè[a^ sÅÅr>+!–Z‡?Ø;Þ;¹Ám�¬ØîÈŠ¶Aê—/ß4v+yËÈøó»ÊþmYžY²O�)÷ÉØâ_ÀˆŸGdî_à‡´DÙo5 ×IVXvX L�Ð׃À\lò,o›¤Ž?£ã½ÑÑž×…$8~>m8#Ú�eEÖØÎ’ÂÈŠ,ï†ÚžÒ0�`#Ä螌âï&uxø®ñix•}Ò––$N˜Xv"/91Y€PNㆪǬíMM'j˜Á´e{Qüírnªl¢ÒVWBBÀï$MÁv¡Ä…ÎÿpøÌMÊíÀƒÃáÈ?ƒ< ˜ù‚=–¢OZªG=ct˜ÌÇÙõ²\zþŒŽöGÏöÃ…hˆtT¥è¤âKÀV†tP£LJt�v¨_ŒŸÁ$ð ó"ÄlÅùºB Êñfj&YjPϲ½& a@Å à`ßɉĨü‹Dd²¾5ùW`ê:7–�0¡ Bo¨Í0/|:?YæØÕ72)Æ�ùiÇÏfV�ø `$;'�#ž6ÇxœÍX[o7~Ÿ_Á5°•ƒH–ã4»©¬ðÆi ʶ+Ýú�š¡,"rJr¤(¿¾ß!‡3#[´íˆ-‘‡çò�+9ýÛÍ¿ßÞþöó;¶ò¥ºÊ¦éŸàþ•Âs–¯¸u¿9©ýrôúdŒu/½W÷ÂËR°ÊšJX/…›ŽãV67,¦ØÃDÎý¶nÂöžI6åLóR¼9Ig?[rr5sbsqõ`ç.°Q±\qçÞœ¬^±¥Ãï_éXnŠNÓI¸eÓÅUb°ó@6WÄêŠÝHW)¾eË@‘ÅõB®“�Ò�^�ì#2W‰\.¥(w,‚Çs/,[‹ÜËð뼕úž¹œ+nÏû°ÌÛ!2é™t,7z ݉F 1¥sÍ•Úâd¶GâYÖšu»lÍU-X„ePˆ%¯•4ö“NÎá£BTBB{æðGj¡&qKŒŒVÛ¬±—�&NÅh6›�jü$nψ]í ãf%4Ð}Ë„eà*=dùŒ•²Ðò~äȨ("| ññ´öþý¤,'εâ2füJØ�t >e+ñoíåj÷&*@åŽg���pðúB™Ü�ý.€¬ðµÕá>;£Ö°´âžÜ1̘&{Ãf^„ eÏ×,¯áÆ2#ÀµTž--VéðÒ(e6$ü³–¤4;ýx3ûøŒ!VTá.3È®÷DÁ™ćm$<ç(Ô(™¢Š�¶\ëmĉyñ•Â*C\UxÐwFeôDÜÕp”¾W‚ý^N 3%!‰«³ÇÉáùB‰ ©Œvr-(QÂÚ‹dè„Ço c aÛ¯® gíÕÔd”_½9yñúï)«·�‡�‹ë6�}Ñ?òúG~Ü^6„Ût’Â3ÂCÕ‚ù�a…¼‡ŸÎ"�1dP`vHàÌh¿š0]— €}º{"©"ß¿ây‹†>úFþ¡®ÄN�߆pñl�ybwP;#ºO ›QÃ5•ïoøi–ÆÝJZ¢¶� 4Û#ìîA6þ÷Á÷¯‡lúT—ÂÊ< +Ñ6³\¤ÑãÐÍèºäU–þ?žu ¡;0Ù7í¦½¿0ïv,O¼´ÇÈ€ƒÃnKõÿ=çÎ1[°ó&ͨ–\ Zçb´á…š‰)çŽl"Qqp[çEüš‹Ù=F�P}©ó@ÙaÛð_¡\P–BÜÇñ‘ÞÀ lBlL­¡-40J”Ý„SÒÿ�ÏÇŸoßöfõ,a ÑóóóÉùy¢Ù5—ò+HcxÆù܆˜Íe¾j¦U³p®�𥢡œ9¾šÃºá1‡Ïf­V&ÕÜÁhg#´NrÜαÉ|Þž —‚¾Qq� �#N¤%:øB°º"§^¼ÌuA¶^ý€{u1Ç„ Ã-fTŠ8Nq\É…}Ý[õúŠ T¬õ é÷ÚÖ;»À¹x7è)xú›°¦±7´�ò‰¶ïëŽûO³Û´šQ ÑÚï�æN,X=lUX݃@XŸì±8Øïív¯çÒx 7*¾€JßY‡kÄ%.&ÃqÔøšB^v5B±S›"öŽ^v÷5]dOäÃ0L]xFÎŽ>n„RtóÙ‹aeT‰Wž•h.Láù#îjU?†æíí 5âͺ®ê)‰·Bo�{í¦å¸¹"(6Üj$4þ¬•ü"Úki1jHykðÅÉûpk´”¿¸jwÑyþ²W:�`Òó—ûôÙ6ðD½…x¨š’�^u€mFÅ£W^þ%tg¦ááÃõ§ëØ6¨:²›ÆŠaØ ÷ó~Ü}¼šÇœoÕH—slÇü —©]ì 'Úù²ä…bTq\º©ë†’J…7üÇ9Î7*äMÕ@±–\M,@ÿ �(VáàŠ¯�•±žlm<�‹¦…"ÝÁH…2ØŸ´/Œˆ$ c}¸‚[K÷%&¹©5tŠWôf2ÿÐU¿X£!yCÔ%ÿBº… @Jô¡3ïr‰Ô…�°‚Œ6çxn�£jÙa—Fæ�UT�‚2Vp Ð;s®IóéV.¤¦öaésÅ)É‚3x²%œ%k³`n­ïœ€^ʬôò–:YÓœ¼IÁð½7\Ò;Ïh6Åàvо÷Ü ân×Yªx@· :šhH"DCw 5Ž�7ü!½l`NÔ¦yIo0°šm€+¥b/^.3 ¡Æ*ÕÚmBå�<‚#—Fû€f žc¨Öd%…Ù«~½uƒ‡±…ÔÜnYó^•^¡š\kFÇqó¬:Žïµ‹+L(g„Ô~#.@SüòÕ�ˆ­Çvqü€òz™\“Û¤*€"¬²Ÿº|€Ûªá´¡uOH{“Ì¢ð´ ZÔ¿|¢� äUe`ˆÂ™Q($ÆJè@’’–`#'0 3TP9««â‡†�ëòšÑúĦCõÌGx ‰•·¸½�÷OyÇ=Œ0@«“ˆk r€2dLf ;’Ñ›EA.$ò+:B,Äö#Ö8™b”¬vRX‚/© Qƒ¹ñ’÷©hÊ fØ(·>0–hórML؆}œžC¨Òµj°0– /†{ñ’b 6¬iÜäë?ü¿ÿ( ãRèB¸Îa~yá¤ïI`Qcøê1ÏOkq¹H d ²éû²ª—×KcƒDÎ1l>ìFõ¾Ä¿„„=„‰’^Ó-BB¬�/-鎟Ì3àg²ýˇ/ÿþ¿ÿþþñ?þñ?ÿþŸÿ¿ÿø_Q“�“ÙvãÄý‹ó[È< Uø‹ÿîm û~¼1»Ò3?v[Ú"÷ÊWà]߆éˆI²Èê$·g�ý´Æ{\´£¦ß½5,]k]-M;?§Zîò"hè j�æl8îÐ( ¤! Z¨ˆÐ~y�àfkâÆÀ0í}B�> \�¤O iÚ!âùØ1Y¦àŒIÀFòn;Õ¢ƒ? �°4Ód™×g*³„SpÝ€å+0³ñ¦Ä  C! Òà8Ø7)a·|ùÆLœÀL0bY-*°D_6_”Öf©>(i™± !VÁâÂi¢&i$$ÚI¦[S"í"YñR$Ó[“Ã.Î3‹>URIÎp×ÂÜHƒéøÂ„aëíl%+Œ2*ÄÉÍ>D­­g‚²WÎG!Z”™¢™9¬³É2§8j�ÞEPZCl±À~‡5YOf Q~‹!Äö„•R*~ïòºÆÙTõ¶Wvž ;€cüPd¸˜ø›Wß° 0ÅYÍÇ%ˆŠ+®w,ßEç“´óI7]1ë¦bæ�‡lç“ËÎ'‰ÿDë O|Î="}¡Ô$š‹4³‹�kÏïÈØœYã¬h Š$)ÓšR×$Ð 8E`-€�b¶D&ӒŵoßIÞ&%(Y�IÞÛ,Ï#ðCRa…(„-}oªrëØ6pw¢`L‘þ¾ÂˆÄa‰U¢ ð`™Ie(€ñh%);£"®0öA#–°$ªƒ¹à‹PQO¥º¬øAÁXˆÈúñàËæ؃A;šî±2ïãC‰,¾4ã É Ïì#í“I®ŠÅmî1’˜E#OSÉBØX›ª*«3o˜œA°ÆÅ0LÓLnæÈ]M½"ƒ¶çã)™¼,Ð8’³q|›”Vƒ2™ôwš‰U" ¤í)g¥%ÇæÎá`¿ÊLq:ha¸ÈÉk]ÃY'>q®ys™E&©Ð¦µ'a{¤s…bQt- ‡Ô¬ïylŸetJü¾£Ö?wíßz²›ïü‹ì(ÉA˦&ìá$ˆŠ÷m‚ 8�pçmLÜ!È$eœ³ÌŒøHóËNœ"W¬HKàú úðµõVç­¤(DYÔT_™O#Ñ!ÙÂh„aßf L•#©L) I+Ê0]€E�ÊüÕU" ”1½Ñ˜”2¢Øg]&Ùó4ËŽ©Q€>¨º€vÓgÜpd[Ñ~ 0í�]¸Yd«Ž1<îÐ0õ~6õ}zê?Žƒð–¿9= Þ¡-äD>©Ê3£cyç«‹«¾¡~¸zÙÞGÂÚÈw±Ñm©m±I"ù(�ú†·mÜëË«ÎJ_¾î¥íâòßÂü1ùƒÄ…1¨˜‹p..EÞ–TÞCt!å:Ðe²j_v£c©_ºl‰Ûõ/<¹È’ß�€óQ3Þë¦?~?:�Ç:O½|ÑYú›Æø‘LÐyóÍëÖøÇ°ûݧ¾ ŸŠ6Ïüâe{iÏúÆ|ñCsLðO±Z#}qј#|¹?ÒóÉšBSF}›eV¨Q!V<Ô›‚ w0±º5�oÚoƒ åŸ µïÓ©·–G�„6+8ak«Šµì’ÁwTÎ'OÀm.+‚Cô^ÔJŸ[xì,©¬?!›ÿ#úã2Üž¬ö¸ C�ºÑü`Ì—6>ýZ¬Þ¹¯� ¡³©À_JŽàcĈ˜lÌ[„‘òOA•Ã…©²2ŤÊ[äþÚ�cײÒöŸ%혰AÈ»w¿üô“×´ÛG€õ#Ÿî!` v.w)D¢R\µ].%ÂÆ =DugÜŸ¡èÇÆiÕ13©é|dº)�ÇYÝylQ÷š“ëÇg.rZÙÚÌCC#±L8O<¬ö‹óÚXCÕPfúÀ¾¾ÿù|3™¿0A¼Úó¦ÑG Ñ`ÖV€§¯‹ì=Œ‡Nms'Ø·”¯ÎkPêJÒ° ßA IP`£ÃW…ùŠ@X…á$YXTHˆeÊ� nÒ²ð•kSКõGôãÖØL÷L©¸·‹�pyäÍ äËRîX¡ÙõÛŒ Ê$Qôµ8Îlµ@#‚­ˆfSê)¢‡ÈsXLû šµ¨i5üÒ;‘»[‘Þß ›Krd7ô&­†AßZE?ØTóÆw¤q—>ØÕ.¢ë#–Ñì5”Hɪýß8/ã‹`ržÊª�™/-E¢2w‚”æ§ÎGx”‚XµN0E£z &‹ hYm�‰‘\�”¦¼yÅz�ïøËW�½›ÛŒ£ÞNhñËX¯h&Gœû…åC·¤È¦Ù2h÷¢Í�ÍdƒIwazkL?†ÛØ”Ξh†¥9-¶7ÒBõµ$l$B—ó+oÇš¶ï`w0C ÆQšžü>µÞ(BÛDoSzëC×û!Mã‘f{ÄŸK©]Ô\íÏŸ:—�ÖÔÞˆÔ„%t,HŸa_W“A \m£×W ukJh/ÝÏ¿åÙ¹Ã@Ðê‚ïçûðeøv3æ[¾>x6zºþ‘Ei³wt´mý(‹r2ÛüÈ_—yZä®f^¦Ä¤]ž]Áó¹ïÃè>QÔ ‡e~£ÙÙ™kM“g– ˜'O}iÀî^ß@Sû0#í¦~˜‘êúpd5ÖŒÕÊwúÌÅ7&Y\²Æº}âìËâ‡EC®JW ÂöQ,5}ü£Ün›>âw’õlòV²@ƒ•²ƒgŸŽ†£ø_}ölòÖ~o*¦;œ Å•ðO9pñ§Žë†cÈTk…˜S8g>NV´Ñ7ל;v6s‡º¼�Ðìº|È�²‹âK¸—]Rcº´³cZ'º¶Ë‘ Ö ¿ˆi»¤]Ÿ„œ›€©IŸïË¿)Ù)¿%àŠ)Aÿ5lÐ~h¹ˆ�=ÝM¶7“–טùù–ö|þBR“ÔLúXª¤vGõ%ŸhE îK�}F½aÎáŸ~¿à,pƒ }‚4ˆèžpø;&ëž4?£™~n»µkˆ~‡ç-:ÆØõ�Ãážg4²±¸¯ï)§ŸV%åj¹Ò6£¤šiô¾;Ö©Z7꘬½²•"Û~k]t@Ôo¦ý”¨R‚1G{Tl>=�|6)S8æâäN) 3õK~œªZø~/Ðľ•ZZX#ÆWeûe<Ør†¾Ç?!@‹à.ƒØ´˜¬(¢d§&qBUŸarFªOÜc–r.K“ÒÒÀ¥]ï�–QkŒ0?#Ev°ßø…ú"Å‚*ˆdf1€Ðr‚á„–™qy>‘Q0î  ”,Š+XN:ÄnxVÎÊ(æÕw@•SQ:7—L“à"HÑØ0èE ƒµ Á':FsÐã~½ž40+¡_Œ¨‘$-�ðguP’Ã8ÿ†š1´]p™”»ôhÁ™`¢^î¿"§æÌå{–YÊÿH_`©‚HQ[âK+^ƒ#¿Ã„ µý�  ùcÕ0s…] ,�ˆñ¼ôÇFR únÑ榸–üìý[c ÉáÅ diä;˜y‡G§ÇÏNOœ‚rç"jÞm‹,( ”Rb�›�ÉF�‹¼6\läV´\b¹‘Ë•ˆ“1¶¥=¸ŸT¥‚{Xƒ±â Ôøçz.Æ(²Vs(KàÅ*¨)u"lt!q ð…øf½´®Ç…¬éV}JæÝ,{L)óQ¯ÈÿÀÃYa¤ÚÒW®BÃIo¢7­XfǪÇFàD±ÀýдcÑ•d=ÄŠraª¹XÊY<’>,‡PgR¹kšžÒ¦¹J0¼(áñ÷ß]¾ù e€ðKm9,i(ŠDn ]1{‹ÕÃe!í‚cqD* NP¹ùF*ß8ô©ƒ;²äÏNŽž=sU@`µåÑÁÉðàpxtढvÇ®Ü#Ÿ2*¼�°zwxìŒÌባ{~£Àx³’»Y‰:b²ó-µ¶õ²ÒY¾/¤éŠ’ßÀšüd‰)ñqkÜE³qЉ9{)ò8µœ«kœË"§ŸðQ/&e‘dØmå}ÂG–d~ÛÙªõ*ì ¯Æ@lqD€�+„c=  ‰?åëp"À¥|ŠåQ>Fçàá�Ú¾�Šuʧ“»•­uRõ[{5‰S;•ÝÁ ñ‚­;žÔùŠzâüNÂü.§Œî&Íl€Ï8B d„g%LžïEä]OV]æ‰Í»'8«Éþ§Wf‘cd mž-6Ël;ªÔ§ƒg'#6€©}º¸-ãwM7ñ¶i¤C)IÏ$û„‹ôÿ€?‹L Í ˆ4-8t_a~ˆ;‚˜«[>äËå\n 9NáMë¤gZ;&Š$ã.U=¶÷`‡' ¥àj¹½@ò²ØËh8y/{J)‹ýäã¶,*ô5®Õ¡Ã¤®.d$ ´ÑQŠáÅ Œ ­(öq &µ4 I„á@ºß(¡8Š•÷¤.‚©'ÅSc'U6Æ—gå­k5áÿÍ5Dþ²uf"À„=e[Jê™ \îÛ¤ü¹r³S\Ÿ ûžvÚ{ÞÛ¹ìs²ÙO¨±ƒO4’![Ö˜¬if5éÜÓŠIFfM?¦kÇ :jú¯‹áï6õUuZ€´„Á»& m,ªõ¼ê{ãn螤ã2®g2_ ÆÖü�0¬2Ÿ’ã´ÔgƒÇª£– ·\üí p©“0h1su´3ô,¼”àd·ôwÑUz|Êh+&.ØÖ†r³2›Ü¡çý¾7q`Ÿ��š,¾‹4�s›"ñQØbà±#¨ˆØNÈ�˜ L‰ÃL¸t®Ý#£’±-óem|~Ù_çrÚ ›Ü €TKØþd±ÈI;ÈAÎAÎ/] Ý-�§f{(«|óâÛ1r+Fvů’:'Ö4e’²› ’Ó =¢xÀÏvÛž»­í’žlù0ÌuYqó¯Ó ê¶\¢2â‘ÞA0Å ®½±€±v¯·Ò4l—$"±gó,¢“³ëý�828´“ZÀ¶DÑæê0ct®äYPØe¥Ô�«i+j‡»M* 4…^ÏùäN†P§? <NY™Vû‡—ª¨!¿Â¿Á.þŠÒêF÷µÅ�£ëY6íL©3rXã®ØÀK: =\ž2½wÇ3ÅÿR×îX楂~É‚Nbúɫ˫'xV!Z«CN…–Eö·¥÷»�ĽvMmÐãþ ¾lV.+É[H9—¥ÈVÔãˆÚ¤ÄÐY4‰N-ó* ;QnÀm°ÕîÚ”P¬P$Wƒ+)ðæžT=¢èè((o· �o°V-Üm¶å°Wå-ŠéWÚ«£u¹êu:ðsâeŽ‘n©äW4U' ³J°ï»–{X°c �I†ÈÛˆ^Q)†Ùlæ~<: ¾<ó}FŒŽhÝrÈ(rí¸z†UT‚bdsI½øÅ±ˆoÊ õ~QÏH´±;Ý«w©ohR�vêR?sg7ÞŸÃJHGv¥úDØPWœX¾ eý�1|YL woŠÀЭ \EMÜ)C“�«(Ü: \ûóš -ˆ%aÏ{×ÃOå˜+qŽÔYR+ü´;HjkjɫݛàþÎÐÿp¤crKŠÀ«ø0…O{É:KȤ|„ì¡LL+m¹ Qãäg ƒÚRÌxHBÆÅOñRóHÉGÝt¢Ov¹“"bÊJ×ËÆV4ÅSäÃávz[ì*ÃX�®Ò£ÛÍTyBQ}$¢ïiÈSAÕ�¡|Wl5+3Êþ.¨-¢ÌÌR½áj‚h ÷æò»ø³“Ñ»o¼t÷•<*Ÿ‹îIÒ\Q·–¤PéˆGE"ˆ 5ç„ùJà¿”ï[§šã%E“ÛÚ®Ë S»”HBÈ 3ÎæS&øèÉÓáXNáuÂC9ãQ¼á›jöÖ€:ººTZ¶4¯·GzƒFRÛ ƒ7>9Á�ñ¨ã Û}„ɰªHÐÜ—% ÉmWãØÿrÎýÁ‘±Y•ãDL6òíÌìšó*„îiœŸçIkjÆÁŽ:_BÑ]gìꀸK¦ËšŠì3©œÅ�Zwâ“t¤\�vûQ™€Ú±$éªÞÜÉ5³ééh¤Í[” ¤~XQ09‰/#âιsMœ'Ò›ÝJɵPi›Ú•MzÆS0JÂ4cܤ÷|»8¿KX•Ž�K×ÕR‘#õòÚÓà>qÞŸmU­’E™ó�c�k©0EG…XY*QÆÄË®E�8Î3 ¨ÙkÑOŸž|öl½H…”Rk¾ôÀ:> YÅ+¨¨ù l‡#,o„“ùÜ›d·QöH\°| a$, »0Û(yZÁ1nÑ\$Ū¥^Bå ”„ð>qи"eÓg©iFyš[WýInÜpï$¼ x£’ÄG‡h ´�Ì7g£]ŽªÑJW’AçdeUä¦>.ËÉ%aí+µ4ðYÈN9ALæq (°CY�»c¼| ‰±`ЪwÍq{“)‚X3ÎÿêëY1Í °Ìî ß:€�ÍË,O5RÁ›ÿ|ºtlf€èÑÛI�É]“ zzJgäÜV¸ª×›bÚIƒ}†ßêJ¤D´A�}Û|3«;8� »àý&'X¢$Ðâ£=IØÖr‹ò– MZo¥jé|Áp‘6ÀF/Át_®!CÕ€,RðrVÒþÇ(OΓMi÷©¿[oì£h¤/@2«yÚM¶O­s±ŠïÕ@o/« ›nK9ð Ã#�7hÇ‹Vúõz¾¡º�Q0|Š… è$‘¶–ðÂÌ_ø[Š$M…F75 ?ØKmÑærÀ”šÑÁÀ,¹»_$ÔPÊÿînŸnöJ«èRÇ ¤@Ž cE]†cà¤�Ö&^w>�“¯„\Ù,ë¯Ì.�îò»ÀT'àƒ0ûÉÿ Ëw`<åvRw2–& Ǹ•¤w¹lO¡÷¡.Ôü%›ª�ô¼¤l`'–ØÝ�`jû–š½ä˜ ØÚÆõ |ñgûºL=b$e³wDQ6Í&èLå^´Z ¸"ìÄ| „û–ð„B�àBKÑS¦„�ø3úÏ÷K“|™Rõ€ëa,4ÌXB£„6•¿xs5IÈrT‰�äú€<†[uGþÈ” Ϧ´EÞÈõ@óG²&½õ’²¼ êÕ5§\,vSß© jÕù©SÑc‰!fø@L#nu“zÓ¨žýp‰´{‹Ýì¶êà$‘rXi¶u Ǭ,ÀuhjÛ5ñ6Þ´]è?psI-X e-g3øòžnlH[Ñ1‰{�Âm'3ȱEE—ä ’?ÑñË×=%/ýò ÿmä.NÑ»[g…�;*º†3ªèç%ø6UÂ¥ àÖ×¾»ÉY*Lüê -ÚŸ¢éhĸU¸²k••l¯ORéô‡Ñƒ¯¹¤°Y l¿PcR.VzáX³'ÈãuF¾Þ¾ö:ÌàBÁ| ®S8ŠÂt}-ÕP­~en/×&„ÚqÚVžLF¨"�’²%˜×÷Ú Û(a“Ò“Lb„¡]¥®�‘�d%¢Óò!V¼ Z™Ö6#»¨/;(µq…ºíñ‡f�¨™¶@ö©º¹(}‘`ö¿Ðˆ´]æâT=jŠU÷ç?愸QñË�M˜ÁnDMÕkº-+0ñ,¬57Q¾õ÷û²Þr]$¨Ò‹%QƒËZ²Èük ¶g¹›ô É�0øÃëë¾x…Ö#ú—Æ�•ïArü<‰g°þGm~‘c6øã<ÿ�Ç#† 8ü¾Çù9ý×£àZúuo4&€-q¯ú¿w_V÷ôjY‘úòÛíO�·?é!£1BêÞLƒ7„y|KfÏÏþ€æ²³½¿üã¿îýñŸ-·üOàÝÆEÿápî§ úMà<þÊÔ-ËBÙ9~ßß—4ÆV®£€í|˃ҀÌ~ƒ[þ'&ÏáÙà ž¿„DÎÚÌÐ3t³$¨‘Ó9®D[x¼[LHÃÞ–‚úvS¾ºËÏ8µwÐ ±e÷>DJÖΡ} ÒÓ+4ÒSÑvwt,jBWuéBtöé~¢ðÛ˜¾WJàó;‰ˆÛ Ù>zÿ!åc§ >X8Zwí. !y8ÊvQh_Säóxïþ…õf¦âN{ºš§}[J©‰;™ï?÷Óì;¥ùÞša«Eÿ‰.©¸àT2b�WanÐ6~¦E‹¶oÂ�H:·l�F—ƒHsÞzmX“PÆ8áùêö ìÖÞQÍ Z.÷žÆî[Eûènî.¶†¥hHP[ÖÃëà—l|VæI´†z¹Ž…O/„ù—i¶ôè çÍò:›÷Âøø^ò:k0lÞø—mÈ+ͰU^¿¦Ûá/¨pÈrÀ`¹�¼½·ÔîHðl¾ ˆ ç±Ð‰R�kûÈÝEr£Öæo•ÜYû¼ù|q ×é;}Ö2�ãx•+I˜%a�>LÚ7{ôðÒŽf¥×<›5w‹cü)ôŠ<ÈN&¸Y¸�ô¶¦§tÐnfU�ø´á{¤’Éí-ÝÚÓ+Ïù%™õ}’œ�i�:t?œZ+6ÑÝäæb5¾Nlþ¶Ä*h¯äÈW÷ž?†ÂãÆÙ*?ä'H„<BÁR¤O¶) Áû Òw$1QÜ5Þ ¡̵ÍçfîÖŸÔ³Gç>ÄOÿBüè~Ò¸QÛVû.{›0&êEúâ.qÃE(€òöF » ¯~¿½¢Gî•y¬$ñž,¾…i»¼Ë�þAÚ/TÚ£~q?8dyÿ7¾wØ¥ÍRÓÝ»H7ö®�{G;©«�ö�*]Ý”¸+š°�Šn¯)V:…»ËEOÁ®»¿…-§«àœKü‰^·^@·EMŒØwqIœ; ÇjâRºÀ�X&äóTt›ñ.ٜ֬vVVõŽS ¥?¼4jÓ±;rÉÚ)Ñ�š£ƒãIôbg¯ñ"ñû�F%1ò²f´Ô¡’S4ŽÂøS=‚{þ€25B^.‹´×Çd¶þ« ˆ2m]ÌÖ¬ëù–uÑpË=k Cí‡\ÓÑÉ1IÈÉ ]_Õ’7œõ½=²è×÷õLŃyzÌÒõæ<áóûÄñ³Ð-ó ;å5È ûééœÅnYÊ�’š}ôh¬ãáÑo"šY½Sn¸7|§oî#€óPu˜­"ÈIõBO‚òÙ!ªæ?FX=ÿgâñ³ß´ ra¢Où›»D=6@}ýŸ�Æ×¢ }½!ü¹ì›ûÁ÷¤I¥g»à+Ržº|*üöî¿[22zÂAPä[‰ {·êDî(­–Ü "Ñz¹¿ŠDwÊ´ÞeG·HYc£e{£�…Ú³?H8ăBDDvyKH¤×€MS5p®Þó»PõaH¡ `/Šl’ápbjo¥¢ûÏNžŽZdlA‘³ôYOüü.¶óÇжñËh9w]$¿Ü`5ÄíaL¦§‰â¾ík‡Tj2üèxDìøéÝñr�{Üå\·Ü Ì!7ß ð)Õ“ Ê­�Rþ‘ÝD�xÈ¿<Àß +Û1²ÑîVv“l”ÈŽdˆD<¼•mÎÿ 6‡Ü)£O “ÑùàH7ß´‹]S¡ë±­LK3ÿµŽ„ Û~wÛzò—»ØUü?2ø}ÆÕ}yŸ0©‘>FÚ)½¯¨æ»8ܽQ“>wÔì` M‡šÈ“óA:àmŸk_­_ƒæÎ`t�ž�h]Ëg;¿ß8�ÃNt޳q|g«œáo_ ÅCÑ-=rÇ ]îÛØ=‡ŠÊgOƒŽÑ³Wîâ^êTo^Ùþx…eñÖ–¥ú=ŸºïUùîNª«†¸tü¹1‡öM^^5Zÿvžôœ¯F çóƒôÍ/]IØAßÚ7p÷¤b›4,jíÐ:v5 /Íp¿o@§´HWøî+“ðÁ"ý|§,orNˆÏSRìF©A:¶ÖÒ<ѺžHn¥äKâ ”Ÿ”»d¤¶%ïÁ$ÉÑgý!œn–Ø-´Þu@ÆvAÜ"ÔÈAÔkŒ\´+¿ä@îºÏJÍ)ßvœm\ª%liüÚ­^} ´D!1á�¸¦†J 7ú3�Nºø‚wboC‰Ü…z&—€Óè(¡EæšoÕg*èÞ¦iðû¢¯—x oÿ@é¬è9,úb P"Ù™ØKxüø6©�Õ£{}µ<¬§hÝ=G®èÒÝwê4¸†&³©õ7ºÒ)õ–Þ—é ÿÄ3]çÑÿe<–  ö öŒ%˜àxœ½XïoÛ6ý®¿‚ˇ¡ìx ÚbhÙ�nw@³CQ´t²¸H¤FRq�¿~ïHI–;N‹b@QGâñøî½ãÝÙÓï~yûóÕßü* _•³dÚ}�ÌðQ‘—"-¤uäÏOŸ�<™à½W¾¤Y&=yU‘x×T•´kñZçÆVÒ+£§“h“L'­¯…ÉÖìù ï“~]“{!îwÛd*…–�Ÿt¦§NÝÒÉl:‘ìíl¶õ{Îðºi)�;?)ž‰ÜÁ85Ùï 1½‘væn§þçbº˜ÅÝ‹™x¯âÚãé$l�NꇺÍTõé!®G½ùÿvÔ“ídzîñû‚ÊRÕ/c<ú»P>o֬ܖûÔ”M¥Ýî›°?éüÇ]ôÍÄ;˜“ ¹Ii­\Ÿ&Ñ$S7ìÊ�Ÿ�Ä]ò®ê|Ü~¹…%ßXíp â7”zcÅJùBø‚ÂÆD<ÒMµ Ëh¨¤Š´w�ÃL �øH;d±@>‡MÛxÅðØü6Òuï¢=”A=ú+P%á!5Ö’«�Δ^0ºšR•+Ê„Ò;NO…x�ï¼ Ì$--#ö¬÷$$uÔ�–7Y”¤— Rê,2Ê|%…;½�� º1P]6¥WuI›—NTr�Y:#4Ø&�pTK Àþ² Z Hÿ‹aàœ¦ñuã{ÃQ«ILÎÃéHì'ÁQ7de9Ò?àzÎÄD‘o6›3€öÀÓD\¸kŽžS'§çØ6&–VÆ�1› „8à!7eæ¢äÞJUn©íp(xåE¾ÐÂh PÃüAÓ�m¾%®Â�âl¼X�ã-ŠŽ–³ îÓ— ¾�£2–D-VP§:¦ŒsGè9\ﳑ(ÌŠ™Œs^º0¥‚„• ¦.¯Þ¶�d]h”=¶É"æ¨Ø 4ncØn3ì g¥‹uz†G­z^£ß/Ëê¥÷·¨÷çÍâñv6[ìd=»_¦"Iäì²Êgzo“1 B%œZkÉŽ€‹Ì&»ÛÑ—†ÁÛkÌÈzŸqÐ é¤ ZÄkðª,Ï“˜ ,1d¥Â_‰�ó2�\1ŒÖE¸(‡ Sëœôj³\™Qöð"öî[m?yÇ„Y'„š‰HŽ%Döä+ÿ•öm¹ô]ÔÙm6â2EïK‘^%b£Åe~\³3)’˜áÏ*DÿœOe•úÒ´J±Èrx1`ižé©¢ÀmWÝÝ-¯²¹Sì»lÚ+(ðòbRbPKöPtŸ�1Áæ!`ƒ>ñ8–ÖŽ˜ù£Œ�.¦Ç(OgG2‘za¬Å(ˆÒ~›(DCæ æñÆ�…Ÿˆweº(¬Åp1{G $ý¬8TKZ‰ùì,x³kô#Ѻ­. Õñ¨î0˜—úgeL/bØ)õ*ìýãÎ;*bˆpX2d`°©{Ô rN|N®ie·'€6úê›(øz§wÔéõTGQ^fš<™F£~H�;3'l:ƒ³vlºõÈ6Zˆ™ˆÈ+«5çõÈ¿ %/mçn¦^ðú  ð#K'`´?…ú?) ¨áS;ÜÀ°iãj¯CÚåYžòdAZäV¨‘cc¢Ö�+” p<ˆKŒ) 3«|©¨15¡SBýò,`wÞýæ øÂý¥¾|´Í!#¿¡A"Û{D˜À®ÀÄ¥]îŸY}\`8¦~#=˜VQ&)£”P°X+×96ÿATô¤¨äϘ�ÔÅq¦¬0.ãÖÇc:¶#ÑfgÀU5³}�RïðxU·ÔèÞ©a[Ó_£�º÷ʈÀÕhOO^C2µÓy–äÙ$t%Ú R’Ê<ìç¤:¿ê¯2¿@HTbk:›è)B-‹bE™™lJž†Fß‘™SaÐXOkSgr2¥)îg2mxU•€é¸Ý(˜á‘Æo®[ºë.«�¸¸´Kk›‚“éx“Ê&ßÐSÊB™ÏæÛŽÙém€5ÉžI�—“æŸÍh’i!¶÷Œ@y2†âÞ¿ó‘ È1¤þ6ñX âŒi‚ çö J¼ú–H§ö›õ*‡úüKM/Z*Á°øWLš”ºÞÀÉçF¶âU—4ÚöVäX©KC[ :€¾N(ûÀ«þ7KåWU[¸»R+l½½Õ %p9^nÚ®`K®ÝÍúôX1uKÜwÝLuªÅfLR«¯Ø}ÍäsÏ®us�RËw«Úöæ&+¬7Ï%Öj·!@= Yo)Ë´@ËèÁí4ÚØ3€¼“×Íó‰$Ÿ·qÈôl…öýð3@º¨æˆ–>Õ!žŒ)ƒ¶$¦z¿œa¬ôŽN°Fî½iå8V-,o¿to?|º~¸»Á‰EoVz{ìŸøª §!Q £ÃLèF‡÷7Ã6ú€ö¥¯õ ×AÞ_ß²áA8ÏòU#ýˆÏ6ØQ×äê¤7ŸáÒv1°Vȱž6�¹ŽÝP×ìŠÙƒÍ Ýt€@>1iÜTØ—0�Ëæ´`åeÛòYæn˜‡Â.õØCs-ÇýàÕV%Ý­^L„¶“Õ ¡ÖITÁ žMð©èÖ*µçE{é± ØÙk¡ `bfK”|F÷­á$\×#œ©ˆ'¼ðöŸòâ�* °^ö·(ò wójånqu¸ÔѲ†hõŸEaª ŸFDÓ�"ž+t«@E&Ø ÍtgÉ;i©´ÁÒ#a«Q8B÷A™ÑGÏ=€¦óm¸”M å Ž¸Îë{¶.¸ ûýóãÓóããÃÓþÉ÷êÜ9YU‹×š Œý&"Ž×ˆÌ�Äô1«kÔÞÀG…mzDW³7Ï6‡þ(ã è—‚jÿ־߷%�úÝ¿¥{NKÚ=[JÅwà¹òB»�¯ªe¦]�Ý�9ø%Û_¥,pžFfR‡³†Bñolí µíµ¶¢œxX•zjÝpÍÞC[ø÷ÀS<¼Öl‡vŽ&)â4îFÙ;‚táÄNï`‹É¨êÔ´ 7†—•|]C#¥zweGÑWîuá 3ØÜÂÁ pàfüUe·ªJ`¯ý”7CÇЗl)wÚöÌë°ìòlru-¥ù›Ó00íüÃË®}æ>ÏmçLÙ^D®D‚óûd±Ç5ÄÛC䬆K•RÐD°3~ ‚˜QÀášg´Ñ%¨*H4cü¹›OÔêm£³:ŸúÏ ÅÕ¨ÔP‰qUÚ¿(°qض iÌâFÀ™ÀŒ�•S¾°Ñ¢à4‡šz´hÿó•Úgì\ÆÄ.bÞñ© ­Ó�,Ï|«{¢º ÇfÉéÉÑ›#¯“²öOêqqÈ'FV ¤ð&GÍ­Îo¡º¢7 —ƱGÈ&P¿‚„ç³³Àüž%fãùxŒ÷5�ѨsĶ—hÖs!2ê;Q+Ê™©ªËìj‚³ :rý³sKõ༞|½P{7\ 0*0fLGç)”¹˜•�Ž@šÞ•U· %üpóÇ—ÀÌ.±�lçC³¤ÄOÐ9è·»€®l�2áÌÎ�Ü )Ð,ŠºÕHaÊ“±íÑ‘(õ4Ê›%<›¢:DÓ¤3‘8Üqk*‹0+óHe’Hg�°U<µ øå˜Mç"y4ãZÔíÁIPÙ›KyXÙ6èºöäºæïòþËæz^ ›m'·^GÂxœå\moܸþÎ_ÁúCrp@¿Î¥Ah}mc }Á}àJ\/kIÜŠ\;›_ßI½¬´’w×— ÜÁ¶D‡Ãgž©\þîãÏ?Þýã/àK[ä×ì2ü�"ƒ…´‚§KQi¯NÖvqöû“<·Êæò:VZUHþ×µ¬6ü畬„Uº4—3jÀ.g^Ð\g{Ž�„ݬ¤yÏwô‡†ìRðRòê$´{«LfìÉõåL ¬w×íÐë<_ñ4Æ\�,à ÿ�ÁŸØ+ÕY­ó{~ù(ªë»O—3üɯøåüÚ‹™_ó7ô–^ž^Î\ßËÙ å_ó�ÒʪP¥äOK•.ë‰<Š|-aL‘ç\•ð|“«û¥åF<ªòž;]‰ÉÔcP´0g?œ�dR²«šÓ«_'^I»®JÃÏõ½JEÎEU‰MgzzÁíR2P´4ê›äÂàÐrµ¶õš}©ks”•äRÀDe. YZPˆyUmµ–Aåá©®*iVºÌpÎQ4Åð'e—Û–aÎ4h2e ýþM—2á¢Ìü@ÐÝÄ‘4ŒS=)#ßrþÓ¢¥/_ÂÌJÍšRtÅðÿ­¶g7gwøü ½w^ä©_»d{ ,N[/:fU¦G»°Â3Xâ~Wò~�‹ªÉþe/šPlmG#H>ªì„Ú¯KeÇv˵FõB3/ò—^-^-ež«ÕÅN?•ð’ÚWej°„^X¾áf%R™Mvцm¢J#n:ä5dÆûê–¢-� ôÈÿ¬Á¥‚Òè<ØÜù sšn€*@[,‹•íú<<Ï?(­P%ú±`Sü'A{–Ú›ÂP·R9Ó+䍿(à¢KÜ£%u)ô#¬¿àÆÊ᛬4ϤI+5wMÜLA—„ í¼]ô‚Áh+ æB#+Јm5¶–ö¹ë8nn¥�á�^|GÔt\Ç‘G@QnuWDâ£GŒ;—^Ÿ×¸†¯ƒð7™\ˆunO“Ðf#!"‡Fñ)À¡¼o¿(`—õãŽ×OR>4Z#Æè°s|+øçy€Èe™�ep I�‚áÀ×JnÖi*�Q�²v¦ Â|ãðíBÑÓRç’—ëb.+¾°Ì@ÖyCšå<ËÕC¿•B‘¦z]úÕµ6¸nòŒƒê÷`Pv…åŒQj ¥K@ÜNG—§€R€">(9€·ä�‘?ÕæoMNYï9Ä“kJ‘üÛ 7UpÍÖÚtú„wÛ}qaq¥:ëˆnײ3í:ÞÝŠÛN´Ã}†yGM0ÝüÌ0Ë0à¤2KÐÐ…J+m$0Kƒ@ǰüAB_$¦Ú}ˆf ‚&<áÃ’Þ¤¢DÏ1¦� [A² ˜†þ"r™ëT@²€D%<íêúUH`-¹wÀ=஡A�«ª&¼?ßÜýéæÍÛÔŒTÀŸŒ/ á 0 @$«Ä“!Ír SèÚ¥[ã9:/,ká_##ޢޓkX)îeî— ûΦìæ ï™;s| 3�2ðgÀ§ØGOÈ|§ä³Þ } Á8ÆæÒ‚µË~Œù—ÏÉS»ØŠ""¶þÞ¢Û\?ɪ€W«ðd¿ôòøzø'ªôƒÍï`o»§ƒäJ†ý�\¸Ÿ—ø)íoÖÚ“Ø.WJúýh^i‘¥°È�¸§sŽe×�”rÛeÈ·P@wËåg€6‚`à'ç²£¶u'H~Ô™#Ÿ»0.Ôלm·ÇÍy!þäZ²¶ó<ܙr^¢-ÿS¹Â˺˜·]ÑÆ–*ýÀR(ú¨rr¦C š¿ ¨@ðñ¿ŽzyDE{Œbjt©©„Ú¿Úôîõš é%ÁXýÚoÉ}Íø»×mê;Ó>»ñz+Sß�À£8¶øQ$]€ bF…é”ùÕ?3(é:ÇóJ‡A<ÕÿØÊ<¾Ð_ÃeDÞìT_®ü2µ¬X(ƒñx©ôò��NñœOßžëCcÄ«À€Îêþ‹Ê Bô‚öçÃnú×¥9Ÿí òZ² |æ/ábÂ4À_jû2΄ã•0ÙgX ¼¥z O(¤È¶?“kióXœ±¿QÎ*;ò“F¢Ê_ÂQuþ‚l9^&ª,.’énƒÞ—ôM˺Ä�RX<ãÁï̬hž’vFvÐSÇêmÌ�-Ò¥Ox‡KRK$ñë-£³GÌNÌJ1ð¡5¼8F(qb¦§Û—��–<€&¿Åäaw-uÊÃmÐÀ‘Ô#~3f›©· °–°¬TÊ|2ÛpþîÇØƒ›Å°)v8è˜;lÕÆ¿£ÖOßPë§c€Ô‰9F²‚:üÄþÿ©N=�i(Ý4zôœy’Nlrâò–ó¤u–y¼3O2ÝÀ­9÷î¨ ’Ž,’µçu-Ê÷ÇUî>ç&ùS�åµ!#¸KÖ5cÎÿ�\p¨~Þ8IG á»5ôî¨ ’Ž�šC¨È{ó‹¡f2yE¦m»F8(\â¢ÏíÃ-owÓÊsþ&Ôgd &ìòäúüjj.€hdqí>¤ï6¹õMÎ]“SPêÃÆ}Ù�ú* ‰é¼ñ/t8óÿ–ÎŒþ©žÿú€:ÒÜœ. b%a‡,Ó°~?vÐÇ'†3h<)ìág7ì£-ä)€pô8àá8ÛzðOyÐ}däÑóðF;ôî òø‚<žyxÿGD 3ûœ�®X y˜•<y˜�ã(üX¬‘utq*5CëSÎèÔ´�º.vÙ¥edSǽ 0Ü�³jÃôÞ1À’J+iÇÆpK/ìuˆ¿¬¡bO xUD1=~ò‘„ÅŽÂRP)›b ß P$:k|¥á×H»ãbhc�¡˜v…µ((B³§¸S„£È_u»›,›@{ÒKÚ‡¥j"©]ˆ˜^É<|¹€ˆ)Aà F! �Y»0ëÙE©çK±â€4Ï`Ô.¦ÙtŸ{iÞlk�Œ‚H­ �Áø'Õœ¿=¿BÓH`’ ~š¸ êŒÌ@÷¶ÚÜÔÛF &þjšÑæ³*o¬ûà�Úãã8+zÖM�Þéç«:éãУbÝáUB1�Åe?,³` gø‚' ’ÿ� …Vß‚Æ�‘ŨhWìv! ðè ŽÛŸ±@ŽÏÌͧèé‘í —÷DÄN_gOî)Xøøa~˜®Is˜÷„†á]´Å#…~.þMÔðiâÿsþýtðOf]þŽ# ¯¥p“"d®nj^A �Íéö%íþûÜƒÑ Ø½JØ \×êV”.Á øúe÷nÁ‰h¸ÂÇÀ·‡(û@3˜f‡ý¡ø&ìÖJ´¥í%íW)³`^…­¾Ô5%ŒÃo ÛYUK`µAÄó6ëÒ  ¿5ˆ°_¶\յЕ2KáÀS²÷�å÷é†ÖíÛ[m²AnÅV?J0ÓE܉zkYeá¤Ó8åñð$KŽLÁŸ‚#¥mr0K«ì`ºÁí4ì¸)—…9,Š}”É*Ìè ¨Ì,ަEÓòC5Ú\ËRPU!séx %u‰7àïÜþœp>¿ÈfQ¹¶Å÷ú“à´š PÂâE Ô¡ÛI¢9kÏg v|@Å�ábŒ;ϤßÙ|v<¤ñ�^…n?pï�ÊB¾Põ™Å})…ñ¿~S\d/Ï¢ùOˆFêCûàº÷Qéæ(�n÷ ‹@h'"ñïߎ� JòlŸ–¼\üæô³±v\ÎJÅÞOd\3 °÷²”›vó(ìÕÏèÍȺè LÝ«ÈYÆ&Ë^“9–aŠ…u9–€Eª^�j_ÈÓ¦—ƒ—”yÁöqßj#À¯�„X¶~2u'º(�JubÚJQ,õà­tÔšwÚ ý�°‘l¼åeƒ¢\¸ëxN¥ ¬|™ƒ.‚Üϲµ*–.¹m§s~/u'KæÀus¯¢¥[“3÷qek<e ´ØTÍÖámˆ‹-)áRÒ¼Z©×õTeU«�ÑÌ`~7³&0Ç¿Ø6µà�µ‡¡Æ¼tnµ FJÀAyS*€ÆƒO)ïUˆ.pË2/Ú¥ðq2ÐS¬1!|�Ý-$ö2ªlàá�%K3?mOÐ9•¥\­D�©»ØD¿²×hÀo+ëZ [Ñêî>A.¥† [¬^óf�ŽÍöéš!d�ç«ä­èÛ~ò\f:€R¾vÖiР¡ýé`áÙ0’Œë¾Ó&ÿå¸v°9ÿ=µxŒ‘´ÅÄÈX ü©Ÿ è¤Fe™5¦Mv›}úžMò/Bgk\8¤gh²¶&ÅÌm=±(œÓŰۻÆÜíãÙÈîÐìfž{Ùà3 Ó»={å ÂyæU�à}#SN¤ËW¥Ž4æÚó&äp$Ì•Ä;�²¹€'­Å3ð¹9{Ð?ª‡ «A�r“ý¿ïQMˆ‡¤&ì?#qª’ggèÁÁ@Ã~}Z�1‹ÈI†GùS v’À‚Þ;¢0à MÀžF›ÂWkõݯ+‹ÏÂÌFú/xbo<ñ=õäMƒ‰ø^)¼ ëCècA ³€;)ìÔÎÙ(™bÇD Ø£PVÄ—]s\°²ø$ Âãƒ~4›ãüH™mîsfh5ƒ4ÆL°#ê3öOõµhЉ$/D1�ŽüÝ÷§�ý3Éœ$ú´Ÿ4þÿ6Á@2â»PaÒðÇ û–úÑ´)è› ±±,º­ñKáDa<ÚÇËGÿ�j¡]ž—= f­þÄ@´†;á;/fhõgšVp•&6afA1Ê­Ó è¤õBŠßRüO_zÆÂ[1:1�$˜“#Xx £ó¿öÊG€ªAðä1@€>?9˜AåT0 #ý)dRü$A�xÈ‹VË;q®ê£Â 4Tй§8þ¨:ÙuÛ8°€a# l€ ,Ù`½?H`JȃXG ̇ ÇH°>;pPa4Ú³£…ûÚúYG{g=é"}‚ËÎPÈJ¦Ã;Iç3Œî)çOçKÆ`o˜ õ†ÜT²/Á>Û*\ýà‡Ò~„££…ý1×±1z3älü¦p—Ê9Gþƒ, ê³c·'G}¿K²BÝà �Á�Àݨ;/ö®š»‘ w&)®Å›Ê`;#õôÆ´kwÃ3/RìH)L�6;¥° , šš½”þÃzsq›“Nà xKºÜÈ‚×t\Q {Œ‡_ìÓu€ñÆĤ^Cʨ“ô½“0V§+•ä{ªît,7§3ñxá"•âÝí%c‘Ù•»A]¹¶¯Ö‡¿uìø¶º¯'4ߊԶ1TÑŸ§äÈBZk{{eÈ»©ÄF9l§Ý×^ÞcäÐQg2³}†×»l†ˆõ’Üå4xf'¥@HCÿ;tïV;{ƒDŒ®¶û�`�™:NÀr­(6Ô¶Ô2c²a½%¤1" ‹VÓ‚`“’âëÉFÆêëÑ6Æêë}ÐÂ?ßAÜ ÐÅð_�ûgð·_ߌ&ÈFÈx#šµZ>fÇÿ‚ö'ñÐÔ<}+ù›ìC)üögÓ8S©‚Ûnì;UÜÑ¡àóç–í–ÆãŠ1þ“v; §aÝE _›³Õ®¡<à­c œ^ÀUï”Ìê¿·xÅt¥$¯€EÜü†¢¡à¶³(ìéO¤tªÔk»üóŸ?üòî×3 ‡‡<ºþf0û vx~ö=Z¥EÏ;K2o¶—ý�ÕñçhÛwçÀÿÔšûg="––Nð�\Ó<…ja|K„q0áÎ¥§+q*Â’|å¹ä!!{›BêÔ®è}ôÀŒ’ùª�¥àuçÕ¯·ýfµÛúÆÖ·œÛCÝ`'Öá¼®[Y4çÝ·3j3øs-^d²Q9�Á»#ù$÷¶ªÜQ,ßb ã& bx±O˜;~*û7_Ÿ@€Œ&IÀËÑIµ³Q­‡v,°‡…¹;mˆ »+«½t�½ä!à –£Œ‰WGDäÂþƒåæßGÿ‚§Óê  RˆO)‘"!xœ½XQoÛ6~ׯ¸ùau;Z Å@Ö®oÉ$/Û°Z<[D)R)þ÷;Š¢-Ùrl¥ê¶HÞ÷ÝñxON~úüǧç¿þü2›ËE”„/dœ¾r´ ÒŒ•íݤ²«ù¯“˜Æ­°œY´"GøŒ&-EaÅáÉ2+Œ©Ib¿,Jân©ùÖ�ß8Sf·š�p…–G År¼›„Õ×925Y$1s€ï�q²yOä’s7É>ÀÊÐâTó½×!Ù°rñ�Äî î Y.¼ñrÓzîÞÏ]%qm™Äň¨3¿’‹ü³Ô–ï†×˜è3§G;Œñç ¥Åm“bj%Ùº7®<�è°}êYY²íuä—p± .åfþaâ­¼;N8ús%ÚªTl†~1ºß]ÎÚÆÀ yeR&Y¹[8ƒTçEe‘»iÁ6X²5D¶4ZÒ|BË”%©Õºž\‰ÒXPZÍ�Pk‰V+ ”£2B«k€ûƒSºŒzRFÞ®+²²`Pbj½#; ©¹«Aä˜%ÇòsF•h*i!¥`z#þE—9³;tÅ›ñg‚ü[+ 3c{‡BŽ(!¿m�ãŠ9x�ÚÎïçÎÈ¿ÚÑœ}mS]’…Vœ¶ q)ùÈžª[((ç!v� «˜ÝÅ?Åo•Ø0IÈrÛ^” ãvvw¶#·b­tIy÷s@vším°*•Ç6™}E,üÆúÀB´1É~ ââ” ¹™ïÐ!o>¾õ㎯E'y~ í9ê‘3M‘j°K4)°¶U)òÄ=ªÔ#G$%mý鑟èuý�éŠJ‚¼À *PU¾ÄÒyÔ”pGí¢àª}ÑTXœË=áU¿�½¦cÑI!óÒ5@¹¢ ¥ àÁëÁ¼ÖÈÈV:y ¸4à =þV«ž­ZÔ[ý4þöÚ¯�G¯ü^ÔÑëþËHUÿO›²qþKçé“ú÷П ÇÕ¯©>V%]/îÒò'c$) ±K„ íÝm:M+:ójí]ê@.ë/ná%£Â6èJXFÍ=HÑ-ý­¢ll÷‘;0“Óž0�‹ÈÇìQë¸Q¨ý‰š+›®þ“Å�j\¼Á £äSyx†( F·s…íL·À �ÃdsÆœëônEˆmÉo8)oÚÜn‹$½ � )´£ç4”Zÿ°c>'ZÆò>Í¢á!’õÔ.ûÚvÅ:Ú¤æå£A!ß@9H.ÑèŒiòS'’‡Ž ‘‰0½î+ÎJN�÷FЛ¿þ¾vl:Q_ Á¦—¾.èãæê ­ðÝY³)UY[‡öÁkÙ Šuf;)îv?ÊiU¸gžz¬_�Yû…dFrºœ`I‚�ùn˜Í&‹ÇùMc¾¸ ê|‚}Õª¶¹š5Š4^÷Önë@¢Ož¬:·g5Ù¿ü·m£nC} —qó'Uìÿ û¸MHþ§#(ÎJ}7xœí]_�Ü6’ç§Ðùaãz<— ì±'ÙàŒ¬ãìfv/‹E8jö41jQ¥™éýôWU$%’¢Ôêžî±à¸%±ŠÅúócU‘¾ü¯ï?|wõ¯Ÿÿ’­›Mñ†]º?_ÂÑð,_óZ‹æõ³¶Y�ÿϳ ø½‘M!Þ,y#¹Ù/ªnÙ²hD½Èx¹Ì~Mö¡5o¤*õå…ù€]^Ø�¯Õr‹d¾ÆAx³­„~™í1|È.yVò�xýÌ}÷bÃnŸ½¹¼à8ô7o‚ßá›oàç*Ë ®õëgë?g+ /çjÙÏäevyÇë7ß^^àÙëìòú�ùøúMöœž½5ÏæÍÍßÎ./hœË‹êd4ìß–r³›&üwm†´ÿí·CÊr)Ì/¿Móó§µ( Y½Šh¿�µ‚EjÖ"+€’n2Qˆ�(�É2ãýÂòºæÛÌ|¶”wŽé�>ÿó33’a8ò3GþY-š¶.5±â¿`ùb=cjE/…¬e> ä})t.Ê¥,o2U/Eý"ËÞ­‚·X&5LñNä�e…1Ëp�½ÇÙ½lÖ!_ŽŸÑ�7¼©åƒ˜fiVÇLX‘Qx³PÀ¤àù:ËUÑnJœa0°#²¶æ:ä¦V÷ÈÉÀê¦- Ê&J &Ç # ½òÀb&P®+Yƒ”ª<× ¼B4 „éFqëÀk‘Õ¼¼ÀÜõ–¾•¥nxÙàÿoÉÄ™VÀ¾]#"ÏhÕþ£J�r2#À,ã׿ßóðµ5 ŸÃð(Þó¢ÅSù­ÓÄCÕnÌ$aýì3]‰\®$ñ�àsä/̦i^ƒ–¢HKq~ðÕˆ˜�ÎyÁ}Ýü±×=xpcÀHºe»¹†@;:[è—Í0ŽêÞ±kU…flfÃÌ¢‘©‰%’-W²”MoMþ²f d`;²äõ6»ãE+´7Ë÷R£‚ô¼<7+ñ¿²Â<£±JÕØñ€Þ*þÊ« ¬i¬ T˜ùYª¾‚Ÿ%̸–75™,ÙĘY.Œ¢©²ØÒ€’‘ßwfÝîC.ÄRG"E…ßÄS"ñ²1ùÂ|þQòVXÔüìÔÆF6[*fŸ|eå÷sÁs¢öU¯‚ -<ÍÝнP=æPŒÅ=˜6Ë*e™aäðM4W›Š×È4п· Â[!ª¤X&5#à¤Ì,Ž4À#øçÞQ—iërU6TÐj1<’¹è|¼ÓÔÀù‡®v, {{™*²)xœíZÿoܶÿ]çµ��íÚmÑ5q ¤Mºbh� VtEQð$Þ‰�NTHê.—¿~Ÿ÷HJÔÝÙq¬†bK"ù¾~Þç=úúOÏ^|óê§<µ_57ÅuúOÉ ÿ­”—¢¬¥uÊ?9êýâì¯GxîµoÔM%½òz¥Ä׺mu»¼¾ϋ닸~nª-ívIßJ¿í”{$ö—á}q-E+WêÉQz}^iWZüøNÝ\_HÚçêæÀ[¬¿ÂËN”�tîÉQý…X8,)M5ÊøH\¯¥½™ëöú‚~OÄõü&ßd~#Nø›Wá‹YX¡ª¥ráÉéõïy}ÑýçλýϪzûžÖ·Úðq?ÝwØ'µjÝ=Ž­eÓ„ϾyØQŸX÷¦7�K,^«KÙÄ'ÿ…#£â”@ÐÿÎð«ßæç©kã¶ÏÃo¿Ü/ÞΉ7âoÖôÝ�íNèÖ�½Ýy¾©ô:ɵrg_…eA¦½�}hš |ÞÛÖÍÄÂX¡dYB5j¥Z/ÌBL7ñµ‚€•zK¯èœ�~œZ)í¾V¥7¶Ø;K¤¥i½„®BûsAHç„´JÔ²YÓ©vVÄmÎM|òÛéÔæ'¿ýåò4)4êm©:ÏB‘ŦÖe-´ƒ�S•�¾sãk¡ÚÊáÜçA]'Lï�®¯dYgB¶UáV¾JJEûàõR¯U1|p?H®û`?íÀ›è"#ÃR×I.,1�™èYBGÓBmÓ(�ð‡šÒ xú—¯!ŒÃ[eÕZYvD�ÏZ1ǦUb£} SˆFÉ5m.ÙÍ} )ª�VmMƒr¤„VÅÎW¦R³ íî+xT ¡]Œ¾ÞJ¯M›¶æÈ㓞M¿€ƒ½la0d]0تo¼îÈÄ��(…¾þhÅ÷�}ª9öª`÷D­wÖÁAäú–2“÷8v ITúÄÇ+Ýö^ �“ŽáX»÷q%·{Ï6J½ÞßùZïoú¦—Ö«ý}·J8L•²$Ë\t\"§z;ˆ‚Ì|U+GIÜr@[%›1žç¦§ÿ5‘%Y›ÃÖyÈFF¬¼¤[øüÁé«KñwÙâñê]Ч�ÕÍŒöÍ–RèR¼ Í• Ð ïÒŠŽ#õCQ¤Cdq`©öpDýt0žP»�ùnù‡'ÓˆÖ!þ<ˆhéŒñ¬ád·oÞ#ÛqVs�wå�”}•¾‘ÖÊ-% #Ä ¯U(œB.µ‚D6‚ðÂ4�Ù!‡L¨TG•Œ‚®6ÁèN�UPÜQ…jg¢5>½o ›€ !ÓžF;3úœìãB»4ÔäØSÊy?²ÆÍçjÉU%æÚy‹£oP„�Ç+]µzYÃ,ññLÀ šò†Ûžws,¥™ãÈ5\qZ‚º S Wàv •·²ušÄ ݯ>#A¯¾„z.(2€ „� Ñ*ì6çWhWŸ³W’d*œ±Ðoa(*½”2ª‘�#Üf©7˜«*9vYúúÑ=['N.O3p ççg‘¥Ä\}^i¾@Cmc$R(�4E@ˆŒhùC #e&k,LaÌ¢˜˜#€©ð#äÒ*ÅlORU'CW!nÙüF„Ç\‘Ù ¤¯Cå{Žñ: bn¡¤‘s:¢`‰ŸP8·‘™Y(å®éØ%Œ›¼I„÷m‚¥�lš@` ÊË~~{½: œ‰'Æhâ@ Èâj½`0¦l�±Þ·ýjN¤cè£@�>Áá®~œ ëi�(U�ß[ýú�Ù (°2›´&Ð+&¯Ñ90è’ø-Nk–`¶P<‹Õ\Z]1ñ ÏÔ†Ì}rR¶ƒ W0‰'sÅD†�‘2l/„XöáRy……„7ˆŒã@�‰ð/%l¥ !Π}Ûè×JüðôÕ÷O¿Î’7�lfØ.ìÈÜ›Bv7–» íY䨅JÏÏ)eØ›9°Ç­Ÿ©µæT kVñàG!´ˆë²B�gèÌÆN‡Hó-å®EšÑ^Y±c šÐSvå{[BúÀ¨§ æ§èëðz À1þd†b»aqÀ1ÉÉ)Y8ôS<<8ô&17,ïzR{ªhjÎbø¡†°,lXú5ƒOÓn¤­ ?’iÃ>Cþ$´J>¢óG Æ ¦£‡PK°¹è?�«:`ø\!^WJºžŒœÒ…£ö[†( ÀäºjÞૉE›â z €} Pyá”§ñ\»Lœž0×â{¿�dZQ¥ŒÜ3•¾§ö~ ùdççö®Ès¼p+ɦž]\~õÙg§Ü ™ÒY‘g�cƒ•�ž¡Î_gA ’œÔÈf¬U!l ›§Žš Î�.<޹ZԵ꼈f¢¹ˆtŒž•^,àà;ÌBŽØ¨¸¡.|­)ÙErާÒTÊ‚R[ëÐ4#Ò‡Žº4ëHÓB7J‚2Ë„š+ª^Q®+A]v0þ‚œÔ�VO¦†BÉ€% ÂÈ85!yIŸ¥ˆÏøê±ì½Ùë=\i¼ß{ºØoŸ‚á2Àt¾³{c³=B0ZåL³&Q*�¼í]"⺯uû#±ˆIë#ó �bûŽŽÈDüR”šä=ÂFÊwìÔ¬ú’¡áA¤¾˜ûBf§Ì¤E£½§rg�/´ùމK[p C̲©¦…m\p÷åW_~@t¹'® Œ›°\ž©¾Šm:)U™žâ%š…?tüèJ�}I9˜ä{Le˜å6 @&Q�OÏ?·"Û¡bæFq9/8Ðj#j°JœV¶&Éšrœò¢)†âU‡Y![€âCDU…¸L%` s®ÊF°ÓmHkBÎÑÁUdÒ•öé8«:¨ ·…b59q± ™ï~¤z�� XŒUç·ãŠiân!R�)ë¡'ל}z‰y÷ü¡uœJ›Û("Æmæp Äé©|­Ú‚ËÞª¡Äp9dLâ:‚�§ö¥{Ç(hÐ(ÇHgVÍö.(ä’á.*½>tBÀÏÑë݆ŒoÏmÈm>vηØ�àÀHûÁ{§Ù5aövÚÇÝêÜ{ÚÎ@ãVRLeGÿ“rôÏùÙûzür¿8s!p癳�» ß'Ã�øŽØÌÒÊÃ|XÂL`¸5xð]Áè“ñôÃAØA¬äÃÝÄô>ÉMDXŒb ‚ VÀÈ|œ2¶XirÎ¥Œú^æü{ø.$©I— ?8ytš•å»§ó„š]4}u×àúýö¸;ñöÆcÞìÐ5 ¹ô‹W±^óm±K!vÑ7/ð²#z¸¬ïi¹Ï:§?~pN"ΆÊ"¹P¼(=QFõÖ£ÿçO‡ªÂ4~º¦:| ‡Ê4�b9Ê‘]›ñƒÇ‰ûÈ’.u"ÑçjJ³ vlÚ?»7½¶Êeêÿ‚oŠÇ#ëBø¥Éìˆ l—aj6’°X’U±d—»]<š?Ómœ´:áôª#’Û†¦f#ƒ]6d<ä!{G}v7ÏH0±ö4·ÉØ÷Pz¥Û‹Ž™Ø™O͆^¹èhØÀ“ÄÀ/A.Æ ¾'äöâ =Kó†ƒá;ËG¡´ÑójB¹ï½Qò¤.’Œ˜¦¢ã\:¨N£i.ÊÍˤÂËfChéú·¶SŽ³ÐØ^ EAýuÔš.Ùyö‰•Ó�ìî4õ™®H|�çº8�Î#uÿßþÿO´ÿ‰A½TŠc½SbMЬ5ÌðäÈõH^+íö×Eßò8Ð�Ó"þ9ÿÀ¸$PëÁ›ä�sÞ™P�·Üñ¯/Âß@þÁzí" >`>†,ŒB¹xœí˜QkÛ0Çßý)nyÔ5+FëÚ­ƒÑŽÑ—=ÊöÅÖ&K‰$§K?ýN–Ý$­K›.I[„8‘t�N÷Ë¿ù|ñéò×�3(m%’ îÈrzThd%ÓípPÛQøqѸåV`’3‹–W?Q0Ë•d.ƨ›Ï&Žüª ŽZµTå3§ýÁE2;£9‚‡Dhu3�¬Âá [¼/ì ‰#æÔ’…QZ@ƒcÈ3f8(adèÒW’©|ž÷ÄS¦“Ë/qäž0„8MœFšÀ»fêÄÏìù…§þÛû8jdâhì¶Jà;Ú’IÈT5fš%a¤ôülLk63û�Éù´Ë¯2ááÀ«øÜng$ìcRnÀ–8©ù” ”Ô¨13iÙßàõEex+ìñ’z'Îdm­¥B<£KjÎÔ`U§ou�]ÔU‰›2¥5š±’9— ÀªMpi{:U™ucÅi—а©.½ÍÃN»z¸ìüî#&ÌÍöŠvÖWÜ`Wu*ñ) ‚rÍ%sUm,¤xû–Aä.RP²)Íù<®•DP”^3CÁµtCùq3“ßHhl{�ÆÒ™«×ÀR£Dm‘Ne,“Öì�¡Ò¹Ù+5߀R£Êæ|4B½ËUjN»pþ›S~��ßÔòT ž+ž„—N¶M†23}Åbs9‘úÌ–tI{ð›dü¶ëÏÙy»º+hD}ÜË&ö²‰k`ŸÎf¨tHxPãnRÜ*¤Ã—BiC‚/ê-\ÕÖ×kÑk¤ÅŒ´XÝH¿j$E½)/-¶è¥Å³{)WKf°3Ò×Èf¯‘k0Òbu#]ds“^ZlÑK‹ç÷Ò–Ò�‘¾vXqÒ+NþV§±"¬g®•¸�m€Oœl�Ïáá4wè¼ÓsÔI¾m Ýü2§;Å#5…Ä)�ÈÿÔÑ�Û=.3Q77ÂdSµ`!Ò“ù40e¯‹Ê5¸¨\ÝE¿IÜšr{Öùæ¢Im³ÃsSxÜ.jßEv†êDîåºëà!®£ößãÈÿCý|=�_™+²>5DxœÍ[mo¹þÎ_ÁúÃÅn%;ö5èÁv ä’ˇçk›w×oÔ.%^-uK®¥èïÌðe¹/²^ìðiw9gžgfÈÜþéÝ/o?þþ�ŸøÜ.Š;vþH‘ß…´‚gsQi_ŸÔv:þáäž[e y— +­ZHþ¦RvߪŒÿ²”•°J—æöÂ}Ån/¼´‰Î7(ûG »YJsÍw �¯Ù­à¥XÈ×'áãóLº<¹»½(ðê®ýF]Áó%Ï aÌë“ù+>5ðu¦óFëk~»ÕÝ¿n/ðÍo'w~ô䎟ÒË7îåÈ}ú£ûuv{A‚n/–½Ià¿1ü´OŸÌX¹Ücö;þ¶’0 ¼åLr=mLºE-Í9sßæjt]˜ñ«7ÜéÙÑŽTÛÇ\nç’Ë?j³ÉÒ¢øÄlJ+>±aù©àëTn+ÊœWÒÖUipezÍW2³ºJ—Çüú¸±¢²ªœqa[’IŠ*30�ÁתähUƒBt)y† 碉^/·zDc–Ú5)68¸¨sÛ^}°èáæÛµ«}{²¶A÷µg3ÑVûÖFÒdŒ›¥ÌÔTɼ«$ŸH»–²„•FfµU+Э� PÌœóÞç 0äD^{óÔˆ$…‚Ù¼ÖyíÂ:hû ‘]/dAo`;`/NéÉDÒVOÕ'Pêê¯ã¹®+†ûdÎF¸�ë¹Êæ°�ìRg™„ýJ4ã"_‰2“|²� œ±Ð5y'ƒÏÄÒÀ#t¢>%->ƒCä>„ìvþkQù€Új6·Üˆ:! ,ÖÖZ_p¨wíuî¡1‹*çÑ/ëR‘™0ÔŠŠez±¬-.ʤþÜþ/Œ?ü¹³5Áä¸Z7äüæåˆ_ŽøÕˆ7—E¡–7Qc£ùB—vNž²‘  …CY,É!1jÀ8î3Uær Zƒ†78‹\Éjµ>•ç³s.ÞØ A�|Zéÿþ’ÿ]”µ€ï7J¹Iž€z?ð÷rR¹ðâgQeó‹:Ÿ…­¸@‡£¨üˆêÉ©¨ ë¦=û†Œ 0o„rØ_tA~zâ¼Ñmì²’FV+Qä Þq¸È*€“èC,u"äVð8Üã7ÞùÖs .Ò ¬¥Vä×kH&KUýÈàŸ`g `‚2=Zþ¨uÈS¾›Ù>`…34ƒ\,íæœõ!5Àµ±ó é¸8F;cI˜Ây¢A 3†�O?ë’8Ô ’¦ Ü^Œ½ÚÊ Uš¥ÈäPÞ�4¾N*°ï|þ×ýã©Hƒ€…0$™ù3äA¿½R‚m¼}ùò%ëêT¾E±‰|C„ç!Œ0=GdÛ�KÁD."ÑõË|aï³¾aÛÇÕ¦/cfb-ä¾~õå¥.ÇËr!‘ŽÁˆ›ZW2¿ájÚ}GÀÙVˆ‹‘ Æ9BõóϲÒt¥œ âp¢}€,È&Ô£‘?Ñ ”5¢Ñ‰Ñp-I”–Qǧ*��`½$­uD¤×à ³tÀPhÌ>à2;$^ðÒ…¼IBÔJå$Ùjæü#ÌmF%×à3ã¬ÐÙ¬E`žfhÛo$('ÁÚ6øó¾»€Ë³kÍá=°ºÄÉ7WÓ)ŒÊ"7Åå™�3rÏÄzŸý )K¯pQNË™&µÅ·t *2œÚÙ1#<ðØ²¬à¬™¨*åiÆùò{]-„M2»�t„üdÄt«­Q²ßoø½¶ã7c­Fy€‰�“C»­sD�4‰ûí¦ºCêÀx!ݶë"õÔKȨÓ£èGRé› õ ×ÊȰH˜ðŠªt=›“H`‰BNŸ]´±,j3Dø|�.vÀøÛÆ��ƒ(lŸçÎé1š"<‚ ÓØª5P‡g¯Ü’U¶Í¡Ü�0ÈÔ°å•uSLuQè5yöè �ÁNTéj|Š.ºÊeuÞ­Ú–‡i»…DC9íO£JhMnæjj1ý(àu•MÅák *5 ôÞ'Å�BBAÊb¹,°nÃ(é@ñÑb‚%àgÒÊ¢·ò-%Æ~`ÁÞCµÆ¯s 9$eèàG!û7g¬~¹ ÌVX´ˆn=à ‚‘^Pb,ª™´þ›õ\– 8 ‹!Ìý]��äˈ¥¾Æ0i-©7HD\…Y+a7Ž6(©ùXOǨ –O <^D´Y�Úý·X­‘\œãÿÖ0lSÇ‚Çz¦@gôn.äe>ÜB‰À8È^ÆPåë¼±Wå r í�É:¸�!õA ¤Èu±�"”§\YUº ¼ò&¤,[3{£>K²8ܤà#Ë@ÐDºp ÷ÕÈ—N%1šËßFh�¦ÌAIptMD©*fâTŽæ1]$%¢]Ä·— sÂS®»8$ØuY饘aì\0¤9»ˆa¡Êaf O¦/å@nøPO²g¤ÒãkñÃøYøaŒyìz`ØK‡ä€?ÌÇeÓjU!™®À�/(Ö’Þ×�Tm8…�³V©;+ìÊÆ,r ·»‰Ü²NlÖ4x¨ÖbCÔb}"Û¤ª”É:i�àÙø© Šõ¥z:‡2‚Î's(sØ�CÇÏÅ¡Áß*‹RŸÌü·@¢ão’D÷ënÀ�,�à9�yüŠˆ¸1D�øü�^Ú»”ùÜà.ñ=Ö9;^ê£ý±C¸ûà¹ü/ØÚ-Tþ."¼‰ü’Ä(‰ ³/·7ª¢žÃV~6F:lé´¨-5£Ê�ð�QË {Ñ…´ºl¼ß7bòÄ0ˆŽÛ¹�µÉo:Ä+‡°{[xÞÔ:ÄUIM«NCêê{Œâ«WÔ± ¿f€-g퀈ÁxHs/ƒýÍpgò±=ÝÒ‡t¤¦r.Kÿ‹ÁŸ�ưgã­Î µUGÞ9¥_Œ1ÅÁ’<Ù¥p"pß�HÁ©Éƒ®ú›ýžºÄm¡å’/‹nƒÇ\mØ{x¬³ùü«£Á/ŽnüÛ*Ø[+Ò³y^,êõôÓ®4åw�4•ŸRN:òiêȯ»¦‹À.îÔjÈW%†’p—O‚Ä&¹�‰Œú�<¢¥žŸÃ&�ÿöðr„ nìŽHˆS‰lÎÒ½«M«]AN& rR´`#õ|xN’ãÑó‹ßÑÜ/â~ù§ÿ„<ÜÊ�?Ó¶ÄÇ¡à~ñ«”ý¯ßÁæ5ã)ÿ 4oë9ð�,|ZMi¦±ñ=È`R¡ˆ f«˜Šº4е?VÇ2ÕMñ_¿°*Ãÿ‘:ð—¦ÿߨÆ�¬À|PË >~‡XÁ¿:šâø/vº|Ìtû1Ç b7“~/òœÒ:mf�Ãöž¨MI§Ãƒpu¥›ßú¢|°1?7Å\Ð8T&H0j†èxêÀ4È&q¨=�2™wνÏ\É޽ߒw�ÜŠT˜Ð›¥ð=ÓtþÊE–áí©NÏx¸\f½’¸iè¶kYŸ;’ öp�½y�‘#Ë üÔH™8eÂÕg=,Ý ¥l –n­å±S5XЇ—Gß�I$  Š£º—™¾DÏ<:>XØ“¯5¸�æç©êB~½uBäê)øýCgµ¹^?ïE¥gQf—‰îø:`íÞ Šq6<‚¿ê¨ëLqOrÜ@=�ht8¯¹."Ó››]"�°_0ç|"²‡Øª£ÉC§7®·£ƒ»øà“5¼w3rwüè8E•²b)iQ<Þ žˆŠèóÂH�˜÷3Nì![ÙK9±Ò{»ß{Ör†F*"\ïÛ?\:<”Êb¹žÓ]*”ëì…©6ãÐ'6».{íÚý}@ƒ6.DØ_ŸÐ]”)s8ðÚ‘aº«¥ü$KÆs8뎨¯DËͱnBnmõþOaÃ*/‘¡Ï‚$´€/à@’· Ac`‰�ðå SúÜt2¤É¦;Ê'$á8¤û¶q¹¬®ð²U08?¥ðÄx÷-Ös é•Òu¿Âë�ÀËí²§d±/Õœ8Ÿè0{£/sn3è5ä8¢“ÂSº¢aÃe·æü‡9ÇW¦3MrwžÒØKªÇ]Pà4s¿ ¹ƒt¿s0öƒ”ñ*¹ÂL±f+¡ |ÙÝKêeº!�ì>-ÁL•®í?¢EŸù½Œ[©3÷’3Íð’tr‡ šlïΓlm„Û”àîzvÞɵ.ü?dºpÿXêÿIÐßý RR•+-ªZ0|xœÍ[ÝoÛ8ç_ÁËC·œº-n�CãHÚÝ»¾tÝà€»¢´DÛÄJ¢—¤œ8‡ûßo†_¢dÉ–ã´·@SÇ"5ßóãÌH™ýåÃ/ïoÿõëOtmÊbNfჳ>JnÍÖLinÞ]Ôfyù·‹)\7Â|ž3Ã�(9½VŠíè/®˜²Ò³©Û@fSOh!ó’}�71³Ûpý–¸6’£+ù»‹°ïeÆÌÅ|6eHéÍ<½ w¼�«šLëwëéRÿKøŠ÷d2oä}Kg[¦æïgSü ïèl1·4súÜ.å¢t‹·õºõí&|{¶æE!6W/fSË`6Ý sú^V@�WðÓhÉPKý’¸]¹ØaK}ùã…»Ñ ÚH�'Íy4¡fÍ©¨6µÑ£¨PVÈjE�7¯4¸‡¶EyIéíšï-kmèšm¹e ÁuT‹Nù}Æ7ÆSÁ%é}ˆ&‰¦¼6´ààD*+/µc´ƒvl ’|€ S{Åð{3¡wkeĆ1Ë |ÙòÌH5�û3ÐPøUÂo|ø¢ËYÂ*4ÚpË•áùR›ñ2º�™õÅüÕ3d«¯^ͦ¼œS^nÌÎSüò5P6îÒþ›¸.jð ­¤Y£YX•GBs%7žO¨–pÍÉBKøA峬.ëôÍéRÉ78¦Ú0eÀ×ÕŽJÔž8»Mš-U]r%2OQ.�suùÀ•tUVÔ92½ñ&¥¤J|þ¦K¡À ÑÎ3+±5ÐÇŠëº0T`ŒYE–ªÄóf­rânáöƒsýÒÄk®cŽ~ãO¼¬P([1Qió–ê:[Û%b±¤¥w¬(À52û„Y€Š%ð€q‡áW+T ‰° 6H0)i\ pI o‘/ÚoäK©Àôজ@\ »ƒ2_ÜÇo^½ùëå«×ð— ñ[à_[Új#@æwʉªµõ™ÒÔò Â5H¼€FËC"W~‡ø¡ÂS#ê|E9#¥ìÔC7á‚`‚K1gì߀¹ î †â@`GyÁ!oM09hÉU­[dUì¬×Ñ —Î ¨„5†¤eè…\«PbÐ;Á§œÞ ³&è�ºr»á¿ Z… ë<©r®Þ¶=mÝìÄ©d”ÖFºÝ.çVsIO‹F6@…·ò-x&ñZWS7ÄߨÅMÊò\[«–Bk„+Q­ˆ3díֲȭVB†§Ba…±D«$À§p@öÕk©ê¿tv-éÄzàÔóÿR Ê$´ñ˜ƒL;¿ðò�“-X^¸DæÔbË Ì^¹t¥€Þ� ÷)«›ÈêË_“Aá¼êÅŽ´ �S뎀D­º‚*,4D4ˆ« ÈæßxHå¬9}Åm䱘™!9Ÿ¼ky‡$…j€ÔíX”Å–çÇâ‘v NýÒÙqé<:Nÿ Döm¢ÔK÷Ý£ôªÅâª?J·^ñnú3„¨â›²?BÝJo€æM˜áö�¸¬ÚG™�Ƨ撿Þw–[øtœÿ‘4ÐÖƒ¨ýy›ßƒÎFÕ™‰™�ØÇ=7í}’:Ñ :FÈ?b§r%Ïö&ñoü$þ“›ÄÃE%î]ŽÛ „Ã�þ„Cw®ÃÐ5e ʪŸ£>!Ò6o‹l![‹U|eÉ>×/"†Êi” J’cm~ ¯GÅÞP–$±H’`<-÷GŒ$�ÄÅnÿMœ�rø¨úþ¶ IÅÜžEgãc8¸áª¬Mo½è—Îîœ"�á‚I`ÑßyÅ+Äb¾bôXE,ík=ÑÄŸ.?<ˆ¼l#åŠí:oÕÈFáFxXq¼^­0}ðµŠ4» /¤{†ç;È&\DÕ•6M€öŠ ÛâØ'ô�1÷SÌ.ì” gØöuŽ ]ÔÆ½<"«ŒûÚÍî÷e¾2v`cƒ¯•øjNìÔR¤i‡[{NßžL{=í ˆˆŽu.�nŽxœ­XßoÛ6~×_Áå¡i1;ÎZÇ€Ûdk‡¥ÝPïa{£%Ú"&Q*IÙuþú}GR²$+qÚ5`›º#ïÇwß5ûáæãÛåßÜ²ÔæÙ<šÕ‚'øÈ…å,N¹6Â^ŸUv=þùl‚u+m&æ ·ÂÊ\°�6z6ñ«Ñl´WE²§½~"In÷¥0SÖWÂÓhƙ⹸>«^ˆ¼´û³ùlÂiƒ—óîh½ÄzÉâŒs}–¾fké¸HVMÙlËõüv6¡vÍf«£mVsöüÅlâôf“ò»íé¤Ì½{ìü�ñÓþ¯ƒr/5ò:ªþõ,Y&Ë«ÞùsöV lŸbn§CJ¸Ö|yÁDnkSs3~}æu½™=㺖BÊ´°•V†q6y³·éÙüòi˜«ËÙDäóž%Q0…±nXê¾ÆmVèè±múi:=V¬ Aîs%Œ 3ò^ŒØ.•qÊòÊX–ò-¤-ËrË %X}ed�=?WBH%äü`[[²NÚ4bª°©T&“ÖQ€Áš$ˆS¼NØ!*ù´GŽò%¾þƒ0qJWb�\NÔ©½,ÔþÆt3g^ˆ9€ÝÀµQyr¶àä�Š‚[GÖ�6Þ–˜[‚1 ¢+¥`ш2JŠaÒá^é£Õ÷‹ ¶tÕë½鯸!ìV( èz£Ïo+]”b²€’2çÁ2àì½uxôB-ßÖ�ÈàšÎ0ý`Å\Elø +t.•H°Õ²%ŠGAÉC9êvÊ\ZT¬‡¤Û�\Ùò¬òŽ¢q±µ.r#‰Œw2ÁÒd•É‚1‘ÏDh—ý±Ë¾£ŒÇ …�ˆ«!­ðû°@h¨ëU¶–"KŒÇy"M™ñ}�ó_ *:Ô”3‹÷ #î ïÈÅ·\f|•‰JMd BøµÚÃm`¯ãw©Å5¨b1­½Ë@«yÉc bá y˜Øš Î%W¼)™ç‰Xó*³ˆ7D —øP¸"=Ø™„e|µÒb+Áäàoì-}•+•±\Ù¦Žë(ÜÞ,{!½½ýÔ_ú­µ‚,ûE÷¡ì—ä}è]žsƦ±\˘‰/Ö÷”)óe“t-¥¸¥ÅNQƒbbKP"¿.àuÒ�ò…П7ðk¢r·Xþ¾x3v1µÉ ùá�:´PÙÞ�ìÈóC¡mÊ9TÁJÎÃʬª)í×»:-¼"qì/h.vÚQ°ÄuRXR¹­TÊ5ÕK—XñàÇWÇS-¿ž¾º¬#b3I0è÷{I—0† û�nWšø6ò€?Ýz{î6ט›tËé»I3/hи®�H’#qÊÕF´ˆ©G±GÀúÚròäëºÙ ›4„u5Gmzº!O)0gL4œ$j/%7@ôªaä.�ÊuÏȽÆÂ`Ô†;(Íœž¶MÌ3®}â±%éĤ¾ï TCt(ièŽìj ÒÏè±Ge Í0Ëÿõ½@‰]0Œ�©±öÅ6°¸Â"Ÿ+zûÐT–+´¡š¤©²R|½v³ä· c�nÐØ BD°D°5§€¿MùÖÙLÆwwwã ìÝ»ižOMs¡k 9m¹¦+l}ãÎv…îŒTÆôz²i{ˆäj3¿iæ2Ç~œÃ�³°‚ cQZ÷^EÄ•¸ÞTD(S+ši©ç„‘Ä�¹Q.(Ðrb4Pø\‚ù€ªöˆfL ½ºHG8wÄšZm%ƒ'(7†—Nþ6D¨ ’%#?WR��nÕ…*ÕÞ¿aq¨ÌÕ(ÄA¸ë¯#?X®PRÅY•¸ëó;ºÿ;,KÿŒ^)ÁˆÇÌâ#Sl©6šçnðID‰q5`ûÝ•p¯x\è’Þô1 ¯ˆ'þµó¬?«ˆ é é‚!0„Fxœm‘ÁjÃ0 †ïy -ç¶f…Â(®/ë;m‡]vTm§58¶±ÔBÞ~JÓ0V ÙÒ÷ÿ¶~Ú¾~ÿ|½Á‰ûh='�NRïÁž°’ç]{ænùÒ*©sàè�;Wä�”š‹¯˜Ùà æWL«2ZØ*è®D3Õ]¸ÌCzZnÚGè¨xºà ÁôxhÙW¸x˹‚q éd1b]|tͽì²€ÀlNÙ}d’$W9†$æ cDÙ<˜¸º­¯dÿ1Ý>DM?ý JĬܗp/¯dOxœÍœmoã6€¿ûWðÜý’¬ÑEÖÀv·{-®Ýímö8à€€–Æ/©�Tç×ß I½8¡_d[ʉIKõp8ß|õ—Ÿßý÷Ÿ¿°Ìù|rUÀS °œ%×ìÛie——�Îðº6‡yZin…’W3ÿ}r5 Ï-Tº¦\¾›§Ür».ÁüÄÚäx}rÅ™ä¼�Ö—§ó«§‡¾ïdŒ_&W%KrnÌÛiö#[ü»Ä¯Ó'*…î®�G˜ŸKp5+)“9{§5_3 ¥Ò ¹j˜ SKfE¬2t})!½ÌA®l®KaÍ›‰Ï,÷5Sa.œúüOCྲ{žW`×ÀŒUR&¤-yž¯7õSªZäP?CåÁ'w²É+«bÚÑBÎKƒ¹¥|m(¾ú c?c6),y•Û ¶Ôž3‰’©Ë­¦îâJeY*L™ó5¤lQYf3zÂ0‡ ŸG°DzJ”‡2 ™UX¯RÕY/•¦Òˆ“;$�×Hr«Ø\é1W.ÙX¢�[,¬Í´ªV™ç`ø cu• XÙ˘¨b!d�h‘Ô¹lÈPH¬>‘R6$V ”0'¾¾Ñ·¯dËJ&^]<ô°i⋺ øÂg—2UµÉ&áb!deáyÒ ·æ*—i¸ƒÜ|ÝC&’,Hf²Y”V­…d¨l…«cÎH·sh+Û+5c_3”ΤS<ª~žÅø=9GÕ$Cs�)¯/;+ͪ ƒGKŠÖ•öæk)ÃÍjêêÖ«’«ù5xlÛ³piÂÚŽLÃòíe2åúCÈù ™¯ož_�Ο_!KsÁº9¡ªxŸCým:¯cÞ69¼ÈÛ©÷ [²´&Ø©ì‡y›_xÓM©U Ú 0.ëéüÏæå‰�þ@•DL¡ž_Ù4X¶½™}SßxóQé‚£Iô¡Ï8?Êÿ?ø†L Ò¥ Ë3|–§~y”ÿ½Ó¯ÔÃÔÌ{|ûÍMsó† U(—Oè6$dßÖ ¿u­y|y+‡¶3I;Uý\5*“ðàu�Tú¥ùlšÑ›®¼ÎÎŒ5ƒº�MÓ vû%Fþ7hLr¨I²Šágl�'dÊ @Åô�¾iv®¤ƒ%�~PŽM,ê7ÐÕÀÇÚ !ÇMv*÷@6~Øj ·!8~ö¦n8±>´xV[09ŠÃ[&Œôƒ¥‡F‘&½­»ÇÄÈ.Ìç*ê|ˆÖÑ¡uPÖ¬À†‡1Æë2/‰œBç©ÄjGq6žVð�eu.À´v<"f¾&…»JÜS·g›^6¸vèl˜:—×Àʰ&Óæ£³ ¯ä±MkÇíXf÷ø œÁ‰œ¶Þ䱬!ƒAiƒs;m½ÜciCË6ÏEƒÜývŠ”Û\6àw;a×UQp½¾ùMúN¿vu§óp‡uîôw³¶eß©9ñ„Î0}ÆÊþl¥¥!Q’SÝŽý88¦ ›í‚Ð'×{¸Î�³4õPh`*¬n¬Ĩ¾*‹£ Ù°/m 2ù„\òi·¬¤’O Õ×�TÞ˜Oç>Œ±ýîÍý¦>y÷å¼(·°þ•d ‘¸ï-AûqC†IÝu.÷ínÖÿ¨©>—P3|›v—Y{¹ƒŽfÜ–V˜Ø&5ÑX‰?âà½-]£ÂfdƒKHÄRàøCs¹‚>²—/QyU8<ÛeutL,_øh4#æ>BcöBQÚ5ñ¹ÈQxîÉóBÝU|Þ&R�†5Ãócz]!D;²b#*·ÛÝý¹‹ðü¥ÏÛÞ;ÉñÝþŠVp7�Ã]¬Ð¿�¯ ì:âÕ÷šj8„b…Ä*ê;ŒJa‘"º^4&E4|u ”Eþê²�( yš,v·Â?8:Ï4Nbí°{÷¤–¸ë5miùM%~ÄÊûnapøFë4}æ6h ×ßø'N2ÆWæÒg”0M…{û‰zqKåaªm4ÿ”4HwÇá¡ã0ó_:©6ì5I@�¨ÚŠäEän�ƒDkCHä‚h›öÚ|ù€ccVT¹eŽ9Ž'³"[ÿðÉkÀånëu>²W‚9,i}üÞoJG€ Qüˆ;Qn¾‚/iy¸!S/f8.ܬZ˜B_É`í&þ‘~ŽýaÜŠé?äNAör\x£Åµø}CbÜ�âžëù»«ì?Ì}ýÙ YPÝ”@o/Á±Êúa6N D›�ñ%sžÐ<ÄË]…hq-É‚ܰzSª£˜ˆDån;·#¹yô®Ü¤RÿÁý�öUH7Ÿ�6ÄvàaèL�¼œÑ9?_*–KÚµŒÎc¾WEIî!Ý 2ÃÂbãéRÚ³­±lhNjٶÛÅã:SY¯ãÏ&Uá}¸SU4b¹EbtÖœ�¹cÚSŠéÚEöátU8,îYïñP þˆâãÑIv/>þøâ#,ößø¨Ë\ÑL™ ¢ ªBZõÀuÊ$¬š‚]Œ®Â©—cÝ)‰-õ5ÛëFa%/–vº¬â]u׈cÔÆzÓü‰Ó2×C³eV¦só¤I™/i%° Ú@÷·ûöû<Ï©ºhßwÎõŠâ¡�žº èXêäÁm8¦0ZeîNÇh ΔaŠd:wÁ.%¢j¥yÁõö'îhæÿnË&çV»Ý!¥»Šk+rh=ÓÁùn+m•¨ulo;äE‰ˆuòQ( N£~µf]¶¸¤CD¤c¼žÒLá^x¾qhKlò2îß?çm(1?jW«´¦CVÁ½>Á  ´¸LŸ‡ ø¼Žé�ˆT£Iá~LWÿƒÌd†«Ü|ƒŠ‘7PZ𥆻 áp¸§’¤Òt¾$Ð�a¤J��Ù™ÎCdsPTL9Ž4Ñ.JXÇöÖéÆásæ. ö‘�gÚua ?÷2)e}Ó6ÔA:²ý1·ð ÁÐNõ;°ª“�"Oc±Â�½l–ÓáÇtä^'&Ñ`ÝÎñ6ÝY‹#ƒrcÒ Ç ÑïÜÝá®™s¶‚æâ1¦_ç Aj­ëâ÷rî�6ø(rô©nÜ„k°7“]0ŸÌýÅd§9ü‡¼xc¶àÖMÜn�/ …ªýü³m`ïI)ä­›ÚMÙ M^ “vî`ûÄÏè,9^M2¿K©Žm%tû�Æ¥³ª¼õtu,~˜¢d»£<å©6­e%zHÓ¹ã‹Ítç\Ï~tnôV~‡#=‰ñ}–pÙ�á¹ñ•ÊoQ9u²å^‰ Ä�ê$ø‰!— tdöA�HjÀúe‰ÙFÙ®J¼ ã#M[úp!<&yeÄ=\*=>#¶ZsÁ6Bwó´Ý½¯ûÝ——½­ÿ9˜Sz×hÆ�¹)úE†$þs ï•Lèd4­¬� ô=™ÒOŽ"D¢ŽŽÒâII:š–4P"ºâ|F2:éÈB$Fö/ú©‹d\. ¥û! Æ'?Kà™Jqúhn? ¹‡"ñlÒµ<{–i¸¶ �¯ó4+ØD§nwýøüB3™›ð ‘¸ÐÜ­á ¡“‰%Y€:Þ¬�PšÍ™¡�†=Œx½¨è$Sˆl?ô˜ãÀ �jÍ¥)•ñ'::`ŸŽ9гN4tbÞot�Û4Sj«×¢m^†Nk�z­ T¨ÿ¯†ìS¾NržÝìg”½½ª‹÷ïJÛ,:›ŒüË­',›_òG):‡; ?M7ó?t÷?wdä »»”B1©.ŠxœÕZ{�Û6ÿ_Ÿ‚çš𮑽nq·uäšöà²=Ü.ŠEq %zÅV]‘²ã~ú›>$ÊòÚÞGdcY$‡ÃùÍ›žÿéíßÞþô¯ïXaª2MæþCð>*a8Ë Þha^OZ³<ûëdï�4¥Hó¶áFªš}ÛnãuÎ2U¯EcØ ?ø‚™íJÌgvI2Ÿ9Ò •oq£WiÎ Ç9úŠ�D–&sÎj^‰×?xî&é|Æqƒ‹tg Ö^ÀЊe%×úõ¤¸dK 2•w§ºbó5oÒ·ó~°×l¾H;‹”}IãíøËùŒVÏg«§¢üÎŽOíì7ÿÓmýàóqq++qcYßéÏ·‹è¼x_¯Zó½j*n^¸}Ý‘ß_ÿáö0Gðï ¾šÓ9û¢e)Wß^FÙØÇD,‡ÕbÃxÓð-SK¶”ŸD~VŠúÎÌÀ¹ƒ©éóÄ®ÍåÚ¡Òg—KÎ21`:˜é¨ °ŒXÐÀC¦Ê¶ªÙZdF5ÈGØ5aËFUÈf[‰Ff ÎØÈOž™#w>ÅDzly*‰±â¡WÝŽá²å`¦8ºt„Bµ�ž²JÖ-P›¢�J˜0?×L¯D&—Räl±e÷°Hž-¶¨Fàb#×¢Ü>ƒö˜¬—JÒËÃ¥’tbq!ùÀ[ÐéSeÔâ™}x ¡íõ0#Šb�ïtÖˆONÏ ì¾Dm4%_i8Ú–>gl| –ñš-À:Šª<3 =kS40ýÎf‡3@–`Dœi¢èY <&¸;k5> 5“ó$EqUUWZ{_‚ä`8ñãy~µ3eIÞç±¢½×±&{=ë^$‘8Yð,SMN²Q$ { %ìí?e›BfE@«9 vyYª лr"i1]*ez�a&LèO:<ã#ÃoêmàT6ÅÓhMâ¥VtªÕJÔ9`ŠÂÚ  ;¨ÃMÁë¡Æ)²Îe†f‰/^:[g¹¼“M¿Ùq=¦Wç77}íë¶7tîN4Cá£u šIPÝ× Y;ü£ý¤†êUÉ·"÷(¢d=µ­7=n%B]lúZUð5é•kÍK™{˜|¸Çb@¶LjX.б^t² ™0Å]ÖÖ j¹ãmAÕâh…Þv¾ ŸDlTìËWôÀÄï-èûË×—ç_]\²‚ÅË]µÍÇiál$ŸžÒÅW6¤Œ)š1b;a‚‹F»S:ç¢Ò©\T#LÛÔè¢tÆKEðß!×±h ‚ô£bÞ ˆF�FÝ*çï`*ÕÊl^o ž=˜gÇÊÏ¿L™ý{Ù³ï®!Z¨ú.½/€– è̽J@‰9ÿ°ìʧÿØ8ŽeàŸ âIJXLMÙÁ%ˆä$Åÿ�\@NRú8r‰u’º‡#—9 '©{8z·.uÁ-»oû 8Y„J´Ëð�ÚRêni÷¼»ô <8oÞFÛßNÒá›6@ëP'=ˆöiŸl)M:5ƒ:a´îßh3=ZzwãÕ÷�‚)ƒµQÍÔ' º·{j¢ã}·ó±ŒÐ­'g+88fQŠ r4ÉaÂqt‘1ÚãrœÃž÷ÀÜæ÷±›!êéP>¢ÜÆ/ �@ øC0÷BbðHº³ôy8?¨ch L7tJe` q¼Ç(ÉÞ=|>@ª°¯Žvm§Aâ ÐÁ<ýaÊâàÿOQ`ûQ-�÷²õŠƒ‘è4ò{ ŠÛ ‡êó‚TÃ3ØÊ…yu£6lÞÏÈXÎSføo¢Æ×¶orVñ_±mÒä¢é…ô« å�¡ò’Dp#sù>o[ñózeæ¢JK±„cÀ$çÊÖ|;|B~ÛȻœýÚjC¥ÑµãÛÛ¾ù{8 4T†[µñýH0E¹Œ3äCá" hëh¨°C'5iŒ‚ƒ'ÐéÌ£Zv?�Ÿâ&ÅÑר±Ñ·JF_]Iüˉ¼Þ§àHädçÓ“™cb�rרL�*7Å$½þ‚êìo¾&õtóp;J T|ÝÕèq° Fà+)T/šáš‡P¿c—‡’t<,¥£SV©Úð‰¹æt¬)×u ¢&c7²Î@u-¬Æje°  Òonkæj,—à¤Së?mÁk9¤Þ@,R°^n°ªq¹}<ÜkWð}¨ÒJ ž˜g�ÒhOË¥hPNQÁÕTàaý5äHæ²N[»òÊ5Cºé\B#%‹ Úé=›�8ÅLi&rœ© ”„Îð:¥©o±–9œ ¯FòE)°_¹h °”¿‰!„VÔZªî¶6üÓÔª¦§Ô$ЍG ðG,£¾*ïwVVàå‘0«­¯ÅV2SNÑÛ7ªÅ6N0ûÀHÐM;ƃ&ìóÅ0ô�0~=tÉDçè0¾ÓòÃö©+á‹f´«»Ž<P9Å݉T ‡®‹Oø(òE)TÔ&(T™ûygܤ7áH ]ä¸xÊÊ–æc;¨=o zÚüš_{6º¶Âx´�ñžn½éö«×-¨Eì9`°ð9è„Õ¾•’Ö-KÓe °´- ù5ÐÕÖIÔ¶AO‹”ƒS£(m0²H]1ÈD�*d ˜‡%#s “‘K£m—Þ/¶‘‘gƒ�eßÒlïÄ¡D݆Á)C›°ÍoTvzXàm±lëÌʰÕâ�yÕhE ¯OIQÞõÍ�ÖžRùìKJ7'Qjqò&‡2 ÑÍâ[¦¶NÙ †º¡™ƒò)PÐ~*8‹øQ&³Àég@�8öåIäÌC è3<¸/»�•�+ÀÄ7ˆCƃVsJjóçÚI7߆² ›Ö@FÚÄšêÂR„ëË G÷¶?�ìø£Î¹|gS4sÑ:7hÁ¦Óât…ñVÛàÔ49ˆæÍ¶×~® Qù¼–wµoLwz�NDzM¥ ƒ×À£lœ�coå„™DLÍ.ÿÐwrž0ÒºV¦ ïѽÆë�Üy);‰Ôι7?Ï[[UÚü2„ŒÎ9îyɯÁJô‘KˆðÆ& µê',˜ÃcvÎO˜�`r¼,w{ƒe¡¥CAGÖDg;ínPvÝÿ>gæzº»îÌuyO�óûŽÍQ9hú¾‰ù¨\cÒÓ±–Žh÷Âáè�ßqå.*FmÜý¶H÷‚ê (wYU2`Ôeͧވœ'±Ì(žÚ,#J/ög‡s¹íxÿ•‚ä#ѵDžܓҹ�½tnûY ½ï‚ÊWwŽ«ä@u—$#9ˆ½6y$¬ŽÊáJÔ€%­{&0-OìTãKN*\MíB.« V óDp9zÌØç�Ìóõü …˽]ÐÂMß#A tž4Gï ùLïy@ó|}Kë_ËŽ™[ÿ¢öÑ6×'öd†×}�õõ~R÷4b� ö8<Ò{#Hgîà3û“óÿ¾ú”ê  0 0‹M2—ˆxœ½˜aoÛ6†¿ëWpù0¤€'AZ ­c [—­@ã[Öa(Š‚–N6ŠÔH*®ýëwGR²¬Èq’Š©ã{Ï�G�¿{ûá§ë¿û™-\!'ɸþ<Ã?8ÎÒ7ÜùAåòá#|ï„“0É*Ã�ЊýQ7+öNåÚþÝxæ$ãQ´5ÓÙŠ,ŸL2î¸[•`_³ûMàÜdÌ™âœÔS�¬XÃÁd<âdít²õ¿9Å×%K%·öü`ñ’å'§:Ûè}ÍÆ·ÜLìz<¢¿ìœ�g“ðõlÂýàÛ0öb<òŸŽGåCÍf¢øòÓƒfúÿ¶ÔÉöãiýøý¤å›û$à¿!>º»R>ykF/í–ùT˪P¶»ÆÆí/ªÿ¹«~‡¢ û\es `4›éœñMqcøê( ³3q[{PØá˃` ¨ï&€·ÕyfüŠ—AÙ-¤N¶nшHØ¡ªŠR Pξð<]0t”%y˜Úþ£m½¬½l-~[i'ÐwÕîJ¦F} XâRm ØR«L¨yK£-!¹€Œ Õ1zÄØ»¼óŽ $“D,²¬zr“&Õ苃›šIPsÉUˆ¯¤FØ‘è4ÚØ¨k£ª«J:QJؼ´¬à«„qi5›Aë3n™…’#@úóÊǪý/’�ëàŠhOW®¬\3qcòtwz $²“àR·`¸låë†õ”À�7M›’€¸àQÂ.ì yO©“Ã’rl[…–‡/C6-ÐŇ\Ë̆�;Ã…ÜŠ¶ÅE‘+ ÒÞfZÁ€!²àæ' hú9æ[²c{P³Óál5 ÿE5–Sܳ7¾ñ£ÐXÉI–�:®j åoáA8Tú³[è%‘ 7{- HZ,Š ½€+.;S[.ë «Ã>ÂJÑwÐ`Þ¶ï¤ñ½GÍî*éá¡àvµŽvö:6ísC¨§W¾¶/bOíÛɲ®&¨h;)cèd/exÊÁÎ^Ê×ÚaÚÜ­äÏGš„<Št[Ž$ßÄY­{)«õs0&+�Èc¥ÕŒÞrð9«õ£øFIô·pgZê0òÚÓ6ëÚÌ^ÜïÃéºÝ"Åê¡t7+dzz?Þúœó§ŠVs¬µI«ªÇ‘{X¥$X’¹¥4mØyM¯¸K›ÒZjã‚¿Í=ÏC’KúÔ‡Î{ÒŠ×Tc�ñF±ò““W×ï/~„äEŽçÀ#Ó$VXìéˆ0([tÛÂ$@„þ2iПfKŒ@­§ÞP^,|å©ó’ã®"ᔀ´ i…©]W™ÔhKó ]X—ÚÜ€±ã‡Ôñ[ QÖ[�“f…Üþâà�¨ ¡¡ž\3½<XÔxœí\moÛFþ¾¿bÏÒ ãs€‡Ø1àÆ-Pàì¤w>EÑ+j%ñL‘*wiYýõ�™}áÛR¢$Ú¹0BîÎÎÎ>óºC]þíæÓÇû_>Ïz™^±K÷GŠ)üYJ-x¼…’úÃI©g§ÿ89ƒç:Ñ©¼š–…ÐIžñŸJYlø§•4ÿW—gf»<³„&ùtƒdϯ¦B ½YIõžo™Ù¥à™XÊ'nÜÛDM¤^K™�\]ž ¤÷îªûf¿ƒw+§B©'‹oùLÁŒ8ŸV<¿ç—�¢¸ºÿáò ÿòürrU#1¹â¯iÄ�™ñi¾–EãI¹Z¹'o.Ïh�˳Ug}øw ÿÕ/Á‡}’dZ�"½i¼â?$Ù´:™Ê¥Ì´âëD/’Œ«•Œ“Y"§¼Ù\¾efÞ4yt[[ªÓoO )³­öfüN—&/¤.‹LqÁxšÏ“X¤\…ØDMÑE|½Hâ,ÉõBrÐá*ùCra$ÙªÔ~³ÌÐàu®¸i$šÇy¦E+š=颔ޗY^p)`•8/ ©Vy6M²¹“«X°"Ä…Ix5aN6�ÝÓÊmàCfy˜‰Ty&r Z¬%ßò�”–¥Ò|"Û»W<ŸÁN–+x8I­¸�ó¦X`Ï",e0ìQ¦ÜÕ†Ç"’¬¶1´±"†ÑüQÆ:/"|&S{f´`k„¢€�.P–fœ=|N¦\˧êô+?ˆÜ ÍçÀaÄv©“�Z7ܧ›@ ÁíµXø°&y™M•#‰™çÑD2³<"V2¥ô›/Ñp§‰Á7qš+9ýÆ1ùz*g¢Lõ`,NË)pK¼ÐB´¾]é™:©|%3OH>2=•3íI€@LÉ|¡Û Ðfý̱M�k ‚Ü#â”| ˜ 1‘ð É‹;f͆�×&;õukìԹܟ�3<-Ø0¸ÜÁWê%Ôû°–5UHÁŠ¢p¸8ƒö¦qž–ËgjÞ}éž¾ÌQj»²Ž7úzŽö–'³JÙ�EHhßDËnú0‡dIY©|�w5F–-WS3ô]‘5m±ß°¨Ý½X[°ÝØ_7ç2ïNoìhgˆ/ç5Èžoߨø+�¹s&ðü77ŒœA–g§™œ“õ·ÆÐž¾Ûp0,`©q¯hì­À�·Tå}gÖJ¹\éM“ôj HZBÇ#’†CšúÅah¸Øiì>¶ßK‘ö¾:&*·ªã:o8ë›w£ÇßWŒø«Ä6Éêb?èÀÉ’Ó"‚§“F/˜XoüÜSF“â ,ŒêL¸8® ÉMÌ 1wß“Ãc­Eò¢½8J25 o¹5e»¢ÑÁ(;&p~]luO’…í®“Ž›tHóÒä|�ÑW4ÌôDæÝÑö {yƒ0`É,‡(RšŒØ-Á@ã(½¹e¢T-áÇŽdaC‚ÚÚÌÅÛ"ŠåÑÆÖìajþ2‰ï�#ÞfkØöä[Æ`} Ô‡œèN E!« [(ónŒ@ÓQÚ+ÒÄ8¾[”#¡  ÍÃôÝÌuÖbFG†¹2D£ÀFC`ÆMS[ñüú‹h,\EÃa*íf™{'ÖPçÌÐvôÒ~ Wüfê$³„3LÃîr}z}zW.'r@²sÂcà‡ÈŒ ·ÙÃ೟ ;£§D‡{Ö†æ�(UWøq†ä´öôøh)À¨?…±aÞ�GiŒÒН# ÃÊÏg·Œ qú×n0NÌ*G-²�©â®a¬tdkKÕÚ ÿð�¿³¤CÌ‚ð#Ñõà‡Þ@; 7×�°à»Ýáó¨ E®øZäkåê®û­þk+*±UÕ©|2O~ qÔªÔÈ£–ô[Nå\Ä›žM÷ÞÐA à’ú@]%††}Ÿ]o…üývýzL»~Ý2ê …:Óƒ"óþNÜùÛ†zŒ�åÚ¦Tù¤«¾Ü”Ý�¶H×�«0+”ÀRÂñݲߪ <ÏR�¾X­RºoÊ+p�i‰¥¹{¬Ž#£PÆÜôa!"�›­ÇB¹‹ sº¶²Üä{È…(0 k°ðAYã‡cZgæ®0ýZ ¡¼3œ«Ú¨êˆ{ˆJîbÜêÖE8«<Å`лË#¥-‰$cMðzIîB=8鎴ÿ¿7Ñþ£a1(žS®E mò2¤]*9�p %Úé¸Ëuç⢅¶Ð1Ù«LVá( �aXõ‚m•ÄA4�®kÈÂç÷•@ø"™/*®aYPºx�—sÞ2{û ñ.ð?Y*•âË2ÕÉ*•v<Ä£O‰ÒÑNžPX%.g37ÏmK�] ð�…I«{" óò¨Ð‘Ø“öö­Qi<3gO4LNµS_ 6ƒË�«¥»WžÔîea-µ‚ÐSf +>íŒ)2AjÀ4Û!Îh³Ò ¢¨ön‡Ø²Z³C¦)Øð= ñFÃŒ IÛ]\ûà—êéÖX´P"«dªU=&ÛZ€fý :ÉêóƒÌ¸ë’dÏöÆøÉÒÔõÒh¬|':Ýà< ºf™-C¹D;²¨]8óÖæÎYƒÊ „“V=ÄèÁ1Msé|²W‰FÒo2ÑS�‡cd¦DfÌÂE¨.³ŸbKÇ/( ¡xÑŒqÙ�âEÇ×' 9/A9ð/�qžÄ>nàXÚÖŠ~ütûùÓÝ÷w÷Ç8ž`(ysßJí¬ ½ùΫFK ÍÍJÄ;„qKeΉ‘µ‹ijv†5K,õdÀ÷HbþQfžÑÈÁ42$áe�6S®Ÿù’Z×È!_Á2¼Ó¨7jR„¨(ýúC9ÙS%AŸ¦Ï–Úûw|^ÂZ\*r:ªâ¶‰µO\ “5—†‹ü^Ê,–†Tdb™?JÇPûdÞÓõ°\¡Æ"³`祊‹dBsi(v„ÚpSƒ‰ÕÓEÑ:$]Ã)ó[l!Ei’àD"€RÛžâ&è‚È÷f¬*q»sÙÆ„ Kñ`ý�[§ðË€y#�`çZª™\Ó] ØQ½Îkl”ÜYoOyræúJÈŒ"d&`›üþ€r½©.F¸¨jx¦,'‹¢êÚûÛMlñÖ˜äx=ÅøJ:‰‰vÅV2ˆÑmë¥ Ó‘¢ðI”OÕ*aª¬½pEÃonÁ',º~–ò¡ûôFlT 'ñ¢?÷ϹY,¥Pe� E*V�¼q�XuzÈ0/Er"•щ™BB`,ÙnÑÈ…sh3.¬r;:•”(í¡ãå Èëí�^…]kÔ¨9²Ê²( RÈ�ñóµ�¹(R=জ�eU&8Eomm6Dg¸ën æß ¦®CMÎÞ¹¾l�`¶óaLM1)51Ë£ø�Û WHëU‚H䓈!6Œ€ÇX@ÖÌU9Ñx»‹ÂÎ$¤‰*ºÖu;ý¥þØ÷Œì'Â*"=ÂÃÒ‹B¢èÊB™ÎQE¬OHû “~[ÎPYm¯@#\#»ë¼L§x8©5ÉN•Aì·×÷ÿ¼þìoŠY`Ðk‰£ñ£{£�'ò­Ë&·¬A|PÓD¥‹<›_ÝÈÇÄœ|³ÖÛË3û-SÃVcfSXmjì°h½�³�éW`¼�×&˜^ê÷2¿oµÎì¤ N¦Ì„øèv$ ¯nü,3Ô¿ˆòZlêP-ÊêR}Kô”¯{"§|=FôLdƸÔÁòÉ1Ͳ8ÿË_éT»Ø&[M±Göm±óñöPódV=Ð8ÑÜç3N=í’ëdùÀ;%áǼ˜Êb@£õîÌ*´Í>|Æ.Ñö¶Yµïö¶Á^çöÖÍ—Î;Z5¸‹ðPÑõu´.îb `XQXŽ_£¶¸Ö5šé3·lÓùb‘Ù H‚Âß,šc¬t»ñIo•’.dü ¨­«~y€àë?[Zñi¶#6k«aúˆ-sH°ó,‰w‘ó½† Ù�5°=8Öb(×- 0›_b†d:4xµ™–°BÓÚf/ ‡KnD&ö—xÞ†²Æ:¼Õ¾áÜa†;Wö®¬ö�±-ܵ¯/_·‹oh?+\ÂÔ=j³ÚWÄV%k6Á]W9.zµµÔù.]¥õU—]Œ (ë¯�Š‹Žc–J›!Ùx€�õ`è[ŽFf#;'ã ½7„aᆵ>,�ý|¾$VBZ˪ËÄ€:¯´�5ȹ;FŽ9w”‚"ïBà ‰ß;]»­NGø“Ø{¶˜¨¾Éð*0מ㮔 ûY¶¥MøþøÔÉPy¶ô)LÞ†)qž¾ärϕŠÛã‹1ñ?“Nmaiì”ÊÞÚ˜0p=ÇgVDj¿ìê�4ƒ–±12– ›Qu뾪ÚY Ú1Ýêî½Oa¿DF!â6L>'·Î‰šK›™E­°ÞÖÃÁé…Ÿcn k}9î“@¬½š‹Xü�¹,0FÂo}ÀòWZVmDصG—NȹcÙ¹½×CÇ?ç*¡Û PhböYïP®‘h#�òúTV7{Ъ¢V¥Ç¡¦/­¢gÓðiU¡ò�s¯ÎÛ¯UÝÅý¿]ó5üšþÿ%‚×õßÉ k^ÕÙóu0óŸךIÕ@éFÙ—q|O6>&SüpÁvgû^|o\×Õô_}Po‚ 2�¶—ÙO#šS{­èh~n»cMÖpæÛê‡!£Æú½™Û^åŽJå¦iÇí1±`¹­G¾Ø¡Ž,ƒ%ÃÓ±ýªÉ<óùzÕ Ûb¿ƒ1½«]�é_,tœÔhPkiLe6׋ˆOJM}O™„dXA€h~ñ­ý5I¥cÊp8Ž.• ®”¼Jõ…WêWs}Q¯›°žÂÉ׺É3×MzP°oíä¨ÒIeá¿–N)�ÿ.›˜wct(8Jct(Ó#cœ/Þ¡à2­a_>ïh�iÔŠk_Ÿ`¨m‚€œŸó×î³z¹ôzqruþ ;òÔÅÝå™\šVÆö�;;䜆¼¡6¶«O&Ø#sf]øÌüxñŸF‰÷[   Ÿl5¿\m—xœí]m�ܶþ®_¡Þ‡Ænöîl£ŠËù¿0j;…í|’ àIÜ]6’¸¥{ɯïÌ�”(-õºÚk["g†ä<ÏÌPäæò/¯¿õùǽ ·Eš\—ö?œÅðŸ”,Œ¶,W¼x~RëÓœœÃóB ¿ŠËœBfá{Vly ŽX~¿ãú±º<×í‚Ës#ïZÆ÷(ýéUÌ VÜ︺‡Å@ûà’…KùóÛüŒ]«“«Ës†Ÿ]¹�¡Ç3xº £„)õüdûM¸VðÏ)üµ€>‘Œkë/ÂË–_½¼<Çÿ„ÏÃËë+’q}>¢W/ô«Ç—çÔñò|‡Â¯Â×J&eÁÖ”<”ëìyÂSž ÿÎêÁ±d0Ù†úD2ϹÚÉŒžXO×iKi I7…}4‹~&xœíYßkÛ0~÷_qÍÃØ ­i¡0:ǰuÝkÇèËûb dÉ•Î鲿~')?ÜÖccÍ’<NN§ï~èôñád'Ÿïn…š•'Ùú�¢äGƒ$ ¨…uHÓIGó³÷“”í$Ia^vV�4¾¡ _„‚»£ÑeiôJ²t…63åÒc_ä¥ AËÝ5ü „½“L€ N'kçs|˜äY*<ÚeÞ³²ÿ%[(”pn:©¯`îøsÆ?‰·¦Üæ} ÙBØüþK–ú'L!›åc–ÃÛ°ô1®œFÇOñ×», 0YÚúP9Ü>tBIZÂÜØm=ÂZ±tçIt+åb�Sãή&qgÌçyøð7áA: Ù»“ ¡P˜y°¸¥&ñ#ù z¦Ó'Økh¡K°H�Õ(SÉ‚�%T�µq˜*l8¦ ³ F¶Ã5Èc�CB…±]kt)uµÝÉéö“IBØ~:1ÔçcóÒ“Hs¡Ü&”á(öQ:\·Û÷¶_ç ä¦s3î•üéókZ>´™ÂSÎZ5�Úñ<¢ó} …Љß:ÌÓ¶ž‚Ñè—7IX6Ù0 ¸@»„Rr±ŽâÔï”6ÙZ‚Û¤ å�2e³Y¼8¸¯Wùšy«¹YUãP"¡m¤Æf˘d¿4ôYÇ_´¥×¾»M uÛŸEÕÅsóår÷�Ï+Òƒ(88,° Ãõ³ Õj\òÜÅ… pdý@Ĺ²Øò�poÚÜ¢è³Â�cà ¬,wô.[Ê·mˆ5*b� _ÏãÀ¬QáÞXãMEŽ‹8Ø¿²(üˆQÍãé¯_¸N>ÀFydä‘Wó òí€Gèðý±å�‘7FÞØ-oèAñ¡w >ôáŇޟø89.å1ôÖTßœŽÌñ�Ì‘®þªIãßA¿1GÒOK>xm\%’éfó:á«ðv+ÀM‹œe*aË$*º#–a‡†“­B™ñÀ¾RÀ,‡g9-¿áù} °HÁJ­°«Èë$A‘)Ð;À –�Ž ÓLqj߆O È^;Iv¶a…b^ð<ˆ˜ è4‡&QÜûf·1/Ö�Àg¾¯Œ¨ ´)5�àpY¢$Ú¸�§¹ˆ ÄÖz#¦ g°¬è‡å0}9‡Y��Ôõšç¨æÙßO·²„ÉA_÷ÆÁx [in9‚@zdÌò×M<Ðdk†©øÃá€zâ=’.AC,QvÑD9—'¬_ú‰b�~ȸ!úr¨u—ûì0‹j‰_7<¯‹\¶.3–€SóxhÀíý‹@/&k#e"[*¯�D¢ ›ìx(Æ>ÉØõ -·˜.A¸ÎeÚ ÌTõ˜ÊßÁ>�ÄßAMàñwP™rûþ…¿‚¿;¹£œMMï`� ×_ˆÃ=„q ‰ŸN&ñ[QlÑ�*/ ÂŒoÐ…†–¦œyW†^ÌêFÊDV£Gqz+i™b—@q¸ Ë“]ÅògEófíN¯ïO«ú­5ø].ã2Bª+n¹A®ŸÛÑJÇCTÐOi+K§š–,Ÿ¶y »¡N ÿÊCt”¹8냅Á¤¸t“âJÐULŠ+ÁpapH\éÌÀ:Áš.ƒÖt\ß³"wÇjz8RÕHÔ”ÆÛ Îò¦/‹¼nà}&JHÊ4S{Gµ£SÌ—·ª�PVŠ&¸zC΋–œ¶-#}=�ç"æ>g7¯ööJÎ!Ñ)áë"DAj9Ÿ7†=”ÓŸý<)<á°ƒzÈ&öm7é TAçöáªJ“³� r}ÂF¥Mæm¤œ˜7H)pÓ@“ù¶}šÉÑ8‘�FÝ¿„È ‘2öÆGQÆDªøÈS&²{ £o &n×b”ÄëyÙdŒ©‹Ìç„N�¢äye0Y­m ж÷š‘ŒªÕæDVdBж™3®; ®¸…ò8—,†:¸GzQ€jü›q²q(a�_ñ·NÐLÁb¿‰ûT² ØÀ{¡ ß+Çc[B‡e&ŠNÁ‘,‘çDÖŠÒ@†�@„û0†Å­{½ŸãFd°à7žK°ÇrL]߯lî�n· ÑÂ�ŸR¨-HÖF‘÷j{´ú3,·yz±ß¿ce!n8´Övëy¬Øž\�>½<çéN¨\¯y®ü²«u¬â ;^†Ÿ=´ ¢•2ÁÔðdmèƶK@!ðXAáæOO›Y.¤µPâgD=që¢ipo÷Œfø<7b†èâ£ú�tC2þ?tó,žÁ7`ñ¾ùó�“sñœïó�íh°æÎËY…³Ë69‡ È’{#†æCƒ4nqw“‘æ&Å0�îìC…Úë6â†û‰#$Ži¼,@iO}“.Uà¤s+œw�¢ÆGØñ{«ˆ9iË”òG7U{õ†ÛÕ:Ï$¤Ÿ¬€ÕkóˆdPì,j ¤Rl¢õ:…ß}m9ª †äϼ؋°³B?¥^�R‘R·èL“Œvû­¼ºÉ ÐÛ%Øu#Qc¦«MI@™!€@dÖ®Öóue’8…0‰ Óe°EÊßfë ŠŒè FEXOh¡Ê¡g’ŒFšÿ¨r0(P¼Æ"anÙ�1 WþÈÃÕÈL%¨k0™�ê5q-ö' ˜ÐtL»]s*§uÓŽ/>?d‰ø•‡ï_|~÷âå 3$˜c¾á ýd[ñ}Ä›#™»8,=iøMýT§J§uå­_˜ý8ýyï[£ì…ŠŒ®g“ WÓ– ”±Ú×K¹ÁcV›R5iKƼL¨ZÄ…àwä+ƒT–÷PY¾•å3©ì£Øl§sÙùR\–Ï岎¤'°ÕíCŠ<—”˜h˜4üèbôÈÈ=Ÿ¢~ö}ˆ²ØjÓéË: zøt"�=|:ŸNƒn>�F§Á>Ÿ.D…mÚ ºyï;ÜØqw(+ß§QV™qµ³Pœ{™hú|çý�çþ� x’Qaýuº?íXDkëÀšNBŽßv­µ‚J¿ºFÅ‚Å5Ë"®3@÷´e½ëeú›�9X¹»¢¶�â‹Ýª“ ćËuRMÍTw”j–5v��–lx"”„ATÖ(´Þ”mÀ-˘_P¶nå+˜#©À¹ ¹áäeÐMæÑ]*BÐ�@-2·#þ2Ñ–àN«°ÈV±ÃïXT$ƒ‡ zÂäRQrn�l|ÑÈ›s™OSâàŸ4üaîðONXžúI£Ú¶8ä“FãTÏ2Ÿ4"¿|ÒøÝ}Òˆd"3kÐ /gtpÅG—+LïŠ)^2Ű|'RQx c ?MÓù6‹rÂÐHK®ÂWtR|œgÃÝD³5x~ãbÂç“Ú^2vÂäÂ[µü‹¶xwÕ ÌøìE= Î7„J0bª-z™ùÖlÎú€¡ëÉó3z!©ôp¦ ­=M--]swö‡À?Þ=Q!ÌéS@Ù:XªZÌky¬e’È[:ë‹7«JÙ˜¹ÇÂ’Ïïv·ˆ$ë�wÊ™šçV/Œ9%^KÄULµñÈ9„úÚBj†·Å ‚pf<òQmk‘ƒ¢Fcž$ð´’ÙÓT?jyŽæöœ&¦ˆþã$æÝl&­TÄö©€”¾áúo²xY.�¦ÕüýC?‹‚LÎrô”ìÜ«NŒO§Qkçø™©hîé“'•I�µ©J²ìȶØ`•d}VÚ×0hµ$èh_Snúó±}RH(á4×ûþ¨~ºM· }†Ì|Ì *2—·ô 7c¾ãð/<ª€¢8Ö›ÕxëYvóHÄwÿþðØ�n‘kÇ×l6Dñã×Îï_°f<kÁ•ŽƒfããQÌ×Ìì¶Â?¦k‹HÄµç ®rпÌê¹Æ�g—– X³,´®‰å_P—ë°€L¿TúP…{Þ¦â51©Ow¸«Y`ú”§�Öû�¹NR{¢Û3Øë¹!Äš:½%£ ::$·t(€ªñ¸‘?8nñFW({bí¦ŸcnÏõ‡:˜í`5ndOÜÛKEÙÙIu¡�â—ðÅ"|>%½þì†Ý{`Ól)±ñÿ<8ªMWfþöúíû®p ^ŠÖ§R�=k"añ€a Ó¬(†Ê„)÷µ?ã¶XÊ‹­ŒC g"Y¬ì~@µÉLÑ4¸.³ž+j”cÚ&ŒrF��§0ô�x ¥¹˜§˜ìvÚ–'»°ÑÔ¦(BdúSö(;Ué=Ú�§8è'wm©ïî9,ÔxdýÇWTpt=Ôñ+�_àͯð¯à4‰Ø}k•±l�°Í¾Šú:¹³�6ãF!Šl]P±><¹ÍfÀ)*ÓDé7S@õª±„¶ÿÀ'xpMRt ÀFëš²ÉÊZ‹EÎ#ýâÀø9ZåxLƒ�2ÑrÞÖò„âÓî—Œ< w1Þj8æ©ðnÕÂã)ï®(õ]ÜÃB-~ú¥:r0Òçk=öó/Áó‡Ü‡Ð£ižÀ6üäaÄá^Æ�~òKË.«øA”Tk«7”Ž´¼O›ʳ£¬¤_ÉÔåëŽ*vʆf#Ë…’Ù{¢ö¯šÎ¬ù¾#Ú|'ð�ÈD ñEŸØÀ:lÒ‡æQQ…Ì_>¤xÄzâ‰Ûj^ÎØOô›Ù!Åv_:ªtÉ]:Ž èY2rô¨ZŽO[ çäçìΛŸ³»ùù9ö]Eë¢ùùXÅËççS†¼l~>RóƒççÖ®£æçm%ÇÊÏ›“|¤ü|Œ’£åç�3yx~ÎŸ£ùGÈÏ÷Åúòs§ÕÌüÜOô›ƒòócD•.¹ÇÈÏ{ô,�Ÿ÷éÒùy¥ëaòó~u”Ÿ?D@ðé9r~~âìÈÏ€Ií�Ž“Ÿ�âÓVÔJgq|ŒJ/¦ê�.”Lï)|:ðÕeŒxã/x¬Ë[d‰?“%oY×—g`¾ ž+îº{QHÖuÝlBTûSãúCà^bhÝlJX¾qŽJ9¿U`oÝàé+÷¬R /è]þh½¦wNjÏXÚ¿S8q,ô™Ûœd‹ýã øX¤|o|Íá…mc«³ÅúHTÐ:¿«ùaïL”á%3§–ŽªstîI%Ó”´àUÓýx’ªç5ž±ê~{lÜ~=pâ)â"ñfJð|6¬uçc¡Ú/ýwj4î8˜V)¤Là†&j¢÷í> {2„èjpÓàÜ>áøç…3-�÷*#¾˜ hÓûXˆî?Ò¾óÁÚkÈ…–�6Yyl·M߇65tá¾Íô¯”Ó©�Bð•9–¬ïÁg˜GætH˜ÝâOmàÕ#<=DžQ� º‡ÝGí©ƒþüÁŸýøóMÞáð| öeY¸ðÔÝÿ(ùÃèowPÈõ@ y×<ú-®þ_p~¼ÁÜéU«Ö8Wÿ{¦Œ¬Ü;([[}mt,{jmëî{Ñh÷k¬3ùò“³=—|2Išù=cé“t:Ͱ:ì{d¹?H0F S¡T5ug^h�›ÿ3Ú¹þÿ¯ý aø ëéë£s7Çjz[xœí]m�Û6þÎ_Áˇ¼Þ,\�Ãf³@²mÑ M\S´AÑ\™^«‘%U”v×ýõ7Ã7‘õb¯í$ÅõÖÉáÌpæy†¤zþ¯oß]~øøþ;º¬WÙ97ÿâlÿZñšÑdÉ*Áë—šzqòŸ§ð{�Ö¿˜7«Ó"§¿U=£ß§YÍ«eùœþÂkú®äê¹8?U Èù©îøª˜¯q˜gsV³z]rqF7è’sFs¶â/˜vOWìîÓƒ‹óS†]?¿ð~‡6Ïáç’&âåƒå7t!à夘·39£ç7¬ºx}~Šÿ¢/éùÕ…j|uAËg¯Ô³™zóGõדóSÙÏùi¹·1ô_ót5>&üsÖݱÿp×]¦ùœß©_þ–çá’gYZ¾ƾ[�‘ê%§«®¹¨)ÏøŠçµ iNYkYVUlý”¨vóôÆH½'ß+Ê€U¹Cßc‘fžä­¦�¢šöf… ×b²çOHˆfü>†š˜Y³Ï¡Ý&F»àKLŒÖw:2Z Ý=hzˆóêãÜÝî8ÆöÆÎÐ^P–y³ߨŠ9ÑühÑø=KN=±Ï­Ü¹&· ,g ·Ì¦Ïœµö  ´Tº&f©}ˆUG.«Ý—Ì–ÀúPïØ¬ÕLúÞ—Ú¼Qq)Úf€ò0N%#X–¯Âð§‘œ*V3—&U£eC¤ÖË TÿÎ8Àå>AšTcGÕµ ýÇ�ZÐýÎ #E¿ŽÕ,©¹WãRáÀQ&aÄû¨†w)M¤0’ÐhÍÆeY­„R~ SÎ&§› f \ª–À¤Ïi"¹ð�+¤ü^N}®FLiÀ±:ZÎ"§5˜Ê³GBwåû‘Äk„ÇÔ NRAÃØ™CLDвLzmÆ \m=@XhÛ W½DeÙ?j¥#Ì"ea1)Hø>‚Û©˜©¶'jÑÉ ˆ� jI!á÷Ó†�ш C˜çÃ'º<À{»÷‹g¨…j�ýéÜ`Ö ÕR¦ó�ÆÌLƘõaãu}x±r1¾ÿ¬�ïÕàî�a�6C1GæÑC`{¥Õýp{<>±‡ò•%¼Ìé9§»î#ó×)òîËÜøØö”h_y ½‰Y‚�V(¬X’)—Ln‹š‚`Az°5ShOÓ®Q|*źæ&ï®~¿Flö%N a¡˜j”½vfÊLŠÄÄ“™ò3ß2èÍ™C–\nH•QªZ•T$L…„Ñk¸VQ…p0Š*\žâ¨ÊÖs£UëùÆ!”‘R٠Ȇ8¶ YÇn(<•é–Ó2šmÌ6—XÀ�ÂS-Û´ÿ¨¡Á®I «Å†äb*o5 Þ!ºº}ñ0Ûµ/qì H+ÔRb ‘~ônÅ;$'ûtV‹+µ9ö`´Ö~Oð,ÆlÚÈíZÁ!솚FnÃõáÌV•­óÚhÕ:«¡Xåt�Õ�ˆw5ÿë‹´7µm‰ÒŸNû!‡$t•°»b8€\×uÓ$ñ£uFÂi6ý´“Ì¥»&¾U‹ `[œ¸¸�c"\v«çþ] §¢¾ÚÝu2œEQ<¦›ÿu t;’ÿ‘- ÂöæeÍÒÓr¶zŒ©fᢠLÃ-¶ º°_r€4èý³6ä÷‹CiÀ‚:t´lWŽâDÓãì±ÀšôÀ´)@am,0…LÙ,�]ãj¸æS)`Ù‚ $°Í¸AEˆ :ÞbR@u£�üýQ䬶íëbÖ‚<ô€©‘l¦ m�ýNÃV6塨Ì?û+£a­êå†p*fJÁõÁ{©ºò´{©]0lï¥öÀ´¹—ÚÖÆTÜ‚;Ñ“¾ãhcä'œKý4_à)IæM¾»©›1�"ìš¡Zl€ˆÏl1a<*™ßë£ »ê—,L¶-Û8*9Ê‚þŒ„¶tj„ÇTÉxT2‡˜hÚ.[0ARÜfÜ@ÖFѮNJþù<*)¤ñ¨$\î#ïk!'í• ©áñkÁ¤0ð]©cBB©PÅT{„`w6?Ïlr ºKTp&![½X…ÒÊçGÐ"±yEh*9æ™îŒ,w9]áFØ dµÜ9®V ²î:aðuëÃhO¡$uÁ=)Xo{ÚñÒEˆ›¬ »á\}öPQéÂ)¢*fALø!äÓÊ¡ì}Ø‚\'`�í`(pÉ#X‹ûú=:øV¨—*ËÏpÅAžÜ~Gž½8ûñSÁ8ÀÁyB�JѺ8»$ª>šCÚ-ýpöì²4UX ß-ÏξÿtM± K0­šýó 44\‰,q4ýÕäÓ-’0¿{yñ¼ô<Ê)y~Š%2 S}“ÍUyo`é0Ÿ\í /vbŠ��–-�Ý"¤$b |b*€Ì)®2w• ž{( [˜Þ˜Ðƒnð÷Œ©vC2•Ç©C¬j6 [½>ië©�&»ÿæ‹&q†¯û»z;„bîÉ¢üŒkžél¤º\ª2NÇß�’¡·¼%ó§û:릚KVMXxnÓÙvžcL=Ú¸õâA´½eÎ c ú?¸î}[ÅnÍ·ÛÜ“å·bÎöV™ý|X�Ÿ‹7Ë-jO>º'ÿå˜”èæ Öê¥$ª²Ø¨%ô0QÀ !0�Ï'(Èðw:ù­®¢4\2•͇Ó󵂠ú24K&ŒJý[´ŠóY�'/ gÙ8×㼨wælõx‡´3OÏ"CŒ™V8¶0§«ZªaµIÝœüè©>aøö8 3e°k´ò1,š>âÔSän’~VÚ$w.«ÿê‡~¹I©r:~š�¤ÓR¿24hWÖU ææèUßYû�z(XÖtq‰–GÅcŒ»Fª*\.e3 ^yÍ’¢5¿JŸÅ]RÚ ËTêl&»‹o§YÃ^–ÐÓm–Œâ¾,æo:±*¦ŸÆÐÜàãÃü�mmTÑpÝMÃú�·8áKºôñ±g£Vù³NS›]oUdñN ÞÃm˜Ò^µR\VR¹µÚXQ-À,$ËÝ1×Rðh¬NmT’³×_˜æXNGÚ3‰éH{NSOGγwŸžë¬þr];€õºH+ðƒ¼ÈO(/ã5¨Øöf\`[{÷-†ˆ ¤•Š’'é"åszµzp%r²j`WìSÉù5øÊ Ǩù5¯¨H8–cº[›K�«@–©‘¼Y]Á`냭º”àèfV\mr–eÚWWlÀµ�ó9 «”÷&_˜ùc°T?ž¸¿b+–‚‰ƒÁÁ�ÓœUkBoXÖpáLým*ÐZ­€�Ug?³Ÿu_OdgyQëAˆK¾–¿²²„¥‚ƒ, r«IëaÝx?§ †*½NÑ­ä²Ra¦›p|¾¦Ež­e‡ :Jƒukט]Ew çsè½oNIêœô)æókž¥Ÿ¸V£€\¤§ñ|¹€ò‚è'�´þÞg,‘£=jý²–…§‰éºUª#ª1»…uFh‰½Ì)r�ê;óJR¬JV¡Ð06ÿ»~⼌ªµ³Î&ÄÿØÚ ?È( lÑÁe':†&E^³4Oµkã4á6à÷õ"±÷—…™Ó)ä�( Hó8 €ß·G²ñžQÀ„1ŠzäGbAk0$º 0’mŒØ—¤�>3 heØ ±÷aÀçƒÎêû„‘µ÷5Á´X (Kn Tã0%Þ7ñÇ{Ý$Õï`” 2´<äŒö=¶q×ÙØºÔ‚¼}¿w .ÕªEþ–×ËbHpùvÿ°®Ï6½°Ëq»Fr(D|ÉGb$#ã˜ÌGNm$™ b§(‚I&'¬à'¼#ÈÔ�^/H ǵc�©iC¢Ë\å3 PX ;QÑ^·™A²OÑ,,GÆ?Cn׋JYÄ&ÙÇs¾``Ž'` ›’uϼ4m÷ Ä™€Hêu¡ò¹Tx“h˜õ0«_4°n)äLJ×õ ºH›5;XE‘s“^E–·ÒK%æŽMqíxòžim4XûÏÒ «Ø¦.Ú)* ¤zm*ÏÔtDw.¢ˆQ9 Fjžrq|èÔWñ)Úp…�ïZãö 'ߊ´F1FKl¥Ûž¢6vŒ ã:ñÞ€9hNW²½Ö àçU‹ d¼q‰ÃDèøÂ}ì­¢†ùZÅ!Ö=~õÄYW�ð�G~ÌÙ 5»}�áQ¤mh¿ I1²F n¢â °®…ßè ø�ê™’L£´#�Ѿl ¤áN SwÈñµ­Öª¥p £äêÔN-ù2º‘–ßxöA±t ÒuÏ œõÕä Û8J /�¡Ý¦‚ïÐ<ÃŒ%ÕëQÎ4Ú`Ú�¬F-;¡íå­=´5B݇yëF4c›3l" K«+2²zoª:F‘Ë&¶#D6Ú÷û� —à ʦ¸ZÐ5ð5æ%üè?>eïm‰X 6/Ô±IŸU}ÀÂ�ÚPz—ÔHëù]­6ÙÎèÛW~zõZTÌE‹Jg@QÌÁ?ÌÆ¢Ü'zfèM�õõ-ƒ09ŸÐÏDÜš<ý»á1Ú¦žlMÚLó]ïBõõ«-*xý“ïA+’¤©*<�ûæöyd‡;°Ú;—«Y÷;d}w¯‚¿_{а—óý*_rê¡;à{jàž 0—ß5Þà*’±{šÖM‚ø1I�!"ñå½f’·ÓÝá"ñåÛVé€%@_ÂW ‰€Õ~U[jÜ=‚ÙÍ΢fW4nÀ°ì/ÙI#̨]“Ïi Ø®/œÜÓz}òËíG~—â~VôT܈ â´ãÊÝ@•Ä3ÄŒ¦OAHy™»¸ÅK\À&E 6´‰=¬4 ¤]$ëŽïOÚñ—éõr²­?|,©LÍmb+§Ý‹é˜�U×�.d×m±]˜˜Æ]õX3˜Ú™ç]¸É]qufK* ‘–,D©CÂDNëµâ×$6¦Á™ÚÏ+ÏäÁJ…žµ¦SÊÅ%»�rm©@{1ÉC�oïµ`ñF6µS^X4¹¤+ñwS¼¶[EëÚºSš—M;õÃÿ|Ãåi5Àãb ˜õ‘¹¬,�»¥êœåóçàv˜A&¢ì¡ljdc»[ݶú¶=ëò‘ÃÿʊߤEcfŠaB7+& `z¶ÝÓ…ž·ChC�a¹,Ä�-ü]˜ÒÕuĶõ+¿õk¿õk¿5Ö*±*/zöú&ßµ57<Ü]¨³Û�ÿ[•�èË"ÍÍI¹ß�8í{Ñcy¢L¶:ö%RRµö{{c3§×{»«¥’“³ƒ¥»–wÔ½£ái;Ö’ñük–¾øÝþäô½ïî¥,¬G"R÷˜�r¦‹¯0¶H‘lÁW.1ª3LC¦ Øb“ò¶Ç¦´¬ˆY‰¯«ŽrlïrÂøy+Šß·ŠÌ7BЦ–ðÅïÓ=õßíá£ctÜæ’‡3ç/]#´ cSxŸuí ¦_ Ð ¿ã¬‚E…PQ]*!jÓÑn4˜]*�õO\h,-»²ÈæK (š•ÄL¦–�#ù¥t|¤×ƒ¹zÓ/KVrë {ªè]?ïL8•ôΨÀk ˜øãÙ™r@OECWÜ!µ½6]ipÀ(ëŽvB¾uvBÚ‹kÄ¡¼~ŒW] -3ô¥wCúH„ G<)L$U±Âv ¼ƒÉ��^�°\ýN”è†í£ÍïúÚÞ 64 ?ÚØ2€¡’eZ>Š.®2å ÇÝUš4W°˜À5PsWGxæþß:/V)ËlÊæi¹VNdòªnº–Df 1yU`�QU –é�ÜÔŒH,…‰K¬äôº�Õ ÐåùPê ¿#=9Ò™�+8FOÁxæÌ{®Šæz á]RÝ W®6\CãPRSÉRR»ÌäÉf<|»VÛº5UIAGÉä²Tæ%‘ιZ‘˯íÅR…˜mÅ6«á§ûd“çøµMÔckÐk£�:«zBÝ�4¾ÔöÊíwKÆÆÄyË£7ª…3?½?�Ö†›Ë³&¹¨¥{hÕÓj'PÄoòX“;d³WF¡¤{…ÇsâÉÔÁÞÓPkˆ°ÃðÁˆ³üÛN‹«¿ }ÍœóÕ&•Ä(òÀ£ÝõrT§Ñb•ª:©¯ÉáËêø€vIíx~Ø`o=ܺ#ÝL $vUY—˜¨­`F;±;ö¾›k�xê›®½Áâ·èwTìÃM —¨zèÙ‰ Š=°ãíÚxËmÇ£¿˜¶Ý8¶ïw¹Ñ¾_ nÿÍ7^Sû.:FÕÛbk~é¯YìuŠOÄ6” ¿¯0 íÝŒ:ñKKúP—ûš<À弦ªÏΞ.WYÕ•¤È©/µKžÉ€žÇšw�fƒ—�L©x½|Í£`mñ{{þïÀ"d»µüÂ\® ·íí5dÝ% `yüRæÍ`ë¬R ž@:M…ÛX£=AÒy¢(äp5Yîã·v"έrãÈf2s:”X=Ùs€Ú…¯ Òš)èÒE4alè{Ñ01´££Ò»³i+« ñG‹Eï éSƒ`€×ëö¸–½.Šg™í5Q÷"Å¥c�±=r¦+k<ªI‡ßİ_»¨Ý¯…y{Æv/ÕÙ˜2ÛKÑííø‰6x4šJ¿²§8ý}ë™Üíîäû×Φ$jÑʯ>_ÇôÞud+iƒí¨|Ç»IñO,8™Ì±ðÔ­œ õðVÎÌœþÝl/GŸ@öoLn:ƒvC†„_w˜4‡é:‚×X®Œ~A=ÚOÙöûES‡Ù –š<öA‘ÔŸáq“)X«3»AMzô#f:b¦c¦ žwDL 1MRÕ/m‡—þt<üý.µü ïZU �¢çZ6CÎ�Q<3 g6Gdá‘»¢ê�#ðä>hD6ß;™0ʾ°È¤¡¿„¢Ž+ë 2áwIÖðÁ“¢Ú)6�á�ÐäMMFïëE&$÷ÞÈdTSG`²»BŽÑ÷±Šó5VqÀaмçF!nên‰™tëýB¦Iƒì1MùKLލƒxI¾·; ¤º;â¤#N:,Nõ»#LR0i‚¢Ž(iw(I«û’¾�tªÿ£Î§ê¿!ý?3T ¤‰¤ƒb9‡Háxœ­”OkÜ0ÅïþSB »Ñ6%P­ ¤9§‡^zÔÚ³+�-¹ÒØe÷Ówä?qÖiJ a齧ߌ„仯wß|»Cu¥29 ¨Kj$ …Ñ!"mó–öëϹày²T¡*Û Ézd0H1ÌfRŒî�/�)ë£*5i:6o`iâÕLjpºÆm>-^bÝÐ1WRèp¥ÎØuÅó •Žq››kØGV¾œ©n@v:¨{)Ò[�»g1;ï?HÑû¤hþ[f¯Š§AöÚü­ù—þi£zP­�›þ. V•mnû+¸ ¨ A;è“æ#Ñ!èãe6KÛM¨u\_çƒwÀ\À�“Í-…€ÔAƒÄú1M“ÉÕæ‚l�ñv#ÖjA’�(çm™6ø›²Á‡ìµ˜å1ÍÐîø=ðÕÍX÷³ÅHXB´'\Á/c u Œî¸¿òÙ‚w%ëb a—®€<œ0x®ðKÕ¹â`;L=‹¬ 8¦GÿBŸÁmbÎCð!ÑTø‚eóÔòÆÉ$B¾¦svý+¦txàðîi…o1Lìkd‘+S9\tXP¢³d2pžŒu°,õO@ä¹A˜½ýò×ïñJ•Å MÝJrøSREp¶"BRusѨÅÕ/ø\1UÐYÞ¢XÅñ!Ȫ©}–Ó‰}M'­¡y•o´Ù³œ(¢65•¯ñŽùð"šÌIIo.Ü{Ï3¢.fÓ Ñ–^ÎÂ�aÆKø´ÆYA¤¼¹X½Â ÿ]Á£ž“Uy‡÷5ž®‰˜½�Nô|ƒ§ó™±1Ÿá§f(g¥¼´¯¾‰žnÝÓ“- V_?›NŒƒé¤Ö fømÅÁ åð¯[%Ñ«”Ï‘}+gk¶”W¯.ìD ´L�† g‰úˆF™Â¤¨øÊ¥žãyŽñ›¢@˜ñºQÎnÙH…WdM±ZQ,a/±dÓûŒÖªµ¨‡*»ó¡}”pÀ7¸Z˜ ±ÈRÈ ÏÁ¾ÚL;R œQ Ë=/L2“LQ�×4S•°Ve þç…¨}å9Ó!"E±¹Ä„[�¶‚k7�™MIËÀ9x¼¿Äw+–­àÿ”ãZÓ%Ǫ2P!ôªÑ¾ð+ ,¨j¬�ôö!¤Í• $KŠ«J< p¢:™ÞŽçÆ¡\ø½ì¡âŠAêUÃzOçF‹o6G&ÍÖж&å ° ›¤zººõ®¾ŽqñÍù <×àÚ¥‘Ð8;†‡–h.%š¤"ïXæY…Ž UŠUèZm± M«-V¡#hµ¦B Ъ:™VÞÎÑ´ú,°ìqHÕ¢ûŽ\\§IµnþƒPjÁ¼1 ŒýϨˆQ‚ÖešPv$ɧ݆¹@Ï—ðLd[…†k ÅÑ«“ŸÓ%ÐnMûç^{�1Ç—�–XÊÔΆàÑÖvÄÀ3òd�;ŽyL™m9¨ÚŸ´ù­+õ=óu|þž£.4ÀÆ€ú:Äå˜à)hŠ[1Q«‹ÙË'FÅ®!×i9k[*Ëi“tp˜á÷>„ÄeìÚCe”“‹Ï~=.³\V0NMå–Ó{½L×DúU2s@™VÒEòe+ci¹  Wói—Â�Þ�º�ûs_®HMÓ©o†N¨ŠÚù�щ� ðhÌ|uUäà%Î Ä߆Ý� ™|–[Cgj�úßK$Ž_ßô„�Ð�?;ÔBPɰ–�ÖE‘¿)‡,/š’ß1]ˆªL\$¡þa¨‰V‹*×êx4òl„.†Ƈª)PP# }”܉22¨~öoCŃV7¼Û«µºl…c?k [5ª©´wvXj­iQqƒ u‹¿u½•ϼ†8Ëa”3‹r&ÇÆ|Ÿ^�ʽ!–¹ˆ‚d<,·¯Q ‰óÍö·ïtÊÁ6ë>?TÌñÝ#²1Þ§ƒ5e£’õb;tr§äí Lr@‹~£œ R°­ù‚€Ðë*b-S B¯Þ-D-¶‘¸|½¤7oÙaC¸ ;,¨þœ/5}¤MHÆþ F 2l›‚ta¼�6$@m:¸,æÍ>œo˜ÇŠv€€·´z¨_³8|Œ¬ÝEÃÍa J‚ûÎ¥àâ­:’éƒk@Ñ"*Á€»¤HÝÖ�“~·céw;ž~‰{½-j�æO’§¨ÇóýßÑÏgEŠ~ðäs*°Ô?©¶÷Å'jÀ(Û¶q uiŸÝÔ¯;‚w ´Õ(Ä?‡¿â3yðäB¾ ;`«²�{•�o³²ÿn·²Çß®síÆ¿‚è÷~—ãn$Âzà,¼æ§MºÇ¶/ößë%Á¤ý}ñÄþ|ù_bƒ ´ BmBŽ(;œTªxœÕYÛŽÛ6}×W°.�¶€lw¤(²^m.EšÈ½¡(h‰¶XH¤BRöºEÿ½‡7Y{×»›<°6-ÏÌœ93TŸ½|ûâú×w¯Haªr™,âFsü©˜¡$+¨ÒÌ\M³ž~;™cÝpS²¥6Š‹ y¡5Œ¼÷ߨÈÉ )¶Lr½¯ÙbîNó`t%ó½=âb™SC žÑÏÉ™¶°)YP"hÅ®&~ÏÌÿ™,sjÍ>]öÖ±ç)–k’•Të«IñŒ¬5Îd=xN[ªì—ÅÜ~ Wd±ZÆí«%ùò«ÅÜ=¿˜×�¶å~çÂÿüÑ g´d"§êe£¨á2“oê‹×RUÔ|NM½Í×?]"4ùÇF�S|5÷EóOˆúEDò¤`eÉëËp8oü/ÿN_FJ‚tliJ•¢ûYâŸÉù6b¬ôôÙÄoóøF¸‚�–Q$sæ5ìk䎪Þ)ÙR3ÂJV1a’Ia(x\VÕf望f†)²e™‘*¢ºÂQ"vñt€¹&¦`D£ð¢ù߈&= +Ï ÂµEik–åÄÈÖL–I•ÛOX³¦Ö²,å ‘µ%‰~ 7V�J¾ìøæ@”bãÚ¢†0Šc•Ü‘ËdÕÁžÄÀ7‚¶'FQ^Z@ ?kš1R+¦™Ú²ü’d E< >·’^�AãP=åÚ``ÑTLñì8\A w\Æ5\�©!Š„÷-µ �´ vûe¼5J�ß¿M–ñÓ‘=¸y 3? /°Ýo_&ËðÁß\èæ˜ÑÆ‡N‚Æ·‡^öŽ\!n�D³Á(Ú3ÖNÇíC£™ófÚçæoÁi‡c}$Âm¬>„Û˜,zÜvfÇí66°­øÍ=ÞcÍœÃðÁii2ò, ×áÕ†kÛY•ÜéÈõ–<ñ=Wxõð0ÙÒ$9/µžîGFQ·~ÿôþÒAƒ‘G%7äý¤ë#l¿;­ÝsÒž3!›ÉÿF¶pæ[«U;®Y:zíeÇ«Hœ9æáÿIæþ¿dþ¸*Õä‘:¢$2Èxœ½›Qo㸀ßý+X÷åØÄh…×À^îÚ[ ¸m/i‹>´4Žy'Q IÙ›üúÎ )YNhÉr¤»&%Rœ�Cr8C)Ë?üøåæîÿüIl]ž­fË:™b’ƒ“"ÙJcÁ}œWnsõ—ùï;å2XYg”~X.üÕl¹O­‹ô‰ÚøÓ*•Nº§ì_E]ïΖRh™Ãǹ¿9_-’øsÓ$fgËR$™´öã|û½ØXüw…—k'E í¶—ëæÁ5¶ÅÅËEIM¬Ä'cä“0P° Ö+Ð XQl¸{2q`ìõÌ?”ª]-9·WßÏC;¡ BrƒÊ ©}þƒØoÁ€™ld�£¬\ @!a(kÖF¢¥Y+g¤yè·­^KKHVf ±•;¹²–Ê‚,K e§j³#6¦È[‚gǽ¤«g0E�)¾q[D¬ÔNfN\Z@^º§ÃSb vèÛá]!@Ëu†U–õº©tâT¡­pDªS,lwÕa…Ú”ø$ö¤²²lV¶1ƒ@¼iZ7}Å¢²@�Ì„Ÿ I¡w`Ü MÔ»â–%Ú0(¡´U)ˆ§¢Â&±èZú0k±ï ó;!ïjç»%“JÇà1îY¼¾ù²ÿÇÍã#þÆLØDfØ›¦«Ç= ]¼+¸Ë/{Ú¤%¶Wè‡Õ-Ï\+‹pk&®Ã­�M½ï=Æ5Ù€?ú[vV–¥%úAô<ˆù©ñ9¿ÁãþÎO©�Œ÷|�KØ”›B;Z"Á¤l¿[½wc@:¸¿ Âuzã[¿¿CëÂó•¯$nÃpéT„J‚*‘dðYFšM”šÕÒ¥ÁT] 2èÿº¶�µ}“ûFÍãÿ€†&öGóéÚWY Êx<©²e35Z1²¿ƒT%C¶)L.�ƒ4 Zºsl1ÉðÔ(ֿኙ¤´r1‹“Èg¢: ƒ{dƒiñ·Ö}Ô\MFìq/e� ;�Xø;«QΣ¾N3O‹ "NQN‡úg"š[Ô«;bFn«<Ç�üþ³öS�ì°·¡D´J†ŒS�7VB=c_é7ÖÓ_ÁUFó.BUŽÚÀéÑ¢S•£aà$†òK•¯qà‘knÈ´á*=O•'1ž»ÂÉL膪ö¨¦dò~×|åÓÕ?¼gÖ'o_ƃÀ¤æh²Ý(¾:RË‘Ë2PRTÚÑÎŽI|¹V´G$Ie×½¤Åè—TãAýO?K‹: ™ø.§Áøx‹UصäÍí$R—ùWÈó¥¤&iq·ÅáöPmö zí$zÇìWq.ºTÀ:r^ëý;Ô­á|¥w²€NíMË:w Öõîú¸0ÖIãÎ!מ 'i²ª�9†«ŽÌ¡lRdU®ç«:×OÒŠœ…f°Yëa⨕�83„ˆ�Ã;6âsÃt3Ô-êƒÚ݆s1˜¿)]{æ/÷@à†÷‘øs FñÙ“,/�8.·ñ=L¦Ø&à {öú˜Ÿh|n�f<˜Â`´Æ0œër,M1&ÿàÈD¨w{ ¢«óÉü¢¡C…ÑéËÈ€!V¯s'ǯÓ;x �Ÿ�DàsÃfPd:w¹*¿BÆ2{í¯ÊÞà´œÐx �è'¥vžQù䕵ö÷ß`©ã ׊�e‰Ú|åÓJXãš¹øl¡S|‚.,PtË™¨ÏÉá¿ wЍZ><ÚqM;#µÍ(ÐtµJ¸:œc6ïÇBÆ›ÌIܽɋpN'çüÂïÂ�~ŽÀítÀ W1ªŸ¨¼�ZûŠ!¦W&¥¯‹J§Ò¨uõÕÑ0 žÏDÉ|‘°Õº^FõKÒ)´æ¥}Ú8 »ÚW]l|<[ó ¹á÷�'¬â·—8Gðgaþ¸cAÃé/Ï]sýw"­'âÑõÙ¬öÝæ$YgSO€ÖE õ3ûãÕfÔÃÁ&©2¨u\´zÐÛW}p‡‘žŽÎ@™ÉÁB&nü¸¨=®—ŸCÓÌ»ãë.¶÷6‚vè&°Þ8gã"¶Ô¿ìhù)�ltð7t‹÷ÇyãÒ áO êÜI>˜¤Äˆ)(‰"PÁ±ßA> d*Wü}Ò˜ú[6Àpþ|" {z¢ýåÔXd[Hçc ÕÑÏ…QÏôæ!£5†•@K¿ëø7TÓMeL|¬¡¢‘FpÉ2�© Ÿˆà¤2ºh-´É\µR¢‡�?1¶OiÚ€yOöº¼SPýV(´›ô{®Û߄ѓU8Ïé7ªŽI†‡Ý2³bO;?']ß„m¢ß¾Cà‡{cQUØW¤âäR*~x\*­0zB,Nc\ÿ汓Y}æ¹+üå/A_G¿þÑ˃ßh³MðIæì„-»i ØÅ‘o§ìmažY~È µ§S0Ñc¦�‰1ý‹Tò^DèÔåäÓNHZ¥òß\êôq�écÊœEòFËy�mˆ¢'1Ö¯0Öª_ —aPHîÝ›âíN»•%û¾œ‰+Ž‹¦Ÿ(“Ø­ÚÐ:¯³QEaa•I“= ®Ã±éÞo'ÞÍ+‡Ê ™Ó_°dêRÊE§d|ŽÝ ýrõã¨XªáR`ŸuˆXø“iñðþœ�˜ùªÉFOþœeè[üîIÖIF éÚ&¿ ®MØ9?tC<4Ðàøï2N~•QÃ\ÿÝAÿö¾d²ð°òs‹0 t ¹t’B=¥A!xœí\[�Û6~ç¯`ç!iO¼ P`‘8Ò¤úÐ6íf±(Š}àH´MTµ"5÷×÷^t%eËòxÑ$@ЙŠä¹_>^’Õo~óþ·wßÑ�Þgk²ò?8KáÇžkF“+ׯn*½¹ýç;k¡3¾Vºù–þRñò@.xÉ´�¹Z-í0Y-™;™�è×ë”i¦W/ht5L#+Fs¶ç¯nì¬g‰Ì5¹ºY¯– I=_÷F`Ýs(h’1¥^Ý쾡œžÈÔKú‚®îY¹~ÿýj‰?é+ºº[7ëïÖôK3fÛ » `Zó2·ß¾Z- ÉÕ²°ƒ?·ð¿úØÂ7CòéÛ\–ü Sü©“Äé²âîSOÈ5}Ï•¦b㌮¨cO�gÄÎLŽWh¯n¿¹±‹-ù¾^ƒ)F£%×U kÍäV$,#”•%;ôL$7Tï8UT‰?9eÊÎxí&8æ@åéF–„r–ì`ZYrUÈ<Å™<ã{žk¤Þ˜1Ý(dX<ÁSP‡²}Øñœ¦¨î­ņô}ÂóTÑ¡w!¼FKØa±À•ÿ�…ÃbáGÎ-Íú«‹©l/\,€½uÄŒjáUøhªEžŸ\¹˜âË¿A¹Pš•:R0š±sKF›ÂU‹ÆtÆ.F€Ù…£Qã£)V¥O´xLóçÕÊGÛ'S ˆ1ìMÜÀ¹¥£^~Õº1‘ë…‹† »õ‰ÿÑ ¯Ï'W(&8r,ÉŒ*1R$B¯B%2«öù°Lø‘`�˜�¹ �aêI<ïsÀ¡÷<Ѳ<’‚!`õÀÖ‹�žMã,cfäûBBV4ó�èḚ̀¡¡p ËuÅì‰TŠ!ƒÙ‘ùótf…�%r�xëòŠŽïïx4œ9·¯7ëkƒ½î·o�7ô ²©«g)ÔÓp£åý{‡¹#.ò”°_þ’çÉŽg™(^žƒZf1¬ÕÍø–%‡ˆÂkú=ô‘T÷<Ý�*®ÏA&^œS\>„$6šÉ¥!‰A$ä$ñÄ Æ m¡#ȃœ=¦!DgÜ`þ¿ ,c@»AV†a Ì€§xk4s¨Ì3ð+ŠL ~“>˜LñIx{ˆîË+Ãln»!Xìü°Æ?ˆPpª�Û*{ÑPŸãÆ]MATàAÂŽI9œÓó1x°ËkÇT@v‚kU? Æ>%�'¥‹Üå Ø<§†˜ëS’ÄÚøÁt)CBä¤ìµ%�e ´î]:H—t³ã+b œ¨,ð^Ì€meíJñt�Krü�‡ÙµZ©¨ªm!7Á„&Ý›�ž5"ưa1µÄˆ!º%º6騢Þ5¡;±Ý5R[»›H[•¼Þœðøþ;ϸ‚íg•iQd¼Þ}ðBéÅQ™0”$Ê707ä¾Yâ ü(ô°Õ/ˆ=ìÐ ðá …k3aœG“íPÈ 0#@Ñ´_G¦ÓÛàîÌ6ì¨F”�õÞE¯ù.lJáÎwXomCëõUè}�ª„¡"ò¢ÒcÈõ”ŽVlÜâ�Ú‰¹ÚUz¾Î¤ã¾“t]ؾgú|)˜ÈmÉŠŠ…=Õ¶}°&s\}eêiÙ¶Z`ªÄ§\Xè`^;°6õ¾nJüêdµŸˆý–ž¬'m T•×'Ó7ka1[§@§�ÎÈÁç”[ðu*¹‡užöAÑ{`$+�~ …uº¿ï©�kÇâ7Vò ƒ„Ê|ž¿A5DfíN±¿]óDdÈ/f7IÁ‚»T;2ßzžÎ,Z"×1^—WÔp²Ô< ÎŒœßaýúi v&iW©S±¿«"ÐÏÕu›.À´€o 286øÑ–ÌwKL©mÝv �ßERãW»Å`eˆåy ±k'à› ½ÖN\Ó�æ�)øã¹ç$ÁMS ·@–)/Çï'Žœ°¢±ÈÝë5Ÿ©8éhÞWð•tK��ç · ýpÛ�G6N0U®e"³ÝjLd�á$j6pôî”6;Ý=th%MXŽð«;ØÈ ¶´Æ=fë¦^8ÓVøð5ÞÀO­›ô®-`¤ç{ÅÀ>žü¡Ð5ôBÔÈt!�o³Hy‡Gœu3Ljíe.µÌErŒœÃº½P%§ð@'š�*uÏÄø™sw„GÓªÈérÄÔýÍQT€óív1¦[»ÉNŒ $[bÜžTNoe$ÜËêLUx[ Böç_ŽmN¾BÅ-��{Q(sÍËV]ð›;/P4e+-�%¬a¯†¢#: æ@«qZÎøe#J¶›®Ûp4fn1vfV`5Šqt1Nêùåâq_éâè\´ki<â�‘wí-‘Ù5Ù=ò=UÇ« ßF˜GFÀö.ÖHCM™„ ©©`xx¸wðFÎ<¿}Û@ƒf�n�ÎxAtÒ>–%ÿøÇ’TvÇ¡¸Î]wua 9ÙkÝqµ‚ÿ8V¦½5ö”�Àv*tv¨ÿúK.óÛ?y)ñVó-/±±IøN�Tºuºˆ÷æ2%÷"û¾Ô:ä¢ï¤ZÜó¦Z›.b5mŸŒ¶ˆô£ÁlÅ2|˦Òji`ÜÜȉmÌc¯¡\BÄCàýÆë5�EsëiÀûHø Þ?ƒ÷�¼¿Îð!¡Éöìκz3k¯êËíÁÍqYuð7Ú˜|ø{³ç®/nñ¦á^¤x¡âî‹Q¤~‰Ðþùd‚ï­°ÌŒÕB.ÙŸuYr½aïd®tÉ­Pr:ñ;’é$HZ¨lCrÏ2n¹ÝT`ž³ÛàÎdÊ™ä%œŸø�§FÜÀÉl:á$él½Åýgø²biÁ�9?Y=e¹Á­©ÊœÏÙôšë™¹™Nè—�³ébæÏ.fì¡_´Ú¯>šNÜÑé¤:Ll&Ê/‡‰µþ7U�·ÏšÇWP¢zqüã£íCùädiµ6[ÂSUÔ¥4} �Ñ_d þsýžûl­%³+`´›©œñ&z¸Ö|sšø½™¸nЗfüôÄ÷Èûîw’üδÓhP šÈ®!µJ³µ°«DÂʺ\€&4P@ ÒšGî�§+†¦‚4Ê ƒÚŠð²mµ üm¬=GïÃ;L-þˆ²Ä=¤Jk0•’�éPš R‘ Ș�=¡§Œ½Ë{ï˜@n’@̈$Ë=ÑI›ò�š —H%—™ç”K{­"_·èZĈê¢.¬¨ è^VòMÂxa[@tŒf âš[@,kç­ˆö¿ êA�(OÕ¶ªm»q¼â#u8@ÉIPÕ5h^Œ¶"¶ãzNÄxžoÚ6'AáiÂ^š+²ž‚'‡5EÙ6&r-÷'}D­ÐĈ‡\™ñ.·š‹bËÛ•"¯´H¹Í”„Cʼ™ŸÐ¡éçqÉ`Š !œ��›±ÿp4Äœ9÷>y�Ô·–”J«8s~G½†H£èáAHþlÄVjM\z&»|ód$e�vÀך½­‘Ñ×ÁXÈÇO°^ì^3w¥Ù½gÜë½ÍP�tÄ!Ô¸V)w^936ßg‚�÷­|1âÎÚ7ÈcSKv+Úëíz6À/º¬ØÃ/½þ~~½”ø½Tƒe·†Åñ~ }}Û5Ñ¿?ÞÆyla#äßû:_—¡R5�æP³ š¦â¹ú¢äs.‰²»¹ †ïëe««ÜÐvaãÚ–¸ZwZP#rÙz%ðÚM¹�€ ðë¸?ݪø3äËvé»C6’t”O-|³dyL¤9:VSUK»kž{}x[Ý3)œlI+n-hyw| �G)mîôï–o¥W7ŽfÉ꺹îw¼ðŠt1•¦µ™ÕDä÷]r¯ íl9†¼¨!¡¨EÊ‹$N®Nz܉¹TÅlÚV„ê¥åBºþ+¾’žÉ»xÈz×owqMoל¹£õ½·¹4ö)n»ñµcM½ˆÈ«È7„ü.ƒo/ W°ù�›=å#,žax ööüqÃë­ùt¸Š@ÿ(S¿‚ßmSµ¦pÈÀõ€÷J�Y€5x-ƒfJ/¢Æn÷Ù“fÝ`fpM ·˜5º>ôÓ€Áë{ì=# ÂZV¼´Úbo}îxž=/„eoçÇ�qòáZpô$ü­Z ÇsUã�ŠI%Ç©ÞTV-5¯ðörD%,¯eJß@ögÆL :ÇÏE?-àÎE5�@}T!È•ç,ÃaÚ¹Ö›x¼X*�­}I|õ„¸}nÁXb‘¡oCIp ù sÒ®P�Ù`IúFã æiÊ3Øïì/Êf,T‹±ÛÁ~� úþ ñ:‹… „îæ¤üMÓ rí7㤆ž´¸û tè@ŒŸˆ�V« ý¨vv à†´ÒÉÁ´�Äí&¤íV“e #Up G)®rjMUk]QŠJ8}Ÿã)$¥.„1Q½õðÝX”u8p¡�÷güÁ"ûìVVU¹9_dúÕ�üêåÅ)¿8Ý —Ù·ÍzYÖR/oÛü±ûBðû¢¼Ó(®»“¯žqF/†ãt>ÙŠñèDÖßʬ—}vÝ6ëLæ«Û¬¬7Ûp»Ù®eÝ[ à/Dv[Œõ¶ë³¥Ìd ´þxÙ Ú«YsML”¯zøîN®ú¦…/šV ÃÓl�¨•ì¾È²+âãÚ™~j³ÞÓ,+)²®ü“š|;™·0þVæ½,²~§õM¥h"+‰`³¼.\t4ÃÆ«¦Ú®kì]5ÅNy5Ñ‘mZÙÉö�Ô�$/+¤P¯öØä~‘mëªü]Ò.]彞Åëm½B©Ð t 4¤CÓÉ¡t¨QñˆŠE ¦½Šë¦Úü¼‘+Í*ôø** OØ_ŠS¯âr9]1ìĨè¢ÿwÂ5C\Û7ÈjŽT oÔ]Ü”Én•oä‰e¹Nþ±•5òýTeᢪðÜk–2þ¬#îC¬ M?_·M–¯VM[à```ø 7îñICÊe­¬E–ÀÀ׆4@†*o‘6¡2X°P¶²ß¶5# µ­z�¥G\Ÿ" “ï<…ƒ$É4¶B¡îo‘r0@¹A1_>ºâ¸�>úë¡ÓƒÕ¨•3¢Pí ›q7NCwŒ÷„¨Ö;0ç-dÓ€…à9b5÷¤oÝ©Óüd˜'è ÔVU‚–ŠÎB&“�CÀY‡½æ½%ç6ï,�ëíz †¶„\ÞùÈñD¡ö6à àGÖmK¦�@·5˜4RÄ×MU¸×jl[ôZªò2ûášz¶�²¾FÎôq—+)‹ÎÿZ¤¸:ÅÔÐ:ñPŠ´Ìç†LHõ˜åU°S$ž/HÈ4Ú}R§Fºßh�Ž•ìp´y=eÈjdø¥%C%¯ûìÌ}ƒÙa?’¯»¼ÚJòX@œAL §ºÆ$…¿‡&2Ο²mô8\'Ø‹8'ïD ­@&ê&>›(þjb4.§È?ǫסQq«4мì›ÕÛM´r †Þ¥T1 ³ëÊ”¥³µ/E]ÀÌ€F—í묳ÌZ›ÖDâ8ãàs�¸©¹%á+Ê"+Q�yÐÓÜÖùx9ù½+m%PNOØwêëÑ Ç¥S]¨¼vLÍÀ:[“Û l®°Fw‘u[IÔNÔìùÿÖÏ�‚fÒOûçFÇkÎ6]ŽÙ+mm bœ…)¾÷qT/9À¤“˜tøC ڧʺǺÏpÈiDÉ1v�Tà±¶yÅ3t!,Q ¢©3E¢¿ýÍN·+‰™îdÛ3+j{(LÓÄà²pXÇ�¨ÝV²ËÑb–WX ͆¥©Açåõµlq`ÌÃQÓ^’vDÅóF•Ÿ’€ ×e�ƒ´úZß"§‘æ-ÚèZŒ{óÐaÌ—§Çs,t w�ë©1ûê6áâþS®›;‰³Ù㪉¨éº(‡.t §éø€`!;=Íjµm[æ[mO�FκW¯öšë�90³ÙÐ ¦Ðõ]• Ì#®+p ¸Óv]Cÿeì›×Ð7/A‰¬òUQ5×0ÒM)Wä'¡$‚ü‚H=:çU)=s:ŽŽ ӯЃôf£ø�š\-e�s˜ÁsãÜ>kðµÀ ÷=~ë’ýmÙuÖ£è†Ú“´œ^]O¦7²¿—²NÈ”z;}‘œ)')Y]Ÿ·ýOyï=”uaí½~>•¦ ¢¼€ž‚’ã΀ÏþØ6ýù›f[`tdÇ¿�¥%¸K|Gî2ûžô‡VKKî+£A²�‚ÿ–üQZJ dÚ‹ô6I= VF&ªGF‹t@gN­·KÐÅ0c_e€Ü"û]Ê�¶¹Šb�.,x¾îduGÁ¶`ÁîRXëÎ0L‹ ºÁC‘Rœ‡ï\¢-ØÝ,Û®÷5¢ˆ”©"åÀ˜ÕNÀp0‚‰¾”7¨£ókĬìϳòZGC å T{×ø›QÔ“Ìj}E×_ˆû»‡ò¡‘o‘p×–2G¹\dINvIzëÁŸu«WÊzUm»òNj¦6�p=‡^„…íóÔ8p³nàÖw¸¼éå @ä¨E!ádðÒÙp3'E�Ç™ø~õ <Ò#ð\¿ÐÒzëÜ=gr?µ(Òz1ë`mZ^cTçþVG:ò\ª¸6ÍMÁktŒÆ]ø·YŒzq…ž·;-ÔÌôÂÃÚL¼‡4J·ûu9øJûE¾7ð]-Ðù°úˆeª�×9-´º еÀ!®~ó|‹s$_Íc¿†OÃn'�]÷ ™`º»4Ta§CkÄÄgúÓ„èèiCµLQX–ŽXaØ/êI:Šær)Ρz‡„˜†¬žÜ—�œìŠ]¨õçqzœ¢ˆÇÈ/Žr5ˆ¤s¶™âîë„íî5áú�wÅm]1áXЄ±ÜÙõN‹é-ÄÕJ1·œ Œè£ê=Òx¦ûZP ±£Û?* ¬ò7HQžž�:¨$¬5 "¶rÞØà¯0ÌÊË1}'«ë´YC®»Ìò‚ÃŽû«¬þ@h§¸¿"æÿîçþþIx!TVÖX.&¬?Ûãõ5÷>:y”«Æ³0 3Î\C6ÆBe¼Ð*wá&süo>?3Ù?ö`ñc˜ËÓêb?µSݱ½»l;:¸>{Lœg÷�ñ�%šÜ×3(p èChðÝ}ï©Â—ðIt8ãúÞ•80ÍJò“ÿ«jñq¾šO�{Œ´¿çŒö·ÝàÌÅôúÉ™�úoÄ>ú_K™§RœÞûYÔùG‘æÜ™÷”èŒ 5ª3f—º§Ëyzcš#ëi—ïó—ñ ^<Í8yq? ÚÔ¤¥²–ÉÝ6˜g0ô3›¼T{÷tz’”c*�§ãx$³&FP‘â[Y‡&0¯zžcŸ�ÜGSÛ¯§GÒ&ÒÄäHÚþ�4±3’¶w MŒDÒö ¤‰t$mæ@ÚŒya'Ðæ8À29üÎ }©ñ:Ûë-–Î\¢h¤b¨»lõ!dÿø’¸Œà¿`¤¦Ï‡Jåêþÿ2ÉÜ™3­e _¥BÉÎËc²ƒP…Øw�o*T!솶CCáR4%£ã¡ ±3V±#T‘>†h>�TÎxœíYMOÛ@½ï¯r)` ©BÆRK[©'ªŠK�{’¬´Þ5»›¨é¯ï¬?'„8%PEŠbl?Ͼ™7/³!ñÑ—›ëÛ_?¾ÂÔç*qs Ìø�“GH§hù«ÁÌ�O?"¾î¥W”8o¥žÀORè¥Ñ¨à¦ [þíâ¨Âˆ8ªc�L¶‘?$zô‹‚Ü%¬ÁX#hÌéjPAÏèn�ĆHçÉý5Æžó¥R…Î] ¦0vü:åSϤ&kø^B5Ý‘ÁÅV:¾TFÕaSÕÛ¦ª‡M9·g� ïÞ ªÃ ª·AUƒÞ+»75ß•5u‡5uokêÖ”z?›'m¼èoÖˆbKv‰X§÷´ØLjƒ*Ê­}תpGPmÃ6 º¾±ª8ËÍÕlóú5Xå…Mvmò-Õ¢¸Ãî¼õ Á93’åR­|Œmq^&¹Ù,)SÑ TfŠRcÛ…f†!aÇÍNþ¡òKƒ%?³š²!¤F{”:¬`ÜÑ3r�‘K­Q82sª¾ƒÍÉ. §|Ä ª¨u©BÍ5Ÿs):z\a´“Y<’31TÎ,Ó2Wñ\Qáx)~ÎqÖhO†MŒ'Omb)¹ÊSr7¦’}]Ç¡¨Ü«Ž´“^Îéä-m™t–&h³¦äÈ¡*¦8"/Ó5\—)ŸÎãg\¹Æ”âàÊÿË•zÕ¤Ó»uzƒYWŸég<:––÷Y�¡�Ú>”l—£1zÓE»·QgíæäWXýàÖƒ[_èÖî!ªw4Eõ&ctdžýWS÷•<Ü=�Åæãy½ßE¿ÿ‹<€ÄÁðoÜðQý£RTýlõ;Äó·Û"Ù#¶¬Mà ) o9(Â8ÌøqÇ$|q�¦s3ˆ]9¶!‰¸ó^)DO¢ám8Swœ9¬f벃`‡˜êï É{¸dÄ7qŽ¢»PÍ©\ álChÚi¡–ј�8 h3jólÍwÁƒ ¢v¦•OØ5¬5ºaPp¸ÝÈ‚<ź¦"ýîÛãíëô8ÿ{1°ÓCÿ{[X›˜ÛÄN˼ˣüddÿ¿Ùiñþƒ­¬bþ§1³c1»"2ï>™ÙOföc0³­ÜTù*baÕ‹9Ê6 ¨¤Akª"”í„1û'Ãrã%e=Ëf^…å¡êö�ˆ7FÌŒé“5Ø,¶svGi•fg Øè —…Tì]ÎO X}`ËAm÷fÿð0’’q`Å 4ºÈ¶›B=UX®v�X?1˜An_êÓ,ì¼h9 ¦fšä™IÀc‚xžÂ˜í§™J�•Ù¯RVÄso^“zÃÆRù3ã 8óNqùRÇV ,J·ûŸíærÔS�ANåM<² ž°†O1Nö�Ë¿;3œ7P�KáTÚÁ˜Ò¨k[±;Ê�Î ÆçÙt]ºÛŽe¦'±­eÀè‚S„AÝ92Õ"Ìü-RüºO}³H8\ß,bÎOXßÎüG—7.å¿Znüƒ×8kÊÿeãSªœÓqž£+NmœG¤ãRÅqÄá{\“n·ð™ð 8ήˆÝ!qà†Øá[êÅO;Õx¤ܱÆ7G8×Ü|~;W‰,Xó÷ÒÏìîî´Q=}ï§Ã�¢Î¸ÓM·UÌåp°]N·¾¼ÿýêä»X¢eÔé~gÕ«ÓD#âÖ; �º(dÒ@R²£æ¡í^"#T°™/¬ð\I|Qoײ-Wž&t8ÙÞ&ƒQÝMU®ÊÖJñ’[Lt6bΗ¼,;è°—6.…†–%¯÷gqÒ1x£²É¢æ„çˆÐ®0Ç~“ï‹ê'H­(Ž˜­�Ê˦ é#síFIÏ•=§®zÔù�`0^ÃK˜vï'U$xÔ®Ö€d‚ÕUj  Êb›Wv1è¯�)‘ª)ÖIÍݪBÖõ¢£Q<þöà7M‘i1ÞÁ(Fk‰Hx/ygÔPÚ5î¦_‘m;Mv_†“0  «îб (€ô µ�å¦Öj6O,ÐU„ÏšÖî¸q04ˆØ\aý®s¿Ôp¦ÙrÛóŸZ‚/Øåm ªKͰO¨]W9%§öÓšôÓšt§”Ò£ëRÁW¡ÝT¶9µ. Ö;¡ã®wÔš+±ÞÙ5¸xý2á$Ö0à9Ä2øø˜ý[ ÀÎϬ°µÊ–U¹.©ºôÃô¸OÊbÜ»÷Ï�¥Gέ"¿°K{ôø�o©p+¡Æûà YÔw2wFž�ìvÚD…n¥KA½Ôy]xÇ0ЙÁ�ºÅÔ]•�ó/RPô}‡øl`¾\€å¹ÇxÍìbÛ–ù�TýÐqbkÐR'eñÚ&ŒÖc»I{>¢4çM�Rv§j>8å&qyÏóMg;r < »7;èsl‡6à‘7ä|Üz}ŽÃ¼xÃEj;òpï±ÙZa· wÿ1¹œeߦ6g €7Qå›g´kè^7¹Ú¢-P‡¸¸©ë¹Së¬x2<$Œn"ËË"£û²({�#×99ŠÄ¡0¤ò‰‰:®Sc ­¹»øù0^øK5xÐÁ:uÒr­¾yk^h¿½töÖ¼9«Y÷röðÖ$ÉÑÒõî 5 9ÝÈš/T³ô“½ÔûÆÕ ëƒcù�,û~ÂÁ¢5m;¡z´Voç�ÙëWiˆD-‡‹J·“áÊM”ßÊÍQÑð4уlÍ`ªÞ_gø§áð@ÁÑÜ„йù¶’9ÅüÈ^iÝ1®³ÝˆKøŒìÊå;û8[â¬çâSî@%@µ¥ wŽÜ„‹pÿØ‚ˆŽŠ-Ĩ’ä\C&‹»p›†ïÕ*\\®ðâî–C,8Ô©8ï©9(! JB°Çß¹øÂ‰M¿<žqfö¯MVvÕjÁXS¬Vî�ç‘O,�%ÞÈU¾UÙ,$›È~—�÷M[(/ 4?Ÿ¼Í·]9÷zk‹îÝ;ŽgÏ cÈ(T÷¨¨Ï2†ßŒ\3N;aˆ%“á*f¨ôGŠø&/†v cŨù“ذdÂi™p¸÷,�Íj.§�k¯Þ÷4� '‡ˆoáùˆ}¼* c±Ö(bÁGœÃ2I»ïv…Ûiµ%œèÚ†rktÉœ>Ç­yuÜXZ5£bÊÛzZ¤+‚íŸFЛӋ�ïÝ…ª³ÛžF�W 2‹GÎC›;Ç�°ÎÆ“4Leï¹±#W#q€r[Z³L;•:=þˆ½æK©�¤åø¢`Ó€˜Që�ÎPÞV¢Úsñœï…‰ÎVÖÀà�È/�Ò™~§všŽ‹&›ÝÔ^¥N§OöµÁ|nÊ;†÷ø ›ºžHži7䪋~ªd•RùÆ©àÃÐíY¨È}ßTX¾‰6ð½R> g”}§/ƒvÈñŸ\=�ú2ï¶-çl,q”ƒY>ö6j{üQY ÷¿©aûqà·¦Œœi‚O�ñ¸ý“¸)ÐO–ßÕá1 ïYƸ‘ËìÛf½Ä¨­çnêí$‡dÄ1i‘WÜã°¬l�7‰$�Qå�›$êpŠ3Þ½C#0\ñêNgJœE±ªÖÜMGZÛ—‚'2cŒÆâ™q-ž=Üêü4ó¿b8î ê\u–8 bò)!êsÉFg–ÐÎ,=žcf ™fÖûЙe“fÖÎÅ3ë¢þ~f¶.ÿØFqáç%êÆó–$¦ šmû¸=[oß§«èîà·zÿë™Dîz´OÝ& MŒû_*MïÇœŠ—�¼_¨­³|>ºØ�±I”ú¹[x¶^×é£ý ó½h}0o¥²Aî–ˆz«½!Ãzï7ÎW¸C-üT° �Ç[)Bèyð‡šÓÞHn´«z%F»QAˆ¿�fr« âôŒÖ0=q»J@ÓDÞÞ2§¼£ÞÂ-¶è°¶É@>¤Žþ!dÕpqrå·~ã·~ã·^ÀÈ*Ùñ¼DæO•öà¡nÆ�"°aµÜ¾#°•",áŸ4†a则_Gï„3—Vl‘’JfäΡšŽJ0=[�t©çà(Âç̧ãåNÐ ¯•TaݰÏÁ ¶ ‘Óƒ:E_Kñ±}¶�ˆþ:¸ ud÷ï¢%´t´U Ž$Â=�T‡>ySekôuà §’µÎrõSy�)‹—Z+ðb“7MK¯w&E1V#‚RX¯,1/áÎN™>a-v¸+%‡Üyq§Ô­ $IØØÚ€.aOëtÙ�ø÷m¿®.ÅÿO*>Æ ßÀ߃^A‡@¼xœ­TÁjÜ0½û+¦>„v×Û”@H´‚’æœzéQkÏ®¶äHc—ݯïÈZoœÅmh „Çï=½y#K|øöôðãç÷GÐÔÔ2オâ¥ARPjåÒ&ïh·¼Í ®“¡e oìžH£Eªe¢8q·®:D¥Ï²R¤èÐb¸ƒ×þ– V5¸ÉÓ§6-r) É×rZfÆ5W[(kÂ&×7° Œ-]5º¹Ñ+/EØ€Ø^Hl%|ü$Š�#Šö]ôL8&ÐßÄùYò+ý÷&MÂ,ÃŽoWëÚ´÷{Kxð¨AY”Æ(ïÕa•%XeúÑd–7yb&ƒƦ®^b�Ôy@�À欥Hçr}E¦Áp¿6rÖL³µÿ¥[p~F#ûÃ\^ÜÎ…n|:ôÜa ¬ ˜#.à—6¥Î é�V=GJP#�œE¨¸GŒcÅçNÕ@ŽèÝ à+Ô¯{Ó#ï�A`¤Ç“|p³)|9›6ɯ÷Ü+{©q>¶õ”ðÆ0¢;>‘çð¢ãp¢/x–÷ŠØm6q_ºÎ[ CŒ²#cÐcIÑœ! Ö‘Ž‘ ††¡q9])Eº¤~‰g‘Ë�=@ ~+,xœÕZQoÛ6~ç¯àòÐ¥@R£ �c I»­/íP†¢´LÛÄ$J#)'ίߑ)R¢lÙNÛ hX¤xß¿ûxG{úÓÛ�wŸÿúã^«"Ÿ¡©ûCÉþTœ­‰�T]ŸÕjyùËÙž+¦r:“J0¾Â7B�-þXQA+¹œNì0šNšeæåb«}9[EÔ¶¢ò |¦¡)ÁœôúÌÎz‘u6›Nˆ^åÕ¬}³_Á³ g9‘òúlý/%ü»„�ú�¬\8œoðtCÄìn:Ñð5žÎgf…ù Ÿ›¡+ìà…�z}ºuŸž­iž³êêùtb–ŸN* a†ïJ Rÿ�wD{'_ ;gÁ6h!/_ŸÙ×,È8Xè4T8kÑHãµ&y o€yÊ%ì ŽÑ¼Àø&Ïf¼ª•[·¨¥Âk²¡X­!°ƒX²GŠéCF+Õ¬¨‡J»ßáú(a€oq¹4/Äf€1$—%žSÃN’)*à)øø@åΨFÖL†Ú9š©RÀ^T°¬™t�K�órÅ2’·ï!ˆaQÌynýpû8��ìst]ŠÇ$O›�¹êWñ|=”Ÿ¿—‚=–\��-9ôÎBˆNåkƒî0d˜I³¹ôŸšmHN¹2Q7ÜÙ·´­/cl|u6_ht�çùE™a�œ–ý–•è`öK -õ=ÕÑq\OQÆõ辡B%©Þ œHu¿ÊÑTÿV0>9ÑlߟèW‘�«4Ñ7�ßÿ š/™€™ÿ_– Z)’ÛçIŽ'¹}rÛ½<@mn?uý´6/»ûì>ì·¾3‹BÁ Ø»¤ÁþD+ I–•£fãHÄÞÑyÛ²hÆÄ¦ªZpÙ±yh²KÆõp¸H¹%y™@“,Ò¶-µàípšs÷ßFïuë5G ‡C[¾h¯½#ËŠflÉèáùÖ æL*#Ä�™D@„]éÊŒä5•#A_àû5ËÖ¨Y~ ü=¨¨Ë6Aa|¡7S’­U:!¶ k¥T^•Úg„+A7¬¬¥“û{¦Ö¡½p)/ü{ÃयŒÑœIÓÇ ™}ûx%;HKv;TÒ %©6û7¾@ˆ£ß>Niìi†SOÔtáÆB›49b�ë9p°cŽã÷ËX»˜Abòò¦Ic¦—O ZJ�%ZCAbÉ@ xÉ/%XÍ©a–8�%àX°˜©TááÃ@º $z£‘T¿î¤ÈCP.ÌAvtø bÈTë�u<+… ²‚2°£¡8Œ�A.¬Ì°ƒ<¥ÌFKvî–ß)’�_Ú#/L¡"»ŠlUÛ˜†ú´¬Ãªq 8zÚ��_.©Ðè‚óÎË70­�q�¼ó–ßÑ=C—8ãºDÀâ;oyQ-ýµ€ ¥'(ø >K˜Ø§�È(Žv�®šüœ® âÜÐîI×]Ìå‰ã£M,ÅÌm˜v?ÚÚ6±ðŒ: äàa oÝRB-•úÓ€ß�·JÔ™ò9¯}ïëÝR”Å“Tƒæˆ?Z¥CLžÄOAWœ·D­Ïf¯ž%»¾ÓbÖ4O6¯ ñà0Ãï8<ò�:¶š€âÉIÆ'ï�cW# œšzmA4>×Çy�ó@™nÎÅùÜc-�à w1’ä*=mR¸³»¶bfÈ>þË5©hŠþfàèZ¨yû[t\'™ì™E¾¸šqðòäèÄß‚Ýížyùô¬¶Ë<Õq;ÜÿÙ“Â77aÇ3d Åætâ—2¬` EQäoÊ�×y]ð{£Fíºw7¨{êĪD¹Ðêéòfäy½ É�U*’£ .JìýEOr`â[Pò ÝÄ5o·Äê*Ô�pÎgu®5Ö«U Âh¯È°ÔÊB¢âø›é\w6áÝlm+Hd#6¦Ž²Ãª&ÍÍSl%id.Ë0-*µuJnÃvþåkØkÖ’…^ædeQ…1YdLdrÍ–©ïØÜБʼÿ=úÄÑæZUªG`%`�Ð4Ñ öκ“k�uT«˜uð4ìV;®šÂÚ?4Ño‘m½ z}¤¢ô…«ë Á�Ѻn1(�ƒóm§­k¯sSüÔ­"/Mâi+€Þgz«pmº¡ž mWeêu?ÞÞó%Z‡˜ñžF‘–lÅÝN™eÏås¯nå°ÒY0N¹%ÇÆ|ŸB�bÞP†LDabÿ¢0Áù¶ÿMð{M8£å’Æ0ƒŠ8¾QD6»•¯¢¢¨U¢lNì†ü*ÃE‘Пß(§‚äìQë» êºZXB[ä…çÃåۣŧA6•o�ô¦­Zd(€¶wÛa_ÕÏùJÏ`@•0»w DûÅ]Ó¶$a¼‹5$}Q_´¶Z‹™•Å5ªGOdATÓL`µºQ#õäåâ<4:à{ âXh”³ƒÉR¬DDGè4 Ä‚$:œdZT™¨Ï«F¨zé{mè8Ó©R͉ÆôãMJ™po§~V��báÛç“ß~¥AfÌ&(r s°V}µðA« PòÔÍHh´v˜ˆS'öA õ^[5Gûš Jh�o@º²Y�Œ¨,ó8¥Õ9}EÞIÆj Ì¿ìÛaÄ%î�w ª0¶%>Läi§ŸØœOž?/Åo¿GnXU›ùè˜f¡@·±Çdˆ›ôþ†1w1r+7ôÓ80Z!¿Ê}fb5³5™.pá}Ó Ö¯LÏCTxçÑ%aX¨LÃ8V Rž�|B¯“š¸ÌjŠ—¨ Aäê æÊÀ Fǘ•ºVj騂5 Õfžëu‚Ž&ÈS»e…f']ÔÝïoÿÑÑ+Ik`®5Š$«=ÚBÊœº[ž�7k#5¡ç“‰ à^¸º¡Î‹¥b߈ìêè“49ó$!…л QÅÆRÀØë Î…•äU…œC€Ìðn" ˆbžݨUòG�ªóIZR—ŸXQ"ÏPžl{#qÌnP5h¾hd<3IoeÚ ¢†’7¨Wr™X0Põ�„ߤšKØ™pb Ä%޲™stÐÈejZª¹„ T-2šôK[r�æ³+Pw�a¢[샔&!‘6& ¾£ Fœ!x-y…$¡o r¡Xé³ÚBãß +«}ó®ÃO &Ç£…ì…�8u®Ÿ#“ƒ-Œ‹Ù›‘§71$$‘ `Ñ%Ù2>£}bŸÎ{³Œ}‚;ýÒ²dQ .ØrUgC’•ªFæ%¨ûˆuÌìFA.bØ� vâ|0È�âe׸í�sÄxâ=éÛˆ�?Œ¹jHÒ¤ÁüÉÌúPÏæ˜’,£+(MKÙ.kv×�R<áÕ­Ç© LΔE€Ÿ6ҳȲQ°¦’ò•;aÒÅ•ÙÒÁ4R!WXÈ,09Ó4iOˆ� �â^JÒ ïC�rtÙÀÑ‘˜ÅþÛ)�R)ºŒ�ü(LPvtâÑîÂ17Šý(aÕ»´ Ú|Ïè!ÙÄ{׉ªÔql$O=e?(Ôt¶;&,,pBæhÎ:�ÍäsÜ�ôæYÚmõõj³& ½<ðF5²—•%dõbnMÍÖj•[bº βOï2”€Áu™[ßp ×.—Æ¢2&9"/›h¹0¢e TcÖ ÃF‚Cžß‘hÔW��&5)ºÖßö‹›¾x<éÇ«TSJ{ÞBݹ;hyç½èV«ÊëZÝy ÀŒƒR£¸òI—µï¢ªyj ‰_’ "Jsæmà H JŒŸq�Rjzk®R$ Ñ;_gKì_C¥ ËÛžòÑ6}à 0›í½'ÛŠ×è¤d(\u)È€ ^>%.Ýûð§#ñ† Ü#'D+­Û i=GñA­ßàvÝç½¼h´é<Ñáߣ\ƒÍ‚‡šß½ºøï«:bz F�„”µ÷dÂ|ÕM$3z—Ò)'â‰fUè»v̯;utO¶°*QÜÿ’É£ëy¿À€(‹BaÚýhÍb �M]æà2ÍÕô£âþ\œ�†W#XI±~¿‡5øSÜäú&ü÷+s ¼J›•—l~Ü'ñ«ãM'ûpß^ÇêøpÏ”éã;"À­3âÂi©oxíuÈÕÂNÚâ»é�±—Ö@úî�ç4þxús÷„~ßá¶Å'(„/ÃÆÛö‘¾áPYòO¸e2žÆ6çp0øû!î¬ð›S˜ç1“�+^à€ŠŒ§É�Ü”a½œ4ú’©Å=‡�5žÆRnÒWa£JVqϰ�, –_>ÿyÌa·7ç°œod=_Èù˜_…Ù7~æ&ÿ”®4÷& ýÜôsëÜ}Šq‰Æ%8Ò·Ûál4< _Èz$ôs+„–[bnô<ºiµ_÷úw[QI³/�òº…d±îÚxz÷M·Ñªd'ž� 0×Ä}<ßo o0ë4ºÉûnö5…¾Ë öçMyùšÃãåEŒAŽÃãE7Qh#.BDÚÃ?<_àQ$]¹Ç Å-ÜeW¡ì‡áE¹€„dñÄeuÌþqk+ì( FèŒ× ;âJ²Ãcaã ¢‹«…]ñ…M‘þä¤?Ž “½Tùú[ào¢ïz<ÝÑ ?ÊuéÍå]uù¾ÃáèÝžVv¼M½q7�Ú¸›Y¿âFÕ1u-Oœ‚unÊœðÆÁ·¯?~z‚ûV»›ßàLé"8(î®ï—ª97EK  ä0ñ@Ðhã%hE_Þ‘s�÷x]u‚g–�Z…�(ȲvÖ“d`×—oûì,j ׈·iÞ¶ŸÎ Œ8à¸R{.ähÿÙê†�— -D÷óÍ àmãñ”Ÿ9 ?Ñ®"NM{�¸ =$€•.ýtÛ§ïw‰�¯0 d™OŸéd†w(èÀ—ásm�´Œ¾T|wpy©)(él8Ê£Ãc”ÝÁm3~Í×+{IÎþƒÂhÑ\Åd[L·à]‡®Å¾óÀ\Ñp R–J9¾à`ýÁc¤²H¤?·àÄø™Cò ÕtÖs$Ú9ÚNeΤ¿²Qv¡Šk¡c²Ûªp/ܰ¡«WÕîYÁŽ8½Y^3ÆXÊnôª ÍŽ-ðu6î÷в ú�3#2“cà± Ë ©/çùDǬ®ñH ðÜ›!ÅRÐ;¨û*pø >òh¤T›€¢£Œ#Ág¬=£êŠY÷„•饪¯#Ce¯¢c1+E¬ ¾!Å•ÞRËa?4Þdky¼¹Ï<¼p¸‹0ãy•žëô¬éàêêYÐÆXÌ+£±Sj†‰Ü+¹\â¡G>¹Étpíô²¸ÆÍdxdµ+úû‘Û`³öðq5€ …|d¥ª!HŒwˆšƒà¦Qö™i¡ÄßþGøÛ ª ïN.´²Ò Êñ7îSÑ@¸Ãý4Š{XŠ{m”‰~ï혱¯fHüÜâÏïƒôë[òÛÿݘ�Ê_š—�rã–ÖRJ µ¢¯Ý3¯Ý6G úh2žâ¿y×VÏt÷؂R€ã Á×M¯Õúн.Òc×ó •kÅLù•RÍ_–Ì�ÁvÅØ÷XùWƒ…õž]Ñc`Õ@÷�3Ð+`à…S¸±Øó•_•«.ûq!Ã¥¦7,ï=s·Eš‡3–z>‡0Ê…üª|î»~=P·Æ&xæ ýx[T Óà9ߨA‚[|æ;X]‚m\~ Ëè*ÃKÁ/÷ô�™cˆjÖ5WB”êÊY–ãG5ºIXÖBÓïãÕUíDè¼3Û‡Q�VFP]qPCC=_ {ÄæÕ0 [w0ºâ–ˆ�l0¦wxÆbqg7Œo_v“yVUU‡páå�dÉ?Ù¸à½wÒ9Œ2H¼ÜUê?Õ»õþ»¼Kt˜ÄÅÆ÷å<�$m±¬WËeµ¾|î9Ü9¿ Šx»#µ];LǨ�èo�ìe»ÍÛIµ)¯¬i—…˃êÏÕxZãed�ÞÆUâÇlëì—–á{„%9•‡×älýgY?]h%¶'×Ï šîuR›ãÀFÁtt_ žÙ }UÿEÓ‘Y îƒ0òs/ŒáFìQá±ãqKúk Ù^B‚Ø�¹€}{í¼.ÈSRoºÐûuµ˜æ-LÛà5�äGž‚¶ ‹Ë{g7½º~ÈsZïÈücÀûûpÃúÎ^/2]â‡''ŒïqÓͪ#‚ö°*uxpw¿³×Ëb%è�~#§�éQ…Žƒ~©o dòôÈz8Òik›ßÆÒuŠ»;¬öVW¯Þñ‡‰1@Å÷"¼ß7 eÇ�»‚h„vñþw“Ü\—•Á¤„Ã_uì.ƒmhÆþIˆB!‡êߘŽ_õV¨¿ ºAr$û¥[[ÁhÇ;²˜—r)«² ¿Tk–mŒvc¦øæuÜÒ¥og…оþ@PTØív($Ÿ[Y, " ºC_n`C}`MaXB áã…ù;é&~®„�…’¿Ô¡õ •öÖ#îv(8 <ö‚)Æaµ™/SÑ)+•ö†Ä݇žˆ† [ŽSÃýzÛöß�ÌÆþ[èC¡b–#,.íͤCÊ r†'zl¿eÒÿ#ä—ñ;(ð:ôÈ2Â`lèÓµnq¾÷nÁVV-qd<1¥B^W`„!ªñ˜Lñ5FL~€Pô?h,õ#“ëmó;È€`1ÿ>p÷›~öÏ­R€ [vløXË›æà ‰çO”µ„Böš6ž«GÏYBn0˜Ä‹ÉñÒ- êiø_hNù?´ù?\+M ðð’ C¤%MxœÕZm�ä4þî_á›7 õLÃJHzGv‘XéX�⃓¸·}$qc;ÝN÷ß©*Û‰óÒÙÌ HwÒ®z:¶«ìª§ž*Wz÷�×ß½zøùû¯ùÁUåÛÅ) ø¨¤¿…¯öên·(âÅÝpV½€çGž—ÂÚ—W‡ÏøÞ¿øê`U®‹°Ã/¢®–ï²»~}¢iÖn{DIwÆh–ùG…:Eñ•½ùìj4kÃíQæj¯dÁ…å`)Y–\#Z®÷d7‘;iøIæN˵�IÖU¿c~Þ-çoöc1~†Ÿ°áÊqey®ë§ÔÌÐFŒ«Ä×¢,ÛUðÚjí¸¬Ž`Ž� ¹Mé>Þ0TS5Ö¡"'TÍÝAr §äuSe äÉRV²vuáp?bôÙr¿ˆyÿ‚ª{ØK·„dg°F×^7£PÖ©:w0ùgÝð\Ô\ä¹´Ö "© uyÐìNÂܹ¬ÜmñžµÁt-Ú $£…+0 .õ>âgåªfü( lä ­$?ä�£e`iSõ¥ÕÀÎ=~z°ÐWPÈxæCÑ`«ÞÕx<î4œ‡Ë÷tžw6§ÈG[ÒᯥÍ�::¥ë È“±µ8oJa†H™‚ý�,Zô“A?™=Áþl)عÍlï–]»Ÿ±„öìl^å,Œ‚'ÆðÎŒ¶bˆ11�0DO�0¹ý¾ã¯Û©ìEÇÿh¥y t8ñzxºË{ Sß…Â-Š2UÕ{m*ñA×/-S`ÝÑÚ}Ýr$|4.’?#¿çD~€ù�EÇCtÚî KìÏF_Yðgºo$x9hÄ»…ƒÏ'§áð3Bw$gêÍnÆŠ„5š;Ž@wÖ7�§/%6LaéŠ4Y}8›±u¾˜ÍX’Î.ÇûÈ‚Ã�O Þòsâ#rÀä_µ<äÖÍâ†ÔÐ,L¾†Äs½á×?…Db¯£Šé‘=1~9l.äLƒCÄ鲩j‹yÒ›„³aÆ®e—WÏÒðn$nÊ©“,[ íoôY‚·€œÓ\ ÛçÚ ŽúÊa¯ äûˆ¯‹-Z"×Õ꺰˜NQH'Ný€åˆ�=‚â@ºÆÔÞå1ßZþo­j�$M§ 5À))_ÐÓ¨—€áANµ!4ú¼Nܤ®g£œ$EÔ%Iëˇß$T7Ìã�x>è2Z¨Ð‹\�;O%ƒÑ§3ÉHÌ”Hâ„<2œú¤ê—=’/þâêw®$eó5©ÇøT™XèÒù{ƒ‰0-YÀyJ¢èŒÉPÄçݲ.@g«K׳œD_á?®ñ�‚ƒ7Ei"½ f Eé¨'ð@·¬âgR©wAü”Â6 ±AW ¬~a¯¼–gŠ=ô›‘¼¶* ðvx1 hôùaÄ,„Aç«åX@>¢§Ñ€´/IPÜt&Ú` Fš”-_Ã<ÊFËw16ºŒõl®qñBö¿’›GÎø‹Sóœô~7ŒïCIPÕ…|?�EÊ>ý–!ú;D€”ouáo›Þè�ÖœO4Ncñœ“1ƒ±ƒØ$ôIÄ>îscz»ôYް½Êø¢Æ“*$’˜p`Ó“(UR¶_ 3i׆hrѹ©é¤çìPÚRÜ&×e±tÅ’ÅÏÓ’{d€]hå°¤—ó¬VÎ0—½™OWçÄ 2UjÉ�¿ÐQ¤e 2}ì@Ø)€%&QŸtgu<–7à6"e†TØ3îˆ J @Ž{{<µôâ@Ì}WxÆó{€A9!-}»R­ 4Ò];¨˜!ù-åX`”I¦O®ÍÄ�Neƒ¸àïàà5Y˜R²ý>YÁ$?Ö°Ù‹B£ÏçŽ f�4œ±Ž-hêÿcÍû÷t|{–`kY-ØÓÔØ£éá;°z`¸XlÌÑ[Í ©ôd;B`k�„•‹TÀVsÁ� �=  ˜ÁV3”ò3,Àæh€v¿&þ_æTÝ(×^$�~Êó™ •µ@y7m‘zi3ÿ:‚` %Ý �ØBÁÞN¯„tb²¦¦Dè5µ•þ…§áLº±“!¨ûŽñ!!+<—'ИtœÞygíV}³Ä/þϧùµkò”RÀ™ â�dAö„ÛÈô2Âb´a_ç1‰?E�? n§Õ�ïkûŒŸ�× Âá\�£ÉX!Ò{°%}Ãû´OÕY N¦ä(ƳÆõ §åXÐ;¼õ+À†¤wY}æßÞ?üëþ+ \$bjƒD- _ð&š|;FôFÔÚJãç{f†%]L��í†;‡¢:–HÊý¡‚~ò�6ü½ûünàRTŠì6òíÄâŒKÈ?ÒЋ8Åw¹ÃÄâI*ó!K*`pC·§Lø†ý‘UøÙ$­ÙQC­ÇÂBºfùUà+HúêQ7áÄñ„§3âTÒ”ýõ˜ñÑÝDÉÍxTL.À—©’D%/û}b‰%G*^\RFÚN'ü¨$éaßÿ}��ÌJ[ˆ�²ÿ…æ·×Òk¦^ùMã8°A¦Ð¦}à4ö˜ïå}è’&<i4ŠŒC§õNl”÷2ûvYßôñ’DQà´¨Á …qTqcsS$ÒèÌ3Hœ]åT£úklF]—mÿ*!ièÏî/ò# §¥P:h*Ç‚ãq£0½²ëz¹Àô]®WañzE¿sÃðóÎ�4>¸Š^íx]‹ýû~è“úÃrá-û¯5ÂÍ}}'¾ð×AhÐf¾tvù9?3-p!ºË &Ã�{ü­ÍK<�ñ Â4a„{ü7â'õ8¥_‡ÇÏë‡oà«ýJ;>ІK#”œ²¦ͼ5«X �ðüÑÈ%ñƒ•xˆÆ6æÍ÷~¾W9²Ð—Ö·ÈeBKg’Im¢�Âî¨Ýqº\Ò‡~sIé=<í¾Ó=Ì+Ê ¡Bî[K™Þ¶àkèš—ªáNJ?_mÜ¿s'LhWü _sBYkUì(Y]hÅ-/-ÈßhÕ¸…^I”VÚ³ô ZÚ‚-ÑŸÏ;ïù$//ÂI·úÝ“àUšGU�9ã}êpuRë%ÉåЦ5"Úí£zÃkŽ1¡B£E ®ÖìPÐÇ×<õR\þÝ»>=Ìž—b#œ›9JXs��²3ÎÚ©õW�²å$Y?ŸS�¢®i©¤eB:¥ß°¶Æ°×-§¥ ; áGßœúñ�¸�c†)©0iØlÆXÝb*“.÷'dwaã�à&hÓ`¨î›6)š>@]( U§præuÙc@pâp©$oööà‚ÖJñ+ø²s#p_ïª>J/öíY°=Ì7˜õœGs‡æbÉÙ�q(y�G§ FH’à‹}:ӂȱ4ðlEœ™±¹ð±S¼É|Mæœ�®gò„“ûò?Ùƒ�4·­–ˆ\Ô‹MÞÛÔÞ’qê/ EO“Õ+gï4eõc“Äõ¹±‘(¨#±Ý6Ç,ÃSll>~{\ذp2߸É}èÕÏ©¦ûš•(4.胈Qqº>øßèé+!ݺrì„p‡ð§V¡ +Khªä­‹™7øŸÿŠvdâè_oîÿró'HÝHÔh…ªÛFö±uÖÕjÃä�úáØ‘zìA;9xÒØeù^«=ø÷@°®Ck¾±Tµkf"-ÿs]\ÿ7 ²Éä¬ËØ”Lž»=eñ„àåm=�;Û¨º†´¾óZ< ÖbE?E2›«Õ)×Äj�_†V ½(1F| .À~‹Èð ’@ÍK^qYÃ/…Ý(däÐôÝïJµëîaº÷`A77‰nlàKªÔ«ÀhùX¤caËt§Feú‹„¿ä†áŠiËÀ ?# 1_—Ä®M1¹�ôÂd0ׯrgaf”ã{ìYŠ ¨ÏZ4£DÄEå¦#i]Ž®¹/–`¢h´3™ƒd;�¬g=B=Ž9WÃPCda�T6„œQ“z‹Ø>È%ë€Ú>6e6'ßfp{«]1�6äëx&iâ&�F6« ;…Jn¡›Tì`|�;籚ˊé�¹^2T Çð¥æÙ–£ªÒINLºAlœèpØÁBn4þ”£t%$«³ü©ZòXä1i;ö€‰Eq%ý#â;×·[=J@ê*xFrÓ7y“Ì®Äfç9ã‰&ÍPHŸg2ÈÛ1ƒôr^J Ó œJ"o$Ò‰˜ã�7O™$”Šáý Ön.­@Þ÷¸ä>Ò;xçâ²>ö˜_ÄSO|ðJÞ{Xñ(}ýäˆ@¢\¼ëu“™Ù0H§§ÔËŸlìQ‹ƒËéo�Â|gFÖTã`0Ë[p6\�GpÙéÌö±%ŽÙ>ŸÏþü8“Ñ~ðäëü»q.G ÏfóÉs–’î(~¯î0˜~=hIáщåeœjbÿFT鿜WwwƒºórN¨¼�Âáå‡g±cö ™?8½øLƒÞ»E:¸É],L"3ƒf»ÝÙqâMŸñOÛà|”È|˜Ro C2Å£ÂÈ3îÀ®åWm3½çàÌî} e‡—ßÙö;ò.dY·îH}È.Ù;( •-Â…+FºäÝ~_ ÿÓÁ Õ�$ìÈ‹º~üôWvv\‰”óêü›(H¯0ïÅä¦{ü[›ƒJï,ß_=ÙßÚ�;Öìkþpò�)›¯}÷¼I£;Ñ;Ë´E× l±o�“œ„;-§¿—³(êDÌÂmñ¿+ÓÑ=YnL�7-îFa™âˆé_‹-¡”Ü®6B0'|KNµîŒ‘œ†ÇMw;y–rhÁl"ËXµGšï¨zÿ©Dr Ñ›éÙMxD’‹g°�˜Ï/……uü&¹�•pbh5;ᄵæ¤?X€dô˜Ý„½(<óåNŽÕ; øÁÆéÃâ¯Ò“7É*`€÷bŽÌGbæjÄÝ™‡ëÙ½i €êèØÂ½[ðf”Ìð@ëR¢8H1ðÚ/pJL£“‡„è$¨=òÍ„n'ÑþR"=X¯ã¨†�H�+þTýAXº+Dû_ŸÚü‰•¶>à[«‡¹w%çwžd=‡ï6ÏPEÎÆGºå’küË€Ü á \@ øê� ¢Ú]0�Ñ�/‡Bp@8þ5ª{kȘÀ| ¸I¢åY�—W�+Hz€�î �U\à /[¯Ýq§óhÍÖ¼6í &’ۅØ®Þ!úÀœ,1îÝqî¸ 4“ )И�æ 1QÍçà>‡Ïne‰…f>[õù¾¼ '¨Küë©ß÷}¡ûtøÏ죂N«vì:ÈvŸSF5xpDÎÀ¸Ä˜‰EÉ‚09NÌùÞ"ü9×ÂÿÝØÿNó÷X xx�Eº4Wxœí[m�Û6þ®_Á p]°÷­h/ÙÝ,�KR @^zÝ´½ë—€¶h[ˆ,¹"µ»î¯¿gfH‰²lïKÓÃõp@›Ø29œ÷yfÄ\üåÕ‡—ÿõýkµpËü2¹�⯥qZMº²Æ=R»Ùøé“#²øFÍ,–OËÔsx¦.&—ÑúÉ¥\\ëêÒMò‹#ú0Rü}>äÁðâˆ÷_­z´ñß_Ýï=Ã?xq;~Òy½��Kõs•9£´×Të”Ë¥[³Ò•v&_Óv5xyõÓPÑa‡‰ìN³ë ÆÒŽ¿y"#ˆñ{+FÝ'V¹…ñÌt7‚·¤/ðÍ"›.ÔR¯Õ„¤ ÇÒSg*um¦®¬FôÌä¹uøXVø†OY1O”�ê\W‡J}Äy•±uîðœåSS]¹ î¦&zúYÝdnáƒH§Ìžg;(’%Þ?1ó¬°²KÔi §ò¬ š«²â“å ø.Öø½°fZ» ®[•76ÁOyÊz€Œ™Ø…œ|ÄŽŒ¿Rc§U¶rvBÂ"Uu‘9|Ì lÓ’¦¦:LÀQi�¢°�Fˆ¶Òx,ó¼¼1�m­Ê‚UåLQ0©Šöñ4’í'ωP°ßòì7ÐÐÖÓ³g~uMÑžg—‰z_/±pÊæå<ƒÊ‘„ÙÜ™‚¨õrb*«~%@¢ôc¯ì#ùzâ¿a¾w™µ¤N:#+fYAîÌÛ;MàEŸ l#„Þë÷žÔ(&%AZ‘øeð¿Qëví.ñ½àï;5ñk]"ìÀª¹u�ðEð<Ð{—¤nUfÙ€Ó2¯—Ù�¶Œ”ш ²ª'š LJ˜Ô–âðµÒiJz³uu ϳ|ÐÒ+s »"‡¹6Õ:QpçjMçi¿ÔoÜmDF’!®ÌråÖð× ä,3yâØêµa+(»‹R¬‰ôDŽûŠvŸËŸáL¿×?¡½Íq"IÇRB>o\¶ ÁÙ*­‘ü`û­…€{sõA=ýöø¢”Õ2H‚ÓÍ-<_qÄ5EÕÂd•J3»Ê‘­h­vç̰|&«˜|†� ²Ä”BÑ•—ÅÜf©é§ƒQdN:9 {Ê‚3×a�±dŠTW¯6$je�‚ÅyYQÔÝåœÉ†¢YÛQ-üˆ²©÷ʆiðˆ=Äl�2g=Yé-ot"ǪEºk÷SòÛ8§€Ì�Øm¥ïiêùK8OJüAŠ,k*¤>Ç“éžDñéê©««MâQÐQÂN:guìEÃq²º#Í¥L©Æ hy£î-8Ìœü‰Âºc“º ä®U›se›þPÞ°Á Â.Kåó}Ã]\N˜Kí—$í.0�s�EplP¯à«²àœ²ªJ¨qGZ3šáÒ»©æ©®ªÌp ;ÇÏÍ.vƒ* L<6H(kø]çy'Èå­Tëžå6ë2×(Îm]ˆ4ÞDHª š BW¯¨^›ô¬�yØëÆ"¶…þOŒˆsŽ)NVÁUÇ.ÐÍW»ä½T§»ÄWU¶ÔÕ¸ÐUø×JÝâù“¯�ÿŠg]A€“ñš¿5k"ádÑ‘“O§™tÚ%Í>sÀØ0T~ö¦ƒ>º4ÞŇ½hм€+DŒ™f³5{%Ü…rz?.º^5HÍL#{YÕ&*öÁÁg€MÞvzŸ*àTìÛQ[ƒ68þÛdçs'G¡@øe`ð=¯Êzå}�3Ë\6ÊsQ¬ð@ptœR9Æ:É‹`T®]¨ІÉRwZ(„úc�lù�ã+ד÷pI“cBîÛg¬˜ÿ#–‘> ||¦{�òï«_ˆLh¸HÇ(ÀÊËÌqµâ<ÃF¡%��I»É>\~ö;ª_²Qþ*ƒPØ%X‡‡¡ $Ñ͆Æ4ŸUI C‹ÒqU/¤¤˜4ä'QÉÖ&µLíŽ&¿<¨Iåõp“zß3¾L“Z¨+S¼*§5÷VvÅùaP+Ö¢‚íÿúµE-]`ppü)²ú½kS‚B¬…Ë~yóýx…hÑ€&Ãd·ÚZ¤~.1‡¢ºqƬsˆä«á~”ª¾{;¼q�CÚ<Ý輓»[oœ¾¯õæþUŽP0…ø¾¢G¡e¤ž×g­!qŽ1wÂ�J*“ÄrCüD 5_3 O%! äh—6I£‡ÏÒÔÍ}úÔLª>ÑìÓ§¦ka~ù-=Ò‡g‚Å cˆsƒGþ¼[šŽSǃ0¢žÖç˜ôéÕÞÞ²à}­òF¾ÓÒÅš¤&+|æ�Ld ·m«eNöªj'¬!ZŦÎìr°ÒN–zE¢|xu% Öï3\hSõÄPå ð /µkÀ/Å׎1„ì ¨bR¢QÖEg#}S F^è `�óщÚÓlËÜ£Íìñ,>­îqžÜÙ—n#ʤFÒ™‚ž�(4Ø­½Ü¶�ÆÖéÀ®Z:Š™ßUŒwذ;/[ü‡Ͷ5 ƒ&_Ô;'Húcgdf*^»ÃØ…Xÿ2¢žöø0iàÙè“ÞšÇv€ 5p÷ëïv·wI¿¿#9~C’ë]™í�%ôÏÆ¬,O¾dåG¬úÛƒv<ò—$ùÿÞñw÷Ž\u÷v$¡ñÜŸ¥"bÙšæ"ä¡xr°Ñöp‘ÎÉŽk¤äÌ:)©‚AÐ¥T9�µ*³Êõ”F6 ªl<ÝT%÷© n! eªk“²?juSVŸQ%>SL«Lê -šCªx¾›sVáaiãþ¼�v°ô«>sZP‘ Oó:õZÜŽçâ¦|oOžÜ·)¿«'OîÕ”ÿOöäwðÿ÷òî—IÞq¼#¶š°Í ޱæ'sQÈ€z¶¹õh“(Žrz@š6ÔäkÔ’}UÙ_ëòÜÊë.d“Ìu È�?Ÿœ¸¡’f0@•’øÄÞÞ9ÇøC‡Ð^Gsè�ãgÁ ùš1ÅÆKØFÎÒØs¯«ù\f.ÐRNØÈ7ᛄ¦€kq§É²Žf  H{Ç9Éýæ9ûz½³/ø6#ç$˜ç<~œÃÂ{óœö§{tâ {§-ESÝ7ÕyÄA_èþ;D†ô¦Þ½øøöÅßÇ4΀±i�¼+»Ïx§åÿAJºkÆCo�·í÷�5óNÍ+e‚¬˜™Š Ú¬*—¾=GÞ¶pÛÆŸÝ­Ûè Ûˆ�ߦmC»QRÃ^Ÿo Ù}C³óøœÛÜÞFa•DÏ—¹×·ˆ�˜µ/?ZI>ÙÒ=œ�ÐF[ ³©tTáÕf£*Ò]„¿,òì³Ù‘ÏŽ6y�ÒÐG° ÒΟΒ6éÐ= Âùšt#¬E³�1£Óh†_œàœM´95f®;¹ (å)G"⦀<„£'¦d^ö^3¢Ç6‘ˆˆt�‘[ ›ÎJ×®¤­S˜Ü!A�™|HpVêˆôÿÑeÙhé¶ï$™øýîÑ*3>mÁõP(“ÛQŸö6›TæÃl–Måmg_-’�öý•eGä¡¥ $Uà»K‰þÀ”�»±…¤ …Ï!mA%¥ÚoLÔ›¦Ï‹ÓX ³ÚÛ�G 4j+¶· âw´Á~Ãc-´t»·-¾Ið¬ãœŠ«j}âÆsΊh±3·³L<„õŒgÁ1! 29ëÞ2£‰ßÄôS9B?�Aÿ5)Zf{’¡Z“A岄š—%»(a¾8ïl†WÈ��:/­EUµùzgËü'èˆOŸÞ£#>}TG,¤·ç¬>m×RHô zÅbøu¨�@‡Il¼•¾ä܇i¿—Lü…*ølegÇÇu=&ïìœvð×ÈÂU/am{_7µ÷e²Õ^Ö¾ãë¡"WÇdNŽnnPå�IaM,áYû,š¯`là˜?j¤š§pîæYÓåX³Ìz+'ºa}S£ƒjãt�ëJ†š÷qéeV üòSH~í³¨o¤ÁBÃmW‰=æ7hßW„ý3*âý**~”õÏšyÒæÔ*Wô00ð—ÅAâÉÌø^Zh2Ã8©#{wÔåKAanú-éná~ÐÅ|®é Æ^œ »®iøQ  èÔÓEtóàå7mïíü»M¿¶§wåjœ›š¬*è‰aØ`†¿HB¡C”öN%8ø¼Ÿ„·ÿí|¢çøÃXáÝ©ŒH„bhÛ'Øxíœw â�‡ÓÕÜxO޶Ÿ®ò=‰FÃQT¶M0 _|ŽÜ÷¤wßÄø9L\s‘[t‡©£Èãe¦:Yw}¶§enòË(Ç0òðãØä¡óغ ð¡vyÚÝ:óVþðêjÍ|«²yѱ=Ü–˜3·z¹"—ªL Œ`¯¼Ý©Wô‚î#K\ŒøíƒŸwÐÅi¥+gÏ9ºú¸m´¥ùЉÃZzˆð¾œp[eÍßwf«ƒ`!7™s E§z-Çs¦×{Ì¿;£‰±§>1À¥êX Nž>{6>9}2ÌN“¿�³â»ãH*P³ö£u퇯´rD=N§]J—ovÅooåB3U/¾Ÿ/}#¿¦ê]–[Ï÷%Ú7´Ü µÆzMePÕ Û1UºGÍ ÔÜÌõLVhR±n#OëÕèé$ž>tH+“‘bÎÉO’-ÝǨßkú».RÔ;C?B;òÿÌçHþýпŸäà" ¨ ¨ÄSG�‰(Ïßxœí][�ÜÆ•~¯_ÁÕƒ%=RâE€…$vdÇ»~°bȲ7B`,ØÝ5Ó\±ÉÉžQo ÿ0iÏ¥î6û2²² ±¤&YuêTÕ¹|眪Wÿò;~÷þ‡ß«a]_ŠWúY.á�µÊb±*»^_=Ú×ÿöè9ü>TC-/‡r^ËâǶfÅ·U=ÈnV”Ͳx+Ë®+›ùê9¿(^=W ÎÛå›ÿíå²Êa·‘ý‹bB;ð�xUM¹–_=¢÷Ÿõð~×Þõ�._=/±Í//ýðÕ—ðû¦XÔeßõhõ»âß^´KEû‹âÕmÙ]óúõ«çø·â«âÕüÒ60¿,žèW®ø•§¯žS¯žoÎßø¬à¿mïÞÀXûǪ·ûîÿ€çßTkìöÓŒÿ€ÿ÷Ÿ¤·/V²®«ÍKÕï²êäb¨Úæ>:ú²U?ü\ÖÛ <†ÿ]À?‡€€?¨F«f)?ò/¿ŒôxIû®V²Àw‹öº(yC>üʲºÕd­û‹ß=⯘¤˜ºÓ쾡7zÛaÕÁ›ð“(Ê~!›eÕÜm·”]1/{¹,Ú†>¼EÒ§ø¯ëªëø­«ˆì¢øîº�µ\ËfèEú¥¢ì {¹‘å —3|¡Q#²Ls©�ïðašÕ­örÑ‚ ÒͲØê[ SsïVÍ »ÿc¶ %/C®•‹° 96´EïBÛ$0{fIøþ²6í ŠUy+íÛ³¢z\TkÞöC×nd7TÐ’¦KSUõ…\o†�âa­urØvMöwCE‹sF—jxœíX]oÛ6}ׯàò04€S· �c`XZ¬Ë ,[±GZ¢mb©’”÷×ï\~È’¿b·(0`‚ÄbÈsÏý:—òä»»ß~~øëÃ[¶ôµš“üGð já9+—Ü:áo¯Z?¿ùájŒu/½SÏgJ°ßÛºævÃÞë¹±5÷ÒèÉ8n(&ã43Õ†`_N+î¹ß4½a'Îcc1áLóZÜ^…}Ï]Üw5�Œ9A½šÖqæ–V*îÜíÕò5›;l.M•˜¾a“Ù´Û=›²g“·S?S“1}¸žŒÃæÉ¸ÙÂÏ ý. �sñ8»½ Ê>X©=ã,�afއèT7Ur•iÔîæõU<)äSG¬°†ÐóKÁ*áJ+ -›[S bxòùka½îùÝöDÆ›¥ÌZTl¶r¼\)™À‘ôá{ë>µæG‡Õ Ǹ®ˆ‰´ •�0îÀl.50¥~šÓŸ û-½Ë¬|þÉžW Gº ëNJŸŠ²¾µÚQD¼mK<ˆÑ ,Fð›û‚•F{.ãÎmì(;R7­�ñKG·N0öÑes)TEûûÈ“»ÁP—ŸEL}Š¿Ôá©4Ö ×]I½è’UtÈ}ט¼i”,#7BÐm=–ÔÒ¹ˆ-8Ä¢1Ö $æxŠç}[ÿwv…œ¡Š%Šì¯—B3é ca÷0_4û,¬É jI–”œ²wÆ aAKu>¸àC0P™ ÉÚ(w t‹ \orá€Dþ)Óˆ¨ôº§–ZÖm=bµ¨$×£@½æ�´˜¼G¸@«`u«¼,�jkͳ|ºØÁFãÙÐc,3'n$µ¡Ø $]XC™18åŸ)Ëe¡KáŠíÁÛŠ^“¥Õw\9Ñk�£ÖÉPθzY@«ZËû#Ñ'—~„ ÔhŸS݈ÇRµTÝe¸+Òg©ùC¯òÚ}Z»fB‰ZÀü¹®!²BWÜÞ ¡¢%ÆÑ@ÚÀ)oÂCŒØQ�#ƒu ˆ¦=RÁu·Öb�{ÑŠ®œ¸è/à^T�t™9'{œNG$]Lú‡àCM�à &¿÷õ‹ò‚PöÅx?’E7¾ÑåbÝ�åÖÞ =ÙÄ“®­¬{AÊIº¨×ÒÓ襗šÎ),3¼SsŒKÐ1±�Îsq2Î|Å¥ ¬sē˃ |Xn—““aÄÛU÷.š$ìúÒî[d ì2»÷Ûºnï�×<Ž3¬²X%Å"+·YNDbB“ÜÞÈEu]Á æ;ö§\±(›b2Š,¯röPq Ú fìÉ5ü§,zpm›}Zà¯À2�ðšL;ãáÇ}Áß²‹_{ÂôìèÝlÛ ˜ïz¨6NýÓôÚ]•½¿be ǃrxQo—RsÑì‡+´÷šr¨ne½‹¦Àò¢noªEY«ÁL³]Ë®ZhöÀ¨; ïTÍ od‡ü¥X©]¿”}…ËÉ ¶Ám_a×öÅ�ä¡Âé�õnäMé}%ìgØ�¯º®i«¡¾¹†Ï›…GBQ¼o·4Jb‚)†¿ÃDÞ–UM«Ì.Ÿ„²„%·å¬fP®«E rN8Ó_v7[\ŽÐ×;ÜñX g.ÔR\h p].ªº£\òRßÂöl¯Y ¦l& f³LëÈœ¤P{±­ËÎÙ¶R.¡Uß•f íÚ¨-a,—6CYZÐÀ�è<è ÚÎÿšD’©ÕE‡Šù±�÷‹®Ú ,J¹çÞ2T°½cw%6zš ™fhºÚJ›Ai‘¢D/%W|$Û´Úʆ$@BEÑŠKÎ&sXó÷1¯ýdžÁ�~ –÷c#ÌYfÁº*7›ºâ ÀõkhΆvÌæç9I›]¨Ah%ÒÚH�To%+Ç…ÒÙöŽh 07X4âò£…=\Ȇ¬ÕhûNö0y‰“ah&ÊŽš}î�Ÿ–+ÎŽ^K)e{ÃV�<éÀ…»r�— U˜q1(gŠöVvl2rKKy]5ÜŽ‘�F¤"ÛÁnÁg"Ý7YŠ€9‹÷í–�p`ݱIwW’ù­Á>ђˈ�[¸Œ€Yé¢ –‚dØ r±%‘l[Ø6CUÓRD[D™ü}[ß“§Do-Ñð"F)‹>�mʬ*{ÇÖ‘ùÓ/ݳÿ�ß8xRÛ(°m_¸ Bñ7¹ãÇ/ïéù év„'k–+H·ÒÈtÔ ðñ€kË]ñ�ÏYÜUÃÊ7ìD�½^POÐ ipÍ«^f‹˜U]é!=þ¾¢uõC].h�>Žˆc᨟# kþFhï’Hi©%©55Û ªaÄÇvhMÔ¦rÔT“ŽÐaO{ÞC»fQ ´ìù­¡Ýà+"²(^êŽÉ {Îu ͽ4q…©2µÈjà°Ëå¯Û5LkÕ·Í÷rXµË —­œÔ¯kz_Ëc¶Çiíl{%8›~h}cé_ ãæ¸ä�«Øþº¼iªÍP| ªÁÚRoYîà·£�}‡Vüv¤š¦]3†y?ÞªOž×ª©‘úm<æ(xªð•imÆ‹LuàRI6¾²*PÞw°Þ‹Wrm@žrX=º|rñ÷¿ÍŠ¿ÿí—WÏåš\Ëëm‡n›1²æ@Ú­zi<ÇmèH–iHU,m|¿^û°ìD u¬ôU¿%oÔØ dΗ�OÂîg/B©ø|YÝFHó¶©þ¼•ÎÌ?�2ëÏïciZÍA/‡? (ùTýµ‹Å¶#ççì¸çÇ«à߯=Ô¥-‹�þD/Kü$”;ÌãŸz=“�åô‹"{)7 âŸ8gè,‘¹jë%û}Zt“êÐ7uõ¦|cìVýË»à—/êáå¶Q†á7ÃËÈâÇ7TûÎó§‚¥ WŒç I3?É8¦Í‰D‚äø 7ÃK¶¡ŠA~ÈY`Z'X#~Ž.d—É"Éå“]GPs:Ñ ã`ëÊŠÆ­¤;O‘ðO€§å¶ž:¾SO]?dNh¼ åÖ‹ZU„S$4µßq¶I¹„‡UÇ-’›l·pNÞ:w9¡pÄìå( Ow¬áB}ëÊ�Ϥ•›N¿—õŒ¢Zb�XÚþž1'‘q|$þ²øz%ŠJkV� ë”Þ5§ÞM¶r]Zó¶â“5˜f=â "‰cI‹M‚½ás‹ÑäYÍ{°îÐi¿µ±U·îz·¦ƒ ¦kái†J¶žÞôðŸ’AŽŠÌ@5¥ÝÖ@ùFÈʲ)ùÆÜºEa#:䘶8§ÎL*4/òÓ46Kãa∉iNíg”˜–^°�kàÆ‹.¹žžB °õþ‘Ÿ:…ãÉv2E8›ÛÓ™ÓlŠ�Í180Å@Œ'{Ûû‚ûÅöÅ„àþ´Ø¾ˆƒû§o q|’É?݉­ƒXЉ)’nzV„0!®Û³dEˆý�Á‰Y”pð�ñ�ñ�qœH™æiKe“$2Aë‡$‰‡$‰‡$‰‡$‰Ã’$²Âî˜át§>°ùÂM°pÌÚfjã ð�nñ�nñ�nñÿ*Ý" ×Ãêû�„êõƒ£Óly\ùÃyÒ èêÞ ç£a @øtÄŽû'éû~Kì|R>“»4QQ(á-+�–9K¡�îzâô"¿ð­ðÞb’®;Ð’iŒÓúY‘#¯C ‚¬H´�]_Ò/Ç›R�'Ür<Ë`WŽg¥L5ž˜VŽ7‘¡IôÜD£õÃÜóÕ�1F|ö¢±ó±pieÌ'X°Ç±”ðx䥊rM¬ùÛ�Ç‹ý5M�8Gñ¥]‡áÜbâ´ €EÉ3a�ÉA×Q™\ñwزŒµçy¶wZh‰ ~ .ÒÅ�‡£à"Wx0 .öÁàQð¨^2ƒOBÁÅ>¼(~jêêƒLC+3L„"�\$PòHKŽ ä¶—½�¡ä3Þ2ý@8³òä=÷U¥¬áØ“ê4ÀÙS0»8gÏÃìâ0œ}Ì.ÃÙs0»˜³„²‹½0û‘µ‡“¥™±‘Ó dÎ2ˆê÷£ãæûÑtößfÀhøÿ›óàÐN/¼j¯®a�X3ŒZ¨ÛEy>|¼ÿ×6ƒ<š€±\ûÃy#ï~VŽ�Q$5RCY ’.ѯíα‘ÐZ?.WÔžeéÐÁ}L ɺêf5h MVÆ Í%ƒÿù{4ÔÕGä`‹×ÒëÝ‘:Ô‹<ùZPÌÁtD»¾ôF ’ò{ÚÌ(Â@w‚�º$«G$(ø¢ëÿ¼m_ö>VÇØ‹Ö#x©sB¿½bÔAü€Y_+×BwI,Q¾@`bᢂœf…1Á,;” ß’´êŽaV²›‘F–ÿ&!¢L¦û�O³¢Nj%!aþj*±‰â ×þù+ë©õ¿Ì®ó¥]"¿Ç½øáÌàB•iÐ/\°¡ü-¶öf8ÇòV61ð† UwÕ´k¶LCN3w®°(5­N�xën sNÓ²2;OsjߨZ^?ÌÓç)«JĘ.SHã7ÑæZ.•\Ë€OQ9±BBb„ÃïÛŸ<‘™½ƒ¡x•èh¥S£ÙìÓÀ8ÇÜ‘v¥â�þ\(†ŒÚsÌÆ¤Ig%­ºCŒ ·©)õŠÓ̈·Ôª�`ŸdEX2O$QxË3�*)Ÿ#ƒ6Œ�"é¸HcëÄÝ’AZöX|Öºštdw„$$ÂQ©à‘žëïß{ÂPÂ_ȶ]¯¦cr,ËoC‘ yž„ J)DÏì!äp÷ù²ø¾½=§ŠÑP•‡ŸôþfOEc ¯¬ ._ëøe9ÿôASÜ·¦À)ü¼ôD‚¢_EKìÝ&'¹ßvë¤"{“œpÇñ9‡s'ôjü‡qîlh‘ÃǼª;nZr×GçušÓþ0¯‡ÎkÆÌ1öÑüÇ“ t×¾'xàDgñ]Îõ‡¤Z$ãA‰ÿÊîÞç§Æ“4}rEž” ý¦®†¤`0OŽ¿Â˶pwx�¶~�÷tˆ� º=4æöf,û[ç²hëíº9£Sc(¹ _éÓý‹”vê[^�ß¡YÚæ)âÃØ:¥b˜÷æâ.Ú�Ê Å“*KLX†ÝVpD¯‘wõÎDÀÒ%È£ûoøøCøœàûÿÓV�‹ìéZ› �Õ¤g.}R˜Êï° �تú’ñ Nƒ+ÇÛY´ÍPV˜lÀKCåªQÒ¡ù�äš²§¬[n­×éV˜Ÿ¤"‘§Fx”½Þéø"õØ{Šš-;¥#@«› ª8¦Ù¨X€¥dl†¯HãQ8ÁË-¬ÄÊ>]‘Ñó®—÷Ãæ(½”ŠWPŠ2ãwâuåp25‹º$JE2¦1JÓâ¶XvSNÿÏîÕ‘ ’xó¶M½ -—Ô΃Èid�«\F¼SLÍw¯“v-‡"@Y0D4ðczG§‚oŽäln»‘¬¦{·šxÖ>'›)AÑç}8ôàXiÚ(0½ù¦á”–jCÖàPHÍ’�Ðn¹SÊ%«"unö›õŸ»^6&dì_ò*QÝ]Áã¡LX¥78]?9O·-|J“ðÀn�9õ{ì"ÈcÀ=MYJH¤´Ö‘ð·ÐÚgÑ®çtŽJ»9Xy;öƒÈV‰Å}…нë^ŒÛ‘žØ:žåA¬Z¦!sæ4•Z°vêÖqæêÏ”&צd$zã�Éiå„vªzk£ôüJî”Ѽæ31±¬EAËD¸W“S÷ †S¸š?ÆQ¶[ý¦$Ë)ü¬¢ûô4pf¾=e†É/ÆT2Î$Ó`¶5L&ØÇ”ÆÖd,3[Y—mrF1z°Ü‡ «ËÐÖ»•œÎ_šå2´sy –7™Ø×Áƒ�ò Â343Ÿ•‘’"és²RA~Ì-b*�ÞÚìýÈmbz»‡JÊÅžT U™Ê�X4;Ò$�A(é§ÓµØßG i¼Sõ¾Ðy¼í`‹ÖR�΄¶a¢4…4üUÿûJ  ©d4fhÕnë¥]“Ú6H LïZõ¢ÚÊKV?ê/œ` _okšñØÄ²ä+:ï ùÇË-霪¡rš¡r—‡’€Å„™ÃíyÛÖäªËc-\Ôo$R¬jø­iìI´µ�Ûa~£9ÿ$>føi¼ï@Ù­L)L¨8 æÈ�ž£�¯¬aâ0¬Ðo»Û þò‚¯¢0¼àz*/‚•Öþ k©PBÐyøU³­†úÔìœnDzbÌj×ņσةïç‘X�i ª“^‚ET’Hç¹æ]´NZjµ¡aC· U9°~€œÊŒa|Ë‹Ç^Èk´@çÎRíêlvªŒZj!üxÛÀwG¥‚3;>Z{wTÔ�¬ØE?€‚èüCå°wr]2rg'胔©Æ]e-$�çÙÉñ3¯­< ´BÞ‘²Iú_s³‘`Æâ{£W"Kæ0�Á!ýD²…n+H¦9Æ… ­32Ækíþ¬55‹Ÿ™Á–¡êWK½ô÷@`ÌX�»×»˜|8w\ˆÖbÚ&ô‡±‹°a;jY¯^o* /V x­)@®H`ri•¤‡ïðØ¡g¢ø/„ʽuG¢Õ`{³_à [C4P%¦F Ëöavi$ú@–ÄÖ6-Ÿ4°XÈÍ�1š”¶)øî­Á¶Ñsýf‹�¯oœϹ8L6QÇY—öö#œŽ`êbN ç¢kè@=¶\éþfì ›qbÄ@X †º•ƒB(o)œ°6ÛÁîRU¬•¨Es¤êïyïÊÝŒCQìûê—éÙ[Fù^ùã�™l‡ÍeÑféŶwhÌ`½ot zà -pï4¼øPÜÚõ§V0šÉæb#+L¯Oú§X"�ÙgËêúšCtT>·Ýh°ÄB `‘�™'Mtg,hg`£Wø`#j7æÄɳ”TÂðίEâ9`sO„ÓÝ«’rœJ½ç)ÖÍg³ë©û$ý$j{럟Ý2Ë¥·¤S� ‘/“&”yr4îë´p`¢ÁAéM£½ƒê¾•-ü�6ÍÀˆéï`�˜S&È ó´ó Pšê±ËP™Œ>œáXãêx¥�à:Ö¸fF‹ ¿M–Ž~ÛÏ(¨�ƒ×Î'K~¯ê8  -¬÷κ¨~ù¬:ÿAÞT -{µj]jÔ1Ú9ÑWiEá`–ì®';³`/l¨þh°�®'vÎà³ÖìÒF²Èl¤šç<8‘ÐP‰v‚b›š%—å*Ä”`P ’xT‹D©!ßȆÎegm  ÿb¯æLLg;©ãF]¥r`@¦¹qüÞµä÷o€{ÇÀôNòh:ö ˜ �ê3ø—åå#ŽîeÇŽ£5&ÝÑ>™­‹$2>Ò‹C�¢—ÇÝ4ýŠ —ü·51ê'½<À‘¾'ãîë£ü255.,ÙÿØn»E 5Œ]ðAjF‚$$‚6®™ùneî,Ôç*E¦§cÓ•Ž�ï™ÊÖªBü�fåºRÖèP7Ïšêɤ ��º×h±‹{Œƒl&+ó“æÔqúífcNÝ)Áæ ¥pV” 6+VÚv¥q1šB!Æ;ÍK–ñe$½ÝÖÆ=¾••�¾UnŠªµ![ö�$,LŒÄ2eëLì]|óó,µfQßÐÈ%e£«baÝVnür‰"Þíz§s§ï èvÙÝ:u_Mãd²q§úÍðÒåt®íP.>Äy¶øëñ9¶üõ§¨ëê/¦¯‘\zuÊ_ÏHɯsýR_ÿüK– 8µß±Â}<—Ãzd¾/ux@þ ¾¯U z{ꌥ3L& ñƒsî€ìæCØb\„ùDØx]ϘMÒ5ŠÀLQŽD¯Ï¼L߀¨/ÌmÀ¬”³„avSœ0�—–‘H ±�F—˱?ƒÇM«ÎÛ4_÷æ¦Âe‚Ê/Ãûàé0TVº)–]™P‡+Þ’~«Tâ0;š°êùÄ"\Ñ$!qªí´ wÇ€Œ’³ÿý“ÛšJAƒÚ»ød8ÿÐûÏci²ä@�ÚsLÙ¦tž*6Î&%ÝÀiÇ)g.�à�õR“£Çâø-ï#då‚˘s³¡ô%™¯š]ø ­ÄõŒÏÜ#­M“HfÇ Ñ‰z™ ×b 3>™®ìí-†Îd:gÝ:V´µ]˜q×”m=!³9#•Æ4ˆÈ«-µŒ\½éÚí†6¼טּ©¨˜¯W^†°>€Y¨æpox}ÓµËí‚‘CéoOŽ…£Û€‰DýOh xå#ßÀG,ñû %=ôÑ7„°ÞASôÙS•¾¦Æ@s©–W $µþ¬ÌÆìÂ^ºø®Ú`Î|1Uj Æ;pn7¶ƒ´ÖG.©y7¹gèÕj�ÁƒÖÛ•liË):”^³ËÔËCî¤u[ióÙ„à’4}NISJBÝ;Xd4Ï=eL}�³²b1‡©<ÈUÙÄqÝ‘pꃠ¤´M•qáN ©À±¨=*×ʉ:Å�v'í)� ÞÓÑÝ7ÐJÑ,„¿6�òOëþ·æ,öLQk.öú¶}åÄÞAT�D¬Amc”Z&¡A»øoä� ìKžÓðÚ¡|º£kCNpæ¿FJð…‹º¿uÒçìï‰�|‡¢æˆaˆ„Mcn �ìuB!ä(Ÿuî¶UØbŽM¹¯S3¤Ènn:¼ IY‡˜já9®êçéWf«ë:òƒå¶Sg¶*¸ëô�áðíz]r櫾º‰¦› “wàÅ}†?ðÅqQªÞÔ<�økk¯óYÿ`§·� Luü1Ç8LP@裢�Ýk"g¸Y ù‘rp®ÃX¦c°•WO™öv«t ÁÀöjfÚÜŸyÍLi.RYVaá\¶±UÁóì�Z‰AÅ 2XûþÙ$ÿÌl¨ÏÊCËPõkøhY§bÊå­`07{ÃRÊãH¬mürPåýÀ6’^§ö@…¸�ŒQ¢‡K!M �Y Äì¸-aRP,%w=½½�&–Ò(¨è¢¿¹�Ëí|ïn¯êéßÿP,Ž=©œãËkT—ÐðÉé‰WGûÅJê�¹ÝfÑÉeÊ…°ÇQÄÍ2Z«ï³á£¢Ž¤cHëùg…p‚”ZâžÊN9Íïä4>¬)ÖQeçú �ˆ´ Ã…d[•R4r20™ÁÄFضR¥cm¶Ý¦å«íä Í(X¸|]+^8�º²çPCÃÿ‚s†Ku1£‡Už>�N>„ò5ûÀtf�¨Á ™Æš«\´‹3⣨æ¢ø0°­°FÇšè½+¢ÌPÕY*Fæ¹±þõÌ}u?¨s=(J³ëkN¸­w¡,4%bsÈÍ›Ÿt£SsÎ;¤«½mÎ h¯àݨoâV‘+j»“”'Ð Z'ˆ„oæ=2àíÄ2$aÝPÿ†8}Õ+1Ýx…<:7¯§Û˜Ñ7©‚|p9Ýä”r²Fü 'dç,Ó¹Äö¬IQ$�ÑÏà<‘I‰Ex¥d"°ü[•�L‘ å•-#¬Pu¶§¹pELýw½ �PNûwðd œ×»õÚ€Äö€¿âÁÛ…û¥­\·IÖ%šöB–´ëåÜ]¹Àëµ—e÷�ÿŠŽ+á%:n'4uHŒh3Å?Ñ?sÑbÕÐÿÀ<±|”çsò.S ³LÇ ‘]G×qRäþù�Õj˜2Áœdð1]ªÕ—�‡äDÍ|gGÕª$–„sjœNÄ1é>™<¥Å Ù2‡ËÂ=)Th^uxž{Š“=rf®¦ø–àà “¿ Ë$Cñ¸‚l©@û¹:È-¦áäAp­j*lÕÊroE÷º�–pÓ6W?~ýÝwλ'A®‘-ü� ºV*ø‹ì"àÕyvráª×Öþ ŒK�*ª–Í˶ª@!u‹ùÐ,Rœzî¡C]¾öã2®8�½i“[2ØznEç�ì§N-Uo­É�ŸñE¹æ~Þä¹Qþ”ÓvS‚ÈëµèA¹2*OX¸1õ¼½îÝk˜>oSÔ ¾fô^²–�©ͬ®&WÆ·×�¢Ý>à@å}‚glì¸W’Y˜·ï*U,ÜL]¥Š¿¬;ß¹ŠÓ4b”æ;Ï.ч\x×›yO°x|ìOÿ4�Ju‰§¥Y<™X±—Z¾ÔÏ¥ÏU&Š|�>gÒCË÷�] ®bÏæƒÂÏNò �º±ÍȼĠqª¾XIWï€_ÿp–™Q]îù>z´æboÅ î™ÃO¨.œ¸1„ñ¼»U[K.c�N“ÔÒÇrÐRljä®/Æ ™"£Æ«ÌÂÓòK…ŠM¨A‰¢¨¹%FëèþÏñ³ÖI•Ì?Ÿ¦‘uÓreGÝfb©cÒ%Ë\fb ã:Ö`4•¬˜¨e')Ys�‡.·Ùw¬Mt™«H© ÔUƒoÔÕŸÀŠeK{Žh*l–+ö¸CiÆ—Ô0n�”°š[ç„rµXŸÏÛåÿ\ ëúRüJb¢ì SS™*H²XNþxœí\moÛ8þÎ_Áˇ&œøv��k év�½í6‡lv‡CQÐó*K®H%qýÍ I‰z±-;nú hb‰/ÃáÌ<ó éŒþöóåËëÿü럙y2f#ÿCŠ~Ì¥<š‰\Kóâ 0Ó“ á¹Q&‘c#&‰äÿÌTÊEó?¤á— ™ £²T�†¶ Ý`“,^âÐ?Œca„Y.¤~Î7Œ�ÙHðTÌå‹j{ú?h{0 Žõã¸z­„g %B볟øTCË(‹�¬ÏùèVäc3IFCü…¿à£ÉØö�Œù‘ûÚ¾”ͯìƒãÑ� Ÿv"÷à ,»öà/‘r³,ðï>š†Lÿ­ åÆT÷nÒ·M)ŸÍd’¨ÅYcª1™Í'*•ÜÜev5Ÿ,yžÝi^h•Þð÷rÉaHEïN™í«[/å\Ÿüt`Dz¶Ô…RôßY‰4oô@£bÍ^(ì\˜h†¢š™QA­šgSü¤r>’óqm £!<𻙊fЛñXNE‘.réFp-á“0\Ïü [HæËa=“ÌÌœÂNYÓD¢,5B¥ htɧYÎ¥€ ñÈÖRŒXN‘å±ÌϘí 4*d‘H#c~§pRlËMds4¯“»Y¦e}ã˜ÕÒ)ç׳ƫ®æ…6~¼HÕ‡Bº}±Þ̰“Uõ€œ]ÞÊ|IãÍpUÍM¤‘'’/r©ej IkO½‘�E�Ó�³,‰qŸ+MFISƒM‘żoœí ¸ríŸ0÷èYbΊöL+~vcμ¹ÁÎT-ÜøÁûc«8PSšqØs™£H ÇWó…ë1òÞ  ¸ÄÏ`Ed Õ­Œ Œ�Á�Gà|Úä8—à“D¤ïýÖ t�™ru?À=n‰¨[/€�ÁNx‚Hþûùõëó V¶PÆšH ‚Â2ÎÜÊ”vëòkÖF% Ÿ&â†úí¨þbYzÆ*ë0HÝcÚ¦ ¦Ÿ$Ù/ú¶N³ôd“‚~ÿ=“)Z¨¢Ùƒœ�q±XH€³†7Âfp]L§êäÏÕ-Ì<ͳ¹ó¦tQ˜g¹þPdg ~SÌÑíx¨vŒ$æ&#Ya©ÓDEmi~4¥M…a4=‰{…©�õZ #mÃá;ìuè7·:xqå_œáf#¤2;/Ÿ 2Fœ}PöHäÔt•«›Yð¬¦Ð2>† …¶šs뙉ÛF¤ mØé¼ƒ’0ž»`"-“©]ÊàÔ„ó¤Å|VÓàÞ_ß]¿û¡”h"µŠ%+å½öoŽirØ\kå 0†.ô;Eg�J—šž"Lûõƒj $¯†8—ÍÕG‚ig(÷õ 4ªlAñô¹ÇÕ„¢=„ƒîÄÖØxÑ“K}X‚8¼€·ñ˜ÿU�V†Œª—i;Ç xDŠokÎÚÌѼbžqþ+D‘BÁPíÙÁ�“ã5ìu( àÞ]¡É…/OAÒ¸¾ŽfÿÁÊÎ=WÙá†í “X@\$K ¢©�IõxD~«é¬×Em‘sY⎣¦ŠBÅ�D\`4e.©W:ù�ELŸGu+¤\_·VZ¯;UÓƒ;t�Ti”± µW†ß‹ 3Z'!EÍ>8`ÃâºÕàP½¢­Àߤ\\¦òe¶Xvèç��íÑ)ÍË¢¨ÈÞBi›M³4Y¢pàØ0¾E”æ ãÂð#Î�P91â î®­¹ 8!Oœg€=—݆1p· fõHømkfú°;:³4EžZý‚›Ë{æ”ú8V�SƒÖ cUê›ÏÀÙŠŒÔGà!ð† Ti*óN&X¾Ù•<'Ür¶Ç%†¯WÛM[ô•lñWlg m"Í�(ù„ı”jöˆvi÷é{I¶’Iî@$Y“I­Xéj%Àç”J'ÊÕGLÄ“d‰A)�„‘H»ˆ'uÓL à­·-Ú¬±)uÈîJ ˆ ÃTn­0³ƒñœdM"¤yµ]hvJk�Z26Ä!a™(5ѬNXÚb>3 °áÌͼ*%Úà�W›Àh¯½C(›Îrc·¨eE ÝZæ ùj�Ôâ žhð þbip+elã^a·'1fë™ñ6Ę}ƒÌx¿ÝHÛY [™–<‘Ü'’ûU“\»X� ̵B²so­ç¯¼†ë 1ë͈{b¶#îKׂ \t²IV£“á'D)ÖëÆ ÕÆT,SÑÌfÅu阯Tém˜ÛÚ¤³äÜ,GÍe%vstU’«ôÝPÝQ]:ð ·eçµ6+Ì [¾Ù•À<�Ýr¶/‰À6D_I`/±Ý£ØRªïˆÀþ™&ê½ô)wUWp¡’µ¬ÚªM½—rá�çU”¥Ú´Š¦&°‚ÔŽèK- ¡V>…TÞ­<¤¯F¼‡-¦Ì×'Ý•’h|Âû¡}÷¡TÕÀ <02|2Ñ)æm”°Ã �<îE�waÎí“ò6uÞŽ9³í©óVÌ™m¢ÎÁ¶>çïŽ83´‹µ=*Ò|!#�™s‡áä’ŠôÌÆ„kh³Ü\FÒè5Tš­ãÒý‹;)1{èaq%f+9ñ7H‰ùÒƒÐáï2¿‘5¼ òŸ¬�`gxê.ÖDYRÌñš ˜©‘¢ÇòPÿEPìëåBv0, Ñx�ýªÂéç¾×´H’R‰Gο�‚©�#1ù.s£ÔÌÏöEÇnBCè#xþƪü¦Ù´Ôl`Ìu[nMÓH?Ïêi^¯<|l™X Ósžd7**£á?‘—Šs�§�IoÇ®äç^™¼�U>�Ib,ݾªÇ5ð´9Š[3Þ�ËÁ1fCºWÖ}Ži±ATj WEÀ‘÷J‹¾µžWž½ÀÝ)-Ú;™J)”�ƒbжUÆ\Ì��0ÌúcGº9¨Ð–Œ]ÈÆ,ü­Ük‰’^æ"Œ­st?UšöTib¥ÞRibe©é›ª4…×):“˜€ñ Ù»±â=Vu„úDU�ðîÀ²$t=5ج[FY޲”RwP¯M·:�FAÁçü£Ì3ës@¬tû˜3Ò¦E¬1 v nI¡ 8ëÊ>E ˆÛ*ùÐÓ]Ë=®sX|9o&{)õl1“{ ¥¹Ä‹É{/ìœ7>_Ô ;� +‹:¦®2Ø.åìT¶¡9ûoAÍo|t‡^”©™Cœw¸@©i‚„¸X$dĶo.çÝbäÁÕ$ œõ¼A� %f–7Dçx TFkh‘Pµ»yÛ¼i$eM¦•v¬»B91lQ®™—^åUwOç1Á:ªóvu“fÐéx`k´“•ÌÒ‡°F€×Ü ý¼T_~( ’VL$8ÒS1Ä6I­WœhY�f3Óèi�kÝ–nŒ˜eë_Q¶ÞÛ`¿Ö\ým¸È¿ŸéÝmÏ™ø&œ¸ÏÚIµ}ü” Þ�½§ú¼e�PÒ•�ñ /¦k¼�tyµÇšŽ�ûSÁ…÷µjdysøH°„çÉkNCz£Û>‘^�¬ÇqÈ6èÁÖŸ‡l�ìû„�^öûµ¢GWÈðkÞp"ñ-ŸF Ýß Ú?s÷Öižl XX�%IºN9«xœí[mo7þ¾¿‚çq È6 À!v 8Mz(�—¢5Z÷�ÒR›Ýåvɵ¢î¿ßÌðuw¹’ìKîÓEíHäp8œ—çÒ×yûéû»¿ÿôŽmL]Ý×þ‡à%ü¨…álµá�æõIoÖç=¹„Ï�4•¸1|Y öAj-›{ö+¯z¡¯/íwÅõ¥“±Tå%¾¸)¹áf× ýŠå§Â˜âš³†×âõ ¹àÍ®¶£Nn®/9 zy3þ f¾„oZ¶ª¸Ö¯O6ß±µ†ÿÎáŸæ­Té´}Å®xws÷Ãõ%þd¯Ùõò&´¼aÏiˆYV·vÐÙõ% ¸¾lq‘öVÑÕ²L®Lv»•¨Ec˜Ổ»(ìœR>xÕj}þ݉cÕë•™U„uÂô]£�Óõƒ ™‚U6ÎT“(Õ8-G2mL™�è¶RÃÆŒ_¤p«¬y¥ý2ldFÂY¥îåŠWLÃÿxÇðP½`»þ°ésß VŠ5X±d|µR]‰_Å@†žÂÐU˜Z3ÁW˜ÕIÒ_6Åx¯Ü"=zn%½e?ò�Þ,ç¬T=Ì^0T–}Ç�T Ø»d °hJÞ½u‚<3u—ˆâFY‹É¨g•¹r¶|vo®â m:ø,;¾oœ†3V°Æ½êО“iÿ:=ýw2RT³ã;�ÆÂ`å+ðOö VFuz2ýô4™F[ nð%Z~¿�¤Ôsñ¾É†ãá0Læ§Q˜ ¯)zagȦsƒÉ/x|j-ül§%Û«…?‹O½i{óƒêjnü¹8}|^›™ôhëãÖG”ñ§d� çœÝCΉqN.³(†Û^°­4ÊŠÓìä…`ï„nUãCÞ¯í’äÜ˃¬3J\v�o›XþŸW¾e^y¼îQ¥IE-Fúß;h<ó°c‚ü�¿B7ô …s½¥Ev3PSÛˆ†éV¬äz‡ ó&;rû­ÐÅy_;Eêþò¾Q�(Aä�k¶S=Ûr€ Çg!ZT¦^ÐÇ�€��­«ÐóúÅ€Ë} žÕ°%lY"†€,aÝ]ÇîH®bð-ÐŒd¦æÜÀ øˆcô²¸•’dÁµ¢¯‚<Õ.´_-¿àþHèEÇõíò°òŽA†û$QÄyöðl–˜Õ*1Ï-ØU\™ÆÅ£á‹ØcÀ[Û „#~pk£ªR;çT]A¿@ ˜óRäÃíÝûÛ7ÉÒo„–%(‰*‹/m%W 9æ<»Å¥0[!ò¾…ikœ$n­ªJmÉ-ËRb6×NN‹d£ÿµmµ»ÈgN›,VõK»É¬ék©‰R§á`ø‹I±)7MÙ@jÝ‚¦ÛNÈÜ‚Er¬öœ%8ì¶±U3‘3Ô;ªÄØ-¤rÜ"h9VžŽ^g£Òª(�ÿ"Gt�ê¦[Ä<’\PÝRÿ¬mYØgºDî"+4µ¢7oĤ¦Ä5ÉE�äø}Š=¬à%ùký –„D[·f‡6\V¼ù<);ÁÍT:Ÿ¯ž@œÃ ØMx 9•v„láð› ÊÔ�Æ¥/'ý�^™+û/¸ÁØÜ[–�ÓðÊ}?2G©rÇ5„ù©¦>MÎã8øð0æs‡W£ZŽe‚%AbÚ…]&ó¡�Eƒ{×Oðç\áyßÍ:ò?Íø×pÔÔÉЪW.["&­ÁÈ+& DÈÌ"T’�?­ À¤jD£q‘ ¤¹s*‚óÖü#:°áÎâàq€ÿÌ}pÆ=ˆ—ŽcÅ<µðer»6 ÆßB,ÄÕPxw´;ñ(¨†RŒåXª¨ ;L1¡ò£­I[<Wv÷؆±_"Ršì Tñazêô ðô¹ƒNgsœf ;c²èz§ Z_ñîtÊ’T,ˤ»zŽI‡oŽfÒíLnh}]án—²þ‹%nŽc>‚9ËR'ñÉþYÖá#àŸ{5šPôŸE­ cøô5[îX§¶š*ÝnXª�âî©™‚F‡;†Î/-ÌÄC% ·q_Ð6õ`šÁ¯±¥8( ·‰\ãC†‰”¾UZ²–*…Kb.ÁI°‰1͆¼àáÕbÊœñ[‡ÓƒA›|¡.’‰ˆ³8.â¼Ú$)v¦q‚ÃW &ÌêâŒÊW'ZÕ!QXz.:®\ˆ0cѳ§¦±0CªZ©ª¯$«=:”jV‚J4)¦p°>•è5Ú·çÝé§8Ö\”;wÐÖ¹A Ï&ÃHTà…¥.[¬¡8ÎÊLz‰  0XöËòÎs;ß´=Ç‚1CÎl×G…T‹Å‚SÕ±†ì„`Ü)zÇK0mŽÜ)à 4öü(›>!™Á9öÚ¨ðƒv* ]BHG¦æöãûX%:MÝž~}â~ÁÄRšÐÂù ›�}íù4d4ÃJ¤¶­Ò€„§[VÓÆmÏ—V'ï…“p†~L~™ ‚î" bŸe2Ç ÆÍ0ÿØÃÛî÷‹Ô-pÙ°j –d¹b¨ºò¡Úê-d¬_½n[ý,0«¬ŒE}‚wà!˜FHÛu‡ð»è~©�yJÕÌÛÛRâadØT'ÖïÍŠè¹sŒ„È� àA5 ;U"{ý5ÒßÞ`çÀж `úFv Þ`È m%H,vøUÁ>Šöí0‡{C?ƒFa´eìæ¼ë½Z‘ßæL÷K�M’ì¬un‹Må'…î ‹á¥“ÎDµí¡ÌdÖ«´'‡¿Úºè2DG–hpÿÔQÒàÓTãîü$t�ÐëòèÀk'¿¸NCÅ•ÊþÄd!-:-ްYyX3Å܇„£ˆNá:`ÓsF1“& œ�Ò)üF"Å!ö¾Âƒ6ÇoTN– Ë¥~ß a³¸/,䆯Èr^ÑÁyL;hÓýy/¹,‡ýáÃx,“MgºÂÞ…ï$ Ù Á™»RصéÝ%.8M,4îèlêI¯Zã‰�÷I‡e�r6Uù劰^‡S(Bîö54ç©×í¨a{oÍmËF5â‹9¸z²|¨Ðú˜%hÜQ«¸;K³/�—®µM–’"׆Šc?ÿ9nåŒNC≊V„s±»6VÒŽ@d¤kU5h6BÞo–ªïR�È™´-fì£o/8˜°È·ÒK¡a÷:½Lržµå¥ÍQPR6 mtÛýè¸ÔBÇ;¡ 4Â0žqÍEΗҫœ±x.P8‰Ñ1Dj¨^ŒtÆÞÒ&j ú‚ª6ðÆñi{G¼Aú°DvÂñÚ Ï‹²�S<„J¬M¦q;JË gCmÞ,”÷ž¼dÓ=^ÁÓc€$[úôL—n鹘ž�Áõ+�¶·» ”DÀËX”‹=0{Ç|¬×®þä†}€KUŽíZÝ� ^+—ÐÈÖ“v ;öÈìGøár XËegãO™¢6Óñ)ïQÔ6v QÙ¥¸Û×aÁTI-%&Þû©í›]l/ o²ÐÞ>3)ç]SÚ稙}¯ÀÁU,q‹�‚Ò>Ò('‘0ÈXž�<{rli b ä9>°Ò©†ØÚR'nV l\ ¼98þ@��(‡¯�¥æ Á#ÑÿJ–ƒv­³d�Ž·Ó _IaïöÈj踟"lÈhQ­c6‚$³ÑT„W¾xT^Û“Öü"ÈF‹Hôca°ÎVµŒ1 ’?º–9“}˜€Š'_Ðv z¼îÂzl-!\„Á÷ ùbxä}ÓXÌs�ÏcüÖ:@ŠGO¥Fœ8iŸl°–š=÷ ØîuM�"íj ôô&ó¹\ü$e98åMz@GæÎñ÷s3èÊÚìƒMŽõ€4ÃãR`CÚFd’[?ÂÑï QÅT™d˜SÝ"ʘøb»ÚÈ6å"öãš–5ŸŒ*_ Þä?/eòM””?Å”"[ð?7apH�tù°¥ê§Ó�þliÓ0ý…%>ð/ã™ ÿÂë¶?)@;:«Â\ÃÏ·§÷ö'üù§ø0Ó¦˜yr·"'j¾Wð•^ «ÄÓ»7ìÇF‹ÎÌ>÷€³Sã;ëÇ1ø©Ò�´ZY»­×!Ô³7“á¹™oÇÎ<[Œ¼&¿w›K27y8s#™yv™y™6Ob �¦r½¼Ý­‰"¼Ü¯x­é:;¾{=;ô^´†rC­ÿnÎUMå÷I=ÖÑÞX²r¯ÒÜKßc@}‘? 6·/{Üz©å[Zv¾·¯±¾½Cz6K྅â.¼ÆßwäÉÍ$]Ó›/Ò¼ š¼�ʽNLÎiè†É5’Á.B"í9Úi‘wô&ww}æŸeÁ?ÏÉçj}l|îÏ(+'Ñkê^©rùÓ™>;Ì�K%q7:ÑYa£q)_¹=$kø®’sA7SÜþñý™LcBZ°e?8÷$½ ¯~y\$Lš^4/ð¢u”N”þV4à Êf¥\y‚²Ó©:¾ëÞð‹nňXîã}ÇqM¢fÈ µm¢X!O˜`r@GÜf‡®3³·™h-` ö«Å·¼fÓ©þºoÀ_›ûü{ï10¾Êu×Ú1@Ÿ[ô¾AØß¨™2r6ôïüûÆ(3Õiª’B—î/'/íßdþu6µÍ 7n7¯R0Kæ9€Ð7ý› Q²R"~-N€÷šçè¿úÄŠq‚ÈMpÆO }]'[X¢7qÊtôãôÂãê4‹ÄomÈõø´,FúçUjÌtª²Øz„Çà#JxR¦-"Áv*�øF$éu]Wõ?ÉÜÕIº�ÚD(D=':L[Àš`÷˜";š*0 úÁЦ0c�*À™1°6è©â&¼IæÊ>u˜ †]}¶/ØõùHÊ|�Qi˜ü  †>.  Ù�+ß5¼ê«"I­Ãïa~ЋaöeV܈4lŠ¡y~|š7m4¿÷h_Íëàùx Ïø"@~@ã8p÷CÈ{�ûDc ×ï óÕ>Út‘ˆu¸Æßö>G�ˆ]ìŠÛÅ=�;ÎÈIŸ„ÐÂúcÄî.à´òt ôð虫ñ\xUÞ ÷ZÕ?%€Fó—¸Ö/4~?ÛnÄ‘øÙ‡À„qÅÑ _¡ þÂ] ©ý”¤ñ‰Gú@xˆƒíÆ�òò±ÔÕ•Ø3¶Ý‹Ý(¸<¢ØÖƒ¶†í…mÍZ¢êŒ‰9I¥ï‰Ç­ #_ä…þr‡Ëv¨yÛ-™ Q",ö×ýöùÈwy>ÁжrVWË`4„EF=€ç:ê;¼Ÿ!˜'æ�À^…¬ ›z“7$nÜÉÓqø^Ûq²,±YÞZeg¬­U!»Ä�« B»ÒÃN^‰Qo€<÷�ÐW"ãÌ�7ÿWå¥Áý(Ú'”%p Î3zl¦ƒm?Àg¤if)ù%\ú~êÓhOÒ¤D›†P¿Z³“öús[ƒñ¾Ç œ²2��âž�7…\«210eŠÇFü¬fh/§ Õwt¡"r®ÍŸ=ˆå E§QwCZ^J)lô‚µ*÷̆!ñoÌþF©çx&e—F£A0ªãÐqmÚ#ldhÒ-—IîÈ»/�+=ò{Vi�CÁ_šû£É/2 ût vúwE†~ºTàœ³ß;eúV%ì·ž»²÷�=ØdÝ{I€Wu€t‡ ¶y5mž6¨[ŒpÃŒüI%hîÑ<¬.vOh»‰+b#„}N—†p[g0H‰»9k3žØ7÷Ý£¶Ÿš~ÜŒ·°)ï(é�*TÚŽfÞ�hä(ÌTŸÁ¢cÌ�åw<1t‚Ò–®tÓ&­óUÏ¿¾;ôZpde2DÁw6ô£êŸõèæ/0t£ÊÌßÉ5¾Â¦ÉÈ yc°¶äW'aI8¢I¦ëà½wÿØÜmŠ$aÅÝ ÝY `q€«–*)#³<öþúÞaæA‹¦ÍÂ^òHÏÉçñŽë¤œ‡€°,‰“÷-0%©3Ç,Mˆ-B´ àFvªÎSd·mOš'W9å6¦ê³�™åã„4î‡Û»ïo_žsî�à%çÒxéàxª“1Tƒ”s|nk¾Â�F£]/Ø·ÙJmhšnµâZE$Ο$ÙBÜôB��ÓÀŽ÷’ ë ˪¼8�1pj4öZÅàæ`ãÖêíWuó©«®ÙyÚ'W½¹3Hì_qñZêǸå0Ëkx�åK68¬©0óãøó+™»[*NàKÆ?RÖJŸìW°ˆ± Pì°�0pÄ—í:0VÅòÀ°wù˜d_ûü‹Ö°¨×÷¸�óHæ²7Ù^¢2–ßBñ'àpýŽqmÖA¶Ê¬ê»›§Òhb9ÀŸšn¥4(ý“ù¡$å‡ÿÕ¯®¬¡TTXë`´™•®ÄQÊ]ãqS^úe1\þýk_ÄÀ,+ƒ87f-–ɱÂbÃÁ8êÇ:T5Ó&I„ƒd!±ÜÝvȺD¹�rçzT+¤FŒ!f›Ù‘S“Š�J•ôÒs¹Å%~Ö¤ µT½s~üï E§èÿ³¹j®àqP5êØ;`AR\lò œhiV `5†<ÖŠ%/bÖñö„qÞ-Ÿ›Ðw–ÆÐ(ànÏ @_ªÍ±® tÅôN¬á2¿M줃Àð\¡ƒ¼«ÝÊ�j»�Å2‡Î•VeÔcÃ*Ÿõâ­Ÿ/ª.tùY² žm”úAe»;î‚“ûݪ¾ÈTšd6è�ÊY ²ÒÕ––s6´`„õ)%ÈÌ@pžW™•Ep9Ȉ;í›cÎg^Õh�“-®™Èò&%eõ•1ÈÌƹ O¸ÏJÊ‘ƒ¨ù\EÞZí�tè-©0�ÀdÞApå#J‘hU°A‰g8ÁöOñüˆó\àlÊ\¶ÅÖ͹�··lÇ…JÖXm;éJæä@ÇížÜž[rdO¸ÔXe‡ìÏ‘Ÿý�ñQšS¯£gmÜr¢_)õ ýä`F6¥­×ÇI’H=›UJébd�0?ÛÞt¼álÒÆa„�N3åõrÕn÷fÉ$2&*�æÅ=¹î®Ä̺°c2®�P ”Î�q˶Χ˜oPH¶©¹7+ �ùù×Cñµ«0a4ä%S�Àb"8»«s>Îáìu6�áæ×966jfî¾|9zVçóEÛ“Ã YDIhAÖÄ¢\t yà ³Ásv}vèMá3Ú§Ú;ÏI¶*<_é¼;!Ëi:øµ’œ‡�wxŽó°ÔœKÍi™¹®lï­„|€Tüߟ‰›œ”†³Õn>ï�ª˜½h( d©Þä\qãd®zþUmRœ˜é:Õ÷°™¾/„è'²#¿há:.Ò88ŸŒƒp ·uæÍ"ç(FvÒsâ4•N&â@åðî(ZRÞ+“Âð7z瘩6É ÷œÖ·þ±èñÓ¯ÈgŠ˜á-Lñìû ö=ìÓ%NcFÙ#¼÷çÂh³¼aŠc4~wF3„™vëõ(NÛ> ɹ�Ü¡[½xµG¡Mƒ-44—L·Œô™iLÚP–ïX@ÇtŸ¼õ6yÇ`<12ªWòÍÔZt2¾Éç"°{ ¢ ‰xÊ5¸‰4Ûšl§fóDïl EåÏK2gЄ±� £Á¸(û÷¹=°’²èA–zÏ­®5ös¾T¦æn¾(¶¸Ià�¸Îê£RXÿ‹кÒÒµ�Õ²ûJ•ÛVçswñwîzጀ5Öë<0iêŽoMEþGÜÎÃMÁ¶[×ô±h�ñAþ¼5µÂ( Ã-Æ=öNÒÊ¿¶vmÿìº+g?×ßþàŸ† ”G«J‘T´ÃI $ÿ:‘¿¢%3ƒ+�±{"aC¯<2ë$Æ\PXa÷9’A‡â@x8ŠÅÃÁC§Ò­ ®X£Z·è¡´ýs:¿4)¸´H¤þ œ¡†Üˆ•›ã†%þEÕ•Äâ ú‚œScŠÃ2µÎɻŠŸœ"{g´ozc«±RèÙ¶ˆt„c§Dc¬N­›ÒñŽ~‰ÖH7µ¢ÙEúø—ŠÍ›t ëtÿþz"Ÿþg<‚ÃÒ,¢ð)ëAY—ù�ty©’z¤gˆm‹a·ÀOnC5õª*¸ÞA+fÈ7›¸Ñ5€ü5èŽî?KŸ.�YXWöF×)»{‘»¶"Å„ü¹üEG�Úh8…‡4]¸&bFjX¸fc'·~mZWI–âÝbx ˜å�­í9k&W$…{$Î�é!Zýr®áž°ÅvßôçZF‚Âuè åˆz êðà´¹ÖsêìAóå Æëœj/3&4t�»ßg+\õä‚m>Xª�#WñÙ+SA*žŽ|£k®rÇr“M2޽¡KMiæÿ�i鎪ûCv®tD£héEÏâäð¹�žÅÁáóxô,Ž Ÿѳ85|nðEƒÕU¾®Ú F¥§{‡¦ïÜxG7�Ä‚,«¯èÚΦ—Vú*­Šý⮓ÇGÉ îïô oŒˆSb½÷´¦ù/ŽtXGªo²Å=¡ÅAå=•Ô~È#»:±e�×Ä<㸣lCŒ±HV*ˆ¢¼«k©^Ù�€ð˜¨õ|dãyWɈG°Ùs¤çóͱŠfÞ°fGslÔè]!+̨èï|qpÉ—e¡šsÞ˜5Ü ÈÁÁ7Jö¸G0c†YfÍGº/¯Ú\ÀúF\c*oû{gÝÃÛt i]Ý઄O]^«ì ò<ÜŸ”€ñŠdÕð-|ýÁ|3>ÝTk&à-Lµ£Ï8ñºA®ð£\ç(� t )' ÛRöu$ÿ~PyÃ@Ê8ÊGÛ,ôΞ±¾ þB�7xô¸i¯;àö;�a4dwÍ·6ðJ_jìÝ£È'ÁMÓY‚iaœƒgÔÌB^êg­Î|8cYiêË'úÞážj9û`›mÜãý2é\x„Äë·)þÌãOšª‘ÓL:|ê“AúZÿXÎ絚'|Û2AS-ùúNPa`7*±4{²X#z?ð¡Ã ƒŠä„‰;&ò\+r_÷އñŽ<úg»uÏþí<Þ·Ž”é»Gþúû3öÑ£~Áy¸¾—àœZ쀠}ÖWÐEzrJÚGoRzëe PU@f[ÂD„öînaXgÊ*ˆŽ[ ©3Ák,Ò''Îe^53õ�—ƒHêo1ŸNl9ÂKàyÓ�f.6´²h2-Úð7xwù²Ò.„Á=¢QÔ�ñÀæÐš,³dnt¹Wý——ž…غí Ý]éˆ(�þ^+±ôö{ïN†%X» ¯ì¿,‚Ý€<ê«ð]Ûe²Ö(1XËf{£q€§‹ë*.2“㌹ÃbÖ/Ë»¿yFƒYEDÍÙwº©¨Ì.ñ` »]·2„n�±øL ¡9îIçù(Á÷sÙ°…�†¾ÁÒ0’· ȱGøÍ¿ÇÖï° F±÷fÖ71Ì̸…WÆï"àטp •ïß0ÙÉ<€¹÷cM„�¦<ÐðE w¦¶ õ«¯øk·Þhäú2/}ÏêÅvÁ[O¸÷"�"ä@ŽF:­@± ¼9wèåÄaÉ…hnaô´ßÎ$ÁùžÉ›°Ô˜^sZS­l6õÀöBîˆO2weeϲýAÒeI䨩—*ÛÿÚ-ϋġÜRÍ€{™¿¶U¶šØÜ´ÆÏÅ020¹MÒàb]°ð�p°�ýíƒÁ~&Y¿�»©ìíŽ#ZÓ+…ª¥>LL�ž³ŒÁŒñ"Y+À&þâ Ë kýS®Ä°óºo')Óã#ïí²±_©>·a�fÏD0À»Z¥]�Ë^lÏi“Çx� :Ž¡i› ÏLÊíÆ–Êè ÌýùÅ‹{/õ¿‘wÉÿ ßÿeM  › ›¬ZJÙ8o¨xœí]ë�Û6¶ÿο‚›� àÉ4Xd&LÒ¤[ MƒÍ ÷‹E K´­Yrô°ãþõ÷¶º›XÉÃÃóø�ÃCæúoßüøêîçw¯å¢]7âÚü¡’ þXª6‘é"©Õ¾xÔµ³‹<º„çmÞê¦M¦…’·«U±•oº2móªld[É;zñª*[U¶Íõ%.®/u·Ó*Ûâ Oo²¤MÚíJ5ÏåÞ½A3q�È2Yª�¨Õ“Y^fóºêVÍ£›ëËû~v3|-ŸÁ›•L‹¤i^ÛâTIr‰-ÚÔì–£šIãK²žæ%(h`æšU‘· ZÈÀÌõ¯ö6s?»fÎmomΠ̰Ï_ï×·'ZÁŸŸšî¾Z€ä«+ÝñÏ?xöðáh»‘ï±/½²è|PÓ*«† b4,þL» ô@ý:îm>{æöÔE9·oØò‘²ŒXA.i F¥žYãfL­¯[ò1øà�—u§)† «À óã½MðK×›¶ã&îöhƒ{Âhú×[˜¼÷à'„ÃE ìíí˜9u¡Ú¡Ø´ŸÏbO~fµŒBù1š&øÖèbIG�9C4hòÒ7eÎР­hŒA Ñ6 uýØH€ùè‘tC ^+† Ý¿Übd�‚ç¡ j€–_ö㣬g.ÏgüîýXó‹7 U+c&úNpÂØQDûäc°tÜýC^ è§ŒpC[«r»¬ºæ¼·©ëø@øwp}À÷UÅà;4‹·#˜òéÚG?D  rYeìQÈÃÁ'¬ °=sFгª(ª Ì@ ‡¸ŽÖ=¯›çF� ¥+z^<ÒA%ÊZ3ϳïPF~26ùÌj-|ße7òn?zR áx)_rYÉ ’™ƒÓÅÈ‘]\#p—(òÿ’È¢š#ðÐþ# ×XhÕDiïµJ¶çμD¯é¼@`^¦E—)Oþ‡ô ‡`Òròã�^Øi£Ö¡rTm,’|üË|>«¿ÙÅêˆÀ–U½“å86ÇWâÁ'a†ÿz#B‚8b¡u˜ìX*ƒu` Ý¢•Ú�x†¬˜ å<}A7�  �ÔÊÄö:ºö>¤¸K/OÓcÓåˆÅ5‰‹¯ >n5÷‘2ñ.™·†/Ò#ÖÄÇd>mp,xÒ»ƒTƒ²Âåý‘ 雪^&íˆÍèåÐÄNˆSø�;Jº¶2‡vZb¥|êr aj[™pûŒüÈYÈc�íST‘I ŽŽ˜oK±b6yxœÍioÛ8ö»~7 Ì$€Ü@�A“è‰�›.¦™Y,Š¢ -:æŽ$ª¤”ÄýõûÞã!êp¬Ø�b�é8–ÈÇw_|¾úÛÛonþó¯wl]ù<¹ò‚gðQˆš³åšk#ê—GM½šýrt ÏkYçb^óE.Ø«æAæ’ë û§¨×*3W§öuruêÀ,T¶A çóŒ×¼ÞT¼`[wò䊳’âå­z¶äõÑüê”#�‹yxk/àQÅ–97æåÑú9[øo_qÃReËìêŽëy½È¯Nñö’]-æb1gÇô2“…}™†ÅçýþÁOk‘粺<¹:¥S®N+ÄdÎÞ¨ÀŠþY ã¹*oY½ìVÞ‰’ÁA¢4R•Ï»-“wž„ÂÌžYHýÞý{ œ°e‹©!ìdY5ué€*ëŸx¿–Ë5+S³…H˜�A³s¦4»H™‘åR0î„Ìó{¾1lÍ |YçVß«¸ñŒT‘çC¬»Hß ]Ë%Ï/3&ü×FÞñ\”@‡J0\F¢¦oˆºÐp‰ŽºØqTÂÖJËoª¬·žæÀÁºoíaÏ»ÎÐå„i\¯],FJ⪺I™V÷öO‚oä7à\Uå›”¾sOÎÙÙGQUBã»z DB¥ª™5Xà2ˆƒkÍ�×üO�ïXÅ5,+Qرø#M<UXŸ#i`��Ôì0€ –¸Ÿåý#H¨K[�NZ­ÞËà~ûàÆ ¡oQ{œQ-6]=Šð´¢HY¥Õ�Ì„*,ëØ%("¿°1ÏŲwPšR~mDO�˜Õ «{¬lŠèƒZ¡’Åæöklót"ÁoU‘,Ü€F‰š;‚‚ƒ˜¸Å €…:XŠ¥0ð쬾Ù�JgBƒMü{ n0Rw^iaÀ0\_4y-«¼ë‘R#5«”‘$VÐ÷‚×Ë5²JÑ-DµJèÉJjSÇ`àðÉ÷à·|í"™~– Ô¡a½˜É2 7ù¡©ÌŸ´ùÚ¨Kï^Þ ³Ô²ÂÍ^)@2Þ–7B¿…˜è_�ð+tjÐPy[��q â(*U9EUo@êy#5_i@[š¢دñd¾yÛo &6`»b`¤7¢+2Î…@ê8³pÀ­/k¥'Ûj„�Ge ¶ºÑ¥—M­áßÈU�½ö½Æ/Ž¡í6o­ÍyøÖÀ,V³H÷9À ¶™)aÝ4¨.xè‚#�È�ž,6å0©�Çeõ>øßÔ1>œíx®DáeÐuVøö Ó ŠZí;ÏR»`#¿6<c$>eä$ÚÝ��¯:�áõ(3÷—֔à �$CçõP|Oî’Ý6;£ÝSÈ&ÇY®n1[cþÇuÒ%Óg”rÌ8ï1^ j8üãc)ÐÅ'£çLÚ˜ƒyT Y³†#<|<õ²+•4¡¯‚^—μ5DªJ•j«„k•gÆ2Õúd“ÆŽ~ÅsÈQ˜+ߣ@üG€dPÉ8þ…óð„ú x@RJC*ÍMÒš•UŸÀcÓ@@P Üòs’sDH 0Êúvüéü’]|2$°˜³†ånµ]×Iv= OÐ –{½ø¶ñÒV%(Ö®QFXò€}Éhâꨒ’VÆÞAn¿±ü °ÉǾÀ #ä]™\­„F´é@€´-8§ã�9íSõ{)kÓÛãßEPá`pß —REÒbŠ¢F¡#exeǃ{ͯÃI…4Æ*ª˜ € „¬[UYNÕ(φ�”»JÍ ¿ Sî$ d]#Úz ɇèiXHe0ñ‚Zªàe4½ot{0¦nˆ Ì4PáH‘¥¸%Èe‚I�ÿx³Íñ¥ÎjÙ’Ñsª£n†”}MBy·¢pT“->@.Ú/Iã@ÿ^•(ÄÛ†¸Ü©Âœî¥hgÊ‚òÙeÞdHíÆ3Íñte²e.‰Áð§ÐZíÌHLÛ"ÞxÊ79 •?4æí·š¯cU pbeíÖsH\ç�Ã{kü€ªï¥Z•NìñVcë òo‘Oì¹DßZíaé´‚Úš&Ô_CìíÚ”ý)Dåï®�%ÜcM׆µº¡Eo[HÌ%Vܬñ¶ ì` ˜Zf†º[4ª¡ ®á‰Ï¨{GäÑA`AÛJäNƒà ª‚÷u¡öXˆ¥*ÚöæC¨ªÜ÷/çá ÝÑ+{¡#:‡ú*4ªßÙ¢’ÔÜW�>ÚVVXöu DŠÿö4`7:*;Ft‹š²—ììsÊÎO:ørwà›Ð*a™V•»÷u!﮽×z õs–é=ì ºß=ÆîóøP‡kFæ-e®7Êú'n ÝP¶´¶ÒºEßn�´ÍŒ‹‘Ùûxr›kÄx‹þHB×vÍ·ÃZ]O?ñéÞ³KUIñ$GA&ÚnÛå#Æ 7XnFb:†ÛÚí‡r @¶‰…ͥѮ†s£�Ñr øp²6xÙZÉØ­;SŽ�P�°/Ó §½i«¿ü$pDûöi‚^~ÞŸ‰[ÌgÊ©mcìiç‡"�zŒ˜vM5œ8ƒ›Á‘}²g¦V3J¬Xû $1ŸŸ%ì&ÊÆ¨å&Ô>­…¼]×Çjñß“¢EV¯éïpïÖŸžË·éT'iµ3gÝÉ¿¤·ñ2¶Ð[Ê6]³îÂÎ�s0éî¾ö÷ª×o^Æ} ïŠz�[<ŒØfÉÙã®ák#Ęw°ÏÎùœ/>¢i…"va…Y«r¯bÍçð®0rÕLU.¢G’�ÈÀypr©­d]B¿ÒªèN8Fÿ’îÌ]&V¼½£øMÝ“¯­½ä¢öv=^à]9@÷�I4»hÙ­¢¹Áÿ÷1ÂñY´í“h º�ú©‰Î.x´¨Ü/‰š[wÓ¨ƒK‚Ý'n©;æì …bº)#‰>­ÉŸÜŠ$ºâ=›-6³³�AO�¶›iÑ9Ö% ™ºÄm´vƾ€¸%>1ÀeªªÁõ~¬€µoTÃRýM0­gŒ¦º°{�%)DDÊÁ°¦ w×1aÏSvvÒZ¦¹Ú°çÑÏh{Ô²ëP×è0 l¬ƒ¥ËØ0`äû-dxxÝq X¢»–D¸Ëuk3ÕÐe…]M6?6GfQ;¶_~þ)ÖÏ¡�0ˆ]ØÙG úŠìZ4Ô í”�‰õÝîÅÌÏ4ÅI¤‡_ÎOì9‚d<ÛÞºÏékJLº©5¤Îó�“h£À¬OÝ£²aÎÖZ¬Â�*ê"ÿ»»™rWHŽ,ü~!õÅýB*ÚÚŽ‡‰Úq ›¢@¿–�åö'«�GAK÷B…ÉÑœ>l”ˆƒÉ©ûØ©ý…Ùÿ½�8A pp� LºH�xœÍ\moã6þî_ÁÓ×; »ÁmQàФ¶»-Ðk÷›l÷É %Úb#‘*IÅëýõ73$%*–�ر�ژߞyáp8$÷ú/o?¼¹ýïÇŸXéêj:¹Ž?‚ðS ÇY^rc…û!kÝâÅ¿²Køî¤«ÄÔI,ŸWâúÒ˜\_††s]¬±›N î¸[7Â~Ï’úP0¹æLñZü�uß³éõ%Çf¯Ò¾!7¹nX^qkÈÊïØÂÂ/ ë A® ‘r=OÛΡGªq}Ù`/SöÚ¾fzÁ F[qð-[•Ú fôÊ2n«ø\T•(Ø|M¨_N|ûBÞGµ}ñ]ºì)c¹†v¹³¬N½JèÖúA¤r+´µ‚q﹑ØÄ2ñ…ç®ZO‡Ñ™§(à§4+´°lÞ: Zð¼D$ D WÒ•ÐqÀ0Ö�&XÁ³ø%c·%‘G5<�q$€&ðkL›¾¨5ÜI­BÑ„ÝiÚ\0­•¨…r¬{¾`\Ì•bM½¯Jî0ÇP,@¡´u¡Í÷„wÂr®Ø\0+�[¢€Ö•\*à8öb„åuø¿*…òý–¼i€ŸE”°þ³ªä� ��:$Özº7ÙxÑUóLPPÚ!ŽVÉ?[q1aV‚\hxÅŠ¶©dl!pV› UHµ„J�ˆki-~éØ Â¬*Æó\4.PU #ª5«yÜ+�4F,QÊR¥’–]‹z*]_BÚ³¥“?²\Ü ýÛ†çÀ#ù„r º0¢!âH©a|Ï¡[寉&Š¿1$êÖ™ú¹„�9¯&¡É ç0¤ã,Væ b‡â‡1‘½\ ޽½çïãØ¤¾ ¼  OéwºòX°ŒAϹ7mŽÊ cûú&RóM¢Í¡¨GýMÛ€ ×sa,ôN8‘­87€qíŒÒà´f¶Ð�b:Y£þ_€ªÉ(õ*hV1 ³”f&4…^œ5(ukªõMâŒ�ùÙ¤‹ª ­W` –!•:F–¤`š-P³æ<¿ƒþÂtñ,Ýä'³]§P8£ö;UBbs¬Ç‘döÈn¢Zi,»bsø… a'[]0w�ƒs˜5o.‰Y~ 0ÞðÆÁ�M K�Ö�…AW¥¬DÚ`ˆ­+è ½/$»èE2!n{¼5J%åLÖ¥Bv‚uãpV:#¿ {‰Z›I·gX¦ L€ŸoÚL6 8Úo�Zk"fÜ3™H{©'Þ€‡ÏÖ›;²õ¦É«yI°¡Á|ylÅ(‰;H¢A° zµh9 ËQU'l»Î’%D½ýгzPæÜ ¶29sÏ«­Ÿ‚µ'÷ß"“HSÉV=Î¥+øŠ$ó¢Ð¨>,qWÔÆhl#SÊÙ°u)¿�Ž�ü,)<{²éÇîv m¿Å�ÖÅ_3½vEØí<Þ[£Oz…N‘ͦ1å»»"ìþÿ­˜ÿr ã=gÐPwK¼í’cÃâ÷h7É$¡É;„ÎÝËë&*¥õí�T8ïÝ×ã ˆþh–øÙ#‚“>n%`•2Ç•ÆO8åAüï*aaéë�—´hy“™nó�€æ­ðžÍ£$3ªÁóJ*Âg+Ì[XG²iL� þº(hÕåUàŒT`åk~<o£õžÓ æÇ0u5üV÷80~>Z@1ÈŽ�ˆN�Í|‚�²;1ôþÀq�$š™àI¿î†5¨xL`Ÿ•t "Êî„ÒúÇÄ€+œT-¬Í=�þÛN4yRí�Þ€;®ë~ñ̦¿ŒÁñuÀMưŒã¤D>&ˆ0D>º½KŠô2.ÉãËúòbg=>¿¸¿yàܰÄ�öœ0"qÒ ó€t…ÃniÕz¹�Za4L‘%œ}6óÞÏ˦ƒbÖBó®‘ÚÁƒwP1›†Ä(Ý]_é&ƒa ™óªZ¿ÜG!ŸŠ¬Ôæ+! ‰=�A ùfËɰ®ý0Å�uØGon`¾‘/SòFl*–Óä£òçiÎî±R ¹,Q>ô;ÆŠ÷-†Ñ<ø­½Ú2kŽi% WfSúÙ ¨�®œ•…ŠÙÔRhaÄ…EÙQàýÄ0ì¤AWèg7sÂÅ9x£`>ÀÊC?»Auç°¤¥à&n¸)1êbÇÐk =1(#”L6 ‰Ñí(qçù£ÐE‡Z†ll*žŸm�fÒÿîÀÊÑCÈÎ+�ßíc·<`çÞ•'°ä“¸_@eFUš. :�ø°q*µ�Œž„w{hj³\€¾4­�În‰t^à ‹}€v?ߨn³ !#ô†â‡ƒüŸAoéýgË+šÚ˜¥XX7úâ V–îéP<Cu8Æ·ñ;€\0‡)†?b\?ž#X_iö¿ó]ÌpO>±~›¿—6zP #ù!9FÿÏ29"ÔI?wúü‹ç.Œ\­;�}z÷¤†z[Ííð†ˆ9VSwP»ä¸É­õ½?D—*×Üs"Ì�ž¥Ö�=⦀5ä]Ä»ùm ø;<û÷§ºA "#ANu!«ªãi’×Òª"ž>ÐÔSq±­knÀ9 ‰Q¯’ŠÀ´lEðˆ=ˆa‡Ù;®ø’üÁ`º€D_r€uØÖ:‹4¼Ã�ØjLÔ–Nlcøðš<RŸÞ1Íz`tŒvBh=°]°Þ Aá í3fÏ“8†�Îȱ˜ç–·e'–k…á �,ÉlÙÐcñVëèµÎÛ& §]¶Â,£pcrg=— o†\^mÇK·é´::PÛ€[â�vÉñC(du[9ùÂ_ê{+íhö±_?㡹¿¿qH|‚%.Ø/*¡í–0,9À~më á†Gx1~ L •†áZ£’³¼kàÇôîúÂê|øRò¤ŽË ÏÄå¨7<Žo‘îô±]œ^Í>à½@\÷h‡)\”ž²Æ2èî©m#ÕcmÒjR£ZL÷ÚⵕgÈçI˜ü5ËlêÇð|¦’ó qº¹óŠ©Q·¯ éæQ%¹@cï¯�ú‹¡Ç Þõ�Û Ÿzææ—§ámÖÓ�öœNsO?Š›Wxƒrí—W¿fm¢l¢rµ³�Fþnoœ£ø™õŸ™žc=—m0RʬG wŸ}dc–ðÁ6\ù [«RÇ}õA'Y(òý„ùjðºLÅ›ˆz�}:jQx“áu*ÚtâÕu®Ê¶uõƒã…çRà=Å€?É<®‡žÃê” J’›‡×çvóÿu|Ý4Õzös¼35szv›žèÐ�¢ £T—uuÑuëê²þöÑÞŠût éÙ7‹VeSÿ;º»!¸œÅ aˆ—B»[b { ý Ì`W³ÿÝ 3ÚÓsÃ]Ý6tËâ”l:Ì�«o(ÜdbÔÞ0¥Ä™ÉuKב’ÌøF U[]´$Žsï"inwØâÿ�û Y9a >3F¯B4[™ï_Kyø ·xsŠ…u§§Ö‡üŽÂmùÝ(ë±àD›ÇCM�}b19:cãŽd\ÛnGÌéAÿ¡%XGü;õßz֯Ϙtïêôü‘ŸA#¤RÂxÀ]ò¹¨/Øè?�IpG—!Q*§§E·.ÒÒ%�GK«ÎK ‰ãdã3;Õ_�¡ÝÈÃ@¾ã99î…T…7ñ3�éçúˆÛWÚ:ƒéÑNã3˜H Lql"X”ÞËÉñ¦=Þx9·»Óª`åCb<úFoè0¦¶í¤ŠçF[ׂÓÃnä½vÙ”~ž|˜Qâûâ:ê~Iä ëSxaº›Amÿw[㊄7»øÂé¡V|™MáÏ(sK¹ðÞb¼Ë9`2][‹ÏqÎ9y�• _gm’ð±u�ãÒõ`ñ‘§¦x¸ 2>2†�4~¤'Idñ,&NO–øB�;ExX0ÈŽ‘öc+«¢{®Ù†—²Ç>,›HAŸߊ4þ±_÷6"¡ãoÆþÙê«ðÆŸæEú 0Ç@É4�^’¦cFLl;fÌéæ3ë/T?ç¸ñ0‹S{ þwÇÁã&Ôg@–þ¾¢çpá¹�Ïì8b³Çjà«a›s¡ÍEUE°˜Þ +ÃçÅ럕FÄ>·f›stx}ÓŸk¦™sS¯èor ߨu*»AѦz?zR~™}莡§_.?t&˜ô4J_nïSê ;FÛðUøˆ~ipƒ^XÑp|oW¼ [&ìïon~ÿ[ȽOê ›b…ìÓ±Òëõ6¼Þe–|N[ Xu�†”ÞÎwÙl:ÌïÁWDo•ù§ä/ðjw’öÚÔGÁno½OŸ—ì¾MÜ)óeø—‚.ý?<ô?åéÞ½ ««šRMµ(2úxœÕ[Ûn$·}çW0ûÙÀHr Ö²€µ×A ÄkÖA8Ý ³}™m²5;òï9U¼4û2£‘ä‡ðZ«&Y$«N�*¹7xûÃ7w¿þø­Üººº7ñ‡V%~ÔÚ)YlUgµûêUï6—~u�ïθJß:CíëJË]×ît猶7×¾MÜ\ë¶<�Ä?Ý–Ê)wØiûZ.Eq£d£jýÕ«Ôåê§v‡_ì«Û›kE¢>¿�7bôçhÛÉ¢RÖ~õjû…ÜXüw‰_Fm™­øuœ÷ oÖ·ƒŒ5¦àž7×;’v+ÑÆ«µÂ*ÍCœ¢¶—_¼šôZI»Ó…Ù]Je¥’~bl]S{�.Û.6ô�r¦mbÃ.\Û ¹7n+ÛFK]éZ7Nn0D«b+;LÕn¤ÛêA‹WRÞmõAªNK‹ñar!‹¶êë&H•û-–ñ ;i·j§I„rot#M1¿4•yÏM<™Â šOIo/ÔhÌдN®µìó¡×ØwÛ9|Å4»N[,ùµ,û]e Ì)¢r”,µ-tSšæ]K¬E5¥¬�µô…{ñ’TUIUz™+îC›a絬u§«ƒ¬Õ{ éBš®Ó÷}¥:lç×¶—…jX”åŽuЭWÿ̓ên�»¹¦ŸW?&,f°òv¡Àèæ¾ñóÓ”Ê�4uXA³†L£‹¶ÞõKòâIÄÏNï‚”«€¡k€hé?;Õ9´õÔú¬gBæ`§ï\ÓYG 8 üÅ«ø…4$Éå Ž~YÅÂFß9tWU»·c1ÐïùwYiõ�tœmˆ¤Õ8†z; ^ ¼î½ Ìù&ÃK¥7Îc¢ii€•ïµÞõ$àAU= åäV•4Ž!@ÒñÉnÍÆYIuð3‘2Ö‡ ªn{8/¡‡½¢{x>6T½«ôOð¦ep¤æ¡#“2‡·ÒæÁYPJ#·÷ÛI|Œ†ÌÈq×Zã@>²éáЦ�¶PpÜx%XP�S, „ªEÝ� ”í‰õ°& 'lJËÀ‚ÙEòN½‹½÷[Ý0 FÞgêA$Ò5&G©îí˜�W0¨Œm~ÐV=ÐJ�œ�ùˆ íHÖáÉ+ŸRä\eGt5‚>céÍXf‚Ütm-ŽÀÅšMr–õU¶ ¸Ç-Âm�ø|° 2 ¶S­­Ý^#8ug…áé(XfèbÝc!zlk1Äè#hH`EG5GY”ǶÄîwûÀ<U·þVÐ΃í¡æ9Õ 9h÷ £Ê0¯uȳУڀg¶Ži\¿FææÉMÉ{µ#`s9&8P‚ø–4Æñ ¢€QKQ¼ñNaåZï÷ªc‡ú‹Þ3„P]X±k r<Ú±�ŽI³VÎA Ü2 ?Û“Ò»6Á”È0—)äõx±q1˘.ŽJ�¬…Z×kÌ[ÂKŽ¡dÚD®œÌƒ8x^^SÐN¬<p… `{ 2IÜí|$j�dn¬dムÛZY÷¦r´ŽKŠ£™ý·Šàõ¡ é‹òÛè{Å€M¤DÓ3RBR—X‚/ÒФþˆÔl¡d¬©�QÆ·�â–ótßôºˆædÁ-!?â�(Z0 i‡½œdŽóDL©£‰ %õ9î’4Jùz½ÃÒY«06÷ Ùm˜� ¾«”I‚ÒT)´©eÙžÑiZò{KÌWåäØmÛþž“$±†wJ\áÉá¾GÞë·²m+vM¿Y©µ®ì—³Xñ�&Ó¼J+†ƒÁ�½ÛŸ8‚(^ð;û¡o¿´äêÞò!ËEJÇš�£qEçº|¯´ú„GÇ ?œ`ÒCçöc.‹G®Ð�È`vÊ�“¾S�D}K|…¦¢™’´›N�³ÞËóÁŸ£ù�:øû#‹9éÿ Ž) ƒŽx,‰Ï"Ô×pçFõ‚Ë©™±5b vñS»¿XÉ‹¿‡lÊÆcÆÂ–=QÇ‹KG¸éÉ—^-�i¼Ó…<Ÿ‘X¿Å!ªãƒ2-Šòûêÿk»§à ²cxJ˜Ð!k´C‘–òÀ�½ûBYĈŸKœÔDÑv4-Õ+|Ò›UC¦çËü‰j2®ï*dAMò_­i<’ZÞ¥GÝPúã¥Î_ãÄ4ÄWÚêUVc -Àu�|§:5Õ„YZÑwt¶ ßk$mª«ì‘âF •í£œ |œRF=žÏ(1sB‰Îà“q×Y-ãˆÛŽ(D<‘7NІ8—72Úxƒœéˆ �ö5þ¸DÂKcqØ.\*'˜ŽJ„8ÞûÈÌ)0nuüÚnÄÄejb/�ÁQÅ iXrT�‚’zi¥ <Ó,sÿŠ?4ÆJ§À=–Í�z�Fyj( §¥`¢¥ÐêMÅPÈaÀõŒá5½âû÷æ‡k¿¢Ò {f¨?‘ Å3N)óCŠˆ^gÜÓ�Aó½CÛû:¸Ï2yúøj*xü¶ü,¯g%­e—r£›¯Ä´Ø­¤DßÅç‚ô8IU¶åçß¿¹ûÛ›¯A¼DÈ\&9v#Ü[ͪÁš‹mÛZí+ÁâtµWIº8kËòý]4Ž/áUV¶©0?Ûnz�—½’Áa©Rë«àÈW3� ©ù-_T`?ŽLxï�µy—ÕþÂ×Ã(¾\¥ÜS'~ã§$çàg••p'· 'ÂŽêô£ìø Br[È[äÆi§ç3ã\Òœ}óTE H'ÕtNÁ&ý4e²¨ìEv¸w )H.^›G^~g™,ašî¿%h£IüJv År?Ë03×Ö/œcÊ£íÊP7Î}0<8ø$Ð& \xTEƦOóü'Ö™CYm( yIª,©Ûð2‘‘Ý9Nqi ˆ)3i¼ç£‘$sÒ”I•å$›ª±mªÃpõ�],®/ò$ ç¡H%ZN §‚ãv£¼®^Üî]J�¸$I‹(fð“¥Z+§N}¯ÏË®¾êúxª7õ_’ ¿Þ�®Ëλ�г½=×ʃ¡¨"éo#èý½áa¢_û7( ?Æ‚ÒÛ äëC~ùÇ“w5~æ9'ø%Ä·J@Ñð}W)ØÀ¿PÇLI$Ô¶²õ¬Bôå+ÓU_úKâ(’ý³¾ô`c�mó!†_Mþ1ƒ˜D¦ø‚F:ˆð«€‹Òýµüìr}¸ü̳ÈðHR*,?ÞrÇGRþ‰Ò˜Í¯Ã?D¹öÿÄå¿BÛÁÒ 8 7¬8ŠqP•faxœíYm‹ã6þî_¡¦_ZÈnØ…ƒrõ®íõåË]é.”RJ�íÙØ¬l¥’œ4ûë;#ɶâ8—ÄûÒ ^[š7éyf4JüÙw¿½ûíç÷,7¥˜Gqóžá£ÃYšs¥ÁÜLjsñÕd†ßMaÌMAã‰v[<ãUÆns¾öqŠ›BV:ž¹©Q<ó*™mÉÀÕ<㆛í ô[v’&‰bÎ*^Âͤ•¸Ì¡Xæf2�gœô^ÏûC(y�#+– ®õÍ$Ãî5þ]à+É¥2 byËâ5Wóã=Ø ‹“y£'™³/ì¨1nøËxf¥ãÙŠ,ÌÙ‡ºL@1yÏ”ÜhVT]h—‘›•ëÆ“R_¼™8AçEÏ´³{À&S`jUifr`վݢ`]wT0®g:å‚«KÆ~2¬pjàϺXs•AU‘wJÓ–ìú0eW�MXÃjHe�©,—XÉÒªEŸ¬+šáfæè§ÉyÕ £Æ‚œÔS¦e„žunor©ƒ ,çk„‡,¨pJ¹†ŒiSà'+©Œv‘»e0Lð„œŒµQ²ZÎo�i‰û=óŸ"Æ_¹‚û_ Bä¹°ˆ\tˆ¼$®|Þíï¦ÈL>™Û�qÊž¨�~b—ßaÛ†0Cü òÀ› �÷h ~ YàÕœC‚nãF1¡3omŸG„ÐvdÇ^Ž ×-˜eAx®Ð'iÐ@ã�Ínø9²ß-_ {€6™²Ig¯Q7{¡NÁ@S?†ÈtâÇ€y’â¬(')Ÿ¶ó�ûÍ~·Z([í¨M¥¨ËÊûãœ(箄"øÆ¡}�qhb#/@@I$!Ô¯!5REHXäë@e"ðæé.1CRt.övh��Ã[³“(Hk�@^Ùwœ •FŒ÷„‘Ñï‚Ì‘c²À˜»é¶haXÛ(PÁ¬KÎ ¤C&1ëéÕ•éÏ^Ý*\.¤¡}<�º� éqJ6òóÒ‘W1¢`~3‡WsNÁôTY/;ëÖôˆƒ£+Zöu.C_ Ì»5æÉ0ynEžƒþòžŒ<§æäu íÉØ#ㇰÇņou Áë^z-tÔLqµ¥u‹ <+U†U¯šÝÃ&çÖ¶¿@kÛAâT×cˆšú™ëŽvúŠÇÇ0Rh(Wf;ˆ?6 'w߇@i5‡Ê�ª�ÝK…‹Î¬Ð�ƽå£9Ьnrè%[P+éûó?ðÌÜ/I£9ïĉO’GxœíX]‹ÜF|ׯèlÀ$ »å áN·?äÉ�BÁÌJ£Õd%�23{ù×§ºG_»ÞøãÎ  ­¤éšêîšj�³¯~xõâõ¯?½¤*4õ&ÉÆ‹V.�ŠòJ9¯ÃÝêÊ‹ïVk<&Ôz ¿ßÖš^Ø6WA·*ÛÒ«N;ùå³u\™dëqk‹žñ¯6… *ô�ö7ô1@ˆH2E­jôÝj ¸¼×. bµÉÖŠq¯7ï¼Cì5^u”×Êû»UõœJ�¸åÀÜ‹\n(»WnÆWº£l»™�¶úfx¤ãúëñþY¥ëÚt·ßfk�ÎÖo¿Y$§ç”=1¸ÉU]÷—I\[˜û‘lã/ž¯bx$zÊn öé´È•ï=…J'äìƒ'[’ žLÛ‚Oé¡2yEÍÁ9ªÔ½æÅäѦ‘ŽqCü%Ñ�!!#hdǶ ”;¦v{Bí÷‘ÂJë?–9¿¸I¹<)§#k]€eÜËÚɯH[Öì̽nIµß%Ôj‰¼uA)®¤¨Fmk›ïŒòÖ!z«Á@#”´rµÁÛjÒmáò¦éêžµ×1O§ý¡dœÓ»C­\JÊSqèj“�-ƽEq8 ¾HÐ×Éá8�*QQél+;ä&‘Â4<ØÈÒGš�Ö¨³þKåTö`10þèÉWJø¼íÉ♣NGµõ`c¢¸ßýŽMhT�*LTl¸^“\SÚžbwUr M¸ID6"¡\9×SkdžÅ Ü[ˆp&@RÆ{Ó~(‚*ÓzÖ Äº*†=8ÅÒ8QšKQXlÐÚ@¯£ˆ ]*n‡.Òü5�–Š.ÁÈ0±!wH£ ˤ�”¤aÏÍg:í�ë_òz?)Ö1fèÓ#=¼Stµ`­”BX,ü3çÿ<ØÛå¨ÞF»�.n„ñ�ÓÿÃBì{ÝK;d'‰IEf…÷ó¡ä5,QC ¢·=Þ@ 6T •ÓzÌ„“öªçîÄã?BBÈÚçÎlõLÀ‹9$¤v@HÇ Ç_t^¢?h¼4A$ZXnÛ¢A™ζ»ÍÏà¡joaÁëáQBÓÃraúoަěyJ\òôúz•uoe8 ?xF¤ô4LÁ°†Öðë³ÃiÚÿÌpš(=y8MHÿÆp¸y xìxÈ=b<©®cß�‡f–áG ©Âø`Ú<œ ©„š.•ȧmÎ5%S@Õ<Šç™#“mlɇ&ÛiVï›lï3ߘ:‚y¯œ:ößXIC¨aóCVqTÍ_X•ZxîÂA$?¾�ÃDJ#^�‚!Ì�ÞõìäSLû‹gñìc�>è§oîÿÖ³ÿɯ?�Wû4ωq|6ßV5Zó˜ÎçcÌ{6î'0M(Ÿ)ŽÄNA6µ^ñy¾¾YzûV"çcÊhò-ˆÔˆ«T|à™òñ:•�]q9š�ätufÞ"®˜„7Ì!l²½>—á;Íb¹û¿ž—sßMÇçf=ü¿Žÿ_ð70—–‰FN“ÙxœÍXßoÛ6~ç_qs�µœ( P`hC²‡½tCèS@K”MT¢T’²áüõ»ãYÖdÏi @[<òîãwÇ勵Üþ}óðåŸ?aeë*ciü¼À�ZXùŠk#ìõ¬³åÙï³Ç­´•Ȭ$û¢p£·ú�4ñ3XšO‹¦Ø’ßˬà–Ûm+Ì8æg²”ƒâµ¸žõãçý·Y–&œ<^eV\…ÆòŠs=[½‡Òà’¼)¸?@ºæ:³6Mè®!]d‹ Þ¹ºÙ<à°ñóæ~þ^Æ�_W¢ªdûqgúäM¿¥‰‹™&í â9)48÷oïú·K˜¹¿©Ÿ(Á¹·¢-ìÏñž×m%î°XG Ë×âq€)@°\[¢j„À^¼M!®|‡4O#z>s-iáðÑdüuÃc²_Ÿ±ˆêÿï) ó“Nþœá£}L·hWF6j T!ëನ–¨Žb³ÓÌsæ'rq׿ìý̯ñaÇ@w ü\¡c‡”r‡Î ¼�÷ͪ1Ð;k_ QÀb˦ÄhŽ‹=`ì‚ÜDç�ކNs‹ôEÃZä�iWÐ(Á@T¶Pâ¨àùŠ¢£_U0ëP‘]!h­ÛÒk~rKݧ¼îyŒó¼¾´B30"oT1e&µÊw[Ž™îé,#&¿ÜÀ»KH¦±<‡”ïîà´뱓»¨-¥ÔƺŠsÚ†‡•#Øì¦‰4a11šQ“|ýe¡%ï*ò× OB7Ê �'¹ê*OðË\Ù` 7ø„¼µ=f¥p:Ðt–ô—8ÿ*¶NâQŠykè8»]à\Œ~©ðžUbÿa/qÙõ1-~­ª&w»Üa œ{©3h޹q}­WÄ@:uÒ?\ázý„YÙƒ u‡i]P_Ç,žÅþ¥°i™cÇÑ|ët5H Y>ó¾±*�]ËAÿ{Êäü‹…1]ÛVRà)Á,pµuiC‡´@¢ 9Ë?tP¦/nìØÍ L+rYÊ Ëëý¶�¥ßL“S�£EZqßô.Ï© û»„!}åH˜v¢Móúë(üS·vëXÇ“ò­\9ZòÿÐþ\Ù±[∄]gÝgŸB-±] Y3ÏAßÐÂvZ‘7ð î�Äß… Æ\´‹éXHz£–Ù½ÀÓP™oØIbxj8¬´(¯gÎ磿 ?>¸ôùxÓ¨µÐö‘Ž÷9½ÐxÞÐß«Á„Éaz­0‡½0ÑôèŠÀxŸîûÐÛhà˜Ÿ;±ì°1K»u€@û˼4ÚOšeƒþ݇£+Áû>}„7,‰ƒó/kBq e çeˆR’ xœíWMoÜ6½ëWL÷P¤€ìE by×NŽiQoE\iÖ"J‘*IYU}gHQ«ýr‚$è©€á]Qœ73oÞ ¹Å÷¿Ü­ÿõ-Ô¾Q«¬H(*úhÐ (kaú›Eç·¯KZ÷Ò+\yÉï7 áÎ4­°Ò],㻬XŽS ŒørU /üТ{§MiOVТÁ›Å´åR:ü«j±*–‚‘®VGïÈöŠ^µP*áÜÍ¢~[G¥©fq¾�âIØÕú]±äO¸�b³š6+xÞ߯×yÜýs|ú©X°bÙ9¢¿ zôßÕa?Ö¨”l¯\¯`�Îï($÷ÆBÀ”~¸Ìâ¶J>¥wñj-c|‡A�}Iú`ÑwV; Ì£,ÉÊÑ?a³ýTsèkYÖ=úô¶Ã„ÑרÁ×8Ïaî„®ö¼g , GÚ'ÿÁJaí°[´¦x.2‘A\¬$Çh°#`ÀQPl­�ÌJc-ºÖèJêÇÉØAmTå"±´ª:‚Îbh!�­PnJÉP¶'"ÿ\¦zxñ'’KëAÆäËIù”—ZkZ´^òöZxŽÚ¢ BWZ¹ 4PÐÆçàÌÉša“$ÃTïM6ïéX+=8%´ç}�s˺9�ì‡öžº4-föA“ lÒ»ª;gg´—º£99ÉÅRiÊÎyÓìÒaMpõCfª¹uÐK_� ïÅû)”F:§ω€ÄT„�6M²g侮!9“↠¦ e w§˜&”%´ ºNMâŸÏ k訔{ÒLôr#xêC§;Êñ]g÷�;ò>À†›ªm•Ä*g³©˜§�ìà, OwçN¶œá¢tÐ♓7ôU“A§¿Ó:,Þÿƒèü ZŽ¿Í–ñWß¿÷¦Æ#ŠQ”0 ¥xœ¥WM�Û6½ëWLÝCÔ»NIÑZ›~œZ´H|é) ¥±E,E*$µªóë;CRý±Ò xMgÞ¼ŽËo~ùóçíßý �oÕ¦(Ç5}´èT�°ýý¢÷û›+Z÷Ò+ÜxÉû;…ð½Vú#]ÃöØa¹ŠFE¹J`;SúûM-¼ðdãÖð 2.JZ´x¿˜lo¥³Ñz±)W‚Aï6WvéümvP)áÜý¢y {GG*SgÞ¯¡|v³ý­\ñ'ÜC¹Ûd» ¼ ÞG‹Wå* ”«î�þnè«ÿÿ·,ã�^Ëë×n`k{„¡A ¾A°fl:0{±Â"àju׉ ëÛ"BÔòiô½u7o5ú}îìäé3\€Eß[íÀON�Ø@#Ä }C_\‡•o@ìœQ½Çàô’¢žì#Je´Ãª÷ò)„X¤9(‡�°Âc »c À‘L`/ÿ¡…ú@—l ð• ÅZØZ£i¯´9±_‚#oâ>Ñ•q¨Ý€–²+”ChÐbT¢Ý›X{Nº#);TféþjnŸUÀ%ÙÅ™.rÎ 25ŒîÛZææ  FS`¼Ãa°1˜å¸X‹£»XçÕâ:“õ§^XO|NDÍ QÌë·Å5O EÊÇ©jB6‚fH;ÌEr#ì¡oQ{ÎPK s�Ž”°åô±-%Ÿc× ¥ˆÒ�&’(¾5ÁAcz{"‚\ÂRg×j‚ò�Ô@’G–oºj’]¸+f†üw½¥¤˜Xº“ ©¹‘”TT¢sdëbhdÕLAŽêw ѲÂ+kœ‹(yhÈD<‘Swp} _Áöš³;ÝÇ©dÀ,ª+IA&“â<Ú ™Pg�¡Òˆ;äÝD!¸Æp²cTä+“èü2 ºž"Ê›•ÅŽ¬Cn¿Îc-eˆãW¼"•*REò=¡8§ñdè6{&ÕâÆÀp̘v­˜kÞš6¤ivˆDc±E.#—z†#•JPˆ¶Søžê Wk|J6uOe"�Nûk¾‘€¿Ì°ó†ïŽ¢SD=·‹ÁñúÔ£®(‚½QŠ 0¬b"7 �Ÿ™V:zØ”õUBÔFßÄ�\§gÍèú²9¶Rý§Õ—¶X3y—¥T!ìDõ˜ Âó4=Y3±B²@ÙAög0³†\ º˜4!kꘒZÄœö)ŸPK'q�ô°ë%E*ÜØ§ë×ð]P¶ƒ—¯×woÖ?ýðjŽ#íüÉ<é4QóQü˜Ç[&Q÷œ$FöÒÝwoή-âÓM¼£æuâËYëyø€”\å ncø £�Å}>ŒòÈüíôu1�|q4 ¸+šµž™d³“WgÙ îÊ4ûµóf†ö¥¹6 ˜’Ú‰¦†�—T†’Ø�ƒ‘aÅ r~¿nÄœûÚ3Cfj3'ãKI³ž— mš-yf4±:ùù¿$™? kéÖ˜gr2æ*ýÃ)[B~Æõu$;„=~Ü÷ºâæ“�?\¦/e²J?•Vñר¿Øy§ð uu—S®,:xœåZmoܸþ®_Áº@’61à€ÂvÈõr@�‹{¸EqàJÜ]Ö’¨#©Ýl}ŸR¥ÕÚqœ´9H`[$‡Ãy}fÈ«?|ÿ·¿Üüý§7bë«r™]u?”,ð£R^Š|+­SþÕYë×Ïÿ|vŽï^ûR-½¦ñU©Ä[휮7â{éåÕyÍ®Î#••)DóOËãþÐ(w!N-ƬìJŠZVêÕY?é…vU˜v¶¼:—Díårfë_b°y)�{u¶ýV¬–ä¦Hø½W;i—7?\�ÓOñJ\­– �ÕR<ãÞ‡ß\�3…«óæˆ:þ=ÇŸþñ»,Â]:—ÞØÙ½—âL‘ШɲUNèZø­Â_VÓÖN˜µ�ƒŒ_dau¡wï•{þíY øž2ÛszBÂ*ßÚÚaŸÒlÀq)¤µò öÚo…©U&¬Ù‹FYþ †Fd„Ä)0Kä¦l«šçuì/„·­û­²ŠÎ•õ#àª?º§áB¼ßJ*míÁË0\¨FÕ$DCfw‘ų^ËëîkcEÝVÊê\�y.ú)7é”ð 5Ž,:zOJÙÖ…ZëZO6þòx)4ª6Æ’œºÕ,Y U5þÀn&s¯,N«r¨_àŸógéÔ]Ýà($NâÇ…|jã¡ Y@<ˆR®TÉGÆ<'¬$9áol´R$V Ô�ïÕB8v2ë(“˜ÝÆX¯ Áš v £ÂP%oUw|Yz«ç&ý�¡0l«É()¬i„kó-öǾ‹À]&äÊ´º'¥2ÏâV5~tŽdÊ`‰�fÐw»Ž¢$,+šaçmsKRëÓµóA)ï·ª®Q¹^hDÖÙìj˜CYÂjײ-ýÔ¿IÑzS«ˆä_×â`Z±—µÞd�j¢:é{­ 1oD°G8ÅòRÔù¶S䜞ȄÕNÙCعã¯MY’ɱ¨lÃPŒŠÓ8Í_S�ÁÔM½Y¾S0´ÒDÏóø)Ã~Rl­Z'áט6~¥´ñ‚ÒÖ‡p7äÙrî+¥�…x8ÝÞ’S²‰yUçáYÒúßêí1ùãÑ;ö™�N脬ÉÒ>G˜M²©Øf²l*¿™4û°D˜»/ß"ìÅ|߯Cœd9±•. fì:Ÿ’ùÖN¥>D�>ò‰g½UgÓùÏ.¾é×Pô¤8Íþ!D�_°èbœ¹CV ܨ‹õpº×ÉQ÷[ãpþº<ŒÏK¬É~¢iíöÊB7ð)5„qç5‚M Äâ(¯0'ö—rÓÔÍb¹ÿ²“>Îy’mg|'ÙÿÕûïRŸIˆ$.óú~Œú9èÇ4YèêKí÷d«ÊR7—q§kI@+Ùú2ðGòŒ¹XŒÓ?ïäê(&ý¬*³ãôŠìš›ª)•� ËØÇCñTV=K§”ŽÈB̸˜©É›·¦,od“ˆxËŽ5ýòÝîPYO�|Ä:!rIÙ oø© I3±ä5†ˆ–"ÐK¹ÈFÁ˜�hÉ,‘1±Ó¢ÀH?5œö)…Š_ºO»óÛaŸ§ou}ÝV1²ôœ<\t5bR ΀¥M)s°D¥W`8ã°æ¦i‰`,‰…zÀ¬R4VåÊk(#p›EØM†EP+ç"T£Õûí!2=T)>··Y‚¯Y&§àz¿ŠË(÷LŸA7)=‘FþÕ÷]X÷ˆy‹™eÓ)ðeUë<ër…ýi/6†²%\� Îqpê·)Xy9�–p ‚/–xÏ Ð,Ô1�„Žw\o³ú¡£ì„*B¦µq%QÊ} va¡ÈUPï!�KÙÙ)¥`!®É×bÝFH"ßFÑaªµûã]¦�ŽóûPågýQbÅN‰uxˆzì{‚Ñ]<¶Cµ×(X•øªmRÆ"|–mAóï×f/Ñ€�FLÄ Ñ›â¤nf7E¨jBD‘(¾×V¹­X[SMŽáZ»Ó»¹hõ#Ÿ¯¦�˜cdÍqôéèhŽÚIsw“íAuÍG²‘ ›;"jdÿ݉Rè­¼UbÛ¬»`4ô 4Ž™˜ü8wQ`%~9Õ:ó}öø£ Ë=*Bf_s÷Ž{Uìœ'rß�áŽ:N{”;(†Ö^Ù=ŽçBW‡²ÈгAˆ}#)Ávì Ù4¥&Ge–7ãV¤KùC *8à§1|΢” år«WÔ¯ÛjEè¯Kƒ4F# '5¡W[©yÿ¦Á„�ÉÉmý49F¾’ª÷$_3÷Oe9‡‰?C�›0ñ³Hrޤ^ö¸!¶:p‘º7öØ�.z¸×7×Qõ\õtf€êl§M›¦å0P#?=Ž¢ø.)ÎõC¹i„™k¥7Û•imÏÐe·¤Ô´èh×Ð@ÿy` ´™Ûº’·º’ý2‡²�)×/G…×Ô´Q†ÜöÅI*€ N’“0ÆÇãË'¾ƒ! ¤9¾Aj©VØHê $yßÏ4¶õ�Kns_s#ºÛBNýþN”jíò‘ýÞd<ÍÅJ0ô0ŇV„cN hIÝU[t[íå+ü^)¾¬2²‹Šà�zÙ0A@tÅ7ô &*_ÄûJ.¡ãeJuÓ¢jmZÆ•.ˆ¢�„ @Âko†CºPênäÔœòb"sYRíròt’úN08΂‘»w<ðl#Tù2ÏUãUq™:l6éÀ$9þ~[SÚçºiݺàHAìIï"ë…<ŠŽÕ>²™@4G†ü¦.8(+�<¸h‹F& JÉfÅ€‘.êDÌÝ “½8]Ðm³ä2’‡‡[Ù¤æ`ø„!ô0—\¨Â»;—JNøzÀ*Ì-ˆaÖ£{ëEìÙpÅ�«²»U÷±s@Q2﯎ãÍ1A?º:nsÔîìtøU8ÝäÆÞƒŽî%•ir÷¯o~|ý]TpÐ,‚ûWd«|ûûIéu¼¬L¬¿GHíSš£ðÿÑíˆ †®ÀV°—]8ò r~§Ê]°ï,ÉÎn£ÑŽ:ÿo òwÒbh+¨ü0ßWc RQ´w+>ëÓ7ê |ìKñާj§ ¨â§®-KÙ`HÚÉÍ©) Ô¢”L”·Èø×P¥6Ü¢ÉM‡XŠÐ¹·“¸M±/‹#¿øŠ�]p'"ÍoIò±X�­QŠi0%¯�׹ˆ'!”ö!äæ_} ‚¿G"]8a’ܯMÂf¯A™b­%òä•Rÿ‚)¶4Sa ž œD¼Ð�«NûĺßZsŠßLE'[këüÉ×>ñZ u !Y¿à÷v>‚1Íðû˳ºªYðc>ÿ;Z“¬ÇâÓóœî³ •G —,ts0® ‚¹ é¹:üU©BË:ü^J»Id´…Î�¢Ùi怰?×î“„³±ìâ„þf.lAíô–ÞËA¥]7¦Ô®k¤Ú“M'í²‡Ñ¯ZŸu€‹˜$�‰òLÀÕqB[ttbpÁ"¢KJða·˜æ“Z� ·:ä ô<„h„gJšøœojj“%:ç¢( 'Š–tdSýLÔ“±¡+.XvÉ G´úkדâãÆ�aì«IÁçñïyx"üÔ§<Ð îQî†`U�D ÚxœÕWQ‹ÛH ~÷¯Ps/WÈ®i¡Pz^¿\ïà ÐÒíKŸ‚˱‰=㛑r¿¾ÒŒ�x³É5´[h!ÁöŒôéÓ§±”dÏÞ¾ÿóÓçAÅm“'Ùx!,äÒ#¬*tžønÖsyóz–Ê:×ÜPεî/‚¿kçÐð忣Ýù,�FI–`K[ìúE^ #ï;òoà+bœd[º›loq–g)*ÞËüá†x½”õV z7«^AéÅze‹ ç7�-óh¾Ìá÷l‹.gÎR½>ÏÒ`œ¥Ý÷Í!ó_}8ÍÇ–º×ž<©_‹êEñâ�IxŸíüɵ­ÿwáø+ûôÇõÏÚ…ÓáÿEÿÂ|è¨Wö‘,T¢\%ŽxœÕZmoã6þ®_ÁËíàÄh�Ь×@¶ÛÜîÛô€ûTÐm‘H�HÙuý=3¤$ÚV;qîP /Eg†œgžeö§�ÿüáþß?ý(Ö¾*çÙ¬û¥d�_•òRäkÙ8åß_µ~yýýÕã^ûRͽ¦ç‹R‰ÉFó‡OÒÈ•ª”ñ³i˜”ͦQØÂ;ýͼ�^ú]­Ü­xF&g3)Œ¬Ôû«~î�,Š ”ºšÏ¦’D~;?z†µßâQ-òR:÷þjý�X҂܉æ·b†Ésï?̦ôA¼³Å¼±˜‹¿Ä waÂ$,À÷7ÝÀWkU–º~7<úýu6åÝfÓú"š$ñÚ¯?¨¥mÔ×q›¸iséµ5ÿ#î–^5/V_×øÓ_D•ÏjÛ]¢Ï¸0î@+£¶<<ªÕ\Ü…ØÄåNx+äp9o²0­Ð›NýÊ]wV†í�ô�Êžqƒ:�„¬ke — %óµ�M#wB:hz%…ô¯•hôjí¯×Ò‹n„øq£š�°F‰ªu>k¹Q´¼’f'»uô­‚o-]g# úÅPÀí¹C>(^B„]òç­m\-s5¨„á�È1'ËÆVa›èœ Ûc£³t»V /ÃOí„�Ö°à.ìã×0qkÛ²ÈDnᢛj¿fÛ­(`€q¸ia­²,íš“3È,þÓ±Øez7¶õ¼ï  üëÚåRÿ&´q •¸d4âx›±8uIâÙaɉAåÈip´¿içµY‘žo™¶ZÀtŽ÷ÑJ:&œ<Ä&&À�c¤¡á…­#ËÃÑv!ÉŠ9¢¶NÓˆø]56±ìx—ÖxÛækUL„³ýEpIM¾j 2‚Y©=÷8ßX³šÿ¬ Ué,Âw‡2!€àëF-ÿµ Ô_´¿¡Üóç!æUÙ� 8?6JØ?/?&üÑb’Õ!±G¦ŠÑÄ•Ú4’»RãFÒ×™h™J{*“=†ˆ_x}r{)¤_ ‹ƒV'k$ U*¯øfg‰>4“>dp]1/÷ˆ‚ð1°Ò¹,Åc|Õ: 0k�Š<%è‰DGânOÆÆq&L+nSeâA©šU¯€vÒ+ ³P¥Sbe•C˜³› ãv/»T’PKf N3úF €GYbó¦ÙÑbí]ª*ô¢‘>p'™h$Á,A"�ÎUÕž³Dbo¯Jb"À³ƒ£ ¡ññ²‡“a€%¤Å4Þá›y¢¨[«ÆëxT—Dѹó¢P¸2¤úÔ‚\…ý3òóš)ÕÒS2n”³%.£·ðØGºô”–õŠÒ�:’ɲ…•ô ½°áþ‡mÞ¹zz:ÂJÿx˜õ$b=‰W'âÔY(u)ò{&–¾LdzÉñ\|Ú‡\ xm^ º'Cî­[?޼ I‡Ë�‚t*„µ’l3yÔù:‹˜ÐsB-ËÕíC\–ð@/ACãLkð É]3±a0¶ÈÐj>c¥ögÀùmÇ_ ¡¹QšµaLgE+;fX´//t<½g´;Þaò58}£lS0Åûƒ¢Ð+ˆÙkÙ�<ÁžºÇ—aOƒ´Óbþ„"ó Mé±=(ú:%3®‚\¨9û�‰qºýȪ9'h¶G#ÛŽHîjÑ�Ê9ääk?o£¤xPµŸ q�õü% omò²-öÂæc_ÿ,éB:¸¤Š·‘€:dû.ŒJþÔ³”›^^4" �) ¦�ÚiáªÇ£¢ô«Æý§µïZŠ€&�êø­n I¡ü‹PH%éÃè[’1ª‹Õ1#ºÕU®†É’øtwÿ�»}Ó¾+�ùœ˜Y¯;Oà�çAsxÊY, ™?¼YWØv£Ã¦ÿöÆpøáNHGÿ(d%Ï/�Y{âN-êÇÞs4 Z?© jÑ5Lªn–Å €˜¡X¢úëÔÎ:QÏ#Yd"³'üƒr2;£ž|ªœÓ–PO²·bK „I,[“s0P3¯ŒÝ+-¨Š,ØFèr¯¡-«U£“Tñ.âcFÇø >~±Û{� #V„^—·oÞ²ÿxÒ«H5«'* îé«J AÈù�š lø\/ý°•Ž¡Ë—XϨô‰žß¹{’p �_> ]ÕB#!:¢àº�A˜6¸ËI3ä¼êªÓüôŽV£¸E|TX�Ð-®c¤p %µôK¯¯s[¶• ­cÔgCÑ‘‰¥nœ�-úJl%µ˜þî¹AÎÉ÷Év|k „Ëž¹ÂÉ)ÿ¬Ì~f:=ØøÂoË&éûÉèÙÜÊ|üE ¶·å± 3\|…’ª5$‚qa†A©ªiy¼W°�1€CXg+õxÖ�ZZSÀZ+ÿâ'÷ß3Vrê-U„ðn;¾kïå"Mè¬ULüO*kBjîôcˆSTLËá¬ýøöù!ÂSßXpWƒJ|Ö”:�™ðL£CF.ŽŒ>#9°ߺ]0�þQ®Ä›Æÿ‰š†»ú/"Ú‡ •}V«~,‰xœÍZ{oÜ6ÿ_Ÿ‚ç.) {Ñ gc é8àÒM®Àáp¸wE¬$ª$å�ïÓßü, _•WÉ:þ£dÿTÊK‘Ò:å_Ÿµ~{þÃÙ ž{íKuå5®oJ%~3ñÞæÊêz'd�‹�Ê»õŠ·%ëU ·1ù ÿö*—^ú›F¹Kq'Øž¬¥¨e¥^Ÿu»/œ±Þšƒ;»Z¯$RýîêxÞþÖ‘•Ò¹×gÅ÷b‹od&ð)Ö×Ò^yÿv½Â?Äk±Þ\õ46WâeØñ†w|³^‰õªy ò)¿Ë?éêWüÏ<þwÒy+TYêæU89×Ve^›ú‰ŽŸ×+6<ø]–í ª†ÿÎá§Ÿááßc&]]çê ?ùÏm|M½a›ð…¸W˜­�}¸\$¼-×ב»Ê�Æo2gG*‰ç.ù³0®?ssƒk‹¿:Ü¥BI[j弨jëü…ÀÈuÂAž È¥mb¯TCtˆ$þu#ʪDèú2À…,M­D®2�«”B×�1�’ºU¢V×@&3µkK¯r`ƈ�UrOv긿ìw›È�"…$ÂÁbЉÅKà kÚ]AOI+`­dÌI8îâƒ5�²À²»€UíÀáñX}~*¤§„'4ÛÂÔ%hOÞ�Ô tF'U— {ÝV�13ANúikïÆêKAôÈ˲¾�ú›>H‰‘qjãa5¡åàÁf–8®MàÆ£•’µ« ]F±“n%�ÊQv~g�n£}!´´‰�3Û@Vô@cQ;Ò²’§,�Êq'h|£,hU•�®ø™¥ÙéL–âž!cE£³=,›Öóæhcéë­ý|{'½¾VAm(†#å%½âu â2Uçè}Ä7yÊTyä6•„óšUÈÊ –' @�Ä¡ÐàH™tjêÍr¯BÜûÖÖBB’%J½‡8�5¨ ­}‹œ–ëƒìÒE‡}!Iæ½ÁâBÐF\�´¤‘'2dG]láÌà®(€áu@üIÊï÷V-æ4S–¨ö=~‡šÄ|±—B0w†RÇZ'…¬…²η’Øó‚bpvàÎ�› ½Köu°­¬J0Í£N±(íb¾öHrANÚ'LÈHÜn`"ôôŽók¬s¢‘ºÓå;íø¡”™BÒ�ºÑ¶h½é–^⑹ÚJÈÇߤq ±8c-,˜ÝcȘMA»ÊÏ ß0ËÜ�MË™­H _­_mÊA¬!\êVrs¨{Š?šª59S¿S¾0�û$,êÜÁ�l ìrF4¹qCÉî’tÙ¹3 pò¬;ïkgˆ’£°: V‡Zç~²tX60–C)ƒ¨ZÀààöX8SÏ•«—Ó£ÒË¡W�›8áçtžºµÊbkMER#Ïàú}æG™ÉZ� ôšÒ­éL ‘IõÜlÏ\¦µP ûV¢"[j�áµkKLƒ;©k®S‘ &’Û½�Ìöis'XK|�XG^¡*èºuôž…ñÕ”ü`h,¨wW•"•&]…G 8 …UÛAópÓçØñ|Ö>cÇs�=Ø_{œ±AZâfgþ9¶@©˜=dBn@�û&b~(r¶ËjkýG«f{,^zL‡)sörÔCá‘ùxv"¦ê£ cÜ#—*)þÐ:úÞì…spá ”Ç)ç`ø¬˜û‚ÒÅüMƒÜY…Å’fè4’i«±€•ëí–'lä¯ZŒÅòkãS2–µÚSa Ãœ Ia�¥œôjÚ¬ E`ºl«šÔƒËYWV:„̈‰ &0 ½�; ÅÎÛêV¯YaŒ‹} 'Eðb\aÃÔ@N/9|Pǹ2Ç�änÝNlè�¡®çº;@û á<@ÅÌ¡Š’£ƒ„ø‡ú£4ÍG|ƒþaUÃ5ÎÅZ/!¾3”Of^A2¨,;„4c¥¡ÏU±§#dø5º``¨ß'Öæå Í{k•NB+ÀÝLK=V鱿hw[Ý–GE›-<Ú{DãíˆF6¥1ÐÁ›�ŠtÀPí¢ÞrŒaL “¦Ÿ{S0ª’h¡TÞf˜>¨¾ŠÓ°D2êÆAO®µ×�ÌþÞ1þÅg`ƒæ™0¸±nÁ9a7A%4{ÐØu&AwêA�o3•\Hhæ@!:LTãC‚@‘nõ?9zèǤýÌ鉰‚7Í~q"ƒz‹E}ÿÕðÃé‡=ÅÜô§?Ù0uÌÓó¦Îóu„¨(‹˜f¤¼9|E£‘Иð˜å!€+2ušŸŽØpCÂRjŵÒ¤ó}ªKã“8ŠýThÊ»ÜXkm¾EàE ¨³4æoeºÜǘ{à~Ô@‡K°æ-¹Oå—�D¯ú †¬÷Ü£ß sNÓÐòèŽÏÂ9$�î&“;°"” ™Ÿ<ÁK&#¼{LðަpI?†Û(�‰ž·Àq«öÅJ}iJ�i_ò8j4ÏMºXF¨CÕ»º+ˆ„^oKDæ-ÎC¨{¬ß8Lc ówt×Äæè}#6çî€ó)0òw¯%àˆ\¤ü+ÞZñ¼I˜¹Wˆ6’ ùa…�l/Ý^È Ê/{:i÷(J—Ò}(?ßÓ„Y™+L[Æ�£+‹7±úq Ɔ˜d˜•:í|íËÕ½”Uºd|]VwŽôÜÇîÏ6Ý-vècÁîѥϧ»£îõá}è½øŠßN=äÜÇMîyâÿŸzX ûÝÿL‘›­o²Ä|{Ãã|º|•µ›wVºÈ]²PoCNç�£b;>îpÛBû%”<›Öó�6~�qt÷™z4#ü¿d@¾Ô?ð¬Û�ÆÞ Ît;nÎéC¦¤¯E ‡öý7YãoIÒ…öš»ê¤k«aGl§ÓI] ßsÝÑÄ$à ‚Ŧ¿Zp··‘Ì}¸� <¡­Ô®LP:5uï|¹ô1ôÔ™y¾›¢‚o‘ÉŒ‰(Ž|éKRb7éüøQxü^%ëlPaŸk�Z…��WüÅñÿå.  û ŸjY¿X< xœí[ûs·þýþ T™Fö )ÉJ'²¬×vf<Û©¥&M: ÈÉ«�w—(šþëûí.€{ð!Êvi;ã„ä°Ø]ìë[@ç¿{þæÙÕÏ?¼P37Ï/’óðatŠ�¹qZ�gº¶Æ=9X¸Éð›ƒcºèÍ]¨{ç7º¾pîü˜>ŠNÀ¡<¸~ÌdÎ�«µ%ðoˆŸî3-弆2:~Ôùb37ê§:sFé–r]‰Ÿãr>×Ck*]kgÒá ‘P÷ž]þx_Ñ‚G‰PH³› ÑÜÑž4Äÿ¾jRKbÈ*73 O‰êL&×D_βñLÍõJ�H2?=v¦V7fìÊz@ÏLž[‡¯e­t¢ð5+¦ÊŽu®ë#¥®°dmì"wô˜h«±.ˆ\ «T#=~§–™›%^>HuYôì½@ DމŒÌ4+,Oõš5…SyV᪬y¹Y¹óÅ ï kÆ —Ýàu¹´ ^å)ë‚f²Eä¶z|¤ÆŽë¬rfBÌ"WàAX�Áþëxœí}{¯ÛF–çÿü\/¶Ý”8qoÏ.|ŽãîÍN:lo‚Æ aPuűD*$u¯•Å~÷=ϪSÅ¢^÷átÚÀLÇW$«N½Îù�g=ý/ß¾zñöï?¾Ì—ýzõ,{ªÿ)‹9üg]öE>[mWö_?Øö‹ÏÿçƒGð{_õ«òY_áóéªÌŸo6«]þ—m=뫦îò¾Éߺ‡/šº/ë¾{úˆ?Ëž>’æ§Í|‡�}õl^ôE¿Û”Ý“üäVáóìi‘ןüú�ûú‹«¢]lëÏž>*°�ÇÏâGðåcx²Ég«¢ë¾~°üs¾èàýY37c{’?…ן}óôþ'ÿ::}¦ßOŸå¤§ð׌_˜ðëÏù¯Ïž>¢Öž>Ú z‚ÿûþìo·Gùë˜Œà‡ŸŠÕÖýò‡e¹ZU›‹ˆ¼g2ãE¾�9Ç)/‹Ù2‡ï*šöf�ýLfü鼺ұ­»Ïÿü€[ãqEƒá‘9oyU%,ý²tDeù²¨ç@KÔDÞ•›¢-ú†�¢Ûð›Ëv—åÐ`Þ–ý¶­¹¯¶ì¶«¾Ë«:õ™Œæ‹<tRuY¾�Y(çùõ²Y•“¼k‚Ù\=4=ßÎ`Hp°šÕvMs\äݬXm~Y]á£,oêòó¶¹BTŒW˜3xœíYm‹7þ¾¿Bu!´àœi Pî|†Ð—omCrPJ)Aö޽"Ú•#iÏñ¿ï3£}·/ ¾´¤´p°öJšÍÛóŒoùÅ÷¿|w÷ÛËTK»Ê–íƒtŽGIQ«M¡} x;«ãöé·³ÞG-­¢áõµ%õJW;R/=åf£#…å"íÈ–‹FÒÚåG–ûÍ*×QÇãžÂµú�ìÌ–ZUº¤ÛY·ñj㪨M<˜­– ÍRŸ­Ø9ϰa¯6V‡p;+ž«mÀ±�Ëö_«å½ö«»— ~ª[µ\¯&rÖ+õ•ìŠ1íš§Cž¶éû×Ë…H].ö'ñ÷_ãÍ¿�T7B…Ù¯Ü!¤·|‹VêÎפUJ|öº *¼äðÝm±,:ø#ï WY’‘›ûö^exú|–Ħ;M]8²ö#-UžbíÙšÖÒLÑ=ù£‚ ¨«È��*k(ð²ÉI.ÑY>Ñ • ™ZS<|`bP¤=Ÿ�JW¹¼°œ}Qyw�›ÏÕÚÅâ7¶Î)¿RêŽU¸C–£*¢ñ4PîUp>âM弪+󮆄€Ó0¨²G±/œ¢*âÜ‚²Æ`í!µ2SJpl×Ù�è·ÐâÔ^{\ƒ­oƒÂ»$èc€÷pß$ÛU¼7烮Jq-<ÁÅØ‰K½ÈTŸ˜Mz¥€ìÌ=»žq‡ Ψay2Q”@éMÁâp‰ äR²“Ï‘ÝÂ?ÖU;ÜÁõb­jR c¥ÝÖ‹B&.Ä­»"úñ¾V"Õ,Ô^Gãªv!l´Õ¾1Fsw¨Ž&“D¸œ#™Ù–žƒ„oŽiN{xŠÝ õÄuR]êc§^·’¤öº¦—;.^¸¹qÑÀÅ/ÛRXû*äвà¶> ÍRÚæ8ʽÐe ?PS).©Óƒ|®ñyœm ®¬áBÍÛR�ÖwMœ†ñÑ£–V¯‰$z€®Eó*S € çm€ùF�õM�¬WŒð_öpéÐ^9@}`�qv®.Óp@g4Õ‰üÁëóÒS›è„ ü•I fÃÃ|Aœ¹˜�¥�œ¡×\L1&r>ŠñH‹¢©Ã�9jçžWzlGµ¢ã•®º„nŒ,Ýø…ØzWŽ1ˆv‚83¡  ÓYBT¤‹¶÷äÌ?m‹?` -�›ãl„¶Påê]qÝùQø –=Ûó^o"øÞ°ýœ�1�g]äתPâC ¨f¤Û¹YãÄÂ'>¼«Ý�€8šþIƒ˜Fï{y°Ž»}"]ëm ãÝ§ŠœõL*°KH“f,ñü�5ò0÷až4•Ds-e` ŒGbã„KË»k¶äü•æ]W>!+ "F¬¤O‡–&2‰èSkΤC_ÆG²:u1?˜Íü`L}§uÓBqvšóC¨íé3¢Éq®WGéÛr AWËj…ÏÕjήI¼³íÝ�¨ƒd\ó2þNL� ©ΦÿLÙ{QG†_Œ§#)Ÿš>Êž!–öÃyMCxŽÀŸbtØýèÁ½‡¡i[Z=–:oÚ‰iÑF:öQã_šþr÷IÑ.ñx8%ã~˜ Àý2L&Ù¦9˜€&µ>ªœ¶º¶èX Ñ©²n0a�bžæaÀ�"œ@¦Ãun=ê;¡-Œý"w¡r…v·\àÙÂozTL¡]ׯb²�gݬØüPÒN%éâÔl}”ÿ‡à�Á Ahü~Âå„Óœ­ÄŸçI3ÁÅà>ªâ³ÐÞVmöXhŸ"{Æ5WMZ´iºbpìi~3O••€[ ¼ þ™ü0�Œh?k¦l¥wH­7’?fþþgÐ|Ñüv¿Hÿøµ¹SvU §·@F³í7ÛÞõ/¶%íûy¾hÚ`’pDý⨃?vù ÞÎm³žä �i]õ»7›²œËÌOhJÊ«²ÝåHG³ÈÌÄ@3m«ë‚�q‚tŽUÛõôkÕç×ÐˬYÙH$ô«[æFûã˜]ž ¶y¾næÕBÉn6%l^–¶Ù^.™øfµj®«ú2‹»x÷K #}¢G`Eã,fÀ(åx 潎óáw5ÌÂOºvÝÙƒWà½ù3Zb¿¶0›vȼyâ-_urDæ°�&´}`éaºUÇíÑM`¥çÕ ÿQ䫿²‚Ý–_•³¾i'9ì³GùlÁ†ûf—åórQàró>°‡iH�R7=4u ³¹� ÏÅNÅ0Øy8)•—÷ÍË«ºD"×M{p~€à Þ&è؆ Qv*Œ ¶jÝáPqj&pR+8ÃòÅtliÚ)ë®\Á©„)‰f?Ã1à µ[ƒ¦ž•Ž÷P“tŒ‚-¾„§›ú€-ȯÁnÅ#µÃiÛ¦äú HÒùË,WØlJà%x¸mÇŪ©/»j^ÂñìÓÇ/šmÝë ÑF.²hæ ®î$Ÿ–1‰�’äXÕk˜óüºê—ÈÈÖU×ñf€ƒƒ4õ.±E�“5ëªï“»ä1¾¿4íºèÇ6ÈB_&F­oåDÒd3S+VÀ•aMaûÃ’Ma›Ë‡Å¶oô»ÏtcŒH#/\Ûº>¼~èx¨6ñ˶‚o§v 7ï±mÓçš§ æ§-v¸‘¥¯ð¶aGÑw°«à5þ81­/Û¶iÿø61­Ïc®{tµ"áVÖÈj‹ªÃ Xç%6=ŠfeuEû†Ø·ÛPXòæN¼šwB l˺GæÜº1©dêºâ²t€çG>©çå÷63nZPZ«¢½Ü® ˆ$‘$:¾Èòïà-–­,R‘WáümVÅÌA8³Ã¶æÕœ¸ϼNñ£9`hüOu•„¦ptÆ )?:šê÷÷M�îñŽ ©0Å›¡RÄ� ÒˆÎqƒÐì8ª‡ßÊDƒ¦ÓlØ�¸ šwœœ¤¾ú ‘kÙØšAB�¼N1/Œ4|�8Wq9|G,sG“ÙqŸ¶*Ü�ìr@——àÂWzô3ÿÛã½›oêCxó¨•ÿ—Å›8û oþöð¦¹þwz‚õzØ5k›Î©q]€.'±k¦/~®‡°«î7<ßcg‘ŽŽƒXâ„yÓ±y¿aÔü°]3A£Dl×Slt¡íz EpõØN=iòƒÞrRz„áÂë}Ðàá É+|Óñœw¾Ú@3í5 KâAô¢2ÿÄ߈´!ö—åsÇ‘sÝ?㡬ÓnË”î AB Òy²ŒçhY¨Ó*½«[€vR:)ž`ݹ87ÿÙßA�Ú7´âOˆË8� Çô³¿é›PÇg\hfak¯ÊÌÒçx%ó¼YQ#ãEMÇwMDµåb„í¾üз ¡^€€TÃíMV†gf&-áÜ žó9MÕ<‹ d%÷�ªA¹@žÎ|`t¡ûx¸6Ÿ”ÔOJêï[I%9'§îp»$•Õð•“”Ö¿Z2nÇ©’}ÀmzþÑ¿BÃËf~X«½gRä'ôlù÷>2}ôë¤ÀªG4ð“ IÖNŽP¬–€·ÚT(Ê"#€˜_§ØŽ¶ xÒCº�›æl|K*ä·X;hR™‚k¼U�%;c9!¦ˆA·N-À è 2lÐk â[Ç"|$¢C?‘Yty4rQþñgí¼l�IÁ)ú=Ãlq„ùÉdμnÈÛÅz¶ ïZ}=5hÉ)Íê]±Ú•�¬w“YI— 'FífÐùÃH<¬Ñ*?¢wmlÞ 0g´xó|~Ø~³¨¼ødeŒŸ­û­}ÊZu6�…Þ9dWJ3u�·6yKv61¾�â2¿„£’±†ôt@„çáo˜è’u÷6ŸoÅ^¥˜¹zîŸÂ|BOµ5Øœ¨ Ëâ 5öEõ¯Êú#•ª ®k·Ý»Ðü|x ªzÛDzÙ&ŽÒ¼Ø¥NËΊþ~Ô\—å{œ’5¨Kf¢ù/Û¢…ãIg°wP­ÁF©ÉaÐ5°8`8³ 20h mÝÁo÷¾£YD¦”éBâ!ds{Ù´¨ß(• J‰UH-¡ˆùpJ¥W€Ùj¿ÒRÝ]—m9¿Èÿöüí÷Ï¿°Ú3ƒ‰þ·?ÿùOÿæ6O[¶äí4Õ\,VMÙ D†€}–ð®àG‰Yæ·ÖBy¤�’&­˜ÍÊ h.Ç[+_—d©¹[ÔÚ™í™o8AË]vŠœ1^ÁÅG\�‘Ðü¬’•ñ`7˜BV ëòzÀÆ46ƒ�¾[j}-Û™Ár&ˆÍH(IßÕ³ÕHx ‡;1y?“ìÓÏ( 7p…�ðWî\­ÊEŸTJ?†àEVu¹t¯±¶¬0„Xù5íÀ$oBµ†Ì:'i!Äźq„ŸßPÿÐFŽDÔ·ƒäOì4Íño©[’nÓÉ6 ]‹ÎaÉ©àœI=i¹ ÏN…áã(\8mæx= R»$CÂ0ä#ÌCy?ÉŽ0v‹üâ×~,[„™ñ;ŒðÕ�H1Cyï>f dö…¸;8ûݬ­¦Î½Á¢(cY»¯/`ö Ú³ÌaÔµÄA #!¾gv“¯Ši¹úøÿÈwÂÖÁ{Ç@º¢K7u¤‹]ö Ò}‚tÒ (ù{ш‡Ý&²#uMÞ"«æ‘«®s¾;RÛDt•÷i×´h\Gñ8©^®7ýîhçcDF[Ý5b³Êr³�ð”ÊÙK;´2Ùˆ¤˜×©»’86ùkÙ6¡ãh'ÆïHvÛõºhwãHR^¸!”t­Ü+–<µ×;3Ÿß!wheëìáX{ÌOu‡h\”<Íç„"pRÜ;¸OŒé&lÀ[x¦°"bºQ7ȪÙï[°¦ÿ!†‰JaéáoŇÁomQ{qî›Ò«"ÑüïàÇ+�¸=ÂïçÃFkXPÆ^CçHýk Íà`ü²5��Pŵ’½_fLƒ“+*_¨öni@Œ°ÞI#])ÊèIkxg±˜NÛòª*zvŸÀ»¸b=­.·Í¶Ë7€ç«¸¢ƒÉ°ƒ3óÆQ¬�F bør â)/a“:=k 8Îû8F“¤£Å±êN…É^gMÒW“�ì« \5¤§ÝúŒyhNqÇÜ¥îŽ=;Awÿ¤Ô}Rêþe•ºEµêËv\§ãç7Té´‘�­[ÝœŽ;T­Î#îã%Yz�®5~h„6<*#j׿—åfÔéaø‡Ÿ’†P§:SïâÜ;Ÿš4&_S)RA†ÔñàÜe ’ôãŒLgr"-…µ3‰õ,5 ZC«ÀŸHŠh4Ƀc;Ùj«L:“ü=läñ(µÛf³QÍ’Šèh�e:}T[@]#Δðíqh<Ò¤š&ü3cÕmÛ+½«òÐ…fñß>ÏëŸB3¼Ùæ>€hÇE¸Í½$!‚�¸oÍ©{›Ø Ì�Ú“ÉNÇMëóù» aWèŸ7ÉÒÈ×»NvuŒ�²R}ò¿ý+ùßF�l׳÷IKON¯}ÿØ"i` “Ý žÛ×éíc¡Þ„tI»’†�ÿüÇ€ÔÑœó7øNÞ!ëÅüÕ1º&a�âì¬Ç.M²fØjÇŒ(„ÂY]^æ™ *©Z‡ƒêM\ó’%<¥Ý‰Tª9¡·@|elŸØ³G5Æi¢8jì.¤ +%±ü¢“gE'¢Ï9Ãsq6%™Ü0µ5#ïì6&îI‘¢p»¤m ¶Æ‰$yiHeÕ+o3_eRƒtB&çÝwÚtd Kž�®oa-Ÿ½)KÚê ÉÉO¨ÌE¾„Se± –„ü¯îÏ^Ø2ZÙl�ƒ>Ff7V톟žp¦+ :L#àq{Ñ Ýœ•Ɔ)µãá±½HÇ C²£wdJ<–MzÊÑÔ ~y<ƒ ‚�ç!gëVxŽÔÅýEÆlŽJAø“Ê¿•¨ª‹•=E�ÓOâ1p¡Qù}¢&b`�°�ö(Bñp³Àêƒ;󪨵“³ßJØÉe{å ChZe3cûîX7¢‰¤üÃ'êö/¬Lâï�8ú>? ^ßD·?¯¯»×í_Ðí=©£ªýÿnÂú¢(׬ ÂO¨´åÍô?ËÙñÞ;c¤£»,F;ŠÄ;`KðæqsAõ62³J5-©VRåòoŒÄt� “x_¢¾ÚÏ–Nµ# %×+ôî¦îÛjºíËÎVVAӴƤ…’ÊMiù+íëHöpqš`s'RP”³ŒÚÓí£€F¦Å1#h°Kµ@�LÉ®úËÁ÷Ï8Õ #‘VùÝ`:^P¶ [_ÊE0d6}ò¿I›­¡l” Ùë5|¿­¡5ö®T�vÖUTÉ++NäúiUt÷žÞŠ·¦çúitšnß4ï?Ž þwXáƒò°íp˜ô‰`CX ØËñœoZäÒÊ úôâq(£¾/½¥ÄYÕ_ãQ:—FSÏÜtäh°Ë(N‹]–Ȳxöø$l8xF¨€�pÇc!O¥aC%œâVÖ"¨YVZs];ë=3#|ïÑÓ‘Q çýU]¾h6»‘?ù‰&læÖ^ð&vçBÛbâ†J´Á^4Ùj&EÜ4²Oÿ�GÝvÁ>âòê§(Û?´Ý/Ûæ‚'�Úvk/y’/àMåLĽ1 ¦–ØUáÚ¿ìtsHd¶Ù=@"›ù¥wÕ’�Ï®bÍ;çãçÉÝ ™Qpu¶ºOC¯˜§R2s&Ì4 8AñÛÝf�ßk±5I²¡×‹�VDl9*öîѪ�ø b˜MÈo¶¬¦2ädGƒÀ'jHýßÁŒ,òñÙv4‹ÊriÂØÅjËÖ;ÄSöçÝ™Õ6MW!™ +ϳQtPÓÑ!”ƒ\8ã!- Êÿ„#'8’TØÇ4ã&4~žÄ’ã†Â˜è ¹CJ&,¨.»žähDÛÚ;+ÜØPs8.(Ê\‹/V£lü3½åŠÿÂqPl2Òí€*M9îSV¡/÷βžn?jF2#Ê퓵IŽJe?qõüºo¶È ŸdÞî¯SŽÁw0Žj;Ûhü¯y#õ?ÝI°­ÆN–†WmV&¹¯ƒ|ýãÉ£ a¨,~1ãà ôºÈƒjî5ÞoQÕsŽ>Nž-ÿøü|\ÛÆmüý‡íF0ÞÛï¾ ô�Sú}–ÿÀ^·Þç&ŽÅú-(?öãY ž¦Ã§Y®†Þé‹Lƒ�˜M…3Oä5uÏ+<#3´È¬§UMü¸sXÆR�,’Н&]Ö7üYOÝ®dNÅ´¤;ŸöŽwÉ—A(ÏÉÅCïÖ[ )ì›ÙmÿdX‰’ú©’ j� Ø$rî™Ã‹c]ðØÛ %‹l¨¾Y ÕG OòB¹Ö% R.þþh‰�&¤p=—œ*It:“鶛ͪÄOŠv÷Nó‚;f9ÝfUõ˜—¹{ðÌÿ™È$¿Ne»Kå] NàTÒ¡Lã×cFAĽFö÷¯´IÕHÿqëí¹’ÓÕåH°»£™œŸIOôys{+�QXoÄ«…=º¬*府]ˆÞz ›1wí�GÉSzõ¦zæxœ?Ähá¡Oœ¢Â7 Rª'÷xMÀÝQ¡$qè-;Â͇�ÒŸšcåY +.Ĭ3Êf³}Ì™5®0íãÅñF úo®R•�öív»k†vÌ,�»¦üá-mXJ¯Zê$Óˆ.¾+çvE8*ƒ#yo™Zeꎘ¡$ö$9¡<»IªkâèÌÐ þç&Vö[ëþãgªÔ�çªnZ æ¶áôДº’$x–ú(´œ½ˆ.�5 Î÷�üÕA9>B ê¸\®DŽê`5…“i  yfÁÇÅE´9½­Œ]¥l&Gø :exôòJ­%›Æo¢•{/µYL†ÄV(Ø+¼ö@1ÑRE¡¸Zà2;��óç‘HiÊc2C›ö -Bøór0Ý~:7:5öW8yž¾ïèÍà£ë;jHfÏ�Õ«ô~a«:z€evܽ¸‡LÞ7Oýœ-±Šäœ §eÍÖRUL‚:šÇ¥vu n�±âUwì‰ÇM8ìùùåe[^ÒtþEDìHÇÆê¥YlZ,LntgŠ×ÀSB Ë–WÃ?@(P�g\Ø÷–)A¦U6å”+Xx¶yº¨t¹ª�²±i5óùÏ�9ÃõŽô‹wºjÝ»¾y÷Ö=Ôkd#Ü£•44î8mª«¦OÂ&zr–ùG‹]¤™sRNG˦ÙêÐ €€~W»Çù׎÷Á{ú‰ø!áâf†VVr!RÞ…–«øåY³ºVÈ8¢”Y–è,Ÿc$~µ).Ù4‚IÕƒ~tŽª>¨G6Ro:ŽP#›W˜bh£¥\òúó4*Ø,¦ô-�¦°p½D/á^ṟui�ŸôÀ ôOÌÒÁ±�E OX¥Íˆ$vÂwcKÇ�pJ¡5·í«œ�í1Rq„=u!ù¥C{U–2Xý‚1+œ,­”ÎÚIN»§¼´»0eÜht®X®w-‚èr)úÿFå³üfæåî›Æ&Ÿ~¤Èèp&s9xæ18Tæøøìì,òâ'$]j�Fú÷ 0w8—1é—¶Kµ§…ã;4±-*vÅ5Á9�5Qs:ÑÎê7Uï€pz/ºàYM°×Óä¬Vû« jßWø,ðÏŽ–ƒ¯/›¬ ×ÓÜ_z÷ç%EžÚÚ¾�–!±k}­ŽÊïî&¸ß3WPwœØ·M d/ °úT—€˜£ç4|¢æTø�RèW¥»îû¡ÃÕ»?¬ú Þ¸ì/¢Zá†ø›ø•b4í!HÙ´2îýœ·¤°I‰8>ýÊz�.Y‡ËÚ±72æjF#¶d2¹Z?©“E2ÍK@œE;[îΆ’{-lá…0áåw[*L˜®„B�N´±}c�¢¶`�âó°Gø|£jogõø¦/7wÒit?ʱՖpÛãI_^—]±Þ¬BÈÝð)¸g=Â.çÉdOZøt¬ëµ+1›w¨Íõ©&&íj±Mæê€k1K}â«wöš˜ú¾ª]áo-3ÑnâI&Öb¬Ùö𵓮|zêÔ wh>ÝV¨ï÷øÂ¤iÅ ‰”¥v±4ç$ºÀƒk`¬œ²mnÜ â›æ‰-¿™øy^T©f°Þfâm*¿™ø]Jq®‚Zž®1,æé §ê–€šÛžÊÈqü`ÕÙ©S@ÅsBH=-¼@|— ý袒¡�‡”ÑÆ° ­Õ¾æùYøxdù¦©0ŒÔ«­«%[9´Ã�‡½çÅ¢‰GÛ§AI� EÃ9&ðNÖÞóäLSÑNV„çÅbxS‡Z’ƒX#ýT+”ÝCRCÉAødìÍ™£Ï’''ÂÒ‰Ší+ c"aÉQè_R$t|Íž�8—ʦ1‡ íàMó8i’Ç×D®A½Ü¶\¯Ð.FÚ•›1õÒlãI6ºóOÝøYz=¢Ú³ö î82Hšw¢ó‘�ûIâŒì)0x<—áÖ%aÐ�>¶ÌdÊp Þê.œ?¢þ~Ç"BŽ’@8¦_qCég°½% @ ¿©ƒElU_µçŽ jFµ<ç%ôhÅ]h±+j#z¢É&žÔ™ëîøžœí¹HŒl|<[4“ÉÖ-È4„Ú7¡iüwuYQèëÀ½E†96¡YF(tÆBžÙ8§D8é×õœ,x]Ì£ r@z¶‰»Ü…ÝÐ]%*Õ�+5µ„¹b\öÇ* ûзE ¤a^ïReNÙÈËc? ,}Z Gâž·Áù¡¶ Ÿ»üº@SÐiù3yNFþ¾Æý/S¾í�×ÍѯÏå šr¼ÙO×ʈ ½š”Þb“¯ª÷%'�‹ð¬R"‹m:;Ú\ž85‘…˜ÿÐX´­˜—Í%JÊZcÓ5…ðZXLeˆÄº±àáÐñØ}‹Dy-œ˜‹û ÄÍ!ð­ j·�-ŠcŸiЍ pú”\Ö)~Q=æÿà-B1vÒ‡ßR&I¹nˆ¡ÆU!w R×L²•›èÌ�øÇØ»ó Viwg k#Žœw¯Ù"#yt¿Ãüê8”¨êÄlcÉÜ�ûOè;ôZÇ­âÊ˪?x–üy_ËçÅDíêÙ&°úµ "£üÏÇx¯VÅeÒu¿ßÄoEŸ]H·èÀéA]#îš7ËjÑ;Ïå –šXŠV º™óè½ e[F]c‰�u·pê�c¨>é�…Ê„èÆr½ƒ�䨢¤êIÌ44KiþÔ5}jmC"œ¡Z"ñpãMd—ê “ãÁ!³B„ƒ—lan¢B4ºÄV†:¶.ÐJ� ´Üšè«˜JbŒI˜‹×#q`n ܨÚx¹cFFIW¬Äöño£Ù+FŠEÈjµá À IQ¯ y_•áØx-Í¢%»B,k[Ίûõ–ºË’Ë1Ç\^$…/#KÅ iUþêø˜ü�â´£M?݉e$²Œ!s—XÕk,ÎàŒ_Mˆ/ šùk �‹sáCÙÙXkº3-.¥Çy¢”F¤!ó‚$\íø$ì‹Qzæ Šû-´�yp%¿`ïT¦1 ¦uÖb¶§ñ°'Zr…oEXò‘Œs\™P¢ËÙ8‹ñˆÆóÄ…†) £>Š\úü†©¥v\©ÜR;À“$g�]j[1âí«@¼¹Œób/Nê0ŠŽèûî¬ë¨«Ûˆm9¥¿Û 59¥×“ÂM~Üö.[ÁuKÖ@ä[]Ù‹,5Å^ðGrf“j‹�Òø�0�OÙõ#9v_fƒJí¢ š½$¹.lXL7kÕBÐJQï Š0S‚N �¤vÕfvZƒÐ³*Æc�\'Æ2ÁfT‚Þ©cóxØz�‘6>9?;ÙêL‘5“ @\#Sœ1¡ ÚÛDÚ9­wFîñ,;"î`ULËñáÆ@k2/‡e$ ‡’rñ¢¾]¢÷Y" Ù:’ˆs$HWŽeÿ¼Ò0¼¾ƒ«–]Ö¹°\‹™Q'“Ï8•®¡–\gM¢ HEÂfx+n¢c‚ú'òš‘pæÁþeeÁÇd±kÀ�ix‰÷UÖˆ (l>‹{Ê7Ö†Á 8ùàñzÖVºí”=.Qcvµ?1xÁ¸ÐBEûÎyO2þIHÔûxôŠÎ¾YY�i6®’®Œ•ËYl{­žJ‚€Å�'וØ]‹©�(%Ó_c€»ÏPžp³ 3 £‘An¾kÌ”t2"÷õ*$fÙKÕc�Ò¥º–Y:Ì™ŽÑ=m¿¤N³€Tc#øÓ%·Á¥�–œt4ZS�#B8çÞL*uª,ï@¾ µ Ü�>çŒtÒ1ΡœmÉâ܆…J¸ BÈåúY�©äð_‰Ë£ˆ“¾Ñ €®ŠÕ_tºêœ‡CRh‹ºl¶S� ñjœsÀߕ쾉Ý.ü4Z7Õ43›%ŒEd>÷C£ÔÛZ$“ƒã„êFQ&Ï„¨ëaÕµÄ8ºHÂ’;&4Sen,a6…¡Â&­‹6É"¬©1Úf€wKP¤\ÔOY3›w=R27gzõù—�Ÿ|ù¥pS¾… (œ .ýÒ+lþøòOø6&F¡iàËÿIà•1'üZ¶�à>mÂÙJü Ø6#nóB߯¹ügéÜb�:8ؘ\œ!)8½SMu>h™â°.ú”mw+ÛLˆŒ¾*×Ï8±³eàŸ “)”â2îFÖµ‹ SŸJâŠV{Sèþ¶Z¯ž¬Y³ÁK &ËM¯áŒYˆÜè �Öˆ[QÊúŠ^ì1(BÓm2÷5g# }ѦXï1=øë­Ü±¢‚ad µÐn®·ÐÀ¿ÝœÇ!Ifœé3£4 ÷ι2Ÿ‘g4À¨Ò÷©œæ ëfcb6öZn¹¡²ÙŸt§0{ �»ðˆrFi-r ™Q�Ûv¡Ñ4o¼%Lš8BAÏlȦlyÙCî}¶ë‘†1ʦ¼ì0�¬£•¤›xúˆ�h"6¼çS.™wž6«Ž‡ßÊ—¥÷™v�c XÔ½éz�kšŠ¸Ø1;ÙXp–8gÂùrÑ¿Ýî{?.½Èa5ê§Š‘Ø�!ãÍTt·&á›<;ó¢*‹p\KcøJ�×T¼7ùÝŒcçùH]˜ÆwÜ7gy� åBöñ$gifØc©äK̃“! Á„ÇøL؈µ€ BŒEÂl ñò\Fº.Èðíƒ�ù�n¢™µlïÇ÷&æÞÑÄ(�fÊ÷`‰U¾Ä2Rçëºô�!›ã¨]ä²|­BÑ$Iøð}[pè*6ŒKeDƒÈ£Çèhs[†ÏÞÌ€Ñlz­…‚LSœ¯-1âþAÿ©_úùÜ�Ãå8R¶ú´‹>&Îk›í¥+Ÿ.ñ�n£|ñ‚v‡ù�ëášÅW›´†Ú¼•;IF¯lÉ‹{åÍ ‹¼ñø¦qkfFñ/wW5o=Êý´ç3?mèÞ—¨ï†û,ÁäL¹°³Ùx2qr¤ä h eîÀÅlÍ•2žé£[0³1^–ÅÌ8»ñ]^*i¿Ln]I°�Òˆå›îåŒ8‰�ÔwtJèÓœ%�Šy~Öyyn�KÐØá»ð`šD÷ah_Oš0/ò±Âa(3^8ýgÿÝ‘ëÎÿEf‹ásß ÞqÙ´–†‰)¥“˜a&Á5ZA¤¸µ¿§ršÇú†MŠô…¶47-¥˜Æÿ}2yòÿ†¡žá)vfc¼-ÁÝÛâ¹D~ñ`°‡ñ×[7ÏrÓ0�-Ʀ§¿ýNõ_�ã?JtÍ-cªòÝ[ú#ýä¨Ùˆ¿!{Hì×c /É(áµ9ƒ½¼²nëÜÅÈ›£9Ì‹ˆÃP‡¯Öô‚-hE ˆP€s‡É0qÜ)¤�7×¹Iôž<ïºþ0( d l ‡iÇ™ùˆ�˲Եo÷É*tg§åÝ'fq³î›à¼Ç± �ŸQf¡u‚ÛéÍ�Mh+·uðÉý8ÐÛ†â¬ã^>�yI( w»méŽÿÇi\íMÄÕ¸�Ã|-êO20G[T%•,�!Ž÷�;eá­³%;þñÆ© ¦IiË'½‡ %-ÍG�n|•Q©©ˆ—#Ì^ ¦"Û?þ@�¥×µÁ õÝè�oÀŸÿÙó}‚ÇûfÍNô$ÅÎAö“›âÐ1ݧÄLWûŒ›ºUN}rg溚â× gëFîú~±n6OÜѶCwDô_ž©¬Øg˜1´ˆ·IÏ9å{ú¢ZÎÀXÅ¡tj(ò%Õƒ ¤¢ÌDaîá >�ƒ¾â*$T¯9²·€¥jvþ4Œ¢p·±ûˆ[Y¹\*e�?Ó]ì‚>kúî—,½aŒ3,¨ºHÎν½ònÁ²[a©Øn�ÕÛÎûxueçH§V*Ñû¦Vé2¾œŠ™< VEO£‰ÏN^ù´ž+[™q6YAë ùm[7ˆrôdE‡r/5�ðBW½6vîðÓÌÁÔ':·ÑþbŒëºÎØÅXvä U,æ#SÛ®Z˜™$r,J>0²¼,¦à}Z—‹Š×’ÐdÉf甬äwC…ýÊŠc!l†$ÞÚ䶪0Ö3 �“\»ølȇëA$£H,á¤,™Ô”�Ó9@t“–¿5ž¾(R¿]e_5QåH6˜ †ÏìÑXÿEÂ>õT¡•Sn´œ¶¼`g¦i |Ü,UOÖÐkÊ»¢¡+íœÏ(aÝP¤‘É�ì¾}_ÀåÏ�ý÷hD/fäÉЬ$6Ó¤[®p U¨7•)ž—ã§{[Ѿڙ1NTÕ¯‘Ê4$•„æ¿3¦B¶Â ­ÔèéÒ™Ê�øÇ—z^åæ-¶¹šƒµ/zV¨5™[ÅA> %l>ö&][hfYÒ(øPY¿ƒ¾(.ê©F™Ôå¼·ÍÞz[áïuéŒP‹1v\¤C I¨�æ™ãœA.*“iHô(2™¤­¯M¾tÞéÜ©®Ÿ÷òPKc(hZÖ'ÂÛn)S·¨ È ¢rk½²”ËbE™ø’RüH²0×ÇM^©ÉÚ`> Œo«#˜×¥�©æ¶ëÁÊì°V3äÇ�¹–å�_}Ñ‘ý ¯ [ØLƒÐÒõËë7Be‚ílÄFl”w§ÜÍÓmÒý ¾F›é^"™^»’é{á=g ¦ë=ˆÇz–H±²À;Q!Kþ¨�çýkPážK}>T¸#¤Ý‚ûß&¤¤ß§«e¸†‘ŽšÈ-ðj±Ö æÜûååà ! Uiz»ò�w|~,q0sa�¸Ž&�U(U«{¬¢PûË«ïïs lè`&r;"qÕ&;`­_ ë܆jFÖO2zÜ‚:|”Œ‘6 ¥—¤»DÕ˜(І½Rö]VÑ/±ˆñ¬,-7L¸á4‘Fù”�‰” .vcéŇìKhiæ—¾ÈY²,Zg ÄÎÎÎq]Výîsw,6éó¥cÑ‘ý ¯ŠOéXH `Ûiîæ®B›3‘TúÍ÷�ìGHû¤û`Ÿ~D¿M²¡!Ñ·Û½;ý‰»úE,¹>¾-ñqë}Ækö>÷êéÕ÷Oÿ8$,¿ù€²O¢+Æ]Tvç&ECBÂj‡~È÷'hï²"=£B.iÞÒËŽ§·U3¢ÃMhˆPµI‡ ^fŠak¡‘ˆŠå¬Î¼G1ðëš�:%HÊ©Æ8©kBÒ?rïlåm ÏGia(^9 ‚XÒÑ ïÁ†žH|ö>·ï7ÐÇãyl¯·P®?0÷åù÷ÄjÊ!TIu–rvÛó º9àX/õ¤E«I§Àj]ÝA&Bî… ;²²‘wêú„( ð-,áo3ÖøÒáAÎVX46¾v¦Xë†ù´¾½iÈYž¸‚ÄÔpš²!mÃ:¿ÏFµy3™dcé! jµ%lS} :ƒš-% e¢cŸW1ƒyköL™÷cÃZÅ;é¸i ÜËžtÔÊs/�ÝYGˆÆlß ˆ2‹5;­! &t4ÂVåψ:×cÎ`2÷ϳ�uê` ‰ ܳ2ÜÃ'øL<Ô²t0Þ%dA&O“öÁ¼Ù਱ñÔv/ÐAÊ–>‹óemj"¨fZ–)•€ØÂ¶ñm:õ þÉs”ÅH´–Úo¤B^¤<¼½2ÔE»˜ßPËìq±¡ÓJå¥Þ~÷L}ûðá#æ ÚÆ Ôi§'§ž|5<ùöêôôìäÿ†'ñÿ¿>�*ߎ_›åõÏ€é‹!«ß@£ª?ÜFñ�h¿Y¢›V¹œš›LÊÐ ÔóG‘©ÇÊ#ä ªfG1šw9[‘‚A`d u¸��é#Ý¢Š�w<ÚØ=ƶvhHúàý*-8jŠéJ g<˜ÎÂÌÍ=¶ÿ÷º>¢×ÕOf»º”m×¼ý°¬Ã[¼OŠêäzUõ”¥ûßÛ_ÚT$µzW�i?ÆŸÆ�¦¾µ•8ÉLžÂ[ýx®è¸¶áÈ<=„{ž0_»<ìÕ4؆V&>…‘­�4ó¬µSþéHGFïÓîJŽ“ˆÓ:Cª2ù¤¯žÃÁáµó§‚Î%ǯÝêÇϳŽ’¯ ƒ¶©/E­½f,Ì­é.xźBàWâ$]oÚöVÜW°ýºÇ±OÔ=N -ä=×z«‹én×¥µ\Y s3qù« Ú¬{×Dî§P6.�ˆ)lQÉ…¤öôeŽæÿÔㆸÒí&¾0‰nñXõitCЖecð½^c³ð¦þ¡,ÞÒÙ§´ ïvh ç}íG´3’ ‘›ÿlÍŽŠi°¡*Jnk˜m(‡žÇZâù#©Ò¨LX«yBå,•ÆÙ% ˆV•ÜMþZí,*,WÆÝj'ž™´C6 ;ÌŽîP8ÓžsÓXÛw¼©½æñF˜ëBÏv[x±&’'¿\’œ ­ëÂE8F]†\#¢SlÙ+£í‚½Œ.IY§Þ¢2üZZ–á±-ë”Ĭ%Ê–ú:TS±{K=g�Qég!–Dz»Ó.'KGª��¹µb�5°äƒØ^˜è,g×xi9>›’TÖ¡¦aú£ëlMyˆ*X´uÅW­ø¢tiXœ¼\¤¼àBÎpkã�þ-uN\û0—àA4xýÎÃ0&@0N5VÌ�¤ZŒòÌ’ë¸9`ÌM¸,+ÈGȰ/墶ÃE'âb¶Qm¼ÛðAî.ÌÍ™ÔÝáuNÏ=>“Ë<4Dû‡íaÈh’ÎÅw±ÙD¸ ý!†‚É‘ïE+œh:þ'´É(‘;NÏ+@³Q¹§z•gÓ™Z}ÃG#ô÷MN78¼z—„¥A:_fv|ú¨VpÇûúÎøŸtyíÓšÌ[[ÅÒ9ð÷ázOöi›yåV{ÄüfïÖp§ù¤s£,P¢Þí®Nð'�åqÅíÍæO]¢îtlo>­»}—…í‡-mëg5907'hJÓSÛ§-Ýg¡·~³]þ8ˆÝÉp´ž¬¯³?ÑÛ5Ù,—ô•ª=Ùÿ±õZDšƒ¬¥Ø™ÓÉu…I®î-,wå>˜ºl 7÷mD’×ú*ÖDDÀ7À³bŒh¬l·2‘ r=BÝÌ7-=¢j è‡€/Ëu±âÚŒú$k6çp#”ÒµnrË5Þ¾~Ë�²u'ûu»Ã2åŒoPTúÞÖ¼¹ÐU.Z ŸÛ}ä^ö�³•/é496«‘Qãín¼šÜp«RNU‡V§ÆÚ¼lÏßC!i9õÙ½æzóëQú„äÔ‹ú­8¿%²·b¹à»’Ëeöù¯H¯/Q˜šë7°HÎÞË6H±Øý—ÏBtf(ß\Èçg!Éïà‚?ÖÓÕ±ÿƒÝcùCà0²”4 ²²�KZ¡2xœ­Ymoã6þ®_ÁS�ûäĹî8$^én8 {Wì¦ú) %Ú"Vu$eÇûëï™!%Q‰�¶»°H‘3ç噡²úËÇÿ|xüíçQù¦^g«áGÉ?�òR•´Nù÷yï·WÿÈ—˜÷Ú×j­öªõ^njµZ†™lµŒ;7¦<’œ¿­Ké¥?vÊÝŠtÞd+)ZÙ¨÷ùô"_¯–’6~?�a¶êDQKçÞçÕbëðï C�…)Uªgµ™mÞ@&/Y-;³�šN†—Âl*·¥r…ÕÝîÄ¡’^¤•ì:ÕÒ”•¾RVøJ¶ãëL4JºÞªò: ²K½@6îê‡<¨»oƒtj'¤ ý$"„hã”°æ€5VEl×B<È¢¢i ‚Z)hãB˜ÎkÓʺ>bª–U“A .À†Í !ÛòÅBÕî|%Œ… Ãk’&ù•vjO³ºõÊîe=;7Ï;/[DÿòB=kç�p&£÷ž•2tXU¶&lM]`ûF §jUxUŠÍñ„�3Ì©–$á­‚¬*Œ-U¹˜!Á^RVÔ¦ø*dmZ5x൹WªYk·Zâ'.Ì“]t ‘gÀ{�CØ1A®õ•)ÉöÁU•ª»ÁK¥QNà€bkêN ki²-à²Ê´3‰ýkg¬'3½’f•3õž‘ÍöŽ˜_mE(Á^0Û€á ²è(Tå]X@1_oT%÷ÐXêíoZ� Ø˱0™mAVæÐ޲$…P&œ<’Ë{?VV)ÑYÓ)ë5$c ´jDì^ZMÒÜ�]jŠLBSQdW¦.9®‡ ì5"‡à·Ô÷$‘ÙC(ÄNsáôÄ^µ^ö{ M?Qf¸_£èh»Eõża4 �A¯…qö¼–V!ˆ©AüÈᔤ‡¶|-†Ã?±Õ­vÕ$dI'ŒfW¬@øƒa²hzßsŠ«ç¢î�Þ«ÛÉ©ì6ŽôgOc õüD,@†àl]ˆ–Al0Bà�‰tÐà‹VivÑ×DSü¨ Ù»àà$ȵLóìoAóI!d�7Ñ!-Èb�$b³ð.v¢Ïª1û�ßFßµB‰ÂéBDQæqN• 9 ŒZÖ;&s²JÓù#8Ï[ý|ÇцTàY¢AZ«ÕŽ�…5s†§[uàS,bè“™2‚€ Ç‰;i=ª…ØZÓˆO÷�?Ýÿ¸ó—¾`ì®�ó� db#Aw)]xƒ½šèøS°K’AHé´{r›ùEõôÏé??ÊÛ ê&…�¡ê-ãø‚.BAé”­—„úHX£F* oz?h±è |p”Þ‚×&¢)òòø–&�œŠƒvÊÍO0Ö°€SS™©·W€0 „x1œþÙ[ 8©2„›Fá|½‘_£·© “¥ÒLèy%u?#5“MøD£²žKä¤É¡§°;帄4Á¬ˆjT�©/Qšê6…pؘ”†R;¹Ã›;bJ ºê@iHÙ˜Õ3ÖMÏq¦ßU¢oq7�'Êä4¡Ö‹ª_K.ð0WGtR%M T]cÊJf!ºÞ�a­ØÃâÆºE61xÅЇÌ+w*4*ÔC£ó¥Ý\ØîÆ~Þ¡œP ‘Á ˆ¼„냼¯ªó4&¶åúz6´ Ó \àð±êÆ£œ3³ˆ&F&rÀ s礃ˆÄ8+àC‹²þoÈÚ\•–q*C|K?mßçS¿I×ÁïÆažÞW$š•Ù¦dC\|fá ã‰}àÂØsR“ ^ïOŒ¶&ùÜ”çUßw]}|ú'"ˆŒèž¼y/…OÀ^|ïšÿz–ƒ‰1®_$ç…ÕNä›êL)™nÌìã%. áö<‹wçêÝznÀ rJFVš¯'H*6¿£ë>­¥_»^ù2Þ·ÿ€¸ï’KωFyˆö0²ÖKI‘O[Y.ÁII¸ ž�üI‚¿Œåëdp2NS™.º ˆ_Ðð|DÇœ¯‡§SêïËPíq™‰ß[ÜyI$aßÖAà¿%çë|| Õ¸"´A—2pTÄ1þ^y¸<ŠGú¦<¡àá›(øþãòËBIb4A”ξ l¶ð²Ð~iµO0ñðM0}XqYÔôè¶×þ8A™æÞÄS$Ë.êCï¼i¦†*_¿œ9(¬ÑßèJ º —éíÀ‰ôÉ÷Ýñ³jŠu9´igz½áƒvhô>…ÑÿÓ奂Ò§ÅIÿþrâ�¸´xõòÒŸ2üYô]úéçwTóÀsFk.㟲–áOcÿÅD�` //�N[» :ùxœÕ[[�·~ç¯`ý�Mín @aopb �8�ãŠ>P3ÔŠõ\drfe¥èï¹ð6�´»Fщµš!ÉÃï|çBêæO¯~þþÝ–Û®®nÅMøÐª„�ZwJ[e�î¾}Öw›Ë??»†ç�é*}«ïuÓuj]i¹³íNÛÎhwsÍ/Å͵²nËŠüê¶T�ê;ížË#}¡‘¸Q²QµþöYjsõÿüQ­uåþ¦¬ÁgÏno®Êýúv¹!HýÚídQ)ç¾}¶ýFnüw _;�R´e¾–çaBy³¾�•·†¡©×Íõ%ßÊðgnš;©U±å5 nSšû0~í.¿yvªÛJÞø”ÊÉnKïµl7²mè±tIsþ̺}ûÂÉvßÈ{/Ô­dk¥®w°’ýd5m|G]’㮄|霹kp¼r×xC�¢JTß០èÌÐw’Uù÷xíÀ‚5@GšîJÊ— �Ä–ëÞT�ÜØ¶†ÑîuÑÁTqa¦%¤ªp¡©º;?I?Àg°ÖîíŒüŠ¿øW3-¾´�ª)¥kh’þŒk4n´H]¯@mæŠsÌgIËVx¯qެè•\ëBõÜ‚LÐ¥S‡r`ciãPvè¶M¤=“Û¶*�_ú•Ï5 gÉ.ts×mÏ2ŒaËO`c� ¦�+C�lÛ½„ýÍÑ.q@]žg+§å71°ž‡O2©,È1�뎔ïàÕ}>;hâ�XöVu¦mú@¦ vR¨J7¥²¯†m@à�,T4I ^ÆaèGŠÝ”™ÖY8@ƒESƒvÚÐÇŸ€F‚w�&ã¨u*Ãáàëp{&îò9,€n8Õ§"n(í ¸Ñîe٘ƸíCÁvLÊ"O‹ÿÔ°µÃaѳ†yX° âRB“˜ƒSަ‰ÏJAÔ'ÔÛvÿç;�£ðö,øDz¢'ɘ‚æmP×"(b«•t;]˜�Ñ%¯ß;èßgÆ>Ïìï„ÜT@EWº&T!d¸ž©ârxûˆ ؇ÖúÁasÛª¯›àE÷àb@uVº­@t¡ ãÔ4 æ·¦2ïu ÅÄŒÊ{gêÔhÁ£¦ȯ^ú[ üŠ+ÛYí`ÊÏeÙï*SÀ˜"(GÉR»0€†ÓÚæ‚®¶6)ÀjESRU%UQèq6¶ÁÅŽ®e­­®²Vï}¬u!�µú®¯”…åüÞöpå¨aíuËê¿‹¹íº›kü¼ú%†›¬RlÀá �L ¤©ÃÊ{q«‹¶Þõh,EüÚé]àï“XÿµS¶Ã^ó`�¯Ÿ‚öLÈîø<Àkc¬ë‹ÐŸí± OôÃÓ©ÖÓЦ­ªvï†bB€ÿŽ ‘?± ÌDßEˆ™#ÝahŒÕÀß$ƒÎ 1•ÞtŒ ˆƒ-Îâ½Ö;Ç}¯ª^c·U%ö‹¡0¤ÈùÊ (k0x �,yÖ�ó¦|C1€i6Ê™×WÙê3²ˆÐkpIæãá–dÌ€m§ \ÁZw{ .ÊžåŒÇ½``#z™=娇»-’§>‚‡WŒp¦8 òbø¶°óïöž}@TÝ’ÜcŠhÀ|ÀQ3ؘW „("® q›Õ œïÆ»Gµ®Ù¢SÆ~ýšB;$8%ïÔ¡‰ìÕÉ]…Œ”¼ˆ”:ôå ›Djªx¿W–Lê/zO ·Úù ?G/‡ñ±Dœµê:HY#®c�J·ö†„X�20D`B¬Ç‹€�‹IÜt‘Xê"‹I-KõÆ-ÁNSŠž+'³!r ÌÍktÝ‘™Ó®0  2‰üݱ7j�Äo¤d®jHÚÐ `—èK³ýß*œëCÔFÔ·ÑwŠi ‡'¤øÐ.ò&þT´�¦��4Qܬ„ÔóRÀ#ñ:¿{ a Sºx�U‘s0¸`«¨„“*3KÜqžˆ1y4A¥i¡Q§“B[Êßâ´å»J™((Ý›:`´Í¬ŽÃ¢å;ä60V ’»mÛßmóR…ËLw=Ä¿¼Ì ³ì“ëI/&þ"ÕêJM³t"%{N&­ÏcBÇ2œẫ{h˜IítÝÇâ²õ ÷Ñ­RmŠWÁé%ÕI�™y+H˜Ï�p]mH‡X¦>EK¯ õ´f‡:�禬Áy¥ÖHˆCz™I°hqej´œ\�[�£<�Qìa¬”[Š,)ŽB“+!ØŒû Z¬<<"™p)œ ¦é äã1?älší±1Pƒ#èjPUà<|]q">ÍÐÛ �ßÀ†^©NÍïxûøÍO¦;ÿ², N’þ5›ÖÖê$–:N<UC4æXP3ž®Q¥�\Hpk=×\r]ÚË´6Ç…?q#G‚Ï0d ªÆÁêߨ£uÁa›'˜óHÎt_c _-[ÚÊQÛ±UB<} ~…Æn&ê7�ðVçØÒóÞ¦Ž=5X‰óŒÞÛü‰É,rÀHƒCÈÀ#Gsâ3?õÝøs£úª[-NÈ w#boÛýÅJ^„¸ éÆÌ’™¬É&S¹ê‰ ±s6;ïg§¶˜L•C| [ÆIaœ_€ÈÿÚîÑ…aû>4$la±£K%[Œ}ÏE³€K,j¢h-‹• }³Z°�÷¸°A�°:Óõ¶q¾ZN+ÿÙš†‘ÔÒ*u© HSÿšž†�± WÝêUVqõ ÀuQ<5VˆIZÑ[̱�ï5„m¬°ì!Ð *ÛÓ¬vx�TMÏ)#1ËwN0ʰ餪qÄp$"È Ä!ÎeŽŒ8^BϘ*ÿN›Pxi¤ÝE Æb¹}î::Ç­Oñ€-BqžœÈNƒƒTaÁE:C ¦ i„¢t|Çx¬é¦ÄÍ<;ÑWøû𣘠î1iòD7h�Í� scÂ�æÜ+o�}p{ƒj{@ë� 9÷ÄIg.‘áÕy ŠìIf*°¥âKRe‰ÍÒ}E4E2è0Ä¥+@L™Iã{üÇ|If¦1š*ËQL6VcÛT‡t ‘ÌÎ/0% §®N´Ž‡åy¶ž]î»>Qq'QLà…?“ UW Ÿúì>/Àrýõt¸7¶.Îúï‡7ËÚi³A™¶wçîrÚ(¬LòÉÞÉÃ[=Dõk¾•Òøß‘áEºòõ!?Ãÿ´�GžrO!Ü^¥‹ê»JÁðÍeql+чøW6Ÿ•÷�\Æ2MQõ%_aG‘ÌWýâŽ=,›º‹4ú™ƒù¦p§FÚ‹àY€‰âi¶üòr}¸ü’Y$]Û)L?œy‡kS|iiÈç×þg¨×ü ×ÿƒ±ŠÀ « ´«”]¨BTxœí\m�Û¸þî_Áªhw׸(¯€m²Wlq—æ.)Š¢- Z¢×l%ÒKRÞuýÍðE¦lɲ•µû¡K"‡Ãg^ÈŽ”L~óá/ï¿üýÓ=Y˜²HG“pa4‡KÉ %Ù‚*ÍÌmR™ùõ’1´n –^2Ígz2v-£ÉØ�œÉ|�|¾Msj¨Y/™~KâÐ3šP"hÉn“MG’Nƾi°‡ÇÑdI²‚j}›,¾'s ®áÑÀˆLæ,žg2k žOK2/‘MJk"çDÓrY0M¸ÈÙ ËÉlmAÞŒiÎWaÎR_ŸøÑÄÍÏaˆœý›e†,d‘kB‰fO‹fº"Œf bè˜ Ô:² O$“e 7¶aŒ¤º"F>2³`ŠÕ:K1S)�4¥Ìùœƒ¾3¹\�¬þ FWèï}~YpXßâ‘å Ð;¯[ÔÑ~‡Ñ êYÅ ƒk$ÉÀg5V= Ÿþ�ÉŠªÔèo'c¼¹"þù�{þ—gÉüÓÝ—ïþ2àZ`"×Cg 8æ 4Áž­›½Û£*pA%W<-àlv ‡½8ÁQ€OJ‚Ï‚ÿ¢§‚(™jv­Ñö†¯è œË¸-loÅEÖνoä ó.ˆC -A&Z‹ãõàÔpƒSEr�Z‰>nˆÀT޶†0�ëuÔ˜el ›ŒX[!bßÏïN?3fÑÁ¦9öM#àNÉB±9lÌ:“E»Þ`lømÜ’¤ñnÜW¤1º¶ÇÔ»�çQ·¿©íš¤m­í³tKní¤Iú©n@®0ø;ÌEÐäxUéÄä>8€]ä…7÷6%©»:æ>c@–ðc�‹Z!€ø¡Žb ³\ÞIêyûô¸Kœhò¿Âõƒõªp×â¶Ÿx@Š„�Àˆ„÷ý~vt 9›Óª0¯Òì‡òYÈ«Îø æÒÍŠw]3×9Y#» !.µ­þ8œ¾bL=’»�F Ó«ÎêôîÕS#ÕÝRWsÿm¨kDL¥?2ñhž 8âgLd·ÚîE¾,D¦`±ÉÕæÐ !tÝN“›Ab?¦§ZóGiÓk)ñg—7'©¿i]��ì:J®_…³fôÐeP—·ÚƒD°i QïXµ¯¼Î_yD)xÒÌÇw…øÛ¯LËaŸzN¯·Œ C4éGªâà]âÖÓvSŸp‚3\ži€£òðFVµ;…ãùºR¸õ˜Ôë²#˜Uå Çc{8Âà ÊQØÑ-Û¬ý_ ‘¾–¢X70�C"БM¼G5°)¬âi8lM§Q¿Ï1™]ì�Œ|³!ûfHæqÐÔ±ãzH|¨ÙÕe€Ûs^;κGã…sHâ#»±ÇQ² ç*{p=2F H# Ýè33{Ñô9æŠóÜ)r²©nHS?„‚‹/hl†8OÏt[fÀ¥ìW”µHô¼Ç8u™×#ÃóˇeSƒÑòÿ²mÀÐÔƒI³lP <m¬×~�ÒP#;³NQ…»±ÙànÛ�¨Í&¥¶¥J_©TuÕÏį¦,_H_§S½ÙBIÁ1›õ›nƒüŠlȯHƒ|ȶ|”-ݺ7!®c7ôÄú¸Üßx£â ;ÇÙ°z[ Ý/EHþ‡°=%äá® è½û¨!ÎŒ}«<àT3\áOÀÑC»øš®«°òDüèÓ‘ãVÚçº`è×Ò¦aÈjÙbËY2*’{<Ié#$jÀÉd/–œ;4xíŃD§Fcüö¡ÁÊÿi±hƒëÜôíPÚ@ô¤ ¥+NÃ+€âZQ•¤ðÓƒ (À^ÙɵT• ¥ªìÓRUžÚu8z2ïuc.xyz0ôÀЗ>0ôå `ø8 üô€±qê©¢ÊpØZ~tb`� ø×dX϶©AôÜWh¾s³£Å/ÝŽÚÿï7 Í<³Eˆ#KX>º.‰lúÝ ÐºHtØ?Y¬ eQA*Š¿�/û�÷8ú«ø“)꾘"¿wŸ›ÍÜcøvê(V"¶—Η¥|>‡ü?|Ú�üúœÈ}%®»ú†'ŽÂ¾$¿~æšáË“¼ÊL� 7¿;«ú½å^1Jj9P€³âW9Ç�!]u7â©’†ÃSŸ%Æç”¤’GHR°¹©ÅiâŸgu§Úe�A¼K)þ¸ÀïV<|°Ç gµGY¤ì±ˆÅÚâ@IÎjö”¤ìiß Ÿ‹ðÓ]†q±ùíí9¡sm‘@2ânŽâÄÈð_ØA…®a6Z‡+>úêĶc‰šÍŠùQ©Ê¼€< OÔîC¡4ß�ênÿñFè’¨ì�ªY·]X·7{Jã”,e±²ä¨$þš2ùÁç–0Iݵ Ÿcuj°ñÐ"€‰@äþí Áí0âÆ]>iiÜ΋梞‹z.깨碞‹z.깨碞‹z.êù¿RO}\ûÿìaìþóˆ_Ä«­ŠI\•kxœ½XmoÜ6 þî_¡Ý°fîâö€bEz1P¤ýP`k,è°O�ÎæÅFdË“èÜ®¿~¤äÙq^‘ ¸Äg‰¤(’_nóÃǯgçÿñIäXª$Út�=J@)Ò\ xºhp·z·ˆi T�À5Tˆr«@ü˜ëÌnb¿mâVÂVg–÷&É$J<Ô`OÄ#QD)*YÂéb 8¾.’M,YÖ:™Ý'kÚ®Eª¤µ§‹ü­ØYbJuêz"6×Ò$€›˜ŸâTl¶I(e›ˆŸÙÄŽm×Ï%Ò“ðâyQ‚õ¤/uâýòé³¢W|â9¯rPª¨ß/=ñrœ'o¾IÕÀ¬‰83 „¬|(ïÊÈïgÅu§giWožÅë8Õ+ˆ¤ÞgÂ6¦²N|Yã!–ª¸¢¤Ïw (–YF¾%¨L³Æ­IãØÛ•s¿C ºÄü>�̧Ó!ÑÞf—À•�óBßÚØw¹ÞGBiú’€çíÀ522Ö×J†t?Ñ‚M”¦”N€S U òq4NB„½Ž«��»ì:N.Þ>O°ì§*{x ßb½‹\X[ɹ•#اÌðz½ Ýåó튪°¹3àgÌCC‰õ~J ÍfÓÙ‹s åc4Mø²£*Ã@Ŷ¶�ÀíâÁ¥9”(*¬uƒOˆýÐ9×* ¥üIϧ:;éЛ@íƒìšÇnƒ¿PyK"W4?´p9‘»>~*Ë©H.ü›ª†ëÞ’S'þ3h»³¨k¡xéö’dÝ»â2l”¨šRâIþB—²šzü¸]Š„ É+7°£Ú]ù˜'Áû×E˜Ý%¥¼/_ø¦þâܽ�~g¾Çº8'D·âœd÷Šž[öÞSß›xv:ä¶ç–Á�·žg&ìd=ûd v„f4B]¿àwãhûýþam:MÝ=²1(¥¹lÊ~Ö@×#F' C§L[N}[Š9årþTQÛ7pßH�’Ø6èÐëŽdq{F5wæÔ¡Ëôê„°ûzµ=¬^O¯vê•òs‡k—ökVyíWpóïüÐ%ÑÐè/yvS@>èØÝH±ç~žëÕw0ÚµýÁ&Á¦/ÞµÑT ±wY è&42–gfSjÈ} ¥Y�‘ðxtîwx \ çûXVÃ5nû‰ýoJÿaª8c 0 %0�r_ŸhNxœ½YmoÛ6þ®_Áe@³JÜ(VÄŽ�¬]�[64†aZ¢-®’è’T÷×ïŽGJ”lÇq–H›ä½ß=wd&ß¼ÿíÝ퟿ÿÄ [•ÓdþžÃŸJXβ‚k#ìåQcç'o�F°n¥-ÅÔÊJ¡¥0ì�Ü ÆëœåÂdZÎ;îö�'#"I&#Ïz¦ò5 :›æÜr»^ sÁâ¤É„³šWâò¨Û9í>M'#ŽBΧ[÷�Ç9l/YVrc.�Š7lŽD™Êcë.Øäžë©…óø—]²Éls™MÙwßOFŽl2Z¾KwÍ£Cÿ—ôÿ×’�zùõŒÚé>7¼”v½_ ø9�¯ö™ê¼*DYÊåÄ:òãkˆè±—çuÙäi[l(Üdjö�ÈìiB¹¼ÊWæäÍ1!Ú¡²QµµéË´°�® ÕÒ®™?±’¶`µrå„=‰ów £y2ôKʈxf…f÷`±Ò ~83VËzÁLÆK®Oûõêö—«™åŸ€Oúû¬œi˜-Ãajê¨Û{qæ³îÝ÷¼lÄ~Þû.µ‡Ý®�Ps�Ôí6NÙG—›l “¢Ð²ÄúÉWçέ ÁÆ—>«› ¾8°Å¦µn%'~>T±Ú^…}ßo*°ŠâŠìxÞýzb=¢¯yšÂ»}ˆsާI7ŸFf´Ï¡D“ì�y¤å‡¸#âTâ‚Þe—Ÿ6pœÁn:D×NÁX¹¸¹âgßçüüÕŸ²‡ê Xy4ßurJê Ô`ÝzgÚ zµJ¨½ô$È4L)û<¸öÈK�Xw ï.<æÞÝEû {È3ˆ…ýï�Aû�þhÚ‡ó½Økva¯9{gSó4@L7ðô`<~‚¬³MaíRkžùþzãñ³�~š{;ÄÍ~e6ZÁ àÑ3ú€Ù�»O1“¦}|æH:-6øl�aèÇ[í¦ ‚ÀÄ_¶ðnX‡E0PK�ïÕ§ÙK�O§ý]¼a» Š oÜ3Ä”À)u“%¢KÝ›Ÿi²‚Æ~�›‹PÝÏôÑ…‚dd”oâ§l|Ä„iµPÆß¯s¼52ñO›¹“º=TE�‰Àh×0Û¥î 3éuØJår.ÁôcKÁï‡]%-[qƒWþdè{j'µs:ľóòv³n1j£­â%ºkv;N'à}w5¿¢vÞÿ�+âg³p�ˆ�@w;�†›™¥;=^E¾n'Y :Éâ%;ÉÈÿsDÿ6ýååá‹X^—4Øxœ¥XQoÛ6~ׯàòÒ9^+RÇ@¶f@€6ë²Ã0ì�–h‹+Ez$åÄûõûŽeٖ㬂ÈÉ#ï»ï»;zò݇_~züãó-«|­¦Ù$=/ñ¨…笨¸uÂ_Ÿ5~>zw6Æ{/½S/kᄕ±¥5Ka=>NÆq0›Œ[#3S®ÉäÅ´äžûõR¸+v`-&eÎ4¯ÅõÙfÎùíJhïΦ“1'S—Ó½1¬½ÄÐ’Š;w}V½es‡¿¾z,,LÙ?ñUÚvÍ&³i21Ãaâd¼$cSöX &Â(3sæñ­Ý5‹SJ¹J;Önôö,®ºa+QxciM»³#çσ©vffc–Ë&ÆYþ•Æž*¡i;+·"cÚh‘³Ùš•bÎåÓþc`²{¼ŒFN‚+‹Îñÿ "~ñ ۂ–·`¼i4+ß$L6ÞçŒë2MëÆçÀ‘gí®¬°‚{Q²'é+ÀM ôÏötÞJ½`®àŠ[&1ÛèÜå{˜^gÌölÅU#hØŠyãD™S\’O7�o~d¼(ÄÌ�¯Ç°«d|Á¥v>€JCIöÜ…W ˆÖ„W¥p…•3òÈš'—gt§dªre°­ X KÚI£óH^Îj¾>‡#:|¡cÕÔÚeíV1MĬ r âc„ë6¸ÏÏ÷)¾Á_’ìDž”™°mУ\ïYî+A<ù! l¦(úŒù'CE½ôkŒ,*¦d“4g‚Í©<1 hË…&¨ãI%å–ð1멞_â`�ÕÇ5G‹îôÜf#�žÌÈhd˜•TAƒîÚ”HŸ‘½¶«ýK·‹Ã7 � v9‚{‚»ÑÀ°«=Mù�½½"9—Zz8”¹Q)(u;¢D8FŽ€g-`“·SªÔô îøç_°ÚµgÊa½Ñ”ùçÖÔGþæ¹õˆ¡Ø©»Ü…Ñõ@&b+I8MäÀ•2O¢ "b�vƦ É©�T Ž–åT~rÏ È„Ö��2�*YmV] o!`m<[H4 !`»»x°¤ðä^iš™JN\¥úôUˆeHx, ó6¹‘¬<9j�T‰]ÄSrΛWU³àaš�¬¬�‘×)«þžÀhì�À©kgzᫎGñe Èλ[]vÉ´KQw¼­7ý]††„h²ŸØÂ¢‚kŠ.ra$Øô-bÿµáJúõp ÚÁ“âÐÙh*Ø?q�ÑûP #_Öÿ]d£ e£‹ËwD»‹Ëò^éàõP§’u­ c·23ªE;çÉC1ìØaâ$'FdL(µkflB+ÿ›&¾W„°H(-hÈCõ‚*•tÈÕ§8¿….²8.;ª¬Þšcz²¾¶ì K,ö,K�V$©¬p·§3ÝÑYvJ%»s¤šŸ¥ î €Ñ›p[vöÁø½�Z»}žEêïuosZ¿ißú5ÿeE´‡³ÍvhЩ[ì"¢ñŽÖqáœ+·µ2;vÒí>s§9q±�ì^»ô568e*YT”„«ÚCgè.æ++ĨÛÍvº�5žDßk¯©ãv›^—ê]ªn[щ^:/•pŸ*r×Q’OíÎÃyjÙ‘0*Ùxy££R6æ[‹bWÜïvCÑîjuÆjé „…ÞÄ%´ê~¸z¡�Ò-õžßs÷I:êyTÔÝi§•Ö}k/sÞyî‘Üdá¨ìV�òd£ Lu•Í4¡y;Nû›î /âù€6—Ñ÷ìûª‹À´îÂ~ãXÖ‡�M%ÿ4“‰¿‡„«Uïf•®D2ÞUX=ÐÄ 3yaGF«õñä9neÇðþqv³o &&•W`«2-¡xœÝZm�ã¶þÎ_Án�$|ëÞ¦‚³ÏÀ5w.H/MnÛ¢(‚-Ѷz’èˆÔz�_ßy!)J–wíÝÍ!(�ËîJäpfÈyž™¡æxýÃ7×ÿþû¹qU¹óðC«~TÚ)™mTcµ{yѺճ¯/¦ðÜ®Ô WTÚê¦ÐV¾V0PÕ¹´ªÚ–Zš­n”+LmçS,æS/tiò=.ñ|‘Ã,·ßjûBž( &‰¹’µªôË‹nÎåZ;Æ3ìÅb>U¸ÄÕâè�uC¶2+•µ//6_É•…ÿžÁŸd&O-|!ç7ª!�çSüM¾”óåb(s¹�_Ð@gyØ„çuÎÿi>%ÉóéW_ÈŸ´k›Zº�–(Iš•´ºÔ™ÓÁöRð輸 ÚVöÙW,€5=Ô®¯Ú‰jɆԱ¨�ˆ ¡rAN^|Ë[7X£`»L“ëFÈuq£ë ¬°ÕÊYx™•m®ó4¦1»1ÙlÜk²‹\™F*¿ž�»�±š�lZP;7»Z }U4ÖÉ^Ԏ΄µ,‹ìÐUšzÍãÞw“¤�‡Í®°úR'´¸�mu@Ï­±…ûÀ*§×ºÕj㤾ʹΠφ|¯ëµÛxS&’L)ͺÈT)+e?Â$‰ÚèôQµ›6óÿóópÏÏñÁ3b]bïµs­�Ó<õ�˜¦ä¦Ñ«4 > Ë?@~à}ÿÐá%BÄûqãnô1ãD>ÉBɉ.~ÕÃõúoÈS�S€ƒdÞ�$ü!ða=$ô?î œ‚‡ÏÔ!ççßðãŸÓ¨h“23e[Áª»Âm0ðy)8$â‰T!ÅAÞ $„ DÄpDûDZ´¼’_öTS!¯n¿¼ýKx£šFíÞ:„ƒaT¦@ÅÚCpý„f�ï`ÃÓbå>‚}¹:{5�ÇójÜ�ÇðjªX§Õ(5òVo¦Ü˜’HÂÔåþ²�Àa.2ˆ‚(O}Ô5,xQ0ÅÂkåô ü‚¤= &æ…Á‡ä{@l‚˜ÍÒée  s‹”×]4œ·QþÒª²p{‰ï­\:ó•¨8LQ³=sªÜ6xja4¬öQo]{uýý«¿ÒécŸ5Ú¶¥óÚ|ÞÖø&ÿ<’(ЦX«¥®¶ rhPŸ“Dó‹¸Ež¬_Ô –Õn&A[MjಂVóCPzõÇ�X‚i2SöwÞOÄÀ<¨µp^ñÅ8åÇ×Oµp®KÔ[/>=¾åĊ8;ÆqÇàÌ�‚Ù§ZvbìUYšÝëv L €r�+uñÆ3V÷kú`¼?Eãçý?¯Ng�_•ƒW”€,µÛiÀI·3 k�ã‡hÅI[Ý/¼ÆR¨c,�FÐ2©XÀçmÀ‹ ¦J-¶b�M˜„r ú(ˆ!>[«ó‰lkjÅ Ç¡K|\ÓêxF˜§“*¿òRp¾Ièf(IgLŠ¥�ôøÝgFô‰ÚûkÕ˜ª?”ê ˜0òGJíþv,d+Ћo땹|ïTã ;›7:°*ìTÝVK*' ®kÛþ»{(«€Èø€ýÔUÑ·nÒe¯øöëÏî‡\o ý~åVÁ‚Ve±µ|rYT¬QP²ôx§Þ¥ ô}§VN7}÷Qv‚:I•¥|Ó'æeëz[äSˆ™83u@.G;15¢ žÒ‡‰`æ·ƒ|àÿ®š~ JUydŒ1i|}.�¦´�cÞZ´{@Q]3,ŽÂ �ùG�îª~¶ÑeYlgqý9E¨àçO¦Åp®wMáô·¥Zÿ6¼~Æþá�÷GØûUk5DÝ€wgðrT$jñ°$Î:B€‰ ©³‡µ\Ò¡U޹ž±~ i’4u¨Ù�¯&€sz[øî…‡Ë~ 8ëZ$]n£ÊF«¡ ö©vP›!¹' R©ÖÄVg¢)¥*—j­À07ÔËEJ#e  –(Ž·¾�¦‘¡¾TÀ-^Ϫ…wu£{}œÎÂÏûKkf1…™íªPQŒÅè~” k#î%ß¡i`ºgàÏ(a�-±/…’ˆv‘L*µ'f€œ’©˜|Dd|œs�U¯Ñ�x¢ƒ È�+ƒ ˜EcV S» j^UÔîë`{C€ä�°˜i—e´3$˜ÃÉ Ë¡.f‹KyBż¢R.Ûè�n¦j—wTÉRá�2YÖ6�®3<‘4M‘Ó–À–7e¡ÉiÜ<Ø÷¼Ä- ’V….sò¦t¦ÚF÷°"b@ n©¶*s//ü/ˆ¹[]`3‡r¶°ßµð^¯õ.œ)DŒÀÐälâÜ!u©+ˆªIPÞ»vÃV`ìOœWv d2^Á„¼½rY98‹ì¥˜p ušòá>sÝ/u` lŸu.&¾êxGà�…¾÷ä}ÿ�ÒEâEÒw¤P©rÇÁ¨ÑÛRezpãĹW° q)¶z‹f°iÅJÙëè2¼Š²1 d/5B(C4wÀ �%lH®W ²G«ÜG+ÿLª<§ÖPpØ4/â·ÏOHÚž"7TÛ­®óÞü褬0Qr,+L´}xV˜9ž¾­s};`ëcãǤ8')óOU¶'gŸØ¹¨?ûuèc ¨ã£œ…Åê5’Ú$mê;oF±ŽwߌÎbŸ9G!û|bÐ^}’6tÛºi¬¯ú7ªi_C‹s4ë+œî°Ó6´óXÓ À=J�&�³€™1ÅD+A`góC b¼Ó0žçŒ¤9ÎS¸xH¦sv–q´ †}€¯¹›Ó' (†òHÞ�»aÐ�ÖÏâu œÇÖt!Ʀ—ôåAøª » åÝ …KL HöQ>U#!©ÍÇKò'¾¸«_q#ÁÎhÁ3Ïî{¥¨—”€ô 'hcO/”ÍÀü�)êjSC?|bõ Ú”Ô:_¥Ø°¤B$ä�¾KœâsL¨T³néÀ£¶0á²îžR4C «šï¶ùÒÅNü…c˜å>É{ñS­A�ìÍöcb¢õ†Ê>Jeà `!bE,HÑŒ‹‚°g©…4<£ÙƒÖޝævFVZ;ßFôÌñ‡ƒA×j+làkË i±ì¾¢€êsÆ)ô° ãeXÍï¨{+Í�×°@ 2U?‚NUqK^»Ž +ßPÒŠ‡Ž}ü4L ´–ªòŸ6ð�«ˆLSÛ'RÁ5~ Õ+±tOÌú±‡Ÿ(ë¿ Š:§žÔú5çY—† ¾´°È,‡…@£ìi`{ÇÈÔ ¾|œ�Ô5ßÌå"ÍE lŒˆEŽþz"u2¿÷Và�ÈÑaPn¡—¥Tkà>Ýñ;°c—бI{ËT§E[U¹S{ëI®¸«'ͦ‹£Š3Ö>;Z+*�üΦۑܺ‹‘k÷#‡àwDO÷²œ·<\KÑãq9ö@.Y¥\2"ôþ�·®¡@ö¸Óµ˜êžÉ�6ªÜ÷×Aµîƒ±4áŒcsùKD¯°Ï>;ʈTT¸¸b÷™c$Œ`Ä[î°¨Úîð€b£…fÏ. Ác@leK—˜€—”ÖÒßÀ¢“ûÉ.oÎù>¢̧þóè)‰ý?·‹®Â D?D�xb›t9Âxœí[[o+5~÷¯0AB ¥ ­t$tšF*œ"!ÁqúÂrv�î®ÖN{òï™ñ%ë½¥IvÓP©RÛõxf<3þ¾ñ^¦_|øõ‡‡?~»§‰Î³™ú_œÅð+çšÑ(a…âúv´Ò‹‹ïF¸®S�ñ™Ns®x‘rEÐj:±d:q æ2^£º«YÌ4Óë%WïiË< SFËùí¨¿dqÌQd4›N*ºžµŒÂük\Ò(cJÝŽ’wt¡`J$ãÐË÷túÄŠÚÃßô–Nç³RÇ|F¿®Œ­¼Æ?¿™NŒÎédÙ°?ð¯Ì.£r5¶ú1£wqL�:Eµ¤Œº+§OÞÕ\]¼Ù9ÖͺkÞ¯�cAÙrÉE †NœR­0Ý—NÚŠÉù_<ÒT”[7ô¡ã Âið…áæ.ˆsUSó,fP1/èc 3ýÂ\å‘߬š b#oŠ7®+ L×U\Ò†P zÌ>c‘†5F˜ÍŸ6[—H_ óÌñOËE¤¤1­( pÏé ®Œj¸Ø˜ý¢;�\k)h¸X¦4¡˜ò·k¹°6b-Wx8±–:^&¸yu³þB©þÎsùÄ=¯Äìì޳]ƒµ�öí®X¤…ÒZ™fÜB!- b¦ELqà³²nêíd^ÇuøE—©k^£"΢äEjÞ5LíÉ ƒF*cG,¦Bi€ž±%F “Ùº"eÀ\Ô;P!¸£FPÐZL›TˆŠGÀˆk‰¸Þ0fæCc ènƒ›ùØ/�|F´ÕÙúXöà­­÷`cÐiΡZøЩKí >×! 4•¿½ïïÀ‹€¤WEÀÍ~o²|™Á¿ÖZYN{€TézÝïÝÏ…ñÎí¼6¼ ‰ÌâÊóžÛFÇs:âUÅìd;3 n†,’ù6!€>3œæûëðl‘Z5Û{¶MÓî5–ë;E¡ÛPQ€^‘n`vecßX…�G%âÎJc§Sp0„ÝÝœg°}±£ªøÜ©=’Ú®€Dʦ£Y^ªe¨/¶ÚÃÀ–˜¹�U!R#¬M8{\™Fr_`&]ÈÜÌv`Ÿ/õÚ· ”~’ÄV°…Ž}Ulm´}þ�Û"�ñ°>(”Î6³2|Ô.ö0ŠعÝÚƒÑ~&¶è§~Îõ3çÂ^[üƒáCŒ Jè›H¿@雘Cêu§£õ}\„Øñœ 4¿»œ€áÝ‚yµÕv¦÷aè}ל¶$1 ?(Á“20g‚ÿOüVz?“ûÿ›Ü_¦õÁý¤Tþz$> sŸŽ³�ÿލÉPL½!P2 S[H%ƒ0µ'j2,S÷ãè EwÜÞ<ôn&’!8ºAѤ'G·Þ.8Œ£Š&}8úÍSôëòÏtýJt] t7gW"Þ›¸+ÚNÂÞûzЛÂÍ­Bæ�û§at„]¿šó!|jß'£ç3øù þÖ þ¨ý3­QØNêCRú© ýÕè|P?‹ož–Ÿ‰|h"ï}L??+?Óø™Æ�{à�PyÍÍ-OË+þöZ^Q·•S¯š—®�ö}·<òV%D»Ó;Qýì oŸ·|cƒõí°6Òó°SسQÀ{»Þžs–ýW7M�MŒçÀ6Q¶ŠÂ…œØÏBÂØ^”ï+4Œ´ßã›·Ð ÂÛ&#ø dËmx¼íŠo[ÁF¿h?ÙõayïüìV|õÃüU[@ZòÂÜ­ïŠV�4ú7âÞÅö�SÌWP¶Ãízü È};To>â»Üè qªÒ˜¦có-Ȟ锇�~^<ÍïI0÷MåÄ~ªù/¦Kû±‘>a£óxœåYQo7~ß_Áº¸ê ¬,ÙMÓ@–¤Mܦ¸ôgßCQµKi‰ì’[’kY-úß;3䮸ZIvš}¸À�,r83¿ù†žôêß_Ý}ÿŸ×¬pU¹Hæí‡à9|TÂq–ÜXá®Ï·¿8›À¸“® '+a…‘²;ø•QPŽ;©Õ|⥒ù$h[ê|‹º/9™m-ìŒ=¦¤“9gŠWâúl'|¾×–Îó G½—‹ƒó ã¦k–•ÜÚë³âs¶²ð3†¯g:�÷2có{nÐEaçü•]³ùr+\.Ø?IÊ‘OçR3ŸÔhjÁþ+\cs…`¤‰é}±¼ªKøÊ-sâÁ�'~E.ï[÷*;þüÌ+ñ®pgçË? Œhì°L—M¥X&Ê’qcø6A_ðPyæ„a÷"sÚØ´ó§ÏŠàkç¹?¤ž¹Y¤³t†)“*8ŒçøF­ôùÿ”DILÁ‘F9‘³•ÑU²/zë¸q¯Àz?gì®[ðX°�‘Î …п’âLOØJ›Š»}µ74Ú¹ Måù¸ªªñþ±o¾™½};»½µÁÛB%` ôIËDU»íì?Ó*·ä‘�­ä°§É£J–¥ôóŒÃƒ$v,kF{•âyò›år•±—mx7ÒLé.OÖò^´Ký­ N2\ðHÓ”UtØAˆé�l¸_”7…DpOÞ‰ÚÍv úZ?1µXâCÉác‹ÄWþv·?LÙ‹çϦSvñÅå øø¤€T‘õÕ�»’þºÝ…/G6¼ÜRáOƒØ+JLo–·F gÐ,/Á‘®¦t˜ñÙl:…Ÿ]ÍÀåa)$P†U:Wr]8ª&XbñäÉ*ÏBåëa\ >ô~ž ìÀ#Š ÞœÌA©=Au:¦³¯—hÊ&>«v¬i ³YvÁÀ¹î À�_jˆã¸úU|̬¥�C„òì3q`0 ¬Ï§›ßqÉñ¦c- ´01 oÀAL™ oôi×úüÅ,}}˜¥¯O±ô÷î%1‰õÑÔSÉ{OÛßSûA$¾§é�ö7‹x†cW‘I� ÍãÿO÷ç�‚jpàŽ¨ÿo^«ü@l„ÊÿŠæçQŸðë¢7Bhu1º uC-åôK_#°uÜ’³àRÜ?ýÁF)ÚÜ`cûîàãbɳÐ'á’$jÞŽöJ@QÃ{”@|±5(É=¹ìÚ¥$î—ZJÛ Ù¾ê¿üÖ�Õºe_ž¦œ¯ä¨“ô°�f�tmtpÐÂ÷QÅú>z1 $ºæ 3_ÀÎÃó²Øue(•Š”f´ æ�‚é,kŒX=À'c$2УB eÐ�ÙðxK1¨«Á8`<*ë 0/†Ô¶Æ Ñ·%ñ£î«­h-¾÷•×FVÜl!×��ÿ”�Ü×gϦÿ8[`Rá3sÏ<Ǽ[ÖÏLpáÄ…'hÒ’÷´Ã+X”Ǥ˜êæ>ŽùžWÔxF~Z=em�Ž›ÆÚ÷ ëi¶ö°æ„)Ö2¬ˆžfáÖ¿ï>5ù°}÷êÉÙw ”ìE(eK`ÏL<ðÌý±³/·'¥Gr*NºP• P¥vœéì¸Â§khSlÆK ½ŸÕÒc¥¨<òþ½~Äb¿°âß¾Ã1ÿ"Œnß,0’Œë@�AØo;V¾r¡+…ˆxLTø+0ÝRo’�Jå_üó}Ÿ2»�7ºrlE军Ü^µ¯¾ßÄ?Ȩ€´õñû?Û’u^‡…HN&× šê¶oøŽ·÷³kɉä§¢C¸Â‹™/"IØ‘�QHã¾CQò:ƒ^/Œú7ü»ŠýŸ¼xOÂß'þo›¿×|ÐÁ 88‘Ec£ -xœÅZÛn7}ï¯àj�(FË€�À3Àk{±A6q²ÖË"N7GÍuw³C²5ýžâ¥ïc],Ã@`Kl²ê°Xuê�Îúooß¿¹úï¯ïXnËb“¬ã_‚gø«–³4çÚ{yÒØÝÙ'KŒ[i ±±²Fh) û±²BתàVªjÁ´0¼¬ YÝ,˜9Ti®U% ~]/ýÒd½ .¶*;�ËMÆ-·‡Z˜WìQ–a"YsVñR\žtÎo„•Îv“«ìd³^ròøbs|¬½Àœš¥7æò$Évÿ�áW ©ÊúûÅÖ·\oÈõzI?±K¶Þn&F·ö½›i�Ÿ÷l½t¦ÖËšÜmØ„mtÅl.ÜF˜Ú¹Ÿe? Ì›;OüªLÞF˜¥9{yâ yˆSX#LGð ΄Ã$ÁÎ)".¸> Ÿqø“ÊãS:Ž–‡6Uú/^e…è–`ÑÜ/QX¶ëc�@Îß"~¬vê|�aQÜ:íÓX¤ÀÍæƒŒFá(–a(a ù�k±ëçÃõÀàõu—Q×׃Œ:§Zø{/GÌ8‘Ž£[°Cù}>8Èàaæ lÏŒúÄu;_âÔçÓ}‚r.Ý'pgÒýhšÇ3òI>15IòŸ:FYÿU½îò»}ÞYØ�ô-ë#UþAØ//ë(óÙ¢>Zg¿.–g(‰¶DQÈáK[ ŸÚ��̪±‹Åæè±DŸ;¾ÿ$´:S:šåªÈì£5Š� žæì– È£©¬,<7Š¿,ó Ÿ-XÉmš‹ áÔK9 fc•öÃ…ÚÃ2}8OØÏ¯¯þýúþ«q+wËii³•i‹LViÑdˆÈâŽËÂ0Uü‘ôºÑŠåB“WxÚs·4˜°C|²>_=þÇ©ì��·‡'(LÜbÇÛƒ‹Ò,ì2+wT<´²@8Š“¹‰¡ä}• Ú‡Øwз�bΕlÏÞOÁ1öÆ’v°3ö¬í {isÙ•AJUÑ”•qQ§Ća¦€êÁð °¢x¥6–e­ŒÓFa…â¬ã…_�©QQØt-tÙØ§:™q¡O +E{,�df >Éö"µCÛߦçMÄÓç4Ó¤ç=€ÈEÏA}³6Ûášë¯ÀÇ7ÖÎÆ±þbåvÔ‡¸ ñÜŸ¸¿~!’0DžŽ´ßw�(Á�á–Á-ZÊÞ³Ïcp„|ßsŠúÚñ�Ç�ŒBḎøÍ[¦^R<õìNã®):蓘ìTS�tZj_Ík 6^KY5%¥ jœN´{€ßióg£VèÂ0¹‚ÝÅÏyâ!8ÄúNº°\[Ð}«¤E¯>$Ž´ØÖ‘º0 ¢{K ÝYô6è‹G:õÛ M½J€í´*>òãŒaCWNìc'AS6J[jà àˆÆGQ[Lüâjs„óºËÉ/­ðÁêB쀬±À‹-4]B�aò tE‡Á�ç„–™‰Þ w} Aû È]ÂÅ[_˜›DÄïn…>°Z+t/{`ÛÆ«È˜ 2lí®–vßdýL•“ê‹;ª½¨¾ž^£d3Vðl�á÷?â4’‚ýJ‰‰ˆ¬þš{�’eJ$ìFÞ†ãü³á…ÄYÐøx:%­K—(ÂBë´hÜßo~¥+”7nM¼'+(9:è˜ÅýÊ$Ê@!Ë �`¡”H-S�bæVª½½”kº œÚ¶ïÝmÏgeÂèd…‰ú{á2^°ErÝ*7Õ!ô¢'-3% Q~2— ÜÓ³ücˆlÏeðDô�wRŠŠ´wª;øèÖF„Ø~H°Ì>—Øø‡˜gt‰ˆ³p;x?­[îÁÈ¡ñáJì\-xp=àe2Ô.ä8µ7 .!e cOVA”«ªe€ÄÝ7¾Ñ“Gwß>wôï�_Cö=ð­åîW‘þFf_Dú;º§hû=pÊÅ�TÌ ÿûá¹ o¹§hެzBmõH|ߡ Y¯&ïaÀUêÑ÷ËP«v¯bÊ« ¬�ª²¤¤¾¯Âºöð;ƒ:Ó«zZ,vÐ-¨o€Bw9ÇCNOPù¶²¨¿»%©º%ºKseDEúl¦1κg®—§¾uög䜤µ6�Q½j$&VŒ„LÈ2Ì ëzíŽd±ª)MÍVÑ8‡Fé>ʸQúRNÖÊH ¡n"á‚^ â4‰™sA§™‰ljµ©ÂÓBÝ0zõô7qÃ�aÂོ Cø’( #œ ›Všo³?;¾™ÙÜ4ù©•^ɯzø½<ôÒ?\+gÀaKšv JfÚeU£¯0çd^îþáð¿ká�^«gMûIŒzÙO ˆ <žA �Å[uïê9õے㌿\íÑû”ñuêw^B:qªBis—‹ûä™wy!Î.žÇ ôÏ«YPT6ºD¹Þ!ÛKñUN÷Í t3•6% ;/¯Ü3wÈ`ySµ¯çí6ß‚kéÏÆ&3ˆÂÓ,IdÿñùùEë Ûñ„Fœñüå€ñ�è—¨gÆ=(ø…ĺÓvðÛà’³SE�¸ÓÞtC ¿­öq}µýзÎÎ%½©_ƒ5®ýÜkz!pîÍÄ ¯k°ðÀ…šêèeøÇù¥ÿ¿þŠm ô j¸jŒKe™*ÜxœíZmoÛ6þÎ_Áeº'Æ Ç@¶îÀ­°À-Q1Q¾($•Ôûõ»ã‹-ÙŠíØJŠÄ–È»‡Ç{ž;‹š|õúíÏïþþã:÷JNÉ$ÿ㬄Š{F‹9³ŽûË“ÆW§?žŒáº^ò©Š;nwôÊ ?‡Ñ¢ L—´0ªfV8£'ã8–LÆÉæÌ” ôðôdžùEÍÝ9ÝÏÌ!F5Süòd5嬖�;™NÆ í¾œ®Ý�y/áFM Éœ»<™¿¢•ƒ¿SøêaZaÊöZÎéä–Ù©‡yøŸ^ÒÉlí̦ô›p“Å{£8t¿};S“q�î¦ôÝœS×(j*MÃø8¢eƒ~ß1’lœ‘h¥·¶r§¯N¢á–•Lĸ>j¹o¬vÔ¯ðuAØIgæˆ:¦jÉél‘>�QúVsœ FUlAgœÂÖ4jÆí9|r“ÌŽ¨±`�Pf-Œ Å¿a¢�ùÉY…ìΤXQÕ8OçF–”Á¦³kˆ"¬Ä˜š[æÑMQðÚ»8ó1¡¨ÐÑ%$ m´ðp£ È<^v!z¿L`¦óL{wA=Œotœ˜]X,“¬ˆªâƒ$1q`«€,£D´g¹kd0 `ØÇ„:â[‹üˆÞA;fµÐ×€ö*b%,R‚é6 -:©ÇK\ÞÊ¥p�*¸™aã_4:|‘�EHj—¥µÅ€zôUY£úðÁ8Bù-×>¬`GTzÓ0)ü‚¢ñˆT Í™…`znk#™F_d8¯�þ¿êÊœ½Ç�ɘôGØFz`x°�—>g·|¹C°Ò¿B¬Hʹ"²‹Œ‘r3ONfS€PyàRFRšf&yFp7‡Ð‚­”@¾V‚Nœ·F_Oÿä<$¨Å8]"”‚8Ía+Úâôa¥g $Vzv†ZûuK¬”ШcëWP×FôhëácÇzø8�uµi^-í§ð�AÍú<­¼OÁSŽ—ðdè �<çºà;¤üt()X¬åœdCÓ�•tÒÕô½%©‘¨'0ϯuëZ€Œ šÝÖw1ïN™�±‘Ø€°G§ôïËàDƒã38: ƒ¹ä äúôN8Ž Y6…ß‘Ëgß •̱Ô=4™Û�Ió0iMîkUž4­¡Wq±ªAó±£*–PØj^ 2œ­bb ”^%;´±ÑÁ¸ü‚5XÞ/èïWï~»ú Š!¯±ì·ê,é/´OTû¶Õ�êÓáÌV[¨­ã¶:œÜŠy+>íIëÁX­£u,ŽÐ‰ÏÚ¸ÄÝ ‰€^slBûúLçǧóã6›‡“ÙÂQò~6§›CÐyiêØbÝ΄mÕz<¯ò#ëuösÁþ.3“6¯ õ“Òö¸Pvåc'Ûå6¶ËáØ.‡b»ä•_¦F?ÑÿŠèòqˆž¦"Ÿ»†÷â9¹ÿâ3Ï? ÏûHh‡åyŸ”(ùž«­e] X×Õ1…=õ¾V\Ï=E;N½£°V×Õ¡…½�št`w€f¾ß[×/(gÅ´${ŠÕ`½‰:´9i�îѪ½{“g­úº˜�¿%Ð…¿ˆvê¿éW~s Tm-@+UÛ ®ã¹u7ÿ8žôö*Àååq°‚ÌoD~†y{ –æZÀφH$[,LYµXx) f)Zû@¿ˆ)xž k_s½ã8-„%E(D½¬HÏŸœßÃPdâ²Ò"Bdò±u(¤ÌA³0°^¬ÁŠÄ gý°˜\ÛA•âŒ7ìM‹ä�Žà¼ÁÃm.«'¢üTD¿j¤k‘/YCA 4Ó®Ï]gÀ^´\bïãærÇtijP–B½„d/þARõÊéÓ�óJô3"½ö…õh�¤Ã��,ÿ\)Žê½&Úÿ�Ä^³¼%Ë×\ –òkv¥Jµ2äžü8€!à{Ò%—€‡�,´‰ë(L£ñ0æ–K"£¾�ïåW7ÇÇéÕÒq|‰õ?mЧ �Ed¡0Éxœí[moÛ6þÎ_ÁyÀÒŽÝtKW$Ž�n°k÷ÒÅP#Ñ6½E¤œxÃþû^,9¶ÚESäÝÃãÝÃ㉞|õú·/ÿúý'¾0a0e÷!…¡4‚{ ‘ji.™™¿Œ¡Ý(È©Q¡Ô2URówF¥�òôdl²É8rû+y2õt[%RŸñ c¡›‰P^ Ê>£PŠh0�Œ z>]{ãžÃƒ„{�Ðúb°8å3 ݽد¢<ã“¥H§ËÉ?øŸ\Míð«)BÏŒ¶ŸNÆ4x2N‚áß1|5R0´]q¢µ†¥²¢å›… •œ¯¡™ò?¥ÉÒˆ›…ä(ŸÇ3.xnÎx)S2ïˆÙþ¾Z:ü¡>>Xû^Ön ž’N]S*…·à¸ª\2”‘a¥ú!×qÝ aGkõ�ÄÑq‹0 ä9¿Uf‘c:ÂÑG¹ZÆo¥š/ŒŠæ\®¬ìqlHŸÐŒx]t´Òü8» d)m!Œ$ˆ †¬‚Hð NcÈEäç?|t3ŸÅii^Å,G®�•÷0i§µçF\Kͪë%FÅ‘Â|,�)4gK\CØÂ"àÔP“æ¡0ÞÌ¥"˜åŠqOhyæ|$à^&ƒ0Ïÿ@?ñ�›ÕÑ™”Y®Ï ƒ?% 7ö)ÇgdÑÜ:ÜÄ<�b ˆ3ÖJkXÌs°(ÍÏÀmЬХW¬ß[ñ6W9ÄEÈBþ Öúi‚uÈ�o‘"0Q›ú ©¤õüV¤` ùpûõ‰½±€^Ãò·L.ï”Êû¨@B#g" @…Õëff§¥Qû9sãUddšÄ8ßQáV*Ð�cXÏ+in¥ŒŠé°˜" V¸Xèì z§Ú´ÀïÂc3ømtðì– sne#LÛ0·Ð¿Z!ˆéaÀó�—Ó$ˆÌ×$ïéÈB�$s ‘CýÀJÃ�Kˆ¸rvB^p},^D{"§¬�{BLc °y)ÖЂƒ}+šƒ-swp¶û°ÁUâµt0t:·ˆ`d{\¦Rè&tî+•þù�fŽbΑc`%-YÙÇ3˜«,lkÛì¾°Öä«F£6¾kqÞá”t‘áçj)›“T©,˜Ž/âÀ'SYm²¡jÀwë-ð_�º)`#ék•€@'8ò›W—¿¾ú¡B˜¶�‹$ �Z[<±Âþà!ø¬J‚~-&b½Œ>‚€€+]Ÿ8õU$ÒUI�Žð“4ö3Ï8gWÀ)Š ¾o,R»©ã(X�¡´Ü´`=GÅ3·XNøsþÉO^ x*% ®Ë3~¿åßó—+žõltzzú¸îgŽ!È*shÝa5Ï@â§pÈÑ€ gé@CüÍ„ péyÏDTeëƒUj¸Ý}^WYó8²‘=‡ÍÕ ´v%ÛÀVDLfdEImo®n¤Ú¤q4Ÿ¾“’ C^5Λìó༩œUóÿËr„ì×µ£j0m4a69äû ÓÔ¤‘©:ŠÊº(ô,ÌqÉcÈÜ6¥ÃvNí ±�ÜRb+à Iñ=¦Å¨¡ÇĘÄí”;ÅErÌ\vü�ä˜m¡ÇÍÉ1âYO�¯¥Ll2¹) .6¯ûóà2 æïm¤çi�Ò”Ù5;³!·CN©gð„¯•+†vG±dƒ#“Ñd@ înˆn]O…d.•4eØ 2gJ_Íf�× ‰¹pC� ¹ðeê–¡q=0‡5¬;áî]FÛy7a°Ôdц° ƒˆVþ"4ÝÙ‹†’»ÚôÇ\x°é�·PØ.¬kÈ{Ry“!MÙÎ)ŒÕøU #º áì)a0¤Ú·0ÛõÜßÊl.„ ìyÛ±•àöç7¶™àø>Çvc¸½ Ž=2ÜCS¾­ÔDù]3Q¢×™˜hôy©]~o´G�È©Ï}¹T�ª7’ä»pT;‚û)*ŠS (¥!‚sâD†&aƒéÛã“ÉX†Ó�¤Åö*V6IK\CX‰+ä’Öµp¹¹nÉö,\¶Ô-¿Xžƒ×dÑw† qm A:3�> C´Ëï�! 'P¬×_ò»ðBUïýg®‡²�#öÈŸ!l¨ëlϰˆÓš/`5§{¾€£™/´Êï/_ *hO ÈÚ%A°*?ÕKÉ\}ù¾Bú•ÃC£¶±W\ïÖìs�ëíuµ©,ªTU.ˆ¶ÊﯦP¼–詬 vª…Å�ZA¡§š¨‹@æÞ¿*|ÿg_úùÊl¿ sK9€u)xîT`_DÁSÜÕãßAv-tæÑÊ%ˆ¦;—àèCrI«üþ¸DÜõË%ân. D:¤’G*Ù?þÕZø«Ô»R ½&i£z_Ò™Jhô©¤]~oTB—|n2‘¼’b¸õF+}ZÙaû‹7Fó$ž­(`»óäšã ާã¨83¤Yp€óQfxíÒ�<ìeA{ËÑ¡`Òzmµ>žG>ñ’îL2—&¿léKíµ“J½O+¿lå;²¤‚u™ÛX§è8$UIµf_»2ºs|×PÕ!m‹vÁ=`$ð§T¬ê¥ýŠ|7z”_wuBpO„h’w|lÒuÅÊäXy·©UÜ/Ñ,ÎEÒ­±’\Š×¥°ò¶˜½¨‘ÄZ9 R<â»ÝBß�qq'�îÇVr ¬7æ†bù ]Œ(—Iüû_uIuQ[]7È)ÀÁ3 ÑþQ¦‘/FR@_lH4@X4›¡1ÎV0¶¿Xø?õr¬ v¤vŒ+g˜Z�xœíYKoÛ8¾ëWpU`€cc[X´®€l6‡,ÚnÑä²§€–Æ[™TEÚ®ûëw†‰²å$N›l’HÉo†3ß�Êô§¿þ9»ú÷Ý9+Ͳʒix/ð±ÃY^òFƒy•®Ìüä�t‚r#L™Ñ¹ª*È�Pr:q²d:ñcgªØÒL¿g7ÜlkÐ/Xö%SÎ$_«4îJ³é„Óà§;‹  ™Ö,¯¸Ö¯Òò9›kü9Á¦Á1¹* ^m:Û>Ãy­ÒtRÓD;k{™š3#–À444#‰'Z£ŠjƉVˆu@°Ô'ÏS7Ó)së÷W´"¦fPÀJUç_Cë Žó»ûÊz”0a4[Âr�±M)ò’iÜ Øö‚q¦ù²®ÀQë 0¡+êhý­Ÿ˜qÓÎ6fìœã¾GhÖ¶¡`³-aIìN�×ûtÍ›ÌäÓ =ǶE®íÍAuY°�µ¶ö¨�d´E#öiÅ+a¶ #=4aIT uŠ;Rz‰Øª­…™kU,ã ».ä\’‡úNø¹ÑŸVê¥;ˆÛux³ÐnÎÖ¼Z�Ûå­ÃL© ô‰Y5’t–ªsYPo-ª øœ¡BÖ+ÃVÓG. ˆ–¾À^-¾­D8åÊúwÆí‘&÷;Ú(ÀE’!ïÿj[Â5F̶>¸ÖoÁ0 d©nW‰‡D‹$½Ñ#‹Äw±…ÀM"xµÒÂeMCMÚöÈÂ7.‚Hªív¬¤ø„þ ©!,ç\d`ˆl#Li×�Ï󄯽L؛ӫקZ�áˆ; –Í)(BÜ:Mîû0¨OCOÎ¥TÓ�tB ‘å”>sæjÕ°ºQ54“2Žm%Ù%ã•VÈ//JC+˜#�µ =&2}ÒµÓ^²stíð¸kÏn¼Ð}fì·ï>O'J¯†Ï*ˆuÒ�95eã˜4BjúI÷;pÎ}¡£sëÉ Ò§£ö3% ¥¼§õòY-Ø™xí-šfïZ͋ßQM"èôDê1…/wšðIÜ9~‹1‘fo-�ñ®^Дø{EMŸcÆNg‚ë=¢´ÔÛ!1öÝVýÍÑÁ¦Õ£ ûÜ¡ÄtoIw >âÚ‡¥ÏU»!Ã0ŸµF"!fDäÓ—X·³'áV‘è˜Õ› g!vx^ƒ\˜²­WNxixc‚,Pî¹,Ú"ƒÜå„2oÐi:6ª*µ‰ŠS<Æq›A€‡¥á[î•3&m�:»%'œ/|Iªìè�]'ì=òñ ÕåîIÈ¿ƒi|†Dnà�p�õ>oÄ ®¯{>½�¢õVPd¿ÄŠ¿Ü/íï ïÏ>ùîRï®oô[Ï‚Çîù½°/OÅøgé{{Œ ù²užpú²ÅþQ j‚¨‡!^‚ùßñáù®*ì9&ͺ÷Ž:$O—­�¾[:Íê�šù݃§2†þ½ ðÞñ™ žŸxå¸:ÈÀ7�ˆBò6[:LÚ—Á2ѬÀ¬£Û¤=©KõØhé®hÑÒËA´›/'Ý@wõ°CŽ,Uo|¸¸zä[÷+:ñT}³xQLLû2é´hŽŽ/ü+Κ‡AàÉE­�p„·á€¥¾ͼQ˃ժ»’xÞÝ—Ca÷LýÃõÄ}Ù0D{u7Õo éª]Þ]`öDǺ=K.Øl¶ß�\º\Æ,ÜÞ`ŽÍ›ÁÉ÷ÈÑ�QñmP|(ÁBÉØ½¹5w@[@å£ì‰ñè¸!h›‡ “Ï-·Òxã2ö^º/»!!{åÃçd[¨ vÛÛ}F³$p³QVvtÊÞÁš’ëbUW"Ç*d×H³=ÑMå¥�¾= ÔXд·âaÜÏgºu»¿…ÿðFÝñ_ëk rø|ôÅãu„Q߈‘Nô»sfÏ¥6¨­µç*wîmæ9]6è·û¥\Âf 4ogÊ YÀgL -W’bMr‘–3ƒ‚ûD+Ü�=oY°ï4Ò Bšù—!—ü­DÿhÄîÊ>°ç>ÑÆR5_,FÿrcüµC+‚üãÊýâw ±å�‰ÿŸéÄýö?¦m’�’Yf¥6£‹xœí™ko¹†¿Ï¯`]`³ (V�b�"–¤Û °Àv[Ôî‡bÔ G"2C*CŽeí¯ï9¼Ì]¶,¹A[ ˆ¬^É÷}H-~÷—¿ýp÷¯¿ÿÈ6¶,–É"~žÁG),gé†WFØ›‹Úæoÿt1‡çVÚB,­,…•†}”……¿Ôšq•±LØJ¨ ¾.æ¾h²˜‡&W:ÛcWËŒ[n÷[aÞ³£Z‚*É‚3ÅKqsÑÖ¸ e.–‹9Ç–ß-Ç/¡ö;x·eiÁ�¹¹Ø|Ïr5R�uÇñž-xµ´f1ÇOvëeÓÄjÉ¾í½Ÿ…â0ÿà»Åܵ¸˜oG½Á¿·ðÕ¾~¯á�ÄÒQ,Ù?D©ãl«‹½Ò¥äs­³¼Ò%<ÉJ|�L>ÄÈKóöû ߈�zióØÄ0S¯lÅSk|ׂ§›„¥º¨KÅtÎìF0\î�N´¹´VdÌj&-[íY! “Ì|©yk†¯¹TÆ&!Æ;Èlèï=”7¬\±\Wáý›TCi®ì›¼úà ó±ÉõƲB*�5b“o𯺮fn…ö’êFÀ-de] ×”�hyµЖTVÀ—,´ú ÿ%6øÀ‹GR [n™®mÌŒÝõ!ïG/öヨöLCÑŠm+½•Ý3iØg±µqJÏ�¿CK,Ö7 ӪػxýlšA�îìÎ081•Þ¶ƒÀELôOgŒ�]ÀPü»œF4ãnv×LYÚX‰P:Óõª� ó� hIóxæ¢Úi–A§ÊHX�п~¸ûùß1ïÍÒ ëÑH‹eâX /·…03Ø-ÛeõÖ=_ó-$ÄÍãjQ:ONrñ±‰']üôêч!ƒ€»H–ø�f ŠýT,à« t¼¢Ä3ôÐÌ¢É9²Á6<©8Ú±á|ű‹ÝF¦h)~µ€�äèóÙ62Ûø,}.|$á0÷_w’އÓTãì?Ž×jмôÀ;“Û<#dÐoüʨ[!‹ÂwÔ9ØCK%xÖIº_Gø×O3n¯OÌïÿÅy÷\öX ;Íðâe¬qÛµvW{déO±ÃÁv�>ßvØ­8ÀW/Á/êuzŒx…‹[¨=ù¦�&Ž6­Á�|vï�lU§ðE8†pzÙ*eÏ#úD6­žýÜ�8˜ô&"wûÖ0(�·øOQ“DŠçkw]ÃÀ[9“r´¡’€q€w¡¡¤Üˆ'â5Ç|8‡ È,E‘€W|Ó†�©Ãú.§õP4”6Þ‰ò£»Qn[ŒFí£w«ËÇmy˜¬N€ÝàÜ„ÊßDŒÒ=�ÊíÆ†w�ƒðM…:8|¥°LüUy ÑmÇy½»JJ'.'ý_Øžêþ’=K¢{°"(Pª\‰ûûÎû‘–ÔÂþ÷§DzX¿[÷(í5‡´×¼D{WKsœ ÎFzúb=>¢¯«qgÍ£o6°8äöú$°æ]W�Oôqa<ë·Ï3²‚[У|À<¯»Ç 6 5áw»&ŠQ;Z;‹X>îÃK þÆ‘ÁN÷IA¡„䀟ƒ£Ð)PöÂÿ Ö{‹üêîa³ip±•p?žø–fþ©˜x܉Y•™Îܸ½ÊÄ#„. �÷OI ¼¾�4ˆòmآᨇé…c‹^°2ìÚ^+lì*wÒJw�ªÐ:4tèÒ.ÝWšÌ¤ç°¥Îd.aè�-º¼»¿ƒ£+÷¼ƒÜ{;Q.é0÷m–§‡uûÄ F¶ºÃCAcvJ'îþÛH��si=ƒUQ‘]Ï'nEÕpnÅi÷¥¿Ôx†[ñh¿®“¬N²þ; QÅ0—P!èn£$œVæh87ÃÂY9ËTHuCØ–‰áðF8Ñy)YÓ-3ú¾‘ GN+¾#eqé€ûwõ �ïê @<¹{Ó÷„^‹§�%«à™JˆÕ²”,�³F=¤Ñ½Þ›.ÆE™ƒ•± sC&äÖ:�×&¸ ?„ÏT¨Ð*¹Pèx YÌ‹$̵·'jmè1JB;ëZNUF㌫qNÿ`?Ñ,MÞkÒ±b§Áÿ#�2& Î¨Táp¦C)õñ6š,µå;‚åöÒ£t†*´pk�µhkN= ¢�¼`uiNL0ÛåìŸZ¬ÝÒÇ P÷¡d$]\W�˜J6²4:r@Ó‚.±ƒ;Á7L¨ÝµÕDg`¶ãUÀ�YÎÂw¬X©ue¦�…""Ûu�th¶ÁÒñˆÚúÍZ¬ÓÄÆ6HÞ#q}ZyŠlÈ5¿©he”…6Ü/ 1aKZfÀ&8…o´r}„6à3^ôi± Œ’ ¨)ŽK!XÃ\`#¡G2 "Kñ{Á¿m¬5pœÍ²á2EÅIe¹X¨¤˜DUÔQ`}yê!fOkž%’Ôë%¾!@šà#ˆ¯x(\!©/V³{yžIè›ØW!Ðz®a´ž¾Ê�±‹þ¼z¾¨U@ &ÖæÎÌ¿³ÚHUš$ %µÞuHôÚæ¦í°òRYïˆëϦ?ÖNO ?ékªWLõõÒðéØú>ìËôüªù‹Ïêp·4+Ùa釚»c;M­V¥·»“Ÿ‘uÕò¤GŒh�r�>î`;Y…¬é‰oP%Ýe Ì7¼Ò´–`E±ïƒ4¬3Ø>ðGSÞ°Ním<ëKÕ¶°w�¼Éh‹�ò»«cõyØ[áv–ÆÈW®�Æc¬B2¦ ª‹4×í1]³½¿�Æ~Û¨ÈöQ6ºA¼ƒE·õ·Õ8F-M'pXÈEfW3Õs·ùžÏk#:hJ"=4ß ½“-S–%ÚÙ†¾êÃ@F“ýŒ&�c´ÅL#™Q‹£N`¹Ú^¶ÕùW®©8y²ÜÉŽ3dóÞ6§E±÷�¯gò«U¯ZT½2T=€3Cöí¦Î�†;t@B•Ì$u(òpnÛöIù­çÞª:¸•ª[U™t¸«2‡9µŠ &�³“Eû»ö§Q­ÿjEÀx,àFÐ ñh¬–¥ÊêÃÉ{'Àuµ'êêÚæAhuw„æ?Q„"ÿ#y@Šn8{�¯k ¥PUÖI7�u¥›göÈ}ÂN4ýÈz[QøvìîZ~¬õDZÂSí¯“4Ÿ’?òãÏÝ`ÿj€g�ëý�…‡yùÛ1×3¤Ôí¤HxsRÿrl¦QB‹%ˆ™ÙékÔélu~i;Ò¶’¾5¨õI8’(ƒ !0’ÒïëýDÜA›º-ƒr¡ØŠÙtíV"¿‰*ÇáWæ2B›÷¬ýà%^\ÐŒðRmJÛ›ókÏ‚9ɳ=LéêI@ßµ"ES�þNF³[’+°ªÃëÅ¡×ê -Š”ÔØ¨ÑÐ 8ÎÓ í|¥Œ@û„sÚ-׉õ(;�ã:T™Ù 6£Ç:…#|¾Y?Ü|UUØ“M±ÎŒ =5ܵ>=\±½q¸âøp‰mû r›tؕܠçù¡uÊÎŒ��LR£­&LÁ:»¬±Py§¤ñ)h›ìiÈ·j(¨Ê3yÎMÞÛÆ­~r´ß×™2úhyîÐ ®{%ð©�na¤•é°,EBïPÊäÕÌ®PùW»±41ˆð{T’s®x Œq;¬EÞë�•~4ÁFoÙažY‡&µ¸‹íî4µn$èÜV‚D=^u½Á+èFOÐ:#å@ÇÆ$&õ$™ù|(_Y­w"FpöPÿ*3ÖKRû�[€9.9¹Ùä2Š;} Aî"¼¸¬Äý#³®üÍÎiÂëª=ªÚçT¥ Ú´ó%Âò:¸ñDÍŽ²©@î£Ü³w¨çí†ÚGe†ÃYtñ,*êÎÖ±R¡,Þ[^…mô†Æ‰ªöÖ° O1•²Ö”Ë{â÷È"¾Dy(ûᱩ7ÑAN\ÞãÌ OþQÈ�6=‹Ì}D «„wQÕKgÍôzóÙþ=—…—è�l~7 < ©<’jk¥X'dxœ½Zmoã6þ®_Á¦@·(q»@�"öH»-º@w{½w(ŠƒAKcKXItE*^÷×ß ‡¢Þc%›\QÄk‰‡3Ï33ôê‹·¿ýx÷Ç?‰É³u°ª?@Æø‘ƒ‘"Jd©Á¼¹¨Ìîòû‹>7©É`mt¤² "“ªBÜÊü��²ˆ…IsX-xP°Z8a[ŸHô·ëXiNÐ×⌠¬¤(do.Úc¯dk;ܨæéÅzµ�´ÆëõùÁ(ý5Ž=ˆ(“Z¿¹H¾;�ÿ_âWƒ’"w÷x-V÷²\›hµ OñF¬¶ë ÑÛµøº3:,ðÕnìÕja¿„N¢Ý«E´ßo;OÈdþÉW dYzXþÉY-´‘µ¸‰c¡��påIä�o¡¼ xTœÞ×Îõåw<‘uëooto³ö<ÏÆDZh(�ö›’F˜ëz¸ÆG€CZìiŒhiœv‚Pe åRH'ޤI–%dV¢—žÄ¡ …{E«í ”V˜7j%\ q‡ÏØ®ÁÈÆRÔ>ÉÈXÇ …‘y®—3ܺ*@H© ÒFȽD˜þ>…daÎCÉP¦ k£E '•UdTiåè@ÐR^�‚ d”µcÕÒ¿¡þ7oí«RÿU©¥·>îû&pïD¡xw±Û+óA~ð®�«#vø7- ìq +QÄ�¼“™†Zi˜©}ÉÌÄÅFK¤ZäÒD ®‹§*‹“ˆ¤†«@܈RI» I“äU^ØMÙ-ãdEp0‡ÎˆÁ,+ò,`–âýÍݯ7? aw•&™† ZWÇÖϵ"µ/S“ ñܲ´·wÅN]ÝYš·Òx{ôÏ<ÅS,*2½¦�“‹Uþ]ÚåÉ{¬dôm’DVÒ-—'ôm­Äì3´ 2ðÉØ’NzŋٯtÒõA[±U2K ßj±Oïñ¯užŽANu±÷"÷¸‰Aeê( í»LRh‘Úh‰/1�ÄY7ÆðkYq…'¡ŠýúÀî‘xáè'R$x]‚Ø82Ù ÃmhÝ+¢¶/;´CÆ:îJ•·YdÎ(bšPt–÷>´Á“”vež¹QÚ’fi§†ÚpÓÅzì)š5ı{Š'·2Nˆ“{¡ÄÙT8)tš ß1|ê±aZÄ<ªGoŸÇÓOPîß2«¦©z@¾ÿ‚\Ý7Oë|6Oh=ÏœÁ„=»HÓFòCÅ=XìÃ`>(�ŒùÉ ŠþáSßòò¿B±7‰[d‰«@"LaôcÐfꈘM¤ÃƒÿüoO+3Ai|j*È¥þH¸Íp˰�@Â0ÑFˆç0_÷À9ÌÀ~|ºM2ѦÓÑ eéó d7/‘Ú l,0ÅÁYâh'Ã?HÁgàôÓ(kœ'�æ4(=R«7a«2ÆÛ‚£†|¨cÌ ¤u‰&Ñ¡§Î:yq~˜¨1æ”Ïà Þ;ÜàŸÎä†=_i44 š¢…áÈÇ3Âë6êŽ \ûõ<ê¿èÂn¬ÞV‡ ÈEšî³Àî™êé:w«(óz’”LU](é¤g¶¦’u}„oŽ˜©£È#X«ÙÌp'³Žq�´u‰yKã.®'*³¬£ŠìÔN4%—6ш0ÿI À•ð ¥ÞµN“LRO²É+ÒT‡lª‡ê`†cÐ4wÎeÞ[ŽIŠ”ÐÚVNT8Å[O0àÃn´—îf&�éÔ,èÅ©¡:ʪâ´7ôÂeªQ£HRŸ1V¤ÖXµ}ð4ˆ¬PÊb®¸­™¼ÇRéÚɬ¬Z}ƒw¾ˆ=”´Xh°æ± óÅ“”õ:,UªÌÔ'^¶Æõ§ŒÒéP�Ø”Žòƒ9…"FÙz4Ù{9=‰!ưýÌ1Ìâ ÌJâÚÑ­¼)Ê |r[mׯ±(€Ý†�%œmÿñ™¨õÜùˆÖh4PgBo,°^X\­ÝØáð$Î1Du•~¹dgÔ“Î%só¹Õgò7â©Þc³Ô™z‘ç=ÇÑ$ç»}Žäôw¶ëtÕit9ç6¾gR‘,Ky HêÁˈZ&\•éÐëJ¯máá€tàU�è1DZL�A›Àmo6ç·8šÁ1¿¶=BI, 0�ï×"?Û§í ƒÑ8¾Ìóüò„ÿ‰_~¹~ÿþúööU/0ˆ‰kDEÖvÈܬŽÅÜN4q$Aí ê,�æ8َ哊¨& xùÀÓ­ð˜xñ`=A»Ø ÄqçZ2*i¢Ycü´g…²>ÊúáPžÝ¯ÑÅo8úgí=rm÷dçÜo`nÁøÊ¼ÕYéДÍˬܧ´ŠôÈLËåZ-  Î’R‘C3¸; ',D´øÎ%äëL×Ç0G0Çë•Sf* ‡?-í|z§ŽEÝÝhçaÍÝKN¥Æ0<-7ƒ{æš“Z ìÔjl;'½˜ PͶEû©Ò@|=žz:ãºê’–uG=à–zhsRâGå ÒÝnídhë[è÷ø”{óÔ`QtUå5 …QÚ;ÃmÎÁ[»Þr!ÅFñN°KKmBz uÕЀ!_ÙZS×§ÖEO¯Q$î©ÏfIµ„LÚ΢Ý'ÒþÏZy‰‡?rK3â4ÔÔJԑɰ>F{_÷ÿÈ�Æ!|ÿ|®Ç!\?Âq ×RðúýçÁw#e²ƒ1ÕIù݃L¢^â®á3uq�h­ 6ù‰"ˆâ¶u˜@p|tÛÆk^«=÷¼:Í…É|“ᬣ(8õcR¹щPD�jÄóÀÞ7”•Ùˆl£p”C“³jõÚ³ÇuÐîtUF§1ô©Î¡Žlº9í mÂ8é¯ýÏÞ‹¶úJ­KÑú6”Õ½ ü}²Luýw}=yKwã=«Ñõ0áoQ·×-ïZ•™Uæ™�!<5Ë Ï�ËFPؾSЮwCqö2f®{=�‚T F#† �,%ûúÒ¢©@þVIÓÃ%ÕÛ¹ —RôKw”=2GÇ|É­Ö×NÃèoekmoèo~øÓöÁN;5N9o©ôB UÊÖÚûq»%:F òJÛŒ'KµáÛœæ·(¿óp[Sýh5rç°kß�Ù¼†/u8Œ0{‘ĽE6bìÇtò°Ç·ÕÞµÑd³qòÑ£7}*¢ç¦ÔÚPoC™£¡Ó§Ý…û¥Û‚L÷?„åš´ŠTj•,\xœÍXm‹ÛFþ®_±u¡´ ;‘ƒ@¹ÓÜ$ M[é§c%�¬å$­»»öÕ„ü÷Ìì‹dɲãs/%p$íÌ<óúÌ^úÝÛ?Þ,ÿþó«LSÏ£4ü/ð§ÃY^q¥ÁÜÏ6¦¼úy–à{#L s£sY×�![öš ”N÷-J¯#“ÅŽ4¾šÜp³[ƒ¾eÓ¢x&J9ky÷³ý#×¼(ŒžÍÓ„“ª›ùáG”¾Áok–×\ëûYõš•$‘Ëbˆô–¥[®æ&Oúe÷,Íæ^E6g?¾Æþ°v�?¥‰Õ—&ë¯aë£7öjdü&<ÿPA]‹õݧS@ðß>š—ržÒ6xAù™ŒÇœ-Š‚5.�ÌH†ÅÓ§)r§ ± p}õzæÔ18‹ì¼”ÐY´Y�WeD”íDC9ÆÝiŠ”‡ÌdÉF¦x[DÏÂöüF9ð¼b0­…–ãIÆ•â;ûf£ÚkÆÞѹfƒ +¾{κŠà#'ÕE<Š4&ŒmZaô¦Äy¦bèÉ®£ÅÅ cVO@]uœÙtC8‡XŽàÀú"ûT¬J>ÙŠú:†jbÄÕEª° ±C®�Ÿº‹·%z©8•wûõ(âÂQ~¢žß kxÀ¦}(@çJdðð081£PSæûg–ö”CÓ¥=åÙK°Ê”Þcü‚KÙpÂÃÓѺ·3¯³�Ë!° AEØN¾¯ñQÜß÷("¦�¨�hËk�7"|<«ÁÍ$ÎþI6òT¤q/å ov›µþD{{;šð !e3ð83Èe€Nyõ‰°Â! aºe³•vá nß!Çx½q·…Fv �QÄÞiiW\ÙZI\ž ]"ú¦ëcžÃÚèf¬7ªÐŽþ‚q{± â®1X!Êl¼Ünâɶ¶@‡’—b·—’Æ(Êyë�VDùPܹ¼pÿÏöz!ý]Dµ'Éû¼©³ì ÐåÄÐ|±™3Vû­Œœ!® 1áf}éV³G¬~l2¬]Ù¬¹™¨…ÙÝ…îj¸z¤/²™–5˜oŽV‡•–ø?u&î�¨ŸZ< ÿ Ûvÿˆtn‘lHxœµWßoÛ6~×_qóK6À?°†Ä1à­ : i‡%}(†=PÒÉ"J‘IÙÕþúÝQ’¥ÚJí ^À6u¼;~÷}wÔò‡7{üüÇ[È}¡VѲû@‘ÒG�^@’ ëÐßN*ŸÍ~™,hÝK¯på]*¼˜{Y ò(­)Ñz‰n¹h¬¢å¢õ›´fß?¯ØÔ×%ºk8儬£¥- ¼�ŒÏ?iéÝdµ\öýj,«Æ†|½"“%œ»�ä¯!sô?£Ÿž$&=Óu—O ËxÕºŠ)`ذ\”ìt�9BÅÁdàéû€-&ÞØyÔ˜¥rÛE/Üìõ¤ÙùQ#ïi¸Ú!~qW­÷i·šŠúx17•íW£n¹�ºòxlî01:=^/¤RòðáÀ[bÍÁS0¶{¬…>|:©Aèáp ŽÀ”Ö"Pf‡6,ß<‘Ä5¤˜‰Jù9ÀÚ9¹ÑRo@z°¨DŒÊ1¸Ñ]Š•BjÐ�6læ¡�v�ƒûõãûõ¯ ’Kª 'µªBÈÑbW••åÉÚ7ôåÏZ³ Qmïí˜mk:IÐ 3­"CÞ‹$Gוõ¯¿G }š‡g€pgl!ü £ËÐù[*]©D Y°b‰ØEÔäpßݺ…‰¢¢ó¶Ãªu&ˆ+¡ñ‰Ä¶ Ïö‚!+ée—Ë$oM6tžØq�éÎJ ÊB’ˆŸ�øÕCnTÊ×&<‚L¢J#à4”"ýTä–jƒÇfS ¥æˆñ1 �RÄêÎG éoö�”3@Â"]:À-Ú=ˆqåCŠî€[Yj’ª@MÝZÖ¯tÞq© E}HJ�s{†Ô¼°žh�'ˆ¶·» ×îÆé¶'W¨ÑAƒ‡ÄTšðʬ)Nñ.ì‚`}_½I¦Üü¼Ü~”8÷M·d2MÉ–ªÀtâ6(,„íí÷¾RCjJõM M#.^?zÒYQ³šþàÝ»ëûû뇇ž;ésN�hÀÕùÝÝÝþ ÄNpk A¸cïH<' %À œ$£œ6eà’�¹Î=_3waS¡#нlD/+Ÿ7ü£”S¥ñÿ£€…e¬œÔ:h¾þtRKÑ`€pü/ˆe#iÏéÜ*‘~Ùß£ÞøüÓ£Ëмó5Îq]1!Êî L‡Ã› ÀŸTš™Ñª~Ù4y«Óç&‹Ë:8?1û çh¯ŸœA{°(ÉÑÿ‚Âï:±Èö{»Ë 2p7Ž‹óX†‘ÊwZnç_g÷Ûƒn›ñÂM…Ô…_©Ý©ðŸJ¨F¤ÿÑš®Û|ÚtHýÄ=»£¾,›>訳)l+vv¹¢¾\7<�Ú�ôVÓžnؽ 2æ`hé䃨ç@ÛhÎ+5˜·m—bxÃ�l›$µ aÃe–;SÄmÇZVùŠmù¬ãJªÔ�ž/|³}æ8=g”^pŒ>EªLÚ—«íE¨w Gc¨w7"yÆ=yѾÌ.šæÿÔ„*T†bm�H ÚxœÅVmkÛ0þî_qó¾&1[)ŒÎ5dm…–•¶û0Ʋ}Ž5lÉHç”üû�d+ë‹»²vÙ �mét÷èôÜsJ_:ºþrq5µM¥á�¢äG‹$ ¨…±H‡qOÕü]œð8Ij0#[  ’-:C÷‘&ÃT”&£‹\—çðMææiÓ¡=€É•l¥”hñ0ž°ˆ³4ÎÕÛéÈ<¥�°ö0®÷¡²ü7çO⥅.ñ6„4Ÿö’so›&�ó—Áu�àl �®@ÀàÑMX4í¸l K¹PZ;ß�_˰l*´Ÿ�ÿÀ‚ D[™£ ñ×<®��=þ$YèÿÎ@(ÐI­D"·ºé Á’0ì^:ƒÒ¹� Aµ¢z•4–ü°K›ký'{¬´iy5vŒÍÈ5–PÝr¼À)�´pS ÈÒµ0¼Í4qÏÅ5;:U•›4H½Q6$kÌrðÛÝ¡!ÞÕèìÌÃÏÆÁ+·‘{c'ª !uÀrª ÃyVÁ*Ý4úæAfÝF ¡”&ÈøìäJaùÞU€Zq–&N RqrD9žé-ޱ¹rÆÜ¹•÷H’0KFiE ÐŽ4®÷2.…Ú`5Y ßehá*6Î.¶®HØËž+S‘ûZ$“¥TŽ5ð'~_OØ,>;rÅ™ ÁÆzq!øß�¤'`ÈÏ­”-«„ýuX| Ç^&ÂÛ¸¥Ú€¯aŒO‰�H5Ú@¡¯ßKr6ÅJô íöGf†`�ž�å³”¶kÄ*oå% Ô³¯ÑôeÆéÄ8Û¾>s ÊÉ˃r)tÏ ÷ª±C¼ƒVÄ[͘Fªú6Ç¡‚½ÆIõ€ª—\Îs­šÍá²bŃlMÝê°×•û­¼j¶mþø­°Æ·5vz#¾søÎzÉ¿ÑOÚ8t‹ilCÛ{Y–#8_^Ÿ-?ðFeÃý¶(°#×<ÇFâr5q#©æ<Ü£¤Z1ñ¸¤%ñpÓDຳ±ê¹CŽ-šWkãHzWˆ’ ïOu ~©uZÅùðõ¢>qÛãdÖï^ÜîÞsžÁ‘A'âþ=Ë_…žÉ�'!®�iÁ?S€.ýý$\D6°Mÿl•}‰uHì4’+¤ßÂØ’ oÚÉp…ÿ è{&ÚŠ"l”Hxœí™ßoÛ6ÇßõWÜ< ÝÇFÅ@Öö¡Ãš K^†a0héq•D•¤ì¹ýîHJ‘»q×b؃€ŽEòî{¿>b�ø›×¿¼ºûý×7�Û²XEqû�"¥�­€$Ú ½š56;ûa¶¤çVÚWÖ$ª(0±RUð¶JñoY݃¨RHT•‹•à¥xé·Gñ2˜Ý¨tÏN^¬Ra…Ý×h.àdkt,ŠT¢Ä«YÿÔb‹ÚÒÎÙ*^ ¶¾:´LÎiµ†¤Æ\Íò—�ú9£¯|6Qé0¶ ˆ·B¯l/ù® Þ¬:c›|Ö_ø óvÿyûýYŽE!ëËï㥳/kV°‚Ÿ”¬àÁ“¤x­r´be‰‹ÈïNå¶U\š³—3oÀ«ëûc(èr$è²ôgm£É¿ª°§'‚\)×ÁæF”u�´'¤ð÷ ô}Sbe�T:¾¸ËqOÙKVÄ[%–Ôf›½«â¬x�•³B{”NQ³þ’ImìvÒæ "Ø ]±œ]NûE¥h‹ö o/!•Y†š4û9Ã6ß]ßý|ý#i{#’<2Vh ÂÒY™%#쌳ür6˜)Mê ÕãC# Wþ5E³SE‚î%'(ƒ5R—¦¾h­7*HQ¥iQ˜(ÔÌC-Ñ,D]“ë¶¼Ó€_º>CjÅ£h,4•´D©ªûÈ>%Ú˜¨¦b™VåCC¼¼eîFªWJŽÔÈ�¹Wî²Qö¢®e¯CPÏ´ùШKœi÷ÀêIå&ÄêŠ$-U?‡Ì¹Æ‰z-âõhÌÓQU±§¤S§¤ê2`•K5¹ 8r*a"*jªYéœKšjU{Ÿ¡%5š¦°ðÑ?w}j…õ5ù×¼×p ¾pÏ›ŠŸ¤ÏCÁz¹‰�ÕT’Õ-RGF9–áQ@ÐÊ)Ð!´Ö-êÖ”‡õu †ñ·„åJt„;ô”©7‡�£®ÇÖ¯‰´Î…/ýZÕ¨�ÜŒÛqv¨C«.Ø%áè†;‘‡1Ü©ýîŒ}9†¹-š¸3zû >.æ³ÜÓ7D0SJóàwLí�ïœ&NÔÒROªA¡v¦Dú-СwïÈÓ›»¶´CéyË%·à˜äǹ}Ö²îŽÎ¼­2µ¸eQ+b;;ÐñÄ�dM“Þ(%ˤ ¯\F$�Ó¿&hDЃ¢Ÿ控tK²ÅMnéEàU܈›à6j�®h¬ Ù�Bîµ²“;ú$ª¬Éƒc‡Ô Rz‰‚„Q7ŽþÍÃ…rüâ÷:'—u»X�at*‹¢ÿŒºëÖá[Ö#õ½óe;`U¤©5#›îÙ‰ô¹Ç£ä¡¥ƒÔùmnû°qç{ ñk#šœjy+ŠŸ¶ÀÁ�>íî|:·æ³üšƒŽWð›V7fàM»I Sy28ÒLjŽä·¤ ëŒnúò05ª¡ë@P"ýÅÆ`6zyd´@t¼ä«ÔHðPݰT�­Q'™µtyR:j™7:Grn‰„¯¤²îŠÊ3ZL" ¡¡6ÉÑý‰ *¾Ni>!Ýœ¦ÝÍ·V;HèuEÌÓZìÛìöòÝ+ºÊ´»²Ò1*zäé aõJó…Û¡Šnž‰–\¯;àÅàXŸ‚Õ¿r”I,RìÈßÃÂD´‰hÑ&¢MD›ˆ6m"ÚD´‰hÑ&¢MD›ˆö_mþ!»ôÿýýrrMï  z ˆ r�šxœµ—_‹ã6Àßý)¦)t[ðÆtËA¹óŽÛîáh¹\J)A¶&±Š,�’�~úŽü/vâ\²Û;XÖHÍŒf4?MÒož~{÷éÏß�ÒW:‹ÒîƒBò§B/ (…#ô�³­_Ýÿ€·ÀÑUkÉؗV#/ïy ÃÊ Î!:á;n½7+Û¹õ MÉ;kÖÙ9«š,ß�¤�Šøæ–W|s{O硸¾=Žg£Süh_“^5ÌB«abe–MÍ65Q;œðÕºROkôWʈ%&«çzÕ,†ES« E2ºÒ'óL;¡·xÝHÜH‡ \·z;jëô,ó4i?ƒ�uáÂÆÙ :€FõÍ€8š=¸á#€/ì¶à‚]îТ£ižíçúXGüÜZ Ât¬{û¾Ä£µáš§G|¡adzøODÁi¦P!´pP _”(AqM›‚0�f*?ìltsfØg÷P Ö œu\žsõÖE<ˆê…�è©Rá¯á¦Y�kôœœÖj‡!9AOt´ÈFêßötÆÖVã ™:n÷´[ðç9§Ãö›YÎêmeêù./`ÛyA,«æý¡j ˜¹°0�Á‘Ëæ]²ÿK‰T8•ãr9X?åk´ñøyD¤«D¤1Ïè6:Ågp‹á»’S¬6oþ/¥zâê+ûs;I×½:Cæ‚Ùó^Ò9²^r^®>.LE}f“”âž#õ¹ –b�pÏâ$hà�é&ù€N‰\ãI;ÚB‰™¨ÑÅÜܨ¢„jK¾f×’eÈç¡í0‹>t�#ŸIB~àÕKýwR°YSEZßöQÏž‘ç6ŸùCÑdõ‰Õ….j^7žœÜ;)t7ÑT [Ýã ýB©ŠF/Oe¥Z)ÎÉŒE`ï8´"t•°|«¦ŸÚs÷Ξº})|»ùòÃq`�w÷+sx}‰Ãë/Ía:á0Mq8i!&ÍOÐÿûî…~qŒfxœ­VQoÓ0~ϯ8ú²—vCHhd• 1 bÝB<8ñ¥18vd_[òï9ÇIX·ŒiRÕ4Îçï>ßÝwiöìòóÛõ·/ï ¢Z/“l¸ �|©‘•péb¶¥rñj–ò:)Ò¸$/‰Óð€á �³ :Rè³4¢’,íÙr+ÛÀý| Ô6èÏá £“L€á§³ ðé�QägË,�ûlJUÄ0×C(´ðþbV½„ÒógÁ·Ä…•“g:ô´�åËž*ç€Ý†,méÖÂ6<[ñMG”ÄçR톰µ_¼œÅ-«.·¢ t°Ã‚¬›C”qrÒÓCÞ‚ÄRl5�\(–ì�ð7�ƒ±T)³I˜‹_�Øe<�À:(¬Ùñüߎ¢ŠÑƒ¾”Éó•a™�Õ‚”5Gò}€}š¼ß¡œÎ¿º &ª¬ÊmSŸÒzÓü÷SwÍcŒ;±ìÆNݰ“r¥µóØ*Æ–%ûé˜/µÝ¨Bhðü%ÆÁX íqz6šÁ6av!ØF·ƒ „”^Ô�·oÔ ûÀÙ}˜Ùáà,2tø` ·=ûho’c<[šÕÅaZYÏ™—Ám V[³éŒò #ž�Š'÷TQ TVË8–™„ïY Û3x‹0äŠ�ÆÍוB`£Ë$ ¯<¡!ÝòÙ?Í�IJ� Åñwë5ÄŠö¢åÊ[­í>¼&ø(£,îiáû#_ù5/¿WÎÓ8þÊ'…D_8•£¿ãõ´Õ§ñïĘRßZ„dp‰L¢xœµUKo›@¾ó+¦ôjƒÚ(Ra¤(î!‡ªQœª¨ªÂVÀ¢ÝÁ–ÿ}gxDv»nÔ‡%`açõíÌ7CòfýùæáËÝG¨¨©Ó ™T/ ’‚¼RÖ!­ÂžÊå‡0æ}ÒTcJ®P¤"yˆ¢¬I<Š‚$ž\d¦Ø‹Ãw©Èiß¡»¯%«‰‚–·V¡G#L“X‰«÷þȼ$äµrnV—P:¾–üIlš›!$™ßKÆQÝ$îÄ_ ‚È`VS‚‚Ñ#éZ�n2‹‚ѰÐÛJã–—áèëz6ó…$`²ï˜èr«3t@ß©¦«ù]"§B_‰ª¶Ð·šÜ*³mÐp«ê^�!í[‘7‹T[ð½�)6=�"Ry….¸%±ßUŠæèÉVY>FË­Ùð¶-Í|‹ÔÛÖÍɈ9cånLKØŠÝP®ê"å’WKoÉ¿uÖth‰ñGÂÌ0½{Ù2°— ¡£ÊΑM*¦Zÿ‰ß·�èQÒ¦Ã2›x!!ø^ ©–ªÐD’hÇ åã¹åÌÙÎÔŠ´iÃôèó>}¨$®LqFˆ�L›õЪó›ØõI¦-&r=}�‰”±*–ª¯éì™eÎntû\ã½Ù ôkw±žÓì“ù޶!c¹½Jc!7MÇiÏt­i¿€�¦ ZX–ÜÞG‡‰gþ¾Ö cýæVø4~ýSzô¦æxÏ«_�c‘Ç ÇŸæå0Òþ²€¯B|FžïüðºæL$؃ @f™�8AâüH6H¿…ñB‚xúcÆã¯ø?ª›‰o’ ¼xœµ˜ÿkë6À÷_qËàu7Þ Fëº÷¼¾�¥ûaŒûkØR¦S²¿~'KNÇnÚ®‰¤ÓÝéÎ÷Ñ9é7ïy÷ðǯ °U9‰Òæ EÎ_ZY! ¡½­íâò§QÂóVÚ'–raÅØÊ � À=ÚBç”&^$J“ j®ó�SüãÄÉÙÝ éÕÀ¢Q*@‰ oG=’GƒÑ$M„ÓÕçÖÑ€5_±ä ²RÝŽŠ·° ÞŸé¼÷L×�n„™X™&în!�÷É¥É|ß}Ÿ&µ¢4Y�áÏ%í« ²zéŽí ¼3(,‚'ûëùߘÙqäår¹i¼¬èòíÈoõv�éKØþÈ`Ю�"*˜€…6n„ÕÊî"ïņ絹~WÒR³ßO]fZåt¦ã0ÿQ›JØFV¨< S+Œ}Ïíèé*øŒji‹Fè‡Øéh«hĽTÞ²&?©ÌðÙ•=6öç_aÜDõy!H$ähä)ê;AÜçû³\_]qV µ®ØR’wbØ2+ Z“…9ÂBrÖ°Ö¬´ºÌ‘õ ’j9ŽàþîáóÝϰtn»Ô˜Ö‹°òE|iæ 4¸AåX+É?ØÃŽ;7^RZ�T{OWœM ûBq±<ñ¡�N3s7$Y]1ÄÊ ôVݰ~6Â…˜cZ•;°š�‘\*‡F—@Üòº¥vÚÉ{<~`CŸÔBïÓÄ»Û΢|>¼So ý³Ö7ΑöcEÖhµœL‘SP’æRJÂTÀÄ, .˜˜û³�Ô¿=ŒGGçvÔ¬U' ‚3Ä]¢=Z–x!_§mâÕjZ„ëåÛ3 lD¹ÆóFb/í‚pÞêÓ‰^[§g™§ªÿVcVF¯ÐØxÕOÆù!ÐìÁ@„(çâ2ëŒèj–«Ó0È÷¦]·÷�¡mé8â§Öâh(Ö{û®öÖ:Â\òus"2‹¦)#G€È9Í8ÊD) ðµ‘\¼’ Ví „mø÷å‡��žœöÃè-dX– Œu\Zžs�ÕEå<ðõÝí*å~ô·f½×X霼Ák­':ØtÔ"ùo8�ÒµÕØÅ„\æD‡=aËVÚÂ;Ï9å†[Ûmf9]®+UÏ7x…N bVù/`©"ä03°àèC?èf¾ß™±ÿ³)3r޳Yk½KB´ãñóˆHg‰H/"â|BO£S|·Þœb¹ºù¼´òª�«¯ìÏÓI:=ïÕ 2§Ìžð’N‘õ’órõqa¢ÈŠè˜õRŠû WÔ§6XŠAqms�Xó@5“|@#żÄλD€3±DÓîéj&q-i†üܽ.4 ˜E÷M�ÊgÊa¾î¯\C›5UrmC“äÚ>öŒ›4‘».-z´C×oœÜ‹\ìöïC]õá }¥TEG7O¥s¹�®í;€±DáØ{Zá:FØ ~ªú¯ÚS÷N®ºm!lØ<|1GØÃ³û•9¼âðòµ9LS‡“ðBâÿ¤øÞͧ: Õ µ î“Õ…;wŠzxœ­UÁnÛ0 ½ë+¸\²I�(0´Ž�l]±ÛÚC.ðcÓ±Y2$%Aöõ£,ËuW¯¹ ˜&ŸÞ#•ôÕíý‡Í÷‡�P¹Ff"�?„ÿ4äò �%·ší]¹|7K8îj')s¶@‡µrdZ-ÑÕZAktKÆÕdÓ$¤‰4éᶺ8yð·™/t§–ì5œEát‘"(lh5›Ê¾¸«P’fYš Ç¿œ¤Óñ’³ZÈ%Z»šUWPZþ,ùÑ1F®‹im×‘Ö Òm6 mùØ®&MZ�›Á¦"Uª”{•wDH*êC<¾±Ë«Y¨[yÀ!G)©´¯ô€&St¼eviâ`]°¬úç×1gS7b‹�¢e1Љ!øö¦—²à#Y|]o¾¬ß_ÃsH`‚¨€†äZîÊ�6vñ$wÄ|?Tá�À1$ Ôj'ÀUT(kcW–[Ĩ`±i¹K<0úxðÙAmûVýøÙ‹�R> ôöòpè:;YPØã•.¸vmm½S~Zølìÿû ½ÖìtœË¼9Ýqͧ.}Þ¿�ÓæÑ†F‚å~ð±®b–ƒ™¸Ï–dP‚­�—‚¥5,Üën˜LNR ¨¸wuFVü½â`»¯¥[2R�³èˆ*ìkD×ô‘@߇¨È°e»æ¡<âÉŽ�©�%YF1 {÷ÜBú�ÛÆÐÇÿ²Šjz=KÐe§çéÓþÅ�¼W]q?cY+B3v¤ÿÖUŒãC,øýà˜ù¤=ϺSL³�<æƒÒê±{‡[+Ø7F<Ú'úâæ%Þ&Œ Û_Ï—FDã0úÔ© Ö{•,­ß Ìsj™(ªðÆ;U€$¾ þÁÞßQŽÈ c;!àŽšïLÅ·„Å�!òACY7éÿ‡’ðg÷Ë_û„yv‰v*xœµUÛnÓ@}÷W æ¡ %¶ ª„�c ¥ õ¨J_xBëx/Ø»Öî$!ýzf/ŽÚ`Sq“_vgΜ93³.ž\~\Þ~¾~ um™à EÍ·IÀªÆ"-Ò-­ç¯Òœ×IR‹%ÙZ�Ȥ"4½nI­Š<ì%E1*]â‹ÒYÓ¡GûÆ]Ù&)(Ñá"3IË"ìåDpÞHŠV­°v‘6°¶ü›ó+±ïJ×xŸEQMÀTÇyïK¸mkÓèôd’Z4môÎ’à_ËÝ@©³ó‹4@¾ÜF)ø-ÐÕW\‘WÄB£÷Ó±@Úp'Ú­ ¬¡BÚ#*�dÁŠ®oÑÎ@¨šAÚÚq:ë­ZùL¨µæ’2€+òX{·‚;a˜f‘»{vÉl¯ÔZgWcŒ ÒÖ¨L:(ÌÃ$âq7…lƒ†ƒ+/ÛA2'ù^G‘-ƒPmeËxjQÎZ©P˜³ÁÝŒ;wº9.?»C£çÚÔhŸýŒS“«¾!öPð£g[E²õl~§(Ûó�‰P‡£` ÃK-B',4läÎ)­,qÇ ä\ôЩK͹*'ŸïÎæ¼äo ®Ç{üKot�†¸´™›Æ´¼>.¸îg˜s7‚¢òcF¦,¨Ž½ý[ÀOÇŒ²w!·´Œ!dˆÿ'à6Gi²`“3«OíOAZºë)7'n"ibZÐË ­HìÂXŽ÷áíïjqr<Û“ƒïäxø9ÿ¥Až|nÔ‰sÊŸ$ZŸÇén�OX¾ŒQ»ñgÄ"¸ÙÿÇÅ:.vœË'¤_9vE?\yø$þ'„3‡\u�< çxœµ—_‹Û8Àßõ)æRhïÀs …£u Çîô¡ÜŸmJ9‚lOb[Êj”„ܧ¿‘­Ävâ4Ù½B‚¤ÑÌh4óÓ$ùáþ�»Ÿþü JWW©Hö?( þ©ÑIÈKi Ýl²v‹›_&1Ï;å*LÒÉéãZV^Ðà=ºÒ”Ä­ˆHâ *3ÅÎ+þ9õrn·Bz_ÕÀ¢"‘ e�³Éˆä`0I“Xzý·cn ¬ù–%W�W’h6)_ÂøsÃCÇzrSŒží $iÓG•Äþf�dcrIœ¥ðãOIÜ(Jâ•7–Â�Eé$xYåvp8±É¾`-ÔfïYM7¯'íîÖ«cÆâw° ÝÚj©ƒ Ø*WŠ ëŽ¿‚d¦î‘r«VN½×!u?Úûæxa¥ªÂÊçÂÜTÀ¯aΩyƒBÚËçÍù ”#0[ý\©xD  ,ÀèjΟ\-5HÛÒTÈË[^C¿Ô ‚£6Ó¿Úh¾Ó sðâó„œ5z™> ¾"ÃW‡)ÀÉUZ\prœ�úüÑ�'ƒƒøkTÇ|I’s‰îBN²Äh*^NÁ‡~6j|Æ ’ã(ýžh`#«5^6µÒ>—­^_a�uz’yµŸÂßM ÀÊšZ®¹VõÕ¥Öš=8á®Ì€Sk�óÁ,7hw¢3ÍSƒýSè[FüÔZ$ÎÅú`ß•ØY;Ž å¸ÌZØ0 Œþp•±ÓJ/�rYI µtyÉÕ¨¸õrIõ 0v?쬸úfØk¶�#ÓCZ+›¸ô<ç:kŠÊ{ š…žè±Ráïþº]�N�–jƒþr¼ÑÙô"õo8�6�ÕÈÇ„üÍÉ* {Â�ÎÖy¾SŽa ªßÌr¦Z׺™ßßÀ3(tZóº} –F B3g<}"ݼ}Žæìÿ¼h°Ÿá|Þ[?&a ´áøiD¤‹D¤g1Ké::E'p‹àeÉW¬Voÿ/ÕmWßÙŸëIúpÙ«d>0{žÁK:EÖsÎËÕÇ…‰2/ÅW`6J)n|QŸÚ`)‰àÚ.¸@=ô~’h•Ì*<ê픘‰Úˆ;•—P¯É5LâZ2 ùÌws{Ì¢÷ûŒÏT@¶ãÕsÍ·=ä±ÙP¥0.t=5jÇž‘ãž™ùCb´åùÀê|¿3ý¨ý�gðª�;z5Òþô›Æ.C¿ÑU‰ÁËS›B-”ïã:0V(={‡¡•¾„­ä¬jOÝ;yê¶¥taóù‡Yp`»ÜýÎ^žãðò[s˜Ž8LcŽÃß­¸ý?÷3ž³ƒƒDt‡ ¸xœ­”A�›0…ïüŠ)ç$(‘V]­Ýô¶U{HUÕÃ`OK³ö‰ßq€¦«¦E•VB2Øß<Æï�ów‡Ï�Ço_>BÍ�-’|µ 1‚ªÑâ}Úói}Ÿf2φ-42n^z´ŒÐyבgC!ÏF*ɳI­rzˆÚÛ"¢M]‘næøñåþ†1ž°œëc2ú›Uq6ùêOx:rÓjÞ8Ð�ïÕm v+ÿZÉ¢ÙþaçݺÖ9Ðú¥¾ïoÇÁ0_¹Dž7ŒGÖ¤^™E%é“Éd>Î.L$•rì+>�Îý˜…ä¢h³Ù?/môWæY2:›p8!§ÉÝfl!“Œ®»mœ9÷|ZGU~&™ÁòãÒôfû¡;¬ú{[ü¨m”h·’·Ä¿•±÷A8½ÕBûº¼+*‰ S ‘l)S‰S{“*üxœµ˜ïoã4Çßç¯x(ÒHÙ“NB[ViÜ1ˆ!^Unâ6F‰Sü8-å¯çk;iӦݺ1NÓuÍóû±ýñ“¥_|úùãã¿ü@…­Êq”vRäø¨¤”°´·£ÆÎ/¾%xn•-åØr.¬¸”+©-=H[Ô9§I�EiÒú˜ÕùÆyüvì´íf)ùšŽ›B'JiQÉÛQ_%ü?§‰p®®ÆC!¬¯ [RV æÛQñ�æ ‹¬Î÷3½¦t%ÌX¦‰û [Jg{ò4™�髯ÓÄ[¦Éò ½zW^P{*~.ðÕ¾Y°8Yu"ú˜>)¬$¡)ô5 ’\­ºÌ*¾ø0 Ê!«ƒ<ö–õTµd¤mŒæm ¿àyDƒ„ÃÞ™•†V2³µ!übk”^g¢j¶ÍæÑׂ|ÑZÙ‚tÝ ÓÊr+½$ú½ ³h*—„-¤O„·ïßwêѶG//ûdßiYŠL2)‹ ¢C-¤x‡N‰²-Ó¥%ð V­$9ERšXfµÎÙk# ‰npDVþm}«œMk2w:vצػÕ$f\—� ~¯}/B5Êy+¤>Ñd—BÛ¬\l¸kÍM]!tM¬0öwÒzîœÂcȘ±°2wÉv®ò‹ªª.6øGŸ?_?<\O&ï·ûe­TYª¶ø¨gzy¿Íbíwµ´ŠXl¬qm!¨KÙ[Õ‡»ÇŸî¾'µÐÈÄõÌÊ份!tŽÖÏö£�ìïìÏZ/ƉÃTr�ãš´�""®€/Æ,�’|é¨ûåîûh¼û= Ï»NpOr!O⢗ÂqÒ'‹·ßå t=áx%ÊF>ë<>DÁ¥�Ê/ Ë'ù«G-M½”Æn(x>›”»¾"�ã ÝÑÑa®ÉðEâÀ¸ãh6Ñ.2õÍ€^œý6bÅÖ¢»S³�u.˜£}2S%lVàdBo(ìø„³t˜*÷s�Î]ÇÃzM™,KU#|Sz‰ãô„« xŠõU�¢�÷®ä1ÕZN  ×­Œóíbâb!Vÿ´ÅáRq&±k »eeÜÚ´&ž`>y,(ZXÏþD½1ôâJûçÝ Iµ…‘¦º±ËÆöî±îÂc·Ç[àêÝe‹(±/=éªm7˜¶ì“p¥‡úÛa® Û öÎð_M}ÃaÿC¿ÜÜDTHlÕA쾋ªÎÕ\aAº`RC›px|£ÖÑbèÈeâºí�*/€o�Ó*  -†ûìd‡ÕÃ'³1Gù4 QS”0Í%gFÍätÚ“²>�{ÿû™ÌçÓÌç—1ìYÇŠÇô®ÀÆVË›×NÌW}.ÿ¿iœqULžÍfp'L€×W\< òkÊapŽ¤ÈŠè ^1@áÀ5Œ¡üÐ�_9˜GE÷õ%f¥Üi¹ ê—ùºPF´†­Ç.ŽÆ*šá@ní�[¼ö ×p£Ìi¶ÙNVýá'‰!•ÝÅàÁ™c€CK1¬yÞ)Íošaðí],¶»XÔóúÒ¿`e÷FÕ³ýnW¾Í:EÇ9Ø#)…»\öú*üÛÁZ`C#É .òu±�ÜOÎzºÛ´oÑÅ¢‹·†(@”�A4iÿ‘„?rü O¦¬+„@z‰¿xœ¥UÁŽ›0½óS.{I‚ºÒJÕ– ¥Ý½µÙJMUÕÃC±J µÚü}ÇRÚ…­´‘¢`ì™çyožMüâîáíáó‡{(ùX%A<>sy‰²�%Þ†-ëWa$󬸢„mŽŒêH34¦nȰ"G~9ˆ£&­ó“}™¸>5doa1[‚Aã‘¶á4jsïþïä=Lââu2 (ײÞ@V¡µÛ°¼�ÂÊo-¯,ÙY�ÿ]ÿíX â4™à¤²UG�CL`§Oà�K‚Ö’dƬ$»üåë�©„R�mÅ›À䪋:ÚõMèd’¹%Ê{™ZbëÖ.$ê!s<5W ÔEO³O]�7f,¤;ʸ6‡aAå k†TDÑêGKÏd|PËŒÝÚ…Œ=ÄCÄGFã,D£~–¤”9O¢œ�Üã~Œ‘C…x*_¹xC¼ßÞíÞf5lA=×H}ÅKºt.vÀ˜W¶u‹NZ�â›ÐŸ #¹úÜ�‚k•½þÂðÚõÍó¦Æïî4ÊÈG_]�wûWP”ÓÜaÕ8½ÇjžRòÜÙ%5'­¿HÑ Î¼ª˜Úºj™ÜuD�¥õ¾´½-çÄ€Bæ0ËUȪóþ[îÀ²QúÛèz5Ž=„Û�'‰o³ZwR8åp=sQ¼¦_|>î¦@_÷ÓÿßÚÑð)‰üwê7N<†"yŒH xœ­VßOÛ@ ~Ï_áeÒx)�6„4m!R¼ †{˜¦ir—d¤wåÎ)ë?;—„ Ú�JmšÜÙþl_|M_œ|:¾úzq %Ïë,JûÂB~æÄy‰ÎÅ Ïö߯‰¬sÅ5eì dÓ’ §IX‹Ò¤ó�Úb¥‘^gjÅ«ùw°é"{QŠ`pNGñúVœ¥ ªó› ²¥ Èkôþ(.aæå³/�ê“Û‚ÖÑÒé÷©Äm�Òd¡�2˜h÷ÀÎ!DàjNž\E¾³GÁ¾¨–=ôÜïÆ]ˆÞmª];ýI9ÃÝ�‡Pi•ñŒ†GpWq hV ŽÀ%A#¸ 3æ%y` �®d+€œjô>/X8» Ç«g³‡ÒÖ…—¨Ð9l�k.q‡êsÙAó`ï h�‘$:,4FïÐØWÖ`]¯`æì|0‘¢k)âé5q¸!Z„˜]ÉpW’QðBНœ¿mìû–Ò*ëToæš|ßôDº¤pl ‡ê[”™ˆ§t4ÛÏ�®+~¬r޳‹aAe%îªaœ¶ze—¥\tâù£€/×7Ç-#'ògÃm€é”¦Áå;yÈó@ó¨cîÛ÷ž±©˜Ò ›šÇÁ?‘,ÿ?Õs‘_œéu[‚*2h§� Œ„€9KÆ�!ŽÆð`HÔl,ÔT · í4Ý«JÓÕëSé¶²y˜®#¬ÁçX£Sø/ªß.ÄrÛˆ”ŠV¿Qgq©òò¨'¡Ó*2T^äªïa°<ÇóÞFv¤(a³©½aŸM®>N>æ9-Tù»¥°­#îËÙÞ•ð®J[Jh`d»LehX¼Þ»í)áÔÛºajÇÃã¼!¾åc2‚ÅÞ^¿?“5Œ¤×5rµ Äëü’¡ã*sÝÓ­Výýp¯ a¹5K)Šd”É|},o™Fô‹æUßò¾g{£‘I?JžœHrŽ–¶èÇÑYxú·Y´j“�îäìΟÇ<Ë‹!E`7óÿV O_“ÀÊeègâÆ™ûƒj‰u#jÚ¶Wl¿û’øYà�·¤û¯’„?¿g0ˆlx‘\.xœµ˜íkã6À¿ë¯x–ÁznÌ ÇÍ1tw+¬ÛX»cŒ ÛJ¬aKA’rýI~·Ó´i ¥ŽdIÏûOO}óå÷Ï÷ÿñä¦,b5F3|”ÌPHsª43«Ee6—!În �QC—\¦v² †K·Ìä2ÓQè×�(¬ÏJdv°'Û]æ°cú<~®%AK¶ZÌ-ŽqR+âjVµáO¿Âµ;H ªõj‘€�ÆR™Í[ö ¢ªb¾‹Bû„DÉìÂ(Lbx÷> ÝQQ¸{K1n±u�_þ˜Tü»Ä¡ymé›|Vv Ÿ£†0Œoéâ 2ù�¥fIüúŒ?4Ú–úòÃÂá5ë4›5­ÇA1S)¡­`/LN�K#M@ÈZ�öÜ䵈ߜ ý~?sqÑŒ©Èê¹›?£…ÿü[�Sž©÷‘¶f˜œARñÂ\rAç�6�ÒŒªVu©š_eÞ̰åLEWÞ4E�à]…ð�‚6Š‹-è”T½Ä°qvtÑ©M!ÀhQa¦d�œµ›J¤îïnêTàbŠÁì©#Øþ *Ñ%nSãe¶¿ç=ŸÙ‡,²Þi'»ÃÞ·~³™àÒ¸i[nPÝŸ�¦ÓL¸†”P¥è¸Fó7•f6ùr¦Ü^ßÿzýc?eY¹3‡&q7 nˆËTY÷^)9ˆ†JŠm|Ç°Ô -±„ÃzŠ >s‰øD5Sœé¥eü·ÝxwŸ-@ìó¡µÿìmàþ€é‹E<3é™ì´ ±ÈO}ËÌ)Žã’sé}×çš;§ÅX“Î/·ÍevZJ0®àW"·¯Ÿ%_Á÷Ÿ.'a§äŽ)LIô“yݹ58âã 0e« ä‰ÀÔ�t¢qj° }IC—O¥5E}‚±­´ [é<$É�’PR“æ&ŽhÄ”fAïò˜‹*KžÔCÉ}Ÿ'è—žæX¿®²¬Ľè-Š.¼±Fÿ>)ØØrwOØàØsH'“ š­­Ã Õn ¬O´�-‚zO½ÅݳNyŒ)ú°†›ÝŒëdQ•ÂÍ78ƒn3±ö÷‹àjHmasìÍ [N®}o³FÖÓ©â [¯{ïÇ�õ\ŽŸIF}šŒú<2&±~¤‚ ãø.Ç@óÝ/nx¯úÔzc…žAÔ»ÓjMÐy‡ :ƒ›zŠ®s Æ*Äe4ÍÉ#P›¥•‘®¸§2p…`�gX¨®l&Ñ@ÅiÒôim[XÃÉukÊö;<Í¡¬´ql‚’û˺=™t[w €6¹~�Ö¾é÷(ÞJl¦4«{#È$òDk¾%5Ó¿vã&Ñ4ô³-ß/b#— kð .2zÐ3ýÛ áíRô•BE7P)3¾á,ë²`Ô2xèZêÐ=Ŭš¿r§êM®¼½ýNä7¿  :¶ËÝ7çñö(�·¯Íc=ⱞãqXÿhúŸEþžtÕ zz�|ž /xœÕY{oÛ6ÿ_Ÿ‚S�&œkQ`HYš½º6ÃZ`†! %:b#“*I%u‡}÷ÝI=l¹‰Ó$À€¦’ø¸û݃÷ 'ß¼:;yÿço§¬p‹ršLâCð á8Ë n¬pGiíæ{ߥcwÒ•bšËÌI­¸YNÆ~$™ŒÃΙΗHçÛiÎwËJØÖÝ3É„3Åâ(m'ÒédÌqãóyøL&ËJníQZ¼ds ÿöàÓÁŽLç¢Ëg2ëmžMZ2WHfÊÞðŠé9«•üX v)––9Í®xY »Ÿøe¹¼Šüvïeêw³À­CŸ˜ž}™c….sË„rF ;b‚gãÈ‚q•3WφI—°¯�ñ>c§WÂ,i•´ˆL+ÁPÚ$hÒoëÌ�ˆìIÚ¥-}‚Õ8Ò‚=dîZ{�¹4iÁÄùº* âÁ÷î²" g9ì°:ê†ÑRe×ÒLé(6¶d¿8EoaY¶nøI‡"×*Ój./j#òC‚5—ƺ† Ïs‘³f ì!Ñ·™xµf–Ìî€j­å±Š¸#Ø&ûJ„ · 0ÔQ.)¯QUœÙŒ—Ü$Á-YU/D(ŽÂ˜•ŸEd‚$ý¨´bQ¹†{.lfäÌ›ÌÓ% hÏuÜíº�ào ¯S?ÞèB×ÊYp·FM¨ÛŒ+¤3 *(m)øË;*9�VÍôhPà ÄU¹ø$Õ…W&:Õ)€·Âv  )t^Êîé”C(§ÜÕœý•và�ÁI{Ò'èÍí×Ô÷&V7k ÎãCoKø´WÎQðpYJƒX_=8HQ¦±µÙíÛÑ*:‰ýÒã¸k)Ô…ƒ¨âŸ7cöë'Vãiè¿hòX¶?.<•Ë:&>¾ðùªáa½PÃüãcmuÚö×q{®sÈȱ&8Xk÷ÖX™1ß>>°¹´TPZLÍÇ .RJ,A îš´Ö/mz¨Ç1µmÈ¥¯¡à"ÈáȨ̂Î×Ô‰Æiï’)‡w…ÇJ/ õÞMif˸wk?œN»ÂmdhÜ+ia:îk1è¶Mæ¢;Çp‡áï6Ñ쾜î_neô�}‚±{¹h;#w õTVÒsHÄã<¿[RÛIJÔú²® ÐóVF�w.÷À_6¤Sÿ€3÷+¶ˆE…¸UEqÙ?ØÐÉ\¼üØÊ}~€¦':½ßÅuZ"]qVz¯t½[ï\Ò�çmƒ¦)¦ŸUB½ÒY�U ³Þ°ÙBçû·md?Ñ‹Šå èƒb¯á‡IµEh�¶î-†H¯d¼�5/)ãáËÆÐ�÷ñ­:ð" ËhoBïVMÞžjð©¯o˜Þò·± ~KôïÿP‹»ŠþG¡„ñmÒ`8»ÖclåPñ†Špµ·ö0\«¹ÂBßê-¸‹“ÝÀ®«�B›Ï0�NÃËphÁ«‚>iVfð>dødî —\¯j°±¦�ÐÓAºŽæÛ–ò*G �}cóRf˨¸[š÷+beZÑä¡C`ÔQQ\§ÑªÅê°z ¾ÉmN&‘c̰4`™ÊOŸÛ³¹œ@þS\›û¿’9¼ƒ]¼zýûa¿ÐHý~þ)å\›b. }„rÁ™EÆÝ›Ñ¿>„uz§Šœ «Ù’T}-“}}WÖvì%½~�µèÃ­Ïæò¬ü� <­§þáÍ¢ÖœOŸ%_§}’A‘ØäÈ Íí¸Û…ãfÑù[+ÊÑ’ïp«jþï²q‹øã°?>ÿ`àó8’r}¥h*³xœíZýnã¸ÿ_OÁ¦ÀæPâK ‰c`»{@�öÒâ.hQ‹€–èX·²¨ŠT²ÎËôEú4}’›Š¢d)þX7w( °-’3Ùß|‘šþæÃŸßßýý/߉¥]å³hÚ|)™Â×JY)’¥¬Œ²7'µ]œýîdÏmfs5K³Äfº�ÕZ¼¯”´JÈ"©2I•Í•8mÇO§^M'Žô\§kdt1K¥•v]*s%ö¢K£©…\©›“vä¼ýy2›N$2¹œ Ž�K.E’KcnN–oÅÂÀ¢D§áî®ÄôQV³t:Á/q#¦óYHd>_}=�Ъé¤<EšñI­ OŠyÉ£Ìkå½Ä>gð×~ ï‹.gÿ÷ÍRåyV^»�O·Ýy·ƒÒͼEE_"7QèùO*±ç¯H³ÇfG+söö„‰ðÚÞØxCˆJÙº* HÔE¢‹EöPW* &Çâ)³ËHZ¨ÂV™27ø j§Ÿ¤mAàlÄÚE†ÝM×J…V²,³âA(™,…ÊÕ äz!ìôWUr-z,„Õ4èæF8yƒ©È šdÀcD©M†ìb~*�~•ªê\ˆ;˜bŸ´XկйŠÆ´ÌdÏ*†©Ô% ¹rZ{ÈU�r©G[Y¯A§‘P«ÒnÊ�³A bÐPd§ÀL ä;$�úJU ÿTiDføYr7Û#3ZùIñ ºËç6JkÕH”Ú5BÀ„HOCªÅ©-ZbØæçD•–Wp •‰U•3_†{5°02?ª(`¥ŸB¨Ý…8ƒ](¦Ë@„ñ?H³l•à—ðÿ 8Ìë,·gh`X¡Ù$ï˜5*‡)@Ô_êôç‚£‡{Qô""qõˆPJSQ2˜ƒö)Ú}±_±Ôyjš9( ),%¨…çÄ ±õbY9Ï•Uó4ìŽ,À6kB&J ô#¦ þ½T•ß¿»ûÓ»ß ™ EpBÞ-.Îæë³ œ¾¿kï£áÐ(d !õ•2«p'À6ôrN1ÌA¦F2²Eô4í…+úŸKDÎ7èXà‡è**Vç‘øc�iRÃ`åP"A{íÂùš¥€pJ®Åq!"†o*óÏZ_JDZ�$9+þÊiÕÁ�Uø¹«‰¸I ºËF/OJ`[ºÊ™O¤=-0ºáÏJ-j 8Dì; ™åQå"O¸oÀï�Ú1{ÛМ«D¯`&?~»µ×où˳…HÒ¸uÜ™øíyojC PY²³? œíÄ�ÐN•hÃÝ¢a²ô{ ÄQ‡· ~CHð E½š«*öÒŠnx¡Óã/Rläæ­2ƒ5s˜ ÝÊÛžõnå]àÓ”�SµÈ °�ã·hbàã H3WÎCk¬Mólæ%<ŰsÚУÐÄÁžœ¼ _cA~†ˆ.0#Eâ©‚,«Ò.X¯�0è2‹Ö‰eÃÃ�³L/âPj�p�ÞÐBš š¨_VÙ#ÖFe¥!ZHqƒrPØëб"1¤ ¡&Sƒ/d’üˆþUV!XP¦�³ƒ*rý€pÒEo�<+ˆE J9….žU¥£9Û˜@G 0Ìw¹@ùÑÇŒ+.�‹úl›º—DâŒSDXëáÆWš« ˜.â"^CqQZÖ0cä¨Ú�óÎ�Ô$%Žw$Ø&QäŠß8 1·U(Õb(�’YÄ©�q$r­?aˆ¬KZ#¹áÀ»Ç(EÁéúäm‚Ü3ÄO:+0‰ s/’žo´HÐ ‹®¥6N PP<ªî¾*6ˆ¯ñ©ß ʽ&‘9Ú°*�W°1y­ÐZW^àr(õBu>SN_I›,»…88’.f?*(72ÍÄ=Š„€Þp H{Ã{WŠœc›û[_k~:Æ�‡ØDÆb„"w5÷`�û¦O½¿Æ™UÐ~f潯qOf#/²Ä´M 9mop@mw(3DúM]–Ü. Ñ&«;r®$ ze@x�qôcVÜx2Øa„vB_®Š»†� ¡=ÈËŽ:Gëÿ¨ûU ŽWaŽIìsiižC¨ñú}ºœñûÃlu¿ ñØOEfÝóùô²>h_Ç]ÞÑt$:7}÷Ðx§Ò {Œ/ðê@ODj›S»~|ûâ£/ B7�|²éæçŸH÷Y�ÞW9X¤DÜÂ,U¼TÈŠÞ:<âÛ¼Çöˆ›ú‰GP‹®\㹊£ ègWhl“ŽÎ`÷ÃFÆ ®d.tmËÚº�æ€Xµ«~5e×ñ¢ÜNAu{•*«þqèUÓÙÕ/ûUž7âuÞßÖ‡'hFñB~ìì€�slàHíj�Í^ýª{†!Œnr°Ïá— èã°#&0+ž�S›½ðªBŒ/E$ŠòÝ%�5*_¸WE×Â*c£�’©'�¸¹ßtÎ-^zGâ¹¶ÚùDn+üÓÌ”|ì8‚ ‡¹A«»ïZ÷) ò¬yï£Q’é `gàw$ ÄÙr*á_™h¯U#¯4&`§d˜¯émäu~£dãÜƒî ›‹ÁàM wXüŸý[ðéÇ×ì—¾^9õåŒyä´ï0ua”’Èß=Òi4ʶq�o!oÈ &s¬¨:nI/MÀJTŽ+QŒ‰{ÆóŠô�^݇9|�zaâ^�ð»©?Á›œ Z ©ZŒK�™¸xœ­XaoÛF ý®_qó°¥d» �uˆÙÚ¶¤Xƒðgé]sÒiºSÿû=ò$Y²œ Ã´ˆ}"yä#ùHyùÅ››ïo÷Vd>7«hÙþQ2ÅŸ\y)’LVNùËIí7Óo'sœ{í�Z¥:ñÚ²Ú‰´Qn9çÑrÞè¯mº#k§«Tzéw¥rb¬†çÑRŠBæêr²<ÛVÚ«ý÷Éj9—dîlõ´¬�A¦‰‘Î]N²s±qÐLlÚ÷øB,׫‘êz%^,dµJ—sú þ¶�›ä\8|¹œ³±å¼]„S|õÿë…ÍáõáÁiêã>­Äot“�}°½Å÷ŸÞß\ [ Yˆ›RolRçªð•Ræ2¥¼ ÛgQ0•ê‡6ÀÜMÏ'Ázî š�`U'ú B:!£ÆÁõ'•xòØgŠý9ŽK(N™xU‰h 0ŠM8_éâ..‘FV±¨Tid‚#Ľ ö춥碛 ñV&™CÑ«|­ ©‹ˆ}°UŠ;XI‰{µs1ÇàUA~o¬1vë.Ðjj!£W,HϽzô¢¨É¦ƒM²¢+á2[ n«ð16_ÿ][¿8��‡ b/ãH{§�x ¼³Ý¾†¯j5PA¬é@b#�ŠÄ}I¦Š4i]IJeð8S;�n»ÂB4‘l,¦¶^ÕÞÐñÆûã¨Ø¨-™ê;í]@�Š-k™ÜÓetÌA!£f+�ÙVû w•V£6`Ng¯:0�B¨ÇÒÈ[$la�ÚÊæb©ò®f¥Ï&«ÓW��.ç*_‰ºdXÖ ù:*8ý†pÞ«;J—zD�™ Á¡ky=†úÇbÓº‘`¼—Ò…ö»ž~Ô<š<‹¹¤[SEmL«T¨ÉVíä]¥œªÔ¯ŠWž´rœ÷…ȵ£ô4ÍáâιÛ#^4£Pz÷/ž,H*Å#6Ù�V¹‡fWƒ%ˆ¦‰Ÿ¯Åù{ qÄ¢@Á©5ÃÞÚ×ab‘Òµ†6Ó$ʘÆ|ˆ \”Y“³´|Â-Ÿ7öeUÉ]̾Rñå¥ßQå©}ìüÙD8k<�ü'Öóôä‘«Do4â¶e€’[…û�YŠJ€LMY[”RW )’ÕOÔz!É€Ú—è2‘îØ8ð0EÑ¡�~P4øl ‚Õ\ “áÛšÙ´ûêrÖ¥�ah¼¬t澯ð?C(©Ï.'gç_álˆ –�¬/óº“é¡„æ>| ; ›N‡¦CGÑvr‹�央>í˾îÉÊÚÛ®á^Ú©ÚÈÚø—¨cc¸Ê0I`-pI ¾ ³EÛŠ³OÎL1³©Ñìl³?]t @Ú'#Ùˆv§Øãˆng!â9@)ÞïÞ�.ü³\uLî… Â¡œRî¾5hÚÖ˜~¥LÊ�Ú'Œ[rZÅ ‰F`-&�v_(lÔ¨‘( ˆ5Fɽûü€Žsä¿ +Ú§·]e>g$ÄÃpF”:cÎM@Ðu …\[ŸQcGi×�m¦BŸbÙ T•%²(¬ÇœÃnó!Ð31mà ,DPÑ .Ú±Í#¡s’ò†eEU: *m16x´‚må�{�«ƒÒmÙüàx8Z:áÞVÌQA¢ê×Ja}ØÔN¥<ÀR¿\Ýþ|õ] egYï•ã.ᤧ‰ÍK£›‰Eµü4V¿`éÑÆ-ÂÞÖâàŠžÁK6³„†«ñ䣇ñ8нð€<€€ƒŽBÔƒÏa´Ì�â¹zzço}h)¯½iL!1HG]‹^ä÷…ÅÄ«sÌÛtJÞ¬Éì)pÑúnA�ƒå±Å…¢ï?ÚïØ°Ø!_'þlO1eœvÊÖÅ4*&^8‰ùz;§ØJ‡î¸mz‘׎†³Š¯Û \˜TÌ:�“.æ r�è7˜£/ öæÍ{tivÔ†çÃ2�Þ…ÝË;°¼ðòž™­™×b]w¡Œ&P/™¿ªÜâ ÏØ$lLñ°�†ñ6æÂ .puYÒûFÚ³²ž¶¸[½WXaŒ³xK�7G‘x'ÏPgýwò�aÃr3ú¡àË!Ö“öééÛaÜ>rŸ4öùœ rKïùìd ?¼)‡ŸæáŠ-©°ŠT•,­xœÍXQ‹#7 ~÷¯PSèÝAnCÊ&XÚëK®ôŽB)eqf4w'v°= 鯯d�'3™ÙÝäRŽÂB6¶%}’¥OrßüôñÇÏüúJ¿©–b‘>Pæô±A/!+¥uèï&µ/Þþ0™ÑºW¾Âe®2¯Œ–ö´· ÝbwÄbÖhX™üÀú¾_æÒKØ¢»…1A:!´ÜàÝäxàFi‡ÖO–‹™d=ï–ƒ=’}G[[È*éÜݤ|…#�Ìä]Œ·°ØI»Ì3þ€;X¬–IÁj ¯»»Óxö®·°“U�ÍÒ›Å,XX̶ëô÷–¾úËQ|WbU©íœ ÉWwh÷Vy|ÕØk øbÆîó0FõFÄÕ\í¼�{û~F'p"–/�È“[fË1œÅ*6ÒgeÄ+õ2é°ç­Ñëå'D €3”•³fI ¥Å¢K} 7Þ0'Û¡=‹³ÃÉr°Ä9… µUÆ<ÖÛž¶¸ô¬¶_(¬tY±ªZ•£=¥a%²xÉŒXiœò¢1ÊOÐ.¡ü]—l“‚)ÿ*~Òä Ð~&NYÉìñwŽãI±ÍÞLÿúÚêc�ÖaÎæý#̈ñÌ�Pi²e×7-†Jt'­•‡lkã¸È —�+åÏfü.»ÏºàOL‘wêZZkì8pæ‰çn¨�ÏÈ™ÐGPR/x´`RöÄ�‰¥NäW9¥×Uûi¯ uø–;‘v"Çg¤'-�î碿ùŸÜL—«s6Ë{sÁ4K(ˆë²G²itu ö†š4ÓM²[L†Ê1ÈQ¦/Qe÷75ñû*tÚ¦k^EªW×ÅÔšfב‘õ j�Ë=­i´9ƒ\Só#×ÔEFÈõ²‰6)º”d™ÓXòŠ6š>ŸÈøt$²ã¼z“ kŽóž+nXpç�±þÕe6ª�ûç¨M­µ åЋÎÄQ� þo-InMº@mJ£+Z—Øå¤ÌÄUg_·&®7š{/ lß§ Ø­…VÃi1\5`œg"ÍÆᾌXOšJLH¥m©ly1ˆ£Þ-5ùV@Þ&¹"EÛÝ%ì¬YÄ jUáuØkÃ$Ž”¸¼Qù>ˆÈJåÜuX\ÀÊñïŒEbóÌä8…�ÓÑ3e´îy5‚‘ÀMêȪŒÀµ ÚÌ < û•Ø{€ªóA­%ýñÑ<ðp¹^¤*wvú;(OÁƒ¦Ìàí– ]?´1C1<:p\c¼”ÇÚ�™ª25HxÉk˜5it3okeµPGƪGœu¼½RGâ€‹Š 7 _¾ÁÙ†q ³k68&”*ŲÄ|ð�@³r(• fªfßR];R5iE˜_õéj§àuÌd“~�#¹™Á«Ÿ­¡«>C/¼ Âø}¡l»+ùÊXH Á¿�‡Ï¹FÍi[NÏê5§º²¦ê aD=œÐÝô†ÂŒEá‚øZ8!i‘ˈRQkÚǸJ•%ðÒ8…1?œÇKn«Ô7•®š+¬IZòïl†äü lô˜ög³ùÈ3ážÎ¼s½–zôޏKý<Ýšš„O?PÅyÂÃWX\í†ïÛØ_s™ÿ0¹ß)d,Ùó\†^ÈÕ&1åý>…õ²×Ïè2«Ò¦“‰àôÓ*°ü_v­�O�äIÄ‘ÿ7üƒC»àY “áîPà›<×O•ÐeËMÏÍY©ÓpXð9ÕŽ¡ºcæ*ìɇøì,ºméžOáK¿7z{â!ƒÁ©ã/ei�ñ;ñhün’7;‰IN¸LµÇ)힊ÔCÍËadÓ‰Gi´ÞâTñe]èºÕ´IŒº£L²‰Ùséܶüf«Æ=&jQñ6úoîÝË“·µŸeÙ§²x‡wç^Ñ¢a»EýÇKä¿„~‡59ƒ|�‡|Wxœå–ÛŠÛ0†ïýSJ IÄJV1¤‡‹BKK»7eYÂØÇ*¶$%múôLJ:›ô¸ôDÁXè0¿þñ|H–wž¼||ùöÕS(BU&‘ìBÅME!+Ðy óxòñÃXðxС¤Dé,hkÐíà™QôA› Q�Y“a ƒõ¬ÍâHŠV4µjWoq–( vkò3øN-Š$‚ÁŠæñç˜IaÝG^'R`­=MŽ'9zÊskÈJô~ç�{~ÆÜ­#3«†9Í@¦Iš&p_nÑ%êLŠºAÓ�vÝ{•¥^_<�b¯$źÞ-�×”oÇÀH>‡…´BAi—úm·Óð�eŸd¥GzLžª‘p\"®�¤ÿYMdÐRàÝåõ¯—ß�dbæXg´Ú¿#9o­&²gq) …5ë¹Ân¢oá kñ”»Â—÷U¦V ÒS,Ëæ¦íÐ÷-2“ØzMIâ;6˜¥4¿®lVÅø-èÚÃÏH �võhPÏØÔgk郴þï}B=¹QÈC�¸£WÏΫ |©`½?>¬âiV´tr?4–�¦褵Rmæ w|;‹ívj“ÿ+³—ˆýyxM‡!`Ë´Ž›‹¬~�ÍtiNçƒ�‰ÍaûgŸ&™Ou·½ð3*4ÔÞ¨ur…€w¡Öæh}²æ¸"¨å¨Ç‘Hn^½8YK7óNÂø—Ì'àÜ7^�R¹W/F›¶­ aXÝ�t–!Ló* �Û/ðæØè©‰sKðƒ”¿Ñ¼E~UV%�8ÂSÒðYsÎØáÊkŠæ×ª•·¸(âÔûá¯iâQxQËqìN¼�§+ì‘§´ž É™¦ËY%~mi.\½$mà ‚ÌEå'“ÿ@¢�L ‘‚òe¡, ô¼ ÃN¬[öA/’ä`±š÷‡'ðàAbúΛÈJËy¿G�Á÷­;<Ëú¹ŒÂÓþDigÇÐŽ;vìŽÚåIM\ç”öŠÏÛÍñ6M|„Ûså×aèxضAs¾ÖœVÁo€üCñn:FÔ¦gÓâk­·MöQ/ŒUÜ £Cöa†=˜Ù9ß§¢:ñ ¢”~Çr»_[ÍpÏ™Peñ!SNÅö(Ñg²èB(¥m3Fs]«9B h‡ú HµÜжCº3Ï�À@‚¢â?0Öwh>pWFi茡]�ÛV¢ £Y†ñ6rm,Õ/5 ÔMEaIûP§í0[ï\¤£q‡\…Ë�¬•ŽÒ¾·©Rm L,gȄä;_¶‚Ü»Ãfl£Uù†àÚÎ>l­ã aÅXñ“�ºÞ~z�L/v.Ü­dýýtènc£‚ž—ýÉ¢ƒµï±ÚFgÍ"Ãf?ÅÐñåËËå\rK\]o?´Î÷±º< «ëí‡5¢ÖÆ;Ð-ȇ˿oCë¹çÜ‹ž^aHî£îǾ¼¯¹¾i«öQºnéÿa‰Ïìåèûª}±øÈæšÑÙ!$zq;8¾­.ýV¤CcHTFŽ*ó èÝöË"Q9*ËšçÐ 5Ñ£š\¤ØÂ0œÊ8ü鉻P¿Ò»³"‘�¢,"“xœÕZmoã6þÎ_ÁæÃ¥œ¸] @Ñz¤»º@»-Š´@QÜJ¢×¼J¢KRvÕÃý÷Î I‰z±¢$÷áH["‡óòÌ3£Qv½ýáÍï?~î*ïØ.þ‘¢€?•t‚ça¬t¯¯·¿ùâj ×�r¥¼s"kJaøÑè£4NI»Ûú;l· 2]´(ï³»B8áÚ£´_ò¹�°‚í¯E%__…·o¥Í�::¥ë«»ÝV ¤Ww3waÿ+¸yäy)¬}}uøœï-üÜÀW[s]tú~Ïmù.»ÉàZºÛQÜY)yÑ/bþV¡Nñ¬ÊÞ|~ua5·G™«½’–{_ŠÜIÃO2wÚpøÜ:£êÜæÍbüÝ~¼o°bÕãÊò\×'0×Ô,Ñ>ª¤×¢,[6ä-ç¿ê†ç¢æ"Ï¥µÜ$÷8ƒ®t¾b\ïAš# w'aî\Vî¶ø�Ÿ•; Ã;·?v¾�ʾ ¾Ü‚3g‚ÿ³•æ-€f&òñÖóÃÞK˜Æü¾(ª)Ê`®ª÷ÚTâÑð/mÜŒB Þ7F´�ûºå˜è`LF±É$Ήüë‹Áwˆy´a)нmô•õ†¿0„#Á�Äï­ªdmÁô÷pÙÎåï`Á Rx$gÑn©hƒ8Z;ÎDwÖ7²”°Æñ\–¥#z¶K42ÍúìNw„<Ž¡¿˜è´`ÃÖ%zÈóG”YÌû‘‡©ŸÀ†=/õçÄGô€Ë¿n�3÷¢)ÝfQ!5Œ ‹¯Òçë ¿þE…JÚëxÄÔdOЀaÊq£Ï¤#ú)×eSÕ–gmH8 ¶Á"]Sj¢· ”¼âcÃ�D¥œ:ɲòþVŸ%D H:ì¡#!„N(�GÆ�p·¦%{e¬ã#þ,¶è‰\ÔÄOáz6ÊI:¨âN’–7Pš —îñ&žºŒ*ô#|Ã{‰N÷ŸÏS2‰ VpÉpé¸\JÙ}°'rÆe°µœ‘PÆ=,À´¼j¦Ä­´TGð /”uªÎ�ÇÄU^7Urhƒü£Á‚È Wõžõ œ§%ÊÐXE48ï¶uIÊ7ŽÜ�ˆšèT=ÏKô~q�¿¼8J)n°=Jà9� :Uà ¹’êCE0?¥0…!?jÐZþ ž]y-Ï”7#Dme*<`?½� tÿå©Ä,¤B¯å|@^¢«Ñ‰´>Kht:7m°3MAÊ.¤É |UBùÞÁ®G²ð–ÜêYQÕ=oÿÕèQ0þË%zNz¯ ãûÆP1Tu!ÿœæ#U¡^e`€ å{]¨}KÙŠötQ XcŸhœÆF:'gg‡ 6 ŒIÄ^îk$ÞIÒ5Ì.ª,�/žxR…D"|z¥*Â�Tõ!Ö(MÚõiš<ú,ekºìåI;”¶”»ÉÂuÕ,ÝñŒ¢ÆF9ô¼¢Æž˜dIŽÁiÄ¿XÂÿ8ôžŸÐ1T²B·ö…,Õ´wóe«ãþƒ8AÅJ=¹ñx”mY‚NŸ¿À�z  ‰ÅÔßÙaBÍò\Ã(C¤Ì {Á3ãÂ!Ð,GÝžN/½8sß5¡Ñ~�pÈ .¤mpg!õ­@%Ý#55$?£­�2©øÉc4±ÄA`PÙ /øx ð'Y˜R²½žCV±ÉÏ5¨»@#tÿåüÄ,Gƒ+Ö1-ýìSªH˜‚½ˆ*¦`k™=ØSÄ”!Ø“)âC°Š`¸ØtÌQ[Í ©ôD�)°5¬€N‡–‹tÀVóÁ� ™=à °‚­fhëg˜€ÍQi¿ŽÞêTÝ(×.A¿èål�ÊZ „¼[¶È ½´™æI°…ö?‡?Ô¢÷ãXç=ý!t&mT„9RS[é®»ÇÁp5ؤ;¹=à1^$t…ëò'&S¨wÄI>ÊY;„V?@ñ›ÿýŸ©ÀßþÙ ~J)Àf‚ù™�=ãÉdú`ÂbÆá¬ç) @Š o ªÓêÆÏ»}åOl�åðT\P ÉY!Û{°%³ÄûtvÕy ,Sr”çYãz–k96÷§ °¡€Ei5?è3ÿþþá»û¯�t‘Œi,OQøŠ,9©±’\:ç­­ô€q¾WàfØrÐÅù8~¨qš(ªc‰ÄÜΧØÙá‹>¯ < •"»�<0ñ8ãj�4ôB¬ø!wX\lìF]—mÿŠ!ôÏê9’„ÓVh!45„cÁÑÜ(ÏT³æ>t-�Q‰|/`±n®J-S“CƧ#V?a}¼ÅgŠ¿†ïíûåìtÙ` Z®Œr(œ@ú·PêytE$Ÿá¼ïw@ÓÞ€'T„äY›¾ä€Ä �Ãÿ0ö�F_–L9Á«@°W„¢X[Œ<–bÀñEÀìB(±z„YV¢Ï&T??®Ru^6…$ ³‹HFfö,Ç̦‡Ä÷Þ{ì9†U)h`d.ƒ¯¤(¾£æŸÞdíͧžE@‹÷ *5¸²õã›lò Õä„É·áD¶þOþôÖ ‹ \/–õ‹„f� ‰Pxœ�UKkÜ0¾ûWL÷ÔÂnL�¼¾¤-äZh.=ŽåñZÅ–\i¼aÿ}Ç’å4În ƒ¤y|óÍk·x÷ùÛÝãÏï_ å¾+³"„µ=1‚jÑyâýfäf÷i“‹œ5wTÑñi x nmí‹<г"ŸÝ+[Ÿ&°�e�Œ“­¿…W^¢Î ƒ=í7³öj>7e‘ãq]®4âw-ŠT‡Þï7í 4^¾�yp/òa QÂ�#d?V^9=0hÃ«Ž :MIép³èXëcb×ûÝÍ&bEf+B©(—˜€ ü BÀSGŠ#�laáÁ6“å@J7šêÈ à±%á=Œ ècO†·° ¸…~ô •$›…Σbrp”(Ö�|ËN›x…:ày‰$üÌ)D‹rí3@¥h`!!eb!ó×SjÍhkk¶’šjýlb„¡Ój6Ûf³¸³-¡“8ᦹ•4R] •ò`Ý bu•Á=ƒBØy;U`6§®œÄö¹èÏÕ¹*kµ�É^²vO¡ë"ú©Šç2< ý¤¹Í`6y7’Oó•Ë€�_�dÕžß� ú�åyüºÚžhY[ýJ#Þ�ë…} ¾2J/¥2P˜i+Ë^´7/Ö_dÓ·³”NöºÐWŠq²8ÓÒÍI´�’n¯ABÂÕਦF™ü€!ÄE”Ù¿2—E½�N2Eô{Än»j‚NKÂn¤ç ±âåž´§Kæ�Ìv²_�Q>ÿZçñ/àó¿ìƒ� †>xxœ�SM‹Û0½ûWÌjωè.�RAI{ »�^z*cY‰U+H“@þýŽ>ì4°»‡‚Í|iÞ<ϳÔÃ�—ͯ߯?¡§ã 5‹›£%Ócˆ–ÖâLûÅW!9OŽ«/èz²J–°Q²¶µ¾»&�/ºCÂt&~ƒù4§…0âÑ®EÍ ­$¦–§*û�:�0ƵèW°�ü,8$>n|gÿ…Wí­³e´\Wò”04ìÎm4Á�ÜHÛÁB{M´\öSë²)�»Ls�q±ä;œÉ Ž®¥Ô#ÁMÊ!ÙqšâÆC�0ÁG@c|èrÅ7Ük]€Äÿn0OQ‘‚zg-à=𬩀׻_‹ ¿Lr=fWèlÊ3–ä¯(+Ýø‘ìH±î´Ö3LÝÙVº÷]Ì€BoK”À¸ç9)žÀ“ ZQW×ÿ)ÊcM.g‰g}ð¦OÂâw,o�u‰÷B} Sé“Læm‘LŸ)eï=N¹À” òL>GàÛ¿ÖäiævÁá|ONN{“õNÈrÁÞNÂ,m…� Š.…xœíXÁjÛ@½ë+&>”‹%•i›öRšréq,�¥…ÕÊÙ9U¿¾³+ÙNŠJkìúbƒAÖìÌÛ·3ûx äìã݇‡ïßn¡äJ§Q²zæò¨ˆ²­#žŽžŸ¿ÅgÅšÒJ9§L÷¤‘UmPÃÝ‚løï’¸KŠ’¸›Õyë¡/Ò¹]�»†¿`Hr” ¬h:ês'ô8J“=Öeº Jö¥Ä�itn:*¯`îäw.¯,Y�¯9_C²D›>|Jbÿ„)$³ÔCÌRx–nº•q—ø¾{{“Ä%‰~§nÔŠ[˜×v}´[7‰º¬\-WŒ*w~5ê ;6¿“ ÇÙ”.I²µDM†¡ž‡ˆk ã�èèÏ‘a:}�½‚F“ƒ%n¬q€ ëBe2’p¢~“œLœúI€�¶ Ǡ̢axR\‚b¤©räö×=«9jGýv“è%)¿ûsVP5;ëwËêj!c¦i O¥ÊJ`‹ÆÉÅ!çñ…J šÈW„v¬ˆŽ¡6ä—–œ­„l˜-ɶ�+¡êäÒ�}¥²Ñ&Üš ù‘ûbÙµ¤Z/^¬¦.#¾[¯÷�‘.5]C*馴¡ö%hZðbð¹¡Ï€‰…ÓZåj9OhéËë%õOþ±ÖPs;*±ÜÀ4 ¢‚vV‘‡ØREŸ-É ­´J&äGZ,Ý�ª :˜ª^üî$¬# ‹wï&¬=‰‰*¦“–ŽZKzȤôî&¥·7©/äÜÿr(}@‡Ò'‡:zU 9”ÞÝ¡ôöµQÕž”t@{Ò'{:r!™!{2»Û“ÙÞž¾ÖÜ·u?B2‡³¤³“—ŒâþS`Ü}müêöÜ­ˆ)��VgxœÅXm�ã4þž_1ôº“Ò�ºÒIèH#· øÀ “!>¸É´1—ØÅvZʯgü’4é¦×²©Òj›ÚãgÞžOš~ñðáýÇßúJSWY”¶È ú¨Ñ0ÈK¦4šå¬1ëùW³„Ö 7f5ך‹ üÜ :À‡-*f¸:Mü~”&g%‹ƒE]d3̶¨ßÂùã$¥ «q9 bw\ç²jj1ËÒ„Y°ûìt‹NÞÓÎòŠi½œ•o`­éoN_ �ËeÑYýÒSÙÇïÒÄ~ÂÒUvZeðÊ üè÷_§‰;�&[«!ƒ_Ð4J€Q _wÎ0¥Ø¸Š›ƒ‚æFª»ÈŸ,ø®µ®Öó73æ-;5¨µfÜPΫ©’ž³ 4ýcjèXÁ¾äyimòj¬É-YnJ<±¾¯mÄ•˜(ÖšUº“¥ö\ãÀ»ás?*Ö´äÌŠ�æÿbðó·3~zÁ?œÌcp¶b{nJRÌnˆH;Œ /Ú& ¡ Œ² ë­9Œ’ËíLÀ­€óbj9œçʽ5Ÿ¼D¢èó,º”±šÅÿM™ßš g-Ðý cÿóRç-¹uî;.§Ï6� lŠ€(×ô|{ �5¨°…¯õI€åî[hÛvºÆqu߈�ãû¶q¸o¿úoç{ôù¾×;w™¡þiœ¢þi”£Wr³E¸@ΗÐÿ‰Šwƒ^S 6eÈ(cXa�ÂC’=³ĵÑãs‹Y2ZÎkÊ8*1=¤|.…a\XE##wvx žµŽ\mõgƒ{:²a°¢¡}d¯u€SñäŒ.Ú˜JN}ò‹Úa™*:€«ÔÖé�öÈyítž1° k!íäæeÙc/~­�ͪÂ^Áïw±émXO´«'Ø“åU˜´…̱ªZQ¹Žž Q‹ùAÜ ¹íÕ„d÷;†ß°gŸj�|»h*•½�g¢›@¯+*\7 Îr�k²ì½e2EÛ'Qœ¸5†FìR�ˆ\�Ü‹tCåºÖU4~4™AÃkáI¨ÒKf‚/»¢’ûÑŽHëÜØe‚ëšp^4»Ûó7¿¬�N\9µL8²/âÁ(>ÙÈîÃ6Ê%¿5�Z  塞ǦÀ�[ªÒ²säJFS|Ü-Ð rõQÍ2+úŸC]�YÝàEºx^�ÒÅoM@—hº¼¤ù„¾5]ÚVrÝËÂ…¾R`¸'# wÚþ\äÞ(ÿnÈ #a¯�»+=ź‹3å,[|iïJý55¬IõˆÈcY8‘×dÔ74o¸¦ÐÞŒqÇý^1 ÓÈ—„_¼ÿ{Ú7½p¶‰ �’Dÿxœ½WmoÛ6þ®_qó€¡ì Ú¢ÈÙÖm·X³Ã0´t²¸H¤JRqÝ_¿#©7+²�ŀߞ{î¹ã}óÓ»¯ÿzÿrSqµ?ÈRú)Ñ0Hr¦4š‹Ym²ÅëYHó†›ã’kÍÅ>ÔeÉÔÞŠLª’.Eú-A6P+™n-ðiœ2Ã̶B}hk1¬Ä‹Y³óDó/8‹£�Y¬³x8M'Îh¶‚¤`Z_Ìò—�iڛȴãzÑS±þ…ö. ZÅþð*†gnñʯ=�Bw2 «¢¦¼¼yò¼ÛþY:ÝžµÃïr, ^}ˆý-hhî1ùÛ�)¹Ñ;è‰,êR豉ÞéÑqÿgL~¡~GS+&G°»AfÀºôaJ±íIà7§ü®å_êÅË™?ï¹�ƒï &£ÊÔd…<„;LŒT°á&ï8ðLÔå •%ƒ–(Œ~î,É�„º¶>HK»¶´l Ö‹�ò~¢^’Š¥ž(˜n~²bî½ï.À䯰ì40Gjg Ëî¢ ‚EÞ<õi8Gn±UMLqh/Æñ»¹O`ŠB1%°�ÿ {˜£_Kà ¸_�¾šÈ–Ç£D2 •àéßâöW¦ó ‘›•Gt~:1”·xL÷?À‡ZhŠ÷ŠiÜÓ_P /Y�ØU×LžÐ!µ†×Ñî÷êÅbÅÍ€€¯ä‡ÂiË-kìÖ\˜W/ZXM¥‘ÙBùæé¹×–|ÛÌÇ€Ô<š‰9P9÷̧͋X¡Héâ à”\®L²B <³‘#½Ówü^…ý ‰0w}È“¥á3IÞÐ H†J*ã2»$Düò>MFõ$ËPYO2%Ë>DÔ%YQû‹�Dozü’S5=H)ozÉ–­H;ÖFÙDŸ| çÞó-rÝä—ëå÷â\;û#Kت'³Œ> €H€ö}óóòÏÅ)A9 V¬¥¢X’hc�²ÖÆ6èé$¡®™‚>eþÐ6pdNo…aŸíË a´JT»èù›ÖÈ(Íwõ/µh8ª›œ'ƒnß{LØ ?Õ\a:|)�5Ì Ô¢à·W—׿]þ0÷dxYù$v-�JR“ÂLê‘{b вK–ä(¥�{2 &bóiÚ^$ÞÆ&o §.~f‰qiÛ(ìrÍ& ™¡·�áIMJ‘|î�‘(©õ 37RÝ¢Ò.Ûß%†ÝYmQ».8L/J8)ÖñD—$TÔÂf* @R%R˜]Ìt]µ~«íMV‹Ä8 ûùømW¯;×¥âcŽ»[8ÜÇ1@ÊÝ :íÏôãYÜÿïû۵„Íghè¿sÿÛ‚ Í ÿ `äÿ…a�‹F¹xœ�UÁŽÓ@ ½ç+L¸€ÔmÄJHhIsY8p@ è…ã$q;£Mfª§Ùþ=öL’-Ý.H•&‰ýìg?�[¾úôí~ûëûgÐÔwUVΪ–�IA£•H›| Ý͇¼àïd¨Ãj4¤ÉuðI»6”Eúœ•ů]{’`ïªV‘¢ÓꆺÓed5ª“x'ªØÎm“½” zÆÔÒûÔF2”æ^8?¯È8;28rçWâAøÈ’iEÀ�k¹ú %rû!PJf�µ¸ç€GÌ®'€Ð¨Nù�ì\K7íÐ×è¥<ëTÓà�K\‰l­‘•ÉĤ£D×5 ¥Áù0ö¬%ÒáiÞ\¨u‚”bA«nµ½êø3Á^Ø…FD+†lÖ:<` ;éEp© l5~Ò�ÕSV8»#úŽC(Û2QQR,™d�MƒçºòÎÈ]ë‚ã[XLŸ2n¯íq·É…ױǵìž×Ëk^-�²Vp Š“ûН Óâˆd ¾|Wv WñŶL:\Y3OÆÿØ4æñ|Óœ‡Z–Í|ݧ›Ì>g“ýlËü@|T.™5‰kؤûÅÝ_CÛ?â6ŽX­šØ{7XÚú£ÆÍ‡¤WÔhÑ\c¿"q¦œoyÌél—Œ2àgCÆ×DGÒ„Z@i1eqýhì– 0‰WL&Eú‡ú ØaK„ �ˆDfxœ�TMoÔ0½çW éµ] ªJy#¡…CD{ᄜd²±êØ+ϤKþ=cçc)ª8 íʞɼ7Ïž—è7Ÿ¾|û =®*ôº ie� 4½‰„¼/GînÞ—JòlÙau¶ÜspZÍa¡Õ«C;%’·UkØðtBú[µ¤ mÀ›÷å’-+­L‚¼»°Ê¾Ð'hœ!Ú—ýt$¿ YÊ›ÐâŸôº¾ kaËϵ:%Ž ÆššhO ÖslÓÙj‡PO9  7=¶Y¨õ©&8ŒÆ7¸+f¢Ö>¯rº¹+gî�0²u–§ùpo6œ„Ρ{ÑõÜÂ¥qgœ+^i›0 ?Wzq]ˆ;€{üeIøklÌ(L&uÉ•X‚h"º ž|8{!Ì4éâ`°M”ÎMðí5PH¬Ö-�Âjà9Œ®-`¤Q´MËiÀ‡$ñ¸^‡œ]Çà�ÕŠGAæ –T Sî#vû2é’#q—üuµ…eµmÓü¯áoP¾¬ (‡3(ogÓd1J†3è<£gZÔßVåâ�ŸïCK™¸¬¾ÌQ"Ìmòw"Ok¬4·‹ÙþÉrµ$w›¡77š‹—ü ãeÔœ´št7W*iÿ_Ä ÷¾µ RY]ö¯)ù޳KúW´¹…R çnÍ[¨©”¦yy ë�°¹·ëà ¬‹|d£Èáu�p^XGQpõPWÃÚ›õ*_¯¶ßöànMj�v'Ôeôkzä'§¶Zúó§Ñç‚ý·ÈÃI�rø`QxÚ b¿¹¶p¥•[™E¹Jî’ïwñvU£ß‡Îmûâ6�28ã@dƒàPa饾ß�‡fïÀÔ�Ã^ a߇á™T-ãÁX5?ƒ9'@è œ·²ôê ¬ iûu pGG¦Õ•ƒ�`Ѭ ›}�Œ®Ó�ÝFk…—F§ W EȰ��žB ç(J²'(�KÒL6/>ê:©±‡qu´H~‚6Ñ!Ž2X#—ßqjž‚‡ÚxÚ÷MF¾‚Ôµ¤ÒÓ!´O!Èi$‘&¤Ó€#�=y�d‘Òº@R°(t_þÿ³ÖËìÜh²S޼’e“R_ 2Ñan�¤z�òöÊlQ[yßøWÉÆk¬°­òoH£TmEÝÁ«ÁIŠ“?± ° °$r–ƒ\+uZÕ q¢¹R3ý®FÉiqÏ¡ ”f‡6C­¯¨'©ÇÈYaa‹VšŠ$´TzJÚÔ$ÃeîÜuH‚•KéØHÝÒ öi½1­=^­¨`G‹{ć9ËäYs,ý¹¥¢à`{˜§WO(ÆgJÍ�@Pp-MKr&¶ŠOÑÈŽ†Mëc$l]Š÷ž'*nW»yÀíp•t~Ž5H謇`EuÒ ÙֽܡŽ~¥Z>¦›ùL*0ªz\Tº&{ò?�Z@9C·óª[ʨ×4ë›ÅžFÝuÉÄðûîa‘w?˜”-a,Þß]Q¡\äýÏÈä‚+ºg‰_#Ü-Åã߇„Ͳ¿©È,<ËDê1948� {‚büÙ ãàд?X÷¹5ï\*4ßÙÈ6©ŠQÄ^Èpå�®ùgÈÈÉYtFíð\’ŽÅ¾,Qöjœ©9«GÙêæßŸâgŸÐ·V�ž{ŠÛt’±”If î úit&†Ÿ§r·"]¶4$¥ÙtWe¾$jiÉf<4aL2ü‰ždÄB|Ù gFn3þÌÀF´9 0&4NdkfëÀ5&êÉžœœ¬}Â�ÙŠ§Í—Öùï·2“â5ròXªécWébW 'Ÿê¢¸Œ™î|G P×ç™XÑæ‚ó/ubÌ‘wcf¯6ža÷ã@#Ç Ü%Pˆò�á›Ï &úÖ˜± „ÜÍÝþ¢­í[‚¢»¸Õ¡=é66:ò^GWáe6MùàgÊzGqês6!…‰Rg¡••ÙwmÇOí–î9¾ζýQW²Ä0Óo¼ÈÇ ºŒŒý÷NÚâ6Ä~wÍŒî¨ñ}Ì=ß=w›C¯�uáJÊ%ƒ»±Úe÷V2~¹ ox¿ë9ˆ3—a†ʶ9¨Èªû#eÿŸùº¡ÿ2…� Š<£xœ­UQkÛ0~÷¯¸¹°§&f+…±9†­ëÃÆÆZ({gë )“ÎÉòïw²l×a…Â($HwºûôÝé“\¾úüíæþç÷[èxgª¬œB%ÃŽ¡éÐâMÞs»z—âg͆*Ö;òh·TÉ‘•ŘX;uŠ0o*…Œ|ÚSx‹xYÈJ‹;Úä³?¯ÊcÚÛ%¶XY¹‡Æ`›¼»†6Èo%&KBã-7)ëen-ˆCDYì#Jw}¯÷ Ú²X1Ö† >‰9$‚kXg)QéÃD`V×yÂú=k£ù”Ö€;dd¨áhxw k¹Ë±s�:´hŒðÈm¤Cþ€f p/©“ :@‡¦]¹=ÙHQQ‹½áK‰hL¯´Ý[E@МA`ô²½0A«€þ,ƒœ�1@VÅ€K.qŠ`P«¸ì²aLµaÓÅZ 6'Áihj�ô¡ ì�ÝVwD€&89…bterÎ�§v“5wìÌ:jìb4òjœÄ“¿„eøÜ²”0›y5O“\…Q’Î�“ÖY£rº«ê r¨õ—tN…:¯¾&+ÂIÖUTx„�£¯JV£ÌžÁ¹˜Ýë…¤jÄG5FLùßxB¦Q£ö¤Ðu +„Ãé0ܤŸ\/‡žWçöS”:A"Ÿô1^ûð»wÔC ' ˆ­6"RñQsÔØBç/AÞSpæ@güë{ªˆĽ·�5@T™H?œ1Ÿ*’CP€[Ô60”ôsYÄñ%ª˜nr^M³gÏWÿ‘¾¢(‰šB¼öÓÓ’‘ñxÀeAÞÀi¼;~±J7>ÏŸ¡½wATàlˆ‡.Jê}¤˜z¹(g|#ϨÓ•+Æ�G‘¾E­NL  Ó 8¹‡�Ž0 ßxœ¥VÛŠÛH}×WÔ:6 ‘@`Éha²�„ ÙyÉcK*[M¤nmwÙŽaÉ·§ú"»åñ…$`°Ô]]—S§N«üãÝ?�_>ý  }••ÓŠ–ÿ$M'ŒEº_¬iyó×¢àu’Ôc…T´”=¡��H�nmY„­¬,¢‹Z·;çðeÕ ´Ѿ�“'Ù$+(1àý"±¸MžUYçîUuÆ‚ý¼bƒš^X{¿è^ÃÒò±F·³Œß@¹¦ÂeY¸¸‡²®f~ê þô6DÁæEYx/e1>‰À¿~¥ß‹äW?ˆ{{2d!0„@ Z¹™²ìÍëE82:N"íÁé:¡ñÁ,ˆ¨ƒØA�l�$Bb%¤²Ä›|\Šºç3zéB–äÖ@‰¦ÃH»½ H:‚¹­Yüœ VÌ ¸•ÔÅüÙ8&–\ÛçÆþ·Öw ·Êû³·ÙÜ kN®›µy>9X©ôNû,4z@X=d i÷'�<ÛÇ'�f¦Ä™Ø¤.h„1;©VœõnBs%yzï8÷àè5�H‘tØŠ¯i1®Ž[€÷ÒÅÝvšÍ­ZIRsûzN©Ýq˜6…ØZPÚ!0r/-÷y†ÈÛ‰ì{m¹·ÂÅ0z v]ÛÆÈ‘rç,öØP¨�·Ãƒ/›kÌöEë ³àaJÏ÷¢– S*<Ãx7!꜇åÿãÒ]¾O6ÒÕDœÒ8jC®ãGäøá°Ž•÷8¸½-wÙÊ”–ŒV«ê_dõVóŒq)`ë .£Šù¾Ü:a}vx_T‡g'i9¤ç\;�P«xlÿº¨ö�§%±ö¯áÐ>R,¢`­8£·£Ñ#’hÏÈíÁà¤Ú^×BOÌTS�5\žÁÏHk£Š‹„ÏAžŽY{Y!;ä0x]ÃÜ{¤L#¢HäÙ5ÁbFïem8ÆY÷’牣„‰ÎüÌ3LC…ùÄɹŒ0ùLÏY2Ü캤†0æ×›Ë³÷ž¡jÎ6÷`ð‹Í•ßÒΦîžt6‡Kì¬Ñ£¶QbW¼ˆðó\ì“þDŹÐù 0ùg×Ás0 ~&zò9½ýJþœñö2FñÞ¹:ѱ¿kÑ|âiÓúÏ‚ù�ð Óz�ßß;üµà®9G#Q± ÐŽÆ–mÜÀ)3kw ?ß,<\Ž»”-Ôz­Z{Äø"~ná;öE²³†{��z¢xœ­VM�ÛF ½ëW° °H¯…6P¬eÅ6‡-Z${é© $ÊDšQgèUüïËù�e!A÷�»0çƒä›G>Úå¿þùøô÷_ï¡çq¨²rù låc$Fhz´Žø˜Ÿ¹»ÿ9/dŸTÑ3iîÔÀdË"nee‘\kÓ^| «ù2‘{€�‡e%‚Æ‘ŽùÍI^•zן¶d�•4:wÌûwÐ9ù»—%‹KcZºMUÖ[ïZ¢†;e1ù8|<×®±jbPš °òï­‚ús� 3:èqšH+}‹Ü“îQǤ÷Y תçØèîßå1Ã/pf5(¾Ä3ïÉPŸÕÐ:I×Ý*VFžPiÇrNðŒVy Lç7²HD`ÈŒMO-lïu ŽjØ…0ÖÌÑ‘›^Ч8NÒ>“Ý<Éá aRÍ'Iyf(iŒÌ¹²ó°†ëÌ0˜9ƒÎšÑïŽË놪d+ÿmu§k7¤#Ú°,'K 7ôÇi ¼êàxÛðšùÍ!æ×Ôíßûƒß±¦ÁÁñwÿž ¬�­<¼ñ©­¯¦OQøÔE„‘Eêuâ-<7Ráér½"̬Áè­¯†§�ÍäÀõƲç^®gº�ÜâoV܃&Za }òƆ+r3%4z¸È¹'E“@Ÿ±aÙY{,R„ž×à!5•ž��Ìèµv)5Zé†Xd¹mtC’!ñ‡ÚÍ>ïÜ«¦õÚ *†^˜Ž×Vr¢XžBÒDa$)¤º³Nˆ>¬Pc¬Ô ËéœÉR0)š�w'>@»„ý¢#cO/­õ@'y. D+Á¤ý+q÷·€[£OÕG Έ¼‹´•Èé-uiˆ„.Øû9öj]çÕjû鲃[?ÿB‹ú”ܮ˼ºš_:ùF`3D—´È«d|=Ç ´ë2æ¸Ko.d Äøh4G1†ù׿­¶Ï�åÿGBô¦u!x^ýW> ø½½J4[4‹öb¤W7û͈ÞÌV\gký£%dZU“B¬jý&$“5YVäòjµ¿†ãñÙêÐs±ÅoÇîˆé>˜­b¡x™Âß ¤ôûo’©ñ Wû�“qÛU)‹j‚0˜£2ÂIÒ}’Ø÷Bl¯d—jìPÇÁºÕöÿas�¶˜—é-ßÔñ—CŠü*‘ü~…�Š2£xœ�UmkÜ0 þž_¡e06È]X¡0º\`tìÃ^èöe�Dé™9v°umÆ~ûä—¤i›ncp‡eKz$=–•êÉÛÏçß¾y{T�UÓ‚¢ãe@Ðî…uH»ü@ýæU^ò9IRX[sÝKEhá#ÒÞt®*£"«ÊИîèá^Ö� AÇݬø±AV ÐbÀ]>ë·³”×U)<ÐI½¢eÿVŽÐ*áÜ.ߟBïØ¥5Ý"Ë3¨®„­m_•~…TM½Àhjx,¨QÑäEUˆªÀóoÃ[úÿ0üÿÄõºÕX5œ[„ <]�*΢¶“WS.ƒÛœæÑ!æq?ø-Ù�mä8R2¤½ ÄÌàÚJ"Ô .…ÔŽX‰ÀîR4Š}LwQ †!¯c½(} ñv›Áû»'É׿O\Š—RÀŒ2¦€ƒîBJš#ó'ÇŽµ“FO¼ü3 ëÔÏL�Ëz³º[”}-¹o4×J†…¡¤zk†œ‰äw·oóRô¬Ù¨Ú»s§ +_•I‘Uålظö`^Õ-2òeTô6÷å «,ê�¯ú¼®J fßm±Eʪމyÿ:’ß­ˆ,Gת­“ªÙÚ6‚oTåPjø45tôzdЖ 06F�óÀ:<7ÍEôGg[ÍÚY�Ÿf‚Gô:S‘%´V?.Q tû&Oî`bm4_Òp� ͤMKOQ'׉b˜œ%8{ͬlxBm‰W—1Qth[ eÔ‘® % dMÝEÛ“„Y<ô*ÛøÑ+.4ªs^]-QãÜgÕò �‹G$¾ %0ƒ/bÉC{™&�«Á¿²¯Õ{ù·õÎ64î¥QÚ(V£¸žs§¾à0•×à;8\»žsc^ì3�å¹ïŠ_”>õ »ïáÕÛ—°³½Øw';#Úï_ÞÀ»Á¿~Êà¿L±ds}fxI>÷’��ŸQ t¨Oê§MEù&T.VSŒB/¸sÈ&éVR�Läý¡Wƒ¤ =åÑž}¬`::kîÙ™ÕàÜ+ÉÉû»‡ßî~Øyúkr{Š1-dƒM¡]ñÑ�Þ†'Eÿ`Í%#…úZÝuÊ '¬“‡J±‚i 4†Â+”éžB{}®„i�<±ÇGí&�3&´LçÝÚ,[ U{gOõ'¥ 9¡`9«2!yïUwÈ×LaÄ<[Å|›#×[£9GÉdòkæþy=–yÆ[ÁËW..T¿³,Û4‰â#K!zšJ÷Î �˜æ¡Ô¿®Wȵeþ€Þµ¡óú}’œX½^‰‘-ÌHøœg«ºØLËM›âuÐÍ»÷ YJ5·|qåÄÿqÿ«Œ–£¢èÞ-€�Š'ocSŽŽâ0¢DºyüȾÚÔÂïª ›x4³ssOB^x,s=}_Êô¹úJéM² =ŽùÞ­rGõУrA Ï ž { Á | 1 â “ . é º � J Ä o ÃX г€Øi*Ë€S Á‚Y< ¦a"ÙŒAâ£za ÉŽ9= E;string_Query_Operations.html=string Query Operations?< KAstring_Summary_Information.html OEstring_Relational_Operations.html>string Relational Operations  Û˪GÕ¦a ë ¸ › : õ ¶ m P #  Ñ ´ ‡ f 5  Û2� =3eventfilter_methods.html“eventfilter Methods"� -#eventfilter.html’eventfilter.� 9/rowfilter_methods.html‘rowfilter Methods� )rowfilter.html�rowfilter*� 5+withtol_methods.html�withtol Methods� %withtol.htmlŽwithtol.�  9/timerange_methods.html�timerange Methods�  )timerange.htmlŒtimerange*�  5+vartype_methods.html‹vartype Methods�  %vartype.htmlŠvartypeF�  QGmissing_Relational_Operations.html‰missing Relational Operations<� G=missing_Query_Operations.htmlˆmissing Query OperationsB� MCmissing_Summary_Information.html‡missing Summary Information^� i_missing_Create_and_convert__missing__type.html†missing Create and convert 'missing' type� %missing.html…missing0� ;1tabular_properties.html„tabular properties� %tabular.htmlƒtabularV� aWdictionary_Indexing_and_concatenation.html‚dictionary Indexing and concatenationB� MCdictionary_Compare_and_hash.html�dictionary Compare and hash,� 7-dictionary_Files.html€dictionary Files/ ;1dictionary_Entries.htmldictionary Entries?~ KAdictionary_Keys_and_values.html~dictionary Keys and valuesa} mcdictionary_Create_and_describe__dictionary_.html}dictionary Create and describe 'dictionary'| +!dictionary.html|dictionary3{ ?5tsdata.event_methods.html{tsdata.event Methodssime' typeBdatetime_Summary_Information.html&Summary Informationldatetime_Convert_to_other_Numeric_Representations.htmlPConvert to other Numeric Representations<datetime_Query_Operations.html Query Operations\datetime_Sort__Filter__and_Set_Operations.html@Sort, Filter, and Set OperationsHdatetime_Descriptive_Statistics.html,Descriptive Statistics*datetime_Binning.htmlBinningFdatetime_Arithmetic_Operations.html*Arithmetic OperationsFdatetime_Relational_Operations.html*Relational Operations<datetime_Array_Operations.html Array Operations&datetime_Other.html Other(duration_arrays.htmlduration arraysduration.htmlduration0duration_properties.htmlProperties`duration_Create_and_convert__duration__type.htmlDCreate and convert 'duration' typeBduration_Summary_Information.html&Summary Information<duration_Query_Operations.html Query OperationsFduration_Relational_Operations.html*Relational OperationsJduration_Mathematical_Operations.html.Mathematical OperationsHduration_Statistical_Operations.html,Statistical Operations\duration_Sort__Filter__and_Set_Operations.html@Sort, Filter, and Set Operations<duration_Array_Operations.html Array Operations&duration_Other.html Other$string_arrays.htmlstring arraysstring.html stringTstring_Create_String_and_Convert_Type.html<Create String and Convert Type>string_Summary_Information.html&Summary Information8string_Query_Operations.html Query OperationsBstring_Relational_Operations.html*Relational Operations:string_String_Operations.html"String Operations8string_Array_Operations.html Array Operations"string_Other.html Other"table_arrays.htmltable arraystable.html table*table_properties.htmlPropertiesPtable_Create_Table_and_Convert_Type.html:Create Table and Convert Type0table_Save_to_Files.htmlSave to Files<table_Summary_Information.html&Summary InformationLtable_Sort__Filter__and_Rearrange.html6Sort, Filter, and RearrangeDttable_Join_and_Set_Operations.html.Join and Set Operations2table_Missing_Values.htmlMissing ValuesXtable_Apply_Functions_to_Table_Contents.htmlBApply Functions to Table Contents8table_Auxiliary_Methods.html"Auxiliary Methods*timetable_arrays.html timetable arraystimetable.htmltimetable2timetable_properties.htmlProperties>timetable_Create_timetable.html Create timetableNtimetable_Concatenation_Operations.html0Concatenation OperationsPtimetable_Size_and_Shape_Operations.html2Size and Shape OperationsDtimetable_Regularity_and_Type.html&Regularity and Type2timetable_Comparison.htmlComparison6timetable_Missing_Data.htmlMissing DataDtimetable_Variable_Management.html&Variable ManagementDtimetable_First_and_Last_Rows.html&First and Last RowsHtimetable_Row_Ordering_and_Sets.html*Row Ordering and Sets>timetable_Range_Predicates.html Range Predicateshtimetable_Apply_Functions_to_Timetable_Contents.htmlJApply Functions to Timetable Contents.timetable_File_I_O.htmlFile I/Oeventtable.htmleventtable4eventtable_properties.htmlProperties.eventtable_methods.htmlMethods.timeseries_objects.html$timeseries objectstimeseries.htmltimeseries4timeseries_properties.htmlProperties`timeseries_Create_and_describe__timeseries_.html@Create and describe 'timeseries'Ttimeseries_Data_and_sample_operations.html4Data and sample operationsFtimeseries_Time_representation.html&Time representation,timeseries_Events.html Eventsptimeseries_Interpolation__resampling__synchronising.htmlPInterpolation, resampling, synchronising4timeseries_Statistics.htmlStatisticsRtimeseries_Arithmetic_and_comparison.html2Arithmetic and comparisonPtimeseries_Filtering_and_detrending.html0Filtering and detrending"tscollection.htmltscollection8tscollection_properties.htmlPropertieshtscollection_Create_and_describe__tscollection_.htmlDCreate and describe 'tscollection'2tscollection_Members.htmlMembersDtscollection_Samples_and_time.html Samples and timeXtscollection_Indexing_and_concatenation.html4Indexing and concatenation0tsdata.timemetadata.html&tsdata.timemetadataFtsdata.timemetadata_properties.htmlProperties@tsdata.timemetadata_methods.htmlMethods0tsdata.datametadata.html&tsdata.datametadataFtsdata.datametadata_properties.htmlProperties@tsdata.datametadata_methods.htmlMethods0tsdata.qualmetadata.html&tsdata.qualmetadataFtsdata.qualmetadata_properties.htmlProperties@tsdata.qualmetadata_methods.htmlMethods2tsdata.interpolation.html(tsdata.interpolationHtsdata.interpolation_properties.htmlPropertiesBtsdata.interpolation_methods.htmlMethods"tsdata.event.htmltsdata.event8tsdata.event_properties.htmlProperties2tsdata.event_methods.htmlMethods.dictionary_objects.html$dictionary objectsdictionary.htmldictionary`dictionary_Create_and_describe__dictionary_.html@Create and describe 'dictionary'>dictionary_Keys_and_values.htmlKeys and values.dictionary_Entries.htmlEntries*dictionary_Files.html Files@dictionary_Compare_and_hash.html Compare and hashTdictionary_Indexing_and_concatenation.html4Indexing and concatenation4supplementary_classes.html*supplementary classestabular.htmltabular.tabular_properties.htmlPropertiesmissing.htmlmissing\missing_Create_and_convert__missing__type.htmlBCreate and convert 'missing' type@missing_Summary_Information.html&Summary Information:missing_Query_Operations.html Query OperationsDmissing_Relational_Operations.html*Relational Operationsvartype.htmlvartype(vartype_methods.htmlMethodstimerange.htmltimerange,timerange_methods.htmlMethodswithtol.htmlwithtol(withtol_methods.htmlMethodsrowfilter.htmlrowfilter,rowfilter_methods.htmlMethods eventfilter.htmleventfilter0eventfilter_methods.htmlMethods8supplementary_functions.html.supplementary functions.compiled_functions.html$compiled functions fÜĪŽnT&ôäÒ¾ª’zX:" î º š | ` D & ì Ì ª Š r J &  Þ ¾ œ v D  ò Ì ® Š l N .  ì Æ ¨ Š r X & èÆ¤ŠfBúÔ®~N.ö¾Žd6ܰ„Z,þФt> ܲ†Z*ôÂ’b0þ̘d,*f/ /categorical.isrowcategorical.isrow6e; ;categorical.isprotectedcategorical.isprotected2d7 7categorical.isordinalcategorical.isordinal2c7 7categorical.ismissingcategorical.ismissing0b5 5categorical.ismembercategorical.ismember0a5 5categorical.ismatrixcategorical.ismatrix0`5 5categorical.isequalncategorical.isequaln._3 3categorical.isequalcategorical.isequal.^3 3categorical.isemptycategorical.isempty0]5 5categorical.iscolumncategorical.iscolumn4\9 9categorical.iscategorycategorical.iscategory.[3 3categorical.keyHashcategorical.keyHash*Z/ /categorical.numelcategorical.numel*Y/ /categorical.ndimscategorical.ndims(X- -categorical.sizecategorical.size,W1 1categorical.lengthcategorical.length2V7 7categorical.countcatscategorical.countcats4U9 9categorical.categoriescategorical.categories.T3 3categorical.summarycategorical.summary*S/ /categorical.uint8categorical.uint8,R1 1categorical.uint16categorical.uint16,Q1 1categorical.uint32categorical.uint32,P1 1categorical.uint64categorical.uint64(O- -categorical.int8categorical.int8*N/ /categorical.int16categorical.int16*M/ /categorical.int32categorical.int32*L/ /categorical.int64categorical.int64,K1 1categorical.singlecategorical.single,J1 1categorical.doublecategorical.double(I- -categorical.charcategorical.char.H3 3categorical.cellstrcategorical.cellstr6G; ;categorical.dispstringscategorical.dispstrings6F; ;categorical.categoricalcategorical.categoricalE# #categoricalcategorical.D3 3__dictionary2json____dictionary2json__.C3 3__json2dictionary____json2dictionary__$B) )__table2xlsx____table2xlsx__$A) )__xlsx2table____xlsx2table__"@' '__table2ods____table2ods__"?' '__ods2table____ods2table__">' '__table2csv____table2csv__"=' '__csv2table____csv2table__< __ldml____ldml__ ;% %__datetime____datetime__ :% %__nkeyHash____nkeyHash__ 9% %__ckeyHash____ckeyHash__8 istabular istabular075 5parsePairedArguments parsePairedArguments6 keyMatch keyMatch5 keyHash keyHash4! !histcounts histcounts3! !discretize discretize$2) )grouptransform grouptransform 1% %groupsummary groupsummary0# #groupfilter groupfilter/# #groupcounts groupcounts.! !splitapply splitapply-! !findgroups findgroups",' '@cell/display _cell_display+! !@cell/disp _cell_disp$*) )readdictionary readdictionary )% %isdictionary isdictionary.(3 3configureDictionary configureDictionary0'5 5timeseries2timetable timeseries2timetable$&) )istscollection istscollection %% %istimeseries istimeseries$# #istimetable istimetable"#' 'readtimetable readtimetable""' 'ods2timetable ods2timetable"!' 'csv2timetable csv2timetable& + +array2timetable array2timetable istableistable# #xlsx2structxlsx2struct % %struct2tablestruct2table# #struct2xlsxstruct2xlsx! !struct2odsstruct2ods readtablereadtable! !ods2structods2struct ods2tableods2table csv2tablecsv2table! !cell2tablecell2table# #array2tablearray2table27 7convertStringsToCharsconvertStringsToChars27 7convertCharsToStringsconvertCharsToStrings stringsstrings! !isdurationisduration % %millisecondsmilliseconds secondsseconds minutesminutes  hourshours  yearsyears  daysdays  NaTNaT  isnatisnat! !isdatetimeisdatetime,1 1iscalendardurationiscalendarduration calyearscalyears# #calquarterscalquarters calmonthscalmonths calweekscalweeks caldayscaldays"' 'iscategoricaliscategorical KØÎœb*øÈ”^( ð À ” n H " ü Ö ° ˆ ` 2  Ò ¨ ~ S í ¾ ‰ X ) ü Ó ¢ q B Þ­xGߨ{PʇLܧp5ø¹v9Í–c*ï²w<ÿ‹NØ4�19 9calendarDuration.isnancalendarDuration.isnan<�0A AcalendarDuration.ismissingcalendarDuration.ismissing:�/? ?calendarDuration.ismatrixcalendarDuration.ismatrix4�.9 9calendarDuration.isinfcalendarDuration.isinf:�-? ?calendarDuration.isfinitecalendarDuration.isfinite:�,? ?calendarDuration.isequalncalendarDuration.isequaln8�+= =calendarDuration.isequalcalendarDuration.isequal8�*= =calendarDuration.isemptycalendarDuration.isempty:�)? ?calendarDuration.iscolumncalendarDuration.iscolumn8�(= =calendarDuration.keyHashcalendarDuration.keyHash6�'; ;calendarDuration.lengthcalendarDuration.length0�&5 5calendarDuration.nnzcalendarDuration.nnz4�%9 9calendarDuration.numelcalendarDuration.numel4�$9 9calendarDuration.ndimscalendarDuration.ndims2�#7 7calendarDuration.sizecalendarDuration.size:�"? ?calendarDuration.calyearscalendarDuration.calyears@�!E EcalendarDuration.calquarterscalendarDuration.calquarters<� A AcalendarDuration.calmonthscalendarDuration.calmonths:�? ?calendarDuration.calweekscalendarDuration.calweeks8�= =calendarDuration.caldayscalendarDuration.caldays4�9 9calendarDuration.splitcalendarDuration.split2�7 7calendarDuration.timecalendarDuration.time8�= =calendarDuration.dateveccalendarDuration.datevec2�7 7calendarDuration.charcalendarDuration.char8�= =calendarDuration.cellstrcalendarDuration.cellstr@�E EcalendarDuration.dispstringscalendarDuration.dispstringsJ�O OcalendarDuration.calendarDurationcalendarDuration.calendarDuration6�; ;calendarDuration.FormatcalendarDuration.Format(�- -calendarDurationcalendarDuration*�/ /categorical.emptycategorical.empty4�9 9categorical.ctransposecategorical.ctranspose2�7 7categorical.transposecategorical.transpose0�5 5categorical.ipermutecategorical.ipermute.�3 3categorical.permutecategorical.permute2�7 7categorical.circshiftcategorical.circshift.�3 3categorical.reshapecategorical.reshape0� 5 5categorical.repelemscategorical.repelems.� 3 3categorical.repelemcategorical.repelem,� 1 1categorical.repmatcategorical.repmat.� 3 3categorical.vertcatcategorical.vertcat.� 3 3categorical.horzcatcategorical.horzcat&�+ +categorical.catcategorical.cat*�/ /categorical.unioncategorical.union,�1 1categorical.setxorcategorical.setxor.�3 3categorical.setdiffcategorical.setdiff2�7 7categorical.intersectcategorical.intersect,�1 1categorical.uniquecategorical.unique0�5 5categorical.topkrowscategorical.topkrows0�5 5categorical.sortrowscategorical.sortrows(�- -categorical.sortcategorical.sort(- -categorical.minkcategorical.mink(~- -categorical.maxkcategorical.maxk4}9 9categorical.histcountscategorical.histcounts(|- -categorical.modecategorical.mode,{1 1categorical.mediancategorical.median&z+ +categorical.maxcategorical.max&y+ +categorical.mincategorical.min$x) )categorical.necategorical.ne$w) )categorical.ltcategorical.lt$v) )categorical.lecategorical.le$u) )categorical.gtcategorical.gt$t) )categorical.gecategorical.ge$s) )categorical.eqcategorical.eq*r/ /categorical.timescategorical.times.q3 3categorical.setcatscategorical.setcats6p; ;categorical.reordercatscategorical.reordercats4o9 9categorical.renamecatscategorical.renamecats4n9 9categorical.removecatscategorical.removecats2m7 7categorical.mergecatscategorical.mergecats.l3 3categorical.addcatscategorical.addcats0k5 5categorical.isvectorcategorical.isvector6j; ;categorical.isundefinedcategorical.isundefined8i= =categorical.issortedrowscategorical.issortedrows0h5 5categorical.issortedcategorical.issorted0g5 5categorical.isscalarcategorical.isscalar S×Ç�Sߦq: Ê ™ h / ü Á † M  Õ š [ ã ¤ c ,  è » Ž [ 0 èÅ uN+Ý´�`3çÀ—l=羑f7×°�R#öÁ”i>ä½�a:æ¹”oJú× ‚% %datetime.max(datetime.max ‚% %datetime.min(datetime.min*‚/ /datetime.sortrows(datetime.sortrows"‚' 'datetime.sort(datetime.sort"‚' 'datetime.mink(datetime.mink"�' 'datetime.maxk(datetime.maxk*�~/ /datetime.isvector'datetime.isvector*�}/ /datetime.isscalar'datetime.isscalar$�|) )datetime.isrow'datetime.isrow$�{) )datetime.isnat'datetime.isnat,�z1 1datetime.ismissing'datetime.ismissing*�y/ /datetime.ismatrix'datetime.ismatrix$�x) )datetime.isinf'datetime.isinf*�w/ /datetime.isfinite'datetime.isfinite*�v/ /datetime.isequaln'datetime.isequaln(�u- -datetime.isequal'datetime.isequal(�t- -datetime.isempty'datetime.isempty*�s/ /datetime.iscolumn'datetime.iscolumn2�r7 7datetime.issortedrows'datetime.issortedrows*�q/ /datetime.issorted'datetime.issorted,�p1 1datetime.isbetween'datetime.isbetween,�o1 1datetime.isweekend'datetime.isweekend,�n1 1datetime.isregular'datetime.isregular$�m) )datetime.isdst'datetime.isdst,�l1 1datetime.convertTo&datetime.convertTo.�k3 3datetime.juliandate&datetime.juliandate,�j1 1datetime.exceltime&datetime.exceltime(�i- -datetime.datenum&datetime.datenum*�h/ /datetime.yyyymmdd&datetime.yyyymmdd&�g+ +datetime.string&datetime.string(�f- -datetime.datestr&datetime.datestr(�e- -datetime.datevec&datetime.datevec,�d1 1datetime.posixtime&datetime.posixtime(�c- -datetime.keyHash%datetime.keyHash&�b+ +datetime.length%datetime.length$�a) )datetime.numel%datetime.numel$�`) )datetime.ndims%datetime.ndims"�_' 'datetime.size%datetime.size*�^/ /datetime.tzoffset$datetime.tzoffset,�]1 1datetime.timeofday$datetime.timeofday"�\' 'datetime.week$datetime.week&�[+ +datetime.second$datetime.second&�Z+ +datetime.minute$datetime.minute"�Y' 'datetime.hour$datetime.hour �X% %datetime.day$datetime.day$�W) )datetime.month$datetime.month(�V- -datetime.quarter$datetime.quarter"�U' 'datetime.year$datetime.year �T% %datetime.hms$datetime.hms �S% %datetime.ymd$datetime.ymd"�R' 'datetime.char$datetime.char(�Q- -datetime.cellstr$datetime.cellstr0�P5 5datetime.dispstrings$datetime.dispstrings*�O/ /datetime.datetime$datetime.datetime*�N/ /datetime.TimeZone#datetime.TimeZone&�M+ +datetime.Format#datetime.Format�L datetime"datetime4�K9 9calendarDuration.empty!calendarDuration.empty>�JC CcalendarDuration.ctranspose calendarDuration.ctranspose<�IA AcalendarDuration.transpose calendarDuration.transpose:�H? ?calendarDuration.ipermute calendarDuration.ipermute8�G= =calendarDuration.permute calendarDuration.permute<�FA AcalendarDuration.circshift calendarDuration.circshift8�E= =calendarDuration.reshape calendarDuration.reshape:�D? ?calendarDuration.repelems calendarDuration.repelems8�C= =calendarDuration.repelem calendarDuration.repelem6�B; ;calendarDuration.repmat calendarDuration.repmat8�A= =calendarDuration.vertcat calendarDuration.vertcat8�@= =calendarDuration.horzcat calendarDuration.horzcat0�?5 5calendarDuration.cat calendarDuration.cat6�>; ;calendarDuration.uniquecalendarDuration.unique.�=3 3calendarDuration.necalendarDuration.ne.�<3 3calendarDuration.eqcalendarDuration.eq6�;; ;calendarDuration.mtimescalendarDuration.mtimes4�:9 9calendarDuration.timescalendarDuration.times4�99 9calendarDuration.upluscalendarDuration.uplus2�87 7calendarDuration.pluscalendarDuration.plus6�7; ;calendarDuration.uminuscalendarDuration.uminus4�69 9calendarDuration.minuscalendarDuration.minus:�5? ?calendarDuration.isvectorcalendarDuration.isvector:�4? ?calendarDuration.isscalarcalendarDuration.isscalar4�39 9calendarDuration.isrowcalendarDuration.isrow6�2; ;calendarDuration.isrealcalendarDuration.isreal ^ÈÓª{T)Ó¨ƒZ5 á ° ‰ \ 7  ë À • f E $  â Á   } R ' þ Ó ¦ { L ! ô Å ” m 4ý¾£zMïÊŸtQ*Þ³ˆS.པi: â·Š]6 Ü­†W0Ö¡tS2ðÏ®‹f?ïÈ$‚b) )duration.times5duration.times&‚a+ +duration.uminus5duration.uminus$‚`) )duration.minus5duration.minus$‚_) )duration.uplus5duration.uplus"‚^' 'duration.plus5duration.plus ‚]% %duration.abs5duration.abs‚\# #duration.ne4duration.ne‚[# #duration.lt4duration.lt‚Z# #duration.le4duration.le‚Y# #duration.gt4duration.gt‚X# #duration.ge4duration.ge‚W# #duration.eq4duration.eq*‚V/ /duration.isvector3duration.isvector2‚U7 7duration.issortedrows3duration.issortedrows*‚T/ /duration.issorted3duration.issorted*‚S/ /duration.isscalar3duration.isscalar$‚R) )duration.isrow3duration.isrow,‚Q1 1duration.isregular3duration.isregular$‚P) )duration.isnan3duration.isnan,‚O1 1duration.ismissing3duration.ismissing*‚N/ /duration.ismember3duration.ismember*‚M/ /duration.ismatrix3duration.ismatrix$‚L) )duration.isinf3duration.isinf*‚K/ /duration.isfinite3duration.isfinite*‚J/ /duration.isequaln3duration.isequaln(‚I- -duration.isequal3duration.isequal(‚H- -duration.isempty3duration.isempty*‚G/ /duration.iscolumn3duration.iscolumn,‚F1 1duration.isbetween3duration.isbetween(‚E- -duration.keyHash2duration.keyHash&‚D+ +duration.length2duration.length ‚C% %duration.nnz2duration.nnz$‚B) )duration.numel2duration.numel$‚A) )duration.ndims2duration.ndims"‚@' 'duration.size2duration.size2‚?7 7duration.milliseconds1duration.milliseconds(‚>- -duration.seconds1duration.seconds(‚=- -duration.minutes1duration.minutes$‚<) )duration.hours1duration.hours"‚;' 'duration.days1duration.days$‚:) )duration.years1duration.years ‚9% %duration.hms1duration.hms(‚8- -duration.datenum1duration.datenum(‚7- -duration.datevec1duration.datevec"‚6' 'duration.char1duration.char(‚5- -duration.cellstr1duration.cellstr0‚45 5duration.dispstrings1duration.dispstrings*‚3/ /duration.duration1duration.duration&‚2+ +duration.Format0duration.Format‚1 duration/duration<‚0A Adatetime.setDefaultFormats.datetime.setDefaultFormats4‚/9 9datetime.zoneNameStyle.datetime.zoneNameStyle6‚.; ;datetime.SystemTimeZone.datetime.SystemTimeZone$‚-) )datetime.empty.datetime.empty.‚,3 3datetime.ctranspose-datetime.ctranspose,‚+1 1datetime.transpose-datetime.transpose*‚*/ /datetime.ipermute-datetime.ipermute(‚)- -datetime.permute-datetime.permute,‚(1 1datetime.circshift-datetime.circshift(‚'- -datetime.reshape-datetime.reshape*‚&/ /datetime.repelems-datetime.repelems(‚%- -datetime.repelem-datetime.repelem&‚$+ +datetime.repmat-datetime.repmat(‚#- -datetime.vertcat-datetime.vertcat(‚"- -datetime.horzcat-datetime.horzcat ‚!% %datetime.cat-datetime.cat‚ # #datetime.ne,datetime.ne‚# #datetime.eq,datetime.eq‚# #datetime.ge,datetime.ge‚# #datetime.gt,datetime.gt‚# #datetime.le,datetime.le‚# #datetime.lt,datetime.lt,‚1 1datetime.dateshift+datetime.dateshift(‚- -datetime.between+datetime.between(‚- -datetime.caldiff+datetime.caldiff"‚' 'datetime.diff+datetime.diff$‚) )datetime.minus+datetime.minus"‚' 'datetime.plus+datetime.plus*‚/ /datetime.linspace+datetime.linspace$‚) )datetime.colon+datetime.colon.‚3 3datetime.histcounts*datetime.histcounts.‚3 3datetime.discretize*datetime.discretize ‚% %datetime.std)datetime.std"‚' 'datetime.mode)datetime.mode&‚+ +datetime.median)datetime.median"‚ ' 'datetime.mean)datetime.mean(‚ - -datetime.interp1(datetime.interp1*‚ / /datetime.ismember(datetime.ismember&‚ + +datetime.setxor(datetime.setxor(‚ - -datetime.setdiff(datetime.setdiff$‚) )datetime.union(datetime.union,‚1 1datetime.intersect(datetime.intersect&‚+ +datetime.unique(datetime.unique*‚/ /datetime.topkrows(datetime.topkrows bâ׬‰f9 é¾—jE" ù Ö ­ Š a :  î É   w P - Ý ¸ “ j E  í Æ ¡ t Q ï Ê ¥ € S &ýÒ£xO(Ú¯†[.Ô©|MõÞ¹ŠcB!üÛ¸•pE"ûÒ©|U,ܳˆe<â¹�sV9ÿâƒD string.ne>string.neƒC string.lt>string.ltƒB string.le>string.leƒA string.gt>string.gtƒ@ string.ge>string.geƒ? string.eq>string.eq&ƒ>+ +string.isvector=string.isvector&ƒ=+ +string.isstring=string.isstring.ƒ<3 3string.issortedrows=string.issortedrows&ƒ;+ +string.issorted=string.issorted&ƒ:+ +string.isscalar=string.isscalar ƒ9% %string.isrow=string.isrow(ƒ8- -string.ismissing=string.ismissing&ƒ7+ +string.ismember=string.ismember&ƒ6+ +string.ismatrix=string.ismatrix$ƒ5) )string.isempty=string.isempty&ƒ4+ +string.iscolumn=string.iscolumn$ƒ3) )string.matches=string.matches*ƒ2/ /string.startsWith=string.startsWith&ƒ1+ +string.endsWith=string.endsWith&ƒ0+ +string.contains=string.contains$ƒ/) )string.keyHashî˪‹lE"ó̧|U*Ø«‚a<êǨV5 åÀ�rI ó*„#/ /table.fillmissingItable.fillmissing&„"+ +table.rmmissingItable.rmmissing&„!+ +table.ismissingItable.ismissing(„ - -table.anymissingItable.anymissing „% %table.setxorHtable.setxor"„' 'table.setdiffHtable.setdiff$„) )table.ismemberHtable.ismember&„+ +table.intersectHtable.intersect„# #table.unionHtable.union&„+ +table.outerjoinHtable.outerjoin&„+ +table.innerjoinHtable.innerjoin„! !table.joinHtable.join „% %table.rmpropGtable.rmprop"„' 'table.addpropGtable.addprop*„/ /table.inner2outerGtable.inner2outer"„' 'table.unstackGtable.unstack„# #table.stackGtable.stack&„+ +table.rows2varsGtable.rows2vars*„/ /table.convertvarsGtable.convertvars&„+ +table.mergevarsGtable.mergevars&„+ +table.splitvarsGtable.splitvars(„- -table.removevarsGtable.removevars$„ ) )table.movevarsGtable.movevars(„ - -table.renamevarsGtable.renamevars"„ ' 'table.addvarsGtable.addvars$„ ) )table.topkrowsGtable.topkrows,„ 1 1table.issortedrowsGtable.issortedrows „% %table.uniqueGtable.unique$„) )table.sortrowsGtable.sortrows„! !table.tailFtable.tail„! !table.headFtable.head„# #table.widthFtable.width „% %table.heightFtable.height"„' 'table.summaryFtable.summary(„- -table.writetableEtable.writetable&„+ +table.table2odsEtable.table2ods&ƒ+ +table.table2csvEtable.table2csv2ƒ~7 7table.table2timetableDtable.table2timetable,ƒ}1 1table.table2structDtable.table2struct(ƒ|- -table.table2cellDtable.table2cell*ƒ{/ /table.table2arrayDtable.table2arrayƒz# #table.tableDtable.table4ƒy9 9table.CustomPropertiesCtable.CustomProperties8ƒx= =table.VariableContinuityCtable.VariableContinuity.ƒw3 3table.VariableUnitsCtable.VariableUnits<ƒvA Atable.VariableDescriptionsCtable.VariableDescriptions.ƒu3 3table.VariableTypesCtable.VariableTypes.ƒt3 3table.VariableNamesCtable.VariableNames0ƒs5 5table.DimensionNamesCtable.DimensionNames$ƒr) )table.UserDataCtable.UserData*ƒq/ /table.DescriptionCtable.Description$ƒp) )table.RowNamesCtable.RowNamesƒo tableBtable ƒn% %string.emptyAstring.empty*ƒm/ /string.ctranspose@string.ctranspose(ƒl- -string.transpose@string.transpose&ƒk+ +string.ipermute@string.ipermute$ƒj) )string.permute@string.permute(ƒi- -string.circshift@string.circshift$ƒh) )string.reshape@string.reshape&ƒg+ +string.repelems@string.repelems$ƒf) )string.repelem@string.repelem"ƒe' 'string.repmat@string.repmat$ƒd) )string.vertcat@string.vertcat$ƒc) )string.horzcat@string.horzcatƒb! !string.cat@string.cat"ƒa' 'string.unique?string.unique ƒ`% %string.upper?string.upper ƒ_% %string.lower?string.lowerƒ^# #string.plus?string.plusƒ]# #string.join?string.joinƒ\! !string.pad?string.pad ƒ[% %string.strip?string.strip"ƒZ' 'string.strcat?string.strcat*ƒY/ /string.splitlines?string.splitlines ƒX% %string.split?string.split&ƒW+ +string.sortrows?string.sortrowsƒV# #string.sort?string.sort$ƒU) )string.reverse?string.reverse2ƒT7 7string.replaceBetween?string.replaceBetween$ƒS) )string.replace?string.replace.ƒR3 3string.insertBefore?string.insertBefore,ƒQ1 1string.insertAfter?string.insertAfter2ƒP7 7string.extractBetween?string.extractBetween0ƒO5 5string.extractBefore?string.extractBefore.ƒN3 3string.extractAfter?string.extractAfter$ƒM) )string.extract?string.extract.ƒL3 3string.eraseBetween?string.eraseBetween ƒK% %string.erase?string.erase$ƒJ) )string.compose?string.compose"ƒI' 'string.append?string.append&ƒH+ +string.strncmpi>string.strncmpi$ƒG) )string.strncmp>string.strncmp$ƒF) )string.strcmpi>string.strcmpi"ƒE' 'string.strcmp>string.strcmp VºÅšoL)úÍ mL/ ã ¾ ™ r K *  Þ · – u P -  é Ä £ † W & ó Ä ™ d 5 ú Á ˆ A ņU(ûÖ«‚[2 ܯ„Sñ‘^-굈[(ùÆ‘`/ᲇX)ò¹€K õº8„y= =timetable.grouptransformXtimetable.grouptransform(„x- -timetable.rowfunXtimetable.rowfun(„w- -timetable.varfunXtimetable.varfun2„v7 7timetable.withinrangeWtimetable.withinrange6„u; ;timetable.overlapsrangeWtimetable.overlapsrange6„t; ;timetable.containsrangeWtimetable.containsrange4„s9 9timetable.issortedrowsVtimetable.issortedrows,„r1 1timetable.issortedVtimetable.issorted,„q1 1timetable.topkrowsVtimetable.topkrows(„p- -timetable.uniqueVtimetable.unique,„o1 1timetable.sortrowsVtimetable.sortrows$„n) )timetable.tailUtimetable.tail$„m) )timetable.headUtimetable.head.„l3 3timetable.splitvarsTtimetable.splitvars.„k3 3timetable.mergevarsTtimetable.mergevars2„j7 7timetable.convertvarsTtimetable.convertvars0„i5 5timetable.renamevarsTtimetable.renamevars,„h1 1timetable.movevarsTtimetable.movevars0„g5 5timetable.removevarsTtimetable.removevars*„f/ /timetable.addvarsTtimetable.addvars*„e/ /timetable.summaryStimetable.summary2„d7 7timetable.fillmissingStimetable.fillmissing@„cE Etimetable.standardizeMissingStimetable.standardizeMissing.„b3 3timetable.rmmissingStimetable.rmmissing0„a5 5timetable.anymissingStimetable.anymissing.„`3 3timetable.ismissingStimetable.ismissing,„_1 1timetable.isequalnRtimetable.isequaln*„^/ /timetable.isequalRtimetable.isequal2„]7 7timetable.istimetableQtimetable.istimetable.„\3 3timetable.isregularQtimetable.isregular(„[- -timetable.repmatPtimetable.repmat*„Z/ /timetable.repelemPtimetable.repelem*„Y/ /timetable.isemptyPtimetable.isempty&„X+ +timetable.ndimsPtimetable.ndims&„W+ +timetable.numelPtimetable.numel$„V) )timetable.sizePtimetable.size&„U+ +timetable.widthPtimetable.width(„T- -timetable.heightPtimetable.height"„S' 'timetable.catOtimetable.cat*„R/ /timetable.horzcatOtimetable.horzcat*„Q/ /timetable.vertcatOtimetable.vertcat.„P3 3timetable.timetableNtimetable.timetable<„OA Atimetable.CustomPropertiesMtimetable.CustomProperties@„NE Etimetable.VariableContinuityMtimetable.VariableContinuity6„M; ;timetable.VariableUnitsMtimetable.VariableUnitsD„LI Itimetable.VariableDescriptionsMtimetable.VariableDescriptions6„K; ;timetable.VariableTypesMtimetable.VariableTypes6„J; ;timetable.VariableNamesMtimetable.VariableNames8„I= =timetable.DimensionNamesMtimetable.DimensionNames,„H1 1timetable.UserDataMtimetable.UserData2„G7 7timetable.DescriptionMtimetable.Description(„F- -timetable.EventsMtimetable.Events,„E1 1timetable.TimeStepMtimetable.TimeStep0„D5 5timetable.SampleRateMtimetable.SampleRate.„C3 3timetable.StartTimeMtimetable.StartTime,„B1 1timetable.RowTimesMtimetable.RowTimes„A timetableLtimetable„@# #table.emptyKtable.empty"„?' 'table.vertcatKtable.vertcat"„>' 'table.squeezeKtable.squeeze„=! !table.sizeKtable.size „<% %table.repmatKtable.repmat"„;' 'table.repelemKtable.repelem„:# #table.numelKtable.numel„9# #table.ndimsKtable.ndims$„8) )table.isvectorKtable.isvector"„7' 'table.istableKtable.istable$„6) )table.isscalarKtable.isscalar„5# #table.isrowKtable.isrow$„4) )table.ismatrixKtable.ismatrix$„3) )table.isequalnKtable.isequaln"„2' 'table.isequalKtable.isequal"„1' 'table.isemptyKtable.isempty$„0) )table.iscolumnKtable.iscolumn"„/' 'table.horzcatKtable.horzcat„. table.catKtable.cat„-# #table.pivotJtable.pivot0„,5 5table.grouptransformJtable.grouptransform*„+/ /table.groupfilterJtable.groupfilter*„*/ /table.groupcountsJtable.groupcounts,„)1 1table.groupsummaryJtable.groupsummary „(% %table.rowfunJtable.rowfun „'% %table.varfunJtable.varfun(„&- -table.splitapplyJtable.splitapply(„%- -table.findgroupsJtable.findgroups8„$= =table.standardizeMissingItable.standardizeMissing KÁË”_6Þ­|G á ´ ‹ ` ;  Í š m B Ú £ f - ô ¹ � q * á ž m : Ô§p?ÇŒCÂM"Ö­|S"ùÈ™b+軆_8û¸ƒ> Ø«h3þÁ:…D? ?timeseries.setuniformtimeatimeseries.setuniformtime2…C7 7timeseries.setabstimeatimeseries.setabstime2…B7 7timeseries.getabstimeatimeseries.getabstime@…AE Etimeseries.hasduplicatetimes`timeseries.hasduplicatetimes*…@/ /timeseries.append`timeseries.append0…?5 5timeseries.delsample`timeseries.delsample0…>5 5timeseries.addsample`timeseries.addsampleB…=G Gtimeseries.getsampleusingtime`timeseries.getsampleusingtime2…<7 7timeseries.getsamples`timeseries.getsamples@…;E Etimeseries.getdatasamplesize`timeseries.getdatasamplesize:…:? ?timeseries.getdatasamples`timeseries.getdatasamples$…9) )timeseries.set_timeseries.set$…8) )timeseries.get_timeseries.get2…77 7timeseries.timeseries_timeseries.timeseries*…6/ /timeseries.Length^timeseries.Length@…5E Etimeseries.TreatNaNasMissing^timeseries.TreatNaNasMissing4…49 9timeseries.IsTimeFirst^timeseries.IsTimeFirst4…39 9timeseries.QualityInfo^timeseries.QualityInfo,…21 1timeseries.Quality^timeseries.Quality.…13 3timeseries.TimeInfo^timeseries.TimeInfo&…0+ +timeseries.Time^timeseries.Time.…/3 3timeseries.DataInfo^timeseries.DataInfo&….+ +timeseries.Data^timeseries.Data.…-3 3timeseries.UserData^timeseries.UserData&…,+ +timeseries.Name^timeseries.Name*…+/ /timeseries.Events^timeseries.Events…*! !timeseries]timeseries(…)- -eventtable.empty\eventtable.empty2…(7 7eventtable.eventtable\eventtable.eventtable>…'C Ceventtable.CustomProperties[eventtable.CustomPropertiesB…&G Geventtable.VariableContinuity[eventtable.VariableContinuity8…%= =eventtable.VariableUnits[eventtable.VariableUnitsF…$K Keventtable.VariableDescriptions[eventtable.VariableDescriptions8…#= =eventtable.VariableTypes[eventtable.VariableTypes8…"= =eventtable.VariableNames[eventtable.VariableNames:…!? ?eventtable.DimensionNames[eventtable.DimensionNames.… 3 3eventtable.UserData[eventtable.UserData4…9 9eventtable.Description[eventtable.Description*…/ /eventtable.Events[eventtable.Events.…3 3eventtable.TimeStep[eventtable.TimeStep2…7 7eventtable.SampleRate[eventtable.SampleRate0…5 5eventtable.StartTime[eventtable.StartTime.…3 3eventtable.RowTimes[eventtable.RowTimes@…E Eeventtable.EventEndsVariable[eventtable.EventEndsVariableF…K Keventtable.EventLengthsVariable[eventtable.EventLengthsVariableD…I Ieventtable.EventLabelsVariable[eventtable.EventLabelsVariable…! !eventtableZeventtable&…+ +timetable.emptyYtimetable.empty8…= =timetable.writetimetableYtimetable.writetimetable6…; ;timetable.timetable2odsYtimetable.timetable2ods6…; ;timetable.timetable2csvYtimetable.timetable2csv:…? ?timetable.timetable2tableXtimetable.timetable2table4…9 9timetable.table2structXtimetable.table2struct0…5 5timetable.table2cellXtimetable.table2cell2…7 7timetable.table2arrayXtimetable.table2array(… - -timetable.rmpropXtimetable.rmprop*… / /timetable.addpropXtimetable.addprop0… 5 5timetable.synceventsXtimetable.syncevents6… ; ;timetable.extracteventsXtimetable.extractevents2… 7 7timetable.synchronizeXtimetable.synchronize"…' 'timetable.lagXtimetable.lag(…- -timetable.retimeXtimetable.retime&…+ +timetable.pivotXtimetable.pivot*…/ /timetable.unstackXtimetable.unstack0…5 5timetable.splitapplyXtimetable.splitapply0…5 5timetable.findgroupsXtimetable.findgroups2…7 7timetable.inner2outerXtimetable.inner2outer.…3 3timetable.outerjoinXtimetable.outerjoin.…3 3timetable.innerjoinXtimetable.innerjoin$„) )timetable.joinXtimetable.join.„~3 3timetable.rows2varsXtimetable.rows2vars&„}+ +timetable.stackXtimetable.stack2„|7 7timetable.groupfilterXtimetable.groupfilter4„{9 9timetable.groupsummaryXtimetable.groupsummary2„z7 7timetable.groupcountsXtimetable.groupcounts Q½Ïž]ßœ] Ù š i 2 Ü ³ Œ e >  ð É Œ c 8 à ± ‚ Q û Ì ƒ T 'ðÉ¢{T-߸‘jCõΧ€Y2 ä½–oH%øË–e(ýÒ™`3Ïœm8ï¦_½P†U Utscollection.delsamplefromcollectionktscollection.delsamplefromcollectionL†Q Qtscollection.addsampletocollectionktscollection.addsampletocollectionD†I Itscollection.setTimeseriesNamejtscollection.setTimeseriesNameF†K Ktscollection.settimeseriesnamesjtscollection.settimeseriesnamesF†K Ktscollection.gettimeseriesnamesjtscollection.gettimeseriesnames2†7 7tscollection.removetsjtscollection.removets,†1 1tscollection.addtsjtscollection.addts0†5 5tscollection.isequalitscollection.isequal0† 5 5tscollection.isemptyitscollection.isempty.† 3 3tscollection.lengthitscollection.length*† / /tscollection.sizeitscollection.size6† ; ;tscollection.propertiesitscollection.properties6† ; ;tscollection.fieldnamesitscollection.fieldnames(†- -tscollection.setitscollection.set(†- -tscollection.getitscollection.get:†? ?tscollection.tscollectionitscollection.tscollection.†3 3tscollection.Lengthhtscollection.Length2†7 7tscollection.TimeInfohtscollection.TimeInfo*†/ /tscollection.Timehtscollection.Time*†/ /tscollection.Namehtscollection.Name †% %tscollectiongtscollection$†) )timeseries.setftimeseries.set$…) )timeseries.getftimeseries.get$…~) )timeseries.setftimeseries.set$…}) )timeseries.getftimeseries.get$…|) )timeseries.setftimeseries.set$…{) )timeseries.getftimeseries.get$…z) )timeseries.setftimeseries.set$…y) )timeseries.getftimeseries.get$…x) )timeseries.setftimeseries.set$…w) )timeseries.getftimeseries.get$…v) )timeseries.setftimeseries.set$…u) )timeseries.getftimeseries.get$…t) )timeseries.setftimeseries.set$…s) )timeseries.getftimeseries.get$…r) )timeseries.setftimeseries.set$…q) )timeseries.getftimeseries.get$…p) )timeseries.setftimeseries.set$…o) )timeseries.getftimeseries.get$…n) )timeseries.setftimeseries.set$…m) )timeseries.getftimeseries.get$…l) )timeseries.setftimeseries.set$…k) )timeseries.getftimeseries.get$…j) )timeseries.setftimeseries.set$…i) )timeseries.getftimeseries.get4…h9 9timeseries.idealfilterftimeseries.idealfilter*…g/ /timeseries.filterftimeseries.filter,…f1 1timeseries.detrendftimeseries.detrendF…eK Ktimeseries.isequalwithequalnansetimeseries.isequalwithequalnans,…d1 1timeseries.isequaletimeseries.isequal"…c' 'timeseries.eqetimeseries.eq.…b3 3timeseries.mldivideetimeseries.mldivide.…a3 3timeseries.mrdivideetimeseries.mrdivide,…`1 1timeseries.ldivideetimeseries.ldivide,…_1 1timeseries.rdivideetimeseries.rdivide*…^/ /timeseries.mtimesetimeseries.mtimes(…]- -timeseries.timesetimeseries.times(…\- -timeseries.minusetimeseries.minus&…[+ +timeseries.plusetimeseries.plus:…Z? ?timeseries.getqualitydescdtimeseries.getqualitydesc$…Y) )timeseries.iqrdtimeseries.iqr$…X) )timeseries.maxdtimeseries.max$…W) )timeseries.mindtimeseries.min$…V) )timeseries.sumdtimeseries.sum$…U) )timeseries.vardtimeseries.var$…T) )timeseries.stddtimeseries.std&…S+ +timeseries.modedtimeseries.mode*…R/ /timeseries.mediandtimeseries.median&…Q+ +timeseries.meandtimeseries.mean4…P9 9timeseries.synchronizectimeseries.synchronize.…O3 3timeseries.resamplectimeseries.resample<…NA Atimeseries.setinterpmethodctimeseries.setinterpmethod<…MA Atimeseries.getinterpmethodctimeseries.getinterpmethodB…LG Gtimeseries.gettsbetweeneventsbtimeseries.gettsbetweenevents<…KA Atimeseries.gettsaftereventbtimeseries.gettsafterevent@…JE Etimeseries.gettsafterateventbtimeseries.gettsafteratevent6…I; ;timeseries.gettsateventbtimeseries.gettsateventB…HG Gtimeseries.gettsbeforeateventbtimeseries.gettsbeforeatevent>…GC Ctimeseries.gettsbeforeeventbtimeseries.gettsbeforeevent.…F3 3timeseries.deleventbtimeseries.delevent.…E3 3timeseries.addeventbtimeseries.addevent G§·p7þÉ–c8 â · † I  Ç ‚ C Å ˆ =  Ë š ]  Í f â©x=ô±f-ôÁ~Aò·|Y"õÈ™b3ݾ‰^%ðŘoDÞµˆ]0Ö§,†\1 1dictionary.entriesdictionary.entries*†[/ /dictionary.removedictionary.remove*†Z/ /dictionary.lookupdictionary.lookup*†Y/ /dictionary.insertdictionary.insert(†X- -dictionary.isKey~dictionary.isKey*†W/ /dictionary.values~dictionary.values&†V+ +dictionary.keys~dictionary.keys4†U9 9dictionary.dispstrings}dictionary.dispstrings,†T1 1dictionary.isempty}dictionary.isempty(†S- -dictionary.ndims}dictionary.ndims&†R+ +dictionary.size}dictionary.size*†Q/ /dictionary.length}dictionary.length(†P- -dictionary.numel}dictionary.numel2†O7 7dictionary.numEntries}dictionary.numEntries6†N; ;dictionary.isConfigured}dictionary.isConfigured(†M- -dictionary.types}dictionary.types2†L7 7dictionary.dictionary}dictionary.dictionary†K! !dictionary|dictionary(†J- -tsdata.event.set{tsdata.event.set(†I- -tsdata.event.get{tsdata.event.get,†H1 1tsdata.event.event{tsdata.event.event4†G9 9tsdata.event.StartDateztsdata.event.StartDate,†F1 1tsdata.event.Unitsztsdata.event.Units*†E/ /tsdata.event.Timeztsdata.event.Time*†D/ /tsdata.event.Nameztsdata.event.Name4†C9 9tsdata.event.EventDataztsdata.event.EventData †B% %tsdata.eventytsdata.event8†A= =tsdata.interpolation.setxtsdata.interpolation.set8†@= =tsdata.interpolation.getxtsdata.interpolation.getL†?Q Qtsdata.interpolation.interpolationxtsdata.interpolation.interpolation:†>? ?tsdata.interpolation.Namewtsdata.interpolation.Name@†=E Etsdata.interpolation.Fhandlewtsdata.interpolation.Fhandle0†<5 5tsdata.interpolationvtsdata.interpolation6†;; ;tsdata.qualmetadata.setutsdata.qualmetadata.set6†:; ;tsdata.qualmetadata.getutsdata.qualmetadata.getH†9M Mtsdata.qualmetadata.qualmetadatautsdata.qualmetadata.qualmetadata@†8E Etsdata.qualmetadata.UserDatattsdata.qualmetadata.UserDataF†7K Ktsdata.qualmetadata.Descriptionttsdata.qualmetadata.Description8†6= =tsdata.qualmetadata.Codettsdata.qualmetadata.Code.†53 3tsdata.qualmetadatastsdata.qualmetadata6†4; ;tsdata.datametadata.setrtsdata.datametadata.set6†3; ;tsdata.datametadata.getrtsdata.datametadata.getH†2M Mtsdata.datametadata.datametadatartsdata.datametadata.datametadatad†1i itsdata.datametadata.InterpretSingleRowDataAs3Dqtsdata.datametadata.InterpretSingleRowDataAs3D@†0E Etsdata.datametadata.UserDataqtsdata.datametadata.UserDataJ†/O Otsdata.datametadata.Interpolationqtsdata.datametadata.Interpolation:†.? ?tsdata.datametadata.Unitsqtsdata.datametadata.Units.†-3 3tsdata.datametadataptsdata.datametadata6†,; ;tsdata.timemetadata.setotsdata.timemetadata.set6†+; ;tsdata.timemetadata.getotsdata.timemetadata.getH†*M Mtsdata.timemetadata.timemetadataotsdata.timemetadata.timemetadata:†)? ?tsdata.timemetadata.Startntsdata.timemetadata.StartB†(G Gtsdata.timemetadata.Incrementntsdata.timemetadata.Increment6†'; ;tsdata.timemetadata.Endntsdata.timemetadata.End<†&A Atsdata.timemetadata.Lengthntsdata.timemetadata.LengthB†%G Gtsdata.timemetadata.StartDatentsdata.timemetadata.StartDate<†$A Atsdata.timemetadata.Formatntsdata.timemetadata.Format@†#E Etsdata.timemetadata.UserDatantsdata.timemetadata.UserData:†"? ?tsdata.timemetadata.Unitsntsdata.timemetadata.Units.†!3 3tsdata.timemetadatamtsdata.timemetadata(† - -tscollection.getltscollection.get(†- -tscollection.getltscollection.get(†- -tscollection.getltscollection.get(†- -tscollection.getltscollection.get0†5 5tscollection.horzcatltscollection.horzcat0†5 5tscollection.vertcatltscollection.vertcat2†7 7tscollection.resamplektscollection.resample6†; ;tscollection.setabstimektscollection.setabstime6†; ;tscollection.getabstimektscollection.getabstimeD†I Itscollection.hasduplicatetimesktscollection.hasduplicatetimesF†K Ktscollection.getsampleusingtimektscollection.getsampleusingtime 8(À�^.þΦŒZ. ö À Š F  Ð ” z P ,  à ¶ Š ` 4  à ´ ˆ h H (  è È ® „ X : ̈X$ à°’`, И`(5‡9 9eventfilter.eventTimes“eventfilter.eventTimes5‡9 9eventfilter.rowIndices“eventfilter.rowIndices5‡9 9eventfilter.properties“eventfilter.properties7‡; ;eventfilter.eventfilter“eventfilter.eventfilter‡# #eventfilter’eventfilter1‡5 5rowfilter.rowIndices‘rowfilter.rowIndices/‡3 3rowfilter.rowfilter‘rowfilter.rowfilter‡  rowfilter�rowfilter-‡ 1 1withtol.rowIndices�withtol.rowIndices'‡ + +withtol.withtol�withtol.withtol‡  withtolŽwithtol1‡ 5 5timerange.rowIndices�timerange.rowIndices-‡1 1timerange.interval�timerange.intervalA‡E Etimerange.resolveEventBounds�timerange.resolveEventBounds9‡= =timerange.hasEventBounds�timerange.hasEventBounds/‡3 3timerange.timerange�timerange.timerange‡ timerangeŒtimerange)‡- -vartype.varMatch‹vartype.varMatch'‡+ +vartype.vartype‹vartype.vartype‡ vartypeŠvartype‡! !missing.ne‰missing.ne†! !missing.lt‰missing.lt†~! !missing.le‰missing.le†}! !missing.gt‰missing.gt†|! !missing.ge‰missing.ge†{! !missing.eq‰missing.eq)†z- -missing.isvectorˆmissing.isvector)†y- -missing.isscalarˆmissing.isscalar#†x' 'missing.isrowˆmissing.isrow+†w/ /missing.ismissingˆmissing.ismissing)†v- -missing.ismatrixˆmissing.ismatrix'†u+ +missing.isemptyˆmissing.isempty)†t- -missing.iscolumnˆmissing.iscolumn'†s+ +missing.keyHash‡missing.keyHash#†r' 'missing.numel‡missing.numel#†q' 'missing.ndims‡missing.ndims!†p% %missing.size‡missing.size'†o+ +missing.missing†missing.missing†n missing…missing9†m= =tabular.CustomProperties„tabular.CustomProperties=†lA Atabular.VariableContinuity„tabular.VariableContinuity3†k7 7tabular.VariableUnits„tabular.VariableUnitsA†jE Etabular.VariableDescriptions„tabular.VariableDescriptions3†i7 7tabular.VariableTypes„tabular.VariableTypes3†h7 7tabular.VariableNames„tabular.VariableNames5†g9 9tabular.DimensionNames„tabular.DimensionNames)†f- -tabular.UserData„tabular.UserData/†e3 3tabular.Description„tabular.Description†d tabularƒtabular%†c) )dictionary.cat‚dictionary.cat-†b1 1dictionary.vertcat‚dictionary.vertcat-†a1 1dictionary.horzcat‚dictionary.horzcat-†`1 1dictionary.keyHash�dictionary.keyHash/†_3 3dictionary.isequaln�dictionary.isequaln-†^1 1dictionary.isequal�dictionary.isequal=†]A Adictionary.writedictionary€dictionary.writedictionarypr0m1th3as-datatypes-9450b29/inst/000077500000000000000000000000001527370255000166535ustar00rootroot00000000000000pr0m1th3as-datatypes-9450b29/inst/+datatypes/000077500000000000000000000000001527370255000207245ustar00rootroot00000000000000pr0m1th3as-datatypes-9450b29/inst/+datatypes/+tabular/000077500000000000000000000000001527370255000224315ustar00rootroot00000000000000pr0m1th3as-datatypes-9450b29/inst/+datatypes/+tabular/CustomProperties.m000066400000000000000000000204221527370255000261360ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftp {datatypes} datatypes.tabular.CustomProperties ## ## The custom metadata of a @code{table} or a @code{timetable}. ## ## It holds the properties added with @code{addprop} and removed with ## @code{rmprop}, and is reached as ## @qcode{@var{tbl}.Properties.CustomProperties.@var{name}}. A single class ## serves both tabular classes, as in MATLAB. It cannot be constructed ## directly. ## ## @end deftp classdef CustomProperties properties (Access = private) Values = struct () Types = struct () endproperties methods (Access = {?datatypes.tabular.TabularProperties}) function this = CustomProperties (s, types) if (nargin > 0 && isstruct (s)) this.Values = s; endif if (nargin > 1 && isstruct (types)) this.Types = types; endif ## Every property carries its type under its own name. A set that does ## not match means a table wrote one container and not the other, and ## every read of 'Properties' passes through here, so it is caught at ## the first look rather than answering with the wrong type. if (! isequal (sort (fieldnames (this.Values)), ... sort (fieldnames (this.Types)))) error (strcat ("datatypes.tabular.CustomProperties: the custom", ... " properties and their types do not match.")); endif endfunction endmethods methods (Access = {?tabular}) ## Return the stored values and the type of each, so that a tabular class ## can take the whole store in one assignment. Both are structs keyed by ## the property names. function [vals, types] = unpack (this) vals = this.Values; types = this.Types; endfunction endmethods methods ## -*- texinfo -*- ## @deftypefn {datatypes.tabular.CustomProperties} {@var{names} =} fieldnames (@var{obj}) ## ## Return the names of the custom properties that are set. ## ## @end deftypefn function names = fieldnames (this) names = fieldnames (this.Values); endfunction ## -*- texinfo -*- ## @deftypefn {datatypes.tabular.CustomProperties} {@var{names} =} properties (@var{obj}) ## ## Return the names of the custom properties that are set. ## ## The names are data rather than properties the class declares, so ## @code{properties} answers from the set that is stored, exactly as ## @code{fieldnames} does. ## ## @end deftypefn function names = properties (this) names = fieldnames (this); endfunction ## -*- texinfo -*- ## @deftypefn {datatypes.tabular.CustomProperties} {@var{tf} =} isfield (@var{obj}, @var{name}) ## ## Return true when a custom property of that name is set. ## ## @var{name} may be a character vector or a cellstr, in which case a ## logical array of the same size is returned, as for a struct. ## ## @end deftypefn function tf = isfield (this, name) tf = isfield (this.Values, name); endfunction ## -*- texinfo -*- ## @deftypefn {datatypes.tabular.CustomProperties} {@var{tf} =} isempty (@var{obj}) ## ## Return true when no custom property is set. ## ## @end deftypefn function tf = isempty (this) tf = isempty (fieldnames (this.Values)); endfunction endmethods methods (Hidden) function varargout = subsref (this, s) if (! strcmp (s(1).type, '.')) error (strcat ("datatypes.tabular.CustomProperties: only '.'", ... " indexing is supported.")); endif name = s(1).subs; ## A property name may be given as a string scalar, as in MATLAB. if (isstring (name) && isscalar (name)) name = char (name); endif if (! (ischar (name) && isrow (name))) error (strcat ("datatypes.tabular.CustomProperties: '.' index", ... " argument must be a character vector or a", ... " string scalar.")); endif if (! isfield (this.Values, name)) error (strcat ("datatypes.tabular.CustomProperties: there is no", ... " custom property named '%s'."), name); endif out = this.Values.(name); if (numel (s) > 1) out = subsref (out, s(2:end)); endif varargout{1} = out; endfunction function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif disp (this); endfunction function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif names = fieldnames (this.Values); if (isempty (names)) fprintf (" No custom properties are set.\n"); fprintf (strcat (" Use 'addprop' and 'rmprop' methods to modify", ... " CustomProperties.\n")); return; endif fprintf ("\n CustomProperties with properties:\n\n"); for i = 1:numel (names) fprintf ("%+24s: %s\n", names{i}, ... datatypes.tabular.TabularProperties.formatValue ( ... this.Values.(names{i}))); endfor endfunction endmethods endclassdef ## The container a tabular class keeps its custom properties in. It cannot ## be constructed, so every fixture reaches it through 'addprop'. Measured ## on MATLAB R2024a. ## Test the object a table hands back is this class %!assert_equal (class (table (1).Properties.CustomProperties), ... %! 'datatypes.tabular.CustomProperties') ## Test a table with no custom property carries an empty container %!test %! C = table (1).Properties.CustomProperties; %! assert_equal (isempty (C), true); %! assert_equal (properties (C), cell (0, 1)); %! assert_equal (fieldnames (C), cell (0, 1)); ## Test 'addprop' names them and 'properties' answers in that order %!test %! T = addprop (table (1), {'p', 'q'}, {'table', 'variable'}); %! C = T.Properties.CustomProperties; %! assert_equal (isempty (C), false); %! assert_equal (properties (C), {'p'; 'q'}); %! assert_equal (fieldnames (C), properties (C)); ## Test 'isfield' answers for a name that is there and one that is not %!test %! T = addprop (table (1), {'p'}, {'table'}); %! C = T.Properties.CustomProperties; %! assert_equal (isfield (C, 'p'), true); %! assert_equal (isfield (C, 'zz'), false); ## Test a custom property reads back by name %!test %! T = addprop (table (1), {'p'}, {'table'}); %! T.Properties.CustomProperties.p = 'hello'; %! assert_equal (T.Properties.CustomProperties.p, 'hello'); ## Test 'rmprop' takes it out of the container again %!test %! T = addprop (table (1), {'p', 'q'}, {'table', 'table'}); %! T = rmprop (T, {'p'}); %! assert_equal (properties (T.Properties.CustomProperties), {'q'}); ## Test input validation for 'datatypes.tabular.CustomProperties' %!error ... %! C = table (1).Properties.CustomProperties; ... %! subsref (C, substruct ('()', {1})); %!error ... %! C = table (1).Properties.CustomProperties; ... %! subsref (C, substruct ('{}', {1})); %!error ... %! C = table (1).Properties.CustomProperties; ... %! C.Nope; %!error ... %! C = table (1).Properties.CustomProperties; ... %! s = struct ('type', '.', 'subs', 7); ... %! subsref (C, s); pr0m1th3as-datatypes-9450b29/inst/+datatypes/+tabular/EventtableProperties.m000066400000000000000000000137441527370255000267660ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftp {datatypes} datatypes.tabular.EventtableProperties ## ## The properties object of an @code{eventtable}. ## ## It carries everything a @code{timetable} publishes, from ## @code{datatypes.tabular.TimetableProperties}, and adds the three that say ## which variables describe the events: @qcode{EventLabelsVariable}, ## @qcode{EventLengthsVariable} and @qcode{EventEndsVariable}. It is what ## @qcode{@var{et}.Properties} returns and cannot be constructed directly. ## ## The three are listed first, ahead of @qcode{Description}, because they are ## what distinguishes an event table from the timetable it otherwise is. ## ## @end deftp classdef EventtableProperties < datatypes.tabular.TimetableProperties properties EventLabelsVariable = [] EventLengthsVariable = [] EventEndsVariable = [] endproperties methods (Access = {?eventtable}) function this = EventtableProperties (s, cpTypes) if (nargin < 1) return; endif if (nargin < 2) cpTypes = struct (); endif names = fieldnames (s); for i = 1:numel (names) if (strcmp (names{i}, 'CustomProperties')) this.CustomProperties = datatypes.tabular.CustomProperties ( ... s.CustomProperties, cpTypes); else this.(names{i}) = s.(names{i}); endif endfor endfunction endmethods methods (Hidden) ## 'Events' is declared by 'TimetableProperties' and a classdef subclass ## cannot delete an inherited property, so the name is refused here ## rather than answered with the default it would otherwise carry. Every ## other name goes on to the inherited reader. function varargout = subsref (this, s) if (strcmp (s(1).type, '.')) name = s(1).subs; if (isstring (name) && isscalar (name)) name = char (name); endif if (ischar (name) && strcmp (name, 'Events')) error (strcat ("eventtable: 'Events' is not a property of an", ... " event table; an event table cannot carry an", ... " event table.")); endif endif varargout{1} = subsref@datatypes.tabular.TabularProperties (this, s); endfunction endmethods methods (Access = protected) ## The three event properties come first, ahead of the metadata every ## tabular class shares, which is neither where inheritance would place ## them nor where the row time properties sit. function names = displayOrder (this) names = {'EventLabelsVariable', 'EventLengthsVariable', ... 'EventEndsVariable', 'Description', 'UserData', ... 'DimensionNames', 'VariableNames', 'VariableTypes', ... 'VariableDescriptions', 'VariableUnits', ... 'VariableContinuity', 'RowTimes', 'StartTime', ... 'SampleRate', 'TimeStep', 'CustomProperties'}; endfunction ## 'Events' is declared by 'TimetableProperties', but an event table does ## not carry one: its name is kept off the list of properties, and ## 'subsref' refuses it so that asking for it says why. function names = withheldNames (this) names = {'Events'}; endfunction endmethods endclassdef ## The properties object of an event table. It cannot be constructed, so ## every fixture reaches it through an event table. Measured on MATLAB ## R2024a. ## Test the object an event table hands back is this class %!assert_equal (class (eventtable (datetime (2024, 1, 1)).Properties), ... %! 'datatypes.tabular.EventtableProperties') ## Test the three event designations are named first, before the metadata ## every tabular class shares %!test %! E = eventtable (datetime (2024, 1, 1) + hours ((0:1)')); %! p = properties (E.Properties); %! assert_equal (p(1:3), {'EventLabelsVariable'; 'EventLengthsVariable'; ... %! 'EventEndsVariable'}); %! assert_equal (p{4}, 'Description'); ## Test the time metadata is named as a timetable's is %!test %! E = eventtable (datetime (2024, 1, 1) + hours ((0:1)')); %! p = properties (E.Properties); %! assert_equal (p{end}, 'CustomProperties'); %! assert_equal (any (strcmp (p, 'RowTimes')), true); ## Test 'fieldnames' answers as 'properties' does %!test %! E = eventtable (datetime (2024, 1, 1) + hours ((0:1)')); %! assert_equal (fieldnames (E.Properties), properties (E.Properties)); ## An event table carries events; it is not carried by one, so 'Events' is ## the one property of a timetable it does not have. ## Test 'Events' is absent from the display order %!test %! E = eventtable (datetime (2024, 1, 1) + hours ((0:1)')); %! assert_equal (any (strcmp (properties (E.Properties), 'Events')), false); ## Test reading it is refused rather than answered %!error ... %! eventtable (datetime (2024, 1, 1)).Properties.Events; ## Test the designations read back through the object %!test %! E = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! assert_equal (E.Properties.EventLabelsVariable, 'EventLabels'); %! assert_equal (isempty (E.Properties.EventLengthsVariable), true); %! assert_equal (isempty (E.Properties.EventEndsVariable), true); pr0m1th3as-datatypes-9450b29/inst/+datatypes/+tabular/TableProperties.m000066400000000000000000000070301527370255000257130ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftp {datatypes} datatypes.tabular.TableProperties ## ## The properties object of a @code{table}. ## ## It carries the metadata every tabular class shares, from ## @code{datatypes.tabular.TabularProperties}, and adds @qcode{RowNames}. ## It is what @qcode{@var{tbl}.Properties} returns and cannot be constructed ## directly. ## ## @end deftp classdef TableProperties < datatypes.tabular.TabularProperties properties RowNames = {} endproperties methods (Access = {?table}) function this = TableProperties (s, cpTypes) if (nargin < 1) return; endif if (nargin < 2) cpTypes = struct (); endif names = fieldnames (s); for i = 1:numel (names) if (strcmp (names{i}, 'CustomProperties')) this.CustomProperties = datatypes.tabular.CustomProperties ( ... s.CustomProperties, cpTypes); else this.(names{i}) = s.(names{i}); endif endfor endfunction endmethods methods (Access = protected) ## MATLAB lists the row label metadata after the variable metadata and ## before the custom properties, which is not where inheritance would ## place it. function names = displayOrder (this) names = {'Description', 'UserData', 'DimensionNames', 'VariableNames', ... 'VariableTypes', 'VariableDescriptions', 'VariableUnits', ... 'VariableContinuity', 'RowNames', 'CustomProperties'}; endfunction endmethods endclassdef ## The properties object of a table. It cannot be constructed, so every ## fixture reaches it through a table. Measured on MATLAB R2024a. ## Test the object a table hands back is this class %!assert_equal (class (table (1).Properties), ... %! 'datatypes.tabular.TableProperties') ## Test the display order names the row labels before the custom properties %!test %! p = properties (table (1).Properties); %! assert_equal (p{end-1}, 'RowNames'); %! assert_equal (p{end}, 'CustomProperties'); %!test %! p = properties (table (1).Properties); %! assert_equal (p(1:3), {'Description'; 'UserData'; 'DimensionNames'}); ## Test 'fieldnames' answers as 'properties' does %!test %! P = table (1).Properties; %! assert_equal (fieldnames (P), properties (P)); ## Test the row labels are the only thing this class adds %!test %! P = table (1).Properties; %! assert_equal (sum (strcmp (properties (P), 'RowNames')), 1); %! assert_equal (any (strcmp (properties (P), 'RowTimes')), false); ## Test the metadata reads back through the object %!test %! T = table ([1; 2], [3; 4], 'VariableNames', {'v', 'w'}); %! assert_equal (T.Properties.VariableNames, {'v', 'w'}); %! assert_equal (T.Properties.DimensionNames, {'Row', 'Variables'}); %! assert_equal (T.Properties.RowNames, {}); pr0m1th3as-datatypes-9450b29/inst/+datatypes/+tabular/TabularProperties.m000066400000000000000000000234571527370255000262710ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftp {datatypes} datatypes.tabular.TabularProperties ## ## Abstract superclass of the properties objects of @code{table} and ## @code{timetable}. ## ## It carries the metadata both classes share and renders it; each subclass ## adds the metadata of its own row labels and states the order the whole ## set is displayed in. Objects of these classes are what ## @qcode{@var{tbl}.Properties} returns; they are never constructed directly. ## ## @end deftp classdef (Abstract) TabularProperties properties Description = '' UserData = [] DimensionNames = {} VariableNames = {} VariableTypes = {} VariableDescriptions = {} VariableUnits = {} VariableContinuity = [] CustomProperties = [] endproperties methods (Access = protected) ## The property names in the order they are displayed. Every subclass ## must state it: inheritance alone would append the subclass's own row ## label metadata after 'CustomProperties', where MATLAB shows it before. function names = displayOrder (this) error ("%s: subclass must implement displayOrder.", class (this)); endfunction ## Inherited properties that a subclass keeps off its list of properties. ## A classdef subclass can neither delete an inherited property nor ## declare it again, so keeping its name off the list is the only way to ## hide it. None by default. function names = withheldNames (this) names = {}; endfunction endmethods methods ## -*- texinfo -*- ## @deftypefn {datatypes.tabular.TabularProperties} {@var{names} =} properties (@var{obj}) ## ## Return the names of the metadata properties. ## ## The names are returned in the order they are displayed in, which is not ## the order they are declared in. ## ## @end deftypefn function names = properties (this) names = orderedNames (this); endfunction ## -*- texinfo -*- ## @deftypefn {datatypes.tabular.TabularProperties} {@var{names} =} fieldnames (@var{obj}) ## ## Return the names of the metadata properties. ## ## The names are returned in the order they are displayed in, which is not ## the order they are declared in. ## ## @end deftypefn function names = fieldnames (this) names = orderedNames (this); endfunction endmethods methods (Hidden) ## Class specific subscripted reference. Only the field name is handled ## here; the properties, the methods and the unknown-name refusal stay ## with the builtin, which already answers correctly. The rest of the ## chain goes back through 'subsref' so that a member carrying its own, ## such as 'CustomProperties', is the one that resolves it. function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); if (strcmp (s.type, '.')) ## A field name may be given as a string scalar, as in MATLAB. if (isstring (s.subs) && isscalar (s.subs)) s.subs = char (s.subs); endif if (! (ischar (s.subs) && isrow (s.subs))) error (strcat ("%s.subsref: '.' index argument must be a", ... " character vector or a string scalar."), ... class (this)); endif endif out = builtin ("subsref", this, s); ## Chained references if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif disp (this); endfunction function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif parts = strsplit (class (this), '.'); fprintf ("\n %s with properties:\n\n", parts{end}); names = displayOrder (this); for i = 1:numel (names) if (strcmp (names{i}, 'CustomProperties')) fprintf ("%s\n", customPropertyText (this)); else fprintf ("%+24s: %s\n", names{i}, ... datatypes.tabular.TabularProperties.formatValue ( ... this.(names{i}))); endif endfor endfunction endmethods methods (Access = private) ## The declared property names sorted into the display order. The ## declared list is what is sorted, rather than 'displayOrder' being ## returned as it stands, so that a property left out of 'displayOrder' ## is still listed instead of disappearing from view. Only the names in ## 'withheldNames' are left out. function names = orderedNames (this) declared = builtin ("properties", this); declared = declared(! ismember (declared, withheldNames (this))); order = displayOrder (this); shown = order(ismember (order, declared)); names = [shown(:); declared(! ismember (declared, order))]; endfunction function txt = customPropertyText (this) if (isempty (this.CustomProperties)) ## The newline leads the second chunk rather than trailing the ## first, 'strcat' stripping whitespace off the end of every one. txt = strcat (" CustomProperties: No custom properties are", ... " set.\n Use 'addprop' and 'rmprop' methods to", ... " modify CustomProperties."); return; endif txt = strcat ("\n Custom Properties (access using t.Properties.", ... "CustomProperties.):"); cpNames = fieldnames (this.CustomProperties); for i = 1:numel (cpNames) cpValue = subsref (this.CustomProperties, ... substruct ('.', cpNames{i})); txt = [txt, sprintf("\n%+24s: %s", cpNames{i}, ... datatypes.tabular.TabularProperties.formatValue (cpValue))]; endfor endfunction endmethods methods (Static, Hidden) ## Render one metadata value the way MATLAB renders it in this listing. ## Called from a private method, so it is Hidden rather than protected. function str = formatValue (val) if (ischar (val)) str = sprintf ("'%s'", val); elseif (isempty (val)) if (iscell (val)) str = "{}"; else str = "[]"; endif elseif (iscellstr (val)) ## Only a row of names is written out; anything else is summarized by ## its size, as MATLAB does. 'RowNames' is the property this reaches. if (all (cellfun (@isempty, val))) str = "{}"; elseif (isrow (val)) str = ["{", strtrim(sprintf ("'%s' ", val{:})), "}"]; else str = sprintf ("{%s cell}", sizestr (val)); endif elseif (isa (val, 'string')) str = ["[", strtrim(sprintf ("""%s"" ", cellstr(val){:})), "]"]; elseif (islogical (val) || isnumeric (val)) if (isscalar (val)) str = ["[", strtrim(disp (val)), "]"]; else str = sprintf ("[%s %s]", sizestr (val), class (val)); endif elseif (iscell (val)) str = sprintf ("{%s cell}", sizestr (val)); else str = sprintf ("[%s %s]", sizestr (val), class (val)); endif endfunction endmethods endclassdef ## The size of a value the way it is written in this listing, as in '2x1'. function str = sizestr (val) str = strjoin (arrayfun (@(x) sprintf ("%d", x), size (val), ... "UniformOutput", false), "x"); endfunction ## The subclass must state the display order, and the stub raises until it ## does, naming the subclass that has not. It is protected, so the fixture is ## a subclass that implements nothing; 'properties' asks for the order and so ## reaches it without a wrapper of its own. %!shared fixdir %! fixdir = tempname (); %! mkdir (fixdir); %! src = {'classdef noprops < datatypes.tabular.TabularProperties', ... %! 'endclassdef'}; %! fid = fopen (fullfile (fixdir, 'noprops.m'), 'w'); %! fprintf (fid, '%s\n', src{:}); %! fclose (fid); %! addpath (fixdir); ## Test 'displayOrder' raises until the subclass implements it %!error ... %! properties (noprops ()); ## Test the fixture is removed again %!test %! rmpath (fixdir); %! delete (fullfile (fixdir, 'noprops.m')); %! rmdir (fixdir); %! assert_equal (exist (fixdir, 'dir'), 0); ## The '.' subscript is read by the shared body, which names the class that ## was asked rather than itself, so a table's properties object and a ## timetable's report under their own names. ## Test the refusal names the calling class %!error ... %! P = table (1).Properties; ... %! s = struct ('type', '.', 'subs', 7); ... %! subsref (P, s); %!error ... %! P = timetable (1, 'RowTimes', datetime (2024, 1, 1)).Properties; ... %! s = struct ('type', '.', 'subs', 7); ... %! subsref (P, s); pr0m1th3as-datatypes-9450b29/inst/+datatypes/+tabular/TimetableProperties.m000066400000000000000000000103631527370255000265750ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftp {datatypes} datatypes.tabular.TimetableProperties ## ## The properties object of a @code{timetable}. ## ## It carries the metadata every tabular class shares, from ## @code{datatypes.tabular.TabularProperties}, and adds the four that ## describe the row times, @qcode{RowTimes}, @qcode{StartTime}, ## @qcode{SampleRate} and @qcode{TimeStep}, together with @qcode{Events}. ## It is what ## @qcode{@var{tt}.Properties} returns and cannot be constructed directly. ## ## @end deftp classdef TimetableProperties < datatypes.tabular.TabularProperties properties RowTimes = [] StartTime = [] SampleRate = [] TimeStep = [] Events = [] endproperties methods (Access = {?timetable}) function this = TimetableProperties (s, cpTypes) if (nargin < 1) return; endif if (nargin < 2) cpTypes = struct (); endif names = fieldnames (s); for i = 1:numel (names) if (strcmp (names{i}, 'CustomProperties')) this.CustomProperties = datatypes.tabular.CustomProperties ( ... s.CustomProperties, cpTypes); else this.(names{i}) = s.(names{i}); endif endfor endfunction endmethods methods (Access = protected) ## MATLAB lists the row time metadata after the variable metadata and ## before the custom properties, which is where a table lists 'RowNames' ## and is not where inheritance would place it. function names = displayOrder (this) names = {'Description', 'UserData', 'DimensionNames', 'VariableNames', ... 'VariableTypes', 'VariableDescriptions', 'VariableUnits', ... 'VariableContinuity', 'RowTimes', 'StartTime', 'SampleRate', ... 'TimeStep', 'Events', 'CustomProperties'}; endfunction endmethods endclassdef ## The properties object of a timetable. It cannot be constructed, so every ## fixture reaches it through a timetable. Measured on MATLAB R2024a. ## Test the object a timetable hands back is this class %!assert_equal (class (timetable (1, 'RowTimes', datetime (2024, 1, 1)) ... %! .Properties), 'datatypes.tabular.TimetableProperties') ## Test the display order names the time metadata before the custom ## properties, where a table names its row labels %!test %! TT = timetable ((1:2)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 1, 1)); %! p = properties (TT.Properties); %! assert_equal (p(end-6:end-1), {'VariableContinuity'; 'RowTimes'; ... %! 'StartTime'; 'SampleRate'; 'TimeStep'; ... %! 'Events'}); %!test %! TT = timetable ((1:2)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 1, 1)); %! p = properties (TT.Properties); %! assert_equal (p{end}, 'CustomProperties'); %! assert_equal (any (strcmp (p, 'RowNames')), false); ## Test 'fieldnames' answers as 'properties' does %!test %! TT = timetable ((1:2)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 1, 1)); %! assert_equal (fieldnames (TT.Properties), properties (TT.Properties)); ## Test the time metadata reads back through the object %!test %! TT = timetable ((1:2)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 1, 1)); %! assert_equal (TT.Properties.TimeStep, hours (1)); %! assert_equal (TT.Properties.StartTime, datetime (2024, 1, 1)); %! assert_equal (TT.Properties.DimensionNames, {'Time', 'Variables'}); pr0m1th3as-datatypes-9450b29/inst/+tsdata/000077500000000000000000000000001527370255000202065ustar00rootroot00000000000000pr0m1th3as-datatypes-9450b29/inst/+tsdata/datametadata.m000066400000000000000000000326731527370255000230110ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftp {datatypes} tsdata.datametadata ## ## The data metadata of a @code{timeseries}. ## ## A @code{tsdata.datametadata} object describes the samples of a ## @code{timeseries}: their units, how the series is evaluated between them, ## and any data the user attaches. It is what ## @qcode{@var{ts}.DataInfo} returns. ## ## @end deftp classdef datametadata properties ## -*- texinfo -*- ## @deftp {tsdata.datametadata} {property} Units ## ## The units of the data. ## ## A character vector, @qcode{''} by default. It is free text: nothing ## checks it against the data or converts the data with it. ## ## @end deftp Units = '' ## -*- texinfo -*- ## @deftp {tsdata.datametadata} {property} Interpolation ## ## The interpolation method. ## ## A @code{tsdata.interpolation} object, @qcode{'linear'} by default. ## A method name or a function handle may be assigned, and is converted. ## MATLAB starts a @code{tsdata.datametadata} created on its own with ## @code{[]} here, and a @code{timeseries} with @qcode{'linear'}; here ## both start with @qcode{'linear'}. ## ## @end deftp Interpolation = tsdata.interpolation ('linear') ## -*- texinfo -*- ## @deftp {tsdata.datametadata} {property} UserData ## ## Any data the user attaches, @code{[]} by default. ## ## @end deftp UserData = [] ## -*- texinfo -*- ## @deftp {tsdata.datametadata} {property} InterpretSingleRowDataAs3D ## ## Stored for compatibility, with no effect. ## ## A logical scalar, @code{false} by default. In MATLAB it affects only ## @code{addsample} given a row of data, where @code{true} gives ## two-dimensional data whose time runs along its last dimension while the ## sample holds the whole row, a state no other method can read ## consistently. Here the orientation of the data always follows its ## dimensions, as @qcode{IsTimeFirst} of @code{timeseries} describes. ## ## @end deftp InterpretSingleRowDataAs3D = false endproperties methods ## -*- texinfo -*- ## @deftypefn {tsdata.datametadata} {@var{di} =} tsdata.datametadata () ## ## Create a data metadata object. ## ## @code{@var{di} = tsdata.datametadata ()} returns an object with ## @qcode{Units} @qcode{''}, @qcode{Interpolation} @qcode{'linear'}, ## @qcode{UserData} @code{[]} and @qcode{InterpretSingleRowDataAs3D} ## @code{false}. A @code{timeseries} creates its own; this is needed ## only to assign a whole new one to @qcode{@var{ts}.DataInfo}. ## ## @seealso{timeseries, tsdata.interpolation} ## @end deftypefn function this = datametadata () endfunction function this = set.Units (this, val) this.Units = checkText (val, 'Units'); endfunction function this = set.Interpolation (this, val) if (isa (val, 'tsdata.interpolation') && ! isscalar (val)) error (strcat ("tsdata.datametadata: 'Interpolation' must be a", ... " single tsdata.interpolation object.")); endif if (! isa (val, 'tsdata.interpolation')) if (! (isa (val, 'function_handle') || (ischar (val) && isrow (val)) || (isstring (val) && isscalar (val)))) error (strcat ("tsdata.datametadata: 'Interpolation' must be a", ... " tsdata.interpolation object, a method name or a", ... " function handle.")); endif val = tsdata.interpolation (val); endif this.Interpolation = val; endfunction function this = set.InterpretSingleRowDataAs3D (this, val) if (! ((islogical (val) || isnumeric (val)) && isscalar (val) && any (val == [0, 1]))) error (strcat ("tsdata.datametadata: 'InterpretSingleRowDataAs3D'", ... " must be a logical scalar.")); endif this.InterpretSingleRowDataAs3D = logical (val); endfunction ## -*- texinfo -*- ## @deftypefn {tsdata.datametadata} {@var{S} =} get (@var{di}) ## @deftypefnx {tsdata.datametadata} {@var{value} =} get (@var{di}, @var{name}) ## @deftypefnx {tsdata.datametadata} {@var{values} =} get (@var{di}, @var{names}) ## ## Return property values. ## ## @code{@var{S} = get (@var{di})} returns a structure of every ## property of @var{di}. @code{@var{value} = get (@var{di}, ## @var{name})} returns the property @var{name}, a character vector or a ## string scalar matched in any case, and @code{@var{values} = get ## (@var{di}, @var{names})} a row cell array of the properties named in ## the cell array @var{names}. For an array, one @var{name} gives a cell ## array of its size, and no name, or several, a cell array with a row ## per object and a column per property. ## ## @seealso{tsdata.datametadata.set, timeseries.get} ## @end deftypefn function out = get (this, varargin) out = timeseries.propertyGet (this, 'tsdata.datametadata.get', ... varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {tsdata.datametadata} {} set (@var{di}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {tsdata.datametadata} {@var{di2} =} set (@var{di}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {tsdata.datametadata} {@var{S} =} set (@var{di}) ## ## Set property values. ## ## @code{set (@var{di}, @var{name}, @var{value}, @dots{})} sets each ## property @var{name}, matched in any case, to its @var{value}, in the ## order given, in the variable @var{di} of the caller, which must ## therefore be a variable. Metadata held by a @code{timeseries} is set ## with dot assignment instead, as ## @code{@var{ts}.TimeInfo.Units = 'days'}. ## ## @code{@var{di2} = set (@var{di}, @var{name}, @var{value}, @dots{})} ## returns the modified object and leaves @var{di} as it was. ## @code{@var{S} = set (@var{di})} returns what @code{get (@var{di})} ## does. ## ## @seealso{tsdata.datametadata.get, timeseries.set} ## @end deftypefn function varargout = set (this, varargin) if (nargin == 1) varargout{1} = get (this); return; endif this = timeseries.propertySet (this, 'tsdata.datametadata.set', ... varargin{:}); if (nargout > 0) varargout{1} = this; return; endif ## With no output the caller's own variable is set, as in MATLAB name = inputname (1); if (isempty (name)) error (strcat ("tsdata.datametadata.set: with no output argument,", ... " DI must be a variable; set anything else", ... " with dot assignment.")); endif assignin ('caller', name, this); endfunction endmethods methods (Hidden) function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif disp (this); endfunction function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif fprintf ("\n datametadata with properties:\n\n"); names = {'Units', 'Interpolation', 'UserData', ... 'InterpretSingleRowDataAs3D'}; for i = 1:numel (names) fprintf ("%30s: %s\n", names{i}, ... timeseries.dispValue (this.(names{i}))); endfor fprintf ("\n"); endfunction endmethods endclassdef ## Text property values are character vectors; a string scalar is converted. function val = checkText (val, name) if (isstring (val) && isscalar (val)) val = char (val); endif if (! (ischar (val) && (isrow (val) || isempty (val)))) error ("tsdata.datametadata: '%s' must be a character vector.", name); endif if (isempty (val)) val = ''; endif endfunction ## Test the defaults %!test %! di = tsdata.datametadata (); %! assert_equal (di.Units, ''); %! assert_equal (class (di.Interpolation), 'tsdata.interpolation'); %! assert_equal (di.Interpolation.Name, 'linear'); %! assert_equal (di.UserData, []); %! assert_equal (di.InterpretSingleRowDataAs3D, false); %!test %! assert_equal (properties (tsdata.datametadata ()), {'Units'; ... %! 'Interpolation'; 'UserData'; 'InterpretSingleRowDataAs3D'}); ## Test assignment %!test %! di = tsdata.datametadata (); %! di.Units = 'volts'; %! assert_equal (di.Units, 'volts'); %!test %! di = tsdata.datametadata (); %! di.Units = string ('volts'); %! assert_equal (di.Units, 'volts'); %!test %! di = tsdata.datametadata (); %! di.Units = 'volts'; %! di.Units = string (''); %! assert_equal (di.Units, ''); %!test %! di = tsdata.datametadata (); %! di.Interpolation = 'zoh'; %! assert_equal (di.Interpolation.Name, 'zoh'); %!test %! di = tsdata.datametadata (); %! di.Interpolation = tsdata.interpolation ('zoh'); %! assert_equal (di.Interpolation.Name, 'zoh'); %!test %! di = tsdata.datametadata (); %! di.Interpolation = @(t, d, tn) tn; %! assert_equal (di.Interpolation.Name, 'myFuncHandle'); %!test %! di = tsdata.datametadata (); %! di.Interpolation.Name = 'zoh'; %! assert_equal (di.Interpolation.Name, 'zoh'); %!test %! di = tsdata.datametadata (); %! di.UserData = {1, 'a'}; %! assert_equal (di.UserData, {1, 'a'}); %!test %! di = tsdata.datametadata (); %! di.InterpretSingleRowDataAs3D = 1; %! assert_equal (di.InterpretSingleRowDataAs3D, true); ## Test value semantics %!test %! a = tsdata.datametadata (); %! b = a; %! b.Units = 'volts'; %! assert_equal (a.Units, ''); ## Test 'get' %!test %! S = get (tsdata.datametadata ()); %! assert_equal (fieldnames (S), {'Units'; 'Interpolation'; 'UserData'; ... %! 'InterpretSingleRowDataAs3D'}); %!test %! assert_equal (get (tsdata.datametadata (), 'Units'), ''); %! assert_equal (get (tsdata.datametadata (), 'units'), ''); %!test %! assert_equal (get (tsdata.datametadata (), {'Units'}), {''}); ## Test 'set' with no output sets the caller's variable %!test %! obj = tsdata.datametadata (); %! set (obj, 'Units', 'volts'); %! assert_equal (obj.Units, 'volts'); %!test %! obj = tsdata.datametadata (); %! set (obj, 'UNITS', 'volts'); %! assert_equal (obj.Units, 'volts'); ## Test 'set' with an output leaves its input as it was %!test %! obj = tsdata.datametadata (); %! obj2 = set (obj, 'Units', 'volts'); %! assert_equal (obj2.Units, 'volts'); %! assert_equal (obj.Units, ''); %!test %! obj = tsdata.datametadata (); %! assert_equal (fieldnames (set (obj)), fieldnames (get (obj))); %!error ... %! setfield (tsdata.datametadata (), 'Units', 5) %!error ... %! setfield (tsdata.datametadata (), 'Units', ['ab'; 'cd']) %!error ... %! setfield (tsdata.datametadata (), 'Interpolation', ... %! [tsdata.interpolation('linear'), tsdata.interpolation('zoh')]) %!error ... %! setfield (tsdata.datametadata (), 'Interpolation', 5) %!error ... %! setfield (tsdata.datametadata (), 'Interpolation', 'cubic') %!error ... %! setfield (tsdata.datametadata (), 'InterpretSingleRowDataAs3D', 'x') %!error ... %! setfield (tsdata.datametadata (), 'InterpretSingleRowDataAs3D', 2) %!error ... %! setfield (tsdata.datametadata (), 'InterpretSingleRowDataAs3D', ... %! [true, false]) %!error ... %! get (tsdata.datametadata (), 'Units', 'Units') %!error get (tsdata.datametadata (), 'Bogus') %!error ... %! get (tsdata.datametadata (), 5) %!error ... %! set (tsdata.datametadata (), 'Units', 'volts') %!error ... %! obj = tsdata.datametadata (); set (obj, 'Units'); %!error ... %! obj = tsdata.datametadata (); set (obj, 'Bogus', 1); %!error ... %! obj = tsdata.datametadata (); set (obj, 5, 1); pr0m1th3as-datatypes-9450b29/inst/+tsdata/event.m000066400000000000000000000362661527370255000215220ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftp {datatypes} tsdata.event ## ## An event of a @code{timeseries}. ## ## A @code{tsdata.event} object marks a named instant, with any data the user ## attaches to it. The @qcode{Events} property of a @code{timeseries} holds ## an array of them. An event carries its own time, in its own units and ## optionally from its own start date, so it keeps its instant when the ## series' time vector changes. ## ## @end deftp classdef event properties ## -*- texinfo -*- ## @deftp {tsdata.event} {property} EventData ## ## Any data the user attaches, @code{[]} by default. ## ## @end deftp EventData = [] ## -*- texinfo -*- ## @deftp {tsdata.event} {property} Name ## ## The name of the event, a character vector. Names need not be unique. ## ## @end deftp Name = '' ## -*- texinfo -*- ## @deftp {tsdata.event} {property} Time ## ## The time of the event, a real scalar in @qcode{Units}, counted from ## @qcode{StartDate} when that is set. @code{NaN} is refused, where ## MATLAB accepts it. ## ## @end deftp Time = 0 ## -*- texinfo -*- ## @deftp {tsdata.event} {property} Units ## ## The units of @qcode{Time}. ## ## One of the units a @code{timeseries} time vector takes, or @qcode{''}. ## MATLAB accepts any value here. ## ## @end deftp Units = '' ## -*- texinfo -*- ## @deftp {tsdata.event} {property} StartDate ## ## The absolute date @qcode{Time} counts from, or @qcode{''} for a ## relative time. A string scalar or a scalar @code{datetime} is converted ## to a character vector; text that is not a date is refused. ## ## @end deftp StartDate = '' endproperties methods ## -*- texinfo -*- ## @deftypefn {tsdata.event} {@var{e} =} tsdata.event () ## @deftypefnx {tsdata.event} {@var{e} =} tsdata.event (@var{name}) ## @deftypefnx {tsdata.event} {@var{e} =} tsdata.event (@var{name}, @var{time}) ## ## Create an event. ## ## @code{@var{e} = tsdata.event (@var{name})} returns an event named ## @var{name}, a character vector or a string scalar, at @qcode{Time} 0 ## with no @qcode{Units}. With no argument the name is @qcode{''}. ## ## @code{@var{e} = tsdata.event (@var{name}, @var{time})} places it at ## @var{time}. A real scalar is a relative time in seconds. A date, as ## text or as a scalar @code{datetime}, is an absolute time: the event is ## then at @qcode{Time} 0 in @qcode{'days'} from a @qcode{StartDate} of ## that date, stored as @qcode{'dd-mmm-yyyy HH:MM:SS'}, with milliseconds ## as @qcode{'.FFF'} when the seconds are not whole. ## ## MATLAB ignores a third argument and refuses a @code{datetime}. ## ## @seealso{timeseries} ## @end deftypefn function this = event (name, time) if (nargin == 0) return; endif if (! ((ischar (name) && (isrow (name) || isempty (name))) || (isstring (name) && isscalar (name)))) error ("tsdata.event: NAME must be a character vector."); endif this.Name = name; if (nargin < 2) return; endif if ((ischar (time) && isrow (time)) || (isstring (time) && isscalar (time)) || (isa (time, 'datetime') && isscalar (time))) [startDate, errmsg] = tsdata.timemetadata.dateValue (time, "TIME"); if (! isempty (errmsg)) error ("tsdata.event: %s", errmsg); endif this.StartDate = startDate; this.Units = 'days'; else this.Time = time; this.Units = 'seconds'; endif endfunction function this = set.Name (this, val) if (isstring (val) && isscalar (val)) val = char (val); endif if (! (ischar (val) && (isrow (val) || isempty (val)))) error ("tsdata.event: 'Name' must be a character vector."); endif if (isempty (val)) val = ''; endif this.Name = val; endfunction function this = set.Time (this, val) if (! (isnumeric (val) && isreal (val) && isscalar (val) && ! isnan (val))) error ("tsdata.event: 'Time' must be a real scalar."); endif this.Time = double (val); endfunction function this = set.Units (this, val) [val, errmsg] = tsdata.timemetadata.unitsValue (val, true); if (! isempty (errmsg)) error ("tsdata.event: %s", errmsg); endif this.Units = val; endfunction function this = set.StartDate (this, val) [val, errmsg] = tsdata.timemetadata.dateValue (val, "'StartDate'"); if (! isempty (errmsg)) error ("tsdata.event: %s", errmsg); endif this.StartDate = val; endfunction ## -*- texinfo -*- ## @deftypefn {tsdata.event} {@var{S} =} get (@var{e}) ## @deftypefnx {tsdata.event} {@var{value} =} get (@var{e}, @var{name}) ## @deftypefnx {tsdata.event} {@var{values} =} get (@var{e}, @var{names}) ## ## Return property values. ## ## @code{@var{S} = get (@var{e})} returns a structure of every ## property of @var{e}. @code{@var{value} = get (@var{e}, ## @var{name})} returns the property @var{name}, a character vector or a ## string scalar matched in any case, and @code{@var{values} = get ## (@var{e}, @var{names})} a row cell array of the properties named in ## the cell array @var{names}. For an array, one @var{name} gives a cell ## array of its size, and no name, or several, a cell array with a row ## per object and a column per property. ## ## MATLAB refuses an output argument to @code{set} on an event and, given ## an array of events and one name, returns the first event's value only; ## here @code{set} returns the modified events as on any other class, and ## @code{get} returns one value per event. ## ## @seealso{tsdata.event.set, timeseries.get} ## @end deftypefn function out = get (this, varargin) out = timeseries.propertyGet (this, 'tsdata.event.get', ... varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {tsdata.event} {} set (@var{e}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {tsdata.event} {@var{e2} =} set (@var{e}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {tsdata.event} {@var{S} =} set (@var{e}) ## ## Set property values. ## ## @code{set (@var{e}, @var{name}, @var{value}, @dots{})} sets each ## property @var{name}, matched in any case, to its @var{value}, in the ## order given, in the variable @var{e} of the caller, which must ## therefore be a variable. Metadata held by a @code{timeseries} is set ## with dot assignment instead, as ## @code{@var{ts}.TimeInfo.Units = 'days'}. ## ## @code{@var{e2} = set (@var{e}, @var{name}, @var{value}, @dots{})} ## returns the modified object and leaves @var{e} as it was. ## @code{@var{S} = set (@var{e})} returns what @code{get (@var{e})} ## does. ## ## @seealso{tsdata.event.get, timeseries.set} ## @end deftypefn function varargout = set (this, varargin) if (nargin == 1) varargout{1} = get (this); return; endif this = timeseries.propertySet (this, 'tsdata.event.set', ... varargin{:}); if (nargout > 0) varargout{1} = this; return; endif ## With no output the caller's own variable is set, as in MATLAB name = inputname (1); if (isempty (name)) error (strcat ("tsdata.event.set: with no output argument,", ... " E must be a variable; set anything else", ... " with dot assignment.")); endif assignin ('caller', name, this); endfunction endmethods methods (Hidden) function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif disp (this); endfunction function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif if (! isscalar (this)) fprintf (" %s tsdata.event array\n\n", sizestr (this)); return; endif fprintf ("\n event with properties:\n\n"); names = {'EventData', 'Name', 'Time', 'Units', 'StartDate'}; for i = 1:numel (names) fprintf ("%13s: %s\n", names{i}, ... timeseries.dispValue (this.(names{i}))); endfor fprintf ("\n"); endfunction endmethods endclassdef ## The size of X as MATLAB writes it in a summary, as '3x1'. function str = sizestr (x) str = strjoin (arrayfun (@num2str, size (x), 'UniformOutput', false), 'x'); endfunction ## Test construction %!test %! e = tsdata.event (); %! assert_equal (e.Name, ''); %! assert_equal (e.Time, 0); %! assert_equal (e.Units, ''); %! assert_equal (e.StartDate, ''); %! assert_equal (e.EventData, []); %!test %! assert_equal (properties (tsdata.event ()), ... %! {'EventData'; 'Name'; 'Time'; 'Units'; 'StartDate'}); %!test %! e = tsdata.event ('x'); %! assert_equal (e.Name, 'x'); %! assert_equal (e.Time, 0); %! assert_equal (e.Units, ''); %!test %! e = tsdata.event (string ('x'), 1); %! assert_equal (e.Name, 'x'); %!test %! e = tsdata.event ('', 1); %! assert_equal (e.Name, ''); %! assert_equal (e.Time, 1); ## Test a numeric time is relative, in seconds %!test %! e = tsdata.event ('x', 5); %! assert_equal (e.Time, 5); %! assert_equal (e.Units, 'seconds'); %! assert_equal (e.StartDate, ''); %!test %! e = tsdata.event ('x', int8 (3)); %! assert_equal (e.Time, 3); %! assert_equal (class (e.Time), 'double'); %!test %! e = tsdata.event ('x', -1); %! assert_equal (e.Time, -1); %!test %! e = tsdata.event ('x', Inf); %! assert_equal (e.Time, Inf); ## Test a date is absolute, day 0 from that date %!test %! e = tsdata.event ('x', '01-Jan-2024'); %! assert_equal (e.Time, 0); %! assert_equal (e.Units, 'days'); %! assert_equal (e.StartDate, '01-Jan-2024 00:00:00'); %!test %! e = tsdata.event ('x', '01-Jan-2024 06:00:00'); %! assert_equal (e.StartDate, '01-Jan-2024 06:00:00'); %!test %! e = tsdata.event ('x', string ('01-Jan-2024')); %! assert_equal (e.StartDate, '01-Jan-2024 00:00:00'); %!test %! e = tsdata.event ('x', datetime (2024, 1, 1, 6, 0, 0)); %! assert_equal (e.Units, 'days'); %! assert_equal (e.StartDate, '01-Jan-2024 06:00:00'); ## Test assignment %!test %! e = tsdata.event ('x', 5); %! e.Units = 'minutes'; %! assert_equal (e.Units, 'minutes'); %! assert_equal (e.Time, 5); %!test %! e = tsdata.event ('x', 5); %! e.Units = ''; %! assert_equal (e.Units, ''); %!test %! e = tsdata.event ('x', 5); %! e.StartDate = '01-Jan-2024'; %! assert_equal (e.StartDate, '01-Jan-2024 00:00:00'); %!test %! e = tsdata.event ('x', 5); %! e.EventData = {1, 'a'}; %! assert_equal (e.EventData, {1, 'a'}); %!test %! e = tsdata.event ('x', 5); %! e.Name = 'a b'; %! assert_equal (e.Name, 'a b'); %!test %! e = tsdata.event ('x', 5); %! e.Time = 7; %! assert_equal (e.Time, 7); ## Test value semantics and arrays %!test %! a = tsdata.event ('x', 1); %! b = a; %! b.Time = 2; %! assert_equal (a.Time, 1); %!test %! E = [tsdata.event('a', 1), tsdata.event('b', 2)]; %! assert_equal (size (E), [1, 2]); %! assert_equal (E(2).Name, 'b'); ## Test 'get' %!test %! S = get (tsdata.event ('x', 1)); %! assert_equal (fieldnames (S), {'EventData'; 'Name'; 'Time'; 'Units'; ... %! 'StartDate'}); %!test %! assert_equal (get (tsdata.event ('x', 1), 'Name'), 'x'); %! assert_equal (get (tsdata.event ('x', 1), 'name'), 'x'); %!test %! assert_equal (get (tsdata.event ('x', 1), {'Name'}), {'x'}); ## Test 'set' with no output sets the caller's variable %!test %! obj = tsdata.event ('x', 1); %! set (obj, 'Name', 'y'); %! assert_equal (obj.Name, 'y'); %!test %! obj = tsdata.event ('x', 1); %! set (obj, 'NAME', 'y'); %! assert_equal (obj.Name, 'y'); ## Test 'set' with an output leaves its input as it was %!test %! obj = tsdata.event ('x', 1); %! obj2 = set (obj, 'Name', 'y'); %! assert_equal (obj2.Name, 'y'); %! assert_equal (obj.Name, 'x'); %!test %! obj = tsdata.event ('x', 1); %! assert_equal (fieldnames (set (obj)), fieldnames (get (obj))); ## Test 'get' and 'set' on an array of events %!test %! E = [tsdata.event('a', 1), tsdata.event('b', 2)]; %! assert_equal (get (E, 'Name'), {'a', 'b'}); %! assert_equal (get (E, {'Name', 'Time'}), {'a', 1; 'b', 2}); %!test %! E = [tsdata.event('a', 1), tsdata.event('b', 2)]; %! set (E, 'Units', 'minutes'); %! assert_equal (E(2).Units, 'minutes'); %!error tsdata.event (5, 1) %!error tsdata.event ({'x'}, 1) %!error tsdata.event ('x', [1, 2]) %!error tsdata.event ('x', 1 + 2i) %!error tsdata.event ('x', true) %!error tsdata.event ('x', NaN) %!error tsdata.event ('x', 'bogus') %!error tsdata.event ('x', NaT) %!error ... %! setfield (tsdata.event ('x', 1), 'Name', 5) %!error ... %! setfield (tsdata.event ('x', 1), 'Time', 'x') %!error ... %! setfield (tsdata.event ('x', 1), 'Units', 'bogus') %!error ... %! setfield (tsdata.event ('x', 1), 'StartDate', 'bogus') %!error ... %! get (tsdata.event ('x', 1), 'Name', 'Name') %!error get (tsdata.event ('x', 1), 'Bogus') %!error ... %! get (tsdata.event ('x', 1), 5) %!error ... %! set (tsdata.event ('x', 1), 'Name', 'y') %!error ... %! obj = tsdata.event ('x', 1); set (obj, 'Name'); %!error ... %! obj = tsdata.event ('x', 1); set (obj, 'Bogus', 1); %!error ... %! obj = tsdata.event ('x', 1); set (obj, 5, 1); pr0m1th3as-datatypes-9450b29/inst/+tsdata/interpolation.m000066400000000000000000000426461527370255000232670ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftp {datatypes} tsdata.interpolation ## ## The interpolation method of a @code{timeseries}. ## ## A @code{tsdata.interpolation} object names how a @code{timeseries} is ## evaluated between its samples, and holds the function that does it. It is ## what @qcode{@var{ts}.DataInfo.Interpolation} returns, and it is the value ## @code{setinterpmethod} sets. ## ## Two methods are built in, @qcode{'linear'} and @qcode{'zoh'} (zero-order ## hold, which keeps each value until the next sample), and any function ## handle may be given instead. ## ## @end deftp classdef interpolation properties (Dependent) ## -*- texinfo -*- ## @deftp {tsdata.interpolation} {property} Fhandle ## ## The interpolating function. ## ## A function handle called as ## @code{@var{newData} = @var{fh} (@var{newTime}, @var{oldTime}, ## @var{oldData})}, as in MATLAB: @var{newTime} and @var{oldTime} are ## column vectors, and @var{oldData} and @var{newData} have time along ## their first dimension, a sample per row. It is @code{[]} for an object ## that names no method. Assigning a function handle sets @qcode{Name} to ## @qcode{'myFuncHandle'}. ## ## MATLAB stores this property in several shapes, among them a cell ## holding the handle and, for a built-in method, a cell holding a handle ## and a function name; here it is always the handle itself. ## ## @end deftp Fhandle ## -*- texinfo -*- ## @deftp {tsdata.interpolation} {property} Name ## ## The name of the interpolation method. ## ## One of @qcode{'linear'}, @qcode{'zoh'} or @qcode{'myFuncHandle'}, or ## @qcode{''} for an object that names no method. Assigning ## @qcode{'linear'} or @qcode{'zoh'} also sets @qcode{Fhandle} to the ## built-in function; @qcode{'myFuncHandle'} is set by assigning a function ## handle to @qcode{Fhandle}, not by name. MATLAB accepts any name and ## leaves @qcode{Fhandle} as it was, so the two can disagree there. ## ## @end deftp Name endproperties properties (Access = private) fh = [] name = '' endproperties methods ## -*- texinfo -*- ## @deftypefn {tsdata.interpolation} {@var{ip} =} tsdata.interpolation () ## @deftypefnx {tsdata.interpolation} {@var{ip} =} tsdata.interpolation (@var{name}) ## @deftypefnx {tsdata.interpolation} {@var{ip} =} tsdata.interpolation (@var{fh}) ## ## Create an interpolation method object. ## ## @code{@var{ip} = tsdata.interpolation ()} returns an object that names ## no method, with @qcode{Name} @qcode{''} and @qcode{Fhandle} @code{[]}. ## ## @code{@var{ip} = tsdata.interpolation (@var{name})} returns the built-in ## method @var{name}, @qcode{'linear'} or @qcode{'zoh'}, given as a ## character vector or a string scalar. ## ## @code{@var{ip} = tsdata.interpolation (@var{fh})} returns a method ## evaluated by the function handle @var{fh}, called as ## @code{@var{newData} = @var{fh} (@var{newTime}, @var{oldTime}, ## @var{oldData})}, and names it @qcode{'myFuncHandle'}. ## ## A cell array is refused, where MATLAB returns an empty object for it ## without an error. ## ## @seealso{timeseries, tsdata.datametadata} ## @end deftypefn function this = interpolation (method) if (nargin == 0) return; endif if (isa (method, 'function_handle')) this.Fhandle = method; elseif ((ischar (method) && isrow (method)) || (isstring (method) && isscalar (method))) this.Name = method; else error (strcat ("tsdata.interpolation: METHOD must be 'linear',", ... " 'zoh' or a function handle.")); endif endfunction function val = get.Fhandle (this) val = this.fh; endfunction function this = set.Fhandle (this, val) if (isa (val, 'function_handle') && isscalar (val)) this.fh = val; this.name = 'myFuncHandle'; elseif (isnumeric (val) && isempty (val)) this.fh = []; this.name = ''; else error (strcat ("tsdata.interpolation: 'Fhandle' must be a", ... " function handle or empty.")); endif endfunction function val = get.Name (this) val = this.name; endfunction function this = set.Name (this, val) if (isstring (val) && isscalar (val)) val = char (val); endif if (! (ischar (val) && (isrow (val) || isempty (val)))) error ("tsdata.interpolation: 'Name' must be a character vector."); endif switch (val) case 'linear' this.fh = @(nt, ot, od) tsdata.interpolation.builtin (nt, ot, od, ... 'linear'); case 'zoh' this.fh = @(nt, ot, od) tsdata.interpolation.builtin (nt, ot, od, ... 'zoh'); case 'myFuncHandle' if (! strcmp (this.name, 'myFuncHandle')) error (strcat ("tsdata.interpolation: 'Name' is set to", ... " 'myFuncHandle' by assigning a function", ... " handle to 'Fhandle'.")); endif case '' this.fh = []; otherwise error (strcat ("tsdata.interpolation: 'Name' must be", ... " 'linear', 'zoh' or 'myFuncHandle'.")); endswitch this.name = val; endfunction ## -*- texinfo -*- ## @deftypefn {tsdata.interpolation} {@var{S} =} get (@var{ip}) ## @deftypefnx {tsdata.interpolation} {@var{value} =} get (@var{ip}, @var{name}) ## @deftypefnx {tsdata.interpolation} {@var{values} =} get (@var{ip}, @var{names}) ## ## Return property values. ## ## @code{@var{S} = get (@var{ip})} returns a structure of every ## property of @var{ip}. @code{@var{value} = get (@var{ip}, ## @var{name})} returns the property @var{name}, a character vector or a ## string scalar matched in any case, and @code{@var{values} = get ## (@var{ip}, @var{names})} a row cell array of the properties named in ## the cell array @var{names}. For an array, one @var{name} gives a cell ## array of its size, and no name, or several, a cell array with a row ## per object and a column per property. ## ## @seealso{tsdata.interpolation.set, timeseries.get} ## @end deftypefn function out = get (this, varargin) out = timeseries.propertyGet (this, 'tsdata.interpolation.get', ... varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {tsdata.interpolation} {} set (@var{ip}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {tsdata.interpolation} {@var{ip2} =} set (@var{ip}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {tsdata.interpolation} {@var{S} =} set (@var{ip}) ## ## Set property values. ## ## @code{set (@var{ip}, @var{name}, @var{value}, @dots{})} sets each ## property @var{name}, matched in any case, to its @var{value}, in the ## order given, in the variable @var{ip} of the caller, which must ## therefore be a variable. Metadata held by a @code{timeseries} is set ## with dot assignment instead, as ## @code{@var{ts}.TimeInfo.Units = 'days'}. ## ## @code{@var{ip2} = set (@var{ip}, @var{name}, @var{value}, @dots{})} ## returns the modified object and leaves @var{ip} as it was. ## @code{@var{S} = set (@var{ip})} returns what @code{get (@var{ip})} ## does. ## ## @seealso{tsdata.interpolation.get, timeseries.set} ## @end deftypefn function varargout = set (this, varargin) if (nargin == 1) varargout{1} = get (this); return; endif this = timeseries.propertySet (this, 'tsdata.interpolation.set', ... varargin{:}); if (nargout > 0) varargout{1} = this; return; endif ## With no output the caller's own variable is set, as in MATLAB name = inputname (1); if (isempty (name)) error (strcat ("tsdata.interpolation.set: with no output argument,", ... " IP must be a variable; set anything else", ... " with dot assignment.")); endif assignin ('caller', name, this); endfunction endmethods methods (Static, Hidden) ## The built-in methods, called as a user function is: OT the old times ## and NT the new, both columns, OD the old data with a sample per row of ## its first dimension. Each data column is interpolated from its own ## samples that are not NaN, as MATLAB does; repeated old times are ## right-continuous. 'linear' interpolates; 'zoh' holds the last sample ## at or before each new time. Past either end the result is NaN. function nd = builtin (nt, ot, od, method) sz = size (od); od = reshape (double (od), sz(1), []); nt = nt(:); ot = ot(:); nd = NaN (numel (nt), columns (od)); for j = 1:columns (od) ok = ! isnan (od(:,j)); t = ot(ok); v = od(ok,j); if (isempty (t)) continue; endif if (strcmp (method, 'zoh')) for i = 1:numel (nt) k = find (t <= nt(i), 1, 'last'); if (! isempty (k) && nt(i) <= t(end)) nd(i,j) = v(k); endif endfor elseif (all (t == t(1))) ## Repeated times are right-continuous: the last sample there nd(nt == t(1),j) = v(end); else nd(:,j) = interp1 (t, v, nt, 'linear', NaN); endif endfor nd = reshape (nd, [numel(nt), sz(2:end)]); endfunction endmethods methods (Hidden) function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif disp (this); endfunction function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif if (isempty (this.fh)) fhText = '[]'; else fhText = ['@', func2str(this.fh)]; ## 'func2str' already prefixes an anonymous function with '@' if (strncmp (fhText, '@@', 2)) fhText = fhText(2:end); endif endif fprintf ("\n interpolation with properties:\n\n"); fprintf (" Fhandle: %s\n", fhText); fprintf (" Name: '%s'\n\n", this.name); endfunction endmethods endclassdef ## Test construction and the built-in methods %!test %! ip = tsdata.interpolation (); %! assert_equal (ip.Name, ''); %! assert_equal (ip.Fhandle, []); %!test %! ip = tsdata.interpolation ('linear'); %! assert_equal (ip.Name, 'linear'); %! assert_equal (ip.Fhandle (1, [0; 2], [0; 10]), 5); %!test %! ip = tsdata.interpolation ('zoh'); %! assert_equal (ip.Name, 'zoh'); %! assert_equal (ip.Fhandle (1, [0; 2], [0; 10]), 0); %!test %! ip = tsdata.interpolation (string ('zoh')); %! assert_equal (ip.Name, 'zoh'); %!test %! ip = tsdata.interpolation ('linear'); %! assert_equal (ip.Fhandle (1, [0; 2], [0, 4; 10, 8]), [5, 6]); %!test %! ip = tsdata.interpolation ('linear'); %! assert_equal (isna (ip.Fhandle (3, [0; 2], [0; 10])), false); %! assert_equal (ip.Fhandle (3, [0; 2], [0; 10]), NaN); ## Test a function handle names the method 'myFuncHandle' %!test %! ip = tsdata.interpolation (@(nt, ot, od) 2 * nt); %! assert_equal (ip.Name, 'myFuncHandle'); %! assert_equal (ip.Fhandle (3, [0; 1], [0; 1]), 6); %!test %! ip = tsdata.interpolation (@sin); %! assert_equal (ip.Fhandle, @sin); ## Test assigning 'Name' re-derives 'Fhandle' %!test %! ip = tsdata.interpolation ('linear'); %! ip.Name = 'zoh'; %! assert_equal (ip.Fhandle (1, [0; 2], [0; 10]), 0); %!test %! ip = tsdata.interpolation (@(t, d, tn) tn); %! ip.Name = 'linear'; %! assert_equal (ip.Fhandle (1, [0; 2], [0; 10]), 5); %!test %! ip = tsdata.interpolation ('linear'); %! ip.Name = ''; %! assert_equal (ip.Fhandle, []); %!test %! ip = tsdata.interpolation (@(t, d, tn) tn); %! ip.Name = 'myFuncHandle'; %! assert_equal (ip.Name, 'myFuncHandle'); %!test %! ip = tsdata.interpolation (); %! ip.Name = string ('linear'); %! assert_equal (ip.Name, 'linear'); ## Test assigning 'Fhandle' sets 'Name' %!test %! ip = tsdata.interpolation ('zoh'); %! ip.Fhandle = @(t, d, tn) tn; %! assert_equal (ip.Name, 'myFuncHandle'); %!test %! ip = tsdata.interpolation ('zoh'); %! ip.Fhandle = []; %! assert_equal (ip.Name, ''); ## Test value semantics %!test %! a = tsdata.interpolation ('linear'); %! b = a; %! b.Name = 'zoh'; %! assert_equal (a.Name, 'linear'); ## Test 'get' %!test %! S = get (tsdata.interpolation ('linear')); %! assert_equal (fieldnames (S), {'Fhandle'; 'Name'}); %!test %! assert_equal (get (tsdata.interpolation ('linear'), 'Name'), 'linear'); %! assert_equal (get (tsdata.interpolation ('linear'), 'name'), 'linear'); %!test %! assert_equal (get (tsdata.interpolation ('linear'), {'Name'}), {'linear'}); ## Test 'set' with no output sets the caller's variable %!test %! obj = tsdata.interpolation ('linear'); %! set (obj, 'Name', 'zoh'); %! assert_equal (obj.Name, 'zoh'); %!test %! obj = tsdata.interpolation ('linear'); %! set (obj, 'NAME', 'zoh'); %! assert_equal (obj.Name, 'zoh'); ## Test 'set' with an output leaves its input as it was %!test %! obj = tsdata.interpolation ('linear'); %! obj2 = set (obj, 'Name', 'zoh'); %! assert_equal (obj2.Name, 'zoh'); %! assert_equal (obj.Name, 'linear'); %!test %! obj = tsdata.interpolation ('linear'); %! assert_equal (fieldnames (set (obj)), fieldnames (get (obj))); ## Test the built-in methods take MATLAB's arguments and skip NaN samples %!test %! ip = tsdata.interpolation ('linear'); %! assert_equal (ip.Fhandle ([0.5; 1.5; 2.5], [0; 1; 2; 3], [1; NaN; 3; 4]), ... %! [1.5; 2.5; 3.5]); %!test %! ip = tsdata.interpolation ('zoh'); %! assert_equal (ip.Fhandle ([0.5; 1.5; 2.5], [0; 1; 2; 3], [1; NaN; 3; 4]), ... %! [1; 1; 3]); ## Test repeated old times are right-continuous %!test %! ip = tsdata.interpolation ('linear'); %! assert_equal (ip.Fhandle ([0.9; 1; 1.1], [0; 1; 1; 2], [1; 2; 3; 4]), ... %! [1.9; 3; 3.1], 1e-14); %!test %! ip = tsdata.interpolation ('zoh'); %! assert_equal (ip.Fhandle ([0.9; 1; 1.1], [0; 1; 1; 2], [1; 2; 3; 4]), ... %! [1; 3; 3]); ## Test past either end gives NaN, for 'zoh' too %!test %! ip = tsdata.interpolation ('zoh'); %! nd = ip.Fhandle ([-1; 0.5; 4; 5], (0:4)', [10; 20; 40; 80; 160]); %! assert_equal (nd, [NaN; 10; 160; NaN]); %!test %! ip = tsdata.interpolation ('linear'); %! od = permute (reshape (1:12, 2, 2, 3), [3, 1, 2]); %! nd = ip.Fhandle ([0.5; 1.5], [0; 1; 2], od); %! assert_equal (size (nd), [2, 2, 2]); %! assert_equal (squeeze (nd(1,:,:)), [3, 5; 4, 6]); %!error ... %! tsdata.interpolation ({@sin}) %!error ... %! tsdata.interpolation (5) %!error ... %! tsdata.interpolation ('cubic') %!error ... %! tsdata.interpolation ('Linear') %!error ... %! setfield (tsdata.interpolation (), 'Name', 5) %!error ... %! setfield (tsdata.interpolation ('linear'), 'Name', 'myFuncHandle') %!error ... %! setfield (tsdata.interpolation (), 'Fhandle', 'zoh') %!error ... %! setfield (tsdata.interpolation (), 'Fhandle', {@sin}) %!error ... %! get (tsdata.interpolation ('linear'), 'Name', 'Name') %!error get (tsdata.interpolation ('linear'), 'Bogus') %!error ... %! get (tsdata.interpolation ('linear'), 5) %!error ... %! set (tsdata.interpolation ('linear'), 'Name', 'zoh') %!error ... %! obj = tsdata.interpolation ('linear'); set (obj, 'Name'); %!error ... %! obj = tsdata.interpolation ('linear'); set (obj, 'Bogus', 1); %!error ... %! obj = tsdata.interpolation ('linear'); set (obj, 5, 1); pr0m1th3as-datatypes-9450b29/inst/+tsdata/qualmetadata.m000066400000000000000000000276561527370255000230470ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftp {datatypes} tsdata.qualmetadata ## ## The quality metadata of a @code{timeseries}. ## ## A @code{tsdata.qualmetadata} object lists the quality codes a ## @code{timeseries} may carry in its @qcode{Quality} property, and a ## description of each. It is what @qcode{@var{ts}.QualityInfo} returns. ## Code @code{@var{Code}(@var{k})} is described by ## @code{@var{Description}@{@var{k}@}}. ## ## @end deftp classdef qualmetadata properties ## -*- texinfo -*- ## @deftp {tsdata.qualmetadata} {property} Code ## ## The quality codes. ## ## A vector of distinct integers from -128 to 127, stored as ## @code{double}, @code{[]} by default. MATLAB accepts any number here, ## repeated codes included. ## ## @end deftp Code = [] ## -*- texinfo -*- ## @deftp {tsdata.qualmetadata} {property} Description ## ## The description of each quality code. ## ## A cell array of character vectors, @code{[]} by default. A character ## vector or a string array is converted. ## ## @end deftp Description = [] ## -*- texinfo -*- ## @deftp {tsdata.qualmetadata} {property} UserData ## ## Any data the user attaches, @code{[]} by default. ## ## @end deftp UserData = [] endproperties methods ## -*- texinfo -*- ## @deftypefn {tsdata.qualmetadata} {@var{qi} =} tsdata.qualmetadata () ## ## Create a quality metadata object. ## ## @code{@var{qi} = tsdata.qualmetadata ()} returns an object with ## @qcode{Code}, @qcode{Description} and @qcode{UserData} all @code{[]}. ## A @code{timeseries} creates its own; this is needed only to assign a ## whole new one to @qcode{@var{ts}.QualityInfo}. ## ## @seealso{timeseries} ## @end deftypefn function this = qualmetadata () endfunction function this = set.Code (this, val) if (isempty (val) && (isnumeric (val) || islogical (val))) this.Code = []; return; endif if (! (isnumeric (val) && isreal (val) && isvector (val) && all (val == fix (val)) && all (val >= -128 & val <= 127))) error (strcat ("tsdata.qualmetadata: 'Code' must be a vector of", ... " integers from -128 to 127.")); endif if (numel (unique (val)) != numel (val)) error ("tsdata.qualmetadata: 'Code' must not repeat a code."); endif this.Code = double (val); endfunction function this = set.Description (this, val) if (isempty (val) && ! iscellstr (val) && ! ischar (val)) this.Description = []; return; endif if (isstring (val)) val = cellstr (val); elseif (ischar (val) && (isrow (val) || isempty (val))) val = {val}; endif if (! (iscellstr (val) && (isvector (val) || isempty (val)))) error (strcat ("tsdata.qualmetadata: 'Description' must be a cell", ... " array of character vectors.")); endif this.Description = val; endfunction ## -*- texinfo -*- ## @deftypefn {tsdata.qualmetadata} {@var{S} =} get (@var{qi}) ## @deftypefnx {tsdata.qualmetadata} {@var{value} =} get (@var{qi}, @var{name}) ## @deftypefnx {tsdata.qualmetadata} {@var{values} =} get (@var{qi}, @var{names}) ## ## Return property values. ## ## @code{@var{S} = get (@var{qi})} returns a structure of every ## property of @var{qi}. @code{@var{value} = get (@var{qi}, ## @var{name})} returns the property @var{name}, a character vector or a ## string scalar matched in any case, and @code{@var{values} = get ## (@var{qi}, @var{names})} a row cell array of the properties named in ## the cell array @var{names}. For an array, one @var{name} gives a cell ## array of its size, and no name, or several, a cell array with a row ## per object and a column per property. ## ## @seealso{tsdata.qualmetadata.set, timeseries.get} ## @end deftypefn function out = get (this, varargin) out = timeseries.propertyGet (this, 'tsdata.qualmetadata.get', ... varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {tsdata.qualmetadata} {} set (@var{qi}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {tsdata.qualmetadata} {@var{qi2} =} set (@var{qi}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {tsdata.qualmetadata} {@var{S} =} set (@var{qi}) ## ## Set property values. ## ## @code{set (@var{qi}, @var{name}, @var{value}, @dots{})} sets each ## property @var{name}, matched in any case, to its @var{value}, in the ## order given, in the variable @var{qi} of the caller, which must ## therefore be a variable. Metadata held by a @code{timeseries} is set ## with dot assignment instead, as ## @code{@var{ts}.TimeInfo.Units = 'days'}. ## ## @code{@var{qi2} = set (@var{qi}, @var{name}, @var{value}, @dots{})} ## returns the modified object and leaves @var{qi} as it was. ## @code{@var{S} = set (@var{qi})} returns what @code{get (@var{qi})} ## does. ## ## @seealso{tsdata.qualmetadata.get, timeseries.set} ## @end deftypefn function varargout = set (this, varargin) if (nargin == 1) varargout{1} = get (this); return; endif this = timeseries.propertySet (this, 'tsdata.qualmetadata.set', ... varargin{:}); if (nargout > 0) varargout{1} = this; return; endif ## With no output the caller's own variable is set, as in MATLAB name = inputname (1); if (isempty (name)) error (strcat ("tsdata.qualmetadata.set: with no output argument,", ... " QI must be a variable; set anything else", ... " with dot assignment.")); endif assignin ('caller', name, this); endfunction endmethods methods (Hidden) function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif disp (this); endfunction function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif fprintf ("\n qualmetadata with properties:\n\n"); names = {'Code', 'Description', 'UserData'}; for i = 1:numel (names) fprintf ("%15s: %s\n", names{i}, ... timeseries.dispValue (this.(names{i}))); endfor fprintf ("\n"); endfunction endmethods endclassdef ## Test the defaults %!test %! qi = tsdata.qualmetadata (); %! assert_equal (qi.Code, []); %! assert_equal (qi.Description, []); %! assert_equal (qi.UserData, []); %!test %! assert_equal (properties (tsdata.qualmetadata ()), ... %! {'Code'; 'Description'; 'UserData'}); ## Test 'Code' %!test %! qi = tsdata.qualmetadata (); %! qi.Code = [0, 1]; %! assert_equal (qi.Code, [0, 1]); %!test %! qi = tsdata.qualmetadata (); %! qi.Code = [0; 1]; %! assert_equal (qi.Code, [0; 1]); %!test %! qi = tsdata.qualmetadata (); %! qi.Code = int8 ([-128, 127]); %! assert_equal (qi.Code, [-128, 127]); %!test %! qi = tsdata.qualmetadata (); %! qi.Code = [0, 1]; %! qi.Code = []; %! assert_equal (qi.Code, []); ## Test 'Description' %!test %! qi = tsdata.qualmetadata (); %! qi.Description = {'good', 'bad'}; %! assert_equal (qi.Description, {'good', 'bad'}); %!test %! qi = tsdata.qualmetadata (); %! qi.Description = 'good'; %! assert_equal (qi.Description, {'good'}); %!test %! qi = tsdata.qualmetadata (); %! qi.Description = string ({'good', 'bad'}); %! assert_equal (qi.Description, {'good', 'bad'}); %!test %! qi = tsdata.qualmetadata (); %! qi.Description = {'good'}; %! qi.Description = []; %! assert_equal (qi.Description, []); ## Test 'Code' and 'Description' may differ in length while being assigned %!test %! qi = tsdata.qualmetadata (); %! qi.Code = [0, 1]; %! assert_equal (qi.Description, []); %! qi.Description = {'good', 'bad'}; %! assert_equal (numel (qi.Description), 2); ## Test 'UserData' %!test %! qi = tsdata.qualmetadata (); %! qi.UserData = struct ('a', 1); %! assert_equal (qi.UserData, struct ('a', 1)); ## Test 'get' %!test %! S = get (tsdata.qualmetadata ()); %! assert_equal (fieldnames (S), {'Code'; 'Description'; 'UserData'}); %!test %! assert_equal (get (tsdata.qualmetadata (), 'Code'), []); %! assert_equal (get (tsdata.qualmetadata (), 'code'), []); %!test %! assert_equal (get (tsdata.qualmetadata (), {'Code'}), {[]}); ## Test 'set' with no output sets the caller's variable %!test %! obj = tsdata.qualmetadata (); %! set (obj, 'Code', [0, 1]); %! assert_equal (obj.Code, [0, 1]); %!test %! obj = tsdata.qualmetadata (); %! set (obj, 'CODE', [0, 1]); %! assert_equal (obj.Code, [0, 1]); ## Test 'set' with an output leaves its input as it was %!test %! obj = tsdata.qualmetadata (); %! obj2 = set (obj, 'Code', [0, 1]); %! assert_equal (obj2.Code, [0, 1]); %! assert_equal (obj.Code, []); %!test %! obj = tsdata.qualmetadata (); %! assert_equal (fieldnames (set (obj)), fieldnames (get (obj))); %!error ... %! setfield (tsdata.qualmetadata (), 'Code', [0.5, 1]) %!error ... %! setfield (tsdata.qualmetadata (), 'Code', [200, 1]) %!error ... %! setfield (tsdata.qualmetadata (), 'Code', 'ab') %!error ... %! setfield (tsdata.qualmetadata (), 'Code', [0, 1; 2, 3]) %!error ... %! setfield (tsdata.qualmetadata (), 'Code', [1i, 1]) %!error ... %! setfield (tsdata.qualmetadata (), 'Code', [0, 0]) %!error ... %! setfield (tsdata.qualmetadata (), 'Description', 5) %!error ... %! setfield (tsdata.qualmetadata (), 'Description', {1, 'a'}) %!error ... %! setfield (tsdata.qualmetadata (), 'Description', {'a', 'b'; 'c', 'd'}) %!error ... %! get (tsdata.qualmetadata (), 'Code', 'Code') %!error get (tsdata.qualmetadata (), 'Bogus') %!error ... %! get (tsdata.qualmetadata (), 5) %!error ... %! set (tsdata.qualmetadata (), 'Code', [0, 1]) %!error ... %! obj = tsdata.qualmetadata (); set (obj, 'Code'); %!error ... %! obj = tsdata.qualmetadata (); set (obj, 'Bogus', 1); %!error ... %! obj = tsdata.qualmetadata (); set (obj, 5, 1); pr0m1th3as-datatypes-9450b29/inst/+tsdata/timemetadata.m000066400000000000000000000720651527370255000230350ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftp {datatypes} tsdata.timemetadata ## ## The time metadata of a @code{timeseries}. ## ## A @code{tsdata.timemetadata} object describes the time vector of a ## @code{timeseries}: its units, an optional absolute start date, and the ## length, first and last times and uniform step derived from it. It is what ## @qcode{@var{ts}.TimeInfo} returns. ## ## The derived properties @qcode{Length}, @qcode{Start}, @qcode{End} and ## @qcode{Increment} follow the time vector and cannot be assigned; change ## the time vector of the series instead, or use @code{setuniformtime}. ## ## @end deftp classdef timemetadata properties ## -*- texinfo -*- ## @deftp {tsdata.timemetadata} {property} Units ## ## The units of the time vector. ## ## One of @qcode{'weeks'}, @qcode{'days'}, @qcode{'hours'}, ## @qcode{'minutes'}, @qcode{'seconds'}, @qcode{'milliseconds'}, ## @qcode{'microseconds'} or @qcode{'nanoseconds'}, in any case, stored in ## lower case; @qcode{'seconds'} by default. Assigning it relabels the ## time vector and does not rescale it. MATLAB accepts any value here. ## ## @end deftp Units = 'seconds' ## -*- texinfo -*- ## @deftp {tsdata.timemetadata} {property} UserData ## ## Any data the user attaches, @code{[]} by default. ## ## @end deftp UserData = [] ## -*- texinfo -*- ## @deftp {tsdata.timemetadata} {property} Format ## ## The display format of absolute times. ## ## A @code{datestr} format as a character vector, @qcode{''} by default, ## which @code{getabstime} writes the dates in. Text holding no date field ## at all, such as @qcode{'bogus'}, is refused, where MATLAB accepts any ## value; MATLAB also ignores every format but the few its documentation ## lists, while here any @code{datestr} format is used. ## ## @end deftp Format = '' ## -*- texinfo -*- ## @deftp {tsdata.timemetadata} {property} StartDate ## ## The absolute date the time vector counts from. ## ## A date as a character vector, or @qcode{''} for a relative time vector, ## by default. Text, a string scalar or a scalar @code{datetime} is ## stored in one form, @qcode{'dd-mmm-yyyy HH:MM:SS'}, with milliseconds ## as @qcode{'.FFF'} when the seconds are not whole, so a date is read ## once, when it is assigned, and never guessed at again. Text that is not ## a whole date (a year, a time of day) is refused, where MATLAB accepts ## any value and keeps the text as given. ## ## @end deftp StartDate = '' endproperties properties (Dependent) ## -*- texinfo -*- ## @deftp {tsdata.timemetadata} {property} Length ## ## The number of times in the time vector. Read-only. ## ## @end deftp Length ## -*- texinfo -*- ## @deftp {tsdata.timemetadata} {property} End ## ## The last time, or @code{[]} for an empty time vector. Read-only. ## ## @end deftp End ## -*- texinfo -*- ## @deftp {tsdata.timemetadata} {property} Increment ## ## The step of a uniform time vector. ## ## The step when every one is exactly equal and not zero, @code{NaN} ## otherwise and for a single time, @code{[]} for an empty time vector. ## Read-only; use @code{setuniformtime} to give a series a uniform time ## vector. MATLAB still accepts an assignment here, with a warning that ## it will not in future, and rebuilds the time vector from it. ## ## @end deftp Increment ## -*- texinfo -*- ## @deftp {tsdata.timemetadata} {property} Start ## ## The first time, or @code{[]} for an empty time vector. Read-only. ## MATLAB still accepts an assignment here, with a warning that it will ## not in future, and ignores it. ## ## @end deftp Start endproperties ## The time vector the derived properties are read from. A series sets ## its own on every read of 'TimeInfo', so assigning it here changes no ## series. It is not restricted to 'timeseries' by an access list, since ## resolving that list would load 'timeseries', whose defaults construct ## this class while it is still being defined. properties (Hidden) TimeVector = zeros (0, 1) endproperties methods ## -*- texinfo -*- ## @deftypefn {tsdata.timemetadata} {@var{ti} =} tsdata.timemetadata () ## @deftypefnx {tsdata.timemetadata} {@var{ti} =} tsdata.timemetadata (@var{time}) ## ## Create a time metadata object. ## ## @code{@var{ti} = tsdata.timemetadata ()} returns an object for an empty ## time vector: @qcode{Units} @qcode{'seconds'}, @qcode{Format} and ## @qcode{StartDate} @qcode{''}, @qcode{Length} 0, and @qcode{Start}, ## @qcode{End} and @qcode{Increment} @code{[]}. ## ## @code{@var{ti} = tsdata.timemetadata (@var{time})} derives ## @qcode{Length}, @qcode{Start}, @qcode{End} and @qcode{Increment} from ## the numeric vector @var{time}, which must be finite and non-decreasing. ## MATLAB gives an @qcode{Increment} of @code{NaN} here even for a uniform ## @var{time}; here it is the step, as for a @code{timeseries}. ## ## A @code{timeseries} creates its own; this is needed only to assign a ## whole new one to @qcode{@var{ts}.TimeInfo}, whose time vector is then ## the series' own. ## ## @seealso{timeseries} ## @end deftypefn function this = timemetadata (time) if (nargin == 0) return; endif this.TimeVector = time; endfunction function this = set.TimeVector (this, time) if (! (isnumeric (time) && isreal (time) && (isvector (time) || isempty (time)))) error ("tsdata.timemetadata: TIME must be a numeric vector."); endif time = double (time(:)); if (! all (isfinite (time))) error ("tsdata.timemetadata: TIME must be finite."); endif if (any (diff (time) < 0)) error ("tsdata.timemetadata: TIME must be non-decreasing."); endif this.TimeVector = time; endfunction function this = set.Units (this, val) [val, errmsg] = tsdata.timemetadata.unitsValue (val, false); if (! isempty (errmsg)) error ("tsdata.timemetadata: %s", errmsg); endif this.Units = val; endfunction function this = set.Format (this, val) if (isstring (val) && isscalar (val)) val = char (val); endif if (! (ischar (val) && (isrow (val) || isempty (val)))) error ("tsdata.timemetadata: 'Format' must be a character vector."); endif if (isempty (val)) val = ''; elseif (isempty (regexp (val, '[ymdHMSF]|AM|PM', 'once'))) error (strcat ("tsdata.timemetadata: 'Format' holds no date", ... " field: '%s'"), val); endif this.Format = val; endfunction function this = set.StartDate (this, val) [val, errmsg] = tsdata.timemetadata.dateValue (val, "'StartDate'"); if (! isempty (errmsg)) error ("tsdata.timemetadata: %s", errmsg); endif this.StartDate = val; endfunction function val = get.Length (this) val = numel (this.TimeVector); endfunction function this = set.Length (this, val) error ("tsdata.timemetadata: 'Length' is read-only."); endfunction function val = get.Start (this) if (isempty (this.TimeVector)) val = []; else val = this.TimeVector(1); endif endfunction function this = set.Start (this, val) error (strcat ("tsdata.timemetadata: 'Start' is read-only; use", ... " 'setuniformtime' to give a series a uniform time", ... " vector.")); endfunction function val = get.End (this) if (isempty (this.TimeVector)) val = []; else val = this.TimeVector(end); endif endfunction function this = set.End (this, val) error (strcat ("tsdata.timemetadata: 'End' is read-only; use", ... " 'setuniformtime' to give a series a uniform time", ... " vector.")); endfunction ## MATLAB sets the increment only when every step is exactly equal, so ## 0:0.1:1, whose steps differ in the last bit, has none. Measured on ## R2024a; reproduced as it stands. function val = get.Increment (this) t = this.TimeVector; if (isempty (t)) val = []; return; endif d = diff (t); if (! isempty (d) && d(1) != 0 && all (d == d(1))) val = d(1); else val = NaN; endif endfunction function this = set.Increment (this, val) error (strcat ("tsdata.timemetadata: 'Increment' is read-only; use", ... " 'setuniformtime' to give a series a uniform time", ... " vector.")); endfunction ## -*- texinfo -*- ## @deftypefn {tsdata.timemetadata} {@var{S} =} get (@var{ti}) ## @deftypefnx {tsdata.timemetadata} {@var{value} =} get (@var{ti}, @var{name}) ## @deftypefnx {tsdata.timemetadata} {@var{values} =} get (@var{ti}, @var{names}) ## ## Return property values. ## ## @code{@var{S} = get (@var{ti})} returns a structure of every ## property of @var{ti}. @code{@var{value} = get (@var{ti}, ## @var{name})} returns the property @var{name}, a character vector or a ## string scalar matched in any case, and @code{@var{values} = get ## (@var{ti}, @var{names})} a row cell array of the properties named in ## the cell array @var{names}. For an array, one @var{name} gives a cell ## array of its size, and no name, or several, a cell array with a row ## per object and a column per property. ## ## @seealso{tsdata.timemetadata.set, timeseries.get} ## @end deftypefn function out = get (this, varargin) out = timeseries.propertyGet (this, 'tsdata.timemetadata.get', ... varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {tsdata.timemetadata} {} set (@var{ti}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {tsdata.timemetadata} {@var{ti2} =} set (@var{ti}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {tsdata.timemetadata} {@var{S} =} set (@var{ti}) ## ## Set property values. ## ## @code{set (@var{ti}, @var{name}, @var{value}, @dots{})} sets each ## property @var{name}, matched in any case, to its @var{value}, in the ## order given, in the variable @var{ti} of the caller, which must ## therefore be a variable. Metadata held by a @code{timeseries} is set ## with dot assignment instead, as ## @code{@var{ts}.TimeInfo.Units = 'days'}. ## ## @code{@var{ti2} = set (@var{ti}, @var{name}, @var{value}, @dots{})} ## returns the modified object and leaves @var{ti} as it was. ## @code{@var{S} = set (@var{ti})} returns what @code{get (@var{ti})} ## does. ## ## @seealso{tsdata.timemetadata.get, timeseries.set} ## @end deftypefn function varargout = set (this, varargin) if (nargin == 1) varargout{1} = get (this); return; endif this = timeseries.propertySet (this, 'tsdata.timemetadata.set', ... varargin{:}); if (nargout > 0) varargout{1} = this; return; endif ## With no output the caller's own variable is set, as in MATLAB name = inputname (1); if (isempty (name)) error (strcat ("tsdata.timemetadata.set: with no output argument,", ... " TI must be a variable; set anything else", ... " with dot assignment.")); endif assignin ('caller', name, this); endfunction endmethods methods (Static, Hidden) ## Validate a time unit. Returns it in lower case and an empty ERRMSG, ## or the body of the message the caller raises under its own name. ## An empty unit is valid only where ALLOWEMPTY is true. function [val, errmsg] = unitsValue (val, allowEmpty) errmsg = ''; if (isstring (val) && isscalar (val)) val = char (val); endif if (allowEmpty && ischar (val) && isempty (val)) val = ''; return; endif units = {'weeks', 'days', 'hours', 'minutes', 'seconds', ... 'milliseconds', 'microseconds', 'nanoseconds'}; if (! (ischar (val) && isrow (val) && any (strcmpi (val, units)))) errmsg = strcat ("'Units' must be 'weeks', 'days', 'hours',", ... " 'minutes', 'seconds', 'milliseconds',", ... " 'microseconds' or 'nanoseconds'."); return; endif val = lower (val); endfunction ## Validate a date given as text or as a scalar datetime. LABEL names ## it in a message as printed: a quoted property or a bare argument. ## Returns it in the stored form of 'dateText', '' for an empty one, and ## an empty ERRMSG, or the body of the message the caller raises. function [val, errmsg] = dateValue (val, label) errmsg = ''; if (isa (val, 'datetime') && isscalar (val)) if (isnat (val)) errmsg = sprintf ("%s must not be NaT.", label); return; endif dv = datevec (val); if (dv(1) < 0 || dv(1) > 9999) errmsg = sprintf ("%s must be a date from the year 0 to 9999.", ... label); return; endif val = tsdata.timemetadata.dateText (dv); return; elseif (isstring (val) && isscalar (val)) val = char (val); endif if (! (ischar (val) && (isrow (val) || isempty (val)))) errmsg = sprintf ("%s must be a date as a character vector.", label); return; endif if (isempty (val)) val = ''; return; endif [dv, ok] = tsdata.timemetadata.dateVector (val); if (! ok) errmsg = sprintf ("%s is not a date: '%s'", label, val); return; endif val = tsdata.timemetadata.dateText (dv); endfunction ## Read the date TXT into a date vector: the stored form of 'dateText' ## exactly, any other text as 'datevec' reads it. OK is false for text ## that is not a whole date read as written: 'datevec' reads '2024' as ## 31 December 2023, a time of day alone on the current date, '05-Mar-24' ## in the year 24 and '29-Feb-2023' as 1 March, so the year must be ## written in four digits and the day, month and time read must be those ## the text holds. function [dv, ok] = dateVector (txt) dv = []; ok = false; months = {'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', ... 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'}; tok = regexp (txt, ['^(\d{2})-([A-Za-z]{3})-(\d{4}) ', ... '(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)$'], ... 'tokens', 'once'); if (! isempty (tok)) m = find (strcmpi (tok{2}, months)); if (! isempty (m)) dv = [str2double(tok{3}), m, str2double(tok{1}), ... str2double(tok{4}), str2double(tok{5}), str2double(tok{6})]; ok = inRange (dv); return; endif endif if (! isempty (regexp (txt, '^\s*\d{1,2}:\d{2}', 'once'))) return; endif try dv = datevec (txt); catch return; end_try_catch words = regexp (txt, '\d+|[A-Za-z]+', 'match'); isNum = cellfun (@(w) isdigit (w(1)), words); nums = str2double (words(isNum)); years = str2double (words(isNum & cellfun (@numel, words) == 4)); names = lower (words(! isNum)); month = any (nums == dv(2)) ... || any (strncmp (names, lower (months{max (1, dv(2))}), 3)); hour = dv(4) == 0 || any (nums == dv(4)) || any (nums == dv(4) - 12); ok = any (years == dv(1)) && any (nums == dv(3)) && month && hour ... && (dv(5) == 0 || any (nums == dv(5))) && inRange (dv); endfunction ## The date vector DV in the stored form, 'dd-mmm-yyyy HH:MM:SS', with ## the milliseconds as '.FFF' when the seconds are not whole. function txt = dateText (dv) dv = dv(1,:); day = datenum (dv(1), dv(2), dv(3)); ms = round ((dv(4) * 3600 + dv(5) * 60 + dv(6)) * 1000); day += floor (ms / 864e5); ms = mod (ms, 864e5); txt = sprintf ("%s %02d:%02d:%02d", datestr (day, 'dd-mmm-yyyy'), ... floor (ms / 36e5), floor (mod (ms, 36e5) / 6e4), ... floor (mod (ms, 6e4) / 1e3)); if (mod (ms, 1e3) != 0) txt = sprintf ("%s.%03d", txt, mod (ms, 1e3)); endif endfunction ## The time from the date vector DV0 to each row of DV, in nanoseconds: ## whole days and the seconds of the day apart, so a whole number of ## seconds is exact. function ns = dateOffset (dv, dv0) days = datenum (dv(:,1), dv(:,2), dv(:,3)) ... - datenum (dv0(1), dv0(2), dv0(3)); secs = (dv(:,4) - dv0(4)) * 3600 + (dv(:,5) - dv0(5)) * 60 ... + (dv(:,6) - dv0(6)); ns = days * 864e11 + secs * 1e9; endfunction endmethods methods (Hidden) function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif fprintf ("\n tsdata.timemetadata\n\n"); fprintf ("%s", summaryText (this)); fprintf (" Common Properties:\n"); fprintf (" Units: '%s'\n", this.Units); fprintf (" Format: '%s'\n", this.Format); fprintf (" StartDate: '%s'\n\n", this.StartDate); endfunction function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif fprintf ("\n timemetadata with properties:\n\n"); names = {'Units', 'UserData', 'Format', 'StartDate', 'Length', ... 'End', 'Increment', 'Start'}; for i = 1:numel (names) fprintf ("%13s: %s\n", names{i}, ... timeseries.dispValue (this.(names{i}))); endfor fprintf ("\n"); endfunction ## The time summary MATLAB shows before the common properties, also used ## by 'timeseries'. function txt = summaryText (this) if (this.Length == 0) txt = " Empty timeseries TimeMetaData object.\n\n"; return; endif u = this.Units; if (isnan (this.Increment)) txt = sprintf (" Non-Uniform Time:\n Length %d\n\n", ... this.Length); else txt = sprintf (strcat (" Uniform Time:\n Length %d\n", ... " Increment %g %s\n\n"), ... this.Length, this.Increment, u); endif txt = [txt, sprintf(" Time Range:\n Start %g %s\n", ... this.Start, u), ... sprintf(" End %g %s\n\n", this.End, u)]; endfunction endmethods endclassdef ## True when every field of the date vector DV is in its range, the day ## within its month, so that nothing rolls over into the next. function tf = inRange (dv) tf = dv(1) >= 0 && dv(1) <= 9999 && dv(2) >= 1 && dv(2) <= 12 ... && dv(3) >= 1 && dv(3) <= eomday (dv(1), dv(2)) ... && dv(4) >= 0 && dv(4) < 24 && dv(5) >= 0 && dv(5) < 60 ... && dv(6) >= 0 && dv(6) < 60; endfunction ## Test the defaults %!test %! ti = tsdata.timemetadata (); %! assert_equal (ti.Units, 'seconds'); %! assert_equal (ti.UserData, []); %! assert_equal (ti.Format, ''); %! assert_equal (ti.StartDate, ''); %! assert_equal (ti.Length, 0); %! assert_equal (ti.End, []); %! assert_equal (ti.Increment, []); %! assert_equal (ti.Start, []); %!test %! assert_equal (properties (tsdata.timemetadata ()), {'Units'; ... %! 'UserData'; 'Format'; 'StartDate'; 'Length'; 'End'; ... %! 'Increment'; 'Start'}); ## Test a time vector derives the read-only properties %!test %! ti = tsdata.timemetadata ([1, 2, 3]); %! assert_equal (ti.Length, 3); %! assert_equal (ti.Start, 1); %! assert_equal (ti.End, 3); %! assert_equal (ti.Increment, 1); %!test %! ti = tsdata.timemetadata ([1; 2; 4]); %! assert_equal (ti.Length, 3); %! assert_equal (ti.End, 4); %! assert_equal (ti.Increment, NaN); %!test %! ti = tsdata.timemetadata (5); %! assert_equal (ti.Length, 1); %! assert_equal (ti.Start, 5); %! assert_equal (ti.End, 5); %! assert_equal (ti.Increment, NaN); %!test %! ti = tsdata.timemetadata ([]); %! assert_equal (ti.Length, 0); %! assert_equal (ti.Increment, []); %!test %! ti = tsdata.timemetadata (int8 ([0, 2, 4])); %! assert_equal (ti.Increment, 2); %! assert_equal (class (ti.Start), 'double'); ## Test 'Increment' needs every step exactly equal and not zero %!test %! ti = tsdata.timemetadata (0:0.1:1); %! assert_equal (ti.Increment, NaN); %!test %! ti = tsdata.timemetadata ([0, 1, 2 + 1e-14, 3]); %! assert_equal (ti.Increment, NaN); %!test %! ti = tsdata.timemetadata ([0, 0, 0]); %! assert_equal (ti.Increment, NaN); %!test %! ti = tsdata.timemetadata ([0, 0.5, 1]); %! assert_equal (ti.Increment, 0.5); ## Test 'Units' %!test %! ti = tsdata.timemetadata (); %! ti.Units = 'minutes'; %! assert_equal (ti.Units, 'minutes'); %!test %! ti = tsdata.timemetadata (); %! ti.Units = 'Minutes'; %! assert_equal (ti.Units, 'minutes'); %!test %! ti = tsdata.timemetadata (); %! ti.Units = string ('days'); %! assert_equal (ti.Units, 'days'); %!test %! ti = tsdata.timemetadata ([0, 1, 2]); %! ti.Units = 'hours'; %! assert_equal (ti.End, 2); ## Test 'Format' %!test %! ti = tsdata.timemetadata (); %! ti.Format = 'dd-mmm-yyyy'; %! assert_equal (ti.Format, 'dd-mmm-yyyy'); %!test %! ti = tsdata.timemetadata (); %! ti.Format = 'dd-mmm-yyyy'; %! ti.Format = ''; %! assert_equal (ti.Format, ''); ## Test 'StartDate' %!test %! ti = tsdata.timemetadata (); %! ti.StartDate = '01-Jan-2024'; %! assert_equal (ti.StartDate, '01-Jan-2024 00:00:00'); %!test %! ti = tsdata.timemetadata (); %! ti.StartDate = string ('01-Jan-2024 06:00:00'); %! assert_equal (ti.StartDate, '01-Jan-2024 06:00:00'); %!test %! ti = tsdata.timemetadata (); %! ti.StartDate = datetime (2024, 1, 1, 6, 0, 0); %! assert_equal (ti.StartDate, '01-Jan-2024 06:00:00'); ## Test a start date is stored in one form, milliseconds kept %!test %! ti = tsdata.timemetadata (); %! ti.StartDate = datetime (2024, 1, 1, 6, 0, 0.25); %! assert_equal (ti.StartDate, '01-Jan-2024 06:00:00.250'); %! ti.StartDate = '01-Jan-2024 06:00:00.250'; %! assert_equal (ti.StartDate, '01-Jan-2024 06:00:00.250'); %! ti.StartDate = '2024-03-05'; %! assert_equal (ti.StartDate, '05-Mar-2024 00:00:00'); %! ti.StartDate = '31-Dec-2024 23:59:59.9996'; %! assert_equal (ti.StartDate, '01-Jan-2025 00:00:00'); %!test %! ti = tsdata.timemetadata (); %! ti.StartDate = '01-Jan-2024'; %! ti.StartDate = ''; %! assert_equal (ti.StartDate, ''); ## Test 'UserData' %!test %! ti = tsdata.timemetadata (); %! ti.UserData = 'note'; %! assert_equal (ti.UserData, 'note'); ## Test value semantics %!test %! a = tsdata.timemetadata ([1, 2]); %! b = a; %! b.Units = 'days'; %! assert_equal (a.Units, 'seconds'); ## Test 'get' %!test %! S = get (tsdata.timemetadata ([1, 2, 3])); %! assert_equal (fieldnames (S), {'Units'; 'UserData'; 'Format'; ... %! 'StartDate'; 'Length'; 'End'; 'Increment'; 'Start'}); %!test %! assert_equal (get (tsdata.timemetadata ([1, 2, 3]), 'Units'), 'seconds'); %! assert_equal (get (tsdata.timemetadata ([1, 2, 3]), 'units'), 'seconds'); %!test %! assert_equal (get (tsdata.timemetadata ([1, 2, 3]), {'Units'}), {'seconds'}); ## Test 'set' with no output sets the caller's variable %!test %! obj = tsdata.timemetadata ([1, 2, 3]); %! set (obj, 'Units', 'days'); %! assert_equal (obj.Units, 'days'); %!test %! obj = tsdata.timemetadata ([1, 2, 3]); %! set (obj, 'UNITS', 'days'); %! assert_equal (obj.Units, 'days'); ## Test 'set' with an output leaves its input as it was %!test %! obj = tsdata.timemetadata ([1, 2, 3]); %! obj2 = set (obj, 'Units', 'days'); %! assert_equal (obj2.Units, 'days'); %! assert_equal (obj.Units, 'seconds'); %!test %! obj = tsdata.timemetadata ([1, 2, 3]); %! assert_equal (fieldnames (set (obj)), fieldnames (get (obj))); %!error ... %! tsdata.timemetadata ('abc') %!error ... %! tsdata.timemetadata (ones (2, 2)) %!error ... %! tsdata.timemetadata ([1, 2i]) %!error ... %! tsdata.timemetadata ([1, NaN]) %!error ... %! tsdata.timemetadata ([1, Inf]) %!error ... %! tsdata.timemetadata ([2, 1]) %!error ... %! setfield (tsdata.timemetadata (), 'Units', 'bogus') %!error ... %! setfield (tsdata.timemetadata (), 'Units', 'years') %!error ... %! setfield (tsdata.timemetadata (), 'Units', 5) %!error ... %! setfield (tsdata.timemetadata (), 'Format', 5) %!error ... %! setfield (tsdata.timemetadata (), 'Format', 'bogus') %!error ... %! setfield (tsdata.timemetadata (), 'StartDate', 5) %!error ... %! setfield (tsdata.timemetadata (), 'StartDate', 'bogus') %!error ... %! setfield (tsdata.timemetadata (), 'StartDate', '2024') %!error ... %! setfield (tsdata.timemetadata (), 'StartDate', '1') %!error ... %! setfield (tsdata.timemetadata (), 'StartDate', '13:00') %!error ... %! setfield (tsdata.timemetadata (), 'StartDate', 'March 2024') %!error ... %! setfield (tsdata.timemetadata (), 'StartDate', '05-Mar-24') %!error ... %! setfield (tsdata.timemetadata (), 'StartDate', '29-Feb-2023') %!error ... %! setfield (tsdata.timemetadata (), 'StartDate', '05-Mar-2024 25:00:00') %!error ... %! setfield (tsdata.timemetadata (), 'StartDate', datetime (12000, 1, 1)) %!error ... %! setfield (tsdata.timemetadata (), 'StartDate', NaT) %!error ... %! setfield (tsdata.timemetadata (), 'Length', 5) %!error ... %! setfield (tsdata.timemetadata (), 'Start', 5) %!error ... %! setfield (tsdata.timemetadata (), 'End', 5) %!error ... %! setfield (tsdata.timemetadata (), 'Increment', 2) %!error ... %! get (tsdata.timemetadata ([1, 2, 3]), 'Units', 'Units') %!error get (tsdata.timemetadata ([1, 2, 3]), 'Bogus') %!error ... %! get (tsdata.timemetadata ([1, 2, 3]), 5) %!error ... %! set (tsdata.timemetadata ([1, 2, 3]), 'Units', 'days') %!error ... %! obj = tsdata.timemetadata ([1, 2, 3]); set (obj, 'Units'); %!error ... %! obj = tsdata.timemetadata ([1, 2, 3]); set (obj, 'Bogus', 1); %!error ... %! obj = tsdata.timemetadata ([1, 2, 3]); set (obj, 5, 1); pr0m1th3as-datatypes-9450b29/inst/@cell/000077500000000000000000000000001527370255000176725ustar00rootroot00000000000000pr0m1th3as-datatypes-9450b29/inst/@cell/disp.m000066400000000000000000000100521527370255000210050ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {cell} {} disp (@var{C}) ## @deftypefnx {cell} {@var{str} =} disp (@var{C}) ## ## Customized display of cell arrays. ## ## This function overloads Octave core @code{disp} with respect to cell arrays ## and displays their contents in MATLAB like manner. ## ## @code{@var{str} = disp (@var{C})} returns the text instead of printing it, ## as Octave core @code{disp} does. ## ## @end deftypefn function str = disp (C) if (nargout > 0) str = evalc ('disp (C)'); return; endif __disp__ (C); endfunction ## These overloads are what make a cell array display in the MATLAB form ## rather than Octave's own, which is the package's most visible side effect. ## The rendering itself is done by the private '__disp__'; what this file ## adds is that nothing names the variable. ## Test a scalar cell prints the header, then the element in braces %!test %! C = {1}; %! want = " 1x1 cell array\n\n {[1]} \n\n"; %! assert_equal (evalc ('disp (C)'), want); ## Test a row of two elements is laid out on one line %!test %! C = {1, 2}; %! want = " 1x2 cell array\n\n {[1]} {[2]} \n\n"; %! assert_equal (evalc ('disp (C)'), want); ## Test a column of two elements takes a line each %!test %! C = {1; 2}; %! want = " 2x1 cell array\n\n {[1]} \n {[2]} \n\n"; %! assert_equal (evalc ('disp (C)'), want); ## Test text is quoted where a number is bracketed %!test %! C = {'ab'}; %! want = " 1x1 cell array\n\n {'ab'} \n\n"; %! assert_equal (evalc ('disp (C)'), want); ## Test an empty cell array says so %!test %! want = "\n 0x0 empty cell array\n\n"; %! assert_equal (evalc ('disp (cell (0, 0))'), want); %!test # a text too wide for the terminal is cut, the text beside it is not %! C = {repmat('a', 1, 500); 'categorical'}; %! out = evalc ('disp (C)'); %! assert_equal (isempty (strfind (out, 'a ... ''')), false); %! assert_equal (isempty (strfind (out, '''categorical''')), false); ## Test 'disp' names nothing, so it prints what 'display' prints for a value ## that has no name of its own %!test %! C = {1, 2}; %! assert_equal (evalc ('disp (C)'), evalc ('display ({1, 2})')); %!test %! C = {@sin}; %! want = " 1x1 cell array\n\n {@sin} \n\n"; %! assert_equal (evalc ('disp (C)'), want); %!test %! C = {@(x) x + 1}; %! want = " 1x1 cell array\n\n {@(x) x + 1} \n\n"; %! assert_equal (evalc ('disp (C)'), want); %!test # function handles align right, without brackets %! C = {@sin; @(x) x + 1}; %! want = " 2x1 cell array\n\n { @sin} \n {@(x) x + 1} \n\n"; %! assert_equal (evalc ('disp (C)'), want); %!test %! C = {1+2i}; %! want = " 1x1 cell array\n\n {[1+2i]} \n\n"; %! assert_equal (evalc ('disp (C)'), want); %!test %! C = {1-2.5i}; %! want = " 1x1 cell array\n\n {[1-2.5i]} \n\n"; %! assert_equal (evalc ('disp (C)'), want); %!test %! C = {[1+2i, 3]}; %! want = " 1x1 cell array\n\n {[1+2i 3+0i]} \n\n"; %! assert_equal (evalc ('disp (C)'), want); %!test %! C = {complex(1, -0)}; %! want = " 1x1 cell array\n\n {[1+0i]} \n\n"; %! assert_equal (evalc ('disp (C)'), want); %!test %! C = {1, 'ab'}; %! assert_equal (disp (C), " 1x2 cell array\n\n {[1]} {'ab'} \n\n"); %!test %! C = {1, 'ab'}; %! assert_equal (disp (C), evalc ('disp (C)')); %!assert_equal (disp (cell (0, 0)), "\n 0x0 empty cell array\n\n") pr0m1th3as-datatypes-9450b29/inst/@cell/display.m000066400000000000000000000040231527370255000215140ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {cell} {} display (@var{C}) ## ## Customized display of cell arrays. ## ## This function overloads Octave core @code{display} with respect to cell ## arrays and displays their contents in MATLAB like manner. ## ## @end deftypefn function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif __disp__ (this, in_name); endfunction ## This overload adds the name line to what 'disp' prints; the rendering ## itself is done by the private '__disp__'. ## Test a named variable is announced before its contents %!test %! x = {1, 2}; %! want = "x =\n 1x2 cell array\n\n {[1]} {[2]} \n\n"; %! assert_equal (evalc ('display (x)'), want); ## Test the name is the variable's own, whatever it is %!test %! myCells = {1}; %! want = "myCells =\n 1x1 cell array\n\n {[1]} \n\n"; %! assert_equal (evalc ('display (myCells)'), want); ## Test a value with no name of its own is printed without a name line %!test %! want = " 1x2 cell array\n\n {[1]} {[2]} \n\n"; %! assert_equal (evalc ('display ({1, 2})'), want); ## Test what follows the name line is exactly what 'disp' prints %!test %! x = {1, 2}; %! p = evalc ('display (x)'); %! assert_equal (p(5:end), evalc ('disp (x)')); pr0m1th3as-datatypes-9450b29/inst/@cell/private/000077500000000000000000000000001527370255000213445ustar00rootroot00000000000000pr0m1th3as-datatypes-9450b29/inst/@cell/private/__disp__.m000066400000000000000000000266751527370255000232750ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . function __disp__ (C, name = 'ans') if (isempty (C)) str = strjoin (repmat ({'%d'}, 1, ndims (C)), 'x'); str = sprintf ('\n %s empty cell array\n\n', str); fprintf (str, size (C)); elseif (ismatrix (C)) fprintf (' %dx%d cell array\n\n', size (C)); dispcellmatrix (C); else str = strjoin (repmat ({'%d'}, 1, ndims (C)), 'x'); str = sprintf ('\n %s cell array\n\n', str); fprintf (str, size (C)); ## Handle each page separately sz = size (C); high_sz = sz(3:end); high_ixs = {}; for i = 1:numel (high_sz) high_ixs{i} = [1:high_sz(i)]'; endfor page_ixs = combvec (high_ixs); for ix = 1:size (page_ixs, 1) p_ix = page_ixs(ix,:); pagestr = sprintf (strjoin (repmat ({'%d'}, 1, numel (p_ix)), ':'), p_ix); fprintf ('%s(:,:,%s) = \n\n', name, pagestr); page_C = C(:,:,p_ix); dispcellmatrix (page_C); endfor endif endfunction function dispcellmatrix (C) sz = terminal_size (); cols = sz(2) - 4; colgap = " "; dispstr = {}; optLens = []; for iCol = 1:size (C, 2) [outstr, optLen] = mixedcell2str (C(:, iCol), cols); dispstr = [dispstr, outstr]; optLens = [optLens, optLen]; endfor if (sum (optLens + 6) <= cols) # all columns fit in terminal size rowSpat = ""; for iCol = 1:size (C, 2) rowSpat = [rowSpat, sprintf("%%-%ds", optLens(iCol)), colgap]; endfor for iRow = 1:size (C, 1) strrow = sprintf (rowSpat, dispstr{iRow,:}); fprintf (" %s\n", strrow); endfor fprintf ("\n"); else # we need to split rows optLen_cs = cumsum (optLens + 6); startCol = 1; while (! isempty (find (optLen_cs > cols))) stopCol = find (optLen_cs > cols, 1) - 1; ## Just in case a single column exceeds terminal size if (stopCol == 0) stopCol = 1; endif rowSpat = ""; for iCol = 1:stopCol rowSpat = [rowSpat, sprintf("%%-%ds", optLens(iCol)), colgap]; endfor optLens(1:iCol) = []; optLen_cs = cumsum (optLens + 6); stopCol = stopCol + startCol - 1; fprintf ("Columns %d through %d:\n\n", startCol, stopCol); for iRow = 1:size (C, 1) strrow = sprintf (rowSpat, dispstr{iRow,[startCol:stopCol]}); fprintf (" %s\n", strrow); endfor fprintf ("\n"); startCol = stopCol + 1; endwhile if (! isempty (optLens)) for iCol = 1:length (optLens) rowSpat = [rowSpat, sprintf("%%-%ds", optLens(iCol)), colgap]; endfor stopCol = startCol + iCol - 1; if (startCol == stopCol) fprintf ("Column %d:\n\n", startCol); else fprintf ("Columns %d through %d:\n\n", startCol, stopCol); endif for iRow = 1:size (C, 1) strrow = sprintf (rowSpat, dispstr{iRow,[startCol:stopCol]}); fprintf (" %s\n", strrow); endfor fprintf ("\n"); endif endif endfunction ## Special function to convert a mixed cell array to cellstr array ## that keeps MATLAB like formatting for each type of element function [dispstr, optLen] = mixedcell2str (data, cols) dispstr = cell (size (data)); ## Preallocate indexes to avoid truncation when last elements are 0 is_char = logical (zeros (size (data))); is_bool = is_char; is_numeric = is_char; is_object = is_char; is_struct = is_char; has_method = is_char; no_method = is_char; is_fhandle = is_char; ## Nested cells are printed by size is_cell = cellfun ('iscell', data); sf = @(x) sprintf (strcat ([strjoin(repmat ({'%d'}, 1, ndims (x)), 'x'), ... ' cell']), size (x)); dispstr(is_cell) = cellfun (sf, data(is_cell), "UniformOutput", false); ## Empty cells are printed as doubles is_empty = cellfun (@isempty, data); sf = @(x) sprintf (strcat (repmat ("%dx", 1, ndims (x))(1:end-1), " %s"), ... size (x), class (x)); dispstr(is_empty) = cellfun (sf, data(is_empty), "UniformOutput", false); ## Index remaining scalar and row vector elements ve = cell2mat (cellfun (@(x) isrow (x), data, "UniformOutput", false)) == 1; ve = ve & ! (is_cell | is_empty); ## Index everything else me = cell2mat (cellfun (@(x) isrow (x), data, "UniformOutput", false)) != 1; me = me & ! (is_cell | is_empty); ## Catch 'char' scalars or row vectors is_char(ve) = cellfun ('ischar', data(ve)); sf = @(x) sprintf ("'%s'", x); dispstr(is_char) = cellfun (sf, data(is_char), "UniformOutput", false); ## Catch 'logical' scalars or row vectors is_bool(ve) = cellfun ('islogical', data(ve)); sf = @(x) sprintf ("%s", strtrim (sprintf ("%d ", x))); dispstr(is_bool) = cellfun (sf, data(is_bool), "UniformOutput", false); ## Catch 'numeric' scalars or row vectors is_numeric(ve) = cellfun ('isnumeric', data(ve)); sf = @(x) sprintf ("%s", strtrim (sprintf ("%g ", x))); dispstr(is_numeric) = cellfun (sf, data(is_numeric), "UniformOutput", false); ## Complex values with their imaginary parts; adding 0 unsigns a zero part is_complex = is_numeric; is_complex(is_numeric) = cellfun ('iscomplex', data(is_numeric)); sf = @(x) strtrim (sprintf ("%g%+gi ", [real(x(:).'); imag(x(:).')] + 0)); dispstr(is_complex) = cellfun (sf, data(is_complex), "UniformOutput", false); ## Catch 'object' scalars or row vectors is_object(ve) = cellfun ('isobject', data(ve)); ## Handle objects with dispstring method available f = @(x) ismethod (x, 'dispstrings'); has_method(is_object) = cellfun (f, data(is_object)); sf = @(x) sprintf ("%s", strjoin (dispstrings (x), ' ')); dispstr(has_method) = cellfun (sf, data(has_method), "UniformOutput", false); ## Handle objects without no_method(is_object) = ! cellfun (f, data(is_object)); sf = @(x) sprintf (strcat ([strjoin(repmat ({'%d'}, 1, ndims (x)), 'x'), ... ' %s']), size (x), class (x)); dispstr(no_method) = cellfun (sf, data(no_method), "UniformOutput", false); ## Catch scalar elements or row vectors of 'struct' type is_struct(ve) = cellfun ('isstruct', data(ve)); sf = @(x) sprintf (strcat ([strjoin(repmat ({'%d'}, 1, ndims (x)), 'x'), ... ' struct']), size (x)); dispstr(is_struct) = cellfun (sf, data(is_struct), "UniformOutput", false); ## Catch function handles, each written with its '@' is_fhandle(ve) = cellfun (@is_function_handle, data(ve)); sf = @(x) regexprep (func2str (x), '^([^@])', '@$1'); dispstr(is_fhandle) = cellfun (sf, data(is_fhandle), "UniformOutput", false); ## Catch remaining elements containing matrices or arrays of any type sf = @(x) sprintf (strcat ([strjoin(repmat ({'%d'}, 1, ndims (x)), 'x'), ... ' %s']), size (x), class (x)); dispstr(me) = cellfun (sf, data(me), "UniformOutput", false); ## Index numerical and logical values to right alignment pad_B = is_numeric | is_bool; # pad before: sprintf("{%%+%ds}" ## Index array types for bracketing brackets = pad_B | has_method; ## Get optimal length if (all (brackets)) optLen = max (cellfun (@length, dispstr(brackets))) + 2; elseif (any (brackets)) optLen1 = max (cellfun (@length, dispstr(brackets))) + 2; optLen2 = max (cellfun (@length, dispstr(! brackets))); optLen = max (optLen1, optLen2); else optLen = max (cellfun (@length, dispstr(! brackets))); endif ## Make sure a single column does not exceed terminal size if (optLen > cols) ## Truncate only a text too wide for the column, leaving shorter ones whole. for i = find (is_char)' if (numel (dispstr{i}) > cols) dispstr{i} = sprintf ("'%s ... '", data{i}(1:cols-10)); endif endfor for i = 1:sum (is_bool) hm_idx = find (is_bool)(i); do_idx = find (cumsum (cellfun ('length', strsplit (dispstr{hm_idx})) ... + 1) > cols - 10, 1) - 1; if (! isempty (do_idx)) sf = @(x) sprintf ("%s ... ", strtrim (sprintf ("%d ", x(1:do_idx)))); dispstr(hm_idx) = cellfun (sf, data(hm_idx), "UniformOutput", false); endif endfor for i = 1:sum (is_numeric) hm_idx = find (is_numeric)(i); do_idx = find (cumsum (cellfun ('length', strsplit (dispstr{hm_idx})) ... + 1) > cols - 8, 1) - 1; if (! isempty (do_idx)) sf = @(x) sprintf ("%s ... ", strtrim (sprintf ("%g ", x(1:do_idx)))); dispstr(hm_idx) = cellfun (sf, data(hm_idx), "UniformOutput", false); endif endfor for i = 1:sum (has_method) hm_idx = find (has_method)(i); do_idx = find (cumsum (cellfun ('length', strsplit (dispstr{hm_idx})) ... + 4) > cols - 6, 1) - 1; if (! isempty (do_idx)) sf = @(x) sprintf ("%s ... ", ... strjoin (dispstrings (x(1:do_idx)), ' ')); dispstr(hm_idx) = cellfun (sf, data(hm_idx), "UniformOutput", false); endif endfor ## Recalculate optimal length if (all (brackets)) optLen = max (cellfun (@length, dispstr(brackets))) + 2; elseif (any (brackets)) optLen1 = max (cellfun (@length, dispstr(brackets))) + 2; optLen2 = max (cellfun (@length, dispstr(! brackets))); optLen = max (optLen1, optLen2); else optLen = max (cellfun (@length, dispstr(! brackets))); endif endif ## Pad data according to optimal length ## numeric and logical is right aligned, everything else is left aligned Ra_wB = sprintf ("{[%%+%ds]}", optLen - 2); fcn = @(x) sprintf (Ra_wB, x); idx = pad_B & brackets; dispstr(idx) = cellfun (fcn, dispstr(idx), "UniformOutput", false); La_wB = sprintf ("{[%%-%ds]}", optLen - 2); fcn = @(x) sprintf (La_wB, x); idx = ! pad_B & brackets; dispstr(idx) = cellfun (fcn, dispstr(idx), "UniformOutput", false); La_nB = sprintf ("{%%-%ds}", optLen); fcn = @(x) sprintf (La_nB, x); idx = ((! pad_B & ! brackets) | me) & ! is_fhandle; dispstr(idx) = cellfun (fcn, dispstr(idx), "UniformOutput", false); ## Function handles are right aligned without brackets Ra_nB = sprintf ("{%%+%ds}", optLen); fcn = @(x) sprintf (Ra_nB, x); dispstr(is_fhandle) = cellfun (fcn, dispstr(is_fhandle), ... "UniformOutput", false); endfunction function out = combvec (vecs) switch (numel (vecs)) case 1 out = vecs{1}(:); case 2 a = vecs{1}(:); b = vecs{2}(:); out = repmat (a, numel (b), 2); i_comb = 1; for i_a = 1:numel (a) for i_b = 1:numel (b) out(i_comb,:) = [a(i_a), b(i_b)]; i_comb = i_comb + 1; endfor endfor otherwise out = []; a = vecs{1}(:); rest = vecs(2:end); rest_combs = combvec (rest); for i = 1:numel (a) out = [out; [repmat(a(i), [size(rest_combs,1), 1]), rest_combs]]; endfor endswitch endfunction pr0m1th3as-datatypes-9450b29/inst/NaT.m000066400000000000000000000131151527370255000175140ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{T} =} NaT (@var{n}) ## @deftypefnx {datatypes} {@var{T} =} NaT (@var{sz1}, @dots{}, @var{szN}) ## @deftypefnx {datatypes} {@var{T} =} NaT (@var{sz}) ## @deftypefnx {datatypes} {@var{T} =} NaT (@dots{}, @qcode{'Format'}, @var{fmt}) ## @deftypefnx {datatypes} {@var{T} =} NaT (@dots{}, @qcode{'TimeZone'}, @var{tz}) ## ## “Not-a-Timeâ€�. Creates missing-valued datetime arrays. ## ## @code{@var{T} = NaT (@var{n})} creates an @math{N*N} datetime matrix with all ## values being Not-a-Time (@qcode{NaT}). When called with no size input ## values, it returns a @qcode{NaT} datetime scalar. ## ## @code{@var{T} = NaT (@var{sz1}, @dots{}, @var{szN})} returns a datetime array ## with @qcode{NaT} values sized according to the input arguments @var{sz1}, ## @dots{}, @var{szN}. Alternatively, individual input size arguments can be ## merged into a single size vector @var{sz}, as in the following syntax ## @code{@var{T} = NaT (@var{sz})}. ## ## @code{@var{T} = NaT (@dots{}, @qcode{'Format'}, @var{fmt})} returns a ## datetime array of @qcode{NaT} values with the specified display format. ## ## @code{@var{T} = NaT (@dots{}, @qcode{'TimeZone'}, @var{tz})} returns a ## datetime array of @qcode{NaT} values in the time zone specified by @var{tz}. ## ## @qcode{NaT} is the @qcode{datetime} equivalent of @qcode{NaN}. It represents ## a missing or invalid value. @qcode{NaT} values never compare equal to, ## greater than, or less than any value, including other @qcode{NaT}s. Doing ## arithmetic with a @qcode{NaT} and any other value results in a @qcode{NaT}. ## ## @seealso{datetime} ## @end deftypefn function T = NaT (varargin) ## Parse optional Name-Value paired arguments optNames = {'Format', 'TimeZone'}; dfValues = {'default', ''}; [Format, TimeZone, args] = parsePairedArguments (optNames, dfValues, ... varargin(:)); ## Check optional Name-Value paired arguments if (! ((ischar (Format) && isvector (Format)) || (isa (Format, "string") && isscalar (Format)))) error (strcat ("NaT: 'Format' must be either a character vector or", ... " a string scalar.")); endif if (! ((ischar (TimeZone) && (isvector (TimeZone) || isempty (TimeZone))) || (isa (TimeZone, "string") && isscalar (TimeZone)))) error (strcat ("NaT: 'TimeZone' must be either a character vector or", ... " a string scalar.")); endif ## Parse and check SIZE arguments. Use the count of positional arguments ## (after the Name-Value pairs are stripped) so that, e.g., ## NaT ('TimeZone', tz) is a scalar rather than falling through to the ## multi-dimension branch with an empty size. if (numel (args) == 0) sz = 1; elseif (numel (args) == 1) if (isscalar (args{1}) && args{1} >= 0 && args{1} == fix (args{1})) sz = [args{1}, args{1}]; elseif (isrow (args{1}) && all (args{1} >= 0) ... && all (args{1} == fix (args{1}))) sz = args{1}; else error (strcat ("NaT: N must be a scalar or a row vector", ... " of non-negative integers.")); endif else posint = cellfun (@(x) (! isscalar (x) || x < 0 || x != fix (x)), args); if (any (posint)) error ("NaT: dimensions must be non-negative integers."); endif sz = [args{:}]; endif ## Construct datetime object with static method. The default 'Format' here is ## the same sentinel the datetime constructor falls back to on its own, so ## pass it on only when the caller actually named a format; that leaves a ## time zone which locks its own display format, 'UTCLeapSeconds', free to ## apply it. if (strcmp (Format, 'default')) T = datetime (nan (sz), 'ConvertFrom', 'datenum', 'TimeZone', TimeZone); else T = datetime (nan (sz), 'ConvertFrom', 'datenum', 'Format', Format, ... 'TimeZone', TimeZone); endif endfunction %!assert_equal (isscalar (NaT), true); %!assert_equal (isnat (NaT), true); %!assert_equal (size (NaT (3)), [3, 3]); %!assert_equal (size (NaT (2, 3, 4)), [2, 3, 4]); ## A Name-Value pair with no size argument yields a scalar (not an empty array). %!test %! z = NaT ('TimeZone', 'America/New_York'); %! assert_equal (size (z), [1, 1]); %! assert_equal (z.TimeZone, 'America/New_York'); %! assert_equal (isnat (z), true); %!assert_equal (size (NaT ('Format', 'yyyy-MM-dd')), [1, 1]); %!assert_equal (size (NaT (2, 3, 'TimeZone', 'UTC')), [2, 3]); %!error ... %! NaT (1, 'Format', 2); %!error ... %! NaT (1, 'TimeZone', 2); %!error ... %! NaT ([1; 2]); %!error ... %! NaT (2, -1); pr0m1th3as-datatypes-9450b29/inst/PKG_ADD000066400000000000000000000005071527370255000176710ustar00rootroot00000000000000addpath (fullfile (fileparts (canonicalize_file_name (mfilename ("fullpath"))), "demos")); addpath (fullfile (fileparts (canonicalize_file_name (mfilename ("fullpath"))), "tests")); setenv ("TZDATA", fullfile (fileparts (canonicalize_file_name (mfilename ("fullpath"))), "tzdata")); warning ("off", "Octave:data-file-in-path") pr0m1th3as-datatypes-9450b29/inst/PKG_DEL000066400000000000000000000002751527370255000177070ustar00rootroot00000000000000rmpath (fullfile (fileparts (canonicalize_file_name (mfilename ("fullpath"))), "demos")); rmpath (fullfile (fileparts (canonicalize_file_name (mfilename ("fullpath"))), "tests")); clear -c pr0m1th3as-datatypes-9450b29/inst/array2table.m000066400000000000000000000145761527370255000212560ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{tbl} =} array2table (@var{A}) ## @deftypefnx {datatypes} {@var{tbl} =} array2table (@var{A}, @var{Name}, @var{Value}) ## ## Convert an array to a table. ## ## @code{@var{tbl} = array2table (@var{A})} converts the 2-D matrix @var{A} to ## the table @var{tbl}, where each column of @var{A} becomes a variable in ## @var{tbl}. ## ## @var{A} can be any type of array supported by @code{table}, including a cell ## array, as long they are constraint to 2 dimensions. However, in the case of ## a cell array @code{array2table} does not extract the contents of its cells, ## resulting to a table with each variable being a column of cells. Use ## @code{cell2table} if you want to create a table from the contents of the ## cells in @var{A}. ## ## @code{@var{tbl} = array2table (@var{A}, @var{Name}, @var{Value})} specifies ## optional parameters for creating the table @var{tbl} with the following ## Name-Value paired arguments. ## ## @multitable @columnfractions 0.23 0.75 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'VariableNames'} @tab A cell array of character vectors or ## a string array defining the variable names of @var{tbl}. The names must be ## valid variable names and unique. ## ## @item @qcode{'RowNames'} @tab A cell array of character vectors or ## a string array defining the row names of @var{tbl}. The names must be unique ## but not necessarily valid variable names. ## ## @item @qcode{'DimensionNames'} @tab A cell array of character vectors or ## a string array defining the dimension names of @var{tbl}. The names must be ## unique and not in conflict with variable names. By default, dimension names ## are @qcode{'Row', 'Variables'}. ## @end multitable ## ## @seealso{cell2table, struct2table, table} ## @end deftypefn function tbl = array2table (A, varargin) ## Check input is a matrix if (ndims (A) > 2) error ("array2table: input array must be a 2-D array."); endif ## Parse optional Name-Value paired arguments optNames = {'VariableNames', 'RowNames', 'DimensionNames'}; dfValues = {{}, {}, {}}; [varNames, rowNames, dimNames, args] = parsePairedArguments ... (optNames, dfValues, varargin); ## Split columns into separate input data arguments for table varN = size (A, 2); varValues = cell (1, varN); for ix = 1:varN varValues{ix} = A(:,ix); endfor ## Handle variable names if (! isempty (varNames)) if (numel (varNames) != varN) error (strcat ("array2table: 'VariableNames' must match the", ... " columns in input array.")); endif else varName = inputname (1); if (isempty (varName)) varName = 'Var'; endif varNames = cell (1, varN); for ix = 1:varN varNames{ix} = sprintf ('%s%d', varName, ix); endfor endif optArgs = {'VariableNames', varNames}; ## Handle remaining paired arguments if (! isempty (rowNames)) if (numel (rowNames) != size (A, 1)) error ("array2table: 'RowNames' must match the rows in input array."); endif optArgs = [optArgs {'RowNames', rowNames}]; endif if (! isempty (dimNames)) if (numel (dimNames) != 2) error ("array2table: 'DimensionNames' must be a two-element vector."); endif optArgs = [optArgs {'DimensionNames', dimNames}]; endif ## Construct table tbl = table (varValues{:}, optArgs{:}); endfunction %!demo %! ## `array2table` wraps the columns of a matrix as table variables. Without %! ## names the variables are auto-labelled `Var1`, `Var2`, ... %! %! A = [38, 71, 176; 43, 69, 163; 40, 67, 133]; %! array2table (A) %!demo %! ## Supply `'VariableNames'` (and optionally `'RowNames'`) to label the result %! ## as you convert; the columns of the matrix keep their order. %! %! A = [38, 71; 43, 69; 40, 67]; %! array2table (A, 'VariableNames', {'Age', 'Height'}, ... %! 'RowNames', {'Li', 'Diaz', 'Brown'}) %!test %! A = [1, 2; 3, 4]; %! tbl = array2table (A); %! assert_equal (tbl.A1, [1; 3]); %! assert_equal (tbl.A2, [2; 4]); %! assert_equal (size (A), size (tbl)); %!test %! tbl = array2table ([1, 2; 3, 4]); %! assert_equal (tbl.Var1, [1; 3]); %! assert_equal (tbl.Var2, [2; 4]); %! assert_equal (size (tbl), [2, 2]); %!test %! tbl = array2table ([1, 2; 3, 4], 'VariableNames', {'A', 'B'}); %! assert_equal (tbl.A, [1; 3]); %! assert_equal (tbl.B, [2; 4]); %! assert_equal (isa (tbl.A, "double"), true); %!test %! tbl = array2table ([1, 2; 3, 4], "RowNames", {'A', 'B'}); %! assert_equal (tbl.Var1, [1; 3]); %! assert_equal (tbl.Var2, [2; 4]); %! assert_equal (tbl.Properties.RowNames, {'A'; 'B'}); %! assert_equal (class (tbl('A', :)), 'table'); %! assert_equal (tbl{'A', :}, [1, 2]); %!test %! tbl = array2table ([1, 2; 3, 4], string ('DimensionNames'), {'A', 'B'}); %! assert_equal (tbl.Var1, [1; 3]); %! assert_equal (tbl.Var2, [2; 4]); %! assert_equal (tbl.A, {}); %! assert_equal (tbl.B, [1, 2; 3, 4]); %!test %! tbl = array2table ([1, 2; 3, 4], "RowNames", {'A', 'B'}, ... %! "DimensionNames", {'A', 'B'}); %! assert_equal (tbl.Var1, [1; 3]); %! assert_equal (tbl.Var2, [2; 4]); %! assert_equal (tbl.A, {'A'; 'B'}); %! assert_equal (tbl.B, [1, 2; 3, 4]); %!error ... %! array2table (ones (3, 3, 3)); %!error ... %! array2table ([1; 2; 3], 'VariableNames', {'A', 'B'}); %!error ... %! array2table ([1; 2; 3], 'RowNames', {'A', 'B'}); %!error ... %! array2table ([1; 2; 3], 'DimensionNames', {'A', 'B', 'C'}); pr0m1th3as-datatypes-9450b29/inst/array2timetable.m000066400000000000000000000160321527370255000221220ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{tt} =} array2timetable (@var{A}, @qcode{'RowTimes'}, @var{rowTimes}) ## @deftypefnx {datatypes} {@var{tt} =} array2timetable (@var{A}, @qcode{'TimeStep'}, @var{dt}) ## @deftypefnx {datatypes} {@var{tt} =} array2timetable (@var{A}, @qcode{'SampleRate'}, @var{fs}) ## @deftypefnx {datatypes} {@var{tt} =} array2timetable (@dots{}, @var{Name}, @var{Value}) ## ## Convert an array to a timetable. ## ## @code{@var{tt} = array2timetable (@var{A}, @qcode{'RowTimes'}, ## @var{rowTimes})} converts the 2-D array @var{A} to a timetable, each ## column becoming a variable and @var{rowTimes} labelling the rows. The row ## times must be given, by one of @qcode{'RowTimes'}, @qcode{'TimeStep'} or ## @qcode{'SampleRate'}, since a timetable cannot be built without them. ## ## Variable names are taken from the name of @var{A} with the column number ## appended, so an array named @var{data} gives @qcode{'data1'}, ## @qcode{'data2'} and so on; an unnamed array gives @qcode{'Var1'} onwards. ## ## The following Name-Value pairs are also accepted. ## ## @multitable @columnfractions 0.23 0.75 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'VariableNames'} @tab A cell array of character vectors or a ## string array defining the variable names, one per column of @var{A}. ## ## @item @qcode{'DimensionNames'} @tab A two-element cell array of character ## vectors or string array naming the rows and the variables. By default the ## rows are named @qcode{'Time'}. ## ## @item @qcode{'StartTime'} @tab The time of the first row, for the ## @qcode{'TimeStep'} and @qcode{'SampleRate'} forms. ## @end multitable ## ## @seealso{table.table2timetable, timetable.timetable2table, timetable, ## array2table} ## @end deftypefn function TT = array2timetable (A, varargin) if (nargin < 1) print_usage (); endif if (ndims (A) > 2) error ("array2timetable: input array must be a 2-D array."); endif if (mod (numel (varargin), 2) != 0) error ("array2timetable: name-value arguments must be in pairs."); endif ## Parse optional paired arguments optNames = {'VariableNames', 'DimensionNames', 'RowTimes', 'TimeStep', ... 'SampleRate', 'StartTime'}; dfValues = {{}, {}, missing, missing, missing, missing}; [varNames, dimNames, RowTimes, TimeStep, SampleRate, StartTime, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error ("array2timetable: invalid optional paired argument."); endif ## Handle variable names varN = size (A, 2); if (! isempty (varNames)) if (numel (varNames) != varN) error (strcat ("array2timetable: 'VariableNames' must match the", ... " columns in input array.")); endif if (isa (varNames, 'string')) varNames = cellstr (varNames); endif else varName = inputname (1); if (isempty (varName)) varName = 'Var'; endif varNames = cell (1, varN); for ix = 1:varN varNames{ix} = sprintf ('%s%d', varName, ix); endfor endif ## Assemble the row time arguments the timetable constructor takes, which ## is the only place they are validated. timeArgs = {}; if (! isa (RowTimes, 'missing')) timeArgs = [timeArgs, {'RowTimes', RowTimes}]; endif if (! isa (TimeStep, 'missing')) timeArgs = [timeArgs, {'TimeStep', TimeStep}]; endif if (! isa (SampleRate, 'missing')) timeArgs = [timeArgs, {'SampleRate', SampleRate}]; endif if (! isa (StartTime, 'missing')) timeArgs = [timeArgs, {'StartTime', StartTime}]; endif if (! isempty (dimNames)) timeArgs = [timeArgs, {'DimensionNames', dimNames}]; endif ## Build the timetable empty and fill it column by column. The columns ## cannot be handed to the constructor positionally: a datetime array's ## first column would be read as the row times. TT = timetable ('Size', [size(A, 1), 0], 'VariableTypes', {}, ... timeArgs{:}); for ix = 1:varN TT.(varNames{ix}) = A(:,ix); endfor endfunction ## Test conversion with explicit row times %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! TT = array2timetable ([(1:3)', (4:6)'], 'RowTimes', tv); %! assert_equal (class (TT), 'timetable'); %! assert_equal (size (TT), [3, 2]); %! assert_equal (TT.Properties.VariableNames, {'Var1', 'Var2'}); %! assert_equal (TT.Properties.DimensionNames, {'Time', 'Variables'}); %! assert_equal (TT.Properties.RowTimes, tv); ## Test variable names come from the name of the input array %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! data = [(1:3)', (4:6)']; %! TT = array2timetable (data, 'RowTimes', tv); %! assert_equal (TT.Properties.VariableNames, {'data1', 'data2'}); ## Test a generated time step %!test %! TT = array2timetable ((1:3)', 'TimeStep', hours (1)); %! assert_equal (TT.Properties.RowTimes, hours (0:2)'); %! assert_equal (TT.Properties.DimensionNames{1}, 'Time'); ## Test a generated sample rate %!test %! TT = array2timetable ((1:3)', 'SampleRate', 2); %! assert_equal (TT.Properties.TimeStep, seconds (0.5)); ## Test explicit variable and dimension names %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! TT = array2timetable ((1:3)', 'RowTimes', tv, 'VariableNames', {'z'}, ... %! 'DimensionNames', {'When', 'What'}); %! assert_equal (TT.Properties.VariableNames, {'z'}); %! assert_equal (TT.Properties.DimensionNames, {'When', 'What'}); ## Test the columns keep their own type %!test %! TT = array2timetable (hours (0:2)' + hours ([0, 10]), ... %! 'TimeStep', hours (1)); %! assert_equal (class (TT.Var1), 'duration'); %!error ... %! array2timetable (ones (2, 2, 2), 'TimeStep', hours (1)); %!error ... %! array2timetable ((1:3)', 'TimeStep', hours (1), 'Bogus'); %!error ... %! array2timetable ((1:3)', 'TimeStep', hours (1), 'Bogus', 1); %!error ... %! array2timetable ((1:3)', 'TimeStep', hours (1), ... %! 'VariableNames', {'a', 'b'}); %!error ... %! array2timetable ((1:3)'); pr0m1th3as-datatypes-9450b29/inst/caldays.m000066400000000000000000000052431527370255000204550ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{calD} =} caldays (@var{X}) ## ## Calendar duration in days. ## ## @code{@var{calD} = caldays (@var{X})} returns a @qcode{calendarDuration} ## array representing calendar days equivalent to the values in @var{X}, which ## must be a numeric array of integer values. ## ## @code{caldays} is also available as a method for @qcode{calendarDuration} ## arrays, in which case it performs the opposite conversion. ## ## @seealso{calendarDuration, calyears, calquarters, calmonths, calweeks, ## calendarDuration.caldays} ## @end deftypefn function out = caldays (x) ## Check input if (nargin == 0) x = 1; elseif (! isnumeric (x)) error ("caldays: input array must be numeric."); elseif (! isreal (x)) error ("caldays: input array must be real."); endif xx = x; xx(isnan (x)) = 0; if (any (fix (xx(:)) != xx(:))) error ("caldays: input array must contain only integer values."); endif out = calendarDuration (0, 0, double (x)); endfunction %!demo %! ## `caldays` builds a calendar duration of whole days. It is one of the %! ## component builders (`calyears`, `calquarters`, `calmonths`, `calweeks`, %! ## `caldays`) that you combine by addition to assemble a full span. %! %! caldays (10) %! %! ## A numeric array builds one duration per element. %! caldays ([7, 14, 21]) %!test %! X = magic (3); %! D = caldays (X); %! assert_equal (size (D), size (X)); %!test %! D = caldays ([1, 2, 3]); %! assert_equal (caldays (D), [1, 2, 3]); %!test %! D = caldays ([1, 2, NaN, 4]); %! assert_equal (caldays (D), [1, 2, NaN, 4]); %!test %! D = caldays (int16 (1)); %! assert_equal (caldays (D), 1); %!test %! D = caldays (); %! assert_equal (caldays (D), 1); %!error caldays ("asd"); %!error caldays (1+i); %!error ... %! caldays (1.2); pr0m1th3as-datatypes-9450b29/inst/calendarDuration.m000066400000000000000000003222101527370255000223100ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef calendarDuration ## -*- texinfo -*- ## @deftp {datatypes} calendarDuration ## ## Array representing durations of time using flexible-length calendar ## date/time units. ## ## Each @code{calendarDuration} element stores internally the number of whole ## months, the number of whole days, and a @code{duration} object representing ## hours, minutes, and seconds. It can be used to simplify calculations on ## @code{datetime} arrays involving calendar units. ## ## @code{calendarDuration} arrays can be created through their constructor by ## combining numeric arrays representing individual calendar duration units or ## through the functions @code{caldays}, @code{calweeks}, @code{calmonths}, ## @code{calquarters}, and @code{calyears}, which create calendar durations in ## terms of a single calendar duration unit. These functions are also ## available as methods of @code{calendarDuration} arrays to extract ## individual calendar duration units as numeric arrays. ## ## @seealso{datetime, duration} ## @end deftp properties ## -*- texinfo -*- ## @deftp {calendarDuration} {property} Format ## ## Display format ## ## Display format, specified as a character vector or string scalar. If ## specified as a string scalar, it is converted and stored internally as ## a character vector. ## ## @end deftp Format = 'ymdt' endproperties properties (SetAccess = private, Hidden) ## Whole calendar months Months = 0 ## Whole calendar days Days = 0 ## Time as duration Time = duration endproperties methods (Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif __disp__ (this, 'calendarDuration', in_name); endfunction ## Custom display function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif __disp__ (this, 'calendarDuration'); endfunction endmethods methods (Static, Access = public) ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{E} =} calendarDuration.empty () ## @deftypefnx {calendarDuration} {@var{E} =} calendarDuration.empty (@var{sz}) ## @deftypefnx {calendarDuration} {@var{E} =} calendarDuration.empty (@var{m}, @var{n}, @dots{}) ## ## Create an empty calendarDuration array. ## ## @code{@var{E} = calendarDuration.empty ()} returns a @math{0*0} empty ## calendarDuration array. @code{calendarDuration.empty (@var{m}, @var{n}, ## @dots{})} or @code{calendarDuration.empty (@var{sz})} returns an empty ## calendarDuration array of the requested size, which must have at least ## one dimension equal to zero. A lone dimension gives a square size, so ## @code{calendarDuration.empty (3)} is an error while ## @code{calendarDuration.empty (0)} is @math{0*0}. As for @code{zeros}, a ## negative dimension counts as zero, and a size vector with nothing in it ## names no size and gives @math{0*0}. ## ## @end deftypefn function E = empty (varargin) if (nargin == 0) sz = [0, 0]; elseif (nargin == 1 && ! isscalar (varargin{1})) sz = double (varargin{1}(:)).'; else sz = [varargin{:}]; endif if (! (isnumeric (sz) && isrow (sz) && all (sz == fix (sz)))) error ("calendarDuration.empty: dimensions must be integer values."); endif ## A negative dimension is no smaller than none at all, and a size vector ## holding no dimensions names no size: 'zeros' and every other array ## constructor read both the same way. sz = max (sz, 0); if (isempty (sz)) sz = [0, 0]; elseif (isscalar (sz)) sz = [sz, sz]; endif if (all (sz != 0)) error (strcat ("calendarDuration.empty: at least one dimension", ... " must be zero for an empty array.")); endif E = calendarDuration (zeros (sz), zeros (sz), zeros (sz)); endfunction endmethods ################################################################################ ## ** Create and convert 'calendarDuration' type ** ## ################################################################################ ## Available Methods ## ## ## ## 'duration' 'dispstrings' 'cellstr' 'char' ## ## 'datevec' 'time' 'split' 'caldays' ## ## 'calweeks' 'calmonths' 'calquarters' 'calyears' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{calD} =} calendarDuration (@var{X}) ## @deftypefnx {calendarDuration} {@var{calD} =} calendarDuration (@var{Y}, @var{MO}, @var{D}) ## @deftypefnx {calendarDuration} {@var{calD} =} calendarDuration (@var{Y}, @var{MO}, @var{D}, @var{H}, @var{MI}, @var{S}) ## @deftypefnx {calendarDuration} {@var{calD} =} calendarDuration (@var{Y}, @var{MO}, @var{D}, @var{T}) ## @deftypefnx {calendarDuration} {@var{calD} =} calendarDuration (@var{calD2}) ## @deftypefnx {calendarDuration} {@var{calD} =} calendarDuration (@dots{}, @qcode{'Format'}, @var{FMT}) ## ## Create a new array of calendar durations. ## ## @code{@var{calD} = calendarDuration (@var{X})} returns an array of ## calendar durations from numeric matrix @var{X}, which must have either ## three or six columns, representing years, months, days, hours, minutes, ## and seconds, accordingly. All but seconds must be represented as whole ## duration units by integer values. ## ## @code{@var{calD} = calendarDuration (@var{Y}, @var{MO}, @var{D})} returns ## an array of calendar durations from numeric arrays @var{Y}, @var{MO}, and ## @var{D}, which correspond to years, months, and days, respectively. The ## size of @var{calD} is the common size of the numeric input arguments, ## which must be of the same size or scalars. A scalar input functions as a ## constant array of the same size as the other inputs. ## ## @code{@var{calD} = calendarDuration (@var{Y}, @var{MO}, @var{D}, @var{H}, ## @var{MI}, @var{S})} returns an array of calendar durations from numeric ## arrays @var{Y}, @var{MO}, @var{D}, @var{H}, @var{MI}, and @var{S}, which ## correspond to years, months, days, hours, minutes, and seconds, ## respectively. The size of @var{calD} is the common size of the numeric ## input arguments, which must be of the same size or scalars. A scalar ## input functions as a constant array of the same size as the other inputs. ## ## @code{@var{calD} = calendarDuration (@var{Y}, @var{MO}, @var{D}, ## @var{T})} returns an array of calendar durations from numeric arrays ## @var{Y}, @var{MO}, and @var{D}, which correspond to years, months, and ## days, as well as a time duration array @var{T}. The size of @var{calD} ## is the common size of the data input arguments, which must be of the same ## size or scalars. A scalar input functions as a constant array of the ## same size as the other inputs. ## ## Numeric input arrays @var{Y}, @var{MO}, @var{D}, @var{H}, and @var{MI} ## must contain integer values corresponding to whole calendar units. ## @var{S} can also be contain fractions of seconds. ## ## @code{@var{calD} = calendarDuration (@var{calD2})} returns a copy of the ## calendarDuration array @var{calD2}, which keeps its size as well as its ## @qcode{'Format'} property unless a new format is specified. ## ## @code{@var{calD} = calendarDuration (@dots{}, @qcode{'Format'}, ## @var{FMT})} specifies the format in which @var{calD} is displayed. ## @var{FMT} must be a character vector or a string scalar containing the ## following letters. ## ## @itemize ## @item @qcode{'y'} years ## @item @qcode{'q'} quarters of a year ## @item @qcode{'m'} months ## @item @qcode{'w'} weeks ## @item @qcode{'d'} days ## @item @qcode{'t'} time duration ## @end itemize ## ## Each character must be specified only once in the same order as they ## appear in the above list. @qcode{'m'}, @qcode{'d'}, and @qcode{'t'} ## characters must always be included in the format specification. No ## other character is accepted. ## ## @strong{Note:} MATLAB does not reject a format that omits a required ## character. It warns and silently substitutes a repaired format, so that ## @qcode{'ymd'} becomes @qcode{'ymdt'}. An invalid format is an error ## here, because rewriting what the user asked for hides the typo that ## caused it. ## ## @strong{Note:} MATLAB also accepts a numeric array in place of @var{T}, ## which it reads as a count of @emph{milliseconds}, thereby exposing the ## internal storage of its @code{duration} type. This is undocumented and ## contradicts MATLAB's own documentation, which requires @var{T} to be a ## duration array, and it is not reproduced here. Use @code{milliseconds ## (@var{N})} to say so explicitly. ## ## @code{@var{calD} = calendarDuration ()} returns a scalar array of ## calendar durations with a value of zero days. To create an empty ## calendarDuration array, use @code{calendarDuration ([], [], [])}. A ## @qcode{'Format'} may be given on its own, as in @code{calendarDuration ## (@qcode{'Format'}, @qcode{'mdt'})}, which returns the same zero scalar ## in that format. MATLAB requires data alongside the option and rejects ## this. ## ## @seealso{calyears, calquarters, calmonths, calweeks, caldays, ## calendarDuration, iscalendarduration, datetime, duration} ## @end deftypefn function this = calendarDuration (varargin) ## Return a scalar calendarDuration object if (nargin == 0) return endif ## Parse optional Name-Value paired arguments optNames = {'Format'}; dfValues = {[]}; [Format, args] = parsePairedArguments (optNames, dfValues, varargin(:)); ## Check optional 'Format' argument. It was supplied only if the parser ## consumed its Name/Value pair: testing the returned value for emptiness ## would read an explicit 'Format', [] as no format having been given. fmtGiven = numel (args) != numel (varargin); if (fmtGiven) [errmsg, Format] = checkFormatString (Format); if (! isempty (errmsg)) error ("calendarDuration: 'Format' %s", errmsg); endif this.Format = Format; endif ## Parse inputs switch (numel (args)) ## this = calendarDuration () case 0 return ## this = calendarDuration (X) case 1 X = args{1}; ## A calendarDuration passes through unchanged, keeping its own ## display format unless another one was specified here. if (isa (X, 'calendarDuration')) if (fmtGiven) X.Format = Format; endif this = X; return endif if (isa (X, 'duration')) error (strcat ("calendarDuration: X must be numeric. Convert a", ... " duration array with 'years', 'days', 'hours',", ... " 'minutes', or 'seconds' first.")); endif if (! (isnumeric (X) && ismatrix (X))) error ("calendarDuration: X must be a numeric matrix."); endif if (! isreal (X)) error ("calendarDuration: X must be real."); endif if (size (X, 2) == 3) tmp = X(:); tmp(isnan (tmp)) = 0; if (any (fix (tmp) != tmp)) error (strcat ("calendarDuration: years, months,", ... " and days must be integer values.")); endif Y = X(:,1); M = X(:,2); D = X(:,3); T = duration (zeros (size (X))); elseif (size (X, 2) == 6) tmp = X(:,[1:5]); tmp(isnan (tmp)) = 0; if (any (fix (tmp) != tmp, 'all')) error (strcat ("calendarDuration: years, months, days,", ... " hours, and minutes must be integer values.")); endif Y = X(:,1); M = X(:,2); D = X(:,3); T = duration (X(:,[4:6])); else error ("calendarDuration: X must have either 3 or 6 columns."); endif ## this = calendarDuration (Y, M, D) case 3 [Y, M, D] = args{:}; if (! (isnumeric (Y) && isnumeric (M) && isnumeric (D))) error ("calendarDuration: Y, MO, and D must be a numeric arrays."); endif if (! (isreal (Y) && isreal (M) && isreal (D))) error ("calendarDuration: Y, MO, and D must be real."); endif ## Expand as necessary if (! isscalar (Y) || ! isscalar (M) || ! isscalar (D)) [err, Y, M, D] = common_size (Y, M, D); if (err > 0) error (strcat ("calendarDuration: Y, MO, and D must", ... " be of common size or scalars.")); endif endif tmp = [Y(:), M(:), D(:)]; tmp(isnan (tmp)) = 0; if (any (fix (tmp) != tmp, 'all')) error (strcat ("calendarDuration: years, months,", ... " and days must be integer values.")); endif T = repmat (duration (0, 0, 0), size (Y)); ## this = calendarDuration (Y, M, D, T) case 4 [Y, M, D, T] = args{:}; if (! (isnumeric (Y) && isnumeric (M) && isnumeric (D))) error ("calendarDuration: Y, MO, and D must be a numeric arrays."); endif if (! (isreal (Y) && isreal (M) && isreal (D))) error ("calendarDuration: Y, MO, and D must be real."); endif if (! isa (T, "duration")) error ("calendarDuration: T must be a duration array."); endif ## Expand as necessary t = ones (size (T)); if (! isscalar (Y) || ! isscalar (M) || ! isscalar (D) || ... ! isscalar (t)) [err, Y, M, D, t] = common_size (Y, M, D, t); if (err > 0) error (strcat ("calendarDuration: Y, MO, D, and T", ... " must be of common size or scalars.")); endif if (! isequal (size (T), size (t))) T = repmat (T, size (t)); endif endif tmp = [Y(:), M(:), D(:)]; tmp(isnan (tmp)) = 0; if (any (fix (tmp) != tmp, 'all')) error (strcat ("calendarDuration: years, months,", ... " and days must be integer values.")); endif ## this = calendarDuration (Y, M, D, H, MI, S) case 6 [Y, M, D, H, MI, S] = args{:}; if (! (isnumeric (Y) && isnumeric (M) && isnumeric (D) && isnumeric (H) && isnumeric (MI) && isnumeric (S))) error (strcat ("calendarDuration: Y, MO, D, H, MI,", ... " and S must be numeric arrays.")); endif if (! (isreal (Y) && isreal (M) && isreal (D) && isreal (H) && isreal (MI) && isreal (S))) error ("calendarDuration: numeric input data must be real."); endif ## Expand as necessary if (! isscalar (Y) || ! isscalar (M) || ! isscalar (D) || ! isscalar (H) || ! isscalar (MI) || ! isscalar (S)) [err, Y, M, D, H, MI, S] = common_size (Y, M, D, H, MI, S); if (err > 0) error (strcat ("calendarDuration: Y, MO, D, H, MI, and", ... " S must be of common size or scalars.")); endif endif tmp = [Y(:), M(:), D(:), H(:), MI(:)]; tmp(isnan (tmp)) = 0; if (any (fix (tmp) != tmp, 'all')) error (strcat ("calendarDuration: years, months, days,", ... " hours, and minutes must be integer values.")); endif T = duration (H, MI, S); otherwise error ("calendarDuration: invalid number of input arguments."); endswitch ## Construction this.Months = double (Y) * 12 + double (M); this.Days = double (D); this.Time = T; ## Broadcast NaNs this = broadcastProperties (this); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{cstr} =} dispstrings (@var{calD}) ## ## Get display formatted strings for each element of a calendarDuration ## array. ## ## @code{@var{cstr} = dispstrings (@var{calD})} returns a cellstr array of ## character vectors, @var{cstr}, which has the same size as the input ## calendarDuration array, @var{calD}. The returned text representations ## in @var{cstr} are formatted according to the @qcode{'Format'} property ## of the input array @var{calD}. ## ## Whole calendar units are rendered with up to six significant digits and ## the seconds component with five, each switching to exponent notation ## beyond that, so a million days reads @qcode{'1e+06d'}. Rounding applies ## to a component on its own and never carries into the one above it, which ## is why @code{59.9999} seconds read @qcode{'60s'} rather than a further ## minute. ## ## @end deftypefn function cstr = dispstrings (this) ## Process all elements sz = size (this); cstr = cell (sz); allels = cell (sz); ## An element with nothing in it renders as a single zero, and the unit ## that zero carries is taken from the rest of the array: the smallest ## unit any element actually uses, or days when none uses any. So a zero ## beside '2mo' reads '0mo', beside '3d' reads '0d', and beside a time ## reads '0s'. MATLAB does the same; it is what stops a lone zero ## claiming a unit the array never mentions. for i = 1:prod (sz) calDur = subset (this, i); if (isnan (calDur.Months)) cstr{i} = 'NaN'; elseif (isinf (calDur.Months)) cstr{i} = num2str (calDur.Months); else els = {}; ## Check Format contains 'y' to split between years and months if (! isempty (strfind (calDur.Format, 'y'))) years = fix (calDur.Months / 12); months = rem (calDur.Months, 12); if (years != 0) els{end+1} = sprintf ('%sy', fmtUnit (years)); endif if (months != 0) ## Check Format contains 'q' to split between quarters and months if (! isempty (strfind (calDur.Format, 'q'))) quarters = fix (months / 3); months = rem (months, 3); if (quarters != 0) els{end+1} = sprintf ('%sq', fmtUnit (quarters)); endif if (months != 0) els{end+1} = sprintf ('%smo', fmtUnit (months)); endif else els{end+1} = sprintf ('%smo', fmtUnit (months)); endif endif else if (calDur.Months != 0) ## Check Format contains 'q' to split between quarters and months if (! isempty (strfind (calDur.Format, 'q'))) quarters = fix (calDur.Months / 3); months = rem (calDur.Months, 3); if (quarters != 0) els{end+1} = sprintf ('%sq', fmtUnit (quarters)); endif if (months != 0) els{end+1} = sprintf ('%smo', fmtUnit (months)); endif else els{end+1} = sprintf ('%smo', fmtUnit (calDur.Months)); endif endif endif if (calDur.Days != 0) ## Check Format contains 'w' to print whole weeks and subtract ## them from days if (! isempty (strfind (calDur.Format, 'w'))) weeks = fix (calDur.Days / 7); if (weeks != 0) els{end+1} = sprintf ('%sw', fmtUnit (weeks)); calDur.Days -= weeks * 7; endif endif if (calDur.Days != 0) els{end+1} = sprintf ('%sd', fmtUnit (calDur.Days)); endif endif millis = milliseconds (calDur.Time); if (abs (millis) > 4e-12) ## Every component is truncated towards zero, so all three carry ## the sign of the time as a whole. sec = millis / 1000; hours = fix (sec / 3600); rest = sec - hours * 3600; minutes = fix (rest / 60); ## The seconds component keeps whatever fraction it has, down to ## the sub-millisecond, and is rounded only by the five significant ## digits it is rendered with. Rounding is per component and never ## carries into the one above, so 59.9999 seconds render as '60s'. seconds = rest - minutes * 60; els{end+1} = sprintf ('%sh %sm %ss', fmtUnit (hours), ... fmtUnit (minutes), fmtSecs (seconds)); endif ## Left empty for now; the unit is chosen once the whole array ## has been rendered (see zeroRenderUnit below). cstr{i} = strjoin (els, ' '); allels{i} = els; endif endfor ## Give every wholly zero element the smallest unit the array actually ## rendered anywhere, so a zero never claims a unit nothing else uses. ## NaN elements also render no components, but they already carry their ## own string, so key on the rendered text rather than the component list. empty = cellfun (@isempty, cstr); if (any (empty(:))) cstr(empty) = {zeroRenderUnit(allels(! empty))}; endif endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{cstr} =} cellstr (@var{calD}) ## @deftypefnx {calendarDuration} {@var{cstr} =} cellstr (@var{calD}, @var{FMT}) ## ## Convert calendarDuration array to a cell array of character vectors. ## ## @code{@var{cstr} = cellstr (@var{calD})} returns a cellstr array of ## character vectors, @var{cstr}, which has the same size as the input ## @var{calD}. @var{cstr} contains the string representations of the ## calendar durations in @var{calD}. ## ## @code{@var{cstr} = cellstr (@var{calD}, @var{FMT})} further specifies ## the format of the returned string representations. @var{FMT} must be a ## character vector conforming to the same specifications required by the ## constructor's @qcode{'Format'} property paired argument. Note that ## @code{cellstr} only accepts @var{FMT} as a single argument and not as a ## property paired argument. ## ## @end deftypefn function cstr = cellstr (this, FMT = '') if (! isempty (FMT)) [errmsg, FMT] = checkFormatString (FMT); if (! isempty (errmsg)) error ("calendarDuration.cellstr: FMT %s", errmsg); endif this.Format = FMT; endif cstr = dispstrings (this); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{cmat} =} char (@var{calD}) ## @deftypefnx {calendarDuration} {@var{cmat} =} char (@var{calD}, @var{FMT}) ## ## Convert calendarDuration array to a character matrix. ## ## @code{@var{cmat} = char (@var{calD})} returns a character matrix with ## one row per element in @var{calD}, taken in column-major order. The ## second optional argument, @var{FMT}, can be used to specify the format of ## the returned string representations of the calendarDuration input array ## @var{calD}. ## ## Rows shorter than the widest are padded on the @emph{left}, so the ## character matrix is right-justified, as MATLAB returns it and as the ## array itself is displayed. ## ## @end deftypefn function cmat = char (this, FMT = '') if (! isempty (FMT)) [errmsg, FMT] = checkFormatString (FMT); if (! isempty (errmsg)) error ("calendarDuration.char: FMT %s", errmsg); endif this.Format = FMT; endif cmat = strjust (char (dispstrings (this)), 'right'); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{DV} =} datevec (@var{calD}) ## @deftypefnx {calendarDuration} {[@var{Y}, @var{MO}] =} datevec (@var{calD}) ## @deftypefnx {calendarDuration} {[@var{Y}, @var{MO}, @var{D}] =} datevec (@var{calD}) ## @deftypefnx {calendarDuration} {[@var{Y}, @var{MO}, @var{D}, @var{H}] =} datevec (@var{calD}) ## @deftypefnx {calendarDuration} {[@var{Y}, @var{MO}, @var{D}, @var{H}, @var{MI}] =} datevec (@var{calD}) ## @deftypefnx {calendarDuration} {[@var{Y}, @var{MO}, @var{D}, @var{H}, @var{MI}, @var{S}] =} datevec (@var{calD}) ## ## Convert calendarDuration array to date vectors. ## ## @code{@var{DV} = datevec (@var{calD})} returns an @math{N*6} numeric ## matrix, where @math{N} is the number of elements in @var{calD} and the ## columns corresponds to years, months, days, hours, minutes, and seconds, ## respectively. ## ## When @code{datevec} is called with more than one output arguments, then ## it returns the components of the date vectors as individual variables ## @var{Y}, @var{MO}, @var{D}, @var{H}, @var{MI},and @var{S} corresponding ## to years, months, days, hours, minutes, and seconds, respectively. In ## this case, the individual variables have the same size as the input array ## @var{calD}. ## ## Every component carries the sign of the span as a whole, so a negative ## calendar duration returns negative components throughout. An element ## that is not finite has no components to divide between: all six take that ## same infinity, or @code{NaN}, which is what @code{split} returns for it ## as well. ## ## @end deftypefn function varargout = datevec (this) [h, m, s] = hms (this.Time); years = fix (this.Months / 12); months = rem (this.Months, 12); ## An infinite span divides into no whole years with a remainder: every ## one of its components is that same infinity, which is what 'split' ## returns for it too. 'rem' would leave NaN in the months alone. nf = ! isfinite (this.Months); if (any (nf, 'all')) months(nf) = this.Months(nf); endif DV = [years(:), months(:), this.Days(:), h(:), m(:), s(:)]; if (nargout == 0 || nargout == 1) varargout{1} = DV; elseif (nargout <= 6) varargout{1} = years; varargout{2} = months; if (nargout > 2) varargout{3} = this.Days; endif if (nargout > 3) varargout{4} = h; endif if (nargout > 4) varargout{5} = m; endif if (nargout > 5) varargout{6} = s; endif else error ("calendarDuration.datevec: too many output arguments."); endif endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{T} =} time (@var{calD}) ## ## Return time portion of calendarDuration array. ## ## @code{@var{T} = time (@var{calD})} returns a duration array @var{T} with ## the time portions of the calendarDuration array @var{calD}. ## ## @end deftypefn function T = time (this) T = this.Time; endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {[@dots{}] =} split (@var{calD}, @var{units}) ## ## Split calendarDuration array into numeric and duration units. ## ## @code{[@dots{}] = split (@var{calD}, @var{units})} splits the calendar ## duration units in @var{calD} into separate numeric arrays according to ## date/time units specified in @var{units}, which must be either a cell ## array of character vectors or a string array containing any of the ## following date/time units in descending order. ## ## @enumerate ## @item @qcode{'years'} ## @item @qcode{'quarters'} ## @item @qcode{'months'} ## @item @qcode{'weeks'} ## @item @qcode{'days'} ## @item @qcode{'time'} ## @end enumerate ## ## When a single date/time unit is specified, @var{units} may also be a ## character vector. When @qcode{'time'} is specified in @var{units}, the ## corresponding returned argument is a @code{duration} array. The values ## of years, quarters, and months are computed independently from the values ## of weeks and days in @var{calD}, with larger units taking precedence when ## specified The same applies for duration arrays, when requested. ## ## Each unit may be abbreviated to any leading part of its name and is ## matched without regard to case, so @qcode{'y'}, @qcode{'Year'} and ## @qcode{'YEARS'} all name years. No abbreviation is ambiguous, the six ## names starting with six different letters. A unit named more than once ## still asks for a single component. ## ## Fewer output arguments than units may be requested, in which case only ## the leading ones are returned; asking for more is an error. An element ## that is not finite keeps that same value in every component it is divided ## into, so splitting an infinite calendar duration gives infinite years, ## months and days alike. ## ## @end deftypefn function varargout = split (this, units) ## Check input if (nargin < 2) error ("calendarDuration.split: too few input arguments."); endif if (ischar (units) && ndims (units) == 2 && size (units, 1) <= 1) ## A character vector names one unit and is taken as it stands: routing ## it through 'cellstr' would strip trailing blanks and quietly accept ## a name that is not one. units = {units}; elseif (isstring (units) || ischar (units)) units = cellstr (units); elseif (! iscellstr (units)) error ("calendarDuration.split: invalid input type for UNITS."); endif if (isempty (units)) error ("calendarDuration.split: UNITS must name at least one unit."); endif valid_units = {'years', 'quarters', 'months', 'weeks', 'days', 'time'}; ## A unit may be abbreviated to any leading part of its name and is ## matched without regard to case, so 'y', 'Year' and 'YEARS' all name ## years. No abbreviation is ambiguous, the six names starting with six ## different letters. idx_order = zeros (1, numel (units)); for i = 1:numel (units) unit = units{i}; if (isempty (unit)) error ("calendarDuration.split: UNITS must not contain empty names."); endif found = strncmpi (unit, valid_units, numel (unit)); if (! any (found)) error ("calendarDuration.split: '%s' is not a valid time unit.", ... unit); endif idx_order(i) = find (found, 1); endfor if (any (diff (idx_order) < 0)) error (strcat ("calendarDuration.split: UNITS must", ... " be specified in descending order.")); endif ## A unit named more than once still asks for one component. idx_order = unique (idx_order, 'stable'); ## Check output. Fewer outputs than units may be requested, in which case ## only the leading ones are returned; a lone output is always returned, ## even where none was asked for. n_args = numel (idx_order); if (nargout > n_args) error ("calendarDuration.split: too many output arguments."); endif n_out = max (nargout, 1); ## An element that is not finite keeps that same value in every component ## it is divided into: subtracting whole units off an infinity otherwise ## leaves NaN behind in the remainder. months = this.Months; days = this.Days; nf_months = ! isfinite (months); nf_days = ! isfinite (days); for i = 1:n_out switch (idx_order(i)) case 1 years = fix (months / 12); months = months - years * 12; months(nf_months) = this.Months(nf_months); varargout{i} = years; case 2 quarters = fix (months / 3); months = months - quarters * 3; months(nf_months) = this.Months(nf_months); varargout{i} = quarters; case 3 varargout{i} = months; case 4 weeks = fix (days / 7); days = days - weeks * 7; days(nf_days) = this.Days(nf_days); varargout{i} = weeks; case 5 varargout{i} = days; case 6 varargout{i} = this.Time; endswitch endfor endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{X} =} caldays (@var{calD}) ## ## Calendar duration in days. ## ## @code{@var{X} = caldays (@var{calD})} returns a numeric array with the ## number of days as represented in @var{calD}. ## ## A month has no fixed number of days, so an array carrying any month at ## all cannot be stated in days and is an error rather than a guess. Use ## @code{split} on such an array, which reports each unit separately. The ## sign plays no part: a negative number of months is refused exactly as a ## positive one is. ## ## A @code{NaN} or infinite month count is not refused. It is not known to ## be non-zero, so it propagates as @code{NaN} or @code{Inf}, just as ## @code{calmonths}, @code{calquarters} and @code{calyears} already report ## it. MATLAB instead refuses such a span when it was built by the ## constructor, while accepting the very same span built by @code{caldays}; ## two spellings of one value cannot sensibly disagree, so both are ## accepted here. ## ## @code{caldays} is also available as a function, in which case it performs ## the opposite conversion. ## ## @seealso{calendarDuration.calyears, calendarDuration.calquarters, ## calendarDuration.calmonths, calendarDuration.calweeks, caldays} ## @end deftypefn function out = caldays (this) if (any (this.Months != 0 & isfinite (this.Months), 'all')) error ("calendarDuration.caldays: %s", monthlessErrMsg ()); endif out = this.Days; endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{X} =} calweeks (@var{calD}) ## ## Calendar duration in weeks. ## ## @code{@var{X} = calweeks (@var{calD})} returns a numeric array with the ## number of weeks as represented in @var{calD}, rounded towards negative ## infinity, so that @math{-15} days is @math{-3} weeks and not @math{-2}. ## ## As for @code{caldays}, an array carrying any month at all cannot be ## stated in weeks and is an error rather than a guess, whatever the sign ## of that month count. Use @code{split} on such an array. A @code{NaN} ## or infinite month count propagates rather than being refused, on the ## same reasoning as @code{caldays}. ## ## @code{calweeks} is also available as a function, in which case it ## performs the opposite conversion. ## ## @seealso{calendarDuration.calyears, calendarDuration.calquarters, ## calendarDuration.calmonths, calendarDuration.caldays, calweeks} ## @end deftypefn function out = calweeks (this) if (any (this.Months != 0 & isfinite (this.Months), 'all')) error ("calendarDuration.calweeks: %s", monthlessErrMsg ()); endif out = floor (this.Days / 7); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{X} =} calmonths (@var{calD}) ## ## Calendar duration in months. ## ## @code{@var{X} = calmonths (@var{calD})} returns a numeric array with the ## number of months as represented in @var{calD}. ## ## @code{calmonths} is also available as a function, in which case it ## performs the opposite conversion. ## ## @seealso{calendarDuration.calyears, calendarDuration.calquarters, ## calendarDuration.calweeks, calendarDuration.caldays, calmonths} ## @end deftypefn function out = calmonths (this) out = this.Months; endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{X} =} calquarters (@var{calD}) ## ## Calendar duration in quarters. ## ## @code{@var{X} = calquarters (@var{calD})} returns a numeric array with ## the number of quarters as represented in @var{calD}, rounded towards ## negative infinity, so that @math{-1} month is @math{-1} quarter. ## ## @code{calquarters} is also available as a function, in which case it ## performs the opposite conversion. ## ## @seealso{calendarDuration.calyears, calendarDuration.calmonths, ## calendarDuration.calweeks, calendarDuration.caldays, calquarters} ## @end deftypefn function out = calquarters (this) out = floor (this.Months / 3); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{X} =} calyears (@var{calD}) ## ## Calendar duration in years. ## ## @code{@var{X} = calyears (@var{calD})} returns a numeric array with the ## number of years as represented in @var{calD}, rounded towards negative ## infinity, so that @math{-1} month is @math{-1} year. ## ## @code{calyears} is also available as a function, in which case it ## performs the opposite conversion. ## ## @seealso{calendarDuration.calquarters, calendarDuration.calmonths, ## calendarDuration.calweeks, calendarDuration.caldays, calyears} ## @end deftypefn function out = calyears (this) out = floor (this.Months / 12); endfunction endmethods ################################################################################ ## ** Summary Information ** ## ################################################################################ ## Available Methods ## ## ## ## 'size' 'ndims' 'numel' 'nnz' ## ## 'length' 'keyHash' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{sz} =} size (@var{calD}) ## @deftypefnx {calendarDuration} {@var{dim_sz} =} size (@var{calD}, @var{dim}) ## @deftypefnx {calendarDuration} {@var{dim_sz} =} size (@var{calD}, @var{d1}, @var{d2}, @dots{}) ## @deftypefnx {calendarDuration} {[@var{rows}, @var{columns}, @dots{}, @var{dim_n_sz}] =} size (@dots{}) ## ## Size of a calendarDuration array. ## ## @code{@var{sz} = size (@var{calD})} returns a row vector with the size ## (number of elements) of each dimension for the calendar duration array ## @var{calD}. ## ## @code{@var{dim_sz} = size (@var{calD}, @var{dim})} returns the size of ## the corresponding dimension specified in @var{dim}. If @var{dim} is a ## vector, then @var{dim_sz} is a vector of the same length and with each ## element corresponding to a specified dimension. Multiple dimensions may ## also be specified as separate arguments. ## ## With a single output argument, @code{size} returns a row vector. With ## several, the size of dimension N is returned in the Nth argument. ## Asking for fewer output arguments than the array has dimensions folds ## the trailing dimensions into the last one, so @code{[r, c] = size ## (@var{calD})} on a 2-by-2-by-2 array returns 2 and 4; asking for more ## pads with ones. When a dimension is named, however, the number of output ## arguments must equal the number of dimensions requested. ## ## @end deftypefn function varargout = size (this, varargin) if (! isempty (varargin)) ## A named dimension fixes how many outputs there may be. sz = size (this.Months, varargin{:}); if (nargout > 1 && numel (sz) != nargout) error (strcat ("calendarDuration.size: nargout does not match the", " number of requested dimensions.")); endif else sz = size (this.Months); ## With no dimension named, asking for more outputs than the array has ## dimensions pads with ones, and asking for fewer folds the trailing ## dimensions into the last output, as MATLAB does. if (nargout > numel (sz)) sz = [sz, ones(1, nargout - numel (sz))]; elseif (nargout > 1 && nargout < numel (sz)) sz = [sz(1:nargout-1), prod(sz(nargout:end))]; endif endif if (nargout == 0 || nargout == 1) varargout{1} = sz; else for i = 1:nargout varargout{i} = sz(i); endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{out} =} ndims (@var{calD}) ## ## Number of dimensions in a calendarDuration array. ## ## @code{@var{out} = ndims (@var{calD})} returns the number of dimensions ## of the calendar duration array @var{calD}. ## ## @end deftypefn function out = ndims (this) out = ndims (this.Months); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{out} =} numel (@var{calD}) ## ## Total number of elements in a calendarDuration array. ## ## @code{@var{out} = numel (@var{calD})} returns the number of elements in ## the calendarDuration array @var{calD}. ## ## @end deftypefn function out = numel (this, varargin) out = numel (this.Months); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{out} =} nnz (@var{calD}) ## ## Number of nonzero elements in calendarDuration array. ## ## @code{@var{out} = nnz (@var{calD})} returns the number of nonzero ## elements in the calendar duration array @var{calD}. ## ## @end deftypefn function out = nnz (this) m = this.Months(:); d = this.Days(:); h = hours (this.Time(:)); out = numel (m) - sum (m == 0 & d == 0 & h == 0); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{N} =} length (@var{calD}) ## ## Length of a calendarDuration vector. ## ## @code{@var{N} = length (@var{calD})} returns the size of the longest ## dimension of the calendarDuration array @var{calD}, unless any of its ## dimensions has zero length, in which case @code{length (@var{calD})} ## returns 0. ## ## @end deftypefn function N = length (this) if (isempty (this.Months)) N = 0; else N = max (size (this.Months)); endif endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{key} =} keyHash (@var{calD}) ## @deftypefnx {calendarDuration} {@var{key} =} keyHash (@var{calD}, @var{base}) ## ## Generate a hash code for a calendarDuration array. ## ## @code{@var{key} = keyHash (@var{calD})} generates a @qcode{uint64} scalar ## that represents the input array @var{calD}. @code{keyHash} utilizes the ## 64-bit FNV-1a variant of the Fowler-Noll-Vo non-cryptographic hash ## function. ## ## @code{@var{key} = keyHash (@var{calD}, @var{base})} also generates a ## 64-bit hash code using @var{base} as the offset basis for the FNV-1a ## hash algorithm. @var{base} must be a @qcode{uint64} integer type ## scalar. Use this syntax to cascade @code{keyHash} on multiple objects ## for which a single hash code is required. ## ## A calendarDuration array is keyed on its months, days and time ## components, so calendar units that are interchangeable share a key: ## @code{calmonths (12)} hashes as @code{calyears (1)}, and ## @code{calweeks (1)} as @code{caldays (7)}. Units that are not ## interchangeable stay distinct, @code{calmonths (1)} and ## @code{caldays (30)} among them. The @qcode{Format} property is display ## only and is not part of the key. Two arrays that @code{keyMatch} ## reports as the same key always hash alike. ## ## Note that unlike MATLAB, this implementation does not use any random ## seed. As a result, @code{keyHash} will always generate the exact same ## hash key for any particular input across different workers and Octave ## sessions. ## ## @end deftypefn function key = keyHash (this, base = []) ## Initialize string with size and class name size_str = sprintf ('%dx', size (this.Months))(1:end-1); init_str = [size_str class(this)]; if (! isempty (base)) if (! (isscalar (base) && isa (base, 'uint64'))) error ("calendarDuration.keyHash: BASE must be a UINT64 scalar."); endif key = __ckeyHash__(init_str, base); else key = __ckeyHash__(init_str); endif ## Compute hash with underlying calendarDuration array values if (! isempty (this.Months)) key = __nkeyHash__(this.Months(:), key); key = __nkeyHash__(this.Days(:), key); key = keyHash (this.Time, key); endif endfunction endmethods ################################################################################ ## ** Query Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'iscolumn' 'isempty' 'isequal' 'isequaln' ## ## 'isfinite' 'isinf' 'ismatrix' 'ismissing' ## ## 'isnan' 'isreal' 'isrow' 'isscalar' ## ## 'isvector' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{TF} =} iscolumn (@var{calD}) ## ## Return true if calendarDuration array is a column vector. ## ## @code{@var{TF} = iscolumn (@var{calD})} returns a logical scalar ## @var{TF}, which is @qcode{true} if the calendar duration array @var{calD} ## is a column vector and @qcode{false} otherwise. A column vector is a 2-D ## array for which @code{size (@var{X})} returns @code{[@var{N}, 1]} with ## non-negative @var{N}. ## ## @end deftypefn function TF = iscolumn (this) TF = iscolumn (this.Months); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{TF} =} isempty (@var{calD}) ## ## Return true if calendarDuration array is empty. ## ## @code{@var{TF} = isempty (@var{calD})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the calendar duration array @var{calD} is empty ## and @qcode{false} otherwise. ## ## @end deftypefn function TF = isempty (this) TF = isempty (this.Months); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{TF} =} isequal (@var{calD1}, @var{calD2}) ## @deftypefnx {calendarDuration} {@var{TF} =} isequal (@var{calD1}, @var{calD2}, @dots{}) ## ## Return true if calendarDuration arrays are equal. ## ## @code{@var{TF} = isequal (@var{calD1}, @var{calD2})} returns a logical ## scalar @var{TF}, which is @qcode{true} if the calendar duration arrays ## @var{calD1} and @var{calD2} contain the same values and @qcode{false} ## otherwise. ## ## @code{@var{TF} = isequal (@var{calD1}, @var{calD2}, @dots{})} returns a ## logical scalar @var{TF}, which is @qcode{true} if all input arguments are ## calendar duration arrays with equal values in each corresponding elements ## and @qcode{false} otherwise. ## ## @end deftypefn function TF = isequal (this, varargin) if (numel (varargin) < 1) error ("calendarDuration.isequal: too few input arguments."); endif n_dim = size (this); for i = 1:numel (varargin) tmp = varargin{i}; if (! isa (tmp, 'calendarDuration')) ## isequal answers about any pair of values and never refuses one: ## an argument of another class simply is not equal to a span. TF = false; return; endif if (! isequal (n_dim, size (tmp))) TF = false; return; endif if (this == tmp) TF = true; else TF = false; return; endif endfor endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{TF} =} isequaln (@var{calD1}, @var{calD2}) ## @deftypefnx {calendarDuration} {@var{TF} =} isequaln (@var{calD1}, @var{calD2}, @dots{}) ## ## Return true if calendarDuration arrays are equal under the assumption ## that missing elements are equal. ## ## @code{@var{TF} = isequaln (@var{calD1}, @var{calD2})} returns a logical ## scalar @var{TF}, which is @qcode{true} if the calendar duration arrays ## @var{calD1} and @var{calD2} contain the same values or corresponding ## missing elements and @qcode{false} otherwise. ## ## @code{@var{TF} = isequaln (@var{calD1}, @var{calD2}, @dots{})} returns a ## logical scalar @var{TF}, which is @qcode{true} if all input arguments ## are calendar duration arrays with equal values or corresponding missing ## elements and @qcode{false} otherwise. ## ## @end deftypefn function TF = isequaln (this, varargin) if (numel (varargin) < 1) error ("calendarDuration.isequaln: too few input arguments."); endif n_dim = size (this); ## Force NaNs to zeros i_nan = isnan (this); if (any (i_nan, 'all')) this.Months(i_nan) = 0; this.Days(i_nan) = 0; this.Time(i_nan) = duration (0, 0, 0); endif for i = 1:numel (varargin) tmp = varargin{i}; if (! isa (tmp, 'calendarDuration')) ## isequal answers about any pair of values and never refuses one: ## an argument of another class simply is not equal to a span. TF = false; return; endif if (! isequal (n_dim, size (tmp))) TF = false; return; endif ## Force NaNs to zeros i_nan = isnan (tmp); if (any (i_nan, 'all')) tmp.Months(i_nan) = 0; tmp.Days(i_nan) = 0; tmp.Time(i_nan) = duration (0, 0, 0); endif if (this == tmp) TF = true; else TF = false; return; endif endfor endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{TF} =} isfinite (@var{calD}) ## ## Return true for calendar durations that are finite. ## ## @code{@var{TF} = isfinite (@var{calD})} returns a logical array @var{TF} ## of the same size as @var{calD} containing @qcode{true} for each ## corresponding element of @var{calD} that is finite and @qcode{false} ## otherwise. Finite elements are those which are neither infinite nor ## Not-A-Number. ## ## @end deftypefn function TF = isfinite (this) TF = isfinite (this.Months); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{TF} =} isinf (@var{calD}) ## ## Return true for calendar durations that are infinite. ## ## @code{@var{TF} = isinf (@var{calD})} returns a logical array @var{TF} ## of the same size as @var{calD} containing @qcode{true} for each ## corresponding element of @var{calD} that is either @qcode{Inf} or ## @qcode{-Inf} and @qcode{false} otherwise. ## ## @end deftypefn function TF = isinf (this) TF = isinf (this.Months); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{TF} =} ismatrix (@var{calD}) ## ## Return true if calendarDuration array is a 2-D array. ## ## @code{@var{TF} = ismatrix (@var{calD})} returns a logical scalar ## @var{TF}, which is @qcode{true} if the calendarDuration array @var{calD} ## is a matrix and @qcode{false} otherwise. A matrix is an array of any ## type where @code{ndims (@var{X}) == 2} and for which ## @code{size (@var{X})} returns @code{[@var{H}, @var{W}]} with non-negative ## @var{H} and @var{W}. ## ## @end deftypefn function TF = ismatrix (this) TF = ismatrix (this.Months); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{TF} =} ismissing (@var{calD}) ## @deftypefnx {calendarDuration} {@var{TF} =} ismissing (@var{calD}, @var{indicator}) ## ## Find missing data in a calendarDuration array. ## ## Missing values in calendarDuration arrays are represented by @qcode{NaN}, ## thus @code{@var{TF} = ismissing (@var{calD})} is equivalent to ## @code{@var{TF} = isnan (@var{calD})}. ## ## @code{@var{TF} = ismissing (@var{calD}, @var{indicator})} marks instead ## the elements of @var{calD} equal to the values in @var{indicator}, a ## calendarDuration scalar or vector; the standard missing values are ## ignored, unless explicitly stated in @var{indicator}. Elements are ## compared as by @code{calendarDuration.eq}, component by component. A ## @qcode{NaN} in @var{indicator} matches the @qcode{NaN} elements of ## @var{calD}, and so does @code{missing}. MATLAB accepts no indicator for ## a calendarDuration array, so the @var{indicator} argument is an Octave ## extension. ## ## @end deftypefn function TF = ismissing (this, varargin) if (nargin > 2) error ("calendarDuration.ismissing: too many input arguments."); endif if (isempty (varargin)) TF = isnan (this.Months); return; endif indicator = varargin{1}; if (isa (indicator, 'missing')) TF = isnan (this.Months); return; endif if (! iscalendarduration (indicator)) error (strcat ("calendarDuration.ismissing: INDICATOR must be", ... " a 'calendarDuration' array.")); endif if (! isvector (indicator)) error ("calendarDuration.ismissing: INDICATOR must be a vector."); endif TF = false (size (this)); for i = 1:numel (indicator) value = subset (indicator, i); if (isnan (value)) TF(isnan (this.Months)) = true; else TF(this == value) = true; endif endfor endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{TF} =} isnan (@var{calD}) ## ## Return true for calendar durations that are Not-A-Number. ## ## @code{@var{TF} = isnan (@var{calD})} returns a logical array @var{TF} ## of the same size as @var{calD} containing @qcode{true} for each ## corresponding element of @var{calD} that is @qcode{NaN} and @qcode{false} ## otherwise. ## ## @end deftypefn function TF = isnan (this) TF = isnan (this.Months); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{TF} =} isreal (@var{calD}) ## ## Always return true for calendarDuration arrays. ## ## @code{@var{TF} = isreal (@var{calD})} always returns a logical scalar ## @qcode{true} value, if the input argument is a calendarDuration array. ## ## @end deftypefn function TF = isreal (this) TF = true; endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{TF} =} isrow (@var{calD}) ## ## Return true if calendarDuration array is a row vector. ## ## @code{@var{TF} = isrow (@var{calD})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the calendarDuration array @var{calD} is a row ## vector and @qcode{false} otherwise. A row vector is a 2-D array for ## which @code{size (@var{X})} returns @code{[1, @var{N}]} with non-negative ## @var{N}. ## ## @end deftypefn function TF = isrow (this) TF = isrow (this.Months); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{TF} =} isscalar (@var{calD}) ## ## Return true if calendarDuration array is a scalar. ## ## @code{@var{TF} = isscalar (@var{calD})} returns a logical scalar ## @var{TF}, which is @qcode{true} if the calendarDuration array @var{calD} ## is also a scalar and @qcode{false} otherwise. A scalar is a single ## element object for which @code{size (@var{X})} returns @code{[1, 1]}. ## ## @end deftypefn function TF = isscalar (this) TF = isscalar (this.Months); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{TF} =} isvector (@var{calD}) ## ## Return true if calendarDuration array is a vector. ## ## @code{@var{TF} = isvector (@var{calD})} returns a logical scalar ## @var{TF}, which is @qcode{true} if the calendarDuration array @var{calD} ## is a vector and @qcode{false} otherwise. A vector is a 2-D array for ## which one of the dimensions is equal to 1 (either @math{1*N} or ## @math{N*1}). By definition, a scalar is also a vector. ## ## @end deftypefn function TF = isvector (this) TF = isvector (this.Months); endfunction endmethods ################################################################################ ## ** Mathematical Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'minus' 'uminus' 'plus' 'uplus' ## ## 'times' 'mtimes' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{C} =} minus (@var{A}, @var{B}) ## ## Subtraction for calendarDuration arrays. ## ## @code{@var{C} = minus (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{C} = @var{A} - @var{B}} and returns the result of subtracting ## the corresponding elements of @var{B} from those of @var{A}. @var{C} is ## a calendarDuration array of the same size as the input arguments after ## the necessary (if required) expansion. @var{A} and @var{B} must be size ## compatible, which translates to they can be the same size, one can be ## scalar, or for every dimension, their dimension sizes must be equal or ## one of them must be 1. ## ## Either @var{A} or @var{B} may also be a duration or a numeric array with ## the latter representing duration days and being internally converted to a ## duration array with the @code{days ()} function. ## ## @end deftypefn function out = minus (A, B) if (isa (A, 'calendarDuration') && isa (B, 'calendarDuration')) out = A; out.Months = A.Months - B.Months; out.Days = A.Days - B.Days; out.Time = A.Time - B.Time; out.Format = unionFormat ({A.Format, B.Format}); elseif (isa (A, 'calendarDuration') && isa (B, 'duration')) out = A; tmp = zeros (size (B)); out.Months = A.Months - tmp; out.Days = A.Days - tmp; out.Time = A.Time - B; elseif (isa (A, 'calendarDuration') && isScaleType (B)) out = A; tmp = zeros (size (B)); out.Months = out.Months - tmp; out.Days = out.Days - tmp; out.Time = out.Time - days (double (B)); elseif (isScaleType (A) && isa (B, 'calendarDuration')) out = B; tmp = zeros (size (A)); out.Months = -out.Months + tmp; out.Days = -out.Days + tmp; out.Time = -out.Time + days (double (A)); else error (strcat ("calendarDuration: subtraction is not defined", ... " between '%s' and '%s' arrays."), class (A), class (B)); endif out = broadcastProperties (out); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{C} =} uminus (@var{A}) ## ## Unary minus for calendarDuration arrays. ## ## @code{@var{C} = uminus (@var{A})} is the equivalent of the syntax ## @code{@var{C} = -@var{A}} and returns @var{A} after negating its ## elements. @var{C} is a calendarDuration array of the same size as ## @var{A}. ## ## @end deftypefn function out = uminus (A) out = A; out.Months = -A.Months; out.Days = -A.Days; out.Time = -A.Time; out = dropNegZero (out); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{C} =} plus (@var{A}, @var{B}) ## ## Addition for calendarDuration arrays. ## ## @code{@var{C} = plus (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{C} = @var{A} + @var{B}} and returns the result of adding the ## corresponding elements of @var{A} and @var{B}. @var{C} is a ## calendarDuration array of the same size as the input arguments after the ## necessary (if required) expansion. @var{A} and @var{B} must be size ## compatible, which translates to they can be the same size, one can be ## scalar, or for every dimension, their dimension sizes must be equal or ## one of them must be 1. ## ## Either @var{A} or @var{B} may also be a duration or a numeric array with ## the latter representing duration days and being internally converted to a ## duration array with the @code{days ()} function. ## ## @end deftypefn function out = plus (A, B) if (isa (B, 'datetime')) out = B + A; return; endif if (isa (A, 'calendarDuration') && isa (B, 'calendarDuration')) out = A; out.Months = A.Months + B.Months; out.Days = A.Days + B.Days; out.Time = A.Time + B.Time; ## Both operands name units, so the result displays either one's, the ## same union concatenation takes. out.Format = unionFormat ({A.Format, B.Format}); elseif (isa (A, 'calendarDuration') && isa (B, 'duration')) out = A; tmp = zeros (size (B)); out.Months = A.Months + tmp; out.Days = A.Days + tmp; out.Time = A.Time + B; elseif (isa (A, 'calendarDuration') && isScaleType (B)) out = A; tmp = zeros (size (B)); out.Months = A.Months + tmp; out.Days = A.Days + tmp; out.Time = A.Time + days (double (B)); elseif (isScaleType (A) && isa (B, 'calendarDuration')) out = B; tmp = zeros (size (A)); out.Months = B.Months + tmp; out.Days = B.Days + tmp; out.Time = B.Time + days (double (A)); else error (strcat ("calendarDuration: addition is not defined", ... " between '%s' and '%s' arrays."), class (A), class (B)); endif out = broadcastProperties (out); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{C} =} uplus (@var{A}) ## ## Unary plus for calendarDuration arrays. ## ## @code{@var{C} = uplus (@var{A})} is the equivalent of the syntax ## @code{@var{C} = +@var{A}} and returns a copy of @var{A}. @var{C} is a ## calendarDuration array of the same size as @var{A}. ## ## @end deftypefn function out = uplus (A) out = A; out.Months = A.Months; out.Days = A.Days; out.Time = A.Time; endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{C} =} times (@var{A}, @var{B}) ## ## Element-by-element multiplication for calendarDuration arrays. ## ## @code{@var{C} = times (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{C} = @var{A} .* @var{B}} and returns the element-by-element ## multiplication product of inputs @var{A} and @var{B}. Either @var{A} or ## @var{B} must be a calendarDuration array and its complement must be a ## double array. ## ## @var{C} is a calendarDuration array of the same size as the input ## arguments after the necessary (if required) expansion. @var{A} and ## @var{B} must be size compatible, which translates to they can be the same ## size, one can be scalar, or for every dimension, their dimension sizes ## must be equal or one of them must be 1. ## ## @end deftypefn function out = times (A, B) if (isa (A, 'calendarDuration') && isScaleType (B)) out = A; tmp = double (B); elseif (isScaleType (A) && isa (B, 'calendarDuration')) out = B; tmp = double (A); else error (strcat ("calendarDuration: multiplication is not defined", ... " between '%s' and '%s' arrays."), class (A), class (B)); endif if (isNonIntegral (tmp)) error (strcat ("calendarDuration: multiplication by non-integer", ... " values is not defined.")); endif M = out.Months; D = out.Days; T = seconds (out.Time); out.Months = out.Months .* tmp; out.Days = out.Days .* tmp; out.Time = out.Time .* tmp; out = scaleNonFinite (out, M, D, T, tmp); out = broadcastProperties (out); out = dropNegZero (out); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{C} =} mtimes (@var{A}, @var{B}) ## ## Matrix multiplication for calendarDuration arrays. ## ## @code{@var{C} = mtimes (@var{A}, @var{B})} is the equivalent of the ## syntax @code{@var{C} = @var{A} * @var{B}} and returns the matrix ## multiplication product of inputs @var{A} and @var{B}. Either @var{A} or ## @var{B} must be a calendarDuration array and its complement must be a ## double array. ## ## @var{C} is a calendarDuration array of the same size as the input ## arguments after the necessary (if required) expansion. @var{A} and ## @var{B} must be size compatible, which translates to they can be the same ## size, one can be scalar, or for every dimension, their dimension sizes ## must be equal or one of them must be 1. ## ## @end deftypefn function out = mtimes (A, B) if (isa (A, 'calendarDuration') && isScaleType (B)) out = A; tmp = double (B); M = A.Months; D = A.Days; T = seconds (A.Time); out.Months = A.Months * tmp; out.Days = A.Days * tmp; out.Time = A.Time * tmp; elseif (isScaleType (A) && isa (B, 'calendarDuration')) out = B; tmp = double (A); M = B.Months; D = B.Days; T = seconds (B.Time); out.Months = tmp * B.Months; out.Days = tmp * B.Days; out.Time = tmp * B.Time; else error (strcat ("calendarDuration: matrix multiplication is", ... " not defined between '%s' and '%s' arrays."), ... class (A), class (B)); endif if (isNonIntegral (tmp)) error (strcat ("calendarDuration: matrix multiplication by", ... " non-integer values is not defined.")); endif out = scaleNonFinite (out, M, D, T, tmp); out = broadcastProperties (out); out = dropNegZero (out); endfunction endmethods ################################################################################ ## ** Equality and Filter Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'eq' 'ne' 'unique' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{TF} =} eq (@var{A}, @var{B}) ## ## Test for equality between calendarDuration arrays. ## ## @code{@var{TF} = eq (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} == @var{B}} and returns a logical array with ## elements set to @qcode{true} where calendarDuration arrays @var{A} and ## @var{B} are equal, otherwise set to @qcode{false}. Missing values are ## not equal to each other. Hence, any @code{NaN} values in @var{A} or ## @var{B} result to @qcode{false} elements in @var{TF}. ## ## @var{A} and @var{B} must be size compatible, which translates to they can ## be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. The size of the ## output @var{TF} is the same as the size of input arrays after their ## expansion according to the broadcasting rules. ## ## MATLAB defines no equality for calendarDuration arrays, so this is an ## Octave extension. Two spans are compared component by component, which ## is what MATLAB's own @code{isequal} does, so the two agree element for ## element: @code{calweeks (1) == caldays (7)} is @qcode{true}, both being ## seven days, while @code{caldays (1) == calendarDuration (0, 0, 0, 24, ## 0, 0)} is @qcode{false}, a day and twenty-four hours sitting in ## different components. ## ## @end deftypefn function TF = eq (A, B) if (! (iscalendarduration (A) && iscalendarduration (B))) error (strcat ("calendarDuration.eq: equality is not defined", ... " between '%s' and '%s' arrays."), class (A), class (B)); endif TF = A.Months == B.Months & A.Days == B.Days & A.Time == B.Time; endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{TF} =} ne (@var{A}, @var{B}) ## ## Test for inequality between calendarDuration arrays. ## ## @code{@var{TF} = ne (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} != @var{B}} and returns a logical array with ## elements set to @qcode{true} where calendarDuration arrays @var{A} and ## @var{B} are not equal, otherwise set to @qcode{false}. Missing values ## are not equal to each other. Hence, any @code{NaN} values in @var{A} or ## @var{B} result to @qcode{true} elements in @var{TF}. ## ## @var{A} and @var{B} must be size compatible, which translates to they can ## be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. The size of the ## output @var{TF} is the same as the size of input arrays after their ## expansion according to the broadcasting rules. ## ## MATLAB defines no equality for calendarDuration arrays, so this is an ## Octave extension. Two spans are compared component by component, which ## is what MATLAB's own @code{isequal} does, so the two agree element for ## element: @code{calweeks (1) == caldays (7)} is @qcode{true}, both being ## seven days, while @code{caldays (1) == calendarDuration (0, 0, 0, 24, ## 0, 0)} is @qcode{false}, a day and twenty-four hours sitting in ## different components. ## ## @end deftypefn function TF = ne (A, B) if (! (iscalendarduration (A) && iscalendarduration (B))) error (strcat ("calendarDuration.ne: inequality is not defined", ... " between '%s' and '%s' arrays."), class (A), class (B)); endif TF = A.Months != B.Months | A.Days != B.Days | A.Time != B.Time; endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{B} =} unique (@var{A}) ## @deftypefnx {calendarDuration} {@var{B} =} unique (@var{A}, @var{setOrder}) ## @deftypefnx {calendarDuration} {@var{B} =} unique (@var{A}, @var{occurrence}) ## @deftypefnx {calendarDuration} {@var{B} =} unique (@var{A}, @var{setOrder}, @var{occurrence}) ## @deftypefnx {calendarDuration} {@var{B} =} unique (@var{A}, @var{occurrence}, @var{setOrder}) ## @deftypefnx {calendarDuration} {@var{B} =} unique (@var{A}, @dots{}, @qcode{'rows'}) ## @deftypefnx {calendarDuration} {[@var{B}, @var{ixA}, @var{ixB}] =} unique (@dots{}) ## ## Unique values in a calendarDuration array. ## ## @code{@var{B} = unique (@var{A})} returns the unique values of the ## calendarDuration array @var{A} in sorted order. ## ## @code{@var{B} = unique (@var{A}, @var{setOrder})} returns the unique ## values of the calendarDuration array @var{A} in an order as specified by ## @var{setOrder}, which can be either of the following values: ## ## @itemize ## @item @qcode{'sorted'} (default) returns the unique values sorted in ## ascending order. ## @item @qcode{'stable'} returns the unique values according to their order ## of occurrence. ## @end itemize ## ## @code{@var{B} = unique (@var{A}, @var{occurrence})} returns the unique ## values of the calendarDuration array @var{tblA} according to their order ## of occurrence. @var{occurrence} can be either of the following values: ## ## @itemize ## @item @qcode{'first'} (default) returns the first occurrence of each ## unique value, i.e. the lowest possible indices are returned. ## @item @qcode{'last'} returns the last occurrence of each unique value, ## i.e. the highest possible indices are returned. ## @end itemize ## ## You can specify @var{setOrder} and @var{occurrence} arguments together. ## ## @code{@var{B} = unique (@var{A}, @dots{}, @qcode{'rows'})} returns the ## unique rows of @var{A} by treating each row as a single entity. The ## @qcode{'rows'} option can be used alone or in any combination with the ## @var{setOrder} and @var{occurrence} arguments. @qcode{'rows'} can be ## placed at any position in the function's argument list after the input ## array @var{A}. However, this syntax is only valid for 2-dimensional ## calendarDuration arrays. ## ## @code{[@var{tblB}, @var{ixA}, @var{ixB}] = unique (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} using any of the previous syntaxes. ## @var{ixA} and @var{ixB} map the arrays @var{A} and @var{B} to one another ## such that @qcode{@var{B} = @var{A}(@var{ixA})} and ## @qcode{@var{A} = @var{B}(@var{ixB})}. When the @qcode{'rows'} optional ## argument is specified, then @qcode{@var{B} = @var{A}(@var{ixA},:)} and ## @qcode{@var{tblA} = @var{tblB}(@var{ixB},:)}. ## ## @end deftypefn function [B, ixA, ixB] = unique (A, varargin) ## 'legacy' option is not supported if (any (strcmp ("legacy", varargin))) error ("calendarDuration.unique: 'legacy' option is not supported."); endif ## Handle each property array separately [~, ~, Midx] = __unique__ (A.Months, varargin{:}); [~, ~, Didx] = __unique__ (A.Days, varargin{:}); [~, ~, Tidx] = unique (A.Time, varargin{:}); ## Use indices to find unique calendarDuration values if (any (strcmp ('rows', varargin))) [~, ixA, ixB] = __unique__ ([Midx, Didx, Tidx], varargin{:}); if (any (strcmp ('last', varargin))) [~, ixA, ~] = __unique__ (ixB, 'last'); endif B = subset (A, ixA, ':'); else [~, ixA, ixB] = __unique__ ([Midx, Didx, Tidx], 'rows', varargin{:}); if (any (strcmp ('last', varargin))) [~, ixA, ~] = __unique__ (ixB, 'last'); endif B = subset (A, ixA); endif endfunction endmethods ################################################################################ ## ** Array Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'cat' 'horzcat' 'vertcat' 'repmat' ## ## 'repelem' 'repelems' 'reshape' 'circshift' ## ## 'permute' 'ipermute' 'transpose' 'ctranspose' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{C} =} cat (@var{dim}, @var{A}, @var{B}, @dots{}) ## ## Concatenate calendarDuration arrays. ## ## @code{@var{C} = cat (@var{dim}, @var{A}, @var{B}, @dots{})} concatenates ## calendarDuration arrays @var{A}, @var{B}, @dots{} along dimension ## @var{dim}. All input arrays must have the same size except along the ## operating dimension @var{dim}. Any of the input arrays may also be ## string arrays or cell arrays of character vectors of compatible size. ## Additionally, an input can be a numeric matrix, which when parsed to the ## constructor will return a calendarDuration array of compatible size. ## ## @end deftypefn function out = cat (dim, varargin) args = varargin; [args{:}] = promote (varargin{:}); out = args{1}; fieldArgs = cellfun (@(obj) obj.Months, args, 'UniformOutput', false); out.Months = cat (dim, fieldArgs{:}); fieldArgs = cellfun (@(obj) obj.Days, args, 'UniformOutput', false); out.Days = cat (dim, fieldArgs{:}); fieldArgs = cellfun (@(obj) obj.Time, args, 'UniformOutput', false); out.Time = cat (dim, fieldArgs{:}); ## The result displays in the union of the inputs' formats, so ## concatenating weeks with quarters keeps both rather than widening ## either back to days and months. fmts = cellfun (@(obj) obj.Format, args, 'UniformOutput', false); out.Format = unionFormat (fmts); out = broadcastProperties (out); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{C} =} horzcat (@var{A}, @var{B}, @dots{}) ## ## Horizontal concatenation of calendarDuration arrays. ## ## @code{@var{C} = horzcat (@var{A}, @var{B}, @dots{}} is the equivalent of ## the syntax @code{@var{B} = [@var{A}, @var{B}, @dots{}]} and horizontally ## concatenates the calendarDuration arrays @var{A}, @var{B}, @dots{}. All ## input arrays must have the same size except along the second dimension. ## Any of the input arrays may also be string arrays or cell arrays of ## character vectors of compatible size. Additionally, an input can be a ## numeric matrix, which when parsed to the constructor will return a ## calendarDuration array of compatible size. ## ## @end deftypefn function out = horzcat (varargin) out = cat (2, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{C} =} vertcat (@var{A}, @var{B}, @dots{}) ## ## Vertical concatenation of calendarDuration arrays. ## ## @code{@var{C} = vertcat (@var{A}, @var{B}, @dots{}} is the equivalent of ## the syntax @code{@var{B} = [@var{A}; @var{B}; @dots{}]} and vertically ## concatenates the calendarDuration arrays @var{A}, @var{B}, @dots{}. All ## input arrays must have the same size except along the first dimension. ## Any of the input arrays may also be string arrays or cell arrays of ## character vectors of compatible size. Additionally, an input can be a ## numeric matrix, which when parsed to the constructor will return a ## calendarDuration array of compatible size. ## ## @end deftypefn function out = vertcat (varargin) out = cat (1, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{B} =} repmat (@var{A}, @var{n}) ## @deftypefnx {calendarDuration} {@var{B} =} repmat (@var{A}, @var{d1}, @dots{}, @var{dN}) ## @deftypefnx {calendarDuration} {@var{B} =} repmat (@var{A}, @var{dimvec}) ## ## Repeat copies of a calendarDuration array. ## ## @code{@var{B} = repmat (@var{A}, @var{n})} returns a calendarDuration ## array @var{B} containing @var{n} copies of the input calendarDuration ## array @var{A} along every dimension of @var{A}. ## ## @code{@var{B} = repmat (@var{A}, @var{d1}, @dots{}, @var{dN})} returns an ## array @var{B} containing copies of @var{A} along the dimensions specified ## by the list of scalar integer values @var{d1}, @dots{}, @var{dN}, which ## specify how many copies of @var{A} are made in each dimension. ## ## @code{@var{B} = repmat (@var{A}, @var{dimvec})} is equivalent to the ## previous syntax with @code{@var{dimvec} = [@var{d1}, @dots{}, @var{dN}]}. ## ## @end deftypefn function this = repmat (this, varargin) this.Months = repmat (this.Months, varargin{:}); this.Days = repmat (this.Days, varargin{:}); this.Time = repmat (this.Time, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{B} =} repelem (@var{A}, @var{n}) ## @deftypefnx {calendarDuration} {@var{B} =} repelem (@var{A}, @var{d1}, @dots{}, @var{dN}) ## ## Repeat copies of calendarDuration array elements. ## ## @code{@var{B} = repelem (@var{A}, @var{n})} returns a calendarDuration ## vector @var{B} containing repeated elements of the input @var{A}, which ## must be a calendarDuration vector. If @var{n} is a scalar, each element ## of @var{A} is repeated @var{n} times along the non-singleton dimension of ## @var{A}. If @var{n} is a vector, it must have the same elements as ## @var{A}, in which case it specifies the number of times to repeat each ## corresponding element of @var{A}. ## ## @code{@var{B} = repelem (@var{A}, @var{d1}, @dots{}, @var{dN})} returns ## an array @var{B} with each element of @var{A} repeated according to the ## the list of input arguments @code{@var{d1}, @dots{}, @var{dN}} each ## corresponding to a different dimension @code{1:ndims (@var{A})} of the ## input array @var{A}. @var{d1}, @dots{}, @var{dN} must be either scalars ## or vectors with the same length as the corresponding dimension of ## @var{A} containing non-negative integer values specifying the number of ## repetitions of each element along the corresponding dimension. ## ## @end deftypefn function this = repelem (this, varargin) this.Months = repelem (this.Months, varargin{:}); this.Days = repelem (this.Days, varargin{:}); this.Time = repelem (this.Time, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{B} =} repelems (@var{A}, @var{R}) ## ## Construct a vector of repeated elements from a calendarDuration array. ## ## @code{@var{B} = repelems (@var{A}, @var{R})} returns a calendarDuration ## vector @var{B} containing repeated elements of the input @var{A}, which ## must be a calendarDuration vector. @var{R} must be a @math{2*N} matrix ## of integers. Entries in the first row of @var{R} correspond to the ## linear indexing of the elements in @var{A} to be repeated. The ## corresponding entries in the second row of @var{R} specify the repeat ## count of each element. ## ## @end deftypefn function this = repelems (this, R) this.Months = repelems (this.Months, R); this.Days = repelems (this.Days, R); this.Time = repelems (this.Time, R); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{B} =} reshape (@var{A}, @var{d1}, @dots{}, @var{dN}) ## @deftypefnx {calendarDuration} {@var{B} =} reshape (@var{A}, @dots{}, @qcode{[]}, @dots{}) ## @deftypefnx {calendarDuration} {@var{B} =} reshape (@var{A}, @var{dimvec}) ## ## Reshape calendarDuration array. ## ## @code{@var{B} = reshape (@var{A}, @var{d1}, @dots{}, @var{dN})} returns a ## calendarDuration array @var{B} with specified dimensions @var{d1}, ## @dots{}, @var{dN}, whose elements are taken columnwise from the ## calendarDuration array @var{A}. The product of @var{d1}, @dots{}, ## @var{dN} must equal the total number of elements in @var{A}. ## ## @code{@var{B} = reshape (@var{A}, @dots{}, @qcode{[]}, @dots{})} returns ## a calendarDuration array @var{B} with one dimension unspecified which is ## calculated automatically so that the product of dimensions in @var{B} ## matches the total elements in @var{A}, which must be divisible the ## product of specified dimensions. An empty matrix @qcode{([])} is used to ## flag the unspecified dimension. ## ## @end deftypefn function this = reshape (this, varargin) this.Months = reshape (this.Months, varargin{:}); this.Days = reshape (this.Days, varargin{:}); this.Time = reshape (this.Time, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{B} =} circshift (@var{A}, @var{n}) ## @deftypefnx {calendarDuration} {@var{B} =} circshift (@var{A}, @var{n}, @var{dim}) ## ## Circularly shift the elements in a calendarDuration array. ## ## @code{@var{B} = circshift (@var{A}, @var{n})} circularly shifts the ## elements of the calendarDuration array @var{A} according to @var{n}. If ## @var{n} is a nonzero integer scalar, then the elements of @var{A} are ## shifted by @var{n} elements along the first non-singleton dimension of ## @var{A}. If @var{n} is a vector, it must not be longer that the number ## of dimensions of @var{A} with each value of @var{n} corresponding to a ## dimension in @var{A}. The sign of the value(s) in @var{n} specify the ## direction in the elements of @var{A} are shifted. ## ## @code{@var{B} = circshift (@var{A}, @var{n}, @var{dim})} circularly ## shifts the elements of the calendarDuration array @var{A} along the ## dimension specified by @var{dim}. In this case, @var{n} must be a scalar ## integer value. ## ## @end deftypefn function this = circshift (this, varargin) this.Months = circshift (this.Months, varargin{:}); this.Days = circshift (this.Days, varargin{:}); this.Time = circshift (this.Time, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{B} =} permute (@var{A}, @var{dims}) ## ## Generalized transpose for a calendarDuration N-D array. ## ## @code{@var{B} = permute (@var{A}, @var{dims})} returns the generalized ## transpose of the calendarDuration array @var{A} by rearranging its ## dimensions according to the permutation vector specified in @var{dims}. ## ## @var{dims} must index all the dimensions @code{1:ndims (@var{A})} of the ## input array @var{A}, in any order, but only once. The @var{N}th ## dimension of @var{A} gets remapped to the dimension in @var{B} specified ## by @code{@var{dims}(@var{N})}. ## ## @end deftypefn function this = permute (this, order) this.Months = permute (this.Months, order); this.Days = permute (this.Days, order); this.Time = permute (this.Time, order); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{A} =} ipermute (@var{B}, @var{dims}) ## ## Inverse of the generalized transpose for a calendarDuration N-D array. ## ## @code{@var{A} = ipermute (@var{B}, @var{dims})} returns the inverse of ## the generalized transpose performed by the @code{permute} function. The ## expression @code{ipermute (permute (@var{A}, @var{dims}), @var{dims})} ## returns the original array @var{A}. ## ## @var{dims} must index all the dimensions @code{1:ndims (@var{B})} of the ## input array @var{B}, in any order, but only once. The dimension of ## @var{B} specified in @code{@var{dims}(@var{N})} gets remapped to the ## @var{N}th dimension of @var{A}. ## ## @end deftypefn function this = ipermute (this, order) this.Months = ipermute (this.Months, order); this.Days = ipermute (this.Days, order); this.Time = ipermute (this.Time, order); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{B} =} transpose (@var{A}) ## ## Transpose a calendarDuration matrix. ## ## @code{@var{B} = transpose (@var{A})} is the equivalent of the syntax ## @code{@var{B} = @var{A}.'} and returns the transpose of the ## calendarDuration matrix @var{A}. ## ## @end deftypefn function this = transpose (this) this.Months = transpose (this.Months); this.Days = transpose (this.Days); this.Time = transpose (this.Time); endfunction ## -*- texinfo -*- ## @deftypefn {calendarDuration} {@var{B} =} ctranspose (@var{A}) ## ## Transpose a calendarDuration matrix. ## ## @code{@var{B} = ctranspose (@var{A})} is the equivalent of the syntax ## @code{@var{B} = @var{A}'} and returns the transpose of the ## calendarDuration matrix @var{A}. For calendarDuration arrays, ## @code{ctranspose} is identical to @code{transpose}. ## ## @end deftypefn function this = ctranspose (this) this.Months = ctranspose (this.Months); this.Days = ctranspose (this.Days); this.Time = ctranspose (this.Time); endfunction endmethods ################################################################################ ## ** Reference and Assignment Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'end' 'subsref' 'subsasgn' ## ## ## ################################################################################ methods (Hidden) ## Overloaded end keyword function last_index = end (this, end_dim, ndim_obj) lastdim = ndims (this); if (end_dim == ndim_obj && ndim_obj == 1) last_index = prod (size (this)); elseif (end_dim == ndim_obj && end_dim < lastdim) last_index = prod (size (this)(end_dim:lastdim)); else last_index = size (this, end_dim); endif endfunction ## Class specific subscripted reference function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case '()' out = this; out.Months = this.Months(s.subs{:}); out.Days = this.Days(s.subs{:}); out.Time = this.Time(s.subs{:}); case '{}' error (strcat ("calendarDuration.subsref: '{}' invalid indexing", ... " for referencing values. Use '()' instead.")); case '.' ## A field name may be given as a string scalar, as in MATLAB. if (isstring (s.subs) && isscalar (s.subs)) s.subs = char (s.subs); endif if (! (ischar (s.subs) && isrow (s.subs))) error (strcat ("calendarDuration.subsref: '.' index argument", ... " must be a character vector or a string", ... " scalar.")); endif switch (s.subs) case 'proxyArray' # used by 'table' class out = proxyArray (this); case 'Format' out = this.Format; otherwise error (strcat ("calendarDuration.subsref: unrecognized", ... " property: '%s'"), s.subs); endswitch endswitch ## Chained references if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction ## Class specific subscripted assignment function this = subsasgn (this, s, val) if (numel (s) > 1) error ("calendarDuration.subsasgn: chained subscripts not allowed."); endif switch s.type case '()' if (isempty (val)) this.Months(s.subs{:}) = []; this.Days(s.subs{:}) = []; this.Time(s.subs{:}) = []; return; elseif (isa (val, "missing")) ## A missing value assigns NaN to every calendar field, so the ## element reads back as missing whichever field is inspected. tmp = NaN (size (val)); this.Months(s.subs{:}) = tmp; this.Days(s.subs{:}) = tmp; this.Time(s.subs{:}) = duration (24 * tmp, 0, 0); this = broadcastProperties (this); elseif (isnumeric (val)) tmp = zeros (size (val)); this.Months(s.subs{:}) = tmp; this.Days(s.subs{:}) = tmp; this.Time(s.subs{:}) = duration (24 * double (val), 0, 0); this = broadcastProperties (this); elseif (isa (val, "calendarDuration")) this.Months(s.subs{:}) = val.Months; this.Days(s.subs{:}) = val.Days; this.Time(s.subs{:}) = val.Time; this = broadcastProperties (this); elseif (isa (val, "duration")) this.Months(s.subs{:}) = 0; this.Days(s.subs{:}) = 0; this.Time(s.subs{:}) = val; this = broadcastProperties (this); else error (strcat ("calendarDuration.subsasgn: assignment value", ... " must be calendarDuration or duration array", ... " or a numeric array representing 24-hour days.")); endif case '{}' error (strcat ("calendarDuration.subsasgn: '{}' invalid indexing", ... " for assigning values. Use '()' instead.")); case '.' ## A field name may be given as a string scalar, as in MATLAB. if (isstring (s.subs) && isscalar (s.subs)) s.subs = char (s.subs); endif if (! (ischar (s.subs) && isrow (s.subs))) error (strcat ("calendarDuration.subsasgn: '.' index argument", ... " must be a character vector or a string", ... " scalar.")); endif switch (s.subs) case 'Format' [errmsg, val] = checkFormatString (val); if (! isempty (errmsg)) error ("calendarDuration.subsasgn: 'Format' %s", errmsg); endif this.Format = val; otherwise error (strcat ("calendarDuration.subsasgn: unrecognized", ... " property: '%s'"), s.subs); endswitch endswitch endfunction endmethods methods (Access = private) ## Negating a span, or scaling it by a negative number, turns a zero ## component into a negative zero, which MATLAB does not keep and which ## shows in any printed component. Adding zero maps -0 to +0 and leaves ## every other value exactly as it was, NaN and the infinities included. ## A non-finite factor scales the span as a whole rather than component by ## component: 0 * Inf would leave NaN in every empty component, but MATLAB ## takes the sign of the span, so 3d * Inf is Inf and -5h * Inf is -Inf. ## A span whose components disagree in sign, or that is entirely zero, has ## no sign and gives NaN. M, D and T are the components before scaling. function this = scaleNonFinite (this, M, D, T, fac) if (! (isscalar (fac) || isscalar (M) || size_equal (M, fac))) return; # a true matrix product is outside the measured domain endif if (all (isfinite (fac(:)))) return; endif pos = M > 0 | D > 0 | T > 0; neg = M < 0 | D < 0 | T < 0; sgn = NaN (size (pos)); sgn(pos & ! neg) = 1; sgn(neg & ! pos) = -1; val = sgn .* fac; nf = ! isfinite (zeros (size (sgn)) + fac); this.Months(nf) = val(nf); this.Days(nf) = val(nf); this.Time(nf) = seconds (val(nf)); endfunction function this = dropNegZero (this) this.Months = this.Months + 0; this.Days = this.Days + 0; this.Time = this.Time + seconds (0); endfunction ## Return a subset of the array function this = subset (this, varargin) this = this; this.Months = this.Months(varargin{:}); this.Days = this.Days(varargin{:}); this.Time = this.Time(varargin{:}); endfunction ## Broadcast properties function this = broadcastProperties (this) ## Handle NaNs and Infs first is_nan = isnan (this.Months) | isnan (this.Days) | isnan (this.Time); isPinf = Inf == this.Months | Inf == this.Days | Inf == this.Time; isNinf = -Inf == this.Months | -Inf == this.Days | -Inf == this.Time; if (any (is_nan, 'all') || any (isPinf & isNinf, 'all')) is_nan = is_nan | (isPinf & isNinf); isPinf = isPinf & ! is_nan; isNinf = isNinf & ! is_nan; endif ## Broadcast NaNs this.Months(is_nan) = NaN; this.Days(is_nan) = NaN; this.Time(is_nan) = duration ([NaN, NaN, NaN]); ## Broadcast Infs if (any (isPinf, 'all')) this.Months(isPinf) = Inf; this.Days(isPinf) = Inf; this.Time(isPinf) = duration ([Inf, Inf, Inf]); endif if (any (isNinf, 'all')) this.Months(isNinf) = -Inf; this.Days(isNinf) = -Inf; this.Time(isNinf) = duration ([-Inf, -Inf, -Inf]); endif endfunction ## Promote numeric arrays to calendarDuration objects function varargout = promote (varargin) for i = 1:numel (varargin) if (isa (varargin{i}, "calendarDuration")) varargout{i} = varargin{i}; elseif (isa (varargin{i}, "missing")) ## A missing value becomes an all-NaN calendarDuration of the same ## shape. varargout{i} = calendarDuration (NaN (size (varargin{i})), NaN, NaN); elseif (isa (varargin{i}, "duration")) varargout{i} = calendarDuration (0, 0, 0, varargin{i}); elseif (isnumeric (varargin{i})) if (isempty (varargin{i})) varargout{i} = calendarDuration ([], [], []); else varargout{i} = calendarDuration (0, 0, 0, 24 * varargin{i}, 0, 0); endif else error ("calendarDuration: invalid input to constructor."); endif endfor endfunction ## Create a proxy array for sorting and set operations in tables function out = proxyArray (this) ## Handle shape (for multicolumn calendarDuration matrix) [rows, cols] = size (this); if (cols > 1) out = []; for i = 1:cols dt = hours (this.Time(:,i)); SC = [this.Months(:,i), this.Days(:,i), dt]; out = [out, SC]; endfor else dt = hours (this.Time); out = [this.Months, this.Days, dt]; endif endfunction endmethods endclassdef ## Days and weeks are only meaningful on their own. A month has no fixed ## number of days, so an array carrying any month at all cannot be stated in ## either unit, and MATLAB refuses to guess: it directs the caller to 'split', ## which reports each unit separately. The test is 'not zero', not 'positive': ## a negative month count is refused just as a positive one is. It is also ## restricted to FINITE months, which is a deviation and a deliberate one. ## MATLAB refuses caldays (calendarDuration (0, 0, NaN)) but accepts ## caldays (caldays (NaN)), and likewise for Inf -- the constructor path throws ## where the component-builder path returns the value. Since broadcasting puts ## the NaN in every component, the two are one value here and no predicate over ## Months can tell them apart; propagating is chosen over throwing because a ## NaN month is unknown rather than known to be non-zero, because it is what ## calmonths/calquarters/calyears already do, and because throwing would break ## the caldays (caldays (x)) == x round trip that MATLAB itself preserves. ## The message body is shared so that the two callers cannot drift apart; each ## emits it under its own name. ## Only a double or a logical may be added to, subtracted from, or multiplied ## with a span; MATLAB refuses every other numeric type, single included. function TF = isScaleType (x) TF = isa (x, 'double') || islogical (x); endfunction ## A span cannot be scaled by a fraction -- a fraction of a month has no ## meaning. Non-finite factors are exempt, being scale rather than count. function TF = isNonIntegral (x) TF = any (isfinite (x(:)) & fix (x(:)) != x(:)); endfunction function errmsg = monthlessErrMsg () errmsg = strcat ("cannot convert a calendarDuration to days or weeks", ... " when", ... " it contains a non-zero number of months. Use 'split'", ... " instead."); endfunction ## Check 'Format' string. The validated format is returned as well, so that a ## string scalar is converted to a character vector once, where it is checked, ## rather than at each of the callers that store it. function [errmsg, Format] = checkFormatString (Format) ## A string scalar is accepted and stored as a character vector if (isa (Format, 'string') && isscalar (Format)) Format = char (Format); endif ## Check for character vector. An empty character vector is let through to ## report the characters it is missing rather than its type. if (! (ischar (Format) && (isvector (Format) || isempty (Format)))) errmsg = "must be a character vector or a string scalar."; return; endif errmsg = ''; sf = @(x) strfind (Format, x); sp = cellfun (sf, {'y','q','m','w','d','t'}, 'UniformOutput', false); ## Check for characters outside the recognized set if (! all (ismember (Format, 'yqmwdt'))) errmsg = "must only contain characters from the 'yqmwdt' sequence."; ## Check for duplicate characters elseif (any (cellfun (@(x) numel (x) > 1, sp))) errmsg = "contains duplicate characters."; ## Check for 'm', 'd', and 't' being present elseif (any (cellfun (@isempty, sp([3,5,6])))) errmsg = "must contain 'm', 'd', and 't'."; ## Check order elseif (any (diff (cell2mat (sp)) < 1)) errmsg = "has invalid order of characters."; endif endfunction ## Render a whole component the way MATLAB does: six significant digits, and an ## exponent past them, so a million days reads '1e+06d'. Adding zero turns the ## negative zero that truncating a negative time leaves behind into a plain one, ## which is what keeps '0h' from reading '-0h'. function s = fmtUnit (v) s = sprintf ('%g', v + 0); endfunction ## Render the seconds component, which unlike the units above may carry a ## fraction. MATLAB shows it to five significant digits, so nothing is lost to ## a millisecond grid and 0.9995 seconds is not rounded up to a whole one. function s = fmtSecs (v) s = sprintf ('%.5g', v + 0); endfunction ## Unit a wholly zero element renders in: the smallest one any element of the ## array actually uses, or days when none does. The ladder runs from the ## largest unit to the smallest, and time is rendered as seconds. ## Unit a wholly zero element renders in: the smallest one the array actually ## used anywhere, or days when nothing did. The ladder runs largest to ## smallest, matching the order calendarDuration prints its components in. function u = zeroRenderUnit (els) ladder = {'y', 'q', 'mo', 'w', 'd', 's'}; used = regexp (strjoin ([els{:}, {''}], ' '), '[a-z]+', 'match'); ## Any time component at all renders the zero in seconds. if (any (ismember (used, {'h', 'm', 's'}))) u = '0s'; return; endif found = find (ismember (ladder, used), 1, 'last'); if (isempty (found)) u = '0d'; else u = ['0', ladder{found}]; endif endfunction ## Union of a set of display formats, in the canonical component order. Every ## calendarDuration format contains 'm', 'd' and 't', so the union always does ## too and stays a valid format. function fmt = unionFormat (fmts) ladder = 'yqmwdt'; all = [fmts{:}]; fmt = ladder(ismember (ladder, all)); endfunction ## 'pkg test' reads the BISTs from the file it is testing, so a class ## whose suite lives in inst/tests/ reads as carrying none. This block ## answers for it and checks nothing itself. %!test %! ## The suite for this class is inst/tests/calendarDuration.m-tst. pr0m1th3as-datatypes-9450b29/inst/calmonths.m000066400000000000000000000050561527370255000210270ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{calD} =} calmonths (@var{X}) ## ## Calendar duration in months. ## ## @code{@var{calD} = calmonths (@var{X})} returns a @qcode{calendarDuration} ## array representing calendar months equivalent to the values in @var{X}, which ## must be a numeric array of integer values. ## ## @code{calmonths} is also available as a method for @qcode{calendarDuration} ## arrays, in which case it performs the opposite conversion. ## ## @seealso{calendarDuration, calyears, calquarters, calweeks, caldays, ## calendarDuration.calmonths} ## @end deftypefn function out = calmonths (x) ## Check input if (nargin == 0) x = 1; elseif (! isnumeric (x)) error ("calmonths: input array must be numeric."); elseif (! isreal (x)) error ("calmonths: input array must be real."); endif xx = x; xx(isnan (x)) = 0; if (any (fix (xx(:)) != xx(:))) error ("calmonths: input array must contain only integer values."); endif out = calendarDuration (0, double (x), 0); endfunction %!demo %! ## `calmonths` builds a calendar duration of whole months. Twelve or more %! ## months normalise into years on display. %! %! calmonths (5) %! calmonths (14) %!test %! X = magic (3); %! D = calmonths (X); %! assert_equal (size (D), size (X)); %!test %! D = calmonths ([1, 2, 3]); %! assert_equal (calmonths (D), [1, 2, 3]); %!test %! D = calmonths ([1, 2, NaN, 4]); %! assert_equal (calmonths (D), [1, 2, NaN, 4]); %!test %! D = calmonths (int16 (1)); %! assert_equal (calmonths (D), 1); %!test %! D = calmonths (); %! assert_equal (calmonths (D), 1); %!error calmonths ("asd"); %!error calmonths (1+i); %!error ... %! calmonths (1.2); pr0m1th3as-datatypes-9450b29/inst/calquarters.m000066400000000000000000000052571527370255000213700ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{calD} =} calquarters (@var{X}) ## ## Calendar duration in quarters. ## ## @code{@var{calD} = calquarters (@var{X})} returns a @qcode{calendarDuration} ## array representing calendar quarters equivalent to the values in @var{X}, ## which must be a numeric array of integer values. ## ## @code{calquarters} is also available as a method for @qcode{calendarDuration} ## arrays, in which case it performs the opposite conversion. ## ## @seealso{calendarDuration, calyears, calmonths, calweeks, caldays, ## calendarDuration.calquarters} ## @end deftypefn function out = calquarters (x) ## Check input if (nargin == 0) x = 1; elseif (! isnumeric (x)) error ("calquarters: input array must be numeric."); elseif (! isreal (x)) error ("calquarters: input array must be real."); endif xx = x; xx(isnan (x)) = 0; if (any (fix (xx(:)) != xx(:))) error ("calquarters: input array must contain only integer values."); endif out = calendarDuration (0, double (x) * 3, 0, 'Format', 'qmdt'); endfunction %!demo %! ## `calquarters` builds a calendar duration of whole quarters (each quarter %! ## is three months). %! %! calquarters (3) %! %! ## Quarters combine with the other component builders. %! calquarters (2) + calmonths (1) %!test %! X = magic (3); %! D = calquarters (X); %! assert_equal (size (D), size (X)); %!test %! D = calquarters ([1, 2, 3]); %! assert_equal (calquarters (D), [1, 2, 3]); %!test %! D = calquarters ([1, 2, NaN, 4]); %! assert_equal (calquarters (D), [1, 2, NaN, 4]); %!test %! D = calquarters (int16 (1)); %! assert_equal (calquarters (D), 1); %!test %! D = calquarters (); %! assert_equal (calquarters (D), 1); %!error calquarters ("asd"); %!error calquarters (1+i); %!error ... %! calquarters (1.2); pr0m1th3as-datatypes-9450b29/inst/calweeks.m000066400000000000000000000051141527370255000206300ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{calD} =} calweeks (@var{X}) ## ## Calendar duration in weeks. ## ## @code{@var{calD} = calweeks (@var{X})} returns a @qcode{calendarDuration} ## array representing calendar weeks equivalent to the values in @var{X}, which ## must be a numeric array of integer values. ## ## @code{calweeks} is also available as a method for @qcode{calendarDuration} ## arrays, in which case it performs the opposite conversion. ## ## @seealso{calendarDuration, calyears, calquarters, calmonths, caldays, ## calendarDuration.calweeks} ## @end deftypefn function out = calweeks (x) ## Check input if (nargin == 0) x = 1; elseif (! isnumeric (x)) error ("calweeks: input array must be numeric."); elseif (! isreal (x)) error ("calweeks: input array must be real."); endif xx = x; xx(isnan (x)) = 0; if (any (fix (xx(:)) != xx(:))) error ("calweeks: input array must contain only integer values."); endif out = calendarDuration (0, 0, double (x) * 7, 'Format', 'ymwdt'); endfunction %!demo %! ## `calweeks` builds a calendar duration of whole weeks (each week is 7 %! ## days). %! %! calweeks (3) %! %! ## Combine the component builders by adding them. %! calweeks (2) + caldays (3) %!test %! X = magic (3); %! D = calweeks (X); %! assert_equal (size (D), size (X)); %!test %! D = calweeks ([1, 2, 3]); %! assert_equal (calweeks (D), [1, 2, 3]); %!test %! D = calweeks ([1, 2, NaN, 4]); %! assert_equal (calweeks (D), [1, 2, NaN, 4]); %!test %! D = calweeks (int16 (1)); %! assert_equal (calweeks (D), 1); %!test %! D = calweeks (); %! assert_equal (calweeks (D), 1); %!error calweeks ("asd"); %!error calweeks (1+i); %!error ... %! calweeks (1.2); pr0m1th3as-datatypes-9450b29/inst/calyears.m000066400000000000000000000050661527370255000206430ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{calD} =} calyears (@var{X}) ## ## Calendar duration in years. ## ## @code{@var{calD} = calyears (@var{X})} returns a @qcode{calendarDuration} ## array representing calendar years equivalent to the values in @var{X}, which ## must be a numeric array of integer values. ## ## @code{calyears} is also available as a method for @qcode{calendarDuration} ## arrays, in which case it performs the opposite conversion. ## ## @seealso{calendarDuration, calquarters, calmonths, calweeks, caldays, ## calendarDuration.calyears} ## @end deftypefn function out = calyears (x) ## Check input if (nargin == 0) x = 1; elseif (! isnumeric (x)) error ("calyears: input array must be numeric."); elseif (! isreal (x)) error ("calyears: input array must be real."); endif xx = x; xx(isnan (x)) = 0; if (any (fix (xx(:)) != xx(:))) error ("calyears: input array must contain only integer values."); endif out = calendarDuration (double (x), 0, 0); endfunction %!demo %! ## `calyears` builds a calendar duration of whole years. Add the component %! ## builders together to assemble a full span. %! %! calyears (2) %! calyears (1) + calmonths (6) + caldays (15) %!test %! X = magic (3); %! D = calyears (X); %! assert_equal (size (D), size (X)); %!test %! D = calyears ([1, 2, 3]); %! assert_equal (calyears (D), [1, 2, 3]); %!test %! D = calyears ([1, 2, NaN, 4]); %! assert_equal (calyears (D), [1, 2, NaN, 4]); %!test %! D = calyears (int16 (1)); %! assert_equal (calyears (D), 1); %!test %! D = calyears (); %! assert_equal (calyears (D), 1); %!error calyears ("asd"); %!error calyears (1+i); %!error ... %! calyears (1.2); pr0m1th3as-datatypes-9450b29/inst/categorical.m000066400000000000000000006667501527370255000213320ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef categorical ## -*- texinfo -*- ## @deftp {datatypes} categorical ## ## Array representing categorical data. ## ## A @code{categorical} array represents an array of values that correspond ## to a finite set of discrete categories, which can be either ordinal (having ## a mathematical ordering) or nominal. It is an efficient way to define ## groups of rows in a table or to other types of variables. ## ## Each @code{categorical} array stores the list of categories as a cell array ## of character vectors and a numeric array of @qcode{uint16} type as indices ## to the categories. The categorical array may also store elements of ## undefined categorical values, which represent the absence of a given ## category and correspond to the @qcode{NaN} value for numeric arrays or in ## general to the missing value for other data types. ## ## @code{categorical} arrays do not have any public properties, which can be ## indexed by using dot notation similarly to structures. However, there are ## several methods which can be used to modify their categories once they are ## constructed. ## ## @end deftp properties (SetAccess = private, Hidden) ## Category Names cats = {} ## Indices to categories code = uint16 ([]) ## elements isMissing = logical ([]) ## Flag for ordinal categories isOrdinal = false ## Flag for protecting category list isProtected = false endproperties methods (Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif __disp__ (this, 'categorical', in_name); endfunction ## Custom display function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif __disp__ (this, 'categorical'); endfunction endmethods methods (Static, Access = public) ## -*- texinfo -*- ## @deftypefn {categorical} {@var{E} =} categorical.empty () ## @deftypefnx {categorical} {@var{E} =} categorical.empty (@var{sz}) ## @deftypefnx {categorical} {@var{E} =} categorical.empty (@var{m}, @var{n}, @dots{}) ## ## Create an empty categorical array. ## ## @code{@var{E} = categorical.empty ()} returns a @math{0*0} empty ## categorical array with no categories. @code{categorical.empty (@var{m}, ## @var{n}, @dots{})} or @code{categorical.empty (@var{sz})} returns an ## empty categorical array of the requested size, which must have at least ## one dimension equal to zero. A lone dimension gives a square size, so ## @code{categorical.empty (3)} is an error while ## @code{categorical.empty (0)} is @math{0*0}. As for @code{zeros}, a ## negative dimension counts as zero, and a size vector with nothing in it ## names no size and gives @math{0*0}. ## ## @end deftypefn function E = empty (varargin) if (nargin == 0) sz = [0, 0]; elseif (nargin == 1 && ! isscalar (varargin{1})) sz = double (varargin{1}(:)).'; else sz = [varargin{:}]; endif if (! (isnumeric (sz) && isrow (sz) && all (sz == fix (sz)))) error ("categorical.empty: dimensions must be integer values."); endif ## A negative dimension is no smaller than none at all, and a size vector ## holding no dimensions names no size: 'zeros' and every other array ## constructor read both the same way. sz = max (sz, 0); if (isempty (sz)) sz = [0, 0]; elseif (isscalar (sz)) sz = [sz, sz]; endif if (all (sz != 0)) error (strcat ("categorical.empty: at least one dimension must be", ... " zero for an empty array.")); endif E = categorical (nan (sz)); endfunction endmethods ################################################################################ ## ** Create and convert 'categorical' type ** ## ################################################################################ ## Available Methods ## ## ## ## 'categorical' 'dispstrings' 'cellstr' 'char' ## ## 'double' 'single' 'int64' 'int32' ## ## 'int16' 'int8' 'uint64' 'uint32' ## ## 'uint16' 'uint8' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {categorical} {@var{C} =} categorical (@var{A}) ## @deftypefnx {categorical} {@var{C} =} categorical (@var{A}, @var{valueset}) ## @deftypefnx {categorical} {@var{C} =} categorical (@var{A}, @var{valueset}, @var{catnames}) ## @deftypefnx {categorical} {@var{C} =} categorical (@dots{}, @var{Name}, @var{Value}) ## ## Create a new array of categorical values. ## ## @code{@var{C} = categorical (@var{A})} creates a categorical array ## @var{C} from the input array @var{A}, which can be numeric, logical, ## datetime, duration, string, or cell array of character vectors. Input ## @var{A} can also be another categorical array. The categories in @var{C} ## the sorted unique values from the input array @var{A}. When the input ## array is string or cell array of character vectors, any leading or ## trailing white spaces are removed. Missing values in the input array ## correspond to @qcode{} elements in the created categorical ## array. By default, there is no category for undefined values in the ## output array. ## ## @code{@var{C} = categorical (@var{A}, @var{valueset})} creates a ## categorical array from input @var{A} with the categories specified in ## @var{valueset}, which must be a vector of unique values. The data type ## of input array @var{A} and @var{valueset} must be the same, unless they ## are string or cell arrays of character vectors, in which case they can be ## used interchangeably. Similarly to input array @var{A} any leading or ## trailing white spaces are removed, if @var{valueset} is a string or cell ## array of character vectors. ## ## @code{@var{C} = categorical (@var{A}, @var{valueset}, @var{catnames})} ## creates a categorical array from input @var{A} with the categories ## specified in @var{valueset} and named after the corresponding values in ## @var{catnames}, which must be specified either as a string array or a ## cell array of character vectors. If omitted, @code{categorical} uses ## the cellstring representation of @var{valueset} to name the specified ## category names. @var{catnames} must not contain any missing values, it ## may have duplicate names, and it must have the same number of elements as ## @var{valueset}. ## ## @code{@var{C} = categorical (@dots{}, @var{Name}, @var{Value})} further ## specifies additional parameters for creating categorical array @var{C}. ## ## @itemize ## @item @qcode{"Ordinal"} must be a logical scalar specifying that the ## categories in @var{C} have a numeric ordering relationship. By default, ## it is @qcode{false} and @code{categorical} creates a non-ordinal array. ## The elements of unordered categorical arrays can only be compared for ## equality. Any other relational operator cannot be used. Setting ## @qcode{"Ordinal"} to @qcode{true} results in a categorical array with ## mathematically ordered categories. The ordering goes from smallest to ## largest according to the order in @var{valueset} or the order of ## appearance in input array @var{A}, when @var{valueset} is not specified, ## in which case the unique values in @var{A} are not sorted in order to set ## the categories. Ordinal categorical arrays allow for relational ## operators such as @qcode{>=, >, <=, <}, as well as statistical operations ## such as @code{min}, @code{max}, and @code{median}. ## ## @item @qcode{"Protected"} must be a logical scalar specifying that the ## categories in @var{C} are protected. By default, it is @qcode{false} for ## unordered categorical arrays and it is always @qcode{true} for ordinal ## categorical arrays. Setting @qcode{"Protected"} to @qcode{true} prevents ## from assigning new values that do not correspond to existing categories. ## When @qcode{false}, assigning new values to the array automatically ## adds the categories of the values assigned. Hence, categorical arrays ## with different sets of categories can be combined/merged into a new ## array with set operations. ## @end itemize ## ## @seealso{categorical.categories, discretize, iscategorical} ## @end deftypefn function this = categorical (x, varargin) ## Return an empty categorical object if (nargin == 0) return; endif ## Parse optional Name-Value paired arguments optNames = {'Ordinal', 'Protected'}; dfValues = {false, false}; [Ordinal, Protected, args] = parsePairedArguments (optNames, dfValues, ... varargin(:)); ## Check optional Name-Value paired arguments if (! ismember (Ordinal, [0, 1])) error (strcat ("categorical: 'Ordinal' variable indicator", ... " must be either false (0) or true (1).")); endif if (! ismember (Protected, [0, 1])) error (strcat ("categorical: 'Protected' categories indicator", ... " must be either false (0) or true (1).")); endif if (Ordinal) this.isOrdinal = true; this.isProtected = true; elseif (Protected) this.isProtected = true; endif ## Function handle for identifying missing text fmt = @(x) isempty (strtrim (x)); ## Handle categorical input first if (isa (x, 'categorical')) this.cats = x.cats; this.code = x.code; this.isMissing = x.isMissing; ## Set new categories if valueset argument is provided if (numel (args) > 0) ## valueset must be a same type as data input (categorical) catvset = args{1}; if (isa (catvset, 'categorical')) isnanvset = catvset.isMissing; codevset = catvset.code; if (sum (isnanvset) > 1) error ("categorical: VALUESET contains multiple missing values."); elseif (numel (unique (codevset)) != numel (codevset)) error ("categorical: VALUESET contains non-unique values."); endif if (any (isnanvset)) if (numel (args) == 1) error (strcat ("categorical: VALUESET with missing value", ... " requires CATNAMES.")); endif ## Keep index of missing values in x isnanx = this.isMissing; ## Get CATNAMES catnames = args{2}; if (! (iscellstr (catnames) || isa (catnames, 'string'))) error ("categorical: invalid type of CATNAMES."); endif catnames = cellstr (catnames); ## codevset must include 0 since there is an undefined element if (numel (codevset) != numel (catnames)) error (strcat ("categorical: CATNAMES and VALUESET", ... " lengths do not match.")); endif if (any (cellfun (fmt, catnames))) error (strcat ("categorical: CATNAMES contain empty or", ... " missing strings.")); endif ## Separate missing from nonmissing catnames valueset = catvset.cats(codevset(! isnanvset)); ## Set nonmissing values this = setcats (this, valueset); ## Find code value for missing catname, add 1 to all code values ## equal to or greater than the missing code value, and convert ## all initial 0 code values to the new missing catname value. nanvalue = find (isnanvset); this.code(this.code >= nanvalue) += 1; this.code(isnanx) = uint16 (nanvalue); this.isMissing(isnanx) = false; this.cats = catnames(:); else valueset = catvset.cats(codevset); this = setcats (this, valueset); ## Set new category names if catname argument is provided if (numel (args) > 1) catnames = args{2}; if (! (iscellstr (catnames) || isa (catnames, 'string'))) error ("categorical: invalid type of CATNAMES."); endif catnames = cellstr (catnames); if (numel (valueset) != numel (catnames)) error (strcat ("categorical: CATNAMES and VALUESET", ... " lengths do not match.")); endif if (any (cellfun (fmt, catnames))) error (strcat ("categorical: CATNAMES contain empty or", ... " missing strings.")); endif this.cats = catnames(:); endif endif ## In case of duplicate catnames, regrouping is required if (numel (args) > 1 && ... numel (unique (catnames)) != numel (catnames)) ## Generate regrouping index to unique categories index2cat = __grp2idx__ (catnames); idx = ! this.isMissing; this.code(idx) = arrayfun (@(x) index2cat(x), this.code(idx)); this.cats = unique (this.cats, 'stable'); endif else error ("categorical: incompatible types of VALUESET and X."); endif endif return; endif ## Handle input arguments index2cat = []; isnanvset = false; ## Input Array (x) if (iscellstr (x)) classx = 'cellstr'; isnanx = cellfun (fmt, x); elseif (isnumeric (x)) classx = 'numeric'; isnanx = isnan (x); elseif (isa (x, 'datetime')) classx = 'datetime'; isnanx = isnat (x); elseif (isa (x, 'duration')) classx = 'duration'; isnanx = isnan (x); elseif (islogical (x)) classx = 'logical'; isnanx = isnan (x); elseif (isa (x, 'string')) classx = 'string'; isnanx = cellfun (fmt, cellstr (x)); else error ("categorical: invalid input type for X."); endif ## Force isnanx to logical array in case of empty text input isnanx = logical (isnanx); ## Categories (valueset) if (numel (args) > 0) valueset = args{1}; if (iscellstr (valueset)) classv = 'cellstr'; elseif (isnumeric (valueset)) classv = 'numeric'; else classv = class (valueset); endif if (any (strcmp (classx, {'cellstr', 'string'}))) if (! (iscellstr (valueset) || isa (valueset, 'string'))) error ("categorical: incompatible types of VALUESET and X."); endif if (iscellstr (valueset)) isnanvset = cellfun (fmt, valueset); else # string isnanvset = cellfun (fmt, cellstr (valueset)); endif elseif (isequal (classx, classv)) if (strcmp (classv, 'datetime')) isnanvset = isnat (valueset); else # numeric, duration, isnanvset = isnan (valueset); endif else error ("categorical: incompatible types of VALUESET and X."); endif if (any (isnanvset) && numel (args) == 1) error ("categorical: VALUESET with missing value requires CATNAMES."); endif if (sum (isnanvset) > 1) error ("categorical: VALUESET contains multiple missing values."); endif if (prod (size (unique (valueset))) < prod (size (valueset))) error ("categorical: VALUESET contains non-unique values."); endif ## Category names (catnames) if (numel (args) > 1) catnames = args{2}; if (! (iscellstr (catnames) || isa (catnames, 'string'))) error ("categorical: invalid type of CATNAMES."); endif catnames = cellstr (catnames); if (prod (size (valueset)) != numel (catnames)) error ("categorical: CATNAMES and VALUESET lengths do not match."); endif if (any (cellfun (@(x) isempty (strtrim (x)), catnames))) error ("categorical: CATNAMES contain empty or missing strings."); endif ## Create index vector to categories index2cat = __grp2idx__ (catnames); else ## Check valueset for missing or empty elements if (any (strcmp (classv, {'cellstr', 'string'}))) valueset = cellstr (valueset); catnames = valueset; elseif (isnumeric (valueset)) catnames = arrayfun (@num2str, valueset, "UniformOutput", false); elseif (strcmp (classv, 'logical')) if (all (valueset)) catnames = {'true'}; elseif (! any (valueset)) catnames = {'false'}; else catnames = {'false'; 'true'}; endif else # datetime, duration catnames = dispstrings (valueset); endif endif else ## Resolve valueset and catnames from input array if (strcmp (classx, 'numeric')) valueset = unique (x); valueset = valueset(! isnan (valueset)); catnames = arrayfun (@num2str, valueset, "UniformOutput", false); elseif (strcmp (classx, 'logical')) valueset = unique (x); if (all (valueset)) catnames = {'true'}; elseif (! any (valueset)) catnames = {'false'}; else catnames = {'false'; 'true'}; endif elseif (strcmp (classx, 'cellstr')) valueset = unique (x); # does not remove empty valueset = valueset(! cellfun (fmt, valueset)); catnames = valueset; elseif (strcmp (classx, 'string')) valueset = unique (x); # removes missing, but not empty valueset = valueset(! cellfun (fmt, cellstr (valueset))); catnames = cellstr (valueset); elseif (strcmp (classx, 'datetime')) valueset = unique (x); # does not remove NaT valueset = valueset(! isnat (valueset)); catnames = dispstrings (valueset); elseif (strcmp (classx, 'duration')) valueset = unique (x); # does not remove NaN valueset = valueset(! ismissing (valueset)); catnames = dispstrings (valueset); endif endif ## Associate input array with valueset; core 'ismember' returns 0x0 ## for any empty cell array of character vectors, so keep the shape [tf, loc] = ismember (x, valueset); tf = reshape (tf, size (x)); loc = reshape (loc, size (x)); maxc = intmax ('uint16'); maxl = max (loc(:)) + sum (isnanvset); if (maxl > maxc) error (strcat ("categorical: too many categories; categorical", ... " supports up to %d categories; this input has", ... " %d."), maxc, maxl); endif ## If missing value in valueset is associated with a category name ## remove missing values in X from 'tf' and assign the index of the ## valueset that corresponds to the missing value to 'loc' if (any (isnanvset)) tf = tf | isnanx; loc(find (isnanx)) = find (isnanvset); endif ## Reassociate to user defined categories (only when regrouping required) if (! isempty (index2cat)) if (numel (index2cat) != unique (index2cat) || any (isnanvset)) valueset = 1:prod (size (valueset)); fcn = @ (x) index2cat(find (valueset == x)); loc(tf) = arrayfun (fcn, loc(tf)); endif endif ## Add constructor properties this.code = uint16 (loc); this.isMissing = ! tf; if (isempty (catnames)) this.cats = {}; else this.cats = unique (catnames(:), 'stable'); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{cstr} =} dispstrings (@var{C}) ## ## Get display formatted strings for each element of a categorical array. ## ## @code{@var{cstr} = dispstrings (@var{C})} returns a cellstr array of ## character vectors, @var{cstr}, which has the same size as the input ## categorical @var{C}. ## ## @end deftypefn function cstr = dispstrings (this) cstr = cell (size (this)); ix = this.code(! this.isMissing); cstr(! this.isMissing) = this.cats(ix); cstr(this.isMissing) = {''}; endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{cstr} =} cellstr (@var{C}) ## ## Convert categorical array to a cell array of character vectors. ## ## @code{@var{cstr} = cellstr (@var{C})} returns a cellstr array of ## character vectors, @var{cstr}, which has the same size as the input ## categorical @var{C}. ## ## @end deftypefn function cstr = cellstr (this) cstr = dispstrings (this); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{CM} =} char (@var{C}) ## ## Convert categorical array to a 2-D character matrix. ## ## @code{@var{CM} = char (@var{C})} returns a character matrix @var{CM}, ## which contains @code{numel (@var{C})} rows and each row contains the ## category name for the corresponding element of @code{@var{C}(:)}. ## ## @end deftypefn function CM = char (this) cstr = dispstrings (this); CM = char (cstr(:)); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{out} =} double (@var{C}) ## ## Convert categorical array to a double array. ## ## @code{@var{out} = double (@var{C})} returns a double array indexing the ## categories in @var{C}. Categorical elements of undefined category are ## returned as @code{NaN}. ## ## @end deftypefn function out = double (this) out = double (this.code); out(out == 0) = NaN; endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{out} =} single (@var{C}) ## ## Convert categorical array to a single array. ## ## @code{@var{out} = single (@var{C})} returns a single array indexing the ## categories in @var{C}. Categorical elements of undefined category are ## returned as @code{NaN}. ## ## @end deftypefn function out = single (this) out = single (this.code); out(out == 0) = NaN; endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{out} =} int64 (@var{C}) ## ## Convert categorical array to a int64 array. ## ## @code{@var{out} = int64 (@var{C})} returns a @qcode{int64} array ## indexing the categories in @var{C}. Categorical elements of undefined ## category are returned as @qcode{0}. ## ## @end deftypefn function out = int64 (this) out = int64 (this.code); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{out} =} int32 (@var{C}) ## ## Convert categorical array to a int32 array. ## ## @code{@var{out} = int32 (@var{C})} returns a @qcode{int32} array ## indexing the categories in @var{C}. Categorical elements of undefined ## category are returned as @qcode{0}. ## ## @end deftypefn function out = int32 (this) out = int32 (this.code); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{out} =} int16 (@var{C}) ## ## Convert categorical array to a int16 array. ## ## @code{@var{out} = int16 (@var{C})} returns a @qcode{int16} array ## indexing the categories in @var{C}. Categorical elements of undefined ## category are returned as @qcode{0}. Note that the returned category ## indices saturate to @qcode{intmax ('int16')}, which is 32767. ## ## @end deftypefn function out = int16 (this) out = int16 (this.code); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{out} =} int8 (@var{C}) ## ## Convert categorical array to a int8 array. ## ## @code{@var{out} = int8 (@var{C})} returns a @qcode{int8} array ## indexing the categories in @var{C}. Categorical elements of undefined ## category are returned as @qcode{0}. Note that the returned category ## indices saturate to @qcode{intmax ('int8')}, which is 127. ## ## @end deftypefn function out = int8 (this) out = int8 (this.code); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{out} =} uint64 (@var{C}) ## ## Convert categorical array to a uint64 array. ## ## @code{@var{out} = uint64 (@var{C})} returns a @qcode{uint64} array ## indexing the categories in @var{C}. Categorical elements of undefined ## category are returned as @qcode{0}. ## ## @end deftypefn function out = uint64 (this) out = uint64 (this.code); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{out} =} uint32 (@var{C}) ## ## Convert categorical array to a uint32 array. ## ## @code{@var{out} = uint32 (@var{C})} returns a @qcode{uint32} array ## indexing the categories in @var{C}. Categorical elements of undefined ## category are returned as @qcode{0}. ## ## @end deftypefn function out = uint32 (this) out = uint32 (this.code); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{out} =} uint16 (@var{C}) ## ## Convert categorical array to a uint16 array. ## ## @code{@var{out} = uint16 (@var{C})} returns a @qcode{uint16} array ## indexing the categories in @var{C}. Categorical elements of undefined ## category are returned as @qcode{0}. ## ## @end deftypefn function out = uint16 (this) out = uint16 (this.code); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{out} =} uint8 (@var{C}) ## ## Convert categorical array to a int8 array. ## ## @code{@var{out} = uint8 (@var{C})} returns a @qcode{uint8} array ## indexing the categories in @var{C}. Categorical elements of undefined ## category are returned as @qcode{0}. Note that the returned category ## indices saturate to @qcode{intmax ('uint8')}, which is 255. ## ## @end deftypefn function out = uint8 (this) out = uint8 (this.code); endfunction endmethods ################################################################################ ## ** Summary Information ** ## ################################################################################ ## Available Methods ## ## ## ## 'summary' 'categories' 'countcats' 'length' ## ## 'size' 'ndims' 'numel' 'keyHash' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {categorical} {} summary (@var{C}) ## @deftypefnx {categorical} {} summary (@var{C}, @var{dim}) ## @deftypefnx {categorical} {} summary (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {categorical} {@var{s} =} summary (@dots{}) ## ## Display summary of categorical array. ## ## @code{summary (@var{C})} displays the number of elements in the ## categorical array @var{C} that are equal to each category of @var{C}. ## Any undefined elements in @var{C} are summed together and displayed as ## @qcode{}. ## ## @end deftypefn function varargout = summary (this, varargin) ## Parse optional Name-Value paired arguments optName = {'Statistics'}; dfValue = {'default'}; [stats, args] = parsePairedArguments (optName, dfValue, varargin(:)); ## Force stats to cellstr stats = cellstr (stats); ## Handle default statistcs didx = strcmpi (stats, 'default'); if (this.isOrdinal) if (any (didx)) default = {'counts', 'nummissing', 'min', 'median', 'max'}; stats(didx) = []; ridx = cellfun (@(x) any (strcmpi (x, default)), stats); stats(ridx) = []; stats = [default, stats]; endif else if (any (didx)) default = {'counts', 'nummissing'}; stats(didx) = []; ridx = cellfun (@(x) any (strcmpi (x, default)), stats); stats(ridx) = []; stats = [default, stats]; endif endif ## Get operating dimension sz = size (this); if (isempty (args)) (dim = find (sz > 1, 1)) || (dim = 1); else if (numel (args) > 1) error ("categorical.summary: unrecognized input argument."); endif dim = args{1}; if (! (isnumeric (dim) && isscalar (dim) && fix (dim) == dim && ... dim > 0)) error ("categorical.summary: DIM must be a positive integer."); endif endif ## Store input size and category names in output struct s.Size = sz; s.Type = 'categorical'; s.Name = inputname (1); s.Categories = this.cats; ## Store 'counts' first (if requested) cidx = strcmpi (stats, 'counts'); NAMES = {}; if (any (cidx)) NAMES = [NAMES; this.cats]; s.Counts = countcats (this, dim); stats(cidx) = []; nidx = strcmpi (stats, 'nummissing'); if (any (nidx)) s.NumMissing = sum (this.isMissing, dim); stats(nidx) = []; NAMES = [NAMES; '']; endif endif ## Store remaining statistics according to their order in 'stats' STATS = {}; while (! isempty (stats)) switch (lower (stats{1})) case 'nummissing' s.NumMissing = sum (this.isMissing, dim); STATS = [STATS; 'NumMissing']; case 'min' if (this.isOrdinal) s.Min = min (this, [], dim); STATS = [STATS; 'Min']; endif case 'median' if (this.isOrdinal) s.Median = median (this, dim); STATS = [STATS; 'Median']; endif case 'max' if (this.isOrdinal) s.Max = max (this, [], dim); STATS = [STATS; 'Max']; endif case 'mode' s.Mode = mode (this, dim); STATS = [STATS; 'Mode']; endswitch stats(1) = []; endwhile ## Return structure or display summary if (nargout > 0) s = rmfield (s, 'Name'); varargout{1} = s; else __summary__ (s, dim, NAMES, STATS); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{cstr} =} categories (@var{C}) ## ## List of categories in categorical array. ## ## @code{@var{cstr} = categories (@var{C})} returns a cell array of ## character vectors with the names of the categories in @var{C}. ## ## @end deftypefn function cstr = categories (this) cstr = this.cats; endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{N} =} countcats (@var{C}) ## @deftypefnx {categorical} {@var{N} =} countcats (@var{C}, @var{dim}) ## ## Count occurrences of categories in a categorical array. ## ## @code{@var{N} = countcats (@var{C})} returns the number of elements for ## each category in @var{C}. If @var{C} is a vector, @var{N} is also a ## vector with one element for each category in @var{C}. If @var{C} is a ## matrix, @var{N} is a matrix with each column containing the category ## counts from each column of @var{C}. For multidimensional arrays, ## @code{countcats} operates along the first non-singleton dimension. ## ## @code{@var{N} = countcats (@var{C}, @var{dim})} operates along the ## dimension @var{dim}. ## ## @end deftypefn function N = countcats (this, dim = []) codes = this.code; sz = size (codes); nc = numel (this.cats); if (isempty (dim)) (dim = find (sz > 1, 1)) || (dim = 1); elseif (! (isnumeric (dim) && isscalar (dim) && fix (dim) == dim && ... dim > 0)) error ("categorical.countcats: DIM must be a positive integer."); endif if (nc == 0) sz(dim) = 0; N = zeros (sz); return; endif if (dim <= ndims (this)) nsz = sz; nsz(dim) = nc; ## Look-up indices. grp = [1:nc]; idx = lookup (grp, codes); ## Zero invalid ones (including NaNs). x < edges(1) are already zero. idx(! (codes <= grp(end))) = 0; iidx = idx; ## In case of matrix input, we adjust the indices. no_vector = (isrow (codes) && dim == 1) || ... (iscolumn (codes) && dim == 2); if (! isvector (codes) || no_vector) nl = prod (sz(1:dim-1)); nn = sz(dim); nu = prod (sz(dim+1:end)); if (nl != 1) iidx = (iidx-1) * nl; iidx += reshape (kron (ones (1, nn*nu), 1:nl), sz); endif if (nu != 1) ne = length (grp); iidx += reshape (kron (nl*ne*(0:nu-1), ones (1, nl*nn)), sz); endif endif ## Select valid elements. iidx = iidx(idx != 0); ## Call accumarray to sum the indexed elements. N = accumarray (iidx(:), 1, nsz); else N = repmat (zeros (size (this)), [ones(1, dim - 1), nc]); totnum = prod (size (this)); linvec = 1:totnum; for i = 1:nc offset = (i - 1) * totnum; N(linvec + offset) = codes == i; endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{N} =} length (@var{C}) ## ## Length of a categorical vector. ## ## @code{@var{N} = length (@var{C})} returns the size of the longest ## dimension of the categorical array @var{C}, unless any of its dimensions ## has zero length, in which case @code{length (@var{C})} returns 0. ## ## @end deftypefn function N = length (this) if (isempty (this.code)) N = 0; else N = max (size (this.code)); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{sz} =} size (@var{C}) ## @deftypefnx {categorical} {@var{dim_sz} =} size (@var{C}, @var{dim}) ## @deftypefnx {categorical} {@var{dim_sz} =} size (@var{C}, @var{d1}, @var{d2}, @dots{}) ## @deftypefnx {categorical} {[@var{rows}, @var{columns}, @dots{}, @var{dim_n_sz}] =} size (@dots{}) ## ## Size of a categorical array. ## ## @code{@var{sz} = size (@var{C})} returns a row vector with the size ## (number of elements) of each dimension for the categorical array @var{C}. ## ## @code{@var{dim_sz} = size (@var{C}, @var{dim})} returns the size of ## the corresponding dimension specified in @var{dim}. If @var{dim} is a ## vector, then @var{dim_sz} is a vector of the same length and with each ## element corresponding to a specified dimension. Multiple dimensions may ## also be specified as separate arguments. ## ## With a single output argument, @code{size} returns a row vector. With ## several, the size of dimension N is returned in the Nth argument. ## Asking for fewer output arguments than the array has dimensions folds ## the trailing dimensions into the last one, so @code{[r, c] = size ## (@var{C})} on a 2-by-2-by-2 array returns 2 and 4; asking for more pads ## with ones. When a dimension is named, however, the number of output ## arguments must equal the number of dimensions requested. ## ## @end deftypefn function varargout = size (this, varargin) if (! isempty (varargin)) ## A named dimension fixes how many outputs there may be. sz = size (this.code, varargin{:}); if (nargout > 1 && numel (sz) != nargout) error (strcat ("categorical.size: nargout does not match the", ... " number of requested dimensions.")); endif else sz = size (this.code); ## With no dimension named, asking for more outputs than the array has ## dimensions pads with ones, and asking for fewer folds the trailing ## dimensions into the last output, as core Octave and MATLAB both do. if (nargout > numel (sz)) sz = [sz, ones(1, nargout - numel (sz))]; elseif (nargout > 1 && nargout < numel (sz)) sz = [sz(1:nargout-1), prod(sz(nargout:end))]; endif endif if (nargout == 0 || nargout == 1) varargout{1} = sz; else for i = 1:nargout varargout{i} = sz(i); endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{out} =} ndims (@var{C}) ## ## Number of dimensions in a categorical array. ## ## @code{@var{out} = ndims (@var{C})} returns the number of dimensions of ## the categorical array @var{C}. ## ## @end deftypefn function out = ndims (this) out = ndims (this.code); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{out} =} numel (@var{C}) ## ## Total number of elements in a categorical array. ## ## @code{@var{out} = numel (@var{C})} returns the number of elements in the ## categorical array @var{C}. ## ## @end deftypefn function out = numel (this, varargin) out = numel (this.code); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{key} =} keyHash (@var{C}) ## @deftypefnx {categorical} {@var{key} =} keyHash (@var{C}, @var{base}) ## ## Generate a hash code for a categorical array. ## ## @code{@var{key} = keyHash (@var{C})} generates a @qcode{uint64} scalar ## that represents the input array @var{C}. @code{keyHash} utilizes the ## 64-bit FNV-1a variant of the Fowler-Noll-Vo non-cryptographic hash ## function. ## ## @code{@var{key} = keyHash (@var{C}, @var{base})} also generates a 64-bit ## hash code using @var{base} as the offset basis for the FNV-1a hash ## algorithm. @var{base} must be a @qcode{uint64} integer type scalar. Use ## this syntax to cascade @code{keyHash} on multiple objects for which a ## single hash code is required. ## ## A categorical array is keyed on its element labels, on its ordinality, ## and -- for an ordinal array alone -- on the order of its categories, ## which is what its comparisons mean. Neither the @qcode{Protected} ## property nor a non-ordinal array's category list is part of the key, so ## two non-ordinal arrays carrying the same labels hash alike however ## their categories differ or are ordered. Undefined elements are equal ## to one another and distinct from every category. Two arrays that ## @code{keyMatch} reports as the same key always hash alike. ## ## Note that unlike MATLAB, this implementation does not use any random ## seed. As a result, @code{keyHash} will always generate the exact same ## hash key for any particular input across different workers and Octave ## sessions. ## ## @end deftypefn function key = keyHash (this, base = []) ## Initialize string with size, class name, and ordinality. What is ## deliberately absent is as important as what is here. The Protected ## flag never affects equality, so it is not part of the key. Neither ## is the category list of a NON-ordinal array: two non-ordinal arrays ## carrying the same labels are the same key however their categories ## differ, or are ordered. For an ORDINAL array the ordering is what ## the comparisons mean and is therefore part of identity, so the ## category list is hashed, in order, for those alone. size_str = sprintf ('%dx', size (this.code))(1:end-1); flag_str = sprintf ('-o%d:', this.isOrdinal); init_str = [size_str class(this) flag_str]; if (! isempty (base)) if (! (isscalar (base) && isa (base, 'uint64'))) error ("categorical.keyHash: BASE must be a UINT64 scalar."); endif key = __ckeyHash__ (init_str, base); else key = __ckeyHash__ (init_str); endif if (this.isOrdinal) key = __ckeyHash__ (this.cats, key); endif if (! isempty (this.code)) ## The element LABELS, framed, and not the code. The code is only a ## label's position in the category list, so two equal non-ordinal ## arrays whose categories were declared in a different order carry ## different codes -- hashing it would break the contract for exactly ## the case being fixed. Undefined elements are carried by the ## isMissing mask instead of by a substituted label, so no real ## category can be confused with one. labels = cell (size (this.code)); labels(:) = {''}; defined = this.code != 0; labels(defined) = this.cats(this.code(defined)); key = __ckeyHash__ (labels, key); key = __nkeyHash__ (this.isMissing(:), key); endif endfunction endmethods ################################################################################ ## ** Query Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'iscategory' 'iscolumn' 'isempty' 'isequal' ## ## 'isequaln' 'ismatrix' 'ismember' 'ismissing' ## ## 'isordinal' 'isprotected' 'isrow' 'isscalar' ## ## 'issorted' 'issortedrows' 'isundefined' 'isvector' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} iscategory (@var{C}, @var{catnames}) ## ## Test for categories in a categorical array. ## ## @code{@var{TF} = iscategory (@var{C}, @var{catnames})} returns a logical ## array @var{TF} of the same size as @var{catnames} containing @qcode{true} ## for each corresponding element of @var{catnames} that is a category in ## categorical array @var{C} and @qcode{false} otherwise. ## ## @end deftypefn function TF = iscategory (this, catnames) if (nargin < 2) error ("categorical.iscategory: CATNAMES input argument is required."); endif catnames = cellstr (catnames); ## Core 'ismember' returns 0x0 for any empty cell array of character ## vectors, so keep the shape TF = reshape (ismember (catnames, this.cats), size (catnames)); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} iscolumn (@var{C}) ## ## Return true if categorical array is a column vector. ## ## @code{@var{TF} = iscolumn (@var{C})} returns a logical scalar @var{TF}, ## which is @qcode{true}, if the categorical array @var{C} is a column ## vector, and @qcode{false} otherwise. A column vector is a 2-D array for ## which @code{size (@var{X})} returns @code{[@var{N}, 1]} with non-negative ## @var{N}. ## ## @end deftypefn function TF = iscolumn (this) TF = iscolumn (this.code); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} isempty (@var{C}) ## ## Return true if categorical array is empty. ## ## @code{@var{TF} = isempty (@var{C})} returns a logical scalar @var{TF}, ## which is @qcode{true}, if the categorical array @var{C} is empty, and ## @qcode{false} otherwise. ## ## @end deftypefn function TF = isempty (this) TF = isempty (this.code); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} isequal (@var{C1}, @var{C2}) ## @deftypefnx {categorical} {@var{TF} =} isequal (@var{C1}, @var{C2}, @dots{}) ## ## Return true if categorical arrays are equal. ## ## @code{@var{TF} = isequal (@var{C1}, @var{C2})} returns a logical scalar ## @var{TF}, which is @qcode{true}, if the categorical arrays @var{C1} and ## @var{C2} contain the same values, and @qcode{false} otherwise. Either ## @var{C1} or @var{C2} may also be a string array, a missing object array, ## a character vector, or a cell array of character vectors, which will be ## promoted to a categorical array prior to comparison. ## ## If categorical arrays @var{C1} and @var{C2} are ordinal, they must have ## the same set and ordering of categories. If neither are ordinal, the ## category names of each pair of elements are compared. Hence, they do not ## need to have the same set of categories. ## ## @code{@var{TF} = isequal (@var{C1}, @var{C2}, @dots{})} returns a logical ## scalar @var{TF}, which is @qcode{true}, if all input arguments are equal, ## and @qcode{false} otherwise. ## ## @end deftypefn function TF = isequal (varargin) args = varargin; [args{:}] = promote (varargin{:}); ## Check for undefined elements (result to false) if (any (cellfun (@(x) any (x.code == 0, 'all'), args))) TF = false; return; endif ## If any categorical value is ordinal, all must be is_ordinal = cellfun (@isordinal, args); if (all (is_ordinal)) ## Check that all categorical arrays have the same categories ## and they are in the same order cats = cellfun (@(x) categories (x), args, 'UniformOutput', false); if (! isequal (cats{:})) TF = false; return; endif fieldArgs = cellfun (@(x) x.code, args, 'UniformOutput', false); TF = isequal (fieldArgs{:}); elseif (any (is_ordinal)) TF = false; else ## Compare the category names of each element fcn = @(x) reshape (x.cats(x.code), size (x.code)); fieldArgs = cellfun (fcn, args, 'UniformOutput', false); TF = isequal (fieldArgs{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} isequaln (@var{C1}, @var{C2}) ## @deftypefnx {categorical} {@var{TF} =} isequaln (@var{C1}, @var{C2}, @dots{}) ## ## Return true if categorical arrays are equal under the assumption that ## undefined elements are equal. ## ## @code{@var{TF} = isequaln (@var{C1}, @var{C2})} returns a logical scalar ## @var{TF}, which is @qcode{true}, if the categorical arrays @var{C1} and ## @var{C2} contain the same values or corresponding undefined elements, and ## @qcode{false} otherwise. Either @var{C1} or @var{C2} may also be a ## string array, a missing object array, a character vector, or a cell array ## of character vectors, which will be promoted to a categorical array prior ## to comparison. ## ## If categorical arrays @var{C1} and @var{C2} are ordinal, they must have ## the same set and ordering of categories. If neither are ordinal, the ## category names of each pair of elements are compared. Hence, they do not ## need to have the same set of categories. ## ## @code{@var{TF} = isequaln (@var{C1}, @var{C2}, @dots{})} returns a ## logical scalar @var{TF}, which is @qcode{true}, if all input arguments ## are equal, and @qcode{false} otherwise. ## ## @end deftypefn function TF = isequaln (varargin) args = varargin; [args{:}] = promote (varargin{:}); ## If any categorical value is ordinal, all must be is_ordinal = cellfun (@isordinal, args); if (all (is_ordinal)) ## Check that all categorical arrays have the same categories ## and they are in the same order cats = cellfun (@(x) categories (x), args, 'UniformOutput', false); if (! isequal (cats{:})) TF = false; return; endif fieldArgs = cellfun (@(x) x.code, args, 'UniformOutput', false); TF = isequal (fieldArgs{:}); elseif (any (is_ordinal)) TF = false; else ## Compare the category names of each element fcn = @(x) reshape (([''; x.cats])(x.code + 1), ... size (x.code)); fieldArgs = cellfun (fcn, args, 'UniformOutput', false); TF = isequal (fieldArgs{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} ismatrix (@var{C}) ## ## Return true if categorical array is a 2-D array. ## ## @code{@var{TF} = ismatrix (@var{C})} returns a logical scalar @var{TF}, ## which is @qcode{true}, if the categorical array @var{C} is a matrix, and ## @qcode{false} otherwise. A matrix is an array of any type where ## @code{ndims (@var{X}) == 2} and for which @code{size (@var{X})} returns ## @code{[@var{H}, @var{W}]} with non-negative @var{H} and @var{W}. ## ## @end deftypefn function TF = ismatrix (this) TF = ismatrix (this.code); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} ismember (@var{A}, @var{B}) ## @deftypefnx {categorical} {@var{TF} =} ismember (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {categorical} {[@var{TF}, @var{index}] =} ismember (@dots{}) ## @deftypefnx {categorical} {[@var{TF}, @var{index}] =} ismember (@dots{}, @qcode{'legacy'}) ## ## Find categorical elements in a set. ## ## @code{@var{TF} = ismember (@var{A}, @var{B})} returns a logical array ## @var{TF} of the same size as @var{A} containing @qcode{true} for each ## corresponding element of @var{A} that is in @var{B} and @qcode{false} ## otherwise. @qcode{} elements are not equal with each other ## and always return @qcode{false}. If @var{A} and @var{B} are ordinal, ## they must have the same ordered set of categories. If neither @var{A} ## nor @var{B} are ordinal, then this restriction is relaxed and comparison ## is performed using the category names. Comparison between an ordinal and ## an unordered categorical array is not allowed. Either @var{A} or @var{B} ## may also be a string array, a character vector, or a cell array of ## character vectors containing one or multiple category names to compare ## against an unordered categorical array. ## ## @code{@var{TF} = ismember (@var{A}, @var{B}, @qcode{'rows'})} only ## applies to categorical matrices with the same number of columns, in which ## case the logical vector @var{TF} contains @qcode{true} for each row of ## @var{A} that is also a row in @var{B}. @var{TF} has the same number of ## rows as @var{A}. ## ## @code{[@var{TF}, @var{index}] = ismember (@var{A}, @var{B})} also returns ## an index array of the same size as @var{A} containing the lowest index in ## @var{B} for each element of @var{A} that is a member of @var{B} and 0 ## otherwise. If the @qcode{'rows'} optional argument is used, then the ## returning index is a column vector with the same rows as @var{A} and it ## contains the lowest index in @var{B} for each row of @var{A} that is a ## member of @var{B} and 0 otherwise. If the @qcode{'legacy'} optional ## argument is specified, then the highest index of matched elements is ## returned. Unless multiple matches exist, the @qcode{'legacy'} option has ## no effect on the returned @var{index}. ## ## @end deftypefn function varargout = ismember (A, B, varargin) ## Either A or B contain categorical text representation if ((ischar (A) && isrow (A)) || isstring (A) || iscellstr (A)) if (ischar (A)) A = cellstr (A); endif A = categorical (A); elseif ((ischar (B) && isrow (B)) || isstring (B) || iscellstr (B)) if (ischar (B)) B = cellstr (B); endif B = categorical (B); elseif (! (iscategorical (A) && iscategorical (B))) error (strcat ("categorical.ismember: A and B must be categorical", ... " arrays, or one of them (A or B) can be a string", ... " array, a character vector, or a cell array of", ... " character vectors specifying category names.")); endif ## Check for 'rows' and 'legacy' optional arguments if (! isempty (varargin)) if (! cellfun ('ischar', varargin)); error (strcat ("categorical.ismember: all options must be", ... " character vectors.")); elseif (! all (strcmpi (varargin, 'rows') | ... strcmpi (varargin, 'legacy'))) error (strcat ("categorical.ismember: only 'rows' and", ... " 'legacy' are valid options.")); endif if (any (strcmpi ('rows', varargin))) if (ndims (A) != 2 || ndims (A) != ndims (B)) error (strcat ("categorical.ismember: 'rows' applies only", ... " to 2-D matrices.")); endif if (size (A, 2) != size (B, 2)) error (strcat ("categorical.ismember: 'rows' requires same", ... " number of columns.")); endif endif endif ## Handle empty input array if (isempty (A) || isempty (B)) sz = size (A); varargout{1} = false (sz); if (nargout > 1) varargout{2} = zeros (sz); endif return; endif ## Both ordinal if (isordinal (A) && isordinal (B)) if (! isequal (categories (A), categories (B))) error (strcat ("categorical.ismember: ordinal categorical arrays", ... " must have the same ordered set of categories.")); endif if (nargout > 1) [varargout{1}, varargout{2}] = __ismember__ (double (A), ... double (B), varargin{:}); else varargout{1} = __ismember__ (double (A), double (B), varargin{:}); endif elseif (isordinal (A) || isordinal (B)) error (strcat ("categorical.ismember: both categorical arrays", ... " must be ordinal.")); else ## Compare the category names of each element (except undefined) A_idx = A.code != 0; B_idx = B.code != 0; cats_A = string (cell (size (A.code))); cats_B = string (cell (size (B.code))); cats_A(A_idx) = A.cats(A.code(A_idx)); cats_B(B_idx) = B.cats(B.code(B_idx)); if (nargout > 1) [varargout{1}, varargout{2}] = ismember (cats_A, cats_B, varargin{:}); else varargout{1} = ismember (cats_A, cats_B, varargin{:}); endif endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{out} =} ismissing (@var{C}) ## @deftypefnx {categorical} {@var{out} =} ismissing (@var{C}, @var{indicator}) ## ## Find missing elements in categorical array. ## ## @code{@var{TF} = ismissing (@var{C})} returns a logical array @var{TF} ## of the same size as @var{C} containing @qcode{true} for each ## corresponding element of @var{C} that does not have a value from one of ## the categories in @var{C} and @qcode{false} otherwise. ## ## @code{@var{TF} = ismissing (@var{C}, @var{indicator})} returns a logical ## array @var{TF} containing @qcode{true} for each corresponding element of ## @var{C} that matches a value in @var{indicator} and @qcode{false} ## otherwise. The undefined elements of @var{C} are not marked, unless ## @var{indicator} names them. ## ## @var{indicator} must be a character vector, a cell vector of character ## vectors, a string vector, @code{missing}, or a categorical vector. ## @itemize ## @item Character vectors are read as when constructing a categorical ## array: an empty or blank character vector matches the undefined ## elements of @var{C}, and any other matches the elements of @var{C} in the ## category of that name. ## @item Strings match only the elements of @var{C} in the category of that ## name, so neither @qcode{""} nor @code{} matches the undefined ## elements. ## @item @code{missing} matches the undefined elements of @var{C}. ## @item A categorical @var{indicator} matches the elements of @var{C} in ## each of its categories, and its undefined elements match the undefined ## elements of @var{C}. MATLAB accepts no categorical @var{indicator}, so ## this is an Octave extension. ## @end itemize ## ## @end deftypefn function TF = ismissing (this, varargin) if (nargin > 2) error ("categorical.ismissing: too many input arguments."); endif if (! isempty (varargin)) indicator = varargin{1}; TF = false (size (this)); if (isa (indicator, 'missing')) TF = this.isMissing; elseif (ischar (indicator) && isempty (indicator)) ## '' names , as it does when constructing categorical TF = this.isMissing; elseif (isvector (indicator)) if (ischar (indicator)) if (! isrow (indicator)) error (strcat ("categorical.ismissing: INDICATOR specified", ... " as a character vector must be a row vector.")); endif if (all (isspace (indicator))) TF = this.isMissing; else TF(this == indicator) = true; endif elseif (iscellstr (indicator)) ## Empty and blank text names for i = 1:numel (indicator) if (all (isspace (indicator{i}))) TF(this.isMissing) = true; else TF(this == indicator{i}) = true; endif endfor elseif (isstring (indicator)) ## A string matches category names only for i = 1:numel (indicator) TF(this == indicator(i)) = true; endfor elseif (isa (indicator, 'categorical')) for i = 1:length (indicator) idx = indicator.code(i); if (idx) cat = indicator.cats{idx}; TF(this == cat) = true; else # code = 0 corresponds to undefined categories TF(this.isMissing) = true; endif endfor else error (strcat ("categorical.ismissing: INDICATOR must be", ... " a text representation of categories or", ... " categorical.")); endif else error ("categorical.ismissing: INDICATOR must be a vector."); endif else TF = this.isMissing; endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} isordinal (@var{C}) ## ## Test if categorical array is ordinal. ## ## @code{@var{TF} = isordinal (@var{C})} returns a logical scalar @var{TF}, ## which is @qcode{true}, if the categorical array @var{C} is ordinal, and ## @qcode{false} otherwise. ## ## @end deftypefn function TF = isordinal (this) TF = this.isOrdinal; endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} isprotected (@var{C}) ## ## Test if categorical array is protected. ## ## @code{@var{TF} = isprotected (@var{C})} returns a logical scalar ## @var{TF}, which is @qcode{true}, if the categorical array @var{C} is ## protected, and @qcode{false} otherwise. ## ## @end deftypefn function TF = isprotected (this) TF = this.isProtected; endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} isrow (@var{C}) ## ## Return true if categorical array is a row vector. ## ## @code{@var{TF} = isrow (@var{C})} returns a logical scalar @var{TF}, ## which is @qcode{true}, if the categorical array @var{C} is a row vector, ## and @qcode{false} otherwise. A row vector is a 2-D array for which ## @code{size (@var{X})} returns @code{[1, @var{N}]} with non-negative ## @var{N}. ## ## @end deftypefn function TF = isrow (this) TF = isrow (this.code); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} isscalar (@var{C}) ## ## Return true if categorical array is a scalar. ## ## @code{@var{TF} = isscalar (@var{C})} returns a logical scalar @var{TF}, ## which is @qcode{true}, if the categorical array @var{C} is also a scalar, ## and @qcode{false} otherwise. A scalar is a single element object for ## which @code{size (@var{X})} returns @code{[1, 1]}. ## ## @end deftypefn function TF = isscalar (this) TF = isscalar (this.code); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} issorted (@var{C}) ## @deftypefnx {categorical} {@var{TF} =} issorted (@var{C}, @var{dim}) ## @deftypefnx {categorical} {@var{TF} =} issorted (@var{C}, @var{direction}) ## @deftypefnx {categorical} {@var{TF} =} issorted (@var{C}, @var{dim}, @var{direction}) ## @deftypefnx {categorical} {@var{TF} =} issorted (@dots{}, @qcode{'MissingPlacement'}, @var{MP}) ## ## Return true if categorical array is sorted. ## ## @code{@var{TF} = issorted (@var{C})} returns a logical scalar @var{TF}, ## which is @qcode{true}, if the categorical array @var{C} is sorted in ## ascending order, and @qcode{false} otherwise. ## ## @code{@var{TF} = issorted (@var{C}, @var{dim})} returns a logical scalar ## @var{TF}, which is @qcode{true}, if the categorical array @var{C} is ## sorted in ascending order along the dimension @var{dim}, and ## @qcode{false} otherwise. ## ## @code{@var{TF} = issorted (@var{A}, @var{direction})} returns a logical ## scalar @var{TF}, which is @qcode{true}, if the categorical array @var{C} ## is sorted in the direction specified by @var{direction}, and ## @qcode{false} otherwise. @var{direction} can be one of the following ## options: ## ## @itemize ## @item @qcode{'ascend'}, which is the default, checks is elements are in ## ascending order. ## @item @qcode{'descend'} checks if elements are in descending order. ## @item @qcode{'monotonic'} checks if elements are either in ascending or ## descending order. ## @item @qcode{'strictascend'} checks if elements are in ascending order ## and there are no duplicate or undefined elements. ## @item @qcode{'strictdescend'} checks if elements are in descending order ## and there are no duplicate or undefined elements. ## @item @qcode{'strictmonotonic'} checks if elements are either in ## ascending or descending order and there are no duplicate or undefined ## elements. ## @end itemize ## ## @code{@var{TF} = issorted (@dots{}, @qcode{'MissingPlacement'}, ## @var{MP})} specifies where missing elements (@qcode{}) are ## placed with ## one of the following options specified in @var{MP}: ## ## @itemize ## @item @qcode{'auto'}, which is the default, places missing elements last ## for ascending sort and first for descending sort. ## @item @qcode{'first'} places missing elements first. ## @item @qcode{'last'} places missing elements last. ## @end itemize ## ## @end deftypefn function TF = issorted (this, varargin) ## Single input argument if (nargin == 1) TF = isequaln (this, sort (this)); return; endif ## Get operating dimension cid = cellfun (@isnumeric, varargin); if (any (cid)) dim = varargin{cid}; else sz = size (this); dim = find (sz != 1, 1); if (isempty (dim)) # scalar dim = 1; endif endif ## Parse and validate optional 'MissingPlacement' paired argument optNames = {'MissingPlacement'}; dfValues = {'auto'}; [MP, args] = parsePairedArguments (optNames, dfValues, varargin(:)); ## The values are matched without regard to case, as MATLAB does. MP = lower (MP); if (! ismember (MP, {'auto', 'first', 'last'})) error ("categorical.issorted: invalid value for 'MissingPlacement'."); endif ## Force strings to character vectors [args{:}] = convertStringsToChars (args{:}); ## Get direction cid = cellfun (@(x) ischar (x), args); if (any (cid)) direction = lower (args{cid}); ## Check for type of direction valid_direction = {'ascend', 'descend', 'monotonic', 'strictascend', ... 'strictdescend', 'strictmonotonic'}; if (! ismember (direction, valid_direction)) error ("categorical.issorted: invalid DIRECTION value."); endif switch (direction) case {'ascend', 'descend'} TF = isequaln (this, sort (this, args{:}, 'MissingPlacement', MP)); case {'strictascend', 'strictdescend'} ## Check for missing values first (fast) if (any (this.isMissing, 'all')) TF = false; return; endif args{cid} = strrep (direction, 'strict', ''); if (! strcmp (MP, 'auto')) args = [args, 'MissingPlacement', MP]; endif sorted = sort (this, args{:}); if (any (diff (double (sorted), 1, dim) == 0, 'all')) TF = false; return; endif TF = isequal (this, sorted); case 'monotonic' ## Check for either ascending or descending args{cid} = 'ascend'; TF = isequaln (this, sort (this, args{:}, 'MissingPlacement', MP)); if (TF) return; endif args{cid} = 'descend'; TF = isequaln (this, sort (this, args{:}, 'MissingPlacement', MP)); case 'strictmonotonic' ## Check missing values first (fast) if (any (this.isMissing, 'all')) TF = false; return; endif ## Check for either ascending or descending args{cid} = 'ascend'; sorted = sort (this, args{:}, 'MissingPlacement', MP); if (any (diff (double (sorted), 1, dim) == 0, 'all')) TF = false; return; endif TF = isequal (this, sorted); if (TF) return; endif args{cid} = 'descend'; sorted = sort (this, args{:}, 'MissingPlacement', MP); if (any (diff (double (sorted), 1, dim) == 0, 'all')) TF = false; return; endif TF = isequal (this, sorted); endswitch else ## No DIRECTION input argument TF = isequaln (this, sort (this, args{:}, 'MissingPlacement', MP)); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} issortedrows (@var{C}) ## @deftypefnx {categorical} {@var{TF} =} issortedrows (@var{C}, @var{col}) ## @deftypefnx {categorical} {@var{TF} =} issortedrows (@var{C}, @var{direction}) ## @deftypefnx {categorical} {@var{TF} =} issortedrows (@var{C}, @var{col}, @var{direction}) ## @deftypefnx {categorical} {@var{TF} =} issortedrows (@dots{}, @qcode{'MissingPlacement'}, @var{MP}) ## ## Return true if categorical matrix rows are sorted. ## ## @code{@var{TF} = issortedrows (@var{C})} returns a logical scalar ## @var{TF}, which is @qcode{true}, if the rows in the 2-D categorical array ## @var{C} are sorted in ascending order, and @qcode{false} otherwise. ## ## @code{@var{TF} = issortedrows (@var{C}, @var{col})} returns a logical ## scalar @var{TF}, which is @qcode{true}, if the categorical array @var{C} ## is sorted according to the columns specified by the vector @var{col}, and ## @qcode{false} otherwise. @var{col} must explicitly contain non-zero ## integers whose absolute values index existing columns in @var{C}. ## Positive elements sort the corresponding columns in ascending order, ## while negative elements sort the corresponding columns in descending ## order. ## ## @code{@var{TF} = issortedrows (@var{C}, @var{direction})} checks if the ## rows in @var{C} are sorted according to the specified direction, which ## can be one of the following options: ## ## @itemize ## @item @qcode{'ascend'}, which is the default, checks is elements are in ## ascending order. ## @item @qcode{'descend'} checks if elements are in descending order. ## @item @qcode{'monotonic'} checks if elements are either in ascending or ## descending order. ## @item @qcode{'strictascend'} checks if elements are in ascending order ## and there are no duplicate or undefined elements. ## @item @qcode{'strictdescend'} checks if elements are in descending order ## and there are no duplicate or undefined elements. ## @item @qcode{'strictmonotonic'} checks if elements are either in ## ascending or descending order and there are no duplicate or undefined ## elements. ## @end itemize ## ## Alternatively, @var{direction} can be a cell array of character ## vectors specifying the sorting direction for each individual column of ## @var{C}, in which case the number of elements in @var{direction} must ## equal the number of columns in @var{C}. ## ## @code{@var{TF} = issortedrows (@var{C}, @var{col}, @var{direction})} ## checks if the rows in the categorical array @var{C} are sorted according ## to the columns specified in @var{col} using the corresponding sorting ## direction specified in @var{direction}. In this case, the sign of the ## values in @var{col} is ignored. @var{col} and @var{direction} must have ## the same length, but not necessarily the same number of elements as the ## columns in @var{C}. ## ## @code{@var{TF} = issortedrows (@dots{}, @qcode{'MissingPlacement'}, ## @var{MP})} specifies where missing elements (@qcode{}) are ## placed with one of the following options specified in @var{MP}: ## ## @itemize ## @item @qcode{'auto'}, which is the default, places missing elements last ## for ascending sort and first for descending sort. ## @item @qcode{'first'} places missing elements first. ## @item @qcode{'last'} places missing elements last. ## @end itemize ## ## @end deftypefn function TF = issortedrows (this, varargin) ## Single input argument if (nargin == 1) TF = isequaln (this, sortrows (this)); return; endif ## Force strings to character vectors or cell arrays of character vectors [varargin{:}] = convertStringsToChars (varargin{:}); ## Get valid direction(s) from input argument list valid = {'ascend', 'descend', 'monotonic', 'strictascend', ... 'strictdescend', 'strictmonotonic'}; fcn = @(x) (ischar (x) && ismember (x, valid)) || iscellstr (x); cid = cellfun (fcn, varargin); if (any (cid)) direction = lower (cellstr (varargin{cid})); ## Check for valid type of directions in cellstring if (! all (cellfun (@(x) ismember (x, valid), direction))) error ("categorical.issortedrows: invalid DIRECTION value."); endif ## Handle non-strict modes first if (all (cellfun (@(x) ismember (x, {'ascend', 'descend'}), direction))) TF = isequaln (this, sortrows (this, varargin{:})); return; endif simple_types = {'ascend', 'descend', 'monotonic'}; if (all (cellfun (@(x) ismember (x, simple_types), direction))) idx = strcmp (direction, 'monotonic'); direction{idx} = 'ascend'; varargin{cid} = direction; TF = isequaln (this, sortrows (this, varargin{:})); if (TF) return; endif direction{idx} = 'descend'; varargin{cid} = direction; TF = isequaln (this, sortrows (this, varargin{:})); return; endif ## Handle strict modes. Determine the sort-key column order and the ## sort position holding the first strict direction, then derive the ## prefix (the columns sorted up to and including that position, on ## which the duplicate test operates) and the binding strict column ## (the single column on which the missing test operates). A missing ## value in the binding column, or two consecutive sorted rows equal ## across the prefix columns, rules out a strict ordering; strict ## positions after the first are subsumed by it. strict_types = {'strictascend', 'strictdescend', 'strictmonotonic'}; sflag = cellfun (@(x) ismember (x, strict_types), direction); nc = size (this, 2); nummask = cellfun (@isnumeric, varargin); if (any (nummask)) col = varargin{nummask}; ocols = abs (col(:)'); if (isscalar (direction)) sflag = repmat (sflag, 1, numel (col)); elseif (numel (direction) != numel (col)) error ("categorical.issortedrows: COL and DIRECTION mismatch."); endif else ocols = 1:nc; if (isscalar (direction)) sflag = repmat (sflag, 1, nc); endif endif K = find (sflag, 1); prefix = ocols(1:K); bindcol = ocols(K); ## A missing value in the binding strict column rules out strictness. if (any (this.isMissing(:,bindcol), 'all')) TF = false; return; endif ## Replace strict modes with their plain counterparts for the sort. direction = strrep (direction, 'strict', ''); varargin{cid} = direction; if (all (cellfun (@(x) ismember (x, {'ascend', 'descend'}), direction))) sorted = sortrows (this, varargin{:}); ## No two consecutive rows may tie across the prefix columns. tmpcol = sorted.code(:, prefix); if (any (all (diff (tmpcol, 1, 1) == 0, 2))) TF = false; return; endif TF = isequaln (this, sorted); else # a 'monotonic' position also exists idx = strcmp (direction, 'monotonic'); direction(idx) = {'ascend'}; varargin{cid} = direction; sorted = sortrows (this, varargin{:}); tmpcol = sorted.code(:, prefix); if (any (all (diff (tmpcol, 1, 1) == 0, 2))) TF = false; return; endif TF = isequaln (this, sorted); if (TF) return; endif direction(idx) = {'descend'}; varargin{cid} = direction; sorted = sortrows (this, varargin{:}); tmpcol = sorted.code(:, prefix); if (any (all (diff (tmpcol, 1, 1) == 0, 2))) TF = false; return; endif TF = isequaln (this, sorted); endif else ## No DIRECTION input argument TF = isequaln (this, sortrows (this, varargin{:})); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{out} =} isundefined (@var{C}) ## ## Test for undefined elements in categorical array. ## ## @code{@var{TF} = isundefined (@var{C})} returns a logical array @var{TF} ## of the same size as @var{C} containing @qcode{true} for each ## corresponding element of @var{C} that does not have a value from one of ## the categories in @var{C} and @qcode{false} otherwise. ## @qcode{} is the equivalent of @qcode{NaN} in numeric arrays. ## ## @end deftypefn function TF = isundefined (this) TF = this.isMissing; endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} isvector (@var{C}) ## ## Return true if categorical array is a vector. ## ## @code{@var{TF} = isvector (@var{C})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the categorical array @var{C} is a vector and ## @qcode{false} otherwise. A vector is a 2-D array for which one of the ## dimensions is equal to 1 (either @math{1*N} or @math{N*1}). By ## definition, a scalar is also a vector. ## ## @end deftypefn function TF = isvector (this) TF = isvector (this.code); endfunction endmethods ################################################################################ ## ** Category Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'addcats' 'mergecats' 'removecats' 'renamecats' ## ## 'reordercats' 'setcats' 'times' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} addcats (@var{A}, @var{newcats}) ## @deftypefnx {categorical} {@var{B} =} addcats (@dots{}, @qcode{'After'}, @var{catname}) ## @deftypefnx {categorical} {@var{B} =} addcats (@dots{}, @qcode{'Before'}, @var{catname}) ## ## Add categories to categorical array. ## ## @code{@var{B} = addcats (@var{A}, @var{newcats})} appends new categories ## specified in @var{newcats} to the categorical array @var{A} at the end of ## any existing categories. The output categorical array @var{B} does not ## contain elements that belong to the newly added categories. ## ## @code{@var{B} = addcats (@dots{}, @qcode{'After'}, @var{catname})} adds ## the categories after the existing category specified by @var{catname}. ## ## @code{@var{B} = addcats (@dots{}, @qcode{'Before'}, @var{catname})} adds ## the categories before the existing category specified by @var{catname}. ## ## @var{catname} must be either a character vector, a cellstr scalar or a ## string scalar. @var{newcats} can be a string array, a cell array of ## character vectors, or any type of array that can be converted to a cell ## array of character vectors with the @code{cellstr} function, as long as ## it does not contain any duplicate names or references existing category ## in @var{A}. ## ## When adding a single category, @var{newcats} can also be specified as a ## character vector. ## ## @end deftypefn function B = addcats (A, newcats, varargin) ## Check input arguments if (nargin < 2) error ("categorical.addcats: too few input arguments."); elseif (isempty (newcats)) error ("categorical.addcats: NEWCATS cannot be empty."); elseif (isnumeric (newcats) || islogical (newcats)) error ("categorical.addcats: NEWCATS cannot be numeric or logical."); endif if (mod (numel (varargin), 2) != 0) error (strcat ("categorical.addcats: name-value", ... " arguments must be in pairs.")); endif ## Convert to cellstring if (! iscellstr (newcats)) try newcats = cellstr (newcats); catch error (strcat ("categorical.addcats: NEWCATS cannot be", ... " converted to cellstr.")); end_try_catch endif ## Force to column vector newcats = newcats(:); ## New catnames must be unique and non-existing if (numel (newcats) != numel (unique (newcats))) error ("categorical.addcats: duplicate category names in NEWCATS."); endif TF = ismember (newcats, A.cats); if (any (TF, 'all')) error ("categorical.addcats: new category names already present."); endif ## Parse optional paired arguments; an empty 'After' or 'Before' ## appends the new categories at the end optNames = {'After', 'Before'}; dfValues = {[], []}; [After, Before, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! isempty (After) && ! isempty (Before)) error (strcat ("categorical.addcats: cannot specify both", ... " 'After' and 'Before'.")); endif if (! isempty (args)) error ("categorical.addcats: invalid optional paired argument."); endif ## Add categories if (! isempty (After)) maxcode = numel (A.cats); idxcode = find (strcmp (After, A.cats)); if (isempty (idxcode)) error (strcat ("categorical.addcats: 'After' indexes a", ... " non-existing category.")); elseif (idxcode == maxcode) B = A; B.cats = [B.cats; newcats]; else B = A; B.cats = [B.cats(1:idxcode); newcats; B.cats(idxcode+1:end)]; idx_add = idxcode+1:maxcode; n_cats = numel (newcats); for i = numel (idx_add):-1:1 B.code(B.code == idx_add(i)) += n_cats; endfor endif elseif (! isempty (Before)) maxcode = numel (A.cats); idxcode = find (strcmp (Before, A.cats)); if (isempty (idxcode)) error (strcat ("categorical.addcats: 'Before' indexes a", ... " non-existing category.")); elseif (idxcode == 1) B = A; B.cats = [newcats; B.cats]; B.code(! B.isMissing) += numel (newcats); else B = A; B.cats = [B.cats(1:idxcode-1); newcats; B.cats(idxcode:end)]; idx_add = idxcode:maxcode; n_cats = numel (newcats); for i = numel (idx_add):-1:1 B.code(B.code == idx_add(i)) += n_cats; endfor endif else B = A; B.cats = [B.cats; newcats]; endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} mergecats (@var{A}, @var{oldcats}) ## @deftypefnx {categorical} {@var{B} =} mergecats (@var{A}, @var{oldcats}, @var{newcat}) ## ## Merge categories in categorical array. ## ## @code{@var{B} = mergecats (@var{A}, @var{oldcats})} merges two or more ## categories specified by @var{oldcats} into a single category with the ## same name as @qcode{@var{oldcats}(1)}. In case of ordinal categorical ## arrays, the categories listed in @var{oldcats} must be in consecutive ## order. All elements of @var{A} corresponding to the categories listed in ## @var{oldcats} are re-indexed to correspond to @qcode{@var{oldcats}(1)} in ## @var{B}. ## ## @code{@var{B} = mergecats (@var{A}, @var{oldcats}, @var{newcat})} merges ## the categories listed in @var{oldcats} into a single new category named ## as specified by @var{newcat}. ## ## @var{newcat} must be either a character vector, a cellstr scalar or a ## string scalar. @var{oldcats} can be string array, a cell array of ## character vectors, or any type of array that can be converted to a cell ## array of character vectors with the @code{cellstr} function. Any names ## in @var{oldcats} that do not reference an existing category are ignored. ## ## @end deftypefn function B = mergecats (A, oldcats, varargin) ## Check input arguments if (nargin < 2) error ("categorical.mergecats: too few input arguments."); elseif (isempty (oldcats)) error ("categorical.mergecats: OLDCATS cannot be empty."); elseif (isnumeric (oldcats) || islogical (oldcats)) error ("categorical.mergecats: OLDCATS cannot be numeric or logical."); elseif (ischar (oldcats)) error ("categorical.mergecats: OLDCATS cannot be a character vector."); endif ## Convert to cellstring if (! iscellstr (oldcats)) try oldcats = cellstr (oldcats); catch error (strcat ("categorical.mergecats: OLDCATS cannot be", ... " converted to cellstr.")); end_try_catch endif ## Force to column vector oldcats = oldcats(:); ## Check for optional third argument if (nargin < 3) newcat = oldcats{1}; else newcat = varargin{1}; if (isstring (newcat) && isscalar (newcat)) newcat = cellstr (newcat); elseif (ischar (newcat) && (isrow (newcat) || isempty (newcat))) newcat = cellstr (newcat); elseif (! (iscellstr (newcat) && isscalar (newcat))) error (strcat ("categorical.mergecats: NEWCAT must be either", ... " a character vector, a cellstring scalar, or", ... " a string scalar.")); endif newcat = cellstr (varargin{1}); if (cellfun ('isempty', newcat)) error ("categorical.mergecats: NEWCAT cannot be empty."); endif endif ## Keep old cat names that reference existing categories, ignore the rest [TF, index] = ismember (oldcats, A.cats); if (! any (TF, 'all')) B = A; return; endif index(! TF) = []; ## Only consecutive categories can be merged in ordinal arrays if (A.isOrdinal && any (diff (sort (index)) != 1)) error (strcat ("categorical.mergecats: only consecutive categories", ... " can be merged in ordinal categorical arrays.")); endif ## Merge categories TF = ismember (A.code, index); [Findex, Fidx] = min (index); index(Fidx) = []; B = A; B.code(TF) = Findex; B.cats(Findex) = newcat; B.cats(index) = []; ## Fix codes due to removed categories index = sort (index, 'descend'); for idx = index(:)' B.code(B.code > idx) -= 1; endfor endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} removecats (@var{A}) ## @deftypefnx {categorical} {@var{B} =} removecats (@var{A}, @var{oldcats}) ## ## Remove categories from categorical array. ## ## @code{@var{B} = removecats (@var{A})} removes all unused categories from ## categorical array @var{A}. The output categorical array @var{B} has the ## same size and values as @var{A}, but potentially fewer categories. ## ## @code{@var{B} = removecats (@var{A}, @var{oldcats})} removes the ## categories specified by @var{oldcats}. The elements of @var{B} that ## correspond to the removed categories are undefined. ## ## @var{oldcats} can be a string array, a cell array of character vectors, ## or any type of array that can be converted to a cell array of character ## vectors with the @code{cellstr} function. Any names in @var{oldcats} ## that do not reference an existing category are ignored. ## ## When removing a single category, @var{oldcats} can also be specified as a ## character vector. ## ## @end deftypefn function B = removecats (A, varargin) ## Remove unused categories if (nargin == 1) usedcodes = __unique__ (A.code); usedcodes(usedcodes == 0) = []; remcodes = setdiff (1:numel (A.cats), usedcodes); B = A; B.cats(remcodes) = []; remcodes = sort (remcodes, 'descend'); for code = remcodes(:)' B.code(B.code > code) -= 1; endfor else oldcats = varargin{1}; ## Some input validation if (isempty (oldcats)) ## Handle MATLAB compatibility here if (iscell (oldcats)) B = A; return; endif error ("categorical.removecats: OLDCATS cannot be empty."); elseif (isnumeric (oldcats) || islogical (oldcats)) error (strcat ("categorical.removecats: OLDCATS cannot be", ... " numeric or logical.")); endif ## Convert to cellstring if (! iscellstr (oldcats)) try oldcats = cellstr (oldcats); catch error (strcat ("categorical.removecats: OLDCATS", ... " cannot be converted to cellstr.")); end_try_catch endif ## Keep old cat names that reference existing categories, ignore the ## rest [TF, remcodes] = ismember (oldcats, A.cats); if (! any (TF, 'all')) B = A; return; endif remcodes(! TF) = []; [TF] = ismember (A.code, remcodes); B = A; B.code(TF) = 0; B.cats(remcodes) = []; B.isMissing = B.isMissing | TF; remcodes = sort (remcodes, 'descend'); for code = remcodes(:)' B.code(B.code > code) -= 1; endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} renamecats (@var{A}, @var{newnames}) ## @deftypefnx {categorical} {@var{B} =} renamecats (@var{A}, @var{oldnames}, @var{newnames}) ## ## Rename categories in categorical array. ## ## @code{@var{B} = renamecats (@var{A}, @var{newnames})} renames all the ## categories in @var{A}, without changing any of its values, with the names ## specified in @var{newnames}. @var{newnames} can be specified as a string ## array, a cell array of character vectors, or any type of array that can ## be converted to a cell array of character vectors with the @code{cellstr} ## function, as long as it has the same number of elements as the categories ## in @var{A}. ## ## @code{@var{B} = renamecats (@var{A}, @var{oldnames}, @var{newnames})} ## renames the categories of @var{A} specified in @var{oldnames} with the ## names specified in @var{newnames}. Both @var{oldnames} and ## @var{newnames} can be a string array, a cell array of character vectors, ## or any type of array that can be converted to a cell array of character ## vectors with the @code{cellstr} function, as long as they have the same ## number of elements. @var{oldnames} must specify a subset of existing ## categories in @var{A}. ## ## When renaming a single category, both @var{oldnames} and @var{newnames} ## can also be specified as character vectors. ## ## @end deftypefn function B = renamecats (A, varargin) ## Check input arguments if (nargin < 2) error ("categorical.renamecats: too few input arguments."); endif ## Process 2 input arguments if (nargin == 2) oldnames = A.cats; newnames = varargin{1}; ## Some input validation if (isempty (newnames)) ## Handle MATLAB compatibility here if (iscell (newnames)) B = A; return; endif error ("categorical.renamecats: NEWNAMES cannot be empty."); elseif (isnumeric (newnames) || islogical (newnames)) error (strcat ("categorical.renamecats: NEWNAMES cannot be", ... " numeric or logical.")); endif ## Convert to cellstring if (! iscellstr (newnames)) try newnames = cellstr (newnames); catch error (strcat ("categorical.renamecats: NEWNAMES", ... " cannot be converted to cellstr.")); end_try_catch endif ## Make sure categories match if (numel (oldnames) != numel (newnames)) error (strcat ("categorical.renamecats: NEWNAMES must equal the", ... " number of existing categories in input array.")); endif ## Process 3 input arguments else oldnames = varargin{1}; newnames = varargin{2}; ## OLDNAMES input validation if (isempty (oldnames) && ! iscell (oldnames)) error ("categorical.renamecats: OLDNAMES cannot be empty."); elseif (isnumeric (oldnames) || islogical (oldnames)) error (strcat ("categorical.renamecats: OLDNAMES cannot be", ... " numeric or logical.")); endif ## Convert to cellstring if (! iscellstr (oldnames)) try oldnames = cellstr (oldnames); catch error (strcat ("categorical.renamecats: OLDNAMES", ... " cannot be converted to cellstr.")); end_try_catch endif ## NEWNAMES input validation if (isempty (newnames) && ! iscell (newnames)) error ("categorical.renamecats: NEWNAMES cannot be empty."); elseif (isnumeric (newnames) || islogical (newnames)) error (strcat ("categorical.renamecats: NEWNAMES cannot be", ... " numeric or logical.")); endif ## Convert to cellstring if (! iscellstr (newnames)) try newnames = cellstr (newnames); catch error (strcat ("categorical.renamecats: NEWNAMES", ... " cannot be converted to cellstr.")); end_try_catch endif ## Make sure category names match if (numel (oldnames) != numel (newnames)) error (strcat ("categorical.renamecats: OLDNAMES and NEWNAMES", ... " must have the same number of elements.")); endif endif ## Find and rename existing categories [TF, index] = ismember (oldnames, A.cats); if (! all (TF)) error (strcat ("categorical.renamecats: OLDNAMES must be", ... " a subset of existing categories.")); endif B = A; B.cats(index) = newnames; endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} reordercats (@var{A}) ## @deftypefnx {categorical} {@var{B} =} reordercats (@var{A}, @var{neworder}) ## ## Reorder categories in categorical array. ## ## @code{@var{B} = reordercats (@var{A})} reorders the categories of @var{A} ## in alphanumeric order. ## ## @code{@var{B} = reordercats (@var{A}, @var{neworder})} reorders the ## categories of @var{A} according to the order specified by @var{neworder}, ## which must define a permutation of @code{categories (@var{A})}. ## ## @var{neworder} can be a numeric vector, a string array, a cell array of ## character vectors, or any type of array that can be converted to a cell ## array of character vectors with the @code{cellstr} function as long as it ## indexes all existing categories in @var{A}. ## ## @end deftypefn function B = reordercats (A, varargin) ## Get NEWORDER if (nargin == 1) neworder = sort (A.cats); else neworder = varargin{1}; ## NEWORDER input validation if (isnumeric (neworder)) catidx = 1:numel (A.cats); if (! (isvector (neworder) && neworder > 0 && ... fix (neworder) == neworder)) error (strcat ("categorical.reordercats: NEWORDER numeric", ... " input must be a vector of positive integers.")); endif if (! isequal (sort (neworder)(:), catidx(:))) error (strcat ("categorical.reordercats: NEWORDER numeric", ... " input must index a permutation of existing", ... " categories.")); endif neworder = A.cats(neworder); elseif (isempty (neworder)) error ("categorical.reordercats: NEWORDER cannot be empty."); elseif (islogical (neworder)) error ("categorical.reordercats: NEWORDER cannot be logical."); elseif (ischar (neworder)) error (strcat ("categorical.reordercats: NEWORDER cannot be", ... " a character vector.")); endif ## Convert to cellstring if (! iscellstr (neworder)) try neworder = cellstr (varargin{1}); catch error (strcat ("categorical.reordercats: NEWORDER", ... " cannot be converted to cellstr.")); end_try_catch endif ## Make sure categories match if (! all (ismember (neworder, A.cats))) error (strcat ("categorical.reordercats: NEWORDER must contain", ... " the same set with the existing categories.")); endif endif ## Reorder [~, newidx] = ismember (A.cats, neworder); B = A; B.code(! B.isMissing) = newidx(A.code(! A.isMissing)); B.cats = neworder(:); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} setcats (@var{A}, @var{newcats}) ## ## Set categories in categorical array. ## ## @code{@var{B} = setcats (@var{A}, @var{newcats})} sets categories in the ## categorical array @var{B} according to the elements of the input array ## @var{A} and the categories specified by @var{newcats} according to the ## following rules: ## ## @itemize ## @item Any element of @var{A} that corresponds to a category listed in ## @var{newcats} is copied to @var{B} with the same categorical value. ## @item Any categories of @var{A} not listed in @var{newcats} are not ## copied to @var{B} and the corresponding elements of @var{B} are ## undefined. ## @item New categories listed in @var{newcats} that are not present in ## @var{A} are added in @var{B}, but without any elements equal to these ## new categories. ## @end itemize ## ## @var{newcats} can be a string array, a cell array of character vectors ## or any type of array that can be converted to a cell array of character ## vectors with the @code{cellstr} function. When setting a single new ## category, @var{newcats} can also be specified as a character vector. ## ## @end deftypefn function B = setcats (A, newcats) ## Check input arguments if (nargin < 2) error ("categorical.setcats: too few input arguments."); endif ## NEWCATS input validation if (isempty (newcats)) ## Empty cell removes all categories if (iscell (newcats)) B = A; B.cats = {}; sz = size (A); B.code = uint16 (zeros (sz)); B.isMissing = true (sz); return; endif error ("categorical.setcats: NEWCATS cannot be empty."); elseif (isnumeric (newcats) || islogical (newcats)) error ("categorical.setcats: NEWCATS cannot be numeric or logical."); endif ## Convert to cellstring if (! iscellstr (newcats)) try newcats = cellstr (newcats); catch error (strcat ("categorical.setcats: NEWCATS cannot be", ... " converted to cellstr.")); end_try_catch endif ## Find existing categories [TF, index] = ismember (A.cats, newcats); ## Remove and reorder categories if (all (TF, 'all')) B = A; else B = removecats (A, A.cats(! TF)); endif [TF, index] = ismember (newcats, B.cats); index(! TF) = []; B = reordercats (B, B.cats(index)); [TF, index] = ismember (newcats, B.cats); index(! TF) = []; ## For each new category added before previously existing categories ## we need to increase the code indexing respectively for i = 1:numel (TF) if (! TF(i)) B.code(B.code >= i) += 1; endif endfor B.cats = newcats(:); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{C} =} times (@var{A}, @var{B}) ## ## Combine categorical arrays. ## ## @code{@var{C} = times (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{C} = @var{A} .* @var{B}} and returns a categorical array whose ## categories are the Cartesian product of the categories in @var{A} and ## @var{B} and each element is indexed to a new category which is the ## combination of the categories of the corresponding elements in @var{A} ## and @var{B}. ## ## @var{A} and @var{B} must be of common size or scalar categorical arrays. ## ## @end deftypefn function C = times (A, B) ## Check input arguments if (nargin < 2) error ("categorical.times: too few input arguments."); endif if (! isa (A, 'categorical') || ! isa (B, 'categorical')) error ("categorical.times: A and B must be categorical arrays."); endif newcats = {}; n_Bcats = numel (B.cats); for i = 1:numel (A.cats) for j = 1:n_Bcats newcats = [newcats; strjoin({A.cats{i}, B.cats{j}})]; endfor endfor C = addcats (categorical, newcats); C.code = A.code .* n_Bcats - (n_Bcats - B.code); C.isMissing = A.isMissing | B.isMissing; endfunction endmethods ################################################################################ ## ** Relational Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'eq' 'ge' 'gt' 'le' ## ## 'lt' 'ne' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} eq (@var{A}, @var{B}) ## ## Equality for categorical arrays. ## ## @code{@var{TF} = eq (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} == @var{B}} and returns a logical array of the ## same size as the largest input with its elements set to @qcode{true} ## where the corresponding elements of @var{A} and @var{B} are equal and set ## to @qcode{false} where they are not. @var{A} and @var{B} must be size ## compatible, which translates to they can be the same size, one can be ## scalar, or for every dimension, their dimension sizes must be equal or ## one of them must be 1. ## ## If categorical arrays @var{A} and @var{B} are ordinal, they must have ## the same set and ordering of categories. If neither are ordinal, the ## category names of each pair of elements are compared. Hence, they do ## not need to have the same set of categories. ## ## One of the input arguments can also be a character vector, a cellstr ## scalar or a string scalar as long as the other is a categorical array. ## In this case, a logical array of the same size as the categorical array ## is returned in which every element is tested for equality by comparing ## its category with that specified by the string argument. ## ## Undefined elements always return @qcode{false}, since they are not ## comparable to any other categorical values including other undefined ## elements. ## ## @end deftypefn function TF = eq (A, B) if (iscellstr (B) || isa (B, 'string') || ischar (B)) B = cellstr (B); if (! isscalar (B)) error (strcat ("categorical.eq: any text representation must", ... " specify a single categorical value.")); endif TF = strcmp (B, cellstr (A)); elseif (iscellstr (A) || isa (A, 'string') || ischar (A)) A = cellstr (A); if (! isscalar (A)) error (strcat ("categorical.eq: any text representation must", ... " specify a single categorical value.")); endif TF = strcmp (A, cellstr (B)); elseif (iscategorical (A) && iscategorical (B)) if (A.isOrdinal && B.isOrdinal) ## Check that both categorical arrays have the same categories ## and they are in the same order cats = cellfun (@(x) categories (x), {A, B}, 'UniformOutput', false); if (! isequal (cats{:})) error (strcat ("categorical.eq: comparison between ordinal", ... " arrays requires that both have the same", ... " categories, which must be ordered in the", ... " same way.")); endif TF = double (A) == double (B); elseif (A.isOrdinal || B.isOrdinal) error (strcat ("categorical.eq: cannot compare a categorical", ... " array that is ordinal with one that is not.")); else [A, B, err] = csize_expand (A, B); if (err) error ("categorical.eq: arrays have incompatible sizes."); endif TF = strcmp (cellstr (A), cellstr (B)); endif else error (strcat ("categorical.eq: comparison is not defined between", ... " '%s' and '%s' arrays."), class (A), class (B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} ge (@var{A}, @var{B}) ## ## Greater than or equal to for ordinal categorical arrays. ## ## @code{@var{TF} = ge (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} >= @var{B}} and returns a logical array of the ## same size as the largest input with its elements set to @qcode{true} ## where the corresponding elements of @var{A} are greater than or equal to ## @var{B} and set to @qcode{false} where they are not. @var{A} and @var{B} ## must be size compatible, which translates to they can be the same size, ## one can be scalar, or for every dimension, their dimension sizes must be ## equal or one of them must be 1. ## ## If categorical arrays @var{A} and @var{B} are both ordinal, they must ## have the same set and ordering of categories. Unordered categorical ## arrays cannot be compared for greater than or equal to inequality. ## ## One of the input arguments can also be a character vector, a cellstr ## scalar or a string scalar as long as the other is a categorical array. ## In this case, a logical array of the same size as the categorical array ## is returned in which every element is tested for greater than or equal to ## inequality by comparing its category with that specified by the string ## argument. ## ## Undefined elements always return @qcode{false}, since they are not ## comparable to any other categorical values including other undefined ## elements. ## ## @end deftypefn function TF = ge (A, B) if (iscellstr (B) || isa (B, 'string') || ischar (B)) if (! A.isOrdinal) error ("categorical.ge: categorical array is not ordinal."); endif B = cellstr (B); if (! isscalar (B)) error (strcat ("categorical.ge: any text representation must", ... " specify a single categorical value.")); endif code = find (ismember (categories (A), B)); if (isempty (code)) error ("categorical.ge: category does not exist in array."); endif TF = double (A) >= code; elseif (iscellstr (A) || isa (A, 'string') || ischar (A)) if (! B.isOrdinal) error ("categorical.ge: categorical array is not ordinal."); endif A = cellstr (A); if (! isscalar (A)) error (strcat ("categorical.ge: any text representation must", ... " specify a single categorical value.")); endif code = find (ismember (categories (B), A)); if (isempty (code)) error ("categorical.ge: category does not exist in array."); endif TF = code >= double (B); elseif (! iscategorical (A) || ! iscategorical (B)) error (strcat ("categorical.ge: relational comparison is not", ... " defined between '%s' and '%s' arrays."), ... class (A), class (B)); elseif (! A.isOrdinal || ! B.isOrdinal) error (strcat ("categorical.ge: relational comparison is not", ... " allowed between non-ordinal categorical arrays.")); else TF = double (A) >= double (B); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} gt (@var{A}, @var{B}) ## ## Greater than for ordinal categorical arrays. ## ## @code{@var{TF} = gt (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} > @var{B}} and returns a logical array of the ## same size as the largest input with its elements set to @qcode{true} ## where the corresponding elements of @var{A} are greater than @var{B} and ## set to @qcode{false} where they are not. @var{A} and @var{B} must be ## size compatible, which translates to they can be the same size, one can ## be scalar, or for every dimension, their dimension sizes must be equal or ## one of them must be 1. ## ## If categorical arrays @var{A} and @var{B} are both ordinal, they must ## have the same set and ordering of categories. Unordered categorical ## arrays cannot be compared for greater than inequality. ## ## One of the input arguments can also be a character vector, a cellstr ## scalar or a string scalar as long as the other is a categorical array. ## In this case, a logical array of the same size as the categorical array ## is returned in which every element is tested for greater than inequality ## by comparing its category with that specified by the string argument. ## ## Undefined elements always return @qcode{false}, since they are not ## comparable to any other categorical values including other undefined ## elements. ## ## @end deftypefn function TF = gt (A, B) if (iscellstr (B) || isa (B, 'string') || ischar (B)) if (! A.isOrdinal) error ("categorical.gt: categorical array is not ordinal."); endif B = cellstr (B); if (! isscalar (B)) error (strcat ("categorical.gt: any text representation must", ... " specify a single categorical value.")); endif code = find (ismember (categories (A), B)); if (isempty (code)) error ("categorical.gt: category does not exist in array."); endif TF = double (A) > code; elseif (iscellstr (A) || isa (A, 'string') || ischar (A)) if (! B.isOrdinal) error ("categorical.gt: categorical array is not ordinal."); endif A = cellstr (A); if (! isscalar (A)) error (strcat ("categorical.gt: any text representation must", ... " specify a single categorical value.")); endif code = find (ismember (categories (B), A)); if (isempty (code)) error ("categorical.gt: category does not exist in array."); endif TF = code > double (B); elseif (! iscategorical (A) || ! iscategorical (B)) error (strcat ("categorical.gt: relational comparison is not", ... " defined between '%s' and '%s' arrays."), ... class (A), class (B)); elseif (! A.isOrdinal || ! B.isOrdinal) error (strcat ("categorical.gt: relational comparison is not", ... " allowed between non-ordinal categorical arrays.")); else TF = double (A) > double (B); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} le (@var{A}, @var{B}) ## ## Less than or equal to for ordinal categorical arrays. ## ## @code{@var{TF} = le (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} <= @var{B}} and returns a logical array of the ## same size as the largest input with its elements set to @qcode{true} ## where the corresponding elements of @var{A} are less than or equal to ## @var{B} and set to @qcode{false} where they are not. @var{A} and @var{B} ## must be size compatible, which translates to they can be the same size, ## one can be scalar, or for every dimension, their dimension sizes must be ## equal or one of them must be 1. ## ## If categorical arrays @var{A} and @var{B} are both ordinal, they must ## have the same set and ordering of categories. Unordered categorical ## arrays cannot be compared for less than or equal to inequality. ## ## One of the input arguments can also be a character vector, a cellstr ## scalar or a string scalar as long as the other is a categorical array. ## In this case, a logical array of the same size as the categorical array ## is returned in which every element is tested for less than or equal to ## inequality by comparing its category with that specified by the string ## argument. ## ## Undefined elements always return @qcode{false}, since they are not ## comparable to any other categorical values including other undefined ## elements. ## ## @end deftypefn function TF = le (A, B) if (iscellstr (B) || isa (B, 'string') || ischar (B)) if (! A.isOrdinal) error ("categorical.le: categorical array is not ordinal."); endif B = cellstr (B); if (! isscalar (B)) error (strcat ("categorical.le: any text representation must", ... " specify a single categorical value.")); endif code = find (ismember (categories (A), B)); if (isempty (code)) error ("categorical.le: category does not exist in array."); endif TF = double (A) <= code; elseif (iscellstr (A) || isa (A, 'string') || ischar (A)) if (! B.isOrdinal) error ("categorical.le: categorical array is not ordinal."); endif A = cellstr (A); if (! isscalar (A)) error (strcat ("categorical.le: any text representation must", ... " specify a single categorical value.")); endif code = find (ismember (categories (B), A)); if (isempty (code)) error ("categorical.le: category does not exist in array."); endif TF = code <= double (B); elseif (! iscategorical (A) || ! iscategorical (B)) error (strcat ("categorical.le: relational comparison is not", ... " defined between '%s' and '%s' arrays."), ... class (A), class (B)); elseif (! A.isOrdinal || ! B.isOrdinal) error (strcat ("categorical.le: relational comparison is not", ... " allowed between non-ordinal categorical arrays.")); else TF = double (A) <= double (B); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} lt (@var{A}, @var{B}) ## ## Less than for ordinal categorical arrays. ## ## @code{@var{TF} = lt (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} < @var{B}} and returns a logical array of the ## same size as the largest input with its elements set to @qcode{true} ## where the corresponding elements of @var{A} are less than @var{B} and ## set to @qcode{false} where they are not. @var{A} and @var{B} must be ## size compatible, which translates to they can be the same size, one can ## be scalar, or for every dimension, their dimension sizes must be equal or ## one of them must be 1. ## ## If categorical arrays @var{A} and @var{B} are both ordinal, they must ## have the same set and ordering of categories. Unordered categorical ## arrays cannot be compared for less than inequality. ## ## One of the input arguments can also be a character vector, a cellstr ## scalar or a string scalar as long as the other is a categorical array. ## In this case, a logical array of the same size as the categorical array ## is returned in which every element is tested for less than inequality ## by comparing its category with that specified by the string argument. ## ## Undefined elements always return @qcode{false}, since they are not ## comparable to any other categorical values including other undefined ## elements. ## ## @end deftypefn function TF = lt (A, B) if (iscellstr (B) || isa (B, 'string') || ischar (B)) if (! A.isOrdinal) error ("categorical.lt: categorical array is not ordinal."); endif B = cellstr (B); if (! isscalar (B)) error (strcat ("categorical.lt: any text representation must", ... " specify a single categorical value.")); endif code = find (ismember (categories (A), B)); if (isempty (code)) error ("categorical.lt: category does not exist in array."); endif TF = double (A) < code; elseif (iscellstr (A) || isa (A, 'string') || ischar (A)) if (! B.isOrdinal) error ("categorical.lt: categorical array is not ordinal."); endif A = cellstr (A); if (! isscalar (A)) error (strcat ("categorical.lt: any text representation must", ... " specify a single categorical value.")); endif code = find (ismember (categories (B), A)); if (isempty (code)) error ("categorical.lt: category does not exist in array."); endif TF = code < double (B); elseif (! iscategorical (A) || ! iscategorical (B)) error (strcat ("categorical.lt: relational comparison is not", ... " defined between '%s' and '%s' arrays."), ... class (A), class (B)); elseif (! A.isOrdinal || ! B.isOrdinal) error (strcat ("categorical.lt: relational comparison is not", ... " allowed between non-ordinal categorical arrays.")); else TF = double (A) < double (B); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{TF} =} ne (@var{A}, @var{B}) ## ## Not equal for categorical arrays. ## ## @code{@var{TF} = ne (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} != @var{B}} and returns a logical array of the ## same size as the largest input with its elements set to @qcode{true} ## where the corresponding elements of @var{A} and @var{B} are not equal and ## set to @qcode{false} where they are equal. @var{A} and @var{B} must be ## size compatible, which translates to they can be the same size, one can ## be scalar, or for every dimension, their dimension sizes must be equal or ## one of them must be 1. ## ## If categorical arrays @var{A} and @var{B} are ordinal, they must have ## the same set and ordering of categories. If neither are ordinal, the ## category names of each pair of elements are compared. Hence, they do ## not need to have the same set of categories. ## ## One of the input arguments can also be a character vector, a cellstr ## scalar or a string scalar as long as the other is a categorical array. ## In this case, a logical array of the same size as the categorical array ## is returned in which every element is tested for inequality by comparing ## its category with that specified by the string argument. ## ## Undefined elements always return @qcode{true}, since they are not ## comparable to any other categorical values including other undefined ## elements. ## ## @end deftypefn function TF = ne (A, B) if (iscellstr (B) || isa (B, 'string') || ischar (B)) B = cellstr (B); if (! isscalar (B)) error (strcat ("categorical.ne: any text representation must", ... " specify a single categorical value.")); endif TF = ! strcmp (B, cellstr (A)); elseif (iscellstr (A) || isa (A, 'string') || ischar (A)) A = cellstr (A); if (! isscalar (A)) error (strcat ("categorical.ne: any text representation must", ... " specify a single categorical value.")); endif TF = ! strcmp (A, cellstr (B)); elseif (iscategorical (A) && iscategorical (B)) if (A.isOrdinal && B.isOrdinal) ## Check that both categorical arrays have the same categories ## and they are in the same order cats = cellfun (@(x) categories (x), {A, B}, 'UniformOutput', false); if (! isequal (cats{:})) error (strcat ("categorical.ne: comparison between ordinal", ... " arrays requires that both have the same", ... " categories, which must be ordered in the", ... " same way.")); endif TF = double (A) != double (B); elseif (A.isOrdinal || B.isOrdinal) error (strcat ("categorical.ne: cannot compare a categorical", ... " array that is ordinal with one that is not.")); else [A, B, err] = csize_expand (A, B); if (err) error ("categorical.ne: arrays have incompatible sizes."); endif TF = ! strcmp (cellstr (A), cellstr (B)); endif else error (strcat ("categorical.ne: comparison is not defined between", ... " '%s' and '%s' arrays."), class (A), class (B)); endif endfunction endmethods ################################################################################ ## ** Arithmetic Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'min' 'max' 'median' 'mode' ## ## 'histcounts' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {categorical} {@var{C} =} min (@var{A}) ## @deftypefnx {categorical} {[@var{C}, @var{index}] =} min (@var{A}) ## @deftypefnx {categorical} {@var{C} =} min (@var{A}, @qcode{[]}, @var{dim}) ## @deftypefnx {categorical} {@var{C} =} min (@var{A}, @qcode{[]}, @var{vecdim}) ## @deftypefnx {categorical} {@var{C} =} min (@var{A}, @qcode{[]}, @qcode{'all'}) ## @deftypefnx {categorical} {[@var{C}, @var{index}] =} min (@var{A}, @qcode{[]}, @qcode{'linear'}) ## @deftypefnx {categorical} {[@var{C}, @var{index}] =} min (@var{A}, @qcode{[]}, @dots{}, @qcode{'linear'}) ## @deftypefnx {categorical} {@var{C} =} min (@var{A}, @var{B}) ## @deftypefnx {categorical} {[@dots{}] =} min (@dots{}, @var{missingflag}) ## ## Smallest elements in ordinal categorical arrays. ## ## @code{@var{C} = min (@var{A})} returns the smallest element in ordinal ## categorical vector @var{A}. If @var{A} is a matrix, @code{min (@var{A})} ## returns a row vector with the smallest element from each column. For ## multidimensional arrays, @code{min (@var{A})} operates along the first ## non-singleton dimension. ## ## @code{[@var{C}, @var{index}] = min (@var{A})} also returns the indices of ## the minimum values in @var{index}, which has the same size as @var{C}. ## When the operating dimension contains more than one minimal elements, the ## index of the first one is returned. ## ## @code{@var{C} = min (@var{A}, @qcode{[]}, @var{dim})} operates along the ## dimension specified by @var{dim}. ## ## @code{@var{C} = min (@var{A}, @qcode{[]}, @var{vecdim})} operates on all ## the elements contained in the dimensions specified by @var{vecdim}, which ## must be a numeric vector of non-repeating positive integers. Any values ## in @var{vecdim} indexing dimensions larger that the actual array @var{A} ## are ignored. ## ## @code{@var{C} = min (@var{A}, @qcode{[]}, @qcode{'all'})} operates on all ## dimensions and returns the smallest element in @var{A}. ## ## @code{[@var{C}, @var{index}] = min (@var{A}, @qcode{[]}, @dots{})} also ## returns the first index of the minimum values in @var{index}. The second ## output is only valid when @code{min} operates on a single input array. ## Setting the @qcode{'linear'} flag returns the linear index to the ## corresponding minimum values in @var{A}. ## ## @code{@var{C} = min (@var{A}, @var{B})} returns an ordinal categorical ## array @var{C} with the smallest elements from @var{A} and @var{B}, which ## both must be ordinal categorical arrays of compatible sizes with the same ## set and ordering of categories. Compatible size means that @var{A} and ## @var{B} can be the same size, one can be scalar, or for every dimension, ## their dimension sizes must be equal or one of them must be 1. ## ## @code{[@dots{}] = min (@dots{}, @var{missingflag})} specifies how to ## handle undefined elements in any of the previous syntaxes. ## @var{missingflag} must be a character vector or a string scalar with one ## of the following values: ## ## @itemize ## @item @qcode{'omitundefined'}, which is the default, ignores all ## undefined elements and returns the minimum of the remaining elements. ## If all elements along the operating dimension are undefined, then it ## returns an undefined element. @qcode{'omitnan'} can also be used as ## equivalent to @qcode{'omitundefined'}. ## @item @qcode{'includeundefined'} returns an undefined element if there ## are any undefined elements along the operating dimension. ## @qcode{'includenan'} can also be used as equivalent to ## @qcode{'includeundefined'}. ## @end itemize ## ## @end deftypefn function [C, index] = min (A, B = [], varargin) ## Check for ordinal categorical array if (! A.isOrdinal) error ("categorical.min: categorical array A is not ordinal."); endif if (numel (varargin) > 3) error ("categorical.min: too many input arguments."); endif ## Handle categorical specific missing flag idx = strcmp ('omitundefined', varargin); if (any (idx)) varargin(idx) = 'omitnan'; endif idx = strcmp ('includeundefined', varargin); if (any (idx)) varargin(idx) = 'includenan'; endif ## Create output array C = A; ## Minimum of one array if (isempty (B) && isa (B, 'double')) A_d = double (A); if (nargin == 1) [C_d, index] = min (A_d); else [C_d, index] = min (A_d, [], varargin{:}); endif ## Fix missing codes C.isMissing = isnan (C_d); C.code = uint16 (C_d); ## Minimum of two arrays else ## No second output allowed if (nargout > 1) error (strcat ("categorical.min: two output arguments are", ... " not supported for two input arrays.")); endif ## Check for ordinal categorical array if (! isa (B, 'categorical')) error ("categorical.min: input array B must be categorical."); endif if (! B.isOrdinal) error ("categorical.min: categorical array B must be ordinal."); endif ## Check for same categories (including their order) Acats = categories (A); Bcats = categories (B); if (! isequal (Acats, Bcats)) error (strcat ("categorical.min: categorical arrays must have", ... " the same set of categories, including their", ... " order.")); endif ## Process codes and missing values A_d = double (A); B_d = double (B); C_d = min (A_d, B_d, varargin{:}); ## Fix missing codes C.isMissing = isnan (C_d); C.code = uint16 (C_d); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{C} =} max (@var{A}) ## @deftypefnx {categorical} {[@var{C}, @var{index}] =} max (@var{A}) ## @deftypefnx {categorical} {@var{C} =} max (@var{A}, @qcode{[]}, @var{dim}) ## @deftypefnx {categorical} {@var{C} =} max (@var{A}, @qcode{[]}, @var{vecdim}) ## @deftypefnx {categorical} {@var{C} =} max (@var{A}, @qcode{[]}, @qcode{'all'}) ## @deftypefnx {categorical} {[@var{C}, @var{index}] =} max (@var{A}, @qcode{[]}, @qcode{'linear'}) ## @deftypefnx {categorical} {[@var{C}, @var{index}] =} max (@var{A}, @qcode{[]}, @dots{}, @qcode{'linear'}) ## @deftypefnx {categorical} {@var{C} =} max (@var{A}, @var{B}) ## @deftypefnx {categorical} {[@dots{}] =} max (@dots{}, @var{missingflag}) ## ## Largest elements in ordinal categorical arrays. ## ## @code{@var{C} = max (@var{A})} returns the largest element in ordinal ## categorical vector @var{A}. If @var{A} is a matrix, @code{max (@var{A})} ## returns a row vector with the largest element from each column. For ## multidimensional arrays, @code{max (@var{A})} operates along the first ## non-singleton dimension. ## ## @code{[@var{C}, @var{index}] = max (@var{A})} also returns the indices of ## the maximum values in @var{index}, which has the same size as @var{C}. ## When the operating dimension contains more than one maximal elements, the ## index of the first one is returned. ## ## @code{@var{C} = max (@var{A}, @qcode{[]}, @var{dim})} operates along the ## dimension specified by @var{dim}. ## ## @code{@var{C} = max (@var{A}, @qcode{[]}, @var{vecdim})} operates on all ## the elements contained in the dimensions specified by @var{vecdim}, which ## must be a numeric vector of non-repeating positive integers. Any values ## in @var{vecdim} indexing dimensions larger that the actual array @var{A} ## are ignored. ## ## @code{@var{C} = max (@var{A}, @qcode{[]}, @qcode{'all'})} operates on all ## dimensions and returns the largest element in @var{A}. ## ## @code{[@var{C}, @var{index}] = max (@var{A}, @qcode{[]}, @dots{})} also ## returns the first index of the maximum values in @var{index}. The second ## output is only valid when @code{max} operates on a single input array. ## Setting the @qcode{'linear'} flag returns the linear index to the ## corresponding maximum values in @var{A}. ## ## @code{@var{C} = max (@var{A}, @var{B})} returns an ordinal categorical ## array @var{C} with the largest elements from @var{A} and @var{B}, which ## both must be ordinal categorical arrays of compatible sizes with the same ## set and ordering of categories. Compatible size means that @var{A} and ## @var{B} can be the same size, one can be scalar, or for every dimension, ## their dimension sizes must be equal or one of them must be 1. ## ## @code{[@dots{}] = max (@dots{}, @var{missingflag})} specifies how to ## handle undefined elements in any of the previous syntaxes. ## @var{missingflag} must be a character vector or a string scalar with one ## of the following values: ## ## @itemize ## @item @qcode{'omitundefined'}, which is the default, ignores all ## undefined elements and returns the maximum of the remaining elements. If ## all elements along the operating dimension are undefined, then it returns ## an undefined element. @qcode{'omitnan'} may also be used as equivalent ## to @qcode{'omitundefined'}. ## @item @qcode{'includeundefined'} returns an undefined element if there ## any undefined elements along the operating dimension. ## @qcode{'includenan'} may also be used as equivalent to ## @qcode{'includeundefined'}. ## @end itemize ## ## @end deftypefn function [C, index] = max (A, B = [], varargin) ## Check for ordinal categorical array if (! A.isOrdinal) error ("categorical.max: categorical array A is not ordinal."); endif if (numel (varargin) > 3) error ("categorical.max: too many input arguments."); endif ## Handle categorical specific missing flag idx = strcmp ('omitundefined', varargin); if (any (idx)) varargin(idx) = 'omitnan'; endif idx = strcmp ('includeundefined', varargin); if (any (idx)) varargin(idx) = 'includenan'; endif ## Create output array C = A; ## Minimum of one array if (isempty (B) && isa (B, 'double')) A_d = double (A); if (nargin == 1) [C_d, index] = max (A_d); else [C_d, index] = max (A_d, [], varargin{:}); endif ## Fix missing codes C.isMissing = isnan (C_d); C.code = uint16 (C_d); ## Minimum of two arrays else ## No second output allowed if (nargout > 1) error (strcat ("categorical.max: two output arguments are", ... " not supported for two input arrays.")); endif ## Check for ordinal categorical array if (! isa (B, 'categorical')) error ("categorical.max: input array B must be categorical."); endif if (! B.isOrdinal) error ("categorical.max: categorical array B must be ordinal."); endif ## Check for same categories (including their order) Acats = categories (A); Bcats = categories (B); if (! isequal (Acats, Bcats)) error (strcat ("categorical.max: categorical arrays must have", ... " the same set of categories, including their", ... " order.")); endif ## Process codes and missing values A_d = double (A); B_d = double (B); C_d = max (A_d, B_d, varargin{:}); ## Fix missing codes C.isMissing = isnan (C_d); C.code = uint16 (C_d); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} median (@var{A}) ## @deftypefnx {categorical} {@var{B} =} median (@var{A}, @var{dim}) ## @deftypefnx {categorical} {@var{B} =} median (@var{A}, @var{vecdim}) ## @deftypefnx {categorical} {@var{B} =} median (@var{A}, @qcode{'all'}) ## @deftypefnx {categorical} {@var{B} =} median (@dots{}, @var{missingflag}) ## ## Median value of an ordinal categorical array. ## ## @code{@var{B} = median (@var{A})} returns the median of the elements in ## ordinal categorical vector @var{A}. If @var{A} is a matrix, ## @code{median (@var{A})} returns a row vector with the median element ## from each column. For multidimensional arrays, @code{median (@var{A})} ## operates along the first non-singleton dimension. @var{B} is also ## ordinal with the same ordered categories as @var{A}. For even number of ## elements along the operating dimension, the returned median value is ## either the midway category between the two middle elements or the larger ## of the two categories midway between the two middle elements. ## ## @code{@var{B} = median (@var{A}, @var{dim})} operates along the dimension ## specified by @var{dim}. ## ## @code{@var{B} = median (@var{A}, @var{vecdim})} operates on all the ## elements contained in the dimensions specified by @var{vecdim}, which ## must be a numeric vector of non-repeating positive integers. Any values ## in @var{vecdim} indexing dimensions larger that the actual array @var{A} ## are ignored. ## ## @code{@var{C} = median (@var{A}, @qcode{[]}, @qcode{'all'})} operates on ## all dimensions and returns the median element in @var{A}. ## ## @code{@var{C} = median (@dots{}, @var{missingflag})} specifies how to ## handle undefined elements in any of the previous syntaxes. ## @var{missingflag} must be a character vector or a string scalar with one ## of the following values: ## ## @itemize ## @item @qcode{'omitundefined'} ignores all undefined elements and returns ## the median of the remaining elements. If all elements along the ## operating dimension are undefined, then it returns an undefined element. ## @qcode{'omitnan'} may also be used as equivalent to ## @qcode{'omitundefined'}. ## @item @qcode{'includeundefined'}, which is the default, returns an ## undefined element if there any undefined elements along the operating ## dimension. @qcode{'includenan'} may also be used as equivalent to ## @qcode{'includeundefined'}. ## @end itemize ## ## @end deftypefn function B = median (A, varargin) ## Check for ordinal categorical array if (! A.isOrdinal) error ("categorical.median: categorical array A is not ordinal."); endif if (numel (varargin) > 2) error ("categorical.median: too many input arguments."); endif ## Handle categorical specific missing flag idx = strcmp ('omitundefined', varargin); if (any (idx)) varargin(idx) = 'omitnan'; endif idx = strcmp ('includeundefined', varargin); if (any (idx)) varargin(idx) = 'includenan'; endif ## Compute median B_d = median (double (A), varargin{:}); ## Save median to output array B = A; B.isMissing = isnan (B_d); B.code = uint16 (round (B_d)); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{M} =} mode (@var{A}) ## @deftypefnx {categorical} {[@var{M}, @var{F}] =} mode (@var{A}) ## @deftypefnx {categorical} {[@var{M}, @var{F}, @var{C}] =} mode (@var{A}) ## @deftypefnx {categorical} {[@dots{}] =} mode (@var{A}, @var{dim}) ## @deftypefnx {categorical} {[@dots{}] =} mode (@var{A}, @var{vecdim}) ## @deftypefnx {categorical} {[@dots{}] =} mode (@var{A}, @qcode{'all'}) ## ## Most frequent element in a categorical array. ## ## @code{@var{M} = mode (@var{A})} returns the most frequent element in the ## categorical vector @var{A}. If @var{A} is a matrix, ## @code{mode (@var{A})} returns a row vector with the most frequent element ## from each column. For multidimensional arrays, @code{mode (@var{A})} ## operates along the first non-singleton dimension. @var{B} is also a ## categorical array with the same categories as @var{A}. For multiple ## elements with the same maximum frequency along the operating dimension, ## the element from the category that occurs first in @var{A} is returned. ## ## @code{[@var{M}, @var{F}] = mode (@var{A})} also returns a numeric array ## @var{F}, which has the same size as @var{M} and it contains the number of ## occurrences of each corresponding element of @var{M}. ## ## @code{[@var{M}, @var{F}, @var{C}] = mode (@var{A})} also returns a cell ## array @var{C}, which has the same size as @var{M} and each element is a ## sorted categorical vector of all the values with the same maximum ## frequency of the corresponding element of @var{M}. ## ## @code{@var{B} = mode (@var{A}, @var{dim})} operates along the dimension ## specified by @var{dim}. ## ## @code{@var{B} = mode (@var{A}, @var{vecdim})} operates on all the ## elements contained in the dimensions specified by @var{vecdim}, which ## must be a numeric vector of non-repeating positive integers. Any values ## in @var{vecdim} indexing dimensions larger that the actual array @var{A} ## are ignored. ## ## @code{@var{C} = mode (@var{A}, @qcode{[]}, @qcode{'all'})} operates on ## all dimensions and returns the most frequent element in @var{A}. ## ## @end deftypefn function [M, F, C] = mode (A, dim = []) ## Handle 'all' option first if (strcmpi (dim, 'all')) A = subset (A, ':'); dim = []; endif ## Simple case, only one input argument (or 'all' flag used) if (isempty (dim)) [m, F, c] = mode (double (A)); M = A; M.code = uint16 (m); M.isMissing = isnan (m); C = cell (size (c)); for i = 1:numel (c) tmp = A; tmp.code = uint16 (c{i}); tmp.isMissing = isnan (c{i}); C(i) = tmp; endfor ## Second input argument is either DIM or VECDIM else [m, F, c] = mode (double (A), dim); M = A; M.code = uint16 (m); M.isMissing = isnan (m); C = cell (size (c)); for i = 1:numel (c) tmp = A; tmp.code = uint16 (c{i}); tmp.isMissing = isnan (c{i}); C(i) = tmp; endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{N} =} histcounts (@var{A}) ## @deftypefnx {categorical} {@var{N} =} histcounts (@var{A}, @var{cats}) ## @deftypefnx {categorical} {@var{N} =} histcounts (@dots{}, @qcode{'Normalization'}, @var{normtype}) ## @deftypefnx {categorical} {[@var{N}, @var{cats}] =} histcounts (@dots{}) ## ## Histogram bin counts of a categorical array. ## ## @code{@var{N} = histcounts (@var{A})} returns a numeric vector @var{N} ## with the number of elements of each category in @var{A}. @var{A} can be ## a categorical array of any dimensions, but it is converted internally to ## a single column vector. ## ## @code{@var{N} = histcounts (@var{A}, @var{cats})} returns the number of ## elements only for the categories named in @var{cats}, in the order they ## are named there. @var{cats} may be a categorical array, a string array, ## or a cell array of character vectors, and the names must be unique. A ## name that is not a category of @var{A} is not an error: it counts zero. ## Note that @qcode{} elements are counted under no name at all, ## so they never appear in @var{N}. ## ## @code{@var{N} = histcounts (@dots{}, @qcode{'Normalization'}, ## @var{normtype})} specifies how to normalize the histogram values returned ## in @var{N} with any of the following options specified in @var{normtype}: ## ## @itemize ## @item @qcode{'count'}, which is the default, returns the number of ## elements in each category. ## @item @qcode{'countdensity'} is the same as @qcode{'count'}, since the ## bin width in categorical arrays is always equal to 1. ## @item @qcode{'probability'} returns the number of elements in each ## category relative to the total number of elements in @var{A}. ## @item @qcode{'percentage'} returns the percentage of the elements of ## @var{A} that fall in each category. ## @item @qcode{'pdf'} is the same as @qcode{'probability'}, since the bin ## width in categorical arrays is always equal to 1. ## @item @qcode{'cumcount'} returns the cumulative number of elements in ## each category and all previous categories. ## @item @qcode{'cdf'} returns the cumulative number of elements in ## each category and all previous categories relative to the total number of ## elements in @var{A}. ## @end itemize ## ## @code{[@var{N}, @var{cats}] = histcounts (@dots{})} also returns the ## corresponding categories of @var{A} for each count in @var{N}. ## @var{cats} is a cell array of character vectors with the same size as ## @var{N}. ## ## @end deftypefn function [N, cats] = histcounts (A, varargin) ## Parse and validate optional Name-Value paired argument optNames = {'Normalization'}; dfValues = {'count'}; [normtype, args] = parsePairedArguments (optNames, dfValues, varargin(:)); vnt = {'count', 'countdensity', 'probability', 'percentage', 'pdf', ... 'cumcount', 'cdf'}; if (isa (normtype, 'string')) normtype = char (normtype); endif ## The type is matched without regard to case, as MATLAB does if (! any (strcmpi (normtype, vnt))) error ("categorical.histcounts: invalid 'Normalization' type."); endif normtype = lower (normtype); ## Check for selected categories if (! isempty (args)) cats = args{1}; if (isa (cats, 'categorical')) ## The elements select the categories, in the order they appear; ## `categories` would sort them and silently reorder the counts. cats = cellstr (cats); elseif (isa (cats, 'string')) cats = cellstr (cats); elseif (! iscellstr (cats)) error (strcat ("categorical.histcounts: invalid", ... " type for CATS input argument.")); endif ## A name that is not a category of A is not an error; it simply counts ## zero, as MATLAB has it. Repeated names are refused, also as MATLAB ## has it -- they would silently return the same count twice. if (numel (unique (cats(:))) != numel (cats)) error (strcat ("categorical.histcounts: input argument CATS must", ... " not contain repeated category names.")); endif else cats = A.cats; endif ## Force cats to row vector cats = cats(:)'; ## Count elements in selected categories. The counts follow the order the ## categories were requested in, which is the order CATS is returned in, ## and not the order they happen to have in A. codes = A.code(:); [known, ccats] = ismember (cats, A.cats); ncats = numel (ccats); N = zeros (1, ncats); for i = 1:ncats ## 'ismember' answers 0 for a name A does not have, and 0 is also the ## code of an element, so a name that is not a category ## must be skipped rather than compared -- otherwise it would be ## credited with every undefined element in the array. if (known(i)) N(i) = sum (codes == ccats(i)); endif endfor ## Apply normalization scheme if (any (strcmp (normtype, {'probability', 'pdf'}))) N = N ./ numel (codes); elseif (strcmp (normtype, 'percentage')) N = 100 * N ./ numel (codes); elseif (strcmp (normtype, 'cumcount')) N = cumsum (N); elseif (strcmp (normtype, 'cdf')) N = cumsum (N ./ numel (codes)); endif endfunction endmethods ################################################################################ ## ** Sort, Filter, and Set Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'maxk' 'mink' 'sort' 'sortrows' ## ## 'topkrows' 'unique' 'intersect' 'setdiff' ## ## 'setxor' 'union' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} maxk (@var{A}, @var{K}) ## @deftypefnx {categorical} {@var{B} =} maxk (@var{A}, @var{K}, @var{dim}) ## @deftypefnx {categorical} {[@var{B}, @var{index}] =} maxk (@var{A}, @dots{}) ## ## Find the largest elements in an ordinal categorical array. ## ## @code{@var{B} = maxk (@var{A}, @var{K})} returns the @var{K} largest ## elements of the categorical array @var{A} in descending order. @var{A} ## must be ordinal, since the elements are ranked by the order of their ## categories. If @var{A} is a vector, then @var{B} is a vector with ## @var{K} elements. If @var{A} is a matrix, then @code{maxk} operates ## along each column of @var{A} and @var{B} has @var{K} rows. For ## multidimensional arrays, @code{maxk} operates along the first ## non-singleton dimension. @var{B} keeps the categories of @var{A}, ## including their order. ## ## @code{@var{B} = maxk (@var{A}, @var{K}, @var{dim})} operates along the ## dimension specified by @var{dim}. ## ## @var{K} must be a nonnegative integer scalar. If @var{K} is larger than ## the number of elements along the operating dimension, then all of them ## are returned. ## ## Missing elements (@qcode{}) are not ranked. They are appended ## after the ranked elements in their original order, and hence they only ## appear in @var{B} when @var{K} exceeds the number of defined elements ## along the operating dimension. Unlike @code{sort}, @code{maxk} has no ## @qcode{'MissingPlacement'} option, since undefined elements are always ## placed last. Elements comparing as equal keep their original order. ## ## @code{[@var{B}, @var{index}] = maxk (@var{A}, @dots{})} also returns an ## index array containing the indices of the returned elements of @var{A} ## along the operating dimension. ## ## @end deftypefn function [B, index] = maxk (A, K, varargin) if (nargin < 2) error ("categorical.maxk: too few input arguments."); endif ## Check for ordinal categorical array if (! A.isOrdinal) error ("categorical.maxk: categorical array A is not ordinal."); endif [index, lidx, errmsg] = __minmaxk__ (double (A), K, true, varargin); if (! isempty (errmsg)) error ("categorical.maxk: %s", errmsg); endif B = subset (A, lidx); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} mink (@var{A}, @var{K}) ## @deftypefnx {categorical} {@var{B} =} mink (@var{A}, @var{K}, @var{dim}) ## @deftypefnx {categorical} {[@var{B}, @var{index}] =} mink (@var{A}, @dots{}) ## ## Find the smallest elements in an ordinal categorical array. ## ## @code{@var{B} = mink (@var{A}, @var{K})} returns the @var{K} smallest ## elements of the categorical array @var{A} in ascending order. @var{A} ## must be ordinal, since the elements are ranked by the order of their ## categories. If @var{A} is a vector, then @var{B} is a vector with ## @var{K} elements. If @var{A} is a matrix, then @code{mink} operates ## along each column of @var{A} and @var{B} has @var{K} rows. For ## multidimensional arrays, @code{mink} operates along the first ## non-singleton dimension. @var{B} keeps the categories of @var{A}, ## including their order. ## ## @code{@var{B} = mink (@var{A}, @var{K}, @var{dim})} operates along the ## dimension specified by @var{dim}. ## ## @var{K} must be a nonnegative integer scalar. If @var{K} is larger than ## the number of elements along the operating dimension, then all of them ## are returned. ## ## Missing elements (@qcode{}) are not ranked. They are appended ## after the ranked elements in their original order, and hence they only ## appear in @var{B} when @var{K} exceeds the number of defined elements ## along the operating dimension. Unlike @code{sort}, @code{mink} has no ## @qcode{'MissingPlacement'} option, since undefined elements are always ## placed last. Elements comparing as equal keep their original order. ## ## @code{[@var{B}, @var{index}] = mink (@var{A}, @dots{})} also returns an ## index array containing the indices of the returned elements of @var{A} ## along the operating dimension. ## ## @end deftypefn function [B, index] = mink (A, K, varargin) if (nargin < 2) error ("categorical.mink: too few input arguments."); endif ## Check for ordinal categorical array if (! A.isOrdinal) error ("categorical.mink: categorical array A is not ordinal."); endif [index, lidx, errmsg] = __minmaxk__ (double (A), K, false, varargin); if (! isempty (errmsg)) error ("categorical.mink: %s", errmsg); endif B = subset (A, lidx); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} sort (@var{A}) ## @deftypefnx {categorical} {@var{B} =} sort (@var{A}, @var{dim}) ## @deftypefnx {categorical} {@var{B} =} sort (@var{A}, @var{direction}) ## @deftypefnx {categorical} {@var{B} =} sort (@var{A}, @var{dim}, @var{direction}) ## @deftypefnx {categorical} {@var{B} =} sort (@dots{}, @qcode{'MissingPlacement'}, @var{MP}) ## @deftypefnx {categorical} {[@var{B}, @var{index}] =} sort (@var{A}, @dots{}) ## ## Sort elements in a categorical array. ## ## @code{@var{B} = sort (@var{A})} sorts the categorical array @var{A} in ## ascending order. The sorted array @var{B} has the same categories as ## @var{A}. If @var{A} is a matrix, @code{sort (@var{A})} sorts each column ## of @var{A} in ascending order. For multidimensional arrays, ## @code{mode (@var{A})} sorts along the first non-singleton dimension. ## ## @code{@var{B} = sort (@var{A}, @var{dim})} sorts along the dimension ## specified by @var{dim}. ## ## @code{@var{B} = sort (@var{A}, @var{direction})} also specifies the ## sorting direction, which can be either @qcode{'ascend'} (default) or ## @qcode{'descend'}. ## ## @code{@var{B} = sort (@dots{}, @qcode{'MissingPlacement'}, @var{MP})} ## specifies where to place the missing elements (@qcode{}) ## returned in @var{B} with any of the following options specified in ## @var{MP}: ## ## @itemize ## @item @qcode{'auto'}, which is the default, places missing elements last ## for ascending sort and first for descending sort. ## @item @qcode{'first'} places missing elements first. ## @item @qcode{'last'} places missing elements last. ## @end itemize ## ## @code{[@var{B}, @var{index}] = sort (@var{A}, @dots{})} also returns a ## sorting index containing the original indices of the elements in the ## sorted array. ## ## @itemize ## @item If @var{A} is a vector, then @var{index} contains the original ## linear indices of the elements in the sorted vector @var{B} such that ## @code{@var{B} = @var{A}(@var{index})}. ## @item If @var{A} is an @math{M*N} matrix and @qcode{@var{dim} = 1}, then ## @var{index} contains the original row indices of the elements in the ## sorted vector @var{B} such that for @qcode{j = 1:N}, ## @code{@var{B}(:,j) = @var{A}(@var{index}(:,j),j)}. ## @end itemize ## ## @end deftypefn function [B, index] = sort (A, varargin) ## Parse and validate optional 'MissingPlacement' paired argument optNames = {'MissingPlacement'}; dfValues = {'auto'}; [MP, args] = parsePairedArguments (optNames, dfValues, varargin(:)); ## The values are matched without regard to case, as MATLAB does. MP = lower (MP); if (! ismember (MP, {'auto', 'first', 'last'})) error ("categorical.sort: invalid value for 'MissingPlacement'."); endif ## Force strings to character vectors [args{:}] = convertStringsToChars (args{:}); ## Only a dimension and a direction may remain, once each for k = 1:numel (args) arg = args{k}; if (isnumeric (arg)) if (! (isscalar (arg) && arg >= 1 && arg == fix (arg))) error ("categorical.sort: DIM must be a positive integer."); endif elseif (ischar (arg) && isrow (arg)) if (! any (strcmpi (arg, {'ascend', 'descend'}))) error ("categorical.sort: invalid DIRECTION '%s'.", arg); endif else error ("categorical.sort: invalid input argument."); endif endfor if (sum (cellfun (@isnumeric, args)) > 1 || sum (cellfun (@ischar, args)) > 1) error ("categorical.sort: invalid input argument."); endif if (numel (args) == 2 && ischar (args{1})) error ("categorical.sort: DIM must precede DIRECTION."); endif ## Get direction cid = cellfun (@ischar, args); if (any (cid)) dir = lower (args{cid}); ## Put it back, since args is forwarded to the core sort further down. args(cid) = {dir}; else dir = 'ascend'; endif ## Since codes are positive integers and 0 is used for undefined elements, ## we assume A.code == 0 for NaN. However, in ascending order NaNs go to ## the end, while 0s at the very top and vice versa. So we have to ## mitigate this in tandem with the 'MissingPlacement' option, by setting ## 0s to max(code) + 1 for certain combinations of options. code = A.code; is_nan = code == 0; if (any (is_nan(:))) if ((strcmp (dir, 'ascend') && any (strcmp (MP, {'auto', 'last'}))) || (strcmp (dir, 'descend') && any (strcmp (MP, {'auto', 'first'})))) ## Get new value for missing elements code nan_code = max (code(:)) + 1; code(is_nan) = nan_code; ## Sort values [code, index] = sort (code, args{:}); ## Get indices of missing values and change them back to 0 is_nan = code == nan_code; code(is_nan) = 0; else ## ## Sort values without swaping code for undefined elements [code, index] = sort (code, args{:}); is_nan = code == 0; endif else ## Sort values with no undefined elements [code, index] = sort (code, args{:}); endif ## Populate output categorical array B = A; B.code = uint16 (code); B.isMissing = is_nan; endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} sortrows (@var{A}) ## @deftypefnx {categorical} {@var{B} =} sortrows (@var{A}, @var{col}) ## @deftypefnx {categorical} {@var{B} =} sortrows (@var{A}, @var{direction}) ## @deftypefnx {categorical} {@var{B} =} sortrows (@var{A}, @var{col}, @var{direction}) ## @deftypefnx {categorical} {@var{B} =} sortrows (@dots{}, @qcode{'MissingPlacement'}, @var{MP}) ## @deftypefnx {categorical} {[@var{B}, @var{index}] =} sortrows (@var{A}, @dots{}) ## ## Sort rows in a categorical array. ## ## @code{@var{B} = sortrows (@var{A})} sorts the rows of the 2-D categorical ## array @var{A} in ascending order. The sorted array @var{B} has the same ## categories as @var{A}. ## ## @code{@var{B} = sortrows (@var{A}, @var{col})} sorts @var{A} according to ## to the columns specified by the numeric vector @var{col}, which must ## explicitly contain non-zero integers whose absolute values index existing ## columns in @var{A}. Positive elements sort the corresponding columns in ## ascending order, while negative elements sort the corresponding columns ## in descending order. ## ## @code{@var{B} = sortrows (@var{A}, @var{direction})} also specifies the ## sorting direction, which can be either @qcode{'ascend'} (default) or ## @qcode{'descend'} applying to all columns in @var{A}. Alternatively, ## @var{direction} can be a cell array of character vectors specifying ## the sorting direction for each individual column of @var{A}, in which ## case the number of elements in @var{direction} must equal the number of ## columns in @var{A}. ## ## @code{@var{B} = sortrows (@var{A}, @var{col}, @var{direction})} sorts the ## categorical array @var{A} according to the columns specified in @var{col} ## using the corresponding sorting direction specified in @var{direction}. ## In this case, the sign of the values in @var{col} is ignored. @var{col} ## and @var{direction} must have the same length, but not necessarily the ## same number of elements as the columns in @var{A}. ## ## @code{@var{B} = sortrows (@dots{}, @qcode{'MissingPlacement'}, @var{MP})} ## specifies where to place the missing elements (@qcode{}) ## returned in @var{B} with any of the following options specified in ## @var{MP}: ## ## @itemize ## @item @qcode{'auto'}, which is the default, places missing elements last ## for ascending sort and first for descending sort. ## @item @qcode{'first'} places missing elements first. ## @item @qcode{'last'} places missing elements last. ## @end itemize ## ## @code{[@var{B}, @var{index}] = sortrows (@var{A}, @dots{})} also returns ## an index vector containing the original row indices of @var{A} in the ## sorted matrix @var{B} such that @code{@var{B} = @var{A}(@var{index},:)}. ## ## @end deftypefn function [B, index] = sortrows (A, varargin) if (ndims (A) != 2) error ("categorical.sortrows: A must be a 2-D matrix."); endif ## Parse and validate optional 'MissingPlacement' paired argument optNames = {'MissingPlacement'}; dfValues = {'auto'}; [MP, args] = parsePairedArguments (optNames, dfValues, varargin(:)); ## The values are matched without regard to case, as MATLAB does. MP = lower (MP); if (! any (strcmp (MP, {'auto', 'first', 'last'}))) error ("categorical.sort: invalid value for 'MissingPlacement'."); endif ## Parse COL / DIRECTION input nc = size (A, 2); col = [1:nc]; # default ascending direction dir_flag = false; if (numel (args) > 2) error ("categorical.sortrows: too many input arguments."); endif if (numel (args) > 0) col = args{1}; if (isnumeric (col)) if (! isvector (col) || fix (col) != col) error (strcat ("categorical.sortrows: COL must be a vector", ... " of nonzero integers indexing columns in A.")); endif if (max (abs (col)) > nc) error ("categorical.sortrows: COL indexes non-existing column."); endif elseif (isvector (col) && (ischar (col) || iscellstr (col) || isa (col, 'string'))) direction = lower (cellstr (col)); if (! all (ismember (direction, {'ascend', 'descend'}))) error (strcat ("categorical.sortrows: DIRECTION input must", ... " contain either 'ascend' or 'descend' values.")); endif ## Apply scalar expansion if (isscalar (direction)) direction = repmat (direction, 1, nc); endif if (numel (direction) != nc) error (strcat ("categorical.sortrows: DIRECTION", ... " does not match the columns in A.")); endif ## Assign DIRECTION to COL col = [1:nc]; idx = strcmp (direction, 'descend'); col(idx) = - col(idx); dir_flag = true; else error ("categorical.sortrows: invalid type for COL argument."); endif endif if (numel (args) > 1) if (dir_flag) error ("categorical.sortrows: invalid third input argument."); endif if ((isvector (args{2}) && ischar (args{2})) || isa (args{2}, 'string')) direction = lower (cellstr (args{2})); elseif (isvector (args{2}) && iscellstr (args{2})) direction = args{2}; else error ("categorical.sortrows: invalid type for DIRECTION argument."); endif if (! all (ismember (direction, {'ascend', 'descend'}))) error (strcat ("categorical.sortrows: DIRECTION input must", ... " contain either 'ascend' or 'descend' values.")); endif ## Assign DIRECTION to COL if (isscalar (direction) && strcmp (direction, 'ascend')) col = abs (col); elseif (isscalar (direction) && strcmp (direction, 'descend')) col = - abs (col); else if (numel (direction) != numel (col)) error (strcat ("categorical.sortrows: DIRECTION", ... " does not match the elements in COL.")); endif col = abs (col); idx = strcmp (direction, 'descend'); col(idx) = - col(idx); endif endif ## Since codes are positive integers and 0 is used for undefined elements, ## we assume A.code == 0 for NaN. However, in ascending order NaNs go to ## the end, while 0s at the very top and vice versa. So we have to ## mitigate this in tandem with the 'MissingPlacement' option, by setting ## 0s to max(code) + 1 for certain combinations of options. code = A.code; is_nan = code == 0; if (any (is_nan(:))) ## Get new value for missing elements code nan_code = max (code(:)) + 1; asc_cols = []; des_cols = []; if (any (strcmp (MP, {'auto', 'last'}))) ## Change codes only in ascending columns asc_cols = col(col > 0); if (! isempty (asc_cols)) asc_code = code(:, asc_cols); a_is_nan = asc_code == 0; asc_code(a_is_nan) = nan_code; code(:, asc_cols) = asc_code; endif endif if (any (strcmp (MP, {'auto', 'first'}))) ## Change codes only in descending columns des_cols = abs (col(col < 0)); if (! isempty (des_cols)) des_code = code(:, des_cols); d_is_nan = des_code == 0; des_code(d_is_nan) = nan_code; code(:, des_cols) = des_code; endif endif ## Sort values [code, index] = sortrows (code, col); ## Get indices of missing values and change them back to 0 adcols = [asc_cols, des_cols]; if (! isempty (adcols)) adcode = code(:, [asc_cols, des_cols]); is_nan = adcode == nan_code; adcode(is_nan) = 0; code(:, [asc_cols, des_cols]) = adcode; endif ## Re-index missing values after sorting is_nan = code == 0; else ## Sort values with no undefined elements [code, index] = sortrows (code, col); endif ## Populate output categorical array B = A; B.code = uint16 (code); B.isMissing = is_nan; endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} topkrows (@var{A}, @var{K}) ## @deftypefnx {categorical} {@var{B} =} topkrows (@var{A}, @var{K}, @var{col}) ## @deftypefnx {categorical} {@var{B} =} topkrows (@var{A}, @var{K}, @var{direction}) ## @deftypefnx {categorical} {@var{B} =} topkrows (@var{A}, @var{K}, @var{col}, @var{direction}) ## ## Top K sorted rows of categorical array. ## ## @code{@var{B} = topkrows (@var{A}, @var{K})} returns the top @var{K} rows ## of the 2-D categorical array @var{A} sorted in descending order as a ## group. @var{K} must be a nonnegative integer scalar. If @var{K} is ## larger than the number of rows in @var{A}, then all of them are returned. ## @var{B} keeps the categories of @var{A}, including their order. ## ## Missing elements (@qcode{}) are not ranked. Within each sort ## column they are placed after the elements that are defined, whichever ## direction is asked for, so a row is demoted only by a missing element in ## a column that actually decides its position. Rows comparing as equal ## keep their original order. ## ## @code{@var{B} = topkrows (@var{A}, @var{K}, @var{col})} returns the top ## @var{K} rows of the 2-D categorical array @var{A} sorted according to the ## columns specified by the numeric vector @var{col}, which must contain ## positive integers indexing existing columns in @var{A}. Columns are used ## as sort keys in the order given, and those not listed are not used at ## all. The direction is descending unless @var{direction} says otherwise. ## ## @code{@var{B} = topkrows (@var{A}, @var{K}, @var{direction})} returns the ## top @var{K} rows of the 2-D categorical array @var{A} sorted according to ## @var{direction}, which can be either @qcode{'descend'} (default) or ## @qcode{'ascend'} applying to all columns in @var{A}. Alternatively, ## @var{direction} can be a cell array of character vectors specifying ## the sorting direction for each individual column of @var{A}, in which ## case the number of elements in @var{direction} must equal the number of ## columns in @var{A}. ## ## @code{@var{B} = topkrows (@var{A}, @var{K}, @var{col}, @var{direction})} ## returns the top @var{K} rows of the 2-D categorical array @var{A} sorted ## according to the columns specified in @var{col} using the corresponding ## sorting direction specified in @var{direction}. @var{col} and ## @var{direction} must have the same length, but not necessarily the same ## number of elements as the columns in @var{A}. ## ## @code{@var{B} = topkrows (@dots{}, @qcode{'MissingPlacement'}, @var{MP})} ## specifies where the missing elements (@qcode{}) are placed ## within each sort column, with any of the following options specified in ## @var{MP}: ## ## @itemize ## @item @qcode{'last'}, which is the default, places missing elements last ## whichever direction is asked for. ## @item @qcode{'first'} places missing elements first. ## @item @qcode{'auto'} places missing elements last for an ascending sort ## and first for a descending one, as @code{sortrows} does. ## @end itemize ## ## This is an Octave extension: MATLAB has no such option here and always ## ranks as @qcode{'last'} does. ## ## @code{[@var{B}, @var{index}] = topkrows (@var{A}, @dots{})} also returns ## an index vector containing the original row indices of @var{A} in ## @var{B}, such that @code{@var{B} = @var{A}(@var{index},:)}. ## ## @end deftypefn function [B, index] = topkrows (A, K, varargin) ## Check input arguments if (nargin < 2) error ("categorical.topkrows: too few input arguments."); endif if (ndims (A) != 2) error ("categorical.topkrows: A must be a 2-D matrix."); endif [col, direction, MP, errmsg] = ... __topkrowsargs__ (K, columns (A), varargin); if (! isempty (errmsg)) error ("categorical.topkrows: %s", errmsg); endif ## Sort rows, keeping undefined elements out of the ranking if (isempty (col)) [B, index] = sortrows (A, direction, 'MissingPlacement', MP); else [B, index] = sortrows (A, col, direction, 'MissingPlacement', MP); endif ## Return top K rows if (K < numel (index)) B = subset (B, 1:K, ':'); index = index(1:K); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} unique (@var{A}) ## @deftypefnx {categorical} {@var{B} =} unique (@var{A}, @var{setOrder}) ## @deftypefnx {categorical} {@var{B} =} unique (@var{A}, @var{occurrence}) ## @deftypefnx {categorical} {@var{B} =} unique (@var{A}, @var{setOrder}, @var{occurrence}) ## @deftypefnx {categorical} {@var{B} =} unique (@var{A}, @var{occurrence}, @var{setOrder}) ## @deftypefnx {categorical} {@var{B} =} unique (@var{A}, @dots{}, @qcode{'rows'}) ## @deftypefnx {categorical} {[@var{B}, @var{ixA}, @var{ixB}] =} unique (@dots{}) ## ## Unique values in a categorical array. ## ## @code{@var{B} = unique (@var{A})} returns the unique values of the ## categorical array @var{A} in the categorical vector @var{B} sorted ## according to the order of categories in @var{A}. @var{B} retains the ## same categories as @var{A}. If @var{A} is a row vector, then @var{B} ## is also a row vector, otherwise @code{unique} returns a column vector. ## ## @code{@var{B} = unique (@var{A}, @var{setOrder})} returns the unique ## values of the categorical array @var{A} in an order as specified by ## @var{setOrder}, which can be either of the following values: ## ## @itemize ## @item @qcode{'sorted'} (default) returns the unique values sorted in ## ascending order. ## @item @qcode{'stable'} returns the unique values according to their order ## of occurrence. ## @end itemize ## ## @code{@var{B} = unique (@var{A}, @var{occurrence})} returns the unique ## values of the categorical array @var{A} according to their order of ## occurrence. @var{occurrence} can be either of the following values: ## ## @itemize ## @item @qcode{'first'} (default) returns the first occurrence of each ## unique value, i.e. the lowest possible indices are returned. ## @item @qcode{'last'} returns the last occurrence of each unique value, ## i.e. the highest possible indices are returned. ## @end itemize ## ## You can specify @var{setOrder} and @var{occurrence} arguments together. ## ## @code{@var{B} = unique (@var{A}, @dots{}, @qcode{'rows'})} returns the ## unique rows of @var{A} by treating each row as a single entity. The ## @qcode{'rows'} option can be used alone or in any combination with the ## @var{setOrder} and @var{occurrence} arguments. @qcode{'rows'} can be ## placed at any position in the function's argument list after the input ## array @var{A}. However, this syntax is only valid for 2-dimensional ## categorical arrays. ## ## @code{[@var{B}, @var{ixA}, @var{ixB}] = unique (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} using any of the previous syntaxes. ## @var{ixA} and @var{ixB} map the arrays @var{A} and @var{B} to one another ## such that @qcode{@var{B} = @var{A}(@var{ixA})} and ## @qcode{@var{A} = @var{B}(@var{ixB})}. When the @qcode{'rows'} optional ## argument is specified, then @qcode{@var{B} = @var{A}(@var{ixA},:)} and ## @qcode{@var{A} = @var{B}(@var{ixB},:)}. ## ## @end deftypefn function [B, ixA, ixB] = unique (A, varargin) ## 'legacy' option is not supported if (any (strcmp ('legacy', varargin))) error ("duration.unique: 'legacy' option is not supported."); endif code = double (A); [~, ixA, ixB] = __unique__ (code, varargin{:}); if (any (strcmp ('rows', varargin))) B = subset (A, ixA, ':'); else B = subset (A, ixA); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{C} =} intersect (@var{A}, @var{B}) ## @deftypefnx {categorical} {@var{C} =} intersect (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {categorical} {[@var{C}, @var{ixA}, @var{ixB}] =} intersect (@dots{}) ## @deftypefnx {categorical} {@dots{} =} intersect (@dots{}, @var{order}) ## ## Set intersection of two categorical arrays. ## ## @code{@var{C} = intersect (@var{A}, @var{B})} returns the unique common ## values of the categorical arrays @var{A} and @var{B}. Either @var{A} or ## @var{B} input arguments may be a character vector, a string array, or a ## cell array of character vectors, which is promoted to a categorical array ## prior to set intersection. If both @var{A} and @var{B} are row vectors, ## then @var{C} is also a row vector, otherwise @code{intersect} returns a ## column vector. ## ## If categorical arrays @var{A} and @var{B} are ordinal, they must have ## the same set and ordering of categories, which is transfered to @var{C}. ## If neither are ordinal, the category names of each pair of elements are ## compared (they do not need to have the same set of categories) in which ## case the categories in @var{C} are the sorted union of the categories in ## @var{A} and @var{B}. ## ## @code{@var{C} = intersect (@var{A}, @var{B}, @qcode{'rows'}} returns the ## unique common rows of the categorical matrices @var{A} and @var{B}, which ## must have the same number of columns. By default, the rows in ## categorical matrix @var{C} are in sorted order. ## ## @code{[@var{C}, @var{ixA}, @var{ixB}] = intersect (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} such that ## @code{@var{C} = @var{A}(@var{ixA})} and ## @code{@var{C} = @var{B}(@var{ixB})}, unless the @qcode{'rows'} optional ## argument is given, in which case @code{@var{C} = @var{A}(@var{ixA},:)} ## and @code{@var{C} = @var{B}(@var{ixB},:)}. ## ## @code{@dots{} = intersect (@dots{}, @var{order})} also specifies the ## order of the returned unique values. @var{order} may be either ## @qcode{'sorted'}, which is the default behavior, or @qcode{'stable'}, ## in which case the unique values are returned in order of appearance. ## ## @end deftypefn function [C, ixA, ixB] = intersect (A, B, varargin) [C, ixA, ixB] = setop (A, B, 'intersect', varargin{:}); if (! iscategorical (C)) error (C); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{C} =} setdiff (@var{A}, @var{B}) ## @deftypefnx {categorical} {@var{C} =} setdiff (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {categorical} {[@var{C}, @var{ixA}] =} setdiff (@dots{}) ## @deftypefnx {categorical} {@dots{} =} setdiff (@dots{}, @var{order}) ## ## Set difference of two categorical arrays. ## ## @code{@var{C} = setdiff (@var{A}, @var{B})} returns the unique common ## values of the categorical arrays @var{A} and @var{B}. Either @var{A} or ## @var{B} input arguments may be a character vector, a string array, or a ## cell array of character vectors, which is promoted to a categorical array ## prior to set difference. If both @var{A} and @var{B} are row vectors, ## then @var{C} is also a row vector, otherwise @code{intersect} returns a ## column vector. ## ## If categorical arrays @var{A} and @var{B} are ordinal, they must have ## the same set and ordering of categories, which is transfered to @var{C}. ## If neither are ordinal, the category names of each pair of elements are ## compared (they do not need to have the same set of categories) in which ## case the categories in @var{C} are the sorted union of the categories in ## @var{A} and @var{B}. ## ## @code{@var{C} = setdiff (@var{A}, @var{B}, @qcode{'rows'}} returns the ## unique common rows of the categorical matrices @var{A} and @var{B}, which ## must have the same number of columns. By default, the rows in ## categorical matrix @var{C} are in sorted order. ## ## @code{[@var{C}, @var{ixA}] = setdiff (@dots{})} also returns the index ## vector @var{ixA} such that @code{@var{C} = @var{A}(@var{ixA})}, unless ## the @qcode{'rows'} optional argument is given, in which case ## @code{@var{C} = @var{A}(@var{ixA},:)}. ## ## @code{@dots{} = setdiff (@dots{}, @var{order})} also specifies the ## order of the returned unique values. @var{order} may be either ## @qcode{'sorted'}, which is the default behavior, or @qcode{'stable'}, ## in which case the unique values are returned in order of appearance. ## ## @end deftypefn function [C, ixA] = setdiff (A, B, varargin) [C, ixA] = setop (A, B, 'setdiff', varargin{:}); if (! iscategorical (C)) error (C); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{C} =} setxor (@var{A}, @var{B}) ## @deftypefnx {categorical} {@var{C} =} setxor (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {categorical} {[@var{C}, @var{ixA}, @var{ixB}] =} setxor (@dots{}) ## @deftypefnx {categorical} {@dots{} =} setxor (@dots{}, @var{order}) ## ## Set exclusive-or of two categorical arrays. ## ## @code{@var{C} = setxor (@var{A}, @var{B})} returns the unique common ## values of the categorical arrays @var{A} and @var{B}. Either @var{A} or ## @var{B} input arguments may be a character vector, a string array, or a ## cell array of character vectors, which is promoted to a categorical array ## prior to set exclusive-or. If both @var{A} and @var{B} are row vectors, ## then @var{C} is also a row vector, otherwise @code{setxor} returns a ## column vector. ## ## If categorical arrays @var{A} and @var{B} are ordinal, they must have ## the same set and ordering of categories, which is transfered to @var{C}. ## If neither are ordinal, the category names of each pair of elements are ## compared (they do not need to have the same set of categories) in which ## case the categories in @var{C} are the sorted union of the categories in ## @var{A} and @var{B}. ## ## @code{@var{C} = setxor (@var{A}, @var{B}, @qcode{'rows'}} returns the ## unique common rows of the categorical matrices @var{A} and @var{B}, which ## must have the same number of columns. By default, the rows in ## categorical matrix @var{C} are in sorted order. ## ## @code{[@var{C}, @var{ixA}, @var{ixB}] = setxor (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} such that ## @code{@var{C} = @var{A}(@var{ixA})} and ## @code{@var{C} = @var{B}(@var{ixB})}, unless the @qcode{'rows'} optional ## argument is given, in which case @code{@var{C} = @var{A}(@var{ixA},:)} ## and @code{@var{C} = @var{B}(@var{ixB},:)}. ## ## @code{@dots{} = setxor (@dots{}, @var{order})} also specifies the ## order of the returned unique values. @var{order} may be either ## @qcode{'sorted'}, which is the default behavior, or @qcode{'stable'}, ## in which case the unique values are returned in order of appearance. ## ## @end deftypefn function [C, ixA, ixB] = setxor (A, B, varargin) [C, ixA, ixB] = setop (A, B, 'setxor', varargin{:}); if (! iscategorical (C)) error (C); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{C} =} union (@var{A}, @var{B}) ## @deftypefnx {categorical} {@var{C} =} union (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {categorical} {[@var{C}, @var{ixA}, @var{ixB}] =} union (@dots{}) ## @deftypefnx {categorical} {@dots{} =} union (@dots{}, @var{order}) ## ## Set union of two categorical arrays. ## ## @code{@var{C} = union (@var{A}, @var{B})} returns the unique common ## values of the categorical arrays @var{A} and @var{B}. Either @var{A} or ## @var{B} input arguments may be a character vector, a string array, or a ## cell array of character vectors, which is promoted to a categorical array ## prior to set exclusive-or. If both @var{A} and @var{B} are row vectors, ## then @var{C} is also a row vector, otherwise @code{union} returns a ## column vector. ## ## If categorical arrays @var{A} and @var{B} are ordinal, they must have ## the same set and ordering of categories, which is transfered to @var{C}. ## If neither are ordinal, the category names of each pair of elements are ## compared (they do not need to have the same set of categories) in which ## case the categories in @var{C} are the sorted union of the categories in ## @var{A} and @var{B}. ## ## @code{@var{C} = union (@var{A}, @var{B}, @qcode{'rows'}} returns the ## unique common rows of the categorical matrices @var{A} and @var{B}, which ## must have the same number of columns. By default, the rows in ## categorical matrix @var{C} are in sorted order. ## ## @code{[@var{C}, @var{ixA}, @var{ixB}] = union (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} such that ## @code{@var{C} = @var{A}(@var{ixA})} and ## @code{@var{C} = @var{B}(@var{ixB})}, unless the @qcode{'rows'} optional ## argument is given, in which case @code{@var{C} = @var{A}(@var{ixA},:)} ## and @code{@var{C} = @var{B}(@var{ixB},:)}. ## ## @code{@dots{} = union (@dots{}, @var{order})} also specifies the ## order of the returned unique values. @var{order} may be either ## @qcode{'sorted'}, which is the default behavior, or @qcode{'stable'}, ## in which case the unique values are returned in order of appearance. ## ## @end deftypefn function [C, ixA, ixB] = union (A, B, varargin) [C, ixA, ixB] = setop (A, B, 'union', varargin{:}); if (! iscategorical (C)) error (C); endif endfunction endmethods ################################################################################ ## ** Array Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'cat' 'horzcat' 'vertcat' 'repmat' ## ## 'repelem' 'repelems' 'reshape' 'circshift' ## ## 'permute' 'ipermute' 'transpose' 'ctranspose' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {categorical} {@var{C} =} cat (@var{dim}, @var{A}, @var{B}, @dots{}) ## ## Concatenate categorical arrays. ## ## @code{@var{C} = cat (@var{dim}, @var{A}, @var{B}, @dots{})} concatenates ## categorical arrays @var{A}, @var{B}, @dots{} along dimension @var{dim}. ## All input arrays must have the same size except along the operating ## dimension @var{dim}. Any of the input arrays may also be character ## vectors, string arrays or cell arrays of character vectors of compatible ## size, whose texts name categories, or missing values, which are ## undefined. Empty arrays are ignored. A numeric or logical array is ## refused, since a number could name a category or its code. ## ## If any input array is an ordinal categorical array, then all inputs must ## be ordinal categorical arrays with the same set and ordering of ## categories. In this case, @var{C} is also an ordinal categorical array ## with the same set and ordering of categories. If none of the input ## arrays are ordinal, then they do not need to have the same set of ## categories. In this case, categorical array @var{C} contains the union ## of the categories from all input arrays. Protected categorical arrays ## can only be concatenated with other arrays that have the same set of ## categories but not necessarily in the same order. ## ## @end deftypefn function out = cat (dim, varargin) ## Remove empty cell and numeric inputs, which add nothing varargin(cellfun (@(x) isempty (x) && (iscell (x) || isnumeric (x)), ... varargin)) = []; ## Only text and missing values join a categorical array, as in MATLAB; ## a number could name a category or its code, and is refused. for i = 1:numel (varargin) x = varargin{i}; if (ischar (x) && ! (isrow (x) || isempty (x))) error (strcat ("categorical.cat: cannot concatenate a character", ... " matrix with a categorical array; use a cell", ... " array of character vectors.")); elseif (iscell (x) && ! iscellstr (x)) error (strcat ("categorical.cat: cannot concatenate a cell array", ... " with a categorical array unless it holds only", ... " character vectors.")); elseif (! (iscategorical (x) || ischar (x) || iscellstr (x) || isa (x, 'string') || isa (x, 'missing'))) error (strcat ("categorical.cat: cannot concatenate a '%s' array", ... " with a categorical array."), class (x)); endif endfor if (numel (varargin) == 1) out = varargin{1}; return; endif ## Identify non-categorical inputs for reordering categories firstcatid = find (cellfun (@(x) iscategorical (x), varargin), 1); ## Promote everything into categorical arrays args = varargin; [args{:}] = promote (varargin{:}); ## Check that all dimensions except DIM are equal if (fix (dim) != dim | dim < 1) error ("categorical.cat: DIM must be a valid dimension."); endif sz_dim = cellfun (@(x) size (x), args, "UniformOutput", false); n_dims = cellfun (@(x) ndims (x), args, "UniformOutput", false); if (! isequal (n_dims{:})) error ("categorical.cat: dimensions mismatch."); elseif (dim <= n_dims{1}) idx = 1:n_dims{1}; idx(dim) = []; rem_dims = cellfun (@(x) (x(idx)), sz_dim, "UniformOutput", false); if (! isequal (rem_dims{:})) error ("categorical.cat: size mismatch."); endif endif ## If any categorical value is ordinal, all must be is_ordinal = cellfun (@isordinal, args); if (all (is_ordinal)) ## Check that all categorical arrays have the same categories ## and they are in the same order cats = cellfun (@(x) categories (x), args, 'UniformOutput', false); if (! isequal (cats{:})) error (strcat ("categorical.cat: cannot concatenate ordinal", ... " categorical arrays unless they have the same", ... " ordered set of categories.")); endif out = args{1}; fieldArgs = cellfun (@(x) x.code, args, 'UniformOutput', false); out.code = cat (dim, fieldArgs{:}); fieldArgs = cellfun (@(x) x.isMissing, args, 'UniformOutput', false); out.isMissing = cat (dim, fieldArgs{:}); return; elseif (any (is_ordinal)) error (strcat ("categorical.cat: cannot concatenate ordinal", ... " with non-ordinal categorical arrays.")); endif ## If any categorical array is protected, all must have the same ## categories is_protected = cellfun (@isprotected, args); if (any (is_protected)) ## Check that all categorical arrays have the same categories ## but they are not necessarily in the same order cats = cellfun (@(x) categories (x), args, 'UniformOutput', false); if (! all (ismember (cats{:}))) error (strcat ("categorical.cat: cannot concatenate protected", ... " categorical arrays that do not have the same", ... " set of categories.")); endif out = args{1}; out.isProtected = true; # returning array must also be protected ## Go through remaining categorical arrays and reorder codes accordingly idx = cell (1, numel (cats{1})); for i = 2:numel (args) for j = 1:numel (out.cats) new_code = find (strcmp (args{i}.cats, out.cats(j))); idx{j} = args{i}.code == new_code; endfor for j = 1:numel (out.cats) args{i}.code(idx{j}) = j; endfor endfor fieldArgs = cellfun (@(x) x.code, args, 'UniformOutput', false); out.code = cat (dim, fieldArgs{:}); fieldArgs = cellfun (@(x) x.isMissing, args, 'UniformOutput', false); out.isMissing = cat (dim, fieldArgs{:}); return; endif ## No constrains, add new categories as necessary and bump code indexing ## to reflect the changes in category list of concatenated output array out = args{1}; for i = 2:numel (args) n_cats = numel (categories (args{i})); idx = cell (2, n_cats); newcat = {}; n_code = 0; for j = 1:n_cats new_code = find (strcmp (out.cats, args{i}.cats(j))); if (! isempty (new_code)) idx{1,j} = args{i}.code == j; idx{2,j} = new_code; else idx{1,j} = args{i}.code == j; idx{2,j} = numel (out.cats) + n_code + 1; n_code += 1; newcat = [newcat; args{i}.cats(j)]; endif endfor out.cats = [out.cats; newcat]; for j = 1:n_cats args{i}.code(idx{1,j}) = idx{2,j}; endfor fieldArgs = cellfun (@(x) x.code, args, 'UniformOutput', false); out.code = cat (dim, fieldArgs{:}); fieldArgs = cellfun (@(x) x.isMissing, args, 'UniformOutput', false); out.isMissing = cat (dim, fieldArgs{:}); endfor ## The categories of the first categorical input array need go first if (firstcatid > 1) catstostart = categories (args{firstcatid}); catsremoved = out.cats(! ismember (out.cats, catstostart)); catsreorder = [catstostart; catsremoved]; out = reordercats (out, catsreorder); endif endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{C} =} horzcat (@var{A}, @var{B}, @dots{}) ## ## Horizontal concatenation of categorical arrays. ## ## @code{@var{C} = horzcat (@var{A}, @var{B}, @dots{}} is the equivalent of ## the syntax @code{@var{B} = [@var{A}, @var{B}, @dots{}]} and horizontally ## concatenates the categorical arrays @var{A}, @var{B}, @dots{}. All input ## arrays must have the same size except along the second dimension. Any of ## the input arrays may also be character vectors, string arrays or cell ## arrays of character vectors of compatible size, whose texts name ## categories, or missing values, which are undefined. Empty arrays are ## ignored. A numeric or logical array is refused, since a number could ## name a category or its code. ## ## If any input array is an ordinal categorical array, then all inputs must ## be ordinal categorical arrays with the same set and ordering of ## categories. In this case, @var{C} is also an ordinal categorical array ## with the same set and ordering of categories. If none of the input ## arrays are ordinal, then they do not need to have the same set of ## categories. In this case, categorical array @var{C} contains the union ## of the categories from all input arrays. Protected categorical arrays ## can only be concatenated with other arrays that have the same set of ## categories but not necessarily in the same order. ## ## @end deftypefn function out = horzcat (varargin) out = cat (2, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{C} =} vertcat (@var{A}, @var{B}, @dots{}) ## ## Vertical concatenation of categorical arrays. ## ## @code{@var{C} = vertcat (@var{A}, @var{B}, @dots{}} is the equivalent of ## the syntax @code{@var{B} = [@var{A}; @var{B}; @dots{}]} and vertically ## concatenates the categorical arrays @var{A}, @var{B}, @dots{}. All input ## arrays must have the same size except along the first dimension. Any of ## the input arrays may also be character vectors, string arrays or cell ## arrays of character vectors of compatible size, whose texts name ## categories, or missing values, which are undefined. Empty arrays are ## ignored. A numeric or logical array is refused, since a number could ## name a category or its code. ## ## If any input array is an ordinal categorical array, then all inputs must ## be ordinal categorical arrays with the same set and ordering of ## categories. In this case, @var{C} is also an ordinal categorical array ## with the same set and ordering of categories. If none of the input ## arrays are ordinal, then they do not need to have the same set of ## categories. In this case, categorical array @var{C} contains the union ## of the categories from all input arrays. Protected categorical arrays ## can only be concatenated with other arrays that have the same set of ## categories but not necessarily in the same order. ## ## @end deftypefn function out = vertcat (varargin) out = cat (1, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} repmat (@var{A}, @var{n}) ## @deftypefnx {categorical} {@var{B} =} repmat (@var{A}, @var{d1}, @dots{}, @var{dN}) ## @deftypefnx {categorical} {@var{B} =} repmat (@var{A}, @var{dimvec}) ## ## Repeat copies of a categorical array. ## ## @code{@var{B} = repmat (@var{A}, @var{n})} returns a categorical array ## @var{B} containing @var{n} copies of the input categorical array @var{A} ## along every dimension of @var{A}. ## ## @code{@var{B} = repmat (@var{A}, @var{d1}, @dots{}, @var{dN})} returns an ## array @var{B} containing copies of @var{A} along the dimensions specified ## by the list of scalar integer values @var{d1}, @dots{}, @var{dN}, which ## specify how many copies of @var{A} are made in each dimension. ## ## @code{@var{B} = repmat (@var{A}, @var{dimvec})} is equivalent to the ## previous syntax with @code{@var{dimvec} = [@var{d1}, @dots{}, @var{dN}]}. ## ## @end deftypefn function this = repmat (this, varargin) this.code = repmat (this.code, varargin{:}); this.isMissing = repmat (this.isMissing, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} repelem (@var{A}, @var{n}) ## @deftypefnx {categorical} {@var{B} =} repelem (@var{A}, @var{d1}, @dots{}, @var{dN}) ## ## Repeat copies of categorical array elements. ## ## @code{@var{B} = repelem (@var{A}, @var{n})} returns a categorical vector ## @var{B} containing repeated elements of the input @var{A}, which must be ## a categorical vector. If @var{n} is a scalar, each element of @var{A} is ## repeated @var{n} times along the non-singleton dimension of @var{A}. If ## @var{n} is a vector, it must have the same elements as @var{A}, in which ## case it specifies the number of times to repeat each corresponding ## element of @var{A}. ## ## @code{@var{B} = repelem (@var{A}, @var{d1}, @dots{}, @var{dN})} returns ## an array @var{B} with each element of @var{A} repeated according to the ## the list of input arguments @code{@var{d1}, @dots{}, @var{dN}} each ## corresponding to a different dimension @code{1:ndims (@var{A})} of the ## input array @var{A}. @var{d1}, @dots{}, @var{dN} must be either scalars ## or vectors with the same length as the corresponding dimension of ## @var{A} containing non-negative integer values specifying the number of ## repetitions of each element along the corresponding dimension. ## ## @end deftypefn function this = repelem (this, varargin) this.code = repelem (this.code, varargin{:}); this.isMissing = repelem (this.isMissing, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} repelems (@var{A}, @var{R}) ## ## Construct a vector of repeated elements from a categorical array. ## ## @code{@var{B} = repelems (@var{A}, @var{R})} returns a categorical vector ## @var{B} containing repeated elements of the input @var{A}, which must be ## a categorical vector. @var{R} must be a @math{2*N} matrix of integers. ## Entries in the first row of @var{R} correspond to the linear indexing of ## the elements in @var{A} to be repeated. The corresponding entries in the ## second row of @var{R} specify the repeat count of each element. ## ## @end deftypefn function this = repelems (this, R) this.code = repelems (this.code, R); this.isMissing = repelems (this.isMissing, R); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} reshape (@var{A}, @var{d1}, @dots{}, @var{dN}) ## @deftypefnx {categorical} {@var{B} =} reshape (@var{A}, @dots{}, @qcode{[]}, @dots{}) ## @deftypefnx {categorical} {@var{B} =} reshape (@var{A}, @var{dimvec}) ## ## Repeat copies of categorical array elements. ## ## @code{@var{B} = reshape (@var{A}, @var{d1}, @dots{}, @var{dN})} returns a ## categorical array @var{B} with specified dimensions @var{d1}, @dots{}, ## @var{dN}, whose elements are taken columnwise from the categorical array ## @var{A}. The product of @var{d1}, @dots{}, @var{dN} must equal the total ## number of elements in @var{A}. ## ## @code{@var{B} = reshape (@var{A}, @dots{}, @qcode{[]}, @dots{})} returns ## a categorical array @var{B} with one dimension unspecified which is ## calculated automatically so that the product of dimensions in @var{B} ## matches the total elements in @var{A}, which must be divisible the ## product of specified dimensions. An empty matrix @qcode{([])} is used to ## flag the unspecified dimension. ## ## @end deftypefn function this = reshape (this, varargin) this.code = reshape (this.code, varargin{:}); this.isMissing = reshape (this.isMissing, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} circshift (@var{A}, @var{n}) ## @deftypefnx {categorical} {@var{B} =} circshift (@var{A}, @var{n}, @var{dim}) ## ## Circularly shift the elements in a categorical array. ## ## @code{@var{B} = circshift (@var{A}, @var{n})} circularly shifts the ## elements of the categorical array @var{A} according to @var{n}. If ## @var{n} is a nonzero integer scalar, then the elements of @var{A} are ## shifted by @var{n} elements along the first non-singleton dimension of ## @var{A}. If @var{n} is a vector, it must not be longer that the number ## of dimensions of @var{A} with each value of @var{n} corresponding to a ## dimension in @var{A}. The sign of the value(s) in @var{n} specify the ## direction in the elements of @var{A} are shifted. ## ## @code{@var{B} = circshift (@var{A}, @var{n}, @var{dim})} circularly ## shifts the elements of the categorical array @var{A} along the dimension ## specified by @var{dim}. In this case, @var{n} must be a scalar integer ## value. ## ## @end deftypefn function this = circshift (this, varargin) this.code = circshift (this.code, varargin{:}); this.isMissing = circshift (this.isMissing, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} permute (@var{A}, @var{dims}) ## ## Generalized transpose for a categorical N-D array. ## ## @code{@var{B} = permute (@var{A}, @var{dims})} returns the generalized ## transpose of the categorical array @var{A} by rearranging its dimensions ## according to the permutation vector specified in @var{dims}. ## ## @var{dims} must index all the dimensions @code{1:ndims (@var{A})} of the ## input array @var{A}, in any order, but only once. The @var{N}th ## dimension of @var{A} gets remapped to the dimension in @var{B} specified ## by @code{@var{dims}(@var{N})}. ## ## @end deftypefn function this = permute (this, dims) this.code = permute (this.code, dims); this.isMissing = permute (this.isMissing, dims); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{A} =} ipermute (@var{B}, @var{dims}) ## ## Inverse of the generalized transpose for a categorical N-D array. ## ## @code{@var{A} = ipermute (@var{B}, @var{dims})} returns the inverse of ## the generalized transpose performed by the @code{permute} function. The ## expression @code{ipermute (permute (@var{A}, @var{dims}), @var{dims})} ## returns the original array @var{A}. ## ## @var{dims} must index all the dimensions @code{1:ndims (@var{B})} of the ## input array @var{B}, in any order, but only once. The dimension of ## @var{B} specified in @code{@var{dims}(@var{N})} gets remapped to the ## @var{N}th dimension of @var{A}. ## ## @end deftypefn function this = ipermute (this, dims) this.code = ipermute (this.code, dims); this.isMissing = ipermute (this.isMissing, dims); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} transpose (@var{A}) ## ## Transpose a categorical matrix. ## ## @code{@var{B} = transpose (@var{A})} is the equivalent of the syntax ## @code{@var{B} = @var{A}.'} and returns the transpose of the categorical ## matrix @var{A}. ## ## @end deftypefn function this = transpose (this) if (ndims (this) != 2) error ("categorical.transpose: not defined for N-D arrays."); endif this.code = transpose (this.code); this.isMissing = transpose (this.isMissing); endfunction ## -*- texinfo -*- ## @deftypefn {categorical} {@var{B} =} ctranspose (@var{A}) ## ## Transpose a categorical matrix. ## ## @code{@var{B} = ctranspose (@var{A})} is the equivalent of the syntax ## @code{@var{B} = @var{A}'} and returns the transpose of the categorical ## matrix @var{A}. For categorical arrays, @code{ctranspose} is identical ## to @code{transpose}. ## ## @end deftypefn function this = ctranspose (this) if (ndims (this) != 2) error ("categorical.ctranspose: not defined for N-D arrays."); endif this.code = ctranspose (this.code); this.isMissing = ctranspose (this.isMissing); endfunction endmethods ################################################################################ ## ** Reference and Assignment Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'end' 'subsref' 'subsasgn' ## ## ## ################################################################################ methods (Hidden) ## Overload 'end' keyword function last_index = end (this, end_dim, ndim_obj) lastdim = ndims (this); if (end_dim == ndim_obj && ndim_obj == 1) last_index = prod (size (this)); elseif (end_dim == ndim_obj && end_dim < lastdim) last_index = prod (size (this)(end_dim:lastdim)); else last_index = size (this, end_dim); endif endfunction ## Class specific subscripted reference function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case '()' out = this; out.code = this.code(s.subs{:}); out.isMissing = this.isMissing(s.subs{:}); case '{}' error (strcat ("categorical.subsref: '{}' invalid indexing", ... " for referencing values. Use '()' instead.")); case '.' error (strcat ("categorical.subsref: '.' invalid indexing", ... " for referencing field of non-structure array.")); endswitch ## Chained references if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction ## Class specific subscripted assignment function this = subsasgn (this, s, val) if (numel (s) > 1) error ("categorical.subsasgn: chained subscripts not allowed."); endif switch s.type case '()' if (isempty (val) && isa (val, 'double')) this.code(s.subs{:}) = []; this.isMissing(s.subs{:}) = []; return; elseif (isa (val, 'missing')) this.code(s.subs{:}) = 0; this.isMissing(s.subs{:}) = true; return; elseif (iscellstr (val) || ischar (val) || isstring (val)) ## Convert to unprotected unordered categorical array and ## handle it as usual below if (ischar (val)) val = cellstr (val); elseif (isempty (val)) error ("categorical.subsasgn: assignment value has no elements."); endif val = categorical (val); elseif (! isa (val, 'categorical')) error (strcat ("categorical.subsasgn: assignment value must", ... " be a categorical array or text representing", ... " a single or multiple categories.")); endif ## After this point VAL is a categorical array ## If any categorical array is ordinal, all must be if (isordinal (this) && isordinal (val)) ## Check that all categorical arrays have the same categories ## and they are in the same order if (! isequal (categories (this), categories (val))) error (strcat ("categorical.subsasgn: cannot assign value", ... " to ordinal categorical array unless they", ... " have the same ordered set of categories.")); endif this.code(s.subs{:}) = val.code; this.isMissing(s.subs{:}) = val.isMissing; return; elseif (isordinal (this)) error (strcat ("categorical.subsasgn: cannot assign unordered", ... " categorical array to ordinal categorical array.")); elseif (isordinal (val)) error (strcat ("categorical.subsasgn: cannot assign ordinal", ... " categorical array to unordered categorical", ... " array.")); endif ## If protected, no new categories can be assigned if (isprotected (this)) if (! all (ismember (val.cats, this.cats))) error (strcat ("categorical.subsasgn: cannot assign new", ... " categories to protected categorical array.")); endif ## Reorder codes accordingly n_cats = numel (val.cats); idx = cell (2, n_cats); for j = 1:n_cats new_code = find (strcmp (this.cats, val.cats(j))); idx{1,j} = val.code == j; idx{2,j} = new_code; endfor for j = 1:n_cats val.code(idx{1,j}) = idx{2,j}; endfor this.code(s.subs{:}) = val.code; this.isMissing(s.subs{:}) = val.isMissing; return; endif ## No constrains, add the new categories the assigned values use ## and bump code indexing to reflect the changes in category list ## of assigned categorical array n_cats = numel (val.cats); maxcat = numel (this.cats); idx = cell (2, n_cats); newcat = {}; n_code = 0; for j = 1:n_cats new_code = find (strcmp (this.cats, val.cats(j))); if (! isempty (new_code)) idx{1,j} = val.code == j; idx{2,j} = new_code; elseif (! any (val.code(:) == j)) idx{1,j} = false (size (val.code)); idx{2,j} = 0; else idx{1,j} = val.code == j; idx{2,j} = maxcat + n_code + 1; n_code += 1; newcat = [newcat; val.cats(j)]; endif endfor this.cats = [this.cats; newcat]; for j = 1:n_cats val.code(idx{1,j}) = idx{2,j}; endfor this.code(s.subs{:}) = val.code; this.isMissing(s.subs{:}) = val.isMissing; case '{}' error (strcat ("categorical.subsasgn: '{}' invalid indexing", ... " for assigning values. Use '()' instead.")); case '.' error (strcat ("categorical.subsasgn: '.' invalid indexing", ... " for assigning field of non-structure array.")); endswitch endfunction endmethods methods (Access = private) ## Return a subset of the array function this = subset (this, varargin) this = this; this.code = this.code(varargin{:}); this.isMissing = this.isMissing(varargin{:}); endfunction ## Common function for set operations function [C, ixA, ixB] = setop (A, B, fname, varargin) ixA = ixB = []; ## 'legacy' option is not supported if (any (strcmp ("legacy", varargin))) C = sprintf (strcat ("categorical.%s: 'legacy'", ... " option is not supported."), fname); return; endif if (ischar (A)) A = categorical (cellstr (A)); elseif (iscellstr (A) || isa (A, 'string')) A = categorical (A); elseif (ischar (B)) B = categorical (cellstr (B)); elseif (iscellstr (B) || isa (B, 'string')) B = categorical (B); endif if (! isa (A, 'categorical') || ! isa (B, 'categorical')) C = sprintf ("categorical.%s: invalid type input arrays.", fname); return; endif if (xor (A.isOrdinal, B.isOrdinal)) C = sprintf (strcat ("categorical.%s: if A is", ... " ordinal, B must be ordinal."), fname); return; endif C = A; if (A.isOrdinal && B.isOrdinal) if (! isequal (A.cats, B.cats)) C = sprintf (strcat ("categorical.%s: ordinal arrays must", ... " have the same set of categories in", ... " the same order."), fname); return; endif ## For ordinal arrays, operate directly on codes Acodes = double (A); Bcodes = double (B); else ## For unordered arrays, operate on categorical values allcats = unique ([A.cats(:); B.cats(:)]); A_cats_idx = cellfun (@(x) find (ismember (allcats, x)), A.cats); Acodes = nan (size (A.code)); for i = 1:numel (A_cats_idx) Acodes(A.code == i) = A_cats_idx(i); endfor B_cats_idx = cellfun (@(x) find (ismember (allcats, x)), B.cats); Bcodes = nan (size (B.code)); for i = 1:numel (B_cats_idx) Bcodes(B.code == i) = B_cats_idx(i); endfor C.cats = allcats; endif ## Apply set operation switch (fname) case 'intersect' [code, ixA, ixB] = intersect (Acodes, Bcodes, varargin{:}); case 'setdiff' [code, ixA] = setdiff (Acodes, Bcodes, varargin{:}); case 'setxor' [code, ixA, ixB] = setxor (Acodes, Bcodes, varargin{:}); case 'union' [code, ixA, ixB] = union (Acodes, Bcodes, varargin{:}); endswitch ## Add codes and missing arrays C.code = uint16 (code); C.isMissing = C.code == 0; endfunction ## Expand categorical arrays of compatible sizes function [A, B, err] = csize_expand (A, B) err = false; szA = size (A); szB = size (B); if (isscalar (A) && ! isscalar (B)) A = repmat (A, szB); elseif (! isscalar (A) && isscalar (B)) B = repmat (B, szA); elseif (! isequal (szA, szB)) ndA = numel (szA); ndB = numel (szB); if (ndA > ndB) padones = ones (1, ndA - ndB); szB = [szB, padones]; elseif (ndA < ndB) padones = ones (1, ndB - ndA); szA = [szA, padones]; endif ## Check that sizes are compatible must_equal = szA != 1 & szB != 1; if (! isequal (szA(must_equal), szB(must_equal))) err = true; return; endif ## Create expansion vectors dims = max (ndA, ndB); repA = ones (1, dims); repB = ones (1, dims); ## Get dimensions that should be expanded expA = szA == 1 & szB != 1; repA(expA) = szB(expA); expB = szB == 1 & szA != 1; repB(expB) = szA(expB); ## Expand accordingly A = repmat (A, repA); B = repmat (B, repB); endif endfunction endmethods endclassdef ## Promote text arrays to categorical objects function varargout = promote (varargin) for i = 1:numel (varargin) val = varargin{i}; if (isa (val, "categorical")) varargout{i} = val; elseif (iscellstr (val) || isa (val, "string") || ischar (val)) val = cellstr (val); varargout{i} = categorical (val); elseif (isnumeric (val) || islogical (val)) varargout{i} = categorical (val); elseif (isa (val, 'missing')) ## Shape from VAL, not from a fresh 'missing': the latter is always 1x1 ## and silently collapsed every non-scalar missing operand. varargout{i} = categorical (nan (size (val))); else error ("categorical: invalid input to constructor."); endif endfor endfunction ## Custom function for displaying categorical summary function dispcellmatrix (C) sz = terminal_size (); cols = sz(2) - 4; colgap = " "; dispstr = {}; optLens = []; for iCol = 1:size (C, 2) [outstr, optLen] = mixedcell2str (C(:, iCol)); dispstr = [dispstr, outstr]; optLens = [optLens, optLen]; endfor if (sum (optLens + 6) <= cols) # all columns fit in terminal size rowSpat = ""; for iCol = 1:size (C, 2) rowSpat = [rowSpat, sprintf("%%-%ds", optLens(iCol)), colgap]; endfor for iRow = 1:size (C, 1) strrow = sprintf (rowSpat, dispstr{iRow,:}); fprintf (" %s\n", strrow); endfor fprintf ("\n"); else # we need to split rows optLen_cs = cumsum (optLens + 6); startCol = 1; while (! isempty (find (optLen_cs > cols))) stopCol = find (optLen_cs > cols, 1) - 1; rowSpat = ""; for iCol = 1:stopCol rowSpat = [rowSpat, sprintf("%%-%ds", optLens(iCol)), colgap]; endfor optLens(1:iCol) = []; optLen_cs = cumsum (optLens + 6); stopCol = stopCol + startCol - 1; fprintf ("Columns %d through %d:\n\n", startCol, stopCol); for iRow = 1:size (C, 1) strrow = sprintf (rowSpat, dispstr{iRow,[startCol:stopCol]}); fprintf (" %s\n", strrow); endfor fprintf ("\n"); startCol = stopCol + 1; endwhile if (! isempty (optLens)) for iCol = 1:length (optLens) rowSpat = [rowSpat, sprintf("%%-%ds", optLens(iCol)), colgap]; endfor stopCol = startCol + iCol - 1; if (startCol == stopCol) fprintf ("Column %d:\n\n", startCol); else fprintf ("Columns %d through %d:\n\n", startCol, stopCol); endif for iRow = 1:size (C, 1) strrow = sprintf (rowSpat, dispstr{iRow,[startCol:stopCol]}); fprintf (" %s\n", strrow); endfor fprintf ("\n"); endif endif endfunction ## Custom function to convert a mixed cell array to cellstr array function [dispstr, optLen] = mixedcell2str (data) dispstr = cell (size (data)); ## Preallocate indexes to avoid truncation when last elements are 0 is_char = logical (zeros (size (data))); is_numb = is_char; ## Index numeric scalar and character row vector ve = cell2mat (cellfun (@(x) size (x,1), data, "UniformOutput", false)) == 1; ## Catch 'char' row vectors is_char(ve) = cellfun ('ischar', data(ve)); sf = @(x) sprintf ("%s", x); dispstr(is_char) = cellfun (sf, data(is_char), "UniformOutput", false); ## Catch 'numeric' scalars is_numb(ve) = cellfun ('isnumeric', data(ve)); sf = @(x) sprintf ("%d", x); dispstr(is_numb) = cellfun (sf, data(is_numb), "UniformOutput", false); ## Get optimal length optLen = max (cellfun (@length, dispstr)); ## Pad data according to optimal length Ra_wB = sprintf ("%%-%ds", optLen); fcn = @(x) sprintf (Ra_wB, x); dispstr(is_numb) = cellfun (fcn, dispstr(is_numb), "UniformOutput", false); La_wB = sprintf ("%%+%ds", optLen); fcn = @(x) sprintf (La_wB, x); dispstr(is_char) = cellfun (fcn, dispstr(is_char), "UniformOutput", false); endfunction ## 'pkg test' reads the BISTs from the file it is testing, so a class ## whose suite lives in inst/tests/ reads as carrying none. This block ## answers for it and checks nothing itself. %!test %! ## The suite for this class is inst/tests/categorical.m-tst. pr0m1th3as-datatypes-9450b29/inst/cell2table.m000066400000000000000000000202431527370255000210430ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{tbl} =} cell2table (@var{C}) ## @deftypefnx {datatypes} {@var{tbl} =} cell2table (@var{C}, @var{Name}, @var{Value}) ## ## Convert a cell array to a table. ## ## @code{@var{tbl} = cell2table (@var{C})} converts the 2-D cell array @var{C} ## to the table @var{tbl}, where the contents of each column of @var{C} becomes ## a variable in @var{tbl}. The contents of each column are concatenated into ## their common data type (i.e. if a column of @var{C} contains explicitly ## @qcode{double} numbers, then the corresponding variable in @var{tbl} is of ## the same type), otherwise they are added as a column of cells. A column ## holding function handles, @code{dictionary}, @code{timeseries} or ## @code{tscollection} objects always becomes a column of cells. ## ## @code{@var{tbl} = cell2table (@var{C}, @var{Name}, @var{Value})} specifies ## optional parameters for creating the table @var{tbl} with the following ## Name-Value paired arguments. ## ## @multitable @columnfractions 0.23 0.75 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'VariableNames'} @tab A cell array of character vectors or ## a string array defining the variable names of @var{tbl}. The names must be ## valid variable names and unique. ## ## @item @qcode{'RowNames'} @tab A cell array of character vectors or ## a string array defining the row names of @var{tbl}. The names must be unique ## but not necessarily valid variable names. ## ## @item @qcode{'DimensionNames'} @tab A cell array of character vectors or ## a string array defining the dimension names of @var{tbl}. The names must be ## unique and not in conflict with variable names. By default, dimension names ## are @qcode{'Row', 'Variables'}. ## @end multitable ## ## @seealso{array2table, struct2table, table} ## @end deftypefn function tbl = cell2table (C, varargin) ## Check input is a matrix if (ndims (C) > 2) error ("cell2table: input array must be a 2-D cell array."); endif if (! iscell (C)) error ("cell2table: input array must be a cell array."); endif ## Parse optional Name-Value paired arguments optNames = {'VariableNames', 'RowNames', 'DimensionNames'}; dfValues = {{}, {}, {}}; [varNames, rowNames, dimNames, args] = parsePairedArguments ... (optNames, dfValues, varargin); ## Split columns into separate input data arguments for table varN = size (C, 2); varValues = cell (1, varN); for ix = 1:varN tmp = C(:,ix); if (iscellstr (tmp) || any (cellfun (@cellOnly, tmp))) varValues{ix} = tmp; else ## Try to concatenate into array try warning ('off', 'Octave:num-to-str'); newArray = cat (1, tmp{:}); warning ('on', 'Octave:num-to-str'); if (size (newArray, 1) == size (tmp, 1)) varValues{ix} = newArray; continue; else error (); endif catch varValues{ix} = tmp; end_try_catch endif endfor ## Handle variable names if (! isempty (varNames)) if (numel (varNames) != varN) error (strcat ("cell2table: 'VariableNames' must match the", ... " columns in input cell.")); endif else varName = inputname (1); if (isempty (varName)) varName = 'Var'; endif varNames = cell (1, varN); for ix = 1:varN varNames{ix} = sprintf ('%s%d', varName, ix); endfor endif optArgs = {'VariableNames', varNames}; ## Handle remaining paired arguments if (! isempty (rowNames)) if (numel (rowNames) != size (C, 1)) error ("cell2table: 'RowNames' must match the rows in input cell."); endif optArgs = [optArgs {'RowNames', rowNames}]; endif if (! isempty (dimNames)) if (numel (dimNames) != 2) error ("cell2table: 'DimensionNames' must be a two-element vector."); endif optArgs = [optArgs {'DimensionNames', dimNames}]; endif ## Construct table tbl = table (varValues{:}, optArgs{:}); endfunction ## True for a value only a cell variable can hold function tf = cellOnly (x) tf = (is_function_handle (x) || isa (x, 'dictionary') || isa (x, 'timeseries') || isa (x, 'tscollection')); endfunction %!demo %! ## `cell2table` turns a 2-D cell array into a table, one column per variable. %! ## Unlike a matrix, the cell columns may hold different types. %! %! C = {'Sanchez', 38, true; 'Johnson', 43, false; 'Li', 38, true}; %! cell2table (C, 'VariableNames', {'LastName', 'Age', 'Smoker'}) %!test %! C = {1, 2; 3, 4}; %! tbl = cell2table (C); %! assert_equal (tbl.C1, [1; 3]); %! assert_equal (tbl.C2, [2; 4]); %! assert_equal (size (C), size (tbl)); %! assert_equal (isa (tbl.C1, 'double'), true); %!test %! tbl = cell2table ({1, 2; 3, 4}); %! assert_equal (tbl.Var1, [1; 3]); %! assert_equal (tbl.Var2, [2; 4]); %! assert_equal (size (tbl), [2, 2]); %! assert_equal (isa (tbl.Var1, 'double'), true); %!test %! tbl = cell2table ({1, ''; 3, 4}, 'VariableNames', {'A', 'B'}); %! assert_equal (tbl.A, [1; 3]); %! assert_equal (tbl.B, {''; 4}); %! assert_equal (isa (tbl.A, 'double'), true); %! assert_equal (isa (tbl.B, 'double'), false); %!test %! tbl = cell2table ({1, ''; 3, 4}, "RowNames", {'A', 'B'}); %! assert_equal (tbl.Var1, [1; 3]); %! assert_equal (tbl.Var2, {''; 4}); %! assert_equal (tbl.Properties.RowNames, {'A'; 'B'}); %! assert_equal (class (tbl('A', :)), 'table'); %!test %! tbl = cell2table ({1, ''; 3, 4}, string ('DimensionNames'), {'A', 'B'}); %! assert_equal (tbl.Var1, [1; 3]); %! assert_equal (tbl.Var2, {""; 4}); %! assert_equal (tbl.A, {}); %! assert_equal (tbl.B, {1, ''; 3, 4}); %!test %! tbl = cell2table ({1, ''; 3, 4}, "RowNames", {'A', 'B'}, ... %! "DimensionNames", {'A', 'B'}); %! assert_equal (tbl.Var1, [1; 3]); %! assert_equal (tbl.Var2, {''; 4}); %! assert_equal (tbl.A, {'A'; 'B'}); %! assert_equal (tbl.B, {1, ''; 3, 4}); %!test %! tbl = cell2table ({1, string(''); 3, 4}, "RowNames", {'R1', 'R2'}, ... %! "DimensionNames", {'A', 'B'}); %! assert_equal (class (tbl('R1', :)), 'table'); %! assert_equal (class (tbl{'R1', :}), 'string'); %! assert_equal (cellstr (tbl{'R1', :}), {'1', ''}); %! assert_equal (tbl.A, {'R1'; 'R2'}); %! assert_equal (class (tbl.B), 'string'); %! assert_equal (cellstr (tbl.B), {'1', ''; '3', '4'}); %!test # a column of function handles stays a cell variable %! tbl = cell2table ({@sin, 1; @cos, 2}); %! assert_equal (class (tbl.Var1), 'cell'); %! assert_equal (func2str (tbl.Var1{2}), 'cos'); %!test # a column of time series stays a cell variable, never one series %! tbl = cell2table ({timeseries([1; 2]); timeseries([3; 4])}); %! assert_equal (class (tbl.Var1), 'cell'); %! assert_equal (size (tbl.Var1), [2, 1]); %! assert_equal (tbl.Var1{2}.Data, [3; 4]); %!test %! tbl = cell2table ({dictionary(1, 2)}); %! assert_equal (class (tbl.Var1), 'cell'); %!test %! tbl = cell2table ({tscollection([0; 1])}); %! assert_equal (class (tbl.Var1), 'cell'); %!error ... %! cell2table (cell (3, 3, 3)); %!error cell2table ([3; 3; 3]); %!error ... %! cell2table ({1; 2; 3}, 'VariableNames', {'A', 'B'}); %!error ... %! cell2table ({1; 2; 3}, 'RowNames', {'A', 'B'}); %!error ... %! cell2table ({1; 2; 3}, 'DimensionNames', {'A', 'B', 'C'}); pr0m1th3as-datatypes-9450b29/inst/configureDictionary.m000066400000000000000000000256331527370255000230510ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{d} =} configureDictionary (@var{keyType}, @var{valueType}) ## ## Create a dictionary with key and value types and no entries. ## ## @code{@var{d} = configureDictionary (@var{keyType}, @var{valueType})} ## returns a configured @code{dictionary} with no entries, whose keys are of ## class @var{keyType} and whose values are of class @var{valueType}, each ## given as a character vector or a string scalar. The names are matched in ## case, and @qcode{'char'} gives @qcode{'string'}, since a character vector ## is stored as a string scalar. ## ## @var{valueType} may name a built-in type (@qcode{'double'}, ## @qcode{'logical'}, @qcode{'cell'}, @qcode{'struct'}, ## @qcode{'function_handle'}, @dots{}) or any class that is not abstract. ## @var{keyType} must also name a type that @code{keyHash} can hash: a ## built-in type or a class with a @code{keyHash} method, such as ## @code{string}, @code{datetime}, @code{duration}, @code{calendarDuration} ## and @code{categorical}. MATLAB accepts any class there, a @code{table} ## or a @code{timetable} among them, and refuses the key only when an entry ## is added. ## ## @seealso{dictionary, dictionary.types, dictionary.isConfigured, keyHash} ## @end deftypefn function d = configureDictionary (keyType, valueType) if (nargin != 2) print_usage (); endif [keyType, errmsg] = typeName (keyType, 'KEYTYPE'); if (! isempty (errmsg)) error ("configureDictionary: %s", errmsg); endif [valueType, errmsg] = typeName (valueType, 'VALUETYPE'); if (! isempty (errmsg)) error ("configureDictionary: %s", errmsg); endif if (! dictionary.hashable (keyType)) error ("configureDictionary: unsupported key type '%s'.", keyType); endif d = dictionary.configured (keyType, valueType); endfunction ## The class names that are not classdef classes function names = builtinTypes () names = {'double', 'single', 'int8', 'int16', 'int32', 'int64', 'uint8', ... 'uint16', 'uint32', 'uint64', 'logical', 'char', 'cell', ... 'struct', 'function_handle'}; endfunction ## The class named by X, the argument ARGNAME, with 'char' as 'string' function [name, errmsg] = typeName (x, argname) name = ''; errmsg = ''; if (! ((ischar (x) && rows (x) <= 1) || (isa (x, 'string') && isscalar (x)))) errmsg = sprintf ("%s must be a character vector or a string scalar.", ... argname); return; endif name = char (x); if (strcmp (name, 'char')) name = 'string'; elseif (! any (strcmp (name, builtinTypes ()))) mc = meta.class.fromName (name); if (isempty (mc)) errmsg = sprintf ("unknown type '%s'.", name); elseif (mc.Abstract) errmsg = sprintf ("'%s' is an abstract class.", name); endif endif endfunction %!demo %! ## `configureDictionary` creates a dictionary with key and value types and %! ## no entries. Entries added later are converted to those types. %! %! d = configureDictionary ('string', 'double') %! d({'a', 'b'}) = int8 ([1, 2]); %! values (d) %!demo %! ## Without configured types the first entries decide them: here the %! ## values are `int8`. Types named beforehand avoid that, and `'char'` %! ## names the `string` type, since text is stored as strings. %! %! u = dictionary (); %! u('a') = int8 (1); %! [keyType, valueType] = types (u) %! c = configureDictionary ('char', 'datetime'); %! c('start') = '2024-03-01' %!test %! [kt, vt] = types (configureDictionary ('string', 'double')); %! assert_equal (kt, string ('string')); %! assert_equal (vt, string ('double')); %!test %! [kt, vt] = types (configureDictionary (string ('int8'), string ('cell'))); %! assert_equal (kt, string ('int8')); %! assert_equal (vt, string ('cell')); %!test %! [kt, vt] = types (configureDictionary ('char', 'char')); %! assert_equal (kt, string ('string')); %! assert_equal (vt, string ('string')); %!assert_equal (isConfigured (configureDictionary ('double', 'double')), true) %!assert_equal (numEntries (configureDictionary ('double', 'double')), 0) %!test %! [kt, vt] = types (configureDictionary ('logical', 'function_handle')); %! assert_equal (kt, string ('logical')); %! assert_equal (vt, string ('function_handle')); %!test %! [kt, vt] = types (configureDictionary ('struct', 'struct')); %! assert_equal (kt, string ('struct')); %! assert_equal (vt, string ('struct')); %!test %! [kt, vt] = types (configureDictionary ('cell', 'single')); %! assert_equal (kt, string ('cell')); %! assert_equal (vt, string ('single')); %!test %! [kt, vt] = types (configureDictionary ('function_handle', 'uint64')); %! assert_equal (kt, string ('function_handle')); %! assert_equal (vt, string ('uint64')); %!test %! [kt, vt] = types (configureDictionary ('datetime', 'duration')); %! assert_equal (kt, string ('datetime')); %! assert_equal (vt, string ('duration')); %!test %! [kt, vt] = types (configureDictionary ('calendarDuration', 'categorical')); %! assert_equal (kt, string ('calendarDuration')); %! assert_equal (vt, string ('categorical')); %!test %! [kt, vt] = types (configureDictionary ('categorical', 'calendarDuration')); %! assert_equal (kt, string ('categorical')); %! assert_equal (vt, string ('calendarDuration')); %!test %! [kt, vt] = types (configureDictionary ('duration', 'datetime')); %! assert_equal (kt, string ('duration')); %! assert_equal (vt, string ('datetime')); %!test %! [~, vt] = types (configureDictionary ('string', 'table')); %! assert_equal (vt, string ('table')); %!test %! [~, vt] = types (configureDictionary ('string', 'timetable')); %! assert_equal (vt, string ('timetable')); %!test %! [~, vt] = types (configureDictionary ('string', 'dictionary')); %! assert_equal (vt, string ('dictionary')); %!test %! [~, vt] = types (configureDictionary ('string', 'tsdata.event')); %! assert_equal (vt, string ('tsdata.event')); %!test %! [~, vt] = types (configureDictionary ('string', 'missing')); %! assert_equal (vt, string ('missing')); %!error ... %! configureDictionary (5, 'double') %!error ... %! configureDictionary ({'string'}, 'double') %!error ... %! configureDictionary (string ({'string', 'double'}), 'double') %!error ... %! configureDictionary (['ab'; 'cd'], 'double') %!error ... %! configureDictionary ('string', 5) %!error ... %! configureDictionary ('String', 'double') %!error ... %! configureDictionary ('string', 'Double') %!error configureDictionary ('', 'double') %!error configureDictionary ('string', '') %!error ... %! configureDictionary ('string', 'int128') %!error ... %! configureDictionary ('string', 'tabular') %!error ... %! configureDictionary ('tabular', 'double') %!error ... %! configureDictionary ('table', 'double') %!error ... %! configureDictionary ('timetable', 'double') %!error ... %! configureDictionary ('timeseries', 'double') ## Measured on R2024a %!error ... %! configureDictionary ('foo', 'double'); %!error ... %! configureDictionary ('double', 'foo'); %!error ... %! configureDictionary (1, 'double'); %!test %! r = configureDictionary ('char', 'char'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 0); %!test %! r = configureDictionary ('cell', 'cell'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'cell', 'cell'})); %! assert_equal (numEntries (r), 0); %!test %! r = configureDictionary ('struct', 'double'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'struct', 'double'})); %! assert_equal (numEntries (r), 0); %!test %! r = configureDictionary ('function_handle', 'double'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'function_handle', 'double'})); %! assert_equal (numEntries (r), 0); ## D2: the key type must be hashable, where MATLAB refuses only entries. %!error ... %! configureDictionary ('table', 'double'); %!test %! r = configureDictionary ('double', 'table'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'table'})); %! assert_equal (numEntries (r), 0); %!test %! r = configureDictionary ('missing', 'missing'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'missing', 'missing'})); %! assert_equal (numEntries (r), 0); %!test %! r = configureDictionary ('dictionary', 'dictionary'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'dictionary', 'dictionary'})); %! assert_equal (numEntries (r), 0); %!test %! r = configureDictionary (string ('double'), string ('double')); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 0); %!error ... %! configureDictionary ('Double', 'double'); %!test %! r = configureDictionary ('double', 'struct'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'struct'})); %! assert_equal (numEntries (r), 0); %!test %! r = configureDictionary ('datetime', 'duration'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'datetime', 'duration'})); %! assert_equal (numEntries (r), 0); %!test %! r = configureDictionary ('categorical', 'calendarDuration'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'categorical', 'calendarDuration'})); %! assert_equal (numEntries (r), 0); pr0m1th3as-datatypes-9450b29/inst/convertCharsToStrings.m000066400000000000000000000054041527370255000233520ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{B} =} convertCharsToStrings (@var{A}) ## @deftypefnx {datatypes} {[@var{B1}, @dots{}, @var{Bn}] =} convertCharsToStrings (@var{A1}, @dots{}, @var{An}) ## ## Convert character arrays to a string arrays, where applicable. ## ## @code{@var{B} = convertCharsToStrings (@var{A})} converts @var{A} to a string ## scalar, if @var{A} is a character vector or array, or to a string array, if ## @var{A} is a cell array of character vectors. Otherwise, @var{A} is returned ## unaltered. ## ## @code{[@var{B1}, @dots{}, @var{Bn}] = convertCharsToStrings (@var{A1}, ## @dots{}, @var{An})} converts any of the input arguments that are either ## character arrays or cell arrays of character vectors to string scalars or ## string arrays, respectively. Otherwise, @code{convertCharsToStrings} returns ## the input arguments unaltered. ## ## @seealso{convertStringsToChars, string} ## @end deftypefn function varargout = convertCharsToStrings (varargin) for i = 1:numel (varargin) if (ischar (varargin{i})) varargout{i} = string (varargin{i}(:)'); elseif (iscellstr (varargin{i})) varargout{i} = string (varargin{i}); else varargout{i} = varargin{i}; endif endfor endfunction %!test %! A = 'text'; %! B = [1, 2, 3]; %! C = string ("test"); %! [A1, B1, C1] = convertCharsToStrings (A, B, C); %! assert_equal (isstring (A1), true); %! assert_equal (B, B1); %! assert_equal (C == C1, true); %!test %! A = {'asd', 'ert'; 'xcv', 'dfg'}; %! A1 = convertCharsToStrings (A); %! assert_equal (isequal (size (A), size (A1)), true); %! assert_equal (isstring (A1), true); %!test %! A = categorical (NaN); %! B = duration; %! C = 'text'; %! [A1, B1, C1] = convertCharsToStrings (A, B, C); %! assert_equal (iscategorical (A1), true); %! assert_equal (isduration (B1), true); %! assert_equal (isstring (C1), true); %!test %! A = ['e','r';'v','b']; %! A1 = convertCharsToStrings (A); %! assert_equal (isscalar (A1), true); %! assert_equal (strcmp (char (A1), 'evrb'), true); pr0m1th3as-datatypes-9450b29/inst/convertStringsToChars.m000066400000000000000000000050301527370255000233450ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{B} =} convertStringsToChars (@var{A}) ## @deftypefnx {datatypes} {[@var{B1}, @dots{}, @var{Bn}] =} convertStringsToChars (@var{A1}, @dots{}, @var{An}) ## ## Convert string arrays to a character arrays, where applicable. ## ## @code{@var{B} = convertStringsToChars (@var{A})} converts @var{A} to a ## character vector, if @var{A} is a string scalar, or to a cell array of ## character vectors, if @var{A} is a string array. Otherwise, @var{A} is ## returned unaltered. ## ## @code{[@var{B1}, @dots{}, @var{Bn}] = convertStringsToChars (@var{A1}, ## @dots{}, @var{An})} converts any of the input arguments that are of string ## type to character vectors or to cell array of character vectors, or leaves ## them unaltered. ## ## @seealso{convertCharsToStrings, string} ## @end deftypefn function varargout = convertStringsToChars (varargin) for i = 1:numel (varargin) if (isa (varargin{i}, 'string')) if (isscalar (varargin{i})) varargout{i} = char (varargin{i}); else varargout{i} = cellstr (varargin{i}); endif else varargout{i} = varargin{i}; endif endfor endfunction %!test %! A = 'text'; %! B = [1, 2, 3]; %! C = string ("test"); %! [A1, B1, C1] = convertStringsToChars (A, B, C); %! assert_equal (A, A1); %! assert_equal (B, B1); %! assert_equal (ischar (C1), true); %!test %! A = string ({'asd', 'ert'; 'xcv', 'dfg'}); %! A1 = convertStringsToChars (A); %! assert_equal (isequal (size (A), size (A1)), true); %! assert_equal (iscellstr (A1), true); %!test %! A = categorical (NaN); %! B = duration; %! C = 'text'; %! [A1, B1, C1] = convertStringsToChars (A, B, C); %! assert_equal (iscategorical (A1), true); %! assert_equal (isduration (B1), true); %! assert_equal (ischar (C1), true); pr0m1th3as-datatypes-9450b29/inst/csv2table.m000066400000000000000000001214351527370255000207240ustar00rootroot00000000000000## Copyright (C) 2025-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{tbl} =} csv2table (@var{filename}) ## @deftypefnx {datatypes} {@var{tbl} =} csv2table (@var{filename}, @var{Name}, @var{Value}) ## ## Load a CSV file into a table. ## ## @code{@var{tbl} = csv2table (@var{filename})} creates a table @var{tbl} by ## reading the data in CSV file specified by @var{filename}, which can be either ## character vector or a string scalar. If the CSV file was saved by the ## @code{table2csv} method, then it reads the custom comment line in the first ## element of the CSV file and reconstructs the table as specified including ## variable types, variable names, and possibly any nested tables or structures ## it may contain. If no such special comment is found, then it treats the CSV ## file as simple columnar data and loads with the following default options. ## ## @enumerate ## @item The first column is considered as @qcode{RowNames} and it is converted ## to cell array of character vectors and it is subsequently removed from the ## remaining data contained in the CSV file. To change the default behavior, ## you need to specify the @qcode{ReadRowNames} and @qcode{RowNamesColumn} ## paired arguments accordingly. ## @item The first line is treated as a header containing the variable names of ## the table. Any numeric values in the header line are converted to character ## vectors and all variable names are automatically modified to valid Octave ## variable names. To change the default behavior, you need to specify the ## @qcode{ReadVariableNames}, @qcode{VariableNamingRule}, and ## @qcode{VariableNamesRow} paired arguments accordingly. ## @item The data type of each column in the remaining data is automatically ## detected according to its contents. Consequently, text is converted to ## character vectors, datetime and duration strings are converted to datetime ## and duration arrays, respectively, and hexadecimal strings are converted to ## the smallest integer type that can represent all variable values. To change ## the default behavior, you need to specify the @qcode{TextType}, ## @qcode{DatetimeType}, @qcode{DurationType}, and @qcode{HexType} accordingly. ## @end enumerate ## ## @code{@var{tbl} = csv2table (@var{filename}, @var{Name}, @var{Value})} ## specifies optional parameters for creating the table @var{tbl} with the ## following Name-Value paired arguments. ## ## @multitable @columnfractions 0.33 0.65 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'NumHeaderLines'} @tab A positive integer scalar value ## specifying the number of rows to omit reading from the CSV file. If the CSV ## contains the custom comment line in its first element, then the ## @qcode{'NumHeaderLines'} applies to the top number of rows to remove from the ## created table. If CSV is a generic case, then @qcode{'NumHeaderLines'} ## specifies the number of lines to omit parsing from the CSV file itself. ## ## @item @qcode{'VariableNames'} @tab A cell array of character vectors or ## a string array specifying the names of the variables of the created table. ## The names must be unique but not necessarily valid variable names. If not ## empty, it overrides any variable names extracted from the CSV file. This ## applies both to custom and generic CSV files. ## ## @item @qcode{'ReadVariableNames'} @tab A logical scalar specifying ## whether to parse or not the CSV files for variable names. If your CSV file ## only contains data, set @qcode{'ReadVariableNames'} to @qcode{false} so that ## @code{csv2table} parses all lines as data rows in the returned table. ## Variable names will be automatically generated to the default style as done ## by @code{table}, unless otherwise specified by the @qcode{'VariableNames'} ## paired argument. The default value is @qcode{true}. ## ## @item @qcode{'VariableNamingRule'} @tab A character vector or a string ## scalar specifying whether the variable names should be modified to valid ## Octave variable names or the original names should be preserved. Valid ## options are @qcode{"modify"} and @qcode{"preserve"}. By default, ## @code{csv2table} modifies the parsed variable names. ## ## @item @qcode{'VariableNamesRow'} @tab A nonnegative integer scalar ## value specifying the line number in the CSV file, which should be parsed for ## variable names. This only applies if the @qcode{'ReadVariableNames'} option ## is @qcode{true}. The specified line is subsequently removed from the ## remaining data contained in the CSV file. If @qcode{'VariableNamesRow'} is ## set to zero, then it is equivalent to setting @qcode{'ReadVariableNames'} to ## @qcode{false}. ## ## @item @qcode{'VariableNamesLine'} @tab MATLAB's spelling of ## @qcode{'VariableNamesRow'} for a text file, and an exact alias of it. ## Passing both names raises an error rather than one silently winning. ## ## @item @qcode{'VariableTypes'} @tab A cell array of character vectors or ## a string array specifying the data type of the variables of the created ## table. The number of elements must much the number of variable in the table. ## This optional argument only has an effect on generic CSV files. When ## specified, it overrides any other data type specification or automatic ## detection by the @code{csv2table} function. ## ## @item @qcode{'VariableUnitsLine'} @tab A nonnegative integer scalar ## value specifying the line number in the CSV file, which should be parsed for ## variable units. The specified line is subsequently removed from the ## remaining data contained in the CSV file. ## ## @item @qcode{'VariableDescriptionsLine'} @tab A nonnegative integer ## scalar value specifying the line number in the CSV file, which should be ## parsed for variable descriptions. The specified line is subsequently removed ## from the remaining data contained in the CSV file. ## ## @item @qcode{'ReadRowNames'} @tab A logical scalar specifying whether to ## parse or not the CSV files for row names. If your CSV file only contains ## data, set @qcode{'ReadRowNames'} to @qcode{false} so that @code{csv2table} ## parses all columns as data columms in the returned table. The default value ## is @qcode{true}. ## ## @item @qcode{'RowNamesColumn'} @tab A nonnegative integer scalar value ## specifying the column number in the CSV file, which should be parsed for ## row names. This only applies if the @qcode{'ReadRowNames'} option is ## @qcode{true}. The specified column is subsequently removed from the ## remaining data contained in the CSV file. If @qcode{'RowNamesColumn'} is set ## to zero, then it is equivalent to setting @qcode{'ReadRowNames'} to ## @qcode{false}. Note that the values in the column specified by ## @qcode{'RowNamesColumn'} must be unique, otherwise @code{csv2table} will ## return an error. The default value is @qcode{0}: a file that does not say ## which column holds its row names is read as data throughout, and a file ## written by @code{table2csv} records the column itself. A leading column ## headed @qcode{Row}, which is what @code{writetable} writes for the row ## names, is taken as the row names without being named here. ## ## @item @qcode{'TextType'} @tab A character vector or a string scalar ## specifying whether the text data in the CSV file should be stored in the ## table as character vectors or string arrays. Valid options are ## @qcode{"char"} and @qcode{"string"}. By default, @code{csv2table} stores ## text data as character vectors. ## ## @item @qcode{'DatetimeType'} @tab A character vector or a string scalar ## specifying whether the datetime strings found in the CSV file should be ## stored in the table as datetime arrays or as text data. Valid options are ## @qcode{"datetime"} and @qcode{"text"}. By default, @code{csv2table} stores ## datetime strings as datetime arrays. If @qcode{"text"} is specified, then ## the data type depends on the @qcode{'TextType'} option. ## ## @item @qcode{'DurationType'} @tab A character vector or a string scalar ## specifying whether the duration strings found in the CSV file should be ## stored in the table as duration arrays or as text data. Valid options are ## @qcode{"duration"} and @qcode{"text"}. By default, @code{csv2table} stores ## duration strings as duration arrays. If @qcode{"text"} is specified, then ## the data type depends on the @qcode{'TextType'} option. ## ## @item @qcode{'HexType'} @tab A character vector or a string scalar specifying ## whether the hexadecimal text found in the CSV file should be stored as a ## suitable integer type, @qcode{"auto"}, as unaltered input text, ## @qcode{"text"} (the default, in which case the data type depends on the ## @qcode{'TextType'} option), or as any of the integer types supported by ## Octave. Valid options are @qcode{"auto"}, @qcode{"text"}, @qcode{"int8"}, ## @qcode{"int16"}, @qcode{"int32"}, @qcode{"int64"}, @qcode{"uint8"}, ## @qcode{"uint16"}, @qcode{"uint32"}, and @qcode{"uint64"}. Detection is off ## by default because MATLAB does not perform it, so a file carrying no header ## of ours is read the way MATLAB reads it. It never applies to a file written ## by @code{table2csv}, which declares every variable's type. ## @end multitable ## ## A @code{datetime} or @code{duration} variable written by @code{table2csv} is ## restored exactly, along with its @qcode{Format} and, for a zone-aware ## @code{datetime}, its @qcode{TimeZone}. Where the column's type is declared, ## either by the file or by @qcode{"VariableTypes"}, a field that is empty and ## unquoted carries no value and is read as a missing entry, where an empty ## quoted field (@qcode{""}) is read as an empty string; a column whose type is ## guessed is unaffected. The following round-trip limitation applies when ## reading a file written by @code{table2csv}: @code{calendarDuration} and ## @code{categorical} variables are returned as cell arrays of character vectors ## and their values are not reconstructed. ## ## @seealso{array2table, struct2table, table} ## @end deftypefn function tbl = csv2table (name, varargin) if (mod (numel (varargin), 2) != 0) error ("csv2table: name-value arguments must be in pairs."); endif ## The options are read before the file, both because the field delimiter is ## needed to read it and because whether the names and row names are wanted ## decides how its header block is used. optNames = {'NumHeaderLines', 'VariableNames', 'ReadVariableNames', ... 'VariableNamingRule', 'VariableNamesRow', 'VariableTypes', ... 'VariableUnitsLine', 'VariableDescriptionsLine', ... 'ReadRowNames', 'RowNamesColumn', 'TextType', ... 'DatetimeType', 'DurationType', 'HexType', 'Delimiter', ... 'VariableNamesLine'}; ## A file that carries no header of ours is read the way MATLAB reads it, so ## hexadecimal detection, which MATLAB does not perform, is off unless it is ## asked for. It applies only to such a file: a file with a vartype header ## declares every type. dfValues = {0, {}, true, 'modify', [], {}, 0, 0, true, 0, 'char', ... 'datetime', 'duration', 'text', ',', []}; [numHeaderLines, varNames, readVarNames, varNamingRule, varNamesRow, ... varTypes, varUnitsLine, varDescrLine, readRowNames, rowNamesColumn, ... textType, datetimeType, durationTypes, hexType, delim, ... varNamesLine, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error ("csv2table: invalid optional paired argument."); endif varNamesRow = __namesrow__ (varNamesRow, varNamesLine, 'csv2table'); if (isa (delim, 'string')) delim = char (delim); endif if (! (ischar (delim) && isscalar (delim))) error ("csv2table: 'Delimiter' must be a single character."); endif if (! (islogical (readVarNames) && isscalar (readVarNames))) error ("csv2table: 'ReadVariableNames' must be a logical scalar."); endif if (! (islogical (readRowNames) && isscalar (readRowNames))) error ("csv2table: 'ReadRowNames' must be a logical scalar."); endif if (! (isnumeric (rowNamesColumn) && isscalar (rowNamesColumn) && rowNamesColumn == fix (rowNamesColumn) && rowNamesColumn >= 0)) error (strcat ("csv2table: 'RowNamesColumn' must be a non-negative", ... " integer.")); endif ## Check first input is a character vector or a string scalar if (ischar (name) && isvector (name)) C = __csv2table__ (name, delim); elseif (isa (name, 'string') && isscalar (name)) C = __csv2table__ (char (name), delim); else error ("csv2table: NAME must be a character vector or a string scalar."); endif if (ischar (C)) error ("csv2table: %s", C); endif ## Get first comment line (as saved by 'table2csv' method). A generic CSV ## may carry a number in its first cell, so only attempt to split a string. if (ischar (C{1,1})) H_key = strsplit (C{1,1}); else H_key = {}; endif if (numel (H_key) == 13) H_txt = strjoin (H_key([1,2,4,5,7,8,10,11,13])); else H_txt = ''; endif txt = '# varTypes rows; varNames rows; varDescriptions rows; varUnits rows.'; if (strcmpi (H_txt, txt)) Trows = str2num (H_key{3}); Nrows = str2num (H_key{6}); Drows = str2num (H_key{9}); Urows = str2num (H_key{12}); C(1,:) = []; # remove comment line else Trows = 0; Nrows = 0; Drows = 0; Urows = 0; endif ## Handle CSV file with vartype header (as saved by table2csv method) if (Trows > 0) Hrows = Trows + Nrows + Drows + Urows; ## A leading column of row times is tagged 'RowTimes' ahead of its own ## type. A table has no row times, so the column is kept as an ordinary ## leading variable under the row dimension name written beside it, which ## is what MATLAB's 'readtable' does with a file holding a timetable; ## 'csv2timetable' reads the same file as the timetable it came from. if (strncmp (C{1,1}, 'RowTimes|', 9)) C{1,1} = C{1,1}(10:end); endif ## Check for RowNames. The column is consumed either way and kept only ## when the caller asked for it. RowNames = {}; if (strcmp (C{1,1}, 'RowNames')) if (readRowNames) RowNames = C([Hrows+1:end],1); endif C(:,1) = []; # remove row names endif ## Split cell into headers and data T = C(1:Trows,:); # variable types C(1:Trows,:) = []; N = C(1:Nrows,:); # variable names C(1:Nrows,:) = []; if (Drows) D = C(1:Drows,:); # variable descriptions C(1:Drows,:) = []; else D = {}; endif if (Urows) U = C(1:Urows,:); # variable units C(1:Urows,:) = []; else U = {}; endif ## After this point C contains only data ## Construct table. Without a name block there is nothing to group the ## columns by, so each column is one variable, numbered, and carries the ## innermost type it was written with; a nested table cannot be rebuilt ## without the names that tagged its columns. if (Nrows == 0 || ! readVarNames) ncol = size (T, 2); names = arrayfun (@(c) sprintf ("Var%d", c), 1:ncol, ... 'UniformOutput', false); vals = cell (1, ncol); for c = 1:ncol col = T(:,c); col = col(! cellfun (@isempty, col)); vals{c} = cell2var (C(:,c), col{end}); endfor if (isempty (RowNames)) tbl = table (vals{:}, 'VariableNames', names); else tbl = table (vals{:}, 'VariableNames', names, 'RowNames', RowNames); endif else tbl = __cell2tbl__ (C, T, N, D, U, RowNames, ... @(varC, varA, typestr) cell2var (varC, typestr)); endif endif ## Apply the remaining options to a file that carried a vartype header if (Trows > 0) if (numHeaderLines) tbl(1:numHeaderLines,:) = []; endif if (! isempty (varNames)) tbl = tbl(:,varNames); endif return endif ## After this point handle generic CSV file ## Remove unnecessary header lines if (numHeaderLines) C(1:numHeaderLines,:) = []; endif ## A leading column headed with the row-labels dimension name is what ## 'writetable' writes for the row names, so the header declares the column ## rather than the caller having to guess at it. if (readRowNames && ! rowNamesColumn && varNamesRow > 0 && size (C, 1) >= varNamesRow && ischar (C{varNamesRow,1}) && strcmp (C{varNamesRow,1}, 'Row')) rowNamesColumn = 1; endif ## Read row names has_RowNames = false; if (readRowNames && rowNamesColumn) has_RowNames = true; RowNames = C(:,rowNamesColumn); ## Force RowNames into cellstr if (! iscellstr (RowNames)) RowNames = cellstr (string (RowNames)); endif ## Remove RowNames column from data C(:,rowNamesColumn) = []; endif ## Read variable names, descriptions, and units from their respective lines. ## All line numbers refer to the same frame, so the values are read first and ## the consumed header rows are removed together below -- removing them one at ## a time would shift the indices of the lines still to be read. cols = size (C, 2); if (readVarNames && varNamesRow) N = C(varNamesRow,:); isnum = cellfun ('isnumeric', N); N(isnum) = cellfun ('num2str', N(isnum), "UniformOutput", false); ## Force to valid variable names if (strcmpi (varNamingRule, 'modify')) N = matlab.lang.makeValidName (N); endif else # Generate default variable names N = arrayfun (@(x) sprintf ("Var%d", x), [1:cols], "UniformOutput", false); endif ## Override variable names with user supplied if (! isempty (varNames)) N = varNames; endif ## A table refuses a variable named after one of its dimensions, so a column ## of a foreign file headed 'Row' takes a suffix rather than being refused. N = __undimname__ (N, {'Row', 'Variables'}); ## Read variable descriptions and units if (varDescrLine) D = C(varDescrLine,:); ## Force variable descriptions into cellstr if (! iscellstr (D)) D = cellstr (string (D)); endif endif if (varUnitsLine) U = C(varUnitsLine,:); ## Force variable units into cellstr if (! iscellstr (U)) U = cellstr (string (U)); endif endif ## Remove all consumed header rows from data and RowNames at once droprows = []; if (readVarNames && varNamesRow) droprows = [droprows, varNamesRow]; endif if (varDescrLine) droprows = [droprows, varDescrLine]; endif if (varUnitsLine) droprows = [droprows, varUnitsLine]; endif droprows = unique (droprows); if (! isempty (droprows)) C(droprows,:) = []; if (has_RowNames) RowNames(droprows,:) = []; endif endif ## Check that RowNames are unique if (has_RowNames) if (numel (unique (RowNames)) != numel (RowNames)) error ("csv2table: 'RowNames' must be unique."); endif endif ## Parse columns into variables with user defined variable types varValues = cell (1, cols); if (! isempty (varTypes)) if (numel (varTypes) != cols) error ("csv2table: 'VariableTypes' do not match columns in CSV."); endif for ix = 1:cols varValues{ix} = cell2var (C(:,ix), varTypes{ix}); endfor ## Parse columns into variables by identifying the variable type else for ix = 1:cols varValues{ix} = cell2auto (C(:,ix), textType, datetimeType, ... durationTypes, hexType); endfor endif ## Create table if (has_RowNames) tbl = table (varValues{:}, 'VariableNames', N, 'RowNames', RowNames); else tbl = table (varValues{:}, 'VariableNames', N); endif ## Add variable descriptions and units if (varDescrLine) tbl.Properties.VariableDescriptions = D; endif if (varUnitsLine) tbl.Properties.VariableUnits = U; endif endfunction function varValue = cell2var (C, T) ## Numeric vartypes numvartype = {'double', 'single', 'int8', 'uint8', 'int16', ... 'uint16', 'int32', 'uint32', 'int64', 'uint64'}; ## A field that was empty and unquoted holds no value: the reader returns [] ## for it, where an empty quoted field ("") returns an empty character ## vector. With the column's type declared, the first is a missing entry. blank = cellfun (@(x) isnumeric (x) && isempty (x), C); if (strcmp (T, "cell")) varValue = C; elseif (strcmp (T, "char")) C(blank) = {''}; varValue = char (C); elseif (strcmp (T, "missing")) varValue = repmat (missing, size (C, 1), size (C, 2)); elseif (strcmp (T, "logical")) varValue = logical (cell2mat (C)); elseif (ismember (T, numvartype)) C(blank) = {NaN}; ## Cast each element to the declared type directly. Large integers may ## have been read as int64/uint64 (to avoid double precision loss); casting ## the whole cell2mat result would let a mixed signed/unsigned column be ## promoted to a type that drops some values, so cast element-wise instead. if (all (cellfun (@(x) isa (x, 'double'), C(:)))) varValue = cast (cell2mat (C), T); else varValue = cellfun (@(x) cast (x, T), C); endif elseif (strcmp (T, "calendarDuration")) warning ("csv2table: 'calendarDuration' strings are not converted."); varValue = C; elseif (strcmp (T, "categorical")) warning ("csv2table: 'categorical' strings are not converted."); varValue = C; elseif (strncmp (T, "datetime", 8)) ## A zone-aware datetime carries its TimeZone after 'datetime ' and its ## display format after a '|'; the date strings are the wall-clock time in ## that zone. [T, dispfmt] = __typefmt__ (T); tz = ''; tzargs = {}; if (numel (T) > 9) tz = T(10:end); tzargs = {'TimeZone', tz}; endif ## 'table2csv' writes ISO 8601; a file written by another program carries ## whatever date strings it chose, which are parsed as they always were. [varValue, isISO] = __iso2dt__ (C, tz); if (! isISO) ## A 'NaT' token cannot be parsed alongside date strings (the format ## cannot be inferred), so reconstruct missing entries explicitly. This ## mirrors how a 'NaN' token round-trips for a duration variable. isNaT = strcmp (strtrim (C), 'NaT'); if (any (isNaT(:))) varValue = NaT (size (C, 1), size (C, 2), tzargs{:}); if (! all (isNaT(:))) varValue(! isNaT) = datetime (C(! isNaT), tzargs{:}); endif else varValue = datetime (C, tzargs{:}); endif endif if (! isempty (dispfmt)) varValue.Format = dispfmt; endif elseif (strncmp (T, "duration", 8)) [~, dispfmt] = __typefmt__ (T); [varValue, isISO] = __iso2dur__ (C); if (! isISO) varValue = duration (C); endif if (! isempty (dispfmt)) varValue.Format = dispfmt; endif elseif (strcmp (T, "string")) C(blank) = {''}; varValue = string (C); varValue(blank) = string (missing); endif endfunction function varValue = cell2auto (C, textType, datetimeType, durationTypes, ... hexType) ## Index empty cells idx = cellfun ('isempty', C); ## Numeric columns are returned as doubles, unless they hold large integers ## that were read as int64/uint64 to avoid double precision loss. if (all (cellfun ('isnumeric', C))) if (all (cellfun (@(x) isa (x, 'double'), C))) varValue = cell2mat (C); elseif (any (cellfun (@(x) isa (x, 'int64'), C))) ## a negative large integer present varValue = cellfun (@(x) int64 (x), C); else varValue = cellfun (@(x) uint64 (x), C); endif elseif (all (cellfun ('isnumeric', C(! idx)))) C(idx) = NaN; varValue = cell2mat (C); ## Mixed cells are forced to text elseif (! iscellstr (C)) if (strcmpi (textType, 'char')) varValue = cellstr (C); else varValue = string (C); endif else # cellstr ## Each detection is attempted in turn and stops at the first success, so a ## later attempt never clobbers an already-recognised datetime or duration. is_datetime = false; is_duration = false; is_hex = false; ## Check for datetime strings. The RFC 9557 form 'writetimetable' uses ## for a zoned datetime carries the zone name in brackets, which no ## ordinary parse accepts and which a duration parse would otherwise ## claim, so it is tested for first. ## A bare clock with no date is a duration, not a time of day today. It ## is what a duration is written as, by this package and by MATLAB alike, ## and both read it back as one, so the datetime parse must not claim it ## first. isclock = all (cellfun (@(x) ! isempty (regexp (x, ... '^\s*-?\d+:\d{2}(:\d{2}(\.\d+)?)?\s*$', 'once')), ... C(! idx))); if (strcmpi (datetimeType, 'datetime') && ! isclock) [dtv, okv] = __rfc95572dt__ (C); if (okv) varValue = dtv; is_datetime = true; else try varValue = datetime (C); is_datetime = true; catch is_datetime = false; end_try_catch endif endif ## Check for duration strings if (! is_datetime && strcmpi (durationTypes, 'duration')) try varValue = duration (C); is_duration = true; catch is_duration = false; end_try_catch endif ## Check for hexadecimal strings (convert to integers) if (! is_datetime && ! is_duration && ! strcmpi (hexType, 'text')) hexval = hex2dec (C); if (any (isnan (hexval))) is_hex = false; else is_hex = true; varValue = hexval; ## Detect smallest integer type or cast user defined if (strcmpi (hexType, 'auto')) minval = min (varValue); maxval = max (varValue); if (minval < 0) # signed integers if (minval >= intmin ('int8') && maxval <= intmax ('int8')) varValue = cast (varValue, 'int8'); elseif (minval >= intmin ('int16') && maxval <= intmax ('int16')) varValue = cast (varValue, 'int16'); elseif (minval >= intmin ('int32') && maxval <= intmax ('int32')) varValue = cast (varValue, 'int32'); elseif (minval >= intmin ('int64') && maxval <= intmax ('int64')) varValue = cast (varValue, 'int64'); endif else # unsigned integers if (maxval <= intmax ('uint8')) varValue = cast (varValue, 'uint8'); elseif (maxval <= intmax ('uint16')) varValue = cast (varValue, 'uint16'); elseif (maxval <= intmax ('uint32')) varValue = cast (varValue, 'uint32'); elseif (maxval <= intmax ('uint64')) varValue = cast (varValue, 'uint64'); endif endif else varValue = cast (varValue, hexType); endif endif endif ## Leave it unaltered as cellstr or convert to strings if (! is_datetime && ! is_duration && ! is_hex) varValue = C; if (strcmpi (textType, 'string')) varValue = string (varValue); endif endif endif endfunction %!demo %! ## `csv2table` reads a CSV back into a table. When the file was written by %! ## `table2csv`, the typed header lets it restore each variable's type %! ## exactly. %! %! T = table ([38; 43], [71.5; 69.0], 'VariableNames', {'Age', 'Height'}); %! filename = fullfile (tempdir (), 'patients.csv'); %! table2csv (T, filename); %! csv2table (filename) %!demo %! ## For a plain CSV that has no type header, the first line is taken as the %! ## variable names and each column's type is detected automatically. Use %! ## `'ReadRowNames'` to turn the first column into row names instead of data. %! %! filename = fullfile (tempdir (), 'plain.csv'); %! fid = fopen (filename, 'w'); %! fputs (fid, "Patient,Age,Height\nLi,38,64\nDiaz,40,67\n"); %! fclose (fid); %! csv2table (filename, 'ReadRowNames', true) %! %! delete (filename); ## Test user-defined 'VariableTypes' are applied per column ## Hexadecimal text is left alone unless the detection is asked for %!test %! fn = [tempname() '.csv']; %! unwind_protect %! fid = fopen (fn, 'w'); %! fputs (fid, "h\nFF\n0A\n"); %! fclose (fid); %! t = csv2table (fn); %! assert_equal (class (t.h), 'cell'); %! assert_equal (t.h, {'FF'; '0A'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A column named after a dimension is kept under a suffixed name %!test %! fn = [tempname() '.csv']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! T.Properties.RowNames = {'r1', 'r2'}; %! unwind_protect %! writetable (T, fn, 'WriteRowNames', true); %! R = csv2table (fn, 'ReadRowNames', false); %! assert_equal (R.Properties.VariableNames, {'Row_1', 'n', 's'}); %! assert_equal (R.Row_1, {'r1'; 'r2'}); %! assert_equal (R.Properties.RowNames, {}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A file written by 'writetable' round-trips, names and values %!test %! fn = [tempname() '.csv']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! unwind_protect %! writetable (T, fn); %! R = csv2table (fn); %! assert_equal (isequaln (R, T), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A 'writetable' file keeps its row names, the header naming the column %!test %! fn = [tempname() '.csv']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! T.Properties.RowNames = {'r1', 'r2'}; %! unwind_protect %! writetable (T, fn, 'WriteRowNames', true); %! R = csv2table (fn); %! assert_equal (isequaln (R, T), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## 'VariableNamesLine' is MATLAB's spelling of the same option %!test %! fn = [tempname() '.csv']; %! unwind_protect %! fid = fopen (fn, 'w'); %! fputs (fid, "skip,me\nn,s\n1,a\n2,b\n"); %! fclose (fid); %! R = csv2table (fn, 'VariableNamesLine', 2); %! assert_equal (R.Properties.VariableNames, {'n', 's'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!error ... %! csv2table ([tempname() '.csv'], 'VariableNamesRow', 1, ... %! 'VariableNamesLine', 1); ## 'VariableNamesRow' names the line of a plain CSV that holds the names %!test %! fn = [tempname() '.csv']; %! unwind_protect %! fid = fopen (fn, 'w'); %! fputs (fid, "skip,me\nn,s\n1,a\n2,b\n"); %! fclose (fid); %! R = csv2table (fn, 'VariableNamesRow', 2); %! assert_equal (R.Properties.VariableNames, {'n', 's'}); %! assert_equal (height (R), 3); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## 'RowNamesColumn' names the column of a plain CSV that holds the row names %!test %! fn = [tempname() '.csv']; %! unwind_protect %! fid = fopen (fn, 'w'); %! fputs (fid, "n,s\n1,a\n2,b\n"); %! fclose (fid); %! R = csv2table (fn, 'RowNamesColumn', 2); %! assert_equal (R.Properties.RowNames, {'a'; 'b'}); %! assert_equal (R.Properties.VariableNames, {'n'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A plain CSV keeps every column when no row-names column is named %!test %! fn = [tempname() '.csv']; %! unwind_protect %! fid = fopen (fn, 'w'); %! fputs (fid, "n,s\n1,a\n2,b\n"); %! fclose (fid); %! R = csv2table (fn); %! assert_equal (R.Properties.VariableNames, {'n', 's'}); %! assert_equal (R.Properties.RowNames, {}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!test %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "A,B,C\n1,2.5,10\n3,4.5,20\n"); %! fclose (fid); %! unwind_protect %! t = csv2table (fn, 'ReadRowNames', false, ... %! 'VariableTypes', {'double', 'single', 'int32'}); %! assert_equal (class (t{:, 1}), 'double'); %! assert_equal (class (t{:, 2}), 'single'); %! assert_equal (class (t{:, 3}), 'int32'); %! assert_equal (t{:, 3}, int32 ([10; 20])); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!error ... %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "A,B,C\n1,2,3\n"); %! fclose (fid); %! unwind_protect %! csv2table (fn, 'ReadRowNames', false, 'VariableTypes', {'double', ... %! 'double'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Test a generic CSV (no vartype header) reads names and auto-detects types %!test %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "A,B,C\n1,2.5,foo\n3,4.5,bar\n"); %! fclose (fid); %! unwind_protect %! t = csv2table (fn, 'ReadRowNames', false); %! assert_equal (t.Properties.VariableNames, {'A', 'B', 'C'}); %! assert_equal (t.A, [1; 3]); %! assert_equal (t.B, [2.5; 4.5]); %! assert_equal (t.C, {'foo'; 'bar'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Test 'ReadVariableNames' false generates default Var1, Var2, ... names %!test %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "1,2\n3,4\n"); %! fclose (fid); %! unwind_protect %! t = csv2table (fn, 'ReadVariableNames', false, 'ReadRowNames', false); %! assert_equal (t.Properties.VariableNames, {'Var1', 'Var2'}); %! assert_equal (height (t), 2); %! assert_equal (t.Var1, [1; 3]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Test 'VariableNames' overrides the names read from the header line %!test %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "A,B\n1,2\n3,4\n"); %! fclose (fid); %! unwind_protect %! t = csv2table (fn, 'VariableNames', {'x', 'y'}, 'ReadRowNames', false); %! assert_equal (t.Properties.VariableNames, {'x', 'y'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Test 'NumHeaderLines' skips leading lines before the header row %!test %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "junk,more\nA,B\n1,2\n3,4\n"); %! fclose (fid); %! unwind_protect %! t = csv2table (fn, 'NumHeaderLines', 1, 'ReadRowNames', false); %! assert_equal (t.Properties.VariableNames, {'A', 'B'}); %! assert_equal (t.A, [1; 3]); %! assert_equal (t.B, [2; 4]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Test 'RowNamesColumn' reads a column as the table row names %!test %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "id,v\nr1,10\nr2,20\n"); %! fclose (fid); %! unwind_protect %! t = csv2table (fn, 'ReadRowNames', true, 'RowNamesColumn', 1); %! assert_equal (t.Properties.RowNames, {'r1'; 'r2'}); %! assert_equal (t.v, [10; 20]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Test 'TextType' string returns text columns as a string array %!test %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "A,B\nfoo,bar\n"); %! fclose (fid); %! unwind_protect %! t = csv2table (fn, 'TextType', 'string', 'ReadRowNames', false); %! assert_equal (class (t.A), 'string'); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Test 'VariableUnitsLine' and 'VariableDescriptionsLine' read the right lines %!test %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "A,B\nkg,m\nd1,d2\n1,2\n3,4\n"); %! fclose (fid); %! unwind_protect %! t = csv2table (fn, 'VariableUnitsLine', 2, 'VariableDescriptionsLine', ... %! 3, 'ReadRowNames', false); %! assert_equal (t.Properties.VariableUnits, {'kg', 'm'}); %! assert_equal (t.Properties.VariableDescriptions, {'d1', 'd2'}); %! assert_equal (t.A, [1; 3]); %! assert_equal (t.B, [2; 4]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Test hexadecimal columns auto-detect to the smallest integer type %!test %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "h\nFF\n0A\n"); %! fclose (fid); %! unwind_protect %! t = csv2table (fn, 'ReadRowNames', false, 'HexType', 'auto'); %! assert_equal (class (t.h), 'uint8'); %! assert_equal (t.h, uint8 ([255; 10])); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Test header names with spaces are made into valid variable names %!test %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "my col,other one\n1,2\n"); %! fclose (fid); %! unwind_protect %! t = csv2table (fn, 'ReadRowNames', false); %! assert_equal (t.Properties.VariableNames, {'myCol', 'otherOne'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Test a generic CSV whose first cell is numeric does not error on read %!test %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "10,20\n30,40\n"); %! fclose (fid); %! unwind_protect %! t = csv2table (fn, 'ReadVariableNames', false, 'ReadRowNames', false); %! assert_equal (height (t), 2); %! assert_equal (t.Var1, [10; 30]); %! assert_equal (t.Var2, [20; 40]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Test duplicate row names are rejected %!error ... %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "id,v\nr1,10\nr1,20\n"); %! fclose (fid); %! unwind_protect %! csv2table (fn, 'ReadRowNames', true, 'RowNamesColumn', 1); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Test a large integer column is read exactly as a 64-bit integer %!test %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "v\n18446744073709551615\n18446744073709551614\n"); %! fclose (fid); %! unwind_protect %! t = csv2table (fn, 'ReadRowNames', false); %! assert_equal (class (t.v), 'uint64'); %! assert_equal (t.v, [intmax('uint64'); intmax('uint64') - uint64(1)]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Test NAME input validation %!error ... %! csv2table (42); ## Test reading a non-existent file reports a clear error %!error ... %! csv2table (fullfile (tempname (), 'no_such_file.csv')); ## Test an unknown option, and one without a value %!error ... %! csv2table ('no_such_file.csv', 'Bogus', 1); %!error ... %! csv2table ('no_such_file.csv', 'Bogus'); ## Test the 'Delimiter' option selects the field separator %!test %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "A;B;C\n1;2.5;foo\n3;4.5;bar\n"); %! fclose (fid); %! unwind_protect %! t = csv2table (fn, 'Delimiter', ';', 'ReadRowNames', false); %! assert_equal (t.Properties.VariableNames, {'A', 'B', 'C'}); %! assert_equal (t.A, [1; 3]); %! assert_equal (t.C, {'foo'; 'bar'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A datetime column is not clobbered by the later duration/hex detection %!test %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "id,d\n1,2024-01-15\n2,2024-02-20\n"); %! fclose (fid); %! unwind_protect %! t = csv2table (fn, 'ReadRowNames', false); %! assert_equal (class (t.d), 'datetime'); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A zoned column whose text carries an offset is read as instants in that zone %!test %! fn = [tempname() '.csv']; %! dt = datetime (2024, 6, [15; 16], 10, 30, 0, 'TimeZone', 'UTC'); %! T = table (dt, 'VariableNames', {'t'}); %! unwind_protect %! table2csv (T, fn); %! txt = fileread (fn); %! txt = regexprep (txt, '(\d{4}-\d{2}-\d{2}T[\d:.]+)', '$1-05:00'); %! fid = fopen (fn, 'w'); %! fputs (fid, txt); %! fclose (fid); %! R = csv2table (fn); %! assert_equal (R.t.TimeZone, 'UTC'); %! assert_equal (hour (R.t), [15; 15]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: a fixed-offset zone keeps its TimeZone %!test %! fn = [tempname() '.csv']; %! dt = datetime (2024, 6, [15; 16], 10, 30, 0, 'TimeZone', '+05:30'); %! T = table (dt, 'VariableNames', {'t'}); %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.t.TimeZone, '+05:30'); %! assert_equal (cellstr (char (R.t)), cellstr (char (dt))); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Hexadecimal auto-detection still works after the detection-order fix %!test %! fn = tempname (); %! fid = fopen (fn, "w"); %! fputs (fid, "h\nFF\n0A\n"); %! fclose (fid); %! unwind_protect %! t = csv2table (fn, 'ReadRowNames', false, 'HexType', 'auto'); %! assert_equal (t.h, uint8 ([255; 10])); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Test a multi-character 'Delimiter' is refused before the file is opened %!error ... %! csv2table (fullfile (tempname (), 'x.csv'), 'Delimiter', 'ab'); pr0m1th3as-datatypes-9450b29/inst/csv2timetable.m000066400000000000000000000263461527370255000216100ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{tt} =} csv2timetable (@var{filename}) ## @deftypefnx {datatypes} {@var{tt} =} csv2timetable (@var{filename}, @var{Name}, @var{Value}) ## ## Read a comma-separated-value (CSV) file into a timetable. ## ## @code{@var{tt} = csv2timetable (@var{filename})} reads the CSV file named by ## @var{filename} into a @code{timetable}. @var{filename} may be a character ## vector, a cellstr, or a string scalar. ## ## A file written by @code{timetable2csv} carries the package's own header ## block, which names and types every variable and tags the leading column of ## row times with their type, their @code{TimeZone} where they have one, and ## their @code{Format}. Such a file comes back as the timetable it was ## written from: the row times keep their type, zone and format exactly, and ## the row dimension keeps its name. ## ## Any other CSV file is read as @code{csv2table} reads it, and its ## @strong{first} @code{datetime} or @code{duration} variable becomes the row ## times, the row dimension taking that variable's name. A file with no such ## variable cannot be read as a timetable. ## ## Every @var{Name}-@var{Value} option of @code{csv2table} is accepted and ## behaves as it does there, with two exceptions. @qcode{'ReadRowNames'} and ## @qcode{'RowNamesColumn'} are refused: a timetable labels its rows by time ## and by nothing else, so a file whose rows are named is read with ## @code{csv2table}. ## ## @code{TimeStep} and @code{SampleRate} are not stored in the file and are ## worked out again from the row times, so a regular timetable comes back ## regular. A CSV file carries no event table, so @qcode{'Events'} of the ## result is always empty even when the timetable that was written had one; ## use @code{timetable2ods} and @code{ods2timetable} to keep events. ## ## @seealso{timetable.timetable2csv, csv2table, ods2timetable, readtimetable, ## timetable} ## @end deftypefn function tt = csv2timetable (filename, varargin) if (nargin < 1) error ("csv2timetable: too few input arguments."); endif if (! ((ischar (filename) && isvector (filename)) || iscellstr (filename) ... || isa (filename, 'string'))) error (strcat ("csv2timetable: FILENAME must be a character vector,", ... " cellstr, or string.")); endif if (mod (numel (varargin), 2) != 0) error ("csv2timetable: name-value arguments must be in pairs."); endif ## A timetable has no row names, so an option asking for them would be ## honoured by the reader and then discarded by the conversion, which is ## worse than saying so. for i = 1:2:numel (varargin) opt = varargin{i}; if (isa (opt, 'string') && isscalar (opt)) opt = char (opt); endif if (ischar (opt) && any (strcmpi (opt, {'ReadRowNames', 'RowNamesColumn'}))) error (strcat ("csv2timetable: '%s' is not supported; a timetable", ... " labels its rows by time. Read a file whose rows", ... " are named with 'csv2table'."), opt); endif endfor ## The file is read by 'csv2table', whose complaints are about the call ## made here and so are raised under this function's name. try tbl = csv2table (filename, varargin{:}); catch err if (strncmp (err.message, "csv2table: ", 11)) error ("csv2timetable: %s", err.message(12:end)); endif rethrow (err); end_try_catch types = tbl.Properties.VariableTypes; if (! any (ismember (types, {'datetime', 'duration'}))) error (strcat ("csv2timetable: the file has no datetime or duration", ... " column to use as row times.")); endif tt = table2timetable (tbl); endfunction %!demo %! ## A timetable written by `timetable2csv` comes back whole: the row times %! ## keep their type, their time zone and their display format, and the row %! ## dimension keeps its name. %! %! t = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours ((0:3)'); %! TT = timetable (t, (1:4)', 'VariableNames', {'reading'}); %! TT.Properties.DimensionNames{1} = 'Stamp'; %! fname = [tempname(), '.csv']; %! timetable2csv (TT, fname); %! csv2timetable (fname) %! delete (fname); ## Test a zoned datetime round-trips whole %!test %! t = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours ((0:3)'); %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! timetable2csv (TT, fname); %! out = csv2timetable (fname); %! assert_equal (class (out), 'timetable'); %! assert_equal (out.Properties.RowTimes.TimeZone, 'America/New_York'); %! assert_equal (out.Properties.RowTimes, TT.Properties.RowTimes); %! assert_equal (out.v, TT.v); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the row times keep their display format %!test %! t = datetime (2024, 1, (1:3)'); %! t.Format = 'uuuu-MM-dd HH:mm:ss.SSS'; %! TT = timetable (t, (1:3)', 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! timetable2csv (TT, fname); %! assert_equal (csv2timetable (fname).Properties.RowTimes.Format, ... %! 'uuuu-MM-dd HH:mm:ss.SSS'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test duration row times round-trip whole, format included %!test %! d = hours ([0.5; 1.5; 2.5]); %! d.Format = 'hh:mm:ss.SSS'; %! TT = timetable (d, (1:3)', 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! timetable2csv (TT, fname); %! out = csv2timetable (fname); %! assert_equal (class (out.Properties.RowTimes), 'duration'); %! assert_equal (out.Properties.RowTimes, TT.Properties.RowTimes); %! assert_equal (out.Properties.RowTimes.Format, 'hh:mm:ss.SSS'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the row dimension name survives %!test %! t = datetime (2024, 1, (1:3)'); %! TT = timetable (t, (1:3)', 'VariableNames', {'v'}); %! TT.Properties.DimensionNames{1} = 'Stamp'; %! fname = [tempname(), '.csv']; %! unwind_protect %! timetable2csv (TT, fname); %! assert_equal (csv2timetable (fname).Properties.DimensionNames, ... %! {'Stamp', 'Variables'}); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the variable descriptions and units survive %!test %! t = datetime (2024, 1, (1:3)'); %! TT = timetable (t, (1:3)', {'a'; 'b'; 'c'}, 'VariableNames', {'v', 'w'}); %! TT.Properties.VariableDescriptions = {'a count', ''}; %! TT.Properties.VariableUnits = {'kg', ''}; %! fname = [tempname(), '.csv']; %! unwind_protect %! timetable2csv (TT, fname); %! out = csv2timetable (fname); %! assert_equal (out.Properties.VariableDescriptions, {'a count', ''}); %! assert_equal (out.Properties.VariableUnits, {'kg', ''}); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test a regular timetable comes back regular %!test %! TT = timetable ((1:4)', 'TimeStep', hours (1), 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! timetable2csv (TT, fname); %! out = csv2timetable (fname); %! assert_equal (isregular (out), true); %! assert_equal (out.Properties.TimeStep, hours (1)); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test 'WriteVariableNames' false numbers the variables and defaults the name %!test %! t = datetime (2024, 1, (1:3)'); %! TT = timetable (t, (1:3)', 'VariableNames', {'v'}); %! TT.Properties.DimensionNames{1} = 'Stamp'; %! fname = [tempname(), '.csv']; %! unwind_protect %! timetable2csv (TT, fname, 'WriteVariableNames', false); %! out = csv2timetable (fname); %! assert_equal (out.Properties.VariableNames, {'Var2'}); %! assert_equal (out.Properties.DimensionNames{1}, 'Var1'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test an attached event table is not written and does not come back %!test %! t = datetime (2024, 1, 1) + hours ((0:3)'); %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t([2 3]), 'EventLabels', {'on'; 'off'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! timetable2csv (TT, fname); %! assert_equal (isempty (csv2timetable (fname).Properties.Events), true); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test a foreign file takes its first datetime column as the row times %!test %! fname = [tempname(), '.csv']; %! unwind_protect %! fid = fopen (fname, 'w'); %! fprintf (fid, "when,val\n2024-01-01,1\n2024-01-02,2\n"); %! fclose (fid); %! out = csv2timetable (fname); %! assert_equal (class (out), 'timetable'); %! assert_equal (out.Properties.DimensionNames{1}, 'when'); %! assert_equal (out.val, [1; 2]); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test a table written with row names is read by 'csv2table' unchanged %!test %! T = table ((1:3)', 'VariableNames', {'v'}, 'RowNames', {'a', 'b', 'c'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! table2csv (T, fname); %! assert_equal (csv2table (fname).Properties.RowNames, {'a'; 'b'; 'c'}); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test too few input arguments %!error csv2timetable () ## Test an invalid FILENAME %!error ... %! csv2timetable (42) ## Test 'ReadRowNames' is refused %!error ... %! csv2timetable ('none.csv', 'ReadRowNames', true) ## Test 'RowNamesColumn' is refused %!error ... %! csv2timetable ('none.csv', 'RowNamesColumn', 1) ## Test an unknown option, and one without a value %!error ... %! csv2timetable ('none.csv', 'Bogus', 1) %!error ... %! csv2timetable ('none.csv', 'Bogus') ## A file whose columns are all numeric, so nothing in it can label the rows. %!shared fnum %! fnum = [tempname(), '.csv']; %! fid = fopen (fnum, 'w'); %! fputs (fid, "a,b\n1,2\n3,4\n"); %! fclose (fid); ## Test a file with no time column cannot become a timetable %!error ... %! csv2timetable (fnum) ## Test the fixture is removed again %!test %! delete (fnum); %! assert_equal (exist (fnum, 'file'), 0); pr0m1th3as-datatypes-9450b29/inst/datetime.m000066400000000000000000015027551527370255000206440ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef datetime ## -*- texinfo -*- ## @deftp {datatypes} datetime ## ## Array representing points in time using the Gregorian calendar. ## ## A @qcode{datetime} array stores internally the datetime points as double ## arrays representing whole years, months, days, hours, and minutes, as well ## as seconds including their fractional part. The underlying implementation ## relies on the @qcode{'date.h'} C++ library. The precision of this ## implementation is set at microseconds, which is substantial for typical ## times. ## ## A @code{datetime} array is a collection of date/time elements, with each ## element holding a complete date/time. The @code{datetime} array also has ## @qcode{TimeZone} and @qcode{Format} properties associated with it, which ## apply to all elements in the array. ## ## When a display @code{Format} contains a time-zone name field ## (@qcode{z}/@qcode{zz}/@qcode{zzz}), the name is rendered according to a ## session-wide style set by @code{datetime.zoneNameStyle}. The default, ## @qcode{'iana'}, is an Octave-specific extension that shows the IANA ## abbreviation for every zone (e.g. @qcode{EEST}); @qcode{'matlab'} restores ## MATLAB's behaviour of naming only North American zones and showing a ## numeric @qcode{UTC+3}-style offset elsewhere. A single @code{Format} may ## override the session style with @qcode{zzzz} (force @qcode{'iana'}) or ## @qcode{zzzzz} (force @qcode{'matlab'}). See @code{datetime.zoneNameStyle}. ## ## @seealso{calendarDuration, duration} ## @end deftp properties (Access = private, Hidden) ## Whole years Year = 0 ## Whole months Month = 0 ## Whole days Day = 0 ## Whole hours Hour = 0 ## Whole minutes Minute = 0 ## Seconds (including fractional seconds) Second = 0 ## UTC offset in seconds for each element, and the only thing that tells ## apart the two instants sharing a wall clock across a fall-back ## transition: 01:30 on 3 November 2024 in America/New_York is -4*3600 on ## its first pass and -5*3600 on its second. Zero for an unzoned array and ## for the leap-second zone, both of which are UTC-based, so ## ## serial = naive_seconds (components) - Offset ## ## holds without a branch. Seconds rather than a boolean fold because ## Australia/Lord_Howe shifts by half an hour and Pacific/Chatham sits at ## a quarter hour. NaN for Not-A-Time. ## ## INVARIANT: Offset always agrees with (components, TimeZone). A ## wall-clock operation goes through 'normalize', which recomputes it as ## the later of an ambiguous pair; an instant-based operation goes through ## 'serial2components', which carries the true one through. An ## instant-derived result must therefore NOT be passed to 'normalize' ## afterwards, or the fold is silently lost. Offset = 0 endproperties properties ## -*- texinfo -*- ## @deftp {datetime} {property} Format ## ## Display format ## ## Display format, specified as a character vector or string scalar. If ## specified as a string scalar, it is converted and stored internally as ## a character vector. ## ## The value @qcode{'default'} is a data-dependent sentinel: a date-only ## format (@qcode{'dd-MMM-uuuu'}) is used when every element sits at ## midnight, and a date and time format (@qcode{'dd-MMM-uuuu HH:mm:ss'}) ## otherwise. The value @qcode{'defaultdate'} always selects the ## date-only format. Reading the property returns the resolved pattern, ## never the sentinel itself. ## ## A custom pattern is built from the following Unicode (LDML) fields; ## repeating a letter widens or names the field. Any other text is ## copied verbatim, and text in single quotes is always literal. ## ## @multitable @columnfractions 0.18 0.82 ## @item @code{y}, @code{u} @tab Year; @code{yy} uses the last two digits. ## @item @code{M} @tab Month: number (@code{M}, @code{MM}), abbreviated ## (@code{MMM}), full (@code{MMMM}), or initial (@code{MMMMM}). ## @item @code{d} @tab Day of the month. ## @item @code{D} @tab Day of the year. ## @item @code{e} @tab Day of the week: number (Sunday is 1), abbreviated ## (@code{eee}), full (@code{eeee}), or initial (@code{eeeee}). ## @item @code{H}, @code{h} @tab Hour, 24-hour and 12-hour clock. ## @item @code{m} @tab Minute. ## @item @code{s} @tab Whole second (fractional seconds are truncated). ## @item @code{S} @tab Fractional second, one digit per @code{S}. ## @item @code{a} @tab AM or PM. ## @item @code{Q} @tab Quarter of the year. ## @item @code{G} @tab Era. ## @item @code{W} @tab Week of the month. ## @item @code{z} @tab Time-zone name (style set by ## @code{datetime.zoneNameStyle}; @code{zzzz}/@code{zzzzz} force it). ## @item @code{Z}, @code{X}, @code{x} @tab Numeric time-zone offset. ## @end multitable ## ## @end deftp Format = 'default' ## -*- texinfo -*- ## @deftp {datetime} {property} TimeZone ## ## Time zone ## ## Time zone, specified as a character vector or string scalar. If ## specified as a string scalar, it is converted and stored internally as ## a character vector. ## ## Setting it to @qcode{'local'}, in any letter case, stores the system time ## zone given by @code{datetime.SystemTimeZone}, exactly as setting that ## zone by name would store it, so a system zone of @qcode{'Etc/UTC'} is ## stored as @qcode{'+00:00'}. ## ## A fixed offset from UTC, which never observes daylight saving time, is ## stored as @qcode{'+HH:MM'} or @qcode{'-HH:MM'}, or as ## @qcode{'+HH:MM:SS'} when the offset has seconds, and may be up to 23 ## hours and 59 minutes either way. It may also be given as @qcode{'+H'}, ## @qcode{'+HH'}, @qcode{'+HMM'}, @qcode{'+HHMM'} or @qcode{'+HHMMSS'}, ## as @qcode{'Z'} for a zero offset, or with @qcode{'UTC'} or @qcode{'GMT'} ## in front, in any letter case, as @qcode{'UTC+H'}, @qcode{'UTC+HH'}, ## @qcode{'UTC+H:MM'}, @qcode{'UTC+HH:MM'} or @qcode{'UTC+HH:MM:SS'}. The ## database's own fixed-offset zones, such as @qcode{'Etc/GMT-3'} and ## @qcode{'Etc/UTC'}, are stored as offsets as well. A @qcode{'GMT'} ## prefix and the names @qcode{'GMT0'}, @qcode{'Zulu'}, @qcode{'UCT'}, ## @qcode{'Universal'} and @qcode{'Greenwich'} raise a warning. Unlike the ## database names, whose sign is reversed, @qcode{'+03:00'}, ## @qcode{'GMT+3'} and @qcode{'Etc/GMT-3'} are all three hours ahead of ## UTC. ## ## Besides the zones of the IANA Time Zone Database, the value ## @qcode{'UTCLeapSeconds'} selects UTC with its inserted leap seconds made ## representable, so that the 60th second of a minute exists on the 27 dates ## that have one. It is not an IANA zone and @code{timezones} does not list ## it. An array in that zone counts elapsed SI seconds, which is what makes ## its arithmetic differ from a UTC array's across an inserted second, and ## for that reason it cannot be combined or compared with an array that does ## not have leap seconds. Its @code{Format} is fixed to ## @qcode{"uuuu-MM-dd'T'HH:mm:ss'Z'"}, optionally with one to nine ## fractional-second digits, since no other pattern can write a 60th second; ## moving such an array to any other zone folds an inserted second back onto ## the 59th and restores the ordinary default format. ## ## @end deftp TimeZone = '' endproperties methods (Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif __disp__ (this, 'datetime', in_name); endfunction ## Custom display function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif __disp__ (this, 'datetime'); endfunction endmethods methods (Static, Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{E} =} datetime.empty () ## @deftypefnx {datetime} {@var{E} =} datetime.empty (@var{sz}) ## @deftypefnx {datetime} {@var{E} =} datetime.empty (@var{m}, @var{n}, @dots{}) ## ## Create an empty datetime array. ## ## @code{@var{E} = datetime.empty ()} returns a @math{0*0} empty datetime ## array. @code{datetime.empty (@var{m}, @var{n}, @dots{})} or ## @code{datetime.empty (@var{sz})} returns an empty datetime array of the ## requested size, which must have at least one dimension equal to zero. A ## lone dimension gives a square size, so @code{datetime.empty (3)} is an ## error while @code{datetime.empty (0)} is @math{0*0}. As for ## @code{zeros}, a negative dimension counts as zero, and a size vector with ## nothing in it names no size and gives @math{0*0}. ## ## @end deftypefn function E = empty (varargin) if (nargin == 0) sz = [0, 0]; elseif (nargin == 1 && ! isscalar (varargin{1})) sz = double (varargin{1}(:)).'; else sz = [varargin{:}]; endif if (! (isnumeric (sz) && isrow (sz) && all (sz == fix (sz)))) error ("datetime.empty: dimensions must be integer values."); endif ## A negative dimension is no smaller than none at all, and a size vector ## holding no dimensions names no size: 'zeros' and every other array ## constructor read both the same way. sz = max (sz, 0); if (isempty (sz)) sz = [0, 0]; elseif (isscalar (sz)) sz = [sz, sz]; endif if (all (sz != 0)) error (strcat ("datetime.empty: at least one dimension must be", ... " zero for an empty array.")); endif E = datetime (nan (sz), 'ConvertFrom', 'datenum'); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{tz} =} datetime.SystemTimeZone () ## ## System time zone setting. ## ## @code{@var{tz} = datetime.SystemTimeZone ()} returns the time zone of the ## system where Octave is running, as the name of a zone in the IANA Time ## Zone Database, such as @qcode{'Europe/Athens'}. It is @qcode{'UTC'} when ## the system time zone cannot be determined. The parentheses may be ## omitted, and the same value is read from any datetime array @var{d} as ## @code{@var{d}.SystemTimeZone}. ## ## @end deftypefn function tz = SystemTimeZone () tz = __datetime__ ('systemtimezone'); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{style} =} datetime.zoneNameStyle () ## @deftypefnx {datetime} {} datetime.zoneNameStyle (@var{style}) ## ## Query or set the session-wide style used to render time-zone names. ## ## The @qcode{z}, @qcode{zz}, and @qcode{zzz} fields of a display ## @code{Format} render a time-zone name. Two styles are available, ## selected by this session-wide preference: ## ## @table @asis ## @item @qcode{'iana'} (default) ## The IANA time-zone database abbreviation active at each instant, such as ## @qcode{EDT}, @qcode{EEST}, @qcode{JST}, or @qcode{UTC}. This is an ## Octave-specific extension: a named abbreviation is shown for every zone. ## ## @item @qcode{'matlab'} ## MATLAB-compatible rendering: a named abbreviation is shown only for the ## North American zones plus @qcode{GMT} and @qcode{UTC}; every other zone ## renders as a numeric UTC offset such as @qcode{UTC+3} or ## @qcode{UTC+5:30}. ## @end table ## ## @code{@var{style} = datetime.zoneNameStyle ()} returns the current ## style. @code{datetime.zoneNameStyle (@var{style})} sets it to ## @var{style}, either @qcode{'iana'} or @qcode{'matlab'}. The setting ## persists for the current Octave session. ## ## An individual @code{Format} string can override the session style with ## the Octave-specific fields @qcode{zzzz} (force @qcode{'iana'}) and ## @qcode{zzzzz} (force @qcode{'matlab'}). ## ## @end deftypefn function out = zoneNameStyle (style) persistent current; if (isempty (current)) current = 'iana'; endif if (nargin < 1) out = current; else if (! (ischar (style) && isrow (style)) ... || ! any (strcmpi (style, {'iana', 'matlab'}))) error ("datetime.zoneNameStyle: STYLE must be 'iana' or 'matlab'."); endif current = lower (style); if (nargout > 0) out = current; endif endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {} datetime.setDefaultFormats (@qcode{'default'}, @var{fmt}) ## @deftypefnx {datetime} {} datetime.setDefaultFormats (@qcode{'defaultdate'}, @var{fmt}) ## @deftypefnx {datetime} {} datetime.setDefaultFormats (@qcode{'reset'}) ## ## Set the default display formats of datetime arrays. ## ## A datetime array whose @qcode{Format} property was never set explicitly ## displays with one of two default formats, and this method chooses them. ## The choice between the two is made from the values: an array whose ## elements all sit at midnight uses the date-only default, and any other ## array uses the date-and-time default. The choice is made afresh on every ## display, so an array moves between the two when its components change. ## ## @code{datetime.setDefaultFormats (@qcode{'default'}, @var{fmt})} sets the ## date-and-time default to @var{fmt}, and ## @code{datetime.setDefaultFormats (@qcode{'defaultdate'}, @var{fmt})} sets ## the date-only default. @var{fmt} is a display format, given as a ## character vector or string scalar, and is validated as the @qcode{Format} ## property is. Arrays that already exist take the new format on their next ## display; those with an explicitly set @qcode{Format} are unaffected. ## ## @code{datetime.setDefaultFormats (@qcode{'reset'})} restores both factory ## formats, @qcode{'dd-MMM-uuuu HH:mm:ss'} and @qcode{'dd-MMM-uuuu'}. ## ## Both formats persist across Octave sessions. ## ## @strong{Deviation from MATLAB.} MATLAB accepts a second argument after ## @qcode{'reset'} and silently ignores it, resetting both defaults whatever ## it says, so @code{datetime.setDefaultFormats ('reset', 'default')} reads ## as a request to reset one and quietly resets two. Anything at all is ## accepted there, a number included. Here that is an error, since no ## correct program can depend on the argument being ignored. ## ## @end deftypefn function setDefaultFormats (what, fmt) if (nargin < 1) error ("datetime.setDefaultFormats: too few input arguments."); endif what = convertStringsToChars (what); if (! (ischar (what) && isrow (what)) ... || ! any (strcmpi (what, {'default', 'defaultdate', 'reset'}))) error (strcat ("datetime.setDefaultFormats: the first input must", ... " be 'default', 'defaultdate', or 'reset'.")); endif ## Unlike MATLAB, which takes a second argument here and discards it. if (strcmpi (what, 'reset')) if (nargin > 1) error (strcat ("datetime.setDefaultFormats: 'reset' takes no", ... " further arguments; it resets both defaults.")); endif dtDefaultFormats ('reset'); return; endif if (nargin < 2) error ("datetime.setDefaultFormats: too few input arguments."); endif fmt = convertStringsToChars (fmt); if (! (ischar (fmt) && isrow (fmt))) error (strcat ("datetime.setDefaultFormats: FMT must be a", ... " character vector or a string scalar.")); endif if (any (strcmpi (fmt, {'default', 'defaultdate', 'preserveinput'}))) error (strcat ("datetime.setDefaultFormats: FMT must be a display", ... " format, not the name of a default.")); endif dtValidateFormat (fmt); dtDefaultFormats ('set', lower (what), fmt); endfunction endmethods ################################################################################ ## ** Create and convert 'datetime' type ** ## ################################################################################ ## Available Methods ## ## ## ## 'datetime' 'dispstrings' 'cellstr' 'char' ## ## 'ymd' 'hms' 'year' 'quarter' ## ## 'month' 'week' 'day' 'hour' ## ## 'minute' 'second' 'timeofday' 'tzoffset' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{T} =} datetime (@qcode{'now'}) ## @deftypefnx {datetime} {@var{T} =} datetime (@qcode{'today'}) ## @deftypefnx {datetime} {@var{T} =} datetime (@qcode{'tomorrow'}) ## @deftypefnx {datetime} {@var{T} =} datetime (@qcode{'yesterday'}) ## @deftypefnx {datetime} {@var{T} =} datetime (@var{DateStrings}) ## @deftypefnx {datetime} {@var{T} =} datetime (@var{DateStrings}, @qcode{'MixedFormats'}, @var{TF}) ## @deftypefnx {datetime} {@var{T} =} datetime (@var{DateStrings}, @qcode{'InputFormat'}, @var{INFMT}) ## @deftypefnx {datetime} {@var{T} =} datetime (@var{DateStrings},@ ## @qcode{'InputFormat'}, @var{INFMT}, @qcode{'PivotYear'}, @var{PIVOT}) ## @deftypefnx {datetime} {@var{T} =} datetime (@var{DateStrings},@ ## @qcode{'InputFormat'}, @var{INFMT}, @qcode{'Locale'}, @var{LOCALE}) ## @deftypefnx {datetime} {@var{T} =} datetime (@var{DateVectors}) ## @deftypefnx {datetime} {@var{T} =} datetime (@var{Y}, @var{MO}, @var{D}) ## @deftypefnx {datetime} {@var{T} =} datetime (@var{Y}, @var{MO}, @var{D}, @var{H}, @var{MI}, @var{S}) ## @deftypefnx {datetime} {@var{T} =} datetime (@var{Y}, @var{MO}, @var{D}, @var{H}, @var{MI}, @var{S}, @var{MS}) ## @deftypefnx {datetime} {@var{T} =} datetime (@var{X}, @qcode{'ConvertFrom'}, @var{TYPE}) ## @deftypefnx {datetime} {@var{T} =} datetime (@var{D}) ## @deftypefnx {datetime} {@var{T} =} datetime (@dots{}, @qcode{'Format'}, @var{FMT}) ## @deftypefnx {datetime} {@var{T} =} datetime (@dots{}, @qcode{'TimeZone'}, @var{TZ}) ## ## Create a new array of datetime values. ## ## @code{@var{T} = datetime (@qcode{'now'})} returns a scalar datetime array ## corresponding to the current date and time. @code{@var{T} = datetime ## (@qcode{'now'})} is the same as @code{@var{T} = datetime ()}. Except for ## @qcode{'now'}, the same syntax can be used with @qcode{'today'}, ## @qcode{'tomorrow'} and @qcode{'yesterday'}. These options return the ## respective date but with time set at midnight. ## ## @code{@var{T} = datetime (@var{DateStrings})} creates a datetime array ## from the text in @var{DateStrings} representing points in time. Without ## an @qcode{'InputFormat'}, one format is detected from the first piece of ## text that is not blank, and every other piece is then read with that same ## format: text written in a different one is not given a format of its own ## but becomes @code{NaT}, so that a column of dates is read as the single ## thing it is meant to be. Text no format can be detected from at all is ## refused outright, and so is text whose first piece that is not blank ## the detected format cannot read, whatever follows it. A character ## matrix is no exception, where MATLAB returns only @code{NaT} for one ## whose first row is blank, even when a later row can be read. A later ## date that the detected format cannot make sense of, such as ## @qcode{'2024-04-31'}, is lost in the same way an explicit ## @qcode{'InputFormat'} loses it. ## ## The formats detected are: ## ## @multitable @columnfractions 0.34 0.66 ## @item @qcode{'uuuu-MM-dd'} @tab optionally followed by @qcode{'HH:mm'}, ## @qcode{'HH:mm:ss'}, or either with fractional seconds, the date and the ## time separated by a space or by @qcode{'T'}. The time may carry a UTC ## offset, @qcode{'Z'}, @qcode{'+HH'}, @qcode{'+HHMM'} or @qcode{'+HH:MM'}, ## which is read as with an offset field and so requires ## @qcode{'TimeZone'}. MATLAB detects no text carrying an offset. ## @item @qcode{'dd-MMM-uuuu'} @tab month named in full or abbreviated, ## optionally followed by a time as above. ## @item @qcode{'MMMM d, uuuu'} @tab as in @qcode{'March 15, 2024'}, in ## English only, optionally followed by a time. ## @item @qcode{'MM/dd/uuuu'} @tab or @qcode{'dd/MM/uuuu'}, the year in two ## digits or four, optionally followed by a time; see below. ## @item @qcode{'dd.MM.uuuu'} @tab day first in every locale, optionally ## followed by a time, or @qcode{'dd.MM.yy'} with all three fields in two ## digits, so that a version number such as @qcode{'1.5.10'} stays text. ## @item @qcode{'HH:mm:ss'} @tab a time alone, taking today's date. ## @end multitable ## ## A two-digit year is read against the pivot year, as with an ## @qcode{'InputFormat'}, where MATLAB reads @qcode{'05.03.24'} as the year ## 24. ## ## Three further shapes are read that MATLAB does not detect, and are ## Octave extensions: @qcode{'uuuu/MM/dd'} with an optional time, ## @qcode{'dd MMMM uuuu'} as in @qcode{'15 March 2024'}, and a year alone. ## Numeric fields need not be padded, so @qcode{'2024-2-9'} reads as ## @qcode{'2024-02-09'} does. ## ## A date written with slashes is ambiguous: @qcode{'03/09/2024'} is the 3rd ## of September to some readers and the 9th of March to others. The whole ## array decides, since one entry naming a day past the twelfth settles the ## order for all of them; where nothing settles it the reading of the ## @qcode{'Locale'} is taken, month first in English but for ## @qcode{'en_GB'} and day first in every other language, and a warning ## (@qcode{'Octave:datetime:ambiguous-format'}) is raised, but for ## @qcode{'pt_PT'}. Under a German locale, which writes dates with dots, ## slashes are not read, as in MATLAB. ## ## @code{@var{T} = datetime (@var{DateStrings}, @qcode{'Locale'}, ## @var{LOCALE})} detects the format in the language of @var{LOCALE}: the ## month names above are its own, abbreviated or full, matched in any case ## and with or without the point after an abbreviation, and English names ## are not read under another language. Each language adds its long form, ## optionally followed by a time: @qcode{'d. MMMM uuuu'} in German ## (@qcode{'1. Januar 2024'}, @qcode{'Jänner'} under @qcode{'de_AT'}), ## @qcode{'d 'de' MMMM 'de' uuuu'} in Spanish and Portuguese ## (@qcode{'5 de marzo de 2024'}), and @qcode{'dd MMMM uuuu'} in the other ## languages but Portuguese (@qcode{'15 mars 2024'}, @qcode{'15 ΜαÏ�τίου ## 2024'}). MATLAB reads the Spanish and Portuguese long form under ## @qcode{'pt_BR'} alone, refuses a point after a Spanish abbreviation, and ## refuses Greek written in capitals; all of these are read here. ## ## @code{@var{T} = datetime (@var{DateStrings}, @qcode{'MixedFormats'}, ## @code{true})} detects a format for each piece of text separately instead, ## so text gathered from several sources into one column is read whatever ## each entry happens to be written in. This is an Octave extension, off by ## default; MATLAB has no equivalent. It reads the wider set of formats ## Octave's core @code{datevec} accepts, and rolls an impossible date over ## rather than refusing it, so @qcode{'2024-04-31'} is read as the 1st of ## May. It cannot tell a mistake from a format it has not seen before, ## which is why it is not the default. ## ## @code{@var{T} = datetime (@var{DateStrings}, @qcode{'InputFormat'}, ## @var{INFMT})} also allows to specify a particular input text format to ## parse @var{DateStrings}. It is always preferable to specify the format ## @var{INFMT} if it is known. @var{INFMT} uses the Unicode LDML date field ## symbols (@qcode{'y'}, @qcode{'M'}, @qcode{'d'}, @qcode{'H'}, @qcode{'h'}, ## @qcode{'m'}, @qcode{'s'}, @qcode{'S'}, @qcode{'a'}, @dots{}), the same ## set used for display formats; text between single quotes is treated as a ## literal. Formats which do not specify a particular time component will ## have the value set to zero. Formats which do not specify any date ## component default to the current date, whereas a partially specified date ## defaults its missing month and day to 1. ## ## A string that does not match @var{INFMT}, or that names a date which does ## not exist, such as @qcode{'2024-04-31'} or a 29th of February outside a ## leap year, cannot be converted. A lone such string is an error; within ## an array only that element is lost and becomes @code{NaT}, so that one ## unreadable entry does not cost the rest of the array. An array none of ## whose text can be read is an error too, unless it holds a blank element; ## a character matrix none of whose rows can be read is always an error. ## ## An @var{INFMT} with a time zone offset field, @qcode{'z'}, @qcode{'Z'}, ## @qcode{'X'} or @qcode{'x'}, reads each string as an instant, its wall ## clock less the offset it carries, and expresses it in the zone given by ## @qcode{'TimeZone'}, which is then required. The offset settles a ## repeated or skipped wall clock, so such text is never rejected for ## naming one. Every width of @qcode{'Z'}, @qcode{'X'} and @qcode{'x'} ## reads the same forms: @qcode{'Z'} for zero, a signed offset such as ## @qcode{'-5'}, @qcode{'-05'}, @qcode{'-0500'}, @qcode{'-05:00'} or ## @qcode{'-05:00:30'}, and either of those after @qcode{'GMT'} or ## @qcode{'UTC'}, which alone is zero. @qcode{'z'} reads the same, and a ## time zone abbreviation such as @qcode{'EST'}, @qcode{'CET'} or ## @qcode{'MSK'}, in any letter case. An abbreviation with two meanings is ## read only under a @var{LOCALE} that settles it: @qcode{'BST'} is +01:00 ## under @qcode{'en_GB'} and +06:00 under @qcode{'en_CA'}, @qcode{'IST'} is ## +01:00 under @qcode{'en_IE'} and +05:30 under @qcode{'en_IN'} and ## @qcode{'en_CA'}, @qcode{'ACST'} is +09:30 under @qcode{'en_AU'} and ## -04:00 under @qcode{'pt_BR'}, and @qcode{'AMT'} is -04:00 under ## @qcode{'pt_BR'}. Under any other @var{LOCALE} they are refused, where ## MATLAB reads @qcode{'BST'} as +06:00 and @qcode{'IST'} as +05:30 by ## default. ## ## @code{@var{T} = datetime (@var{DateStrings}, @qcode{'InputFormat'}, ## @var{INFMT}, @qcode{'PivotYear'}, @var{PIVOT})} also allows to specify a ## pivot year, which refers to the year at the start of the century to which ## two-digit years will be referenced. When not specified, it defaults to ## the current year minus 50. ## ## @code{@var{T} = datetime (@var{DateStrings}, @qcode{'InputFormat'}, ## @var{INFMT}, @qcode{'Locale'}, @var{LOCALE})} also interprets the month ## names, weekday names, and day-period markers in @var{DateStrings} ## according to @var{LOCALE}, given as an @qcode{'xx_YY'} identifier whose ## language part selects the names. The supported languages are ## @qcode{'en'} (the default), @qcode{'fr'}, @qcode{'de'}, @qcode{'es'}, ## @qcode{'it'}, @qcode{'pt'}, and @qcode{'el'}; @qcode{'system'} is treated ## as @qcode{'en'}. Both full (@qcode{'MMMM'}/@qcode{'eeee'}) and ## abbreviated (@qcode{'MMM'}/@qcode{'eee'}) month and weekday names are ## recognized case-insensitively; for Greek, matching is also ## accent-insensitive, so the accentless all-caps spelling is accepted, and ## the genitive month forms (@qcode{'ΜαÏ�τίου'}) are used. A weekday name is ## validated but does not otherwise affect the result. A hyphen may stand ## for the underscore, as in @qcode{'fr-FR'}. ## ## @code{@var{T} = datetime (@var{DateVectors})} creates a column vector of ## datetime values from the date vectors in @var{DateVectors}. ## ## @code{@var{T} = datetime (@var{Y}, @var{MO}, @var{D}} creates an array of ## datetime values for corresponding elements of the @var{Y}, @var{MO}, and ## @var{D} arrays, which must be of the same size or scalars, must contain ## integer values, and they correspond to years, months, and days, ## respectively. ## ## @code{@var{T} = datetime (@var{Y}, @var{MO}, @var{D}, @var{H}, @var{MI}, ## @var{S})} also adds time components to the constructed datetime arrays. ## @var{H}, @var{MI}, and @var{S} must be of the same size or scalars. ## @var{H} and @var{MI} must contain integer values, whereas @var{S} may ## also contain a fractional part. ## ## @code{@var{T} = datetime (@var{Y}, @var{MO}, @var{D}, @var{H}, @var{MI}, ## @var{S}, @var{MS})} also adds a milliseconds component, @var{MS}, which ## may also have a fractional part. ## ## @code{@var{T} = datetime (@var{X}, @qcode{'ConvertFrom'}, @var{TYPE})} ## converts the numeric values in @var{X} to a datetime array according to ## the data type specified by @var{TYPE}. The following types are ## supported: ## ## @itemize ## @item @qcode{'datenum'} ## @item @qcode{'excel'} ## @item @qcode{'excel1904'} ## @item @qcode{'posixtime'} ## @item @qcode{'epochtime'} ## @item @qcode{'juliandate'} ## @item @qcode{'modifiedjuliandate'} ## @item @qcode{'yyyymmdd'} ## @item @qcode{'ntp'} -- requires @code{uint64} input. ## @item @qcode{'ntfs'} -- requires @code{uint64} input. ## @item @qcode{'.net'} -- requires @code{uint64} input. ## @item @qcode{'tt2000'} -- requires @code{int64} input and the ## @qcode{'UTCLeapSeconds'} time zone (see @code{convertTo}). ## @end itemize ## ## @qcode{'Epoch'} (a scalar datetime marking tick zero, default ## @code{1970-01-01}) and @qcode{'TicksPerSecond'} (a positive scalar, ## default @code{1}) bear on @qcode{'epochtime'} alone; they are accepted ## alongside any other conversion and ignored, each of the others counting ## from an epoch of its own. ## ## Whether the count measures @strong{elapsed} or @strong{wall-clock} time ## follows from the epoch. When the epoch and @qcode{'TimeZone'} are ## @emph{both} zoned the count is elapsed time from a known instant, so ## @code{datetime (@var{X}, 'ConvertFrom', 'epochtime', 'Epoch', ## datetime (1970, 1, 1, 'TimeZone', 'UTC'), 'TimeZone', @var{Z})} agrees ## with @qcode{'posixtime'}. Otherwise the epoch is a wall clock, its zone ## dropped if it had one, and so is the count -- which is why the ## @strong{default} does not agree with @qcode{'posixtime'}: its epoch is ## unzoned, so @code{1730611800} names 05:30 on the local clock of whatever ## zone is asked for rather than the instant 05:30 UTC. That is not an ## inconsistency but the meaning of an epoch-relative count, and MATLAB ## reads it the same way. ## ## Each is the inverse of the @code{convertTo} conversion of the same name ## and treats an inserted leap second the way that conversion does, so the ## two round-trip. Where the conversion folded the inserted second away -- ## @qcode{'datenum'}, @qcode{'excel'}, @qcode{'excel1904'}, ## @qcode{'epochtime'}, @qcode{'ntp'}, @qcode{'ntfs'} and @qcode{'.net'} ## all do -- it cannot be recovered and the preceding second is returned, ## just as those conversions give the two instants of a repeated wall clock ## the same value. The Julian scales measure the fraction of a day against ## that day's own length and so keep an inserted second, returning ## @code{23:59:60} where one was given. A Julian day number is a large one ## and a double holds it to about 40 microseconds at present-day dates, ## which bounds the accuracy of @qcode{'juliandate'} in both directions; ## @qcode{'modifiedjuliandate'} counts from a nearer epoch and is some ## forty times finer. ## ## @strong{Deviation from MATLAB.} For a @qcode{'UTCLeapSeconds'} array, ## @qcode{'epochtime'} given an @qcode{'Epoch'} counts both operands on the ## same timeline here, while MATLAB folds the array onto the POSIX one and ## measures the epoch on the leap-second one, so its answer runs long by the ## seconds inserted before that epoch: counting @code{86399} from ## @code{2016-12-31} it returns @code{2017-01-01T00:00:25}, 26 seconds past ## the @code{23:59:59} that both consistent readings give. This is the same ## inconsistency already documented for @code{convertTo} in the other ## direction, and inverting it the same way is what makes the pair ## round-trip. ## ## @strong{Deviation from MATLAB.} For a @qcode{'UTCLeapSeconds'} array, ## MATLAB's @qcode{'juliandate'} does not invert its own ## @code{convertTo (@var{T}, 'juliandate')}: it returns an instant half a ## second later than the one it was given, on the day of an inserted second ## and for every element of it. Its @qcode{'modifiedjuliandate'} inverts ## correctly over the same day, so the two disagree with each other as well. ## The conversion here inverts exactly, in both scales. This is the same ## half-day slip already documented for @qcode{'tt2000'} between 1960 and ## 1972 in @code{convertTo}. ## ## @code{@var{T} = datetime (@var{D})}, where @var{D} is already a datetime ## array, copies it: the components, the @code{TimeZone} and the ## @code{Format} all carry over. A @qcode{'Format'} or @qcode{'TimeZone'} ## may still be given to change either, and take effect exactly as assigning ## those properties does, so attaching a zone to an unzoned array keeps its ## wall-clock values whereas changing between two zones keeps the instant. ## The options describing how text is read, @qcode{'InputFormat'}, ## @qcode{'Locale'} and @qcode{'PivotYear'}, have nothing to act on and are ## ignored, while @qcode{'ConvertFrom'} is an error. ## ## @code{@var{T} = datetime (@dots{}, @qcode{'Format'}, @var{FMT})} ## specifies the display format of the values in the output datetime array. ## @var{FMT} uses the same Unicode LDML date field symbols as ## @qcode{'InputFormat'}, @qcode{'z'}, @qcode{'Z'}, @qcode{'X'}, and ## @qcode{'x'} naming the time zone, and text between single quotes taken ## literally. The default format renders a date alone when ## every element sits at midnight and a date with a time otherwise; a ## @code{NaT} carries no time of day and does not affect that choice. ## ## @code{@var{T} = datetime (@dots{}, @qcode{'TimeZone'}, @var{TZ})} sets ## the time zone of the values in the output datetime array. If not ## specified, the array is unzoned: its values are wall-clock readings that ## name no absolute instant, and no daylight saving rule applies to them. ## Supported time zones are those of the IANA Time Zone Database, and ## @qcode{'local'}, in any letter case, names the system time zone given by ## @code{datetime.SystemTimeZone}, stored as naming that zone would store ## it. A fixed offset from UTC such as @qcode{'+05:30'} is accepted too; ## see the @code{TimeZone} property for its spellings. A zone may also be ## attached, changed, or dropped afterwards through the ## @qcode{'TimeZone'} property; attaching one reinterprets the wall-clock ## values in that zone, whereas changing between two zones preserves the ## absolute instant and shifts the wall-clock values by the difference in ## offset. ## ## Twice a year a zone that observes Daylight Saving Time (DST) has ## wall-clock readings that name no unique instant. Where the clock goes ## back an hour repeats, and such a reading is taken at the later of the two ## offsets, that is, standard time. Where the clock goes forward an ## interval is skipped, and a reading inside it is one no clock in that zone ## ever shows. Given as numeric components, such a reading is moved ahead ## by the length of the skipped interval, so that with the usual one-hour ## skip @qcode{'02:30'} becomes @qcode{'03:30'}; given as text it is instead ## rejected, since text is a claim about a reading that never occurred. A ## lone such string is an error, whereas within an array only that element ## is lost and becomes @code{NaT}; without an @qcode{'InputFormat'} the ## whole input is rejected, as when no format can be detected at all. ## ## In the @qcode{'UTCLeapSeconds'} zone the seconds component may reach 60, ## naming an inserted leap second, but only on one of the 27 dates that has ## one and only in the last minute of the day; anywhere else it rolls over ## as usual. The seconds component is counted along the leap-second ## timeline, so @qcode{'23:59:61'} on such a date is the next midnight ## rather than one second past it, whereas an hour or minute that overflows ## over the inserted second entirely. Text naming a leap second that was ## never inserted is rejected, exactly as text naming a wall clock a zone ## skips is. Without an @qcode{'InputFormat'} such an array reads only the ## ISO 8601 UTC shape it also writes. ## ## @strong{The representable range of years.} The calendar is proleptic ## Gregorian and its arithmetic is 64-bit, so an unzoned array holds any ## year its components can, and so do the @qcode{'UTC'} and ## @qcode{'UTCLeapSeconds'} zones, whose offset is zero at every instant. ## Every @emph{other} zone answers for any year as well, though beyond the ## IANA database's own range there is no zone @emph{data} to consult -- ## only the rule it carries for all future time. That rule is written as an ## nth weekday of a month, so it repeats exactly every 400 years, the period ## over which weekday and leap year both return to where they started; the ## offset for a distant year is therefore taken from the congruent year ## inside the database's range. MATLAB extrapolates in the same way and ## agrees period for period, up to its own limit of about year 144684, ## beyond which it raises where this class continues to answer. A date ## before any zone had a transition takes that zone's local mean time, ## again as MATLAB does within its range. In such a zone MATLAB also ## refuses a day component beyond the range of a 32-bit integer, whether ## given to the constructor or assigned, with an error calling it not an ## integer; this class accepts it and answers with the date and time it ## gives without a zone. ## ## @strong{Deviation from MATLAB} in the components of an @strong{infinite} ## datetime. Building one is documented behaviour and is followed exactly: ## a component of @code{Inf} or @code{-Inf} makes the datetime infinite, and ## one of @code{NaN} makes it @code{NaT}. What the components of such a ## value read back as is documented nowhere, and the two implementations ## differ. Here every component is that infinity, whichever one carried it ## and whatever its sign, so @code{ymd} and @code{datevec} answer with one ## value rather than two, the sign is visible in each, and the components ## rebuild the value they came from. MATLAB reports the year alone as ## infinite and @code{NaN} for the other five, a tuple its own constructor ## then reads as @code{NaT} -- so its components do not reconstruct the ## datetime they describe. Everything else agrees: @code{isnat}, ## @code{isinf}, the display, the ordering, and that @code{NaN} beats an ## infinity while two infinities of opposite sign name no moment at all. ## ## @strong{Deviation from MATLAB} in how wide a year may be written in ## text. MATLAB reads at most six significant digits there, whatever the ## format asks for, so it cannot read back a year past 999999 -- including ## one its own @code{writetable} has just written. Here the year is read as ## far as the calendar carries it, so that a file this class writes is one ## it can read. The divergence is additive: every string MATLAB accepts is ## read the same way, and only text it refuses outright is also accepted. ## ## @strong{Deviation from MATLAB} in how a very distant year is displayed. ## MATLAB stops honouring the requested display format about year 144684 -- ## where the instant it stores internally stops being exact to the ## millisecond -- and prints @qcode{'1000000 CE'} or @qcode{'1000000 BCE'} ## instead, text which cannot be read back even by giving an era field. ## Here the components are separate double values and lose nothing there, so ## the format asked for is always the format rendered. ## ## @strong{Deviations from MATLAB} when copying a datetime array and giving ## a @qcode{'TimeZone'}. MATLAB's constructor keeps the display format of ## the array it copies, while its @code{TimeZone} property assignment ## replaces it for the same change of zone; the two disagree, so here both ## follow the one rule, that of the property. Leaving ## @qcode{'UTCLeapSeconds'} therefore restores the ordinary default format ## rather than keeping the ISO 8601 pattern, whose @qcode{'Z'} would ## misdescribe any zone but UTC. For the same reason, entering ## @qcode{'UTCLeapSeconds'} works here, the locked format being applied, ## where MATLAB carries the copied format across and then rejects it as one ## that zone does not allow. ## ## @seealso{NaT, datetime, isdatetime, calendarDuration, duration} ## @end deftypefn function this = datetime (varargin) ## Return an scalar datetime object with current local time if (nargin == 0) [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second] = __datetime__ ('now'); this.Offset = zeros (size (this.Year)); return; endif ## Parse optional Name-Value paired arguments optNames = {'ConvertFrom', 'Epoch', 'Format', 'InputFormat', 'Locale', ... 'MixedFormats', 'PivotYear', 'TicksPerSecond', 'TimeZone'}; dfValues = {[], [], [], [], [], false, [], [], []}; [ConvertFrom, Epoch, Format, inputFormat, Locale, MixedFormats, ... PivotYear, TicksPerSecond, ... TimeZone, args] = parsePairedArguments (optNames, dfValues, varargin(:)); ## Only the first positional input can be text, so text after it is an ## option this constructor does not know. if (any (cellfun (@(x) ischar (x) || isa (x, 'string'), args(2:end)))) error ("datetime: invalid optional paired argument."); endif if (! ((islogical (MixedFormats) || isnumeric (MixedFormats)) && isscalar (MixedFormats))) error ("datetime: 'MixedFormats' must be a logical scalar."); endif MixedFormats = logical (MixedFormats); ## 'local' names the system time zone, which is then stored exactly as ## naming that zone would store it: 'Etc/UTC' as '+00:00'. if (ischar (TimeZone) && strcmpi (TimeZone, 'local')) TimeZone = datetime.SystemTimeZone; endif if (ischar (TimeZone) && ! isempty (TimeZone)) TimeZone = dtZoneName (TimeZone, 'datetime'); endif ## A datetime input is copied: its components, time zone and display ## format all carry over. 'Format' and 'TimeZone' may still be given to ## change either, and are applied by the same rules as assigning the ## properties -- attaching a zone to an unzoned array keeps its wall clock ## while changing between two zones keeps the instant. 'InputFormat', ## 'Locale' and 'PivotYear' describe how text is read and so have nothing ## to act on here; MATLAB ignores them and so do we. if (isa (args{1}, 'datetime')) if (numel (args) > 1) error (strcat ("datetime: a datetime array must be the only", ... " positional input.")); elseif (! isempty (ConvertFrom)) error (strcat ("datetime: 'ConvertFrom' cannot be used with a", ... " datetime array input.")); endif this = args{1}; ## Routing through subsasgn keeps one rule in the class for what a ## change of zone means. MATLAB has two: its constructor carries the ## copied display format across the change while its property assignment ## replaces it. So leaving 'UTCLeapSeconds' here restores the default ## format rather than keeping a pattern whose 'Z' would misdescribe any ## zone but UTC, and entering it works rather than failing on a format ## carried over from an array that never had leap seconds. Documented ## in the constructor's docstring. ## An unset option arrives as [], so an empty character vector is told ## apart by its class: given as '' it means drop the zone, which is not ## the same as saying nothing about it. if (ischar (TimeZone) || ! isempty (TimeZone)) this = subsasgn (this, substruct ('.', 'TimeZone'), TimeZone); endif if (! isempty (Format)) this = subsasgn (this, substruct ('.', 'Format'), Format); endif return; endif ## Nothing else non-numeric can name a date. A duration and a ## calendarDuration measure elapsed time rather than name an instant, so ## neither can be converted to one. if (! (isnumeric (args{1}) || islogical (args{1}) || ischar (args{1}) || iscellstr (args{1}) || isa (args{1}, 'string'))) error (strcat ("datetime: input data must be a numeric array, a", ... " string array, a cell array of character vectors,", ... " or a character vector.")); endif ## Six conversions that 'convertTo' performs are inverted in M-code. ## Five of them reduce exactly to a conversion the builtin already does, ## so they are rewritten into it here, ahead of the check below, and take ## that path unchanged: the fixed-epoch counts become POSIX seconds and ## the 1904 Excel date a serial date number. Only the two Julian scales ## in the leap-second zone cannot be rewritten -- a stretched Julian day ## can name an inserted second and neither POSIX time nor a date number ## can -- so those keep a branch of their own further down. ## 'Epoch' and 'TicksPerSecond' bear on 'epochtime' alone. MATLAB accepts ## them alongside any other conversion and quietly ignores them, and so do ## we: each of the others counts from an epoch of its own that the option ## has no business moving. An epoch of 1970-01-01 with one tick per ## second is what 'epochtime' already assumes, so the default path is left ## exactly as it was and only an explicit option is intercepted here. if (! isempty (Epoch)) if (! (isa (Epoch, 'datetime') && isscalar (Epoch))) error ("datetime: 'Epoch' must be a scalar datetime."); endif endif if (! isempty (TicksPerSecond)) if (! (isnumeric (TicksPerSecond) && isscalar (TicksPerSecond) && isreal (TicksPerSecond) && isfinite (TicksPerSecond) && TicksPerSecond > 0)) error (strcat ("datetime: 'TicksPerSecond' must be a positive", ... " finite scalar.")); endif endif if (! isempty (ConvertFrom) && dtIsTextScalar (ConvertFrom) && strcmpi (char (ConvertFrom), 'epochtime') && (! isempty (Epoch) || ! isempty (TicksPerSecond))) x = args{1}; if (! isnumeric (x)) error ("datetime: 'epochtime' input must be numeric."); endif secs = double (x); if (! isempty (TicksPerSecond)) secs /= double (TicksPerSecond); endif if (isempty (Epoch)) Epoch = datetime (1970, 1, 1); endif if (! isempty (Epoch.TimeZone) && ! isempty (TimeZone)) ## Both carry a zone, so the count measures ELAPSED time from a known ## instant and the answer is that instant advanced by it. This is the ## reading that makes an epoch of 1970-01-01 UTC agree with ## 'posixtime'; the wall-clock reading below is why the default, ## whose epoch is unzoned, does not. args{1} = posixtime (Epoch) + secs; ConvertFrom = 'posixtime'; else ## Either side unzoned: the epoch is a WALL CLOCK, its zone dropped if ## it had one, and the count is wall-clock time from it. Shifting the ## count by the epoch's own wall clock leaves the ordinary 'epochtime' ## path, which counts from 1970-01-01, to do the rest. [eY, eM, eD] = ymd (Epoch); [eh, em, es] = hms (Epoch); args{1} = secs + posixtime (datetime (eY, eM, eD, eh, em, es)); endif endif leapJulian = false; if (! isempty (ConvertFrom) && dtIsTextScalar (ConvertFrom)) cf = lower (char (ConvertFrom)); if (any (strcmp (cf, {'juliandate', 'modifiedjuliandate'})) && dtIsLeapZone (TimeZone)) leapJulian = true; elseif (any (strcmp (cf, {'excel1904', 'juliandate', ... 'modifiedjuliandate', 'ntp', 'ntfs', ... '.net'}))) [args{1}, ConvertFrom] = dtEpochAlias (args{1}, cf); endif endif ## Check optional 'Format' and 'InputFormat' arguments. 'yyyymmdd' and ## 'tt2000' are handled in M-code below, so they bypass the builtin ## ConvertFrom check, as does a Julian scale in the leap-second zone. if (! isempty (ConvertFrom) && ! leapJulian && ! any (strcmpi (ConvertFrom, {'yyyymmdd', 'tt2000'}))) ## Call __datetime__ to check for valid ConvertFrom string and ## data input [~,~,~,~,~,~,errmsg] = __datetime__ (args{:}, 'ConvertFrom', ... ConvertFrom); if (! isnumeric (errmsg)) error ("datetime: %s", errmsg); elseif (! isempty (inputFormat)) error ("datetime: 'ConvertFrom' cannot be used with 'InputFormat'."); endif endif if (! isempty (Format)) if (! (ischar (Format) && isvector (Format))) error ("datetime: 'Format' must be a character vector."); endif dtValidateFormat (Format); this.Format = Format; endif if (! isempty (inputFormat)) if (! (ischar (inputFormat) && isvector (inputFormat))) error ("datetime: 'InputFormat' must be a character vector."); elseif (! isempty (ConvertFrom)) error ("datetime: 'InputFormat' cannot be used with 'ConvertFrom'."); elseif (strcmpi (this.Format, 'preserveinput')) this.Format = inputFormat; endif endif if (! isempty (Locale)) if (! (ischar (Locale) && isvector (Locale))) error ("datetime: 'Locale' must be a character vector."); endif ## Validate against the supported locale set (errors if unsupported). dtLocaleNames (Locale); endif if (! isempty (PivotYear)) if (isempty (inputFormat)) error ("datetime: 'PivotYear' can only be used with 'InputFormat'."); elseif (! (isnumeric (PivotYear) && isscalar (PivotYear))) error ("datetime: 'PivotYear' must be a numeric scalar."); elseif (fix (PivotYear) != PivotYear) error ("datetime: 'PivotYear' must be an integer value."); endif endif if (! isempty (TimeZone)) ## Call __datetime__ to check for valid timezone string [~,~,~,~,~,~,errmsg] = __datetime__ (0, 0, 0, 'TimeZone', TimeZone); if (! isnumeric (errmsg)) error ("datetime: %s", errmsg); endif this.TimeZone = TimeZone; endif if (dtIsLeapZone (TimeZone)) ## Only one display pattern can render a 60th second, so a leap-second ## array carries it instead of the data-dependent default, and any other ## pattern the caller asked for is rejected. if (isempty (Format)) this.Format = dtLeapFormat (); else dtValidateLeapFormat (Format, 'datetime'); endif endif ## Datestrings are currently handled by 'datevec' if (iscellstr (args{1}) || isstring (args{1}) || ischar (args{1})) DateStrings = ""; ## The relative-day keywords are text too, but name no date to parse and ## are answered by the builtin further down. isRelDay = ischar (args{1}) && isrow (args{1}) ... && any (strcmpi (args{1}, {'now', 'today', 'yesterday', ... 'tomorrow'})); ## A format that reads an offset names an instant, which only a zone ## can hold, so it is refused without one before any text is read, as ## MATLAB refuses it. A leap-second array reads only the one pattern ## it can write. hasOff = false; if (! isempty (inputFormat) && ! isRelDay) dtValidateFormat (inputFormat); hasOff = any (ismember (__ldml__ ('symbols', inputFormat), 'zZXx')); if (dtIsLeapZone (TimeZone)) dtValidateLeapFormat (inputFormat, 'datetime', "'InputFormat'"); elseif (hasOff && isempty (TimeZone)) error (strcat ("datetime: 'InputFormat' '%s' has a time zone", ... " offset field, which requires 'TimeZone' to", ... " name a time zone."), inputFormat); endif endif if (ischar (args{1}) && ndims (args{1}) > 2) error ("datetime: invalid type for 'DateStrings'."); elseif (! isRelDay) if (ischar (args{1}) && rows (args{1}) == 0 && columns (args{1}) > 0) ## A character matrix with no rows holds no text at all, unlike '' ## which is one empty piece of it. Octave's 'cellstr' does not draw ## that line, reporting one empty string for either. DateStrings = cell (0, 1); else ## A character matrix holds one date per row, which is what ## 'cellstr' reads it as, trailing blanks trimmed. DateStrings = cellstr (args{1}); endif endif if (! isRelDay) if (isempty (DateStrings)) ## A container holding no text at all gives an empty datetime rather ## than a missing one: there is no element for a NaT to occupy. ## Note this is not the same as an empty character vector, which ## 'cellstr' reports as one piece of text and which becomes a NaT. [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second] = deal (nan (size (DateStrings))); this.Offset = nan (size (this.Year)); return; endif ## Empty text names no date at all. It is missing rather than ## unreadable, so it becomes NaT and leaves the text beside it alone, ## as MATLAB does: a blank field among imported dates must not cost ## the whole column. Only the rest is handed to a parser. strs = DateStrings(:); blank = cellfun (@isempty, strs); DATEVEC = nan (numel (strs), 6); OFFSET = nan (numel (strs), 1); if (! all (blank)) live = strs(! blank); if (! isempty (inputFormat)) ## LDML-aware parse under the supplied 'InputFormat'. MATLAB's ## default pivot for two-digit years is the current year minus 50. if (! isempty (PivotYear)) pivot = PivotYear; else now6 = clock (); pivot = now6(1) - 50; endif ## Blanks are gone from LIVE, so its length cannot say whether ## the input was a single string: an unreadable element beside a ## blank is NaT, as MATLAB does. A character matrix keeps the ## old count, since MATLAB refuses one with an unreadable row. lone = numel (DateStrings) == 1 ... || (ischar (args{1}) && numel (live) == 1); [DV, OFF] = dtParseInput (live, inputFormat, pivot, Locale, ... dtIsLeapZone (TimeZone), lone); OFFSET(! blank) = OFF; ## An array none of whose text can be read is refused as a whole, ## as MATLAB refuses it, unless it holds a blank element; a ## character matrix is refused whatever its blank rows. if (! lone && all (isnan (DV(:,1))) ... && (ischar (args{1}) || ! any (blank))) error (strcat ("datetime: could not parse the date/time", ... " text with 'InputFormat' '%s'."), inputFormat); endif elseif (dtIsLeapZone (TimeZone)) ## A leap-second array reads text in the one shape it can also ## write, so nothing is auto-detected here: the string must be ## the ISO 8601 UTC form, with or without fractional seconds. ## Anything else is rejected even where a general datetime array ## would accept it. DV = dtParseLeapText (live); elseif (MixedFormats) ## The Octave extension: read every string on its own terms, so a ## column gathered from several sources parses whatever each entry ## happens to be written in. Core 'datevec' detects per element ## and is what makes that possible; it also rolls an impossible ## date over rather than refusing it. fcn = @(x) datevec (x); try DV = cellfun (fcn, live, "UniformOutput", false); DV = cell2mat (DV(:)); catch error (strcat ("datetime: could not recognize date/time", ... " format from input.")); end_try_catch else ## No format supplied: detect one from the text and read every ## string with it, as MATLAB does. A string the detected format ## cannot read becomes NaT rather than costing the rest, which is ## the rule an explicit 'InputFormat' already follows; text no ## format can be detected from at all is refused outright. detected = dtDetectFormat (live, Locale); if (isempty (detected)) error (strcat ("datetime: could not recognize the", ... " date/time format of '%s'."), live{1}); endif ## Detected text carrying an offset follows the rule for an ## explicit offset field: it names an instant, so a zone is ## required rather than assumed. hasOff = any (ismember (__ldml__ ('symbols', detected), 'zZXx')); if (hasOff && isempty (TimeZone)) error (strcat ("datetime: the text '%s' carries a time", ... " zone offset, which requires 'TimeZone' to", ... " name a time zone."), live{1}); endif try [DV, OFF] = dtParseInput (live, detected, dtDefaultPivot (), ... Locale, dtIsLeapZone (TimeZone)); OFFSET(! blank) = OFF; ## The first text that is not blank is what the format was ## detected from, so text it cannot read leaves no format at ## all, whatever follows, as MATLAB has it. Caught below. if (isnan (DV(1,1))) error ("datetime: the first text is unreadable."); endif catch ## Only a lone string raises here, the array case losing just ## the offending element. Its message names an 'InputFormat' ## the caller never gave, so say instead what is true: the text ## could not be read. error (strcat ("datetime: could not recognize date/time", ... " format from input.")); end_try_catch endif DATEVEC(! blank, :) = DV; endif ## Text can name a year the calendar cannot represent as readily as ## numbers can, and the zoned normalization below is a timezone ## lookup like any other. ## Split DATEVEC into individual date/time units and reshape this.Year = reshape (DATEVEC(:,1), size (DateStrings)); this.Month = reshape (DATEVEC(:,2), size (DateStrings)); this.Day = reshape (DATEVEC(:,3), size (DateStrings)); this.Hour = reshape (DATEVEC(:,4), size (DateStrings)); this.Minute = reshape (DATEVEC(:,5), size (DateStrings)); this.Second = reshape (DATEVEC(:,6), size (DateStrings)); if (hasOff) ## The text names an instant: its wall clock less the offset it ## carries, read in 'TimeZone'. The offset settles a repeated or ## skipped wall clock, so nothing is resolved and nothing refused. [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second, this.Offset] = dtRezone (this.Year, this.Month, ... this.Day, this.Hour, this.Minute, this.Second, ... reshape (OFFSET, size (DateStrings)), '', this.TimeZone); return; endif if (! isempty (TimeZone)) ## Text naming a wall clock its zone never shows -- one inside the ## interval the clock skips going forward -- is rejected, matching ## MATLAB. (Numeric components are instead moved ahead by the gap; ## MATLAB draws that distinction too.) Normalizing in the zone ## moves exactly those elements, because every other parsed wall ## clock is already canonical, so an element that moves marks text ## to reject. The same test catches text naming a leap second that ## was never inserted: a genuine one survives normalization on the ## leap-second timeline untouched, a spurious one rolls over. if (dtIsLeapZone (TimeZone)) [Yz, Mz, Dz, Hz, MIz, Sz] = dtLeapNormalize (this.Year, ... this.Month, this.Day, this.Hour, ... this.Minute, this.Second); else [Yz, Mz, Dz, Hz, MIz, Sz] = __datetime__ (this.Year, ... this.Month, this.Day, this.Hour, ... this.Minute, this.Second, 'TimeZone', ... TimeZone, 'toTimeZone', TimeZone, ... 'Precision', 'microseconds'); endif skipped = (Yz != this.Year | Mz != this.Month | Dz != this.Day ... | Hz != this.Hour | MIz != this.Minute ... | abs (Sz - this.Second) > 1e-3) & ! isnan (this.Year); if (any (skipped(:))) if (isempty (inputFormat)) ## Without a format, an unreadable element condemns the whole ## input, as it does when 'datevec' fails to detect a format. error (strcat ("datetime: could not recognize date/time", ... " format from input.")); elseif (numel (skipped) == 1) error (strcat ("datetime: could not parse the date/time", ... " string '%s' with 'InputFormat' '%s'."), ... DateStrings{1}, inputFormat); endif this.Year(skipped) = NaN; this.Month(skipped) = NaN; this.Day(skipped) = NaN; this.Hour(skipped) = NaN; this.Minute(skipped) = NaN; this.Second(skipped) = NaN; endif endif this.Offset = dtOffsetOf (this.Year, this.Month, this.Day, ... this.Hour, this.Minute, this.Second, ... this.TimeZone); return; endif endif ## Handle inputs (no errors here). INSTOFF stays empty unless a branch ## builds from an instant and measures its own offset; see the choke ## point at the end of the constructor. instOff = []; if (! isempty (ConvertFrom) && strcmpi (ConvertFrom, 'yyyymmdd')) ## Decompose a YYYYMMDD integer into year, month, and day components ## and construct through the normal component path. x = args{1}; if (! isnumeric (x)) error ("datetime: 'yyyymmdd' input must be numeric."); endif Y = floor (x ./ 10000); M = floor (mod (x, 10000) ./ 100); D = mod (x, 100); dtCheckIntegerComponents ({Y, M, D}); if (! isempty (TimeZone)) [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second] = __datetime__ (Y, M, D, 'TimeZone', TimeZone, ... 'Precision', 'microseconds'); else [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second] = __datetime__ (Y, M, D, 'Precision', 'microseconds'); endif elseif (! isempty (ConvertFrom) && strcmpi (ConvertFrom, 'tt2000')) ## A tt2000 time is a count of SI seconds, so it can name an inserted ## second and only means anything in the leap-second zone. MATLAB ## insists on int64 input rather than rounding a double, since a double ## cannot hold a nanosecond count of this magnitude exactly. x = args{1}; if (! isa (x, 'int64')) error (strcat ("datetime: input for converting from tt2000 times", ... " must be int64.")); elseif (! dtIsLeapZone (TimeZone)) error (strcat ("datetime: to create datetimes from tt2000 times,", ... " the 'TimeZone' parameter must be", ... " 'UTCLeapSeconds'.")); endif [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second] = dtLeapComponents (dtTT20002Serial (x)); elseif (leapJulian) ## A Julian day of the leap-second zone measures the fraction of the ## day that has run against that day's OWN length, 86401 seconds where ## a second was inserted, so the scale can name 23:59:60 and inverting ## it recovers that second. The two scales do not share a day: a ## Julian day runs noon to noon and a Modified Julian day midnight to ## midnight, so on a day holding a leap second they divide different ## spans by different lengths and are NOT 2400000.5 apart. Going ## through one to reach the other would introduce half a second. x = args{1}; if (! isnumeric (x)) error ("datetime: '%s' input must be numeric.", lower (ConvertFrom)); endif [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second] = dtLeapComponents (dtJulian2Serial (double (x), ... strcmpi (ConvertFrom, 'modifiedjuliandate'))); elseif (! isempty (ConvertFrom) && ! isempty (TimeZone)) if (strcmpi (ConvertFrom, 'posixtime')) ## POSIX time is an absolute UTC instant, so read it as a UTC wall ## clock and then convert into the requested zone (honouring DST). [Yp, Mp, Dp, hp, mp, sp] = __datetime__ (args{1}, 'ConvertFrom', ... 'posixtime', 'Precision', ... 'microseconds'); [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second] = __datetime__ (Yp, Mp, Dp, hp, mp, sp, 'TimeZone', ... 'UTC', 'toTimeZone', TimeZone, 'Precision', ... 'microseconds'); ## This branch alone names an INSTANT rather than a wall clock, and ## which of the two moments sharing a clock on a fall-back day was ## meant is settled by the number given, not by resolve_local. So ## the offset is measured against that number here, and the ## wall-clock derivation at the end of the constructor skipped. instOff = round (__datetime__ (this.Year, this.Month, this.Day, ... this.Hour, this.Minute, this.Second, 'ConvertTo', ... 'posixtime', 'TimeZone', 'UTC', 'Precision', ... 'microseconds') - double (args{1})); else [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second] = __datetime__ (args{1}, 'ConvertFrom', ConvertFrom, ... 'TimeZone', TimeZone, 'Precision', ... 'microseconds'); endif elseif (! isempty (ConvertFrom) && isempty (TimeZone)) [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second] = __datetime__ (args{1}, 'ConvertFrom', ConvertFrom, ... 'Precision', 'microseconds'); else dtCheckIntegerComponents (args); if (dtIsLeapZone (TimeZone)) ## Let the builtin validate the shape of the positional arguments, ## then normalize on the leap-second timeline instead, where a 60th ## second of a minute exists and so must survive the round trip. [~,~,~,~,~,~, errmsg] = __datetime__ (args{:}, 'Precision', ... 'microseconds'); if (! isnumeric (errmsg)) error ("datetime: %s", errmsg); endif [Yr, Mr, Dr, hr, mir, sr] = dtSplitComponents (args); [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second] = dtLeapNormalize (Yr, Mr, Dr, hr, mir, sr); elseif (! isempty (TimeZone)) ## Normalize the wall clock in its own zone, so that a time the ## local clock never shows -- the gap when the clock goes forward -- ## moves ahead by the length of that gap, as MATLAB does. Round ## tripping through the zone leaves every other wall clock alone. [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second] = __datetime__ (args{:}, 'TimeZone', TimeZone, ... 'toTimeZone', TimeZone, ... 'Precision', 'microseconds'); else [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second] = __datetime__ (args{:}, 'Precision', 'microseconds'); endif endif ## Every branch above builds the array from a WALL CLOCK, so the offset ## follows from resolve_local's choices. Setting it here rather than in ## each branch keeps the size invariant (Offset always matches Year) at ## one choke point. The exception is a branch that was handed an instant ## and has already measured its own offset against it, which no reading ## of the resulting wall clock could recover. if (isempty (instOff)) this.Offset = dtOffsetOf (this.Year, this.Month, this.Day, ... this.Hour, this.Minute, this.Second, ... this.TimeZone); else this.Offset = instOff + zeros (size (this.Year)); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{cstr} =} dispstrings (@var{T}) ## ## Get display formatted strings for each element of a datetime object. ## ## @code{@var{cstr} = dispstrings (@var{T})} returns a cellstr array of ## character vectors, @var{cstr}, which has the same size as the input ## datetime @var{T}. ## ## @end deftypefn function cstr = dispstrings (this) ## Resolve the (possibly sentinel) Format to a concrete LDML pattern, ## then render every element under that single array-wide pattern. fmt = dtResolveFormat (this.Format, this.Hour, this.Minute, ... this.Second); cstr = dtFormatStrings (this.Year, this.Month, this.Day, this.Hour, ... this.Minute, this.Second, this.TimeZone, ... this.Offset, fmt, datetime.zoneNameStyle ()); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{cstr} =} cellstr (@var{T}) ## @deftypefnx {datetime} {@var{cstr} =} cellstr (@var{T}, @var{Format}) ## ## Convert datetime array to a cell array of character vectors. ## ## @code{@var{cstr} = cellstr (@var{T})} returns a cellstr array of ## character vectors, @var{cstr}, which has the same size as the input ## datetime @var{T}. ## ## @end deftypefn function cstr = cellstr (this, Format = '') if (! isempty (Format)) if (! (ischar (Format) && isvector (Format))) error ("datetime.cellstr: FORMAT must be a character vector."); endif this.Format = Format; endif cstr = dispstrings (this); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{cmat} =} char (@var{T}) ## @deftypefnx {datetime} {@var{cmat} =} char (@var{T}, @var{Format}) ## ## Convert datetime array to a character matrix. ## ## @code{@var{cmat} = char (@var{T})} returns a character matrix with one ## row per element in @var{T}. ## ## @end deftypefn function cmat = char (this, Format = '') cmat = char (cellstr (this, Format)); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {[@var{Y}, @var{M}, @var{D}] =} ymd (@var{T}) ## ## Year, Month, and Day components of datetime array. ## ## @code{[@var{Y}, @var{M}, @var{D}] = ymd (@var{T})} returns the year, ## month, and day components of the corresponding datetime values in @var{T} ## as separate numeric arrays. @var{Y}, @var{M}, @var{D} contain integer ## values and have the same size as @var{T}. Not-A-Time (@qcode{NaT}) ## values in @var{T} are returned as @qcode{NaN} in the output arrays. ## ## @end deftypefn function [Y, M, D] = ymd (this) Y = this.Year; M = this.Month; D = this.Day; endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {[@var{h}, @var{m}, @var{s}] =} hms (@var{T}) ## ## Hour, Minute, and Second components of a datetime array. ## ## @code{[@var{h}, @var{m}, @var{s}] = hms (@var{T})} returns the hour, ## minute, and second components of the corresponding datetime values in ## @var{T} as separate numeric arrays. @var{h} and @var{m} contain integer ## values and @var{s} may also contain a fractional part. All outputs have ## the same size as @var{T}. Not-A-Time (@qcode{NaT}) values in @var{T} are ## returned as @qcode{NaN} in the output arrays. ## ## @end deftypefn function [h, m, s] = hms (this) h = this.Hour; m = this.Minute; s = this.Second; endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{Y} =} year (@var{T}) ## ## Year component of a datetime array. ## ## @code{@var{Y} = year (@var{T})} returns the year number for each element ## of the input datetime array @var{T}. The output @var{Y} is a ## @qcode{double} array and it has the same size as @var{T}. Not-A-Time ## (@qcode{NaT}) values in @var{T} are returned as @qcode{NaN} in the output ## array. ## ## @end deftypefn function out = year (this) out = this.Year; endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{Q} =} quarter (@var{T}) ## ## Quarter component of a datetime array. ## ## @code{@var{Q} = quarter (@var{T})} returns the quarter number for each ## element of the input datetime array @var{T}. The output @var{Q} is a ## @qcode{double} array containing integer values in the range @math{[1, 4]} ## and it has the same size as @var{T}. Not-A-Time (@qcode{NaT}) values in ## @var{T} are returned as @qcode{NaN} in the output array. ## ## @end deftypefn function out = quarter (this) out = ceil (this.Month / 3); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{M} =} month (@var{T}) ## @deftypefnx {datetime} {@var{M} =} month (@var{T}, @var{monthType}) ## ## Month component of a datetime array. ## ## @code{@var{M} = month (@var{T})} returns the month number for each ## element of the input datetime array @var{T}. The output @var{M} is a ## @qcode{double} array containing integer values in the range ## @math{[1, 12]} and it has the same size as @var{T}. Not-A-Time ## (@qcode{NaT}) values in @var{T} are returned as @qcode{NaN} in the output ## array. ## ## @code{@var{M} = month (@var{T}, @var{monthType})} returns the month ## number or name for each element of the input datetime array @var{T} as ## specified by @var{monthType}, which may have any of the following ## options: ## ## @itemize ## @item @qcode{'monthofyear'} (default) returns the month number in a ## numeric array. ## @item @qcode{'name'} returns the corresponding full name of the month in ## a cell array of character vectors. ## @item @qcode{'shortname'} returns the corresponding 3-letter abbreviation ## of the month in a cell array of character vectors. ## @end itemize ## ## @end deftypefn function out = month (this, type = 'monthofyear') if (strcmpi (type, 'monthofyear')) out = this.Month; elseif (strcmpi (type, 'name')) mn = {'January', 'February', 'March', 'April', 'May', 'June', ... 'July', 'August', 'September', 'October', 'November', 'December'}; out = mn(this.Month); elseif (strcmpi (type, 'shortname')) mn = {'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', ... 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'}; out = mn(this.Month); else error ("datetime: unrecognized MONTHTYPE."); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{D} =} day (@var{T}) ## @deftypefnx {datetime} {@var{D} =} day (@var{T}, @var{dayType}) ## ## Day component of a datetime array. ## ## @code{@var{D} = day (@var{T})} returns the day number for each element of ## the input datetime array @var{T}. The output @var{D} is a @qcode{double} ## array containing integer values in the range @math{[1, 31]}, depending on ## the month and year, and it has the same size as @var{T}. Not-A-Time ## (@qcode{NaT}) values in @var{T} are returned as @qcode{NaN} in the output ## array. ## ## @code{@var{D} = day (@var{T}, @var{dayType})} returns the day number or ## name for each element of the input datetime array @var{T} as specified by ## @var{dayType}, which may have any of the following options: ## ## @itemize ## @item @qcode{'dayofmonth'} (default) returns the day-of-month number in a ## numeric array. Depending on the month and year, it can range from 1 to ## 28, 29, 30, or 31. ## @item @qcode{'dayofweek'} returns the day-of-week number in a numeric ## array of @qcode{double} integer values ranging from 1 to 7, where 1 ## corresponds to Sunday. ## @item @qcode{'iso-dayofweek'} returns the day-of-week number in a numeric ## array of @qcode{double} integer values ranging from 1 to 7, where 1 ## corresponds to Monday according to the ISO 8601 standard. ## @item @qcode{'dayofyear'} returns the day-of-year number in a numeric ## array of @qcode{double} integer values ranging from 1 to 365 or 366, ## depending on the year. ## @item @qcode{'name'} returns the corresponding full name of the day in ## a cell array of character vectors. ## @item @qcode{'shortname'} returns the corresponding 3-letter abbreviation ## of the day in a cell array of character vectors. ## @end itemize ## ## @end deftypefn function out = day (this, type = 'dayofmonth') vtypes = {'dayofweek', 'iso-dayofweek', 'name', 'shortname'}; if (strcmpi (type, 'dayofmonth')) out = this.Day; elseif (any (strcmpi (type, vtypes))) m = this.Month - 2; y = this.Year; ## Compute the Jan/Feb borrow mask once, before mutating 'm'; adjusting ## 'm' first would clear the mask before 'y' is decremented. janfeb = m < 1; m(janfeb) += 12; y(janfeb) -= 1; K = mod (y, 100); J = floor (y ./ 100); code = floor ((26 .* m - 2) ./ 10); out = mod ((this.Day + code + K + floor (K ./ 4) ... + floor (J ./ 4) + 5 .* J), 7) + 1; if (strcmpi (type, 'iso-dayofweek')) out = mod (out - 1, 7); out(out == 0) = 7; elseif (strcmpi (type, 'name')) dn = {'Sunday', 'Monday', 'Tuesday', 'Wednesday', ... 'Thursday', 'Friday', 'Saturday'}; out = dn(out); elseif (strcmpi (type, 'shortname')) dn = {'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'}; out = dn(out); endif elseif (strcmpi (type, 'dayofyear')) m = this.Month; y = this.Year; ## Column vector so that logical-indexed lookups keep column shape. cumdays = [0; 31; 59; 90; 120; 151; 181; 212; 243; 273; 304; 334]; isly = mod (y, 4) == 0 & (mod (y, 100) != 0 | mod (y, 400) == 0); out = nan (size (m)); valid = isfinite (m) & isfinite (this.Day); out(valid) = cumdays(m(valid)) + this.Day(valid)(:) ... + double (isly(valid) & m(valid) > 2)(:); else error ("datetime: unrecognized DAYTYPE."); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{h} =} hour (@var{T}) ## ## Hour component of a datetime array. ## ## @code{@var{h} = hour (@var{T})} returns the hour number for each element ## of the input datetime array @var{T}. The output @var{h} is a ## @qcode{double} array containing integer values in the range ## @math{[0, 23]} and it has the same size as @var{T}. Not-A-Time ## (@qcode{NaT}) values in @var{T} are returned as @qcode{NaN} in the output ## array. ## ## @end deftypefn function out = hour (this) out = this.Hour; endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{m} =} minute (@var{T}) ## ## Minute component of a datetime array. ## ## @code{@var{m} = minute (@var{T})} returns the minute number for each ## element of the input datetime array @var{T}. The output @var{m} is a ## @qcode{double} array containing integer values in the range ## @math{[0, 59]} and it has the same size as @var{T}. Not-A-Time ## (@qcode{NaT}) values in @var{T} are returned as @qcode{NaN} in the output ## array. ## ## @end deftypefn function out = minute (this) out = this.Minute; endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{s} =} second (@var{T}) ## @deftypefnx {datetime} {@var{s} =} second (@var{T}, @var{secondType}) ## ## Seconds component of a datetime array. ## ## @code{@var{s} = second (@var{T})} returns the number of seconds for each ## element of the input datetime array @var{T}. The output @var{s} is a ## @qcode{double} array containing values in the range @math{[0, 60)}, ## including any fractional part of the second, and it has the same size as ## @var{T}. Not-A-Time (@qcode{NaT}) values in @var{T} are returned as ## @qcode{NaN} in the output array. ## ## @code{@var{s} = second (@var{T}, @var{secondType})} returns the seconds ## for each element of the input datetime array @var{T} as specified by ## @var{secondType}, which may have any of the following options: ## ## @itemize ## @item @qcode{'secondofminute'} (default) returns the second of the minute ## in a numeric array, in the range @math{[0, 60)}. ## @item @qcode{'secondofday'} returns the second of the day in a numeric ## array, in the range @math{[1, 86400)}. ## @end itemize ## ## @end deftypefn function out = second (this, secondType = 'secondofminute') if (strcmpi (secondType, 'secondofminute')) out = this.Second; elseif (strcmpi (secondType, 'secondofday')) out = this.Hour * 3600 + this.Minute * 60 + this.Second; else error ("datetime.second: unrecognized SECONDTYPE."); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{W} =} week (@var{T}) ## @deftypefnx {datetime} {@var{W} =} week (@var{T}, @var{weekType}) ## ## Week component of a datetime array. ## ## @code{@var{W} = week (@var{T})} returns the week-of-year number for each ## element of the input datetime array @var{T}. The output @var{W} is a ## @qcode{double} array containing integer values in the range ## @math{[1, 54]} and it has the same size as @var{T}. Weeks are counted ## from Sunday to Saturday, and the week containing January 1 is week 1. ## Not-A-Time (@qcode{NaT}) values in @var{T} are returned as @qcode{NaN} ## in the output array. ## ## @code{@var{W} = week (@var{T}, @var{weekType})} returns the week number ## for each element of the input datetime array @var{T} as specified by ## @var{weekType}, which may have any of the following options: ## ## @itemize ## @item @qcode{'weekofyear'} (default) returns the week of the year in a ## numeric array, in the range @math{[1, 54]}. ## @item @qcode{'weekofmonth'} returns the week of the month in a numeric ## array, in the range @math{[1, 6]}. ## @end itemize ## ## @end deftypefn function out = week (this, weekType = 'weekofyear') ## Weekday (Sun = 1 @dots{} Sat = 7) of each element, NaN for NaT. dow = day (this, 'dayofweek'); if (strcmpi (weekType, 'weekofyear')) D = day (this, 'dayofyear'); ## 0-based weekday of January 1 derived from this element's own ## weekday and day-of-year (0 = Sunday @dots{} 6 = Saturday). w0 = mod ((dow - 1) - (D - 1), 7); out = floor ((D - 1 + w0) ./ 7) + 1; elseif (strcmpi (weekType, 'weekofmonth')) dm = this.Day; w0 = mod ((dow - 1) - (dm - 1), 7); out = floor ((dm - 1 + w0) ./ 7) + 1; else error ("datetime.week: unrecognized WEEKTYPE."); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{D} =} timeofday (@var{T}) ## ## Elapsed time since midnight of a datetime array. ## ## @code{@var{D} = timeofday (@var{T})} returns the elapsed time since ## midnight for each element of the input datetime array @var{T} as a ## @code{duration} array @var{D} of the same size as @var{T}. For datetime ## arrays with a time zone, the result accounts for any daylight saving time ## shift occurring since midnight, so it may differ from the displayed clock ## time on the day of a transition. Not-A-Time (@qcode{NaT}) values in ## @var{T} are returned as @qcode{NaN} durations. ## ## @end deftypefn function out = timeofday (this) out = this - dateshift (this, 'start', 'day'); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{DT} =} tzoffset (@var{T}) ## @deftypefnx {datetime} {[@var{DT}, @var{DST}] =} tzoffset (@var{T}) ## ## Time zone offset of a datetime array. ## ## @code{@var{DT} = tzoffset (@var{T})} returns the offset from UTC of each ## element of the input datetime array @var{T} as a @code{duration} array ## @var{DT} of the same size as @var{T}. The offset is positive for time ## zones east of UTC and includes the additional hour when daylight saving ## time is in effect. If @var{T} has no time zone, or for Not-A-Time ## (@qcode{NaT}) and infinite values, the corresponding offset is ## @qcode{NaN}. ## ## @code{[@var{DT}, @var{DST}] = tzoffset (@var{T})} also returns the ## daylight saving time part of each offset as a @code{duration} array ## @var{DST}: zero in standard time and in a time zone without daylight ## saving time, and @qcode{NaN} wherever @var{DT} is. Where the IANA ## database writes the winter of a time zone as a negative saving, as for ## @qcode{'Europe/Dublin'} or @qcode{'Africa/Casablanca'}, the other half of ## the year is counted as the daylight saving time instead, and ## @code{isdst} agrees. ## ## @end deftypefn function [out, dst] = tzoffset (this) if (isempty (this.TimeZone)) secs = nan (size (this)); dsecs = secs; else ## The stored offset is the answer already, and reading it rather than ## resolving the wall clock again is what tells the two passes over a ## repeated clock apart: the tz database would give both the later one. ## An element naming no instant has no offset, whatever is stored. secs = this.Offset + zeros (size (this.Year)); secs(! isfinite (this.Year)) = NaN; if (nargout > 1) dsecs = dtDstOffset (this.Year, this.Month, this.Day, this.Hour, ... this.Minute, this.Second, this.TimeZone, ... this.Offset); dsecs(! isfinite (this.Year)) = NaN; endif endif out = duration (0, 0, secs); out.Format = 'hh:mm'; if (nargout > 1) dst = duration (0, 0, dsecs); dst.Format = 'hh:mm'; endif endfunction endmethods ################################################################################ ## ** Summary Information ** ## ################################################################################ ## Available Methods ## ## ## ## 'size' 'ndims' 'numel' 'length' ## ## 'keyHash' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{sz} =} size (@var{T}) ## @deftypefnx {datetime} {@var{dim_sz} =} size (@var{T}, @var{dim}) ## @deftypefnx {datetime} {@var{dim_sz} =} size (@var{T}, @var{d1}, @var{d2}, @dots{}) ## @deftypefnx {datetime} {[@var{rows}, @var{columns}, @dots{}, @var{dim_n_sz}] =} size (@dots{}) ## ## Size of a datetime array. ## ## @code{@var{sz} = size (@var{T})} returns a row vector with the size ## (number of elements) of each dimension for the datetime array @var{T}. ## ## @code{@var{dim_sz} = size (@var{T}, @var{dim})} returns the size of ## the corresponding dimension specified in @var{dim}. If @var{dim} is a ## vector, then @var{dim_sz} is a vector of the same length and with each ## element corresponding to a specified dimension. Multiple dimensions may ## also be specified as separate arguments. ## ## With a single output argument, @code{size} returns a row vector. With ## several, the size of dimension N is returned in the Nth argument. ## Asking for fewer output arguments than the array has dimensions folds ## the trailing dimensions into the last one, so @code{[r, c] = size ## (@var{T})} on a 2-by-2-by-2 array returns 2 and 4; asking for more pads ## with ones. When a dimension is named, however, the number of output ## arguments must equal the number of dimensions requested. ## ## @end deftypefn function varargout = size (this, varargin) if (! isempty (varargin)) ## A named dimension fixes how many outputs there may be. sz = size (this.Year, varargin{:}); if (nargout > 1 && numel (sz) != nargout) error (strcat ("datetime.size: nargout does not match the", " number of requested dimensions.")); endif else sz = size (this.Year); ## With no dimension named, asking for more outputs than the array has ## dimensions pads with ones, and asking for fewer folds the trailing ## dimensions into the last output, as core Octave and MATLAB both do. if (nargout > numel (sz)) sz = [sz, ones(1, nargout - numel (sz))]; elseif (nargout > 1 && nargout < numel (sz)) sz = [sz(1:nargout-1), prod(sz(nargout:end))]; endif endif if (nargout == 0 || nargout == 1) varargout{1} = sz; else for i = 1:nargout varargout{i} = sz(i); endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{out} =} ndims (@var{T}) ## ## Number of dimensions in a datetime array. ## ## @code{@var{out} = ndims (@var{T})} returns the number of dimensions of ## the datetime array @var{T}. ## ## @end deftypefn function out = ndims (this) out = ndims (this.Year); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{out} =} numel (@var{T}) ## ## Total number of elements in a datetime array. ## ## @code{@var{out} = numel (@var{T})} returns the number of elements in the ## datetime array @var{T}, which is the product of the sizes of its ## dimensions. ## ## @end deftypefn function out = numel (this, varargin) out = prod (size (this)); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{N} =} length (@var{T}) ## ## Length of a datetime vector. ## ## @code{@var{N} = length (@var{T})} returns the size of the longest ## dimension of the datetime array @var{T}, unless any of its dimensions ## has zero length, in which case @code{length (@var{T})} returns 0. ## ## @end deftypefn function N = length (this) if (isempty (this.Year)) N = 0; else N = max (size (this.Year)); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{key} =} keyHash (@var{T}) ## @deftypefnx {datetime} {@var{key} =} keyHash (@var{T}, @var{base}) ## ## Generate a hash code for datetime array. ## ## @code{@var{key} = keyHash (@var{T})} generates a @qcode{uint64} scalar ## that represents the input array @var{T}. @code{keyHash} utilizes the ## 64-bit FNV-1a variant of the Fowler-Noll-Vo non-cryptographic hash ## function. ## ## @code{@var{key} = keyHash (@var{T}, @var{base})} also generates a 64-bit ## hash code using @var{base} as the offset basis for the FNV-1a hash ## algorithm. @var{base} must be a @qcode{uint64} integer type scalar. Use ## this syntax to cascade @code{keyHash} on multiple objects for which a ## single hash code is required. ## ## A datetime array is keyed on the @emph{instant} its elements name and ## not on their wall clock and time zone, so the same moment expressed in ## two zones has the same hash code. An unzoned array, a zoned array and ## a leap-second array are three separate frames that never share a key, ## whatever instants they name, and the two instants sharing a wall clock ## across a daylight-saving fall-back stay distinct. The @qcode{Format} ## property is display only and is not part of the key. Two arrays that ## @code{keyMatch} reports as the same key always hash alike. ## ## Note that unlike MATLAB, this implementation does not use any random ## seed. As a result, @code{keyHash} will always generate the exact same ## hash key for any particular input across different workers and Octave ## sessions. ## ## @end deftypefn function key = keyHash (this, base = []) ## Initialize string with size, class name, and time frame. The zone ## NAME is deliberately absent, because two zoned arrays naming the same ## instant are the same key however they spell their zone. What stays ## is the frame, and it has THREE states rather than a zoned/unzoned ## boolean: 'isequaln' refuses to equate an unzoned array with a zoned ## one, and a leap-second array with an ordinary one, whatever instant ## either names, so all three have to be kept apart. A plain boolean ## would also let an unzoned array collide with a UTC one outright, ## since 'serial' returns the same count for both. size_str = sprintf ('%dx', size (this.Year))(1:end-1); if (isempty (this.TimeZone)) flag_str = '-unzoned:'; elseif (dtIsLeapZone (this.TimeZone)) flag_str = '-leapzone:'; else flag_str = '-zoned:'; endif init_str = [size_str class(this) flag_str]; if (! isempty (base)) if (! (isscalar (base) && isa (base, 'uint64'))) error ("datetime.keyHash: BASE must be a UINT64 scalar."); endif key = __ckeyHash__ (init_str, base); else key = __ckeyHash__ (init_str); endif if (! isempty (this.Year)) ## The instant, not the wall clock. 'serial' is the count the ## instant-based comparisons are already built on, and within each of ## the three frames it is in one-to-one correspondence with what ## 'isequaln' compares -- so agreement between keyHash and keyMatch is ## structural here rather than a numerical coincidence. It also does ## the aligning that 'do_isequal' does by assigning TimeZone = 'UTC', ## which is not available inside a method: that syntax writes the ## property directly and would skip the conversion the setter makes. ## The stored Offset is what 'serial' consumes, so it must not be ## hashed as well -- doing so would re-separate the zones this is ## meant to bring together, which is the defect being fixed. key = __nkeyHash__ (serial (this)(:), key); endif endfunction endmethods ################################################################################ ## ** Convert to other Numeric Representations ** ## ################################################################################ ## Available Methods ## ## ## ## 'convertTo' 'exceltime' 'posixtime' 'juliandate' ## ## 'yyyymmdd' 'datevec' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{P} =} posixtime (@var{T}) ## ## Convert datetime array to POSIX time. ## ## @code{@var{P} = posixtime (@var{T})} returns the number of seconds ## elapsed since the epoch @code{1970-01-01 00:00:00 UTC} for each element ## of the input datetime array @var{T}. The output @var{P} is a ## @qcode{double} array of the same size as @var{T} and includes any ## fractional seconds. Datetime arrays without a time zone are treated as ## UTC. Not-A-Time (@qcode{NaT}) values are returned as @qcode{NaN}. ## ## POSIX time has no stamp of its own for an inserted leap second, so for a ## @qcode{'UTCLeapSeconds'} array the 60th second of a minute shares the ## stamp of the second that follows it: @code{posixtime} of ## @code{2016-12-31T23:59:60Z} and of @code{2017-01-01T00:00:00Z} are both ## @code{1483228800}. Every other conversion folds the other way (see ## @code{convertTo}). ## ## @end deftypefn function out = posixtime (this) if (dtIsLeapZone (this.TimeZone)) out = dtLeapPosix (this.Year, this.Month, this.Day, this.Hour, ... this.Minute, this.Second); else out = serial (this); endif endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{DV} =} datevec (@var{T}) ## ## Convert datetime array to date vectors. ## ## @code{@var{DV} = datevec (@var{T})} returns a numeric matrix with one row ## per element in @var{T}. ## ## @end deftypefn function varargout = datevec (this) DV = [this.Year(:), this.Month(:), this.Day(:), ... this.Hour(:), this.Minute(:), this.Second(:)]; if (nargout == 0 || nargout == 1) varargout{1} = DV; elseif (nargout <= 6) for i = 1:nargout varargout{i} = DV(:,i); endfor else error ("datetime.datevec: too many output arguments."); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{S} =} datestr (@var{T}) ## @deftypefnx {datetime} {@var{S} =} datestr (@var{T}, @var{f}) ## @deftypefnx {datetime} {@var{S} =} datestr (@dots{}, @var{opt}) ## ## Convert datetime array to a character array of date strings. ## ## @code{@var{S} = datestr (@var{T})} converts the datetime array @var{T} to ## a character array @var{S} with one date string per row, using the default ## format of the core @code{datestr} function. ## ## @code{@var{S} = datestr (@var{T}, @var{f})} uses the format specified by ## @var{f}, a format number or a format string that follows the ## @emph{legacy} @code{datestr} field codes, in which @qcode{'mm'} denotes ## months and @qcode{'MM'} denotes minutes. Any further arguments @var{opt} ## are passed on to the core @code{datestr} function. ## ## @code{datestr} is provided for compatibility with legacy code. It ## renders the wall-clock components of @var{T} and, for a zoned array, does ## not include the time zone. For time-zone-aware formatting with the ## modern Unicode (LDML) field codes, use @code{char}, @code{cellstr}, or ## set the @qcode{Format} property of @var{T} instead. ## ## The legacy field codes have no way to write a 60th second, so for a ## @qcode{'UTCLeapSeconds'} array an inserted second folds backward onto the ## 59th, as it does in @code{datenum}. ## ## @end deftypefn function S = datestr (this, varargin) DV = datevec (this); if (dtIsLeapZone (this.TimeZone)) DV(:,6) = dtLeapBackFold (DV(:,6)); endif S = datestr (DV, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{S} =} string (@var{T}) ## @deftypefnx {datetime} {@var{S} =} string (@var{T}, @var{fmt}) ## ## Convert datetime array to a string array. ## ## @code{@var{S} = string (@var{T})} converts the datetime array @var{T} to ## a @code{string} array @var{S} of the same size, formatting each element ## with the display format of @var{T} (its @qcode{Format} property). ## Not-A-Time (@qcode{NaT}) values become missing string elements. ## ## @code{@var{S} = string (@var{T}, @var{fmt})} formats each element with ## the Unicode (LDML) format @var{fmt} instead of the @qcode{Format} ## property of @var{T} (see @code{char}). ## ## @end deftypefn function s = string (this, fmt) if (nargin < 2) cstr = cellstr (this); else cstr = cellstr (this, fmt); endif nat = isnat (this); if (any (nat, 'all')) cstr(nat) = {missing}; endif s = string (cstr); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{D} =} yyyymmdd (@var{T}) ## ## Convert datetime array to @code{YYYYMMDD} numeric form. ## ## @code{@var{D} = yyyymmdd (@var{T})} returns a @qcode{double} array ## @var{D} of the same size as @var{T} in which each element encodes the ## date of the corresponding datetime as @code{@var{year} * 10000 + ## @var{month} * 100 + @var{day}}. The time of day is ignored. Not-A-Time ## (@qcode{NaT}) values are returned as @qcode{NaN}. ## ## @end deftypefn function out = yyyymmdd (this) out = this.Year * 10000 + this.Month * 100 + this.Day; endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{N} =} datenum (@var{T}) ## ## Convert datetime array to serial date numbers. ## ## @code{@var{N} = datenum (@var{T})} returns a @qcode{double} array @var{N} ## of the same size as @var{T} holding the serial date number of each ## element, where @code{1} corresponds to January 1 of the year 0000 and the ## fractional part represents the time of day. The time zone of a zoned ## @var{T} is ignored; its wall-clock components are used. Not-A-Time ## (@qcode{NaT}) values are returned as @qcode{NaN}, and infinite datetimes ## preserve their sign. ## ## A serial date number has no room for an inserted leap second, so for a ## @qcode{'UTCLeapSeconds'} array the 60th second of a minute folds backward ## onto the 59th: @code{datenum} of @code{2016-12-31T23:59:60Z} equals that ## of @code{2016-12-31T23:59:59Z}. ## ## @end deftypefn function out = datenum (this) Y = this.Year; M = this.Month; D = this.Day; h = this.Hour; mi = this.Minute; s = this.Second; if (dtIsLeapZone (this.TimeZone)) s = dtLeapBackFold (s); endif out = nan (size (Y)); ## Core datenum errors on NaN components, so screen NaT out first and let ## infinite datetimes carry their sign through unchanged. fin = isfinite (Y); out(fin) = datenum (Y(fin), M(fin), D(fin), h(fin), mi(fin), s(fin)); out(isinf (Y)) = Y(isinf (Y)); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{E} =} exceltime (@var{T}) ## @deftypefnx {datetime} {@var{E} =} exceltime (@var{T}, @var{dateType}) ## ## Convert datetime array to Excel serial date numbers. ## ## @code{@var{E} = exceltime (@var{T})} returns a @qcode{double} array ## @var{E} of the same size as @var{T} holding the Excel serial date number ## of each element, using the Excel 1900 date system, in which the ## fractional part represents the time of day. As in Excel, the year 1900 ## is treated as a leap year, so serial numbers on or after 1900-03-01 ## account for the nonexistent date 1900-02-29. ## ## @code{@var{E} = exceltime (@var{T}, @var{dateType})} selects the date ## system: @qcode{'1900'} (default) or @qcode{'1904'}. The 1904 system ## counts days from 1904-01-01 and has no leap-year anomaly, so a date ## before 1900-03-01 is a plain day count and its serial is one greater than ## MATLAB returns; MATLAB derives the 1904 serial from the 1900 one and ## carries the nonexistent 1900-02-29 into a system that never had it. ## Excel stores no negative serial in either system, so this affects only ## dates neither can represent, where the value returned here is the one ## @code{datetime (@var{E}, 'ConvertFrom', 'excel1904')} converts back ## exactly. ## ## The time zone of a zoned @var{T} is ignored; its wall-clock components ## are used. Not-A-Time (@qcode{NaT}) values are returned as @qcode{NaN}. ## ## @end deftypefn function out = exceltime (this, dateType = '1900') if (! (ischar (dateType) && isrow (dateType) ... && any (strcmpi (dateType, {'1900', '1904'})))) error ("datetime.exceltime: DATETYPE must be '1900' or '1904'."); endif ## Counted from Excel's OWN epoch rather than through 'datenum', whose ## epoch lies 1900 years further off: a double's resolution follows the ## magnitude it holds, so going the long way round cost sixteen times the ## precision and put the time of day several microseconds out. Y = this.Year; M = this.Month; D = this.Day; h = this.Hour; mi = this.Minute; sec = this.Second; if (dtIsLeapZone (this.TimeZone)) sec = dtLeapBackFold (sec); endif fin = isfinite (Y); dayNo = nan (size (Y)); dayNo(fin) = dtDaysFromCivil (Y(fin), M(fin), D(fin)); frac = zeros (size (Y)); frac(fin) = (h(fin) .* 3600 + mi(fin) .* 60 + sec(fin)) ./ 86400; out = nan (size (Y)); if (strcmpi (dateType, '1904')) ## The 1904 system has no leap-year anomaly, so this is a plain day ## count. MATLAB routes it through the 1900 serial and so returns one ## less for a date before 1900-03-01 -- a region outside both systems, ## and one MATLAB's own 'ConvertFrom' refuses to convert back. out(fin) = dayNo(fin) - dtDaysFromCivil (1904, 1, 1) + frac(fin); else ## Excel's 1900 system counts days from the serial-0 epoch 1899-12-30, ## but wrongly treats 1900 as a leap year, so serials on or after ## 1900-03-01 are one greater, spanning the nonexistent 1900-02-29. out(fin) = dayNo(fin) - dtDaysFromCivil (1899, 12, 30) + frac(fin); early = fin & (dayNo < dtDaysFromCivil (1900, 3, 1)); out(early) -= 1; endif out(isinf (Y)) = Y(isinf (Y)); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{J} =} juliandate (@var{T}) ## @deftypefnx {datetime} {@var{J} =} juliandate (@var{T}, @var{dateType}) ## ## Convert datetime array to Julian dates. ## ## @code{@var{J} = juliandate (@var{T})} returns a @qcode{double} array ## @var{J} of the same size as @var{T} holding the Julian date of each ## element, that is, the number of days (including a fractional part) since ## noon UTC on January 1, 4713 BCE. Unlike @code{datenum}, the result is ## based on the absolute UTC instant, so the time zone of a zoned @var{T} is ## taken into account; datetime arrays without a time zone are treated as ## UTC. Not-A-Time (@qcode{NaT}) values are returned as @qcode{NaN}. ## ## @code{@var{J} = juliandate (@var{T}, @var{dateType})} selects the kind of ## Julian date: @qcode{'juliandate'} (default) or ## @qcode{'modifiedjuliandate'}, the latter being the Julian date minus ## @code{2400000.5}. ## ## For a @qcode{'UTCLeapSeconds'} array a Julian day that holds an inserted ## second is 86401 seconds long, and the fractional part is that fraction of ## the day's true length. Julian days run from noon to noon and modified ## Julian days from midnight to midnight, so on such a day the two are ## stretched over different spans and are not related by exactly ## @code{2400000.5}; both are @qcode{'juliandate'} for the day at hand. ## ## @end deftypefn function out = juliandate (this, dateType = 'juliandate') if (! (ischar (dateType) && isrow (dateType) && any (strcmpi (dateType, {'juliandate', ... 'modifiedjuliandate'})))) error (strcat ("datetime.juliandate: DATETYPE must be 'juliandate'", ... " or 'modifiedjuliandate'.")); endif if (dtIsLeapZone (this.TimeZone)) out = dtLeapJulian (this.Year, this.Month, this.Day, this.Hour, ... this.Minute, this.Second, ... strcmpi (dateType, 'modifiedjuliandate')); return; endif ## Julian date of the Unix epoch (1970-01-01 00:00 UTC) is 2440587.5; the ## POSIX instant places the array on the absolute UTC timeline. ## Each scale counts from its own epoch. Reaching the Modified scale by ## subtracting 2400000.5 from the other loses about 14 microseconds to ## cancellation, the Julian day number being large enough to eat the ## fraction's last digits, so the two are counted separately here. Away ## from the leap-second zone the scales do share a day and the constant ## relates them exactly; on a day holding an inserted second they do not, ## which is the same reason the inverse keeps them apart. if (strcmpi (dateType, 'modifiedjuliandate')) out = posixtime (this) / 86400 + 40587; else out = posixtime (this) / 86400 + 2440587.5; endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{X} =} convertTo (@var{T}, @var{dateType}) ## @deftypefnx {datetime} {@var{X} =} convertTo (@var{T}, @qcode{'epochtime'}, @var{Name}, @var{Value}) ## ## Convert datetime array to a numeric representation. ## ## @code{@var{X} = convertTo (@var{T}, @var{dateType})} converts the ## datetime array @var{T} to the numeric date/time representation named by ## @var{dateType}, returning an array @var{X} of the same size as @var{T}. ## It is a convenience wrapper around the individual conversion methods. ## @var{dateType} may be one of: ## ## @itemize ## @item @qcode{'datenum'} -- serial date number, @code{double} (see ## @code{datenum}). ## @item @qcode{'excel'} or @qcode{'excel1900'} -- Excel serial date, 1900 ## system, @code{double} (see @code{exceltime}). ## @item @qcode{'excel1904'} -- Excel serial date, 1904 system, ## @code{double}. ## @item @qcode{'juliandate'} -- Julian date, @code{double} (see ## @code{juliandate}). ## @item @qcode{'modifiedjuliandate'} -- modified Julian date, ## @code{double}. ## @item @qcode{'posixtime'} -- seconds since the Unix epoch, @code{double} ## (see @code{posixtime}). ## @item @qcode{'yyyymmdd'} -- @code{YYYYMMDD} numeric date, @code{double} ## (see @code{yyyymmdd}). ## @item @qcode{'epochtime'} -- ticks since an epoch, @code{int64} (see ## below). ## @item @qcode{'ntp'} -- NTP timestamp, @code{uint64}, valid from ## 1900-01-01 up to 2036-02-07. ## @item @qcode{'ntfs'} -- NTFS/@w{FILETIME} 100-ns ticks since 1601-01-01, ## @code{uint64}. ## @item @qcode{'.net'} -- @w{.NET} 100-ns ticks since 0001-01-01, ## @code{uint64}. ## @item @qcode{'tt2000'} -- CDF @w{TT2000} nanoseconds since the J2000 ## Terrestrial Time epoch, @code{int64} (see below). ## @end itemize ## ## For the @code{double} conversions, Not-A-Time (@qcode{NaT}) values are ## returned as @qcode{NaN}. The integer conversions cannot represent ## @qcode{NaN}, so a @qcode{NaT} value, an infinite datetime, or a datetime ## outside the target format's representable range raises an error. ## ## @code{@var{X} = convertTo (@var{T}, @qcode{'epochtime'}, @var{Name}, ## @var{Value})} accepts the options @qcode{'Epoch'} (a scalar datetime ## marking tick zero; default @code{1970-01-01}) and ## @qcode{'TicksPerSecond'} (a positive scalar; default @code{1}). The ## epoch and @var{T} must both be zoned or both be unzoned. ## ## @code{@var{X} = convertTo (@var{T}, @qcode{'tt2000'})} returns the number ## of nanoseconds since the J2000 Terrestrial Time epoch, ## @code{2000-01-01T11:58:55.816Z}, as an @code{int64} array. Because that ## count includes leap seconds, @var{T} must be in the ## @qcode{'UTCLeapSeconds'} time zone. The inverse is ## @code{datetime (@var{X}, @qcode{'ConvertFrom'}, @qcode{'tt2000'}, ## @qcode{'TimeZone'}, @qcode{'UTCLeapSeconds'})}, which likewise requires ## both the @code{int64} type and that zone. ## ## Each conversion treats an inserted leap second the way its own format ## does. @code{posixtime} folds it forward onto the following second, while ## @code{datenum}, @code{exceltime}, @qcode{'epochtime'}, @qcode{'ntp'}, ## @qcode{'ntfs'} and @qcode{'.net'} fold it backward onto the preceding ## one; @code{juliandate} stretches the day that holds it (see ## @code{juliandate}), and @qcode{'tt2000'} counts it. ## ## @strong{Deviations from MATLAB.} Two results differ deliberately for a ## @qcode{'UTCLeapSeconds'} array, because MATLAB's own are inconsistent. ## ## @itemize ## @item @qcode{'epochtime'} with an @qcode{'Epoch'}: MATLAB folds the array ## onto the POSIX timeline but measures the epoch on the leap-second one, so ## its tick count is short by the number of seconds inserted before that ## epoch. Counting from @code{2016-12-31} it returns @code{86373} for ## @code{2016-12-31T23:59:59Z}, 26 short. Both operands are folded the same ## way here, giving @code{86399}. Only the offset differs: a difference ## between two such counts is the same in either. ## @item @qcode{'tt2000'} between 1960 and 1972: before 1972 UTC did not ## tick with atomic time, and its offset from it is tabulated by the IERS ## as a base value plus a rate per day. That table is followed here; MATLAB ## evaluates the rate half a day from where the table places it, leaving its ## results up to @code{1.3} milliseconds off. Outside those twelve years ## the two agree exactly. ## @end itemize ## ## @end deftypefn function out = convertTo (this, dateType, varargin) if (nargin < 2) error ("datetime.convertTo: not enough input arguments."); endif if (! (ischar (dateType) && isrow (dateType))) error ("datetime.convertTo: DATETYPE must be a character vector."); endif dt = tolower (dateType); ## 'epochtime' is the only type taking Name/Value options. if (strcmp (dt, 'epochtime')) if (mod (numel (varargin), 2) != 0) error (strcat ("datetime.convertTo: name-value", ... " arguments must be in pairs.")); endif ## Parse optional paired arguments; an empty 'Epoch' stands for ## 1970-01-01 optNames = {'Epoch', 'TicksPerSecond'}; dfValues = {[], 1}; [epochVal, ticks, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! isempty (epochVal)) if (! (isa (epochVal, 'datetime') && isscalar (epochVal))) error ("datetime.convertTo: 'Epoch' must be a scalar datetime."); endif if (xor (isempty (this.TimeZone), isempty (epochVal.TimeZone))) error (strcat ("datetime.convertTo: the epoch and the input", ... " datetime array must both have a time zone, or", ... " must both be unzoned.")); endif dtCheckLeapPair (this, epochVal, 'convertTo'); endif if (! (isnumeric (ticks) && isscalar (ticks) && isreal (ticks) ... && ticks > 0)) error (strcat ("datetime.convertTo: 'TicksPerSecond' must be a", ... " positive scalar.")); endif if (! isempty (args)) error ("datetime.convertTo: invalid optional paired argument."); endif if (isempty (epochVal)) epochMs = 0; else epochMs = round (epochBase (epochVal) * 1000); endif ms = round (epochBase (this) * 1000); res = round ((ms - epochMs) ./ 1000 .* ticks); if (! all (isfinite (ms(:))) ... || any (abs (res(:)) > double (intmax ('int64')))) error (strcat ("datetime.convertTo: 'epochtime' conversion is", ... " not", ... " supported for missing values, infinite", ... " datetimes,", ... " or datetimes outside the int64 range for the", ... " given", ... " epoch and TicksPerSecond.")); endif out = reshape (int64 (res), size (this)); return; endif if (! isempty (varargin)) error ("datetime.convertTo: too many input arguments."); endif switch (dt) case 'datenum' out = datenum (this); case {'excel', 'excel1900'} out = exceltime (this, '1900'); case 'excel1904' out = exceltime (this, '1904'); case 'juliandate' out = juliandate (this, 'juliandate'); case 'modifiedjuliandate' out = juliandate (this, 'modifiedjuliandate'); case 'posixtime' out = posixtime (this); case 'yyyymmdd' out = yyyymmdd (this); case 'ntp' out = reshape (dtFixedEpoch (epochBase (this), 'ntp'), size (this)); case 'ntfs' out = reshape (dtFixedEpoch (epochBase (this), 'ntfs'), size (this)); case '.net' out = reshape (dtFixedEpoch (epochBase (this), 'dotnet'), ... size (this)); case 'tt2000' if (! dtIsLeapZone (this.TimeZone)) error (strcat ("datetime.convertTo: to convert datetimes to", ... " tt2000 times, the 'TimeZone' property of the", ... " input must be 'UTCLeapSeconds'.")); endif out = reshape (dtSerial2TT2000 (serial (this)), size (this)); otherwise error ("datetime.convertTo: unrecognized conversion type '%s'.", ... dateType); endswitch endfunction endmethods ################################################################################ ## ** Query Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'isbetween' 'iscolumn' 'isdst' 'isempty' ## ## 'isequal' 'isequaln' 'isfinite' 'isinf' ## ## 'ismatrix' 'ismember' 'isnat' 'isregular' ## ## 'isrow' 'isscalar' 'issorted' 'issortedrows' ## ## 'isvector' 'isweekend' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} isdst (@var{T}) ## ## Determine which datetime values fall in daylight saving time. ## ## @code{@var{TF} = isdst (@var{T})} returns a logical array @var{TF} of the ## same size as the input datetime array @var{T}, where each element is ## @qcode{true} if the corresponding datetime falls within daylight saving ## time in its time zone, and @qcode{false} otherwise. If @var{T} has no ## time zone, or for Not-A-Time (@qcode{NaT}) values, the corresponding ## element of @var{TF} is @qcode{false}. ## ## @end deftypefn function TF = isdst (this) if (isempty (this.TimeZone)) TF = false (size (this)); else TF = dtIsDst (this.Year, this.Month, this.Day, this.Hour, ... this.Minute, this.Second, this.TimeZone, this.Offset); endif endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{tf} =} isregular (@var{T}) ## @deftypefnx {datetime} {@var{tf} =} isregular (@var{T}, @var{unit}) ## @deftypefnx {datetime} {[@var{tf}, @var{dt}] =} isregular (@dots{}) ## ## Determine whether a datetime vector is regularly spaced. ## ## @code{@var{tf} = isregular (@var{T})} returns @qcode{true} if the ## elements of the datetime vector @var{T} are equally spaced in time, and ## @qcode{false} otherwise. A scalar or empty @var{T}, or one containing a ## Not-A-Time (@qcode{NaT}) value, is not regular. Neither is a @var{T} ## that does not move: a step of zero describes no spacing, so a vector of ## repeated instants is not regularly spaced. ## ## @code{@var{tf} = isregular (@var{T}, @var{unit})} tests for regular ## spacing with respect to @var{unit}, which may be @qcode{'time'} (the ## default), @qcode{'years'}, @qcode{'quarters'}, @qcode{'months'}, ## @qcode{'weeks'}, or @qcode{'days'}. With a calendar unit, @var{T} is ## regular when successive elements differ by the same whole number of that ## unit, which -- unlike @qcode{'time'} -- accounts for varying month ## lengths and daylight saving time. ## ## @code{[@var{tf}, @var{dt}] = isregular (@dots{})} also returns the common ## time step @var{dt}. For @qcode{'time'} it is a @code{duration}; for a ## calendar unit it is a @code{calendarDuration}. When @var{T} is not ## regular, @var{dt} is @qcode{NaN}. ## ## Steps are compared as stored, to the microsecond this class keeps. A ## spacing that differs by less than that cannot be seen and reads as ## regular, while one that rounds differently at the microsecond -- thirds ## of a second, say -- reads as irregular. MATLAB stores instants more ## finely and so draws that line elsewhere. ## ## @end deftypefn function [TF, dt] = isregular (this, unit = 'time') units = {'time', 'years', 'quarters', 'months', 'weeks', 'days'}; if (! (ischar (unit) && isrow (unit) && any (strcmpi (unit, units)))) error (strcat ("datetime.isregular: UNIT must be 'time', 'years',", ... " 'quarters', 'months', 'weeks', or 'days'.")); endif unit = tolower (unit); istime = strcmp (unit, 'time'); ## A scalar or empty array is never regular; anything else must be a ## vector (MATLAB rejects matrices). if (numel (this) < 2) TF = false; if (istime) dt = duration (0, 0, NaN); else dt = calmonths (NaN); endif return; elseif (! isvector (this)) error ("datetime.isregular: input must be a datetime vector."); endif if (istime) ## Fixed-length regularity: every successive instant difference equal. d = diff (this); ds = seconds (d); ## A sequence that does not move has no spacing to be regular about, so ## a step of zero is not regular, as MATLAB reads it too. TF = isfinite (ds(1)) && ds(1) != 0 && all (ds(:) == ds(1)); if (TF) dt = d(1); else dt = duration (0, 0, NaN); endif else ## Calendar regularity: the full calendar difference must be constant ## and consist purely of whole units of the requested kind. d = caldiff (this); moA = calmonths (d); dyA = split (d, 'days'); tA = seconds (split (d, 'time')); allEqual = ! any (isnan ([moA(:); dyA(:); tA(:)])) ... && all (moA(:) == moA(1)) && all (dyA(:) == dyA(1)) ... && all (tA(:) == tA(1)); mo = moA(1); dy = dyA(1); ti = tA(1); switch (unit) case 'years' pure = mod (mo, 12) == 0 && dy == 0 && ti == 0; case 'quarters' pure = mod (mo, 3) == 0 && dy == 0 && ti == 0; case 'months' pure = dy == 0 && ti == 0; case 'weeks' pure = mo == 0 && ti == 0 && mod (dy, 7) == 0; case 'days' pure = mo == 0 && ti == 0; endswitch ## As for time, a step of no calendar at all is not a spacing. TF = allEqual && pure && ! (mo == 0 && dy == 0 && ti == 0); if (TF) dt = d(1); else dt = calmonths (NaN); endif endif endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} isweekend (@var{T}) ## ## Determine which datetime values fall on a weekend. ## ## @code{@var{TF} = isweekend (@var{T})} returns a logical array @var{TF} of ## the same size as the input datetime array @var{T}, where each element is ## @qcode{true} if the corresponding datetime falls on a Saturday or Sunday, ## and @qcode{false} otherwise. Not-A-Time (@qcode{NaT}) values return ## @qcode{false}. ## ## @end deftypefn function TF = isweekend (this) dow = day (this, 'dayofweek'); TF = dow == 1 | dow == 7; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} isbetween (@var{X}, @var{lower}, @var{upper}) ## @deftypefnx {datetime} {@var{TF} =} isbetween (@var{X}, @var{lower}, @var{upper}, @var{intervalType}) ## ## Determine which datetime values lie within an interval. ## ## @code{@var{TF} = isbetween (@var{X}, @var{lower}, @var{upper})} returns a ## logical array @var{TF}, the same size as the broadcast of its inputs, ## containing @qcode{true} where the element of @var{X} lies between the ## corresponding @var{lower} and @var{upper} bounds. @var{lower} and ## @var{upper} may each be a datetime array or a date/time character vector, ## string array, or cell array of character vectors, and either may be ## scalar to broadcast against @var{X}. The comparison is made on the ## absolute instant, so zoned inputs may be in different time zones. A ## Not-A-Time element in any input makes the corresponding result ## @qcode{false}. ## ## @code{@var{TF} = isbetween (@var{X}, @var{lower}, @var{upper}, ## @var{intervalType})} selects which endpoints are included. ## @var{intervalType} is @qcode{'closed'} (the default, ## @w{@var{lower} @leq{} @var{X} @leq{} @var{upper}}), @qcode{'open'} ## (both endpoints excluded), @qcode{'openleft'} (exclude @var{lower}), or ## @qcode{'openright'} (exclude @var{upper}). ## ## @end deftypefn function TF = isbetween (X, varargin) if (numel (varargin) < 2) error ("datetime.isbetween: not enough input arguments."); endif if (numel (varargin) > 3) error ("datetime.isbetween: too many input arguments."); endif lo = varargin{1}; hi = varargin{2}; itype = 'closed'; if (numel (varargin) == 3) itype = varargin{3}; if (! (ischar (itype) && isrow (itype) && any (strcmpi (itype, ... {'closed', 'open', 'openleft', 'openright'})))) error (strcat ("datetime.isbetween: interval type must be", ... " 'closed', 'open', 'openleft', or 'openright'.")); endif endif if (isa (X, 'datetime')) ref = X; elseif (isa (lo, 'datetime')) ref = lo; else ref = hi; endif X = dtIsbetweenArg (X, ref); lo = dtIsbetweenArg (lo, ref); hi = dtIsbetweenArg (hi, ref); zoned = [! isempty(X.TimeZone), ! isempty(lo.TimeZone), ... ! isempty(hi.TimeZone)]; if (any (zoned) && ! all (zoned)) error (strcat ("datetime.isbetween: cannot combine a datetime", ... " array", ... " with a time zone with one without a time zone.")); endif dtCheckLeapPair (X, lo, 'isbetween'); dtCheckLeapPair (X, hi, 'isbetween'); sX = serial (X); sL = serial (lo); sU = serial (hi); switch (lower (itype)) case 'closed' TF = sL <= sX & sX <= sU; case 'open' TF = sL < sX & sX < sU; case 'openleft' TF = sL < sX & sX <= sU; case 'openright' TF = sL <= sX & sX < sU; endswitch endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} issorted (@var{A}) ## @deftypefnx {datetime} {@var{TF} =} issorted (@var{A}, @var{dim}) ## @deftypefnx {datetime} {@var{TF} =} issorted (@var{A}, @var{direction}) ## @deftypefnx {datetime} {@var{TF} =} issorted (@var{A}, @var{dim}, @var{direction}) ## ## Determine whether a datetime array is sorted. ## ## @code{@var{TF} = issorted (@var{A})} returns @qcode{true} if the elements ## of the datetime array @var{A} are sorted in ascending (non-decreasing) ## order along its first non-singleton dimension, and @qcode{false} ## otherwise. For a matrix, every column (or row, depending on the ## operating dimension) must be sorted for @var{TF} to be @qcode{true}. ## Not-A-Time (@qcode{NaT}) elements are treated as greater than any other ## value, so an array is sorted in ascending order only when its @qcode{NaT} ## elements come last. ## ## @code{@var{TF} = issorted (@var{A}, @var{dim})} operates along dimension ## @var{dim}. ## ## @code{@var{TF} = issorted (@var{A}, @var{direction})} tests whether the ## elements are sorted according to @var{direction}, which may be one of: ## ## @itemize ## @item @qcode{'ascend'} (default) tests non-decreasing order. ## @item @qcode{'descend'} tests non-increasing order. ## @item @qcode{'monotonic'} tests non-decreasing or non-increasing order. ## @item @qcode{'strictascend'} tests strictly increasing order. ## @item @qcode{'strictdescend'} tests strictly decreasing order. ## @item @qcode{'strictmonotonic'} tests strictly monotonic order. ## @end itemize ## ## @end deftypefn function TF = issorted (A, varargin) dim = []; direction = 'ascend'; valid = {'ascend', 'descend', 'monotonic', 'strictascend', ... 'strictdescend', 'strictmonotonic'}; for i = 1:numel (varargin) arg = varargin{i}; if (isnumeric (arg)) if (! isscalar (arg) || arg < 1 || arg != fix (arg)) error ("datetime.issorted: DIM must be a positive integer."); endif dim = arg; elseif (ischar (arg) && isrow (arg)) didx = find (strcmpi (arg, valid)); if (isempty (didx)) error ("datetime.issorted: invalid DIRECTION '%s'.", arg); endif direction = valid{didx}; else error ("datetime.issorted: invalid input argument."); endif endfor if (isempty (dim)) dim = find (size (A) != 1, 1); if (isempty (dim)) dim = 1; endif endif ## NaT sorts as greater than any value; map it to +Inf on the serial. M = serial (A); M(isnan (M)) = Inf; if (dim > 2 || size (M, dim) < 2) TF = true; return; endif if (dim == 2) M = M.'; endif lo = M(1:end-1, :); hi = M(2:end, :); switch (direction) case 'ascend' ok = all (lo <= hi, 1); case 'descend' ok = all (lo >= hi, 1); case 'strictascend' ok = all (lo < hi, 1); case 'strictdescend' ok = all (lo > hi, 1); case 'monotonic' ok = all (lo <= hi, 1) | all (lo >= hi, 1); case 'strictmonotonic' ok = all (lo < hi, 1) | all (lo > hi, 1); endswitch TF = all (ok(:)); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} issortedrows (@var{A}) ## @deftypefnx {datetime} {@var{TF} =} issortedrows (@var{A}, @var{column}) ## @deftypefnx {datetime} {@var{TF} =} issortedrows (@var{A}, @var{direction}) ## @deftypefnx {datetime} {@var{TF} =} issortedrows (@var{A}, @var{column}, @var{direction}) ## ## Determine whether the rows of a datetime array are sorted. ## ## @code{@var{TF} = issortedrows (@var{A})} returns @qcode{true} if the rows ## of the 2-D datetime array @var{A} are sorted in ascending order, i.e.@: ## lexicographically by the first column, ties broken by the second column, ## and so on, and @qcode{false} otherwise. Not-A-Time (@qcode{NaT}) ## elements are treated as greater than any other value. ## ## @code{@var{TF} = issortedrows (@var{A}, @var{column})} checks the order ## using only the columns listed in @var{column}, in the given priority. A ## negative entry checks the corresponding column for descending order. ## ## @code{@var{TF} = issortedrows (@var{A}, @var{direction})} checks for the ## order given by @var{direction}, which may be one of @qcode{'ascend'} ## (default), @qcode{'descend'}, @qcode{'monotonic'}, ## @qcode{'strictascend'}, @qcode{'strictdescend'}, or ## @qcode{'strictmonotonic'}. It may also be a cell array of ## @qcode{'ascend'}/@qcode{'descend'} strings, one per sort column. For the ## strict options a matrix qualifies only when its first sort column is ## strictly monotonic and free of @qcode{NaT}. ## ## @code{@var{TF} = issortedrows (@var{A}, @var{column}, @var{direction})} ## combines an explicit column list with a @var{direction}. ## ## @end deftypefn function TF = issortedrows (A, varargin) if (ndims (A) != 2) error ("datetime.issortedrows: A must be a 2-D datetime array."); endif ncol = size (A, 2); keywords = {'ascend', 'descend', 'monotonic', 'strictascend', ... 'strictdescend', 'strictmonotonic'}; column = []; direction = []; if (numel (varargin) >= 1) if (isnumeric (varargin{1})) column = varargin{1}; if (numel (varargin) > 2) error ("datetime.issortedrows: too many input arguments."); elseif (numel (varargin) == 2) direction = varargin{2}; endif else direction = varargin{1}; if (numel (varargin) > 1) error ("datetime.issortedrows: COLUMN must precede DIRECTION."); endif endif endif if (isempty (column)) column = 1:ncol; endif column = column(:).'; if (any (column == 0) || any (column != fix (column)) ... || any (abs (column) > ncol)) error ("datetime.issortedrows: COLUMN out of range."); endif colmag = abs (column); desc = column < 0; check = 'ascend'; if (! isempty (direction)) if (ischar (direction) && isrow (direction)) check = lower (direction); if (! any (strcmp (check, keywords))) error ("datetime.issortedrows: invalid DIRECTION '%s'.", direction); endif if (any (strcmp (check, {'ascend', 'strictascend'}))) desc = false (size (colmag)); elseif (any (strcmp (check, {'descend', 'strictdescend'}))) desc = true (size (colmag)); endif elseif (iscellstr (direction)) if (numel (direction) != numel (colmag)) error (strcat ("datetime.issortedrows: DIRECTION must have one", ... " entry per sort column.")); endif desc = false (size (colmag)); for j = 1:numel (direction) if (strcmpi (direction{j}, 'descend')) desc(j) = true; elseif (! strcmpi (direction{j}, 'ascend')) error ("datetime.issortedrows: invalid DIRECTION '%s'.", ... direction{j}); endif endfor else error ("datetime.issortedrows: invalid DIRECTION argument."); endif endif if (isempty (colmag)) TF = true; return; endif S = serial (A); if (any (strcmp (check, {'strictascend', 'strictdescend', ... 'strictmonotonic'}))) ## Strict: only the first sort column matters; it must be strictly ## monotonic and contain no NaT. p = S(:, colmag(1)); if (any (isnan (p))) TF = false; else d = diff (p); if (strcmp (check, 'strictascend')) TF = all (d > 0); elseif (strcmp (check, 'strictdescend')) TF = all (d < 0); else TF = all (d > 0) || all (d < 0); endif endif return; endif ## Non-strict: lexicographic check on the selected columns. NaT maps to ## +Inf (largest); each column is negated to fold in a descending key. K = S(:, colmag); K(isnan (K)) = Inf; Kd = K; for j = 1:numel (colmag) if (desc(j)) Kd(:, j) = -Kd(:, j); endif endfor if (strcmp (check, 'monotonic')) TF = rowsNonDecreasing (A, Kd) || rowsNonDecreasing (A, -Kd); else TF = rowsNonDecreasing (A, Kd); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} iscolumn (@var{T}) ## ## Return true if datetime array is a column vector. ## ## @code{@var{TF} = iscolumn (@var{T})} returns a logical scalar @var{TF}, ## which is @qcode{true}, if the datetime array @var{T} is a column ## vector, and @qcode{false} otherwise. A column vector is a 2-D array for ## which @code{size (@var{X})} returns @code{[@var{N}, 1]} with non-negative ## @var{N}. ## ## @end deftypefn function TF = iscolumn (this) TF = iscolumn (this.Year); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} isempty (@var{T}) ## ## Return true if datetime array is empty. ## ## @code{@var{TF} = isempty (@var{T})} returns a logical scalar @var{TF}, ## which is @qcode{true}, if the datetime array @var{T} is empty, and ## @qcode{false} otherwise. ## ## @end deftypefn function TF = isempty (this) TF = isempty (this.Year); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} isequal (@var{A}, @var{B}) ## @deftypefnx {datetime} {@var{TF} =} isequal (@var{A}, @var{B}, @dots{}) ## ## Test datetime arrays for equality. ## ## @code{@var{TF} = isequal (@var{A}, @var{B})} returns a logical scalar ## @var{TF}, which is @qcode{true} if the datetime arrays @var{A} and ## @var{B} are the same size and each pair of corresponding elements is the ## same point in time, and @qcode{false} otherwise. As with @qcode{NaN}, ## Not-A-Time (@qcode{NaT}) elements are never equal, so any @qcode{NaT} in ## either array makes the result @qcode{false}; use @code{isequaln} to treat ## @qcode{NaT} elements as equal. ## ## Additional arrays may be supplied, as in @code{isequal (@var{A}, @var{B}, ## @var{C}, @dots{})}, in which case @var{TF} is @qcode{true} only when all ## of the arrays are equal to one another. Any input argument that is not a ## datetime array, or a datetime array whose time zone is not compatible ## with the others (one zoned and one unzoned), makes the result ## @qcode{false} rather than raising an error. Zoned arrays are compared by ## their absolute instants, so equal instants in different time zones are ## equal. ## ## @end deftypefn function TF = isequal (varargin) if (nargin < 2) print_usage (); endif TF = do_isequal (varargin, false); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} isequaln (@var{A}, @var{B}) ## @deftypefnx {datetime} {@var{TF} =} isequaln (@var{A}, @var{B}, @dots{}) ## ## Test datetime arrays for equality, treating Not-A-Time as equal. ## ## @code{@var{TF} = isequaln (@var{A}, @var{B})} is identical to ## @code{isequal (@var{A}, @var{B})} except that Not-A-Time (@qcode{NaT}) ## elements are treated as equal to one another, in the same way that ## @code{isequaln} treats @qcode{NaN}. It returns a logical scalar ## @var{TF}, which is @qcode{true} if the datetime arrays @var{A} and ## @var{B} are the same size and each pair of corresponding elements is ## either the same point in time or both @qcode{NaT}, and @qcode{false} ## otherwise. ## ## Additional arrays may be supplied, as in @code{isequaln (@var{A}, ## @var{B}, @var{C}, @dots{})}, in which case @var{TF} is @qcode{true} only ## when all of the arrays are equal to one another. Any input argument that ## is not a datetime array, or a datetime array whose time zone is not ## compatible with the others (one zoned and one unzoned), makes the result ## @qcode{false} rather than raising an error. Zoned arrays are compared by ## their absolute instants, so equal instants in different time zones are ## equal. ## ## @end deftypefn function TF = isequaln (varargin) if (nargin < 2) print_usage (); endif TF = do_isequal (varargin, true); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{out} =} isfinite (@var{T}) ## ## Test for finite elements in datetime array. ## ## @code{@var{TF} = isfinite (@var{T})} returns a logical array @var{TF} of ## the same size as @var{T} containing @qcode{true} for each corresponding ## element of @var{T} that is finite and @qcode{false} otherwise. Finite ## elements in datetime arrays are those which are neither @qcode{Inf} nor ## @qcode{NaT}. ## ## @end deftypefn function TF = isfinite (this) TF = isfinite (this.Year); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{out} =} isinf (@var{T}) ## ## Test for infinite elements in datetime array. ## ## @code{@var{TF} = isinf (@var{T})} returns a logical array @var{TF} of the ## same size as @var{T} containing @qcode{true} for each corresponding ## element of @var{T} that is infinite and @qcode{false} otherwise. ## ## @end deftypefn function TF = isinf (this) TF = isinf (this.Year); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} ismatrix (@var{T}) ## ## Return true if datetime array is a 2-D array. ## ## @code{@var{TF} = ismatrix (@var{T})} returns a logical scalar @var{TF}, ## which is @qcode{true}, if the datetime array @var{T} is a matrix, and ## @qcode{false} otherwise. A matrix is an array of any type where ## @code{ndims (@var{X}) == 2} and for which @code{size (@var{X})} returns ## @code{[@var{H}, @var{W}]} with non-negative @var{H} and @var{W}. ## ## @end deftypefn function TF = ismatrix (this) TF = ismatrix (this.Year); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} ismissing (@var{T}) ## @deftypefnx {datetime} {@var{TF} =} ismissing (@var{T}, @var{indicator}) ## ## Test for missing elements in datetime array. ## ## @var{TF} is a logical array of the same size as @var{T}, @qcode{true} ## where an element of @var{T} is @qcode{NaT}. ## ## @code{@var{TF} = ismissing (@var{T}, @var{indicator})} marks instead the ## elements of @var{T} equal to the values in @var{indicator}, a datetime ## scalar or vector; the standard missing values are ignored, unless ## explicitly stated in @var{indicator}. A @qcode{NaT} in @var{indicator} ## matches the @qcode{NaT} elements of @var{T}, and so does ## @code{missing}. ## ## @end deftypefn function TF = ismissing (this, varargin) if (nargin > 2) error ("datetime.ismissing: too many input arguments."); endif if (! isempty (varargin)) indicator = varargin{1}; TF = false (size (this)); if (isa (indicator, 'missing')) TF = isnan (this.Year); elseif (isvector (indicator)) if (isa (indicator, 'datetime')) for i = 1:numel (indicator.Year) if (isnan (indicator.Year(i))) TF(isnan (this.Year)) = true; continue; endif is_eq = indicator.Year(i) == this.Year & ... indicator.Month(i) == this.Month & ... indicator.Day(i) == this.Day & ... indicator.Hour(i) == this.Hour & ... indicator.Minute(i) == this.Minute & ... indicator.Second(i) == this.Second; TF(is_eq) = true; endfor else error ("datetime.ismissing: INDICATOR must be a 'datetime' array."); endif else error ("datetime.ismissing: INDICATOR must be a vector."); endif else TF = isnan (this.Year); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} isnat (@var{T}) ## ## Test for Not-A-Time elements in datetime array. ## ## @code{@var{TF} = isnat (@var{T})} returns a logical array @var{TF} of the ## same size as @var{T} containing @qcode{true} for each corresponding ## element of @var{T} that is Not-A-Time (@qcode{NaT}) and @qcode{false} ## otherwise. @qcode{NaT} is the equivalent of @qcode{NaN} in numeric ## arrays. ## ## If @var{T} is not a datetime array, @code{isnat} returns an error. ## ## @end deftypefn function TF = isnat (this) TF = isnan (this.Year); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} isrow (@var{T}) ## ## Return true if datetime array is a row vector. ## ## @code{@var{TF} = isrow (@var{T})} returns a logical scalar @var{TF}, ## which is @qcode{true}, if the datetime array @var{T} is a row vector, ## and @qcode{false} otherwise. A row vector is a 2-D array for which ## @code{size (@var{X})} returns @code{[1, @var{N}]} with non-negative ## @var{N}. ## ## @end deftypefn function TF = isrow (this) TF = isrow (this.Year); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} isscalar (@var{T}) ## ## Return true if datetime array is a scalar. ## ## @code{@var{TF} = isscalar (@var{T})} returns a logical scalar @var{TF}, ## which is @qcode{true}, if the datetime array @var{T} is also a scalar, ## and @qcode{false} otherwise. A scalar is a single element object for ## which @code{size (@var{X})} returns @code{[1, 1]}. ## ## @end deftypefn function TF = isscalar (this) TF = isscalar (this.Year); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} isvector (@var{T}) ## ## Return true if datetime array is a vector. ## ## @code{@var{TF} = isvector (@var{T})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the datetime array @var{T} is a vector and ## @qcode{false} otherwise. A vector is a 2-D array for which one of the ## dimensions is equal to 1 (either @math{1*N} or @math{N*1}). By ## definition, a scalar is also a vector. ## ## @end deftypefn function TF = isvector (this) TF = isvector (this.Year); endfunction endmethods ################################################################################ ## ** Sort, Filter, and Set Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'maxk' 'mink' 'sort' 'sortrows' ## ## 'topkrows' 'unique' 'interp1' 'intersect' ## ## 'setdiff' 'setxor' 'union' 'min' ## ## 'max' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{B} =} maxk (@var{A}, @var{K}) ## @deftypefnx {datetime} {@var{B} =} maxk (@var{A}, @var{K}, @var{dim}) ## @deftypefnx {datetime} {[@var{B}, @var{index}] =} maxk (@var{A}, @dots{}) ## ## Find the latest elements in a datetime array. ## ## @code{@var{B} = maxk (@var{A}, @var{K})} returns the @var{K} latest ## elements of the datetime array @var{A} in descending order. If @var{A} ## is a vector, then @var{B} is a vector with @var{K} elements. If @var{A} ## is a matrix, then @code{maxk} operates along each column of @var{A} and ## @var{B} has @var{K} rows. For multidimensional arrays, @code{maxk} ## operates along the first non-singleton dimension. Elements are ranked ## by the instant they name, so an array with a time zone is ranked by ## absolute time rather than by its wall clock. ## ## @code{@var{B} = maxk (@var{A}, @var{K}, @var{dim})} operates along the ## dimension specified by @var{dim}. ## ## @var{K} must be a nonnegative integer scalar. If @var{K} is larger than ## the number of elements along the operating dimension, then all of them ## are returned. Infinite datetimes are ranked as ordinary values. ## ## Missing elements (@qcode{NaT}) are not ranked. They are appended after ## the ranked elements in their original order, and hence they only appear ## in @var{B} when @var{K} exceeds the number of non-missing elements along ## the operating dimension. Unlike @code{sort}, @code{maxk} has no ## @qcode{'MissingPlacement'} option, since @qcode{NaT} elements are always ## placed last. Elements comparing as equal keep their original order. ## ## @code{[@var{B}, @var{index}] = maxk (@var{A}, @dots{})} also returns an ## index array containing the indices of the returned elements of @var{A} ## along the operating dimension. ## ## @end deftypefn function [B, index] = maxk (A, K, varargin) if (nargin < 2) error ("datetime.maxk: too few input arguments."); endif [index, lidx, errmsg] = __minmaxk__ (serial (A), K, true, varargin); if (! isempty (errmsg)) error ("datetime.maxk: %s", errmsg); endif B = subset (A, lidx); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{B} =} mink (@var{A}, @var{K}) ## @deftypefnx {datetime} {@var{B} =} mink (@var{A}, @var{K}, @var{dim}) ## @deftypefnx {datetime} {[@var{B}, @var{index}] =} mink (@var{A}, @dots{}) ## ## Find the earliest elements in a datetime array. ## ## @code{@var{B} = mink (@var{A}, @var{K})} returns the @var{K} earliest ## elements of the datetime array @var{A} in ascending order. If @var{A} ## is a vector, then @var{B} is a vector with @var{K} elements. If @var{A} ## is a matrix, then @code{mink} operates along each column of @var{A} and ## @var{B} has @var{K} rows. For multidimensional arrays, @code{mink} ## operates along the first non-singleton dimension. Elements are ranked ## by the instant they name, so an array with a time zone is ranked by ## absolute time rather than by its wall clock. ## ## @code{@var{B} = mink (@var{A}, @var{K}, @var{dim})} operates along the ## dimension specified by @var{dim}. ## ## @var{K} must be a nonnegative integer scalar. If @var{K} is larger than ## the number of elements along the operating dimension, then all of them ## are returned. Infinite datetimes are ranked as ordinary values. ## ## Missing elements (@qcode{NaT}) are not ranked. They are appended after ## the ranked elements in their original order, and hence they only appear ## in @var{B} when @var{K} exceeds the number of non-missing elements along ## the operating dimension. Unlike @code{sort}, @code{mink} has no ## @qcode{'MissingPlacement'} option, since @qcode{NaT} elements are always ## placed last. Elements comparing as equal keep their original order. ## ## @code{[@var{B}, @var{index}] = mink (@var{A}, @dots{})} also returns an ## index array containing the indices of the returned elements of @var{A} ## along the operating dimension. ## ## @end deftypefn function [B, index] = mink (A, K, varargin) if (nargin < 2) error ("datetime.mink: too few input arguments."); endif [index, lidx, errmsg] = __minmaxk__ (serial (A), K, false, varargin); if (! isempty (errmsg)) error ("datetime.mink: %s", errmsg); endif B = subset (A, lidx); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{B} =} sort (@var{A}) ## @deftypefnx {datetime} {@var{B} =} sort (@var{A}, @var{dim}) ## @deftypefnx {datetime} {@var{B} =} sort (@var{A}, @var{direction}) ## @deftypefnx {datetime} {@var{B} =} sort (@var{A}, @var{dim}, @var{direction}) ## @deftypefnx {datetime} {@var{B} =} sort (@dots{}, @qcode{'MissingPlacement'}, @var{mp}) ## @deftypefnx {datetime} {[@var{B}, @var{I}] =} sort (@dots{}) ## ## Sort a datetime array. ## ## @code{@var{B} = sort (@var{A})} returns the elements of the datetime ## array @var{A} sorted in ascending order along its first non-singleton ## dimension. For a matrix, each column is sorted independently. Sorting ## is stable: elements that compare as equal keep their original relative ## order. ## ## @code{@var{B} = sort (@var{A}, @var{dim})} sorts along dimension ## @var{dim}. ## ## @code{@var{B} = sort (@var{A}, @var{direction})} sorts in the order given ## by @var{direction}, which is either @qcode{'ascend'} (default) or ## @qcode{'descend'}. ## ## @code{@var{B} = sort (@dots{}, @qcode{'MissingPlacement'}, @var{mp})} ## controls where Not-A-Time (@qcode{NaT}) elements are placed. @var{mp} ## may be @qcode{'auto'} (default; @qcode{NaT} last for ascending order and ## first for descending order), @qcode{'first'}, or @qcode{'last'}. ## ## @code{[@var{B}, @var{I}] = sort (@dots{})} also returns an index array ## @var{I} of the same size as @var{A} such that @var{B} is @var{A} indexed ## by @var{I} along the operating dimension. ## ## @end deftypefn function [B, I] = sort (A, varargin) dim = []; direction = 'ascend'; placement = 'auto'; i = 1; while (i <= numel (varargin)) arg = varargin{i}; if (ischar (arg) && isrow (arg) && strcmpi (arg, 'MissingPlacement')) if (i == numel (varargin)) error ("datetime.sort: 'MissingPlacement' requires a value."); endif placement = lower (varargin{i+1}); if (! any (strcmp (placement, {'auto', 'first', 'last'}))) error ("datetime.sort: invalid 'MissingPlacement' value."); endif i += 2; elseif (isnumeric (arg)) if (! isscalar (arg) || arg < 1 || arg != fix (arg)) error ("datetime.sort: DIM must be a positive integer."); endif dim = arg; i += 1; elseif (ischar (arg) && isrow (arg)) if (strcmpi (arg, 'ascend')) direction = 'ascend'; elseif (strcmpi (arg, 'descend')) direction = 'descend'; else error ("datetime.sort: invalid DIRECTION '%s'.", arg); endif i += 1; else error ("datetime.sort: invalid input argument."); endif endwhile if (isempty (dim)) dim = find (size (A) != 1, 1); if (isempty (dim)) dim = 1; endif endif ## Sorting along a singleton dimension is a no-op. if (size (A, dim) < 2) B = A; I = ones (size (A)); return; endif descend = strcmp (direction, 'descend'); ## Resolve 'auto': NaT goes last for ascending, first for descending. if (strcmp (placement, 'auto')) if (descend) placement = 'first'; else placement = 'last'; endif endif ## Work on the numeric serial (NaT -> NaN). Bring the sorted dimension to ## the front and flatten everything after it, so the column loop below ## holds for an array of any number of dimensions; the result is put back ## the way it came at the end. Transposing instead would only ever have ## worked for a matrix. S = serial (A); Y = A.Year; MO = A.Month; D = A.Day; H = A.Hour; MI = A.Minute; SE = A.Second; nd = max (ndims (S), dim); sz = size (S); sz(end+1:nd) = 1; perm = [dim, 1:dim-1, dim+1:nd]; psz = sz(perm); flat = @(V) reshape (permute (V, perm), psz(1), []); S = flat (S); Y = flat (Y); MO = flat (MO); D = flat (D); H = flat (H); MI = flat (MI); SE = flat (SE); OF = flat (A.Offset); [nr, nc] = size (S); idx = zeros (nr, nc); for j = 1:nc col = S(:, j); nat = isnan (col); finidx = find (! nat); natidx = find (nat); ## Stable order: sort by value, breaking ties by original position. if (descend) [~, ord] = sortrows ([-col(finidx), finidx]); else [~, ord] = sortrows ([col(finidx), finidx]); endif finsorted = finidx(ord); if (strcmp (placement, 'first')) idx(:, j) = [natidx; finsorted]; else idx(:, j) = [finsorted; natidx]; endif endfor ## Reorder the component arrays column-wise using linear indexing, then ## restore the original shape and dimension order. lin = idx + repmat ((0:nc-1) .* nr, nr, 1); back = @(V) ipermute (reshape (V(lin), psz), perm); Y = back (Y); MO = back (MO); D = back (D); H = back (H); MI = back (MI); SE = back (SE); OF = back (OF); I = ipermute (reshape (idx, psz), perm); B = A; B.Year = Y; B.Month = MO; B.Day = D; B.Hour = H; B.Minute = MI; B.Second = SE; B.Offset = OF; endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{B} =} sortrows (@var{A}) ## @deftypefnx {datetime} {@var{B} =} sortrows (@var{A}, @var{column}) ## @deftypefnx {datetime} {@var{B} =} sortrows (@var{A}, @var{direction}) ## @deftypefnx {datetime} {@var{B} =} sortrows (@var{A}, @var{column}, @var{direction}) ## @deftypefnx {datetime} {@var{B} =} sortrows (@dots{}, @qcode{'MissingPlacement'}, @var{mp}) ## @deftypefnx {datetime} {[@var{B}, @var{index}] =} sortrows (@dots{}) ## ## Sort the rows of a datetime array. ## ## @code{@var{B} = sortrows (@var{A})} sorts the rows of the 2-D datetime ## array @var{A} in ascending order. Rows are ordered lexicographically: by ## the first column, ties broken by the second column, and so on. The sort ## is stable, so rows that compare as equal keep their original order. ## ## @code{@var{B} = sortrows (@var{A}, @var{column})} sorts using only the ## columns listed in @var{column}, in the given priority. A negative entry ## sorts the corresponding column in descending order. Columns not listed ## are not used as sort keys. ## ## @code{@var{B} = sortrows (@var{A}, @var{direction})} sorts every column ## in the given @var{direction}, either @qcode{'ascend'} (default) or ## @qcode{'descend'}. @var{direction} may also be a cell array of strings ## with one such value per sort column. ## ## @code{@var{B} = sortrows (@var{A}, @var{column}, @var{direction})} ## combines an explicit column list with a per-column @var{direction}. ## ## @code{@var{B} = sortrows (@dots{}, @qcode{'MissingPlacement'}, @var{mp})} ## controls where Not-A-Time (@qcode{NaT}) elements are placed. @var{mp} ## may be @qcode{'auto'} (default; @qcode{NaT} sorts as the largest value, ## i.e.@: last for ascending and first for descending columns), ## @qcode{'first'}, or @qcode{'last'}. ## ## @code{[@var{B}, @var{index}] = sortrows (@dots{})} also returns a column ## index vector @var{index} that maps the rows of @var{A} to @var{B}, such ## that @code{@var{B} = @var{A}(@var{index}, :)}. ## ## @end deftypefn function [B, index] = sortrows (A, varargin) if (ndims (A) != 2) error ("datetime.sortrows: A must be a 2-D datetime array."); endif ncol = size (A, 2); ## Split the 'MissingPlacement' (and ignored 'ComparisonMethod') ## name-value pairs off from the positional COLUMN/DIRECTION arguments. placement = 'auto'; pos = {}; k = 1; while (k <= numel (varargin)) a = varargin{k}; if (ischar (a) && isrow (a) && strcmpi (a, 'MissingPlacement')) if (k == numel (varargin)) error ("datetime.sortrows: 'MissingPlacement' requires a value."); endif placement = lower (varargin{k+1}); if (! any (strcmp (placement, {'auto', 'first', 'last'}))) error ("datetime.sortrows: invalid 'MissingPlacement' value."); endif k += 2; elseif (ischar (a) && isrow (a) && strcmpi (a, 'ComparisonMethod')) k += 2; else pos{end+1} = a; k += 1; endif endwhile if (numel (pos) > 2) error ("datetime.sortrows: too many input arguments."); endif ## Resolve the column selection and the per-column sort direction. column = []; direction = []; if (numel (pos) >= 1) if (isnumeric (pos{1})) column = pos{1}; if (numel (pos) == 2) direction = pos{2}; endif else direction = pos{1}; if (numel (pos) == 2) error ("datetime.sortrows: COLUMN must precede DIRECTION."); endif endif endif if (isempty (column)) column = 1:ncol; endif column = column(:).'; if (any (column == 0) || any (column != fix (column)) ... || any (abs (column) > ncol)) error ("datetime.sortrows: COLUMN out of range."); endif colmag = abs (column); desc = column < 0; if (! isempty (direction)) if (ischar (direction) && isrow (direction)) if (strcmpi (direction, 'descend')) desc = true (size (colmag)); elseif (strcmpi (direction, 'ascend')) desc = false (size (colmag)); else error ("datetime.sortrows: invalid DIRECTION '%s'.", direction); endif elseif (iscellstr (direction)) if (numel (direction) != numel (colmag)) error (strcat ("datetime.sortrows: DIRECTION must have one", ... " entry per sort column.")); endif desc = false (size (colmag)); for j = 1:numel (direction) if (strcmpi (direction{j}, 'descend')) desc(j) = true; elseif (! strcmpi (direction{j}, 'ascend')) error ("datetime.sortrows: invalid DIRECTION '%s'.", ... direction{j}); endif endfor else error ("datetime.sortrows: invalid DIRECTION argument."); endif endif ## Build the numeric key matrix from each selected column's instant. NaT ## maps to a signed-Inf sentinel that places it per MissingPlacement in ## the column's own direction ('auto' treats NaT as the largest value). S = serial (A); N = size (S, 1); K = S(:, colmag); for j = 1:numel (colmag) if (strcmp (placement, 'auto')) sentinel = Inf; elseif (strcmp (placement, 'last')) if (desc(j)) sentinel = -Inf; else sentinel = Inf; endif else if (desc(j)) sentinel = Inf; else sentinel = -Inf; endif endif col = K(:, j); col(isnan (col)) = sentinel; K(:, j) = col; endfor ## Append the original row index as a final ascending tie-break so equal ## rows keep their order, then defer to the built-in lexicographic sort. nkey = numel (colmag); spec = (1:nkey) .* (1 - 2 * desc); [~, index] = sortrows ([K, (1:N)'], [spec, nkey + 1]); index = index(:); B = subset (A, index, ':'); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{M} =} min (@var{A}) ## @deftypefnx {datetime} {@var{M} =} min (@var{A}, @var{B}) ## @deftypefnx {datetime} {@var{M} =} min (@var{A}, [], @var{dim}) ## @deftypefnx {datetime} {@var{M} =} min (@dots{}, @var{nanflag}) ## @deftypefnx {datetime} {@var{M} =} min (@var{A}, [], @qcode{'all'}) ## @deftypefnx {datetime} {[@var{M}, @var{I}] =} min (@dots{}) ## ## Minimum of a datetime array. ## ## @code{@var{M} = min (@var{A})} returns the smallest element of the ## datetime array @var{A} along its first non-singleton dimension. For a ## matrix, @var{M} is a row vector with the minimum of each column. ## Not-A-Time (@qcode{NaT}) elements are omitted; a reduction over ## @qcode{NaT} elements only yields @qcode{NaT}. ## ## @code{@var{M} = min (@var{A}, @var{B})} returns an array the same size as ## @var{A} and @var{B} (after broadcasting) holding the element-wise minimum ## of the two datetime arrays. ## ## @code{@var{M} = min (@var{A}, [], @var{dim})} operates along dimension ## @var{dim}. The empty second argument distinguishes this from the ## element-wise form. ## ## @code{@var{M} = min (@dots{}, @var{nanflag})} sets the treatment of ## @qcode{NaT}: @qcode{'omitnan'} (default) ignores @qcode{NaT}, while ## @qcode{'includenan'} returns @qcode{NaT} whenever a @qcode{NaT} takes ## part in the comparison. ## ## @code{@var{M} = min (@var{A}, [], @qcode{'all'})} returns the smallest ## element of the whole array. ## ## @code{[@var{M}, @var{I}] = min (@dots{})} also returns the indices of the ## minima. A second output is not available for the element-wise form. ## ## @end deftypefn function [M, I] = min (A, varargin) [M, I] = minmaxImpl (A, varargin, false, nargout); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{M} =} max (@var{A}) ## @deftypefnx {datetime} {@var{M} =} max (@var{A}, @var{B}) ## @deftypefnx {datetime} {@var{M} =} max (@var{A}, [], @var{dim}) ## @deftypefnx {datetime} {@var{M} =} max (@dots{}, @var{nanflag}) ## @deftypefnx {datetime} {@var{M} =} max (@var{A}, [], @qcode{'all'}) ## @deftypefnx {datetime} {[@var{M}, @var{I}] =} max (@dots{}) ## ## Maximum of a datetime array. ## ## @code{@var{M} = max (@var{A})} returns the largest element of the ## datetime array @var{A} along its first non-singleton dimension. For a ## matrix, @var{M} is a row vector with the maximum of each column. ## Not-A-Time (@qcode{NaT}) elements are omitted; a reduction over ## @qcode{NaT} elements only yields @qcode{NaT}. ## ## @code{@var{M} = max (@var{A}, @var{B})} returns an array the same size as ## @var{A} and @var{B} (after broadcasting) holding the element-wise maximum ## of the two datetime arrays. ## ## @code{@var{M} = max (@var{A}, [], @var{dim})} operates along dimension ## @var{dim}. The empty second argument distinguishes this from the ## element-wise form. ## ## @code{@var{M} = max (@dots{}, @var{nanflag})} sets the treatment of ## @qcode{NaT}: @qcode{'omitnan'} (default) ignores @qcode{NaT}, while ## @qcode{'includenan'} returns @qcode{NaT} whenever a @qcode{NaT} takes ## part in the comparison. ## ## @code{@var{M} = max (@var{A}, [], @qcode{'all'})} returns the largest ## element of the whole array. ## ## @code{[@var{M}, @var{I}] = max (@dots{})} also returns the indices of the ## maxima. A second output is not available for the element-wise form. ## ## @end deftypefn function [M, I] = max (A, varargin) [M, I] = minmaxImpl (A, varargin, true, nargout); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{B} =} topkrows (@var{A}, @var{K}) ## @deftypefnx {datetime} {@var{B} =} topkrows (@var{A}, @var{K}, @var{col}) ## @deftypefnx {datetime} {@var{B} =} topkrows (@var{A}, @var{K}, @var{direction}) ## @deftypefnx {datetime} {@var{B} =} topkrows (@var{A}, @var{K}, @var{col}, @var{direction}) ## @deftypefnx {datetime} {@var{B} =} topkrows (@dots{}, @qcode{'MissingPlacement'}, @var{MP}) ## @deftypefnx {datetime} {[@var{B}, @var{index}] =} topkrows (@var{A}, @dots{}) ## ## Top K sorted rows of a datetime array. ## ## @code{@var{B} = topkrows (@var{A}, @var{K})} returns the top @var{K} rows ## of the 2-D datetime array @var{A} sorted in descending order as a group. ## @var{K} must be a nonnegative integer scalar. If @var{K} is larger than ## the number of rows in @var{A}, then all of them are returned. ## ## Missing elements (@qcode{NaT}) are not ranked. Within each sort ## column they are placed after the elements that are defined, whichever ## direction is asked for, so a row is demoted only by a missing element in ## a column that actually decides its position. Rows comparing as equal ## keep their original order. ## ## @code{@var{B} = topkrows (@var{A}, @var{K}, @var{col})} sorts using only ## the columns listed in the numeric vector @var{col}, which must contain ## positive integers indexing existing columns in @var{A}. Columns are used ## as sort keys in the order given, and those not listed are not used at ## all. The direction is descending unless @var{direction} says otherwise. ## ## @code{@var{B} = topkrows (@var{A}, @var{K}, @var{direction})} sorts in ## the given @var{direction}, either @qcode{'descend'} (default) or ## @qcode{'ascend'} applying to all columns in @var{A}. Alternatively, ## @var{direction} can be a cell array of character vectors specifying the ## sorting direction for each individual column of @var{A}. ## ## @code{@var{B} = topkrows (@var{A}, @var{K}, @var{col}, @var{direction})} ## combines an explicit column list with a per-column @var{direction}. ## ## @code{@var{B} = topkrows (@dots{}, @qcode{'MissingPlacement'}, @var{MP})} ## specifies where the missing elements are placed within each sort column, ## with any of the following options specified in @var{MP}: ## ## @itemize ## @item @qcode{'last'}, which is the default, places missing elements last ## whichever direction is asked for. ## @item @qcode{'first'} places missing elements first. ## @item @qcode{'auto'} places missing elements last for an ascending sort ## and first for a descending one, as @code{sortrows} does. ## @end itemize ## ## This is an Octave extension: MATLAB has no such option here and always ## ranks as @qcode{'last'} does. ## ## @code{[@var{B}, @var{index}] = topkrows (@var{A}, @dots{})} also returns ## an index vector containing the original row indices of @var{A} in ## @var{B}, such that @code{@var{B} = @var{A}(@var{index},:)}. ## ## @end deftypefn function [B, index] = topkrows (A, K, varargin) ## Check input arguments if (nargin < 2) error ("datetime.topkrows: too few input arguments."); endif if (ndims (A) != 2) error ("datetime.topkrows: A must be a 2-D array."); endif [col, direction, MP, errmsg] = ... __topkrowsargs__ (K, columns (A), varargin); if (! isempty (errmsg)) error ("datetime.topkrows: %s", errmsg); endif ## Sort rows, keeping missing elements out of the ranking if (isempty (col)) [B, index] = sortrows (A, direction, 'MissingPlacement', MP); else [B, index] = sortrows (A, col, direction, 'MissingPlacement', MP); endif ## Return top K rows if (K < numel (index)) B = subset (B, 1:K, ':'); index = index(1:K); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{B} =} unique (@var{A}) ## @deftypefnx {datetime} {@var{B} =} unique (@var{A}, @var{setOrder}) ## @deftypefnx {datetime} {@var{B} =} unique (@var{A}, @var{occurrence}) ## @deftypefnx {datetime} {@var{B} =} unique (@var{A}, @var{setOrder}, @var{occurrence}) ## @deftypefnx {datetime} {@var{B} =} unique (@var{A}, @var{occurrence}, @var{setOrder}) ## @deftypefnx {datetime} {@var{B} =} unique (@var{A}, @dots{}, @qcode{'rows'}) ## @deftypefnx {datetime} {[@var{B}, @var{ixA}, @var{ixB}] =} unique (@dots{}) ## ## Unique values in a datetime array. ## ## @code{@var{B} = unique (@var{A})} returns the unique values of the ## datetime array @var{A} in sorted order. ## ## @code{@var{B} = unique (@var{A}, @var{setOrder})} returns the unique ## values of the datetime array @var{A} in an order as specified by ## @var{setOrder}, which can be either of the following values: ## ## @itemize ## @item @qcode{'sorted'} (default) returns the unique values sorted in ## ascending order. ## @item @qcode{'stable'} returns the unique values according to their order ## of occurrence. ## @end itemize ## ## @code{@var{B} = unique (@var{A}, @var{occurrence})} returns the unique ## values of the datetime array @var{tblA} according to their order of ## occurrence. @var{occurrence} can be either of the following values: ## ## @itemize ## @item @qcode{'first'} (default) returns the first occurrence of each ## unique value, i.e. the lowest possible indices are returned. ## @item @qcode{'last'} returns the last occurrence of each unique value, ## i.e. the highest possible indices are returned. ## @end itemize ## ## You can specify @var{setOrder} and @var{occurrence} arguments together. ## ## @code{@var{B} = unique (@var{A}, @dots{}, @qcode{'rows'})} returns the ## unique rows of @var{A} by treating each row as a single entity. The ## @qcode{'rows'} option can be used alone or in any combination with the ## @var{setOrder} and @var{occurrence} arguments. @qcode{'rows'} can be ## placed at any position in the function's argument list after the input ## array @var{A}. However, this syntax is only valid for 2-dimensional ## datetime arrays. ## ## @code{[@var{tblB}, @var{ixA}, @var{ixB}] = unique (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} using any of the previous syntaxes. ## @var{ixA} and @var{ixB} map the arrays @var{A} and @var{B} to one another ## such that @qcode{@var{B} = @var{A}(@var{ixA})} and ## @qcode{@var{A} = @var{B}(@var{ixB})}. When the @qcode{'rows'} optional ## argument is specified, then @qcode{@var{B} = @var{A}(@var{ixA},:)} and ## @qcode{@var{tblA} = @var{tblB}(@var{ixB},:)}. ## ## @end deftypefn function [B, ixA, ixB] = unique (A, varargin) ## 'legacy' option is not supported if (any (strcmp ("legacy", varargin))) error ("datetime.unique: 'legacy' option is not supported."); endif ## Handle each property array separately [~, ~, Yidx] = __unique__ (A.Year, varargin{:}); [~, ~, MOidx] = __unique__ (A.Month, varargin{:}); [~, ~, Didx] = __unique__ (A.Day, varargin{:}); [~, ~, Hidx] = __unique__ (A.Hour, varargin{:}); [~, ~, MIidx] = __unique__ (A.Minute, varargin{:}); [~, ~, Sidx] = __unique__ (A.Second, varargin{:}); ## The offset joins the components as a seventh key, negated so that the ## larger offset -- the earlier of the two moments a repeated wall clock ## names -- codes lower and the row order stays chronological. It is a ## constant column for an unzoned array and for any array that does not ## straddle a fall-back, so it changes nothing outside that case. [~, ~, OFidx] = __unique__ (-A.Offset, varargin{:}); DT = [Yidx, MOidx, Didx, Hidx, MIidx, Sidx, OFidx]; ## Use indices to find unique datetime values if (any (strcmp ('rows', varargin))) [~, ixA, ixB] = __unique__ (DT, varargin{:}); if (any (strcmp ('last', varargin))) [~, ixA, ~] = __unique__ (ixB, 'last'); endif B = subset (A, ixA, ':'); else [~, ixA, ixB] = __unique__ (DT, 'rows', varargin{:}); if (any (strcmp ('last', varargin))) [~, ixA, ~] = __unique__ (ixB, 'last'); endif B = subset (A, ixA); ## Match MATLAB: a non-row input yields a column (so an empty 0-by-0 ## input returns a 0-by-1 result rather than 0-by-0). if (isempty (B) && ! isrow (A)) B = reshape (B, numel (B), 1); endif endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{C} =} intersect (@var{A}, @var{B}) ## @deftypefnx {datetime} {@var{C} =} intersect (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {datetime} {@var{C} =} intersect (@dots{}, @var{order}) ## @deftypefnx {datetime} {[@var{C}, @var{ixA}, @var{ixB}] =} intersect (@dots{}) ## ## Set intersection of two datetime arrays. ## ## @code{@var{C} = intersect (@var{A}, @var{B})} returns the unique datetime ## values common to both @var{A} and @var{B}. Either input may instead be a ## date/time character vector, string array, or cell array of character ## vectors, which is promoted to a datetime array before the operation. ## Membership is decided on the absolute instant, so two zoned inputs may be ## in different time zones; the result carries the time zone and display ## format of @var{A}. Not-A-Time (@qcode{NaT}) elements are treated like ## @qcode{NaN} and never match. @var{C} is a row vector when both @var{A} ## and @var{B} are row vectors and a column vector otherwise. ## ## @code{@var{C} = intersect (@var{A}, @var{B}, @qcode{'rows'})} treats each ## row of the datetime matrices @var{A} and @var{B}, which must have the ## same number of columns, as a single element and returns their common ## rows. ## ## @code{@dots{} = intersect (@dots{}, @var{order})} returns the values in ## @qcode{'sorted'} order (the default) or in @qcode{'stable'} order, i.e.@: ## the order in which they appear in @var{A}. ## ## @code{[@var{C}, @var{ixA}, @var{ixB}] = intersect (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} such that @code{@var{C} = ## @var{A}(@var{ixA})} and @code{@var{C} = @var{B}(@var{ixB})}, or the ## corresponding row selections when @qcode{'rows'} is used. ## ## @end deftypefn function [C, ixA, ixB] = intersect (A, B, varargin) if (any (strcmpi ('legacy', varargin))) error ("datetime.intersect: 'legacy' option is not supported."); endif A = dtSetPromote (A, B, 'intersect'); B = dtSetPromote (B, A, 'intersect'); [A, B] = prepSetOp (A, B, 'intersect'); SA = serial (A); SB = serial (B); if (any (strcmpi ('rows', varargin))) [~, ixA, ixB] = intersect (SA, SB, varargin{:}); C = subset (A, ixA, ':'); else [~, ixA, ixB] = intersect (SA(:), SB(:), varargin{:}); C = subset (A, ixA); C = reshapeSetResult (C, isrow (A) && isrow (B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{C} =} union (@var{A}, @var{B}) ## @deftypefnx {datetime} {@var{C} =} union (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {datetime} {@var{C} =} union (@dots{}, @var{order}) ## @deftypefnx {datetime} {[@var{C}, @var{ixA}, @var{ixB}] =} union (@dots{}) ## ## Set union of two datetime arrays. ## ## @code{@var{C} = union (@var{A}, @var{B})} returns the unique datetime ## values present in either @var{A} or @var{B}. Either input may instead be ## a date/time character vector, string array, or cell array of character ## vectors, which is promoted to a datetime array before the operation. ## Membership is decided on the absolute instant, so two zoned inputs may be ## in different time zones; the result carries the time zone and display ## format of @var{A}. Distinct @qcode{NaT} elements are all retained. ## @var{C} is a row vector when both @var{A} and @var{B} are row vectors and ## a column vector otherwise. ## ## @code{@var{C} = union (@var{A}, @var{B}, @qcode{'rows'})} treats each row ## of the datetime matrices @var{A} and @var{B}, which must have the same ## number of columns, as a single element and returns their combined unique ## rows. ## ## @code{@dots{} = union (@dots{}, @var{order})} returns the values in ## @qcode{'sorted'} order (the default) or in @qcode{'stable'} order. ## ## @code{[@var{C}, @var{ixA}, @var{ixB}] = union (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} such that @var{C} is the ## combination of @code{@var{A}(@var{ixA})} and @code{@var{B}(@var{ixB})}, ## or the corresponding row selections when @qcode{'rows'} is used. ## ## @end deftypefn function [C, ixA, ixB] = union (A, B, varargin) if (any (strcmpi ('legacy', varargin))) error ("datetime.union: 'legacy' option is not supported."); endif A = dtSetPromote (A, B, 'union'); B = dtSetPromote (B, A, 'union'); [A, B] = prepSetOp (A, B, 'union'); SA = serial (A); SB = serial (B); stable = any (strcmpi ('stable', varargin)); if (any (strcmpi ('rows', varargin))) [~, ixA, ixB] = union (SA, SB, varargin{:}); C = combineSets (A, B, ixA, ixB, SA, SB, true, stable); else [~, ixA, ixB] = union (SA(:), SB(:), varargin{:}); C = combineSets (A, B, ixA, ixB, SA, SB, false, stable); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{C} =} setdiff (@var{A}, @var{B}) ## @deftypefnx {datetime} {@var{C} =} setdiff (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {datetime} {@var{C} =} setdiff (@dots{}, @var{order}) ## @deftypefnx {datetime} {[@var{C}, @var{ixA}] =} setdiff (@dots{}) ## ## Set difference of two datetime arrays. ## ## @code{@var{C} = setdiff (@var{A}, @var{B})} returns the unique datetime ## values in @var{A} that are not in @var{B}. Either input may instead be a ## date/time character vector, string array, or cell array of character ## vectors, which is promoted to a datetime array before the operation. ## Membership is decided on the absolute instant, so two zoned inputs may be ## in different time zones; the result carries the time zone and display ## format of @var{A}. @qcode{NaT} elements in @var{A} are all retained, as ## they never match an element of @var{B}. @var{C} is a row vector when ## both @var{A} and @var{B} are row vectors and a column vector otherwise. ## ## @code{@var{C} = setdiff (@var{A}, @var{B}, @qcode{'rows'})} treats each ## row of the datetime matrices @var{A} and @var{B}, which must have the ## same number of columns, as a single element and returns the rows of ## @var{A} that are not rows of @var{B}. ## ## @code{@dots{} = setdiff (@dots{}, @var{order})} returns the values in ## @qcode{'sorted'} order (the default) or in @qcode{'stable'} order. ## ## @code{[@var{C}, @var{ixA}] = setdiff (@dots{})} also returns an index ## vector @var{ixA} such that @code{@var{C} = @var{A}(@var{ixA})}, or ## @code{@var{C} = @var{A}(@var{ixA},:)} when @qcode{'rows'} is used. ## ## @end deftypefn function [C, ixA] = setdiff (A, B, varargin) if (any (strcmpi ('legacy', varargin))) error ("datetime.setdiff: 'legacy' option is not supported."); endif A = dtSetPromote (A, B, 'setdiff'); B = dtSetPromote (B, A, 'setdiff'); [A, B] = prepSetOp (A, B, 'setdiff'); SA = serial (A); SB = serial (B); if (any (strcmpi ('rows', varargin))) [~, ixA] = setdiff (SA, SB, varargin{:}); C = subset (A, ixA, ':'); else [~, ixA] = setdiff (SA(:), SB(:), varargin{:}); C = subset (A, ixA); C = reshapeSetResult (C, isrow (A) && isrow (B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{C} =} setxor (@var{A}, @var{B}) ## @deftypefnx {datetime} {@var{C} =} setxor (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {datetime} {@var{C} =} setxor (@dots{}, @var{order}) ## @deftypefnx {datetime} {[@var{C}, @var{ixA}, @var{ixB}] =} setxor (@dots{}) ## ## Set exclusive-or of two datetime arrays. ## ## @code{@var{C} = setxor (@var{A}, @var{B})} returns the unique datetime ## values that are in @var{A} or in @var{B} but not in both. Either input ## may instead be a date/time character vector, string array, or cell array ## of character vectors, which is promoted to a datetime array before the ## operation. Membership is decided on the absolute instant, so two zoned ## inputs may be in different time zones; the result carries the time zone ## and display format of @var{A}. Distinct @qcode{NaT} elements are all ## retained. @var{C} is a row vector when both @var{A} and @var{B} are row ## vectors and a column vector otherwise. ## ## @code{@var{C} = setxor (@var{A}, @var{B}, @qcode{'rows'})} treats each ## row of the datetime matrices @var{A} and @var{B}, which must have the ## same number of columns, as a single element and returns the rows that are ## in one input but not both. ## ## @code{@dots{} = setxor (@dots{}, @var{order})} returns the values in ## @qcode{'sorted'} order (the default) or in @qcode{'stable'} order. ## ## @code{[@var{C}, @var{ixA}, @var{ixB}] = setxor (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} such that @var{C} is the ## combination of @code{@var{A}(@var{ixA})} and @code{@var{B}(@var{ixB})}, ## or the corresponding row selections when @qcode{'rows'} is used. ## ## @end deftypefn function [C, ixA, ixB] = setxor (A, B, varargin) if (any (strcmpi ('legacy', varargin))) error ("datetime.setxor: 'legacy' option is not supported."); endif A = dtSetPromote (A, B, 'setxor'); B = dtSetPromote (B, A, 'setxor'); [A, B] = prepSetOp (A, B, 'setxor'); SA = serial (A); SB = serial (B); stable = any (strcmpi ('stable', varargin)); if (any (strcmpi ('rows', varargin))) [~, ixA, ixB] = setxor (SA, SB, varargin{:}); C = combineSets (A, B, ixA, ixB, SA, SB, true, stable); else [~, ixA, ixB] = setxor (SA(:), SB(:), varargin{:}); C = combineSets (A, B, ixA, ixB, SA, SB, false, stable); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} ismember (@var{A}, @var{B}) ## @deftypefnx {datetime} {@var{TF} =} ismember (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {datetime} {[@var{TF}, @var{index}] =} ismember (@dots{}) ## ## Test for datetime elements in a set. ## ## @code{@var{TF} = ismember (@var{A}, @var{B})} returns a logical array ## @var{TF} of the same size as @var{A} containing @qcode{true} for each ## corresponding element of @var{A} that is in @var{B} and @qcode{false} ## otherwise. Either input may instead be a date/time character vector, ## string array, or cell array of character vectors, which is promoted to a ## datetime array before the test. Membership is decided on the absolute ## instant, so two zoned inputs may be in different time zones. Similarly ## to @qcode{NaN} values, Not-A-Time (@qcode{NaT}) elements are not equal ## with each other and always return @qcode{false}. ## ## @code{@var{TF} = ismember (@var{A}, @var{B}, @qcode{'rows'})} only ## applies to datetime matrices with the same number of columns, in which ## case the logical vector @var{TF} contains @qcode{true} for each row of ## @var{A} that is also a row in @var{B}. @var{TF} has the same number of ## rows as @var{A}. ## ## @code{[@var{TF}, @var{index}] = ismember (@var{A}, @var{B})} also returns ## an index array of the same size as @var{A} containing the lowest index in ## @var{B} for each element of @var{A} that is a member of @var{B} and 0 ## otherwise. If the @qcode{'rows'} optional argument is used, then the ## returning index is a column vector with the same rows as @var{A} and it ## contains the lowest index in @var{B} for each row of @var{A} that is a ## member of @var{B} and 0 otherwise. ## ## @end deftypefn function [TF, index] = ismember (A, B, varargin) do_rows = false; if (! isempty (varargin)) if (strcmpi (varargin{1}, 'rows')) do_rows = true; if (ndims (A) != 2 || ndims (A) != ndims (B)) error ("datetime.ismember: 'rows' applies only to 2-D matrices."); endif if (size (A, 2) != size (B, 2)) error (strcat ("datetime.ismember: 'rows' requires same", ... " number of columns.")); endif else error ("datetime.ismember: invalid optional argument."); endif endif A = dtSetPromote (A, B, 'ismember'); B = dtSetPromote (B, A, 'ismember'); [A, B] = prepSetOp (A, B, 'ismember'); SA = serial (A); SB = serial (B); if (do_rows) [TF, index] = __ismember__ (SA, SB, 'rows'); else [TF, index] = __ismember__ (SA, SB); endif endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{VI} =} interp1 (@var{X}, @var{V}, @var{XI}) ## @deftypefnx {datetime} {@var{VI} =} interp1 (@dots{}, @var{method}) ## @deftypefnx {datetime} {@var{VI} =} interp1 (@dots{}, @var{method}, @var{extrap}) ## ## One-dimensional interpolation involving datetime arrays. ## ## @code{@var{VI} = interp1 (@var{X}, @var{V}, @var{XI})} interpolates to ## find @var{VI}, the values of the underlying function @code{@var{V} = f ## (@var{X})} at the query points @var{XI}. Datetime arrays are handled by ## interpolating on their absolute (POSIX) instants: ## ## @itemize ## @item When the sample points @var{X} are a datetime array, the query ## points @var{XI} must also be a datetime array, and vice versa. ## @item When the sampled values @var{V} are a datetime array, @var{VI} is a ## datetime array carrying the time zone and display format of @var{V}; ## otherwise @var{VI} is numeric. ## @end itemize ## ## The optional @var{method} (@qcode{'linear'} by default) and @var{extrap} ## arguments are passed to the built-in @code{interp1} (see its ## documentation for the supported interpolation methods and extrapolation ## options). Query points outside the range of @var{X}, and Not-A-Time ## query points, yield @qcode{NaN} or @qcode{NaT} unless extrapolation is ## requested. ## ## @end deftypefn function BI = interp1 (A, B, AI, varargin) vIsDT = isa (B, 'datetime'); if (isa (A, 'datetime')) if (! isa (AI, 'datetime')) error (strcat ("datetime.interp1: query points must be a", ... " datetime", ... " array when the sample points are datetime.")); endif x = serial (A); xq = serial (AI); else if (isa (AI, 'datetime')) error (strcat ("datetime.interp1: query points must be numeric", ... " when", ... " the sample points are numeric.")); endif x = A; xq = AI; endif if (vIsDT) v = serial (B); else v = B; endif vi = interp1 (x, v, xq, varargin{:}); if (vIsDT) BI = fromReducedSerial (B, vi); else BI = vi; endif endfunction endmethods ################################################################################ ## ** Descriptive Statistics ** ## ################################################################################ ## Available Methods ## ## ## ## 'mean' 'median' 'mode' 'std' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{M} =} mean (@var{A}) ## @deftypefnx {datetime} {@var{M} =} mean (@var{A}, @var{dim}) ## @deftypefnx {datetime} {@var{M} =} mean (@var{A}, @qcode{'all'}) ## @deftypefnx {datetime} {@var{M} =} mean (@dots{}, @var{nanflag}) ## ## Mean of a datetime array. ## ## @code{@var{M} = mean (@var{A})} returns the mean of the datetime array ## @var{A} as a scalar datetime, computed as the average of the absolute ## instants along the first non-singleton dimension. A @var{dim} or ## @qcode{'all'} argument selects the dimension(s) to operate on. The ## result carries the @code{Format} and @code{TimeZone} of @var{A}. ## ## By default a Not-A-Time element makes the corresponding result ## @qcode{NaT}; pass @qcode{'omitnat'} (equivalently @qcode{'omitmissing'}) ## to ignore missing values, or @qcode{'includenat'} to keep the default. ## ## @end deftypefn function R = mean (A, varargin) args = dtStatFlags (varargin); R = fromReducedSerial (A, mean (serial (A), args{:})); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{M} =} median (@var{A}) ## @deftypefnx {datetime} {@var{M} =} median (@var{A}, @var{dim}) ## @deftypefnx {datetime} {@var{M} =} median (@var{A}, @qcode{'all'}) ## @deftypefnx {datetime} {@var{M} =} median (@dots{}, @var{nanflag}) ## ## Median of a datetime array. ## ## @code{@var{M} = median (@var{A})} returns the median of the datetime ## array @var{A} as a datetime, computed on the absolute instants along the ## first non-singleton dimension (for an even number of elements the average ## of the two middle instants). A @var{dim} or @qcode{'all'} argument ## selects the dimension(s). The result carries the @code{Format} and ## @code{TimeZone} of @var{A}. Missing-value handling matches @code{mean}. ## ## @end deftypefn function R = median (A, varargin) args = dtStatFlags (varargin); R = fromReducedSerial (A, median (serial (A), args{:})); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{M} =} mode (@var{A}) ## @deftypefnx {datetime} {@var{M} =} mode (@var{A}, @var{dim}) ## @deftypefnx {datetime} {@var{M} =} mode (@var{A}, @qcode{'all'}) ## @deftypefnx {datetime} {[@var{M}, @var{F}, @var{C}] =} mode (@dots{}) ## ## Most frequent value of a datetime array. ## ## @code{@var{M} = mode (@var{A})} returns the most frequently occurring ## datetime in @var{A} along the first non-singleton dimension; when several ## values are equally frequent the smallest is returned. A @var{dim} or ## @qcode{'all'} argument selects the dimension(s). Not-A-Time elements are ## ignored. The result carries the @code{Format} and @code{TimeZone} of ## @var{A}. ## ## @code{[@var{M}, @var{F}, @var{C}] = mode (@dots{})} also returns the ## frequency @var{F} of the modal value and a cell array @var{C} whose ## elements list all values that achieve that frequency. ## ## @end deftypefn function [R, F, C] = mode (A, varargin) if (nargout > 2) [ser, F, C] = mode (serial (A), varargin{:}); C = cellfun (@(x) fromReducedSerial (A, x), C, 'UniformOutput', false); elseif (nargout == 2) [ser, F] = mode (serial (A), varargin{:}); else ser = mode (serial (A), varargin{:}); endif R = fromReducedSerial (A, ser); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{S} =} std (@var{A}) ## @deftypefnx {datetime} {@var{S} =} std (@var{A}, @var{w}) ## @deftypefnx {datetime} {@var{S} =} std (@var{A}, @var{w}, @var{dim}) ## @deftypefnx {datetime} {@var{S} =} std (@var{A}, @var{w}, @qcode{'all'}) ## @deftypefnx {datetime} {@var{S} =} std (@dots{}, @var{nanflag}) ## @deftypefnx {datetime} {[@var{S}, @var{M}] =} std (@dots{}) ## ## Standard deviation of a datetime array. ## ## @code{@var{S} = std (@var{A})} returns the standard deviation of the ## absolute instants of @var{A} as a @code{duration}. The weight @var{w} ## selects the normalisation (@code{0}, the default, divides by @math{N-1}; ## @code{1} divides by @math{N}), and a @var{dim} or @qcode{'all'} argument ## selects the dimension(s). Missing-value handling matches @code{mean}. ## ## @code{[@var{S}, @var{M}] = std (@dots{})} also returns the mean @var{M} ## as a datetime. ## ## @end deftypefn function [S, M] = std (A, varargin) args = dtStatFlags (varargin); if (nargout > 1) [sSec, mSec] = std (serial (A), args{:}); M = fromReducedSerial (A, mSec); else sSec = std (serial (A), args{:}); endif S = duration (0, 0, sSec); endfunction endmethods ################################################################################ ## ** Binning ** ## ################################################################################ ## Available Methods ## ## ## ## 'discretize' 'histcounts' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{bin} =} discretize (@var{T}, @var{edges}) ## @deftypefnx {datetime} {@var{bin} =} discretize (@var{T}, @var{N}) ## @deftypefnx {datetime} {@var{bin} =} discretize (@var{T}, @var{dur}) ## @deftypefnx {datetime} {@var{bin} =} discretize (@var{T}, @var{unit}) ## @deftypefnx {datetime} {@var{Y} =} discretize (@dots{}, @var{values}) ## @deftypefnx {datetime} {@var{C} =} discretize (@dots{}, 'categorical') ## @deftypefnx {datetime} {@var{C} =} discretize (@dots{}, 'categorical', @var{names}) ## @deftypefnx {datetime} {[@var{bin}, @var{E}] =} discretize (@dots{}) ## ## Group datetimes into bins. ## ## @code{@var{bin} = discretize (@var{T}, @var{edges})} returns, for each ## element of @var{T}, the index of the bin of the @qcode{datetime} vector ## @var{edges} that contains it. Bins are half open, ## @code{[@var{E}(j), @var{E}(j+1))}, except the last which is closed at ## both ends. Elements outside the edges, and @qcode{NaT} elements, give ## @qcode{NaN}. ## ## @code{@var{bin} = discretize (@var{T}, @var{N})} uses @var{N} bins ## spanning the data, placed on whole calendar or clock units wherever that ## can be done without leaving a bin unused. ## ## @code{@var{bin} = discretize (@var{T}, @var{dur})} uses bins one ## @var{dur} wide, where @var{dur} is a scalar @qcode{duration} or ## @qcode{calendarDuration}, aligned to whole multiples of that width. ## ## @code{@var{bin} = discretize (@var{T}, @var{unit})} uses bins one named ## unit wide, @var{unit} being one of @qcode{'second'}, @qcode{'minute'}, ## @qcode{'hour'}, @qcode{'day'}, @qcode{'week'}, @qcode{'month'}, ## @qcode{'quarter'}, @qcode{'year'}, @qcode{'decade'} or ## @qcode{'century'}. These land on real calendar boundaries: a ## @qcode{'week'} bin starts on a Sunday, a @qcode{'quarter'} on 1 January, ## 1 April, 1 July or 1 October, and a @qcode{'decade'} on a year that is a ## multiple of ten. ## ## @code{@var{Y} = discretize (@dots{}, @var{values})} returns ## @code{@var{values}(@var{bin})} instead of the bin index, and ## @code{@var{C} = discretize (@dots{}, 'categorical')} returns a ## @qcode{categorical} array whose categories are named after the bins. ## ## @code{[@var{bin}, @var{E}] = discretize (@dots{})} also returns the bin ## edges as a @qcode{datetime} array carrying this array's @qcode{Format} ## and @qcode{TimeZone}. ## ## Note that whether a bin follows the calendar depends on how its width ## is given, not on how long that width is. A named unit (@qcode{'day'} ## and coarser) or a @qcode{calendarDuration} width begins at ## @strong{local midnight}, so in a time zone that observes daylight ## saving the bin holding a transition is 23 or 25 hours long while its ## neighbours are 24. A @qcode{duration} width is a fixed span of elapsed ## time whatever its length: @code{days (1)} bins are each exactly 24 ## hours, and their edges therefore read an hour later on the far side of ## a transition. The two agree for an unzoned array, and for a zoned one ## that spans no transition. ## ## A named unit opens one bin past the data when the largest element sits ## on a unit boundary of the wall clock. Sub-day bins step in elapsed ## time, so in a zone whose shift is not a whole number of those units -- ## @code{Australia/Lord_Howe} moves its clock half an hour, against an ## @qcode{'hour'} bin -- the grid leaves the wall clock past a transition ## and an element may then land mid-unit. Such an element gets a bin of ## its own here. This is @strong{deliberately unlike MATLAB}, which closes ## its last edge short of it and leaves it out of every bin. ## ## @strong{Deviation from MATLAB: bin placement where a zone's clock shifts ## by a fraction of the bin unit.} A bin grid is anchored on the wall clock ## and stepped in elapsed time, so a zone that moves its clock by less than ## one bin unit -- @code{Australia/Lord_Howe} moves it half an hour -- puts ## the grid out of step with the clock from the transition onward. Where ## the shift is exactly @strong{half} a unit the placement matches MATLAB, ## having been measured against it. For any other fraction, which today ## arises only from the historical offset changes of the early twentieth ## century (@code{Asia/Singapore} moved by twenty minutes in 1933), the ## placement is @strong{our own}: the grid is centred on the data with no ## correction for the transition, exactly as in a zone that has none. ## MATLAB places those bins differently, by a rule we have not been able to ## derive from its output; both cover the data, and neither is more correct ## than the other. Only a requested bin @strong{count} is affected -- and ## @qcode{'auto'}, @qcode{'scott'}, @qcode{'fd'}, @qcode{'sturges'} and ## @qcode{'sqrt'}, which resolve to one. An explicit @qcode{'BinWidth'} or ## a named unit is placed identically to MATLAB in every zone. ## ## A bin width below a second carries a little noise in its edges. An ## instant is counted in seconds from 1970 and so runs to about 1.7e9, where ## a double resolves to some 2e-7 of a second; a width of, say, 0.4 s ## therefore lands its edges within a few hundred nanoseconds of the exact ## grid rather than on it. The opening edge is exact, the drift is in the ## step, and it is a limit of the representation rather than of the ## placement. ## ## When @var{T} is empty the edges are anchored on the epoch, ## @qcode{1970-01-01}. This is @strong{deliberately unlike MATLAB}, which ## answers an empty @qcode{datetime} with edges taken from the current ## clock, so that the same call returns a different result every time it is ## run. ## ## @end deftypefn function [BIN, EDGES] = discretize (this, arg2, varargin) if (nargin < 2) error ("datetime.discretize: not enough input arguments."); endif xv = serial (this)(:); [s2c, c2s, d2s, w2s] = dtCalHandles (this); isCount = false; if (isa (arg2, 'datetime')) ev = d2s (arg2)(:).'; else [ev, isCount] = dtBinEdges (xv, arg2, s2c, c2s, w2s, ... 'datetime.discretize'); endif EDGES = fromReducedSerial (this, ev); if (isCount) EDGES = reshape (EDGES, 1, numel (ev)); endif ## Bins asked for as a 'categorical' are named after the edges as they ## DISPLAY, not after their serial values, so the names are built here ## and handed to the numeric function rather than left to it. The last ## bin is closed at both ends but is labelled half open, as MATLAB ## labels it. ic = find (cellfun (@(a) dtIsTextScalar (a) ... && strcmpi (char (a), 'categorical'), varargin), 1); if (! isempty (ic)) named = numel (varargin) > ic ... && ! (dtIsTextScalar (varargin{ic+1}) ... && strcmpi (char (varargin{ic+1}), 'IncludedEdge')); if (! named) strs = dispstrings (EDGES); nm = cell (1, numel (ev) - 1); for j = 1:numel (nm) nm{j} = sprintf ("[%s, %s)", strs{j}, strs{j+1}); endfor varargin = [varargin(1:ic), {nm}, varargin(ic+1:end)]; endif endif ## Delegate the assignment and the value mapping to the numeric function [BIN, ~] = discretize (reshape (xv, size (this.Year)), ev, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{N} =} histcounts (@var{T}) ## @deftypefnx {datetime} {@var{N} =} histcounts (@var{T}, @var{nbins}) ## @deftypefnx {datetime} {@var{N} =} histcounts (@var{T}, @var{edges}) ## @deftypefnx {datetime} {@var{N} =} histcounts (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {datetime} {[@var{N}, @var{edges}] =} histcounts (@dots{}) ## @deftypefnx {datetime} {[@var{N}, @var{edges}, @var{bin}] =} histcounts (@dots{}) ## ## Histogram bin counts for datetimes. ## ## @code{@var{N} = histcounts (@var{T})} bins the datetimes in @var{T}, ## chosen automatically, and returns the number of elements in each bin. ## @var{T} is treated as @code{@var{T}(:)} and @qcode{NaT} elements are ## excluded. ## ## @code{@var{N} = histcounts (@var{T}, @var{nbins})} and ## @code{@var{N} = histcounts (@var{T}, @var{edges})} bin by count and by ## explicit @qcode{datetime} edges respectively. ## ## @qcode{'BinWidth'} takes a scalar @qcode{duration} or ## @qcode{calendarDuration} and, @strong{as an Octave extension}, any of ## the unit names listed for @code{discretize}; MATLAB accepts only the ## two classes here and requires @qcode{'BinMethod'} for a named unit. ## The two spellings give identical results, so ## @code{histcounts (@var{T}, 'BinWidth', 'day')} is simply another way of ## writing @code{histcounts (@var{T}, 'BinMethod', 'day')} -- and note ## that neither is the same as @code{'BinWidth', days (1)}, for the reason ## given below. @qcode{'BinLimits'} takes a two-element @qcode{datetime}, ## and @qcode{'BinEdges'} a @qcode{datetime} vector. ## @qcode{'BinMethod'} accepts @qcode{'auto'}, @qcode{'scott'}, ## @qcode{'fd'}, @qcode{'sturges'} and @qcode{'sqrt'}, and any of the named ## calendar units listed for @code{discretize}, but not ## @qcode{'integers'}, which has no meaning for a datetime. ## ## @qcode{'Normalization'} accepts @qcode{'count'}, @qcode{'cumcount'}, ## @qcode{'probability'}, @qcode{'percentage'} and @qcode{'cdf'}. ## @qcode{'countdensity'} and @qcode{'pdf'} are not accepted, since a ## density per unit time has no meaning here. ## ## As for @code{discretize}, a named unit or a @qcode{calendarDuration} ## width begins at local midnight and so varies in length across a ## daylight-saving transition, while a @qcode{duration} width is a fixed ## span of elapsed time whatever its length, and an empty @var{T} anchors ## the edges on the epoch rather than on the current clock as MATLAB does. ## The same paragraph on named units also applies: where a zone shifts by ## less than one bin unit, an element landing mid-unit past the transition ## gets a bin of its own, so @var{N} always sums to the number of finite ## elements. MATLAB can leave such an element out of every bin, and its ## counts then sum to less. ## ## A bin width below a second carries a little noise in its edges. An ## instant is counted in seconds from 1970 and so runs to about 1.7e9, where ## a double resolves to some 2e-7 of a second; a width of, say, 0.4 s ## therefore lands its edges within a few hundred nanoseconds of the exact ## grid rather than on it. The opening edge is exact, the drift is in the ## step, and it is a limit of the representation rather than of the ## placement. ## ## @seealso{discretize, histcounts} ## @end deftypefn function [N, EDGES, BIN] = histcounts (this, varargin) xv = serial (this)(:); [s2c, c2s, d2s, w2s] = dtCalHandles (this); args = dtHistArgs (varargin, xv, s2c, c2s, d2s, w2s, ... 'datetime.histcounts'); if (nargout > 2) [N, ev, BIN] = histcounts (xv, args{:}); BIN = reshape (BIN, size (this.Year)); else [N, ev] = histcounts (xv, args{:}); endif EDGES = fromReducedSerial (this, ev); endfunction endmethods ################################################################################ ## ** Arithmetic Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'plus' 'minus' 'colon' 'linspace' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{R} =} colon (@var{A}, @var{B}) ## @deftypefnx {datetime} {@var{R} =} colon (@var{A}, @var{step}, @var{B}) ## ## Create a range of datetime values. ## ## @code{@var{R} = colon (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{R} = @var{A}:@var{B}} and returns a row vector of datetime ## values starting at @var{A} and increasing in steps of one calendar day up ## to, and possibly including, @var{B}. ## ## @code{@var{R} = colon (@var{A}, @var{step}, @var{B})} is the equivalent ## of the syntax @code{@var{R} = @var{A}:@var{step}:@var{B}} and uses the ## specified @var{step} between consecutive elements. @var{step} may be: ## ## @itemize ## @item a @code{duration} or a numeric scalar (a number of fixed 24-hour ## days), in which case successive elements advance by a fixed amount of ## elapsed time; for a zoned range this is aware of daylight saving time. ## ## @item a @code{calendarDuration}, in which case successive elements ## advance in calendar units. Each element is computed as @code{@var{A} + ## k*@var{step}} for @code{k = 0, 1, 2, @dots{}}, so month and year steps ## clamp the day of month independently for every element (e.g.@: a ## one-month step from 31 January yields 31 January, 28 February, 31 March, ## @dots{}). ## @end itemize ## ## The default step of @code{@var{A}:@var{B}} is one calendar day ## (@code{caldays (1)}), which preserves the time of day across daylight ## saving time changes. A range whose @var{step} points away from @var{B} ## (for example an increasing step with @code{@var{A} > @var{B}}) is empty. ## @var{A} and @var{B} must be datetime scalars that are either both zoned ## or both unzoned, and must be finite. ## ## @end deftypefn function R = colon (varargin) if (nargin == 2) A = varargin{1}; step = caldays (1); B = varargin{2}; elseif (nargin == 3) A = varargin{1}; step = varargin{2}; B = varargin{3}; else print_usage (); endif if (! (isa (A, 'datetime') && isa (B, 'datetime'))) error ("datetime.colon: range endpoints must be datetime arrays."); endif if (! (isscalar (A) && isscalar (B))) error ("datetime.colon: range endpoints must be datetime scalars."); endif if (xor (isempty (A.TimeZone), isempty (B.TimeZone))) error (strcat ("datetime.colon: cannot create a range between a", ... " datetime with a time zone and one without a time", ... " zone.")); endif dtCheckLeapPair (A, B, 'colon'); if (! (isfinite (A) && isfinite (B))) error (strcat ("datetime.colon: range endpoints must be finite", ... " (neither NaT nor Inf).")); endif if (! isscalar (step)) error ("datetime.colon: STEP must be a scalar."); endif if (isa (step, 'calendarDuration')) R = colonCalendar (A, step, B); elseif (isa (step, 'duration')) R = colonLinear (A, days (step) * 86400, B); elseif (isnumeric (step) && isreal (step)) R = colonLinear (A, double (step) * 86400, B); else error (strcat ("datetime.colon: STEP must be a duration,", ... " calendarDuration, or numeric scalar.")); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{R} =} linspace (@var{A}, @var{B}) ## @deftypefnx {datetime} {@var{R} =} linspace (@var{A}, @var{B}, @var{N}) ## ## Create linearly spaced datetime values. ## ## @code{@var{R} = linspace (@var{A}, @var{B})} returns a row vector of 100 ## datetime values spaced equally between the scalar datetimes @var{A} and ## @var{B}, both of which are included. ## ## @code{@var{R} = linspace (@var{A}, @var{B}, @var{N})} returns @var{N} ## values instead of 100. A non-integer @var{N} is floored; if @var{N} is ## one, @var{B} alone is returned, and if @var{N} is zero or negative the ## result is empty. ## ## The spacing is by absolute instant, so for zoned inputs it is daylight ## saving aware: a span that loses an hour to a transition is divided into ## equal instants, whose wall-clock readings are therefore not equally ## spaced. The two endpoints may be in different time zones, in which case ## the result is expressed in the zone of @var{A}, but they must either both ## be zoned or both be unzoned. @var{R} carries the @code{Format} and ## @code{TimeZone} of @var{A}. ## ## A Not-A-Time endpoint makes every value that depends on it @code{NaT}, ## leaving only the opposite endpoint; an infinite endpoint likewise carries ## its infinity through the values that depend on it. ## ## @end deftypefn function R = linspace (A, B, n = 100) if (nargin < 2) error ("datetime.linspace: too few input arguments."); endif if (! (isa (A, 'datetime') && isa (B, 'datetime'))) error ("datetime.linspace: both endpoints must be datetime arrays."); endif if (! (isscalar (A) && isscalar (B))) error ("datetime.linspace: endpoints must be datetime scalars."); endif if (xor (isempty (A.TimeZone), isempty (B.TimeZone))) error (strcat ("datetime.linspace: cannot combine a datetime array", ... " with a time zone with one without a time zone.")); endif dtCheckLeapPair (A, B, 'linspace'); if (! (isnumeric (n) && isscalar (n) && isreal (n))) error ("datetime.linspace: N must be a real numeric scalar."); endif R = A; sA = serial (A); sB = serial (B); ## Step from the first instant rather than calling the numeric linspace, ## which spaces a range differently once an endpoint is not finite: MATLAB ## takes each interior point as the first instant plus so many steps, and ## keeps both endpoints exactly. With the step itself infinite or ## Not-A-Number, plain IEEE arithmetic then gives every case MATLAB ## does -- an interior point infinitely far from a finite start is that ## infinity, and one between two infinities, or reached by stepping ## backwards from an infinite start, is Not-A-Time. n = floor (double (n)); if (n < 1) ser = zeros (1, 0); elseif (n == 1) ser = sB; else ser = sA + (0:n-1) * ((sB - sA) / (n - 1)); ser(1) = sA; ser(end) = sB; endif [Y, M, D, h, mi, s, off] = serial2components (A, ser); R.Year = Y; R.Month = M; R.Day = D; R.Hour = h; R.Minute = mi; R.Second = s; R.Offset = off; endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{C} =} plus (@var{A}, @var{B}) ## ## Addition for datetime arrays. ## ## @code{@var{C} = plus (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{C} = @var{A} + @var{B}} and supports the following operand ## combinations, in either order. ## ## @itemize ## @item @code{datetime + duration} returns a @code{datetime} array shifted ## later by a fixed number of 24-hour days. For a zoned array the shift is ## applied to the absolute instant, so it is aware of daylight saving time ## transitions. ## ## @item @code{datetime + calendarDuration} returns a @code{datetime} array ## shifted later in calendar units. Whole months (and years) are applied ## first, clamping the day of month to the last day of the target month when ## necessary (e.g.@: 31 January plus one month is 28 February), then whole ## calendar days, and finally the time-of-day component as an instant. ## ## @item @code{datetime + X}, where @var{X} is a numeric or logical array, ## treats the elements of @var{X} as a number of fixed 24-hour days. ## @end itemize ## ## Adding two datetime arrays is not defined and raises an error. @var{A} ## and @var{B} must be size compatible: they can be the same size, one can ## be scalar, or for every dimension their sizes must be equal or one of ## them must be 1. Not-A-Time and infinite elements propagate to the ## result. ## ## @end deftypefn function C = plus (A, B) ## Addition is symmetric for every supported operand type, so commute the ## operands to keep the datetime array on the left. if (! isa (A, 'datetime')) [A, B] = deal (B, A); endif if (isa (B, 'datetime')) ## datetime + datetime is not defined (MATLAB parity) error (strcat ("datetime.plus: addition is not defined between two", ... " datetime arrays.")); elseif (isa (B, 'duration')) ## datetime + duration -> datetime (fixed-length instant shift) C = addSeconds (A, days (B) * 86400); elseif (isa (B, 'calendarDuration')) ## datetime + calendarDuration -> datetime (calendar-aware shift) C = addCalendar (A, B, 1); elseif (islogical (B) || (isnumeric (B) && isfloat (B))) ## numeric/logical operand: a number of fixed 24-hour days C = addSeconds (A, double (B) * 86400); elseif (isinteger (B)) error (strcat ("datetime.plus: cannot add a '%s' array to a", ... " datetime", ... " array; convert it to double or a duration first."), ... class (B)); else error (strcat ("datetime.plus: cannot add a '%s' array to a", ... " datetime", ... " array."), class (B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{C} =} minus (@var{A}, @var{B}) ## ## Subtraction for datetime arrays. ## ## @code{@var{C} = minus (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{C} = @var{A} - @var{B}} and supports the following operand ## combinations. ## ## @itemize ## @item @code{datetime - datetime} returns a @code{duration} array holding ## the elapsed time between the corresponding elements. Both operands must ## either both have a time zone or both be unzoned; a zoned difference is ## computed from the absolute instants, so the two time zones may differ. ## ## @item @code{datetime - duration} returns a @code{datetime} array shifted ## earlier by a fixed number of 24-hour days. For a zoned array the shift ## is applied to the absolute instant, so it is aware of daylight saving ## time transitions. ## ## @item @code{datetime - calendarDuration} returns a @code{datetime} array ## shifted earlier in calendar units. Whole months (and years) are applied ## first, clamping the day of month to the last day of the target month when ## necessary (e.g.@: 31 March minus one month is 28 February), then whole ## calendar days, and finally the time-of-day component as an instant. ## ## @item @code{datetime - X}, where @var{X} is a numeric or logical array, ## treats the elements of @var{X} as a number of fixed 24-hour days. ## @end itemize ## ## @var{A} and @var{B} must be size compatible: they can be the same size, ## one can be scalar, or for every dimension their sizes must be equal or ## one of them must be 1. Not-A-Time and infinite elements propagate to the ## result. ## ## @end deftypefn function C = minus (A, B) ## Only 'datetime - ' is defined; a datetime array cannot be ## subtracted from a non-datetime left operand (MATLAB parity). if (! isa (A, 'datetime')) error (strcat ("datetime.minus: cannot subtract a datetime array", ... " from a '%s' array."), class (A)); endif if (isa (B, 'datetime')) ## datetime - datetime -> duration (elapsed time) if (xor (isempty (A.TimeZone), isempty (B.TimeZone))) error (strcat ("datetime.minus: cannot subtract a datetime array", ... " with a time zone from one without a time zone.")); endif dtCheckLeapPair (A, B, 'minus'); C = duration (0, 0, elapsedSeconds (A, B)); elseif (isa (B, 'duration')) ## datetime - duration -> datetime (fixed-length instant shift) C = addSeconds (A, - days (B) * 86400); elseif (isa (B, 'calendarDuration')) ## datetime - calendarDuration -> datetime (calendar-aware shift) C = addCalendar (A, B, -1); elseif (islogical (B) || (isnumeric (B) && isfloat (B))) ## numeric/logical operand: a number of fixed 24-hour days C = addSeconds (A, - double (B) * 86400); elseif (isinteger (B)) error (strcat ("datetime.minus: cannot subtract a '%s' array from", ... " a", ... " datetime array; convert it to double or a", ... " duration", ... " first."), class (B)); else error (strcat ("datetime.minus: cannot subtract a '%s' array from", ... " a", ... " datetime array."), class (B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{D} =} diff (@var{A}) ## @deftypefnx {datetime} {@var{D} =} diff (@var{A}, @var{N}) ## @deftypefnx {datetime} {@var{D} =} diff (@var{A}, @var{N}, @var{dim}) ## ## Differences between successive datetime elements. ## ## @code{@var{D} = diff (@var{A})} returns a @code{duration} array holding ## the elapsed time between successive elements of @var{A} along its first ## non-singleton dimension. The differences are computed from the absolute ## instants, so for a zoned array they are aware of daylight saving time ## transitions (a calendar day spanning a transition is 23 or 25 hours, not ## 24). Not-A-Time elements propagate as @qcode{NaN} durations. ## ## @code{@var{D} = diff (@var{A}, @var{N})} applies @code{diff} ## recursively @var{N} times, returning the @var{N}-th order difference. ## @var{N} must be a positive integer scalar. ## ## @code{@var{D} = diff (@var{A}, @var{N}, @var{dim})} operates along ## dimension @var{dim}. ## ## @end deftypefn function D = diff (A, varargin) n = 1; dim = []; if (numel (varargin) > 2) error ("datetime.diff: too many input arguments."); endif if (numel (varargin) >= 1) n = varargin{1}; if (! (isnumeric (n) && isscalar (n) && n > 0 && n == fix (n))) error (strcat ("datetime.diff: order N must be a positive", ... " integer", ... " scalar.")); endif endif if (numel (varargin) >= 2) dim = varargin{2}; if (! (isnumeric (dim) && isscalar (dim) && dim > 0 ... && dim == fix (dim))) error (strcat ("datetime.diff: DIM must be a positive integer", ... " scalar.")); endif endif S = serial (A); if (! isempty (dim) && dim > ndims (S)) ## Differencing along a trailing singleton dimension leaves at most one ## element there, so the result is empty along DIM (MATLAB parity). sz = size (S); sz(end+1:dim) = 1; sz(dim) = 0; DS = zeros (sz); elseif (isempty (dim)) DS = diff (S, n); else DS = diff (S, n, dim); endif D = duration (0, 0, DS); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{D} =} caldiff (@var{A}) ## @deftypefnx {datetime} {@var{D} =} caldiff (@var{A}, @var{components}) ## @deftypefnx {datetime} {@var{D} =} caldiff (@var{A}, @var{components}, @var{dim}) ## ## Calendar differences between successive datetime elements. ## ## @code{@var{D} = caldiff (@var{A})} returns a @code{calendarDuration} ## array holding the calendar difference between successive elements of ## @var{A} along its first non-singleton dimension. Unlike @code{diff}, the ## result is expressed in whole calendar units (years, months, days) plus a ## leftover time, so it is aware of month lengths and, for a zoned array, of ## daylight saving time. Not-A-Time elements yield @qcode{NaN}. ## ## @code{@var{D} = caldiff (@var{A}, @var{components})} expresses each ## difference using only the requested calendar components. @var{components} ## is one of @qcode{'Years'}, @qcode{'Quarters'}, @qcode{'Months'}, ## @qcode{'Weeks'}, @qcode{'Days'}, or @qcode{'Time'}, or a cell array or ## string array containing several of them. The default is ## @qcode{@{'Years', 'Months', 'Days', 'Time'@}}. ## ## @code{@var{D} = caldiff (@var{A}, @var{components}, @var{dim})} operates ## along dimension @var{dim}. ## ## @end deftypefn function D = caldiff (A, varargin) comps = []; dim = []; for k = 1:numel (varargin) x = varargin{k}; if (ischar (x) || iscellstr (x) || isa (x, 'string')) comps = x; elseif (isnumeric (x) && isscalar (x)) dim = x; else error ("datetime.caldiff: invalid input argument."); endif endfor if (isempty (dim)) dim = find (size (A) != 1, 1); if (isempty (dim)) dim = 1; endif endif n = size (A, dim); idx = repmat ({':'}, 1, max (ndims (A), dim)); i1 = idx; i1{dim} = 1:n-1; i2 = idx; i2{dim} = 2:n; D = calDiff (subset (A, i1{:}), subset (A, i2{:}), comps, 'caldiff'); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{D} =} between (@var{A}, @var{B}) ## @deftypefnx {datetime} {@var{D} =} between (@var{A}, @var{B}, @var{components}) ## ## Calendar difference between two datetime arrays. ## ## @code{@var{D} = between (@var{A}, @var{B})} returns a ## @code{calendarDuration} array holding the calendar difference from each ## element of @var{A} to the corresponding element of @var{B}. The result ## is signed (it is negative where @var{B} precedes @var{A}) and is ## expressed in whole calendar units plus a leftover time, taking month ## lengths and daylight saving time into account. @var{A} and @var{B} must ## be the same size or one of them must be scalar. Not-A-Time elements ## yield @qcode{NaN}. ## ## @code{@var{D} = between (@var{A}, @var{B}, @var{components})} expresses ## each difference using only the requested calendar components (see ## @code{caldiff}). The default is @qcode{@{'Years', 'Months', 'Days', ## 'Time'@}}. ## ## @end deftypefn function D = between (A, B, varargin) if (numel (varargin) > 1) error ("datetime.between: too many input arguments."); endif comps = []; if (numel (varargin) == 1) comps = varargin{1}; endif A = dtSetPromote (A, B, 'between'); B = dtSetPromote (B, A, 'between'); [A, B] = prepSetOp (A, B, 'between'); [A, B] = broadcastPair (A, B, 'between'); D = calDiff (A, B, comps, 'between'); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{R} =} dateshift (@var{A}, @qcode{'start'}, @var{unit}) ## @deftypefnx {datetime} {@var{R} =} dateshift (@var{A}, @qcode{'end'}, @var{unit}) ## @deftypefnx {datetime} {@var{R} =} dateshift (@dots{}, @var{rule}) ## @deftypefnx {datetime} {@var{R} =} dateshift (@var{A}, @qcode{'dayofweek'}, @var{dow}) ## @deftypefnx {datetime} {@var{R} =} dateshift (@var{A}, @qcode{'dayofweek'}, @var{dow}, @var{rule}) ## ## Shift datetime values to calendar boundaries. ## ## @code{@var{R} = dateshift (@var{A}, @qcode{'start'}, @var{unit})} returns ## a datetime array in which each element of @var{A} is moved back to the ## start of the calendar @var{unit} that contains it, with the finer ## components set to zero. @var{unit} is @qcode{'second'}, ## @qcode{'minute'}, @qcode{'hour'}, @qcode{'day'}, @qcode{'week'}, ## @qcode{'month'}, @qcode{'quarter'}, or @qcode{'year'}. A week starts on ## Sunday. ## ## @code{@var{R} = dateshift (@var{A}, @qcode{'end'}, @var{unit})} moves ## each element to the end of its unit: the start of the next second, ## minute, hour, or day, and the last day (at midnight) of the week, month, ## quarter, or year. ## ## @code{@var{R} = dateshift (@dots{}, @var{rule})} first shifts each ## element by @var{rule} whole units. @var{rule} is @qcode{'current'} (the ## default), @qcode{'next'}, @qcode{'previous'}, @qcode{'nearest'}, or an ## integer number of units. ## ## @code{@var{R} = dateshift (@var{A}, @qcode{'dayofweek'}, @var{dow})} ## moves each element to the next date, on or after it, whose day of the ## week is @var{dow} (a number from 1 for Sunday to 7 for Saturday, or a day ## name), keeping the time of day. A trailing @var{rule} of ## @qcode{'previous'}, @qcode{'nearest'}, @qcode{'current'} (the day within ## the current week), or an integer occurrence selects a different date. ## ## Not-A-Time and infinite elements are returned unchanged. ## ## @end deftypefn function R = dateshift (this, op, varargin) ops = {'start', 'end', 'dayofweek'}; if (! (ischar (op) && isrow (op) && any (strcmpi (op, ops)))) error (strcat ("datetime.dateshift: second input must be 'start',", ... " 'end', or 'dayofweek'.")); endif op = lower (op); if (numel (varargin) < 1) error ("datetime.dateshift: not enough input arguments."); endif if (numel (varargin) > 2) error ("datetime.dateshift: too many input arguments."); endif Y = this.Year; M = this.Month; D = this.Day; h = this.Hour; mi = this.Minute; s = this.Second; elapsed = 0; keep = isnan (Y) | isinf (Y); if (any (keep(:))) Y(keep) = 2000; M(keep) = 1; D(keep) = 1; h(keep) = 0; mi(keep) = 0; s(keep) = 0; endif if (strcmp (op, 'dayofweek')) dow = varargin{1}; if (ischar (dow) && isrow (dow)) dow = dsDayName (dow); elseif (! (isnumeric (dow) && isscalar (dow))) dow = NaN; endif if (! (isscalar (dow) && dow >= 1 && dow <= 7 && dow == fix (dow))) error (strcat ("datetime.dateshift: day of week must be a number", ... " from 1 to 7 or a day name.")); endif kind = 'next'; n = 0; if (numel (varargin) == 2) [kind, n] = dsRule (varargin{2}); endif dowT = weekday (datenum (Y, M, D)); dNext = mod (dow - dowT, 7); dPrev = mod (dowT - dow, 7); switch (kind) case 'next' delta = dNext; case 'previous' delta = -dPrev; case 'current' delta = dow - dowT; case 'nearest' delta = dNext; useprev = dPrev < dNext; delta(useprev) = -dPrev(useprev); case 'int' if (n >= 1) delta = dNext + (n - 1) * 7; elseif (n <= -1) delta = -(dPrev + (- n - 1) * 7); else delta = dow - dowT; endif endswitch [Y, M, D] = dtAddDays (Y, M, D, delta); else unit = varargin{1}; units = {'second', 'minute', 'hour', 'day', 'week', 'month', ... 'quarter', 'year'}; if (! (ischar (unit) && isrow (unit) && any (strcmpi (unit, units)))) error (strcat ("datetime.dateshift: unit must be 'second',", ... " 'minute', 'hour', 'day', 'week', 'month',", ... " 'quarter', or 'year'.")); endif unit = lower (unit); n = 0; if (numel (varargin) == 2) [kind, nint] = dsRule (varargin{2}); switch (kind) case 'current' n = 0; case 'next' n = 1; case 'previous' n = -1; case 'int' n = nint; case 'nearest' [cY, cM, cD, ch, cm, cs] = dsStartComp (Y, M, D, h, mi, s, unit); sc = dsSerialOf (this, cY, cM, cD, ch, cm, cs); [nY, nM, nD, nh, nm, ns] = ... dsShiftUnits (cY, cM, cD, ch, cm, cs, unit, 1); sn = dsSerialOf (this, nY, nM, nD, nh, nm, ns); st = serial (this); st(keep) = sc(keep); n = double ((st - sc) >= (sn - st)); endswitch endif [Y, M, D, h, mi, s] = dsShiftUnits (Y, M, D, h, mi, s, unit, n); ## The end of a sub-day unit is its start advanced by one unit of ## ELAPSED time, which is not the wall clock one unit later: on the day ## a clock goes back, the hour beginning at 01:00 daylight time ends at ## 01:00 standard time -- a moment the component arithmetic cannot ## name, since it reads 02:00 on a clock that never shows it. Taking ## the start and stepping the instant names it and leaves every other ## case alone, the two agreeing wherever no clock is put back. The ## calendar units keep the component form: their 'end' is a date at ## midnight rather than the following boundary, so it is not a step. ## The leap-second zone keeps it too, for the opposite reason: it has ## no daylight saving to straddle, and a minute there may hold ## sixty-one seconds, so a step of sixty would stop inside it. if (strcmp (op, 'start')) [Y, M, D, h, mi, s] = dsStartComp (Y, M, D, h, mi, s, unit); elseif (any (strcmp (unit, {'second', 'minute', 'hour'})) ... && ! dtIsLeapZone (this.TimeZone)) [Y, M, D, h, mi, s] = dsStartComp (Y, M, D, h, mi, s, unit); switch (unit) case 'second' elapsed = 1; case 'minute' elapsed = 60; case 'hour' elapsed = 3600; endswitch else [Y, M, D, h, mi, s] = dsEndComp (Y, M, D, h, mi, s, unit); endif endif R = this; R.Year = Y; R.Month = M; R.Day = D; R.Hour = h; R.Minute = mi; R.Second = s; ## Checked before the offset rather than in 'normalize' below, because ## the offset is the timezone lookup and so comes first. R.Offset = dtOffsetOf (R.Year, R.Month, R.Day, R.Hour, R.Minute, ... R.Second, R.TimeZone); R = normalize (R); ## Shifting within the day does not move an element out of the fold it is ## in: the start of the hour containing 01:30 daylight time is 01:00 ## daylight time, not the 01:00 that the clock shows again an hour later. srcOff = this.Offset + zeros (size (R.Year)); R = keepFold (R, srcOff); if (elapsed != 0) R = addSeconds (R, elapsed); endif if (any (keep(:))) R.Year(keep) = this.Year(keep); R.Month(keep) = this.Month(keep); R.Day(keep) = this.Day(keep); R.Hour(keep) = this.Hour(keep); R.Minute(keep) = this.Minute(keep); R.Second(keep) = this.Second(keep); R.Offset(keep) = srcOff(keep); endif endfunction endmethods ################################################################################ ## ** Relational Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'lt' 'le' 'gt' 'ge' ## ## 'eq' 'ne' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} lt (@var{A}, @var{B}) ## ## Less-than comparison for datetime arrays. ## ## @code{@var{TF} = lt (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} < @var{B}} and returns a logical array set to ## @qcode{true} where the corresponding element of @var{A} is an earlier ## point in time than that of @var{B}, and @qcode{false} otherwise. ## ## Both operands must be datetime arrays and either both have a time zone or ## both be unzoned; zoned arrays are compared by their absolute instants, so ## the two time zones may differ. @var{A} and @var{B} must be size ## compatible. Not-A-Time compares as @qcode{false} against anything, just ## like @code{NaN}. ## ## @end deftypefn function TF = lt (A, B) TF = relcompare (A, B, 'lt'); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} le (@var{A}, @var{B}) ## ## Less-than-or-equal comparison for datetime arrays. ## ## @code{@var{TF} = le (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} <= @var{B}} and returns a logical array set to ## @qcode{true} where the corresponding element of @var{A} is an earlier or ## equal point in time to that of @var{B}, and @qcode{false} otherwise. ## ## Both operands must be datetime arrays and either both have a time zone or ## both be unzoned; zoned arrays are compared by their absolute instants, so ## the two time zones may differ. @var{A} and @var{B} must be size ## compatible. Not-A-Time compares as @qcode{false} against anything, just ## like @code{NaN}. ## ## @end deftypefn function TF = le (A, B) TF = relcompare (A, B, 'le'); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} gt (@var{A}, @var{B}) ## ## Greater-than comparison for datetime arrays. ## ## @code{@var{TF} = gt (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} > @var{B}} and returns a logical array set to ## @qcode{true} where the corresponding element of @var{A} is a later point ## in time than that of @var{B}, and @qcode{false} otherwise. ## ## Both operands must be datetime arrays and either both have a time zone or ## both be unzoned; zoned arrays are compared by their absolute instants, so ## the two time zones may differ. @var{A} and @var{B} must be size ## compatible. Not-A-Time compares as @qcode{false} against anything, just ## like @code{NaN}. ## ## @end deftypefn function TF = gt (A, B) TF = relcompare (A, B, 'gt'); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} ge (@var{A}, @var{B}) ## ## Greater-than-or-equal comparison for datetime arrays. ## ## @code{@var{TF} = ge (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} >= @var{B}} and returns a logical array set to ## @qcode{true} where the corresponding element of @var{A} is a later or ## equal point in time to that of @var{B}, and @qcode{false} otherwise. ## ## Both operands must be datetime arrays and either both have a time zone or ## both be unzoned; zoned arrays are compared by their absolute instants, so ## the two time zones may differ. @var{A} and @var{B} must be size ## compatible. Not-A-Time compares as @qcode{false} against anything, just ## like @code{NaN}. ## ## @end deftypefn function TF = ge (A, B) TF = relcompare (A, B, 'ge'); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} eq (@var{A}, @var{B}) ## ## Equality comparison for datetime arrays. ## ## @code{@var{TF} = eq (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} == @var{B}} and returns a logical array set to ## @qcode{true} where the corresponding elements of @var{A} and @var{B} are ## the same point in time, and @qcode{false} otherwise. ## ## Both operands must be datetime arrays and either both have a time zone or ## both be unzoned; zoned arrays are compared by their absolute instants, so ## the two time zones may differ. @var{A} and @var{B} must be size ## compatible. Not-A-Time is never equal to anything, including another ## Not-A-Time, just like @code{NaN}. ## ## @end deftypefn function TF = eq (A, B) TF = relcompare (A, B, 'eq'); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{TF} =} ne (@var{A}, @var{B}) ## ## Inequality comparison for datetime arrays. ## ## @code{@var{TF} = ne (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} != @var{B}} and returns a logical array set to ## @qcode{true} where the corresponding elements of @var{A} and @var{B} are ## not the same point in time, and @qcode{false} otherwise. ## ## Both operands must be datetime arrays and either both have a time zone or ## both be unzoned; zoned arrays are compared by their absolute instants, so ## the two time zones may differ. @var{A} and @var{B} must be size ## compatible. Not-A-Time is never equal to anything, so it compares as ## @qcode{true} against everything, including another Not-A-Time, just like ## @code{NaN}. ## ## @end deftypefn function TF = ne (A, B) TF = relcompare (A, B, 'ne'); endfunction endmethods ################################################################################ ## ** Array Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'cat' 'horzcat' 'vertcat' 'repmat' ## ## 'repelem' 'repelems' 'reshape' 'circshift' ## ## 'permute' 'ipermute' 'transpose' 'ctranspose' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {datetime} {@var{C} =} cat (@var{dim}, @var{A}, @var{B}, @dots{}) ## ## Concatenate datetime arrays. ## ## @code{@var{C} = cat (@var{dim}, @var{A}, @var{B}, @dots{})} concatenates ## the inputs @var{A}, @var{B}, @dots{} along dimension @var{dim}. They ## must have the same size except along the operating dimension @var{dim}. ## ## At least one input must be a datetime array. Date/time text, whether a ## character vector, a cellstr, or a @code{string} array, is converted; a ## @math{0*0} empty @code{[]} or @code{@{@}} contributes nothing and is ## dropped, so an array may be accumulated from an empty start. Any other ## input, an empty numeric array of non-zero size included, is an error. ## ## The first datetime input gives the result its @code{Format} and ## @code{TimeZone}, and is what date/time text is read against: such text ## names a wall clock in that time zone rather than an instant to be ## converted into it. Text may therefore appear first, as in ## @code{[@qcode{'2024-01-01'}, @var{T}]}, and still be read in @var{T}'s ## zone. ## ## Zoned inputs need not share a time zone; each is converted into the zone ## of the first, which preserves the instant every element names and changes ## only its wall-clock reading. A zoned input cannot be concatenated with ## an unzoned one, in either order: a wall clock that names no instant and ## one that does do not belong in the same array. A bare @code{NaT} and ## @code{datetime.empty} are unzoned and so take part in that rule, whereas ## @code{NaT (@qcode{'TimeZone'}, @var{tz})} does not. ## ## Date/time text that is empty names no date and becomes @code{NaT}, so an ## empty character vector adds one missing element, whereas an empty ## @code{@{@}} holds no text at all and adds none. ## ## @end deftypefn function out = cat (dim, varargin) args = varargin; isdt = cellfun (@(x) isa (x, 'datetime'), args); ## An operand that is 0-by-0 and not a datetime -- the [] or {} an array ## is often accumulated from -- contributes nothing and is dropped. An ## empty datetime is kept, since whether it carries a time zone still ## counts towards the test below, and so is an empty character vector, ## which is one piece of text naming no date and becomes a NaT. drop = ! isdt & cellfun (@(x) isequal (size (x), [0, 0]) ... && ! ischar (x), args); args(drop) = []; isdt(drop) = []; ## The first datetime operand gives the result its Format and TimeZone, ## and is what any date/time text is read against: such text names a wall ## clock in that zone rather than an instant to be converted into it, the ## rule the set operations and 'isbetween' already follow. out = args{find (isdt, 1)}; for k = find (! isdt) args{k} = dtCatPromote (args{k}, out); endfor ## Every operand must agree on whether it is zoned at all, as in MATLAB: ## a wall clock that names no instant and one that does cannot sit in the ## same array. zoned = cellfun (@(x) ! isempty (x.TimeZone), args); if (any (zoned) && ! all (zoned)) error (strcat ("datetime.cat: cannot concatenate a datetime array", ... " that has a time zone with one that does not have", ... " a", ... " time zone.")); endif ## Zoned operands need not share a zone. Concatenating changes which ## array an element belongs to, not the instant it names, so an operand ## from another zone is converted into the zone of the first rather than ## having its wall clock read as if it had always been there. This also ## makes the leap-second check, which cannot be met by a conversion. for k = 1:numel (args) [~, args{k}] = prepSetOp (out, args{k}, 'cat'); endfor fieldArgs = cellfun (@(obj) obj.Year, args, 'UniformOutput', false); out.Year = cat (dim, fieldArgs{:}); fieldArgs = cellfun (@(obj) obj.Month, args, 'UniformOutput', false); out.Month = cat (dim, fieldArgs{:}); fieldArgs = cellfun (@(obj) obj.Day, args, 'UniformOutput', false); out.Day = cat (dim, fieldArgs{:}); fieldArgs = cellfun (@(obj) obj.Hour, args, 'UniformOutput', false); out.Hour = cat (dim, fieldArgs{:}); fieldArgs = cellfun (@(obj) obj.Minute, args, 'UniformOutput', false); out.Minute = cat (dim, fieldArgs{:}); fieldArgs = cellfun (@(obj) obj.Second, args, 'UniformOutput', false); out.Second = cat (dim, fieldArgs{:}); fieldArgs = cellfun (@(obj) obj.Offset, args, 'UniformOutput', false); out.Offset = cat (dim, fieldArgs{:}); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{C} =} horzcat (@var{A}, @var{B}, @dots{}) ## ## Horizontal concatenation of datetime arrays. ## ## @code{@var{C} = horzcat (@var{A}, @var{B}, @dots{}} is the equivalent of ## the syntax @code{@var{B} = [@var{A}, @var{B}, @dots{}]} and horizontally ## concatenates the inputs @var{A}, @var{B}, @dots{}, which must have the ## same size except along the second dimension. See @code{cat} for which ## inputs are accepted and how their time zones are resolved. ## ## @end deftypefn function out = horzcat (varargin) out = cat (2, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{C} =} vertcat (@var{A}, @var{B}, @dots{}) ## ## Vertical concatenation of datetime arrays. ## ## @code{@var{C} = vertcat (@var{A}, @var{B}, @dots{}} is the equivalent of ## the syntax @code{@var{B} = [@var{A}; @var{B}; @dots{}]} and vertically ## concatenates the inputs @var{A}, @var{B}, @dots{}, which must have the ## same size except along the first dimension. See @code{cat} for which ## inputs are accepted and how their time zones are resolved. ## ## @end deftypefn function out = vertcat (varargin) out = cat (1, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{B} =} repmat (@var{A}, @var{n}) ## @deftypefnx {datetime} {@var{B} =} repmat (@var{A}, @var{d1}, @dots{}, @var{dN}) ## @deftypefnx {datetime} {@var{B} =} repmat (@var{A}, @var{dimvec}) ## ## Repeat copies of a datetime array. ## ## @code{@var{B} = repmat (@var{A}, @var{n})} returns a datetime array ## @var{B} containing @var{n} copies of the input datetime array @var{A} ## along every dimension of @var{A}. ## ## @code{@var{B} = repmat (@var{A}, @var{d1}, @dots{}, @var{dN})} returns an ## array @var{B} containing copies of @var{A} along the dimensions specified ## by the list of scalar integer values @var{d1}, @dots{}, @var{dN}, which ## specify how many copies of @var{A} are made in each dimension. ## ## @code{@var{B} = repmat (@var{A}, @var{dimvec})} is equivalent to the ## previous syntax with @code{@var{dimvec} = [@var{d1}, @dots{}, @var{dN}]}. ## ## @end deftypefn function this = repmat (this, varargin) this.Year = repmat (this.Year, varargin{:}); this.Month = repmat (this.Month, varargin{:}); this.Day = repmat (this.Day, varargin{:}); this.Hour = repmat (this.Hour, varargin{:}); this.Minute = repmat (this.Minute, varargin{:}); this.Second = repmat (this.Second, varargin{:}); this.Offset = repmat (this.Offset, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{B} =} repelem (@var{A}, @var{n}) ## @deftypefnx {datetime} {@var{B} =} repelem (@var{A}, @var{d1}, @dots{}, @var{dN}) ## ## Repeat copies of datetime array elements. ## ## @code{@var{B} = repelem (@var{A}, @var{n})} returns a datetime vector ## @var{B} containing repeated elements of the input @var{A}, which must be ## a datetime vector. If @var{n} is a scalar, each element of @var{A} is ## repeated @var{n} times along the non-singleton dimension of @var{A}. If ## @var{n} is a vector, it must have the same elements as @var{A}, in which ## case it specifies the number of times to repeat each corresponding ## element of @var{A}. ## ## @code{@var{B} = repelem (@var{A}, @var{d1}, @dots{}, @var{dN})} returns ## an array @var{B} with each element of @var{A} repeated according to the ## the list of input arguments @code{@var{d1}, @dots{}, @var{dN}} each ## corresponding to a different dimension @code{1:ndims (@var{A})} of the ## input array @var{A}. @var{d1}, @dots{}, @var{dN} must be either scalars ## or vectors with the same length as the corresponding dimension of ## @var{A} containing non-negative integer values specifying the number of ## repetitions of each element along the corresponding dimension. ## ## @end deftypefn function this = repelem (this, varargin) this.Year = repelem (this.Year, varargin{:}); this.Month = repelem (this.Month, varargin{:}); this.Day = repelem (this.Day, varargin{:}); this.Hour = repelem (this.Hour, varargin{:}); this.Minute = repelem (this.Minute, varargin{:}); this.Second = repelem (this.Second, varargin{:}); this.Offset = repelem (this.Offset, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{B} =} repelems (@var{A}, @var{R}) ## ## Construct a vector of repeated elements from a datetime array. ## ## @code{@var{B} = repelems (@var{A}, @var{R})} returns a datetime vector ## @var{B} containing repeated elements of the input @var{A}, which must be ## a datetime vector. @var{R} must be a @math{2*N} matrix of integers. ## Entries in the first row of @var{R} correspond to the linear indexing of ## the elements in @var{A} to be repeated. The corresponding entries in the ## second row of @var{R} specify the repeat count of each element. ## ## @end deftypefn function this = repelems (this, R) this.Year = repelems (this.Year, R); this.Month = repelems (this.Month, R); this.Day = repelems (this.Day, R); this.Hour = repelems (this.Hour, R); this.Minute = repelems (this.Minute, R); this.Second = repelems (this.Second, R); this.Offset = repelems (this.Offset, R); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{B} =} reshape (@var{A}, @var{d1}, @dots{}, @var{dN}) ## @deftypefnx {datetime} {@var{B} =} reshape (@var{A}, @dots{}, @qcode{[]}, @dots{}) ## @deftypefnx {datetime} {@var{B} =} reshape (@var{A}, @var{dimvec}) ## ## Reshape datetime array. ## ## @code{@var{B} = reshape (@var{A}, @var{d1}, @dots{}, @var{dN})} returns ## a datetime array @var{B} with specified dimensions @var{d1}, @dots{}, ## @var{dN}, whose elements are taken columnwise from the datetime array ## @var{A}. The product of @var{d1}, @dots{}, @var{dN} must equal the total ## number of elements in @var{A}. ## ## @code{@var{B} = reshape (@var{A}, @dots{}, @qcode{[]}, @dots{})} returns ## a datetime array @var{B} with one dimension unspecified which is ## calculated automatically so that the product of dimensions in @var{B} ## matches the total elements in @var{A}, which must be divisible the ## product of specified dimensions. An empty matrix @qcode{([])} is used to ## flag the unspecified dimension. ## ## @end deftypefn function this = reshape (this, varargin) this.Year = reshape (this.Year, varargin{:}); this.Month = reshape (this.Month, varargin{:}); this.Day = reshape (this.Day, varargin{:}); this.Hour = reshape (this.Hour, varargin{:}); this.Minute = reshape (this.Minute, varargin{:}); this.Second = reshape (this.Second, varargin{:}); this.Offset = reshape (this.Offset, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{B} =} circshift (@var{A}, @var{n}) ## @deftypefnx {datetime} {@var{B} =} circshift (@var{A}, @var{n}, @var{dim}) ## ## Circularly shift the elements in a datetime array. ## ## @code{@var{B} = circshift (@var{A}, @var{n})} circularly shifts the ## elements of the datetime array @var{A} according to @var{n}. If @var{n} ## is a nonzero integer scalar, then the elements of @var{A} are shifted by ## @var{n} elements along the first non-singleton dimension of @var{A}. If ## @var{n} is a vector, it must not be longer that the number of dimensions ## of @var{A} with each value of @var{n} corresponding to a dimension in ## @var{A}. The sign of the value(s) in @var{n} specify the direction in ## the elements of @var{A} are shifted. ## ## @code{@var{B} = circshift (@var{A}, @var{n}, @var{dim})} circularly ## shifts the elements of the datetime array @var{A} along the dimension ## specified by @var{dim}. In this case, @var{n} must be a scalar integer ## value. ## ## @end deftypefn function this = circshift (this, varargin) this.Year = circshift (this.Year, varargin{:}); this.Month = circshift (this.Month, varargin{:}); this.Day = circshift (this.Day, varargin{:}); this.Hour = circshift (this.Hour, varargin{:}); this.Minute = circshift (this.Minute, varargin{:}); this.Second = circshift (this.Second, varargin{:}); this.Offset = circshift (this.Offset, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{B} =} permute (@var{A}, @var{dims}) ## ## Generalized transpose for a datetime N-D array. ## ## @code{@var{B} = permute (@var{A}, @var{dims})} returns the generalized ## transpose of the datetime array @var{A} by rearranging its dimensions ## according to the permutation vector specified in @var{dims}. ## ## @var{dims} must index all the dimensions @code{1:ndims (@var{A})} of the ## input array @var{A}, in any order, but only once. The @var{N}th ## dimension of @var{A} gets remapped to the dimension in @var{B} specified ## by @code{@var{dims}(@var{N})}. ## ## @end deftypefn function this = permute (this, order) this.Year = permute (this.Year, order); this.Month = permute (this.Month, order); this.Day = permute (this.Day, order); this.Hour = permute (this.Hour, order); this.Minute = permute (this.Minute, order); this.Second = permute (this.Second, order); this.Offset = permute (this.Offset, order); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{A} =} ipermute (@var{B}, @var{dims}) ## ## Inverse of the generalized transpose for a datetime N-D array. ## ## @code{@var{A} = ipermute (@var{B}, @var{dims})} returns the inverse of ## the generalized transpose performed by the @code{permute} function. The ## expression @code{ipermute (permute (@var{A}, @var{dims}), @var{dims})} ## returns the original array @var{A}. ## ## @var{dims} must index all the dimensions @code{1:ndims (@var{B})} of the ## input array @var{B}, in any order, but only once. The dimension of ## @var{B} specified in @code{@var{dims}(@var{N})} gets remapped to the ## @var{N}th dimension of @var{A}. ## ## @end deftypefn function this = ipermute (this, order) this.Year = ipermute (this.Year, order); this.Month = ipermute (this.Month, order); this.Day = ipermute (this.Day, order); this.Hour = ipermute (this.Hour, order); this.Minute = ipermute (this.Minute, order); this.Second = ipermute (this.Second, order); this.Offset = ipermute (this.Offset, order); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{B} =} transpose (@var{A}) ## ## Transpose a datetime matrix. ## ## @code{@var{B} = transpose (@var{A})} is the equivalent of the syntax ## @code{@var{B} = @var{A}.'} and returns the transpose of the datetime ## matrix @var{A}. ## ## @end deftypefn function this = transpose (this) this.Year = transpose (this.Year); this.Month = transpose (this.Month); this.Day = transpose (this.Day); this.Hour = transpose (this.Hour); this.Minute = transpose (this.Minute); this.Second = transpose (this.Second); this.Offset = transpose (this.Offset); endfunction ## -*- texinfo -*- ## @deftypefn {datetime} {@var{B} =} ctranspose (@var{A}) ## ## Transpose a datetime matrix. ## ## @code{@var{B} = ctranspose (@var{A})} is the equivalent of the syntax ## @code{@var{B} = @var{A}'} and returns the transpose of the datetime ## matrix @var{A}. For datetime arrays, @code{ctranspose} is identical to ## @code{transpose}. ## ## @end deftypefn function this = ctranspose (this) this.Year = ctranspose (this.Year); this.Month = ctranspose (this.Month); this.Day = ctranspose (this.Day); this.Hour = ctranspose (this.Hour); this.Minute = ctranspose (this.Minute); this.Second = ctranspose (this.Second); this.Offset = ctranspose (this.Offset); endfunction endmethods ################################################################################ ## ** Reference and Assignment Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'end' 'subsref' 'subsasgn' ## ## ## ################################################################################ methods (Hidden) ## Overloaded end keyword function last_index = end (this, end_dim, ndim_obj) lastdim = ndims (this); if (end_dim == ndim_obj && ndim_obj == 1) last_index = prod (size (this)); elseif (end_dim == ndim_obj && end_dim < lastdim) last_index = prod (size (this)(end_dim:lastdim)); else last_index = size (this, end_dim); endif endfunction ## Class specific subscripted reference function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case '()' out = this; out.Year = this.Year(s.subs{:}); out.Month = this.Month(s.subs{:}); out.Day = this.Day(s.subs{:}); out.Hour = this.Hour(s.subs{:}); out.Minute = this.Minute(s.subs{:}); out.Second = this.Second(s.subs{:}); out.Offset = this.Offset(s.subs{:}); case '{}' error (strcat ("datetime.subsref: '{}' invalid indexing", ... " for referencing values. Use '()' instead.")); case '.' ## A field name may be given as a string scalar, as in MATLAB. if (isstring (s.subs) && isscalar (s.subs)) s.subs = char (s.subs); endif if (! (ischar (s.subs) && isrow (s.subs))) error (strcat ("datetime.subsref: '.' index argument must", ... " be a character vector or a string scalar.")); endif switch (s.subs) case 'proxyArray' # used by 'table' class out = proxyArray (this); case 'Format' out = dtResolveFormat (this.Format, this.Hour, this.Minute, ... this.Second); case 'SystemTimeZone' out = datetime.SystemTimeZone (); case 'TimeZone' out = this.TimeZone; case {'Year'} out = this.Year; case {'Month'} out = this.Month; case {'Day'} out = this.Day; case {'Hour'} out = this.Hour; case {'Minute'} out = this.Minute; case {'Second'} out = this.Second; otherwise error ("datetime.subsref: unrecognized property: %s", s.subs); endswitch endswitch ## Chained references if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction ## Class specific subscripted assignment function this = subsasgn (this, s, val) if (numel (s) > 1) # error ("duration.subsasgn: chained subscripts not allowed."); p = s(2); s = s(1); if (! strcmp (p.type, '()')) error (strcat ("datetime.subsasgn: '%s' invalid indexing", ... " for assigning values. Use '()' instead."), p.type); endif else p.subs = {':'}; endif switch s.type case '()' if (isempty (val)) this.Year(s.subs{:}) = []; this.Month(s.subs{:}) = []; this.Day(s.subs{:}) = []; this.Hour(s.subs{:}) = []; this.Minute(s.subs{:}) = []; this.Second(s.subs{:}) = []; this.Offset(s.subs{:}) = []; return; elseif (isa (val, "missing")) ## A missing value assigns NaT, in the array's own zone so that ## the assignment cannot change whether the array is zoned. if (isempty (this.TimeZone)) val = NaT (size (val)); else val = NaT (size (val), 'TimeZone', this.TimeZone); endif elseif (! isa (val, "datetime")) error (strcat ("datetime.subsasgn: cannot assign %s values", ... " to a datetime array."), class (val)); endif ## A 'NaT' names no instant, so it carries no zone that could ## disagree with the array's own and MATLAB assigns one into a ## zoned array as readily as it assigns 'missing'. It is brought ## to the array's zone here, so that the check below -- which is ## right for every datetime that does name an instant -- has ## nothing left to refuse. if (isa (val, "datetime") && ! isempty (this.TimeZone) && isempty (val.TimeZone)) miss = ismissing (val); if (! isempty (miss) && all (miss(:))) val = NaT (size (val), 'TimeZone', this.TimeZone); endif endif ## An element arriving from another zone is CONVERTED into this ## array's, as it is when concatenated: assignment changes which array ## an element belongs to, not the instant it names. Going through ## 'prepSetOp' also refuses the two mixtures that have no answer -- ## zoned with unzoned, and the leap-second timeline with any other -- ## which is what MATLAB does here as well. Without it the operand was ## copied across whole, so the array held an element carrying another ## zone's offset: it displayed in a zone it was not in, compared ## unequal to the value it had been assigned from, and reported an ## offset its own array could not account for. [~, val] = prepSetOp (this, val, 'subsasgn'); ## Track which positions are real so that any elements created by ## growing the array (out-of-range assignment) can be filled with ## Not-A-Time; Octave would otherwise pad them with 0, i.e. the ## invalid 0000-00-00 datetime (MATLAB pads such gaps with NaT). filled = true (size (this.Year)); filled(s.subs{:}) = true; this.Year(s.subs{:}) = val.Year; this.Month(s.subs{:}) = val.Month; this.Day(s.subs{:}) = val.Day; this.Hour(s.subs{:}) = val.Hour; this.Minute(s.subs{:}) = val.Minute; this.Second(s.subs{:}) = val.Second; this.Offset(s.subs{:}) = val.Offset; gap = ! filled; if (any (gap(:))) this.Year(gap) = NaN; this.Month(gap) = NaN; this.Day(gap) = NaN; this.Hour(gap) = NaN; this.Minute(gap) = NaN; this.Second(gap) = NaN; this.Offset(gap) = NaN; endif case '{}' error (strcat ("datetime.subsasgn: '{}' invalid indexing", ... " for assigning values. Use '()' instead.")); case '.' ## A field name may be given as a string scalar, as in MATLAB. if (isstring (s.subs) && isscalar (s.subs)) s.subs = char (s.subs); endif if (! (ischar (s.subs) && isrow (s.subs))) error (strcat ("datetime.subsasgn: '.' index argument must", ... " be a character vector or a string scalar.")); endif switch (s.subs) case 'Format' if (! (ischar (val) && (isrow (val) || isempty (val)))) error (strcat ("datetime.subsasgn: 'Format' must be a", ... " character vector.")); endif if (dtIsLeapZone (this.TimeZone)) dtValidateLeapFormat (val, 'datetime.subsasgn'); else dtValidateFormat (val); endif this.Format = val; case 'TimeZone' toTimeZone = val; if (! (ischar (toTimeZone) && (isrow (toTimeZone) ... || isempty (toTimeZone)))) error (strcat ("datetime.subsasgn: 'TimeZone' must be a", ... " character vector.")); endif if (strcmpi (toTimeZone, 'local')) toTimeZone = datetime.SystemTimeZone; endif if (! isempty (toTimeZone)) toTimeZone = dtZoneName (toTimeZone, 'datetime.subsasgn'); endif ## Validate the target zone (empty means an unzoned array). if (! isempty (toTimeZone)) [~,~,~,~,~,~,errmsg] = __datetime__ (0, 0, 0, ... 'TimeZone', toTimeZone); if (! isnumeric (errmsg)) error ("datetime.subsasgn: %s", errmsg); endif endif ## Assigning the zone an array already has changes nothing. Worth ## short-circuiting rather than round-tripping, because for the ## leap-second zone the round trip is not the identity: it would ## roll a 60th second over. if (strcmp (this.TimeZone, toTimeZone)) return; endif ## Either side being a real zone means the components have to be ## inside what the timezone database can answer for, whatever the ## calendar can carry. Both are asked, since the wall clock is ## read in one zone and written in the other. wasLeap = dtIsLeapZone (this.TimeZone); nowLeap = dtIsLeapZone (toTimeZone); if (wasLeap) ## No other zone has a 60th second for an inserted one to move ## into, so it folds back onto the 59th, and the display format ## reverts to the data-dependent default. this.Second = dtLeapBackFold (this.Second); this.Format = 'default'; endif if (isempty (toTimeZone)) ## Dropping the zone keeps the wall-clock values as they are. this.TimeZone = toTimeZone; this.Offset = zeros (size (this.Year)); elseif (isempty (this.TimeZone)) ## Attaching a zone to an unzoned array reinterprets the ## wall-clock values in that zone without converting, but a ## wall clock the local clock never shows moves ahead by the ## gap, exactly as in the constructor. [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second, errmsg] = __datetime__ (this.Year, this.Month, ... this.Day, this.Hour, this.Minute, this.Second, ... 'TimeZone', toTimeZone, 'toTimeZone', toTimeZone, ... 'Precision', 'microseconds'); if (! isnumeric (errmsg)) error ("datetime.subsasgn: %s", errmsg); endif this.TimeZone = toTimeZone; this.Offset = dtOffsetOf (this.Year, this.Month, this.Day, ... this.Hour, this.Minute, this.Second, toTimeZone); elseif (wasLeap || nowLeap) ## Switching between two zones preserves the absolute instant, ## so the wall-clock values shift by the offset difference. [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second, errmsg] = __datetime__ (this.Year, this.Month, ... this.Day, this.Hour, this.Minute, this.Second, ... 'TimeZone', this.TimeZone, 'toTimeZone', toTimeZone, ... 'Precision', 'microseconds'); if (! isnumeric (errmsg)) error ("datetime.subsasgn: %s", errmsg); endif this.TimeZone = toTimeZone; this.Offset = dtOffsetOf (this.Year, this.Month, this.Day, ... this.Hour, this.Minute, this.Second, toTimeZone); else ## Switching zones preserves the absolute INSTANT, so it is ## taken from the stored offset rather than re-resolved from ## the wall clock: an element on the earlier pass of a repeated ## hour keeps its identity instead of collapsing to the later. ser = serial (this); this.TimeZone = toTimeZone; [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second, this.Offset] = serial2components (this, ser); endif if (nowLeap) this.Format = dtLeapFormat (); endif case {'Year'} this.Year(p.subs{:}) = val; this = normalize (this); case {'Month'} this.Month(p.subs{:}) = val; this = normalize (this); case {'Day'} this.Day(p.subs{:}) = val; this = normalize (this); case {'Hour'} this.Hour(p.subs{:}) = val; this = normalize (this); case {'Minute'} this.Minute(p.subs{:}) = val; this = normalize (this); case {'Second'} this.Second(p.subs{:}) = val; this = normalize (this); otherwise error ("datetime.subsasgn: unrecognized property: %s", s.subs); endswitch endswitch endfunction endmethods methods (Access = private) ## Return true if the rows of the numeric matrix KD are in lexicographic ## non-decreasing order (used by 'issortedrows'). Equal entries (including ## the +Inf sentinel used for NaT, and genuine infinities) count as ties and ## are resolved by the next column. function tf = rowsNonDecreasing (this, KD) m = rows (KD); if (m < 2) tf = true; return; endif a = KD(1:end-1, :); b = KD(2:end, :); undecided = true (m - 1, 1); bad = false (m - 1, 1); for j = 1:columns (KD) lt = a(:, j) < b(:, j); gt = a(:, j) > b(:, j); bad = bad | (undecided & gt); undecided = undecided & ! (lt | gt); endfor tf = ! any (bad); endfunction ## Shared engine for 'min' and 'max'. ARGS is the method's varargin, ISMAX ## selects max over min, and NOUT is the caller's nargout. Handles both the ## reduction form (min (A), min (A, [], DIM), '-all', nan flags) and the ## two-array elementwise form (min (A, B)). function [M, I] = minmaxImpl (A, args, ismax, nout) if (ismax) fname = 'max'; else fname = 'min'; endif I = []; ## Two-array elementwise form is signalled by a non-'[]' second argument. elementwise = ! isempty (args) ... && ! (isnumeric (args{1}) && isempty (args{1})); if (elementwise) if (nout > 1) error (strcat ("datetime.", fname, ": a second output is not", ... " supported when comparing two arrays.")); endif B = args{1}; if (! isa (B, 'datetime')) error (strcat ("datetime.", fname, ": comparison of two arrays", ... " requires both to be datetime.")); endif if (xor (isempty (A.TimeZone), isempty (B.TimeZone))) error (strcat ("datetime.", fname, ": cannot compare a datetime", ... " with a time zone to one without a time zone.")); endif dtCheckLeapPair (A, B, fname); nanflag = 'omitnan'; for k = 2:numel (args) x = args{k}; if (ischar (x) && isrow (x) && strcmpi (x, 'omitnan')) nanflag = 'omitnan'; elseif (ischar (x) && isrow (x) && strcmpi (x, 'includenan')) nanflag = 'includenan'; else error (strcat ("datetime.", fname, ": invalid option in a", ... " two-array comparison.")); endif endfor ## Compare by absolute instant (zone-independent), but pick exact ## component values from whichever operand wins to avoid a lossy ## instant round-trip. SA = serial (A); SB = serial (B); common = size (SA + SB); SA = SA + zeros (common); SB = SB + zeros (common); if (ismax) takeA = (SA >= SB) | isnan (SB); else takeA = (SA <= SB) | isnan (SB); endif ## Express B's components in A's time zone so selected values are ## exact. This is the same instant-preserving conversion the set ## operations need, so it goes through 'prepSetOp' rather than being ## repeated here -- which is also what keeps B's offset in step with ## its new wall clock, since the two are picked together below. [~, B] = prepSetOp (A, B, fname); YB = B.Year; MB = B.Month; DB = B.Day; hB = B.Hour; mB = B.Minute; sB = B.Second; z = zeros (common); Y = A.Year + z; Mo = A.Month + z; D = A.Day + z; h = A.Hour + z; mi = A.Minute + z; s = A.Second + z; of = A.Offset + z; ofB = B.Offset + z; YB = YB + z; MB = MB + z; DB = DB + z; hB = hB + z; mB = mB + z; sB = sB + z; takeB = ! takeA; Y(takeB) = YB(takeB); Mo(takeB) = MB(takeB); D(takeB) = DB(takeB); h(takeB) = hB(takeB); mi(takeB) = mB(takeB); s(takeB) = sB(takeB); of(takeB) = ofB(takeB); if (strcmp (nanflag, 'includenan')) nanpos = isnan (SA) | isnan (SB); Y(nanpos) = NaN; Mo(nanpos) = NaN; D(nanpos) = NaN; h(nanpos) = NaN; mi(nanpos) = NaN; s(nanpos) = NaN; of(nanpos) = NaN; endif M = A; M.Year = Y; M.Month = Mo; M.Day = D; M.Hour = h; M.Minute = mi; M.Second = s; M.Offset = of; return; endif ## Reduction form. Skip the '[]' placeholder, then read DIM / 'all' / ## the NaN flag from the remaining arguments. rest = args; if (! isempty (rest)) rest = rest(2:end); endif dim = []; allflag = false; nanflag = 'omitnan'; for k = 1:numel (rest) x = rest{k}; if (isnumeric (x)) if (! isscalar (x) || x < 1 || x != fix (x)) error (strcat ("datetime.", fname, ... ": DIM must be a positive integer.")); endif dim = x; elseif (ischar (x) && isrow (x)) if (strcmpi (x, 'all')) allflag = true; elseif (strcmpi (x, 'omitnan')) nanflag = 'omitnan'; elseif (strcmpi (x, 'includenan')) nanflag = 'includenan'; else error (strcat ("datetime.", fname, ": invalid option '", x, "'.")); endif else error (strcat ("datetime.", fname, ": invalid input argument.")); endif endfor if (isempty (A)) M = A; return; endif S = serial (A); if (allflag) Sv = S(:); if (ismax) [~, iv] = max (Sv, [], 1); else [~, iv] = min (Sv, [], 1); endif if (strcmp (nanflag, 'includenan') && any (isnan (Sv))) iv = find (isnan (Sv), 1); endif M = subset (A, iv); I = iv; return; endif if (isempty (dim)) dim = find (size (A) != 1, 1); if (isempty (dim)) dim = 1; endif endif if (size (A, dim) < 2) M = A; I = ones (size (A)); return; endif ## Ask for the winner's position both ways: as a subscript along DIM, ## which is what the second output promises, and as a linear index, which ## is what picking the winning elements out of the component arrays needs. ## The linear form comes from the built-in rather than being worked out ## here, since doing that arithmetic by hand only holds for a matrix and ## quietly addresses the wrong page of anything larger. if (ismax) [~, I] = max (S, [], dim); [~, lin] = max (S, [], dim, 'linear'); else [~, I] = min (S, [], dim); [~, lin] = min (S, [], dim, 'linear'); endif if (strcmp (nanflag, 'includenan')) nanmask = any (isnan (S), dim); [~, firstnan] = max (isnan (S), [], dim); [~, firstnanlin] = max (isnan (S), [], dim, 'linear'); I(nanmask) = firstnan(nanmask); lin(nanmask) = firstnanlin(nanmask); endif M = subset (A, lin); endfunction ## Return a subset of the array function this = subset (this, varargin) this = this; this.Year = this.Year(varargin{:}); this.Month = this.Month(varargin{:}); this.Day = this.Day(varargin{:}); this.Hour = this.Hour(varargin{:}); this.Minute = this.Minute(varargin{:}); this.Second = this.Second(varargin{:}); this.Offset = this.Offset(varargin{:}); endfunction ## Numeric proxy used by 'table' and set operations for sorting, grouping, ## and set membership. Each datetime element maps to its six canonical ## components [Year, Month, Day, Hour, Minute, Second] followed by the ## negated UTC offset; for a datetime matrix, each column contributes a ## seven-column block. Not-A-Time (NaT) elements map to NaN across their ## components, just like the stored arrays. ## ## The offset is carried because the components alone do not identify an ## element: across a fall-back one tuple names two moments, which must ## group and sort apart. It is negated so that the larger offset -- the ## earlier moment -- sorts first, leaving the block in chronological order. function out = proxyArray (this) [~, cols] = size (this.Year); off = this.Offset + zeros (size (this.Year)); ## An unzoned array stores a zero offset for every element, Not-A-Time ## included; the proxy answers NaN across all of a NaT's columns, so the ## seventh is masked to match the six beside it. off(isnan (this.Year)) = NaN; if (cols > 1) out = []; for i = 1:cols SC = [this.Year(:,i), this.Month(:,i), this.Day(:,i), ... this.Hour(:,i), this.Minute(:,i), this.Second(:,i), ... -off(:,i)]; out = [out, SC]; endfor else out = [this.Year(:), this.Month(:), this.Day(:), ... this.Hour(:), this.Minute(:), this.Second(:), -off(:)]; endif endfunction ## Re-canonicalise the component arrays after a direct component assignment ## (e.g. 'd.Month = 13' rolls the extra month into the year). This routes ## the raw values back through the same C++ normaliser used by the ## constructor, at microsecond precision so no sub-second detail is lost. ## Not-A-Time and infinite elements are passed through unchanged. function this = normalize (this) if (isempty (this.Year)) this.Offset = zeros (size (this.Year)); return; endif if (dtIsLeapZone (this.TimeZone)) [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second] = dtLeapNormalize (this.Year, this.Month, this.Day, ... this.Hour, this.Minute, this.Second); elseif (isempty (this.TimeZone)) [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second] = __datetime__ (this.Year, this.Month, this.Day, ... this.Hour, this.Minute, this.Second, 'Precision', 'microseconds'); else [this.Year, this.Month, this.Day, this.Hour, this.Minute, ... this.Second] = __datetime__ (this.Year, this.Month, this.Day, ... this.Hour, this.Minute, this.Second, 'TimeZone', this.TimeZone, ... 'toTimeZone', this.TimeZone, 'Precision', 'microseconds'); endif ## A wall clock names at most one instant here: an ambiguous one resolves ## to the LATER pass and one in a spring-forward gap is shifted past it, ## both in resolve_local. Recomputing the offset from the normalized ## components therefore re-asserts that choice, which is right for every ## wall-clock operation and wrong for an instant-based one -- see the ## invariant on the Offset property. this.Offset = dtOffsetOf (this.Year, this.Month, this.Day, this.Hour, ... this.Minute, this.Second, this.TimeZone); endfunction ## Seconds from B to A, taken as a DIFFERENCE of components rather than of ## two counts from 1970. Those counts are each one double, so past about ## year 3000 they no longer resolve a microsecond and two clocks a genuine ## microsecond apart subtracted to zero -- while the difference itself is ## small and keeps every figure. The day counts are exact integers, the ## seconds within a day are far below where a double loses microseconds, ## and the stored offsets carry whatever the zones contribute. The ## leap-second timeline keeps its own serial, which alone counts the ## inserted seconds. function sec = elapsedSeconds (A, B) if (dtIsLeapZone (A.TimeZone) || dtIsLeapZone (B.TimeZone)) sec = serial (A) - serial (B); return; endif dA = dtDaysFromCivil (A.Year, A.Month, A.Day); dB = dtDaysFromCivil (B.Year, B.Month, B.Day); ## Differenced field by field rather than by building each seconds-of-day ## first: a whole day of seconds is large enough that adding a microsecond ## to it loses the microsecond's tail, whereas each difference here is ## small and exact. sec = (dA - dB) .* 86400 + (A.Hour - B.Hour) .* 3600 ... + (A.Minute - B.Minute) .* 60 + (A.Second - B.Second) ... - (A.Offset - B.Offset); ## An infinite or Not-a-Time element has no civil date to difference, so ## it keeps the answer the instants give: infinity less a finite moment ## is still infinite, where the day count would make it NaN. nf = ! (isfinite (A.Year) & isfinite (B.Year)); if (any (nf(:))) sd = serial (A) - serial (B); sec = sec + zeros (size (nf)); sec(nf) = sd(nf); endif endfunction ## Absolute instant of each element as POSIX seconds (double, microsecond ## precision). Unzoned arrays are treated as UTC so the serial carries no ## system-zone daylight-saving offset; zoned arrays honour their zone (and ## DST). Not-A-Time maps to NaN and infinite elements keep their sign. ## Used by the arithmetic and relational instant-based comparisons. ## ## For a leap-second array the count is instead the continuous SI-second ## count of 'dtLeapSerial', which differs from POSIX time by the number of ## seconds inserted so far. That is what makes the arithmetic, the ## relational operators, the orderings and the set operations count inserted ## seconds without any of them knowing about leap seconds; 'posixtime' asks ## for POSIX time proper and so does not go through here. function s = serial (this) if (dtIsLeapZone (this.TimeZone)) s = dtLeapSerial (this.Year, this.Month, this.Day, this.Hour, ... this.Minute, this.Second); return; endif ## Read the instant off the stored offset rather than asking the tz ## database to resolve the wall clock, which is exactly the question ## that has two answers on a fall-back day. Offset is zero for an ## unzoned array, so the one expression serves both, and the 'instant' ## mode consults no zone at all -- which is what makes this, the hottest ## path in the class, cheaper than any conversion. s = __datetime__ (this.Year, this.Month, this.Day, this.Hour, ... this.Minute, this.Second, 'ConvertTo', 'instant', ... 'Offset', this.Offset, 'Precision', 'microseconds'); endfunction ## POSIX instant used by the integer fixed-epoch conversions ('epochtime', ## 'ntp', 'ntfs', '.net'). Ordinary arrays use their absolute instant; a ## leap-second array folds an inserted second backward, which is the rule ## every one of those formats follows. ## ## Deliberately unlike MATLAB, which applies this fold to the array but ## measures a user-supplied 'epochtime' Epoch on the leap-second timeline ## instead, so its count is short by the seconds inserted before that epoch ## -- 86373 rather than 86399 for 2016-12-31T23:59:59Z counted from ## 2016-12-31. Mixing the two timelines cannot be right whichever is ## chosen, so both operands are folded alike here. Documented in convertTo. function p = epochBase (this) if (dtIsLeapZone (this.TimeZone)) p = dtLeapPosix (this.Year, this.Month, this.Day, this.Hour, ... this.Minute, dtLeapBackFold (this.Second)); else p = serial (this); endif endfunction ## Enforce the zone-compatibility rule for a set operation and express B in ## A's time zone so membership is decided on a shared wall clock (the ## absolute instants are preserved by the conversion). Both operands are ## already datetime arrays here; text/numeric promotion and rejection is ## done by the 'dtSetPromote' local function before this is called. function [A, B] = prepSetOp (A, B, op) if (xor (isempty (A.TimeZone), isempty (B.TimeZone))) error (strcat ("datetime.%s: cannot combine a datetime array with", ... " a", ... " time zone with one without a time zone."), op); endif dtCheckLeapPair (A, B, op); if (! isempty (A.TimeZone) && ! strcmp (A.TimeZone, B.TimeZone)) ## Go through the instant rather than converting the wall clock in ## place -- see 'dtRezone'. The pair is never a leap zone here, since ## 'dtCheckLeapPair' has just required both operands to agree and the ## only leap zone would then also have compared equal above. [B.Year, B.Month, B.Day, B.Hour, B.Minute, B.Second, B.Offset] = ... dtRezone (B.Year, B.Month, B.Day, B.Hour, B.Minute, B.Second, ... B.Offset, B.TimeZone, A.TimeZone); B.TimeZone = A.TimeZone; endif endfunction ## Broadcast two datetime arrays to a common size for an element-wise ## operation, erroring if their sizes are incompatible. function [A, B] = broadcastPair (A, B, op) try z = zeros (size (A.Year + B.Year)); catch error (strcat ("datetime.%s: A and B must be of common size or", ... " scalars."), op); end_try_catch A.Year = A.Year + z; A.Month = A.Month + z; A.Day = A.Day + z; A.Hour = A.Hour + z; A.Minute = A.Minute + z; A.Second = A.Second + z; A.Offset = A.Offset + z; B.Year = B.Year + z; B.Month = B.Month + z; B.Day = B.Day + z; B.Hour = B.Hour + z; B.Minute = B.Minute + z; B.Second = B.Second + z; B.Offset = B.Offset + z; endfunction ## Assemble the result of a two-source set operation ('union'/'setxor'), ## whose values are drawn from both A (at IXA) and B (at IXB). The built-in ## returns those indices grouped A-then-B, which is exactly the 'stable' ## order; for the default 'sorted' order the gathered elements are reordered ## by their absolute instant (SA/SB), NaT sorting last. DOROWS selects the ## row-wise variant. function C = combineSets (A, B, ixA, ixB, SA, SB, doRows, stable) if (doRows) C = vertcat (subset (A, ixA, ':'), subset (B, ixB, ':')); if (! stable) [~, perm] = sortrows ([SA(ixA,:); SB(ixB,:)]); C = subset (C, perm, ':'); endif else Ca = subset (A, ixA); Cb = subset (B, ixB); C = vertcat (reshape (Ca, numel (Ca), 1), reshape (Cb, numel (Cb), 1)); if (! stable) sa = SA(ixA); sb = SB(ixB); [~, perm] = sort ([sa(:); sb(:)]); C = subset (C, perm); endif C = reshapeSetResult (C, isrow (A) && isrow (B)); endif endfunction ## Calendar-aware difference from A to B, element-wise on equal-sized ## inputs, expressed in the requested COMPS. Whole units are taken greedily ## from the largest requested down to the smallest (years, quarters, months, ## then weeks, days), each taking the most that does not step past B; the ## day of month is clamped on month steps and a whole unit is only borrowed ## if the time of day allows it (31 Jan 10:00 to 28 Feb 08:00 is 27 days 22 ## hours, not one month). When 'Time' is requested the leftover is its ## absolute instant difference, so it is daylight-saving aware. Returns a ## calendarDuration; NaT operands yield NaN. function C = calDiff (A, B, comps, op) [f, fmt] = parseCalComponents (comps, op); Y1 = A.Year; M1 = A.Month; D1 = A.Day; h1 = A.Hour; mi1 = A.Minute; s1 = A.Second; Y2 = B.Year; M2 = B.Month; D2 = B.Day; h2 = B.Hour; mi2 = B.Minute; s2 = B.Second; sz = size (Y1); ## A Not-A-Time operand yields a NaN result. Replace NaN components with ## a harmless placeholder for the calendar arithmetic, then restore NaN in ## the outputs, so month/day indexing never hits a NaN subscript. bad = isnan (Y1) | isnan (Y2); if (any (bad(:))) Y1(bad) = 2000; M1(bad) = 1; D1(bad) = 1; h1(bad) = 0; mi1(bad) = 0; s1(bad) = 0; Y2(bad) = 2000; M2(bad) = 1; D2(bad) = 1; h2(bad) = 0; mi2(bad) = 0; s2(bad) = 0; endif ## Direction: +1 where A < B (forward), -1 where A > B, 0 where equal. fwd = lexlt (Y1, M1, D1, h1, mi1, s1, Y2, M2, D2, h2, mi2, s2); bwd = lexlt (Y2, M2, D2, h2, mi2, s2, Y1, M1, D1, h1, mi1, s1); sgn = double (fwd) - double (bwd); Yc = Y1; Mc = M1; Dc = D1; # current date; time stays A's throughout monthsOut = zeros (sz); daysOut = zeros (sz); munits = []; if (f.y) munits(end+1) = 12; endif if (f.q) munits(end+1) = 3; endif if (f.m) munits(end+1) = 1; endif for u = munits totalM = (Y2 - Yc) .* 12 + (M2 - Mc); kApprox = fix (totalM ./ u); [Yk, Mk, Dk] = dtAddMonths (Yc, Mc, Dc, kApprox .* u); candGT = lexlt (Y2, M2, D2, h2, mi2, s2, Yk, Mk, Dk, h1, mi1, s1); candLT = lexlt (Yk, Mk, Dk, h1, mi1, s1, Y2, M2, D2, h2, mi2, s2); over = (sgn > 0 & candGT) | (sgn < 0 & candLT); k = kApprox - sgn .* double (over); monthsOut = monthsOut + k .* u; [Yc, Mc, Dc] = dtAddMonths (Yc, Mc, Dc, k .* u); endfor dunits = []; if (f.w) dunits(end+1) = 7; endif if (f.d) dunits(end+1) = 1; endif if (! isempty (dunits)) tLT = lexlt (0, 0, 0, h2, mi2, s2, 0, 0, 0, h1, mi1, s1); tGT = lexlt (0, 0, 0, h1, mi1, s1, 0, 0, 0, h2, mi2, s2); for u = dunits totalD = datenum (Y2, M2, D2) - datenum (Yc, Mc, Dc); wholeD = totalD - (sgn > 0 & tLT) + (sgn < 0 & tGT); k = fix (wholeD ./ u); daysOut = daysOut + k .* u; [Yc, Mc, Dc] = dtAddDays (Yc, Mc, Dc, k .* u); endfor endif if (f.t) cur = A; cur.Year = Yc; cur.Month = Mc; cur.Day = Dc; ## CUR carries A's components no longer, so A's offset need not belong ## to it either; re-derive it before the instant below is taken, then ## give A's own back wherever it still describes CUR's clock. Both ## halves are load-bearing: dropping the first leaves an hour of error ## whenever the calendar step crossed a transition, and dropping the ## second loses the fold when the step did not move the clock at all, ## so that the two passes over a repeated clock report no time between ## them rather than the hour that separates them. cur.Offset = dtOffsetOf (cur.Year, cur.Month, cur.Day, cur.Hour, ... cur.Minute, cur.Second, cur.TimeZone); cur = keepFold (cur, A.Offset + zeros (sz)); remSec = serial (B) - serial (cur); else remSec = zeros (sz); endif monthsOut(bad) = NaN; daysOut(bad) = NaN; remSec(bad) = NaN; Tdur = duration (0, 0, remSec); C = calendarDuration (zeros (sz), monthsOut, daysOut, Tdur, 'Format', ... fmt); endfunction ## Absolute instant of the given wall-clock components, interpreted in this ## array's time zone. Used by dateshift's 'nearest' rule to measure how far ## an element sits into its current unit. function ser = dsSerialOf (this, Y, M, D, h, m, s) tmp = this; tmp.Year = Y; tmp.Month = M; tmp.Day = D; tmp.Hour = h; tmp.Minute = m; tmp.Second = s; ser = serial (normalize (tmp)); endfunction ## Handles that carry this array's calendar into the binning helpers. Those ## are local functions, so they have no 'this' and cannot reach the class's ## own conversions; only the class knows its zone, its leap seconds and its ## epoch, so the knowledge is passed in rather than duplicated there. S2C ## maps a serial to its local year/month/day, C2S a local date to the serial ## of its midnight, and D2S any datetime to its serial. W2S maps a local ## date plus a count of WALL seconds into that day to a serial, which C2S ## plus a numeric offset cannot do: adding seconds to a midnight serial adds ## ELAPSED time, and the two part company across a daylight-saving ## transition. The seconds may run past a day; 'normalize' carries them. function [s2c, c2s, d2s, w2s] = dtCalHandles (this) s2c = @(s) serial2components (this, s); c2s = @(Y, M, D) dsSerialOf (this, Y, M, D, zeros (size (Y)), ... zeros (size (Y)), zeros (size (Y))); d2s = @(D) serial (D); w2s = @(Y, M, D, sec) dsSerialOf (this, Y, M, D, zeros (size (Y)), ... zeros (size (Y)), sec); endfunction ## Build a datetime from reduced POSIX seconds (the result of mean/median/ ## mode/std on this array's serial), preserving the Format and TimeZone. function R = fromReducedSerial (this, ser) R = this; [Y, M, D, h, m, s, off] = serial2components (this, ser); R.Year = Y; R.Month = M; R.Day = D; R.Offset = off; R.Hour = h; R.Minute = m; R.Second = s; endfunction ## Inverse of 'serial': map POSIX seconds back to the wall-clock components ## of this array's time zone. For a zoned array the serial is first read as ## a UTC wall clock and then converted into the target zone (honouring DST). ## A leap-second array inverts the continuous SI-second count instead, so a ## count that lands inside an inserted second yields a 60th second. function [Y, M, D, h, m, s, off] = serial2components (this, ser) if (dtIsLeapZone (this.TimeZone)) [Y, M, D, h, m, s] = dtLeapComponents (ser); off = zeros (size (Y)); return; endif ## An instant names one wall clock, so nothing is resolved on the way in ## and the offset comes back from the same lookup that produced the ## components -- which is what distinguishes the two passes over a ## repeated clock, and could not be recovered from the components alone. ## An unzoned array is read as UTC, where the offset is zero throughout. tz = this.TimeZone; if (isempty (tz)) tz = 'UTC'; endif [Y, M, D, h, m, s, off] = __datetime__ (ser, 'ConvertTo', 'fromposix', ... 'TimeZone', tz, 'Precision', 'microseconds'); endfunction ## Shift each element by a fixed number of seconds applied to its absolute ## instant (daylight-saving aware for zoned arrays), then rebuild the ## wall-clock components. DSEC may broadcast against the array size. The ## Format and TimeZone properties are preserved. function this = addSeconds (this, dsec) if (isempty (this.TimeZone) || strcmp (this.TimeZone, 'UTC') ... || ! isempty (dtFixedOffset (this.TimeZone))) ## With no transitions to cross, a shift lands on the wall clock just ## as it does on the instant, so it can be added to the components -- ## which keeps every figure, where a count of seconds from 1970 cannot. ## That count is one double: past about year 3000 it no longer resolves ## a microsecond, and 't + milliseconds (0.001)' did not move the value ## at all. Whole days go on the day, the remainder on the seconds, and ## 'normalize' folds both in 64-bit civil arithmetic. The leap-second ## zone is excluded deliberately: there a shift is counted on the ## leap-second timeline, which is what its instant exists to carry. dd = floor (dsec ./ 86400); rs = dsec - dd .* 86400; ## An infinite or Not-a-Number shift is carried by the day term alone: ## the remainder would be Inf - Inf, and a NaN there would turn an ## infinite datetime into NaT. rs(! isfinite (dsec)) = 0; this.Day = this.Day + dd; this.Second = this.Second + rs; this = normalize (this); return; endif ser = serial (this) + dsec; [Y, M, D, h, m, s, off] = serial2components (this, ser); this.Year = Y; this.Month = M; this.Day = D; this.Hour = h; this.Minute = m; this.Second = s; this.Offset = off; endfunction ## Shift each element by a calendarDuration (SGN is +1 for addition, -1 for ## subtraction). Whole months are applied first with end-of-month day ## clamping, then whole calendar days (wall-clock preserving), then the ## time-of-day component as an instant. Not-A-Time and infinite elements ## are carried through unchanged. function this = addCalendar (this, calD, sgn) dMonths = sgn * calmonths (calD); dDays = sgn * split (calD, 'days'); dTime = sgn * days (time (calD)) * 86400; # seconds ## Broadcast the instant components and the calendar deltas to a common ## size so the month math and the Not-A-Time / infinite masks all align. base = zeros (size (this.Year)) + zeros (size (this.Month)) ... + zeros (size (this.Day)) + zeros (size (this.Hour)) ... + zeros (size (this.Minute)) + zeros (size (this.Second)) ... + zeros (size (dMonths)) + zeros (size (dDays)); Y = this.Year + base; M = this.Month + base; D = this.Day + base; h = this.Hour + base; m = this.Minute + base; s = this.Second + base; dM = dMonths + base; dD = dDays + base; ## An element is "live" only when both the instant and the calendar delta ## are finite; Not-A-Time and infinite inputs (from either operand) ## propagate straight through as the corresponding non-finite marker. ok = isfinite (Y) & isfinite (dM) & isfinite (dD); mk = Y + dM + dD; ## Add whole months, clamping the day to the last day of the target month ## (e.g. 31 Jan + 1 month -> 28 Feb), then add whole calendar days. total = Y * 12 + (M - 1) + dM; Y(ok) = floor (total(ok) / 12); M(ok) = mod (total(ok), 12) + 1; D(ok) = min (D(ok), eomday (Y(ok), M(ok))) + dD(ok); Y(! ok) = mk(! ok); M(! ok) = mk(! ok); D(! ok) = mk(! ok); h(! ok) = mk(! ok); m(! ok) = mk(! ok); s(! ok) = mk(! ok); ## Checked here rather than left to 'normalize' below: the leap-second ## clamp asks the builtin for a POSIX time first, which is a timezone ## lookup like any other. if (dtIsLeapZone (this.TimeZone)) ## Calendar arithmetic keeps the wall clock, so an inserted second can ## be carried onto a minute that never had one; clamp it to the 59th ## second there, as the day of the month is clamped above. s = dtLeapClampSecond (s, dtLeapMinutePosix (Y, M, D, h, m, ... zeros (size (Y)))); endif srcOff = this.Offset + base; this.Year = Y; this.Month = M; this.Day = D; this.Hour = h; this.Minute = m; this.Second = s; this = normalize (this); ## Calendar arithmetic keeps the wall clock, and where that clock is a ## repeated one it keeps the fold as well: a day added to the pass that ## is still on daylight saving arrives on the pass that is still on it. ## 'normalize' has just resolved every element to the later pass, which ## is right only for elements that did not come from the earlier one. this = keepFold (this, srcOff); ## Add the time-of-day component as an instant (daylight-saving aware). if (any (dTime(:) != 0)) this = addSeconds (this, dTime + zeros (size (this.Year))); endif endfunction ## Restore SRCOFF as this array's offset wherever it still describes the ## array's own wall clock, leaving the resolved offset in place everywhere ## else. The test is the round trip: read the instant that SRCOFF names ## and put it back on the clock, and keep SRCOFF exactly when the clock ## comes back unchanged. That admits the earlier pass of a repeated clock, ## which is the case this exists for, and refuses a stale offset carried in ## from a date whose zone was on the other side of a transition -- for ## which no fold of the target clock corresponds to it and the trip fails. function this = keepFold (this, srcOff) ## A fixed-offset zone has no fold to keep, and asking the database would ## only risk a year it cannot hold. if (dtIsZeroOffsetZone (this.TimeZone) ... || ! isempty (dtFixedOffset (this.TimeZone))) return; endif this.Offset = __datetime__ (this.Year, this.Month, this.Day, ... this.Hour, this.Minute, this.Second, ... 'ConvertTo', 'keepfold', 'TimeZone', ... this.TimeZone, 'Offset', srcOff, ... 'Precision', 'microseconds'); endfunction ## Range with a fixed-length (duration/numeric) step of STEPSEC seconds. ## The endpoints' absolute instants are stepped by the ordinary numeric ## colon (so the inclusive-endpoint tolerance matches numeric ranges), then ## rebuilt into this array's time zone. Empty and reversed ranges fall out ## naturally from the numeric colon. function R = colonLinear (A, stepSec, B) ser = serial (A) : stepSec : serial (B); [Y, M, D, h, m, s, off] = serial2components (A, ser); R = A; R.Year = Y; R.Month = M; R.Day = D; R.Hour = h; R.Minute = m; R.Second = s; R.Offset = off; endfunction ## Range with a calendarDuration STEP. Each element is A + k*STEP for ## k = 0, 1, 2, ... (non-iterative, so month/year steps clamp the day of ## month per element). The number of steps is found by bracketing then ## binary-searching the largest k whose element has not passed B, which ## keeps calendar arithmetic exact without assuming a fixed element spacing. function R = colonCalendar (A, step, B) first = A + step; if (first == A) error ("datetime.colon: STEP must be nonzero."); endif incr = first > A; if ((incr && A > B) || (! incr && A < B)) R = A + (0:-1) .* step; # empty range, keeps A's Format and TimeZone return; endif hi = 1; while (colon_within (A + hi .* step, B, incr) && hi < 2^40) hi *= 2; endwhile lo = 0; while (hi - lo > 1) mid = floor ((lo + hi) / 2); if (colon_within (A + mid .* step, B, incr)) lo = mid; else hi = mid; endif endwhile R = A + (0:lo) .* step; endfunction ## Shared implementation of the six relational operators. Both operands ## must be datetime and either both zoned or both unzoned; a zoned pair with ## differing zones is aligned onto A's zone (preserving the instant) so the ## wall-clock components can be compared directly. Comparison is ## lexicographic on [Year Month Day Hour Minute Second] and therefore exact ## at every magnitude. Not-A-Time (NaN components) never compares ## less/greater/equal, so only 'ne' returns true when a NaT is involved. function TF = relcompare (A, B, op) if (! (isa (A, 'datetime') && isa (B, 'datetime'))) error ("datetime.%s: both operands must be datetime arrays.", op); endif if (xor (isempty (A.TimeZone), isempty (B.TimeZone))) error (strcat ("datetime.%s: cannot compare a datetime array with", ... " a", ... " time zone to one without a time zone."), op); endif dtCheckLeapPair (A, B, op); ## Express B on A's clock through 'prepSetOp', which goes by the instant. ## Converting the components on their own would resolve an operand ## sitting on the earlier pass of a repeated clock to the later one and ## compare the wrong moment. [~, B] = prepSetOp (A, B, op); aY = A.Year; aM = A.Month; aD = A.Day; ah = A.Hour; am = A.Minute; asec = A.Second; aoff = A.Offset; bY = B.Year; bM = B.Month; bD = B.Day; bh = B.Hour; bm = B.Minute; bsec = B.Second; boff = B.Offset; ## Two elements are the same moment only if they also agree on the ## offset: identical components on a fall-back day name two of them. EQ = (aY == bY) & (aM == bM) & (aD == bD) ... & (ah == bh) & (am == bm) & (asec == bsec) & (aoff == boff); switch (op) case 'eq' TF = EQ; case 'ne' TF = ! EQ; case 'lt' TF = lexlt (aY, aM, aD, ah, am, asec, bY, bM, bD, bh, bm, bsec, ... aoff, boff); case 'gt' TF = lexlt (bY, bM, bD, bh, bm, bsec, aY, aM, aD, ah, am, asec, ... boff, aoff); case 'le' TF = lexlt (aY, aM, aD, ah, am, asec, bY, bM, bD, bh, bm, bsec, ... aoff, boff) | EQ; case 'ge' TF = lexlt (bY, bM, bD, bh, bm, bsec, aY, aM, aD, ah, am, asec, ... boff, aoff) | EQ; endswitch endfunction endmethods endclassdef ## Shared back-end for 'isequal' (NANEQUAL false) and 'isequaln' (NANEQUAL ## true). ARGS is the cell array of operands. Returns true only when every ## operand is a datetime of the same size as the first and each pair of ## corresponding elements is the same point in time. A non-datetime operand or ## a time-zone mismatch (one zoned, one unzoned) yields false rather than an ## error. Zoned arrays are compared by absolute instant. When NANEQUAL is ## true, Not-A-Time elements compare equal to one another (component-wise NaN ## matches NaN); otherwise any NaT makes the result false, as with NaN. function TF = do_isequal (args, nanEqual) A = args{1}; TF = true; for i = 2:numel (args) B = args{i}; if (! (isa (A, 'datetime') && isa (B, 'datetime'))) TF = false; return; endif if (! isequal (size (A), size (B))) TF = false; return; endif if (xor (isempty (A.TimeZone), isempty (B.TimeZone))) TF = false; return; endif ## Leap seconds are part of what an array counts, so an array that has ## them is never equal to one that does not. if (dtIsLeapZone (A.TimeZone) != dtIsLeapZone (B.TimeZone)) TF = false; return; endif if (isempty (A)) continue; # two empties of equal size compare equal endif ## Zoned operands are moved to UTC, where no wall clock names more than ## one moment. That does two things at once: it aligns operands in ## different zones by their instant, and it separates the two moments that ## share a local clock on the day a clock goes back, which no comparison ## of local components could tell apart. Assigning the zone preserves the ## instant, so nothing else is needed here. The leap-second zone is left ## alone: both operands are already in it, it is its own frame, and moving ## out of it would fold an inserted second onto its neighbour. if (! isempty (A.TimeZone) && ! dtIsLeapZone (A.TimeZone)) A.TimeZone = 'UTC'; B.TimeZone = 'UTC'; endif aY = A.Year; aM = A.Month; aD = A.Day; ah = A.Hour; am = A.Minute; asec = A.Second; bY = B.Year; bM = B.Month; bD = B.Day; bh = B.Hour; bm = B.Minute; bsec = B.Second; if (nanEqual) E = ceq (aY, bY) & ceq (aM, bM) & ceq (aD, bD) ... & ceq (ah, bh) & ceq (am, bm) & ceq (asec, bsec); else E = (aY == bY) & (aM == bM) & (aD == bD) ... & (ah == bh) & (am == bm) & (asec == bsec); endif if (! all (E(:))) TF = false; return; endif endfor endfunction ## Component equality that also treats NaN as equal to NaN (used by isequaln). function TF = ceq (x, y) TF = (x == y) | (isnan (x) & isnan (y)); endfunction ## B's wall-clock components and UTC offset expressed in the time zone TZ, ## keeping each element's instant. B is returned untouched when it is already ## in TZ or when TZ is empty. ## ## The route is the instant and not the wall clock, and the difference is the ## whole point: asking the tz database to convert B's components between zones ## makes it resolve them in B's own zone first, and on a fall-back day that ## resolution has two answers, of which it always takes the later. An element ## sitting on the earlier pass would come out an hour wrong. Reading the ## instant off B's stored offset, then putting it back on the clock in TZ, ## asks nothing that has two answers. ## Takes and returns the bare component arrays rather than the object, since ## a file-scope function reaches a datetime's properties through 'subsref', ## which exposes the six components but deliberately not the offset. function [Y, M, D, h, m, s, off] = dtRezone (Y, M, D, h, m, s, off, ... fromTZ, TZ) off = off + zeros (size (Y)); if (isempty (TZ) || strcmp (TZ, fromTZ)) return; endif ## Reading the clock in one zone and writing it in another asks the database ## twice, so both years have to be inside what it can answer for. [Y, M, D, h, m, s, off] = __datetime__ (Y, M, D, h, m, s, 'ConvertTo', ... 'rezone', 'Offset', off, 'toTimeZone', TZ, ... 'Precision', 'microseconds'); endfunction ## True while a candidate datetime X has not yet passed the range endpoint B, ## for an increasing (INCR true) or decreasing calendar range. Used to bracket ## and binary-search the element count in 'colonCalendar'. function TF = colon_within (X, B, incr) if (incr) TF = X <= B; else TF = X >= B; endif endfunction ## Lexicographic strictly-less-than on datetime component arrays. Returns true ## where the [Year Month Day Hour Minute Second] tuple of the first operand is ## strictly earlier than that of the second. Any NaN component (Not-A-Time) ## makes the element false, matching NaN comparison semantics. All arguments ## broadcast against each other element-wise. ## ## AOFF and BOFF are the operands' UTC offsets, and break the tie the six ## components cannot: on the day a clock goes back the same tuple names two ## moments an hour apart, and the one still on daylight saving -- the earlier ## of the two -- is the one carrying the LARGER offset, whence the reversed ## test at the bottom. They default to zero, which is the right answer for a ## caller comparing wall clocks rather than instants (calendar arithmetic does ## exactly that) and for any unzoned array. The comparison stays componentwise ## and so stays exact at every magnitude, which a comparison of instants held ## as seconds in a double would not be. function TF = lexlt (aY, aM, aD, ah, am, asec, bY, bM, bD, bh, bm, bsec, ... aoff = 0, boff = 0) eqY = aY == bY; eqM = aM == bM; eqD = aD == bD; eqh = ah == bh; eqm = am == bm; eqs = asec == bsec; TF = (aY < bY) ... | (eqY & aM < bM) ... | (eqY & eqM & aD < bD) ... | (eqY & eqM & eqD & ah < bh) ... | (eqY & eqM & eqD & eqh & am < bm) ... | (eqY & eqM & eqD & eqh & eqm & asec < bsec) ... | (eqY & eqM & eqD & eqh & eqm & eqs & aoff > boff); endfunction ## Promote a set-operation operand to a datetime array. A datetime is returned ## unchanged; text (character vector, string, or cellstr) is parsed by the ## constructor, inheriting REF's time zone so the two operands share a frame; ## numeric, logical, and duration operands are rejected the way MATLAB rejects ## them. Defined at file scope (not as a method) so it dispatches correctly ## when the first set-operation argument is text rather than a datetime. ## Promote a non-datetime operand of a concatenation to datetime. Date/time ## text is read as a wall clock in REF's time zone, the same rule the set ## operations and 'isbetween' follow; nothing else can be concatenated with a ## datetime array. function d = dtCatPromote (x, ref) if (isa (x, 'missing')) ## A missing value becomes NaT of the same shape. It must take the ## reference's zone: the caller requires every operand to agree on whether ## it is zoned, so an unzoned filler could not join a zoned array. if (isempty (ref.TimeZone)) d = NaT (size (x)); else d = NaT (size (x), 'TimeZone', ref.TimeZone); endif elseif (ischar (x) || iscellstr (x) || isa (x, 'string')) if (isempty (ref.TimeZone)) d = datetime (x); else d = datetime (x, 'TimeZone', ref.TimeZone); endif else error (strcat ("datetime.cat: all inputs must be datetime arrays or", ... " date/time text.")); endif endfunction function d = dtSetPromote (x, ref, op) if (isa (x, 'datetime')) d = x; elseif (ischar (x) || iscellstr (x) || isa (x, 'string')) if (isempty (ref.TimeZone)) d = datetime (x); else d = datetime (x, 'TimeZone', ref.TimeZone); endif elseif (isa (x, 'duration') || isa (x, 'calendarDuration')) error (strcat ("datetime.%s: comparison is not defined between", ... " datetime", ... " and duration arrays."), op); else error (strcat ("datetime.%s: comparison is not defined between", ... " datetime", ... " and numeric arrays."), op); endif endfunction ## Orient a (non-'rows') set-operation result: a row vector when both operands ## were row vectors, and a column vector otherwise (matching MATLAB, so an empty ## result is 0-by-1 rather than 0-by-0). C is a datetime array. function C = reshapeSetResult (C, bothRows) if (bothRows) C = reshape (C, 1, numel (C)); else C = reshape (C, numel (C), 1); endif endfunction ## Promote a bound of 'isbetween' to a datetime array. A datetime is returned ## unchanged; text is parsed by the constructor, inheriting REF's time zone; ## anything else (numeric, duration, ...) is rejected. Defined at file scope so ## it dispatches correctly when the argument is text rather than a datetime. function d = dtIsbetweenArg (x, ref) if (isa (x, 'datetime')) d = x; elseif (ischar (x) || iscellstr (x) || isa (x, 'string')) if (isempty (ref.TimeZone)) d = datetime (x); else d = datetime (x, 'TimeZone', ref.TimeZone); endif else error (strcat ("datetime.isbetween: LOWER and UPPER must be datetime", ... " arrays or date/time text.")); endif endfunction ## Translate datetime missing-value flags into the 'omitnan'/'includenan' flags ## understood by the core reduction functions, leaving dims, 'all', and the core ## flags untouched. function args = dtStatFlags (args) for i = 1:numel (args) if (ischar (args{i}) && isrow (args{i})) switch (lower (args{i})) case {'omitnat', 'omitmissing'} args{i} = 'omitnan'; case {'includenat', 'includemissing'} args{i} = 'includenan'; endswitch endif endfor endfunction ## Number of days in month M of year Y (element-wise, proleptic Gregorian). function d = dtDaysInMonth (Y, M) dpm = [31 28 31 30 31 30 31 31 30 31 30 31]; d = dpm(M); d = reshape (d, size (M)); leap = (mod (Y, 4) == 0 & mod (Y, 100) != 0) | (mod (Y, 400) == 0); d(M == 2 & leap) = 29; endfunction ## Add K whole calendar months to the date (Y, M, D), clamping the day of month ## to the last valid day of the target month (e.g. 31 Jan + 1 month -> 28 Feb). ## Time of day is not represented here; it is carried unchanged by the caller. function [Yo, Mo, Do] = dtAddMonths (Y, M, D, K) idx = Y .* 12 + (M - 1) + K; Yo = floor (idx ./ 12); Mo = idx - Yo .* 12 + 1; Do = min (D, dtDaysInMonth (Yo, Mo)); endfunction ## Add K whole calendar days to the date (Y, M, D), element-wise. function [Yo, Mo, Do] = dtAddDays (Y, M, D, K) dn = datenum (Y(:), M(:), D(:)) + K(:); dv = datevec (dn); Yo = reshape (dv(:,1), size (Y)); Mo = reshape (dv(:,2), size (Y)); Do = reshape (dv(:,3), size (Y)); endfunction ## Map a day-of-week name to its number (Sunday = 1 .. Saturday = 7); NaN for an ## unrecognised name. function n = dsDayName (name) names = {'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', ... 'friday', 'saturday'}; n = find (strcmpi (name, names), 1); if (isempty (n)) n = NaN; endif endfunction ## Parse a dateshift RULE into a kind ('current'/'next'/'previous'/'nearest'/ ## 'int') and, for the integer kind, its value. function [kind, n] = dsRule (r) n = 0; msg = strcat ("datetime.dateshift: rule must be an integer, 'next',", ... " 'previous', 'current', or 'nearest'."); if (ischar (r) && isrow (r)) if (any (strcmpi (r, {'current', 'next', 'previous', 'nearest'}))) kind = lower (r); else error (msg); endif elseif (isnumeric (r) && isscalar (r) && isreal (r) && r == fix (r)) kind = 'int'; n = r; else error (msg); endif endfunction ## Shift a date/time by N whole units (used by dateshift's rule). Calendar ## units produce a canonical date; sub-day units may overflow and are ## canonicalised by the caller's final normalisation. function [Y, M, D, h, mi, s] = dsShiftUnits (Y, M, D, h, mi, s, unit, n) switch (unit) case 'year' [Y, M, D] = dtAddMonths (Y, M, D, 12 .* n); case 'quarter' [Y, M, D] = dtAddMonths (Y, M, D, 3 .* n); case 'month' [Y, M, D] = dtAddMonths (Y, M, D, n); case 'week' [Y, M, D] = dtAddDays (Y, M, D, 7 .* n); case 'day' [Y, M, D] = dtAddDays (Y, M, D, n); case 'hour' h = h + n; case 'minute' mi = mi + n; case 'second' s = s + n; endswitch endfunction ## Truncate a date/time down to the start of the given calendar unit. function [Y, M, D, h, mi, s] = dsStartComp (Y, M, D, h, mi, s, unit) z = zeros (size (Y)); switch (unit) case 'year' M = ones (size (M)); D = ones (size (D)); h = z; mi = z; s = z; case 'quarter' M = 3 .* floor ((M - 1) / 3) + 1; D = ones (size (D)); h = z; mi = z; s = z; case 'month' D = ones (size (D)); h = z; mi = z; s = z; case 'week' dow = weekday (datenum (Y, M, D)); [Y, M, D] = dtAddDays (Y, M, D, -(dow - 1)); h = z; mi = z; s = z; case 'day' h = z; mi = z; s = z; case 'hour' mi = z; s = z; case 'minute' s = z; case 'second' s = floor (s); endswitch endfunction ## Compute the end of the given calendar unit. For the sub-day units and 'day' ## this is the start of the next unit; for a week it is the last day (Saturday); ## for month/quarter/year it is the last day of the unit at midnight. The ## results may overflow and are canonicalised by the caller's normalisation. function [Y, M, D, h, mi, s] = dsEndComp (Y, M, D, h, mi, s, unit) z = zeros (size (Y)); switch (unit) case 'year' M = 12 .* ones (size (M)); D = 31 .* ones (size (D)); h = z; mi = z; s = z; case 'quarter' qm = 3 .* ceil (M / 3); D = dtDaysInMonth (Y, qm); M = qm; h = z; mi = z; s = z; case 'month' D = dtDaysInMonth (Y, M); h = z; mi = z; s = z; case 'week' dow = weekday (datenum (Y, M, D)); [Y, M, D] = dtAddDays (Y, M, D, 7 - dow); h = z; mi = z; s = z; case 'day' [Y, M, D] = dtAddDays (Y, M, D, 1); h = z; mi = z; s = z; case 'hour' h = h + 1; mi = z; s = z; case 'minute' mi = mi + 1; s = z; case 'second' s = floor (s) + 1; endswitch endfunction ## Error message shared by caldiff and between for an invalid COMPONENTS input. function msg = calCompError (op) msg = strcat ("datetime.", op, ... ": COMPONENTS must be 'Years', 'Quarters',", ... " 'Months', 'Weeks', 'Days', or 'Time', or a string array", ... " or", ... " cell array containing those components."); endfunction ## Parse the COMPONENTS argument of caldiff/between into presence flags for each ## calendar unit and the display Format string of the resulting calendarDuration ## (which always contains 'm', 'd', and 't', with 'y'/'q'/'w' added only when ## those units are requested). An empty COMPONENTS selects the default set ## {Years, Months, Days, Time}. function [f, fmt] = parseCalComponents (comps, op) if (isempty (comps)) f = struct ('y', true, 'q', false, 'm', true, ... 'w', false, 'd', true, 't', true); else if (ischar (comps) && isrow (comps)) toks = {comps}; elseif (iscellstr (comps)) toks = comps(:)'; elseif (isa (comps, 'string')) toks = cellstr (comps)(:)'; else error (calCompError (op)); endif f = struct ('y', false, 'q', false, 'm', false, ... 'w', false, 'd', false, 't', false); for i = 1:numel (toks) switch (lower (toks{i})) case {'years', 'year', 'y'} f.y = true; case {'quarters', 'quarter', 'q'} f.q = true; case {'months', 'month', 'mo', 'm'} f.m = true; case {'weeks', 'week', 'w'} f.w = true; case {'days', 'day', 'd'} f.d = true; case {'time', 't'} f.t = true; otherwise error (calCompError (op)); endswitch endfor if (! (f.y || f.q || f.m || f.w || f.d || f.t)) error (calCompError (op)); endif endif ## The calendarDuration Format must contain 'm', 'd', and 't', so a ## single-component result such as caldiff (..., 'Years') keeps them even ## though only years are populated. This is invisible for a non-zero ## result (the other fields are zero and are not shown), and an all-zero ## result displays as '0d', as it does in MATLAB. fmt = ''; if (f.y) fmt = [fmt, 'y']; endif if (f.q) fmt = [fmt, 'q']; endif fmt = [fmt, 'm']; if (f.w) fmt = [fmt, 'w']; endif fmt = [fmt, 'dt']; endfunction ## Resolve the Format property to a concrete LDML pattern. The sentinels ## 'default' and 'defaultdate' are data-dependent, matching MATLAB: 'default' ## renders a date-only pattern when every element sits at midnight (all-zero ## time-of-day) and a date+time pattern otherwise; 'defaultdate' is always ## date-only. A concrete user pattern is returned verbatim (case-sensitive, ## as 'M' and 'm' differ). NaT elements carry no time of day and are left out ## of the decision, so a date-only array keeps its date-only rendering when a ## NaT sits beside it; an array holding nothing but NaT says nothing either ## way and keeps the date+time pattern. An infinite datetime carries no time of ## day either, and is left out on the same grounds. function fmt = dtResolveFormat (fmtProp, H, Mi, S) if (strcmpi (fmtProp, 'default')) [dflt, dfltdate] = dtDefaultFormats (); dated = isfinite (H(:)); if (! isempty (H) && any (dated) && all ((H(dated) == 0) ... & (Mi(dated) == 0) ... & (S(dated) == 0))) fmt = dfltdate; else fmt = dflt; endif elseif (strcmpi (fmtProp, 'defaultdate')) [~, fmt] = dtDefaultFormats (); elseif (strcmpi (fmtProp, 'preserveinput')) fmt = dtDefaultFormats (); else fmt = fmtProp; endif endfunction ## Owner of the two default display formats set by 'datetime.setDefaultFormats'. ## ## Called with no arguments it returns the date-and-time default and the ## date-only default, in that order. @code{dtDefaultFormats ('set', which, ## fmt)} stores one of them, @var{which} being 'default' or 'defaultdate', and ## @code{dtDefaultFormats ('reset')} restores both factory patterns. ## ## The patterns persist across sessions in Octave's preferences, as MATLAB's do ## through its settings tree. The session cache in front of the preferences is ## not an optimization detail: 'dtResolveFormat' runs on every display, and ## without it every rendering of a datetime array would read a file. Clearing ## the cache is always safe, since it is re-seeded from the preference on the ## next call, which is what makes this survive the 'clear classes' that ## pkg-octave-doc runs between demos. ## ## A factory pattern is stored as the absence of a preference rather than as its ## value, so that 'reset' leaves nothing behind, matching what MATLAB's own ## 'reset' leaves in its settings tree. function [dflt, dfltdate] = dtDefaultFormats (mode = 'get', which = '', ... fmt = '') persistent cache_dflt cache_date FACTORY_DFLT = 'dd-MMM-uuuu HH:mm:ss'; FACTORY_DATE = 'dd-MMM-uuuu'; if (isempty (cache_dflt)) if (ispref ('datatypes', 'datetime_DefaultFormat')) cache_dflt = getpref ('datatypes', 'datetime_DefaultFormat'); else cache_dflt = FACTORY_DFLT; endif endif if (isempty (cache_date)) if (ispref ('datatypes', 'datetime_DefaultDateFormat')) cache_date = getpref ('datatypes', 'datetime_DefaultDateFormat'); else cache_date = FACTORY_DATE; endif endif switch (mode) case 'set' if (strcmpi (which, 'default')) cache_dflt = fmt; setpref ('datatypes', 'datetime_DefaultFormat', fmt); else cache_date = fmt; setpref ('datatypes', 'datetime_DefaultDateFormat', fmt); endif case 'reset' cache_dflt = FACTORY_DFLT; cache_date = FACTORY_DATE; if (ispref ('datatypes', 'datetime_DefaultFormat')) rmpref ('datatypes', 'datetime_DefaultFormat'); endif if (ispref ('datatypes', 'datetime_DefaultDateFormat')) rmpref ('datatypes', 'datetime_DefaultDateFormat'); endif endswitch dflt = cache_dflt; dfltdate = cache_date; endfunction ## Validate a user-supplied Format string, mirroring MATLAB's rejection of ## unsupported field letters. The sentinels are accepted as-is. function dtValidateFormat (fmt) if (any (strcmpi (fmt, {'default', 'defaultdate', 'preserveinput'}))) return; endif supported = 'yuMdDeaHhmsSQGWzZXx'; syms = __ldml__ ('symbols', fmt); for t = 1:numel (syms) if (! any (syms(t) == supported)) error (strcat ("datetime: the format '%s' contains an unsupported", ... " symbol: '%s'"), fmt, syms(t)); endif endfor endfunction ## Enforce MATLAB's rule that the year, month, day, hour, and minute components ## must be integer-valued; only seconds (and milliseconds) may be fractional. ## Not-A-Number and infinite values are permitted (they yield NaT / Inf). ARGS ## is the positional argument list: a single numeric date-vector matrix (columns ## 1-5 are Y,M,D,H,MI) or separate component arrays (the first five are ## Y,M,D,H,MI). function dtCheckIntegerComponents (args) if (numel (args) == 1 && isnumeric (args{1})) M = args{1}; vals = M(:, 1:min (5, columns (M))); else vals = []; for k = 1:min (5, numel (args)) if (isnumeric (args{k})) vals = [vals(:); args{k}(:)]; endif endfor endif if (any (isfinite (vals(:)) & (fix (vals(:)) != vals(:)))) error (strcat ("datetime: Year, Month, Day, Hour, and Minute", ... " components", ... " must be integer values.")); endif endfunction ## Days between 1970-01-01 and a proleptic Gregorian civil date, and the year a ## date given as such a day count falls in. Vectorized, and exact in double ## precision over any range the calendar carries. Used by the Excel serial ## conversions and by the elapsed-time difference, both of which need a day ## count rather than a second calendar. function z = dtDaysFromCivil (y, m, d) y = y - (m <= 2); era = floor (y ./ 400); yoe = y - era .* 400; mp = m + 9 .* (m <= 2) - 3 .* (m > 2); doy = floor ((153 .* mp + 2) ./ 5) + d - 1; doe = yoe .* 365 + floor (yoe ./ 4) - floor (yoe ./ 100) + doy; z = era .* 146097 + doe - 719468; endfunction function y = dtYearFromDays (z) z = z + 719468; era = floor (z ./ 146097); doe = z - era .* 146097; yoe = floor ((doe - floor (doe ./ 1460) + floor (doe ./ 36524) ... - floor (doe ./ 146096)) ./ 365); doy = doe - (365 .* yoe + floor (yoe ./ 4) - floor (yoe ./ 100)); mp = floor ((5 .* doy + 2) ./ 153); mo = mp + 3 - 12 .* (mp >= 10); y = yoe + era .* 400 + (mo <= 2); endfunction ## Return the full and abbreviated month- and weekday-name tables for an LDML ## locale, used to parse 'MMMM'/'MMM' and 'eeee'/'eee' fields under ## 'InputFormat'. Names are matched case-insensitively; abbreviations are ## stored without any trailing period (locales that write one -- fr, de, pt -- ## have it consumed during parsing). Only a curated set of locales is ## supported; English is the default and the fallback for 'system'. The country ## part of an 'xx_YY' locale is ignored, as names vary by language, not region. function [mFull, mAbbr, wFull, wAbbr, dpMark] = dtLocaleNames (locale) if (isempty (locale)) lang = 'en'; else lang = strtok (tolower (locale), '_-'); endif if (strcmp (lang, 'system')) lang = 'en'; endif ## Day-period markers {am, pm}; English 'am'/'pm' is the default and is used ## by every supported locale except Spanish (overridden below). dpMark = {'am', 'pm'}; switch (lang) case 'en' mFull = {'january','february','march','april','may','june','july', ... 'august','september','october','november','december'}; mAbbr = {'jan','feb','mar','apr','may','jun','jul','aug','sep', ... 'oct','nov','dec'}; wFull = {'sunday','monday','tuesday','wednesday','thursday','friday', ... 'saturday'}; wAbbr = {'sun','mon','tue','wed','thu','fri','sat'}; case 'fr' mFull = {'janvier','février','mars','avril','mai','juin','juillet', ... 'août','septembre','octobre','novembre','décembre'}; mAbbr = {'janv','févr','mars','avr','mai','juin','juil','août', ... 'sept','oct','nov','déc'}; wFull = {'dimanche','lundi','mardi','mercredi','jeudi','vendredi', ... 'samedi'}; wAbbr = {'dim','lun','mar','mer','jeu','ven','sam'}; case 'de' mFull = {'januar','februar','märz','april','mai','juni','juli', ... 'august','september','oktober','november','dezember'}; mAbbr = {'jan','feb','märz','apr','mai','juni','juli','aug', ... 'sept','okt','nov','dez'}; wFull = {'sonntag','montag','dienstag','mittwoch','donnerstag', ... 'freitag','samstag'}; wAbbr = {'so','mo','di','mi','do','fr','sa'}; case 'es' mFull = {'enero','febrero','marzo','abril','mayo','junio','julio', ... 'agosto','septiembre','octubre','noviembre','diciembre'}; mAbbr = {'ene','feb','mar','abr','may','jun','jul','ago','sept', ... 'oct','nov','dic'}; wFull = {'domingo','lunes','martes','miércoles','jueves','viernes', ... 'sábado'}; wAbbr = {'dom','lun','mar','mié','jue','vie','sáb'}; ## Spanish markers use a UTF-8 no-break space (U+00A0 = bytes 194 160). nbsp = char ([194, 160]); dpMark = {['a.', nbsp, 'm.'], ['p.', nbsp, 'm.']}; case 'it' mFull = {'gennaio','febbraio','marzo','aprile','maggio','giugno', ... 'luglio','agosto','settembre','ottobre','novembre','dicembre'}; mAbbr = {'gen','feb','mar','apr','mag','giu','lug','ago','set', ... 'ott','nov','dic'}; wFull = {'domenica','lunedì','martedì','mercoledì','giovedì', ... 'venerdì','sabato'}; wAbbr = {'dom','lun','mar','mer','gio','ven','sab'}; case 'pt' mFull = {'janeiro','fevereiro','março','abril','maio','junho','julho', ... 'agosto','setembro','outubro','novembro','dezembro'}; mAbbr = {'jan','fev','mar','abr','mai','jun','jul','ago','set', ... 'out','nov','dez'}; wFull = {'domingo','segunda-feira','terça-feira','quarta-feira', ... 'quinta-feira','sexta-feira','sábado'}; wAbbr = {'dom','seg','ter','qua','qui','sex','sáb'}; case 'el' ## Greek month names are the genitive (format-context) forms -- a date ## reads "9 Μα�τίου 2024", not the nominative "Μά�τιος". Greek has no ## ASCII letters, so matching against these canonical-case forms is in ## effect case-sensitive (strcmpi folds only ASCII). mFull = {'Ιανουα�ίου','Φεβ�ουα�ίου','Μα�τίου','Απ�ιλίου','Μα�ου', ... 'Ιουνίου','Ιουλίου','Αυγο�στου','Σεπτεμβ�ίου','Οκτωβ�ίου', ... '�οεμβ�ίου','Δεκεμβ�ίου'}; mAbbr = {'Ιαν','Φεβ','Μα�','Απ�','Μα�','Ιουν','Ιουλ','Αυγ','Σεπ', ... 'Οκτ','�οε','Δεκ'}; wFull = {'Κυ�ιακή','Δευτέ�α','Τ�ίτη','Τετά�τη','Πέμπτη','Πα�ασκευή', ... 'Σάββατο'}; wAbbr = {'Κυ�','Δευ','Τ�ί','Τετ','Πέμ','Πα�','Σάβ'}; dpMark = {'π.μ.', 'μ.μ.'}; otherwise error (strcat ("datetime: unsupported 'Locale' '%s'; supported", ... " languages are en, fr, de, es, it, pt, and el."), locale); endswitch endfunction ## Fast path for the constructor's format auto-detection. STRS is a cellstr ## column; the outputs are the N-by-6 date-vector matrix and a flag saying ## whether the fast path claimed the input. Core 'datevec' auto-detects a long ## list of formats and must stay the authority on what is accepted, so this ## claims an array only when every string carries one of the shapes below AND ## every parsed component is in range -- the conditions under which the LDML ## parser was verified to reproduce 'datevec' element for element. Anything ## else is declined and falls back to 'datevec' untouched. ## MATLAB's default pivot for a two-digit year: the current year less fifty. ## None of the auto-detected shapes carries a two-digit year, so this is never ## actually consulted there, but the parser wants a value. function pivot = dtDefaultPivot () now6 = clock (); pivot = now6(1) - 50; endfunction ## Detect the one date/time format a set of strings is written in, returning it ## as an LDML pattern, or '' when none is recognized. The format is taken from ## the first string, as MATLAB takes it, and every other string is then read ## with it; a string in a different format does not get one of its own. Where a ## shape carries fractional seconds, the pattern is widened to the digits ## actually present. ## ## LOCALE decides the shapes that carry names, as MATLAB has them: the month ## names are the locale's, and some long forms belong to one language only -- ## 'MMMM d, y' to English, 'd. MMMM y' to German, 'd 'de' MMMM 'de' y' to ## Spanish and Portuguese, and 'd MMMM y' to the rest but German and ## Portuguese. The name in a shape is any run of letters; the parser then ## reads it against the locale's tables. ## ## The slash shape is ambiguous: 03/09/2024 could be the 3rd of September or the ## 9th of March. The whole set decides, since one string past the twelfth of a ## month settles it for all of them, and only when nothing does is the reading ## of the locale assumed and a warning raised -- both as MATLAB does. ## ## A year is matched as four digits OR MORE and may carry a sign, which is why ## the year token emitted is 'uuuu' rather than 'yyyy': the two parse a digit ## run identically, but only 'u' accepts the sign. How wide a year may be is ## not settled here but in the parser, which bounds its VALUE at six ## significant digits -- so 32767, 0032767 and 100000 are read and 1000000 is ## refused, as MATLAB does. Two shapes deliberately keep the narrow year, ## because MATLAB refuses them too: a year-first SLASH date (32767/03/15) and a ## bare year standing alone (32767). function fmt = dtDetectFormat (strs, locale = '') persistent shapes; if (isempty (shapes)) ## A month name: letters of any script, with a point after an abbreviation nm = '[^\s\d,./:-]+\.?'; tm = {'', ''; ' \d{1,2}:\d{2}', ' HH:mm'; ' \d{1,2}:\d{2}:\d{2}', ... ' HH:mm:ss'}; shapes = { '^-?\d{4,}-\d{1,2}-\d{1,2}$', 'uuuu-MM-dd', {}; '^-?\d{4,}-\d{1,2}-\d{1,2} \d{1,2}:\d{2}$', 'uuuu-MM-dd HH:mm', {}; '^-?\d{4,}-\d{1,2}-\d{1,2} \d{1,2}:\d{2}:\d{2}$', ... 'uuuu-MM-dd HH:mm:ss', {}; '^-?\d{4,}-\d{1,2}-\d{1,2} \d{1,2}:\d{2}:\d{2}\.\d+$', ... 'uuuu-MM-dd HH:mm:ss.', {}; '^-?\d{4,}-\d{1,2}-\d{1,2}T\d{1,2}:\d{2}$', "uuuu-MM-dd'T'HH:mm", {}; '^-?\d{4,}-\d{1,2}-\d{1,2}T\d{1,2}:\d{2}:\d{2}$', ... "uuuu-MM-dd'T'HH:mm:ss", {}; '^-?\d{4,}-\d{1,2}-\d{1,2}T\d{1,2}:\d{2}:\d{2}\.\d+$', ... "uuuu-MM-dd'T'HH:mm:ss.", {}; '^\d{4}/\d{1,2}/\d{1,2}$', 'yyyy/MM/dd', {}; '^\d{4}/\d{1,2}/\d{1,2} \d{1,2}:\d{2}$', 'yyyy/MM/dd HH:mm', {}; '^\d{4}/\d{1,2}/\d{1,2} \d{1,2}:\d{2}:\d{2}$', 'yyyy/MM/dd HH:mm:ss', {}; '^\d{2}\.\d{2}\.\d{2}$', 'dd.MM.yy', {}}; ## Slashed dates, ordered by dtSlashOrder: '/' and the year and time for k = 1:rows (tm) shapes(end+1,:) = {['^\d{1,2}/\d{1,2}/-?\d{4,}', tm{k,1}, '$'], ... ['/uuuu', tm{k,2}], {}}; shapes(end+1,:) = {['^\d{1,2}/\d{1,2}/\d{2}', tm{k,1}, '$'], ... ['/yy', tm{k,2}], {}}; endfor ## Dotted dates, day first in every locale. With a two-digit year all ## three fields have two digits, so that text such as a version number, ## '1.5.10', is not taken for a date when a column is read. for k = 1:rows (tm) shapes(end+1,:) = {['^\d{1,2}\.\d{1,2}\.-?\d{4,}', tm{k,1}, '$'], ... ['dd.MM.uuuu', tm{k,2}], {}}; endfor ## The shapes with a month name, and the languages each belongs to named = {'^\d{1,2}-%s--?\d{4,}', 'dd-MMM-uuuu', {}; '^%s \d{1,2}, -?\d{4,}', 'MMMM d, uuuu', {'en'}; '^\d{1,2}\. %s -?\d{4,}', 'd. MMMM uuuu', {'de'}; '^\d{1,2} de %s de -?\d{4,}', "d 'de' MMMM 'de' uuuu", ... {'es', 'pt'}; '^\d{1,2} %s -?\d{4,}', 'dd MMMM uuuu', ... {'en', 'fr', 'es', 'it', 'el'}}; for j = 1:rows (named) for k = 1:rows (tm) n = rows (shapes) + 1; shapes{n,1} = [strrep(named{j,1}, '%s', nm), tm{k,1}, '$']; shapes{n,2} = [named{j,2}, tm{k,2}]; shapes{n,3} = named{j,3}; endfor endfor shapes(end+1:end+3,:) = {'^\d{1,2}:\d{2}:\d{2}$', 'HH:mm:ss', {}; '^\d{1,2}:\d{2}$', 'HH:mm', {}; '^\d{4}$', 'yyyy', {}}; endif [lang, region] = dtLocaleTag (locale); fmt = ''; first = strs{1}; ## An ISO 8601 date and time carrying a UTC offset, as RFC 3339 writes it. ## MATLAB detects none of these; reading them is an Octave extension. offPat = '(Z|[+-]\d{2}(:?\d{2})?)$'; if (! isempty (regexp (first, ['^-?\d{4,}-\d{1,2}-\d{1,2}[T ]', ... '\d{1,2}:\d{2}(:\d{2}(\.\d+)?)?', offPat], ... 'once'))) body = regexprep (first, offPat, ''); if (any (body == 'T')) fmt = "uuuu-MM-dd'T'HH:mm"; else fmt = 'uuuu-MM-dd HH:mm'; endif if (sum (body == ':') == 2) fmt = [fmt, ':ss']; frac = regexp (body, '\.(\d+)$', 'tokens', 'once'); if (! isempty (frac)) fmt = [fmt, '.', repmat('S', 1, numel (frac{1}))]; endif endif fmt = [fmt, 'XXX']; return; endif idx = 0; for k = 1:rows (shapes) langs = shapes{k,3}; if ((isempty (langs) || any (strcmp (lang, langs))) && ! isempty (regexp (first, shapes{k,1}, 'once'))) idx = k; break; endif endfor if (idx == 0) return; endif fmt = shapes{idx,2}; if (fmt(1) == '/') order = dtSlashOrder (strs, lang, region); if (! isempty (order)) order = [order(1:6), fmt(2:end)]; endif fmt = order; elseif (fmt(end) == '.') ## Widen the fractional field to the digits the text actually carries. frac = regexp (first, '\.(\d+)$', 'tokens', 'once'); fmt = [fmt, repmat('S', 1, numel (frac{1}))]; endif endfunction ## The language and region of LOCALE in lower case, English for none or for ## 'system' function [lang, region] = dtLocaleTag (locale) if (isempty (locale)) locale = ''; endif [lang, rest] = strtok (lower (char (locale)), '_-'); region = strtok (rest, '_-'); if (isempty (lang) || strcmp (lang, 'system')) lang = 'en'; region = ''; endif endfunction ## Decide whether a set of day/month/year strings separated by slashes is ## written the American way round or the European one. A first field past 12 ## can only be a day and a second field past 12 can only be a month, so one such ## string settles the whole set; with nothing to go on, the reading of the ## locale is taken and said so: month first in English but for 'en_GB', day ## first elsewhere, and without a word in 'pt_PT'. German writes dates with ## dots, and MATLAB refuses slashes under it, so '' is returned for it. function fmt = dtSlashOrder (strs, lang = 'en', region = '') if (strcmp (lang, 'de')) fmt = ''; return; endif first = regexp (strs, '^(\d{1,2})/(\d{1,2})/', 'tokens', 'once'); f1 = cellfun (@(t) str2double (t{1}), first); f2 = cellfun (@(t) str2double (t{2}), first); dayFirst = ! (strcmp (lang, 'en') && ! strcmp (region, 'gb')); if (any (f1 > 12)) fmt = 'dd/MM/uuuu'; elseif (any (f2 > 12)) fmt = 'MM/dd/uuuu'; elseif (dayFirst) fmt = 'dd/MM/uuuu'; if (! (strcmp (lang, 'pt') && strcmp (region, 'pt'))) warning ('Octave:datetime:ambiguous-format', ... strcat ("datetime: the text was read with the format", ... " 'dd/MM/yyyy', but 'MM/dd/yyyy' would read it just", ... " as well; give 'InputFormat' to say which is", ... " meant.")); endif else fmt = 'MM/dd/uuuu'; ## The message names 'MM/dd/yyyy' rather than the 'uuuu' actually used: it ## exists to say which of day-first and month-first was assumed, and reads ## the same either way for any year a slash date carries. warning ('Octave:datetime:ambiguous-format', ... strcat ("datetime: the text was read with the format", ... " 'MM/dd/yyyy', but 'dd/MM/yyyy' would read it just", ... " as well; give 'InputFormat' to say which is", ... " meant.")); endif endfunction ## Parse a cell array of date/time strings under an LDML 'InputFormat', ## returning an N-by-6 date-vector matrix. The work is done by the compiled ## __ldml__ helper: the format is tokenized like a display format, literals ## must match, and each field consumes characters from the string -- numeric ## fields take digits (up to the field's natural width, or exactly the run ## length when butted against another numeric field), name fields (MMM/MMMM, ## the weekday names and the day period 'a') take letters, matched case- and ## accent-insensitively against the locale tables. Two-digit years are ## resolved against PIVOT. Fields absent from the format default to the ## current date (year/month/day) or to zero (time), matching MATLAB. function [DV, OFF] = dtParseInput (strs, fmt, pivot, locale, leapok = false, ... lone = numel (strs) == 1) ## An unset 'Locale' arrives as [], which the helper reads as English. if (isempty (locale)) locale = ''; endif [DV, OFF] = __ldml__ ('parse', strs, fmt, pivot, locale, leapok, lone); endfunction ## Render each element of a datetime array to a display string under a ## concrete LDML pattern. Y..S are the wall-clock component arrays; TZ is the ## time zone ('' for unzoned); FMT is a concrete pattern already resolved by ## dtResolveFormat. NaT elements render as 'NaT' and infinite years as ## '-Inf'/'Inf', matching the component-store sentinels. function cstr = dtFormatStrings (Y, M, D, H, Mi, S, TZ, OFF, fmt, zoneStyle) ## The zone-dependent quantities still come from the compiled tz database; ## the rendering itself is done by __ldml__. Read them only when the format ## actually names a zone field: the 'z' name field needs both the ## abbreviation (mode 'iana', and the whitelist test of mode 'matlab') and ## the offset (the mode 'matlab' fallback). syms = __ldml__ ('symbols', fmt); hasZ = any (syms == 'z'); hasTZ = ! isempty (TZ); needOff = (any (ismember (syms, 'ZXx')) || hasZ) && hasTZ; needAbbr = hasZ && hasTZ; off = []; abbr = {}; if (needOff) ## The array carries its own offsets, so the numeric zone fields are read ## off them rather than resolved from the wall clock a second time. off = OFF + zeros (size (Y)); endif if (needAbbr) ## Looking the name up by wall clock alone would name the later of a ## repeated pair, so the offset goes with it and the database answers for ## the pass the element is actually on -- 'EDT' for the hour still on ## daylight saving, 'EST' for the hour that repeats it. abbr = dtZoneAbbrev (Y, M, D, H, Mi, S, TZ, OFF); endif cstr = __ldml__ ('format', Y, M, D, H, Mi, S, fmt, zoneStyle, hasTZ, ... off, abbr); endfunction ## Zone abbreviation (e.g. 'EDT', 'EST', 'UTC') for each element of a zoned ## datetime, from the compiled tz database via the __datetime__ builtin. function ab = dtZoneAbbrev (Y, M, D, H, Mi, S, TZ, off) if (dtIsZeroOffsetZone (TZ)) ab = repmat ({'UTC'}, size (Y)); return; endif ab = __datetime__ (Y, M, D, H, Mi, S, 'ConvertTo', 'zoneabbrev', ... 'Offset', off, ... 'TimeZone', TZ, 'Precision', 'microseconds'); endfunction ## Logical daylight-saving-time flag for each element of a zoned datetime, ## from the compiled tz database via the __datetime__ builtin. function tf = dtIsDst (Y, M, D, H, Mi, S, TZ, off) if (dtIsZeroOffsetZone (TZ) || ! isempty (dtFixedOffset (TZ))) tf = false (size (Y)); return; endif tf = logical (__datetime__ (Y, M, D, H, Mi, S, 'ConvertTo', 'isdst', ... 'TimeZone', TZ, 'Offset', off, 'Precision', ... 'microseconds')); endfunction ## Daylight saving part of the offset in seconds for each element of a zoned ## datetime, from the compiled tz database via the __datetime__ builtin; zero ## for a zone without transitions. function dst = dtDstOffset (Y, M, D, H, Mi, S, TZ, off) if (dtIsZeroOffsetZone (TZ) || ! isempty (dtFixedOffset (TZ))) dst = zeros (size (Y)); return; endif dst = __datetime__ (Y, M, D, H, Mi, S, 'ConvertTo', 'dstoffset', ... 'TimeZone', TZ, 'Offset', off, 'Precision', ... 'microseconds'); endfunction ## Parse text into an N-by-6 date-vector matrix for a leap-second array with no ## 'InputFormat'. The only shape accepted is the one such an array displays: an ## ISO 8601 UTC instant, with the fractional seconds optional and of any width. ## A string that does not match cannot be read, and since there is no format to ## name in the message, the whole input is rejected -- as it is when no format ## can be detected at all. The seconds field is left as parsed, 60 included; ## the caller decides whether that second was really inserted. function DV = dtParseLeapText (strs) pat = '^(-?\d+)-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)Z$'; tok = regexp (strs, pat, 'tokens', 'once'); if (any (cellfun (@isempty, tok))) error (strcat ("datetime: text for a 'UTCLeapSeconds' datetime array", ... " must be an ISO 8601 UTC instant, as in", ... " '2016-12-31T23:59:60Z', optionally with fractional", ... " seconds.")); endif DV = cellfun (@(t) str2double (t(:).'), tok, 'UniformOutput', false); DV = cell2mat (DV(:)); endfunction ## The display format a leap-second array carries. It is a concrete pattern, ## not one of the data-dependent sentinels: the array must be able to render a ## 60th second, and only this ISO 8601 UTC pattern does. function fmt = dtLeapFormat () fmt = "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'"; endfunction ## Validate a Format for a leap-second array. Only the ISO 8601 UTC pattern is ## allowed, optionally with one to nine fractional-second digits; MATLAB rejects ## everything else, the sentinels included. OP names the caller. function dtValidateLeapFormat (fmt, op, what = 'display format') ok = ischar (fmt) && isrow (fmt) ... && ! isempty (regexp (fmt, "^uuuu-MM-dd'T'HH:mm:ss(\\.S{1,9})?'Z'$", ... 'once')); if (! ok) error (strcat ("%s: the %s of a 'UTCLeapSeconds' datetime", ... " array must be \"uuuu-MM-dd'T'HH:mm:ss'Z'\",", ... " optionally with one to nine fractional second digits,", ... " as in \"uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'\"."), ... op, what); endif endfunction ## Enforce MATLAB's rule that an array with leap seconds cannot be combined ## with, or compared against, one without them -- not even a UTC array. The two ## do not count the same seconds, so there is no shared timeline on which the ## operation would mean anything. Callers make the zoned/unzoned check first, ## as MATLAB does. function dtCheckLeapPair (A, B, op) if (dtIsLeapZone (A.TimeZone) != dtIsLeapZone (B.TimeZone)) error (strcat ("datetime.%s: cannot combine or compare a datetime", ... " array with leap seconds with one without leap", ... " seconds."), op); endif endfunction ## Split the constructor's positional numeric arguments into six component ## arrays, which is how the leap-second normaliser needs them: a single date ## vector matrix carries one element per row, separate arrays broadcast against ## each other, and a seventh argument holds milliseconds. The shapes have ## already been validated by the builtin when this is called. function [Y, M, D, h, mi, sec] = dtSplitComponents (args) if (numel (args) == 1) V = args{1}; Y = V(:,1); M = V(:,2); D = V(:,3); if (columns (V) >= 6) h = V(:,4); mi = V(:,5); sec = V(:,6); else h = zeros (size (Y)); mi = h; sec = h; endif else Y = args{1}; M = args{2}; D = args{3}; if (numel (args) >= 6) h = args{4}; mi = args{5}; sec = args{6}; else h = 0; mi = 0; sec = 0; endif if (numel (args) >= 7) sec = sec + args{7} / 1000; endif endif endfunction ## True when TZ names the leap-second time zone. 'UTCLeapSeconds' is UTC with ## the inserted seconds made representable, and is the only time zone in which ## the 60th second of a minute exists. It is not an IANA zone and, as in ## MATLAB, 'timezones' does not list it, but it is accepted wherever a zone is. function tf = dtIsLeapZone (TZ) tf = ! isempty (TZ) && strcmp (TZ, 'UTCLeapSeconds'); endfunction ## POSIX time of each leap-second insertion in the shipped tz database, that is, ## of the first instant after each inserted second (78796800 for the second ## inserted at the end of 1972-06-30). Returned as a row vector so the tests ## below broadcast against a column of instants. Read once and cached: the ## table only changes when the package's tzdata does. function ins = dtLeapInsertions () persistent tbl = []; if (isempty (tbl)) tbl = __datetime__ ('leapseconds')(:).'; endif ins = tbl; endfunction ## Position of each inserted second on the continuous SI-second timeline of ## 'dtLeapSerial'. The n-th insertion is preceded by n-1 earlier ones, each of ## which pushed the timeline one second ahead of POSIX time, so it begins that ## many seconds after its own POSIX time. function S = dtLeapStarts () ins = dtLeapInsertions (); S = ins + (0:numel (ins) - 1); endfunction ## POSIX time of the start of the minute named by the components, resolving any ## overflow above the seconds field by ordinary leap-free calendar arithmetic. ## The seconds are deliberately left out of the call: they may hold 60, which ## the C++ normaliser would roll into the next minute. This is the seam that ## makes MATLAB's split behaviour fall out -- an hour or minute that overflows ## steps over an inserted second (23:60:00 on a leap day is the next midnight), ## while the seconds field is counted on the leap-second timeline by the callers ## below. function p = dtLeapMinutePosix (Y, M, D, h, mi, z) p = __datetime__ (Y + z, M + z, D + z, h + z, mi + z, z, 'ConvertTo', ... 'posixtime', 'TimeZone', 'UTC', 'Precision', ... 'microseconds'); endfunction ## Continuous count of SI seconds since 1970-01-01T00:00:00 UTC for a POSIX ## instant: POSIX time plus every second inserted up to it. An insertion is ## stamped with the POSIX time of the instant after it, so an instant equal to ## that stamp already has the inserted second behind it. function s = dtPosix2Leap (p) ins = dtLeapInsertions (); s = p + reshape (sum (p(:) >= ins, 2), size (p)); endfunction ## Continuous count of SI seconds since 1970-01-01T00:00:00 UTC for leap-second ## wall-clock components. Every UTC day contributes its true length, 86401 ## seconds on a day that ends with an inserted second, so the count is injective ## across an inserted second and strictly increasing through it. That is what ## lets every instant-based operation of the class -- arithmetic, comparison, ## sorting, set membership, interpolation -- count inserted seconds without ## knowing they exist. Not-A-Time maps to NaN and infinite elements keep their ## sign. function s = dtLeapSerial (Y, M, D, h, mi, sec) z = zeros (size (Y + M + D + h + mi + sec)); s = dtPosix2Leap (dtLeapMinutePosix (Y, M, D, h, mi, z)) + (sec + z); endfunction ## POSIX time of leap-second wall-clock components. POSIX time cannot name an ## inserted second, and MATLAB resolves that by folding forward here: the 60th ## second of a minute shares its stamp with the following second, so 23:59:60 ## reads as the next midnight. Every other conversion folds backward instead ## (see dtLeapBackFold), which is not an inconsistency: the tz project's own ## reference conversion, time2posix(3), folds POSIX forward the same way, while ## C++20's utc_clock, Rust's chrono and java.time all fold backward. function p = dtLeapPosix (Y, M, D, h, mi, sec) z = zeros (size (Y + M + D + h + mi + sec)); p = dtLeapMinutePosix (Y, M, D, h, mi, z) + (sec + z); endfunction ## Seconds field folded backward, that is, mapped to the last second that is not ## an inserted one. The whole second drops by one and any fractional part is ## kept, so 23:59:60.25 is read as 23:59:59.25. This is the rule every ## conversion other than 'posixtime' follows. function sec = dtLeapBackFold (sec) sec = sec - (sec >= 60); endfunction ## Inverse of 'dtLeapSerial': map a continuous SI-second count back to ## leap-second wall-clock components. A count inside an inserted second yields ## the 60th second of its minute; any other count is an ordinary POSIX time once ## the whole seconds inserted before it have been taken back out. function [Y, M, D, h, mi, sec] = dtLeapComponents (ser) S = dtLeapStarts (); sz = size (ser); v = ser(:); inLeap = any (v >= S & v < S + 1, 2); ## Reading an inserted second as the 59th second of its minute and adding one ## afterwards keeps any fractional part, and needs no leap-day lookup. posix = v - sum (v >= S + 1, 2) - inLeap; [Y, M, D, h, mi, sec] = __datetime__ (reshape (posix, sz), 'ConvertFrom', ... 'posixtime', 'Precision', ... 'microseconds'); inLeap = reshape (inLeap, sz); sec(inLeap) += 1; endfunction ## Re-canonicalise leap-second wall-clock components: read them as a count of ## SI seconds and read that count back. Because the seconds field is counted on ## the leap-second timeline while the fields above it are not, this reproduces ## MATLAB's whole rule at once. A 60th second stays put on a day that ends with ## an inserted second (2016-12-31 23:59:60) and rolls over on any other day or ## minute (2016-12-30 23:59:60 and 2016-12-31 23:58:60 both become the next ## minute); one second past it rolls to the next midnight (23:59:61 is ## 2017-01-01 00:00:00, not 00:00:01, because the minute really did hold 61 ## seconds); one second before a midnight that follows an insertion lands on the ## inserted second; and a minute or hour that overflows steps over the insertion ## entirely (23:60:00 is the next midnight). function [Yo, Mo, Do, ho, mio, so] = dtLeapNormalize (Y, M, D, h, mi, sec) [Yo, Mo, Do, ho, mio, so] = dtLeapComponents (dtLeapSerial (Y, M, D, h, ... mi, sec)); endfunction ## Julian or modified Julian date of leap-second wall-clock components, under ## the stretched-day rule: the day number of the scale at hand, plus the SI ## seconds elapsed since that day began divided by the day's true length, 86401 ## seconds on a day that holds an inserted second. Julian days begin at noon ## and modified Julian days at midnight, so the two scales stretch different ## spans and on such a day differ by more than the usual 2400000.5. function out = dtLeapJulian (Y, M, D, h, mi, sec, doModified) z = zeros (size (Y + M + D + h + mi + sec)); Yv = Y + z; Mv = M + z; Dv = D + z; ser = dtLeapSerial (Y, M, D, h, mi, sec); if (doModified) ## Modified Julian day 40587 begins at midnight on 1970-01-01. dayNum = datenum (Yv, Mv, Dv) - datenum (1970, 1, 1) + 40587; hourOfDay = 0; else ## Julian day 2440588 begins at noon on 1970-01-01, so an element before ## noon still belongs to the Julian day that opened at the previous noon. dayNum = datenum (Yv, Mv, Dv) - datenum (1970, 1, 1) + 2440588; hourOfDay = 12; early = ser < dtLeapSerial (Yv, Mv, Dv, 12, 0, 0); dayNum(early) -= 1; Dv(early) -= 1; endif ## Taking the day's length as the distance to the next day of the same scale ## picks up an inserted second wherever one falls inside it. dayStart = dtLeapSerial (Yv, Mv, Dv, hourOfDay, 0, 0); dayLen = dtLeapSerial (Yv, Mv, Dv + 1, hourOfDay, 0, 0) - dayStart; out = dayNum + (ser - dayStart) ./ dayLen; endfunction ## TAI-UTC offset, in seconds, of an instant before 1972-01-01, given as a POSIX ## time (which is also the SI-second count there, no second having been inserted ## yet). Until 1972 UTC was kept near UT1 by running its seconds at a slightly ## different rate rather than by inserting whole ones, so the offset of that era ## is tabulated by the IERS as a base value plus a rate per day, referred to a ## Modified Julian Date. Before 1960 there is no table and the offset is zero. ## ## The table is followed as published. MATLAB's tt2000 disagrees across these ## twelve years by up to 1.3 ms, its rate evidently evaluated half a day from ## where the table refers it -- the discrepancy is exactly half a day's worth of ## each row's rate. Being bug-compatible here would mean writing an arithmetic ## error into a published table, so it is not done; documented in convertTo. ## Outside 1960-1972 the two agree exactly, every probed anchor from 1707 to ## 2292 included. function dAT = dtDeltaATPre (p) ## POSIX time from which the row applies, base offset in seconds, the MJD the ## base is referred to, and the rate in seconds per day. persistent T = [ -315619200, 1.4178180, 37300, 0.0012960; -283996800, 1.4228180, 37300, 0.0012960; -265680000, 1.3728180, 37300, 0.0012960; -252460800, 1.8458580, 37665, 0.0011232; -194659200, 1.9458580, 37665, 0.0011232; -189388800, 3.2401300, 38761, 0.0012960; -181526400, 3.3401300, 38761, 0.0012960; -168307200, 3.4401300, 38761, 0.0012960; -157766400, 3.5401300, 38761, 0.0012960; -152668800, 3.6401300, 38761, 0.0012960; -142128000, 3.7401300, 38761, 0.0012960; -136771200, 3.8401300, 38761, 0.0012960; -126230400, 4.3131700, 39126, 0.0025920; -60480000, 4.2131700, 39126, 0.0025920]; mjd = p / 86400 + 40587; dAT = zeros (size (p)); for k = 1:rows (T) in = p >= T(k,1); dAT(in) = T(k,2) + (mjd(in) - T(k,3)) * T(k,4); endfor endfunction ## Correction that carries the SI-second count of an instant before 1972 onto ## the scale tt2000 is measured on. From 1972-01-01 the offset from TAI is the ## ten seconds UTC started out behind plus every second inserted since, and the ## count already carries both, so the epoch constant alone does the work and the ## correction is zero. Earlier there were no inserted seconds and no ten-second ## head start either, so the tabulated offset of the era replaces them. function corr = dtTT2000PreCorr (ser) corr = zeros (size (ser)); early = ser < 63072000; # 1972-01-01T00:00:00Z if (any (early(:))) corr(early) = dtDeltaATPre (ser(early)) - 10; endif endfunction ## Nanoseconds since the tt2000 epoch for a continuous SI-second count. The ## epoch is the J2000 Terrestrial Time epoch, 2000-01-01T11:58:55.816Z, which is ## SI second 946727957.816 of the leap-second timeline (22 seconds had been ## inserted by then). The arithmetic is carried on a whole-second part and a ## nanosecond part because the product overflows a double, whose 53-bit mantissa ## runs out at 2^53 nanoseconds -- about 104 days -- long before an int64 does. function out = dtSerial2TT2000 (ser) ser = ser + dtTT2000PreCorr (ser); sw = floor (ser); dw = sw - 946727957; dn = round ((ser - sw) * 1e6) * 1000 - 816000000; ## Carry so that the nanosecond part stays in [0, 1e9). borrow = dn < 0; dw(borrow) -= 1; dn(borrow) += 1e9; ## An int64 spans -9223372037 s + 145224192 ns to 9223372036 s + 854775807 ns. ok = isfinite (ser) ... & (dw > -9223372037 | (dw == -9223372037 & dn >= 145224192)) ... & (dw < 9223372036 | (dw == 9223372036 & dn <= 854775807)); if (! all (ok(:))) error (strcat ("datetime.convertTo: 'tt2000' conversion is not", ... " supported for missing values, infinite datetimes,", ... " or datetimes outside the interval", ... " [1707-09-22T12:12:10Z, 2292-04-11T11:46:08Z).")); endif ## Assemble the int64 without letting an intermediate overflow: below the ## epoch the whole-second part alone already saturates, so borrow a second ## from it first and take the remainder off afterwards. out = zeros (size (ser), 'int64'); lo = dw < 0; out(lo) = (int64 (dw(lo)) + 1) * 1000000000 - (1000000000 - int64 (dn(lo))); out(! lo) = int64 (dw(! lo)) * 1000000000 + int64 (dn(! lo)); endfunction ## Continuous SI-second count of a tt2000 nanosecond value; the inverse of ## 'dtSerial2TT2000'. The int64 is split before it is widened, since converting ## it whole to a double would lose the nanoseconds. function ser = dtTT20002Serial (ns) if (any (ns(:) <= int64 (-9223372036854775805))) error (strcat ("datetime: int64 input values for tt2000 times must be", ... " larger than -9223372036854775805.")); endif dw = double (idivide (ns, int64 (1000000000), 'floor')); dn = double (mod (ns, int64 (1000000000))); w = 946727957 + dw; us = 816000 + round (dn / 1000); carry = us >= 1e6; w(carry) += 1; us(carry) -= 1e6; ser = w + us / 1e6; ## Undo the pre-1972 correction. It depends on the instant it is applied to, ## but only through a rate of a few milliseconds per year, so a couple of ## passes settle it far below the microsecond the class stores. early = ser < 63072000; if (any (early(:))) target = ser(early); s = target; for k = 1:3 s = target - (dtDeltaATPre (s) - 10); endfor ser(early) = s; endif endfunction ## Clamp a seconds field that names an inserted second onto a minute that has ## none. Calendar arithmetic preserves the wall clock, so adding a month or a ## calendar day to 2016-12-31 23:59:60 asks for a 60th second of a minute that ## never had one; MATLAB clamps it to the 59th, just as it clamps the day of the ## month when a month is added to the 31st. P is the POSIX time of the start of ## each target minute. function sec = dtLeapClampSecond (sec, p) ins = dtLeapInsertions (); keep = reshape (any (p(:) + 60 == ins, 2), size (p)); sec(sec >= 60 & ! keep) -= 1; endfunction ## Integer fixed-epoch conversions (NTP, NTFS/FILETIME, .NET) for convertTo. ## POSIX is the absolute UTC instant in seconds (from the 'serial' proxy). The ## result is built from integer milliseconds through a whole-second/fraction ## split, keeping every intermediate below 2^53 so the final uint64 product is ## exact. Missing, infinite, or out-of-range instants raise an error because ## the integer formats cannot represent NaN. function out = dtFixedEpoch (posix, kind) switch (kind) case 'ntp' offset = 2208988800; scale = 4294967296; hiLimit = 4294967296; tname = 'NTP'; case 'ntfs' offset = 11644473600; scale = 10000000; hiLimit = 1844674407370; tname = 'NTFS'; case 'dotnet' offset = 62135596800; scale = 10000000; hiLimit = 1844674407370; tname = '.NET'; endswitch ms = round (posix * 1000); secWhole = floor (ms / 1000) + offset; fracMs = ms - floor (ms / 1000) * 1000; ok = isfinite (ms) & secWhole >= 0 & secWhole < hiLimit; if (! all (ok(:))) error (strcat ("datetime.convertTo: '%s' conversion is not supported", ... " for", ... " missing values, infinite datetimes, or datetimes", ... " outside", ... " the representable range."), tname); endif frac = round (fracMs .* scale ./ 1000); out = uint64 (secWhole) .* uint64 (scale) + uint64 (frac); endfunction ## The instant at WALLSEC wall-clock seconds into the local day opening on ## Y-M-D. Where that wall time does not exist -- it fell in the gap a ## spring-forward opens -- the conversion resolves forward and MATLAB steps ## back, so the overshoot is measured off the result and removed. It is zero ## whenever the wall time exists, which is every call away from a transition. function s = dtWallGridPoint (Y, M, D, wallsec, wall2ser, ser2cal) s = wall2ser (Y, M, D, wallsec); [~, ~, ~, hR, nR, sR] = ser2cal (s); s -= (hR * 3600 + nR * 60 + sR) - mod (wallsec, 86400); endfunction ## Rewrite one of the five invertible 'convertTo' counts into the conversion ## the builtin already performs, returning the value and the name of that ## conversion. Each is the exact inverse of the forward direction, so the pair ## round-trips: the Julian scales are instant-based away from the leap-second ## zone (where 'juliandate' is posixtime / 86400 + 2440587.5, and the Modified ## scale exactly 2400000.5 less, the two sharing a day there), and the 1904 ## Excel date is a serial date number counted from 1904-01-01. function [v, alias] = dtEpochAlias (x, kind) if (! isnumeric (x) && ! islogical (x)) if (any (strcmp (kind, {'ntp', 'ntfs', '.net'}))) v = dtFixedEpochInv (x, kind); # raises the type error for us endif error ("datetime: '%s' input must be numeric.", kind); endif switch (kind) case 'excel1904' v = double (x) + datenum (1904, 1, 1); alias = 'datenum'; case 'juliandate' v = (double (x) - 2440587.5) * 86400; alias = 'posixtime'; case 'modifiedjuliandate' ## Counted from its own epoch rather than through the Julian one, for ## the reason given in 'juliandate'. v = (double (x) - 40587) * 86400; alias = 'posixtime'; otherwise v = dtFixedEpochInv (x, kind); alias = 'posixtime'; endswitch endfunction ## Inverse of 'dtFixedEpoch': read an NTP, NTFS or .NET count back into POSIX ## seconds. MATLAB requires the unsigned 64-bit type rather than rounding a ## double, and so do we: an NTP stamp runs to 1.7e19, far past the 2^53 a ## double holds exactly, so the whole and fractional parts are separated in ## integer arithmetic before either becomes a double. The whole seconds are ## under 2^36 once the epoch is removed, well inside a double. function p = dtFixedEpochInv (x, kind) switch (kind) case 'ntp' offset = 2208988800; scale = 4294967296; tname = 'NTP'; case 'ntfs' offset = 11644473600; scale = 10000000; tname = 'NTFS'; case '.net' offset = 62135596800; scale = 10000000; tname = '.NET'; endswitch if (! isa (x, 'uint64')) error (strcat ("datetime: input for converting from %s times must be", ... " uint64."), tname); endif whole = idivide (x, uint64 (scale), 'floor'); frac = double (x - whole * uint64 (scale)); p = (double (whole) - offset) + frac / scale; endfunction ## Inverse of 'dtLeapJulian': read a Julian or Modified Julian day of the ## leap-second zone back into a serial. DOMODIFIED selects the scale, which ## fixes both the epoch and the hour the day opens at -- midnight for the ## Modified scale, noon for the other. The fraction is taken against the ## day's own length, so an inserted second inside that day is recovered rather ## than folded away, which is what lets this scale name 23:59:60. function ser = dtJulian2Serial (val, doModified) dayNum = floor (val); frac = val - dayNum; if (doModified) dn = dayNum - 40587 + datenum (1970, 1, 1); hourOfDay = 0; else dn = dayNum - 2440588 + datenum (1970, 1, 1); hourOfDay = 12; endif [Y, M, D] = datevec (dn); Y = reshape (Y, size (val)); M = reshape (M, size (val)); D = reshape (D, size (val)); z = zeros (size (val)); dayStart = dtLeapSerial (Y, M, D, hourOfDay + z, z, z); dayLen = dtLeapSerial (Y, M, D + 1, hourOfDay + z, z, z) - dayStart; ser = dayStart + frac .* dayLen; endfunction ## Bin edges for a requested bin count, snapped to whole calendar or clock ## units. XMIN, XMAX and the result all count seconds on the 'serial' ## timeline and NBINS is the number of bins asked for. SER2CAL and CAL2SER ## carry the calendar: 'SER2CAL (s)' returns the year, month and day holding the ## instant S, and 'CAL2SER (Y, M, D)' the instant at which that date begins, ## both in the array's own time zone. They are handed in rather than worked out ## here because only the class knows its zone, its leap seconds and its epoch. ## TICK is the half-width used when the data are constant and defaults to half a ## second, where a 'duration' uses half a millisecond. ## ## This is NOT the placement 'duration' uses and 'gridbinedges', its own local ## function, must not be borrowed for it. Three differences separate them, ## each verified against ## R2024a and each of which on its own yields edges that look plausible and are ## wrong: ## ## * the left edge is fixed BEFORE the width; 'duration' fixes the width first ## * the provisional width rounds up STRICTLY (floor + 1, never ceil), so a ## span that divides exactly still widens ## * the acceptance test applies to the FINAL left edge and width, not to the ## provisional one, which is what decides the hour/day boundary ## ## The ladder runs year, month, day, hour, minute, second, largest first, and ## the first unit that fits wins. There is no week rung and no separate ## quarter, decade or century: a quarter is a three-month width and a decade a ## ten-year one, both of which fall out of the multiples. Below one second no ## unit is left and the plain numeric rule takes over. ## ## The ladder is split, and the split is not where it looks. Year, month AND ## DAY are true calendar units: their edges land on 1 January, on the 1st, and ## on local midnight, so a bin holding a daylight-saving transition is 23 or 25 ## hours long while its neighbours are 24. Only hour, minute and second are ## fixed spans of seconds, and they stay fixed across a transition -- an hour ## bin spanning a spring-forward covers two wall-clock hours. Both halves are ## measured: in America/New_York a 5-day span gives bins of 48, 48 and 47 hours ## with every edge at local midnight, and a 1-day span at 3 bins gives 9-hour ## bins whose wall clock jumps from 20:00 to 06:00 across the gap. ## ## Every rung runs the same arithmetic, in 'dtBinGridIndex', on the INTEGER ## count of units between the one holding XMIN and the one holding XMAX. It is ## that integer count, and never the elapsed time, that drives the width and ## the acceptance test: 2024-01-01 to 2024-12-31 is eleven months here and ## 11.97 by elapsed time, and eleven is what reproduces R2024a. The same point ## in a different guise is that XMIN's position inside its unit reaches the ## result only through the unit it lands in, so sliding the data through a day ## moves where the bins sit but never how wide they are or which unit they use. function edges = dtBinEdgesGrid (xmin, xmax, nbins, ser2cal, cal2ser, ... wall2ser, tick = 0.5) ## Constant data: an interval of one tick centred on the value. The offsets ## are formed first and added once, rather than subtracting the tick and then ## stepping, so that only one rounding lands on a serial of order 1e9. if (xmin == xmax) edges = xmin + (-tick + (0:nbins) * (2 * tick / nbins)); return; endif [yLo, mLo, dLo] = ser2cal (xmin); [yHi, mHi, dHi] = ser2cal (xmax); ## Calendar years. idx = dtBinGridIndex (yHi - yLo, nbins); if (! isempty (idx)) one = ones (size (idx)); edges = cal2ser (yLo + idx, one, one); return; endif ## Calendar months. idx = dtBinGridIndex ((12 * yHi + mHi) - (12 * yLo + mLo), nbins); if (! isempty (idx)) one = ones (size (idx)); [Y, M, D] = dtAddMonths (yLo * one, mLo * one, one, idx); edges = cal2ser (Y, M, D); return; endif ## Calendar days. The count of days between the two dates is recovered by ## rounding the gap between their midnights: a daylight-saving shift moves ## those by an hour or two and an inserted leap second by one, never by ## anything approaching half a day. spanU = round ((cal2ser (yHi, mHi, dHi) - cal2ser (yLo, mLo, dLo)) / 86400); idx = dtBinGridIndex (spanU, nbins); if (! isempty (idx)) one = ones (size (idx)); [Y, M, D] = dtAddDays (yLo * one, mLo * one, dLo * one, idx); edges = cal2ser (Y, M, D); return; endif ## Fixed units: one hour, one minute, one second. A moment's position on ## this grid is its WALL CLOCK reading -- seconds of the local day, counted ## from the midnight opening the day that holds XMIN -- and NOT the elapsed ## seconds since that midnight. The two part company as soon as a ## daylight-saving transition falls between the midnight and the moment: noon ## on the day Australia/Lord_Howe puts its clock forward half an hour is 11.5 ## elapsed hours after midnight, so flooring the elapsed count lands every ## edge on the half hour instead of the hour. Flooring XMIN itself would ## anchor on absolute time, which goes wrong the same way in a zone merely ## offset by half an hour (Asia/Kolkata, +05:30). ## ## The STEP, by contrast, is elapsed: once the left edge is placed, each ## further edge is a fixed span of seconds later, so an edge beyond a ## transition does read off the wall clock. MATLAB does the same. [yA, mA, dA, hA, nA, sA, offA] = ser2cal (xmin); [yB, mB, dB, hB, nB, sB, offB] = ser2cal (xmax); origin = cal2ser (yA, mA, dA); todA = hA * 3600 + nA * 60 + sA; ## A transition inside the data moves the whole grid one unit earlier, but ## only when the zone's shift is NOT a whole number of those units. That is ## the same law the named units obey: the edges step in elapsed time, so a ## whole-unit shift (America/New_York, Pacific/Chatham -- both an hour) keeps ## the grid on the wall clock and nothing moves, while Australia/Lord_Howe's ## half hour against an HOUR bin does not, and the grid shifts. Against a ## MINUTE bin that same half hour is whole again and nothing moves, which is ## why the test belongs inside the loop, keyed on the rung. Established by ## sweeping the data window across a spring-forward and a fall-back alike: ## it holds in both directions, so what decides is the presence of the ## discrepancy rather than its sign. A net offset change is the test, so two ## transitions cancelling inside the range would go unnoticed; no zone shifts ## twice within a span short enough to reach this rung. ## The SPAN is read off the ELAPSED axis, except where the zone's shift is ## not a whole number of grid units, when it is read off the WALL clock ## instead. The same discrepancy the anchor turns on decides this: with a ## whole-unit shift both readings serve and the elapsed one is MATLAB's, but ## a half-hour shift against an hour bin leaves them differing by a unit, ## and the wall reading is the one that counts. Australia/Lord_Howe in 1983, ## when the island still moved its clock a whole hour, takes the elapsed ## reading; the same zone today, on the same dates and spans, takes the wall ## one. todB = 86400 * round ((cal2ser (yB, mB, dB) - origin) / 86400) ... + hB * 3600 + nB * 60 + sB; for g = [3600, 60, 1] nLo = floor (todA / g); if (mod (offB - offA, g) != 0) spanU = floor (todB / g) - nLo; else spanU = floor ((xmax - origin) / g) - floor ((xmin - origin) / g); endif idx = dtBinGridIndex (spanU, nbins); if (! isempty (idx)) width = idx(2) - idx(1); shift = 0; ## The correction below is EMPIRICAL: measured against R2024a, never ## derived. It is therefore applied only over the domain it was measured ## on -- a shift of exactly HALF the bin unit, which is what ## Australia/Lord_Howe's half hour is against an hour bin, and the only ## fractional shift any zone still observes. Extrapolating it to other ## ratios is not merely unjustified but wrong: at Asia/Singapore's ## 20-minute shift of 1933 (a third of an hour) it fires and should not, ## and switching it off there recovers 60 rows. Where the ratio is ## anything else the plain rule stands and the placement is OUR OWN, a ## documented deviation -- see the note in 'discretize'. if (nbins > 1 && mod (offB - offA, g) != 0 && mod (2 * (offB - offA), g) == 0 && (mod (spanU, 2) != 0 || mod (nbins * width, 2) != 0)) shift = 1; endif ## The anchor is the grid point at or below XMIN on the wall clock, and ## every edge is counted from it in elapsed time. Where that grid point ## names a wall time that does not exist -- it fell in the gap a ## spring-forward opens -- MATLAB steps BACK out of the gap, while the ## conversion resolves forward, which is right for a clock a user wrote ## and wrong for a grid point. The overshoot is measured off the result ## and removed; it is zero whenever the wall time exists, so this is not ## a special case in the code even though it is one in the calendar. ## Counting from the anchor rather than converting the left edge's own ## index is what puts the remaining edges where MATLAB has them: 02:00 on ## Australia/Lord_Howe's spring-forward day resolves back to 01:30, and ## the edge one unit earlier is 00:30, not the 01:00 the index names. if (nbins == 1) ## A single bin is placed differently: BOTH its edges are grid points ## of the wall clock, so the one bin spans a whole number of units of ## the local clock and its elapsed length absorbs whatever the zone ## did in between -- two wall hours across Australia/Lord_Howe's ## spring forward are 90 minutes. With no interior edge there is no ## grid to step, and MATLAB names the far edge rather than reaching it. ## From two bins up the interior edges are stepped in elapsed time and ## leave the wall clock, which is what the branch below does. edges = arrayfun (@(k) dtWallGridPoint (yA, mA, dA, g * k, ... wall2ser, ser2cal), idx - shift + nLo); else anchor = dtWallGridPoint (yA, mA, dA, g * nLo, wall2ser, ser2cal); edges = anchor + (idx - shift) * g; endif return; endif endfor ## Finer than a second: no coarser unit is left to snap to, so the plain ## numeric rule takes over. This is the terminating case of the ladder and ## not a separate regime. ## ## The origin is moved to the whole second below the data first. That rule ## anchors the left edge on a decimal grid, and a serial runs to 1e9 seconds, ## where a double resolves only to about 2e-7 of a second -- far too coarse ## for a grid of, say, 0.4 s to land where it should. The shift is exact ## (the span here is under a second) and MATLAB, which holds its instants in ## a different unit entirely, never meets the problem. origin = floor (xmin); edges = origin + __binedges__ (xmin - origin, xmax - origin, nbins); endfunction ## Resolve the second argument of 'discretize' into explicit edges in serial ## seconds. A datetime argument is handled by the caller, which is the only ## place that can convert one. ISCOUNT reports a bin count, the one form whose ## edges MATLAB returns as a row whatever the shape of the input. function [ev, isCount] = dtBinEdges (xv, arg2, s2c, c2s, w2s, scope) isCount = false; xf = xv(isfinite (xv)); if (isa (arg2, 'duration') || isa (arg2, 'calendarDuration')) ev = dtUnitBinEdges (xf, arg2, s2c, c2s, w2s, scope); elseif (dtIsTextScalar (arg2)) ev = dtUnitBinEdges (xf, char (arg2), s2c, c2s, w2s, scope); elseif (isnumeric (arg2) && isscalar (arg2) && ! islogical (arg2)) if (! isreal (arg2) || ! isfinite (arg2) || arg2 < 1 || fix (arg2) != arg2) error ("%s: N must be a real positive integer.", scope); endif isCount = true; ev = dtCountBinEdges (xf, double (arg2), s2c, c2s, w2s, scope); elseif (isnumeric (arg2)) error (strcat (scope, ": numeric bin edges are not accepted for a", ... " datetime; give a datetime vector.")); else error (strcat (scope, ": the second argument must be a datetime, a bin", ... " count, a duration, a calendarDuration, or a unit name.")); endif endfunction ## True for a character vector or a scalar string function tf = dtIsTextScalar (x) tf = (ischar (x) && isrow (x)) || (isa (x, 'string') && isscalar (x)); endfunction ## Day of the week as an offset from Sunday, 0 .. 6. function k = dtWeekdayIndex (Y, M, D) k = weekday (datenum (Y, M, D)) - 1; endfunction ## Classify a bin width into the kind of unit it steps in and how many of that ## unit one bin spans. 'fixed' counts seconds, 'day' whole days and 'month' ## whole months; a quarter is three months, a year twelve, a decade a hundred ## and twenty and a century twelve hundred, so one snapping rule serves them ## all. SPEC is a scalar duration, a scalar calendarDuration, or a unit name. function [kind, step, named] = dtUnitStep (spec, scope) ## NAMED distinguishes a unit given by name from one given as a width. ## They differ only when the data span an exact multiple of the unit: ## a named unit then opens one more bin, a width does not. MATLAB ## makes that distinction for a datetime and not for a duration. named = ! (isa (spec, 'duration') || isa (spec, 'calendarDuration')); ## A unit NAME is accepted from every caller, including histcounts's ## 'BinWidth'. That last one is an OCTAVE EXTENSION, deliberate and kept: ## MATLAB's 'BinWidth' takes only a duration or a calendarDuration and sends ## named units to 'BinMethod', which we also accept. The two spellings are ## equivalent here, so refusing one buys the user nothing. Documented in the ## histcounts docstring and in PENDING_NEWS.md. Positionally -- discretize ## (T, 'day') -- a name is ordinary MATLAB behaviour, not an extension. if (! isa (spec, 'duration') && ! isa (spec, 'calendarDuration') && ! dtIsTextScalar (spec)) error (strcat (scope, ": a bin width must be a scalar duration or", ... " calendarDuration, or a unit name, and must be", ... " positive and finite.")); endif if (isa (spec, 'calendarDuration')) mo = calmonths (spec); dy = split (spec, 'days'); sc = seconds (time (spec)); if (! isscalar (spec) || ! all (isfinite ([mo, dy, sc])) || any ([mo, dy, sc] < 0) || ! any ([mo, dy, sc])) error (strcat (scope, ": a bin width must be positive and finite.")); endif ## A MIXED width -- months and days together, or either with a time of ## day -- is accepted, as MATLAB accepts it; refusing it was an invention ## of ours. Pure single-unit widths keep the branches they were verified ## on; only a mixture, or a calendarDuration carrying nothing but a time, ## needs the general grid. ## A month-bearing WIDTH anchors on 1 January of the data's year, which is ## not the 'month' branch's absolute grid: that one floors onto a multiple ## of the step counted from year zero, which a named 'decade' or 'century' ## needs (a decade must open on a year divisible by ten) but a width does ## not. The two agree whenever the step divides 12, which is why only ## calyears (1) + calmonths (1), a step of 13, exposes the difference. if (sc == 0 && mo == 0) kind = 'day'; step = dy; else kind = 'cal'; step = [mo, dy, sc]; endif return; endif if (isa (spec, 'duration')) kind = 'fixed'; step = seconds (spec); if (! isscalar (step) || ! (step > 0) || ! isfinite (step)) error (strcat (scope, ": a bin width must be positive and finite.")); endif return; endif switch (lower (char (spec))) case 'second' kind = 'fixed'; step = 1; case 'minute' kind = 'fixed'; step = 60; case 'hour' kind = 'fixed'; step = 3600; case 'day' kind = 'day'; step = 1; case 'week' kind = 'week'; step = 7; case 'month' kind = 'month'; step = 1; case 'quarter' kind = 'month'; step = 3; case 'year' kind = 'month'; step = 12; case 'decade' kind = 'month'; step = 120; case 'century' kind = 'month'; step = 1200; otherwise error (strcat (scope, ": UNIT must be one of 'second', 'minute',", ... " 'hour', 'day', 'week', 'month', 'quarter', 'year',", ... " 'decade', or 'century'.")); endswitch endfunction ## Edges one unit wide covering the data, aligned to whole multiples of that ## unit. Calendar units land on real boundaries: a day and a week on local ## midnight, a week on a Sunday, and a month, quarter, year, decade or century ## on the 1st of the month that starts the containing multiple. ## ## The bin count is 'floor (gap / step) + 1', so the largest value always opens ## a bin of its own rather than sitting on the closing edge -- 'day' bins over ## ten whole days give ten bins, not nine. That is MATLAB's rule here and it ## differs from 'duration', which rounds the same quantity up instead. function ev = dtUnitBinEdges (xf, spec, s2c, c2s, w2s, scope) [kind, step, named] = dtUnitStep (spec, scope); ## A named unit always opens a bin past the data; a width opens one ## only when the data do not end exactly on an edge. if (named) bins = @(gap) floor (gap / step) + 1; else bins = @(gap) max (1, ceil (gap / step)); endif ## Empty data anchors on the epoch, for the reason given in dtCountBinEdges. if (isempty (xf)) lo = 0; hi = 0; else lo = min (xf); hi = max (xf); endif switch (kind) case 'fixed' ## A fixed width is anchored on the start of the largest clock unit that ## does not exceed it, and stepped from there by a fixed span of ELAPSED ## seconds: ## ## a whole number of days -> the 1st of the month ## an hour or more -> local midnight ## a minute or more -> the start of the hour ## anything shorter -> the start of the minute ## ## Anchoring everything below a day on midnight leaves a width that does ## not divide its unit out of phase: minutes (50) over data opening at ## 07:00 opened the bins at 06:40, the midnight grid having run 8.4 steps ## by then, where MATLAB opens at 07:00. A width that divides its unit ## is unaffected, which is why the named units -- 'second', 'minute' and ## 'hour' -- land on the same edges under either reading and none of ## their measured behaviour moves. [yA, mA, dA, hA, nA] = s2c (lo); if (mod (step, 86400) == 0) ## A duration width of whole days is anchored at local midnight on the ## 1st of the month holding the smallest element and stepped from ## there by a fixed span of seconds -- NOT from that element's own ## midnight. The two agree unless a daylight-saving transition falls ## between the 1st and the data, which is why this is only visible in ## a zone such as Pacific/Chatham: seven 24-hour steps from 1 April ## 2024 land on the 7th at 23:00, the 7th being 25 hours long. ## Stepping in elapsed time is what puts days (2) and caldays (2) on ## the same opening edge away from a transition. origin = c2s (yA, mA, 1); elseif (step >= 3600) origin = c2s (yA, mA, dA); elseif (step >= 60) ## The hour and minute the data open in are WALL-clock times, so they ## are built from the components rather than reached by subtracting ## elapsed seconds from the element: a transition inside that hour ## would put the subtraction out by the shift, the same trap as in ## dtBinEdgesGrid. origin = w2s (yA, mA, dA, hA * 3600); else origin = w2s (yA, mA, dA, hA * 3600 + nA * 60); endif left = origin + step * floor ((lo - origin) / step); ## A named unit opens its extra bin only when the largest element sits on ## a unit boundary of the WALL CLOCK. The bins step in elapsed time, so ## a transition shifting the zone by less than one unit -- Lord Howe's ## half hour against an hour bin -- carries the grid off the wall clock ## from there on, and the element then lands mid-unit however exactly the ## elapsed span divides. Away from such a transition the two readings ## agree, LEFT being a unit boundary itself, which is why only a zone ## whose shift is not a whole number of units can tell them apart. if (named && ! dtOnUnitBoundary (hi, step, s2c)) n = max (1, ceil ((hi - left) / step)); else n = bins (hi - left); endif ev = left + (0:n) * step; case {'day', 'week'} [Y, M, D] = s2c (lo); ## Only the named 'week' unit starts on a Sunday. A calendarDuration of ## seven days is a seven-day step on the day-of-month grid below, not a ## week grid, which is why the snap keys off the unit and not off the ## step: calweeks (1) over 10 June 2024 opens on Saturday the 8th. if (strcmp (kind, 'week')) [Y, M, D] = dtAddDays (Y, M, D, -dtWeekdayIndex (Y, M, D)); elseif (step > 1) ## A width of several days lands on a grid anchored at the 1st of the ## month holding the smallest element, not on that element's own date: ## caldays (2) opens on an odd day of the month and caldays (3) on the ## 1st, 4th, 7th and so on. The grid does not restart at a month ## boundary the bins happen to cross -- only the anchor is taken from ## the month. D = 1 + step * floor ((D - 1) / step); endif [Yh, Mh, Dh] = s2c (hi); gap = round ((c2s (Yh, Mh, Dh) - c2s (Y, M, D)) / 86400); n = bins (gap); ## GAP is measured midnight to midnight, so it discards HI's time of ## day. A width that divides GAP exactly then closes on a last edge ## that the largest element has already passed, and that element falls ## outside every bin. A named unit is immune, having opened its extra ## bin regardless. One further step is always enough, the discarded ## remainder being under a day. if (! named) [Yn, Mn, Dn] = dtAddDays (Y, M, D, n * step); if (c2s (Yn, Mn, Dn) < hi) n += 1; endif endif k = (0:n) * step; one = ones (size (k)); [Ye, Me, De] = dtAddDays (Y * one, M * one, D * one, k); ev = c2s (Ye, Me, De); case 'month' [Y, M, D] = s2c (lo); ## 12*Y is divisible by 3, 12, 120 and 1200 alike, so flooring the month ## index onto a multiple of STEP lands a quarter on January/April/July/ ## October and a decade on a year divisible by ten, with no special case. idx = step * floor ((12 * Y + (M - 1)) / step); [Yh, Mh, Dh] = s2c (hi); n = bins ((12 * Yh + (Mh - 1)) - idx); ## The month index discards the day of the month, so a width needs the ## same guard as the day path above. if (! named) kn = idx + n * step; Yn = floor (kn / 12); if (c2s (Yn, kn - Yn * 12 + 1, 1) < hi) n += 1; endif endif k = idx + (0:n) * step; Ye = floor (k / 12); Me = k - Ye * 12 + 1; ev = c2s (Ye, Me, ones (size (k))); case 'cal' ## A mixed calendarDuration width. The grid is anchored on 1 January of ## the smallest element's year when the width carries months, and on the ## 1st of its month when it does not, and steps by the WHOLE width -- ## calendar part and time of day together. The opening edge is the last ## grid point not past that element. Because the anchor is always a 1st, ## no day-of-month clamping can occur, so the k-th point is computed ## directly rather than by stepping k times, and the two agree. [Y, M, D] = s2c (lo); if (step(1) != 0) M = 1; endif ## Mean Gregorian month, for the initial guess only; the walk below is ## what actually places the edge. slen = step(1) * 30.436875 * 86400 + step(2) * 86400 + step(3); k = max (0, floor ((lo - c2s (Y, M, 1)) / slen)); while (k > 0 && dtCalGridPoint (Y, M, step, k, c2s) > lo) k -= 1; endwhile while (dtCalGridPoint (Y, M, step, k + 1, c2s) <= lo) k += 1; endwhile ev = dtCalGridPoint (Y, M, step, k, c2s); while (ev(end) < hi) k += 1; ev(end+1) = dtCalGridPoint (Y, M, step, k, c2s); endwhile if (numel (ev) < 2) ev(2) = dtCalGridPoint (Y, M, step, k + 1, c2s); endif endswitch endfunction ## True when SER's wall clock lands on a boundary of a unit of STEP seconds. ## Read off the components rather than off the time elapsed since midnight, ## which a daylight-saving transition earlier in the day would have carried off ## the grid. Only 'second', 'minute' and 'hour' reach this, every coarser unit ## having its own branch, so STEP always divides a day. function tf = dtOnUnitBoundary (ser, step, s2c) [~, ~, ~, h, m, s] = s2c (ser); tf = (mod (h * 3600 + m * 60 + s, step) == 0); endfunction ## UTC offset in seconds for each element, derived from the WALL CLOCK, so an ## ambiguous time gets the later pass and one inside a gap the shifted one -- ## resolve_local's choices. Zero for an unzoned or leap-second array. Used by ## every wall-clock operation; an instant-based one must take its offset from ## 'serial2components' instead, which reads it off the instant. ## Whether a zone has a zero offset at every instant: no zone at all, UTC, or ## the leap-second timeline, none of which needs the timezone database. function tf = dtIsZeroOffsetZone (TZ) tf = isempty (TZ) || strcmp (TZ, 'UTC') || dtIsLeapZone (TZ); endfunction ## The offset in seconds of a fixed-offset zone, stored as '+HH:MM' or ## '+HH:MM:SS', and empty for any other zone. Such a zone has no transitions, ## so it needs the timezone database no more than UTC does. function off = dtFixedOffset (TZ) off = []; if (isempty (regexp (TZ, '^[+-]\d\d:\d\d(:\d\d)?$', 'once'))) return; endif v = sscanf (TZ(2:end), '%d:%d:%d'); off = v(1) * 3600 + v(2) * 60; if (numel (v) == 3) off += v(3); endif if (TZ(1) == '-') off = -off; endif endfunction ## The name a zone is stored under. A fixed offset from UTC in any accepted ## spelling becomes '+HH:MM', or '+HH:MM:SS' when it has seconds. Bare, either ## sign: '+H', '+HH', '+HMM', '+HHMM', '+HHMMSS', '+HH:MM' and '+HH:MM:SS'. ## After a 'UTC' or 'GMT' prefix, in any letter case: '+H', '+HH', '+H:MM', ## '+HH:MM' and '+HH:MM:SS'. 'Z' and the database's fixed-offset zones are ## stored as offsets too. A 'GMT' prefix and the bare zero-offset aliases warn, ## as MATLAB does. Anything else, an offset out of range included, is returned ## as given, for the timezone database to refuse. All measured against R2024a. function TZ = dtZoneName (TZ, caller) ## The database's own fixed-offset zones are stored as offsets, as MATLAB ## stores them: 'Etc/GMT-3' is '+03:00', its sign reversed by POSIX custom. ## Only the names the database has are read, -14 to +12. if (any (strcmp (TZ, {'Z', 'Etc/GMT', 'Etc/UTC', 'Etc/GMT0', 'Etc/UCT', ... 'Etc/Zulu', 'Etc/Greenwich', 'Etc/Universal'}))) TZ = '+00:00'; return; endif if (any (strcmp (TZ, {'GMT0', 'Zulu', 'UCT', 'Universal', 'Greenwich'}))) dtWarnFixedZone (caller, TZ, '+00:00'); TZ = '+00:00'; return; endif tok = regexp (TZ, '^Etc/GMT([+-])(\d{1,2})$', 'tokens', 'once'); if (! isempty (tok)) h = str2double (tok{2}); if (h == 0) TZ = '+00:00'; elseif (tok{1} == '+' && h <= 12) TZ = sprintf ('-%02d:00', h); elseif (tok{1} == '-' && h <= 14) TZ = sprintf ('+%02d:00', h); endif return; endif given = TZ; prefix = ''; if (numel (TZ) > 4 && any (strcmpi (TZ(1:3), {'UTC', 'GMT'})) ... && any (TZ(4) == '+-')) prefix = upper (TZ(1:3)); TZ = TZ(4:end); endif if (numel (TZ) < 2 || ! any (TZ(1) == '+-')) TZ = given; return; endif body = TZ(2:end); if (! isempty (regexp (body, '^(\d{1,4}|\d{6})$', 'once')) ... && (isempty (prefix) || numel (body) <= 2)) n = numel (body); if (n <= 2) v = [str2double(body), 0, 0]; elseif (n == 3) v = [str2double(body(1)), str2double(body(2:3)), 0]; elseif (n == 4) v = [str2double(body(1:2)), str2double(body(3:4)), 0]; else v = [str2double(body(1:2)), str2double(body(3:4)), ... str2double(body(5:6))]; endif elseif (! isempty (regexp (body, '^\d\d:\d\d(:\d\d)?$', 'once')) ... || (! isempty (prefix) ... && ! isempty (regexp (body, '^\d:\d\d$', 'once')))) v = sscanf (body, '%d:%d:%d')'; v(end+1:3) = 0; else TZ = given; return; endif if (v(1) > 23 || v(2) > 59 || v(3) > 59) TZ = given; return; endif sgn = TZ(1); if (! any (v)) sgn = '+'; endif TZ = sprintf ('%c%02d:%02d', sgn, v(1), v(2)); if (v(3) > 0) TZ = sprintf ('%s:%02d', TZ, v(3)); endif if (strcmp (prefix, 'GMT')) dtWarnFixedZone (caller, given, TZ); endif endfunction ## Warn that the zone named GIVEN, stored as the offset TZ, is a fixed offset ## from UTC under a name that does not say so. function dtWarnFixedZone (caller, given, TZ) warning ('Octave:datetime:nonstandard-time-zone', ... strcat ("%s: '%s' specifies a time zone with a fixed offset", ... " from UTC, %s, which does not follow daylight saving", ... " time."), caller, given, TZ); endfunction function off = dtOffsetOf (Y, M, D, h, m, s, tz) if (dtIsZeroOffsetZone (tz)) off = zeros (size (Y)); return; endif fixed = dtFixedOffset (tz); if (! isempty (fixed)) ## As the database lookup answers: NaN for Not-a-Time, zero where infinite. off = fixed + zeros (size (Y)); off(isnan (Y)) = NaN; off(isinf (Y)) = 0; return; endif off = __datetime__ (Y, M, D, h, m, s, 'ConvertTo', 'zoneoffset', ... 'TimeZone', tz, 'Precision', 'microseconds'); endfunction ## The K-th point of a mixed-calendarDuration bin grid, as a serial. ANCHOR is ## the 1st of month M in year Y; STEP is [months days seconds]. The month part ## is applied first and the day part second, matching calendar addition, and the ## time of day is added last as an instant. No clamping is possible because the ## anchor's day of month is 1. function s = dtCalGridPoint (Y, M, step, k, c2s) tot = Y * 12 + (M - 1) + k * step(1); Yk = floor (tot / 12); [Yk, Mk, Dk] = dtAddDays (Yk, tot - Yk * 12 + 1, 1, k * step(2)); s = c2s (Yk, Mk, Dk) + k * step(3); endfunction ## Edges for a requested bin count over the data range. function ev = dtCountBinEdges (xf, nbins, s2c, c2s, w2s, scope) if (! isnumeric (nbins) || ! isscalar (nbins) || ! isreal (nbins) || ! isfinite (nbins) || nbins < 1 || fix (nbins) != nbins) error (strcat (scope, ": 'NumBins' must be a real, finite, positive,", ... " integer value.")); endif nbins = double (nbins); if (isempty (xf)) ## Deliberately unlike MATLAB, which answers an empty datetime with edges ## taken from the CURRENT CLOCK, so that the same call returns a different ## result every time it is run and no test of it can be written. We anchor ## on the epoch, which is reproducible. See section 14 of the coding style. ev = 0:nbins; else ev = dtBinEdgesGrid (min (xf), max (xf), nbins, s2c, c2s, w2s); endif endfunction ## Bin count for one of the automatic bin-selection rules function nbins = dtMethodBins (xf, lo, hi, method) n = numel (xf); if (n < 2 || isempty (lo) || hi == lo) nbins = 1; return; endif switch (method) case 'sturges' nbins = ceil (1 + log2 (n)); return; case 'sqrt' nbins = ceil (sqrt (n)); return; case 'fd' q = quantile (xf(:), [0.25, 0.75], 1, 5); rawWidth = 2 * (q(2) - q(1)) * n ^ (-1/3); otherwise rawWidth = 3.5 * std (xf(:)) * n ^ (-1/3); endswitch if (! (rawWidth > 0)) nbins = 1; else nbins = max (1, ceil ((hi - lo) / rawWidth)); endif endfunction ## Clamp the outer edges into the requested limits. MATLAB keeps the interior ## grid and cuts the two end bins short, so a 'BinLimits' of two data points ## returns edges that start and end exactly on them. function ev = dtClampEdges (ev, lim) if (! isempty (lim)) ev(1) = max (ev(1), lim(1)); ev(end) = min (ev(end), lim(2)); endif endfunction ## Translate datetime-valued histcounts options into serial seconds, resolving ## any bin count, bin width or unit BinMethod into explicit edges so that every ## path goes through the same placement as 'discretize'. function args = dtHistArgs (args, xv, s2c, c2s, d2s, w2s, scope) xf = xv(isfinite (xv)); ## BinLimits decides the range everything else is derived from lim = []; for k = 1:numel (args) - 1 if (dtIsTextScalar (args{k}) && strcmpi (char (args{k}), 'BinLimits')) v = args{k+1}; if (isa (v, 'datetime')) v = d2s (v); endif if (isnumeric (v) && numel (v) == 2) lim = double (v(:)).'; endif endif endfor if (! isempty (lim)) xf = xf(xf >= lim(1) & xf <= lim(2)); lo = lim(1); hi = lim(2); elseif (! isempty (xf)) lo = min (xf); hi = max (xf); else lo = []; hi = []; endif ## A leading positional argument is a bin count or a set of edges. Either ## way it is resolved here, and the option loop below starts past it. hasSpec = false; k = 1; if (! isempty (args) && ! dtIsTextScalar (args{1})) if (isa (args{1}, 'datetime')) dv = d2s (args{1}); args{1} = dv(:).'; hasSpec = true; k = 2; elseif (isnumeric (args{1}) && isscalar (args{1}) && ! islogical (args{1})) ev = dtClampEdges (dtCountBinEdges (xf, args{1}, s2c, c2s, w2s, ... scope), lim); args = [{'BinEdges', ev}, args(2:end)]; hasSpec = true; k = 3; elseif (isa (args{1}, 'duration')) ## A bin WIDTH is not accepted here, as it is not in MATLAB: this ## position holds a bin count or a set of edges. Refusing it outright ## also keeps a duration from travelling on in the argument list, where ## it would dispatch the numeric call below to 'duration.histcounts' and ## answer with an error from inside that method. error (strcat ("%s: a bin width must be given as 'BinWidth'; the", ... " second argument is a bin count or a vector of bin", ... " edges."), scope); else error (strcat ("%s: the second argument must be a bin count or a", ... " datetime vector of bin edges."), scope); endif endif while (k < numel (args)) if (! dtIsTextScalar (args{k})) k += 2; continue; endif switch (lower (char (args{k}))) case 'numbins' args{k} = 'BinEdges'; args{k+1} = dtClampEdges (dtCountBinEdges (xf, args{k+1}, s2c, c2s, ... w2s, scope), lim); hasSpec = true; case 'binwidth' hasSpec = true; args{k} = 'BinEdges'; args{k+1} = dtClampEdges (dtUnitBinEdges (xf, args{k+1}, s2c, c2s, ... w2s, scope), lim); case 'binedges' hasSpec = true; if (isa (args{k+1}, 'datetime')) dv = d2s (args{k+1}); args{k+1} = dv(:).'; elseif (isnumeric (args{k+1})) error ("%s: '%s' must be a datetime.", scope, char (args{k})); endif case 'binlimits' if (isa (args{k+1}, 'datetime')) dv = d2s (args{k+1}); args{k+1} = dv(:).'; elseif (isnumeric (args{k+1})) error ("%s: '%s' must be a datetime.", scope, char (args{k})); endif case 'binmethod' if (dtIsTextScalar (args{k+1})) name = lower (char (args{k+1})); if (strcmp (name, 'integers')) error (strcat (scope, ": 'integers' is not a valid 'BinMethod'", ... " for a datetime.")); elseif (any (strcmp (name, {'auto', 'scott', 'fd', 'sturges', ... 'sqrt'}))) args{k} = 'BinEdges'; args{k+1} = dtClampEdges (dtCountBinEdges (xf, ... dtMethodBins (xf, lo, hi, name), s2c, c2s, w2s, ... scope), lim); else args{k} = 'BinEdges'; args{k+1} = dtClampEdges (dtUnitBinEdges (xf, name, s2c, c2s, ... w2s, scope), lim); endif endif hasSpec = true; case 'normalization' if (dtIsTextScalar (args{k+1}) && any (strcmpi (char (args{k+1}), {'countdensity', 'pdf'}))) error (strcat (scope, ": '%s' is not a valid 'Normalization' for", ... " a datetime; a density per unit time has no", ... " meaning."), lower (char (args{k+1}))); endif endswitch k += 2; endwhile ## Nothing said how to bin: use the automatic rule, which is a bin count from ## Scott's rule fed through the same grid edges as every other path if (! hasSpec) ev = dtClampEdges (dtCountBinEdges (xf, dtMethodBins (xf, lo, hi, ... 'auto'), ... s2c, c2s, w2s, scope), lim); args = [args, {'BinEdges', ev}]; endif endfunction ## Edge offsets for one rung of the ladder, in whole units. SPANU is the count ## of units between the one holding the smallest value and the one holding the ## largest; the result is each edge's offset, in units, from the one holding the ## smallest value. It is empty when this rung cannot be used, which is the ## signal to try the next one down. ## ## A rung is kept only when its width still needs every bin requested to span ## the data. Rounding a width up to a unit that then leaves bins unused would ## both waste a bin and make the width jump about as the data change. function idx = dtBinGridIndex (spanU, nbins) idx = []; ## The width is a strict round up of the span, so a span that divides ## exactly still widens. Plain ceil () fails every case that divides ## exactly, and recomputing the width from the reach -- which looks like the ## natural thing to do -- is provably the same number for every NBINS >= 2 ## and one unit too small at NBINS == 1, where it is the whole answer. width = floor (spanU / nbins) + 1; ## Centre the excess. The reach the bins must cover is the span plus that ## shift; a rung too coarse to hold a whole unit of reach cannot be used. c = floor ((nbins * width - spanU) / 2); reach = spanU + c; if (reach < 1) return; endif if (ceil (reach / width) != nbins) return; endif idx = -c + (0:nbins) * width; endfunction ## 'pkg test' reads the BISTs from the file it is testing, so a class ## whose suite lives in inst/tests/ reads as carrying none. This block ## answers for it and checks nothing itself. %!test %! ## The suite for this class is inst/tests/datetime.m-tst. pr0m1th3as-datatypes-9450b29/inst/days.m000066400000000000000000000041231527370255000177710ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{D} =} days (@var{X}) ## ## Fixed-time duration in days. ## ## @code{@var{D} = days (@var{X})} returns a @qcode{duration} array representing ## fixed-time duration days equivalent to the values in @var{X}, which must be a ## numeric array. ## ## @code{days} is also available as a method for @qcode{duration} arrays, in ## which case it performs the opposite conversion. ## ## @seealso{duration, years, hours, minutes, seconds, milliseconds, ## duration.days} ## @end deftypefn function out = days (x) if (nargin == 0) x = 1; elseif (! isnumeric (x)) error ("days: input array must be numeric."); elseif (! isreal (x)) error ("days: input array must be real."); endif out = duration (0, 0, 0, double (x) * 86400000, 'Format', 'd'); endfunction %!demo %! ## `days` builds a *fixed-length* day of 24 hours (contrast a calendar day, %! ## whose length varies across DST changes). %! %! days (2.5) %!test %! X = magic (3); %! D = days (X); %! assert_equal (size (D), size (X)); %!test %! D = days ([1, 2, 3]); %! assert_equal (days (D), [1, 2, 3]); %!test %! D = days (int16 (1)); %! assert_equal (days (D), 1); %!test %! D = days (); %! assert_equal (days (D), 1); %!error days ("asd"); %!error days (1+i); pr0m1th3as-datatypes-9450b29/inst/demos/000077500000000000000000000000001527370255000177625ustar00rootroot00000000000000pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.caldays000066400000000000000000000007601527370255000246060ustar00rootroot00000000000000%!demo %! ## As a `calendarDuration` method, `caldays` returns the day part of the span %! ## as a whole number of days. %! %! d = calendarDuration (0, 0, 40) %! caldays (d) %!demo %! ## A month has no fixed number of days, so a span carrying any month at all %! ## cannot be stated in days and `caldays` refuses rather than guess. Ask %! ## `split` instead, which reports each unit on its own. %! %! d = calendarDuration (2, 3, 40) %! [mo, days] = split (d, {'months', 'days'}); %! [mo, days] pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.calendarDuration000066400000000000000000000032351527370255000264450ustar00rootroot00000000000000%!demo %! ## `calendarDuration` represents a span in *calendar* units: years, months, %! ## days, and a time-of-day. A calendar month or year is a real calendar %! ## step, not a fixed number of days, which is what sets it apart from %! ## `duration`. Give years, months and days: %! %! calendarDuration (1, 3, 10) %!demo %! ## Overflowing whole units normalise: 14 months rolls up into 1 year 2 %! ## months. Days are kept separate (a month has no fixed number of days), so %! ## 40 stays 40. %! %! calendarDuration (1, 14, 40) %!demo %! ## The six-argument form adds a time-of-day: years, months, days, hours, %! ## minutes, seconds. %! %! calendarDuration (0, 2, 5, 6, 30, 0) %!demo %! ## The `'Format'` option controls how the SAME span is broken down for %! ## display. A format must contain `m`, `d` and `t`; it may also use `y` %! ## (years), `q` (quarters) and `w` (weeks), listed in the order y q m w d t. %! ## Here one span shown four ways: %! %! calendarDuration (1, 14, 40, 'Format', 'ymdt') %! calendarDuration (1, 14, 40, 'Format', 'mdt') %! calendarDuration (1, 14, 40, 'Format', 'qmwdt') %! calendarDuration (1, 14, 40, 'Format', 'ymwdt') %!demo %! ## A numeric matrix with three (or six) columns builds a whole array at once, %! ## one calendar duration per row. %! %! calendarDuration ([1, 2, 15; 0, 6, 3]) %!demo %! ## Passing an existing array back through the constructor copies it, size and %! ## display format included. Adding `'Format'` re-formats the copy and leaves %! ## the original alone, which is the tidy way to show one span two ways. %! %! calD = calendarDuration (1, 14, 40, 'Format', 'qmwdt') %! calendarDuration (calD, 'Format', 'ymdt') %! calD pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.calmonths000066400000000000000000000004671527370255000251620ustar00rootroot00000000000000%!demo %! ## As a `calendarDuration` *method*, `calmonths` inverts the builder: it %! ## returns the whole span expressed as a number of months (years and months %! ## rolled together). Compare `split`, which instead decomposes the span into %! ## parts. %! %! d = calendarDuration (2, 3, 40) %! calmonths (d) pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.calquarters000066400000000000000000000003051527370255000255070ustar00rootroot00000000000000%!demo %! ## As a `calendarDuration` method, `calquarters` returns the year-and-month %! ## part of the span as a whole number of quarters. %! %! d = calendarDuration (2, 3, 40) %! calquarters (d) pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.calweeks000066400000000000000000000007231527370255000247630ustar00rootroot00000000000000%!demo %! ## As a `calendarDuration` method, `calweeks` returns the day part of the %! ## span as a whole number of weeks. %! %! d = calendarDuration (0, 0, 40) %! calweeks (d) %!demo %! ## Weeks round towards negative infinity, so a negative span rounds away from %! ## zero: -15 days is -3 weeks, not -2. As with `caldays`, a span carrying %! ## any month cannot be stated in weeks at all. %! %! calweeks (calendarDuration (0, 0, [-15, -8, -7, -1, 0, 6, 7, 15])) pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.calyears000066400000000000000000000003131527370255000247630ustar00rootroot00000000000000%!demo %! ## As a `calendarDuration` method, `calyears` returns the number of whole %! ## years in the span (the inverse of the `calyears` builder). %! %! d = calendarDuration (2, 3, 40) %! calyears (d) pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.cellstr000066400000000000000000000007011527370255000246310ustar00rootroot00000000000000%!demo %! ## `cellstr` converts the array into a cell array of its display strings. %! %! d = calmonths ([1, 13, 25]) %! cellstr (d) %!demo %! ## A second argument renders the array in another format for this call only, %! ## leaving the array's own `'Format'` untouched. Unlike the constructor, it %! ## is given on its own and not as a `'Format'` name-value pair. %! %! d = calmonths ([1, 13, 25]) %! cellstr (d, 'mdt') %! cellstr (d, 'yqmwdt') pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.char000066400000000000000000000010111527370255000240710ustar00rootroot00000000000000%!demo %! ## `char` converts the array into a character matrix of its display strings, %! ## one row per element. Rows shorter than the widest are padded on the LEFT, %! ## so the matrix comes out right-justified, the way the array itself is %! ## displayed. %! %! d = calmonths ([1, 13]) %! char (d) %!demo %! ## A second argument renders the same array in another format, without %! ## altering it. Here the months are shown whole rather than split into %! ## years. %! %! d = calmonths ([1, 13]) %! char (d, 'mdt') pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.datevec000066400000000000000000000006721527370255000246030ustar00rootroot00000000000000%!demo %! ## `datevec` returns the span as a numeric row `[Y MO D H MI S]` per element, %! ## handy for feeding the raw components into other calculations. %! %! d = calendarDuration (1, 2, 3, 4, 5, 6) %! datevec (d) %!demo %! ## An element that is not finite has no components to divide between, so all %! ## six take that same infinity, the same answer `split` gives for it. %! %! d = calendarDuration ([1, Inf, NaN], 2, 3) %! datevec (d) pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.eq000066400000000000000000000003261527370255000235710ustar00rootroot00000000000000%!demo %! ## `eq` (`==`) tests calendar durations for equality. Equivalent spans %! ## compare equal even when written with different builders. %! %! calmonths (12) == calyears (1) %! calweeks (2) == caldays (14) pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.ismissing000066400000000000000000000003561527370255000251740ustar00rootroot00000000000000%!demo %! ## For a calendar duration `ismissing` is the same test as `isnan`: a NaN %! ## span is the missing value, so calendar durations plug into the generic %! ## missing-data tools. %! %! d = calmonths ([1, NaN, 3]) %! ismissing (d) pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.isnan000066400000000000000000000002261527370255000242730ustar00rootroot00000000000000%!demo %! ## `isnan` flags the elements built from a NaN component, that is, %! ## Not-a-Number spans. %! %! d = calmonths ([1, NaN, 3]) %! isnan (d) pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.minus000066400000000000000000000002051527370255000243130ustar00rootroot00000000000000%!demo %! ## `minus` (`-`) subtracts one calendar duration from another. %! %! a = calyears (2) + calmonths (2) %! a - calmonths (5) pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.plus000066400000000000000000000003471527370255000241520ustar00rootroot00000000000000%!demo %! ## `plus` (`+`) adds calendar durations. Because the units are calendar %! ## units, the sum normalises on display (here 6 + 9 months rolls into a %! ## year). %! %! a = calyears (1) + calmonths (6) %! a + calmonths (9) pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.split000066400000000000000000000012571527370255000243230ustar00rootroot00000000000000%!demo %! ## `split` decomposes a calendar duration into the whole-unit components you %! ## request, distributing the span across them. (Contrast the single-unit %! ## extractors such as `calmonths`, which each return the span as a *total* in %! ## one unit.) %! %! d = calendarDuration (2, 3, 40) %! [y, mo, days] = split (d, {'years', 'months', 'days'}); %! [y, mo, days] %!demo %! ## Unit names may be abbreviated to any leading part and are matched without %! ## regard to case, and fewer outputs than units may be asked for; you get the %! ## leading ones. %! %! d = calendarDuration (2, 3, 40) %! split (d, 'y') %! [y, q] = split (d, {'Years', 'QUARTERS', 'months'}); %! [y, q] pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.time000066400000000000000000000002631527370255000241220ustar00rootroot00000000000000%!demo %! ## `time` extracts the time-of-day part as a `duration`, discarding the %! ## calendar (year/month/day) part. %! %! d = calendarDuration (0, 1, 2, 6, 30, 0) %! time (d) pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.times000066400000000000000000000003051527370255000243020ustar00rootroot00000000000000%!demo %! ## `times` (`.*`) scales a calendar duration by an integer factor. %! %! q = calmonths (3) %! q * 4 %! %! ## Scaling by an array yields an array of durations. %! caldays (7) .* [1, 2, 3] pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.unique000066400000000000000000000002501527370255000244660ustar00rootroot00000000000000%!demo %! ## `unique` returns the distinct calendar durations in an array (equivalent %! ## spans count as one). %! %! d = calmonths ([1, 13, 1, 25, 13]) %! unique (d) pr0m1th3as-datatypes-9450b29/inst/demos/calendarDuration.vertcat000066400000000000000000000002631527370255000246340ustar00rootroot00000000000000%!demo %! ## Calendar durations concatenate like ordinary arrays; `[a; b]` stacks them %! ## into a single column. %! %! a = calyears ([1; 2]) %! b = calmonths ([3; 4]) %! [a; b] pr0m1th3as-datatypes-9450b29/inst/demos/categorical.addcats000066400000000000000000000006711527370255000235700ustar00rootroot00000000000000%!demo %! ## `addcats` adds new, initially-empty categories without changing any %! ## values, which is handy to reserve labels that may appear later. For an %! ## ordinal array place the new category with `'Before'`/`'After'` so the %! ## order stays sensible. %! %! C = categorical ({'S'; 'M'; 'S'}, {'S', 'M', 'L'}, 'Ordinal', true) %! categories (C) %! %! ## Reserve `XL` just after `L`. %! categories (addcats (C, 'XL', 'After', 'L')) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.categorical000066400000000000000000000015451527370255000244430ustar00rootroot00000000000000%!demo %! ## `categorical` stores discrete labels compactly: each value becomes an %! ## index into a shared list of *categories*. Starting from repeated text %! ## labels ... %! %! sizes = {'M'; 'S'; 'L'; 'M'; 'S'; 'L'; 'M'} %! %! ## ... the categories are detected automatically: the sorted unique values. %! C = categorical (sizes) %! categories (C) %!demo %! ## Supply a *valueset* with `'Ordinal', true` to impose a meaningful order, %! ## so the values compare with `<` and `>`, not only `==`. %! %! raw = {'M'; 'S'; 'L'; 'M'} %! C = categorical (raw, {'S', 'M', 'L'}, 'Ordinal', true) %! %! ## Ordering questions now make sense. %! C >= 'M' %!demo %! ## A third argument renames the categories as you build; empty labels become %! ## the special `` element. %! %! codes = {'1'; '2'; ''; '1'} %! C = categorical (codes, {'1', '2'}, {'low', 'high'}) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.categories000066400000000000000000000005671527370255000243160ustar00rootroot00000000000000%!demo %! ## `categories` lists an array's categories: the full declared label set, not %! ## merely the values that happen to be present. %! %! C = categorical ({'M'; 'S'; 'M'}, {'S', 'M', 'L'}, 'Ordinal', true) %! categories (C) %! %! ## `L` is a declared category (and, being ordinal, keeps its place in the %! ## order) even though no element currently takes that value. pr0m1th3as-datatypes-9450b29/inst/demos/categorical.cellstr000066400000000000000000000003351527370255000236320ustar00rootroot00000000000000%!demo %! ## `cellstr` converts the array back into a cell array of character vectors, %! ## the labels themselves. `` elements become empty strings. %! %! C = categorical ({'M'; 'S'; ''; 'L'}) %! cellstr (C) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.char000066400000000000000000000003511527370255000230750ustar00rootroot00000000000000%!demo %! ## `char` converts the array to a character matrix of its labels, one row per %! ## element (shorter labels are padded with spaces, as for any `char` matrix). %! %! C = categorical ({'small'; 'large'; 'small'}) %! char (C) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.countcats000066400000000000000000000005011527370255000241600ustar00rootroot00000000000000%!demo %! ## `countcats` tallies how many elements fall in each category, in category %! ## order. Categories with no elements are counted as zero, so the result %! ## lines up one-to-one with `categories`. %! %! C = categorical ({'M'; 'S'; 'L'; 'M'; 'S'; 'M'}, {'S', 'M', 'L', 'XL'}) %! categories (C) %! countcats (C) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.double000066400000000000000000000004161527370255000234340ustar00rootroot00000000000000%!demo %! ## `double` returns the integer *category codes*, each value's 1-based index %! ## into the category list. `` elements map to `NaN`. %! %! C = categorical ({'S'; 'M'; 'L'; ''; 'S'}, {'S', 'M', 'L'}, 'Ordinal', true) %! categories (C) %! double (C) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.eq000066400000000000000000000006411527370255000225670ustar00rootroot00000000000000%!demo %! ## `eq` (the `==` operator) compares a categorical array element by element, %! ## either to a single category name or to another categorical array. %! ## Equality works for every categorical, nominal or ordinal. %! %! C = categorical ({'M'; 'S'; 'L'; 'M'}) %! %! ## Which elements equal a given category? %! C == 'M' %! %! ## Element-wise against a second array. %! C == categorical ({'M'; 'M'; 'L'; 'S'}) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.histcounts000066400000000000000000000004631527370255000243670ustar00rootroot00000000000000%!demo %! ## `histcounts` bins a categorical array by category and returns the count in %! ## each. It is the histogram companion of `countcats`, with a second output %! ## naming the bins. %! %! C = categorical ({'M'; 'S'; 'L'; 'M'; 'S'; 'M'}, {'S', 'M', 'L'}) %! [N, cats] = histcounts (C); %! N %! cats pr0m1th3as-datatypes-9450b29/inst/demos/categorical.intersect000066400000000000000000000003451527370255000241630ustar00rootroot00000000000000%!demo %! ## `intersect` returns the categories common to the values of two arrays, as %! ## set arithmetic over categorical data. %! %! A = categorical ({'S'; 'M'; 'L'}) %! B = categorical ({'M'; 'L'; 'XL'}) %! intersect (A, B) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.iscategory000066400000000000000000000004471527370255000243370ustar00rootroot00000000000000%!demo %! ## `iscategory` tests whether names are categories of the array, %! ## independently of whether any element currently takes those values. %! %! C = categorical ({'M'; 'S'}, {'S', 'M', 'L'}) %! iscategory (C, {'M', 'L', 'XL'}) %! %! ## `M` and `L` are declared categories; `XL` is not. pr0m1th3as-datatypes-9450b29/inst/demos/categorical.ismember000066400000000000000000000003471527370255000237700ustar00rootroot00000000000000%!demo %! ## `ismember` tests, element by element, whether each value appears in a %! ## second set given as category names or as another categorical array. %! %! C = categorical ({'M'; 'S'; 'XL'; 'L'}) %! ismember (C, {'S', 'M'}) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.ismissing000066400000000000000000000004711527370255000241700ustar00rootroot00000000000000%!demo %! ## For a categorical array `ismissing` is the same test as `isundefined`: the %! ## missing value *is* the `` element. It exists so categoricals %! ## plug into the generic missing-data tools (`rmmissing`, %! ## `standardizeMissing`). %! %! C = categorical ({'M'; ''; 'L'}) %! ismissing (C) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.isordinal000066400000000000000000000007251527370255000241510ustar00rootroot00000000000000%!demo %! ## `isordinal` reports whether the categories carry an order. A plain array %! ## is *nominal*: its categories are just labels with no ranking ... %! %! N = categorical ({'red'; 'green'; 'blue'}) %! isordinal (N) %! %! ## ... while one built with `'Ordinal', true` is ordered, which is what lets %! ## it be compared with `<` and `>` and sorted meaningfully. %! %! O = categorical ({'lo'; 'hi'; 'mid'}, {'lo', 'mid', 'hi'}, 'Ordinal', true) %! isordinal (O) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.isprotected000066400000000000000000000007171527370255000245130ustar00rootroot00000000000000%!demo %! ## `isprotected` reports whether the category list is locked. A nominal %! ## array is *unprotected*: assigning a new label simply extends its %! ## categories ... %! %! N = categorical ({'M'; 'S'}) %! isprotected (N) %! %! ## ... whereas ordinal arrays (and ones built with `'Protected', true`) are %! ## protected, so only their declared categories may be used. %! %! O = categorical ({'M'; 'S'}, {'S', 'M', 'L'}, 'Ordinal', true) %! isprotected (O) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.issorted000066400000000000000000000005461527370255000240220ustar00rootroot00000000000000%!demo %! ## `issorted` checks whether an ordinal array is already in non-decreasing %! ## category order, a cheap test to skip a needless `sort`. %! %! C = categorical ({'S'; 'M'; 'L'}, {'S', 'M', 'L'}, 'Ordinal', true) %! issorted (C) %! %! ## The same categories out of order: %! issorted (categorical ({'M'; 'S'; 'L'}, {'S', 'M', 'L'}, 'Ordinal', true)) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.isundefined000066400000000000000000000005601527370255000244570ustar00rootroot00000000000000%!demo %! ## `isundefined` flags the elements that belong to no category, the %! ## `` values that stand in for missing data. Empty labels become %! ## undefined at construction time. %! %! C = categorical ({'M'; ''; 'L'; 'M'}) %! isundefined (C) %! %! ## Dropping a category also undefines every element that used it. %! isundefined (removecats (C, 'M')) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.lt000066400000000000000000000007331527370255000226030ustar00rootroot00000000000000%!demo %! ## `lt` (the `<` operator) ranks values by category order, so it needs an %! ## *ordinal* array. It compares against a category name or another ordinal %! ## array. %! %! C = categorical ({'M'; 'S'; 'L'; 'M'}, {'S', 'M', 'L'}, 'Ordinal', true) %! %! ## Which sizes are strictly smaller than `L`? %! C < 'L' %! %! ## `>`, `<=` and `>=` behave the same way; the same test on a nominal array %! ## would raise an error, because unordered categories cannot be ranked. pr0m1th3as-datatypes-9450b29/inst/demos/categorical.max000066400000000000000000000003341527370255000227460ustar00rootroot00000000000000%!demo %! ## `max` returns the highest-ranked category present in an ordinal array, %! ## by category order rather than by label. %! %! C = categorical ({'M'; 'L'; 'S'; 'M'}, {'S', 'M', 'L'}, 'Ordinal', true) %! max (C) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.median000066400000000000000000000003611527370255000234160ustar00rootroot00000000000000%!demo %! ## `median` returns the middle category by rank. It is defined for ordinal %! ## arrays, where the categories have a genuine order. %! %! C = categorical ({'S'; 'M'; 'L'; 'M'; 'L'}, {'S', 'M', 'L'}, 'Ordinal', true) %! median (C) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.mergecats000066400000000000000000000005161527370255000241350ustar00rootroot00000000000000%!demo %! ## `mergecats` collapses several categories into one, relabelling every %! ## element that used them, which is a quick way to coarsen a classification. %! %! C = categorical ({'S'; 'M'; 'L'; 'S'; 'M'}, {'S', 'M', 'L'}) %! %! ## Fold `S` and `M` together into a single `small` category. %! mergecats (C, {'S', 'M'}, 'small') pr0m1th3as-datatypes-9450b29/inst/demos/categorical.min000066400000000000000000000005431527370255000227460ustar00rootroot00000000000000%!demo %! ## `min` returns the lowest-ranked category present in an ordinal array. %! ## Ranking follows the category order, not the alphabetical label. %! %! C = categorical ({'M'; 'L'; 'S'; 'M'}, {'S', 'M', 'L'}, 'Ordinal', true) %! min (C) %! %! ## A second output gives the index of that element (as for numeric `min`). %! [lo, idx] = min (C); %! idx pr0m1th3as-datatypes-9450b29/inst/demos/categorical.mode000066400000000000000000000004151527370255000231050ustar00rootroot00000000000000%!demo %! ## `mode` returns the most frequent category. It is the one central-tendency %! ## statistic that also works for *nominal* arrays, since it counts %! ## occurrences rather than ranking them. %! %! C = categorical ({'M'; 'S'; 'M'; 'L'; 'M'; 'S'}) %! mode (C) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.removecats000066400000000000000000000006011527370255000243260ustar00rootroot00000000000000%!demo %! ## `removecats` drops categories from an array. %! %! C = categorical ({'S'; 'M'; 'S'}, {'S', 'M', 'L'}) %! %! ## Naming a category removes it and turns its elements into ``. %! removecats (C, 'M') %! %! ## With no list only the *unused* categories go (values unchanged); here the %! ## unused `L` disappears from the category list. %! categories (removecats (C)) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.renamecats000066400000000000000000000006021527370255000243010ustar00rootroot00000000000000%!demo %! ## `renamecats` changes category *names* while leaving the values in place. %! %! C = categorical ({'S'; 'M'; 'L'; 'S'}, {'S', 'M', 'L'}) %! %! ## Give a full set of new names (in category order) to rename them all ... %! renamecats (C, {'Small', 'Medium', 'Large'}) %! %! ## ... or rename only specific categories by old-name to new-name. %! renamecats (C, {'S'}, {'Small'}) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.reordercats000066400000000000000000000006621527370255000245020ustar00rootroot00000000000000%!demo %! ## `reordercats` changes the order of the categories. For an *ordinal* array %! ## this redefines the ranking used by `<`, `>`, `sort`, `min` and `max`. %! %! C = categorical ({'M'; 'S'; 'L'}, {'S', 'M', 'L'}, 'Ordinal', true) %! categories (C) %! %! ## Reverse the order so that `S` now ranks highest. %! B = reordercats (C, {'L', 'M', 'S'}); %! categories (B) %! %! ## The comparison follows the new ranking. %! B >= 'M' pr0m1th3as-datatypes-9450b29/inst/demos/categorical.setcats000066400000000000000000000006101527370255000236240ustar00rootroot00000000000000%!demo %! ## `setcats` replaces the whole category list with the one you give: values %! ## that fall outside the new set become ``, and the categories end %! ## up as exactly the names you specified (in that order). %! %! C = categorical ({'S'; 'M'; 'L'; 'M'}, {'S', 'M', 'L'}) %! %! ## Keep only `S` and `M`; the `L` element is dropped to . %! setcats (C, {'S', 'M'}) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.setdiff000066400000000000000000000002771527370255000236130ustar00rootroot00000000000000%!demo %! ## `setdiff` returns the values present in the first array but not the %! ## second. %! %! A = categorical ({'S'; 'M'; 'L'}) %! B = categorical ({'M'; 'L'; 'XL'}) %! setdiff (A, B) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.setxor000066400000000000000000000003121527370255000235010ustar00rootroot00000000000000%!demo %! ## `setxor` returns the symmetric difference: the values in exactly one of %! ## the two arrays. %! %! A = categorical ({'S'; 'M'; 'L'}) %! B = categorical ({'M'; 'L'; 'XL'}) %! setxor (A, B) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.sort000066400000000000000000000004101527370255000231430ustar00rootroot00000000000000%!demo %! ## `sort` orders an ordinal array by its category *ranking*, not %! ## alphabetically by label. %! %! C = categorical ({'L'; 'S'; 'M'; 'S'; 'L'}, {'S', 'M', 'L'}, 'Ordinal', true) %! sort (C) %! %! ## Any `` values sort to the end by default. pr0m1th3as-datatypes-9450b29/inst/demos/categorical.summary000066400000000000000000000003561527370255000236620ustar00rootroot00000000000000%!demo %! ## `summary` tabulates the count of each category, including the %! ## `` element, giving a quick frequency overview of the array. %! %! C = categorical ({'M'; 'S'; 'L'; 'M'; ''; 'M'}, {'S', 'M', 'L'}) %! summary (C) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.union000066400000000000000000000002751527370255000233150ustar00rootroot00000000000000%!demo %! ## `union` returns the distinct values present in either array, the set %! ## union. %! %! A = categorical ({'S'; 'M'; 'L'}) %! B = categorical ({'M'; 'L'; 'XL'}) %! union (A, B) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.unique000066400000000000000000000004261527370255000234710ustar00rootroot00000000000000%!demo %! ## `unique` returns the distinct values *present* in the array, as a %! ## categorical array. (Contrast `categories`, which lists the full declared %! ## label set even when some categories are unused.) %! %! C = categorical ({'M'; 'S'; 'M'; 'L'; 'S'}) %! unique (C) pr0m1th3as-datatypes-9450b29/inst/demos/categorical.vertcat000066400000000000000000000004341527370255000236320ustar00rootroot00000000000000%!demo %! ## Concatenating categorical arrays *unions* their category lists, so the %! ## result can hold categories that neither operand had alone. `[A; B]` %! ## stacks them. %! %! A = categorical ({'S'; 'M'}) %! B = categorical ({'L'; 'XL'}) %! C = [A; B]; %! C %! categories (C) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.between000066400000000000000000000002651527370255000231340ustar00rootroot00000000000000%!demo %! ## `between` gives the calendar difference between two datetimes, broken into %! ## years, months and days. %! %! between (datetime (2024, 1, 15), datetime (2025, 3, 20)) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.caldiff000066400000000000000000000003351527370255000230710ustar00rootroot00000000000000%!demo %! ## `caldiff` returns the successive differences as *calendar* amounts, here %! ## one month between each element, even though the months differ in length. %! %! t = datetime (2024, [1, 2, 3], 15) %! caldiff (t) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.char000066400000000000000000000003151527370255000224140ustar00rootroot00000000000000%!demo %! ## `char` renders datetimes as a character array, using the display format or %! ## a format you pass. %! %! char (datetime (2024, 3, 9, 14, 30, 0)) %! char (datetime (2024, 3, 9), 'yyyy-MM-dd') pr0m1th3as-datatypes-9450b29/inst/demos/datetime.colon000066400000000000000000000005761527370255000226220ustar00rootroot00000000000000%!demo %! ## The colon operator builds a range of datetimes; the step is a `duration` %! ## or `calendarDuration`. A `caldays` step gives one row per calendar day. %! %! datetime (2024, 1, 1) : caldays (1) : datetime (2024, 1, 4) %!demo %! ## Use a time-based step for a finer range, here every six hours. %! %! datetime (2024, 1, 1) : hours (6) : datetime (2024, 1, 1, 18, 0, 0) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.convertTo000066400000000000000000000011571527370255000234670ustar00rootroot00000000000000%!demo %! ## `convertTo` turns a datetime into a numeric date/time representation; one %! ## call selects the target system: the MATLAB serial date number, Unix %! ## `posixtime`, the Excel serial date, or a `yyyymmdd` integer. %! %! t = datetime (2024, 3, 9, 6, 0, 0) %! convertTo (t, 'datenum') %! convertTo (t, 'posixtime') %! convertTo (t, 'excel') %! convertTo (t, 'yyyymmdd') %!demo %! ## The integer epoch systems return *exact* `int64`/`uint64` values, for %! ## example `'epochtime'` (seconds since 1970) and `'ntp'`. %! %! t = datetime (2024, 3, 9, 6, 0, 0) %! convertTo (t, 'epochtime') %! convertTo (t, 'ntp') pr0m1th3as-datatypes-9450b29/inst/demos/datetime.datenum000066400000000000000000000003071527370255000231350ustar00rootroot00000000000000%!demo %! ## `datenum` returns the serial date number (days since the start of the year %! ## 0), with the time of day in the fractional part. %! %! t = datetime (2024, 3, 9, 6, 0, 0) %! datenum (t) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.dateshift000066400000000000000000000006511527370255000234550ustar00rootroot00000000000000%!demo %! ## `dateshift` snaps a datetime to a calendar boundary, the start or end of a %! ## unit such as the month. %! %! t = datetime (2024, 3, 15, 10, 30, 0) %! dateshift (t, 'start', 'month') %! dateshift (t, 'end', 'month') %!demo %! ## The `'dayofweek'` mode moves to the next occurrence of a weekday, keeping %! ## the time of day. %! %! t = datetime (2024, 3, 15, 10, 30, 0) %! dateshift (t, 'dayofweek', 'Monday') pr0m1th3as-datatypes-9450b29/inst/demos/datetime.datestr000066400000000000000000000006071527370255000231510ustar00rootroot00000000000000%!demo %! ## `datestr` produces text using the *legacy* `datestr` format codes. Note %! ## that here `mm` means months and `MM` means minutes, the reverse of the %! ## modern Unicode (LDML) codes used by `char` and the `'Format'` property, %! ## which are the preferred way to format a datetime. %! %! t = datetime (2024, 3, 9, 6, 0, 0) %! datestr (t) %! datestr (t, 'yyyy-mm-dd HH:MM:SS') pr0m1th3as-datatypes-9450b29/inst/demos/datetime.datetime000066400000000000000000000035741527370255000233050ustar00rootroot00000000000000%!demo %! ## A `datetime` is a specific point in time. The simplest form gives the %! ## year, month and day; add hour, minute and second for a time of day. %! %! datetime (2024, 3, 9) %! datetime (2024, 3, 9, 14, 30, 15) %!demo %! ## Text is parsed automatically for the common ISO and `dd-MMM-yyyy` layouts. %! %! datetime ('2024-03-09') %! datetime ('09-Mar-2024 14:30:15') %!demo %! ## A single numeric row is read as a date vector `[Y M D h m s]`, and a range %! ## of datetimes is built with the colon operator and a step, here one %! ## calendar day with `caldays`. %! %! datetime ([2024, 3, 9, 14, 30, 15]) %! datetime (2024, 1, 1) : caldays (1) : datetime (2024, 1, 4) %!demo %! ## The defining feature of `datetime` is **time-zone awareness**. Attach a %! ## zone with `'TimeZone'` and the value becomes a true instant: `tzoffset` %! ## reports its offset from UTC, and arithmetic across a daylight-saving %! ## change is handled for you. %! %! t = datetime (2024, 3, 9, 14, 30, 0, 'TimeZone', 'America/New_York') %! tzoffset (t) %!demo %! ## The `'Format'` property controls display only, never the stored value. %! ## Here the SAME instant is shown several ways by setting different Unicode %! ## (LDML) field patterns: `y` year, `M` month, `d` day, `H` hour, `e` %! ## weekday. %! %! datetime (2024, 3, 9, 14, 30, 15, 'Format', 'yyyy-MM-dd HH:mm:ss') %! datetime (2024, 3, 9, 14, 30, 15, 'Format', 'MMMM d, yyyy') %! datetime (2024, 3, 9, 14, 30, 15, 'Format', 'eeee') %!demo %! ## By default the display omits sub-seconds even though they are stored; ask %! ## for them explicitly with fractional-second (`S`) codes. %! %! datetime (2024, 3, 9, 14, 30, 15.5) %! datetime (2024, 3, 9, 14, 30, 15.5, 'Format', 'HH:mm:ss.SSS') %!demo %! ## A missing datetime is `NaT` (Not-a-Time), the datetime analogue of `NaN`; %! ## a `NaN` component produces one. %! %! NaT %! datetime (2024, 1, [1, NaN, 3]) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.datevec000066400000000000000000000002571527370255000231170ustar00rootroot00000000000000%!demo %! ## `datevec` returns the six date-vector components `[Y M D h m s]`, one row %! ## per element of the array. %! %! t = datetime (2024, 3, 9, 6, 0, 0) %! datevec (t) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.day000066400000000000000000000004371527370255000222610ustar00rootroot00000000000000%!demo %! ## `day` returns the day of the month by default. Pass a type to get instead %! ## the day of the week (Sunday is 1), the day of the year, or the weekday %! ## name. %! %! t = datetime (2024, 3, 9) %! day (t) %! day (t, 'dayofweek') %! day (t, 'dayofyear') %! day (t, 'name') pr0m1th3as-datatypes-9450b29/inst/demos/datetime.diff000066400000000000000000000002361527370255000224110ustar00rootroot00000000000000%!demo %! ## `diff` returns the elapsed time between successive elements as a %! ## `duration` array. %! %! t = datetime (2024, 1, [1, 3, 8, 15]) %! diff (t) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.eq000066400000000000000000000003411527370255000221030ustar00rootroot00000000000000%!demo %! ## Datetimes compare with `==` element-wise. Comparison is by absolute %! ## instant, so zoned datetimes compare correctly even across different zones. %! %! datetime (2024, 3, 9) == datetime (2024, 3, [8, 9, 10]) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.exceltime000066400000000000000000000003451527370255000234610ustar00rootroot00000000000000%!demo %! ## `exceltime` returns an Excel serial date number. The `'1900'` system is %! ## the default; pass `'1904'` for the Excel 1904 system. %! %! t = datetime (2024, 3, 9, 6, 0, 0) %! exceltime (t) %! exceltime (t, '1904') pr0m1th3as-datatypes-9450b29/inst/demos/datetime.hms000066400000000000000000000002461527370255000222710ustar00rootroot00000000000000%!demo %! ## `hms` splits each datetime into its hour, minute and second components. %! %! t = datetime (2024, 3, 9, 14, 30, 45) %! [h, m, s] = hms (t); %! [h, m, s] pr0m1th3as-datatypes-9450b29/inst/demos/datetime.interp1000066400000000000000000000006231527370255000230630ustar00rootroot00000000000000%!demo %! ## `interp1` interpolates values sampled at datetimes. Here numeric readings %! ## taken on 1 and 5 January are interpolated at 3 January. %! %! t = datetime (2024, 1, [1, 5]) %! v = [10, 50]; %! interp1 (t, v, datetime (2024, 1, 3)) %!demo %! ## When the sampled *values* are datetimes, the interpolated result is a %! ## datetime. %! %! interp1 ([0, 10], datetime (2024, 1, [1, 11]), 2.5) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.isbetween000066400000000000000000000002751527370255000234710ustar00rootroot00000000000000%!demo %! ## `isbetween` tests whether each element falls within a closed interval. %! %! t = datetime (2024, 3, [1, 9, 20]) %! isbetween (t, datetime (2024, 3, 5), datetime (2024, 3, 15)) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.isdst000066400000000000000000000003561527370255000226320ustar00rootroot00000000000000%!demo %! ## `isdst` is true where an element falls in daylight saving time. For a New %! ## York datetime that is `false` in January and `true` in July. %! %! t = datetime (2024, [1, 7], 15, 'TimeZone', 'America/New_York') %! isdst (t) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.ismember000066400000000000000000000002371527370255000233050ustar00rootroot00000000000000%!demo %! ## `ismember` tests which datetimes also appear in a second set. %! %! ismember (datetime (2024, 1, [1, 5, 9]), datetime (2024, 1, [1, 2, 3, 4, 5])) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.isnat000066400000000000000000000002141527370255000226130ustar00rootroot00000000000000%!demo %! ## `isnat` is true where an element is `NaT`, a missing datetime. %! %! isnat ([datetime(2024, 1, 1), NaT, datetime(2024, 1, 3)]) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.isregular000066400000000000000000000011361527370255000234760ustar00rootroot00000000000000%!demo %! ## `isregular` tests whether a datetime vector is equally spaced. A daily %! ## series is regular; drop a day and it is not. %! %! isregular (datetime (2024, 1, 1) : caldays (1) : datetime (2024, 1, 5)) %! isregular (datetime (2024, 1, [1, 2, 5])) %!demo %! ## A monthly series is *not* equally spaced in absolute time, since months %! ## differ in length, but it IS regular with respect to the `'months'` unit, %! ## and the second output returns the common step. %! %! m = datetime (2024, 1, 1) : calmonths (1) : datetime (2024, 5, 1) %! isregular (m) %! [tf, step] = isregular (m, 'months') pr0m1th3as-datatypes-9450b29/inst/demos/datetime.isweekend000066400000000000000000000003331527370255000234550ustar00rootroot00000000000000%!demo %! ## `isweekend` is true on Saturdays and Sundays. Here a run from Friday to %! ## Monday flags only the two middle days. %! %! t = datetime (2024, 3, 8) : caldays (1) : datetime (2024, 3, 11) %! isweekend (t) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.juliandate000066400000000000000000000005001527370255000236130ustar00rootroot00000000000000%!demo %! ## `juliandate` returns the Julian date, the days since noon UTC on 1 January %! ## 4713 BCE. Unlike `datenum`, it is based on the absolute UTC instant. %! ## Pass `'modifiedjuliandate'` for the modified form. %! %! t = datetime (2024, 3, 9, 6, 0, 0) %! juliandate (t) %! juliandate (t, 'modifiedjuliandate') pr0m1th3as-datatypes-9450b29/inst/demos/datetime.max000066400000000000000000000001471527370255000222670ustar00rootroot00000000000000%!demo %! ## `max` returns the latest datetime. %! %! t = datetime (2024, 3, [9, 2, 20, 5]) %! max (t) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.mean000066400000000000000000000001701527370255000224160ustar00rootroot00000000000000%!demo %! ## `mean` returns the midpoint instant of the datetimes. %! %! t = datetime (2024, 3, [1, 9, 20]) %! mean (t) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.min000066400000000000000000000002341527370255000222620ustar00rootroot00000000000000%!demo %! ## `min` returns the earliest datetime; a second output gives its index. %! %! t = datetime (2024, 3, [9, 2, 20, 5]) %! [earliest, idx] = min (t) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.minus000066400000000000000000000007611527370255000226370ustar00rootroot00000000000000%!demo %! ## Subtracting two datetimes gives the elapsed time between them as a %! ## `duration`. %! %! datetime (2024, 3, 15) - datetime (2024, 3, 9) %! datetime (2024, 3, 9, 12, 0, 0) - datetime (2024, 3, 9, 6, 0, 0) %!demo %! ## Subtracting a `duration` shifts by a fixed amount, while subtracting a %! ## `calendarDuration` is calendar-aware: one month before 31 March clamps to %! ## the last day of February. %! %! datetime (2024, 3, 9) - days (3) %! datetime (2024, 3, 31) - calmonths (1) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.month000066400000000000000000000002541527370255000226260ustar00rootroot00000000000000%!demo %! ## `month` returns the month number, or its full or abbreviated name. %! %! t = datetime (2024, 3, 9) %! month (t) %! month (t, 'name') %! month (t, 'shortname') pr0m1th3as-datatypes-9450b29/inst/demos/datetime.posixtime000066400000000000000000000003031527370255000235150ustar00rootroot00000000000000%!demo %! ## `posixtime` returns seconds since the Unix epoch (1970-01-01 UTC). A %! ## datetime without a time zone is treated as UTC. %! %! t = datetime (2024, 3, 9, 6, 0, 0) %! posixtime (t) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.quarter000066400000000000000000000002151527370255000231610ustar00rootroot00000000000000%!demo %! ## `quarter` returns the quarter of the year (1 to 4) for each element. %! %! t = datetime (2024, [2, 5, 8, 11], 1) %! quarter (t) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.second000066400000000000000000000003351527370255000227540ustar00rootroot00000000000000%!demo %! ## `second` returns the seconds of the minute by default, or the seconds %! ## elapsed since midnight with `'secondofday'`. %! %! t = datetime (2024, 3, 9, 14, 30, 45) %! second (t) %! second (t, 'secondofday') pr0m1th3as-datatypes-9450b29/inst/demos/datetime.sort000066400000000000000000000001561527370255000224710ustar00rootroot00000000000000%!demo %! ## `sort` orders datetimes chronologically. %! %! t = datetime (2024, 3, [9, 2, 20, 5]) %! sort (t) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.std000066400000000000000000000002061527370255000222700ustar00rootroot00000000000000%!demo %! ## `std` measures the spread of the datetimes and returns a `duration`. %! %! t = datetime (2024, 3, [1, 9, 20]) %! std (t) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.string000066400000000000000000000003771527370255000230150ustar00rootroot00000000000000%!demo %! ## `string` renders a datetime as a `string` array, using its display format %! ## or a format you pass, and turns a `NaT` into a missing string. %! %! t = datetime (2024, 3, 9, 6, 0, 0) %! string (t) %! string (t, 'yyyy-MM-dd') %! string (NaT) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.timeofday000066400000000000000000000002171527370255000234610ustar00rootroot00000000000000%!demo %! ## `timeofday` returns the elapsed time since midnight as a `duration`. %! %! t = datetime (2024, 3, 9, 14, 30, 15) %! timeofday (t) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.tzoffset000066400000000000000000000006031527370255000233430ustar00rootroot00000000000000%!demo %! ## `tzoffset` gives each element's offset from UTC as a `duration`. Because %! ## the offset depends on daylight saving time, the same zone reports a %! ## different value in winter and in summer. %! %! winter = datetime (2024, 1, 15, 'TimeZone', 'America/New_York') %! tzoffset (winter) %! summer = datetime (2024, 7, 15, 'TimeZone', 'America/New_York') %! tzoffset (summer) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.unique000066400000000000000000000002021527370255000230000ustar00rootroot00000000000000%!demo %! ## `unique` returns the distinct datetimes in sorted order. %! %! t = datetime (2024, 1, [3, 1, 3, 2, 1]) %! unique (t) pr0m1th3as-datatypes-9450b29/inst/demos/datetime.week000066400000000000000000000006121527370255000224320ustar00rootroot00000000000000%!demo %! ## `week` returns the week of the year, counted Sunday to Saturday with the %! ## week that contains January 1 as week 1. (This is *not* the ISO 8601 %! ## rule.) %! %! d = datetime (2024, 1, 1) : caldays (7) : datetime (2024, 2, 12) %! week (d) %!demo %! ## Pass `'weekofmonth'` for the week within the month instead of the year. %! %! week (datetime (2024, 3, 15), 'weekofmonth') pr0m1th3as-datatypes-9450b29/inst/demos/datetime.ymd000066400000000000000000000002771527370255000222770ustar00rootroot00000000000000%!demo %! ## `ymd` splits each datetime into its year, month and day as three separate %! ## numeric outputs. %! %! t = datetime (2024, 3, 9, 14, 30, 45) %! [y, m, d] = ymd (t); %! [y, m, d] pr0m1th3as-datatypes-9450b29/inst/demos/datetime.yyyymmdd000066400000000000000000000002651527370255000233700ustar00rootroot00000000000000%!demo %! ## `yyyymmdd` packs the date into one number as `year*10000 + month*100 + %! ## day`; the time of day is ignored. %! %! t = datetime (2024, 3, 9, 6, 0, 0) %! yyyymmdd (t) pr0m1th3as-datatypes-9450b29/inst/demos/dictionary.dictionary000066400000000000000000000046711527370255000242260ustar00rootroot00000000000000%!demo %! ## A dictionary maps unique keys to values, of one key type and one value %! ## type. In Octave both `'a'` and `"a"` are character vectors, which a %! ## dictionary stores as strings, one per row. A cell array is a key type %! ## of its own, each cell one key, so text keys are given as a `string` %! ## array. %! %! fruit = dictionary (string ({'apple', 'pear', 'fig'}), [3, 5, 2]) %! types (fruit) %! types (dictionary ('apple', 3)) %! types (dictionary ({'apple', 'pear'}, [3, 5])) %!demo %! ## A dictionary is a scalar object, whatever it holds: `numel` and `size` %! ## describe the object, and `numEntries` counts the entries. %! %! fruit = dictionary (string ({'apple', 'pear', 'fig'}), [3, 5, 2]); %! numel (fruit) %! numEntries (fruit) %!demo %! ## Entries keep the order their keys were first added. Replacing a value %! ## keeps the key in place, and a key repeated in the constructor keeps its %! ## first position and takes its last value. %! %! d = dictionary (string ({'b', 'a', 'b'}), [1, 2, 3]) %! d('b') = 10 %!demo %! ## Index with keys to read values, assign to add or replace entries, and %! ## assign `[]` to remove them. Keys are converted to the key type, so a %! ## dictionary of `double` keys accepts integers and logical values. %! %! sq = dictionary ([1, 2, 3], [1, 4, 9]) %! sq([3, 1]) %! sq(int8 (4)) = 16; %! sq(2) = [] %!demo %! ## Several pairs of keys and values can be given at once: the first pair %! ## sets the types, and later pairs are converted to them. Here the dates %! ## of a dictionary of `datetime` keys are also given as text. %! %! marks = dictionary (datetime (2024, 3, 1), 'start', '2024-03-15', 'middle') %!demo %! ## Values of the class `cell` hold anything, one cell per key, and `{}` %! ## reaches their contents. %! %! cfg = dictionary (string ({'size', 'name', 'when'}), ... %! {[640, 480], 'demo', duration(1, 30, 0)}) %! cfg{'size'} %! cfg{'name'} = 'renamed'; %! cfg('name') %!demo %! ## A value can be any object, a `table` of any size or another dictionary %! ## included, and indexing continues into it. %! %! shelf = dictionary (string ({'north', 'south'}), ... %! {table([1; 2], {'pen'; 'ink'}, 'VariableNames', ... %! {'Qty', 'Item'}), ... %! table(5, {'tape'}, 'VariableNames', {'Qty', 'Item'})}); %! shelf{'north'} %! nested = dictionary ('inner', dictionary (string ({'x', 'y'}), [1, 2])) %! nested('inner')('y') pr0m1th3as-datatypes-9450b29/inst/demos/dictionary.entries000066400000000000000000000004521527370255000235230ustar00rootroot00000000000000%!demo %! ## `entries` returns the keys and values together: a table by default, a %! ## structure array, or a cell array of two columns. %! %! d = dictionary (categorical ({'red', 'green'}), [0.8, 0.3]) %! entries (d) %! s = entries (d, 'struct'); %! s(2).Key %! s(2).Value %! entries (d, 'cell') pr0m1th3as-datatypes-9450b29/inst/demos/dictionary.insert000066400000000000000000000005361527370255000233610ustar00rootroot00000000000000%!demo %! ## `insert` adds entries as assignment does. With `'Overwrite', false` %! ## the values of keys already present are kept, and only new keys are %! ## added. %! %! stock = dictionary (string ({'pen', 'ink'}), [12, 4]) %! stock = insert (stock, {'ink', 'tape'}, [9, 7]) %! stock = insert (stock, {'pen', 'glue'}, [0, 3], 'Overwrite', false) pr0m1th3as-datatypes-9450b29/inst/demos/dictionary.isConfigured000066400000000000000000000004431527370255000244730ustar00rootroot00000000000000%!demo %! ## A dictionary with no entries is configured when it has key and value %! ## types; the first entries added configure an unconfigured one. %! %! u = dictionary () %! isConfigured (u) %! u('first') = 1 %! isConfigured (u) %! isConfigured (configureDictionary ('double', 'string')) pr0m1th3as-datatypes-9450b29/inst/demos/dictionary.isKey000066400000000000000000000004561527370255000231420ustar00rootroot00000000000000%!demo %! ## `isKey` answers for each element of an array of keys, after converting %! ## them to the key type: a number asks for its text in a dictionary of %! ## `string` keys. %! %! d = dictionary (string ({'1', '2', 'three'}), [1, 2, 3]) %! isKey (d, {'three', 'four'; '1', 'one'}) %! isKey (d, 2) pr0m1th3as-datatypes-9450b29/inst/demos/dictionary.keys000066400000000000000000000003521527370255000230240ustar00rootroot00000000000000%!demo %! ## `keys` returns the keys as a column of the key type, in the order they %! ## were added, or each in a cell of its own. %! %! d = dictionary (datetime (2024, 1, [15, 3, 9]), [120, 95, 101]) %! keys (d) %! keys (d, 'cell') pr0m1th3as-datatypes-9450b29/inst/demos/dictionary.lookup000066400000000000000000000005051527370255000233620ustar00rootroot00000000000000%!demo %! ## `lookup` returns the values of keys as indexing does, and a key not in %! ## the dictionary is an error unless a `'FallbackValue'` is given. %! %! price = dictionary (string ({'tea', 'coffee'}), [2.5, 3.2]) %! lookup (price, {'coffee', 'tea'}) %! lookup (price, {'tea', 'cocoa', 'coffee'}, 'FallbackValue', NaN) pr0m1th3as-datatypes-9450b29/inst/demos/dictionary.numEntries000066400000000000000000000003111527370255000241750ustar00rootroot00000000000000%!demo %! ## `numEntries` counts the entries, where `numel` is always 1. %! %! d = dictionary (hours ([1, 2, 6]), {'short', 'medium', 'long'}) %! numEntries (d) %! d(hours (2)) = []; %! numEntries (d) pr0m1th3as-datatypes-9450b29/inst/demos/dictionary.remove000066400000000000000000000004061527370255000233460ustar00rootroot00000000000000%!demo %! ## `remove` removes the entries of the keys given, ignoring keys not in %! ## the dictionary. A key removed and added again goes after the others. %! %! d = dictionary (string ({'a', 'b', 'c'}), [1, 2, 3]) %! d = remove (d, {'b', 'z'}) %! d('b') = 20 pr0m1th3as-datatypes-9450b29/inst/demos/dictionary.types000066400000000000000000000004271527370255000232200ustar00rootroot00000000000000%!demo %! ## `types` returns the key and value types as string scalars, missing %! ## strings while a dictionary is unconfigured. %! %! d = dictionary (categorical ({'low', 'high'}), [0.1, 0.9]) %! [keyType, valueType] = types (d) %! [keyType, valueType] = types (dictionary ()) pr0m1th3as-datatypes-9450b29/inst/demos/dictionary.values000066400000000000000000000006071527370255000233530ustar00rootroot00000000000000%!demo %! ## `values` returns the values as a column of the value type, in the order %! ## of their keys. Function handles cannot be joined into an array, so a %! ## dictionary of them gives its values as a cell array. %! %! d = dictionary (string ({'x', 'y', 'z'}), [10, 20, 30]) %! values (d) %! f = dictionary ('sin', @sin); %! f('cos') = @cos %! fns = values (f, 'cell'); %! fns{2} (0) pr0m1th3as-datatypes-9450b29/inst/demos/dictionary.writedictionary000066400000000000000000000037421527370255000252770ustar00rootroot00000000000000%!demo %! ## `writedictionary` writes a dictionary as a JSON object, indented by four %! ## spaces. A `double` is always written with a point, and keys that are %! ## not text are written as text. %! %! f = [tempname() '.json']; %! d = dictionary (string ({'pi', 'one', 'big'}), [pi, 1, 1e20]) %! writedictionary (d, f); %! disp (fileread (f)) %! writedictionary (dictionary ([1.5, 2], [true, false]), f, ... %! 'PrettyPrint', false); %! disp (fileread (f)) %! delete (f); %!demo %! ## Nested dictionaries become objects and the contents of cells their %! ## values, a vector an array. `NaN` and `Inf` are written as such, or as %! ## `null` under `'PreserveInfAndNaN'` false, as missing values are. %! %! f = [tempname() '.json']; %! inner = dictionary (string ({'mean', 'max'}), [NaN, Inf]); %! d = dictionary (string ({'stats', 'samples', 'note'}), ... %! {inner, [3, 5, 8], string(missing)}) %! writedictionary (d, f, 'PreserveInfAndNaN', false); %! disp (fileread (f)) %! delete (f); %!demo %! ## What JSON cannot hold is refused before anything is written: here a %! ## categorical value. Dates are written as text, and read back as dates. %! %! f = [tempname() '.json']; %! try %! writedictionary (dictionary ('size', categorical ({'large'})), f); %! catch err %! disp (err.message) %! end_try_catch %! days = dictionary (string ({'start', 'end'}), datetime (2024, 3, [1, 15])); %! writedictionary (days, f); %! disp (fileread (f)) %! readdictionary (f) %! delete (f); %!demo %! ## A dictionary of tables or timetables can be written to an OpenDocument %! ## spreadsheet, one sheet per key, which `readdictionary` reads back. %! %! readings = dictionary (string ({'Lab A', 'Lab B'}), ... %! {timetable(hours ([1; 2]), [20.5; 21.0]), ... %! timetable(hours ([1; 2]), [19.8; 20.1])}) %! f = [tempname() '.ods']; %! writedictionary (readings, f); %! back = readdictionary (f); %! keys (back) %! back('Lab B') %! delete (f); pr0m1th3as-datatypes-9450b29/inst/demos/duration.abs000066400000000000000000000001721527370255000222760ustar00rootroot00000000000000%!demo %! ## `abs` returns the magnitude of a (possibly negative) duration. %! %! d = minutes (30) - hours (2) %! abs (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.bounds000066400000000000000000000002231527370255000230200ustar00rootroot00000000000000%!demo %! ## `bounds` returns the smallest and largest durations in one call. %! %! d = hours ([2, 5, 3, 9]) %! [lo, hi] = bounds (d); %! [lo, hi] pr0m1th3as-datatypes-9450b29/inst/demos/duration.ceil000066400000000000000000000001461527370255000224460ustar00rootroot00000000000000%!demo %! ## `ceil` rounds a duration up to a whole unit. %! %! d = minutes (90) %! ceil (d, 'hours') pr0m1th3as-datatypes-9450b29/inst/demos/duration.cellstr000066400000000000000000000002011527370255000231720ustar00rootroot00000000000000%!demo %! ## `cellstr` converts the array into a cell array of its display strings. %! %! d = minutes ([90, 150]) %! cellstr (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.center000066400000000000000000000002761527370255000230160ustar00rootroot00000000000000%!demo %! ## `center` subtracts the mean from each element, so the result is centred on %! ## zero: the deviations from the average duration. %! %! d = hours ([1, 2, 3, 4, 5]) %! center (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.char000066400000000000000000000002341527370255000224450ustar00rootroot00000000000000%!demo %! ## `char` converts the array into a character matrix of its display strings, %! ## one row per element. %! %! d = hours ([1.5; 2.25]) %! char (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.colon000066400000000000000000000002701527370255000226420ustar00rootroot00000000000000%!demo %! ## The colon operator builds an evenly-spaced range of durations, using a %! ## duration step, here every half hour from 0 to 2 hours. %! %! hours (0):minutes (30):hours (2) pr0m1th3as-datatypes-9450b29/inst/demos/duration.cummax000066400000000000000000000002331527370255000230210ustar00rootroot00000000000000%!demo %! ## `cummax` returns the running maximum: each element is the largest duration %! ## seen so far. %! %! d = hours ([2, 4, 3, 7, 5]) %! cummax (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.cummin000066400000000000000000000002341527370255000230200ustar00rootroot00000000000000%!demo %! ## `cummin` returns the running minimum: each element is the smallest %! ## duration seen so far. %! %! d = hours ([7, 4, 5, 2, 3]) %! cummin (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.cumsum000066400000000000000000000002011527370255000230330ustar00rootroot00000000000000%!demo %! ## `cumsum` returns the running total of the durations, a cumulative sum. %! %! d = hours ([1, 2, 3, 4]) %! cumsum (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.datevec000066400000000000000000000003211527370255000231400ustar00rootroot00000000000000%!demo %! ## `datevec` returns the components as a numeric row `[Y MO D H MI S]` per %! ## element (a duration has no year/month/day, so those columns are zero). %! %! d = duration (26, 30, 15) %! datevec (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.days000066400000000000000000000002221527370255000224650ustar00rootroot00000000000000%!demo %! ## As a `duration` method, `days` returns the duration expressed in %! ## fixed-length (24-hour) days. %! %! d = hours (36) %! days (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.diff000066400000000000000000000002641527370255000224430ustar00rootroot00000000000000%!demo %! ## `diff` returns the differences between successive durations, for example %! ## the gaps between a series of elapsed times. %! %! d = hours ([1, 3, 6, 10]) %! diff (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.duration000066400000000000000000000023451527370255000233620ustar00rootroot00000000000000%!demo %! ## `duration` represents a *fixed-length* elapsed time. Unlike %! ## `calendarDuration`, here a day is always 24 hours and a year a fixed %! ## number of days. Give hours, minutes and seconds: %! %! duration (1, 30, 15) %!demo %! ## Values need not lie in the usual ranges: 25 hours is fine and stays 25 %! ## hours in the default `hh:mm:ss` display. A fourth argument adds %! ## milliseconds. %! %! duration (25, 30, 0) %! duration (0, 0, 1, 500) %!demo %! ## Build from time strings, optionally naming the `'InputFormat'`. %! %! duration ('36:15:00') %! duration ('06:15', 'InputFormat', 'mm:ss') %!demo %! ## The `'Format'` option controls display: a digital timer (`hh:mm:ss`, %! ## `dd:hh:mm:ss`, `mm:ss`, ...) or a single number in one unit (`y`, `d`, %! ## `h`, `m`, `s`). Here the SAME duration shown six ways: %! %! duration (30, 15, 0, 'Format', 'hh:mm:ss') %! duration (30, 15, 0, 'Format', 'dd:hh:mm:ss') %! duration (30, 15, 0, 'Format', 'h') %! duration (30, 15, 0, 'Format', 'm') %! duration (30, 15, 0, 'Format', 's') %! duration (30, 15, 0, 'Format', 'd') %!demo %! ## A numeric matrix builds a column of durations, one per row; its columns %! ## are hours, minutes and seconds. %! %! duration ([1, 30, 0; 0, 45, 30]) pr0m1th3as-datatypes-9450b29/inst/demos/duration.eq000066400000000000000000000003651527370255000221420ustar00rootroot00000000000000%!demo %! ## `eq` (`==`) tests durations for equality; equal spans compare equal even %! ## when built from different units. %! %! hours (1) == minutes (60) %! %! ## Element-wise against a scalar duration. %! minutes ([30, 60, 90]) == hours (1) pr0m1th3as-datatypes-9450b29/inst/demos/duration.floor000066400000000000000000000001741527370255000226540ustar00rootroot00000000000000%!demo %! ## `floor` rounds a duration down to a whole unit (default: seconds). %! %! d = hours (2.9) %! floor (d, 'hours') pr0m1th3as-datatypes-9450b29/inst/demos/duration.hms000066400000000000000000000006151527370255000223220ustar00rootroot00000000000000%!demo %! ## `hms` splits each duration into its hours, minutes and seconds as three %! ## separate numeric outputs. %! %! d = duration (1, 30, 45) %! [h, m, s] = hms (d); %! [h, m, s] %!demo %! ## Every component carries the sign of the duration as a whole, and the %! ## seconds keep their fraction on either side of zero. %! %! d = duration (-4, -5, -6.5) %! [h, m, s] = hms (d); %! [h, m, s] pr0m1th3as-datatypes-9450b29/inst/demos/duration.hours000066400000000000000000000003761527370255000226770ustar00rootroot00000000000000%!demo %! ## As a `duration` *method*, `hours` returns the duration expressed as a %! ## number of hours, the inverse of the `hours` builder. A single duration %! ## can be read out in any unit this way. %! %! d = hours (30) + minutes (15) %! hours (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.interp1000066400000000000000000000003641527370255000231160ustar00rootroot00000000000000%!demo %! ## `interp1` interpolates duration-valued data at new sample points, which is %! ## useful for resampling a time series measured in durations. %! %! t = hours ([0, 1, 2]); %! v = minutes ([10, 20, 40]); %! interp1 (t, v, hours (0.5)) pr0m1th3as-datatypes-9450b29/inst/demos/duration.intersect000066400000000000000000000002161527370255000235300ustar00rootroot00000000000000%!demo %! ## `intersect` returns the durations common to two arrays. %! %! A = hours ([1, 2, 3]) %! B = hours ([2, 3, 4]) %! intersect (A, B) pr0m1th3as-datatypes-9450b29/inst/demos/duration.iqr000066400000000000000000000002561527370255000223270ustar00rootroot00000000000000%!demo %! ## `iqr` returns the interquartile range (the 75th minus the 25th %! ## percentile), a robust measure of spread. %! %! d = hours ([2, 4, 4, 4, 5, 7, 9]) %! iqr (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.isbetween000066400000000000000000000003061527370255000235150ustar00rootroot00000000000000%!demo %! ## `isbetween` tests, element by element, whether each duration lies within %! ## the closed interval `[lower, upper]`. %! %! d = hours ([1, 5, 10]) %! isbetween (d, hours (2), hours (8)) pr0m1th3as-datatypes-9450b29/inst/demos/duration.isfinite000066400000000000000000000002011527370255000233340ustar00rootroot00000000000000%!demo %! ## `isfinite` flags the elements that are neither infinite nor NaN. %! %! d = hours ([1, Inf, NaN, 4]) %! isfinite (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.isinf000066400000000000000000000001711527370255000226400ustar00rootroot00000000000000%!demo %! ## `isinf` flags infinite durations (from an infinite input value). %! %! d = hours ([1, Inf, 3]) %! isinf (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.ismember000066400000000000000000000002471527370255000233370ustar00rootroot00000000000000%!demo %! ## `ismember` tests, element by element, whether each duration appears in a %! ## second set. %! %! d = hours ([1, 2, 5]) %! ismember (d, hours ([2, 3, 5])) pr0m1th3as-datatypes-9450b29/inst/demos/duration.ismissing000066400000000000000000000003261527370255000235370ustar00rootroot00000000000000%!demo %! ## For a duration `ismissing` is the same test as `isnan`: a NaN duration is %! ## the missing value, so durations plug into the generic missing-data tools. %! %! d = hours ([1, NaN, 3]) %! ismissing (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.isnan000066400000000000000000000001601527370255000226360ustar00rootroot00000000000000%!demo %! ## `isnan` flags the elements whose value is Not-a-Number. %! %! d = hours ([1, NaN, 3]) %! isnan (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.issorted000066400000000000000000000002431527370255000233640ustar00rootroot00000000000000%!demo %! ## `issorted` checks whether the durations are already in non-decreasing %! ## order. %! %! issorted (hours ([1, 2, 3])) %! issorted (hours ([2, 1, 3])) pr0m1th3as-datatypes-9450b29/inst/demos/duration.kurtosis000066400000000000000000000003171527370255000234150ustar00rootroot00000000000000%!demo %! ## `kurtosis` measures the "tailedness" of the duration distribution. It is %! ## a dimensionless shape statistic, returned as a plain number. %! %! d = hours ([1, 2, 2, 2, 3, 9]) %! kurtosis (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.linspace000066400000000000000000000002171527370255000233270ustar00rootroot00000000000000%!demo %! ## `linspace` builds a fixed number of durations evenly spaced between two %! ## endpoints. %! %! linspace (hours (0), hours (2), 5) pr0m1th3as-datatypes-9450b29/inst/demos/duration.lt000066400000000000000000000002671527370255000221550ustar00rootroot00000000000000%!demo %! ## `lt` (`<`) compares durations by length. `<`, `<=`, `>` and `>=` all %! ## work, since durations have a natural order. %! %! d = minutes ([30, 60, 90]) %! d < hours (1) pr0m1th3as-datatypes-9450b29/inst/demos/duration.mad000066400000000000000000000002431527370255000222710ustar00rootroot00000000000000%!demo %! ## `mad` returns the mean absolute deviation of the durations, another robust %! ## spread statistic. %! %! d = hours ([2, 4, 4, 4, 5, 7, 9]) %! mad (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.mape000066400000000000000000000003771527370255000224620ustar00rootroot00000000000000%!demo %! ## `mape` returns the mean absolute percentage error between two duration %! ## arrays. Being a ratio it is dimensionless, a plain percentage number. %! %! predicted = hours ([1, 2, 4]) %! actual = hours ([1, 2, 3]) %! mape (predicted, actual) pr0m1th3as-datatypes-9450b29/inst/demos/duration.max000066400000000000000000000003231527370255000223140ustar00rootroot00000000000000%!demo %! ## `max` returns the longest duration in an array. %! %! d = minutes ([90, 30, 150, 45]) %! max (d) %! %! ## A second output gives its index (as for numeric `max`). %! [longest, idx] = max (d); %! idx pr0m1th3as-datatypes-9450b29/inst/demos/duration.mean000066400000000000000000000001741527370255000224530ustar00rootroot00000000000000%!demo %! ## `mean` averages the durations in an array, returning a duration. %! %! d = hours ([1, 2, 3, 4, 5]) %! mean (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.median000066400000000000000000000001731527370255000227670ustar00rootroot00000000000000%!demo %! ## `median` returns the middle duration of an array by value. %! %! d = minutes ([10, 20, 30, 90]) %! median (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.milliseconds000066400000000000000000000002261527370255000242160ustar00rootroot00000000000000%!demo %! ## As a `duration` method, `milliseconds` returns the duration expressed in %! ## milliseconds. %! %! d = seconds (2.5) %! milliseconds (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.min000066400000000000000000000001571527370255000223170ustar00rootroot00000000000000%!demo %! ## `min` returns the shortest duration in an array. %! %! d = minutes ([90, 30, 150, 45]) %! min (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.minus000066400000000000000000000001501527370255000226600ustar00rootroot00000000000000%!demo %! ## `minus` (`-`) subtracts one duration from another. %! %! a = hours (3) %! a - minutes (45) pr0m1th3as-datatypes-9450b29/inst/demos/duration.minutes000066400000000000000000000002301527370255000232100ustar00rootroot00000000000000%!demo %! ## As a `duration` method, `minutes` returns the whole duration expressed in %! ## minutes. %! %! d = hours (1) + minutes (30) %! minutes (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.mode000066400000000000000000000001621527370255000224540ustar00rootroot00000000000000%!demo %! ## `mode` returns the most frequent duration in an array. %! %! d = hours ([1, 2, 2, 3, 2]) %! mode (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.plus000066400000000000000000000001751527370255000225170ustar00rootroot00000000000000%!demo %! ## `plus` (`+`) adds durations together, giving a duration. %! %! a = hours (2) + minutes (30) %! a + seconds (45) pr0m1th3as-datatypes-9450b29/inst/demos/duration.prctile000066400000000000000000000002261527370255000231730ustar00rootroot00000000000000%!demo %! ## `prctile` returns the requested percentiles of a duration array. %! %! d = hours ([1, 2, 3, 4, 5, 6, 7, 8]) %! prctile (d, [25, 50, 75]) pr0m1th3as-datatypes-9450b29/inst/demos/duration.quantile000066400000000000000000000002401527370255000233470ustar00rootroot00000000000000%!demo %! ## `quantile` is the same idea as `prctile` but on a scale from 0 to 1. %! %! d = hours ([1, 2, 3, 4, 5, 6, 7, 8]) %! quantile (d, [0.25, 0.5, 0.75]) pr0m1th3as-datatypes-9450b29/inst/demos/duration.range000066400000000000000000000002011527370255000226160ustar00rootroot00000000000000%!demo %! ## `range` returns the span from shortest to longest, that is `max - min`. %! %! d = hours ([2, 5, 3, 9]) %! range (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.rdivide000066400000000000000000000003141527370255000231550ustar00rootroot00000000000000%!demo %! ## `rdivide` (`./`) divides a duration by a number to scale it down. %! %! hours (3) ./ 2 %! %! ## Dividing one duration by another gives their dimensionless ratio. %! hours (3) ./ minutes (30) pr0m1th3as-datatypes-9450b29/inst/demos/duration.rmse000066400000000000000000000003471527370255000225030ustar00rootroot00000000000000%!demo %! ## `rmse` returns the root-mean-square error between two duration arrays, a %! ## duration measuring their typical difference. %! %! predicted = hours ([1, 2, 3]) %! actual = hours ([1, 2, 5]) %! rmse (predicted, actual) pr0m1th3as-datatypes-9450b29/inst/demos/duration.round000066400000000000000000000003771527370255000226670ustar00rootroot00000000000000%!demo %! ## `round` rounds a duration. With no unit it rounds to the nearest second; %! ## pass a unit to round to whole hours, minutes, and so on. %! %! d = hours (2.4) %! round (d, 'hours') %! %! ## Default (nearest second): %! round (seconds (90.7)) pr0m1th3as-datatypes-9450b29/inst/demos/duration.seconds000066400000000000000000000002241527370255000231650ustar00rootroot00000000000000%!demo %! ## As a `duration` method, `seconds` returns the duration expressed in %! ## seconds. %! %! d = minutes (2) + seconds (30) %! seconds (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.setdiff000066400000000000000000000002331527370255000231530ustar00rootroot00000000000000%!demo %! ## `setdiff` returns the durations in the first array but not the second. %! %! A = hours ([1, 2, 3]) %! B = hours ([2, 3, 4]) %! setdiff (A, B) pr0m1th3as-datatypes-9450b29/inst/demos/duration.setxor000066400000000000000000000002501527370255000230520ustar00rootroot00000000000000%!demo %! ## `setxor` returns the symmetric difference: the durations in exactly one %! ## array. %! %! A = hours ([1, 2, 3]) %! B = hours ([2, 3, 4]) %! setxor (A, B) pr0m1th3as-datatypes-9450b29/inst/demos/duration.sign000066400000000000000000000001731527370255000224720ustar00rootroot00000000000000%!demo %! ## `sign` returns -1, 0 or 1 for negative, zero and positive durations. %! %! d = hours ([-2, 0, 3]) %! sign (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.skewness000066400000000000000000000003321527370255000233710ustar00rootroot00000000000000%!demo %! ## `skewness` measures the asymmetry of the duration distribution. Being a %! ## shape statistic it is dimensionless, so it comes back as a plain number. %! %! d = hours ([1, 1, 1, 2, 3, 8]) %! skewness (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.sort000066400000000000000000000001651527370255000225220ustar00rootroot00000000000000%!demo %! ## `sort` orders the durations from shortest to longest. %! %! d = minutes ([180, 30, 90, 45]) %! sort (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.std000066400000000000000000000002041527370255000223170ustar00rootroot00000000000000%!demo %! ## `std` returns the standard deviation of the durations, itself a duration. %! %! d = hours ([1, 2, 3, 4, 5]) %! std (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.sum000066400000000000000000000001401527370255000223300ustar00rootroot00000000000000%!demo %! ## `sum` adds up the durations in an array. %! %! d = hours ([1, 2, 3, 4]) %! sum (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.times000066400000000000000000000002761527370255000226570ustar00rootroot00000000000000%!demo %! ## `times` (`.*`) scales a duration by a numeric factor. %! %! hours (1.5) * 4 %! %! ## Element-wise scaling by an array yields an array of durations. %! minutes (30) .* [1, 2, 3] pr0m1th3as-datatypes-9450b29/inst/demos/duration.union000066400000000000000000000002221527370255000226550ustar00rootroot00000000000000%!demo %! ## `union` returns the distinct durations present in either array. %! %! A = hours ([1, 2, 3]) %! B = hours ([2, 3, 4]) %! union (A, B) pr0m1th3as-datatypes-9450b29/inst/demos/duration.unique000066400000000000000000000002211527370255000230320ustar00rootroot00000000000000%!demo %! ## `unique` returns the distinct durations in an array (equal spans count %! ## once). %! %! d = hours ([2, 1, 2, 3, 1]) %! unique (d) pr0m1th3as-datatypes-9450b29/inst/demos/duration.vertcat000066400000000000000000000002761527370255000232060ustar00rootroot00000000000000%!demo %! ## Durations concatenate like ordinary arrays; `[a; b]` stacks them into one %! ## column, converting units as needed. %! %! a = hours ([1; 2]) %! b = minutes ([90; 30]) %! [a; b] pr0m1th3as-datatypes-9450b29/inst/demos/duration.years000066400000000000000000000002401527370255000226500ustar00rootroot00000000000000%!demo %! ## As a `duration` method, `years` returns the duration expressed in %! ## fixed-length years (365.2425 days). %! %! d = days (365.2425) %! years (d) pr0m1th3as-datatypes-9450b29/inst/demos/eventfilter.eventfilter000066400000000000000000000041771527370255000245730ustar00rootroot00000000000000%!demo %! ## An event filter selects the rows of a timetable by what was happening, %! ## rather than by the clock. The condition picks out *events*; the rows %! ## the matching events cover follow from them. %! %! t0 = datetime (2024, 3, 1); %! TT = timetable ([12; 45; 23; 31], 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'Depth'}); %! TT.Properties.Events = eventtable (t0 + hours (1), ... %! 'EventLabels', "rain", ... %! 'EventLengths', hours (2)); %! EF = eventfilter (TT); %! TT(EF.EventLabels == "rain", :) %!demo %! ## Conditions combine with `&` and `|`, and `Time` names the *event's* %! ## time rather than the row's. %! %! t0 = datetime (2024, 3, 1); %! TT = timetable ([12; 45; 23; 31], 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'Depth'}); %! TT.Properties.Events = eventtable (t0 + hours ([1; 2]), ... %! 'EventLabels', ["rain"; "hail"], ... %! 'EventLengths', hours ([1; 2])); %! EF = eventfilter (TT); %! TT(EF.EventLabels == "hail" & EF.EventLengths > hours (1), :) %!demo %! ## A filter made from labels alone is a whole condition already, and %! ## needs no timetable to be written against. %! %! t0 = datetime (2024, 3, 1); %! TT = timetable ([12; 45; 23; 31], 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'Depth'}); %! TT.Properties.Events = eventtable (t0 + hours ([1; 2]), ... %! 'EventLabels', ["rain"; "hail"]); %! TT(eventfilter ("hail"), :) %!demo %! ## `timerange` takes two event filters as bounds, running from the start %! ## of one event to the start of the other and stopping short of it. %! %! t0 = datetime (2024, 3, 1); %! TT = timetable ([12; 45; 23; 31], 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'Depth'}); %! TT.Properties.Events = eventtable (t0 + hours ([1; 3]), ... %! 'EventLabels', ["rain"; "snow"]); %! EF = eventfilter (TT); %! TT(timerange (EF.EventLabels == "rain", EF.EventLabels == "snow"), :) pr0m1th3as-datatypes-9450b29/inst/demos/eventtable.eventtable000066400000000000000000000040511527370255000241660ustar00rootroot00000000000000%!demo %! ## An event table is a timetable whose rows are events. Given only the %! ## times, it labels them `Event 1`, `Event 2` and so on and points %! ## `EventLabelsVariable` at the variable holding them. %! %! t0 = datetime (2024, 3, 1); %! eventtable (t0 + hours ([2; 9; 14])) %!demo %! ## `EventLabels` names the events, and `EventLengths` makes each of them %! ## an interval rather than an instant. An interval runs from the event's %! ## own time and stops short of its end, so a two-hour event beginning at %! ## 02:00 covers 02:00 and 03:00 but not 04:00. %! %! t0 = datetime (2024, 3, 1); %! ET = eventtable (t0 + hours ([2; 9]), ... %! 'EventLabels', ["rain"; "snow"], ... %! 'EventLengths', hours ([2; 1])) %! %! ## The three properties hold variable *names*, not the values. %! ET.Properties.EventLabelsVariable %!demo %! ## `EventEnds` says the same thing the other way round. A length and an %! ## end are mutually exclusive: an event states its extent once. %! %! t0 = datetime (2024, 3, 1); %! eventtable (t0 + hours ([2; 9]), 'EventLabels', ["rain"; "snow"], ... %! 'EventEnds', t0 + hours ([4; 10])) %!demo %! ## An existing timetable becomes an event table, keeping its variables. %! ## No variable is guessed to hold the labels, so `EventLabelsVariable` %! ## has to be named if one of them does. %! %! t0 = datetime (2024, 3, 1); %! Phase = ["dry"; "wet"]; %! Depth = [12; 45]; %! TT = timetable (Phase, Depth, 'RowTimes', t0 + hours ([2; 9])); %! ET = eventtable (TT, 'EventLabelsVariable', 'Phase'); %! ET.Properties.EventLabelsVariable %!demo %! ## The event table is attached to the timetable it describes, and a %! ## row-preserving operation carries it through untouched. %! %! t0 = datetime (2024, 3, 1); %! TT = timetable ([12; 45; 23; 31], 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'Depth'}); %! TT.Properties.Events = eventtable (t0 + hours ([1; 3]), ... %! 'EventLabels', ["rain"; "snow"]); %! height (sortrows (TT).Properties.Events) pr0m1th3as-datatypes-9450b29/inst/demos/string.append000066400000000000000000000003201527370255000224540ustar00rootroot00000000000000%!demo %! ## `append` joins its arguments end to end, element-wise, like `strcat`, but %! ## it keeps trailing whitespace. %! %! first = string ({'Ann'; 'Bob'}) %! append (first, ' ', string ({'Lee'; 'Ng'})) pr0m1th3as-datatypes-9450b29/inst/demos/string.cell000066400000000000000000000002041527370255000221250ustar00rootroot00000000000000%!demo %! ## `cell` also returns a cell array of character vectors (like `cellstr`). %! %! s = string ({'a'; 'b'; 'c'}) %! cell (s) pr0m1th3as-datatypes-9450b29/inst/demos/string.cellstr000066400000000000000000000003041527370255000226570ustar00rootroot00000000000000%!demo %! ## `cellstr` converts a string array into a cell array of character vectors, %! ## the inverse of building a string from a cellstr. %! %! s = string ({'apple'; 'banana'}) %! cellstr (s) pr0m1th3as-datatypes-9450b29/inst/demos/string.char000066400000000000000000000002711527370255000221270ustar00rootroot00000000000000%!demo %! ## `char` converts a string array to a plain character matrix, one row per %! ## element (shorter rows padded with spaces). %! %! s = string ({'small'; 'medium'}) %! char (s) pr0m1th3as-datatypes-9450b29/inst/demos/string.compose000066400000000000000000000003771527370255000226660ustar00rootroot00000000000000%!demo %! ## `compose` applies a `printf`-style format to data, returning a string %! ## array with one element per row of the data. (The format itself is given %! ## as a string.) %! %! compose (string ('%s scored %d'), string ({'Ann'; 'Bob'}), [7; 9]) pr0m1th3as-datatypes-9450b29/inst/demos/string.contains000066400000000000000000000002551527370255000230320ustar00rootroot00000000000000%!demo %! ## `contains` tests, element-wise, whether each string contains a substring. %! %! files = string ({'cat.txt'; 'dog.csv'; 'fish.txt'}) %! contains (files, '.txt') pr0m1th3as-datatypes-9450b29/inst/demos/string.count000066400000000000000000000002101527370255000223330ustar00rootroot00000000000000%!demo %! ## `count` returns how many times a substring occurs in each string. %! %! s = string ({'banana'; 'apple'}) %! count (s, 'a') pr0m1th3as-datatypes-9450b29/inst/demos/string.double000066400000000000000000000002431527370255000224630ustar00rootroot00000000000000%!demo %! ## `double` parses numeric text back into numbers; entries that are not %! ## numbers become `NaN`. %! %! s = string ({'3.14'; '42'; 'x'}) %! double (s) pr0m1th3as-datatypes-9450b29/inst/demos/string.endsWith000066400000000000000000000002421527370255000227750ustar00rootroot00000000000000%!demo %! ## `endsWith` tests whether each string ends with a given suffix. %! %! files = string ({'cat.txt'; 'dog.csv'; 'fish.txt'}) %! endsWith (files, '.csv') pr0m1th3as-datatypes-9450b29/inst/demos/string.eq000066400000000000000000000002031527370255000216120ustar00rootroot00000000000000%!demo %! ## `eq` (`==`) compares strings for equality, element by element. %! %! a = string ({'cat'; 'dog'; 'cat'}) %! a == 'cat' pr0m1th3as-datatypes-9450b29/inst/demos/string.erase000066400000000000000000000002261527370255000223110ustar00rootroot00000000000000%!demo %! ## `erase` deletes every occurrence of a substring from each string. %! %! files = string ({'cat.txt'; 'dog.txt'}) %! erase (files, '.txt') pr0m1th3as-datatypes-9450b29/inst/demos/string.eraseBetween000066400000000000000000000002741527370255000236260ustar00rootroot00000000000000%!demo %! ## `eraseBetween` deletes the text between two delimiters, keeping the %! ## delimiters themselves. %! %! s = string ({'ab'; 'xy'}) %! eraseBetween (s, '<', '>') pr0m1th3as-datatypes-9450b29/inst/demos/string.extract000066400000000000000000000002661527370255000226700ustar00rootroot00000000000000%!demo %! ## `extract` pulls out every occurrence of a pattern, returning the matched %! ## pieces as a string array. %! %! s = string ('cat dog cat bird cat') %! extract (s, 'cat') pr0m1th3as-datatypes-9450b29/inst/demos/string.extractAfter000066400000000000000000000003411527370255000236440ustar00rootroot00000000000000%!demo %! ## `extractAfter` returns the part of each string after the first occurrence %! ## of a substring, for example the extension after the dot. %! %! files = string ({'cat.txt'; 'dog.csv'}) %! extractAfter (files, '.') pr0m1th3as-datatypes-9450b29/inst/demos/string.extractBefore000066400000000000000000000003661527370255000240140ustar00rootroot00000000000000%!demo %! ## `extractBefore` returns the part of each string before the first %! ## occurrence of a substring, for example a file's base name before its %! ## extension. %! %! files = string ({'cat.txt'; 'dog.csv'}) %! extractBefore (files, '.') pr0m1th3as-datatypes-9450b29/inst/demos/string.extractBetween000066400000000000000000000002251527370255000241750ustar00rootroot00000000000000%!demo %! ## `extractBetween` returns the text lying between two delimiters. %! %! s = string ({'[abc]'; '[hello]'}) %! extractBetween (s, '[', ']') pr0m1th3as-datatypes-9450b29/inst/demos/string.insertAfter000066400000000000000000000002431527370255000234770ustar00rootroot00000000000000%!demo %! ## `insertAfter` inserts text right after each occurrence of a substring. %! %! s = string ({'hello world'; 'good day'}) %! insertAfter (s, ' ', 'big ') pr0m1th3as-datatypes-9450b29/inst/demos/string.insertBefore000066400000000000000000000003451527370255000236430ustar00rootroot00000000000000%!demo %! ## `insertBefore` inserts text right before each occurrence of a substring, %! ## for example a suffix just before a file extension. %! %! files = string ({'cat.txt'; 'dog.txt'}) %! insertBefore (files, '.txt', '_bak') pr0m1th3as-datatypes-9450b29/inst/demos/string.ismember000066400000000000000000000002771527370255000230230ustar00rootroot00000000000000%!demo %! ## `ismember` tests, element by element, whether each string appears in a %! ## second set. %! %! s = string ({'apple'; 'kiwi'; 'pear'}) %! ismember (s, string ({'apple'; 'pear'})) pr0m1th3as-datatypes-9450b29/inst/demos/string.ismissing000066400000000000000000000003321527370255000232150ustar00rootroot00000000000000%!demo %! ## `ismissing` flags the `` elements, the string form of missing %! ## data, distinct from an empty string `""`. %! %! s = string ({'a'; 'b'; 'c'}); %! s(2) = string (missing); %! s %! ismissing (s) pr0m1th3as-datatypes-9450b29/inst/demos/string.issorted000066400000000000000000000002471527370255000230510ustar00rootroot00000000000000%!demo %! ## `issorted` checks whether the strings are already in alphabetical order. %! %! issorted (string ({'a'; 'b'; 'c'})) %! issorted (string ({'b'; 'a'; 'c'})) pr0m1th3as-datatypes-9450b29/inst/demos/string.isstring000066400000000000000000000004011527370255000230470ustar00rootroot00000000000000%!demo %! ## `isstring` returns true only for string arrays. It is the reliable way to %! ## tell a string from a character vector, which (see `string`) look alike in %! ## source. %! %! isstring (string ('text')) %! isstring ('text') %! isstring ("text") pr0m1th3as-datatypes-9450b29/inst/demos/string.join000066400000000000000000000002731527370255000221530ustar00rootroot00000000000000%!demo %! ## `join` combines the elements of a string array into a single string, %! ## placing a delimiter between them. %! %! parts = string ({'2024', '01', '15'}) %! join (parts, '-') pr0m1th3as-datatypes-9450b29/inst/demos/string.lower000066400000000000000000000001541527370255000223420ustar00rootroot00000000000000%!demo %! ## `lower` converts each string to lower case. %! %! s = string ({'Hello'; 'WORLD'}) %! lower (s) pr0m1th3as-datatypes-9450b29/inst/demos/string.matches000066400000000000000000000003311527370255000226330ustar00rootroot00000000000000%!demo %! ## `matches` tests, element-wise, whether each string equals a pattern %! ## *exactly* (contrast `contains`, which tests for a substring). %! %! s = string ({'cat'; 'category'; 'cat'}) %! matches (s, 'cat') pr0m1th3as-datatypes-9450b29/inst/demos/string.pad000066400000000000000000000002541527370255000217570ustar00rootroot00000000000000%!demo %! ## `pad` pads each string with spaces to a common width, which is handy for %! ## aligning text into columns. %! %! s = string ({'a'; 'bb'; 'ccc'}) %! pad (s, 5) pr0m1th3as-datatypes-9450b29/inst/demos/string.plus000066400000000000000000000003711527370255000221760ustar00rootroot00000000000000%!demo %! ## `plus` (`+`) concatenates two strings, a compact alternative to `append`. %! ## Both operands must be strings (a bare `'-'` is a char, so wrap it). %! %! a = string ({'Ann'; 'Bob'}) %! b = string ({'Lee'; 'Ng'}) %! a + string (' ') + b pr0m1th3as-datatypes-9450b29/inst/demos/string.replace000066400000000000000000000002411527370255000226220ustar00rootroot00000000000000%!demo %! ## `replace` substitutes every occurrence of a substring with another. %! %! files = string ({'cat.txt'; 'dog.txt'}) %! replace (files, '.txt', '.md') pr0m1th3as-datatypes-9450b29/inst/demos/string.replaceBetween000066400000000000000000000002341527370255000241360ustar00rootroot00000000000000%!demo %! ## `replaceBetween` substitutes the text lying between two delimiters. %! %! s = string ({'ab'; 'cd'}) %! replaceBetween (s, '<', '>', 'Z') pr0m1th3as-datatypes-9450b29/inst/demos/string.reverse000066400000000000000000000001701527370255000226630ustar00rootroot00000000000000%!demo %! ## `reverse` reverses the characters within each string. %! %! s = string ({'Hello'; 'World'}) %! reverse (s) pr0m1th3as-datatypes-9450b29/inst/demos/string.sort000066400000000000000000000001711527370255000222000ustar00rootroot00000000000000%!demo %! ## `sort` orders the strings alphabetically. %! %! s = string ({'pear'; 'apple'; 'fig'; 'banana'}) %! sort (s) pr0m1th3as-datatypes-9450b29/inst/demos/string.split000066400000000000000000000002271527370255000223460ustar00rootroot00000000000000%!demo %! ## `split` breaks a string at each occurrence of a delimiter, the inverse of %! ## `join`. %! %! s = string ('2024-01-15') %! split (s, '-') pr0m1th3as-datatypes-9450b29/inst/demos/string.splitlines000066400000000000000000000002531527370255000234000ustar00rootroot00000000000000%!demo %! ## `splitlines` breaks a string at its newline characters into one element %! ## per line. %! %! s = string (sprintf ('first\nsecond\nthird')) %! splitlines (s) pr0m1th3as-datatypes-9450b29/inst/demos/string.startsWith000066400000000000000000000002401527370255000233620ustar00rootroot00000000000000%!demo %! ## `startsWith` tests whether each string begins with a given prefix. %! %! files = string ({'img_1'; 'img_2'; 'doc_1'}) %! startsWith (files, 'img') pr0m1th3as-datatypes-9450b29/inst/demos/string.strcat000066400000000000000000000002371527370255000225140ustar00rootroot00000000000000%!demo %! ## `strcat` concatenates strings horizontally, element-wise across its %! ## inputs. %! %! name = string ({'Ann'; 'Bob'}) %! strcat (name, ' Smith') pr0m1th3as-datatypes-9450b29/inst/demos/string.strcmp000066400000000000000000000003111527370255000225150ustar00rootroot00000000000000%!demo %! ## `strcmp` compares strings for exact equality, the same idea as `==`, kept %! ## for compatibility with the classic string functions. %! %! a = string ({'cat'; 'dog'}) %! strcmp (a, 'cat') pr0m1th3as-datatypes-9450b29/inst/demos/string.string000066400000000000000000000020571527370255000225240ustar00rootroot00000000000000%!demo %! ## A `string` is MATLAB's string-array type. There is a crucial Octave %! ## difference to keep in mind: in core Octave BOTH `'text'` and `"text"` %! ## produce ordinary **character vectors** (class `char`). Unlike MATLAB, the %! ## double quote does *not* make a string. Only the `string` constructor %! ## produces a string array. %! %! class ('text') % single quotes -> char %! class ("text") % double quotes -> ALSO char in Octave %! class (string ('text')) % the constructor -> string %!demo %! ## Build a string array from a cell array of character vectors, one element %! ## per cell. A string array prints its elements in double quotes. %! %! string ({'apple'; 'banana'; 'cherry'}) %!demo %! ## Numbers convert to their text form. %! %! string ([3.14, 42, -1]) %!demo %! ## The special `` value is the string counterpart of `NaN`, and is %! ## distinct from an empty string `""`. Assign `string (missing)` to %! ## introduce one. %! %! s = string ({'a'; 'b'; 'c'}) %! s(2) = string (missing) %! ismissing (s) pr0m1th3as-datatypes-9450b29/inst/demos/string.strip000066400000000000000000000002061527370255000223510ustar00rootroot00000000000000%!demo %! ## `strip` removes leading and trailing whitespace from each string. %! %! s = string ({' spaced '; 'tidy'}) %! strip (s) pr0m1th3as-datatypes-9450b29/inst/demos/string.strlength000066400000000000000000000003201527370255000232170ustar00rootroot00000000000000%!demo %! ## `strlength` returns the number of characters in each string (contrast %! ## `length`/`numel`, which count the array's elements). %! %! s = string ({'a'; 'four'; 'twelve chars'}) %! strlength (s) pr0m1th3as-datatypes-9450b29/inst/demos/string.unique000066400000000000000000000002031527370255000225130ustar00rootroot00000000000000%!demo %! ## `unique` returns the distinct strings in an array, sorted. %! %! s = string ({'b'; 'a'; 'b'; 'c'; 'a'}) %! unique (s) pr0m1th3as-datatypes-9450b29/inst/demos/string.upper000066400000000000000000000001541527370255000223450ustar00rootroot00000000000000%!demo %! ## `upper` converts each string to upper case. %! %! s = string ({'Hello'; 'World'}) %! upper (s) pr0m1th3as-datatypes-9450b29/inst/demos/string.vertcat000066400000000000000000000003021527370255000226550ustar00rootroot00000000000000%!demo %! ## String arrays concatenate like ordinary arrays; `[a; b]` stacks them into %! ## a single column. %! %! a = string ({'apple'; 'banana'}) %! b = string ({'cherry'; 'date'}) %! [a; b] pr0m1th3as-datatypes-9450b29/inst/demos/table.DimensionNames000066400000000000000000000016241527370255000237070ustar00rootroot00000000000000%!demo %! ## Create a table and display its dimension names. You can access row %! ## names and data using dimension names with dot syntax. %! %! LastName = {'Sanchez'; 'Johnson'; 'Li'; 'Diaz'; 'Brown'}; %! Age = [38;43;38;40;49]; %! Smoker = logical ([1; 0; 1; 0; 1]); %! Height = [71; 69; 64; 67; 64]; %! Weight = [176; 163; 131; 133; 119]; %! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80]; %! T = table (Age, Smoker, Height, Weight, BloodPressure, 'RowNames', LastName) %! T.Properties.DimensionNames %! %! ## Access the row names using the first dimension name. %! T.Row %! %! ## Access the data using the second dimension name. %! T.Variables %! %! ## Modify the names of its dimensions using the Properties %! T.Properties.DimensionNames = {'Patient', 'Data'}; %! T.Properties %! %! ## Change a single dimension name %! T.Properties.DimensionNames(1) = 'Patients' %! T.Properties %! T.Patients pr0m1th3as-datatypes-9450b29/inst/demos/table.RowNames000066400000000000000000000020701527370255000225250ustar00rootroot00000000000000%!demo %! ## Access rows by selecting row names %! %! LastName = {'Sanchez'; 'Johnson'; 'Lee'; 'Diaz'; 'Brown'}; %! Age = [38;43;38;40;49]; %! Height = [71;69;64;67;64]; %! Weight = [176;163;131;133;119]; %! T = table (Age, Weight, Height, 'RowNames', LastName) %! T('Lee',:) %! T{'Lee',:} %! T({'Lee', 'Diaz'},:) %! T{{'Lee', 'Diaz'},:} %!demo %! ## Set row names by assigning a new variable %! %! LastName = {'Sanchez'; 'Johnson'; 'Lee'; 'Diaz'; 'Brown'}; %! Age = [38;43;38;40;49]; %! Height = [71;69;64;67;64]; %! Weight = [176;163;131;133;119]; %! T = table (Age, Weight, Height) %! T.Properties.RowNames = LastName %!demo %! ## Set row names by assigning an existing variable %! %! LastName = {'Sanchez'; 'Johnson'; 'Lee'; 'Diaz'; 'Brown'}; %! Age = [38;43;38;40;49]; %! Height = [71;69;64;67;64]; %! Weight = [176;163;131;133;119]; %! T = table (Age, Weight, Height, LastName) %! %! ## In this case the variable persists in the table %! T.Properties.RowNames = T.LastName %! %! ## In this case the variable is removed from the table %! T.Properties.RowNames = 'LastName' pr0m1th3as-datatypes-9450b29/inst/demos/table.VariableNames000066400000000000000000000012101527370255000234760ustar00rootroot00000000000000%!demo %! ## Store patient data in a table %! %! LastName = {'Sanchez'; 'Johnson'; 'Li'; 'Diaz'; 'Brown'}; %! Age = [38;43;38;40;49]; %! Smoker = logical ([1; 0; 1; 0; 1]); %! Height = [71; 69; 64; 67; 64]; %! Weight = [176; 163; 131; 133; 119]; %! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80]; %! T = table (LastName, Age, Smoker, Height, Weight, BloodPressure) %! %! ## Use indexing to access variables %! meanHeight = mean (T.Height) %! %! ## Calculate body mass index (BMI), and add it as a new table variable. %! pounds2kg = 0.4535924; %! inch2meter = 0.0254; %! T.BMI = (T.Weight * pounds2kg) ./ (T.Height * inch2meter) .^ 2 pr0m1th3as-datatypes-9450b29/inst/demos/table.addprop000066400000000000000000000021361527370255000224260ustar00rootroot00000000000000%!demo %! ## `addprop` attaches your own metadata to a table. Each property is %! ## declared with a *scope*: a `'table'`-scoped property holds a single value %! ## for the whole table, while a `'variable'`-scoped property holds one value %! ## per variable, aligned with the table's columns. %! %! Age = [38; 43; 40]; %! Height = duration (0, 0, 0) + hours ([1.9; 1.8; 1.7]); %! Gender = categorical ({'M'; 'F'; 'M'}); %! T = table (Age, Height, Gender) %! T = addprop (T, {'DataSource', 'Instrument'}, {'table', 'variable'}); %! %! ## The table-scoped `DataSource` takes one value; the variable-scoped %! ## `Instrument` takes a cell with one entry per variable (Age, Height, %! ## Gender). %! T.Properties.CustomProperties.DataSource = 'Clinic A'; %! T.Properties.CustomProperties.Instrument = {'form', 'stadiometer', 'form'}; %! T.Properties.CustomProperties %! %! ## Because it is variable-scoped, `Instrument` follows column operations: %! ## after removing `Height` its entry drops out too, staying aligned with the %! ## columns. %! T2 = removevars (T, 'Height'); %! T2.Properties.CustomProperties.Instrument pr0m1th3as-datatypes-9450b29/inst/demos/table.addvars000066400000000000000000000012271527370255000224210ustar00rootroot00000000000000%!demo %! ## `addvars` inserts one or more variables into a table. Appended at the end %! ## by default, the new columns can be named with `'NewVariableNames'` and may %! ## be of any type; here a `datetime` is added to a table of mixed variables. %! %! Name = string ({'Sanchez'; 'Johnson'; 'Li'}); %! Age = [38; 43; 40]; %! Gender = categorical ({'M'; 'M'; 'F'}); %! T = table (Name, Age, Gender) %! Visit = datetime (2024, [1; 2; 3], [5; 6; 7]); %! addvars (T, Visit) %! %! ## Use `'Before'` or `'After'` to place the new variable at a chosen %! ## position. %! addvars (T, logical ([1; 0; 1]), 'After', 'Age', ... %! 'NewVariableNames', {'Smoker'}) pr0m1th3as-datatypes-9450b29/inst/demos/table.anymissing000066400000000000000000000010221527370255000231470ustar00rootroot00000000000000%!demo %! ## `anymissing` is a fast scalar check: `true` as soon as *any* element of %! ## the table is missing, across every variable and type (`NaN` for numeric, %! ## `NaT` for datetime, `` for string, `` for %! ## categorical). %! %! Name = string ({'Li'; 'Diaz'; 'Brown'}); %! Age = [38; 43; 40]; %! Grade = categorical ({'A'; ''; 'B'}); %! T = table (Name, Age, Grade) %! anymissing (T) %! %! ## A table with no gaps of any kind returns `false`. %! anymissing (table (string ({'Li'; 'Diaz'}), [38; 40])) pr0m1th3as-datatypes-9450b29/inst/demos/table.convertvars000066400000000000000000000010301527370255000233410ustar00rootroot00000000000000%!demo %! ## `convertvars` changes the data type of selected variables, leaving the %! ## rest alone. The target type is given as a name, or as a conversion %! ## function. %! %! T = table ([1; 0; 1], [38; 43; 40], 'VariableNames', {'Smoker', 'Age'}) %! convertvars (T, 'Smoker', 'logical') %! %! ## Select several variables at once and hand them a conversion function. %! T2 = table ({'M'; 'F'; 'M'}, {'NY'; 'CA'; 'MA'}, ... %! 'VariableNames', {'Gender', 'State'}) %! convertvars (T2, {'Gender', 'State'}, 'categorical') pr0m1th3as-datatypes-9450b29/inst/demos/table.fillmissing000066400000000000000000000013221527370255000233110ustar00rootroot00000000000000%!demo %! ## `fillmissing` replaces gaps rather than dropping rows. Fill with a %! ## constant, applied across the (numeric) variables. %! %! T = table ([38; NaN; 40], [71; 69; NaN], 'VariableNames', {'Age', 'Height'}) %! fillmissing (T, 'constant', 0) %!demo %! ## Interpolation rules fill from neighbouring rows and work across types: %! ## `'previous'` carries the last value forward (here through both a %! ## `datetime` and a numeric column), while `'linear'` interpolates numeric %! ## gaps. %! %! Date = datetime (2024, 1, [1; NaN; 3; NaN]); %! Reading = [10; NaN; 30; 40]; %! T = table (Date, Reading) %! fillmissing (T, 'previous') %! %! fillmissing (table ([1; NaN; NaN; 4], 'VariableNames', {'v'}), 'linear') pr0m1th3as-datatypes-9450b29/inst/demos/table.findgroups000066400000000000000000000011221527370255000231470ustar00rootroot00000000000000%!demo %! ## `findgroups` assigns each row an integer group number based on its %! ## grouping variable(s), the setup step for a `splitapply` computation. A %! ## second output returns a table naming each group. %! %! Species = {'setosa'; 'virginica'; 'setosa'; 'virginica'; 'setosa'}; %! Petal = [1.4; 5.1; 1.5; 5.9; 1.3]; %! T = table (Species, Petal) %! [G, groupNames] = findgroups (T(:, 'Species')); %! G' %! groupNames %! %! ## The group numbers index into `groupNames`, and feed straight into %! ## `splitapply` to aggregate `Petal` per species. %! splitapply (@mean, T(:, 'Petal'), G) pr0m1th3as-datatypes-9450b29/inst/demos/table.groupcounts000066400000000000000000000004421527370255000233630ustar00rootroot00000000000000%!demo %! ## `groupcounts` tallies how many rows fall in each group, as a frequency %! ## table. It also reports each group's share as a percentage of the total. %! %! Species = {'setosa'; 'virginica'; 'setosa'; 'virginica'; 'setosa'}; %! T = table (Species) %! groupcounts (T, 'Species') pr0m1th3as-datatypes-9450b29/inst/demos/table.groupfilter000066400000000000000000000007641527370255000233440ustar00rootroot00000000000000%!demo %! ## `groupfilter` keeps the rows belonging to groups that satisfy a predicate; %! ## it filters *whole groups*, not individual rows. Here only species with %! ## more than two observations are retained. %! %! Species = {'setosa'; 'virginica'; 'setosa'; 'virginica'; 'setosa'}; %! Petal = [1.4; 5.1; 1.5; 5.9; 1.3]; %! T = table (Species, Petal) %! groupfilter (T, 'Species', @(x) numel (x) > 2) %! %! ## `setosa` has three rows and stays; `virginica` has two and is dropped %! ## entirely. pr0m1th3as-datatypes-9450b29/inst/demos/table.groupsummary000066400000000000000000000010111527370255000235360ustar00rootroot00000000000000%!demo %! ## `groupsummary` is the one-call grouped aggregation: group by one or more %! ## variables, then apply a summary method to the data variables. It reports %! ## the group count alongside each requested statistic. %! %! Species = {'setosa'; 'virginica'; 'setosa'; 'virginica'; 'setosa'}; %! Petal = [1.4; 5.1; 1.5; 5.9; 1.3]; %! T = table (Species, Petal) %! groupsummary (T, 'Species', 'mean', 'Petal') %! %! ## Several methods can be requested at once. %! groupsummary (T, 'Species', {'min', 'max'}, 'Petal') pr0m1th3as-datatypes-9450b29/inst/demos/table.grouptransform000066400000000000000000000006701527370255000240660ustar00rootroot00000000000000%!demo %! ## `grouptransform` applies a group-wise transform but returns a table of the %! ## *same height* as the input, each value replaced by its transformed %! ## version. Here every petal length is centred on its own species mean. %! %! Species = {'setosa'; 'virginica'; 'setosa'; 'virginica'; 'setosa'}; %! Petal = [1.4; 5.1; 1.5; 5.9; 1.3]; %! T = table (Species, Petal) %! grouptransform (T, 'Species', @(x) x - mean (x), 'Petal') pr0m1th3as-datatypes-9450b29/inst/demos/table.head000066400000000000000000000010621527370255000216730ustar00rootroot00000000000000%!demo %! ## `head` previews the top of a table without you having to know its size. %! ## With no count it returns the first 8 rows (or fewer if the table is %! ## short), which is handy for glancing at a wide, mixed-type table. %! %! n = (1:20)'; %! T = table (string (num2str (n, 'ID%02d')), datetime (2024, 1, n), ... %! categorical (mod (n, 3), 0:2, {'low', 'mid', 'hi'}), n .^ 2, ... %! 'VariableNames', {'Tag', 'Date', 'Band', 'Score'}); %! head (T) %! %! ## Pass a count to ask for a specific number of leading rows. %! head (T, 3) pr0m1th3as-datatypes-9450b29/inst/demos/table.height000066400000000000000000000007151527370255000222460ustar00rootroot00000000000000%!demo %! ## `height` counts rows, independent of how many columns each variable spans. %! ## Here `BloodPressure` occupies two columns, yet the table still has 5 rows. %! %! Age = [38; 43; 38; 40; 49]; %! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80]; %! T = table (Age, BloodPressure) %! height (T) %! %! ## It is the row count you index against, so `height` is the natural bound %! ## for a loop or a final-row reference. %! T(height (T), :) pr0m1th3as-datatypes-9450b29/inst/demos/table.horzcat000066400000000000000000000027061527370255000224520ustar00rootroot00000000000000%!demo %! ## `horzcat` joins tables side by side, keeping all their variables. The %! ## tables must have the same number of rows and distinct variable names. The %! ## `[A, B]` bracket syntax calls it for you, and variables of any type %! ## combine freely. %! %! A = table ([38; 43; 40], 'VariableNames', {'Age'}) %! B = table (categorical ({'M'; 'F'; 'M'}), logical ([1; 0; 1]), ... %! 'VariableNames', {'Gender', 'Smoker'}) %! [A, B] %!demo %! ## Custom properties are carried through the concatenation. A %! ## *variable*-scoped property is concatenated alongside its columns, so the %! ## merged table keeps one entry per variable; a *table*-scoped property is %! ## preserved when the operands agree on it. %! %! A = table ([38; 43], categorical ({'M'; 'F'}), ... %! 'VariableNames', {'Age', 'Gender'}) %! A = addprop (A, {'Study', 'Source'}, {'table', 'variable'}); %! A.Properties.CustomProperties.Study = 'Cohort-7'; %! A.Properties.CustomProperties.Source = {'intake', 'intake'}; %! %! B = table (duration (0, [15; 30], 0), 'VariableNames', {'Wait'}) %! B = addprop (B, {'Study', 'Source'}, {'table', 'variable'}); %! B.Properties.CustomProperties.Study = 'Cohort-7'; %! B.Properties.CustomProperties.Source = {'clock'}; %! %! H = [A, B]; %! ## The variable-scoped Source now spans all three variables ... %! H.Properties.CustomProperties.Source %! ## ... and the shared table-scoped Study is retained. %! H.Properties.CustomProperties.Study pr0m1th3as-datatypes-9450b29/inst/demos/table.inner2outer000066400000000000000000000006261527370255000232530ustar00rootroot00000000000000%!demo %! ## `inner2outer` swaps the nesting of a table whose variables are themselves %! ## tables: the inner (nested) variables are lifted to the top level. %! %! Inner = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'x', 'y'}) %! NT = table ([10; 20; 30], Inner, 'VariableNames', {'id', 'inner'}) %! NT %! %! ## After `inner2outer` the nested `x` and `y` become ordinary variables. %! inner2outer (NT) pr0m1th3as-datatypes-9450b29/inst/demos/table.innerjoin000066400000000000000000000014651527370255000227740ustar00rootroot00000000000000%!demo %! ## `innerjoin` keeps only rows whose key exists in *both* tables, dropping %! ## unmatched rows, and, unlike `join`, the right key need not be unique. %! ## Pass `'Keys'` to match on several variables at once; here a row must agree %! ## on both `Name` and `Year` to match. %! %! L = table ({'Li'; 'Li'; 'Diaz'}, [2023; 2024; 2023], [10; 11; 12], ... %! 'VariableNames', {'Name', 'Year', 'Visits'}) %! R = table ({'Li'; 'Diaz'}, [2023; 2023], categorical ({'NY'; 'CA'}), ... %! 'VariableNames', {'Name', 'Year', 'State'}) %! innerjoin (L, R, 'Keys', {'Name', 'Year'}) %! %! ## Li's 2024 row has no match on the right, so it is excluded. Extra outputs %! ## give the source row indices in the left and right tables. %! [T, iL, iR] = innerjoin (L, R, 'Keys', {'Name', 'Year'}); %! [iL, iR] pr0m1th3as-datatypes-9450b29/inst/demos/table.intersect000066400000000000000000000012341527370255000227730ustar00rootroot00000000000000%!demo %! ## `intersect` returns the rows common to both tables, the set intersection. %! ## A row matches only when *every* variable agrees, so mixed-type rows are %! ## compared field by field. %! %! A = table ({'Li'; 'Diaz'; 'Brown'}, [38; 40; 49], ... %! categorical ({'NY'; 'CA'; 'MA'}), ... %! 'VariableNames', {'Name', 'Age', 'State'}) %! B = table ({'Diaz'; 'Brown'; 'Lee'}, [40; 49; 30], ... %! categorical ({'CA'; 'MA'; 'TX'}), ... %! 'VariableNames', {'Name', 'Age', 'State'}) %! intersect (A, B) %! %! ## The extra outputs locate the shared rows within each input. %! [C, iA, iB] = intersect (A, B); %! [iA, iB] pr0m1th3as-datatypes-9450b29/inst/demos/table.iscolumn000066400000000000000000000007211527370255000226240ustar00rootroot00000000000000%!demo %! ## A table is a *column* when it has a single variable, regardless of how %! ## many columns that variable spans internally. %! %! T1 = table ([1; 2; 3], 'VariableNames', {'x'}) %! iscolumn (T1) %! %! ## The lone variable may itself be multi-column; the table is still a column. %! T2 = table ([1 2; 3 4; 5 6], 'VariableNames', {'xy'}) %! iscolumn (T2) %! %! ## Two or more variables make it no longer a column. %! iscolumn (table ([1; 2; 3], [4; 5; 6])) pr0m1th3as-datatypes-9450b29/inst/demos/table.isempty000066400000000000000000000005331527370255000224660ustar00rootroot00000000000000%!demo %! ## A table is empty when it has no rows **or** no variables. Note that a %! ## table with named variables but zero rows still counts as empty. %! %! T = table ('Size', [0, 3], 'VariableTypes', {'double', 'string', 'double'}) %! isempty (T) %! %! ## Adding a row makes it non-empty. %! T(1, :) = {42, string('S1'), 3.14}; %! isempty (T) pr0m1th3as-datatypes-9450b29/inst/demos/table.ismember000066400000000000000000000013301527370255000225730ustar00rootroot00000000000000%!demo %! ## `ismember` tests, row by row, whether each row of the first table also %! ## occurs in the second, returning a logical column, not a new table. A row %! ## counts as a member only when all of its variables match a row of `B`. %! %! A = table ({'Li'; 'Diaz'; 'Brown'}, [38; 40; 49], ... %! categorical ({'NY'; 'CA'; 'MA'}), ... %! 'VariableNames', {'Name', 'Age', 'State'}) %! B = table ({'Diaz'; 'Brown'; 'Lee'}, [40; 49; 30], ... %! categorical ({'CA'; 'MA'; 'TX'}), ... %! 'VariableNames', {'Name', 'Age', 'State'}) %! ismember (A, B) %! %! ## A second output gives, for each matching row, its position in `B` (0 if %! ## none). %! [TF, loc] = ismember (A, B); %! loc pr0m1th3as-datatypes-9450b29/inst/demos/table.ismissing000066400000000000000000000010661527370255000230030ustar00rootroot00000000000000%!demo %! ## Where `anymissing` gives a single yes/no, `ismissing` locates *every* gap, %! ## returning a logical array the size of the table with one flag per element. %! ## It recognises the missing value proper to each type at once. %! %! Name = string ({'Li'; 'Diaz'; 'Brown'}); %! Age = [38; NaN; 40]; %! Grade = categorical ({'A'; 'B'; ''}); %! Visit = datetime (2024, 1, [5; NaN; 7]); %! T = table (Name, Age, Grade, Visit) %! ismissing (T) %! %! ## Pass an indicator to also treat specific sentinel values as missing. %! ismissing (table ([1; -99; 3]), -99) pr0m1th3as-datatypes-9450b29/inst/demos/table.isrow000066400000000000000000000005051527370255000221360ustar00rootroot00000000000000%!demo %! ## A table is a *row* when it has exactly one row of data, whatever the %! ## number of variables. %! %! T1 = table (38, true, 71, 'VariableNames', {'Age', 'Smoker', 'Height'}) %! isrow (T1) %! %! ## Add a second row and it is no longer a row vector. %! T2 = table ([38; 43], [true; false], [71; 69]) %! isrow (T2) pr0m1th3as-datatypes-9450b29/inst/demos/table.issortedrows000066400000000000000000000010031527370255000235340ustar00rootroot00000000000000%!demo %! ## `issortedrows` tests whether the rows are already in sorted order, using %! ## the same key/direction syntax as `sortrows`, which is handy to skip a %! ## needless sort. %! %! T = table ([1; 3; 2], [10; 20; 30], 'VariableNames', {'Age', 'Height'}) %! issortedrows (T, 'Age') %! %! ## The `Height` column *is* ascending, so a check on that variable succeeds. %! issortedrows (T, 'Height') %! %! ## Directions are checked too: this asks whether `Age` is descending. %! issortedrows (T, 'Age', 'descend') pr0m1th3as-datatypes-9450b29/inst/demos/table.isvector000066400000000000000000000006461527370255000226370ustar00rootroot00000000000000%!demo %! ## A table is a *vector* when it is a single row **or** a single variable, %! ## the table analogue of a 1-by-N or N-by-1 array. %! %! ## One variable, several rows: a column, hence a vector. %! isvector (table ([1; 2; 3])) %! %! ## One row, several variables: a row, hence a vector. %! isvector (table (1, 2, 3)) %! %! ## Multiple rows and multiple variables: not a vector. %! isvector (table ([1; 2], [3; 4])) pr0m1th3as-datatypes-9450b29/inst/demos/table.join000066400000000000000000000012411527370255000217300ustar00rootroot00000000000000%!demo %! ## `join` performs a key-based merge: every row of the left table is matched %! ## to the one row of the right table sharing its key. The key is the %! ## variable the two tables have in common (`Name`), and it must be *unique* %! ## on the right. The remaining right-hand variables, here of several %! ## different types, are appended to each matched left row. %! %! L = table ({'Li'; 'Diaz'; 'Brown'}, [38; 40; 49], ... %! 'VariableNames', {'Name', 'Age'}) %! R = table ({'Li'; 'Diaz'; 'Brown'}, categorical ({'NY'; 'CA'; 'MA'}), ... %! logical ([1; 0; 1]), ... %! 'VariableNames', {'Name', 'State', 'Smoker'}) %! join (L, R) pr0m1th3as-datatypes-9450b29/inst/demos/table.mergevars000066400000000000000000000006031527370255000227650ustar00rootroot00000000000000%!demo %! ## `mergevars` combines several variables into one multi-column variable, the %! ## inverse of `splitvars`. Name the merged variable with %! ## `'NewVariableName'`. %! %! T = table ([38; 43; 40], [124; 109; 125], [93; 77; 83], ... %! 'VariableNames', {'Age', 'Systolic', 'Diastolic'}) %! mergevars (T, {'Systolic', 'Diastolic'}, 'NewVariableName', 'BloodPressure') pr0m1th3as-datatypes-9450b29/inst/demos/table.movevars000066400000000000000000000010221527370255000226300ustar00rootroot00000000000000%!demo %! ## `movevars` reorders variables, placing one (or several) `'Before'` or %! ## `'After'` another. The data is unchanged; only the column order shifts. %! %! Name = string ({'Sanchez'; 'Johnson'; 'Li'}); %! Age = [38; 43; 40]; %! Gender = categorical ({'M'; 'M'; 'F'}); %! Visit = datetime (2024, [1; 2; 3], [5; 6; 7]); %! T = table (Name, Age, Gender, Visit) %! movevars (T, 'Visit', 'After', 'Name') %! %! ## Move a variable to the front by placing it before the first one. %! movevars (T, 'Gender', 'Before', 'Name') pr0m1th3as-datatypes-9450b29/inst/demos/table.numel000066400000000000000000000007421527370255000221160ustar00rootroot00000000000000%!demo %! ## For a table `numel` returns rows times variables, the number of table %! ## *cells*, not the count of underlying data elements. This 3-by-2 table %! ## therefore has 6 cells even though `BloodPressure` holds two columns. %! %! Age = [38; 43; 40]; %! Smoker = logical ([1; 0; 1]); %! BloodPressure = [124, 93; 109, 77; 117, 75]; %! T = table (Age, Smoker, BloodPressure) %! numel (T) %! %! ## It is simply the product of `height` and `width`. %! height (T) * width (T) pr0m1th3as-datatypes-9450b29/inst/demos/table.outerjoin000066400000000000000000000012361527370255000230130ustar00rootroot00000000000000%!demo %! ## `outerjoin` keeps *all* rows from both tables, filling in missing values %! ## where a key has no match; each variable is filled with the missing value %! ## proper to its type (`NaN`, ``, empty string, ...). %! %! L = table ({'Li'; 'Diaz'; 'Brown'}, [38; 40; 49], ... %! 'VariableNames', {'Name', 'Age'}) %! R = table ({'Li'; 'Diaz'; 'Lee'}, categorical ({'NY'; 'CA'; 'TX'}), ... %! [176; 163; 150], ... %! 'VariableNames', {'Name', 'State', 'Weight'}) %! outerjoin (L, R, 'MergeKeys', true) %! %! ## Brown (left only) and Lee (right only) both survive, each with the other %! ## table's variables left missing. pr0m1th3as-datatypes-9450b29/inst/demos/table.pivot000066400000000000000000000015051527370255000221350ustar00rootroot00000000000000%!demo %! ## `pivot` builds a cross-tabulation. With only `'Rows'` it counts the rows %! ## in each group, as a one-way frequency table. %! %! Region = {'N'; 'N'; 'S'; 'S'; 'N'; 'S'}; %! Quarter = categorical ({'Q1'; 'Q2'; 'Q1'; 'Q2'; 'Q1'; 'Q2'}); %! Sales = [10; 20; 30; 40; 15; 25]; %! T = table (Region, Quarter, Sales) %! pivot (T, 'Rows', 'Region') %!demo %! ## Add `'Columns'` to spread a second grouping variable across the columns, %! ## and aggregate a data variable in each cell, here total `Sales` by region %! ## and quarter. %! %! Region = {'N'; 'N'; 'S'; 'S'; 'N'; 'S'}; %! Quarter = categorical ({'Q1'; 'Q2'; 'Q1'; 'Q2'; 'Q1'; 'Q2'}); %! Sales = [10; 20; 30; 40; 15; 25]; %! T = table (Region, Quarter, Sales) %! pivot (T, 'Rows', 'Region', 'Columns', 'Quarter', ... %! 'DataVariable', 'Sales', 'Method', 'sum') pr0m1th3as-datatypes-9450b29/inst/demos/table.removevars000066400000000000000000000006521527370255000231670ustar00rootroot00000000000000%!demo %! ## `removevars` drops variables, selected by name or by position index. %! %! Name = string ({'Sanchez'; 'Johnson'; 'Li'}); %! Age = [38; 43; 40]; %! Gender = categorical ({'M'; 'M'; 'F'}); %! Visit = datetime (2024, [1; 2; 3], [5; 6; 7]); %! T = table (Name, Age, Gender, Visit) %! removevars (T, 'Gender') %! %! ## Several variables can go at once, here selected by their column numbers. %! removevars (T, [2, 4]) pr0m1th3as-datatypes-9450b29/inst/demos/table.renamevars000066400000000000000000000006221527370255000231360ustar00rootroot00000000000000%!demo %! ## `renamevars` changes variable names in place, matching old names to new %! ## ones position by position. It touches only the names; the data, of %! ## whatever type, is untouched. %! %! Nm = string ({'Sanchez'; 'Johnson'; 'Li'}); %! Yrs = [38; 43; 40]; %! Sex = categorical ({'M'; 'M'; 'F'}); %! T = table (Nm, Yrs, Sex) %! renamevars (T, {'Nm', 'Yrs', 'Sex'}, {'Name', 'Age', 'Gender'}) pr0m1th3as-datatypes-9450b29/inst/demos/table.repelem000066400000000000000000000006661527370255000224340ustar00rootroot00000000000000%!demo %! ## `repelem` repeats each row in place. With a scalar count every row is %! ## duplicated the same number of times, carrying all variable types along. %! %! Name = string ({'Li'; 'Diaz'; 'Brown'}); %! Grade = categorical ({'A'; 'B'; 'A'}); %! T = table (Name, Grade) %! repelem (T, 2, 1) %! %! ## A vector count sets the repeat count per row: here row 1 once, row 2 %! ## twice, row 3 three times. %! repelem (T, [1; 2; 3], 1) pr0m1th3as-datatypes-9450b29/inst/demos/table.repmat000066400000000000000000000004561527370255000222700ustar00rootroot00000000000000%!demo %! ## `repmat` tiles a whole table, repeating the full block of rows. Contrast %! ## with `repelem`, which repeats each row in place: `repmat` copies the %! ## sequence. %! %! Name = string ({'Li'; 'Diaz'}); %! Visit = datetime (2024, 1, [5; 6]); %! T = table (Name, Visit) %! repmat (T, 3, 1) pr0m1th3as-datatypes-9450b29/inst/demos/table.rmmissing000066400000000000000000000011321527370255000230000ustar00rootroot00000000000000%!demo %! ## `rmmissing` drops every row that has a missing value in any variable %! ## (listwise deletion), counting gaps of every type (`NaN`, `NaT`, %! ## ``, ``). Here the rows with a missing age, grade, or %! ## visit all go. %! %! Name = string ({'Li'; 'Diaz'; 'Brown'; 'Lee'}); %! Age = [38; NaN; 40; 49]; %! Grade = categorical ({'A'; 'B'; ''; 'C'}); %! Visit = datetime (2024, 1, [5; 6; 7; 8]); %! T = table (Name, Age, Grade, Visit) %! rmmissing (T) %! %! ## A second output is the logical mask of the rows that were removed. %! [C, removed] = rmmissing (T); %! removed' pr0m1th3as-datatypes-9450b29/inst/demos/table.rmprop000066400000000000000000000012771527370255000223210ustar00rootroot00000000000000%!demo %! ## `rmprop` removes custom properties previously added with `addprop`, %! ## whatever their scope. Here a table starts with one property of each kind. %! %! Age = [38; 43]; %! Gender = categorical ({'M'; 'F'}); %! T = table (Age, Gender) %! T = addprop (T, {'DataSource', 'Instrument'}, {'table', 'variable'}); %! T.Properties.CustomProperties.DataSource = 'Clinic A'; %! T.Properties.CustomProperties.Instrument = {'form', 'form'}; %! T.Properties.CustomProperties %! %! ## Remove the variable-scoped `Instrument`; the table-scoped `DataSource` %! ## remains. The built-in properties (names, units, ...) are never affected. %! T = rmprop (T, 'Instrument'); %! T.Properties.CustomProperties pr0m1th3as-datatypes-9450b29/inst/demos/table.rowfun000066400000000000000000000005131527370255000223120ustar00rootroot00000000000000%!demo %! ## `rowfun` applies a function to each row, passing that row's variables as %! ## separate arguments: a row-wise map, complementing the column-wise %! ## `varfun`. %! %! Weight = [176; 163; 133]; %! Height = [71; 69; 64]; %! T = table (Weight, Height) %! rowfun (@(w, h) 703 * w / h^2, T, 'OutputVariableNames', {'BMI'}) pr0m1th3as-datatypes-9450b29/inst/demos/table.rows2vars000066400000000000000000000005661527370255000227520ustar00rootroot00000000000000%!demo %! ## `rows2vars` transposes a table: the original variables become rows and the %! ## original rows become variables. The old variable names move into a new %! ## leading variable, and the row names (if any) become the new headers. %! %! Age = [38; 43; 40]; %! Height = [71; 69; 64]; %! T = table (Age, Height, 'RowNames', {'Li', 'Diaz', 'Brown'}) %! rows2vars (T) pr0m1th3as-datatypes-9450b29/inst/demos/table.setdiff000066400000000000000000000011561527370255000224220ustar00rootroot00000000000000%!demo %! ## `setdiff` returns the rows that are in the first table but not the second, %! ## comparing whole multi-variable rows across all their types. %! %! A = table ({'Li'; 'Diaz'; 'Brown'}, [38; 40; 49], ... %! categorical ({'NY'; 'CA'; 'MA'}), ... %! 'VariableNames', {'Name', 'Age', 'State'}) %! B = table ({'Diaz'; 'Brown'; 'Lee'}, [40; 49; 30], ... %! categorical ({'CA'; 'MA'; 'TX'}), ... %! 'VariableNames', {'Name', 'Age', 'State'}) %! setdiff (A, B) %! %! ## Only Li is unique to `A`; a second output gives its row index in `A`. %! [C, iA] = setdiff (A, B); %! iA pr0m1th3as-datatypes-9450b29/inst/demos/table.setxor000066400000000000000000000011531527370255000223170ustar00rootroot00000000000000%!demo %! ## `setxor` returns the symmetric difference: rows in exactly one of the two %! ## tables, but not in both. Row identity spans every variable, of any type. %! %! A = table ({'Li'; 'Diaz'; 'Brown'}, [38; 40; 49], ... %! categorical ({'NY'; 'CA'; 'MA'}), ... %! 'VariableNames', {'Name', 'Age', 'State'}) %! B = table ({'Diaz'; 'Brown'; 'Lee'}, [40; 49; 30], ... %! categorical ({'CA'; 'MA'; 'TX'}), ... %! 'VariableNames', {'Name', 'Age', 'State'}) %! setxor (A, B) %! %! ## Diaz and Brown, shared by both, drop out, leaving Li (from `A`) and Lee %! ## (from `B`). pr0m1th3as-datatypes-9450b29/inst/demos/table.size000066400000000000000000000007411527370255000217470ustar00rootroot00000000000000%!demo %! ## `size` reports `[height, width]`, rows by *variables*. A variable that %! ## spans several columns still counts as one, so the second element matches %! ## `width`, not the total number of underlying columns. %! %! Age = [38; 43; 38; 40; 49]; %! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80]; %! T = table (Age, BloodPressure) %! size (T) %! %! ## Query a single dimension, or capture both at once. %! nRows = size (T, 1) %! [nRows, nVars] = size (T) pr0m1th3as-datatypes-9450b29/inst/demos/table.sortrows000066400000000000000000000017661527370255000227070ustar00rootroot00000000000000%!demo %! ## `sortrows` orders the rows by one or more variables of any comparable %! ## type. Name the variables to sort by; a trailing direction (or one per %! ## key) flips ascending to descending. Here an *ordinal* categorical `Grade` %! ## sets the primary order, with a `datetime` breaking ties. %! %! Name = string ({'Li'; 'Diaz'; 'Brown'; 'Lee'}); %! Visit = datetime (2024, [3; 1; 2; 1], [7; 5; 6; 2]); %! Grade = categorical ({'B'; 'A'; 'A'; 'B'}, {'A', 'B', 'C'}, 'Ordinal', true); %! T = table (Name, Visit, Grade) %! sortrows (T, {'Grade', 'Visit'}) %! %! ## Sort the same table by `Grade` descending instead. %! sortrows (T, 'Grade', 'descend') %!demo %! ## A second output returns the permutation index, and passing `'RowNames'` %! ## sorts by the row names themselves rather than by a data variable. %! %! Age = [38; 43; 38; 40]; %! Smoker = logical ([1; 0; 1; 0]); %! T = table (Age, Smoker, 'RowNames', {'Sanchez', 'Johnson', 'Li', 'Diaz'}) %! [S, idx] = sortrows (T, 'RowNames'); %! S %! idx pr0m1th3as-datatypes-9450b29/inst/demos/table.splitapply000066400000000000000000000011741527370255000231770ustar00rootroot00000000000000%!demo %! ## `splitapply` divides a table by the group numbers from `findgroups`, %! ## applies a function to each group, and concatenates the results. Here it %! ## computes a mean petal length per species. %! %! Species = {'setosa'; 'virginica'; 'setosa'; 'virginica'; 'setosa'}; %! Petal = [1.4; 5.1; 1.5; 5.9; 1.3]; %! T = table (Species, Petal) %! G = findgroups (T(:, 'Species')); %! splitapply (@mean, T(:, 'Petal'), G) %! %! ## The applied function may return several outputs, one column each, and can %! ## read several variables at once. %! [lo, hi] = splitapply (@(x) deal (min (x), max (x)), T(:, 'Petal'), G); %! [lo, hi] pr0m1th3as-datatypes-9450b29/inst/demos/table.splitvars000066400000000000000000000007541527370255000230300ustar00rootroot00000000000000%!demo %! ## `splitvars` expands a multi-column variable into several single-column %! ## variables, the inverse of `mergevars`. Auto-generated names get a numeric %! ## suffix; pass `'NewVariableNames'` to choose your own. %! %! BloodPressure = [124, 93; 109, 77; 125, 83]; %! T = table ([38; 43; 40], BloodPressure, 'VariableNames', {'Age', 'BP'}) %! splitvars (T, 'BP') %! %! ## Name the resulting columns explicitly. %! splitvars (T, 'BP', 'NewVariableNames', {'Systolic', 'Diastolic'}) pr0m1th3as-datatypes-9450b29/inst/demos/table.stack000066400000000000000000000011461527370255000221020ustar00rootroot00000000000000%!demo %! ## `stack` reshapes wide data to tall: it stacks several data variables into %! ## a single variable and adds an indicator recording which one each value %! ## came from. Here the monthly columns collapse into one `Value` column. %! %! Name = {'Li'; 'Diaz'}; %! Jan = [1; 2]; %! Feb = [3; 4]; %! T = table (Name, Jan, Feb) %! stack (T, {'Jan', 'Feb'}, ... %! 'NewDataVariableName', 'Value', 'IndexVariableName', 'Month') %! %! ## The unstacked variables (`Name`) are repeated down each group; the %! ## indicator `Month` is a categorical naming the source column. `unstack` %! ## reverses this. pr0m1th3as-datatypes-9450b29/inst/demos/table.standardizeMissing000066400000000000000000000007411527370255000246370ustar00rootroot00000000000000%!demo %! ## `standardizeMissing` turns your own sentinel codes into the *standard* %! ## missing value for each type (`NaN` here), so later %! ## `ismissing`/`rmmissing`/ `fillmissing` calls recognise them. This dataset %! ## uses `-99` for "no data". %! %! T = table ([38; -99; 40], [71; 69; -99], 'VariableNames', {'Age', 'Height'}) %! S = standardizeMissing (T, -99) %! %! ## The sentinels are now genuine missing values, ready to be filled or %! ## dropped. %! rmmissing (S) pr0m1th3as-datatypes-9450b29/inst/demos/table.summary000066400000000000000000000007201527370255000224670ustar00rootroot00000000000000%!demo %! ## Create a Table from patient data %! %! load patients %! BloodPressure = [Systolic Diastolic]; %! T = table (Gender, Age, Smoker, BloodPressure, 'RowNames', LastName); %! %! ## Add descriptions and units to table %! %! T.Properties.Description = 'Simulated patient data'; %! T.Properties.VariableUnits = {'', 'Yrs', '', 'mm Hg'}; %! T.Properties.VariableDescriptions(4) = {'Systolic/Diastolic'}; %! %! ## Print a summary of the table %! %! summary (T) pr0m1th3as-datatypes-9450b29/inst/demos/table.table000066400000000000000000000101521527370255000220610ustar00rootroot00000000000000%!demo %! ## Preallocate a table by specifying its size and the variable data types %! %! sz = [4, 3]; %! varTypes = {'double', 'datetime', 'string'}; %! T = table ('Size', sz, 'VariableTypes', varTypes) %!demo %! ## Specify variable names with the `VariableNames` name-value pair argument %! sz = [4, 3]; %! varTypes = {'double', 'datetime', 'string'}; %! varNames = {'Temperature', 'Time', 'Station'}; %! T2 = table ('Size', sz, 'VariableTypes', varTypes, 'VariableNames', varNames) %! %! ## Add rows of data to the first two rows of table T2 %! T2(1,:) = {75, datetime(2024, 2, 5), string('S1')}; %! T2(2,:) = {75, datetime(2024, 2, 6), string('S2')} %!demo %! ## Create a table from various types of arrays %! %! T = table (string ({'M';'F';'M'}), [45;32;34], ... %! {'NY';'CA';'MA'}, logical ([1;0;0]), ... %! 'VariableNames', {'Gender', 'Age', 'State', 'Vote'}) %!demo %! ## Create the same table using the state names as row names %! %! T = table (string ({'M';'F';'M'}), [45;32;34], logical ([1;0;0]), ... %! 'VariableNames', {'Gender', 'Age', 'Vote'}, ... %! 'RowNames', {'NY';'CA';'MA'}) %!demo %! ## Display a table with mixed cell arrays as unicolumnar variables %! ## and other types as multicolumnar variables %! %! Data_A = {[34, 32]; ['text';'picture']; 'text'; struct('c', 'data'); ... %! [true, false]; ['some','text']; {'some','text'}; 25.34}; %! Data_B = {32, 25; 0.2, 135; 0.123, 456; 42, 5; 154, 12; 32, 10; 4, 4; 9, 94}; %! Data_C = datetime (2000, [1:8;9:16]', 1); %! %! T = table (Data_A, Data_B, Data_C) %!demo %! ## Create a nested table %! T1 = table ([1; 2; 3], [4; 5; 6], [7; 8; 9]) %! T2 = table ({'a'; 'b'; 'c'}, {'d'; 'e'; 'f'}, {'g'; 'h'; 'i'}) %! NT = table ([1; 2; 3], T1, [4; 5; 6], T2, {5; 6; 7}, ... %! 'VariableNames', {'A', 'B', 'C', 'D', 'E'}) %!demo %! ## A table is *read* with three complementary indexing styles. Build a table %! ## whose variables span several types to see how each one behaves. %! %! Name = string ({'Sanchez'; 'Johnson'; 'Li'; 'Diaz'}); %! Age = [38; 43; 38; 40]; %! Visit = datetime (2024, [1; 2; 3; 4], [5; 6; 7; 8]); %! Gender = categorical ({'M'; 'M'; 'F'; 'F'}); %! T = table (Name, Age, Visit, Gender) %! %! ## Dot indexing extracts one variable as its own native type. %! T.Visit %! %! ## Parenthesis indexing selects a sub-table; the result is still a table. %! T(1:2, {'Name', 'Age'}) %! %! ## Brace indexing pulls the raw contents out of the selected variables. %! T{1:2, 'Age'} %!demo %! ## Assignment mirrors the three styles. Starting from a mixed-type table: %! %! Name = string ({'Sanchez'; 'Johnson'; 'Li'}); %! Age = [38; 43; 38]; %! Gender = categorical ({'M'; 'M'; 'F'}); %! T = table (Name, Age, Gender) %! %! ## Dot assignment adds or overwrites a whole variable (here computed from %! ## Age). %! T.Senior = T.Age >= 40 %! %! ## Parenthesis assignment with a cell sets a sub-table; a new row index grows %! ## the table, taking one cell per variable in variable order. %! T(4, :) = {string('Diaz'), 40, categorical({'F'}), true} %! %! ## Dot-then-index writes into part of an existing variable. %! T.Age(1) = 39; %! T.Age' %!demo %! ## This implementation fully supports **chained** referencing and assignment: %! ## indexing that reaches through several levels in a single expression. %! ## (Octave extension: MATLAB permits chained *reference* but not chained %! ## *assignment*.) %! %! Age = [38; 43; 38; 40]; %! Height = [71; 69; 64; 67]; %! T = table (Age, Height) %! %! ## Chained reference: select a sub-table, then a variable, then an element. %! T(2:3, :).Age(1) %! %! ## Chained assignment writes straight through that same path. %! T(2:3, :).Age(1) = 99; %! T.Age' %!demo %! ## Chaining is most useful with nested tables, reaching an inner variable %! ## directly, again both for reading and, unlike MATLAB, for writing. %! %! Inner = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'p', 'q'}) %! NT = table ([10; 20; 30], Inner, 'VariableNames', {'id', 'inner'}) %! %! ## Read the 2nd element of inner variable q. %! NT.inner.q(2) %! %! ## Assign straight into it in one chained expression. %! NT.inner.q(2) = 55; %! NT.inner pr0m1th3as-datatypes-9450b29/inst/demos/table.table2array000066400000000000000000000010041527370255000231760ustar00rootroot00000000000000%!demo %! ## `table2array` horizontally concatenates the variables into a single array, %! ## so it is meant for tables whose variables share a common type. %! ## Multi-column variables keep all their columns, so the result can be wider %! ## than the table. %! %! Age = [38; 43; 40]; %! BloodPressure = [124, 93; 109, 77; 117, 75]; %! T = table (Age, BloodPressure) %! A = table2array (T) %! %! ## The 3-by-2 table becomes a 3-by-3 numeric matrix, because `BloodPressure` %! ## contributes two columns. %! size (A) pr0m1th3as-datatypes-9450b29/inst/demos/table.table2cell000066400000000000000000000007531527370255000230110ustar00rootroot00000000000000%!demo %! ## `table2cell` unpacks a table into a cell array with one cell per element, %! ## preserving heterogeneous variable types that `table2array` could not %! ## merge. %! %! LastName = {'Sanchez'; 'Johnson'; 'Li'}; %! Age = [38; 43; 38]; %! Smoker = logical ([1; 0; 1]); %! T = table (LastName, Age, Smoker) %! C = table2cell (T) %! %! ## Each column of the cell array corresponds to one table variable, so the %! ## result is independent of the original variable types. %! class (C) pr0m1th3as-datatypes-9450b29/inst/demos/table.table2csv000066400000000000000000000011661527370255000226640ustar00rootroot00000000000000%!demo %! ## `table2csv` writes a table to CSV in the package's round-trippable format: %! ## a metadata comment line, then header rows for the variable types, names, %! ## and units, followed by one row of data per table row. %! %! Age = [38; 43; 40]; %! Height = [71; 69; 64]; %! T = table (Age, Height) %! T.Properties.VariableUnits = {'Yrs', 'in'}; %! %! filename = fullfile (tempdir (), 'patients.csv'); %! table2csv (T, filename); %! type (filename) %! %! ## Because the type and unit metadata is preserved in the header, `csv2table` %! ## reconstructs the table faithfully. %! csv2table (filename) %! %! delete (filename); pr0m1th3as-datatypes-9450b29/inst/demos/table.table2ods000066400000000000000000000015301527370255000226510ustar00rootroot00000000000000%!demo %! ## `table2ods` writes a table to an OpenDocument spreadsheet, one natively %! ## typed cell per value. The data lands on `Sheet1` by default. %! %! Age = [38; 43; 40]; %! Height = [71; 69; 64]; %! T = table (Age, Height) %! filename = fullfile (tempdir (), 'patients.ods'); %! table2ods (T, filename); %! ods2table (filename) %!demo %! ## Writing to a named `'Sheet'` of an existing workbook adds or replaces just %! ## that sheet, leaving the others intact, so several tables can share one %! ## file. %! %! filename = fullfile (tempdir (), 'workbook.ods'); %! table2ods (table ([38; 43], 'VariableNames', {'Age'}), filename, ... %! 'Sheet', 'Patients'); %! table2ods (table ([1; 2; 3], 'VariableNames', {'Visit'}), filename, ... %! 'Sheet', 'Visits'); %! ods2table (filename, 'Sheet', 'Visits') %! %! delete (filename); pr0m1th3as-datatypes-9450b29/inst/demos/table.table2struct000066400000000000000000000014101527370255000234050ustar00rootroot00000000000000%!demo %! ## By default `table2struct` returns a **struct array**, one element per row, %! ## with one field per variable, which is handy for row-wise iteration. %! %! LastName = {'Sanchez'; 'Johnson'; 'Li'}; %! Age = [38; 43; 38]; %! Height = [71; 69; 64]; %! T = table (LastName, Age, Height) %! S = table2struct (T); %! S(2).LastName %! S(2).Age %!demo %! ## Pass `'ToScalar', true` to instead get a **scalar struct** whose fields %! ## are whole columns, the layout used by `struct2table` with %! ## `'AsArray', true`. %! %! LastName = {'Sanchez'; 'Johnson'; 'Li'}; %! Age = [38; 43; 38]; %! Height = [71; 69; 64]; %! T = table (LastName, Age, Height) %! S = table2struct (T, 'ToScalar', true) %! %! ## Now each field holds every value of that variable at once. %! S.Age pr0m1th3as-datatypes-9450b29/inst/demos/table.tail000066400000000000000000000007471527370255000217340ustar00rootroot00000000000000%!demo %! ## `tail` mirrors `head`, previewing the *bottom* of a table. With no count %! ## it returns the last 8 rows (or fewer if the table is short). %! %! n = (1:20)'; %! T = table (string (num2str (n, 'ID%02d')), datetime (2024, 1, n), ... %! categorical (mod (n, 3), 0:2, {'low', 'mid', 'hi'}), n .^ 2, ... %! 'VariableNames', {'Tag', 'Date', 'Band', 'Score'}); %! tail (T) %! %! ## Pass a count to ask for a specific number of trailing rows. %! tail (T, 3) pr0m1th3as-datatypes-9450b29/inst/demos/table.topkrows000066400000000000000000000011161527370255000226620ustar00rootroot00000000000000%!demo %! ## `topkrows` returns the top `k` rows by a sort key without sorting the %! ## whole table, like `sortrows` followed by `head`, but stated in one call. %! ## The default direction is descending, so the largest values come first. %! %! LastName = {'Sanchez'; 'Johnson'; 'Li'; 'Diaz'; 'Brown'}; %! Age = [38; 43; 38; 40; 49]; %! T = table (Age, 'RowNames', LastName) %! topkrows (T, 3, 'Age') %! %! ## Add a direction to take the *smallest* instead, and capture the index of %! ## the selected rows in the original table. %! [B, idx] = topkrows (T, 2, 'Age', 'ascend'); %! B %! idx pr0m1th3as-datatypes-9450b29/inst/demos/table.union000066400000000000000000000013321527370255000221220ustar00rootroot00000000000000%!demo %! ## `union` returns the rows present in either table, with duplicates removed: %! ## set arithmetic over *whole rows*, comparing all variables together. Both %! ## tables must share the same variables; here each row spans three types. %! %! A = table ({'Li'; 'Diaz'; 'Brown'}, [38; 40; 49], ... %! categorical ({'NY'; 'CA'; 'MA'}), ... %! 'VariableNames', {'Name', 'Age', 'State'}) %! B = table ({'Diaz'; 'Brown'; 'Lee'}, [40; 49; 30], ... %! categorical ({'CA'; 'MA'; 'TX'}), ... %! 'VariableNames', {'Name', 'Age', 'State'}) %! union (A, B) %! %! ## Diaz and Brown match on every variable, so they appear once; the result %! ## has four distinct rows out of the six inputs. pr0m1th3as-datatypes-9450b29/inst/demos/table.unique000066400000000000000000000011061527370255000222770ustar00rootroot00000000000000%!demo %! ## `unique` removes duplicate rows, comparing whole rows across all variables %! ## regardless of their types. By default the surviving rows come back %! ## sorted. %! %! Gender = categorical ({'M'; 'F'; 'M'; 'F'; 'M'}); %! State = string ({'NY'; 'CA'; 'NY'; 'CA'; 'NY'}); %! Visits = [1; 2; 1; 3; 1]; %! T = table (Gender, State, Visits) %! unique (T) %! %! ## Use `'stable'` to keep the rows in their order of first appearance %! ## instead, and capture the index vectors relating the inputs to the unique %! ## rows. %! [U, ia, ic] = unique (T, 'stable'); %! U %! ic' pr0m1th3as-datatypes-9450b29/inst/demos/table.unstack000066400000000000000000000010001527370255000224320ustar00rootroot00000000000000%!demo %! ## `unstack` reshapes tall data to wide, the inverse of `stack`. It spreads %! ## the values of one data variable across new columns, one per level of an %! ## indicator variable, grouping by the remaining variables. %! %! Name = categorical ({'Li'; 'Li'; 'Diaz'; 'Diaz'}); %! Month = categorical ({'Jan'; 'Feb'; 'Jan'; 'Feb'}); %! Value = [1; 3; 2; 4]; %! T = table (Name, Month, Value) %! unstack (T, 'Value', 'Month') %! %! ## Each distinct `Month` becomes its own column, with one row per `Name`. pr0m1th3as-datatypes-9450b29/inst/demos/table.varfun000066400000000000000000000006231527370255000222750ustar00rootroot00000000000000%!demo %! ## `varfun` applies a function to each variable of a table, as a column-wise %! ## map. The result is itself a table, its variable names prefixed by the %! ## function. %! %! T = table ([38; 43; 40], [71; 69; 64], 'VariableNames', {'Age', 'Height'}) %! varfun (@mean, T) %! %! ## Ask for a plain array instead of a table with `'OutputFormat'`. %! varfun (@mean, T, 'OutputFormat', 'uniform') pr0m1th3as-datatypes-9450b29/inst/demos/table.vertcat000066400000000000000000000025741527370255000224530ustar00rootroot00000000000000%!demo %! ## `vertcat` stacks tables on top of one another, appending rows. The tables %! ## must share the same variables (same names, compatible types). The %! ## `[A; B]` bracket syntax calls it for you. %! %! A = table (string ({'Li'; 'Diaz'}), [38; 40], categorical ({'M'; 'F'}), ... %! 'VariableNames', {'Name', 'Age', 'Gender'}) %! B = table (string ({'Brown'; 'Lee'}), [49; 30], categorical ({'M'; 'M'}), ... %! 'VariableNames', {'Name', 'Age', 'Gender'}) %! [A; B] %!demo %! ## Because vertical concatenation only adds rows, the column-aligned metadata %! ## is unchanged: a *variable*-scoped custom property carries straight %! ## through, and a *table*-scoped one is kept when the operands agree. %! %! A = table ([38; 40], categorical ({'M'; 'F'}), ... %! 'VariableNames', {'Age', 'Gender'}) %! A = addprop (A, {'Study', 'Source'}, {'table', 'variable'}); %! A.Properties.CustomProperties.Study = 'Cohort-7'; %! A.Properties.CustomProperties.Source = {'intake', 'intake'}; %! %! B = table ([49; 30], categorical ({'M'; 'M'}), ... %! 'VariableNames', {'Age', 'Gender'}) %! B = addprop (B, {'Study', 'Source'}, {'table', 'variable'}); %! B.Properties.CustomProperties.Study = 'Cohort-7'; %! B.Properties.CustomProperties.Source = {'intake', 'intake'}; %! %! V = [A; B]; %! V.Properties.CustomProperties.Source %! V.Properties.CustomProperties.Study pr0m1th3as-datatypes-9450b29/inst/demos/table.width000066400000000000000000000007171527370255000221170ustar00rootroot00000000000000%!demo %! ## `width` counts *variables*, not the underlying columns. `BloodPressure` %! ## is a single variable spanning two columns, so the width is 2, not 3. %! %! Age = [38; 43; 38; 40; 49]; %! Smoker = logical ([1; 0; 1; 0; 1]); %! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80]; %! T = table (Age, Smoker, BloodPressure) %! width (T) %! %! ## Splitting the multi-column variable into scalar columns raises the width. %! width (splitvars (T)) pr0m1th3as-datatypes-9450b29/inst/demos/table.writetable000066400000000000000000000017711527370255000231430ustar00rootroot00000000000000%!demo %! ## `writetable` writes a table in a **MATLAB-interoperable** format: a plain %! ## header row of variable names followed by the data, with no %! ## package-specific metadata. The file type follows the extension; %! ## `'Delimiter'` sets the separator for text files. %! %! T = table ([38; 43; 40], [71; 69; 64], 'VariableNames', {'Age', 'Height'}) %! filename = fullfile (tempdir (), 'patients.txt'); %! writetable (T, filename, 'Delimiter', 'tab'); %! type (filename) %! %! delete (filename); %!demo %! ## For spreadsheets, choose the target `'Sheet'` and, with `'WriteRowNames'`, %! ## emit the row names as the leading column; `readtable` reads it straight %! ## back. %! %! T = table ([38; 43], [71; 69], 'VariableNames', {'Age', 'Height'}, ... %! 'RowNames', {'Li', 'Diaz'}) %! filename = fullfile (tempdir (), 'patients.ods'); %! writetable (T, filename, 'Sheet', 'Cohort', 'WriteRowNames', true); %! readtable (filename, 'Sheet', 'Cohort', 'ReadRowNames', true) %! %! delete (filename); pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.addevent000066400000000000000000000005261527370255000236520ustar00rootroot00000000000000%!demo %! ## An event marks a time in a series, by name. It may be given as a %! ## `tsdata.event` object, which can carry data of its own. %! %! ts = timeseries ((1:10)', 0:9); %! ts = addevent (ts, 'start', 2); %! e = tsdata.event ('stop', 7); %! e.EventData = 'valve closed'; %! ts = addevent (ts, e); %! {ts.Events.Name; ts.Events.Time} pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.addsample000066400000000000000000000012661527370255000240140ustar00rootroot00000000000000%!demo %! ## New samples are put in time order; one at a time already present goes %! ## after the samples there. %! %! ts = timeseries ([10; 20; 30], [0; 1; 2]); %! ts = addsample (ts, 'Data', [15; 25], 'Time', [0.5; 1]); %! [ts.Time, ts.Data] %!demo %! ## A series with quality codes gives each new sample the code of the %! ## nearest sample, unless `'Quality'` is given. `'OverwriteFlag'` %! ## replaces the samples at a time instead of adding one. %! %! ts = timeseries ([10; 20; 30], [0; 1; 2], [1; 2; 3]); %! s = addsample (ts, 'Data', 99, 'Time', 1.8); %! [s.Time, s.Data, s.Quality] %! s = addsample (ts, 'Data', 99, 'Time', 1, 'OverwriteFlag', true); %! [s.Time, s.Data, s.Quality] pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.append000066400000000000000000000010141527370255000233200ustar00rootroot00000000000000%!demo %! ## `append` joins series end to end. Each must start at or after the end %! ## of the one before; where two meet at a time, that time repeats. %! %! a = timeseries ([1; 2; 3], [0; 1; 2]); %! b = timeseries ([4; 5], [2; 3]); %! ts = append (a, b); %! [ts.Time, ts.Data] %!demo %! ## Times in different units are converted to the coarser. %! %! a = timeseries ([1; 2], [0; 30]); %! b = timeseries ([3; 4], [1; 2]); %! b.TimeInfo.Units = 'minutes'; %! ts = append (a, b); %! ts.TimeInfo.Units %! [ts.Time, ts.Data] pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.delevent000066400000000000000000000004171527370255000236650ustar00rootroot00000000000000%!demo %! ## `delevent` removes events by name; a number chooses one occurrence of %! ## a repeated name. %! %! ts = timeseries ((1:10)', 0:9); %! ts = addevent (ts, {'mark', 'mark', 'peak'}, [2, 5, 7]); %! s = delevent (ts, 'mark', 2); %! {s.Events.Name; s.Events.Time} pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.delsample000066400000000000000000000004621527370255000240250ustar00rootroot00000000000000%!demo %! ## Samples are removed by position with `'Index'`, or by time with %! ## `'Value'`, which removes every sample at that time. %! %! ts = timeseries ([10; 20; 30; 40], [0; 1; 1; 2]); %! s = delsample (ts, 'Index', [1, 4]); %! [s.Time, s.Data] %! s = delsample (ts, 'Value', 1); %! [s.Time, s.Data] pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.detrend000066400000000000000000000004201527370255000234760ustar00rootroot00000000000000%!demo %! ## `detrend` removes the mean, or the best line, fitted against the %! ## times, so irregular times are handled correctly. %! %! ts = timeseries ([2; 3; 8; 13], [0; 1; 2; 6]); %! s = detrend (ts, 'linear'); %! s.Data' %! s = detrend (ts, 'constant'); %! s.Data' pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.filter000066400000000000000000000003611527370255000233420ustar00rootroot00000000000000%!demo %! ## `filter` applies a digital filter along time, here a three-point %! ## moving average. The times must be uniform. %! %! ts = timeseries ([3; 6; 9; 12; 9; 6; 3], 0:6); %! s = filter (ts, ones (1, 3) / 3, 1); %! [s.Time, s.Data] pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.getabstime000066400000000000000000000004711527370255000242030ustar00rootroot00000000000000%!demo %! ## `getabstime` writes the date of each sample, in `TimeInfo.Format` when %! ## it is set. %! %! ts = timeseries ([1; 2; 3], [0; 6; 12]); %! ts.TimeInfo.Units = 'hours'; %! ts.TimeInfo.StartDate = '01-Mar-2024 06:00:00'; %! getabstime (ts) %! ts.TimeInfo.Format = 'yyyy-mm-dd HH:MM'; %! getabstime (ts) pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.getdatasamples000066400000000000000000000003431527370255000250530ustar00rootroot00000000000000%!demo %! ## `getdatasamples` returns the data of the samples asked for, in the %! ## order asked, repeats included. %! %! ts = timeseries ([10; 20; 30; 40; 50], 'Name', 'level'); %! ts.Data' %! getdatasamples (ts, [4, 1, 1])' pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.getqualitydesc000066400000000000000000000004021527370255000251000ustar00rootroot00000000000000%!demo %! ## `getqualitydesc` reads each sample's quality code against %! ## `QualityInfo`. %! %! ts = timeseries ([1; 2; 3], [0; 1; 2], [0; 1; 0]); %! ts.QualityInfo.Code = [0, 1]; %! ts.QualityInfo.Description = {'good', 'suspect'}; %! getqualitydesc (ts) pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.getsamples000066400000000000000000000005721527370255000242250ustar00rootroot00000000000000%!demo %! ## `getsamples` returns a series of the samples asked for, in time order, %! ## with their times and quality codes. A logical mask works too. %! %! ts = timeseries ([10; 20; 30; 40], [0; 1; 2; 3], [1; 1; 0; 1]); %! [ts.Time, ts.Data, ts.Quality] %! s = getsamples (ts, [3, 1]); %! [s.Time, s.Data, s.Quality] %! s = getsamples (ts, ts.Data > 15); %! [s.Time, s.Data] pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.getsampleusingtime000066400000000000000000000010521527370255000257610ustar00rootroot00000000000000%!demo %! ## One time gives the sample at that time; two give every sample between %! ## them, both ends included. %! %! ts = timeseries ([10; 20; 30; 40; 50], [0; 1; 2; 3; 4]); %! s = getsampleusingtime (ts, 2); %! [s.Time, s.Data] %! s = getsampleusingtime (ts, 0.5, 3); %! [s.Time, s.Data] %!demo %! ## A dated series takes dates. %! %! ts = timeseries ([3; 5; 4; 6], {'01-Mar-2024', '02-Mar-2024', ... %! '03-Mar-2024', '04-Mar-2024'}); %! s = getsampleusingtime (ts, '02-Mar-2024', '03-Mar-2024'); %! getabstime (s) pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.gettsafterevent000066400000000000000000000005431527370255000252710ustar00rootroot00000000000000%!demo %! ## The six event queries cut a series at an event: before it, after it, %! ## at it, or between two. The `at` forms include the event's own time. %! %! ts = timeseries ((10:10:60)', 0:5); %! ts = addevent (ts, 'switch', 3); %! s = gettsafterevent (ts, 'switch'); %! [s.Time, s.Data] %! s = gettsafteratevent (ts, 'switch'); %! [s.Time, s.Data] pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.gettsbetweenevents000066400000000000000000000004051527370255000260010ustar00rootroot00000000000000%!demo %! ## `gettsbetweenevents` keeps the samples from one event to the other, %! ## both times included. %! %! ts = timeseries ((10:10:60)', 0:5); %! ts = addevent (ts, {'on', 'off'}, [1, 4]); %! s = gettsbetweenevents (ts, 'on', 'off'); %! [s.Time, s.Data] pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.idealfilter000066400000000000000000000005611527370255000243430ustar00rootroot00000000000000%!demo %! ## `idealfilter` keeps (`'pass'`) or removes (`'notch'`) the frequencies in %! ## the intervals given, in cycles per unit of time. The result is %! ## centred on zero. %! %! t = (0:99)'; %! ts = timeseries (sin (2 * pi * 0.02 * t) + 0.5 * sin (2 * pi * 0.3 * t), t); %! s = idealfilter (ts, [0, 0.1], 'pass'); %! max (abs (s.Data - sin (2 * pi * 0.02 * t))) pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.mean000066400000000000000000000011171527370255000227750ustar00rootroot00000000000000%!demo %! ## The statistics run over time. `NaN` is left out while %! ## `TreatNaNasMissing` is true, and `'Quality'` names codes whose samples %! ## are left out. %! %! ts = timeseries ([2; NaN; 4; 100], [0; 1; 2; 3], [0; 0; 0; 5]); %! mean (ts) %! mean (ts, 'Quality', 5) %!demo %! ## `'Weighting', 'time'` weights each sample by the time it stands for, %! ## which matters on irregular times. The other statistics take the same %! ## options. %! %! ts = timeseries ([1; 2; 4; 8; 16], [0; 1; 3; 7; 8]); %! mean (ts) %! mean (ts, 'Weighting', 'time') %! median (ts, 'Weighting', 'time') pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.plus000066400000000000000000000006641527370255000230460ustar00rootroot00000000000000%!demo %! ## The operators work sample by sample, between two series on the same %! ## times or between a series and a number. Units survive `+` and `-` %! ## and are dropped by products and quotients. %! %! a = timeseries ([1; 2; 3], [0; 1; 2]); %! a.DataInfo.Units = 'm'; %! b = timeseries ([10; 20; 30], [0; 1; 2]); %! b.DataInfo.Units = 'm'; %! s = a + b; %! [s.Time, s.Data] %! s.DataInfo.Units %! p = a .* b; %! p.DataInfo.Units pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.resample000066400000000000000000000012611527370255000236650ustar00rootroot00000000000000%!demo %! ## `resample` evaluates a series at new times. Outside the series the %! ## data is `NaN`, and each new time takes the quality code of the nearest %! ## sample. %! %! ts = timeseries ([0; 10; 20], [0; 1; 2], [1; 2; 3]); %! s = resample (ts, [0.5, 1.5, 3]); %! [s.Time, s.Data, s.Quality] %!demo %! ## A method given here overrides the series' own for this call, and a %! ## code, listed in `QualityInfo.Code`, marks the times the series did %! ## not hold. %! %! ts = timeseries ([0; 10; 20], [0; 1; 2], [1; 1; 1]); %! ts.QualityInfo.Code = [1, 9]; %! ts.QualityInfo.Description = {'measured', 'filled'}; %! s = resample (ts, 0:0.5:2, 'zoh', 9); %! [s.Time, s.Data, s.Quality] pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.setabstime000066400000000000000000000013051527370255000242140ustar00rootroot00000000000000%!demo %! ## `setabstime` gives the samples dates, keeping the series' own units, %! ## so a series in seconds given a date a day gets steps of 86400. %! %! ts = timeseries ([10; 20; 30]); %! ts = setabstime (ts, {'01-Mar-2024', '02-Mar-2024', '03-Mar-2024'}); %! ts.Time' %! ts.TimeInfo.StartDate %!demo %! ## The dates need not be sorted: each sample keeps the date it was given, %! ## and the series is sorted with them. MATLAB sorts the dates alone, %! ## leaving every sample on another's date. %! %! ts = timeseries ([10; 20; 30], [0; 1; 2], [1; 2; 3]); %! ts.TimeInfo.Units = 'days'; %! ts = setabstime (ts, {'03-Mar-2024', '01-Mar-2024', '02-Mar-2024'}); %! getabstime (ts) %! [ts.Data, ts.Quality] pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.setinterpmethod000066400000000000000000000006221527370255000252730ustar00rootroot00000000000000%!demo %! ## The interpolation method says how the series behaves between samples, %! ## which `resample` and `synchronize` follow: `'linear'` joins them, %! ## `'zoh'` holds each value until the next sample. %! %! ts = timeseries ([0; 10; 20], [0; 1; 2]); %! s = resample (ts, 0:0.5:2); %! s.Data' %! ts = setinterpmethod (ts, 'zoh'); %! getinterpmethod (ts) %! s = resample (ts, 0:0.5:2); %! s.Data' pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.setuniformtime000066400000000000000000000005411527370255000251270ustar00rootroot00000000000000%!demo %! ## `setuniformtime` spaces the samples evenly without moving them: from a %! ## start time, by a step, or up to an end time, in any usable pair. %! %! ts = timeseries ([10; 20; 30; 40], [0; 1; 5; 6]); %! s = setuniformtime (ts, 'StartTime', 10, 'Interval', 0.5); %! s.Time' %! s = setuniformtime (ts, 'StartTime', 0, 'EndTime', 9); %! s.Time' pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.synchronize000066400000000000000000000006541527370255000244350ustar00rootroot00000000000000%!demo %! ## `synchronize` resamples two series onto common times within the span %! ## both cover: the union of their times, their intersection, or a %! ## uniform grid. %! %! a = timeseries ([1; 2; 3], [0; 2; 4]); %! b = timeseries ([10; 20; 30], [1; 2; 3]); %! [a2, b2] = synchronize (a, b, 'union'); %! [a2.Time, a2.Data, b2.Data] %! [a2, b2] = synchronize (a, b, 'uniform', 'Interval', 0.5); %! [a2.Time, a2.Data, b2.Data] pr0m1th3as-datatypes-9450b29/inst/demos/timeseries.timeseries000066400000000000000000000021411527370255000242240ustar00rootroot00000000000000%!demo %! ## A series is a sequence of samples, each taken at a time. The data %! ## alone gives the times 0, 1, 2, ... seconds, and the display summarises %! ## the series rather than listing it. %! %! ts = timeseries ([12.1; 14.5; 13.2; 15.8], 'Name', 'temp') %! [ts.Time, ts.Data] %!demo %! ## Times may be given, in any order: the samples are sorted with them. %! ## A third argument sets a quality code for each sample. %! %! ts = timeseries ([12.1; 14.5; 13.2], [30; 0; 60], [0; 1; 0]); %! [ts.Time, ts.Data, ts.Quality] %!demo %! ## Dates as text, or a `datetime` array, give times in days counted from %! ## the earliest date, which becomes `TimeInfo.StartDate`. %! %! ts = timeseries ([3; 5; 4], {'01-Mar-2024', '03-Mar-2024', '02-Mar-2024'}); %! ts.Time %! ts.TimeInfo.StartDate %! ts.TimeInfo.Units %!demo %! ## A matrix holds one sample per row, and data of three dimensions one %! ## sample per slice along the last, which `IsTimeFirst` reports. %! %! ts2 = timeseries ([1, 10; 2, 20; 3, 30]); %! size (ts2.Data), ts2.IsTimeFirst %! ts3 = timeseries (rand (2, 2, 5)); %! size (ts3.Data), ts3.IsTimeFirst pr0m1th3as-datatypes-9450b29/inst/demos/timetable.addvars000066400000000000000000000020371527370255000233000ustar00rootroot00000000000000%!demo %! ## `addvars` appends variables, naming them after the workspace variables %! ## they came from. The row times are untouched, so the time step stands. %! %! t0 = datetime (2024, 1, 1); %! Depth = [12; 45; 23]; %! TT = timetable (Depth, 'RowTimes', t0 + hours (0:2)'); %! Salinity = [34.1; 34.6; 35.0]; %! addvars (TT, Salinity) %!demo %! ## `'Before'` and `'After'` place the new variable beside an existing one. %! ## The row times are not a variable and cannot be used as the location; a %! ## name that would clash with the row dimension is refused outright, while %! ## one arriving from the workspace is given a suffix instead. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([12; 45; 23], [1; 2; 3], 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'Depth', 'Cast'}); %! addvars (TT, [34.1; 34.6; 35.0], 'Before', 'Cast', ... %! 'NewVariableNames', {'Salinity'}) %! %! ## A workspace variable called `Time` becomes `Time_1`. %! Time = t0 + hours (0:2)'; %! addvars (TT, Time).Properties.VariableNames pr0m1th3as-datatypes-9450b29/inst/demos/timetable.containsrange000066400000000000000000000027221527370255000245100ustar00rootroot00000000000000%!demo %! ## `containsrange` asks whether a timetable spans the whole of a range of %! ## times. A timetable's range runs from its earliest row time to its %! ## latest, so the question is about the two ends and not about the rows in %! ## between. %! %! t0 = datetime (2024, 1, 1); %! Temp = [17.2; 18.9; 21.4; 22.0; 20.1; 18.3]; %! Site = categorical ({'N'; 'N'; 'S'; 'S'; 'N'; 'S'}); %! TT = timetable (Temp, Site, 'RowTimes', t0 + hours (0:5)') %! containsrange (TT, timerange (t0 + hours (1), t0 + hours (3))) %! %! ## A range reaching past the last reading is not one the timetable spans. %! containsrange (TT, timerange (t0 + hours (3), t0 + hours (8))) %!demo %! ## The second output says which rows fell inside the range. It answers a %! ## different question from the first: an instant between two readings lies %! ## in the timetable's range while belonging to no row of it. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([17.2; 18.9; 21.4], 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'Temp'}); %! [tf, whichRows] = containsrange (TT, t0 + minutes (90)) %!demo %! ## Another timetable may stand for the range, in which case it is the range %! ## between that one's earliest and latest row times, both ends included. %! %! t0 = datetime (2024, 1, 1); %! Wide = timetable ((1:12)', 'RowTimes', t0 + hours (0:11)'); %! Narrow = timetable ((1:4)', 'RowTimes', t0 + hours (2:5)'); %! containsrange (Wide, Narrow) %! containsrange (Narrow, Wide) pr0m1th3as-datatypes-9450b29/inst/demos/timetable.convertvars000066400000000000000000000007211527370255000242260ustar00rootroot00000000000000%!demo %! ## `convertvars` changes the type of chosen variables, by type name or by %! ## a function handle. The row times are not a data variable and are left %! ## for `Properties.RowTimes` to change. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3], {'a'; 'b'; 'c'}, 'RowTimes', ... %! t0 + hours (0:2)', 'VariableNames', {'Cast', 'Site'}) %! C = convertvars (TT, 'Site', 'categorical'); %! class (C.Site) %! C.Properties.TimeStep pr0m1th3as-datatypes-9450b29/inst/demos/timetable.extractevents000066400000000000000000000035351527370255000245570ustar00rootroot00000000000000%!demo %! ## `extractevents` builds an event table out of a timetable's own rows. %! ## Given a row subscript it takes the times and nothing else. %! %! t0 = datetime (2024, 3, 1); %! TT = timetable ([12; 45; 23; 31], 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'Depth'}); %! extractevents (TT, [2; 4]) %!demo %! ## Given a `categorical` with one element per row, every row whose label %! ## is not missing becomes an event labelled by it. Consecutive rows %! ## sharing a label are *not* gathered into one interval: each row is its %! ## own instantaneous event. %! %! t0 = datetime (2024, 3, 1); %! TT = timetable ([12; 45; 23; 31], 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'Depth'}); %! phase = categorical ({'dry'; 'dry'; ''; 'wet'}); %! extractevents (TT, phase) %!demo %! ## Options say what else the event table carries. A value given as %! ## `EventLengths` becomes a variable of that name; a variable of the %! ## timetable named by `EventDataVariables` keeps its own. %! %! t0 = datetime (2024, 3, 1); %! TT = timetable ([12; 45; 23; 31], 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'Depth'}); %! extractevents (TT, [2; 4], 'EventLabels', ["rain"; "snow"], ... %! 'EventLengths', hours ([1; 2]), ... %! 'EventDataVariables', {'Depth'}) %!demo %! ## A second output returns the timetable without the variables the event %! ## table took from it. Only a variable that was *named* is taken, so a %! ## call that names none leaves it as it was. %! %! t0 = datetime (2024, 3, 1); %! TT = timetable ([12; 45; 23; 31], ["a"; "b"; "c"; "d"], ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'Depth', 'Tag'}); %! [ET, TT2] = extractevents (TT, [2; 4], 'EventLabelsVariable', 'Tag'); %! TT2.Properties.VariableNames pr0m1th3as-datatypes-9450b29/inst/demos/timetable.fillmissing000066400000000000000000000025541527370255000242000ustar00rootroot00000000000000%!demo %! ## `fillmissing` runs against the *row times*, not the order of the rows. %! ## The gap below sits an hour after its left neighbour and two hours before %! ## its right one, so `'linear'` fills it a third of the way between them. %! ## Counting rows would have put it midway, at 20. %! %! t0 = datetime (2024, 1, 1); %! Depth = [10; NaN; 30]; %! TT = timetable (Depth, 'RowTimes', t0 + hours ([0 1 3])') %! fillmissing (TT, 'linear') %!demo %! ## The same spacing decides which neighbour is nearest. Here the earlier %! ## reading is an hour away and the later one two, so `'nearest'` takes the %! ## earlier; by row count the two would be equally close. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([10; NaN; 30], 'RowTimes', t0 + hours ([0 1 3])', ... %! 'VariableNames', {'Depth'}); %! fillmissing (TT, 'nearest').Depth %! fillmissing (TT, 'next').Depth %! fillmissing (TT, 'constant', 0).Depth %!demo %! ## Because the row times are already the sample points, `'SamplePoints'` is %! ## not accepted, and a timetable that cannot say when a row happened is %! ## refused rather than filled around the gap. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([10; NaN; 30], 'RowTimes', [t0; NaT; t0 + hours(3)], ... %! 'VariableNames', {'Depth'}); %! try %! fillmissing (TT, 'previous') %! catch err %! disp (err.message) %! end pr0m1th3as-datatypes-9450b29/inst/demos/timetable.head000066400000000000000000000020211527370255000225460ustar00rootroot00000000000000%!demo %! ## `head` shows the first rows of a timetable, eight of them unless told %! ## otherwise, which is a quick way to look at a long one. %! %! t0 = datetime (2024, 1, 1); %! Temp = 15 + (1:12)' / 2; %! TT = timetable (Temp, 'RowTimes', t0 + hours (0:11)'); %! head (TT, 4) %!demo %! ## `head` takes the *first* rows and not the earliest ones. On a timetable %! ## whose times are out of order it returns whatever happens to be stored %! ## first, so sort it beforehand to ask the other question. %! %! t0 = datetime (2024, 1, 1); %! U = timetable ([30; 10; 40; 20], 'RowTimes', t0 + hours ([2 0 3 1])', ... %! 'VariableNames', {'Depth'}); %! head (U, 2) %! head (sortrows (U), 2) %!demo %! ## A count of zero is allowed and returns a timetable with no rows, which %! ## keeps the variables and their types. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'Reading'}); %! E = head (TT, 0); %! size (E) %! E.Properties.VariableNames pr0m1th3as-datatypes-9450b29/inst/demos/timetable.ismissing000066400000000000000000000011041527370255000236530ustar00rootroot00000000000000%!demo %! ## `ismissing` marks the missing values of each variable, one column per %! ## variable and one row per row. What counts as missing depends on the %! ## type: `NaN` for numbers, `NaT` for datetimes, an empty string for text. %! %! t0 = datetime (2024, 1, 1); %! Depth = [12; 45; NaN]; %! Site = {'N'; ''; 'S'}; %! TT = timetable (Depth, Site, 'RowTimes', t0 + hours (0:2)') %! ismissing (TT) %! %! ## The row times are not read: they label the rows rather than being one %! ## of them. A second argument names further values to treat as missing. %! ismissing (TT, 45) pr0m1th3as-datatypes-9450b29/inst/demos/timetable.issorted000066400000000000000000000015261527370255000235120ustar00rootroot00000000000000%!demo %! ## `issorted` asks one question: are the row times in ascending order? The %! ## ordering is not strict, so repeated times are sorted. %! %! t0 = datetime (2024, 1, 1); %! Reading = [12; 20; 25; 44]; %! issorted (timetable (Reading, 'RowTimes', t0 + hours (0:3)')) %! issorted (timetable (Reading, 'RowTimes', t0 + hours ([2 0 3 1])')) %! issorted (timetable (Reading, 'RowTimes', t0 + hours ([0 1 1 3])')) %!demo %! ## A missing row time leaves a timetable unsorted, and a timetable with %! ## fewer than two rows has nothing out of order. %! %! t0 = datetime (2024, 1, 1); %! issorted (timetable ([1; 2; 3], 'RowTimes', [t0; NaT; t0 + hours(2)])) %! TT = timetable ((1:4)', 'RowTimes', t0 + hours (0:3)'); %! issorted (TT(1,:)) %! %! ## For anything beyond that question, ask `issortedrows` instead. %! issortedrows (TT, 'Time', 'descend') pr0m1th3as-datatypes-9450b29/inst/demos/timetable.issortedrows000066400000000000000000000016531527370255000244260ustar00rootroot00000000000000%!demo %! ## `issortedrows` asks whether the rows are already in the order `sortrows` %! ## would put them in, and takes every form `sortrows` does. With nothing %! ## else asked, that order is ascending row times. %! %! t0 = datetime (2024, 1, 1); %! Depth = [12; 45; 23; 8]; %! TT = timetable (Depth, 'RowTimes', t0 + hours ([2 0 3 1])') %! issortedrows (TT) %! issortedrows (sortrows (TT)) %!demo %! ## Name a variable or a direction to ask a narrower question. The sort is %! ## stable, so the answer is exactly whether sorting would leave every row %! ## where it already is. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! issortedrows (TT, 'B') %! issortedrows (TT, 'B', 'descend') %! %! ## A numeric index counts the variables, and a negative one asks about %! ## descending order. %! issortedrows (TT, -1) pr0m1th3as-datatypes-9450b29/inst/demos/timetable.mergevars000066400000000000000000000010621527370255000236440ustar00rootroot00000000000000%!demo %! ## `mergevars` combines several variables into one multi-column variable, %! ## put where the first of them was. Their units do not survive: one %! ## variable carries one set, and there is no saying which it should be. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([12; 45; 23], [34.1; 34.6; 35.0], 'RowTimes', ... %! t0 + hours (0:2)', 'VariableNames', {'Depth', 'Salinity'}); %! TT.Properties.VariableUnits = {'m', 'psu'}; %! M = mergevars (TT, {'Depth', 'Salinity'}, 'NewVariableName', 'Cast') %! M.Properties.VariableUnits pr0m1th3as-datatypes-9450b29/inst/demos/timetable.movevars000066400000000000000000000007431527370255000235200ustar00rootroot00000000000000%!demo %! ## `movevars` reorders variables, each carrying its units and description %! ## along with it. The row times stay where they are. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([12; 45; 23], [34.1; 34.6; 35.0], 'RowTimes', ... %! t0 + hours (0:2)', 'VariableNames', {'Depth', 'Salinity'}); %! TT.Properties.VariableUnits = {'m', 'psu'}; %! M = movevars (TT, 'Salinity', 'Before', 'Depth'); %! M.Properties.VariableNames %! M.Properties.VariableUnits pr0m1th3as-datatypes-9450b29/inst/demos/timetable.overlapsrange000066400000000000000000000020051527370255000245170ustar00rootroot00000000000000%!demo %! ## `overlapsrange` asks whether a timetable and a range of times share any %! ## instant at all. Meeting at a single one is enough, so a range that %! ## starts exactly at the last row time still overlaps. %! %! t0 = datetime (2024, 1, 1); %! Flow = [3.1; 3.4; 4.0; 3.8; 3.2; 2.9]; %! TT = timetable (Flow, 'RowTimes', t0 + hours (0:5)') %! overlapsrange (TT, timerange (t0 + hours (5), t0 + hours (9))) %! %! ## Move the range one hour later and nothing is shared. %! overlapsrange (TT, timerange (t0 + hours (6), t0 + hours (9))) %!demo %! ## Overlap is of the ranges, not of the rows, so a timetable can overlap a %! ## range without a single row falling in it. The two outputs disagree here %! ## and both are right. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([3.1; 4.0; 2.9], 'RowTimes', t0 + hours ([0 3 6])', ... %! 'VariableNames', {'Flow'}); %! [tf, whichRows] = overlapsrange (TT, timerange (t0 + hours (1), ... %! t0 + hours (2))) pr0m1th3as-datatypes-9450b29/inst/demos/timetable.removevars000066400000000000000000000010041527370255000240360ustar00rootroot00000000000000%!demo %! ## `removevars` deletes variables. The row times are not a variable, so %! ## they cannot be removed: a timetable emptied of variables still carries %! ## its times and its time step. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([12; 45; 23], [1; 2; 3], 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'Depth', 'Cast'}) %! removevars (TT, 'Cast') %! %! ## Take both away and the times remain. %! E = removevars (TT, {'Depth', 'Cast'}); %! size (E) %! E.Properties.TimeStep pr0m1th3as-datatypes-9450b29/inst/demos/timetable.renamevars000066400000000000000000000010041527370255000240100ustar00rootroot00000000000000%!demo %! ## `renamevars` renames variables and keeps everything else about them. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([12; 45; 23], [34.1; 34.6; 35.0], 'RowTimes', ... %! t0 + hours (0:2)', 'VariableNames', {'Depth', 'Salinity'}); %! TT.Properties.VariableUnits = {'m', 'psu'}; %! R = renamevars (TT, 'Salinity', 'Sal'); %! R.Properties.VariableNames %! R.Properties.VariableUnits %! %! ## Dimension names are not renamed here; assign to them instead. %! R.Properties.DimensionNames pr0m1th3as-datatypes-9450b29/inst/demos/timetable.rmmissing000066400000000000000000000016051527370255000236640ustar00rootroot00000000000000%!demo %! ## `rmmissing` drops the rows holding a missing value, and reports which %! ## ones went. %! %! t0 = datetime (2024, 1, 1); %! Depth = [12; 45; NaN; 8]; %! TT = timetable (Depth, 'RowTimes', t0 + hours (0:3)') %! [R, TF] = rmmissing (TT); %! R %! TF %!demo %! ## A row whose *time* is missing goes whatever its data holds. That is a %! ## precondition rather than a report of missingness: `ismissing` does not %! ## mark such a row and `anymissing` does not count it, yet `rmmissing` %! ## cannot keep a row it has no way to place in time. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([12; 45; 23], 'RowTimes', [t0; NaT; t0 + hours(2)], ... %! 'VariableNames', {'Depth'}); %! anymissing (TT) %! rmmissing (TT) %! %! ## Neither `'DataVariables'` nor `'MinNumMissing'` reaches the row times, %! ## so neither can spare that row. %! rmmissing (TT, 'MinNumMissing', 2) pr0m1th3as-datatypes-9450b29/inst/demos/timetable.sortrows000066400000000000000000000027611527370255000235620ustar00rootroot00000000000000%!demo %! ## `sortrows` on a timetable orders by the row times unless told otherwise. %! ## Rows sharing a time keep the order they were in: the times alone decide, %! ## and the variables are never consulted to break a tie. %! %! t0 = datetime (2024, 1, 1); %! Reading = [9.4; 3.1; 1.7; 7.2]; %! TT = timetable (Reading, 'RowTimes', t0 + hours ([1 0 1 0])') %! sortrows (TT) %!demo %! ## Name the row dimension to sort by the times explicitly, or a variable to %! ## sort by that instead, in which case the row times travel with their rows. %! %! t0 = datetime (2024, 1, 1); %! Depth = [12; 45; 23; 8]; %! Site = categorical ({'N'; 'S'; 'N'; 'S'}); %! TT = timetable (Depth, Site, 'RowTimes', t0 + hours ([2 0 3 1])') %! sortrows (TT, 'Time', 'descend') %! %! ## A numeric index counts the *variables*, so 1 means `Depth`, never the %! ## times, and a negative index sorts that variable downwards. %! sortrows (TT, -1) %!demo %! ## The time step is read afresh from the sorted row times rather than %! ## carried over. Sorting an out-of-order hourly timetable makes it regular %! ## again, and sorting one that ran backwards turns its step positive. %! %! t0 = datetime (2024, 1, 1); %! U = timetable ([30; 10; 40; 20], 'RowTimes', t0 + hours ([2 0 3 1])'); %! U.Properties.TimeStep %! S = sortrows (U); %! S.Properties.TimeStep %! %! ## The same timetable built backwards steps by minus one hour. %! Z = timetable ((1:4)', 'RowTimes', t0 - hours (0:3)'); %! Z.Properties.TimeStep %! sortrows (Z).Properties.TimeStep pr0m1th3as-datatypes-9450b29/inst/demos/timetable.splitvars000066400000000000000000000007501527370255000237030ustar00rootroot00000000000000%!demo %! ## `splitvars` is the other direction: a multi-column variable becomes one %! ## variable per column, named after it unless told otherwise. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([12 34.1; 45 34.6; 23 35.0], (1:3)', 'RowTimes', ... %! t0 + hours (0:2)', 'VariableNames', {'Cast', 'Station'}); %! splitvars (TT).Properties.VariableNames %! splitvars (TT, 'Cast', 'NewVariableNames', ... %! {'Depth', 'Salinity'}).Properties.VariableNames pr0m1th3as-datatypes-9450b29/inst/demos/timetable.summary000066400000000000000000000026221527370255000233510ustar00rootroot00000000000000%!demo %! ## `summary` prints what a timetable holds: its size, its row times, its %! ## variables with their types, and a table of statistics for everything %! ## that has any. The row times come first, under the row dimension's name. %! %! t0 = datetime (2024, 1, 1); %! Depth = [12; 45; 23; 8]; %! Site = categorical ({'N'; 'S'; 'N'; 'S'}); %! TT = timetable (Depth, Site, 'RowTimes', t0 + hours (0:3)'); %! summary (TT) %!demo %! ## Ask for an output and the same thing comes back as a structure, one %! ## field per variable and one for the row times. Those report where the %! ## times start, how fast they run and how far apart they step. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([12; 45; 23; 8], 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'Depth'}); %! s = summary (TT); %! fieldnames (s) %! s.Time.StartTime %! s.Time.TimeStep %! s.Depth.Mean %!demo %! ## Which statistics a variable reports follows its type. An integer has no %! ## standard deviation and its median rounds to its own type; a `logical` %! ## reports how many are true and appears in no statistics at all. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable (int8 ([1; 2; 3; 4]), logical ([1; 0; 1; 1]), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'Count', 'Flag'}); %! s = summary (TT); %! fieldnames (s.Count) %! s.Count.Median %! fieldnames (s.Flag) pr0m1th3as-datatypes-9450b29/inst/demos/timetable.syncevents000066400000000000000000000030311527370255000240500ustar00rootroot00000000000000%!demo %! ## `syncevents` copies the attached event table's data onto the rows its %! ## events cover. A row no event covers takes a missing value. %! %! t0 = datetime (2024, 3, 1); %! TT = timetable ([12; 45; 23; 31], 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'Depth'}); %! TT.Properties.Events = eventtable (t0 + hours ([1; 3]), ... %! 'EventLabels', ["rain"; "snow"]); %! syncevents (TT) %!demo %! ## An interval event covers the rows from its own time up to but not %! ## including its end, so a two-hour event beginning at 01:00 covers 01:00 %! ## and 02:00. A default label answers for the rows left over. %! %! t0 = datetime (2024, 3, 1); %! TT = timetable ([12; 45; 23; 31], 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'Depth'}); %! TT.Properties.Events = eventtable (t0 + hours (1), ... %! 'EventLabels', "rain", ... %! 'EventLengths', hours (2)); %! syncevents (TT, "dry") %!demo %! ## A row two events both cover is repeated, once per event, which %! ## generally makes the result irregular. %! %! t0 = datetime (2024, 3, 1); %! TT = timetable ([12; 45; 23; 31], 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'Depth'}); %! TT.Properties.Events = eventtable (t0 + hours ([1; 2]), ... %! 'EventLabels', ["rain"; "hail"], ... %! 'EventLengths', hours ([2; 2])); %! height (syncevents (TT)) pr0m1th3as-datatypes-9450b29/inst/demos/timetable.tail000066400000000000000000000012041527370255000226000ustar00rootroot00000000000000%!demo %! ## `tail` shows the last rows of a timetable, eight of them unless told %! ## otherwise. %! %! t0 = datetime (2024, 1, 1); %! Temp = 15 + (1:12)' / 2; %! TT = timetable (Temp, 'RowTimes', t0 + hours (0:11)'); %! tail (TT, 4) %!demo %! ## The time step is read afresh from the rows that are kept, so the tail of %! ## a timetable whose spacing changes partway may not be regular even when %! ## the head of it is. %! %! t0 = datetime (2024, 1, 1); %! G = timetable ((1:4)', 'RowTimes', t0 + hours ([0 1 2 9])', ... %! 'VariableNames', {'Reading'}); %! head (G, 3).Properties.TimeStep %! tail (G, 3).Properties.TimeStep pr0m1th3as-datatypes-9450b29/inst/demos/timetable.timetable2csv000066400000000000000000000034441527370255000244230ustar00rootroot00000000000000%!demo %! ## `timetable2csv` writes a timetable to a text file that keeps everything: %! ## the row times lead the file in ISO 8601 form, and their type, time zone %! ## and display format ride in the header block rather than in the values. %! ## `csv2timetable` gives back what went in. %! %! t = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours (0:3)'; %! Reading = [12.5; 13.1; 11.8; 12.0]; %! Site = categorical ({'N'; 'S'; 'N'; 'S'}); %! TT = timetable (Reading, Site, 'RowTimes', t) %! filename = fullfile (tempdir (), 'readings.csv'); %! timetable2csv (TT, filename); %! back = csv2timetable (filename) %! %! delete (filename); %!demo %! ## The file is readable in any text editor. Its first line says how many %! ## header rows follow, the second types every column, and the third names %! ## them. The row-time column is tagged `RowTimes` ahead of its own type, %! ## and the row dimension's name sits beside it. %! %! TT = timetable ([1; 2], 'RowTimes', datetime (2024, 1, [1; 2]), ... %! 'VariableNames', {'Count'}); %! TT.Properties.DimensionNames{1} = 'Day'; %! filename = fullfile (tempdir (), 'counts.csv'); %! timetable2csv (TT, filename); %! type (filename); %! %! delete (filename); %!demo %! ## A CSV file holds one table and has nowhere to put a second, so an %! ## attached event table is not written and nothing warns. Write the %! ## timetable to an OpenDocument spreadsheet to keep its events. %! %! t = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable ([1; 2; 3; 4], 'RowTimes', t, 'VariableNames', {'Flow'}); %! TT.Properties.Events = eventtable (t(2), 'EventLabels', {'valve opened'}); %! filename = fullfile (tempdir (), 'flow.csv'); %! timetable2csv (TT, filename); %! csv2timetable (filename).Properties.Events %! %! delete (filename); pr0m1th3as-datatypes-9450b29/inst/demos/timetable.timetable2ods000066400000000000000000000037711527370255000244200ustar00rootroot00000000000000%!demo %! ## `timetable2ods` writes a timetable to an OpenDocument spreadsheet, one %! ## natively typed cell per value. The row times become real date cells, so %! ## a spreadsheet application shows them as times rather than as text, and %! ## `ods2timetable` restores their type, zone and format exactly. %! %! t = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours (0:2)'; %! Reading = [12.5; 13.1; 11.8]; %! TT = timetable (Reading, 'RowTimes', t) %! filename = fullfile (tempdir (), 'readings.ods'); %! timetable2ods (TT, filename); %! ods2timetable (filename) %! %! delete (filename); %!demo %! ## This is the only file format of the package that carries an event table. %! ## It goes on a sheet of its own, and a cross-reference in the hidden %! ## metadata sheet ties the two together, so the events come back attached. %! %! t = datetime (2024, 1, 1) + hours (0:5)'; %! TT = timetable ([1; 2; 3; 4; 5; 6], 'RowTimes', t, ... %! 'VariableNames', {'Flow'}); %! TT.Properties.Events = eventtable (t([2 4]), ... %! 'EventLabels', {'opened'; 'closed'}, ... %! 'EventLengths', hours ([2; 1])); %! filename = fullfile (tempdir (), 'flow.ods'); %! timetable2ods (TT, filename); %! back = ods2timetable (filename); %! back.Properties.Events %! %! delete (filename); %!demo %! ## Writing to a named `'Sheet'` of an existing workbook adds or replaces %! ## just that sheet and leaves the others alone, so a table and a timetable %! ## can share one file and each comes back as what it was. %! %! filename = fullfile (tempdir (), 'workbook.ods'); %! table2ods (table ([38; 43], 'VariableNames', {'Age'}), filename, ... %! 'Sheet', 'Patients'); %! TT = timetable ([1; 2], 'RowTimes', datetime (2024, 1, [1; 2]), ... %! 'VariableNames', {'Visit'}); %! timetable2ods (TT, filename, 'Sheet', 'Visits'); %! s = ods2struct (filename); %! class (s.Patients) %! class (s.Visits) %! %! delete (filename); pr0m1th3as-datatypes-9450b29/inst/demos/timetable.topkrows000066400000000000000000000023611527370255000235440ustar00rootroot00000000000000%!demo %! ## `topkrows` returns the rows with the latest times, latest first. This %! ## is where it parts company with `head`, which takes the first rows as %! ## they happen to be stored rather than ranking them. %! %! t0 = datetime (2024, 1, 1); %! Depth = [30; 10; 40; 20]; %! TT = timetable (Depth, 'RowTimes', t0 + hours ([2 0 3 1])') %! topkrows (TT, 2) %! head (TT, 2) %!demo %! ## Name a variable to rank by that instead, and a direction to take the %! ## bottom rows rather than the top. A numeric index counts the variables, %! ## and its sign reads the other way round from `sortrows`: positive ranks %! ## downwards. %! %! t0 = datetime (2024, 1, 1); %! Depth = [12; 45; 23; 8]; %! Site = categorical ({'N'; 'S'; 'N'; 'S'}); %! TT = timetable (Depth, Site, 'RowTimes', t0 + hours (0:3)'); %! topkrows (TT, 2, 'Depth') %! topkrows (TT, 2, 'Depth', 'ascend') %!demo %! ## Missing keys rank last whichever way the sort runs, which is the one %! ## place `topkrows` differs from `sortrows` beyond its direction. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; NaN; 4], 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'Reading'}); %! topkrows (TT, 4, 'Reading').Reading %! sortrows (TT, 'Reading', 'descend').Reading pr0m1th3as-datatypes-9450b29/inst/demos/timetable.unique000066400000000000000000000026741527370255000231710ustar00rootroot00000000000000%!demo %! ## For a timetable a row is its time *together with* its values, so two %! ## rows count as one only when they agree on both. Here the two readings %! ## at 01:00 differ, so both are kept, while the pair at 04:00 is identical %! ## and collapses to one. %! %! t0 = datetime (2024, 1, 1); %! Reading = [12; 20; 25; 44; 44]; %! TT = timetable (Reading, 'RowTimes', t0 + hours ([0 1 1 4 4])') %! unique (TT) %!demo %! ## The row times are a column of the comparison, not a key that overrides %! ## it, so equal values at different times are all kept too. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([7; 7; 7], 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'Reading'}); %! height (unique (TT)) %! %! ## Strip the variables and only the times are left to compare, so repeated %! ## times do reduce to one. %! D = timetable ([1; 2; 3], 'RowTimes', t0 + hours ([0 1 1])', ... %! 'VariableNames', {'Reading'}); %! unique (D(:, [])) %!demo %! ## The result is ordered by row time unless `'stable'` asks for the order %! ## the rows were met in, and the index vectors map each way between the %! ## two timetables. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([30; 10; 40; 10], 'RowTimes', t0 + hours ([2 0 3 0])', ... %! 'VariableNames', {'Depth'}); %! [U, ia, ic] = unique (TT); %! U %! ia %! ic %! %! ## `'stable'` reports them in the order they arrived instead. %! unique (TT, 'stable') pr0m1th3as-datatypes-9450b29/inst/demos/timetable.withinrange000066400000000000000000000020251527370255000241700ustar00rootroot00000000000000%!demo %! ## `withinrange` asks whether the whole of a timetable lies inside a range %! ## of times. A `timerange` is half open by default, excluding the time it %! ## ends at, so a timetable whose last row falls exactly there is *not* %! ## within it. %! %! t0 = datetime (2024, 1, 1); %! Load = [220; 245; 260; 255; 231; 219]; %! TT = timetable (Load, 'RowTimes', t0 + hours (0:5)') %! withinrange (TT, timerange (t0, t0 + hours (5))) %! %! ## Close that same range and the last row is inside it. %! withinrange (TT, timerange (t0, t0 + hours (5), 'closed')) %!demo %! ## The three predicates answer three different questions about the same %! ## pair of ranges. A range wider than the timetable is one the timetable %! ## lies within and one it does not contain. %! %! t0 = datetime (2024, 1, 1); %! TT = timetable ([220; 245; 260], 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'Load'}); %! R = timerange (t0 - hours (1), t0 + hours (9)); %! containsrange (TT, R) %! overlapsrange (TT, R) %! withinrange (TT, R) pr0m1th3as-datatypes-9450b29/inst/demos/timetable.writetimetable000066400000000000000000000036321527370255000246770ustar00rootroot00000000000000%!demo %! ## `writetimetable` writes the MATLAB-compatible form: no hidden metadata, %! ## one header row of names, the row times leading under the row dimension's %! ## name, and one row per row. `readtimetable` reads it back. %! %! TT = timetable ([12.5; 13.1; 11.8], ... %! 'RowTimes', datetime (2024, 1, (1:3)'), ... %! 'VariableNames', {'Reading'}); %! filename = fullfile (tempdir (), 'plain.csv'); %! writetimetable (TT, filename); %! type (filename); %! readtimetable (filename) %! %! delete (filename); %!demo %! ## A zone-aware datetime is written in the RFC 9557 form, which carries the %! ## offset **and** the zone name. MATLAB writes the display string instead %! ## and cannot read the zone back at all, so this file says more than a %! ## MATLAB one does about the same instants. %! %! t = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours (0:2)'; %! TT = timetable ([1; 2; 3], 'RowTimes', t, 'VariableNames', {'Reading'}); %! filename = fullfile (tempdir (), 'zoned.csv'); %! writetimetable (TT, filename); %! type (filename); %! readtimetable (filename).Properties.RowTimes.TimeZone %! %! delete (filename); %!demo %! ## Carrying the offset as well as the zone name is what makes a repeated %! ## hour survive. These two rows are the same wall clock on the night the %! ## clocks go back, one hour apart in real time, and they come back as the %! ## two distinct instants they are. %! %! a = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC'); %! a.TimeZone = 'America/New_York'; %! b = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC'); %! b.TimeZone = 'America/New_York'; %! TT = timetable ([1; 2], 'RowTimes', [a; b], 'VariableNames', {'Reading'}); %! filename = fullfile (tempdir (), 'fold.csv'); %! writetimetable (TT, filename); %! type (filename); %! back = readtimetable (filename); %! tzoffset (back.Properties.RowTimes) %! %! delete (filename); pr0m1th3as-datatypes-9450b29/inst/demos/tscollection.addsampletocollection000066400000000000000000000005661527370255000267660ustar00rootroot00000000000000%!demo %! ## New samples go in time order. A member not given data takes `NaN` %! ## there, or 0 for integer data and `false` for logical data. %! %! tc = tscollection ({timeseries([1; 3], [0; 2], 'Name', 'a'), ... %! timeseries([10; 30], [0; 2], 'Name', 'b')}); %! tc = addsampletocollection (tc, 'Time', 1, 'a', 2); %! [tc.Time, tc.a.Data, tc.b.Data] pr0m1th3as-datatypes-9450b29/inst/demos/tscollection.addts000066400000000000000000000004401527370255000235030ustar00rootroot00000000000000%!demo %! ## `addts` adds a series on the collection's times, or data with a name. %! %! tc = tscollection (timeseries ([1; 2; 3], [0; 1; 2], 'Name', 'a')); %! tc = addts (tc, timeseries ([4; 5; 6], [0; 1; 2], 'Name', 'b')); %! tc = addts (tc, [7; 8; 9], 'c'); %! gettimeseriesnames (tc) pr0m1th3as-datatypes-9450b29/inst/demos/tscollection.delsamplefromcollection000066400000000000000000000004651527370255000273210ustar00rootroot00000000000000%!demo %! ## Samples are removed from every member, by position or by time. %! %! tc = tscollection ({timeseries([1; 2; 3], [0; 1; 2], 'Name', 'a'), ... %! timeseries([10; 20; 30], [0; 1; 2], 'Name', 'b')}); %! s = delsamplefromcollection (tc, 'Value', 1); %! [s.Time, s.a.Data, s.b.Data] pr0m1th3as-datatypes-9450b29/inst/demos/tscollection.getsampleusingtime000066400000000000000000000004341527370255000263150ustar00rootroot00000000000000%!demo %! ## The samples between two times, both included, from every member. %! %! tc = tscollection ({timeseries((1:5)', 0:4, 'Name', 'a'), ... %! timeseries((10:10:50)', 0:4, 'Name', 'b')}); %! s = getsampleusingtime (tc, 1, 3); %! [s.Time, s.a.Data, s.b.Data] pr0m1th3as-datatypes-9450b29/inst/demos/tscollection.horzcat000066400000000000000000000004341527370255000240610ustar00rootroot00000000000000%!demo %! ## `[tc1, tc2]` joins the members of collections on the same times, %! ## sorted by name, as in MATLAB. %! %! tc1 = tscollection (timeseries ([1; 2], [0; 1], 'Name', 'z')); %! tc2 = tscollection (timeseries ([3; 4], [0; 1], 'Name', 'a')); %! gettimeseriesnames ([tc1, tc2]) pr0m1th3as-datatypes-9450b29/inst/demos/tscollection.removets000066400000000000000000000004151527370255000242520ustar00rootroot00000000000000%!demo %! ## `removets` removes members by name; the times stay. %! %! tc = tscollection ({timeseries([1; 2], [0; 1], 'Name', 'a'), ... %! timeseries([3; 4], [0; 1], 'Name', 'b')}); %! tc = removets (tc, 'a'); %! gettimeseriesnames (tc) %! tc.Time' pr0m1th3as-datatypes-9450b29/inst/demos/tscollection.resample000066400000000000000000000005071527370255000242200ustar00rootroot00000000000000%!demo %! ## Every member is evaluated at the new times by its own method. %! %! a = timeseries ([0; 10; 20], [0; 1; 2], 'Name', 'a'); %! b = setinterpmethod (timeseries ([0; 10; 20], [0; 1; 2], 'Name', 'b'), ... %! 'zoh'); %! s = resample (tscollection ({a, b}), 0:0.5:2); %! [s.Time, s.a.Data, s.b.Data] pr0m1th3as-datatypes-9450b29/inst/demos/tscollection.setabstime000066400000000000000000000007031527370255000245460ustar00rootroot00000000000000%!demo %! ## `setabstime` dates the samples of every member. Unsorted dates move %! ## each member's samples with them; MATLAB sorts the dates alone. %! %! tc = tscollection ({timeseries([10; 20; 30], [0; 1; 2], 'Name', 'a'), ... %! timeseries([1; 2; 3], [0; 1; 2], 'Name', 'b')}); %! tc.TimeInfo.Units = 'days'; %! tc = setabstime (tc, {'03-Mar-2024', '01-Mar-2024', '02-Mar-2024'}); %! getabstime (tc) %! [tc.a.Data, tc.b.Data] pr0m1th3as-datatypes-9450b29/inst/demos/tscollection.settimeseriesnames000066400000000000000000000005151527370255000263200ustar00rootroot00000000000000%!demo %! ## Renaming a member renames its series too, and makes it the last %! ## member, as in MATLAB. %! %! tc = tscollection ({timeseries([1; 2], [0; 1], 'Name', 'a'), ... %! timeseries([3; 4], [0; 1], 'Name', 'b')}); %! tc = settimeseriesnames (tc, 'a', 'alpha'); %! gettimeseriesnames (tc) %! tc.alpha.Name pr0m1th3as-datatypes-9450b29/inst/demos/tscollection.tscollection000066400000000000000000000021061527370255000251070ustar00rootroot00000000000000%!demo %! ## A collection holds several series on one time vector. Each member is %! ## reached by its name, and a sample of the collection is the sample of %! ## every member at one time. %! %! temp = timeseries ([12.1; 14.5; 13.2], [0; 1; 2], 'Name', 'temp'); %! wind = timeseries ([3, 40; 5, 45; 4, 90], [0; 1; 2], 'Name', 'wind'); %! tc = tscollection ({temp, wind}, 'Name', 'station') %! size (tc) %! tc.wind.Data %!demo %! ## Member names follow `matlab.lang.makeValidName`, and must be unique %! ## ignoring case. %! %! tc = tscollection (timeseries ([1; 2], [0; 1], 'Name', 'air temp')); %! gettimeseriesnames (tc) %!demo %! ## `tc(i, j)` selects samples, then members by position or by name. %! ## The shared `Time` and `TimeInfo` belong to the collection: assigning %! ## them assigns every member. %! %! temp = timeseries ([12.1; 14.5; 13.2], [0; 1; 2], 'Name', 'temp'); %! rain = timeseries ([0; 2.5; 0.4], [0; 1; 2], 'Name', 'rain'); %! tc = tscollection ({temp, rain}); %! s = tc(2:3, 'rain'); %! [s.Time, s.rain.Data] %! tc.TimeInfo.Units = 'hours'; %! tc.temp.TimeInfo.Units pr0m1th3as-datatypes-9450b29/inst/demos/tscollection.vertcat000066400000000000000000000006421527370255000240600ustar00rootroot00000000000000%!demo %! ## `[tc1; tc2]` joins collections of the same members end to end in time. %! %! tc1 = tscollection ({timeseries([1; 2], [0; 1], 'Name', 'a'), ... %! timeseries([10; 20], [0; 1], 'Name', 'b')}); %! tc2 = tscollection ({timeseries([3; 4], [2; 3], 'Name', 'a'), ... %! timeseries([30; 40], [2; 3], 'Name', 'b')}); %! tc = [tc1; tc2]; %! [tc.Time, tc.a.Data, tc.b.Data] pr0m1th3as-datatypes-9450b29/inst/dictionary.m000066400000000000000000002166751527370255000212170ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef dictionary ## -*- texinfo -*- ## @deftp {datatypes} dictionary ## ## Map of unique keys to values. ## ## A @code{dictionary} object holds entries, each a key and the value it ## maps to. Every key is of one type and every value of one type, the key ## type and the value type of the dictionary; two keys are the same key when ## @code{keyMatch} says so. ## ## A dictionary with no entries may have no key and value types yet, when ## it is unconfigured; the first entries added configure it, and ## @code{configureDictionary} creates one configured with no entries. ## ## @code{dictionary} is a value class, and a dictionary is always a scalar ## object: @code{numel}, @code{size} and @code{isempty} describe the object, ## not its entries, which @code{numEntries} counts. Dictionaries cannot be ## concatenated. ## ## Entries are reached by indexing with keys, converted to the key type as ## @code{dictionary} converts them: ## ## @itemize ## @item @code{@var{d}(@var{keys})} returns the values of @var{keys}, an ## array of keys, in an array of the same shape; a key not in @var{d} is an ## error. ## ## @item @code{@var{d}(@var{keys}) = @var{values}} adds entries, or replaces ## the values of keys already present, which keep their positions; ## @var{values} has the size of @var{keys}, or is one value for all. An ## unconfigured dictionary takes its types from the first entries. ## ## @item @code{@var{d}(@var{keys}) = []} removes the entries of ## @var{keys}, ignoring keys not in @var{d}. A key removed and added ## again goes after the others. ## ## @item @code{@var{d}@{@var{keys}@}}, for a dictionary of @qcode{'cell'} ## values, returns the contents of their cells as a comma-separated list, ## and @code{@var{d}@{@var{key}@} = @var{value}} stores @var{value} in a ## cell under the single key @var{key}, @code{[]} included. ## ## @item @code{@var{d}.@var{method} (@dots{})} calls a method, as ## @code{@var{d}.keys ()}. ## @end itemize ## ## Indexing can continue into a value, as in @code{@var{d}(@var{key}).x} ## or @code{@var{d}@{@var{key}@}(2) = 5}; assigning into the contents of ## a new key starts from empty. @code{end} is not supported. Octave ## cannot tell @code{@var{d}(:)} from @code{@var{d}(':')}, so @qcode{':'} ## is the key @qcode{":"}, as it is in MATLAB in an assignment; use ## @code{keys} or @code{values} for every entry. ## ## @code{jsonencode} refuses a dictionary, as in MATLAB, and ## @code{writedictionary} writes one to a JSON file. ## ## @seealso{configureDictionary, isdictionary, keyHash, keyMatch} ## @end deftp properties (Access = private) ## The key and value types, '' while unconfigured keyType_ = '' valueType_ = '' ## One key and one value per entry, in insertion order, and an empty ## column of each type, which keeps its time zone or categories keys_ = cell (0, 1) values_ = cell (0, 1) keyEmpty_ = [] valueEmpty_ = [] ## The hash codes of the keys, sorted, and the entry of each hashes_ = zeros (0, 1, 'uint64') order_ = zeros (0, 1) endproperties methods (Hidden) function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n\n", in_name); endif disp (this); endfunction function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif n = numel (this.keys_); if (n == 0) fprintf (" %s.\n\n", summaryLine (this)); return; endif fprintf (" %s:\n\n", summaryLine (this)); K = cell (n, 1); V = cell (n, 1); w = zeros (n, 1); for i = 1:n K{i} = itemText (this.keys_{i}); V{i} = itemText (this.values_{i}); w(i) = textWidth (K{i}); endfor for i = 1:n fprintf (" %s%s ⟼ %s\n", K{i}, blanks (max (w) - w(i)), V{i}); endfor fprintf ("\n"); endfunction ## A dictionary has no properties to list function names = properties (this) if (nargout == 0) fprintf ("\nNo properties for class dictionary.\n\n"); else names = cell (0, 1); endif endfunction ## Refused, as in MATLAB, rather than exposing the private storage. A ## dictionary among the arguments also brings 'struct' here, as in ## struct ('d', d), which the built-in function answers. function s = struct (varargin) if (nargin == 1) error (strcat ("dictionary.struct: cannot convert a dictionary", ... " to a structure.")); endif s = builtin ('struct', varargin{:}); endfunction function json = jsonencode (this, varargin) error ("dictionary.jsonencode: cannot encode a dictionary as JSON text."); endfunction endmethods ################################################################################ ## ** Create and describe 'dictionary' ** ## ################################################################################ ## Available Methods ## ## ## ## 'dictionary' 'types' 'isConfigured' 'numEntries' ## ## 'numel' 'length' 'size' 'ndims' ## ## 'isempty' 'dispstrings' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{d} =} dictionary () ## @deftypefnx {dictionary} {@var{d} =} dictionary (@var{keys}, @var{values}) ## @deftypefnx {dictionary} {@var{d} =} dictionary (@var{k1}, @var{v1}, @dots{}, @var{kN}, @var{vN}) ## ## Create a @code{dictionary} object. ## ## @code{@var{d} = dictionary ()} returns an unconfigured dictionary, with ## no entries and no key and value types. ## ## @code{@var{d} = dictionary (@var{keys}, @var{values})} returns a ## dictionary mapping each element of the array @var{keys} to the element ## of @var{values} in the same position, in that order. The two must be ## of the same size, or @var{values} a single value given to every key; an ## empty @var{keys} gives a configured dictionary with no entries. A key ## repeated keeps its first position and takes its last value. The class ## of @var{keys} is the key type and that of @var{values} the value type, ## except that a character array is a string array of its rows. ## ## The key type must be one that @code{keyHash} can hash: a built-in type ## or a class with a @code{keyHash} method; a cell array is a key type of ## its own, each cell one key. An array of objects holds one key or value ## per object, but a @code{table} or @code{timetable} of any size is one ## value, where MATLAB accepts only a 1-by-1 one. ## ## @code{@var{d} = dictionary (@var{k1}, @var{v1}, @dots{}, @var{kN}, ## @var{vN})} adds each pair of keys and values in turn, the first pair ## setting the key and value types and later pairs converted to them. ## Keys and values of another class are converted by the function of the ## type's name, as in MATLAB: @code{string (@var{x})}, @code{double ## (@var{x})}, @code{categorical (@var{x})} and so on, and refused where ## that fails or gives another class. So @code{5} becomes @qcode{"5"} as ## a string, @qcode{"3.5"} becomes 3.5 as a @code{double} but is refused ## by @code{single} and the integer types, a character array gives its ## character codes as a number, a @code{categorical} its code, and ## @code{missing} gives @code{NaN}, @code{NaT} or an undefined category. ## The exceptions: ## ## @itemize ## @item a @qcode{'cell'} type takes only cell arrays, which are never ## wrapped, as in MATLAB; ## @item a @qcode{'struct'} type takes only structures, where MATLAB turns ## an object into a structure of its private properties; ## @item a @qcode{'table'} type makes a table of the variable ## @qcode{Var1} and refuses character arrays, as in MATLAB; ## @item a @qcode{'logical'} type takes a character as true for a nonzero ## code, as in MATLAB; ## @item a @qcode{'datetime'} type reads dates given as text in the time ## zone of the dictionary, and moves a key with a time zone to that of the ## keys; a value keeps its own. ## @end itemize ## ## A date with a time zone and one without are different keys, so that ## looking up one as the other finds nothing, and @code{keys} cannot join ## a dictionary holding both. Dates given as text to a dictionary whose ## keys have a time zone are read in that zone, so that they find its ## keys; MATLAB reads them without one, a key that no date with a zone ## matches. ## ## @seealso{configureDictionary, dictionary.isConfigured, dictionary.keys, ## keyHash, keyMatch} ## @end deftypefn function this = dictionary (varargin) if (nargin == 0) return; endif if (mod (nargin, 2) != 0) error ("dictionary: each key must be followed by a value."); endif for i = 1:2:nargin [this, errmsg] = addEntries (this, varargin{i}, varargin{i+1}); if (! isempty (errmsg)) error ("dictionary: %s", errmsg); endif endfor endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {[@var{keyType}, @var{valueType}] =} types (@var{d}) ## @deftypefnx {dictionary} {@var{keyType} =} types (@var{d}) ## ## Return the key and value types. ## ## @code{[@var{keyType}, @var{valueType}] = types (@var{d})} returns the ## class names of the keys and of the values of @var{d}, each a string ## scalar, or a missing string while @var{d} is unconfigured. ## ## @seealso{dictionary.isConfigured, configureDictionary} ## @end deftypefn function [keyType, valueType] = types (this) if (isempty (this.keyType_)) keyType = string (missing); valueType = string (missing); else keyType = string (this.keyType_); valueType = string (this.valueType_); endif endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{tf} =} isConfigured (@var{d}) ## ## True for a dictionary with key and value types. ## ## @code{@var{tf} = isConfigured (@var{d})} returns @code{false} for an ## unconfigured dictionary, which has no entries and no key and value ## types yet, and @code{true} otherwise, entries or not. ## ## @seealso{dictionary.types, configureDictionary} ## @end deftypefn function tf = isConfigured (this) tf = ! isempty (this.keyType_); endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{n} =} numEntries (@var{d}) ## ## Return the number of entries. ## ## @code{@var{n} = numEntries (@var{d})} returns the number of entries in ## @var{d} as a @code{double}, 0 for an unconfigured dictionary. ## ## @seealso{dictionary.numel} ## @end deftypefn function n = numEntries (this) n = numel (this.keys_); endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{n} =} numel (@var{d}) ## ## Return 1, the number of elements of a scalar object. ## ## @code{@var{n} = numel (@var{d})} is always 1, however many entries ## @var{d} holds; @code{numEntries} counts them. ## ## @seealso{dictionary.numEntries, dictionary.size} ## @end deftypefn function n = numel (this, varargin) n = 1; ## Octave asks with the subscripts how many values '{}' yields if (numel (varargin) == 1 && ! isempty (this.keyType_)) [K, errmsg] = convert (varargin{1}, this.keyType_, this.keyEmpty_, ... 'key'); if (isempty (errmsg)) n = prod (arraySize (K)); endif endif endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{n} =} length (@var{d}) ## ## Return 1, the length of a scalar object. ## ## @code{@var{n} = length (@var{d})} is always 1, however many entries ## @var{d} holds; @code{numEntries} counts them. ## ## @seealso{dictionary.numEntries, dictionary.size} ## @end deftypefn function n = length (this) n = 1; endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{sz} =} size (@var{d}) ## @deftypefnx {dictionary} {@var{dim_sz} =} size (@var{d}, @var{dim}) ## @deftypefnx {dictionary} {@var{dim_sz} =} size (@var{d}, @var{d1}, @var{d2}, @dots{}) ## @deftypefnx {dictionary} {[@var{rows}, @var{cols}, @dots{}] =} size (@var{d}) ## ## Return the size of a scalar object. ## ## @code{@var{sz} = size (@var{d})} is always @code{[1, 1]}, however many ## entries @var{d} holds. @code{size (@var{d}, @var{dim})} returns 1 for ## each dimension in @var{dim}, a positive integer or a vector of them, ## and @code{size (@var{d}, @var{d1}, @var{d2}, @dots{})} returns 1 for ## each dimension given. With several outputs each takes one dimension. ## ## @seealso{dictionary.numel, dictionary.numEntries} ## @end deftypefn function varargout = size (this, varargin) if (nargin > 1) if (nargin > 2 && ! all (cellfun ('numel', varargin) == 1)) error ("dictionary.size: each dimension must be a scalar."); endif if (! (all (cellfun ('isnumeric', varargin)) && all (cellfun ('isreal', varargin)))) error ("dictionary.size: DIM must be positive integers."); endif dim = [varargin{:}]; if (! (! isempty (dim) && all (dim == fix (dim) & dim >= 1))) error ("dictionary.size: DIM must be positive integers."); endif if (nargout > 1) [varargout{1:nargout}] = deal (1); else varargout{1} = ones (1, numel (dim)); endif elseif (nargout <= 1) varargout{1} = [1, 1]; else [varargout{1:nargout}] = deal (1); endif endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{n} =} ndims (@var{d}) ## ## Return 2, the number of dimensions of a scalar object. ## ## @seealso{dictionary.size} ## @end deftypefn function n = ndims (this) n = 2; endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{tf} =} isempty (@var{d}) ## ## Return @code{false}: a dictionary is never an empty array. ## ## @code{@var{tf} = isempty (@var{d})} is @code{false} even for a ## dictionary with no entries, since the object itself is a scalar; test ## @code{numEntries (@var{d}) == 0} for a dictionary with no entries. ## ## @seealso{dictionary.numEntries, dictionary.isConfigured} ## @end deftypefn function tf = isempty (this) tf = false; endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{cstr} =} dispstrings (@var{d}) ## ## Return the line that describes a dictionary. ## ## @code{@var{cstr} = dispstrings (@var{d})} returns a 1-by-1 cell array ## holding the line that describes @var{d} by its key and value types and ## its number of entries, as @qcode{'dictionary (string ⟼ double) with 2 ## entries'}, or @qcode{'dictionary with unset key and value types'} while ## it is unconfigured. A cell array displays a dictionary by this line, ## and so does a dictionary holding dictionaries. ## ## @seealso{dictionary.types, dictionary.numEntries} ## @end deftypefn function cstr = dispstrings (this) cstr = {summaryLine(this)}; endfunction endmethods ################################################################################ ## ** Keys and values ** ## ################################################################################ ## Available Methods ## ## ## ## 'keys' 'values' 'isKey' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{k} =} keys (@var{d}) ## @deftypefnx {dictionary} {@var{k} =} keys (@var{d}, @var{format}) ## ## Return the keys. ## ## @code{@var{k} = keys (@var{d})} returns the keys of @var{d} as a ## column array of the key type, in the order they were added. Keys of a ## type that cannot be joined into one array, such as function handles, ## are refused unless there is exactly one. ## ## @code{@var{k} = keys (@var{d}, @var{format})} returns them as above for ## @var{format} @qcode{'uniform'}, the default, and as a column cell ## array, each key in a cell of its own, for @qcode{'cell'}. ## @var{format} is matched in any case and by its first letters. ## ## @var{d} must be configured. ## ## @seealso{dictionary.values, dictionary.isKey, dictionary.numEntries} ## @end deftypefn function k = keys (this, format = 'uniform') scope = 'dictionary.keys'; [format, errmsg] = formatName (format, {'uniform', 'cell'}); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif if (isempty (this.keyType_)) error ("%s: D is not configured.", scope); endif if (strcmp (format, 'cell')) k = this.keys_; return; endif [k, errmsg] = join (this.keys_, this.keyEmpty_, this.keyType_, 'keys'); if (! isempty (errmsg)) error ("%s: %s; use the 'cell' format.", scope, errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{v} =} values (@var{d}) ## @deftypefnx {dictionary} {@var{v} =} values (@var{d}, @var{format}) ## ## Return the values. ## ## @code{@var{v} = values (@var{d})} returns the values of @var{d} as a ## column array of the value type, in the order their keys were added. ## Values that cannot be joined into one array, such as function handles, ## dictionaries or structures with different fields, are refused unless ## there is exactly one. Tables are joined only when each has one row, ## so that row @var{i} is the value of key @var{i}. ## ## @code{@var{v} = values (@var{d}, @var{format})} returns them as above ## for @var{format} @qcode{'uniform'}, the default, and as a column cell ## array, each value in a cell of its own, for @qcode{'cell'}. ## @var{format} is matched in any case and by its first letters. ## ## @var{d} must be configured. ## ## @seealso{dictionary.keys, dictionary.numEntries} ## @end deftypefn function v = values (this, format = 'uniform') scope = 'dictionary.values'; [format, errmsg] = formatName (format, {'uniform', 'cell'}); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif if (isempty (this.keyType_)) error ("%s: D is not configured.", scope); endif if (strcmp (format, 'cell')) v = this.values_; return; endif [v, errmsg] = join (this.values_, this.valueEmpty_, this.valueType_, ... 'values'); if (! isempty (errmsg)) error ("%s: %s; use the 'cell' format.", scope, errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{tf} =} isKey (@var{d}, @var{keys}) ## ## True for each of the keys that is in the dictionary. ## ## @code{@var{tf} = isKey (@var{d}, @var{keys})} returns a logical array ## of the size of @var{keys}, true where the element of @var{keys} is a key ## of @var{d}. The keys are first converted to the key type of @var{d}, as ## @code{dictionary} converts them, so that @code{isKey (@var{d}, 1)} on a ## dictionary of string keys asks for the key @qcode{"1"}; a class that ## cannot be converted is refused. @var{d} must be configured. ## ## @seealso{dictionary.keys, keyMatch} ## @end deftypefn function tf = isKey (this, keys) scope = 'dictionary.isKey'; if (nargin != 2) error ("%s: invalid number of input arguments.", scope); endif if (isempty (this.keyType_)) error ("%s: D is not configured.", scope); endif [keys, errmsg] = convert (keys, this.keyType_, this.keyEmpty_, 'key'); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif if (isArray (keys)) tf = false (size (keys)); else tf = false; endif for i = 1:numel (tf) k = element (keys, i); try h = keyHash (k); catch err error ("%s: %s", scope, regexprep (err.message, '^keyHash: ', '')); end_try_catch tf(i) = locate (this.hashes_, this.order_, this.keys_, k, h) > 0; endfor endfunction endmethods ################################################################################ ## ** Entries ** ## ################################################################################ ## Available Methods ## ## ## ## 'insert' 'lookup' 'remove' 'entries' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{d} =} insert (@var{d}, @var{keys}, @var{values}) ## @deftypefnx {dictionary} {@var{d} =} insert (@dots{}, @qcode{'Overwrite'}, @var{tf}) ## ## Add entries. ## ## @code{@var{d} = insert (@var{d}, @var{keys}, @var{values})} adds the ## entries of @var{keys} and @var{values} to @var{d}, as ## @code{@var{d}(@var{keys}) = @var{values}} does: @var{values} has the ## size of @var{keys}, or is one value for all, and both are converted to ## the types of @var{d}, which an unconfigured dictionary takes from them. ## A key already present has its value replaced and keeps its position. ## ## @code{@var{d} = insert (@dots{}, @qcode{'Overwrite'}, @var{tf})} with ## @var{tf} false leaves the value of a key already present unchanged, and ## a key repeated in @var{keys} takes its first value; it still adds the ## keys not yet present. @var{tf} is a logical or numeric scalar, true by ## default. The option name is matched in any case. ## ## @seealso{dictionary.remove, dictionary.lookup, dictionary.isKey} ## @end deftypefn function this = insert (this, keys, vals, varargin) scope = 'dictionary.insert'; if (nargin < 3) error ("%s: invalid number of input arguments.", scope); endif if (mod (numel (varargin), 2) != 0) error ("%s: name-value arguments must be in pairs.", scope); endif [overwrite, rest] = parsePairedArguments ({'Overwrite'}, {true}, ... varargin); if (! isempty (rest)) error ("%s: invalid optional paired argument.", scope); endif if (! ((islogical (overwrite) || isnumeric (overwrite)) && isscalar (overwrite) && ! isnan (overwrite))) error ("%s: 'Overwrite' must be a logical scalar.", scope); endif [this, errmsg] = addEntries (this, keys, vals, logical (overwrite)); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{v} =} lookup (@var{d}, @var{keys}) ## @deftypefnx {dictionary} {@var{v} =} lookup (@dots{}, @qcode{'FallbackValue'}, @var{fallback}) ## ## Return the values of keys. ## ## @code{@var{v} = lookup (@var{d}, @var{keys})} returns the values of ## @var{keys}, an array of keys, in an array of the same shape, as ## @code{@var{d}(@var{keys})} does; a key not in @var{d} is an error. ## ## @code{@var{v} = lookup (@dots{}, @qcode{'FallbackValue'}, ## @var{fallback})} returns @var{fallback} for each key not in @var{d} ## instead. @var{fallback} is a single value, converted to the value type ## of @var{d}, so that a dictionary of @qcode{'cell'} values needs a cell; ## it is checked only when some key is missing. The option name is ## matched in any case. ## ## @var{d} must be configured. ## ## @seealso{dictionary.isKey, dictionary.insert, dictionary.values} ## @end deftypefn function v = lookup (this, keys, varargin) scope = 'dictionary.lookup'; if (nargin < 2) error ("%s: invalid number of input arguments.", scope); endif if (mod (numel (varargin), 2) != 0) error ("%s: name-value arguments must be in pairs.", scope); endif names = varargin(1:2:end); names = names(cellfun (@(x) ischar (x) || isa (x, 'string'), names)); given = any (strcmpi (cellfun (@char, names, 'UniformOutput', false), ... 'FallbackValue')); [fallback, rest] = parsePairedArguments ({'FallbackValue'}, {[]}, ... varargin); if (! isempty (rest)) error ("%s: invalid optional paired argument.", scope); endif if (given) fallback = {fallback}; else fallback = {}; endif [v, errmsg] = lookupValues (this, {keys}, false, fallback); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{d} =} remove (@var{d}, @var{keys}) ## ## Remove entries. ## ## @code{@var{d} = remove (@var{d}, @var{keys})} removes the entries of ## @var{keys}, an array of keys converted to the key type of @var{d}, as ## @code{@var{d}(@var{keys}) = []} does. A key not in @var{d} is ignored. ## A key removed and added again goes after the others. @var{d} must be ## configured. ## ## @seealso{dictionary.insert, dictionary.isKey} ## @end deftypefn function this = remove (this, keys) scope = 'dictionary.remove'; if (nargin != 2) error ("%s: invalid number of input arguments.", scope); endif if (isempty (this.keyType_)) error ("%s: D is not configured.", scope); endif [this, errmsg] = removeEntries (this, keys); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{e} =} entries (@var{d}) ## @deftypefnx {dictionary} {@var{e} =} entries (@var{d}, @var{format}) ## ## Return the keys and values together. ## ## @code{@var{e} = entries (@var{d})} returns a table with the variables ## @qcode{Key} and @qcode{Value}, one row per entry in the order the keys ## were added: the columns @code{keys (@var{d})} and @code{values ## (@var{d})} return. Keys or values that a table cannot hold, function ## handles, dictionaries or values that cannot be joined, are refused. ## ## @code{@var{e} = entries (@var{d}, @var{format})} returns the table above ## for @var{format} @qcode{'table'}, the default; for @qcode{'struct'} a ## column structure array with the fields @qcode{Key} and @qcode{Value}, ## one element per entry; and for @qcode{'cell'} a cell array of two ## columns, each key and value in a cell of its own. @var{format} is ## matched in any case and by its first letters. ## ## @var{d} must be configured. ## ## @seealso{dictionary.keys, dictionary.values} ## @end deftypefn function e = entries (this, format = 'table') scope = 'dictionary.entries'; [format, errmsg] = formatName (format, {'table', 'struct', 'cell'}); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif if (isempty (this.keyType_)) error ("%s: D is not configured.", scope); endif switch (format) case 'cell' e = [this.keys_, this.values_]; case 'struct' e = struct ('Key', this.keys_, 'Value', this.values_); if (isempty (this.keys_)) e = reshape (e, 0, 1); endif case 'table' if (any (strcmp ({this.keyType_, this.valueType_}, ... 'function_handle')) || any (strcmp ({this.keyType_, this.valueType_}, 'dictionary'))) error (strcat ("%s: a table cannot hold these entries; use the", ... " 'cell' or 'struct' format."), scope); endif [k, errmsg] = join (this.keys_, this.keyEmpty_, this.keyType_, ... 'keys'); if (isempty (errmsg)) [v, errmsg] = join (this.values_, this.valueEmpty_, ... this.valueType_, 'values'); endif if (! isempty (errmsg)) error (strcat ("%s: a table cannot hold these entries; use the", ... " 'cell' or 'struct' format."), scope); endif e = table (k, v, 'VariableNames', {'Key', 'Value'}); endswitch endfunction endmethods ################################################################################ ## ** Files ** ## ################################################################################ ## Available Methods ## ## ## ## 'writedictionary' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {dictionary} {} writedictionary (@var{d}, @var{filename}) ## @deftypefnx {dictionary} {} writedictionary (@var{d}, @var{filename}, @var{Name}, @var{Value}) ## ## Write a dictionary to a JSON or an OpenDocument spreadsheet file. ## ## @code{writedictionary (@var{d}, @var{filename})} writes @var{d} as a ## JSON object to the file @var{filename}, a character vector or a string ## scalar, replacing any file of that name. Each entry is a member, in ## the order of the keys, written as follows: ## ## @itemize ## @item keys as text: numbers in their shortest form (@qcode{"1.5"}), ## logical values as @qcode{"true"} and @qcode{"false"}, dates and ## durations as they display; ## @item a @code{double} or @code{single} in the fewest digits that read ## back as its value, always with a point (@qcode{1.0}), with an exponent ## only from @math{10^21} up and below @math{10^-6}; integers exactly; ## @item @code{NaN} and @code{Inf} as @qcode{NaN}, @qcode{Infinity} and ## @qcode{-Infinity}, or as @qcode{null} under ## @qcode{'PreserveInfAndNaN'} false; missing strings, @code{NaT} and ## @code{missing} as @qcode{null}; logical values as @qcode{true} and ## @qcode{false}; ## @item dates and durations as text, as they display; ## @item a dictionary as an object, nested ones too; the contents of a ## cell as the value they hold, a vector of them as an array, and an empty ## one as @qcode{[]}. ## @end itemize ## ## @code{writedictionary (@var{d}, @var{filename}, @var{Name}, ## @var{Value})} specifies options with the following Name-Value paired ## arguments, matched in any case. ## ## @multitable @columnfractions 0.25 0.75 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'FileType'} @tab @qcode{'auto'} (the default) tells the ## type from the extension, @qcode{.json}, @qcode{.ods} or @qcode{.fods}; ## @qcode{'json'} or @qcode{'ods'} sets it. ## @item @qcode{'PrettyPrint'} @tab A logical scalar: true (the default) ## indents by four spaces, one member or element a line; false writes no ## spaces or line breaks. ## @item @qcode{'PreserveInfAndNaN'} @tab A logical scalar: true (the ## default) writes @code{NaN} and @code{Inf}, false writes @qcode{null}. ## @end multitable ## ## Nothing is written when any key or value cannot be. Values of types ## other than @code{string}, the numeric types, @code{logical}, ## @code{datetime}, @code{duration}, @code{missing}, @code{dictionary} ## and @code{cell} are refused, as in MATLAB, and so are matrices in ## cells and complex values, on which MATLAB fails; keys of types other ## than @code{string}, a cell array of text, the numeric types, ## @code{logical}, @code{datetime} and @code{duration} are refused, and so ## are missing string keys. ## ## An OpenDocument spreadsheet, @qcode{FileType} @qcode{'ods'}, holds one ## sheet per entry, named by the key and holding its @code{table} or ## @code{timetable}, written by @code{struct2ods}, so that ## @code{readdictionary} reads it back as it was. The keys must be text ## and the values tables or timetables, in a cell or not. Writing ODS is ## an extension of this package; it takes no option but ## @qcode{'FileType'}. ## ## Remote locations, which MATLAB writes to, are not supported. ## ## @seealso{readdictionary, struct2ods} ## @end deftypefn function writedictionary (this, filename, varargin) scope = 'dictionary.writedictionary'; if (nargin < 2 || ! ((ischar (filename) && rows (filename) <= 1) || (isa (filename, 'string') && isscalar (filename)))) error (strcat ("%s: FILENAME must be a character vector or a", ... " string scalar."), scope); endif filename = char (filename); if (mod (numel (varargin), 2) != 0) error ("%s: name-value arguments must be in pairs.", scope); endif [fileType, pretty, preserve, args] = parsePairedArguments ... ({'FileType', 'PrettyPrint', 'PreserveInfAndNaN'}, ... {[], [], []}, varargin(:)); if (! isempty (args)) error ("%s: invalid optional paired argument.", scope); endif if (! isempty (regexp (filename, '^(s3|wasbs?|hdfs|https?)://', 'once'))) error ("%s: writing to remote locations is not supported.", scope); endif [errmsg, fileType] = fileKind (fileType, filename); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif if (strcmp (fileType, 'ods')) if (! isempty (pretty) || ! isempty (preserve)) name = 'PrettyPrint'; if (isempty (pretty)) name = 'PreserveInfAndNaN'; endif error ("%s: '%s' does not apply to ODS files.", scope, name); endif errmsg = writeOds (this, filename); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif return; endif flags = {pretty, preserve}; names = {'PrettyPrint', 'PreserveInfAndNaN'}; for i = 1:2 if (isempty (flags{i})) flags{i} = true; elseif (! ((islogical (flags{i}) || isnumeric (flags{i})) && isscalar (flags{i}) && ! isnan (flags{i}))) error ("%s: '%s' must be a logical scalar.", scope, names{i}); endif endfor ## Every key and value is checked before anything is written [tree, errmsg] = jsonObject (this); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif errmsg = __dictionary2json__ (tree, filename, logical (flags{1}), ... logical (flags{2})); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif endfunction endmethods ################################################################################ ## ** Compare and hash ** ## ################################################################################ ## Available Methods ## ## ## ## 'isequal' 'isequaln' 'keyHash' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{tf} =} isequal (@var{d1}, @var{d2}, @dots{}) ## ## True when dictionaries hold the same entries. ## ## @code{@var{tf} = isequal (@var{d1}, @var{d2}, @dots{})} returns ## @code{true} when every argument is a dictionary with the same keys, in ## any order, each mapped to values that @code{isequal} finds equal, so ## that @code{NaN} values are unequal and values of different classes may ## be equal. Keys of different classes are different keys. Two ## unconfigured dictionaries are equal, an unconfigured and a configured ## one are not, and two configured dictionaries with no entries are equal ## whatever their types, as in MATLAB. ## ## @seealso{dictionary.isequaln, keyMatch} ## @end deftypefn function tf = isequal (varargin) if (nargin < 2) error ("dictionary.isequal: invalid number of input arguments."); endif tf = false; for i = 2:nargin if (! (isa (varargin{1}, 'dictionary') && isa (varargin{i}, 'dictionary') && sameEntries (varargin{1}, varargin{i}, false))) return; endif endfor tf = true; endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{tf} =} isequaln (@var{d1}, @var{d2}, @dots{}) ## ## True when dictionaries hold the same entries, @code{NaN} values equal. ## ## @code{@var{tf} = isequaln (@var{d1}, @var{d2}, @dots{})} is ## @code{isequal} with values compared by @code{isequaln}, so that ## @code{NaN} and other missing values are equal to themselves. ## ## @seealso{dictionary.isequal, keyMatch} ## @end deftypefn function tf = isequaln (varargin) if (nargin < 2) error ("dictionary.isequaln: invalid number of input arguments."); endif tf = false; for i = 2:nargin if (! (isa (varargin{1}, 'dictionary') && isa (varargin{i}, 'dictionary') && sameEntries (varargin{1}, varargin{i}, true))) return; endif endfor tf = true; endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {@var{key} =} keyHash (@var{d}) ## @deftypefnx {dictionary} {@var{key} =} keyHash (@var{d}, @var{base}) ## ## Generate a hash code for a dictionary. ## ## @code{@var{key} = keyHash (@var{d})} returns a 64-bit hash code of the ## dictionary @var{d} as a @qcode{uint64} scalar, from its key and value ## types and its entries in the order they were added, so that a ## dictionary can be a key of another. Unlike @code{isequal}, the code, ## and with it @code{keyMatch}, depends on the order of the entries and on ## the types, as in MATLAB. A dictionary whose keys or values ## @code{keyHash} cannot hash, such as tables, is refused. ## ## @code{@var{key} = keyHash (@var{d}, @var{base})} also generates a ## 64-bit hash code using @var{base} as the offset basis for the FNV-1a ## hash algorithm. @var{base} must be a @qcode{uint64} integer type ## scalar. Use this syntax to cascade @code{keyHash} on multiple objects ## for which a single hash code is required. ## ## Note that unlike MATLAB, this implementation does not use any random ## seed. As a result, @code{keyHash} will always generate the exact same ## hash key for any particular input across different workers and Octave ## sessions. ## ## @seealso{keyHash, keyMatch, dictionary.isequal} ## @end deftypefn function key = keyHash (this, base = []) if (! isempty (base)) if (! (isscalar (base) && isa (base, 'uint64'))) error ("dictionary.keyHash: BASE must be a UINT64 scalar."); endif key = __ckeyHash__ ('1x1dictionary', base); else key = __ckeyHash__ ('1x1dictionary'); endif key = __ckeyHash__ ({this.keyType_, this.valueType_}, key); for i = 1:numel (this.keys_) key = keyHash (this.keys_{i}, key); key = keyHash (this.values_{i}, key); endfor endfunction endmethods ################################################################################ ## ** Indexing and concatenation ** ## ################################################################################ ## Available Methods ## ## ## ## 'subsref' 'subsasgn' 'end' ## ## 'horzcat' 'vertcat' 'cat' ## ## ## ################################################################################ methods (Hidden) ## Lookup by '()' and '{}', and method calls by '.' function varargout = subsref (this, s) scope = 'dictionary.subsref'; switch (s(1).type) case '()' [out, errmsg] = lookupValues (this, s(1).subs, false); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif out = {out}; s = s(2:end); case '{}' [out, errmsg] = lookupValues (this, s(1).subs, true); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif s = s(2:end); case '.' name = s(1).subs; if (! (ischar (name) && any (strcmp (name, publicMethods ())))) error ("%s: no method named '%s'.", scope, char (name)); endif args = {}; if (numel (s) > 1 && strcmp (s(2).type, '()')) args = s(2).subs; s = s(3:end); else s = s(2:end); endif ## A method that returns nothing is called for its effect if (strcmp (name, 'writedictionary')) feval (name, this, args{:}); varargout = {}; return; endif out = cell (1, max (nargout, 1)); [out{:}] = feval (name, this, args{:}); endswitch if (isempty (s)) varargout = out; else varargout = cell (1, max (nargout, 1)); [varargout{:}] = subsref (out{1}, s); endif endfunction ## Insertion and update by '()' and '{}', deletion by '() = []' function this = subsasgn (this, s, val) scope = 'dictionary.subsasgn'; switch (s(1).type) case '()' if (numel (s(1).subs) != 1) error ("%s: index with one array of keys.", scope); endif K = s(1).subs{1}; if (numel (s) > 1) ## Assigning into a value needs it there, as in MATLAB [cur, errmsg] = lookupValues (this, s(1).subs, false); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif val = subsasgn (cur, s(2:end), val); elseif (isa (val, 'double') && isequal (size (val), [0, 0])) if (isempty (this.keyType_)) error ("%s: D is not configured.", scope); endif [this, errmsg] = removeEntries (this, K); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif return; endif [this, errmsg] = addEntries (this, K, val); case '{}' if (! (isempty (this.keyType_) || strcmp (this.valueType_, 'cell'))) error ("%s: '{}' indexing needs a dictionary of 'cell' values.", ... scope); endif if (numel (s(1).subs) != 1) error ("%s: index with one array of keys.", scope); endif K = s(1).subs{1}; if (! isempty (this.keyType_)) [K, errmsg] = convert (K, this.keyType_, this.keyEmpty_, 'key'); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif endif if (prod (arraySize (asStored (K))) != 1) error ("%s: '{}' assignment takes a single key.", scope); endif if (numel (s) > 1) ## The contents of a new key start empty, as in MATLAB cur = []; if (! isempty (this.keyType_) && isKey (this, K)) cur = subsref (this, s(1)); endif val = subsasgn (cur, s(2:end), val); endif [this, errmsg] = addEntries (this, K, {val}); case '.' error ("%s: a dictionary has no properties to assign.", scope); endswitch if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif endfunction function last = end (this, k, n) error ("dictionary.end: 'end' is not supported for a dictionary."); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {dictionary} {} horzcat (@var{d1}, @var{d2}, @dots{}) ## ## Refuse to concatenate: a dictionary is a scalar object. ## ## @code{[@var{d1}, @var{d2}]} raises an error, whatever the other ## arguments, as in MATLAB. Add the entries of one dictionary to another ## by assignment instead. ## ## @seealso{dictionary.vertcat, dictionary.cat} ## @end deftypefn function out = horzcat (varargin) error ("dictionary.horzcat: dictionaries cannot be concatenated."); endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {} vertcat (@var{d1}, @var{d2}, @dots{}) ## ## Refuse to concatenate: a dictionary is a scalar object. ## ## @code{[@var{d1}; @var{d2}]} raises an error, whatever the other ## arguments, as in MATLAB. ## ## @seealso{dictionary.horzcat, dictionary.cat} ## @end deftypefn function out = vertcat (varargin) error ("dictionary.vertcat: dictionaries cannot be concatenated."); endfunction ## -*- texinfo -*- ## @deftypefn {dictionary} {} cat (@var{dim}, @var{d1}, @var{d2}, @dots{}) ## ## Refuse to concatenate: a dictionary is a scalar object. ## ## @code{cat (@var{dim}, @var{d1}, @var{d2}, @dots{})} raises an error, ## whatever the dimension and the other arguments, as in MATLAB. ## ## @seealso{dictionary.horzcat, dictionary.vertcat} ## @end deftypefn function out = cat (varargin) error ("dictionary.cat: dictionaries cannot be concatenated."); endfunction endmethods methods (Access = private) ## The line describing the dictionary, by its types and number of entries function t = summaryLine (this) if (isempty (this.keyType_)) t = 'dictionary with unset key and value types'; return; endif n = numel (this.keys_); if (n == 0) count = 'no entries'; elseif (n == 1) count = '1 entry'; else count = sprintf ('%d entries', n); endif t = sprintf ('dictionary (%s ⟼ %s) with %s', this.keyType_, ... this.valueType_, count); endfunction ## True when OTHER has the same key type and holds the keys of this ## dictionary, in any order, with values equal by isequal or, for ## NANEQUAL, by isequaln; the value types may differ. function tf = sameEntries (this, other, nanEqual) if (isempty (this.keyType_) || isempty (other.keyType_)) tf = isempty (this.keyType_) && isempty (other.keyType_); return; endif tf = false; if (! strcmp (this.keyType_, other.keyType_) || numel (this.keys_) != numel (other.keys_)) return; endif for i = 1:numel (this.keys_) k = this.keys_{i}; e = locate (other.hashes_, other.order_, other.keys_, k, keyHash (k)); if (e == 0) return; endif if (nanEqual) same = isequaln (this.values_{i}, other.values_{e}); else same = isequal (this.values_{i}, other.values_{e}); endif if (! same) return; endif endfor tf = true; endfunction ## The values of the keys in SUBS, the subscripts of '()' or, for BRACE, ## of '{}': one array of the shape of the keys, or the contents of the ## cells in a cell array. FALLBACK, when given as {value}, stands for the ## keys not in the dictionary. function [out, errmsg] = lookupValues (this, subs, brace, fallback = {}) out = []; errmsg = ''; if (isempty (this.keyType_)) errmsg = "D is not configured."; return; endif if (brace && ! strcmp (this.valueType_, 'cell')) errmsg = "'{}' indexing needs a dictionary of 'cell' values."; return; endif if (numel (subs) != 1) errmsg = "index with one array of keys."; return; endif K = subs{1}; ## Octave cannot tell d(:) from d(':'), so ':' is the key ":" colon = ischar (K) && strcmp (K, ':'); [K, errmsg] = convert (K, this.keyType_, this.keyEmpty_, 'key'); if (! isempty (errmsg)) return; endif ksz = arraySize (K); n = prod (ksz); idx = zeros (n, 1); for i = 1:n k = element (K, i); try h = keyHash (k); catch err errmsg = regexprep (err.message, '^keyHash: ', ''); return; end_try_catch idx(i) = locate (this.hashes_, this.order_, this.keys_, k, h); if (idx(i) == 0 && isempty (fallback)) if (colon) errmsg = "':' is not supported; use 'keys' or 'values'."; elseif (n == 1) errmsg = "key not found."; else errmsg = sprintf ("element %d of the keys not found.", i); endif return; endif endfor if (brace) out = cellfun (@(c) c{1}, this.values_(idx)', 'UniformOutput', false); return; endif items = cell (n, 1); found = idx > 0; items(found) = this.values_(idx(found)); if (! all (found)) ## The fallback is read only when a key needs it, as in MATLAB [fb, errmsg] = convert (fallback{1}, this.valueType_, ... this.valueEmpty_, 'value'); if (! isempty (errmsg)) return; endif ## Checked once converted, so that text is one string or date if (! isequal (arraySize (fb), [1, 1])) errmsg = "'FallbackValue' must be a scalar."; return; endif items(! found) = {fb}; endif [out, errmsg] = join (items, this.valueEmpty_, this.valueType_, ... 'values'); if (! isempty (errmsg)) errmsg = [errmsg "; look up each key separately."]; elseif (isArray (out) && ! isequal (size (out), ksz)) out = reshape (out, ksz); endif endfunction ## Remove the entries of the keys K; a key not in the dictionary is ## ignored. function [this, errmsg] = removeEntries (this, K) [K, errmsg] = convert (K, this.keyType_, this.keyEmpty_, 'key'); if (! isempty (errmsg)) return; endif drop = false (numel (this.keys_), 1); for i = 1:prod (arraySize (K)) k = element (K, i); try h = keyHash (k); catch err errmsg = regexprep (err.message, '^keyHash: ', ''); return; end_try_catch e = locate (this.hashes_, this.order_, this.keys_, k, h); if (e > 0) drop(e) = true; endif endfor if (! any (drop)) return; endif keep = ! drop; renumber = zeros (size (keep)); renumber(keep) = 1:nnz (keep); sorted = keep(this.order_); this.hashes_ = this.hashes_(sorted); this.order_ = renumber(this.order_(sorted)); this.keys_ = this.keys_(keep); this.values_ = this.values_(keep); endfunction ## Add the entries of the keys K and the values V, configuring the ## dictionary from them when it has no types yet. Without OVERWRITE a key ## already present, or repeated in K, keeps its first value. function [this, errmsg] = addEntries (this, K, V, overwrite = true) if (isempty (this.keyType_)) K = asStored (K); V = asStored (V); if (! dictionary.hashable (class (K))) errmsg = sprintf ("unsupported key type '%s'.", class (K)); return; endif this.keyType_ = class (K); this.valueType_ = class (V); else [K, errmsg] = convert (K, this.keyType_, this.keyEmpty_, 'key'); if (! isempty (errmsg)) return; endif [V, errmsg] = convert (V, this.valueType_, this.valueEmpty_, 'value'); if (! isempty (errmsg)) return; endif endif ksz = arraySize (K); vsz = arraySize (V); if (! (isequal (ksz, vsz) || prod (vsz) == 1)) errmsg = "KEYS and VALUES must be the same size, or VALUES a scalar."; return; endif errmsg = ''; ## A dictionary with no entries takes its empty columns, which carry a ## time zone or categories, from the first entries added if (isempty (this.keys_)) this.keyEmpty_ = emptyOf (K); this.valueEmpty_ = emptyOf (V); endif ## Work on locals, assigned back once keys = this.keys_; vals = this.values_; hashes = this.hashes_; order = this.order_; n = numel (keys); keys(end+1:end+prod (ksz),1) = {[]}; vals(end+1:end+prod (ksz),1) = {[]}; for i = 1:prod (ksz) k = element (K, i); if (prod (vsz) == 1) v = V; else v = element (V, i); endif try h = keyHash (k); catch err errmsg = regexprep (err.message, '^keyHash: ', ''); return; end_try_catch e = locate (hashes, order, keys, k, h); if (e > 0) if (overwrite) vals{e} = v; endif else n += 1; keys{n} = k; vals{n} = v; p = lookup (hashes, h); hashes = [hashes(1:p); h; hashes(p+1:end)]; order = [order(1:p); n; order(p+1:end)]; endif endfor this.keys_ = keys(1:n); this.values_ = vals(1:n); this.hashes_ = hashes; this.order_ = order; endfunction endmethods methods (Static, Hidden) ## A configured dictionary with no entries, for configureDictionary, ## which validates both type names. function d = configured (keyType, valueType) d = dictionary (); d.keyType_ = keyType; d.valueType_ = valueType; d.keyEmpty_ = emptyColumn (keyType); d.valueEmpty_ = emptyColumn (valueType); endfunction ## True for a key type keyHash can hash: a built-in type or a class with ## a keyHash method. function tf = hashable (type) tf = (any (strcmp (type, {'double', 'single', 'int8', 'int16', ... 'int32', 'int64', 'uint8', 'uint16', ... 'uint32', 'uint64', 'logical', 'char', ... 'cell', 'struct', 'function_handle'})) || any (strcmp (methods (type), 'keyHash'))); endfunction ## A dictionary is a scalar object, so there is no empty one function d = empty (varargin) error ("dictionary.empty: a dictionary cannot be an empty array."); endfunction endmethods endclassdef ## True for an array whose elements are keys or values one by one. A table ## is one value whatever its size, and a dictionary or a function handle is ## always one; an object of another class is one when it is a single object. function tf = isArray (x) if (istabular (x) || isa (x, 'dictionary') || is_function_handle (x)) tf = false; elseif (isForeign (x)) tf = builtin ('numel', x) != 1; else tf = true; endif endfunction ## True for an object of a class other than the arrays of this package, whose ## own size and indexing may mean something else, as for 'timeseries' function tf = isForeign (x) tf = (isobject (x) && ! (isa (x, 'string') || isa (x, 'datetime') || isa (x, 'duration') || isa (x, 'calendarDuration') || isa (x, 'categorical') || isa (x, 'missing'))); endfunction function sz = arraySize (x) if (! isArray (x)) sz = [1, 1]; elseif (isForeign (x)) sz = builtin ('size', x); else sz = size (x); endif endfunction function x = element (x, i) if (! isArray (x)) return; elseif (isForeign (x)) x = builtin ('subsref', x, substruct ('()', {i})); else x = x(i); endif endfunction ## Keys and values as stored: a character array as a string array of its ## rows, a sparse matrix as a full one. function x = asStored (x) if (ischar (x)) x = string (x); elseif (issparse (x)) x = full (x); endif endfunction ## An empty column of the class of X, keeping its time zone or categories function e = emptyOf (x) if (istabular (x)) e = x(zeros (0, 1), :); elseif (isArray (x) && ! isForeign (x)) e = x(:); e = e(zeros (0, 1)); else e = emptyColumn (class (x)); endif endfunction ## An empty column of the class TYPE, or [] for a class that has none function e = emptyColumn (type) switch (type) case {'double', 'single', 'int8', 'int16', 'int32', 'int64', ... 'uint8', 'uint16', 'uint32', 'uint64'} e = zeros (0, 1, type); case 'logical' e = false (0, 1); case 'cell' e = cell (0, 1); case 'struct' e = repmat (struct (), 0, 1); case {'function_handle', 'dictionary'} e = []; otherwise try e = feval (type); if (istabular (e)) e = e(zeros (0, 1), :); else e = e(zeros (0, 1)); endif catch e = []; end_try_catch endswitch endfunction ## X converted to the class TYPE, its empty column EMPTY, as a WHAT ('key' or ## 'value') of a dictionary: by the function of the type's name, as in ## MATLAB, except where noted. function [x, errmsg] = convert (x, type, empty, what) errmsg = ''; if (issparse (x)) x = full (x); endif if (strcmp (class (x), type)) if (isa (x, 'datetime') && strcmp (what, 'key')) x = inZone (x, empty); endif return; endif bad = sprintf ("unable to use '%s' as %s of a dictionary of '%s' %ss.", class (x), what, type, what); switch (type) case {'cell', 'struct'} ## A cell is never wrapped, as in MATLAB, and an object never becomes ## a structure of its private properties, as it does in MATLAB errmsg = bad; return; case 'logical' ## A character is true for a nonzero code, as in MATLAB if (ischar (x)) x = double (x); endif case 'datetime' if (ischar (x) || iscellstr (x) || isa (x, 'string')) ## Text is read in the time zone of the dictionary, if any try x = datetime (x); catch errmsg = bad; return; end_try_catch if (isa (empty, 'datetime') && ! isempty (empty.TimeZone)) x.TimeZone = empty.TimeZone; endif return; endif case 'table' ## Named as MATLAB names it, not after this function's variable; text ## is refused, as in MATLAB if (ischar (x)) errmsg = bad; return; endif try x = table (x, 'VariableNames', {'Var1'}); catch errmsg = bad; end_try_catch return; endswitch try x = feval (type, x); catch errmsg = bad; return; end_try_catch if (! strcmp (class (x), type)) errmsg = bad; endif endfunction ## The datetime X moved to the time zone of the datetime EMPTY, when both ## have one; a date without a zone is a different key from any with one. function x = inZone (x, empty) if (isa (empty, 'datetime') && ! isempty (empty.TimeZone) && ! isempty (x.TimeZone)) x.TimeZone = empty.TimeZone; endif endfunction ## The entry holding the key K, with hash code H, or 0 function e = locate (hashes, order, keys, k, h) e = 0; p = lookup (hashes, h); while (p > 0 && hashes(p) == h) if (isequaln (keys{order(p)}, k)) e = order(p); return; endif p -= 1; endwhile endfunction ## The ITEMS, one key or value per cell, joined into a column of the class ## TYPE, its empty column EMPTY. function [x, errmsg] = join (items, empty, type, what) x = []; errmsg = ''; if (isempty (items) && strcmp (class (empty), type)) x = empty; return; endif ## Tables of several rows would lose which rows belong to which key if (! (istabular (empty) && any (cellfun ('height', items) != 1))) try x = vertcat (empty, items{:}); return; catch end_try_catch endif if (isscalar (items)) x = items{1}; return; endif errmsg = sprintf ("cannot join %s of type '%s'", what, type); endfunction ## The text of one key or value X in a dictionary display: the contents of ## a cell in braces, a handle as written, and anything without a text of its ## own by its size and class. function t = itemText (x) if (iscell (x)) t = ['{', cellText(x{1}), '}']; elseif (is_function_handle (x)) t = handleText (x); elseif (isnumeric (x) || islogical (x) || hasText (x)) t = scalarText (x); else t = sizeClass (x); endif endfunction ## The text of the contents X of a cell, as a cell array shows them function t = cellText (x) if (ischar (x) && rows (x) == 1) t = oneLine (["'", x, "'"]); elseif (is_function_handle (x)) t = handleText (x); elseif (isa (x, 'dictionary')) ## A dictionary is one object; indexing it would look up a key t = ['[', dispstrings(x){1}, ']']; elseif (! isempty (x) && isrow (x) && (isnumeric (x) || islogical (x) || hasText (x))) t = cell (1, columns (x)); for i = 1:columns (x) t{i} = scalarText (x(i)); endfor t = ['[', strjoin(t, ' '), ']']; else t = sizeClass (x); endif endfunction ## The text of the scalar X: a number as Octave shows it, a string quoted on ## one line, and the other classes of this package by 'dispstrings' function t = scalarText (x) if (isnumeric (x) || islogical (x)) t = strtrim (disp (x)); elseif (isa (x, 'string') && ! ismissing (x)) t = oneLine (['"', char(x), '"']); else t = dispstrings (x){1}; endif endfunction ## The text T on one line, its breaks and tabs shown as a string array ## shows them function t = oneLine (t) t = strrep (t, sprintf ("\r\n"), '↵'); t = strrep (t, sprintf ("\n"), '↵'); t = strrep (t, sprintf ("\t"), '→'); endfunction ## True for a class whose values 'dispstrings' writes as text function tf = hasText (x) tf = (isa (x, 'string') || isa (x, 'datetime') || isa (x, 'duration') || isa (x, 'calendarDuration') || isa (x, 'categorical') || isa (x, 'missing') || isa (x, 'dictionary')); endfunction function t = handleText (f) t = func2str (f); if (t(1) != '@') t = ['@', t]; endif endfunction function t = sizeClass (x) t = sprintf ('%dx', size (x)); t = sprintf ('%s %s', t(1:end-1), class (x)); endfunction ## The number of characters in the UTF-8 text T, which has more bytes function n = textWidth (t) n = numel (t) - nnz (t >= 128 & t < 192); endfunction ## The file type named by TYPE, or told from the extension of FILENAME function [errmsg, type] = fileKind (type, filename) errmsg = ''; if (isempty (type)) type = 'auto'; endif if (! ((ischar (type) && rows (type) == 1) || (isa (type, 'string') && isscalar (type)))) errmsg = "'FileType' must be a character vector or a string scalar."; return; endif type = lower (char (type)); if (! any (strcmp (type, {'auto', 'json', 'ods'}))) errmsg = sprintf ("invalid 'FileType' value '%s'.", char (type)); return; endif if (strcmp (type, 'auto')) [~, ~, ext] = fileparts (filename); switch (lower (ext)) case '.json' type = 'json'; case {'.ods', '.fods'} type = 'ods'; otherwise errmsg = strcat ("unable to tell the file type from the", ... " extension; set 'FileType'."); endswitch endif endfunction ## The JSON tree of the dictionary D: an object of its keys and values as ## __dictionary2json__ writes them, or an error message function [tree, errmsg] = jsonObject (d) errmsg = ''; tree = struct ('keys', {cell(0, 1)}, 'values', {cell(0, 1)}); if (! isConfigured (d) || numEntries (d) == 0) return; endif [kt, vt] = types (d); kt = char (kt); vt = char (vt); if (! any (strcmp (vt, {'string', 'double', 'single', 'int8', 'uint8', ... 'int16', 'uint16', 'int32', 'uint32', 'int64', ... 'uint64', 'logical', 'datetime', 'duration', ... 'missing', 'dictionary', 'cell'}))) errmsg = sprintf ("unable to write values of type '%s' to JSON.", vt); return; endif K = keys (d, 'cell'); V = values (d, 'cell'); for i = 1:numel (K) [K{i}, errmsg] = jsonKey (K{i}, kt); if (! isempty (errmsg)) return; endif endfor for i = 1:numel (V) if (strcmp (vt, 'cell')) [V{i}, errmsg] = jsonContent (V{i}{1}); else [V{i}, errmsg] = jsonContent (V{i}); endif if (! isempty (errmsg)) return; endif endfor tree = struct ('keys', {K}, 'values', {V}); endfunction ## The key K of the key type KT as __dictionary2json__ writes it function [k, errmsg] = jsonKey (k, kt) errmsg = ''; switch (kt) case 'string' if (ismissing (k)) errmsg = "unable to write a missing string key to JSON."; return; endif k = char (k); case 'cell' k = k{1}; if (! (ischar (k) && rows (k) <= 1)) errmsg = "unable to write keys of type 'cell' to JSON."; return; endif case {'double', 'single', 'int8', 'uint8', 'int16', 'uint16', ... 'int32', 'uint32', 'int64', 'uint64', 'logical'} if (iscomplex (k)) errmsg = "unable to write complex values to JSON."; endif case {'datetime', 'duration'} k = char (k); otherwise errmsg = sprintf ("unable to write keys of type '%s' to JSON.", kt); endswitch endfunction ## The value X, or the contents of a cell value, as __dictionary2json__ ## writes it: text, numbers, logical values, null, arrays and objects function [v, errmsg] = jsonContent (x) errmsg = ''; v = []; null = repmat (struct (), 0, 0); if (isa (x, 'dictionary')) [v, errmsg] = jsonObject (x); return; endif if (isnumeric (x) && iscomplex (x)) errmsg = "unable to write complex values to JSON."; return; endif if (ischar (x)) if (rows (x) > 1) errmsg = "unable to write matrices to JSON."; return; endif v = x; return; endif isText = (isa (x, 'string') || isa (x, 'datetime') || isa (x, 'duration') || isa (x, 'missing')); if (! (isnumeric (x) || islogical (x) || iscell (x) || isText)) errmsg = sprintf ("unable to write values of type '%s' to JSON.", ... class (x)); return; endif if (isempty (x)) v = cell (0, 1); return; endif if (! isvector (x)) errmsg = "unable to write matrices to JSON."; return; endif if (isnumeric (x) || islogical (x)) v = x; return; endif ## Text and cells, one element at a time n = numel (x); c = cell (n, 1); for i = 1:n if (iscell (x)) [c{i}, errmsg] = jsonContent (x{i}); if (! isempty (errmsg)) return; endif elseif (isa (x, 'missing') || ismissing (x(i))) c{i} = null; else c{i} = char (x(i)); endif endfor if (n == 1 && ! iscell (x)) v = c{1}; else v = c; endif endfunction ## Write the dictionary D of tables or timetables to the OpenDocument ## spreadsheet FILENAME, a sheet per entry named by its key function errmsg = writeOds (d, filename) errmsg = ''; if (! isConfigured (d) || numEntries (d) == 0) errmsg = "a dictionary with no entries has no sheets to write."; return; endif [kt, vt] = types (d); K = keys (d, 'cell'); V = values (d, 'cell'); for i = 1:numel (K) if (strcmp (kt, 'cell')) K{i} = K{i}{1}; endif if (! ((ischar (K{i}) && rows (K{i}) <= 1) || (isa (K{i}, 'string') && ! ismissing (K{i})))) errmsg = "the keys must be text to name the sheets of an ODS file."; return; endif K{i} = char (K{i}); if (strcmp (vt, 'cell')) V{i} = V{i}{1}; endif if (! (isa (V{i}, 'table') || isa (V{i}, 'timetable'))) errmsg = strcat ("only tables and timetables can be written to", ... " ODS files."); return; endif endfor ## Fields named after the keys, each key kept as its sheet name f = matlab.lang.makeUniqueStrings (matlab.lang.makeValidName (K)); s = struct (); for i = 1:numel (K) t = V{i}; if (! strcmp (f{i}, K{i})) if (! any (strcmp (properties (t.Properties.CustomProperties), ... 'ActualSheetName'))) t = addprop (t, 'ActualSheetName', 'table'); endif t.Properties.CustomProperties.ActualSheetName = K{i}; endif s.(f{i}) = t; endfor struct2ods (filename, s); endfunction ## The methods '.' may call function names = publicMethods () names = {'types', 'isConfigured', 'numEntries', 'numel', 'length', ... 'size', 'ndims', 'isempty', 'keys', 'values', 'isKey', ... 'insert', 'lookup', 'remove', 'entries', 'isequal', 'isequaln', ... 'keyHash', 'dispstrings', 'writedictionary', 'horzcat', ... 'vertcat', 'cat'}; endfunction ## The one of OPTIONS that FORMAT names, in any case or by its first letters function [format, errmsg] = formatName (format, options) errmsg = ''; if (! ((ischar (format) && rows (format) <= 1) || (isa (format, 'string') && isscalar (format)))) errmsg = "FORMAT must be a character vector or a string scalar."; return; endif format = char (format); match = ! isempty (format) & strncmpi (format, options, numel (format)); if (nnz (match) != 1) errmsg = sprintf ("invalid format '%s'.", format); return; endif format = options{match}; endfunction ## 'pkg test' reads the BISTs from the file it is testing, so a class ## whose suite lives in inst/tests/ reads as carrying none. This block ## answers for it and checks nothing itself. %!test %! ## The suite for this class is inst/tests/dictionary.m-tst. pr0m1th3as-datatypes-9450b29/inst/discretize.m000066400000000000000000000373171527370255000212110ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{bin} =} discretize (@var{X}, @var{edges}) ## @deftypefnx {datatypes} {@var{bin} =} discretize (@var{X}, @var{N}) ## @deftypefnx {datatypes} {@var{Y} =} discretize (@dots{}, @var{values}) ## @deftypefnx {datatypes} {@var{C} =} discretize (@dots{}, 'categorical') ## @deftypefnx {datatypes} {@var{C} =} discretize (@dots{}, 'categorical', @var{names}) ## @deftypefnx {datatypes} {@var{Y} =} discretize (@dots{}, 'IncludedEdge', @var{side}) ## @deftypefnx {datatypes} {[@var{bin}, @var{edges}] =} discretize (@dots{}) ## ## Group data into bins or categories. ## ## @code{@var{bin} = discretize (@var{X}, @var{edges})} returns an array of the ## same size as @var{X} whose elements give the index of the bin that each value ## of @var{X} falls into. @var{edges} must be a real numeric or logical vector ## of monotonically non-decreasing values, and defines @code{numel (@var{edges}) ## - 1} bins. Bin @math{j} covers the half-open interval @code{[@var{edges}(j), ## @var{edges}(j+1))}, except the last bin, which is closed at both ends. ## Values outside @code{[@var{edges}(1), @var{edges}(end)]}, and any ## @qcode{NaN}, return @qcode{NaN}. @var{bin} is always of type @qcode{double}. ## ## Repeated edges are permitted and meaningful: they define empty bins, which ## are simply never selected. @code{discretize ([1, 2, 3], [1, 2, 2, 3])} ## returns @code{[1, 3, 3]}. ## ## @code{@var{bin} = discretize (@var{X}, @var{N})} uses @var{N} bins of uniform ## width spanning the range of @var{X}, where @var{N} is a positive integer ## scalar. The edges are placed at "nice" decimal positions rather than exactly ## at @code{min (@var{X})} and @code{max (@var{X})}, so the bins generally ## extend slightly beyond the data. @qcode{NaN} and infinite values are ignored ## when determining the range. This syntax is not available when @var{X} is of ## an integer type; supply explicit edges instead. ## ## @code{@var{Y} = discretize (@dots{}, @var{values})} returns the corresponding ## element of @var{values} in place of the bin index, so @var{values} must be a ## vector whose length equals the number of bins. @var{Y} takes the type of ## @var{values}. Elements of @var{X} that fall in no bin return @qcode{NaN} ## when @var{values} is a floating-point array, zero when it is of an integer ## type, and raise an error when it is a cell array. ## ## @code{@var{C} = discretize (@dots{}, 'categorical')} returns an ordinal ## @qcode{categorical} array whose categories are named after the bin intervals, ## for example @qcode{'[1, 3)'}. @code{@var{C} = discretize (@dots{}, ## 'categorical', @var{names})} names the categories explicitly; @var{names} ## must be a cell array of character vectors or a @qcode{string} vector whose ## length equals the number of bins. ## ## @code{@var{Y} = discretize (@dots{}, 'IncludedEdge', @var{side})} selects ## which end of each bin is closed. @var{side} may be @qcode{'left'} (the ## default, giving @code{[@var{edges}(j), @var{edges}(j+1))} with the last bin ## closed at both ends) or @qcode{'right'} (giving @code{(@var{edges}(j), ## @var{edges}(j+1)]} with the first bin closed at both ends). ## ## @code{[@var{bin}, @var{edges}] = discretize (@dots{})} also returns the bin ## edges used. When the edges were supplied they are returned unchanged; when a ## bin count was requested they are returned as a row vector. ## ## @seealso{histcounts, categorical} ## @end deftypefn function [BIN, EDGES] = discretize (X, arg2, varargin) ## Input validation if (nargin < 2) error ("discretize: not enough input arguments."); endif if (! (isnumeric (X) || islogical (X)) || ! isreal (X)) error ("discretize: X must be a real numeric or logical array."); endif ## Split off a trailing 'IncludedEdge', SIDE pair from the optional arguments args = varargin; side = 'left'; for k = numel (args) - 1:-1:1 if (isTextScalar (args{k}) && strcmpi (charOf (args{k}), 'IncludedEdge')) side = args{k+1}; args(k:k+1) = []; break; endif endfor if (! isTextScalar (side) || ! any (strcmpi (charOf (side), {'left', ... 'right'}))) error ("discretize: 'IncludedEdge' value must be 'left' or 'right'."); endif leftClosed = strcmpi (charOf (side), 'left'); ## Determine the bin edges, either supplied or derived from a bin count if (isscalar (arg2) && ! islogical (arg2)) if (! isnumeric (arg2) || ! isreal (arg2) || ! isfinite (arg2) || arg2 < 1 || fix (arg2) != arg2) error ("discretize: N must be a real positive integer."); endif if (isinteger (X)) error (strcat ("discretize: when X is an integer data type,", ... " specify bin edges instead of number of bins.")); endif nbins = double (arg2); xf = double (X(isfinite (X))); if (isempty (xf)) EDGES = 0:nbins; else EDGES = __binedges__ (min (xf), max (xf), nbins); endif if (isa (X, 'single')) EDGES = single (EDGES); endif else EDGES = arg2; if (! (isnumeric (EDGES) || islogical (EDGES)) || ! isreal (EDGES) || ! isvector (EDGES) || numel (EDGES) < 2 || any (isnan (EDGES(:))) || any (diff (double (EDGES(:))) < 0)) error (strcat ("discretize: EDGES must be a vector that is real,", ... " numeric or logical, and monotonically increasing.")); endif nbins = numel (EDGES) - 1; endif ## Assign each element to a bin ev = double (EDGES(:)).'; xv = double (X(:)); BIN = reshape (__binassign__ (xv, ev, leftClosed), size (X)); ## Map bin indices onto values or categories, if requested if (isempty (args)) return; endif if (isTextScalar (args{1}) && strcmpi (charOf (args{1}), 'categorical')) if (numel (args) > 1) names = args{2}; if (isa (names, 'string')) names = cellstr (names); endif if (! iscellstr (names) || numel (names) != nbins) error (strcat ("discretize: category names must be a cellstr or", ... " string vector with length equal to the number", ... " of bins.")); endif names = names(:).'; else names = defaultLabels (double (EDGES(:)).', leftClosed); endif BIN = categorical (BIN, 1:nbins, names, 'Ordinal', true); else values = args{1}; if (! isvector (values) || numel (values) != nbins) error (strcat ("discretize: VALUES must be a vector with length", ... " equal to the number of bins.")); endif isbinned = ! isnan (BIN); if (iscell (values)) if (! all (isbinned(:))) error (strcat ("discretize: all elements of X must belong to a bin", ... " when VALUES is a cell array.")); endif BIN = reshape (values(BIN(:)), size (X)); else out = zeros (size (BIN), class (values)); if (isfloat (values)) out(:) = NaN; endif out(isbinned) = values(BIN(isbinned)); BIN = out; endif endif endfunction ## Default category labels naming each bin interval function names = defaultLabels (ev, leftClosed) nbins = numel (ev) - 1; names = cell (1, nbins); for ii = 1:nbins lo = sprintf ("%g", ev(ii)); hi = sprintf ("%g", ev(ii+1)); if (leftClosed) if (ii == nbins) names{ii} = sprintf ("[%s, %s]", lo, hi); else names{ii} = sprintf ("[%s, %s)", lo, hi); endif else if (ii == 1) names{ii} = sprintf ("[%s, %s]", lo, hi); else names{ii} = sprintf ("(%s, %s]", lo, hi); endif endif endfor endfunction ## True for a character vector or a scalar string function tf = isTextScalar (x) tf = (ischar (x) && isrow (x)) || (isa (x, 'string') && isscalar (x)); endfunction ## Character vector of a char or scalar string function s = charOf (x) if (isa (x, 'string')) s = char (x); else s = x; endif endfunction %!demo %! ## Bin data with explicit edges. Bins are closed on the left, except the %! ## last, which is closed at both ends. %! %! discretize ([1, 2, 3, 4, 5], [1, 3, 5]) %!demo %! ## Ask for a bin count instead. The edges land on round numbers, so they %! ## generally extend a little beyond the data. %! %! [bin, edges] = discretize ([1, 2, 3, 4, 5], 3) %!demo %! ## Return a label per bin rather than an index. Every value must fall in a %! ## bin when the labels are a cell array. %! %! discretize ([1, 2, 3, 4, 5], [1, 3, 5], {'low', 'high'}) ## Explicit edges %!assert_equal (discretize ([1, 2, 3, 4, 5], [1, 3, 5]), [1, 1, 2, 2, 2]) %!assert_equal (discretize ([0, 1, 2, 3, 4, 5, 6], [1, 3, 5]), ... %! [NaN, 1, 1, 2, 2, 2, NaN]) %!assert_equal (class (discretize ([1, 2, 3], [1, 2, 3])), 'double') %!assert_equal (class (discretize (single ([1, 2, 3]), [1, 2, 3])), 'double') %!assert_equal (discretize (int8 ([1, 2, 3]), [1, 2, 3]), [1, 2, 2]) %!assert_equal (discretize (logical ([0, 1, 1]), [0, 1, 2]), [1, 2, 2]) ## Repeated edges define empty bins %!assert_equal (discretize ([1, 2, 3], [1, 1, 2, 3]), [2, 3, 3]) %!assert_equal (discretize ([1, 2, 3], [1, 2, 2, 3]), [1, 3, 3]) %!assert_equal (discretize (2, [1, 2, 2, 3]), 3) ## Missing and infinite values %!assert_equal (discretize ([-Inf, 0, Inf, NaN], [0, 1, 2]), [NaN, 1, NaN, NaN]) %!assert_equal (discretize ([-Inf, 0, Inf, NaN], [-Inf, 1, Inf]), ... %! [1, 1, 2, NaN]) ## Included edge %!assert_equal (discretize ([1, 2, 3, 4, 5], [1, 3, 5], 'IncludedEdge', ... %! 'left'), ... %! [1, 1, 2, 2, 2]) %!assert_equal (discretize ([1, 2, 3, 4, 5], [1, 3, 5], 'IncludedEdge', ... %! 'right'), ... %! [1, 1, 1, 2, 2]) %!assert_equal (discretize ([0, 1, 2, 3, 4, 5, 6], [1, 3, 5], ... %! 'IncludedEdge', 'right'), [NaN, 1, 1, 1, 2, 2, NaN]) ## Shape is preserved, and edges keep the orientation they were given %!assert_equal (discretize ([1, 2; 3, 4], [1, 3, 5]), [1, 1; 2, 2]) %!assert_equal (size (discretize (reshape (1:24, 2, 3, 4), [1, 10, 20, ... %! 30])), ... %! [2, 3, 4]) %!assert_equal (discretize ([1; 2; 3], [1, 2, 3]), [1; 2; 2]) %!test %! [~, e] = discretize ([1, 2, 3], [1, 2, 3]'); %! assert_equal (size (e), [3, 1]); %!test %! [~, e] = discretize ([1, 5]', 2); %! assert_equal (size (e), [1, 3]); ## Bin counts -- values verified against MATLAB R2024a %!test %! [~, e] = discretize ([1, 2, 3, 4, 5], 3); %! assert_equal (e, [1, 2.4000000000000004, 3.8000000000000003, ... %! 5.2000000000000002]); %!test %! [~, e] = discretize ([0, 10], 3); %! assert_equal (e, [0, 4, 8, 12]); %!test %! [~, e] = discretize ([0, 100], 1); %! assert_equal (e, [0, 100]); %!test %! [~, e] = discretize ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 4); %! assert_equal (e, [0, 2.5, 5, 7.5, 10]); %!test %! [~, e] = discretize ([-5, 5], 4); %! assert_equal (e, [-6, -3.1999999999999997, -0.39999999999999947, ... %! 2.4000000000000004, 5.2000000000000011]); %!test %! [~, e] = discretize ([1.234, 9.876], 5); %! assert_equal (e, [1, 2.7999999999999998, 4.5999999999999996, ... %! 6.4000000000000004, 8.1999999999999993, 10]); %!test %! [~, e] = discretize ([-7.5, 2.5], 6); %! assert_equal (e, [-8, -6.2000000000000002, -4.4000000000000004, ... %! -2.5999999999999996, -0.79999999999999982, 1, ... %! 2.8000000000000007]); %!test %! [~, e] = discretize ([1e6, 1e6+1], 4); %! assert_equal (e, [1000000, 1000000.25, 1000000.5, 1000000.75, 1000001]); %!test %! [~, e] = discretize ([0, 2/3], 3); %! assert_equal (e, [0, 0.30000000000000004, 0.60000000000000009, ... %! 0.90000000000000013]); ## NaN and Inf are ignored when the range is determined %!test %! [~, e] = discretize ([NaN, 1, 5, NaN], 2); %! assert_equal (e, [0, 3, 6]); %!test %! [~, e] = discretize ([-Inf, 1, 5], 2); %! assert_equal (e, [0, 3, 6]); ## Constant data spans a unit interval centred on the value %!test %! [~, e] = discretize ([3, 3, 3], 3); %! assert_equal (e, [2.5, 2.8333333333333335, 3.1666666666666665, 3.5]); %!test %! [~, e] = discretize (7, 3); %! assert_equal (e, [6.5, 6.833333333333333, 7.166666666666667, 7.5]); ## Empty input %!assert_equal (size (discretize ([], [1, 2, 3])), [0, 0]) %!assert_equal (class (discretize ([], [1, 2, 3])), 'double') %!test %! [~, e] = discretize ([], 3); %! assert_equal (e, [0, 1, 2, 3]); ## Edge type follows the input for floating-point data %!test %! [~, e] = discretize (single ([1, 5]), 2); %! assert_equal (class (e), 'single'); ## Values %!assert_equal (discretize ([1, 2, 3, 4, 5], [1, 3, 5], [10, 20]), ... %! [10, 10, 20, 20, 20]) %!assert_equal (class (discretize ([1, 2, 3, 4, 5], [1, 3, 5], [10, 20])), ... %! 'double') %!assert_equal (discretize ([0, 1, 2, 3, 4, 5, 6], [1, 3, 5], int8 ([10, ... %! 20])), ... %! int8 ([0, 10, 10, 20, 20, 20, 0])) %!assert_equal (class (discretize ([0, 1, 2, 3, 4, 5, 6], [1, 3, 5], ... %! int8 ([10, 20]))), 'int8') %!assert_equal (discretize ([1, 2, 3, 4, 5], [1, 3, 5], {'lo', 'hi'}), ... %! {'lo', 'lo', 'hi', 'hi', 'hi'}) ## Categorical %!test %! C = discretize ([1, 2, 3, 4, 5], [1, 3, 5], 'categorical'); %! assert_equal (categories (C), {'[1, 3)'; '[3, 5]'}); %! assert_equal (isordinal (C), true); %!test %! C = discretize ([1, 2, 3, 4, 5], [1, 3, 5], 'categorical', {'lo', 'hi'}); %! assert_equal (categories (C), {'lo'; 'hi'}); ## Error branches %!error discretize ([1, 2, 3]); %!error ... %! discretize ('abc', [1, 2, 3]); %!error ... %! discretize ({1, 2}, [1, 2, 3]); %!error ... %! discretize (complex ([1, 2], [1, 1]), [1, 2, 3]); %!error discretize ([1, 2, 3], 0); %!error discretize ([1, 2, 3], -1); %!error discretize ([1, 2, 3], 2.5); %!error ... %! discretize (int8 ([1, 5]), 2); %!error ... %! discretize ([1, 2, 3], [3, 1]); %!error ... %! discretize ([1, 2, 3], [1, NaN, 3]); %!error ... %! discretize ([1, 2, 3], [1, 3, 5], [10, 20, 30]); %!error ... %! discretize ([1, 2, 3], [1, 3, 5], 'IncludedEdge', 'middle'); %!error ... %! discretize ([1, 2, 3], [1, 2, 3], 'categorical', {'a'}); %!error ... %! discretize ([0, 1, 2, 3, 4, 5, 6], [1, 3, 5], {'lo', 'hi'}); pr0m1th3as-datatypes-9450b29/inst/doc-cache000066400000000000000000036204221527370255000204150ustar00rootroot00000000000000# doc-cache created by Octave 11.3.0 # name: cache # type: cell # rows: 3 # columns: 876 # name: # type: sq_string # elements: 1 # length: 10 @cell/disp # name: # type: sq_string # elements: 1 # length: 274 cell: disp (C) cell: str = disp (C) Customized display of cell arrays. This function overloads Octave core disp with respect to cell arrays and displays their contents in MATLAB like manner. str = disp (C) returns the text instead of printing it, as Octave core disp does. # name: # type: sq_string # elements: 1 # length: 34 Customized display of cell arrays. # name: # type: sq_string # elements: 1 # length: 13 @cell/display # name: # type: sq_string # elements: 1 # length: 176 cell: display (C) Customized display of cell arrays. This function overloads Octave core display with respect to cell arrays and displays their contents in MATLAB like manner. # name: # type: sq_string # elements: 1 # length: 34 Customized display of cell arrays. # name: # type: sq_string # elements: 1 # length: 3 NaT # name: # type: sq_string # elements: 1 # length: 1100 datatypes: T = NaT (n) datatypes: T = NaT (sz1, ..., szN) datatypes: T = NaT (sz) datatypes: T = NaT (..., 'Format', fmt) datatypes: T = NaT (..., 'TimeZone', tz) “Not-a-Time�. Creates missing-valued datetime arrays. T = NaT (n) creates an NxN datetime matrix with all values being Not-a-Time (NaT). When called with no size input values, it returns a NaT datetime scalar. T = NaT (sz1, ..., szN) returns a datetime array with NaT values sized according to the input arguments sz1, ..., szN. Alternatively, individual input size arguments can be merged into a single size vector sz, as in the following syntax T = NaT (sz). T = NaT (..., 'Format', fmt) returns a datetime array of NaT values with the specified display format. T = NaT (..., 'TimeZone', tz) returns a datetime array of NaT values in the time zone specified by tz. NaT is the datetime equivalent of NaN. It represents a missing or invalid value. NaT values never compare equal to, greater than, or less than any value, including other NaTs. Doing arithmetic with a NaT and any other value results in a NaT. See also: datetime # name: # type: sq_string # elements: 1 # length: 17 “Not-a-Time�. # name: # type: sq_string # elements: 1 # length: 11 array2table # name: # type: sq_string # elements: 1 # length: 1317 datatypes: tbl = array2table (A) datatypes: tbl = array2table (A, Name, Value) Convert an array to a table. tbl = array2table (A) converts the 2-D matrix A to the table tbl, where each column of A becomes a variable in tbl. A can be any type of array supported by table, including a cell array, as long they are constraint to 2 dimensions. However, in the case of a cell array array2table does not extract the contents of its cells, resulting to a table with each variable being a column of cells. Use cell2table if you want to create a table from the contents of the cells in A. tbl = array2table (A, Name, Value) specifies optional parameters for creating the table tbl with the following Name-Value paired arguments. Name Value 'VariableNames' A cell array of character vectors or a string array defining the variable names of tbl. The names must be valid variable names and unique. 'RowNames' A cell array of character vectors or a string array defining the row names of tbl. The names must be unique but not necessarily valid variable names. 'DimensionNames' A cell array of character vectors or a string array defining the dimension names of tbl. The names must be unique and not in conflict with variable names. By default, dimension names are 'Row', 'Variables'. See also: cell2table, struct2table, table # name: # type: sq_string # elements: 1 # length: 28 Convert an array to a table. # name: # type: sq_string # elements: 1 # length: 15 array2timetable # name: # type: sq_string # elements: 1 # length: 1201 datatypes: tt = array2timetable (A, 'RowTimes', rowTimes) datatypes: tt = array2timetable (A, 'TimeStep', dt) datatypes: tt = array2timetable (A, 'SampleRate', fs) datatypes: tt = array2timetable (..., Name, Value) Convert an array to a timetable. tt = array2timetable (A, 'RowTimes', rowTimes) converts the 2-D array A to a timetable, each column becoming a variable and rowTimes labelling the rows. The row times must be given, by one of 'RowTimes', 'TimeStep' or 'SampleRate', since a timetable cannot be built without them. Variable names are taken from the name of A with the column number appended, so an array named data gives 'data1', 'data2' and so on; an unnamed array gives 'Var1' onwards. The following Name-Value pairs are also accepted. Name Value 'VariableNames' A cell array of character vectors or a string array defining the variable names, one per column of A. 'DimensionNames' A two-element cell array of character vectors or string array naming the rows and the variables. By default the rows are named 'Time'. 'StartTime' The time of the first row, for the 'TimeStep' and 'SampleRate' forms. See also: table.table2timetable, timetable.timetable2table, timetable, array2table # name: # type: sq_string # elements: 1 # length: 32 Convert an array to a timetable. # name: # type: sq_string # elements: 1 # length: 7 caldays # name: # type: sq_string # elements: 1 # length: 433 datatypes: calD = caldays (X) Calendar duration in days. calD = caldays (X) returns a calendarDuration array representing calendar days equivalent to the values in X, which must be a numeric array of integer values. caldays is also available as a method for calendarDuration arrays, in which case it performs the opposite conversion. See also: calendarDuration, calyears, calquarters, calmonths, calweeks, calendarDuration.caldays # name: # type: sq_string # elements: 1 # length: 26 Calendar duration in days. # name: # type: sq_string # elements: 1 # length: 16 calendarDuration # name: # type: sq_string # elements: 1 # length: 833 datatypes: calendarDuration Array representing durations of time using flexible-length calendar date/time units. Each calendarDuration element stores internally the number of whole months, the number of whole days, and a duration object representing hours, minutes, and seconds. It can be used to simplify calculations on datetime arrays involving calendar units. calendarDuration arrays can be created through their constructor by combining numeric arrays representing individual calendar duration units or through the functions caldays, calweeks, calmonths, calquarters, and calyears, which create calendar durations in terms of a single calendar duration unit. These functions are also available as methods of calendarDuration arrays to extract individual calendar duration units as numeric arrays. See also: datetime, duration # name: # type: sq_string # elements: 1 # length: 84 Array representing durations of time using flexible-length calendar date/time units. # name: # type: sq_string # elements: 1 # length: 23 calendarDuration.Format # name: # type: sq_string # elements: 1 # length: 209 calendarDuration: property Format Display format Display format, specified as a character vector or string scalar. If specified as a string scalar, it is converted and stored internally as a character vector. # name: # type: sq_string # elements: 1 # length: 14 Display format # name: # type: sq_string # elements: 1 # length: 24 calendarDuration.caldays # name: # type: sq_string # elements: 1 # length: 1054 calendarDuration: X = caldays (calD) Calendar duration in days. X = caldays (calD) returns a numeric array with the number of days as represented in calD. A month has no fixed number of days, so an array carrying any month at all cannot be stated in days and is an error rather than a guess. Use split on such an array, which reports each unit separately. The sign plays no part: a negative number of months is refused exactly as a positive one is. A NaN or infinite month count is not refused. It is not known to be non-zero, so it propagates as NaN or Inf, just as calmonths, calquarters and calyears already report it. MATLAB instead refuses such a span when it was built by the constructor, while accepting the very same span built by caldays; two spellings of one value cannot sensibly disagree, so both are accepted here. caldays is also available as a function, in which case it performs the opposite conversion. See also: calendarDuration.calyears, calendarDuration.calquarters, calendarDuration.calmonths, calendarDuration.calweeks, caldays # name: # type: sq_string # elements: 1 # length: 26 Calendar duration in days. # name: # type: sq_string # elements: 1 # length: 33 calendarDuration.calendarDuration # name: # type: sq_string # elements: 1 # length: 3706 calendarDuration: calD = calendarDuration (X) calendarDuration: calD = calendarDuration (Y, MO, D) calendarDuration: calD = calendarDuration (Y, MO, D, H, MI, S) calendarDuration: calD = calendarDuration (Y, MO, D, T) calendarDuration: calD = calendarDuration (calD2) calendarDuration: calD = calendarDuration (..., 'Format', FMT) Create a new array of calendar durations. calD = calendarDuration (X) returns an array of calendar durations from numeric matrix X, which must have either three or six columns, representing years, months, days, hours, minutes, and seconds, accordingly. All but seconds must be represented as whole duration units by integer values. calD = calendarDuration (Y, MO, D) returns an array of calendar durations from numeric arrays Y, MO, and D, which correspond to years, months, and days, respectively. The size of calD is the common size of the numeric input arguments, which must be of the same size or scalars. A scalar input functions as a constant array of the same size as the other inputs. calD = calendarDuration (Y, MO, D, H, MI, S) returns an array of calendar durations from numeric arrays Y, MO, D, H, MI, and S, which correspond to years, months, days, hours, minutes, and seconds, respectively. The size of calD is the common size of the numeric input arguments, which must be of the same size or scalars. A scalar input functions as a constant array of the same size as the other inputs. calD = calendarDuration (Y, MO, D, T) returns an array of calendar durations from numeric arrays Y, MO, and D, which correspond to years, months, and days, as well as a time duration array T. The size of calD is the common size of the data input arguments, which must be of the same size or scalars. A scalar input functions as a constant array of the same size as the other inputs. Numeric input arrays Y, MO, D, H, and MI must contain integer values corresponding to whole calendar units. S can also be contain fractions of seconds. calD = calendarDuration (calD2) returns a copy of the calendarDuration array calD2, which keeps its size as well as its 'Format' property unless a new format is specified. calD = calendarDuration (..., 'Format', FMT) specifies the format in which calD is displayed. FMT must be a character vector or a string scalar containing the following letters. 'y' years 'q' quarters of a year 'm' months 'w' weeks 'd' days 't' time duration Each character must be specified only once in the same order as they appear in the above list. 'm', 'd', and 't' characters must always be included in the format specification. No other character is accepted. Note: MATLAB does not reject a format that omits a required character. It warns and silently substitutes a repaired format, so that 'ymd' becomes 'ymdt'. An invalid format is an error here, because rewriting what the user asked for hides the typo that caused it. Note: MATLAB also accepts a numeric array in place of T, which it reads as a count of milliseconds, thereby exposing the internal storage of its duration type. This is undocumented and contradicts MATLAB's own documentation, which requires T to be a duration array, and it is not reproduced here. Use milliseconds (N) to say so explicitly. calD = calendarDuration () returns a scalar array of calendar durations with a value of zero days. To create an empty calendarDuration array, use calendarDuration ([], [], []). A 'Format' may be given on its own, as in calendarDuration ('Format', 'mdt'), which returns the same zero scalar in that format. MATLAB requires data alongside the option and rejects this. See also: calyears, calquarters, calmonths, calweeks, caldays, calendarDuration, iscalendarduration, datetime, duration # name: # type: sq_string # elements: 1 # length: 41 Create a new array of calendar durations. # name: # type: sq_string # elements: 1 # length: 26 calendarDuration.calmonths # name: # type: sq_string # elements: 1 # length: 389 calendarDuration: X = calmonths (calD) Calendar duration in months. X = calmonths (calD) returns a numeric array with the number of months as represented in calD. calmonths is also available as a function, in which case it performs the opposite conversion. See also: calendarDuration.calyears, calendarDuration.calquarters, calendarDuration.calweeks, calendarDuration.caldays, calmonths # name: # type: sq_string # elements: 1 # length: 28 Calendar duration in months. # name: # type: sq_string # elements: 1 # length: 28 calendarDuration.calquarters # name: # type: sq_string # elements: 1 # length: 466 calendarDuration: X = calquarters (calD) Calendar duration in quarters. X = calquarters (calD) returns a numeric array with the number of quarters as represented in calD, rounded towards negative infinity, so that -1 month is -1 quarter. calquarters is also available as a function, in which case it performs the opposite conversion. See also: calendarDuration.calyears, calendarDuration.calmonths, calendarDuration.calweeks, calendarDuration.caldays, calquarters # name: # type: sq_string # elements: 1 # length: 30 Calendar duration in quarters. # name: # type: sq_string # elements: 1 # length: 25 calendarDuration.calweeks # name: # type: sq_string # elements: 1 # length: 744 calendarDuration: X = calweeks (calD) Calendar duration in weeks. X = calweeks (calD) returns a numeric array with the number of weeks as represented in calD, rounded towards negative infinity, so that -15 days is -3 weeks and not -2. As for caldays, an array carrying any month at all cannot be stated in weeks and is an error rather than a guess, whatever the sign of that month count. Use split on such an array. A NaN or infinite month count propagates rather than being refused, on the same reasoning as caldays. calweeks is also available as a function, in which case it performs the opposite conversion. See also: calendarDuration.calyears, calendarDuration.calquarters, calendarDuration.calmonths, calendarDuration.caldays, calweeks # name: # type: sq_string # elements: 1 # length: 27 Calendar duration in weeks. # name: # type: sq_string # elements: 1 # length: 25 calendarDuration.calyears # name: # type: sq_string # elements: 1 # length: 448 calendarDuration: X = calyears (calD) Calendar duration in years. X = calyears (calD) returns a numeric array with the number of years as represented in calD, rounded towards negative infinity, so that -1 month is -1 year. calyears is also available as a function, in which case it performs the opposite conversion. See also: calendarDuration.calquarters, calendarDuration.calmonths, calendarDuration.calweeks, calendarDuration.caldays, calyears # name: # type: sq_string # elements: 1 # length: 27 Calendar duration in years. # name: # type: sq_string # elements: 1 # length: 20 calendarDuration.cat # name: # type: sq_string # elements: 1 # length: 504 calendarDuration: C = cat (dim, A, B, ...) Concatenate calendarDuration arrays. C = cat (dim, A, B, ...) concatenates calendarDuration arrays A, B, ... along dimension dim. All input arrays must have the same size except along the operating dimension dim. Any of the input arrays may also be string arrays or cell arrays of character vectors of compatible size. Additionally, an input can be a numeric matrix, which when parsed to the constructor will return a calendarDuration array of compatible size. # name: # type: sq_string # elements: 1 # length: 36 Concatenate calendarDuration arrays. # name: # type: sq_string # elements: 1 # length: 24 calendarDuration.cellstr # name: # type: sq_string # elements: 1 # length: 673 calendarDuration: cstr = cellstr (calD) calendarDuration: cstr = cellstr (calD, FMT) Convert calendarDuration array to a cell array of character vectors. cstr = cellstr (calD) returns a cellstr array of character vectors, cstr, which has the same size as the input calD. cstr contains the string representations of the calendar durations in calD. cstr = cellstr (calD, FMT) further specifies the format of the returned string representations. FMT must be a character vector conforming to the same specifications required by the constructor's 'Format' property paired argument. Note that cellstr only accepts FMT as a single argument and not as a property paired argument. # name: # type: sq_string # elements: 1 # length: 68 Convert calendarDuration array to a cell array of character vectors. # name: # type: sq_string # elements: 1 # length: 21 calendarDuration.char # name: # type: sq_string # elements: 1 # length: 549 calendarDuration: cmat = char (calD) calendarDuration: cmat = char (calD, FMT) Convert calendarDuration array to a character matrix. cmat = char (calD) returns a character matrix with one row per element in calD, taken in column-major order. The second optional argument, FMT, can be used to specify the format of the returned string representations of the calendarDuration input array calD. Rows shorter than the widest are padded on the left, so the character matrix is right-justified, as MATLAB returns it and as the array itself is displayed. # name: # type: sq_string # elements: 1 # length: 53 Convert calendarDuration array to a character matrix. # name: # type: sq_string # elements: 1 # length: 26 calendarDuration.circshift # name: # type: sq_string # elements: 1 # length: 764 calendarDuration: B = circshift (A, n) calendarDuration: B = circshift (A, n, dim) Circularly shift the elements in a calendarDuration array. B = circshift (A, n) circularly shifts the elements of the calendarDuration array A according to n. If n is a nonzero integer scalar, then the elements of A are shifted by n elements along the first non-singleton dimension of A. If n is a vector, it must not be longer that the number of dimensions of A with each value of n corresponding to a dimension in A. The sign of the value(s) in n specify the direction in the elements of A are shifted. B = circshift (A, n, dim) circularly shifts the elements of the calendarDuration array A along the dimension specified by dim. In this case, n must be a scalar integer value. # name: # type: sq_string # elements: 1 # length: 58 Circularly shift the elements in a calendarDuration array. # name: # type: sq_string # elements: 1 # length: 27 calendarDuration.ctranspose # name: # type: sq_string # elements: 1 # length: 259 calendarDuration: B = ctranspose (A) Transpose a calendarDuration matrix. B = ctranspose (A) is the equivalent of the syntax B = A' and returns the transpose of the calendarDuration matrix A. For calendarDuration arrays, ctranspose is identical to transpose. # name: # type: sq_string # elements: 1 # length: 36 Transpose a calendarDuration matrix. # name: # type: sq_string # elements: 1 # length: 24 calendarDuration.datevec # name: # type: sq_string # elements: 1 # length: 1125 calendarDuration: DV = datevec (calD) calendarDuration: [Y, MO] = datevec (calD) calendarDuration: [Y, MO, D] = datevec (calD) calendarDuration: [Y, MO, D, H] = datevec (calD) calendarDuration: [Y, MO, D, H, MI] = datevec (calD) calendarDuration: [Y, MO, D, H, MI, S] = datevec (calD) Convert calendarDuration array to date vectors. DV = datevec (calD) returns an Nx6 numeric matrix, where N is the number of elements in calD and the columns corresponds to years, months, days, hours, minutes, and seconds, respectively. When datevec is called with more than one output arguments, then it returns the components of the date vectors as individual variables Y, MO, D, H, MI,and S corresponding to years, months, days, hours, minutes, and seconds, respectively. In this case, the individual variables have the same size as the input array calD. Every component carries the sign of the span as a whole, so a negative calendar duration returns negative components throughout. An element that is not finite has no components to divide between: all six take that same infinity, or NaN, which is what split returns for it as well. # name: # type: sq_string # elements: 1 # length: 47 Convert calendarDuration array to date vectors. # name: # type: sq_string # elements: 1 # length: 28 calendarDuration.dispstrings # name: # type: sq_string # elements: 1 # length: 724 calendarDuration: cstr = dispstrings (calD) Get display formatted strings for each element of a calendarDuration array. cstr = dispstrings (calD) returns a cellstr array of character vectors, cstr, which has the same size as the input calendarDuration array, calD. The returned text representations in cstr are formatted according to the 'Format' property of the input array calD. Whole calendar units are rendered with up to six significant digits and the seconds component with five, each switching to exponent notation beyond that, so a million days reads '1e+06d'. Rounding applies to a component on its own and never carries into the one above it, which is why 59.9999 seconds read '60s' rather than a further minute. # name: # type: sq_string # elements: 1 # length: 75 Get display formatted strings for each element of a calendarDuration array. # name: # type: sq_string # elements: 1 # length: 22 calendarDuration.empty # name: # type: sq_string # elements: 1 # length: 690 calendarDuration: E = calendarDuration.empty () calendarDuration: E = calendarDuration.empty (sz) calendarDuration: E = calendarDuration.empty (m, n, ...) Create an empty calendarDuration array. E = calendarDuration.empty () returns a 0x0 empty calendarDuration array. calendarDuration.empty (m, n, ...) or calendarDuration.empty (sz) returns an empty calendarDuration array of the requested size, which must have at least one dimension equal to zero. A lone dimension gives a square size, so calendarDuration.empty (3) is an error while calendarDuration.empty (0) is 0x0. As for zeros, a negative dimension counts as zero, and a size vector with nothing in it names no size and gives 0x0. # name: # type: sq_string # elements: 1 # length: 39 Create an empty calendarDuration array. # name: # type: sq_string # elements: 1 # length: 19 calendarDuration.eq # name: # type: sq_string # elements: 1 # length: 1093 calendarDuration: TF = eq (A, B) Test for equality between calendarDuration arrays. TF = eq (A, B) is the equivalent of the syntax TF = A == B and returns a logical array with elements set to true where calendarDuration arrays A and B are equal, otherwise set to false. Missing values are not equal to each other. Hence, any NaN values in A or B result to false elements in TF. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. The size of the output TF is the same as the size of input arrays after their expansion according to the broadcasting rules. MATLAB defines no equality for calendarDuration arrays, so this is an Octave extension. Two spans are compared component by component, which is what MATLAB's own isequal does, so the two agree element for element: calweeks (1) == caldays (7) is true, both being seven days, while caldays (1) == calendarDuration (0, 0, 0, 24, 0, 0) is false, a day and twenty-four hours sitting in different components. # name: # type: sq_string # elements: 1 # length: 50 Test for equality between calendarDuration arrays. # name: # type: sq_string # elements: 1 # length: 24 calendarDuration.horzcat # name: # type: sq_string # elements: 1 # length: 559 calendarDuration: C = horzcat (A, B, ...) Horizontal concatenation of calendarDuration arrays. C = horzcat (A, B, ... is the equivalent of the syntax B = [A, B, ...] and horizontally concatenates the calendarDuration arrays A, B, .... All input arrays must have the same size except along the second dimension. Any of the input arrays may also be string arrays or cell arrays of character vectors of compatible size. Additionally, an input can be a numeric matrix, which when parsed to the constructor will return a calendarDuration array of compatible size. # name: # type: sq_string # elements: 1 # length: 52 Horizontal concatenation of calendarDuration arrays. # name: # type: sq_string # elements: 1 # length: 25 calendarDuration.ipermute # name: # type: sq_string # elements: 1 # length: 479 calendarDuration: A = ipermute (B, dims) Inverse of the generalized transpose for a calendarDuration N-D array. A = ipermute (B, dims) returns the inverse of the generalized transpose performed by the permute function. The expression ipermute (permute (A, dims), dims) returns the original array A. dims must index all the dimensions 1:ndims (B) of the input array B, in any order, but only once. The dimension of B specified in dims(N) gets remapped to the Nth dimension of A. # name: # type: sq_string # elements: 1 # length: 70 Inverse of the generalized transpose for a calendarDuration N-D array. # name: # type: sq_string # elements: 1 # length: 25 calendarDuration.iscolumn # name: # type: sq_string # elements: 1 # length: 323 calendarDuration: TF = iscolumn (calD) Return true if calendarDuration array is a column vector. TF = iscolumn (calD) returns a logical scalar TF, which is true if the calendar duration array calD is a column vector and false otherwise. A column vector is a 2-D array for which size (X) returns [N, 1] with non-negative N. # name: # type: sq_string # elements: 1 # length: 57 Return true if calendarDuration array is a column vector. # name: # type: sq_string # elements: 1 # length: 24 calendarDuration.isempty # name: # type: sq_string # elements: 1 # length: 215 calendarDuration: TF = isempty (calD) Return true if calendarDuration array is empty. TF = isempty (calD) returns a logical scalar TF, which is true if the calendar duration array calD is empty and false otherwise. # name: # type: sq_string # elements: 1 # length: 47 Return true if calendarDuration array is empty. # name: # type: sq_string # elements: 1 # length: 24 calendarDuration.isequal # name: # type: sq_string # elements: 1 # length: 510 calendarDuration: TF = isequal (calD1, calD2) calendarDuration: TF = isequal (calD1, calD2, ...) Return true if calendarDuration arrays are equal. TF = isequal (calD1, calD2) returns a logical scalar TF, which is true if the calendar duration arrays calD1 and calD2 contain the same values and false otherwise. TF = isequal (calD1, calD2, ...) returns a logical scalar TF, which is true if all input arguments are calendar duration arrays with equal values in each corresponding elements and false otherwise. # name: # type: sq_string # elements: 1 # length: 49 Return true if calendarDuration arrays are equal. # name: # type: sq_string # elements: 1 # length: 25 calendarDuration.isequaln # name: # type: sq_string # elements: 1 # length: 604 calendarDuration: TF = isequaln (calD1, calD2) calendarDuration: TF = isequaln (calD1, calD2, ...) Return true if calendarDuration arrays are equal under the assumption that missing elements are equal. TF = isequaln (calD1, calD2) returns a logical scalar TF, which is true if the calendar duration arrays calD1 and calD2 contain the same values or corresponding missing elements and false otherwise. TF = isequaln (calD1, calD2, ...) returns a logical scalar TF, which is true if all input arguments are calendar duration arrays with equal values or corresponding missing elements and false otherwise. # name: # type: sq_string # elements: 1 # length: 102 Return true if calendarDuration arrays are equal under the assumption that missing elements are equal. # name: # type: sq_string # elements: 1 # length: 25 calendarDuration.isfinite # name: # type: sq_string # elements: 1 # length: 326 calendarDuration: TF = isfinite (calD) Return true for calendar durations that are finite. TF = isfinite (calD) returns a logical array TF of the same size as calD containing true for each corresponding element of calD that is finite and false otherwise. Finite elements are those which are neither infinite nor Not-A-Number. # name: # type: sq_string # elements: 1 # length: 51 Return true for calendar durations that are finite. # name: # type: sq_string # elements: 1 # length: 22 calendarDuration.isinf # name: # type: sq_string # elements: 1 # length: 263 calendarDuration: TF = isinf (calD) Return true for calendar durations that are infinite. TF = isinf (calD) returns a logical array TF of the same size as calD containing true for each corresponding element of calD that is either Inf or -Inf and false otherwise. # name: # type: sq_string # elements: 1 # length: 53 Return true for calendar durations that are infinite. # name: # type: sq_string # elements: 1 # length: 25 calendarDuration.ismatrix # name: # type: sq_string # elements: 1 # length: 344 calendarDuration: TF = ismatrix (calD) Return true if calendarDuration array is a 2-D array. TF = ismatrix (calD) returns a logical scalar TF, which is true if the calendarDuration array calD is a matrix and false otherwise. A matrix is an array of any type where ndims (X) == 2 and for which size (X) returns [H, W] with non-negative H and W. # name: # type: sq_string # elements: 1 # length: 53 Return true if calendarDuration array is a 2-D array. # name: # type: sq_string # elements: 1 # length: 26 calendarDuration.ismissing # name: # type: sq_string # elements: 1 # length: 742 calendarDuration: TF = ismissing (calD) calendarDuration: TF = ismissing (calD, indicator) Find missing data in a calendarDuration array. Missing values in calendarDuration arrays are represented by NaN, thus TF = ismissing (calD) is equivalent to TF = isnan (calD). TF = ismissing (calD, indicator) marks instead the elements of calD equal to the values in indicator, a calendarDuration scalar or vector; the standard missing values are ignored, unless explicitly stated in indicator. Elements are compared as by calendarDuration.eq, component by component. A NaN in indicator matches the NaN elements of calD, and so does missing. MATLAB accepts no indicator for a calendarDuration array, so the indicator argument is an Octave extension. # name: # type: sq_string # elements: 1 # length: 46 Find missing data in a calendarDuration array. # name: # type: sq_string # elements: 1 # length: 22 calendarDuration.isnan # name: # type: sq_string # elements: 1 # length: 252 calendarDuration: TF = isnan (calD) Return true for calendar durations that are Not-A-Number. TF = isnan (calD) returns a logical array TF of the same size as calD containing true for each corresponding element of calD that is NaN and false otherwise. # name: # type: sq_string # elements: 1 # length: 57 Return true for calendar durations that are Not-A-Number. # name: # type: sq_string # elements: 1 # length: 23 calendarDuration.isreal # name: # type: sq_string # elements: 1 # length: 199 calendarDuration: TF = isreal (calD) Always return true for calendarDuration arrays. TF = isreal (calD) always returns a logical scalar true value, if the input argument is a calendarDuration array. # name: # type: sq_string # elements: 1 # length: 47 Always return true for calendarDuration arrays. # name: # type: sq_string # elements: 1 # length: 22 calendarDuration.isrow # name: # type: sq_string # elements: 1 # length: 307 calendarDuration: TF = isrow (calD) Return true if calendarDuration array is a row vector. TF = isrow (calD) returns a logical scalar TF, which is true if the calendarDuration array calD is a row vector and false otherwise. A row vector is a 2-D array for which size (X) returns [1, N] with non-negative N. # name: # type: sq_string # elements: 1 # length: 54 Return true if calendarDuration array is a row vector. # name: # type: sq_string # elements: 1 # length: 25 calendarDuration.isscalar # name: # type: sq_string # elements: 1 # length: 298 calendarDuration: TF = isscalar (calD) Return true if calendarDuration array is a scalar. TF = isscalar (calD) returns a logical scalar TF, which is true if the calendarDuration array calD is also a scalar and false otherwise. A scalar is a single element object for which size (X) returns [1, 1]. # name: # type: sq_string # elements: 1 # length: 50 Return true if calendarDuration array is a scalar. # name: # type: sq_string # elements: 1 # length: 25 calendarDuration.isvector # name: # type: sq_string # elements: 1 # length: 355 calendarDuration: TF = isvector (calD) Return true if calendarDuration array is a vector. TF = isvector (calD) returns a logical scalar TF, which is true if the calendarDuration array calD is a vector and false otherwise. A vector is a 2-D array for which one of the dimensions is equal to 1 (either 1xN or Nx1). By definition, a scalar is also a vector. # name: # type: sq_string # elements: 1 # length: 50 Return true if calendarDuration array is a vector. # name: # type: sq_string # elements: 1 # length: 24 calendarDuration.keyHash # name: # type: sq_string # elements: 1 # length: 1224 calendarDuration: key = keyHash (calD) calendarDuration: key = keyHash (calD, base) Generate a hash code for a calendarDuration array. key = keyHash (calD) generates a uint64 scalar that represents the input array calD. keyHash utilizes the 64-bit FNV-1a variant of the Fowler-Noll-Vo non-cryptographic hash function. key = keyHash (calD, base) also generates a 64-bit hash code using base as the offset basis for the FNV-1a hash algorithm. base must be a uint64 integer type scalar. Use this syntax to cascade keyHash on multiple objects for which a single hash code is required. A calendarDuration array is keyed on its months, days and time components, so calendar units that are interchangeable share a key: calmonths (12) hashes as calyears (1), and calweeks (1) as caldays (7). Units that are not interchangeable stay distinct, calmonths (1) and caldays (30) among them. The Format property is display only and is not part of the key. Two arrays that keyMatch reports as the same key always hash alike. Note that unlike MATLAB, this implementation does not use any random seed. As a result, keyHash will always generate the exact same hash key for any particular input across different workers and Octave sessions. # name: # type: sq_string # elements: 1 # length: 50 Generate a hash code for a calendarDuration array. # name: # type: sq_string # elements: 1 # length: 23 calendarDuration.length # name: # type: sq_string # elements: 1 # length: 254 calendarDuration: N = length (calD) Length of a calendarDuration vector. N = length (calD) returns the size of the longest dimension of the calendarDuration array calD, unless any of its dimensions has zero length, in which case length (calD) returns 0. # name: # type: sq_string # elements: 1 # length: 36 Length of a calendarDuration vector. # name: # type: sq_string # elements: 1 # length: 22 calendarDuration.minus # name: # type: sq_string # elements: 1 # length: 700 calendarDuration: C = minus (A, B) Subtraction for calendarDuration arrays. C = minus (A, B) is the equivalent of the syntax C = A - B and returns the result of subtracting the corresponding elements of B from those of A. C is a calendarDuration array of the same size as the input arguments after the necessary (if required) expansion. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. Either A or B may also be a duration or a numeric array with the latter representing duration days and being internally converted to a duration array with the days () function. # name: # type: sq_string # elements: 1 # length: 40 Subtraction for calendarDuration arrays. # name: # type: sq_string # elements: 1 # length: 23 calendarDuration.mtimes # name: # type: sq_string # elements: 1 # length: 603 calendarDuration: C = mtimes (A, B) Matrix multiplication for calendarDuration arrays. C = mtimes (A, B) is the equivalent of the syntax C = A * B and returns the matrix multiplication product of inputs A and B. Either A or B must be a calendarDuration array and its complement must be a double array. C is a calendarDuration array of the same size as the input arguments after the necessary (if required) expansion. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. # name: # type: sq_string # elements: 1 # length: 50 Matrix multiplication for calendarDuration arrays. # name: # type: sq_string # elements: 1 # length: 22 calendarDuration.ndims # name: # type: sq_string # elements: 1 # length: 176 calendarDuration: out = ndims (calD) Number of dimensions in a calendarDuration array. out = ndims (calD) returns the number of dimensions of the calendar duration array calD. # name: # type: sq_string # elements: 1 # length: 49 Number of dimensions in a calendarDuration array. # name: # type: sq_string # elements: 1 # length: 19 calendarDuration.ne # name: # type: sq_string # elements: 1 # length: 1098 calendarDuration: TF = ne (A, B) Test for inequality between calendarDuration arrays. TF = ne (A, B) is the equivalent of the syntax TF = A != B and returns a logical array with elements set to true where calendarDuration arrays A and B are not equal, otherwise set to false. Missing values are not equal to each other. Hence, any NaN values in A or B result to true elements in TF. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. The size of the output TF is the same as the size of input arrays after their expansion according to the broadcasting rules. MATLAB defines no equality for calendarDuration arrays, so this is an Octave extension. Two spans are compared component by component, which is what MATLAB's own isequal does, so the two agree element for element: calweeks (1) == caldays (7) is true, both being seven days, while caldays (1) == calendarDuration (0, 0, 0, 24, 0, 0) is false, a day and twenty-four hours sitting in different components. # name: # type: sq_string # elements: 1 # length: 52 Test for inequality between calendarDuration arrays. # name: # type: sq_string # elements: 1 # length: 20 calendarDuration.nnz # name: # type: sq_string # elements: 1 # length: 182 calendarDuration: out = nnz (calD) Number of nonzero elements in calendarDuration array. out = nnz (calD) returns the number of nonzero elements in the calendar duration array calD. # name: # type: sq_string # elements: 1 # length: 53 Number of nonzero elements in calendarDuration array. # name: # type: sq_string # elements: 1 # length: 22 calendarDuration.numel # name: # type: sq_string # elements: 1 # length: 177 calendarDuration: out = numel (calD) Total number of elements in a calendarDuration array. out = numel (calD) returns the number of elements in the calendarDuration array calD. # name: # type: sq_string # elements: 1 # length: 53 Total number of elements in a calendarDuration array. # name: # type: sq_string # elements: 1 # length: 24 calendarDuration.permute # name: # type: sq_string # elements: 1 # length: 449 calendarDuration: B = permute (A, dims) Generalized transpose for a calendarDuration N-D array. B = permute (A, dims) returns the generalized transpose of the calendarDuration array A by rearranging its dimensions according to the permutation vector specified in dims. dims must index all the dimensions 1:ndims (A) of the input array A, in any order, but only once. The Nth dimension of A gets remapped to the dimension in B specified by dims(N). # name: # type: sq_string # elements: 1 # length: 55 Generalized transpose for a calendarDuration N-D array. # name: # type: sq_string # elements: 1 # length: 21 calendarDuration.plus # name: # type: sq_string # elements: 1 # length: 680 calendarDuration: C = plus (A, B) Addition for calendarDuration arrays. C = plus (A, B) is the equivalent of the syntax C = A + B and returns the result of adding the corresponding elements of A and B. C is a calendarDuration array of the same size as the input arguments after the necessary (if required) expansion. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. Either A or B may also be a duration or a numeric array with the latter representing duration days and being internally converted to a duration array with the days () function. # name: # type: sq_string # elements: 1 # length: 37 Addition for calendarDuration arrays. # name: # type: sq_string # elements: 1 # length: 24 calendarDuration.repelem # name: # type: sq_string # elements: 1 # length: 962 calendarDuration: B = repelem (A, n) calendarDuration: B = repelem (A, d1, ..., dN) Repeat copies of calendarDuration array elements. B = repelem (A, n) returns a calendarDuration vector B containing repeated elements of the input A, which must be a calendarDuration vector. If n is a scalar, each element of A is repeated n times along the non-singleton dimension of A. If n is a vector, it must have the same elements as A, in which case it specifies the number of times to repeat each corresponding element of A. B = repelem (A, d1, ..., dN) returns an array B with each element of A repeated according to the the list of input arguments d1, ..., dN each corresponding to a different dimension 1:ndims (A) of the input array A. d1, ..., dN must be either scalars or vectors with the same length as the corresponding dimension of A containing non-negative integer values specifying the number of repetitions of each element along the corresponding dimension. # name: # type: sq_string # elements: 1 # length: 49 Repeat copies of calendarDuration array elements. # name: # type: sq_string # elements: 1 # length: 25 calendarDuration.repelems # name: # type: sq_string # elements: 1 # length: 479 calendarDuration: B = repelems (A, R) Construct a vector of repeated elements from a calendarDuration array. B = repelems (A, R) returns a calendarDuration vector B containing repeated elements of the input A, which must be a calendarDuration vector. R must be a 2xN matrix of integers. Entries in the first row of R correspond to the linear indexing of the elements in A to be repeated. The corresponding entries in the second row of R specify the repeat count of each element. # name: # type: sq_string # elements: 1 # length: 70 Construct a vector of repeated elements from a calendarDuration array. # name: # type: sq_string # elements: 1 # length: 23 calendarDuration.repmat # name: # type: sq_string # elements: 1 # length: 610 calendarDuration: B = repmat (A, n) calendarDuration: B = repmat (A, d1, ..., dN) calendarDuration: B = repmat (A, dimvec) Repeat copies of a calendarDuration array. B = repmat (A, n) returns a calendarDuration array B containing n copies of the input calendarDuration array A along every dimension of A. B = repmat (A, d1, ..., dN) returns an array B containing copies of A along the dimensions specified by the list of scalar integer values d1, ..., dN, which specify how many copies of A are made in each dimension. B = repmat (A, dimvec) is equivalent to the previous syntax with dimvec = [d1, ..., dN]. # name: # type: sq_string # elements: 1 # length: 42 Repeat copies of a calendarDuration array. # name: # type: sq_string # elements: 1 # length: 24 calendarDuration.reshape # name: # type: sq_string # elements: 1 # length: 744 calendarDuration: B = reshape (A, d1, ..., dN) calendarDuration: B = reshape (A, ..., [], ...) calendarDuration: B = reshape (A, dimvec) Reshape calendarDuration array. B = reshape (A, d1, ..., dN) returns a calendarDuration array B with specified dimensions d1, ..., dN, whose elements are taken columnwise from the calendarDuration array A. The product of d1, ..., dN must equal the total number of elements in A. B = reshape (A, ..., [], ...) returns a calendarDuration array B with one dimension unspecified which is calculated automatically so that the product of dimensions in B matches the total elements in A, which must be divisible the product of specified dimensions. An empty matrix ([]) is used to flag the unspecified dimension. # name: # type: sq_string # elements: 1 # length: 31 Reshape calendarDuration array. # name: # type: sq_string # elements: 1 # length: 21 calendarDuration.size # name: # type: sq_string # elements: 1 # length: 1092 calendarDuration: sz = size (calD) calendarDuration: dim_sz = size (calD, dim) calendarDuration: dim_sz = size (calD, d1, d2, ...) calendarDuration: [rows, columns, ..., dim_n_sz] = size (...) Size of a calendarDuration array. sz = size (calD) returns a row vector with the size (number of elements) of each dimension for the calendar duration array calD. dim_sz = size (calD, dim) returns the size of the corresponding dimension specified in dim. If dim is a vector, then dim_sz is a vector of the same length and with each element corresponding to a specified dimension. Multiple dimensions may also be specified as separate arguments. With a single output argument, size returns a row vector. With several, the size of dimension N is returned in the Nth argument. Asking for fewer output arguments than the array has dimensions folds the trailing dimensions into the last one, so [r, c] = size (calD) on a 2-by-2-by-2 array returns 2 and 4; asking for more pads with ones. When a dimension is named, however, the number of output arguments must equal the number of dimensions requested. # name: # type: sq_string # elements: 1 # length: 33 Size of a calendarDuration array. # name: # type: sq_string # elements: 1 # length: 22 calendarDuration.split # name: # type: sq_string # elements: 1 # length: 1440 calendarDuration: [...] = split (calD, units) Split calendarDuration array into numeric and duration units. [...] = split (calD, units) splits the calendar duration units in calD into separate numeric arrays according to date/time units specified in units, which must be either a cell array of character vectors or a string array containing any of the following date/time units in descending order. 'years' 'quarters' 'months' 'weeks' 'days' 'time' When a single date/time unit is specified, units may also be a character vector. When 'time' is specified in units, the corresponding returned argument is a duration array. The values of years, quarters, and months are computed independently from the values of weeks and days in calD, with larger units taking precedence when specified The same applies for duration arrays, when requested. Each unit may be abbreviated to any leading part of its name and is matched without regard to case, so 'y', 'Year' and 'YEARS' all name years. No abbreviation is ambiguous, the six names starting with six different letters. A unit named more than once still asks for a single component. Fewer output arguments than units may be requested, in which case only the leading ones are returned; asking for more is an error. An element that is not finite keeps that same value in every component it is divided into, so splitting an infinite calendar duration gives infinite years, months and days alike. # name: # type: sq_string # elements: 1 # length: 61 Split calendarDuration array into numeric and duration units. # name: # type: sq_string # elements: 1 # length: 21 calendarDuration.time # name: # type: sq_string # elements: 1 # length: 183 calendarDuration: T = time (calD) Return time portion of calendarDuration array. T = time (calD) returns a duration array T with the time portions of the calendarDuration array calD. # name: # type: sq_string # elements: 1 # length: 46 Return time portion of calendarDuration array. # name: # type: sq_string # elements: 1 # length: 22 calendarDuration.times # name: # type: sq_string # elements: 1 # length: 626 calendarDuration: C = times (A, B) Element-by-element multiplication for calendarDuration arrays. C = times (A, B) is the equivalent of the syntax C = A .* B and returns the element-by-element multiplication product of inputs A and B. Either A or B must be a calendarDuration array and its complement must be a double array. C is a calendarDuration array of the same size as the input arguments after the necessary (if required) expansion. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. # name: # type: sq_string # elements: 1 # length: 62 Element-by-element multiplication for calendarDuration arrays. # name: # type: sq_string # elements: 1 # length: 26 calendarDuration.transpose # name: # type: sq_string # elements: 1 # length: 191 calendarDuration: B = transpose (A) Transpose a calendarDuration matrix. B = transpose (A) is the equivalent of the syntax B = A.' and returns the transpose of the calendarDuration matrix A. # name: # type: sq_string # elements: 1 # length: 36 Transpose a calendarDuration matrix. # name: # type: sq_string # elements: 1 # length: 23 calendarDuration.uminus # name: # type: sq_string # elements: 1 # length: 224 calendarDuration: C = uminus (A) Unary minus for calendarDuration arrays. C = uminus (A) is the equivalent of the syntax C = -A and returns A after negating its elements. C is a calendarDuration array of the same size as A. # name: # type: sq_string # elements: 1 # length: 40 Unary minus for calendarDuration arrays. # name: # type: sq_string # elements: 1 # length: 23 calendarDuration.unique # name: # type: sq_string # elements: 1 # length: 1892 calendarDuration: B = unique (A) calendarDuration: B = unique (A, setOrder) calendarDuration: B = unique (A, occurrence) calendarDuration: B = unique (A, setOrder, occurrence) calendarDuration: B = unique (A, occurrence, setOrder) calendarDuration: B = unique (A, ..., 'rows') calendarDuration: [B, ixA, ixB] = unique (...) Unique values in a calendarDuration array. B = unique (A) returns the unique values of the calendarDuration array A in sorted order. B = unique (A, setOrder) returns the unique values of the calendarDuration array A in an order as specified by setOrder, which can be either of the following values: 'sorted' (default) returns the unique values sorted in ascending order. 'stable' returns the unique values according to their order of occurrence. B = unique (A, occurrence) returns the unique values of the calendarDuration array tblA according to their order of occurrence. occurrence can be either of the following values: 'first' (default) returns the first occurrence of each unique value, i.e. the lowest possible indices are returned. 'last' returns the last occurrence of each unique value, i.e. the highest possible indices are returned. You can specify setOrder and occurrence arguments together. B = unique (A, ..., 'rows') returns the unique rows of A by treating each row as a single entity. The 'rows' option can be used alone or in any combination with the setOrder and occurrence arguments. 'rows' can be placed at any position in the function's argument list after the input array A. However, this syntax is only valid for 2-dimensional calendarDuration arrays. [tblB, ixA, ixB] = unique (...) also returns index vectors ixA and ixB using any of the previous syntaxes. ixA and ixB map the arrays A and B to one another such that B = A(ixA) and A = B(ixB). When the 'rows' optional argument is specified, then B = A(ixA,:) and tblA = tblB(ixB,:). # name: # type: sq_string # elements: 1 # length: 42 Unique values in a calendarDuration array. # name: # type: sq_string # elements: 1 # length: 22 calendarDuration.uplus # name: # type: sq_string # elements: 1 # length: 203 calendarDuration: C = uplus (A) Unary plus for calendarDuration arrays. C = uplus (A) is the equivalent of the syntax C = +A and returns a copy of A. C is a calendarDuration array of the same size as A. # name: # type: sq_string # elements: 1 # length: 39 Unary plus for calendarDuration arrays. # name: # type: sq_string # elements: 1 # length: 24 calendarDuration.vertcat # name: # type: sq_string # elements: 1 # length: 554 calendarDuration: C = vertcat (A, B, ...) Vertical concatenation of calendarDuration arrays. C = vertcat (A, B, ... is the equivalent of the syntax B = [A; B; ...] and vertically concatenates the calendarDuration arrays A, B, .... All input arrays must have the same size except along the first dimension. Any of the input arrays may also be string arrays or cell arrays of character vectors of compatible size. Additionally, an input can be a numeric matrix, which when parsed to the constructor will return a calendarDuration array of compatible size. # name: # type: sq_string # elements: 1 # length: 50 Vertical concatenation of calendarDuration arrays. # name: # type: sq_string # elements: 1 # length: 9 calmonths # name: # type: sq_string # elements: 1 # length: 443 datatypes: calD = calmonths (X) Calendar duration in months. calD = calmonths (X) returns a calendarDuration array representing calendar months equivalent to the values in X, which must be a numeric array of integer values. calmonths is also available as a method for calendarDuration arrays, in which case it performs the opposite conversion. See also: calendarDuration, calyears, calquarters, calweeks, caldays, calendarDuration.calmonths # name: # type: sq_string # elements: 1 # length: 28 Calendar duration in months. # name: # type: sq_string # elements: 1 # length: 11 calquarters # name: # type: sq_string # elements: 1 # length: 453 datatypes: calD = calquarters (X) Calendar duration in quarters. calD = calquarters (X) returns a calendarDuration array representing calendar quarters equivalent to the values in X, which must be a numeric array of integer values. calquarters is also available as a method for calendarDuration arrays, in which case it performs the opposite conversion. See also: calendarDuration, calyears, calmonths, calweeks, caldays, calendarDuration.calquarters # name: # type: sq_string # elements: 1 # length: 30 Calendar duration in quarters. # name: # type: sq_string # elements: 1 # length: 8 calweeks # name: # type: sq_string # elements: 1 # length: 438 datatypes: calD = calweeks (X) Calendar duration in weeks. calD = calweeks (X) returns a calendarDuration array representing calendar weeks equivalent to the values in X, which must be a numeric array of integer values. calweeks is also available as a method for calendarDuration arrays, in which case it performs the opposite conversion. See also: calendarDuration, calyears, calquarters, calmonths, caldays, calendarDuration.calweeks # name: # type: sq_string # elements: 1 # length: 27 Calendar duration in weeks. # name: # type: sq_string # elements: 1 # length: 8 calyears # name: # type: sq_string # elements: 1 # length: 438 datatypes: calD = calyears (X) Calendar duration in years. calD = calyears (X) returns a calendarDuration array representing calendar years equivalent to the values in X, which must be a numeric array of integer values. calyears is also available as a method for calendarDuration arrays, in which case it performs the opposite conversion. See also: calendarDuration, calquarters, calmonths, calweeks, caldays, calendarDuration.calyears # name: # type: sq_string # elements: 1 # length: 27 Calendar duration in years. # name: # type: sq_string # elements: 1 # length: 11 categorical # name: # type: sq_string # elements: 1 # length: 951 datatypes: categorical Array representing categorical data. A categorical array represents an array of values that correspond to a finite set of discrete categories, which can be either ordinal (having a mathematical ordering) or nominal. It is an efficient way to define groups of rows in a table or to other types of variables. Each categorical array stores the list of categories as a cell array of character vectors and a numeric array of uint16 type as indices to the categories. The categorical array may also store elements of undefined categorical values, which represent the absence of a given category and correspond to the NaN value for numeric arrays or in general to the missing value for other data types. categorical arrays do not have any public properties, which can be indexed by using dot notation similarly to structures. However, there are several methods which can be used to modify their categories once they are constructed. # name: # type: sq_string # elements: 1 # length: 36 Array representing categorical data. # name: # type: sq_string # elements: 1 # length: 19 categorical.addcats # name: # type: sq_string # elements: 1 # length: 1053 categorical: B = addcats (A, newcats) categorical: B = addcats (..., 'After', catname) categorical: B = addcats (..., 'Before', catname) Add categories to categorical array. B = addcats (A, newcats) appends new categories specified in newcats to the categorical array A at the end of any existing categories. The output categorical array B does not contain elements that belong to the newly added categories. B = addcats (..., 'After', catname) adds the categories after the existing category specified by catname. B = addcats (..., 'Before', catname) adds the categories before the existing category specified by catname. catname must be either a character vector, a cellstr scalar or a string scalar. newcats can be a string array, a cell array of character vectors, or any type of array that can be converted to a cell array of character vectors with the cellstr function, as long as it does not contain any duplicate names or references existing category in A. When adding a single category, newcats can also be specified as a character vector. # name: # type: sq_string # elements: 1 # length: 36 Add categories to categorical array. # name: # type: sq_string # elements: 1 # length: 15 categorical.cat # name: # type: sq_string # elements: 1 # length: 1140 categorical: C = cat (dim, A, B, ...) Concatenate categorical arrays. C = cat (dim, A, B, ...) concatenates categorical arrays A, B, ... along dimension dim. All input arrays must have the same size except along the operating dimension dim. Any of the input arrays may also be character vectors, string arrays or cell arrays of character vectors of compatible size, whose texts name categories, or missing values, which are undefined. Empty arrays are ignored. A numeric or logical array is refused, since a number could name a category or its code. If any input array is an ordinal categorical array, then all inputs must be ordinal categorical arrays with the same set and ordering of categories. In this case, C is also an ordinal categorical array with the same set and ordering of categories. If none of the input arrays are ordinal, then they do not need to have the same set of categories. In this case, categorical array C contains the union of the categories from all input arrays. Protected categorical arrays can only be concatenated with other arrays that have the same set of categories but not necessarily in the same order. # name: # type: sq_string # elements: 1 # length: 31 Concatenate categorical arrays. # name: # type: sq_string # elements: 1 # length: 23 categorical.categorical # name: # type: sq_string # elements: 1 # length: 3259 categorical: C = categorical (A) categorical: C = categorical (A, valueset) categorical: C = categorical (A, valueset, catnames) categorical: C = categorical (..., Name, Value) Create a new array of categorical values. C = categorical (A) creates a categorical array C from the input array A, which can be numeric, logical, datetime, duration, string, or cell array of character vectors. Input A can also be another categorical array. The categories in C the sorted unique values from the input array A. When the input array is string or cell array of character vectors, any leading or trailing white spaces are removed. Missing values in the input array correspond to elements in the created categorical array. By default, there is no category for undefined values in the output array. C = categorical (A, valueset) creates a categorical array from input A with the categories specified in valueset, which must be a vector of unique values. The data type of input array A and valueset must be the same, unless they are string or cell arrays of character vectors, in which case they can be used interchangeably. Similarly to input array A any leading or trailing white spaces are removed, if valueset is a string or cell array of character vectors. C = categorical (A, valueset, catnames) creates a categorical array from input A with the categories specified in valueset and named after the corresponding values in catnames, which must be specified either as a string array or a cell array of character vectors. If omitted, categorical uses the cellstring representation of valueset to name the specified category names. catnames must not contain any missing values, it may have duplicate names, and it must have the same number of elements as valueset. C = categorical (..., Name, Value) further specifies additional parameters for creating categorical array C. "Ordinal" must be a logical scalar specifying that the categories in C have a numeric ordering relationship. By default, it is false and categorical creates a non-ordinal array. The elements of unordered categorical arrays can only be compared for equality. Any other relational operator cannot be used. Setting "Ordinal" to true results in a categorical array with mathematically ordered categories. The ordering goes from smallest to largest according to the order in valueset or the order of appearance in input array A, when valueset is not specified, in which case the unique values in A are not sorted in order to set the categories. Ordinal categorical arrays allow for relational operators such as >=, >, <=, <, as well as statistical operations such as min, max, and median. "Protected" must be a logical scalar specifying that the categories in C are protected. By default, it is false for unordered categorical arrays and it is always true for ordinal categorical arrays. Setting "Protected" to true prevents from assigning new values that do not correspond to existing categories. When false, assigning new values to the array automatically adds the categories of the values assigned. Hence, categorical arrays with different sets of categories can be combined/merged into a new array with set operations. See also: categorical.categories, discretize, iscategorical # name: # type: sq_string # elements: 1 # length: 41 Create a new array of categorical values. # name: # type: sq_string # elements: 1 # length: 22 categorical.categories # name: # type: sq_string # elements: 1 # length: 179 categorical: cstr = categories (C) List of categories in categorical array. cstr = categories (C) returns a cell array of character vectors with the names of the categories in C. # name: # type: sq_string # elements: 1 # length: 40 List of categories in categorical array. # name: # type: sq_string # elements: 1 # length: 19 categorical.cellstr # name: # type: sq_string # elements: 1 # length: 219 categorical: cstr = cellstr (C) Convert categorical array to a cell array of character vectors. cstr = cellstr (C) returns a cellstr array of character vectors, cstr, which has the same size as the input categorical C. # name: # type: sq_string # elements: 1 # length: 63 Convert categorical array to a cell array of character vectors. # name: # type: sq_string # elements: 1 # length: 16 categorical.char # name: # type: sq_string # elements: 1 # length: 234 categorical: CM = char (C) Convert categorical array to a 2-D character matrix. CM = char (C) returns a character matrix CM, which contains numel (C) rows and each row contains the category name for the corresponding element of C(:). # name: # type: sq_string # elements: 1 # length: 52 Convert categorical array to a 2-D character matrix. # name: # type: sq_string # elements: 1 # length: 21 categorical.circshift # name: # type: sq_string # elements: 1 # length: 739 categorical: B = circshift (A, n) categorical: B = circshift (A, n, dim) Circularly shift the elements in a categorical array. B = circshift (A, n) circularly shifts the elements of the categorical array A according to n. If n is a nonzero integer scalar, then the elements of A are shifted by n elements along the first non-singleton dimension of A. If n is a vector, it must not be longer that the number of dimensions of A with each value of n corresponding to a dimension in A. The sign of the value(s) in n specify the direction in the elements of A are shifted. B = circshift (A, n, dim) circularly shifts the elements of the categorical array A along the dimension specified by dim. In this case, n must be a scalar integer value. # name: # type: sq_string # elements: 1 # length: 53 Circularly shift the elements in a categorical array. # name: # type: sq_string # elements: 1 # length: 21 categorical.countcats # name: # type: sq_string # elements: 1 # length: 524 categorical: N = countcats (C) categorical: N = countcats (C, dim) Count occurrences of categories in a categorical array. N = countcats (C) returns the number of elements for each category in C. If C is a vector, N is also a vector with one element for each category in C. If C is a matrix, N is a matrix with each column containing the category counts from each column of C. For multidimensional arrays, countcats operates along the first non-singleton dimension. N = countcats (C, dim) operates along the dimension dim. # name: # type: sq_string # elements: 1 # length: 55 Count occurrences of categories in a categorical array. # name: # type: sq_string # elements: 1 # length: 22 categorical.ctranspose # name: # type: sq_string # elements: 1 # length: 239 categorical: B = ctranspose (A) Transpose a categorical matrix. B = ctranspose (A) is the equivalent of the syntax B = A' and returns the transpose of the categorical matrix A. For categorical arrays, ctranspose is identical to transpose. # name: # type: sq_string # elements: 1 # length: 31 Transpose a categorical matrix. # name: # type: sq_string # elements: 1 # length: 23 categorical.dispstrings # name: # type: sq_string # elements: 1 # length: 234 categorical: cstr = dispstrings (C) Get display formatted strings for each element of a categorical array. cstr = dispstrings (C) returns a cellstr array of character vectors, cstr, which has the same size as the input categorical C. # name: # type: sq_string # elements: 1 # length: 70 Get display formatted strings for each element of a categorical array. # name: # type: sq_string # elements: 1 # length: 18 categorical.double # name: # type: sq_string # elements: 1 # length: 209 categorical: out = double (C) Convert categorical array to a double array. out = double (C) returns a double array indexing the categories in C. Categorical elements of undefined category are returned as NaN. # name: # type: sq_string # elements: 1 # length: 44 Convert categorical array to a double array. # name: # type: sq_string # elements: 1 # length: 17 categorical.empty # name: # type: sq_string # elements: 1 # length: 639 categorical: E = categorical.empty () categorical: E = categorical.empty (sz) categorical: E = categorical.empty (m, n, ...) Create an empty categorical array. E = categorical.empty () returns a 0x0 empty categorical array with no categories. categorical.empty (m, n, ...) or categorical.empty (sz) returns an empty categorical array of the requested size, which must have at least one dimension equal to zero. A lone dimension gives a square size, so categorical.empty (3) is an error while categorical.empty (0) is 0x0. As for zeros, a negative dimension counts as zero, and a size vector with nothing in it names no size and gives 0x0. # name: # type: sq_string # elements: 1 # length: 34 Create an empty categorical array. # name: # type: sq_string # elements: 1 # length: 14 categorical.eq # name: # type: sq_string # elements: 1 # length: 1214 categorical: TF = eq (A, B) Equality for categorical arrays. TF = eq (A, B) is the equivalent of the syntax TF = A == B and returns a logical array of the same size as the largest input with its elements set to true where the corresponding elements of A and B are equal and set to false where they are not. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. If categorical arrays A and B are ordinal, they must have the same set and ordering of categories. If neither are ordinal, the category names of each pair of elements are compared. Hence, they do not need to have the same set of categories. One of the input arguments can also be a character vector, a cellstr scalar or a string scalar as long as the other is a categorical array. In this case, a logical array of the same size as the categorical array is returned in which every element is tested for equality by comparing its category with that specified by the string argument. Undefined elements always return false, since they are not comparable to any other categorical values including other undefined elements. # name: # type: sq_string # elements: 1 # length: 32 Equality for categorical arrays. # name: # type: sq_string # elements: 1 # length: 14 categorical.ge # name: # type: sq_string # elements: 1 # length: 1232 categorical: TF = ge (A, B) Greater than or equal to for ordinal categorical arrays. TF = ge (A, B) is the equivalent of the syntax TF = A >= B and returns a logical array of the same size as the largest input with its elements set to true where the corresponding elements of A are greater than or equal to B and set to false where they are not. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. If categorical arrays A and B are both ordinal, they must have the same set and ordering of categories. Unordered categorical arrays cannot be compared for greater than or equal to inequality. One of the input arguments can also be a character vector, a cellstr scalar or a string scalar as long as the other is a categorical array. In this case, a logical array of the same size as the categorical array is returned in which every element is tested for greater than or equal to inequality by comparing its category with that specified by the string argument. Undefined elements always return false, since they are not comparable to any other categorical values including other undefined elements. # name: # type: sq_string # elements: 1 # length: 56 Greater than or equal to for ordinal categorical arrays. # name: # type: sq_string # elements: 1 # length: 14 categorical.gt # name: # type: sq_string # elements: 1 # length: 1183 categorical: TF = gt (A, B) Greater than for ordinal categorical arrays. TF = gt (A, B) is the equivalent of the syntax TF = A > B and returns a logical array of the same size as the largest input with its elements set to true where the corresponding elements of A are greater than B and set to false where they are not. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. If categorical arrays A and B are both ordinal, they must have the same set and ordering of categories. Unordered categorical arrays cannot be compared for greater than inequality. One of the input arguments can also be a character vector, a cellstr scalar or a string scalar as long as the other is a categorical array. In this case, a logical array of the same size as the categorical array is returned in which every element is tested for greater than inequality by comparing its category with that specified by the string argument. Undefined elements always return false, since they are not comparable to any other categorical values including other undefined elements. # name: # type: sq_string # elements: 1 # length: 44 Greater than for ordinal categorical arrays. # name: # type: sq_string # elements: 1 # length: 22 categorical.histcounts # name: # type: sq_string # elements: 1 # length: 1879 categorical: N = histcounts (A) categorical: N = histcounts (A, cats) categorical: N = histcounts (..., 'Normalization', normtype) categorical: [N, cats] = histcounts (...) Histogram bin counts of a categorical array. N = histcounts (A) returns a numeric vector N with the number of elements of each category in A. A can be a categorical array of any dimensions, but it is converted internally to a single column vector. N = histcounts (A, cats) returns the number of elements only for the categories named in cats, in the order they are named there. cats may be a categorical array, a string array, or a cell array of character vectors, and the names must be unique. A name that is not a category of A is not an error: it counts zero. Note that elements are counted under no name at all, so they never appear in N. N = histcounts (..., 'Normalization', normtype) specifies how to normalize the histogram values returned in N with any of the following options specified in normtype: 'count', which is the default, returns the number of elements in each category. 'countdensity' is the same as 'count', since the bin width in categorical arrays is always equal to 1. 'probability' returns the number of elements in each category relative to the total number of elements in A. 'percentage' returns the percentage of the elements of A that fall in each category. 'pdf' is the same as 'probability', since the bin width in categorical arrays is always equal to 1. 'cumcount' returns the cumulative number of elements in each category and all previous categories. 'cdf' returns the cumulative number of elements in each category and all previous categories relative to the total number of elements in A. [N, cats] = histcounts (...) also returns the corresponding categories of A for each count in N. cats is a cell array of character vectors with the same size as N. # name: # type: sq_string # elements: 1 # length: 44 Histogram bin counts of a categorical array. # name: # type: sq_string # elements: 1 # length: 19 categorical.horzcat # name: # type: sq_string # elements: 1 # length: 1195 categorical: C = horzcat (A, B, ...) Horizontal concatenation of categorical arrays. C = horzcat (A, B, ... is the equivalent of the syntax B = [A, B, ...] and horizontally concatenates the categorical arrays A, B, .... All input arrays must have the same size except along the second dimension. Any of the input arrays may also be character vectors, string arrays or cell arrays of character vectors of compatible size, whose texts name categories, or missing values, which are undefined. Empty arrays are ignored. A numeric or logical array is refused, since a number could name a category or its code. If any input array is an ordinal categorical array, then all inputs must be ordinal categorical arrays with the same set and ordering of categories. In this case, C is also an ordinal categorical array with the same set and ordering of categories. If none of the input arrays are ordinal, then they do not need to have the same set of categories. In this case, categorical array C contains the union of the categories from all input arrays. Protected categorical arrays can only be concatenated with other arrays that have the same set of categories but not necessarily in the same order. # name: # type: sq_string # elements: 1 # length: 47 Horizontal concatenation of categorical arrays. # name: # type: sq_string # elements: 1 # length: 17 categorical.int16 # name: # type: sq_string # elements: 1 # length: 289 categorical: out = int16 (C) Convert categorical array to a int16 array. out = int16 (C) returns a int16 array indexing the categories in C. Categorical elements of undefined category are returned as 0. Note that the returned category indices saturate to intmax ('int16'), which is 32767. # name: # type: sq_string # elements: 1 # length: 43 Convert categorical array to a int16 array. # name: # type: sq_string # elements: 1 # length: 17 categorical.int32 # name: # type: sq_string # elements: 1 # length: 203 categorical: out = int32 (C) Convert categorical array to a int32 array. out = int32 (C) returns a int32 array indexing the categories in C. Categorical elements of undefined category are returned as 0. # name: # type: sq_string # elements: 1 # length: 43 Convert categorical array to a int32 array. # name: # type: sq_string # elements: 1 # length: 17 categorical.int64 # name: # type: sq_string # elements: 1 # length: 203 categorical: out = int64 (C) Convert categorical array to a int64 array. out = int64 (C) returns a int64 array indexing the categories in C. Categorical elements of undefined category are returned as 0. # name: # type: sq_string # elements: 1 # length: 43 Convert categorical array to a int64 array. # name: # type: sq_string # elements: 1 # length: 16 categorical.int8 # name: # type: sq_string # elements: 1 # length: 282 categorical: out = int8 (C) Convert categorical array to a int8 array. out = int8 (C) returns a int8 array indexing the categories in C. Categorical elements of undefined category are returned as 0. Note that the returned category indices saturate to intmax ('int8'), which is 127. # name: # type: sq_string # elements: 1 # length: 42 Convert categorical array to a int8 array. # name: # type: sq_string # elements: 1 # length: 21 categorical.intersect # name: # type: sq_string # elements: 1 # length: 1564 categorical: C = intersect (A, B) categorical: C = intersect (A, B, 'rows') categorical: [C, ixA, ixB] = intersect (...) categorical: ... = intersect (..., order) Set intersection of two categorical arrays. C = intersect (A, B) returns the unique common values of the categorical arrays A and B. Either A or B input arguments may be a character vector, a string array, or a cell array of character vectors, which is promoted to a categorical array prior to set intersection. If both A and B are row vectors, then C is also a row vector, otherwise intersect returns a column vector. If categorical arrays A and B are ordinal, they must have the same set and ordering of categories, which is transfered to C. If neither are ordinal, the category names of each pair of elements are compared (they do not need to have the same set of categories) in which case the categories in C are the sorted union of the categories in A and B. C = intersect (A, B, 'rows' returns the unique common rows of the categorical matrices A and B, which must have the same number of columns. By default, the rows in categorical matrix C are in sorted order. [C, ixA, ixB] = intersect (...) also returns index vectors ixA and ixB such that C = A(ixA) and C = B(ixB), unless the 'rows' optional argument is given, in which case C = A(ixA,:) and C = B(ixB,:). ... = intersect (..., order) also specifies the order of the returned unique values. order may be either 'sorted', which is the default behavior, or 'stable', in which case the unique values are returned in order of appearance. # name: # type: sq_string # elements: 1 # length: 43 Set intersection of two categorical arrays. # name: # type: sq_string # elements: 1 # length: 20 categorical.ipermute # name: # type: sq_string # elements: 1 # length: 469 categorical: A = ipermute (B, dims) Inverse of the generalized transpose for a categorical N-D array. A = ipermute (B, dims) returns the inverse of the generalized transpose performed by the permute function. The expression ipermute (permute (A, dims), dims) returns the original array A. dims must index all the dimensions 1:ndims (B) of the input array B, in any order, but only once. The dimension of B specified in dims(N) gets remapped to the Nth dimension of A. # name: # type: sq_string # elements: 1 # length: 65 Inverse of the generalized transpose for a categorical N-D array. # name: # type: sq_string # elements: 1 # length: 22 categorical.iscategory # name: # type: sq_string # elements: 1 # length: 295 categorical: TF = iscategory (C, catnames) Test for categories in a categorical array. TF = iscategory (C, catnames) returns a logical array TF of the same size as catnames containing true for each corresponding element of catnames that is a category in categorical array C and false otherwise. # name: # type: sq_string # elements: 1 # length: 43 Test for categories in a categorical array. # name: # type: sq_string # elements: 1 # length: 20 categorical.iscolumn # name: # type: sq_string # elements: 1 # length: 300 categorical: TF = iscolumn (C) Return true if categorical array is a column vector. TF = iscolumn (C) returns a logical scalar TF, which is true, if the categorical array C is a column vector, and false otherwise. A column vector is a 2-D array for which size (X) returns [N, 1] with non-negative N. # name: # type: sq_string # elements: 1 # length: 52 Return true if categorical array is a column vector. # name: # type: sq_string # elements: 1 # length: 19 categorical.isempty # name: # type: sq_string # elements: 1 # length: 192 categorical: TF = isempty (C) Return true if categorical array is empty. TF = isempty (C) returns a logical scalar TF, which is true, if the categorical array C is empty, and false otherwise. # name: # type: sq_string # elements: 1 # length: 42 Return true if categorical array is empty. # name: # type: sq_string # elements: 1 # length: 19 categorical.isequal # name: # type: sq_string # elements: 1 # length: 832 categorical: TF = isequal (C1, C2) categorical: TF = isequal (C1, C2, ...) Return true if categorical arrays are equal. TF = isequal (C1, C2) returns a logical scalar TF, which is true, if the categorical arrays C1 and C2 contain the same values, and false otherwise. Either C1 or C2 may also be a string array, a missing object array, a character vector, or a cell array of character vectors, which will be promoted to a categorical array prior to comparison. If categorical arrays C1 and C2 are ordinal, they must have the same set and ordering of categories. If neither are ordinal, the category names of each pair of elements are compared. Hence, they do not need to have the same set of categories. TF = isequal (C1, C2, ...) returns a logical scalar TF, which is true, if all input arguments are equal, and false otherwise. # name: # type: sq_string # elements: 1 # length: 44 Return true if categorical arrays are equal. # name: # type: sq_string # elements: 1 # length: 20 categorical.isequaln # name: # type: sq_string # elements: 1 # length: 927 categorical: TF = isequaln (C1, C2) categorical: TF = isequaln (C1, C2, ...) Return true if categorical arrays are equal under the assumption that undefined elements are equal. TF = isequaln (C1, C2) returns a logical scalar TF, which is true, if the categorical arrays C1 and C2 contain the same values or corresponding undefined elements, and false otherwise. Either C1 or C2 may also be a string array, a missing object array, a character vector, or a cell array of character vectors, which will be promoted to a categorical array prior to comparison. If categorical arrays C1 and C2 are ordinal, they must have the same set and ordering of categories. If neither are ordinal, the category names of each pair of elements are compared. Hence, they do not need to have the same set of categories. TF = isequaln (C1, C2, ...) returns a logical scalar TF, which is true, if all input arguments are equal, and false otherwise. # name: # type: sq_string # elements: 1 # length: 99 Return true if categorical arrays are equal under the assumption that undefined elements are equal. # name: # type: sq_string # elements: 1 # length: 20 categorical.ismatrix # name: # type: sq_string # elements: 1 # length: 322 categorical: TF = ismatrix (C) Return true if categorical array is a 2-D array. TF = ismatrix (C) returns a logical scalar TF, which is true, if the categorical array C is a matrix, and false otherwise. A matrix is an array of any type where ndims (X) == 2 and for which size (X) returns [H, W] with non-negative H and W. # name: # type: sq_string # elements: 1 # length: 48 Return true if categorical array is a 2-D array. # name: # type: sq_string # elements: 1 # length: 20 categorical.ismember # name: # type: sq_string # elements: 1 # length: 1706 categorical: TF = ismember (A, B) categorical: TF = ismember (A, B, 'rows') categorical: [TF, index] = ismember (...) categorical: [TF, index] = ismember (..., 'legacy') Find categorical elements in a set. TF = ismember (A, B) returns a logical array TF of the same size as A containing true for each corresponding element of A that is in B and false otherwise. elements are not equal with each other and always return false. If A and B are ordinal, they must have the same ordered set of categories. If neither A nor B are ordinal, then this restriction is relaxed and comparison is performed using the category names. Comparison between an ordinal and an unordered categorical array is not allowed. Either A or B may also be a string array, a character vector, or a cell array of character vectors containing one or multiple category names to compare against an unordered categorical array. TF = ismember (A, B, 'rows') only applies to categorical matrices with the same number of columns, in which case the logical vector TF contains true for each row of A that is also a row in B. TF has the same number of rows as A. [TF, index] = ismember (A, B) also returns an index array of the same size as A containing the lowest index in B for each element of A that is a member of B and 0 otherwise. If the 'rows' optional argument is used, then the returning index is a column vector with the same rows as A and it contains the lowest index in B for each row of A that is a member of B and 0 otherwise. If the 'legacy' optional argument is specified, then the highest index of matched elements is returned. Unless multiple matches exist, the 'legacy' option has no effect on the returned index. # name: # type: sq_string # elements: 1 # length: 35 Find categorical elements in a set. # name: # type: sq_string # elements: 1 # length: 21 categorical.ismissing # name: # type: sq_string # elements: 1 # length: 1281 categorical: out = ismissing (C) categorical: out = ismissing (C, indicator) Find missing elements in categorical array. TF = ismissing (C) returns a logical array TF of the same size as C containing true for each corresponding element of C that does not have a value from one of the categories in C and false otherwise. TF = ismissing (C, indicator) returns a logical array TF containing true for each corresponding element of C that matches a value in indicator and false otherwise. The undefined elements of C are not marked, unless indicator names them. indicator must be a character vector, a cell vector of character vectors, a string vector, missing, or a categorical vector. Character vectors are read as when constructing a categorical array: an empty or blank character vector matches the undefined elements of C, and any other matches the elements of C in the category of that name. Strings match only the elements of C in the category of that name, so neither "" nor matches the undefined elements. missing matches the undefined elements of C. A categorical indicator matches the elements of C in each of its categories, and its undefined elements match the undefined elements of C. MATLAB accepts no categorical indicator, so this is an Octave extension. # name: # type: sq_string # elements: 1 # length: 43 Find missing elements in categorical array. # name: # type: sq_string # elements: 1 # length: 21 categorical.isordinal # name: # type: sq_string # elements: 1 # length: 193 categorical: TF = isordinal (C) Test if categorical array is ordinal. TF = isordinal (C) returns a logical scalar TF, which is true, if the categorical array C is ordinal, and false otherwise. # name: # type: sq_string # elements: 1 # length: 37 Test if categorical array is ordinal. # name: # type: sq_string # elements: 1 # length: 23 categorical.isprotected # name: # type: sq_string # elements: 1 # length: 201 categorical: TF = isprotected (C) Test if categorical array is protected. TF = isprotected (C) returns a logical scalar TF, which is true, if the categorical array C is protected, and false otherwise. # name: # type: sq_string # elements: 1 # length: 39 Test if categorical array is protected. # name: # type: sq_string # elements: 1 # length: 17 categorical.isrow # name: # type: sq_string # elements: 1 # length: 285 categorical: TF = isrow (C) Return true if categorical array is a row vector. TF = isrow (C) returns a logical scalar TF, which is true, if the categorical array C is a row vector, and false otherwise. A row vector is a 2-D array for which size (X) returns [1, N] with non-negative N. # name: # type: sq_string # elements: 1 # length: 49 Return true if categorical array is a row vector. # name: # type: sq_string # elements: 1 # length: 20 categorical.isscalar # name: # type: sq_string # elements: 1 # length: 276 categorical: TF = isscalar (C) Return true if categorical array is a scalar. TF = isscalar (C) returns a logical scalar TF, which is true, if the categorical array C is also a scalar, and false otherwise. A scalar is a single element object for which size (X) returns [1, 1]. # name: # type: sq_string # elements: 1 # length: 45 Return true if categorical array is a scalar. # name: # type: sq_string # elements: 1 # length: 20 categorical.issorted # name: # type: sq_string # elements: 1 # length: 1680 categorical: TF = issorted (C) categorical: TF = issorted (C, dim) categorical: TF = issorted (C, direction) categorical: TF = issorted (C, dim, direction) categorical: TF = issorted (..., 'MissingPlacement', MP) Return true if categorical array is sorted. TF = issorted (C) returns a logical scalar TF, which is true, if the categorical array C is sorted in ascending order, and false otherwise. TF = issorted (C, dim) returns a logical scalar TF, which is true, if the categorical array C is sorted in ascending order along the dimension dim, and false otherwise. TF = issorted (A, direction) returns a logical scalar TF, which is true, if the categorical array C is sorted in the direction specified by direction, and false otherwise. direction can be one of the following options: 'ascend', which is the default, checks is elements are in ascending order. 'descend' checks if elements are in descending order. 'monotonic' checks if elements are either in ascending or descending order. 'strictascend' checks if elements are in ascending order and there are no duplicate or undefined elements. 'strictdescend' checks if elements are in descending order and there are no duplicate or undefined elements. 'strictmonotonic' checks if elements are either in ascending or descending order and there are no duplicate or undefined elements. TF = issorted (..., 'MissingPlacement', MP) specifies where missing elements () are placed with one of the following options specified in MP: 'auto', which is the default, places missing elements last for ascending sort and first for descending sort. 'first' places missing elements first. 'last' places missing elements last. # name: # type: sq_string # elements: 1 # length: 43 Return true if categorical array is sorted. # name: # type: sq_string # elements: 1 # length: 24 categorical.issortedrows # name: # type: sq_string # elements: 1 # length: 2514 categorical: TF = issortedrows (C) categorical: TF = issortedrows (C, col) categorical: TF = issortedrows (C, direction) categorical: TF = issortedrows (C, col, direction) categorical: TF = issortedrows (..., 'MissingPlacement', MP) Return true if categorical matrix rows are sorted. TF = issortedrows (C) returns a logical scalar TF, which is true, if the rows in the 2-D categorical array C are sorted in ascending order, and false otherwise. TF = issortedrows (C, col) returns a logical scalar TF, which is true, if the categorical array C is sorted according to the columns specified by the vector col, and false otherwise. col must explicitly contain non-zero integers whose absolute values index existing columns in C. Positive elements sort the corresponding columns in ascending order, while negative elements sort the corresponding columns in descending order. TF = issortedrows (C, direction) checks if the rows in C are sorted according to the specified direction, which can be one of the following options: 'ascend', which is the default, checks is elements are in ascending order. 'descend' checks if elements are in descending order. 'monotonic' checks if elements are either in ascending or descending order. 'strictascend' checks if elements are in ascending order and there are no duplicate or undefined elements. 'strictdescend' checks if elements are in descending order and there are no duplicate or undefined elements. 'strictmonotonic' checks if elements are either in ascending or descending order and there are no duplicate or undefined elements. Alternatively, direction can be a cell array of character vectors specifying the sorting direction for each individual column of C, in which case the number of elements in direction must equal the number of columns in C. TF = issortedrows (C, col, direction) checks if the rows in the categorical array C are sorted according to the columns specified in col using the corresponding sorting direction specified in direction. In this case, the sign of the values in col is ignored. col and direction must have the same length, but not necessarily the same number of elements as the columns in C. TF = issortedrows (..., 'MissingPlacement', MP) specifies where missing elements () are placed with one of the following options specified in MP: 'auto', which is the default, places missing elements last for ascending sort and first for descending sort. 'first' places missing elements first. 'last' places missing elements last. # name: # type: sq_string # elements: 1 # length: 50 Return true if categorical matrix rows are sorted. # name: # type: sq_string # elements: 1 # length: 23 categorical.isundefined # name: # type: sq_string # elements: 1 # length: 343 categorical: out = isundefined (C) Test for undefined elements in categorical array. TF = isundefined (C) returns a logical array TF of the same size as C containing true for each corresponding element of C that does not have a value from one of the categories in C and false otherwise. is the equivalent of NaN in numeric arrays. # name: # type: sq_string # elements: 1 # length: 49 Test for undefined elements in categorical array. # name: # type: sq_string # elements: 1 # length: 20 categorical.isvector # name: # type: sq_string # elements: 1 # length: 331 categorical: TF = isvector (C) Return true if categorical array is a vector. TF = isvector (C) returns a logical scalar TF, which is true if the categorical array C is a vector and false otherwise. A vector is a 2-D array for which one of the dimensions is equal to 1 (either 1xN or Nx1). By definition, a scalar is also a vector. # name: # type: sq_string # elements: 1 # length: 45 Return true if categorical array is a vector. # name: # type: sq_string # elements: 1 # length: 19 categorical.keyHash # name: # type: sq_string # elements: 1 # length: 1289 categorical: key = keyHash (C) categorical: key = keyHash (C, base) Generate a hash code for a categorical array. key = keyHash (C) generates a uint64 scalar that represents the input array C. keyHash utilizes the 64-bit FNV-1a variant of the Fowler-Noll-Vo non-cryptographic hash function. key = keyHash (C, base) also generates a 64-bit hash code using base as the offset basis for the FNV-1a hash algorithm. base must be a uint64 integer type scalar. Use this syntax to cascade keyHash on multiple objects for which a single hash code is required. A categorical array is keyed on its element labels, on its ordinality, and -- for an ordinal array alone -- on the order of its categories, which is what its comparisons mean. Neither the Protected property nor a non-ordinal array's category list is part of the key, so two non-ordinal arrays carrying the same labels hash alike however their categories differ or are ordered. Undefined elements are equal to one another and distinct from every category. Two arrays that keyMatch reports as the same key always hash alike. Note that unlike MATLAB, this implementation does not use any random seed. As a result, keyHash will always generate the exact same hash key for any particular input across different workers and Octave sessions. # name: # type: sq_string # elements: 1 # length: 45 Generate a hash code for a categorical array. # name: # type: sq_string # elements: 1 # length: 14 categorical.le # name: # type: sq_string # elements: 1 # length: 1220 categorical: TF = le (A, B) Less than or equal to for ordinal categorical arrays. TF = le (A, B) is the equivalent of the syntax TF = A <= B and returns a logical array of the same size as the largest input with its elements set to true where the corresponding elements of A are less than or equal to B and set to false where they are not. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. If categorical arrays A and B are both ordinal, they must have the same set and ordering of categories. Unordered categorical arrays cannot be compared for less than or equal to inequality. One of the input arguments can also be a character vector, a cellstr scalar or a string scalar as long as the other is a categorical array. In this case, a logical array of the same size as the categorical array is returned in which every element is tested for less than or equal to inequality by comparing its category with that specified by the string argument. Undefined elements always return false, since they are not comparable to any other categorical values including other undefined elements. # name: # type: sq_string # elements: 1 # length: 53 Less than or equal to for ordinal categorical arrays. # name: # type: sq_string # elements: 1 # length: 18 categorical.length # name: # type: sq_string # elements: 1 # length: 227 categorical: N = length (C) Length of a categorical vector. N = length (C) returns the size of the longest dimension of the categorical array C, unless any of its dimensions has zero length, in which case length (C) returns 0. # name: # type: sq_string # elements: 1 # length: 31 Length of a categorical vector. # name: # type: sq_string # elements: 1 # length: 14 categorical.lt # name: # type: sq_string # elements: 1 # length: 1171 categorical: TF = lt (A, B) Less than for ordinal categorical arrays. TF = lt (A, B) is the equivalent of the syntax TF = A < B and returns a logical array of the same size as the largest input with its elements set to true where the corresponding elements of A are less than B and set to false where they are not. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. If categorical arrays A and B are both ordinal, they must have the same set and ordering of categories. Unordered categorical arrays cannot be compared for less than inequality. One of the input arguments can also be a character vector, a cellstr scalar or a string scalar as long as the other is a categorical array. In this case, a logical array of the same size as the categorical array is returned in which every element is tested for less than inequality by comparing its category with that specified by the string argument. Undefined elements always return false, since they are not comparable to any other categorical values including other undefined elements. # name: # type: sq_string # elements: 1 # length: 41 Less than for ordinal categorical arrays. # name: # type: sq_string # elements: 1 # length: 15 categorical.max # name: # type: sq_string # elements: 1 # length: 2558 categorical: C = max (A) categorical: [C, index] = max (A) categorical: C = max (A, [], dim) categorical: C = max (A, [], vecdim) categorical: C = max (A, [], 'all') categorical: [C, index] = max (A, [], 'linear') categorical: [C, index] = max (A, [], ..., 'linear') categorical: C = max (A, B) categorical: [...] = max (..., missingflag) Largest elements in ordinal categorical arrays. C = max (A) returns the largest element in ordinal categorical vector A. If A is a matrix, max (A) returns a row vector with the largest element from each column. For multidimensional arrays, max (A) operates along the first non-singleton dimension. [C, index] = max (A) also returns the indices of the maximum values in index, which has the same size as C. When the operating dimension contains more than one maximal elements, the index of the first one is returned. C = max (A, [], dim) operates along the dimension specified by dim. C = max (A, [], vecdim) operates on all the elements contained in the dimensions specified by vecdim, which must be a numeric vector of non-repeating positive integers. Any values in vecdim indexing dimensions larger that the actual array A are ignored. C = max (A, [], 'all') operates on all dimensions and returns the largest element in A. [C, index] = max (A, [], ...) also returns the first index of the maximum values in index. The second output is only valid when max operates on a single input array. Setting the 'linear' flag returns the linear index to the corresponding maximum values in A. C = max (A, B) returns an ordinal categorical array C with the largest elements from A and B, which both must be ordinal categorical arrays of compatible sizes with the same set and ordering of categories. Compatible size means that A and B can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. [...] = max (..., missingflag) specifies how to handle undefined elements in any of the previous syntaxes. missingflag must be a character vector or a string scalar with one of the following values: 'omitundefined', which is the default, ignores all undefined elements and returns the maximum of the remaining elements. If all elements along the operating dimension are undefined, then it returns an undefined element. 'omitnan' may also be used as equivalent to 'omitundefined'. 'includeundefined' returns an undefined element if there any undefined elements along the operating dimension. 'includenan' may also be used as equivalent to 'includeundefined'. # name: # type: sq_string # elements: 1 # length: 47 Largest elements in ordinal categorical arrays. # name: # type: sq_string # elements: 1 # length: 16 categorical.maxk # name: # type: sq_string # elements: 1 # length: 1345 categorical: B = maxk (A, K) categorical: B = maxk (A, K, dim) categorical: [B, index] = maxk (A, ...) Find the largest elements in an ordinal categorical array. B = maxk (A, K) returns the K largest elements of the categorical array A in descending order. A must be ordinal, since the elements are ranked by the order of their categories. If A is a vector, then B is a vector with K elements. If A is a matrix, then maxk operates along each column of A and B has K rows. For multidimensional arrays, maxk operates along the first non-singleton dimension. B keeps the categories of A, including their order. B = maxk (A, K, dim) operates along the dimension specified by dim. K must be a nonnegative integer scalar. If K is larger than the number of elements along the operating dimension, then all of them are returned. Missing elements () are not ranked. They are appended after the ranked elements in their original order, and hence they only appear in B when K exceeds the number of defined elements along the operating dimension. Unlike sort, maxk has no 'MissingPlacement' option, since undefined elements are always placed last. Elements comparing as equal keep their original order. [B, index] = maxk (A, ...) also returns an index array containing the indices of the returned elements of A along the operating dimension. # name: # type: sq_string # elements: 1 # length: 58 Find the largest elements in an ordinal categorical array. # name: # type: sq_string # elements: 1 # length: 18 categorical.median # name: # type: sq_string # elements: 1 # length: 1837 categorical: B = median (A) categorical: B = median (A, dim) categorical: B = median (A, vecdim) categorical: B = median (A, 'all') categorical: B = median (..., missingflag) Median value of an ordinal categorical array. B = median (A) returns the median of the elements in ordinal categorical vector A. If A is a matrix, median (A) returns a row vector with the median element from each column. For multidimensional arrays, median (A) operates along the first non-singleton dimension. B is also ordinal with the same ordered categories as A. For even number of elements along the operating dimension, the returned median value is either the midway category between the two middle elements or the larger of the two categories midway between the two middle elements. B = median (A, dim) operates along the dimension specified by dim. B = median (A, vecdim) operates on all the elements contained in the dimensions specified by vecdim, which must be a numeric vector of non-repeating positive integers. Any values in vecdim indexing dimensions larger that the actual array A are ignored. C = median (A, [], 'all') operates on all dimensions and returns the median element in A. C = median (..., missingflag) specifies how to handle undefined elements in any of the previous syntaxes. missingflag must be a character vector or a string scalar with one of the following values: 'omitundefined' ignores all undefined elements and returns the median of the remaining elements. If all elements along the operating dimension are undefined, then it returns an undefined element. 'omitnan' may also be used as equivalent to 'omitundefined'. 'includeundefined', which is the default, returns an undefined element if there any undefined elements along the operating dimension. 'includenan' may also be used as equivalent to 'includeundefined'. # name: # type: sq_string # elements: 1 # length: 45 Median value of an ordinal categorical array. # name: # type: sq_string # elements: 1 # length: 21 categorical.mergecats # name: # type: sq_string # elements: 1 # length: 942 categorical: B = mergecats (A, oldcats) categorical: B = mergecats (A, oldcats, newcat) Merge categories in categorical array. B = mergecats (A, oldcats) merges two or more categories specified by oldcats into a single category with the same name as oldcats(1). In case of ordinal categorical arrays, the categories listed in oldcats must be in consecutive order. All elements of A corresponding to the categories listed in oldcats are re-indexed to correspond to oldcats(1) in B. B = mergecats (A, oldcats, newcat) merges the categories listed in oldcats into a single new category named as specified by newcat. newcat must be either a character vector, a cellstr scalar or a string scalar. oldcats can be string array, a cell array of character vectors, or any type of array that can be converted to a cell array of character vectors with the cellstr function. Any names in oldcats that do not reference an existing category are ignored. # name: # type: sq_string # elements: 1 # length: 38 Merge categories in categorical array. # name: # type: sq_string # elements: 1 # length: 15 categorical.min # name: # type: sq_string # elements: 1 # length: 2567 categorical: C = min (A) categorical: [C, index] = min (A) categorical: C = min (A, [], dim) categorical: C = min (A, [], vecdim) categorical: C = min (A, [], 'all') categorical: [C, index] = min (A, [], 'linear') categorical: [C, index] = min (A, [], ..., 'linear') categorical: C = min (A, B) categorical: [...] = min (..., missingflag) Smallest elements in ordinal categorical arrays. C = min (A) returns the smallest element in ordinal categorical vector A. If A is a matrix, min (A) returns a row vector with the smallest element from each column. For multidimensional arrays, min (A) operates along the first non-singleton dimension. [C, index] = min (A) also returns the indices of the minimum values in index, which has the same size as C. When the operating dimension contains more than one minimal elements, the index of the first one is returned. C = min (A, [], dim) operates along the dimension specified by dim. C = min (A, [], vecdim) operates on all the elements contained in the dimensions specified by vecdim, which must be a numeric vector of non-repeating positive integers. Any values in vecdim indexing dimensions larger that the actual array A are ignored. C = min (A, [], 'all') operates on all dimensions and returns the smallest element in A. [C, index] = min (A, [], ...) also returns the first index of the minimum values in index. The second output is only valid when min operates on a single input array. Setting the 'linear' flag returns the linear index to the corresponding minimum values in A. C = min (A, B) returns an ordinal categorical array C with the smallest elements from A and B, which both must be ordinal categorical arrays of compatible sizes with the same set and ordering of categories. Compatible size means that A and B can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. [...] = min (..., missingflag) specifies how to handle undefined elements in any of the previous syntaxes. missingflag must be a character vector or a string scalar with one of the following values: 'omitundefined', which is the default, ignores all undefined elements and returns the minimum of the remaining elements. If all elements along the operating dimension are undefined, then it returns an undefined element. 'omitnan' can also be used as equivalent to 'omitundefined'. 'includeundefined' returns an undefined element if there are any undefined elements along the operating dimension. 'includenan' can also be used as equivalent to 'includeundefined'. # name: # type: sq_string # elements: 1 # length: 48 Smallest elements in ordinal categorical arrays. # name: # type: sq_string # elements: 1 # length: 16 categorical.mink # name: # type: sq_string # elements: 1 # length: 1346 categorical: B = mink (A, K) categorical: B = mink (A, K, dim) categorical: [B, index] = mink (A, ...) Find the smallest elements in an ordinal categorical array. B = mink (A, K) returns the K smallest elements of the categorical array A in ascending order. A must be ordinal, since the elements are ranked by the order of their categories. If A is a vector, then B is a vector with K elements. If A is a matrix, then mink operates along each column of A and B has K rows. For multidimensional arrays, mink operates along the first non-singleton dimension. B keeps the categories of A, including their order. B = mink (A, K, dim) operates along the dimension specified by dim. K must be a nonnegative integer scalar. If K is larger than the number of elements along the operating dimension, then all of them are returned. Missing elements () are not ranked. They are appended after the ranked elements in their original order, and hence they only appear in B when K exceeds the number of defined elements along the operating dimension. Unlike sort, mink has no 'MissingPlacement' option, since undefined elements are always placed last. Elements comparing as equal keep their original order. [B, index] = mink (A, ...) also returns an index array containing the indices of the returned elements of A along the operating dimension. # name: # type: sq_string # elements: 1 # length: 59 Find the smallest elements in an ordinal categorical array. # name: # type: sq_string # elements: 1 # length: 16 categorical.mode # name: # type: sq_string # elements: 1 # length: 1505 categorical: M = mode (A) categorical: [M, F] = mode (A) categorical: [M, F, C] = mode (A) categorical: [...] = mode (A, dim) categorical: [...] = mode (A, vecdim) categorical: [...] = mode (A, 'all') Most frequent element in a categorical array. M = mode (A) returns the most frequent element in the categorical vector A. If A is a matrix, mode (A) returns a row vector with the most frequent element from each column. For multidimensional arrays, mode (A) operates along the first non-singleton dimension. B is also a categorical array with the same categories as A. For multiple elements with the same maximum frequency along the operating dimension, the element from the category that occurs first in A is returned. [M, F] = mode (A) also returns a numeric array F, which has the same size as M and it contains the number of occurrences of each corresponding element of M. [M, F, C] = mode (A) also returns a cell array C, which has the same size as M and each element is a sorted categorical vector of all the values with the same maximum frequency of the corresponding element of M. B = mode (A, dim) operates along the dimension specified by dim. B = mode (A, vecdim) operates on all the elements contained in the dimensions specified by vecdim, which must be a numeric vector of non-repeating positive integers. Any values in vecdim indexing dimensions larger that the actual array A are ignored. C = mode (A, [], 'all') operates on all dimensions and returns the most frequent element in A. # name: # type: sq_string # elements: 1 # length: 45 Most frequent element in a categorical array. # name: # type: sq_string # elements: 1 # length: 17 categorical.ndims # name: # type: sq_string # elements: 1 # length: 151 categorical: out = ndims (C) Number of dimensions in a categorical array. out = ndims (C) returns the number of dimensions of the categorical array C. # name: # type: sq_string # elements: 1 # length: 44 Number of dimensions in a categorical array. # name: # type: sq_string # elements: 1 # length: 14 categorical.ne # name: # type: sq_string # elements: 1 # length: 1222 categorical: TF = ne (A, B) Not equal for categorical arrays. TF = ne (A, B) is the equivalent of the syntax TF = A != B and returns a logical array of the same size as the largest input with its elements set to true where the corresponding elements of A and B are not equal and set to false where they are equal. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. If categorical arrays A and B are ordinal, they must have the same set and ordering of categories. If neither are ordinal, the category names of each pair of elements are compared. Hence, they do not need to have the same set of categories. One of the input arguments can also be a character vector, a cellstr scalar or a string scalar as long as the other is a categorical array. In this case, a logical array of the same size as the categorical array is returned in which every element is tested for inequality by comparing its category with that specified by the string argument. Undefined elements always return true, since they are not comparable to any other categorical values including other undefined elements. # name: # type: sq_string # elements: 1 # length: 33 Not equal for categorical arrays. # name: # type: sq_string # elements: 1 # length: 17 categorical.numel # name: # type: sq_string # elements: 1 # length: 153 categorical: out = numel (C) Total number of elements in a categorical array. out = numel (C) returns the number of elements in the categorical array C. # name: # type: sq_string # elements: 1 # length: 48 Total number of elements in a categorical array. # name: # type: sq_string # elements: 1 # length: 19 categorical.permute # name: # type: sq_string # elements: 1 # length: 434 categorical: B = permute (A, dims) Generalized transpose for a categorical N-D array. B = permute (A, dims) returns the generalized transpose of the categorical array A by rearranging its dimensions according to the permutation vector specified in dims. dims must index all the dimensions 1:ndims (A) of the input array A, in any order, but only once. The Nth dimension of A gets remapped to the dimension in B specified by dims(N). # name: # type: sq_string # elements: 1 # length: 50 Generalized transpose for a categorical N-D array. # name: # type: sq_string # elements: 1 # length: 22 categorical.removecats # name: # type: sq_string # elements: 1 # length: 777 categorical: B = removecats (A) categorical: B = removecats (A, oldcats) Remove categories from categorical array. B = removecats (A) removes all unused categories from categorical array A. The output categorical array B has the same size and values as A, but potentially fewer categories. B = removecats (A, oldcats) removes the categories specified by oldcats. The elements of B that correspond to the removed categories are undefined. oldcats can be a string array, a cell array of character vectors, or any type of array that can be converted to a cell array of character vectors with the cellstr function. Any names in oldcats that do not reference an existing category are ignored. When removing a single category, oldcats can also be specified as a character vector. # name: # type: sq_string # elements: 1 # length: 41 Remove categories from categorical array. # name: # type: sq_string # elements: 1 # length: 22 categorical.renamecats # name: # type: sq_string # elements: 1 # length: 1062 categorical: B = renamecats (A, newnames) categorical: B = renamecats (A, oldnames, newnames) Rename categories in categorical array. B = renamecats (A, newnames) renames all the categories in A, without changing any of its values, with the names specified in newnames. newnames can be specified as a string array, a cell array of character vectors, or any type of array that can be converted to a cell array of character vectors with the cellstr function, as long as it has the same number of elements as the categories in A. B = renamecats (A, oldnames, newnames) renames the categories of A specified in oldnames with the names specified in newnames. Both oldnames and newnames can be a string array, a cell array of character vectors, or any type of array that can be converted to a cell array of character vectors with the cellstr function, as long as they have the same number of elements. oldnames must specify a subset of existing categories in A. When renaming a single category, both oldnames and newnames can also be specified as character vectors. # name: # type: sq_string # elements: 1 # length: 39 Rename categories in categorical array. # name: # type: sq_string # elements: 1 # length: 23 categorical.reordercats # name: # type: sq_string # elements: 1 # length: 590 categorical: B = reordercats (A) categorical: B = reordercats (A, neworder) Reorder categories in categorical array. B = reordercats (A) reorders the categories of A in alphanumeric order. B = reordercats (A, neworder) reorders the categories of A according to the order specified by neworder, which must define a permutation of categories (A). neworder can be a numeric vector, a string array, a cell array of character vectors, or any type of array that can be converted to a cell array of character vectors with the cellstr function as long as it indexes all existing categories in A. # name: # type: sq_string # elements: 1 # length: 40 Reorder categories in categorical array. # name: # type: sq_string # elements: 1 # length: 19 categorical.repelem # name: # type: sq_string # elements: 1 # length: 937 categorical: B = repelem (A, n) categorical: B = repelem (A, d1, ..., dN) Repeat copies of categorical array elements. B = repelem (A, n) returns a categorical vector B containing repeated elements of the input A, which must be a categorical vector. If n is a scalar, each element of A is repeated n times along the non-singleton dimension of A. If n is a vector, it must have the same elements as A, in which case it specifies the number of times to repeat each corresponding element of A. B = repelem (A, d1, ..., dN) returns an array B with each element of A repeated according to the the list of input arguments d1, ..., dN each corresponding to a different dimension 1:ndims (A) of the input array A. d1, ..., dN must be either scalars or vectors with the same length as the corresponding dimension of A containing non-negative integer values specifying the number of repetitions of each element along the corresponding dimension. # name: # type: sq_string # elements: 1 # length: 44 Repeat copies of categorical array elements. # name: # type: sq_string # elements: 1 # length: 20 categorical.repelems # name: # type: sq_string # elements: 1 # length: 459 categorical: B = repelems (A, R) Construct a vector of repeated elements from a categorical array. B = repelems (A, R) returns a categorical vector B containing repeated elements of the input A, which must be a categorical vector. R must be a 2xN matrix of integers. Entries in the first row of R correspond to the linear indexing of the elements in A to be repeated. The corresponding entries in the second row of R specify the repeat count of each element. # name: # type: sq_string # elements: 1 # length: 65 Construct a vector of repeated elements from a categorical array. # name: # type: sq_string # elements: 1 # length: 18 categorical.repmat # name: # type: sq_string # elements: 1 # length: 580 categorical: B = repmat (A, n) categorical: B = repmat (A, d1, ..., dN) categorical: B = repmat (A, dimvec) Repeat copies of a categorical array. B = repmat (A, n) returns a categorical array B containing n copies of the input categorical array A along every dimension of A. B = repmat (A, d1, ..., dN) returns an array B containing copies of A along the dimensions specified by the list of scalar integer values d1, ..., dN, which specify how many copies of A are made in each dimension. B = repmat (A, dimvec) is equivalent to the previous syntax with dimvec = [d1, ..., dN]. # name: # type: sq_string # elements: 1 # length: 37 Repeat copies of a categorical array. # name: # type: sq_string # elements: 1 # length: 19 categorical.reshape # name: # type: sq_string # elements: 1 # length: 727 categorical: B = reshape (A, d1, ..., dN) categorical: B = reshape (A, ..., [], ...) categorical: B = reshape (A, dimvec) Repeat copies of categorical array elements. B = reshape (A, d1, ..., dN) returns a categorical array B with specified dimensions d1, ..., dN, whose elements are taken columnwise from the categorical array A. The product of d1, ..., dN must equal the total number of elements in A. B = reshape (A, ..., [], ...) returns a categorical array B with one dimension unspecified which is calculated automatically so that the product of dimensions in B matches the total elements in A, which must be divisible the product of specified dimensions. An empty matrix ([]) is used to flag the unspecified dimension. # name: # type: sq_string # elements: 1 # length: 44 Repeat copies of categorical array elements. # name: # type: sq_string # elements: 1 # length: 19 categorical.setcats # name: # type: sq_string # elements: 1 # length: 885 categorical: B = setcats (A, newcats) Set categories in categorical array. B = setcats (A, newcats) sets categories in the categorical array B according to the elements of the input array A and the categories specified by newcats according to the following rules: Any element of A that corresponds to a category listed in newcats is copied to B with the same categorical value. Any categories of A not listed in newcats are not copied to B and the corresponding elements of B are undefined. New categories listed in newcats that are not present in A are added in B, but without any elements equal to these new categories. newcats can be a string array, a cell array of character vectors or any type of array that can be converted to a cell array of character vectors with the cellstr function. When setting a single new category, newcats can also be specified as a character vector. # name: # type: sq_string # elements: 1 # length: 36 Set categories in categorical array. # name: # type: sq_string # elements: 1 # length: 19 categorical.setdiff # name: # type: sq_string # elements: 1 # length: 1497 categorical: C = setdiff (A, B) categorical: C = setdiff (A, B, 'rows') categorical: [C, ixA] = setdiff (...) categorical: ... = setdiff (..., order) Set difference of two categorical arrays. C = setdiff (A, B) returns the unique common values of the categorical arrays A and B. Either A or B input arguments may be a character vector, a string array, or a cell array of character vectors, which is promoted to a categorical array prior to set difference. If both A and B are row vectors, then C is also a row vector, otherwise intersect returns a column vector. If categorical arrays A and B are ordinal, they must have the same set and ordering of categories, which is transfered to C. If neither are ordinal, the category names of each pair of elements are compared (they do not need to have the same set of categories) in which case the categories in C are the sorted union of the categories in A and B. C = setdiff (A, B, 'rows' returns the unique common rows of the categorical matrices A and B, which must have the same number of columns. By default, the rows in categorical matrix C are in sorted order. [C, ixA] = setdiff (...) also returns the index vector ixA such that C = A(ixA), unless the 'rows' optional argument is given, in which case C = A(ixA,:). ... = setdiff (..., order) also specifies the order of the returned unique values. order may be either 'sorted', which is the default behavior, or 'stable', in which case the unique values are returned in order of appearance. # name: # type: sq_string # elements: 1 # length: 41 Set difference of two categorical arrays. # name: # type: sq_string # elements: 1 # length: 18 categorical.setxor # name: # type: sq_string # elements: 1 # length: 1537 categorical: C = setxor (A, B) categorical: C = setxor (A, B, 'rows') categorical: [C, ixA, ixB] = setxor (...) categorical: ... = setxor (..., order) Set exclusive-or of two categorical arrays. C = setxor (A, B) returns the unique common values of the categorical arrays A and B. Either A or B input arguments may be a character vector, a string array, or a cell array of character vectors, which is promoted to a categorical array prior to set exclusive-or. If both A and B are row vectors, then C is also a row vector, otherwise setxor returns a column vector. If categorical arrays A and B are ordinal, they must have the same set and ordering of categories, which is transfered to C. If neither are ordinal, the category names of each pair of elements are compared (they do not need to have the same set of categories) in which case the categories in C are the sorted union of the categories in A and B. C = setxor (A, B, 'rows' returns the unique common rows of the categorical matrices A and B, which must have the same number of columns. By default, the rows in categorical matrix C are in sorted order. [C, ixA, ixB] = setxor (...) also returns index vectors ixA and ixB such that C = A(ixA) and C = B(ixB), unless the 'rows' optional argument is given, in which case C = A(ixA,:) and C = B(ixB,:). ... = setxor (..., order) also specifies the order of the returned unique values. order may be either 'sorted', which is the default behavior, or 'stable', in which case the unique values are returned in order of appearance. # name: # type: sq_string # elements: 1 # length: 43 Set exclusive-or of two categorical arrays. # name: # type: sq_string # elements: 1 # length: 18 categorical.single # name: # type: sq_string # elements: 1 # length: 209 categorical: out = single (C) Convert categorical array to a single array. out = single (C) returns a single array indexing the categories in C. Categorical elements of undefined category are returned as NaN. # name: # type: sq_string # elements: 1 # length: 44 Convert categorical array to a single array. # name: # type: sq_string # elements: 1 # length: 16 categorical.size # name: # type: sq_string # elements: 1 # length: 1040 categorical: sz = size (C) categorical: dim_sz = size (C, dim) categorical: dim_sz = size (C, d1, d2, ...) categorical: [rows, columns, ..., dim_n_sz] = size (...) Size of a categorical array. sz = size (C) returns a row vector with the size (number of elements) of each dimension for the categorical array C. dim_sz = size (C, dim) returns the size of the corresponding dimension specified in dim. If dim is a vector, then dim_sz is a vector of the same length and with each element corresponding to a specified dimension. Multiple dimensions may also be specified as separate arguments. With a single output argument, size returns a row vector. With several, the size of dimension N is returned in the Nth argument. Asking for fewer output arguments than the array has dimensions folds the trailing dimensions into the last one, so [r, c] = size (C) on a 2-by-2-by-2 array returns 2 and 4; asking for more pads with ones. When a dimension is named, however, the number of output arguments must equal the number of dimensions requested. # name: # type: sq_string # elements: 1 # length: 28 Size of a categorical array. # name: # type: sq_string # elements: 1 # length: 16 categorical.sort # name: # type: sq_string # elements: 1 # length: 1487 categorical: B = sort (A) categorical: B = sort (A, dim) categorical: B = sort (A, direction) categorical: B = sort (A, dim, direction) categorical: B = sort (..., 'MissingPlacement', MP) categorical: [B, index] = sort (A, ...) Sort elements in a categorical array. B = sort (A) sorts the categorical array A in ascending order. The sorted array B has the same categories as A. If A is a matrix, sort (A) sorts each column of A in ascending order. For multidimensional arrays, mode (A) sorts along the first non-singleton dimension. B = sort (A, dim) sorts along the dimension specified by dim. B = sort (A, direction) also specifies the sorting direction, which can be either 'ascend' (default) or 'descend'. B = sort (..., 'MissingPlacement', MP) specifies where to place the missing elements () returned in B with any of the following options specified in MP: 'auto', which is the default, places missing elements last for ascending sort and first for descending sort. 'first' places missing elements first. 'last' places missing elements last. [B, index] = sort (A, ...) also returns a sorting index containing the original indices of the elements in the sorted array. If A is a vector, then index contains the original linear indices of the elements in the sorted vector B such that B = A(index). If A is an MxN matrix and dim = 1, then index contains the original row indices of the elements in the sorted vector B such that for j = 1:N, B(:,j) = A(index(:,j),j). # name: # type: sq_string # elements: 1 # length: 37 Sort elements in a categorical array. # name: # type: sq_string # elements: 1 # length: 20 categorical.sortrows # name: # type: sq_string # elements: 1 # length: 1976 categorical: B = sortrows (A) categorical: B = sortrows (A, col) categorical: B = sortrows (A, direction) categorical: B = sortrows (A, col, direction) categorical: B = sortrows (..., 'MissingPlacement', MP) categorical: [B, index] = sortrows (A, ...) Sort rows in a categorical array. B = sortrows (A) sorts the rows of the 2-D categorical array A in ascending order. The sorted array B has the same categories as A. B = sortrows (A, col) sorts A according to to the columns specified by the numeric vector col, which must explicitly contain non-zero integers whose absolute values index existing columns in A. Positive elements sort the corresponding columns in ascending order, while negative elements sort the corresponding columns in descending order. B = sortrows (A, direction) also specifies the sorting direction, which can be either 'ascend' (default) or 'descend' applying to all columns in A. Alternatively, direction can be a cell array of character vectors specifying the sorting direction for each individual column of A, in which case the number of elements in direction must equal the number of columns in A. B = sortrows (A, col, direction) sorts the categorical array A according to the columns specified in col using the corresponding sorting direction specified in direction. In this case, the sign of the values in col is ignored. col and direction must have the same length, but not necessarily the same number of elements as the columns in A. B = sortrows (..., 'MissingPlacement', MP) specifies where to place the missing elements () returned in B with any of the following options specified in MP: 'auto', which is the default, places missing elements last for ascending sort and first for descending sort. 'first' places missing elements first. 'last' places missing elements last. [B, index] = sortrows (A, ...) also returns an index vector containing the original row indices of A in the sorted matrix B such that B = A(index,:). # name: # type: sq_string # elements: 1 # length: 33 Sort rows in a categorical array. # name: # type: sq_string # elements: 1 # length: 19 categorical.summary # name: # type: sq_string # elements: 1 # length: 351 categorical: summary (C) categorical: summary (C, dim) categorical: summary (..., Name, Value) categorical: s = summary (...) Display summary of categorical array. summary (C) displays the number of elements in the categorical array C that are equal to each category of C. Any undefined elements in C are summed together and displayed as . # name: # type: sq_string # elements: 1 # length: 37 Display summary of categorical array. # name: # type: sq_string # elements: 1 # length: 17 categorical.times # name: # type: sq_string # elements: 1 # length: 415 categorical: C = times (A, B) Combine categorical arrays. C = times (A, B) is the equivalent of the syntax C = A .* B and returns a categorical array whose categories are the Cartesian product of the categories in A and B and each element is indexed to a new category which is the combination of the categories of the corresponding elements in A and B. A and B must be of common size or scalar categorical arrays. # name: # type: sq_string # elements: 1 # length: 27 Combine categorical arrays. # name: # type: sq_string # elements: 1 # length: 20 categorical.topkrows # name: # type: sq_string # elements: 1 # length: 2551 categorical: B = topkrows (A, K) categorical: B = topkrows (A, K, col) categorical: B = topkrows (A, K, direction) categorical: B = topkrows (A, K, col, direction) Top K sorted rows of categorical array. B = topkrows (A, K) returns the top K rows of the 2-D categorical array A sorted in descending order as a group. K must be a nonnegative integer scalar. If K is larger than the number of rows in A, then all of them are returned. B keeps the categories of A, including their order. Missing elements () are not ranked. Within each sort column they are placed after the elements that are defined, whichever direction is asked for, so a row is demoted only by a missing element in a column that actually decides its position. Rows comparing as equal keep their original order. B = topkrows (A, K, col) returns the top K rows of the 2-D categorical array A sorted according to the columns specified by the numeric vector col, which must contain positive integers indexing existing columns in A. Columns are used as sort keys in the order given, and those not listed are not used at all. The direction is descending unless direction says otherwise. B = topkrows (A, K, direction) returns the top K rows of the 2-D categorical array A sorted according to direction, which can be either 'descend' (default) or 'ascend' applying to all columns in A. Alternatively, direction can be a cell array of character vectors specifying the sorting direction for each individual column of A, in which case the number of elements in direction must equal the number of columns in A. B = topkrows (A, K, col, direction) returns the top K rows of the 2-D categorical array A sorted according to the columns specified in col using the corresponding sorting direction specified in direction. col and direction must have the same length, but not necessarily the same number of elements as the columns in A. B = topkrows (..., 'MissingPlacement', MP) specifies where the missing elements () are placed within each sort column, with any of the following options specified in MP: 'last', which is the default, places missing elements last whichever direction is asked for. 'first' places missing elements first. 'auto' places missing elements last for an ascending sort and first for a descending one, as sortrows does. This is an Octave extension: MATLAB has no such option here and always ranks as 'last' does. [B, index] = topkrows (A, ...) also returns an index vector containing the original row indices of A in B, such that B = A(index,:). # name: # type: sq_string # elements: 1 # length: 39 Top K sorted rows of categorical array. # name: # type: sq_string # elements: 1 # length: 21 categorical.transpose # name: # type: sq_string # elements: 1 # length: 176 categorical: B = transpose (A) Transpose a categorical matrix. B = transpose (A) is the equivalent of the syntax B = A.' and returns the transpose of the categorical matrix A. # name: # type: sq_string # elements: 1 # length: 31 Transpose a categorical matrix. # name: # type: sq_string # elements: 1 # length: 18 categorical.uint16 # name: # type: sq_string # elements: 1 # length: 207 categorical: out = uint16 (C) Convert categorical array to a uint16 array. out = uint16 (C) returns a uint16 array indexing the categories in C. Categorical elements of undefined category are returned as 0. # name: # type: sq_string # elements: 1 # length: 44 Convert categorical array to a uint16 array. # name: # type: sq_string # elements: 1 # length: 18 categorical.uint32 # name: # type: sq_string # elements: 1 # length: 207 categorical: out = uint32 (C) Convert categorical array to a uint32 array. out = uint32 (C) returns a uint32 array indexing the categories in C. Categorical elements of undefined category are returned as 0. # name: # type: sq_string # elements: 1 # length: 44 Convert categorical array to a uint32 array. # name: # type: sq_string # elements: 1 # length: 18 categorical.uint64 # name: # type: sq_string # elements: 1 # length: 207 categorical: out = uint64 (C) Convert categorical array to a uint64 array. out = uint64 (C) returns a uint64 array indexing the categories in C. Categorical elements of undefined category are returned as 0. # name: # type: sq_string # elements: 1 # length: 44 Convert categorical array to a uint64 array. # name: # type: sq_string # elements: 1 # length: 17 categorical.uint8 # name: # type: sq_string # elements: 1 # length: 286 categorical: out = uint8 (C) Convert categorical array to a int8 array. out = uint8 (C) returns a uint8 array indexing the categories in C. Categorical elements of undefined category are returned as 0. Note that the returned category indices saturate to intmax ('uint8'), which is 255. # name: # type: sq_string # elements: 1 # length: 42 Convert categorical array to a int8 array. # name: # type: sq_string # elements: 1 # length: 17 categorical.union # name: # type: sq_string # elements: 1 # length: 1521 categorical: C = union (A, B) categorical: C = union (A, B, 'rows') categorical: [C, ixA, ixB] = union (...) categorical: ... = union (..., order) Set union of two categorical arrays. C = union (A, B) returns the unique common values of the categorical arrays A and B. Either A or B input arguments may be a character vector, a string array, or a cell array of character vectors, which is promoted to a categorical array prior to set exclusive-or. If both A and B are row vectors, then C is also a row vector, otherwise union returns a column vector. If categorical arrays A and B are ordinal, they must have the same set and ordering of categories, which is transfered to C. If neither are ordinal, the category names of each pair of elements are compared (they do not need to have the same set of categories) in which case the categories in C are the sorted union of the categories in A and B. C = union (A, B, 'rows' returns the unique common rows of the categorical matrices A and B, which must have the same number of columns. By default, the rows in categorical matrix C are in sorted order. [C, ixA, ixB] = union (...) also returns index vectors ixA and ixB such that C = A(ixA) and C = B(ixB), unless the 'rows' optional argument is given, in which case C = A(ixA,:) and C = B(ixB,:). ... = union (..., order) also specifies the order of the returned unique values. order may be either 'sorted', which is the default behavior, or 'stable', in which case the unique values are returned in order of appearance. # name: # type: sq_string # elements: 1 # length: 36 Set union of two categorical arrays. # name: # type: sq_string # elements: 1 # length: 18 categorical.unique # name: # type: sq_string # elements: 1 # length: 2010 categorical: B = unique (A) categorical: B = unique (A, setOrder) categorical: B = unique (A, occurrence) categorical: B = unique (A, setOrder, occurrence) categorical: B = unique (A, occurrence, setOrder) categorical: B = unique (A, ..., 'rows') categorical: [B, ixA, ixB] = unique (...) Unique values in a categorical array. B = unique (A) returns the unique values of the categorical array A in the categorical vector B sorted according to the order of categories in A. B retains the same categories as A. If A is a row vector, then B is also a row vector, otherwise unique returns a column vector. B = unique (A, setOrder) returns the unique values of the categorical array A in an order as specified by setOrder, which can be either of the following values: 'sorted' (default) returns the unique values sorted in ascending order. 'stable' returns the unique values according to their order of occurrence. B = unique (A, occurrence) returns the unique values of the categorical array A according to their order of occurrence. occurrence can be either of the following values: 'first' (default) returns the first occurrence of each unique value, i.e. the lowest possible indices are returned. 'last' returns the last occurrence of each unique value, i.e. the highest possible indices are returned. You can specify setOrder and occurrence arguments together. B = unique (A, ..., 'rows') returns the unique rows of A by treating each row as a single entity. The 'rows' option can be used alone or in any combination with the setOrder and occurrence arguments. 'rows' can be placed at any position in the function's argument list after the input array A. However, this syntax is only valid for 2-dimensional categorical arrays. [B, ixA, ixB] = unique (...) also returns index vectors ixA and ixB using any of the previous syntaxes. ixA and ixB map the arrays A and B to one another such that B = A(ixA) and A = B(ixB). When the 'rows' optional argument is specified, then B = A(ixA,:) and A = B(ixB,:). # name: # type: sq_string # elements: 1 # length: 37 Unique values in a categorical array. # name: # type: sq_string # elements: 1 # length: 19 categorical.vertcat # name: # type: sq_string # elements: 1 # length: 1190 categorical: C = vertcat (A, B, ...) Vertical concatenation of categorical arrays. C = vertcat (A, B, ... is the equivalent of the syntax B = [A; B; ...] and vertically concatenates the categorical arrays A, B, .... All input arrays must have the same size except along the first dimension. Any of the input arrays may also be character vectors, string arrays or cell arrays of character vectors of compatible size, whose texts name categories, or missing values, which are undefined. Empty arrays are ignored. A numeric or logical array is refused, since a number could name a category or its code. If any input array is an ordinal categorical array, then all inputs must be ordinal categorical arrays with the same set and ordering of categories. In this case, C is also an ordinal categorical array with the same set and ordering of categories. If none of the input arrays are ordinal, then they do not need to have the same set of categories. In this case, categorical array C contains the union of the categories from all input arrays. Protected categorical arrays can only be concatenated with other arrays that have the same set of categories but not necessarily in the same order. # name: # type: sq_string # elements: 1 # length: 45 Vertical concatenation of categorical arrays. # name: # type: sq_string # elements: 1 # length: 10 cell2table # name: # type: sq_string # elements: 1 # length: 1337 datatypes: tbl = cell2table (C) datatypes: tbl = cell2table (C, Name, Value) Convert a cell array to a table. tbl = cell2table (C) converts the 2-D cell array C to the table tbl, where the contents of each column of C becomes a variable in tbl. The contents of each column are concatenated into their common data type (i.e. if a column of C contains explicitly double numbers, then the corresponding variable in tbl is of the same type), otherwise they are added as a column of cells. A column holding function handles, dictionary, timeseries or tscollection objects always becomes a column of cells. tbl = cell2table (C, Name, Value) specifies optional parameters for creating the table tbl with the following Name-Value paired arguments. Name Value 'VariableNames' A cell array of character vectors or a string array defining the variable names of tbl. The names must be valid variable names and unique. 'RowNames' A cell array of character vectors or a string array defining the row names of tbl. The names must be unique but not necessarily valid variable names. 'DimensionNames' A cell array of character vectors or a string array defining the dimension names of tbl. The names must be unique and not in conflict with variable names. By default, dimension names are 'Row', 'Variables'. See also: array2table, struct2table, table # name: # type: sq_string # elements: 1 # length: 32 Convert a cell array to a table. # name: # type: sq_string # elements: 1 # length: 19 configureDictionary # name: # type: sq_string # elements: 1 # length: 948 datatypes: d = configureDictionary (keyType, valueType) Create a dictionary with key and value types and no entries. d = configureDictionary (keyType, valueType) returns a configured dictionary with no entries, whose keys are of class keyType and whose values are of class valueType, each given as a character vector or a string scalar. The names are matched in case, and 'char' gives 'string', since a character vector is stored as a string scalar. valueType may name a built-in type ('double', 'logical', 'cell', 'struct', 'function_handle', ...) or any class that is not abstract. keyType must also name a type that keyHash can hash: a built-in type or a class with a keyHash method, such as string, datetime, duration, calendarDuration and categorical. MATLAB accepts any class there, a table or a timetable among them, and refuses the key only when an entry is added. See also: dictionary, dictionary.types, dictionary.isConfigured, keyHash # name: # type: sq_string # elements: 1 # length: 60 Create a dictionary with key and value types and no entries. # name: # type: sq_string # elements: 1 # length: 21 convertCharsToStrings # name: # type: sq_string # elements: 1 # length: 687 datatypes: B = convertCharsToStrings (A) datatypes: [B1, ..., Bn] = convertCharsToStrings (A1, ..., An) Convert character arrays to a string arrays, where applicable. B = convertCharsToStrings (A) converts A to a string scalar, if A is a character vector or array, or to a string array, if A is a cell array of character vectors. Otherwise, A is returned unaltered. [B1, ..., Bn] = convertCharsToStrings (A1, ..., An) converts any of the input arguments that are either character arrays or cell arrays of character vectors to string scalars or string arrays, respectively. Otherwise, convertCharsToStrings returns the input arguments unaltered. See also: convertStringsToChars, string # name: # type: sq_string # elements: 1 # length: 62 Convert character arrays to a string arrays, where applicable. # name: # type: sq_string # elements: 1 # length: 21 convertStringsToChars # name: # type: sq_string # elements: 1 # length: 597 datatypes: B = convertStringsToChars (A) datatypes: [B1, ..., Bn] = convertStringsToChars (A1, ..., An) Convert string arrays to a character arrays, where applicable. B = convertStringsToChars (A) converts A to a character vector, if A is a string scalar, or to a cell array of character vectors, if A is a string array. Otherwise, A is returned unaltered. [B1, ..., Bn] = convertStringsToChars (A1, ..., An) converts any of the input arguments that are of string type to character vectors or to cell array of character vectors, or leaves them unaltered. See also: convertCharsToStrings, string # name: # type: sq_string # elements: 1 # length: 62 Convert string arrays to a character arrays, where applicable. # name: # type: sq_string # elements: 1 # length: 9 csv2table # name: # type: sq_string # elements: 1 # length: 8263 datatypes: tbl = csv2table (filename) datatypes: tbl = csv2table (filename, Name, Value) Load a CSV file into a table. tbl = csv2table (filename) creates a table tbl by reading the data in CSV file specified by filename, which can be either character vector or a string scalar. If the CSV file was saved by the table2csv method, then it reads the custom comment line in the first element of the CSV file and reconstructs the table as specified including variable types, variable names, and possibly any nested tables or structures it may contain. If no such special comment is found, then it treats the CSV file as simple columnar data and loads with the following default options. The first column is considered as RowNames and it is converted to cell array of character vectors and it is subsequently removed from the remaining data contained in the CSV file. To change the default behavior, you need to specify the ReadRowNames and RowNamesColumn paired arguments accordingly. The first line is treated as a header containing the variable names of the table. Any numeric values in the header line are converted to character vectors and all variable names are automatically modified to valid Octave variable names. To change the default behavior, you need to specify the ReadVariableNames, VariableNamingRule, and VariableNamesRow paired arguments accordingly. The data type of each column in the remaining data is automatically detected according to its contents. Consequently, text is converted to character vectors, datetime and duration strings are converted to datetime and duration arrays, respectively, and hexadecimal strings are converted to the smallest integer type that can represent all variable values. To change the default behavior, you need to specify the TextType, DatetimeType, DurationType, and HexType accordingly. tbl = csv2table (filename, Name, Value) specifies optional parameters for creating the table tbl with the following Name-Value paired arguments. Name Value 'NumHeaderLines' A positive integer scalar value specifying the number of rows to omit reading from the CSV file. If the CSV contains the custom comment line in its first element, then the 'NumHeaderLines' applies to the top number of rows to remove from the created table. If CSV is a generic case, then 'NumHeaderLines' specifies the number of lines to omit parsing from the CSV file itself. 'VariableNames' A cell array of character vectors or a string array specifying the names of the variables of the created table. The names must be unique but not necessarily valid variable names. If not empty, it overrides any variable names extracted from the CSV file. This applies both to custom and generic CSV files. 'ReadVariableNames' A logical scalar specifying whether to parse or not the CSV files for variable names. If your CSV file only contains data, set 'ReadVariableNames' to false so that csv2table parses all lines as data rows in the returned table. Variable names will be automatically generated to the default style as done by table, unless otherwise specified by the 'VariableNames' paired argument. The default value is true. 'VariableNamingRule' A character vector or a string scalar specifying whether the variable names should be modified to valid Octave variable names or the original names should be preserved. Valid options are "modify" and "preserve". By default, csv2table modifies the parsed variable names. 'VariableNamesRow' A nonnegative integer scalar value specifying the line number in the CSV file, which should be parsed for variable names. This only applies if the 'ReadVariableNames' option is true. The specified line is subsequently removed from the remaining data contained in the CSV file. If 'VariableNamesRow' is set to zero, then it is equivalent to setting 'ReadVariableNames' to false. 'VariableNamesLine' MATLAB's spelling of 'VariableNamesRow' for a text file, and an exact alias of it. Passing both names raises an error rather than one silently winning. 'VariableTypes' A cell array of character vectors or a string array specifying the data type of the variables of the created table. The number of elements must much the number of variable in the table. This optional argument only has an effect on generic CSV files. When specified, it overrides any other data type specification or automatic detection by the csv2table function. 'VariableUnitsLine' A nonnegative integer scalar value specifying the line number in the CSV file, which should be parsed for variable units. The specified line is subsequently removed from the remaining data contained in the CSV file. 'VariableDescriptionsLine' A nonnegative integer scalar value specifying the line number in the CSV file, which should be parsed for variable descriptions. The specified line is subsequently removed from the remaining data contained in the CSV file. 'ReadRowNames' A logical scalar specifying whether to parse or not the CSV files for row names. If your CSV file only contains data, set 'ReadRowNames' to false so that csv2table parses all columns as data columms in the returned table. The default value is true. 'RowNamesColumn' A nonnegative integer scalar value specifying the column number in the CSV file, which should be parsed for row names. This only applies if the 'ReadRowNames' option is true. The specified column is subsequently removed from the remaining data contained in the CSV file. If 'RowNamesColumn' is set to zero, then it is equivalent to setting 'ReadRowNames' to false. Note that the values in the column specified by 'RowNamesColumn' must be unique, otherwise csv2table will return an error. The default value is 0: a file that does not say which column holds its row names is read as data throughout, and a file written by table2csv records the column itself. A leading column headed Row, which is what writetable writes for the row names, is taken as the row names without being named here. 'TextType' A character vector or a string scalar specifying whether the text data in the CSV file should be stored in the table as character vectors or string arrays. Valid options are "char" and "string". By default, csv2table stores text data as character vectors. 'DatetimeType' A character vector or a string scalar specifying whether the datetime strings found in the CSV file should be stored in the table as datetime arrays or as text data. Valid options are "datetime" and "text". By default, csv2table stores datetime strings as datetime arrays. If "text" is specified, then the data type depends on the 'TextType' option. 'DurationType' A character vector or a string scalar specifying whether the duration strings found in the CSV file should be stored in the table as duration arrays or as text data. Valid options are "duration" and "text". By default, csv2table stores duration strings as duration arrays. If "text" is specified, then the data type depends on the 'TextType' option. 'HexType' A character vector or a string scalar specifying whether the hexadecimal text found in the CSV file should be stored as a suitable integer type, "auto", as unaltered input text, "text" (the default, in which case the data type depends on the 'TextType' option), or as any of the integer types supported by Octave. Valid options are "auto", "text", "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", and "uint64". Detection is off by default because MATLAB does not perform it, so a file carrying no header of ours is read the way MATLAB reads it. It never applies to a file written by table2csv, which declares every variable's type. A datetime or duration variable written by table2csv is restored exactly, along with its Format and, for a zone-aware datetime, its TimeZone. Where the column's type is declared, either by the file or by "VariableTypes", a field that is empty and unquoted carries no value and is read as a missing entry, where an empty quoted field ("") is read as an empty string; a column whose type is guessed is unaffected. The following round-trip limitation applies when reading a file written by table2csv: calendarDuration and categorical variables are returned as cell arrays of character vectors and their values are not reconstructed. See also: array2table, struct2table, table # name: # type: sq_string # elements: 1 # length: 29 Load a CSV file into a table. # name: # type: sq_string # elements: 1 # length: 13 csv2timetable # name: # type: sq_string # elements: 1 # length: 1575 datatypes: tt = csv2timetable (filename) datatypes: tt = csv2timetable (filename, Name, Value) Read a comma-separated-value (CSV) file into a timetable. tt = csv2timetable (filename) reads the CSV file named by filename into a timetable. filename may be a character vector, a cellstr, or a string scalar. A file written by timetable2csv carries the package's own header block, which names and types every variable and tags the leading column of row times with their type, their TimeZone where they have one, and their Format. Such a file comes back as the timetable it was written from: the row times keep their type, zone and format exactly, and the row dimension keeps its name. Any other CSV file is read as csv2table reads it, and its first datetime or duration variable becomes the row times, the row dimension taking that variable's name. A file with no such variable cannot be read as a timetable. Every Name-Value option of csv2table is accepted and behaves as it does there, with two exceptions. 'ReadRowNames' and 'RowNamesColumn' are refused: a timetable labels its rows by time and by nothing else, so a file whose rows are named is read with csv2table. TimeStep and SampleRate are not stored in the file and are worked out again from the row times, so a regular timetable comes back regular. A CSV file carries no event table, so 'Events' of the result is always empty even when the timetable that was written had one; use timetable2ods and ods2timetable to keep events. See also: timetable.timetable2csv, csv2table, ods2timetable, readtimetable, timetable # name: # type: sq_string # elements: 1 # length: 57 Read a comma-separated-value (CSV) file into a timetable. # name: # type: sq_string # elements: 1 # length: 8 datetime # name: # type: sq_string # elements: 1 # length: 1218 datatypes: datetime Array representing points in time using the Gregorian calendar. A datetime array stores internally the datetime points as double arrays representing whole years, months, days, hours, and minutes, as well as seconds including their fractional part. The underlying implementation relies on the 'date.h' C++ library. The precision of this implementation is set at microseconds, which is substantial for typical times. A datetime array is a collection of date/time elements, with each element holding a complete date/time. The datetime array also has TimeZone and Format properties associated with it, which apply to all elements in the array. When a display Format contains a time-zone name field (z/zz/zzz), the name is rendered according to a session-wide style set by datetime.zoneNameStyle. The default, 'iana', is an Octave-specific extension that shows the IANA abbreviation for every zone (e.g. EEST); 'matlab' restores MATLAB's behaviour of naming only North American zones and showing a numeric UTC+3-style offset elsewhere. A single Format may override the session style with zzzz (force 'iana') or zzzzz (force 'matlab'). See datetime.zoneNameStyle. See also: calendarDuration, duration # name: # type: sq_string # elements: 1 # length: 63 Array representing points in time using the Gregorian calendar. # name: # type: sq_string # elements: 1 # length: 15 datetime.Format # name: # type: sq_string # elements: 1 # length: 1290 datetime: property Format Display format Display format, specified as a character vector or string scalar. If specified as a string scalar, it is converted and stored internally as a character vector. The value 'default' is a data-dependent sentinel: a date-only format ('dd-MMM-uuuu') is used when every element sits at midnight, and a date and time format ('dd-MMM-uuuu HH:mm:ss') otherwise. The value 'defaultdate' always selects the date-only format. Reading the property returns the resolved pattern, never the sentinel itself. A custom pattern is built from the following Unicode (LDML) fields; repeating a letter widens or names the field. Any other text is copied verbatim, and text in single quotes is always literal. y, u Year; yy uses the last two digits. M Month: number (M, MM), abbreviated (MMM), full (MMMM), or initial (MMMMM). d Day of the month. D Day of the year. e Day of the week: number (Sunday is 1), abbreviated (eee), full (eeee), or initial (eeeee). H, h Hour, 24-hour and 12-hour clock. m Minute. s Whole second (fractional seconds are truncated). S Fractional second, one digit per S. a AM or PM. Q Quarter of the year. G Era. W Week of the month. z Time-zone name (style set by datetime.zoneNameStyle; zzzz/zzzzz force it). Z, X, x Numeric time-zone offset. # name: # type: sq_string # elements: 1 # length: 14 Display format # name: # type: sq_string # elements: 1 # length: 23 datetime.SystemTimeZone # name: # type: sq_string # elements: 1 # length: 406 datetime: tz = datetime.SystemTimeZone () System time zone setting. tz = datetime.SystemTimeZone () returns the time zone of the system where Octave is running, as the name of a zone in the IANA Time Zone Database, such as 'Europe/Athens'. It is 'UTC' when the system time zone cannot be determined. The parentheses may be omitted, and the same value is read from any datetime array d as d.SystemTimeZone. # name: # type: sq_string # elements: 1 # length: 25 System time zone setting. # name: # type: sq_string # elements: 1 # length: 17 datetime.TimeZone # name: # type: sq_string # elements: 1 # length: 1927 datetime: property TimeZone Time zone Time zone, specified as a character vector or string scalar. If specified as a string scalar, it is converted and stored internally as a character vector. Setting it to 'local', in any letter case, stores the system time zone given by datetime.SystemTimeZone, exactly as setting that zone by name would store it, so a system zone of 'Etc/UTC' is stored as '+00:00'. A fixed offset from UTC, which never observes daylight saving time, is stored as '+HH:MM' or '-HH:MM', or as '+HH:MM:SS' when the offset has seconds, and may be up to 23 hours and 59 minutes either way. It may also be given as '+H', '+HH', '+HMM', '+HHMM' or '+HHMMSS', as 'Z' for a zero offset, or with 'UTC' or 'GMT' in front, in any letter case, as 'UTC+H', 'UTC+HH', 'UTC+H:MM', 'UTC+HH:MM' or 'UTC+HH:MM:SS'. The database's own fixed-offset zones, such as 'Etc/GMT-3' and 'Etc/UTC', are stored as offsets as well. A 'GMT' prefix and the names 'GMT0', 'Zulu', 'UCT', 'Universal' and 'Greenwich' raise a warning. Unlike the database names, whose sign is reversed, '+03:00', 'GMT+3' and 'Etc/GMT-3' are all three hours ahead of UTC. Besides the zones of the IANA Time Zone Database, the value 'UTCLeapSeconds' selects UTC with its inserted leap seconds made representable, so that the 60th second of a minute exists on the 27 dates that have one. It is not an IANA zone and timezones does not list it. An array in that zone counts elapsed SI seconds, which is what makes its arithmetic differ from a UTC array's across an inserted second, and for that reason it cannot be combined or compared with an array that does not have leap seconds. Its Format is fixed to "uuuu-MM-dd'T'HH:mm:ss'Z'", optionally with one to nine fractional-second digits, since no other pattern can write a 60th second; moving such an array to any other zone folds an inserted second back onto the 59th and restores the ordinary default format. # name: # type: sq_string # elements: 1 # length: 9 Time zone # name: # type: sq_string # elements: 1 # length: 16 datetime.between # name: # type: sq_string # elements: 1 # length: 703 datetime: D = between (A, B) datetime: D = between (A, B, components) Calendar difference between two datetime arrays. D = between (A, B) returns a calendarDuration array holding the calendar difference from each element of A to the corresponding element of B. The result is signed (it is negative where B precedes A) and is expressed in whole calendar units plus a leftover time, taking month lengths and daylight saving time into account. A and B must be the same size or one of them must be scalar. Not-A-Time elements yield NaN. D = between (A, B, components) expresses each difference using only the requested calendar components (see caldiff). The default is {'Years', 'Months', 'Days', 'Time'}. # name: # type: sq_string # elements: 1 # length: 48 Calendar difference between two datetime arrays. # name: # type: sq_string # elements: 1 # length: 16 datetime.caldiff # name: # type: sq_string # elements: 1 # length: 895 datetime: D = caldiff (A) datetime: D = caldiff (A, components) datetime: D = caldiff (A, components, dim) Calendar differences between successive datetime elements. D = caldiff (A) returns a calendarDuration array holding the calendar difference between successive elements of A along its first non-singleton dimension. Unlike diff, the result is expressed in whole calendar units (years, months, days) plus a leftover time, so it is aware of month lengths and, for a zoned array, of daylight saving time. Not-A-Time elements yield NaN. D = caldiff (A, components) expresses each difference using only the requested calendar components. components is one of 'Years', 'Quarters', 'Months', 'Weeks', 'Days', or 'Time', or a cell array or string array containing several of them. The default is {'Years', 'Months', 'Days', 'Time'}. D = caldiff (A, components, dim) operates along dimension dim. # name: # type: sq_string # elements: 1 # length: 58 Calendar differences between successive datetime elements. # name: # type: sq_string # elements: 1 # length: 12 datetime.cat # name: # type: sq_string # elements: 1 # length: 1456 datetime: C = cat (dim, A, B, ...) Concatenate datetime arrays. C = cat (dim, A, B, ...) concatenates the inputs A, B, ... along dimension dim. They must have the same size except along the operating dimension dim. At least one input must be a datetime array. Date/time text, whether a character vector, a cellstr, or a string array, is converted; a 0x0 empty [] or {} contributes nothing and is dropped, so an array may be accumulated from an empty start. Any other input, an empty numeric array of non-zero size included, is an error. The first datetime input gives the result its Format and TimeZone, and is what date/time text is read against: such text names a wall clock in that time zone rather than an instant to be converted into it. Text may therefore appear first, as in ['2024-01-01', T], and still be read in T's zone. Zoned inputs need not share a time zone; each is converted into the zone of the first, which preserves the instant every element names and changes only its wall-clock reading. A zoned input cannot be concatenated with an unzoned one, in either order: a wall clock that names no instant and one that does do not belong in the same array. A bare NaT and datetime.empty are unzoned and so take part in that rule, whereas NaT ('TimeZone', tz) does not. Date/time text that is empty names no date and becomes NaT, so an empty character vector adds one missing element, whereas an empty {} holds no text at all and adds none. # name: # type: sq_string # elements: 1 # length: 28 Concatenate datetime arrays. # name: # type: sq_string # elements: 1 # length: 16 datetime.cellstr # name: # type: sq_string # elements: 1 # length: 247 datetime: cstr = cellstr (T) datetime: cstr = cellstr (T, Format) Convert datetime array to a cell array of character vectors. cstr = cellstr (T) returns a cellstr array of character vectors, cstr, which has the same size as the input datetime T. # name: # type: sq_string # elements: 1 # length: 60 Convert datetime array to a cell array of character vectors. # name: # type: sq_string # elements: 1 # length: 13 datetime.char # name: # type: sq_string # elements: 1 # length: 180 datetime: cmat = char (T) datetime: cmat = char (T, Format) Convert datetime array to a character matrix. cmat = char (T) returns a character matrix with one row per element in T. # name: # type: sq_string # elements: 1 # length: 45 Convert datetime array to a character matrix. # name: # type: sq_string # elements: 1 # length: 18 datetime.circshift # name: # type: sq_string # elements: 1 # length: 724 datetime: B = circshift (A, n) datetime: B = circshift (A, n, dim) Circularly shift the elements in a datetime array. B = circshift (A, n) circularly shifts the elements of the datetime array A according to n. If n is a nonzero integer scalar, then the elements of A are shifted by n elements along the first non-singleton dimension of A. If n is a vector, it must not be longer that the number of dimensions of A with each value of n corresponding to a dimension in A. The sign of the value(s) in n specify the direction in the elements of A are shifted. B = circshift (A, n, dim) circularly shifts the elements of the datetime array A along the dimension specified by dim. In this case, n must be a scalar integer value. # name: # type: sq_string # elements: 1 # length: 50 Circularly shift the elements in a datetime array. # name: # type: sq_string # elements: 1 # length: 14 datetime.colon # name: # type: sq_string # elements: 1 # length: 1254 datetime: R = colon (A, B) datetime: R = colon (A, step, B) Create a range of datetime values. R = colon (A, B) is the equivalent of the syntax R = A:B and returns a row vector of datetime values starting at A and increasing in steps of one calendar day up to, and possibly including, B. R = colon (A, step, B) is the equivalent of the syntax R = A:step:B and uses the specified step between consecutive elements. step may be: a duration or a numeric scalar (a number of fixed 24-hour days), in which case successive elements advance by a fixed amount of elapsed time; for a zoned range this is aware of daylight saving time. a calendarDuration, in which case successive elements advance in calendar units. Each element is computed as A + k*step for k = 0, 1, 2, ..., so month and year steps clamp the day of month independently for every element (e.g. a one-month step from 31 January yields 31 January, 28 February, 31 March, ...). The default step of A:B is one calendar day (caldays (1)), which preserves the time of day across daylight saving time changes. A range whose step points away from B (for example an increasing step with A > B) is empty. A and B must be datetime scalars that are either both zoned or both unzoned, and must be finite. # name: # type: sq_string # elements: 1 # length: 34 Create a range of datetime values. # name: # type: sq_string # elements: 1 # length: 18 datetime.convertTo # name: # type: sq_string # elements: 1 # length: 3254 datetime: X = convertTo (T, dateType) datetime: X = convertTo (T, 'epochtime', Name, Value) Convert datetime array to a numeric representation. X = convertTo (T, dateType) converts the datetime array T to the numeric date/time representation named by dateType, returning an array X of the same size as T. It is a convenience wrapper around the individual conversion methods. dateType may be one of: 'datenum' -- serial date number, double (see datenum). 'excel' or 'excel1900' -- Excel serial date, 1900 system, double (see exceltime). 'excel1904' -- Excel serial date, 1904 system, double. 'juliandate' -- Julian date, double (see juliandate). 'modifiedjuliandate' -- modified Julian date, double. 'posixtime' -- seconds since the Unix epoch, double (see posixtime). 'yyyymmdd' -- YYYYMMDD numeric date, double (see yyyymmdd). 'epochtime' -- ticks since an epoch, int64 (see below). 'ntp' -- NTP timestamp, uint64, valid from 1900-01-01 up to 2036-02-07. 'ntfs' -- NTFS/FILETIME 100-ns ticks since 1601-01-01, uint64. '.net' -- .NET 100-ns ticks since 0001-01-01, uint64. 'tt2000' -- CDF TT2000 nanoseconds since the J2000 Terrestrial Time epoch, int64 (see below). For the double conversions, Not-A-Time (NaT) values are returned as NaN. The integer conversions cannot represent NaN, so a NaT value, an infinite datetime, or a datetime outside the target format's representable range raises an error. X = convertTo (T, 'epochtime', Name, Value) accepts the options 'Epoch' (a scalar datetime marking tick zero; default 1970-01-01) and 'TicksPerSecond' (a positive scalar; default 1). The epoch and T must both be zoned or both be unzoned. X = convertTo (T, 'tt2000') returns the number of nanoseconds since the J2000 Terrestrial Time epoch, 2000-01-01T11:58:55.816Z, as an int64 array. Because that count includes leap seconds, T must be in the 'UTCLeapSeconds' time zone. The inverse is datetime (X, 'ConvertFrom', 'tt2000', 'TimeZone', 'UTCLeapSeconds'), which likewise requires both the int64 type and that zone. Each conversion treats an inserted leap second the way its own format does. posixtime folds it forward onto the following second, while datenum, exceltime, 'epochtime', 'ntp', 'ntfs' and '.net' fold it backward onto the preceding one; juliandate stretches the day that holds it (see juliandate), and 'tt2000' counts it. Deviations from MATLAB. Two results differ deliberately for a 'UTCLeapSeconds' array, because MATLAB's own are inconsistent. 'epochtime' with an 'Epoch': MATLAB folds the array onto the POSIX timeline but measures the epoch on the leap-second one, so its tick count is short by the number of seconds inserted before that epoch. Counting from 2016-12-31 it returns 86373 for 2016-12-31T23:59:59Z, 26 short. Both operands are folded the same way here, giving 86399. Only the offset differs: a difference between two such counts is the same in either. 'tt2000' between 1960 and 1972: before 1972 UTC did not tick with atomic time, and its offset from it is tabulated by the IERS as a base value plus a rate per day. That table is followed here; MATLAB evaluates the rate half a day from where the table places it, leaving its results up to 1.3 milliseconds off. Outside those twelve years the two agree exactly. # name: # type: sq_string # elements: 1 # length: 51 Convert datetime array to a numeric representation. # name: # type: sq_string # elements: 1 # length: 19 datetime.ctranspose # name: # type: sq_string # elements: 1 # length: 227 datetime: B = ctranspose (A) Transpose a datetime matrix. B = ctranspose (A) is the equivalent of the syntax B = A' and returns the transpose of the datetime matrix A. For datetime arrays, ctranspose is identical to transpose. # name: # type: sq_string # elements: 1 # length: 28 Transpose a datetime matrix. # name: # type: sq_string # elements: 1 # length: 16 datetime.datenum # name: # type: sq_string # elements: 1 # length: 672 datetime: N = datenum (T) Convert datetime array to serial date numbers. N = datenum (T) returns a double array N of the same size as T holding the serial date number of each element, where 1 corresponds to January 1 of the year 0000 and the fractional part represents the time of day. The time zone of a zoned T is ignored; its wall-clock components are used. Not-A-Time (NaT) values are returned as NaN, and infinite datetimes preserve their sign. A serial date number has no room for an inserted leap second, so for a 'UTCLeapSeconds' array the 60th second of a minute folds backward onto the 59th: datenum of 2016-12-31T23:59:60Z equals that of 2016-12-31T23:59:59Z. # name: # type: sq_string # elements: 1 # length: 46 Convert datetime array to serial date numbers. # name: # type: sq_string # elements: 1 # length: 18 datetime.dateshift # name: # type: sq_string # elements: 1 # length: 1329 datetime: R = dateshift (A, 'start', unit) datetime: R = dateshift (A, 'end', unit) datetime: R = dateshift (..., rule) datetime: R = dateshift (A, 'dayofweek', dow) datetime: R = dateshift (A, 'dayofweek', dow, rule) Shift datetime values to calendar boundaries. R = dateshift (A, 'start', unit) returns a datetime array in which each element of A is moved back to the start of the calendar unit that contains it, with the finer components set to zero. unit is 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', or 'year'. A week starts on Sunday. R = dateshift (A, 'end', unit) moves each element to the end of its unit: the start of the next second, minute, hour, or day, and the last day (at midnight) of the week, month, quarter, or year. R = dateshift (..., rule) first shifts each element by rule whole units. rule is 'current' (the default), 'next', 'previous', 'nearest', or an integer number of units. R = dateshift (A, 'dayofweek', dow) moves each element to the next date, on or after it, whose day of the week is dow (a number from 1 for Sunday to 7 for Saturday, or a day name), keeping the time of day. A trailing rule of 'previous', 'nearest', 'current' (the day within the current week), or an integer occurrence selects a different date. Not-A-Time and infinite elements are returned unchanged. # name: # type: sq_string # elements: 1 # length: 45 Shift datetime values to calendar boundaries. # name: # type: sq_string # elements: 1 # length: 16 datetime.datestr # name: # type: sq_string # elements: 1 # length: 1014 datetime: S = datestr (T) datetime: S = datestr (T, f) datetime: S = datestr (..., opt) Convert datetime array to a character array of date strings. S = datestr (T) converts the datetime array T to a character array S with one date string per row, using the default format of the core datestr function. S = datestr (T, f) uses the format specified by f, a format number or a format string that follows the legacy datestr field codes, in which 'mm' denotes months and 'MM' denotes minutes. Any further arguments opt are passed on to the core datestr function. datestr is provided for compatibility with legacy code. It renders the wall-clock components of T and, for a zoned array, does not include the time zone. For time-zone-aware formatting with the modern Unicode (LDML) field codes, use char, cellstr, or set the Format property of T instead. The legacy field codes have no way to write a 60th second, so for a 'UTCLeapSeconds' array an inserted second folds backward onto the 59th, as it does in datenum. # name: # type: sq_string # elements: 1 # length: 60 Convert datetime array to a character array of date strings. # name: # type: sq_string # elements: 1 # length: 17 datetime.datetime # name: # type: sq_string # elements: 1 # length: 19322 datetime: T = datetime ('now') datetime: T = datetime ('today') datetime: T = datetime ('tomorrow') datetime: T = datetime ('yesterday') datetime: T = datetime (DateStrings) datetime: T = datetime (DateStrings, 'MixedFormats', TF) datetime: T = datetime (DateStrings, 'InputFormat', INFMT) datetime: T = datetime (DateStrings,'InputFormat', INFMT, 'PivotYear', PIVOT) datetime: T = datetime (DateStrings,'InputFormat', INFMT, 'Locale', LOCALE) datetime: T = datetime (DateVectors) datetime: T = datetime (Y, MO, D) datetime: T = datetime (Y, MO, D, H, MI, S) datetime: T = datetime (Y, MO, D, H, MI, S, MS) datetime: T = datetime (X, 'ConvertFrom', TYPE) datetime: T = datetime (D) datetime: T = datetime (..., 'Format', FMT) datetime: T = datetime (..., 'TimeZone', TZ) Create a new array of datetime values. T = datetime ('now') returns a scalar datetime array corresponding to the current date and time. T = datetime ('now') is the same as T = datetime (). Except for 'now', the same syntax can be used with 'today', 'tomorrow' and 'yesterday'. These options return the respective date but with time set at midnight. T = datetime (DateStrings) creates a datetime array from the text in DateStrings representing points in time. Without an 'InputFormat', one format is detected from the first piece of text that is not blank, and every other piece is then read with that same format: text written in a different one is not given a format of its own but becomes NaT, so that a column of dates is read as the single thing it is meant to be. Text no format can be detected from at all is refused outright, and so is text whose first piece that is not blank the detected format cannot read, whatever follows it. A character matrix is no exception, where MATLAB returns only NaT for one whose first row is blank, even when a later row can be read. A later date that the detected format cannot make sense of, such as '2024-04-31', is lost in the same way an explicit 'InputFormat' loses it. The formats detected are: 'uuuu-MM-dd' optionally followed by 'HH:mm', 'HH:mm:ss', or either with fractional seconds, the date and the time separated by a space or by 'T'. The time may carry a UTC offset, 'Z', '+HH', '+HHMM' or '+HH:MM', which is read as with an offset field and so requires 'TimeZone'. MATLAB detects no text carrying an offset. 'dd-MMM-uuuu' month named in full or abbreviated, optionally followed by a time as above. 'MMMM d, uuuu' as in 'March 15, 2024', in English only, optionally followed by a time. 'MM/dd/uuuu' or 'dd/MM/uuuu', the year in two digits or four, optionally followed by a time; see below. 'dd.MM.uuuu' day first in every locale, optionally followed by a time, or 'dd.MM.yy' with all three fields in two digits, so that a version number such as '1.5.10' stays text. 'HH:mm:ss' a time alone, taking today's date. A two-digit year is read against the pivot year, as with an 'InputFormat', where MATLAB reads '05.03.24' as the year 24. Three further shapes are read that MATLAB does not detect, and are Octave extensions: 'uuuu/MM/dd' with an optional time, 'dd MMMM uuuu' as in '15 March 2024', and a year alone. Numeric fields need not be padded, so '2024-2-9' reads as '2024-02-09' does. A date written with slashes is ambiguous: '03/09/2024' is the 3rd of September to some readers and the 9th of March to others. The whole array decides, since one entry naming a day past the twelfth settles the order for all of them; where nothing settles it the reading of the 'Locale' is taken, month first in English but for 'en_GB' and day first in every other language, and a warning ('Octave:datetime:ambiguous-format') is raised, but for 'pt_PT'. Under a German locale, which writes dates with dots, slashes are not read, as in MATLAB. T = datetime (DateStrings, 'Locale', LOCALE) detects the format in the language of LOCALE: the month names above are its own, abbreviated or full, matched in any case and with or without the point after an abbreviation, and English names are not read under another language. Each language adds its long form, optionally followed by a time: 'd. MMMM uuuu' in German ('1. Januar 2024', 'Jänner' under 'de_AT'), 'd 'de' MMMM 'de' uuuu' in Spanish and Portuguese ('5 de marzo de 2024'), and 'dd MMMM uuuu' in the other languages but Portuguese ('15 mars 2024', '15 Μα�τίου 2024'). MATLAB reads the Spanish and Portuguese long form under 'pt_BR' alone, refuses a point after a Spanish abbreviation, and refuses Greek written in capitals; all of these are read here. T = datetime (DateStrings, 'MixedFormats', true) detects a format for each piece of text separately instead, so text gathered from several sources into one column is read whatever each entry happens to be written in. This is an Octave extension, off by default; MATLAB has no equivalent. It reads the wider set of formats Octave's core datevec accepts, and rolls an impossible date over rather than refusing it, so '2024-04-31' is read as the 1st of May. It cannot tell a mistake from a format it has not seen before, which is why it is not the default. T = datetime (DateStrings, 'InputFormat', INFMT) also allows to specify a particular input text format to parse DateStrings. It is always preferable to specify the format INFMT if it is known. INFMT uses the Unicode LDML date field symbols ('y', 'M', 'd', 'H', 'h', 'm', 's', 'S', 'a', ...), the same set used for display formats; text between single quotes is treated as a literal. Formats which do not specify a particular time component will have the value set to zero. Formats which do not specify any date component default to the current date, whereas a partially specified date defaults its missing month and day to 1. A string that does not match INFMT, or that names a date which does not exist, such as '2024-04-31' or a 29th of February outside a leap year, cannot be converted. A lone such string is an error; within an array only that element is lost and becomes NaT, so that one unreadable entry does not cost the rest of the array. An array none of whose text can be read is an error too, unless it holds a blank element; a character matrix none of whose rows can be read is always an error. An INFMT with a time zone offset field, 'z', 'Z', 'X' or 'x', reads each string as an instant, its wall clock less the offset it carries, and expresses it in the zone given by 'TimeZone', which is then required. The offset settles a repeated or skipped wall clock, so such text is never rejected for naming one. Every width of 'Z', 'X' and 'x' reads the same forms: 'Z' for zero, a signed offset such as '-5', '-05', '-0500', '-05:00' or '-05:00:30', and either of those after 'GMT' or 'UTC', which alone is zero. 'z' reads the same, and a time zone abbreviation such as 'EST', 'CET' or 'MSK', in any letter case. An abbreviation with two meanings is read only under a LOCALE that settles it: 'BST' is +01:00 under 'en_GB' and +06:00 under 'en_CA', 'IST' is +01:00 under 'en_IE' and +05:30 under 'en_IN' and 'en_CA', 'ACST' is +09:30 under 'en_AU' and -04:00 under 'pt_BR', and 'AMT' is -04:00 under 'pt_BR'. Under any other LOCALE they are refused, where MATLAB reads 'BST' as +06:00 and 'IST' as +05:30 by default. T = datetime (DateStrings, 'InputFormat', INFMT, 'PivotYear', PIVOT) also allows to specify a pivot year, which refers to the year at the start of the century to which two-digit years will be referenced. When not specified, it defaults to the current year minus 50. T = datetime (DateStrings, 'InputFormat', INFMT, 'Locale', LOCALE) also interprets the month names, weekday names, and day-period markers in DateStrings according to LOCALE, given as an 'xx_YY' identifier whose language part selects the names. The supported languages are 'en' (the default), 'fr', 'de', 'es', 'it', 'pt', and 'el'; 'system' is treated as 'en'. Both full ('MMMM'/'eeee') and abbreviated ('MMM'/'eee') month and weekday names are recognized case-insensitively; for Greek, matching is also accent-insensitive, so the accentless all-caps spelling is accepted, and the genitive month forms ('Μα�τίου') are used. A weekday name is validated but does not otherwise affect the result. A hyphen may stand for the underscore, as in 'fr-FR'. T = datetime (DateVectors) creates a column vector of datetime values from the date vectors in DateVectors. T = datetime (Y, MO, D creates an array of datetime values for corresponding elements of the Y, MO, and D arrays, which must be of the same size or scalars, must contain integer values, and they correspond to years, months, and days, respectively. T = datetime (Y, MO, D, H, MI, S) also adds time components to the constructed datetime arrays. H, MI, and S must be of the same size or scalars. H and MI must contain integer values, whereas S may also contain a fractional part. T = datetime (Y, MO, D, H, MI, S, MS) also adds a milliseconds component, MS, which may also have a fractional part. T = datetime (X, 'ConvertFrom', TYPE) converts the numeric values in X to a datetime array according to the data type specified by TYPE. The following types are supported: 'datenum' 'excel' 'excel1904' 'posixtime' 'epochtime' 'juliandate' 'modifiedjuliandate' 'yyyymmdd' 'ntp' -- requires uint64 input. 'ntfs' -- requires uint64 input. '.net' -- requires uint64 input. 'tt2000' -- requires int64 input and the 'UTCLeapSeconds' time zone (see convertTo). 'Epoch' (a scalar datetime marking tick zero, default 1970-01-01) and 'TicksPerSecond' (a positive scalar, default 1) bear on 'epochtime' alone; they are accepted alongside any other conversion and ignored, each of the others counting from an epoch of its own. Whether the count measures elapsed or wall-clock time follows from the epoch. When the epoch and 'TimeZone' are both zoned the count is elapsed time from a known instant, so datetime (X, 'ConvertFrom', 'epochtime', 'Epoch', datetime (1970, 1, 1, 'TimeZone', 'UTC'), 'TimeZone', Z) agrees with 'posixtime'. Otherwise the epoch is a wall clock, its zone dropped if it had one, and so is the count -- which is why the default does not agree with 'posixtime': its epoch is unzoned, so 1730611800 names 05:30 on the local clock of whatever zone is asked for rather than the instant 05:30 UTC. That is not an inconsistency but the meaning of an epoch-relative count, and MATLAB reads it the same way. Each is the inverse of the convertTo conversion of the same name and treats an inserted leap second the way that conversion does, so the two round-trip. Where the conversion folded the inserted second away -- 'datenum', 'excel', 'excel1904', 'epochtime', 'ntp', 'ntfs' and '.net' all do -- it cannot be recovered and the preceding second is returned, just as those conversions give the two instants of a repeated wall clock the same value. The Julian scales measure the fraction of a day against that day's own length and so keep an inserted second, returning 23:59:60 where one was given. A Julian day number is a large one and a double holds it to about 40 microseconds at present-day dates, which bounds the accuracy of 'juliandate' in both directions; 'modifiedjuliandate' counts from a nearer epoch and is some forty times finer. Deviation from MATLAB. For a 'UTCLeapSeconds' array, 'epochtime' given an 'Epoch' counts both operands on the same timeline here, while MATLAB folds the array onto the POSIX one and measures the epoch on the leap-second one, so its answer runs long by the seconds inserted before that epoch: counting 86399 from 2016-12-31 it returns 2017-01-01T00:00:25, 26 seconds past the 23:59:59 that both consistent readings give. This is the same inconsistency already documented for convertTo in the other direction, and inverting it the same way is what makes the pair round-trip. Deviation from MATLAB. For a 'UTCLeapSeconds' array, MATLAB's 'juliandate' does not invert its own convertTo (T, 'juliandate'): it returns an instant half a second later than the one it was given, on the day of an inserted second and for every element of it. Its 'modifiedjuliandate' inverts correctly over the same day, so the two disagree with each other as well. The conversion here inverts exactly, in both scales. This is the same half-day slip already documented for 'tt2000' between 1960 and 1972 in convertTo. T = datetime (D), where D is already a datetime array, copies it: the components, the TimeZone and the Format all carry over. A 'Format' or 'TimeZone' may still be given to change either, and take effect exactly as assigning those properties does, so attaching a zone to an unzoned array keeps its wall-clock values whereas changing between two zones keeps the instant. The options describing how text is read, 'InputFormat', 'Locale' and 'PivotYear', have nothing to act on and are ignored, while 'ConvertFrom' is an error. T = datetime (..., 'Format', FMT) specifies the display format of the values in the output datetime array. FMT uses the same Unicode LDML date field symbols as 'InputFormat', 'z', 'Z', 'X', and 'x' naming the time zone, and text between single quotes taken literally. The default format renders a date alone when every element sits at midnight and a date with a time otherwise; a NaT carries no time of day and does not affect that choice. T = datetime (..., 'TimeZone', TZ) sets the time zone of the values in the output datetime array. If not specified, the array is unzoned: its values are wall-clock readings that name no absolute instant, and no daylight saving rule applies to them. Supported time zones are those of the IANA Time Zone Database, and 'local', in any letter case, names the system time zone given by datetime.SystemTimeZone, stored as naming that zone would store it. A fixed offset from UTC such as '+05:30' is accepted too; see the TimeZone property for its spellings. A zone may also be attached, changed, or dropped afterwards through the 'TimeZone' property; attaching one reinterprets the wall-clock values in that zone, whereas changing between two zones preserves the absolute instant and shifts the wall-clock values by the difference in offset. Twice a year a zone that observes Daylight Saving Time (DST) has wall-clock readings that name no unique instant. Where the clock goes back an hour repeats, and such a reading is taken at the later of the two offsets, that is, standard time. Where the clock goes forward an interval is skipped, and a reading inside it is one no clock in that zone ever shows. Given as numeric components, such a reading is moved ahead by the length of the skipped interval, so that with the usual one-hour skip '02:30' becomes '03:30'; given as text it is instead rejected, since text is a claim about a reading that never occurred. A lone such string is an error, whereas within an array only that element is lost and becomes NaT; without an 'InputFormat' the whole input is rejected, as when no format can be detected at all. In the 'UTCLeapSeconds' zone the seconds component may reach 60, naming an inserted leap second, but only on one of the 27 dates that has one and only in the last minute of the day; anywhere else it rolls over as usual. The seconds component is counted along the leap-second timeline, so '23:59:61' on such a date is the next midnight rather than one second past it, whereas an hour or minute that overflows over the inserted second entirely. Text naming a leap second that was never inserted is rejected, exactly as text naming a wall clock a zone skips is. Without an 'InputFormat' such an array reads only the ISO 8601 UTC shape it also writes. The representable range of years. The calendar is proleptic Gregorian and its arithmetic is 64-bit, so an unzoned array holds any year its components can, and so do the 'UTC' and 'UTCLeapSeconds' zones, whose offset is zero at every instant. Every other zone answers for any year as well, though beyond the IANA database's own range there is no zone data to consult -- only the rule it carries for all future time. That rule is written as an nth weekday of a month, so it repeats exactly every 400 years, the period over which weekday and leap year both return to where they started; the offset for a distant year is therefore taken from the congruent year inside the database's range. MATLAB extrapolates in the same way and agrees period for period, up to its own limit of about year 144684, beyond which it raises where this class continues to answer. A date before any zone had a transition takes that zone's local mean time, again as MATLAB does within its range. In such a zone MATLAB also refuses a day component beyond the range of a 32-bit integer, whether given to the constructor or assigned, with an error calling it not an integer; this class accepts it and answers with the date and time it gives without a zone. Deviation from MATLAB in the components of an infinite datetime. Building one is documented behaviour and is followed exactly: a component of Inf or -Inf makes the datetime infinite, and one of NaN makes it NaT. What the components of such a value read back as is documented nowhere, and the two implementations differ. Here every component is that infinity, whichever one carried it and whatever its sign, so ymd and datevec answer with one value rather than two, the sign is visible in each, and the components rebuild the value they came from. MATLAB reports the year alone as infinite and NaN for the other five, a tuple its own constructor then reads as NaT -- so its components do not reconstruct the datetime they describe. Everything else agrees: isnat, isinf, the display, the ordering, and that NaN beats an infinity while two infinities of opposite sign name no moment at all. Deviation from MATLAB in how wide a year may be written in text. MATLAB reads at most six significant digits there, whatever the format asks for, so it cannot read back a year past 999999 -- including one its own writetable has just written. Here the year is read as far as the calendar carries it, so that a file this class writes is one it can read. The divergence is additive: every string MATLAB accepts is read the same way, and only text it refuses outright is also accepted. Deviation from MATLAB in how a very distant year is displayed. MATLAB stops honouring the requested display format about year 144684 -- where the instant it stores internally stops being exact to the millisecond -- and prints '1000000 CE' or '1000000 BCE' instead, text which cannot be read back even by giving an era field. Here the components are separate double values and lose nothing there, so the format asked for is always the format rendered. Deviations from MATLAB when copying a datetime array and giving a 'TimeZone'. MATLAB's constructor keeps the display format of the array it copies, while its TimeZone property assignment replaces it for the same change of zone; the two disagree, so here both follow the one rule, that of the property. Leaving 'UTCLeapSeconds' therefore restores the ordinary default format rather than keeping the ISO 8601 pattern, whose 'Z' would misdescribe any zone but UTC. For the same reason, entering 'UTCLeapSeconds' works here, the locked format being applied, where MATLAB carries the copied format across and then rejects it as one that zone does not allow. See also: NaT, datetime, isdatetime, calendarDuration, duration # name: # type: sq_string # elements: 1 # length: 575 'InputFormat', INFMT, 'PivotYear', PIVOT) @deftypefnx {datetime} {T =} datetime (DateStrings,@ 'InputFormat', INFMT, 'Locale', LOCALE) @deftypefnx {datetime} {T =} datetime (DateVectors) @deftypefnx {datetime} {T =} datetime (Y, MO, D) @deftypefnx {datetime} {T =} datetime (Y, MO, D, H, MI, S) @deftypefnx {datetime} {T =} datetime (Y, MO, D, H, MI, S, MS) @deftypefnx {datetime} {T =} datetime (X, 'ConvertFrom', TYPE) @deftypefnx {datetime} {T =} datetime (D) @deftypefnx {datetime} {T =} datetime (, 'Format', FMT) @deftypefnx {datetime} {T =} datetime (, 'TimeZone', TZ) # name: # type: sq_string # elements: 1 # length: 16 datetime.datevec # name: # type: sq_string # elements: 1 # length: 140 datetime: DV = datevec (T) Convert datetime array to date vectors. DV = datevec (T) returns a numeric matrix with one row per element in T. # name: # type: sq_string # elements: 1 # length: 39 Convert datetime array to date vectors. # name: # type: sq_string # elements: 1 # length: 12 datetime.day # name: # type: sq_string # elements: 1 # length: 1363 datetime: D = day (T) datetime: D = day (T, dayType) Day component of a datetime array. D = day (T) returns the day number for each element of the input datetime array T. The output D is a double array containing integer values in the range [1, 31], depending on the month and year, and it has the same size as T. Not-A-Time (NaT) values in T are returned as NaN in the output array. D = day (T, dayType) returns the day number or name for each element of the input datetime array T as specified by dayType, which may have any of the following options: 'dayofmonth' (default) returns the day-of-month number in a numeric array. Depending on the month and year, it can range from 1 to 28, 29, 30, or 31. 'dayofweek' returns the day-of-week number in a numeric array of double integer values ranging from 1 to 7, where 1 corresponds to Sunday. 'iso-dayofweek' returns the day-of-week number in a numeric array of double integer values ranging from 1 to 7, where 1 corresponds to Monday according to the ISO 8601 standard. 'dayofyear' returns the day-of-year number in a numeric array of double integer values ranging from 1 to 365 or 366, depending on the year. 'name' returns the corresponding full name of the day in a cell array of character vectors. 'shortname' returns the corresponding 3-letter abbreviation of the day in a cell array of character vectors. # name: # type: sq_string # elements: 1 # length: 34 Day component of a datetime array. # name: # type: sq_string # elements: 1 # length: 13 datetime.diff # name: # type: sq_string # elements: 1 # length: 687 datetime: D = diff (A) datetime: D = diff (A, N) datetime: D = diff (A, N, dim) Differences between successive datetime elements. D = diff (A) returns a duration array holding the elapsed time between successive elements of A along its first non-singleton dimension. The differences are computed from the absolute instants, so for a zoned array they are aware of daylight saving time transitions (a calendar day spanning a transition is 23 or 25 hours, not 24). Not-A-Time elements propagate as NaN durations. D = diff (A, N) applies diff recursively N times, returning the N-th order difference. N must be a positive integer scalar. D = diff (A, N, dim) operates along dimension dim. # name: # type: sq_string # elements: 1 # length: 49 Differences between successive datetime elements. # name: # type: sq_string # elements: 1 # length: 19 datetime.discretize # name: # type: sq_string # elements: 1 # length: 4467 datetime: bin = discretize (T, edges) datetime: bin = discretize (T, N) datetime: bin = discretize (T, dur) datetime: bin = discretize (T, unit) datetime: Y = discretize (..., values) datetime: C = discretize (..., 'categorical') datetime: C = discretize (..., 'categorical', names) datetime: [bin, E] = discretize (...) Group datetimes into bins. bin = discretize (T, edges) returns, for each element of T, the index of the bin of the datetime vector edges that contains it. Bins are half open, [E(j), E(j+1)), except the last which is closed at both ends. Elements outside the edges, and NaT elements, give NaN. bin = discretize (T, N) uses N bins spanning the data, placed on whole calendar or clock units wherever that can be done without leaving a bin unused. bin = discretize (T, dur) uses bins one dur wide, where dur is a scalar duration or calendarDuration, aligned to whole multiples of that width. bin = discretize (T, unit) uses bins one named unit wide, unit being one of 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year', 'decade' or 'century'. These land on real calendar boundaries: a 'week' bin starts on a Sunday, a 'quarter' on 1 January, 1 April, 1 July or 1 October, and a 'decade' on a year that is a multiple of ten. Y = discretize (..., values) returns values(bin) instead of the bin index, and C = discretize (..., 'categorical') returns a categorical array whose categories are named after the bins. [bin, E] = discretize (...) also returns the bin edges as a datetime array carrying this array's Format and TimeZone. Note that whether a bin follows the calendar depends on how its width is given, not on how long that width is. A named unit ('day' and coarser) or a calendarDuration width begins at local midnight, so in a time zone that observes daylight saving the bin holding a transition is 23 or 25 hours long while its neighbours are 24. A duration width is a fixed span of elapsed time whatever its length: days (1) bins are each exactly 24 hours, and their edges therefore read an hour later on the far side of a transition. The two agree for an unzoned array, and for a zoned one that spans no transition. A named unit opens one bin past the data when the largest element sits on a unit boundary of the wall clock. Sub-day bins step in elapsed time, so in a zone whose shift is not a whole number of those units -- Australia/Lord_Howe moves its clock half an hour, against an 'hour' bin -- the grid leaves the wall clock past a transition and an element may then land mid-unit. Such an element gets a bin of its own here. This is deliberately unlike MATLAB, which closes its last edge short of it and leaves it out of every bin. Deviation from MATLAB: bin placement where a zone's clock shifts by a fraction of the bin unit. A bin grid is anchored on the wall clock and stepped in elapsed time, so a zone that moves its clock by less than one bin unit -- Australia/Lord_Howe moves it half an hour -- puts the grid out of step with the clock from the transition onward. Where the shift is exactly half a unit the placement matches MATLAB, having been measured against it. For any other fraction, which today arises only from the historical offset changes of the early twentieth century (Asia/Singapore moved by twenty minutes in 1933), the placement is our own: the grid is centred on the data with no correction for the transition, exactly as in a zone that has none. MATLAB places those bins differently, by a rule we have not been able to derive from its output; both cover the data, and neither is more correct than the other. Only a requested bin count is affected -- and 'auto', 'scott', 'fd', 'sturges' and 'sqrt', which resolve to one. An explicit 'BinWidth' or a named unit is placed identically to MATLAB in every zone. A bin width below a second carries a little noise in its edges. An instant is counted in seconds from 1970 and so runs to about 1.7e9, where a double resolves to some 2e-7 of a second; a width of, say, 0.4 s therefore lands its edges within a few hundred nanoseconds of the exact grid rather than on it. The opening edge is exact, the drift is in the step, and it is a limit of the representation rather than of the placement. When T is empty the edges are anchored on the epoch, 1970-01-01. This is deliberately unlike MATLAB, which answers an empty datetime with edges taken from the current clock, so that the same call returns a different result every time it is run. # name: # type: sq_string # elements: 1 # length: 26 Group datetimes into bins. # name: # type: sq_string # elements: 1 # length: 20 datetime.dispstrings # name: # type: sq_string # elements: 1 # length: 226 datetime: cstr = dispstrings (T) Get display formatted strings for each element of a datetime object. cstr = dispstrings (T) returns a cellstr array of character vectors, cstr, which has the same size as the input datetime T. # name: # type: sq_string # elements: 1 # length: 68 Get display formatted strings for each element of a datetime object. # name: # type: sq_string # elements: 1 # length: 14 datetime.empty # name: # type: sq_string # elements: 1 # length: 578 datetime: E = datetime.empty () datetime: E = datetime.empty (sz) datetime: E = datetime.empty (m, n, ...) Create an empty datetime array. E = datetime.empty () returns a 0x0 empty datetime array. datetime.empty (m, n, ...) or datetime.empty (sz) returns an empty datetime array of the requested size, which must have at least one dimension equal to zero. A lone dimension gives a square size, so datetime.empty (3) is an error while datetime.empty (0) is 0x0. As for zeros, a negative dimension counts as zero, and a size vector with nothing in it names no size and gives 0x0. # name: # type: sq_string # elements: 1 # length: 31 Create an empty datetime array. # name: # type: sq_string # elements: 1 # length: 11 datetime.eq # name: # type: sq_string # elements: 1 # length: 555 datetime: TF = eq (A, B) Equality comparison for datetime arrays. TF = eq (A, B) is the equivalent of the syntax TF = A == B and returns a logical array set to true where the corresponding elements of A and B are the same point in time, and false otherwise. Both operands must be datetime arrays and either both have a time zone or both be unzoned; zoned arrays are compared by their absolute instants, so the two time zones may differ. A and B must be size compatible. Not-A-Time is never equal to anything, including another Not-A-Time, just like NaN. # name: # type: sq_string # elements: 1 # length: 40 Equality comparison for datetime arrays. # name: # type: sq_string # elements: 1 # length: 18 datetime.exceltime # name: # type: sq_string # elements: 1 # length: 1172 datetime: E = exceltime (T) datetime: E = exceltime (T, dateType) Convert datetime array to Excel serial date numbers. E = exceltime (T) returns a double array E of the same size as T holding the Excel serial date number of each element, using the Excel 1900 date system, in which the fractional part represents the time of day. As in Excel, the year 1900 is treated as a leap year, so serial numbers on or after 1900-03-01 account for the nonexistent date 1900-02-29. E = exceltime (T, dateType) selects the date system: '1900' (default) or '1904'. The 1904 system counts days from 1904-01-01 and has no leap-year anomaly, so a date before 1900-03-01 is a plain day count and its serial is one greater than MATLAB returns; MATLAB derives the 1904 serial from the 1900 one and carries the nonexistent 1900-02-29 into a system that never had it. Excel stores no negative serial in either system, so this affects only dates neither can represent, where the value returned here is the one datetime (E, 'ConvertFrom', 'excel1904') converts back exactly. The time zone of a zoned T is ignored; its wall-clock components are used. Not-A-Time (NaT) values are returned as NaN. # name: # type: sq_string # elements: 1 # length: 52 Convert datetime array to Excel serial date numbers. # name: # type: sq_string # elements: 1 # length: 11 datetime.ge # name: # type: sq_string # elements: 1 # length: 559 datetime: TF = ge (A, B) Greater-than-or-equal comparison for datetime arrays. TF = ge (A, B) is the equivalent of the syntax TF = A >= B and returns a logical array set to true where the corresponding element of A is a later or equal point in time to that of B, and false otherwise. Both operands must be datetime arrays and either both have a time zone or both be unzoned; zoned arrays are compared by their absolute instants, so the two time zones may differ. A and B must be size compatible. Not-A-Time compares as false against anything, just like NaN. # name: # type: sq_string # elements: 1 # length: 53 Greater-than-or-equal comparison for datetime arrays. # name: # type: sq_string # elements: 1 # length: 11 datetime.gt # name: # type: sq_string # elements: 1 # length: 542 datetime: TF = gt (A, B) Greater-than comparison for datetime arrays. TF = gt (A, B) is the equivalent of the syntax TF = A > B and returns a logical array set to true where the corresponding element of A is a later point in time than that of B, and false otherwise. Both operands must be datetime arrays and either both have a time zone or both be unzoned; zoned arrays are compared by their absolute instants, so the two time zones may differ. A and B must be size compatible. Not-A-Time compares as false against anything, just like NaN. # name: # type: sq_string # elements: 1 # length: 44 Greater-than comparison for datetime arrays. # name: # type: sq_string # elements: 1 # length: 19 datetime.histcounts # name: # type: sq_string # elements: 1 # length: 2515 datetime: N = histcounts (T) datetime: N = histcounts (T, nbins) datetime: N = histcounts (T, edges) datetime: N = histcounts (..., Name, Value) datetime: [N, edges] = histcounts (...) datetime: [N, edges, bin] = histcounts (...) Histogram bin counts for datetimes. N = histcounts (T) bins the datetimes in T, chosen automatically, and returns the number of elements in each bin. T is treated as T(:) and NaT elements are excluded. N = histcounts (T, nbins) and N = histcounts (T, edges) bin by count and by explicit datetime edges respectively. 'BinWidth' takes a scalar duration or calendarDuration and, as an Octave extension, any of the unit names listed for discretize; MATLAB accepts only the two classes here and requires 'BinMethod' for a named unit. The two spellings give identical results, so histcounts (T, 'BinWidth', 'day') is simply another way of writing histcounts (T, 'BinMethod', 'day') -- and note that neither is the same as 'BinWidth', days (1), for the reason given below. 'BinLimits' takes a two-element datetime, and 'BinEdges' a datetime vector. 'BinMethod' accepts 'auto', 'scott', 'fd', 'sturges' and 'sqrt', and any of the named calendar units listed for discretize, but not 'integers', which has no meaning for a datetime. 'Normalization' accepts 'count', 'cumcount', 'probability', 'percentage' and 'cdf'. 'countdensity' and 'pdf' are not accepted, since a density per unit time has no meaning here. As for discretize, a named unit or a calendarDuration width begins at local midnight and so varies in length across a daylight-saving transition, while a duration width is a fixed span of elapsed time whatever its length, and an empty T anchors the edges on the epoch rather than on the current clock as MATLAB does. The same paragraph on named units also applies: where a zone shifts by less than one bin unit, an element landing mid-unit past the transition gets a bin of its own, so N always sums to the number of finite elements. MATLAB can leave such an element out of every bin, and its counts then sum to less. A bin width below a second carries a little noise in its edges. An instant is counted in seconds from 1970 and so runs to about 1.7e9, where a double resolves to some 2e-7 of a second; a width of, say, 0.4 s therefore lands its edges within a few hundred nanoseconds of the exact grid rather than on it. The opening edge is exact, the drift is in the step, and it is a limit of the representation rather than of the placement. See also: discretize, histcounts # name: # type: sq_string # elements: 1 # length: 35 Histogram bin counts for datetimes. # name: # type: sq_string # elements: 1 # length: 12 datetime.hms # name: # type: sq_string # elements: 1 # length: 406 datetime: [h, m, s] = hms (T) Hour, Minute, and Second components of a datetime array. [h, m, s] = hms (T) returns the hour, minute, and second components of the corresponding datetime values in T as separate numeric arrays. h and m contain integer values and s may also contain a fractional part. All outputs have the same size as T. Not-A-Time (NaT) values in T are returned as NaN in the output arrays. # name: # type: sq_string # elements: 1 # length: 56 Hour, Minute, and Second components of a datetime array. # name: # type: sq_string # elements: 1 # length: 16 datetime.horzcat # name: # type: sq_string # elements: 1 # length: 344 datetime: C = horzcat (A, B, ...) Horizontal concatenation of datetime arrays. C = horzcat (A, B, ... is the equivalent of the syntax B = [A, B, ...] and horizontally concatenates the inputs A, B, ..., which must have the same size except along the second dimension. See cat for which inputs are accepted and how their time zones are resolved. # name: # type: sq_string # elements: 1 # length: 44 Horizontal concatenation of datetime arrays. # name: # type: sq_string # elements: 1 # length: 13 datetime.hour # name: # type: sq_string # elements: 1 # length: 323 datetime: h = hour (T) Hour component of a datetime array. h = hour (T) returns the hour number for each element of the input datetime array T. The output h is a double array containing integer values in the range [0, 23] and it has the same size as T. Not-A-Time (NaT) values in T are returned as NaN in the output array. # name: # type: sq_string # elements: 1 # length: 35 Hour component of a datetime array. # name: # type: sq_string # elements: 1 # length: 16 datetime.interp1 # name: # type: sq_string # elements: 1 # length: 942 datetime: VI = interp1 (X, V, XI) datetime: VI = interp1 (..., method) datetime: VI = interp1 (..., method, extrap) One-dimensional interpolation involving datetime arrays. VI = interp1 (X, V, XI) interpolates to find VI, the values of the underlying function V = f (X) at the query points XI. Datetime arrays are handled by interpolating on their absolute (POSIX) instants: When the sample points X are a datetime array, the query points XI must also be a datetime array, and vice versa. When the sampled values V are a datetime array, VI is a datetime array carrying the time zone and display format of V; otherwise VI is numeric. The optional method ('linear' by default) and extrap arguments are passed to the built-in interp1 (see its documentation for the supported interpolation methods and extrapolation options). Query points outside the range of X, and Not-A-Time query points, yield NaN or NaT unless extrapolation is requested. # name: # type: sq_string # elements: 1 # length: 56 One-dimensional interpolation involving datetime arrays. # name: # type: sq_string # elements: 1 # length: 18 datetime.intersect # name: # type: sq_string # elements: 1 # length: 1228 datetime: C = intersect (A, B) datetime: C = intersect (A, B, 'rows') datetime: C = intersect (..., order) datetime: [C, ixA, ixB] = intersect (...) Set intersection of two datetime arrays. C = intersect (A, B) returns the unique datetime values common to both A and B. Either input may instead be a date/time character vector, string array, or cell array of character vectors, which is promoted to a datetime array before the operation. Membership is decided on the absolute instant, so two zoned inputs may be in different time zones; the result carries the time zone and display format of A. Not-A-Time (NaT) elements are treated like NaN and never match. C is a row vector when both A and B are row vectors and a column vector otherwise. C = intersect (A, B, 'rows') treats each row of the datetime matrices A and B, which must have the same number of columns, as a single element and returns their common rows. ... = intersect (..., order) returns the values in 'sorted' order (the default) or in 'stable' order, i.e. the order in which they appear in A. [C, ixA, ixB] = intersect (...) also returns index vectors ixA and ixB such that C = A(ixA) and C = B(ixB), or the corresponding row selections when 'rows' is used. # name: # type: sq_string # elements: 1 # length: 40 Set intersection of two datetime arrays. # name: # type: sq_string # elements: 1 # length: 17 datetime.ipermute # name: # type: sq_string # elements: 1 # length: 463 datetime: A = ipermute (B, dims) Inverse of the generalized transpose for a datetime N-D array. A = ipermute (B, dims) returns the inverse of the generalized transpose performed by the permute function. The expression ipermute (permute (A, dims), dims) returns the original array A. dims must index all the dimensions 1:ndims (B) of the input array B, in any order, but only once. The dimension of B specified in dims(N) gets remapped to the Nth dimension of A. # name: # type: sq_string # elements: 1 # length: 62 Inverse of the generalized transpose for a datetime N-D array. # name: # type: sq_string # elements: 1 # length: 18 datetime.isbetween # name: # type: sq_string # elements: 1 # length: 937 datetime: TF = isbetween (X, lower, upper) datetime: TF = isbetween (X, lower, upper, intervalType) Determine which datetime values lie within an interval. TF = isbetween (X, lower, upper) returns a logical array TF, the same size as the broadcast of its inputs, containing true where the element of X lies between the corresponding lower and upper bounds. lower and upper may each be a datetime array or a date/time character vector, string array, or cell array of character vectors, and either may be scalar to broadcast against X. The comparison is made on the absolute instant, so zoned inputs may be in different time zones. A Not-A-Time element in any input makes the corresponding result false. TF = isbetween (X, lower, upper, intervalType) selects which endpoints are included. intervalType is 'closed' (the default, lower X upper), 'open' (both endpoints excluded), 'openleft' (exclude lower), or 'openright' (exclude upper). # name: # type: sq_string # elements: 1 # length: 55 Determine which datetime values lie within an interval. # name: # type: sq_string # elements: 1 # length: 17 datetime.iscolumn # name: # type: sq_string # elements: 1 # length: 291 datetime: TF = iscolumn (T) Return true if datetime array is a column vector. TF = iscolumn (T) returns a logical scalar TF, which is true, if the datetime array T is a column vector, and false otherwise. A column vector is a 2-D array for which size (X) returns [N, 1] with non-negative N. # name: # type: sq_string # elements: 1 # length: 49 Return true if datetime array is a column vector. # name: # type: sq_string # elements: 1 # length: 14 datetime.isdst # name: # type: sq_string # elements: 1 # length: 404 datetime: TF = isdst (T) Determine which datetime values fall in daylight saving time. TF = isdst (T) returns a logical array TF of the same size as the input datetime array T, where each element is true if the corresponding datetime falls within daylight saving time in its time zone, and false otherwise. If T has no time zone, or for Not-A-Time (NaT) values, the corresponding element of TF is false. # name: # type: sq_string # elements: 1 # length: 61 Determine which datetime values fall in daylight saving time. # name: # type: sq_string # elements: 1 # length: 16 datetime.isempty # name: # type: sq_string # elements: 1 # length: 183 datetime: TF = isempty (T) Return true if datetime array is empty. TF = isempty (T) returns a logical scalar TF, which is true, if the datetime array T is empty, and false otherwise. # name: # type: sq_string # elements: 1 # length: 39 Return true if datetime array is empty. # name: # type: sq_string # elements: 1 # length: 16 datetime.isequal # name: # type: sq_string # elements: 1 # length: 905 datetime: TF = isequal (A, B) datetime: TF = isequal (A, B, ...) Test datetime arrays for equality. TF = isequal (A, B) returns a logical scalar TF, which is true if the datetime arrays A and B are the same size and each pair of corresponding elements is the same point in time, and false otherwise. As with NaN, Not-A-Time (NaT) elements are never equal, so any NaT in either array makes the result false; use isequaln to treat NaT elements as equal. Additional arrays may be supplied, as in isequal (A, B, C, ...), in which case TF is true only when all of the arrays are equal to one another. Any input argument that is not a datetime array, or a datetime array whose time zone is not compatible with the others (one zoned and one unzoned), makes the result false rather than raising an error. Zoned arrays are compared by their absolute instants, so equal instants in different time zones are equal. # name: # type: sq_string # elements: 1 # length: 34 Test datetime arrays for equality. # name: # type: sq_string # elements: 1 # length: 17 datetime.isequaln # name: # type: sq_string # elements: 1 # length: 957 datetime: TF = isequaln (A, B) datetime: TF = isequaln (A, B, ...) Test datetime arrays for equality, treating Not-A-Time as equal. TF = isequaln (A, B) is identical to isequal (A, B) except that Not-A-Time (NaT) elements are treated as equal to one another, in the same way that isequaln treats NaN. It returns a logical scalar TF, which is true if the datetime arrays A and B are the same size and each pair of corresponding elements is either the same point in time or both NaT, and false otherwise. Additional arrays may be supplied, as in isequaln (A, B, C, ...), in which case TF is true only when all of the arrays are equal to one another. Any input argument that is not a datetime array, or a datetime array whose time zone is not compatible with the others (one zoned and one unzoned), makes the result false rather than raising an error. Zoned arrays are compared by their absolute instants, so equal instants in different time zones are equal. # name: # type: sq_string # elements: 1 # length: 64 Test datetime arrays for equality, treating Not-A-Time as equal. # name: # type: sq_string # elements: 1 # length: 17 datetime.isfinite # name: # type: sq_string # elements: 1 # length: 304 datetime: out = isfinite (T) Test for finite elements in datetime array. TF = isfinite (T) returns a logical array TF of the same size as T containing true for each corresponding element of T that is finite and false otherwise. Finite elements in datetime arrays are those which are neither Inf nor NaT. # name: # type: sq_string # elements: 1 # length: 43 Test for finite elements in datetime array. # name: # type: sq_string # elements: 1 # length: 14 datetime.isinf # name: # type: sq_string # elements: 1 # length: 226 datetime: out = isinf (T) Test for infinite elements in datetime array. TF = isinf (T) returns a logical array TF of the same size as T containing true for each corresponding element of T that is infinite and false otherwise. # name: # type: sq_string # elements: 1 # length: 45 Test for infinite elements in datetime array. # name: # type: sq_string # elements: 1 # length: 17 datetime.ismatrix # name: # type: sq_string # elements: 1 # length: 313 datetime: TF = ismatrix (T) Return true if datetime array is a 2-D array. TF = ismatrix (T) returns a logical scalar TF, which is true, if the datetime array T is a matrix, and false otherwise. A matrix is an array of any type where ndims (X) == 2 and for which size (X) returns [H, W] with non-negative H and W. # name: # type: sq_string # elements: 1 # length: 45 Return true if datetime array is a 2-D array. # name: # type: sq_string # elements: 1 # length: 17 datetime.ismember # name: # type: sq_string # elements: 1 # length: 1276 datetime: TF = ismember (A, B) datetime: TF = ismember (A, B, 'rows') datetime: [TF, index] = ismember (...) Test for datetime elements in a set. TF = ismember (A, B) returns a logical array TF of the same size as A containing true for each corresponding element of A that is in B and false otherwise. Either input may instead be a date/time character vector, string array, or cell array of character vectors, which is promoted to a datetime array before the test. Membership is decided on the absolute instant, so two zoned inputs may be in different time zones. Similarly to NaN values, Not-A-Time (NaT) elements are not equal with each other and always return false. TF = ismember (A, B, 'rows') only applies to datetime matrices with the same number of columns, in which case the logical vector TF contains true for each row of A that is also a row in B. TF has the same number of rows as A. [TF, index] = ismember (A, B) also returns an index array of the same size as A containing the lowest index in B for each element of A that is a member of B and 0 otherwise. If the 'rows' optional argument is used, then the returning index is a column vector with the same rows as A and it contains the lowest index in B for each row of A that is a member of B and 0 otherwise. # name: # type: sq_string # elements: 1 # length: 36 Test for datetime elements in a set. # name: # type: sq_string # elements: 1 # length: 18 datetime.ismissing # name: # type: sq_string # elements: 1 # length: 471 datetime: TF = ismissing (T) datetime: TF = ismissing (T, indicator) Test for missing elements in datetime array. TF is a logical array of the same size as T, true where an element of T is NaT. TF = ismissing (T, indicator) marks instead the elements of T equal to the values in indicator, a datetime scalar or vector; the standard missing values are ignored, unless explicitly stated in indicator. A NaT in indicator matches the NaT elements of T, and so does missing. # name: # type: sq_string # elements: 1 # length: 44 Test for missing elements in datetime array. # name: # type: sq_string # elements: 1 # length: 14 datetime.isnat # name: # type: sq_string # elements: 1 # length: 338 datetime: TF = isnat (T) Test for Not-A-Time elements in datetime array. TF = isnat (T) returns a logical array TF of the same size as T containing true for each corresponding element of T that is Not-A-Time (NaT) and false otherwise. NaT is the equivalent of NaN in numeric arrays. If T is not a datetime array, isnat returns an error. # name: # type: sq_string # elements: 1 # length: 47 Test for Not-A-Time elements in datetime array. # name: # type: sq_string # elements: 1 # length: 18 datetime.isregular # name: # type: sq_string # elements: 1 # length: 1337 datetime: tf = isregular (T) datetime: tf = isregular (T, unit) datetime: [tf, dt] = isregular (...) Determine whether a datetime vector is regularly spaced. tf = isregular (T) returns true if the elements of the datetime vector T are equally spaced in time, and false otherwise. A scalar or empty T, or one containing a Not-A-Time (NaT) value, is not regular. Neither is a T that does not move: a step of zero describes no spacing, so a vector of repeated instants is not regularly spaced. tf = isregular (T, unit) tests for regular spacing with respect to unit, which may be 'time' (the default), 'years', 'quarters', 'months', 'weeks', or 'days'. With a calendar unit, T is regular when successive elements differ by the same whole number of that unit, which -- unlike 'time' -- accounts for varying month lengths and daylight saving time. [tf, dt] = isregular (...) also returns the common time step dt. For 'time' it is a duration; for a calendar unit it is a calendarDuration. When T is not regular, dt is NaN. Steps are compared as stored, to the microsecond this class keeps. A spacing that differs by less than that cannot be seen and reads as regular, while one that rounds differently at the microsecond -- thirds of a second, say -- reads as irregular. MATLAB stores instants more finely and so draws that line elsewhere. # name: # type: sq_string # elements: 1 # length: 56 Determine whether a datetime vector is regularly spaced. # name: # type: sq_string # elements: 1 # length: 14 datetime.isrow # name: # type: sq_string # elements: 1 # length: 276 datetime: TF = isrow (T) Return true if datetime array is a row vector. TF = isrow (T) returns a logical scalar TF, which is true, if the datetime array T is a row vector, and false otherwise. A row vector is a 2-D array for which size (X) returns [1, N] with non-negative N. # name: # type: sq_string # elements: 1 # length: 46 Return true if datetime array is a row vector. # name: # type: sq_string # elements: 1 # length: 17 datetime.isscalar # name: # type: sq_string # elements: 1 # length: 267 datetime: TF = isscalar (T) Return true if datetime array is a scalar. TF = isscalar (T) returns a logical scalar TF, which is true, if the datetime array T is also a scalar, and false otherwise. A scalar is a single element object for which size (X) returns [1, 1]. # name: # type: sq_string # elements: 1 # length: 42 Return true if datetime array is a scalar. # name: # type: sq_string # elements: 1 # length: 17 datetime.issorted # name: # type: sq_string # elements: 1 # length: 1086 datetime: TF = issorted (A) datetime: TF = issorted (A, dim) datetime: TF = issorted (A, direction) datetime: TF = issorted (A, dim, direction) Determine whether a datetime array is sorted. TF = issorted (A) returns true if the elements of the datetime array A are sorted in ascending (non-decreasing) order along its first non-singleton dimension, and false otherwise. For a matrix, every column (or row, depending on the operating dimension) must be sorted for TF to be true. Not-A-Time (NaT) elements are treated as greater than any other value, so an array is sorted in ascending order only when its NaT elements come last. TF = issorted (A, dim) operates along dimension dim. TF = issorted (A, direction) tests whether the elements are sorted according to direction, which may be one of: 'ascend' (default) tests non-decreasing order. 'descend' tests non-increasing order. 'monotonic' tests non-decreasing or non-increasing order. 'strictascend' tests strictly increasing order. 'strictdescend' tests strictly decreasing order. 'strictmonotonic' tests strictly monotonic order. # name: # type: sq_string # elements: 1 # length: 45 Determine whether a datetime array is sorted. # name: # type: sq_string # elements: 1 # length: 21 datetime.issortedrows # name: # type: sq_string # elements: 1 # length: 1175 datetime: TF = issortedrows (A) datetime: TF = issortedrows (A, column) datetime: TF = issortedrows (A, direction) datetime: TF = issortedrows (A, column, direction) Determine whether the rows of a datetime array are sorted. TF = issortedrows (A) returns true if the rows of the 2-D datetime array A are sorted in ascending order, i.e. lexicographically by the first column, ties broken by the second column, and so on, and false otherwise. Not-A-Time (NaT) elements are treated as greater than any other value. TF = issortedrows (A, column) checks the order using only the columns listed in column, in the given priority. A negative entry checks the corresponding column for descending order. TF = issortedrows (A, direction) checks for the order given by direction, which may be one of 'ascend' (default), 'descend', 'monotonic', 'strictascend', 'strictdescend', or 'strictmonotonic'. It may also be a cell array of 'ascend'/'descend' strings, one per sort column. For the strict options a matrix qualifies only when its first sort column is strictly monotonic and free of NaT. TF = issortedrows (A, column, direction) combines an explicit column list with a direction. # name: # type: sq_string # elements: 1 # length: 58 Determine whether the rows of a datetime array are sorted. # name: # type: sq_string # elements: 1 # length: 17 datetime.isvector # name: # type: sq_string # elements: 1 # length: 322 datetime: TF = isvector (T) Return true if datetime array is a vector. TF = isvector (T) returns a logical scalar TF, which is true if the datetime array T is a vector and false otherwise. A vector is a 2-D array for which one of the dimensions is equal to 1 (either 1xN or Nx1). By definition, a scalar is also a vector. # name: # type: sq_string # elements: 1 # length: 42 Return true if datetime array is a vector. # name: # type: sq_string # elements: 1 # length: 18 datetime.isweekend # name: # type: sq_string # elements: 1 # length: 321 datetime: TF = isweekend (T) Determine which datetime values fall on a weekend. TF = isweekend (T) returns a logical array TF of the same size as the input datetime array T, where each element is true if the corresponding datetime falls on a Saturday or Sunday, and false otherwise. Not-A-Time (NaT) values return false. # name: # type: sq_string # elements: 1 # length: 50 Determine which datetime values fall on a weekend. # name: # type: sq_string # elements: 1 # length: 19 datetime.juliandate # name: # type: sq_string # elements: 1 # length: 1086 datetime: J = juliandate (T) datetime: J = juliandate (T, dateType) Convert datetime array to Julian dates. J = juliandate (T) returns a double array J of the same size as T holding the Julian date of each element, that is, the number of days (including a fractional part) since noon UTC on January 1, 4713 BCE. Unlike datenum, the result is based on the absolute UTC instant, so the time zone of a zoned T is taken into account; datetime arrays without a time zone are treated as UTC. Not-A-Time (NaT) values are returned as NaN. J = juliandate (T, dateType) selects the kind of Julian date: 'juliandate' (default) or 'modifiedjuliandate', the latter being the Julian date minus 2400000.5. For a 'UTCLeapSeconds' array a Julian day that holds an inserted second is 86401 seconds long, and the fractional part is that fraction of the day's true length. Julian days run from noon to noon and modified Julian days from midnight to midnight, so on such a day the two are stretched over different spans and are not related by exactly 2400000.5; both are 'juliandate' for the day at hand. # name: # type: sq_string # elements: 1 # length: 39 Convert datetime array to Julian dates. # name: # type: sq_string # elements: 1 # length: 16 datetime.keyHash # name: # type: sq_string # elements: 1 # length: 1282 datetime: key = keyHash (T) datetime: key = keyHash (T, base) Generate a hash code for datetime array. key = keyHash (T) generates a uint64 scalar that represents the input array T. keyHash utilizes the 64-bit FNV-1a variant of the Fowler-Noll-Vo non-cryptographic hash function. key = keyHash (T, base) also generates a 64-bit hash code using base as the offset basis for the FNV-1a hash algorithm. base must be a uint64 integer type scalar. Use this syntax to cascade keyHash on multiple objects for which a single hash code is required. A datetime array is keyed on the instant its elements name and not on their wall clock and time zone, so the same moment expressed in two zones has the same hash code. An unzoned array, a zoned array and a leap-second array are three separate frames that never share a key, whatever instants they name, and the two instants sharing a wall clock across a daylight-saving fall-back stay distinct. The Format property is display only and is not part of the key. Two arrays that keyMatch reports as the same key always hash alike. Note that unlike MATLAB, this implementation does not use any random seed. As a result, keyHash will always generate the exact same hash key for any particular input across different workers and Octave sessions. # name: # type: sq_string # elements: 1 # length: 40 Generate a hash code for datetime array. # name: # type: sq_string # elements: 1 # length: 11 datetime.le # name: # type: sq_string # elements: 1 # length: 559 datetime: TF = le (A, B) Less-than-or-equal comparison for datetime arrays. TF = le (A, B) is the equivalent of the syntax TF = A <= B and returns a logical array set to true where the corresponding element of A is an earlier or equal point in time to that of B, and false otherwise. Both operands must be datetime arrays and either both have a time zone or both be unzoned; zoned arrays are compared by their absolute instants, so the two time zones may differ. A and B must be size compatible. Not-A-Time compares as false against anything, just like NaN. # name: # type: sq_string # elements: 1 # length: 50 Less-than-or-equal comparison for datetime arrays. # name: # type: sq_string # elements: 1 # length: 15 datetime.length # name: # type: sq_string # elements: 1 # length: 218 datetime: N = length (T) Length of a datetime vector. N = length (T) returns the size of the longest dimension of the datetime array T, unless any of its dimensions has zero length, in which case length (T) returns 0. # name: # type: sq_string # elements: 1 # length: 28 Length of a datetime vector. # name: # type: sq_string # elements: 1 # length: 17 datetime.linspace # name: # type: sq_string # elements: 1 # length: 1028 datetime: R = linspace (A, B) datetime: R = linspace (A, B, N) Create linearly spaced datetime values. R = linspace (A, B) returns a row vector of 100 datetime values spaced equally between the scalar datetimes A and B, both of which are included. R = linspace (A, B, N) returns N values instead of 100. A non-integer N is floored; if N is one, B alone is returned, and if N is zero or negative the result is empty. The spacing is by absolute instant, so for zoned inputs it is daylight saving aware: a span that loses an hour to a transition is divided into equal instants, whose wall-clock readings are therefore not equally spaced. The two endpoints may be in different time zones, in which case the result is expressed in the zone of A, but they must either both be zoned or both be unzoned. R carries the Format and TimeZone of A. A Not-A-Time endpoint makes every value that depends on it NaT, leaving only the opposite endpoint; an infinite endpoint likewise carries its infinity through the values that depend on it. # name: # type: sq_string # elements: 1 # length: 39 Create linearly spaced datetime values. # name: # type: sq_string # elements: 1 # length: 11 datetime.lt # name: # type: sq_string # elements: 1 # length: 542 datetime: TF = lt (A, B) Less-than comparison for datetime arrays. TF = lt (A, B) is the equivalent of the syntax TF = A < B and returns a logical array set to true where the corresponding element of A is an earlier point in time than that of B, and false otherwise. Both operands must be datetime arrays and either both have a time zone or both be unzoned; zoned arrays are compared by their absolute instants, so the two time zones may differ. A and B must be size compatible. Not-A-Time compares as false against anything, just like NaN. # name: # type: sq_string # elements: 1 # length: 41 Less-than comparison for datetime arrays. # name: # type: sq_string # elements: 1 # length: 12 datetime.max # name: # type: sq_string # elements: 1 # length: 1075 datetime: M = max (A) datetime: M = max (A, B) datetime: M = max (A, [], dim) datetime: M = max (..., nanflag) datetime: M = max (A, [], 'all') datetime: [M, I] = max (...) Maximum of a datetime array. M = max (A) returns the largest element of the datetime array A along its first non-singleton dimension. For a matrix, M is a row vector with the maximum of each column. Not-A-Time (NaT) elements are omitted; a reduction over NaT elements only yields NaT. M = max (A, B) returns an array the same size as A and B (after broadcasting) holding the element-wise maximum of the two datetime arrays. M = max (A, [], dim) operates along dimension dim. The empty second argument distinguishes this from the element-wise form. M = max (..., nanflag) sets the treatment of NaT: 'omitnan' (default) ignores NaT, while 'includenan' returns NaT whenever a NaT takes part in the comparison. M = max (A, [], 'all') returns the largest element of the whole array. [M, I] = max (...) also returns the indices of the maxima. A second output is not available for the element-wise form. # name: # type: sq_string # elements: 1 # length: 28 Maximum of a datetime array. # name: # type: sq_string # elements: 1 # length: 13 datetime.maxk # name: # type: sq_string # elements: 1 # length: 1357 datetime: B = maxk (A, K) datetime: B = maxk (A, K, dim) datetime: [B, index] = maxk (A, ...) Find the latest elements in a datetime array. B = maxk (A, K) returns the K latest elements of the datetime array A in descending order. If A is a vector, then B is a vector with K elements. If A is a matrix, then maxk operates along each column of A and B has K rows. For multidimensional arrays, maxk operates along the first non-singleton dimension. Elements are ranked by the instant they name, so an array with a time zone is ranked by absolute time rather than by its wall clock. B = maxk (A, K, dim) operates along the dimension specified by dim. K must be a nonnegative integer scalar. If K is larger than the number of elements along the operating dimension, then all of them are returned. Infinite datetimes are ranked as ordinary values. Missing elements (NaT) are not ranked. They are appended after the ranked elements in their original order, and hence they only appear in B when K exceeds the number of non-missing elements along the operating dimension. Unlike sort, maxk has no 'MissingPlacement' option, since NaT elements are always placed last. Elements comparing as equal keep their original order. [B, index] = maxk (A, ...) also returns an index array containing the indices of the returned elements of A along the operating dimension. # name: # type: sq_string # elements: 1 # length: 45 Find the latest elements in a datetime array. # name: # type: sq_string # elements: 1 # length: 13 datetime.mean # name: # type: sq_string # elements: 1 # length: 596 datetime: M = mean (A) datetime: M = mean (A, dim) datetime: M = mean (A, 'all') datetime: M = mean (..., nanflag) Mean of a datetime array. M = mean (A) returns the mean of the datetime array A as a scalar datetime, computed as the average of the absolute instants along the first non-singleton dimension. A dim or 'all' argument selects the dimension(s) to operate on. The result carries the Format and TimeZone of A. By default a Not-A-Time element makes the corresponding result NaT; pass 'omitnat' (equivalently 'omitmissing') to ignore missing values, or 'includenat' to keep the default. # name: # type: sq_string # elements: 1 # length: 25 Mean of a datetime array. # name: # type: sq_string # elements: 1 # length: 15 datetime.median # name: # type: sq_string # elements: 1 # length: 507 datetime: M = median (A) datetime: M = median (A, dim) datetime: M = median (A, 'all') datetime: M = median (..., nanflag) Median of a datetime array. M = median (A) returns the median of the datetime array A as a datetime, computed on the absolute instants along the first non-singleton dimension (for an even number of elements the average of the two middle instants). A dim or 'all' argument selects the dimension(s). The result carries the Format and TimeZone of A. Missing-value handling matches mean. # name: # type: sq_string # elements: 1 # length: 27 Median of a datetime array. # name: # type: sq_string # elements: 1 # length: 12 datetime.min # name: # type: sq_string # elements: 1 # length: 1077 datetime: M = min (A) datetime: M = min (A, B) datetime: M = min (A, [], dim) datetime: M = min (..., nanflag) datetime: M = min (A, [], 'all') datetime: [M, I] = min (...) Minimum of a datetime array. M = min (A) returns the smallest element of the datetime array A along its first non-singleton dimension. For a matrix, M is a row vector with the minimum of each column. Not-A-Time (NaT) elements are omitted; a reduction over NaT elements only yields NaT. M = min (A, B) returns an array the same size as A and B (after broadcasting) holding the element-wise minimum of the two datetime arrays. M = min (A, [], dim) operates along dimension dim. The empty second argument distinguishes this from the element-wise form. M = min (..., nanflag) sets the treatment of NaT: 'omitnan' (default) ignores NaT, while 'includenan' returns NaT whenever a NaT takes part in the comparison. M = min (A, [], 'all') returns the smallest element of the whole array. [M, I] = min (...) also returns the indices of the minima. A second output is not available for the element-wise form. # name: # type: sq_string # elements: 1 # length: 28 Minimum of a datetime array. # name: # type: sq_string # elements: 1 # length: 13 datetime.mink # name: # type: sq_string # elements: 1 # length: 1360 datetime: B = mink (A, K) datetime: B = mink (A, K, dim) datetime: [B, index] = mink (A, ...) Find the earliest elements in a datetime array. B = mink (A, K) returns the K earliest elements of the datetime array A in ascending order. If A is a vector, then B is a vector with K elements. If A is a matrix, then mink operates along each column of A and B has K rows. For multidimensional arrays, mink operates along the first non-singleton dimension. Elements are ranked by the instant they name, so an array with a time zone is ranked by absolute time rather than by its wall clock. B = mink (A, K, dim) operates along the dimension specified by dim. K must be a nonnegative integer scalar. If K is larger than the number of elements along the operating dimension, then all of them are returned. Infinite datetimes are ranked as ordinary values. Missing elements (NaT) are not ranked. They are appended after the ranked elements in their original order, and hence they only appear in B when K exceeds the number of non-missing elements along the operating dimension. Unlike sort, mink has no 'MissingPlacement' option, since NaT elements are always placed last. Elements comparing as equal keep their original order. [B, index] = mink (A, ...) also returns an index array containing the indices of the returned elements of A along the operating dimension. # name: # type: sq_string # elements: 1 # length: 47 Find the earliest elements in a datetime array. # name: # type: sq_string # elements: 1 # length: 14 datetime.minus # name: # type: sq_string # elements: 1 # length: 1310 datetime: C = minus (A, B) Subtraction for datetime arrays. C = minus (A, B) is the equivalent of the syntax C = A - B and supports the following operand combinations. datetime - datetime returns a duration array holding the elapsed time between the corresponding elements. Both operands must either both have a time zone or both be unzoned; a zoned difference is computed from the absolute instants, so the two time zones may differ. datetime - duration returns a datetime array shifted earlier by a fixed number of 24-hour days. For a zoned array the shift is applied to the absolute instant, so it is aware of daylight saving time transitions. datetime - calendarDuration returns a datetime array shifted earlier in calendar units. Whole months (and years) are applied first, clamping the day of month to the last day of the target month when necessary (e.g. 31 March minus one month is 28 February), then whole calendar days, and finally the time-of-day component as an instant. datetime - X, where X is a numeric or logical array, treats the elements of X as a number of fixed 24-hour days. A and B must be size compatible: they can be the same size, one can be scalar, or for every dimension their sizes must be equal or one of them must be 1. Not-A-Time and infinite elements propagate to the result. # name: # type: sq_string # elements: 1 # length: 32 Subtraction for datetime arrays. # name: # type: sq_string # elements: 1 # length: 15 datetime.minute # name: # type: sq_string # elements: 1 # length: 331 datetime: m = minute (T) Minute component of a datetime array. m = minute (T) returns the minute number for each element of the input datetime array T. The output m is a double array containing integer values in the range [0, 59] and it has the same size as T. Not-A-Time (NaT) values in T are returned as NaN in the output array. # name: # type: sq_string # elements: 1 # length: 37 Minute component of a datetime array. # name: # type: sq_string # elements: 1 # length: 13 datetime.mode # name: # type: sq_string # elements: 1 # length: 611 datetime: M = mode (A) datetime: M = mode (A, dim) datetime: M = mode (A, 'all') datetime: [M, F, C] = mode (...) Most frequent value of a datetime array. M = mode (A) returns the most frequently occurring datetime in A along the first non-singleton dimension; when several values are equally frequent the smallest is returned. A dim or 'all' argument selects the dimension(s). Not-A-Time elements are ignored. The result carries the Format and TimeZone of A. [M, F, C] = mode (...) also returns the frequency F of the modal value and a cell array C whose elements list all values that achieve that frequency. # name: # type: sq_string # elements: 1 # length: 40 Most frequent value of a datetime array. # name: # type: sq_string # elements: 1 # length: 14 datetime.month # name: # type: sq_string # elements: 1 # length: 815 datetime: M = month (T) datetime: M = month (T, monthType) Month component of a datetime array. M = month (T) returns the month number for each element of the input datetime array T. The output M is a double array containing integer values in the range [1, 12] and it has the same size as T. Not-A-Time (NaT) values in T are returned as NaN in the output array. M = month (T, monthType) returns the month number or name for each element of the input datetime array T as specified by monthType, which may have any of the following options: 'monthofyear' (default) returns the month number in a numeric array. 'name' returns the corresponding full name of the month in a cell array of character vectors. 'shortname' returns the corresponding 3-letter abbreviation of the month in a cell array of character vectors. # name: # type: sq_string # elements: 1 # length: 36 Month component of a datetime array. # name: # type: sq_string # elements: 1 # length: 14 datetime.ndims # name: # type: sq_string # elements: 1 # length: 142 datetime: out = ndims (T) Number of dimensions in a datetime array. out = ndims (T) returns the number of dimensions of the datetime array T. # name: # type: sq_string # elements: 1 # length: 41 Number of dimensions in a datetime array. # name: # type: sq_string # elements: 1 # length: 11 datetime.ne # name: # type: sq_string # elements: 1 # length: 604 datetime: TF = ne (A, B) Inequality comparison for datetime arrays. TF = ne (A, B) is the equivalent of the syntax TF = A != B and returns a logical array set to true where the corresponding elements of A and B are not the same point in time, and false otherwise. Both operands must be datetime arrays and either both have a time zone or both be unzoned; zoned arrays are compared by their absolute instants, so the two time zones may differ. A and B must be size compatible. Not-A-Time is never equal to anything, so it compares as true against everything, including another Not-A-Time, just like NaN. # name: # type: sq_string # elements: 1 # length: 42 Inequality comparison for datetime arrays. # name: # type: sq_string # elements: 1 # length: 14 datetime.numel # name: # type: sq_string # elements: 1 # length: 197 datetime: out = numel (T) Total number of elements in a datetime array. out = numel (T) returns the number of elements in the datetime array T, which is the product of the sizes of its dimensions. # name: # type: sq_string # elements: 1 # length: 45 Total number of elements in a datetime array. # name: # type: sq_string # elements: 1 # length: 16 datetime.permute # name: # type: sq_string # elements: 1 # length: 425 datetime: B = permute (A, dims) Generalized transpose for a datetime N-D array. B = permute (A, dims) returns the generalized transpose of the datetime array A by rearranging its dimensions according to the permutation vector specified in dims. dims must index all the dimensions 1:ndims (A) of the input array A, in any order, but only once. The Nth dimension of A gets remapped to the dimension in B specified by dims(N). # name: # type: sq_string # elements: 1 # length: 47 Generalized transpose for a datetime N-D array. # name: # type: sq_string # elements: 1 # length: 13 datetime.plus # name: # type: sq_string # elements: 1 # length: 1115 datetime: C = plus (A, B) Addition for datetime arrays. C = plus (A, B) is the equivalent of the syntax C = A + B and supports the following operand combinations, in either order. datetime + duration returns a datetime array shifted later by a fixed number of 24-hour days. For a zoned array the shift is applied to the absolute instant, so it is aware of daylight saving time transitions. datetime + calendarDuration returns a datetime array shifted later in calendar units. Whole months (and years) are applied first, clamping the day of month to the last day of the target month when necessary (e.g. 31 January plus one month is 28 February), then whole calendar days, and finally the time-of-day component as an instant. datetime + X, where X is a numeric or logical array, treats the elements of X as a number of fixed 24-hour days. Adding two datetime arrays is not defined and raises an error. A and B must be size compatible: they can be the same size, one can be scalar, or for every dimension their sizes must be equal or one of them must be 1. Not-A-Time and infinite elements propagate to the result. # name: # type: sq_string # elements: 1 # length: 29 Addition for datetime arrays. # name: # type: sq_string # elements: 1 # length: 18 datetime.posixtime # name: # type: sq_string # elements: 1 # length: 717 datetime: P = posixtime (T) Convert datetime array to POSIX time. P = posixtime (T) returns the number of seconds elapsed since the epoch 1970-01-01 00:00:00 UTC for each element of the input datetime array T. The output P is a double array of the same size as T and includes any fractional seconds. Datetime arrays without a time zone are treated as UTC. Not-A-Time (NaT) values are returned as NaN. POSIX time has no stamp of its own for an inserted leap second, so for a 'UTCLeapSeconds' array the 60th second of a minute shares the stamp of the second that follows it: posixtime of 2016-12-31T23:59:60Z and of 2017-01-01T00:00:00Z are both 1483228800. Every other conversion folds the other way (see convertTo). # name: # type: sq_string # elements: 1 # length: 37 Convert datetime array to POSIX time. # name: # type: sq_string # elements: 1 # length: 16 datetime.quarter # name: # type: sq_string # elements: 1 # length: 334 datetime: Q = quarter (T) Quarter component of a datetime array. Q = quarter (T) returns the quarter number for each element of the input datetime array T. The output Q is a double array containing integer values in the range [1, 4] and it has the same size as T. Not-A-Time (NaT) values in T are returned as NaN in the output array. # name: # type: sq_string # elements: 1 # length: 38 Quarter component of a datetime array. # name: # type: sq_string # elements: 1 # length: 16 datetime.repelem # name: # type: sq_string # elements: 1 # length: 922 datetime: B = repelem (A, n) datetime: B = repelem (A, d1, ..., dN) Repeat copies of datetime array elements. B = repelem (A, n) returns a datetime vector B containing repeated elements of the input A, which must be a datetime vector. If n is a scalar, each element of A is repeated n times along the non-singleton dimension of A. If n is a vector, it must have the same elements as A, in which case it specifies the number of times to repeat each corresponding element of A. B = repelem (A, d1, ..., dN) returns an array B with each element of A repeated according to the the list of input arguments d1, ..., dN each corresponding to a different dimension 1:ndims (A) of the input array A. d1, ..., dN must be either scalars or vectors with the same length as the corresponding dimension of A containing non-negative integer values specifying the number of repetitions of each element along the corresponding dimension. # name: # type: sq_string # elements: 1 # length: 41 Repeat copies of datetime array elements. # name: # type: sq_string # elements: 1 # length: 17 datetime.repelems # name: # type: sq_string # elements: 1 # length: 447 datetime: B = repelems (A, R) Construct a vector of repeated elements from a datetime array. B = repelems (A, R) returns a datetime vector B containing repeated elements of the input A, which must be a datetime vector. R must be a 2xN matrix of integers. Entries in the first row of R correspond to the linear indexing of the elements in A to be repeated. The corresponding entries in the second row of R specify the repeat count of each element. # name: # type: sq_string # elements: 1 # length: 62 Construct a vector of repeated elements from a datetime array. # name: # type: sq_string # elements: 1 # length: 15 datetime.repmat # name: # type: sq_string # elements: 1 # length: 562 datetime: B = repmat (A, n) datetime: B = repmat (A, d1, ..., dN) datetime: B = repmat (A, dimvec) Repeat copies of a datetime array. B = repmat (A, n) returns a datetime array B containing n copies of the input datetime array A along every dimension of A. B = repmat (A, d1, ..., dN) returns an array B containing copies of A along the dimensions specified by the list of scalar integer values d1, ..., dN, which specify how many copies of A are made in each dimension. B = repmat (A, dimvec) is equivalent to the previous syntax with dimvec = [d1, ..., dN]. # name: # type: sq_string # elements: 1 # length: 34 Repeat copies of a datetime array. # name: # type: sq_string # elements: 1 # length: 16 datetime.reshape # name: # type: sq_string # elements: 1 # length: 688 datetime: B = reshape (A, d1, ..., dN) datetime: B = reshape (A, ..., [], ...) datetime: B = reshape (A, dimvec) Reshape datetime array. B = reshape (A, d1, ..., dN) returns a datetime array B with specified dimensions d1, ..., dN, whose elements are taken columnwise from the datetime array A. The product of d1, ..., dN must equal the total number of elements in A. B = reshape (A, ..., [], ...) returns a datetime array B with one dimension unspecified which is calculated automatically so that the product of dimensions in B matches the total elements in A, which must be divisible the product of specified dimensions. An empty matrix ([]) is used to flag the unspecified dimension. # name: # type: sq_string # elements: 1 # length: 23 Reshape datetime array. # name: # type: sq_string # elements: 1 # length: 15 datetime.second # name: # type: sq_string # elements: 1 # length: 771 datetime: s = second (T) datetime: s = second (T, secondType) Seconds component of a datetime array. s = second (T) returns the number of seconds for each element of the input datetime array T. The output s is a double array containing values in the range [0, 60), including any fractional part of the second, and it has the same size as T. Not-A-Time (NaT) values in T are returned as NaN in the output array. s = second (T, secondType) returns the seconds for each element of the input datetime array T as specified by secondType, which may have any of the following options: 'secondofminute' (default) returns the second of the minute in a numeric array, in the range [0, 60). 'secondofday' returns the second of the day in a numeric array, in the range [1, 86400). # name: # type: sq_string # elements: 1 # length: 38 Seconds component of a datetime array. # name: # type: sq_string # elements: 1 # length: 26 datetime.setDefaultFormats # name: # type: sq_string # elements: 1 # length: 1575 datetime: datetime.setDefaultFormats ('default', fmt) datetime: datetime.setDefaultFormats ('defaultdate', fmt) datetime: datetime.setDefaultFormats ('reset') Set the default display formats of datetime arrays. A datetime array whose Format property was never set explicitly displays with one of two default formats, and this method chooses them. The choice between the two is made from the values: an array whose elements all sit at midnight uses the date-only default, and any other array uses the date-and-time default. The choice is made afresh on every display, so an array moves between the two when its components change. datetime.setDefaultFormats ('default', fmt) sets the date-and-time default to fmt, and datetime.setDefaultFormats ('defaultdate', fmt) sets the date-only default. fmt is a display format, given as a character vector or string scalar, and is validated as the Format property is. Arrays that already exist take the new format on their next display; those with an explicitly set Format are unaffected. datetime.setDefaultFormats ('reset') restores both factory formats, 'dd-MMM-uuuu HH:mm:ss' and 'dd-MMM-uuuu'. Both formats persist across Octave sessions. Deviation from MATLAB. MATLAB accepts a second argument after 'reset' and silently ignores it, resetting both defaults whatever it says, so datetime.setDefaultFormats ('reset', 'default') reads as a request to reset one and quietly resets two. Anything at all is accepted there, a number included. Here that is an error, since no correct program can depend on the argument being ignored. # name: # type: sq_string # elements: 1 # length: 51 Set the default display formats of datetime arrays. # name: # type: sq_string # elements: 1 # length: 16 datetime.setdiff # name: # type: sq_string # elements: 1 # length: 1145 datetime: C = setdiff (A, B) datetime: C = setdiff (A, B, 'rows') datetime: C = setdiff (..., order) datetime: [C, ixA] = setdiff (...) Set difference of two datetime arrays. C = setdiff (A, B) returns the unique datetime values in A that are not in B. Either input may instead be a date/time character vector, string array, or cell array of character vectors, which is promoted to a datetime array before the operation. Membership is decided on the absolute instant, so two zoned inputs may be in different time zones; the result carries the time zone and display format of A. NaT elements in A are all retained, as they never match an element of B. C is a row vector when both A and B are row vectors and a column vector otherwise. C = setdiff (A, B, 'rows') treats each row of the datetime matrices A and B, which must have the same number of columns, as a single element and returns the rows of A that are not rows of B. ... = setdiff (..., order) returns the values in 'sorted' order (the default) or in 'stable' order. [C, ixA] = setdiff (...) also returns an index vector ixA such that C = A(ixA), or C = A(ixA,:) when 'rows' is used. # name: # type: sq_string # elements: 1 # length: 38 Set difference of two datetime arrays. # name: # type: sq_string # elements: 1 # length: 15 datetime.setxor # name: # type: sq_string # elements: 1 # length: 1195 datetime: C = setxor (A, B) datetime: C = setxor (A, B, 'rows') datetime: C = setxor (..., order) datetime: [C, ixA, ixB] = setxor (...) Set exclusive-or of two datetime arrays. C = setxor (A, B) returns the unique datetime values that are in A or in B but not in both. Either input may instead be a date/time character vector, string array, or cell array of character vectors, which is promoted to a datetime array before the operation. Membership is decided on the absolute instant, so two zoned inputs may be in different time zones; the result carries the time zone and display format of A. Distinct NaT elements are all retained. C is a row vector when both A and B are row vectors and a column vector otherwise. C = setxor (A, B, 'rows') treats each row of the datetime matrices A and B, which must have the same number of columns, as a single element and returns the rows that are in one input but not both. ... = setxor (..., order) returns the values in 'sorted' order (the default) or in 'stable' order. [C, ixA, ixB] = setxor (...) also returns index vectors ixA and ixB such that C is the combination of A(ixA) and B(ixB), or the corresponding row selections when 'rows' is used. # name: # type: sq_string # elements: 1 # length: 40 Set exclusive-or of two datetime arrays. # name: # type: sq_string # elements: 1 # length: 13 datetime.size # name: # type: sq_string # elements: 1 # length: 1022 datetime: sz = size (T) datetime: dim_sz = size (T, dim) datetime: dim_sz = size (T, d1, d2, ...) datetime: [rows, columns, ..., dim_n_sz] = size (...) Size of a datetime array. sz = size (T) returns a row vector with the size (number of elements) of each dimension for the datetime array T. dim_sz = size (T, dim) returns the size of the corresponding dimension specified in dim. If dim is a vector, then dim_sz is a vector of the same length and with each element corresponding to a specified dimension. Multiple dimensions may also be specified as separate arguments. With a single output argument, size returns a row vector. With several, the size of dimension N is returned in the Nth argument. Asking for fewer output arguments than the array has dimensions folds the trailing dimensions into the last one, so [r, c] = size (T) on a 2-by-2-by-2 array returns 2 and 4; asking for more pads with ones. When a dimension is named, however, the number of output arguments must equal the number of dimensions requested. # name: # type: sq_string # elements: 1 # length: 25 Size of a datetime array. # name: # type: sq_string # elements: 1 # length: 13 datetime.sort # name: # type: sq_string # elements: 1 # length: 983 datetime: B = sort (A) datetime: B = sort (A, dim) datetime: B = sort (A, direction) datetime: B = sort (A, dim, direction) datetime: B = sort (..., 'MissingPlacement', mp) datetime: [B, I] = sort (...) Sort a datetime array. B = sort (A) returns the elements of the datetime array A sorted in ascending order along its first non-singleton dimension. For a matrix, each column is sorted independently. Sorting is stable: elements that compare as equal keep their original relative order. B = sort (A, dim) sorts along dimension dim. B = sort (A, direction) sorts in the order given by direction, which is either 'ascend' (default) or 'descend'. B = sort (..., 'MissingPlacement', mp) controls where Not-A-Time (NaT) elements are placed. mp may be 'auto' (default; NaT last for ascending order and first for descending order), 'first', or 'last'. [B, I] = sort (...) also returns an index array I of the same size as A such that B is A indexed by I along the operating dimension. # name: # type: sq_string # elements: 1 # length: 22 Sort a datetime array. # name: # type: sq_string # elements: 1 # length: 17 datetime.sortrows # name: # type: sq_string # elements: 1 # length: 1398 datetime: B = sortrows (A) datetime: B = sortrows (A, column) datetime: B = sortrows (A, direction) datetime: B = sortrows (A, column, direction) datetime: B = sortrows (..., 'MissingPlacement', mp) datetime: [B, index] = sortrows (...) Sort the rows of a datetime array. B = sortrows (A) sorts the rows of the 2-D datetime array A in ascending order. Rows are ordered lexicographically: by the first column, ties broken by the second column, and so on. The sort is stable, so rows that compare as equal keep their original order. B = sortrows (A, column) sorts using only the columns listed in column, in the given priority. A negative entry sorts the corresponding column in descending order. Columns not listed are not used as sort keys. B = sortrows (A, direction) sorts every column in the given direction, either 'ascend' (default) or 'descend'. direction may also be a cell array of strings with one such value per sort column. B = sortrows (A, column, direction) combines an explicit column list with a per-column direction. B = sortrows (..., 'MissingPlacement', mp) controls where Not-A-Time (NaT) elements are placed. mp may be 'auto' (default; NaT sorts as the largest value, i.e. last for ascending and first for descending columns), 'first', or 'last'. [B, index] = sortrows (...) also returns a column index vector index that maps the rows of A to B, such that B = A(index, :). # name: # type: sq_string # elements: 1 # length: 34 Sort the rows of a datetime array. # name: # type: sq_string # elements: 1 # length: 12 datetime.std # name: # type: sq_string # elements: 1 # length: 538 datetime: S = std (A) datetime: S = std (A, w) datetime: S = std (A, w, dim) datetime: S = std (A, w, 'all') datetime: S = std (..., nanflag) datetime: [S, M] = std (...) Standard deviation of a datetime array. S = std (A) returns the standard deviation of the absolute instants of A as a duration. The weight w selects the normalisation (0, the default, divides by N-1; 1 divides by N), and a dim or 'all' argument selects the dimension(s). Missing-value handling matches mean. [S, M] = std (...) also returns the mean M as a datetime. # name: # type: sq_string # elements: 1 # length: 39 Standard deviation of a datetime array. # name: # type: sq_string # elements: 1 # length: 15 datetime.string # name: # type: sq_string # elements: 1 # length: 437 datetime: S = string (T) datetime: S = string (T, fmt) Convert datetime array to a string array. S = string (T) converts the datetime array T to a string array S of the same size, formatting each element with the display format of T (its Format property). Not-A-Time (NaT) values become missing string elements. S = string (T, fmt) formats each element with the Unicode (LDML) format fmt instead of the Format property of T (see char). # name: # type: sq_string # elements: 1 # length: 41 Convert datetime array to a string array. # name: # type: sq_string # elements: 1 # length: 18 datetime.timeofday # name: # type: sq_string # elements: 1 # length: 481 datetime: D = timeofday (T) Elapsed time since midnight of a datetime array. D = timeofday (T) returns the elapsed time since midnight for each element of the input datetime array T as a duration array D of the same size as T. For datetime arrays with a time zone, the result accounts for any daylight saving time shift occurring since midnight, so it may differ from the displayed clock time on the day of a transition. Not-A-Time (NaT) values in T are returned as NaN durations. # name: # type: sq_string # elements: 1 # length: 48 Elapsed time since midnight of a datetime array. # name: # type: sq_string # elements: 1 # length: 17 datetime.topkrows # name: # type: sq_string # elements: 1 # length: 2117 datetime: B = topkrows (A, K) datetime: B = topkrows (A, K, col) datetime: B = topkrows (A, K, direction) datetime: B = topkrows (A, K, col, direction) datetime: B = topkrows (..., 'MissingPlacement', MP) datetime: [B, index] = topkrows (A, ...) Top K sorted rows of a datetime array. B = topkrows (A, K) returns the top K rows of the 2-D datetime array A sorted in descending order as a group. K must be a nonnegative integer scalar. If K is larger than the number of rows in A, then all of them are returned. Missing elements (NaT) are not ranked. Within each sort column they are placed after the elements that are defined, whichever direction is asked for, so a row is demoted only by a missing element in a column that actually decides its position. Rows comparing as equal keep their original order. B = topkrows (A, K, col) sorts using only the columns listed in the numeric vector col, which must contain positive integers indexing existing columns in A. Columns are used as sort keys in the order given, and those not listed are not used at all. The direction is descending unless direction says otherwise. B = topkrows (A, K, direction) sorts in the given direction, either 'descend' (default) or 'ascend' applying to all columns in A. Alternatively, direction can be a cell array of character vectors specifying the sorting direction for each individual column of A. B = topkrows (A, K, col, direction) combines an explicit column list with a per-column direction. B = topkrows (..., 'MissingPlacement', MP) specifies where the missing elements are placed within each sort column, with any of the following options specified in MP: 'last', which is the default, places missing elements last whichever direction is asked for. 'first' places missing elements first. 'auto' places missing elements last for an ascending sort and first for a descending one, as sortrows does. This is an Octave extension: MATLAB has no such option here and always ranks as 'last' does. [B, index] = topkrows (A, ...) also returns an index vector containing the original row indices of A in B, such that B = A(index,:). # name: # type: sq_string # elements: 1 # length: 38 Top K sorted rows of a datetime array. # name: # type: sq_string # elements: 1 # length: 18 datetime.transpose # name: # type: sq_string # elements: 1 # length: 167 datetime: B = transpose (A) Transpose a datetime matrix. B = transpose (A) is the equivalent of the syntax B = A.' and returns the transpose of the datetime matrix A. # name: # type: sq_string # elements: 1 # length: 28 Transpose a datetime matrix. # name: # type: sq_string # elements: 1 # length: 17 datetime.tzoffset # name: # type: sq_string # elements: 1 # length: 886 datetime: DT = tzoffset (T) datetime: [DT, DST] = tzoffset (T) Time zone offset of a datetime array. DT = tzoffset (T) returns the offset from UTC of each element of the input datetime array T as a duration array DT of the same size as T. The offset is positive for time zones east of UTC and includes the additional hour when daylight saving time is in effect. If T has no time zone, or for Not-A-Time (NaT) and infinite values, the corresponding offset is NaN. [DT, DST] = tzoffset (T) also returns the daylight saving time part of each offset as a duration array DST: zero in standard time and in a time zone without daylight saving time, and NaN wherever DT is. Where the IANA database writes the winter of a time zone as a negative saving, as for 'Europe/Dublin' or 'Africa/Casablanca', the other half of the year is counted as the daylight saving time instead, and isdst agrees. # name: # type: sq_string # elements: 1 # length: 37 Time zone offset of a datetime array. # name: # type: sq_string # elements: 1 # length: 14 datetime.union # name: # type: sq_string # elements: 1 # length: 1150 datetime: C = union (A, B) datetime: C = union (A, B, 'rows') datetime: C = union (..., order) datetime: [C, ixA, ixB] = union (...) Set union of two datetime arrays. C = union (A, B) returns the unique datetime values present in either A or B. Either input may instead be a date/time character vector, string array, or cell array of character vectors, which is promoted to a datetime array before the operation. Membership is decided on the absolute instant, so two zoned inputs may be in different time zones; the result carries the time zone and display format of A. Distinct NaT elements are all retained. C is a row vector when both A and B are row vectors and a column vector otherwise. C = union (A, B, 'rows') treats each row of the datetime matrices A and B, which must have the same number of columns, as a single element and returns their combined unique rows. ... = union (..., order) returns the values in 'sorted' order (the default) or in 'stable' order. [C, ixA, ixB] = union (...) also returns index vectors ixA and ixB such that C is the combination of A(ixA) and B(ixB), or the corresponding row selections when 'rows' is used. # name: # type: sq_string # elements: 1 # length: 33 Set union of two datetime arrays. # name: # type: sq_string # elements: 1 # length: 15 datetime.unique # name: # type: sq_string # elements: 1 # length: 1796 datetime: B = unique (A) datetime: B = unique (A, setOrder) datetime: B = unique (A, occurrence) datetime: B = unique (A, setOrder, occurrence) datetime: B = unique (A, occurrence, setOrder) datetime: B = unique (A, ..., 'rows') datetime: [B, ixA, ixB] = unique (...) Unique values in a datetime array. B = unique (A) returns the unique values of the datetime array A in sorted order. B = unique (A, setOrder) returns the unique values of the datetime array A in an order as specified by setOrder, which can be either of the following values: 'sorted' (default) returns the unique values sorted in ascending order. 'stable' returns the unique values according to their order of occurrence. B = unique (A, occurrence) returns the unique values of the datetime array tblA according to their order of occurrence. occurrence can be either of the following values: 'first' (default) returns the first occurrence of each unique value, i.e. the lowest possible indices are returned. 'last' returns the last occurrence of each unique value, i.e. the highest possible indices are returned. You can specify setOrder and occurrence arguments together. B = unique (A, ..., 'rows') returns the unique rows of A by treating each row as a single entity. The 'rows' option can be used alone or in any combination with the setOrder and occurrence arguments. 'rows' can be placed at any position in the function's argument list after the input array A. However, this syntax is only valid for 2-dimensional datetime arrays. [tblB, ixA, ixB] = unique (...) also returns index vectors ixA and ixB using any of the previous syntaxes. ixA and ixB map the arrays A and B to one another such that B = A(ixA) and A = B(ixB). When the 'rows' optional argument is specified, then B = A(ixA,:) and tblA = tblB(ixB,:). # name: # type: sq_string # elements: 1 # length: 34 Unique values in a datetime array. # name: # type: sq_string # elements: 1 # length: 16 datetime.vertcat # name: # type: sq_string # elements: 1 # length: 339 datetime: C = vertcat (A, B, ...) Vertical concatenation of datetime arrays. C = vertcat (A, B, ... is the equivalent of the syntax B = [A; B; ...] and vertically concatenates the inputs A, B, ..., which must have the same size except along the first dimension. See cat for which inputs are accepted and how their time zones are resolved. # name: # type: sq_string # elements: 1 # length: 42 Vertical concatenation of datetime arrays. # name: # type: sq_string # elements: 1 # length: 13 datetime.week # name: # type: sq_string # elements: 1 # length: 798 datetime: W = week (T) datetime: W = week (T, weekType) Week component of a datetime array. W = week (T) returns the week-of-year number for each element of the input datetime array T. The output W is a double array containing integer values in the range [1, 54] and it has the same size as T. Weeks are counted from Sunday to Saturday, and the week containing January 1 is week 1. Not-A-Time (NaT) values in T are returned as NaN in the output array. W = week (T, weekType) returns the week number for each element of the input datetime array T as specified by weekType, which may have any of the following options: 'weekofyear' (default) returns the week of the year in a numeric array, in the range [1, 54]. 'weekofmonth' returns the week of the month in a numeric array, in the range [1, 6]. # name: # type: sq_string # elements: 1 # length: 35 Week component of a datetime array. # name: # type: sq_string # elements: 1 # length: 13 datetime.year # name: # type: sq_string # elements: 1 # length: 276 datetime: Y = year (T) Year component of a datetime array. Y = year (T) returns the year number for each element of the input datetime array T. The output Y is a double array and it has the same size as T. Not-A-Time (NaT) values in T are returned as NaN in the output array. # name: # type: sq_string # elements: 1 # length: 35 Year component of a datetime array. # name: # type: sq_string # elements: 1 # length: 12 datetime.ymd # name: # type: sq_string # elements: 1 # length: 346 datetime: [Y, M, D] = ymd (T) Year, Month, and Day components of datetime array. [Y, M, D] = ymd (T) returns the year, month, and day components of the corresponding datetime values in T as separate numeric arrays. Y, M, D contain integer values and have the same size as T. Not-A-Time (NaT) values in T are returned as NaN in the output arrays. # name: # type: sq_string # elements: 1 # length: 50 Year, Month, and Day components of datetime array. # name: # type: sq_string # elements: 1 # length: 17 datetime.yyyymmdd # name: # type: sq_string # elements: 1 # length: 319 datetime: D = yyyymmdd (T) Convert datetime array to YYYYMMDD numeric form. D = yyyymmdd (T) returns a double array D of the same size as T in which each element encodes the date of the corresponding datetime as year * 10000 + month * 100 + day. The time of day is ignored. Not-A-Time (NaT) values are returned as NaN. # name: # type: sq_string # elements: 1 # length: 48 Convert datetime array to YYYYMMDD numeric form. # name: # type: sq_string # elements: 1 # length: 22 datetime.zoneNameStyle # name: # type: sq_string # elements: 1 # length: 1018 datetime: style = datetime.zoneNameStyle () datetime: datetime.zoneNameStyle (style) Query or set the session-wide style used to render time-zone names. The z, zz, and zzz fields of a display Format render a time-zone name. Two styles are available, selected by this session-wide preference: 'iana' (default) The IANA time-zone database abbreviation active at each instant, such as EDT, EEST, JST, or UTC. This is an Octave-specific extension: a named abbreviation is shown for every zone. 'matlab' MATLAB-compatible rendering: a named abbreviation is shown only for the North American zones plus GMT and UTC; every other zone renders as a numeric UTC offset such as UTC+3 or UTC+5:30. style = datetime.zoneNameStyle () returns the current style. datetime.zoneNameStyle (style) sets it to style, either 'iana' or 'matlab'. The setting persists for the current Octave session. An individual Format string can override the session style with the Octave-specific fields zzzz (force 'iana') and zzzzz (force 'matlab'). # name: # type: sq_string # elements: 1 # length: 67 Query or set the session-wide style used to render time-zone names. # name: # type: sq_string # elements: 1 # length: 4 days # name: # type: sq_string # elements: 1 # length: 380 datatypes: D = days (X) Fixed-time duration in days. D = days (X) returns a duration array representing fixed-time duration days equivalent to the values in X, which must be a numeric array. days is also available as a method for duration arrays, in which case it performs the opposite conversion. See also: duration, years, hours, minutes, seconds, milliseconds, duration.days # name: # type: sq_string # elements: 1 # length: 28 Fixed-time duration in days. # name: # type: sq_string # elements: 1 # length: 10 dictionary # name: # type: sq_string # elements: 1 # length: 1907 datatypes: dictionary Map of unique keys to values. A dictionary object holds entries, each a key and the value it maps to. Every key is of one type and every value of one type, the key type and the value type of the dictionary; two keys are the same key when keyMatch says so. A dictionary with no entries may have no key and value types yet, when it is unconfigured; the first entries added configure it, and configureDictionary creates one configured with no entries. dictionary is a value class, and a dictionary is always a scalar object: numel, size and isempty describe the object, not its entries, which numEntries counts. Dictionaries cannot be concatenated. Entries are reached by indexing with keys, converted to the key type as dictionary converts them: d(keys) returns the values of keys, an array of keys, in an array of the same shape; a key not in d is an error. d(keys) = values adds entries, or replaces the values of keys already present, which keep their positions; values has the size of keys, or is one value for all. An unconfigured dictionary takes its types from the first entries. d(keys) = [] removes the entries of keys, ignoring keys not in d. A key removed and added again goes after the others. d{keys}, for a dictionary of 'cell' values, returns the contents of their cells as a comma-separated list, and d{key} = value stores value in a cell under the single key key, [] included. d.method (...) calls a method, as d.keys (). Indexing can continue into a value, as in d(key).x or d{key}(2) = 5; assigning into the contents of a new key starts from empty. end is not supported. Octave cannot tell d(:) from d(':'), so ':' is the key ":", as it is in MATLAB in an assignment; use keys or values for every entry. jsonencode refuses a dictionary, as in MATLAB, and writedictionary writes one to a JSON file. See also: configureDictionary, isdictionary, keyHash, keyMatch # name: # type: sq_string # elements: 1 # length: 29 Map of unique keys to values. # name: # type: sq_string # elements: 1 # length: 14 dictionary.cat # name: # type: sq_string # elements: 1 # length: 243 dictionary: cat (dim, d1, d2, ...) Refuse to concatenate: a dictionary is a scalar object. cat (dim, d1, d2, ...) raises an error, whatever the dimension and the other arguments, as in MATLAB. See also: dictionary.horzcat, dictionary.vertcat # name: # type: sq_string # elements: 1 # length: 55 Refuse to concatenate: a dictionary is a scalar object. # name: # type: sq_string # elements: 1 # length: 21 dictionary.dictionary # name: # type: sq_string # elements: 1 # length: 2617 dictionary: d = dictionary () dictionary: d = dictionary (keys, values) dictionary: d = dictionary (k1, v1, ..., kN, vN) Create a dictionary object. d = dictionary () returns an unconfigured dictionary, with no entries and no key and value types. d = dictionary (keys, values) returns a dictionary mapping each element of the array keys to the element of values in the same position, in that order. The two must be of the same size, or values a single value given to every key; an empty keys gives a configured dictionary with no entries. A key repeated keeps its first position and takes its last value. The class of keys is the key type and that of values the value type, except that a character array is a string array of its rows. The key type must be one that keyHash can hash: a built-in type or a class with a keyHash method; a cell array is a key type of its own, each cell one key. An array of objects holds one key or value per object, but a table or timetable of any size is one value, where MATLAB accepts only a 1-by-1 one. d = dictionary (k1, v1, ..., kN, vN) adds each pair of keys and values in turn, the first pair setting the key and value types and later pairs converted to them. Keys and values of another class are converted by the function of the type's name, as in MATLAB: string (x), double (x), categorical (x) and so on, and refused where that fails or gives another class. So 5 becomes "5" as a string, "3.5" becomes 3.5 as a double but is refused by single and the integer types, a character array gives its character codes as a number, a categorical its code, and missing gives NaN, NaT or an undefined category. The exceptions: a 'cell' type takes only cell arrays, which are never wrapped, as in MATLAB; a 'struct' type takes only structures, where MATLAB turns an object into a structure of its private properties; a 'table' type makes a table of the variable Var1 and refuses character arrays, as in MATLAB; a 'logical' type takes a character as true for a nonzero code, as in MATLAB; a 'datetime' type reads dates given as text in the time zone of the dictionary, and moves a key with a time zone to that of the keys; a value keeps its own. A date with a time zone and one without are different keys, so that looking up one as the other finds nothing, and keys cannot join a dictionary holding both. Dates given as text to a dictionary whose keys have a time zone are read in that zone, so that they find its keys; MATLAB reads them without one, a key that no date with a zone matches. See also: configureDictionary, dictionary.isConfigured, dictionary.keys, keyHash, keyMatch # name: # type: sq_string # elements: 1 # length: 27 Create a dictionary object. # name: # type: sq_string # elements: 1 # length: 22 dictionary.dispstrings # name: # type: sq_string # elements: 1 # length: 491 dictionary: cstr = dispstrings (d) Return the line that describes a dictionary. cstr = dispstrings (d) returns a 1-by-1 cell array holding the line that describes d by its key and value types and its number of entries, as 'dictionary (string ⟼ double) with 2 entries', or 'dictionary with unset key and value types' while it is unconfigured. A cell array displays a dictionary by this line, and so does a dictionary holding dictionaries. See also: dictionary.types, dictionary.numEntries # name: # type: sq_string # elements: 1 # length: 44 Return the line that describes a dictionary. # name: # type: sq_string # elements: 1 # length: 18 dictionary.entries # name: # type: sq_string # elements: 1 # length: 765 dictionary: e = entries (d) dictionary: e = entries (d, format) Return the keys and values together. e = entries (d) returns a table with the variables Key and Value, one row per entry in the order the keys were added: the columns keys (d) and values (d) return. Keys or values that a table cannot hold, function handles, dictionaries or values that cannot be joined, are refused. e = entries (d, format) returns the table above for format 'table', the default; for 'struct' a column structure array with the fields Key and Value, one element per entry; and for 'cell' a cell array of two columns, each key and value in a cell of its own. format is matched in any case and by its first letters. d must be configured. See also: dictionary.keys, dictionary.values # name: # type: sq_string # elements: 1 # length: 36 Return the keys and values together. # name: # type: sq_string # elements: 1 # length: 18 dictionary.horzcat # name: # type: sq_string # elements: 1 # length: 274 dictionary: horzcat (d1, d2, ...) Refuse to concatenate: a dictionary is a scalar object. [d1, d2] raises an error, whatever the other arguments, as in MATLAB. Add the entries of one dictionary to another by assignment instead. See also: dictionary.vertcat, dictionary.cat # name: # type: sq_string # elements: 1 # length: 55 Refuse to concatenate: a dictionary is a scalar object. # name: # type: sq_string # elements: 1 # length: 17 dictionary.insert # name: # type: sq_string # elements: 1 # length: 762 dictionary: d = insert (d, keys, values) dictionary: d = insert (..., 'Overwrite', tf) Add entries. d = insert (d, keys, values) adds the entries of keys and values to d, as d(keys) = values does: values has the size of keys, or is one value for all, and both are converted to the types of d, which an unconfigured dictionary takes from them. A key already present has its value replaced and keeps its position. d = insert (..., 'Overwrite', tf) with tf false leaves the value of a key already present unchanged, and a key repeated in keys takes its first value; it still adds the keys not yet present. tf is a logical or numeric scalar, true by default. The option name is matched in any case. See also: dictionary.remove, dictionary.lookup, dictionary.isKey # name: # type: sq_string # elements: 1 # length: 12 Add entries. # name: # type: sq_string # elements: 1 # length: 23 dictionary.isConfigured # name: # type: sq_string # elements: 1 # length: 288 dictionary: tf = isConfigured (d) True for a dictionary with key and value types. tf = isConfigured (d) returns false for an unconfigured dictionary, which has no entries and no key and value types yet, and true otherwise, entries or not. See also: dictionary.types, configureDictionary # name: # type: sq_string # elements: 1 # length: 47 True for a dictionary with key and value types. # name: # type: sq_string # elements: 1 # length: 16 dictionary.isKey # name: # type: sq_string # elements: 1 # length: 456 dictionary: tf = isKey (d, keys) True for each of the keys that is in the dictionary. tf = isKey (d, keys) returns a logical array of the size of keys, true where the element of keys is a key of d. The keys are first converted to the key type of d, as dictionary converts them, so that isKey (d, 1) on a dictionary of string keys asks for the key "1"; a class that cannot be converted is refused. d must be configured. See also: dictionary.keys, keyMatch # name: # type: sq_string # elements: 1 # length: 52 True for each of the keys that is in the dictionary. # name: # type: sq_string # elements: 1 # length: 18 dictionary.isempty # name: # type: sq_string # elements: 1 # length: 300 dictionary: tf = isempty (d) Return false: a dictionary is never an empty array. tf = isempty (d) is false even for a dictionary with no entries, since the object itself is a scalar; test numEntries (d) == 0 for a dictionary with no entries. See also: dictionary.numEntries, dictionary.isConfigured # name: # type: sq_string # elements: 1 # length: 51 Return false: a dictionary is never an empty array. # name: # type: sq_string # elements: 1 # length: 18 dictionary.isequal # name: # type: sq_string # elements: 1 # length: 588 dictionary: tf = isequal (d1, d2, ...) True when dictionaries hold the same entries. tf = isequal (d1, d2, ...) returns true when every argument is a dictionary with the same keys, in any order, each mapped to values that isequal finds equal, so that NaN values are unequal and values of different classes may be equal. Keys of different classes are different keys. Two unconfigured dictionaries are equal, an unconfigured and a configured one are not, and two configured dictionaries with no entries are equal whatever their types, as in MATLAB. See also: dictionary.isequaln, keyMatch # name: # type: sq_string # elements: 1 # length: 45 True when dictionaries hold the same entries. # name: # type: sq_string # elements: 1 # length: 19 dictionary.isequaln # name: # type: sq_string # elements: 1 # length: 279 dictionary: tf = isequaln (d1, d2, ...) True when dictionaries hold the same entries, NaN values equal. tf = isequaln (d1, d2, ...) is isequal with values compared by isequaln, so that NaN and other missing values are equal to themselves. See also: dictionary.isequal, keyMatch # name: # type: sq_string # elements: 1 # length: 63 True when dictionaries hold the same entries, NaN values equal. # name: # type: sq_string # elements: 1 # length: 18 dictionary.keyHash # name: # type: sq_string # elements: 1 # length: 1033 dictionary: key = keyHash (d) dictionary: key = keyHash (d, base) Generate a hash code for a dictionary. key = keyHash (d) returns a 64-bit hash code of the dictionary d as a uint64 scalar, from its key and value types and its entries in the order they were added, so that a dictionary can be a key of another. Unlike isequal, the code, and with it keyMatch, depends on the order of the entries and on the types, as in MATLAB. A dictionary whose keys or values keyHash cannot hash, such as tables, is refused. key = keyHash (d, base) also generates a 64-bit hash code using base as the offset basis for the FNV-1a hash algorithm. base must be a uint64 integer type scalar. Use this syntax to cascade keyHash on multiple objects for which a single hash code is required. Note that unlike MATLAB, this implementation does not use any random seed. As a result, keyHash will always generate the exact same hash key for any particular input across different workers and Octave sessions. See also: keyHash, keyMatch, dictionary.isequal # name: # type: sq_string # elements: 1 # length: 38 Generate a hash code for a dictionary. # name: # type: sq_string # elements: 1 # length: 15 dictionary.keys # name: # type: sq_string # elements: 1 # length: 594 dictionary: k = keys (d) dictionary: k = keys (d, format) Return the keys. k = keys (d) returns the keys of d as a column array of the key type, in the order they were added. Keys of a type that cannot be joined into one array, such as function handles, are refused unless there is exactly one. k = keys (d, format) returns them as above for format 'uniform', the default, and as a column cell array, each key in a cell of its own, for 'cell'. format is matched in any case and by its first letters. d must be configured. See also: dictionary.values, dictionary.isKey, dictionary.numEntries # name: # type: sq_string # elements: 1 # length: 16 Return the keys. # name: # type: sq_string # elements: 1 # length: 17 dictionary.length # name: # type: sq_string # elements: 1 # length: 200 dictionary: n = length (d) Return 1, the length of a scalar object. n = length (d) is always 1, however many entries d holds; numEntries counts them. See also: dictionary.numEntries, dictionary.size # name: # type: sq_string # elements: 1 # length: 40 Return 1, the length of a scalar object. # name: # type: sq_string # elements: 1 # length: 17 dictionary.lookup # name: # type: sq_string # elements: 1 # length: 640 dictionary: v = lookup (d, keys) dictionary: v = lookup (..., 'FallbackValue', fallback) Return the values of keys. v = lookup (d, keys) returns the values of keys, an array of keys, in an array of the same shape, as d(keys) does; a key not in d is an error. v = lookup (..., 'FallbackValue', fallback) returns fallback for each key not in d instead. fallback is a single value, converted to the value type of d, so that a dictionary of 'cell' values needs a cell; it is checked only when some key is missing. The option name is matched in any case. d must be configured. See also: dictionary.isKey, dictionary.insert, dictionary.values # name: # type: sq_string # elements: 1 # length: 26 Return the values of keys. # name: # type: sq_string # elements: 1 # length: 16 dictionary.ndims # name: # type: sq_string # elements: 1 # length: 107 dictionary: n = ndims (d) Return 2, the number of dimensions of a scalar object. See also: dictionary.size # name: # type: sq_string # elements: 1 # length: 54 Return 2, the number of dimensions of a scalar object. # name: # type: sq_string # elements: 1 # length: 21 dictionary.numEntries # name: # type: sq_string # elements: 1 # length: 190 dictionary: n = numEntries (d) Return the number of entries. n = numEntries (d) returns the number of entries in d as a double, 0 for an unconfigured dictionary. See also: dictionary.numel # name: # type: sq_string # elements: 1 # length: 29 Return the number of entries. # name: # type: sq_string # elements: 1 # length: 16 dictionary.numel # name: # type: sq_string # elements: 1 # length: 210 dictionary: n = numel (d) Return 1, the number of elements of a scalar object. n = numel (d) is always 1, however many entries d holds; numEntries counts them. See also: dictionary.numEntries, dictionary.size # name: # type: sq_string # elements: 1 # length: 52 Return 1, the number of elements of a scalar object. # name: # type: sq_string # elements: 1 # length: 17 dictionary.remove # name: # type: sq_string # elements: 1 # length: 319 dictionary: d = remove (d, keys) Remove entries. d = remove (d, keys) removes the entries of keys, an array of keys converted to the key type of d, as d(keys) = [] does. A key not in d is ignored. A key removed and added again goes after the others. d must be configured. See also: dictionary.insert, dictionary.isKey # name: # type: sq_string # elements: 1 # length: 15 Remove entries. # name: # type: sq_string # elements: 1 # length: 15 dictionary.size # name: # type: sq_string # elements: 1 # length: 494 dictionary: sz = size (d) dictionary: dim_sz = size (d, dim) dictionary: dim_sz = size (d, d1, d2, ...) dictionary: [rows, cols, ...] = size (d) Return the size of a scalar object. sz = size (d) is always [1, 1], however many entries d holds. size (d, dim) returns 1 for each dimension in dim, a positive integer or a vector of them, and size (d, d1, d2, ...) returns 1 for each dimension given. With several outputs each takes one dimension. See also: dictionary.numel, dictionary.numEntries # name: # type: sq_string # elements: 1 # length: 35 Return the size of a scalar object. # name: # type: sq_string # elements: 1 # length: 16 dictionary.types # name: # type: sq_string # elements: 1 # length: 325 dictionary: [keyType, valueType] = types (d) dictionary: keyType = types (d) Return the key and value types. [keyType, valueType] = types (d) returns the class names of the keys and of the values of d, each a string scalar, or a missing string while d is unconfigured. See also: dictionary.isConfigured, configureDictionary # name: # type: sq_string # elements: 1 # length: 31 Return the key and value types. # name: # type: sq_string # elements: 1 # length: 17 dictionary.values # name: # type: sq_string # elements: 1 # length: 721 dictionary: v = values (d) dictionary: v = values (d, format) Return the values. v = values (d) returns the values of d as a column array of the value type, in the order their keys were added. Values that cannot be joined into one array, such as function handles, dictionaries or structures with different fields, are refused unless there is exactly one. Tables are joined only when each has one row, so that row i is the value of key i. v = values (d, format) returns them as above for format 'uniform', the default, and as a column cell array, each value in a cell of its own, for 'cell'. format is matched in any case and by its first letters. d must be configured. See also: dictionary.keys, dictionary.numEntries # name: # type: sq_string # elements: 1 # length: 18 Return the values. # name: # type: sq_string # elements: 1 # length: 18 dictionary.vertcat # name: # type: sq_string # elements: 1 # length: 206 dictionary: vertcat (d1, d2, ...) Refuse to concatenate: a dictionary is a scalar object. [d1; d2] raises an error, whatever the other arguments, as in MATLAB. See also: dictionary.horzcat, dictionary.cat # name: # type: sq_string # elements: 1 # length: 55 Refuse to concatenate: a dictionary is a scalar object. # name: # type: sq_string # elements: 1 # length: 26 dictionary.writedictionary # name: # type: sq_string # elements: 1 # length: 2400 dictionary: writedictionary (d, filename) dictionary: writedictionary (d, filename, Name, Value) Write a dictionary to a JSON or an OpenDocument spreadsheet file. writedictionary (d, filename) writes d as a JSON object to the file filename, a character vector or a string scalar, replacing any file of that name. Each entry is a member, in the order of the keys, written as follows: keys as text: numbers in their shortest form ("1.5"), logical values as "true" and "false", dates and durations as they display; a double or single in the fewest digits that read back as its value, always with a point (1.0), with an exponent only from 10^21 up and below 10^-6; integers exactly; NaN and Inf as NaN, Infinity and -Infinity, or as null under 'PreserveInfAndNaN' false; missing strings, NaT and missing as null; logical values as true and false; dates and durations as text, as they display; a dictionary as an object, nested ones too; the contents of a cell as the value they hold, a vector of them as an array, and an empty one as []. writedictionary (d, filename, Name, Value) specifies options with the following Name-Value paired arguments, matched in any case. Name Value 'FileType' 'auto' (the default) tells the type from the extension, .json, .ods or .fods; 'json' or 'ods' sets it. 'PrettyPrint' A logical scalar: true (the default) indents by four spaces, one member or element a line; false writes no spaces or line breaks. 'PreserveInfAndNaN' A logical scalar: true (the default) writes NaN and Inf, false writes null. Nothing is written when any key or value cannot be. Values of types other than string, the numeric types, logical, datetime, duration, missing, dictionary and cell are refused, as in MATLAB, and so are matrices in cells and complex values, on which MATLAB fails; keys of types other than string, a cell array of text, the numeric types, logical, datetime and duration are refused, and so are missing string keys. An OpenDocument spreadsheet, FileType 'ods', holds one sheet per entry, named by the key and holding its table or timetable, written by struct2ods, so that readdictionary reads it back as it was. The keys must be text and the values tables or timetables, in a cell or not. Writing ODS is an extension of this package; it takes no option but 'FileType'. Remote locations, which MATLAB writes to, are not supported. See also: readdictionary, struct2ods # name: # type: sq_string # elements: 1 # length: 65 Write a dictionary to a JSON or an OpenDocument spreadsheet file. # name: # type: sq_string # elements: 1 # length: 10 discretize # name: # type: sq_string # elements: 1 # length: 2581 datatypes: bin = discretize (X, edges) datatypes: bin = discretize (X, N) datatypes: Y = discretize (..., values) datatypes: C = discretize (..., 'categorical') datatypes: C = discretize (..., 'categorical', names) datatypes: Y = discretize (..., 'IncludedEdge', side) datatypes: [bin, edges] = discretize (...) Group data into bins or categories. bin = discretize (X, edges) returns an array of the same size as X whose elements give the index of the bin that each value of X falls into. edges must be a real numeric or logical vector of monotonically non-decreasing values, and defines numel (edges) - 1 bins. Bin j covers the half-open interval [edges(j), edges(j+1)), except the last bin, which is closed at both ends. Values outside [edges(1), edges(end)], and any NaN, return NaN. bin is always of type double. Repeated edges are permitted and meaningful: they define empty bins, which are simply never selected. discretize ([1, 2, 3], [1, 2, 2, 3]) returns [1, 3, 3]. bin = discretize (X, N) uses N bins of uniform width spanning the range of X, where N is a positive integer scalar. The edges are placed at "nice" decimal positions rather than exactly at min (X) and max (X), so the bins generally extend slightly beyond the data. NaN and infinite values are ignored when determining the range. This syntax is not available when X is of an integer type; supply explicit edges instead. Y = discretize (..., values) returns the corresponding element of values in place of the bin index, so values must be a vector whose length equals the number of bins. Y takes the type of values. Elements of X that fall in no bin return NaN when values is a floating-point array, zero when it is of an integer type, and raise an error when it is a cell array. C = discretize (..., 'categorical') returns an ordinal categorical array whose categories are named after the bin intervals, for example '[1, 3)'. C = discretize (..., 'categorical', names) names the categories explicitly; names must be a cell array of character vectors or a string vector whose length equals the number of bins. Y = discretize (..., 'IncludedEdge', side) selects which end of each bin is closed. side may be 'left' (the default, giving [edges(j), edges(j+1)) with the last bin closed at both ends) or 'right' (giving (edges(j), edges(j+1)] with the first bin closed at both ends). [bin, edges] = discretize (...) also returns the bin edges used. When the edges were supplied they are returned unchanged; when a bin count was requested they are returned as a row vector. See also: histcounts, categorical # name: # type: sq_string # elements: 1 # length: 35 Group data into bins or categories. # name: # type: sq_string # elements: 1 # length: 8 duration # name: # type: sq_string # elements: 1 # length: 811 datatypes: duration Array representing durations of time using fixed-length time units. duration values are stored internally as double type array representing numbers of elapsed days as a fixed-length time unit. By default, fractional seconds of duration values are not displayed, but their actual precision is closer to nanoseconds for typical time lengths. duration arrays can be created through their constructor by combining numeric arrays representing individual fixed-length elapsed time units or through the functions years, days, hours, minutes, seconds, and calyears, which create fixed-length durations in terms of a single duration units. These functions are also available as methods of duration arrays to extract individual duration units as numeric arrays. See also: calendarDuration, datetime # name: # type: sq_string # elements: 1 # length: 67 Array representing durations of time using fixed-length time units. # name: # type: sq_string # elements: 1 # length: 15 duration.Format # name: # type: sq_string # elements: 1 # length: 201 duration: property Format Display format Display format, specified as a character vector or string scalar. If specified as a string scalar, it is converted and stored internally as a character vector. # name: # type: sq_string # elements: 1 # length: 14 Display format # name: # type: sq_string # elements: 1 # length: 12 duration.abs # name: # type: sq_string # elements: 1 # length: 224 duration: B = abs (A) Absolute value of the elements of a duration array. B = abs (A) returns the absolute value of each element in the duration array A. The returned duration array B has the same size as the input array A. # name: # type: sq_string # elements: 1 # length: 51 Absolute value of the elements of a duration array. # name: # type: sq_string # elements: 1 # length: 15 duration.bounds # name: # type: sq_string # elements: 1 # length: 417 duration: [s, l] = bounds (D) duration: [s, l] = bounds (D, dim) duration: [s, l] = bounds (D, vecdim) duration: [s, l] = bounds (D, 'all') duration: [s, l] = bounds (..., nanflag) Return the smallest and largest values of a duration array. This method is a specialization of the core bounds function for duration arrays. The functionality is identical to core bounds function. Type help bounds for more information. # name: # type: sq_string # elements: 1 # length: 59 Return the smallest and largest values of a duration array. # name: # type: sq_string # elements: 1 # length: 12 duration.cat # name: # type: sq_string # elements: 1 # length: 472 duration: C = cat (dim, A, B, ...) Concatenate duration arrays. C = cat (dim, A, B, ...) concatenates duration arrays A, B, ... along dimension dim. All input arrays must have the same size except along the operating dimension dim. Any of the input arrays may also be string arrays or cell arrays of character vectors of compatible size. Additionally, an input can be a numeric matrix, which when parsed to the constructor will return a duration array of compatible size. # name: # type: sq_string # elements: 1 # length: 28 Concatenate duration arrays. # name: # type: sq_string # elements: 1 # length: 13 duration.ceil # name: # type: sq_string # elements: 1 # length: 480 duration: Y = ceil (D) duration: Y = ceil (D, unit) Round toward negative infinity for duration arrays. Y = ceil (D) rounds each element of the duration array D to the smallest integer number of seconds not less than that element. Y = ceil (D, unit) rounds each element of the duration array D to the smallest integer number of the specified unit of time not less than that element. unit must be one of the following values: 'seconds' (default) 'minutes' 'hours' 'days' 'years' # name: # type: sq_string # elements: 1 # length: 51 Round toward negative infinity for duration arrays. # name: # type: sq_string # elements: 1 # length: 16 duration.cellstr # name: # type: sq_string # elements: 1 # length: 247 duration: cstr = cellstr (D) duration: cstr = cellstr (D, Format) Convert duration array to a cell array of character vectors. cstr = cellstr (D) returns a cellstr array of character vectors, cstr, which has the same size as the input duration D. # name: # type: sq_string # elements: 1 # length: 60 Convert duration array to a cell array of character vectors. # name: # type: sq_string # elements: 1 # length: 15 duration.center # name: # type: sq_string # elements: 1 # length: 354 duration: C = center (D) duration: C = center (D, dim) duration: C = center (D, vecdim) duration: C = center (D, 'all') duration: C = center (..., nanflag) Center values in a duration array. This method overloads the core center function for duration arrays. The functionality is identical to core center function. Type help center for more information. # name: # type: sq_string # elements: 1 # length: 34 Center values in a duration array. # name: # type: sq_string # elements: 1 # length: 13 duration.char # name: # type: sq_string # elements: 1 # length: 332 duration: cmat = char (D) Convert duration array to a character matrix. cmat = char (D) returns a character matrix with one row per element in D, taken in column-major order. Rows shorter than the widest are padded on the left, so the character matrix is right-justified, as MATLAB returns it and as the array itself is displayed. # name: # type: sq_string # elements: 1 # length: 45 Convert duration array to a character matrix. # name: # type: sq_string # elements: 1 # length: 18 duration.circshift # name: # type: sq_string # elements: 1 # length: 724 duration: B = circshift (A, n) duration: B = circshift (A, n, dim) Circularly shift the elements in a duration array. B = circshift (A, n) circularly shifts the elements of the duration array A according to n. If n is a nonzero integer scalar, then the elements of A are shifted by n elements along the first non-singleton dimension of A. If n is a vector, it must not be longer that the number of dimensions of A with each value of n corresponding to a dimension in A. The sign of the value(s) in n specify the direction in the elements of A are shifted. B = circshift (A, n, dim) circularly shifts the elements of the duration array A along the dimension specified by dim. In this case, n must be a scalar integer value. # name: # type: sq_string # elements: 1 # length: 50 Circularly shift the elements in a duration array. # name: # type: sq_string # elements: 1 # length: 14 duration.colon # name: # type: sq_string # elements: 1 # length: 747 duration: R = colon (Base, Limit) duration: R = colon (Base, Increment, Limit) Create a range of durations in a vector. R = colon (Base, Limit) is the equivalent of the syntax C = Base:Limit and returns a duration vector in the range from Base to Limit incremented by 24-hour days. R = colon (Base, Increment, Limit) is equivalent to C = Base:Increment:Limit. dimension sizes must be equal or one of them must be 1. The size of C is determined by the size compatibility of A and B. As long as one of the inputs is a duration scalar, the following types are additionally supported for the remaining input arguments: numeric scalar (24-hour day) character vector (duration string) cellstr scalar (duration string) string scalar (duration string) # name: # type: sq_string # elements: 1 # length: 40 Create a range of durations in a vector. # name: # type: sq_string # elements: 1 # length: 19 duration.ctranspose # name: # type: sq_string # elements: 1 # length: 227 duration: B = ctranspose (A) Transpose a duration matrix. B = ctranspose (A) is the equivalent of the syntax B = A' and returns the transpose of the duration matrix A. For duration arrays, ctranspose is identical to transpose. # name: # type: sq_string # elements: 1 # length: 28 Transpose a duration matrix. # name: # type: sq_string # elements: 1 # length: 15 duration.cummax # name: # type: sq_string # elements: 1 # length: 551 duration: M = cummax (D) duration: M = cummax (D, dim) duration: M = cummax (D, vecdim) duration: M = cummax (D, 'all') duration: M = cummax ([], nanflag) duration: M = cummax ([], direction) duration: [M, index] = cummax (...) duration: [M, index] = cummax (..., 'linear') duration: ... = cummax (..., 'ComparisonMethod', method) Return the cumulative maximum values in duration arrays. This method overloads the core cummax function for duration arrays. The functionality is identical to core cummax function. Type help cummax for more information. # name: # type: sq_string # elements: 1 # length: 56 Return the cumulative maximum values in duration arrays. # name: # type: sq_string # elements: 1 # length: 15 duration.cummin # name: # type: sq_string # elements: 1 # length: 551 duration: M = cummin (D) duration: M = cummin (D, dim) duration: M = cummin (D, vecdim) duration: M = cummin (D, 'all') duration: M = cummin ([], nanflag) duration: M = cummin ([], direction) duration: [M, index] = cummin (...) duration: [M, index] = cummin (..., 'linear') duration: ... = cummin (..., 'ComparisonMethod', method) Return the cumulative minimum values in duration arrays. This method overloads the core cummin function for duration arrays. The functionality is identical to core cummin function. Type help cummin for more information. # name: # type: sq_string # elements: 1 # length: 56 Return the cumulative minimum values in duration arrays. # name: # type: sq_string # elements: 1 # length: 15 duration.cumsum # name: # type: sq_string # elements: 1 # length: 427 duration: CS = cumsum (D) duration: CS = cumsum (D, dim) duration: CS = cumsum (D, vecdim) duration: CS = cumsum (D, 'all') duration: CS = cumsum (..., direction) duration: CS = cumsum (..., nanflag) Compute the cumulative sum of the elements of a duration array. This method overloads the core cumsum function for duration arrays. The functionality is identical to core cumsum function. Type help cumsum for more information. # name: # type: sq_string # elements: 1 # length: 63 Compute the cumulative sum of the elements of a duration array. # name: # type: sq_string # elements: 1 # length: 16 duration.datenum # name: # type: sq_string # elements: 1 # length: 513 duration: N = datenum (D) Convert a duration array to numbers of days. N = datenum (D) returns a double array of the same size as D holding the length of each duration in days, including any fractional part. NaN durations give NaN. A duration measures elapsed time rather than naming a point in it, so the result is a count of days and not a serial date number: adding it to a serial date number shifts that date by the duration. This is the same value days returns, under the name the older date functions use. # name: # type: sq_string # elements: 1 # length: 44 Convert a duration array to numbers of days. # name: # type: sq_string # elements: 1 # length: 16 duration.datevec # name: # type: sq_string # elements: 1 # length: 859 duration: DV = datevec (DT) duration: [Y, MO, D, h, mi, s] = datevec (DT) Convert duration array to date vectors. DV = datevec (DT) returns an Nx6 numeric matrix whose rows represent each element in DT and each column corresponds to years, months, days, hours, minutes, and seconds, respectively. Since months cannot be represented as a fixed length of time, the second column of DV is always zero. DV represents a length of time split across different fixed-length elapsed time units. The number of rows in DV equals to the number of elements in the duration array DT. [Y, MO, D, h, mi, s] = datevec (DT) returns the components of DT as individual variables, but unlike DV in the previous syntax, each variable has the same size as the duration array DT. Values containing a fractional portion less than 1 picosecond are rounded to the nearest picosecond. # name: # type: sq_string # elements: 1 # length: 39 Convert duration array to date vectors. # name: # type: sq_string # elements: 1 # length: 13 duration.days # name: # type: sq_string # elements: 1 # length: 231 duration: X = days (D) Duration equivalent numeric values in fixed-length days. X = days (D) converts durations in D to the equivalent number of fixed-length days (1 day equals 24 hours). X is a double array of the same size as D. # name: # type: sq_string # elements: 1 # length: 56 Duration equivalent numeric values in fixed-length days. # name: # type: sq_string # elements: 1 # length: 13 duration.diff # name: # type: sq_string # elements: 1 # length: 307 duration: DT = diff (D) duration: DT = diff (D, K) duration: DT = diff (D, K, DIM) Compute differences between adjacent elements in a duration array. This method overloads the core diff function for duration arrays. The functionality is identical to core diff function. Type help diff for more information. # name: # type: sq_string # elements: 1 # length: 66 Compute differences between adjacent elements in a duration array. # name: # type: sq_string # elements: 1 # length: 19 duration.discretize # name: # type: sq_string # elements: 1 # length: 1874 duration: bin = discretize (D, edges) duration: bin = discretize (D, N) duration: bin = discretize (D, dur) duration: bin = discretize (D, unit) duration: Y = discretize (..., values) duration: C = discretize (..., 'categorical') duration: C = discretize (..., 'categorical', names) duration: Y = discretize (..., 'IncludedEdge', side) duration: [bin, edges] = discretize (...) Group durations into bins. bin = discretize (D, edges) returns the index of the bin each element of D falls into, given a duration vector of bin edges. Bin j covers [edges(j), edges(j+1)), except the last, which is closed at both ends. Elements outside the edges, and NaN durations, return NaN. bin = discretize (D, N) uses N bins of uniform width, placed on whole time units where that is possible. bin = discretize (D, dur) uses bins of the fixed width dur, a scalar duration, and bin = discretize (D, unit) uses bins one unit wide, where unit is one of 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year', 'decade' or 'century'. A month is a twelfth of the 365.2425-day year that years counts, since a duration has no calendar. The remaining options are as for the top-level discretize. Deviation from MATLAB, for an explicitly requested bin count only. Every other way of binning -- automatic, by width, by unit, or by explicit edges -- agrees with MATLAB exactly. Asked for a bin count, MATLAB chooses between whole-unit and decimal widths in a way that is not monotonic in the data: over seconds ([0 16.8]) it gives 7 bins of 2.5 s, over the wider seconds ([0 17.15]) 7 bins of 3 s, and decimal widths again above that. It also discards bins, giving hours (0:24) in 7 bins a 4-hour width that covers the data in 6. A whole time unit is used here whenever it still needs every bin requested, which is monotonic and never wastes a bin. See also: histcounts, discretize # name: # type: sq_string # elements: 1 # length: 26 Group durations into bins. # name: # type: sq_string # elements: 1 # length: 20 duration.dispstrings # name: # type: sq_string # elements: 1 # length: 225 duration: cstr = dispstrings (D) Get display formatted strings for each element of a duration array. cstr = dispstrings (D) returns a cellstr array of character vectors, cstr, which has the same size as the input duration D. # name: # type: sq_string # elements: 1 # length: 67 Get display formatted strings for each element of a duration array. # name: # type: sq_string # elements: 1 # length: 17 duration.duration # name: # type: sq_string # elements: 1 # length: 2035 duration: D = duration (X) duration: D = duration (H, MI, S) duration: D = duration (H, MI, S, MS) duration: D = duration (TimeStrings) duration: D = duration (TimeStrings, 'InputFormat', INFMT) duration: D = duration (..., 'Format', FMT) Create a new array of fixed-length time durations. D = duration (X) creates a column vector of durations from a numeric matrix. D = duration (H, MI, S) creates a duration array from numeric arrays containing the number of hours, minutes, and seconds specified by H, MI and S, respectively. D = duration (H, MI, S, MS) creates a duration array from numeric arrays containing the number of hours, minutes, seconds, and milliseconds specified by H, MI, S, and MS, respectively. D = duration (TimeStrings) creates a duration array from text that represents elapsed times. TimeStrings can be a character vector, a cell array of character vectors, or a string array representing times using either the 'hh:mm:ss' or the 'dd:hh:mm:ss' format. D = duration (TimeStrings, 'InputFormat', INFMT) creates a duration array from text that represents elapsed times according to the format specified by INFMT, which can be any of the following: 'dd:hh:mm:ss' 'hh:mm:ss' 'mm:ss' 'hh:mm' Any of the first three formats can also be appended with up to nine S characters to indicate fractional second digits, such as 'dd:hh:mm:ss.SS' or 'mm:ss.SS'. D = duration (..., 'Format', FMT) specifies the format in which D is displayed. FMT can specify either a digital timer, which can have any of the valid formats for 'InputFormat' as shown above or a single number with time units by specifying one of the following: 'y' fixed-length years (1 year equals 365.2425 days) 'd' fixed-length days (1 day equals 24 hours) 'h' hours 'm' minutes 's' seconds D = duration () returns a scalar array of durations with an elapsed time value of zero. To create an empty duration array, use duration ([], [], []). See also: years, days, hours, minutes, seconds, milliseconds, duration, isduration, calendarDuration, datetime # name: # type: sq_string # elements: 1 # length: 50 Create a new array of fixed-length time durations. # name: # type: sq_string # elements: 1 # length: 14 duration.empty # name: # type: sq_string # elements: 1 # length: 578 duration: E = duration.empty () duration: E = duration.empty (sz) duration: E = duration.empty (m, n, ...) Create an empty duration array. E = duration.empty () returns a 0x0 empty duration array. duration.empty (m, n, ...) or duration.empty (sz) returns an empty duration array of the requested size, which must have at least one dimension equal to zero. A lone dimension gives a square size, so duration.empty (3) is an error while duration.empty (0) is 0x0. As for zeros, a negative dimension counts as zero, and a size vector with nothing in it names no size and gives 0x0. # name: # type: sq_string # elements: 1 # length: 31 Create an empty duration array. # name: # type: sq_string # elements: 1 # length: 12 duration.eps # name: # type: sq_string # elements: 1 # length: 508 duration: E = eps (D) Spacing of duration values. E = eps (D) returns the distance from each element of D to the next duration that can be told apart from it, as a duration array of the same size. It is the resolution available at that magnitude: two durations closer together than this are the same value. eps of a negative duration is that of its magnitude, and eps of NaN is NaN. The spacing is that of the milliseconds a duration is stored in, which is what MATLAB stores too, so the two agree exactly. # name: # type: sq_string # elements: 1 # length: 27 Spacing of duration values. # name: # type: sq_string # elements: 1 # length: 11 duration.eq # name: # type: sq_string # elements: 1 # length: 684 duration: TF = eq (A, B) Equality for duration arrays. TF = eq (A, B) is the equivalent of the syntax TF = A == B and returns a logical array whose elements set to true where the corresponding elements of A and B are equal and set to false otherwise. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. The size of C is determined by the size compatibility of A and B. One of the input arguments can also be a character vector, a cell array of character vectors, or a string array representing duration strings or a numeric array representing days. # name: # type: sq_string # elements: 1 # length: 29 Equality for duration arrays. # name: # type: sq_string # elements: 1 # length: 14 duration.floor # name: # type: sq_string # elements: 1 # length: 488 duration: Y = floor (D) duration: Y = floor (D, unit) Round toward negative infinity for duration arrays. Y = floor (D) rounds each element of the duration array D to the largest integer number of seconds not greater than that element. Y = floor (D, unit) rounds each element of the duration array D to the largest integer number of the specified unit of time not greater than that element. unit must be one of the following values: 'seconds' (default) 'minutes' 'hours' 'days' 'years' # name: # type: sq_string # elements: 1 # length: 51 Round toward negative infinity for duration arrays. # name: # type: sq_string # elements: 1 # length: 11 duration.ge # name: # type: sq_string # elements: 1 # length: 699 duration: TF = ge (A, B) Equality for duration arrays. TF = ge (A, B) is the equivalent of the syntax TF = A >= B and returns a logical array whose elements set to true where the corresponding elements of A are greater than or equal to B and set to false otherwise. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. The size of C is determined by the size compatibility of A and B. One of the input arguments can also be a character vector, a cell array of character vectors, or a string array representing duration strings or a numeric array representing days. # name: # type: sq_string # elements: 1 # length: 29 Equality for duration arrays. # name: # type: sq_string # elements: 1 # length: 11 duration.gt # name: # type: sq_string # elements: 1 # length: 686 duration: TF = gt (A, B) Equality for duration arrays. TF = gt (A, B) is the equivalent of the syntax TF = A > B and returns a logical array whose elements set to true where the corresponding elements of A are greater than B and set to false otherwise. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. The size of C is determined by the size compatibility of A and B. One of the input arguments can also be a character vector, a cell array of character vectors, or a string array representing duration strings or a numeric array representing days. # name: # type: sq_string # elements: 1 # length: 29 Equality for duration arrays. # name: # type: sq_string # elements: 1 # length: 14 duration.histc # name: # type: sq_string # elements: 1 # length: 306 duration: n = histc (D, edges) duration: n = histc (D, edges, dim) duration: [n, idx] = histc (...) Compute histogram counts in a duration array. This method overloads the core histc function for duration arrays. The functionality is identical to core histc function. Type help histc for more information. # name: # type: sq_string # elements: 1 # length: 45 Compute histogram counts in a duration array. # name: # type: sq_string # elements: 1 # length: 19 duration.histcounts # name: # type: sq_string # elements: 1 # length: 990 duration: N = histcounts (D) duration: N = histcounts (D, nbins) duration: N = histcounts (D, edges) duration: N = histcounts (..., Name, Value) duration: [N, edges] = histcounts (...) duration: [N, edges, bin] = histcounts (...) Histogram bin counts for durations. N = histcounts (D) bins the durations in D, chosen automatically, and returns the number of elements in each bin. D is treated as D(:) and NaN durations are excluded. N = histcounts (D, nbins) and N = histcounts (D, edges) bin by count and by explicit duration edges respectively. 'BinWidth' takes a scalar duration, 'BinLimits' a two-element duration, and 'BinEdges' a duration vector. 'BinMethod' accepts 'auto', 'scott', 'fd', 'sturges' and 'sqrt', but not 'integers', which has no meaning for a duration. 'Normalization' accepts 'count', 'cumcount', 'probability', 'percentage' and 'cdf'. 'countdensity' and 'pdf' are not accepted, since a density per unit time has no meaning here. See also: discretize, histcounts # name: # type: sq_string # elements: 1 # length: 35 Histogram bin counts for durations. # name: # type: sq_string # elements: 1 # length: 12 duration.hms # name: # type: sq_string # elements: 1 # length: 765 duration: H = hms (D) duration: [H, M] = hms (D) duration: [H, M, S] = hms (D) Split duration array into separate time unit values. [H, M, S] = hms (D) splits the duration array D into separate numeric arrays H, M, and S, which correspond to hours, minutes, and seconds, respectively. Hours and minutes are returned as whole numbers, while seconds may also have a fractional part. Each component is what truncating division leaves, so every one of them carries the sign of the duration as a whole and a span a hair under an hour reports 59 minutes rather than a whole one. Nothing is snapped to a whole unit and nothing is rounded to a fixed precision. A duration that is not finite has no components to divide between: all three take that same infinity, or NaN. # name: # type: sq_string # elements: 1 # length: 52 Split duration array into separate time unit values. # name: # type: sq_string # elements: 1 # length: 16 duration.horzcat # name: # type: sq_string # elements: 1 # length: 527 duration: C = horzcat (A, B, ...) Horizontal concatenation of duration arrays. C = horzcat (A, B, ... is the equivalent of the syntax B = [A, B, ...] and horizontally concatenates the duration arrays A, B, .... All input arrays must have the same size except along the second dimension. Any of the input arrays may also be string arrays or cell arrays of character vectors of compatible size. Additionally, an input can be a numeric matrix, which when parsed to the constructor will return a duration array of compatible size. # name: # type: sq_string # elements: 1 # length: 44 Horizontal concatenation of duration arrays. # name: # type: sq_string # elements: 1 # length: 14 duration.hours # name: # type: sq_string # elements: 1 # length: 185 duration: X = hours (D) Duration equivalent numeric values in hours. X = hours (D) converts durations in D to the equivalent number of hours. X is a double array of the same size as D. # name: # type: sq_string # elements: 1 # length: 44 Duration equivalent numeric values in hours. # name: # type: sq_string # elements: 1 # length: 16 duration.interp1 # name: # type: sq_string # elements: 1 # length: 2619 duration: YI = interp1 (X, Y, XI) duration: YI = interp1 (Y, XI) duration: YI = interp1 (..., method) duration: YI = interp1 (..., method, extrapolation) duration: pp = interp1 (X, Y, 'pp') duration: pp = interp1 (X, Y, method, 'pp') One-dimensional interpolation for duration arrays. YI = interp1 (X, Y, XI) computes the linearly interpolated values of a one-dimensional function, which is represented by sample points X and corresponding values Y, at specific query points XI. X must be a vector. If Y is vector, then it must have the same length as X. If Y is matrix, then each column is treated as a different set of one-dimensional sample values and the number of rows must equal the length of X. XI must be a vector and the same data type as X. The output XI is the same data type as Y and its size depends on Y and XI. X and Y can both be duration arrays or one of them can be a numeric array. YI = interp1 (Y, XI) computes the linearly interpolated values of a one-dimensional function assuming a default set of sampling points determined by the shape of Y: If Y is a vector, then the default sampling points are [1:length(Y). If Y is an array, then the default sampling points are [1:size(Y). YI = interp1 (..., method) specifies one of the following interpolation methods to be used: 'linear' (default) computes the linear interpolation from nearest neighbors. 'nearest' returns the nearest neighbor. 'next' returns the next neighbor. 'previous' returns the previous neighbor. 'pchip' computes the piecewise cubic Hermite interpolating polynomial, which corresponds to shape-preserving interpolation with smooth first derivative. 'cubic' computes the cubic interpolation. 'spline' computes the cubic spline interpolation, which corresponds to smooth first and second derivatives throughout the curve. YI = interp1 (..., method, extrapolation) further specifies a strategy for evaluating points that lie outside the range of the sample points in X. Set extrapolation to 'extrap' to use the current method to extrapolate values. Set extrapolation to a scalar value of the same data type as Y to return a constant value outside the range of X. When unspecified, extrapolation defaults to NaN. pp = interp1 (X, Y, 'pp' returns a piecewise polynomial object, using the default linear interpolation algorithm, which can be later used with ppval to evaluate the interpolation at new query points. pp = interp1 (X, Y, method, 'pp' return a piecewise polynomial object, using the interpolation algorithm specified by method, which can be later used with ppval to evaluate the interpolation at new query points. # name: # type: sq_string # elements: 1 # length: 50 One-dimensional interpolation for duration arrays. # name: # type: sq_string # elements: 1 # length: 18 duration.intersect # name: # type: sq_string # elements: 1 # length: 1306 duration: C = intersect (A, B) duration: C = intersect (A, B, 'rows') duration: C = intersect (A, B, ..., order) duration: [C, ixA, ixB] = intersect (...) Set intersection of two duration arrays. C = intersect (A, B) returns the unique common values of the duration arrays A and B. Either A or B input arguments can also be a duration string specified as a character vector, a string array, or a cell array of character vectors, or a numeric array representing 24-hour days. In such case, the input is promoted to a duration array prior to calculating the intersection. If both A and B are row vectors, then C is also a row vector, otherwise intersect returns a column vector. C = intersect (A, B, 'rows' returns the unique common rows of the duration matrices A and B, which must have the same number of columns. By default, the rows in duration matrix C are in sorted order. ... = intersect (A, B, ..., order) also specifies the order of the returned unique values. order can be 'sorted', which is the default behavior, or 'stable', in which case the unique values are returned in order of appearance. [C, ixA, ixB] = intersect (...) also returns index vectors ixA and ixB such that C = A(ixA) and C = B(ixB), unless the 'rows' optional argument is given, in which case C = A(ixA,:) and C = B(ixB,:). # name: # type: sq_string # elements: 1 # length: 40 Set intersection of two duration arrays. # name: # type: sq_string # elements: 1 # length: 17 duration.ipermute # name: # type: sq_string # elements: 1 # length: 463 duration: A = ipermute (B, dims) Inverse of the generalized transpose for a duration N-D array. A = ipermute (B, dims) returns the inverse of the generalized transpose performed by the permute function. The expression ipermute (permute (A, dims), dims) returns the original array A. dims must index all the dimensions 1:ndims (B) of the input array B, in any order, but only once. The dimension of B specified in dims(N) gets remapped to the Nth dimension of A. # name: # type: sq_string # elements: 1 # length: 62 Inverse of the generalized transpose for a duration N-D array. # name: # type: sq_string # elements: 1 # length: 12 duration.iqr # name: # type: sq_string # elements: 1 # length: 344 duration: r = iqr (D) duration: r = iqr (D, dim) duration: r = iqr (D, vecdim) duration: r = iqr (D, 'all') duration: [r, q] = iqr (...) Compute the interquartile range of a duration array. This method overloads the core iqr function for duration arrays. The functionality is identical to core iqr function. Type help iqr for more information. # name: # type: sq_string # elements: 1 # length: 52 Compute the interquartile range of a duration array. # name: # type: sq_string # elements: 1 # length: 18 duration.isbetween # name: # type: sq_string # elements: 1 # length: 1154 duration: TF = isbetween (D, lower, upper) duration: TF = isbetween (D, lower, upper, intervalType) Find duration elements within specified range. TF = isbetween (D, lower, upper) returns a logical array, TF, which is the same size as the input duration array D and it contains true for each corresponding element which is within the range specified by lower and upper and false otherwise. lower and upper must be duration arrays of compatible size with D or alternatively they can be specified as a character vector, a cell array of character vectors or a string array containing valid text duration representations. TF = isbetween (D, lower, upper, intervalType) specifies the type of interval for the lower and upper bounds and it can be one of the following values. 'closed' (default) includes lower and upper bounds. 'open' excludes lower and upper bounds. 'openleft' or 'closedright' exclude the lower and include the upper bound. They have identical behavior. 'closedleft' or 'openright' include the lower and exclude the upper bound. They have identical behavior. intervalType can be specified either as a character vector or a string scalar. # name: # type: sq_string # elements: 1 # length: 46 Find duration elements within specified range. # name: # type: sq_string # elements: 1 # length: 17 duration.iscolumn # name: # type: sq_string # elements: 1 # length: 338 duration: TF = iscolumn (D) Return true if duration array is a column vector. TF = iscolumn (D) returns a logical scalar TF, which is true if the duration array D is a column vector and false otherwise. A column vector is a 2-D array for which size (D) returns [N, 1] with non-negative N. By definition, a scalar is also a column vector. # name: # type: sq_string # elements: 1 # length: 49 Return true if duration array is a column vector. # name: # type: sq_string # elements: 1 # length: 16 duration.isempty # name: # type: sq_string # elements: 1 # length: 181 duration: TF = isempty (D) Return true if duration array is empty. TF = isempty (D) returns a logical scalar TF, which is true if the duration array D is empty and false otherwise. # name: # type: sq_string # elements: 1 # length: 39 Return true if duration array is empty. # name: # type: sq_string # elements: 1 # length: 16 duration.isequal # name: # type: sq_string # elements: 1 # length: 544 duration: TF = isequal (D1, D2) duration: TF = isequal (D1, D2, ...) Return true if duration arrays are equal. TF = isequal (D1, D2) returns a logical scalar TF, which is true, if the duration arrays D1 and D2 contain the same values, and false otherwise. Either D1 or D2 can also be specified as a character vector, a cell array of character vectors or a string array containing valid text duration representations. TF = isequal (D1, D2, ...) returns a logical scalar TF, which is true, if all input arguments are equal, and false otherwise. # name: # type: sq_string # elements: 1 # length: 41 Return true if duration arrays are equal. # name: # type: sq_string # elements: 1 # length: 17 duration.isequaln # name: # type: sq_string # elements: 1 # length: 688 duration: TF = isequaln (D1, D2) duration: TF = isequaln (D1, D2, ...) Return true if duration arrays are equal under the assumption that missing elements are equal. TF = isequaln (D1, D2) returns a logical scalar TF, which is true, if the duration arrays D1 and D2 contain the same values or corresponding missing elements, and false otherwise. Either D1 or D2 can also be specified as a character vector, a cell array of character vectors or a string array containing valid text duration representations. TF = isequaln (D1, D2, ...) returns a logical scalar TF, which is true, if all input arguments are equal under the assumption that missing elements are equal, and false otherwise. # name: # type: sq_string # elements: 1 # length: 94 Return true if duration arrays are equal under the assumption that missing elements are equal. # name: # type: sq_string # elements: 1 # length: 17 duration.isfinite # name: # type: sq_string # elements: 1 # length: 308 duration: TF = isfinite (D) Return true for duration elements that are finite. TF = isfinite (D) returns a logical array TF of the same size as calD containing true for each corresponding element of D that is finite and false otherwise. Finite elements are those which are neither infinite nor Not-A-Number. # name: # type: sq_string # elements: 1 # length: 50 Return true for duration elements that are finite. # name: # type: sq_string # elements: 1 # length: 14 duration.isinf # name: # type: sq_string # elements: 1 # length: 245 duration: TF = isinf (D) Return true for duration elements that are infinite. TF = isinf (D) returns a logical array TF of the same size as D containing true for each corresponding element of calD that is either Inf or -Inf and false otherwise. # name: # type: sq_string # elements: 1 # length: 52 Return true for duration elements that are infinite. # name: # type: sq_string # elements: 1 # length: 17 duration.ismatrix # name: # type: sq_string # elements: 1 # length: 289 duration: TF = ismatrix (D) Return true if duration array is a 2-D array. TF = ismatrix (D) returns a logical scalar TF, which is true if the duration array D is a matrix and false otherwise. A matrix is an array of any type where ndims (D) == 2. By definition, a scalar is also a matrix. # name: # type: sq_string # elements: 1 # length: 45 Return true if duration array is a 2-D array. # name: # type: sq_string # elements: 1 # length: 17 duration.ismember # name: # type: sq_string # elements: 1 # length: 1213 duration: TF = ismember (A, B) duration: TF = ismember (A, B, 'rows') duration: [TF, index] = ismember (...) duration: [TF, index] = ismember (..., 'legacy') Find duration elements in a set. TF = ismember (A, B) returns a logical array TF of the same size as A containing true for each corresponding element of A that is in B and false otherwise. NaN elements are not equal with each other and always return false. TF = ismember (A, B, 'rows') only applies to duration matrices with the same number of columns, in which case the logical vector TF contains true for each row of A that is also a row in B. TF has the same number of rows as A. [TF, index] = ismember (A, B) also returns an index array of the same size as A containing the lowest index in B for each element of A that is a member of B and 0 otherwise. If the 'rows' optional argument is used, then the returning index is a column vector with the same rows as A and it contains the lowest index in B for each row of A that is a member of B and 0 otherwise. If the 'legacy' optional argument is specified, then the highest index of matched elements is returned. Unless multiple matches exist, the 'legacy' option has no effect on the returned index. # name: # type: sq_string # elements: 1 # length: 32 Find duration elements in a set. # name: # type: sq_string # elements: 1 # length: 18 duration.ismissing # name: # type: sq_string # elements: 1 # length: 599 duration: TF = ismissing (D) duration: TF = ismissing (D, indicator) Find missing elements in duration array. TF = ismissing (D) returns a logical array, TF, with the same dimensions as D, where true values match the standard missing values in the input duration array. The optional input indicator can be a scalar or a vector duration array, specifying alternative missing values in the input data. When specifying indicator values, the standard missing values are ignored, unless explicitly stated in the indicator. A NaN duration in indicator matches the NaN elements of D, and so does missing. # name: # type: sq_string # elements: 1 # length: 40 Find missing elements in duration array. # name: # type: sq_string # elements: 1 # length: 14 duration.isnan # name: # type: sq_string # elements: 1 # length: 234 duration: TF = isnan (D) Return true for duration elements that are Not-A-Number. TF = isnan (D) returns a logical array TF of the same size as D containing true for each corresponding element of calD that is NaN and false otherwise. # name: # type: sq_string # elements: 1 # length: 56 Return true for duration elements that are Not-A-Number. # name: # type: sq_string # elements: 1 # length: 18 duration.isregular # name: # type: sq_string # elements: 1 # length: 1679 duration: TF = isregular (D) duration: TF = isregular (D, COMPONENT) duration: [TF, DT] = isregular (...) Return true if a duration vector is regularly spaced. TF = isregular (D) returns true when the elements of the duration vector D are evenly spaced, that is, when every step from one element to the next is the same and is not zero. A second output DT gives that step, and is NaN when there is none. A sequence that does not move is not regularly spaced: a step of zero describes no spacing, so isregular of a constant vector is false. A descending vector is regular, with a negative step. A NaN or an infinite element anywhere makes the vector irregular, as does having fewer than two elements, so a scalar and an empty array are both false. D must be a vector; a matrix is an error. TF = isregular (D, COMPONENT) asks about a particular component, one of 'Years', 'Quarters', 'Months', 'Weeks', 'Days' or 'Time'. A duration measures elapsed time and knows nothing of a calendar, so only 'Time', the default, can be true; every calendar component is false however the vector is spaced. The argument exists so that a duration can be asked the same question as a datetime. Steps are compared within a few eps of the largest value present rather than exactly, because subtracting neighbouring durations rounds: even one, two and three hours have steps differing in the last bit, and an exact test would call that irregular. MATLAB tolerates a difference of about the same size, for the same reason. Deviation from MATLAB. A negative step is returned there formatted in seconds whatever the vector's own format; here the step keeps D's format whichever way the vector runs. # name: # type: sq_string # elements: 1 # length: 53 Return true if a duration vector is regularly spaced. # name: # type: sq_string # elements: 1 # length: 14 duration.isrow # name: # type: sq_string # elements: 1 # length: 320 duration: TF = isrow (D) Return true if duration array is a row vector. TF = isrow (D) returns a logical scalar TF, which is true if the duration array D is a row vector and false otherwise. A row vector is a 2-D array for which size (D) returns [1, N] with non-negative N. By definition, a scalar is also a row vector. # name: # type: sq_string # elements: 1 # length: 46 Return true if duration array is a row vector. # name: # type: sq_string # elements: 1 # length: 17 duration.isscalar # name: # type: sq_string # elements: 1 # length: 265 duration: TF = isscalar (D) Return true if duration array is a scalar. TF = isscalar (D) returns a logical scalar TF, which is true if the duration array D is also a scalar and false otherwise. A scalar is a single element object for which size (D) returns [1, 1]. # name: # type: sq_string # elements: 1 # length: 42 Return true if duration array is a scalar. # name: # type: sq_string # elements: 1 # length: 17 duration.issorted # name: # type: sq_string # elements: 1 # length: 1961 duration: TF = issorted (D) duration: TF = issorted (D, dim) duration: TF = issorted (D, direction) duration: TF = issorted (D, dim, direction) duration: TF = issorted (..., 'MissingPlacement', MP) duration: TF = issorted (..., 'ComparisonMethod', CM) Return true if duration array is sorted. TF = issorted (D) returns a logical scalar TF, which is true, if the duration array D is sorted in ascending order, and false otherwise. TF = issorted (D, dim) returns a logical scalar TF, which is true, if the duration array D is sorted in ascending order along the dimension dim, and false otherwise. TF = issorted (D, direction) returns a logical scalar TF, which is true, if the duration array D is sorted in the direction specified by direction, and false otherwise. direction can be any of the following options: 'ascend', which is the default, checks is elements are in ascending order. 'descend' checks if elements are in descending order. 'monotonic' checks if elements are either in ascending or descending order. 'strictascend' checks if elements are in ascending order and there are no duplicate or undefined elements. 'strictdescend' checks if elements are in descending order and there are no duplicate or undefined elements. 'strictmonotonic' checks if elements are either in ascending or descending order and there are no duplicate or undefined elements. TF = issorted (..., 'MissingPlacement', MP) specifies where missing elements (NaN) are placed with one of the following options specified in MP: 'auto', which is the default, places missing elements last for ascending sort and first for descending sort. 'first' places missing elements first. 'last' places missing elements last. TF = issorted (..., 'ComparisonMethod', CM) specifies the comparison method for determining the order of elements with one of following options specified in CM: 'auto', which is the default, sorts by real (A). 'real' sorts by real (A). 'abs' sorts by abs (A). # name: # type: sq_string # elements: 1 # length: 40 Return true if duration array is sorted. # name: # type: sq_string # elements: 1 # length: 21 duration.issortedrows # name: # type: sq_string # elements: 1 # length: 2811 duration: TF = issortedrows (D) duration: TF = issortedrows (D, col) duration: TF = issortedrows (D, direction) duration: TF = issortedrows (D, col, direction) duration: TF = issortedrows (..., 'MissingPlacement', MP) duration: TF = issortedrows (..., 'ComparisonMethod', CM) Return true if duration matrix rows are sorted. TF = issortedrows (D) returns a logical scalar TF, which is true, if the rows in the 2-D duration array D are sorted in ascending order, and false otherwise. TF = issortedrows (D, col) returns a logical scalar TF, which is true, if the duration array D is sorted according to the columns specified by the vector col, and false otherwise. col must explicitly contain non-zero integers whose absolute values index existing columns in D. Positive elements sort the corresponding columns in ascending order, while negative elements sort the corresponding columns in descending order. TF = issortedrows (D, direction) checks if the rows in D are sorted according to the specified direction, which can be one of the following options: 'ascend', which is the default, checks is elements are in ascending order. 'descend' checks if elements are in descending order. 'monotonic' checks if elements are either in ascending or descending order. 'strictascend' checks if elements are in ascending order and there are no duplicate or undefined elements. 'strictdescend' checks if elements are in descending order and there are no duplicate or undefined elements. 'strictmonotonic' checks if elements are either in ascending or descending order and there are no duplicate or undefined elements. Alternatively, direction can be a cell array of character vectors specifying the sorting direction for each individual column of D, in which case the number of elements in direction must equal the number of columns in D. TF = issortedrows (D, col, direction) checks if the rows in the duration array D are sorted according to the columns specified in col using the corresponding sorting direction specified in direction. In this case, the sign of the values in col is ignored. col and direction must have the same length, but not necessarily the same number of elements as the columns in D. TF = issortedrows (..., 'MissingPlacement', MP) specifies where missing elements () are placed with one of the following options specified in MP: 'auto', which is the default, places missing elements last for ascending sort and first for descending sort. 'first' places missing elements first. 'last' places missing elements last. TF = issortedrows (..., 'ComparisonMethod', CM) specifies the comparison method for determining the order of elements with one of following options specified in CM: 'auto', which is the default, sorts by real (A). 'real' sorts by real (A). 'abs' sorts by abs (A). # name: # type: sq_string # elements: 1 # length: 47 Return true if duration matrix rows are sorted. # name: # type: sq_string # elements: 1 # length: 17 duration.isvector # name: # type: sq_string # elements: 1 # length: 322 duration: TF = isvector (D) Return true if duration array is a vector. TF = isvector (D) returns a logical scalar TF, which is true if the duration array D is a vector and false otherwise. A vector is a 2-D array for which one of the dimensions is equal to 1 (either 1xN or Nx1). By definition, a scalar is also a vector. # name: # type: sq_string # elements: 1 # length: 42 Return true if duration array is a vector. # name: # type: sq_string # elements: 1 # length: 16 duration.keyHash # name: # type: sq_string # elements: 1 # length: 1105 duration: key = keyHash (D) duration: key = keyHash (D, base) Generate a hash code for duration array. key = keyHash (D) generates a uint64 scalar that represents the input array D. keyHash utilizes the 64-bit FNV-1a variant of the Fowler-Noll-Vo non-cryptographic hash function. key = keyHash (D, base) also generates a 64-bit hash code using base as the offset basis for the FNV-1a hash algorithm. base must be a uint64 integer type scalar. Use this syntax to cascade keyHash on multiple objects for which a single hash code is required. A duration array is keyed on its length in milliseconds, so the unit it was written in is not part of the key and seconds (1) hashes as milliseconds (1000). A missing duration is keyed alike whatever produced its NaN. The Format property is display only and is not part of the key. Two arrays that keyMatch reports as the same key always hash alike. Note that unlike MATLAB, this implementation does not use any random seed. As a result, keyHash will always generate the exact same hash key for any particular input across different workers and Octave sessions. # name: # type: sq_string # elements: 1 # length: 40 Generate a hash code for duration array. # name: # type: sq_string # elements: 1 # length: 17 duration.kurtosis # name: # type: sq_string # elements: 1 # length: 533 duration: k = kurtosis (D) duration: k = kurtosis (D, flag) duration: k = kurtosis (D, flag, dim) duration: k = kurtosis (D, flag, vecdim) duration: k = kurtosis (D, flag, 'all') Compute the sample kurtosis of a duration array. This method overloads the core kurtosis function for duration arrays. The functionality is identical to core kurtosis function. Type help kurtosis for more information. Note that kurtosis is a dimensionless quantity Thus, the returned argument is a numeric array of double type and not a duration array. # name: # type: sq_string # elements: 1 # length: 48 Compute the sample kurtosis of a duration array. # name: # type: sq_string # elements: 1 # length: 16 duration.ldivide # name: # type: sq_string # elements: 1 # length: 665 duration: C = ldivide (A, B) Element-wise left division for duration arrays. C = ldivide (A, B) is the equivalent of the syntax C = A .\ B and returns the element-wise division of the duration array B by the corresponding elements of input array A, which can either be a duration or a numeric array. If A is a duration array, then C is a double numeric array. If A is a numeric array, then C is a duration array. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. The size of C is determined by the size compatibility of A and B. # name: # type: sq_string # elements: 1 # length: 47 Element-wise left division for duration arrays. # name: # type: sq_string # elements: 1 # length: 11 duration.le # name: # type: sq_string # elements: 1 # length: 696 duration: TF = le (A, B) Equality for duration arrays. TF = le (A, B) is the equivalent of the syntax TF = A <= B and returns a logical array whose elements set to true where the corresponding elements of A are less than or equal to B and set to false otherwise. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. The size of C is determined by the size compatibility of A and B. One of the input arguments can also be a character vector, a cell array of character vectors, or a string array representing duration strings or a numeric array representing days. # name: # type: sq_string # elements: 1 # length: 29 Equality for duration arrays. # name: # type: sq_string # elements: 1 # length: 15 duration.length # name: # type: sq_string # elements: 1 # length: 218 duration: N = length (D) Length of a duration vector. N = length (D) returns the size of the longest dimension of the duration array D, unless any of its dimensions has zero length, in which case length (D) returns 0. # name: # type: sq_string # elements: 1 # length: 28 Length of a duration vector. # name: # type: sq_string # elements: 1 # length: 17 duration.linspace # name: # type: sq_string # elements: 1 # length: 988 duration: R = linspace (Start, End) duration: R = linspace (Start, End, N) Create linearly spaced duration elements. R = linspace (Start, End) returns 100 linearly spaced elements between Start and End. If Start and End are scalars, then R is a vector. If one or both inputs are vectors, then R is a matrix where each row is an independent sequence between Start(idx_N) and End(idx_N). R = linspace (Start, End, N) specifies the number (default is 100) of equally spaced elements between Start and End. If N is not an integer value, then it is floored to the nearest integer. If N is zero or negative, then an empty matrix is returned. If N is one, then End is returned. If N greater than one, then Start and End are always included in the range. Either Start or End input arguments can also be one of the following types: numeric scalar or vector (24-hour days) character vector (duration string) cellstr scalar or vector (duration strings) string scalar or vector (duration strings) # name: # type: sq_string # elements: 1 # length: 41 Create linearly spaced duration elements. # name: # type: sq_string # elements: 1 # length: 11 duration.lt # name: # type: sq_string # elements: 1 # length: 683 duration: TF = lt (A, B) Equality for duration arrays. TF = lt (A, B) is the equivalent of the syntax TF = A < B and returns a logical array whose elements set to true where the corresponding elements of A are less than B and set to false otherwise. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. The size of C is determined by the size compatibility of A and B. One of the input arguments can also be a character vector, a cell array of character vectors, or a string array representing duration strings or a numeric array representing days. # name: # type: sq_string # elements: 1 # length: 29 Equality for duration arrays. # name: # type: sq_string # elements: 1 # length: 12 duration.mad # name: # type: sq_string # elements: 1 # length: 371 duration: M = mad (D) duration: M = mad (D, opt) duration: M = mad (D, opt, dim) duration: M = mad (D, opt, vecdim) duration: M = mad (D, opt, 'all') Compute the mean or median absolute deviation of a duration array. This method overloads the core mad function for duration arrays. The functionality is identical to core mad function. Type help mad for more information. # name: # type: sq_string # elements: 1 # length: 66 Compute the mean or median absolute deviation of a duration array. # name: # type: sq_string # elements: 1 # length: 13 duration.mape # name: # type: sq_string # elements: 1 # length: 638 duration: E = mape (F, A) duration: E = mape (F, A, dim) duration: E = mape (F, A, vecdim) duration: E = mape (F, A, 'all') duration: E = mape (..., nanflag) duration: E = mape (..., zeroflag) duration: E = mape (..., 'Weights', W) Compute the mean absolute percentage error between duration arrays. This method overloads the core mape function for duration arrays. The functionality is identical to core mape function. Type help mape for more information. Note that MAPE is expressed as a percentage. Thus, the returned argument is a numeric array of double type and not a duration array. However, both F and A must be duration arrays. # name: # type: sq_string # elements: 1 # length: 67 Compute the mean absolute percentage error between duration arrays. # name: # type: sq_string # elements: 1 # length: 12 duration.max # name: # type: sq_string # elements: 1 # length: 582 duration: M = max (D) duration: M = max (D, [], dim) duration: M = max (D, [], vecdim) duration: M = max (D, [], 'all') duration: M = max (D, [], nanflag) duration: M = max (D, [], ..., nanflag) duration: [M, index] = max (...) duration: [M, index] = max (..., 'linear') duration: M = max (D1, D2) duration: M = max (D1, D2, nanflag) duration: ... = max (..., 'ComparisonMethod', method) Find maximum values in duration arrays. This method overloads the core max function for duration arrays. The functionality is identical to core max function. Type help max for more information. # name: # type: sq_string # elements: 1 # length: 39 Find maximum values in duration arrays. # name: # type: sq_string # elements: 1 # length: 13 duration.maxk # name: # type: sq_string # elements: 1 # length: 1220 duration: B = maxk (A, K) duration: B = maxk (A, K, dim) duration: [B, index] = maxk (A, ...) Find the largest elements in a duration array. B = maxk (A, K) returns the K largest elements of the duration array A in descending order. If A is a vector, then B is a vector with K elements. If A is a matrix, then maxk operates along each column of A and B has K rows. For multidimensional arrays, maxk operates along the first non-singleton dimension. B = maxk (A, K, dim) operates along the dimension specified by dim. K must be a nonnegative integer scalar. If K is larger than the number of elements along the operating dimension, then all of them are returned. Inf and -Inf are ranked as ordinary values. Missing elements (NaN) are not ranked. They are appended after the ranked elements in their original order, and hence they only appear in B when K exceeds the number of non-missing elements along the operating dimension. Unlike sort, maxk has no 'MissingPlacement' option, since NaN elements are always placed last. Elements comparing as equal keep their original order. [B, index] = maxk (A, ...) also returns an index array containing the indices of the returned elements of A along the operating dimension. # name: # type: sq_string # elements: 1 # length: 46 Find the largest elements in a duration array. # name: # type: sq_string # elements: 1 # length: 13 duration.mean # name: # type: sq_string # elements: 1 # length: 430 duration: M = mean (D) duration: M = mean (D, dim) duration: M = mean (D, vecdim) duration: M = mean (D, 'all') duration: M = mean (..., nanflag) duration: M = mean (..., outtype) duration: M = mean (..., 'Weights', W) Compute the mean of the elements of a duration array. This method overloads the core mean function for duration arrays. The functionality is identical to core mean function. Type help mean for more information. # name: # type: sq_string # elements: 1 # length: 53 Compute the mean of the elements of a duration array. # name: # type: sq_string # elements: 1 # length: 15 duration.median # name: # type: sq_string # elements: 1 # length: 417 duration: M = median (D) duration: M = median (D, dim) duration: M = median (D, vecdim) duration: M = median (D, 'all') duration: M = median (..., nanflag) duration: M = median (..., outtype) Compute the median value of the elements of a duration array. This method overloads the core median function for duration arrays. The functionality is identical to core median function. Type help median for more information. # name: # type: sq_string # elements: 1 # length: 61 Compute the median value of the elements of a duration array. # name: # type: sq_string # elements: 1 # length: 21 duration.milliseconds # name: # type: sq_string # elements: 1 # length: 213 duration: X = milliseconds (D) Duration equivalent numeric values in milliseconds. X = milliseconds (D) converts durations in D to the equivalent number of milliseconds. X is a double array of the same size as D. # name: # type: sq_string # elements: 1 # length: 51 Duration equivalent numeric values in milliseconds. # name: # type: sq_string # elements: 1 # length: 12 duration.min # name: # type: sq_string # elements: 1 # length: 582 duration: M = min (D) duration: M = min (D, [], dim) duration: M = min (D, [], vecdim) duration: M = min (D, [], 'all') duration: M = min (D, [], nanflag) duration: M = min (D, [], ..., nanflag) duration: [M, index] = min (...) duration: [M, index] = min (..., 'linear') duration: M = min (D1, D2) duration: M = min (D1, D2, nanflag) duration: ... = min (..., 'ComparisonMethod', method) Find minimum values in duration arrays. This method overloads the core min function for duration arrays. The functionality is identical to core min function. Type help min for more information. # name: # type: sq_string # elements: 1 # length: 39 Find minimum values in duration arrays. # name: # type: sq_string # elements: 1 # length: 13 duration.mink # name: # type: sq_string # elements: 1 # length: 1221 duration: B = mink (A, K) duration: B = mink (A, K, dim) duration: [B, index] = mink (A, ...) Find the smallest elements in a duration array. B = mink (A, K) returns the K smallest elements of the duration array A in ascending order. If A is a vector, then B is a vector with K elements. If A is a matrix, then mink operates along each column of A and B has K rows. For multidimensional arrays, mink operates along the first non-singleton dimension. B = mink (A, K, dim) operates along the dimension specified by dim. K must be a nonnegative integer scalar. If K is larger than the number of elements along the operating dimension, then all of them are returned. Inf and -Inf are ranked as ordinary values. Missing elements (NaN) are not ranked. They are appended after the ranked elements in their original order, and hence they only appear in B when K exceeds the number of non-missing elements along the operating dimension. Unlike sort, mink has no 'MissingPlacement' option, since NaN elements are always placed last. Elements comparing as equal keep their original order. [B, index] = mink (A, ...) also returns an index array containing the indices of the returned elements of A along the operating dimension. # name: # type: sq_string # elements: 1 # length: 47 Find the smallest elements in a duration array. # name: # type: sq_string # elements: 1 # length: 14 duration.minus # name: # type: sq_string # elements: 1 # length: 692 duration: C = minus (A, B) Subtraction for duration arrays. C = minus (A, B) is the equivalent of the syntax C = A - B and returns the subtraction of B from A by subtracting the corresponding elements. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. The size of C is determined by the size compatibility of A and B. One of the input arguments can also be a numeric array in which case its elements are treated as a number of 24-hour days. If the second argument B is a calendarDuration arrays, then the returned array C is also a calendarDuration array. # name: # type: sq_string # elements: 1 # length: 32 Subtraction for duration arrays. # name: # type: sq_string # elements: 1 # length: 16 duration.minutes # name: # type: sq_string # elements: 1 # length: 193 duration: X = minutes (D) Duration equivalent numeric values in minutes. X = minutes (D) converts durations in D to the equivalent number of minutes. X is a double array of the same size as D. # name: # type: sq_string # elements: 1 # length: 46 Duration equivalent numeric values in minutes. # name: # type: sq_string # elements: 1 # length: 17 duration.mldivide # name: # type: sq_string # elements: 1 # length: 758 duration: C = mldivide (A, B) Left division of duration arrays, A \ B. C = mldivide (A, B) divides B by A. The denominator A decides what the result means and must be a scalar. Dividing a duration by a number scales it, giving a duration; dividing one duration by another gives how many times one goes into the other, a plain number carrying no unit. A zero denominator gives Inf, as it does for numbers. Dividing a number by a duration is not defined, having no meaning, and neither is a non-scalar denominator. The display format of a duration result is that of B. Unlike MATLAB, an integer denominator is accepted and converted, as it is by rdivide and times here; MATLAB requires a double while accepting a logical, which the same rule cannot explain. # name: # type: sq_string # elements: 1 # length: 40 Left division of duration arrays, A \ B. # name: # type: sq_string # elements: 1 # length: 12 duration.mod # name: # type: sq_string # elements: 1 # length: 666 duration: C = mod (A, B) Remainder after division of duration arrays, with the sign of the divisor. C = mod (A, B) returns the remainder left after dividing A by B, as a duration array of the size the two broadcast to. At least one of A and B must be a duration; the other may be a duration or a numeric array, which is read as a number of days, the unit a duration counts in. The result takes its sign from B and is zero when B is, which is what distinguishes mod from rem: mod of an hour by -1 day differs from rem of the same. A remainder by a zero duration is A itself. The display format is taken from whichever operand is a duration, or from A when both are. # name: # type: sq_string # elements: 1 # length: 74 Remainder after division of duration arrays, with the sign of the divisor. # name: # type: sq_string # elements: 1 # length: 13 duration.mode # name: # type: sq_string # elements: 1 # length: 367 duration: M = mode (D) duration: M = mode (D, dim) duration: M = mode (D, vecdim) duration: M = mode (D, 'all') duration: [M, F, C] = mode (...) Compute the most frequently occurring value in a duration array. This method overloads the core mode function for duration arrays. The functionality is identical to core mode function. Type help mode for more information. # name: # type: sq_string # elements: 1 # length: 64 Compute the most frequently occurring value in a duration array. # name: # type: sq_string # elements: 1 # length: 17 duration.mrdivide # name: # type: sq_string # elements: 1 # length: 552 duration: C = mrdivide (A, B) Right division of duration arrays, A / B. C = mrdivide (A, B) divides A by B and is the mirror of mldivide: A / B is B \ A. The denominator B decides what the result means and must be a scalar. Dividing a duration by a number scales it, giving a duration; dividing one duration by another gives how many times one goes into the other, a plain number carrying no unit. Dividing a number by a duration is not defined, and neither is a non-scalar denominator. For element-wise division by an array of divisors, see rdivide. # name: # type: sq_string # elements: 1 # length: 41 Right division of duration arrays, A / B. # name: # type: sq_string # elements: 1 # length: 15 duration.mtimes # name: # type: sq_string # elements: 1 # length: 385 duration: C = mtimes (A, B) Matrix multiplication for duration arrays. C = mtimes (A, B) is the equivalent of the syntax C = A * B and returns the matrix multiplication product of input arrays A and B, one of which must be a numeric array and the other a duration array. The columns of A must equal the rows of B and the size of C is determined by the rows of A and the columns of B. # name: # type: sq_string # elements: 1 # length: 42 Matrix multiplication for duration arrays. # name: # type: sq_string # elements: 1 # length: 14 duration.ndims # name: # type: sq_string # elements: 1 # length: 142 duration: out = ndims (D) Number of dimensions in a duration array. out = ndims (D) returns the number of dimensions of the duration array D. # name: # type: sq_string # elements: 1 # length: 41 Number of dimensions in a duration array. # name: # type: sq_string # elements: 1 # length: 11 duration.ne # name: # type: sq_string # elements: 1 # length: 688 duration: TF = ne (A, B) Equality for duration arrays. TF = ne (A, B) is the equivalent of the syntax TF = A != B and returns a logical array whose elements set to true where the corresponding elements of A and B are not equal and set to false otherwise. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. The size of C is determined by the size compatibility of A and B. One of the input arguments can also be a character vector, a cell array of character vectors, or a string array representing duration strings or a numeric array representing days. # name: # type: sq_string # elements: 1 # length: 29 Equality for duration arrays. # name: # type: sq_string # elements: 1 # length: 12 duration.nnz # name: # type: sq_string # elements: 1 # length: 148 duration: out = nnz (D) Number of nonzero elements in duration array. out = nnz (D) returns the number of nonzero elements in the duration array D. # name: # type: sq_string # elements: 1 # length: 45 Number of nonzero elements in duration array. # name: # type: sq_string # elements: 1 # length: 14 duration.numel # name: # type: sq_string # elements: 1 # length: 144 duration: out = numel (D) Total number of elements in a duration array. out = numel (D) returns the number of elements in the duration array D. # name: # type: sq_string # elements: 1 # length: 45 Total number of elements in a duration array. # name: # type: sq_string # elements: 1 # length: 16 duration.permute # name: # type: sq_string # elements: 1 # length: 425 duration: B = permute (A, dims) Generalized transpose for a duration N-D array. B = permute (A, dims) returns the generalized transpose of the duration array A by rearranging its dimensions according to the permutation vector specified in dims. dims must index all the dimensions 1:ndims (A) of the input array A, in any order, but only once. The Nth dimension of A gets remapped to the dimension in B specified by dims(N). # name: # type: sq_string # elements: 1 # length: 47 Generalized transpose for a duration N-D array. # name: # type: sq_string # elements: 1 # length: 13 duration.plus # name: # type: sq_string # elements: 1 # length: 673 duration: C = plus (A, B) Addition for duration arrays. C = plus (A, B) is the equivalent of the syntax C = A + B and returns the sum of A and B by adding the corresponding elements. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. The size of C is determined by the size compatibility of A and B. One of the input arguments can also be a numeric array in which case its elements are treated as a number of 24-hour days. If the second argument B is a calendarDuration arrays, then the returned array C is also a calendarDuration array. # name: # type: sq_string # elements: 1 # length: 29 Addition for duration arrays. # name: # type: sq_string # elements: 1 # length: 16 duration.prctile # name: # type: sq_string # elements: 1 # length: 415 duration: Q = prctile (D) duration: q = prctile (D, p) duration: Q = prctile (D, p, dim) duration: Q = prctile (D, p, vecdim) duration: Q = prctile (D, p, 'all') duration: Q = prctile (D, p, ..., method) Compute the percentiles of a duration array. This method overloads the core prctile function for duration arrays. The functionality is identical to core prctile function. Type help prctile for more information. # name: # type: sq_string # elements: 1 # length: 44 Compute the percentiles of a duration array. # name: # type: sq_string # elements: 1 # length: 17 duration.quantile # name: # type: sq_string # elements: 1 # length: 501 duration: Q = quantile (D) duration: Q = quantile (D, p) duration: Q = quantile (D, n) duration: Q = quantile (D, ..., dim) duration: Q = quantile (D, ..., vecdim) duration: Q = quantile (D, ..., 'all') duration: Q = quantile (D, p, ..., method) duration: Q = quantile (D, n, ..., method) Compute the quantiles of a duration array. This method overloads the core quantile function for duration arrays. The functionality is identical to core quantile function. Type help quantile for more information. # name: # type: sq_string # elements: 1 # length: 42 Compute the quantiles of a duration array. # name: # type: sq_string # elements: 1 # length: 14 duration.range # name: # type: sq_string # elements: 1 # length: 350 duration: R = range (D) duration: R = range (D, dim) duration: R = range (D, vecdim) duration: R = range (D, 'all') duration: R = range (..., nanflag) Compute the range of a duration array. This method overloads the core range function for duration arrays. The functionality is identical to core range function. Type help range for more information. # name: # type: sq_string # elements: 1 # length: 38 Compute the range of a duration array. # name: # type: sq_string # elements: 1 # length: 16 duration.rdivide # name: # type: sq_string # elements: 1 # length: 666 duration: C = rdivide (A, B) Element-wise right division for duration arrays. C = rdivide (A, B) is the equivalent of the syntax C = A ./ B and returns the element-wise division of the duration array A by the corresponding elements of input array B, which can either be a duration or a numeric array. If B is a duration array, then C is a double numeric array. If B is a numeric array, then C is a duration array. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. The size of C is determined by the size compatibility of A and B. # name: # type: sq_string # elements: 1 # length: 48 Element-wise right division for duration arrays. # name: # type: sq_string # elements: 1 # length: 12 duration.rem # name: # type: sq_string # elements: 1 # length: 648 duration: C = rem (A, B) Remainder after division of duration arrays, with the sign of the dividend. C = rem (A, B) returns the remainder left after dividing A by B, as a duration array of the size the two broadcast to. At least one of A and B must be a duration; the other may be a duration or a numeric array, which is read as a number of days, the unit a duration counts in. The result takes its sign from A, where mod takes it from B: the two agree only where the operands share a sign. A remainder by a zero duration is NaN, where mod gives A itself. The display format is taken from whichever operand is a duration, or from A when both are. # name: # type: sq_string # elements: 1 # length: 75 Remainder after division of duration arrays, with the sign of the dividend. # name: # type: sq_string # elements: 1 # length: 16 duration.repelem # name: # type: sq_string # elements: 1 # length: 922 duration: B = repelem (A, n) duration: B = repelem (A, d1, ..., dN) Repeat copies of duration array elements. B = repelem (A, n) returns a duration vector B containing repeated elements of the input A, which must be a duration vector. If n is a scalar, each element of A is repeated n times along the non-singleton dimension of A. If n is a vector, it must have the same elements as A, in which case it specifies the number of times to repeat each corresponding element of A. B = repelem (A, d1, ..., dN) returns an array B with each element of A repeated according to the the list of input arguments d1, ..., dN each corresponding to a different dimension 1:ndims (A) of the input array A. d1, ..., dN must be either scalars or vectors with the same length as the corresponding dimension of A containing non-negative integer values specifying the number of repetitions of each element along the corresponding dimension. # name: # type: sq_string # elements: 1 # length: 41 Repeat copies of duration array elements. # name: # type: sq_string # elements: 1 # length: 17 duration.repelems # name: # type: sq_string # elements: 1 # length: 431 duration: B = repelems (A, R) Construct a vector of repeated duration array. B = repelems (A, R) returns a duration vector B containing repeated elements of the input A, which must be a duration vector. R must be a 2xN matrix of integers. Entries in the first row of R correspond to the linear indexing of the elements in A to be repeated. The corresponding entries in the second row of R specify the repeat count of each element. # name: # type: sq_string # elements: 1 # length: 46 Construct a vector of repeated duration array. # name: # type: sq_string # elements: 1 # length: 15 duration.repmat # name: # type: sq_string # elements: 1 # length: 562 duration: B = repmat (A, n) duration: B = repmat (A, d1, ..., dN) duration: B = repmat (A, dimvec) Repeat copies of a duration array. B = repmat (A, n) returns a duration array B containing n copies of the input duration array A along every dimension of A. B = repmat (A, d1, ..., dN) returns an array B containing copies of A along the dimensions specified by the list of scalar integer values d1, ..., dN, which specify how many copies of A are made in each dimension. B = repmat (A, dimvec) is equivalent to the previous syntax with dimvec = [d1, ..., dN]. # name: # type: sq_string # elements: 1 # length: 34 Repeat copies of a duration array. # name: # type: sq_string # elements: 1 # length: 16 duration.reshape # name: # type: sq_string # elements: 1 # length: 688 duration: B = reshape (A, d1, ..., dN) duration: B = reshape (A, ..., [], ...) duration: B = reshape (A, dimvec) Reshape duration array. B = reshape (A, d1, ..., dN) returns a duration array B with specified dimensions d1, ..., dN, whose elements are taken columnwise from the duration array A. The product of d1, ..., dN must equal the total number of elements in A. B = reshape (A, ..., [], ...) returns a duration array B with one dimension unspecified which is calculated automatically so that the product of dimensions in B matches the total elements in A, which must be divisible the product of specified dimensions. An empty matrix ([]) is used to flag the unspecified dimension. # name: # type: sq_string # elements: 1 # length: 23 Reshape duration array. # name: # type: sq_string # elements: 1 # length: 13 duration.rmse # name: # type: sq_string # elements: 1 # length: 415 duration: E = rmse (F, A) duration: E = rmse (F, A, dim) duration: E = rmse (F, A, vecdim) duration: E = rmse (F, A, 'all') duration: E = rmse (..., nanflag) duration: E = rmse (..., 'Weights', W) Compute the root mean squared error between duration arrays. This method overloads the core rmse function for duration arrays. The functionality is identical to core rmse function. Type help rmse for more information. # name: # type: sq_string # elements: 1 # length: 60 Compute the root mean squared error between duration arrays. # name: # type: sq_string # elements: 1 # length: 14 duration.round # name: # type: sq_string # elements: 1 # length: 525 duration: Y = round (D) duration: Y = round (D, unit) Round to the nearest integer time unit for duration arrays. Y = round (D) rounds each element of the duration array D to the nearest integer number of seconds to that element. In case of a tie, return the one further away from zero. Y = round (D, unit) rounds each element of the duration array D to the nearest integer number of the specified unit of time to that element. unit must be one of the following values: 'seconds' (default) 'minutes' 'hours' 'days' 'years' # name: # type: sq_string # elements: 1 # length: 59 Round to the nearest integer time unit for duration arrays. # name: # type: sq_string # elements: 1 # length: 16 duration.seconds # name: # type: sq_string # elements: 1 # length: 193 duration: X = seconds (D) Duration equivalent numeric values in seconds. X = seconds (D) converts durations in D to the equivalent number of seconds. X is a double array of the same size as D. # name: # type: sq_string # elements: 1 # length: 46 Duration equivalent numeric values in seconds. # name: # type: sq_string # elements: 1 # length: 16 duration.setdiff # name: # type: sq_string # elements: 1 # length: 1241 duration: C = setdiff (A, B) duration: C = setdiff (A, B, 'rows') duration: C = setdiff (A, B, ..., order) duration: [C, ixA] = setdiff (...) Set difference of two duration arrays. C = setdiff (A, B) returns the unique common values of the duration arrays A and B. Either A or B input arguments can also be a duration string specified as a character vector, a string array, or a cell array of character vectors, or a numeric array representing 24-hour days. In such case, the input is promoted to a duration array prior to calculating the intersection. If both A and B are row vectors, then C is also a row vector, otherwise intersect returns a column vector. C = setdiff (A, B, 'rows' returns the unique common rows of the duration matrices A and B, which must have the same number of columns. By default, the rows in duration matrix C are in sorted order. ... = setdiff (A, B, ..., order) also specifies the order of the returned unique values. order can be 'sorted', which is the default behavior, or 'stable', in which case the unique values are returned in order of appearance. [C, ixA] = setdiff (...) also returns the index vector ixA such that C = A(ixA), unless the 'rows' optional argument is given, in which case C = A(ixA,:). # name: # type: sq_string # elements: 1 # length: 38 Set difference of two duration arrays. # name: # type: sq_string # elements: 1 # length: 15 duration.setxor # name: # type: sq_string # elements: 1 # length: 1282 duration: C = setxor (A, B) duration: C = setxor (A, B, 'rows') duration: C = setxor (A, B, ..., order) duration: [C, ixA, ixB] = setxor (...) Set exclusive-or of two duration arrays. C = setxor (A, B) returns the unique common values of the duration arrays A and B. Either A or B input arguments can also be a duration string specified as a character vector, a string array, or a cell array of character vectors, or a numeric array representing 24-hour days. In such case, the input is promoted to a duration array prior to calculating the intersection. If both A and B are row vectors, then C is also a row vector, otherwise intersect returns a column vector. C = setxor (A, B, 'rows' returns the unique common rows of the duration matrices A and B, which must have the same number of columns. By default, the rows in duration matrix C are in sorted order. ... = setxor (A, B, ..., order) also specifies the order of the returned unique values. order can be 'sorted', which is the default behavior, or 'stable', in which case the unique values are returned in order of appearance. [C, ixA, ixB] = setxor (...) also returns index vectors ixA and ixB such that C = A(ixA) and C = B(ixB), unless the 'rows' optional argument is given, in which case C = A(ixA,:) and C = B(ixB,:). # name: # type: sq_string # elements: 1 # length: 40 Set exclusive-or of two duration arrays. # name: # type: sq_string # elements: 1 # length: 13 duration.sign # name: # type: sq_string # elements: 1 # length: 456 duration: out = sign (D) duration: out = sign (D, unit) Compute the signum function for duration arrays. out = sign (D) returns an array of doubles, out, the same size as the duration array D, where each element has one following values: 1 if the corresponding element of D is greater than 0. 0 if the corresponding element of D is equal to 0. -1 if the corresponding element of D is less than 0. NaN if the corresponding element of D is a missing value. # name: # type: sq_string # elements: 1 # length: 48 Compute the signum function for duration arrays. # name: # type: sq_string # elements: 1 # length: 13 duration.size # name: # type: sq_string # elements: 1 # length: 1033 duration: sz = size (D) duration: dim_sz = size (D, dim) duration: dim_sz = size (D, d1, d2, ...) duration: [rows, columns, ..., dim_n_sz] = size (...) Return the size of a duration array. sz = size (D) returns a row vector with the size (number of elements) of each dimension for the duration array D. dim_sz = size (D, dim) returns the size of the corresponding dimension specified in dim. If dim is a vector, then dim_sz is a vector of the same length and with each element corresponding to a specified dimension. Multiple dimensions may also be specified as separate arguments. With a single output argument, size returns a row vector. With several, the size of dimension N is returned in the Nth argument. Asking for fewer output arguments than the array has dimensions folds the trailing dimensions into the last one, so [r, c] = size (D) on a 2-by-2-by-2 array returns 2 and 4; asking for more pads with ones. When a dimension is named, however, the number of output arguments must equal the number of dimensions requested. # name: # type: sq_string # elements: 1 # length: 36 Return the size of a duration array. # name: # type: sq_string # elements: 1 # length: 17 duration.skewness # name: # type: sq_string # elements: 1 # length: 533 duration: y = skewness (D) duration: y = skewness (D, flag) duration: y = skewness (D, flag, dim) duration: y = skewness (D, flag, vecdim) duration: y = skewness (D, flag, 'all') Compute the sample skewness of a duration array. This method overloads the core skewness function for duration arrays. The functionality is identical to core skewness function. Type help skewness for more information. Note that skewness is a dimensionless quantity Thus, the returned argument is a numeric array of double type and not a duration array. # name: # type: sq_string # elements: 1 # length: 48 Compute the sample skewness of a duration array. # name: # type: sq_string # elements: 1 # length: 13 duration.sort # name: # type: sq_string # elements: 1 # length: 1821 duration: B = sort (A) duration: B = sort (A, dim) duration: B = sort (A, direction) duration: B = sort (A, dim, direction) duration: B = sort (..., 'MissingPlacement', MP) duration: B = sort (..., 'ComparisonMethod', CM) duration: [B, index] = sort (A, ...) Sort elements in a duration array. B = sort (A) sorts the duration array A in ascending order. If A is a matrix, sort (A) sorts each column of A in ascending order. For multidimensional arrays, mode (A) sorts along the first non-singleton dimension. B = sort (A, dim) sorts along the dimension specified by dim. B = sort (A, direction) also specifies the sorting direction, which can be either 'ascend' (default) or 'descend'. B = sort (..., 'MissingPlacement', MP) specifies where to place the missing elements () returned in B with one of the following options specified in MP: 'auto', which is the default, places missing elements last for ascending sort and first for descending sort. 'first' places missing elements first. 'last' places missing elements last. B = sort (..., 'ComparisonMethod', CM) specifies the comparison method for determining the order of elements returned in B with one of following options: 'auto', which is the default, sorts by real (A). 'real' sorts by real (A). 'abs' sorts by abs (A). [B, index] = sort (A, ...) also returns a sorting index containing the original indices of the elements in the sorted array. index is the same size as A and it comprises indexing vectors oriented along the operating dimensions. If A is a vector, then index contains the original linear indices of the elements in the sorted vector B such that B = A(index). If A is an MxN matrix and dim = 1, then index contains the original row indices of the elements in the sorted vector B such that for j = 1:N, B(:,j) = A(index(:,j),j). # name: # type: sq_string # elements: 1 # length: 34 Sort elements in a duration array. # name: # type: sq_string # elements: 1 # length: 17 duration.sortrows # name: # type: sq_string # elements: 1 # length: 1953 duration: B = sortrows (A) duration: B = sortrows (A, col) duration: B = sortrows (A, direction) duration: B = sortrows (A, col, direction) duration: B = sortrows (..., 'MissingPlacement', MP) duration: [B, index] = sortrows (A, ...) Sort rows in a duration array. B = sortrows (A) sorts the rows of the 2-D duration array A in ascending order. The sorted array B has the same size as A. B = sortrows (A, col) sorts A according to to the columns specified by the numeric vector col, which must explicitly contain non-zero integers whose absolute values index existing columns in A. Positive elements sort the corresponding columns in ascending order, while negative elements sort the corresponding columns in descending order. B = sortrows (A, direction) also specifies the sorting direction, which can be either 'ascend' (default) or 'descend' applying to all columns in A. Alternatively, direction can be either a string array or a cell array of character vectors specifying the sorting direction for each individual column of A, in which case the number of elements in direction must equal the number of columns in A. B = sortrows (A, col, direction) sorts the categorical array A according to the columns specified in col using the corresponding sorting direction specified in direction. In this case, the sign of the values in col is ignored. col and direction must have the same number of elements, but not necessarily equal to the columns of A. B = sortrows (..., 'MissingPlacement', MP) specifies where to place the missing elements (NaN) returned in B with any of the following options specified in MP: 'auto', which is the default, places missing elements last for ascending sort and first for descending sort. 'first' places missing elements first. 'last' places missing elements last. [B, index] = sortrows (A, ...) also returns an index vector containing the original row indices of A in the sorted matrix B such that B = A(index,:). # name: # type: sq_string # elements: 1 # length: 30 Sort rows in a duration array. # name: # type: sq_string # elements: 1 # length: 12 duration.std # name: # type: sq_string # elements: 1 # length: 410 duration: S = std (D) duration: S = std (D, w) duration: S = std (D, w, dim) duration: S = std (D, w, vecdim) duration: S = std (D, w, 'all') duration: S = std (..., nanflag) duration: [S, M] = std (...) Compute the standard deviation of a duration array. This method overloads the core std function for duration arrays. The functionality is identical to core std function. Type help std for more information. # name: # type: sq_string # elements: 1 # length: 51 Compute the standard deviation of a duration array. # name: # type: sq_string # elements: 1 # length: 12 duration.sum # name: # type: sq_string # elements: 1 # length: 348 duration: S = sum (D) duration: S = sum (D, dim) duration: S = sum (D, vecdim) duration: S = sum (D, 'all') duration: S = sum (..., nanflag) Compute the sum of the elements of a duration array. This method overloads the core sum function for duration arrays. The functionality is identical to core sum function. Type help sum for more information. # name: # type: sq_string # elements: 1 # length: 52 Compute the sum of the elements of a duration array. # name: # type: sq_string # elements: 1 # length: 14 duration.times # name: # type: sq_string # elements: 1 # length: 575 duration: C = times (A, B) Element-wise multiplication for duration arrays. C = times (A, B) is the equivalent of the syntax C = A .* B and returns the element-by-element multiplication product between the corresponding elements of input arrays A and B, one of which must be a numeric array and the other a duration array. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. The size of C is determined by the size compatibility of A and B. # name: # type: sq_string # elements: 1 # length: 48 Element-wise multiplication for duration arrays. # name: # type: sq_string # elements: 1 # length: 17 duration.topkrows # name: # type: sq_string # elements: 1 # length: 2117 duration: B = topkrows (A, K) duration: B = topkrows (A, K, col) duration: B = topkrows (A, K, direction) duration: B = topkrows (A, K, col, direction) duration: B = topkrows (..., 'MissingPlacement', MP) duration: [B, index] = topkrows (A, ...) Top K sorted rows of a duration array. B = topkrows (A, K) returns the top K rows of the 2-D duration array A sorted in descending order as a group. K must be a nonnegative integer scalar. If K is larger than the number of rows in A, then all of them are returned. Missing elements (NaN) are not ranked. Within each sort column they are placed after the elements that are defined, whichever direction is asked for, so a row is demoted only by a missing element in a column that actually decides its position. Rows comparing as equal keep their original order. B = topkrows (A, K, col) sorts using only the columns listed in the numeric vector col, which must contain positive integers indexing existing columns in A. Columns are used as sort keys in the order given, and those not listed are not used at all. The direction is descending unless direction says otherwise. B = topkrows (A, K, direction) sorts in the given direction, either 'descend' (default) or 'ascend' applying to all columns in A. Alternatively, direction can be a cell array of character vectors specifying the sorting direction for each individual column of A. B = topkrows (A, K, col, direction) combines an explicit column list with a per-column direction. B = topkrows (..., 'MissingPlacement', MP) specifies where the missing elements are placed within each sort column, with any of the following options specified in MP: 'last', which is the default, places missing elements last whichever direction is asked for. 'first' places missing elements first. 'auto' places missing elements last for an ascending sort and first for a descending one, as sortrows does. This is an Octave extension: MATLAB has no such option here and always ranks as 'last' does. [B, index] = topkrows (A, ...) also returns an index vector containing the original row indices of A in B, such that B = A(index,:). # name: # type: sq_string # elements: 1 # length: 38 Top K sorted rows of a duration array. # name: # type: sq_string # elements: 1 # length: 18 duration.transpose # name: # type: sq_string # elements: 1 # length: 167 duration: B = transpose (A) Transpose a duration matrix. B = transpose (A) is the equivalent of the syntax B = A.' and returns the transpose of the duration matrix A. # name: # type: sq_string # elements: 1 # length: 28 Transpose a duration matrix. # name: # type: sq_string # elements: 1 # length: 15 duration.uminus # name: # type: sq_string # elements: 1 # length: 168 duration: B = uminus (A) Unary minus for duration arrays. B = uminus (A) is the equivalent of the syntax B = - A and returns the input array with its elements negated. # name: # type: sq_string # elements: 1 # length: 32 Unary minus for duration arrays. # name: # type: sq_string # elements: 1 # length: 14 duration.union # name: # type: sq_string # elements: 1 # length: 1267 duration: C = union (A, B) duration: C = union (A, B, 'rows') duration: C = union (A, B, ..., order) duration: [C, ixA, ixB] = union (...) Set union of two duration arrays. C = union (A, B) returns the unique common values of the duration arrays A and B. Either A or B input arguments can also be a duration string specified as a character vector, a string array, or a cell array of character vectors, or a numeric array representing 24-hour days. In such case, the input is promoted to a duration array prior to calculating the intersection. If both A and B are row vectors, then C is also a row vector, otherwise intersect returns a column vector. C = union (A, B, 'rows' returns the unique common rows of the duration matrices A and B, which must have the same number of columns. By default, the rows in duration matrix C are in sorted order. ... = union (A, B, ..., order) also specifies the order of the returned unique values. order can be 'sorted', which is the default behavior, or 'stable', in which case the unique values are returned in order of appearance. [C, ixA, ixB] = union (...) also returns index vectors ixA and ixB such that C = A(ixA) and C = B(ixB), unless the 'rows' optional argument is given, in which case C = A(ixA,:) and C = B(ixB,:). # name: # type: sq_string # elements: 1 # length: 33 Set union of two duration arrays. # name: # type: sq_string # elements: 1 # length: 15 duration.unique # name: # type: sq_string # elements: 1 # length: 1784 duration: B = unique (A) duration: B = unique (A, setOrder) duration: B = unique (A, occurrence) duration: B = unique (A, setOrder, occurrence) duration: B = unique (A, occurrence, setOrder) duration: B = unique (A, ..., 'rows') duration: [B, ixA, ixB] = unique (...) Unique values in a duration array. B = unique (A) returns the unique values of the duration array A in sorted order. B = unique (A, setOrder) returns the unique values of the duration array A in an order as specified by setOrder, which can be either of the following values: 'sorted' (default) returns the unique values sorted in ascending order. 'stable' returns the unique values according to their order of occurrence. B = unique (A, occurrence) returns the unique values of the duration array A according to their order of occurrence. occurrence can be either of the following values: 'first' (default) returns the first occurrence of each unique value, i.e. the lowest possible indices are returned. 'last' returns the last occurrence of each unique value, i.e. the highest possible indices are returned. You can specify setOrder and occurrence arguments together. B = unique (A, ..., 'rows') returns the unique rows of A by treating each row as a single entity. The 'rows' option can be used alone or in any combination with the setOrder and occurrence arguments. 'rows' can be placed at any position in the function's argument list after the input array A. However, this syntax is only valid for 2-dimensional duration arrays. [B, ixA, ixB] = unique (...) also returns index vectors ixA and ixB using any of the previous syntaxes. ixA and ixB map the arrays A and B to one another such that B = A(ixA) and A = B(ixB). When the 'rows' optional argument is specified, then B = A(ixA,:) and A = B(ixB,:). # name: # type: sq_string # elements: 1 # length: 34 Unique values in a duration array. # name: # type: sq_string # elements: 1 # length: 14 duration.uplus # name: # type: sq_string # elements: 1 # length: 149 duration: B = uplus (A) Unary plus for duration arrays. B = uplus (A) is the equivalent of the syntax B = + A and returns the input array unaltered. # name: # type: sq_string # elements: 1 # length: 31 Unary plus for duration arrays. # name: # type: sq_string # elements: 1 # length: 16 duration.vertcat # name: # type: sq_string # elements: 1 # length: 522 duration: C = vertcat (A, B, ...) Vertical concatenation of duration arrays. C = vertcat (A, B, ... is the equivalent of the syntax B = [A; B; ...] and vertically concatenates the duration arrays A, B, .... All input arrays must have the same size except along the first dimension. All of the input arrays may also be string arrays or cell arrays of character vectors of compatible size. Additionally, an input can be a numeric matrix, which when parsed to the constructor will return a duration array of compatible size. # name: # type: sq_string # elements: 1 # length: 42 Vertical concatenation of duration arrays. # name: # type: sq_string # elements: 1 # length: 14 duration.years # name: # type: sq_string # elements: 1 # length: 241 duration: X = years (D) Duration equivalent numeric values in fixed-length years. X = years (D) converts durations in D to the equivalent number of fixed-length years (1 year equals 365.2425 days). X is a double array of the same size as D. # name: # type: sq_string # elements: 1 # length: 57 Duration equivalent numeric values in fixed-length years. # name: # type: sq_string # elements: 1 # length: 11 eventfilter # name: # type: sq_string # elements: 1 # length: 803 datatypes: eventfilter Subscript into a timetable by what was happening rather than by when. A utility class that builds a condition against the variables of the event table attached to a timetable, and selects the rows the matching events cover. The condition picks out events; the rows follow from them. ef = eventfilter (tt); tt(ef.EventLabels == "rain", :) An event covers the times from its own onwards and stops short of its end, and an event with neither a length nor an end covers only a row at exactly its own time. Rows covered by more than one matching event are selected once: a filter answers which rows, not how often. Time names the event's own time, not the row's, so ef.Time > d selects the rows of the events that began after d. See also: eventtable, timerange, withtol, timetable # name: # type: sq_string # elements: 1 # length: 69 Subscript into a timetable by what was happening rather than by when. # name: # type: sq_string # elements: 1 # length: 22 eventfilter.eventTimes # name: # type: sq_string # elements: 1 # length: 236 eventfilter: t = eventTimes (ef, tt) Return the times of the events of tt the filter matches. The times come back in the order the event table holds them. It is what a timerange reads when an event filter is given as one of its bounds. # name: # type: sq_string # elements: 1 # length: 56 Return the times of the events of tt the filter matches. # name: # type: sq_string # elements: 1 # length: 23 eventfilter.eventfilter # name: # type: sq_string # elements: 1 # length: 807 eventfilter: ef = eventfilter (tt) eventfilter: ef = eventfilter (eventLabels) Create an event filter. ef = eventfilter (tt) creates a filter that may be written against the variables of the event table attached to the timetable tt, together with Time, the events' own times. tt must have an event table attached, since the names come from it. ef = eventfilter (eventLabels) creates a filter that matches the events carrying any of the given labels, without a timetable to take the names from. It is a whole condition already and needs no comparison written against it. A filter is used as a row subscript, and selects the rows the matching events cover. Conditions combine with & and |; ~ is not supported, an event filter having no complement worth the name. See also: eventtable, timerange, timetable # name: # type: sq_string # elements: 1 # length: 23 Create an event filter. # name: # type: sq_string # elements: 1 # length: 22 eventfilter.properties # name: # type: sq_string # elements: 1 # length: 276 eventfilter: names = properties (ef) Return the names a condition may be written against. They are the variables of the event table the filter came from, together with Time. A filter made from a list of labels carries none, having been given no event table to read them from. # name: # type: sq_string # elements: 1 # length: 52 Return the names a condition may be written against. # name: # type: sq_string # elements: 1 # length: 22 eventfilter.rowIndices # name: # type: sq_string # elements: 1 # length: 96 eventfilter: ix = rowIndices (ef, tt) Return the positions of the rows of tt the filter selects. # name: # type: sq_string # elements: 1 # length: 58 Return the positions of the rows of tt the filter selects. # name: # type: sq_string # elements: 1 # length: 10 eventtable # name: # type: sq_string # elements: 1 # length: 3005 datatypes: eventtable Timetable of events, describing what was happening rather than what was measured. An event table is a timetable whose rows are events. Each row has a time, optionally a label naming the event, and optionally a length or an end time making it an interval rather than an instant. It exists so that the rows of another timetable can be selected by what was happening when they were recorded, rather than by the clock alone. An event table is a timetable and inherits every one of its methods. help does not follow method inheritance, so help eventtable.sortrows does not resolve and help timetable.sortrows is where that method is documented; where a method behaves differently for an event table, its own documentation says so. Three properties say which variables describe the events, and each holds a variable name rather than the values themselves: EventLabelsVariable, the variable naming each event. EventLengthsVariable, how long each event lasted. EventEndsVariable, when each event finished. The last two are mutually exclusive: an event says its extent one way or the other, never both. An event with neither is an instant. Because the properties hold names, an operation that stops a name resolving affects the property. Removing the variable clears it, and so does deleting it by assigning an empty matrix; renaming the variable carries the property across to the new name, which is a deliberate departure from MATLAB, where the designation is lost and renaming back does not restore it. Moving a variable or converting its type leaves the property alone. An event table cannot itself carry an event table. It inherits the Events property of a timetable but cannot read or assign it: asking for it, or assigning one, raises. An interval event is half-open on the right: an event beginning at 02:00 and lasting two hours covers 02:00 and 03:00 but not 04:00. An event of zero length therefore covers nothing at all. Three things read an event table once it is attached to a timetable by that timetable's Events property. syncevents copies the events' data onto the rows they cover, repeating a row that more than one event covers. eventfilter selects the rows the events matching a condition cover, and a row covered more than once is selected once. timerange takes two event filters as bounds, running from one event to another. In the other direction, extractevents builds an event table out of a timetable's own rows. A binary operation over two timetables carrying event tables merges them, and refuses where their three event properties disagree; a row-preserving one carries the event table through unchanged. Labels given as text are held as a cell array of character vectors, which is how this package holds a list of names; a string given as such is kept as one. An eventfilter compares either as text, so a condition reads the same whichever they are. See also: timetable, table, istimetable, eventfilter, timetable.extractevents, timetable.syncevents # name: # type: sq_string # elements: 1 # length: 81 Timetable of events, describing what was happening rather than what was measured. # name: # type: sq_string # elements: 1 # length: 27 eventtable.CustomProperties # name: # type: sq_string # elements: 1 # length: 1136 tabular: property CustomProperties Customized metadata of table and its variables Custom properties that contain metadata of a table and its variables. By default, this is an empty container. Each custom property holds either table metadata or per-variable metadata, according to the property type ('table' or 'variable') specified when the property is created with the addprop method. A variable-scoped property holds one element per variable. You can add an individual custom property only by using the addprop method and you can only remove a custom property with the rmprop method. To access existing custom properties use dot name structure assignment as in tbl.Properties.CustomProperties.PropertyName, where PropertyName is the name used with the addprop method. The whole set may also be taken from another table by assigning that table's CustomProperties to this one, which replaces every custom property with those of the other table, their types included. A variable-scoped property arriving that way must hold one element for each variable of the receiving table, or be a 0-by-0 empty. Nothing else can be assigned there. # name: # type: sq_string # elements: 1 # length: 46 Customized metadata of table and its variables # name: # type: sq_string # elements: 1 # length: 22 eventtable.Description # name: # type: sq_string # elements: 1 # length: 300 tabular: property Description Table description Table description specified as a character vector or a string scalar. If specified as a string scalar, it is converted and stored internally as a character vector. You can access the Description property of a table tbl with tbl.Properties.Description. # name: # type: sq_string # elements: 1 # length: 17 Table description # name: # type: sq_string # elements: 1 # length: 25 eventtable.DimensionNames # name: # type: sq_string # elements: 1 # length: 818 tabular: property DimensionNames Dimension names Dimension names specified as a two-element cell array of character vectors or a two-element string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. You can access the DimensionNames property of a table tbl with tbl.Properties.DimensionNames. By default, DimensionNames is specified as 'Row', 'Variables'. You can access table data per rows or per columns by using either one of the two dimension names, respectively. However, if the table contains row names, then the first element of the DimensionNames corresponds to the row names. The second name returns the variables joined into one array, as table2array joins them, and assigning to it writes them all, as curly brackets over the whole table do. # name: # type: sq_string # elements: 1 # length: 15 Dimension names # name: # type: sq_string # elements: 1 # length: 28 eventtable.EventEndsVariable # name: # type: sq_string # elements: 1 # length: 393 eventtable: property EventEndsVariable Variable holding when each event finished Variable holding when each event finished, given as the name of one of the event table's own variables, or empty when the events are instants. The variable holds the same type as the row times. It cannot be set while EventLengthsVariable is set; an event states its extent as a length or as an end, not as both. # name: # type: sq_string # elements: 1 # length: 41 Variable holding when each event finished # name: # type: sq_string # elements: 1 # length: 30 eventtable.EventLabelsVariable # name: # type: sq_string # elements: 1 # length: 534 eventtable: property EventLabelsVariable Variable naming each event Variable naming each event, given as the name of one of the event table's own variables, or empty when no variable names the events. Assigning a name points the property at that variable; assigning [] clears it. An event table built from a vector of times alone labels its events "Event 1", "Event 2" and so on and points this property at them, while one built from a timetable leaves it empty, because nothing says which of that timetable's variables holds labels. # name: # type: sq_string # elements: 1 # length: 26 Variable naming each event # name: # type: sq_string # elements: 1 # length: 31 eventtable.EventLengthsVariable # name: # type: sq_string # elements: 1 # length: 399 eventtable: property EventLengthsVariable Variable holding how long each event lasted Variable holding how long each event lasted, given as the name of one of the event table's own variables, or empty when the events are instants. The variable holds a duration or a calendarDuration. It cannot be set while EventEndsVariable is set; an event states its extent as a length or as an end, not as both. # name: # type: sq_string # elements: 1 # length: 43 Variable holding how long each event lasted # name: # type: sq_string # elements: 1 # length: 17 eventtable.Events # name: # type: sq_string # elements: 1 # length: 928 timetable: property Events Event table attached to this timetable Event table attached to this timetable, specified as an eventtable, or empty when no events are attached. A plain timetable may be assigned and is converted, though no variable of it is guessed to hold the event labels; assigning [] detaches whatever was there. The event table's row times must be of the same type as this timetable's, so a duration-keyed timetable takes a duration-keyed event table and a datetime-keyed one takes a datetime-keyed event table. Nothing requires the events to lie within the span of the row times, and events entirely outside it are accepted. An operation that preserves the rows carries the event table through unchanged: it is never subsetted, filtered, re-timed or reordered along with them. An eventtable inherits this property but cannot read or assign it, an event table not being something that can itself carry events. # name: # type: sq_string # elements: 1 # length: 38 Event table attached to this timetable # name: # type: sq_string # elements: 1 # length: 19 eventtable.RowTimes # name: # type: sq_string # elements: 1 # length: 518 timetable: property RowTimes Row times Row times, specified as a datetime or duration vector with one element for each row of the timetable. They are stored as a column vector whatever shape they are given in. Unlike the row names of a table they need not be unique, sorted or present: duplicate times, a descending order and missing times are all accepted, and each of them merely makes the timetable irregular. You can access them with tt.Properties.RowTimes and assign them the same way, which recomputes TimeStep. # name: # type: sq_string # elements: 1 # length: 9 Row times # name: # type: sq_string # elements: 1 # length: 21 eventtable.SampleRate # name: # type: sq_string # elements: 1 # length: 394 timetable: property SampleRate Sampling rate in hertz Sampling rate, specified as a positive numeric scalar, being the reciprocal of TimeStep measured in seconds. It is NaN whenever the time step is a calendarDuration, a calendar step having no fixed length in seconds, and whenever the timetable is irregular. Assigning it regenerates the row times from StartTime, as assigning TimeStep does. # name: # type: sq_string # elements: 1 # length: 22 Sampling rate in hertz # name: # type: sq_string # elements: 1 # length: 20 eventtable.StartTime # name: # type: sq_string # elements: 1 # length: 337 timetable: property StartTime Time of the first row Time of the first row, of the same type as RowTimes. It follows the first row: deleting the first row leaves StartTime naming the row that is now first. A timetable left with no rows keeps the value it had. Assigning it shifts every row time by the same amount and preserves TimeStep. # name: # type: sq_string # elements: 1 # length: 21 Time of the first row # name: # type: sq_string # elements: 1 # length: 19 eventtable.TimeStep # name: # type: sq_string # elements: 1 # length: 800 timetable: property TimeStep Spacing between row times Spacing between consecutive row times, specified as a duration or calendarDuration scalar, or a NaN duration when the timetable is irregular. Two rows or more always imply a step of their own and it is read off them afresh, so a subset with a gap in it steps by nothing and a reversed one steps backwards. Fewer than two rows imply nothing, and there it matters how the step was arrived at: one given by 'TimeStep' or 'SampleRate' is remembered, while one read off the row times is not and becomes NaN, its class resetting to duration with it. A freshly built one-row timetable has no step either way. A negative step is as regular as a positive one. Assigning it regenerates the row times from StartTime, even when the timetable was irregular. # name: # type: sq_string # elements: 1 # length: 25 Spacing between row times # name: # type: sq_string # elements: 1 # length: 19 eventtable.UserData # name: # type: sq_string # elements: 1 # length: 245 tabular: property UserData Additional table information Additional table information, specified as an array. Any type of data can be attached using this property. You can access the UserData property of a table tbl with tbl.Properties.UserData. # name: # type: sq_string # elements: 1 # length: 28 Additional table information # name: # type: sq_string # elements: 1 # length: 29 eventtable.VariableContinuity # name: # type: sq_string # elements: 1 # length: 935 tabular: property VariableContinuity Variable continuity Continuity of each variable, specified as a cell array of character vectors or a string array carrying one element per variable, each of them 'unset', 'continuous', 'step' or 'event'. It is empty by default, and assigning {} or [] clears it. If specified as a string array, it is converted and stored internally as a cell array of character vectors. You can access it with tbl.Properties.VariableContinuity and you can index individual variables to read or assign their continuity. A table carries the property but does not act on it, which is also how MATLAB behaves. A timetable uses it to choose the default fill method of each variable when resampling. MATLAB stores this property as a matlab.tabular.Continuity enumeration. Octave has no enumeration classes, so it is stored and returned here as a cell array of character vectors, as VariableNames and VariableUnits are. # name: # type: sq_string # elements: 1 # length: 19 Variable continuity # name: # type: sq_string # elements: 1 # length: 31 eventtable.VariableDescriptions # name: # type: sq_string # elements: 1 # length: 864 tabular: property VariableDescriptions Variable descriptions Variable descriptions, specified as a cell array of character vectors or a string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. If not empty (default), it must contain the same number of elements as the number of variables. If a specific variable does not have a description, this can be specified with an individual empty character vector or an empty string. You can access the VariableDescriptions property of a table tbl with tbl.Properties.VariableDescriptions. You can further index specific variables to access their description. Assigning an empty cell or string array returns the property to its default, and describing a single variable of a table that has no descriptions gives the remaining variables an empty description. # name: # type: sq_string # elements: 1 # length: 21 Variable descriptions # name: # type: sq_string # elements: 1 # length: 24 eventtable.VariableNames # name: # type: sq_string # elements: 1 # length: 550 tabular: property VariableNames Variable names Variable names, specified as a cell array of character vectors or a string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. All elements must be nonempty and distinct, and their number must equal the number of variables. You can access the data type of a specific variable by using dot name assignment, as in tbl.varname, where varname is the name of the variable in table tbl. If the variable name does not exist, a new one is created. # name: # type: sq_string # elements: 1 # length: 14 Variable names # name: # type: sq_string # elements: 1 # length: 24 eventtable.VariableTypes # name: # type: sq_string # elements: 1 # length: 700 tabular: property VariableTypes Variable data types The class of the data of each variable, defined as a cell array of character vectors or a string array with the same number of elements as the number of variables in the table. If specified as a string array, it is converted and stored internally as a cell array of character vectors. You can access the VariableTypes property of a table tbl with tbl.Properties.VariableTypes. You can further index specific variables to access their data type. Modifying the elements of the VariableTypes property automatically converts the underlying data of the corresponding variable into the specified data types provided that a valid conversion is requested. # name: # type: sq_string # elements: 1 # length: 19 Variable data types # name: # type: sq_string # elements: 1 # length: 24 eventtable.VariableUnits # name: # type: sq_string # elements: 1 # length: 803 tabular: property VariableUnits Variable units Variable units, specified as a cell array of character vectors or a string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. If not empty (default), it must contain the same number of elements as the number of variables. If a specific variable does not have a unit, this can be specified with an individual empty character vector or an empty string. You can access the VariableUnits property of a table tbl with tbl.Properties.VariableUnits. You can further index specific variables to access their unit. Assigning an empty cell or string array returns the property to its default, and giving a unit to a single variable of a table that has none gives the remaining variables an empty unit. # name: # type: sq_string # elements: 1 # length: 14 Variable units # name: # type: sq_string # elements: 1 # length: 16 eventtable.empty # name: # type: sq_string # elements: 1 # length: 493 eventtable: et = eventtable.empty () eventtable: et = eventtable.empty (r, v) eventtable: et = eventtable.empty (sz) Create an empty event table. The arguments are those of timetable.empty, which this is in every respect but the class of what comes back: a 0-by-0 event table with no arguments, and otherwise one of r rows and v variables, at least one of the two being zero. None of the three event properties is set, there being no variable for one to name. See also: eventtable, timetable # name: # type: sq_string # elements: 1 # length: 28 Create an empty event table. # name: # type: sq_string # elements: 1 # length: 21 eventtable.eventtable # name: # type: sq_string # elements: 1 # length: 1967 eventtable: et = eventtable () eventtable: et = eventtable (eventTimes) eventtable: et = eventtable (tt) eventtable: et = eventtable (..., Name, Value) Create an event table. et = eventtable () returns an empty event table with no events and no variables. et = eventtable (eventTimes) returns an event table whose events happen at eventTimes, a datetime or duration vector. With no labels given, the events are labelled "Event 1", "Event 2" and so on in a variable named EventLabels, and EventLabelsVariable names it. et = eventtable (tt) converts the timetable tt, whose row times become the event times and whose variables are kept as they are. No variable is guessed to be the labels, so EventLabelsVariable is left empty unless 'EventLabelsVariable' says which one it is. The following Name-Value options are supported: Name Value 'EventLabels' A scalar or a vector with one element per event, naming the events. It may be of any type other than datetime, duration, calendarDuration, table and timetable. A scalar labels every event alike. It is added as a variable named EventLabels. 'EventLengths' A duration or calendarDuration, scalar or one element per event, saying how long each event lasted. Only a duration is accepted when the event times are durations. It is added as a variable named EventLengths. 'EventEnds' A scalar or one element per event, of the same type as the event times, saying when each event finished. It is added as a variable named EventEnds. 'EventLabelsVariable' The name of a variable of tt that names the events. Only with a timetable input. 'EventLengthsVariable' The name of a variable of tt that holds the event lengths. Only with a timetable input. 'EventEndsVariable' The name of a variable of tt that holds the event end times. Only with a timetable input. A length and an end are mutually exclusive, however they are given, and so are 'EventLabels' and 'EventLabelsVariable'. See also: timetable, table.table2timetable # name: # type: sq_string # elements: 1 # length: 22 Create an event table. # name: # type: sq_string # elements: 1 # length: 10 findgroups # name: # type: sq_string # elements: 1 # length: 1261 datatypes: G = findgroups (A) datatypes: G = findgroups (A1, ..., AN) datatypes: [G, ID] = findgroups (A) datatypes: [G, ID1, ..., IDN] = findgroups (A1, ..., AN) Find groups defined by one or more grouping variables. G = findgroups (A) returns G, a column vector of positive integer group numbers with one element per element of the grouping variable A. The groups are the sorted unique values of A: if N groups are found, every integer between 1 and N labels a group. Elements of A that are missing (NaN, NaT, , '', or ) are labelled NaN in G. A can be a numeric, logical, string, cellstr, char, datetime, duration, calendarDuration, or categorical vector. G = findgroups (A1, ..., AN) defines groups as the sorted unique combinations of values across the grouping variables A1, ..., AN, which must all have the same number of elements. [G, ID] = findgroups (A) also returns ID, the sorted unique values of A that identify each group. With multiple grouping variables, [G, ID1, ..., IDN] = findgroups (A1, ..., AN) returns one identifier vector per grouping variable. To group the variables of a table, call findgroups (T), which dispatches to the table method and returns the group identifiers as a table. See also: splitapply, table # name: # type: sq_string # elements: 1 # length: 54 Find groups defined by one or more grouping variables. # name: # type: sq_string # elements: 1 # length: 11 groupcounts # name: # type: sq_string # elements: 1 # length: 1880 datatypes: B = groupcounts (A) datatypes: B = groupcounts (A, groupbins) datatypes: [B, BG, BP] = groupcounts (...) datatypes: [...] = groupcounts (..., Name, Value) Count the number of elements in each group of an array. B = groupcounts (A) groups the rows of A by their values and returns B, a column vector with the number of elements in each group. A is a grouping vector, a matrix whose columns are grouping variables, or a cell array of grouping vectors. Groups are the sorted unique combinations of grouping values; rows holding a missing value in a grouping variable form their own groups, sorted after the non-missing groups. [B, BG, BP] = groupcounts (...) also returns BG, the grouping values that identify each group, and BP, a column vector giving each group's count as a percentage of the total. When A is a single grouping vector, BG holds its representative value for each group; when several grouping variables are given, BG is a cell array with one element per grouping variable. The optional groupbins argument bins the grouping variables before grouping (a vector of bin edges or a positive integer number of bins, applied to a numeric, datetime, or duration grouping variable, or a cell array with one scheme per grouping variable); each binned variable becomes a categorical of bin interval labels. The behaviour can be modified with the 'IncludeMissingGroups' (default true), 'IncludeEmptyGroups' (default false), and 'IncludedEdge' (default 'left', the inclusive bin edge) Name/Value pairs, as for the table method. When 'IncludeEmptyGroups' is true, the unused categories of a categorical or binned grouping variable contribute empty groups. To count the rows in each group of a table, call groupcounts (T, groupvars, ...), which dispatches to the table method and returns the result as a table. See also: groupsummary, findgroups, splitapply, table # name: # type: sq_string # elements: 1 # length: 55 Count the number of elements in each group of an array. # name: # type: sq_string # elements: 1 # length: 11 groupfilter # name: # type: sq_string # elements: 1 # length: 1819 datatypes: B = groupfilter (A, groupvars, method) datatypes: B = groupfilter (A, groupvars, groupbins, method) datatypes: [B, BG] = groupfilter (...) Filter the rows of an array by a per-group condition. B = groupfilter (A, groupvars, method) groups the rows of the array A by the grouping variables groupvars, applies the filter function method to each group, and returns B, the rows of A that satisfy the condition, in their original order. groupvars is a grouping vector with one element per row of A, or a cell array of such vectors. Rows holding a missing value in a grouping variable form their own groups, to which method is applied like any other group. method is a function handle applied to each group's slice of every column of A. It must return either a logical scalar, which keeps or drops the whole group, or a logical vector with one element per row of the group, which keeps or drops the individual rows. A row is kept only when the condition holds for it across all columns of A. [B, BG] = groupfilter (...) also returns BG, the grouping values of the kept rows. When groupvars is a single grouping vector, BG is a column vector with one element per kept row; when several grouping variables are given, BG is a cell array with one element per grouping variable. The optional groupbins argument bins the grouping variables before grouping (a vector of bin edges or a positive integer number of bins, or a cell array with one scheme per grouping variable); see groupsummary for details. The 'IncludedEdge' Name-Value pair ('left' by default, or 'right') selects which bin edge is inclusive. To filter the rows of a table, call groupfilter (T, groupvars, ...), which dispatches to the table method and returns the result as a table. See also: groupsummary, groupcounts, findgroups, splitapply, table # name: # type: sq_string # elements: 1 # length: 53 Filter the rows of an array by a per-group condition. # name: # type: sq_string # elements: 1 # length: 12 groupsummary # name: # type: sq_string # elements: 1 # length: 2393 datatypes: B = groupsummary (A, groupvars, method) datatypes: B = groupsummary (A, groupvars, groupbins, method) datatypes: [B, BG, BC] = groupsummary (...) Compute summary statistics by group for an array. B = groupsummary (A, groupvars, method) groups the rows of the array A by the grouping variables groupvars and applies method to each column of A within each group, returning the results in B, which has one row per group. groupvars is a grouping vector with one element per row of A, or a cell array of such vectors. Groups are the sorted unique combinations of grouping values; rows holding a missing value in a grouping variable form their own groups, sorted after the non-missing groups. method is one of the names 'sum', 'mean', 'median', 'mode', 'var', 'std', 'min', 'max', 'range', 'nnz', 'nummissing', or 'numunique', a function handle, or a cell array of method names andor function handles. NaN values are omitted for every named method except 'nummissing'; a function handle receives the values with NaN included and must return a single row. When several methods are requested the columns of B are ordered by column of A first, then by method. [B, BG, BC] = groupsummary (...) also returns BG, the grouping values that identify each group, and BC, a column vector with the number of rows in each group. When groupvars is a single grouping vector, BG holds its representative value for each group; when several grouping variables are given, BG is a cell array with one element per grouping variable. The optional groupbins argument bins the grouping variables before grouping: a vector of bin edges or a positive integer number of equal-width bins, applied to a numeric, datetime, or duration grouping variable, or a cell array with one scheme per grouping variable. Each binned variable becomes a categorical of bin interval labels. The behaviour can be modified with the 'IncludeMissingGroups' (default true), 'IncludeEmptyGroups' (default false), and 'IncludedEdge' (default 'left', the inclusive bin edge) Name/Value pairs, as for the table method. When 'IncludeEmptyGroups' is true, the unused categories of a categorical or binned grouping variable contribute empty groups. To summarise the variables of a table, call groupsummary (T, groupvars, ...), which dispatches to the table method and returns the result as a table. See also: findgroups, splitapply, table # name: # type: sq_string # elements: 1 # length: 49 Compute summary statistics by group for an array. # name: # type: sq_string # elements: 1 # length: 14 grouptransform # name: # type: sq_string # elements: 1 # length: 2389 datatypes: B = grouptransform (A, groupvars, method) datatypes: B = grouptransform (A, groupvars, groupbins, method) datatypes: [B, BG] = grouptransform (...) datatypes: [...] = grouptransform (..., Name, Value) Transform the columns of an array group by group. B = grouptransform (A, groupvars, method) groups the rows of the array A by the grouping variables groupvars, applies method to each column of A within each group, and returns B, the transformed values, one row per row of A and in the original order. groupvars is a grouping vector with one element per row of A, or a cell array of such vectors. Rows holding a missing value in a grouping variable form their own groups, which are transformed like any other group. method is one of the names 'zscore', 'norm', 'meancenter', 'rescale', 'meanfill', 'linearfill', or a function handle. 'zscore' centers and scales each group to zero mean and unit standard deviation; 'norm' divides by the group 2-norm; 'meancenter' subtracts the group mean; 'rescale' rescales to the range [0, 1]; 'meanfill' replaces missing values with the group mean; and 'linearfill' fills missing values by linear interpolation within the group (leaving leading and trailing missing values unchanged). For the named methods NaN values are omitted when computing the group statistics. A function handle is applied to each group's column slice and must return either a single row (broadcast) or one row per row of the group. [B, BG] = grouptransform (...) also returns BG, the grouping values of each row. When groupvars is a single grouping vector, BG is that vector; when several grouping variables are given, BG is a cell array with one element per grouping variable. The 'ReplaceValues' Name/Value pair (default true) controls whether the transformed values replace the columns of A or, when false, are appended to them. The optional groupbins argument bins the grouping variables before grouping (a vector of bin edges or a positive integer number of bins, or a cell array with one scheme per grouping variable); see groupsummary for details. The 'IncludedEdge' pair ('left' by default, or 'right') selects the inclusive bin edge. To transform the variables of a table, call grouptransform (T, groupvars, ...), which dispatches to the table method and returns the result as a table. See also: groupsummary, groupfilter, findgroups, splitapply, table # name: # type: sq_string # elements: 1 # length: 49 Transform the columns of an array group by group. # name: # type: sq_string # elements: 1 # length: 10 histcounts # name: # type: sq_string # elements: 1 # length: 2276 datatypes: N = histcounts (X) datatypes: N = histcounts (X, nbins) datatypes: N = histcounts (X, edges) datatypes: N = histcounts (..., Name, Value) datatypes: [N, edges] = histcounts (...) datatypes: [N, edges, bin] = histcounts (...) Histogram bin counts. N = histcounts (X) partitions the values of X into bins chosen automatically and returns the number of elements in each bin as a row vector. X is treated as X(:) regardless of its shape, and NaN and infinite values are excluded. N = histcounts (X, nbins) uses nbins bins, and N = histcounts (X, edges) uses the bin edges given in the monotonically non-decreasing vector edges. Bin j covers [edges(j), edges(j+1)), except the last bin, which is closed at both ends. [N, edges, bin] = histcounts (...) also returns the bin edges and, in bin, the index of the bin each element of X belongs to. bin has the same size as X and holds 0 for elements that fall in no bin. Note this differs from discretize, which returns NaN for such elements. The following Name, Value pairs are supported. When more than one of 'NumBins', 'BinWidth', 'BinEdges' and 'BinMethod' is given, the last one specified takes effect. Name Value 'NumBins' A positive integer scalar giving the number of bins, equivalent to the nbins syntax. 'BinEdges' A vector of bin edges, equivalent to the edges syntax. 'BinWidth' A positive finite scalar giving a uniform bin width. The edges are placed at multiples of the width covering the data. 'BinLimits' A two-element vector [lo, hi] restricting the binning to that range. Values outside it are not counted, and the outermost edges are clamped to lo and hi. 'BinMethod' One of 'auto' (default), 'scott', 'fd', 'integers', 'sturges' or 'sqrt'. 'auto' uses the 'integers' rule when the data are integer-valued and span at most 50, and 'scott' otherwise. 'Normalization' One of 'count' (default), 'countdensity', 'cumcount', 'probability', 'percentage', 'pdf' or 'cdf'. The divisor for 'probability', 'percentage', 'pdf' and 'cdf' is numel (X), not the number of elements actually counted, so values excluded by being NaN or out of range still contribute to it. MATLAB accepts 'percentage' but omits it from the list of valid values in its own error message; it is documented here. See also: discretize # name: # type: sq_string # elements: 1 # length: 21 Histogram bin counts. # name: # type: sq_string # elements: 1 # length: 5 hours # name: # type: sq_string # elements: 1 # length: 385 datatypes: D = hours (X) Fixed-time duration in hours. D = hours (X) returns a duration array representing fixed-time duration hours equivalent to the values in X, which must be a numeric array. hours is also available as a method for duration arrays, in which case it performs the opposite conversion. See also: duration, years, days, minutes, seconds, milliseconds, duration.hours # name: # type: sq_string # elements: 1 # length: 29 Fixed-time duration in hours. # name: # type: sq_string # elements: 1 # length: 18 iscalendarduration # name: # type: sq_string # elements: 1 # length: 191 datatypes: TF = iscalendarduration (X) True if input is a calendarDuration array, false otherwise. TF = iscalendarduration (X) always returns a logical scalar, irrespective of the size of X. # name: # type: sq_string # elements: 1 # length: 59 True if input is a calendarDuration array, false otherwise. # name: # type: sq_string # elements: 1 # length: 13 iscategorical # name: # type: sq_string # elements: 1 # length: 176 datatypes: TF = iscategorical (X) True if input is a categorical array, false otherwise. TF = iscategorical (X) always returns a logical scalar, irrespective of the size of X. # name: # type: sq_string # elements: 1 # length: 54 True if input is a categorical array, false otherwise. # name: # type: sq_string # elements: 1 # length: 10 isdatetime # name: # type: sq_string # elements: 1 # length: 167 datatypes: TF = isdatetime (X) True if input is a datetime array, false otherwise. TF = isdatetime (X) always returns a logical scalar, irrespective of the size of X. # name: # type: sq_string # elements: 1 # length: 51 True if input is a datetime array, false otherwise. # name: # type: sq_string # elements: 1 # length: 12 isdictionary # name: # type: sq_string # elements: 1 # length: 319 datatypes: TF = isdictionary (X) True if input is a dictionary object, false otherwise. TF = isdictionary (X) always returns a logical scalar, for a configured or an unconfigured dictionary alike. MATLAB has no such function; it is provided as istable and istimeseries are for their classes. See also: dictionary, isa # name: # type: sq_string # elements: 1 # length: 54 True if input is a dictionary object, false otherwise. # name: # type: sq_string # elements: 1 # length: 10 isduration # name: # type: sq_string # elements: 1 # length: 167 datatypes: TF = isduration (X) True if input is a duration array, false otherwise. TF = isduration (X) always returns a logical scalar, irrespective of the size of X. # name: # type: sq_string # elements: 1 # length: 51 True if input is a duration array, false otherwise. # name: # type: sq_string # elements: 1 # length: 5 isnat # name: # type: sq_string # elements: 1 # length: 339 datatypes: TF = isnat (T) Test for Not-A-Time elements in datetime array. TF = isnat (T) returns a logical array TF of the same size as T containing true for each corresponding element of T that is Not-A-Time (NaT) and false otherwise. NaT is the equivalent of NaN in numeric arrays. If T is not a datetime array, isnat returns an error. # name: # type: sq_string # elements: 1 # length: 47 Test for Not-A-Time elements in datetime array. # name: # type: sq_string # elements: 1 # length: 7 istable # name: # type: sq_string # elements: 1 # length: 152 datatypes: TF = istable (X) True if input is a table, false otherwise. TF = istable (X) always returns a logical scalar, irrespective of the size of X. # name: # type: sq_string # elements: 1 # length: 42 True if input is a table, false otherwise. # name: # type: sq_string # elements: 1 # length: 9 istabular # name: # type: sq_string # elements: 1 # length: 393 datatypes: TF = istabular (X) True if input is a table or a timetable, false otherwise. TF = istabular (X) always returns a logical scalar, irrespective of the size of X. It is true for every class of tabular data, whereas istable and istimetable each single one of them out. A timetable is not a table and neither is a subclass of the other. See also: istable, istimetable, table, timetable # name: # type: sq_string # elements: 1 # length: 57 True if input is a table or a timetable, false otherwise. # name: # type: sq_string # elements: 1 # length: 12 istimeseries # name: # type: sq_string # elements: 1 # length: 352 datatypes: TF = istimeseries (X) True if input is a timeseries object, false otherwise. TF = istimeseries (X) always returns a logical scalar, irrespective of the size of X, so it is true for an array of timeseries objects too. MATLAB has no such function; it is provided as isduration and istimetable are for their classes. See also: timeseries, isa # name: # type: sq_string # elements: 1 # length: 54 True if input is a timeseries object, false otherwise. # name: # type: sq_string # elements: 1 # length: 11 istimetable # name: # type: sq_string # elements: 1 # length: 294 datatypes: TF = istimetable (X) True if input is a timetable, false otherwise. TF = istimetable (X) always returns a logical scalar, irrespective of the size of X. It is false for a table, which is not a timetable and is not a superclass of one. See also: istable, istabular, timetable, table # name: # type: sq_string # elements: 1 # length: 46 True if input is a timetable, false otherwise. # name: # type: sq_string # elements: 1 # length: 14 istscollection # name: # type: sq_string # elements: 1 # length: 362 datatypes: TF = istscollection (X) True if input is a tscollection object, false otherwise. TF = istscollection (X) always returns a logical scalar, irrespective of the size of X, so it is true for an array of tscollection objects too. MATLAB has no such function; it is provided as isduration and istimetable are for their classes. See also: istimeseries, isa # name: # type: sq_string # elements: 1 # length: 56 True if input is a tscollection object, false otherwise. # name: # type: sq_string # elements: 1 # length: 7 keyHash # name: # type: sq_string # elements: 1 # length: 2083 datatypes: key = keyHash (X) datatypes: key = keyHash (X, base) Generate a hash code for an array. key = keyHash (X) generates a uint64 scalar that represents the input X, which may be a numeric, logical, character, cell, struct or function handle array. keyHash utilizes the 64-bit FNV-1a variant of the Fowler-Noll-Vo non-cryptographic hash function. A cell array is keyed on its elements, each hashed in turn, so a cell may hold any type that keyHash accepts, nested to any depth. A struct is keyed on its field names and their values; the order in which its fields were created is not part of the key, so two structs differing only in field order are the same key. A function handle is keyed on its text alone: two named handles for the same function are one key, while distinct anonymous handles never match even when written identically, so nothing a handle captures can be observed through keyMatch. key = keyHash (X, base) also generates a 64-bit hash code using base as the offset basis for the FNV-1a hash algorithm. base must be a uint64 integer type scalar. Use this syntax to cascade keyHash on multiple objects for which a single hash code is required. keyMatch decides key identity and keyHash agrees with it: two values that keyMatch reports as the same key always have the same hash code. The converse does not hold, since distinct keys are permitted to share a hash code. Values that differ only in a representation that keyMatch ignores therefore hash alike, so -0 hashes as 0, and every NaN hashes alike whatever produced it. Elements of a cell array of character vectors are hashed together with their lengths, so that the boundaries between them are part of the key and {'ab', 'c'} does not hash as {'a', 'bc'}. Note that unlike MATLAB, this implementation does not use any random seed. As a result, keyHash will always generate the exact same hash key for any particular input across different workers and Octave sessions. Hash codes are not stable across package versions, however: those produced before version 1.3.1 are invalid and must be recomputed. # name: # type: sq_string # elements: 1 # length: 34 Generate a hash code for an array. # name: # type: sq_string # elements: 1 # length: 8 keyMatch # name: # type: sq_string # elements: 1 # length: 1048 datatypes: TF = keyMatch (A, B) Return true if both inputs are the same key. TF = keyMatch (A, B) returns a logical scalar, which is true, if the inputs A and B are the same key, and false otherwise. Two values are the same key when they have the same class, the same size, and equal contents, with missing values comparing equal to one another as in isequaln. Neither class nor size is promoted, so 1 and int8 (1) are different keys, and so are a row and a column holding the same elements. keyMatch decides key identity and keyHash agrees with it: two values that keyMatch reports as the same key always have the same hash code. The converse does not hold, since distinct keys are permitted to share a hash code, and so comparing hash codes is not a substitute for calling keyMatch. Both inputs must be of a type that can be a key, that is, one that keyHash can hash; anything else raises an error. Answering for a value that cannot be hashed would assert that two values are the same key, or are different keys, when no key exists for either. # name: # type: sq_string # elements: 1 # length: 44 Return true if both inputs are the same key. # name: # type: sq_string # elements: 1 # length: 12 milliseconds # name: # type: sq_string # elements: 1 # length: 420 datatypes: D = milliseconds (X) Fixed-time duration in milliseconds. D = milliseconds (X) returns a duration array representing fixed-time duration milliseconds equivalent to the values in X, which must be a numeric array. milliseconds is also available as a method for duration arrays, in which case it performs the opposite conversion. See also: duration, years, days, hours, minutes, seconds, duration.milliseconds # name: # type: sq_string # elements: 1 # length: 36 Fixed-time duration in milliseconds. # name: # type: sq_string # elements: 1 # length: 7 minutes # name: # type: sq_string # elements: 1 # length: 395 datatypes: D = minutes (X) Fixed-time duration in minutes. D = minutes (X) returns a duration array representing fixed-time duration minutes equivalent to the values in X, which must be a numeric array. minutes is also available as a method for duration arrays, in which case it performs the opposite conversion. See also: duration, years, days, hours, seconds, milliseconds, duration.minutes # name: # type: sq_string # elements: 1 # length: 31 Fixed-time duration in minutes. # name: # type: sq_string # elements: 1 # length: 7 missing # name: # type: sq_string # elements: 1 # length: 107 datatypes: missing Array of missing values. A special class to represent missing data to other data types. # name: # type: sq_string # elements: 1 # length: 24 Array of missing values. # name: # type: sq_string # elements: 1 # length: 10 missing.eq # name: # type: sq_string # elements: 1 # length: 511 missing: TF = eq (A, B) Equality for missing arrays. TF = eq (A, B) is the equivalent of the syntax TF = A == B and returns a logical array of the same size as the largest input with its elements set to false. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. One of the input arguments may also be any type of array. Any comparison with missing arrays always returns false. # name: # type: sq_string # elements: 1 # length: 28 Equality for missing arrays. # name: # type: sq_string # elements: 1 # length: 10 missing.ge # name: # type: sq_string # elements: 1 # length: 527 missing: TF = ge (A, B) Greater than or equal to for missing arrays. TF = ge (A, B) is the equivalent of the syntax TF = A >= B and returns a logical array of the same size as the largest input with its elements set to false. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. One of the input arguments may also be any type of array. Any comparison with missing arrays always returns false. # name: # type: sq_string # elements: 1 # length: 44 Greater than or equal to for missing arrays. # name: # type: sq_string # elements: 1 # length: 10 missing.gt # name: # type: sq_string # elements: 1 # length: 514 missing: TF = gt (A, B) Greater than for missing arrays. TF = gt (A, B) is the equivalent of the syntax TF = A > B and returns a logical array of the same size as the largest input with its elements set to false. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. One of the input arguments may also be any type of array. Any comparison with missing arrays always returns false. # name: # type: sq_string # elements: 1 # length: 32 Greater than for missing arrays. # name: # type: sq_string # elements: 1 # length: 16 missing.iscolumn # name: # type: sq_string # elements: 1 # length: 286 missing: TF = iscolumn (M) Return true if missing array is a column vector. TF = iscolumn (M) returns a logical scalar TF, which is true if the missing array M is a column vector and false otherwise. A column vector is a 2-D array for which size (X) returns [N, 1] with non-negative N. # name: # type: sq_string # elements: 1 # length: 48 Return true if missing array is a column vector. # name: # type: sq_string # elements: 1 # length: 15 missing.isempty # name: # type: sq_string # elements: 1 # length: 178 missing: TF = isempty (M) Return true if missing array is empty. TF = isempty (M) returns a logical scalar TF, which is true if the missing array M is empty and false otherwise. # name: # type: sq_string # elements: 1 # length: 38 Return true if missing array is empty. # name: # type: sq_string # elements: 1 # length: 16 missing.ismatrix # name: # type: sq_string # elements: 1 # length: 308 missing: TF = ismatrix (M) Return true if missing array is a 2-D array. TF = ismatrix (M) returns a logical scalar TF, which is true if the missing array M is a matrix and false otherwise. A matrix is an array of any type where ndims (X) == 2 and for which size (X) returns [H, W] with non-negative H and W. # name: # type: sq_string # elements: 1 # length: 44 Return true if missing array is a 2-D array. # name: # type: sq_string # elements: 1 # length: 17 missing.ismissing # name: # type: sq_string # elements: 1 # length: 694 missing: TF = ismissing (M) missing: TF = ismissing (A, M) Return true for each element in missing array. TF = ismissing (M) returns a logical array TF of the same size as M containing true in every element. TF = ismissing (A, M), with a missing array M as the indicator, marks the standard missing values of A, as ismissing (A) does: NaN in double and single arrays and empty character vectors in cell arrays of character vectors. Integer, logical and char arrays and other cell arrays have no standard missing value, and M is refused for them. Classes with an ismissing method of their own, such as duration, datetime, categorical, string, calendarDuration and table, take M in that method. # name: # type: sq_string # elements: 1 # length: 46 Return true for each element in missing array. # name: # type: sq_string # elements: 1 # length: 13 missing.isrow # name: # type: sq_string # elements: 1 # length: 271 missing: TF = isrow (M) Return true if missing array is a row vector. TF = isrow (M) returns a logical scalar TF, which is true if the missing array M is a row vector and false otherwise. A row vector is a 2-D array for which size (X) returns [1, N] with non-negative N. # name: # type: sq_string # elements: 1 # length: 45 Return true if missing array is a row vector. # name: # type: sq_string # elements: 1 # length: 16 missing.isscalar # name: # type: sq_string # elements: 1 # length: 262 missing: TF = isscalar (M) Return true if missing array is a scalar. TF = isscalar (M) returns a logical scalar TF, which is true if the missing array M is also a scalar and false otherwise. A scalar is a single element object for which size (X) returns [1, 1]. # name: # type: sq_string # elements: 1 # length: 41 Return true if missing array is a scalar. # name: # type: sq_string # elements: 1 # length: 16 missing.isvector # name: # type: sq_string # elements: 1 # length: 319 missing: TF = isvector (M) Return true if missing array is a vector. TF = isvector (M) returns a logical scalar TF, which is true if the missing array M is a vector and false otherwise. A vector is a 2-D array for which one of the dimensions is equal to 1 (either 1xN or Nx1). By definition, a scalar is also a vector. # name: # type: sq_string # elements: 1 # length: 41 Return true if missing array is a vector. # name: # type: sq_string # elements: 1 # length: 15 missing.keyHash # name: # type: sq_string # elements: 1 # length: 979 missing: key = keyHash (M) missing: key = keyHash (M, base) Generate a hash code for a missing array. key = keyHash (M) returns a 64-bit hash code of the missing array M as a uint64 scalar. Every element of a missing array is missing, so the code depends on its size alone: two missing arrays of the same size are the same key, and keyMatch reports them so. A missing array is a different key from a missing value of another type, such as NaN or a missing string. key = keyHash (M, base) also generates a 64-bit hash code using base as the offset basis for the FNV-1a hash algorithm. base must be a uint64 integer type scalar. Use this syntax to cascade keyHash on multiple objects for which a single hash code is required. Note that unlike MATLAB, this implementation does not use any random seed. As a result, keyHash will always generate the exact same hash key for any particular input across different workers and Octave sessions. See also: keyHash, keyMatch, dictionary # name: # type: sq_string # elements: 1 # length: 41 Generate a hash code for a missing array. # name: # type: sq_string # elements: 1 # length: 10 missing.le # name: # type: sq_string # elements: 1 # length: 524 missing: TF = le (A, B) Less than or equal to for missing arrays. TF = le (A, B) is the equivalent of the syntax TF = A <= B and returns a logical array of the same size as the largest input with its elements set to false. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. One of the input arguments may also be any type of array. Any comparison with missing arrays always returns false. # name: # type: sq_string # elements: 1 # length: 41 Less than or equal to for missing arrays. # name: # type: sq_string # elements: 1 # length: 10 missing.lt # name: # type: sq_string # elements: 1 # length: 511 missing: TF = lt (A, B) Less than for missing arrays. TF = lt (A, B) is the equivalent of the syntax TF = A < B and returns a logical array of the same size as the largest input with its elements set to false. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. One of the input arguments may also be any type of array. Any comparison with missing arrays always returns false. # name: # type: sq_string # elements: 1 # length: 29 Less than for missing arrays. # name: # type: sq_string # elements: 1 # length: 15 missing.missing # name: # type: sq_string # elements: 1 # length: 180 missing: M = missing () Create missing values. missing always returns a scalar missing value. Use repmat to expand a scalar missing value to a missing array of desired dimensions. # name: # type: sq_string # elements: 1 # length: 22 Create missing values. # name: # type: sq_string # elements: 1 # length: 13 missing.ndims # name: # type: sq_string # elements: 1 # length: 139 missing: out = ndims (M) Number of dimensions in a missing array. out = ndims (M) returns the number of dimensions of the missing array M. # name: # type: sq_string # elements: 1 # length: 40 Number of dimensions in a missing array. # name: # type: sq_string # elements: 1 # length: 10 missing.ne # name: # type: sq_string # elements: 1 # length: 512 missing: TF = ne (A, B) Not equal for missing arrays. TF = ne (A, B) is the equivalent of the syntax TF = A != B and returns a logical array of the same size as the largest input with its elements set to false. A and B must be size compatible, which translates to they can be the same size, one can be scalar, or for every dimension, their dimension sizes must be equal or one of them must be 1. One of the input arguments may also be any type of array. Any comparison with missing arrays always returns false. # name: # type: sq_string # elements: 1 # length: 29 Not equal for missing arrays. # name: # type: sq_string # elements: 1 # length: 13 missing.numel # name: # type: sq_string # elements: 1 # length: 141 missing: out = numel (M) Total number of elements in a missing array. out = numel (M) returns the number of elements in the missing array M. # name: # type: sq_string # elements: 1 # length: 44 Total number of elements in a missing array. # name: # type: sq_string # elements: 1 # length: 12 missing.size # name: # type: sq_string # elements: 1 # length: 738 missing: sz = size (M) missing: dim_sz = size (M, dim) missing: dim_sz = size (M, d1, d2, ...) missing: [rows, columns, ..., dim_n_sz] = size (...) Return the size of a missing array. sz = size (M) returns a row vector with the size (number of elements) of each dimension for the missing array M. dim_sz = size (M, dim) returns the size of the corresponding dimension specified in dim. If dim is a vector, then dim_sz is a vector of the same length and with each element corresponding to a specified dimension. Multiple dimensions may also be specified as separate arguments. With a single output argument, size returns a row vector. When called with multiple output arguments, size returns the size of dimension N in the Nth argument. # name: # type: sq_string # elements: 1 # length: 35 Return the size of a missing array. # name: # type: sq_string # elements: 1 # length: 10 ods2struct # name: # type: sq_string # elements: 1 # length: 1242 datatypes: s = ods2struct (filename) Read every sheet of an OpenDocument spreadsheet into a scalar structure. s = ods2struct (filename) reads each data sheet of the OpenDocument spreadsheet named by filename into a table and returns a scalar structure with one field per sheet, in sheet order. A sheet written from a timetable is tagged as such and comes back as one, so a workbook of both round-trips through struct2ods. A sheet named as another sheet's event table is consumed: it is attached to the timetable that names it and gets no field of its own, since returning the same event table twice would leave an edit to one copy disagreeing with the other on the rewrite. Both the compressed .ods and the flat .fods formats are read. Each sheet is reconstructed exactly as by ods2table; it is the inverse of struct2ods. A sheet name that is not a valid structure field name is canonicalised with matlab.lang.makeValidName (and made unique if two sheet names collide). Whenever the field name differs from the sheet name, the original sheet name is stored on that field's table as the 'ActualSheetName' custom property, so a subsequent struct2ods restores the exact sheet name. See also: struct2ods, ods2table, table.table2ods, readtable # name: # type: sq_string # elements: 1 # length: 72 Read every sheet of an OpenDocument spreadsheet into a scalar structure. # name: # type: sq_string # elements: 1 # length: 9 ods2table # name: # type: sq_string # elements: 1 # length: 3729 datatypes: tbl = ods2table (filename) datatypes: tbl = ods2table (filename, 'Sheet', sheet) datatypes: [tbl, rowTimesName] = ods2table (...) Read an OpenDocument spreadsheet file into a table. tbl = ods2table (filename) reads the OpenDocument spreadsheet named by filename, which may be a character vector, a cellstr, or a string scalar, and returns it as a table. Both the compressed .ods and the flat .fods formats are read; the format is detected from the file contents, not its extension. The following Name-Value options are supported: Name Value 'Sheet' The sheet to read, selected by its name (a character vector or string scalar) or by a 1-based index over the data sheets. The default is the first data sheet. 'ReadVariableNames' A logical scalar specifying whether the variable names are taken from the file (default true). Setting it false numbers the variables Var1, Var2, and so on. 'ReadRowNames' A logical scalar specifying whether the table takes row names from the file (default true). Setting it false leaves the table without row names. 'VariableNamesRow' A nonnegative integer scalar naming the row of the sheet that holds the variable names (default 1). Zero is equivalent to setting 'ReadVariableNames' to false. It applies only to a sheet with no metadata; a sheet written by table2ods records the names, so there is no row to name. 'RowNamesColumn' A nonnegative integer scalar naming the column of the sheet that holds the row names (default 0). Zero is equivalent to setting 'ReadRowNames' to false. It applies only to a sheet with no metadata, which says nothing about which column holds row names; a sheet written by table2ods records the column. A leading column headed Row, which is what writetable writes for the row names, is taken as the row names without being named here. When the file carries the hidden __datatypes_meta__ sheet written by the table2ods method, the variable types, descriptions and units are restored from it and the variable names from the rows heading the data sheet, and date and time cells are reconstructed as datetime and duration arrays. Integers are restored without loss of precision and missing cells become NaN, NaT, or missing strings as appropriate. A file written before the names moved to the data sheet carries them on the metadata sheet, and is read just as well. A sheet whose cells contradict the types its metadata declares, such as text under a numeric type after another program rewrote the sheet, raises an error. A sheet written from a timetable tags its leading column as row times. A table has no row times, so the column is returned as an ordinary leading variable under the row dimension name, and the second output rowTimesName names it; rowTimesName is empty for every other sheet, which is how ods2struct tells a sheet that held a timetable from one whose first variable merely happens to be a datetime. Read such a sheet with ods2timetable to get the timetable back. When the metadata sheet is absent (a spreadsheet written by another application) the variable types are inferred from the cell value types. Where no names are available the variables are numbered, and the columns can then no longer be grouped, so a multicolumn variable comes back as separate variables and a nested table as flat columns. A datetime or duration variable is restored exactly, along with its Format and, for a zone-aware datetime, its TimeZone. A cell that carries no value at all is a missing entry, which is not the same as a cell holding an empty string. The following round-trip limitation applies, mirroring csv2table: calendarDuration and categorical variables are returned as cell arrays of character vectors and their values are not reconstructed. # name: # type: sq_string # elements: 1 # length: 51 Read an OpenDocument spreadsheet file into a table. # name: # type: sq_string # elements: 1 # length: 13 ods2timetable # name: # type: sq_string # elements: 1 # length: 2356 datatypes: tt = ods2timetable (filename) datatypes: tt = ods2timetable (filename, Name, Value) Read an OpenDocument spreadsheet file into a timetable. tt = ods2timetable (filename) reads a sheet of the OpenDocument spreadsheet named by filename into a timetable. filename may be a character vector, a cellstr, or a string scalar. A sheet written by timetable2ods carries the package's own metadata, which names and types every variable and tags the leading column of row times with their type, their TimeZone where they have one, and their Format. Such a sheet comes back as the timetable it was written from: the row times keep their type, zone and format exactly, and the row dimension keeps its name. Any other sheet is read as ods2table reads it, and its first datetime or duration variable becomes the row times, the row dimension taking that variable's name. A sheet with no such variable cannot be read as a timetable. Every Name-Value option of ods2table is accepted and behaves as it does there, with two exceptions. 'ReadRowNames' and 'RowNamesColumn' are refused: a timetable labels its rows by time and by nothing else, so a sheet whose rows are named is read with ods2table. TimeStep and SampleRate are not stored in the file and are worked out again from the row times, so a regular timetable comes back regular. An event table is held on a sheet of its own, named by a ## Events crossref: line in the hidden metadata sheet, and is attached to the timetable that names it. With no 'Sheet' given the first data sheet is read less the sheets that hold somebody's events, so a file holding one timetable and its events reads with no argument at all. A sheet asked for by name or by index is read whichever it is, the index running over every data sheet so that one number means one sheet here and in ods2table alike; an event sheet asked for by name comes back as the eventtable it is. A reference is refused when it names a sheet the file does not have, when the sheet it names holds no row times, when those row times are of a different type than the referring timetable's, or when the sheet it names carries a reference of its own, an event table not being something that can carry an event table. All four are reachable only in a file edited by hand. See also: timetable.timetable2ods, ods2table, ods2struct, csv2timetable, timetable # name: # type: sq_string # elements: 1 # length: 55 Read an OpenDocument spreadsheet file into a timetable. # name: # type: sq_string # elements: 1 # length: 20 parsePairedArguments # name: # type: sq_string # elements: 1 # length: 1575 datatypes: [optarg_1, ..., optarg_N] = parsePairedArguments (optarg_names, default_values, arg_list) datatypes: [optarg_1, ..., optarg_N, rem_args] = parsePairedArguments (optarg_names, default_values, arg_list) Parse optional paired arguments from variable argument list. parsePairedArguments parses the optional paired arguments specified by optarg_names from the variable input argument list, arg_list. Any optarg_names that are not found in arg_list are returned with their default value specified by default_values, which must be a cell array with the same number of elements as optarg_names. optarg_names must be a cell array of character vectors or a string array with the same number of elements as the number of output arguments specified as [optarg_1, ..., optarg_N], while an extra output argument, rem_args, may be specified for the remaining input arguments in arg_list that were not specified by optarg_names. Each property name specified by optarg_names is case insensitive. A property specified more than once in arg_list takes the value of its last occurrence, as in MATLAB. Every occurrence is consumed, so none of them is returned in rem_args. The following example illustrates how to use parsePairedArguments inside a function to parse optional paired arguments for three properties, namely 'A', 'B', and 'C'. ## Declare optional property Names and their default Values optNames = {'A', 'B', 'C'}; dfValues = {1, 2, 3}; ## Parse optional Name-Value paired arguments [var_A, var_B, var_C, args] = ... parsePairedArguments (optNames, dfValues, args); # name: # type: sq_string # elements: 1 # length: 186 parsePairedArguments (optarg_names, default_values, arg_list) @deftypefnx {datatypes} {[optarg_1, , optarg_N, rem_args] =} @ parsePairedArguments (optarg_names, default_values, arg_list) # name: # type: sq_string # elements: 1 # length: 14 readdictionary # name: # type: sq_string # elements: 1 # length: 3584 datatypes: d = readdictionary (filename) datatypes: d = readdictionary (filename, Name, Value) Read a dictionary from a JSON or an OpenDocument spreadsheet file. d = readdictionary (filename) reads the JSON object in the file filename, a character vector or a string scalar naming a local file or an http or https address, and returns it as a dictionary with string keys in the order of the file. The value type is chosen from the values, all of them together: numbers give double; integers beyond 2^53 give uint64, or int64 when one is negative and all fit, and a number with a fraction among them gives cell; text gives string; dates in ISO 8601 form without a time zone (2024-01-01, 2024-01-01T10:00:00) or as 01-Jan-2024 give datetime, and times as 01:30:00 give duration; a timestamp with a zone mark (Z, +02:00) stays text; true and false give logical; null is missing: NaN, a missing string or NaT among the values above, the missing type when all are null; among logical or 64-bit integer values, which have no missing value, it gives cell; any other mixture, an array, or an object gives cell: an array becomes a column cell array of its elements, each typed alone, and an object a dictionary read by these rules, {} an unconfigured one. d = readdictionary (filename, Name, Value) specifies options with the following Name-Value paired arguments, whose names and text values are matched in any case. Name Value 'FileType' 'auto' (the default) tells the type from the extension, .json, .ods or .fods; 'json' or 'ods' sets it. 'ValueType' 'auto' (the default) as above, or one of 'cell', 'string', 'double', 'single', the integer types, 'logical', 'datetime' and 'duration'. Text is never read as a number: it gives NaN, 0 or false; numbers give text under 'string', and true gives "true" or 1; arrays, objects and null give the missing value of the type. Under 'datetime' text is read as dates, each keeping the zone it states: Z gives UTC, an offset a fixed-offset zone, no mark no zone. Under 'cell' every value is in a cell, objects read the same way. 'DuplicateKeyRule' 'auto' and 'makeUnique' rename a repeated key a as a_1, a_2 and so on, skipping names in use; 'preserveLast' keeps the first position with the last value; 'error' refuses. 'DictionarySelector' A JSON Pointer to the object to read, as '/data/2', counting array elements from 0; '' is the whole file. 'DictionaryNodeName' The name of the object to read: the first key of that name, depth first. It cannot be given with 'DictionarySelector'. 'DateLocale' The locale of month names in dates, as 'de_DE'. 'ParsingMode' 'lenient' (the default) allows comments, trailing commas and NaN, Inf and Infinity; 'strict' refuses them. 'AllowComments', 'AllowTrailingCommas', 'AllowInfAndNaN' Logical scalars allowing each form; each overrides 'ParsingMode'. The top level, or the object selected, must be a JSON object. MATLAB's 'ValueType', 'datetime' reads an ISO 8601 date as NaT; here it reads it as the default does. An OpenDocument spreadsheet, FileType 'ods', gives one entry per sheet, the sheet name the key and the table or timetable read by ods2struct the value, so that a dictionary written by writedictionary comes back as written. A workbook holding both tables and timetables gives cell values. Reading ODS is an extension of this package; it takes no option but 'FileType'. Remote locations other than http and https (Amazon S3, Azure Blob Storage, HDFS), compressed files and 'WebOptions', which MATLAB accepts, are not supported. See also: dictionary, dictionary.writedictionary, ods2struct # name: # type: sq_string # elements: 1 # length: 66 Read a dictionary from a JSON or an OpenDocument spreadsheet file. # name: # type: sq_string # elements: 1 # length: 9 readtable # name: # type: sq_string # elements: 1 # length: 2598 datatypes: tbl = readtable (filename) datatypes: tbl = readtable (filename, Name, Value) Read a file into a table, detecting variable types automatically. tbl = readtable (filename) reads the file named by filename (a character vector or string scalar) and returns it as a table. The file type is inferred from the extension: .txt, .csv, and .dat are read as delimited text; .ods and .fods are read as OpenDocument spreadsheets. Use the 'FileType' option to override the inferred type. By default the first row supplies the variable names and each column's data type is detected automatically (numeric, datetime, duration, or text). The following Name-Value options are supported: Name Value 'FileType' 'text' or 'spreadsheet'. 'ReadVariableNames' Logical; read the first row as variable names (default true). 'ReadRowNames' Logical; read the first column as row names (default false). 'Delimiter' Field delimiter for text files: a single character or one of 'comma', 'space', 'tab', 'semi', 'bar' (default ','). 'NumHeaderLines' Number of lines to skip before the header (default 0). 'TextType' 'char' or 'string' for text columns (default 'char'). 'VariableNamingRule' 'modify' or 'preserve' (default 'modify'). 'Sheet' Spreadsheet only: the sheet to read, selected by name or by a 1-based index over the data sheets (default: the first sheet). 'Range' Spreadsheet only: an A1-style range such as 'C5' or 'C5:D8' limiting the region read. 'VariableNamesRow' A nonnegative integer naming the line or row that holds the variable names (default 1). Zero is equivalent to 'ReadVariableNames', false. 'VariableNamesLine' MATLAB's spelling of 'VariableNamesRow' for a text file, and an exact alias of it. Passing both names raises an error rather than one silently winning. 'RowNamesColumn' A nonnegative integer naming the column that holds the row names (default 1), used when 'ReadRowNames' is true. Zero is equivalent to 'ReadRowNames', false. An OpenDocument file written by table2ods carries the package's own metadata, which names and types every variable exactly and leaves the data sheet without a header row. Such a file is read by ods2table, so it comes back whole rather than being read positionally, which would take its first row of data for the variable names. An explicit 'Range' asks for a block of the sheet instead and is read positionally as usual. Office Open XML spreadsheets (.xlsx, .xlsm) are read via the same interface as ODS. The legacy binary formats .xls and .xlsb are not supported; use .xlsx, .ods, or a text format. See also: table.writetable, csv2table, ods2table # name: # type: sq_string # elements: 1 # length: 65 Read a file into a table, detecting variable types automatically. # name: # type: sq_string # elements: 1 # length: 13 readtimetable # name: # type: sq_string # elements: 1 # length: 1772 datatypes: tt = readtimetable (filename) datatypes: tt = readtimetable (filename, Name, Value) Read a file into a timetable, in the MATLAB-compatible form. tt = readtimetable (filename) reads filename as readtable reads it, taking the file to carry a header row of names and then one row per row, and turns the result into a timetable. Text files (.txt, .csv, .dat) and spreadsheets (.ods, .fods, .xlsx, .xlsm) are supported, and 'FileType' overrides what the extension says. The first datetime or duration variable becomes the row times and the row dimension takes its name, which is what MATLAB does and what makes a file written by writetimetable come back as it went out. 'RowTimes' names a different variable, by name or by index. A file with no such variable cannot be read as a timetable. A column written in the RFC 9557 form, 2024-03-09T22:00:00-05:00[America/New_York], is recognised and comes back as a zone-aware datetime with its zone and its side of any daylight-saving fold intact. That is what writetimetable writes for zoned row times; see deviation D7 in that method's help for why. A column MATLAB wrote is read as MATLAB reads it, so a bare wall clock comes back unzoned, exactly as it does there. Every Name-Value option of readtable is accepted and behaves as it does there, with two exceptions. 'ReadRowNames' and 'RowNamesColumn' are refused: a timetable labels its rows by time and by nothing else, so a file whose rows are named is read with readtable. TimeStep and SampleRate are not stored in the file and are worked out again from the row times. No file MATLAB can read carries an event table, so the result never has one; use ods2timetable for that. See also: timetable.writetimetable, readtable, ods2timetable, csv2timetable, timetable # name: # type: sq_string # elements: 1 # length: 60 Read a file into a timetable, in the MATLAB-compatible form. # name: # type: sq_string # elements: 1 # length: 9 rowfilter # name: # type: sq_string # elements: 1 # length: 693 datatypes: rowfilter Subscript into a table or timetable by a condition on its variables. A utility class that builds a condition out of comparisons written against variable names, and selects the rows satisfying it. The condition is written before the rows are looked at, so the same filter may be used on any table naming the same variables. rf = rowfilter (tbl); tbl(rf.Height > 180 & rf.Age < 40, :) A filter of this package is a rowfilter, a class of its own beside vartype, timerange and withtol, where MATLAB's is a matlab.io.RowFilter. Only the class name differs: nothing here calls itself matlab.*, and no behaviour follows from it. See also: timerange, withtol, table, timetable # name: # type: sq_string # elements: 1 # length: 68 Subscript into a table or timetable by a condition on its variables. # name: # type: sq_string # elements: 1 # length: 20 rowfilter.rowIndices # name: # type: sq_string # elements: 1 # length: 96 rowfilter: ix = rowIndices (rf, tbl) Return the positions of the rows of tbl the filter selects. # name: # type: sq_string # elements: 1 # length: 59 Return the positions of the rows of tbl the filter selects. # name: # type: sq_string # elements: 1 # length: 19 rowfilter.rowfilter # name: # type: sq_string # elements: 1 # length: 682 rowfilter: rf = rowfilter (tbl) rowfilter: rf = rowfilter (varNames) Create a row filter. rf = rowfilter (tbl) creates a filter that may be written against the variables of tbl, a table or a timetable. For a timetable the row times may be filtered on too, under the name of the row dimension. rf = rowfilter (varNames) creates one against the named variables without a table to take them from, so that a filter may be written before the data it will be applied to exists. A filter carries no condition until one is written against it, and a condition is a comparison of a variable with a value, optionally combined with &, | and ~. See also: timerange, withtol, table, timetable # name: # type: sq_string # elements: 1 # length: 20 Create a row filter. # name: # type: sq_string # elements: 1 # length: 7 seconds # name: # type: sq_string # elements: 1 # length: 395 datatypes: D = seconds (X) Fixed-time duration in seconds. D = seconds (X) returns a duration array representing fixed-time duration seconds equivalent to the values in X, which must be a numeric array. seconds is also available as a method for duration arrays, in which case it performs the opposite conversion. See also: duration, years, days, hours, minutes, milliseconds, duration.seconds # name: # type: sq_string # elements: 1 # length: 31 Fixed-time duration in seconds. # name: # type: sq_string # elements: 1 # length: 10 splitapply # name: # type: sq_string # elements: 1 # length: 1036 datatypes: Y = splitapply (func, X, G) datatypes: Y = splitapply (func, X1, ..., XN, G) datatypes: [Y1, ..., YM] = splitapply (...) Split data into groups and apply a function to each group. Y = splitapply (func, X, G) splits the data variable X into groups according to the group numbers G (typically produced by findgroups), applies the function handle func to each group, and concatenates the per-group results into the output Y. G must be a vector of positive integers with one element per element of X; if it identifies N groups, every integer between 1 and N must occur at least once. Elements for which G is NaN are omitted. Y = splitapply (func, X1, ..., XN, G) splits each of the data variables X1, ..., XN and passes the corresponding group of each as a separate input argument to func. [Y1, ..., YM] = splitapply (...) returns the multiple outputs of func, each concatenated across groups. To split the variables of a table, call splitapply (func, T, G), which dispatches to the table method. See also: findgroups, table # name: # type: sq_string # elements: 1 # length: 58 Split data into groups and apply a function to each group. # name: # type: sq_string # elements: 1 # length: 6 string # name: # type: sq_string # elements: 1 # length: 761 datatypes: string Array representing sequences of characters. A string array is an array, where each element stores a sequence of characters of arbitrary length. A string array can also have missing elements, which differ from a sequence of characters of zero length (the equivalent of an empty character vector). To enable existing functions to handle string arrays as if they were cell arrays of character vectors or character arrays, use the convertCharsToStrings function inside your code. To enable functions working with string arrays to accept cell arrays of character vectors or character vectors as if they were string arrays or string scalars, use the convertStringsToChars function. See also: strings, convertCharsToStrings, convertStringsToChars # name: # type: sq_string # elements: 1 # length: 43 Array representing sequences of characters. # name: # type: sq_string # elements: 1 # length: 13 string.append # name: # type: sq_string # elements: 1 # length: 501 string: newstr = append (str1, ..., strN) Combine string arrays. newstr = append (str1, ..., strN) combines the text from each input argument, str1, ..., strN), which must be either string arrays, cell arrays of character vectors, or character vectors or matrices. All input arguments must be of compatible sizes. Character vectors are treated as a single text element and character matrices are treated as a column of elements. append preserves any trailing white spaces, unlike the strcat function. # name: # type: sq_string # elements: 1 # length: 22 Combine string arrays. # name: # type: sq_string # elements: 1 # length: 10 string.cat # name: # type: sq_string # elements: 1 # length: 370 string: C = cat (dim, A, B, ...) Concatenate string arrays. C = cat (dim, A, B, ...) concatenates string arrays A, B, ... along dimension dim. All input arrays must have the same size except along the operating dimension dim. Any of the input arrays may also be character matrixes, cell arrays of character vectors, numeric arrays, or logical arrays of compatible size. # name: # type: sq_string # elements: 1 # length: 26 Concatenate string arrays. # name: # type: sq_string # elements: 1 # length: 11 string.cell # name: # type: sq_string # elements: 1 # length: 467 string: c_arr = cell (str) Convert string array to a cell array. c_arr = cell (str) returns a cell array, c_arr, which has the same size as the input string str. All elements in str that represent real or complex numbers are converted to equivalent double values, whereas all other non-missing elements are converted to character vectors. Zero-length strings are converted to '' empty character vectors, while missing values are returned as [] empty numeric vectors. # name: # type: sq_string # elements: 1 # length: 37 Convert string array to a cell array. # name: # type: sq_string # elements: 1 # length: 14 string.cellstr # name: # type: sq_string # elements: 1 # length: 286 string: cstr = cellstr (str) Convert string array to a cell array of character vectors. cstr = cellstr (str) returns a cell array of character vectors, cstr, which has the same size as the input string str. Both empty strings and missing values are returned as empty character vectors. # name: # type: sq_string # elements: 1 # length: 58 Convert string array to a cell array of character vectors. # name: # type: sq_string # elements: 1 # length: 11 string.char # name: # type: sq_string # elements: 1 # length: 263 string: c_mat = char (str) Convert string array to a character matrix. c_mat = char (str) returns a character matrix, c_mat, which contains as many rows as the elements of the string. Both empty strings and missing values are returned as empty character vectors. # name: # type: sq_string # elements: 1 # length: 43 Convert string array to a character matrix. # name: # type: sq_string # elements: 1 # length: 16 string.circshift # name: # type: sq_string # elements: 1 # length: 714 string: B = circshift (A, n) string: B = circshift (A, n, dim) Circularly shift the elements in a string array. B = circshift (A, n) circularly shifts the elements of the string array A according to n. If n is a nonzero integer scalar, then the elements of A are shifted by n elements along the first non-singleton dimension of A. If n is a vector, it must not be longer that the number of dimensions of A with each value of n corresponding to a dimension in A. The sign of the value(s) in n specify the direction in the elements of A are shifted. B = circshift (A, n, dim) circularly shifts the elements of the string array A along the dimension specified by dim. In this case, n must be a scalar integer value. # name: # type: sq_string # elements: 1 # length: 48 Circularly shift the elements in a string array. # name: # type: sq_string # elements: 1 # length: 14 string.compose # name: # type: sq_string # elements: 1 # length: 2031 string: str = compose (formatSpec, A) string: str = compose (formatSpec, A1, ..., AN) string: str = compose (txt) Format data into a string array or translate escape-character sequences. str = compose (formatSpec, A) formats the data in the array A according to the formatting operators in formatSpec, which must be a string scalar or character vector, and returns the result in the string array str. The formatting operators are the same as those accepted by the sprintf function. Unlike sprintf, which returns a single character vector, compose returns a string array whose elements correspond to the rows of A. compose applies formatSpec to each row of A so that str has the same number of rows as A. The size of str is further determined as follows: If the number of columns in A exceeds the number of formatting operators in formatSpec, then formatSpec is applied repeatedly along each row of A, adding columns to str. If the number of columns in A is less than the number of formatting operators, then the operators left without a corresponding value appear unchanged in str. If A has zero columns, then str has the same size as A and no formatting operators are applied. str = compose (formatSpec, A1, ..., AN) formats the data from the arrays A1, ..., AN. The formatting operators are assigned to the input arrays in order: once an operator has consumed a value from an input array, it becomes unavailable to the following arrays. All input arrays must be of compatible sizes. str = compose (txt) translates escape-character sequences, such as '\n' and '\t', in txt and returns the result in str, which has the same size as txt. Any formatting operators in txt are left unchanged. In all syntaxes, escape-character sequences appearing in literal text are translated and each '%%' literal is converted to a single '%' character, following the same rules as the sprintf function. The only difference from sprintf is that a formatting operator left without a corresponding value is emitted unchanged rather than dropped. # name: # type: sq_string # elements: 1 # length: 72 Format data into a string array or translate escape-character sequences. # name: # type: sq_string # elements: 1 # length: 15 string.contains # name: # type: sq_string # elements: 1 # length: 516 string: TF = contains (str, pattern) string: TF = contains (str, pattern, 'IgnoreCase', true) Test if strings contain pattern. TF = contains (str, pattern) returns a logical array TF of the same size as A containing true for each corresponding element of str that contains the specified pattern and false otherwise. Similarly to NaN values, elements do not match any pattern and always return false. TF = contains (str, pattern, 'IgnoreCase', true) ignores case when determining if str ends with pattern. # name: # type: sq_string # elements: 1 # length: 32 Test if strings contain pattern. # name: # type: sq_string # elements: 1 # length: 12 string.count # name: # type: sq_string # elements: 1 # length: 402 string: out = count (str, pattern) string: out = count (str, pattern, 'IgnoreCase', true) Count occurences of pattern in string array. out = count (str, pattern) returns a numerical array out of the same size as str containing the number of occurences of pattern in each corresponding element of str. out = count (str, pattern, 'IgnoreCase', true) ignores case when identifying occurences of pattern. # name: # type: sq_string # elements: 1 # length: 44 Count occurences of pattern in string array. # name: # type: sq_string # elements: 1 # length: 17 string.ctranspose # name: # type: sq_string # elements: 1 # length: 219 string: B = ctranspose (A) Transpose a string matrix. B = ctranspose (A) is the equivalent of the syntax B = A' and returns the transpose of the string matrix A. For string arrays, ctranspose is identical to transpose. # name: # type: sq_string # elements: 1 # length: 26 Transpose a string matrix. # name: # type: sq_string # elements: 1 # length: 18 string.dispstrings # name: # type: sq_string # elements: 1 # length: 700 string: cstr = dispstrings (str) Get display formatted strings for each element of a string object. cstr = dispstrings (str) returns a cellstr array of character vectors, cstr, which has the same size as the input string object, str. These character vectors will either be the string contents of each corresponding element or for missing values. Composed string elements, i.e. double quoted strings, are translated so that any special characters are represented by their corresponding escaped character sequence, unless the input string, str, is a scalar, in which case text retains its original composition but newlines are prepadded with four white space characters for aligned display. # name: # type: sq_string # elements: 1 # length: 66 Get display formatted strings for each element of a string object. # name: # type: sq_string # elements: 1 # length: 13 string.double # name: # type: sq_string # elements: 1 # length: 288 string: X = double (str) Convert string array to a double array. X = double (str) returns a double array, X, which has the same size as the input string str. All elements in str that represent real or complex numbers are converted to equivalent double values. Otherwise, NaN is returned. # name: # type: sq_string # elements: 1 # length: 39 Convert string array to a double array. # name: # type: sq_string # elements: 1 # length: 12 string.empty # name: # type: sq_string # elements: 1 # length: 550 string: E = string.empty () string: E = string.empty (sz) string: E = string.empty (m, n, ...) Create an empty string array. E = string.empty () returns a 0x0 empty string array. string.empty (m, n, ...) or string.empty (sz) returns an empty string array of the requested size, which must have at least one dimension equal to zero. A lone dimension gives a square size, so string.empty (3) is an error while string.empty (0) is 0x0. As for zeros, a negative dimension counts as zero, and a size vector with nothing in it names no size and gives 0x0. # name: # type: sq_string # elements: 1 # length: 29 Create an empty string array. # name: # type: sq_string # elements: 1 # length: 15 string.endsWith # name: # type: sq_string # elements: 1 # length: 518 string: TF = endsWith (str, pattern) string: TF = endsWith (str, pattern, 'IgnoreCase', true) Test if strings end with pattern. TF = endsWith (str, pattern) returns a logical array TF of the same size as A containing true for each corresponding element of str that ends with the specified pattern and false otherwise. Similarly to NaN values, elements do not match any pattern and always return false. TF = endsWith (str, pattern, 'IgnoreCase', true) ignores case when determining if str ends with pattern. # name: # type: sq_string # elements: 1 # length: 33 Test if strings end with pattern. # name: # type: sq_string # elements: 1 # length: 9 string.eq # name: # type: sq_string # elements: 1 # length: 271 string: TF = eq (A, B) Test for equality. TF is true, if string A is lexicographically equal to B. If one input is a string array, the other input can be a string array, a character vector, or a cell array of character vectors. This is equivalent to the strcmp function. # name: # type: sq_string # elements: 1 # length: 18 Test for equality. # name: # type: sq_string # elements: 1 # length: 12 string.erase # name: # type: sq_string # elements: 1 # length: 502 string: newstr = erase (str, match) Remove content from string array. newstr = erase (str, match) removes the occurrences of match from each element of the string array str. match can be a string array, a character vector, or a cell array of character vectors. When match contains more than one piece of text, every occurrence of every element of match is removed. newstr is a string array of the same size as str; the size of match need not match the size of str. Missing values in str are preserved. # name: # type: sq_string # elements: 1 # length: 33 Remove content from string array. # name: # type: sq_string # elements: 1 # length: 19 string.eraseBetween # name: # type: sq_string # elements: 1 # length: 1652 string: newstr = eraseBetween (str, startPat, endPat) string: newstr = eraseBetween (str, startPos, endPos) string: newstr = eraseBetween (..., "Boundaries", bounds) Erase content between start and end boundaries. newstr = eraseBetween (str, startPat, endPat) removes from each element of the string array str the text that occurs between the substrings startPat and endPat, keeping the boundary substrings themselves. startPat and endPat can be string arrays, character vectors, or cell arrays of character vectors. For each element, the first occurrence of startPat is matched and then the first occurrence of endPat that begins after it; if either boundary is not found, the element is returned unchanged. newstr = eraseBetween (str, startPos, endPos) removes the text between the character positions startPos and endPos, inclusive of the characters at those positions. startPos and endPos must be positive integers with startPos not exceeding endPos and both within the length of the corresponding element of str. newstr = eraseBetween (..., "Boundaries", bounds) specifies whether the boundaries are included in or excluded from the erased text. bounds can be either "inclusive" or "exclusive". When boundaries are given as substrings, the default is "exclusive" and the boundary substrings are preserved; when given as positions, the default is "inclusive" and the characters at those positions are erased. startPat/endPat and startPos/endPos must either be scalars, applied to every element of str, or be of the same size as str and applied element-wise. newstr is a string array of the same size as str. Missing values in str are preserved. # name: # type: sq_string # elements: 1 # length: 47 Erase content between start and end boundaries. # name: # type: sq_string # elements: 1 # length: 14 string.extract # name: # type: sq_string # elements: 1 # length: 1227 string: newstr = extract (str, pat) string: newstr = extract (str, pos) Extract substrings from a string array. newstr = extract (str, pat) returns the substrings of str that match pat. pat can be a string array, a character vector, or a cell array of character vectors; when it contains more than one piece of text, any of them may match. Within each element of str the matches are found left to right and do not overlap; where alternatives match at the same position, the first listed in pat is taken. The matches of an element occupy a row of newstr, so the matches run along the second dimension and every element of str must yield the same number of matches. For a string scalar with n matches, newstr is 1xn; for a non-scalar str, newstr has one row per element (taken in column-major order) and one column per match. Elements with no match, including missing values, are treated as having zero matches. newstr = extract (str, pos) returns the single character located at position pos in each element of str. pos must be a positive integer that is either a scalar, applied to every element, or the same size as str, applied element-wise. In this syntax newstr has the same size as str and missing values are preserved. # name: # type: sq_string # elements: 1 # length: 39 Extract substrings from a string array. # name: # type: sq_string # elements: 1 # length: 19 string.extractAfter # name: # type: sq_string # elements: 1 # length: 894 string: newstr = extractAfter (str, pat) string: newstr = extractAfter (str, pos) Extract the substring after a position or pattern. newstr = extractAfter (str, pat) returns, for each element of str, the part of the text that follows the first occurrence of pat, excluding pat itself. pat can be a string array, a character vector, or a cell array of character vectors, and must either be a scalar, applied to every element of str, or be of the same size as str and applied element-wise. If pat is not found in an element, the corresponding element of newstr is a missing value. newstr = extractAfter (str, pos) returns the part of each element of str that follows the character position pos, that is, from pos+1 to the end. pos must be a positive integer that is either a scalar or the same size as str. newstr is a string array of the same size as str. Missing values in str are preserved. # name: # type: sq_string # elements: 1 # length: 50 Extract the substring after a position or pattern. # name: # type: sq_string # elements: 1 # length: 20 string.extractBefore # name: # type: sq_string # elements: 1 # length: 906 string: newstr = extractBefore (str, pat) string: newstr = extractBefore (str, pos) Extract the substring before a position or pattern. newstr = extractBefore (str, pat) returns, for each element of str, the part of the text that precedes the first occurrence of pat, excluding pat itself. pat can be a string array, a character vector, or a cell array of character vectors, and must either be a scalar, applied to every element of str, or be of the same size as str and applied element-wise. If pat is not found in an element, the corresponding element of newstr is a missing value. newstr = extractBefore (str, pos) returns the part of each element of str that precedes the character position pos, that is, from the start up to pos-1. pos must be a positive integer that is either a scalar or the same size as str. newstr is a string array of the same size as str. Missing values in str are preserved. # name: # type: sq_string # elements: 1 # length: 51 Extract the substring before a position or pattern. # name: # type: sq_string # elements: 1 # length: 21 string.extractBetween # name: # type: sq_string # elements: 1 # length: 1782 string: newstr = extractBetween (str, startPat, endPat) string: newstr = extractBetween (str, startPos, endPos) string: newstr = extractBetween (..., "Boundaries", bounds) Extract the substrings between start and end boundaries. newstr = extractBetween (str, startPat, endPat) returns the text that occurs between the substrings startPat and endPat in each element of str. startPat and endPat can be string arrays, character vectors, or cell arrays of character vectors. Within each element the boundary pairs are matched from left to right and do not overlap, so an element may yield several substrings; these run along the second dimension, and every element of str must yield the same number of matches. For a string scalar with n matches, newstr is 1xn; for a non-scalar str, newstr has one row per element (taken in column-major order) and one column per match. Elements with no match, including missing values, are treated as having zero matches. newstr = extractBetween (str, startPos, endPos) returns the substring between the character positions startPos and endPos, inclusive of the characters at those positions. This syntax extracts a single substring per element, so newstr has the same size as str. newstr = extractBetween (..., "Boundaries", bounds) specifies whether the boundaries are included in or excluded from the extracted text. bounds can be either "inclusive" or "exclusive". When boundaries are given as substrings, the default is "exclusive" and the boundary substrings are not included; when given as positions, the default is "inclusive" and the characters at those positions are included. startPat/endPat and startPos/endPos must either be scalars, applied to every element of str, or be of the same size as str. Missing values in str are preserved. # name: # type: sq_string # elements: 1 # length: 56 Extract the substrings between start and end boundaries. # name: # type: sq_string # elements: 1 # length: 9 string.ge # name: # type: sq_string # elements: 1 # length: 260 string: TF = ge (A, B) Test for greater than or equal to. TF is true, if string A is lexicographically greater than or equal to B. If one input is a string array, the other input can be a string array, a character vector, or a cell array of character vectors. # name: # type: sq_string # elements: 1 # length: 34 Test for greater than or equal to. # name: # type: sq_string # elements: 1 # length: 9 string.gt # name: # type: sq_string # elements: 1 # length: 236 string: TF = gt (A, B) Test for greater than. TF is true, if string A is lexicographically greater than B. If one input is a string array, the other input can be a string array, a character vector, or a cell array of character vectors. # name: # type: sq_string # elements: 1 # length: 22 Test for greater than. # name: # type: sq_string # elements: 1 # length: 14 string.horzcat # name: # type: sq_string # elements: 1 # length: 426 string: C = horzcat (A, B, ...) Horizontal concatenation of string arrays. C = horzcat (A, B, ...) is the equivalent of the syntax C = [A, B, ...] and horizontally concatenates the string arrays A, B, .... All input arrays must have the same size except along the second dimension. Any of the input arrays may also be character matrixes, cell arrays of character vectors, numeric arrays, or logical arrays of compatible size. # name: # type: sq_string # elements: 1 # length: 42 Horizontal concatenation of string arrays. # name: # type: sq_string # elements: 1 # length: 18 string.insertAfter # name: # type: sq_string # elements: 1 # length: 1207 string: newstr = insertAfter (str, pat, newtext) string: newstr = insertAfter (str, pos, newtext) Insert text after a pattern or position. newstr = insertAfter (str, pat, newtext) inserts the text newtext into each element of str after every non-overlapping occurrence of the substring pat. pat can be a string array, a character vector, or a cell array of character vectors, and must either be a scalar, applied to every element of str, or be of the same size as str and applied element-wise. If pat is not found in an element, that element is returned unchanged. newstr = insertAfter (str, pos, newtext) inserts newtext after the character position pos, that is, between the characters at positions pos and pos+1. pos must be a positive integer not exceeding the length of the corresponding element of str, and must be either a scalar or the same size as str. newtext can be a string array, a character vector, or a cell array of character vectors, and must be either a scalar, inserted at every position, or of the same size as str. newstr is a string array of the same size as str. Missing values in str are preserved, and a missing value in newtext makes the corresponding element of newstr missing. # name: # type: sq_string # elements: 1 # length: 40 Insert text after a pattern or position. # name: # type: sq_string # elements: 1 # length: 19 string.insertBefore # name: # type: sq_string # elements: 1 # length: 1214 string: newstr = insertBefore (str, pat, newtext) string: newstr = insertBefore (str, pos, newtext) Insert text before a pattern or position. newstr = insertBefore (str, pat, newtext) inserts the text newtext into each element of str before every non-overlapping occurrence of the substring pat. pat can be a string array, a character vector, or a cell array of character vectors, and must either be a scalar, applied to every element of str, or be of the same size as str and applied element-wise. If pat is not found in an element, that element is returned unchanged. newstr = insertBefore (str, pos, newtext) inserts newtext before the character position pos, that is, between the characters at positions pos-1 and pos. pos must be a positive integer not exceeding the length of the corresponding element of str, and must be either a scalar or the same size as str. newtext can be a string array, a character vector, or a cell array of character vectors, and must be either a scalar, inserted at every position, or of the same size as str. newstr is a string array of the same size as str. Missing values in str are preserved, and a missing value in newtext makes the corresponding element of newstr missing. # name: # type: sq_string # elements: 1 # length: 41 Insert text before a pattern or position. # name: # type: sq_string # elements: 1 # length: 15 string.ipermute # name: # type: sq_string # elements: 1 # length: 459 string: A = ipermute (B, dims) Inverse of the generalized transpose for a string N-D array. A = ipermute (B, dims) returns the inverse of the generalized transpose performed by the permute function. The expression ipermute (permute (A, dims), dims) returns the original array A. dims must index all the dimensions 1:ndims (B) of the input array B, in any order, but only once. The dimension of B specified in dims(N) gets remapped to the Nth dimension of A. # name: # type: sq_string # elements: 1 # length: 60 Inverse of the generalized transpose for a string N-D array. # name: # type: sq_string # elements: 1 # length: 15 string.iscolumn # name: # type: sq_string # elements: 1 # length: 121 string: TF = iscolumn (str) Test if string array is a column vector. TF is true, if string array str is a column vector. # name: # type: sq_string # elements: 1 # length: 40 Test if string array is a column vector. # name: # type: sq_string # elements: 1 # length: 14 string.isempty # name: # type: sq_string # elements: 1 # length: 100 string: TF = isempty (str) Test if string array is empty. TF is true, if string array str is empty. # name: # type: sq_string # elements: 1 # length: 30 Test if string array is empty. # name: # type: sq_string # elements: 1 # length: 15 string.ismatrix # name: # type: sq_string # elements: 1 # length: 107 string: TF = ismatrix (str) Test if string array is a matrix. TF is true, if string array str is a matrix. # name: # type: sq_string # elements: 1 # length: 33 Test if string array is a matrix. # name: # type: sq_string # elements: 1 # length: 15 string.ismember # name: # type: sq_string # elements: 1 # length: 1232 string: TF = ismember (A, B) string: TF = ismember (A, B, 'rows') string: [TF, index] = ismember (...) string: [TF, index] = ismember (..., 'legacy') Find string elements in a set. TF = ismember (A, B) returns a logical array TF of the same size as A containing true for each corresponding element of A that is in B and false otherwise. Similarly to NaN values, elements are not equal with each other and always return false. TF = ismember (A, B, 'rows') only applies to string matrices with the same number of columns, in which case the logical vector TF contains true for each row of A that is also a row in B. TF has the same number of rows as A. [TF, index] = ismember (A, B) also returns an index array of the same size as A containing the lowest index in B for each element of A that is a member of B and 0 otherwise. If the 'rows' optional argument is used, then the returning index is a column vector with the same rows as A and it contains the lowest index in B for each row of A that is a member of B and 0 otherwise. If the 'legacy' optional argument is specified, then the highest index of matched elements is returned. Unless multiple matches exist, the 'legacy' option has no effect on the returned index. # name: # type: sq_string # elements: 1 # length: 30 Find string elements in a set. # name: # type: sq_string # elements: 1 # length: 16 string.ismissing # name: # type: sq_string # elements: 1 # length: 719 string: TF = ismissing (str) string: TF = ismissing (str, indicator) Find missing elements in string array. TF = ismissing (str) returns a logical array, TF, with any true values corresponding to missing elements in the input string array str. TF = ismissing (str, indicator) also returns a logical array, TF, with any true values corresponding to elements in the input string array str, which are lexicographically equal to the values in indicator. indicator must be either a character vector or a string vector or a cell vector of character vectors. A element of a string indicator matches the elements of str, and so does missing. The output array TF has the same size as the input array str. # name: # type: sq_string # elements: 1 # length: 38 Find missing elements in string array. # name: # type: sq_string # elements: 1 # length: 12 string.isrow # name: # type: sq_string # elements: 1 # length: 112 string: TF = isrow (str) Test if string array is a row vector. TF is true, if string array str is a row vector. # name: # type: sq_string # elements: 1 # length: 37 Test if string array is a row vector. # name: # type: sq_string # elements: 1 # length: 15 string.isscalar # name: # type: sq_string # elements: 1 # length: 107 string: TF = isscalar (str) Test if string array is a scalar. TF is true, if string array str is a scalar. # name: # type: sq_string # elements: 1 # length: 33 Test if string array is a scalar. # name: # type: sq_string # elements: 1 # length: 15 string.issorted # name: # type: sq_string # elements: 1 # length: 1647 string: TF = issorted (str) string: TF = issorted (str, dim) string: TF = issorted (str, direction) string: TF = issorted (str, dim, direction) string: TF = issorted (..., 'MissingPlacement', MP) Return true if string array is sorted. TF = issorted (str) returns a logical scalar TF, which is true, if the string array str is sorted in ascending order, and false otherwise. TF = issorted (str, dim) returns a logical scalar TF, which is true, if the string array str is sorted in ascending order along the dimension dim, and false otherwise. TF = issorted (str, direction) returns a logical scalar TF, which is true, if the string array str is sorted in the direction specified by direction, and false otherwise. direction can be any of the following options: 'ascend', which is the default, checks if elements are in ascending order. 'descend' checks if elements are in descending order. 'monotonic' checks if elements are either in ascending or descending order. 'strictascend' checks if elements are in ascending order and there are no duplicate or missing elements. 'strictdescend' checks if elements are in descending order and there are no duplicate or missing elements. 'strictmonotonic' checks if elements are either in ascending or descending order and there are no duplicate or missing elements. TF = issorted (..., 'MissingPlacement', MP) specifies where missing elements () are placed with one of the following options specified in MP: 'auto', which is the default, places missing elements last for ascending sort and first for descending sort. 'first' places missing elements first. 'last' places missing elements last. # name: # type: sq_string # elements: 1 # length: 38 Return true if string array is sorted. # name: # type: sq_string # elements: 1 # length: 19 string.issortedrows # name: # type: sq_string # elements: 1 # length: 2493 string: TF = issortedrows (str) string: TF = issortedrows (str, col) string: TF = issortedrows (str, direction) string: TF = issortedrows (str, col, direction) string: TF = issortedrows (..., 'MissingPlacement', MP) Return true if string matrix rows are sorted. TF = issortedrows (str) returns a logical scalar TF, which is true, if the rows in the 2-D string array str are sorted in ascending order, and false otherwise. TF = issortedrows (str, col) returns a logical scalar TF, which is true, if the string array str is sorted according to the columns specified by the vector col, and false otherwise. col must explicitly contain non-zero integers whose absolute values index existing columns in str. Positive elements sort the corresponding columns in ascending order, while negative elements sort the corresponding columns in descending order. TF = issortedrows (str, direction) checks if the rows in str are sorted according to the specified direction, which can be one of the following options: 'ascend', which is the default, checks if elements are in ascending order. 'descend' checks if elements are in descending order. 'monotonic' checks if elements are either in ascending or descending order. 'strictascend' checks if elements are in ascending order and there are no duplicate or missing elements. 'strictdescend' checks if elements are in descending order and there are no duplicate or missing elements. 'strictmonotonic' checks if elements are either in ascending or descending order and there are no duplicate or missing elements. Alternatively, direction can be a cell array of character vectors specifying the sorting direction for each individual column of str, in which case the number of elements in direction must equal the number of columns in str. TF = issortedrows (str, col, direction) checks if the rows in the string array str are sorted according to the columns specified in col using the corresponding sorting direction specified in direction. In this case, the sign of the values in col is ignored. col and direction must have the same length, but not necessarily the same number of elements as the columns in str. TF = issortedrows (..., 'MissingPlacement', MP) specifies where missing elements () are placed with one of the following options specified in MP: 'auto', which is the default, places missing elements last for ascending sort and first for descending sort. 'first' places missing elements first. 'last' places missing elements last. # name: # type: sq_string # elements: 1 # length: 45 Return true if string matrix rows are sorted. # name: # type: sq_string # elements: 1 # length: 15 string.isstring # name: # type: sq_string # elements: 1 # length: 81 string: TF = isstring (str) Test for string array. TF is true for string inputs. # name: # type: sq_string # elements: 1 # length: 22 Test for string array. # name: # type: sq_string # elements: 1 # length: 15 string.isvector # name: # type: sq_string # elements: 1 # length: 107 string: TF = isvector (str) Test if string array is a vector. TF is true, if string array str is a vector. # name: # type: sq_string # elements: 1 # length: 33 Test if string array is a vector. # name: # type: sq_string # elements: 1 # length: 11 string.join # name: # type: sq_string # elements: 1 # length: 1087 string: newstr = join (str) string: newstr = join (str, delimiter) string: newstr = join (str, dim) string: newstr = join (str, delimiter, dim) Combine string array elements. newstr = join (str) combines the elements of str along the last dimension whose size is not 1, placing a single space between consecutive elements. That dimension is reduced to size 1. newstr = join (str, delimiter) uses delimiter instead of a space. delimiter can be a string array, a character vector, or a cell array of character vectors. A scalar delimiter is placed between every pair of elements. An array delimiter supplies the text placed between consecutive elements and must have one fewer element than str along the joined dimension; its other dimensions must be 1 or match str. newstr = join (str, dim) and newstr = join (str, delimiter, dim) combine the elements along dimension dim. newstr has the size of str with the joined dimension reduced to 1. If any element being combined, or any delimiter placed between them, is a missing value, the corresponding element of newstr is a missing value. # name: # type: sq_string # elements: 1 # length: 30 Combine string array elements. # name: # type: sq_string # elements: 1 # length: 14 string.keyHash # name: # type: sq_string # elements: 1 # length: 1085 string: key = keyHash (str) string: key = keyHash (str, base) Generate a hash code for string array. key = keyHash (str) generates a uint64 scalar that represents the input array str. keyHash utilizes the 64-bit FNV-1a variant of the Fowler-Noll-Vo non-cryptographic hash function. key = keyHash (str, base) also generates a 64-bit hash code using base as the offset basis for the FNV-1a hash algorithm. base must be a uint64 integer type scalar. Use this syntax to cascade keyHash on multiple objects for which a single hash code is required. Elements are hashed together with their lengths, so the boundaries between them are part of the key and a string array does not hash as any other splitting of the same characters. Missing elements are equal to one another and distinct from the empty string. Two arrays that keyMatch reports as the same key always hash alike. Note that unlike MATLAB, this implementation does not use any random seed. As a result, keyHash will always generate the exact same hash key for any particular input across different workers and Octave sessions. # name: # type: sq_string # elements: 1 # length: 38 Generate a hash code for string array. # name: # type: sq_string # elements: 1 # length: 9 string.le # name: # type: sq_string # elements: 1 # length: 254 string: TF = le (A, B) Test for less than or equal to. TF is true, if string A is lexicographically less than or equal to B. If one input is a string array, the other input can be a string array, a character vector, or a cell array of character vectors. # name: # type: sq_string # elements: 1 # length: 31 Test for less than or equal to. # name: # type: sq_string # elements: 1 # length: 13 string.length # name: # type: sq_string # elements: 1 # length: 218 string: N = length (str) Length of a string vector. N = length (str) returns the size of the longest dimension of the string array str, unless any of its dimensions has zero length, in which case length (D) returns 0. # name: # type: sq_string # elements: 1 # length: 26 Length of a string vector. # name: # type: sq_string # elements: 1 # length: 12 string.lower # name: # type: sq_string # elements: 1 # length: 239 string: newstr = lower (str) Convert contents of string array to lower case. newstr = lower (str) converts all upper case characters in every element of the string array str to lower case. newstr is a string array of the same size as str. # name: # type: sq_string # elements: 1 # length: 47 Convert contents of string array to lower case. # name: # type: sq_string # elements: 1 # length: 9 string.lt # name: # type: sq_string # elements: 1 # length: 230 string: TF = lt (A, B) Test for less than. TF is true, if string A is lexicographically less than B. If one input is a string array, the other input can be a string array, a character vector, or a cell array of character vectors. # name: # type: sq_string # elements: 1 # length: 19 Test for less than. # name: # type: sq_string # elements: 1 # length: 14 string.matches # name: # type: sq_string # elements: 1 # length: 511 string: TF = matches (str, pattern) string: TF = matches (str, pattern, 'IgnoreCase', true) Test if strings match pattern. TF = matches (str, pattern) returns a logical array TF of the same size as A containing true for each corresponding element of str that matches the specified pattern and false otherwise. Similarly to NaN values, elements do not match any pattern and always return false. TF = matches (str, pattern, 'IgnoreCase', true) ignores case when determining if str starts with pattern. # name: # type: sq_string # elements: 1 # length: 30 Test if strings match pattern. # name: # type: sq_string # elements: 1 # length: 12 string.ndims # name: # type: sq_string # elements: 1 # length: 140 string: out = ndims (str) Number of dimensions in a string array. out = ndims (str) returns the number of dimensions of the string array D. # name: # type: sq_string # elements: 1 # length: 39 Number of dimensions in a string array. # name: # type: sq_string # elements: 1 # length: 9 string.ne # name: # type: sq_string # elements: 1 # length: 288 string: TF = ne (A, B) Test for inequality. TF is true, if string A is lexicographically not equal to B. If one input is a string array, the other input can be a string array, a character vector, or a cell array of character vectors. TF = ne (A, B) is equivalent to TF = ! strcmp (A, B). # name: # type: sq_string # elements: 1 # length: 20 Test for inequality. # name: # type: sq_string # elements: 1 # length: 12 string.numel # name: # type: sq_string # elements: 1 # length: 69 string: out = numel (str) Total number of elements in a string array. # name: # type: sq_string # elements: 1 # length: 43 Total number of elements in a string array. # name: # type: sq_string # elements: 1 # length: 10 string.pad # name: # type: sq_string # elements: 1 # length: 1079 string: newstr = pad (str) string: newstr = pad (str, nchars) string: newstr = pad (str, side) string: newstr = pad (str, nchars, side) string: newstr = pad (..., padchar) Add leading or trailing characters to string array. newstr = pad (str) pads each element of str with trailing spaces so that every element is as long as the longest one. newstr = pad (str, nchars) pads each element to nchars characters. Elements that already have more than nchars characters are left unchanged; pad never truncates. newstr = pad (str, side) and newstr = pad (str, nchars, side) add the padding on the side given by side, which can be "left", "right", or "both". The default is "right"; for "both", an odd number of pad characters places the extra one on the right. newstr = pad (..., padchar) pads with the single character padchar instead of a space. As padchar is the last argument and a single character, it is told apart from side by its length. Length is measured in characters, not bytes. newstr is a string array of the same size as str, and missing values in str are preserved. # name: # type: sq_string # elements: 1 # length: 51 Add leading or trailing characters to string array. # name: # type: sq_string # elements: 1 # length: 14 string.permute # name: # type: sq_string # elements: 1 # length: 419 string: B = permute (A, dims) Generalized transpose for a string N-D array. B = permute (A, dims) returns the generalized transpose of the string array A by rearranging its dimensions according to the permutation vector specified in dims. dims must index all the dimensions 1:ndims (A) of the input array A, in any order, but only once. The Nth dimension of A gets remapped to the dimension in B specified by dims(N). # name: # type: sq_string # elements: 1 # length: 45 Generalized transpose for a string N-D array. # name: # type: sq_string # elements: 1 # length: 11 string.plus # name: # type: sq_string # elements: 1 # length: 214 string: newstr = plus (str1, str2) Append strings. newstr = lower (str) is the equivalent of the syntax newstr = str1 + str2 and appends str2 to str1. Both input arguments must be string arrays of compatible size. # name: # type: sq_string # elements: 1 # length: 15 Append strings. # name: # type: sq_string # elements: 1 # length: 14 string.repelem # name: # type: sq_string # elements: 1 # length: 912 string: B = repelem (A, n) string: B = repelem (A, d1, ..., dN) Repeat copies of string array elements. B = repelem (A, n) returns a string vector B containing repeated elements of the input A, which must be a string vector. If n is a scalar, each element of A is repeated n times along the non-singleton dimension of A. If n is a vector, it must have the same elements as A, in which case it specifies the number of times to repeat each corresponding element of A. B = repelem (A, d1, ..., dN) returns an array B with each element of A repeated according to the the list of input arguments d1, ..., dN each corresponding to a different dimension 1:ndims (A) of the input array A. d1, ..., dN must be either scalars or vectors with the same length as the corresponding dimension of A containing non-negative integer values specifying the number of repetitions of each element along the corresponding dimension. # name: # type: sq_string # elements: 1 # length: 39 Repeat copies of string array elements. # name: # type: sq_string # elements: 1 # length: 15 string.repelems # name: # type: sq_string # elements: 1 # length: 439 string: B = repelems (A, R) Construct a vector of repeated elements from a string array. B = repelems (A, R) returns a string vector B containing repeated elements of the input A, which must be a string vector. R must be a 2xN matrix of integers. Entries in the first row of R correspond to the linear indexing of the elements in A to be repeated. The corresponding entries in the second row of R specify the repeat count of each element. # name: # type: sq_string # elements: 1 # length: 60 Construct a vector of repeated elements from a string array. # name: # type: sq_string # elements: 1 # length: 14 string.replace # name: # type: sq_string # elements: 1 # length: 1021 string: newstr = replace (str, old, new) Replace substrings in string array. newstr = replace (str, old, new) replaces every occurrence of the substring old with new in each element of the string array str. old and new can be string arrays, character vectors, or cell arrays of character vectors. When old contains several substrings, new must either be a single substring, used to replace all of them, or be of the same size as old, replacing each substring of old with the corresponding element of new. All substrings are replaced in a single left to right pass over each element: at each position the substrings of old are tried in order, the first that matches is replaced, and the scan resumes past the inserted text, so replacements are not themselves re-scanned. old and new are applied to every element of str; their size need not match the size of str. newstr is a string array of the same size as str. A substring of old that does not occur leaves the text unchanged, and missing values in str are preserved. # name: # type: sq_string # elements: 1 # length: 35 Replace substrings in string array. # name: # type: sq_string # elements: 1 # length: 21 string.replaceBetween # name: # type: sq_string # elements: 1 # length: 1959 string: newstr = replaceBetween (str, startPat, endPat, newtext) string: newstr = replaceBetween (str, startPos, endPos, newtext) string: newstr = replaceBetween (..., "Boundaries", bounds) Replace content between start and end boundaries. newstr = replaceBetween (str, startPat, endPat, newtext) replaces, in each element of the string array str, the text that occurs between the substrings startPat and endPat with newtext, keeping the boundary substrings themselves. startPat and endPat can be string arrays, character vectors, or cell arrays of character vectors. For each element, the first occurrence of startPat is matched and then the first occurrence of endPat that begins after it; if either boundary is not found, the element is returned unchanged. newstr = replaceBetween (str, startPos, endPos, newtext) replaces the text between the character positions startPos and endPos, inclusive of the characters at those positions, with newtext. startPos and endPos must be positive integers with startPos not exceeding endPos and both within the length of the corresponding element of str. newstr = replaceBetween (..., "Boundaries", bounds) specifies whether the boundaries are included in or excluded from the replaced text. bounds can be either "inclusive" or "exclusive". When boundaries are given as substrings, the default is "exclusive" and the boundary substrings are preserved; when given as positions, the default is "inclusive" and the characters at those positions are replaced. startPat/endPat and startPos/endPos must either be scalars, applied to every element of str, or be of the same size as str and applied element-wise. newtext can be a string array, a character vector, or a cell array of character vectors, and must be either a scalar or the same size as str. newstr is a string array of the same size as str. Missing values in str are preserved, and a missing value in newtext makes the corresponding element of newstr missing. # name: # type: sq_string # elements: 1 # length: 49 Replace content between start and end boundaries. # name: # type: sq_string # elements: 1 # length: 13 string.repmat # name: # type: sq_string # elements: 1 # length: 550 string: B = repmat (A, n) string: B = repmat (A, d1, ..., dN) string: B = repmat (A, dimvec) Repeat copies of a string array. B = repmat (A, n) returns a string array B containing n copies of the input string array A along every dimension of A. B = repmat (A, d1, ..., dN) returns an array B containing copies of A along the dimensions specified by the list of scalar integer values d1, ..., dN, which specify how many copies of A are made in each dimension. B = repmat (A, dimvec) is equivalent to the previous syntax with dimvec = [d1, ..., dN]. # name: # type: sq_string # elements: 1 # length: 32 Repeat copies of a string array. # name: # type: sq_string # elements: 1 # length: 14 string.reshape # name: # type: sq_string # elements: 1 # length: 674 string: B = reshape (A, d1, ..., dN) string: B = reshape (A, ..., [], ...) string: B = reshape (A, dimvec) Reshape string array. B = reshape (A, d1, ..., dN) returns a string array B with specified dimensions d1, ..., dN, whose elements are taken columnwise from the string array A. The product of d1, ..., dN must equal the total number of elements in A. B = reshape (A, ..., [], ...) returns a string array B with one dimension unspecified which is calculated automatically so that the product of dimensions in B matches the total elements in A, which must be divisible the product of specified dimensions. An empty matrix ([]) is used to flag the unspecified dimension. # name: # type: sq_string # elements: 1 # length: 21 Reshape string array. # name: # type: sq_string # elements: 1 # length: 14 string.reverse # name: # type: sq_string # elements: 1 # length: 233 string: newstr = reverse (str) Reverse order of characters in string array. newstr = reverse (str) reverses the order of the characters in every each element of the string array str. newstr is a string array of the same size as str. # name: # type: sq_string # elements: 1 # length: 44 Reverse order of characters in string array. # name: # type: sq_string # elements: 1 # length: 11 string.size # name: # type: sq_string # elements: 1 # length: 1035 string: sz = size (str) string: dim_sz = size (str, dim) string: dim_sz = size (str, d1, d2, ...) string: [rows, columns, ..., dim_n_sz] = size (...) Return the size of a string array. sz = size (str) returns a row vector with the size (number of elements) of each dimension for the string array str. dim_sz = size (str, dim) returns the size of the corresponding dimension specified in dim. If dim is a vector, then dim_sz is a vector of the same length and with each element corresponding to a specified dimension. Multiple dimensions may also be specified as separate arguments. With a single output argument, size returns a row vector. With several, the size of dimension N is returned in the Nth argument. Asking for fewer output arguments than the array has dimensions folds the trailing dimensions into the last one, so [r, c] = size (str) on a 2-by-2-by-2 array returns 2 and 4; asking for more pads with ones. When a dimension is named, however, the number of output arguments must equal the number of dimensions requested. # name: # type: sq_string # elements: 1 # length: 34 Return the size of a string array. # name: # type: sq_string # elements: 1 # length: 11 string.sort # name: # type: sq_string # elements: 1 # length: 1544 string: B = sort (A) string: B = sort (A, dim) string: B = sort (A, direction) string: B = sort (A, dim, direction) string: B = sort (..., 'MissingPlacement', MP) string: [B, index] = sort (A, ...) Sort elements in a string array. B = sort (A) sorts the elements of the string array A in ascending order. Elements are compared lexicographically by their Unicode code points, with the empty string "" sorting before any non-empty string. If A is a matrix, sort (A) sorts each column of A in ascending order. For multidimensional arrays, sort (A) sorts along the first non-singleton dimension. B = sort (A, dim) sorts along the dimension specified by dim. B = sort (A, direction) also specifies the sorting direction, which can be either 'ascend' (default) or 'descend'. B = sort (..., 'MissingPlacement', MP) specifies where to place the missing elements () returned in B with any of the following options specified in MP: 'auto', which is the default, places missing elements last for ascending sort and first for descending sort. 'first' places missing elements first. 'last' places missing elements last. [B, index] = sort (A, ...) also returns a sorting index containing the original indices of the elements in the sorted array. If A is a vector, then index contains the original linear indices of the elements in the sorted vector B such that B = A(index). If A is an MxN matrix and dim = 1, then index contains the original row indices of the elements in the sorted vector B such that for j = 1:N, B(:,j) = A(index(:,j),j). # name: # type: sq_string # elements: 1 # length: 32 Sort elements in a string array. # name: # type: sq_string # elements: 1 # length: 15 string.sortrows # name: # type: sq_string # elements: 1 # length: 1935 string: B = sortrows (A) string: B = sortrows (A, col) string: B = sortrows (A, direction) string: B = sortrows (A, col, direction) string: B = sortrows (..., 'MissingPlacement', MP) string: [B, index] = sortrows (A, ...) Sort rows in a string array. B = sortrows (A) sorts the rows of the 2-D string array A in ascending order. The sorted array B has the same size as A. B = sortrows (A, col) sorts A according to the columns specified by the numeric vector col, which must explicitly contain non-zero integers whose absolute values index existing columns in A. Positive elements sort the corresponding columns in ascending order, while negative elements sort the corresponding columns in descending order. B = sortrows (A, direction) also specifies the sorting direction, which can be either 'ascend' (default) or 'descend' applying to all columns in A. Alternatively, direction can be either a string array or a cell array of character vectors specifying the sorting direction for each individual column of A, in which case the number of elements in direction must equal the number of columns in A. B = sortrows (A, col, direction) sorts the string array A according to the columns specified in col using the corresponding sorting direction specified in direction. In this case, the sign of the values in col is ignored. col and direction must have the same number of elements, but not necessarily equal to the columns of A. B = sortrows (..., 'MissingPlacement', MP) specifies where to place the missing elements () returned in B with any of the following options specified in MP: 'auto', which is the default, places missing elements last for ascending sort and first for descending sort. 'first' places missing elements first. 'last' places missing elements last. [B, index] = sortrows (A, ...) also returns an index vector containing the original row indices of A in the sorted matrix B such that B = A(index,:). # name: # type: sq_string # elements: 1 # length: 28 Sort rows in a string array. # name: # type: sq_string # elements: 1 # length: 12 string.split # name: # type: sq_string # elements: 1 # length: 1520 string: newstr = split (str) string: newstr = split (str, delimiter) string: newstr = split (str, delimiter, dim) string: [newstr, match] = split (...) Split string array at delimiters. newstr = split (str) divides each element of str at whitespace characters and returns the pieces as a string array. The whitespace characters are the space, tab, newline, carriage return, form feed, and vertical tab. newstr = split (str, delimiter) divides each element at the substrings specified by delimiter, which can be a string array, a character vector, or a cell array of character vectors. When delimiter contains several substrings they are all used; at a given position the substrings are tried in order and the first that matches is taken. Delimiters are not collapsed, so consecutive delimiters yield empty strings in newstr. The pieces of each element are laid out along a new dimension. For a string scalar that splits into n pieces, newstr is nx1; for an Mx1 column it is Mxn; for a 1xM row it is 1xMxn; and in general the pieces extend the first trailing singleton dimension. Every element of str must split into the same number of pieces. Missing values in str are preserved and count as a single piece. newstr = split (str, delimiter, dim) lays the pieces out along dimension dim, which must be a dimension along which str has size 1. [newstr, match] = split (...) also returns the delimiters matched between the pieces. match has the same layout as newstr but with one fewer element along the split dimension. # name: # type: sq_string # elements: 1 # length: 33 Split string array at delimiters. # name: # type: sq_string # elements: 1 # length: 17 string.splitlines # name: # type: sq_string # elements: 1 # length: 768 string: newstr = splitlines (str) Split string array at newline characters. newstr = splitlines (str) divides each element of str at its newline characters and returns the lines as a string array. The recognized newline characters are the line feed, the carriage return, a carriage return followed by a line feed (treated as a single boundary), the vertical tab, the form feed, and the Unicode next-line (U+0085), line-separator (U+2028), and paragraph-separator (U+2029) characters. The lines are laid out along a new dimension exactly as for split: a string scalar with n lines yields an nx1 array, and an Mx1 column yields an Mxn array. Every element of str must contain the same number of newlines. Missing values in str are preserved and count as a single line. # name: # type: sq_string # elements: 1 # length: 41 Split string array at newline characters. # name: # type: sq_string # elements: 1 # length: 17 string.startsWith # name: # type: sq_string # elements: 1 # length: 532 string: TF = startsWith (str, pattern) string: TF = startsWith (str, pattern, 'IgnoreCase', true) Test if strings start with pattern. TF = startsWith (str, pattern) returns a logical array TF of the same size as A containing true for each corresponding element of str that starts with the specified pattern and false otherwise. Similarly to NaN values, elements do not match any pattern and always return false. TF = startsWith (str, pattern, 'IgnoreCase', true) ignores case when determining if str starts with pattern. # name: # type: sq_string # elements: 1 # length: 35 Test if strings start with pattern. # name: # type: sq_string # elements: 1 # length: 13 string.strcat # name: # type: sq_string # elements: 1 # length: 385 string: newstr = strcat (str1, str2, ...) Horizontal concatenation of texts in string array. newstr = strcat (str1, str2, ...) merges horizontally all the input arguments into a string array, as long as any of the input arguments is a string array. All inputs must be of common size or scalars. All inputs must be character vectors, cell arrays of character vectors, or string arrays. # name: # type: sq_string # elements: 1 # length: 50 Horizontal concatenation of texts in string array. # name: # type: sq_string # elements: 1 # length: 13 string.strcmp # name: # type: sq_string # elements: 1 # length: 577 string: TF = strcmp (A, B) Compare strings. TF is true, if string A is lexicographically equal to B. If one input is a string array, the other input can be a string array, a character vector, or a cell array of character vectors. If either A or B is a string array or a cell array of character vectors, then a logical array TF of the same size is returned, containing the values described above for every member of the array. In this case, the other argument may also be a string array or a cell array of character vectors (of the same size or scalar), or a character vector. # name: # type: sq_string # elements: 1 # length: 16 Compare strings. # name: # type: sq_string # elements: 1 # length: 14 string.strcmpi # name: # type: sq_string # elements: 1 # length: 641 string: TF = strcmpi (A, B) Compare strings (case insensitive). TF is true, if string A is lexicographically equal to B, disregarding case of alphabetic characters. If one input is a string array, the other input can be a string array, a character vector, or a cell array of character vectors. If either A or B is a string array or a cell array of character vectors, then a logical array TF of the same size is returned, containing the values described above for every member of the array. In this case, the other argument may also be a string array or a cell array of character vectors (of the same size or scalar), or a character vector. # name: # type: sq_string # elements: 1 # length: 35 Compare strings (case insensitive). # name: # type: sq_string # elements: 1 # length: 13 string.string # name: # type: sq_string # elements: 1 # length: 1984 string: str = string () string: str = string (in) string: str = string (calendarDuration, 'Format', FMT) string: str = string (duration, 'Format', FMT) string: str = string ({in1, ..., inN}) Create a new string array. str = string () creates a scalar string array, whose element contains an empty character vector. str = string (in) creates a string array of the same size as in, which is converted to string according to the following options: character arrays are converted so that each row becomes a string element, with any trailing whitespace preserved; cell arrays of character vectors are stored as-is. numeric arrays are converted with one precision for the whole array: P significant digits, where P is four more than the base-10 logarithm of its largest finite magnitude, rounded up, and lies between 5 and 16. Integer types are written exactly, and NaN becomes a missing string. logical arrays are converted to either false or true character sequences. categorical arrays are converted via their cellstr method. datetime arrays are converted via their dispstrings method. calendarDuration arrays and duration arrays are converted via their respective cellstr methods, in which case an extra pair argument is supported to allow parsing to the respective method the appropriate display format. See calendarDuration and duration for valid formats parsed through FMT to each class method. Extra input arguments to the string constructor except for this case are ignored. missing arrays are converted to a string array of missing elements. str = string ({in1, ..., inN}) creates a string array from a cell array, which may contain any combination of the aforementioned data types, provided that each cell element is compatible to a string scalar. When using this syntax, calendarDuration arrays and duration arrays are converted via their dispstrings method, hence no extra format argument is meaningful. See also: calendarDuration, categorical, datetime, duration, missing # name: # type: sq_string # elements: 1 # length: 26 Create a new string array. # name: # type: sq_string # elements: 1 # length: 12 string.strip # name: # type: sq_string # elements: 1 # length: 943 string: newstr = strip (str) string: newstr = strip (str, side) string: newstr = strip (str, stripchar) string: newstr = strip (str, side, stripchar) Remove leading and trailing characters from string array. newstr = strip (str) removes all consecutive whitespace characters from the beginning and end of each element of str. The whitespace characters are the space, tab, newline, carriage return, form feed, and vertical tab. newstr = strip (str, side) removes whitespace from the side given by side, which can be "left", "right", or "both" (the default). newstr = strip (str, stripchar) removes the single character stripchar instead of whitespace. Because the side keywords are longer than one character, a single-character second argument is always treated as stripchar. newstr = strip (str, side, stripchar) removes stripchar from the given side. newstr is a string array of the same size as str. Missing values in str are preserved. # name: # type: sq_string # elements: 1 # length: 57 Remove leading and trailing characters from string array. # name: # type: sq_string # elements: 1 # length: 16 string.strlength # name: # type: sq_string # elements: 1 # length: 62 string: out = strlength (str) Length of text in string arrays. # name: # type: sq_string # elements: 1 # length: 32 Length of text in string arrays. # name: # type: sq_string # elements: 1 # length: 14 string.strncmp # name: # type: sq_string # elements: 1 # length: 632 string: TF = strncmp (A, B, n) Compare first n characters of strings. TF is true, if the first n characters of strings A and B are lexicographically equal. If one input is a string array, the other input can be a string array, a character vector, or a cell array of character vectors. If either A or B is a string array or a cell array of character vectors, then a logical array TF of the same size is returned, containing the values described above for every member of the array. In this case, the other argument may also be a string array or a cell array of character vectors (of the same size or scalar), or a character vector. # name: # type: sq_string # elements: 1 # length: 38 Compare first n characters of strings. # name: # type: sq_string # elements: 1 # length: 15 string.strncmpi # name: # type: sq_string # elements: 1 # length: 696 string: TF = strncmpi (A, B, n) Compare first n characters of strings (case insensitive). TF is true, if the first n characters of strings A and B are lexicographically equal, disregarding case of alphabetic characters. If one input is a string array, the other input can be a string array, a character vector, or a cell array of character vectors. If either A or B is a string array or a cell array of character vectors, then a logical array TF of the same size is returned, containing the values described above for every member of the array. In this case, the other argument may also be a string array or a cell array of character vectors (of the same size or scalar), or a character vector. # name: # type: sq_string # elements: 1 # length: 57 Compare first n characters of strings (case insensitive). # name: # type: sq_string # elements: 1 # length: 16 string.transpose # name: # type: sq_string # elements: 1 # length: 161 string: B = transpose (A) Transpose a string matrix. B = transpose (A) is the equivalent of the syntax B = A.' and returns the transpose of the string matrix A. # name: # type: sq_string # elements: 1 # length: 26 Transpose a string matrix. # name: # type: sq_string # elements: 1 # length: 13 string.unique # name: # type: sq_string # elements: 1 # length: 1326 string: B = unique (A) string: B = unique (A, 'rows') string: [B, ixA, ixB] = unique (...) string: ... = unique (..., order) string: ... = unique (..., occurrence) Unique values in a string array. B = unique (A) returns the unique values of the string array A in the string vector B sorted lexicographically. If If A is a column vector, then B is also a column vector, otherwise unique returns a row vector. B = unique (A, 'rows') returns the unique rows of the string matrix A in the string matrix B sorted in lexicographical order. [B, ixA, ixB] = unique (...) also returns index vectors ixA and ixB such that B = A(ixA) and A = B(ixB), unless the 'rows' optional argument is given, in which case B = A(ixA,:) and A = B(ixB,:). ... = unique (..., order) also specifies the order of the returned unique values. order may be either 'sorted', which is the default behavior, or 'stable', in which case the unique values are returned in order of appearance. ... = unique (..., occurrence) also specifies the which index is returned in ixA, where there are repeated values or rows (if opted) in the input categorical array. occurrence may be either 'first', which is the default and returns the index of the first occurrence of each unique value, or 'last', in which case the last occurrence of each unique value is returned. # name: # type: sq_string # elements: 1 # length: 32 Unique values in a string array. # name: # type: sq_string # elements: 1 # length: 12 string.upper # name: # type: sq_string # elements: 1 # length: 239 string: newstr = upper (str) Convert contents of string array to upper case. newstr = upper (str) converts all lower case characters in every element of the string array str to upper case. newstr is a string array of the same size as str. # name: # type: sq_string # elements: 1 # length: 47 Convert contents of string array to upper case. # name: # type: sq_string # elements: 1 # length: 14 string.vertcat # name: # type: sq_string # elements: 1 # length: 421 string: C = vertcat (A, B, ...) Vertical concatenation of string arrays. C = vertcat (A, B, ...) is the equivalent of the syntax C = [A; B; ...] and vertically concatenates the string arrays A, B, .... All input arrays must have the same size except along the first dimension. Any of the input arrays may also be character matrixes, cell arrays of character vectors, numeric arrays, or logical arrays of compatible size. # name: # type: sq_string # elements: 1 # length: 40 Vertical concatenation of string arrays. # name: # type: sq_string # elements: 1 # length: 7 strings # name: # type: sq_string # elements: 1 # length: 868 datatypes: str = strings datatypes: str = strings (n) datatypes: str = strings (sz1, ..., szN) datatypes: str = strings (sz) Create a string array of strings with no characters. str = strings returns a string scalar with no characters, "". str = strings (n) returns an NxN string array whose every element has no characters. str = strings (sz1, ..., szN) returns a string array of size sz1-by-...-by-szN, and str = strings (sz) takes the same sizes as the row vector sz. A size may be numeric or logical, and must be a real, finite integer. A negative size counts as zero, as does an empty size argument, and an empty size vector gives a 0x0 array. The elements are strings with no characters, not missing strings, so ismissing is false and strlength is zero for every one of them. string (cell (sz)) gives missing strings instead. See also: string, string.empty # name: # type: sq_string # elements: 1 # length: 52 Create a string array of strings with no characters. # name: # type: sq_string # elements: 1 # length: 10 struct2ods # name: # type: sq_string # elements: 1 # length: 1450 datatypes: struct2ods (filename, s) Write a scalar structure of tables to a multi-sheet OpenDocument spreadsheet. struct2ods (filename, s) writes each field of the scalar structure s to its own sheet in the OpenDocument spreadsheet named by filename. Every field of s must hold a table or a timetable; the field name becomes the sheet name. A timetable's row times lead its sheet and are tagged as row times, so ods2struct returns a timetable for that sheet and a table for the others. A timetable carrying an event table gets a second sheet holding it, named _Events, and a ## Events crossref: line above the first sheet marker of the hidden sheet tying the two together and carrying the event table's three variable designations. Both the compressed .ods and the flat .fods formats are supported, selected by the file extension. Full type fidelity is preserved through a hidden, sectioned __datatypes_meta__ sheet, exactly as for the single-table table2ods method; the workbook round-trips through ods2struct. A field whose table carries an 'ActualSheetName' custom property (see addprop) uses that value as the sheet name instead of the field name, which lets a sheet name that is not a valid identifier (for example 'Sales 2024') round-trip. Sheet names must be non-empty and must not contain any of the characters [ ] * ? : \ /, and the resolved names must be unique. See also: ods2struct, table.table2ods, ods2table, table.writetable # name: # type: sq_string # elements: 1 # length: 77 Write a scalar structure of tables to a multi-sheet OpenDocument spreadsheet. # name: # type: sq_string # elements: 1 # length: 12 struct2table # name: # type: sq_string # elements: 1 # length: 1281 datatypes: tbl = struct2table (S) datatypes: tbl = struct2table (S, Name, Value) Convert a structure array to a table. tbl = struct2table (S) converts a structure array S to the table tbl, where each field of the input structure becomes a variable in the output table. For a scalar structure with N fields, all of which have M rows, or an Mx1 or 1xM structure array with N fields, the output is an MxN table. A field of function handles, dictionary, timeseries or tscollection objects becomes a cell variable, since no other variable can hold one. tbl = struct2table (S, Name, Value) specifies optional parameters for creating the table tbl with the following Name-Value paired arguments. Name Value 'AsArray' A logical scalar specifying whether to treat a scalar input as a structure array, which allows the fields containing data of different sizes. 'RowNames' A cell array of character vectors or a string array defining the row names of tbl. The names must be unique but not necessarily valid variable names. 'DimensionNames' A cell array of character vectors or a string array defining the dimension names of tbl. The names must be unique and not in conflict with variable names. By default, dimension names are 'Row', 'Variables'. See also: array2table, cell2table, table # name: # type: sq_string # elements: 1 # length: 37 Convert a structure array to a table. # name: # type: sq_string # elements: 1 # length: 11 struct2xlsx # name: # type: sq_string # elements: 1 # length: 884 datatypes: struct2xlsx (filename, s) Write a scalar structure of tables to a multi-sheet Excel workbook. struct2xlsx (filename, s) writes each field of the scalar structure s to its own sheet in the Office Open XML workbook named by filename (.xlsx or .xlsm). Every field must hold a table; the field name becomes the sheet name. This is the Excel counterpart of struct2ods. Like writetable, it writes the MATLAB-interoperable format (a variable-name header row followed by the data, with no hidden type metadata); read it back with xlsx2struct. A field whose table carries an 'ActualSheetName' custom property uses that value as the sheet name instead of the field name. Sheet names must be non-empty, at most 31 characters, and must not contain any of the characters [ ] * ? : \ /; the resolved names must be unique. See also: xlsx2struct, struct2ods, table.writetable, readtable # name: # type: sq_string # elements: 1 # length: 67 Write a scalar structure of tables to a multi-sheet Excel workbook. # name: # type: sq_string # elements: 1 # length: 5 table # name: # type: sq_string # elements: 1 # length: 3901 datatypes: table Array of tabular data containing multiple columnar variables. A table is a 2-dimensional data structure that collects heterogeneous data and metadata into a single container. Tables are suitable for storing columnar data much like spreadsheets but they can also be used for storing more complex data including multicolumnar variables and nested tables. Tables can be subscripted using parentheses like ordinary numeric arrays, but in addition to indexing with numeric and logical vectors, you can also use the table's variable or row names much like indexing a structure field as well as using a vartype class object to make a selection of variable types. While these methods will return a subset of the original table, you can also use curly brackets much like cell arrays to retrieve the contents of the table. In this case, the original data types of the selected variables are returned. An empty numeric variable subscript selects no variables, so tbl(:,[]) is a table of the same height with nothing in it. An empty cell is not a subscript and is refused, as it is on any other array. Assigning an empty matrix to a subscripted table deletes rows or variables. tbl(rows,:) = [] removes the referenced rows, tbl(:,vars) = [] removes the referenced variables, and tbl.varname = [] removes a single variable by name. One of the two subscripts must be a colon, and when both of them are, the rows are removed. A table that has lost its last variable keeps its height, so a five-row table displays as 5x0 rather than as empty. Assigning to a subscripted table with parentheses takes another table or a cell array with one element per row and variable assigned. A variable name the table does not carry, or the index after its last variable, creates a new variable, named VarN when it is given by index; an index that would leave a gap is refused. Rows and variables the assignment adds but does not fill take the default value of their type, with a warning. A table with neither rows nor variables takes the height of what is assigned to it. Assigning with curly brackets writes the contents of the variables, as reading with them returns them: an array whose columns are those of the variables assigned, side by side, and whose rows are the rows assigned, or a scalar that fills them all. A single new variable also takes one row as every one of its rows. Each variable converts what it is given as its own type does, so a cell variable takes a cell array and a double variable reads text as numbers. Variables and rows are created as with parentheses, a table with neither rows nor variables takes one variable for each column, and assigning an empty matrix is refused, since curly brackets never delete. Assigning to tbl.Variables, the second dimension name, writes every variable as curly brackets over the whole table do, but takes only a value with one row for each row of the table. Besides the table constructor, you can also use array2table, cell2table, and struct2table to create tables from the respective data types. Besides all numeric data types, other supported data types that can be stored in a table array are logical, categorical, cell, (including cellstr), calendarDuration, duration, datetime, string, and struct arrays, as well as table itself. A variable cannot be a function handle, a dictionary, a timeseries or a tscollection; only a cell variable can hold one. Every way of creating or replacing a variable refuses them. For a function handle, the conversions cell2table and struct2table, as well as assignment of a cell array with parentheses, give a cell variable instead. MATLAB refuses a function handle or a dictionary only in the constructors and addvars, and lets tbl.varname assignment, struct2table and convertvars create one; it accepts a timeseries or a tscollection everywhere. See also: vartype, array2table, cell2table, struct2table # name: # type: sq_string # elements: 1 # length: 61 Array of tabular data containing multiple columnar variables. # name: # type: sq_string # elements: 1 # length: 22 table.CustomProperties # name: # type: sq_string # elements: 1 # length: 1136 tabular: property CustomProperties Customized metadata of table and its variables Custom properties that contain metadata of a table and its variables. By default, this is an empty container. Each custom property holds either table metadata or per-variable metadata, according to the property type ('table' or 'variable') specified when the property is created with the addprop method. A variable-scoped property holds one element per variable. You can add an individual custom property only by using the addprop method and you can only remove a custom property with the rmprop method. To access existing custom properties use dot name structure assignment as in tbl.Properties.CustomProperties.PropertyName, where PropertyName is the name used with the addprop method. The whole set may also be taken from another table by assigning that table's CustomProperties to this one, which replaces every custom property with those of the other table, their types included. A variable-scoped property arriving that way must hold one element for each variable of the receiving table, or be a 0-by-0 empty. Nothing else can be assigned there. # name: # type: sq_string # elements: 1 # length: 46 Customized metadata of table and its variables # name: # type: sq_string # elements: 1 # length: 17 table.Description # name: # type: sq_string # elements: 1 # length: 300 tabular: property Description Table description Table description specified as a character vector or a string scalar. If specified as a string scalar, it is converted and stored internally as a character vector. You can access the Description property of a table tbl with tbl.Properties.Description. # name: # type: sq_string # elements: 1 # length: 17 Table description # name: # type: sq_string # elements: 1 # length: 20 table.DimensionNames # name: # type: sq_string # elements: 1 # length: 818 tabular: property DimensionNames Dimension names Dimension names specified as a two-element cell array of character vectors or a two-element string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. You can access the DimensionNames property of a table tbl with tbl.Properties.DimensionNames. By default, DimensionNames is specified as 'Row', 'Variables'. You can access table data per rows or per columns by using either one of the two dimension names, respectively. However, if the table contains row names, then the first element of the DimensionNames corresponds to the row names. The second name returns the variables joined into one array, as table2array joins them, and assigning to it writes them all, as curly brackets over the whole table do. # name: # type: sq_string # elements: 1 # length: 15 Dimension names # name: # type: sq_string # elements: 1 # length: 14 table.RowNames # name: # type: sq_string # elements: 1 # length: 555 table: property RowNames Row names Row names, specified as a cell array of character vectors or a string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. If not empty (default), it must contain the same number of elements as the number of rows in the table. All elements must be nonempty and distinct. You can access the rows of the table tbl by specifying one or more row names within parentheses or curly braces. You can also set RowNames by dot name assignment to an existing variable. # name: # type: sq_string # elements: 1 # length: 9 Row names # name: # type: sq_string # elements: 1 # length: 14 table.UserData # name: # type: sq_string # elements: 1 # length: 245 tabular: property UserData Additional table information Additional table information, specified as an array. Any type of data can be attached using this property. You can access the UserData property of a table tbl with tbl.Properties.UserData. # name: # type: sq_string # elements: 1 # length: 28 Additional table information # name: # type: sq_string # elements: 1 # length: 24 table.VariableContinuity # name: # type: sq_string # elements: 1 # length: 935 tabular: property VariableContinuity Variable continuity Continuity of each variable, specified as a cell array of character vectors or a string array carrying one element per variable, each of them 'unset', 'continuous', 'step' or 'event'. It is empty by default, and assigning {} or [] clears it. If specified as a string array, it is converted and stored internally as a cell array of character vectors. You can access it with tbl.Properties.VariableContinuity and you can index individual variables to read or assign their continuity. A table carries the property but does not act on it, which is also how MATLAB behaves. A timetable uses it to choose the default fill method of each variable when resampling. MATLAB stores this property as a matlab.tabular.Continuity enumeration. Octave has no enumeration classes, so it is stored and returned here as a cell array of character vectors, as VariableNames and VariableUnits are. # name: # type: sq_string # elements: 1 # length: 19 Variable continuity # name: # type: sq_string # elements: 1 # length: 26 table.VariableDescriptions # name: # type: sq_string # elements: 1 # length: 864 tabular: property VariableDescriptions Variable descriptions Variable descriptions, specified as a cell array of character vectors or a string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. If not empty (default), it must contain the same number of elements as the number of variables. If a specific variable does not have a description, this can be specified with an individual empty character vector or an empty string. You can access the VariableDescriptions property of a table tbl with tbl.Properties.VariableDescriptions. You can further index specific variables to access their description. Assigning an empty cell or string array returns the property to its default, and describing a single variable of a table that has no descriptions gives the remaining variables an empty description. # name: # type: sq_string # elements: 1 # length: 21 Variable descriptions # name: # type: sq_string # elements: 1 # length: 19 table.VariableNames # name: # type: sq_string # elements: 1 # length: 550 tabular: property VariableNames Variable names Variable names, specified as a cell array of character vectors or a string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. All elements must be nonempty and distinct, and their number must equal the number of variables. You can access the data type of a specific variable by using dot name assignment, as in tbl.varname, where varname is the name of the variable in table tbl. If the variable name does not exist, a new one is created. # name: # type: sq_string # elements: 1 # length: 14 Variable names # name: # type: sq_string # elements: 1 # length: 19 table.VariableTypes # name: # type: sq_string # elements: 1 # length: 700 tabular: property VariableTypes Variable data types The class of the data of each variable, defined as a cell array of character vectors or a string array with the same number of elements as the number of variables in the table. If specified as a string array, it is converted and stored internally as a cell array of character vectors. You can access the VariableTypes property of a table tbl with tbl.Properties.VariableTypes. You can further index specific variables to access their data type. Modifying the elements of the VariableTypes property automatically converts the underlying data of the corresponding variable into the specified data types provided that a valid conversion is requested. # name: # type: sq_string # elements: 1 # length: 19 Variable data types # name: # type: sq_string # elements: 1 # length: 19 table.VariableUnits # name: # type: sq_string # elements: 1 # length: 803 tabular: property VariableUnits Variable units Variable units, specified as a cell array of character vectors or a string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. If not empty (default), it must contain the same number of elements as the number of variables. If a specific variable does not have a unit, this can be specified with an individual empty character vector or an empty string. You can access the VariableUnits property of a table tbl with tbl.Properties.VariableUnits. You can further index specific variables to access their unit. Assigning an empty cell or string array returns the property to its default, and giving a unit to a single variable of a table that has none gives the remaining variables an empty unit. # name: # type: sq_string # elements: 1 # length: 14 Variable units # name: # type: sq_string # elements: 1 # length: 13 table.addprop # name: # type: sq_string # elements: 1 # length: 1440 table: T = addprop (T, propertyNames, propertyTypes) Add custom properties to a table. T = addprop (T, propertyNames, propertyTypes) adds properties that contain custom metadata to the table T. The input argument propertyNames specifies the names of the custom properties to be added and propertyTypes the type of each corresponding custom property, that is whether the metadata values contained in the property apply to table T as a whole, or to the variables of T. Both propertyNames and propertyTypes can be character vectors, cell arrays of character vectors, or strings. When defined as cell arrays of character vectors or strings, they must have the same number of elements. Valid propertyTypes are either 'table' or 'variable'. When defined as 'table', the custom property can contain any value of any type and size, which applies as metadata to the table as a whole and is stored exactly as it is given. When defined as 'variable', the custom property contains a vector with one element per variable in the table. A 'variable' property is cleared by assigning an empty 0-by-0 value, such as [] or {}, whatever the width of the table; empty values of any other size are not accepted. A character vector is not a valid value for a 'variable' property: use a cell array of character vectors or a string array instead. After adding custom properties using addprop, metadata values can be assigned to the properties using dot syntax. # name: # type: sq_string # elements: 1 # length: 33 Add custom properties to a table. # name: # type: sq_string # elements: 1 # length: 13 table.addvars # name: # type: sq_string # elements: 1 # length: 1625 table: tblB = addvars (tblA, var1, ..., varN) table: tblB = addvars (..., 'After', location) table: tblB = addvars (..., 'Before', location) table: tblB = addvars (..., 'NewVariableNames', newNames) Add new variables to a table. tblB = addvars (tblA, var1, ..., varN) adds new variables to the right of the last variable in table tblA. Each of the arrays specified by the input arguments var1, ..., varN becomes a new variable and its name is derived from the input argument's variable name or a default is created if the input argument is not a variable itself. The input arrays can be of any data type including a table as long as they have the same number of rows as tblA. tblB = addvars (..., 'After', location) adds the new variables after (i.e. to the right of) the table variable specified in location, which can be a character vector, a string scalar, a scalar integer value, or even a logical vector with width (tblA) elements, as long as it indexes a single variable in tblA. tblB = addvars (..., 'Before', location) adds the new variables before (i.e. to the left of) the table variable specified in location, which can be a character vector, a string scalar, a scalar integer value, or even a logical vector with width (tblA) elements, as long as it indexes a single variable in tblA. tblB = addvars (..., 'NewVariableNames', newNames) renames the new variables added from the previous syntaxes according to the names specified by newNames, which can be a character vector, a cell array of character vectors or a string array. The number of names in newNames must be the same as the number of added variables. # name: # type: sq_string # elements: 1 # length: 29 Add new variables to a table. # name: # type: sq_string # elements: 1 # length: 16 table.anymissing # name: # type: sq_string # elements: 1 # length: 473 table: TF = anymissing (tblA) Determine if any table element is missing. TF = anymissing (tblA) returns true if at least one element in table tblA is missing, otherwise it returns false. TF is a logical scalar value. Missing values are defined according to the data type of each variable in tblA: NaN - double, single, duration and calendarDuration NaT - datetime - string - categorical {''} - cell arrays of character vectors '' - character arrays # name: # type: sq_string # elements: 1 # length: 42 Determine if any table element is missing. # name: # type: sq_string # elements: 1 # length: 9 table.cat # name: # type: sq_string # elements: 1 # length: 544 table: tbl = cat (dim, tbl1, tbl2, ...) Concatenate tables along the given dimension. tbl = cat (dim, tbl1, tbl2, ...) concatenates the input tables along dimension dim, which must be either 1 or 2, since a table always has exactly two dimensions. cat (1, ...) concatenates vertically and is equivalent to vertcat, whereas cat (2, ...) concatenates horizontally and is equivalent to horzcat. The same requirements on variable names, row names and size apply, and a 0x0 operand that is not a character array takes no part in the concatenation. # name: # type: sq_string # elements: 1 # length: 45 Concatenate tables along the given dimension. # name: # type: sq_string # elements: 1 # length: 17 table.convertvars # name: # type: sq_string # elements: 1 # length: 1881 table: tblB = convertvars (tblA, vars, dataType) Convert table variables to specified data type. tblB = convertvars (tblA, vars, dataType) converts the variables in tblA specified by vars to the specified data type. vars can be any of the following types. a character vector specifying a single variable. a cell array of character vectors specifying a single or multiple variables. a string array specifying a single or multiple variables. a numeric array of integer values indexing the variables to be converted. a logical vector of the same length as the width of the table tblA indexing as true the variables to be converted. a vartype object used to create a subscript that selects variables of a specified type. dataType specifies the data type to convert those variables to. It can either be a character vector defining the name of the data type to convert to or a function handle, which will perform the conversion. When specifying a name for data type conversion, it can either be a one-argument constructor for the specified data type, which must accept the selected variables' current data types as input, or an available method, which can be applied on selected variables' current data types. When specifying a function handle for applying a conversion on selected variables, this function handle must accept a single input argument and return in its output the same rows as the input argument. Either way, each resulting variable must have the same number of rows as the respective variable selected for conversion. However, depending on the chosen type of conversion, the columns of the converted variable(s) might differ. It is up to the user to ensure that the appropriate type of conversion is performed. convertvars only checks the custom function handles for returning the correct number of rows, which must equal the number of rows of the input table, tblA. # name: # type: sq_string # elements: 1 # length: 47 Convert table variables to specified data type. # name: # type: sq_string # elements: 1 # length: 11 table.empty # name: # type: sq_string # elements: 1 # length: 702 table: tbl = table.empty () table: tbl = table.empty (n) table: tbl = table.empty (r, v) table: tbl = table.empty (sz) Create an empty table. tbl = table.empty () returns a 0-by-0 table. tbl = table.empty (r, v) returns a table with r rows and v variables, at least one of which must be zero. A table with rows but no variables keeps its height, so table.empty (5, 0) answers 5 to height. A table with variables but no rows names them Var1 to VarN and gives each of them a double value, exactly as table ('Size', ...) does. tbl = table.empty (sz) takes the two dimensions from the two-element vector sz, and table.empty (n) is the same as table.empty (n, n). See also: table, isempty, height, width # name: # type: sq_string # elements: 1 # length: 22 Create an empty table. # name: # type: sq_string # elements: 1 # length: 17 table.fillmissing # name: # type: sq_string # elements: 1 # length: 2885 table: tblB = fillmissing (tblA, 'constant', val) table: tblB = fillmissing (tblA, method) table: tblB = fillmissing (..., Name, Value) table: [tblB, TF] = fillmissing (...) Fill missing entries of a table, variable by variable. tblB = fillmissing (tblA, 'constant', val) replaces the missing entries of each table variable with the fill value val. val can be a scalar that is broadcast to every targeted variable, a vector with one element per targeted variable, or a cell array with one fill value per targeted variable. The fill value of each variable must be compatible with that variable's data type. tblB = fillmissing (tblA, method) fills missing entries using the gap-filling method method, which can be one of: 'previous' Fill with the previous non-missing entry along each column. 'next' Fill with the next non-missing entry along each column. 'nearest' Fill with the nearest non-missing entry along each column. When two non-missing entries are equidistant, the later (next) one is used. 'linear' Fill by linear interpolation of the neighbouring entries that are not missing. Numeric, logical, datetime and duration variables can be interpolated; a targeted variable of any other type raises an error. The 'previous', 'next', and 'nearest' methods operate on variables of any data type. Leading or trailing missing entries that cannot be reached by the method are left missing. A variable with nothing ismissing counts as missing, such as a cell array that is not a cell array of character vectors, a structure or a nested table, is left as it is. MATLAB refuses the whole call when such a variable is among the data variables. The following Name/Value pairs are supported: 'DataVariables' Restrict the operation to the indicated subset of table variables. The value uses the same variable referencing as the rest of the table methods. By default, every variable is targeted. 'EndValues' Control how leading and trailing missing entries are filled, whatever the fill method is. Leading entries are those before the first entry that is not missing, and trailing entries those after the last. Valid values are 'extrap' (default), which leaves them to the fill method itself, 'none', which leaves them missing, 'previous', 'next' and 'nearest', which take the value of the nearest entry that is not missing on the side they name and leave the other side missing, or a scalar constant, which must be assignable to the variable it fills. [tblB, TF] = fillmissing (...) also returns a logical array TF with height (tblA) rows and one column per table variable. TF(i,j) is true when an entry of the j-th variable in the i-th row was missing and has been filled. Not yet supported: the 'spline', 'pchip', 'makima', 'movmean', 'movmedian', 'mean', 'median', 'mode', and 'knn' methods, as well as the 'ReplaceValues', 'MaxGap', 'SamplePoints', and 'MissingLocations' options. # name: # type: sq_string # elements: 1 # length: 54 Fill missing entries of a table, variable by variable. # name: # type: sq_string # elements: 1 # length: 16 table.findgroups # name: # type: sq_string # elements: 1 # length: 728 table: G = findgroups (T) table: [G, TID] = findgroups (T) Find groups defined by the variables of a table. G = findgroups (T) returns G, a column vector of positive integer group numbers, with one element for each row of the table T. Each variable of T acts as a grouping variable, and the groups are the unique combinations of values across those variables, sorted in ascending order. If N groups are found, every integer between 1 and N labels a group. Rows holding a missing value (NaN, NaT, , '', or ) in any grouping variable are labelled NaN in G. [G, TID] = findgroups (T) also returns TID, a table whose rows are the sorted unique combinations identifying each group, with the same variables as T. # name: # type: sq_string # elements: 1 # length: 48 Find groups defined by the variables of a table. # name: # type: sq_string # elements: 1 # length: 17 table.groupcounts # name: # type: sq_string # elements: 1 # length: 1597 table: G = groupcounts (T, groupvars) table: G = groupcounts (T, groupvars, groupbins) table: G = groupcounts (..., Name, Value) Count the number of rows in each group of a table. G = groupcounts (T, groupvars) groups the rows of the table T by the grouping variables groupvars and returns the table G with one row per group, holding the grouping variables, a GroupCount variable counting the rows in each group, and a Percent variable giving each group's count as a percentage of the total. groupvars selects the grouping variables by name, index, logical vector, function handle, or vartype subscript. The optional groupbins argument bins the grouping variables before grouping, using bin edges, a number of equal-width bins, a duration bin width, or a datetime calendar-unit keyword, or a cell array with one scheme per grouping variable. A binned grouping variable becomes a categorical and is renamed disc_ or _. See groupsummary for details. Groups are the sorted unique combinations of grouping values. The following Name/Value pairs are accepted: 'IncludeMissingGroups' A logical scalar. When true (the default), rows holding a missing value in a grouping variable form their own groups, sorted after the non-missing groups. When false, such rows are excluded. 'IncludeEmptyGroups' A logical scalar, false by default. When true, the unused categories of a categorical or binned grouping variable contribute empty groups with a GroupCount of 0. 'IncludedEdge' Either 'left' (the default) or 'right', selecting which edge of each bin is inclusive when groupbins is given. # name: # type: sq_string # elements: 1 # length: 50 Count the number of rows in each group of a table. # name: # type: sq_string # elements: 1 # length: 17 table.groupfilter # name: # type: sq_string # elements: 1 # length: 1693 table: G = groupfilter (T, groupvars, method) table: G = groupfilter (T, groupvars, groupbins, method) table: G = groupfilter (..., method, datavars) Filter the rows of a table by a per-group condition. G = groupfilter (T, groupvars, method) groups the rows of the table T by the grouping variables groupvars, applies the filter function method to each group, and returns the table G holding the rows that satisfy the condition, in their original order and with all the variables of T. groupvars selects the grouping variables by name, index, logical vector, function handle, or vartype subscript. method is a function handle applied to each group's slice of every data variable. It must return either a logical scalar, which keeps or drops the whole group, or a logical vector with one element per row of the group, which keeps or drops the individual rows. A row is kept only when the condition holds for it across all data variables. G = groupfilter (T, groupvars, method, datavars) applies method only to the data variables selected by datavars (named, indexed, logical, function handle, or vartype subscript). By default every variable that is not a grouping variable is a data variable. Rows holding a missing value in a grouping variable form their own groups, to which method is applied like any other group. The optional groupbins argument bins the grouping variables before grouping, using bin edges, a number of equal-width bins, a duration bin width, or a datetime calendar-unit keyword, or a cell array with one scheme per grouping variable; see groupsummary for details. The 'IncludedEdge' Name-Value pair ('left' by default, or 'right') selects which bin edge is inclusive. # name: # type: sq_string # elements: 1 # length: 52 Filter the rows of a table by a per-group condition. # name: # type: sq_string # elements: 1 # length: 18 table.groupsummary # name: # type: sq_string # elements: 1 # length: 3453 table: G = groupsummary (T, groupvars) table: G = groupsummary (T, groupvars, groupbins) table: G = groupsummary (T, groupvars, groupbins, method) table: G = groupsummary (T, groupvars, groupbins, method, datavars) table: G = groupsummary (..., Name, Value) Compute summary statistics by group for the variables of a table. G = groupsummary (T, groupvars) groups the rows of the table T by the grouping variables groupvars and returns the table G with one row per group, holding the grouping variables and a GroupCount variable counting the rows in each group. groupvars selects the grouping variables by name, index, logical vector, function handle, or vartype subscript. G = groupsummary (T, groupvars, method) also applies method to each data variable within each group and appends the results to G. method is one of the method names below, a function handle, or a cell array of method names andor function handles: 'sum', 'mean', 'median', 'mode' 'var', 'std', 'min', 'max' 'range', 'nnz' Standard statistics, computed over numeric or logical data variables. NaN values are omitted (as in MATLAB) for every named method except 'nummissing'. 'nummissing' The number of missing values in the group, supported for data variables of any type. 'numunique' The number of unique non-missing values in the group, supported for data variables of any type. A function handle is applied to each group's slice of each data variable and must return a single row (its first dimension must be 1); it receives the values with NaN included. G = groupsummary (T, groupvars, method, datavars) applies method only to the data variables selected by datavars (named, indexed, logical, function handle, or vartype subscript). By default every variable that is not a grouping variable is a data variable. The computed variables of G are named _, e.g. mean_X; results from a function handle are named fun_, where n is the position of the handle among the requested methods. When several methods are requested the computed variables are ordered method first, then data variable. The optional groupbins argument bins the grouping variables before grouping. A binning scheme is one of: a vector of bin edges; a positive integer number of equal-width bins spanning the data range; a duration scalar giving a fixed bin width (for a datetime or duration grouping variable); or, for a datetime grouping variable, a calendar-unit keyword ('second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year', 'decade', or 'century') that bins by that calendar period. A binned grouping variable becomes a categorical and is renamed disc_ for edge, bin-count, or width binning, or _ for calendar-unit binning. Pass a cell array with one scheme per grouping variable to bin them differently, or 'none' to leave a variable unbinned. The following Name/Value pairs are accepted: 'IncludeMissingGroups' A logical scalar. When true (the default), rows holding a missing value in a grouping variable form their own groups, sorted after the non-missing groups. When false, such rows are excluded. 'IncludeEmptyGroups' A logical scalar, false by default. When true, the unused categories of a categorical or binned grouping variable contribute empty groups (GroupCount 0, 0 for 'sum' and 'nnz', NaN otherwise). 'IncludedEdge' Either 'left' (the default) or 'right', selecting which edge of each bin is inclusive when groupbins is given. # name: # type: sq_string # elements: 1 # length: 65 Compute summary statistics by group for the variables of a table. # name: # type: sq_string # elements: 1 # length: 20 table.grouptransform # name: # type: sq_string # elements: 1 # length: 2465 table: G = grouptransform (T, groupvars, method) table: G = grouptransform (T, groupvars, groupbins, method) table: G = grouptransform (..., method, datavars) table: G = grouptransform (..., Name, Value) Transform the data variables of a table group by group. G = grouptransform (T, groupvars, method) groups the rows of the table T by the grouping variables groupvars, applies method to each data variable within each group, and returns the table G with the transformed values, one row per row of T and in the original order. groupvars selects the grouping variables by name, index, logical vector, function handle, or vartype subscript. method is one of the transform names below or a function handle: 'zscore' Center and scale each group to zero mean and unit standard deviation. 'norm' Divide each group by its 2-norm. 'meancenter' Subtract the group mean. 'rescale' Rescale each group to the range [0, 1]. 'meanfill' Replace missing values with the group mean. 'linearfill' Fill missing values by linear interpolation within the group; leading and trailing missing values are left unchanged. For the named methods NaN values are omitted when computing the group statistics. A function handle is applied to each group's slice of each data variable and must return either a single row (broadcast to all the group's rows) or a result with one row per row of the group. G = grouptransform (T, groupvars, method, datavars) transforms only the data variables selected by datavars (named, indexed, logical, function handle, or vartype subscript). By default every variable that is not a grouping variable is a data variable. The following Name/Value pair is accepted: 'ReplaceValues' A logical scalar. When true (the default), each data variable is replaced by its transformed values. When false, the transformed values are appended as new variables named _ (fun1_ for a function handle), leaving the originals in place. 'IncludedEdge' Either 'left' (the default) or 'right', selecting which edge of each bin is inclusive when groupbins is given. Rows holding a missing value in a grouping variable form their own groups, which are transformed like any other group. The optional groupbins argument bins the grouping variables before grouping, using bin edges, a number of equal-width bins, a duration bin width, or a datetime calendar-unit keyword, or a cell array with one scheme per grouping variable; see groupsummary for details. # name: # type: sq_string # elements: 1 # length: 55 Transform the data variables of a table group by group. # name: # type: sq_string # elements: 1 # length: 10 table.head # name: # type: sq_string # elements: 1 # length: 743 table: head (tbl) table: head (tbl, k) table: out = head (tbl, k) Display or return the first k rows of a table. head (tbl) displays the first eight rows of the table tbl. If tbl has fewer than eight rows, then all rows are displayed. head (tbl, k) displays the first k rows of the table tbl. k must be a real, nonnegative, integer scalar value. If tbl has fewer than k rows, then all rows are displayed, and k of zero displays none. out = head (tbl, k) returns the first k rows in a new table out instead of displaying them. If k is omitted or empty, then it defaults to eight. If tbl has fewer than k rows, then all available rows are returned. The returned table preserves the variable names, row names, and all other properties of tbl. # name: # type: sq_string # elements: 1 # length: 46 Display or return the first k rows of a table. # name: # type: sq_string # elements: 1 # length: 12 table.height # name: # type: sq_string # elements: 1 # length: 290 table: H = height (tbl) Number of rows in table. H = height (tbl) returns the number of rows in the table tbl as a scalar. It is the equivalent of size (tbl, 1). For an empty table, or a table created with zero rows, height returns 0. The presence of row names does not affect the result. # name: # type: sq_string # elements: 1 # length: 24 Number of rows in table. # name: # type: sq_string # elements: 1 # length: 13 table.horzcat # name: # type: sq_string # elements: 1 # length: 675 table: tbl = horzcat (tbl1, tbl2, ...) Horizontal concatenation for tables. tbl = horzcat (tbl1, tbl2, ...) merges tables by horizontally concatenating them, provided that all input tables have collectively unique variable names and the same number of rows. Input tables that have row names must share the same unique set of row names but not necessarily in the same order. When row names are present in multiple input tables, their position is matched to the row names of the first input table. Input tables without row names are concatenated by position without re-indexing. Output table's Description and UserData properties are assigned using the first non-empty value. # name: # type: sq_string # elements: 1 # length: 36 Horizontal concatenation for tables. # name: # type: sq_string # elements: 1 # length: 17 table.inner2outer # name: # type: sq_string # elements: 1 # length: 1278 table: tblB = inner2outer (tblA) Invert the nested hierarchy of nested tables in a table. tblB = inner2outer (tblA) finds the variables in tblA that are themselves tables (nested tables) and returns a table tblB in which the inner and outer levels of nesting are transposed. The variables of the nested tables in tblA become the variables of tblB, and the variables of tblA that contain the nested tables become the variables of the nested tables in tblB. Any variable in tblA that is not a nested table is copied unaltered into tblB. For example, if tblA has two variables A and B that both contain nested tables with the variables X and Y, then tblB has two variables X and Y, each containing a nested table with the variables A and B. As a result, the table variables tblA.A.X and tblA.B.X are regrouped into tblB.X.A and tblB.X.B, while tblA.A.Y and tblA.B.Y are regrouped into tblB.Y.A and tblB.Y.B. The new variables of tblB are the union of the variable names of the nested tables in tblA, placed at the position of the first nested table. An inner variable name shared by more than one nested table becomes a nested table in tblB grouping the corresponding variables; an inner variable name held by a single nested table becomes a plain variable carrying that column. # name: # type: sq_string # elements: 1 # length: 56 Invert the nested hierarchy of nested tables in a table. # name: # type: sq_string # elements: 1 # length: 15 table.innerjoin # name: # type: sq_string # elements: 1 # length: 2388 table: tbl = innerjoin (tblL, tblR) table: tbl = innerjoin (tblL, tblR, Name, Value) table: [tbl, ixL, ixR] = innerjoin (...) Inner join between two tables by rows using key variables. tbl = innerjoin (tblL, tblR) combines the tables tblL and tblR by matching the values of their key variables, which by default are the variables that share the same name in both tables. Each row of tbl is formed by horizontally concatenating a row of tblL with a row of tblR whose key variables share the same combination of values. If m rows in tblL and n rows in tblR share the same key combination, then tbl contains all mxn pairings for that combination. The rows of tbl are sorted by the values of the key variables, and any row names are dropped. A key holding a missing value (NaN, NaT, or ) matches no other key. Empty text, an empty character vector in a cellstr or a blank row of a character matrix, is a value and matches empty text, where MATLAB matches it with nothing; ismissing still flags it as missing. By default tbl contains all the variables of tblL followed by the non-key variables of tblR. Whenever a non-key variable name appears in both tables, a suffix derived from each input's argument name is appended to the conflicting names in tbl (for inputs named tblL and tblR, the suffixes '_tblL' and '_tblR'; when an input has no name, '_left' and '_right' are used). When both inputs have the same name, as when a table is joined with itself, the left names are numbered as well ('_T_1' beside '_T'), as in MATLAB. tbl = innerjoin (tblL, tblR, Name, Value) customizes the join with the following options: 'Keys' Variables to use as keys in both tables, given as variable names or indices. It cannot be combined with 'LeftKeys' or 'RightKeys'. 'LeftKeys', 'RightKeys' Variables to use as keys in tblL and tblR, respectively, when the key variables have different names. They must be specified together and reference the same number of variables. 'LeftVariables', 'RightVariables' Variables of tblL and tblR to include in tbl. They may include or exclude key variables. By default 'LeftVariables' is all the variables of tblL and 'RightVariables' is the non-key variables of tblR. [tbl, ixL, ixR] = innerjoin (...) also returns the row-index vectors ixL and ixR such that tbl is the horizontal concatenation of tblL(ixL, leftVars) and tblR(ixR, rightVars). # name: # type: sq_string # elements: 1 # length: 58 Inner join between two tables by rows using key variables. # name: # type: sq_string # elements: 1 # length: 15 table.intersect # name: # type: sq_string # elements: 1 # length: 887 table: tbl = intersect (tblA, tblB) table: tbl = intersect (tblA, tblB, setOrder) table: [tbl, ixA, ixB] = intersect (...) Intersection of two tables by rows. tbl = intersect (tblA, tblB) returns the set of rows common to both tblA and tblB, with duplicate rows removed. Both tables must have the same variable names, although not necessarily in the same order; tbl keeps the variable order of tblA. Rows are compared by their variable values only (row names are ignored), and by default tbl is sorted by those values. A row holding a missing value (NaN, NaT, or ) equals no other row, itself included, and missing values sort last. tbl = intersect (tblA, tblB, setOrder) controls the ordering of tbl, either 'sorted' (default) or 'stable'. [tbl, ixA, ixB] = intersect (...) also returns index vectors ixA and ixB such that tbl equals tblA(ixA,:) and tblB(ixB,:). # name: # type: sq_string # elements: 1 # length: 35 Intersection of two tables by rows. # name: # type: sq_string # elements: 1 # length: 14 table.iscolumn # name: # type: sq_string # elements: 1 # length: 210 table: TF = iscolumn (tbl) Test input table for being a column vector. TF = iscolumn (tbl) returns true if the input table tbl has a single variable. The number of columns within that variable does not matter. # name: # type: sq_string # elements: 1 # length: 43 Test input table for being a column vector. # name: # type: sq_string # elements: 1 # length: 13 table.isempty # name: # type: sq_string # elements: 1 # length: 148 table: TF = isempty (tbl) Test input table for being empty. For tables, isempty is true if the number of rows is 0 or the number of variables is 0. # name: # type: sq_string # elements: 1 # length: 33 Test input table for being empty. # name: # type: sq_string # elements: 1 # length: 13 table.isequal # name: # type: sq_string # elements: 1 # length: 1140 table: TF = isequal (A, B) table: TF = isequal (A, B, ...) Test tables for equality. TF = isequal (A, B) returns a logical scalar TF, which is true when the tables A and B are the same size, carry the same variable names, row names and metadata, and each pair of corresponding variables holds equal values, and false otherwise. Variables are compared by value and not by class, exactly as isequal compares arrays elsewhere, so a table holding int8 ([1; 2]) equals one holding [1; 2]. The VariableTypes property, which only restates those classes, takes no part in the comparison. Every other property does: two tables differing only in Description, UserData, VariableUnits, VariableDescriptions or a custom property are not equal. As with NaN, missing values are never equal, so a missing element anywhere in either table makes the result false; use isequaln to treat missing values as equal. Further tables may be supplied, as in isequal (A, B, C, ...), in which case TF is true only when all of them are equal to one another. Any argument that is not a table, a timetable included, makes the result false rather than raising an error. # name: # type: sq_string # elements: 1 # length: 25 Test tables for equality. # name: # type: sq_string # elements: 1 # length: 14 table.isequaln # name: # type: sq_string # elements: 1 # length: 758 table: TF = isequaln (A, B) table: TF = isequaln (A, B, ...) Test tables for equality, treating missing values as equal. TF = isequaln (A, B) is identical to isequal (A, B) except that missing values are treated as equal to one another, in the same way that isequaln treats NaN. It returns a logical scalar TF, which is true when the tables are the same size, carry the same variable names, row names and metadata, and each pair of corresponding elements is either equal or missing in both, and false otherwise. Further tables may be supplied, as in isequaln (A, B, C, ...), in which case TF is true only when all of them are equal to one another. Any argument that is not a table, a timetable included, makes the result false rather than raising an error. # name: # type: sq_string # elements: 1 # length: 59 Test tables for equality, treating missing values as equal. # name: # type: sq_string # elements: 1 # length: 14 table.ismatrix # name: # type: sq_string # elements: 1 # length: 116 table: TF = ismatrix (tbl) Test input table for being a matrix. For tables, ismatrix is always true, by definition. # name: # type: sq_string # elements: 1 # length: 36 Test input table for being a matrix. # name: # type: sq_string # elements: 1 # length: 14 table.ismember # name: # type: sq_string # elements: 1 # length: 727 table: TF = ismember (tblA, tblB) table: [TF, ixB] = ismember (tblA, tblB) Find set members between two tables by rows. TF = ismember (tblA, tblB) returns a logical column vector TF with one element per row of tblA, where TF(i) is true when the i-th row of tblA also appears as a row of tblB. Both tables must have the same variable names, although not necessarily in the same order, and rows are compared by their variable values only (row names are ignored). A row holding a missing value (NaN, NaT, or ) is a member of nothing. [TF, ixB] = ismember (tblA, tblB) also returns a column vector ixB containing, for each row of tblA, the index of the lowest matching row in tblB, or 0 if there is no match. # name: # type: sq_string # elements: 1 # length: 44 Find set members between two tables by rows. # name: # type: sq_string # elements: 1 # length: 15 table.ismissing # name: # type: sq_string # elements: 1 # length: 2577 table: TF = ismissing (tbl) table: TF = ismissing (tbl, indicator) table: TF = ismissing (..., 'OutputFormat', outFmt) Find missing values in table. TF = ismissing (tbl) returns a logical array, TF, with any true values corresponding to missing elements in the input table tbl. Missing values are defined according to the data type of each variable in tbl: NaN - double, single, duration and calendarDuration NaT - datetime - string - categorical {''} - cell arrays of character vectors '' - character arrays TF = ismissing (tbl, indicator) also returns a logical array, TF, with any true values corresponding to elements in the input table tbl, which are equal to the values in indicator. When specifying an indicator, all default missing values are ignored. If you want to keep them, you need to define them in indicator. indicator can be either a vector of specific data type, in which case all other data types in table tbl are ignored, or a cell array containing mixed types of data types, in which case they match the data types of the variables in table tbl. A nested table variable is never missing, whatever it holds, with or without indicator, as in MATLAB. Besides the explicit data type match between indicator and tbl, the following additional data types matches apply. double indicators match numeric and logical variables. single, integer, and logical indicators match variables of their own class and double variables. Any other numeric or logical variable is not matched, and it is not an error. char and cellstr indicators match string variables. char, cellstr, and string indicators match categorical variables, as categorical.ismissing reads them: an empty or blank character vector matches elements, while strings match category names only, so neither "" nor matches ; missing does. missing, alone or as an element of a cell array indicator, matches the standard missing values of every variable, in addition to any other values in indicator. calendarDuration indicators match calendarDuration variables, as calendarDuration.ismissing does; MATLAB accepts no calendarDuration indicator, so this is an Octave extension. The output array TF has the same size as the input table tbl. TF = ismissing (..., 'OutputFormat', outFmt) specifies whether TF is returned as a logical array or as a table, which maintains the variable names and all other information of the input table tbl. Specifying outFmt as 'logical' (default) returns a logical array. Specifying outFmt as 'tabular' returns a table. # name: # type: sq_string # elements: 1 # length: 29 Find missing values in table. # name: # type: sq_string # elements: 1 # length: 11 table.isrow # name: # type: sq_string # elements: 1 # length: 136 table: TF = isrow (tbl) Test input table for being a row vector. TF = isrow (tbl) returns true if the input table tbl has a single row. # name: # type: sq_string # elements: 1 # length: 40 Test input table for being a row vector. # name: # type: sq_string # elements: 1 # length: 14 table.isscalar # name: # type: sq_string # elements: 1 # length: 160 table: TF = isscalar (tbl) Test input table for being a scalar. TF = isscalar (tbl) returns true if the input table tbl has a single row and a single variable. # name: # type: sq_string # elements: 1 # length: 36 Test input table for being a scalar. # name: # type: sq_string # elements: 1 # length: 18 table.issortedrows # name: # type: sq_string # elements: 1 # length: 4218 table: TF = issortedrows (tblA) table: TF = issortedrows (tblA, 'RowNames') table: TF = issortedrows (tblA, rowDimName) table: TF = issortedrows (tblA, vars) table: TF = issortedrows (tblA, ..., direction) table: TF = issortedrows (..., Name, Value) Check if table rows are sorted accordingly. TF = issortedrows (tblA) determines if the rows in tblA are sorted in ascending order based on the values in the first variable or subsequent variables if elements of the former are repeated. TF is a logical scalar and it is true when tblA == sortrows (tblA) or false otherwise. TF = issortedrows (tblA, 'RowNames') determines if the rows in tblA are sorted according to its row names. TF is true when tblA == sortrows (tblA, 'RowNames') or false otherwise. If tblA does not have row names, i.e. tblA.Properties.RowNames is empty, then TF is true. TF = issortedrows (tblA, rowDimName) determines if the rows in table tblA are sorted along the first dimension, rowDimName, which is the equivalent to the previous syntax, i.e. according to its row names. For this syntax to work, rowDimName must match the first element in tblA.Properties.DimensionNames, otherwise rowDimName is considered a variable name, as in the following syntax. TF is true when tblA == sortrows (tblA, rowDimName) or false otherwise. If tblA does not have row names, i.e. tblA.Properties.RowNames is empty, then TF is true. TF = issortedrows (tblA, vars) determines if the rows in tblA are sorted by the elements in the variables specified by vars, which can be a character vector (for a single variable) or a cell array of character vectors or a string array (specifying a single or multiple variables). If tblA has row names, then vars can include the row names. Alternatively, vars can be a logical vector or a numeric vector of real integers indexing the desired variables. Positive integers specify an ascending order, whereas negative integers specify a descending order for the referenced variables. You can also index all available variables in tblA by passing a semicolon character argument. This Octave-specific syntax facilitates the use of the direction input argument when no particular variable needs to be selected to sort on. Additionally, vars can be a vartype object used to create a subscript that selects variables of a specified type. TF = issortedrows (tblA, ..., direction) determines if the rows in tblA are sorted in the order specified by direction for any of the previous syntaxes. direction can be 'ascend' or 'descend', which is applied to all specified variables or row names that sortrows operates on. direction can also be a cell array of character vectors, whose elements are 'ascend' and 'descend', where each element corresponds to the specified variables and/or row names used for sorting the table. The order specified by direction always takes precedence over the order defined by a numerical vector of integers in vars. direction must always be the 3rd input argument. If you want to omit passing selected variables and allow sortrows to work on consecutive variables until all ties are resolved, then you can leave the second input argument empty, as in sortrows (tblA, {[]}, direction) or pass a colon argument for vars as in sortrows (tblA, {':'}, direction). TF = issortedrows (..., Name, Value) determines if the rows in tblA are sorted according the additional parameters specifying the sorting of rows of a table with the following Name-Value paired arguments. 'MissingPlacement' specifies the placement of missing values with one of the following options: 'auto' places the missing elements at the bottom for ascending order and at the top for descending order; 'first' places missing elements at the top; 'last' places missing elements at the bottom. 'ComparisonMethod' specifies the element comparison method with one of the following options: 'auto' sorts rows using the real part for real numbers and the magnitude for complex numbers; 'real' sorts rows using the real part for both real and complex numbers; 'abs' sorts rows using the magnitude for both real and complex numbers. For complex numbers with equal magnitude, the phase angle in the interval (-Ï€, Ï€] is further used to break ties. # name: # type: sq_string # elements: 1 # length: 43 Check if table rows are sorted accordingly. # name: # type: sq_string # elements: 1 # length: 13 table.istable # name: # type: sq_string # elements: 1 # length: 58 table: tf = istable (tbl) Return true if input is a table. # name: # type: sq_string # elements: 1 # length: 32 Return true if input is a table. # name: # type: sq_string # elements: 1 # length: 14 table.isvector # name: # type: sq_string # elements: 1 # length: 157 table: TF = isvector (tbl) Test input table for being a vector. TF = isvector (tbl) returns true if the input table tbl has a single row or a single column. # name: # type: sq_string # elements: 1 # length: 36 Test input table for being a vector. # name: # type: sq_string # elements: 1 # length: 10 table.join # name: # type: sq_string # elements: 1 # length: 2335 table: tbl = join (tblL, tblR) table: tbl = join (tblL, tblR, Name, Value) table: [tbl, ixR] = join (...) Combine two tables by rows using key variables. tbl = join (tblL, tblR) combines tblL and tblR by matching the values of their key variables, which by default are the variables that share the same name in both tables. tbl contains one row for each row of tblL, in the same order; each is completed with the single row of tblR whose key variables match. The key variables of tblR must contain unique combinations of values, and every key combination in tblL must be present in tblR. A key holding a missing value (NaN, NaT, or ) matches no other key. Empty text, an empty character vector in a cellstr or a blank row of a character matrix, is a value and matches empty text, where MATLAB matches it with nothing; ismissing still flags it as missing. By default tbl contains all the variables of tblL followed by the non-key variables of tblR. Whenever a non-key variable name appears in both tables, a suffix derived from each input's argument name is appended to the conflicting names (for inputs named tblL and tblR, the suffixes '_tblL' and '_tblR'; when an input has no name, '_left' and '_right' are used). When both inputs have the same name, as when a table is joined with itself, the left names are numbered as well ('_T_1' beside '_T'), as in MATLAB. The row names of tblL, if any, are preserved. tbl = join (tblL, tblR, Name, Value) customizes the join with the following options: 'Keys' Variables to use as keys in both tables, given as variable names or indices. It cannot be combined with 'LeftKeys' or 'RightKeys'. 'LeftKeys', 'RightKeys' Variables to use as keys in tblL and tblR, respectively, when the key variables have different names. They must be specified together and reference the same number of variables. 'LeftVariables', 'RightVariables' Variables of tblL and tblR to include in tbl. By default 'LeftVariables' is all the variables of tblL and 'RightVariables' is the non-key variables of tblR. 'KeepOneCopy' Names of non-key variables that occur in both tables for which only the copy from tblL is kept (no suffix is added and the tblR copy is dropped). [tbl, ixR] = join (...) also returns the index vector ixR that identifies, for each row of tbl, the matching row of tblR. # name: # type: sq_string # elements: 1 # length: 47 Combine two tables by rows using key variables. # name: # type: sq_string # elements: 1 # length: 15 table.mergevars # name: # type: sq_string # elements: 1 # length: 2356 table: tblB = mergevars (tblA, vars) table: tblB = mergevars (tblA, vars, Name, Value) Merge table variables into a single multicolumn variable. tblB = mergevars (tblA, vars) combines the table variables in tblA specified by vars to create a new multicolumn variable in tblB. All other variables in tblA are copied to tblB unaltered. By default, the name of the merged variable in tblB takes the form VarN, where N is the position of the first variable in tblA among those to be merged, which is also the location of the merged variable in tblB. Note that merging variables with a 'string' data type variable will result in a multicolumn variable of 'string' data type, by initially converting all other to-be-merged variables into 'string' data type. vars can be any of the following types. a character vector specifying a single variable. a cell array of character vectors specifying a single or multiple variables. a string array specifying a single or multiple variables. a numeric array of integer values indexing the variables to be merged. a logical vector of the same length as the width of the table tblA indexing as true the variables to be merged. a vartype object used to create a subscript that selects variables of a specified type. tblB = mergevars (..., Name, Value) further specifies additional parameters for merging table variables with the following Name-Value paired arguments. 'NewVariableName' specifies the name of the merged variable in tblB, which must be unique. 'NewVariableName' must be either a cellstr or string scalar or a character vector. 'MergeAsTable' specifies whether the selected variables should be merged into a multicolumn variable (default) or into a table nested into a variable, which is useful for variables that cannot be concatenated due to incompatible variable types. 'MergeAsTable' must be either a boolean scalar or a numeric scalar value of 1 (true) or 0 (false). Nothing the merged variables carried about themselves survives: one variable carries one unit, one description, one continuity and one entry of a custom property describing the variables, and there is no saying which of them the merged variable should take, so it takes none: where the continuity is set at all, the merged variable's reads 'unset', and a property that was never set stays unset. The remaining variables keep theirs. # name: # type: sq_string # elements: 1 # length: 57 Merge table variables into a single multicolumn variable. # name: # type: sq_string # elements: 1 # length: 14 table.movevars # name: # type: sq_string # elements: 1 # length: 1455 table: tblB = movevars (tblA, vars) table: tblB = movevars (..., 'After', location) table: tblB = movevars (..., 'Before', location) Move variables in a table. tblB = movevars (tblA, vars) moves the variables specified by vars to the end of the input table tblA. vars can be any of the following types. a character vector specifying a single variable. a cell array of character vectors specifying a single or multiple variables. a string array specifying a single or multiple variables. a numeric array of integer values indexing the variables to be moved. a logical vector of the same length as the width of the table tblA indexing as true the variables to be moved. a vartype object used to create a subscript that selects variables of a specified type. tblB = movevars (..., 'After', location) moves the selected variables after (i.e. to the right of) the table variable specified in location, which can be a character vector, a string scalar, a scalar integer value, or even a logical vector with width (tblA) elements, as long as it indexes a single variable in tblA which is not selected by vars. tblB = movevars (..., 'Before', location) moves the selected variables before (i.e. to the left of) the table variable specified in location, which can be a character vector, a string scalar, a scalar integer value, or even a logical vector with width (tblA) elements, as long as it indexes a single variable in tblA which is not selected by vars. # name: # type: sq_string # elements: 1 # length: 26 Move variables in a table. # name: # type: sq_string # elements: 1 # length: 11 table.ndims # name: # type: sq_string # elements: 1 # length: 90 table: out = ndims (tbl) Number of table dimensions. For tables, ndims (tbl) is always 2. # name: # type: sq_string # elements: 1 # length: 27 Number of table dimensions. # name: # type: sq_string # elements: 1 # length: 11 table.numel # name: # type: sq_string # elements: 1 # length: 425 table: out = numel (tbl) Total number of elements in table. out = numel (tbl) returns the number of elements in the table, tbl, equivalent to prod (size (tbl)). A table is treated as a two-dimensional container, so this is the number of rows times the number of variables. Variables may themselves span multiple columns, but numel only accounts for the number of rows and the number of variables, not the underlying columns. # name: # type: sq_string # elements: 1 # length: 34 Total number of elements in table. # name: # type: sq_string # elements: 1 # length: 15 table.outerjoin # name: # type: sq_string # elements: 1 # length: 3047 table: tbl = outerjoin (tblL, tblR) table: tbl = outerjoin (tblL, tblR, Name, Value) table: [tbl, ixL, ixR] = outerjoin (...) Outer join between two tables by rows using key variables. tbl = outerjoin (tblL, tblR) combines the tables tblL and tblR by matching the values of their key variables, which by default are the variables that share the same name in both tables. Unlike innerjoin, an outer join also keeps the rows of each table that have no match in the other table, filling the variables taken from the non-matching table with missing values (NaN, NaT, , empty string, etc., as appropriate). If m rows in tblL and n rows in tblR share the same key combination, then tbl contains all mxn pairings for that combination. The rows of tbl are sorted by the values of the key variables and any row names are dropped. A key holding a missing value (NaN, NaT, or ) matches no other key. Empty text, an empty character vector in a cellstr or a blank row of a character matrix, is a value and matches empty text, where MATLAB matches it with nothing; ismissing still flags it as missing. By default tbl contains all the variables of tblL followed by all the variables of tblR. Because the key variables are kept from both tables, conflicting names receive a suffix derived from each input's argument name (for inputs named tblL and tblR, the suffixes '_tblL' and '_tblR'; when an input has no name, '_left' and '_right' are used). When both inputs have the same name, as when a table is joined with itself, the left names are numbered as well ('_T_1' beside '_T'), as in MATLAB. See 'MergeKeys' to combine the keys into single columns instead. tbl = outerjoin (tblL, tblR, Name, Value) customizes the join with the following options: 'Type' The type of outer join: 'full' (default) keeps unmatched rows from both tables, 'left' keeps all rows of tblL and only matching rows of tblR, and 'right' keeps all rows of tblR and only matching rows of tblL. 'MergeKeys' A logical scalar (default false). When true, each pair of key variables is merged into a single variable that takes the value from tblL where a matching left row exists and from tblR otherwise. The merged variable is named after the left key when both keys share the same name, or 'leftName_rightName' when their names differ. 'Keys' Variables to use as keys in both tables, given as variable names or indices. It cannot be combined with 'LeftKeys' or 'RightKeys'. 'LeftKeys', 'RightKeys' Variables to use as keys in tblL and tblR, respectively, when the key variables have different names. They must be specified together and reference the same number of variables. 'LeftVariables', 'RightVariables' Variables of tblL and tblR to include in tbl. By default all the variables of each table are included. [tbl, ixL, ixR] = outerjoin (...) also returns the row-index vectors ixL and ixR that identify the row of tblL and tblR, respectively, corresponding to each row of tbl. A zero indicates a row of tbl that has no corresponding row in that table. # name: # type: sq_string # elements: 1 # length: 58 Outer join between two tables by rows using key variables. # name: # type: sq_string # elements: 1 # length: 11 table.pivot # name: # type: sq_string # elements: 1 # length: 3421 table: P = pivot (T, 'Columns', colvars) table: P = pivot (T, 'Rows', rowvars) table: P = pivot (..., Name, Value) Summarize tabular data in a pivoted table. P = pivot (T, 'Columns', colvars, 'Rows', rowvars) reshapes the table T into the pivoted table P. The unique combinations of the grouping variables colvars become the variables (columns) of P, the unique combinations of the grouping variables rowvars become its rows, and each cell holds one statistic computed over the rows of T that fall into that row-and-column group. At least one of 'Columns' or 'Rows' is required; an omitted dimension collapses to a single group. Each of colvars and rowvars selects variables by name, index, or logical vector, and may name several variables. Groups are the sorted unique combinations of the grouping values, with the first variable varying slowest; a categorical variable groups by its category order. Column variable names are taken from the grouping values (e.g. 'true'/'false' for a logical variable), joined with '_' when several variables define the columns. The following Name/Value pairs are accepted: 'DataVariable' The single variable whose values are aggregated. When omitted, the cells hold group counts. 'Method' The aggregation applied to 'DataVariable': one of 'count', 'sum', 'mean', 'median', 'mode', 'std', 'var', 'min', 'max', 'range', 'nummissing', 'numunique', 'nnz', 'percentage', 'none', or a function handle. Named methods omit missing values. The default is 'count' when no data variable is given or the data variable is non-numeric, and 'sum' when it is numeric. 'none' rearranges the data without aggregating and requires at most one value per cell. 'IncludeMissingGroups' A logical scalar, true by default. When true, rows holding a missing value in a grouping variable form their own group, sorted last; when false, such rows are excluded. 'IncludeEmptyGroups' A logical scalar, false by default. When true, every category of a categorical grouping variable contributes a group even if it is unused in the data, so unused combinations appear as empty cells. 'IncludeTotals' A logical scalar, false by default. When true, a 'Total' marginal row and/or column holding the same statistic computed over each margin is appended. Row labels are then placed in the row names. 'RowLabelPlacement' Either 'variable' (the default), which keeps the row grouping variables as the leftmost variables of P, or 'rownames', which places the row group labels in the RowNames property. 'ColumnsBinMethod', 'RowsBinMethod' A binning scheme applied to the 'Columns' or 'Rows' grouping variables before pivoting: a vector of bin edges, a number of equal-width bins, a duration bin width, or a datetime calendar-unit keyword (see groupsummary), or a cell array with one scheme per variable. Each binned variable becomes a categorical. The default 'none' applies no binning. 'IncludedEdge' Either 'left' (the default) or 'right', selecting which edge of each bin is inclusive when a binning scheme is given. 'OutputFormat' 'flat' (default) names each output column after the joined column grouping values (lvl_lvl). 'nested' instead groups two or more 'Columns' variables into nested tables: one outer variable per level of the first column grouping variable, each a nested table whose variables are the next grouping variable's levels (recursively). A marginal-total column, if any, stays a flat outer variable. # name: # type: sq_string # elements: 1 # length: 42 Summarize tabular data in a pivoted table. # name: # type: sq_string # elements: 1 # length: 16 table.removevars # name: # type: sq_string # elements: 1 # length: 666 table: tblB = removevars (tblA, vars) Remove variables from a table. tblB = removevars (tblA, vars) removes the variables specified by vars from the input table tblA. vars can be any of the following types. a character vector specifying a single variable. a cell array of character vectors specifying a single or multiple variables. a string array specifying a single or multiple variables. a numeric array of integer values indexing the variables to be removed. a logical vector of the same length as the width of the table tblA indexing as true the variables to be removed. a vartype object used to create a subscript that selects variables of a specified type. # name: # type: sq_string # elements: 1 # length: 30 Remove variables from a table. # name: # type: sq_string # elements: 1 # length: 16 table.renamevars # name: # type: sq_string # elements: 1 # length: 942 table: tblB = renamevars (tblA, vars, newNames) Rename variables in a table. tblB = renamevars (tblA, vars, newNames) renames the selected variables in the table tblA specified by vars using the names in newNames. vars can be any of the following types. a character vector specifying a single variable. a cell array of character vectors specifying a single or multiple variables. a string array specifying a single or multiple variables. a numeric array of integer values indexing the variables to be renamed. a logical vector of the same length as the width of the table tblA indexing as true the variables to be renamed. a vartype object used to create a subscript that selects variables of a specified type. newNames can either be a character vector (when renaming a single variable) or a cell array of character vectors or a string array. The number of names specified by newNames must match the number of variables specified by vars. # name: # type: sq_string # elements: 1 # length: 28 Rename variables in a table. # name: # type: sq_string # elements: 1 # length: 13 table.repelem # name: # type: sq_string # elements: 1 # length: 650 table: tblB = repelem (tblA, rows, columns) Replicate elements of a table. Replicates each row of the input table tblA rows times and each variable columns times, keeping the repeats of a row together, in a similar fashion to how repelem applies to a matrix. Each repeated variable takes a numbered name, x becoming x, x_1, and so does each repeated row name. Either count may be a vector with one count per row, or per variable, as in repelem (tblA, [1; 2; 0], 1), and a count of zero drops that row or variable. Both counts must be given. A table has exactly two dimensions, so a lone count is not read as applying to both, as it is for a matrix. # name: # type: sq_string # elements: 1 # length: 30 Replicate elements of a table. # name: # type: sq_string # elements: 1 # length: 12 table.repmat # name: # type: sq_string # elements: 1 # length: 242 table: tblB = repmat (tblA, sz) table: tblB = repmat (tblA, rows, columns) Repeat copies of a table. Repeats copies of the input table tblA in a similar fashion to how repmat applies to a matrix. Only two dimensions are supported for tables. # name: # type: sq_string # elements: 1 # length: 25 Repeat copies of a table. # name: # type: sq_string # elements: 1 # length: 15 table.rmmissing # name: # type: sq_string # elements: 1 # length: 2107 table: tbl = rmmissing (tblA) table: tbl = rmmissing (tblA, dim) table: tbl = rmmissing (..., Name, Value) table: [tbl, TF] = rmmissing (...) Remove missing table elements by rows or by variables. tbl = rmmissing (tblA) returns a table with the rows of tblA that contain at least one missing value removed. Missing values are determined per variable according to its data type (NaN for numeric, NaT for datetime, for string, for categorical, {''} for cellstr, etc.), as reported by ismissing. A variable with several columns counts once in a row where any of its columns is missing. tbl = rmmissing (tblA, dim) removes rows when dim is 1, the default, and variables when dim is 2. With dim equal to 2, a variable is removed when it holds a missing value, and the options below count down each variable instead of across each row. tbl = rmmissing (..., Name, Value) customizes the operation with the following options: 'MinNumMissing' A positive integer n (default 1). A row is removed only when it has at least n variables with a missing value, and with dim equal to 2 a variable only when it is missing in at least n rows. 'DataVariables' Restrict the search for missing values to the indicated subset of table variables, using the same variable referencing as the other table methods. Variables outside the subset are not inspected, so they are kept in the output whatever they hold. 'MissingLocations' Supply the missing-value locations explicitly instead of deriving them with ismissing; the values the table holds are then not consulted. The value is either a logical matrix with one row per row of the input and one column per variable of tblA or per inspected variable, or a table of logical variables whose names and sizes match the inspected variables. Where both sizes agree, the columns are taken in the order of the variables of tblA. [tbl, TF] = rmmissing (...) also returns a logical vector TF that is true for each removed row, a column with one element per row of tblA, or with dim equal to 2 for each removed variable, a row with one element per variable of tblA. # name: # type: sq_string # elements: 1 # length: 54 Remove missing table elements by rows or by variables. # name: # type: sq_string # elements: 1 # length: 12 table.rmprop # name: # type: sq_string # elements: 1 # length: 434 table: T = rmprop (T, propertyNames) Remove custom properties from a table. T = rmprop (T, propertyNames) removes properties that contain custom metadata from the table T. The input argument propertyNames specifies the names of the custom properties to be removed and it can either be a character vector, a cell array of character vectors, or a string array. Names that do not match any existing custom property are silently ignored. # name: # type: sq_string # elements: 1 # length: 38 Remove custom properties from a table. # name: # type: sq_string # elements: 1 # length: 12 table.rowfun # name: # type: sq_string # elements: 1 # length: 2351 table: B = rowfun (func, A) table: B = rowfun (func, A, Name, Value, ...) Apply a function to each row of a table. B = rowfun (func, A) applies the function handle func to each row of the table A and returns the results in the table B, which has one row for each row of A. By default the value of each variable in the row is passed to func as a separate input argument, and the output variables of B are named Var1, Var2, and so on. B = rowfun (func, A, Name, Value, ...) modifies the operation through the following Name/Value pairs: 'InputVariables' The variables of A that are passed to func, given as variable names, indices, a logical vector, or a function handle. By default every variable of A that is not a grouping variable is used. 'GroupingVariables' One or more variables of A that define groups of rows. When grouping variables are given, func is applied once to each group, receiving the values of each input variable across the rows of the group; B has one row per group and also includes the grouping variables and a GroupCount variable. Rows with a missing value in any grouping variable are omitted. 'OutputVariableNames' The names of the output variables of B, one per output of func. 'NumOutputs' The number of output arguments to request from func. It defaults to the number of 'OutputVariableNames' if those are given, otherwise to 1. 'SeparateInputs' A logical scalar. When true (the default), the value of each input variable is passed to func as a separate argument. When false, the values of the row are joined into one array, as table2array joins the variables, and passed as a single argument. Variables that cannot form one array are refused. 'ExtractCellContents' A logical scalar. When true, the contents of cell-valued variables are extracted before being passed to func. It defaults to false. 'OutputFormat' The format of B, one of 'auto' (the default, equivalent to 'table'), 'table', 'uniform', or 'cell'. For 'uniform', every call to func must return scalars of the same type, which are concatenated into an array. For 'cell' the results are returned in a cell array. The 'uniform' and 'cell' formats return only the results of func. 'ErrorHandler' A function handle that is called when func throws an error, receiving a structure with fields identifier, message, and index followed by the inputs passed to func. # name: # type: sq_string # elements: 1 # length: 40 Apply a function to each row of a table. # name: # type: sq_string # elements: 1 # length: 15 table.rows2vars # name: # type: sq_string # elements: 1 # length: 2151 table: tblB = rows2vars (tblA) table: tblB = rows2vars (tblA, Name, Value) Reorient table by swapping rows into variables. tblB = rows2vars (tblA) reorients the input table tblA so that its rows become variables in the output table tblB and the variables are swapped into rows and their names are stored into a new variable at the beginning of the output table. If the contents of tblA can be concatenated, then the corresponding variables of tblB are arrays, otherwise they are cell arrays. If the input table tblA contains RowNames, then those names become the variable names of the output table tblB, otherwise the variable names of tblB are generated automatically. rows2vars cannot handle multicolumn variables or nested tables. tblB = rows2vars (..., Name, Value) further specifies additional parameters for reorienting the table with the following Name-Value paired arguments. 'DataVariables' specifies the variables from input table tblA which will be reoriented. 'DataVariables' can be any of the following types: a character vector specifying a single variable; a cell array of character vectors or a string array specifying a single or multiple variables; a numeric array of integer values specifying a single or multiple variables; a logical vector of the same length as the width of the input table specifying a single or multiple variables. 'VariableNamesSource' specifies a single variable that contains the variable names for the output table. The values of the selected variable must have a data type which can be converted to strings and the number of unique names in the selected variable must match the number of rows of the input table. 'VariableNamesSource' accepts the same data types supported by 'DataVariables' as long as they index a single variable, which, however, must not be specified by the 'DataVariables' Name-Value paired argument. 'VariableNamingRule' must be a character vector specifying the rule for naming variables in the output table tblB. When set to 'modify' (default), the variable names are modified so that they are valid variable identifiers. When set to 'preserve', the original names are preserved. # name: # type: sq_string # elements: 1 # length: 47 Reorient table by swapping rows into variables. # name: # type: sq_string # elements: 1 # length: 13 table.setdiff # name: # type: sq_string # elements: 1 # length: 859 table: tbl = setdiff (tblA, tblB) table: tbl = setdiff (tblA, tblB, setOrder) table: [tbl, ixA] = setdiff (...) Difference between two tables by rows. tbl = setdiff (tblA, tblB) returns the set of rows that are present in tblA but not in tblB, with duplicate rows removed. Both tables must have the same variable names, although not necessarily in the same order; tbl keeps the variable order of tblA. Rows are compared by their variable values only (row names are ignored), and by default tbl is sorted by those values. A row holding a missing value (NaN, NaT, or ) equals no other row, itself included, and missing values sort last. tbl = setdiff (tblA, tblB, setOrder) controls the ordering of tbl, either 'sorted' (default) or 'stable'. [tbl, ixA] = setdiff (...) also returns the index vector ixA such that tbl equals tblA(ixA,:). # name: # type: sq_string # elements: 1 # length: 38 Difference between two tables by rows. # name: # type: sq_string # elements: 1 # length: 12 table.setxor # name: # type: sq_string # elements: 1 # length: 922 table: tbl = setxor (tblA, tblB) table: tbl = setxor (tblA, tblB, setOrder) table: [tbl, ixA, ixB] = setxor (...) Exclusive OR of two tables by rows. tbl = setxor (tblA, tblB) returns the set of rows that are present in either tblA or tblB but not in both, with duplicate rows removed. Both tables must have the same variable names, although not necessarily in the same order; tbl keeps the variable order of tblA. Rows are compared by their variable values only (row names are ignored), and by default tbl is sorted by those values. A row holding a missing value (NaN, NaT, or ) equals no other row, itself included, and missing values sort last. tbl = setxor (tblA, tblB, setOrder) controls the ordering of tbl, either 'sorted' (default) or 'stable'. [tbl, ixA, ixB] = setxor (...) also returns index vectors ixA and ixB such that tbl is the vertical concatenation of tblA(ixA,:) and tblB(ixB,:). # name: # type: sq_string # elements: 1 # length: 35 Exclusive OR of two tables by rows. # name: # type: sq_string # elements: 1 # length: 10 table.size # name: # type: sq_string # elements: 1 # length: 495 table: sz = size (tbl) table: dim_sz = size (tbl, dim) table: dim_sz = size (tbl, vecdim) table: [rows, columns] = size (tbl) table: [rows, columns, ...] = size (tbl) Return the size of a table. For tables, the size is [number-of-rows x number-of-variables]. This is the same as [height(obj), width(obj)]. size (tbl, dim) returns the size along dimension dim; dimensions greater than 2 have size 1. dim may be a vector vecdim, in which case a row vector of the corresponding sizes is returned. # name: # type: sq_string # elements: 1 # length: 27 Return the size of a table. # name: # type: sq_string # elements: 1 # length: 14 table.sortrows # name: # type: sq_string # elements: 1 # length: 3997 table: tblB = sortrows (tblA) table: tblB = sortrows (tblA, 'RowNames') table: tblB = sortrows (tblA, rowDimName) table: tblB = sortrows (tblA, vars) table: tblB = sortrows (tblA, ..., direction) table: tblB = sortrows (..., Name, Value) table: [tblB, index] = sortrows (...) Sort the rows of a table. tblB = sortrows (tblA) sorts the rows in tblA in ascending order based on the values in the first variable. If elements in the first variable are repeated, then sortrows sorts by the elements in the second variable, and so on. tblB = sortrows (tblA, 'RowNames') sorts the table tblA according to its row names. If tblA does not have row names, i.e. tblA.Properties.RowNames is empty, then it returns tblA. tblB = sortrows (tblA, rowDimName) also sorts the table tblA along the first dimension, rowDimName, which is the equivalent to the previous syntax, i.e. according to its row names. If tblA does not have row names, that is tblA.Properties.RowNames is empty, then it returns tblA. For this syntax to work, rowDimName must match the first element in tblA.Properties.DimensionNames, otherwise rowDimName is considered a variable name, as in the following syntax. tblB = sortrows (tblA, vars) sorts the rows in table tblA by the elements in the variables specified by vars, which can be a character vector (for a single variable) or a cell array of character vectors or a string array (specifying a single or multiple variables). If tblA has row names, then vars can include the row names. Alternatively, vars can be a logical vector or a numeric vector of real integers indexing the desired variables. Positive integers specify an ascending order, whereas negative integers specify a descending order for the referenced variables. You can also index all available variables in tblA by passing a semicolon character argument. This Octave-specific syntax facilitates the use of the direction input argument when no particular variable needs to be selected to sort on. Additionally, vars can be a vartype object used to create a subscript that selects variables of a specified type. tblB = sortrows (tblA, ..., direction) sorts the rows in table tblA in the order specified by direction for any of the previous syntaxes. direction can be 'ascend' or 'descend', which is applied to all specified variables or row names that sortrows operates on. direction can also be a cell array of character vectors, whose elements are 'ascend' and 'descend', where each element corresponds to the specified variables and/or row names used for sorting the table. The order specified by direction always takes precedence over the order defined by a numerical vector of integers in vars. direction must always be the 3rd input argument. If you want to omit passing selected variables and allow sortrows to work on consecutive variables until all ties are resolved, then you can leave the second input argument empty, as in sortrows (tblA, {[]}, direction) or pass a colon argument for vars as in sortrows (tblA, {':'}, direction). tblB = sortrows (..., Name, Value) specifies additional parameters for sorting rows of a table with the following Name-Value paired arguments. 'MissingPlacement' specifies the placement of missing values with one of the following options: 'auto' places the missing elements at the bottom for ascending order and at the top for descending order; 'first' places missing elements at the top; 'last' places missing elements at the bottom. 'ComparisonMethod' specifies the element comparison method with one of the following options: 'auto' sorts rows using the real part for real numbers and the magnitude for complex numbers; 'real' sorts rows using the real part for both real and complex numbers; 'abs' sorts rows using the magnitude for both real and complex numbers. For complex numbers with equal magnitude, the phase angle in the interval (-Ï€, Ï€] is further used to break ties. [tblB, index] = sortrows (...) also returns an index vector such that tblB = tblA(index,:). # name: # type: sq_string # elements: 1 # length: 25 Sort the rows of a table. # name: # type: sq_string # elements: 1 # length: 16 table.splitapply # name: # type: sq_string # elements: 1 # length: 821 table: Y = splitapply (func, T, G) table: [Y1, ..., YM] = splitapply (func, T, G) Split table data into groups and apply a function to each group. Y = splitapply (func, T, G) splits the rows of the table T into groups according to the group numbers G (typically produced by findgroups), applies the function handle func to each group, and concatenates the per-group results into the output Y. G must be a column vector of positive integers with one element per row of T; if it identifies N groups, every integer between 1 and N must occur at least once. Rows for which G is NaN are omitted. Each variable of T is passed to func as a separate input argument, so func must accept as many arguments as T has variables. [Y1, ..., YM] = splitapply (...) returns the multiple outputs of func, each concatenated across groups. # name: # type: sq_string # elements: 1 # length: 64 Split table data into groups and apply a function to each group. # name: # type: sq_string # elements: 1 # length: 15 table.splitvars # name: # type: sq_string # elements: 1 # length: 1866 table: tblB = splitvars (tblA) table: tblB = splitvars (tblA, vars) table: tblB = splitvars (..., 'NewVariableNames', NewNames) Split multicolumn variables in a table. tblB = splitvars (tblA) splits multicolumn variables in tblA so that they are single-column variables in tblB, while all single-column variables in tblA are copied to tblB unaltered. Each newly created single-column variable in tblB is uniquely named by joining the name of its parent multicolumn variable in tblA and the corresponding column number. If a variable in tblA contains a table, then each variable of this nested table is returned as a newly created variable in tblB. By default, these variables retain their original name in the nested table, unless there are duplicate names, in which case the name of the nested table is also used. If the nested table in tblA contains a multicolumn variable, then the newly created variable in tblB is also multicolumnar. tblB = splitvars (tblA, vars) splits only the variables in tblA specified by vars. If left empty, it defaults to all variables that can be split. Single-column variables specified in vars are copied unaltered. vars can be any of the following types. a character vector specifying a single variable. a cell array of character vectors specifying a single or multiple variables. a string array specifying a single or multiple variables. a numeric array of integer values indexing the variables to be split. a logical vector of the same length as the width of the table tblA indexing as true the variables to be split. a vartype object used to create a subscript that selects variables of a specified type. tblB = splitvars (..., 'NewVariableNames', NewNames) assigns new names to the variables that are split out of tblA and copied to tblB. NewNames can be specified as a cell array of character vectors and/or string arrays. # name: # type: sq_string # elements: 1 # length: 39 Split multicolumn variables in a table. # name: # type: sq_string # elements: 1 # length: 13 table.squeeze # name: # type: sq_string # elements: 1 # length: 183 table: tblB = squeeze (tblA) Remove singleton dimensions. For tables, this is always a no-op that returns the input table unmodified, because tables always have exactly 2 dimensions. # name: # type: sq_string # elements: 1 # length: 28 Remove singleton dimensions. # name: # type: sq_string # elements: 1 # length: 11 table.stack # name: # type: sq_string # elements: 1 # length: 3072 table: tblB = stack (tblA, vars) table: tblB = stack (tblA, {vars1, ..., varsN}) table: tblB = stack (..., Name, Value) table: [tblB, idxA] = stack (...) Stack multiple table variables into a single table variable. tblB = stack (tblA, vars) stacks the values from the variables vars in input tblA into a single variable in output table tblB. By default, the stacked variable in tblB is named by joining the names of the variables in tblA as defined by vars, and it inherits the units and description of the first variable in vars. Additionally, a new categorical variable is included in tblB that indicates which variable in tblA the stacked data in each row of tblB comes from. By default, this categorical variable is named by appending '_Indicator' to the name of the stacked variable. Variables in tblA that are not defined in vars for stacking are replicated in tblB. If tblA contains RowNames, these are not stacked. The stacked variables are joined as table2array joins them, and must each have one column; variables that cannot join, such as a categorical and a numeric variable, are refused. tblB = stack (tblA, {vars1, ..., varsN}) stacks multiple groups of variables, given as a cell array of variable references, producing one stacked data variable in tblB per group (each named and metadata-inherited from its own group). All groups must contain the same number of variables. In this case a single indicator variable, named 'Indicator' by default, holds the numeric position within each group of the source variable for each stacked value. vars can be any of the following types. a character vector specifying a single variable. a cell array of character vectors specifying a single or multiple variables. a string array specifying a single or multiple variables. a numeric array of integer values indexing the variables to be stacked. a logical vector of the same length as the width of the table tblA indexing as true the variables to be stacked. a vartype object used to create a subscript that selects variables of a specified type. tblB = stack (..., Name, Value) further specifies additional parameters for stacking table variables with the following Name-Value paired arguments. 'ConstantVariables' specifies the variables other than vars to include in the output table. By default, all remaining variables not specified by vars are included in the output table. Specifying 'ConstantVariables' allows you to select specific variables to replicate in tblB. Row names in tblA are always replicated in tblB. You can specify 'ConstantVariables' in the same manner as with vars. 'NewDataVariableName' specifies the name for the new data variable in the output table tblB. It can be a character vector, a string scalar, or a cellstring scalar. 'IndexVariableName' specifies the name for the new indicator variable in the output table tblB. It can be a character vector, a string scalar, or a cellstring scalar. [tblB, idxA] = stack (...) also returns an index vector, idxA, indicating the correspondence between the rows in tblB and the rows in tblA. # name: # type: sq_string # elements: 1 # length: 60 Stack multiple table variables into a single table variable. # name: # type: sq_string # elements: 1 # length: 24 table.standardizeMissing # name: # type: sq_string # elements: 1 # length: 1515 table: tblB = standardizeMissing (tblA, indicator) table: tblB = standardizeMissing (..., Name, Value) Insert standard missing values into a table. tblB = standardizeMissing (tblA, indicator) replaces every entry of tblA that matches a value in indicator with the standard missing value of that variable's data type (NaN for double/single and duration, '' for cell arrays of character vectors, for string, for categorical, and NaT for datetime). indicator may be a numeric scalar or vector, a character vector, a string array, a cell array of character vectors, a duration or datetime array, or a cell array mixing any of these. Each indicator is applied only to the variables whose type is compatible with it: numeric indicators match double and single variables, text indicators (char, string, or cellstr) match cell-array-of-character-vector, string, and categorical variables, duration indicators match duration variables, and datetime indicators match datetime variables. A datetime indicator matches the same instant in any time zone, but indicators with and without a time zone cannot be mixed, nor compared with a variable that differs from them in having one. The 'DataVariables' Name/Value pair restricts the operation to a subset of variables, using the same variable referencing as the other table methods. Variables not selected pass through unchanged. Logical and integer variables (which have no standard missing value) and calendarDuration variables pass through unchanged. # name: # type: sq_string # elements: 1 # length: 44 Insert standard missing values into a table. # name: # type: sq_string # elements: 1 # length: 13 table.summary # name: # type: sq_string # elements: 1 # length: 1328 table: summary (tbl) table: s = summary (tbl) Print a summary of a table. summary (tbl) prints the description from tbl.Properties.Description followed by a summary of each table variable's values and their properties as defined in tbl.Properties.VariableUnits and tbl.Properties.VariableDescriptions. s = summary (tbl) returns a structure, s, that contains a summary of the input table, tbl. Each field of s is a structure that summarizes the values in the corresponding variable of tbl. Where applicable, the number of missing values is reported in a NumMissing field and printed when it is greater than zero. For numerical variables of double, single or any int type, it prints the minimum, median, and maximum values. For multicolumnar numerical variables it prints the minimum, median, and maximum values for each column separately. For variables of logical type, it prints the occurrences of True and False. For variables of type datetime and duration it prints the minimum, median, and maximum values, computed after excluding any missing (NaT or NaN) elements. For variables of type calendarDuration, which are not totally ordered, only the size, the type, and the number of missing values are reported. For variables of type cellstr, cell, string, categorical, and struct it prints the size and the type of variable. # name: # type: sq_string # elements: 1 # length: 27 Print a summary of a table. # name: # type: sq_string # elements: 1 # length: 11 table.table # name: # type: sq_string # elements: 1 # length: 1701 table: tbl = table (var1, var2, ..., varN) table: tbl = table ('Size', sz, 'VariableTypes', varTypes) table: tbl = table (..., 'VariableNames', varNames) table: tbl = table (..., 'RowNames', rowNames) table: tbl = table (..., 'DimensionNames', dimNames) Create a new table. tbl = table (var1, var2, ..., varN) creates a new table with the given variables. The variables passed as input arguments become the variables of the table. Their names are automatically detected from the input variable names that you used. tbl = table ('Size', sz, 'VariableTypes', varTypes) creates a new table of the given size, sz, and with the given variable types, varTypes. sz must be a two-element numeric array, where sz(1) specifies the number of rows and sz(2) specifies the number of variables. The variables will contain the default value for elements of that type. tbl = table (..., 'VariableNames', varNames) specifies the variable names to use in the constructed table. varNames must be either a cell array of character vectors or a string array with the same number of nonempty and unique elements as the number of table variables. tbl = table (..., 'RowNames', rowNames) specifies the row names to use in the constructed table. rowNames must be either a cell array of character vectors or a string array with the same number of nonempty and unique elements as the number of rows in the table. tbl = table (..., 'DimensionNames', dimNames) specifies the dimension names to use in the constructed table. dimNames must be either a two-element cell array of character vectors or a two-element string array with nonempty and unique elements. tbl = table () returns an empty table with 0 rows and 0 variables. # name: # type: sq_string # elements: 1 # length: 19 Create a new table. # name: # type: sq_string # elements: 1 # length: 17 table.table2array # name: # type: sq_string # elements: 1 # length: 1790 table: A = table2array (tbl) Converts a table to an array. A = table2array (tbl) places the variables of tbl side by side and joins them into one array A, as concatenation with [] joins them in MATLAB. A multicolumn variable gives as many columns. The row names and every other property of tbl are left out. tbl{:,:} and tbl.Variables return the same array. The variables join as follows: Numeric and logical variables join as numbers do. An integer type takes precedence over single, double and logical, the leftmost integer type over any other integer type, single over double, and double over logical. Values outside the range of the resulting integer type are clipped to its limits. A numeric variable beside a char variable becomes the characters of its values. A logical variable does not join a char variable. Cell variables join only with other cell variables. A duration variable joins double and logical variables, whose values count days, and a calendarDuration variable joins all three. categorical variables join only if all or none of them are ordinal, and datetime variables only if all or none of them have a time zone. Structures join only with the same fields, and nested tables only with different variable names. A missing variable joins double, single, categorical, datetime, duration and calendarDuration variables, as the missing value of their type. Where the variables cannot form one array, A is the cell array that table2cell returns; MATLAB raises an error instead. The exception is a string variable. Numeric, logical, char, cell and missing variables beside it are converted to strings, and A is a string array instead of a cell array. Each row of a char variable becomes one string, each cell one string, and a logical value the string "true" or "false". # name: # type: sq_string # elements: 1 # length: 29 Converts a table to an array. # name: # type: sq_string # elements: 1 # length: 16 table.table2cell # name: # type: sq_string # elements: 1 # length: 451 table: C = table2cell (tbl) Converts a table to a cell array. C = table2cell (tbl) returns a cell array C of the same size as tbl, in which each cell holds one row of one variable, in the variable's own type. A multicolumn variable gives a row in each cell, a char matrix one row of text, and a nested table a table of one row. A cell variable of one column gives the contents of its cells. The row names and every other property of tbl are left out. # name: # type: sq_string # elements: 1 # length: 33 Converts a table to a cell array. # name: # type: sq_string # elements: 1 # length: 15 table.table2csv # name: # type: sq_string # elements: 1 # length: 2599 table: table2csv (tbl, file) table: table2csv (tbl, file, Name, Value) Write a table to a comma-separated-value (CSV) file. table2csv (tbl, file) writes the table tbl to file, which may be a character vector, a cellstr, or a string scalar. The resulting file can be read back with csv2table. The file begins with a comment line reporting how many consecutive rows hold the variable types, names, descriptions, and units, in that order. Those header rows are followed by one row of data per table row. Variables are serialized as follows: Numeric and logical variables are written as numbers (logicals as 0/1). Missing and infinite values are written as the tokens NaN, NA, Inf, and -Inf. Character, cellstr, and string variables are written as quoted text. A char matrix is one column of text, each row written whole so that its padding survives. A missing string, and every entry of a missing variable, is written as an empty unquoted field, which says that there is no value where "" says the value is an empty string. datetime and duration variables are written in ISO 8601 form, which is exact whatever their display format; the format itself is recorded alongside the variable type, so that it is restored on read. calendarDuration and categorical variables are written as their display strings. A multicolumn variable is split into consecutive columns that share the same variable name. A nested table is split into columns tagged with both the outer and the nested variable name. A structure is split into one column per field, tagged with the variable name and the field name. When tbl has row names they are written under a leading RowNames column. A row of variable descriptions and a row of variable units are written whenever the corresponding property is set, or a nested variable carries one; a property that was never set writes no row at all, so that it reads back unset. The following Name-Value options are supported: Name Value 'WriteVariableNames' A logical scalar specifying whether the variable names are written (default true). When false the file carries none, so csv2table numbers the variables on read and can no longer group the columns: a multicolumn variable comes back as separate variables and a nested table as flat columns. 'WriteRowNames' A logical scalar specifying whether the row labels are written as a leading column (default true). Note the following round-trip limitation when reading the file back with csv2table: calendarDuration and categorical variables are returned as cell arrays of character vectors and their values are not reconstructed. # name: # type: sq_string # elements: 1 # length: 52 Write a table to a comma-separated-value (CSV) file. # name: # type: sq_string # elements: 1 # length: 15 table.table2ods # name: # type: sq_string # elements: 1 # length: 3153 table: table2ods (tbl, file) table: table2ods (tbl, file, Name, Value) Write a table to an OpenDocument spreadsheet file. table2ods (tbl, file) writes the table tbl to file, which may be a character vector, a cellstr, or a string scalar. When file ends in .ods a compressed (ZIP-packaged) OpenDocument spreadsheet is written; when it ends in .fods a flat (single-XML) OpenDocument spreadsheet is written instead. The resulting file can be read back with ods2table. The data sheet (named Sheet1 by default) is headed by the variable names and then carries one natively typed cell per value, and a hidden __datatypes_meta__ sheet carries the variable types, descriptions and units needed to restore the exact Octave types on read-back. The names are on the data sheet, where anyone opening the file can see them, and a nested table takes one header row per nesting level. Variables map to ODS cell types as follows: Numeric variables become float cells and logical variables become boolean cells. Integers are written with their exact digits. datetime variables become native date cells and duration variables become native time cells, both encoded as ISO 8601 strings. Character, cellstr, string, categorical, and calendarDuration variables become string cells. A multicolumn variable is split into consecutive columns that share the same variable name. Missing values (NaN, NaT, and missing strings) are written as empty cells. When tbl has row names they are written under a leading RowNames column. A row of variable descriptions and a row of variable units are written whenever the corresponding property is set; a property that was never set writes no row at all, so that it reads back unset. A zone-aware datetime variable keeps its TimeZone on read-back. The following Name-Value options are supported: Name Value 'Sheet' The name of the sheet to write (default 'Sheet1'). When file already exists the named sheet is added or replaced while every other sheet is preserved, so a workbook can be built up one table at a time. 'WriteVariableNames' A logical scalar specifying whether the variable names are written (default true). When false the file carries none at all, the hidden metadata sheet included, so ods2table numbers the variables on read and can no longer group the columns: a multicolumn variable comes back as separate variables and a nested table as flat columns. 'WriteRowNames' A logical scalar specifying whether the row labels are written as a leading column (default true). 'WriteMode' 'overwritesheet' or 'inplace' replace the sheet (the default when the sheet exists), 'append' appends the table's rows to it, and 'replacefile' discards any existing file. A nested table is split into columns tagged with both the outer and the nested variable name, and a structure into one column per field, exactly as table2csv does; the tagging rows live on the hidden metadata sheet, so the data sheet stays flat. Note the following round-trip limitation when reading the file back with ods2table: calendarDuration and categorical variables are returned as cell arrays of character vectors and their values are not reconstructed. # name: # type: sq_string # elements: 1 # length: 50 Write a table to an OpenDocument spreadsheet file. # name: # type: sq_string # elements: 1 # length: 18 table.table2struct # name: # type: sq_string # elements: 1 # length: 532 table: S = table2struct (tbl) table: S = table2struct (tbl, 'ToScalar', true) Converts a table to a scalar structure or structure array. S = table2struct (tbl) returns a structure array with the same fields as the variables in tbl. The length of S is the same as the height of tbl. S = table2struct (tbl, 'ToScalar', true) returns a scalar structure with the same fields as the variables in tbl. Each field has the same rows as the tbl. The output S does not include any of the table's properties. This also applies to row names. # name: # type: sq_string # elements: 1 # length: 58 Converts a table to a scalar structure or structure array. # name: # type: sq_string # elements: 1 # length: 21 table.table2timetable # name: # type: sq_string # elements: 1 # length: 1360 table: tt = table2timetable (tbl) table: tt = table2timetable (tbl, 'RowTimes', rowTimes) table: tt = table2timetable (tbl, 'TimeStep', dt) table: tt = table2timetable (tbl, 'SampleRate', fs) table: tt = table2timetable (..., 'StartTime', t0) Convert a table to a timetable. tt = table2timetable (tbl) converts the table tbl to a timetable, taking its row times from the first variable that is a datetime or a duration. That variable stops being a variable and becomes the row times, and the row dimension is named after it. A table with no such variable cannot be converted this way. tt = table2timetable (tbl, 'RowTimes', rowTimes) says which times to use. rowTimes may be a datetime or duration vector with one element per row, in which case every variable of tbl is kept and the row dimension is named 'Time'; or it may name one of the variables, by name or by index, which is then taken as the row times exactly as above. tt = table2timetable (tbl, 'TimeStep', dt) and tt = table2timetable (tbl, 'SampleRate', fs) generate the row times instead, keeping every variable. 'StartTime' sets the time of the first row for either, and the row dimension is named 'Time'. The row names of tbl, if it has any, are not carried over: a timetable labels its rows by time and by nothing else. See also: timetable.timetable2table, array2timetable, timetable, table # name: # type: sq_string # elements: 1 # length: 31 Convert a table to a timetable. # name: # type: sq_string # elements: 1 # length: 10 table.tail # name: # type: sq_string # elements: 1 # length: 739 table: tail (tbl) table: tail (tbl, k) table: out = tail (tbl, k) Display or return the last k rows of a table. tail (tbl) displays the last eight rows of the table tbl. If tbl has fewer than eight rows, then all rows are displayed. tail (tbl, k) displays the last k rows of the table tbl. k must be a real, nonnegative, integer scalar value. If tbl has fewer than k rows, then all rows are displayed, and k of zero displays none. out = tail (tbl, k) returns the last k rows in a new table out instead of displaying them. If k is omitted or empty, then it defaults to eight. If tbl has fewer than k rows, then all available rows are returned. The returned table preserves the variable names, row names, and all other properties of tbl. # name: # type: sq_string # elements: 1 # length: 45 Display or return the last k rows of a table. # name: # type: sq_string # elements: 1 # length: 14 table.topkrows # name: # type: sq_string # elements: 1 # length: 4293 table: tblB = topkrows (tblA, k) table: tblB = topkrows (tblA, k, 'RowNames') table: tblB = topkrows (tblA, k, rowDimName) table: tblB = topkrows (tblA, k, vars) table: tblB = topkrows (tblA, k, ..., direction) table: tblB = topkrows (..., Name, Value) table: [tblB, index] = topkrows (...) Return the top rows of a table. tblB = topkrows (tblA, k) returns the top k rows from table tblA sorted in descending order based on all of its variables. If elements in the first variable are repeated, then topkrows sorts by the elements in the second variable, and so on. tblB = topkrows (tblA, k, 'RowNames') returns the top k rows from table tblA sorted according to its row names. If tblA does not have row names, i.e. tblA.Properties.RowNames is empty, then it returns tblA. tblB = topkrows (tblA, k, rowDimName) also returns the top k rows from table tblA sorted along its first dimension, rowDimName, which is the equivalent to the previous syntax, i.e. according to its row names. If tblA does not have row names, i.e. tblA.Properties.RowNames is empty, then it returns tblA. For this syntax to work, rowDimName must match the first element in tblA.Properties.DimensionNames, otherwise rowDimName is considered a variable name, as in the following syntax. tblB = topkrows (tblA, k, vars) returns the top k rows from table tblA sorted by the elements in the variables specified by vars, which can be a character vector (for a single variable) or a cell array of character vectors or a string array (specifying a single or multiple variables). If tblA has row names, then vars can include the row names. Alternatively, vars can be a logical vector or a numeric vector of real integers indexing the desired variables. Unlike sortrows, positive integers specify a descending order, whereas negative integers specify an ascending order for the referenced variables, consistent with the descending default of topkrows. You can also index all available variables in tblA by passing a semicolon character argument. This Octave-specific syntax facilitates the use of the direction input argument when no particular variable needs to be selected to sort on. Additionally, vars can be a vartype object used to create a subscript that selects variables of a specified type. tblB = topkrows (tblA, k, ..., direction) returns the top k rows from table tblA sorted in the order specified by direction for any of the previous syntaxes. direction can be 'ascend' or 'descend', which is applied to all specified variables or row names that sortrows operates on. direction can also be a cell array of character vectors, whose elements are 'ascend' and 'descend', where each element corresponds to the specified variables and/or row names used for sorting the table. The order specified by direction always takes precedence over the order defined by a numerical vector of integers in vars. direction must always be the 3rd input argument. If you want to omit passing selected variables and allow sortrows to work on consecutive variables until all ties are resolved, then you can leave the second input argument empty, as in sortrows (tblA, {[]}, direction) or pass a colon argument for vars as in sortrows (tblA, {':'}, direction). tblB = topkrows (..., k, Name, Value) returns the top k rows from table tblA sorted with any of the previous syntaxes and further specified by additional parameters for sorting rows of a table with the following Name-Value paired arguments. 'MissingPlacement' specifies the placement of missing values with one of the following options: 'auto' places the missing elements at the bottom for ascending order and at the top for descending order; 'first' places missing elements at the top; 'last' places missing elements at the bottom. 'ComparisonMethod' specifies the element comparison method with one of the following options: 'auto' sorts rows using the real part for real numbers and the magnitude for complex numbers; 'real' sorts rows using the real part for both real and complex numbers; 'abs' sorts rows using the magnitude for both real and complex numbers. For complex numbers with equal magnitude, the phase angle in the interval (-Ï€, Ï€] is further used to break ties. [tblB, index] = topkrows (...) also returns an index vector such that tblB = tblA(index,:). # name: # type: sq_string # elements: 1 # length: 31 Return the top rows of a table. # name: # type: sq_string # elements: 1 # length: 11 table.union # name: # type: sq_string # elements: 1 # length: 976 table: tbl = union (tblA, tblB) table: tbl = union (tblA, tblB, setOrder) table: [tbl, ixA, ixB] = union (...) Union of two tables by rows. tbl = union (tblA, tblB) returns the combined set of rows of tblA and tblB, with duplicate rows removed. Both tables must have the same variable names, although not necessarily in the same order; tbl keeps the variable order of tblA. Rows are compared by their variable values only (row names are ignored), and by default tbl is sorted by those values. A row holding a missing value (NaN, NaT, or ) equals no other row, itself included, and missing values sort last. tbl = union (tblA, tblB, setOrder) controls the ordering of tbl. setOrder is either 'sorted' (default) for ascending order, or 'stable' to keep the order in which the rows appear in tblA and tblB. [tbl, ixA, ixB] = union (...) also returns the index vectors ixA and ixB such that tbl is the vertical concatenation of tblA(ixA,:) and tblB(ixB,:). # name: # type: sq_string # elements: 1 # length: 28 Union of two tables by rows. # name: # type: sq_string # elements: 1 # length: 12 table.unique # name: # type: sq_string # elements: 1 # length: 1254 table: tblB = unique (tblA) table: tblB = unique (tblA, setOrder) table: tblB = unique (tblA, occurrence) table: [tblB, ixA, ixB] = unique (...) Unique rows in a table. tblB = unique (tblA) returns the unique rows of table tblA in sorted order. A row holding a missing value (NaN, NaT, or ) equals no other row, so each such row is kept; empty text is a value. tblB = unique (tblA, setOrder) returns the unique rows of table tblA in a specified order. setOrder can be either 'sorted' (default) or 'stable'. 'sorted' returns the unique rows sorted in ascending order. 'stable' returns the unique rows according to their order of occurrence. tblB = unique (tblA, occurrence) returns the unique rows of table tblA according to their order of occurrence. occurrence can be either 'first' (default) or 'last'. 'first' returns the first occurrence of each unique row, i.e. the lowest possible indices are returned. 'last' returns the last occurrence of each unique row, i.e. the highest possible indices are returned. [tblB, ixA, ixB] = unique (...) also returns index vectors ixA and ixB using any of the previous syntaxes. ixA and ixB map the tables tblA and tblB to one another such that tblB = tblA(ixA,:) and tblA = tblB(ixB,:). # name: # type: sq_string # elements: 1 # length: 23 Unique rows in a table. # name: # type: sq_string # elements: 1 # length: 13 table.unstack # name: # type: sq_string # elements: 1 # length: 4276 table: tblB = unstack (tblA, vars, ivar) table: tblB = unstack (..., Name, Value) table: [tblB, idxA] = unstack (...) Unstack a single table variable into multiple table variables. tblB = unstack (tblA, vars, ivar) unstacks the values from the variables vars according to the indicator variable ivar in input tblA into multiple variables in output table tblB. The new (unstacked) variables in tblB are named according to the unique values of the indicator variable and the rows with matching indicator values are aggregated into the new (unstacked) variables. By default, numeric and duration data types are aggregated by summation, whereas from other data types the first unique element of each group is returned. vars may specify one or more variables of any data type supported by the table class except for nested tables, whereas ivar must only specify a single variable, which must be numeric, logical, categorical, string, or cellstring. Both vars and ivar can be specified as follows: a character vector specifying a single variable. a cell array of character vectors specifying a single or multiple variables. a string array specifying a single or multiple variables. a numeric array of integer values indexing the variables to be unstacked. a logical vector of the same length as the width of the table tblA indexing as true the variables to be unstacked. a vartype object used to create a subscript that selects variables of a specified type. By default, all remaining variables in tblA which are not specified by vars and ivar are treated as grouping variables, in which case each unique combination of values in the grouping variables identifies a group of rows in tblA that is unstacked into one row of tblB. tblB = unstack (..., Name, Value) further specifies additional parameters for unstacking table variables with the following Name-Value paired arguments. 'GroupingVariables' specifies the variables that should be used as grouping variables. All valid schemes for indexing a table variable can be used. If grouping variables have missing values, the data from corresponding rows are not aggregated in the output table. Table row names cannot be assigned as a grouping variable, since these must be unique for each row, which would defeat the purpose of unstacking a table onto itself. 'ConstantVariables' specifies the variables that are constant within each group. All valid schemes for indexing a table variable can be used. The values for these variables in the output are taken from the first row in each group in the input. By default, no variable is treated as constant unless specified. However, if the input table has row names, these effectively are treated as constant variables. 'NewDataVariableNames' specifies the names for the new data variables in the output table tblB. It can be a character vector, a string scalar, or a cellstring scalar. By default, the names of the new unstacked data variables are based on the string representation of the unique values in the indicator variable ivar. If multiple variables are unstacked, then unstack generates composite names using both the values from the indicator variable and the name of the variable being unstacked. The number of names must match the number of unique values in the indicator variable. 'AggregationFunction' specifies a function handle used to aggregate each group's data into a single value. By default, @sum is applied on numeric data, whereas @unique is applied on all other supported data types, including duration and calendarDuration. In the latter case, if a group contains more than one distinct value for the same indicator value, the default aggregation errors, and an explicit 'AggregationFunction' that returns a scalar must be specified. 'VariableNamingRule', specified as either 'modify' or 'preserve', defines the rule for naming the new unstacked variables in the output table tblB. 'modify' (default) forces all variable names to be valid Octave variable names. 'preserve' preserves the original names taken from the input table, which can have any Unicode characters, including spaces and non-ASCII characters. [tblB, idxA] = unstack (...) also returns an index vector, idxA, indicating the correspondence between the rows in tblB and the rows in tblA. # name: # type: sq_string # elements: 1 # length: 62 Unstack a single table variable into multiple table variables. # name: # type: sq_string # elements: 1 # length: 12 table.varfun # name: # type: sq_string # elements: 1 # length: 1893 table: B = varfun (func, A) table: B = varfun (func, A, Name, Value, ...) Apply a function to each variable of a table. B = varfun (func, A) applies the function handle func separately to each variable of the table A and returns the results in the table B. func is called once per variable with that variable as its single input argument. By default each output variable of B is named f_v, where f is the name of func (or Fun when func is anonymous) and v is the name of the corresponding variable of A. B = varfun (func, A, Name, Value, ...) modifies the operation through the following Name/Value pairs: 'InputVariables' The variables of A to which func is applied, given as variable names, indices, a logical vector, or a function handle that returns true for the variables to include. By default func is applied to every variable of A that is not a grouping variable. 'GroupingVariables' One or more variables of A that define groups of rows. When grouping variables are given, func is applied to the values of each input variable within each group, B has one row per group, and B also includes the grouping variables and a GroupCount variable holding the number of rows in each group. Rows with a missing value in any grouping variable are omitted. 'OutputFormat' The format of B, one of 'auto' (the default, equivalent to 'table'), 'table', 'uniform', or 'cell'. For 'uniform', func must return a scalar on each call and the results are concatenated into an array. For 'cell' the results are returned in a cell array. The 'uniform' and 'cell' formats return only the results of func, without the grouping variables or GroupCount. 'ErrorHandler' A function handle that is called when func throws an error. It receives a structure with fields identifier, message, and index, followed by the same inputs that were passed to func, and its outputs are used in place of the outputs of func. # name: # type: sq_string # elements: 1 # length: 45 Apply a function to each variable of a table. # name: # type: sq_string # elements: 1 # length: 13 table.vertcat # name: # type: sq_string # elements: 1 # length: 663 table: tbl = vertcat (tbl1, tbl2, ...) Vertical concatenation for tables. tbl = vertcat (tbl1, tbl2, ...) merges tables by vertically concatenating them, provided that all input tables have the same variable names but not necessarily in the same order. The positions of the variable names are matched to those of the first input table. When any input table has row names, they must be unique across all input tables. In such a case, rows coming from input tables without row names are assigned default RowN names, where N is the row's position in the output table. Output table's Description and UserData properties are assigned using the first non-empty value. # name: # type: sq_string # elements: 1 # length: 34 Vertical concatenation for tables. # name: # type: sq_string # elements: 1 # length: 11 table.width # name: # type: sq_string # elements: 1 # length: 448 table: W = width (tbl) Number of variables in table. W = width (tbl) returns the number of variables in the table tbl as a scalar. It is the equivalent of size (tbl, 2). Note that this is the number of table variables, not the total number of columns. A single variable may itself contain several columns (for example, a matrix-valued variable), but it still counts as one towards the table width. For a table with no variables, width returns 0. # name: # type: sq_string # elements: 1 # length: 29 Number of variables in table. # name: # type: sq_string # elements: 1 # length: 16 table.writetable # name: # type: sq_string # elements: 1 # length: 3181 table: writetable (tbl, filename) table: writetable (tbl, filename, Name, Value) Write a table to a file in a MATLAB-compatible format. writetable (tbl, filename) writes the table tbl to filename. The file type is inferred from the extension: .txt, .csv, and .dat are written as delimited text; .ods as an OpenDocument spreadsheet; and .xlsx and .xlsm as Excel spreadsheets. Use the 'FileType' option to override the inferred type. Unlike table2csv/table2ods, no type metadata is written: the file holds only an optional variable-name header row followed by the data, so it can be read by other applications. Type information is recovered by readtable through automatic detection (text) or the native cell types (spreadsheet). .ods is the OpenDocument spreadsheet format, an open standard that LibreOffice and other applications read and write on every platform. MATLAB is the exception and cannot be given one. Its writetable refuses the extension, and its readers open an OpenDocument file only by handing it to Excel, so a MATLAB user without Excel cannot read one at all. A file written for a MATLAB user should therefore be .csv or .xlsx. Everywhere else the format is a good choice, and table2ods writes it losslessly. The following options are supported: Name Value 'FileType' 'text' or 'spreadsheet'. 'WriteVariableNames' Logical; write the variable names as the first row (default true). 'WriteRowNames' Logical; write the row names as the first column (default false). 'Delimiter' Field delimiter for text files: a single character or one of 'comma', 'space', 'tab', 'semi', 'bar' (default ','). 'QuoteStrings' 'minimal', 'all', or 'none' for text files (default 'minimal'). 'Sheet' Spreadsheet only: the name of the sheet to write. The default is the first sheet of an existing workbook, or 'Sheet1' for a new file. 'Range' Spreadsheet only: an A1-style anchor such as 'C5' at which to place the top-left corner of the data (fresh writes only). 'WriteMode' For text: 'overwrite' (default) or 'append'. For spreadsheets: 'overwritesheet' / 'inplace' (replace the target sheet), 'append' (append rows to it), or 'replacefile' (overwrite the whole file). When the target spreadsheet already exists, the sheet named by 'Sheet' (defaulting to the first existing sheet) is added or replaced while every other sheet is preserved, unless 'WriteMode' is 'replacefile'. For ODS, existing foreign spreadsheets (for example those written by LibreOffice) are updated in place, keeping their other parts; for Excel (.xlsx, .xlsm) the workbook is read back and rewritten, so only its cell values are preserved. Dates written to Excel use its 1900 serial date system, which counts a 29 February 1900 that never existed: 1900-03-01 is serial 61, and every earlier date is one less than its plain day count. That system has no serial below 0 (1899-12-31), so any earlier date is written as text rather than as a date, as MATLAB does. MATLAB spells that text in the datetime display format; here it is written in ISO 8601 form. Nested tables and structures are not supported, and the legacy binary formats .xls and .xlsb are not supported either; use .xlsx, .ods, or a text format. # name: # type: sq_string # elements: 1 # length: 54 Write a table to a file in a MATLAB-compatible format. # name: # type: sq_string # elements: 1 # length: 7 tabular # name: # type: sq_string # elements: 1 # length: 559 datatypes: tabular Abstract superclass of table and timetable. tabular holds everything the two tabular classes share: their common metadata properties, their variable storage, and the machinery that does not depend on how rows are labelled. It is abstract and cannot be instantiated; isa (obj, "tabular") is true for both table and timetable, and neither derives from the other. Everything that does depend on row labels is reached through a small set of hooks that each subclass implements: table labels its rows with RowNames and timetable with RowTimes. # name: # type: sq_string # elements: 1 # length: 43 Abstract superclass of table and timetable. # name: # type: sq_string # elements: 1 # length: 24 tabular.CustomProperties # name: # type: sq_string # elements: 1 # length: 1136 tabular: property CustomProperties Customized metadata of table and its variables Custom properties that contain metadata of a table and its variables. By default, this is an empty container. Each custom property holds either table metadata or per-variable metadata, according to the property type ('table' or 'variable') specified when the property is created with the addprop method. A variable-scoped property holds one element per variable. You can add an individual custom property only by using the addprop method and you can only remove a custom property with the rmprop method. To access existing custom properties use dot name structure assignment as in tbl.Properties.CustomProperties.PropertyName, where PropertyName is the name used with the addprop method. The whole set may also be taken from another table by assigning that table's CustomProperties to this one, which replaces every custom property with those of the other table, their types included. A variable-scoped property arriving that way must hold one element for each variable of the receiving table, or be a 0-by-0 empty. Nothing else can be assigned there. # name: # type: sq_string # elements: 1 # length: 46 Customized metadata of table and its variables # name: # type: sq_string # elements: 1 # length: 19 tabular.Description # name: # type: sq_string # elements: 1 # length: 300 tabular: property Description Table description Table description specified as a character vector or a string scalar. If specified as a string scalar, it is converted and stored internally as a character vector. You can access the Description property of a table tbl with tbl.Properties.Description. # name: # type: sq_string # elements: 1 # length: 17 Table description # name: # type: sq_string # elements: 1 # length: 22 tabular.DimensionNames # name: # type: sq_string # elements: 1 # length: 818 tabular: property DimensionNames Dimension names Dimension names specified as a two-element cell array of character vectors or a two-element string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. You can access the DimensionNames property of a table tbl with tbl.Properties.DimensionNames. By default, DimensionNames is specified as 'Row', 'Variables'. You can access table data per rows or per columns by using either one of the two dimension names, respectively. However, if the table contains row names, then the first element of the DimensionNames corresponds to the row names. The second name returns the variables joined into one array, as table2array joins them, and assigning to it writes them all, as curly brackets over the whole table do. # name: # type: sq_string # elements: 1 # length: 15 Dimension names # name: # type: sq_string # elements: 1 # length: 16 tabular.UserData # name: # type: sq_string # elements: 1 # length: 245 tabular: property UserData Additional table information Additional table information, specified as an array. Any type of data can be attached using this property. You can access the UserData property of a table tbl with tbl.Properties.UserData. # name: # type: sq_string # elements: 1 # length: 28 Additional table information # name: # type: sq_string # elements: 1 # length: 26 tabular.VariableContinuity # name: # type: sq_string # elements: 1 # length: 935 tabular: property VariableContinuity Variable continuity Continuity of each variable, specified as a cell array of character vectors or a string array carrying one element per variable, each of them 'unset', 'continuous', 'step' or 'event'. It is empty by default, and assigning {} or [] clears it. If specified as a string array, it is converted and stored internally as a cell array of character vectors. You can access it with tbl.Properties.VariableContinuity and you can index individual variables to read or assign their continuity. A table carries the property but does not act on it, which is also how MATLAB behaves. A timetable uses it to choose the default fill method of each variable when resampling. MATLAB stores this property as a matlab.tabular.Continuity enumeration. Octave has no enumeration classes, so it is stored and returned here as a cell array of character vectors, as VariableNames and VariableUnits are. # name: # type: sq_string # elements: 1 # length: 19 Variable continuity # name: # type: sq_string # elements: 1 # length: 28 tabular.VariableDescriptions # name: # type: sq_string # elements: 1 # length: 864 tabular: property VariableDescriptions Variable descriptions Variable descriptions, specified as a cell array of character vectors or a string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. If not empty (default), it must contain the same number of elements as the number of variables. If a specific variable does not have a description, this can be specified with an individual empty character vector or an empty string. You can access the VariableDescriptions property of a table tbl with tbl.Properties.VariableDescriptions. You can further index specific variables to access their description. Assigning an empty cell or string array returns the property to its default, and describing a single variable of a table that has no descriptions gives the remaining variables an empty description. # name: # type: sq_string # elements: 1 # length: 21 Variable descriptions # name: # type: sq_string # elements: 1 # length: 21 tabular.VariableNames # name: # type: sq_string # elements: 1 # length: 550 tabular: property VariableNames Variable names Variable names, specified as a cell array of character vectors or a string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. All elements must be nonempty and distinct, and their number must equal the number of variables. You can access the data type of a specific variable by using dot name assignment, as in tbl.varname, where varname is the name of the variable in table tbl. If the variable name does not exist, a new one is created. # name: # type: sq_string # elements: 1 # length: 14 Variable names # name: # type: sq_string # elements: 1 # length: 21 tabular.VariableTypes # name: # type: sq_string # elements: 1 # length: 700 tabular: property VariableTypes Variable data types The class of the data of each variable, defined as a cell array of character vectors or a string array with the same number of elements as the number of variables in the table. If specified as a string array, it is converted and stored internally as a cell array of character vectors. You can access the VariableTypes property of a table tbl with tbl.Properties.VariableTypes. You can further index specific variables to access their data type. Modifying the elements of the VariableTypes property automatically converts the underlying data of the corresponding variable into the specified data types provided that a valid conversion is requested. # name: # type: sq_string # elements: 1 # length: 19 Variable data types # name: # type: sq_string # elements: 1 # length: 21 tabular.VariableUnits # name: # type: sq_string # elements: 1 # length: 803 tabular: property VariableUnits Variable units Variable units, specified as a cell array of character vectors or a string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. If not empty (default), it must contain the same number of elements as the number of variables. If a specific variable does not have a unit, this can be specified with an individual empty character vector or an empty string. You can access the VariableUnits property of a table tbl with tbl.Properties.VariableUnits. You can further index specific variables to access their unit. Assigning an empty cell or string array returns the property to its default, and giving a unit to a single variable of a table that has none gives the remaining variables an empty unit. # name: # type: sq_string # elements: 1 # length: 14 Variable units # name: # type: sq_string # elements: 1 # length: 9 timerange # name: # type: sq_string # elements: 1 # length: 350 datatypes: timerange Subscript into a timetable by a range of times. A utility class that selects the rows of a timetable whose times fall in an interval. The interval is half-open by default, including the time it starts at and excluding the one it ends at, so that ranges laid end to end select each row exactly once. See also: withtol, timetable # name: # type: sq_string # elements: 1 # length: 47 Subscript into a timetable by a range of times. # name: # type: sq_string # elements: 1 # length: 24 timerange.hasEventBounds # name: # type: sq_string # elements: 1 # length: 102 timerange: tf = hasEventBounds (tr) Whether the range's bounds are event filters awaiting a timetable. # name: # type: sq_string # elements: 1 # length: 66 Whether the range's bounds are event filters awaiting a timetable. # name: # type: sq_string # elements: 1 # length: 18 timerange.interval # name: # type: sq_string # elements: 1 # length: 316 timerange: [lo, hi, cl, cr] = interval (tr, rowTimes) Return the interval the range describes, in the row times' own type. lo and hi come back as the class of rowTimes, or as -Inf or Inf where the range is unbounded on that side. cl and cr say whether the interval includes the bound at its lower and its upper end. # name: # type: sq_string # elements: 1 # length: 68 Return the interval the range describes, in the row times' own type. # name: # type: sq_string # elements: 1 # length: 28 timerange.resolveEventBounds # name: # type: sq_string # elements: 1 # length: 276 timerange: tr2 = resolveEventBounds (tr, tt) Return the range with its event filter bounds read against tt. Each bound becomes the time of the first event of tt its filter matches, so the range runs from the start of one event to the start of the other and stops short of it. # name: # type: sq_string # elements: 1 # length: 62 Return the range with its event filter bounds read against tt. # name: # type: sq_string # elements: 1 # length: 20 timerange.rowIndices # name: # type: sq_string # elements: 1 # length: 198 timerange: ix = rowIndices (tr, rowTimes) Return the positions in rowTimes the range selects. The positions come back in ascending order, being the rows of the timetable in the order it holds them. # name: # type: sq_string # elements: 1 # length: 51 Return the positions in rowTimes the range selects. # name: # type: sq_string # elements: 1 # length: 19 timerange.timerange # name: # type: sq_string # elements: 1 # length: 1437 timerange: tr = timerange (startTime, endTime) timerange: tr = timerange (startTime, endTime, intervalType) timerange: tr = timerange (t, unit) timerange: tr = timerange (startTime, endTime, unit) Create a time range subscript. tr = timerange (startTime, endTime) creates a subscript selecting the rows of a timetable whose times are at or after startTime and strictly before endTime. The bounds may be datetime or duration scalars, text that reads as one, or -Inf and Inf for no bound at that end; they may not both be infinite, there being nothing to say what kind of time is meant. tr = timerange (startTime, endTime, intervalType) says which bounds belong to the interval: 'openright' (the default) includes the start alone, 'openleft' the end alone, 'closed' both and 'open' neither. tr = timerange (t, unit) covers the whole calendar period containing t, one of 'seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'quarters' or 'years', in the singular or the plural. t must be a datetime: a duration is elapsed time and sits on no calendar. tr = timerange (startTime, endTime, unit) widens the range to whole periods of unit: it runs from the start of the period holding startTime to the start of the period after the one holding endTime, the far end left out as usual. Both bounds must be datetime scalars. A unit and an interval type cannot both be given, the unit fixing the ends itself. See also: withtol, timetable # name: # type: sq_string # elements: 1 # length: 30 Create a time range subscript. # name: # type: sq_string # elements: 1 # length: 10 timeseries # name: # type: sq_string # elements: 1 # length: 1189 datatypes: timeseries Array of samples indexed by time. A timeseries object holds a sequence of samples, each taken at a time on a common time vector, together with the metadata that describes them: the units of the data and of the time, an optional absolute start date, the method of evaluating the series between samples, a quality code per sample, and named events. Each sample may be a scalar, a vector, or an array. The time vector runs either down the first dimension of the data, each row being a sample, or along its last dimension, each slice being one; IsTimeFirst reports which, as the constructor describes. timeseries is a value class: every method returns a modified copy and leaves its input unchanged. An array of timeseries objects is built by concatenation, as [ts1, ts2]. MATLAB recommends timetable for new code; timeseries is provided for code that exchanges it. Property names are case-sensitive in dot indexing, as for every other class; MATLAB also reads ts.name for ts.Name. get and set accept any case. See also: tscollection, timeseries2timetable, timetable, tsdata.timemetadata, tsdata.datametadata, tsdata.qualmetadata, tsdata.event, tsdata.interpolation # name: # type: sq_string # elements: 1 # length: 33 Array of samples indexed by time. # name: # type: sq_string # elements: 1 # length: 15 timeseries.Data # name: # type: sq_string # elements: 1 # length: 455 timeseries: property Data The samples. A numeric or logical array of any class, holding one sample per time and read against the time vector as the constructor describes: rows, or slices along the last dimension, with a matrix of as many columns as there are times stored as an Rx1xN array. Any other number of samples than there are times is refused, except while one of the two is empty, which lets a series be built by assigning Time and Data in turn. # name: # type: sq_string # elements: 1 # length: 12 The samples. # name: # type: sq_string # elements: 1 # length: 19 timeseries.DataInfo # name: # type: sq_string # elements: 1 # length: 85 timeseries: property DataInfo The metadata of the data, a tsdata.datametadata object. # name: # type: sq_string # elements: 1 # length: 55 The metadata of the data, a tsdata.datametadata object. # name: # type: sq_string # elements: 1 # length: 17 timeseries.Events # name: # type: sq_string # elements: 1 # length: 127 timeseries: property Events The events of the series. A vector of tsdata.event objects, or [] when there are none, by default. # name: # type: sq_string # elements: 1 # length: 25 The events of the series. # name: # type: sq_string # elements: 1 # length: 22 timeseries.IsTimeFirst # name: # type: sq_string # elements: 1 # length: 515 timeseries: property IsTimeFirst Whether the time vector runs along the first dimension of the data. true when each row of the data is a sample, false when the time vector runs along the last dimension of the data, as the constructor describes. It is kept when samples are selected, so a three-dimensional series cut to one sample holds a matrix with IsTimeFirst still false. Assigning the value it already has is accepted, and assigning the other is refused, since it would misalign the data with the time vector. # name: # type: sq_string # elements: 1 # length: 67 Whether the time vector runs along the first dimension of the data. # name: # type: sq_string # elements: 1 # length: 17 timeseries.Length # name: # type: sq_string # elements: 1 # length: 101 timeseries: property Length The number of samples, which is the length of the time vector. Read-only. # name: # type: sq_string # elements: 1 # length: 62 The number of samples, which is the length of the time vector. # name: # type: sq_string # elements: 1 # length: 15 timeseries.Name # name: # type: sq_string # elements: 1 # length: 221 timeseries: property Name The name of the series. A character vector, 'unnamed' by default, and '' for a series created with no argument. A string scalar is converted; any other value is refused, where MATLAB accepts it. # name: # type: sq_string # elements: 1 # length: 23 The name of the series. # name: # type: sq_string # elements: 1 # length: 18 timeseries.Quality # name: # type: sq_string # elements: 1 # length: 252 timeseries: property Quality A quality code per sample. Integers from -128 to 127, stored as double, [] by default. Either one code per sample, as a vector, or one per data element, as an array the size of Data. Their meaning is listed in QualityInfo. # name: # type: sq_string # elements: 1 # length: 26 A quality code per sample. # name: # type: sq_string # elements: 1 # length: 22 timeseries.QualityInfo # name: # type: sq_string # elements: 1 # length: 100 timeseries: property QualityInfo The description of the quality codes, a tsdata.qualmetadata object. # name: # type: sq_string # elements: 1 # length: 67 The description of the quality codes, a tsdata.qualmetadata object. # name: # type: sq_string # elements: 1 # length: 15 timeseries.Time # name: # type: sq_string # elements: 1 # length: 392 timeseries: property Time The time vector. A column vector of finite, non-decreasing times, in ts.TimeInfo.Units, counted from ts.TimeInfo.StartDate when that is set. Repeated times are allowed. An unsorted time vector is refused on assignment, since sorting it would move samples it was not given with. It is stored as double: MATLAB keeps an integer class, and then rounds new times to it. # name: # type: sq_string # elements: 1 # length: 16 The time vector. # name: # type: sq_string # elements: 1 # length: 19 timeseries.TimeInfo # name: # type: sq_string # elements: 1 # length: 165 timeseries: property TimeInfo The metadata of the time vector, a tsdata.timemetadata object. Its Length, Start, End and Increment follow Time and cannot be assigned. # name: # type: sq_string # elements: 1 # length: 62 The metadata of the time vector, a tsdata.timemetadata object. # name: # type: sq_string # elements: 1 # length: 28 timeseries.TreatNaNasMissing # name: # type: sq_string # elements: 1 # length: 155 timeseries: property TreatNaNasMissing Whether the statistics methods treat NaN as missing data. A logical scalar, true by default; 0 and 1 are converted. # name: # type: sq_string # elements: 1 # length: 57 Whether the statistics methods treat NaN as missing data. # name: # type: sq_string # elements: 1 # length: 19 timeseries.UserData # name: # type: sq_string # elements: 1 # length: 72 timeseries: property UserData Any data the user attaches, [] by default. # name: # type: sq_string # elements: 1 # length: 42 Any data the user attaches, [] by default. # name: # type: sq_string # elements: 1 # length: 19 timeseries.addevent # name: # type: sq_string # elements: 1 # length: 1096 timeseries: ts = addevent (ts, event) timeseries: ts = addevent (ts, name, time) Add events to a series. ts = addevent (ts, event) appends the tsdata.event object or array event to the Events of the series ts, in the order given. ts = addevent (ts, name, time) appends an event named name at time. name is a character vector or a string scalar, and time a real number in the series' units, counted from its TimeInfo.StartDate when it has one; for a series with a start date time may also be a date, as text or a datetime. For several events, name is a cell array of character vectors or a string array, and time a cell array, a numeric vector or a string array of as many times. Events are kept in the order they are added, not sorted; an event whose name and time match one already held is not added again, while the same name at another time is. A date on a series with no start date is refused, since the series has no calendar to place it on; MATLAB accepts it and reads the event as time 0. A NaN time is refused too. See also: timeseries.delevent, tsdata.event, timeseries.gettsatevent # name: # type: sq_string # elements: 1 # length: 23 Add events to a series. # name: # type: sq_string # elements: 1 # length: 20 timeseries.addsample # name: # type: sq_string # elements: 1 # length: 1546 timeseries: ts = addsample (ts, 'Data', data, 'Time', time) timeseries: ts = addsample (..., 'Quality', quality) timeseries: ts = addsample (..., 'OverwriteFlag', tf) timeseries: ts = addsample (ts, S) Add samples to a series. ts = addsample (ts, 'Data', data, 'Time', time) inserts the samples data at the times time, one sample per time, keeping the series in time order; a sample at a time already present goes after the samples there. data is read against time as the constructor reads data against times, and each sample must have the size of the series' samples; a series with no samples takes any. For a series with no TimeInfo.StartDate time is numbers in its units; for one with a start date it may also be dates as text, or a datetime. The class of the data follows concatenation, so an int8 sample makes a double series int8. The option names are matched in any case, and a later occurrence overrides an earlier one. They may also be given as the fields of a structure S. 'Quality' The quality codes of the new samples, one per sample or one per data element, as the series has them. Refused for a series without quality codes. When a series with codes is given none, each new sample takes the code of the sample nearest in time, the later one on a tie. 'OverwriteFlag' When true, a new sample at a time already present replaces the data of every sample there, and their quality codes if 'Quality' is given, instead of being inserted. false by default. MATLAB refuses 0 and 1 here. See also: timeseries.delsample, timeseries.append # name: # type: sq_string # elements: 1 # length: 24 Add samples to a series. # name: # type: sq_string # elements: 1 # length: 17 timeseries.append # name: # type: sq_string # elements: 1 # length: 1103 timeseries: ts = append (ts1, ts2, ...) Join series end to end in time. ts = append (ts1, ts2, ...) returns one series holding the samples of every argument in turn. An argument may be an array of series, taken element by element, and series with no samples are skipped. Each series must start at or after the end of the one before, so equal times where two meet give a repeated time; a gap is kept. Their samples must be of one size; the class of the data follows concatenation. Series with a TimeInfo.StartDate and series without one cannot be mixed. Times are converted to the coarsest units among them, and dates are counted from the first series' start date. Quality codes are joined when every series has them and refused when only some do; MATLAB then keeps too few codes or drops them. The result takes its name, DataInfo.Units and QualityInfo from the series when they all agree, and otherwise 'unnamed', '' and none; MATLAB always names it 'unnamed' and drops QualityInfo. Events are gathered from every series, and every other property is the first series'. See also: timeseries.addsample # name: # type: sq_string # elements: 1 # length: 31 Join series end to end in time. # name: # type: sq_string # elements: 1 # length: 19 timeseries.delevent # name: # type: sq_string # elements: 1 # length: 591 timeseries: ts = delevent (ts, name) timeseries: ts = delevent (ts, name, n) Remove events from a series. ts = delevent (ts, name) removes the first event named name, a character vector or a string scalar matched in case, from the Events of the series ts; for a cell array of names, the first event of each. ts = delevent (ts, name, n) removes the nth event of each name instead, counting only the events of that name in the order they are held. A name no event has, or an n beyond the events of that name, is refused; MATLAB then removes nothing, silently. See also: timeseries.addevent # name: # type: sq_string # elements: 1 # length: 28 Remove events from a series. # name: # type: sq_string # elements: 1 # length: 20 timeseries.delsample # name: # type: sq_string # elements: 1 # length: 885 timeseries: ts = delsample (ts, 'Index', ind) timeseries: ts = delsample (ts, 'Value', time) Remove samples from a series. ts = delsample (ts, 'Index', ind) removes the samples ind, a vector of positive integers not exceeding Length; repeats are allowed and [] removes nothing. A logical mask is refused, as in MATLAB. ts = delsample (ts, 'Value', time) removes every sample at each of the times time; a time no sample has removes nothing. For a series with no TimeInfo.StartDate the times are numbers in its units; for one with a start date they may also be dates as text, or a datetime. The option names are matched in any case. Every property but the samples is kept. MATLAB turns quality codes held per data element of three-dimensional data into a vector here; they lose the slice of each removed sample, as the data does. See also: timeseries.addsample, timeseries.getsamples # name: # type: sq_string # elements: 1 # length: 29 Remove samples from a series. # name: # type: sq_string # elements: 1 # length: 18 timeseries.detrend # name: # type: sq_string # elements: 1 # length: 924 timeseries: ts = detrend (ts, type) timeseries: ts = detrend (ts, type, ind) Remove a polynomial trend from a series. ts = detrend (ts, type) subtracts from each column of the data the polynomial fitted to it by least squares against Time: its mean for 'constant' or 0, a straight line for 'linear' or 1, and a polynomial of that degree for any larger integer. NaN values are left out of the fit and stay NaN. Every other property is kept. ts = detrend (ts, type, ind) detrends only the columns ind of the data, or its rows where IsTimeFirst is false. The data must be double or single, of two dimensions. MATLAB fits against the positions of the samples, closing up the gaps left by NaN, so its line is wrong where the time is not uniform or a value is missing; it also returns a series of fewer than three samples unchanged, where here 'constant' of [1 3] is [-1 1]. See also: timeseries.filter, timeseries.idealfilter # name: # type: sq_string # elements: 1 # length: 40 Remove a polynomial trend from a series. # name: # type: sq_string # elements: 1 # length: 13 timeseries.eq # name: # type: sq_string # elements: 1 # length: 387 timeseries: tf = eq (a, b) True when two series are equal, a == b. tf = eq (a, b) returns a single logical for the whole series, as in MATLAB, not one per sample: true when a and b are series equal in every property but UserData, the class of the data aside, and false when either is not a series. NaN is unequal to itself. See also: timeseries.isequal, timeseries.isequalwithequalnans # name: # type: sq_string # elements: 1 # length: 39 True when two series are equal, a == b. # name: # type: sq_string # elements: 1 # length: 17 timeseries.filter # name: # type: sq_string # elements: 1 # length: 857 timeseries: ts = filter (ts, b, a) timeseries: ts = filter (ts, b, a, ind) Filter a series. ts = filter (ts, b, a) runs each column of the data through the filter of numerator coefficients b and denominator coefficients a, as filter (b, a, x) does, along time from a zero initial state. NaN values are first filled by the interpolation method of the series, so the data must not begin or end with one. Every other property is kept. ts = filter (ts, b, a, ind) filters only the columns ind of the data, or its rows where IsTimeFirst is false. The data must be double or single, and the times uniformly spaced. MATLAB filters data of three dimensions across its rows when ind is not given, and filters a series with irregular times as if they were uniform, though its documentation requires them to be. See also: timeseries.idealfilter, timeseries.detrend # name: # type: sq_string # elements: 1 # length: 16 Filter a series. # name: # type: sq_string # elements: 1 # length: 14 timeseries.get # name: # type: sq_string # elements: 1 # length: 711 timeseries: S = get (ts) timeseries: value = get (ts, name) timeseries: values = get (ts, names) Return property values. S = get (ts) returns a structure of every property of the series ts. value = get (ts, name) returns the property name, a character vector or a string scalar matched in any case. values = get (ts, names) returns a row cell array of the properties named in the cell array names. For an array of series, get with one name returns a cell array the size of the array, with a cell array of names a cell array with a row per series and a column per property, and with no name a cell array with a row per series and a column per property of every property. See also: timeseries.set, timeseries # name: # type: sq_string # elements: 1 # length: 23 Return property values. # name: # type: sq_string # elements: 1 # length: 21 timeseries.getabstime # name: # type: sq_string # elements: 1 # length: 788 timeseries: dates = getabstime (ts) Return the dates of the samples as text. dates = getabstime (ts) returns a column cell array of character vectors, the date of each sample of the series ts: its time, in TimeInfo.Units, counted from TimeInfo.StartDate. They are written in the datestr format TimeInfo.Format, or 'dd-mmm-yyyy HH:MM:SS' when that is empty; seconds are rounded to the millisecond and the rest cut off, as datestr does. A series with no samples gives {}. A series with no TimeInfo.StartDate is refused. MATLAB uses TimeInfo.Format only when it is one of the formats its documentation lists, such as 'dd-mmm-yyyy' or 'mm/dd/yyyy', and writes the default for any other, 'yyyy-mm-dd' included; here every format is used. See also: timeseries.setabstime, tsdata.timemetadata # name: # type: sq_string # elements: 1 # length: 40 Return the dates of the samples as text. # name: # type: sq_string # elements: 1 # length: 25 timeseries.getdatasamples # name: # type: sq_string # elements: 1 # length: 510 timeseries: data = getdatasamples (ts, ind) Return the data of selected samples. data = getdatasamples (ts, ind) returns the data of the samples ind of the series ts, in the order given, repeats included: the rows ind of Data for a series whose time runs down its first dimension, the slices ind along the last dimension otherwise. ind is a vector of positive integers not exceeding Length, or a logical mask no longer than Length; [] returns []. See also: timeseries.getsamples, timeseries.getdatasamplesize # name: # type: sq_string # elements: 1 # length: 36 Return the data of selected samples. # name: # type: sq_string # elements: 1 # length: 28 timeseries.getdatasamplesize # name: # type: sq_string # elements: 1 # length: 379 timeseries: sz = getdatasamplesize (ts) Return the size of one sample. sz = getdatasamplesize (ts) returns the size of each sample of the series ts: [1 C] for data of C columns with a sample per row, and the size of a slice for data whose time runs along the last dimension, so [2 3] for a 2x3x4 array. It is [] for a series with no samples. See also: timeseries.getdatasamples # name: # type: sq_string # elements: 1 # length: 30 Return the size of one sample. # name: # type: sq_string # elements: 1 # length: 26 timeseries.getinterpmethod # name: # type: sq_string # elements: 1 # length: 255 timeseries: name = getinterpmethod (ts) Return the name of the interpolation method. name = getinterpmethod (ts) returns 'linear', 'zoh' or 'myFuncHandle', the Name of ts.DataInfo.Interpolation. See also: timeseries.setinterpmethod, tsdata.interpolation # name: # type: sq_string # elements: 1 # length: 44 Return the name of the interpolation method. # name: # type: sq_string # elements: 1 # length: 25 timeseries.getqualitydesc # name: # type: sq_string # elements: 1 # length: 389 timeseries: desc = getqualitydesc (ts) Return the description of each quality code. desc = getqualitydesc (ts) returns a cell array the size of ts.Quality holding, for each code, its description in ts.QualityInfo: the element of Description at the position of the code in Code. A series without quality codes gives {}. A code with no description is refused. See also: tsdata.qualmetadata # name: # type: sq_string # elements: 1 # length: 44 Return the description of each quality code. # name: # type: sq_string # elements: 1 # length: 21 timeseries.getsamples # name: # type: sq_string # elements: 1 # length: 594 timeseries: ts2 = getsamples (ts, ind) Return a series of selected samples. ts2 = getsamples (ts, ind) returns the series ts holding only the samples ind, which are taken in time order whatever order they are given in, repeats included. ind is as for getdatasamples. Their times and quality codes go with them, and every other property is kept. MATLAB names the result 'unnamed', and for an empty ind returns timeseries () with every property reset; here the name and properties are kept in both cases. See also: timeseries.getdatasamples, timeseries.getsampleusingtime, timeseries.delsample # name: # type: sq_string # elements: 1 # length: 36 Return a series of selected samples. # name: # type: sq_string # elements: 1 # length: 29 timeseries.getsampleusingtime # name: # type: sq_string # elements: 1 # length: 991 timeseries: ts2 = getsampleusingtime (ts, t) timeseries: ts2 = getsampleusingtime (ts, t, 'AllowDuplicateTimes', tf) timeseries: ts2 = getsampleusingtime (ts, t1, t2) Return a series of the samples at a time or between two times. ts2 = getsampleusingtime (ts, t) returns the series ts holding only the sample at time t. When several samples share that time the call is refused, unless 'AllowDuplicateTimes' is true, which returns them all. MATLAB returns them all for false too. ts2 = getsampleusingtime (ts, t1, t2) returns the samples from t1 to t2, both included. For a series with no TimeInfo.StartDate the times are numbers in its units. For a series with one they are dates: text, a datenum, or a datetime, which MATLAB does not accept. No sample in range, NaN, or t1 after t2 give a series with no samples. Every property but the samples is kept; MATLAB names the result 'unnamed' and, when it is empty, resets every property. See also: timeseries.getsamples, timeseries.delsample # name: # type: sq_string # elements: 1 # length: 62 Return a series of the samples at a time or between two times. # name: # type: sq_string # elements: 1 # length: 28 timeseries.gettsafteratevent # name: # type: sq_string # elements: 1 # length: 1039 timeseries: ts2 = gettsafteratevent (ts, event) timeseries: ts2 = gettsafteratevent (ts, event, n) Return a series of the samples at and after the event. ts2 = gettsafteratevent (ts, event) returns the series ts holding only the samples whose time is at or after the time of event, compared exactly. event is the name of an event of ts, a character vector or a string scalar matched in case, which selects the first event of that name; or a tsdata.event object, which need not belong to ts. ts2 = gettsafteratevent (ts, event, n) uses the nth event named event, counting only the events of that name in the order they are held. For a tsdata.event object n can only be 1. A name no event has, or an n beyond the events of that name, is refused; MATLAB then returns an empty series. So is a dated event on a series with no start date, which MATLAB reads as time 0. See also: timeseries.gettsbeforeevent, timeseries.gettsbeforeatevent, timeseries.gettsatevent, timeseries.gettsafterevent, timeseries.gettsbetweenevents, timeseries.addevent # name: # type: sq_string # elements: 1 # length: 54 Return a series of the samples at and after the event. # name: # type: sq_string # elements: 1 # length: 26 timeseries.gettsafterevent # name: # type: sq_string # elements: 1 # length: 1029 timeseries: ts2 = gettsafterevent (ts, event) timeseries: ts2 = gettsafterevent (ts, event, n) Return a series of the samples after the event. ts2 = gettsafterevent (ts, event) returns the series ts holding only the samples whose time is strictly after the time of event, compared exactly. event is the name of an event of ts, a character vector or a string scalar matched in case, which selects the first event of that name; or a tsdata.event object, which need not belong to ts. ts2 = gettsafterevent (ts, event, n) uses the nth event named event, counting only the events of that name in the order they are held. For a tsdata.event object n can only be 1. A name no event has, or an n beyond the events of that name, is refused; MATLAB then returns an empty series. So is a dated event on a series with no start date, which MATLAB reads as time 0. See also: timeseries.gettsbeforeevent, timeseries.gettsbeforeatevent, timeseries.gettsatevent, timeseries.gettsafteratevent, timeseries.gettsbetweenevents, timeseries.addevent # name: # type: sq_string # elements: 1 # length: 47 Return a series of the samples after the event. # name: # type: sq_string # elements: 1 # length: 23 timeseries.gettsatevent # name: # type: sq_string # elements: 1 # length: 1005 timeseries: ts2 = gettsatevent (ts, event) timeseries: ts2 = gettsatevent (ts, event, n) Return a series of the samples at the event. ts2 = gettsatevent (ts, event) returns the series ts holding only the samples whose time is at the time of event, compared exactly. event is the name of an event of ts, a character vector or a string scalar matched in case, which selects the first event of that name; or a tsdata.event object, which need not belong to ts. ts2 = gettsatevent (ts, event, n) uses the nth event named event, counting only the events of that name in the order they are held. For a tsdata.event object n can only be 1. A name no event has, or an n beyond the events of that name, is refused; MATLAB then returns an empty series. So is a dated event on a series with no start date, which MATLAB reads as time 0. See also: timeseries.gettsbeforeevent, timeseries.gettsbeforeatevent, timeseries.gettsafteratevent, timeseries.gettsafterevent, timeseries.gettsbetweenevents, timeseries.addevent # name: # type: sq_string # elements: 1 # length: 44 Return a series of the samples at the event. # name: # type: sq_string # elements: 1 # length: 29 timeseries.gettsbeforeatevent # name: # type: sq_string # elements: 1 # length: 1044 timeseries: ts2 = gettsbeforeatevent (ts, event) timeseries: ts2 = gettsbeforeatevent (ts, event, n) Return a series of the samples before and at the event. ts2 = gettsbeforeatevent (ts, event) returns the series ts holding only the samples whose time is before or at the time of event, compared exactly. event is the name of an event of ts, a character vector or a string scalar matched in case, which selects the first event of that name; or a tsdata.event object, which need not belong to ts. ts2 = gettsbeforeatevent (ts, event, n) uses the nth event named event, counting only the events of that name in the order they are held. For a tsdata.event object n can only be 1. A name no event has, or an n beyond the events of that name, is refused; MATLAB then returns an empty series. So is a dated event on a series with no start date, which MATLAB reads as time 0. See also: timeseries.gettsbeforeevent, timeseries.gettsatevent, timeseries.gettsafteratevent, timeseries.gettsafterevent, timeseries.gettsbetweenevents, timeseries.addevent # name: # type: sq_string # elements: 1 # length: 55 Return a series of the samples before and at the event. # name: # type: sq_string # elements: 1 # length: 27 timeseries.gettsbeforeevent # name: # type: sq_string # elements: 1 # length: 1034 timeseries: ts2 = gettsbeforeevent (ts, event) timeseries: ts2 = gettsbeforeevent (ts, event, n) Return a series of the samples before the event. ts2 = gettsbeforeevent (ts, event) returns the series ts holding only the samples whose time is strictly before the time of event, compared exactly. event is the name of an event of ts, a character vector or a string scalar matched in case, which selects the first event of that name; or a tsdata.event object, which need not belong to ts. ts2 = gettsbeforeevent (ts, event, n) uses the nth event named event, counting only the events of that name in the order they are held. For a tsdata.event object n can only be 1. A name no event has, or an n beyond the events of that name, is refused; MATLAB then returns an empty series. So is a dated event on a series with no start date, which MATLAB reads as time 0. See also: timeseries.gettsbeforeatevent, timeseries.gettsatevent, timeseries.gettsafteratevent, timeseries.gettsafterevent, timeseries.gettsbetweenevents, timeseries.addevent # name: # type: sq_string # elements: 1 # length: 48 Return a series of the samples before the event. # name: # type: sq_string # elements: 1 # length: 29 timeseries.gettsbetweenevents # name: # type: sq_string # elements: 1 # length: 780 timeseries: ts2 = gettsbetweenevents (ts, event1, event2) timeseries: ts2 = gettsbetweenevents (ts, event1, event2, n1, n2) Return a series of the samples between two events. ts2 = gettsbetweenevents (ts, event1, event2) returns the series ts holding only the samples from the time of event1 to the time of event2, both included; none when event2 comes before event1. Each event is a name or a tsdata.event object, as for gettsafterevent. ts2 = gettsbetweenevents (ts, event1, event2, n1, n2) uses the n1th event named event1 and the n2th named event2; for an event given as a tsdata.event object its number can only be 1. Names and numbers no event has are refused, as for gettsafterevent. See also: timeseries.gettsafterevent, timeseries.gettsbeforeevent, timeseries.addevent # name: # type: sq_string # elements: 1 # length: 50 Return a series of the samples between two events. # name: # type: sq_string # elements: 1 # length: 28 timeseries.hasduplicatetimes # name: # type: sq_string # elements: 1 # length: 367 timeseries: tf = hasduplicatetimes (ts) True where a series repeats a time. tf = hasduplicatetimes (ts) returns true when two samples of the series ts share a time. For an array of series it returns a logical array of its size, one answer per series; MATLAB returns a single answer that can miss a series with repeated times. See also: timeseries.getsampleusingtime # name: # type: sq_string # elements: 1 # length: 35 True where a series repeats a time. # name: # type: sq_string # elements: 1 # length: 22 timeseries.idealfilter # name: # type: sq_string # elements: 1 # length: 1244 timeseries: ts = idealfilter (ts, intervals, type) timeseries: ts = idealfilter (ts, intervals, type, ind) Filter a series with an ideal filter. ts = idealfilter (ts, intervals, type) removes the mean of each column of the data, then keeps the frequencies in intervals (type 'pass') or removes them ('notch') through the discrete Fourier transform. The mean is not added back. intervals holds one frequency interval per row, [lo, hi], in cycles per unit of TimeInfo.Units; a frequency f is in it when lo < f <= hi. NaN values are first filled by the interpolation method of the series, so the data must not begin or end with one. A series with irregular times is resampled first, by its interpolation method, onto as many uniformly spaced times from its first time to its last, which it keeps. Every other property is kept. ts = idealfilter (ts, intervals, type, ind) filters only the columns ind of the data, or its rows where IsTimeFirst is false. The data must be double or single, of two dimensions. MATLAB returns complex NaN throughout for data that begins or ends with NaN, keeps the mean of a series of fewer than three samples, and reads only the first and last columns of intervals. See also: timeseries.filter, timeseries.detrend # name: # type: sq_string # elements: 1 # length: 37 Filter a series with an ideal filter. # name: # type: sq_string # elements: 1 # length: 14 timeseries.iqr # name: # type: sq_string # elements: 1 # length: 544 timeseries: v = iqr (ts) timeseries: v = iqr (ts, name, value, ...) Return the interquartile range of a series over time. v = iqr (ts) returns the interquartile range of each data element over time, the quartiles placed by the (k - 0.5)/n rule, so v has the size of one sample; with 'time' weighting the cumulative weight takes the place of k. v is double whatever the class of the data, and [] for a series with no samples. The options, and where MATLAB differs, are given under timeseries.mean. See also: timeseries.median, timeseries.mean # name: # type: sq_string # elements: 1 # length: 53 Return the interquartile range of a series over time. # name: # type: sq_string # elements: 1 # length: 18 timeseries.isequal # name: # type: sq_string # elements: 1 # length: 243 timeseries: tf = isequal (a, b) True when two series are equal. tf = isequal (a, b) is a == b: every property but UserData compared, the class of the data aside, NaN unequal to itself. See also: timeseries.eq, timeseries.isequalwithequalnans # name: # type: sq_string # elements: 1 # length: 31 True when two series are equal. # name: # type: sq_string # elements: 1 # length: 31 timeseries.isequalwithequalnans # name: # type: sq_string # elements: 1 # length: 228 timeseries: tf = isequalwithequalnans (a, b) True when two series are equal, NaN equal to itself. tf = isequalwithequalnans (a, b) is isequal (a, b) with NaN counted as equal to NaN. See also: timeseries.isequal, timeseries.eq # name: # type: sq_string # elements: 1 # length: 52 True when two series are equal, NaN equal to itself. # name: # type: sq_string # elements: 1 # length: 18 timeseries.ldivide # name: # type: sq_string # elements: 1 # length: 487 timeseries: ts = ldivide (a, b) The element-wise left quotient, a .\ b. ts = ldivide (a, b) returns the element-wise quotient of b by a, sample by sample. One of them may be a number: a scalar, or an array the size of one sample. The rules for two series are given under timeseries.plus, save that DataInfo.Units is dropped, since a product or quotient of units is the unit of neither; MATLAB keeps it where both series have the same. See also: timeseries.rdivide, timeseries.mldivide # name: # type: sq_string # elements: 1 # length: 39 The element-wise left quotient, a .\ b. # name: # type: sq_string # elements: 1 # length: 14 timeseries.max # name: # type: sq_string # elements: 1 # length: 547 timeseries: v = max (ts) timeseries: v = max (ts, name, value, ...) Return the maximum of a series over time. v = max (ts) returns the largest value of each data element over time, so v has the size of one sample; weighting leaves it unchanged. v keeps the class of the data, and is [] for a series with no samples. Where a value is left without samples it is NaN, and v is then double, as integer and logical classes hold no NaN. The options, and where MATLAB differs, are given under timeseries.mean. See also: timeseries.min, timeseries.mean # name: # type: sq_string # elements: 1 # length: 41 Return the maximum of a series over time. # name: # type: sq_string # elements: 1 # length: 15 timeseries.mean # name: # type: sq_string # elements: 1 # length: 1664 timeseries: v = mean (ts) timeseries: v = mean (ts, name, value, ...) Return the mean of a series over time. v = mean (ts) returns the mean of each data element over time, so v has the size of one sample; with 'time' weighting it is the time-weighted mean. v is double whatever the class of the data, and [] for a series with no samples. v = mean (ts, name, value, ...) takes these options, which all nine statistics share, the names matched in any case: 'Quality' the quality codes of samples to leave out, as missing; a value left without samples is NaN, a sum 0. A code no sample has leaves nothing out, with a warning. 'MissingData' 'remove', the default, leaves missing values out; 'interpolate' fills those between samples linearly in time first. 'Weighting' 'none', the default, or 'time', which weights each sample by the time it covers: an interior sample by the mean of its two intervals, an end sample by its one interval, the weights scaled to a mean of 1, so that uniform time changes nothing. NaN is missing while TreatNaNasMissing is true; when it is false, mean, median, std, var and sum give NaN where the data holds one, while min, max, mode and iqr still skip it, as in MATLAB. MATLAB applies 'time' weighting by multiplying the data by the weights and taking the ordinary statistic of the product, which is right for mean and sum only: there the minimum of [1 2 4 8 16] at times [0 1 3 7 8] is 0.5556. Here each statistic is the time-weighted one: 1 for that minimum. MATLAB also fails on logical data, and in std, var and iqr on integer data, which here are computed in double. See also: timeseries.median, timeseries.std, timeseries.sum # name: # type: sq_string # elements: 1 # length: 38 Return the mean of a series over time. # name: # type: sq_string # elements: 1 # length: 17 timeseries.median # name: # type: sq_string # elements: 1 # length: 588 timeseries: v = median (ts) timeseries: v = median (ts, name, value, ...) Return the median of a series over time. v = median (ts) returns the median of each data element over time, so v has the size of one sample; with 'time' weighting it is the time-weighted median. v keeps the class of the data, and is [] for a series with no samples. Where a value is left without samples it is NaN, and v is then double, as integer and logical classes hold no NaN. The options, and where MATLAB differs, are given under timeseries.mean. See also: timeseries.mean, timeseries.mode, timeseries.iqr # name: # type: sq_string # elements: 1 # length: 40 Return the median of a series over time. # name: # type: sq_string # elements: 1 # length: 14 timeseries.min # name: # type: sq_string # elements: 1 # length: 548 timeseries: v = min (ts) timeseries: v = min (ts, name, value, ...) Return the minimum of a series over time. v = min (ts) returns the smallest value of each data element over time, so v has the size of one sample; weighting leaves it unchanged. v keeps the class of the data, and is [] for a series with no samples. Where a value is left without samples it is NaN, and v is then double, as integer and logical classes hold no NaN. The options, and where MATLAB differs, are given under timeseries.mean. See also: timeseries.max, timeseries.mean # name: # type: sq_string # elements: 1 # length: 41 Return the minimum of a series over time. # name: # type: sq_string # elements: 1 # length: 16 timeseries.minus # name: # type: sq_string # elements: 1 # length: 295 timeseries: ts = minus (a, b) The difference of series, a - b. ts = minus (a, b) returns the difference of a and b, sample by sample. One of them may be a number: a scalar, or an array the size of one sample. The rules for two series are given under timeseries.plus. See also: timeseries.plus # name: # type: sq_string # elements: 1 # length: 32 The difference of series, a - b. # name: # type: sq_string # elements: 1 # length: 19 timeseries.mldivide # name: # type: sq_string # elements: 1 # length: 532 timeseries: ts = mldivide (a, b) The matrix left division of series, a \ b. ts = mldivide (a, b) returns the matrix left division of b by a, sample by sample; each result keeps the size of one sample. One of them may be a number whose size fits the division with one sample. The rules for two series are given under timeseries.plus, save that DataInfo.Units is dropped, since a product or quotient of units is the unit of neither; MATLAB keeps it where both series have the same. See also: timeseries.mrdivide, timeseries.ldivide # name: # type: sq_string # elements: 1 # length: 42 The matrix left division of series, a \ b. # name: # type: sq_string # elements: 1 # length: 15 timeseries.mode # name: # type: sq_string # elements: 1 # length: 608 timeseries: v = mode (ts) timeseries: v = mode (ts, name, value, ...) Return the mode of a series over time. v = mode (ts) returns the most frequent value of each data element over time, the smallest on a tie, so v has the size of one sample; with 'time' weighting it is the value of most total weight. v keeps the class of the data, and is [] for a series with no samples. Where a value is left without samples it is NaN, and v is then double, as integer and logical classes hold no NaN. The options, and where MATLAB differs, are given under timeseries.mean. See also: timeseries.mean, timeseries.median # name: # type: sq_string # elements: 1 # length: 38 Return the mode of a series over time. # name: # type: sq_string # elements: 1 # length: 19 timeseries.mrdivide # name: # type: sq_string # elements: 1 # length: 649 timeseries: ts = mrdivide (a, b) The matrix right division of series, a / b. ts = mrdivide (a, b) returns the matrix right division of a by b, sample by sample; each result keeps the size of one sample. One of them may be a number whose size fits the division with one sample. The rules for two series are given under timeseries.plus, save that DataInfo.Units is dropped, since a product or quotient of units is the unit of neither; MATLAB keeps it where both series have the same. A number divided by a series, 2 / ts, divides each sample; MATLAB divides by the whole data column as one matrix. See also: timeseries.mldivide, timeseries.rdivide # name: # type: sq_string # elements: 1 # length: 43 The matrix right division of series, a / b. # name: # type: sq_string # elements: 1 # length: 17 timeseries.mtimes # name: # type: sq_string # elements: 1 # length: 468 timeseries: ts = mtimes (a, b) The matrix product of series, a * b. ts = mtimes (a, b) returns the matrix product of a and b, sample by sample. One of them may be a number whose size fits the product with one sample. The rules for two series are given under timeseries.plus, save that DataInfo.Units is dropped, since a product or quotient of units is the unit of neither; MATLAB keeps it where both series have the same. See also: timeseries.times, timeseries.plus # name: # type: sq_string # elements: 1 # length: 36 The matrix product of series, a * b. # name: # type: sq_string # elements: 1 # length: 15 timeseries.plus # name: # type: sq_string # elements: 1 # length: 848 timeseries: ts = plus (a, b) The sum of series, a + b. ts = plus (a, b) returns the sum of a and b, sample by sample. One of them may be a number: a scalar, or an array the size of one sample. Two series must hold as many samples, of sizes the operator accepts, at times in the same units. Two dated series must be at the same instants; two undated ones may also differ by a constant offset, and the result then takes the times of a, with a warning. A dated series and an undated one are refused. The result is named 'unnamed' and takes its other properties from a, with the events of both, no quality codes and linear interpolation; DataInfo.Units is kept where both series have the same. With a number, every property of the series is kept. The data is double whatever its class. See also: timeseries.minus, timeseries.times, timeseries.mtimes # name: # type: sq_string # elements: 1 # length: 25 The sum of series, a + b. # name: # type: sq_string # elements: 1 # length: 18 timeseries.rdivide # name: # type: sq_string # elements: 1 # length: 493 timeseries: ts = rdivide (a, b) The element-wise quotient of series, a ./ b. ts = rdivide (a, b) returns the element-wise quotient of a and b, sample by sample. One of them may be a number: a scalar, or an array the size of one sample. The rules for two series are given under timeseries.plus, save that DataInfo.Units is dropped, since a product or quotient of units is the unit of neither; MATLAB keeps it where both series have the same. See also: timeseries.ldivide, timeseries.mrdivide # name: # type: sq_string # elements: 1 # length: 44 The element-wise quotient of series, a ./ b. # name: # type: sq_string # elements: 1 # length: 19 timeseries.resample # name: # type: sq_string # elements: 1 # length: 1449 timeseries: ts = resample (ts, time) timeseries: ts = resample (ts, time, method) timeseries: ts = resample (ts, time, method, code) Evaluate a series at new times. ts = resample (ts, time) returns the series ts with its samples replaced by its values at time, found with its own interpolation method. time is numeric, in the series' units; for a series with a TimeInfo.StartDate it may also be dates, as text or a datetime, and numbers count from the start date. The new times are sorted, repeats kept. Samples that are NaN are left out before interpolating, as in MATLAB; at a time outside the series the data is NaN. The class of the data is kept. Every property but the samples is kept. ts = resample (ts, time, method) uses method, 'linear' or 'zoh' in any case, instead; [] keeps the series' own. ts = resample (ts, time, method, code) gives the quality code code, an integer listed in QualityInfo.Code, to every new time the series did not already hold. [] gives none. A series with quality codes gives each other new time the code of the nearest sample, a tie going to the later, as MATLAB does; under 'zoh' a value takes the code of the sample it is held from, where MATLAB takes the nearest one's, which may be the next sample's. Outside the series a code is the nearest sample's too, where MATLAB raises for a series with codes. Custom methods are called on the data only. See also: timeseries.synchronize, timeseries.setinterpmethod # name: # type: sq_string # elements: 1 # length: 31 Evaluate a series at new times. # name: # type: sq_string # elements: 1 # length: 14 timeseries.set # name: # type: sq_string # elements: 1 # length: 758 timeseries: set (ts, name, value) timeseries: set (ts, name1, value1, ...) timeseries: ts2 = set (ts, name, value, ...) timeseries: S = set (ts) Set property values. set (ts, name, value, ...) sets each property name, matched in any case, to its value, in the order given, in the variable ts of the caller. ts must therefore be a variable, not an expression such as an indexed element or tc.Series; set those with dot assignment. For an array of series every element is set. ts2 = set (ts, name, value, ...) returns the modified series and leaves ts as it was, so ts may then be any expression. S = set (ts) returns what get (ts) does. A name with no value is refused; MATLAB returns the current value quoted as text. See also: timeseries.get, timeseries # name: # type: sq_string # elements: 1 # length: 20 Set property values. # name: # type: sq_string # elements: 1 # length: 21 timeseries.setabstime # name: # type: sq_string # elements: 1 # length: 2024 timeseries: ts = setabstime (ts, dates) timeseries: ts = setabstime (ts, dates, format) Set the time vector of a series from dates. ts = setabstime (ts, dates) gives the samples of the series ts the dates dates, one per sample, in order: a cell array of character vectors, a string array, a character matrix of one date per row, a character vector for a single sample, or a datetime array. The earliest date becomes TimeInfo.StartDate, stored as 'dd-mmm-yyyy HH:MM:SS' with milliseconds as '.FFF' when the seconds are not whole, and Time holds each date's offset from it in the series' own TimeInfo.Units, which are kept: a series in seconds given one date a day gets times [0 86400 172800 ...]. Each date is read by its own format. Dates given as times of day alone, such as '13:00:00', give times since midnight and no start date, as in MATLAB. ts = setabstime (ts, dates, format) reads every date strictly in the datestr format format. A date not written in format is refused. The start date is stored as above whatever the format, where MATLAB writes it in format, from which every later reading has to guess it again: 'dd/mm/yyyy' then comes back as a date of another month. The dates need not be sorted: the samples, with their quality codes, are sorted with them, keeping the order of samples at equal dates, as the constructor sorts them. MATLAB differs in four ways. It does not move the samples with unsorted dates, so each is given another sample's date. It reads every date by the format of the first, so {'01-Jan-2024', '2024-01-02'} gives a date in year 7. It forces text into format when it does not fit, so 'yyyy-mm-dd' on '01-Jan-2024' gives a date in year 6. And it takes datenum values, datetime values and plain numbers as relative seconds, dropping the dates; here numbers are refused, since a datenum cannot be told from a plain number, and a datetime is read as its dates. Convert a datenum x with datetime (x, 'ConvertFrom', 'datenum'). See also: timeseries.getabstime, timeseries.setuniformtime # name: # type: sq_string # elements: 1 # length: 43 Set the time vector of a series from dates. # name: # type: sq_string # elements: 1 # length: 26 timeseries.setinterpmethod # name: # type: sq_string # elements: 1 # length: 916 timeseries: ts = setinterpmethod (ts, name) timeseries: ts = setinterpmethod (ts, fh) timeseries: ts = setinterpmethod (ts, ip) Set the interpolation method. ts = setinterpmethod (ts, name) sets the method resample and synchronize use to name, 'linear' or 'zoh' (zero-order hold, keeping each value until the next sample), matched in any case and stored in lower case. MATLAB stores any name, 'cubic' included, and fails only on resampling; here another name is refused. ts = setinterpmethod (ts, fh) sets a method evaluated by the function handle fh, called as MATLAB calls it: newData = fh (newTime, oldTime, oldData), with the times as columns and the data with time along its first dimension, so three-dimensional data is passed permuted. ts = setinterpmethod (ts, ip) sets the method held by the tsdata.interpolation object ip. See also: timeseries.getinterpmethod, timeseries.resample, tsdata.interpolation # name: # type: sq_string # elements: 1 # length: 29 Set the interpolation method. # name: # type: sq_string # elements: 1 # length: 25 timeseries.setuniformtime # name: # type: sq_string # elements: 1 # length: 1387 timeseries: ts = setuniformtime (ts, 'StartTime', start) timeseries: ts = setuniformtime (ts, 'Interval', step) timeseries: ts = setuniformtime (ts, 'EndTime', end) timeseries: ts = setuniformtime (ts, name1, value1, name2, value2, ...) Give a series a uniform time vector. ts = setuniformtime (ts, ...) replaces the time vector of the series ts by one of evenly spaced times, one per sample, in its own units; the samples are not moved. The times are fixed by any one or two of these options, or all three when they agree, the names matched in any case and a later occurrence overriding an earlier one: Given Times 'StartTime' from start, a step of 1 'Interval' from 0, a step of step 'EndTime' from 0 to end 'StartTime', 'Interval' from start, a step of step 'StartTime', 'EndTime' from start to end, both exact 'Interval', 'EndTime' to end, a step of step Each value is a finite real scalar in the series' units, relative to TimeInfo.StartDate when the series has one. A step of zero gives equal times. A negative step, a start after the end, or an end below zero given alone are refused, since the times would run backwards; MATLAB then empties the time vector and keeps the data. MATLAB also ignores NaN, which is refused here, and a series of one sample given equal start and end times, which MATLAB refuses, gets that time. See also: timeseries.setabstime, tsdata.timemetadata # name: # type: sq_string # elements: 1 # length: 36 Give a series a uniform time vector. # name: # type: sq_string # elements: 1 # length: 14 timeseries.std # name: # type: sq_string # elements: 1 # length: 510 timeseries: v = std (ts) timeseries: v = std (ts, name, value, ...) Return the standard deviation of a series over time. v = std (ts) returns the standard deviation of each data element over time, normalised by N-1, so v has the size of one sample; with 'time' weighting it is taken about the time-weighted mean. v is double whatever the class of the data, and [] for a series with no samples. The options, and where MATLAB differs, are given under timeseries.mean. See also: timeseries.mean, timeseries.var # name: # type: sq_string # elements: 1 # length: 52 Return the standard deviation of a series over time. # name: # type: sq_string # elements: 1 # length: 14 timeseries.sum # name: # type: sq_string # elements: 1 # length: 442 timeseries: v = sum (ts) timeseries: v = sum (ts, name, value, ...) Return the sum of a series over time. v = sum (ts) returns the sum of each data element over time, so v has the size of one sample; with 'time' weighting it is the sum of the weighted samples. v is double whatever the class of the data, and [] for a series with no samples. The options, and where MATLAB differs, are given under timeseries.mean. See also: timeseries.mean # name: # type: sq_string # elements: 1 # length: 37 Return the sum of a series over time. # name: # type: sq_string # elements: 1 # length: 22 timeseries.synchronize # name: # type: sq_string # elements: 1 # length: 1751 timeseries: [ts1, ts2] = synchronize (ts1, ts2, method) timeseries: [ts1, ts2] = synchronize (..., name, value) Resample two series onto common times. [ts1, ts2] = synchronize (ts1, ts2, method) resamples both series onto one set of times within the span both cover, chosen by method, matched in any case: 'union' every time of either series in that span; 'intersection' the times both series hold, within the tolerance; 'uniform' times from the start of the span, a step of 'Interval' apart. Series that do not overlap become series with no samples. Each keeps its own units, the times converted exactly; MATLAB converts them with errors near the tenth digit. Dated series are placed by their start dates, and both results count from the earlier of the two unless 'KeepOriginalTimes' is true. A dated series with an undated one is refused, since the undated one has no calendar; MATLAB then reads both as undated. The options, matched in any case: 'Interval' the step of 'uniform', a positive number in the first series' units, 1 by default; MATLAB accepts zero and negative steps and gives one sample or none. 'InterpMethod' 'linear' or 'zoh', for both series; by default each uses its own. 'QualityCode' a quality code given to the times each series did not already hold, as for resample. 'KeepOriginalTimes' true to keep each dated series on its own start date. 'tolerance' how close two times must be to count as one, in the first series' units, 1e-10 by default; the second series' time is kept. This is the documented meaning; MATLAB ignores any tolerance above about 1e-10, so there 0.1 matches no times 0.05 apart, while here it matches them. Quality codes follow the rules of resample. See also: timeseries.resample, timeseries.append # name: # type: sq_string # elements: 1 # length: 38 Resample two series onto common times. # name: # type: sq_string # elements: 1 # length: 16 timeseries.times # name: # type: sq_string # elements: 1 # length: 482 timeseries: ts = times (a, b) The element-wise product of series, a .* b. ts = times (a, b) returns the element-wise product of a and b, sample by sample. One of them may be a number: a scalar, or an array the size of one sample. The rules for two series are given under timeseries.plus, save that DataInfo.Units is dropped, since a product or quotient of units is the unit of neither; MATLAB keeps it where both series have the same. See also: timeseries.mtimes, timeseries.plus # name: # type: sq_string # elements: 1 # length: 43 The element-wise product of series, a .* b. # name: # type: sq_string # elements: 1 # length: 21 timeseries.timeseries # name: # type: sq_string # elements: 1 # length: 2727 timeseries: ts = timeseries () timeseries: ts = timeseries (name) timeseries: ts = timeseries (data) timeseries: ts = timeseries (data, time) timeseries: ts = timeseries (data, time, quality) timeseries: ts = timeseries (..., 'Name', name) Create a timeseries object. ts = timeseries () returns an empty series with no name. ts = timeseries (name) returns an empty series named name, a character vector or a string scalar. MATLAB takes a string scalar here as the data of a one-sample series. ts = timeseries (data) returns a series of the samples in data, a numeric or logical array, at times 0, 1, 2, ... seconds. For data of two dimensions or fewer each row is a sample; for data of three dimensions or more each slice along the last dimension is one. A row vector of more than one element is taken as that many scalar samples and stored as a 1x1xN array. ts = timeseries (data, time) takes the samples at time, which is one of: a numeric vector of one time per sample, in seconds; a cell array of character vectors, a string array or a datetime array of dates, one per sample, which gives Time in days from the earliest of them, with that date as TimeInfo.StartDate; each date is read by its own format, a time zone is dropped and the clock time kept, and NaT is refused; a duration array, relative times in the units its display format names: 's', 'm', 'h' and 'd' give seconds, minutes, hours and days, any other format seconds; a cell array of numeric scalars, read as a numeric vector; [], for the default times. With N times, data of three dimensions or more must have N slices along its last dimension. A matrix with N rows has a sample per row; otherwise one with N columns is stored as an Rx1xN array, a sample per column, so a row vector of N elements gives N scalar samples; and with a single time the whole matrix is one sample. The times need not be sorted: the samples are sorted with them, keeping the order of samples at equal times. They are stored as double whatever their class. MATLAB accepts neither datetime nor duration arrays here. ts = timeseries (data, time, quality) also sets a quality code per sample: integers from -128 to 127, as a vector of one per sample or an array the size of data as stored, or [] for none. A matrix stored a sample per column, as an Rx1xN array, takes codes per element in that shape too; MATLAB refuses them in the matrix's own RxN shape, as here. ts = timeseries (..., 'Name', name) names the series; the option name is matched in any case, and a later occurrence overrides an earlier one. A name that is not text is refused, where MATLAB ignores it. ts = timeseries (ts0) returns the series ts0 itself. See also: timetable, istimeseries, tsdata.timemetadata # name: # type: sq_string # elements: 1 # length: 27 Create a timeseries object. # name: # type: sq_string # elements: 1 # length: 14 timeseries.var # name: # type: sq_string # elements: 1 # length: 490 timeseries: v = var (ts) timeseries: v = var (ts, name, value, ...) Return the variance of a series over time. v = var (ts) returns the variance of each data element over time, normalised by N-1, so v has the size of one sample; with 'time' weighting it is taken about the time-weighted mean. v is double whatever the class of the data, and [] for a series with no samples. The options, and where MATLAB differs, are given under timeseries.mean. See also: timeseries.mean, timeseries.std # name: # type: sq_string # elements: 1 # length: 42 Return the variance of a series over time. # name: # type: sq_string # elements: 1 # length: 20 timeseries2timetable # name: # type: sq_string # elements: 1 # length: 2495 datatypes: TT = timeseries2timetable (ts) datatypes: TT = timeseries2timetable (ts1, ..., tsN) datatypes: TT = timeseries2timetable (tc) Convert time series to a timetable. TT = timeseries2timetable (ts) returns a timetable with a variable for each series in the timeseries array ts, and TT = timeseries2timetable (ts1, ..., tsN) one for each of the series ts1 to tsN, which must then each be a single series. A tscollection tc may stand for any of them and gives a variable for each of its members, in order; MATLAB accepts timeseries alone. Every series must have samples, and all the same times, in the same units, with the same TimeInfo.StartDate; convert series on other times one by one and combine the timetables with synchronize. The row times are the time vector: a duration array for series without a start date, displayed in the units of the series (seconds and finer units in seconds, weeks in days), and a datetime array from the start date otherwise, in the format TimeInfo.Format gives, or 'dd-MMM-uuuu HH:mm:ss' when it is empty. MATLAB copies TimeInfo.Format, a datestr format, as it is and fails; here it is translated to the datetime format that writes the same text, but for an hour on a 12-hour clock, which datestr pads with a space and a datetime writes without padding. Each variable is named after its series, Data for a series with no name, made unique with the suffixes _1, _2, ... in order. A name reserved in a timetable, Properties, Time or Variables, takes a suffix too, with a warning; MATLAB raises for the last two. The data are laid with time along the first dimension, so data whose time runs along its last dimension is permuted; a series whose samples then need more than two dimensions is refused, since a timetable here holds two-dimensional variables only, where MATLAB gives a variable of more. DataInfo.Units becomes VariableUnits, and the interpolation method VariableContinuity: 'linear' is 'continuous' and 'zoh' is 'step'; a series with an interpolation function is refused. Properties.UserData is the UserData of the first series that has any. The events of the series become the Properties.Events of the timetable: an eventtable of their times, with their names as EventLabels and, when any event has some, their EventData as a variable; an event held by several series appears once. MATLAB drops the events, with a warning. Quality codes are dropped, with a warning, as in MATLAB. See also: timeseries, tscollection, timetable, eventtable # name: # type: sq_string # elements: 1 # length: 35 Convert time series to a timetable. # name: # type: sq_string # elements: 1 # length: 9 timetable # name: # type: sq_string # elements: 1 # length: 2414 datatypes: timetable Array of tabular data whose rows are labelled by time. A timetable collects heterogeneous data into columnar variables exactly as a table does, but labels each of its rows with a time instead of a name. The row times are a datetime or a duration vector, one element per row, and they are what the type exists for: rows can be selected, aligned and resampled by when they happened. Unlike the row names of a table, the row times need not be unique, sorted, or present; duplicates, a descending order and missing times are all accepted and merely make the timetable irregular. A timetable is regular when its rows are evenly spaced. The spacing is reported by the TimeStep property and its reciprocal SampleRate, and a timetable that is not evenly spaced reports a NaN time step. A timetable told its step at construction, by 'TimeStep' or 'SampleRate', remembers it even where a subset is too short to imply one, so a single row taken out of it is still hourly; one that read its step off the row times it was given has nothing to fall back on and reports NaN there. A timetable is not a table and neither is a subclass of the other; both derive from the same abstract class, so istabular is true for either while istable and istimetable each single one of them out. Assigning an empty matrix to a subscripted timetable deletes rows or variables exactly as it does for a table, and the row times go with the rows they label. The row times themselves cannot be deleted. Assigning to a subscripted timetable with parentheses or curly brackets, or to its variable dimension name, writes values and creates variables and rows as it does for a table; a row added this way has a missing row time, or the next one along a step given when the timetable was created. A timetable assigned with parentheses must carry the row times of the rows it is assigned to, and is refused otherwise, where MATLAB pastes its rows by position and ignores their times; synchronize aligns two timetables first. Besides the timetable constructor, you can also use table2timetable and array2timetable to create timetables from the respective data types. eventtable derives from this class and inherits every method documented here; where one of them behaves differently for an event table, its own documentation says so. See also: table, eventtable, istimetable, istabular, timetable.isregular, datetime, duration # name: # type: sq_string # elements: 1 # length: 54 Array of tabular data whose rows are labelled by time. # name: # type: sq_string # elements: 1 # length: 26 timetable.CustomProperties # name: # type: sq_string # elements: 1 # length: 1136 tabular: property CustomProperties Customized metadata of table and its variables Custom properties that contain metadata of a table and its variables. By default, this is an empty container. Each custom property holds either table metadata or per-variable metadata, according to the property type ('table' or 'variable') specified when the property is created with the addprop method. A variable-scoped property holds one element per variable. You can add an individual custom property only by using the addprop method and you can only remove a custom property with the rmprop method. To access existing custom properties use dot name structure assignment as in tbl.Properties.CustomProperties.PropertyName, where PropertyName is the name used with the addprop method. The whole set may also be taken from another table by assigning that table's CustomProperties to this one, which replaces every custom property with those of the other table, their types included. A variable-scoped property arriving that way must hold one element for each variable of the receiving table, or be a 0-by-0 empty. Nothing else can be assigned there. # name: # type: sq_string # elements: 1 # length: 46 Customized metadata of table and its variables # name: # type: sq_string # elements: 1 # length: 21 timetable.Description # name: # type: sq_string # elements: 1 # length: 300 tabular: property Description Table description Table description specified as a character vector or a string scalar. If specified as a string scalar, it is converted and stored internally as a character vector. You can access the Description property of a table tbl with tbl.Properties.Description. # name: # type: sq_string # elements: 1 # length: 17 Table description # name: # type: sq_string # elements: 1 # length: 24 timetable.DimensionNames # name: # type: sq_string # elements: 1 # length: 818 tabular: property DimensionNames Dimension names Dimension names specified as a two-element cell array of character vectors or a two-element string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. You can access the DimensionNames property of a table tbl with tbl.Properties.DimensionNames. By default, DimensionNames is specified as 'Row', 'Variables'. You can access table data per rows or per columns by using either one of the two dimension names, respectively. However, if the table contains row names, then the first element of the DimensionNames corresponds to the row names. The second name returns the variables joined into one array, as table2array joins them, and assigning to it writes them all, as curly brackets over the whole table do. # name: # type: sq_string # elements: 1 # length: 15 Dimension names # name: # type: sq_string # elements: 1 # length: 16 timetable.Events # name: # type: sq_string # elements: 1 # length: 928 timetable: property Events Event table attached to this timetable Event table attached to this timetable, specified as an eventtable, or empty when no events are attached. A plain timetable may be assigned and is converted, though no variable of it is guessed to hold the event labels; assigning [] detaches whatever was there. The event table's row times must be of the same type as this timetable's, so a duration-keyed timetable takes a duration-keyed event table and a datetime-keyed one takes a datetime-keyed event table. Nothing requires the events to lie within the span of the row times, and events entirely outside it are accepted. An operation that preserves the rows carries the event table through unchanged: it is never subsetted, filtered, re-timed or reordered along with them. An eventtable inherits this property but cannot read or assign it, an event table not being something that can itself carry events. # name: # type: sq_string # elements: 1 # length: 38 Event table attached to this timetable # name: # type: sq_string # elements: 1 # length: 18 timetable.RowTimes # name: # type: sq_string # elements: 1 # length: 518 timetable: property RowTimes Row times Row times, specified as a datetime or duration vector with one element for each row of the timetable. They are stored as a column vector whatever shape they are given in. Unlike the row names of a table they need not be unique, sorted or present: duplicate times, a descending order and missing times are all accepted, and each of them merely makes the timetable irregular. You can access them with tt.Properties.RowTimes and assign them the same way, which recomputes TimeStep. # name: # type: sq_string # elements: 1 # length: 9 Row times # name: # type: sq_string # elements: 1 # length: 20 timetable.SampleRate # name: # type: sq_string # elements: 1 # length: 394 timetable: property SampleRate Sampling rate in hertz Sampling rate, specified as a positive numeric scalar, being the reciprocal of TimeStep measured in seconds. It is NaN whenever the time step is a calendarDuration, a calendar step having no fixed length in seconds, and whenever the timetable is irregular. Assigning it regenerates the row times from StartTime, as assigning TimeStep does. # name: # type: sq_string # elements: 1 # length: 22 Sampling rate in hertz # name: # type: sq_string # elements: 1 # length: 19 timetable.StartTime # name: # type: sq_string # elements: 1 # length: 337 timetable: property StartTime Time of the first row Time of the first row, of the same type as RowTimes. It follows the first row: deleting the first row leaves StartTime naming the row that is now first. A timetable left with no rows keeps the value it had. Assigning it shifts every row time by the same amount and preserves TimeStep. # name: # type: sq_string # elements: 1 # length: 21 Time of the first row # name: # type: sq_string # elements: 1 # length: 18 timetable.TimeStep # name: # type: sq_string # elements: 1 # length: 800 timetable: property TimeStep Spacing between row times Spacing between consecutive row times, specified as a duration or calendarDuration scalar, or a NaN duration when the timetable is irregular. Two rows or more always imply a step of their own and it is read off them afresh, so a subset with a gap in it steps by nothing and a reversed one steps backwards. Fewer than two rows imply nothing, and there it matters how the step was arrived at: one given by 'TimeStep' or 'SampleRate' is remembered, while one read off the row times is not and becomes NaN, its class resetting to duration with it. A freshly built one-row timetable has no step either way. A negative step is as regular as a positive one. Assigning it regenerates the row times from StartTime, even when the timetable was irregular. # name: # type: sq_string # elements: 1 # length: 25 Spacing between row times # name: # type: sq_string # elements: 1 # length: 18 timetable.UserData # name: # type: sq_string # elements: 1 # length: 245 tabular: property UserData Additional table information Additional table information, specified as an array. Any type of data can be attached using this property. You can access the UserData property of a table tbl with tbl.Properties.UserData. # name: # type: sq_string # elements: 1 # length: 28 Additional table information # name: # type: sq_string # elements: 1 # length: 28 timetable.VariableContinuity # name: # type: sq_string # elements: 1 # length: 935 tabular: property VariableContinuity Variable continuity Continuity of each variable, specified as a cell array of character vectors or a string array carrying one element per variable, each of them 'unset', 'continuous', 'step' or 'event'. It is empty by default, and assigning {} or [] clears it. If specified as a string array, it is converted and stored internally as a cell array of character vectors. You can access it with tbl.Properties.VariableContinuity and you can index individual variables to read or assign their continuity. A table carries the property but does not act on it, which is also how MATLAB behaves. A timetable uses it to choose the default fill method of each variable when resampling. MATLAB stores this property as a matlab.tabular.Continuity enumeration. Octave has no enumeration classes, so it is stored and returned here as a cell array of character vectors, as VariableNames and VariableUnits are. # name: # type: sq_string # elements: 1 # length: 19 Variable continuity # name: # type: sq_string # elements: 1 # length: 30 timetable.VariableDescriptions # name: # type: sq_string # elements: 1 # length: 864 tabular: property VariableDescriptions Variable descriptions Variable descriptions, specified as a cell array of character vectors or a string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. If not empty (default), it must contain the same number of elements as the number of variables. If a specific variable does not have a description, this can be specified with an individual empty character vector or an empty string. You can access the VariableDescriptions property of a table tbl with tbl.Properties.VariableDescriptions. You can further index specific variables to access their description. Assigning an empty cell or string array returns the property to its default, and describing a single variable of a table that has no descriptions gives the remaining variables an empty description. # name: # type: sq_string # elements: 1 # length: 21 Variable descriptions # name: # type: sq_string # elements: 1 # length: 23 timetable.VariableNames # name: # type: sq_string # elements: 1 # length: 550 tabular: property VariableNames Variable names Variable names, specified as a cell array of character vectors or a string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. All elements must be nonempty and distinct, and their number must equal the number of variables. You can access the data type of a specific variable by using dot name assignment, as in tbl.varname, where varname is the name of the variable in table tbl. If the variable name does not exist, a new one is created. # name: # type: sq_string # elements: 1 # length: 14 Variable names # name: # type: sq_string # elements: 1 # length: 23 timetable.VariableTypes # name: # type: sq_string # elements: 1 # length: 700 tabular: property VariableTypes Variable data types The class of the data of each variable, defined as a cell array of character vectors or a string array with the same number of elements as the number of variables in the table. If specified as a string array, it is converted and stored internally as a cell array of character vectors. You can access the VariableTypes property of a table tbl with tbl.Properties.VariableTypes. You can further index specific variables to access their data type. Modifying the elements of the VariableTypes property automatically converts the underlying data of the corresponding variable into the specified data types provided that a valid conversion is requested. # name: # type: sq_string # elements: 1 # length: 19 Variable data types # name: # type: sq_string # elements: 1 # length: 23 timetable.VariableUnits # name: # type: sq_string # elements: 1 # length: 803 tabular: property VariableUnits Variable units Variable units, specified as a cell array of character vectors or a string array. If specified as a string array, it is converted and stored internally as a cell array of character vectors. If not empty (default), it must contain the same number of elements as the number of variables. If a specific variable does not have a unit, this can be specified with an individual empty character vector or an empty string. You can access the VariableUnits property of a table tbl with tbl.Properties.VariableUnits. You can further index specific variables to access their unit. Assigning an empty cell or string array returns the property to its default, and giving a unit to a single variable of a table that has none gives the remaining variables an empty unit. # name: # type: sq_string # elements: 1 # length: 14 Variable units # name: # type: sq_string # elements: 1 # length: 17 timetable.addprop # name: # type: sq_string # elements: 1 # length: 758 timetable: tt = addprop (tt, propertyNames, propertyTypes) Add custom properties to a timetable. tt = addprop (tt, propertyNames, propertyTypes) adds properties that carry custom metadata to the timetable tt. propertyNames names them, as a character vector, a cell array of character vectors, or a string array, and propertyTypes says what each one describes, one per name: 'table' for a property describing the timetable as a whole and 'variable' for one carrying an entry per variable, which is kept in step as variables are added, removed, moved and renamed. A property is added empty; assign it through tt.Properties.CustomProperties.PropertyName. The row times are not a variable and carry no entry of their own. See also: timetable.rmprop, timetable # name: # type: sq_string # elements: 1 # length: 37 Add custom properties to a timetable. # name: # type: sq_string # elements: 1 # length: 17 timetable.addvars # name: # type: sq_string # elements: 1 # length: 930 timetable: ttB = addvars (ttA, var1, ..., varN) timetable: ttB = addvars (..., 'Before', location) timetable: ttB = addvars (..., 'After', location) timetable: ttB = addvars (..., 'NewVariableNames', newNames) Add variables to a timetable. ttB = addvars (ttA, var1, ...) appends each array as a new variable at the right-hand end. Every one must have as many rows as the timetable has. Unnamed variables take the name of the workspace variable they came from, or VarN where there is none. A name that would collide with the row dimension is not allowed and one that arrives by the workspace route is given a suffix instead. 'Before' and 'After' place the new variables beside an existing one, named or numbered. The row times are not a variable and cannot be used as the location, nor can position zero. The row times are untouched, so the time step is unchanged. See also: timetable.removevars, timetable.movevars, timetable # name: # type: sq_string # elements: 1 # length: 29 Add variables to a timetable. # name: # type: sq_string # elements: 1 # length: 20 timetable.anymissing # name: # type: sq_string # elements: 1 # length: 375 timetable: TF = anymissing (tt) True when any variable of a timetable has a missing value. TF = anymissing (tt) is any (ismissing (tt)(:)) and reads the same thing: the variables, and not the row times. A timetable whose only missing value is a row time answers false here and still loses that row to rmmissing. See also: timetable.ismissing, timetable.rmmissing, timetable # name: # type: sq_string # elements: 1 # length: 58 True when any variable of a timetable has a missing value. # name: # type: sq_string # elements: 1 # length: 13 timetable.cat # name: # type: sq_string # elements: 1 # length: 316 timetable: tt = cat (dim, tt1, tt2, ...) Concatenate timetables along a dimension. tt = cat (dim, tt1, tt2, ...) concatenates along dim, which must be 1 or 2: a timetable has two dimensions and there is nothing to stack along a third. cat (1, ...) is vertcat and cat (2, ...) is horzcat. See also: vertcat, horzcat # name: # type: sq_string # elements: 1 # length: 41 Concatenate timetables along a dimension. # name: # type: sq_string # elements: 1 # length: 23 timetable.containsrange # name: # type: sq_string # elements: 1 # length: 1349 timetable: TF = containsrange (tt, ref) timetable: [TF, whichRows] = containsrange (tt, ref) True when a timetable spans the whole of a range of times. TF = containsrange (tt, ref) returns true when every instant of ref lies inside the range of tt, that is between its earliest and its latest row time, both included. The row times need be neither sorted nor unique, since only the two ends of the range are read, and missing row times take no part in it. ref says what range is meant, in one of three ways. A timerange gives its own bounds, and says at each end whether the bound itself belongs to the range. A timetable gives the range between its earliest and its latest row time, both ends included. A datetime or duration scalar gives a single instant. A reference whose times are of the other kind is refused: elapsed time and a calendar cannot be compared. [TF, whichRows] = containsrange (...) also returns a column of logicals, one per row of tt, saying which of its rows fall in ref. That answer is the same for all three range predicates and is independent of TF: rows may fall in a range the timetable does not contain. A timetable whose row times are all missing has no range and answers false, and so does a reference naming no instant, such as a NaT. See also: timetable.overlapsrange, timetable.withinrange, timerange, timetable # name: # type: sq_string # elements: 1 # length: 58 True when a timetable spans the whole of a range of times. # name: # type: sq_string # elements: 1 # length: 21 timetable.convertvars # name: # type: sq_string # elements: 1 # length: 491 timetable: ttB = convertvars (ttA, vars, dataType) Convert variables of a timetable to a given type. ttB = convertvars (ttA, vars, dataType) converts each variable in vars, which may be named, numbered, selected by a logical vector or picked out by a vartype. dataType is a type name or a function handle that performs the conversion. The row times are not a data variable and cannot be converted; assign to tt.Properties.RowTimes to change them. See also: timetable.renamevars, timetable # name: # type: sq_string # elements: 1 # length: 49 Convert variables of a timetable to a given type. # name: # type: sq_string # elements: 1 # length: 15 timetable.empty # name: # type: sq_string # elements: 1 # length: 750 timetable: tt = timetable.empty () timetable: tt = timetable.empty (n) timetable: tt = timetable.empty (r, v) timetable: tt = timetable.empty (sz) Create an empty timetable. tt = timetable.empty () returns a 0-by-0 timetable. tt = timetable.empty (r, v) returns a timetable with r rows and v variables, at least one of which must be zero. The row times are NaT, one per row, since a row of a timetable is labelled whether or not it holds anything. A timetable with variables but no rows names them Var1 to VarN and gives each of them a double value. tt = timetable.empty (sz) takes the two dimensions from the two-element vector sz, and timetable.empty (n) is the same as timetable.empty (n, n). See also: timetable, table, isempty, height, width # name: # type: sq_string # elements: 1 # length: 26 Create an empty timetable. # name: # type: sq_string # elements: 1 # length: 23 timetable.extractevents # name: # type: sq_string # elements: 1 # length: 2572 timetable: et = extractevents (tt, rows) timetable: et = extractevents (tt, labels) timetable: et = extractevents (..., Name, Value) timetable: [et, tt2] = extractevents (...) Build an event table out of the rows of a timetable. et = extractevents (tt, rows) returns an eventtable whose events happen at the times of the rows rows picks out. rows is any row subscript a timetable accepts: row numbers, a logical vector, row times, a timerange, a withtol or a colon. The events come out in the order the subscript names them and a row named twice gives two events; with no option saying otherwise the event table carries no variables at all and designates nothing. et = extractevents (tt, labels) takes a categorical vector with one element per row of tt and makes an event of every row whose label is not missing, labelled by it. Consecutive rows sharing a label are not gathered into one interval: each row is its own instantaneous event. [et, tt2] = extractevents (...) also returns tt2, a copy of tt without the variables the event table took from it. Only variables named by 'EventDataVariables' or by one of the three '...Variable' options are taken, so a call that names none leaves tt2 exactly as tt was. The following Name-Value options are supported: Name Value 'EventLabels' A scalar, labelling every event alike, or one value per event. Added as a variable named EventLabels. 'EventLabelsVariable' The name of a variable of tt holding the labels. It keeps its own name in the event table and leaves tt2. 'EventLengths' A duration or calendarDuration, scalar or one per event, added as a variable named EventLengths. 'EventLengthsVariable' The name of a variable of tt holding the lengths, which must be a duration or calendarDuration column. 'EventEnds' Scalar or one per event, of the same type as the row times, added as a variable named EventEnds. 'EventEndsVariable' The name of a variable of tt holding the end times, which must be a datetime or duration column. 'EventDataVariables' Variables of tt to carry into the event table as event data. Each keeps its own name. 'PreserveEventVariables' A logical scalar. When true the variables the event table took stay in tt2 as well. It says what to do with variables that were named, so it requires at least one option that names some. Lengths and ends are mutually exclusive, however they are given, and so are labels given as values and labels named as a variable. Neither labels option may be combined with the labels form, which carries its own. See also: eventtable, timetable.syncevents, timetable # name: # type: sq_string # elements: 1 # length: 52 Build an event table out of the rows of a timetable. # name: # type: sq_string # elements: 1 # length: 21 timetable.fillmissing # name: # type: sq_string # elements: 1 # length: 1679 timetable: ttB = fillmissing (ttA, method) timetable: ttB = fillmissing (ttA, 'constant', v) timetable: ttB = fillmissing (..., Name, Value) timetable: [ttB, TF] = fillmissing (...) Fill the missing values of a timetable. ttB = fillmissing (ttA, method) replaces each missing value by one worked out from the values around it. 'previous', 'next' and 'nearest' copy a neighbouring value; 'linear', 'spline', 'pchip' and 'makima' interpolate; and 'constant' takes the value given after it. The row times are what the filling runs against, not the order of the rows. A gap an hour after its left neighbour and two hours before its right one is filled a third of the way between them by 'linear', and takes the left value under 'nearest', where counting rows would put it midway and call the two neighbours equally close. The row times are already the sample points, so 'SamplePoints' is not accepted; a timetable whose row times are not all known is refused outright rather than filled around the gap. 'DataVariables' names the variables to fill and 'EndValues' says what to do with a gap that has no neighbour on one side, taking 'extrap', another method name, or a constant. A variable with nothing ismissing counts as missing, such as a cell array that is not a cell array of character vectors, a structure or a nested table, is left as it is. MATLAB refuses the whole call when such a variable is among the data variables. [ttB, TF] = fillmissing (...) also returns a logical array marking what was filled. The row times themselves are never filled and the time step is unchanged. See also: timetable.ismissing, timetable.rmmissing, timetable.standardizeMissing, timetable # name: # type: sq_string # elements: 1 # length: 39 Fill the missing values of a timetable. # name: # type: sq_string # elements: 1 # length: 20 timetable.findgroups # name: # type: sq_string # elements: 1 # length: 672 timetable: G = findgroups (tt) timetable: [G, TID] = findgroups (tt) Number the groups the rows of a timetable fall into. G = findgroups (tt) returns a column of group numbers, one per row of tt, numbering the distinct combinations of its variables in sorted order. A row holding a missing value in any variable belongs to no group and is numbered NaN. The row times take no part: a timetable groups by its variables, as a table does. [G, TID] = findgroups (tt) also returns a table with one row per group, holding the combination of values that defines it. It is a table and not a timetable, its rows being groups rather than instants. See also: splitapply, timetable # name: # type: sq_string # elements: 1 # length: 52 Number the groups the rows of a timetable fall into. # name: # type: sq_string # elements: 1 # length: 21 timetable.groupcounts # name: # type: sq_string # elements: 1 # length: 1728 timetable: G = groupcounts (tt, groupvars) timetable: G = groupcounts (tt, groupvars, groupbins) timetable: G = groupcounts (..., Name, Value) Count the rows of a timetable in each group. G = groupcounts (tt, groupvars) groups the rows of tt by the variables named in groupvars and returns a table with one row per group, carrying the grouping variables, a GroupCount variable and a Percent variable. The result is a table and not a timetable: its rows describe groups rather than instants, so there is nothing left for row times to label. groupvars names one or more variables, or the row dimension name, which groups by the row times themselves. G = groupcounts (tt, groupvars, groupbins) bins the grouping variables before grouping, groupbins being a bin count, a vector of edges, or a time unit for a datetime or duration variable. A duration is binned by the units that have a fixed length of their own, 'second', 'minute', 'hour', 'day' and 'year'; a week, a month and a quarter have none and are refused. A narrow bin over a wide span asks for more bins than a categorical can name, and the call is refused rather than answered; MATLAB stops at 65536 bins there and returns a grid that does not reach the end of the span. The following Name/Value pairs are accepted: 'IncludeMissingGroups' A logical scalar. When true (the default) rows whose grouping value is missing form a group of their own, sorted last. 'IncludeEmptyGroups' A logical scalar. When true the unused categories of a categorical or binned grouping variable are reported as groups with a count of zero. It defaults to false. 'IncludedEdge' Which edge of a bin is included, 'left' (the default) or 'right'. It applies only where groupbins was given. # name: # type: sq_string # elements: 1 # length: 44 Count the rows of a timetable in each group. # name: # type: sq_string # elements: 1 # length: 21 timetable.groupfilter # name: # type: sq_string # elements: 1 # length: 1815 timetable: G = groupfilter (tt, groupvars, method) timetable: G = groupfilter (tt, groupvars, method, datavars) timetable: G = groupfilter (tt, groupvars, groupbins, method, ...) timetable: G = groupfilter (..., 'IncludedEdge', edge) Keep the rows of a timetable whose group passes a test. G = groupfilter (tt, groupvars, method) groups the rows of tt by the variables named in groupvars, applies the function handle method to each data variable of each group, and returns the rows of the groups it accepted. method receives the values of one variable across the rows of one group and answers with a logical scalar, keeping or dropping the whole group, or with one logical per row of the group, keeping those rows. The rows that survive keep their order and their row times. groupvars names one or more variables, or the row dimension name, which groups by the row times themselves. G = groupfilter (tt, groupvars, method, datavars) applies method only to the variables named in datavars. By default every variable that is not a grouping variable is used, and a row is kept only where every one of them accepts it. G = groupfilter (tt, groupvars, groupbins, method, ...) bins the grouping variables before grouping, groupbins being a bin count, a vector of edges, or a time unit for a datetime or duration variable. The 'IncludedEdge' option then says which edge of a bin is included, 'left' (the default) or 'right'. A duration is binned by the units that have a fixed length of their own, 'second', 'minute', 'hour', 'day' and 'year'; a week, a month and a quarter have none and are refused. A narrow bin over a wide span asks for more bins than a categorical can name, and the call is refused rather than answered; MATLAB stops at 65536 bins there and returns a grid that does not reach the end of the span. # name: # type: sq_string # elements: 1 # length: 55 Keep the rows of a timetable whose group passes a test. # name: # type: sq_string # elements: 1 # length: 22 timetable.groupsummary # name: # type: sq_string # elements: 1 # length: 2152 timetable: G = groupsummary (tt, groupvars) timetable: G = groupsummary (tt, groupvars, method) timetable: G = groupsummary (tt, groupvars, method, datavars) timetable: G = groupsummary (tt, groupvars, groupbins, ...) timetable: G = groupsummary (..., Name, Value) Summarise the variables of a timetable group by group. G = groupsummary (tt, groupvars) groups the rows of tt by the variables named in groupvars and returns a table with one row per group, carrying the grouping variables and a GroupCount variable. The result is a table and not a timetable: its rows describe groups rather than instants, so there is nothing left for row times to label. groupvars names one or more variables, or the row dimension name, which groups by the row times themselves. G = groupsummary (tt, groupvars, method) also applies method to each data variable of each group, adding one variable per method and data variable named for both, as in mean_Speed. method is one of 'mean', 'sum', 'min', 'max', 'range', 'median', 'mode', 'var', 'std', 'nummissing', 'nnz' and 'numunique', or 'all' for every one of them in that order, or a function handle, or a cell array of any of these. A name may be abbreviated to any unambiguous prefix. 'std' and 'var' are not defined on an integer variable and refuse one. G = groupsummary (tt, groupvars, method, datavars) summarises only the variables named in datavars. By default every variable that is not a grouping variable is used. G = groupsummary (tt, groupvars, groupbins, ...) bins the grouping variables before grouping, groupbins being a bin count, a vector of edges, or a time unit for a datetime or duration variable. The following Name/Value pairs are accepted: 'IncludeMissingGroups' A logical scalar. When true (the default) rows whose grouping value is missing form a group of their own, sorted last. 'IncludeEmptyGroups' A logical scalar. When true the unused categories of a categorical or binned grouping variable are reported as groups with a count of zero. It defaults to false. 'IncludedEdge' Which edge of a bin is included, 'left' (the default) or 'right'. It applies only where groupbins was given. # name: # type: sq_string # elements: 1 # length: 54 Summarise the variables of a timetable group by group. # name: # type: sq_string # elements: 1 # length: 24 timetable.grouptransform # name: # type: sq_string # elements: 1 # length: 2013 timetable: G = grouptransform (tt, groupvars, method) timetable: G = grouptransform (tt, groupvars, method, datavars) timetable: G = grouptransform (tt, groupvars, groupbins, method, ...) timetable: G = grouptransform (..., Name, Value) Transform the variables of a timetable group by group. G = grouptransform (tt, groupvars, method) groups the rows of tt by the variables named in groupvars and applies method to each data variable within each group. The result has the same rows as tt, in the same order and carrying the same row times, so a transform never moves a row in time. groupvars names one or more variables, or the row dimension name, which groups by the row times themselves. method is one of 'zscore', 'norm', 'meancenter', 'rescale', 'meanfill' and 'linearfill', or a function handle applied to each group. G = grouptransform (tt, groupvars, method, datavars) transforms only the variables named in datavars. By default every variable that is not a grouping variable is transformed. G = grouptransform (tt, groupvars, groupbins, method, ...) bins the grouping variables before grouping, groupbins being a bin count, a vector of edges, or a time unit for a datetime or duration variable. A duration is binned by the units that have a fixed length of their own, 'second', 'minute', 'hour', 'day' and 'year'; a week, a month and a quarter have none and are refused. A narrow bin over a wide span asks for more bins than a categorical can name, and the call is refused rather than answered; MATLAB stops at 65536 bins there and returns a grid that does not reach the end of the span. The following Name/Value pairs are accepted: 'ReplaceValues' A logical scalar. When true (the default) each transformed variable replaces the one it came from; when false the results are appended as new variables named for the method and the variable they came from, as in zscore_Speed. 'IncludedEdge' Which edge of a bin is included, 'left' (the default) or 'right'. It applies only where groupbins was given. # name: # type: sq_string # elements: 1 # length: 54 Transform the variables of a timetable group by group. # name: # type: sq_string # elements: 1 # length: 14 timetable.head # name: # type: sq_string # elements: 1 # length: 970 timetable: head (tt) timetable: head (tt, k) timetable: out = head (tt, k) Display or return the first k rows of a timetable. head (tt) displays the first eight rows of tt, or all of them if it has fewer. head (tt, k) displays the first k instead. k must be a real, nonnegative, integer scalar value; a k of zero displays no rows at all rather than raising. out = head (tt, k) returns those rows in a new timetable instead of displaying them. If k is omitted or empty it defaults to eight. The rows come back in the order the timetable holds them. head takes the first rows and not the earliest ones, so on a timetable whose times are out of order it returns whatever happens to be stored first. Sort it beforehand to ask the other question. The time step is read afresh from the row times that are kept, so the head of an hourly timetable is hourly and the head of a timetable whose spacing changes partway may not be. See also: timetable.tail, sortrows, timetable # name: # type: sq_string # elements: 1 # length: 50 Display or return the first k rows of a timetable. # name: # type: sq_string # elements: 1 # length: 16 timetable.height # name: # type: sq_string # elements: 1 # length: 348 timetable: H = height (tt) Number of rows in timetable. H = height (tt) returns the number of rows in the timetable tt as a scalar. It is the equivalent of size (tt, 1). The count comes from the row times rather than from the variables, so a timetable whose variables have all been removed still reports the rows it labels. See also: width, size # name: # type: sq_string # elements: 1 # length: 28 Number of rows in timetable. # name: # type: sq_string # elements: 1 # length: 17 timetable.horzcat # name: # type: sq_string # elements: 1 # length: 937 timetable: tt = horzcat (tt1, tt2, ...) Concatenate timetables horizontally. tt = horzcat (tt1, tt2, ...) appends the variables of its inputs, which must all have distinct variable names and, where they are timetables, identical row times. It is the operation [tt1, tt2] performs. A table may be appended to a timetable, but only after it, and it must have as many rows as the timetable has. The row times and the time step are those of the timetable, and the row dimension is named after the first input that does not use the default name. Where an operand is an eventtable, or carries one on its Events property, the result is an event table too and the operands' event tables are merged: an outer join keyed on the event times and every variable the two share, ordered by those keys. The three properties saying which variables describe the events must agree, and the operation is refused where they do not. See also: vertcat, cat # name: # type: sq_string # elements: 1 # length: 36 Concatenate timetables horizontally. # name: # type: sq_string # elements: 1 # length: 21 timetable.inner2outer # name: # type: sq_string # elements: 1 # length: 538 timetable: tt2 = inner2outer (tt) Turn the nesting of a timetable inside out. tt2 = inner2outer (tt) returns a timetable whose variables are named for the variables of the tables nested inside tt, each holding a nested table named for the variables of tt that held them. The rows and their row times are untouched: only the nesting is turned inside out. An inner variable name held by one nested table alone becomes a plain variable rather than a nested one. A timetable holding nothing nested is returned as it is. See also: timetable # name: # type: sq_string # elements: 1 # length: 43 Turn the nesting of a timetable inside out. # name: # type: sq_string # elements: 1 # length: 19 timetable.innerjoin # name: # type: sq_string # elements: 1 # length: 1521 timetable: ttC = innerjoin (ttL, tblR) timetable: ttC = innerjoin (ttL, tblR, Name, Value) timetable: [ttC, iL, iR] = innerjoin (...) Join a timetable with another tabular object, keeping matched rows. ttC = innerjoin (ttL, tblR) returns a timetable holding one row for every pair of rows of ttL and tblR whose keys match, in key order. Each row carries the row time of the row of ttL it came from, so a key matched more than once repeats its row time and the result is generally irregular. With no key named, two timetables join on their row times; a timetable and a table have no key in common unless one is named, since the row times are not a variable. [ttC, iL, iR] = innerjoin (...) also returns the rows of ttL and of tblR each row came from. The following Name/Value pairs are accepted: 'Keys' The variables to match on, named on both sides. The row dimension name names the row times. 'LeftKeys', 'RightKeys' The variables to match on, named separately for each side and given together. 'LeftVariables', 'RightVariables' The variables each side contributes. By default the left contributes all of its own and the right all but its keys. Where an operand is an eventtable, or carries one on its Events property, the result is an event table too and the operands' event tables are merged: an outer join keyed on the event times and every variable the two share, ordered by those keys. The three properties saying which variables describe the events must agree, and the operation is refused where they do not. # name: # type: sq_string # elements: 1 # length: 67 Join a timetable with another tabular object, keeping matched rows. # name: # type: sq_string # elements: 1 # length: 17 timetable.isempty # name: # type: sq_string # elements: 1 # length: 178 timetable: TF = isempty (tt) True for an empty timetable. TF = isempty (tt) returns true when tt has no rows or no variables, and false otherwise. See also: size, height, width # name: # type: sq_string # elements: 1 # length: 28 True for an empty timetable. # name: # type: sq_string # elements: 1 # length: 17 timetable.isequal # name: # type: sq_string # elements: 1 # length: 1550 timetable: TF = isequal (A, B) timetable: TF = isequal (A, B, ...) Test timetables for equality. TF = isequal (A, B) returns a logical scalar TF, which is true when the timetables A and B are the same size, carry the same row times, variable names and metadata, and each pair of corresponding variables holds equal values, and false otherwise. The row times take part in the comparison and the properties that merely describe them do not, so TimeStep, SampleRate and StartTime are excluded. A timetable told its time step at construction therefore equals one that read the same step off the row times it was given, although the two report a different TimeStep once subset to a single row. Variables are compared by value and not by class, exactly as isequal compares arrays elsewhere, so a timetable holding int8 ([1; 2]) equals one holding [1; 2]. The VariableTypes property, which only restates those classes, takes no part in the comparison. Every other property does: two timetables differing only in Description, UserData, VariableUnits, VariableDescriptions, VariableContinuity or a custom property are not equal. As with NaN, missing values are never equal, so a missing row time or element anywhere in either timetable makes the result false; use isequaln to treat missing values as equal. Further timetables may be supplied, as in isequal (A, B, C, ...), in which case TF is true only when all of them are equal to one another. Any argument that is not a timetable, a table included, makes the result false rather than raising an error. # name: # type: sq_string # elements: 1 # length: 29 Test timetables for equality. # name: # type: sq_string # elements: 1 # length: 18 timetable.isequaln # name: # type: sq_string # elements: 1 # length: 654 timetable: TF = isequaln (A, B) timetable: TF = isequaln (A, B, ...) Test timetables for equality, treating missing values as equal. TF = isequaln (A, B) is identical to isequal (A, B) except that missing values are treated as equal to one another, in the same way that isequaln treats NaN. Two timetables whose row times are NaT in the same places are therefore equal, where isequal calls them unequal. Further timetables may be supplied, as in isequaln (A, B, C, ...), in which case TF is true only when all of them are equal to one another. Any argument that is not a timetable, a table included, makes the result false rather than raising an error. # name: # type: sq_string # elements: 1 # length: 63 Test timetables for equality, treating missing values as equal. # name: # type: sq_string # elements: 1 # length: 19 timetable.ismissing # name: # type: sq_string # elements: 1 # length: 1074 timetable: TF = ismissing (tt) timetable: TF = ismissing (tt, indicator) Find missing values in the variables of a timetable. TF = ismissing (tt) returns a logical array with one row per row of tt and one column per variable, true where the variable is missing there. What counts as missing depends on the type: NaN for numeric data, NaT for datetime, for categorical, and an empty character vector or string. The row times are not read. They label the rows rather than being one of them, so a missing row time is not reported here and does not make anymissing true. rmmissing does drop such a row, being about what can be kept rather than about what is missing. TF = ismissing (tt, indicator) treats the values in indicator as missing instead. When specifying an indicator, all default missing values are ignored. If you want to keep them, you need to define them in indicator, which missing does for every variable. indicator follows the rules of table.ismissing. See also: timetable.anymissing, timetable.rmmissing, timetable.standardizeMissing, timetable # name: # type: sq_string # elements: 1 # length: 52 Find missing values in the variables of a timetable. # name: # type: sq_string # elements: 1 # length: 19 timetable.isregular # name: # type: sq_string # elements: 1 # length: 1966 timetable: TF = isregular (tt) timetable: TF = isregular (tt, unit) True when the row times of a timetable are evenly spaced. TF = isregular (tt) returns true when tt is regular with respect to absolute time, that is when consecutive row times are separated by the same fixed length of time. A calendar month has no fixed length, so a monthly timetable answers false here and true for 'months' below. TF = isregular (tt, unit) returns true when the row times are evenly spaced by a whole number of unit, one of 'time', 'days', 'weeks', 'months', 'quarters' or 'years'. 'time' means absolute time and is what the one-argument form asks. There is no unit smaller than a day: an hourly timetable is regular in time and in nothing else. The calendar units are measured on the calendar rather than in elapsed time, which is what separates them. Across a daylight saving change a run of calendar days is regular in 'days' and regular in nothing else, one of those days being an hour shorter than the rest, and such a timetable reports no time step at all while still answering true here. A timetable with fewer than two rows has no spacing to measure and answers from the time step it remembers, so a single row taken out of a timetable told it was hourly is still regular in time, while one taken out of a timetable that read its step off its row times is not. The answer is always computed from the row times and unit is always checked, which MATLAB does neither of when the step was declared with 'TimeStep' or 'SampleRate' and is a duration: there it answers from the stored step alone. Two consequences follow there and not here. A misspelt unit, and the non-units 'seconds', 'minutes' and 'hours', come back false rather than raising. And two timetables with identical row times disagree: times built as d0 + hours (0:24:96) answer true for 'days' while the same times declared as 'TimeStep', hours (24) answer false. Both answer true here. See also: timetable # name: # type: sq_string # elements: 1 # length: 57 True when the row times of a timetable are evenly spaced. # name: # type: sq_string # elements: 1 # length: 18 timetable.issorted # name: # type: sq_string # elements: 1 # length: 755 timetable: TF = issorted (tt) True when the row times of a timetable are in ascending order. TF = issorted (tt) returns true when each row time is at or after the one before it. The ordering is not strict, so repeated times are sorted; a timetable with any missing row time is not, and one with fewer than two rows has nothing out of order and so is. Only the row times are read: the variables take no part. This is the whole of the question issorted answers here. It takes no dimension, no direction and no options; use issortedrows to ask about a direction, about a variable, or about where missing values should fall. A table has no issorted, its row names being labels rather than an ordering. See also: timetable.issortedrows, sortrows, timetable # name: # type: sq_string # elements: 1 # length: 62 True when the row times of a timetable are in ascending order. # name: # type: sq_string # elements: 1 # length: 22 timetable.issortedrows # name: # type: sq_string # elements: 1 # length: 882 timetable: TF = issortedrows (tt) timetable: TF = issortedrows (tt, rowDimName) timetable: TF = issortedrows (tt, vars) timetable: TF = issortedrows (tt, ..., direction) timetable: TF = issortedrows (..., Name, Value) True when the rows of a timetable are already in a given order. TF = issortedrows (tt) returns true when the rows are in ascending order of their row times, which is the order sortrows would put them in with nothing else asked. Every form sortrows accepts is accepted here and asks the same question of it: the row dimension name, one or more variables named, indexed or selected, a direction, and the 'MissingPlacement' and 'ComparisonMethod' pairs. A direction must follow the keys it applies to, as it must there. The sort is stable, so the answer is exactly whether sorting would leave every row where it already is. See also: issorted, sortrows, timetable # name: # type: sq_string # elements: 1 # length: 63 True when the rows of a timetable are already in a given order. # name: # type: sq_string # elements: 1 # length: 21 timetable.istimetable # name: # type: sq_string # elements: 1 # length: 195 timetable: TF = istimetable (tt) True if input is a timetable, false otherwise. TF = istimetable (tt) always returns true for a timetable, irrespective of its size. See also: istable, istabular # name: # type: sq_string # elements: 1 # length: 46 True if input is a timetable, false otherwise. # name: # type: sq_string # elements: 1 # length: 14 timetable.join # name: # type: sq_string # elements: 1 # length: 1781 timetable: ttC = join (ttL, tblR) timetable: ttC = join (ttL, tblR, Name, Value) timetable: [ttC, index] = join (...) Join a timetable with another tabular object. ttC = join (ttL, tblR) returns a timetable with every row of ttL, its row times kept, and beside it the row of tblR whose key matches. The right operand contributes variables only, so the result is a timetable exactly as long as ttL. The key must name a row of tblR for every row of ttL, and the keys of tblR must be unique. With no key named, two timetables join on their row times; a timetable and a table have no key in common unless one is named, since the row times are not a variable. [ttC, index] = join (...) also returns index, naming the row of tblR each row took. The following Name/Value pairs are accepted: 'Keys' The variables to match on, named on both sides. The row dimension name names the row times. 'LeftKeys', 'RightKeys' The variables to match on, named separately for each side and given together. They must name the same number of keys. 'LeftVariables', 'RightVariables' The variables each side contributes. By default the left contributes all of its own and the right all but its keys. 'KeepOneCopy' Variables that both sides carry and only the left contributes. A variable name carried by both sides and contributed by both is suffixed with the caller's own name for each operand, falling back to _left and _right. Where an operand is an eventtable, or carries one on its Events property, the result is an event table too and the operands' event tables are merged: an outer join keyed on the event times and every variable the two share, ordered by those keys. The three properties saying which variables describe the events must agree, and the operation is refused where they do not. # name: # type: sq_string # elements: 1 # length: 45 Join a timetable with another tabular object. # name: # type: sq_string # elements: 1 # length: 13 timetable.lag # name: # type: sq_string # elements: 1 # length: 1106 timetable: ttB = lag (ttA) timetable: ttB = lag (ttA, n) Shift the data of a timetable along its own row times. ttB = lag (ttA) moves every value one row later, leaving the first row missing and dropping what fell off the end. The row times themselves do not move, so the values arrive against later times than the ones they were recorded at. n says how far, as a whole number of rows, as a duration that must be a multiple of the time step, or as a calendarDuration whose unit the timetable must be regular in. A negative n moves the values earlier instead, and a lag past the height of the timetable leaves every row missing. The timetable must be regular: a shift by rows only means anything when the rows are evenly spaced, and a timetable of one row or of none is not regular. A row left with no value takes the missing value of its variable's own type, which for a logical variable is false and for a character one a space, neither type having a missing value of its own. Every property of the timetable survives. See also: timetable.retime, timetable.synchronize, timetable.isregular, timetable # name: # type: sq_string # elements: 1 # length: 54 Shift the data of a timetable along its own row times. # name: # type: sq_string # elements: 1 # length: 19 timetable.mergevars # name: # type: sq_string # elements: 1 # length: 1257 timetable: ttB = mergevars (ttA, vars) timetable: ttB = mergevars (..., 'NewVariableName', name) timetable: ttB = mergevars (..., 'MergeAsTable', tf) Combine several variables of a timetable into one. ttB = mergevars (ttA, vars) replaces the variables in vars with a single multi-column one, put where the first of them was. It is named VarN unless 'NewVariableName' says otherwise, and the merged variables are no longer reachable by their old names. Nothing the merged variables carried about themselves survives: one variable carries one unit, one description, one continuity and one entry of a custom property describing the variables, and there is no saying which of them the merged variable should take, so it takes none: where the continuity is set at all, the merged variable's reads 'unset', and a property that was never set stays unset. The remaining variables keep theirs. The row times are not a variable and cannot be merged. Merging a variable of an eventtable that one of its three event properties names clears that designation, the merged variable being a different variable under a different name. MATLAB refuses the call instead, on the ground that the merged variable is no longer a column. See also: timetable.splitvars, timetable # name: # type: sq_string # elements: 1 # length: 50 Combine several variables of a timetable into one. # name: # type: sq_string # elements: 1 # length: 18 timetable.movevars # name: # type: sq_string # elements: 1 # length: 555 timetable: ttB = movevars (ttA, vars, 'Before', location) timetable: ttB = movevars (ttA, vars, 'After', location) Move variables within a timetable. ttB = movevars (ttA, vars, ...) puts the variables named in vars before or after location, which names or numbers another variable. Each variable takes its units, description and continuity with it. The row times are not a variable: they can be neither moved nor used as the location, and they stay where they are whatever else is reordered. See also: timetable.addvars, timetable.removevars, timetable # name: # type: sq_string # elements: 1 # length: 34 Move variables within a timetable. # name: # type: sq_string # elements: 1 # length: 15 timetable.ndims # name: # type: sq_string # elements: 1 # length: 241 timetable: N = ndims (tt) Number of dimensions in timetable. N = ndims (tt) always returns 2. A timetable is always a two-dimensional container of rows and variables, whatever the shape of the values held in those variables. See also: size # name: # type: sq_string # elements: 1 # length: 34 Number of dimensions in timetable. # name: # type: sq_string # elements: 1 # length: 15 timetable.numel # name: # type: sq_string # elements: 1 # length: 169 timetable: N = numel (tt) Number of elements in timetable. N = numel (tt) returns the number of rows times the number of variables in tt. See also: size, height, width # name: # type: sq_string # elements: 1 # length: 32 Number of elements in timetable. # name: # type: sq_string # elements: 1 # length: 19 timetable.outerjoin # name: # type: sq_string # elements: 1 # length: 1736 timetable: ttC = outerjoin (ttL, tblR) timetable: ttC = outerjoin (ttL, tblR, Name, Value) timetable: [ttC, iL, iR] = outerjoin (...) Join a timetable with another tabular object, keeping unmatched rows. ttC = outerjoin (ttL, tblR) returns a timetable holding one row for every pair of rows of ttL and tblR whose keys match, and one for every row of either that matched nothing, its variables from the other side filled with missing values. A row that came from a row of ttL carries its row time; a row that matched nothing on the left has none of its own. With no key named, two timetables join on their row times; a timetable and a table have no key in common unless one is named. [ttC, iL, iR] = outerjoin (...) also returns the rows of ttL and of tblR each row came from, 0 where it came from neither. The following Name/Value pairs are accepted: 'Keys', 'LeftKeys', 'RightKeys' The variables to match on. The row dimension name names the row times. 'LeftVariables', 'RightVariables' The variables each side contributes. By default each contributes all of its own, keys included. 'Type' 'full' (the default) keeps the unmatched rows of both sides, 'left' and 'right' only those of the side named. 'MergeKeys' A logical scalar. When true each pair of keys becomes a single variable in the left one's position, taking its value from whichever side had a row. Where an operand is an eventtable, or carries one on its Events property, the result is an event table too and the operands' event tables are merged: an outer join keyed on the event times and every variable the two share, ordered by those keys. The three properties saying which variables describe the events must agree, and the operation is refused where they do not. # name: # type: sq_string # elements: 1 # length: 69 Join a timetable with another tabular object, keeping unmatched rows. # name: # type: sq_string # elements: 1 # length: 23 timetable.overlapsrange # name: # type: sq_string # elements: 1 # length: 1066 timetable: TF = overlapsrange (tt, ref) timetable: [TF, whichRows] = overlapsrange (tt, ref) True when a timetable and a range of times have any instant in common. TF = overlapsrange (tt, ref) returns true when the range of tt, from its earliest to its latest row time, shares at least one instant with ref. Meeting at a single instant is enough: a range that starts exactly at the last row time overlaps. Whether the ends themselves count is ref's to say. A timerange that excludes its lower bound does not overlap a timetable that reaches only as far as that bound. ref takes the same three forms as in containsrange: a timerange, a timetable whose earliest and latest row times give the range, or a datetime or duration scalar naming one instant. [TF, whichRows] = overlapsrange (...) also returns a column of logicals saying which rows of tt fall in ref. A timetable can overlap a range without any of its rows falling in it, the overlap being of the ranges and not of the rows. See also: timetable.containsrange, timetable.withinrange, timerange, timetable # name: # type: sq_string # elements: 1 # length: 70 True when a timetable and a range of times have any instant in common. # name: # type: sq_string # elements: 1 # length: 15 timetable.pivot # name: # type: sq_string # elements: 1 # length: 1646 timetable: P = pivot (tt, Name, Value, ...) Summarise a timetable across two grouping dimensions. P = pivot (tt, 'Rows', rowvars, 'Columns', colvars) groups the rows of tt by rowvars down the page and by colvars across it, and returns a table holding one row per row group and one variable per column group. At least one of the two must be given. Either may name the row dimension, which groups by the row times: as 'Rows' they become a variable of the result, and as 'Columns' they name its variables. The result is a table and not a timetable, its rows and columns being groups rather than instants. With no 'DataVariable' the cells count the rows of each group; with one they summarise its values, by 'Method', which defaults to summing them. The following Name/Value pairs are accepted: 'Rows', 'Columns' The variables grouping down the page and across it, or the row dimension name. 'DataVariable' The variable summarised in each cell. 'Method' The summary applied to each cell, a method name or a function handle. 'RowsBinMethod', 'ColumnsBinMethod' Binning applied to the grouping variables before grouping, with 'IncludedEdge' saying which edge of a bin is included. 'IncludeMissingGroups', 'IncludeEmptyGroups' Whether a group of missing values, and whether an unused category, are reported. 'IncludeTotals' Whether a marginal row and column are added, labelled Overall_. 'RowLabelPlacement' Whether the row groups become a variable of the result or its row names. 'OutputFormat' 'flat' (the default) or 'nested', which groups the data variables into nested tables by the column hierarchy. See also: groupsummary, timetable # name: # type: sq_string # elements: 1 # length: 53 Summarise a timetable across two grouping dimensions. # name: # type: sq_string # elements: 1 # length: 20 timetable.removevars # name: # type: sq_string # elements: 1 # length: 706 timetable: ttB = removevars (ttA, vars) Remove variables from a timetable. ttB = removevars (ttA, vars) deletes the variables named, numbered, selected by a logical vector or picked out by a vartype. The row times are not a variable and cannot be removed: a timetable keeps them whatever else goes. Removing every variable leaves a timetable with none, still carrying its row times and its time step, rather than an empty one. Removing a variable of an eventtable that one of its three event properties names clears that designation, the name having nothing left to resolve to. Deleting the variable by assigning an empty matrix does the same. See also: timetable.addvars, timetable.movevars, timetable # name: # type: sq_string # elements: 1 # length: 34 Remove variables from a timetable. # name: # type: sq_string # elements: 1 # length: 20 timetable.renamevars # name: # type: sq_string # elements: 1 # length: 712 timetable: ttB = renamevars (ttA, vars, newNames) Rename variables in a timetable. ttB = renamevars (ttA, vars, newNames) gives each variable in vars the matching name in newNames. Everything else about the variable is kept, its units and its description included. Dimension names are not renamed here: assign to tt.Properties.DimensionNames for that. A variable cannot be given the row dimension's name either, the two sharing one namespace. Renaming a variable of an eventtable that one of its three event properties names carries that designation to the new name. MATLAB clears it instead and does not restore it when the variable is renamed back; see deviation D6. See also: timetable.movevars, timetable # name: # type: sq_string # elements: 1 # length: 32 Rename variables in a timetable. # name: # type: sq_string # elements: 1 # length: 17 timetable.repelem # name: # type: sq_string # elements: 1 # length: 641 timetable: tt2 = repelem (tt, rows, cols) Repeat each row and variable of a timetable in place. tt2 = repelem (tt, rows, cols) repeats each row of tt rows times and each variable cols times, keeping the repeats of a row together. Either count may be a vector with one count per row, or per variable, and a count of zero drops that row or variable. Both counts must be given, a timetable having exactly two dimensions. Each repeated row carries the row time of the row it came from, so the result has repeated row times and is generally irregular; each repeated variable takes a numbered name, A becoming A, A_1. See also: repmat, timetable # name: # type: sq_string # elements: 1 # length: 53 Repeat each row and variable of a timetable in place. # name: # type: sq_string # elements: 1 # length: 16 timetable.repmat # name: # type: sq_string # elements: 1 # length: 458 timetable: tt2 = repmat (tt, sz) timetable: tt2 = repmat (tt, rows, cols) Repeat a timetable as a block. tt2 = repmat (tt, rows, cols) repeats the whole timetable rows times downwards and cols times across. Given a single argument both counts take it. Each copy carries the row times it came from, so the result has repeated row times and is generally irregular; each repeated variable takes a numbered name, A becoming A, A_1. See also: repelem, timetable # name: # type: sq_string # elements: 1 # length: 30 Repeat a timetable as a block. # name: # type: sq_string # elements: 1 # length: 16 timetable.retime # name: # type: sq_string # elements: 1 # length: 4649 timetable: ttB = retime (ttA, newTimes) timetable: ttB = retime (ttA, newTimeStep) timetable: ttB = retime (..., method) timetable: ttB = retime (..., Name, Value) Resample a timetable onto new row times. ttB = retime (ttA, newTimes) returns a timetable whose rows sit at newTimes, a datetime or duration vector of the same kind as the row times of ttA, sorted and without repeats. ttB = retime (ttA, newTimeStep) builds those times instead from a time unit, one of 'secondly', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'quarterly' and 'yearly'. The grid starts at the unit's own boundary at or before the first row time. A filling method reads the times as sample points and runs the grid to the first boundary at or after the last row time, so three hours of data retimed 'daily' spans two days; an aggregating method reads them as bins and stops at the bin holding the last row time, so the same three hours give one day. Weeks begin on Sunday. A duration row time carries no calendar, so a unit measures it only where a duration counts in that unit: 'secondly', 'minutely', 'hourly', 'daily' and 'yearly' are taken and 'weekly', 'monthly' and 'quarterly' are refused. ttB = retime (ttA, 'regular', ...) builds a grid from the first row time in steps of the 'TimeStep' or 'SampleRate' given after it, exactly one of which is required. 'SampleRate' is in hertz. It is bounded by the same rule. method says how a row that the old times do not carry takes its value. 'fillwithmissing' leaves it missing, a character variable blank, where MATLAB puts char (0), and a logical or cell variable, which has no missing value, is refused; 'previous', 'next' and 'nearest' copy a neighbouring value; and 'linear', 'spline', 'pchip' and 'makima' interpolate, which restricts the timetable to numeric, datetime and duration variables. 'default', which is also what no method at all means, reads VariableContinuity and treats a 'continuous' variable as 'linear', a 'step' one as 'previous', and an 'unset' or 'event' one as 'fillwithmissing'. method may instead aggregate the rows the target times gather into each bin, as 'sum', 'prod', 'mean', 'median', 'mode', 'min', 'max', 'count', 'firstvalue' or 'lastvalue', or as a function handle taking the rows of one bin and returning a single row. A bin holding no row answers 0 for 'sum', 1 for 'prod', 0 for 'count' and missing for the rest, and a function handle is called on it too, with an empty slice of the variable's own type. Every method but 'count', 'firstvalue' and 'lastvalue' needs a numeric, logical, datetime or duration variable. 'IncludedEdge' says which edge of a bin belongs to it, 'left' by default, under which the last bin runs on past the last target time; 'right' instead lets the first bin run back before the first. A grid built from a unit or a step moves with it: closed on the right it starts at the first bin that can hold a row and ends one unit past the last row time. It is read only by an aggregating method. An option the chosen method cannot use is warned about and ignored rather than dropped in silence, once per call. MATLAB says nothing in that case. An aggregation reads the row times as bins rather than as sample points, so it accepts row times that repeat and row times out of order, both of which the other methods refuse. 'fillwithconstant' gives every added row the value of 'Constant', which is required with it and must be a value of the variable's own kind: a number for a numeric or logical variable, a duration for a duration one, and text for a string or a categorical one, where a name the variable does not yet carry is added to its categories. A number standing for a duration or for text is refused rather than read as days or as digits. Needing no missing value of its own, it serves a variable that has none. 'EndValues' says what a target time outside the span of the old ones takes, either 'extrap' or a constant. It is 'extrap' by default, under which 'linear' and the interpolating methods extrapolate and the neighbouring methods carry the nearest known value outward. A value that was missing before the call is a gap like any other and is filled with them: a row the new times keep is resampled along with the rest, so 'previous' gives it the last value that was not missing rather than the missing value it held. Only 'fillwithmissing' leaves such a value alone. Every property of the timetable survives, VariableUnits, VariableDescriptions and VariableContinuity included, and the result carries the time step its new row times imply. See also: timetable, timetable.isregular, timetable.fillmissing, timetable.synchronize # name: # type: sq_string # elements: 1 # length: 40 Resample a timetable onto new row times. # name: # type: sq_string # elements: 1 # length: 19 timetable.rmmissing # name: # type: sq_string # elements: 1 # length: 1405 timetable: ttB = rmmissing (ttA) timetable: ttB = rmmissing (ttA, dim) timetable: ttB = rmmissing (..., Name, Value) timetable: [ttB, TF] = rmmissing (...) Remove the incomplete rows or variables of a timetable. ttB = rmmissing (ttA) removes every row holding a missing value. rmmissing (ttA, 2) removes every variable holding one instead, and keeps every row and its row time. A row whose row time is missing is removed whatever its variables hold, and whatever 'DataVariables' or 'MinNumMissing' say: a row that cannot be placed in time is not a row a timetable can keep. That is a precondition rather than a report of missingness, which is why ismissing does not mark such a row and anymissing does not count it. 'DataVariables' names the variables to judge a row by, and 'MinNumMissing' how many missing values a row must hold before it goes, or with dim equal to 2 how many a variable must. 'MissingLocations' marks the missing values in place of ismissing, as a logical matrix or a table, exactly as for table.rmmissing. None of them reaches the row times. [ttB, TF] = rmmissing (...) also returns a logical column marking the rows that were removed, the ones dropped for their row times included, or with dim equal to 2 a logical row marking the removed variables. The time step is read afresh from the rows that survive. See also: timetable.ismissing, timetable.standardizeMissing, timetable # name: # type: sq_string # elements: 1 # length: 55 Remove the incomplete rows or variables of a timetable. # name: # type: sq_string # elements: 1 # length: 16 timetable.rmprop # name: # type: sq_string # elements: 1 # length: 393 timetable: tt = rmprop (tt, propertyNames) Remove custom properties from a timetable. tt = rmprop (tt, propertyNames) removes the named properties carrying custom metadata from the timetable tt. propertyNames is a character vector, a cell array of character vectors, or a string array. A name matching no custom property is ignored rather than refused. See also: timetable.addprop, timetable # name: # type: sq_string # elements: 1 # length: 42 Remove custom properties from a timetable. # name: # type: sq_string # elements: 1 # length: 16 timetable.rowfun # name: # type: sq_string # elements: 1 # length: 2514 timetable: B = rowfun (func, A) timetable: B = rowfun (func, A, Name, Value, ...) Apply a function to each row of a timetable. B = rowfun (func, A) applies the function handle func to each row of the timetable A and returns the results in the timetable B, which has one row for each row of A and carries its row times. By default the value of each variable in the row is passed to func as a separate input argument, and the output variables of B are named Var1, Var2, and so on. B = rowfun (func, A, Name, Value, ...) modifies the operation through the following Name/Value pairs: 'InputVariables' The variables of A that are passed to func, given as variable names, indices, a logical vector, or a function handle. By default every variable of A that is not a grouping variable is used. 'GroupingVariables' One or more variables of A that define groups of rows, or the row dimension name, which groups by the row times themselves. func is then applied once to each group, receiving the values of each input variable across the rows of the group, and B has one row per group, carrying the row time of the first row of the group. Grouping variables appear in B alongside a GroupCount variable; the row times do not, being the row times of the result. Rows with a missing value in any grouping variable are omitted. 'OutputVariableNames' The names of the output variables of B, one per output of func. 'NumOutputs' The number of output arguments to request from func. It defaults to the number of 'OutputVariableNames' if those are given, otherwise to 1. 'SeparateInputs' A logical scalar. When true (the default), the value of each input variable is passed to func as a separate argument. When false, the values of the row are joined into one array, as table2array joins the variables, and passed as a single argument. Variables that cannot form one array are refused. 'ExtractCellContents' A logical scalar. When true, the contents of cell-valued variables are extracted before being passed to func. It defaults to false. 'OutputFormat' One of 'table' (the default, and also selected by 'auto'), which returns the results in a timetable; 'uniform', which requires func to return a scalar and returns them in an array; or 'cell', which returns them in a cell array. 'ErrorHandler' A function handle called whenever func raises an error, receiving a structure with fields identifier, message and index, followed by the arguments func was called with. Its outputs are used in place of the ones func did not return. # name: # type: sq_string # elements: 1 # length: 44 Apply a function to each row of a timetable. # name: # type: sq_string # elements: 1 # length: 19 timetable.rows2vars # name: # type: sq_string # elements: 1 # length: 1215 timetable: tbl = rows2vars (tt) timetable: tbl = rows2vars (tt, Name, Value) Turn the rows of a timetable into variables. tbl = rows2vars (tt) returns a table whose rows are the variables of tt and whose variables are its rows. The first variable, OriginalVariableNames, names the variables of tt; the rest are named for the row times they came from, rendered as the timetable displays them and made into valid names, and the second dimension of the result takes the name of the row dimension. The result is a table and not a timetable: its rows are variables and there is no longer a time to label them by. Where the variables are not all of one type the result holds cell arrays, every value being wrapped so that one variable can carry them all. The following Name/Value pairs are accepted: 'DataVariables' The variables of tt that become rows. By default all of them do. 'VariableNamesSource' A variable of tt whose values name the new variables instead of the row times. A repeated name is numbered rather than refused, and the second dimension of the result takes the name of that variable. 'VariableNamingRule' 'modify' (the default) makes each new name a valid identifier; 'preserve' keeps it as it is. # name: # type: sq_string # elements: 1 # length: 44 Turn the rows of a timetable into variables. # name: # type: sq_string # elements: 1 # length: 14 timetable.size # name: # type: sq_string # elements: 1 # length: 412 timetable: sz = size (tt) timetable: dim_sz = size (tt, dim) timetable: [rows, columns] = size (tt) Size of a timetable. sz = size (tt) returns a two-element row vector with the number of rows and the number of variables in tt. dim_sz = size (tt, dim) returns the size along the dimension dim. A timetable has two dimensions, so any dimension above the second has size 1. See also: height, width, ndims, numel # name: # type: sq_string # elements: 1 # length: 20 Size of a timetable. # name: # type: sq_string # elements: 1 # length: 18 timetable.sortrows # name: # type: sq_string # elements: 1 # length: 2158 timetable: ttB = sortrows (ttA) timetable: ttB = sortrows (ttA, rowDimName) timetable: ttB = sortrows (ttA, vars) timetable: ttB = sortrows (ttA, ..., direction) timetable: ttB = sortrows (..., Name, Value) timetable: [ttB, index] = sortrows (...) Sort the rows of a timetable. ttB = sortrows (ttA) orders the rows by their row times, earliest first. Rows sharing a time keep the order they were in: the row times alone decide, and the variables are never consulted to break a tie. ttB = sortrows (ttA, rowDimName) does the same, naming the row times through the first of ttA.Properties.DimensionNames. That name is the only way to reach them: a numeric index counts the variables, so sortrows (ttA, 1) orders by the first variable and not by the times, and there is no index that means the row times. Renaming the row dimension renames the key with it. ttB = sortrows (ttA, vars) orders by one or more variables, named, indexed by number, selected by a logical vector or picked out by a vartype. A negative index sorts that variable in descending order. The row dimension name may appear among vars, in which case the row times take their turn as a key like any other. ttB = sortrows (..., direction) sorts as 'ascend' or 'descend', either one direction for every key or one per key. A direction must follow the keys it applies to, so sortrows (ttA, 'descend') is an error rather than a reversed sort: with nothing else given, the first argument is read as the variables to sort by. The name-value pair 'MissingPlacement' takes 'auto' (the default), 'first' or 'last' and says where missing keys go. 'auto' puts them last when sorting up and first when sorting down. 'ComparisonMethod' takes 'auto', 'real' or 'abs' and applies to numeric variables. Both must follow the keys as well. [ttB, index] = sortrows (...) also returns the permutation, so that ttA(index,:) is ttB. The time step is read afresh from the sorted row times rather than carried over, so sorting an out-of-order hourly timetable makes it regular again, and sorting one that ran backwards turns its step from minus one hour to plus one. See also: timetable.issortedrows, timetable # name: # type: sq_string # elements: 1 # length: 29 Sort the rows of a timetable. # name: # type: sq_string # elements: 1 # length: 20 timetable.splitapply # name: # type: sq_string # elements: 1 # length: 653 timetable: Y = splitapply (func, tt, G) timetable: [Y1, ...] = splitapply (func, tt, G) Apply a function to each group of rows of a timetable. Y = splitapply (func, tt, G) splits the rows of tt into the groups the numbers in G name, calls func once per group with one argument per variable holding that group's rows, and stacks the results. G holds one number per row of tt, as findgroups returns; a row numbered NaN belongs to no group and is left out. The row times take no part: func receives the variables alone. [Y1, ...] = splitapply (...) asks func for as many outputs as are requested and stacks each of them. See also: findgroups, timetable # name: # type: sq_string # elements: 1 # length: 54 Apply a function to each group of rows of a timetable. # name: # type: sq_string # elements: 1 # length: 19 timetable.splitvars # name: # type: sq_string # elements: 1 # length: 677 timetable: ttB = splitvars (ttA) timetable: ttB = splitvars (ttA, vars) timetable: ttB = splitvars (..., 'NewVariableNames', newNames) Split multi-column variables of a timetable into one each. ttB = splitvars (ttA) splits every multi-column variable, and every nested table, into one variable per column. ttB = splitvars (ttA, vars) splits only those named. The new variables are named after the one they came from with a column number appended, unless 'NewVariableNames' gives them names. Splitting a variable that was merged does not bring back the units the merge discarded. The row times are not a variable and cannot be split. See also: timetable.mergevars, timetable # name: # type: sq_string # elements: 1 # length: 58 Split multi-column variables of a timetable into one each. # name: # type: sq_string # elements: 1 # length: 15 timetable.stack # name: # type: sq_string # elements: 1 # length: 1490 timetable: tt2 = stack (tt, vars) timetable: tt2 = stack (tt, vars, Name, Value) timetable: [tt2, index] = stack (...) Stack several variables of a timetable into one. tt2 = stack (tt, vars) replaces the variables named in vars with a single variable holding their values one under the other, and an indicator variable naming which of them each value came from. Every other variable is carried along, repeated once per stacked variable, and so is the row time of the row it came from: a timetable of two rows stacking two variables has four rows and two pairs of equal row times, which generally makes it irregular. vars names the variables to stack. A cell array of variable references stacks several groups at once, one new variable per group, each group holding the same number of variables. The stacked variables are joined as table2array joins them, and must each have one column; variables that cannot join are refused. [tt2, index] = stack (...) also returns index, naming the row of tt each row of tt2 came from. The following Name/Value pairs are accepted: 'ConstantVariables' The variables carried along unstacked. By default every variable that is not being stacked is carried. 'NewDataVariableName' The name of the stacked variable, one per group. By default the names of the stacked variables are joined with underscores. 'IndexVariableName' The name of the indicator variable. By default it is the name of the stacked variable followed by _Indicator. See also: timetable # name: # type: sq_string # elements: 1 # length: 48 Stack several variables of a timetable into one. # name: # type: sq_string # elements: 1 # length: 28 timetable.standardizeMissing # name: # type: sq_string # elements: 1 # length: 809 timetable: ttB = standardizeMissing (ttA, indicator) timetable: ttB = standardizeMissing (..., 'DataVariables', vars) Make given values missing in a timetable. ttB = standardizeMissing (ttA, indicator) replaces every value matching indicator with the missing value of its own type, so that ismissing and rmmissing will afterwards treat it as missing. Each indicator applies only to the variables of its own kind, as table.standardizeMissing describes: numbers to floating point variables, text to text and categorical variables, and duration and datetime indicators to variables of their own type. 'DataVariables' restricts it to the variables named. The row times are not a data variable and are never rewritten, so the time step is unchanged. See also: timetable.ismissing, timetable.rmmissing, timetable # name: # type: sq_string # elements: 1 # length: 41 Make given values missing in a timetable. # name: # type: sq_string # elements: 1 # length: 17 timetable.summary # name: # type: sq_string # elements: 1 # length: 1338 timetable: summary (tt) timetable: s = summary (tt) Summarise a timetable. summary (tt) prints what the timetable holds: its size, its description where it has one, the row times, the variables with their types and any units and descriptions, and a table of statistics for everything that has any. s = summary (tt) returns that as a structure instead, one field per variable and one for the row times, filed under the row dimension's name and coming first. The row times report where they start, how fast they run and how far apart they step, in StartTime, SampleRate and TimeStep, alongside the statistics a datetime or duration variable would report. They carry no description, units or continuity, having none. Which statistics a variable reports follows its type. Numeric, datetime and duration variables report the count of missing values, the smallest, the median, the largest, the mean and the standard deviation; an integer reports no deviation and its median rounds to its own type; an ordinal categorical is ordered but has no mean; a plain one reports its categories and their counts; a logical reports how many are true and false and appears in no statistics; and everything else reports the count of missing values alone. A variable of several columns reports one row per column. See also: timetable.ismissing, timetable # name: # type: sq_string # elements: 1 # length: 22 Summarise a timetable. # name: # type: sq_string # elements: 1 # length: 20 timetable.syncevents # name: # type: sq_string # elements: 1 # length: 1685 timetable: tt2 = syncevents (tt) timetable: tt2 = syncevents (tt, defaultLabel) timetable: tt2 = syncevents (..., 'EventDataVariables', vars) Copy the attached event table's data onto the rows it covers. tt2 = syncevents (tt) adds the variables of the event table attached to tt as variables of tt, each row taking the values of the event covering its time. A row no event covers takes missing values. tt must have an event table attached. An event covers the times from its own onwards and stops short of its end, so an event beginning at 02:00 and lasting two hours covers 02:00 and 03:00 but not 04:00, and an event of zero length covers nothing. An event with neither a length nor an end is an instant and covers only a row at exactly its own time. A row covered by more than one event is repeated, once per event, which generally makes the result irregular. By default every variable of the event table is copied except the one holding the event lengths or ends, which describes the events rather than what happened. 'EventDataVariables' names the variables to copy instead, and replaces that default rather than adding to it, so it can ask for the lengths and can leave the labels out. tt2 = syncevents (tt, defaultLabel) gives defaultLabel to the rows no event covers, in place of a missing label. It reaches the labels variable alone; every other copied variable stays missing there. A copied variable whose name tt already uses has both of them renamed, the timetable's with a _tt suffix and the event table's with _et, so neither is lost to the other. The result keeps the event table it was synchronised from. See also: timetable.extractevents, eventtable, timetable # name: # type: sq_string # elements: 1 # length: 61 Copy the attached event table's data onto the rows it covers. # name: # type: sq_string # elements: 1 # length: 21 timetable.synchronize # name: # type: sq_string # elements: 1 # length: 1991 timetable: tt = synchronize (tt1, tt2) timetable: tt = synchronize (tt1, ..., ttN) timetable: tt = synchronize (..., newTimes) timetable: tt = synchronize (..., method) timetable: tt = synchronize (..., Name, Value) Put several timetables on one set of row times and set them side by side. tt = synchronize (tt1, tt2) returns a timetable holding the variables of both, at every row time either of them carries. Any number of timetables may be given, and they must all have row times of the same type. newTimes says where the rows of the answer sit. It is a datetime or duration vector, one of the time units retime takes, 'regular' with a 'TimeStep' or a 'SampleRate', or one of: 'union' Every time any of them carries. This is the default. 'intersection' Only the times every one of them carries. 'commonrange' Every time any of them carries that lies within the span they all cover. 'first', 'last' The row times of the first or of the last timetable given. A timetable with no rows says nothing about where the answer should sit and takes no part in choosing. method and the options are retime's, and one method serves every timetable: a method for each is not accepted. Two timetables carrying a variable of the same name have both of them renamed for the timetable they came from, so a from tt1 and tt2 become a_tt1 and a_tt2. An operand that is an expression rather than a variable is known by its place in the call. Operands with the same name, as a timetable synchronized with itself, number the later names (a_tt, a_tt_1), as in MATLAB. Where an operand is an eventtable, or carries one on its Events property, the result is an event table too and the operands' event tables are merged: an outer join keyed on the event times and every variable the two share, ordered by those keys. The three properties saying which variables describe the events must agree, and the operation is refused where they do not. See also: timetable.retime, timetable, horzcat, timetable.isregular # name: # type: sq_string # elements: 1 # length: 73 Put several timetables on one set of row times and set them side by side. # name: # type: sq_string # elements: 1 # length: 21 timetable.table2array # name: # type: sq_string # elements: 1 # length: 422 timetable: A = table2array (tt) Converts a timetable to an array. A = table2array (tt) places the variables of tt side by side and joins them into one array A, as table.table2array joins the variables of a table. The row times and every other property of tt are left out. tt{:,:} and tt.Variables return the same array. See also: timetable.table2cell, timetable.table2struct, timetable.timetable2table, table.table2array # name: # type: sq_string # elements: 1 # length: 33 Converts a timetable to an array. # name: # type: sq_string # elements: 1 # length: 20 timetable.table2cell # name: # type: sq_string # elements: 1 # length: 413 timetable: C = table2cell (tt) Converts a timetable to a cell array. C = table2cell (tt) returns a cell array C of the same size as tt, in which each cell holds one row of one variable, in the variable's own type, as table.table2cell does for a table. The row times and every other property of tt are left out. See also: timetable.table2array, timetable.table2struct, timetable.timetable2table, table.table2cell # name: # type: sq_string # elements: 1 # length: 37 Converts a timetable to a cell array. # name: # type: sq_string # elements: 1 # length: 22 timetable.table2struct # name: # type: sq_string # elements: 1 # length: 600 timetable: S = table2struct (tt) timetable: S = table2struct (tt, 'ToScalar', true) Converts a timetable to a scalar structure or structure array. S = table2struct (tt) returns a structure array with the same fields as the variables in tt. The length of S is the same as the height of tt. S = table2struct (tt, 'ToScalar', true) returns a scalar structure with the same fields as the variables in tt. Each field has the same rows as tt. The row times and every other property of tt are left out. See also: timetable.table2array, timetable.table2cell, timetable.timetable2table, table.table2struct # name: # type: sq_string # elements: 1 # length: 62 Converts a timetable to a scalar structure or structure array. # name: # type: sq_string # elements: 1 # length: 14 timetable.tail # name: # type: sq_string # elements: 1 # length: 713 timetable: tail (tt) timetable: tail (tt, k) timetable: out = tail (tt, k) Display or return the last k rows of a timetable. tail (tt) displays the last eight rows of tt, or all of them if it has fewer. tail (tt, k) displays the last k instead. k must be a real, nonnegative, integer scalar value; a k of zero displays no rows at all rather than raising. out = tail (tt, k) returns those rows in a new timetable instead of displaying them. If k is omitted or empty it defaults to eight. The rows come back in the order the timetable holds them, and they are the last rows rather than the latest ones. The time step is read afresh from the row times that are kept. See also: timetable.head, sortrows, timetable # name: # type: sq_string # elements: 1 # length: 49 Display or return the last k rows of a timetable. # name: # type: sq_string # elements: 1 # length: 19 timetable.timetable # name: # type: sq_string # elements: 1 # length: 2420 timetable: tt = timetable (rowTimes, var1, ..., varN) timetable: tt = timetable (var1, ..., varN, 'RowTimes', rowTimes) timetable: tt = timetable (var1, ..., varN, 'TimeStep', dt) timetable: tt = timetable (var1, ..., varN, 'SampleRate', fs) timetable: tt = timetable (..., 'StartTime', t0) timetable: tt = timetable ('Size', sz, 'VariableTypes', varTypes, ...) timetable: tt = timetable (..., 'VariableNames', varNames) timetable: tt = timetable (..., 'DimensionNames', dimNames) Create a new timetable. tt = timetable (rowTimes, var1, ..., varN) creates a timetable whose rows are labelled by rowTimes, a datetime or duration vector with one element for each row, and whose variables are the remaining input arguments. Variable names are taken from the names of the input variables, and the row dimension is named after rowTimes itself. tt = timetable (var1, ..., varN, 'RowTimes', rowTimes) does the same with the row times given as a Name-Value pair, in which case the row dimension is named 'Time'. tt = timetable (var1, ..., varN, 'TimeStep', dt) generates the row times instead of taking them, starting at t0 and stepping by dt, a duration or calendarDuration scalar. A calendar step requires a datetime start time. tt = timetable (var1, ..., varN, 'SampleRate', fs) generates them at fs rows per second, which is the same as a TimeStep of seconds (1 / fs). tt = timetable (..., 'StartTime', t0) sets the time of the first row for either of the two generating forms. It defaults to a zero duration in the units of the step, so that a timetable generated without one is keyed by elapsed time rather than by date. tt = timetable ('Size', sz, 'VariableTypes', varTypes, ...) creates a preallocated timetable of the given size, filled with the default value of each type. sz must be a two-element numeric array giving the number of rows and of variables. The row times must still be supplied, by any one of 'RowTimes', 'TimeStep' or 'SampleRate'. tt = timetable (..., 'VariableNames', varNames) specifies the variable names to use, as a cell array of character vectors or a string array with one nonempty and unique element per variable. tt = timetable (..., 'DimensionNames', dimNames) specifies the two dimension names to use, naming the rows and the variables. tt = timetable () returns an empty timetable with 0 rows and 0 variables. See also: table.table2timetable, array2timetable, timetable.isregular # name: # type: sq_string # elements: 1 # length: 23 Create a new timetable. # name: # type: sq_string # elements: 1 # length: 23 timetable.timetable2csv # name: # type: sq_string # elements: 1 # length: 2355 timetable: timetable2csv (tt, file) timetable: timetable2csv (tt, file, Name, Value) Write a timetable to a comma-separated-value (CSV) file. timetable2csv (tt, file) writes the timetable tt to file, which may be a character vector, a cellstr, or a string scalar. The resulting file can be read back with csv2timetable. The file begins with a comment line reporting how many consecutive rows hold the variable types, names, descriptions, and units, in that order. Those header rows are followed by one row of data per timetable row. The variables are serialized exactly as table2csv serializes them. The row times lead the file as a column of their own, written in ISO 8601 form so that they are exact whatever their display format. The column is tagged RowTimes in the variable-type row, followed by the row times' own type, their TimeZone where they have one, and their Format; the row dimension name travels in the variable-name row beside it. A zone-aware datetime and a duration of any resolution therefore both come back exactly as they went out, which writetimetable does not manage for either. The row times are not optional: a timetable without them is not one, so there is no switch to leave them out. The following Name-Value options are supported: Name Value 'WriteVariableNames' A logical scalar specifying whether the variable names are written (default true). When false the file carries none, so csv2timetable numbers the variables on read and can no longer group the columns: a multicolumn variable comes back as separate variables and a nested table as flat columns. The row dimension name is a name too and goes with them, so the row times come back under the default Time. A CSV file holds one table and has nowhere to put a second, so an attached event table is not written and nothing warns. Write the timetable to an OpenDocument spreadsheet with timetable2ods to keep its events. TimeStep, SampleRate and VariableContinuity are not written either; the first two are worked out again from the row times on read. Note the following round-trip limitation when reading the file back with csv2timetable: calendarDuration and categorical variables are returned as cell arrays of character vectors and their values are not reconstructed. See also: csv2timetable, timetable.timetable2ods, timetable.writetimetable, table.table2csv # name: # type: sq_string # elements: 1 # length: 56 Write a timetable to a comma-separated-value (CSV) file. # name: # type: sq_string # elements: 1 # length: 23 timetable.timetable2ods # name: # type: sq_string # elements: 1 # length: 2736 timetable: timetable2ods (tt, file) timetable: timetable2ods (tt, file, Name, Value) Write a timetable to an OpenDocument spreadsheet file. timetable2ods (tt, file) writes the timetable tt to file, which may be a character vector, a cellstr, or a string scalar. When file ends in .ods a compressed (ZIP-packaged) OpenDocument spreadsheet is written; when it ends in .fods a flat (single-XML) one is written instead. The resulting file can be read back with ods2timetable. The data sheet is headed by the row dimension name and the variable names and then carries one natively typed cell per value, and a hidden __datatypes_meta__ sheet carries the variable types, descriptions and units, exactly as table2ods writes them. The row times lead the sheet as a column of their own, written as native date or time cells as a datetime or duration variable is, so a spreadsheet application shows them as times rather than as text. The column is tagged RowTimes in the hidden sheet, followed by the row times' own type, their TimeZone where they have one, and their Format, so that all three come back exactly. The row times are not optional: a timetable without them is not one, so there is no switch to leave them out. The following Name-Value options are supported: Name Value 'Sheet' The name of the sheet to write (default 'Sheet1'). When file already exists the named sheet is added or replaced while every other sheet is preserved, so a workbook can be built up one object at a time. 'WriteVariableNames' A logical scalar specifying whether the variable names are written (default true). When false the file carries none at all, the hidden metadata sheet included, so ods2timetable numbers the variables on read and can no longer group the columns. The row dimension name is a name too and goes with them. 'WriteMode' 'overwritesheet' or 'inplace' replace the sheet (the default when the sheet exists), 'append' appends the timetable's rows to it, and 'replacefile' discards any existing file. An attached event table is written too, on a sheet of its own named _Events, with a ## Events crossref: line in the hidden metadata sheet tying the two together and carrying the event table's three variable designations. ods2timetable reads that sheet back and attaches it, and skips it when choosing which sheet to read. This is the only file format of the package that carries events: timetable2csv and writetimetable drop them. Note the following round-trip limitation when reading the file back with ods2timetable: calendarDuration and categorical variables are returned as cell arrays of character vectors and their values are not reconstructed. See also: ods2timetable, timetable.timetable2csv, struct2ods, table.table2ods # name: # type: sq_string # elements: 1 # length: 54 Write a timetable to an OpenDocument spreadsheet file. # name: # type: sq_string # elements: 1 # length: 25 timetable.timetable2table # name: # type: sq_string # elements: 1 # length: 985 timetable: tbl = timetable2table (tt) timetable: tbl = timetable2table (tt, 'ConvertRowTimes', tf) Convert a timetable to a table. tbl = timetable2table (tt) converts the timetable tt to a table whose first variable is the row times, named after the row dimension they came from. The dimension names of the result are the defaults, 'Row' and 'Variables', a table labelling its rows by name rather than by time. tbl = timetable2table (tt, 'ConvertRowTimes', tf) keeps the row times as that first variable when tf is true, which is the default, and discards them when it is false: the result has only the variables, and the times are gone rather than kept elsewhere. An event table attached to tt is not carried across, a table having nowhere to keep one, and converting the result back with table2timetable does not bring it back. An eventtable given here becomes a plain table like any other timetable. See also: table.table2timetable, array2timetable, timetable, table, eventtable # name: # type: sq_string # elements: 1 # length: 31 Convert a timetable to a table. # name: # type: sq_string # elements: 1 # length: 18 timetable.topkrows # name: # type: sq_string # elements: 1 # length: 1553 timetable: ttB = topkrows (ttA, k) timetable: ttB = topkrows (ttA, k, vars) timetable: ttB = topkrows (ttA, k, vars, direction) timetable: ttB = topkrows (..., Name, Value) timetable: [ttB, index] = topkrows (...) The top k rows of a timetable, in sorted order. ttB = topkrows (ttA, k) returns the k rows with the latest row times, latest first. This is where it parts company with head, which takes the first rows as they are stored: topkrows ranks them. ttB = topkrows (ttA, k, vars) ranks by one or more variables instead, named, indexed by number, selected by a logical vector or picked out by a vartype. The row dimension name may be used to rank by the row times explicitly. A numeric index counts the variables, and its sign is read the other way round from sortrows: a positive index ranks downwards. ttB = topkrows (..., direction) sorts as 'ascend' or 'descend', the latter being the default and what makes these the top rows rather than the bottom ones. Missing keys rank last however the sort runs, which is the one place topkrows differs from sortrows beyond its direction. 'MissingPlacement' overrides that, and 'ComparisonMethod' applies to numeric variables as usual. Asking for more rows than there are returns all of them, still ranked, and a k of zero returns none. [ttB, index] = topkrows (...) also returns the rows chosen, so that ttA(index,:) is ttB. The time step is read afresh from the row times that are kept, so the default ranking of a regular timetable steps backwards. See also: timetable.head, sortrows, timetable # name: # type: sq_string # elements: 1 # length: 47 The top k rows of a timetable, in sorted order. # name: # type: sq_string # elements: 1 # length: 16 timetable.unique # name: # type: sq_string # elements: 1 # length: 1236 timetable: ttB = unique (ttA) timetable: ttB = unique (ttA, setOrder) timetable: [ttB, ia, ic] = unique (...) Unique rows of a timetable. ttB = unique (ttA) returns the distinct rows of ttA, ordered by row time. A row is its time together with its values, so two rows count as one only when they agree on both. Rows sharing a time but differing in a variable are all kept, as are rows sharing their values at different times; the row times are a column of the comparison and not a key that overrides it. ttB = unique (ttA, setOrder) chooses the order of the result. 'sorted' is the default and 'stable' keeps the rows in the order they were met. 'first' and 'last' say which of a set of equal rows is the one reported in ia. 'rows' is accepted and changes nothing, rows being the only thing a timetable compares. [ttB, ia, ic] = unique (...) also returns index vectors, such that ttB is ttA(ia,:) and ttA is ttB(ic,:). A timetable with no variables is compared on its row times alone, so repeated times reduce to one. The time step is read afresh from the row times that survive. Variables of cell, other than a cell array of character vectors, and of struct have no order to compare and are refused. See also: sortrows, timetable # name: # type: sq_string # elements: 1 # length: 27 Unique rows of a timetable. # name: # type: sq_string # elements: 1 # length: 17 timetable.unstack # name: # type: sq_string # elements: 1 # length: 1391 timetable: tt2 = unstack (tt, vars, ivar) timetable: tt2 = unstack (tt, vars, ivar, Name, Value) timetable: [tt2, index] = unstack (...) Spread one variable of a timetable across several. tt2 = unstack (tt, vars, ivar) replaces the variable named in vars with one variable per distinct value of the indicator variable ivar, each holding the values that carried that indicator. Rows sharing a group become one row. The row times group: with no 'GroupingVariables' given, one row comes back per distinct row time, so tt2 is a timetable carrying those times. A group that has no value for an indicator is filled with a missing value. [tt2, index] = unstack (...) also returns index, naming a row of tt in each group. The following Name/Value pairs are accepted: 'GroupingVariables' The variables whose combinations define the rows of the result, alongside the row times. 'ConstantVariables' The variables carried along unchanged, taken from one row of each group. 'NewDataVariableNames' The names of the new variables, one per distinct indicator value. 'AggregationFunction' The function applied where a group holds several values for one indicator. By default numeric data are summed and everything else must be unique within its group. 'VariableNamingRule' 'modify' (the default) makes each new name a valid identifier; 'preserve' keeps it as it is. See also: timetable.stack, timetable # name: # type: sq_string # elements: 1 # length: 50 Spread one variable of a timetable across several. # name: # type: sq_string # elements: 1 # length: 16 timetable.varfun # name: # type: sq_string # elements: 1 # length: 1819 timetable: B = varfun (func, A) timetable: B = varfun (func, A, Name, Value, ...) Apply a function to each variable of a timetable. B = varfun (func, A) applies the function handle func separately to each variable of the timetable A and returns the results in the timetable B. Each variable is passed whole, so a function that reduces a column to a scalar gives a one-row result. The output variables are named for the function and the variable they came from, as in mean_Speed, and every row of the result carries the row time of the first row it was computed from. B = varfun (func, A, Name, Value, ...) modifies the operation through the following Name/Value pairs: 'InputVariables' The variables of A that func is applied to, given as variable names, indices, a logical vector, or a function handle. By default every variable of A that is not a grouping variable is used. 'GroupingVariables' One or more variables of A that define groups of rows, or the row dimension name, which groups by the row times themselves. func is then applied once to each group and B has one row per group, carrying the row time of the first row of the group. Grouping variables appear in B alongside a GroupCount variable; the row times do not, being the row times of the result. Rows with a missing value in any grouping variable are omitted. 'OutputFormat' One of 'table' (the default, and also selected by 'auto'), which returns the results in a timetable; 'uniform', which requires func to return a scalar and returns them in an array; or 'cell', which returns them in a cell array. 'ErrorHandler' A function handle called whenever func raises an error, receiving a structure with fields identifier, message and index, followed by the arguments func was called with. Its outputs are used in place of the ones func did not return. # name: # type: sq_string # elements: 1 # length: 49 Apply a function to each variable of a timetable. # name: # type: sq_string # elements: 1 # length: 17 timetable.vertcat # name: # type: sq_string # elements: 1 # length: 1158 timetable: tt = vertcat (tt1, tt2, ...) Concatenate timetables vertically. tt = vertcat (tt1, tt2, ...) stacks the rows of its inputs, which must all have the same variable names. It is the operation [tt1; tt2] performs. The row times are stacked in the order the inputs are given and are never sorted, so a later block that starts before an earlier one ends simply makes the result irregular, as duplicate times do. The time step is read afresh from the stacked times, so two blocks that meet exactly keep the step they share and any other pair loses it. A table may be stacked onto a timetable, but only after it: its rows carry no times and are labelled with missing ones. The row dimension is named after the first input that does not use the default name. Where an operand is an eventtable, or carries one on its Events property, the result is an event table too and the operands' event tables are merged: an outer join keyed on the event times and every variable the two share, ordered by those keys. The three properties saying which variables describe the events must agree, and the operation is refused where they do not. See also: horzcat, cat # name: # type: sq_string # elements: 1 # length: 34 Concatenate timetables vertically. # name: # type: sq_string # elements: 1 # length: 15 timetable.width # name: # type: sq_string # elements: 1 # length: 256 timetable: W = width (tt) Number of variables in timetable. W = width (tt) returns the number of variables in the timetable tt as a scalar. It is the equivalent of size (tt, 2). The row times are not a variable and are not counted. See also: height, size # name: # type: sq_string # elements: 1 # length: 33 Number of variables in timetable. # name: # type: sq_string # elements: 1 # length: 21 timetable.withinrange # name: # type: sq_string # elements: 1 # length: 1169 timetable: TF = withinrange (tt, ref) timetable: [TF, whichRows] = withinrange (tt, ref) True when the whole range of a timetable lies inside a range of times. TF = withinrange (tt, ref) returns true when every row time of tt lies inside ref, which is to say that its earliest and its latest row time both do. Whether the ends themselves count is ref's to say, and this is where it tells. A timerange is half open by default, excluding the time it ends at, so a timetable whose last row falls exactly on that time is not within it; the same range built closed contains that instant, and the answer turns true. ref takes the same three forms as in containsrange: a timerange, a timetable whose earliest and latest row times give the range, or a datetime or duration scalar naming one instant. A timetable is within a single instant only when every row time it has is that instant. [TF, whichRows] = withinrange (...) also returns a column of logicals saying which rows of tt fall in ref. When TF is true they are all true, this being the one predicate for which the two answers agree. See also: timetable.containsrange, timetable.overlapsrange, timerange, timetable # name: # type: sq_string # elements: 1 # length: 70 True when the whole range of a timetable lies inside a range of times. # name: # type: sq_string # elements: 1 # length: 24 timetable.writetimetable # name: # type: sq_string # elements: 1 # length: 3627 timetable: writetimetable (tt, filename) timetable: writetimetable (tt, filename, Name, Value) Write a timetable to a file in the MATLAB-compatible form. writetimetable (tt, filename) writes the timetable tt to filename with no hidden metadata of any kind: one header row of names and then one row per row of the timetable, which is what readtimetable and MATLAB both expect. The file type follows the extension, .txt, .csv and .dat being text and .ods, .fods, .xlsx and .xlsm spreadsheets, and 'FileType' overrides it. The row times lead the file under the row dimension name, as MATLAB writes them. They are not optional: a timetable without them is not one, so there is no switch to leave them out, and 'WriteRowNames' is refused because a timetable has no row names. .ods is the OpenDocument spreadsheet format, an open standard that LibreOffice and other applications read and write on every platform. MATLAB is the exception and cannot be given one. Its writetimetable refuses the extension, and its readers open an OpenDocument file only by handing it to Excel, so a MATLAB user without Excel cannot read one at all. A file written for a MATLAB user should therefore be .csv or .xlsx. Everywhere else the format is a good choice, and timetable2ods writes it losslessly and is the only format of the package that carries an event table. A zone-aware datetime is written in the RFC 9557 form, 2024-03-09T22:00:00-05:00[America/New_York], rather than as the display string MATLAB writes. This is deviation D7; see the note below for why it is better. An unzoned datetime and a duration are written as MATLAB writes them, both of those round-tripping through MATLAB exactly. The following Name-Value options are supported: Name Value 'FileType' 'text' or 'spreadsheet', overriding what the extension says. 'WriteVariableNames' A logical scalar specifying whether the header row of names is written (default true). 'Delimiter' The field delimiter of a text file, named ('comma', 'space', 'tab', 'semi', 'bar') or given as the character itself (default ','). 'QuoteStrings' 'minimal', 'all' or 'none', saying which text fields are quoted (default 'minimal'). 'Sheet' The sheet of a spreadsheet to write. 'Range' The top-left cell a fresh spreadsheet write is anchored at, in A1 notation. 'WriteMode' 'overwrite' or 'append' for a text file; 'overwritesheet', 'inplace', 'append' or 'replacefile' for a spreadsheet. An attached event table is not written and nothing warns, as nothing warns in MATLAB. Use timetable2ods, the only format of the package that carries events. Deviation D7, and why the RFC 9557 form is the better answer. MATLAB writes a zoned datetime as its display string, so the zone is written only when the Format happens to ask for it, and readtimetable then reads the file back unzoned and at the wrong instant without complaining. Measured against R2026a, no text form of a zoned row time survives MATLAB's own reader: given a format carrying the offset, MATLAB writes 2024-03-09T22:00:00-05:00 itself and then fails to read that very file, so there is no encoding that keeps MATLAB working. The choice is between a loud failure there and a silent one, and this package takes the loud one. RFC 9557 is a published standard, not an invention of ours, and it carries both the offset and the zone name: the offset alone cannot name a zone, and the name alone cannot say which side of a repeated hour an instant falls on, so a timestamp inside a daylight-saving fold round-trips here and would not otherwise. See also: readtimetable, timetable.timetable2ods, timetable.timetable2csv, table.writetable # name: # type: sq_string # elements: 1 # length: 58 Write a timetable to a file in the MATLAB-compatible form. # name: # type: sq_string # elements: 1 # length: 12 tscollection # name: # type: sq_string # elements: 1 # length: 901 datatypes: tscollection Collection of time series on one time vector. A tscollection object holds several timeseries objects, its members, which share one time vector: a sample of the collection is the sample of every member at one time. Each member is reached by its name, as tc.name, and keeps its own data, quality codes, events and metadata; only the time vector and its TimeInfo are the collection's. tscollection is a value class: every method returns a modified copy and leaves its input unchanged. Its size is the number of samples by the number of members, and tc(i, j) selects samples i of members j, the members given by position or by name. Member names are unique ignoring case, and read with the exact name; MATLAB reads tc.A for a member a. A member cannot take the name of one of the four properties. See also: timeseries, istscollection, timeseries2timetable, tsdata.timemetadata # name: # type: sq_string # elements: 1 # length: 45 Collection of time series on one time vector. # name: # type: sq_string # elements: 1 # length: 19 tscollection.Length # name: # type: sq_string # elements: 1 # length: 103 tscollection: property Length The number of samples, which is the length of the time vector. Read-only. # name: # type: sq_string # elements: 1 # length: 62 The number of samples, which is the length of the time vector. # name: # type: sq_string # elements: 1 # length: 17 tscollection.Name # name: # type: sq_string # elements: 1 # length: 181 tscollection: property Name The name of the collection. A character vector, 'unnamed' by default. A string scalar is converted; any other value is refused, where MATLAB accepts it. # name: # type: sq_string # elements: 1 # length: 27 The name of the collection. # name: # type: sq_string # elements: 1 # length: 17 tscollection.Time # name: # type: sq_string # elements: 1 # length: 394 tscollection: property Time The time vector every member shares. A column vector of finite, non-decreasing times, in tc.TimeInfo.Units, counted from tc.TimeInfo.StartDate when that is set, and stored as double. Repeated times are allowed. Assigning it gives every member the new times, without moving any sample; it must hold as many times as there are samples, and an unsorted one is refused. # name: # type: sq_string # elements: 1 # length: 36 The time vector every member shares. # name: # type: sq_string # elements: 1 # length: 21 tscollection.TimeInfo # name: # type: sq_string # elements: 1 # length: 262 tscollection: property TimeInfo The metadata of the time vector, a tsdata.timemetadata object, which every member shares. Assigning it, or one of its properties, assigns it to every member. Its Length, Start, End and Increment follow Time and cannot be assigned. # name: # type: sq_string # elements: 1 # length: 89 The metadata of the time vector, a tsdata.timemetadata object, which every member shares. # name: # type: sq_string # elements: 1 # length: 34 tscollection.addsampletocollection # name: # type: sq_string # elements: 1 # length: 1087 tscollection: tc = addsampletocollection (tc, 'Time', time, name1, data1, ...) Add samples to every member. tc = addsampletocollection (tc, 'Time', time, name1, data1, ...) inserts samples at the times time, keeping the collection in time order; a sample at a time already present goes after the samples there. The member name1, its exact name, takes the samples data1, one per time, read against time as the timeseries constructor reads data against times and each of the size of the member's samples. A member not named takes NaN, or 0 for integer data and false for logical data. 'Time' is matched in any case. A row of data for a column of times is accepted, as the timeseries constructor accepts it; MATLAB refuses it for a collection. For a collection with no TimeInfo.StartDate time is numbers in its units; for one with a start date it may also be dates as text, or a datetime. A member with quality codes gives each new sample the code of the sample nearest in time, the later one on a tie, as MATLAB does. See also: tscollection.delsamplefromcollection, timeseries.addsample # name: # type: sq_string # elements: 1 # length: 28 Add samples to every member. # name: # type: sq_string # elements: 1 # length: 18 tscollection.addts # name: # type: sq_string # elements: 1 # length: 1097 tscollection: tc = addts (tc, ts) tscollection: tc = addts (tc, {ts1, ts2, ...}) tscollection: tc = addts (tc, data, name) Add members to a collection. tc = addts (tc, ts) adds the timeseries ts as the last member of tc, and tc = addts (tc, {ts1, ts2, ...}) adds each series in the cell array in turn. Each must have the times of the collection, in the same units; a series with a TimeInfo.StartDate added to a collection without one loses its start date, with a warning, as in MATLAB, and the reverse is refused. A collection with no samples and no members takes the times of the first series. Each member is named as the constructor names it. tc = addts (tc, data, name) adds a member name holding the samples data, a numeric or logical array of one sample per time of the collection, read against the time vector as the timeseries constructor reads it. Names must be unique ignoring case: MATLAB refuses A beside a here, though settimeseriesnames allows it there. MATLAB also ignores a fourth argument, which is refused here. See also: tscollection.removets, tscollection.settimeseriesnames # name: # type: sq_string # elements: 1 # length: 28 Add members to a collection. # name: # type: sq_string # elements: 1 # length: 36 tscollection.delsamplefromcollection # name: # type: sq_string # elements: 1 # length: 828 tscollection: tc = delsamplefromcollection (tc, 'Index', ind) tscollection: tc = delsamplefromcollection (tc, 'Value', time) Remove samples from every member. tc = delsamplefromcollection (tc, 'Index', ind) removes the samples ind, a vector of positive integers not exceeding Length; repeats are allowed and [] removes nothing. A logical mask is refused, as in MATLAB. tc = delsamplefromcollection (tc, 'Value', time) removes every sample at each of the times time; a time no sample has removes nothing. For a collection with no TimeInfo.StartDate the times are numbers in its units; for one with a start date they may also be dates as text, or a datetime. The option names are matched in any case. Removing every sample leaves the members, with no samples. See also: tscollection.addsampletocollection, timeseries.delsample # name: # type: sq_string # elements: 1 # length: 33 Remove samples from every member. # name: # type: sq_string # elements: 1 # length: 23 tscollection.fieldnames # name: # type: sq_string # elements: 1 # length: 295 tscollection: names = fieldnames (tc) Return the names of the properties and members. names = fieldnames (tc) returns a column cell array of Name, Time, TimeInfo and Length, followed by the names of the members of tc in order. See also: tscollection.properties, tscollection.gettimeseriesnames # name: # type: sq_string # elements: 1 # length: 47 Return the names of the properties and members. # name: # type: sq_string # elements: 1 # length: 16 tscollection.get # name: # type: sq_string # elements: 1 # length: 526 tscollection: S = get (tc) tscollection: value = get (tc, name) tscollection: values = get (tc, names) Return property values and members. S = get (tc) returns a structure of the four properties of the collection tc followed by its members. value = get (tc, name) returns the property or member name, a character vector or a string scalar matched in any case. values = get (tc, names) returns a row cell array of the properties and members named in the cell array names. See also: tscollection.set, tscollection.fieldnames # name: # type: sq_string # elements: 1 # length: 35 Return property values and members. # name: # type: sq_string # elements: 1 # length: 23 tscollection.getabstime # name: # type: sq_string # elements: 1 # length: 489 tscollection: dates = getabstime (tc) Return the dates of the samples as text. dates = getabstime (tc) returns a column cell array of character vectors, the date of each sample of the collection tc, written as timeseries.getabstime writes them: in the format TimeInfo.Format, or 'dd-mmm-yyyy HH:MM:SS' when that is empty. MATLAB writes the default whatever TimeInfo.Format says. A collection with no TimeInfo.StartDate is refused. See also: tscollection.setabstime, timeseries.getabstime # name: # type: sq_string # elements: 1 # length: 40 Return the dates of the samples as text. # name: # type: sq_string # elements: 1 # length: 31 tscollection.getsampleusingtime # name: # type: sq_string # elements: 1 # length: 847 tscollection: tc2 = getsampleusingtime (tc, t) tscollection: tc2 = getsampleusingtime (tc, t, 'AllowDuplicateTimes', tf) tscollection: tc2 = getsampleusingtime (tc, t1, t2) Return a collection of the samples at a time or between two times. tc2 = getsampleusingtime (tc, t) returns the collection tc holding only the sample at time t. When several samples share that time the call is refused, unless 'AllowDuplicateTimes' is true, which returns them all. tc2 = getsampleusingtime (tc, t1, t2) returns the samples from t1 to t2, both included. The times are read as timeseries.getsampleusingtime reads them. No sample in range gives a collection with no samples and every member. The name and every member's properties are kept; MATLAB names the result 'unnamed' and, when it is empty, drops every member. See also: timeseries.getsampleusingtime # name: # type: sq_string # elements: 1 # length: 66 Return a collection of the samples at a time or between two times. # name: # type: sq_string # elements: 1 # length: 31 tscollection.gettimeseriesnames # name: # type: sq_string # elements: 1 # length: 273 tscollection: names = gettimeseriesnames (tc) Return the names of the members. names = gettimeseriesnames (tc) returns a row cell array of the names of the members of tc, in order, or {} when it has none. See also: tscollection.settimeseriesnames, tscollection.fieldnames # name: # type: sq_string # elements: 1 # length: 32 Return the names of the members. # name: # type: sq_string # elements: 1 # length: 30 tscollection.hasduplicatetimes # name: # type: sq_string # elements: 1 # length: 218 tscollection: tf = hasduplicatetimes (tc) True where a collection repeats a time. tf = hasduplicatetimes (tc) returns true when two samples of the collection tc share a time. See also: tscollection.getsampleusingtime # name: # type: sq_string # elements: 1 # length: 39 True where a collection repeats a time. # name: # type: sq_string # elements: 1 # length: 20 tscollection.horzcat # name: # type: sq_string # elements: 1 # length: 680 tscollection: tc = horzcat (tc1, tc2, ...) Join the members of collections. tc = [tc1, tc2, ...] returns one collection holding the members of every argument, sorted by name, as in MATLAB, capitals before lower case letters. The collections must have the same time vector, in the same units, and the same TimeInfo.StartDate. A member name present in more than one is taken once when the members are equal in every property, NaN equal to itself, and refused otherwise; MATLAB compares their data alone. Names differing only in case are refused. The result keeps the name of the first collection, where MATLAB names it 'unnamed'. See also: tscollection.vertcat, tscollection.addts # name: # type: sq_string # elements: 1 # length: 32 Join the members of collections. # name: # type: sq_string # elements: 1 # length: 20 tscollection.isempty # name: # type: sq_string # elements: 1 # length: 253 tscollection: tf = isempty (tc) True for a collection with no samples. tf = isempty (tc) returns true when the collection tc has no times, whether it has members or not, and false for a collection with times and no members. See also: tscollection.size # name: # type: sq_string # elements: 1 # length: 38 True for a collection with no samples. # name: # type: sq_string # elements: 1 # length: 20 tscollection.isequal # name: # type: sq_string # elements: 1 # length: 334 tscollection: tf = isequal (tc1, tc2, ...) True when collections are equal. tf = isequal (tc1, tc2, ...) returns true when every argument is a tscollection with the same name, time vector and TimeInfo, and the same members in the same order, each equal as isequal compares series, NaN unequal to itself. See also: timeseries.isequal # name: # type: sq_string # elements: 1 # length: 32 True when collections are equal. # name: # type: sq_string # elements: 1 # length: 19 tscollection.length # name: # type: sq_string # elements: 1 # length: 177 tscollection: n = length (tc) Return the number of samples. n = length (tc) returns tc.Length, the number of times, whatever the number of members. See also: tscollection.size # name: # type: sq_string # elements: 1 # length: 29 Return the number of samples. # name: # type: sq_string # elements: 1 # length: 23 tscollection.properties # name: # type: sq_string # elements: 1 # length: 223 tscollection: names = properties (tc) Return the names of the properties and members. names = properties (tc) returns what fieldnames (tc) does: the members are reached as properties are. See also: tscollection.fieldnames # name: # type: sq_string # elements: 1 # length: 47 Return the names of the properties and members. # name: # type: sq_string # elements: 1 # length: 21 tscollection.removets # name: # type: sq_string # elements: 1 # length: 453 tscollection: tc = removets (tc, names) Remove members from a collection. tc = removets (tc, names) removes the members names, a character vector, a cell array of character vectors or a string array, each the exact name of a member. The time vector stays, so removing every member leaves a collection with times and no members. A name that is no member is refused, where MATLAB ignores it. See also: tscollection.addts, tscollection.gettimeseriesnames # name: # type: sq_string # elements: 1 # length: 33 Remove members from a collection. # name: # type: sq_string # elements: 1 # length: 21 tscollection.resample # name: # type: sq_string # elements: 1 # length: 1063 tscollection: tc = resample (tc, time) tscollection: tc = resample (tc, time, method) tscollection: tc = resample (tc, time, method, code) Evaluate every member at new times. tc = resample (tc, time) returns the collection tc with every member evaluated at time by its own interpolation method, as timeseries.resample evaluates a series: at a time outside the collection the data is NaN, and a member with quality codes gives each new time the code of the nearest sample. MATLAB raises there for a member with quality codes. time is taken in any order and sorted, as timeseries.resample takes it; MATLAB refuses descending times for a collection. tc = resample (tc, time, method) uses method, 'linear' or 'zoh', for every member; [] keeps each member's own. tc = resample (tc, time, method, code) gives the quality code code to every new time the collection did not already hold, in each member with quality codes; it must be listed in the QualityInfo.Code of each of them. Members without quality codes are resampled without it. See also: timeseries.resample # name: # type: sq_string # elements: 1 # length: 35 Evaluate every member at new times. # name: # type: sq_string # elements: 1 # length: 16 tscollection.set # name: # type: sq_string # elements: 1 # length: 555 tscollection: set (tc, name, value) tscollection: set (tc, name1, value1, ...) tscollection: tc2 = set (tc, name, value, ...) tscollection: S = set (tc) Set property values and members. set (tc, name, value, ...) sets each property or member name, matched in any case, to its value, in the order given, in the variable tc of the caller, as dot assignment does. tc must therefore be a variable. tc2 = set (tc, name, value, ...) returns the modified collection and leaves tc as it was. S = set (tc) returns what get (tc) does. See also: tscollection.get # name: # type: sq_string # elements: 1 # length: 32 Set property values and members. # name: # type: sq_string # elements: 1 # length: 30 tscollection.setTimeseriesName # name: # type: sq_string # elements: 1 # length: 223 tscollection: tc = setTimeseriesName (tc, old, new) Rename a member. tc = setTimeseriesName (tc, old, new) is settimeseriesnames, kept for compatibility; MATLAB marks it obsolete. See also: tscollection.settimeseriesnames # name: # type: sq_string # elements: 1 # length: 16 Rename a member. # name: # type: sq_string # elements: 1 # length: 23 tscollection.setabstime # name: # type: sq_string # elements: 1 # length: 849 tscollection: tc = setabstime (tc, dates) tscollection: tc = setabstime (tc, dates, format) Set the time vector of a collection from dates. tc = setabstime (tc, dates) gives the samples of the collection tc the dates dates, one per sample, in order, exactly as timeseries.setabstime gives them to a series, in the collection's own units; every member takes them. With format every date is read strictly in it. The dates need not be sorted: every member's samples, with their quality codes, are sorted with them. MATLAB sorts the times alone, so each sample is given another's date. It also reads every date by the format of the first, forces text into format, and takes numbers and datetime values as relative times, dropping the dates; see timeseries.setabstime for how each is read here. See also: tscollection.getabstime, timeseries.setabstime # name: # type: sq_string # elements: 1 # length: 47 Set the time vector of a collection from dates. # name: # type: sq_string # elements: 1 # length: 31 tscollection.settimeseriesnames # name: # type: sq_string # elements: 1 # length: 649 tscollection: tc = settimeseriesnames (tc, old, new) Rename a member. tc = settimeseriesnames (tc, old, new) renames the member old, its exact name, to new, a valid variable name, both character vectors or string scalars; the series takes the new name too, and the member becomes the last, as in MATLAB. new must not be the name of another member, ignoring case, nor of one of the four properties. MATLAB accepts the name of another member and drops that member, and accepts a name differing from another only in case, after which that member cannot be reached; both are refused here. See also: tscollection.gettimeseriesnames, tscollection.addts # name: # type: sq_string # elements: 1 # length: 16 Rename a member. # name: # type: sq_string # elements: 1 # length: 17 tscollection.size # name: # type: sq_string # elements: 1 # length: 483 tscollection: sz = size (tc) tscollection: dim_sz = size (tc, dim) tscollection: [nsamples, nmembers] = size (tc) Return the number of samples and the number of members. sz = size (tc) returns [nsamples, nmembers]. size (tc, dim) returns the size along dimension dim, a positive integer or a vector of them; dimensions above 2 have size 1. With several outputs each takes one dimension, where MATLAB refuses more than one output. See also: tscollection.length, tscollection.isempty # name: # type: sq_string # elements: 1 # length: 55 Return the number of samples and the number of members. # name: # type: sq_string # elements: 1 # length: 25 tscollection.tscollection # name: # type: sq_string # elements: 1 # length: 1701 tscollection: tc = tscollection () tscollection: tc = tscollection (ts) tscollection: tc = tscollection ({ts1, ts2, ...}) tscollection: tc = tscollection (time) tscollection: tc = tscollection (..., 'Name', name) Create a tscollection object. tc = tscollection () returns an empty collection, with no samples and no members. tc = tscollection (ts) returns a collection of the single timeseries ts, on its time vector and its TimeInfo. A series with no samples gives an empty collection, as in MATLAB. tc = tscollection ({ts1, ts2, ...}) returns a collection of the series in the cell array, a row or a column, on the time vector of the first; every other series must have the same times, in the same units, and the same TimeInfo.StartDate. An empty cell array gives an empty collection, and a series with no samples is refused, as in MATLAB. tc = tscollection (time) returns a collection with no members on the numeric vector time, which must be finite and is sorted, in seconds. Each member is named by matlab.lang.makeValidName of the name of its series, which it then takes: a series named 'my data' becomes the member myData, and one named '' the member x. The names must be unique ignoring case, and none may be the name of a property: Name, Time, TimeInfo or Length. MATLAB refuses only Length, and a member named Time then hides the time vector. tc = tscollection (..., 'Name', name) names the collection, 'unnamed' by default; the option name is matched in any case, and a later occurrence overrides an earlier one. MATLAB ignores a second positional argument, so that tscollection (ts1, ts2) holds ts1 only; here it is refused. See also: timeseries, tscollection.addts, istscollection # name: # type: sq_string # elements: 1 # length: 29 Create a tscollection object. # name: # type: sq_string # elements: 1 # length: 20 tscollection.vertcat # name: # type: sq_string # elements: 1 # length: 926 tscollection: tc = vertcat (tc1, tc2, ...) Join collections end to end in time. tc = [tc1; tc2; ...] returns one collection holding the samples of every argument in turn. The collections must have the same members, by name, taken in the order of the first, with a warning when another orders them differently, as in MATLAB. Each must start at or after the end of the one before, so equal times where two meet give a repeated time. Each member is joined as timeseries.append joins series: times in the coarsest units among them, dates counted from the first start date, and samples of one size and one class, as in MATLAB. A member's quality codes are joined when it has them in every collection, and refused when only some do; MATLAB then gives a member that cannot be read, or drops them. The result keeps the name of the first collection, where MATLAB names it 'unnamed'. See also: tscollection.horzcat, timeseries.append # name: # type: sq_string # elements: 1 # length: 36 Join collections end to end in time. # name: # type: sq_string # elements: 1 # length: 19 tsdata.datametadata # name: # type: sq_string # elements: 1 # length: 253 datatypes: tsdata.datametadata The data metadata of a timeseries. A tsdata.datametadata object describes the samples of a timeseries: their units, how the series is evaluated between them, and any data the user attaches. It is what ts.DataInfo returns. # name: # type: sq_string # elements: 1 # length: 34 The data metadata of a timeseries. # name: # type: sq_string # elements: 1 # length: 33 tsdata.datametadata.Interpolation # name: # type: sq_string # elements: 1 # length: 324 tsdata.datametadata: property Interpolation The interpolation method. A tsdata.interpolation object, 'linear' by default. A method name or a function handle may be assigned, and is converted. MATLAB starts a tsdata.datametadata created on its own with [] here, and a timeseries with 'linear'; here both start with 'linear'. # name: # type: sq_string # elements: 1 # length: 25 The interpolation method. # name: # type: sq_string # elements: 1 # length: 46 tsdata.datametadata.InterpretSingleRowDataAs3D # name: # type: sq_string # elements: 1 # length: 460 tsdata.datametadata: property InterpretSingleRowDataAs3D Stored for compatibility, with no effect. A logical scalar, false by default. In MATLAB it affects only addsample given a row of data, where true gives two-dimensional data whose time runs along its last dimension while the sample holds the whole row, a state no other method can read consistently. Here the orientation of the data always follows its dimensions, as IsTimeFirst of timeseries describes. # name: # type: sq_string # elements: 1 # length: 41 Stored for compatibility, with no effect. # name: # type: sq_string # elements: 1 # length: 25 tsdata.datametadata.Units # name: # type: sq_string # elements: 1 # length: 176 tsdata.datametadata: property Units The units of the data. A character vector, '' by default. It is free text: nothing checks it against the data or converts the data with it. # name: # type: sq_string # elements: 1 # length: 22 The units of the data. # name: # type: sq_string # elements: 1 # length: 28 tsdata.datametadata.UserData # name: # type: sq_string # elements: 1 # length: 81 tsdata.datametadata: property UserData Any data the user attaches, [] by default. # name: # type: sq_string # elements: 1 # length: 42 Any data the user attaches, [] by default. # name: # type: sq_string # elements: 1 # length: 32 tsdata.datametadata.datametadata # name: # type: sq_string # elements: 1 # length: 351 tsdata.datametadata: di = tsdata.datametadata () Create a data metadata object. di = tsdata.datametadata () returns an object with Units '', Interpolation 'linear', UserData [] and InterpretSingleRowDataAs3D false. A timeseries creates its own; this is needed only to assign a whole new one to ts.DataInfo. See also: timeseries, tsdata.interpolation # name: # type: sq_string # elements: 1 # length: 30 Create a data metadata object. # name: # type: sq_string # elements: 1 # length: 23 tsdata.datametadata.get # name: # type: sq_string # elements: 1 # length: 603 tsdata.datametadata: S = get (di) tsdata.datametadata: value = get (di, name) tsdata.datametadata: values = get (di, names) Return property values. S = get (di) returns a structure of every property of di. value = get (di, name) returns the property name, a character vector or a string scalar matched in any case, and values = get (di, names) a row cell array of the properties named in the cell array names. For an array, one name gives a cell array of its size, and no name, or several, a cell array with a row per object and a column per property. See also: tsdata.datametadata.set, timeseries.get # name: # type: sq_string # elements: 1 # length: 23 Return property values. # name: # type: sq_string # elements: 1 # length: 23 tsdata.datametadata.set # name: # type: sq_string # elements: 1 # length: 610 tsdata.datametadata: set (di, name, value, ...) tsdata.datametadata: di2 = set (di, name, value, ...) tsdata.datametadata: S = set (di) Set property values. set (di, name, value, ...) sets each property name, matched in any case, to its value, in the order given, in the variable di of the caller, which must therefore be a variable. Metadata held by a timeseries is set with dot assignment instead, as ts.TimeInfo.Units = 'days'. di2 = set (di, name, value, ...) returns the modified object and leaves di as it was. S = set (di) returns what get (di) does. See also: tsdata.datametadata.get, timeseries.set # name: # type: sq_string # elements: 1 # length: 20 Set property values. # name: # type: sq_string # elements: 1 # length: 12 tsdata.event # name: # type: sq_string # elements: 1 # length: 344 datatypes: tsdata.event An event of a timeseries. A tsdata.event object marks a named instant, with any data the user attaches to it. The Events property of a timeseries holds an array of them. An event carries its own time, in its own units and optionally from its own start date, so it keeps its instant when the series' time vector changes. # name: # type: sq_string # elements: 1 # length: 25 An event of a timeseries. # name: # type: sq_string # elements: 1 # length: 22 tsdata.event.EventData # name: # type: sq_string # elements: 1 # length: 75 tsdata.event: property EventData Any data the user attaches, [] by default. # name: # type: sq_string # elements: 1 # length: 42 Any data the user attaches, [] by default. # name: # type: sq_string # elements: 1 # length: 17 tsdata.event.Name # name: # type: sq_string # elements: 1 # length: 96 tsdata.event: property Name The name of the event, a character vector. Names need not be unique. # name: # type: sq_string # elements: 1 # length: 42 The name of the event, a character vector. # name: # type: sq_string # elements: 1 # length: 22 tsdata.event.StartDate # name: # type: sq_string # elements: 1 # length: 204 tsdata.event: property StartDate The absolute date Time counts from, or '' for a relative time. A string scalar or a scalar datetime is converted to a character vector; text that is not a date is refused. # name: # type: sq_string # elements: 1 # length: 62 The absolute date Time counts from, or '' for a relative time. # name: # type: sq_string # elements: 1 # length: 17 tsdata.event.Time # name: # type: sq_string # elements: 1 # length: 156 tsdata.event: property Time The time of the event, a real scalar in Units, counted from StartDate when that is set. NaN is refused, where MATLAB accepts it. # name: # type: sq_string # elements: 1 # length: 87 The time of the event, a real scalar in Units, counted from StartDate when that is set. # name: # type: sq_string # elements: 1 # length: 18 tsdata.event.Units # name: # type: sq_string # elements: 1 # length: 135 tsdata.event: property Units The units of Time. One of the units a timeseries time vector takes, or ''. MATLAB accepts any value here. # name: # type: sq_string # elements: 1 # length: 18 The units of Time. # name: # type: sq_string # elements: 1 # length: 18 tsdata.event.event # name: # type: sq_string # elements: 1 # length: 682 tsdata.event: e = tsdata.event () tsdata.event: e = tsdata.event (name) tsdata.event: e = tsdata.event (name, time) Create an event. e = tsdata.event (name) returns an event named name, a character vector or a string scalar, at Time 0 with no Units. With no argument the name is ''. e = tsdata.event (name, time) places it at time. A real scalar is a relative time in seconds. A date, as text or as a scalar datetime, is an absolute time: the event is then at Time 0 in 'days' from a StartDate of that date, stored as 'dd-mmm-yyyy HH:MM:SS', with milliseconds as '.FFF' when the seconds are not whole. MATLAB ignores a third argument and refuses a datetime. See also: timeseries # name: # type: sq_string # elements: 1 # length: 16 Create an event. # name: # type: sq_string # elements: 1 # length: 16 tsdata.event.get # name: # type: sq_string # elements: 1 # length: 801 tsdata.event: S = get (e) tsdata.event: value = get (e, name) tsdata.event: values = get (e, names) Return property values. S = get (e) returns a structure of every property of e. value = get (e, name) returns the property name, a character vector or a string scalar matched in any case, and values = get (e, names) a row cell array of the properties named in the cell array names. For an array, one name gives a cell array of its size, and no name, or several, a cell array with a row per object and a column per property. MATLAB refuses an output argument to set on an event and, given an array of events and one name, returns the first event's value only; here set returns the modified events as on any other class, and get returns one value per event. See also: tsdata.event.set, timeseries.get # name: # type: sq_string # elements: 1 # length: 23 Return property values. # name: # type: sq_string # elements: 1 # length: 16 tsdata.event.set # name: # type: sq_string # elements: 1 # length: 571 tsdata.event: set (e, name, value, ...) tsdata.event: e2 = set (e, name, value, ...) tsdata.event: S = set (e) Set property values. set (e, name, value, ...) sets each property name, matched in any case, to its value, in the order given, in the variable e of the caller, which must therefore be a variable. Metadata held by a timeseries is set with dot assignment instead, as ts.TimeInfo.Units = 'days'. e2 = set (e, name, value, ...) returns the modified object and leaves e as it was. S = set (e) returns what get (e) does. See also: tsdata.event.get, timeseries.set # name: # type: sq_string # elements: 1 # length: 20 Set property values. # name: # type: sq_string # elements: 1 # length: 20 tsdata.interpolation # name: # type: sq_string # elements: 1 # length: 443 datatypes: tsdata.interpolation The interpolation method of a timeseries. A tsdata.interpolation object names how a timeseries is evaluated between its samples, and holds the function that does it. It is what ts.DataInfo.Interpolation returns, and it is the value setinterpmethod sets. Two methods are built in, 'linear' and 'zoh' (zero-order hold, which keeps each value until the next sample), and any function handle may be given instead. # name: # type: sq_string # elements: 1 # length: 41 The interpolation method of a timeseries. # name: # type: sq_string # elements: 1 # length: 28 tsdata.interpolation.Fhandle # name: # type: sq_string # elements: 1 # length: 569 tsdata.interpolation: property Fhandle The interpolating function. A function handle called as newData = fh (newTime, oldTime, oldData), as in MATLAB: newTime and oldTime are column vectors, and oldData and newData have time along their first dimension, a sample per row. It is [] for an object that names no method. Assigning a function handle sets Name to 'myFuncHandle'. MATLAB stores this property in several shapes, among them a cell holding the handle and, for a built-in method, a cell holding a handle and a function name; here it is always the handle itself. # name: # type: sq_string # elements: 1 # length: 27 The interpolating function. # name: # type: sq_string # elements: 1 # length: 25 tsdata.interpolation.Name # name: # type: sq_string # elements: 1 # length: 393 tsdata.interpolation: property Name The name of the interpolation method. One of 'linear', 'zoh' or 'myFuncHandle', or '' for an object that names no method. Assigning 'linear' or 'zoh' also sets Fhandle to the built-in function; 'myFuncHandle' is set by assigning a function handle to Fhandle, not by name. MATLAB accepts any name and leaves Fhandle as it was, so the two can disagree there. # name: # type: sq_string # elements: 1 # length: 37 The name of the interpolation method. # name: # type: sq_string # elements: 1 # length: 24 tsdata.interpolation.get # name: # type: sq_string # elements: 1 # length: 607 tsdata.interpolation: S = get (ip) tsdata.interpolation: value = get (ip, name) tsdata.interpolation: values = get (ip, names) Return property values. S = get (ip) returns a structure of every property of ip. value = get (ip, name) returns the property name, a character vector or a string scalar matched in any case, and values = get (ip, names) a row cell array of the properties named in the cell array names. For an array, one name gives a cell array of its size, and no name, or several, a cell array with a row per object and a column per property. See also: tsdata.interpolation.set, timeseries.get # name: # type: sq_string # elements: 1 # length: 23 Return property values. # name: # type: sq_string # elements: 1 # length: 34 tsdata.interpolation.interpolation # name: # type: sq_string # elements: 1 # length: 729 tsdata.interpolation: ip = tsdata.interpolation () tsdata.interpolation: ip = tsdata.interpolation (name) tsdata.interpolation: ip = tsdata.interpolation (fh) Create an interpolation method object. ip = tsdata.interpolation () returns an object that names no method, with Name '' and Fhandle []. ip = tsdata.interpolation (name) returns the built-in method name, 'linear' or 'zoh', given as a character vector or a string scalar. ip = tsdata.interpolation (fh) returns a method evaluated by the function handle fh, called as newData = fh (newTime, oldTime, oldData), and names it 'myFuncHandle'. A cell array is refused, where MATLAB returns an empty object for it without an error. See also: timeseries, tsdata.datametadata # name: # type: sq_string # elements: 1 # length: 38 Create an interpolation method object. # name: # type: sq_string # elements: 1 # length: 24 tsdata.interpolation.set # name: # type: sq_string # elements: 1 # length: 614 tsdata.interpolation: set (ip, name, value, ...) tsdata.interpolation: ip2 = set (ip, name, value, ...) tsdata.interpolation: S = set (ip) Set property values. set (ip, name, value, ...) sets each property name, matched in any case, to its value, in the order given, in the variable ip of the caller, which must therefore be a variable. Metadata held by a timeseries is set with dot assignment instead, as ts.TimeInfo.Units = 'days'. ip2 = set (ip, name, value, ...) returns the modified object and leaves ip as it was. S = set (ip) returns what get (ip) does. See also: tsdata.interpolation.get, timeseries.set # name: # type: sq_string # elements: 1 # length: 20 Set property values. # name: # type: sq_string # elements: 1 # length: 19 tsdata.qualmetadata # name: # type: sq_string # elements: 1 # length: 277 datatypes: tsdata.qualmetadata The quality metadata of a timeseries. A tsdata.qualmetadata object lists the quality codes a timeseries may carry in its Quality property, and a description of each. It is what ts.QualityInfo returns. Code Code(k) is described by Description{k}. # name: # type: sq_string # elements: 1 # length: 37 The quality metadata of a timeseries. # name: # type: sq_string # elements: 1 # length: 24 tsdata.qualmetadata.Code # name: # type: sq_string # elements: 1 # length: 192 tsdata.qualmetadata: property Code The quality codes. A vector of distinct integers from -128 to 127, stored as double, [] by default. MATLAB accepts any number here, repeated codes included. # name: # type: sq_string # elements: 1 # length: 18 The quality codes. # name: # type: sq_string # elements: 1 # length: 31 tsdata.qualmetadata.Description # name: # type: sq_string # elements: 1 # length: 181 tsdata.qualmetadata: property Description The description of each quality code. A cell array of character vectors, [] by default. A character vector or a string array is converted. # name: # type: sq_string # elements: 1 # length: 37 The description of each quality code. # name: # type: sq_string # elements: 1 # length: 28 tsdata.qualmetadata.UserData # name: # type: sq_string # elements: 1 # length: 81 tsdata.qualmetadata: property UserData Any data the user attaches, [] by default. # name: # type: sq_string # elements: 1 # length: 42 Any data the user attaches, [] by default. # name: # type: sq_string # elements: 1 # length: 23 tsdata.qualmetadata.get # name: # type: sq_string # elements: 1 # length: 603 tsdata.qualmetadata: S = get (qi) tsdata.qualmetadata: value = get (qi, name) tsdata.qualmetadata: values = get (qi, names) Return property values. S = get (qi) returns a structure of every property of qi. value = get (qi, name) returns the property name, a character vector or a string scalar matched in any case, and values = get (qi, names) a row cell array of the properties named in the cell array names. For an array, one name gives a cell array of its size, and no name, or several, a cell array with a row per object and a column per property. See also: tsdata.qualmetadata.set, timeseries.get # name: # type: sq_string # elements: 1 # length: 23 Return property values. # name: # type: sq_string # elements: 1 # length: 32 tsdata.qualmetadata.qualmetadata # name: # type: sq_string # elements: 1 # length: 290 tsdata.qualmetadata: qi = tsdata.qualmetadata () Create a quality metadata object. qi = tsdata.qualmetadata () returns an object with Code, Description and UserData all []. A timeseries creates its own; this is needed only to assign a whole new one to ts.QualityInfo. See also: timeseries # name: # type: sq_string # elements: 1 # length: 33 Create a quality metadata object. # name: # type: sq_string # elements: 1 # length: 23 tsdata.qualmetadata.set # name: # type: sq_string # elements: 1 # length: 610 tsdata.qualmetadata: set (qi, name, value, ...) tsdata.qualmetadata: qi2 = set (qi, name, value, ...) tsdata.qualmetadata: S = set (qi) Set property values. set (qi, name, value, ...) sets each property name, matched in any case, to its value, in the order given, in the variable qi of the caller, which must therefore be a variable. Metadata held by a timeseries is set with dot assignment instead, as ts.TimeInfo.Units = 'days'. qi2 = set (qi, name, value, ...) returns the modified object and leaves qi as it was. S = set (qi) returns what get (qi) does. See also: tsdata.qualmetadata.get, timeseries.set # name: # type: sq_string # elements: 1 # length: 20 Set property values. # name: # type: sq_string # elements: 1 # length: 19 tsdata.timemetadata # name: # type: sq_string # elements: 1 # length: 458 datatypes: tsdata.timemetadata The time metadata of a timeseries. A tsdata.timemetadata object describes the time vector of a timeseries: its units, an optional absolute start date, and the length, first and last times and uniform step derived from it. It is what ts.TimeInfo returns. The derived properties Length, Start, End and Increment follow the time vector and cannot be assigned; change the time vector of the series instead, or use setuniformtime. # name: # type: sq_string # elements: 1 # length: 34 The time metadata of a timeseries. # name: # type: sq_string # elements: 1 # length: 23 tsdata.timemetadata.End # name: # type: sq_string # elements: 1 # length: 91 tsdata.timemetadata: property End The last time, or [] for an empty time vector. Read-only. # name: # type: sq_string # elements: 1 # length: 46 The last time, or [] for an empty time vector. # name: # type: sq_string # elements: 1 # length: 26 tsdata.timemetadata.Format # name: # type: sq_string # elements: 1 # length: 373 tsdata.timemetadata: property Format The display format of absolute times. A datestr format as a character vector, '' by default, which getabstime writes the dates in. Text holding no date field at all, such as 'bogus', is refused, where MATLAB accepts any value; MATLAB also ignores every format but the few its documentation lists, while here any datestr format is used. # name: # type: sq_string # elements: 1 # length: 37 The display format of absolute times. # name: # type: sq_string # elements: 1 # length: 29 tsdata.timemetadata.Increment # name: # type: sq_string # elements: 1 # length: 388 tsdata.timemetadata: property Increment The step of a uniform time vector. The step when every one is exactly equal and not zero, NaN otherwise and for a single time, [] for an empty time vector. Read-only; use setuniformtime to give a series a uniform time vector. MATLAB still accepts an assignment here, with a warning that it will not in future, and rebuilds the time vector from it. # name: # type: sq_string # elements: 1 # length: 34 The step of a uniform time vector. # name: # type: sq_string # elements: 1 # length: 26 tsdata.timemetadata.Length # name: # type: sq_string # elements: 1 # length: 87 tsdata.timemetadata: property Length The number of times in the time vector. Read-only. # name: # type: sq_string # elements: 1 # length: 39 The number of times in the time vector. # name: # type: sq_string # elements: 1 # length: 25 tsdata.timemetadata.Start # name: # type: sq_string # elements: 1 # length: 194 tsdata.timemetadata: property Start The first time, or [] for an empty time vector. Read-only. MATLAB still accepts an assignment here, with a warning that it will not in future, and ignores it. # name: # type: sq_string # elements: 1 # length: 47 The first time, or [] for an empty time vector. # name: # type: sq_string # elements: 1 # length: 29 tsdata.timemetadata.StartDate # name: # type: sq_string # elements: 1 # length: 512 tsdata.timemetadata: property StartDate The absolute date the time vector counts from. A date as a character vector, or '' for a relative time vector, by default. Text, a string scalar or a scalar datetime is stored in one form, 'dd-mmm-yyyy HH:MM:SS', with milliseconds as '.FFF' when the seconds are not whole, so a date is read once, when it is assigned, and never guessed at again. Text that is not a whole date (a year, a time of day) is refused, where MATLAB accepts any value and keeps the text as given. # name: # type: sq_string # elements: 1 # length: 46 The absolute date the time vector counts from. # name: # type: sq_string # elements: 1 # length: 25 tsdata.timemetadata.Units # name: # type: sq_string # elements: 1 # length: 321 tsdata.timemetadata: property Units The units of the time vector. One of 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds' or 'nanoseconds', in any case, stored in lower case; 'seconds' by default. Assigning it relabels the time vector and does not rescale it. MATLAB accepts any value here. # name: # type: sq_string # elements: 1 # length: 29 The units of the time vector. # name: # type: sq_string # elements: 1 # length: 28 tsdata.timemetadata.UserData # name: # type: sq_string # elements: 1 # length: 81 tsdata.timemetadata: property UserData Any data the user attaches, [] by default. # name: # type: sq_string # elements: 1 # length: 42 Any data the user attaches, [] by default. # name: # type: sq_string # elements: 1 # length: 23 tsdata.timemetadata.get # name: # type: sq_string # elements: 1 # length: 603 tsdata.timemetadata: S = get (ti) tsdata.timemetadata: value = get (ti, name) tsdata.timemetadata: values = get (ti, names) Return property values. S = get (ti) returns a structure of every property of ti. value = get (ti, name) returns the property name, a character vector or a string scalar matched in any case, and values = get (ti, names) a row cell array of the properties named in the cell array names. For an array, one name gives a cell array of its size, and no name, or several, a cell array with a row per object and a column per property. See also: tsdata.timemetadata.set, timeseries.get # name: # type: sq_string # elements: 1 # length: 23 Return property values. # name: # type: sq_string # elements: 1 # length: 23 tsdata.timemetadata.set # name: # type: sq_string # elements: 1 # length: 610 tsdata.timemetadata: set (ti, name, value, ...) tsdata.timemetadata: ti2 = set (ti, name, value, ...) tsdata.timemetadata: S = set (ti) Set property values. set (ti, name, value, ...) sets each property name, matched in any case, to its value, in the order given, in the variable ti of the caller, which must therefore be a variable. Metadata held by a timeseries is set with dot assignment instead, as ts.TimeInfo.Units = 'days'. ti2 = set (ti, name, value, ...) returns the modified object and leaves ti as it was. S = set (ti) returns what get (ti) does. See also: tsdata.timemetadata.get, timeseries.set # name: # type: sq_string # elements: 1 # length: 20 Set property values. # name: # type: sq_string # elements: 1 # length: 32 tsdata.timemetadata.timemetadata # name: # type: sq_string # elements: 1 # length: 698 tsdata.timemetadata: ti = tsdata.timemetadata () tsdata.timemetadata: ti = tsdata.timemetadata (time) Create a time metadata object. ti = tsdata.timemetadata () returns an object for an empty time vector: Units 'seconds', Format and StartDate '', Length 0, and Start, End and Increment []. ti = tsdata.timemetadata (time) derives Length, Start, End and Increment from the numeric vector time, which must be finite and non-decreasing. MATLAB gives an Increment of NaN here even for a uniform time; here it is the step, as for a timeseries. A timeseries creates its own; this is needed only to assign a whole new one to ts.TimeInfo, whose time vector is then the series' own. See also: timeseries # name: # type: sq_string # elements: 1 # length: 30 Create a time metadata object. # name: # type: sq_string # elements: 1 # length: 7 vartype # name: # type: sq_string # elements: 1 # length: 169 datatypes: vartype Subscript into a table by variable type. A utility class that facilitates subscripting table variables according to their data type. See also: table # name: # type: sq_string # elements: 1 # length: 40 Subscript into a table by variable type. # name: # type: sq_string # elements: 1 # length: 16 vartype.varMatch # name: # type: sq_string # elements: 1 # length: 264 vartype: TF = varMatch (obj, value) Match a vartype object to a variable value. TF = varMatch (obj, value) compares the class type of the variable in value with the predefined class type in the vartype object. If they are equal, TF is true, otherwise TF is false. # name: # type: sq_string # elements: 1 # length: 43 Match a vartype object to a variable value. # name: # type: sq_string # elements: 1 # length: 15 vartype.vartype # name: # type: sq_string # elements: 1 # length: 498 vartype: S = vartype (type) Create a subscript into table by variable type. S = vartype (type) creates a subscript to select table variables of a specified type. The input argument, type, must be a character vector or a string scalar that specifies any type that is accepted by the isa function, such as numeric, logical, integer, string, categorical, etc. It can also be cellstr to select variables that contain cell arrays of character vectors or numeric to select variables with numeric values. # name: # type: sq_string # elements: 1 # length: 47 Create a subscript into table by variable type. # name: # type: sq_string # elements: 1 # length: 7 withtol # name: # type: sq_string # elements: 1 # length: 354 datatypes: withtol Subscript into a timetable by times matched within a tolerance. A utility class that selects the rows of a timetable whose times fall within a tolerance of the times asked for. It exists because a row time is rarely known to the last microsecond, so asking for one exactly would usually select nothing. See also: timerange, timetable # name: # type: sq_string # elements: 1 # length: 63 Subscript into a timetable by times matched within a tolerance. # name: # type: sq_string # elements: 1 # length: 18 withtol.rowIndices # name: # type: sq_string # elements: 1 # length: 244 withtol: ix = rowIndices (wt, rowTimes) Return the positions in rowTimes the subscript selects. The positions come back grouped by the time that matched them, in the order those times were asked for, rather than in the order the rows are held. # name: # type: sq_string # elements: 1 # length: 55 Return the positions in rowTimes the subscript selects. # name: # type: sq_string # elements: 1 # length: 15 withtol.withtol # name: # type: sq_string # elements: 1 # length: 667 withtol: wt = withtol (subscriptTimes, tol) Create a tolerant time subscript. wt = withtol (subscriptTimes, tol) creates a subscript selecting the rows of a timetable whose times are within tol of one of subscriptTimes. The window is closed: a row exactly tol away is selected. subscriptTimes may be a datetime or duration vector, or text that reads as one. tol must be a nonnegative duration scalar; a bare number is not accepted, there being nothing to say what unit it is in. The tolerance must be less than half the smallest gap between the times asked for, so that their windows cannot overlap and no row can be selected twice. See also: timerange, timetable # name: # type: sq_string # elements: 1 # length: 33 Create a tolerant time subscript. # name: # type: sq_string # elements: 1 # length: 11 xlsx2struct # name: # type: sq_string # elements: 1 # length: 835 datatypes: s = xlsx2struct (filename) Read every sheet of an Excel workbook into a scalar structure. s = xlsx2struct (filename) reads each sheet of the Office Open XML workbook named by filename (.xlsx or .xlsm) into a table and returns a scalar structure with one field per sheet, in sheet order. Each sheet is read as by readtable (variable names from the first row, types detected automatically); it is the inverse of struct2xlsx. A sheet name that is not a valid structure field name is canonicalised with matlab.lang.makeValidName (and made unique if two names collide); when the field name differs from the sheet name the original is stored on that field's table as the 'ActualSheetName' custom property, so a subsequent struct2xlsx restores the exact sheet name. See also: struct2xlsx, readtable, table.writetable, ods2struct # name: # type: sq_string # elements: 1 # length: 62 Read every sheet of an Excel workbook into a scalar structure. # name: # type: sq_string # elements: 1 # length: 5 years # name: # type: sq_string # elements: 1 # length: 432 datatypes: D = years (X) Fixed-time duration in years. D = years (X) returns a duration array representing fixed-time duration years equivalent to the values in X, which must be a numeric array. A fixed-length year is equal to 365.2425 days. years is also available as a method for duration arrays, in which case it performs the opposite conversion. See also: duration, days, hours, minutes, seconds, milliseconds, duration.years # name: # type: sq_string # elements: 1 # length: 29 Fixed-time duration in years. pr0m1th3as-datatypes-9450b29/inst/duration.m000066400000000000000000007057641527370255000207010ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef duration ## -*- texinfo -*- ## @deftp {datatypes} duration ## ## Array representing durations of time using fixed-length time units. ## ## @qcode{duration} values are stored internally as @qcode{double} type array ## representing numbers of elapsed days as a fixed-length time unit. By ## default, fractional seconds of duration values are not displayed, but their ## actual precision is closer to nanoseconds for typical time lengths. ## ## @code{duration} arrays can be created through their constructor by ## combining numeric arrays representing individual fixed-length elapsed time ## units or through the functions @code{years}, @code{days}, @code{hours}, ## @code{minutes}, @code{seconds}, and @code{calyears}, which create ## fixed-length durations in terms of a single duration units. These ## functions are also available as methods of @code{duration} arrays to ## extract individual duration units as numeric arrays. ## ## @seealso{calendarDuration, datetime} ## @end deftp properties ## -*- texinfo -*- ## @deftp {duration} {property} Format ## ## Display format ## ## Display format, specified as a character vector or string scalar. If ## specified as a string scalar, it is converted and stored internally as ## a character vector. ## ## @end deftp Format = 'hh:mm:ss' endproperties properties (SetAccess = private, Hidden) ## Duration length in milliseconds. MATLAB stores a duration this way too, ## and its representable set follows from it: 'eps' of a duration is the ## spacing of a double at that many milliseconds, which is what this class ## reproduces. Storing days instead put the grid somewhere else and cost ## about three significant digits on every conversion back to seconds. Millis = 0 endproperties methods (Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif __disp__ (this, 'duration', in_name); endfunction ## Custom display function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif __disp__ (this, 'duration'); endfunction endmethods methods (Static, Access = public) ## -*- texinfo -*- ## @deftypefn {duration} {@var{E} =} duration.empty () ## @deftypefnx {duration} {@var{E} =} duration.empty (@var{sz}) ## @deftypefnx {duration} {@var{E} =} duration.empty (@var{m}, @var{n}, @dots{}) ## ## Create an empty duration array. ## ## @code{@var{E} = duration.empty ()} returns a @math{0*0} empty duration ## array. @code{duration.empty (@var{m}, @var{n}, @dots{})} or ## @code{duration.empty (@var{sz})} returns an empty duration array of the ## requested size, which must have at least one dimension equal to zero. A ## lone dimension gives a square size, so @code{duration.empty (3)} is an ## error while @code{duration.empty (0)} is @math{0*0}. As for ## @code{zeros}, a negative dimension counts as zero, and a size vector with ## nothing in it names no size and gives @math{0*0}. ## ## @end deftypefn function E = empty (varargin) if (nargin == 0) sz = [0, 0]; elseif (nargin == 1 && ! isscalar (varargin{1})) sz = double (varargin{1}(:)).'; else sz = [varargin{:}]; endif if (! (isnumeric (sz) && isrow (sz) && all (sz == fix (sz)))) error ("duration.empty: dimensions must be integer values."); endif ## A negative dimension is no smaller than none at all, and a size vector ## holding no dimensions names no size: 'zeros' and every other array ## constructor read both the same way. sz = max (sz, 0); if (isempty (sz)) sz = [0, 0]; elseif (isscalar (sz)) sz = [sz, sz]; endif if (all (sz != 0)) error (strcat ("duration.empty: at least one dimension must be", ... " zero for an empty array.")); endif E = duration (zeros (sz), zeros (sz), zeros (sz)); endfunction endmethods ################################################################################ ## ** Create and convert 'duration' type ** ## ################################################################################ ## Available Methods ## ## ## ## 'duration' 'dispstrings' 'cellstr' 'char' ## ## 'datevec' 'datenum' 'hms' 'years' ## ## 'days' 'hours' 'minutes' 'seconds' ## ## 'milliseconds' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {duration} {@var{D} =} duration (@var{X}) ## @deftypefnx {duration} {@var{D} =} duration (@var{H}, @var{MI}, @var{S}) ## @deftypefnx {duration} {@var{D} =} duration (@var{H}, @var{MI}, @var{S}, @var{MS}) ## @deftypefnx {duration} {@var{D} =} duration (@var{TimeStrings}) ## @deftypefnx {duration} {@var{D} =} duration (@var{TimeStrings}, @qcode{'InputFormat'}, @var{INFMT}) ## @deftypefnx {duration} {@var{D} =} duration (@dots{}, @qcode{'Format'}, @var{FMT}) ## ## Create a new array of fixed-length time durations. ## ## @code{@var{D} = duration (@var{X})} creates a column vector of durations ## from a numeric matrix. ## ## @code{@var{D} = duration (@var{H}, @var{MI}, @var{S})} creates a duration ## array from numeric arrays containing the number of hours, minutes, and ## seconds specified by @var{H}, @var{MI} and @var{S}, respectively. ## ## @code{@var{D} = duration (@var{H}, @var{MI}, @var{S}, @var{MS})} creates ## a duration array from numeric arrays containing the number of hours, ## minutes, seconds, and milliseconds specified by @var{H}, @var{MI}, ## @var{S}, and @var{MS}, respectively. ## ## @code{@var{D} = duration (@var{TimeStrings})} creates a duration array ## from text that represents elapsed times. @var{TimeStrings} can be a ## character vector, a cell array of character vectors, or a string array ## representing times using either the @qcode{'hh:mm:ss'} or the ## @qcode{'dd:hh:mm:ss'} format. ## ## @code{@var{D} = duration (@var{TimeStrings}, @qcode{'InputFormat'}, ## @var{INFMT})} creates a duration array from text that represents elapsed ## times according to the format specified by @var{INFMT}, which can be any ## of the following: ## ## @itemize ## @item @qcode{'dd:hh:mm:ss'} ## @item @qcode{'hh:mm:ss'} ## @item @qcode{'mm:ss'} ## @item @qcode{'hh:mm'} ## @item Any of the first three formats can also be appended with up to nine ## @qcode{S} characters to indicate fractional second digits, such as ## @qcode{'dd:hh:mm:ss.SS'} or @qcode{'mm:ss.SS'}. ## @end itemize ## ## @code{@var{D} = duration (@dots{}, @qcode{'Format'}, @var{FMT})} ## specifies the format in which @var{D} is displayed. @var{FMT} can ## specify either a digital timer, which can have any of the valid formats ## for @qcode{'InputFormat'} as shown above or a single number with time ## units by specifying one of the following: ## ## @itemize ## @item @qcode{'y'} fixed-length years (1 year equals 365.2425 days) ## @item @qcode{'d'} fixed-length days (1 day equals 24 hours) ## @item @qcode{'h'} hours ## @item @qcode{'m'} minutes ## @item @qcode{'s'} seconds ## @end itemize ## ## @code{@var{D} = duration ()} returns a scalar array of durations with an ## elapsed time value of zero. To create an empty duration array, use ## @code{duration ([], [], [])}. ## ## @seealso{years, days, hours, minutes, seconds, milliseconds, duration, ## isduration, calendarDuration, datetime} ## @end deftypefn function this = duration (varargin) ## Return a scalar duration object if (nargin == 0) return endif ## Parse optional Name-Value paired arguments optNames = {'Format', 'InputFormat'}; dfValues = {[], []}; [Format, inputFormat, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Check optional 'Format' and 'InputFormat' arguments if (! isempty (Format)) ## Convert string to character vector if necessary if (isstring (Format)) if (! isscalar (Format)) error (strcat ("duration: 'Format' must be a character", ... " vector or a string scalar.")); endif Format = char (Format); elseif (! (ischar (Format) && isrow (Format))) error (strcat ("duration: 'Format' must be a character", ... " vector or a string scalar.")); endif errmsg = checkFormatString (Format); if (! isempty (errmsg)) error ("duration: %s", errmsg); endif this.Format = Format; endif if (! isempty (inputFormat)) ## Convert string to character vector if necessary if (isstring (inputFormat)) if (! isscalar (inputFormat)) error (strcat ("duration: 'InputFormat' must be a", ... " character vector or a string scalar.")); endif inputFormat = char (inputFormat); elseif (! (ischar (inputFormat) && isrow (inputFormat))) error (strcat ("duration: 'InputFormat' must be a character", ... " vector or a string scalar.")); endif errmsg = checkInputFormatString (inputFormat); if (! isempty (errmsg)) error ("duration: %s", errmsg); endif endif ## Parse inputs switch (numel (args)) ## this = duration () case 0 return ## this = duration (X) case 1 X = args{1}; if (isnumeric (X)) if (! ismatrix (X)) error ("duration: numeric X must be a matrix."); endif if (! isreal (X)) error ("duration: numeric X must be real."); endif if (size (X, 2) == 3) H = X(:,1); MI = X(:,2); S = X(:,3); else error ("duration: X must have 3 columns."); endif [~, this.Millis] = hms2millis (H, MI, S); ## Return a warning if InputFormat is defined if (! isempty (inputFormat)) warning (strcat ("duration: 'InputFormat' has no effect", ... " on numeric data.")); endif elseif (iscellstr (X) || ischar (X) || isa (X, "string")) if (! iscellstr (X)) X = cellstr (X); endif this.Millis = timestrings2millis (X, inputFormat); else error ("duration: invalid type of single input data argument."); endif ## this = duration (H, MI, S) case 3 [H, MI, S] = args{:}; if (! (isnumeric (H) && isnumeric (MI) && isnumeric (S))) error ("duration: H, MI, and S must be numeric arrays."); endif if (! (isreal (H) && isreal (MI) && isreal (S))) error ("duration: H, MI, and S must be real."); endif ## Expansion is handled by the helper function [err, millis] = hms2millis (H, MI, S); if (err > 0) error ("duration: H, MI, and S must be of common size or scalars."); endif this.Millis = millis; ## Return a warning if InputFormat is defined if (! isempty (inputFormat)) warning ("duration: 'InputFormat' has no effect on numeric data."); endif ## this = duration (H, MI, S, MS) case 4 [H, MI, S, MS] = args{:}; if (! (isnumeric (H) && isnumeric (MI) && isnumeric (S) && ... isnumeric (MS))) error ("duration: H, MI, S, and MS must be numeric arrays."); endif if (! (isreal (H) && isreal (MI) && isreal (S) && isreal (MS))) error ("duration: H, MI, S, and MS must be real."); endif ## Expansion is handled by the helper function [err, millis] = hms2millis (H, MI, S, MS); if (err > 0) error (strcat ("duration: H, MI, S, and MS must be of", ... " common size or scalars.")); endif this.Millis = millis; ## Return a warning if InputFormat is defined if (! isempty (inputFormat)) warning ("duration: 'InputFormat' has no effect on numeric data."); endif otherwise error ("duration: invalid number of input arguments."); endswitch endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{cstr} =} dispstrings (@var{D}) ## ## Get display formatted strings for each element of a duration array. ## ## @code{@var{cstr} = dispstrings (@var{D})} returns a cellstr array of ## character vectors, @var{cstr}, which has the same size as the input ## duration @var{D}. ## ## @end deftypefn function cstr = dispstrings (this) ## Get display format fmt = strsplit (this.Format, '.')'; if (numel (fmt) == 1) fmt = fmt{1}; fracSec = false; else fracSec = true; fdigits = numel (fmt{2}); pat = sprintf ('.%%0%dd', fdigits); fmt = fmt{1}; endif ## Process all elements sz = size (this); cstr = cell (sz); for i = 1:prod (sz) ## The format branches below work in days, as they always have; ## only the stored unit changed underneath them. d = this.Millis(i) / 86400000; ## Handle NaNs and Infs early if (! isfinite (d)) switch (fmt) case 'y' cstr{i} = sprintf ('%g yrs', d); case 'd' cstr{i} = sprintf ('%g days', d); case 'h' cstr{i} = sprintf ('%g hr', d); case 'm' cstr{i} = sprintf ('%g min', d); case 's' cstr{i} = sprintf ('%g sec', d); otherwise cstr{i} = sprintf ('%g', d); endswitch else ## Get sign for positive/negative duration str = ''; if (d < 0) str = [str, '-']; d = abs (d); endif ## Build string according to display format if (strcmp (fmt, 'y')) years = d / 365.2425; if (years == 1) str = [str, sprintf('%g yr', years)]; else str = [str, sprintf('%g yrs', years)]; endif elseif (strcmp (fmt, 'd')) if (d == 1) str = [str, sprintf('%g day', d)]; else str = [str, sprintf('%g days', d)]; endif elseif (strcmp (fmt, 'h')) str = [str, sprintf('%g hr', d * 24)]; elseif (strcmp (fmt, 'm')) str = [str, sprintf('%g min', d * 1440)]; elseif (strcmp (fmt, 's')) str = [str, sprintf('%g sec', d * 86400)]; elseif (strcmp (fmt, 'hh:mm')) ## Convert to minutes x = d * 1440; ## Calculate hours tmp = x / 60; h = fix (tmp); ## Fix round-off errors with threshold scaled by hours if (abs (tmp - h - 1) < 1e-13 * (1 + h)) h++; endif ## Calculate remaining minutes tmp = x - h * 60; m = fix (tmp); ## Fix round-off errors with threshold scaled by minutes if (abs (tmp - m - 1) < 1e-13 * (1 + m)) m++; endif str = [str, sprintf('%02d:%02d', h, m)]; elseif (strcmp (fmt, 'mm:ss')) ## Convert to seconds x = d * 86400; ## Calculate minutes tmp = x / 60; m = fix (tmp); ## Fix round-off errors with threshold scaled by minutes if (abs (tmp - m - 1) < 1e-13 * (1 + m)) m++; endif ## Calculate remaining seconds tmp = x - m * 60; s = fix (tmp); ## Fix round-off errors with threshold scaled by seconds if (abs (tmp - s - 1) < 1e-12 * (1 + s)) s++; endif str = [str, sprintf('%02d:%02d', m, s)]; if (fracSec) fs = rem (x, 60) - s; # fraction of a second ## Promote to integer value according to requested digits ms = abs (fix (fs * 10 ^ fdigits)); str = [str, sprintf(pat, ms)]; endif elseif (strcmp (fmt, 'hh:mm:ss')) ## Convert to seconds x = d * 86400; ## Calculate hours tmp = x / 3600; h = fix (tmp); ## Fix round-off errors with threshold scaled by hours if (abs (tmp - h - 1) < 1e-13 * (1 + h)) h++; endif ## Calculate remaining duration in seconds x = x - h * 3600; ## Calculate remaining minutes tmp = x / 60; m = fix (tmp); ## Fix round-off errors with threshold scaled by minutes if (abs (tmp - m - 1) < 1e-13 * (1 + m)) m++; endif ## Calculate remaining seconds x = x - m * 60; s = fix (x); ## Fix round-off errors with threshold scaled by seconds if (abs (x - s - 1) < 1e-12 * (1 + s)) s++; endif str = [str, sprintf('%02d:%02d:%02d', h, m, s)]; if (fracSec) fs = x - s; # fraction of a second ## Round to nearest nanosecond fs = round (fs * 1e+9) * 1e-9; ## Promote to integer value according to requested digits ms = floor (fs * 10 ^ fdigits); str = [str, sprintf(pat, ms)]; endif elseif (strcmp (fmt, 'dd:hh:mm:ss')) ## Calculate days tmp = fix (d); ## Fix round-off errors with threshold scaled by days if (abs (d - tmp - 1) < 1e-15 * (1 + d)) tmp++; endif x = d - tmp; d = tmp; ## Calculate remaining duration in seconds x = x * 86400; ## Calculate hours tmp = x / 3600; h = fix (tmp); ## Fix round-off errors with threshold scaled by hours if (abs (tmp - h - 1) < 1e-13 * (1 + h)) h++; endif ## Calculate remaining duration in seconds x = x - h * 3600; ## Calculate remaining minutes tmp = x / 60; m = fix (tmp); ## Fix round-off errors with threshold scaled by minutes if (abs (tmp - m - 1) < 1e-13 * (1 + m)) m++; endif ## Calculate remaining seconds x = x - m * 60; s = fix (x); ## Fix round-off errors with threshold scaled by seconds if (abs (x - s - 1) < 1e-12 * (1 + s)) s++; endif if (fix (d) > 0) str = [str, sprintf('%02d:%02d:%02d:%02d', d, h, m, s)]; else str = [str, sprintf('%02d:%02d:%02d', h, m, s)]; endif if (fracSec) fs = x - s; # fraction of a second ## Round to nearest nanosecond fs = round (fs * 1e+9) * 1e-9; ## Promote to integer value according to requested digits ms = floor (fs * 10 ^ fdigits); str = [str, sprintf(pat, ms)]; endif endif cstr{i} = str; endif endfor endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{cstr} =} cellstr (@var{D}) ## @deftypefnx {duration} {@var{cstr} =} cellstr (@var{D}, @var{Format}) ## ## Convert duration array to a cell array of character vectors. ## ## @code{@var{cstr} = cellstr (@var{D})} returns a cellstr array of ## character vectors, @var{cstr}, which has the same size as the input ## duration @var{D}. ## ## @end deftypefn function cstr = cellstr (this, Format = '') if (! isempty (Format)) ## Convert string to character vector if necessary if (isstring (Format)) if (! isscalar (Format)) error (strcat ("duration.cellstr: FORMAT must be a", ... " character vector or a string scalar.")); endif Format = char (Format); elseif (! (ischar (Format) && isrow (Format))) error (strcat ("duration.cellstr: FORMAT must be a", ... " character vector or a string scalar.")); endif errmsg = checkFormatString (Format); if (! isempty (errmsg)) error ("duration.cellstr: %s", errmsg); endif this.Format = Format; endif cstr = dispstrings (this); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{cmat} =} char (@var{D}) ## ## Convert duration array to a character matrix. ## ## @code{@var{cmat} = char (@var{D})} returns a character matrix with one ## row per element in @var{D}, taken in column-major order. ## ## Rows shorter than the widest are padded on the @emph{left}, so the ## character matrix is right-justified, as MATLAB returns it and as the ## array itself is displayed. ## ## @end deftypefn function cmat = char (this, Format = '') if (! isempty (Format)) ## Convert string to character vector if necessary if (isstring (Format)) if (! isscalar (Format)) error (strcat ("duration.char: FORMAT must be a", ... " character vector or a string scalar.")); endif Format = char (Format); elseif (! (ischar (Format) && isrow (Format))) error (strcat ("duration.char: FORMAT must be a", ... " character vector or a string scalar.")); endif errmsg = checkFormatString (Format); if (! isempty (errmsg)) error ("duration.char: %s", errmsg); endif this.Format = Format; endif cmat = strjust (char (dispstrings (this)), 'right'); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{DV} =} datevec (@var{DT}) ## @deftypefnx {duration} {[@var{Y}, @var{MO}, @var{D}, @var{h}, @var{mi}, @var{s}] =} datevec (@var{DT}) ## ## Convert duration array to date vectors. ## ## @code{@var{DV} = datevec (@var{DT})} returns an @math{N*6} numeric matrix ## whose rows represent each element in @var{DT} and each column corresponds ## to years, months, days, hours, minutes, and seconds, respectively. Since ## months cannot be represented as a fixed length of time, the second column ## of @var{DV} is always zero. @var{DV} represents a length of time split ## across different fixed-length elapsed time units. The number of rows in ## @var{DV} equals to the number of elements in the duration array @var{DT}. ## ## @code{[@var{Y}, @var{MO}, @var{D}, @var{h}, @var{mi}, @var{s}] = datevec ## (@var{DT})} returns the components of @var{DT} as individual variables, ## but unlike @var{DV} in the previous syntax, each variable has the same ## size as the duration array @var{DT}. ## ## Values containing a fractional portion less than 1 picosecond are rounded ## to the nearest picosecond. ## ## @end deftypefn function varargout = datevec (this) d = this.Millis / 86400000; tmp = d / 365.2425; y = fix (tmp); ## Fix round-off errors with threshold scaled by years idx = abs (tmp - y - 1) < 1e-15 * y; y(idx) += 1; x = rem (d, 365.2425); d = fix (x); x = x - d; ## Get remaining duration in seconds x = x * 86400; tmp = x / 3600; h = fix (tmp); idx = abs (tmp - h - 1) < 1e-15 * h; h(idx) += 1; x = x - h * 3600; tmp = x / 60; m = fix (tmp); idx = abs (tmp - m - 1) < 1e-15 * m; m(idx) += 1; s = x - m * 60; ## Fix floating point precision to nearest picosecond s = round (s * 1e+12) * 1e-12; ## Add months column mo = zeros (size (s)); mo(isnan (d)) = NaN; ## Transform to matrix DV = [y(:), mo(:), d(:), h(:), m(:), s(:)]; if (nargout == 0 || nargout == 1) varargout{1} = DV; elseif (nargout <= 6) for i = 1:nargout ## Multiple outputs are reshaped to original input size varargout{i} = reshape (DV(:,i), size (this)); endfor else error ("duration.datevec: too many output arguments."); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{N} =} datenum (@var{D}) ## ## Convert a duration array to numbers of days. ## ## @code{@var{N} = datenum (@var{D})} returns a @code{double} array of the ## same size as @var{D} holding the length of each duration in days, ## including any fractional part. @code{NaN} durations give @code{NaN}. ## ## A duration measures elapsed time rather than naming a point in it, so the ## result is a count of days and not a serial date number: adding it to a ## serial date number shifts that date by the duration. This is the same ## value @code{days} returns, under the name the older date functions use. ## ## @end deftypefn function N = datenum (D) N = D.Millis / 86400000; endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{H} =} hms (@var{D}) ## @deftypefnx {duration} {[@var{H}, @var{M}] =} hms (@var{D}) ## @deftypefnx {duration} {[@var{H}, @var{M}, @var{S}] =} hms (@var{D}) ## ## Split duration array into separate time unit values. ## ## @code{[@var{H}, @var{M}, @var{S}] = hms (@var{D})} splits the duration ## array @var{D} into separate numeric arrays @var{H}, @var{M}, and @var{S}, ## which correspond to hours, minutes, and seconds, respectively. Hours and ## minutes are returned as whole numbers, while seconds may also have a ## fractional part. ## ## Each component is what truncating division leaves, so every one of them ## carries the sign of the duration as a whole and a span a hair under an ## hour reports 59 minutes rather than a whole one. Nothing is snapped to a ## whole unit and nothing is rounded to a fixed precision. ## ## A duration that is not finite has no components to divide between: all ## three take that same infinity, or @code{NaN}. ## ## @end deftypefn function varargout = hms (this) ## Each component is what truncating division leaves, exactly as MATLAB ## computes it. Nothing is snapped to a whole unit and nothing is rounded ## to a fixed precision: a span a hair under an hour must report 59 ## minutes, and a fraction of a second must survive whichever side of zero ## it falls. Truncation towards zero also gives every component the sign ## of the whole, down to the negative zeros MATLAB returns. x = this.Millis / 1000; h = fix (x / 3600); x = x - h * 3600; m = fix (x / 60); s = x - m * 60; ## A non-finite duration has no components to divide between: each of ## them is that same infinity, or NaN, as MATLAB returns. Subtracting ## the hours off an infinite count of seconds otherwise leaves NaN ## behind in the minutes and seconds. nf = ! isfinite (this.Millis); if (any (nf, 'all')) h(nf) = this.Millis(nf); m(nf) = this.Millis(nf); s(nf) = this.Millis(nf); endif if (nargout == 0 || nargout == 1) varargout{1} = h; elseif (nargout == 2) varargout{1} = h; varargout{2} = m; elseif (nargout == 3) varargout{1} = h; varargout{2} = m; varargout{3} = s; else error ("duration.hms: too many output arguments."); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{X} =} years (@var{D}) ## ## Duration equivalent numeric values in fixed-length years. ## ## @code{@var{X} = years (@var{D})} converts durations in @var{D} to the ## equivalent number of fixed-length years (1 year equals 365.2425 days). ## @var{X} is a double array of the same size as @var{D}. ## ## @end deftypefn function out = years (this) out = this.Millis / 31556952000; endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{X} =} days (@var{D}) ## ## Duration equivalent numeric values in fixed-length days. ## ## @code{@var{X} = days (@var{D})} converts durations in @var{D} to the ## equivalent number of fixed-length days (1 day equals 24 hours). @var{X} ## is a double array of the same size as @var{D}. ## ## @end deftypefn function out = days (this) out = this.Millis / 86400000; endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{X} =} hours (@var{D}) ## ## Duration equivalent numeric values in hours. ## ## @code{@var{X} = hours (@var{D})} converts durations in @var{D} to the ## equivalent number of hours. @var{X} is a double array of the same size ## as @var{D}. ## ## @end deftypefn function out = hours (this) out = this.Millis / 3600000; endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{X} =} minutes (@var{D}) ## ## Duration equivalent numeric values in minutes. ## ## @code{@var{X} = minutes (@var{D})} converts durations in @var{D} to the ## equivalent number of minutes. @var{X} is a double array of the same size ## as @var{D}. ## ## @end deftypefn function out = minutes (this) out = this.Millis / 60000; endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{X} =} seconds (@var{D}) ## ## Duration equivalent numeric values in seconds. ## ## @code{@var{X} = seconds (@var{D})} converts durations in @var{D} to the ## equivalent number of seconds. @var{X} is a double array of the same size ## as @var{D}. ## ## @end deftypefn function out = seconds (this) out = this.Millis / 1000; endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{X} =} milliseconds (@var{D}) ## ## Duration equivalent numeric values in milliseconds. ## ## @code{@var{X} = milliseconds (@var{D})} converts durations in @var{D} to ## the equivalent number of milliseconds. @var{X} is a double array of the ## same size as @var{D}. ## ## @end deftypefn function out = milliseconds (this) out = this.Millis; endfunction endmethods ################################################################################ ## ** Summary Information ** ## ################################################################################ ## Available Methods ## ## ## ## 'size' 'ndims' 'numel' 'nnz' ## ## 'length' 'keyHash' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {duration} {@var{sz} =} size (@var{D}) ## @deftypefnx {duration} {@var{dim_sz} =} size (@var{D}, @var{dim}) ## @deftypefnx {duration} {@var{dim_sz} =} size (@var{D}, @var{d1}, @var{d2}, @dots{}) ## @deftypefnx {duration} {[@var{rows}, @var{columns}, @dots{}, @var{dim_n_sz}] =} size (@dots{}) ## ## Return the size of a duration array. ## ## @code{@var{sz} = size (@var{D})} returns a row vector with the size ## (number of elements) of each dimension for the duration array @var{D}. ## ## @code{@var{dim_sz} = size (@var{D}, @var{dim})} returns the size of ## the corresponding dimension specified in @var{dim}. If @var{dim} is a ## vector, then @var{dim_sz} is a vector of the same length and with each ## element corresponding to a specified dimension. Multiple dimensions may ## also be specified as separate arguments. ## ## With a single output argument, @code{size} returns a row vector. With ## several, the size of dimension N is returned in the Nth argument. ## Asking for fewer output arguments than the array has dimensions folds ## the trailing dimensions into the last one, so @code{[r, c] = size ## (@var{D})} on a 2-by-2-by-2 array returns 2 and 4; asking for more pads ## with ones. When a dimension is named, however, the number of output ## arguments must equal the number of dimensions requested. ## ## @end deftypefn function varargout = size (this, varargin) if (! isempty (varargin)) ## A named dimension fixes how many outputs there may be. sz = size (this.Millis, varargin{:}); if (nargout > 1 && numel (sz) != nargout) error (strcat ("duration.size: nargout does not match the", " number of requested dimensions.")); endif else sz = size (this.Millis); ## With no dimension named, asking for more outputs than the array has ## dimensions pads with ones, and asking for fewer folds the trailing ## dimensions into the last output, as core Octave and MATLAB both do. if (nargout > numel (sz)) sz = [sz, ones(1, nargout - numel (sz))]; elseif (nargout > 1 && nargout < numel (sz)) sz = [sz(1:nargout-1), prod(sz(nargout:end))]; endif endif if (nargout == 0 || nargout == 1) varargout{1} = sz; else for i = 1:nargout varargout{i} = sz(i); endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{out} =} ndims (@var{D}) ## ## Number of dimensions in a duration array. ## ## @code{@var{out} = ndims (@var{D})} returns the number of dimensions of ## the duration array @var{D}. ## ## @end deftypefn function out = ndims (this) out = ndims (this.Millis); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{out} =} numel (@var{D}) ## ## Total number of elements in a duration array. ## ## @code{@var{out} = numel (@var{D})} returns the number of elements in the ## duration array @var{D}. ## ## @end deftypefn function out = numel (this, varargin) out = numel (this.Millis); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{out} =} nnz (@var{D}) ## ## Number of nonzero elements in duration array. ## ## @code{@var{out} = nnz (@var{D})} returns the number of nonzero ## elements in the duration array @var{D}. ## ## @end deftypefn function out = nnz (this) d = this.Millis(:); out = numel (d) - sum (d == 0); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{N} =} length (@var{D}) ## ## Length of a duration vector. ## ## @code{@var{N} = length (@var{D})} returns the size of the longest ## dimension of the duration array @var{D}, unless any of its dimensions has ## zero length, in which case @code{length (@var{D})} returns 0. ## ## @end deftypefn function N = length (this) if (isempty (this.Millis)) N = 0; else N = max (size (this.Millis)); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{key} =} keyHash (@var{D}) ## @deftypefnx {duration} {@var{key} =} keyHash (@var{D}, @var{base}) ## ## Generate a hash code for duration array. ## ## @code{@var{key} = keyHash (@var{D})} generates a @qcode{uint64} scalar ## that represents the input array @var{D}. @code{keyHash} utilizes the ## 64-bit FNV-1a variant of the Fowler-Noll-Vo non-cryptographic hash ## function. ## ## @code{@var{key} = keyHash (@var{D}, @var{base})} also generates a 64-bit ## hash code using @var{base} as the offset basis for the FNV-1a hash ## algorithm. @var{base} must be a @qcode{uint64} integer type scalar. Use ## this syntax to cascade @code{keyHash} on multiple objects for which a ## single hash code is required. ## ## A duration array is keyed on its length in milliseconds, so the unit it ## was written in is not part of the key and @code{seconds (1)} hashes as ## @code{milliseconds (1000)}. A missing duration is keyed alike whatever ## produced its @qcode{NaN}. The @qcode{Format} property is display only ## and is not part of the key. Two arrays that @code{keyMatch} reports as ## the same key always hash alike. ## ## Note that unlike MATLAB, this implementation does not use any random ## seed. As a result, @code{keyHash} will always generate the exact same ## hash key for any particular input across different workers and Octave ## sessions. ## ## @end deftypefn function key = keyHash (this, base = []) ## Initialize string with size and class name size_str = sprintf ('%dx', size (this.Millis))(1:end-1); init_str = [size_str class(this)]; if (! isempty (base)) if (! (isscalar (base) && isa (base, 'uint64'))) error ("duration.keyHash: BASE must be a UINT64 scalar."); endif key = __ckeyHash__(init_str, base); else key = __ckeyHash__(init_str); endif if (! isempty (this.Millis)) key = __nkeyHash__(this.Millis(:), key); endif endfunction endmethods ################################################################################ ## ** Query Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'isbetween' 'iscolumn' 'isempty' 'isequal' ## ## 'isequaln' 'isfinite' 'isinf' 'ismatrix' ## ## 'ismember' 'ismissing' 'isnan' 'isregular' ## ## 'isrow' 'isscalar' 'issorted' 'issortedrows' ## ## 'isvector' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} isbetween (@var{D}, @var{lower}, @var{upper}) ## @deftypefnx {duration} {@var{TF} =} isbetween (@var{D}, @var{lower}, @var{upper}, @var{intervalType}) ## ## Find duration elements within specified range. ## ## @code{@var{TF} = isbetween (@var{D}, @var{lower}, @var{upper})} returns a ## logical array, @var{TF}, which is the same size as the input duration ## array @var{D} and it contains @qcode{true} for each corresponding element ## which is within the range specified by @var{lower} and @var{upper} and ## @qcode{false} otherwise. @var{lower} and @var{upper} must be duration ## arrays of compatible size with @var{D} or alternatively they can be ## specified as a character vector, a cell array of character vectors or a ## string array containing valid text duration representations. ## ## @code{@var{TF} = isbetween (@var{D}, @var{lower}, @var{upper}, ## @var{intervalType})} specifies the type of interval for the @var{lower} ## and @var{upper} bounds and it can be one of the following values. ## ## @itemize ## @item @qcode{'closed'} (default) includes lower and upper bounds. ## ## @item @qcode{'open'} excludes lower and upper bounds. ## ## @item @qcode{'openleft'} or @qcode{'closedright'} exclude the lower and ## include the upper bound. They have identical behavior. ## ## @item @qcode{'closedleft'} or @qcode{'openright'} include the lower and ## exclude the upper bound. They have identical behavior. ## @end itemize ## ## @var{intervalType} can be specified either as a character vector or a ## string scalar. ## ## @end deftypefn function TF = isbetween (this, lower, upper, varargin) if (nargin < 3) error ("duration.isbetween: too few input arguments."); elseif (nargin == 3) intervaltype = 'closed'; elseif (nargin == 4) intervaltype = varargin{1}; elseif (nargin > 4) error (strcat ("duration.isbetween: optional paired arguments", ... " are not supported for duration arrays.")); endif if (isnumeric (lower) || isnumeric (upper)) error ("duration.isbetween: LOWER and UPPER cannot be numeric."); endif if (strcmpi (intervaltype, 'closed')) TF = lower <= this & this <= upper; elseif (strcmpi (intervaltype, 'open')) TF = lower < this & this < upper; elseif (any (strcmpi (intervaltype, {'openleft', 'closedright'}))) TF = lower < this & this <= upper; elseif (any (strcmpi (intervaltype, {'openright', 'closedleft'}))) TF = lower <= this & this < upper; else error ("duration.isbetween: invalid INTERVALTYPE option."); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} iscolumn (@var{D}) ## ## Return true if duration array is a column vector. ## ## @code{@var{TF} = iscolumn (@var{D})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the duration array @var{D} is a column vector ## and @qcode{false} otherwise. A column vector is a 2-D array for which ## @code{size (@var{D})} returns @code{[@var{N}, 1]} with non-negative ## @var{N}. By definition, a scalar is also a column vector. ## ## @end deftypefn function TF = iscolumn (this) TF = iscolumn (this.Millis); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} isempty (@var{D}) ## ## Return true if duration array is empty. ## ## @code{@var{TF} = isempty (@var{D})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the duration array @var{D} is empty and ## @qcode{false} otherwise. ## ## @end deftypefn function TF = isempty (this) TF = isempty (this.Millis); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} isequal (@var{D1}, @var{D2}) ## @deftypefnx {duration} {@var{TF} =} isequal (@var{D1}, @var{D2}, @dots{}) ## ## Return true if duration arrays are equal. ## ## @code{@var{TF} = isequal (@var{D1}, @var{D2})} returns a logical scalar ## @var{TF}, which is @qcode{true}, if the duration arrays @var{D1} and ## @var{D2} contain the same values, and @qcode{false} otherwise. Either ## @var{D1} or @var{D2} can also be specified as a character vector, a cell ## array of character vectors or a string array containing valid text ## duration representations. ## ## @code{@var{TF} = isequal (@var{D1}, @var{D2}, @dots{})} returns a logical ## scalar @var{TF}, which is @qcode{true}, if all input arguments are equal, ## and @qcode{false} otherwise. ## ## @end deftypefn function TF = isequal (varargin) args = varargin; [args{:}] = promote (varargin{:}); mils = cellfun (@(obj) obj.Millis, args, 'UniformOutput', false); TF = isequal (mils{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} isequaln (@var{D1}, @var{D2}) ## @deftypefnx {duration} {@var{TF} =} isequaln (@var{D1}, @var{D2}, @dots{}) ## ## Return true if duration arrays are equal under the assumption that ## missing elements are equal. ## ## @code{@var{TF} = isequaln (@var{D1}, @var{D2})} returns a logical scalar ## @var{TF}, which is @qcode{true}, if the duration arrays @var{D1} and ## @var{D2} contain the same values or corresponding missing elements, and ## @qcode{false} otherwise. Either @var{D1} or @var{D2} can also be ## specified as a character vector, a cell array of character vectors or a ## string array containing valid text duration representations. ## ## @code{@var{TF} = isequaln (@var{D1}, @var{D2}, @dots{})} returns a ## logical scalar @var{TF}, which is @qcode{true}, if all input arguments ## are equal under the assumption that missing elements are equal, and ## @qcode{false} otherwise. ## ## @end deftypefn function TF = isequaln (varargin) args = varargin; [args{:}] = promote (varargin{:}); mils = cellfun (@(obj) obj.Millis, args, 'UniformOutput', false); TF = isequaln (mils{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} isfinite (@var{D}) ## ## Return true for duration elements that are finite. ## ## @code{@var{TF} = isfinite (@var{D})} returns a logical array @var{TF} ## of the same size as @var{calD} containing @qcode{true} for each ## corresponding element of @var{D} that is finite and @qcode{false} ## otherwise. Finite elements are those which are neither infinite nor ## Not-A-Number. ## ## @end deftypefn function TF = isfinite (this) TF = isfinite (this.Millis); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} isinf (@var{D}) ## ## Return true for duration elements that are infinite. ## ## @code{@var{TF} = isinf (@var{D})} returns a logical array @var{TF} ## of the same size as @var{D} containing @qcode{true} for each ## corresponding element of @var{calD} that is either @qcode{Inf} or ## @qcode{-Inf} and @qcode{false} otherwise. ## ## @end deftypefn function TF = isinf (this) TF = isinf (this.Millis); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} ismatrix (@var{D}) ## ## Return true if duration array is a 2-D array. ## ## @code{@var{TF} = ismatrix (@var{D})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the duration array @var{D} is a matrix and ## @qcode{false} otherwise. A matrix is an array of any type where ## @code{ndims (@var{D}) == 2}. By definition, a scalar is also a matrix. ## ## @end deftypefn function TF = ismatrix (this) TF = ismatrix (this.Millis); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} ismember (@var{A}, @var{B}) ## @deftypefnx {duration} {@var{TF} =} ismember (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {duration} {[@var{TF}, @var{index}] =} ismember (@dots{}) ## @deftypefnx {duration} {[@var{TF}, @var{index}] =} ismember (@dots{}, @qcode{'legacy'}) ## ## Find duration elements in a set. ## ## @code{@var{TF} = ismember (@var{A}, @var{B})} returns a logical array ## @var{TF} of the same size as @var{A} containing @qcode{true} for each ## corresponding element of @var{A} that is in @var{B} and @qcode{false} ## otherwise. @qcode{NaN} elements are not equal with each other and always ## return @qcode{false}. ## ## @code{@var{TF} = ismember (@var{A}, @var{B}, @qcode{'rows'})} only ## applies to duration matrices with the same number of columns, in which ## case the logical vector @var{TF} contains @qcode{true} for each row of ## @var{A} that is also a row in @var{B}. @var{TF} has the same number of ## rows as @var{A}. ## ## @code{[@var{TF}, @var{index}] = ismember (@var{A}, @var{B})} also returns ## an index array of the same size as @var{A} containing the lowest index in ## @var{B} for each element of @var{A} that is a member of @var{B} and 0 ## otherwise. If the @qcode{'rows'} optional argument is used, then the ## returning index is a column vector with the same rows as @var{A} and it ## contains the lowest index in @var{B} for each row of @var{A} that is a ## member of @var{B} and 0 otherwise. If the @qcode{'legacy'} optional ## argument is specified, then the highest index of matched elements is ## returned. Unless multiple matches exist, the @qcode{'legacy'} option has ## no effect on the returned @var{index}. ## ## @end deftypefn function varargout = ismember (A, B, varargin) ## Check input arguments if (! isa (B, 'duration')) B = promote (B); endif ## Check for 'rows' and 'legacy' optional arguments if (! isempty (varargin)) if (! cellfun ('ischar', varargin)); error ("duration.ismember: all options must be character vectors."); elseif (! all (strcmpi (varargin, 'rows') | ... strcmpi (varargin, 'legacy'))) error (strcat ("duration.ismember: only 'rows' and 'legacy'", ... " are valid options.")); endif do_rows = any (strcmpi ('rows', varargin)); if (do_rows) if (ndims (A) != 2 || ndims (A) != ndims (B)) error ("duration.ismember: 'rows' applies only to 2-D matrices."); endif if (size (A, 2) != size (B, 2)) error (strcat ("duration.ismember: 'rows' requires same", ... " number of columns.")); endif endif if (nargout > 1) [varargout{1}, varargout{2}] = __ismember__ (A.Millis, B.Millis, ... varargin{:}); else varargout{1} = __ismember__ (A.Millis, B.Millis, varargin{:}); endif else if (nargout > 1) [varargout{1}, varargout{2}] = __ismember__ (A.Millis, B.Millis); else varargout{1} = __ismember__ (A.Millis, B.Millis); endif endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} ismissing (@var{D}) ## @deftypefnx {duration} {@var{TF} =} ismissing (@var{D}, @var{indicator}) ## ## Find missing elements in duration array. ## ## @code{@var{TF} = ismissing (@var{D})} returns a logical array, @var{TF}, ## with the same dimensions as @var{D}, where @code{true} values match the ## standard missing values in the input duration array. ## ## The optional input @var{indicator} can be a scalar or a vector duration ## array, specifying alternative missing values in the input data. When ## specifying @var{indicator} values, the standard missing values are ## ignored, unless explicitly stated in the @var{indicator}. A @qcode{NaN} ## duration in @var{indicator} matches the @qcode{NaN} elements of @var{D}, ## and so does @code{missing}. ## ## @end deftypefn function TF = ismissing (this, varargin) if (nargin > 2) error ("duration.ismissing: too many input arguments."); endif if (! isempty (varargin)) indicator = varargin{1}; TF = false (size (this)); if (isa (indicator, 'missing')) TF = isnan (this.Millis); elseif (isvector (indicator)) if (isa (indicator, 'duration')) for i = 1:length (indicator) if (isnan (indicator.Millis(i))) TF(isnan (this.Millis)) = true; else TF(this.Millis == indicator.Millis(i)) = true; endif endfor else error ("duration.ismissing: INDICATOR must be a 'duration' array."); endif else error ("duration.ismissing: INDICATOR must be a vector."); endif else TF = isnan (this.Millis); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} isnan (@var{D}) ## ## Return true for duration elements that are Not-A-Number. ## ## @code{@var{TF} = isnan (@var{D})} returns a logical array @var{TF} of ## the same size as @var{D} containing @qcode{true} for each corresponding ## element of @var{calD} that is @qcode{NaN} and @qcode{false} otherwise. ## ## @end deftypefn function TF = isnan (this) TF = isnan (this.Millis); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} isregular (@var{D}) ## @deftypefnx {duration} {@var{TF} =} isregular (@var{D}, @var{COMPONENT}) ## @deftypefnx {duration} {[@var{TF}, @var{DT}] =} isregular (@dots{}) ## ## Return true if a duration vector is regularly spaced. ## ## @code{@var{TF} = isregular (@var{D})} returns true when the elements of ## the duration vector @var{D} are evenly spaced, that is, when every step ## from one element to the next is the same and is not zero. A second ## output @var{DT} gives that step, and is @code{NaN} when there is none. ## ## A sequence that does not move is not regularly spaced: a step of zero ## describes no spacing, so @code{isregular} of a constant vector is false. ## A descending vector is regular, with a negative step. A @code{NaN} or an ## infinite element anywhere makes the vector irregular, as does having ## fewer than two elements, so a scalar and an empty array are both false. ## @var{D} must be a vector; a matrix is an error. ## ## @code{@var{TF} = isregular (@var{D}, @var{COMPONENT})} asks about a ## particular component, one of @qcode{'Years'}, @qcode{'Quarters'}, ## @qcode{'Months'}, @qcode{'Weeks'}, @qcode{'Days'} or @qcode{'Time'}. A ## duration measures elapsed time and knows nothing of a calendar, so only ## @qcode{'Time'}, the default, can be true; every calendar component is ## false however the vector is spaced. The argument exists so that a ## duration can be asked the same question as a @code{datetime}. ## ## Steps are compared within a few @code{eps} of the largest value present ## rather than exactly, because subtracting neighbouring durations rounds: ## even one, two and three hours have steps differing in the last bit, and ## an exact test would call that irregular. MATLAB tolerates a difference ## of about the same size, for the same reason. ## ## @strong{Deviation from MATLAB.} A negative step is returned there ## formatted in seconds whatever the vector's own format; here the step ## keeps @var{D}'s format whichever way the vector runs. ## ## @end deftypefn function [TF, DT] = isregular (this, component = 'time') comps = {'time', 'years', 'quarters', 'months', 'weeks', 'days'}; if (! (ischar (component) && isrow (component) && any (strcmpi (component, comps)))) error (strcat ("duration.isregular: COMPONENT must be 'Years',", ... " 'Quarters', 'Months', 'Weeks', 'Days', or 'Time'.")); endif DT = duration (0, 0, NaN); ## Only elapsed time can be regular: a duration has no calendar for a ## calendar component to be counted against. if (! strcmpi (component, 'time')) TF = false; return; endif ## Fewer than two elements have no step to be regular about. if (numel (this) < 2) TF = false; return; elseif (! isvector (this)) error ("duration.isregular: input must be a duration vector."); endif d = diff (this); ds = milliseconds (d); ## Steps are compared within a few eps of the largest value present, not ## exactly. Subtracting neighbouring durations can round in the last bit, ## and an exact test would then call a plainly regular vector irregular. ## The tolerance is taken from the values rather than the steps, since ## that is where the rounding comes from, and matches MATLAB's own. scale = max (abs (milliseconds (this)(:))); if (! isfinite (scale)) scale = 0; endif tol = 4 * eps (scale); TF = isfinite (ds(1)) && ds(1) != 0 ... && all (abs (ds(:) - ds(1)) <= tol); if (TF) ## Build the scalar step by setting the value rather than by indexing: ## inside the class's own methods, d(1) does not go through subsref and ## hands back the whole array. DT = d; DT.Millis = ds(1); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} isrow (@var{D}) ## ## Return true if duration array is a row vector. ## ## @code{@var{TF} = isrow (@var{D})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the duration array @var{D} is a row vector ## and @qcode{false} otherwise. A row vector is a 2-D array for which ## @code{size (@var{D})} returns @code{[1, @var{N}]} with non-negative ## @var{N}. By definition, a scalar is also a row vector. ## ## @end deftypefn function TF = isrow (this) TF = isrow (this.Millis); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} isscalar (@var{D}) ## ## Return true if duration array is a scalar. ## ## @code{@var{TF} = isscalar (@var{D})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the duration array @var{D} is also a scalar ## and @qcode{false} otherwise. A scalar is a single element object for ## which @code{size (@var{D})} returns @code{[1, 1]}. ## ## @end deftypefn function TF = isscalar (this) TF = isscalar (this.Millis); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} issorted (@var{D}) ## @deftypefnx {duration} {@var{TF} =} issorted (@var{D}, @var{dim}) ## @deftypefnx {duration} {@var{TF} =} issorted (@var{D}, @var{direction}) ## @deftypefnx {duration} {@var{TF} =} issorted (@var{D}, @var{dim}, @var{direction}) ## @deftypefnx {duration} {@var{TF} =} issorted (@dots{}, @qcode{'MissingPlacement'}, @var{MP}) ## @deftypefnx {duration} {@var{TF} =} issorted (@dots{}, @qcode{'ComparisonMethod'}, @var{CM}) ## ## Return true if duration array is sorted. ## ## @code{@var{TF} = issorted (@var{D})} returns a logical scalar @var{TF}, ## which is @qcode{true}, if the duration array @var{D} is sorted in ## ascending order, and @qcode{false} otherwise. ## ## @code{@var{TF} = issorted (@var{D}, @var{dim})} returns a logical scalar ## @var{TF}, which is @qcode{true}, if the duration array @var{D} is ## sorted in ascending order along the dimension @var{dim}, and ## @qcode{false} otherwise. ## ## @code{@var{TF} = issorted (@var{D}, @var{direction})} returns a logical ## scalar @var{TF}, which is @qcode{true}, if the duration array @var{D} ## is sorted in the direction specified by @var{direction}, and ## @qcode{false} otherwise. @var{direction} can be any of the following ## options: ## ## @itemize ## @item @qcode{'ascend'}, which is the default, checks is elements are in ## ascending order. ## @item @qcode{'descend'} checks if elements are in descending order. ## @item @qcode{'monotonic'} checks if elements are either in ascending or ## descending order. ## @item @qcode{'strictascend'} checks if elements are in ascending order ## and there are no duplicate or undefined elements. ## @item @qcode{'strictdescend'} checks if elements are in descending order ## and there are no duplicate or undefined elements. ## @item @qcode{'strictmonotonic'} checks if elements are either in ## ascending or descending order and there are no duplicate or undefined ## elements. ## @end itemize ## ## @code{@var{TF} = issorted (@dots{}, @qcode{'MissingPlacement'}, ## @var{MP})} ## specifies where missing elements (@qcode{NaN}) are placed with one of the ## following options specified in @var{MP}: ## ## @itemize ## @item @qcode{'auto'}, which is the default, places missing elements last ## for ascending sort and first for descending sort. ## @item @qcode{'first'} places missing elements first. ## @item @qcode{'last'} places missing elements last. ## @end itemize ## ## @code{@var{TF} = issorted (@dots{}, @qcode{'ComparisonMethod'}, ## @var{CM})} ## specifies the comparison method for determining the order of elements ## with one of following options specified in @var{CM}: ## ## @itemize ## @item @qcode{'auto'}, which is the default, sorts by @code{real (A)}. ## @item @qcode{'real'} sorts by @code{real (A)}. ## @item @qcode{'abs'} sorts by @code{abs (A)}. ## @end itemize ## ## @end deftypefn function TF = issorted (this, varargin) ## Single input argument if (nargin == 1) TF = isequaln (this, sort (this)); return; endif ## Get operating dimension cid = cellfun (@isnumeric, varargin); if (any (cid)) dim = varargin{cid}; else sz = size (this); dim = find (sz != 1, 1); if (isempty (dim)) # scalar dim = 1; endif endif ## Force strings to character vectors [varargin{:}] = convertStringsToChars (varargin{:}); ## Get direction from input argument list valid_direction = {'ascend', 'descend', 'monotonic', 'strictascend', ... 'strictdescend', 'strictmonotonic'}; fcn = @(x) ischar (x) && ismember (x, valid_direction); cid = cellfun (fcn, varargin); if (any (cid)) direction = lower (varargin{cid}); switch (direction) case {'ascend', 'descend'} TF = isequaln (this, sort (this, varargin{:})); case 'monotonic' ## Check for either ascending or descending varargin{cid} = 'ascend'; TF = isequaln (this, sort (this, varargin{:})); if (TF) return; endif varargin{cid} = 'descend'; TF = isequaln (this, sort (this, varargin{:})); case 'strictascend' ## Check for missing values first (fast) if (any (ismissing (this), 'all')) TF = false; return; endif varargin{cid} = strrep (direction, 'strict', ''); sorted = sort (this, varargin{:}); if (any (diff (sorted, 1, dim) <= 0, 'all')) TF = false; return; endif TF = isequaln (this, sorted); case 'strictdescend' ## Check for missing values first (fast) if (any (ismissing (this), 'all')) TF = false; return; endif varargin{cid} = strrep (direction, 'strict', ''); sorted = sort (this, varargin{:}); if (any (diff (sorted, 1, dim) >= 0, 'all')) TF = false; return; endif TF = isequaln (this, sorted); case 'strictmonotonic' ## Check missing values first (fast) if (any (ismissing (this), 'all')) TF = false; return; endif ## Check for either ascending or descending varargin{cid} = 'ascend'; sorted = sort (this, varargin{:}); if (any (diff (sorted, 1, dim) <= 0, 'all')) TF = false; return; endif TF = isequaln (this, sorted); if (TF) return; endif varargin{cid} = 'descend'; sorted = sort (this, varargin{:}); if (any (diff (sorted, 1, dim) >= 0, 'all')) TF = false; return; endif TF = isequaln (this, sorted); endswitch else ## No DIRECTION input argument TF = isequaln (this, sort (this, varargin{:})); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} issortedrows (@var{D}) ## @deftypefnx {duration} {@var{TF} =} issortedrows (@var{D}, @var{col}) ## @deftypefnx {duration} {@var{TF} =} issortedrows (@var{D}, @var{direction}) ## @deftypefnx {duration} {@var{TF} =} issortedrows (@var{D}, @var{col}, @var{direction}) ## @deftypefnx {duration} {@var{TF} =} issortedrows (@dots{}, @qcode{'MissingPlacement'}, @var{MP}) ## @deftypefnx {duration} {@var{TF} =} issortedrows (@dots{}, @qcode{'ComparisonMethod'}, @var{CM}) ## ## Return true if duration matrix rows are sorted. ## ## @code{@var{TF} = issortedrows (@var{D})} returns a logical scalar ## @var{TF}, which is @qcode{true}, if the rows in the 2-D duration array ## @var{D} are sorted in ascending order, and @qcode{false} otherwise. ## ## @code{@var{TF} = issortedrows (@var{D}, @var{col})} returns a logical ## scalar @var{TF}, which is @qcode{true}, if the duration array @var{D} ## is sorted according to the columns specified by the vector @var{col}, and ## @qcode{false} otherwise. @var{col} must explicitly contain non-zero ## integers whose absolute values index existing columns in @var{D}. ## Positive elements sort the corresponding columns in ascending order, ## while negative elements sort the corresponding columns in descending ## order. ## ## @code{@var{TF} = issortedrows (@var{D}, @var{direction})} checks if the ## rows in @var{D} are sorted according to the specified direction, which ## can be one of the following options: ## ## @itemize ## @item @qcode{'ascend'}, which is the default, checks is elements are in ## ascending order. ## @item @qcode{'descend'} checks if elements are in descending order. ## @item @qcode{'monotonic'} checks if elements are either in ascending or ## descending order. ## @item @qcode{'strictascend'} checks if elements are in ascending order ## and there are no duplicate or undefined elements. ## @item @qcode{'strictdescend'} checks if elements are in descending order ## and there are no duplicate or undefined elements. ## @item @qcode{'strictmonotonic'} checks if elements are either in ## ascending or descending order and there are no duplicate or undefined ## elements. ## @end itemize ## ## Alternatively, @var{direction} can be a cell array of character ## vectors specifying the sorting direction for each individual column of ## @var{D}, in which case the number of elements in @var{direction} must ## equal the number of columns in @var{D}. ## ## @code{@var{TF} = issortedrows (@var{D}, @var{col}, @var{direction})} ## checks if the rows in the duration array @var{D} are sorted according ## to the columns specified in @var{col} using the corresponding sorting ## direction specified in @var{direction}. In this case, the sign of the ## values in @var{col} is ignored. @var{col} and @var{direction} must have ## the same length, but not necessarily the same number of elements as the ## columns in @var{D}. ## ## @code{@var{TF} = issortedrows (@dots{}, @qcode{'MissingPlacement'}, ## @var{MP})} specifies where missing elements (@qcode{}) are ## placed with one of the following options specified in @var{MP}: ## ## @itemize ## @item @qcode{'auto'}, which is the default, places missing elements last ## for ascending sort and first for descending sort. ## @item @qcode{'first'} places missing elements first. ## @item @qcode{'last'} places missing elements last. ## @end itemize ## ## @code{@var{TF} = issortedrows (@dots{}, @qcode{'ComparisonMethod'}, ## @var{CM})} specifies the comparison method for determining the order of ## elements with one of following options specified in @var{CM}: ## ## @itemize ## @item @qcode{'auto'}, which is the default, sorts by @code{real (A)}. ## @item @qcode{'real'} sorts by @code{real (A)}. ## @item @qcode{'abs'} sorts by @code{abs (A)}. ## @end itemize ## ## @end deftypefn function TF = issortedrows (this, varargin) ## Single input argument if (nargin == 1) TF = isequaln (this, sortrows (this)); return; endif ## Force strings to character vectors or cell arrays of character vectors [varargin{:}] = convertStringsToChars (varargin{:}); ## Get valid direction(s) from input argument list valid = {'ascend', 'descend', 'monotonic', 'strictascend', ... 'strictdescend', 'strictmonotonic'}; fcn = @(x) (ischar (x) && ismember (x, valid)) || iscellstr (x); cid = cellfun (fcn, varargin); if (any (cid)) direction = lower (cellstr (varargin{cid})); ## Check for valid type of directions in cellstring if (! all (cellfun (@(x) ismember (x, valid), direction))) error ("duration.issortedrows: invalid DIRECTION value."); endif ## Handle non-strict modes first if (all (cellfun (@(x) ismember (x, {'ascend', 'descend'}), direction))) TF = isequaln (this, sortrows (this, varargin{:})); return; endif simple_types = {'ascend', 'descend', 'monotonic'}; if (all (cellfun (@(x) ismember (x, simple_types), direction))) idx = strcmp (direction, 'monotonic'); direction{idx} = 'ascend'; varargin{cid} = direction; TF = isequaln (this, sortrows (this, varargin{:})); if (TF) return; endif direction{idx} = 'descend'; varargin{cid} = direction; TF = isequaln (this, sortrows (this, varargin{:})); return; endif ## Handle strict modes. Determine the sort-key column order and the ## sort position holding the first strict direction, then derive the ## prefix (the columns sorted up to and including that position, on ## which the duplicate test operates) and the binding strict column ## (the single column on which the missing test operates). A missing ## value in the binding column, or two consecutive sorted rows equal ## across the prefix columns, rules out a strict ordering; strict ## positions after the first are subsumed by it. strict_types = {'strictascend', 'strictdescend', 'strictmonotonic'}; sflag = cellfun (@(x) ismember (x, strict_types), direction); nc = size (this, 2); nummask = cellfun (@isnumeric, varargin); if (any (nummask)) col = varargin{nummask}; ocols = abs (col(:)'); if (isscalar (direction)) sflag = repmat (sflag, 1, numel (col)); elseif (numel (direction) != numel (col)) error ("duration.issortedrows: COL and DIRECTION mismatch."); endif else ocols = 1:nc; if (isscalar (direction)) sflag = repmat (sflag, 1, nc); endif endif K = find (sflag, 1); prefix = ocols(1:K); bindcol = ocols(K); ## A missing value in the binding strict column rules out strictness. if (any (isnan (this.Millis(:,bindcol)), 'all')) TF = false; return; endif ## Replace strict modes with their plain counterparts for the sort. direction = strrep (direction, 'strict', ''); varargin{cid} = direction; if (all (cellfun (@(x) ismember (x, {'ascend', 'descend'}), direction))) sorted = sortrows (this, varargin{:}); ## No two consecutive rows may tie across the prefix columns. tmpcol = sorted.Millis(:, prefix); if (any (all (diff (tmpcol, 1, 1) == 0, 2))) TF = false; return; endif TF = isequaln (this, sorted); else # a 'monotonic' position also exists idx = strcmp (direction, 'monotonic'); direction(idx) = {'ascend'}; varargin{cid} = direction; sorted = sortrows (this, varargin{:}); tmpcol = sorted.Millis(:, prefix); if (any (all (diff (tmpcol, 1, 1) == 0, 2))) TF = false; return; endif TF = isequaln (this, sorted); if (TF) return; endif direction(idx) = {'descend'}; varargin{cid} = direction; sorted = sortrows (this, varargin{:}); tmpcol = sorted.Millis(:, prefix); if (any (all (diff (tmpcol, 1, 1) == 0, 2))) TF = false; return; endif TF = isequaln (this, sorted); endif else ## No DIRECTION input argument TF = isequaln (this, sortrows (this, varargin{:})); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} isvector (@var{D}) ## ## Return true if duration array is a vector. ## ## @code{@var{TF} = isvector (@var{D})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the duration array @var{D} is a vector and ## @qcode{false} otherwise. A vector is a 2-D array for which one of the ## dimensions is equal to 1 (either @math{1*N} or @math{N*1}). By ## definition, a scalar is also a vector. ## ## @end deftypefn function TF = isvector (this) TF = isvector (this.Millis); endfunction endmethods ################################################################################ ## ** Relational Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'eq' 'ge' 'gt' 'le' ## ## 'lt' 'ne' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} eq (@var{A}, @var{B}) ## ## Equality for duration arrays. ## ## @code{@var{TF} = eq (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} == @var{B}} and returns a logical array whose ## elements set to @qcode{true} where the corresponding elements of @var{A} ## and @var{B} are equal and set to @qcode{false} otherwise. ## ## @var{A} and @var{B} must be size compatible, which translates to they can ## be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. The size of ## @var{C} is determined by the size compatibility of @var{A} and @var{B}. ## ## One of the input arguments can also be a character vector, a cell array ## of character vectors, or a string array representing duration strings or ## a numeric array representing days. ## ## @end deftypefn function TF = eq (A, B) [A, B] = promote (A, B); TF = A.Millis == B.Millis; endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} ge (@var{A}, @var{B}) ## ## Equality for duration arrays. ## ## @code{@var{TF} = ge (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} >= @var{B}} and returns a logical array whose ## elements set to @qcode{true} where the corresponding elements of @var{A} ## are greater than or equal to @var{B} and set to @qcode{false} otherwise. ## ## @var{A} and @var{B} must be size compatible, which translates to they can ## be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. The size of ## @var{C} is determined by the size compatibility of @var{A} and @var{B}. ## ## One of the input arguments can also be a character vector, a cell array ## of character vectors, or a string array representing duration strings or ## a numeric array representing days. ## ## @end deftypefn function TF = ge (A, B) [A, B] = promote (A, B); TF = A.Millis >= B.Millis; endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} gt (@var{A}, @var{B}) ## ## Equality for duration arrays. ## ## @code{@var{TF} = gt (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} > @var{B}} and returns a logical array whose ## elements set to @qcode{true} where the corresponding elements of @var{A} ## are greater than @var{B} and set to @qcode{false} otherwise. ## ## @var{A} and @var{B} must be size compatible, which translates to they can ## be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. The size of ## @var{C} is determined by the size compatibility of @var{A} and @var{B}. ## ## One of the input arguments can also be a character vector, a cell array ## of character vectors, or a string array representing duration strings or ## a numeric array representing days. ## ## @end deftypefn function TF = gt (A, B) [A, B] = promote (A, B); TF = A.Millis > B.Millis; endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} le (@var{A}, @var{B}) ## ## Equality for duration arrays. ## ## @code{@var{TF} = le (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} <= @var{B}} and returns a logical array whose ## elements set to @qcode{true} where the corresponding elements of @var{A} ## are less than or equal to @var{B} and set to @qcode{false} otherwise. ## ## @var{A} and @var{B} must be size compatible, which translates to they can ## be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. The size of ## @var{C} is determined by the size compatibility of @var{A} and @var{B}. ## ## One of the input arguments can also be a character vector, a cell array ## of character vectors, or a string array representing duration strings or ## a numeric array representing days. ## ## @end deftypefn function TF = le (A, B) [A, B] = promote (A, B); TF = A.Millis <= B.Millis; endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} lt (@var{A}, @var{B}) ## ## Equality for duration arrays. ## ## @code{@var{TF} = lt (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} < @var{B}} and returns a logical array whose ## elements set to @qcode{true} where the corresponding elements of @var{A} ## are less than @var{B} and set to @qcode{false} otherwise. ## ## @var{A} and @var{B} must be size compatible, which translates to they can ## be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. The size of ## @var{C} is determined by the size compatibility of @var{A} and @var{B}. ## ## One of the input arguments can also be a character vector, a cell array ## of character vectors, or a string array representing duration strings or ## a numeric array representing days. ## ## @end deftypefn function TF = lt (A, B) [A, B] = promote (A, B); TF = A.Millis < B.Millis; endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{TF} =} ne (@var{A}, @var{B}) ## ## Equality for duration arrays. ## ## @code{@var{TF} = ne (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} != @var{B}} and returns a logical array whose ## elements set to @qcode{true} where the corresponding elements of @var{A} ## and @var{B} are not equal and set to @qcode{false} otherwise. ## ## @var{A} and @var{B} must be size compatible, which translates to they can ## be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. The size of ## @var{C} is determined by the size compatibility of @var{A} and @var{B}. ## ## One of the input arguments can also be a character vector, a cell array ## of character vectors, or a string array representing duration strings or ## a numeric array representing days. ## ## @end deftypefn function TF = ne (A, B) [A, B] = promote (A, B); TF = A.Millis != B.Millis; endfunction endmethods ################################################################################ ## ** Mathematical Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'abs' 'plus' 'uplus' 'minus' ## ## 'uminus' 'times' 'mtimes' 'ldivide' ## ## 'rdivide' 'mldivide' 'mrdivide' 'mod' ## ## 'rem' 'eps' 'colon' 'linspace' ## ## 'diff' ## ## 'sum' 'cumsum' 'min' 'cummin' ## ## 'max' 'cummax' 'floor' 'ceil' ## ## 'round' 'sign' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} abs (@var{A}) ## ## Absolute value of the elements of a duration array. ## ## @code{@var{B} = abs (@var{A})} returns the absolute value of each element ## in the duration array @var{A}. The returned duration array @var{B} has ## the same size as the input array @var{A}. ## ## @end deftypefn function this = abs (this) this.Millis = abs (this.Millis); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} plus (@var{A}, @var{B}) ## ## Addition for duration arrays. ## ## @code{@var{C} = plus (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{C} = @var{A} + @var{B}} and returns the sum of @var{A} and ## @var{B} by adding the corresponding elements. @var{A} and @var{B} must ## be size compatible, which translates to they can be the same size, one ## can be scalar, or for every dimension, their dimension sizes must be ## equal or one of them must be 1. The size of @var{C} is determined by the ## size compatibility of @var{A} and @var{B}. ## ## One of the input arguments can also be a numeric array in which case its ## elements are treated as a number of 24-hour days. If the second argument ## @var{B} is a calendarDuration arrays, then the returned array @var{C} is ## also a calendarDuration array. ## ## @end deftypefn function C = plus (A, B) ## Overload methods for certain data types if (isa (B, 'datetime')) C = B + A; return; endif if (isa (B, 'calendarDuration')) C = B + A; return; endif if (isa (A, 'duration') && isa (B, 'duration')) C = A; C.Millis = A.Millis + B.Millis; elseif (isnumeric (A)) C = B; C.Millis = double (A) * 86400000 + B.Millis; elseif (isnumeric (B)) C = A; C.Millis = A.Millis + double (B) * 86400000; else error (strcat ("duration: addition is not defined between", ... " '%s' and '%s' arrays."), class (A), class (B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} uplus (@var{A}) ## ## Unary plus for duration arrays. ## ## @code{@var{B} = uplus (@var{A})} is the equivalent of the syntax ## @code{@var{B} = + @var{A}} and returns the input array unaltered. ## ## @end deftypefn function this = uplus (this) endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} minus (@var{A}, @var{B}) ## ## Subtraction for duration arrays. ## ## @code{@var{C} = minus (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{C} = @var{A} - @var{B}} and returns the subtraction of @var{B} ## from @var{A} by subtracting the corresponding elements. @var{A} and ## @var{B} must be size compatible, which translates to they can be the same ## size, one can be scalar, or for every dimension, their dimension sizes ## must be equal or one of them must be 1. The size of @var{C} is ## determined by the size compatibility of @var{A} and @var{B}. ## ## One of the input arguments can also be a numeric array in which case its ## elements are treated as a number of 24-hour days. If the second argument ## @var{B} is a calendarDuration arrays, then the returned array @var{C} is ## also a calendarDuration array. ## ## @end deftypefn function C = minus (A, B) ## Overload methods for certain data types if (isa (B, 'calendarDuration')) C = -B + A; return; endif if (isa (A, 'duration') && isa (B, 'duration')) C = A; C.Millis = A.Millis - B.Millis; elseif (isnumeric (A)) C = B; C.Millis = double (A) * 86400000 - B.Millis; elseif (isnumeric (B)) C = A; C.Millis = A.Millis - double (B) * 86400000; else error (strcat ("duration: subtraction is not defined between", ... " '%s' and '%s' arrays."), class (A), class (B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} uminus (@var{A}) ## ## Unary minus for duration arrays. ## ## @code{@var{B} = uminus (@var{A})} is the equivalent of the syntax ## @code{@var{B} = - @var{A}} and returns the input array with its elements ## negated. ## ## @end deftypefn function this = uminus (this) this.Millis = - this.Millis; endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} times (@var{A}, @var{B}) ## ## Element-wise multiplication for duration arrays. ## ## @code{@var{C} = times (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{C} = @var{A} .* @var{B}} and returns the element-by-element ## multiplication product between the corresponding elements of input arrays ## @var{A} and @var{B}, one of which must be a numeric array and the other a ## duration array. ## ## @var{A} and @var{B} must be size compatible, which translates to they ## can be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. The size of ## @var{C} is determined by the size compatibility of @var{A} and @var{B}. ## ## @end deftypefn function C = times (A, B) if (isa (A, 'duration') && isnumeric (B)) C = A; C.Millis = A.Millis .* double (B); elseif (isnumeric (A) && isa (B, 'duration')) C = B; C.Millis = double (A) .* B.Millis; else error (strcat ("duration: multiplication is not defined between", ... " '%s' and '%s' arrays."), class (A), class (B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} mtimes (@var{A}, @var{B}) ## ## Matrix multiplication for duration arrays. ## ## @code{@var{C} = mtimes (@var{A}, @var{B})} is the equivalent of the ## syntax @code{@var{C} = @var{A} * @var{B}} and returns the matrix ## multiplication product of input arrays @var{A} and @var{B}, one of which ## must be a numeric array and the other a duration array. ## ## The columns of @var{A} must equal the rows of @var{B} and the size of ## @var{C} is determined by the rows of @var{A} and the columns of @var{B}. ## ## @end deftypefn function C = mtimes (A, B) if (isa (A, 'duration') && isnumeric (B)) C = A; C.Millis = A.Millis * double (B); elseif (isnumeric (A) && isa (B, 'duration')) C = B; C.Millis = double (A) * B.Millis; else error (strcat ("duration: matrix multiplication is not defined", ... " between '%s' and '%s' arrays."), class (A), class (B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} ldivide (@var{A}, @var{B}) ## ## Element-wise left division for duration arrays. ## ## @code{@var{C} = ldivide (@var{A}, @var{B})} is the equivalent of the ## syntax @code{@var{C} = @var{A} .\ @var{B}} and returns the element-wise ## division of the duration array @var{B} by the corresponding elements of ## input array @var{A}, which can either be a duration or a numeric array. ## If @var{A} is a duration array, then @var{C} is a double numeric array. ## If @var{A} is a numeric array, then @var{C} is a duration array. ## ## @var{A} and @var{B} must be size compatible, which translates to they ## can be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. The size of ## @var{C} is determined by the size compatibility of @var{A} and @var{B}. ## ## @end deftypefn function C = ldivide (A, B) if (! isa (B, 'duration')) error (strcat ("duration: right-hand side must be a duration", ... " array for left division: got '%s'."), class (B)); endif if (isa (A, 'duration')) C = A.Millis .\ B.Millis; elseif (isnumeric (A)) C = B; C.Millis = double (A) .\ B.Millis; else error (strcat ("duration: left division is not defined", ... " between '%s' and 'duration' arrays."), class (A)); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} mod (@var{A}, @var{B}) ## ## Remainder after division of duration arrays, with the sign of the ## divisor. ## ## @code{@var{C} = mod (@var{A}, @var{B})} returns the remainder left after ## dividing @var{A} by @var{B}, as a @code{duration} array of the size the ## two broadcast to. At least one of @var{A} and @var{B} must be a ## @code{duration}; the other may be a @code{duration} or a numeric array, ## which is read as a number of days, the unit a @code{duration} counts in. ## ## The result takes its sign from @var{B} and is zero when @var{B} is, which ## is what distinguishes @code{mod} from @code{rem}: @code{mod} of an hour ## by @math{-1} day differs from @code{rem} of the same. A remainder by a ## zero duration is @var{A} itself. The display format is taken from ## whichever operand is a @code{duration}, or from @var{A} when both are. ## ## @end deftypefn function C = mod (A, B) if (isa (A, 'duration') && isa (B, 'duration')) C = A; C.Millis = mod (A.Millis, B.Millis); elseif (isa (A, 'duration') && isnumeric (B)) C = A; C.Millis = mod (A.Millis, double (B) * 86400000); elseif (isnumeric (A) && isa (B, 'duration')) C = B; C.Millis = mod (double (A) * 86400000, B.Millis); else error (strcat ("duration: 'mod' is not defined between", ... " '%s' and '%s' arrays."), class (A), class (B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} rem (@var{A}, @var{B}) ## ## Remainder after division of duration arrays, with the sign of the ## dividend. ## ## @code{@var{C} = rem (@var{A}, @var{B})} returns the remainder left after ## dividing @var{A} by @var{B}, as a @code{duration} array of the size the ## two broadcast to. At least one of @var{A} and @var{B} must be a ## @code{duration}; the other may be a @code{duration} or a numeric array, ## which is read as a number of days, the unit a @code{duration} counts in. ## ## The result takes its sign from @var{A}, where @code{mod} takes it from ## @var{B}: the two agree only where the operands share a sign. A remainder ## by a zero duration is @code{NaN}, where @code{mod} gives @var{A} itself. ## The display format is taken from whichever operand is a @code{duration}, ## or from @var{A} when both are. ## ## @end deftypefn function C = rem (A, B) if (isa (A, 'duration') && isa (B, 'duration')) C = A; C.Millis = rem (A.Millis, B.Millis); elseif (isa (A, 'duration') && isnumeric (B)) C = A; C.Millis = rem (A.Millis, double (B) * 86400000); elseif (isnumeric (A) && isa (B, 'duration')) C = B; C.Millis = rem (double (A) * 86400000, B.Millis); else error (strcat ("duration: 'rem' is not defined between", ... " '%s' and '%s' arrays."), class (A), class (B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} mldivide (@var{A}, @var{B}) ## ## Left division of duration arrays, @code{@var{A} \ @var{B}}. ## ## @code{@var{C} = mldivide (@var{A}, @var{B})} divides @var{B} by @var{A}. ## The denominator @var{A} decides what the result means and must be a ## scalar. Dividing a @code{duration} by a number scales it, giving a ## @code{duration}; dividing one @code{duration} by another gives how many ## times one goes into the other, a plain number carrying no unit. A zero ## denominator gives @code{Inf}, as it does for numbers. ## ## Dividing a number by a @code{duration} is not defined, having no meaning, ## and neither is a non-scalar denominator. The display format of a ## @code{duration} result is that of @var{B}. ## ## Unlike MATLAB, an integer denominator is accepted and converted, as it is ## by @code{rdivide} and @code{times} here; MATLAB requires a @code{double} ## while accepting a @code{logical}, which the same rule cannot explain. ## ## @end deftypefn function C = mldivide (A, B) if (isa (A, 'duration')) if (! isa (B, 'duration')) error (strcat ("duration: left division of a '%s' array by a", ... " duration is not defined."), class (B)); elseif (! isscalar (A)) error (strcat ("duration: the denominator in left division must", ... " be a scalar.")); endif C = B.Millis ./ A.Millis; elseif (isnumeric (A) || islogical (A)) if (! isa (B, 'duration')) error (strcat ("duration: left division is not defined between", ... " '%s' and '%s' arrays."), class (A), class (B)); elseif (! isscalar (A)) error (strcat ("duration: the denominator in left division must", ... " be a scalar.")); elseif (! isreal (A)) error (strcat ("duration: the denominator in left division must", ... " be real.")); endif C = B; C.Millis = B.Millis ./ double (A); else error (strcat ("duration: left division is not defined between", ... " '%s' and '%s' arrays."), class (A), class (B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} mrdivide (@var{A}, @var{B}) ## ## Right division of duration arrays, @code{@var{A} / @var{B}}. ## ## @code{@var{C} = mrdivide (@var{A}, @var{B})} divides @var{A} by @var{B} ## and is the mirror of @code{mldivide}: @code{@var{A} / @var{B}} is ## @code{@var{B} \ @var{A}}. The denominator @var{B} decides what the ## result means and must be a scalar. Dividing a @code{duration} by a ## number scales it, giving a @code{duration}; dividing one @code{duration} ## by another gives how many times one goes into the other, a plain number ## carrying no unit. ## ## Dividing a number by a @code{duration} is not defined, and neither is a ## non-scalar denominator. For element-wise division by an array of ## divisors, see @code{rdivide}. ## ## @end deftypefn function C = mrdivide (A, B) if (isa (B, 'duration')) if (! isa (A, 'duration')) error (strcat ("duration: right division of a '%s' array by a", ... " duration is not defined."), class (A)); elseif (! isscalar (B)) error (strcat ("duration: the denominator in right division must", ... " be a scalar.")); endif C = A.Millis ./ B.Millis; elseif (isnumeric (B) || islogical (B)) if (! isa (A, 'duration')) error (strcat ("duration: right division is not defined between", ... " '%s' and '%s' arrays."), class (A), class (B)); elseif (! isscalar (B)) error (strcat ("duration: the denominator in right division must", ... " be a scalar.")); elseif (! isreal (B)) error (strcat ("duration: the denominator in right division must", ... " be real.")); endif C = A; C.Millis = A.Millis ./ double (B); else error (strcat ("duration: right division is not defined between", ... " '%s' and '%s' arrays."), class (A), class (B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{E} =} eps (@var{D}) ## ## Spacing of duration values. ## ## @code{@var{E} = eps (@var{D})} returns the distance from each element of ## @var{D} to the next duration that can be told apart from it, as a ## @code{duration} array of the same size. It is the resolution available ## at that magnitude: two durations closer together than this are the same ## value. @code{eps} of a negative duration is that of its magnitude, and ## @code{eps} of @code{NaN} is @code{NaN}. ## ## The spacing is that of the milliseconds a @code{duration} is stored in, ## which is what MATLAB stores too, so the two agree exactly. ## ## @end deftypefn function E = eps (D) E = D; E.Millis = eps (D.Millis); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} rdivide (@var{A}, @var{B}) ## ## Element-wise right division for duration arrays. ## ## @code{@var{C} = rdivide (@var{A}, @var{B})} is the equivalent of the ## syntax @code{@var{C} = @var{A} ./ @var{B}} and returns the element-wise ## division of the duration array @var{A} by the corresponding elements of ## input array @var{B}, which can either be a duration or a numeric array. ## If @var{B} is a duration array, then @var{C} is a double numeric array. ## If @var{B} is a numeric array, then @var{C} is a duration array. ## ## @var{A} and @var{B} must be size compatible, which translates to they ## can be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. The size of ## @var{C} is determined by the size compatibility of @var{A} and @var{B}. ## ## @end deftypefn function C = rdivide (A, B) if (! isa (A, 'duration')) error (strcat ("duration: left-hand side must be a duration", ... " array for right division: got '%s'."), class (A)); endif if (isa (B, 'duration')) C = A.Millis ./ B.Millis; elseif (isnumeric (B)) C = A; C.Millis = A.Millis ./ double (B); else error (strcat ("duration: right division is not defined", ... " between 'duration' and '%s' arrays."), class (B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{R} =} colon (@var{Base}, @var{Limit}) ## @deftypefnx {duration} {@var{R} =} colon (@var{Base}, @var{Increment}, @var{Limit}) ## ## Create a range of durations in a vector. ## ## @code{@var{R} = colon (@var{Base}, @var{Limit})} is the equivalent of the ## syntax @code{@var{C} = @var{Base}:@var{Limit}} and returns a duration ## vector in the range from @var{Base} to @var{Limit} incremented by 24-hour ## days. ## ## @code{@var{R} = colon (@var{Base}, @var{Increment}, @var{Limit})} is ## equivalent to @code{@var{C} = @var{Base}:@var{Increment}:@var{Limit}}. ## dimension sizes must be equal or one of them must be 1. The size of ## @var{C} is determined by the size compatibility of @var{A} and @var{B}. ## ## As long as one of the inputs is a duration scalar, the following types ## are additionally supported for the remaining input arguments: ## ## @itemize ## @item numeric scalar (24-hour day) ## @item character vector (duration string) ## @item cellstr scalar (duration string) ## @item string scalar (duration string) ## @end itemize ## ## @end deftypefn function R = colon (varargin) if (nargin < 2 || nargin > 3) error ("duration.colon: invalid number of input arguments."); endif idx = find (cellfun ('isduration', varargin), 1); R = varargin{idx}; if (nargin == 2) [from, to] = promote (varargin{:}); if (! isscalar (from) || ! isscalar (to)) error ("duration.colon: input arguments must be scalars."); endif increment = days (1); else [from, increment, to] = promote (varargin{:}); if (! isscalar (from) || ! isscalar (increment) || ! isscalar (to)) error ("duration.colon: input arguments must be scalars."); endif endif R.Millis = from.Millis:increment.Millis:to.Millis; endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{R} =} linspace (@var{Start}, @var{End}) ## @deftypefnx {duration} {@var{R} =} linspace (@var{Start}, @var{End}, @var{N}) ## ## Create linearly spaced duration elements. ## ## @code{@var{R} = linspace (@var{Start}, @var{End})} returns 100 linearly ## spaced elements between @var{Start} and @var{End}. If @var{Start} and ## @var{End} are scalars, then @var{R} is a vector. If one or both inputs ## are vectors, then @var{R} is a matrix where each row is an independent ## sequence between @code{@var{Start}(idx_N)} and @code{@var{End}(idx_N)}. ## ## @code{@var{R} = linspace (@var{Start}, @var{End}, @var{N})} specifies the ## number (default is 100) of equally spaced elements between @var{Start} ## and @var{End}. If @var{N} is not an integer value, then it is floored ## to the nearest integer. If @var{N} is zero or negative, then an empty ## matrix is returned. If @var{N} is one, then @var{End} is returned. ## If @var{N} greater than one, then @var{Start} and @var{End} are always ## included in the range. ## ## Either @var{Start} or @var{End} input arguments can also be one of the ## following types: ## ## @itemize ## @item numeric scalar or vector (24-hour days) ## @item character vector (duration string) ## @item cellstr scalar or vector (duration strings) ## @item string scalar or vector (duration strings) ## @end itemize ## ## @end deftypefn function R = linspace (A, B, n = 100) if (nargin < 2) error ("duration.linspace: too few input arguments."); endif if (isduration (A)) R = A; else R = B; endif [A, B] = promote (A, B); R.Millis = linspace (A.Millis, B.Millis, n); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{DT} =} diff (@var{D}) ## @deftypefnx {duration} {@var{DT} =} diff (@var{D}, @var{K}) ## @deftypefnx {duration} {@var{DT} =} diff (@var{D}, @var{K}, @var{DIM}) ## ## Compute differences between adjacent elements in a duration array. ## ## This method overloads the core @code{diff} function for duration arrays. ## The functionality is identical to core @code{diff} function. Type ## @code{help diff} for more information. ## ## @end deftypefn function this = diff (this, varargin) this.Millis = diff (this.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{S} =} sum (@var{D}) ## @deftypefnx {duration} {@var{S} =} sum (@var{D}, @var{dim}) ## @deftypefnx {duration} {@var{S} =} sum (@var{D}, @var{vecdim}) ## @deftypefnx {duration} {@var{S} =} sum (@var{D}, @qcode{'all'}) ## @deftypefnx {duration} {@var{S} =} sum (@dots{}, @var{nanflag}) ## ## Compute the sum of the elements of a duration array. ## ## This method overloads the core @code{sum} function for duration arrays. ## The functionality is identical to core @code{sum} function. Type ## @code{help sum} for more information. ## ## @end deftypefn function this = sum (this, varargin) ## Force strings to character vectors or cell arrays of character vectors if (any (cellfun ('isstring', varargin))) [varargin{:}] = convertStringsToChars (varargin{:}); endif this.Millis = sum (this.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{CS} =} cumsum (@var{D}) ## @deftypefnx {duration} {@var{CS} =} cumsum (@var{D}, @var{dim}) ## @deftypefnx {duration} {@var{CS} =} cumsum (@var{D}, @var{vecdim}) ## @deftypefnx {duration} {@var{CS} =} cumsum (@var{D}, @qcode{'all'}) ## @deftypefnx {duration} {@var{CS} =} cumsum (@dots{}, @var{direction}) ## @deftypefnx {duration} {@var{CS} =} cumsum (@dots{}, @var{nanflag}) ## ## Compute the cumulative sum of the elements of a duration array. ## ## This method overloads the core @code{cumsum} function for duration ## arrays. The functionality is identical to core @code{cumsum} function. ## Type @code{help cumsum} for more information. ## ## @end deftypefn function this = cumsum (this, varargin) ## Force strings to character vectors or cell arrays of character vectors if (any (cellfun ('isstring', varargin))) [varargin{:}] = convertStringsToChars (varargin{:}); endif this.Millis = cumsum (this.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{M} =} min (@var{D}) ## @deftypefnx {duration} {@var{M} =} min (@var{D}, @qcode{[]}, @var{dim}) ## @deftypefnx {duration} {@var{M} =} min (@var{D}, @qcode{[]}, @var{vecdim}) ## @deftypefnx {duration} {@var{M} =} min (@var{D}, @qcode{[]}, @qcode{'all'}) ## @deftypefnx {duration} {@var{M} =} min (@var{D}, @qcode{[]}, @var{nanflag}) ## @deftypefnx {duration} {@var{M} =} min (@var{D}, @qcode{[]}, @dots{}, @var{nanflag}) ## @deftypefnx {duration} {[@var{M}, @var{index}] =} min (@dots{}) ## @deftypefnx {duration} {[@var{M}, @var{index}] =} min (@dots{}, @qcode{'linear'}) ## @deftypefnx {duration} {@var{M} =} min (@var{D1}, @var{D2}) ## @deftypefnx {duration} {@var{M} =} min (@var{D1}, @var{D2}, @var{nanflag}) ## @deftypefnx {duration} {@dots{} =} min (@dots{}, @qcode{'ComparisonMethod'}, @var{method}) ## ## Find minimum values in duration arrays. ## ## This method overloads the core @code{min} function for duration ## arrays. The functionality is identical to core @code{min} function. ## Type @code{help min} for more information. ## ## @end deftypefn function varargout = min (D, varargin) M = D; if (isempty (varargin)) if (nargout > 1) [M.Millis, varargout{2}] = min (D.Millis); else M.Millis = min (D.Millis); endif varargout{1} = M; else ## Force strings to character vectors or cell arrays of character ## vectors if (any (cellfun ('isstring', varargin))) [varargin{:}] = convertStringsToChars (varargin{:}); endif ## Second argument is a duration, or a bare number, which counts ## days as it does everywhere else a duration meets a number. ## An empty second argument is the reduction form, not an operand. if (isduration (varargin{1}) || (isnumeric (varargin{1}) && ! isempty (varargin{1}))) D2 = varargin{1}; if (! isduration (D2)) D2 = promote (D2); endif varargin(1) = []; ## Make sure first argument is also a duration if (! isduration (D)) D = promote (D); M = D2; endif M.Millis = min (D.Millis, D2.Millis, varargin{:}); varargout{1} = M; else if (nargout > 1) [M.Millis, varargout{2}] = min (D.Millis, varargin{:}); else M.Millis = min (D.Millis, varargin{:}); endif varargout{1} = M; endif endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{M} =} cummin (@var{D}) ## @deftypefnx {duration} {@var{M} =} cummin (@var{D}, @var{dim}) ## @deftypefnx {duration} {@var{M} =} cummin (@var{D}, @var{vecdim}) ## @deftypefnx {duration} {@var{M} =} cummin (@var{D}, @qcode{'all'}) ## @deftypefnx {duration} {@var{M} =} cummin (@qcode{[]}, @var{nanflag}) ## @deftypefnx {duration} {@var{M} =} cummin (@qcode{[]}, @var{direction}) ## @deftypefnx {duration} {[@var{M}, @var{index}] =} cummin (@dots{}) ## @deftypefnx {duration} {[@var{M}, @var{index}] =} cummin (@dots{}, @qcode{'linear'}) ## @deftypefnx {duration} {@dots{} =} cummin (@dots{}, @qcode{'ComparisonMethod'}, @var{method}) ## ## Return the cumulative minimum values in duration arrays. ## ## This method overloads the core @code{cummin} function for duration ## arrays. The functionality is identical to core @code{cummin} function. ## Type @code{help cummin} for more information. ## ## @end deftypefn function varargout = cummin (this, varargin) if (isempty (varargin)) if (nargout > 1) [this.Millis, varargout{2}] = cummin (this.Millis); else this.Millis = cummin (this.Millis); endif varargout{1} = this; else ## Force strings to character vectors or cell arrays of character ## vectors if (any (cellfun ('isstring', varargin))) [varargin{:}] = convertStringsToChars (varargin{:}); endif if (nargout > 1) [this.Millis, varargout{2}] = cummin (this.Millis, varargin{:}); else this.Millis = cummin (this.Millis, varargin{:}); endif varargout{1} = this; endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{M} =} max (@var{D}) ## @deftypefnx {duration} {@var{M} =} max (@var{D}, @qcode{[]}, @var{dim}) ## @deftypefnx {duration} {@var{M} =} max (@var{D}, @qcode{[]}, @var{vecdim}) ## @deftypefnx {duration} {@var{M} =} max (@var{D}, @qcode{[]}, @qcode{'all'}) ## @deftypefnx {duration} {@var{M} =} max (@var{D}, @qcode{[]}, @var{nanflag}) ## @deftypefnx {duration} {@var{M} =} max (@var{D}, @qcode{[]}, @dots{}, @var{nanflag}) ## @deftypefnx {duration} {[@var{M}, @var{index}] =} max (@dots{}) ## @deftypefnx {duration} {[@var{M}, @var{index}] =} max (@dots{}, @qcode{'linear'}) ## @deftypefnx {duration} {@var{M} =} max (@var{D1}, @var{D2}) ## @deftypefnx {duration} {@var{M} =} max (@var{D1}, @var{D2}, @var{nanflag}) ## @deftypefnx {duration} {@dots{} =} max (@dots{}, @qcode{'ComparisonMethod'}, @var{method}) ## ## Find maximum values in duration arrays. ## ## This method overloads the core @code{max} function for duration ## arrays. The functionality is identical to core @code{max} function. ## Type @code{help max} for more information. ## ## @end deftypefn function varargout = max (D, varargin) M = D; if (isempty (varargin)) if (nargout > 1) [M.Millis, varargout{2}] = max (D.Millis); else M.Millis = max (D.Millis); endif varargout{1} = M; else ## Force strings to character vectors or cell arrays of character ## vectors if (any (cellfun ('isstring', varargin))) [varargin{:}] = convertStringsToChars (varargin{:}); endif ## Second argument is a duration, or a bare number, which counts ## days as it does everywhere else a duration meets a number. ## An empty second argument is the reduction form, not an operand. if (isduration (varargin{1}) || (isnumeric (varargin{1}) && ! isempty (varargin{1}))) D2 = varargin{1}; if (! isduration (D2)) D2 = promote (D2); endif varargin(1) = []; ## Make sure first argument is also a duration if (! isduration (D)) D = promote (D); M = D2; endif M.Millis = max (D.Millis, D2.Millis, varargin{:}); varargout{1} = M; else if (nargout > 1) [M.Millis, varargout{2}] = max (D.Millis, varargin{:}); else M.Millis = max (D.Millis, varargin{:}); endif varargout{1} = M; endif endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{M} =} cummax (@var{D}) ## @deftypefnx {duration} {@var{M} =} cummax (@var{D}, @var{dim}) ## @deftypefnx {duration} {@var{M} =} cummax (@var{D}, @var{vecdim}) ## @deftypefnx {duration} {@var{M} =} cummax (@var{D}, @qcode{'all'}) ## @deftypefnx {duration} {@var{M} =} cummax (@qcode{[]}, @var{nanflag}) ## @deftypefnx {duration} {@var{M} =} cummax (@qcode{[]}, @var{direction}) ## @deftypefnx {duration} {[@var{M}, @var{index}] =} cummax (@dots{}) ## @deftypefnx {duration} {[@var{M}, @var{index}] =} cummax (@dots{}, @qcode{'linear'}) ## @deftypefnx {duration} {@dots{} =} cummax (@dots{}, @qcode{'ComparisonMethod'}, @var{method}) ## ## Return the cumulative maximum values in duration arrays. ## ## This method overloads the core @code{cummax} function for duration ## arrays. The functionality is identical to core @code{cummax} function. ## Type @code{help cummax} for more information. ## ## @end deftypefn function varargout = cummax (this, varargin) if (isempty (varargin)) if (nargout > 1) [this.Millis, varargout{2}] = cummax (this.Millis); else this.Millis = cummax (this.Millis); endif varargout{1} = this; else ## Force strings to character vectors or cell arrays of character ## vectors if (any (cellfun ('isstring', varargin))) [varargin{:}] = convertStringsToChars (varargin{:}); endif if (nargout > 1) [this.Millis, varargout{2}] = cummax (this.Millis, varargin{:}); else this.Millis = cummax (this.Millis, varargin{:}); endif varargout{1} = this; endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{Y} =} floor (@var{D}) ## @deftypefnx {duration} {@var{Y} =} floor (@var{D}, @var{unit}) ## ## Round toward negative infinity for duration arrays. ## ## @code{@var{Y} = floor (@var{D})} rounds each element of the duration ## array @var{D} to the largest integer number of seconds not greater than ## that element. ## ## @code{@var{Y} = floor (@var{D}, @var{unit})} rounds each element of the ## duration array @var{D} to the largest integer number of the specified ## unit of time not greater than that element. @var{unit} must be one of ## the following values: ## ## @itemize ## @item @qcode{'seconds'} (default) ## @item @qcode{'minutes'} ## @item @qcode{'hours'} ## @item @qcode{'days'} ## @item @qcode{'years'} ## @end itemize ## ## @end deftypefn function this = floor (this, unit = 'seconds') if (strcmpi (unit, 'seconds')) this.Millis = floor (seconds (this)) * 1000; elseif (strcmpi (unit, 'minutes')) this.Millis = floor (minutes (this)) * 60000; elseif (strcmpi (unit, 'hours')) this.Millis = floor (hours (this)) * 3600000; elseif (strcmpi (unit, 'days')) this.Millis = floor (days (this)) * 86400000; elseif (strcmpi (unit, 'years')) this.Millis = floor (years (this)) * 31556952000; else error ("duration.floor: invalid UNIT."); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{Y} =} ceil (@var{D}) ## @deftypefnx {duration} {@var{Y} =} ceil (@var{D}, @var{unit}) ## ## Round toward negative infinity for duration arrays. ## ## @code{@var{Y} = ceil (@var{D})} rounds each element of the duration ## array @var{D} to the smallest integer number of seconds not less than ## that element. ## ## @code{@var{Y} = ceil (@var{D}, @var{unit})} rounds each element of the ## duration array @var{D} to the smallest integer number of the specified ## unit of time not less than that element. @var{unit} must be one of the ## following values: ## ## @itemize ## @item @qcode{'seconds'} (default) ## @item @qcode{'minutes'} ## @item @qcode{'hours'} ## @item @qcode{'days'} ## @item @qcode{'years'} ## @end itemize ## ## @end deftypefn function this = ceil (this, unit = 'seconds') if (strcmpi (unit, 'seconds')) this.Millis = ceil (seconds (this)) * 1000; elseif (strcmpi (unit, 'minutes')) this.Millis = ceil (minutes (this)) * 60000; elseif (strcmpi (unit, 'hours')) this.Millis = ceil (hours (this)) * 3600000; elseif (strcmpi (unit, 'days')) this.Millis = ceil (days (this)) * 86400000; elseif (strcmpi (unit, 'years')) this.Millis = ceil (years (this)) * 31556952000; else error ("duration.ceil: invalid UNIT."); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{Y} =} round (@var{D}) ## @deftypefnx {duration} {@var{Y} =} round (@var{D}, @var{unit}) ## ## Round to the nearest integer time unit for duration arrays. ## ## @code{@var{Y} = round (@var{D})} rounds each element of the duration ## array @var{D} to the nearest integer number of seconds to that element. ## In case of a tie, return the one further away from zero. ## ## @code{@var{Y} = round (@var{D}, @var{unit})} rounds each element of the ## duration array @var{D} to the nearest integer number of the specified ## unit of time to that element. @var{unit} must be one of the following ## values: ## ## @itemize ## @item @qcode{'seconds'} (default) ## @item @qcode{'minutes'} ## @item @qcode{'hours'} ## @item @qcode{'days'} ## @item @qcode{'years'} ## @end itemize ## ## @end deftypefn function this = round (this, unit = 'seconds') if (strcmpi (unit, 'seconds')) this.Millis = round (seconds (this)) * 1000; elseif (strcmpi (unit, 'minutes')) this.Millis = round (minutes (this)) * 60000; elseif (strcmpi (unit, 'hours')) this.Millis = round (hours (this)) * 3600000; elseif (strcmpi (unit, 'days')) this.Millis = round (days (this)) * 86400000; elseif (strcmpi (unit, 'years')) this.Millis = round (years (this)) * 31556952000; else error ("duration.round: invalid UNIT."); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{out} =} sign (@var{D}) ## @deftypefnx {duration} {@var{out} =} sign (@var{D}, @var{unit}) ## ## Compute the signum function for duration arrays. ## ## @code{@var{out} = sign (@var{D})} returns an array of doubles, @var{out}, ## the same size as the duration array @var{D}, where each element has one ## following values: ## ## @itemize ## @item @qcode{1} if the corresponding element of D is greater than 0. ## @item @qcode{0} if the corresponding element of D is equal to 0. ## @item @qcode{-1} if the corresponding element of D is less than 0. ## @item @qcode{NaN} if the corresponding element of D is a missing value. ## @end itemize ## ## @end deftypefn function out = sign (this) out = sign (this.Millis); endfunction endmethods ################################################################################ ## ** Statistical Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'bounds' 'center' 'histc' 'iqr' ## ## 'kurtosis' 'mad' 'mape' 'mean' ## ## 'median' 'mode' 'prctile' 'quantile' ## ## 'range' 'rmse' 'skewness' 'std' ## ## 'discretize' 'histcounts' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {duration} {[@var{s}, @var{l}] =} bounds (@var{D}) ## @deftypefnx {duration} {[@var{s}, @var{l}] =} bounds (@var{D}, @var{dim}) ## @deftypefnx {duration} {[@var{s}, @var{l}] =} bounds (@var{D}, @var{vecdim}) ## @deftypefnx {duration} {[@var{s}, @var{l}] =} bounds (@var{D}, @qcode{'all'}) ## @deftypefnx {duration} {[@var{s}, @var{l}] =} bounds (@dots{}, @var{nanflag}) ## ## Return the smallest and largest values of a duration array. ## ## This method is a specialization of the core @code{bounds} function for ## duration arrays. The functionality is identical to core @code{bounds} ## function. Type @code{help bounds} for more information. ## ## @end deftypefn function [s, l] = bounds (this, varargin) if (nargin < 1 || nargin > 3) error ("duration.bounds: invalid number of input arguments."); endif if (isempty (varargin)) s = min (this); l = max (this); else s = min (this, [], varargin{:}); l = max (this, [], varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} center (@var{D}) ## @deftypefnx {duration} {@var{C} =} center (@var{D}, @var{dim}) ## @deftypefnx {duration} {@var{C} =} center (@var{D}, @var{vecdim}) ## @deftypefnx {duration} {@var{C} =} center (@var{D}, @qcode{'all'}) ## @deftypefnx {duration} {@var{C} =} center (@dots{}, @var{nanflag}) ## ## Center values in a duration array. ## ## This method overloads the core @code{center} function for duration ## arrays. The functionality is identical to core @code{center} function. ## Type @code{help center} for more information. ## ## @end deftypefn function this = center (this, varargin) this.Millis = center (this.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{n} =} histc (@var{D}, @var{edges}) ## @deftypefnx {duration} {@var{n} =} histc (@var{D}, @var{edges}, @var{dim}) ## @deftypefnx {duration} {[@var{n}, @var{idx}] =} histc (@dots{}) ## ## Compute histogram counts in a duration array. ## ## This method overloads the core @code{histc} function for duration ## arrays. The functionality is identical to core @code{histc} function. ## Type @code{help histc} for more information. ## ## @end deftypefn function varargout = histc (D, varargin) ## The edges are given as bare numbers, which count days. if (nargout > 1) [varargout{1}, varargout{2}] = histc (days (D), varargin{:}); else varargout{1} = histc (days (D), varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{r} =} iqr (@var{D}) ## @deftypefnx {duration} {@var{r} =} iqr (@var{D}, @var{dim}) ## @deftypefnx {duration} {@var{r} =} iqr (@var{D}, @var{vecdim}) ## @deftypefnx {duration} {@var{r} =} iqr (@var{D}, @qcode{'all'}) ## @deftypefnx {duration} {[@var{r}, @var{q}] =} iqr (@dots{}) ## ## Compute the interquartile range of a duration array. ## ## This method overloads the core @code{iqr} function for duration arrays. ## The functionality is identical to core @code{iqr} function. ## Type @code{help iqr} for more information. ## ## @end deftypefn function varargout = iqr (this, varargin) if (nargout > 1) [this.Millis, varargout{2}] = iqr (this.Millis, varargin{:}); else this.Millis = iqr (this.Millis, varargin{:}); endif varargout{1} = this; endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{k} =} kurtosis (@var{D}) ## @deftypefnx {duration} {@var{k} =} kurtosis (@var{D}, @var{flag}) ## @deftypefnx {duration} {@var{k} =} kurtosis (@var{D}, @var{flag}, @var{dim}) ## @deftypefnx {duration} {@var{k} =} kurtosis (@var{D}, @var{flag}, @var{vecdim}) ## @deftypefnx {duration} {@var{k} =} kurtosis (@var{D}, @var{flag}, @qcode{'all'}) ## ## Compute the sample kurtosis of a duration array. ## ## This method overloads the core @code{kurtosis} function for duration ## arrays. The functionality is identical to core @code{kurtosis} function. ## Type @code{help kurtosis} for more information. ## ## Note that kurtosis is a dimensionless quantity Thus, the returned ## argument is a numeric array of double type and not a duration array. ## ## @end deftypefn function k = kurtosis (D, varargin) k = kurtosis (D.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{M} =} mad (@var{D}) ## @deftypefnx {duration} {@var{M} =} mad (@var{D}, @var{opt}) ## @deftypefnx {duration} {@var{M} =} mad (@var{D}, @var{opt}, @var{dim}) ## @deftypefnx {duration} {@var{M} =} mad (@var{D}, @var{opt}, @var{vecdim}) ## @deftypefnx {duration} {@var{M} =} mad (@var{D}, @var{opt}, @qcode{'all'}) ## ## Compute the mean or median absolute deviation of a duration array. ## ## This method overloads the core @code{mad} function for duration arrays. ## The functionality is identical to core @code{mad} function. ## Type @code{help mad} for more information. ## ## @end deftypefn function this = mad (this, varargin) this.Millis = mad (this.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{E} =} mape (@var{F}, @var{A}) ## @deftypefnx {duration} {@var{E} =} mape (@var{F}, @var{A}, @var{dim}) ## @deftypefnx {duration} {@var{E} =} mape (@var{F}, @var{A}, @var{vecdim}) ## @deftypefnx {duration} {@var{E} =} mape (@var{F}, @var{A}, @qcode{'all'}) ## @deftypefnx {duration} {@var{E} =} mape (@dots{}, @var{nanflag}) ## @deftypefnx {duration} {@var{E} =} mape (@dots{}, @var{zeroflag}) ## @deftypefnx {duration} {@var{E} =} mape (@dots{}, @qcode{'Weights'}, @var{W}) ## ## Compute the mean absolute percentage error between duration arrays. ## ## This method overloads the core @code{mape} function for duration arrays. ## The functionality is identical to core @code{mape} function. ## Type @code{help mape} for more information. ## ## Note that MAPE is expressed as a percentage. Thus, the returned argument ## is a numeric array of double type and not a duration array. However, ## both @var{F} and @var{A} must be duration arrays. ## ## @end deftypefn function E = mape (F, A, varargin) if (! isduration (F) || ! isduration (A)) error ("duration.mape: both F and A must be duration arrays."); endif E = mape (F.Millis, A.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{M} =} mean (@var{D}) ## @deftypefnx {duration} {@var{M} =} mean (@var{D}, @var{dim}) ## @deftypefnx {duration} {@var{M} =} mean (@var{D}, @var{vecdim}) ## @deftypefnx {duration} {@var{M} =} mean (@var{D}, @qcode{'all'}) ## @deftypefnx {duration} {@var{M} =} mean (@dots{}, @var{nanflag}) ## @deftypefnx {duration} {@var{M} =} mean (@dots{}, @var{outtype}) ## @deftypefnx {duration} {@var{M} =} mean (@dots{}, @qcode{'Weights'}, @var{W}) ## ## Compute the mean of the elements of a duration array. ## ## This method overloads the core @code{mean} function for duration arrays. ## The functionality is identical to core @code{mean} function. ## Type @code{help mean} for more information. ## ## @end deftypefn function this = mean (this, varargin) this.Millis = mean (this.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{M} =} median (@var{D}) ## @deftypefnx {duration} {@var{M} =} median (@var{D}, @var{dim}) ## @deftypefnx {duration} {@var{M} =} median (@var{D}, @var{vecdim}) ## @deftypefnx {duration} {@var{M} =} median (@var{D}, @qcode{'all'}) ## @deftypefnx {duration} {@var{M} =} median (@dots{}, @var{nanflag}) ## @deftypefnx {duration} {@var{M} =} median (@dots{}, @var{outtype}) ## ## Compute the median value of the elements of a duration array. ## ## This method overloads the core @code{median} function for duration ## arrays. The functionality is identical to core @code{median} function. ## Type @code{help median} for more information. ## ## @end deftypefn function this = median (this, varargin) this.Millis = median (this.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{M} =} mode (@var{D}) ## @deftypefnx {duration} {@var{M} =} mode (@var{D}, @var{dim}) ## @deftypefnx {duration} {@var{M} =} mode (@var{D}, @var{vecdim}) ## @deftypefnx {duration} {@var{M} =} mode (@var{D}, @qcode{'all'}) ## @deftypefnx {duration} {[@var{M}, @var{F}, @var{C}] =} mode (@dots{}) ## ## Compute the most frequently occurring value in a duration array. ## ## This method overloads the core @code{mode} function for duration arrays. ## The functionality is identical to core @code{mode} function. ## Type @code{help mode} for more information. ## ## @end deftypefn function [this, F, C] = mode (this, varargin) [this.Millis, F, C] = mode (this.Millis, varargin{:}); if (nargout == 3) ## C holds stored values, which are milliseconds. C = cellfun ('milliseconds', C, 'UniformOutput', false); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{Q} =} prctile (@var{D}) ## @deftypefnx {duration} {@var{q} =} prctile (@var{D}, @var{p}) ## @deftypefnx {duration} {@var{Q} =} prctile (@var{D}, @var{p}, @var{dim}) ## @deftypefnx {duration} {@var{Q} =} prctile (@var{D}, @var{p}, @var{vecdim}) ## @deftypefnx {duration} {@var{Q} =} prctile (@var{D}, @var{p}, @qcode{'all'}) ## @deftypefnx {duration} {@var{Q} =} prctile (@var{D}, @var{p}, @dots{}, @var{method}) ## ## Compute the percentiles of a duration array. ## ## This method overloads the core @code{prctile} function for duration ## arrays. The functionality is identical to core @code{prctile} function. ## Type @code{help prctile} for more information. ## ## @end deftypefn function this = prctile (this, varargin) this.Millis = prctile (this.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{Q} =} quantile (@var{D}) ## @deftypefnx {duration} {@var{Q} =} quantile (@var{D}, @var{p}) ## @deftypefnx {duration} {@var{Q} =} quantile (@var{D}, @var{n}) ## @deftypefnx {duration} {@var{Q} =} quantile (@var{D}, @dots{}, @var{dim}) ## @deftypefnx {duration} {@var{Q} =} quantile (@var{D}, @dots{}, @var{vecdim}) ## @deftypefnx {duration} {@var{Q} =} quantile (@var{D}, @dots{}, @qcode{'all'}) ## @deftypefnx {duration} {@var{Q} =} quantile (@var{D}, @var{p}, @dots{}, @var{method}) ## @deftypefnx {duration} {@var{Q} =} quantile (@var{D}, @var{n}, @dots{}, @var{method}) ## ## Compute the quantiles of a duration array. ## ## This method overloads the core @code{quantile} function for duration ## arrays. The functionality is identical to core @code{quantile} function. ## Type @code{help quantile} for more information. ## ## @end deftypefn function this = quantile (this, varargin) this.Millis = quantile (this.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{R} =} range (@var{D}) ## @deftypefnx {duration} {@var{R} =} range (@var{D}, @var{dim}) ## @deftypefnx {duration} {@var{R} =} range (@var{D}, @var{vecdim}) ## @deftypefnx {duration} {@var{R} =} range (@var{D}, @qcode{'all'}) ## @deftypefnx {duration} {@var{R} =} range (@dots{}, @var{nanflag}) ## ## Compute the range of a duration array. ## ## This method overloads the core @code{range} function for duration arrays. ## The functionality is identical to core @code{range} function. ## Type @code{help range} for more information. ## ## @end deftypefn function this = range (this, varargin) this.Millis = range (this.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{E} =} rmse (@var{F}, @var{A}) ## @deftypefnx {duration} {@var{E} =} rmse (@var{F}, @var{A}, @var{dim}) ## @deftypefnx {duration} {@var{E} =} rmse (@var{F}, @var{A}, @var{vecdim}) ## @deftypefnx {duration} {@var{E} =} rmse (@var{F}, @var{A}, @qcode{'all'}) ## @deftypefnx {duration} {@var{E} =} rmse (@dots{}, @var{nanflag}) ## @deftypefnx {duration} {@var{E} =} rmse (@dots{}, @qcode{'Weights'}, @var{W}) ## ## Compute the root mean squared error between duration arrays. ## ## This method overloads the core @code{rmse} function for duration arrays. ## The functionality is identical to core @code{rmse} function. ## Type @code{help rmse} for more information. ## ## @end deftypefn function this = rmse (this, A, varargin) if (! isduration (this) || ! isduration (A)) error ("duration.rmse: both F and A must be duration arrays."); endif this.Millis = rmse (this.Millis, A.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{y} =} skewness (@var{D}) ## @deftypefnx {duration} {@var{y} =} skewness (@var{D}, @var{flag}) ## @deftypefnx {duration} {@var{y} =} skewness (@var{D}, @var{flag}, @var{dim}) ## @deftypefnx {duration} {@var{y} =} skewness (@var{D}, @var{flag}, @var{vecdim}) ## @deftypefnx {duration} {@var{y} =} skewness (@var{D}, @var{flag}, @qcode{'all'}) ## ## Compute the sample skewness of a duration array. ## ## This method overloads the core @code{skewness} function for duration ## arrays. The functionality is identical to core @code{skewness} function. ## Type @code{help skewness} for more information. ## ## Note that skewness is a dimensionless quantity Thus, the returned ## argument is a numeric array of double type and not a duration array. ## ## @end deftypefn function y = skewness (D, varargin) y = skewness (D.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{S} =} std (@var{D}) ## @deftypefnx {duration} {@var{S} =} std (@var{D}, @var{w}) ## @deftypefnx {duration} {@var{S} =} std (@var{D}, @var{w}, @var{dim}) ## @deftypefnx {duration} {@var{S} =} std (@var{D}, @var{w}, @var{vecdim}) ## @deftypefnx {duration} {@var{S} =} std (@var{D}, @var{w}, @qcode{'all'}) ## @deftypefnx {duration} {@var{S} =} std (@dots{}, @var{nanflag}) ## @deftypefnx {duration} {[@var{S}, @var{M}] =} std (@dots{}) ## ## Compute the standard deviation of a duration array. ## ## This method overloads the core @code{std} function for duration arrays. ## The functionality is identical to core @code{std} function. ## Type @code{help std} for more information. ## ## @end deftypefn function varargout = std (this, varargin) if (nargout > 1) M = this; [this.Millis, M.Millis] = std (this.Millis, varargin{:}); varargout{1} = this; varargout{2} = M; else this.Millis = std (this.Millis, varargin{:}); varargout{1} = this; endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{bin} =} discretize (@var{D}, @var{edges}) ## @deftypefnx {duration} {@var{bin} =} discretize (@var{D}, @var{N}) ## @deftypefnx {duration} {@var{bin} =} discretize (@var{D}, @var{dur}) ## @deftypefnx {duration} {@var{bin} =} discretize (@var{D}, @var{unit}) ## @deftypefnx {duration} {@var{Y} =} discretize (@dots{}, @var{values}) ## @deftypefnx {duration} {@var{C} =} discretize (@dots{}, 'categorical') ## @deftypefnx {duration} {@var{C} =} discretize (@dots{}, 'categorical', @var{names}) ## @deftypefnx {duration} {@var{Y} =} discretize (@dots{}, 'IncludedEdge', @var{side}) ## @deftypefnx {duration} {[@var{bin}, @var{edges}] =} discretize (@dots{}) ## ## Group durations into bins. ## ## @code{@var{bin} = discretize (@var{D}, @var{edges})} returns the index of ## the bin each element of @var{D} falls into, given a @qcode{duration} ## vector of bin edges. Bin @math{j} covers @code{[@var{edges}(j), ## @var{edges}(j+1))}, except the last, which is closed at both ends. ## Elements outside the edges, and @qcode{NaN} durations, return ## @qcode{NaN}. ## ## @code{@var{bin} = discretize (@var{D}, @var{N})} uses @var{N} bins of ## uniform width, placed on whole time units where that is possible. ## ## @code{@var{bin} = discretize (@var{D}, @var{dur})} uses bins of the fixed ## width @var{dur}, a scalar @qcode{duration}, ## and @code{@var{bin} = discretize (@var{D}, @var{unit})} uses bins one ## @var{unit} wide, where @var{unit} is one of @qcode{'second'}, ## @qcode{'minute'}, @qcode{'hour'}, @qcode{'day'}, @qcode{'week'}, ## @qcode{'month'}, @qcode{'quarter'}, @qcode{'year'}, @qcode{'decade'} or ## @qcode{'century'}. A month is a twelfth of the 365.2425-day year that ## @code{years} counts, since a @qcode{duration} has no calendar. ## ## The remaining options are as for the top-level @code{discretize}. ## ## @strong{Deviation from MATLAB, for an explicitly requested bin count ## only.} Every other way of binning -- automatic, by width, by unit, or by ## explicit edges -- agrees with MATLAB exactly. Asked for a bin count, ## MATLAB chooses between whole-unit and decimal widths in a way that is not ## monotonic in the data: over @code{seconds ([0 16.8])} it gives 7 bins of ## 2.5 s, over the wider @code{seconds ([0 17.15])} 7 bins of 3 s, and ## decimal widths again above that. It also discards bins, giving ## @code{hours (0:24)} in 7 bins a 4-hour width that covers the data in 6. ## A whole time unit is used here whenever it still needs every bin ## requested, which is monotonic and never wastes a bin. ## ## @seealso{histcounts, discretize} ## @end deftypefn function [BIN, EDGES] = discretize (this, arg2, varargin) ## Input validation if (nargin < 2) error ("duration.discretize: not enough input arguments."); endif xv = this.Millis(:); [ev, isCount] = durbinedges (xv, arg2, 'duration.discretize'); EDGES = milliseconds (ev); EDGES.Format = this.Format; if (isCount) EDGES = reshape (EDGES, 1, numel (ev)); endif ## Delegate the assignment and the value mapping to the numeric function [BIN, ~] = discretize (reshape (xv, size (this.Millis)), ev, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{N} =} histcounts (@var{D}) ## @deftypefnx {duration} {@var{N} =} histcounts (@var{D}, @var{nbins}) ## @deftypefnx {duration} {@var{N} =} histcounts (@var{D}, @var{edges}) ## @deftypefnx {duration} {@var{N} =} histcounts (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {duration} {[@var{N}, @var{edges}] =} histcounts (@dots{}) ## @deftypefnx {duration} {[@var{N}, @var{edges}, @var{bin}] =} histcounts (@dots{}) ## ## Histogram bin counts for durations. ## ## @code{@var{N} = histcounts (@var{D})} bins the durations in @var{D}, ## chosen automatically, and returns the number of elements in each bin. ## @var{D} is treated as @code{@var{D}(:)} and @qcode{NaN} durations are ## excluded. ## ## @code{@var{N} = histcounts (@var{D}, @var{nbins})} and ## @code{@var{N} = histcounts (@var{D}, @var{edges})} bin by count and by ## explicit @qcode{duration} edges respectively. ## ## @qcode{'BinWidth'} takes a scalar @qcode{duration}, ## @qcode{'BinLimits'} a two-element ## @qcode{duration}, and @qcode{'BinEdges'} a @qcode{duration} vector. ## @qcode{'BinMethod'} accepts @qcode{'auto'}, @qcode{'scott'}, ## @qcode{'fd'}, @qcode{'sturges'} and @qcode{'sqrt'}, but not ## @qcode{'integers'}, which has no meaning for a duration. ## ## @qcode{'Normalization'} accepts @qcode{'count'}, @qcode{'cumcount'}, ## @qcode{'probability'}, @qcode{'percentage'} and @qcode{'cdf'}. ## @qcode{'countdensity'} and @qcode{'pdf'} are not accepted, since a ## density per unit time has no meaning here. ## ## @seealso{discretize, histcounts} ## @end deftypefn function [N, EDGES, BIN] = histcounts (this, varargin) xv = this.Millis(:); args = durhistargs (varargin, xv, 'duration.histcounts'); if (nargout > 2) [N, ev, BIN] = histcounts (xv, args{:}); BIN = reshape (BIN, size (this.Millis)); else [N, ev] = histcounts (xv, args{:}); endif EDGES = milliseconds (ev); EDGES.Format = this.Format; endfunction endmethods ################################################################################ ## ** Sort, Filter, and Set Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'maxk' 'mink' 'sort' 'sortrows' ## ## 'topkrows' 'unique' 'interp1' 'intersect' ## ## 'setdiff' 'setxor' 'union' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} maxk (@var{A}, @var{K}) ## @deftypefnx {duration} {@var{B} =} maxk (@var{A}, @var{K}, @var{dim}) ## @deftypefnx {duration} {[@var{B}, @var{index}] =} maxk (@var{A}, @dots{}) ## ## Find the largest elements in a duration array. ## ## @code{@var{B} = maxk (@var{A}, @var{K})} returns the @var{K} largest ## elements of the duration array @var{A} in descending order. If @var{A} ## is a vector, then @var{B} is a vector with @var{K} elements. If @var{A} ## is a matrix, then @code{maxk} operates along each column of @var{A} and ## @var{B} has @var{K} rows. For multidimensional arrays, @code{maxk} ## operates along the first non-singleton dimension. ## ## @code{@var{B} = maxk (@var{A}, @var{K}, @var{dim})} operates along the ## dimension specified by @var{dim}. ## ## @var{K} must be a nonnegative integer scalar. If @var{K} is larger than ## the number of elements along the operating dimension, then all of them ## are returned. @qcode{Inf} and @qcode{-Inf} are ranked as ordinary ## values. ## ## Missing elements (@qcode{NaN}) are not ranked. They are appended after ## the ranked elements in their original order, and hence they only appear ## in @var{B} when @var{K} exceeds the number of non-missing elements along ## the operating dimension. Unlike @code{sort}, @code{maxk} has no ## @qcode{'MissingPlacement'} option, since @qcode{NaN} elements are always ## placed last. Elements comparing as equal keep their original order. ## ## @code{[@var{B}, @var{index}] = maxk (@var{A}, @dots{})} also returns an ## index array containing the indices of the returned elements of @var{A} ## along the operating dimension. ## ## @end deftypefn function [B, index] = maxk (A, K, varargin) if (nargin < 2) error ("duration.maxk: too few input arguments."); endif [index, lidx, errmsg] = __minmaxk__ (A.Millis, K, true, varargin); if (! isempty (errmsg)) error ("duration.maxk: %s", errmsg); endif B = subset (A, lidx); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} mink (@var{A}, @var{K}) ## @deftypefnx {duration} {@var{B} =} mink (@var{A}, @var{K}, @var{dim}) ## @deftypefnx {duration} {[@var{B}, @var{index}] =} mink (@var{A}, @dots{}) ## ## Find the smallest elements in a duration array. ## ## @code{@var{B} = mink (@var{A}, @var{K})} returns the @var{K} smallest ## elements of the duration array @var{A} in ascending order. If @var{A} ## is a vector, then @var{B} is a vector with @var{K} elements. If @var{A} ## is a matrix, then @code{mink} operates along each column of @var{A} and ## @var{B} has @var{K} rows. For multidimensional arrays, @code{mink} ## operates along the first non-singleton dimension. ## ## @code{@var{B} = mink (@var{A}, @var{K}, @var{dim})} operates along the ## dimension specified by @var{dim}. ## ## @var{K} must be a nonnegative integer scalar. If @var{K} is larger than ## the number of elements along the operating dimension, then all of them ## are returned. @qcode{Inf} and @qcode{-Inf} are ranked as ordinary ## values. ## ## Missing elements (@qcode{NaN}) are not ranked. They are appended after ## the ranked elements in their original order, and hence they only appear ## in @var{B} when @var{K} exceeds the number of non-missing elements along ## the operating dimension. Unlike @code{sort}, @code{mink} has no ## @qcode{'MissingPlacement'} option, since @qcode{NaN} elements are always ## placed last. Elements comparing as equal keep their original order. ## ## @code{[@var{B}, @var{index}] = mink (@var{A}, @dots{})} also returns an ## index array containing the indices of the returned elements of @var{A} ## along the operating dimension. ## ## @end deftypefn function [B, index] = mink (A, K, varargin) if (nargin < 2) error ("duration.mink: too few input arguments."); endif [index, lidx, errmsg] = __minmaxk__ (A.Millis, K, false, varargin); if (! isempty (errmsg)) error ("duration.mink: %s", errmsg); endif B = subset (A, lidx); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} sort (@var{A}) ## @deftypefnx {duration} {@var{B} =} sort (@var{A}, @var{dim}) ## @deftypefnx {duration} {@var{B} =} sort (@var{A}, @var{direction}) ## @deftypefnx {duration} {@var{B} =} sort (@var{A}, @var{dim}, @var{direction}) ## @deftypefnx {duration} {@var{B} =} sort (@dots{}, @qcode{'MissingPlacement'}, @var{MP}) ## @deftypefnx {duration} {@var{B} =} sort (@dots{}, @qcode{'ComparisonMethod'}, @var{CM}) ## @deftypefnx {duration} {[@var{B}, @var{index}] =} sort (@var{A}, @dots{}) ## ## Sort elements in a duration array. ## ## @code{@var{B} = sort (@var{A})} sorts the duration array @var{A} in ## ascending order. If @var{A} is a matrix, @code{sort (@var{A})} sorts ## each column of @var{A} in ascending order. For multidimensional arrays, ## @code{mode (@var{A})} sorts along the first non-singleton dimension. ## ## @code{@var{B} = sort (@var{A}, @var{dim})} sorts along the dimension ## specified by @var{dim}. ## ## @code{@var{B} = sort (@var{A}, @var{direction})} also specifies the ## sorting direction, which can be either @qcode{'ascend'} (default) or ## @qcode{'descend'}. ## ## @code{@var{B} = sort (@dots{}, @qcode{'MissingPlacement'}, @var{MP})} ## specifies where to place the missing elements (@qcode{}) ## returned in @var{B} with one of the following options specified in ## @var{MP}: ## ## @itemize ## @item @qcode{'auto'}, which is the default, places missing elements last ## for ascending sort and first for descending sort. ## @item @qcode{'first'} places missing elements first. ## @item @qcode{'last'} places missing elements last. ## @end itemize ## ## @code{@var{B} = sort (@dots{}, @qcode{'ComparisonMethod'}, @var{CM})} ## specifies the comparison method for determining the order of elements ## returned in @var{B} with one of following options: ## ## @itemize ## @item @qcode{'auto'}, which is the default, sorts by @code{real (A)}. ## @item @qcode{'real'} sorts by @code{real (A)}. ## @item @qcode{'abs'} sorts by @code{abs (A)}. ## @end itemize ## ## @code{[@var{B}, @var{index}] = sort (@var{A}, @dots{})} also returns a ## sorting index containing the original indices of the elements in the ## sorted array. @var{index} is the same size as @var{A} and it comprises ## indexing vectors oriented along the operating dimensions. ## ## @itemize ## @item If @var{A} is a vector, then @var{index} contains the original ## linear indices of the elements in the sorted vector @var{B} such that ## @code{@var{B} = @var{A}(@var{index})}. ## @item If @var{A} is an @math{M*N} matrix and @qcode{@var{dim} = 1}, then ## @var{index} contains the original row indices of the elements in the ## sorted vector @var{B} such that for @qcode{j = 1:N}, ## @code{@var{B}(:,j) = @var{A}(@var{index}(:,j),j)}. ## @end itemize ## ## @end deftypefn function [B, index] = sort (A, varargin) ## Parse and validate optional paired arguments optNames = {'MissingPlacement', 'ComparisonMethod'}; dfValues = {'auto', 'auto'}; [MP, CM, args] = parsePairedArguments (optNames, dfValues, varargin(:)); ## The values are matched without regard to case, as MATLAB does. MP = lower (MP); CM = lower (CM); if (! ismember (MP, {'auto', 'first', 'last'})) error ("duration.sort: invalid value for 'MissingPlacement'."); endif if (! ismember (CM, {'auto', 'real', 'abs'})) error ("duration.sort: invalid value for 'ComparisonMethod'."); endif ## Force strings to character vectors [args{:}] = convertStringsToChars (args{:}); ## Only a dimension and a direction may remain, once each for k = 1:numel (args) arg = args{k}; if (isnumeric (arg)) if (! (isscalar (arg) && arg >= 1 && arg == fix (arg))) error ("duration.sort: DIM must be a positive integer."); endif elseif (ischar (arg) && isrow (arg)) if (! any (strcmpi (arg, {'ascend', 'descend'}))) error ("duration.sort: invalid DIRECTION '%s'.", arg); endif else error ("duration.sort: invalid input argument."); endif endfor if (sum (cellfun (@isnumeric, args)) > 1 || sum (cellfun (@ischar, args)) > 1) error ("duration.sort: invalid input argument."); endif if (numel (args) == 2 && ischar (args{1})) error ("duration.sort: DIM must precede DIRECTION."); endif ## Get direction cid = cellfun (@ischar, args); if (any (cid)) dir = lower (args{cid}); ## Put it back, since args is forwarded to the core sort further down. args(cid) = {dir}; else dir = 'ascend'; endif ## Get operating dimension szA = size (A); cid = cellfun (@isnumeric, args); if (any (cid)) dim = args{cid}; else dim = find (szA != 1, 1); if (isempty (dim)) # scalar dim = 1; endif endif ## Apply comparison method (Octave specific) data = A.Millis; if (strcmp (CM, 'abs')) data = abs (data); endif ## Special handling for missing elements when missing placement overrides ## default behavior (only if missing data actually exist). is_nan = isnan (data); if (any (is_nan, 'all')) ## FIX ME: this workaround will be removed once the 'sort' function ## in core Octave supports 'MissingPlacement' optional argument. ## This implementation fails the edge case where -Inf and -realmax ## elements are present along the operating dimension. if ((strcmp (dir, 'ascend') && strcmp (MP, {'first'})) || (strcmp (dir, 'descend') && strcmp (MP, {'last'}))) ## Convert missing values to -Inf so that they are placed ## appropriately according to 'MissingPlacement' specification. ## If -Inf values already exist in data, then convert them to the ## next smallest possible value is_m_inf = data == -Inf; if (any (is_m_inf, 'all')) m_inf_rep = - realmax; data(is_m_inf) = m_inf_rep; endif data(is_nan) = -Inf; endif endif ## Sort values [~, index] = sort (data, args{:}); ## Calculate linear index n_dims = ndims (A); dimarg = cell (1, n_dims); for i = 1:n_dims if (i == dim) dimarg{i} = index; else dim_sz = szA(i); tmpvec = ones (1, n_dims); tmpvec(i) = dim_sz; tmp_sz = szA; tmp_sz(i) = 1; dimarg{i} = repmat (reshape ([1:dim_sz], tmpvec), tmp_sz); endif endfor ## Return sorted duration array B = subset (A, sub2ind (szA, dimarg{:})); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} sortrows (@var{A}) ## @deftypefnx {duration} {@var{B} =} sortrows (@var{A}, @var{col}) ## @deftypefnx {duration} {@var{B} =} sortrows (@var{A}, @var{direction}) ## @deftypefnx {duration} {@var{B} =} sortrows (@var{A}, @var{col}, @var{direction}) ## @deftypefnx {duration} {@var{B} =} sortrows (@dots{}, @qcode{'MissingPlacement'}, @var{MP}) ## @deftypefnx {duration} {[@var{B}, @var{index}] =} sortrows (@var{A}, @dots{}) ## ## Sort rows in a duration array. ## ## @code{@var{B} = sortrows (@var{A})} sorts the rows of the 2-D duration ## array @var{A} in ascending order. The sorted array @var{B} has the same ## size as @var{A}. ## ## @code{@var{B} = sortrows (@var{A}, @var{col})} sorts @var{A} according to ## to the columns specified by the numeric vector @var{col}, which must ## explicitly contain non-zero integers whose absolute values index existing ## columns in @var{A}. Positive elements sort the corresponding columns in ## ascending order, while negative elements sort the corresponding columns ## in descending order. ## ## @code{@var{B} = sortrows (@var{A}, @var{direction})} also specifies the ## sorting direction, which can be either @qcode{'ascend'} (default) or ## @qcode{'descend'} applying to all columns in @var{A}. Alternatively, ## @var{direction} can be either a string array or a cell array of character ## vectors specifying the sorting direction for each individual column of ## @var{A}, in which case the number of elements in @var{direction} must ## equal the number of columns in @var{A}. ## ## @code{@var{B} = sortrows (@var{A}, @var{col}, @var{direction})} sorts the ## categorical array @var{A} according to the columns specified in @var{col} ## using the corresponding sorting direction specified in @var{direction}. ## In this case, the sign of the values in @var{col} is ignored. @var{col} ## and @var{direction} must have the same number of elements, but not ## necessarily equal to the columns of @var{A}. ## ## @code{@var{B} = sortrows (@dots{}, @qcode{'MissingPlacement'}, @var{MP})} ## specifies where to place the missing elements (@qcode{NaN}) returned in ## @var{B} with any of the following options specified in @var{MP}: ## ## @itemize ## @item @qcode{'auto'}, which is the default, places missing elements last ## for ascending sort and first for descending sort. ## @item @qcode{'first'} places missing elements first. ## @item @qcode{'last'} places missing elements last. ## @end itemize ## ## @code{[@var{B}, @var{index}] = sortrows (@var{A}, @dots{})} also returns ## an index vector containing the original row indices of @var{A} in the ## sorted matrix @var{B} such that @code{@var{B} = @var{A}(@var{index},:)}. ## ## @end deftypefn function [B, index] = sortrows (A, varargin) ## Input array must be a matrix if (ndims (A) != 2) error ("duration.sortrows: A must be a 2-D matrix."); endif ## Parse and validate optional paired arguments optNames = {'MissingPlacement', 'ComparisonMethod'}; dfValues = {'auto', 'auto'}; [MP, CM, args] = parsePairedArguments (optNames, dfValues, varargin(:)); ## The values are matched without regard to case, as MATLAB does. MP = lower (MP); CM = lower (CM); if (! any (strcmp (MP, {'auto', 'first', 'last'}))) error ("duration.sortrows: invalid value for 'MissingPlacement'."); endif if (! any (strcmp (CM, {'auto', 'real', 'abs'}))) error ("duration.sortrows: invalid value for 'ComparisonMethod'."); endif ## Parse COL / DIRECTION input nc = size (A, 2); col = [1:nc]; # default ascending direction dir_flag = false; if (numel (args) > 2) error ("duration.sortrows: too many input arguments."); endif if (numel (args) > 0) col = args{1}; if (isnumeric (col)) if (! isvector (col) || any (fix (col) != col) || any (col == 0)) error (strcat ("duration.sortrows: COL must be a vector", ... " of nonzero integers indexing columns in A.")); endif if (max (abs (col)) > nc) error ("duration.sortrows: COL indexes non-existing column."); endif elseif (isvector (col) && (ischar (col) || iscellstr (col) || isa (col, 'string'))) direction = lower (cellstr (col)); if (! all (ismember (direction, {'ascend', 'descend'}))) error (strcat ("duration.sortrows: DIRECTION input must", ... " contain either 'ascend' or 'descend' values.")); endif ## Apply scalar expansion if (isscalar (direction)) direction = repmat (direction, 1, nc); endif if (numel (direction) != nc) error (strcat ("duration.sortrows: DIRECTION", ... " does not match the columns in A.")); endif ## Assign DIRECTION to COL col = [1:nc]; idx = strcmp (direction, 'descend'); col(idx) = - col(idx); dir_flag = true; else error ("duration.sortrows: invalid type for COL argument."); endif endif if (numel (args) > 1) if (dir_flag) error ("duration.sortrows: invalid third input argument."); endif if ((isvector (args{2}) && ischar (args{2})) || isa (args{2}, 'string')) direction = lower (cellstr (args{2})); elseif (isvector (args{2}) && iscellstr (args{2})) direction = args{2}; else error ("duration.sortrows: invalid type for DIRECTION argument."); endif if (! all (ismember (direction, {'ascend', 'descend'}))) error (strcat ("duration.sortrows: DIRECTION input must", ... " contain either 'ascend' or 'descend' values.")); endif ## Assign DIRECTION to COL if (isscalar (direction) && strcmp (direction, 'ascend')) col = abs (col); elseif (isscalar (direction) && strcmp (direction, 'descend')) col = - abs (col); else if (numel (direction) != numel (col)) error (strcat ("duration.sortrows: DIRECTION does", ... " not match the elements in COL.")); endif col = abs (col); idx = strcmp (direction, 'descend'); col(idx) = - col(idx); endif endif ## Apply comparison method (Octave specific) data = A.Millis; if (strcmp (CM, 'abs')) data = abs (data); endif ## FIX ME: this workaround will be removed once the 'sortrows' function ## in core Octave supports 'MissingPlacement' optional argument. ## This implementation fails the edge case where -Inf and -realmax ## elements are present along the operating dimension. pos_dir = col > 0; neg_dir = col < 0; fix_pos_dir = any (pos_dir) && strcmp (MP, {'first'}); fix_neg_dir = any (neg_dir) && strcmp (MP, {'last'}); if (fix_pos_dir || fix_neg_dir) ## Apply on selected columns if (fix_pos_dir) col_idx = col(pos_dir); else # must be fix_neg_dir col_idx = - col(neg_dir); endif cdata = data(:,col_idx); ## Only if missing data exist in operating columns. is_nan = isnan (cdata); if (any (is_nan, 'all')) ## Convert missing values to -Inf so that they are placed ## appropriately according to 'MissingPlacement' specification. ## If -Inf values already exist in data, then convert them to the ## next smallest possible value is_m_inf = cdata == -Inf; if (any (is_m_inf, 'all')) m_inf_rep = - realmax; cdata(is_m_inf) = m_inf_rep; endif cdata(is_nan) = -Inf; data(:,col_idx) = cdata; ## Sort values [~, index] = sortrows (data, col); else ## Sort values without special handling [~, index] = sortrows (data, col); endif else ## Sort values with no undefined elements [~, index] = sortrows (data, col); endif ## Return sorted duration array B = subset (A, index, ':'); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} topkrows (@var{A}, @var{K}) ## @deftypefnx {duration} {@var{B} =} topkrows (@var{A}, @var{K}, @var{col}) ## @deftypefnx {duration} {@var{B} =} topkrows (@var{A}, @var{K}, @var{direction}) ## @deftypefnx {duration} {@var{B} =} topkrows (@var{A}, @var{K}, @var{col}, @var{direction}) ## @deftypefnx {duration} {@var{B} =} topkrows (@dots{}, @qcode{'MissingPlacement'}, @var{MP}) ## @deftypefnx {duration} {[@var{B}, @var{index}] =} topkrows (@var{A}, @dots{}) ## ## Top K sorted rows of a duration array. ## ## @code{@var{B} = topkrows (@var{A}, @var{K})} returns the top @var{K} rows ## of the 2-D duration array @var{A} sorted in descending order as a group. ## @var{K} must be a nonnegative integer scalar. If @var{K} is larger than ## the number of rows in @var{A}, then all of them are returned. ## ## Missing elements (@qcode{NaN}) are not ranked. Within each sort ## column they are placed after the elements that are defined, whichever ## direction is asked for, so a row is demoted only by a missing element in ## a column that actually decides its position. Rows comparing as equal ## keep their original order. ## ## @code{@var{B} = topkrows (@var{A}, @var{K}, @var{col})} sorts using only ## the columns listed in the numeric vector @var{col}, which must contain ## positive integers indexing existing columns in @var{A}. Columns are used ## as sort keys in the order given, and those not listed are not used at ## all. The direction is descending unless @var{direction} says otherwise. ## ## @code{@var{B} = topkrows (@var{A}, @var{K}, @var{direction})} sorts in ## the given @var{direction}, either @qcode{'descend'} (default) or ## @qcode{'ascend'} applying to all columns in @var{A}. Alternatively, ## @var{direction} can be a cell array of character vectors specifying the ## sorting direction for each individual column of @var{A}. ## ## @code{@var{B} = topkrows (@var{A}, @var{K}, @var{col}, @var{direction})} ## combines an explicit column list with a per-column @var{direction}. ## ## @code{@var{B} = topkrows (@dots{}, @qcode{'MissingPlacement'}, @var{MP})} ## specifies where the missing elements are placed within each sort column, ## with any of the following options specified in @var{MP}: ## ## @itemize ## @item @qcode{'last'}, which is the default, places missing elements last ## whichever direction is asked for. ## @item @qcode{'first'} places missing elements first. ## @item @qcode{'auto'} places missing elements last for an ascending sort ## and first for a descending one, as @code{sortrows} does. ## @end itemize ## ## This is an Octave extension: MATLAB has no such option here and always ## ranks as @qcode{'last'} does. ## ## @code{[@var{B}, @var{index}] = topkrows (@var{A}, @dots{})} also returns ## an index vector containing the original row indices of @var{A} in ## @var{B}, such that @code{@var{B} = @var{A}(@var{index},:)}. ## ## @end deftypefn function [B, index] = topkrows (A, K, varargin) ## Check input arguments if (nargin < 2) error ("duration.topkrows: too few input arguments."); endif if (ndims (A) != 2) error ("duration.topkrows: A must be a 2-D array."); endif [col, direction, MP, errmsg] = ... __topkrowsargs__ (K, columns (A), varargin); if (! isempty (errmsg)) error ("duration.topkrows: %s", errmsg); endif ## Sort rows, keeping missing elements out of the ranking if (isempty (col)) [B, index] = sortrows (A, direction, 'MissingPlacement', MP); else [B, index] = sortrows (A, col, direction, 'MissingPlacement', MP); endif ## Return top K rows if (K < numel (index)) B = subset (B, 1:K, ':'); index = index(1:K); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} unique (@var{A}) ## @deftypefnx {duration} {@var{B} =} unique (@var{A}, @var{setOrder}) ## @deftypefnx {duration} {@var{B} =} unique (@var{A}, @var{occurrence}) ## @deftypefnx {duration} {@var{B} =} unique (@var{A}, @var{setOrder}, @var{occurrence}) ## @deftypefnx {duration} {@var{B} =} unique (@var{A}, @var{occurrence}, @var{setOrder}) ## @deftypefnx {duration} {@var{B} =} unique (@var{A}, @dots{}, @qcode{'rows'}) ## @deftypefnx {duration} {[@var{B}, @var{ixA}, @var{ixB}] =} unique (@dots{}) ## ## Unique values in a duration array. ## ## @code{@var{B} = unique (@var{A})} returns the unique values of the ## duration array @var{A} in sorted order. ## ## @code{@var{B} = unique (@var{A}, @var{setOrder})} returns the unique ## values of the duration array @var{A} in an order as specified by ## @var{setOrder}, which can be either of the following values: ## ## @itemize ## @item @qcode{'sorted'} (default) returns the unique values sorted in ## ascending order. ## @item @qcode{'stable'} returns the unique values according to their order ## of occurrence. ## @end itemize ## ## @code{@var{B} = unique (@var{A}, @var{occurrence})} returns the unique ## values of the duration array @var{A} according to their order of ## occurrence. @var{occurrence} can be either of the following values: ## ## @itemize ## @item @qcode{'first'} (default) returns the first occurrence of each ## unique value, i.e. the lowest possible indices are returned. ## @item @qcode{'last'} returns the last occurrence of each unique value, ## i.e. the highest possible indices are returned. ## @end itemize ## ## You can specify @var{setOrder} and @var{occurrence} arguments together. ## ## @code{@var{B} = unique (@var{A}, @dots{}, @qcode{'rows'})} returns the ## unique rows of @var{A} by treating each row as a single entity. The ## @qcode{'rows'} option can be used alone or in any combination with the ## @var{setOrder} and @var{occurrence} arguments. @qcode{'rows'} can be ## placed at any position in the function's argument list after the input ## array @var{A}. However, this syntax is only valid for 2-dimensional ## duration arrays. ## ## @code{[@var{B}, @var{ixA}, @var{ixB}] = unique (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} using any of the previous syntaxes. ## @var{ixA} and @var{ixB} map the arrays @var{A} and @var{B} to one another ## such that @qcode{@var{B} = @var{A}(@var{ixA})} and ## @qcode{@var{A} = @var{B}(@var{ixB})}. When the @qcode{'rows'} optional ## argument is specified, then @qcode{@var{B} = @var{A}(@var{ixA},:)} and ## @qcode{@var{A} = @var{B}(@var{ixB},:)}. ## ## @end deftypefn function [this, ixA, ixB] = unique (this, varargin) ## 'legacy' option is not supported if (any (strcmp ('legacy', varargin))) error ("duration.unique: 'legacy' option is not supported."); endif [this.Millis, ixA, ixB] = __unique__ (this.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{YI} =} interp1 (@var{X}, @var{Y}, @var{XI}) ## @deftypefnx {duration} {@var{YI} =} interp1 (@var{Y}, @var{XI}) ## @deftypefnx {duration} {@var{YI} =} interp1 (@dots{}, @var{method}) ## @deftypefnx {duration} {@var{YI} =} interp1 (@dots{}, @var{method}, @var{extrapolation}) ## @deftypefnx {duration} {@var{pp} =} interp1 (@var{X}, @var{Y}, @qcode{'pp'}) ## @deftypefnx {duration} {@var{pp} =} interp1 (@var{X}, @var{Y}, @var{method}, @qcode{'pp'}) ## ## One-dimensional interpolation for duration arrays. ## ## @code{@var{YI} = interp1 (@var{X}, @var{Y}, @var{XI})} computes the ## linearly interpolated values of a one-dimensional function, which is ## represented by sample points @var{X} and corresponding values @var{Y}, ## at specific query points @var{XI}. @var{X} must be a vector. If @var{Y} ## is vector, then it must have the same length as @var{X}. If @var{Y} is ## matrix, then each column is treated as a different set of one-dimensional ## sample values and the number of rows must equal the length of @var{X}. ## @var{XI} must be a vector and the same data type as @var{X}. The output ## @var{XI} is the same data type as @var{Y} and its size depends on @var{Y} ## and @var{XI}. @var{X} and @var{Y} can both be duration arrays or one of ## them can be a numeric array. ## ## @code{@var{YI} = interp1 (@var{Y}, @var{XI})} computes the linearly ## interpolated values of a one-dimensional function assuming a default set ## of sampling points determined by the shape of @var{Y}: ## ## @itemize ## @item If @var{Y} is a vector, then the default sampling points are ## @code{[1:length(@var{Y})}. ## ## @item If @var{Y} is an array, then the default sampling points are ## @code{[1:size(@var{Y})}. ## @end itemize ## ## @code{@var{YI} = interp1 (@dots{}, @var{method})} specifies one of the ## following interpolation methods to be used: ## ## @itemize ## @item @qcode{'linear'} (default) computes the linear interpolation from ## nearest neighbors. ## ## @item @qcode{'nearest'} returns the nearest neighbor. ## ## @item @qcode{'next'} returns the next neighbor. ## ## @item @qcode{'previous'} returns the previous neighbor. ## ## @item @qcode{'pchip'} computes the piecewise cubic Hermite interpolating ## polynomial, which corresponds to shape-preserving interpolation with ## smooth first derivative. ## ## @item @qcode{'cubic'} computes the cubic interpolation. ## ## @item @qcode{'spline'} computes the cubic spline interpolation, which ## corresponds to smooth first and second derivatives throughout the curve. ## @end itemize ## ## @code{@var{YI} = interp1 (@dots{}, @var{method}, @var{extrapolation})} ## further specifies a strategy for evaluating points that lie outside the ## range of the sample points in @var{X}. Set @var{extrapolation} to ## @qcode{'extrap'} to use the current @var{method} to extrapolate values. ## Set @var{extrapolation} to a scalar value of the same data type as ## @var{Y} to return a constant value outside the range of @var{X}. When ## unspecified, @var{extrapolation} defaults to @qcode{NaN}. ## ## @code{@var{pp} = interp1 (@var{X}, @var{Y}, @qcode{'pp'}} returns a ## piecewise polynomial object, using the default linear interpolation ## algorithm, which can be later used with @code{ppval} to evaluate the ## interpolation at new query points. ## ## @code{@var{pp} = interp1 (@var{X}, @var{Y}, @var{method}, @qcode{'pp'}} ## return a piecewise polynomial object, using the interpolation algorithm ## specified by @var{method}, which can be later used with @code{ppval} to ## evaluate the interpolation at new query points. ## ## @end deftypefn function YI = interp1 (X, Y, varargin) if (isempty (varargin)) ## YI = interp1 (Y, XI) if (isduration (Y)) Y = Y.days; endif if (isduration (X)) YI = duration ('Format', X.Format); YI.Millis = interp1 (X.Millis, Y); else YI = interp1 (X, Y); endif else [varargin{:}] = convertStringsToChars (varargin{:}); method = varargin{1}; if (ischar (method)) ## YI = interp1 (Y, XI, method) ## YI = interp1 (Y, XI, method, extrap) ## PP = interp1 (X, Y, pp) ## PP = interp1 (X, Y, method, pp) X_isDur = isa (X, 'duration'); Y_isDur = isa (Y, 'duration'); if (X_isDur) F = X.Format; X = X.days; endif if (Y_isDur) Y = Y.days; endif ## Handle numeric extrapolation input extrap = cellfun (@(x) isduration (x) || isnumeric (x), varargin); if (any (extrap)) tmp = varargin{extrap}; ExtDur = isduration (tmp); if (xor (ExtDur, X_isDur)) error (strcat ("duration.interp1: EXTRAPOLATION scalar", ... " value must match Y.")); elseif (ExtDur) varargin{extrap} = days (tmp); endif endif YI = interp1 (X, Y, varargin{:}); if (isnumeric (YI) && X_isDur) YI = days (YI); YI.Format = F; endif else XI = method; varargin(1) = []; ## YI = interp1 (X, Y, XI) ## YI = interp1 (X, Y, XI, method) ## YI = interp1 (X, Y, XI, method, extrap) X_isDur = isa (X, 'duration'); Y_isDur = isa (Y, 'duration'); XIisDur = isa (XI, 'duration'); if (xor (X_isDur, XIisDur)) error (strcat ("duration.interp1: if X is a duration array,", ... " XI must be also.")); endif ## Handle numeric extrapolation input extrap = cellfun (@(x) isduration (x) || isnumeric (x), varargin); if (any (extrap)) tmp = varargin{extrap}; ExtDur = isduration (tmp); if (xor (ExtDur, Y_isDur)) error (strcat ("duration.interp1: EXTRAPOLATION scalar", ... " value must match Y.")); elseif (ExtDur) ## This branch interpolates the stored milliseconds, so the ## extrapolation scalar has to be given in them too. varargin{extrap} = milliseconds (tmp); endif endif if (Y_isDur) YI = duration ('Format', Y.Format); if (X_isDur) YI.Millis = interp1 (X.Millis, Y.Millis, XI.Millis, varargin{:}); else YI.Millis = interp1 (X, Y.Millis, XI, varargin{:}); endif elseif (isnumeric (Y)) YI = interp1 (X.Millis, Y, XI.Millis, varargin{:}); else error ("duration.interp1: Y must be a duration or numeric array."); endif endif endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} intersect (@var{A}, @var{B}) ## @deftypefnx {duration} {@var{C} =} intersect (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {duration} {@var{C} =} intersect (@var{A}, @var{B}, @dots{}, @var{order}) ## @deftypefnx {duration} {[@var{C}, @var{ixA}, @var{ixB}] =} intersect (@dots{}) ## ## Set intersection of two duration arrays. ## ## @code{@var{C} = intersect (@var{A}, @var{B})} returns the unique common ## values of the duration arrays @var{A} and @var{B}. Either @var{A} or ## @var{B} input arguments can also be a duration string specified as a ## character vector, a string array, or a cell array of character vectors, ## or a numeric array representing 24-hour days. In such case, the input is ## promoted to a duration array prior to calculating the intersection. If ## both @var{A} and @var{B} are row vectors, then @var{C} is also a row ## vector, otherwise @code{intersect} returns a column vector. ## ## @code{@var{C} = intersect (@var{A}, @var{B}, @qcode{'rows'}} returns the ## unique common rows of the duration matrices @var{A} and @var{B}, which ## must have the same number of columns. By default, the rows in duration ## matrix @var{C} are in sorted order. ## ## @code{@dots{} = intersect (@var{A}, @var{B}, @dots{}, @var{order})} also ## specifies the order of the returned unique values. @var{order} can be ## @qcode{'sorted'}, which is the default behavior, or @qcode{'stable'}, in ## which case the unique values are returned in order of appearance. ## ## @code{[@var{C}, @var{ixA}, @var{ixB}] = intersect (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} such that ## @code{@var{C} = @var{A}(@var{ixA})} and ## @code{@var{C} = @var{B}(@var{ixB})}, unless the @qcode{'rows'} optional ## argument is given, in which case @code{@var{C} = @var{A}(@var{ixA},:)} ## and @code{@var{C} = @var{B}(@var{ixB},:)}. ## ## @end deftypefn function [C, ixA, ixB] = intersect (A, B, varargin) ## 'legacy' option is not supported if (any (strcmp ("legacy", varargin))) error ("duration.intersect: 'legacy' option is not supported."); endif if (ischar (A) || isstring (A) || iscellstr (A) || isnumeric (A)) C = duration ('Format', B.Format); A = promote (A); elseif (ischar (B) || isstring (B) || iscellstr (B) || isnumeric (B)) C = duration ('Format', A.Format); B = promote (B); elseif (isduration (A) && isduration (B)) C = duration ('Format', A.Format); else error (strcat ("duration.intersect: set operation not defined", ... " between '%s' and '%s' arrays."), class (A), class (B)); endif [C.Millis, ixA, ixB] = intersect (A.Millis, B.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} setdiff (@var{A}, @var{B}) ## @deftypefnx {duration} {@var{C} =} setdiff (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {duration} {@var{C} =} setdiff (@var{A}, @var{B}, @dots{}, @var{order}) ## @deftypefnx {duration} {[@var{C}, @var{ixA}] =} setdiff (@dots{}) ## ## Set difference of two duration arrays. ## ## @code{@var{C} = setdiff (@var{A}, @var{B})} returns the unique common ## values of the duration arrays @var{A} and @var{B}. Either @var{A} or ## @var{B} input arguments can also be a duration string specified as a ## character vector, a string array, or a cell array of character vectors, ## or a numeric array representing 24-hour days. In such case, the input is ## promoted to a duration array prior to calculating the intersection. If ## both @var{A} and @var{B} are row vectors, then @var{C} is also a row ## vector, otherwise @code{intersect} returns a column vector. ## ## @code{@var{C} = setdiff (@var{A}, @var{B}, @qcode{'rows'}} returns the ## unique common rows of the duration matrices @var{A} and @var{B}, which ## must have the same number of columns. By default, the rows in duration ## matrix @var{C} are in sorted order. ## ## @code{@dots{} = setdiff (@var{A}, @var{B}, @dots{}, @var{order})} also ## specifies the order of the returned unique values. @var{order} can be ## @qcode{'sorted'}, which is the default behavior, or @qcode{'stable'}, in ## which case the unique values are returned in order of appearance. ## ## @code{[@var{C}, @var{ixA}] = setdiff (@dots{})} also returns the index ## vector @var{ixA} such that @code{@var{C} = @var{A}(@var{ixA})}, unless ## the @qcode{'rows'} optional argument is given, in which case ## @code{@var{C} = @var{A}(@var{ixA},:)}. ## ## @end deftypefn function [C, index] = setdiff (A, B, varargin) ## 'legacy' option is not supported if (any (strcmp ("legacy", varargin))) error ("duration.setdiff: 'legacy' option is not supported."); endif if (ischar (A) || isstring (A) || iscellstr (A) || isnumeric (A)) C = duration ('Format', B.Format); A = promote (A); elseif (ischar (B) || isstring (B) || iscellstr (B) || isnumeric (B)) C = duration ('Format', A.Format); B = promote (B); elseif (isduration (A) && isduration (B)) C = duration ('Format', A.Format); else error (strcat ("duration.setdiff: set operation not defined", ... " between '%s' and '%s' arrays."), class (A), class (B)); endif [C.Millis, index] = setdiff (A.Millis, B.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} setxor (@var{A}, @var{B}) ## @deftypefnx {duration} {@var{C} =} setxor (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {duration} {@var{C} =} setxor (@var{A}, @var{B}, @dots{}, @var{order}) ## @deftypefnx {duration} {[@var{C}, @var{ixA}, @var{ixB}] =} setxor (@dots{}) ## ## Set exclusive-or of two duration arrays. ## ## @code{@var{C} = setxor (@var{A}, @var{B})} returns the unique common ## values of the duration arrays @var{A} and @var{B}. Either @var{A} or ## @var{B} input arguments can also be a duration string specified as a ## character vector, a string array, or a cell array of character vectors, ## or a numeric array representing 24-hour days. In such case, the input is ## promoted to a duration array prior to calculating the intersection. If ## both @var{A} and @var{B} are row vectors, then @var{C} is also a row ## vector, otherwise @code{intersect} returns a column vector. ## ## @code{@var{C} = setxor (@var{A}, @var{B}, @qcode{'rows'}} returns the ## unique common rows of the duration matrices @var{A} and @var{B}, which ## must have the same number of columns. By default, the rows in ## duration matrix @var{C} are in sorted order. ## ## @code{@dots{} = setxor (@var{A}, @var{B}, @dots{}, @var{order})} also ## specifies the order of the returned unique values. @var{order} can be ## @qcode{'sorted'}, which is the default behavior, or @qcode{'stable'}, in ## which case the unique values are returned in order of appearance. ## ## @code{[@var{C}, @var{ixA}, @var{ixB}] = setxor (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} such that ## @code{@var{C} = @var{A}(@var{ixA})} and ## @code{@var{C} = @var{B}(@var{ixB})}, unless the @qcode{'rows'} optional ## argument is given, in which case @code{@var{C} = @var{A}(@var{ixA},:)} ## and @code{@var{C} = @var{B}(@var{ixB},:)}. ## ## @end deftypefn function [C, ixA, ixB] = setxor (A, B, varargin) ## 'legacy' option is not supported if (any (strcmp ("legacy", varargin))) error ("duration.setxor: 'legacy' option is not supported."); endif if (ischar (A) || isstring (A) || iscellstr (A) || isnumeric (A)) C = duration ('Format', B.Format); A = promote (A); elseif (ischar (B) || isstring (B) || iscellstr (B) || isnumeric (B)) C = duration ('Format', A.Format); B = promote (B); elseif (isduration (A) && isduration (B)) C = duration ('Format', A.Format); else error (strcat ("duration.setxor: set operation not defined", ... " between '%s' and '%s' arrays."), class (A), class (B)); endif [C.Millis, ixA, ixB] = setxor (A.Millis, B.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} union (@var{A}, @var{B}) ## @deftypefnx {duration} {@var{C} =} union (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {duration} {@var{C} =} union (@var{A}, @var{B}, @dots{}, @var{order}) ## @deftypefnx {duration} {[@var{C}, @var{ixA}, @var{ixB}] =} union (@dots{}) ## ## Set union of two duration arrays. ## ## @code{@var{C} = union (@var{A}, @var{B})} returns the unique common ## values of the duration arrays @var{A} and @var{B}. Either @var{A} or ## @var{B} input arguments can also be a duration string specified as a ## character vector, a string array, or a cell array of character vectors, ## or a numeric array representing 24-hour days. In such case, the input is ## promoted to a duration array prior to calculating the intersection. If ## both @var{A} and @var{B} are row vectors, then @var{C} is also a row ## vector, otherwise @code{intersect} returns a column vector. ## ## @code{@var{C} = union (@var{A}, @var{B}, @qcode{'rows'}} returns the ## unique common rows of the duration matrices @var{A} and @var{B}, which ## must have the same number of columns. By default, the rows in ## duration matrix @var{C} are in sorted order. ## ## @code{@dots{} = union (@var{A}, @var{B}, @dots{}, @var{order})} also ## specifies the order of the returned unique values. @var{order} can be ## @qcode{'sorted'}, which is the default behavior, or @qcode{'stable'}, in ## which case the unique values are returned in order of appearance. ## ## @code{[@var{C}, @var{ixA}, @var{ixB}] = union (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} such that ## @code{@var{C} = @var{A}(@var{ixA})} and ## @code{@var{C} = @var{B}(@var{ixB})}, unless the @qcode{'rows'} optional ## argument is given, in which case @code{@var{C} = @var{A}(@var{ixA},:)} ## and @code{@var{C} = @var{B}(@var{ixB},:)}. ## ## @end deftypefn function [C, ixA, ixB] = union (A, B, varargin) ## 'legacy' option is not supported if (any (strcmp ("legacy", varargin))) error ("duration.union: 'legacy' option is not supported."); endif if (ischar (A) || isstring (A) || iscellstr (A) || isnumeric (A)) C = duration ('Format', B.Format); A = promote (A); elseif (ischar (B) || isstring (B) || iscellstr (B) || isnumeric (B)) C = duration ('Format', A.Format); B = promote (B); elseif (isduration (A) && isduration (B)) C = duration ('Format', A.Format); else error (strcat ("duration.union: set operation not defined", ... " between '%s' and '%s' arrays."), class (A), class (B)); endif [C.Millis, ixA, ixB] = union (A.Millis, B.Millis, varargin{:}); endfunction endmethods ################################################################################ ## ** Array Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'cat' 'horzcat' 'vertcat' 'repmat' ## ## 'repelem' 'repelems' 'reshape' 'circshift' ## ## 'permute' 'ipermute' 'transpose' 'ctranspose' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} cat (@var{dim}, @var{A}, @var{B}, @dots{}) ## ## Concatenate duration arrays. ## ## @code{@var{C} = cat (@var{dim}, @var{A}, @var{B}, @dots{})} concatenates ## duration arrays @var{A}, @var{B}, @dots{} along dimension @var{dim}. All ## input arrays must have the same size except along the operating dimension ## @var{dim}. Any of the input arrays may also be string arrays or cell ## arrays of character vectors of compatible size. Additionally, an input ## can be a numeric matrix, which when parsed to the constructor will return ## a duration array of compatible size. ## ## @end deftypefn function out = cat (dim, varargin) ## If any of the input arrays is a calendarDuration array, then convert ## the first input to calendarDuration array and call the corresponding ## method. if (any (cellfun ('iscalendarduration', varargin))) if (isduration (varargin{1})) varargin{1} = calendarDuration (0, 0, 0, varargin{1}); elseif (isnumeric (varargin{1})) if (isempty (varargin{1})) varargout{1} = calendarDuration ([], [], []); else varargout{1} = calendarDuration (0, 0, 0, 24 * varargin{i}, 0, 0); endif else error ("calendarDuration: invalid input to constructor."); endif out = cat (dim, varargin{:}); else args = varargin; [args{:}] = promote (varargin{:}); ## Get format from first duration array argument idx = find (cellfun ('isduration', varargin), 1); fmt = varargin{idx}.Format; out = duration ('Format', fmt); mils = cellfun (@(obj) obj.Millis, args, 'UniformOutput', false); out.Millis = cat (dim, mils{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} horzcat (@var{A}, @var{B}, @dots{}) ## ## Horizontal concatenation of duration arrays. ## ## @code{@var{C} = horzcat (@var{A}, @var{B}, @dots{}} is the equivalent of ## the syntax @code{@var{B} = [@var{A}, @var{B}, @dots{}]} and horizontally ## concatenates the duration arrays @var{A}, @var{B}, @dots{}. All input ## arrays must have the same size except along the second dimension. Any of ## the input arrays may also be string arrays or cell arrays of character ## vectors of compatible size. Additionally, an input can be a numeric ## matrix, which when parsed to the constructor will return a duration array ## of compatible size. ## ## @end deftypefn function out = horzcat (varargin) out = cat (2, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{C} =} vertcat (@var{A}, @var{B}, @dots{}) ## ## Vertical concatenation of duration arrays. ## ## @code{@var{C} = vertcat (@var{A}, @var{B}, @dots{}} is the equivalent of ## the syntax @code{@var{B} = [@var{A}; @var{B}; @dots{}]} and vertically ## concatenates the duration arrays @var{A}, @var{B}, @dots{}. All input ## arrays must have the same size except along the first dimension. All of ## the input arrays may also be string arrays or cell arrays of character ## vectors of compatible size. Additionally, an input can be a numeric ## matrix, which when parsed to the constructor will return a duration array ## of compatible size. ## ## @end deftypefn function out = vertcat (varargin) out = cat (1, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} repmat (@var{A}, @var{n}) ## @deftypefnx {duration} {@var{B} =} repmat (@var{A}, @var{d1}, @dots{}, @var{dN}) ## @deftypefnx {duration} {@var{B} =} repmat (@var{A}, @var{dimvec}) ## ## Repeat copies of a duration array. ## ## @code{@var{B} = repmat (@var{A}, @var{n})} returns a duration array ## @var{B} containing @var{n} copies of the input duration array @var{A} ## along every dimension of @var{A}. ## ## @code{@var{B} = repmat (@var{A}, @var{d1}, @dots{}, @var{dN})} returns an ## array @var{B} containing copies of @var{A} along the dimensions specified ## by the list of scalar integer values @var{d1}, @dots{}, @var{dN}, which ## specify how many copies of @var{A} are made in each dimension. ## ## @code{@var{B} = repmat (@var{A}, @var{dimvec})} is equivalent to the ## previous syntax with @code{@var{dimvec} = [@var{d1}, @dots{}, @var{dN}]}. ## ## @end deftypefn function this = repmat (this, varargin) this.Millis = repmat (this.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} repelem (@var{A}, @var{n}) ## @deftypefnx {duration} {@var{B} =} repelem (@var{A}, @var{d1}, @dots{}, @var{dN}) ## ## Repeat copies of duration array elements. ## ## @code{@var{B} = repelem (@var{A}, @var{n})} returns a duration vector ## @var{B} containing repeated elements of the input @var{A}, which must be ## a duration vector. If @var{n} is a scalar, each element of @var{A} is ## repeated @var{n} times along the non-singleton dimension of @var{A}. If ## @var{n} is a vector, it must have the same elements as @var{A}, in which ## case it specifies the number of times to repeat each corresponding ## element of @var{A}. ## ## @code{@var{B} = repelem (@var{A}, @var{d1}, @dots{}, @var{dN})} returns ## an array @var{B} with each element of @var{A} repeated according to the ## the list of input arguments @code{@var{d1}, @dots{}, @var{dN}} each ## corresponding to a different dimension @code{1:ndims (@var{A})} of the ## input array @var{A}. @var{d1}, @dots{}, @var{dN} must be either scalars ## or vectors with the same length as the corresponding dimension of ## @var{A} containing non-negative integer values specifying the number of ## repetitions of each element along the corresponding dimension. ## ## @end deftypefn function this = repelem (this, varargin) this.Millis = repelem (this.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} repelems (@var{A}, @var{R}) ## ## Construct a vector of repeated duration array. ## ## @code{@var{B} = repelems (@var{A}, @var{R})} returns a duration vector ## @var{B} containing repeated elements of the input @var{A}, which must be ## a duration vector. @var{R} must be a @math{2*N} matrix of integers. ## Entries in the first row of @var{R} correspond to the linear indexing of ## the elements in @var{A} to be repeated. The corresponding entries in the ## second row of @var{R} specify the repeat count of each element. ## ## @end deftypefn function this = repelems (this, R) this.Millis = repelems (this.Millis, R); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} reshape (@var{A}, @var{d1}, @dots{}, @var{dN}) ## @deftypefnx {duration} {@var{B} =} reshape (@var{A}, @dots{}, @qcode{[]}, @dots{}) ## @deftypefnx {duration} {@var{B} =} reshape (@var{A}, @var{dimvec}) ## ## Reshape duration array. ## ## @code{@var{B} = reshape (@var{A}, @var{d1}, @dots{}, @var{dN})} returns a ## duration array @var{B} with specified dimensions @var{d1}, @dots{}, ## @var{dN}, whose elements are taken columnwise from the duration array ## @var{A}. The product of @var{d1}, @dots{}, @var{dN} must equal the total ## number of elements in @var{A}. ## ## @code{@var{B} = reshape (@var{A}, @dots{}, @qcode{[]}, @dots{})} returns ## a duration array @var{B} with one dimension unspecified which is ## calculated automatically so that the product of dimensions in @var{B} ## matches the total elements in @var{A}, which must be divisible the ## product of specified dimensions. An empty matrix @qcode{([])} is used to ## flag the unspecified dimension. ## ## @end deftypefn function this = reshape (this, varargin) this.Millis = reshape (this.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} circshift (@var{A}, @var{n}) ## @deftypefnx {duration} {@var{B} =} circshift (@var{A}, @var{n}, @var{dim}) ## ## Circularly shift the elements in a duration array. ## ## @code{@var{B} = circshift (@var{A}, @var{n})} circularly shifts the ## elements of the duration array @var{A} according to @var{n}. If @var{n} ## is a nonzero integer scalar, then the elements of @var{A} are shifted by ## @var{n} elements along the first non-singleton dimension of @var{A}. If ## @var{n} is a vector, it must not be longer that the number of dimensions ## of @var{A} with each value of @var{n} corresponding to a dimension in ## @var{A}. The sign of the value(s) in @var{n} specify the direction in ## the elements of @var{A} are shifted. ## ## @code{@var{B} = circshift (@var{A}, @var{n}, @var{dim})} circularly ## shifts the elements of the duration array @var{A} along the dimension ## specified by @var{dim}. In this case, @var{n} must be a scalar integer ## value. ## ## @end deftypefn function this = circshift (this, varargin) this.Millis = circshift (this.Millis, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} permute (@var{A}, @var{dims}) ## ## Generalized transpose for a duration N-D array. ## ## @code{@var{B} = permute (@var{A}, @var{dims})} returns the generalized ## transpose of the duration array @var{A} by rearranging its dimensions ## according to the permutation vector specified in @var{dims}. ## ## @var{dims} must index all the dimensions @code{1:ndims (@var{A})} of the ## input array @var{A}, in any order, but only once. The @var{N}th ## dimension of @var{A} gets remapped to the dimension in @var{B} specified ## by @code{@var{dims}(@var{N})}. ## ## @end deftypefn function this = permute (this, order) this.Millis = permute (this.Millis, order); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{A} =} ipermute (@var{B}, @var{dims}) ## ## Inverse of the generalized transpose for a duration N-D array. ## ## @code{@var{A} = ipermute (@var{B}, @var{dims})} returns the inverse of ## the generalized transpose performed by the @code{permute} function. The ## expression @code{ipermute (permute (@var{A}, @var{dims}), @var{dims})} ## returns the original array @var{A}. ## ## @var{dims} must index all the dimensions @code{1:ndims (@var{B})} of the ## input array @var{B}, in any order, but only once. The dimension of ## @var{B} specified in @code{@var{dims}(@var{N})} gets remapped to the ## @var{N}th dimension of @var{A}. ## ## @end deftypefn function this = ipermute (this, order) this.Millis = ipermute (this.Millis, order); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} transpose (@var{A}) ## ## Transpose a duration matrix. ## ## @code{@var{B} = transpose (@var{A})} is the equivalent of the syntax ## @code{@var{B} = @var{A}.'} and returns the transpose of the duration ## matrix @var{A}. ## ## @end deftypefn function this = transpose (this) this.Millis = transpose (this.Millis); endfunction ## -*- texinfo -*- ## @deftypefn {duration} {@var{B} =} ctranspose (@var{A}) ## ## Transpose a duration matrix. ## ## @code{@var{B} = ctranspose (@var{A})} is the equivalent of the syntax ## @code{@var{B} = @var{A}'} and returns the transpose of the duration ## matrix @var{A}. For duration arrays, @code{ctranspose} is identical to ## @code{transpose}. ## ## @end deftypefn function this = ctranspose (this) this.Millis = ctranspose (this.Millis); endfunction endmethods ################################################################################ ## ** Reference and Assignment Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'end' 'subsref' 'subsasgn' ## ## ## ################################################################################ methods (Hidden) ## Overload 'end' keyword function last_index = end (this, end_dim, ndim_obj) lastdim = ndims (this); if (end_dim == ndim_obj && ndim_obj == 1) last_index = prod (size (this)); elseif (end_dim == ndim_obj && end_dim < lastdim) last_index = prod (size (this)(end_dim:lastdim)); else last_index = size (this, end_dim); endif endfunction ## Class specific subscripted reference function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case '()' out = this; out.Millis = this.Millis(s.subs{:}); case '{}' error (strcat ("duration.subsref: '{}' invalid indexing", ... " for referencing values. Use '()' instead.")); case '.' ## A field name may be given as a string scalar, as in MATLAB. if (isstring (s.subs) && isscalar (s.subs)) s.subs = char (s.subs); endif if (! (ischar (s.subs) && isrow (s.subs))) error (strcat ("duration.subsref: '.' index argument must", ... " be a character vector or a string scalar.")); endif switch (s.subs) case 'Format' out = this.Format; otherwise error ("duration.subsref: unrecognized property: '%s'", s.subs); endswitch endswitch ## Chained references if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction ## Class specific subscripted assignment function this = subsasgn (this, s, val) if (numel (s) > 1) error ("duration.subsasgn: chained subscripts not allowed."); endif switch (s.type) case '()' if (isempty (val)) this.Millis(s.subs{:}) = []; elseif (isa (val, 'missing')) this.Millis(s.subs{:}) = NaN; elseif (iscellstr (val) || ischar (val) || isstring (val)) if (! iscellstr (val)) val = cellstr (val); endif this.Millis(s.subs{:}) = timestrings2millis (val, []); elseif (isnumeric (val)) ## A bare number counts days, as it does everywhere else a ## duration meets one. this.Millis(s.subs{:}) = double (val) * 86400000; elseif (isa (val, "duration")) this.Millis(s.subs{:}) = val.Millis; else error (strcat ("duration.subsasgn: assignment value must", ... " be a duration array, a numeric array of", ... " days, or a text represention of durations.")); endif case '{}' error (strcat ("duration.subsasgn: '{}' invalid indexing", ... " for assigning values. Use '()' instead.")); case '.' ## A field name may be given as a string scalar, as in MATLAB. if (isstring (s.subs) && isscalar (s.subs)) s.subs = char (s.subs); endif if (! (ischar (s.subs) && isrow (s.subs))) error (strcat ("duration.subsasgn: '.' index argument must", ... " be a character vector or a string scalar.")); endif switch (s.subs) case 'Format' ## Convert string to character vector if necessary if (isstring (val)) if (! isscalar (val)) error (strcat ("duration.subsasgn: 'Format' must be a", ... " character vector or a string scalar.")); endif val = char (val); elseif (! (ischar (val) && isrow (val))) error (strcat ("duration.subsasgn: 'Format' must be a", ... " character vector or a string scalar.")); endif errmsg = checkFormatString (val); if (! isempty (errmsg)) error ("duration.subsasgn: %s", errmsg); endif this.Format = val; otherwise error ("duration.subsasgn: unrecognized property: '%s'", s.subs); endswitch endswitch endfunction endmethods methods (Access = private) ## Return a subset of a duration array function out = subset (this, varargin) out = this; out.Millis = this.Millis(varargin{:}); endfunction endmethods endclassdef ## Promote numeric and string arrays to duration objects function varargout = promote (varargin) for i = 1:numel (varargin) x = varargin{i}; if (isa (x, "duration")) varargout{i} = x; elseif (isa (x, "missing")) ## A missing value becomes an all-NaN duration of the same shape. Only ## its Millis are read back by 'cat', which takes the result's format ## from the first real duration operand, so this filler's own format ## never reaches the result. varargout{i} = days (NaN (size (x))); elseif (isnumeric (x)) varargout{i} = days (x); elseif (iscellstr (x) || ischar (x) || isa (x, "string")) varargout{i} = duration (x); else error ("duration: invalid input to constructor."); endif endfor endfunction ## Parse H, M, S, and MS numeric inputs into days function [err, millis] = hms2millis (H, MI, S, MS = 0) err = 0; if (! isscalar (H) || ! isscalar (MI) || ! isscalar (S) || ! isscalar (MS)) [err, H, MI, S, MS] = common_size (H, MI, S, MS); if (err > 0) millis = NaN; return endif endif H = double (H); MI = double (MI); S = double (S); MS = double (MS); millis = H * 3600000 + MI * 60000 + S * 1000 + MS; endfunction ## Parse TimeString inputs into days function millis = timestrings2millis (TS, inputFormat) millis = NaN (size (TS)); ## Find default format (either 'dd:hh:mm:ss' or 'hh:mm:ss') from 1st element if (isempty (inputFormat)) str1 = TS{1}; nCols = numel (find (str1 == ':')); nDots = numel (find (str1 == '.')); if (nDots > 1) error ("duration: could not recognize time string format of '%s'.", str1); endif if (nCols > 3 || nCols < 1) error ("duration: could not recognize time string format of '%s'.", str1); endif if (nCols == 1) error ("duration: time string format is ambiguous."); endif if (nDots) cstr = strsplit (str1, '.'); if (! isempty (cstr{2}) && isnan (str2double (cstr{2}))) error (strcat ("duration: could not recognize time string", ... " format of '%s'."), str1); endif cstr = strsplit (cstr{1}, ':'); if (numel (cstr) != nCols + 1) error (strcat ("duration: could not recognize time string", ... " format of '%s'."), str1); endif for i = 1:nCols + 1 if (isnan (str2double (cstr{i}))) error (strcat ("duration: could not recognize time string", ... " format of '%s'."), str1); endif endfor endif else nCols = numel (find (inputFormat == ':')); nDots = numel (find (inputFormat == '.')); endif ## Process all elements according to inputFormat (unrecognized return NaN) for i = 1:numel (TS) str1 = TS{i}; pnd_sign = 1; if (numel (find (str1 == '.'))) cstr = strsplit (str1, '.'); if (isempty (cstr{2})) MS = 0; else MSnumber = str2double (cstr{2}); MSdigits = 10 ^ (numel (cstr{2}) - 3); MS = MSnumber / MSdigits; endif cstr = strsplit (cstr{1}, ':'); if (numel (cstr) != nCols + 1) D = H = MI = S = NaN; elseif (nCols == 1) # 'mm:ss' only D = 0; H = 0; MI = str2double (cstr{1}); if (MI < 0) pnd_sign = -1; MI = -MI; endif S = str2double (cstr{2}); if (S >= 60) S = NaN; endif elseif (nCols == 2) # 'hh:mm:ss' D = 0; H = str2double (cstr{1}); if (H < 0) pnd_sign = -1; H = -H; endif MI = str2double (cstr{2}); S = str2double (cstr{3}); if (MI >= 60 || S >= 60) MI = S = NaN; endif else # 'dd:hh:mm:ss' D = str2double (cstr{1}); if (D < 0) pnd_sign = -1; D = -D; endif H = str2double (cstr{2}); MI = str2double (cstr{3}); S = str2double (cstr{4}); if (H >= 24 || MI >= 60 || S >= 60) H = MI = S = NaN; endif endif else cstr = strsplit (str1, ':'); if (numel (cstr) != nCols + 1) D = H = MI = S = MS = NaN; elseif (nCols == 1) # either 'mm:ss' or 'hh:mm' if (strcmp (inputFormat, 'mm:ss')) D = 0; H = 0; MI = str2double (cstr{1}); if (MI < 0) pnd_sign = -1; MI = -MI; endif S = str2double (cstr{2}); MS = 0; else # 'hh:mm' D = 0; H = str2double (cstr{1}); if (H < 0) pnd_sign = -1; H = -H; endif MI = str2double (cstr{2}); if (MI >= 60) MI = NaN; endif S = 0; MS = 0; endif elseif (nCols == 2) # 'hh:mm:ss' D = 0; H = str2double (cstr{1}); if (H < 0) pnd_sign = -1; H = -H; endif MI = str2double (cstr{2}); S = str2double (cstr{3}); if (MI >= 60 || S >= 60) MI = S = NaN; endif MS = 0; else # 'dd:hh:mm:ss' D = str2double (cstr{1}); if (D < 0) pnd_sign = -1; D = -D; endif H = str2double (cstr{2}); MI = str2double (cstr{3}); S = str2double (cstr{4}); if (H >= 24 || MI >= 60 || S >= 60) H = MI = S = NaN; endif MS = 0; endif endif millis(i) = D * 86400000 + H * 3600000 + MI * 60000 + S * 1000 + MS; millis(i) *= pnd_sign; endfor endfunction ## Check 'Format' string function errmsg = checkFormatString (Format) errmsg = ""; Format = strsplit (Format, '.')'; validFmt = {'y', 'd', 'h', 'm', 's', 'dd:hh:mm:ss', 'hh:mm:ss', ... 'mm:ss', 'hh:mm'}; foundFmt = ismember (validFmt, Format(1)); if (! any (foundFmt) || numel (Format) > 2) errmsg = "invalid display format."; endif if (any (foundFmt([1:5])) && numel (Format) > 1) errmsg = "invalid display format."; endif if (foundFmt(9) && numel (Format) > 1) errmsg = "'hh:mm' display format cannot indicate fractional second digits."; endif if (numel (Format) == 2) if (any (char (Format(2)) != 'S')) errmsg = "invalid display format for fractional second digits."; endif if (numel (Format{2}) > 9) errmsg = "more than nine fractional second digits in display format."; endif endif endfunction ## Check 'InputFormat' string function errmsg = checkInputFormatString (inputFormat) errmsg = ''; inputFormat = strsplit (inputFormat, '.'); validInFmt = {'dd:hh:mm:ss','hh:mm:ss','mm:ss','hh:mm'}; foundInFmt = ismember (validInFmt, inputFormat(1)); if (! any (foundInFmt) || numel (inputFormat) > 2) errmsg = "invalid 'InputFormat'."; endif if (foundInFmt(4) && numel (inputFormat) > 1) errmsg = "'hh:mm' input format cannot indicate fractional second digits."; endif if (numel (inputFormat) == 2) if (any (char (inputFormat(2)) != 'S')) errmsg = "invalid 'InputFormat' for fractional second digits."; endif if (numel (inputFormat{2}) > 9) errmsg = "more than nine fractional second digits in 'InputFormat'."; endif endif endfunction ## Bin edges in seconds for a duration, from a count, a width or a unit name function [ev, isCount] = durbinedges (xv, arg2, scope) isCount = false; xf = xv(isfinite (xv)); if (isa (arg2, 'duration')) dv = milliseconds (arg2); if (isscalar (dv)) ev = unitbinedges (xf, dv, scope); else ev = dv(:).'; endif elseif (ischar (arg2) || isa (arg2, 'string')) ev = unitbinedges (xf, unitseconds (char (arg2), scope), scope); elseif (isnumeric (arg2) && isscalar (arg2) && ! islogical (arg2)) if (! isreal (arg2) || ! isfinite (arg2) || arg2 < 1 || fix (arg2) != arg2) error ("%s: N must be a real positive integer.", scope); endif isCount = true; ## With no finite value to bin, MATLAB places the bins as though the data ## were a single zero, so the constant-data rule applies at zero rather ## than the edges running 0, 1, ... N. if (isempty (xf)) lo = 0; hi = 0; else lo = min (xf); hi = max (xf); endif ev = gridbinedges (lo, hi, double (arg2), 0.5); elseif (isnumeric (arg2)) error (strcat (scope, ": numeric bin edges are not accepted for a", ... " duration; give a duration vector.")); else error (strcat (scope, ": the second argument must be a duration, a", ... " bin count, or a unit name.")); endif endfunction ## Edges one UNIT wide, aligned to multiples of the unit, covering the data function ev = unitbinedges (xf, unit, scope) if (! (unit > 0) || ! isfinite (unit)) error (strcat (scope, ": a bin width must be positive and finite.")); endif if (isempty (xf)) ev = [0, unit]; return; endif left = unit * floor (min (xf) / unit); nbins = max (1, ceil ((max (xf) - left) / unit)); ev = left + (0:nbins) * unit; endfunction ## Length in milliseconds of a named time unit. The binning path works in the ## stored unit throughout, as MATLAB's does; computing it in seconds instead ## puts a rounding into every width that is not a whole number of seconds. function s = unitseconds (name, scope) yr = 31556952000; switch (lower (name)) case 'second' s = 1000; case 'minute' s = 60000; case 'hour' s = 3600000; case 'day' s = 86400000; case 'week' s = 604800000; case 'month' s = yr / 12; case 'quarter' s = yr / 4; case 'year' s = yr; case 'decade' s = 10 * yr; case 'century' s = 100 * yr; otherwise error (strcat (scope, ": UNIT must be one of 'second', 'minute',", ... " 'hour', 'day', 'week', 'month', 'quarter', 'year',", ... " 'decade', or 'century'.")); endswitch endfunction ## Translate duration-valued histcounts options into seconds, resolving any ## bin count or unit BinMethod into explicit edges so the grid rule is used function args = durhistargs (args, xv, scope) xf = xv(isfinite (xv)); ## BinLimits decides the range everything else is derived from lim = []; for k = 1:numel (args) - 1 if (istextscalar (args{k}) && strcmpi (char (args{k}), 'BinLimits')) v = args{k+1}; if (isa (v, 'duration')) v = milliseconds (v); endif if (isnumeric (v) && numel (v) == 2) lim = double (v(:)).'; endif endif endfor if (! isempty (lim)) xf = xf(xf >= lim(1) & xf <= lim(2)); lo = lim(1); hi = lim(2); elseif (! isempty (xf)) lo = min (xf); hi = max (xf); else lo = []; hi = []; endif ## A leading positional argument is a bin count or a set of edges. Either ## way it is resolved here, and the option loop below starts past it. hasSpec = false; k = 1; if (! isempty (args) && ! istextscalar (args{1})) if (isa (args{1}, 'duration')) if (isscalar (args{1})) ## A lone duration reads as a bin WIDTH, which this position does not ## take, in MATLAB or here. Passed on it would become a bin COUNT of ## however many milliseconds it holds: 'days (2)' asked for 172800000 ## bins and the array to hold them. error (strcat ("%s: a bin width must be given as 'BinWidth'; the", ... " second argument is a bin count or a vector of bin", ... " edges."), scope); endif dv = milliseconds (args{1}); args{1} = dv(:).'; hasSpec = true; k = 2; elseif (isnumeric (args{1}) && isscalar (args{1}) && ! islogical (args{1})) args = [{'BinEdges', countbinedges(lo, hi, args{1}, scope)}, args(2:end)]; hasSpec = true; k = 3; else k = 2; endif endif while (k < numel (args)) if (! istextscalar (args{k})) k += 2; continue; endif switch (lower (char (args{k}))) case 'numbins' args{k} = 'BinEdges'; args{k+1} = countbinedges (lo, hi, args{k+1}, scope); hasSpec = true; case {'binwidth', 'binedges'} hasSpec = true; if (isa (args{k+1}, 'duration')) dv = milliseconds (args{k+1}); args{k+1} = dv(:).'; elseif (isnumeric (args{k+1})) error ("%s: '%s' must be a duration.", scope, char (args{k})); endif ## A width with no finite data to place it against: the numeric ## function answers [0 1] and ignores the width entirely, where MATLAB ## gives a single bin one width across. Resolving it to explicit edges ## here is the same thing 'BinMethod' with a unit name already does. if (strcmpi (char (args{k}), 'BinWidth') && isempty (xf) && isempty (lim) && isnumeric (args{k+1})) args{k} = 'BinEdges'; args{k+1} = unitbinedges (xf, args{k+1}, scope); endif case 'binlimits' if (isa (args{k+1}, 'duration')) dv = milliseconds (args{k+1}); args{k+1} = dv(:).'; elseif (isnumeric (args{k+1})) error ("%s: '%s' must be a duration.", scope, char (args{k})); endif case 'binmethod' if (istextscalar (args{k+1})) name = lower (char (args{k+1})); if (strcmp (name, 'integers')) error (strcat (scope, ": 'integers' is not a valid 'BinMethod'", ... " for a duration.")); elseif (any (strcmp (name, {'auto', 'scott', 'fd', 'sturges', ... 'sqrt'}))) args{k} = 'BinEdges'; args{k+1} = countbinedges (lo, hi, methodbins (xf, lo, hi, ... name), ... scope); else args{k} = 'BinEdges'; args{k+1} = unitbinedges (xf, unitseconds (name, scope), scope); endif endif hasSpec = true; case 'normalization' if (istextscalar (args{k+1}) && any (strcmpi (char (args{k+1}), {'countdensity', 'pdf'}))) error (strcat (scope, ": '%s' is not a valid 'Normalization' for", ... " a duration; a density per unit time has no", ... " meaning."), lower (char (args{k+1}))); endif endswitch k += 2; endwhile ## Nothing said how to bin: use the automatic rule, which is a bin count from ## Scott's rule fed through the same grid edges as every other path if (! hasSpec) args = [args, {'BinEdges', countbinedges(lo, hi, ... methodbins (xf, lo, hi, 'auto'), scope)}]; endif endfunction ## Bin count for one of the automatic bin-selection rules function nbins = methodbins (xf, lo, hi, method) n = numel (xf); if (n < 2 || isempty (lo) || hi == lo) nbins = 1; return; endif switch (method) case 'sturges' nbins = ceil (1 + log2 (n)); return; case 'sqrt' nbins = ceil (sqrt (n)); return; case 'fd' q = quantile (xf(:), [0.25, 0.75], 1, 5); rawWidth = 2 * (q(2) - q(1)) * n ^ (-1/3); otherwise rawWidth = 3.5 * std (xf(:)) * n ^ (-1/3); endswitch if (! (rawWidth > 0)) nbins = 1; else nbins = max (1, ceil ((hi - lo) / rawWidth)); endif endfunction ## Bin edges for a requested bin count, snapped to whole time units. LO, HI and ## the result all count seconds; NBINS is the number of bins asked for and TICK ## the half-width used when the data are constant. ## ## Taking G as a whole time unit, everything runs on INTEGER counts of G: ## ## nLo = floor (LO / G) ## spanU = ceil (HI / G) - nLo % note ceil against floor ## w = ceil (spanU / NBINS) ## c = ceil ((NBINS * w - spanU) / 2) ## L = G * (nLo - c) ## ## with G the largest unit for which 'spanU / NBINS > 4/3' and the width still ## needs every bin, that is 'ceil (spanU / w) == NBINS'. ## ## **The left edge is not a rounding of the centred value.** It is the unit ## holding LO, moved down by a whole number of units. Every attempt to write it ## as a rounding of 'LO - (NBINS*w - range)/2' fails, because two cases can ## share that value and be placed a unit apart -- 'xmin = 0.25, range = 5' and ## 'xmin = 0.75, range = 10' both give -0.25, and MATLAB answers 0 and -1. ## Sweeping XMIN through a unit settles it: the answer is constant across the ## whole interval and steps once, which is a floor and not a rounding. ## ## **The ceiling in SPANU against the floor in nLo is the load-bearing part.** ## Data ending part way through a unit claims the whole of it, so the same ## elapsed range gives a different width depending on where it starts: ## 'seconds ([0 6])' is SPANU 6 and a 2-second width, 'seconds ([0.125 6.125])' ## is SPANU 7 and a 3-second width. ## ## **The threshold is on SPANU, not on the elapsed range**, and at exactly 4/3 ## the numeric rule wins. That is what separates 'seconds ([0 4])' (SPANU 4, ## ratio 4/3, numeric) from 'seconds ([0.125 4.125])' (SPANU 5, gridded), which ## no threshold on the elapsed range can do since both have the same range. ## 'binthr_probe.m' pins it at two scales. ## ## **The ladder stops at one day.** There is no week and no month -- a week ## grid reproduces some spans and gets others wrong, and every span that looked ## like months is reproduced by days. There is no year either: a year rung ## makes the acceptance test vacuous at NBINS == 1, where it returned a bin one ## year wide for an hour of data. ## ## When no unit clears the threshold the plain numeric rule in '__binedges__' ## takes over. That is the terminating case at the bottom of the ladder rather ## than a separate regime: below 4/3 seconds there is no coarser unit to snap ## to. function ev = gridbinedges (lo, hi, nbins, tick = 0.5) ## Constant data: an interval of one tick centred on the value. The offsets ## are formed first and added once, so only one rounding lands on LO. if (lo == hi) ev = lo + (-tick + (0:nbins) * (2 * tick / nbins)); return; endif ## Everything is integer arithmetic on unit indices. SPANU is the count of ## whole units the data touches -- note the ceiling at the top against the ## floor at the bottom, so data ending part way through a unit still claims ## the whole of it. That asymmetry is the whole rule: 6 seconds of data ## starting on a second gives SPANU 6 and a 2-second width, and the same 6 ## seconds starting an eighth of a second later gives SPANU 7 and a 3-second ## width. for u = [86400000, 3600000, 60000, 1000] nLo = floor (lo / u); spanU = ceil (hi / u) - nLo; if (spanU < 1) continue; endif ## The threshold is on SPANU, not on the elapsed range. At exactly 4/3 the ## numeric rule wins, which is what separates a 4-second range starting on ## a second (SPANU 4, ratio 4/3, numeric) from the same range starting an ## eighth later (SPANU 5, ratio 5/3, gridded). if (spanU / nbins <= 4/3) continue; endif width = ceil (spanU / nbins); ## Keep this unit while the bins overshoot the data by less than twice one ## width less a unit; past that the finer decimal rule wins. The bound ## rises with the width, which is why a three-unit width tolerates an ## excess of three and a five-unit width tolerates seven. ## ## Measured, not derived: this separates all 712 captured cases that reach ## a unit -- 583 gridded by R2024a and 129 sent to the decimal rule -- with ## no exceptions either way. It sits one unit tighter than "the first bin ## still holds data" (which would be 'excess <= 2*width - 2' and wrongly ## grids 38 of the 129), and why that last unit is withheld is not known. excess = nbins * width - spanU; if (excess >= 2 * (width - 1)) continue; endif ## Centre the excess, rounding a half unit outwards. c = ceil (excess / 2); ev = u * ((nLo - c) + (0:nbins) * width); return; endfor ## Finer than the threshold allows: the plain numeric rule takes over ev = __binedges__ (lo, hi, nbins); endfunction ## Edges for a requested bin count, over a range given in seconds function ev = countbinedges (lo, hi, nbins, scope) if (! isnumeric (nbins) || ! isscalar (nbins) || ! isreal (nbins) || ! isfinite (nbins) || nbins < 1 || fix (nbins) != nbins) error (strcat (scope, ": 'NumBins' must be a real, finite, positive,", ... " integer value.")); endif ## No finite value to bin: the bins go where they would for a single zero. if (isempty (lo)) lo = 0; hi = 0; endif ev = gridbinedges (lo, hi, double (nbins), 0.5); endfunction ## True for a character vector or a scalar string function tf = istextscalar (x) tf = (ischar (x) && isrow (x)) || (isa (x, 'string') && isscalar (x)); endfunction ## 'pkg test' reads the BISTs from the file it is testing, so a class ## whose suite lives in inst/tests/ reads as carrying none. This block ## answers for it and checks nothing itself. %!test %! ## The suite for this class is inst/tests/duration.m-tst. pr0m1th3as-datatypes-9450b29/inst/eventfilter.m000066400000000000000000000612151527370255000213650ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef eventfilter ## -*- texinfo -*- ## @deftp {datatypes} eventfilter ## ## Subscript into a timetable by what was happening rather than by when. ## ## A utility class that builds a condition against the variables of the ## event table attached to a timetable, and selects the rows the matching ## events cover. The condition picks out @emph{events}; the rows follow ## from them. ## ## @example ## @group ## ef = eventfilter (tt); ## tt(ef.EventLabels == "rain", :) ## @end group ## @end example ## ## An event covers the times from its own onwards and stops short of its ## end, and an event with neither a length nor an end covers only a row at ## exactly its own time. Rows covered by more than one matching event are ## selected once: a filter answers which rows, not how often. ## ## @qcode{Time} names the event's own time, not the row's, so ## @code{ef.Time > d} selects the rows of the events that began after ## @var{d}. ## ## @seealso{eventtable, timerange, withtol, timetable} ## @end deftp properties (SetAccess = private, Hidden) ## The names a condition may be written against varNames ## The variable a comparison is waiting for, empty once one is made pending ## The condition, as a function of the event table it will be applied to condition ## The condition as written, for the display text ## The labels of the bare-label form, empty for every other filter labels endproperties methods (Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif disp (this); endfunction ## Custom display function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif if (isempty (this.condition) && isempty (this.labels)) fprintf (" eventfilter with no constraint\n\n"); else fprintf (" eventfilter with constraint:\n\n %s\n\n", this.text); endif if (! isempty (this.varNames)) fprintf (" VariableNames: %s\n\n", strjoin (this.varNames, ", ")); endif endfunction ## Class specific subscripted reference. Every '.' name is a variable of ## the event table the filter will be applied to, so the filter has no ## readable properties of its own. function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); if (! strcmp (s.type, '.')) error ("eventfilter.subsref: only '.' indexing is supported."); endif name = s.subs; if (isstring (name) && isscalar (name)) name = char (name); endif if (! (ischar (name) && isrow (name))) error (strcat ("eventfilter.subsref: '.' index argument must be a", ... " character vector or a string scalar.")); endif if (! any (strcmp (this.varNames, name))) error (strcat ("eventfilter: no variable named '%s' to filter on;", ... " use one of: %s"), name, ... strjoin (this.varNames, ", ")); endif out = this; out.pending = name; if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {eventfilter} {@var{ef} =} eventfilter (@var{tt}) ## @deftypefnx {eventfilter} {@var{ef} =} eventfilter (@var{eventLabels}) ## ## Create an event filter. ## ## @code{@var{ef} = eventfilter (@var{tt})} creates a filter that may be ## written against the variables of the event table attached to the ## timetable @var{tt}, together with @qcode{Time}, the events' own times. ## @var{tt} must have an event table attached, since the names come from ## it. ## ## @code{@var{ef} = eventfilter (@var{eventLabels})} creates a filter ## that matches the events carrying any of the given labels, without a ## timetable to take the names from. It is a whole condition already and ## needs no comparison written against it. ## ## A filter is used as a row subscript, and selects the rows the matching ## events cover. Conditions combine with @code{&} and @code{|}; ## @code{~} is not supported, an event filter having no complement worth ## the name. ## ## @seealso{eventtable, timerange, timetable} ## @end deftypefn function this = eventfilter (arg) if (nargin != 1) print_usage (); endif this.varNames = {}; this.pending = ''; this.condition = []; this.text = ''; this.labels = []; if (isa (arg, 'timetable')) ev = arg.Properties.Events; if (isempty (ev)) error (strcat ("eventfilter: the timetable has no event table", ... " attached, so there are no event variables to", ... " filter on; assign one to 'Properties.Events'", ... " first.")); endif props = ev.Properties; this.varNames = [props.DimensionNames(1), props.VariableNames]; elseif (ischar (arg) || iscellstr (arg) || isa (arg, 'string') || iscategorical (arg)) ## A character row vector is one label, not a column of letters, ## which is what indexing it would make of it. if (iscategorical (arg) || isa (arg, 'string')) lab = arg(:); else lab = cellstr (arg); lab = lab(:); endif this.labels = lab; this.text = sprintf ("event labels in %s", labelstr (arg)); else error (strcat ("eventfilter: input must be a timetable carrying", ... " an event table, or a list of event labels.")); endif endfunction ## -*- texinfo -*- ## @deftypefn {eventfilter} {@var{names} =} properties (@var{ef}) ## ## Return the names a condition may be written against. ## ## They are the variables of the event table the filter came from, ## together with @qcode{Time}. A filter made from a list of labels ## carries none, having been given no event table to read them from. ## ## @end deftypefn function names = properties (this) names = this.varNames(:); endfunction ## -*- texinfo -*- ## @deftypefn {eventfilter} {@var{ix} =} rowIndices (@var{ef}, @var{tt}) ## ## Return the positions of the rows of @var{tt} the filter selects. ## ## @end deftypefn function ix = rowIndices (this, tt) ev = filterEvents (this, tt); mask = eventMask (this, ev); ix = find (coveredRows (ev, mask, tt.Properties.RowTimes)); endfunction ## -*- texinfo -*- ## @deftypefn {eventfilter} {@var{t} =} eventTimes (@var{ef}, @var{tt}) ## ## Return the times of the events of @var{tt} the filter matches. ## ## The times come back in the order the event table holds them. It is ## what a @code{timerange} reads when an event filter is given as one of ## its bounds. ## ## @end deftypefn function t = eventTimes (this, tt) ev = filterEvents (this, tt); mask = eventMask (this, ev); rt = ev.Properties.RowTimes; t = rt(mask); endfunction endmethods methods (Access = private) ## The event table the filter is to be applied against, with the two ## refusals a filter can meet on its way to one. function ev = filterEvents (this, tt) if (isa (tt, 'eventtable')) error (strcat ("eventfilter: an event filter selects the rows of a", ... " timetable that its events cover, so it cannot", ... " subscript an event table; attach the event table", ... " to a timetable and subscript that.")); endif if (! isa (tt, 'timetable')) error ("eventfilter: an event filter subscripts a timetable."); endif ev = tt.Properties.Events; if (isempty (ev)) error (strcat ("eventfilter: the timetable has no event table", ... " attached, so no event covers any of its rows;", ... " assign one to 'Properties.Events' first.")); endif endfunction ## Which events the condition picks out. function mask = eventMask (this, ev) if (! isempty (this.labels)) lv = ev.Properties.EventLabelsVariable; if (isempty (lv)) error (strcat ("eventfilter: the event table does not say which", ... " of its variables holds the labels, so a filter", ... " written against labels cannot be applied to it.")); endif ## Both sides are read as text so that a label given as a string ## matches a categorical labels variable and the other way round. col = ev.(lv); mask = ismember (astext (col), astext (this.labels)); return endif if (isempty (this.condition)) error (strcat ("eventfilter: the filter carries no condition;", ... " compare one of its variables with a value first.")); endif mask = this.condition (ev); mask = mask(:); if (! (islogical (mask) && numel (mask) == height (ev))) error (strcat ("eventfilter: the condition did not yield one true", ... " or false value for each event.")); endif endfunction endmethods methods (Static, Hidden) ## Build the condition for one comparison, as 'rowfilter' does, except ## that the condition reads an event table rather than the timetable the ## filter will subscript. function out = compare (a, b, sym, fcn, mirror) if (isa (a, 'eventfilter') && isa (b, 'eventfilter')) error (strcat ("eventfilter: '%s' compares a variable with a", ... " value, not two filters."), sym); endif swapped = ! isa (a, 'eventfilter'); if (swapped) ef = b; val = a; else ef = a; val = b; endif if (isempty (ef.pending)) error (strcat ("eventfilter: '%s' needs a variable to compare;", ... " name one with '.' first."), sym); endif name = ef.pending; out = ef; out.pending = ''; if (swapped) out.condition = @(e) cmpValues (fcn, sym, val, e.(name)); out.text = sprintf ("%s %s %s", name, mirror, labelstr (val)); else out.condition = @(e) cmpValues (fcn, sym, e.(name), val); out.text = sprintf ("%s %s %s", name, sym, labelstr (val)); endif endfunction function [ef, ca, cb] = combination (a, b, op) if (! (isa (a, 'eventfilter') && isa (b, 'eventfilter'))) error ("eventfilter: '%s' combines two filters.", op); endif if (isempty (a.condition) || isempty (b.condition)) error (strcat ("eventfilter: '%s' combines two conditions; compare", ... " a variable with a value on both sides first."), op); endif ef = a; ef.varNames = unique ([a.varNames, b.varNames], 'stable'); ca = a.condition; cb = b.condition; endfunction endmethods methods (Hidden) function out = eq (a, b) out = eventfilter.compare (a, b, '==', @eq, '=='); endfunction function out = ne (a, b) out = eventfilter.compare (a, b, '~=', @ne, '~='); endfunction function out = lt (a, b) out = eventfilter.compare (a, b, '<', @lt, '>'); endfunction function out = le (a, b) out = eventfilter.compare (a, b, '<=', @le, '>='); endfunction function out = gt (a, b) out = eventfilter.compare (a, b, '>', @gt, '<'); endfunction function out = ge (a, b) out = eventfilter.compare (a, b, '>=', @ge, '<='); endfunction function out = and (a, b) [out, ca, cb] = eventfilter.combination (a, b, '&'); out.condition = @(e) ca(e) & cb(e); out.text = sprintf ("%s & %s", a.text, b.text); endfunction function out = or (a, b) [out, ca, cb] = eventfilter.combination (a, b, '|'); out.condition = @(e) ca(e) | cb(e); out.text = sprintf ("%s | %s", a.text, b.text); endfunction function out = not (a) error (strcat ("eventfilter: an event filter does not support the", ... " negation operator '~'; write the condition the", ... " other way round.")); endfunction endmethods endclassdef ## Which rows of ROWTIMES the events MASK picks out cover. An interval runs ## from the event's own time and stops short of its end, so an event of zero ## length covers nothing; an event with neither a length nor an end covers a ## row at exactly its own time. Rows covered more than once are selected ## once. function keep = coveredRows (ev, mask, rowTimes) props = ev.Properties; evTimes = props.RowTimes; if (! isempty (props.EventLengthsVariable)) evEnds = evTimes + ev.(props.EventLengthsVariable); elseif (! isempty (props.EventEndsVariable)) evEnds = ev.(props.EventEndsVariable); else evEnds = []; endif keep = false (numel (rowTimes), 1); for k = find (mask(:)') if (isempty (evEnds)) keep = keep | (rowTimes == evTimes(k)); else keep = keep | (rowTimes >= evTimes(k) & rowTimes < evEnds(k)); endif endfor endfunction ## One comparison of a variable with a value. Octave has no '==' for a cell ## array against text, and a list of names is held here as a cellstr, so an ## equality against one is answered by comparing the text itself; every other ## comparison is the operator's own. function out = cmpValues (fcn, sym, a, b) if (! (any (strcmp (sym, {'==', '~='})) && (iscellstr (a) || iscellstr (b)))) out = fcn (a, b); return endif if (iscellstr (a)) col = a; val = b; else col = b; val = a; endif if (! (ischar (val) || iscellstr (val) || isa (val, 'string') || iscategorical (val))) error (strcat ("eventfilter: '%s' compares a list of names with text,", ... " not with a %s."), sym, class (val)); endif v = astext (val); if (isscalar (v)) tf = strcmp (col(:), v{1}); else tf = strcmp (col(:), v(:)); endif if (strcmp (sym, '~=')) tf = ! tf; endif out = tf(:); endfunction ## Any text, as a cell array of character vectors, for comparing by value. function c = astext (x) if (iscellstr (x)) c = x(:); elseif (ischar (x)) c = cellstr (x); c = c(:); else c = cellstr (string (x(:))); c = c(:); endif endfunction ## A value rendered for the display of a condition. function txt = labelstr (val) if (ischar (val)) txt = sprintf ("'%s'", val); elseif (isa (val, 'string') || iscategorical (val) || iscellstr (val)) c = cellstr (string (val(:)')); txt = strjoin (strcat ('"', c, '"'), ", "); elseif (isnumeric (val) && isscalar (val)) txt = num2str (val); else txt = sprintf ("<%s>", class (val)); endif endfunction %!shared TT, ET %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! ET = eventtable (t([3 6]), 'EventLabels', string ({'rain'; 'snow'}), ... %! 'EventLengths', hours ([2; 1])); %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! TT.Properties.Events = ET; ## Test a filter is built from a timetable carrying events %!assert (class (eventfilter (TT)), 'eventfilter') ## Test a filter is built from a list of labels %!assert (class (eventfilter ("rain")), 'eventfilter') ## Test the names a condition may be written against %!test %! assert_equal (properties (eventfilter (TT))', ... %! {'Time', 'EventLabels', 'EventLengths'}); ## Test a comparison yields another filter rather than a logical %!test %! EF = eventfilter (TT); %! assert_equal (class (EF.EventLabels == "rain"), 'eventfilter'); ## Test an interval event covers its own time and stops short of its end %!test %! EF = eventfilter (TT); %! assert_equal (TT(EF.EventLabels == "rain", :).v', [3, 4]); ## Test a one-hour event covers only its own row %!test %! EF = eventfilter (TT); %! assert_equal (TT(EF.EventLabels == "snow", :).v', 6); ## Test an event given an end covers the same rows as one given a length %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! A = timetable (t, (1:8)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (t(3), 'EventLabels', "rain", ... %! 'EventEnds', t(5)); %! EF = eventfilter (A); %! assert_equal (A(EF.EventLabels == "rain", :).v', [3, 4]); ## Test an instantaneous event covers only a row at its own time %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! A = timetable (t, (1:8)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (t([3 6]), 'EventLabels', ... %! string ({'rain'; 'snow'})); %! EF = eventfilter (A); %! assert_equal (A(EF.EventLabels == "rain", :).v', 3); ## Test an event of zero length covers nothing %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! A = timetable (t, (1:8)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (t([3 6]), 'EventLabels', ... %! string ({'rain'; 'snow'}), ... %! 'EventLengths', hours ([0; 0])); %! EF = eventfilter (A); %! assert_equal (height (A(EF.EventLabels == "rain", :)), 0); ## Test rows covered by two matching events are selected once %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! A = timetable (t, (1:8)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (t([3 4]), 'EventLabels', ... %! string ({'rain'; 'rain'}), ... %! 'EventLengths', hours ([3; 3])); %! EF = eventfilter (A); %! assert_equal (A(EF.EventLabels == "rain", :).v', [3, 4, 5, 6]); ## Test a condition matching no event selects no row %!test %! EF = eventfilter (TT); %! assert_equal (height (TT(EF.EventLabels == "nothing", :)), 0); ## Test '~=' selects the events whose label differs %!test %! EF = eventfilter (TT); %! assert_equal (TT(EF.EventLabels ~= "rain", :).v', 6); ## Test '&' combines two conditions %!test %! EF = eventfilter (TT); %! f = EF.EventLabels == "rain" & EF.EventLengths < hours (3); %! assert_equal (TT(f, :).v', [3, 4]); ## Test '|' combines two conditions %!test %! EF = eventfilter (TT); %! f = EF.EventLabels == "rain" | EF.EventLabels == "snow"; %! assert_equal (TT(f, :).v', [3, 4, 6]); ## Test 'Time' compares the event's time and not the row's %!test %! EF = eventfilter (TT); %! rt = TT.Properties.RowTimes; %! assert_equal (TT(EF.Time > rt(3), :).v', 6); %! assert_equal (TT(EF.Time <= rt(3), :).v', [3, 4]); ## Test a comparison on the lengths variable %!test %! EF = eventfilter (TT); %! assert_equal (TT(EF.EventLengths >= hours (2), :).v', [3, 4]); ## Test a comparison on an event data variable %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! A = timetable (t, (1:8)', 'VariableNames', {'v'}); %! E = addvars (ET, [7; 8], 'NewVariableNames', {'amount'}); %! A.Properties.Events = E; %! EF = eventfilter (A); %! assert_equal (A(EF.amount > 7, :).v', 6); ## Test a filter of one bare label used as a subscript %!assert (TT(eventfilter ("rain"), :).v', [3, 4]) ## Test a filter of several bare labels %!assert (TT(eventfilter (string ({'rain'; 'snow'})), :).v', [3, 4, 6]) ## Test a bare label no event carries selects nothing %!assert (height (TT(eventfilter ("zzz"), :)), 0) ## Test a filter combines with a variable subscript %!test %! EF = eventfilter (TT); %! out = TT(EF.EventLabels == "rain", {'v'}); %! assert_equal (out.v', [3, 4]); ## Test the result is a plain timetable %!test %! EF = eventfilter (TT); %! assert_equal (class (TT(EF.EventLabels == "rain", :)), 'timetable'); ## Test a filter is reusable and answers the same twice %!test %! EF = eventfilter (TT); %! f = EF.EventLabels == "rain"; %! assert_equal (isequal (TT(f, :), TT(f, :)), true); ## Test rows are deleted through a filter %!test %! EF = eventfilter (TT); %! A = TT; %! A(EF.EventLabels == "rain", :) = []; %! assert_equal (A.v', [1, 2, 5, 6, 7, 8]); ## Test a duration-keyed timetable %!test %! td = seconds ((0:7))'; %! A = timetable (td, (1:8)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (td([3 6]), 'EventLabels', ... %! string ({'rain'; 'snow'}), ... %! 'EventLengths', seconds ([2; 1])); %! EF = eventfilter (A); %! assert_equal (A(EF.EventLabels == "rain", :).v', [3, 4]); ## Test a range running from one event to another %!test %! EF = eventfilter (TT); %! a = EF.EventLabels == "rain"; %! b = EF.EventLabels == "snow"; %! assert_equal (TT(timerange (a, b), :).v', [3, 4, 5]); ## Test a filter built from a timetable carrying no events %!error ... %! eventfilter (timetable ((1:3)', 'TimeStep', hours (1))); ## Test a filter applied to a timetable carrying no events %!error ... %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! A = timetable (t, (1:8)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (t([3 6])); %! B = timetable (t, (1:8)', 'VariableNames', {'v'}); %! EF = eventfilter (A); %! B(EF.EventLabels == "Event 1", :); ## Test a filter applied to an event table %!error ... %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! A = timetable (t, (1:8)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (t([3 6])); %! EF = eventfilter (A); %! E = A.Properties.Events; %! E(EF.EventLabels == "Event 1", :); ## Test the negation operator %!error ... %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! A = timetable (t, (1:8)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (t([3 6])); %! ~ eventfilter (A); ## Test a filter carrying no condition %!error ... %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! A = timetable (t, (1:8)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (t([3 6])); %! A(eventfilter (A), :); ## Test a name no event variable answers to %!error ... %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! A = timetable (t, (1:8)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (t([3 6])); %! eventfilter (A).Nope; ## Test an input that is neither a timetable nor a list of labels %!error ... %! eventfilter (5); ## Test comparing two filters %!error ... %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! A = timetable (t, (1:8)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (t([3 6])); %! EF = eventfilter (A); %! EF.EventLabels == EF.EventLabels; ## Test only '.' indexing is supported %!error ... %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! A = timetable (t, (1:8)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (t([3 6])); %! EF = eventfilter (A); EF(1); ## Test an event filter bound needs one at the other end %!error ... %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! A = timetable (t, (1:8)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (t([3 6])); %! EF = eventfilter (A); %! timerange (EF.EventLabels == "Event 1", t(1)); pr0m1th3as-datatypes-9450b29/inst/eventtable.m000066400000000000000000000661411527370255000211720ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef eventtable < timetable ## -*- texinfo -*- ## @deftp {datatypes} eventtable ## ## Timetable of events, describing what was happening rather than what was ## measured. ## ## An event table is a @code{timetable} whose rows are events. Each row has ## a time, optionally a label naming the event, and optionally a length or ## an end time making it an interval rather than an instant. It exists so ## that the rows of another timetable can be selected by what was happening ## when they were recorded, rather than by the clock alone. ## ## An event table @emph{is} a timetable and inherits every one of its ## methods. @code{help} does not follow method inheritance, so ## @code{help eventtable.sortrows} does not resolve and ## @code{help timetable.sortrows} is where that method is documented; where ## a method behaves differently for an event table, its own documentation ## says so. ## ## Three properties say which variables describe the events, and each holds ## a variable @emph{name} rather than the values themselves: ## ## @itemize ## @item ## @qcode{EventLabelsVariable}, the variable naming each event. ## ## @item ## @qcode{EventLengthsVariable}, how long each event lasted. ## ## @item ## @qcode{EventEndsVariable}, when each event finished. ## ## @end itemize ## ## The last two are mutually exclusive: an event says its extent one way or ## the other, never both. An event with neither is an instant. ## ## Because the properties hold names, an operation that stops a name ## resolving affects the property. Removing the variable clears it, and so ## does deleting it by assigning an empty matrix; @emph{renaming} the ## variable carries the property across to the new name, which is a ## deliberate departure from MATLAB, where the designation is lost and ## renaming back does not restore it. Moving a variable or converting its ## type leaves the property alone. ## ## An event table cannot itself carry an event table. It inherits the ## @qcode{Events} property of a timetable but cannot read or assign it: ## asking for it, or assigning one, raises. ## ## An interval event is half-open on the right: an event beginning at ## 02:00 and lasting two hours covers 02:00 and 03:00 but not 04:00. An ## event of zero length therefore covers nothing at all. ## ## Three things read an event table once it is attached to a timetable by ## that timetable's @qcode{Events} property. @code{syncevents} copies the ## events' data onto the rows they cover, repeating a row that more than ## one event covers. @code{eventfilter} selects the rows the events ## matching a condition cover, and a row covered more than once is selected ## once. @code{timerange} takes two event filters as bounds, running from ## one event to another. In the other direction, @code{extractevents} ## builds an event table out of a timetable's own rows. ## ## A binary operation over two timetables carrying event tables merges ## them, and refuses where their three event properties disagree; a ## row-preserving one carries the event table through unchanged. ## ## Labels given as text are held as a cell array of character vectors, ## which is how this package holds a list of names; a @code{string} given ## as such is kept as one. An @code{eventfilter} compares either as text, ## so a condition reads the same whichever they are. ## ## @seealso{timetable, table, istimetable, eventfilter, ## timetable.extractevents, timetable.syncevents} ## @end deftp properties ## -*- texinfo -*- ## @deftp {eventtable} {property} EventLabelsVariable ## ## Variable naming each event ## ## Variable naming each event, given as the name of one of the event ## table's own variables, or empty when no variable names the events. ## Assigning a name points the property at that variable; assigning ## @code{[]} clears it. An event table built from a vector of times ## alone labels its events @qcode{"Event 1"}, @qcode{"Event 2"} and so ## on and points this property at them, while one built from a timetable ## leaves it empty, because nothing says which of that timetable's ## variables holds labels. ## ## @end deftp EventLabelsVariable = [] ## -*- texinfo -*- ## @deftp {eventtable} {property} EventLengthsVariable ## ## Variable holding how long each event lasted ## ## Variable holding how long each event lasted, given as the name of one ## of the event table's own variables, or empty when the events are ## instants. The variable holds a @code{duration} or a ## @code{calendarDuration}. It cannot be set while ## @qcode{EventEndsVariable} is set; an event states its extent as a ## length or as an end, not as both. ## ## @end deftp EventLengthsVariable = [] ## -*- texinfo -*- ## @deftp {eventtable} {property} EventEndsVariable ## ## Variable holding when each event finished ## ## Variable holding when each event finished, given as the name of one of ## the event table's own variables, or empty when the events are ## instants. The variable holds the same type as the row times. It ## cannot be set while @qcode{EventLengthsVariable} is set; an event ## states its extent as a length or as an end, not as both. ## ## @end deftp EventEndsVariable = [] endproperties methods (Access = public) ## -*- texinfo -*- ## @deftypefn {eventtable} {@var{et} =} eventtable () ## @deftypefnx {eventtable} {@var{et} =} eventtable (@var{eventTimes}) ## @deftypefnx {eventtable} {@var{et} =} eventtable (@var{tt}) ## @deftypefnx {eventtable} {@var{et} =} eventtable (@dots{}, @var{Name}, @var{Value}) ## ## Create an event table. ## ## @code{@var{et} = eventtable ()} returns an empty event table with no ## events and no variables. ## ## @code{@var{et} = eventtable (@var{eventTimes})} returns an event table ## whose events happen at @var{eventTimes}, a @code{datetime} or ## @code{duration} vector. With no labels given, the events are labelled ## @qcode{"Event 1"}, @qcode{"Event 2"} and so on in a variable named ## @qcode{EventLabels}, and @qcode{EventLabelsVariable} names it. ## ## @code{@var{et} = eventtable (@var{tt})} converts the timetable ## @var{tt}, whose row times become the event times and whose variables ## are kept as they are. No variable is guessed to be the labels, so ## @qcode{EventLabelsVariable} is left empty unless ## @qcode{'EventLabelsVariable'} says which one it is. ## ## The following @var{Name}-@var{Value} options are supported: ## ## @multitable @columnfractions 0.28 0.72 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'EventLabels'} @tab A scalar or a vector with one element ## per event, naming the events. It may be of any type other than ## @code{datetime}, @code{duration}, @code{calendarDuration}, ## @code{table} and @code{timetable}. A scalar labels every event alike. ## It is added as a variable named @qcode{EventLabels}. ## ## @item @qcode{'EventLengths'} @tab A @code{duration} or ## @code{calendarDuration}, scalar or one element per event, saying how ## long each event lasted. Only a @code{duration} is accepted when the ## event times are durations. It is added as a variable named ## @qcode{EventLengths}. ## ## @item @qcode{'EventEnds'} @tab A scalar or one element per event, of ## the same type as the event times, saying when each event finished. It ## is added as a variable named @qcode{EventEnds}. ## ## @item @qcode{'EventLabelsVariable'} @tab The name of a variable of ## @var{tt} that names the events. Only with a timetable input. ## ## @item @qcode{'EventLengthsVariable'} @tab The name of a variable of ## @var{tt} that holds the event lengths. Only with a timetable input. ## ## @item @qcode{'EventEndsVariable'} @tab The name of a variable of ## @var{tt} that holds the event end times. Only with a timetable input. ## ## @end multitable ## ## A length and an end are mutually exclusive, however they are given, ## and so are @qcode{'EventLabels'} and @qcode{'EventLabelsVariable'}. ## ## @seealso{timetable, table.table2timetable} ## @end deftypefn function this = eventtable (varargin) ## An empty event table: no events, no variables, nothing designated. if (nargin == 0) this = this@timetable (); return endif ## Parse optional paired arguments optNames = {'EventLabels', 'EventLengths', 'EventEnds', ... 'EventLabelsVariable', 'EventLengthsVariable', ... 'EventEndsVariable'}; dfValues = {missing, missing, missing, missing, missing, missing}; [Labels, Lengths, Ends, LabelsVar, LengthsVar, EndsVar, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## The event times or their timetable come first, so text after them ## is an option this constructor does not know. if (numel (args) > 1 && any (cellfun (@(x) ischar (x) || isa (x, 'string'), args(2:end)))) if (mod (numel (varargin), 2) == 0) error ("eventtable: name-value arguments must be in pairs."); endif error ("eventtable: invalid optional paired argument."); endif if (numel (args) != 1) error (strcat ("eventtable: the event times or the timetable they", ... " come from must be given exactly once.")); endif src = args{1}; fromTT = isa (src, 'timetable'); if (! (fromTT || isdatetime (src) || isduration (src))) error (strcat ("eventtable: first input must be a timetable, or a", ... " datetime or duration vector.")); endif ## The three '*Variable' options name a variable of the input, so they ## need an input that has variables to name. if (! fromTT) named = {'EventLabelsVariable', LabelsVar; ... 'EventLengthsVariable', LengthsVar; ... 'EventEndsVariable', EndsVar}; for i = 1:rows (named) if (wasGiven (named{i,2})) error (strcat ("eventtable: '%s' names a variable of a", ... " timetable and cannot be used with a vector", ... " of event times."), named{i,1}); endif endfor endif ## An event states its extent one way or the other, and is labelled by ## given values or by a named variable, never by both. if ((wasGiven (Lengths) || wasGiven (LengthsVar)) && (wasGiven (Ends) || wasGiven (EndsVar))) error (strcat ("eventtable: specify either the event lengths or", ... " the event ends, but not both.")); endif if (wasGiven (Labels) && wasGiven (LabelsVar)) error (strcat ("eventtable: specify either the event labels or an", ... " event labels variable, but not both.")); endif ## The event times, and the variables the input already carries. if (fromTT) eventTimes = src.Properties.RowTimes; varNames = src.Properties.VariableNames; varValues = cell (1, numel (varNames)); for i = 1:numel (varNames) varValues{i} = getvar (src, i); endfor dimNames = src.Properties.DimensionNames; else eventTimes = src(:); varNames = {}; varValues = {}; dimNames = {'Time', 'Variables'}; endif nev = numel (eventTimes); ## Values given as options become variables of the event table, named ## after the option that carried them. A vector input with no labels ## given gets generated ones, which is the only place a label is ## invented; a timetable input never has one guessed for it. lblVar = []; lenVar = []; endVar = []; if (wasGiven (Labels)) Labels = checkEventLabels (Labels, nev); [varNames, varValues] = addEventVar (varNames, varValues, ... 'EventLabels', Labels); lblVar = 'EventLabels'; elseif (! fromTT) auto = cell (nev, 1); for i = 1:nev auto{i} = sprintf ("Event %d", i); endfor [varNames, varValues] = addEventVar (varNames, varValues, ... 'EventLabels', auto); lblVar = 'EventLabels'; endif if (wasGiven (Lengths)) Lengths = checkEventLengths (Lengths, nev, eventTimes); [varNames, varValues] = addEventVar (varNames, varValues, ... 'EventLengths', Lengths); lenVar = 'EventLengths'; endif if (wasGiven (Ends)) Ends = checkEventEnds (Ends, nev, eventTimes); [varNames, varValues] = addEventVar (varNames, varValues, ... 'EventEnds', Ends); endVar = 'EventEnds'; endif this = this@timetable (eventTimes, varValues{:}, ... 'VariableNames', varNames, ... 'DimensionNames', dimNames); ## The metadata of a converted timetable travels with its variables. if (fromTT) this = adoptSourceMetadata (this, src); endif ## The '*Variable' options name a variable that is already there, so ## they are resolved once the object exists to resolve them against. if (wasGiven (LabelsVar)) lblVar = checkEventVariable (this, LabelsVar, 'EventLabelsVariable'); endif if (wasGiven (LengthsVar)) lenVar = checkEventVariable (this, LengthsVar, ... 'EventLengthsVariable'); endif if (wasGiven (EndsVar)) endVar = checkEventVariable (this, EndsVar, 'EventEndsVariable'); endif this.EventLabelsVariable = lblVar; this.EventLengthsVariable = lenVar; this.EventEndsVariable = endVar; endfunction endmethods methods (Static) ## -*- texinfo -*- ## @deftypefn {eventtable} {@var{et} =} eventtable.empty () ## @deftypefnx {eventtable} {@var{et} =} eventtable.empty (@var{r}, @var{v}) ## @deftypefnx {eventtable} {@var{et} =} eventtable.empty (@var{sz}) ## ## Create an empty event table. ## ## The arguments are those of @code{timetable.empty}, which this is in ## every respect but the class of what comes back: a 0-by-0 event table ## with no arguments, and otherwise one of @var{r} rows and @var{v} ## variables, at least one of the two being zero. None of the three ## event properties is set, there being no variable for one to name. ## ## @seealso{eventtable, timetable} ## @end deftypefn function et = empty (varargin) [sz, errmsg] = tabular.emptySize ('eventtable', varargin); if (! isempty (errmsg)) error ('eventtable.empty: %s', errmsg); endif et = eventtable (timetable ('Size', sz, 'VariableTypes', ... repmat ({'double'}, 1, sz(2)), ... 'RowTimes', NaT (sz(1), 1))); endfunction endmethods methods (Access = protected) ## The three event properties travel with the four a timetable publishes ## about its row times. 'Events' is not among them: an event table does ## not carry one, so the field goes before the properties object is ## handed the rest. function out = rowLabelProperties (this) out = rowLabelProperties@timetable (this); out = rmfield (out, 'Events'); out.EventLabelsVariable = this.EventLabelsVariable; out.EventLengthsVariable = this.EventLengthsVariable; out.EventEndsVariable = this.EventEndsVariable; endfunction ## An event table has no attached event table and never will, so the ## reader answers with nothing rather than reaching the inherited ## property. With this one, 'detachEvents', 'carryEvents' and ## 'setEventsOf' below are the only ways to that property, and none of ## them stores anything in it: storing an event table there would build ## a cycle that every recursive operation would follow. function out = eventsOf (this) out = []; endfunction ## A designation naming a variable that is no longer there is dropped. ## Removing the variable, and deleting it by assigning an empty matrix, ## both arrive here; moving one or converting its type do too and change ## nothing, the name still resolving. function this = varsChanged (this) names = this.VariableNames; this.EventLabelsVariable = carried (this.EventLabelsVariable, names); this.EventLengthsVariable = carried (this.EventLengthsVariable, names); this.EventEndsVariable = carried (this.EventEndsVariable, names); endfunction ## A designation follows the variable it names across a rename. MATLAB ## clears it instead, and does not restore it when the variable is ## renamed back, so a cosmetic operation loses the designation for good; ## see deviation D6. function this = varsRenamed (this, oldNames, newNames) this.EventLabelsVariable = renamed (this.EventLabelsVariable, ... oldNames, newNames); this.EventLengthsVariable = renamed (this.EventLengthsVariable, ... oldNames, newNames); this.EventEndsVariable = renamed (this.EventEndsVariable, ... oldNames, newNames); endfunction ## An event table is the more derived class, so a result built from one ## and a plain timetable is an event table however the two were ordered. ## The three properties come from this operand; one naming a variable the ## result does not carry is left unset, the designation having nothing to ## point at. function tbl = promoteResult (this, tbl) if (isa (tbl, 'eventtable')) return; endif tbl = eventtable (tbl); names = tbl.VariableNames; tbl.EventLabelsVariable = carried (this.EventLabelsVariable, names); tbl.EventLengthsVariable = carried (this.EventLengthsVariable, names); tbl.EventEndsVariable = carried (this.EventEndsVariable, names); endfunction ## An event table has nothing to detach. function this = detachEvents (this) endfunction ## A binary operation over event tables has nothing to carry: an event ## table holds no event table of its own. function [tbl, errmsg] = carryEvents (tbl, ops) errmsg = ''; endfunction ## Nothing may attach an event table to an event table. function this = setEventsOf (this, val) error (strcat ("eventtable.subsasgn: 'Events' is not a property of", ... " an event table; an event table cannot carry an", ... " event table.")); endfunction ## One of the seven assigned. The three event properties hold a variable ## name, so a name is checked against the variables this object has and ## an empty matrix clears the designation; anything else is left to the ## timetable, which owns the row time four. function [this, handled] = setRowLabelProperty (this, name, val, chain_s) handled = true; switch (name) case 'EventLabelsVariable' this.EventLabelsVariable = checkEventVariable (this, val, name); case 'EventLengthsVariable' val = checkEventVariable (this, val, name); if (! isempty (val) && ! isempty (this.EventEndsVariable)) error (strcat ("eventtable.subsasgn: 'EventLengthsVariable'", ... " cannot be set while 'EventEndsVariable' is", ... " set; clear that one first.")); endif this.EventLengthsVariable = val; case 'EventEndsVariable' val = checkEventVariable (this, val, name); if (! isempty (val) && ! isempty (this.EventLengthsVariable)) error (strcat ("eventtable.subsasgn: 'EventEndsVariable'", ... " cannot be set while 'EventLengthsVariable'", ... " is set; clear that one first.")); endif this.EventEndsVariable = val; otherwise [this, handled] = setRowLabelProperty@timetable (this, name, ... val, chain_s); endswitch endfunction function out = makeProperties (this) out = datatypes.tabular.EventtableProperties (getProperties (this), ... this.CustomPropTypes); endfunction ## The variable metadata a converted timetable brings with it. The ## variables themselves went in through the constructor; this is ## everything about them, and about the object, that the constructor has ## no argument for. It is a method rather than a local function because ## it reads the source's protected state. function this = adoptSourceMetadata (this, src) props = getProperties (src); this.Description = props.Description; this.UserData = props.UserData; this.VariableDescriptions = props.VariableDescriptions; this.VariableUnits = props.VariableUnits; this.VariableContinuity = props.VariableContinuity; this.CustomProperties = props.CustomProperties; this.CustomPropTypes = customPropTypes (src); endfunction endmethods endclassdef ## A value given as an option becomes a variable, appended after whatever the ## input already carried. A name the input already uses is a conflict the ## caller has to resolve, since silently replacing a variable would lose it. function [names, values] = addEventVar (names, values, name, val) if (any (strcmp (names, name))) error (strcat ("eventtable: the input already has a variable named", ... " '%s'; give the values with '%sVariable' instead."), ... name, name); endif names{end+1} = name; values{end+1} = val; endfunction ## Event labels may be of any type that can name a thing, which rules out the ## time types, whose values describe when rather than what, and the tabular ## types, which are not values at all. function val = checkEventLabels (val, nev) bad = {'datetime', 'duration', 'calendarDuration', 'table', 'timetable'}; if (any (cellfun (@(c) isa (val, c), bad))) error (strcat ("eventtable: 'EventLabels' must not be a datetime, a", ... " duration, a calendarDuration, a table or a timetable.")); endif ## Text labels are held as a cellstr, which is how this package holds a ## list of names: Octave has no string literal of its own, so text arrives ## as char far more often than as a 'string', and a class does not turn one ## representation into another behind the caller's back. A character ## matrix is one label per row. A 'string' given as such is kept as it is. if (ischar (val)) val = cellstr (val); endif val = broadcastEventVar (val, nev, 'EventLabels'); endfunction ## An event lasts for a duration or for a calendar duration, and a calendar ## length has no meaning against row times that are themselves durations, ## there being no calendar to place them in. function val = checkEventLengths (val, nev, eventTimes) if (isduration (eventTimes)) if (! isduration (val)) error (strcat ("eventtable: 'EventLengths' must be a duration when", ... " the event times are durations.")); endif elseif (! (isduration (val) || iscalendarduration (val))) error (strcat ("eventtable: 'EventLengths' must be a duration or a", ... " calendarDuration.")); endif val = broadcastEventVar (val, nev, 'EventLengths'); endfunction ## An event ends at a time of the same kind as it began. function val = checkEventEnds (val, nev, eventTimes) if (! strcmp (class (val), class (eventTimes))) error (strcat ("eventtable: 'EventEnds' must be a %s, the same type", ... " as the event times."), class (eventTimes)); endif val = broadcastEventVar (val, nev, 'EventEnds'); endfunction ## One value labels, lengthens or ends every event alike; otherwise there is ## one value per event. function val = broadcastEventVar (val, nev, opt) if (isscalar (val)) val = repmat (val(:), nev, 1); elseif (numel (val) == nev) val = val(:); else error (strcat ("eventtable: '%s' must be a scalar or have one element", ... " per event; got %d for %d events."), ... opt, numel (val), nev); endif endfunction ## A variable name for one of the three event properties, resolved against ## the variables THIS actually has. An empty matrix clears the designation, ## which is how a property is unset; an empty character vector does not, ## being a name of no characters rather than the absence of a name. function name = checkEventVariable (this, val, prop) if (isnumeric (val) && isempty (val)) name = []; return endif if (isstring (val) && isscalar (val)) val = char (val); endif if (! (ischar (val) && isrow (val))) error (strcat ("eventtable: '%s' must be the name of a variable, given", ... " as a character vector or a string scalar, or [] to", ... " clear it."), prop); endif names = this.Properties.VariableNames; if (! any (strcmp (names, val))) error ("eventtable: '%s' names no variable of this event table: '%s'.", ... prop, val); endif name = val; endfunction ## An event property carried onto a promoted result, or unset where the ## variable it names is not among that result's. function val = carried (val, names) if (! isempty (val) && ! any (strcmp (names, val))) val = []; endif endfunction ## An event property carried across a rename: where it names one of the ## renamed variables it takes that variable's new name, and otherwise it is ## left as it stands. function val = renamed (val, oldNames, newNames) if (isempty (val)) return; endif ix = find (strcmp (cellstr (oldNames), val), 1); if (! isempty (ix)) newNames = cellstr (newNames); val = newNames{ix}; endif endfunction function tf = wasGiven (x) tf = ! isa (x, 'missing'); endfunction ## 'pkg test' reads the BISTs from the file it is testing, so a class ## whose suite lives in inst/tests/ reads as carrying none. This block ## answers for it and checks nothing itself. %!test %! ## The suite for this class is inst/tests/eventtable.m-tst. pr0m1th3as-datatypes-9450b29/inst/findgroups.m000066400000000000000000000172531527370255000212210ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{G} =} findgroups (@var{A}) ## @deftypefnx {datatypes} {@var{G} =} findgroups (@var{A1}, @dots{}, @var{AN}) ## @deftypefnx {datatypes} {[@var{G}, @var{ID}] =} findgroups (@var{A}) ## @deftypefnx {datatypes} {[@var{G}, @var{ID1}, @dots{}, @var{IDN}] =} findgroups (@var{A1}, @dots{}, @var{AN}) ## ## Find groups defined by one or more grouping variables. ## ## @code{@var{G} = findgroups (@var{A})} returns @var{G}, a column vector of ## positive integer group numbers with one element per element of the grouping ## variable @var{A}. The groups are the sorted unique values of @var{A}: if ## @var{N} groups are found, every integer between 1 and @var{N} labels a group. ## Elements of @var{A} that are missing (@code{NaN}, @code{NaT}, ## @code{}, @code{''}, or @code{}) are labelled @code{NaN} ## in @var{G}. @var{A} can be a numeric, logical, @code{string}, ## @code{cellstr}, @code{char}, @code{datetime}, @code{duration}, ## @code{calendarDuration}, or @code{categorical} vector. ## ## @code{@var{G} = findgroups (@var{A1}, @dots{}, @var{AN})} defines groups as ## the sorted unique combinations of values across the grouping variables ## @var{A1}, @dots{}, @var{AN}, which must all have the same number of elements. ## ## @code{[@var{G}, @var{ID}] = findgroups (@var{A})} also returns @var{ID}, the ## sorted unique values of @var{A} that identify each group. With multiple ## grouping variables, @code{[@var{G}, @var{ID1}, @dots{}, @var{IDN}] = ## findgroups (@var{A1}, @dots{}, @var{AN})} returns one identifier vector per ## grouping variable. ## ## To group the variables of a @code{table}, call @code{findgroups (@var{T})}, ## which dispatches to the @code{table} method and returns the group identifiers ## as a table. ## ## @seealso{splitapply, table} ## @end deftypefn function [G, varargout] = findgroups (varargin) if (nargin < 1) print_usage (); endif ## Force each grouping variable to a column (char matrices keep their rows as ## observations); reject anything that is not a vector or a char matrix. vars = varargin; for k = 1:numel (vars) v = vars{k}; if (ischar (v)) if (ndims (v) > 2) error ("findgroups: grouping variables must be vectors."); endif elseif (isvector (v) || isempty (v)) vars{k} = v(:); else error ("findgroups: grouping variables must be vectors."); endif endfor ## Build the combined proxy matrix and the overall missing-row mask. nvar = numel (vars); n = size (vars{1}, 1); P = []; miss = false (n, 1); for k = 1:nvar if (size (vars{k}, 1) != n) error (strcat ("findgroups: grouping variables must have the same", ... " number of elements.")); endif [p, m, errmsg] = group_col_proxy (vars{k}); if (! isempty (errmsg)) error ("findgroups: %s", errmsg); endif P = [P, p]; miss = miss | m; endfor ## Label the non-missing rows by sorted unique combination. G = NaN (n, 1); keep = find (! miss); repRows = []; if (! isempty (keep)) [~, ia, ic] = unique (P(keep,:), "rows"); G(keep) = ic; repRows = keep(ia); endif if (nargout > 1) if (nargout - 1 > nvar) print_usage (); endif varargout = cell (1, nargout - 1); for k = 1:(nargout - 1) varargout{k} = vars{k}(repRows,:); endfor endif endfunction ## Build a single-column grouping proxy for one grouping variable COL: a numeric ## matrix P (one row per element) whose sort order matches COL's value order, so ## that 'unique (P, "rows")' recovers the sorted unique groups, together with a ## logical MISS mask flagging the elements that findgroups treats as missing ## (NaN/NaT//''/). Returns an errmsg body (empty on ## success) emitted by the caller under its own name. Self-contained: no table ## dependency. function [p, miss, errmsg] = group_col_proxy (col) p = []; miss = []; errmsg = ''; if (isa (col, 'categorical')) ## Categorical groups follow category order (ordinal or reordered), which ## the underlying category codes encode; maps to NaN. p = double (col)(:); miss = isnan (p); elseif (isa (col, 'string') || iscellstr (col) || ischar (col)) c = cellstr (col); c = c(:); miss = cellfun (@isempty, c); [~, ~, ic] = unique (c); p = ic(:); elseif (isa (col, 'datetime')) DV = datevec (col); # (numel)-by-6 in column-major order nat = any (isnan (DV), 2); DV(nat,:) = 0; DV(nat,2:3) = 1; # valid month/day placeholder p = datenum (DV); p(nat) = NaN; p = p(:); miss = isnan (p); elseif (isa (col, 'duration')) p = days (col)(:); miss = isnan (p); elseif (isa (col, 'calendarDuration')) p = proxyArray (col); miss = any (isnan (p), 2); elseif (isnumeric (col) || islogical (col)) p = double (col)(:); miss = isnan (p); else errmsg = sprintf ("unsupported grouping variable type '%s'.", class (col)); endif endfunction %!assert_equal (findgroups ([1; 3; 1; 2]), [1; 3; 1; 2]) %!assert_equal (findgroups ([30, 10, 10, 20]), [3; 1; 1; 2]) %!assert_equal (findgroups ({'b'; 'a'; 'b'; 'c'}), [2; 1; 2; 3]) %!assert_equal (findgroups (logical ([1; 0; 1; 0])), [2; 1; 2; 1]) %!test %! ## Missing values map to NaN in G %! assert_equal (findgroups ([1; NaN; 2; 1]), [1; NaN; 2; 1]); %! assert_equal (findgroups ({'b'; ''; 'a'}), [2; NaN; 1]); %!test %! ## Categorical groups follow category order, not alphabetical order %! c = categorical ({'medium'; 'low'; 'high'; 'low'}, ... %! {'low', 'medium', 'high'}, 'Ordinal', true); %! assert_equal (findgroups (c), [2; 1; 3; 1]); %! [g, id] = findgroups (c); %! assert_equal (cellstr (id), {'low'; 'medium'; 'high'}); %!test %! ## Reordered nominal categoricals also group by category order %! c = reordercats (categorical ({'b'; 'a'; 'c'; 'a'}), {'c', 'a', 'b'}); %! assert_equal (findgroups (c), [3; 2; 1; 2]); %!test %! ## Multiple grouping variables group by sorted unique combinations %! assert_equal (findgroups ([1; 1; 2; 1], {'b'; 'a'; 'a'; 'b'}), [2; 1; 3; 2]); %!test %! ## Second output holds the sorted unique identifiers of each input %! [G, ID] = findgroups ([30; 10; 10; 20]); %! assert_equal (G, [3; 1; 1; 2]); %! assert_equal (ID, [10; 20; 30]); %!test %! [G, ID1, ID2] = findgroups ([1; 1; 2], {'b'; 'a'; 'a'}); %! assert_equal (G, [2; 1; 3]); %! assert_equal (ID1, [1; 1; 2]); %! assert_equal (ID2, {'a'; 'b'; 'a'}); %!error findgroups () %!error findgroups ([1, 2; 3, 4]) %!error ... %! findgroups (repmat ('a', [2, 2, 2])) %!error ... %! findgroups (ones (2, 2)) %!error ... %! findgroups ([1; 2; 3], [1; 2]) %!error ... %! findgroups ({{1}; {2}}) pr0m1th3as-datatypes-9450b29/inst/groupcounts.m000066400000000000000000000402301527370255000214200ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{B} =} groupcounts (@var{A}) ## @deftypefnx {datatypes} {@var{B} =} groupcounts (@var{A}, @var{groupbins}) ## @deftypefnx {datatypes} {[@var{B}, @var{BG}, @var{BP}] =} groupcounts (@dots{}) ## @deftypefnx {datatypes} {[@dots{}] =} groupcounts (@dots{}, @var{Name}, @var{Value}) ## ## Count the number of elements in each group of an array. ## ## @code{@var{B} = groupcounts (@var{A})} groups the rows of @var{A} by their ## values and returns @var{B}, a column vector with the number of elements in ## each group. @var{A} is a grouping vector, a matrix whose columns are ## grouping variables, or a cell array of grouping vectors. Groups are the ## sorted unique combinations of grouping values; rows holding a missing value ## in a grouping variable form their own groups, sorted after the non-missing ## groups. ## ## @code{[@var{B}, @var{BG}, @var{BP}] = groupcounts (@dots{})} also returns ## @var{BG}, the grouping values that identify each group, and @var{BP}, a ## column vector giving each group's count as a percentage of the total. When ## @var{A} is a single grouping vector, @var{BG} holds its representative value ## for each group; when several grouping variables are given, @var{BG} is a cell ## array with one element per grouping variable. ## ## The optional @var{groupbins} argument bins the grouping variables before ## grouping (a vector of bin edges or a positive integer number of bins, applied ## to a numeric, datetime, or duration grouping variable, or a cell array with ## one scheme per grouping variable); each binned variable becomes a categorical ## of bin interval labels. ## ## The behaviour can be modified with the @qcode{'IncludeMissingGroups'} ## (default @code{true}), @qcode{'IncludeEmptyGroups'} (default @code{false}), ## and @qcode{'IncludedEdge'} (default @qcode{'left'}, the inclusive bin edge) ## @var{Name}/@var{Value} pairs, as for the @code{table} method. When ## @qcode{'IncludeEmptyGroups'} is @code{true}, the unused categories of a ## categorical or binned grouping variable contribute empty groups. ## ## To count the rows in each group of a @code{table}, call @code{groupcounts ## (@var{T}, @var{groupvars}, @dots{})}, which dispatches to the @code{table} ## method and returns the result as a table. ## ## @seealso{groupsummary, findgroups, splitapply, table} ## @end deftypefn function [B, varargout] = groupcounts (A, varargin) if (nargin < 1) print_usage (); endif ## An optional GROUPBINS positional argument may precede the Name-Value ## options; anything else after A must be a recognised option. optNames = {'IncludeMissingGroups', 'IncludeEmptyGroups', 'IncludedEdge'}; hasGB = false; groupbins = []; if (! isempty (varargin)) a = varargin{1}; isOpt = ((ischar (a) && isrow (a)) || (isa (a, ... 'string') && isscalar (a))) ... && any (strcmpi (char (a), optNames)); if (! isOpt) if (__groupbins__ ('is_spec', a)) hasGB = true; groupbins = a; varargin = varargin(2:end); else error (strcat ("groupcounts: invalid argument; expected a", ... " GROUPBINS", ... " binning scheme or a Name-Value option.")); endif endif endif ## Parse Name-Value options. dfValues = {true, false, 'left'}; [incMiss, incEmpty, incEdge] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! (isscalar (incMiss) && (islogical (incMiss) || isnumeric (incMiss)))) error ("groupcounts: 'IncludeMissingGroups' must be a logical scalar."); endif incMiss = logical (incMiss); if (! (isscalar (incEmpty) && (islogical (incEmpty) || isnumeric (incEmpty)))) error ("groupcounts: 'IncludeEmptyGroups' must be a logical scalar."); endif incEmpty = logical (incEmpty); incEdge = check_included_edge ('groupcounts', incEdge); ## Resolve A into a cell array of grouping column vectors: a cell array holds ## one grouping variable per element, a numeric/logical matrix one per column, ## and anything else is a single grouping variable. if (iscell (A) && ! (iscellstr (A) && isvector (A))) gvs = A(:)'; elseif (! ischar (A) && ismatrix (A) && size (A, 2) > 1) gvs = cell (1, size (A, 2)); for j = 1:size (A, 2) gvs{j} = A(:,j); endfor else gvs = {A}; endif for j = 1:numel (gvs) g = gvs{j}; if (ischar (g)) if (ndims (g) > 2) error ("groupcounts: grouping variables must be vectors."); endif elseif (isvector (g) || isempty (g)) gvs{j} = g(:); else error ("groupcounts: grouping variables must be vectors."); endif endfor n = size (gvs{1}, 1); for j = 2:numel (gvs) if (size (gvs{j}, 1) != n) error (strcat ("groupcounts: each grouping variable must have the", ... " same number of elements.")); endif endfor ## Bin the grouping variables when a GROUPBINS argument was given. if (hasGB) names = cell (1, numel (gvs)); for j = 1:numel (gvs) names{j} = sprintf ("%d", j); endfor [gvs, ~, errmsg] = __groupbins__ ('bin', gvs, names, groupbins, incEdge, ... 'groupcounts'); if (! isempty (errmsg)) error ("groupcounts: %s", errmsg); endif endif ## Group the rows, treating missing grouping values as their own groups; ## IncludeEmptyGroups adds the unused categories of a categorical or binned ## grouping variable as empty groups. [Grp, ng, gvals, errmsg] = gb_grouping (gvs, incMiss, incEmpty); if (! isempty (errmsg)) error ("groupcounts: %s", errmsg); endif B = accumarray (Grp(! isnan (Grp)), 1, [ng, 1]); ## Optional outputs: the grouping identifiers and the group percentages. if (nargout > 1) if (numel (gvs) == 1) BG = gvals{1}; else BG = gvals; endif varargout{1} = BG; endif if (nargout > 2) varargout{2} = 100 * B / sum (B); endif endfunction ## Group rows by the grouping-variable values GVS (a cell array of column ## vectors), treating each variable's missing values as a single group value. ## Returns G (1..NGROUPS), NGROUPS, REPROWS (a representative row per group), ## and an errmsg body. Groups are sorted by value with missing groups last; ## when INCMISS is false the rows with a missing grouping value are dropped. ## Self-contained: no table dependency. function [G, ngroups, repRows, errmsg] = gc_group_rows (gvs, incMiss) errmsg = ''; G = []; ngroups = 0; repRows = []; n = size (gvs{1}, 1); KEY = []; SORT = []; anyMiss = false (n, 1); for j = 1:numel (gvs) [p, m, e] = gc_col_proxy (gvs{j}); if (! isempty (e)) errmsg = e; return; endif pc = p; pc(m,:) = 0; KEY = [KEY, pc, double(m)]; sp = p; sp(m,:) = Inf; SORT = [SORT, sp]; anyMiss = anyMiss | m; endfor [~, ia, ic] = unique (KEY, "rows"); ng = numel (ia); grpMiss = anyMiss(ia); [~, ord] = sortrows (SORT(ia,:)); reps = ia(ord); grpMiss = grpMiss(ord); pos = zeros (ng, 1); pos(ord) = 1:ng; G = pos(ic); if (! incMiss && any (grpMiss)) keep = find (! grpMiss); newId = NaN (ng, 1); newId(keep) = 1:numel (keep); G = newId(G); reps = reps(keep); ng = numel (keep); endif ngroups = ng; repRows = reps; endfunction ## Build a single-column grouping proxy for one grouping variable COL: a numeric ## matrix P whose sort order matches COL's value order, together with a logical ## MISS mask flagging the missing elements. Returns an errmsg body emitted by ## the caller. Self-contained: no table dependency. function [p, miss, errmsg] = gc_col_proxy (col) p = []; miss = []; errmsg = ''; if (isa (col, 'categorical')) ## Categorical groups follow category order (ordinal or reordered), which ## the underlying category codes encode; maps to NaN. p = double (col)(:); miss = isnan (p); elseif (isa (col, 'string') || iscellstr (col) || ischar (col)) c = cellstr (col); c = c(:); miss = cellfun (@isempty, c); [~, ~, ic] = unique (c); p = ic(:); elseif (isa (col, 'datetime')) DV = datevec (col); nat = any (isnan (DV), 2); DV(nat,:) = 0; DV(nat,2:3) = 1; p = datenum (DV); p(nat) = NaN; p = p(:); miss = isnan (p); elseif (isa (col, 'duration')) p = days (col)(:); miss = isnan (p); elseif (isa (col, 'calendarDuration')) p = proxyArray (col); miss = any (isnan (p), 2); elseif (isnumeric (col) || islogical (col)) p = double (col)(:); miss = isnan (p); else errmsg = sprintf ("unsupported grouping variable type '%s'.", class (col)); endif endfunction ## Validate an 'IncludedEdge' binning option VAL for CALLER, returning it ## lowercased as 'left' or 'right'. Self-contained: no table dependency. function e = check_included_edge (caller, val) if (isa (val, 'string') && isscalar (val)) val = char (val); endif if (! (ischar (val) && isrow (val) ... && any (strcmpi (val, {'left', 'right'})))) error ("%s: 'IncludedEdge' must be 'left' or 'right'.", caller); endif e = lower (val); endfunction ## Group rows by the grouping-variable values GVS (already binned when a ## GROUPBINS argument was given). Returns G (1..NG, NaN excluded), NG, GVALS (a ## 1-by-K cell of typed level-value columns, one per group), and an errmsg body. ## When INCEMPTY is true the unused categories of a categorical or binned ## grouping variable contribute empty groups, from the full Cartesian product of ## levels. function [G, ng, gvals, errmsg] = gb_grouping (gvs, incMiss, incEmpty) errmsg = ''; G = []; ng = 0; gvals = {}; K = numel (gvs); n = size (gvs{1}, 1); if (! incEmpty) [G, ng, repRows, errmsg] = gc_group_rows (gvs, incMiss); if (! isempty (errmsg)) return; endif gvals = cell (1, K); for j = 1:K gvals{j} = gvs{j}(repRows, :); endfor return; endif idxAll = NaN (n, K); levVals = cell (1, K); sizes = zeros (1, K); for j = 1:K [idx, lv, errmsg] = gb_levels (gvs{j}, incMiss); if (! isempty (errmsg)) return; endif idxAll(:,j) = idx; levVals{j} = lv; sizes(j) = size (lv, 1); endfor ng = prod (sizes); lvlOf = ones (ng, K); period = 1; for j = K:-1:1 lvlOf(:,j) = mod (floor ((0:ng-1)' / period), sizes(j)) + 1; period = period * sizes(j); endfor valid = all (! isnan (idxAll), 2); lin = zeros (n, 1); period = 1; for j = K:-1:1 col = idxAll(:,j); col(isnan (col)) = 1; lin = lin + (col - 1) * period; period = period * sizes(j); endfor G = NaN (n, 1); G(valid) = lin(valid) + 1; gvals = cell (1, K); for j = 1:K gvals{j} = levVals{j}(lvlOf(:,j), :); endfor endfunction ## Build the level structure of one grouping variable COL for empty-group-aware ## grouping: IDX (n-by-1 level index, NaN excluded), LEVVALS (one representative ## value per level), and an errmsg body. A categorical uses its full category ## order; a missing value forms one extra level, sorted last, when INCMISS. function [idx, levVals, errmsg] = gb_levels (col, incMiss) idx = []; levVals = []; errmsg = ''; n = size (col, 1); [p, miss, errmsg] = gc_col_proxy (col); if (! isempty (errmsg)) return; endif if (isa (col, 'categorical')) cats = categories (col); L = numel (cats); idx = double (col)(:); levVals = categorical (cats(:), cats, 'Ordinal', isordinal (col)); else idx = NaN (n, 1); keep = find (! miss); if (isempty (keep)) levVals = col([], :); L = 0; else [~, ia, ic] = unique (p(keep,:), "rows"); idx(keep) = ic; levVals = col(keep(ia), :); L = numel (ia); endif endif if (any (miss)) if (incMiss) L = L + 1; idx(miss) = L; mrow = find (miss, 1); levVals = [levVals; col(mrow, :)]; else idx(miss) = NaN; endif endif endfunction %!assert_equal (groupcounts ([1; 1; 2; 2; 1]), [3; 2]) %!assert_equal (groupcounts ([30; 10; 10; 20]), [2; 1; 1]) %!test %! ## The second and third outputs are the group values and the percentages %! [B, BG, BP] = groupcounts ([1; 1; 2; 2; 1]); %! assert_equal (B, [3; 2]); %! assert_equal (BG, [1; 2]); %! assert_equal (BP, [60; 40]); %!test %! ## Groups follow the sorted unique order of the values %! [B, BG, BP] = groupcounts ([1; 1; 2; 2; 3; 5; 3; 3; 1; 4]); %! assert_equal (B, [3; 2; 3; 1; 1]); %! assert_equal (BG, [1; 2; 3; 4; 5]); %! assert_equal (BP, [30; 20; 30; 10; 10]); %!test %! ## A matrix groups by the unique combinations of its columns %! [B, BG, BP] = groupcounts ([1 0; 1 0; 1 1; 2 1]); %! assert_equal (B, [2; 1; 1]); %! assert_equal (BG, {[1; 1; 2], [0; 1; 1]}); %! assert_equal (BP, [50; 25; 25]); %!test %! ## A cell array of vectors groups by the unique combinations, BG a cell array %! [B, BG, BP] = groupcounts ({[1; 1; 2; 2; 1], {'b'; 'a'; 'a'; 'b'; 'b'}}); %! assert_equal (B, [1; 2; 1; 1]); %! assert_equal (BG, {[1; 1; 2; 2], {'a'; 'b'; 'a'; 'b'}}); %! assert_equal (BP, [20; 40; 20; 20]); %!test %! ## A missing value forms its own group, sorted last %! [B, BG, BP] = groupcounts ([1; 1; 2; NaN; 2]); %! assert_equal (B, [2; 2; 1]); %! assert_equal (BG, [1; 2; NaN]); %! assert_equal (BP, [40; 40; 20]); %!test %! ## 'IncludeMissingGroups' false drops the rows with a missing grouping value %! [B, BG] = groupcounts ([1; 1; 2; NaN; 2], 'IncludeMissingGroups', false); %! assert_equal (B, [2; 2]); %! assert_equal (BG, [1; 2]); %!test %! ## A categorical vector groups by category order, not alphabetically %! c = categorical ({'medium'; 'low'; 'high'; 'low'}, ... %! {'low', 'medium', 'high'}, 'Ordinal', true); %! [B, BG] = groupcounts (c); %! assert_equal (B, [2; 1; 1]); %! assert_equal (cellstr (BG), {'low'; 'medium'; 'high'}); %!test %! ## A GROUPBINS edge vector groups a numeric variable by bin interval %! [B, BG, BP] = groupcounts ([1; 3; 5; 7; 9; 11], [0 6 12]); %! assert_equal (B, [3; 3]); %! assert_equal (iscategorical (BG), true); %! assert_equal (cellstr (BG), {'[0, 6)'; '[6, 12]'}); %! assert_equal (BP, [50; 50]); %!test %! ## A GROUPBINS number of bins makes equal-width bins over the data range %! [B, BG] = groupcounts ([1; 2; 3; 4], 3); %! assert_equal (cellstr (BG), {'[1, 2)'; '[2, 3)'; '[3, 4]'}); %! assert_equal (B, [1; 1; 2]); %!test %! ## 'IncludeEmptyGroups' adds unused categorical categories with zero count %! c = categorical ({'a'; 'a'; 'b'}, {'a', 'b', 'c'}); %! [B, BG] = groupcounts (c, 'IncludeEmptyGroups', true); %! assert_equal (cellstr (BG), {'a'; 'b'; 'c'}); %! assert_equal (B, [2; 1; 0]); %!error groupcounts () %!error ... %! groupcounts ([1; 2], 'bogus') %!error ... %! groupcounts ([1; 2], 2, 'IncludedEdge', 'mid') %!error ... %! groupcounts ([1; 2], 'month') %!error ... %! groupcounts ({[1; 2; 3], [1; 2]}) %!error ... %! groupcounts ([1; 2], 'IncludeMissingGroups', [1, 2]) %!error ... %! groupcounts ([1; 2], 'IncludeEmptyGroups', [1, 2]) %!error ... %! groupcounts (repmat ('a', [2, 2, 2])) %!error ... %! groupcounts (ones (2, 2, 2)) pr0m1th3as-datatypes-9450b29/inst/groupfilter.m000066400000000000000000000320561527370255000214010ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{B} =} groupfilter (@var{A}, @var{groupvars}, @var{method}) ## @deftypefnx {datatypes} {@var{B} =} groupfilter (@var{A}, @var{groupvars}, @var{groupbins}, @var{method}) ## @deftypefnx {datatypes} {[@var{B}, @var{BG}] =} groupfilter (@dots{}) ## ## Filter the rows of an array by a per-group condition. ## ## @code{@var{B} = groupfilter (@var{A}, @var{groupvars}, @var{method})} groups ## the rows of the array @var{A} by the grouping variables @var{groupvars}, ## applies the filter function @var{method} to each group, and returns @var{B}, ## the rows of @var{A} that satisfy the condition, in their original order. ## @var{groupvars} is a grouping vector with one element per row of @var{A}, or ## a cell array of such vectors. Rows holding a missing value in a grouping ## variable form their own groups, to which @var{method} is applied like any ## other group. ## ## @var{method} is a function handle applied to each group's slice of every ## column of @var{A}. It must return either a logical scalar, which keeps or ## drops the whole group, or a logical vector with one element per row of the ## group, which keeps or drops the individual rows. A row is kept only when the ## condition holds for it across all columns of @var{A}. ## ## @code{[@var{B}, @var{BG}] = groupfilter (@dots{})} also returns @var{BG}, the ## grouping values of the kept rows. When @var{groupvars} is a single grouping ## vector, @var{BG} is a column vector with one element per kept row; when ## several grouping variables are given, @var{BG} is a cell array with one ## element per grouping variable. ## ## The optional @var{groupbins} argument bins the grouping variables before ## grouping (a vector of bin edges or a positive integer number of bins, or a ## cell array with one scheme per grouping variable); see @code{groupsummary} ## for details. The @qcode{'IncludedEdge'} Name-Value pair (@qcode{'left'} by ## default, or @qcode{'right'}) selects which bin edge is inclusive. ## ## To filter the rows of a @code{table}, call @code{groupfilter (@var{T}, ## @var{groupvars}, @dots{})}, which dispatches to the @code{table} method and ## returns the result as a table. ## ## @seealso{groupsummary, groupcounts, findgroups, splitapply, table} ## @end deftypefn function [B, varargout] = groupfilter (A, groupvars, varargin) if (nargin < 3) print_usage (); endif ## Split off a trailing 'IncludedEdge' Name-Value option, then an optional ## GROUPBINS positional argument that precedes the filter function METHOD. args = varargin; optNames = {'IncludedEdge'}; nvStart = numel (args) + 1; for k = 1:numel (args) a = args{k}; if (((ischar (a) && isrow (a)) || (isa (a, 'string') && isscalar (a))) && any (strcmpi (char (a), optNames))) nvStart = k; break; endif endfor nvArgs = args(nvStart:end); args = args(1:nvStart-1); incEdge = parsePairedArguments (optNames, {'left'}, nvArgs(:)); incEdge = check_included_edge ('groupfilter', incEdge); hasGB = false; groupbins = []; if (! isempty (args) && __groupbins__ ('is_spec', args{1})) hasGB = true; groupbins = args{1}; args = args(2:end); endif if (isempty (args)) print_usage (); endif method = args{1}; if (! is_function_handle (method)) error ("groupfilter: METHOD must be a function handle."); endif if (numel (args) > 1) error ("groupfilter: too many positional arguments."); endif ## Force the grouping variables to a cell array of column vectors. if (iscell (groupvars) && ! (iscellstr (groupvars) && isvector (groupvars))) gvs = groupvars(:)'; else gvs = {groupvars}; endif n = size (A, 1); for j = 1:numel (gvs) g = gvs{j}; if (ischar (g)) if (ndims (g) > 2) error ("groupfilter: grouping variables must be vectors."); endif elseif (isvector (g) || isempty (g)) gvs{j} = g(:); else error ("groupfilter: grouping variables must be vectors."); endif if (size (gvs{j}, 1) != n) error (strcat ("groupfilter: each grouping variable must have one", ... " element per row of A.")); endif endfor ## Bin the grouping variables when a GROUPBINS argument was given. if (hasGB) names = cell (1, numel (gvs)); for j = 1:numel (gvs) names{j} = sprintf ("%d", j); endfor [gvs, ~, errmsg] = __groupbins__ ('bin', gvs, names, groupbins, incEdge, ... 'groupfilter'); if (! isempty (errmsg)) error ("groupfilter: %s", errmsg); endif endif ## Group the rows, treating missing grouping values as their own groups so ## that every row belongs to exactly one group. [Grp, ng, errmsg] = gf_group_rows (gvs); if (! isempty (errmsg)) error ("groupfilter: %s", errmsg); endif ## Build the row keep-mask by applying METHOD to each column of A. dataCols = cell (1, size (A, 2)); for c = 1:size (A, 2) dataCols{c} = A(:,c); endfor [keep, errmsg] = gf_keep_mask (method, dataCols, Grp, ng); if (! isempty (errmsg)) error ("groupfilter: %s", errmsg); endif B = A(keep,:); ## Optional output: the grouping values of the kept rows. if (nargout > 1) if (numel (gvs) == 1) BG = gvs{1}(keep,:); else BG = cell (1, numel (gvs)); for j = 1:numel (gvs) BG{j} = gvs{j}(keep,:); endfor endif varargout{1} = BG; endif endfunction ## Build the row keep-mask by applying the filter function METHOD to each data ## column's per-group slice. DATACOLS is a cell array of column values; G the ## n-by-1 group numbers (1..NG), every row assigned to a group. For each group ## METHOD receives the slice and must return a logical scalar (keep/drop the ## whole group) or a logical vector with one element per group row. The ## per-column masks are combined with logical AND. Returns KEEP (n-by-1 ## logical) and an errmsg body emitted by the caller. Self-contained. function [keep, errmsg] = gf_keep_mask (method, dataCols, G, ng) errmsg = ''; n = numel (G); keep = true (n, 1); for d = 1:numel (dataCols) col = dataCols{d}; for g = 1:ng rows = find (G == g); if (isempty (rows)) continue; endif r = method (col(rows,:)); if (! (islogical (r) || isnumeric (r))) errmsg = "the filter function must return a logical result."; return; endif r = logical (r(:)); if (isscalar (r)) m = repmat (r, numel (rows), 1); elseif (numel (r) == numel (rows)) m = r; else errmsg = strcat ("the filter function must return a logical scalar", ... " or a logical vector with one element per group", ... " row."); return; endif keep(rows) = keep(rows) & m; endfor endfor endfunction ## Group rows by the grouping-variable values GVS (a cell array of column ## vectors), treating each variable's missing values as a single group value so ## that every row is assigned to a group. Returns G (1..NGROUPS), NGROUPS, and ## an errmsg body emitted by the caller. Self-contained: no table dependency. function [G, ngroups, errmsg] = gf_group_rows (gvs) errmsg = ''; G = []; ngroups = 0; n = size (gvs{1}, 1); KEY = []; for j = 1:numel (gvs) [p, m, e] = gf_col_proxy (gvs{j}); if (! isempty (e)) errmsg = e; return; endif pc = p; pc(m,:) = 0; KEY = [KEY, pc, double(m)]; endfor [~, ~, ic] = unique (KEY, "rows"); G = ic; ngroups = max (ic); endfunction ## Build a single-column grouping proxy for one grouping variable COL: a numeric ## matrix P whose value order matches COL's, together with a logical MISS mask ## flagging the missing elements. Returns an errmsg body emitted by the caller. ## Self-contained: no table dependency. function [p, miss, errmsg] = gf_col_proxy (col) p = []; miss = []; errmsg = ''; if (isa (col, 'categorical')) p = double (col)(:); miss = isnan (p); elseif (isa (col, 'string') || iscellstr (col) || ischar (col)) c = cellstr (col); c = c(:); miss = cellfun (@isempty, c); [~, ~, ic] = unique (c); p = ic(:); elseif (isa (col, 'datetime')) DV = datevec (col); nat = any (isnan (DV), 2); DV(nat,:) = 0; DV(nat,2:3) = 1; p = datenum (DV); p(nat) = NaN; p = p(:); miss = isnan (p); elseif (isa (col, 'duration')) p = days (col)(:); miss = isnan (p); elseif (isa (col, 'calendarDuration')) p = proxyArray (col); miss = any (isnan (p), 2); elseif (isnumeric (col) || islogical (col)) p = double (col)(:); miss = isnan (p); else errmsg = sprintf ("unsupported grouping variable type '%s'.", class (col)); endif endfunction ## Validate an 'IncludedEdge' binning option VAL for CALLER, returning it ## lowercased as 'left' or 'right'. Self-contained: no table dependency. function e = check_included_edge (caller, val) if (isa (val, 'string') && isscalar (val)) val = char (val); endif if (! (ischar (val) && isrow (val) ... && any (strcmpi (val, {'left', 'right'})))) error ("%s: 'IncludedEdge' must be 'left' or 'right'.", caller); endif e = lower (val); endfunction %!test %! ## A scalar-returning method keeps or drops whole groups %! g = [1; 1; 1; 2; 2; 3]; %! x = [3; 1; 2; 9; 8; 5]; %! assert_equal (groupfilter (x, g, @(v) numel (v) > 2), [3; 1; 2]); %!test %! ## A vector-returning method keeps individual rows within each group %! d = [1; 1; 1; 1; 2; 2; 2; 2]; %! t = [67; 65; 71; 55; 61; 79; 58; 78]; %! assert_equal (groupfilter (t, d, @(x) x == max (x)), [71; 79]); %!test %! ## Output rows keep their original order, not group order %! g = [2; 1; 2; 1]; %! x = [1; 2; 3; 4]; %! assert_equal (groupfilter (x, g, @(v) numel (v) >= 1), [1; 2; 3; 4]); %!test %! ## The second output returns the grouping values of the kept rows %! d = [1; 1; 1; 1; 2; 2; 2; 2]; %! t = [67; 65; 71; 55; 61; 79; 58; 78]; %! [B, BG] = groupfilter (t, d, @(x) x == max (x)); %! assert_equal (B, [71; 79]); %! assert_equal (BG, [1; 2]); %!test %! ## A matrix groups by its grouping vector; a row is kept only when the %! ## condition holds across all columns of A %! A = [5 1; 1 9; 9 1; 2 9]; %! g = [1; 1; 2; 2]; %! assert_equal (groupfilter (A, g, @(v) v > 3), zeros (0, 2)); %!test %! ## Rows with a missing grouping value form their own group and are filtered %! g = [1; 1; NaN; 2; 2]; %! x = [10; 20; 30; 40; 50]; %! [B, BG] = groupfilter (x, g, @(v) mean (v) > 25); %! assert_equal (B, [30; 40; 50]); %! assert_equal (BG, [NaN; 2; 2]); %!test %! ## Several grouping vectors return BG as a cell array, one per variable %! x = [3; 1; 9; 2; 8]; %! g1 = [1; 1; 2; 2; 1]; %! g2 = {'a'; 'a'; 'b'; 'b'; 'a'}; %! [B, BG] = groupfilter (x, {g1, g2}, @(v) v == max (v)); %! assert_equal (B, [9; 8]); %! assert_equal (BG, {[2; 1], {'b'; 'a'}}); %!test %! ## A categorical grouping vector groups by category %! c = categorical ({'a'; 'b'; 'a'; 'b'; 'a'}); %! x = [1; 5; 2; 6; 9]; %! [B, BG] = groupfilter (x, c, @(v) v == max (v)); %! assert_equal (B, [6; 9]); %! assert_equal (cellstr (BG), {'b'; 'a'}); %!test %! ## A GROUPBINS edge vector filters within bins of a numeric grouping variable %! x = (1:6)'; %! g = [1; 3; 5; 7; 9; 11]; %! B = groupfilter (x, g, [0 6 12], @(v) numel (v) > 2); %! assert_equal (B, (1:6)'); %! B2 = groupfilter (x, g, [0 6 12], @(v) mean (v) > 4, 'IncludedEdge', ... %! 'right'); %! assert_equal (B2, [4; 5; 6]); %!error groupfilter ([1; 2], [1; 2]) %!error ... %! groupfilter ([1; 2], [1; 2], 'sum') %!error ... %! groupfilter ([1; 2], [1; 2], 'month', @(x) x > 0) %!error ... %! groupfilter ([1; 2], [1; 2], @(x) x > 0, 5) %!error ... %! groupfilter ([1; 2; 3], [1; 2], @(x) x > 0) %!error ... %! groupfilter ([1; 1], [1; 1], @(x) [true; true; true]) %!error ... %! groupfilter ([1; 2], repmat ('a', [2, 2, 2]), @(x) x > 0) %!error ... %! groupfilter ([1; 2], ones (2, 2), @(x) x > 0) pr0m1th3as-datatypes-9450b29/inst/groupsummary.m000066400000000000000000000572221527370255000216130ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{B} =} groupsummary (@var{A}, @var{groupvars}, @var{method}) ## @deftypefnx {datatypes} {@var{B} =} groupsummary (@var{A}, @var{groupvars}, @var{groupbins}, @var{method}) ## @deftypefnx {datatypes} {[@var{B}, @var{BG}, @var{BC}] =} groupsummary (@dots{}) ## ## Compute summary statistics by group for an array. ## ## @code{@var{B} = groupsummary (@var{A}, @var{groupvars}, @var{method})} groups ## the rows of the array @var{A} by the grouping variables @var{groupvars} and ## applies @var{method} to each column of @var{A} within each group, returning ## the results in @var{B}, which has one row per group. @var{groupvars} is a ## grouping vector with one element per row of @var{A}, or a cell array of such ## vectors. Groups are the sorted unique combinations of grouping values; rows ## holding a missing value in a grouping variable form their own groups, sorted ## after the non-missing groups. ## ## @var{method} is one of the names @qcode{'sum'}, @qcode{'mean'}, ## @qcode{'median'}, @qcode{'mode'}, @qcode{'var'}, @qcode{'std'}, ## @qcode{'min'}, @qcode{'max'}, @qcode{'range'}, @qcode{'nnz'}, ## @qcode{'nummissing'}, or @qcode{'numunique'}, a function handle, or a cell ## array of method names and@/or function handles. @code{NaN} values are ## omitted for every named method except @qcode{'nummissing'}; a function handle ## receives the values with @code{NaN} included and must return a single row. ## When several methods are requested the columns of @var{B} are ordered by ## column of @var{A} first, then by method. ## ## @code{[@var{B}, @var{BG}, @var{BC}] = groupsummary (@dots{})} also returns ## @var{BG}, the grouping values that identify each group, and @var{BC}, a ## column vector with the number of rows in each group. When @var{groupvars} is ## a single grouping vector, @var{BG} holds its representative value for each ## group; when several grouping variables are given, @var{BG} is a cell array ## with one element per grouping variable. ## ## The optional @var{groupbins} argument bins the grouping variables before ## grouping: a vector of bin edges or a positive integer number of equal-width ## bins, applied to a numeric, datetime, or duration grouping variable, or a ## cell array with one scheme per grouping variable. Each binned variable ## becomes a categorical of bin interval labels. ## ## The behaviour can be modified with the @qcode{'IncludeMissingGroups'} ## (default @code{true}), @qcode{'IncludeEmptyGroups'} (default @code{false}), ## and @qcode{'IncludedEdge'} (default @qcode{'left'}, the inclusive bin edge) ## @var{Name}/@var{Value} pairs, as for the @code{table} method. When ## @qcode{'IncludeEmptyGroups'} is @code{true}, the unused categories of a ## categorical or binned grouping variable contribute empty groups. ## ## To summarise the variables of a @code{table}, call @code{groupsummary ## (@var{T}, @var{groupvars}, @dots{})}, which dispatches to the @code{table} ## method and returns the result as a table. ## ## @seealso{findgroups, splitapply, table} ## @end deftypefn function [B, varargout] = groupsummary (A, groupvars, varargin) if (nargin < 3) print_usage (); endif ## An optional GROUPBINS positional argument precedes the method; the method ## is the next trailing argument, and the rest are Name-Value pairs. args = varargin; hasGB = false; groupbins = []; if (! isempty (args) && __groupbins__ ('is_spec', args{1})) hasGB = true; groupbins = args{1}; args = args(2:end); endif if (isempty (args)) print_usage (); endif method = args{1}; nvArgs = args(2:end); if (mod (numel (nvArgs), 2) != 0) error ("groupsummary: name-value arguments must be in pairs."); endif ## Parse optional paired arguments optNames = {'IncludeMissingGroups', 'IncludeEmptyGroups', 'IncludedEdge'}; dfValues = {true, false, 'left'}; [incMiss, incEmpty, incEdge, nvArgs] = ... parsePairedArguments (optNames, dfValues, nvArgs(:)); if (! isempty (nvArgs)) error ("groupsummary: invalid optional paired argument."); endif if (! (isscalar (incMiss) && (islogical (incMiss) || isnumeric (incMiss)))) error ("groupsummary: 'IncludeMissingGroups' must be a logical scalar."); endif incMiss = logical (incMiss); if (! (isscalar (incEmpty) && (islogical (incEmpty) || isnumeric (incEmpty)))) error ("groupsummary: 'IncludeEmptyGroups' must be a logical scalar."); endif incEmpty = logical (incEmpty); incEdge = check_included_edge ('groupsummary', incEdge); [methods, methNames, errmsg] = gs_normalise_methods (method); if (! isempty (errmsg)) error ("groupsummary: %s", errmsg); endif if (isempty (methods)) error ("groupsummary: a method is required for array input."); endif ## Force the grouping variables to a cell array of column vectors. if (iscell (groupvars) && ! (iscellstr (groupvars) && isvector (groupvars))) gvs = groupvars(:)'; else gvs = {groupvars}; endif n = size (A, 1); for j = 1:numel (gvs) g = gvs{j}; if (ischar (g)) if (ndims (g) > 2) error ("groupsummary: grouping variables must be vectors."); endif elseif (isvector (g) || isempty (g)) gvs{j} = g(:); else error ("groupsummary: grouping variables must be vectors."); endif if (size (gvs{j}, 1) != n) error (strcat ("groupsummary: each grouping variable must have one", ... " element per row of A.")); endif endfor ## Bin the grouping variables when a GROUPBINS argument was given. if (hasGB) names = cell (1, numel (gvs)); for j = 1:numel (gvs) names{j} = sprintf ("%d", j); endfor [gvs, ~, errmsg] = __groupbins__ ('bin', gvs, names, groupbins, incEdge, ... 'groupsummary'); if (! isempty (errmsg)) error ("groupsummary: %s", errmsg); endif endif ## Group the rows, treating missing grouping values as their own groups; ## IncludeEmptyGroups adds the unused categories of a categorical or binned ## grouping variable as empty groups. [Grp, ng, gvals, errmsg] = gb_grouping (gvs, incMiss, incEmpty); if (! isempty (errmsg)) error ("groupsummary: %s", errmsg); endif ## Compute each method over each column of A (column first, then method). ncol = size (A, 2); B = []; for c = 1:ncol for mi = 1:numel (methods) vals = cell (ng, 1); for g = 1:ng rows = (Grp == g); [v, errmsg] = gs_apply_method (methods{mi}, A(rows,c)); if (! isempty (errmsg)) error ("groupsummary: column %d of A: %s", c, errmsg); endif vals{g} = v; endfor B = [B, vertcat(vals{:})]; endfor endfor ## Optional outputs: the grouping identifiers and the group counts. if (nargout > 1) if (numel (gvs) == 1) BG = gvals{1}; else BG = gvals; endif varargout{1} = BG; endif if (nargout > 2) varargout{2} = accumarray (Grp(! isnan (Grp)), 1, [ng, 1]); endif endfunction ## Normalise the METHOD argument into a cell array of method specs METHODS (each ## a method-name char vector or a function handle) and a parallel cell array of ## display names METHNAMES. Returns an errmsg body emitted by the caller. ## Self-contained: no table dependency. function [methods, methNames, errmsg] = gs_normalise_methods (method) methods = {}; methNames = {}; errmsg = ''; if (isempty (method) && ! iscell (method) && ! ischar (method) && ! is_function_handle (method)) return; endif if (is_function_handle (method) || (ischar (method) && isrow (method)) || isa (method, 'string')) items = {method}; elseif (iscell (method)) items = method(:)'; else errmsg = strcat ("METHOD must be a method name, a function handle, or", ... " a", ... " cell array of method names and function handles."); return; endif known = {'sum', 'mean', 'median', 'mode', 'var', 'std', 'min', 'max', ... 'range', 'nnz', 'nummissing', 'numunique'}; nfun = 0; for k = 1:numel (items) it = items{k}; if (is_function_handle (it)) nfun++; methods{end+1} = it; methNames{end+1} = sprintf ("fun%d", nfun); elseif ((ischar (it) && isrow (it)) || (isa (it, ... 'string') && isscalar (it))) nm = lower (char (it)); if (! any (strcmp (nm, known))) errmsg = sprintf ("'%s' is not a supported method name.", char (it)); return; endif methods{end+1} = nm; methNames{end+1} = nm; else errmsg = strcat ("each method must be a method name or a function", ... " handle."); return; endif endfor endfunction ## Group rows by the grouping-variable values GVS (a cell array of column ## vectors), treating each variable's missing values as a single group value. ## Returns G (1..NGROUPS), NGROUPS, REPROWS (a representative row per group), ## and an errmsg body. Groups are sorted by value with missing groups last; ## when INCMISS is false the rows with a missing grouping value are dropped. function [G, ngroups, repRows, errmsg] = gs_group_rows (gvs, incMiss) errmsg = ''; G = []; ngroups = 0; repRows = []; n = size (gvs{1}, 1); KEY = []; SORT = []; anyMiss = false (n, 1); for j = 1:numel (gvs) [p, m, e] = group_col_proxy (gvs{j}); if (! isempty (e)) errmsg = e; return; endif pc = p; pc(m,:) = 0; KEY = [KEY, pc, double(m)]; sp = p; sp(m,:) = Inf; SORT = [SORT, sp]; anyMiss = anyMiss | m; endfor [~, ia, ic] = unique (KEY, "rows"); ng = numel (ia); grpMiss = anyMiss(ia); [~, ord] = sortrows (SORT(ia,:)); reps = ia(ord); grpMiss = grpMiss(ord); pos = zeros (ng, 1); pos(ord) = 1:ng; G = pos(ic); if (! incMiss && any (grpMiss)) keep = find (! grpMiss); newId = NaN (ng, 1); newId(keep) = 1:numel (keep); G = newId(G); reps = reps(keep); ng = numel (keep); endif ngroups = ng; repRows = reps; endfunction ## Build a single-column grouping proxy for one grouping variable COL: a numeric ## matrix P whose sort order matches COL's value order, together with a logical ## MISS mask flagging the missing elements. Returns an errmsg body emitted by ## the caller. Self-contained: no table dependency. function [p, miss, errmsg] = group_col_proxy (col) p = []; miss = []; errmsg = ''; if (isa (col, 'categorical')) ## Categorical groups follow category order (ordinal or reordered), which ## the underlying category codes encode; maps to NaN. p = double (col)(:); miss = isnan (p); elseif (isa (col, 'string') || iscellstr (col) || ischar (col)) c = cellstr (col); c = c(:); miss = cellfun (@isempty, c); [~, ~, ic] = unique (c); p = ic(:); elseif (isa (col, 'datetime')) DV = datevec (col); nat = any (isnan (DV), 2); DV(nat,:) = 0; DV(nat,2:3) = 1; p = datenum (DV); p(nat) = NaN; p = p(:); miss = isnan (p); elseif (isa (col, 'duration')) p = days (col)(:); miss = isnan (p); elseif (isa (col, 'calendarDuration')) p = proxyArray (col); miss = any (isnan (p), 2); elseif (isnumeric (col) || islogical (col)) p = double (col)(:); miss = isnan (p); else errmsg = sprintf ("unsupported grouping variable type '%s'.", class (col)); endif endfunction ## Apply a single method M (a method-name char vector or a function handle) to ## the column slice X of one group, returning a row result V. Named methods ## omit missing values (except 'nummissing'); a function handle receives X ## unchanged and must return a single row. Returns an errmsg body emitted by ## the caller. function [v, errmsg] = gs_apply_method (m, x) v = []; errmsg = ''; if (is_function_handle (m)) v = m (x); if (size (v, 1) != 1) errmsg = "a function handle method must return a single row."; endif return; endif if (strcmp (m, 'nummissing')) v = sum (gs_missing_mask (x), 1); return; endif if (strcmp (m, 'numunique')) miss = gs_missing_mask (x); if (size (x, 2) == 1) v = numel (unique (x(! miss,:))); else v = zeros (1, size (x, 2)); for c = 1:size (x, 2) col = x(:,c); v(c) = numel (unique (col(! miss(:,c)))); endfor endif return; endif if (! (isnumeric (x) || islogical (x))) errmsg = sprintf (strcat ("named method '%s' is not supported for data", ... " of type '%s'; use a function handle."), m, ... class (x)); return; endif x = double (x); ## An empty group (e.g. an unused IncludeEmptyGroups category) takes the ## method's empty value: 0 for the additive 'sum'/'nnz', NaN otherwise. if (rows (x) == 0) if (any (strcmp (m, {'sum', 'nnz'}))) v = zeros (1, columns (x)); else v = NaN (1, columns (x)); endif return; endif nan = isnan (x); cnt = sum (! nan, 1); z = x; z(nan) = 0; switch (m) case 'sum' v = sum (z, 1); case 'mean' v = sum (z, 1) ./ cnt; case 'min' v = min (x, [], 1); case 'max' v = max (x, [], 1); case 'range' v = max (x, [], 1) - min (x, [], 1); case 'nnz' v = sum (x != 0 & ! nan, 1); case {'median', 'mode', 'var', 'std'} v = NaN (1, size (x, 2)); for c = 1:size (x, 2) col = x(! nan(:,c), c); if (! isempty (col)) switch (m) case 'median' v(c) = median (col); case 'mode' v(c) = mode (col); case 'var' v(c) = var (col); case 'std' v(c) = std (col); endswitch endif endfor endswitch endfunction ## Return a logical mask the size of X flagging its missing elements. Supports ## the numeric, logical, text, datetime, duration, calendarDuration, and ## categorical types. Self-contained: no table dependency. function mask = gs_missing_mask (x) if (isa (x, 'datetime')) DV = datevec (x); mask = any (isnan (DV), 2); mask = reshape (mask, size (x, 1), size (x, 2)); elseif (isa (x, 'duration')) mask = isnan (days (x)); elseif (isa (x, 'calendarDuration')) mask = any (isnan (proxyArray (x)), 2); elseif (isa (x, 'categorical') || isa (x, 'string')) mask = ismissing (x); elseif (iscellstr (x)) mask = cellfun (@isempty, x); elseif (islogical (x)) mask = false (size (x)); elseif (isnumeric (x)) mask = isnan (x); else mask = false (size (x)); endif endfunction ## Validate an 'IncludedEdge' binning option VAL for CALLER, returning it ## lowercased as 'left' or 'right'. Self-contained: no table dependency. function e = check_included_edge (caller, val) if (isa (val, 'string') && isscalar (val)) val = char (val); endif if (! (ischar (val) && isrow (val) ... && any (strcmpi (val, {'left', 'right'})))) error ("%s: 'IncludedEdge' must be 'left' or 'right'.", caller); endif e = lower (val); endfunction ## Group rows by the grouping-variable values GVS (already binned when a ## GROUPBINS argument was given). Returns G (1..NG, NaN for an excluded row), ## NG, GVALS (a 1-by-K cell of the typed level-value columns, one per group), ## and an errmsg body. When INCEMPTY is true the unused categories of a ## categorical or binned grouping variable contribute empty groups, built from ## the full Cartesian product of the per-variable levels; otherwise only the ## observed groups are returned. function [G, ng, gvals, errmsg] = gb_grouping (gvs, incMiss, incEmpty) errmsg = ''; G = []; ng = 0; gvals = {}; K = numel (gvs); n = size (gvs{1}, 1); if (! incEmpty) [G, ng, repRows, errmsg] = gs_group_rows (gvs, incMiss); if (! isempty (errmsg)) return; endif gvals = cell (1, K); for j = 1:K gvals{j} = gvs{j}(repRows, :); endfor return; endif idxAll = NaN (n, K); levVals = cell (1, K); sizes = zeros (1, K); for j = 1:K [idx, lv, errmsg] = gb_levels (gvs{j}, incMiss); if (! isempty (errmsg)) return; endif idxAll(:,j) = idx; levVals{j} = lv; sizes(j) = size (lv, 1); endfor ng = prod (sizes); lvlOf = ones (ng, K); period = 1; for j = K:-1:1 lvlOf(:,j) = mod (floor ((0:ng-1)' / period), sizes(j)) + 1; period = period * sizes(j); endfor valid = all (! isnan (idxAll), 2); lin = zeros (n, 1); period = 1; for j = K:-1:1 col = idxAll(:,j); col(isnan (col)) = 1; lin = lin + (col - 1) * period; period = period * sizes(j); endfor G = NaN (n, 1); G(valid) = lin(valid) + 1; gvals = cell (1, K); for j = 1:K gvals{j} = levVals{j}(lvlOf(:,j), :); endfor endfunction ## Build the level structure of one grouping variable COL for empty-group-aware ## grouping: IDX (n-by-1 level index, NaN for an excluded row), LEVVALS (a typed ## column of one representative value per level), and an errmsg body. A ## categorical uses its full category order (so unused categories are levels); a ## missing value forms one extra level, sorted last, when INCMISS is true. function [idx, levVals, errmsg] = gb_levels (col, incMiss) idx = []; levVals = []; errmsg = ''; n = size (col, 1); [p, miss, errmsg] = group_col_proxy (col); if (! isempty (errmsg)) return; endif if (isa (col, 'categorical')) cats = categories (col); L = numel (cats); idx = double (col)(:); levVals = categorical (cats(:), cats, 'Ordinal', isordinal (col)); else idx = NaN (n, 1); keep = find (! miss); if (isempty (keep)) levVals = col([], :); L = 0; else [~, ia, ic] = unique (p(keep,:), "rows"); idx(keep) = ic; levVals = col(keep(ia), :); L = numel (ia); endif endif if (any (miss)) if (incMiss) L = L + 1; idx(miss) = L; mrow = find (miss, 1); levVals = [levVals; col(mrow, :)]; else idx(miss) = NaN; endif endif endfunction %!assert_equal (groupsummary ([10; 20; 30; 40], [1; 1; 2; 2], 'mean'), [15; 35]) %!assert_equal (groupsummary ([10; 20; 30; 40], [1; 1; 2; 2], 'sum'), [30; 70]) %!test %! ## Multiple outputs return the group identifiers and the group counts %! [B, BG, BC] = groupsummary ([10; 20; 30; 40], [1; 1; 2; 2], 'mean'); %! assert_equal (B, [15; 35]); %! assert_equal (BG, [1; 2]); %! assert_equal (BC, [2; 2]); %!test %! ## A matrix is summarised column by column %! assert_equal (groupsummary ([10 1; 20 2; 30 3; 40 4], [1; 1; 2; 2], ... %! 'sum'), ... %! [30, 3; 70, 7]); %!test %! ## Several methods order the columns of B by data column first, then method %! B = groupsummary ([10; 20; 30; 40], [1; 1; 2; 2], {'mean', 'sum'}); %! assert_equal (B, [15, 30; 35, 70]); %!test %! ## Named methods omit NaN in the data %! assert_equal (groupsummary ([10; 20; NaN; 40], [1; 1; 2; 2], 'mean'), ... %! [15; 40]); %!test %! ## A missing value in a grouping variable forms its own group, sorted last %! [B, BG, BC] = groupsummary ([10; 20; 30; 40], [1; 1; NaN; 2], 'mean'); %! assert_equal (B, [15; 40; 30]); %! assert_equal (BG, [1; 2; NaN]); %! assert_equal (BC, [2; 1; 1]); %!test %! ## 'IncludeMissingGroups' false drops the rows with a missing grouping value %! B = groupsummary ([10; 20; 30; 40], [1; 1; NaN; 2], 'mean', ... %! 'IncludeMissingGroups', false); %! assert_equal (B, [15; 40]); %!test %! ## A function handle is applied to each group's slice %! B = groupsummary ([10; 20; 30; 40], [1; 1; 2; 2], @(v) max (v) - min (v)); %! assert_equal (B, [10; 10]); %!test %! ## Several grouping vectors return BG as a cell array, one per variable %! [B, BG, BC] = groupsummary ([10; 20; 30; 40; 50], ... %! {[1; 1; 2; 2; 1], ... %! {'b'; 'a'; 'a'; 'b'; 'b'}}, 'sum'); %! assert_equal (B, [20; 60; 30; 40]); %! assert_equal (BG, {[1; 1; 2; 2], {'a'; 'b'; 'a'; 'b'}}); %! assert_equal (BC, [1; 2; 1; 1]); %!test %! ## A categorical grouping vector groups by category order, not alphabetically %! c = categorical ({'medium'; 'low'; 'high'; 'low'}, ... %! {'low', 'medium', 'high'}, 'Ordinal', true); %! [B, BG, BC] = groupsummary ((1:4)', c, 'sum'); %! assert_equal (B, [6; 1; 3]); %! assert_equal (cellstr (BG), {'low'; 'medium'; 'high'}); %! assert_equal (BC, [2; 1; 1]); %!test %! ## A GROUPBINS edge vector groups a numeric variable by bin interval %! [B, BG, BC] = groupsummary ((1:6)', [1; 3; 5; 7; 9; 11], [0 6 12], 'sum'); %! assert_equal (B, [6; 15]); %! assert_equal (iscategorical (BG), true); %! assert_equal (cellstr (BG), {'[0, 6)'; '[6, 12]'}); %! assert_equal (BC, [3; 3]); %!test %! ## A GROUPBINS number of bins makes equal-width bins over the data range %! [B, BG] = groupsummary ((1:4)', [1; 2; 3; 4], 3, 'sum'); %! assert_equal (cellstr (BG), {'[1, 2)'; '[2, 3)'; '[3, 4]'}); %! assert_equal (B, [1; 2; 7]); %!test %! ## 'IncludedEdge' 'right' makes the right bin edge the inclusive one %! [B, BG, BC] = groupsummary ((1:3)', [0; 6; 12], [0 6 12], 'sum', ... %! 'IncludedEdge', 'right'); %! assert_equal (cellstr (BG), {'[0, 6]'; '(6, 12]'}); %! assert_equal (BC, [2; 1]); %!test %! ## 'IncludeEmptyGroups' adds unused categorical categories as empty groups %! c = categorical ({'a'; 'a'; 'b'}, {'a', 'b', 'c'}); %! [B, BG, BC] = groupsummary ([10; 20; 30], c, 'sum', ... %! 'IncludeEmptyGroups', true); %! assert_equal (cellstr (BG), {'a'; 'b'; 'c'}); %! assert_equal (B, [30; 30; 0]); %! assert_equal (BC, [2; 1; 0]); %!error groupsummary ([1; 2], [1; 2]) %!error ... %! groupsummary ([1; 2], [1; 2], 'bogus') %!error ... %! groupsummary ([1; 2], [1; 2], 2, 'sum', 'IncludedEdge', 'mid') %!error ... %! groupsummary ([1; 2], [1; 2], 'month', 'sum') %!error ... %! groupsummary ([1; 2; 3], [1; 2], 'sum') %!error ... %! groupsummary ([1; 2], [1; 2], 'mean', 'IncludeMissingGroups', [1, 2]) %!error ... %! groupsummary ([1; 2], [1; 2], 'mean', 'IncludeEmptyGroups', [1, 2]) %!error ... %! groupsummary ([1; 2], [1; 2], 'mean', 'Bogus', 1) %!error ... %! groupsummary ([1; 2], [1; 2], 'mean', 'Bogus') %!error ... %! groupsummary ([1; 2], [1; 2], {}) %!error ... %! groupsummary ([1; 2], repmat ('a', [2, 2, 2]), 'mean') %!error ... %! groupsummary ([1; 2], ones (2, 2, 2), 'mean') %!error ... %! groupsummary ({'a'; 'b'}, [1; 2], 'mean') pr0m1th3as-datatypes-9450b29/inst/grouptransform.m000066400000000000000000000422711527370255000221270ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{B} =} grouptransform (@var{A}, @var{groupvars}, @var{method}) ## @deftypefnx {datatypes} {@var{B} =} grouptransform (@var{A}, @var{groupvars}, @var{groupbins}, @var{method}) ## @deftypefnx {datatypes} {[@var{B}, @var{BG}] =} grouptransform (@dots{}) ## @deftypefnx {datatypes} {[@dots{}] =} grouptransform (@dots{}, @var{Name}, @var{Value}) ## ## Transform the columns of an array group by group. ## ## @code{@var{B} = grouptransform (@var{A}, @var{groupvars}, @var{method})} ## groups the rows of the array @var{A} by the grouping variables ## @var{groupvars}, applies @var{method} to each column of @var{A} within each ## group, and returns @var{B}, the transformed values, one row per row of ## @var{A} and in the original order. @var{groupvars} is a grouping vector with ## one element per row of @var{A}, or a cell array of such vectors. Rows ## holding a missing value in a grouping variable form their own groups, which ## are transformed like any other group. ## ## @var{method} is one of the names @qcode{'zscore'}, @qcode{'norm'}, ## @qcode{'meancenter'}, @qcode{'rescale'}, @qcode{'meanfill'}, ## @qcode{'linearfill'}, or a function handle. @qcode{'zscore'} centers and ## scales each group to zero mean and unit standard deviation; @qcode{'norm'} ## divides by the group 2-norm; @qcode{'meancenter'} subtracts the group mean; ## @qcode{'rescale'} rescales to the range @code{[0, 1]}; @qcode{'meanfill'} ## replaces missing values with the group mean; and @qcode{'linearfill'} fills ## missing values by linear interpolation within the group (leaving leading and ## trailing missing values unchanged). For the named methods @code{NaN} values ## are omitted when computing the group statistics. A function handle is ## applied to each group's column slice and must return either a single row ## (broadcast) or one row per row of the group. ## ## @code{[@var{B}, @var{BG}] = grouptransform (@dots{})} also returns @var{BG}, ## the grouping values of each row. When @var{groupvars} is a single grouping ## vector, @var{BG} is that vector; when several grouping variables are given, ## @var{BG} is a cell array with one element per grouping variable. ## ## The @qcode{'ReplaceValues'} @var{Name}/@var{Value} pair (default @code{true}) ## controls whether the transformed values replace the columns of @var{A} or, ## when @code{false}, are appended to them. The optional @var{groupbins} ## argument bins the grouping variables before grouping (a vector of bin edges ## or a positive integer number of bins, or a cell array with one scheme per ## grouping variable); see @code{groupsummary} for details. The ## @qcode{'IncludedEdge'} pair (@qcode{'left'} by default, or @qcode{'right'}) ## selects the inclusive bin edge. ## ## To transform the variables of a @code{table}, call @code{grouptransform ## (@var{T}, @var{groupvars}, @dots{})}, which dispatches to the @code{table} ## method and returns the result as a table. ## ## @seealso{groupsummary, groupfilter, findgroups, splitapply, table} ## @end deftypefn function [B, varargout] = grouptransform (A, groupvars, varargin) if (nargin < 3) print_usage (); endif ## An optional GROUPBINS positional argument precedes the transform METHOD (a ## known method name or a function handle). args = varargin; hasGB = false; groupbins = []; if (! isempty (args) && __groupbins__ ('is_spec', args{1})) hasGB = true; groupbins = args{1}; args = args(2:end); endif if (isempty (args)) print_usage (); endif method = args{1}; knownMethods = {'zscore', 'norm', 'meancenter', 'rescale', ... 'meanfill', 'linearfill'}; if (is_function_handle (method)) ## ok elseif (((ischar (method) && isrow (method)) || (isa (method, 'string') && isscalar (method))) && any (strcmpi (char (method), knownMethods))) method = lower (char (method)); else error (strcat ("grouptransform: METHOD must be one of 'zscore',", ... " 'norm',", ... " 'meancenter', 'rescale', 'meanfill', 'linearfill', or", ... " a", ... " function handle.")); endif ## Anything after METHOD must be the 'ReplaceValues' or 'IncludedEdge' ## Name-Value pair (the array form has no DATAVARS argument; A's columns are ## the data). rest = args(2:end); optNames = {'ReplaceValues', 'IncludedEdge'}; nvStart = numel (rest) + 1; for k = 1:numel (rest) a = rest{k}; if (((ischar (a) && isrow (a)) || (isa (a, 'string') && isscalar (a))) && any (strcmpi (char (a), optNames))) nvStart = k; break; endif endfor if (nvStart > 1) error ("grouptransform: too many positional arguments."); endif dfValues = {true, 'left'}; [replaceVals, incEdge] = parsePairedArguments (optNames, dfValues, rest(:)); if (! (isscalar (replaceVals) && (islogical (replaceVals) || isnumeric (replaceVals)))) error ("grouptransform: 'ReplaceValues' must be a logical scalar."); endif replaceVals = logical (replaceVals); incEdge = check_included_edge ('grouptransform', incEdge); ## Force the grouping variables to a cell array of column vectors. if (iscell (groupvars) && ! (iscellstr (groupvars) && isvector (groupvars))) gvs = groupvars(:)'; else gvs = {groupvars}; endif n = size (A, 1); for j = 1:numel (gvs) g = gvs{j}; if (ischar (g)) if (ndims (g) > 2) error ("grouptransform: grouping variables must be vectors."); endif elseif (isvector (g) || isempty (g)) gvs{j} = g(:); else error ("grouptransform: grouping variables must be vectors."); endif if (size (gvs{j}, 1) != n) error (strcat ("grouptransform: each grouping variable must have one", ... " element per row of A.")); endif endfor ## Bin the grouping variables when a GROUPBINS argument was given. if (hasGB) names = cell (1, numel (gvs)); for j = 1:numel (gvs) names{j} = sprintf ("%d", j); endfor [gvs, ~, errmsg] = __groupbins__ ('bin', gvs, names, groupbins, incEdge, ... 'grouptransform'); if (! isempty (errmsg)) error ("grouptransform: %s", errmsg); endif endif ## Group the rows, treating missing grouping values as their own groups so ## that every row belongs to exactly one group. [Grp, ng, errmsg] = gt_group_rows (gvs); if (! isempty (errmsg)) error ("grouptransform: %s", errmsg); endif ## Transform the columns of A, group by group. [trans, errmsg] = gt_transform_col (method, A, Grp, ng); if (! isempty (errmsg)) error ("grouptransform: %s", errmsg); endif if (replaceVals) B = trans; else B = [A, trans]; endif ## Optional output: the grouping values of each row. if (nargout > 1) if (numel (gvs) == 1) BG = gvs{1}; else BG = gvs; endif varargout{1} = BG; endif endfunction ## Transform one data column COL (n-by-k) group by group, applying METHOD (a ## transform-name char vector or a function handle) to each group's slice and ## returning OUT the same size as COL. G is the n-by-1 group-number vector ## (1..NG), every row assigned to a group. A function handle must return a ## single row (broadcast) or one row per group row. Returns an errmsg body ## emitted by the caller. Self-contained: no table dependency. function [out, errmsg] = gt_transform_col (method, col, G, ng) out = []; errmsg = ''; if (! (isnumeric (col) || islogical (col))) errmsg = sprintf (strcat ("grouptransform requires numeric or logical", ... " data; got '%s'."), class (col)); return; endif x = double (col); out = x; for g = 1:ng rows = find (G == g); if (isempty (rows)) continue; endif slice = x(rows,:); if (is_function_handle (method)) r = method (slice); if (! (isnumeric (r) || islogical (r))) errmsg = "the transform function must return a numeric result."; out = []; return; endif if (size (r, 1) == 1) r = repmat (r, numel (rows), 1); endif if (! isequal (size (r), size (slice))) errmsg = strcat ("the transform function must return a result the", ... " same size as the group, or a single row."); out = []; return; endif out(rows,:) = r; else for c = 1:columns (slice) out(rows,c) = gt_apply_named (method, slice(:,c)); endfor endif endfor endfunction ## Apply a single named transform METHOD to the column vector X, returning the ## transformed values V the same size as X. NaN values are omitted when ## computing the group statistics; the centring and scaling methods leave NaN in ## place, while 'meanfill'/'linearfill' fill them. Self-contained. function v = gt_apply_named (method, x) nan = isnan (x); xo = x(! nan); switch (method) case 'meancenter' v = x - mean (xo); case 'zscore' v = (x - mean (xo)) / std (xo); case 'norm' v = x / norm (xo); case 'rescale' mn = min (xo); mx = max (xo); v = (x - mn) / (mx - mn); case 'meanfill' v = x; v(nan) = mean (xo); case 'linearfill' v = gt_linearfill (x); endswitch endfunction ## Fill the missing values of the column vector X by linear interpolation over ## the non-missing positions, leaving leading and trailing missing values (and ## any group with fewer than two non-missing values) unchanged. Self-contained. function v = gt_linearfill (x) v = x; idx = find (! isnan (x)); if (numel (idx) >= 2) pos = (1:numel (x))'; vi = interp1 (idx, x(idx), pos, "linear"); fill = isnan (x) & pos > idx(1) & pos < idx(end); v(fill) = vi(fill); endif endfunction ## Group rows by the grouping-variable values GVS (a cell array of column ## vectors), treating each variable's missing values as a single group value so ## that every row is assigned to a group. Returns G (1..NGROUPS), NGROUPS, and ## an errmsg body emitted by the caller. Self-contained: no table dependency. function [G, ngroups, errmsg] = gt_group_rows (gvs) errmsg = ''; G = []; ngroups = 0; KEY = []; for j = 1:numel (gvs) [p, m, e] = gt_col_proxy (gvs{j}); if (! isempty (e)) errmsg = e; return; endif pc = p; pc(m,:) = 0; KEY = [KEY, pc, double(m)]; endfor [~, ~, ic] = unique (KEY, "rows"); G = ic; ngroups = max (ic); endfunction ## Build a single-column grouping proxy for one grouping variable COL: a numeric ## matrix P whose value order matches COL's, together with a logical MISS mask ## flagging the missing elements. Returns an errmsg body emitted by the caller. ## Self-contained: no table dependency. function [p, miss, errmsg] = gt_col_proxy (col) p = []; miss = []; errmsg = ''; if (isa (col, 'categorical')) p = double (col)(:); miss = isnan (p); elseif (isa (col, 'string') || iscellstr (col) || ischar (col)) c = cellstr (col); c = c(:); miss = cellfun (@isempty, c); [~, ~, ic] = unique (c); p = ic(:); elseif (isa (col, 'datetime')) DV = datevec (col); nat = any (isnan (DV), 2); DV(nat,:) = 0; DV(nat,2:3) = 1; p = datenum (DV); p(nat) = NaN; p = p(:); miss = isnan (p); elseif (isa (col, 'duration')) p = days (col)(:); miss = isnan (p); elseif (isa (col, 'calendarDuration')) p = proxyArray (col); miss = any (isnan (p), 2); elseif (isnumeric (col) || islogical (col)) p = double (col)(:); miss = isnan (p); else errmsg = sprintf ("unsupported grouping variable type '%s'.", class (col)); endif endfunction ## Validate an 'IncludedEdge' binning option VAL for CALLER, returning it ## lowercased as 'left' or 'right'. Self-contained: no table dependency. function e = check_included_edge (caller, val) if (isa (val, 'string') && isscalar (val)) val = char (val); endif if (! (ischar (val) && isrow (val) ... && any (strcmpi (val, {'left', 'right'})))) error ("%s: 'IncludedEdge' must be 'left' or 'right'.", caller); endif e = lower (val); endfunction %!test %! ## 'meancenter' subtracts the group mean %! g = [1; 1; 1; 2; 2]; %! x = [10; 20; 30; 40; 60]; %! assert_equal (grouptransform (x, g, 'meancenter'), [-10; 0; 10; -10; 10]); %!test %! ## 'zscore', 'norm', and 'rescale' spot checks %! g = [1; 1; 1; 2; 2]; %! x = [10; 20; 30; 40; 60]; %! assert_equal (grouptransform (x, g, 'zscore'), ... %! [-1; 0; 1; -1/sqrt(2); 1/sqrt(2)], 8 * eps); %! assert_equal (grouptransform (x, g, 'norm'), ... %! [[10; 20; 30] ./ norm([10; 20; 30]); ... %! [40; 60] ./ norm([40; 60])], 8 * eps); %! assert_equal (grouptransform (x, g, 'rescale'), [0; 0.5; 1; 0; 1]); %!test %! ## 'meanfill' replaces missing values with the group mean %! g = [1; 1; 2; 2]; %! x = [10; NaN; 30; 40]; %! assert_equal (grouptransform (x, g, 'meanfill'), [10; 10; 30; 40]); %!test %! ## 'linearfill' interpolates interior missing values, leaving the edges NaN %! g = [1; 1; 1; 1; 2; 2; 2]; %! x = [1; NaN; NaN; 4; NaN; 10; NaN]; %! assert_equal (grouptransform (x, g, 'linearfill'), ... %! [1; 2; 3; 4; NaN; 10; NaN]); %!test %! ## A function handle is applied to each group's slice %! g = [1; 1; 1; 2; 2]; %! x = [10; 20; 30; 40; 60]; %! assert_equal (grouptransform (x, g, @(v) v - mean (v)), ... %! [-10; 0; 10; -10; 10]); %!test %! ## The second output returns the grouping values of each row %! g = [1; 1; 2; 2]; %! x = [10; 20; 30; 40]; %! [B, BG] = grouptransform (x, g, 'norm'); %! assert_equal (B, ... %! [[10; 20] ./ norm([10; 20]); [30; 40] ./ norm([30; 40])], ... %! 8 * eps); %! assert_equal (BG, [1; 1; 2; 2]); %!test %! ## A matrix transforms each column independently per group %! A = [10 1; 20 2; 30 3; 40 4]; %! g = [1; 1; 2; 2]; %! assert_equal (grouptransform (A, g, 'meancenter'), ... %! [-5 -0.5; 5 0.5; -5 -0.5; 5 0.5]); %!test %! ## 'ReplaceValues' false appends the transformed columns to A %! g = [1; 1; 1; 2; 2]; %! x = [10; 20; 30; 40; 60]; %! B = grouptransform (x, g, 'meancenter', 'ReplaceValues', false); %! assert_equal (B, [10 -10; 20 0; 30 10; 40 -10; 60 10]); %!test %! ## Rows with a missing grouping value form their own group, in original order %! g = [1; 1; NaN; 2; 2]; %! x = [10; 20; 30; 40; 60]; %! [B, BG] = grouptransform (x, g, 'meancenter'); %! assert_equal (B, [-5; 5; 0; -10; 10]); %! assert_equal (BG, [1; 1; NaN; 2; 2]); %!test %! ## Several grouping vectors return BG as a cell array, one per variable %! x = [10; 20; 30; 40; 60]; %! g1 = [1; 1; 2; 2; 1]; %! g2 = {'a'; 'a'; 'b'; 'b'; 'a'}; %! [B, BG] = grouptransform (x, {g1, g2}, 'meancenter'); %! assert_equal (B, [-20; -10; -5; 5; 30]); %! assert_equal (BG, {[1; 1; 2; 2; 1], {'a'; 'a'; 'b'; 'b'; 'a'}}); %!test %! ## A GROUPBINS edge vector bins the grouping variable before transforming %! x = [10; 20; 30; 40]; %! v = [1; 2; 3; 4]; %! [B, BG] = grouptransform (x, v, [0 2.5 5], 'meancenter'); %! assert_equal (B, [-5; 5; -5; 5]); %! assert_equal (iscategorical (BG), true); %! assert_equal (cellstr (BG), ... %! {'[0, 2.5)'; '[0, 2.5)'; '[2.5, 5]'; '[2.5, 5]'}); %!test %! ## A GROUPBINS number of bins makes equal-width bins over the data range %! x = [10; 20; 30; 40]; %! v = [1; 2; 3; 4]; %! B = grouptransform (x, v, 2, 'meancenter'); %! assert_equal (B, [-5; 5; -5; 5]); %!error grouptransform ([1; 2], [1; 2]) %!error ... %! grouptransform ([1; 2], [1; 2], 'bogus') %!error ... %! grouptransform ([1; 2], [1; 2], 2, 'zscore', 'IncludedEdge', 'mid') %!error ... %! grouptransform ([1; 2], [1; 2], 'month', 'meancenter') %!error ... %! grouptransform ([1; 2], [1; 2], 'zscore', 'ReplaceValues', 'yes') %!error ... %! grouptransform ([1; 2], [1; 2], 'zscore', 5) %!error ... %! grouptransform ([1; 2; 3], [1; 2], 'zscore') %!error ... %! grouptransform ([1; 2], repmat ('a', [2, 2, 2]), 'zscore') %!error ... %! grouptransform ([1; 2], ones (2, 2), 'zscore') pr0m1th3as-datatypes-9450b29/inst/histcounts.m000066400000000000000000000531201527370255000212350ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{N} =} histcounts (@var{X}) ## @deftypefnx {datatypes} {@var{N} =} histcounts (@var{X}, @var{nbins}) ## @deftypefnx {datatypes} {@var{N} =} histcounts (@var{X}, @var{edges}) ## @deftypefnx {datatypes} {@var{N} =} histcounts (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {datatypes} {[@var{N}, @var{edges}] =} histcounts (@dots{}) ## @deftypefnx {datatypes} {[@var{N}, @var{edges}, @var{bin}] =} histcounts (@dots{}) ## ## Histogram bin counts. ## ## @code{@var{N} = histcounts (@var{X})} partitions the values of @var{X} into ## bins chosen automatically and returns the number of elements in each bin as a ## row vector. @var{X} is treated as @code{@var{X}(:)} regardless of its shape, ## and @qcode{NaN} and infinite values are excluded. ## ## @code{@var{N} = histcounts (@var{X}, @var{nbins})} uses @var{nbins} bins, and ## @code{@var{N} = histcounts (@var{X}, @var{edges})} uses the bin edges given ## in the monotonically non-decreasing vector @var{edges}. Bin @math{j} covers ## @code{[@var{edges}(j), @var{edges}(j+1))}, except the last bin, which is ## closed at both ends. ## ## @code{[@var{N}, @var{edges}, @var{bin}] = histcounts (@dots{})} also returns ## the bin edges and, in @var{bin}, the index of the bin each element of @var{X} ## belongs to. @var{bin} has the same size as @var{X} and holds @math{0} for ## elements that fall in no bin. Note this differs from @code{discretize}, ## which returns @qcode{NaN} for such elements. ## ## The following @var{Name}, @var{Value} pairs are supported. When more than ## one of @qcode{'NumBins'}, @qcode{'BinWidth'}, @qcode{'BinEdges'} and ## @qcode{'BinMethod'} is given, the last one specified takes effect. ## ## @multitable @columnfractions 0.2 0.8 ## @headitem Name @tab Value ## ## @item @qcode{'NumBins'} @tab A positive integer scalar giving the number of ## bins, equivalent to the @var{nbins} syntax. ## ## @item @qcode{'BinEdges'} @tab A vector of bin edges, equivalent to the ## @var{edges} syntax. ## ## @item @qcode{'BinWidth'} @tab A positive finite scalar giving a uniform bin ## width. The edges are placed at multiples of the width covering the data. ## ## @item @qcode{'BinLimits'} @tab A two-element vector @code{[@var{lo}, ## @var{hi}]} restricting the binning to that range. Values outside it are not ## counted, and the outermost edges are clamped to @var{lo} and @var{hi}. ## ## @item @qcode{'BinMethod'} @tab One of @qcode{'auto'} (default), ## @qcode{'scott'}, @qcode{'fd'}, @qcode{'integers'}, @qcode{'sturges'} or ## @qcode{'sqrt'}. @qcode{'auto'} uses the @qcode{'integers'} rule when the ## data are integer-valued and span at most 50, and @qcode{'scott'} ## otherwise. ## ## @item @qcode{'Normalization'} @tab One of @qcode{'count'} (default), ## @qcode{'countdensity'}, @qcode{'cumcount'}, @qcode{'probability'}, ## @qcode{'percentage'}, @qcode{'pdf'} or @qcode{'cdf'}. The divisor for ## @qcode{'probability'}, @qcode{'percentage'}, @qcode{'pdf'} and @qcode{'cdf'} ## is @code{numel (@var{X})}, not the number of elements actually counted, so ## values excluded by being @qcode{NaN} or out of range still contribute to it. ## @end multitable ## ## MATLAB accepts @qcode{'percentage'} but omits it from the list of valid ## values in its own error message; it is documented here. ## ## @seealso{discretize} ## @end deftypefn function [N, EDGES, BIN] = histcounts (X, varargin) ## Input validation if (nargin < 1) error ("histcounts: not enough input arguments."); endif if (! (isnumeric (X) || islogical (X)) || ! isreal (X)) error ("histcounts: X must be a real numeric or logical array."); endif ## A leading positional argument is a bin count or a set of edges args = varargin; spec = 'auto'; specVal = []; if (! isempty (args) && ! isTextScalar (args{1})) if (isscalar (args{1}) && ! islogical (args{1})) spec = 'numbins'; specVal = checkNumBins (args{1}); else spec = 'binedges'; specVal = checkBinEdges (args{1}); endif args(1) = []; endif ## Name/Value options. The bin-determining options override one another in ## the order given, so they are applied as they are parsed. limits = []; normalization = 'count'; if (mod (numel (args), 2) != 0) error ("histcounts: name-value arguments must be in pairs."); endif for k = 1:2:numel (args) if (! isTextScalar (args{k})) error ("histcounts: invalid optional paired argument."); endif name = charOf (args{k}); value = args{k+1}; switch (lower (name)) case 'numbins' spec = 'numbins'; specVal = checkNumBins (value); case 'binedges' spec = 'binedges'; specVal = checkBinEdges (value); case 'binwidth' if (! isnumeric (value) || ! isscalar (value) || ! isreal (value) || ! isfinite (value) || value <= 0) error (strcat ("histcounts: 'BinWidth' must be a real, finite,", ... " positive, numeric scalar.")); endif spec = 'binwidth'; specVal = double (value); case 'binmethod' if (! isTextScalar (value) || ! any (strcmpi (charOf (value), {'auto', 'scott', 'fd', ... 'integers', 'sturges', 'sqrt'}))) error (strcat ("histcounts: invalid value for 'BinMethod'.", ... " Possible values are: 'auto', 'scott', 'fd',", ... " 'integers', 'sturges', and 'sqrt'.")); endif spec = lower (charOf (value)); specVal = []; case 'binlimits' if (! isnumeric (value) || numel (value) != 2 || ! isreal (value) || any (! isfinite (value))) error (strcat ("histcounts: 'BinLimits' must be a two-element", ... " vector of real, finite values.")); endif if (value(1) > value(2)) error ("histcounts: 'BinLimits' must be in ascending order."); endif limits = double (value(:)).'; case 'normalization' if (! isTextScalar (value) || ! any (strcmpi (charOf (value), {'count', 'countdensity', ... 'cumcount', 'probability', ... 'percentage', 'pdf', 'cdf'}))) error (strcat ("histcounts: invalid value for 'Normalization'.", ... " Possible values are: 'count', 'countdensity',", ... " 'cumcount', 'probability', 'percentage', 'pdf',", ... " and 'cdf'.")); endif normalization = lower (charOf (value)); otherwise error ("histcounts: invalid optional paired argument."); endswitch endfor ## Restrict the data to the finite values that take part in the binning xv = double (X(:)); xf = xv(isfinite (xv)); if (! isempty (limits)) xf = xf(xf >= limits(1) & xf <= limits(2)); lo = limits(1); hi = limits(2); elseif (isempty (xf)) lo = []; hi = []; else lo = min (xf); hi = max (xf); endif ## Determine the bin edges if (strcmp (spec, 'binedges')) EDGES = specVal; elseif (isempty (lo)) EDGES = [0, 1]; elseif (strcmp (spec, 'numbins')) EDGES = __binedges__ (lo, hi, specVal); EDGES = clampToLimits (EDGES, limits); elseif (strcmp (spec, 'binwidth')) EDGES = clampToLimits (placeEdges (specVal, lo, hi), limits); else EDGES = clampToLimits (methodEdges (spec, xf, lo, hi), limits); endif if (isa (X, 'single') && ! strcmp (spec, 'binedges')) EDGES = single (EDGES); endif ## Count ev = double (EDGES(:)).'; bin = __binassign__ (xv, ev, true); nbins = numel (ev) - 1; counted = ! isnan (bin); N = accumarray (bin(counted), 1, [nbins, 1]).'; ## Apply the requested normalization. Note the divisor below is numel (X), ## not sum (N) -- elements that were never counted still contribute to it. switch (normalization) case 'countdensity' N = N ./ diff (double (ev)); case 'cumcount' N = cumsum (N); case 'probability' N = N / numel (X); case 'percentage' N = 100 * N / numel (X); case 'pdf' N = N ./ (numel (X) * diff (double (ev))); case 'cdf' N = cumsum (N) / numel (X); endswitch if (nargout > 2) bin(! counted) = 0; BIN = reshape (bin, size (X)); endif endfunction ## Bin edges for one of the automatic bin-selection rules function edges = methodEdges (method, xf, lo, hi) n = numel (xf); if (lo == hi) edges = [lo - 0.5, lo + 0.5]; return; endif if (strcmp (method, 'auto')) if (all (xf == fix (xf)) && (hi - lo) <= 50) method = 'integers'; else method = 'scott'; endif endif switch (method) case 'integers' centres = round (lo):round (hi); edges = [centres - 0.5, centres(end) + 0.5]; return; case 'scott' rawWidth = 3.5 * std (xf) * n ^ (-1/3); case 'fd' ## Guard against a vanishing interquartile range, which would otherwise ## give a zero width for heavily tied data. q = quantile (xf, [0.25, 0.75], 1, 5); rawWidth = 2 * max (q(2) - q(1), (hi - lo) / 10) * n ^ (-1/3); case 'sturges' rawWidth = (hi - lo) / ceil (1 + log2 (n)); case 'sqrt' rawWidth = (hi - lo) / ceil (sqrt (n)); endswitch edges = placeEdges (niceWidth (rawWidth), lo, hi); endfunction ## Round a raw bin width up onto the 1-2-3-5-10 ladder function w = niceWidth (rawWidth) if (! (rawWidth > 0)) w = 1; return; endif powTen = 10 ^ floor (log10 (rawWidth)); relSize = rawWidth / powTen; if (relSize < 1.5) w = powTen; elseif (relSize < 2.5) w = 2 * powTen; elseif (relSize < 4) w = 3 * powTen; elseif (relSize < 7.5) w = 5 * powTen; else w = 10 * powTen; endif endfunction ## Place edges of a given width at multiples of that width, covering the data function edges = placeEdges (w, lo, hi) if (lo == hi) edges = [lo - w/2, lo + w/2]; return; endif left = w * floor (lo / w); nbins = max (1, ceil ((hi - left) / w)); edges = left + (0:nbins) * w; endfunction ## Clamp the outermost edges into an explicit BinLimits range function edges = clampToLimits (edges, limits) if (isempty (limits)) return; endif edges = [limits(1), edges(edges > limits(1) & edges < limits(2)), limits(2)]; endfunction ## True for a character vector or a scalar string function tf = isTextScalar (x) tf = (ischar (x) && isrow (x)) || (isa (x, 'string') && isscalar (x)); endfunction ## Character vector of a char or scalar string function s = charOf (x) if (isa (x, 'string')) s = char (x); else s = x; endif endfunction function n = checkNumBins (value) if (! isnumeric (value) || ! isscalar (value) || ! isreal (value) || ! isfinite (value) || value < 1 || fix (value) != value) error (strcat ("histcounts: 'NumBins' must be a real, finite,", ... " positive, integer value.")); endif n = double (value); endfunction function e = checkBinEdges (value) if (! (isnumeric (value) || islogical (value)) || ! isreal (value) || ! isvector (value) || numel (value) < 2 || any (isnan (value(:)))) error (strcat ("histcounts: 'BinEdges' must be a non-empty, real,", ... " numeric vector with no missing values.")); endif if (any (diff (double (value(:))) < 0)) error ("histcounts: 'BinEdges' must be in ascending order."); endif e = value(:).'; endfunction %!demo %! ## Automatic binning. Integer-valued data spanning at most 50 gets one bin %! ## per integer, with edges at the half-integers. %! %! [N, edges] = histcounts ([1, 2, 3, 4, 5]) %!demo %! ## Explicit edges, and the bin each value landed in. %! %! [N, edges, bin] = histcounts ([0, 1, 2, 3, 4, 5, 6], [1, 3, 5]) %!demo %! ## Normalization by relative frequency. The divisor is numel (X), so the %! ## NaN below still counts against the total. %! %! histcounts ([1, NaN, 2, 3], [1, 2, 3, 4], 'Normalization', 'probability') ## Explicit edges %!assert_equal (histcounts ([1, 2, 3, 4, 5], [1, 3, 5]), [2, 3]) %!assert_equal (histcounts ([0, 1, 2, 3, 4, 5, 6], [1, 3, 5]), [2, 3]) %!assert_equal (class (histcounts ([1, 2, 3], [1, 2, 3])), 'double') %!test %! [~, e, b] = histcounts ([0, 1, 2, 3, 4, 5, 6], [1, 3, 5]); %! assert_equal (e, [1, 3, 5]); %! assert_equal (b, [0, 1, 1, 2, 2, 2, 0]); %!test %! [~, ~, b] = histcounts ([-Inf, 0, Inf, NaN, 1], [0, 1, 2]); %! assert_equal (b, [0, 1, 0, 0, 2]); %!assert_equal (histcounts ([-Inf, 0, Inf, NaN, 1], [-Inf, 1, Inf]), [2, 2]) ## Automatic binning -- values verified against MATLAB R2024a %!test %! [N, e] = histcounts ([1, 2, 3, 4, 5]); %! assert_equal (N, [1, 1, 1, 1, 1]); %! assert_equal (e, [0.5, 1.5, 2.5, 3.5, 4.5, 5.5]); %!test %! [~, e] = histcounts ([0, 1]); %! assert_equal (e, [-0.5, 0.5, 1.5]); %!test %! [~, e] = histcounts ([0, 100]); %! assert_equal (e, [0, 200]); %!test %! [~, e] = histcounts (1:100); %! assert_equal (e, [0, 20, 40, 60, 80, 100]); %!test %! [~, e] = histcounts (1:1000); %! assert_equal (e, [0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]); %!test %! [~, e] = histcounts ([1, 2, 3, 4, 5, 100]); %! assert_equal (e, [0, 100]); %!test %! [~, e] = histcounts ([-7.5, 2.5]); %! assert_equal (e, [-20, 0, 20]); %!test %! [~, e] = histcounts (0.5 * (1:40)); %! assert_equal (e, [0, 5, 10, 15, 20]); %!test %! N = histcounts (0.5 * (1:40)); %! assert_equal (N, [9, 10, 10, 11]); ## The 'auto' integer switch turns over at a range of 50 %!test %! [~, e] = histcounts (1:51); %! assert_equal (e(1), 0.5); %! assert_equal (numel (e) - 1, 51); %!test %! [~, e] = histcounts (1:52); %! assert_equal (numel (e) - 1, 6); %!test %! [~, e] = histcounts ([0, 50]); %! assert_equal (e(1), -0.5); %!test %! [~, e] = histcounts ([0, 51]); %! assert_equal (e, [0, 100]); ## Constant and degenerate input %!test %! [~, e] = histcounts ([3, 3, 3]); %! assert_equal (e, [2.5, 3.5]); %!test %! [~, e] = histcounts (7); %! assert_equal (e, [6.5, 7.5]); %!test %! [N, e] = histcounts ([]); %! assert_equal (N, 0); %! assert_equal (e, [0, 1]); %! assert_equal (size (N), [1, 1]); %!test %! [~, e] = histcounts ([3, 3, 3], 'BinMethod', 'scott'); %! assert_equal (e, [2.5, 3.5]); ## Bin methods %!test %! [~, e] = histcounts (1:100, 'BinMethod', 'scott'); %! assert_equal (e, [0, 20, 40, 60, 80, 100]); %!test %! [~, e] = histcounts (1:100, 'BinMethod', 'fd'); %! assert_equal (e, [0, 20, 40, 60, 80, 100]); %!test %! [~, e] = histcounts (1:100, 'BinMethod', 'sturges'); %! assert_equal (e, [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]); %!test %! [~, e] = histcounts (1:100, 'BinMethod', 'sqrt'); %! assert_equal (e, [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]); %!test %! [~, e] = histcounts ((1:100)/7, 'BinMethod', 'scott'); %! assert_equal (e, [0, 3, 6, 9, 12, 15]); %!test %! [~, e] = histcounts ((1:100)/7, 'BinMethod', 'fd'); %! assert_equal (e, [0, 3, 6, 9, 12, 15]); %!test %! [~, e] = histcounts ((1:100)/7, 'BinMethod', 'sturges'); %! assert_equal (e, [0, 2, 4, 6, 8, 10, 12, 14, 16]); %!test %! [~, e] = histcounts ((1:100)/7, 'BinMethod', 'sqrt'); %! assert_equal (e, 0:15); %!test %! [~, e] = histcounts ([1.5, 2.5], 'BinMethod', 'integers'); %! assert_equal (e, [1.5, 2.5, 3.5]); %!test %! [~, e] = histcounts ([-3, 0, 3], 'BinMethod', 'integers'); %! assert_equal (e, [-3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5]); ## Bin counts go through the same edge algorithm as discretize %!test %! [~, e] = histcounts (1:100, 7); %! assert_equal (e, [0, 15, 30, 45, 60, 75, 90, 105]); %!test %! [~, e1] = histcounts ([1, 2, 3, 4, 5], 3); %! [~, e2] = discretize ([1, 2, 3, 4, 5], 3); %! assert_equal (e1, e2); ## NumBins, BinWidth, BinEdges, BinLimits %!test %! [~, e] = histcounts (1:100, 'NumBins', 7); %! assert_equal (e, [0, 15, 30, 45, 60, 75, 90, 105]); %!test %! [~, e] = histcounts (1:100, 'BinWidth', 7); %! assert_equal (e, 0:7:105); %!test %! [~, e] = histcounts (1:100, 'BinEdges', [0, 50, 100]); %! assert_equal (e, [0, 50, 100]); %!test %! [N, e] = histcounts (1:100, 'BinLimits', [20, 40]); %! assert_equal (e, [20, 20.5:1:39.5, 40]); %! assert_equal (N, ones (1, 21)); %!test %! [~, ~, b] = histcounts (1:10, 'BinLimits', [3, 6]); %! assert_equal (b, [0, 0, 1, 2, 3, 4, 0, 0, 0, 0]); %!test %! [~, e] = histcounts (1:100, 'BinWidth', 7, 'BinLimits', [0, 70]); %! assert_equal (e, 0:7:70); ## The last-specified bin option wins %!test %! [~, e] = histcounts (1:100, 'NumBins', 3, 'BinWidth', 7); %! assert_equal (e, 0:7:105); ## Normalization %!shared x, ed %! x = [1, 1, 2, 3, 3, 3]; %! ed = [1, 2, 3, 4]; %!assert_equal (histcounts (x, ed), [2, 1, 3]) %!assert_equal (histcounts (x, ed, 'Normalization', 'count'), [2, 1, 3]) %!assert_equal (histcounts (x, ed, 'Normalization', 'probability'), ... %! [2, 1, 3] / 6) %!assert_equal (histcounts (x, ed, 'Normalization', 'percentage'), ... %! 100 * [2, 1, 3] / 6) %!assert_equal (histcounts (x, ed, 'Normalization', 'countdensity'), [2, 1, 3]) %!assert_equal (histcounts (x, ed, 'Normalization', 'pdf'), [2, 1, 3] / 6) %!assert_equal (histcounts (x, ed, 'Normalization', 'cumcount'), [2, 3, 6]) %!assert_equal (histcounts (x, ed, 'Normalization', 'cdf'), [2, 3, 6] / 6) %!assert_equal (histcounts (x, [1, 3, 4], 'Normalization', 'pdf'), [0.25, 0.5]) %!assert_equal (histcounts (x, [1, 3, 4], 'Normalization', 'countdensity'), ... %! [1.5, 3]) ## The divisor is numel (X), not the number of elements counted %!assert_equal (histcounts ([1, NaN, 2, 3], [1, 2, 3, 4], ... %! 'Normalization', 'probability'), [0.25, 0.25, 0.25]) %!assert_equal (histcounts ([1, 5, 2, 3], [1, 2, 3, 4], ... %! 'Normalization', 'probability'), [0.25, 0.25, 0.25]) %!assert_equal (class (histcounts ([1, 2, 3], [1, 2, 3], ... %! 'Normalization', 'pdf')), 'double') ## Types and shapes %!assert_equal (class (histcounts (single ([1, 2, 3]))), 'double') %!test %! [~, e] = histcounts (single ([1, 2, 3])); %! assert_equal (class (e), 'single'); %!test %! [~, e] = histcounts (int8 ([1, 2, 3])); %! assert_equal (class (e), 'double'); %! assert_equal (e, [0.5, 1.5, 2.5, 3.5]); %!test %! [~, e] = histcounts (uint8 ([1, 2, 3])); %! assert_equal (e, [0.5, 1.5, 2.5, 3.5]); %!test %! [~, e] = histcounts (logical ([0, 1, 1])); %! assert_equal (e, [-0.5, 0.5, 1.5]); %!assert_equal (histcounts (logical ([0, 1, 1])), [1, 2]) %!assert_equal (histcounts ([1, 2; 3, 4], [1, 3, 5]), [2, 2]) %!assert_equal (size (histcounts (reshape (1:24, 2, 3, 4), [1, 10, 20, ... %! 30])), ... %! [1, 3]) %!test %! [~, ~, b] = histcounts ([1, 2; 3, 4], [1, 3, 5]); %! assert_equal (size (b), [2, 2]); %!assert_equal (histcounts ([1, 2, 3]', [1, 2, 3]), [1, 2]) %!assert_equal (size (histcounts ([1, 2, 3]', [1, 2, 3])), [1, 2]) %!assert_equal (histcounts ([1, 2, 3], 1), 3) ## Error branches %!error histcounts ('abc'); %!error histcounts ({1, 2}); %!error ... %! histcounts (complex ([1, 2], [1, 1])); %!error ... %! histcounts ([1, 2, 3], 0); %!error ... %! histcounts ([1, 2, 3], -1); %!error ... %! histcounts ([1, 2, 3], 2.5); %!error ... %! histcounts ([1, 2, 3], 'NumBins', 0); %!error ... %! histcounts ([1, 2, 3], [3, 1]); %!error ... %! histcounts ([1, 2, 3], [1, NaN, 3]); %!error ... %! histcounts ([1, 2, 3], 'BinMethod', 'bogus'); %!error ... %! histcounts ([1, 2, 3], 'Normalization', 'bogus'); %!error ... %! histcounts ([1, 2, 3], 'BinWidth', 0); %!error ... %! histcounts ([1, 2, 3], 'BinWidth', -1); %!error ... %! histcounts ([1, 2, 3], 'BinLimits', [3, 1]); %!error ... %! histcounts ([1, 2, 3], 'BinLimits', [1, 2, 3]); %!error ... %! histcounts () %!error ... %! histcounts ([1, 2, 3], 'Normalization') %!error ... %! histcounts ([1, 2, 3], 5, 'Normalization', 'count', 7, 1) %!error ... %! histcounts ([1, 2, 3], 'Nope', 1) pr0m1th3as-datatypes-9450b29/inst/hours.m000066400000000000000000000041311527370255000201700ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{D} =} hours (@var{X}) ## ## Fixed-time duration in hours. ## ## @code{@var{D} = hours (@var{X})} returns a @qcode{duration} array ## representing fixed-time duration hours equivalent to the values in @var{X}, ## which must be a numeric array. ## ## @code{hours} is also available as a method for @qcode{duration} arrays, in ## which case it performs the opposite conversion. ## ## @seealso{duration, years, days, minutes, seconds, milliseconds, ## duration.hours} ## @end deftypefn function out = hours (x) if (nargin == 0) x = 1; elseif (! isnumeric (x)) error ("hours: input array must be numeric."); elseif (! isreal (x)) error ("hours: input array must be real."); endif out = duration (0, 0, 0, double (x) * 3600000, 'Format', 'h'); endfunction %!demo %! ## `hours` builds a duration from a number of hours; fractional values are %! ## fine. %! %! hours (1.5) %! hours ([1, 2.5, 8]) %!test %! X = magic (3); %! D = hours (X); %! assert_equal (size (D), size (X)); %!test %! D = hours ([1, 2, 3]); %! assert_equal (hours (D), [1, 2, 3]); %!test %! D = hours (int16 (1)); %! assert_equal (hours (D), 1); %!test %! D = hours (); %! assert_equal (hours (D), 1); %!error hours ("asd"); %!error hours (1+i); pr0m1th3as-datatypes-9450b29/inst/iscalendarduration.m000066400000000000000000000027471527370255000227160ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{TF} =} iscalendarduration (@var{X}) ## ## True if input is a @code{calendarDuration} array, false otherwise. ## ## @code{@var{TF} = iscalendarduration (@var{X})} always returns a logical ## scalar, irrespective of the size of @var{X}. ## ## @end deftypefn function TF = iscalendarduration (x) TF = isa (x, 'calendarDuration'); endfunction %!assert_equal (iscalendarduration (calendarDuration (0, 0, 0)), true); %!assert_equal (iscalendarduration (calendarDuration ([1, 2], 0, 0)), true); %!assert_equal (iscalendarduration ([0, 0, 0]), false); %!assert_equal (iscalendarduration ({0, 0, 0}), false); %!assert_equal (iscalendarduration ({calendarDuration(0, 0, 0)}), false); pr0m1th3as-datatypes-9450b29/inst/iscategorical.m000066400000000000000000000026561527370255000216530ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{TF} =} iscategorical (@var{X}) ## ## True if input is a @code{categorical} array, false otherwise. ## ## @code{@var{TF} = iscategorical (@var{X})} always returns a logical scalar, ## irrespective of the size of @var{X}. ## ## @end deftypefn function TF = iscategorical (x) TF = isa (x, 'categorical'); endfunction %!assert_equal (iscategorical (categorical ([1, 2, 3])), true); %!assert_equal (iscategorical (categorical ([1, 2; 1, 4])), true); %!assert_equal (iscategorical ([0, 1, 2]), false); %!assert_equal (iscategorical ({true, false}), false); %!assert_equal (iscategorical ({categorical([1, 2, 2])}), false); pr0m1th3as-datatypes-9450b29/inst/isdatetime.m000066400000000000000000000026151527370255000211650ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{TF} =} isdatetime (@var{X}) ## ## True if input is a @code{datetime} array, false otherwise. ## ## @code{@var{TF} = isdatetime (@var{X})} always returns a logical scalar, ## irrespective of the size of @var{X}. ## ## @end deftypefn function TF = isdatetime (x) TF = isa (x, 'datetime'); endfunction %!assert_equal (isdatetime (datetime ([1, 2, 3])), true); %!assert_equal (isdatetime (datetime ([1, 2, 0; 1, 4, 0])), true); %!assert_equal (isdatetime ([0, 1, 2]), false); %!assert_equal (isdatetime ({true, false}), false); %!assert_equal (isdatetime ({datetime([1, 2, 3])}), false); pr0m1th3as-datatypes-9450b29/inst/isdictionary.m000066400000000000000000000036141527370255000215360ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{TF} =} isdictionary (@var{X}) ## ## True if input is a @code{dictionary} object, false otherwise. ## ## @code{@var{TF} = isdictionary (@var{X})} always returns a logical scalar, ## for a configured or an unconfigured dictionary alike. MATLAB has no such ## function; it is provided as @code{istable} and @code{istimeseries} are for ## their classes. ## ## @seealso{dictionary, isa} ## @end deftypefn function TF = isdictionary (x) TF = isa (x, 'dictionary'); endfunction %!demo %! ## `isdictionary` is true for a dictionary, configured or not, and false %! ## for anything else, a cell array holding a dictionary included. %! %! isdictionary (dictionary ()) %! isdictionary (dictionary ('a', 1)) %! isdictionary (struct ('a', 1)) %! isdictionary ({dictionary()}) %!assert_equal (isdictionary (dictionary ()), true) %!assert_equal (isdictionary (configureDictionary ('string', 'double')), true) %!assert_equal (isdictionary (struct ()), false) %!assert_equal (isdictionary ({dictionary()}), false) %!assert_equal (isdictionary ([1, 2, 3]), false) %!assert_equal (isdictionary ('dictionary'), false) pr0m1th3as-datatypes-9450b29/inst/isduration.m000066400000000000000000000026151527370255000212160ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{TF} =} isduration (@var{X}) ## ## True if input is a @code{duration} array, false otherwise. ## ## @code{@var{TF} = isduration (@var{X})} always returns a logical scalar, ## irrespective of the size of @var{X}. ## ## @end deftypefn function TF = isduration (x) TF = isa (x, 'duration'); endfunction %!assert_equal (isduration (duration ([1, 2, 3])), true); %!assert_equal (isduration (duration ([1, 2, 0; 1, 4, 0])), true); %!assert_equal (isduration ([0, 1, 2]), false); %!assert_equal (isduration ({true, false}), false); %!assert_equal (isduration ({duration([1, 2, 3])}), false); pr0m1th3as-datatypes-9450b29/inst/isnat.m000066400000000000000000000033571527370255000201570ustar00rootroot00000000000000## Copyright (C) 2025-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{TF} =} isnat (@var{T}) ## ## Test for Not-A-Time elements in datetime array. ## ## @code{@var{TF} = isnat (@var{T})} returns a logical array @var{TF} of the ## same size as @var{T} containing @qcode{true} for each corresponding ## element of @var{T} that is Not-A-Time (@qcode{NaT}) and @qcode{false} ## otherwise. @qcode{NaT} is the equivalent of @qcode{NaN} in numeric ## arrays. ## ## If @var{T} is not a datetime array, @code{isnat} returns an error. ## ## @end deftypefn function TF = isnat (T) error ("isnat: input argument must be a datetime array."); endfunction %!error isnat (1) %!error isnat ({'asd'}) %!error isnat ('er') %!error isnat (ones (2, 3)) %!error isnat (days (2)) pr0m1th3as-datatypes-9450b29/inst/istable.m000066400000000000000000000025401527370255000204550ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{TF} =} istable (@var{X}) ## ## True if input is a @code{table}, false otherwise. ## ## @code{@var{TF} = istable (@var{X})} always returns a logical scalar, ## irrespective of the size of @var{X}. ## ## @end deftypefn function TF = istable (x) TF = isa (x, 'table'); endfunction %!assert_equal (istable (table ([1, 2, 3])), true); %!assert_equal (istable (table ([1, 2, 0; 1, 4, 0])), true); %!assert_equal (istable ([0, 1, 2]), false); %!assert_equal (istable ({true, false}), false); %!assert_equal (istable ({table([1, 2, 3])}), false); pr0m1th3as-datatypes-9450b29/inst/istabular.m000066400000000000000000000033161527370255000210220ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{TF} =} istabular (@var{X}) ## ## True if input is a @code{table} or a @code{timetable}, false otherwise. ## ## @code{@var{TF} = istabular (@var{X})} always returns a logical scalar, ## irrespective of the size of @var{X}. It is true for every class of ## tabular data, whereas @code{istable} and @code{istimetable} each single ## one of them out. A @code{timetable} is not a @code{table} and neither is ## a subclass of the other. ## ## @seealso{istable, istimetable, table, timetable} ## @end deftypefn function TF = istabular (x) TF = isa (x, 'tabular'); endfunction %!assert_equal (istabular (table ([1, 2, 3])), true); %!assert_equal (istabular (timetable ([1; 2], 'TimeStep', hours (1))), true); %!assert_equal (istabular ([0, 1, 2]), false); %!assert_equal (istabular ({true, false}), false); %!assert_equal (istabular ({table([1, 2, 3])}), false); %!assert_equal (istabular (struct ('a', 1)), false); pr0m1th3as-datatypes-9450b29/inst/istimeseries.m000066400000000000000000000033441527370255000215420ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{TF} =} istimeseries (@var{X}) ## ## True if input is a @code{timeseries} object, false otherwise. ## ## @code{@var{TF} = istimeseries (@var{X})} always returns a logical scalar, ## irrespective of the size of @var{X}, so it is true for an array of ## @code{timeseries} objects too. MATLAB has no such function; it is provided ## as @code{isduration} and @code{istimetable} are for their classes. ## ## @seealso{timeseries, isa} ## @end deftypefn function TF = istimeseries (x) TF = isa (x, 'timeseries'); endfunction %!assert_equal (istimeseries (timeseries ([1; 2; 3])), true) %!assert_equal (istimeseries (timeseries ()), true) %!assert_equal (istimeseries ([timeseries(1), timeseries(2)]), true) %!assert_equal (istimeseries ([1, 2, 3]), false) %!assert_equal (istimeseries ({timeseries(1)}), false) %!assert_equal (istimeseries (timetable (seconds ([1; 2]), [1; 2])), false) %!assert_equal (istimeseries ('timeseries'), false) pr0m1th3as-datatypes-9450b29/inst/istimetable.m000066400000000000000000000031001527370255000213250ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{TF} =} istimetable (@var{X}) ## ## True if input is a @code{timetable}, false otherwise. ## ## @code{@var{TF} = istimetable (@var{X})} always returns a logical scalar, ## irrespective of the size of @var{X}. It is false for a @code{table}, ## which is not a @code{timetable} and is not a superclass of one. ## ## @seealso{istable, istabular, timetable, table} ## @end deftypefn function TF = istimetable (x) TF = isa (x, 'timetable'); endfunction %!assert_equal (istimetable (timetable ([1; 2], 'TimeStep', hours (1))), true); %!assert_equal (istimetable (table ([1, 2, 3])), false); %!assert_equal (istimetable ([0, 1, 2]), false); %!assert_equal (istimetable ({true, false}), false); %!assert_equal (istimetable ({timetable([1;2], 'TimeStep', hours (1))}), false); pr0m1th3as-datatypes-9450b29/inst/istscollection.m000066400000000000000000000032421527370255000220700ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{TF} =} istscollection (@var{X}) ## ## True if input is a @code{tscollection} object, false otherwise. ## ## @code{@var{TF} = istscollection (@var{X})} always returns a logical scalar, ## irrespective of the size of @var{X}, so it is true for an array of ## @code{tscollection} objects too. MATLAB has no such function; it is provided ## as @code{isduration} and @code{istimetable} are for their classes. ## ## @seealso{istimeseries, isa} ## @end deftypefn function TF = istscollection (x) TF = isa (x, 'tscollection'); endfunction %!assert_equal (istscollection (tscollection ()), true) %!assert_equal (istscollection (tscollection ([0; 1])), true) %!assert_equal (istscollection (timeseries ([1; 2; 3])), false) %!assert_equal (istscollection ([1, 2, 3]), false) %!assert_equal (istscollection ({1}), false) %!assert_equal (istscollection ('tscollection'), false) pr0m1th3as-datatypes-9450b29/inst/keyHash.m000066400000000000000000000461421527370255000204340ustar00rootroot00000000000000## Copyright (C) 2025-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{key} =} keyHash (@var{X}) ## @deftypefnx {datatypes} {@var{key} =} keyHash (@var{X}, @var{base}) ## ## Generate a hash code for an array. ## ## @code{@var{key} = keyHash (@var{X})} generates a @qcode{uint64} scalar that ## represents the input @var{X}, which may be a numeric, logical, character, ## cell, struct or function handle array. @code{keyHash} utilizes the 64-bit ## FNV-1a variant of the Fowler-Noll-Vo non-cryptographic hash function. ## ## A @qcode{cell} array is keyed on its elements, each hashed in turn, so a ## cell may hold any type that @code{keyHash} accepts, nested to any depth. A ## @qcode{struct} is keyed on its field names and their values; the order in ## which its fields were created is @emph{not} part of the key, so two structs ## differing only in field order are the same key. A function handle is keyed ## on its text alone: two named handles for the same function are one key, ## while distinct anonymous handles never match even when written identically, ## so nothing a handle captures can be observed through @code{keyMatch}. ## ## @code{@var{key} = keyHash (@var{X}, @var{base})} also generates a 64-bit ## hash code using @var{base} as the offset basis for the FNV-1a hash ## algorithm. @var{base} must be a @qcode{uint64} integer type scalar. Use ## this syntax to cascade @code{keyHash} on multiple objects for which a ## single hash code is required. ## ## @code{keyMatch} decides key identity and @code{keyHash} agrees with it: two ## values that @code{keyMatch} reports as the same key always have the same ## hash code. The converse does not hold, since distinct keys are permitted ## to share a hash code. Values that differ only in a representation that ## @code{keyMatch} ignores therefore hash alike, so @code{-0} hashes as ## @code{0}, and every @qcode{NaN} hashes alike whatever produced it. ## ## Elements of a cell array of character vectors are hashed together with ## their lengths, so that the boundaries between them are part of the key and ## @code{@{'ab', 'c'@}} does not hash as @code{@{'a', 'bc'@}}. ## ## Note that unlike MATLAB, this implementation does not use any random seed. ## As a result, @code{keyHash} will always generate the exact same hash key ## for any particular input across different workers and Octave sessions. ## Hash codes are @emph{not} stable across package versions, however: those ## produced before version 1.3.1 are invalid and must be recomputed. ## ## @end deftypefn function key = keyHash (x = [], base = []) ## Validate input if (nargin < 1) print_usage; endif ## Initialize string with size and class name size_str = sprintf ('%dx', size (x))(1:end-1); init_str = [size_str class(x)]; if (! isempty (base)) if (! (isscalar (base) && isa (base, 'uint64'))) error ("keyHash: BASE must be a UINT64 scalar."); endif key = __ckeyHash__ (init_str, base); else key = __ckeyHash__ (init_str); endif ## Select data type if (isnumeric (x) || islogical (x)) key = __nkeyHash__ (x(:), key); elseif (ischar (x)) ## Passed whole: __ckeyHash__ reads the array in column-major order, so ## x(:) would only copy it to say what it already does. key = __ckeyHash__ (x, key); elseif (iscellstr (x)) ## Passed whole, not flattened: __ckeyHash__ frames each element with its ## length so the boundaries between them are part of the key. This branch ## stays ahead of the general cell one below so that a cellstr keeps the ## hash codes it has had since 1.3.1. key = __ckeyHash__ (x, key); elseif (iscell (x)) ## Every element is hashed by a recursive call, in column-major order. ## Each one frames itself with its own size and class on the way in, so ## the boundaries between elements are part of the key for free and no ## two different arrangements of the same contents can collide. An ## element of a class that has its own keyHash method reaches it, which ## is how a cell holding a datetime or categorical is keyed correctly ## without this function knowing about either. for i = 1:numel (x) key = keyHash (x{i}, key); endfor elseif (isstruct (x)) ## Field names are hashed SORTED. Two structs differing only in the ## order their fields were created are one key -- R2024a reports them as ## matching and hashes them alike, and isequaln agrees -- so storage ## order must not reach the hash. Values follow in that same sorted ## order, element by element. fnames = sort (fieldnames (x)); key = __ckeyHash__ (fnames, key); for i = 1:numel (x) for j = 1:numel (fnames) key = keyHash (x(i).(fnames{j}), key); endfor endfor elseif (is_function_handle (x)) ## The function text alone. A named handle is the same key as itself, ## while two anonymous handles never match however they were written, so ## nothing beyond the text can be observed through keyMatch. The captured ## workspace is deliberately not hashed: R2024a hashes two handles that ## differ only in a captured value alike, and hashing it here would also ## make the key depend on values the handle merely closed over. key = __ckeyHash__ (func2str (x), key); else ## Name the class. A cell or struct hashes its contents by recursion, so ## this is raised by the innermost frame and names the value that actually ## has no key -- not the container, which does. error ("keyHash: unsupported input type '%s'.", class (x)); endif endfunction %!test %! key = keyHash (1); %! assert_equal (isscalar (key), true); %! key = keyHash ([1:5]); %! assert_equal (isscalar (key), true); %!test %! key1 = keyHash (1); %! key2 = keyHash (1, 0xcbf29ce484222325); # default offset basis %! assert_equal (key1, key2); %!test %! key1 = keyHash (0); %! assert_equal (class (key1), 'uint64'); %!test %! A = [1:5]; %! B = [1:5]; %! key1 = keyHash (A); %! key2 = keyHash (B); %! assert_equal (key1, key2); %!test %! A = [1:5]; %! B = [1:5]'; %! key1 = keyHash (A); %! key2 = keyHash (B); %! assert_equal (isequal (key1, key2), false); %!test %! A = ''; %! E = uint64 (15921358368119480423); %! key = keyHash (A); %! assert (isequal (key, E), sprintf ("k: %lx e: %lx d: %lx", key, E, key - E)); %! assert_equal (key, E); %!test %! A = uint64 (128); %! E = uint64 (8038837787959150693); %! key = keyHash (A); %! assert (isequal (key, E), sprintf ("k: %lx e: %lx d: %lx", key, E, key - E)); %! assert_equal (key, E); ## Add more tests on endianness (see GitHub issue 43) %!assert_equal (keyHash ([1, 2]), uint64 (1291405323040189622)) %!assert_equal (keyHash ([1+2i]), uint64 (4391366703481493027)) %!assert_equal (keyHash (single ([1, 2])), uint64 (10607299016200876763)) %!assert_equal (keyHash (int64 ([1, 2])), uint64 (8387921810608003298)) %!assert_equal (keyHash (uint64 ([1, 2])), uint64 (15129359585364357711)) %!assert_equal (keyHash (int32 ([1, 2])), uint64 (7340059757986521701)) %!assert_equal (keyHash (uint32 ([1, 2])), uint64 (6705001899901839972)) %!assert_equal (keyHash (int16 ([1, 2])), uint64 (2451959940856797955)) %!assert_equal (keyHash (uint16 ([1, 2])), uint64 (10303904549507999050)) %!assert_equal (keyHash (int8 ([1, 2])), uint64 (3623669810820070662)) %!assert_equal (keyHash (uint8 ([1, 2])), uint64 (12877398120156278681)) %!assert_equal (keyHash (logical ([1, 2])), uint64 (16918259908176314871)) %!assert_equal (__nkeyHash__ ([1, 2]), uint64 (4062974330926783736)) %!assert_equal (__nkeyHash__ ([1+2i]), uint64 (4062974330926783736)) %!assert_equal (__nkeyHash__ (single ([1, 2])), uint64 (10375111465485032152)) %!assert_equal (__nkeyHash__ (int64 ([1, 2])), uint64 (8581494755304202342)) %!assert_equal (__nkeyHash__ (uint64 ([1, 2])), uint64 (8581494755304202342)) %!assert_equal (__nkeyHash__ (int32 ([1, 2])), uint64 (14538333428393601222)) %!assert_equal (__nkeyHash__ (uint32 ([1, 2])), uint64 (14538333428393601222)) %!assert_equal (__nkeyHash__ (int16 ([1, 2])), uint64 (12479921481467174326)) %!assert_equal (__nkeyHash__ (uint16 ([1, 2])), uint64 (12479921481467174326)) %!assert_equal (__nkeyHash__ (int8 ([1, 2])), uint64 (589729691727335466)) %!assert_equal (__nkeyHash__ (uint8 ([1, 2])), uint64 (589729691727335466)) %!assert_equal (__nkeyHash__ (logical ([1, 2])), uint64 (589728592215707255)) ## Add more tests on different architectures (see GitHub issues 38 and 40) %!assert_equal (__nkeyHash__ (uint8 (128)), uint64 (5808531584386460767)) %!assert_equal (keyHash (uint8 (128)), uint64 (10759574069356082695)) %!assert_equal (__nkeyHash__ (uint32 (0xdeadbeef)), ... %! uint64 (12840711468051582507)) %!assert_equal (keyHash (uint32 (0xdeadbeef)), uint64 (3790509136731937468)) ## Composite keys are hashed with their element boundaries framed, so a cell ## array is not confusable with any other splitting of the same characters. %!test %! assert_equal (isequal (keyHash ({'ab', 'c'}), keyHash ({'a', 'bc'})), false); %! assert_equal (isequal (keyHash ({'abc'}), keyHash ({'ab', 'c'})), false); %! assert_equal (isequal (keyHash ({'a', 'b', 'c'}), keyHash ({'ab', 'c'})), ... %! false); %! assert_equal (keyHash ({'ab', 'c'}), keyHash ({'ab', 'c'})); ## An empty cell hashes rather than erroring, and stays distinct from a cell ## holding one empty character vector. %!test %! assert_equal (class (keyHash ({})), 'uint64'); %! assert_equal (keyHash ({}), keyHash ({})); %! assert_equal (isequal (keyHash ({}), keyHash ({''})), false); %! assert_equal (isequal (keyHash ({''}), keyHash ({'', ''})), false); %! assert_equal (isequal (keyHash ({''}), keyHash ('')), false); ## Shape is carried by the size in the init string, so a row and a column of ## the same elements are different keys. %!test %! assert_equal (isequal (keyHash ({'a', 'b'}), keyHash ({'a'; 'b'})), false); %! assert_equal (isequal (keyHash ({}), keyHash (cell (1, 0))), false); ## A character array is hashed whole. Until 1.3.1 keyHash passed x(:), an ## N-by-1 char matrix, which was read with string_value () -- taking only the ## first row. Every character vector was therefore hashed as its first ## character alone, so 'hello' and 'hxxxx' were one key. %!test %! assert_equal (isequal (keyHash ('hello'), keyHash ('hxxxx')), false); %! assert_equal (isequal (keyHash ('abc'), keyHash ('axx')), false); %! assert_equal (isequal (keyHash ('abc'), keyHash ('abd')), false); %! assert_equal (keyHash ('abc'), keyHash ('abc')); %! assert_equal (isequal (keyHash (['ab'; 'cd']), keyHash (['ab'; 'ce'])), ... %! false); %! assert_equal (keyHash (['ab'; 'cd']), keyHash (['ab'; 'cd'])); ## The class is part of the key, so a character vector and a cell holding it ## never collide. %!test %! assert_equal (isequal (keyHash ('ab'), keyHash ({'ab'})), false); %! assert_equal (isequal (keyHash (''), keyHash ({})), false); ## Signed zero and NaN are canonicalised, so bit patterns that isequaln calls ## equal are one key. Integer types have neither and are hashed untouched. %!test %! assert_equal (keyHash (0), keyHash (-0)); %! assert_equal (keyHash (0), keyHash (0 * -1)); %! assert_equal (keyHash (NaN), keyHash (0/0)); %! assert_equal (keyHash (NaN), keyHash (Inf - Inf)); %! assert_equal (keyHash ([0, NaN]), keyHash ([-0, 0/0])); %! assert_equal (keyHash (single (0)), keyHash (single (-0))); %! assert_equal (keyHash (single (NaN)), keyHash (single (0/0))); %! assert_equal (isequal (keyHash (0), keyHash (NaN)), false); %! assert_equal (isequal (keyHash (Inf), keyHash (-Inf)), false); ## A zero offset basis is a basis like any other and must not be read as no ## basis at all. The guard tested truthiness, so uint64 (0) was silently ## replaced by the default basis, and every other falsy value -- 0, false, and ## any uint64 array containing a zero -- skipped validation entirely. %!test %! assert_equal (class (keyHash (1, uint64 (0))), 'uint64'); %! assert_equal (keyHash (1, uint64 (0)), keyHash (1, uint64 (0))); %! assert_equal (isequal (keyHash (1), keyHash (1, uint64 (0))), false); %! ## an empty base is the default, meaning no base at all %! assert_equal (keyHash (1, []), keyHash (1)); %!error keyHash (); %!error keyHash (1, 1); %!error keyHash (1, 0); %!error keyHash (1, false); %!error keyHash (1, uint64 ([0, 1])); ## The error names the value that has no key, not the container holding it: ## a cell and a struct are both hashable, so reporting either would send the ## caller after the wrong type. %!error keyHash (table (1)); %!error keyHash ({table (1)}); %!error keyHash ({1, table (1), 3}); %!error keyHash ({{table (1)}}); %!error keyHash (struct ('a', table (1))); %!error ... %! keyHash (struct ('a', 1, 'b', table (1))); ## A struct is keyed on its field names and their values. The order the ## fields were created in is NOT part of the key: R2024a reports two structs ## differing only in field order as one key and hashes them alike. %!assert_equal (keyHash (struct ('a', 1)), keyHash (struct ('a', 1))) %!assert_equal (keyHash (struct ('a', 1, 'b', 2)), keyHash (struct ('b', 2, ... %! 'a', 1))) %!assert_equal (keyHash (struct ()), keyHash (struct ())) %!assert_equal (isequal (keyHash (struct ('a', 1)), keyHash (struct ('a', ... %! 2))), false) %!assert_equal (isequal (keyHash (struct ('a', 1)), keyHash (struct ('b', ... %! 1))), false) %!assert_equal (isequal (keyHash (struct ('a', 1)), keyHash (struct ('a', 1, ... %! 'b', 2))), false) %!assert_equal (isequal (keyHash (struct ('a', 1)), keyHash (struct ('a', ... %! int8 (1)))), false) %!test ## nesting, and a struct array against a scalar of the same field %! assert_equal (keyHash (struct ('a', struct ('b', 1))), ... %! keyHash (struct ('a', struct ('b', 1)))); %! assert_equal (isequal (keyHash (struct ('a', struct ('b', 1))), ... %! keyHash (struct ('a', struct ('b', 2)))), false); %!test %! A(1).a = 1; A(2).a = 2; %! B(1).a = 1; B(2).a = 2; %! assert_equal (keyHash (A), keyHash (B)); %! assert_equal (isequal (keyHash (A), keyHash (A')), false); ## A cell is keyed on its elements, each framed by its own size and class on ## the way in, so nesting and shape are part of the key and any type keyHash ## accepts may appear inside. %!assert_equal (keyHash ({1, 2}), keyHash ({1, 2})) %!assert_equal (keyHash ({}), keyHash ({})) %!assert_equal (keyHash ({1, 'a'}), keyHash ({1, 'a'})) %!assert_equal (keyHash ({{1}}), keyHash ({{1}})) %!assert_equal (keyHash ({struct('a', 1)}), keyHash ({struct('a', 1)})) %!assert_equal (keyHash ({@sin}), keyHash ({@sin})) %!assert_equal (isequal (keyHash ({1, 2}), keyHash ({1, 3})), false) %!assert_equal (isequal (keyHash ({1, 2}), keyHash ({1; 2})), false) %!assert_equal (isequal (keyHash ({{1}}), keyHash ({1})), false) %!assert_equal (isequal (keyHash ({1}), keyHash ({int8(1)})), false) ## A cell array of character vectors is hashed by the cellstr path, which sits ## ahead of the general cell one, so it keeps the codes it has had since 1.3.1 ## and the element boundaries stay part of the key. %!assert_equal (isequal (keyHash ({'ab', 'c'}), keyHash ({'a', 'bc'})), false) ## A function handle is keyed on its text alone. Two named handles for the ## same function are one key. What an anonymous handle captures is NOT part ## of the key -- R2024a hashes two handles differing only in a captured value ## alike, and distinct anonymous handles never match in any case. %!assert_equal (keyHash (@sin), keyHash (@sin)) %!assert_equal (keyHash (@(x) x + 1), keyHash (@(x) x + 1)) %!assert_equal (isequal (keyHash (@sin), keyHash (@cos)), false) %!assert_equal (isequal (keyHash (@(x) x + 1), keyHash (@(x) x + 2)), false) %!assert_equal (isequal (keyHash (@sin), keyHash ('sin')), false) %!test %! n = 1; f = @(x) x + n; %! n = 2; g = @(x) x + n; %! assert_equal (keyHash (f), keyHash (g)); ## The class is part of every key, so none of the new types collides with ## another or with a numeric holding the same contents. %!assert_equal (isequal (keyHash (struct ('a', 1)), keyHash ({1})), false) %!assert_equal (isequal (keyHash ({1}), keyHash (1)), false) %!assert_equal (isequal (keyHash (@sin), keyHash (1)), false) ## Frozen hash codes, measured from the 1.3.1 release and baked in. Hash codes ## are a published interface here -- this package offers codes that are stable ## across workers and sessions, which MATLAB's randomly seeded keyHash does not ## -- so a code changing is a breaking change and must be a deliberate one. ## 1.3.1 changed many of these on purpose and said so in NEWS; these literals ## exist so the next change cannot happen by accident. A failure here is not a ## test to update: it means stored keys from 1.3.1 no longer match, and either ## the change is reverted or the release notes carry it. %!assert_equal (keyHash (1), uint64 (10584184954000009987)) %!assert_equal (keyHash (0), uint64 (7866423903081946394)) %!assert_equal (keyHash (NaN), uint64 (10576514760883123851)) %!assert_equal (keyHash (Inf), uint64 (10584114585255804483)) %!assert_equal (keyHash (int8 (1)), uint64 (12242639213500019555)) %!assert_equal (keyHash (uint64 (7)), uint64 (7587370060813975170)) %!assert_equal (keyHash (single (1.5)), uint64 (5289544833140628974)) %!assert_equal (keyHash (true), uint64 (5335886618314126645)) %!assert_equal (keyHash ([1:5]), uint64 (1432333402695462731)) %!assert_equal (keyHash ([1:5]'), uint64 (9563671367466119291)) %!assert_equal (keyHash (zeros (2, 3)), uint64 (11685217659039319009)) %!assert_equal (keyHash ([]), uint64 (15028164940097692402)) %!assert_equal (keyHash ('a'), uint64 (9989574163509938906)) %!assert_equal (keyHash ('hello'), uint64 (10275689077389484113)) %!assert_equal (keyHash (''), uint64 (15921358368119480423)) %!assert_equal (keyHash (['ab'; 'cd']), uint64 (17517398146429816181)) %!assert_equal (keyHash ({}), uint64 (12134391156607149443)) %!assert_equal (keyHash ({''}), uint64 (17643749256637597450)) %!assert_equal (keyHash ({'a'}), uint64 (10819011992493875390)) %!assert_equal (keyHash ({'ab', 'c'}), uint64 (5911896368001593873)) %!assert_equal (keyHash ({'a', 'bc'}), uint64 (2035578884344404753)) %!assert_equal (keyHash (1, uint64 (12345)), uint64 (1427258369762746415)) pr0m1th3as-datatypes-9450b29/inst/keyMatch.m000066400000000000000000000353431527370255000206060ustar00rootroot00000000000000## Copyright (C) 2025-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{TF} =} keyMatch (@var{A}, @var{B}) ## ## Return true if both inputs are the same key. ## ## @code{@var{TF} = keyMatch (@var{A}, @var{B})} returns a logical scalar, ## which is @qcode{true}, if the inputs @var{A} and @var{B} are the same key, ## and @qcode{false} otherwise. Two values are the same key when they have ## the same class, the same size, and equal contents, with missing values ## comparing equal to one another as in @code{isequaln}. Neither class nor ## size is promoted, so @code{1} and @code{int8 (1)} are different keys, and ## so are a row and a column holding the same elements. ## ## @code{keyMatch} decides key identity and @code{keyHash} agrees with it: ## two values that @code{keyMatch} reports as the same key always have the ## same hash code. The converse does not hold, since distinct keys are ## permitted to share a hash code, and so comparing hash codes is not a ## substitute for calling @code{keyMatch}. ## ## Both inputs must be of a type that can be a key, that is, one that ## @code{keyHash} can hash; anything else raises an error. Answering for a ## value that cannot be hashed would assert that two values are the same key, ## or are different keys, when no key exists for either. ## ## @end deftypefn function TF = keyMatch (A, B) if (nargin != 2) print_usage; endif ## Both inputs are checked before anything is compared, so a value that ## cannot be a key is rejected rather than quietly reported as unequal: ## returning false would assert that both operands are keys that merely ## differ. ## keyHash's message is passed through rather than restated, so a container ## whose contents cannot be hashed reports the value that has no key instead ## of the container, which does. try A_key = keyHash (A); catch err error ("keyMatch: %s", regexprep (err.message, '^keyHash: ', '')); end_try_catch try B_key = keyHash (B); catch err error ("keyMatch: %s", regexprep (err.message, '^keyHash: ', '')); end_try_catch ## Hash codes reject only, never accept: distinct keys may share one. Class ## and size need no separate check, both being part of every hash. if (A_key != B_key) TF = false; else TF = isequaln (A, B); endif endfunction %!assert_equal (keyMatch (1, {'1'}), false); %!assert_equal (keyMatch (ones (2), 2), false); %!assert_equal (keyMatch (1, 2), false); %!assert_equal (keyMatch (1, 1), true); ## The equality table. Every row asserts BOTH directions of the contract: ## keyMatch decides identity, and any pair it calls equal must also hash ## equally, or a hash-bucketed container would never reach the candidate. ## Asserting only keyMatch would let the two drift apart again. ## No promotion across classes: the class is part of the key. %!test %! assert_equal (keyMatch (1, int8 (1)), false); %! assert_equal (keyMatch (1, single (1)), false); %! assert_equal (keyMatch (1, true), false); %! assert_equal (keyMatch (string ('a'), 'a'), false); ## Shape is part of the key. %!test %! assert_equal (keyMatch ([1, 2], [1; 2]), false); %! assert_equal (keyMatch (zeros (0, 0), zeros (1, 0)), false); %! assert_equal (keyMatch ({}, {''}), false); ## Missing values compare equal -- isequaln semantics, not isequal. %!test %! assert_equal (keyMatch (NaN, NaN), true); %! assert_equal (keyHash (NaN), keyHash (NaN)); %! assert_equal (keyMatch ([1, NaN], [1, NaN]), true); %! assert_equal (keyHash ([1, NaN]), keyHash ([1, NaN])); ## A NaN is a NaN whatever bit pattern produced it. On x86-64 the literal ## NaN is positive but 0/0 and Inf-Inf set the sign bit, so hashing raw bytes ## made two isequaln-equal arrays different keys. %!test %! assert_equal (keyMatch (NaN, 0/0), true); %! assert_equal (keyHash (NaN), keyHash (0/0)); %! assert_equal (keyMatch (NaN, Inf - Inf), true); %! assert_equal (keyHash (NaN), keyHash (Inf - Inf)); %! assert_equal (keyMatch ([1, NaN], [1, 0/0]), true); %! assert_equal (keyHash ([1, NaN]), keyHash ([1, 0/0])); %! assert_equal (keyMatch (single (NaN), single (0/0)), true); %! assert_equal (keyHash (single (NaN)), keyHash (single (0/0))); ## Signed zero is one key, from the literal and from arithmetic alike. %!test %! assert_equal (keyMatch (0, -0), true); %! assert_equal (keyHash (0), keyHash (-0)); %! assert_equal (keyMatch (0, 0 * -1), true); %! assert_equal (keyHash (0), keyHash (0 * -1)); %! assert_equal (keyMatch (single (0), single (-0)), true); %! assert_equal (keyHash (single (0)), keyHash (single (-0))); %! assert_equal (keyMatch ([0, 1], [-0, 1]), true); %! assert_equal (keyHash ([0, 1]), keyHash ([-0, 1])); ## Integers have neither signed zero nor NaN; they must be left alone. %!test %! assert_equal (keyMatch (int8 (0), int8 (0)), true); %! assert_equal (keyMatch (int8 (0), int8 (1)), false); %! assert_equal (keyMatch (uint8 (0), uint8 (0)), true); ## Complex: a zero imaginary part still compares equal to the real value. %!test %! assert_equal (keyMatch (complex (1, 0), 1), true); %! assert_equal (keyHash (complex (1, 0)), keyHash (1)); %! assert_equal (keyMatch (1 + 2i, 1 + 3i), false); ## Character arrays are compared over their whole contents. Until 1.3.1 this ## compared hash codes, and keyHash hashed only a character vector's first ## character, so any two equal-length vectors sharing it matched. %!test %! assert_equal (keyMatch ('hello', 'hxxxx'), false); %! assert_equal (keyMatch ('hello', 'hello'), true); %! assert_equal (keyHash ('hello'), keyHash ('hello')); %! assert_equal (keyMatch ('abc', 'abd'), false); %! assert_equal (keyMatch (['ab'; 'cd'], ['ab'; 'ce']), false); %! assert_equal (keyMatch (['ab'; 'cd'], ['ab'; 'cd']), true); %! assert_equal (keyHash (['ab'; 'cd']), keyHash (['ab'; 'cd'])); ## Composite keys keep their element boundaries. Flattening made {'ab','c'} ## and {'a','bc'} one key. %!test %! assert_equal (keyMatch ({'ab', 'c'}, {'a', 'bc'}), false); %! assert_equal (isequal (keyHash ({'ab', 'c'}), keyHash ({'a', 'bc'})), false); %! assert_equal (keyMatch ({'ab', 'c'}, {'ab', 'c'}), true); %! assert_equal (keyHash ({'ab', 'c'}), keyHash ({'ab', 'c'})); %! assert_equal (keyMatch ({'a', '', 'b'}, {'a', 'b', ''}), false); %! assert_equal (keyMatch ({'', 'ab'}, {'ab', ''}), false); ## The same for string arrays. %!test %! assert_equal (keyMatch (string ({'ab', 'c'}), string ({'a', 'bc'})), false); %! assert_equal (isequal (keyHash (string ({'ab', 'c'})), ... %! keyHash (string ({'a', 'bc'}))), false); %! assert_equal (keyMatch (string ({'ab', 'c'}), string ({'ab', 'c'})), true); %! assert_equal (keyMatch (string (missing), string (missing)), true); %! assert_equal (keyHash (string (missing)), keyHash (string (missing))); ## Empty and degenerate composites are distinguishable from one another. %!test %! assert_equal (keyMatch ({}, {}), true); %! assert_equal (keyHash ({}), keyHash ({})); %! assert_equal (keyMatch ({''}, {''}), true); %! assert_equal (keyHash ({''}), keyHash ({''})); %! assert_equal (isequal (keyHash ({}), keyHash ({''})), false); %! assert_equal (isequal (keyHash ({''}), keyHash ({'', ''})), false); %! assert_equal (keyMatch ('', ''), true); %! assert_equal (keyMatch ([], []), true); ## duration is counted in milliseconds, so the unit it was written in is not ## part of the key -- but a NaN duration is still one key. %!test %! assert_equal (keyMatch (seconds (1), milliseconds (1000)), true); %! assert_equal (keyHash (seconds (1)), keyHash (milliseconds (1000))); %! assert_equal (keyMatch (seconds (NaN), seconds (0/0)), true); %! assert_equal (keyHash (seconds (NaN)), keyHash (seconds (0/0))); %! assert_equal (keyMatch (seconds (1), seconds (2)), false); ## calendarDuration keeps calendar units distinct where they are not ## interchangeable, and merges them where they are. %!test %! assert_equal (keyMatch (calmonths (12), calyears (1)), true); %! assert_equal (keyHash (calmonths (12)), keyHash (calyears (1))); %! assert_equal (keyMatch (calmonths (1), caldays (30)), false); ## datetime is keyed on the instant, not on the wall clock and zone name. %!test %! a = datetime (2024, 3, 5, 12, 0, 0, 'TimeZone', 'UTC'); %! b = datetime (2024, 3, 5, 7, 0, 0, 'TimeZone', 'America/New_York'); %! assert_equal (keyMatch (a, b), true); %! assert_equal (keyHash (a), keyHash (b)); %! assert_equal (keyMatch (NaT, NaT), true); %! assert_equal (keyHash (NaT), keyHash (NaT)); ## An unzoned datetime names no instant, so it is never the same key as a ## zoned one that happens to share its wall clock. %!test %! a = datetime (2024, 3, 5, 12, 0, 0, 'TimeZone', 'UTC'); %! u = datetime (2024, 3, 5, 12, 0, 0); %! assert_equal (keyMatch (u, a), false); %! assert_equal (keyMatch (u, datetime (2024, 3, 5, 12, 0, 0)), true); %! assert_equal (keyHash (u), keyHash (datetime (2024, 3, 5, 12, 0, 0))); ## categorical identity is the label, plus the ordering only when ordinal. %!test %! a = categorical ({'a'}, {'a', 'b'}); %! b = categorical ({'a'}, {'a', 'c'}); %! c = categorical ({'a'}, {'a', 'b', 'c'}); %! d = categorical ({'a'}, {'a', 'c', 'b'}); %! p = categorical ({'a'}, {'a', 'b'}, 'Protected', true); %! assert_equal (keyMatch (a, b), true); %! assert_equal (keyHash (a), keyHash (b)); %! assert_equal (keyMatch (c, d), true); %! assert_equal (keyHash (c), keyHash (d)); %! assert_equal (keyMatch (a, p), true); %! assert_equal (keyHash (a), keyHash (p)); ## For ordinals the whole category ordering is part of identity, and an ## ordinal is never equal to a non-ordinal. %!test %! o = categorical ({'a'}, {'a', 'b'}, 'Ordinal', true); %! o2 = categorical ({'a'}, {'b', 'a'}, 'Ordinal', true); %! n = categorical ({'a'}, {'a', 'b'}); %! assert_equal (keyMatch (o, n), false); %! assert_equal (keyMatch (o, o2), false); %! assert_equal (keyMatch (o, categorical ({'a'}, {'a', 'b'}, 'Ordinal', ... %! true)), ... %! true); ## Undefined categorical elements compare equal to one another. %!test %! u = categorical ({''}, {'a'}); %! assert_equal (keyMatch (u, categorical ({''}, {'a'})), true); %! assert_equal (keyHash (u), keyHash (categorical ({''}, {'a'}))); %! assert_equal (keyMatch (u, categorical ({'a'}, {'a'})), false); ## The hash code rejects, but the class comparison is what keeps a value of ## one class from matching another should their codes ever collide. %!assert_equal (keyMatch (1, int8 (1)), false) %!assert_equal (keyMatch (1, uint8 (1)), false) %!assert_equal (keyMatch (1, true), false) %!assert_equal (keyMatch (string ('a'), 'a'), false) ## keyHash answers for every key type, which is what keyMatch's check asks. %!assert_equal (class (keyHash (0)), 'uint64') %!assert_equal (class (keyHash ('ab')), 'uint64') %!assert_equal (class (keyHash ({'ab', 'c'})), 'uint64') %!assert_equal (class (keyHash (string ('a'))), 'uint64') %!assert_equal (class (keyHash (seconds (1))), 'uint64') %!assert_equal (class (keyHash (calmonths (1))), 'uint64') %!assert_equal (class (keyHash (datetime (2024, 3, 5))), 'uint64') %!assert_equal (class (keyHash (categorical ({'a'}))), 'uint64') %!error keyMatch (2); %!error ... %! keyMatch (table ([1; 2]), table ([1; 2])) %!error keyMatch (table ([1; 2]), 1) %!error keyMatch (1, table ([1; 2])) ## keyHash's message is passed through, so a container whose contents cannot ## be hashed names the value that has no key rather than the container. A ## cell and a struct are both keys in their own right, so naming either would ## be wrong. %!error ... %! keyMatch ({table(1)}, {table(1)}) %!error keyMatch ({1}, {table(1)}) %!error keyMatch ({table(1)}, {1}) %!error ... %! keyMatch (struct ('a', table (1)), struct ('a', table (1))) ## struct, cell and function handle became keys when keyHash learned to hash ## them; keyMatch follows automatically, asking keyHash rather than keeping a ## list of its own. Measured against R2024a. ## A struct is keyed on its field names and values. Field order is not part ## of the key, but the class of a value inside one is. %!test %! assert_equal (keyMatch (struct ('a', 1), struct ('a', 1)), true); %! assert_equal (keyHash (struct ('a', 1)), keyHash (struct ('a', 1))); %!test %! assert_equal (keyMatch (struct ('a', 1, 'b', 2), struct ('b', 2, 'a', ... %! 1)), true); %! assert_equal (keyHash (struct ('a', 1, 'b', 2)), keyHash (struct ('b', 2, ... %! 'a', 1))); %!test %! assert_equal (keyMatch (struct ('a', 1), struct ('a', 2)), false); %! assert_equal (keyMatch (struct ('a', 1), struct ('b', 1)), false); %! assert_equal (keyMatch (struct ('a', 1), struct ('a', 1, 'b', 2)), false); %! assert_equal (keyMatch (struct ('a', 1), struct ('a', int8 (1))), false); ## A cell is keyed on its elements, so shape and nesting count. %!test %! assert_equal (keyMatch ({1, 2}, {1, 2}), true); %! assert_equal (keyHash ({1, 2}), keyHash ({1, 2})); %!test %! assert_equal (keyMatch ({1, 2}, {1; 2}), false); %! assert_equal (keyMatch ({{1}}, {1}), false); %! assert_equal (keyMatch ({1}, {int8(1)}), false); %!test %! assert_equal (keyMatch ({struct('a', 1)}, {struct('a', 1)}), true); %! assert_equal (keyMatch ({@sin}, {@sin}), true); ## A function handle is keyed on its text. Two named handles for the same ## function are one key; distinct anonymous handles never match, whatever ## they capture, so an anonymous handle can never be found again as a key. %!test %! assert_equal (keyMatch (@sin, @sin), true); %! assert_equal (keyHash (@sin), keyHash (@sin)); %!test %! assert_equal (keyMatch (@sin, @cos), false); %! assert_equal (keyMatch (@(x) x + 1, @(x) x + 1), false); %! assert_equal (keyMatch (@sin, 'sin'), false); ## The class is part of every key, so the new types do not collide with one ## another or with a numeric holding the same contents. %!test %! assert_equal (keyMatch (struct ('a', 1), {1}), false); %! assert_equal (keyMatch ({1}, 1), false); %! assert_equal (keyMatch (@sin, 1), false); pr0m1th3as-datatypes-9450b29/inst/milliseconds.m000066400000000000000000000042611527370255000215210ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{D} =} milliseconds (@var{X}) ## ## Fixed-time duration in milliseconds. ## ## @code{@var{D} = milliseconds (@var{X})} returns a @qcode{duration} array ## representing fixed-time duration milliseconds equivalent to the values in ## @var{X}, which must be a numeric array. ## ## @code{milliseconds} is also available as a method for @qcode{duration} ## arrays, in which case it performs the opposite conversion. ## ## @seealso{duration, years, days, hours, minutes, seconds, ## duration.milliseconds} ## @end deftypefn function out = milliseconds (x) if (nargin == 0) x = 1; elseif (! isnumeric (x)) error ("milliseconds: input array must be numeric."); elseif (! isreal (x)) error ("milliseconds: input array must be real."); endif out = duration (0, 0, 0, double (x), 'Format', 's'); endfunction %!demo %! ## `milliseconds` builds a duration from a number of milliseconds. %! %! milliseconds (1500) %!test %! X = magic (3); %! D = milliseconds (X); %! assert_equal (size (D), size (X)); %!test %! D = milliseconds ([1, 2, 3]); %! assert_equal (milliseconds (D), [1, 2, 3]); %!test %! D = milliseconds (int16 (1)); %! assert_equal (milliseconds (D), 1); %!test %! D = milliseconds (); %! assert_equal (milliseconds (D), 1); %!error milliseconds ("asd"); %!error milliseconds (1+i); pr0m1th3as-datatypes-9450b29/inst/minutes.m000066400000000000000000000042171527370255000205210ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{D} =} minutes (@var{X}) ## ## Fixed-time duration in minutes. ## ## @code{@var{D} = minutes (@var{X})} returns a @qcode{duration} array ## representing fixed-time duration minutes equivalent to the values in @var{X}, ## which must be a numeric array. ## ## @code{minutes} is also available as a method for @qcode{duration} arrays, in ## which case it performs the opposite conversion. ## ## @seealso{duration, years, days, hours, seconds, milliseconds, ## duration.minutes} ## @end deftypefn function out = minutes (x) if (nargin == 0) x = 1; elseif (! isnumeric (x)) error ("minutes: input array must be numeric."); elseif (! isreal (x)) error ("minutes: input array must be real."); endif out = duration (0, 0, 0, double (x) * 60000, 'Format', 'm'); endfunction %!demo %! ## `minutes` builds a duration from a number of minutes. %! %! minutes (90) %! %! ## Combine builders by adding them. %! hours (1) + minutes (30) %!test %! X = magic (3); %! D = minutes (X); %! assert_equal (size (D), size (X)); %!test %! D = minutes ([1, 2, 3]); %! assert_equal (minutes (D), [1, 2, 3]); %!test %! D = minutes (int16 (1)); %! assert_equal (minutes (D), 1); %!test %! D = minutes (); %! assert_equal (minutes (D), 1); %!error minutes ("asd"); %!error minutes (1+i); pr0m1th3as-datatypes-9450b29/inst/missing.m000066400000000000000000000732741527370255000205170ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef missing ## -*- texinfo -*- ## @deftp {datatypes} missing ## ## Array of missing values. ## ## A special class to represent missing data to other data types. ## ## @end deftp properties (SetAccess = private, Hidden) data = NaN endproperties methods (Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif __disp__ (this, 'missing', in_name); endfunction ## Custom display function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif __disp__ (this, 'missing'); endfunction endmethods ################################################################################ ## ** Create and convert 'missing' type ** ## ################################################################################ ## Available Methods ## ## ## ## 'missing' 'dispstrings' 'double' 'single' ## ## 'calendarDuration' 'categorical' 'datetime' 'duration' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {missing} {@var{M} =} missing () ## ## Create missing values. ## ## @code{missing} always returns a scalar missing value. Use @code{repmat} ## to expand a scalar missing value to a missing array of desired ## dimensions. ## ## @end deftypefn function this = missing () if (nargin > 0 ) error ("missing: too many input arguments."); endif endfunction endmethods methods (Hidden) function cstr = dispstrings (this) cstr = repmat ({''}, size (this)); endfunction ## A missing value has no text, so it cannot become one. 'dispstrings' ## still yields '' because that is a display placeholder, not a ## conversion. function cstr = cellstr (this) error ("missing.cellstr: cannot convert 'missing' to 'cell' type."); endfunction function out = double (this) out = this.data; endfunction function out = single (this) out = single (this.data); endfunction ## Each of the four conversions below hands whatever follows the array to ## the constructor it targets rather than refusing it: a missing value ## says nothing about a display format, a time zone or a category set, so ## a conversion naming one has to be able to say so. function out = calendarDuration (this, varargin) out = calendarDuration (NaN (size (this)), NaN, NaN, varargin{:}); endfunction function out = categorical (this, varargin) out = categorical (NaN (size (this)), varargin{:}); endfunction function out = datetime (this, varargin) out = NaT (size (this)); if (nargin > 1) out = datetime (out, varargin{:}); endif endfunction ## 'duration' reads a numeric input as an Nx3 [h m s] matrix, so build one ## row per element and restore the shape. NaN (size (this)) has a single ## column and is rejected. function out = duration (this, varargin) out = reshape (duration (NaN (numel (this), 3), varargin{:}), ... size (this)); endfunction ## A missing value equals nothing, itself included, exactly as NaN does ## under isequal. Any comparison reaching here involves one, so it is ## false whatever the other operands are. function TF = isequal (varargin) TF = false; endfunction ## Missing values are alike under isequaln, as NaN is. Every operand must ## be entirely missing and all shapes must agree. function TF = isequaln (varargin) TF = false; sz = size (varargin{1}); for i = 1:numel (varargin) if (! isequal (size (varargin{i}), sz)) return; elseif (! allmissing (varargin{i})) return; endif endfor TF = true; endfunction endmethods ################################################################################ ## ** Summary Information ** ## ################################################################################ ## Available Methods ## ## ## ## 'size' 'ndims' 'numel' 'keyHash' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {missing} {@var{sz} =} size (@var{M}) ## @deftypefnx {missing} {@var{dim_sz} =} size (@var{M}, @var{dim}) ## @deftypefnx {missing} {@var{dim_sz} =} size (@var{M}, @var{d1}, @var{d2}, @dots{}) ## @deftypefnx {missing} {[@var{rows}, @var{columns}, @dots{}, @var{dim_n_sz}] =} size (@dots{}) ## ## Return the size of a missing array. ## ## @code{@var{sz} = size (@var{M})} returns a row vector with the size ## (number of elements) of each dimension for the missing array @var{M}. ## ## @code{@var{dim_sz} = size (@var{M}, @var{dim})} returns the size of ## the corresponding dimension specified in @var{dim}. If @var{dim} is a ## vector, then @var{dim_sz} is a vector of the same length and with each ## element corresponding to a specified dimension. Multiple dimensions may ## also be specified as separate arguments. ## ## With a single output argument, @code{size} returns a row vector. When ## called with multiple output arguments, @code{size} returns the size of ## dimension N in the Nth argument. ## ## @end deftypefn function varargout = size (this, varargin) if (! isempty (varargin)) sz = size (this.data, varargin{:}); else sz = size (this.data); endif if (nargout == 0 || nargout == 1) varargout{1} = sz; elseif (numel (sz) != nargout) error (strcat ("missing.size: nargout does not match the", ... " number of requested dimensions.")); else for i = 1:nargout varargout{i} = sz(i); endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {missing} {@var{out} =} ndims (@var{M}) ## ## Number of dimensions in a missing array. ## ## @code{@var{out} = ndims (@var{M})} returns the number of dimensions of ## the missing array @var{M}. ## ## @end deftypefn function out = ndims (this) out = ndims (this.data); endfunction ## -*- texinfo -*- ## @deftypefn {missing} {@var{out} =} numel (@var{M}) ## ## Total number of elements in a missing array. ## ## @code{@var{out} = numel (@var{M})} returns the number of elements in the ## missing array @var{M}. ## ## @end deftypefn function out = numel (this) out = numel (this.data); endfunction ## -*- texinfo -*- ## @deftypefn {missing} {@var{key} =} keyHash (@var{M}) ## @deftypefnx {missing} {@var{key} =} keyHash (@var{M}, @var{base}) ## ## Generate a hash code for a missing array. ## ## @code{@var{key} = keyHash (@var{M})} returns a 64-bit hash code of the ## missing array @var{M} as a @qcode{uint64} scalar. Every element of a ## missing array is missing, so the code depends on its size alone: two ## missing arrays of the same size are the same key, and @code{keyMatch} ## reports them so. A missing array is a different key from a missing ## value of another type, such as @code{NaN} or a missing string. ## ## @code{@var{key} = keyHash (@var{M}, @var{base})} also generates a ## 64-bit hash code using @var{base} as the offset basis for the FNV-1a ## hash algorithm. @var{base} must be a @qcode{uint64} integer type ## scalar. Use this syntax to cascade @code{keyHash} on multiple objects ## for which a single hash code is required. ## ## Note that unlike MATLAB, this implementation does not use any random ## seed. As a result, @code{keyHash} will always generate the exact same ## hash key for any particular input across different workers and Octave ## sessions. ## ## @seealso{keyHash, keyMatch, dictionary} ## @end deftypefn function key = keyHash (this, base = []) size_str = sprintf ('%dx', size (this.data))(1:end-1); init_str = [size_str class(this)]; if (! isempty (base)) if (! (isscalar (base) && isa (base, 'uint64'))) error ("missing.keyHash: BASE must be a UINT64 scalar."); endif key = __ckeyHash__ (init_str, base); else key = __ckeyHash__ (init_str); endif endfunction endmethods ################################################################################ ## ** Query Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'iscolumn' 'isempty' 'ismatrix' 'ismissing' ## ## 'isrow' 'isscalar' 'isvector' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {missing} {@var{TF} =} iscolumn (@var{M}) ## ## Return true if missing array is a column vector. ## ## @code{@var{TF} = iscolumn (@var{M})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the missing array @var{M} is a column vector ## and @qcode{false} otherwise. A column vector is a 2-D array for which ## @code{size (@var{X})} returns @code{[@var{N}, 1]} with non-negative ## @var{N}. ## ## @end deftypefn function out = iscolumn (this) out = iscolumn (this.data); endfunction ## -*- texinfo -*- ## @deftypefn {missing} {@var{TF} =} isempty (@var{M}) ## ## Return true if missing array is empty. ## ## @code{@var{TF} = isempty (@var{M})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the missing array @var{M} is empty and ## @qcode{false} otherwise. ## ## @end deftypefn function out = isempty (this) out = isempty (this.data); endfunction ## -*- texinfo -*- ## @deftypefn {missing} {@var{TF} =} ismatrix (@var{M}) ## ## Return true if missing array is a 2-D array. ## ## @code{@var{TF} = ismatrix (@var{M})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the missing array @var{M} is a matrix and ## @qcode{false} otherwise. A matrix is an array of any type where ## @code{ndims (@var{X}) == 2} and for which @code{size (@var{X})} returns ## @code{[@var{H}, @var{W}]} with non-negative @var{H} and @var{W}. ## ## @end deftypefn function out = ismatrix (this) out = ismatrix (this.data); endfunction ## -*- texinfo -*- ## @deftypefn {missing} {@var{TF} =} ismissing (@var{M}) ## @deftypefnx {missing} {@var{TF} =} ismissing (@var{A}, @var{M}) ## ## Return true for each element in missing array. ## ## @code{@var{TF} = ismissing (@var{M})} returns a logical array @var{TF} ## of the same size as @var{M} containing @qcode{true} in every element. ## ## @code{@var{TF} = ismissing (@var{A}, @var{M})}, with a missing array ## @var{M} as the indicator, marks the standard missing values of @var{A}, ## as @code{ismissing (@var{A})} does: @qcode{NaN} in @qcode{double} and ## @qcode{single} arrays and empty character vectors in cell arrays of ## character vectors. Integer, @qcode{logical} and @qcode{char} arrays and ## other cell arrays have no standard missing value, and @var{M} is refused ## for them. Classes with an @code{ismissing} method of their own, such as ## @code{duration}, @code{datetime}, @code{categorical}, @code{string}, ## @code{calendarDuration} and @code{table}, take @var{M} in that method. ## ## @end deftypefn function TF = ismissing (this, varargin) if (nargin > 2) error ("missing.ismissing: too many input arguments."); endif if (isempty (varargin) || isa (this, 'missing')) if (! isempty (varargin) && ! isa (varargin{1}, 'missing')) error (strcat ("missing.ismissing: a missing array takes", ... " only 'missing' as INDICATOR.")); endif TF = true (size (this)); elseif (isfloat (this)) TF = isnan (this); elseif (iscellstr (this)) TF = cellfun ('isempty', this); else error (strcat ("missing.ismissing: a 'missing' indicator does", ... " not apply to '%s' data."), class (this)); endif endfunction ## -*- texinfo -*- ## @deftypefn {missing} {@var{TF} =} isrow (@var{M}) ## ## Return true if missing array is a row vector. ## ## @code{@var{TF} = isrow (@var{M})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the missing array @var{M} is a row vector ## and @qcode{false} otherwise. A row vector is a 2-D array for which ## @code{size (@var{X})} returns @code{[1, @var{N}]} with non-negative ## @var{N}. ## ## @end deftypefn function out = isrow (this) out = isrow (this.data); endfunction ## -*- texinfo -*- ## @deftypefn {missing} {@var{TF} =} isscalar (@var{M}) ## ## Return true if missing array is a scalar. ## ## @code{@var{TF} = isscalar (@var{M})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the missing array @var{M} is also a scalar ## and @qcode{false} otherwise. A scalar is a single element object for ## which @code{size (@var{X})} returns @code{[1, 1]}. ## ## @end deftypefn function out = isscalar (this) out = isscalar (this.data); endfunction ## -*- texinfo -*- ## @deftypefn {missing} {@var{TF} =} isvector (@var{M}) ## ## Return true if missing array is a vector. ## ## @code{@var{TF} = isvector (@var{M})} returns a logical scalar @var{TF}, ## which is @qcode{true} if the missing array @var{M} is a vector and ## @qcode{false} otherwise. A vector is a 2-D array for which one of the ## dimensions is equal to 1 (either @math{1*N} or @math{N*1}). By ## definition, a scalar is also a vector. ## ## @end deftypefn function out = isvector (this) out = isvector (this.data); endfunction endmethods ################################################################################ ## ** Relational Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'eq' 'ge' 'gt' 'le' ## ## 'lt' 'ne' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {missing} {@var{TF} =} eq (@var{A}, @var{B}) ## ## Equality for missing arrays. ## ## @code{@var{TF} = eq (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} == @var{B}} and returns a logical array of the ## same size as the largest input with its elements set to @qcode{false}. ## @var{A} and @var{B} must be size compatible, which translates to they can ## be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. ## ## One of the input arguments may also be any type of array. Any comparison ## with missing arrays always returns @qcode{false}. ## ## @end deftypefn function TF = eq (A, B) A = nan (size (A)); B = nan (size (B)); try TF = A == B; catch error ("missing.eq: arrays have incompatible sizes."); end_try_catch endfunction ## -*- texinfo -*- ## @deftypefn {missing} {@var{TF} =} ge (@var{A}, @var{B}) ## ## Greater than or equal to for missing arrays. ## ## @code{@var{TF} = ge (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} >= @var{B}} and returns a logical array of the ## same size as the largest input with its elements set to @qcode{false}. ## @var{A} and @var{B} must be size compatible, which translates to they can ## be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. ## ## One of the input arguments may also be any type of array. Any comparison ## with missing arrays always returns @qcode{false}. ## ## @end deftypefn function TF = ge (A, B) A = nan (size (A)); B = nan (size (B)); try TF = A >= B; catch error ("missing.ge: arrays have incompatible sizes."); end_try_catch endfunction ## -*- texinfo -*- ## @deftypefn {missing} {@var{TF} =} gt (@var{A}, @var{B}) ## ## Greater than for missing arrays. ## ## @code{@var{TF} = gt (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} > @var{B}} and returns a logical array of the ## same size as the largest input with its elements set to @qcode{false}. ## @var{A} and @var{B} must be size compatible, which translates to they can ## be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. ## ## One of the input arguments may also be any type of array. Any comparison ## with missing arrays always returns @qcode{false}. ## ## @end deftypefn function TF = gt (A, B) A = nan (size (A)); B = nan (size (B)); try TF = A > B; catch error ("missing.gt: arrays have incompatible sizes."); end_try_catch endfunction ## -*- texinfo -*- ## @deftypefn {missing} {@var{TF} =} le (@var{A}, @var{B}) ## ## Less than or equal to for missing arrays. ## ## @code{@var{TF} = le (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} <= @var{B}} and returns a logical array of the ## same size as the largest input with its elements set to @qcode{false}. ## @var{A} and @var{B} must be size compatible, which translates to they can ## be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. ## ## One of the input arguments may also be any type of array. Any comparison ## with missing arrays always returns @qcode{false}. ## ## @end deftypefn function TF = le (A, B) A = nan (size (A)); B = nan (size (B)); try TF = A <= B; catch error ("missing.le: arrays have incompatible sizes."); end_try_catch endfunction ## -*- texinfo -*- ## @deftypefn {missing} {@var{TF} =} lt (@var{A}, @var{B}) ## ## Less than for missing arrays. ## ## @code{@var{TF} = lt (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} < @var{B}} and returns a logical array of the ## same size as the largest input with its elements set to @qcode{false}. ## @var{A} and @var{B} must be size compatible, which translates to they can ## be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. ## ## One of the input arguments may also be any type of array. Any comparison ## with missing arrays always returns @qcode{false}. ## ## @end deftypefn function TF = lt (A, B) A = nan (size (A)); B = nan (size (B)); try TF = A < B; catch error ("missing.lt: arrays have incompatible sizes."); end_try_catch endfunction ## -*- texinfo -*- ## @deftypefn {missing} {@var{TF} =} ne (@var{A}, @var{B}) ## ## Not equal for missing arrays. ## ## @code{@var{TF} = ne (@var{A}, @var{B})} is the equivalent of the syntax ## @code{@var{TF} = @var{A} != @var{B}} and returns a logical array of the ## same size as the largest input with its elements set to @qcode{false}. ## @var{A} and @var{B} must be size compatible, which translates to they can ## be the same size, one can be scalar, or for every dimension, their ## dimension sizes must be equal or one of them must be 1. ## ## One of the input arguments may also be any type of array. Any comparison ## with missing arrays always returns @qcode{false}. ## ## @end deftypefn function TF = ne (A, B) A = nan (size (A)); B = nan (size (B)); try TF = A != B; catch error ("missing.ne: arrays have incompatible sizes."); end_try_catch endfunction endmethods ################################################################################ ## ** Array Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'cat' 'horzcat' 'vertcat' 'repmat' ## ## 'reshape' 'squeeze' 'transpose' 'ctranspose' ## ## ## ################################################################################ methods (Hidden) function out = cat (dim, varargin) datatype = ''; for i = 1:numel (varargin) if (isa (varargin{i}, 'missing')) continue elseif (isa (varargin{i}, 'calendarDuration')) datatype = 'calendarDuration'; elseif (isa (varargin{i}, 'categorical')) datatype = 'categorical'; elseif (isa (varargin{i}, 'datetime')) datatype = 'datetime'; elseif (isa (varargin{i}, 'double')) datatype = 'double'; elseif (isa (varargin{i}, 'duration')) datatype = 'duration'; elseif (isa (varargin{i}, 'single')) datatype = 'single'; elseif (isa (varargin{i}, 'string')) datatype = 'string'; else error ("missing.cat: cannot convert 'missing' to '%s' type.", ... class (varargin{i})); endif endfor if (isempty (datatype)) sz = cellfun (@(x) x.data, varargin, 'UniformOutput', false); out = varargin{1}; out.data = cat (dim, sz{:}); else args = varargin; for i = 1:numel (varargin) if (isa (varargin{i}, 'missing')) switch datatype case 'calendarDuration' args{i} = calendarDuration (NaN (size (varargin{i})), NaN, NaN); case 'categorical' args{i} = categorical (NaN (size (varargin{i}))); case 'datetime' ## The filler carries the real operand's zone and format: an ## unzoned NaT cannot sit in a zoned array at all, and a ## default-format one would render the result wrongly. k = find (cellfun (@(x) isa (x, 'datetime'), varargin), 1); if (isempty (varargin{k}.TimeZone)) args{i} = NaT (size (varargin{i}), ... 'Format', varargin{k}.Format); else args{i} = NaT (size (varargin{i}), ... 'TimeZone', varargin{k}.TimeZone, ... 'Format', varargin{k}.Format); endif case 'double' args{i} = NaN (size (varargin{i})); case 'duration' ## The result takes its display format from the real operand, ## not from the all-NaN filler built here. k = find (cellfun (@(x) isa (x, 'duration'), varargin), 1); fmt = varargin{k}.Format; args{i} = reshape (duration (NaN (numel (varargin{i}), 3), ... 'Format', fmt), ... size (varargin{i})); case 'single' args{i} = single (NaN (size (varargin{i}))); case 'string' args{i} = string (NaN (size (varargin{i}))); endswitch endif endfor out = cat (dim, args{:}); endif endfunction function out = horzcat (varargin) out = cat (2, varargin{:}); endfunction function out = vertcat (varargin) out = cat (1, varargin{:}); endfunction function this = repmat (this, varargin) this.data = repmat (this.data, varargin{:}); endfunction function this = reshape (this, varargin) this.data = reshape (this.data, varargin{:}); endfunction function this = squeeze (this, varargin) this.data = squeeze (this.data, varargin{:}); endfunction function this = ctranspose (this, varargin) this.data = ctranspose (this.data, varargin{:}); endfunction function this = transpose (this, varargin) this.data = transpose (this.data, varargin{:}); endfunction endmethods ################################################################################ ## ** Reference and Assignment Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'subsref' 'subsasgn' ## ## ## ################################################################################ methods (Hidden) ## Class specific subscripted reference function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case '()' out = this; out.data = this.data(s.subs{:}); case '{}' error (strcat ("missing.subsref: '{}' invalid indexing", ... " for referencing values. Use '()' instead.")); case '.' error (strcat ("missing.subsref: '.' invalid indexing", ... " for referencing values. Use '()' instead.")); endswitch ## Chained references if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction ## Class specific subscripted assignment function this = subsasgn (this, s, val) if (numel (s) > 1) error ("missing.subsasgn: chained subscripts not allowed."); endif switch (s.type) case '()' if (isa (val, "missing")) this.data(s.subs{:}) = val.data; elseif (isempty (val)) this.data(s.subs{:}) = []; else error ("missing.subsasgn: unable to perform assignment."); endif case '{}' error (strcat ("missing.subsasgn: '{}' invalid indexing", ... " for assigning values. Use '()' instead.")); case '.' error (strcat ("missing.subsasgn: '.' invalid indexing", ... " for assigning values. Use '()' instead.")); endswitch endfunction endmethods endclassdef ## True when every element of V is a missing value. Each type answers for ## itself through its own 'ismissing' method; the floating types have no such ## method and use NaN, and a type carrying no missing marker never qualifies. function TF = allmissing (V) if (isa (V, 'missing')) TF = true; elseif (isfloat (V)) TF = all (isnan (V)(:)); elseif (isobject (V) && ismember ('ismissing', methods (V))) TF = all (ismissing (V)(:)); else TF = false; endif endfunction ## 'pkg test' reads the BISTs from the file it is testing, so a class ## whose suite lives in inst/tests/ reads as carrying none. This block ## answers for it and checks nothing itself. %!test %! ## The suite for this class is inst/tests/missing.m-tst. pr0m1th3as-datatypes-9450b29/inst/ods2struct.m000066400000000000000000000143251527370255000211520ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{s} =} ods2struct (@var{filename}) ## ## Read every sheet of an OpenDocument spreadsheet into a scalar structure. ## ## @code{@var{s} = ods2struct (@var{filename})} reads each data sheet of the ## OpenDocument spreadsheet named by @var{filename} into a @code{table} and ## returns a scalar structure with one field per sheet, in sheet order. A ## sheet written from a @code{timetable} is tagged as such and comes back as ## one, so a workbook of both round-trips through @code{struct2ods}. ## ## A sheet named as another sheet's event table is @strong{consumed}: it is ## attached to the timetable that names it and gets no field of its own, since ## returning the same event table twice would leave an edit to one copy ## disagreeing with the other on the rewrite. Both ## the compressed @qcode{.ods} and the flat @qcode{.fods} formats are read. ## Each sheet is reconstructed exactly as by @code{ods2table}; it is the inverse ## of @code{struct2ods}. ## ## A sheet name that is not a valid structure field name is canonicalised with ## @code{matlab.lang.makeValidName} (and made unique if two sheet names ## collide). Whenever the field name differs from the sheet name, the original ## sheet name is stored on that field's table as the @qcode{'ActualSheetName'} ## custom property, so a subsequent @code{struct2ods} restores the exact sheet ## name. ## ## @seealso{struct2ods, ods2table, table.table2ods, readtable} ## @end deftypefn function s = ods2struct (filename) if (nargin != 1) print_usage (); endif if (! (ischar (filename) || iscellstr (filename) || isa (filename, 'string'))) error (strcat ("ods2struct: FILENAME must be a character vector,", ... " cellstr, or string.")); endif file = char (cellstr (filename)); ## Enumerate the data sheet names (the first output doubles as an error ## probe). [data, ~, ~, names, preamble] = __ods2table__ (file); if (ischar (data)) error ("ods2struct: %s", data); endif ## A sheet named as the event table of another is consumed by it and gets no ## field of its own; returning it twice would put the same event table in ## two places, and an edit to one copy would disagree with the other on the ## rewrite. [xrefs, errmsg] = __odscrossrefs__ (preamble, names); if (! isempty (errmsg)) error ("ods2struct: %s", errmsg); endif consumed = {xrefs.to}; s = struct (); usedFields = {}; for k = 1:numel (names) sn = names{k}; if (any (strcmp (sn, consumed))) continue; endif ## A sheet whose leading column is tagged as row times came from a ## timetable and goes back to being one; every other sheet is a table, ## including one whose first variable merely happens to be a datetime. [R, rowTimesName] = ods2table (file, 'Sheet', sn); if (! isempty (rowTimesName)) R = table2timetable (R, 'RowTimes', rowTimesName); ix = find (strcmp (sn, {xrefs.from}), 1); if (! isempty (ix)) R = __odsattach__ (R, file, xrefs(ix), names, 'ods2struct'); endif endif fn = matlab.lang.makeValidName (sn); ## Make the field name unique if canonicalisation collided with an earlier ## one; the ActualSheetName property below preserves the true sheet name. base = fn; j = 1; while (any (strcmp (fn, usedFields))) fn = sprintf ('%s_%d', base, j); j += 1; endwhile usedFields{end+1} = fn; ## Stash the original sheet name whenever the field name had to change. if (! strcmp (fn, sn)) R = addprop (R, 'ActualSheetName', 'table'); R.Properties.CustomProperties.ActualSheetName = sn; endif s.(fn) = R; endfor endfunction %!demo %! ## `ods2struct` is the inverse of `struct2ods`: it reads *every* data sheet %! ## of a workbook into one scalar struct, one field per sheet, in sheet order. %! %! wb.Patients = table ({'Li'; 'Diaz'}, [38; 40], 'VariableNames', {'Name', ... %! 'Age'}); %! wb.Visits = table ([1; 2; 3], 'VariableNames', {'Visit'}); %! filename = fullfile (tempdir (), 'clinic.ods'); %! struct2ods (filename, wb); %! %! s = ods2struct (filename); %! s.Patients %! %! delete (filename); %!test # round-trip a multi-sheet workbook written by struct2ods %! s.alpha = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); %! s.beta = table ([10.5; 20.5], 'VariableNames', {'v'}); %! fn = [tempname() '.ods']; %! unwind_protect %! struct2ods (fn, s); %! r = ods2struct (fn); %! assert_equal (fieldnames (r), {'alpha'; 'beta'}); %! assert_equal (r.alpha.x, [1; 2; 3]); %! assert_equal (r.beta.v, [10.5; 20.5]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!test # an odd sheet name is canonicalised and stashed in ActualSheetName %! T = table ([7; 8], 'VariableNames', {'v'}); %! T = addprop (T, 'ActualSheetName', 'table'); %! T.Properties.CustomProperties.ActualSheetName = 'My Sheet!'; %! s.only = T; %! fn = [tempname() '.fods']; %! unwind_protect %! struct2ods (fn, s); %! r = ods2struct (fn); %! f = fieldnames (r); %! assert_equal (numel (f), 1); %! assert_equal (isvarname (f{1}), true); %! assert_equal (r.(f{1}).Properties.CustomProperties.ActualSheetName, ... %! 'My Sheet!'); %! assert_equal (r.(f{1}).v, [7; 8]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!error ... %! ods2struct (42) %!error ... %! ods2struct ([tempname() '.ods']) pr0m1th3as-datatypes-9450b29/inst/ods2table.m000066400000000000000000001426721527370255000207240ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{tbl} =} ods2table (@var{filename}) ## @deftypefnx {datatypes} {@var{tbl} =} ods2table (@var{filename}, @qcode{'Sheet'}, @var{sheet}) ## @deftypefnx {datatypes} {[@var{tbl}, @var{rowTimesName}] =} ods2table (@dots{}) ## ## Read an OpenDocument spreadsheet file into a table. ## ## @code{@var{tbl} = ods2table (@var{filename})} reads the OpenDocument ## spreadsheet named by @var{filename}, which may be a character vector, a ## cellstr, or a string scalar, and returns it as a @code{table}. Both the ## compressed @qcode{.ods} and the flat @qcode{.fods} formats are read; the ## format is detected from the file contents, not its extension. ## ## The following @var{Name}-@var{Value} options are supported: ## ## @multitable @columnfractions 0.30 0.70 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'Sheet'} @tab The sheet to read, selected by its name (a ## character vector or string scalar) or by a 1-based index over the data ## sheets. The default is the first data sheet. ## @item @qcode{'ReadVariableNames'} @tab A logical scalar specifying whether ## the variable names are taken from the file (default @qcode{true}). Setting ## it @qcode{false} numbers the variables @qcode{Var1}, @qcode{Var2}, and so on. ## @item @qcode{'ReadRowNames'} @tab A logical scalar specifying whether the ## table takes row names from the file (default @qcode{true}). Setting it ## @qcode{false} leaves the table without row names. ## @item @qcode{'VariableNamesRow'} @tab A nonnegative integer scalar naming the ## row of the sheet that holds the variable names (default @qcode{1}). Zero is ## equivalent to setting @qcode{'ReadVariableNames'} to @qcode{false}. It ## applies only to a sheet with no metadata; a sheet written by ## @code{table2ods} records the names, so there is no row to name. ## @item @qcode{'RowNamesColumn'} @tab A nonnegative integer scalar naming the ## column of the sheet that holds the row names (default @qcode{0}). Zero is ## equivalent to setting @qcode{'ReadRowNames'} to @qcode{false}. It applies ## only to a sheet with no metadata, which says nothing about which column holds ## row names; a sheet written by @code{table2ods} records the column. A leading ## column headed @qcode{Row}, which is what @code{writetable} writes for the row ## names, is taken as the row names without being named here. ## @end multitable ## ## When the file carries the hidden @qcode{__datatypes_meta__} sheet written by ## the @code{table2ods} method, the variable types, descriptions and units are ## restored from it and the variable names from the rows heading the data ## sheet, and @code{date} and @code{time} cells are ## reconstructed as @code{datetime} and @code{duration} arrays. Integers are ## restored without loss of precision and missing cells become @code{NaN}, ## @code{NaT}, or missing strings as appropriate. A file written before the ## names moved to the data sheet carries them on the metadata sheet, and is ## read just as well. A sheet whose cells contradict the types its metadata ## declares, such as text under a numeric type after another program rewrote ## the sheet, raises an error. ## ## A sheet written from a timetable tags its leading column as row times. A ## table has no row times, so the column is returned as an ordinary leading ## variable under the row dimension name, and the second output ## @var{rowTimesName} names it; @var{rowTimesName} is empty for every other ## sheet, which is how @code{ods2struct} tells a sheet that held a timetable ## from one whose first variable merely happens to be a @code{datetime}. Read ## such a sheet with @code{ods2timetable} to get the timetable back. ## ## When the metadata sheet is absent (a spreadsheet written by another ## application) the variable types are inferred from the cell value types. ## Where no names are available the variables are numbered, and the columns can ## then no longer be grouped, so a multicolumn variable comes back as separate ## variables and a nested table as flat columns. ## ## A @code{datetime} or @code{duration} variable is restored exactly, along ## with its @qcode{Format} and, for a zone-aware @code{datetime}, its ## @qcode{TimeZone}. A cell that carries no value at all is a missing entry, ## which is not the same as a cell holding an empty string. The following ## round-trip limitation applies, mirroring @code{csv2table}: ## @code{calendarDuration} and @code{categorical} variables are returned as cell ## arrays of character vectors and their values are not reconstructed. ## ## @end deftypefn function [tbl, rowTimesName] = ods2table (filename, varargin) if (nargin < 1) print_usage (); endif if (! (ischar (filename) || iscellstr (filename) || isa (filename, 'string'))) error (strcat ("ods2table: FILENAME must be a character vector,", ... " cellstr, or string.")); endif file = char (cellstr (filename)); ## Empty unless the sheet's leading column was tagged as row times, in which ## case it names the variable that column became. rowTimesName = ''; if (mod (numel (varargin), 2) != 0) error ("ods2table: name-value arguments must be in pairs."); endif ## Parse optional paired arguments optNames = {'Sheet', 'ReadVariableNames', 'ReadRowNames', ... 'VariableNamesRow', 'RowNamesColumn'}; ## A sheet with no metadata says nothing about which column holds row names, ## and taking the first one uninvited would leave a single-column sheet with ## no variables at all, so the column defaults to none. dfValues = {[], true, true, 1, 0}; [sheet, readVarNames, readRowNames, varNamesRow, rowNamesCol, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! (islogical (readVarNames) && isscalar (readVarNames))) error ("ods2table: 'ReadVariableNames' must be a logical scalar."); endif if (! (islogical (readRowNames) && isscalar (readRowNames))) error ("ods2table: 'ReadRowNames' must be a logical scalar."); endif if (! (isnumeric (varNamesRow) && isscalar (varNamesRow) && varNamesRow == fix (varNamesRow) && varNamesRow >= 0)) error (strcat ("ods2table: 'VariableNamesRow' must be a non-negative", ... " integer.")); endif if (! (isnumeric (rowNamesCol) && isscalar (rowNamesCol) && rowNamesCol == fix (rowNamesCol) && rowNamesCol >= 0)) error (strcat ("ods2table: 'RowNamesColumn' must be a non-negative", ... " integer.")); endif ## Either switch turns its position off; the position alone can too. if (! readVarNames) varNamesRow = 0; endif if (! readRowNames) rowNamesCol = 0; endif if (! isempty (args)) error ("ods2table: invalid optional paired argument."); endif if (! (isempty (sheet) || (ischar (sheet) && isrow (sheet)) ... || (isa (sheet, 'string') && isscalar (sheet)) ... || (isnumeric (sheet) && isscalar (sheet)))) error (strcat ("ods2table: 'Sheet' must be a sheet name or a scalar", ... " index.")); endif if (isa (sheet, 'string')) sheet = char (sheet); endif ## Read the workbook into raw grids [data, vtype, meta] = __ods2table__ (file, sheet); if (ischar (data)) error ("ods2table: %s", data); endif ## No metadata sheet -> infer everything from the data cell value types if (isempty (meta)) tbl = ods_autodetect (data, vtype, varNamesRow, rowNamesCol, readRowNames); return; endif ## Parse the descriptive comment on the metadata sheet hdr = sscanf (meta{1,1}, ... strcat ("# varTypes %d rows; varNames %d rows;", ... " varDescriptions %d rows; varUnits %d rows.")); if (numel (hdr) != 4) error ("ods2table: malformed metadata header in '%s'.", file); endif Trows = hdr(1); Nrows = hdr(2); Drows = hdr(3); Urows = hdr(4); ## A table with no variables round-trips to an empty table. A written table ## always has a type row, so an absent one means there was nothing to write; ## an absent NAME row means only that the names were not written. if (Trows == 0) tbl = table (); return; endif ## Split the metadata rows into type, name, description, and unit blocks. ## The variable names sit on the data sheet, above the data; a file written ## before they moved carries them on the metadata sheet instead, which its ## extra block of rows gives away. body = meta(2:end,:); namesOnMeta = (size (body, 1) == Trows + Nrows + Drows + Urows && Nrows > 0); T = body(1:Trows,:); body(1:Trows,:) = []; if (namesOnMeta) N = body(1:Nrows,:); body(1:Nrows,:) = []; elseif (Nrows > 0) N = data(1:Nrows,:); data(1:Nrows,:) = []; vtype(1:Nrows,:) = []; else N = {}; endif if (Drows) D = body(1:Drows,:); body(1:Drows,:) = []; else D = {}; endif if (Urows) U = body(1:Urows,:); else U = {}; endif ## When the table has no rows the data sheet is empty, so the data grid comes ## back without columns; restore the expected column count from the metadata. metacols = size (T, 2); if (size (data, 2) != metacols) data = cell (size (data, 1), metacols); vtype = cell (size (data, 1), metacols); endif ## A leading column of row times is tagged 'RowTimes' ahead of its own ## type. A table has no row times, so the column is kept as an ordinary ## leading variable under the row dimension name written beside it, which is ## what MATLAB's 'readtable' does with a file holding a timetable; ## 'ods2timetable' reads the same sheet as the timetable it came from. hasRowTimes = strncmp (T{1,1}, 'RowTimes|', 9); if (hasRowTimes) T{1,1} = T{1,1}(10:end); endif ## A leading RowNames column is tagged in the type row; the column is ## consumed either way, and kept only when the caller asked for it. RowNames = {}; rowNamesTagged = strcmp (T{1,1}, 'RowNames'); if (rowNamesTagged) if (readRowNames) RowNames = ods_column_strings (data(:,1), vtype(:,1)); endif data(:,1) = []; vtype(:,1) = []; T(:,1) = []; if (! isempty (N)), N(:,1) = []; endif if (! isempty (D)), D(:,1) = []; endif if (! isempty (U)), U(:,1) = []; endif endif ## A sheet replaced by another writer can leave metadata that no longer fits ## it; a text, date or time cell under a numeric or logical type gives it ## away. The text Inf, -Inf and NaN is how an infinity is written, not a ## mismatch. numvartype = {'double', 'single', 'int8', 'uint8', 'int16', 'uint16', ... 'int32', 'uint32', 'int64', 'uint64', 'logical'}; number = @(x) ischar (x) && any (strcmpi (x, {'Inf', '-Inf', 'NaN'})); for c = 1:min (size (T, 2), size (vtype, 2)) col = T(:,c); col = col(! cellfun (@isempty, col)); text = ismember (vtype(:,c), {'date', 'time'}) ... | (strcmp (vtype(:,c), 'string') & ! cellfun (number, data(:,c))); if (! isempty (col) && any (strcmp (col{end}, numvartype)) && any (text)) error (strcat ("ods2table: the metadata in '%s' does not match its", ... " sheet: column %d holds text but is declared '%s'."), ... file, c + rowNamesTagged, col{end}); endif endfor ## Without a name block there is nothing to group the columns by, so each ## column is one variable, numbered, and carries the innermost type it was ## written with. A nested table cannot be rebuilt without the names that ## tagged its columns. if (Nrows == 0 || ! readVarNames) ncol = size (T, 2); varNames = arrayfun (@(c) sprintf ("Var%d", c), 1:ncol, ... 'UniformOutput', false); varValues = cell (1, ncol); for c = 1:ncol col = T(:,c); col = col(! cellfun (@isempty, col)); varValues{c} = ods_cell2var (data(:,c), vtype(:,c), col{end}); endfor if (isempty (RowNames)) tbl = table (varValues{:}, 'VariableNames', varNames); else tbl = table (varValues{:}, 'VariableNames', varNames, ... 'RowNames', RowNames); endif if (hasRowTimes && width (tbl) > 0) rowTimesName = tbl.Properties.VariableNames{1}; endif return; endif ## Group the columns that share a variable name into one variable, rebuild ## any nested table or structure from the deeper header rows, and restore the ## descriptions and units. The value types travel alongside the data so that ## the leaf conversion still sees them. tbl = __cell2tbl__ (data, T, N, D, U, RowNames, ... @(varC, varVT, typestr) ods_cell2var (varC, varVT, ... typestr), vtype); if (hasRowTimes && width (tbl) > 0) rowTimesName = tbl.Properties.VariableNames{1}; endif endfunction ## Reconstruct one table variable (n-by-k) from its data and value-type columns ## and its declared type T. Missing cells (empty value-type) become NaN, NaT, ## or missing strings according to the type. function v = ods_cell2var (C, VT, T) numvartype = {'double', 'single', 'int8', 'uint8', 'int16', 'uint16', ... 'int32', 'uint32', 'int64', 'uint64'}; if (strcmp (T, 'cell')) v = C; elseif (strcmp (T, 'char')) v = char (ods_column_strings (C, VT)); elseif (strcmp (T, 'missing')) v = repmat (missing, size (C, 1), size (C, 2)); elseif (strcmp (T, 'logical')) v = logical (cell2mat (C)); elseif (ismember (T, numvartype)) M = __numcells__ (C); if (any (cellfun (@(x) isinteger (x), C(:)))) v = cellfun (@(x) cast (x, T), num2cell (M)); # element-wise, mixed types else v = cast (M, T); endif elseif (strncmp (T, 'datetime', 8)) ## A zone-aware datetime carries its TimeZone after 'datetime ' and its ## display format after a '|'. [T, dispfmt] = __typefmt__ (T); tz = ''; if (numel (T) > 9) tz = T(10:end); endif v = __iso2dt__ (C, tz); if (! isempty (dispfmt)) v.Format = dispfmt; endif elseif (strncmp (T, 'duration', 8)) [~, dispfmt] = __typefmt__ (T); v = __iso2dur__ (C); if (! isempty (dispfmt)) v.Format = dispfmt; endif elseif (strcmp (T, 'string')) ## A cell with no value type at all carries no value: the string is ## missing, which is not the same as an empty one. blank = cellfun (@isempty, VT); v = string (ods_column_strings (C, VT)); v(blank) = string (missing); elseif (strcmp (T, 'calendarDuration')) warning ("ods2table: 'calendarDuration' strings are not converted."); v = ods_column_strings (C, VT); elseif (strcmp (T, 'categorical')) warning ("ods2table: 'categorical' strings are not converted."); v = ods_column_strings (C, VT); else v = ods_column_strings (C, VT); endif endfunction ## Build a cellstr block from a data/value-type cell block: missing cells and ## numeric cells are coerced to text; genuine empty-string cells stay empty. function S = ods_column_strings (C, VT) S = cell (size (C)); for i = 1:numel (C) x = C{i}; if (ischar (x)) S{i} = x; elseif (isempty (x)) S{i} = ''; else S{i} = num2str (x); endif endfor endfunction ## Foreign-file fallback: no metadata sheet, so infer each column's type from ## its cell value types and name the variables Var1, Var2, ... function tbl = ods_autodetect (data, vtype, varNamesRow, rowNamesCol, ... readRowNames) ## A foreign sheet carries no metadata, so the caller's options say which row ## holds the names and which column the row names, a zero meaning neither. ## A leading column headed with the row-labels dimension name is what ## 'writetable' writes for the row names, so the header declares the column ## rather than the caller having to guess at it. if (readRowNames && ! rowNamesCol && varNamesRow > 0 && size (data, 1) >= varNamesRow && ischar (data{varNamesRow,1}) && strcmp (data{varNamesRow,1}, 'Row')) rowNamesCol = 1; endif RowNames = {}; if (rowNamesCol > 0 && rowNamesCol <= size (data, 2)) RowNames = ods_column_strings (data(:,rowNamesCol), vtype(:,rowNamesCol)); data(:,rowNamesCol) = []; vtype(:,rowNamesCol) = []; endif ncol = size (data, 2); if (varNamesRow > 0 && varNamesRow <= size (data, 1)) varNames = ods_column_strings (data(varNamesRow,:), vtype(varNamesRow,:)); empt = cellfun (@isempty, varNames); varNames(empt) = arrayfun (@(x) sprintf ("Var%d", x), find (empt), ... 'UniformOutput', false); varNames = matlab.lang.makeValidName (varNames); data(varNamesRow,:) = []; vtype(varNamesRow,:) = []; if (! isempty (RowNames)) RowNames(varNamesRow) = []; endif else varNames = arrayfun (@(x) sprintf ("Var%d", x), 1:ncol, ... 'UniformOutput', false); endif ## A table refuses a variable named after one of its dimensions, so a column ## of a foreign sheet headed 'Row' takes a suffix rather than being refused. varNames = __undimname__ (varNames, {'Row', 'Variables'}); varValues = cell (1, ncol); for c = 1:ncol switch (__colkind__ (data(:,c), vtype(:,c))) case 'float' varValues{c} = __numcells__ (data(:,c)); case 'boolean' varValues{c} = logical (__numcells__ (data(:,c))); case 'date' varValues{c} = __iso2dt__ (data(:,c)); case 'time' varValues{c} = __iso2dur__ (data(:,c)); otherwise varValues{c} = ods_column_strings (data(:,c), vtype(:,c)); endswitch endfor if (isempty (RowNames)) tbl = table (varValues{:}, 'VariableNames', varNames); else tbl = table (varValues{:}, 'VariableNames', varNames, ... 'RowNames', RowNames); endif endfunction %!demo %! ## `ods2table` reads an OpenDocument spreadsheet into a table. With no %! ## options it reads the first data sheet. %! %! T = table ([38; 43], [71; 69], 'VariableNames', {'Age', 'Height'}); %! filename = fullfile (tempdir (), 'patients.ods'); %! table2ods (T, filename); %! ods2table (filename) %!demo %! ## Point `'Sheet'` at a specific sheet, by name or by 1-based index, to read %! ## one page of a multi-sheet workbook. %! %! filename = fullfile (tempdir (), 'workbook.ods'); %! table2ods (table ([38; 43], 'VariableNames', {'Age'}), filename, 'Sheet', ... %! 'Patients'); %! table2ods (table ([1; 2; 3], 'VariableNames', {'Visit'}), filename, ... %! 'Sheet', 'Visits'); %! ods2table (filename, 'Sheet', 'Visits') %! %! delete (filename); ## Round-trip: numeric double and cellstr text %!test %! fn = [tempname() '.fods']; %! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'g'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.Properties.VariableNames, {'x', 'g'}); %! assert_equal (R.x, [1; 2; 3]); %! assert_equal (R.g, {'a'; 'b'; 'c'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: full-range int64/uint64 preserved without precision loss %!test %! fn = [tempname() '.fods']; %! T = table (int64 ([9223372036854775807; -5; 0]), ... %! uint64 ([18446744073709551615; 1; 0]), ... %! 'VariableNames', {'i', 'u'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (class (R.i), 'int64'); %! assert_equal (class (R.u), 'uint64'); %! assert_equal (R.i, int64 ([9223372036854775807; -5; 0])); %! assert_equal (R.u, uint64 ([18446744073709551615; 1; 0])); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: narrow integer types cast back from the declared metadata type %!test %! fn = [tempname() '.fods']; %! T = table (int8 ([1; -2]), uint16 ([3; 4]), single ([1.5; 2.5]), ... %! 'VariableNames', {'a', 'b', 'c'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (class (R.a), 'int8'); %! assert_equal (class (R.b), 'uint16'); %! assert_equal (class (R.c), 'single'); %! assert_equal (R.a, int8 ([1; -2])); %! assert_equal (R.c, single ([1.5; 2.5])); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: logical variable %!test %! fn = [tempname() '.fods']; %! T = table ([true; false; true], 'VariableNames', {'flag'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (class (R.flag), 'logical'); %! assert_equal (R.flag, [true; false; true]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## 'WriteVariableNames' false leaves no names in the file at all %!test %! fn = [tempname() '.fods']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! unwind_protect %! table2ods (T, fn, 'WriteVariableNames', false); %! R = ods2table (fn); %! assert_equal (R.Properties.VariableNames, {'Var1', 'Var2'}); %! assert_equal (R.Var1, [1; 2]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## 'WriteRowNames' false leaves the row labels out %!test %! fn = [tempname() '.fods']; %! T = table ([1; 2], 'VariableNames', {'n'}); %! T.Properties.RowNames = {'r1', 'r2'}; %! unwind_protect %! table2ods (T, fn, 'WriteRowNames', false); %! R = ods2table (fn); %! assert_equal (R.Properties.RowNames, {}); %! assert_equal (R.Properties.VariableNames, {'n'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## 'ReadVariableNames' false numbers the variables of a file that has names %!test %! fn = [tempname() '.fods']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn, 'ReadVariableNames', false); %! assert_equal (R.Properties.VariableNames, {'Var1', 'Var2'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## 'ReadRowNames' false drops the row labels a file carries %!test %! fn = [tempname() '.fods']; %! T = table ([1; 2], 'VariableNames', {'n'}); %! T.Properties.RowNames = {'r1', 'r2'}; %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn, 'ReadRowNames', false); %! assert_equal (R.Properties.RowNames, {}); %! assert_equal (R.n, [1; 2]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A sheet with no metadata takes its names from the first row %!test %! fn = [tempname() '.ods']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! unwind_protect %! writetable (T, fn); %! R = ods2table (fn); %! assert_equal (R.Properties.VariableNames, {'n', 's'}); %! assert_equal (height (R), 2); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A column named after a dimension is kept under a suffixed name %!test %! fn = [tempname() '.ods']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! T.Properties.RowNames = {'r1', 'r2'}; %! unwind_protect %! writetable (T, fn, 'WriteRowNames', true); %! R = ods2table (fn, 'ReadRowNames', false); %! assert_equal (R.Properties.VariableNames, {'Row_1', 'n', 's'}); %! assert_equal (R.Row_1, {'r1'; 'r2'}); %! assert_equal (R.Properties.RowNames, {}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A file written by 'writetable' round-trips, names and values %!test %! fn = [tempname() '.ods']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! unwind_protect %! writetable (T, fn); %! R = ods2table (fn); %! assert_equal (isequaln (R, T), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A 'writetable' file keeps its row names, the header naming the column %!test %! fn = [tempname() '.ods']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! T.Properties.RowNames = {'r1', 'r2'}; %! unwind_protect %! writetable (T, fn, 'WriteRowNames', true); %! R = ods2table (fn); %! assert_equal (isequaln (R, T), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## 'VariableNamesRow' names the row of a sheet that carries the names %!test %! fn = [tempname() '.ods']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! unwind_protect %! writetable (T, fn); %! R = ods2table (fn, 'VariableNamesRow', 2); %! assert_equal (R.Properties.VariableNames, {'x1', 'a'}); %! assert_equal (height (R), 2); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## 'VariableNamesRow' zero is the same as 'ReadVariableNames' false %!test %! fn = [tempname() '.ods']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! unwind_protect %! writetable (T, fn); %! R = ods2table (fn, 'VariableNamesRow', 0); %! assert_equal (R.Properties.VariableNames, {'Var1', 'Var2'}); %! assert_equal (height (R), 3); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A sheet with no metadata gives up a column as row names only when named %!test %! fn = [tempname() '.ods']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! unwind_protect %! writetable (T, fn); %! R = ods2table (fn, 'RowNamesColumn', 1); %! assert_equal (R.Properties.RowNames, {'1'; '2'}); %! assert_equal (R.Properties.VariableNames, {'s'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## The variable names sit on the data sheet, where a reader of the file sees ## them, and a nested table's take one row per level %!test %! fn = [tempname() '.fods']; %! inner = table ([3; 4], [5; 6], 'VariableNames', {'p', 'q'}); %! T = table ([1; 2], inner, 'VariableNames', {'a', 'v'}); %! unwind_protect %! table2ods (T, fn); %! txt = fileread (fn); %! ix = strfind (txt, 'table:name="__datatypes_meta__"'); %! visible = txt(1:ix-1); %! assert_equal (! isempty (strfind (visible, 'a')), true); %! assert_equal (! isempty (strfind (visible, 'p')), true); %! hidden = txt(ix:end); %! assert_equal (isempty (strfind (hidden, 'p')), true); %! assert_equal (isequaln (ods2table (fn), T), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: a nested table, values and metadata %!test %! fn = [tempname() '.fods']; %! inner = table ([3; 4], [5; 6], 'VariableNames', {'p', 'q'}); %! inner.Properties.VariableUnits = {'m', 's'}; %! T = table ([1; 2], inner, 'VariableNames', {'a', 'v'}); %! T.Properties.VariableUnits = {'kg', ''}; %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (isequaln (R, T), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: an inner description leaves the outer property unset %!test %! fn = [tempname() '.fods']; %! inner = table ([3; 4], [5; 6], 'VariableNames', {'p', 'q'}); %! inner.Properties.VariableDescriptions = {'inner p', 'inner q'}; %! T = table ([1; 2], inner, 'VariableNames', {'a', 'v'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.Properties.VariableDescriptions, {}); %! assert_equal (isequaln (R, T), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: an outer property set to blanks stays set beside a set inner one %!test %! fn = [tempname() '.fods']; %! inner = table ([3; 4], [5; 6], 'VariableNames', {'p', 'q'}); %! inner.Properties.VariableUnits = {'m', 's'}; %! T = table ([1; 2], inner, 'VariableNames', {'a', 'v'}); %! T.Properties.VariableUnits = {'', ''}; %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.Properties.VariableUnits, {'', ''}); %! assert_equal (R.v.Properties.VariableUnits, {'m', 's'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: a nested table in a zipped '.ods' package %!test %! fn = [tempname() '.ods']; %! inner = table ([3; 4], [5; 6], 'VariableNames', {'p', 'q'}); %! T = table ([1; 2], inner, 'VariableNames', {'a', 'v'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (isequaln (R, T), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: a struct variable becomes one column per field %!test %! fn = [tempname() '.fods']; %! T = table ([9; 8], struct ('f', {1; 2}, 'g', {3; 4}), ... %! 'VariableNames', {'a', 'v'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (isequaln (R, T), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: a nested table alongside a multicolumn variable and row names %!test %! fn = [tempname() '.fods']; %! inner = table ([3; 4], [5; 6], 'VariableNames', {'p', 'q'}); %! T = table ([1; 2], inner, [7, 8; 9, 10], 'VariableNames', {'a', 'v', 'm'}); %! T.Properties.RowNames = {'r1', 'r2'}; %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (isequaln (R, T), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: a nested table holding a datetime and a duration %!test %! fn = [tempname() '.fods']; %! inner = table (datetime (2024, 1, [1; 2]), hours ([1; 2]), ... %! 'VariableNames', {'d', 'u'}); %! T = table ([1; 2], inner, 'VariableNames', {'a', 'v'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (isequaln (R, T), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: a char matrix keeps its padding %!test %! fn = [tempname() '.fods']; %! T = table (['ab '; 'c '], 'VariableNames', {'v'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.v, ['ab '; 'c ']); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: a missing string stays missing, an empty one stays empty %!test %! fn = [tempname() '.fods']; %! T = table ([string('a'); string(missing); string('')], 'VariableNames', ... %! {'v'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (ismissing (R.v), [false; true; false]); %! assert_equal (char (R.v(3)), ''); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: a double is restored bit for bit %!test %! fn = [tempname() '.fods']; %! x = [pi; 0.1; realmin; eps; 1e308; -1/3]; %! T = table (x, 'VariableNames', {'v'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.v, x); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: a datetime keeps its display format %!test %! fn = [tempname() '.fods']; %! d = datetime ({'2020-01-02 03:04:05'; '2021-03-04 05:06:07'}); %! d.Format = 'dd/MM/yyyy'; %! T = table (d, 'VariableNames', {'v'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (isequaln (R.v, d), true); %! assert_equal (R.v.Format, 'dd/MM/yyyy'); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: a duration keeps its display format %!test %! fn = [tempname() '.fods']; %! u = hours ([1; 2]); %! T = table (u, 'VariableNames', {'v'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (isequaln (R.v, u), true); %! assert_equal (R.v.Format, 'h'); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: datetime with a NaT, restored to native datetime %!test %! fn = [tempname() '.fods']; %! d = [datetime(2024, 1, 15, 10, 30, 15.5); NaT; datetime(1999, 12, 31, 23, ... %! 59, 59)]; %! T = table (d, 'VariableNames', {'when'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (class (R.when), 'datetime'); %! assert_equal (isequaln (datevec (R.when), datevec (d)), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: duration with negative, sub-second, over-24h, and zero values %!test %! fn = [tempname() '.fods']; %! du = [duration(25, 30, 15.5); duration(-1, -30, 0); duration(0, 0, 0)]; %! T = table (du, 'VariableNames', {'elapsed'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (class (R.elapsed), 'duration'); %! assert_equal (isequaln (seconds (R.elapsed), seconds (du)), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: missing duration (NaN) restored as a missing duration %!test %! fn = [tempname() '.fods']; %! du = [duration(1, 0, 0); duration(NaN, 0, 0)]; %! T = table (du, 'VariableNames', {'d'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (isnan (seconds (R.d)), [false; true]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: string variable %!test %! fn = [tempname() '.fods']; %! T = table (string ({'x'; 'y'; 'z'}), 'VariableNames', {'s'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (class (R.s), 'string'); %! assert_equal (R.s, string ({'x'; 'y'; 'z'})); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: missing numeric value restored as NaN %!test %! fn = [tempname() '.fods']; %! T = table ([1; NaN; 3], 'VariableNames', {'v'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.v, [1; NaN; 3]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: row names %!test %! fn = [tempname() '.fods']; %! T = table ([1; 2; 3], 'VariableNames', {'v'}, 'RowNames', {'r1', 'r2', ... %! 'r3'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.Properties.RowNames, {'r1'; 'r2'; 'r3'}); %! assert_equal (R.v, [1; 2; 3]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: multicolumn variable kept as one matrix-valued variable %!test %! fn = [tempname() '.fods']; %! T = table ([1 2; 3 4; 5 6], {'p'; 'q'; 'r'}, 'VariableNames', {'mat', ... %! 'tag'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (size (R.mat), [3, 2]); %! assert_equal (R.mat, [1 2; 3 4; 5 6]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: descriptions and units restored when every variable carries one %!test %! fn = [tempname() '.fods']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'g'}); %! T.Properties.VariableDescriptions = {'count', 'group'}; %! T.Properties.VariableUnits = {'kg', '-'}; %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.Properties.VariableDescriptions, {'count', 'group'}); %! assert_equal (R.Properties.VariableUnits, {'kg', '-'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: descriptions/units written when only some variables carry one %!test %! fn = [tempname() '.fods']; %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! T.Properties.VariableDescriptions = {'first', ''}; %! T.Properties.VariableUnits = {'', 'kg'}; %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.Properties.VariableDescriptions, {'first', ''}); %! assert_equal (R.Properties.VariableUnits, {'', 'kg'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: a zone-aware datetime keeps its TimeZone %!test %! fn = [tempname() '.ods']; %! dt = datetime (2024, 6, [15; 16], 10, 30, 0, 'TimeZone', 'America/New_York'); %! T = table (dt, 'VariableNames', {'t'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.t.TimeZone, 'America/New_York'); %! assert_equal (cellstr (char (R.t)), cellstr (char (dt))); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: a fixed-offset zone keeps its TimeZone %!test %! fn = [tempname() '.ods']; %! dt = datetime (2024, 6, [15; 16], 10, 30, 0, 'TimeZone', '+05:30'); %! T = table (dt, 'VariableNames', {'t'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.t.TimeZone, '+05:30'); %! assert_equal (cellstr (char (R.t)), cellstr (char (dt))); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A sheet 'writetable' adds to a 'table2ods' file has no metadata of its own, ## and the first sheet keeps its own %!test %! fn = [tempname() '.ods']; %! T = table (string ({'a'; 'b'; 'a'}), [1; 2; 3], ... %! 'VariableNames', {'g', 'x'}); %! TR = table ([38; 43], [71; 69], 'VariableNames', {'Age', 'Height'}, ... %! 'RowNames', {'Li', 'Diaz'}); %! unwind_protect %! table2ods (T, fn); %! writetable (TR, fn, 'Sheet', 'Cohort', 'WriteRowNames', true); %! R = readtable (fn, 'Sheet', 'Cohort', 'ReadRowNames', true); %! assert_equal (R.Properties.RowNames, {'Li'; 'Diaz'}); %! assert_equal (R.Height, [71; 69]); %! assert_equal (class (ods2table (fn).g), 'string'); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## 'writetable' replacing a 'table2ods' sheet drops its metadata %!test %! fn = [tempname() '.ods']; %! T = table ([38; 43; 40], [71; 69; 64], 'VariableNames', {'Age', 'Height'}); %! T1 = table ([38; 43], 'VariableNames', {'Age'}, 'RowNames', {'Li', 'Diaz'}); %! unwind_protect %! table2ods (T, fn); %! writetable (T1, fn, 'WriteRowNames', true); %! R = readtable (fn, 'ReadRowNames', true); %! assert_equal (R.Age, [38; 43]); %! assert_equal (R.Properties.RowNames, {'Li'; 'Diaz'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## 'writetable' replacing one sheet of a sectioned workbook leaves the other ## sheet's metadata alone %!test %! fn = [tempname() '.ods']; %! TA = table ([1; 2], 'VariableNames', {'x'}); %! TB = table (string ({'p'; 'q'}), 'VariableNames', {'g'}); %! unwind_protect %! table2ods (TA, fn, 'Sheet', 'A'); %! table2ods (TB, fn, 'Sheet', 'B'); %! writetable (table ([5; 6], 'VariableNames', {'y'}), fn, 'Sheet', 'A'); %! assert_equal (ods2table (fn, 'Sheet', 'A').y, [5; 6]); %! assert_equal (class (ods2table (fn, 'Sheet', 'B').g), 'string'); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Metadata that no longer fits its sheet raises instead of being misapplied %!error %! fn = [tempname() '.fods']; %! T = table ([38; 43], {'Li'; 'Diaz'}, 'VariableNames', {'Age', 'Who'}); %! table2ods (T, fn); %! unwind_protect %! txt = strrep (fileread (fn), 'cell', ... %! 'double'); %! fid = fopen (fn, 'w'); %! fputs (fid, txt); %! fclose (fid); %! ods2table (fn); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!error %! fn = [tempname() '.fods']; %! T = table ([38; 43], {'Li'; 'Diaz'}, 'VariableNames', {'Age', 'Who'}, ... %! 'RowNames', {'r1', 'r2'}); %! table2ods (T, fn); %! unwind_protect %! txt = strrep (fileread (fn), 'cell', ... %! 'double'); %! fid = fopen (fn, 'w'); %! fputs (fid, txt); %! fclose (fid); %! ods2table (fn); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## An unsectioned metadata grid describes only the first sheet, even in a file ## another program has added a sheet to %!test %! fn = [tempname() '.fods']; %! table2ods (table (string ({'a'; 'b'}), 'VariableNames', {'g'}), fn); %! unwind_protect %! txt = fileread (fn); %! s = regexp (txt, '', ... %! 'match', 'once'); %! txt = strrep (txt, s, [s, strrep(s, 'table:name="Sheet1"', ... %! 'table:name="Other"')]); %! fid = fopen (fn, 'w'); %! fputs (fid, txt); %! fclose (fid); %! assert_equal (class (ods2table (fn).g), 'string'); %! assert_equal (class (ods2table (fn, 'Sheet', 'Other').g), 'cell'); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A sheet with no metadata sheet: types inferred, names from the header row %!test %! fn = [tempname() '.fods']; %! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'g'}); %! unwind_protect %! table2ods (T, fn); %! txt = fileread (fn); %! txt = regexprep (txt, ... %! '', ''); %! fid = fopen (fn, 'w'); fputs (fid, txt); fclose (fid); %! R = ods2table (fn); %! assert_equal (R.Properties.VariableNames, {'x', 'g'}); %! assert_equal (R.x, [1; 2; 3]); %! assert_equal (class (R.g), 'cell'); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Select a specific sheet by name and by 1-based index from a multi-sheet file %!test %! fn = [tempname() '.fods']; %! doc = ['' ... %! '' ... %! '' ... %! '' ... %! '11' ... %! '' ... %! '' ... %! '' ... %! '22' ... %! '' ... %! '']; %! fid = fopen (fn, 'w'); fputs (fid, doc); fclose (fid); %! unwind_protect %! Rdef = ods2table (fn, 'ReadVariableNames', false); %! assert_equal (Rdef.Var1, 11); # the default reads the first sheet %! Rname = ods2table (fn, 'Sheet', 'Two', 'ReadVariableNames', false); %! assert_equal (Rname.Var1, 22); %! Ridx = ods2table (fn, 'Sheet', 2, 'ReadVariableNames', false); %! assert_equal (Ridx.Var1, 22); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Error: a requested sheet that does not exist %!error ... %! fn = [tempname() '.fods']; ... %! doc = ['' ... %! '' ... %! '']; ... %! fid = fopen (fn, 'w'); fputs (fid, doc); fclose (fid); ... %! ods2table (fn, 'Sheet', 'X'); ## Error: 'Sheet' of an invalid type %!error ... %! ods2table ([tempname() '.fods'], 'Sheet', {1, 2}); ## Error: FILENAME of the wrong type %!error ... %! ods2table (42); ## Error: a file that cannot be read as an OpenDocument spreadsheet %!error ... %! ods2table ([tempname() '.fods']); ## Round-trip: a table with no rows preserves its variable types %!test %! fn = [tempname() '.fods']; %! T = table (zeros (0, 1), int64 (zeros (0, 1)), datetime (zeros (0, 1), 1, ... %! 1), ... %! seconds (zeros (0, 1)), string (cell (0, 1)), ... %! 'VariableNames', {'d', 'i', 'dt', 'du', 's'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (size (R), [0, 5]); %! assert_equal (class (R.i), 'int64'); %! assert_equal (class (R.dt), 'datetime'); %! assert_equal (class (R.du), 'duration'); %! assert_equal (class (R.s), 'string'); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: a table with no variables comes back as an empty table %!test %! fn = [tempname() '.fods']; %! unwind_protect %! table2ods (table (), fn); %! R = ods2table (fn); %! assert_equal (istable (R), true); %! assert_equal (size (R), [0, 0]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: XML metacharacters in a string are escaped and restored %!test %! fn = [tempname() '.fods']; %! T = table ({'a&"c"'; 'x&y'}, 'VariableNames', {'s'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.s, {'a&"c"'; 'x&y'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: leading and trailing whitespace in a string is preserved %!test %! fn = [tempname() '.fods']; %! T = table ({' pad '; ''}, 'VariableNames', {'s'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.s, {' pad '; ''}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip: infinite numeric values are preserved %!test %! fn = [tempname() '.fods']; %! T = table ([Inf; -Inf; 1.5], 'VariableNames', {'v'}); %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.v, [Inf; -Inf; 1.5]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## An infinity is written as text, since Calc loads a non-finite number as 0 %!test %! fn = [tempname() '.fods']; %! T = table ([Inf; -Inf; 1.5], 'VariableNames', {'v'}); %! unwind_protect %! table2ods (T, fn); %! xml = fileread (fn); %! assert_equal (isempty (strfind (xml, 'office:value="inf"')), true); %! assert_equal (isempty (strfind (xml, '-Inf')), false); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A foreign sheet's numeric column reads text Inf and an error cell %!test %! fn = [tempname() '.fods']; %! f = @(v) ['', v, '']; %! s = @(v) ['', v, ... %! '']; %! e = ['#N/A']; %! r = @(varargin) ['', varargin{:}, '']; %! xml = ['', ... %! '', ... %! r(s('x'), s('y')), r(f('1'), s('-Inf')), r(e, f('2')), ... %! r(s('Inf'), f('3')), r(s('abc'), f('4')), ... %! '', ... %! '']; %! unwind_protect %! fid = fopen (fn, 'w'); %! fputs (fid, xml); %! fclose (fid); %! R = ods2table (fn); %! assert_equal (isequaln (R.x, [1; NaN; Inf; NaN]), true); %! assert_equal (R.y, [-Inf; 2; 3; 4]); %! R = readtable (fn); %! assert_equal (isequaln (R.x, [1; NaN; Inf; NaN]), true); %! assert_equal (R.y, [-Inf; 2; 3; 4]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip through the compressed '.ods' (ZIP) container %!test %! fn = [tempname() '.ods']; %! d = [datetime(2024, 1, 15, 10, 30, 15.5); NaT]; %! T = table ([1; 2], int64 ([9223372036854775807; -5]), d, ... %! seconds ([90; 3661]), {'a&"c"'; 'y'}, ... %! 'VariableNames', {'x', 'big', 'when', 'dur', 's'}); %! T.Properties.RowNames = {'r1', 'r2'}; %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.Properties.RowNames, {'r1'; 'r2'}); %! assert_equal (R.x, [1; 2]); %! assert_equal (R.big, int64 ([9223372036854775807; -5])); %! assert_equal (class (R.when), 'datetime'); %! assert_equal (isequaln (datevec (R.when), datevec (d)), true); %! assert_equal (isequaln (seconds (R.dur), [90; 3661]), true); %! assert_equal (R.s, {'a&"c"'; 'y'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!error ... %! ods2table ('x.ods', 'Nope', 1) %!error ... %! ods2table ('x.ods', 'Nope') ## a categorical or calendarDuration column is written as text and warns on ## the way back, the strings not being converted to the original type %!warning ... %! T = table (categorical ({'a'; 'b'}), 'VariableNames', {'c'}); %! fn = [tempname() '.ods']; %! unwind_protect %! table2ods (T, fn); %! ods2table (fn); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!warning ... %! T = table (calendarDuration ([1; 2], [0; 0], [0; 0]), 'VariableNames', ... %! {'c'}); %! fn = [tempname() '.ods']; %! unwind_protect %! table2ods (T, fn); %! ods2table (fn); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect pr0m1th3as-datatypes-9450b29/inst/ods2timetable.m000066400000000000000000000621041527370255000215720ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{tt} =} ods2timetable (@var{filename}) ## @deftypefnx {datatypes} {@var{tt} =} ods2timetable (@var{filename}, @var{Name}, @var{Value}) ## ## Read an OpenDocument spreadsheet file into a timetable. ## ## @code{@var{tt} = ods2timetable (@var{filename})} reads a sheet of the ## OpenDocument spreadsheet named by @var{filename} into a @code{timetable}. ## @var{filename} may be a character vector, a cellstr, or a string scalar. ## ## A sheet written by @code{timetable2ods} carries the package's own metadata, ## which names and types every variable and tags the leading column of row ## times with their type, their @code{TimeZone} where they have one, and their ## @code{Format}. Such a sheet comes back as the timetable it was written ## from: the row times keep their type, zone and format exactly, and the row ## dimension keeps its name. ## ## Any other sheet is read as @code{ods2table} reads it, and its @strong{first} ## @code{datetime} or @code{duration} variable becomes the row times, the row ## dimension taking that variable's name. A sheet with no such variable cannot ## be read as a timetable. ## ## Every @var{Name}-@var{Value} option of @code{ods2table} is accepted and ## behaves as it does there, with two exceptions. @qcode{'ReadRowNames'} and ## @qcode{'RowNamesColumn'} are refused: a timetable labels its rows by time ## and by nothing else, so a sheet whose rows are named is read with ## @code{ods2table}. ## ## @code{TimeStep} and @code{SampleRate} are not stored in the file and are ## worked out again from the row times, so a regular timetable comes back ## regular. ## ## An event table is held on a sheet of its own, named by a ## @qcode{## Events crossref:} line in the hidden metadata sheet, and is ## attached to the timetable that names it. With no @qcode{'Sheet'} given the ## first data sheet is read @strong{less the sheets that hold somebody's ## events}, so a file holding one timetable and its events reads with no ## argument at all. A sheet asked for by name or by index is read whichever it ## is, the index running over every data sheet so that one number means one ## sheet here and in @code{ods2table} alike; an event sheet asked for by name ## comes back as the @code{eventtable} it is. ## ## A reference is refused when it names a sheet the file does not have, when ## the sheet it names holds no row times, when those row times are of a ## different type than the referring timetable's, or when the sheet it names ## carries a reference of its own, an event table not being something that can ## carry an event table. All four are reachable only in a file edited by hand. ## ## @seealso{timetable.timetable2ods, ods2table, ods2struct, csv2timetable, ## timetable} ## @end deftypefn function tt = ods2timetable (filename, varargin) if (nargin < 1) error ("ods2timetable: too few input arguments."); endif if (! ((ischar (filename) && isvector (filename)) || iscellstr (filename) ... || isa (filename, 'string'))) error (strcat ("ods2timetable: FILENAME must be a character vector,", ... " cellstr, or string.")); endif if (mod (numel (varargin), 2) != 0) error ("ods2timetable: name-value arguments must be in pairs."); endif ## A timetable has no row names, so an option asking for them would be ## honoured by the reader and then discarded by the conversion, which is ## worse than saying so. for i = 1:2:numel (varargin) opt = varargin{i}; if (isa (opt, 'string') && isscalar (opt)) opt = char (opt); endif if (ischar (opt) && any (strcmpi (opt, {'ReadRowNames', 'RowNamesColumn'}))) error (strcat ("ods2timetable: '%s' is not supported; a timetable", ... " labels its rows by time. Read a sheet whose rows", ... " are named with 'ods2table'."), opt); endif endfor file = char (cellstr (filename)); [data, ~, ~, names, preamble] = __ods2table__ (file); if (ischar (data)) error ("ods2timetable: %s", data); endif [xrefs, errmsg] = __odscrossrefs__ (preamble, names); if (! isempty (errmsg)) error ("ods2timetable: %s", errmsg); endif ## With no sheet asked for, the first data sheet is read, less the sheets ## that hold somebody's events: a file holding one timetable and its events ## therefore reads with no argument at all. A sheet asked for by name or by ## index is read whichever it is, the index running over every data sheet so ## that one number means one sheet here and in 'ods2table' alike. args = varargin; if (! any (strcmpi (args(1:2:end), 'Sheet'))) pick = names(! ismember (names, {xrefs.to})); args = [{'Sheet', pick{1}}, args]; endif ## The sheet is read by 'ods2table', whose complaints are about the call ## made here and so are raised under this function's name. try [tbl, rowTimesName] = ods2table (file, args{:}); catch err if (strncmp (err.message, "ods2table: ", 11)) error ("ods2timetable: %s", err.message(12:end)); endif rethrow (err); end_try_catch if (isempty (rowTimesName)) types = tbl.Properties.VariableTypes; if (! any (ismember (types, {'datetime', 'duration'}))) error (strcat ("ods2timetable: the sheet has no datetime or duration", ... " column to use as row times.")); endif tt = table2timetable (tbl); return; endif tt = table2timetable (tbl, 'RowTimes', rowTimesName); ## A sheet asked for by name may be an event table, which is what it comes ## back as; otherwise its own events are attached where it has any. sheet = args{find (strcmpi (args(1:2:end), 'Sheet'), 1) * 2}; if (isnumeric (sheet) && isscalar (sheet) && sheet >= 1 ... && sheet <= numel (names)) sheet = names{sheet}; endif jx = []; if (ischar (sheet)) jx = find (strcmp (sheet, {xrefs.to}), 1); endif if (! isempty (jx)) [evargs, errmsg] = __odseventopts__ (xrefs(jx), ... tt.Properties.VariableNames); if (! isempty (errmsg)) error ("ods2timetable: %s", errmsg); endif tt = eventtable (tt, evargs{:}); return; endif ix = find (strcmp (sheet, {xrefs.from}), 1); if (! isempty (ix)) tt = __odsattach__ (tt, file, xrefs(ix), names, 'ods2timetable'); endif endfunction %!demo %! ## A timetable written by `timetable2ods` comes back whole: the row times %! ## keep their type, their time zone and their display format, the row %! ## dimension keeps its name, and the spreadsheet shows the times as times %! ## because they are written as native date cells. %! %! t = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours ((0:3)'); %! TT = timetable (t, (1:4)', 'VariableNames', {'reading'}); %! TT.Properties.DimensionNames{1} = 'Stamp'; %! fname = [tempname(), '.ods']; %! timetable2ods (TT, fname); %! ods2timetable (fname) %! delete (fname); ## Test a zoned datetime round-trips whole %!test %! t = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours ((0:3)'); %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! fname = [tempname(), '.ods']; %! unwind_protect %! timetable2ods (TT, fname); %! out = ods2timetable (fname); %! assert_equal (class (out), 'timetable'); %! assert_equal (out.Properties.RowTimes.TimeZone, 'America/New_York'); %! assert_equal (out.Properties.RowTimes, TT.Properties.RowTimes); %! assert_equal (out.v, TT.v); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test duration row times round-trip whole, format included %!test %! d = hours ([0.5; 1.5; 2.5]); %! d.Format = 'hh:mm:ss.SSS'; %! TT = timetable (d, (1:3)', 'VariableNames', {'v'}); %! fname = [tempname(), '.ods']; %! unwind_protect %! timetable2ods (TT, fname); %! out = ods2timetable (fname); %! assert_equal (class (out.Properties.RowTimes), 'duration'); %! assert_equal (out.Properties.RowTimes, TT.Properties.RowTimes); %! assert_equal (out.Properties.RowTimes.Format, 'hh:mm:ss.SSS'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the row dimension name survives %!test %! t = datetime (2024, 1, (1:3)'); %! TT = timetable (t, (1:3)', 'VariableNames', {'v'}); %! TT.Properties.DimensionNames{1} = 'Stamp'; %! fname = [tempname(), '.ods']; %! unwind_protect %! timetable2ods (TT, fname); %! assert_equal (ods2timetable (fname).Properties.DimensionNames, ... %! {'Stamp', 'Variables'}); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the row times are written as native date cells %!test %! t = datetime (2024, 1, (1:2)'); %! TT = timetable (t, (1:2)', 'VariableNames', {'v'}); %! fname = [tempname(), '.fods']; %! unwind_protect %! timetable2ods (TT, fname); %! txt = fileread (fname); %! hit = strfind (txt, 'office:date-value="2024-01-01'); %! assert_equal (! isempty (hit), true); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the duration row times are written as native time cells %!test %! TT = timetable (hours ([1; 2]), (1:2)', 'VariableNames', {'v'}); %! fname = [tempname(), '.fods']; %! unwind_protect %! timetable2ods (TT, fname); %! txt = fileread (fname); %! assert_equal (! isempty (strfind (txt, 'office:time-value=')), true); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the variable descriptions and units survive %!test %! t = datetime (2024, 1, (1:3)'); %! TT = timetable (t, (1:3)', {'a'; 'b'; 'c'}, 'VariableNames', {'v', 'w'}); %! TT.Properties.VariableDescriptions = {'a count', ''}; %! TT.Properties.VariableUnits = {'kg', ''}; %! fname = [tempname(), '.ods']; %! unwind_protect %! timetable2ods (TT, fname); %! out = ods2timetable (fname); %! assert_equal (out.Properties.VariableDescriptions, {'a count', ''}); %! assert_equal (out.Properties.VariableUnits, {'kg', ''}); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test a regular timetable comes back regular %!test %! TT = timetable ((1:4)', 'TimeStep', hours (1), 'VariableNames', {'v'}); %! fname = [tempname(), '.ods']; %! unwind_protect %! timetable2ods (TT, fname); %! out = ods2timetable (fname); %! assert_equal (isregular (out), true); %! assert_equal (out.Properties.TimeStep, hours (1)); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test a workbook of a table and a timetable round-trips as both %!test %! t = datetime (2024, 1, (1:3)'); %! wb.Plain = table ((1:2)', 'VariableNames', {'a'}); %! wb.Timed = timetable (t, (1:3)', 'VariableNames', {'v'}); %! fname = [tempname(), '.ods']; %! unwind_protect %! struct2ods (fname, wb); %! s = ods2struct (fname); %! assert_equal (class (s.Plain), 'table'); %! assert_equal (class (s.Timed), 'timetable'); %! assert_equal (s.Timed.Properties.RowTimes, t); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test a table whose first variable is a datetime stays a table %!test %! T = table (datetime (2024, 1, (1:3)'), (1:3)', ... %! 'VariableNames', {'when', 'v'}); %! fname = [tempname(), '.ods']; %! unwind_protect %! table2ods (T, fname); %! assert_equal (class (ods2struct (fname).Sheet1), 'table'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test a second sheet is added to an existing workbook %!test %! t = datetime (2024, 1, (1:3)'); %! TT = timetable (t, (1:3)', 'VariableNames', {'v'}); %! fname = [tempname(), '.ods']; %! unwind_protect %! table2ods (table ((1:2)', 'VariableNames', {'a'}), fname, 'Sheet', 'One'); %! timetable2ods (TT, fname, 'Sheet', 'Two'); %! s = ods2struct (fname); %! assert_equal (fieldnames (s), {'One'; 'Two'}); %! assert_equal (class (s.Two), 'timetable'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test 'ods2table' reads the row times as a leading variable %!test %! t = datetime (2024, 1, (1:3)'); %! TT = timetable (t, (1:3)', 'VariableNames', {'v'}); %! TT.Properties.DimensionNames{1} = 'Stamp'; %! fname = [tempname(), '.ods']; %! unwind_protect %! timetable2ods (TT, fname); %! [T, rtn] = ods2table (fname); %! assert_equal (class (T), 'table'); %! assert_equal (T.Properties.VariableNames, {'Stamp', 'v'}); %! assert_equal (rtn, 'Stamp'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test a sheet holding no row times reports none %!test %! fname = [tempname(), '.ods']; %! unwind_protect %! table2ods (table ((1:2)', 'VariableNames', {'a'}), fname); %! [~, rtn] = ods2table (fname); %! assert_equal (rtn, ''); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test too few input arguments %!error ods2timetable () ## Test an invalid FILENAME %!error ... %! ods2timetable (42) ## Test 'ReadRowNames' is refused %!error ... %! ods2timetable ('none.ods', 'ReadRowNames', true) ## Test 'RowNamesColumn' is refused %!error ... %! ods2timetable ('none.ods', 'RowNamesColumn', 1) ## Test an unknown option, and one without a value %!error ... %! fn = [tempname() '.ods']; %! timetable2ods (timetable (hours (1), 1), fn); %! unwind_protect %! ods2timetable (fn, 'Bogus', 1); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!error ... %! ods2timetable ('none.ods', 'Bogus') ## Test an attached event table round-trips whole %!test %! t = datetime (2024, 1, 1) + hours ((0:5)'); %! TT = timetable (t, (1:6)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t([2 4]), 'EventLabels', ... %! {'on'; 'off'}, ... %! 'EventLengths', hours ([2; 1])); %! fname = [tempname(), '.ods']; %! unwind_protect %! timetable2ods (TT, fname); %! out = ods2timetable (fname); %! assert_equal (class (out.Properties.Events), 'eventtable'); %! assert_equal (isequal (out.Properties.Events, TT.Properties.Events), true); %! assert_equal (isequal (out, TT), true); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test 'writetable' replacing an event sheet detaches it from its timetable %!test %! t = datetime (2024, 1, 1) + hours ((0:3)'); %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t(2), 'EventLabels', {'on'}); %! fname = [tempname(), '.ods']; %! unwind_protect %! timetable2ods (TT, fname, 'Sheet', 'Data'); %! writetable (table ([7; 8], 'VariableNames', {'w'}), fname, ... %! 'Sheet', 'Data_Events'); %! out = ods2timetable (fname, 'Sheet', 'Data'); %! assert_equal (isempty (out.Properties.Events), true); %! assert_equal (out.v, (1:4)'); %! assert_equal (readtable (fname, 'Sheet', 'Data_Events').w, [7; 8]); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the three event designations survive %!test %! t = datetime (2024, 1, 1) + hours ((0:3)'); %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t([2 3]), 'EventLabels', ... %! {'on'; 'off'}, 'EventEnds', t([3 4])); %! fname = [tempname(), '.ods']; %! unwind_protect %! timetable2ods (TT, fname); %! P = ods2timetable (fname).Properties.Events.Properties; %! assert_equal (P.EventLabelsVariable, 'EventLabels'); %! assert_equal (P.EventEndsVariable, 'EventEnds'); %! assert_equal (P.EventLengthsVariable, []); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the event sheet is consumed and gets no field of its own %!test %! t = datetime (2024, 1, 1) + hours ((0:3)'); %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t(2), 'EventLabels', {'on'}); %! fname = [tempname(), '.ods']; %! unwind_protect %! timetable2ods (TT, fname, 'Sheet', 'Data'); %! s = ods2struct (fname); %! assert_equal (fieldnames (s), {'Data'}); %! assert_equal (class (s.Data.Properties.Events), 'eventtable'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test an event sheet asked for by name comes back as an event table %!test %! t = datetime (2024, 1, 1) + hours ((0:3)'); %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t(2), 'EventLabels', {'on'}); %! fname = [tempname(), '.ods']; %! unwind_protect %! timetable2ods (TT, fname, 'Sheet', 'Data'); %! ev = ods2timetable (fname, 'Sheet', 'Data_Events'); %! assert_equal (class (ev), 'eventtable'); %! assert_equal (ev.Properties.EventLabelsVariable, 'EventLabels'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test a numeric 'Sheet' indexes every data sheet, event sheets included %!test %! t = datetime (2024, 1, 1) + hours ((0:3)'); %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t(2), 'EventLabels', {'on'}); %! fname = [tempname(), '.ods']; %! unwind_protect %! timetable2ods (TT, fname, 'Sheet', 'Data'); %! assert_equal (class (ods2timetable (fname, 'Sheet', 2)), 'eventtable'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test a workbook of two timetables keeps each one's events apart %!test %! t = datetime (2024, 1, 1) + hours ((0:3)'); %! A = timetable (t, (1:4)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (t(2), 'EventLabels', {'up'}); %! B = timetable (t, (5:8)', 'VariableNames', {'w'}); %! B.Properties.Events = eventtable (t(3), 'EventLabels', {'down'}); %! fname = [tempname(), '.ods']; %! unwind_protect %! struct2ods (fname, struct ('A', A, 'B', B)); %! s = ods2struct (fname); %! assert_equal (fieldnames (s), {'A'; 'B'}); %! assert_equal (cellstr (s.A.Properties.Events.EventLabels), {'up'}); %! assert_equal (cellstr (s.B.Properties.Events.EventLabels), {'down'}); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test one event sheet referenced by two timetables is read as written %!test %! t = datetime (2024, 1, 1) + hours ((0:3)'); %! A = timetable (t, (1:4)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (t(2), 'EventLabels', {'up'}); %! B = timetable (t, (5:8)', 'VariableNames', {'w'}); %! B.Properties.Events = eventtable (t(3), 'EventLabels', {'down'}); %! fname = [tempname(), '.fods']; %! unwind_protect %! struct2ods (fname, struct ('A', A, 'B', B)); %! txt = fileread (fname); %! txt = strrep (txt, 'B_Events', ... %! 'A_Events'); %! fid = fopen (fname, 'w'); fputs (fid, txt); fclose (fid); %! s = ods2struct (fname); %! assert_equal (cellstr (s.A.Properties.Events.EventLabels), {'up'}); %! assert_equal (cellstr (s.B.Properties.Events.EventLabels), {'up'}); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Four files a hand edit has made inconsistent: one whose cross reference ## names a sheet that is not there, one whose event table is said to carry an ## event table of its own, one whose event table keeps row times of another ## type than the sheet it annotates, and one whose event table lacks the ## labels variable the cross reference names. %!shared fmiss, fdeep, ftype, fvar %! t = datetime (2024, 1, 1) + hours ((0:3)'); %! A = timetable (t, (1:4)', 'VariableNames', {'v'}); %! A.Properties.Events = eventtable (t(2), 'EventLabels', {'up'}); %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t(2), 'EventLabels', {'on'}); %! fmiss = [tempname(), '.fods']; %! timetable2ods (TT, fmiss, 'Sheet', 'Data'); %! txt = fileread (fmiss); %! txt = strrep (txt, 'Data_Events', ... %! 'Nope'); %! fid = fopen (fmiss, 'w'); fputs (fid, txt); fclose (fid); %! B = timetable (t, (5:8)', 'VariableNames', {'w'}); %! B.Properties.Events = eventtable (t(3), 'EventLabels', {'down'}); %! fdeep = [tempname(), '.fods']; %! struct2ods (fdeep, struct ('A', A, 'B', B)); %! txt = fileread (fdeep); %! txt = strrep (txt, 'B_Events', 'A'); %! fid = fopen (fdeep, 'w'); fputs (fid, txt); fclose (fid); %! C = timetable (hours ((0:3)'), (5:8)', 'VariableNames', {'w'}); %! ftype = [tempname(), '.fods']; %! struct2ods (ftype, struct ('A', A, 'B', C)); %! txt = fileread (ftype); %! txt = strrep (txt, 'A_Events', 'B'); %! fid = fopen (ftype, 'w'); fputs (fid, txt); fclose (fid); %! fvar = [tempname(), '.fods']; %! struct2ods (fvar, struct ('A', A, 'B', timetable (t, (5:8)'))); %! txt = fileread (fvar); %! txt = strrep (txt, 'A_Events', 'B'); %! fid = fopen (fvar, 'w'); fputs (fid, txt); fclose (fid); ## Test a reference to a sheet the file does not have is refused %!error ... %! ods2timetable (fmiss) ## Test an event table said to carry an event table is refused %!error ... %! ods2struct (fdeep) %!error ... %! ods2timetable (fdeep) ## Test an event table whose row times are of another type is refused %!error ... %! ods2struct (ftype) %!error ... %! ods2timetable (ftype) ## Test an event table lacking a variable its cross reference names is refused %!error ... %! ods2timetable (ftype, 'Sheet', 'B') %!error ... %! ods2timetable (fvar) %!error ... %! ods2struct (fvar) ## Test the fixtures are removed again %!test %! delete (fmiss); %! delete (fdeep); %! delete (ftype); %! delete (fvar); %! assert_equal (exist (fmiss, 'file'), 0); %! assert_equal (exist (fdeep, 'file'), 0); %! assert_equal (exist (ftype, 'file'), 0); %! assert_equal (exist (fvar, 'file'), 0); ## Two files nothing can be read out of: one whose columns are all numeric, ## so nothing in it can label the rows, and one in which a second cross ## reference points back at the data sheet from its event table. %!shared fnum, fev %! fnum = [tempname(), '.fods']; %! table2ods (table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}), fnum); %! fev = [tempname(), '.fods']; %! t = datetime (2024, 1, 1) + hours ((0:3)'); %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! ET = eventtable (timetable (t([2, 3]), {'a'; 'b'}, ... %! 'VariableNames', {'L'})); %! ET.EventLabelsVariable = 'L'; %! TT.Properties.Events = ET; %! timetable2ods (TT, fev); %! txt = fileread (fev); %! tc = ''; %! row = ['', tc, ... %! '## Events crossref:', ... %! tc, 'Sheet1_Events', ... %! tc, 'Sheet1', ... %! '']; %! ix = strfind (txt, '## Events crossref:')(1); %! ie = ix + strfind (txt(ix:end), '')(1) + 17; %! fid = fopen (fev, 'w'); %! fputs (fid, [txt(1:ie-1), row, txt(ie:end)]); %! fclose (fid); ## Test a sheet with no time column cannot become a timetable %!error ... %! ods2timetable (fnum) ## Test a file whose event table names its timetable as its own is refused %!error ... %! ods2timetable (fev) ## Test the fixtures are removed again %!test %! delete (fnum); %! delete (fev); %! assert_equal (exist (fnum, 'file'), 0); %! assert_equal (exist (fev, 'file'), 0); pr0m1th3as-datatypes-9450b29/inst/parsePairedArguments.m000066400000000000000000000174631527370255000231710ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {[@var{optarg_1}, @dots{}, @var{optarg_N}] =} @ ## parsePairedArguments (@var{optarg_names}, @var{default_values}, @var{arg_list}) ## @deftypefnx {datatypes} {[@var{optarg_1}, @dots{}, @var{optarg_N}, @var{rem_args}] =} @ ## parsePairedArguments (@var{optarg_names}, @var{default_values}, @var{arg_list}) ## ## Parse optional paired arguments from variable argument list. ## ## @code{parsePairedArguments} parses the optional paired arguments specified ## by @var{optarg_names} from the variable input argument list, @var{arg_list}. ## Any @var{optarg_names} that are not found in @var{arg_list} are returned with ## their default value specified by @var{default_values}, which must be a cell ## array with the same number of elements as @var{optarg_names}. ## ## @var{optarg_names} must be a cell array of character vectors or a string ## array with the same number of elements as the number of output arguments ## specified as @code{[@var{optarg_1}, @dots{}, @var{optarg_N}]}, while an extra ## output argument, @var{rem_args}, may be specified for the remaining input ## arguments in @var{arg_list} that were not specified by @var{optarg_names}. ## ## Each property name specified by @var{optarg_names} is case insensitive. ## ## A property specified more than once in @var{arg_list} takes the value of its ## last occurrence, as in MATLAB. Every occurrence is consumed, so none of them ## is returned in @var{rem_args}. ## ## The following example illustrates how to use @code{parsePairedArguments} ## inside a function to parse optional paired arguments for three properties, ## namely @qcode{'A'}, @qcode{'B'}, and @qcode{'C'}. ## ## @example ## ## Declare optional property Names and their default Values ## optNames = @{'A', 'B', 'C'@}; ## dfValues = @{1, 2, 3@}; ## ## ## Parse optional Name-Value paired arguments ## [var_A, var_B, var_C, args] = ... ## parsePairedArguments (optNames, dfValues, args); ## @end example ## ## @end deftypefn function [varargout] = parsePairedArguments (optNames, dfValues, args) ## Input validation if (nargin != 3) error ("parsePairedArguments: invalid number of input arguments."); endif if (isa (optNames, 'string')) optNames = cellstr (optNames); endif if (! iscellstr (optNames)) error (strcat ("parsePairedArguments: OPTARG_NAMES must be a cell", ... " array of character vectors or a string array.")); endif if (! iscell (dfValues)) error ("parsePairedArguments: DFVALUES must be a cell array."); endif optN = numel (optNames); if (optN != numel (dfValues)) error ("parsePairedArguments: OPTARG_NAMES mismatches DFVALUES."); endif if (nargout < optN || nargout > optN + 1) error ("parsePairedArguments: inconsistent number of output arguments."); endif if (! iscell (args)) error ("parsePairedArguments: ARG_LIST must be a cell array."); endif ## Search through all input arguments for Name/Value pairs. The scan runs ## from the back, where the pairs are, so that leading positional arguments ## are never matched against a property name. foundNames = []; nargs = numel (args); for ii = nargs-1:-1:1 ## A pair consumed further back has shortened ARGS, so this position may ## no longer have a value to pair with. if (ii + 1 > numel (args)) continue; endif tmp_arg = args{ii}; if (isstring (tmp_arg)) tmp_arg = char (tmp_arg); endif if (ischar (tmp_arg)) idx = strcmpi (tmp_arg, optNames); if (any (idx)) idx = find (idx); ## A property given more than once keeps the value it was last given. ## Since the scan runs backwards, a name resolved already lies further ## back, so an earlier occurrence is consumed without overwriting it. if (! any (foundNames == idx)) varargout{idx} = args{ii+1}; foundNames = [foundNames, idx]; endif args(ii:ii+1) = []; endif endif endfor ## Find optNames that were not in args and add defaults allNames = 1:numel (optNames); notfound = ! ismember (allNames, foundNames); defNames = optNames(notfound); for ii = 1:numel (defNames) idx = find (strcmpi (defNames{ii}, optNames)); varargout{idx} = dfValues{idx}; endfor ## Append remaining input arguments in varargout if (nargout > optN) idx = optN + 1; varargout{idx} = args(:); endif endfunction %!shared optNames, dfValues %! optNames = {'A', 'B', 'C'}; %! dfValues = {{3}, [1, 2], 'text'}; %!test %! [a, b, c] = parsePairedArguments (optNames, dfValues, {'A', 5}); %! assert_equal (a, 5); %! assert_equal (b, [1, 2]); %! assert_equal (c, 'text'); %!test %! [a, b, c, args] = parsePairedArguments (optNames, dfValues, {4, 'D', 5}); %! assert_equal (numel (args), 3); %! assert_equal (args{2}, 'D'); %! assert_equal (a, {3}); %! assert_equal (b, [1, 2]); %! assert_equal (c, 'text'); %!test %! [a, b, c] = parsePairedArguments (optNames, dfValues, ... %! {'A', 5, 'b', 1, 'C', 'test'}); %! assert_equal (a, 5); %! assert_equal (b, 1); %! assert_equal (c, 'test'); %!test %! [a, b, c, args] = parsePairedArguments (optNames, dfValues, {1}); %! assert_equal (args, {1}); %! assert_equal (a, {3}); %! assert_equal (b, [1, 2]); %! assert_equal (c, 'text'); %!test %! [a, b, c, args] = parsePairedArguments (optNames, dfValues, {'A', 5, 'A', ... %! 9}); %! assert_equal (a, 9); %! assert_equal (numel (args), 0); %!test %! [a, b, c] = parsePairedArguments (optNames, dfValues, {'A', 5, 'A', 9, ... %! 'A', 7}); %! assert_equal (a, 7); %!test %! [a, b, c] = parsePairedArguments (optNames, dfValues, {'A', 5, 'a', 9}); %! assert_equal (a, 9); %!test %! [a, b, c, args] = parsePairedArguments (optNames, dfValues, ... %! {7, 'c', 4, 'B', 1, 'C', 9}); %! assert_equal (a, {3}); %! assert_equal (b, 1); %! assert_equal (c, 9); %! assert_equal (args, {7}); %!test %! [a, b, c, args] = parsePairedArguments (optNames, dfValues, ... %! {'A', 'B', 'C', 6}); %! assert_equal (a, 'B'); %! assert_equal (b, [1, 2]); %! assert_equal (c, 6); %! assert_equal (numel (args), 0); %!error ... %! parsePairedArguments (optNames, dfValues) %!error ... %! parsePairedArguments ([1, 2, 3], dfValues, {1}) %!error ... %! parsePairedArguments (optNames, [1, 2, 3], {1}) %!error ... %! parsePairedArguments (optNames(1:2), dfValues, {1}) %!error ... %! [a, b] = parsePairedArguments (optNames, dfValues, {1}) %!error ... %! [a, b, c, d, e] = parsePairedArguments (optNames, dfValues, {1}) %!error ... %! [a, b, c, e] = parsePairedArguments (optNames, dfValues, 1) pr0m1th3as-datatypes-9450b29/inst/patients.mat000066400000000000000000000660711527370255000212170ustar00rootroot00000000000000Octave-1-LAgeÿmatrixþÿÿÿdC@€E@C@D@€H@G@€@@D@<@?@€F@E@9@€C@B@H@@@;@€B@I@H@€C@€D@F@<@9@€C@9@B@>@€F@D@9@€G@F@H@F@€A@€@@C@€C@F@F@€B@€F@€B@>@€C@E@E@€H@F@€E@€G@I@C@€D@€F@B@C@=@<@>@<@=@B@€F@@@?@H@9@D@€C@€D@€@@?@€A@@@E@H@A@€C@<@=@@@€C@€B@€H@?@€B@C@€F@>@H@H@9@F@€H@€F@H@ Diastolicÿmatrixþÿÿÿd@W@@S@ÀT@ÀR@T@€Q@V@€T@€S@€U@@S@Q@€R@ÀW@ÀS@W@ÀW@ÀS@@S@S@ÀR@ÀS@V@€V@X@@S@T@S@ÀT@@V@W@ÀT@T@U@W@ÀT@€V@@U@€V@€R@W@T@@V@X@@V@@S@@T@S@ÀT@€S@ÀW@ÀV@ÀV@€U@@V@ÀS@€R@€T@S@@T@@S@@R@@U@S@T@T@ÀS@€T@ÀS@€T@ÀR@ÀV@€R@€S@@U@U@ÀR@€S@@T@ÀS@@U@ÀS@€T@T@T@W@W@X@ÀU@@T@€V@@S@ÀV@ÀS@@R@ÀX@W@€R@@W@€U@Genderÿcellþÿÿÿdÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleHeightÿmatrixþÿÿÿdÀQ@@Q@P@ÀP@P@Q@P@Q@Q@€P@Q@€P@ÀQ@R@@P@ÀQ@@Q@@Q@€Q@Q@@P@P@O@€P@@P@€Q@€O@€O@Q@ÀP@€Q@€P@P@€Q@ÀQ@€P@ÀQ@€P@€P@€O@ÀQ@@Q@€Q@€Q@ÀP@@P@Q@O@€Q@ÀP@Q@O@P@€P@R@€O@€P@€Q@ÀQ@Q@€O@@P@ÀP@€P@Q@ÀQ@€Q@N@P@P@€P@P@R@@P@ÀP@R@P@Q@€P@P@Q@@Q@@Q@P@€O@Q@@P@€O@€P@@P@Q@ÀQ@€Q@ÀQ@€P@@Q@@Q@€Q@Q@€P@LastNameÿcellþÿÿÿdÿ sq_stringþÿÿÿSmithÿ sq_stringþÿÿÿJohnsonÿ sq_stringþÿÿÿWilliamsÿ sq_stringþÿÿÿJonesÿ sq_stringþÿÿÿBrownÿ sq_stringþÿÿÿDavisÿ sq_stringþÿÿÿMillerÿ sq_stringþÿÿÿWilsonÿ sq_stringþÿÿÿMooreÿ sq_stringþÿÿÿTaylorÿ sq_stringþÿÿÿAndersonÿ sq_stringþÿÿÿThomasÿ sq_stringþÿÿÿJacksonÿ sq_stringþÿÿÿWhiteÿ sq_stringþÿÿÿHarrisÿ sq_stringþÿÿÿMartinÿ sq_stringþÿÿÿThompsonÿ sq_stringþÿÿÿGarciaÿ sq_stringþÿÿÿMartinezÿ sq_stringþÿÿÿRobinsonÿ sq_stringþÿÿÿClarkÿ sq_stringþÿÿÿ Rodriguezÿ sq_stringþÿÿÿLewisÿ sq_stringþÿÿÿLeeÿ sq_stringþÿÿÿWalkerÿ sq_stringþÿÿÿHallÿ sq_stringþÿÿÿAllenÿ sq_stringþÿÿÿYoungÿ sq_stringþÿÿÿ Hernandezÿ sq_stringþÿÿÿKingÿ sq_stringþÿÿÿWrightÿ sq_stringþÿÿÿLopezÿ sq_stringþÿÿÿHillÿ sq_stringþÿÿÿScottÿ sq_stringþÿÿÿGreenÿ sq_stringþÿÿÿAdamsÿ sq_stringþÿÿÿBakerÿ sq_stringþÿÿÿGonzalezÿ sq_stringþÿÿÿNelsonÿ sq_stringþÿÿÿCarterÿ sq_stringþÿÿÿMitchellÿ sq_stringþÿÿÿPerezÿ sq_stringþÿÿÿRobertsÿ sq_stringþÿÿÿTurnerÿ sq_stringþÿÿÿPhillipsÿ sq_stringþÿÿÿCampbellÿ sq_stringþÿÿÿParkerÿ sq_stringþÿÿÿEvansÿ sq_stringþÿÿÿEdwardsÿ sq_stringþÿÿÿCollinsÿ sq_stringþÿÿÿStewartÿ sq_stringþÿÿÿSanchezÿ sq_stringþÿÿÿMorrisÿ sq_stringþÿÿÿRogersÿ sq_stringþÿÿÿReedÿ sq_stringþÿÿÿCookÿ sq_stringþÿÿÿMorganÿ sq_stringþÿÿÿBellÿ sq_stringþÿÿÿMurphyÿ sq_stringþÿÿÿBaileyÿ sq_stringþÿÿÿRiveraÿ sq_stringþÿÿÿCooperÿ sq_stringþÿÿÿ Richardsonÿ sq_stringþÿÿÿCoxÿ sq_stringþÿÿÿHowardÿ sq_stringþÿÿÿWardÿ sq_stringþÿÿÿTorresÿ sq_stringþÿÿÿPetersonÿ sq_stringþÿÿÿGrayÿ sq_stringþÿÿÿRamirezÿ sq_stringþÿÿÿJamesÿ sq_stringþÿÿÿWatsonÿ sq_stringþÿÿÿBrooksÿ sq_stringþÿÿÿKellyÿ sq_stringþÿÿÿSandersÿ sq_stringþÿÿÿPriceÿ sq_stringþÿÿÿBennettÿ sq_stringþÿÿÿWoodÿ sq_stringþÿÿÿBarnesÿ sq_stringþÿÿÿRossÿ sq_stringþÿÿÿ Hendersonÿ sq_stringþÿÿÿColemanÿ sq_stringþÿÿÿJenkinsÿ sq_stringþÿÿÿPerryÿ sq_stringþÿÿÿPowellÿ sq_stringþÿÿÿLongÿ sq_stringþÿÿÿ Pattersonÿ sq_stringþÿÿÿHughesÿ sq_stringþÿÿÿFloresÿ sq_stringþÿÿÿ Washingtonÿ sq_stringþÿÿÿButlerÿ sq_stringþÿÿÿSimmonsÿ sq_stringþÿÿÿFosterÿ sq_stringþÿÿÿGonzalesÿ sq_stringþÿÿÿBryantÿ sq_stringþÿÿÿ Alexanderÿ sq_stringþÿÿÿRussellÿ sq_stringþÿÿÿGriffinÿ sq_stringþÿÿÿDiazÿ sq_stringþÿÿÿHayesLocationÿcellþÿÿÿdÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General HospitalSelfAssessedHealthStatusÿcellþÿÿÿdÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿFairSmokerÿ bool matrixþÿÿÿdSystolicÿmatrixþÿÿÿd_@@[@@_@@]@€^@@^@@`@À\@À\@€]@€\@À\@À_@@`@€\@@`@_@À^@À]@@_@@^@À^@€\@`@ `@€\@@\@@_@^@À_@À`@@^@À\@À_@@^@À_@a@@]@_@^@`@]@€`@ a@@]@]@À]@À^@]@_@ `@@`@€`@@]@ `@€]@^@@a@@]@@\@€^@À\@^@@]@À^@À^@À]@€[@@^@@a@@_@€^@^@@]@@_@_@@^@€]@^@€]@€]@€^@À`@``@@\@@_@à`@`@À^@€^@@a@_@@`@À^@ `@`@_@À]@a@€\@Weightÿmatrixþÿÿÿdf@`d@``@ `@À]@Àa@Àa@€f@àf@€`@`@ a@Àe@@i@ `@ f@àg@``@`f@€e@ `@@]@ a@@b@À^@ g@àa@€\@Àd@@g@€_@ a@@a@`g@ h@ a@h@€]@€f@`@€d@àf@ e@@h@€e@à`@Àf@@^@Àc@`f@@e@a@à`@`b@@g@_@À`@@e@€f@@`@@`@À_@ a@À[@À`@ g@ a@a@@`@ a@@g@À_@f@À_@À\@@f@``@àf@@h@€_@@g@€g@ g@^@€`@Àf@^@À^@ a@ `@g@ f@_@Àe@À`@`e@€g@@g@€e@ f@pr0m1th3as-datatypes-9450b29/inst/private/000077500000000000000000000000001527370255000203255ustar00rootroot00000000000000pr0m1th3as-datatypes-9450b29/inst/private/__a1ref__.m000066400000000000000000000057501527370255000223040ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {[@var{r1}, @var{c1}, @var{r2}, @var{c2}] =} __a1ref__ (@var{spec}) ## ## Parse a spreadsheet A1-style range into 1-based row and column bounds. ## ## @var{spec} is a character vector or string scalar such as @qcode{'C5'} (a ## single-cell anchor) or @qcode{'C5:D8'} (a rectangle). The return values are ## the top-left corner (@var{r1}, @var{c1}) and bottom-right corner (@var{r2}, ## @var{c2}). A single-cell spec yields an open-ended rectangle with ## @var{r2} = @var{c2} = @code{Inf}, meaning "from this cell to the end of the ## used range". Reversed corners (e.g.@: @qcode{'D8:C5'}) are normalised. ## ## This is an internal helper shared by @code{readtable} and @code{writetable}. ## Do NOT call it directly. ## ## @end deftypefn function [r1, c1, r2, c2] = __a1ref__ (spec) if (isa (spec, 'string')) spec = char (spec); endif if (! (ischar (spec) && isrow (spec))) error ("__a1ref__: RANGE must be a character vector or string scalar."); endif parts = ostrsplit (spec, ':'); if (numel (parts) == 1) [r1, c1] = parse_cell (spec); r2 = Inf; c2 = Inf; elseif (numel (parts) == 2) [r1, c1] = parse_cell (parts{1}); [r2, c2] = parse_cell (parts{2}); ## Normalise so (r1,c1) is the top-left and (r2,c2) the bottom-right corner if (r2 < r1) [r1, r2] = deal (r2, r1); endif if (c2 < c1) [c1, c2] = deal (c2, c1); endif else error ("__a1ref__: invalid range '%s'.", spec); endif endfunction ## Parse a single A1 cell reference ('C5') into 1-based (row, column). function [r, c] = parse_cell (s) tok = regexp (s, '^([A-Za-z]+)([0-9]+)$', 'tokens'); if (isempty (tok)) error ("__a1ref__: invalid cell reference '%s'.", s); endif letters = toupper (tok{1}{1}); c = 0; for i = 1:numel (letters) c = c * 26 + (double (letters(i)) - double ('A') + 1); endfor r = str2double (tok{1}{2}); if (r < 1) error ("__a1ref__: invalid row number in cell reference '%s'.", s); endif endfunction ## __a1ref__ is a private helper; it carries no inline BIST blocks (private ## functions are not reachable by 'pkg test'). Its behaviour is covered ## indirectly through the 'Range' tests of readtable and table.writetable. pr0m1th3as-datatypes-9450b29/inst/private/__binassign__.m000066400000000000000000000040301527370255000232510ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{bin} =} __binassign__ (@var{xv}, @var{ev}, @var{leftClosed}) ## ## Assign values to bins. ## ## @code{@var{bin} = __binassign__ (@var{xv}, @var{ev}, @var{leftClosed})} ## returns the index of the bin each element of the column vector @var{xv} falls ## into, given the non-decreasing row vector of edges @var{ev}. @qcode{NaN} is ## returned for values that fall in no bin and for @qcode{NaN} input. ## ## When @var{leftClosed} is true each bin covers @code{[e(j), e(j+1))} with the ## last bin closed at both ends; otherwise each covers @code{(e(j), e(j+1)]} ## with the first bin closed at both ends. Repeated edges define empty bins, ## which are never selected. ## ## This is shared by @code{discretize} and @code{histcounts} so that the two can ## never disagree about which bin a value belongs to. ## ## @end deftypefn function bin = __binassign__ (xv, ev, leftClosed) nbins = numel (ev) - 1; if (leftClosed) bin = lookup (ev, xv); bin(bin == 0) = NaN; bin(xv > ev(end)) = NaN; bin(xv == ev(end)) = nbins; else bin = nbins + 1 - lookup (-fliplr (ev), -xv); bin(bin < 1 | bin > nbins) = NaN; bin(xv < ev(1) | xv > ev(end)) = NaN; bin(xv == ev(1)) = 1; endif bin(isnan (xv)) = NaN; endfunction pr0m1th3as-datatypes-9450b29/inst/private/__binedges__.m000066400000000000000000000060661527370255000230670ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{edges} =} __binedges__ (@var{xmin}, @var{xmax}, @var{nbins}) ## ## Bin edges for a requested bin count. ## ## @code{@var{edges} = __binedges__ (@var{xmin}, @var{xmax}, @var{nbins})} ## returns a row vector of @code{@var{nbins} + 1} uniformly spaced edges ## covering the closed interval @code{[@var{xmin}, @var{xmax}]}, placed at ## "nice" decimal positions. @var{xmin} and @var{xmax} must be finite real ## scalars with @code{@var{xmin} <= @var{xmax}}, and @var{nbins} a positive ## integer. ## ## This is the shared edge generator behind the bin-count syntax of both ## @code{discretize} and @code{histcounts}; the two must never disagree. ## ## @end deftypefn function edges = __binedges__ (xmin, xmax, nbins) ## Constant data: a unit-wide interval centred on the value if (xmin == xmax) edges = (xmin - 0.5) + (0:nbins) * (1 / nbins); return; endif ## The left edge is anchored to a multiple of the raw bin width truncated to ## one significant digit. Note this is NOT the {1,2,3,5,10} ladder used by ## the automatic bin-width rules -- every leading digit occurs here. rawWidth = (xmax - xmin) / nbins; powTen = 10 ^ floor (log10 (rawWidth)); anchor = powTen * floor (rawWidth / powTen); left = anchor * floor (xmin / anchor); ## The width is then recomputed from the SHIFTED range, and rounded up onto a ## decimal grid whose fineness tracks NBINS. Because the grid unit is about ## rawWidth / NBINS, the total over-coverage never exceeds one bin width. ## ## Known residual: 1 case in 426 measured against MATLAB R2024a lands on a ## ceil() boundary where MATLAB's internal scaling rounds the other way ## ([0, 0.0091] with 14 bins gives 0.00065 here, 0.00066 there). Scaling by ## the reciprocal power of ten instead reproduces that case but breaks two ## others, so the division is kept. Both binnings are valid -- ours is the ## tighter of the two and still covers the data. if (nbins == 1) width = xmax - left; else shifted = (xmax - left) / nbins; unit = 10 ^ floor (log10 (shifted / (nbins - 1))); width = ceil (shifted / unit) * unit; endif edges = left + (0:nbins) * width; ## Guard against a rounding shortfall at the top edge if (edges(end) < xmax) edges(end) = xmax; endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__cell2tbl__.m000066400000000000000000000115661527370255000230130ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{tbl} =} __cell2tbl__ (@var{C}, @var{T}, @var{N}, @var{D}, @var{U}, @var{RowNames}, @var{leaf}) ## @deftypefnx {datatypes} {@var{tbl} =} __cell2tbl__ (@var{C}, @var{T}, @var{N}, @var{D}, @var{U}, @var{RowNames}, @var{leaf}, @var{A}) ## ## Rebuild a table from the flat data and header blocks of a house I/O file. ## ## @var{C} is the data block, one column per written column, and @var{T}, ## @var{N}, @var{D} and @var{U} are the variable-type, name, description and ## unit blocks, each with one row per nesting level. Columns sharing a name are ## one multicolumn variable; a variable whose type row goes deeper is a nested ## table or a structure and is rebuilt by recursion. ## ## @var{leaf} converts one variable's columns to its value and is called as ## @code{@var{leaf} (@var{varC}, @var{varA}, @var{typestr})}. @var{A} is an ## optional companion block sliced by column alongside @var{C}, for a reader ## that carries per-cell information of its own, such as the ODS value types; it ## defaults to empty. ## ## @end deftypefn function tbl = __cell2tbl__ (C, T, N, D, U, RowNames, leaf, A = {}) ## Get names, number, and positions of top level variables [varNames, ii, varIdx] = __unique__ (N(1,:), 'stable'); varlen = numel (ii); varValues = cell (1, varlen); ## No nested table or structure if (size (T, 1) == 1) for ix = 1:varlen colIdx = varIdx == ix; varC = C(:,colIdx); varA = slice_companion (A, colIdx); varValues{ix} = leaf (varC, varA, T{ii(ix)}); endfor ## Table contains nested tables or structures else ## For each top level variable search for nested tables or structures for ix = 1:varlen colIdx = varIdx == ix; varC = C(:,colIdx); varA = slice_companion (A, colIdx); varN = N(:,colIdx); varT = T(:,colIdx); ## No nested table or structure in this variable if (all (__ismissing__ (varT(2,:)))) varValues{ix} = leaf (varC, varA, varT{1}); ## Check for structure elseif (all (strcmp (varT(1,:), 'struct'))) varValues{ix} = cell2struct (varC, varN(2,:), 2); ## Check for table elseif (all (strcmp (varT(1,:), 'table'))) ## Pass the nested descriptions/units (rows below the top-level one) ## down so the recursive call restores them on the inner table. if (isempty (D)) varD = []; else varD = D(2:end,colIdx); endif if (isempty (U)) varU = []; else varU = U(2:end,colIdx); endif varValues{ix} = __cell2tbl__ (varC, varT(2:end,:), varN(2:end,:), ... varD, varU, [], leaf, varA); endif endfor endif ## Create table if (isempty (RowNames)) tbl = table (varValues{:}, 'VariableNames', varNames); else tbl = table (varValues{:}, 'VariableNames', varNames, 'RowNames', RowNames); endif ## Restore variable descriptions and units, when present. The first header ## row holds each top-level variable's metadata (deeper rows belong to nested ## tables and are restored by the recursive calls above). Multicolumn ## variables repeat the entry across split columns, so take the first column ## belonging to each variable. ## A level whose entries are all value-less carries no property at all, ## where one holding an empty string carries a blank; a file written before ## the two were told apart has empty strings throughout and reads as set. tbl = restore_meta (tbl, 'VariableDescriptions', D, ii); tbl = restore_meta (tbl, 'VariableUnits', U, ii); endfunction ## A companion block is sliced by the same columns as the data block; a reader ## that has none passes an empty one through untouched. function varA = slice_companion (A, colIdx) if (isempty (A)) varA = {}; else varA = A(:,colIdx); endif endfunction function tbl = restore_meta (tbl, prop, block, ii) if (isempty (block)) return; endif entries = block(1,ii); isset = cellfun (@ischar, entries); if (! any (isset)) return; endif entries(! isset) = {''}; tbl.Properties.(prop) = entries; endfunction pr0m1th3as-datatypes-9450b29/inst/private/__colkind__.m000066400000000000000000000034571527370255000227330ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{kind} =} __colkind__ (@var{C}, @var{VT}) ## ## The kind of a spreadsheet column that declares no type of its own. ## ## @var{C} holds the column's cells and @var{VT} their value types. A column ## whose typed cells are numbers, apart from text cells reading @qcode{Inf}, ## @qcode{-Inf} or @qcode{NaN}, is @qcode{float}, so that an infinity written ## as text keeps its column numeric wherever it falls. Otherwise the kind is ## that of the first typed cell, and a column with no typed cell is ## @qcode{string}. ## ## @end deftypefn function kind = __colkind__ (C, VT) typed = ! cellfun (@isempty, VT); if (! any (typed)) kind = 'string'; return; endif vt = VT(typed); kind = vt{1}; isnum = strcmp (vt, 'float'); if (any (isnum)) txt = C(typed); txt = txt(! isnum); special = @(x) ischar (x) && any (strcmpi (x, {'Inf', '-Inf', 'NaN'})); if (all (strcmp (vt(! isnum), 'string')) && all (cellfun (special, txt))) kind = 'float'; endif endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__disp__.m000066400000000000000000000111111527370255000222310ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {private} {} __disp__ (@var{obj}, @var{class}) ## @deftypefnx {private} {} __disp__ (@var{obj}, @var{class}, @var{varname}) ## ## Display arrays of datatype objects. ## ## @end deftypefn function __disp__ (this, datatype, name = 'ans') if (isempty (this)) str = strjoin (repmat ({'%d'}, 1, ndims (this)), 'x'); str = sprintf ('\n %s empty %s array\n\n', str, datatype); fprintf (str, size (this)); elseif (isscalar (this)) fprintf (' %s\n\n', datatype); fprintf (' %s\n', dispstrings (this){:}); elseif (ismatrix (this)) fprintf (' %dx%d %s array\n\n', size (this), datatype); dispcstrmatrix (dispstrings (this)); else str = strjoin (repmat ({'%d'}, 1, ndims (this)), 'x'); str = sprintf ('\n %s %s array\n\n', str, datatype); fprintf (str, size (this)); ## Handle each page separately sz = size (this); high_sz = sz(3:end); high_ixs = {}; for i = 1:numel (high_sz) high_ixs{i} = [1:high_sz(i)]'; endfor page_ixs = combvec (high_ixs); idx.type = '()'; for ix = 1:size (page_ixs, 1) p_ix = page_ixs(ix,:); idx.subs = {":", ":", num2cell(p_ix){:}}; pagestr = sprintf (strjoin (repmat ({'%d'}, 1, numel (p_ix)), ':'), p_ix); fprintf ('%s(:,:,%s) = \n\n', name, pagestr); page_this = subsref (this, idx); dispcstrmatrix (dispstrings (page_this)); endfor endif endfunction function dispcstrmatrix (cstr) sz = terminal_size (); cols = sz(2) - 4; colgap = " "; optLens = []; for iCol = 1:size (cstr, 2) optLen = max (cellfun ('length', cstr(:, iCol))); optLens = [optLens, optLen]; endfor if (sum (optLens + 4) <= cols) # all columns fit in terminal size rowSpat = ""; for iCol = 1:size (cstr, 2) rowSpat = [rowSpat, sprintf("%%-%ds", optLens(iCol)), colgap]; endfor for iRow = 1:size (cstr, 1) strrow = sprintf (rowSpat, cstr{iRow,:}); fprintf (" %s\n", strrow); endfor fprintf ("\n"); else # we need to split rows optLen_cs = cumsum (optLens + 6); startCol = 1; while (! isempty (find (optLen_cs > cols))) stopCol = find (optLen_cs > cols, 1) - 1; rowSpat = ""; for iCol = 1:stopCol rowSpat = [rowSpat, sprintf("%%-%ds", optLens(iCol)), colgap]; endfor optLens(1:iCol) = []; optLen_cs = cumsum (optLens + 6); stopCol = stopCol + startCol - 1; fprintf ("Columns %d through %d:\n\n", startCol, stopCol); for iRow = 1:size (cstr, 1) strrow = sprintf (rowSpat, cstr{iRow,[startCol:stopCol]}); fprintf (" %s\n", strrow); endfor fprintf ("\n"); startCol = stopCol + 1; endwhile if (! isempty (optLens)) for iCol = 1:length (optLens) rowSpat = [rowSpat, sprintf("%%-%ds", optLens(iCol)), colgap]; endfor stopCol = startCol + iCol - 1; if (startCol == stopCol) fprintf ("Column %d:\n\n", startCol); else fprintf ("Columns %d through %d:\n\n", startCol, stopCol); endif for iRow = 1:size (cstr, 1) strrow = sprintf (rowSpat, cstr{iRow,[startCol:stopCol]}); fprintf (" %s\n", strrow); endfor fprintf ("\n"); endif endif endfunction function out = combvec (vecs) switch (numel (vecs)) case 1 out = vecs{1}(:); case 2 a = vecs{1}(:); b = vecs{2}(:); out = repmat (a, numel (b), 2); i_comb = 1; for i_a = 1:numel (a) for i_b = 1:numel (b) out(i_comb,:) = [a(i_a), b(i_b)]; i_comb = i_comb + 1; endfor endfor otherwise out = []; a = vecs{1}(:); rest = vecs(2:end); rest_combs = combvec (rest); for i = 1:numel (a) out = [out; [repmat(a(i), [size(rest_combs,1), 1]), rest_combs]]; endfor endswitch endfunction pr0m1th3as-datatypes-9450b29/inst/private/__dt2rfc9557__.m000066400000000000000000000047361527370255000230270ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{C} =} __dt2rfc9557__ (@var{dt}) ## ## Format a zone-aware @code{datetime} column as RFC 9557 text. ## ## Each entry is written as the local time, the offset from UTC, and the zone ## name in square brackets, as in ## @qcode{2024-03-09T22:00:00-05:00[America/New_York]}. That is the ## Internet Extended Date/Time Format of RFC 9557, which extends ISO 8601 with ## the zone name, and is what a Java @code{ZonedDateTime} prints. ## ## The offset alone does not name a zone, and the zone name alone does not say ## which side of a repeated hour an instant falls on; the two together say both, ## so a fold round-trips. @code{NaT} yields an empty string, which a writer ## records as a missing field. ## ## @seealso{__rfc95572dt__} ## @end deftypefn function C = __dt2rfc9557__ (dt) dt = dt(:); n = numel (dt); C = cell (n, 1); zone = dt.TimeZone; osec = seconds (tzoffset (dt)); [Y, M, D] = ymd (dt); [h, m, s] = hms (dt); for i = 1:n if (isnan (Y(i))) C{i} = ''; continue; endif o = osec(i); sgn = '+'; if (o < 0) sgn = '-'; endif a = abs (o); oh = floor (a / 3600); om = floor (mod (a, 3600) / 60); C{i} = sprintf ('%04d-%02d-%02dT%02d:%02d:%s%s%02d:%02d[%s]', ... Y(i), M(i), D(i), h(i), m(i), sec_text (s(i)), ... sgn, oh, om, zone); endfor endfunction ## Seconds with a fractional part only where there is one, so that a whole ## second is written as two digits rather than with trailing zeros. function out = sec_text (s) if (s == fix (s)) out = sprintf ('%02d', s); else out = sprintf ('%09.6f', s); out = regexprep (out, '0+$', ''); endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__groupbins__.m000066400000000000000000000535331527370255000233200ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{tf} =} __groupbins__ (@qcode{'is_spec'}, @var{x}) ## @deftypefnx {datatypes} {[@var{cols}, @var{names}, @var{errmsg}] =} __groupbins__ (@qcode{'bin'}, @var{cols}, @var{names}, @var{scheme}, @var{incedge}, @var{caller}) ## ## Shared @qcode{'groupbins'} engine for the grouping methods and functions. ## ## @code{__groupbins__ (@qcode{'is_spec'}, @var{x})} returns true when @var{x} ## is a @qcode{'groupbins'} binning specification (used to tell a positional ## binning argument apart from a method argument). ## ## @code{__groupbins__ (@qcode{'bin'}, @var{cols}, @var{names}, @var{scheme}, ## @var{incedge}, @var{caller})} bins each grouping-variable column in ## @var{cols} per @var{scheme} (a number of bins, an edge vector, a ## @code{duration} / @code{calendarDuration} bin width, or a datetime time-unit ## keyword), returning the binned columns (categoricals) and the MATLAB-style ## output variable names (@qcode{@var{unit}_@var{var}} for a time unit, ## @qcode{disc_@var{var}} otherwise; unbinned variables keep their name). ## @var{errmsg} is a message body the caller emits under its own name (empty on ## success). ## ## This is an internal helper; do NOT call it directly. ## ## @end deftypefn function varargout = __groupbins__ (op, varargin) switch (op) case 'is_spec' varargout{1} = gb_is_spec (varargin{1}); case 'bin' [varargout{1}, varargout{2}, varargout{3}] = gb_bin (varargin{:}); otherwise error ("__groupbins__: unknown operation '%s'.", op); endswitch endfunction ## Contiguous calendar-period time-unit keywords (datetime only). The component ## keywords ('monthofyear', 'dayofweek', ...) are recognised as specs but not ## yet implemented (they raise a clean "not yet supported" error when binning). ## The duration a time unit stands for, or [] where the unit has no fixed ## length of its own and so cannot measure a duration. function w = gb_duration_unit (unit) switch (unit) case 'second' w = seconds (1); case 'minute' w = minutes (1); case 'hour' w = hours (1); case 'day' w = days (1); case 'year' w = years (1); otherwise w = []; endswitch endfunction function kw = gb_period_units () kw = {'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', ... 'year', 'decade', 'century'}; endfunction function kw = gb_keywords () kw = [{'none'}, gb_period_units(), {'dayname', 'monthname', 'dayofweek', ... 'dayofmonth', 'dayofyear', 'hourofday', 'weekofmonth', 'weekofyear', ... 'monthofyear', 'quarterofyear', 'secondofminute', 'minuteofhour'}]; endfunction function tf = gb_is_charrow (x) tf = (ischar (x) && isrow (x)) || (isa (x, 'string') && isscalar (x)); endfunction function tf = gb_is_none (x) tf = gb_is_charrow (x) && strcmpi (char (x), 'none'); endfunction ## True if X is a single binning-scheme element. function tf = gb_is_scheme_elem (x) tf = false; if ((isnumeric (x) || islogical (x)) && ! isempty (x)) tf = true; elseif (isa (x, 'datetime') || isa (x, 'duration') ... || isa (x, 'calendarDuration')) tf = true; elseif (gb_is_charrow (x)) tf = any (strcmpi (char (x), gb_keywords ())); endif endfunction ## True if X is a binning specification: a single element or a cell of elements. function tf = gb_is_spec (x) tf = gb_is_scheme_elem (x); if (! tf && iscell (x) && ! isempty (x)) tf = all (cellfun (@gb_is_scheme_elem, x(:)')); endif endfunction ## Normalise SCHEME into a 1-by-K per-variable cell. function [schemes, errmsg] = gb_normalise_schemes (scheme, K, caller) errmsg = ''; schemes = {}; if (iscell (scheme)) if (numel (scheme) != K) errmsg = sprintf (strcat ("GROUPBINS as a cell array must hold one", ... " binning scheme per grouping variable", ... " (%d)."), ... K); return; endif schemes = scheme(:)'; else schemes = repmat ({scheme}, 1, K); endif endfunction ## Bin each grouping column; return updated columns and their output names. function [cols, names, errmsg] = gb_bin (cols, names, scheme, incEdge, caller) errmsg = ''; K = numel (cols); [schemes, errmsg] = gb_normalise_schemes (scheme, K, caller); if (! isempty (errmsg)) return; endif for j = 1:K if (gb_is_none (schemes{j})) continue; # no binning for this variable endif [b, nm, errmsg] = gb_bin_col (cols{j}, schemes{j}, incEdge, names{j}); if (! isempty (errmsg)) return; endif cols{j} = b; names{j} = nm; endfor endfunction ## Bin one grouping column COL per SCHEME; return the categorical, its output ## name, and an errmsg body (empty on success). function [binned, newname, errmsg] = gb_bin_col (col, scheme, incEdge, varname) binned = []; newname = varname; errmsg = ''; n = numel (col); ## Time-unit keyword scheme. if (gb_is_charrow (scheme)) unit = lower (char (scheme)); if (any (strcmp (unit, gb_period_units ()))) if (isduration (col)) ## A duration carries no calendar, so a unit measures it only where ## the unit has a fixed length of its own, which is to say where ## there is a duration constructor for it. A week, a month and a ## quarter have none and are refused; a year is the fixed 365.2425 ## days that 'years' builds. w = gb_duration_unit (unit); if (isempty (w)) errmsg = sprintf (strcat ("binning grouping variable '%s' by", ... " time", ... " unit '%s' is not supported for duration", ... " variables; use 'second', 'minute', 'hour',", ... " 'day' or 'year'."), varname, unit); return; endif [idx, labs, errmsg] = gb_width_bins (col, w, incEdge, varname); if (! isempty (errmsg)) return; endif binned = gb_make_categorical (idx, labs, n); newname = [unit, '_', varname]; return; endif if (! isa (col, 'datetime')) errmsg = sprintf (strcat ("binning grouping variable '%s' by time", ... " unit", ... " '%s' is only supported for datetime", ... " variables."), ... varname, unit); return; endif [idx, labs] = gb_period_bins (col, unit, incEdge); binned = gb_make_categorical (idx, labs, n); newname = [unit, '_', varname]; return; endif errmsg = sprintf (strcat ("binning grouping variable '%s' by '%s' is", ... " not", ... " yet supported."), varname, unit); return; endif ## Duration / calendarDuration bin-width scalar. if ((isa (scheme, 'duration') || isa (scheme, 'calendarDuration')) ... && isscalar (scheme)) [idx, labs, errmsg] = gb_width_bins (col, scheme, incEdge, varname); if (! isempty (errmsg)) return; endif binned = gb_make_categorical (idx, labs, n); newname = ['disc_', varname]; return; endif ## Number of bins / edge vector. [idx, labs, errmsg] = gb_edge_bins (col, scheme, incEdge, varname); if (! isempty (errmsg)) return; endif binned = gb_make_categorical (idx, labs, n); newname = ['disc_', varname]; endfunction ## Build a categorical from per-row bin indices IDX (NaN -> ) and the ## ordered category labels LABS. function c = gb_make_categorical (idx, labs, n) rowLab = repmat ({''}, n, 1); ok = ! isnan (idx); rowLab(ok) = labs(idx(ok)); c = categorical (rowLab, labs); endfunction ## ---- Number-of-bins / edge-vector binning (numeric, datetime, duration) ---- ## The numeric proxy of a set of edges, in the same space 'gb_edge_bins' ## works in: a serial date number for a datetime, days for a duration, and ## the value itself for anything numeric. function p = gb_proxy_of (edges, ctype) switch (ctype) case 'datetime' p = gb_dt2dn (edges)(:)'; case 'duration' p = days (edges)(:)'; otherwise p = double (edges)(:)'; endswitch endfunction function [idx, labs, errmsg] = gb_edge_bins (col, scheme, incEdge, varname) idx = []; labs = {}; errmsg = ''; if (isa (col, 'datetime')) proxy = gb_dt2dn (col)(:); ctype = 'datetime'; elseif (isa (col, 'duration')) proxy = days (col)(:); ctype = 'duration'; elseif (isnumeric (col) || islogical (col)) proxy = double (col)(:); ctype = 'numeric'; else errmsg = sprintf (strcat ("binning is not supported for grouping", ... " variable", ... " '%s' of type '%s'."), varname, class (col)); return; endif if (isscalar (scheme) && (isnumeric (scheme) || islogical (scheme))) nb = double (scheme); if (! (nb >= 1 && nb == fix (nb))) errmsg = sprintf (strcat ("the number of bins for grouping variable", ... " '%s' must be a positive integer."), varname); return; endif ## The edges a bin count implies are the ones 'discretize' picks: the ## grouping methods share their rule with it and with 'histcounts', which ## is a rounded bin width rather than the data range divided evenly, and ## each type has its own ladder of units to snap to. Asking 'discretize' ## is how they stay one rule rather than three. [~, edges] = discretize (col, nb); edgesP = gb_proxy_of (edges, ctype); elseif (isnumeric (scheme)) if (! strcmp (ctype, 'numeric')) errmsg = sprintf (strcat ("bin edges for grouping variable '%s' must", ... " be", ... " of type '%s'."), varname, ctype); return; endif edgesP = sort (double (scheme(:)')); elseif (isa (scheme, 'datetime')) if (! strcmp (ctype, 'datetime')) errmsg = sprintf (strcat ("bin edges for grouping variable '%s' must", ... " be", ... " of type '%s'."), varname, ctype); return; endif edgesP = sort (gb_dt2dn (scheme)(:)'); elseif (isa (scheme, 'duration')) if (! strcmp (ctype, 'duration')) errmsg = sprintf (strcat ("bin edges for grouping variable '%s' must", ... " be", ... " of type '%s'."), varname, ctype); return; endif edgesP = sort (days (scheme(:))'); else errmsg = sprintf ("invalid binning scheme for grouping variable '%s'.", ... varname); return; endif if (numel (edgesP) < 2 || any (isnan (edgesP)) || any (diff (edgesP) <= 0)) errmsg = sprintf (strcat ("bin edges for grouping variable '%s' must", ... " be at", ... " least two finite, strictly increasing", ... " values."), varname); return; endif [idx, labs] = gb_assign_intervals (proxy, edgesP, incEdge, ... gb_edge_labels (edgesP, ctype, ... gb_dur_fmt (col, ctype))); endfunction ## ---- Duration / calendarDuration bin-width binning ---- function [idx, labs, errmsg] = gb_width_bins (col, width, incEdge, varname) idx = []; labs = {}; errmsg = ''; if (isa (col, 'datetime')) proxy = gb_dt2dn (col)(:); ctype = 'datetime'; elseif (isa (col, 'duration')) proxy = days (col)(:); ctype = 'duration'; else errmsg = sprintf (strcat ("bin-width binning is not supported for", ... " grouping", ... " variable '%s' of type '%s'."), varname, class (col)); return; endif good = proxy(! isnan (proxy)); if (isempty (good)) idx = NaN (numel (proxy), 1); labs = {'[0, 0)'}; return; endif if (! isa (width, 'duration')) # calendarDuration width errmsg = sprintf (strcat ("calendarDuration bin widths for grouping", ... " variable '%s' are not yet supported; use a time-unit", ... " keyword", ... " such as 'month'."), varname); return; endif w = days (width); if (! (w > 0)) errmsg = sprintf (strcat ("bin width for grouping variable '%s'", ... " must be positive."), varname); return; endif ## Anchor width bins to multiples of the width from 0. The last bin is ## closed and so holds the largest value itself: a span that is an exact ## multiple of the width needs that many bins and not one more, which is ## what rounding the ratio up after nudging it would have added. The nudge ## goes downwards instead, so that a ratio a hair over a whole number from ## the division alone does not buy an empty bin at the end. lo = floor (min (good) / w) * w; hi = max (good); r = (hi - lo) / w; ne = max (1, ceil (r - 4 * eps (max (r, 1)))); ## A bin per width across the span, which a narrow width over a wide span ## makes a great many of. MATLAB silently stops at 65536 of them, leaving ## a grid that does not reach the end of the span; the count is refused ## here instead, so that no result comes back binned by a grid that covers ## less than it says. The ceiling is the categorical's own. if (ne > 65535) errmsg = sprintf (strcat ("binning grouping variable '%s' by that", ... " width", ... " needs %d bins, more than the 65535 a categorical", ... " can hold; use a wider bin."), varname, ne); return; endif edgesP = lo + (0:ne) * w; ## The last bin is closed, so the largest value belongs in it; the top edge ## is computed by multiplication and can land an ulp below that value, which ## would leave the row in no bin at all. Widening the edge to reach it ## changes nothing else: where the span is not a whole number of widths the ## edge is already past it and the maximum wins nothing. edgesP(end) = max (edgesP(end), hi); [idx, labs] = gb_assign_intervals (proxy, edgesP, incEdge, ... gb_edge_labels (edgesP, ctype, ... gb_dur_fmt (col, ctype))); endfunction ## ---- Calendar-period time-unit binning (datetime) ---- function [idx, labs] = gb_period_bins (col, unit, incEdge) DV = datevec (col(:)); n = size (DV, 1); nat = any (isnan (DV), 2); Y = DV(:,1); Mo = DV(:,2); D = DV(:,3); h = DV(:,4); mi = DV(:,5); s = DV(:,6); dn = gb_dt2dn (col)(:); ## Per-row integer period key and grid step (in key units). right = strcmpi (incEdge, 'right'); switch (unit) case 'year' key = Y; step = 1; case 'month' key = Y .* 12 + (Mo - 1); step = 1; case 'quarter' key = Y .* 4 + floor ((Mo - 1) / 3); step = 1; case 'decade' key = floor (Y / 10); step = 1; case 'century' key = floor (Y / 100); step = 1; case 'day' key = floor (dn); step = 1; case 'week' ## Sunday-anchored key = floor (dn) - (weekday (floor (dn)) - 1); step = 7; case 'hour' key = floor (dn .* 24); step = 1; case 'minute' key = floor (dn .* 1440); step = 1; case 'second' key = round (dn .* 86400); step = 1; endswitch ## 'right' edge: a value exactly on a period boundary joins the previous bin. if (right) onEdge = gb_on_boundary (unit, Y, Mo, D, h, mi, s, dn); key(onEdge & ! nat) = key(onEdge & ! nat) - step; endif key(nat) = NaN; good = key(! isnan (key)); if (isempty (good)) idx = NaN (n, 1); labs = {gb_period_label(unit, 0)}; return; endif kmin = min (good); kmax = max (good); gridKeys = kmin:step:kmax; nb = numel (gridKeys); labs = cell (1, nb); for b = 1:nb labs{b} = gb_period_label (unit, gridKeys(b)); endfor idx = NaN (n, 1); ok = ! isnan (key); idx(ok) = (key(ok) - kmin) / step + 1; idx(idx < 1 | idx > nb) = NaN; endfunction ## Whether each row sits exactly on the start boundary of its period (used for ## 'IncludedEdge','right'). function tf = gb_on_boundary (unit, Y, Mo, D, h, mi, s, dn) switch (unit) case {'year', 'decade', 'century'} tf = (Mo == 1 & D == 1 & h == 0 & mi == 0 & s == 0); case {'month', 'quarter'} tf = (D == 1 & h == 0 & mi == 0 & s == 0); case {'day', 'week'} tf = (h == 0 & mi == 0 & s == 0); case 'hour' tf = (mi == 0 & s == 0); case 'minute' tf = (s == 0); otherwise tf = (s == round (s)); endswitch endfunction ## Category label for period key K of the given time UNIT. function lab = gb_period_label (unit, k) mon = {'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov', ... 'Dec'}; switch (unit) case 'year' lab = sprintf ("%d", k); case 'month' lab = sprintf ("%s-%04d", mon{mod(k,12)+1}, floor (k / 12)); case 'quarter' lab = sprintf ("Q%d %d", mod (k, 4) + 1, floor (k / 4)); case 'decade' lab = sprintf ("[%d, %d)", k * 10, k * 10 + 10); case 'century' lab = sprintf ("[%d, %d)", k * 100, k * 100 + 100); case 'day' lab = gb_date_str (k); case 'week' lab = sprintf ("[%s, %s)", gb_date_str (k), gb_date_str (k + 7)); case 'hour' lab = gb_datetime_str (k / 24); case 'minute' lab = gb_datetime_str (k / 1440); case 'second' lab = gb_datetime_str (k / 86400); endswitch endfunction ## 'dd-mmm-yyyy' for an integer datenum. function s = gb_date_str (dn) mon = {'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov', ... 'Dec'}; DV = datevec (dn); s = sprintf ("%02d-%s-%04d", DV(3), mon{DV(2)}, DV(1)); endfunction ## 'dd-mmm-yyyy HH:MM:SS' for a datenum. function s = gb_datetime_str (dn) mon = {'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov', ... 'Dec'}; DV = datevec (dn); s = sprintf ("%02d-%s-%04d %02d:%02d:%02d", DV(3), mon{DV(2)}, DV(1), ... DV(4), DV(5), round (DV(6))); endfunction ## ---- Shared interval assignment + labels ---- function [idx, labs] = gb_assign_intervals (proxy, edgesP, incEdge, estr) nb = numel (edgesP) - 1; left = ! strcmpi (incEdge, 'right'); idx = NaN (numel (proxy), 1); for k = 1:nb if (left) if (k < nb) in = proxy >= edgesP(k) & proxy < edgesP(k+1); else in = proxy >= edgesP(k) & proxy <= edgesP(k+1); endif else if (k == 1) in = proxy >= edgesP(k) & proxy <= edgesP(k+1); else in = proxy > edgesP(k) & proxy <= edgesP(k+1); endif endif idx(in) = k; endfor labs = cell (1, nb); for k = 1:nb if (left) br = '['; bl = ')'; if (k == nb) bl = ']'; endif else br = '('; bl = ']'; if (k == 1) br = '['; endif endif labs{k} = sprintf ("%s%s, %s%s", br, estr{k}, estr{k+1}, bl); endfor endfunction ## Format proxy bin edges as label strings of type CTYPE. DFMT is the duration ## column's display format (e.g. 'm'), so duration edges label in the same unit. ## The edges are labelled all at once rather than one at a time: a narrow bin ## over a wide span makes tens of thousands of them, and formatting each on ## its own -- building a one-element object, assigning its format through ## 'subsasgn' and rendering it -- costs seconds where the whole set costs a ## fraction of one. The strings are the same either way. function s = gb_edge_labels (edgesP, ctype, dfmt) n = numel (edgesP); s = cell (1, n); if (n == 0) return; endif switch (ctype) case 'datetime' ## Use a uniform format across the edges: show the time component on every ## edge (including midnight) when any edge carries one, else date only. DVe = datevec (edgesP(:)); hasTime = any (DVe(:,4) != 0 | DVe(:,5) != 0 | round (DVe(:,6)) != 0); mon = {'Jan','Feb','Mar','Apr','May','Jun', ... 'Jul','Aug','Sep','Oct','Nov','Dec'}; dash = repmat ('-', n, 1); txt = [num2str(DVe(:,3), '%02d'), dash, char(mon(DVe(:,2))), dash, ... num2str(DVe(:,1), '%04d')]; if (hasTime) colon = repmat (':', n, 1); txt = [txt, repmat(' ', n, 1), num2str(DVe(:,4), '%02d'), colon, ... num2str(DVe(:,5), '%02d'), colon, ... num2str(round (DVe(:,6)), '%02d')]; endif s = cellstr (txt)'; case 'duration' ed = days (edgesP(:)); if (! isempty (dfmt)) ed.Format = dfmt; endif s = dispstrings (ed)(:)'; otherwise for i = 1:n s{i} = gb_num_str (edgesP(i)); endfor endswitch endfunction ## A numeric edge as its label. Six significant digits, so a large edge ## reads as an exponent rather than as its digits: 3e+06, not 3000000. function s = gb_num_str (v) s = sprintf ('%g', v); endfunction ## The duration column's display format (for duration edge labels), else ''. function f = gb_dur_fmt (col, ctype) f = ''; if (strcmp (ctype, 'duration')) f = col.Format; endif endfunction ## datetime -> datenum, NaT -> NaN. function dn = gb_dt2dn (v) sz = size (v); DV = datevec (v); nat = any (isnan (DV), 2); DV(nat,:) = 0; DV(nat,2:3) = 1; dn = datenum (DV); dn(nat) = NaN; dn = reshape (dn, sz); endfunction pr0m1th3as-datatypes-9450b29/inst/private/__grp2idx__.m000066400000000000000000000050551527370255000226630ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 ## . ## -*- texinfo -*- ## @deftypefn {private} {[@var{g}, @var{gn}, @var{gl}] =} __grp2idx__ (@var{s}) ## ## Get index for group variables. ## ## @end deftypefn function [g, gn, gl] = __grp2idx__ (s) if (nargin != 1) print_usage (); endif s_was_char = false; if (ischar (s)) s_was_char = true; s = cellstr (s); elseif (! isvector (s)) error (strcat ("grp2idx: S must be a vector, cell array of", ... " strings, or char matrix.")); endif [gl, I, g] = unique (s(:)); ## Fix order in here, since unique does not support this yet if (iscellstr (s)) I = sort (I); for i = 1:length (gl) gl_s(i) = gl(g(I(i))); idx(i,:) = (g == g(I(i))); endfor for i = 1:length (gl) g(idx(i,:)) = i; endfor gl = gl_s; gl = gl'; else I = sort (I); for i = 1:length (gl) gl_s(i) = gl(g(I(i))); idx(i,:) = (g == g(I(i))); endfor for i = 1:length (gl) g(idx(i,:)) = i; endfor gl = gl_s; gl = gl'; endif ## Handle NaNs and empty strings if (iscellstr (s)) empties = cellfun (@isempty, s); if (any (empties)) g(empties) = NaN; while (min (g) > 1) g--; endwhile endif empties = cellfun (@isempty, gl); if (any (empties)) gl(empties) = []; endif else ## This works fine because NaN come at the end after sorting, we don't ## have to worry about change on the indices. g(isnan (s)) = NaN; gl(isnan (gl)) = []; endif if (nargout > 1) if (iscellstr (gl)) gn = gl; elseif (iscell (gl)) gn = cellfun (@num2str, gl, "UniformOutput", false); else gn = arrayfun (@num2str, gl, "UniformOutput", false); endif endif if (nargout > 2 && s_was_char) gl = char (gl); endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__intstr__.m000066400000000000000000000027711527370255000226310ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## The exact decimal text of an integer-type scalar V. A 64-bit value ## beyond flintmax is divided by ten in two 32-bit halves held as doubles, ## where every step is exact. function s = __intstr__ (v) if (abs (double (v)) < flintmax ()) s = sprintf ("%d", double (v)); return; endif neg = v < 0; if (neg) m = uint64 (-(v + ones (class (v)))) + uint64 (1); else m = uint64 (v); endif hi = double (bitshift (m, -32)); lo = double (bitand (m, uint64 (4294967295))); s = ''; while (hi > 0 || lo > 0) qh = floor (hi / 10); t = (hi - 10 * qh) * 4294967296 + lo; ql = floor (t / 10); s = [char(48 + t - 10 * ql), s]; hi = qh; lo = ql; endwhile if (neg) s = ['-', s]; endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__ismember__.m000066400000000000000000000112001527370255000230740ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {private} {@var{TF} =} __ismember__ (@var{a}, @var{s}) ## @deftypefnx {private} {@var{TF} =} __ismember__ (@var{a}, @var{s}, @qcode{'rows'}) ## @deftypefnx {private} {[@var{TF}, @var{index}] =} __ismember__ (@dots{}) ## @deftypefnx {private} {[@var{TF}, @var{index}] =} __ismember__ (@dots{}, @qcode{'legacy'}) ## ## Find set memders of data. ## ## @end deftypefn function [TF, index] = __ismember__ (a, s, varargin) if (nargin < 2 || nargin > 4) print_usage (); endif by_rows = any (strcmpi ('rows', varargin)); optlegacy = any (strcmpi ('legacy', varargin)); ## lookup() uses absolute values for complex input so we handle the ## real and imaginary parts separately (bug #52437) unless 'rows', which ## does not use lookup() so it can be handled normally. if (! by_rows && (iscomplex (a) || iscomplex (s))) real_argout = cell (nargout, 1); imag_argout = cell (nargout, 1); [real_argout{:}] = ismember (real (a), real (s), varargin{:}); [imag_argout{:}] = ismember (imag (a), imag (s), varargin{:}); TF = real_argout{1} & imag_argout{1}; if (nargout > 1) index = zeros (size (real_argout{2})); if (optlegacy) index(TF) = min (real_argout{2}(TF), imag_argout{2}(TF)); else index(TF) = max (real_argout{2}(TF), imag_argout{2}(TF)); endif endif return; endif ## lookup() does not handle logical values if (islogical (a)) a = uint8 (a); endif if (islogical (s)) s = uint8 (s); endif ## Matlab-compatible behavior (R2016b). See bug #51187. if (ischar (a) && rows (a) == 1 && iscell (s)) a = {a}; endif ## Another Matlab-compatible behavior. See bug #53924. if (isnumeric (a) && ischar (s)) s = double (s); elseif (ischar (a) && isnumeric (s)) a = double (a); endif if (! isempty (varargin)) if (! cellfun ('ischar', varargin)); error ("ismember: all options must be strings."); elseif (! all (strcmpi (varargin, 'rows') | strcmpi (varargin, 'legacy'))) error ('ismember: only "rows" and "legacy" are valid options.'); endif endif if (! by_rows) s = s(:); ## Check sort status, because we expect the array will often be sorted. if (issorted (s)) is = []; else [s, is] = sort (s); endif ## Remove NaNs from table because lookup can't handle them if (isreal (s) && ! isempty (s) && isnan (s(end))) s = s(1:(end - sum (isnan (s)))); endif if (nargout > 1) if (!optlegacy) s = s(end : -1: 1); if (! (isempty (s) || isempty (a))) if (isempty (is)) is = [numel(s) : -1 : 1]; else is = is(1 : numel (s))(end : -1 : 1); endif endif endif index = lookup (s, a, "m"); TF = logical (index); if (! isempty (is)) index(TF) = is(index(TF)); endif else TF = lookup (s, a, "b"); endif else # "rows" argument if (isempty (a) || isempty (s)) TF = false (rows (a), 1); if (nargout > 1) index = zeros (rows (a), 1); endif else if (rows (s) == 1 && ! iscell (a) && ! iscell (s)) TF = all (a == s, 2); if (nargout > 1) index = double (TF); endif else ## FIXME: lookup does not support "rows", so we just use unique. na = rows (a); if (optlegacy) [~, ii, jj] = __unique__ ([a; s], 'rows', 'last'); jj = ii(jj(1:na)); TF = jj > na; if (nargout > 1) index = max (0, jj - na); endif else [~, ii, jj] = __unique__ ([s; a], 'rows', 'first'); nj = numel (jj) - (na - 1); jj = ii(jj(nj:end)); TF = jj < nj; if (nargout > 1) index = jj; index (jj > rows (s)) = 0; endif endif endif endif endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__ismissing__.m000066400000000000000000000052641527370255000233130ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {private} {@var{TF} =} __ismissing__ (@var{A}) ## @deftypefnx {private} {@var{TF} =} __ismissing__ (@var{A}, @var{indicator}) ## ## Find missing data in a numeric or string array. ## ## @end deftypefn function TF = __ismissing__ (A, indicator) ## Check "indicator" if (nargin != 2) indicator = []; endif ## If A is an array of cell strings and indicator just a string, ## convert indicator to a cell string with one element if (iscellstr (A) && ischar (indicator) && ! iscellstr (indicator)) indicator = {indicator}; endif if ((! isempty (indicator)) && ((isnumeric (A) && ! (isnumeric (indicator) || islogical (indicator))) || (ischar (A) && ! ischar (indicator)) || (iscellstr (A) && ! iscellstr (indicator)))) error ("ismissing: 'indicator' and 'A' must have the same data type."); endif ## Main logic if (isempty (indicator)) if (isnumeric (A)) ## numeric matrix: just find the NaNs ## integer types have no missing value, but isnan will return false TF = isnan (A); elseif (iscellstr (A)) ## cell strings - find empty cells TF = cellfun ('isempty', A); elseif (ischar (A)) ## char matrix: find the white spaces TF = isspace (A); else ## no missing type defined, return false TF = false (size (A)); endif else ## Indicator specified for missing data TF = false (size (A)); if (isnumeric (A) || islogical (A)) for iter = 1:numel (indicator) if (isnan (indicator(iter))) TF(isnan(A)) = true; else TF(A == indicator(iter)) = true; endif endfor elseif (ischar (A)) for iter = 1:numel (indicator) TF(A == indicator(iter)) = true; endfor elseif (iscellstr (A)) for iter = 1:numel (indicator) TF(strcmp (A, indicator(iter))) = true; endfor endif endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__iso2dt__.m000066400000000000000000000047751527370255000225200ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {[@var{dt}, @var{ok}] =} __iso2dt__ (@var{C}, @var{tz}) ## ## Parse a cell block of ISO 8601 date-time strings into a @code{datetime}. ## ## An empty cell becomes @code{NaT}. A non-empty @var{tz} restores the ## @qcode{TimeZone}; the strings are the wall-clock time in that zone. @var{ok} ## is false when a non-empty entry is not an ISO 8601 date-time, in which case ## @var{dt} is empty and the caller falls back to its own parsing. ## ## @end deftypefn function [dt, ok] = __iso2dt__ (C, tz = '') ## Text carrying a UTC offset names an instant rather than a wall clock, and ## the scan below would silently drop the offset, so it goes to the ## constructor instead, which reads it into TZ and refuses it without one. isOff = @(x) ischar (x) && ! isempty (regexp (x, ['T\d{1,2}:\d{2}', ... '(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}(:?\d{2})?)\s*$'], 'once')); if (any (cellfun (isOff, C(:)))) if (isempty (tz)) dt = datetime (C); else dt = datetime (C, 'TimeZone', tz); endif ok = true; return; endif sz = size (C); Y = nan (sz); Mo = nan (sz); D = nan (sz); h = nan (sz); mi = nan (sz); s = nan (sz); ok = true; for i = 1:numel (C) str = C{i}; if (! ischar (str)) str = ''; endif if (! isempty (str)) val = sscanf (str, "%d-%d-%dT%d:%d:%f"); if (numel (val) != 6) dt = []; ok = false; return; endif Y(i) = val(1); Mo(i) = val(2); D(i) = val(3); h(i) = val(4); mi(i) = val(5); s(i) = val(6); endif endfor if (isempty (tz)) dt = datetime (Y, Mo, D, h, mi, s); # NaN components yield NaT else dt = datetime (Y, Mo, D, h, mi, s, 'TimeZone', tz); endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__iso2dur__.m000066400000000000000000000037771527370255000227040ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {[@var{du}, @var{ok}] =} __iso2dur__ (@var{C}) ## ## Parse a cell block of ISO 8601 duration strings into a @code{duration}. ## ## The strings are of the form @qcode{PTnHnMnS}, optionally signed, with hours ## unwrapped so that a duration of any magnitude is read back exactly. An empty ## cell becomes a missing duration. @var{ok} is false when a non-empty entry is ## not such a string, in which case @var{du} is empty and the caller falls back ## to its own parsing. ## ## @end deftypefn function [du, ok] = __iso2dur__ (C) tot = nan (size (C)); ok = true; for i = 1:numel (C) str = C{i}; if (! ischar (str)) str = ''; endif if (! isempty (str)) neg = (str(1) == '-'); if (neg) str(1) = []; endif tk = regexp (str, '^PT([\d.]+)H([\d.]+)M([\d.]+)S$', 'tokens'); if (isempty (tk)) du = []; ok = false; return; endif H = str2double (tk{1}{1}); M = str2double (tk{1}{2}); S = str2double (tk{1}{3}); val = H * 3600 + M * 60 + S; if (neg) val = -val; endif tot(i) = val; endif endfor du = seconds (tot); # NaN yields a missing duration endfunction pr0m1th3as-datatypes-9450b29/inst/private/__makima__.m000066400000000000000000000175621527370255000225510ustar00rootroot00000000000000## Copyright (C) 2025-2026 Avanish Salunke ## ## This file is part of the datatypes package for GNU Octave. ## ## Octave 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. ## ## Octave 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 Octave; see the file COPYING. If not, ## see . ## Copied from the statistics package's 'makima' so that this package's ## interpolating methods can offer it: core Octave's 'interp1' has no ## 'makima'. It goes when the function reaches core, and nothing here is ## meant to diverge from the original in the meantime. ## -*- texinfo -*- ## @deftypefn {private} {@var{yi} =} __makima__ (@var{x}, @var{y}, @var{xq}) ## @deftypefnx {private} {@var{yi} =} __makima__ (@var{y}, @var{xq}) ## @deftypefnx {private} {@var{yi} =} __makima__ (@dots{}, @qcode{'extrap'}) ## ## Compute the 1-D Modified Akima piecewise cubic Hermite interpolant of ## sample data @var{x} and @var{y}. ## ## The Modified Akima (MAKIMA) algorithm generates a shape-preserving ## piecewise cubic interpolant. It differs from standard splines by avoiding ## excessive local undulations and overshoots, and it connects collinear points ## (flat regions) with straight lines. It is particularly well-suited for ## oscillatory data where @code{pchip} might aggressively flatten local ## extrema. ## ## The sample points @var{x} must be a vector of unique values. If @var{x} ## is not sorted, the function will automatically sort it and rearrange ## @var{y} accordingly. ## ## The sample values @var{y} can be a scalar, vector, or an N-dimensional ## array. If @var{y} is an N-dimensional array, the interpolation is ## performed along its last dimension, which must have the same length as ## @var{x}. Complex values for @var{y} are supported. ## ## If query points @var{xq} are provided, the function evaluates the ## interpolant and returns the interpolated values @var{yi}. By default, ## @code{__makima__} uses the boundary polynomials to extrapolate for points ## outside the range of @var{x}. The optional string argument @qcode{'extrap'} ## is accepted for compatibility with other interpolation functions. ## ## If only @var{x} and @var{y} are provided, the function returns a ## piecewise polynomial structure @var{pp} that represents the interpolant. ## This structure can be evaluated later at specific query points using ## @code{ppval}. ## ## Evaluating the interpolant at query points outside the domain of @var{x} ## automatically extrapolates using the boundary polynomials. ## ## @seealso{interp1, pchip, spline} ## ## This is an internal helper; do NOT call it directly. ## ## @end deftypefn function yi = __makima__ (x, y, xq, varargin) if (nargin < 2 || nargin > 4) error ("__makima__: invalid number of inputs"); endif if (nargin == 4 && ! strcmpi (varargin{1}, 'extrap')) error ("__makima__: unknown option '%s'", varargin{1}); endif return_pp = (nargin == 2); if (! return_pp) size_xq = size (xq); endif x = x(:); n = numel (x); is_y_vector = isvector (y); size_y = size (y); if (is_y_vector) if (numel (y) != n) error (strcat ("__makima__: the number of sample points X, %d, is", ... " incompatible with the number of values Y, %d."), ... n, numel (y)); endif y = y(:); dim_y = 1; nc = 1; else dim_y = ndims (y); if (size_y(dim_y) != n) error (strcat ("__makima__: the number of sample points X, %d, is", ... " incompatible with the number of values Y, %d."), ... n, size_y(dim_y)); endif ## permute the interpolation dimension to be the first perm_order = 1:numel (size_y); perm_order(dim_y) = 1; perm_order(1) = dim_y; y = permute (y, perm_order); nc = numel (y) / n; y = reshape (y, n, nc); endif if (iscomplex (y)) if (return_pp) ## Build complex pp struct pp_real = __makima__ (x, real (y)); pp_imag = __makima__ (x, imag (y)); yi = pp_real; yi.coefs = pp_real.coefs + 1i * pp_imag.coefs; else yi = __makima__ (x, real (y), xq, varargin{:}) ... + 1i * __makima__ (x, imag (y), xq, varargin{:}); endif return; endif if (! return_pp) xqv = xq(:); nq = numel (xqv); endif if (n < 2) error ("__makima__: the first two inputs must have at least two elements."); endif if (! issorted (x)) [x, sort_idx] = sort (x); y = y(sort_idx, :); endif math_done = false; if (n == 2) if (return_pp) ## Linear coefficients for 2-point pp struct coefs = zeros (nc, 4, class (y)); coefs(:, 3) = (y(2, :).' - y(1, :).') ./ (x(2) - x(1)); coefs(:, 4) = y(1, :).'; if (is_y_vector) dim_out = 1; else dim_out = size_y; dim_out(dim_y) = []; endif yi = mkpp (x.', coefs, dim_out); return; else yi = interp1 (x, y, xqv, 'linear', 'extrap'); yi = reshape (yi, [nq, nc]); math_done = true; endif endif if (! math_done) dx = diff (x); if (any (dx <= 0)) error ("__makima__: the sample points x must be unique."); endif dy = diff (y); m = dy ./ dx; m_0 = 2 * m(1, :) - m(2, :); m_m1 = 2 * m_0 - m(1, :); m_n = 2 * m(end, :) - m(end-1, :); m_n1 = 2 * m_n - m(end, :); m_ext = [m_m1; m_0; m; m_n; m_n1]; d = zeros (n, nc, class (y)); k_idx = (1 : n)'; s_im2 = m_ext(k_idx , :); s_im1 = m_ext(k_idx + 1, :); s_i = m_ext(k_idx + 2, :); s_ip1 = m_ext(k_idx + 3, :); w1 = abs (s_ip1 - s_i) + abs (s_ip1 + s_i) / 2; w2 = abs (s_im1 - s_im2) + abs (s_im1 + s_im2) / 2; W = w1 + w2; numer = (w1 .* s_im1 + w2 .* s_i); denom = max (W, eps); d = numer ./ denom; zero_mask = (W == 0); if (any (zero_mask(:))) fallback = (s_im1 + s_i) / 2; d(zero_mask) = fallback(zero_mask); endif if (return_pp) hseg = dx; delta = m; d0 = d(1:end-1, :); d1 = d(2:end, :); y0 = y(1:end-1, :); c3 = (d0 + d1 - 2*delta) ./ (hseg .* hseg); c2 = (3*delta - 2*d0 - d1) ./ hseg; c1 = d0; c0 = y0; c3_t = c3.'; c2_t = c2.'; c1_t = c1.'; c0_t = c0.'; coefs = [c3_t(:), c2_t(:), c1_t(:), c0_t(:)]; if (is_y_vector) dim_out = 1; else dim_out = size_y; dim_out(dim_y) = []; if (isempty (dim_out)) dim_out = 1; endif endif yi = mkpp (x.', coefs, dim_out); return; endif yi = NaN (nq, nc, class (y)); if (nq > 0) idx = lookup (x, xqv); idx(idx >= n) = n - 1; idx(idx == 0) = 1; x_left = x(idx); hseg = dx(idx); s = xqv - x_left; for c = 1:nc y0 = y(idx, c); y1 = y(idx + 1, c); d0 = d(idx, c); d1 = d(idx + 1, c); delta = (y1 - y0) ./ hseg; c2 = (3*delta - 2*d0 - d1) ./ hseg; c3 = (d0 + d1 - 2*delta) ./ (hseg.^2); yi(:, c) = y0 + s .* (d0 + s .* (c2 + s .* c3)); endfor endif endif if (! return_pp) if (is_y_vector) yi = reshape (yi, size_xq); else out_shape = size_y; out_shape(dim_y) = nq; yi = reshape (yi, out_shape(perm_order)); yi = ipermute (yi, perm_order); ## only append size_xq if multi-dimensional array is given if (! isvector (xq)) final_shape = size_y; final_shape(dim_y) = []; final_shape = [final_shape, size_xq]; yi = reshape (yi, final_shape); endif endif endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__mergesheet__.m000066400000000000000000000067551527370255000234440ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{s} =} __mergesheet__ (@var{s}, @var{obj}, @var{sheet}, @var{writeMode}) ## ## Merge one tabular object into the struct of sheets a workbook was read into. ## ## @var{s} is the struct @code{ods2struct} returned, @var{obj} the object being ## written, @var{sheet} the sheet name it is to occupy, and @var{writeMode} ## either @qcode{'append'} or a replacing mode. It is a private helper of ## @code{table.table2ods} and @code{timetable.timetable2ods}, which reach it ## when the file already exists and its other sheets have to survive. ## ## It lives here rather than on @code{tabular} because a dot access to ## @qcode{Properties} inside a method does not go through @code{subsref}, which ## is what synthesises that property. ## ## @end deftypefn function s = __mergesheet__ (s, T, sheet, writeMode) ## Find the field whose sheet name matches, which is its ## 'ActualSheetName' where it has one and its field name otherwise. fields = fieldnames (s); targetField = ''; for i = 1:numel (fields) fsheet = fields{i}; cp = s.(fields{i}).Properties.CustomProperties; if (isfield (cp, 'ActualSheetName') && ! isempty (cp.ActualSheetName)) fsheet = cp.ActualSheetName; endif if (strcmp (fsheet, sheet)) targetField = fields{i}; break; endif endfor ## Select mode if (strcmp (writeMode, 'append') && ! isempty (targetField)) ## Append the rows; vertcat errors if the variables do not agree. combined = [s.(targetField); T]; s.(targetField) = copy_actual_sheet_name (combined, ... s.(targetField)); elseif (! isempty (targetField)) ## Replace the sheet, keeping its resolved name. s.(targetField) = copy_actual_sheet_name (T, s.(targetField)); else ## A new sheet: canonicalise SHEET to a unique field name and stash the ## original name when it had to change. fn = matlab.lang.makeValidName (sheet); base = fn; j = 1; while (isfield (s, fn)) fn = sprintf ("%s_%d", base, j); j += 1; endwhile if (! strcmp (fn, sheet)) T = addprop (T, 'ActualSheetName', 'table'); T.Properties.CustomProperties.ActualSheetName = sheet; endif s.(fn) = T; endif endfunction ## Copy the 'ActualSheetName' custom property from SRC onto T, where SRC ## carries one. function T = copy_actual_sheet_name (T, src) cp = src.Properties.CustomProperties; if (isfield (cp, 'ActualSheetName') && ! isempty (cp.ActualSheetName)) tcp = T.Properties.CustomProperties; if (! isfield (tcp, 'ActualSheetName')) T = addprop (T, 'ActualSheetName', 'table'); endif T.Properties.CustomProperties.ActualSheetName = cp.ActualSheetName; endif endfunctionpr0m1th3as-datatypes-9450b29/inst/private/__minmaxk__.m000066400000000000000000000130451527370255000227460ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {private} {[@var{I}, @var{lidx}] =} __minmaxk__ (@var{x}, @var{K}, @var{largest}, @var{args}) ## @deftypefnx {private} {[@var{I}, @var{lidx}, @var{errmsg}] =} __minmaxk__ (@dots{}) ## ## Rank the @var{K} largest or smallest elements of a numeric proxy array. ## ## This is the shared engine of the @code{maxk} and @code{mink} methods of the ## @code{datetime}, @code{duration}, and @code{categorical} classes. It works ## on a real numeric proxy @var{x} of the calling class, in which missing ## elements are represented by @qcode{NaN}, and it returns indices only, so the ## caller can gather whatever component arrays it stores. ## ## @var{largest} must be @code{true} for @code{maxk} and @code{false} for ## @code{mink}. @var{args} is the cell array of the caller's optional input ## arguments, which may contain the operating dimension. ## ## @var{I} contains the indices of the ranked elements along the operating ## dimension, which is the second output of @code{maxk} and @code{mink}, while ## @var{lidx} contains the equivalent linear indices into @var{x}, with which ## the caller can index its own arrays directly. Both have the size of @var{x} ## with the operating dimension reduced to @code{min (@var{K}, n)}, where ## @code{n} is the size of @var{x} along that dimension. ## ## Each slice along the operating dimension is ranked independently. The ## non-missing elements are ordered by value, with ties keeping their original ## relative order, and the missing ones are appended after them in their ## original order. Hence, missing elements are never ranked and they only ## appear in the output when @var{K} exceeds the number of non-missing elements ## in that slice. This differs from @code{sort}, which orders missing elements ## according to its @qcode{'MissingPlacement'} option. ## ## Input validation is done here so that it cannot drift between the calling ## classes, but no error is raised. Instead, @var{errmsg} returns the message ## body, which is empty on success, and the caller is expected to raise the ## error under its own class and method name. ## ## @end deftypefn function [I, lidx, errmsg] = __minmaxk__ (x, K, largest, args) I = []; lidx = []; errmsg = ''; ## Check K if (! (isnumeric (K) && isscalar (K) && isreal (K) && isfinite (K) && fix (K) == K && K >= 0)) errmsg = "K must be a nonnegative integer scalar."; return; endif ## Check optional arguments. Named options are reported by name, since ## otherwise 'MissingPlacement' and 'ComparisonMethod', which both belong to ## 'sort' but not here, would be reported as an invalid dimension. if (numel (args) > 0) [args{:}] = convertStringsToChars (args{:}); cid = cellfun (@ischar, args); if (any (cid)) errmsg = sprintf ("'%s' is not a supported option.", args{find (cid, 1)}); return; endif endif if (numel (args) > 1) errmsg = "too many input arguments."; return; endif ## Get operating dimension if (numel (args) == 1) dim = args{1}; if (! (isnumeric (dim) && isscalar (dim) && isreal (dim) && isfinite (dim) && fix (dim) == dim && dim > 0)) errmsg = "DIM must be a positive integer scalar."; return; endif else dim = find (size (x) != 1, 1); if (isempty (dim)) # scalar dim = 1; endif endif ## Elements are only ever taken from those available along the operating ## dimension, so K is clamped rather than padded. A dimension beyond the ## dimensions of x is singleton, which leaves x untouched. nd = max (ndims (x), dim); sz = size (x); sz(end+1:nd) = 1; kk = min (K, sz(dim)); ## Bring the operating dimension to the front and flatten everything after ## it, so that the loop below holds for an array of any number of dimensions. ## The result is put back the way it came at the end. perm = [dim, 1:dim-1, dim+1:nd]; psz = sz(perm); X = reshape (permute (x, perm), psz(1), []); nc = columns (X); ## Rank each slice: the non-missing elements by value, ties broken by original ## position, followed by the missing elements in their original order. idx = zeros (kk, nc); for j = 1:nc col = X(:,j); mis = isnan (col); fin = find (! mis); if (largest) [~, ord] = sortrows ([-col(fin), fin]); else [~, ord] = sortrows ([col(fin), fin]); endif ranked = [fin(ord); find(mis)]; idx(:,j) = ranked(1:kk); endfor ## Restore the original shape and dimension order. The linear indices are ## mapped through the same permutation, so that they index x as it was given. osz = psz; osz(1) = kk; I = ipermute (reshape (idx, osz), perm); map = reshape (permute (reshape (1:prod (sz), sz), perm), psz(1), []); lin = idx + repmat ((0:nc-1) * psz(1), kk, 1); lidx = ipermute (reshape (map(lin), osz), perm); endfunction pr0m1th3as-datatypes-9450b29/inst/private/__namesrow__.m000066400000000000000000000035751527370255000231440ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{row} =} __namesrow__ (@var{row}, @var{line}, @var{caller}) ## ## Resolve the two spellings of the variable-names row into one value. ## ## @qcode{'VariableNamesRow'} is the spelling the package uses throughout, and ## @qcode{'VariableNamesLine'} is MATLAB's for a text file; they mean the same ## thing, so giving both is an error rather than a precedence puzzle. An empty ## @var{row} and @var{line} mean neither was given and the default applies. ## @var{caller} names the function for the error message. ## ## @end deftypefn function row = __namesrow__ (row, line, caller) name = 'VariableNamesRow'; if (! isempty (row) && ! isempty (line)) error (strcat ("%s: 'VariableNamesRow' and 'VariableNamesLine' name", ... " the same thing; pass one of them."), caller); elseif (! isempty (line)) row = line; name = 'VariableNamesLine'; elseif (isempty (row)) row = 1; endif if (! (isnumeric (row) && isscalar (row) && row == fix (row) && row >= 0)) error ("%s: '%s' must be a non-negative integer.", caller, name); endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__nanmax__.m000066400000000000000000000115201527370255000225600ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {private} {@var{v} =} __nanmax__ (@var{x}) ## @deftypefnx {private} {@var{v} =} __nanmax__ (@var{x}, [], @var{dim}) ## @deftypefnx {private} {[@var{v}, @var{idx}] =} __nanmax__ (@dots{}) ## @deftypefnx {private} {@var{v} =} __nanmax__ (@var{x}, [], @qcode{'all'}) ## @deftypefnx {private} {@var{v} =} __nanmax__ (@var{x}, [], @var{vecdim}) ## @deftypefnx {private} {@var{v} =} __nanmax__ (@var{x}, [], @dots{}, @var{includenan}) ## @deftypefnx {private} {@var{v} =} __nanmax__ (@var{x}, @var{y}) ## @deftypefnx {private} {@var{v} =} __nanmax__ (@var{x}, @var{y}, @var{includenan}) ## ## Find the maximum while ignoring NaN values. ## ## @end deftypefn function [v, idx] = __nanmax__ (x, varargin) if (nargin < 1 || nargin > 4) print_usage; endif ## Get optional arguments nargs = numel (varargin); if (nargs == 0) y = []; dim = []; include = false; elseif (nargs == 1) y = varargin{1}; dim = []; include = false; elseif (nargs == 2) y = varargin{1}; if (isempty (y)) dim = varargin{2}; include = false; else dim = []; include = varargin{2}; endif elseif (nargs == 3) y = varargin{1}; if (isempty (y)) dim = varargin{2}; include = varargin{3}; else dim = []; include = varargin{2}; endif endif ## Process according to given input arguments if (isempty (y) && isempty (dim)) nanvals = isnan (x); x(nanvals) = -Inf; [v, idx] = max (x); if (include) v(any (nanvals)) = NaN; else v(all (nanvals)) = NaN; endif elseif (isempty (y) && strcmpi (dim, "all")) x = x(:); nanvals = isnan (x); x(nanvals) = -Inf; [v, idx] = max (x); if (include) v(any (nanvals)) = NaN; else v(all (nanvals)) = NaN; endif elseif (isempty (y)) if (isscalar (dim)) nanvals = isnan (x); x(nanvals) = -Inf; [v, idx] = max (x, [], dim); if (include) v(any (nanvals, dim)) = NaN; else v(all (nanvals, dim)) = NaN; endif else vecdim = sort (dim); if (! all (diff (vecdim))) error ("nanmax: VECDIM must contain non-repeating positive integers."); endif ## Ignore dimensions in VECDIM larger than actual array vecdim(find (vecdim > ndims (x))) = []; if (isempty (vecdim)) v = x; if (nargout > 1) idx = reshape ([1:numel(x)], size (x)); endif else ## Calculate permutation vector szx = size (x); remdims = 1:ndims (x); # All dimensions remdims(vecdim) = []; # Delete dimensions specified by vecdim nremd = numel (remdims); ## If all dimensions are given, it is equivalent to 'all' flag if (nremd == 0) x = x(:); nanvals = isnan (x); x(nanvals) = -Inf; [v, idx] = max (x); if (include) v(any (nanvals)) = NaN; else v(all (nanvals)) = NaN; endif else ## Permute to push vecdims to back perm = [remdims, vecdim]; x = permute (x, perm); ## Reshape to squash all vecdims in final dimension sznew = [szx(remdims), prod(szx(vecdim))]; x = reshape (x, sznew); ## Calculate nanmax on final dimension dim = nremd + 1; nanvals = isnan (x); x(nanvals) = -Inf; [v, idx] = max (x, [], dim); if (include) v(any (nanvals, dim)) = NaN; else v(all (nanvals, dim)) = NaN; endif ## Inverse permute back to correct dimensions v = ipermute (v, perm); idx = ipermute (idx, perm); endif endif endif else # y is not empty if (nargout > 1) error ("nanmax: a second output is not supported with this syntax."); endif Xnan = isnan (x); Ynan = isnan (y); x(Xnan) = -Inf; y(Ynan) = -Inf; v = max (x, y); if (include) v(Xnan | Ynan) = NaN; else v(Xnan & Ynan) = NaN; endif endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__nanmin__.m000066400000000000000000000115111527370255000225560ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {private} {@var{v} =} __nanmin__ (@var{x}) ## @deftypefnx {private} {@var{v} =} __nanmin__ (@var{x}, [], @var{dim}) ## @deftypefnx {private} {[@var{v}, @var{idx}] =} __nanmin__ (@dots{}) ## @deftypefnx {private} {@var{v} =} __nanmin__ (@var{x}, [], @qcode{'all'}) ## @deftypefnx {private} {@var{v} =} __nanmin__ (@var{x}, [], @var{vecdim}) ## @deftypefnx {private} {@var{v} =} __nanmin__ (@var{x}, [], @dots{}, @var{includenan}) ## @deftypefnx {private} {@var{v} =} __nanmin__ (@var{x}, @var{y}) ## @deftypefnx {private} {@var{v} =} __nanmin__ (@var{x}, @var{y}, @var{includenan}) ## ## Find the minimum while ignoring NaN values. ## ## @end deftypefn function [v, idx] = __nanmin__ (x, varargin) if (nargin < 1 || nargin > 4) print_usage; endif ## Get optional arguments nargs = numel (varargin); if (nargs == 0) y = []; dim = []; include = false; elseif (nargs == 1) y = varargin{1}; dim = []; include = false; elseif (nargs == 2) y = varargin{1}; if (isempty (y)) dim = varargin{2}; include = false; else dim = []; include = varargin{2}; endif elseif (nargs == 3) y = varargin{1}; if (isempty (y)) dim = varargin{2}; include = varargin{3}; else dim = []; include = varargin{2}; endif endif ## Process according to given input arguments if (isempty (y) && isempty (dim)) nanvals = isnan (x); x(nanvals) = Inf; [v, idx] = min (x); if (include) v(any (nanvals)) = NaN; else v(all (nanvals)) = NaN; endif elseif (isempty (y) && strcmpi (dim, "all")) x = x(:); nanvals = isnan (x); x(nanvals) = Inf; [v, idx] = min (x); if (include) v(any (nanvals)) = NaN; else v(all (nanvals)) = NaN; endif elseif (isempty (y)) if (isscalar (dim)) nanvals = isnan (x); x(nanvals) = Inf; [v, idx] = min (x, [], dim); if (include) v(any (nanvals, dim)) = NaN; else v(all (nanvals, dim)) = NaN; endif else vecdim = sort (dim); if (! all (diff (vecdim))) error ("nanmin: VECDIM must contain non-repeating positive integers."); endif ## Ignore dimensions in VECDIM larger than actual array vecdim(find (vecdim > ndims (x))) = []; if (isempty (vecdim)) v = x; if (nargout > 1) idx = reshape ([1:numel(x)], size (x)); endif else ## Calculate permutation vector szx = size (x); remdims = 1:ndims (x); # All dimensions remdims(vecdim) = []; # Delete dimensions specified by vecdim nremd = numel (remdims); ## If all dimensions are given, it is equivalent to 'all' flag if (nremd == 0) x = x(:); nanvals = isnan (x); x(nanvals) = Inf; [v, idx] = min (x); if (include) v(any (nanvals)) = NaN; else v(all (nanvals)) = NaN; endif else ## Permute to push vecdims to back perm = [remdims, vecdim]; x = permute (x, perm); ## Reshape to squash all vecdims in final dimension sznew = [szx(remdims), prod(szx(vecdim))]; x = reshape (x, sznew); ## Calculate nanmin on final dimension dim = nremd + 1; nanvals = isnan (x); x(nanvals) = Inf; [v, idx] = min (x, [], dim); if (include) v(any (nanvals, dim)) = NaN; else v(all (nanvals, dim)) = NaN; endif ## Inverse permute back to correct dimensions v = ipermute (v, perm); idx = ipermute (idx, perm); endif endif endif else # y is not empty if (nargout > 1) error ("nanmin: a second output is not supported with this syntax."); endif Xnan = isnan (x); Ynan = isnan (y); x(Xnan) = Inf; y(Ynan) = Inf; v = min (x, y); if (include) v(Xnan | Ynan) = NaN; else v(Xnan & Ynan) = NaN; endif endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__numcells__.m000066400000000000000000000030661527370255000231260ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{M} =} __numcells__ (@var{C}) ## ## Build a numeric matrix from the cells of a spreadsheet column. ## ## An empty cell is @code{NaN}. A text cell reading @qcode{Inf}, @qcode{-Inf} ## or @qcode{NaN}, in any capitals, is that number: an infinity is written to ## ODS and XLSX as text, since a spreadsheet application loads a non-finite ## number as 0. Any other text is @code{NaN}, as MATLAB reads text in a ## numeric column. ## ## @end deftypefn function M = __numcells__ (C) M = nan (size (C)); for i = 1:numel (C) x = C{i}; if (ischar (x)) if (strcmpi (x, 'Inf')) M(i) = Inf; elseif (strcmpi (x, '-Inf')) M(i) = -Inf; endif elseif (! isempty (x)) M(i) = double (x); endif endfor endfunction pr0m1th3as-datatypes-9450b29/inst/private/__odsattach__.m000066400000000000000000000050621527370255000232540ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{tt} =} __odsattach__ (@var{tt}, @var{file}, @var{x}, @var{names}, @var{caller}) ## ## Attach the event table one cross-reference names to the timetable it names. ## ## @var{x} is one element of the struct array @code{__odscrossrefs__} returns, ## @var{names} the workbook's data sheet names, and @var{caller} the name the ## complaint is raised under. The referenced sheet is read, rebuilt as an ## @code{eventtable} under the three designations the reference carries, and ## assigned to @var{tt}. ## ## The check is per attachment and not per sheet: one sheet may be referenced ## by several timetables, and a sheet valid for one of them can be invalid for ## another. ## ## @end deftypefn function tt = __odsattach__ (tt, file, x, names, caller) if (! any (strcmp (x.to, names))) error (strcat ("%s: the event table of sheet '%s' is said to be on", ... " sheet '%s', which the file does not have."), ... caller, x.from, x.to); endif [etbl, rtn] = ods2table (file, 'Sheet', x.to); if (isempty (rtn)) error (strcat ("%s: sheet '%s' is named as the event table of sheet", ... " '%s' but holds no row times."), caller, x.to, x.from); endif ev = table2timetable (etbl, 'RowTimes', rtn); evClass = class (ev.Properties.RowTimes); ttClass = class (tt.Properties.RowTimes); if (! strcmp (evClass, ttClass)) error (strcat ("%s: the event table on sheet '%s' has %s row times", ... " where sheet '%s' has %s ones."), caller, x.to, ... evClass, x.from, ttClass); endif [args, errmsg] = __odseventopts__ (x, ev.Properties.VariableNames); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif tt.Properties.Events = eventtable (ev, args{:}); endfunction pr0m1th3as-datatypes-9450b29/inst/private/__odscrossrefs__.m000066400000000000000000000074401527370255000240230ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{x} =} __odscrossrefs__ (@var{preamble}) ## @deftypefnx {datatypes} {[@var{x}, @var{errmsg}] =} __odscrossrefs__ (@var{preamble}, @var{names}) ## ## The event cross-references a workbook's metadata preamble carries. ## ## @var{preamble} is the block of rows above the first @qcode{## Sheet:} marker ## of the hidden metadata sheet. Each line reading ## @qcode{## Events crossref:} names, in the cells after it, the sheet holding ## a timetable, the sheet holding that timetable's event table, and the event ## table's three variable designations, any of which may be blank. ## ## @var{x} is a struct array with fields @qcode{from}, @qcode{to}, ## @qcode{labels}, @qcode{lengths} and @qcode{ends}, empty when the preamble ## carries no such line. ## ## Given the workbook's data sheet names @var{names}, @var{errmsg} is the body ## of a complaint for the caller to raise under its own name when a reference ## starts from a sheet the file does not have, or names as an event table a ## sheet that carries one of its own; it is empty otherwise. ## ## Each value goes in a cell of its own rather than into one string because a ## sheet name may contain spaces. A line whose keyword is not recognised is ## skipped, so that a file written by a later version is read rather than ## refused. ## ## @end deftypefn function [x, errmsg] = __odscrossrefs__ (preamble, names = {}) x = struct ('from', {}, 'to', {}, 'labels', {}, 'lengths', {}, 'ends', {}); errmsg = ''; if (isempty (preamble) || columns (preamble) < 3) return; endif key = '## Events crossref:'; for r = 1:rows (preamble) if (! (ischar (preamble{r,1}) && strcmp (strtrim (preamble{r,1}), key))) continue; endif a = cell_text (preamble, r, 2); b = cell_text (preamble, r, 3); if (isempty (a) || isempty (b)) continue; endif x(end+1) = struct ('from', a, 'to', b, ... 'labels', cell_text (preamble, r, 4), ... 'lengths', cell_text (preamble, r, 5), ... 'ends', cell_text (preamble, r, 6)); endfor if (nargin < 2) return; endif for i = 1:numel (x) if (! any (strcmp (x(i).from, names))) errmsg = sprintf (strcat ("sheet '%s' is said to have its events on", ... " sheet '%s', but the file has no sheet", ... " '%s'."), x(i).from, x(i).to, x(i).from); return; endif if (any (strcmp (x(i).to, {x.from}))) errmsg = sprintf (strcat ("sheet '%s' is named as an event table and", ... " carries one of its own; an event table", ... " cannot carry an event table."), x(i).to); return; endif endfor endfunction ## One cell of the preamble as a character vector, empty when the column is ## not there at all or holds anything but text. function s = cell_text (preamble, r, c) s = ''; if (columns (preamble) >= c && ischar (preamble{r,c})) s = preamble{r,c}; endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__odseventopts__.m000066400000000000000000000041371527370255000240410ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {[@var{args}, @var{errmsg}] =} __odseventopts__ (@var{x}, @var{varNames}) ## ## The @code{eventtable} constructor options one cross-reference carries. ## ## @var{x} is one element of the struct array @code{__odscrossrefs__} returns. ## @var{args} is a cell of Name-Value pairs naming whichever of the three ## variable designations the reference carries, and is empty when it carries ## none. ## ## @var{varNames} are the variables of the sheet read as the event table. ## @var{errmsg} is the body of a complaint for the caller to raise under its ## own name when a designation names none of them, and is empty otherwise. ## ## @end deftypefn function [args, errmsg] = __odseventopts__ (x, varNames) args = {}; if (! isempty (x.labels)) args = [args, {'EventLabelsVariable', x.labels}]; endif if (! isempty (x.lengths)) args = [args, {'EventLengthsVariable', x.lengths}]; endif if (! isempty (x.ends)) args = [args, {'EventEndsVariable', x.ends}]; endif errmsg = ''; named = args(2:2:end); bad = find (! ismember (named, varNames), 1); if (! isempty (bad)) errmsg = sprintf (strcat ("sheet '%s' is named as the event table of", ... " sheet '%s' but has no variable '%s'."), ... x.to, x.from, named{bad}); endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__rfc95572dt__.m000066400000000000000000000057441527370255000230270ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {[@var{dt}, @var{ok}] =} __rfc95572dt__ (@var{C}) ## ## Parse a cell block of RFC 9557 date-time strings into a zoned ## @code{datetime}. ## ## @var{ok} is false unless every non-empty entry carries the bracketed zone ## name and every one of them names the @strong{same} zone, a column being one ## variable; @var{dt} is empty then and the caller falls back to its own ## parsing. An empty entry becomes @code{NaT}. ## ## The instant is taken from the local time @strong{less the offset the string ## carries}, and the zone is applied to that, so a time written in a repeated ## hour comes back on the side of the fold it was written on. Reading the ## local components alone would resolve every fold the same way and silently ## move half of them. ## ## @seealso{__dt2rfc9557__} ## @end deftypefn function [dt, ok] = __rfc95572dt__ (C) dt = []; ok = false; pat = strcat ('^\s*(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):', ... '(\d{2}(?:\.\d+)?)([+-])(\d{2}):(\d{2})', ... '\[([^\]]+)\]\s*$'); sz = size (C); Y = nan (sz); Mo = nan (sz); D = nan (sz); h = nan (sz); mi = nan (sz); s = nan (sz); off = nan (sz); zone = ''; any_value = false; for i = 1:numel (C) str = C{i}; if (! ischar (str)) str = ''; endif if (isempty (str)) continue; endif tok = regexp (str, pat, 'tokens', 'once'); if (isempty (tok)) return; endif if (isempty (zone)) zone = tok{10}; elseif (! strcmp (zone, tok{10})) return; endif Y(i) = str2double (tok{1}); Mo(i) = str2double (tok{2}); D(i) = str2double (tok{3}); h(i) = str2double (tok{4}); mi(i) = str2double (tok{5}); s(i) = str2double (tok{6}); osec = str2double (tok{8}) * 3600 + str2double (tok{9}) * 60; if (strcmp (tok{7}, '-')) osec = -osec; endif off(i) = osec; any_value = true; endfor if (! any_value) return; endif ## The instant is the local time less its offset, read in UTC; assigning the ## zone then converts it, which is what carries a fold across. utc = datetime (Y, Mo, D, h, mi, s, 'TimeZone', 'UTC') - seconds (off); utc.TimeZone = zone; dt = utc; ok = true; endfunction pr0m1th3as-datatypes-9450b29/inst/private/__summary__.m000066400000000000000000000336751527370255000230120ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . function __summary__ (S, DIM, NAMES, STATS) ## Grab size and type sz = S.Size; type = S.Type; name = S.Name; ## Print header str = strjoin (repmat ({'%d'}, 1, numel (sz)), 'x'); if (isempty (name)) str = sprintf ('\n%s %s array\n\n', str, type); else str = sprintf ('\n%s: %s %s array\n\n', name, str, type); endif fprintf (str, sz); ## Handle empty arrays if (false)#(any (sz == 0)) nanfn = {'Size', 'Type', 'Name', 'Categories', 'Counts'}; remfn = fieldnames (S); remfn(ismember (remfn, nanfn)) = []; if (! isempty (remfn)) fprintf ('Additional statistics:\n\n'); for i = 1:numel (remfn) stat = S.(remfn{i}); endfor endif return; endif ## Concatenate STATS (if any) disp_stats = false; if (! isempty (STATS)) disp_stats = true; stats = {}; for idx = 1:numel (STATS) sname = STATS{idx}; tmp = S.(sname); if (isnumeric (tmp)) tmp = num2cell (tmp); else tmp = dispstrings (tmp); endif stats = cat (DIM, stats, tmp); endfor ## Append stats to stat names tmpsz = sz; tmpsz(1:2) = 1; if (DIM == 1) tmp = repmat (STATS, tmpsz); stats = [tmp, stats]; elseif (DIM == 2) STATS = STATS'; tmpS = repmat (STATS, tmpsz); tmpE = repmat (repmat ({' '}, size (STATS)), tmpsz); stats = [tmpS; tmpE; stats]; endif endif ## Check if is included in NAMES disp_counts = false; if (! isempty (NAMES)) disp_counts = true; if (any (strcmpi (NAMES, ''))) counts = cat (DIM, S.Counts, S.NumMissing); else counts = S.Counts; endif counts = num2cell (counts); ## Append counts to categories tmpsz = sz; tmpsz(1:2) = 1; if (DIM == 1) tmp = repmat (NAMES, tmpsz); counts = [tmp, counts]; elseif (DIM == 2) tmp = repmat (NAMES', tmpsz); counts = [tmp; counts]; endif endif if (disp_counts) if (DIM < 3) disp_summary_low (counts); else disp_summary_high (counts, NAMES, DIM); endif endif if (disp_stats) if (DIM < 3) disp_summary_low (stats, true); else disp_summary_high (stats, STATS, DIM, true); endif endif endfunction function disp_summary_low (C, show_stats = false) if (ismatrix (C)) if (show_stats) fprintf ('Additional statistics:\n\n'); endif dispcellmatrix (C); else if (show_stats) fprintf ('Additional statistics:\n\n'); endif ## Handle each page separately sz = size (C); high_sz = sz(3:end); high_ixs = {}; for i = 1:numel (high_sz) high_ixs{i} = [1:high_sz(i)]'; endfor page_ixs = combvec (high_ixs); page_num = size (page_ixs, 1); for ix = 1:page_num p_ix = page_ixs(ix,:); pagestr = sprintf (strjoin (repmat ({'%d'}, 1, numel (p_ix)), ','), p_ix); if (show_stats) fprintf ('(:,:,%s) summary:\n\n', pagestr); else fprintf ('(:,:,%s) = \n\n', pagestr); endif idx.type = '()'; idx.subs = {':', ':', num2cell(p_ix){:}}; dispcellmatrix (subsref (C, idx)); endfor endif endfunction function disp_summary_high (C, D, DIM, show_stats = false) if (show_stats) fprintf ('Additional statistics:\n\n'); endif ## Handle each page separately sz = size (C); DIM = DIM - 2; high_sz = sz(3:end); high_ixs = {}; for i = 1:numel (high_sz) high_ixs{i} = [1:high_sz(i)]'; endfor if (isempty (high_ixs)) fprintf ('%s:\n', D{1}); dispcellmatrix (C); return; endif page_ixs = combvec (high_ixs); ## Group categories together if (DIM > 1) new_page = []; for i = 1:numel (D) new_page = [new_page; page_ixs(page_ixs(:,DIM) == i, :)]; endfor page_ixs = new_page; endif page_num = size (page_ixs, 1); for ix = 1:page_num p_ix = page_ixs(ix,:); cidx = p_ix(DIM); p_ixx = arrayfun (@(x) cellstr (num2str (x)), p_ix); p_ixx(DIM) = D(cidx); pagestr = sprintf (strjoin (repmat ({'%s'}, 1, numel (p_ix)), ','), ... p_ixx{:}); if (show_stats) fprintf ('(:,:,%s) summary:\n\n', pagestr); else fprintf ('(:,:,%s) = \n\n', pagestr); endif idx.type = '()'; idx.subs = {':', ':', num2cell(p_ix){:}}; dispcellmatrix (subsref (C, idx)); endfor endfunction function dispcellmatrix (C) sz = terminal_size (); cols = sz(2) - 4; colgap = " "; dispstr = {}; optLens = []; for iCol = 1:size (C, 2) [outstr, optLen] = mixedcell2str (C(:, iCol), cols); dispstr = [dispstr, outstr]; optLens = [optLens, optLen]; endfor if (sum (optLens + 6) <= cols) # all columns fit in terminal size rowSpat = ""; for iCol = 1:size (C, 2) rowSpat = [rowSpat, sprintf("%%-%ds", optLens(iCol)), colgap]; endfor for iRow = 1:size (C, 1) strrow = sprintf (rowSpat, dispstr{iRow,:}); fprintf (" %s\n", strrow); endfor fprintf ("\n"); else # we need to split rows optLen_cs = cumsum (optLens + 6); startCol = 1; while (! isempty (find (optLen_cs > cols))) stopCol = find (optLen_cs > cols, 1) - 1; ## Just in case a single column exceeds terminal size if (stopCol == 0) stopCol = 1; endif rowSpat = ""; for iCol = 1:stopCol rowSpat = [rowSpat, sprintf("%%-%ds", optLens(iCol)), colgap]; endfor optLens(1:iCol) = []; optLen_cs = cumsum (optLens + 6); stopCol = stopCol + startCol - 1; fprintf ("Columns %d through %d:\n\n", startCol, stopCol); for iRow = 1:size (C, 1) strrow = sprintf (rowSpat, dispstr{iRow,[startCol:stopCol]}); fprintf (" %s\n", strrow); endfor fprintf ("\n"); startCol = stopCol + 1; endwhile if (! isempty (optLens)) for iCol = 1:length (optLens) rowSpat = [rowSpat, sprintf("%%-%ds", optLens(iCol)), colgap]; endfor stopCol = startCol + iCol - 1; if (startCol == stopCol) fprintf ("Column %d:\n\n", startCol); else fprintf ("Columns %d through %d:\n\n", startCol, stopCol); endif for iRow = 1:size (C, 1) strrow = sprintf (rowSpat, dispstr{iRow,[startCol:stopCol]}); fprintf (" %s\n", strrow); endfor fprintf ("\n"); endif endif endfunction ## Special function to convert a mixed cell array to cellstr array ## that keeps MATLAB like formatting for each type of element function [dispstr, optLen] = mixedcell2str (data, cols) dispstr = cell (size (data)); ## Preallocate indexes to avoid truncation when last elements are 0 is_char = logical (zeros (size (data))); is_bool = is_char; is_numeric = is_char; is_object = is_char; is_struct = is_char; has_method = is_char; no_method = is_char; ## Nested cells are printed by size is_cell = cellfun ('iscell', data); sf = @(x) sprintf (strcat ([strjoin(repmat ({'%d'}, 1, ndims (x)), 'x'), ... ' cell']), size (x)); dispstr(is_cell) = cellfun (sf, data(is_cell), "UniformOutput", false); ## Empty cells are printed as doubles is_empty = cellfun (@isempty, data); sf = @(x) sprintf (strcat (repmat ("%dx", 1, ndims (x))(1:end-1), " %s"), ... size (x), class (x)); dispstr(is_empty) = cellfun (sf, data(is_empty), "UniformOutput", false); ## Index remaining scalar and row vector elements ve = cell2mat (cellfun (@(x) isrow (x), data, "UniformOutput", false)) == 1; ve = ve & ! (is_cell | is_empty); ## Index everything else me = cell2mat (cellfun (@(x) isrow (x), data, "UniformOutput", false)) != 1; me = me & ! (is_cell | is_empty); ## Catch 'char' scalars or row vectors is_char(ve) = cellfun ('ischar', data(ve)); sf = @(x) sprintf ("%s", x); dispstr(is_char) = cellfun (sf, data(is_char), "UniformOutput", false); ## Catch 'logical' scalars or row vectors is_bool(ve) = cellfun ('islogical', data(ve)); sf = @(x) sprintf ("%s", strtrim (sprintf ("%d ", x))); dispstr(is_bool) = cellfun (sf, data(is_bool), "UniformOutput", false); ## Catch 'numeric' scalars or row vectors is_numeric(ve) = cellfun ('isnumeric', data(ve)); sf = @(x) sprintf ("%s", strtrim (sprintf ("%g ", x))); dispstr(is_numeric) = cellfun (sf, data(is_numeric), "UniformOutput", false); ## Catch 'object' scalars or row vectors is_object(ve) = cellfun ('isobject', data(ve)); ## Handle objects with dispstring method available f = @(x) ismethod (x, 'dispstrings'); has_method(is_object) = cellfun (f, data(is_object)); sf = @(x) sprintf ("%s", strjoin (dispstrings (x), ' ')); dispstr(has_method) = cellfun (sf, data(has_method), "UniformOutput", false); ## Handle objects without no_method(is_object) = ! cellfun (f, data(is_object)); sf = @(x) sprintf (strcat ([strjoin(repmat ({'%d'}, 1, ndims (x)), 'x'), ... ' %s']), size (x), class (x)); dispstr(no_method) = cellfun (sf, data(no_method), "UniformOutput", false); ## Catch scalar elements or row vectors of 'struct' type is_struct(ve) = cellfun ('isstruct', data(ve)); sf = @(x) sprintf (strcat ([strjoin(repmat ({'%d'}, 1, ndims (x)), 'x'), ... ' struct']), size (x)); dispstr(is_struct) = cellfun (sf, data(is_struct), "UniformOutput", false); ## Catch remaining elements containing matrices or arrays of any type sf = @(x) sprintf (strcat ([strjoin(repmat ({'%d'}, 1, ndims (x)), 'x'), ... ' %s']), size (x), class (x)); dispstr(me) = cellfun (sf, data(me), "UniformOutput", false); ## Index numerical and logical values to right alignment pad_B = is_numeric | is_bool; # pad before: sprintf("{%%+%ds}" ## Index array types for bracketing brackets = pad_B | has_method; ## Get optimal length if (all (brackets)) optLen = max (cellfun (@length, dispstr(brackets))); elseif (any (brackets)) optLen1 = max (cellfun (@length, dispstr(brackets))); optLen2 = max (cellfun (@length, dispstr(! brackets))); optLen = max (optLen1, optLen2); else optLen = max (cellfun (@length, dispstr(! brackets))); endif ## Make sure a single column does not exceed terminal size if (optLen > cols) ## Truncate only a text too wide for the column, leaving shorter ones whole. for i = find (is_char)' if (numel (dispstr{i}) > cols) dispstr{i} = sprintf ("'%s ... '", data{i}(1:cols-10)); endif endfor for i = 1:sum (is_bool) hm_idx = find (is_bool)(i); do_idx = find (cumsum (cellfun ('length', strsplit (dispstr{hm_idx})) ... + 1) > cols - 10, 1) - 1; if (! isempty (do_idx)) sf = @(x) sprintf ("%s ... ", strtrim (sprintf ("%d ", x(1:do_idx)))); dispstr(hm_idx) = cellfun (sf, data(hm_idx), "UniformOutput", false); endif endfor for i = 1:sum (is_numeric) hm_idx = find (is_numeric)(i); do_idx = find (cumsum (cellfun ('length', strsplit (dispstr{hm_idx})) ... + 1) > cols - 8, 1) - 1; if (! isempty (do_idx)) sf = @(x) sprintf ("%s ... ", strtrim (sprintf ("%g ", x(1:do_idx)))); dispstr(hm_idx) = cellfun (sf, data(hm_idx), "UniformOutput", false); endif endfor for i = 1:sum (has_method) hm_idx = find (has_method)(i); do_idx = find (cumsum (cellfun ('length', strsplit (dispstr{hm_idx})) ... + 4) > cols - 6, 1) - 1; if (! isempty (do_idx)) sf = @(x) sprintf ("%s ... ", ... strjoin (dispstrings (x(1:do_idx)), ' ')); dispstr(hm_idx) = cellfun (sf, data(hm_idx), "UniformOutput", false); endif endfor ## Recalculate optimal length if (all (brackets)) optLen = max (cellfun (@length, dispstr(brackets))) + 2; elseif (any (brackets)) optLen1 = max (cellfun (@length, dispstr(brackets))) + 2; optLen2 = max (cellfun (@length, dispstr(! brackets))); optLen = max (optLen1, optLen2); else optLen = max (cellfun (@length, dispstr(! brackets))); endif endif ## Pad data according to optimal length ## numeric and logical is right aligned, everything else is left aligned Ra_wB = sprintf ("%%+%ds", optLen); fcn = @(x) sprintf (Ra_wB, x); idx = pad_B & brackets; dispstr(idx) = cellfun (fcn, dispstr(idx), "UniformOutput", false); La_wB = sprintf ("%%-%ds", optLen); fcn = @(x) sprintf (La_wB, x); idx = ! pad_B & brackets; dispstr(idx) = cellfun (fcn, dispstr(idx), "UniformOutput", false); La_nB = sprintf ("%%-%ds", optLen); fcn = @(x) sprintf (La_nB, x); idx = (! pad_B & ! brackets) | me; dispstr(idx) = cellfun (fcn, dispstr(idx), "UniformOutput", false); endfunction function out = combvec (vecs) switch (numel (vecs)) case 1 out = vecs{1}(:); case 2 a = vecs{1}(:); b = vecs{2}(:); out = repmat (a, numel (b), 2); i_comb = 1; for i_a = 1:numel (a) for i_b = 1:numel (b) out(i_comb,:) = [a(i_a), b(i_b)]; i_comb = i_comb + 1; endfor endfor otherwise out = []; a = vecs{1}(:); rest = vecs(2:end); rest_combs = combvec (rest); for i = 1:numel (a) out = [out; [repmat(a(i), [size(rest_combs,1), 1]), rest_combs]]; endfor endswitch endfunction pr0m1th3as-datatypes-9450b29/inst/private/__topkrowsargs__.m000066400000000000000000000104421527370255000240450ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {private} {[@var{col}, @var{direction}, @var{MP}, @var{errmsg}] =} __topkrowsargs__ (@var{K}, @var{ncols}, @var{args}) ## ## Validate the arguments of the @code{topkrows} methods. ## ## This is the shared argument handling of the @code{topkrows} methods of the ## @code{categorical}, @code{datetime}, and @code{duration} classes. @var{K} is ## the number of rows asked for, @var{ncols} the number of columns of the array ## being ranked, and @var{args} the cell array of the caller's optional input ## arguments. ## ## @var{col} is the list of sort columns, empty when none was given, and ## @var{direction} is either a character vector or a cell array of them. The ## default direction is @qcode{'descend'}: these are the top rows, not the first ## ones, which is where @code{topkrows} parts company with @code{sortrows}. ## ## @var{MP} is the missing placement, defaulting to @qcode{'last'}. MATLAB has ## no such option in @code{topkrows} and always ranks as @qcode{'last'} does, so ## the default reproduces it and naming the option is an Octave extension. ## ## No error is raised here. Instead @var{errmsg} returns the message body, ## which is empty on success, and the caller is expected to raise the error ## under its own class and method name. ## ## @end deftypefn function [col, direction, MP, errmsg] = __topkrowsargs__ (K, ncols, args) col = []; direction = 'descend'; MP = 'last'; errmsg = ''; ## Check K if (! (isnumeric (K) && isscalar (K) && isreal (K) && isfinite (K) && fix (K) == K && K >= 0)) errmsg = "K must be a nonnegative integer scalar."; return; endif if (numel (args) > 0) [args{:}] = convertStringsToChars (args{:}); endif ## Take 'MissingPlacement' out before the positional arguments are read, so ## that naming it is reported as itself rather than as one argument too many. mid = find (cellfun (@(x) ischar (x) && ... strcmpi (x, 'MissingPlacement'), args)); if (! isempty (mid)) if (mid(1) == numel (args)) errmsg = "'MissingPlacement' requires a value."; return; endif MP = args{mid(1)+1}; if (! (ischar (MP) && isrow (MP)) || ! any (strcmpi (MP, {'auto', 'first', 'last'}))) errmsg = "invalid value for 'MissingPlacement'."; return; endif args([mid(1), mid(1)+1]) = []; endif if (numel (args) > 2) errmsg = "too many input arguments."; return; endif ## Split the remaining arguments into COL and DIRECTION for i = 1:numel (args) if (isnumeric (args{i})) col = args{i}; elseif (ischar (args{i}) || iscellstr (args{i})) direction = args{i}; else errmsg = strcat ("optional arguments must be a column list or a", ... " sorting direction."); return; endif endfor ## Unlike 'sortrows', COL must be positive: a negative column does not select ## a direction here, as MATLAB also refuses it. if (! isempty (col)) if (! (isvector (col) && isreal (col) && all (isfinite (col)) && all (fix (col) == col) && all (col > 0) && all (col <= ncols))) errmsg = strcat ("COL must contain positive integers indexing", ... " existing columns in A."); return; endif endif if (ischar (direction)) dirlist = {direction}; else dirlist = direction; endif if (! all (cellfun (@(d) ischar (d) && ... any (strcmpi (d, {'ascend', 'descend'})), dirlist))) errmsg = "DIRECTION must be 'ascend' or 'descend'."; return; endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__typefmt__.m000066400000000000000000000030131527370255000227640ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {[@var{base}, @var{fmt}] =} __typefmt__ (@var{T}) ## ## Split a variable-type string into its type and its display format. ## ## A @code{datetime} or @code{duration} variable written by @code{table2csv} or ## @code{table2ods} carries its @qcode{Format} in the variable-type row, after a ## @qcode{"|"}, so that the format survives the round trip. @var{base} is the ## part before the separator, which still carries a @code{datetime}'s ## @qcode{TimeZone}, and @var{fmt} is the part after it, empty when the string ## carries no format. ## ## @end deftypefn function [base, fmt] = __typefmt__ (T) base = T; fmt = ''; k = index (T, '|'); if (k > 0) base = T(1:k-1); fmt = T(k+1:end); endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__undimname__.m000066400000000000000000000031721527370255000232570ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{names} =} __undimname__ (@var{names}, @var{dims}) ## ## Rename any variable name that collides with a dimension name. ## ## A table refuses a variable named after one of its dimensions, so a file of ## another program's whose column happens to be headed @qcode{Row} could not be ## read at all. Such a name takes a numeric suffix, chosen so that it collides ## with neither the dimension names @var{dims} nor another variable name. ## ## @end deftypefn function names = __undimname__ (names, dims) for i = 1:numel (names) if (any (strcmp (names{i}, dims))) k = 1; cand = sprintf ('%s_%d', names{i}, k); while (any (strcmp (cand, names)) || any (strcmp (cand, dims))) k += 1; cand = sprintf ('%s_%d', names{i}, k); endwhile names{i} = cand; endif endfor endfunction pr0m1th3as-datatypes-9450b29/inst/private/__unique__.m000066400000000000000000000131011527370255000226010ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {private} {@var{B} =} __unique__ (@var{A}) ## @deftypefnx {private} {@var{B} =} __unique__ (@var{A}, @var{setOrder}) ## @deftypefnx {private} {@var{B} =} __unique__ (@var{A}, @var{occurrence}) ## @deftypefnx {private} {@var{B} =} __unique__ (@var{A}, @var{setOrder}, @var{occurrence}) ## @deftypefnx {private} {@var{B} =} __unique__ (@var{A}, @var{occurrence}, @var{setOrder}) ## @deftypefnx {private} {@var{B} =} __unique__ (@var{A}, @dots{}, @qcode{'rows'}) ## @deftypefnx {private} {@var{B} =} __unique__ (@var{A}, @qcode{'rows'}, @dots{}) ## @deftypefnx {private} {[@var{B}, @var{ixA}, @var{ixB}] =} __unique__ (@dots{}) ## ## Return the unique elements of @var{x}. ## ## @end deftypefn function [y, i, j] = __unique__ (x, varargin) if (nargin < 1) print_usage (); elseif (! (isnumeric (x) || islogical (x) || ischar (x) || iscellstr (x))) error ("unique: X must be an array or cell array of strings."); endif if (nargin > 1) ## Parse options if (! iscellstr (varargin)) error ("unique: options must be strings."); endif optrows = any (strcmp ("rows", varargin)); optfirst = any (strcmp ("first", varargin)); optlast = any (strcmp ("last", varargin)); optsorted = any (strcmp ("sorted", varargin)); optstable = any (strcmp ("stable", varargin)); if (optrows && ndims (x) != 2) error ("unique: 'rows' applies only to 2-D matrices."); elseif (optfirst && optlast) error ("unique: cannot specify both 'first' and 'last'."); elseif (optsorted && optstable) error ("unique: cannot specify both 'sorted' and 'stable'."); elseif (optrows + optfirst + optlast + optsorted + optstable != nargin-1) error ("unique: invalid optional argument."); endif ## Set defaults if not set earlier. if (! optfirst && ! optlast) optfirst = true; endif if (! optsorted && ! optstable) optsorted = true; endif else optrows = false; optfirst = true; optsorted = true; endif if (optrows) n = rows (x); isrowvec = false; else n = numel (x); isrowvec = isrow (x); endif ## Special cases 0 and 1 if (n == 0) y = x; if (! optrows && any (size (x))) if (iscellstr (x)) y = cell (0, 1); else y = zeros (0, 1, class (x)); endif endif i = j = []; return; elseif (n == 1) y = x; i = j = 1; return; endif ## Calculate y output if (optrows) if (nargout > 1 || ! optsorted) [y, j] = sortrows (x); j = j(:); else y = sortrows (x); endif if (iscellstr (x)) match = all (cellfun (@isequal, y(1:n-1,:), y(2:n,:)), 2); else match = all (y(1:n-1,:) == y(2:n,:), 2); endif if (optsorted) y(match,:) = []; else y = x; if (optfirst) y(j([false; match]), :) = []; else y(j([match; false]), :) = []; endif endif else if (isvector (x)) y = x; else y = x(:); endif if (nargout > 1 || ! optsorted) [y, j] = sort (y); j = j(:); else y = sort (y); endif if (iscellstr (y)) match = strcmp (y(1:n-1), y(2:n)); else match = (y(1:n-1) == y(2:n)); endif if (optsorted) y(match) = []; else if (isvector (x)) y = x; else y = x(:); endif if (optfirst) y(j([false; match(:)])) = []; else y(j([match(:); false])) = []; endif endif endif ## Calculate 2nd and 3rd outputs if (nargout > 1) if (optsorted) idx = find (match); if (optfirst) idx += 1; endif i = j; i(idx) = []; if (nargout > 2) j(j) = cumsum (! [false; match(:)]); endif else ## Get inverse of sort index j so that sort(x)(k) = x(j)(k) = x. k = j; # cheap way to copy dimensions k(j) = 1:n; ## Generate logical index of sorted unique value locations. if (optfirst) uniquex = ! [false; match(:)]; else uniquex = ! [match(:); false]; endif ## Remap unique locations to unsorted x, such that y = x(i). i = find (uniquex(k)); if (nargout > 2) ni = numel (i); u = find (uniquex); # Linear index of unique elements of sort(x) p = j; # cheap way to copy dimensions if (optfirst) l = u(cumsum (uniquex)); # Expand u for all elements in sort(x) p(i) = 1:ni; # set p to contain the vector positions of i elseif (optrows) l = u(cumsum (uniquex, 'reverse')); p(i) = ni:-1:1; else l = u(cumsum (! [false; match(:)])); p(i) = 1:ni; endif j = p(j(l(k))); # Replace j with 3rd output mapping y->x. endif endif endif endfunction pr0m1th3as-datatypes-9450b29/inst/private/__varmissing__.m000066400000000000000000000030451527370255000234630ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {private} {@var{M} =} __varmissing__ (@var{v}) ## ## Find missing data in one variable of a tabular object. ## ## @code{ismissing} is a method of the datatypes classes and reaches no plain ## numeric, logical or cellstr array, so the value's type chooses the route. ## A nested table and a character matrix have no missing value of their own. ## ## @end deftypefn function M = __varmissing__ (v) if (isa (v, 'table') || isa (v, 'timetable')) M = false (size (v)); elseif (any (isa (v, {'calendarDuration', 'categorical', 'datetime', ... 'duration', 'missing', 'string'}))) M = ismissing (v); elseif (ischar (v)) M = false (size (v)); else # numeric, logical, cellstr M = __ismissing__ (v); endif endfunction pr0m1th3as-datatypes-9450b29/inst/readdictionary.m000066400000000000000000003107371527370255000220450ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{d} =} readdictionary (@var{filename}) ## @deftypefnx {datatypes} {@var{d} =} readdictionary (@var{filename}, @var{Name}, @var{Value}) ## ## Read a dictionary from a JSON or an OpenDocument spreadsheet file. ## ## @code{@var{d} = readdictionary (@var{filename})} reads the JSON object in ## the file @var{filename}, a character vector or a string scalar naming a ## local file or an @qcode{http} or @qcode{https} address, and returns it as a ## @code{dictionary} with @code{string} keys in the order of the file. The ## value type is chosen from the values, all of them together: ## ## @itemize ## @item numbers give @code{double}; integers beyond @math{2^53} give ## @code{uint64}, or @code{int64} when one is negative and all fit, and a ## number with a fraction among them gives @code{cell}; ## @item text gives @code{string}; dates in ISO 8601 form without a time zone ## (@qcode{2024-01-01}, @qcode{2024-01-01T10:00:00}) or as ## @qcode{01-Jan-2024} give @code{datetime}, and times as ## @qcode{01:30:00} give @code{duration}; a timestamp with a zone mark ## (@qcode{Z}, @qcode{+02:00}) stays text; ## @item @qcode{true} and @qcode{false} give @code{logical}; ## @item @qcode{null} is missing: @code{NaN}, a missing string or @code{NaT} ## among the values above, the @code{missing} type when all are ## @qcode{null}; among @code{logical} or 64-bit integer values, which have no ## missing value, it gives @code{cell}; ## @item any other mixture, an array, or an object gives @code{cell}: an ## array becomes a column cell array of its elements, each typed alone, and ## an object a dictionary read by these rules, @qcode{@{@}} an unconfigured ## one. ## @end itemize ## ## @code{@var{d} = readdictionary (@var{filename}, @var{Name}, @var{Value})} ## specifies options with the following Name-Value paired arguments, whose ## names and text values are matched in any case. ## ## @multitable @columnfractions 0.25 0.75 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'FileType'} @tab @qcode{'auto'} (the default) tells the type ## from the extension, @qcode{.json}, @qcode{.ods} or @qcode{.fods}; ## @qcode{'json'} or @qcode{'ods'} sets it. ## @item @qcode{'ValueType'} @tab @qcode{'auto'} (the default) as above, or ## one of @qcode{'cell'}, @qcode{'string'}, @qcode{'double'}, ## @qcode{'single'}, the integer types, @qcode{'logical'}, ## @qcode{'datetime'} and @qcode{'duration'}. Text is never read as a ## number: it gives @code{NaN}, 0 or false; numbers give text under ## @qcode{'string'}, and @qcode{true} gives @qcode{"true"} or 1; arrays, ## objects and @qcode{null} give the missing value of the type. Under ## @qcode{'datetime'} text is read as dates, each keeping the zone it states: ## @qcode{Z} gives UTC, an offset a fixed-offset zone, no mark no zone. ## Under @qcode{'cell'} every value is in a cell, objects read the same way. ## @item @qcode{'DuplicateKeyRule'} @tab @qcode{'auto'} and ## @qcode{'makeUnique'} rename a repeated key @qcode{a} as @qcode{a_1}, ## @qcode{a_2} and so on, skipping names in use; @qcode{'preserveLast'} ## keeps the first position with the last value; @qcode{'error'} refuses. ## @item @qcode{'DictionarySelector'} @tab A JSON Pointer to the object to ## read, as @qcode{'/data/2'}, counting array elements from 0; @qcode{''} is ## the whole file. ## @item @qcode{'DictionaryNodeName'} @tab The name of the object to read: ## the first key of that name, depth first. It cannot be given with ## @qcode{'DictionarySelector'}. ## @item @qcode{'DateLocale'} @tab The locale of month names in dates, as ## @qcode{'de_DE'}. ## @item @qcode{'ParsingMode'} @tab @qcode{'lenient'} (the default) allows ## comments, trailing commas and @code{NaN}, @code{Inf} and ## @code{Infinity}; @qcode{'strict'} refuses them. ## @item @qcode{'AllowComments'}, @qcode{'AllowTrailingCommas'}, ## @qcode{'AllowInfAndNaN'} @tab Logical scalars allowing each form; each ## overrides @qcode{'ParsingMode'}. ## @end multitable ## ## The top level, or the object selected, must be a JSON object. MATLAB's ## @qcode{'ValueType', 'datetime'} reads an ISO 8601 date as @code{NaT}; ## here it reads it as the default does. ## ## An OpenDocument spreadsheet, @qcode{FileType} @qcode{'ods'}, gives one ## entry per sheet, the sheet name the key and the @code{table} or ## @code{timetable} read by @code{ods2struct} the value, so that a dictionary ## written by @code{writedictionary} comes back as written. A workbook ## holding both tables and timetables gives @code{cell} values. Reading ## ODS is an extension of this package; it takes no option but ## @qcode{'FileType'}. ## ## Remote locations other than @qcode{http} and @qcode{https} (Amazon S3, ## Azure Blob Storage, HDFS), compressed files and @qcode{'WebOptions'}, ## which MATLAB accepts, are not supported. ## ## @seealso{dictionary, dictionary.writedictionary, ods2struct} ## @end deftypefn function d = readdictionary (filename, varargin) if (nargin < 1) print_usage (); endif if (! ((ischar (filename) && rows (filename) <= 1) || (isa (filename, 'string') && isscalar (filename)))) error (strcat ("readdictionary: FILENAME must be a character vector", ... " or a string scalar.")); endif filename = char (filename); if (mod (numel (varargin), 2) != 0) error ("readdictionary: name-value arguments must be in pairs."); endif ## An empty default marks an option not given optNames = {'FileType', 'ValueType', 'DuplicateKeyRule', ... 'DictionarySelector', 'DictionaryNodeName', 'DateLocale', ... 'ParsingMode', 'AllowComments', 'AllowTrailingCommas', ... 'AllowInfAndNaN', 'WebOptions'}; dfValues = repmat ({[]}, size (optNames)); [fileType, valueType, dupRule, selector, nodeName, locale, mode, ... comments, commas, infnan, webOptions, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error ("readdictionary: invalid optional paired argument."); endif if (! isempty (webOptions)) error ("readdictionary: 'WebOptions' is not supported."); endif ## Remote stores and archives are out of reach scheme = regexp (filename, '^(s3|wasbs?|hdfs)://', 'tokens', 'once'); if (! isempty (scheme)) error ("readdictionary: reading from '%s' locations is not supported.", ... scheme{1}); endif [~, ~, ext] = fileparts (regexprep (filename, '[?#].*$', '')); if (any (strcmpi (ext, {'.gz', '.zip', '.tar', '.tgz', '.bz2', '.7z'}))) error ("readdictionary: compressed files are not supported."); endif [fileType, errmsg] = choice (fileType, 'auto', {'auto', 'json', 'ods'}, ... 'FileType'); if (! isempty (errmsg)) error ("readdictionary: %s", errmsg); endif if (strcmp (fileType, 'auto')) switch (lower (ext)) case '.json' fileType = 'json'; case {'.ods', '.fods'} fileType = 'ods'; otherwise error (strcat ("readdictionary: unable to tell the file type", ... " from the extension; set 'FileType'.")); endswitch endif ## A web address is read through a local copy isURL = ! isempty (regexp (filename, '^https?://', 'once')); if (isURL) local = [tempname() ext]; [~, ok, msg] = urlwrite (filename, local); if (! ok) error ("readdictionary: unable to read '%s': %s", filename, msg); endif unwind_protect d = readlocal (local, fileType, valueType, dupRule, selector, ... nodeName, locale, mode, comments, commas, infnan); unwind_protect_cleanup if (exist (local, 'file')) delete (local); endif end_unwind_protect else d = readlocal (filename, fileType, valueType, dupRule, selector, ... nodeName, locale, mode, comments, commas, infnan); endif endfunction ## The dictionary in the local file FILENAME function d = readlocal (filename, fileType, valueType, dupRule, selector, ... nodeName, locale, mode, comments, commas, infnan) ## The options are checked before the file is touched if (strcmp (fileType, 'ods')) given = {'ValueType', 'DuplicateKeyRule', 'DictionarySelector', ... 'DictionaryNodeName', 'DateLocale', 'ParsingMode', ... 'AllowComments', 'AllowTrailingCommas', 'AllowInfAndNaN'}; used = ! cellfun ('isempty', {valueType, dupRule, selector, nodeName, ... locale, mode, comments, commas, infnan}); if (any (used)) error ("readdictionary: '%s' does not apply to ODS files.", ... given{find (used, 1)}); endif if (! exist (filename, 'file')) error ("readdictionary: unable to find the file '%s'.", filename); endif d = readods (filename); return; endif ## Options of JSON [valueType, errmsg] = choice (valueType, 'auto', {'auto', 'cell', ... 'string', 'double', 'single', 'int8', ... 'uint8', 'int16', 'uint16', 'int32', ... 'uint32', 'int64', 'uint64', 'logical', ... 'datetime', 'duration'}, 'ValueType'); if (! isempty (errmsg)) error ("readdictionary: %s", errmsg); endif [dupRule, errmsg] = choice (dupRule, 'auto', {'auto', 'preserveLast', ... 'makeUnique', 'error'}, 'DuplicateKeyRule'); if (! isempty (errmsg)) error ("readdictionary: %s", errmsg); endif [mode, errmsg] = choice (mode, 'lenient', {'lenient', 'strict'}, ... 'ParsingMode'); if (! isempty (errmsg)) error ("readdictionary: %s", errmsg); endif lenient = strcmp (mode, 'lenient'); flags = {comments, commas, infnan}; flagNames = {'AllowComments', 'AllowTrailingCommas', 'AllowInfAndNaN'}; for i = 1:3 if (isempty (flags{i})) flags{i} = lenient; elseif (! ((islogical (flags{i}) || isnumeric (flags{i})) && isscalar (flags{i}) && ! isnan (flags{i}))) error ("readdictionary: '%s' must be a logical scalar.", flagNames{i}); else flags{i} = logical (flags{i}); endif endfor if (! isempty (selector) && ! isempty (nodeName)) error (strcat ("readdictionary: 'DictionarySelector' and", ... " 'DictionaryNodeName' cannot be given together.")); endif if (isempty (locale)) locale = ''; elseif (! ((ischar (locale) && rows (locale) == 1) || (isa (locale, 'string') && isscalar (locale)))) error (strcat ("readdictionary: 'DateLocale' must be a character", ... " vector or a string scalar.")); endif locale = char (locale); if (! exist (filename, 'file')) error ("readdictionary: unable to find the file '%s'.", filename); endif text = fileread (filename); [tree, errmsg] = __json2dictionary__ (text, flags{:}); if (! isempty (errmsg)) error ("readdictionary: %s", errmsg); endif ## The object to read if (! isempty (selector)) [tree, errmsg] = select (tree, selector); elseif (! isempty (nodeName)) [tree, errmsg] = findNode (tree, nodeName); endif if (! isempty (errmsg)) error ("readdictionary: %s", errmsg); endif if (! strcmp (kind (tree), 'object')) error ("readdictionary: the JSON value read must be an object."); endif [d, errmsg] = toDictionary (tree, valueType, dupRule, locale); if (! isempty (errmsg)) error ("readdictionary: %s", errmsg); endif endfunction ## The text option VALUE, one of OPTIONS in any case, or DEFAULT when empty function [value, errmsg] = choice (value, default, options, name) errmsg = ''; if (isempty (value)) value = default; return; endif if (! ((ischar (value) && rows (value) == 1) || (isa (value, 'string') && isscalar (value)))) errmsg = sprintf ("'%s' must be a character vector or a string scalar.", name); return; endif match = strcmpi (char (value), options); if (! any (match)) errmsg = sprintf ("invalid '%s' value '%s'.", name, char (value)); return; endif value = options{match}; endfunction ## The kind of the parsed JSON value X function k = kind (x) if (isstruct (x)) if (isempty (fieldnames (x))) k = 'null'; else k = 'object'; endif elseif (iscell (x)) k = 'array'; elseif (ischar (x)) k = 'text'; elseif (islogical (x)) k = 'bool'; elseif (isinteger (x)) k = 'int'; else k = 'num'; endif endfunction ## The value at the JSON Pointer P within TREE function [tree, errmsg] = select (tree, p) errmsg = ''; if (! ((ischar (p) && rows (p) <= 1) || (isa (p, 'string') && isscalar (p)))) errmsg = strcat ("'DictionarySelector' must be a character vector", ... " or a string scalar."); return; endif p = char (p); if (isempty (p)) return; endif if (p(1) != '/') errmsg = sprintf ("invalid JSON Pointer '%s'.", p); return; endif tokens = strsplit (p(2:end), '/'); for i = 1:numel (tokens) t = strrep (strrep (tokens{i}, '~1', '/'), '~0', '~'); switch (kind (tree)) case 'object' idx = find (strcmp (tree.keys, t), 1); if (isempty (idx)) errmsg = sprintf ("selector '%s' not found.", p); return; endif tree = tree.values{idx}; case 'array' if (isempty (regexp (t, '^(0|[1-9][0-9]*)$', 'once')) || str2double (t) >= numel (tree)) errmsg = sprintf ("selector '%s' not found.", p); return; endif tree = tree{str2double (t) + 1}; otherwise errmsg = sprintf ("selector '%s' not found.", p); return; endswitch endfor endfunction ## The value of the first key named NAME within TREE, depth first function [node, errmsg] = findNode (tree, name) errmsg = ''; if (! ((ischar (name) && rows (name) == 1) || (isa (name, 'string') && isscalar (name)))) errmsg = strcat ("'DictionaryNodeName' must be a character vector", ... " or a string scalar."); node = []; return; endif [node, found] = searchNode (tree, char (name)); if (! found) errmsg = sprintf ("node '%s' not found.", char (name)); endif endfunction function [node, found] = searchNode (tree, name) node = []; found = false; switch (kind (tree)) case 'object' for i = 1:numel (tree.keys) if (strcmp (tree.keys{i}, name)) node = tree.values{i}; found = true; return; endif [node, found] = searchNode (tree.values{i}, name); if (found) return; endif endfor case 'array' for i = 1:numel (tree) [node, found] = searchNode (tree{i}, name); if (found) return; endif endfor endswitch endfunction ## The dictionary of the JSON object TREE function [d, errmsg] = toDictionary (tree, valueType, dupRule, locale) errmsg = ''; keys = tree.keys; vals = tree.values; if (isempty (keys)) d = dictionary (); return; endif [keys, errmsg] = uniqueKeys (keys, dupRule); if (! isempty (errmsg)) d = []; return; endif keys = string (keys); if (strcmp (valueType, 'auto')) [V, errmsg] = autoValues (vals, locale); elseif (strcmp (valueType, 'cell')) V = cell (numel (vals), 1); for i = 1:numel (vals) [V{i}, errmsg] = element (vals{i}, 'cell', locale); if (! isempty (errmsg)) d = []; return; endif endfor else V = typedValues (vals, valueType, locale); endif if (! isempty (errmsg)) d = []; return; endif ## Dates in several zones are held one per entry, each as written if (strcmp (valueType, 'datetime') && iscell (V)) d = configureDictionary ('string', 'datetime'); for i = 1:numel (keys) d(keys(i)) = V{i}; endfor return; endif d = dictionary (keys, V); endfunction ## The keys KEYS by the rule DUPRULE: repeated ones renamed, kept for the ## dictionary to merge, or refused function [keys, errmsg] = uniqueKeys (keys, dupRule) errmsg = ''; u = unique (keys); if (numel (u) == numel (keys)) return; endif switch (dupRule) case 'error' for i = 2:numel (keys) if (any (strcmp (keys(1:i-1), keys{i}))) errmsg = sprintf ("duplicate key '%s'.", keys{i}); return; endif endfor case 'preserveLast' ## The dictionary keeps the first position and the last value otherwise used = keys; seen = {}; for i = 1:numel (keys) k = keys{i}; if (any (strcmp (seen, k))) n = 1; while (any (strcmp (used, sprintf ('%s_%d', k, n)))) n += 1; endwhile keys{i} = sprintf ('%s_%d', k, n); used{end+1} = keys{i}; else seen{end+1} = k; endif endfor endswitch endfunction ## The values VALS of one object under 'auto': one typed column, or a column ## cell array when they share no type function [V, errmsg] = autoValues (vals, locale) errmsg = ''; n = numel (vals); kinds = cellfun (@kind, vals, 'UniformOutput', false); isnull = strcmp (kinds, 'null'); present = unique (kinds(! isnull)); if (isempty (present)) V = repmat (missing, n, 1); return; endif if (isequal (present, {'num'})) V = nan (n, 1); V(! isnull) = [vals{! isnull}]; return; endif if (all (ismember (present, {'num', 'int'})) && ! any (isnull)) [V, ok] = bigIntegers (vals); if (ok) return; endif elseif (isequal (present, {'bool'}) && ! any (isnull)) V = [vals{:}]'; return; elseif (isequal (present, {'text'})) [V, ok] = textColumn (vals, isnull, locale); if (ok) return; endif endif ## Anything else is a cell, each value typed alone V = cell (n, 1); for i = 1:n [V{i}, errmsg] = element (vals{i}, 'auto', locale); if (! isempty (errmsg)) return; endif endfor endfunction ## Integers beyond 2^53 among numbers: uint64, or int64 when one is negative ## and all fit; a fraction among them, or a negative number beside a uint64 ## too large for int64 (ours: unmeasured), fails function [V, ok] = bigIntegers (vals) V = []; ok = false; isint = cellfun ('isinteger', vals); num = vals(! isint); if (any (cellfun (@(x) x != fix (x) || ! isfinite (x), num))) return; endif neg = any (cellfun (@(x) x < 0, vals)); if (neg) big = cellfun (@(x) isa (x, 'uint64') && x > intmax ('int64'), vals); if (any (big)) return; endif V = int64 (zeros (numel (vals), 1)); else V = uint64 (zeros (numel (vals), 1)); endif for i = 1:numel (vals) V(i) = vals{i}; endfor ok = true; endfunction ## Text, with NULL marking the nulls: dates, durations or strings function [V, ok] = textColumn (vals, isnull, locale) ok = true; txt = vals(! isnull); [dt, isdate, fmt] = cellfun (@(t) autoDate (t, locale), txt, ... 'UniformOutput', false); if (all ([isdate{:}])) V = NaT (numel (vals), 1); V.Format = fmt{1}; V(! isnull) = vertcat (dt{:}); return; endif isdur = cellfun (@isDuration, txt); if (all (isdur)) V = duration (nan (numel (vals), 1), 0, 0); V(! isnull) = duration (txt); return; endif if (any ([isdate{:}]) || any (isdur)) ## A date among other text is a cell of dates and text V = []; ok = false; return; endif V = repmat (string (missing), numel (vals), 1); V(! isnull) = string (txt); endfunction ## The JSON value X as one element of a cell under the value type VT, 'auto' ## or 'cell' function [v, errmsg] = element (x, vt, locale) errmsg = ''; switch (kind (x)) case 'null' v = missing; case 'object' [v, errmsg] = toDictionary (x, vt, 'auto', locale); case 'array' v = cell (numel (x), 1); for i = 1:numel (x) [v{i}, errmsg] = element (x{i}, 'auto', locale); if (! isempty (errmsg)) return; endif endfor case 'text' [dt, isdate] = autoDate (x, locale); if (isdate) v = dt; elseif (isDuration (x)) v = duration (x); else v = string (x); endif otherwise v = x; endswitch endfunction ## The values VALS converted to the value type VT, never parsing text into a ## number; what does not fit is the type's missing value function V = typedValues (vals, vt, locale) n = numel (vals); kinds = cellfun (@kind, vals, 'UniformOutput', false); switch (vt) case 'string' V = repmat (string (missing), n, 1); for i = 1:n switch (kinds{i}) case 'text' V(i) = string (vals{i}); case 'bool' V(i) = string (ifelse (vals{i}, 'true', 'false')); case {'num', 'int'} V(i) = string (vals{i}); endswitch endfor case 'logical' V = false (n, 1); for i = 1:n if (any (strcmp (kinds{i}, {'num', 'int', 'bool'}))) V(i) = vals{i} != 0 && ! isnan (double (vals{i})); endif endfor case 'datetime' dts = cell (n, 1); for i = 1:n if (strcmp (kinds{i}, 'text')) dts{i} = anyDate (vals{i}, locale); else dts{i} = NaT; endif endfor V = joinDates (dts); case 'duration' V = duration (nan (n, 1), 0, 0); for i = 1:n if (strcmp (kinds{i}, 'text') && isDuration (vals{i})) V(i) = duration (vals{i}); endif endfor otherwise ## The numeric types: NaN, which the integer types make 0, for text, ## arrays, objects and null V = nan (n, 1); if (any (strcmp (vt, {'int64', 'uint64'}))) V = zeros (n, 1, vt); endif ## A 64-bit integer assigned into a double array would make it one isfloat = any (strcmp (vt, {'double', 'single'})); for i = 1:n if (any (strcmp (kinds{i}, {'num', 'int', 'bool'}))) if (isfloat) V(i) = double (vals{i}); else V(i) = vals{i}; endif endif endfor V = cast (V, vt); endswitch endfunction function s = ifelse (tf, a, b) if (tf) s = a; else s = b; endif endfunction ## The text T as a date under 'auto': ISO 8601 without a zone, or ## dd-MMM-yyyy, with its display format function [dt, isdate, fmt] = autoDate (t, locale) dt = NaT; isdate = false; fmt = ''; ## Named tokens: Octave leaves out an optional group that took no part m = regexp (t, ['^(?\d{4})-(?\d{2})-(?\d{2})', ... '(T(?\d{2}):(?\d{2})(:(?\d{2}(\.\d+)?))?)?$'], ... 'names', 'once'); if (! isempty (m)) c = str2double ({m.Y, m.M, m.D, m.h, m.mi, m.s}); if (! validDate (c)) return; endif if (isempty (m.h)) dt = datetime (c(1), c(2), c(3)); fmt = 'uuuu-MM-dd'; else c(isnan (c)) = 0; dt = datetime (c(1), c(2), c(3), c(4), c(5), c(6)); fmt = "uuuu-MM-dd'T'HH:mm:ss"; if (any (m.s == '.')) fmt = "uuuu-MM-dd'T'HH:mm:ss.SSS"; endif endif dt.Format = fmt; isdate = ! isnat (dt); return; endif dmy = '^\d{1,2}-[^\W\d_]+-\d{4}( \d{2}:\d{2}:\d{2})?$'; if (! isempty (regexp (t, dmy, 'once'))) fmt = 'dd-MMM-uuuu'; if (any (t == ' ')) fmt = 'dd-MMM-uuuu HH:mm:ss'; endif dt = readDate (t, fmt, locale); ## A day beyond the month would roll into the next one isdate = ! isnat (dt) && day (dt) == str2double (strtok (t, '-')); if (isdate) dt.Format = fmt; endif endif endfunction ## The text T as a date under 'ValueType' 'datetime': the forms of 'auto', ## ISO 8601 with a zone mark keeping that zone, and the forms datetime reads ## for the locale; NaT when none fits function dt = anyDate (t, locale) [dt, isdate] = autoDate (t, locale); if (isdate) return; endif m = regexp (t, ['^(?\d{4})-(?\d{2})-(?\d{2})T(?\d{2}):', ... '(?\d{2})(:(?\d{2}(\.\d+)?))?', ... '(?Z|[+-]\d{2}:?\d{2})$'], 'names', 'once'); if (! isempty (m)) c = str2double ({m.Y, m.M, m.D, m.h, m.mi, m.s}); c(isnan (c)) = 0; if (! validDate (c)) return; endif if (strcmp (m.z, 'Z')) tz = 'UTC'; else tz = regexprep (m.z, '^([+-]\d{2}):?(\d{2})$', '$1:$2'); endif dt = datetime (c(1), c(2), c(3), c(4), c(5), c(6), 'TimeZone', tz); dt.Format = "uuuu-MM-dd'T'HH:mm:ssXXX"; return; endif dt = readDate (t, '', locale); endfunction ## The text T read by datetime in the format FMT, or in the format it ## detects in the language of LOCALE; NaT when it cannot function dt = readDate (t, fmt, locale) args = {}; if (! isempty (fmt)) args = {'InputFormat', fmt}; endif if (! isempty (locale)) args(end+1:end+2) = {'Locale', locale}; endif try dt = datetime (t, args{:}); catch dt = NaT; end_try_catch endfunction ## The dates DTS, one per value, joined when they share a zone, else kept ## as a cell for the dictionary to hold one per entry function V = joinDates (dts) zones = cellfun (@(x) x.TimeZone, dts, 'UniformOutput', false); nat = cellfun (@isnat, dts); z = unique (zones(! nat)); if (numel (z) <= 1) if (! isempty (z)) for i = find (nat)' dts{i}.TimeZone = z{1}; endfor endif V = vertcat (dts{:}); else V = dts; endif endfunction ## True for the date components C, year, month, day, hour, minute, second, ## within their ranges (absent ones NaN); datetime would roll them over function tf = validDate (c) c(isnan (c)) = 0; tf = (c(2) >= 1 && c(2) <= 12 && c(3) >= 1 && c(3) <= eomday (c(1), c(2)) && c(4) < 24 && c(5) < 60 && c(6) < 60); endfunction ## True for text written as a duration, hh:mm:ss function tf = isDuration (t) tf = ischar (t) && ! isempty (regexp (t, '^\d+:\d{2}:\d{2}(\.\d+)?$', ... 'once')); endfunction ## The dictionary in the OpenDocument spreadsheet FILENAME, one entry per ## sheet function d = readods (filename) s = ods2struct (filename); f = fieldnames (s); if (isempty (f)) d = dictionary (); return; endif keys = f; vals = cell (numel (f), 1); for i = 1:numel (f) t = s.(f{i}); p = t.Properties.CustomProperties; if (isprop (p, 'ActualSheetName') || isfield (p, 'ActualSheetName')) keys{i} = char (t.Properties.CustomProperties.ActualSheetName); t = rmprop (t, 'ActualSheetName'); endif vals{i} = t; endfor keys = string (keys); istt = cellfun (@(t) isa (t, 'timetable'), vals); if (all (istt) || ! any (istt)) d = dictionary (keys(1), vals{1}); for i = 2:numel (vals) d(keys(i)) = vals{i}; endfor else d = dictionary (keys, vals); endif endfunction %!demo %! ## `readdictionary` reads the JSON object in a file as a dictionary with %! ## `string` keys, in the order of the file. The value type follows the %! ## values: here they are all numbers, so they are `double`. %! %! f = [tempname() '.json']; %! text = '{"Boston": 55, "Denver": 48, "Dallas": 75}' %! fid = fopen (f, 'w'); %! fputs (fid, text); %! fclose (fid); %! temps = readdictionary (f) %! delete (f); %!demo %! ## Values of different kinds give `cell` values. An array becomes a column %! ## cell array and a nested object a dictionary of its own, so `{}` reaches %! ## into both; `null` is `missing`. %! %! f = [tempname() '.json']; %! text = ['{"id": 12345, "tags": ["new", "urgent"], ', ... %! '"sender": {"name": "Ann", "id": 7}, "reply": null}'] %! fid = fopen (f, 'w'); %! fputs (fid, text); %! fclose (fid); %! msg = readdictionary (f) %! msg{'tags'} %! sender = msg{'sender'} %! delete (f); %!demo %! ## Text in ISO 8601 form becomes dates, and `hh:mm:ss` durations. A %! ## timestamp marked with a time zone stays text, unless dates are asked for %! ## with `'ValueType'`, when each keeps the zone it states. %! %! f = [tempname() '.json']; %! text = '{"start": "2024-03-01", "end": "2024-03-15"}' %! fid = fopen (f, 'w'); %! fputs (fid, text); %! fclose (fid); %! dates = readdictionary (f) %! text = ['{"utc": "2024-03-01T10:00:00Z", ', ... %! '"local": "2024-03-01T12:00:00+02:00"}'] %! fid = fopen (f, 'w'); %! fputs (fid, text); %! fclose (fid); %! asText = readdictionary (f) %! asDates = readdictionary (f, 'ValueType', 'datetime') %! v = values (asDates, 'cell'); %! v{2}.TimeZone %! delete (f); %!demo %! ## `'DictionarySelector'` reads one object within the file by a JSON %! ## Pointer, counting array elements from 0, and `'DuplicateKeyRule'` says %! ## what a repeated key becomes: renamed by default, or the last value kept. %! %! f = [tempname() '.json']; %! text = '{"site": {"a": 1, "a": 2, "b": 3}, "runs": [{"x": 10}, {"x": 20}]}' %! fid = fopen (f, 'w'); %! fputs (fid, text); %! fclose (fid); %! readdictionary (f, 'DictionarySelector', '/site') %! readdictionary (f, 'DictionarySelector', '/site', ... %! 'DuplicateKeyRule', 'preserveLast') %! readdictionary (f, 'DictionarySelector', '/runs/1') %! delete (f); %!demo %! ## An OpenDocument spreadsheet gives one entry per sheet, the sheet name %! ## the key and its table or timetable the value, so a dictionary of tables %! ## written by `writedictionary` comes back as it was. %! %! sales = dictionary (string ({'North', 'South 2024'}), ... %! {table([10; 12], {'pens'; 'ink'}), ... %! table(7, {'tape'})}) %! f = [tempname() '.ods']; %! writedictionary (sales, f); %! back = readdictionary (f) %! back('South 2024') %! delete (f); ## Write TXT to a temporary file, read it, and delete the file %!function d = rj (txt, varargin) %! d = rjx (txt, '.json', varargin{:}); %!endfunction %!function d = rjx (txt, ext, varargin) %! f = [tempname() ext]; %! fid = fopen (f, 'w'); %! fputs (fid, txt); %! fclose (fid); %! unwind_protect %! d = readdictionary (f, varargin{:}); %! unwind_protect_cleanup %! delete (f); %! end_unwind_protect %!endfunction ## Measured on MATLAB Online R2026b, unless marked otherwise %!test %! r = rj ('{"a": 1, "b": 2}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = rj ('{"b": 1, "a": 2, "c": 3}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('b'); string('a'); string('c')}); %! assert_equal (values (r, 'cell'), {1; 2; 3}); %!test %! r = rj ('{"a": "x", "b": "y"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {string('x'); string('y')}); %!test %! r = rj ('{"a": true, "b": false}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'logical'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {true; false}); %!test %! r = rj ('{"a": 1, "b": true}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {{1}; {true}}); %!test %! r = rj ('{"a": 1, "b": "x"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {{1}; {string('x')}}); %!test %! r = rj ('{"a": 1, "b": null}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {1; NaN}); %!test %! r = rj ('{"a": "x", "b": null}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! assert_equal (vc4{1}, string ('x')); %! assert_equal (class (vc4{2}), 'string'); %! assert_equal (ismissing (vc4{2}), true); %!test %! r = rj ('{"a": null, "b": null}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'missing'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! assert_equal (class (vc4{1}), 'missing'); %! assert_equal (size (vc4{1}), [1, 1]); %! assert_equal (class (vc4{2}), 'missing'); %! assert_equal (size (vc4{2}), [1, 1]); %!test %! r = rj ('{"a": true, "b": null}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! assert_equal (vc4{1}, {true}); %! ce5 = vc4{2}; %! assert_equal (size (ce5), [1, 1]); %! assert_equal (class (ce5{1}), 'missing'); %! assert_equal (size (ce5{1}), [1, 1]); %!test %! r = rj ('{}'); %! assert_equal (isConfigured (r), false); %!test %! r = rj ('{"a": [1, 2, 3], "b": [4]}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {{{1; 2; 3}}; {{4}}}); %!test %! r = rj ('{"a": [1, "x"]}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {{{1; string('x')}}}); %!test %! r = rj ('{"a": []}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {{cell(0, 1)}}); %!test %! r = rj ('{"a": [[1, 2], [3, 4]]}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {{{{1; 2}; {3; 4}}}}); %!test %! r = rj ('{"a": [true, false]}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {{{true; false}}}); %!test %! r = rj ('{"a": ["x", "y"]}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {{{string('x'); string('y')}}}); %!test %! r = rj ('{"a": [1, null, 3]}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! ce6 = ce5{1}; %! assert_equal (size (ce6), [3, 1]); %! assert_equal (ce6{1}, 1); %! assert_equal (class (ce6{2}), 'missing'); %! assert_equal (size (ce6{2}), [1, 1]); %! assert_equal (ce6{3}, 3); %!test %! r = rj ('{"a": [{"x": 1}, {"y": 2}]}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! ce6 = ce5{1}; %! assert_equal (size (ce6), [2, 1]); %! dc7 = ce6{1}; %! [kt8, vt9] = types (dc7); %! assert_equal ([kt8, vt9], string ({'string', 'double'})); %! assert_equal (numEntries (dc7), 1); %! assert_equal (keys (dc7, 'cell'), {string('x')}); %! assert_equal (values (dc7, 'cell'), {1}); %! dc12 = ce6{2}; %! [kt13, vt14] = types (dc12); %! assert_equal ([kt13, vt14], string ({'string', 'double'})); %! assert_equal (numEntries (dc12), 1); %! assert_equal (keys (dc12, 'cell'), {string('y')}); %! assert_equal (values (dc12, 'cell'), {2}); %!test %! r = rj ('{"a": {"x": 1, "y": 2}}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! dc6 = ce5{1}; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'string', 'double'})); %! assert_equal (numEntries (dc6), 2); %! assert_equal (keys (dc6, 'cell'), {string('x'); string('y')}); %! assert_equal (values (dc6, 'cell'), {1; 2}); %!test %! r = rj ('{"a": {"x": 1}, "b": {"y": "s"}}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! dc6 = ce5{1}; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'string', 'double'})); %! assert_equal (numEntries (dc6), 1); %! assert_equal (keys (dc6, 'cell'), {string('x')}); %! assert_equal (values (dc6, 'cell'), {1}); %! ce11 = vc4{2}; %! assert_equal (size (ce11), [1, 1]); %! dc12 = ce11{1}; %! [kt13, vt14] = types (dc12); %! assert_equal ([kt13, vt14], string ({'string', 'string'})); %! assert_equal (numEntries (dc12), 1); %! assert_equal (keys (dc12, 'cell'), {string('y')}); %! assert_equal (values (dc12, 'cell'), {string('s')}); %!test %! r = rj ('{"a": {"b": {"c": 1}}}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! dc6 = ce5{1}; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'string', 'cell'})); %! assert_equal (numEntries (dc6), 1); %! assert_equal (keys (dc6, 'cell'), {string('b')}); %! vc10 = values (dc6, 'cell'); %! ce11 = vc10{1}; %! assert_equal (size (ce11), [1, 1]); %! dc12 = ce11{1}; %! [kt13, vt14] = types (dc12); %! assert_equal ([kt13, vt14], string ({'string', 'double'})); %! assert_equal (numEntries (dc12), 1); %! assert_equal (keys (dc12, 'cell'), {string('c')}); %! assert_equal (values (dc12, 'cell'), {1}); %!test %! r = rj ('{"a": {}, "b": 1}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! assert_equal (isConfigured (ce5{1}), false); %! assert_equal (vc4{2}, {1}); %!error ... %! rj ('[1, 2]'); %!error ... %! rj ('[{"a": 1}, {"b": 2}]'); %!error ... %! rj ('5'); %!error ... %! rj ('"x"'); %!test %! r = rj ('{"a": 1e20, "b": -0.5, "c": 3}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('c')}); %! assert_equal (values (r, 'cell'), {1e+20; -0.5; 3}); %!test %! r = rj ('{"a": 1e400}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {Inf}); %!test %! r = rj ('{"a": 12345678901234567890}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'uint64'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), ... %! {0xAB54A98CEB1F0AD2u64}); %!test %! r = rj (['{"a": NaN, "b": Infinity, "c": -Infinity, ', ... %! '"d": Inf, "e": -Inf}']); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 5); %! assert_equal (keys (r, 'cell'), ... %! {string('a'); string('b'); string('c'); string('d'); ... %! string('e')}); %! assert_equal (values (r, 'cell'), {NaN; Inf; -Inf; Inf; -Inf}); %!error ... %! rj (['{"a": NaN, "b": Infinity, "c": -Infinity, ', ... %! '"d": Inf, "e": -Inf}'], 'AllowInfAndNaN', false); %!error ... %! rj (['{"a": NaN, "b": Infinity, "c": -Infinity, ', ... %! '"d": Inf, "e": -Inf}'], 'ParsingMode', 'strict'); %!test %! r = rj ('{"a": "2024-01-01", "b": "2024-02-03"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, ''); %! x6 = vc4{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'2024-02-03 00:00:00.000'}); %! assert_equal (x6.TimeZone, ''); ## J2: ISO dates are read alike under ValueType datetime; R2026b gives NaT. %!test %! r = rj ('{"a": "2024-01-01", "b": "2024-02-03"}', 'ValueType', 'datetime'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, ''); %! x6 = vc4{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'2024-02-03 00:00:00.000'}); %! assert_equal (x6.TimeZone, ''); %!test %! r = rj ('{"a": "01-Jan-2024 10:00:00"}', 'ValueType', 'datetime'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 10:00:00.000'}); %! assert_equal (x5.TimeZone, ''); %!test %! r = rj ('{"a": "1. Januar 2024"}', 'ValueType', 'datetime', 'DateLocale', ... %! 'de_DE'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, ''); %!test %! r = rj ('{"a": "01:30:00", "b": "2:00:00"}', 'ValueType', 'duration'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'duration'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! assert_equal (class (vc4{1}), 'duration'); %! assert_equal (seconds (vc4{1}), 5400); %! assert_equal (class (vc4{2}), 'duration'); %! assert_equal (seconds (vc4{2}), 7200); %!test %! r = rj ('{"a": 1, "b": 2}', 'ValueType', 'cell'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {{1}; {2}}); %!test %! r = rj ('{"a": 1, "b": 2}', 'ValueType', 'string'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {string('1'); string('2')}); %!test %! r = rj ('{"a": 1, "b": 2}', 'ValueType', 'single'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'single'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {single(1); single(2)}); %!test %! r = rj ('{"a": 1, "b": 2}', 'ValueType', 'int8'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'int8'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {int8(1); int8(2)}); %!test %! r = rj ('{"a": 1, "b": 2}', 'ValueType', 'logical'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'logical'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {true; true}); %!test %! r = rj ('{"a": 1, "b": 2}', 'ValueType', 'datetime'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'NaT'}); %! assert_equal (x5.TimeZone, ''); %! x6 = vc4{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'NaT'}); %! assert_equal (x6.TimeZone, ''); %!test %! r = rj ('{"a": 1, "b": 2}', 'ValueType', 'duration'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'duration'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! assert_equal (class (vc4{1}), 'duration'); %! assert_equal (seconds (vc4{1}), NaN); %! assert_equal (class (vc4{2}), 'duration'); %! assert_equal (seconds (vc4{2}), NaN); %!test %! r = rj ('{"a": 1, "b": 2}', 'ValueType', 'uint64'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'uint64'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {uint64(1); uint64(2)}); %!test %! r = rj ('{"a": 1, "b": 2}', 'valuetype', 'cell'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {{1}; {2}}); %!error ... %! rj ('{"a": 1, "b": 2}', 'ValueType', 'struct'); %!test %! r = rj ('{"a": 300, "b": -5, "c": 1.5}', 'ValueType', 'int8'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'int8'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('c')}); %! assert_equal (values (r, 'cell'), {int8(127); int8(-5); int8(2)}); %!test %! r = rj ('{"a": 300, "b": -5, "c": 1.5}', 'ValueType', 'uint8'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'uint8'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('c')}); %! assert_equal (values (r, 'cell'), {uint8(255); uint8(0); uint8(2)}); %!test %! r = rj ('{"a": 0, "b": 1, "c": 2}', 'ValueType', 'logical'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'logical'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('c')}); %! assert_equal (values (r, 'cell'), {false; true; true}); %!test %! r = rj ('{"a": "5", "b": "x"}', 'ValueType', 'double'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {NaN; NaN}); %!test %! r = rj ('{"a": 1, "b": "x"}', 'ValueType', 'double'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {1; NaN}); %!test %! r = rj ('{"a": 1, "b": "x"}', 'ValueType', 'string'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {string('1'); string('x')}); %!test %! r = rj ('{"a": {"x": 1}}', 'ValueType', 'cell'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! dc6 = ce5{1}; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'string', 'cell'})); %! assert_equal (numEntries (dc6), 1); %! assert_equal (keys (dc6, 'cell'), {string('x')}); %! assert_equal (values (dc6, 'cell'), {{1}}); %!test %! r = rj ('{"a": 1, "a": 2, "b": 3}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('a_1'); string('b')}); %! assert_equal (values (r, 'cell'), {1; 2; 3}); %!test %! r = rj ('{"a": 1, "a": 2, "b": 3}', 'DuplicateKeyRule', 'preserveLast'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {2; 3}); %!test %! r = rj ('{"a": 1, "a": 2, "b": 3}', 'DuplicateKeyRule', 'makeUnique'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('a_1'); string('b')}); %! assert_equal (values (r, 'cell'), {1; 2; 3}); %!error ... %! rj ('{"a": 1, "a": 2, "b": 3}', 'DuplicateKeyRule', 'error'); %!test %! r = rj ('{"a": 1, "a": 2, "b": 3}', 'DuplicateKeyRule', 'auto'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('a_1'); string('b')}); %! assert_equal (values (r, 'cell'), {1; 2; 3}); %!test %! r = rj ('{"a": 1, "a": 2, "a": 3, "a_1": 4}', 'DuplicateKeyRule', ... %! 'makeUnique'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 4); %! assert_equal (keys (r, 'cell'), ... %! {string('a'); string('a_2'); string('a_3'); string('a_1')}); %! assert_equal (values (r, 'cell'), {1; 2; 3; 4}); %!test %! r = rj ('{"a": 1, "a": 2, "a": 3, "a_1": 4}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 4); %! assert_equal (keys (r, 'cell'), ... %! {string('a'); string('a_2'); string('a_3'); string('a_1')}); %! assert_equal (values (r, 'cell'), {1; 2; 3; 4}); %!test %! r = rj ('{"a": 1, "b": 2, "a": 3}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('a_1')}); %! assert_equal (values (r, 'cell'), {1; 2; 3}); %!test %! r = rj ('{"x": {"y": 1, "z": 2}, "w": [{"q": 2}, {"r": 3}]}', ... %! 'DictionarySelector', '/x'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('y'); string('z')}); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = rj ('{"x": {"y": 1, "z": 2}, "w": [{"q": 2}, {"r": 3}]}', ... %! 'DictionarySelector', '/w/1'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('r')}); %! assert_equal (values (r, 'cell'), {3}); %!error ... %! rj ('{"x": {"y": 1, "z": 2}, "w": [{"q": 2}, {"r": 3}]}', ... %! 'DictionarySelector', '/w'); %!error ... %! rj ('{"x": {"y": 1, "z": 2}, "w": [{"q": 2}, {"r": 3}]}', ... %! 'DictionarySelector', '/nope'); %!test %! r = rj ('{"x": {"y": 1, "z": 2}, "w": [{"q": 2}, {"r": 3}]}', ... %! 'DictionarySelector', ''); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('x'); string('w')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! dc6 = ce5{1}; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'string', 'double'})); %! assert_equal (numEntries (dc6), 2); %! assert_equal (keys (dc6, 'cell'), {string('y'); string('z')}); %! assert_equal (values (dc6, 'cell'), {1; 2}); %! ce11 = vc4{2}; %! assert_equal (size (ce11), [1, 1]); %! ce12 = ce11{1}; %! assert_equal (size (ce12), [2, 1]); %! dc13 = ce12{1}; %! [kt14, vt15] = types (dc13); %! assert_equal ([kt14, vt15], string ({'string', 'double'})); %! assert_equal (numEntries (dc13), 1); %! assert_equal (keys (dc13, 'cell'), {string('q')}); %! assert_equal (values (dc13, 'cell'), {2}); %! dc18 = ce12{2}; %! [kt19, vt20] = types (dc18); %! assert_equal ([kt19, vt20], string ({'string', 'double'})); %! assert_equal (numEntries (dc18), 1); %! assert_equal (keys (dc18, 'cell'), {string('r')}); %! assert_equal (values (dc18, 'cell'), {3}); %!error ... %! rj ('{"x": {"y": 1, "z": 2}, "w": [{"q": 2}, {"r": 3}]}', ... %! 'DictionarySelector', 'x'); %!test %! r = rj ('{"outer": {"inner": {"a": 1}}, "inner": {"b": 2}}', ... %! 'DictionaryNodeName', 'inner'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {1}); %!test %! r = rj ('{"outer": {"inner": {"a": 1}}, "inner": {"b": 2}}', ... %! 'DictionaryNodeName', 'outer'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('inner')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! dc6 = ce5{1}; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'string', 'double'})); %! assert_equal (numEntries (dc6), 1); %! assert_equal (keys (dc6, 'cell'), {string('a')}); %! assert_equal (values (dc6, 'cell'), {1}); %!error ... %! rj ('{"outer": {"inner": {"a": 1}}, "inner": {"b": 2}}', ... %! 'DictionaryNodeName', 'nope'); %!error ... %! rj ('{"outer": {"inner": {"a": 1}}, "inner": {"b": 2}}', ... %! 'DictionaryNodeName', 'inner', 'DictionarySelector', '/outer'); %!test %! r = rj (strjoin ({'{"a": 1, // note', '"b": 2, /* block */', '}'}, "\n")); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {1; 2}); %!error ... %! rj (strjoin ({'{"a": 1, // note', '"b": 2, /* block */', '}'}, "\n"), ... %! 'ParsingMode', 'strict'); %!error ... %! rj (strjoin ({'{"a": 1, // note', '"b": 2, /* block */', '}'}, "\n"), ... %! 'AllowComments', false); %!error ... %! rj (strjoin ({'{"a": 1, // note', '"b": 2, /* block */', '}'}, "\n"), ... %! 'AllowTrailingCommas', false); %!test %! r = rj (strjoin ({'{"a": 1, // note', '"b": 2, /* block */', '}'}, "\n"), ... %! 'ParsingMode', 'strict', 'AllowComments', true, ... %! 'AllowTrailingCommas', true); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {1; 2}); %!error ... %! rjx ('{"a": 1}', '.txt'); %!test %! r = rjx ('{"a": 1}', '.txt', 'FileType', 'json'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {1}); %!error ... %! rj (strjoin ({'{"a": 1, // note', '"b": 2, /* block */', '}'}, "\n"), ... %! 'FileType', 'text'); %!test %! r = rj ('{"": 1, "In-Reply-To": 2, "9x": 3, "ä": 4, "a b": 5}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 5); %! assert_equal (keys (r, 'cell'), ... %! {string(''); string('In-Reply-To'); string('9x'); ... %! string('ä'); string('a b')}); %! assert_equal (values (r, 'cell'), {1; 2; 3; 4; 5}); %!test %! r = rj (['{"a\"b": 1, "c": "line\nbreak", "e": "tab\tx", ', '"u": "é"}']); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 4); %! assert_equal (keys (r, 'cell'), ... %! {string('a"b'); string('c'); string('e'); string('u')}); %! vc4 = values (r, 'cell'); %! assert_equal (vc4{1}, {1}); %! ce5 = vc4{2}; %! assert_equal (size (ce5), [1, 1]); %! assert_equal (class (ce5{1}), 'string'); %! assert_equal (ismissing (ce5{1}), false); %! assert_equal (double (char (ce5{1}(1))), ... %! [108, 105, 110, 101, 10, 98, 114, 101, 97, 107]); %! ce6 = vc4{3}; %! assert_equal (size (ce6), [1, 1]); %! assert_equal (class (ce6{1}), 'string'); %! assert_equal (ismissing (ce6{1}), false); %! assert_equal (double (char (ce6{1}(1))), [116, 97, 98, 9, 120]); %! assert_equal (vc4{4}, {string('é')}); %!error ... %! rj ('{"a": 1'); %!error ... %! rj ('{"a": 1', 'Foo', 1); %!test %! r = rj ('{"a": "5", "b": "x", "c": 7}', 'ValueType', 'int8'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'int8'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('c')}); %! assert_equal (values (r, 'cell'), {int8(0); int8(0); int8(7)}); %!test %! r = rj ('{"a": "5", "b": "x", "c": 7}', 'ValueType', 'uint64'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'uint64'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('c')}); %! assert_equal (values (r, 'cell'), {uint64(0); uint64(0); uint64(7)}); %!test %! r = rj ('{"a": "5", "b": "x", "c": 7}', 'ValueType', 'single'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'single'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('c')}); %! assert_equal (values (r, 'cell'), {single(NaN); single(NaN); single(7)}); %!test %! r = rj ('{"a": "5", "b": "x", "c": 7}', 'ValueType', 'logical'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'logical'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('c')}); %! assert_equal (values (r, 'cell'), {false; false; true}); %!test %! r = rj ('{"a": "true", "b": "1", "c": 0}', 'ValueType', 'logical'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'logical'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('c')}); %! assert_equal (values (r, 'cell'), {false; false; false}); %!test %! r = rj ('{"a": "x", "b": "01:30:00"}', 'ValueType', 'duration'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'duration'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! assert_equal (class (vc4{1}), 'duration'); %! assert_equal (seconds (vc4{1}), NaN); %! assert_equal (class (vc4{2}), 'duration'); %! assert_equal (seconds (vc4{2}), 5400); %!test %! r = rj ('{"a": "x", "b": "01-Jan-2024"}', 'ValueType', 'datetime'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'NaT'}); %! assert_equal (x5.TimeZone, ''); %! x6 = vc4{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'2024-01-01 00:00:00.000'}); %! assert_equal (x6.TimeZone, ''); %!test %! r = rj ('{"a": true, "b": false, "c": null}', 'ValueType', 'string'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('c')}); %! vc4 = values (r, 'cell'); %! assert_equal (vc4{1}, string ('true')); %! assert_equal (vc4{2}, string ('false')); %! assert_equal (class (vc4{3}), 'string'); %! assert_equal (ismissing (vc4{3}), true); %!test %! r = rj ('{"a": true, "b": null}', 'ValueType', 'double'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {1; NaN}); %!test %! r = rj ('{"a": 1, "b": null}', 'ValueType', 'int8'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'int8'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {int8(1); int8(0)}); %!test %! r = rj ('{"a": 1, "b": null}', 'ValueType', 'logical'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'logical'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {true; false}); %!test %! r = rj ('{"a": 1, "b": null}', 'ValueType', 'datetime'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'NaT'}); %! assert_equal (x5.TimeZone, ''); %! x6 = vc4{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'NaT'}); %! assert_equal (x6.TimeZone, ''); %!test %! r = rj ('{"a": [1, 2], "b": 3}', 'ValueType', 'double'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {NaN; 3}); %!test %! r = rj ('{"a": {"x": 1}, "b": 3}', 'ValueType', 'double'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {NaN; 3}); %!test %! r = rj ('{"a": [1, 2], "b": "s"}', 'ValueType', 'string'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! assert_equal (class (vc4{1}), 'string'); %! assert_equal (ismissing (vc4{1}), true); %! assert_equal (vc4{2}, string ('s')); %!test %! r = rj ('{"a": {"x": 1}}', 'ValueType', 'string'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! assert_equal (class (vc4{1}), 'string'); %! assert_equal (ismissing (vc4{1}), true); %!test %! r = rj ('{"a": 1, "b": "x", "c": null}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('c')}); %! vc4 = values (r, 'cell'); %! assert_equal (vc4{1}, {1}); %! assert_equal (vc4{2}, {string('x')}); %! ce5 = vc4{3}; %! assert_equal (size (ce5), [1, 1]); %! assert_equal (class (ce5{1}), 'missing'); %! assert_equal (size (ce5{1}), [1, 1]); %!test %! r = rj ('{"a": [true, null]}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! ce6 = ce5{1}; %! assert_equal (size (ce6), [2, 1]); %! assert_equal (ce6{1}, true); %! assert_equal (class (ce6{2}), 'missing'); %! assert_equal (size (ce6{2}), [1, 1]); %!test %! r = rj ('{"a": ["x", null]}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! ce6 = ce5{1}; %! assert_equal (size (ce6), [2, 1]); %! assert_equal (ce6{1}, string ('x')); %! assert_equal (class (ce6{2}), 'missing'); %! assert_equal (size (ce6{2}), [1, 1]); %!test %! r = rj ('{"a": [1, 2, 3]}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {{{1; 2; 3}}}); %!test %! r = rj ('{"a": [[1, 2], [3]]}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {{{{1; 2}; {3}}}}); %!test %! r = rj ('{"a": {"x": null}}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! dc6 = ce5{1}; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'string', 'missing'})); %! assert_equal (numEntries (dc6), 1); %! assert_equal (keys (dc6, 'cell'), {string('x')}); %! vc10 = values (dc6, 'cell'); %! assert_equal (class (vc10{1}), 'missing'); %! assert_equal (size (vc10{1}), [1, 1]); %!test %! r = rj ('{"a": {"x": 1}, "b": null}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! dc6 = ce5{1}; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'string', 'double'})); %! assert_equal (numEntries (dc6), 1); %! assert_equal (keys (dc6, 'cell'), {string('x')}); %! assert_equal (values (dc6, 'cell'), {1}); %! ce11 = vc4{2}; %! assert_equal (size (ce11), [1, 1]); %! assert_equal (class (ce11{1}), 'missing'); %! assert_equal (size (ce11{1}), [1, 1]); %!test %! r = rj ('{"a": [1], "b": null}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! assert_equal (vc4{1}, {{1}}); %! ce5 = vc4{2}; %! assert_equal (size (ce5), [1, 1]); %! assert_equal (class (ce5{1}), 'missing'); %! assert_equal (size (ce5{1}), [1, 1]); %!test %! r = rj ('{"a": "2024-01-01", "b": null}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, ''); %! x6 = vc4{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'NaT'}); %! assert_equal (x6.TimeZone, ''); %!test %! r = rj ('{"a": 12345678901234567890, "b": null}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! assert_equal (vc4{1}, ... %! {0xAB54A98CEB1F0AD2u64}); %! ce5 = vc4{2}; %! assert_equal (size (ce5), [1, 1]); %! assert_equal (class (ce5{1}), 'missing'); %! assert_equal (size (ce5{1}), [1, 1]); %!test %! r = rj ('{"a": null}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'missing'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! assert_equal (class (vc4{1}), 'missing'); %! assert_equal (size (vc4{1}), [1, 1]); %!test %! r = rj ('{"a": 1, "b": 12345678901234567890}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'uint64'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), ... %! {uint64(1); ... %! 0xAB54A98CEB1F0AD2u64}); %!test %! r = rj ('{"a": 1.5, "b": 12345678901234567890}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), ... %! {{1.5}; ... %! {0xAB54A98CEB1F0AD2u64}}); %!test %! r = rj ('{"a": -12345678901234567890}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {-1.2345678901234567e+19}); %!test %! r = rj ('{"a": -9223372036854775808, "b": 9007199254740993}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'int64'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), ... %! {intmin('int64'); 0x20000000000001s64}); %!test %! r = rj ('{"a": 9007199254740993}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'uint64'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), ... %! {0x20000000000001u64}); %!test %! r = rj ('{"a": 1e2, "b": 100}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {100; 100}); %!test %! r = rj ('{"a": "2024-01-01T10:00:00Z"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {string('2024-01-01T10:00:00Z')}); %!test %! r = rj ('{"a": "2024-01-01T10:00:00"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 10:00:00.000'}); %! assert_equal (x5.TimeZone, ''); %!test %! r = rj ('{"a": "2024-01-01T10:00:00+02:00"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {string('2024-01-01T10:00:00+02:00')}); %!test %! r = rj ('{"a": "2024-01-01", "b": "x"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! x6 = ce5{1}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'2024-01-01 00:00:00.000'}); %! assert_equal (x6.TimeZone, ''); %! assert_equal (vc4{2}, {string('x')}); %!test %! r = rj ('{"a": "01-Jan-2024", "b": "02-Jan-2024"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, ''); %! x6 = vc4{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'2024-01-02 00:00:00.000'}); %! assert_equal (x6.TimeZone, ''); %!test %! r = rj ('{"a": "01:30:00", "b": "02:00:00"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'duration'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! assert_equal (class (vc4{1}), 'duration'); %! assert_equal (seconds (vc4{1}), 5400); %! assert_equal (class (vc4{2}), 'duration'); %! assert_equal (seconds (vc4{2}), 7200); %!test %! r = rj ('{"a": "1 hr", "b": "2 hr"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {string('1 hr'); string('2 hr')}); %!test %! r = rj ('{"a": "2024-01-01"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, ''); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": 1}', 'FileType'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": 1}', 'ValueType', 5); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": 1}', 'ValueType', 'CELL'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {{1}}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": 1}', 'filetype', 'JSON'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {1}); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": 1}', 'DuplicateKeyRule', 'x'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": 1}', 'ParsingMode', 'x'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": 1}', 'AllowComments', 'yes'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": 1}', 'AllowTrailingCommas', [true, true]); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": 1}', 'AllowInfAndNaN', NaN); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": 1}', 'AllowInfAndNaN', 1); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {1}); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": 1}', 'DateLocale', 5); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": 1}', 'DictionarySelector', 5); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": 1}', 'DictionaryNodeName', 5); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": 1}', 'WebOptions', 1); ## Ours: not measured on R2026b. %!error ... %! rjx ('{"a": 1}', '.gz'); ## Ours: not measured on R2026b. %!test %! r = rj ('{"x": {"y": 1, "z": 2}, "w": [{"q": 2}, {"r": 3}]}', ... %! 'DictionarySelector', '/w/0'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('q')}); %! assert_equal (values (r, 'cell'), {2}); ## Ours: not measured on R2026b. %!error ... %! rj ('{"x": {"y": 1, "z": 2}, "w": [{"q": 2}, {"r": 3}]}', ... %! 'DictionarySelector', '/w/2'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"x": {"y": 1, "z": 2}, "w": [{"q": 2}, {"r": 3}]}', ... %! 'DictionarySelector', '/w/01'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"x": {"y": 1, "z": 2}, "w": [{"q": 2}, {"r": 3}]}', ... %! 'DictionarySelector', '/x/y'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"x": {"y": 1, "z": 2}, "w": [{"q": 2}, {"r": 3}]}', ... %! 'DictionarySelector', '/'); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a/b": {"c": 1}, "m~n": {"d": 2}}', 'DictionarySelector', '/a~1b'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('c')}); %! assert_equal (values (r, 'cell'), {1}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a/b": {"c": 1}, "m~n": {"d": 2}}', 'DictionarySelector', '/m~0n'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('d')}); %! assert_equal (values (r, 'cell'), {2}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"arr": [{"t": {"q": 1}}]}', 'DictionaryNodeName', 't'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('q')}); %! assert_equal (values (r, 'cell'), {1}); ## Ours: not measured on R2026b. %!error ... %! rj ('{"t": 5}', 'DictionaryNodeName', 't'); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": 1, "b": 2, "a": 3}', 'DuplicateKeyRule', 'preserveLast'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {3; 2}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": 1, "a": 2, "a_1": 3, "a_2": 4, "a": 5}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 5); %! assert_equal (keys (r, 'cell'), ... %! {string('a'); string('a_3'); string('a_1'); string('a_2'); ... %! string('a_4')}); %! assert_equal (values (r, 'cell'), {1; 2; 3; 4; 5}); ## Ours: not measured on R2026b. %!error ... %! rj ('{"b": 1, "a": 2, "b": 3}', 'DuplicateKeyRule', 'error'); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": {"x": 1, "x": 2}}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! dc6 = ce5{1}; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'string', 'double'})); %! assert_equal (numEntries (dc6), 2); %! assert_equal (keys (dc6, 'cell'), {string('x'); string('x_1')}); %! assert_equal (values (dc6, 'cell'), {1; 2}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": -1, "b": 9223372036854775807}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'int64'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {int64(-1); intmax('int64')}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": -1, "b": 9223372036854775808}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), ... %! {{-1}; {0x8000000000000000u64}}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": 18446744073709551615}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'uint64'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {intmax('uint64')}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": 18446744073709551616}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {1.8446744073709552e+19}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": 9007199254740992}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {9007199254740992.0}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": 12345678901234567890, "b": 1}', 'ValueType', 'int64'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'int64'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {intmax('int64'); int64(1)}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": 12345678901234567890}', 'ValueType', 'double'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {1.2345678901234567e+19}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": 12345678901234567890}', 'ValueType', 'string'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {string('12345678901234567890')}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": 1.5, "b": -2}', 'ValueType', 'string'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {string('1.5'); string('-2')}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": NaN, "b": 0, "c": 2}', 'ValueType', 'logical'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'logical'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('c')}); %! assert_equal (values (r, 'cell'), {false; false; true}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": [1], "b": {"x": 1}, "c": null}', 'ValueType', 'logical'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'logical'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('c')}); %! assert_equal (values (r, 'cell'), {false; false; false}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": [1], "b": {"x": 1}, "c": null}', 'ValueType', 'duration'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'duration'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('c')}); %! vc4 = values (r, 'cell'); %! assert_equal (class (vc4{1}), 'duration'); %! assert_equal (seconds (vc4{1}), NaN); %! assert_equal (class (vc4{2}), 'duration'); %! assert_equal (seconds (vc4{2}), NaN); %! assert_equal (class (vc4{3}), 'duration'); %! assert_equal (seconds (vc4{3}), NaN); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": true, "b": false}', 'ValueType', 'uint8'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'uint8'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {uint8(1); uint8(0)}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": true}', 'ValueType', 'datetime'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'NaT'}); %! assert_equal (x5.TimeZone, ''); ## Ours: not measured on R2026b. %!test %! r = rj (['{"a": "2024-01-01T10:00:00.250", ', ... %! '"b": "2024-01-02T11:30:00.500"}']); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 10:00:00.250'}); %! assert_equal (x5.TimeZone, ''); %! x6 = vc4{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'2024-01-02 11:30:00.500'}); %! assert_equal (x6.TimeZone, ''); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": "2024-01-01", "b": "01-Jan-2024"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, ''); %! x6 = vc4{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'2024-01-01 00:00:00.000'}); %! assert_equal (x6.TimeZone, ''); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": "01-Jan-2024 10:00:00"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 10:00:00.000'}); %! assert_equal (x5.TimeZone, ''); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": "2024-13-01"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {string('2024-13-01')}); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": "01:30:00", "b": null}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'duration'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! assert_equal (class (vc4{1}), 'duration'); %! assert_equal (seconds (vc4{1}), 5400); %! assert_equal (class (vc4{2}), 'duration'); %! assert_equal (seconds (vc4{2}), NaN); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": "01:30:00", "b": "x"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! assert_equal (class (ce5{1}), 'duration'); %! assert_equal (seconds (ce5{1}), 5400); %! assert_equal (vc4{2}, {string('x')}); ## Ours: not measured on R2026b. %!test %! r = rj (['{"a": "2024-01-01T10:00:00Z", ', ... %! '"b": "2024-01-01T10:00:00+02:00", ', ... %! '"c": "2024-01-01T10:00:00", "d": "no"}'], 'ValueType', 'datetime'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 4); %! assert_equal (keys (r, 'cell'), ... %! {string('a'); string('b'); string('c'); string('d')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 10:00:00.000'}); %! assert_equal (x5.TimeZone, 'UTC'); %! x6 = vc4{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'2024-01-01 10:00:00.000'}); %! assert_equal (x6.TimeZone, '+02:00'); %! x7 = vc4{3}; %! x7.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x7), {'2024-01-01 10:00:00.000'}); %! assert_equal (x7.TimeZone, ''); %! x8 = vc4{4}; %! x8.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x8), {'NaT'}); %! assert_equal (x8.TimeZone, ''); ## Ours: not measured on R2026b. %!test %! r = rj (['{"a": "2024-01-01T10:00:00Z", ', ... %! '"b": "2024-06-01T08:00:00Z", "c": null}'], 'ValueType', ... %! 'datetime'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {string('a'); string('b'); string('c')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 10:00:00.000'}); %! assert_equal (x5.TimeZone, 'UTC'); %! x6 = vc4{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'2024-06-01 08:00:00.000'}); %! assert_equal (x6.TimeZone, 'UTC'); %! x7 = vc4{3}; %! x7.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x7), {'NaT'}); %! assert_equal (x7.TimeZone, 'UTC'); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": "2024-01-01T10:00:00+0530"}', 'ValueType', 'datetime'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'datetime'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 10:00:00.000'}); %! assert_equal (x5.TimeZone, '+05:30'); ## Ours: not measured on R2026b. %!test %! r = rj (['{"a": ["2024-01-01", "x", "01:30:00", null, ', ... %! 'true, 1, 12345678901234567890]}']); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! ce6 = ce5{1}; %! assert_equal (size (ce6), [7, 1]); %! x7 = ce6{1}; %! x7.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x7), {'2024-01-01 00:00:00.000'}); %! assert_equal (x7.TimeZone, ''); %! assert_equal (ce6{2}, string ('x')); %! assert_equal (class (ce6{3}), 'duration'); %! assert_equal (seconds (ce6{3}), 5400); %! assert_equal (class (ce6{4}), 'missing'); %! assert_equal (size (ce6{4}), [1, 1]); %! assert_equal (ce6{5}, true); %! assert_equal (ce6{6}, 1); %! assert_equal (ce6{7}, 0xAB54A98CEB1F0AD2u64); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": {"x": "2024-01-01"}}', 'ValueType', 'cell'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! dc6 = ce5{1}; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'string', 'cell'})); %! assert_equal (numEntries (dc6), 1); %! assert_equal (keys (dc6, 'cell'), {string('x')}); %! vc10 = values (dc6, 'cell'); %! ce11 = vc10{1}; %! assert_equal (size (ce11), [1, 1]); %! x12 = ce11{1}; %! x12.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x12), {'2024-01-01 00:00:00.000'}); %! assert_equal (x12.TimeZone, ''); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": {"b": {"c": {}}}}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! ce5 = vc4{1}; %! assert_equal (size (ce5), [1, 1]); %! dc6 = ce5{1}; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'string', 'cell'})); %! assert_equal (numEntries (dc6), 1); %! assert_equal (keys (dc6, 'cell'), {string('b')}); %! vc10 = values (dc6, 'cell'); %! ce11 = vc10{1}; %! assert_equal (size (ce11), [1, 1]); %! dc12 = ce11{1}; %! [kt13, vt14] = types (dc12); %! assert_equal ([kt13, vt14], string ({'string', 'cell'})); %! assert_equal (numEntries (dc12), 1); %! assert_equal (keys (dc12, 'cell'), {string('c')}); %! vc16 = values (dc12, 'cell'); %! ce17 = vc16{1}; %! assert_equal (size (ce17), [1, 1]); %! assert_equal (isConfigured (ce17{1}), false); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a" 1}'); ## Ours: not measured on R2026b. %!error ... %! rj ('{a: 1}'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": 01}'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": 1.}'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": "x}'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": "\q"}'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": "\u12"}'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": "\ud83d"}'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": 1} x'); ## Ours: not measured on R2026b. %!error ... %! rj (''); ## Ours: not measured on R2026b. %!error ... %! rj ('/* only */'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": 1} /* tail'); ## Ours: not measured on R2026b. %!error ... %! rj ('{"a": -}'); ## Ours: not measured on R2026b. %!error ... %! rj ('[1 2]'); ## Ours: not measured on R2026b. %!test %! r = rj ('{"a": "\ud83d\ude00", "b": "\u00e9\/"}'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {string('😀'); string('é/')}); ## Ours: not measured on R2026b. %!error ... %! rj (sprintf ('{"a": "x\ny"}')); ## Ours: not measured on R2026b. %!test %! r = rj ([char([239, 187, 191]), '{"bom": 1}']); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('bom')}); %! assert_equal (values (r, 'cell'), {1}); ## Ours: not measured on R2026b. %!test %! r = rj (['{"a": "2024-13-01", "b": "2024-02-30", ', ... %! '"c": "31-Feb-2024", "d": "2024-01-01T24:00:00"}']); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 4); %! assert_equal (keys (r, 'cell'), ... %! {string('a'); string('b'); string('c'); string('d')}); %! assert_equal (values (r, 'cell'), ... %! {string('2024-13-01'); string('2024-02-30'); ... %! string('31-Feb-2024'); string('2024-01-01T24:00:00')}); ## The nesting the parser allows, which readdictionary follows %!test %! r = rj ([repmat('{"a": ', 1, 63), '[1]', repmat('}', 1, 63)]); %! assert_equal (numEntries (r), 1); %!error ... %! rj ([repmat('{"a": ', 1, 64), '[1]', repmat('}', 1, 64)]); ## File names and locations %!error ... %! readdictionary (5); %!error ... %! readdictionary ('/nonexistent/x.json'); %!error ... %! readdictionary ('s3://bucket/x.json'); %!error ... %! readdictionary ('wasbs://box@account/x.json'); %!error ... %! readdictionary ('hdfs:///data/x.json'); %!error ... %! readdictionary ('/nonexistent/x'); ## OpenDocument spreadsheets, an extension of this package %!test %! s.Sales = table ([1; 2]); %! s.Costs = table ([3; 4]); %! f = [tempname() '.ods']; %! unwind_protect %! struct2ods (f, s); %! r = readdictionary (f); %! assert_equal (keys (r), string ({'Sales'; 'Costs'})); %! assert_equal (types (r), string ('string')); %! [~, vt] = types (r); %! assert_equal (vt, string ('table')); %! assert_equal (r(string ('Costs')).Var1, [3; 4]); %! unwind_protect_cleanup %! delete (f); %! end_unwind_protect %!test %! s.T = timetable (hours ([1; 2]), [3; 4]); %! f = [tempname() '.ods']; %! unwind_protect %! struct2ods (f, s); %! r = readdictionary (f, 'FileType', 'ods'); %! [~, vt] = types (r); %! assert_equal (vt, string ('timetable')); %! assert_equal (r(string ('T')).Time, hours ([1; 2])); %! unwind_protect_cleanup %! delete (f); %! end_unwind_protect %!test %! s.T = table (1); %! s.TT = timetable (hours (1), 2); %! f = [tempname() '.ods']; %! unwind_protect %! struct2ods (f, s); %! r = readdictionary (f); %! [~, vt] = types (r); %! assert_equal (vt, string ('cell')); %! assert_equal (class (r{string('TT')}), 'timetable'); %! unwind_protect_cleanup %! delete (f); %! end_unwind_protect %!test %! t = addprop (table (5), 'ActualSheetName', 'table'); %! t.Properties.CustomProperties.ActualSheetName = 'Q 2024'; %! s.Q_2024 = t; %! f = [tempname() '.ods']; %! unwind_protect %! struct2ods (f, s); %! r = readdictionary (f); %! assert_equal (keys (r), string ('Q 2024')); %! p = properties (r(string ('Q 2024')).Properties.CustomProperties); %! assert_equal (any (strcmp (p, 'ActualSheetName')), false); %! unwind_protect_cleanup %! delete (f); %! end_unwind_protect %!error ... %! readdictionary ('/nonexistent/x.ods', 'ValueType', 'cell'); pr0m1th3as-datatypes-9450b29/inst/readtable.m000066400000000000000000001040571527370255000207630ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{tbl} =} readtable (@var{filename}) ## @deftypefnx {datatypes} {@var{tbl} =} readtable (@var{filename}, @var{Name}, @var{Value}) ## ## Read a file into a table, detecting variable types automatically. ## ## @code{@var{tbl} = readtable (@var{filename})} reads the file named by ## @var{filename} (a character vector or string scalar) and returns it as a ## @code{table}. The file type is inferred from the extension: ## @qcode{.txt}, @qcode{.csv}, and @qcode{.dat} are read as delimited text; ## @qcode{.ods} and @qcode{.fods} are read as OpenDocument spreadsheets. Use ## the @qcode{'FileType'} option to override the inferred type. ## ## By default the first row supplies the variable names and each column's data ## type is detected automatically (numeric, @code{datetime}, @code{duration}, or ## text). The following @var{Name}-@var{Value} options are supported: ## ## @multitable @columnfractions 0.28 0.72 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'FileType'} @tab @qcode{'text'} or @qcode{'spreadsheet'}. ## @item @qcode{'ReadVariableNames'} @tab Logical; read the first row as ## variable names (default @qcode{true}). ## @item @qcode{'ReadRowNames'} @tab Logical; read the first column as row names ## (default @qcode{false}). ## @item @qcode{'Delimiter'} @tab Field delimiter for text files: a single ## character or one of @qcode{'comma'}, @qcode{'space'}, @qcode{'tab'}, ## @qcode{'semi'}, @qcode{'bar'} (default @qcode{','}). ## @item @qcode{'NumHeaderLines'} @tab Number of lines to skip before the header ## (default @qcode{0}). ## @item @qcode{'TextType'} @tab @qcode{'char'} or @qcode{'string'} for text ## columns (default @qcode{'char'}). ## @item @qcode{'VariableNamingRule'} @tab @qcode{'modify'} or ## @qcode{'preserve'} (default @qcode{'modify'}). ## @item @qcode{'Sheet'} @tab Spreadsheet only: the sheet to read, selected by ## name or by a 1-based index over the data sheets (default: the first sheet). ## @item @qcode{'Range'} @tab Spreadsheet only: an A1-style range such as ## @qcode{'C5'} or @qcode{'C5:D8'} limiting the region read. ## @item @qcode{'VariableNamesRow'} @tab A nonnegative integer naming the line ## or row that holds the variable names (default @qcode{1}). Zero is ## equivalent to @qcode{'ReadVariableNames'}, @qcode{false}. ## @item @qcode{'VariableNamesLine'} @tab MATLAB's spelling of ## @qcode{'VariableNamesRow'} for a text file, and an exact alias of it. ## Passing both names raises an error rather than one silently winning. ## @item @qcode{'RowNamesColumn'} @tab A nonnegative integer naming the column ## that holds the row names (default @qcode{1}), used when ## @qcode{'ReadRowNames'} is @qcode{true}. Zero is equivalent to ## @qcode{'ReadRowNames'}, @qcode{false}. ## @end multitable ## ## An OpenDocument file written by @code{table2ods} carries the package's own ## metadata, which names and types every variable exactly and leaves the data ## sheet without a header row. Such a file is read by @code{ods2table}, so it ## comes back whole rather than being read positionally, which would take its ## first row of data for the variable names. An explicit @qcode{'Range'} asks ## for a block of the sheet instead and is read positionally as usual. ## ## Office Open XML spreadsheets (@qcode{.xlsx}, @qcode{.xlsm}) are read via the ## same interface as ODS. The legacy binary formats @qcode{.xls} and ## @qcode{.xlsb} are not supported; use @qcode{.xlsx}, @qcode{.ods}, or a text ## format. ## ## @seealso{table.writetable, csv2table, ods2table} ## @end deftypefn function tbl = readtable (filename, varargin) if (nargin < 1) print_usage (); endif if (! ((ischar (filename) && isvector (filename)) ... || (isa (filename, 'string') && isscalar (filename)))) error ("readtable: FILENAME must be a character vector or string scalar."); endif file = char (filename); if (mod (numel (varargin), 2) != 0) error ("readtable: name-value arguments must be in pairs."); endif ## Parse optional paired arguments optNames = {'FileType', 'ReadVariableNames', 'ReadRowNames', 'Delimiter', ... 'NumHeaderLines', 'TextType', 'VariableNamingRule', 'Sheet', ... 'Range', 'VariableNamesRow', 'RowNamesColumn', ... 'VariableNamesLine'}; dfValues = {'', true, false, ',', 0, 'char', 'modify', [], '', [], 1, []}; [fileType, readVarNames, readRowNames, delim, numHeaderLines, textType, ... namingRule, sheet, range, varNamesRow, rowNamesCol, varNamesLine, args] = ... parsePairedArguments (optNames, dfValues, ... varargin(:)); varNamesRow = __namesrow__ (varNamesRow, varNamesLine, 'readtable'); if (! isempty (args)) error ("readtable: invalid optional paired argument."); endif if (isa (sheet, 'string')) sheet = char (sheet); endif if (isa (range, 'string')) range = char (range); endif if (! (isnumeric (rowNamesCol) && isscalar (rowNamesCol) && rowNamesCol == fix (rowNamesCol) && rowNamesCol >= 0)) error (strcat ("readtable: 'RowNamesColumn' must be a non-negative", ... " integer.")); endif ## Resolve the file type from the option or the extension [~, ~, ext] = fileparts (file); if (isempty (fileType)) switch (lower (ext)) case {'.txt', '.csv', '.dat'} fileType = 'text'; case {'.ods', '.fods', '.xlsx', '.xlsm'} fileType = 'spreadsheet'; case {'.xls', '.xlsb'} error (strcat ("readtable: '%s' Excel files are not supported; use", ... " '.xlsx', '.ods', or a text format."), ext); otherwise error (strcat ("readtable: cannot infer the file type from '%s';", ... " specify 'FileType'."), ext); endswitch endif ## Office Open XML (.xlsx/.xlsm) uses a separate reader from ODS. isXlsx = any (strcmpi (ext, {'.xlsx', '.xlsm'})); ## 'Sheet' and 'Range' apply only to spreadsheets; MATLAB rejects them on text ## files rather than silently ignoring them. if (strcmpi (fileType, 'text') && (! isempty (sheet) || ! isempty (range))) error (strcat ("readtable: 'Sheet' and 'Range' are not supported for", ... " text files.")); endif switch (lower (fileType)) case 'text' d = resolve_delimiter (delim); ## Hexadecimal auto-detection is a csv2table extension that MATLAB's ## readtable does not perform, so keep hex-like strings as text. tbl = csv2table (file, 'ReadVariableNames', readVarNames, ... 'ReadRowNames', readRowNames, 'RowNamesColumn', rowNamesCol, ... 'VariableNamesRow', varNamesRow, 'NumHeaderLines', numHeaderLines, ... 'TextType', textType, 'VariableNamingRule', namingRule, 'Delimiter', ... d, 'HexType', 'text'); case 'spreadsheet' tbl = read_spreadsheet (file, readVarNames, readRowNames, textType, ... namingRule, sheet, range, isXlsx, ... varNamesRow, rowNamesCol); otherwise error ("readtable: 'FileType' must be 'text' or 'spreadsheet'."); endswitch endfunction ## Translate a MATLAB delimiter (named or literal) into a single character. function d = resolve_delimiter (delim) if (isa (delim, 'string')) delim = char (delim); endif if (! ischar (delim)) error ("readtable: 'Delimiter' must be a character vector or string."); endif switch (lower (delim)) case {'comma', ','} d = ','; case {'space', ' '} d = ' '; case {'tab', "\t"} d = "\t"; case {'semi', ';'} d = ';'; case {'bar', '|'} d = '|'; otherwise if (isscalar (delim)) d = delim; else error ("readtable: unsupported 'Delimiter' value '%s'.", delim); endif endswitch endfunction ## Read an OpenDocument spreadsheet as a plain sheet: variable names from the ## first row (when requested), then one column per sheet column with its type ## taken from the native ODS cell value types. function tbl = read_spreadsheet (file, readVarNames, readRowNames, textType, ... namingRule, sheet, range, isXlsx, ... varNamesRow, rowNamesCol) ## Either switch turns its position off; the position alone can too. if (! readVarNames) varNamesRow = 0; endif if (! readRowNames) rowNamesCol = 0; endif if (isXlsx) [data, vtype] = __xlsx2table__ (file, sheet); else [data, vtype, meta] = __ods2table__ (file, sheet); if (ischar (data)) error ("readtable: %s", data); endif ## A file written by 'table2ods' carries the package's own metadata sheet, ## so its variable names, types and row names are known exactly and its ## data sheet holds no header row. Read it with 'ods2table' rather than ## positionally, which would take the first row of data for the names; the ## caller's own 'ReadVariableNames' and 'ReadRowNames' still decide what is ## taken from it. An explicit 'Range' asks for a block of the sheet ## instead, and is honoured. if (! isempty (meta) && isempty (range)) odsargs = {'ReadVariableNames', readVarNames, ... 'ReadRowNames', readRowNames, ... 'VariableNamesRow', varNamesRow, ... 'RowNamesColumn', rowNamesCol}; if (! isempty (sheet)) odsargs = [{'Sheet', sheet}, odsargs]; endif tbl = ods2table (file, odsargs{:}); return; endif endif if (ischar (data)) error ("readtable: %s", data); endif ## Apply an explicit 'Range' (absolute A1 coordinates), otherwise auto-trim ## the sheet to its used block so leading blank rows/columns are ignored. if (! isempty (range)) [data, vtype] = clip_range (data, vtype, range); else [data, vtype] = trim_used_block (data, vtype); endif ncols = size (data, 2); if (ncols == 0) tbl = table (); return; endif ## Variable names from the row the caller named if (varNamesRow > 0 && size (data, 1) >= varNamesRow) names = cell (1, ncols); for c = 1:ncols x = data{varNamesRow,c}; if (ischar (x) && ! isempty (x)) names{c} = x; elseif (isempty (x)) names{c} = sprintf ("Var%d", c); else names{c} = num2str (x); endif endfor data(varNamesRow,:) = []; vtype(varNamesRow,:) = []; else names = arrayfun (@(c) sprintf ("Var%d", c), 1:ncols, ... 'UniformOutput', false); endif if (strcmpi (namingRule, 'modify')) names = matlab.lang.makeValidName (names); endif ## A table refuses a variable named after one of its dimensions, so a column ## headed 'Row' takes a suffix rather than being refused. names = __undimname__ (names, {'Row', 'Variables'}); ## A leading column becomes row names when requested rowNames = {}; if (rowNamesCol > 0 && rowNamesCol <= size (data, 2)) rowNames = ods_strings (data(:,rowNamesCol), vtype(:,rowNamesCol)); data(:,rowNamesCol) = []; vtype(:,rowNamesCol) = []; names(rowNamesCol) = []; endif ## Reconstruct each column from its native value type varValues = cell (1, numel (names)); for c = 1:numel (names) varValues{c} = reconstruct_column (data(:,c), vtype(:,c), textType); endfor if (isempty (rowNames)) tbl = table (varValues{:}, 'VariableNames', names); else tbl = table (varValues{:}, 'VariableNames', names, 'RowNames', rowNames); endif endfunction ## Clip the raw grid to an absolute A1 range; open-ended (Inf) bounds and ## bounds past the used area are capped to the grid, matching MATLAB, which ## reads whatever of the requested rectangle actually holds data. function [data, vtype] = clip_range (data, vtype, range) [r1, c1, r2, c2] = __a1ref__ (range); nr = size (data, 1); nc = size (data, 2); r2 = min (r2, nr); c2 = min (c2, nc); if (r1 > r2 || c1 > c2) data = cell (0, 0); vtype = cell (0, 0); return; endif data = data(r1:r2, c1:c2); vtype = vtype(r1:r2, c1:c2); endfunction ## Trim leading and trailing empty rows and columns to the used bounding box ## (interior blanks are preserved). This mirrors readtable's automatic used- ## range detection for foreign spreadsheets whose data does not start at A1. function [data, vtype] = trim_used_block (data, vtype) if (isempty (data)) return; endif blank = cellfun (@isempty, data); keptRows = find (! all (blank, 2)); keptCols = find (! all (blank, 1)); if (isempty (keptRows) || isempty (keptCols)) data = cell (0, 0); vtype = cell (0, 0); return; endif rr = keptRows(1):keptRows(end); cc = keptCols(1):keptCols(end); data = data(rr, cc); vtype = vtype(rr, cc); endfunction ## Reconstruct one column from its data cells and their ODS value types. function v = reconstruct_column (C, VT, textType) switch (__colkind__ (C, VT)) case 'float' v = __numcells__ (C); case 'boolean' v = logical (__numcells__ (C)); case 'date' v = ods_iso2datetime (C); case 'time' v = ods_iso2duration (C); otherwise s = ods_strings (C, VT); if (strcmpi (textType, 'string')) v = string (s); else v = s; endif endswitch endfunction ## Cellstr column from ODS cells; numbers are stringified, missing become ''. function S = ods_strings (C, VT) S = cell (size (C)); for i = 1:numel (C) x = C{i}; if (ischar (x)) S{i} = x; elseif (isempty (x)) S{i} = ''; else S{i} = num2str (x); endif endfor endfunction ## Parse an ISO 8601 date block into a datetime array; empty cells become NaT. function dt = ods_iso2datetime (C) sz = size (C); Y = nan (sz); Mo = nan (sz); D = nan (sz); h = nan (sz); mi = nan (sz); s = nan (sz); for i = 1:numel (C) str = C{i}; if (ischar (str) && ! isempty (str)) val = sscanf (str, "%d-%d-%dT%d:%d:%f"); if (numel (val) == 6) Y(i) = val(1); Mo(i) = val(2); D(i) = val(3); h(i) = val(4); mi(i) = val(5); s(i) = val(6); endif endif endfor dt = datetime (Y, Mo, D, h, mi, s); endfunction ## Parse an ISO 8601 duration block (PTnHnMnS) into a duration array; empty ## cells become NaN durations. function du = ods_iso2duration (C) tot = nan (size (C)); for i = 1:numel (C) str = C{i}; if (ischar (str) && ! isempty (str)) neg = (str(1) == '-'); if (neg) str(1) = []; endif tk = regexp (str, '^PT([\d.]+)H([\d.]+)M([\d.]+)S$', 'tokens'); if (! isempty (tk)) val = str2double (tk{1}{1}) * 3600 + str2double (tk{1}{2}) * 60 ... + str2double (tk{1}{3}); if (neg) val = -val; endif tot(i) = val; endif endif endfor du = seconds (tot); endfunction %!demo %! ## `readtable` is the unified, MATLAB-compatible reader: it picks text vs %! ## spreadsheet from the file extension. Here it reads a comma-delimited %! ## file, taking the first row as variable names and detecting each column's %! ## type. %! %! T = table ([38; 43], [71.5; 69.0], 'VariableNames', {'Age', 'Height'}); %! filename = fullfile (tempdir (), 'patients.csv'); %! writetable (T, filename); %! readtable (filename) %!demo %! ## Options let you override the defaults, for example to read a specific %! ## spreadsheet `'Sheet'`, or to turn the first column into row names. %! %! filename = fullfile (tempdir (), 'patients.ods'); %! T = table ([38; 43], 'VariableNames', {'Age'}, 'RowNames', {'Li', 'Diaz'}); %! writetable (T, filename, 'WriteRowNames', true); %! readtable (filename, 'ReadRowNames', true) %! %! delete (filename); ## Read a comma-delimited file with a header row and automatic type detection ## A file written by 'table2ods' is read through its own metadata, not ## positionally ## A text file written by 'writetable' with row names keeps the column %!test %! fn = [tempname() '.csv']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! T.Properties.RowNames = {'r1', 'r2'}; %! unwind_protect %! writetable (T, fn, 'WriteRowNames', true); %! R = readtable (fn); %! assert_equal (R.Properties.VariableNames, {'Row_1', 'n', 's'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A spreadsheet written by 'writetable' with row names keeps the column %!test %! fn = [tempname() '.ods']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! T.Properties.RowNames = {'r1', 'r2'}; %! unwind_protect %! writetable (T, fn, 'WriteRowNames', true); %! R = readtable (fn); %! assert_equal (R.Properties.VariableNames, {'Row_1', 'n', 's'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## 'ReadRowNames' true restores the row names of a text 'writetable' file %!test %! fn = [tempname() '.csv']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! T.Properties.RowNames = {'r1', 'r2'}; %! unwind_protect %! writetable (T, fn, 'WriteRowNames', true); %! assert_equal (isequaln (readtable (fn, 'ReadRowNames', true), T), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## 'ReadRowNames' true restores the row names of a 'writetable' spreadsheet %!test %! fn = [tempname() '.ods']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! T.Properties.RowNames = {'r1', 'r2'}; %! unwind_protect %! writetable (T, fn, 'WriteRowNames', true); %! assert_equal (isequaln (readtable (fn, 'ReadRowNames', true), T), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## 'VariableNamesLine' is MATLAB's spelling of the same option %!test %! fn = [tempname() '.csv']; %! unwind_protect %! fid = fopen (fn, 'w'); %! fputs (fid, "skip,me\nn,s\n1,a\n2,b\n"); %! fclose (fid); %! R = readtable (fn, 'VariableNamesLine', 2); %! assert_equal (R.Properties.VariableNames, {'n', 's'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!error ... %! readtable ([tempname() '.csv'], 'VariableNamesRow', 1, ... %! 'VariableNamesLine', 1); ## 'VariableNamesRow' and 'RowNamesColumn' reach the text reader %!test %! fn = [tempname() '.csv']; %! unwind_protect %! fid = fopen (fn, 'w'); %! fputs (fid, "skip,me\nn,s\n1,a\n2,b\n"); %! fclose (fid); %! R = readtable (fn, 'VariableNamesRow', 2); %! assert_equal (R.Properties.VariableNames, {'n', 's'}); %! R2 = readtable (fn, 'VariableNamesRow', 2, 'ReadRowNames', true, ... %! 'RowNamesColumn', 2); %! assert_equal (R2.Properties.VariableNames, {'n'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## 'VariableNamesRow' reaches the spreadsheet reader %!test %! fn = [tempname() '.ods']; %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! unwind_protect %! writetable (T, fn); %! R = readtable (fn, 'VariableNamesRow', 0); %! assert_equal (R.Properties.VariableNames, {'Var1', 'Var2'}); %! assert_equal (height (R), 3); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!error ... %! readtable ([tempname() '.csv'], 'VariableNamesRow', -1); %!error ... %! readtable ([tempname() '.csv'], 'RowNamesColumn', 1.5); %!test %! fn = [tempname() '.fods']; %! T = table ([1; 2], {'a'; 'b'}, hours ([1; 2]), ... %! 'VariableNames', {'n', 's', 'u'}); %! T.Properties.VariableUnits = {'kg', '', 'h'}; %! unwind_protect %! table2ods (T, fn); %! R = readtable (fn); %! assert_equal (isequaln (R, T), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A nested table survives 'readtable' the same way %!test %! fn = [tempname() '.fods']; %! inner = table ([3; 4], [5; 6], 'VariableNames', {'p', 'q'}); %! T = table ([1; 2], inner, 'VariableNames', {'a', 'v'}); %! unwind_protect %! table2ods (T, fn); %! R = readtable (fn); %! assert_equal (isequaln (R, T), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## An explicit 'Range' reads the block positionally %!test %! fn = [tempname() '.fods']; %! T = table ([1; 2], [3; 4], 'VariableNames', {'n', 'm'}); %! unwind_protect %! table2ods (T, fn); %! R = readtable (fn, 'Range', 'A1:B2'); %! assert_equal (size (R), [1, 2]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!test %! fn = [tempname() '.csv']; %! fid = fopen (fn, 'w'); %! fputs (fid, sprintf ("id,val,name\n1,2.5,foo\n2,3.5,bar\n")); %! fclose (fid); %! unwind_protect %! t = readtable (fn); %! assert_equal (t.Properties.VariableNames, {'id', 'val', 'name'}); %! assert_equal (t.id, [1; 2]); %! assert_equal (t.val, [2.5; 3.5]); %! assert_equal (t.name, {'foo'; 'bar'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Read row names from the first column %!test %! fn = [tempname() '.csv']; %! fid = fopen (fn, 'w'); %! fputs (fid, sprintf ("rn,v\nr1,10\nr2,20\n")); %! fclose (fid); %! unwind_protect %! t = readtable (fn, 'ReadRowNames', true); %! assert_equal (t.Properties.RowNames, {'r1'; 'r2'}); %! assert_equal (t.v, [10; 20]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A named delimiter selects the field separator %!test %! fn = [tempname() '.csv']; %! fid = fopen (fn, 'w'); %! fputs (fid, sprintf ("A;B\n1;x\n2;y\n")); %! fclose (fid); %! unwind_protect %! t = readtable (fn, 'Delimiter', 'semi'); %! assert_equal (t.Properties.VariableNames, {'A', 'B'}); %! assert_equal (t.A, [1; 2]); %! assert_equal (t.B, {'x'; 'y'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## 'TextType' string returns text columns as string arrays %!test %! fn = [tempname() '.csv']; %! fid = fopen (fn, 'w'); %! fputs (fid, sprintf ("A,B\nfoo,bar\n")); %! fclose (fid); %! unwind_protect %! t = readtable (fn, 'TextType', 'string'); %! assert_equal (class (t.A), 'string'); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A datetime column that is not the first column is detected as datetime %!test %! fn = [tempname() '.csv']; %! fid = fopen (fn, 'w'); %! fputs (fid, sprintf ("id,d\n1,2024-01-15\n2,2024-02-20\n")); %! fclose (fid); %! unwind_protect %! t = readtable (fn); %! assert_equal (class (t.d), 'datetime'); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Hex-like text is kept as text (readtable does not auto-detect hexadecimals) %!test %! fn = [tempname() '.csv']; %! fid = fopen (fn, 'w'); %! fputs (fid, sprintf ("id,code\n1,a\n2,b\n")); %! fclose (fid); %! unwind_protect %! t = readtable (fn); %! assert_equal (t.code, {'a'; 'b'}); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Round-trip a spreadsheet written by writetable, restoring native types %!test %! fn = [tempname() '.ods']; %! T = table ([1; 2], {'x'; 'y'}, datetime (2024, 1, [1; 2]), ... %! seconds ([30; 90]), 'VariableNames', {'a', 'b', 'c', 'd'}); %! unwind_protect %! writetable (T, fn); %! R = readtable (fn); %! assert_equal (R.Properties.VariableNames, {'a', 'b', 'c', 'd'}); %! assert_equal (R.a, [1; 2]); %! assert_equal (R.b, {'x'; 'y'}); %! assert_equal (class (R.c), 'datetime'); %! assert_equal (isequaln (datevec (R.c), datevec (T.c)), true); %! assert_equal (class (R.d), 'duration'); %! assert_equal (isequaln (seconds (R.d), [30; 90]), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Build a flat '.fods' fixture with three sheets: Alpha (x,y), Beta (p,q), and ## Offset, whose data starts at C2 and is padded with the large repeat counts a ## real spreadsheet emits -- exercising sheet selection, used-block trimming, ## and the trailing-repeat cap. %!function fn = wr_multisheet () %! fn = [tempname() '.fods']; %! sc = @(s) ['' s ... %! '']; %! fc = @(x) sprintf (['%g' ... %! ''], x, x); %! rw = @(c) ['' c '']; %! alpha = ['' rw([sc('x') sc('y')]) ... %! rw([fc(1) fc(10)]) rw([fc(2) fc(20)]) rw([fc(3) fc(30)]) ... %! '']; %! beta = ['' rw([sc('p') sc('q')]) ... %! rw([fc(4) fc(40)]) rw([fc(5) fc(50)]) '']; %! pad = ''; %! big = ''; %! offset = ['' ... %! '' ... %! rw([pad sc('a') sc('b') big]) rw([pad fc(7) fc(8)]) ... %! '' ... %! '' ... %! '']; %! doc = ['' ... %! '' ... %! '' alpha beta offset ... %! '']; %! fid = fopen (fn, 'w'); %! fputs (fid, doc); %! fclose (fid); %!endfunction ## Select a sheet by name %!test %! fn = wr_multisheet (); %! unwind_protect %! R = readtable (fn, 'Sheet', 'Beta'); %! assert_equal (R.Properties.VariableNames, {'p', 'q'}); %! assert_equal (R.p, [4; 5]); %! assert_equal (R.q, [40; 50]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Select a sheet by 1-based index (2 -> Beta) %!test %! fn = wr_multisheet (); %! unwind_protect %! R = readtable (fn, 'Sheet', 2); %! assert_equal (R.Properties.VariableNames, {'p', 'q'}); %! assert_equal (R.p, [4; 5]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## A sheet whose data is offset and padded is trimmed to its used block %!test %! fn = wr_multisheet (); %! unwind_protect %! R = readtable (fn, 'Sheet', 'Offset'); %! assert_equal (size (R), [1, 2]); %! assert_equal (R.Properties.VariableNames, {'a', 'b'}); %! assert_equal (R.a, 7); %! assert_equal (R.b, 8); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## An explicit 'Range' clips absolutely: with the header row, and without it %!test %! fn = wr_multisheet (); %! unwind_protect %! R = readtable (fn, 'Sheet', 'Alpha', 'Range', 'A2:B3', ... %! 'ReadVariableNames', false); %! assert_equal (R.Var1, [1; 2]); %! assert_equal (R.Var2, [10; 20]); %! R2 = readtable (fn, 'Sheet', 'Alpha', 'Range', 'A1:B2'); %! assert_equal (R2.Properties.VariableNames, {'x', 'y'}); %! assert_equal (R2.x, 1); %! assert_equal (R2.y, 10); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Error: a requested sheet that does not exist %!error ... %! fn = wr_multisheet (); ... %! readtable (fn, 'Sheet', 'Nope'); ## Error: a sheet index past the last data sheet %!error ... %! fn = wr_multisheet (); ... %! readtable (fn, 'Sheet', 9); ## Error: 'Sheet' / 'Range' rejected on text files %!error ... %! readtable ([tempname() '.csv'], 'Sheet', 'A'); %!error ... %! readtable ([tempname() '.csv'], 'Range', 'A1:B2'); ## Round-trip through an '.xlsx' workbook written by writetable, including ## native datetime/duration/boolean columns and sheet/range selection %!test %! T = table ([1; 2; 3], {'a'; 'bb'; 'ccc'}, datetime (2024, 3, ... %! [1; 15; 31]), ... %! seconds ([30; 90; 3661]), [true; false; true], ... %! 'VariableNames', {'n', 's', 'when', 'dur', 'flag'}); %! fn = [tempname() '.xlsx']; %! unwind_protect %! writetable (T, fn, 'Sheet', 'Data'); %! R = readtable (fn, 'Sheet', 'Data'); %! assert_equal (R.Properties.VariableNames, {'n', 's', 'when', 'dur', ... %! 'flag'}); %! assert_equal (R.n, [1; 2; 3]); %! assert_equal (R.s, {'a'; 'bb'; 'ccc'}); %! assert_equal (class (R.when), 'datetime'); %! assert_equal (isequaln (datevec (R.when), datevec (T.when)), true); %! assert_equal (class (R.dur), 'duration'); %! assert_equal (isequaln (seconds (R.dur), [30; 90; 3661]), true); %! assert_equal (R.flag, [true; false; true]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## XLSX has no infinite number: an infinity is written as text and read back %!test %! T = table ([Inf; -Inf; NaN; 1.5], 'VariableNames', {'v'}); %! fn = [tempname() '.xlsx']; %! d = tempname (); %! unwind_protect %! writetable (T, fn); %! R = readtable (fn); %! assert_equal (isequaln (R.v, [Inf; -Inf; NaN; 1.5]), true); %! unzip (fn, d); %! xml = fileread (fullfile (d, 'xl', 'worksheets', 'sheet1.xml')); %! assert_equal (isempty (strfind (xml, '-Inf')), false); %! assert_equal (isempty (strfind (xml, 'inf')), true); %! unwind_protect_cleanup %! delete (fn); %! if (exist (d, 'dir')) %! confirm_recursive_rmdir (false, 'local'); %! rmdir (d, 's'); %! endif %! end_unwind_protect ## An error cell in an XLSX file is a missing value %!test %! d = tempname (); %! fn = [tempname() '.xlsx']; %! ns = 'http://schemas.openxmlformats.org/'; %! hdr = ''; %! parts = { ... %! '[Content_Types].xml', [hdr '', ... %! '']; ... %! '_rels/.rels', [hdr '']; ... %! 'xl/workbook.xml', [hdr '']; ... %! 'xl/_rels/workbook.xml.rels', [hdr '']; ... %! 'xl/worksheets/sheet1.xml', [hdr 'x', ... %! '1#N/A', ... %! '#NUM!', ... %! '4']}; %! unwind_protect %! for k = 1:rows (parts) %! p = fullfile (d, parts{k,1}); %! mkdir (fileparts (p)); %! fid = fopen (p, 'w'); %! fputs (fid, parts{k,2}); %! fclose (fid); %! endfor %! zip (fn, parts(:,1), d); %! R = readtable (fn); %! assert_equal (isequaln (R.x, [1; NaN; NaN; 4]), true); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! if (exist (d, 'dir')) %! confirm_recursive_rmdir (false, 'local'); %! rmdir (d, 's'); %! endif %! end_unwind_protect ## '.xlsx' 'Range' anchor on write, clipped on read %!test %! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'n', 's'}); %! fn = [tempname() '.xlsx']; %! unwind_protect %! writetable (T, fn, 'Range', 'C3'); %! R = readtable (fn); %! assert_equal (R.n, [1; 2; 3]); %! R2 = readtable (fn, 'Range', 'C4:D6', 'ReadVariableNames', false); %! assert_equal (R2.Var1, [1; 2; 3]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## '.xlsm' round-trips too %!test %! T = table ([7; 8], 'VariableNames', {'v'}); %! fn = [tempname() '.xlsm']; %! unwind_protect %! writetable (T, fn); %! assert_equal (readtable (fn).v, [7; 8]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## Error: legacy binary Excel formats are not supported %!error ... %! readtable ('data.xls'); %!error ... %! readtable ('data.xlsb'); ## Error: an unknown option, and one without a value %!error ... %! readtable ([tempname() '.csv'], 'Bogus', 1); %!error ... %! readtable ([tempname() '.csv'], 'Bogus'); %!error ... %! readtable (42) %!error ... %! readtable ('no_such.zzz') %!error ... %! readtable ('x.csv', 'FileType', 'nope') %!error ... %! readtable ('x.csv', 'Delimiter', 42) %!error ... %! readtable ('x.csv', 'Delimiter', 'nope') pr0m1th3as-datatypes-9450b29/inst/readtimetable.m000066400000000000000000000277641527370255000216530ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{tt} =} readtimetable (@var{filename}) ## @deftypefnx {datatypes} {@var{tt} =} readtimetable (@var{filename}, @var{Name}, @var{Value}) ## ## Read a file into a timetable, in the MATLAB-compatible form. ## ## @code{@var{tt} = readtimetable (@var{filename})} reads @var{filename} as ## @code{readtable} reads it, taking the file to carry a header row of names and ## then one row per row, and turns the result into a @code{timetable}. Text ## files (@qcode{.txt}, @qcode{.csv}, @qcode{.dat}) and spreadsheets ## (@qcode{.ods}, @qcode{.fods}, @qcode{.xlsx}, @qcode{.xlsm}) are supported, ## and @qcode{'FileType'} overrides what the extension says. ## ## The @strong{first} @code{datetime} or @code{duration} variable becomes the ## row times and the row dimension takes its name, which is what MATLAB does and ## what makes a file written by @code{writetimetable} come back as it went out. ## @qcode{'RowTimes'} names a different variable, by name or by index. A file ## with no such variable cannot be read as a timetable. ## ## A column written in the @strong{RFC 9557} form, ## @qcode{2024-03-09T22:00:00-05:00[America/New_York]}, is recognised and comes ## back as a zone-aware @code{datetime} with its zone and its side of any ## daylight-saving fold intact. That is what @code{writetimetable} writes for ## zoned row times; see deviation @strong{D7} in that method's help for why. ## A column MATLAB wrote is read as MATLAB reads it, so a bare wall clock comes ## back unzoned, exactly as it does there. ## ## Every @var{Name}-@var{Value} option of @code{readtable} is accepted and ## behaves as it does there, with two exceptions. @qcode{'ReadRowNames'} and ## @qcode{'RowNamesColumn'} are refused: a timetable labels its rows by time and ## by nothing else, so a file whose rows are named is read with ## @code{readtable}. ## ## @code{TimeStep} and @code{SampleRate} are not stored in the file and are ## worked out again from the row times. No file MATLAB can read carries an ## event table, so the result never has one; use @code{ods2timetable} for that. ## ## @seealso{timetable.writetimetable, readtable, ods2timetable, csv2timetable, ## timetable} ## @end deftypefn function tt = readtimetable (filename, varargin) if (nargin < 1) error ("readtimetable: too few input arguments."); endif if (! ((ischar (filename) && isvector (filename)) || iscellstr (filename) ... || isa (filename, 'string'))) error (strcat ("readtimetable: FILENAME must be a character vector,", ... " cellstr, or string.")); endif if (mod (numel (varargin), 2) != 0) error ("readtimetable: name-value arguments must be in pairs."); endif ## A timetable has no row names, so an option asking for them would be ## honoured by the reader and then discarded by the conversion, which is ## worse than saying so. for i = 1:2:numel (varargin) opt = varargin{i}; if (isa (opt, 'string') && isscalar (opt)) opt = char (opt); endif if (ischar (opt) && any (strcmpi (opt, {'ReadRowNames', 'RowNamesColumn'}))) error (strcat ("readtimetable: '%s' is not supported; a timetable", ... " labels its rows by time. Read a file whose rows", ... " are named with 'readtable'."), opt); endif endfor ## 'RowTimes' is ours to act on and 'readtable' knows nothing of it. [rowTimes, args] = parsePairedArguments ({'RowTimes'}, {[]}, varargin(:)); ## The file is read by 'readtable', whose complaints are about the call ## made here and so are raised under this function's name. try tbl = readtable (filename, args{:}); catch err if (strncmp (err.message, "readtable: ", 11)) error ("readtimetable: %s", err.message(12:end)); endif rethrow (err); end_try_catch ## A column written in RFC 9557 form arrives as text, no reader having a ## type for it; every entry of a column must parse, a column being one ## variable. names = tbl.Properties.VariableNames; for i = 1:numel (names) col = tbl.(names{i}); if (ischar (col)) col = cellstr (col); elseif (! iscellstr (col)) continue; endif [dt, ok] = __rfc95572dt__ (col); if (ok) tbl.(names{i}) = dt; endif endfor if (isempty (rowTimes)) types = tbl.Properties.VariableTypes; if (! any (ismember (types, {'datetime', 'duration'}))) error (strcat ("readtimetable: the file has no datetime or duration", ... " column to use as row times.")); endif tt = table2timetable (tbl); else tt = table2timetable (tbl, 'RowTimes', rowTimes); endif endfunction %!demo %! ## `writetimetable` writes a zoned datetime in the RFC 9557 form, which %! ## carries both the offset and the zone name, so `readtimetable` returns %! ## the timetable that was written rather than an unzoned shadow of it. %! %! t = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours ((0:2)'); %! TT = timetable (t, (1:3)', 'VariableNames', {'reading'}); %! fname = [tempname(), '.csv']; %! writetimetable (TT, fname); %! type (fname); %! readtimetable (fname) %! delete (fname); ## Test a zoned datetime round-trips through the interop pair %!test %! t = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours ((0:3)'); %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! writetimetable (TT, fname); %! out = readtimetable (fname); %! assert_equal (out.Properties.RowTimes.TimeZone, 'America/New_York'); %! assert_equal (out.Properties.RowTimes, TT.Properties.RowTimes); %! assert_equal (out.v, TT.v); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the written form is RFC 9557 %!test %! t = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'America/New_York'); %! TT = timetable (t, 1, 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! writetimetable (TT, fname); %! txt = fileread (fname); %! hit = strfind (txt, '2024-03-09T22:00:00-05:00[America/New_York]'); %! assert_equal (! isempty (hit), true); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test a timestamp inside a daylight-saving fold keeps its side of it %!test %! a = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC'); %! a.TimeZone = 'America/New_York'; %! b = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC'); %! b.TimeZone = 'America/New_York'; %! TT = timetable ([a; b], (1:2)', 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! writetimetable (TT, fname); %! rt = readtimetable (fname).Properties.RowTimes; %! assert_equal (rt, TT.Properties.RowTimes); %! assert_equal (char (tzoffset (rt(1))), '-04:00'); %! assert_equal (char (tzoffset (rt(2))), '-05:00'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test an unzoned datetime is written as MATLAB writes it %!test %! TT = timetable (datetime (2024, 1, (1:2)'), (1:2)', 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! writetimetable (TT, fname); %! txt = fileread (fname); %! assert_equal (! isempty (strfind (txt, '01-Jan-2024')), true); %! assert_equal (isempty (strfind (txt, '[')), true); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test duration row times round-trip %!test %! TT = timetable (hours ([1; 2; 3]), (1:3)', 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! writetimetable (TT, fname); %! out = readtimetable (fname); %! assert_equal (class (out.Properties.RowTimes), 'duration'); %! assert_equal (out.Properties.RowTimes, TT.Properties.RowTimes); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the row dimension name leads the file and comes back %!test %! TT = timetable (datetime (2024, 1, (1:2)'), (1:2)', 'VariableNames', {'v'}); %! TT.Properties.DimensionNames{1} = 'Stamp'; %! fname = [tempname(), '.csv']; %! unwind_protect %! writetimetable (TT, fname); %! lines = strsplit (fileread (fname), "\n"); %! assert_equal (lines{1}, 'Stamp,v'); %! assert_equal (readtimetable (fname).Properties.DimensionNames{1}, 'Stamp'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test a spreadsheet round-trips through the interop pair %!test %! TT = timetable (datetime (2024, 1, (1:3)'), (1:3)', 'VariableNames', {'v'}); %! fname = [tempname(), '.ods']; %! unwind_protect %! writetimetable (TT, fname); %! out = readtimetable (fname); %! assert_equal (out.Properties.RowTimes, TT.Properties.RowTimes); %! assert_equal (out.v, TT.v); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test 'RowTimes' names another variable %!test %! fname = [tempname(), '.csv']; %! unwind_protect %! fid = fopen (fname, 'w'); %! fprintf (fid, "a,when\n1,2024-01-01\n2,2024-01-02\n"); %! fclose (fid); %! out = readtimetable (fname, 'RowTimes', 'when'); %! assert_equal (out.Properties.DimensionNames{1}, 'when'); %! assert_equal (out.a, [1; 2]); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test an attached event table is not written %!test %! t = datetime (2024, 1, 1) + hours ((0:3)'); %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t(2), 'EventLabels', {'on'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! writetimetable (TT, fname); %! assert_equal (isempty (readtimetable (fname).Properties.Events), true); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test too few input arguments %!error readtimetable () ## Test an invalid FILENAME %!error ... %! readtimetable (42) ## Test 'ReadRowNames' is refused %!error ... %! readtimetable ('none.csv', 'ReadRowNames', true) ## Test 'RowNamesColumn' is refused %!error ... %! readtimetable ('none.csv', 'RowNamesColumn', 1) ## Test an unknown option, and one without a value %!error ... %! readtimetable ('none.csv', 'Bogus', 1) %!error ... %! readtimetable ('none.csv', 'Bogus') ## Test 'WriteRowNames' is refused by the writer %!error ... %! writetimetable (timetable ((1:3)', 'TimeStep', hours (1)), 'f.csv', ... %! 'WriteRowNames', true) ## A file whose columns are all numeric, so nothing in it can label the rows. %!shared fnum %! fnum = [tempname(), '.csv']; %! fid = fopen (fnum, 'w'); %! fputs (fid, "a,b\n1,2\n3,4\n"); %! fclose (fid); ## Test a file with no time column cannot become a timetable %!error ... %! readtimetable (fnum) ## Test the fixture is removed again %!test %! delete (fnum); %! assert_equal (exist (fnum, 'file'), 0); pr0m1th3as-datatypes-9450b29/inst/rowfilter.m000066400000000000000000000321011527370255000210430ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef rowfilter ## -*- texinfo -*- ## @deftp {datatypes} rowfilter ## ## Subscript into a table or timetable by a condition on its variables. ## ## A utility class that builds a condition out of comparisons written ## against variable names, and selects the rows satisfying it. The ## condition is written before the rows are looked at, so the same filter ## may be used on any table naming the same variables. ## ## @example ## @group ## rf = rowfilter (tbl); ## tbl(rf.Height > 180 & rf.Age < 40, :) ## @end group ## @end example ## ## A filter of this package is a @code{rowfilter}, a class of its own beside ## @code{vartype}, @code{timerange} and @code{withtol}, where MATLAB's is a ## @code{matlab.io.RowFilter}. Only the class name differs: nothing here ## calls itself @code{matlab.*}, and no behaviour follows from it. ## ## @seealso{timerange, withtol, table, timetable} ## @end deftp properties (SetAccess = private, Hidden) ## The names a condition may be written against varNames ## The variable a comparison is waiting for, empty once one is made pending ## The condition, as a function of the table it will be applied to condition ## The condition as written, for the display text endproperties methods (Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif disp (this); endfunction ## Custom display function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif if (isempty (this.condition)) fprintf (" rowfilter with no constraint\n\n"); else fprintf (" rowfilter with constraint:\n\n %s\n\n", this.text); endif fprintf (" VariableNames: %s\n\n", strjoin (this.varNames, ", ")); endfunction ## Class specific subscripted reference. Every '.' name is a variable of ## the table the filter will be applied to, as in MATLAB, so the filter ## has no readable properties of its own. function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); if (! strcmp (s.type, '.')) error ("rowfilter.subsref: only '.' indexing is supported."); endif name = s.subs; if (isstring (name) && isscalar (name)) name = char (name); endif if (! (ischar (name) && isrow (name))) error (strcat ("rowfilter.subsref: '.' index argument must be a", ... " character vector or a string scalar.")); endif if (! any (strcmp (this.varNames, name))) error (strcat ("rowfilter: no variable named '%s' to filter on;", ... " use one of: %s"), name, ... strjoin (this.varNames, ", ")); endif out = this; out.pending = name; if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {rowfilter} {@var{rf} =} rowfilter (@var{tbl}) ## @deftypefnx {rowfilter} {@var{rf} =} rowfilter (@var{varNames}) ## ## Create a row filter. ## ## @code{@var{rf} = rowfilter (@var{tbl})} creates a filter that may be ## written against the variables of @var{tbl}, a table or a timetable. ## For a timetable the row times may be filtered on too, under the name ## of the row dimension. ## ## @code{@var{rf} = rowfilter (@var{varNames})} creates one against the ## named variables without a table to take them from, so that a filter ## may be written before the data it will be applied to exists. ## ## A filter carries no condition until one is written against it, and a ## condition is a comparison of a variable with a value, optionally ## combined with @code{&}, @code{|} and @code{~}. ## ## @seealso{timerange, withtol, table, timetable} ## @end deftypefn function this = rowfilter (arg) if (nargin != 1) print_usage (); endif if (istabular (arg)) props = arg.Properties; names = props.VariableNames; if (istimetable (arg)) names = [props.DimensionNames(1), names]; endif elseif (ischar (arg) || iscellstr (arg) || isa (arg, 'string')) names = cellstr (arg); names = names(:)'; if (any (cellfun (@isempty, names))) error ("rowfilter: variable names must be nonempty."); endif else error (strcat ("rowfilter: input must be a table, a timetable, or", ... " a list of variable names.")); endif this.varNames = names; this.pending = ''; this.condition = []; this.text = ''; endfunction ## -*- texinfo -*- ## @deftypefn {rowfilter} {@var{ix} =} rowIndices (@var{rf}, @var{tbl}) ## ## Return the positions of the rows of @var{tbl} the filter selects. ## ## @end deftypefn function ix = rowIndices (this, tbl) if (isempty (this.condition)) error (strcat ("rowfilter: the filter carries no condition;", ... " compare one of its variables with a value first.")); endif mask = this.condition (tbl); if (! (islogical (mask) && numel (mask) == height (tbl))) error (strcat ("rowfilter: the condition did not yield one true", ... " or false value for each row.")); endif ix = find (mask(:)); endfunction endmethods methods (Static, Hidden) ## Build the condition for one comparison. Only one side may be the ## filter: comparing two of them says nothing. A value on the left is ## the same condition read the other way round, so the operator is ## mirrored and the condition still reads as the variable first. ## ## This lives in the class because the filter overloads '.', so reading ## its own fields is direct only inside a method. function out = compare (a, b, sym, fcn, mirror) if (isa (a, 'rowfilter') && isa (b, 'rowfilter')) error (strcat ("rowfilter: '%s' compares a variable with a", ... " value, not two filters."), sym); endif swapped = ! isa (a, 'rowfilter'); if (swapped) rf = b; val = a; else rf = a; val = b; endif if (isempty (rf.pending)) error (strcat ("rowfilter: '%s' needs a variable to compare; name", ... " one with '.' first."), sym); endif name = rf.pending; out = rf; out.pending = ''; if (swapped) out.condition = @(t) fcn (val, t.(name)); out.text = sprintf ("%s %s %s", name, mirror, valuestr (val)); else out.condition = @(t) fcn (t.(name), val); out.text = sprintf ("%s %s %s", name, sym, valuestr (val)); endif endfunction function [rf, ca, cb] = combination (a, b, op) if (! (isa (a, 'rowfilter') && isa (b, 'rowfilter'))) error (strcat ("rowfilter: '%s' combines two filters."), op); endif if (isempty (a.condition) || isempty (b.condition)) error (strcat ("rowfilter: '%s' combines two conditions; compare a", ... " variable with a value on both sides first."), op); endif rf = a; rf.varNames = unique ([a.varNames, b.varNames], 'stable'); ca = a.condition; cb = b.condition; endfunction endmethods methods (Hidden) function out = eq (a, b) out = rowfilter.compare (a, b, '==', @eq, '=='); endfunction function out = ne (a, b) out = rowfilter.compare (a, b, '~=', @ne, '~='); endfunction function out = lt (a, b) out = rowfilter.compare (a, b, '<', @lt, '>'); endfunction function out = le (a, b) out = rowfilter.compare (a, b, '<=', @le, '>='); endfunction function out = gt (a, b) out = rowfilter.compare (a, b, '>', @gt, '<'); endfunction function out = ge (a, b) out = rowfilter.compare (a, b, '>=', @ge, '<='); endfunction function out = and (a, b) [out, ca, cb] = rowfilter.combination (a, b, '&'); out.condition = @(t) ca (t) & cb (t); out.text = sprintf ("(%s) & (%s)", a.text, b.text); endfunction function out = or (a, b) [out, ca, cb] = rowfilter.combination (a, b, '|'); out.condition = @(t) ca (t) | cb (t); out.text = sprintf ("(%s) | (%s)", a.text, b.text); endfunction function out = not (a) if (isempty (a.condition)) error (strcat ("rowfilter: '~' needs a condition to negate;", ... " compare a variable with a value first.")); endif out = a; ca = a.condition; out.condition = @(t) ! ca (t); out.text = sprintf ("~(%s)", a.text); endfunction endmethods endclassdef ## A compared-against value as text, for the display. function s = valuestr (val) if (ischar (val)) s = sprintf ("'%s'", val); elseif (isnumeric (val) && isscalar (val)) s = num2str (val); elseif (islogical (val) && isscalar (val)) s = mat2str (val); else s = sprintf ("<%s>", class (val)); endif endfunction ## Test a comparison selects the rows satisfying it %!test %! T = table ((1:6)', 'VariableNames', {'A'}); %! assert_equal (T(rowfilter (T).A > 2, :).A, [3; 4; 5; 6]); %! assert_equal (T(rowfilter (T).A == 3, :).A, 3); %! assert_equal (T(rowfilter (T).A ~= 3, :).A, [1; 2; 4; 5; 6]); %! assert_equal (T(rowfilter (T).A <= 2, :).A, [1; 2]); ## Test the conditions combine %!test %! T = table ((1:6)', 'VariableNames', {'A'}); %! rf = rowfilter (T); %! assert_equal (T(rf.A > 2 & rf.A < 5, :).A, [3; 4]); %! assert_equal (T(rf.A < 2 | rf.A > 5, :).A, [1; 6]); %! assert_equal (T(~ (rf.A > 2), :).A, [1; 2]); ## Test a value on the left of the comparison %!test %! T = table ((1:6)', 'VariableNames', {'A'}); %! assert_equal (T(2 < rowfilter (T).A, :).A, [3; 4; 5; 6]); ## Test a categorical variable compares against a category name %!test %! T = table ((1:6)', categorical ({'x';'y';'x';'y';'x';'y'}), ... %! 'VariableNames', {'A', 'G'}); %! assert_equal (T(rowfilter (T).G == 'x', :).A, [1; 3; 5]); ## Test a timetable filters on its row times by the row dimension name %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! V = timetable (tv, (1:6)', 'VariableNames', {'A'}); %! assert_equal (V(rowfilter (V).tv > tv(3), :).A, [4; 5; 6]); ## Test a filter built from names alone %!test %! T = table ((1:6)', 'VariableNames', {'A'}); %! rf = rowfilter ({'A'}); %! assert_equal (T(rf.A > 4, :).A, [5; 6]); %! assert_equal (class (rowfilter (["A" "G"])), 'rowfilter'); ## Test a partly written filter is still a filter %!test %! T = table ((1:6)', 'VariableNames', {'A'}); %! assert_equal (class (rowfilter (T)), 'rowfilter'); %! assert_equal (class (rowfilter (T).A), 'rowfilter'); %! assert_equal (class (rowfilter (T).A > 2), 'rowfilter'); %!error ... %! rowfilter (table ((1:3)', 'VariableNames', {'A'})).Nope; %!error ... %! rowfilter (5); %!error rowfilter ({''}); ## The message of a comparison naming no variable, tested through '==' since ## a '%!error' pattern ends at the first '>' and cannot carry one. %!error ... %! rowfilter (table ((1:3)', 'VariableNames', {'A'})) == 2; %!error ... %! rf = rowfilter (table ((1:3)', 'VariableNames', {'A'})); rf.A == rf.A; %!error ... %! rf = rowfilter (table ((1:3)', 'VariableNames', {'A'})); rf.A & rf.A; %!error ... %! ~ rowfilter (table ((1:3)', 'VariableNames', {'A'})); %!error ... %! T = table ((1:3)', 'VariableNames', {'A'}); T(rowfilter (T), :); %!error ... %! rf = rowfilter (table ((1:3)', 'VariableNames', {'A'})); rf(1); pr0m1th3as-datatypes-9450b29/inst/seconds.m000066400000000000000000000044701527370255000204740ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{D} =} seconds (@var{X}) ## ## Fixed-time duration in seconds. ## ## @code{@var{D} = seconds (@var{X})} returns a @qcode{duration} array ## representing fixed-time duration seconds equivalent to the values in @var{X}, ## which must be a numeric array. ## ## @code{seconds} is also available as a method for @qcode{duration} arrays, in ## which case it performs the opposite conversion. ## ## @seealso{duration, years, days, hours, minutes, milliseconds, ## duration.seconds} ## @end deftypefn function out = seconds (x) if (nargin == 0) x = 1; elseif (! isnumeric (x)) error ("seconds: input array must be numeric."); elseif (! isreal (x)) error ("seconds: input array must be real."); endif out = duration (0, 0, 0, double (x) * 1000, 'Format', 's'); endfunction %!demo %! ## `seconds` builds a fixed-length duration from a number of seconds. It is %! ## one of the builders (`seconds`, `minutes`, `hours`, `days`, `years`, %! ## `milliseconds`) you add together to make longer spans. %! %! seconds (90) %! %! ## A numeric array builds one duration per element. %! seconds ([30, 60, 90]) %!test %! X = magic (3); %! D = seconds (X); %! assert_equal (size (D), size (X)); %!test %! D = seconds ([1, 2, 3]); %! assert_equal (seconds (D), [1, 2, 3]); %!test %! D = seconds (int16 (1)); %! assert_equal (seconds (D), 1); %!test %! D = seconds (); %! assert_equal (seconds (D), 1); %!error seconds ("asd"); %!error seconds (1+i); pr0m1th3as-datatypes-9450b29/inst/splitapply.m000066400000000000000000000133101527370255000212300ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{Y} =} splitapply (@var{func}, @var{X}, @var{G}) ## @deftypefnx {datatypes} {@var{Y} =} splitapply (@var{func}, @var{X1}, @dots{}, @var{XN}, @var{G}) ## @deftypefnx {datatypes} {[@var{Y1}, @dots{}, @var{YM}] =} splitapply (@dots{}) ## ## Split data into groups and apply a function to each group. ## ## @code{@var{Y} = splitapply (@var{func}, @var{X}, @var{G})} splits the data ## variable @var{X} into groups according to the group numbers @var{G} ## (typically produced by @code{findgroups}), applies the function handle ## @var{func} to each group, and concatenates the per-group results into the ## output @var{Y}. @var{G} must be a vector of positive integers with one ## element per element of @var{X}; if it identifies @var{N} groups, every ## integer between 1 and @var{N} must occur at least once. Elements for which ## @var{G} is @code{NaN} are omitted. ## ## @code{@var{Y} = splitapply (@var{func}, @var{X1}, @dots{}, @var{XN}, ## @var{G})} splits each of the data variables @var{X1}, @dots{}, @var{XN} and ## passes the corresponding group of each as a separate input argument to ## @var{func}. ## ## @code{[@var{Y1}, @dots{}, @var{YM}] = splitapply (@dots{})} returns the ## multiple outputs of @var{func}, each concatenated across groups. ## ## To split the variables of a @code{table}, call ## @code{splitapply (@var{func}, @var{T}, @var{G})}, which dispatches to the ## @code{table} method. ## ## @seealso{findgroups, table} ## @end deftypefn function varargout = splitapply (func, varargin) if (nargin < 3) print_usage (); endif if (! is_function_handle (func)) error ("splitapply: FUNC must be a function handle."); endif ## The last argument is the grouping vector; the rest are data variables. G = varargin{end}; data = varargin(1:end-1); ## Force each data variable to a column (char matrices keep their rows). for k = 1:numel (data) v = data{k}; if (! ischar (v) && isvector (v)) data{k} = v(:); endif endfor ## All data variables must share the same number of rows. n = size (data{1}, 1); for k = 2:numel (data) if (size (data{k}, 1) != n) error (strcat ("splitapply: all data variables must have the same", ... " number of rows.")); endif endfor ## Validate the grouping vector. if (! (isnumeric (G) && isvector (G) && numel (G) == n)) error (strcat ("splitapply: G must be a numeric vector with one", ... " element", ... " per row of the data variables.")); endif G = G(:); gv = G(! isnan (G)); if (any (gv != fix (gv)) || any (gv < 1)) error ("splitapply: G must contain positive integers."); endif if (isempty (gv)) N = 0; else N = max (gv); if (! isequal (unique (gv), (1:N)')) error (strcat ("splitapply: G must contain every integer between 1", ... " and the number of groups.")); endif endif ## Apply FUNC to each group and concatenate the per-group results. nvar = numel (data); nout = max (nargout, 1); results = cell (N, nout); for g = 1:N rows = (G == g); args = cell (1, nvar); for j = 1:nvar v = data{j}; args{j} = v(rows,:); endfor [results{g,:}] = func (args{:}); endfor varargout = cell (1, nout); for k = 1:nout varargout{k} = vertcat (results{:,k}); endfor endfunction %!assert_equal (splitapply (@(x) mean (x), [1; 2; 3; 4], ... %! [1; 1; 2; 2]), [1.5; 3.5]) %!assert_equal (splitapply (@(x) sum (x), [1; 2; 3], [2; 1; 2]), [2; 4]) %!test %! ## Multiple data variables become separate arguments to FUNC %! y = splitapply (@(a, b) sum (a) + sum (b), [1; 2; 3; 4], ... %! [10; 20; 30; 40], ... %! [1; 1; 2; 2]); %! assert_equal (y, [33; 77]); %!test %! ## Multiple outputs are concatenated separately %! [lo, hi] = splitapply (@(x) deal (min (x), max (x)), [4; 1; 3; 2], ... %! [1; 1; 2; 2]); %! assert_equal (lo, [1; 2]); %! assert_equal (hi, [4; 3]); %!test %! ## NaN group numbers omit the corresponding elements %! y = splitapply (@(x) mean (x), [1; 2; 99; 4], [1; 1; NaN; 2]); %! assert_equal (y, [1.5; 4]); %!error splitapply (@mean, [1; 2]) %!error splitapply (1, [1; 2], [1; 2]) %!error splitapply (@mean, [1; 2], [1; 3]) %!error ... %! splitapply (5, [1; 2], [1; 2]) %!error ... %! splitapply (@(x, y) sum (x), [1; 2], [1; 2; 3], [1; 2]) %!error ... %! splitapply (@(x) sum (x), [1; 2; 3], [1; 2]) %!error ... %! splitapply (@(x) sum (x), [1; 2], [0; 1]) %!error ... %! splitapply (@(x) sum (x), [1; 2], [1; 3]) pr0m1th3as-datatypes-9450b29/inst/string.m000066400000000000000000006424611527370255000203540ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef string ## -*- texinfo -*- ## @deftp {datatypes} string ## ## Array representing sequences of characters. ## ## A string array is an array, where each element stores a sequence of ## characters of arbitrary length. ## ## A string array can also have missing elements, which differ from a sequence ## of characters of zero length (the equivalent of an empty character vector). ## ## To enable existing functions to handle string arrays as if they were cell ## arrays of character vectors or character arrays, use the ## @code{convertCharsToStrings} function inside your code. To enable ## functions working with string arrays to accept cell arrays of character ## vectors or character vectors as if they were string arrays or string ## scalars, use the @code{convertStringsToChars} function. ## ## @seealso{strings, convertCharsToStrings, convertStringsToChars} ## @end deftp properties (SetAccess = private, Hidden) ## Text data strs = {''} ## Missing values flag isMissing = false endproperties methods (Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif __disp__ (this, 'string', in_name); endfunction ## Custom display function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif __disp__ (this, 'string'); endfunction endmethods methods (Static, Access = public) ## -*- texinfo -*- ## @deftypefn {string} {@var{E} =} string.empty () ## @deftypefnx {string} {@var{E} =} string.empty (@var{sz}) ## @deftypefnx {string} {@var{E} =} string.empty (@var{m}, @var{n}, @dots{}) ## ## Create an empty string array. ## ## @code{@var{E} = string.empty ()} returns a @math{0*0} empty string array. ## @code{string.empty (@var{m}, @var{n}, @dots{})} or @code{string.empty ## (@var{sz})} returns an empty string array of the requested size, which ## must have at least one dimension equal to zero. A lone dimension gives a ## square size, so @code{string.empty (3)} is an error while ## @code{string.empty (0)} is @math{0*0}. As for @code{zeros}, a negative ## dimension counts as zero, and a size vector with nothing in it names no ## size and gives @math{0*0}. ## ## @end deftypefn function E = empty (varargin) if (nargin == 0) sz = [0, 0]; elseif (nargin == 1 && ! isscalar (varargin{1})) sz = double (varargin{1}(:)).'; else sz = [varargin{:}]; endif if (! (isnumeric (sz) && isrow (sz) && all (sz == fix (sz)))) error ("string.empty: dimensions must be integer values."); endif ## A negative dimension is no smaller than none at all, and a size vector ## holding no dimensions names no size: 'zeros' and every other array ## constructor read both the same way. sz = max (sz, 0); if (isempty (sz)) sz = [0, 0]; elseif (isscalar (sz)) sz = [sz, sz]; endif if (all (sz != 0)) error (strcat ("string.empty: at least one dimension must be zero", ... " for an empty array.")); endif E = string (cell (sz)); endfunction endmethods ################################################################################ ## ** Create String and Convert Type ** ## ################################################################################ ## Available Methods ## ## ## ## 'string' 'dispstrings' 'cellstr' 'cell' ## ## 'char' 'double' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {string} {@var{str} =} string () ## @deftypefnx {string} {@var{str} =} string (@var{in}) ## @deftypefnx {string} {@var{str} =} string (@var{calendarDuration}, @qcode{'Format'}, @var{FMT}) ## @deftypefnx {string} {@var{str} =} string (@var{duration}, @qcode{'Format'}, @var{FMT}) ## @deftypefnx {string} {@var{str} =} string (@{@var{in1}, @dots{}, @var{inN}@}) ## ## Create a new string array. ## ## @code{@var{str} = string ()} creates a scalar string array, whose element ## contains an empty character vector. ## ## @code{@var{str} = string (@var{in})} creates a string array of the same ## size as @var{in}, which is converted to string according to the following ## options: ## ## @itemize ## @item character arrays are converted so that each row becomes a string ## element, with any trailing whitespace preserved; cell arrays of character ## vectors are stored as-is. ## @item numeric arrays are converted with one precision for the whole ## array: @math{P} significant digits, where @math{P} is four more than the ## base-10 logarithm of its largest finite magnitude, rounded up, and lies ## between 5 and 16. Integer types are written exactly, and @qcode{NaN} ## becomes a missing string. ## @item logical arrays are converted to either @qcode{false} or ## @qcode{true} character sequences. ## @item categorical arrays are converted via their @code{cellstr} method. ## @item datetime arrays are converted via their @code{dispstrings} method. ## @item calendarDuration arrays and duration arrays are converted via their ## respective @code{cellstr} methods, in which case an extra pair argument ## is supported to allow parsing to the respective method the appropriate ## display format. See @qcode{calendarDuration} and @qcode{duration} for ## valid formats parsed through @var{FMT} to each class method. Extra input ## arguments to the @code{string} constructor except for this case are ## ignored. ## @item missing arrays are converted to a string array of missing elements. ## @end itemize ## ## @code{@var{str} = string (@{@var{in1}, @dots{}, @var{inN}@})} creates a ## string array from a cell array, which may contain any combination of the ## aforementioned data types, provided that each cell element is compatible ## to a string scalar. When using this syntax, calendarDuration arrays and ## duration arrays are converted via their @code{dispstrings} method, hence ## no extra format argument is meaningful. ## ## @seealso{calendarDuration, categorical, datetime, duration, missing} ## @end deftypefn function this = string (in, varargin) ## Return empty string if (nargin == 0) return; endif ## Handle empty input if (isempty (in) && ! ischar (in)) sz = size (in); this.strs = repmat ({''}, sz); this.isMissing = false (sz); return; endif ## Handle string input first if (isa (in, "string")) this.strs = in.strs; this.isMissing = in.isMissing; return endif ## Handle all other valid cases if (isa (in, "categorical")) ## An undefined element becomes a missing string and carries no text. ## 'cellstr' of a categorical renders it as '', which is a ## display string and not a value: leaving it in the array would put ## those eleven characters where a missing string has nothing, and ## 'char' and 'cellstr' of the result would hand them back. this.strs = cellstr (in); this.isMissing = isundefined (in); this.strs(this.isMissing) = {''}; elseif (ischar (in)) ## Convert each row to a string element with 'num2cell' rather than ## 'cellstr', which would deblank and silently drop trailing whitespace ## (MATLAB's 'string' preserves it). An empty char array yields a ## single empty string element, matching the no-argument constructor. if (isempty (in)) this.strs = {''}; elseif (ndims (in) > 2) sz = size (in); nr = prod (sz([1,3:end])); nc = sz(2); in = reshape (in, nr, nc); in = num2cell (in, 2); sz(2) = []; this.strs = reshape (in, sz); else this.strs = num2cell (in, 2); endif this.isMissing = false (size (this.strs)); elseif (iscellstr (in)) this.strs = in; this.isMissing = false (size (this.strs)); elseif (iscell (in)) sz = size (in); this.strs = repmat ({''}, sz); this.isMissing = false (sz); fcn = @(x) isscalar (x) | isempty (x) | (ischar (x) & isvector (x)); all_scalar = all (cellfun (fcn, in)); if (! all_scalar) error (strcat ("string: cell array must explicitly contain", ... " scalar elements or character vectors.")); endif is_numeric = cellfun (@isnumeric, in); is_logical = cellfun (@islogical, in); is_cellstr = cellfun (@iscellstr, in); is_charvec = cellfun (@ischar, in); is_strings = cellfun (@(x) isa (x, "string"), in); is_missing = cellfun (@(x) isa (x, "missing"), in); is_datetime = cellfun (@(x) isa (x, "datetime"), in); class_types = {"duration", "calendarDuration"}; is_duration = cellfun (@(x) any (isa (x, class_types)), in); ## Check for unsupported classes all_support = all (is_numeric | is_logical | is_cellstr | ... is_charvec | is_strings | is_missing | ... is_datetime | is_duration); if (! all_support) error ("string: cell array contains unsupported types."); endif ## Handle numeric elements first (including empty cells []) if (any (is_numeric(:))) tmpval = in(is_numeric); sz = size (tmpval); strs = repmat ({''}, sz); tf_m = false (sz); is_empty = cellfun (@isempty, tmpval); tf_m(is_empty) = true; tmpval = cell2mat (tmpval(! tf_m)); strs(! tf_m) = arrayfun (@(x) num2strs (x, x){1}, tmpval, ... "UniformOutput", false); is_nan = strcmp (strs, 'NaN'); strs(is_nan) = {''}; tf_m(is_nan) = true; this.strs(is_numeric) = strs; this.isMissing(is_numeric) = tf_m; endif ## Handle logical (no missing values here) if (any (is_logical(:))) tmpval = in(is_logical); sz = size (tmpval); strs = repmat ({'false'}, sz); tf_m = false (sz); is_true = logical (cell2mat (tmpval)); strs(is_true) = {'true'}; this.strs(is_logical) = strs; this.isMissing(is_logical) = tf_m; endif ## Handle cell arrays of character vectors if (any (is_cellstr(:))) this.strs(is_cellstr) = in{is_cellstr}; endif ## Handle character vectors (including empty 0x0 char vectors '') if (any (is_charvec(:))) this.strs(is_charvec) = in(is_charvec); endif ## Handle strings if (any (is_strings(:))) this.strs(is_strings) = [in{is_strings}].strs; this.isMissing(is_strings) = [in{is_strings}].isMissing; endif ## Handle missing objects if (any (is_missing(:))) this.isMissing(is_missing) = true; endif ## Handle datetime objects if (any (is_datetime(:))) tmpval = in(is_datetime); sz = size (tmpval); strs = repmat ({''}, sz); tf_m = false (sz); is_nat = cellfun (@isnat, tmpval); tf_m(is_nat) = true; strs(! is_nat) = cellfun (@dispstrings, tmpval(! is_nat)); this.strs(is_datetime) = strs; this.isMissing(is_datetime) = tf_m; endif ## Handle duration and calendarDuration objects if (any (is_duration(:))) tmpval = in(is_duration); sz = size (tmpval); strs = repmat ({''}, sz); tf_m = false (sz); is_nan = cellfun (@isnan, tmpval); tf_m(is_nan) = true; strs(! is_nan) = cellfun (@dispstrings, tmpval(! is_nan)); this.strs(is_duration) = strs; this.isMissing(is_duration) = tf_m; endif elseif (isnumeric (in)) is_nan = isnan (in); this.isMissing = is_nan; strs = repmat ({''}, size (in)); strs(! is_nan) = num2strs (in(! is_nan), in); this.strs = strs; elseif (islogical (in)) sz = size (in); strs = repmat ({'false'}, sz); strs(in) = {'true'}; this.strs = strs; this.isMissing = false (sz); elseif (isa (in, 'datetime')) is_nat = isnat (in); this.isMissing = is_nat; if (any (is_nat(:))) strs = repmat ({''}, size (in)); strs(! is_nat) = dispstrings (in(! is_nat)); this.strs = strs; else this.strs = dispstrings (in); endif elseif (isa (in, 'duration') || isa (in, 'calendarDuration')) is_nan = isnan (in); this.isMissing = is_nan; if (any (is_nan(:))) strs = repmat ({''}, size (in)); strs(! is_nan) = cellstr (in(! is_nan), varargin{:}); this.strs = strs; else this.strs = cellstr (in, varargin{:}); endif elseif (isa (in, 'missing')) this.strs = repmat ({''}, size (in)); this.isMissing = true (size (in)); else error ("string: unsupported input type: '%s'", class (in)); endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{cstr} =} dispstrings (@var{str}) ## ## Get display formatted strings for each element of a string object. ## ## @code{@var{cstr} = dispstrings (@var{str})} returns a cellstr array of ## character vectors, @var{cstr}, which has the same size as the input ## string object, @var{str}. These character vectors will either be the ## string contents of each corresponding element or @qcode{} for ## missing values. ## ## Composed string elements, i.e. double quoted strings, are translated so ## that any special characters are represented by their corresponding ## escaped character sequence, unless the input string, @var{str}, is a ## scalar, in which case text retains its original composition but newlines ## are prepadded with four white space characters for aligned display. ## ## @end deftypefn function cstr = dispstrings (this) cstr = strcat ({'"'}, this.strs, {'"'}); if (! isscalar (this)) cstr = strrep (cstr, sprintf ("\r\n"), '↵'); cstr = strrep (cstr, sprintf ("\n"), '↵'); cstr = strrep (cstr, sprintf ("\t"), '→'); else cstr = strrep (cstr, sprintf ("\n"), sprintf ("\n%s", ' ')); endif cstr(this.isMissing) = ""; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{cstr} =} cellstr (@var{str}) ## ## Convert string array to a cell array of character vectors. ## ## @code{@var{cstr} = cellstr (@var{str})} returns a cell array of ## character vectors, @var{cstr}, which has the same size as the input ## string @var{str}. Both empty strings and missing values are returned as ## empty character vectors. ## ## @end deftypefn function cstr = cellstr (this) cstr = this.strs; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{c_arr} =} cell (@var{str}) ## ## Convert string array to a cell array. ## ## @code{@var{c_arr} = cell (@var{str})} returns a cell array, @var{c_arr}, ## which has the same size as the input string @var{str}. All elements in ## @var{str} that represent real or complex numbers are converted to ## equivalent double values, whereas all other non-missing elements are ## converted to character vectors. Zero-length strings are converted to ## @qcode{''} empty character vectors, while missing values are returned as ## @qcode{[]} empty numeric vectors. ## ## @end deftypefn function c_arr = cell (this) ## Convert numbers first X = double (this); c_arr = num2cell (X); ## Grab everything else is_text = isnan (X) & ! this.isMissing; c_arr(is_text) = this.strs(is_text); c_arr(this.isMissing) = {[]}; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{c_mat} =} char (@var{str}) ## ## Convert string array to a character matrix. ## ## @code{@var{c_mat} = char (@var{str})} returns a character matrix, ## @var{c_mat}, which contains as many rows as the elements of the string. ## Both empty strings and missing values are returned as empty character ## vectors. ## ## @end deftypefn function c_mat = char (this) c_mat = char (this.strs{:}); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{X} =} double (@var{str}) ## ## Convert string array to a double array. ## ## @code{@var{X} = double (@var{str})} returns a double array, @var{X}, which ## has the same size as the input string @var{str}. All elements in ## @var{str} that represent real or complex numbers are converted to ## equivalent double values. Otherwise, @qcode{NaN} is returned. ## ## @end deftypefn function out = double (this) out = NaN (size (this)); fcn = @(x) str2num (char (x)); c_out = cellfun (fcn, this.strs, "UniformOutput", false); t_num = ! cellfun (@isempty, c_out); out(t_num) = cell2mat (c_out(t_num)); endfunction endmethods ################################################################################ ## ** Summary Information ** ## ################################################################################ ## Available Methods ## ## ## ## 'size' 'ndims' 'numel' 'length' ## ## 'strlength' 'count' 'keyHash' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {string} {@var{sz} =} size (@var{str}) ## @deftypefnx {string} {@var{dim_sz} =} size (@var{str}, @var{dim}) ## @deftypefnx {string} {@var{dim_sz} =} size (@var{str}, @var{d1}, @var{d2}, @dots{}) ## @deftypefnx {string} {[@var{rows}, @var{columns}, @dots{}, @var{dim_n_sz}] =} size (@dots{}) ## ## Return the size of a string array. ## ## @code{@var{sz} = size (@var{str})} returns a row vector with the size ## (number of elements) of each dimension for the string array @var{str}. ## ## @code{@var{dim_sz} = size (@var{str}, @var{dim})} returns the size of ## the corresponding dimension specified in @var{dim}. If @var{dim} is a ## vector, then @var{dim_sz} is a vector of the same length and with each ## element corresponding to a specified dimension. Multiple dimensions may ## also be specified as separate arguments. ## ## With a single output argument, @code{size} returns a row vector. With ## several, the size of dimension N is returned in the Nth argument. ## Asking for fewer output arguments than the array has dimensions folds ## the trailing dimensions into the last one, so @code{[r, c] = size ## (@var{str})} on a 2-by-2-by-2 array returns 2 and 4; asking for more pads ## with ones. When a dimension is named, however, the number of output ## arguments must equal the number of dimensions requested. ## ## @end deftypefn function varargout = size (this, varargin) if (! isempty (varargin)) ## A named dimension fixes how many outputs there may be. sz = size (this.strs, varargin{:}); if (nargout > 1 && numel (sz) != nargout) error (strcat ("string.size: nargout does not match the", ... " number of requested dimensions.")); endif else sz = size (this.strs); ## With no dimension named, asking for more outputs than the array has ## dimensions pads with ones, and asking for fewer folds the trailing ## dimensions into the last output, as core Octave and MATLAB both do. if (nargout > numel (sz)) sz = [sz, ones(1, nargout - numel (sz))]; elseif (nargout > 1 && nargout < numel (sz)) sz = [sz(1:nargout-1), prod(sz(nargout:end))]; endif endif if (nargout == 0 || nargout == 1) varargout{1} = sz; else for i = 1:nargout varargout{i} = sz(i); endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{out} =} ndims (@var{str}) ## ## Number of dimensions in a string array. ## ## @code{@var{out} = ndims (@var{str})} returns the number of dimensions of ## the string array @var{D}. ## ## @end deftypefn function out = ndims (this) out = ndims (this.strs); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{out} =} numel (@var{str}) ## ## Total number of elements in a string array. ## ## @end deftypefn function out = numel (this, varargin) out = numel (this.strs); #out = 1 endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{N} =} length (@var{str}) ## ## Length of a string vector. ## ## @code{@var{N} = length (@var{str})} returns the size of the longest ## dimension of the string array @var{str}, unless any of its dimensions ## has zero length, in which case @code{length (@var{D})} returns 0. ## ## @end deftypefn function N = length (this) if (isempty (this.strs)) N = 0; else N = max (size (this.strs)); endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{out} =} strlength (@var{str}) ## ## Length of text in string arrays. ## ## @end deftypefn function out = strlength (this) out = NaN (size (this)); fcn = @(x) __unicode_length__ (x); TF = ! this.isMissing; out(TF) = cell2mat (cellfun (fcn, this.strs(TF), "UniformOutput", false)); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{out} =} count (@var{str}, @var{pattern}) ## @deftypefnx {string} {@var{out} =} count (@var{str}, @var{pattern}, @qcode{'IgnoreCase'}, @qcode{true}) ## ## Count occurences of pattern in string array. ## ## @code{@var{out} = count (@var{str}, @var{pattern})} returns a numerical ## array @var{out} of the same size as @var{str} containing the number of ## occurences of @var{pattern} in each corresponding element of @var{str}. ## ## @code{@var{out} = count (@var{str}, @var{pattern}, @qcode{'IgnoreCase'}, ## @qcode{true})} ignores case when identifying occurences of @var{pattern}. ## ## @end deftypefn function out = count (this, pattern, varargin) ## Check pattern if (nargin < 2) error ("string.count: PATTERN is required."); elseif (ischar (pattern) || isstring (pattern)) pattern = cellstr (pattern); elseif (! iscellstr (pattern)) error (strcat ("string.count: PATTERN must be a character vector,", ... " a string array, or cell array of character vectors.")); endif if (mod (numel (varargin), 2) != 0) error ("string.count: name-value arguments must be in pairs."); endif ## Parse optional paired arguments optNames = {'IgnoreCase'}; dfValues = {false}; [IgnoreCase, args] = parsePairedArguments (optNames, dfValues, ... varargin(:)); ## Validate optional paired arguments if (! (islogical (IgnoreCase) && isscalar (IgnoreCase))) error ("string.count: 'IgnoreCase' must be a logical scalar."); endif if (! isempty (args)) error ("string.count: invalid optional paired argument."); endif ## Accumulate the number of occurences of each pattern in each element out = zeros (size (this)); vid = ! this.isMissing; str = this.strs(vid); if (IgnoreCase) for i = 1:numel (pattern) out(vid) += cellfun ('numel', ... strfind (lower (str), lower (pattern{i}))); endfor else for i = 1:numel (pattern) out(vid) += cellfun ('numel', strfind (str, pattern{i})); endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{key} =} keyHash (@var{str}) ## @deftypefnx {string} {@var{key} =} keyHash (@var{str}, @var{base}) ## ## Generate a hash code for string array. ## ## @code{@var{key} = keyHash (@var{str})} generates a @qcode{uint64} scalar ## that represents the input array @var{str}. @code{keyHash} utilizes the ## 64-bit FNV-1a variant of the Fowler-Noll-Vo non-cryptographic hash ## function. ## ## @code{@var{key} = keyHash (@var{str}, @var{base})} also generates a ## 64-bit hash code using @var{base} as the offset basis for the FNV-1a ## hash algorithm. @var{base} must be a @qcode{uint64} integer type ## scalar. Use this syntax to cascade @code{keyHash} on multiple objects ## for which a single hash code is required. ## ## Elements are hashed together with their lengths, so the boundaries ## between them are part of the key and a string array does not hash as ## any other splitting of the same characters. Missing elements are equal ## to one another and distinct from the empty string. Two arrays that ## @code{keyMatch} reports as the same key always hash alike. ## ## Note that unlike MATLAB, this implementation does not use any random ## seed. As a result, @code{keyHash} will always generate the exact same ## hash key for any particular input across different workers and Octave ## sessions. ## ## @end deftypefn function key = keyHash (this, base = []) ## Initialize string with size and class name size_str = sprintf ('%dx', size (this.strs))(1:end-1); init_str = [size_str class(this)]; if (! isempty (base)) if (! (isscalar (base) && isa (base, 'uint64'))) error ("string.keyHash: BASE must be a UINT64 scalar."); endif key = __ckeyHash__(init_str, base); else key = __ckeyHash__(init_str); endif ## Compute hash with underlying string array values. Passed whole ## rather than flattened with [this.strs{:}]: __ckeyHash__ frames each ## element with its length, so the boundaries between elements are part ## of the key. Flattening lost them, and ["ab", "c"] was the same key ## as ["a", "bc"]. key = __ckeyHash__ (this.strs, key); key = __nkeyHash__ (this.isMissing(:), key); endfunction endmethods ################################################################################ ## ** Query Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'contains' 'endsWith' 'startsWith' 'matches' ## ## 'iscolumn' 'isempty' 'ismatrix' 'ismember' ## ## 'ismissing' 'isrow' 'isscalar' 'issorted' ## ## 'issortedrows' 'isstring' 'isvector' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} contains (@var{str}, @var{pattern}) ## @deftypefnx {string} {@var{TF} =} contains (@var{str}, @var{pattern}, @qcode{'IgnoreCase'}, @qcode{true}) ## ## Test if strings contain pattern. ## ## @code{@var{TF} = contains (@var{str}, @var{pattern})} returns a logical ## array @var{TF} of the same size as @var{A} containing @qcode{true} for ## each corresponding element of @var{str} that contains the specified ## @var{pattern} and @qcode{false} otherwise. Similarly to @qcode{NaN} ## values, @qcode{} elements do not match any pattern and always ## return @qcode{false}. ## ## @code{@var{TF} = contains (@var{str}, @var{pattern}, ## @qcode{'IgnoreCase'}, @qcode{true})} ignores case when determining if ## @var{str} ends with @var{pattern}. ## ## @end deftypefn function TF = contains (this, pattern, varargin) ## Check pattern if (nargin < 2) error ("string.contains: PATTERN is required."); elseif (ischar (pattern) || isstring (pattern)) pattern = cellstr (pattern); elseif (! iscellstr (pattern)) error (strcat ("string.contains: PATTERN must be a character", ... " vector, a string array, or cell array of", ... " character vectors.")); endif if (mod (numel (varargin), 2) != 0) error ("string.contains: name-value arguments must be in pairs."); endif ## Parse optional paired arguments optNames = {'IgnoreCase'}; dfValues = {false}; [IgnoreCase, args] = parsePairedArguments (optNames, dfValues, ... varargin(:)); ## Validate optional paired arguments if (! (islogical (IgnoreCase) && isscalar (IgnoreCase))) error ("string.contains: 'IgnoreCase' must be a logical scalar."); endif if (! isempty (args)) error ("string.contains: invalid optional paired argument."); endif ## Check for the occurence of each pattern in the nonmissing elements of ## the input string array and boolean OR the results TF = false (size (this)); vid = ! this.isMissing; str = this.strs(vid); if (IgnoreCase) for i = 1:numel (pattern) idx = strfind (lower (str), lower (pattern{i})); TF(vid) |= ! cellfun ('isempty', idx); endfor else for i = 1:numel (pattern) idx = strfind (str, pattern{i}); TF(vid) |= ! cellfun ('isempty', idx); endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} endsWith (@var{str}, @var{pattern}) ## @deftypefnx {string} {@var{TF} =} endsWith (@var{str}, @var{pattern}, @qcode{'IgnoreCase'}, @qcode{true}) ## ## Test if strings end with pattern. ## ## @code{@var{TF} = endsWith (@var{str}, @var{pattern})} returns a logical ## array @var{TF} of the same size as @var{A} containing @qcode{true} for ## each corresponding element of @var{str} that ends with the specified ## @var{pattern} and @qcode{false} otherwise. Similarly to @qcode{NaN} ## values, @qcode{} elements do not match any pattern and always ## return @qcode{false}. ## ## @code{@var{TF} = endsWith (@var{str}, @var{pattern}, ## @qcode{'IgnoreCase'}, @qcode{true})} ignores case when determining if ## @var{str} ends with @var{pattern}. ## ## @end deftypefn function TF = endsWith (this, pattern, varargin) ## Check pattern if (nargin < 2) error ("string.endsWith: PATTERN is required."); elseif (ischar (pattern) || isstring (pattern)) pattern = cellstr (pattern); elseif (! iscellstr (pattern)) error (strcat ("string.endsWith: PATTERN must be a character", ... " vector, a string array, or cell array of", ... " character vectors.")); endif if (mod (numel (varargin), 2) != 0) error ("string.endsWith: name-value arguments must be in pairs."); endif ## Parse optional paired arguments optNames = {'IgnoreCase'}; dfValues = {false}; [IgnoreCase, args] = parsePairedArguments (optNames, dfValues, ... varargin(:)); ## Validate optional paired arguments if (! (islogical (IgnoreCase) && isscalar (IgnoreCase))) error ("string.endsWith: 'IgnoreCase' must be a logical scalar."); endif if (! isempty (args)) error ("string.endsWith: invalid optional paired argument."); endif ## Reverse str and pattern str = cellfun ('flip', this.strs, "UniformOutput", false); pattern = cellfun ('flip', cellstr (pattern), "UniformOutput", false); ## For each pattern, trim all elements of the input string array to the ## length of the pattern and compare strings according to IgnoreCase TF = false (size (this)); vid = ! this.isMissing; if (IgnoreCase) for i = 1:numel (pattern) TF(vid) |= strncmpi (str(vid), pattern{i}, length (pattern{i})); endfor else for i = 1:numel (pattern) TF(vid) |= strncmp (str(vid), pattern{i}, length (pattern{i})); endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} startsWith (@var{str}, @var{pattern}) ## @deftypefnx {string} {@var{TF} =} startsWith (@var{str}, @var{pattern}, @qcode{'IgnoreCase'}, @qcode{true}) ## ## Test if strings start with pattern. ## ## @code{@var{TF} = startsWith (@var{str}, @var{pattern})} returns a logical ## array @var{TF} of the same size as @var{A} containing @qcode{true} for ## each corresponding element of @var{str} that starts with the specified ## @var{pattern} and @qcode{false} otherwise. Similarly to @qcode{NaN} ## values, @qcode{} elements do not match any pattern and always ## return @qcode{false}. ## ## @code{@var{TF} = startsWith (@var{str}, @var{pattern}, ## @qcode{'IgnoreCase'}, @qcode{true})} ignores case when determining if ## @var{str} starts with @var{pattern}. ## ## @end deftypefn function TF = startsWith (this, pattern, varargin) ## Check pattern if (nargin < 2) error ("string.startsWith: PATTERN is required."); elseif (ischar (pattern) || isstring (pattern)) pattern = cellstr (pattern); elseif (! iscellstr (pattern)) error (strcat ("string.startsWith: PATTERN must be a character", ... " vector, a string array, or cell array of", ... " character vectors.")); endif if (mod (numel (varargin), 2) != 0) error ("string.startsWith: name-value arguments must be in pairs."); endif ## Parse optional paired arguments optNames = {'IgnoreCase'}; dfValues = {false}; [IgnoreCase, args] = parsePairedArguments (optNames, dfValues, ... varargin(:)); ## Validate optional paired arguments if (! (islogical (IgnoreCase) && isscalar (IgnoreCase))) error ("string.startsWith: 'IgnoreCase' must be a logical scalar."); endif if (! isempty (args)) error ("string.startsWith: invalid optional paired argument."); endif ## For each pattern, trim all elements of the input string array to the ## length of the pattern and compare strings according to IgnoreCase TF = false (size (this)); vid = ! this.isMissing; str = this.strs(vid); if (IgnoreCase) for i = 1:numel (pattern) TF(vid) |= strncmpi (str, pattern{i}, length (pattern{i})); endfor else for i = 1:numel (pattern) TF(vid) |= strncmp (str, pattern{i}, length (pattern{i})); endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} matches (@var{str}, @var{pattern}) ## @deftypefnx {string} {@var{TF} =} matches (@var{str}, @var{pattern}, @qcode{'IgnoreCase'}, @qcode{true}) ## ## Test if strings match pattern. ## ## @code{@var{TF} = matches (@var{str}, @var{pattern})} returns a logical ## array @var{TF} of the same size as @var{A} containing @qcode{true} for ## each corresponding element of @var{str} that matches the specified ## @var{pattern} and @qcode{false} otherwise. Similarly to @qcode{NaN} ## values, @qcode{} elements do not match any pattern and always ## return @qcode{false}. ## ## @code{@var{TF} = matches (@var{str}, @var{pattern}, @qcode{'IgnoreCase'}, ## @qcode{true})} ignores case when determining if @var{str} starts with ## @var{pattern}. ## ## @end deftypefn function TF = matches (this, pattern, varargin) ## Check pattern if (nargin < 2) error ("string.matches: PATTERN is required."); elseif (ischar (pattern) || isstring (pattern)) pattern = cellstr (pattern); elseif (! iscellstr (pattern)) error (strcat ("string.matches: PATTERN must be a character", ... " vector, a string array, or cell array of", ... " character vectors.")); endif if (mod (numel (varargin), 2) != 0) error ("string.matches: name-value arguments must be in pairs."); endif ## Parse optional paired arguments optNames = {'IgnoreCase'}; dfValues = {false}; [IgnoreCase, args] = parsePairedArguments (optNames, dfValues, ... varargin(:)); ## Validate optional paired arguments if (! (islogical (IgnoreCase) && isscalar (IgnoreCase))) error ("string.matches: 'IgnoreCase' must be a logical scalar."); endif if (! isempty (args)) error ("string.matches: invalid optional paired argument."); endif ## Check for the matching of each pattern in the nonmissing elements of ## the input string array and boolean OR the results TF = false (size (this)); vid = ! this.isMissing; str = this.strs(vid); if (IgnoreCase) for i = 1:numel (pattern) TF(vid) |= strcmpi (str, pattern{i}); endfor else for i = 1:numel (pattern) TF(vid) |= strcmp (str, pattern{i}); endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} iscolumn (@var{str}) ## ## Test if string array is a column vector. ## ## @var{TF} is @qcode{true}, if string array @var{str} is a column vector. ## ## @end deftypefn function TF = iscolumn (this) TF = iscolumn (this.isMissing); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} isempty (@var{str}) ## ## Test if string array is empty. ## ## @var{TF} is @qcode{true}, if string array @var{str} is empty. ## ## @end deftypefn function TF = isempty (this) TF = isempty (this.isMissing); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} ismatrix (@var{str}) ## ## Test if string array is a matrix. ## ## @var{TF} is @qcode{true}, if string array @var{str} is a matrix. ## ## @end deftypefn function TF = ismatrix (this) TF = ismatrix (this.isMissing); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} ismember (@var{A}, @var{B}) ## @deftypefnx {string} {@var{TF} =} ismember (@var{A}, @var{B}, @qcode{'rows'}) ## @deftypefnx {string} {[@var{TF}, @var{index}] =} ismember (@dots{}) ## @deftypefnx {string} {[@var{TF}, @var{index}] =} ismember (@dots{}, @qcode{'legacy'}) ## ## Find string elements in a set. ## ## @code{@var{TF} = ismember (@var{A}, @var{B})} returns a logical array ## @var{TF} of the same size as @var{A} containing @qcode{true} for each ## corresponding element of @var{A} that is in @var{B} and @qcode{false} ## otherwise. Similarly to @qcode{NaN} values, @qcode{} elements ## are not equal with each other and always return @qcode{false}. ## ## @code{@var{TF} = ismember (@var{A}, @var{B}, @qcode{'rows'})} only ## applies to string matrices with the same number of columns, in which ## case the logical vector @var{TF} contains @qcode{true} for each row of ## @var{A} that is also a row in @var{B}. @var{TF} has the same number of ## rows as @var{A}. ## ## @code{[@var{TF}, @var{index}] = ismember (@var{A}, @var{B})} also returns ## an index array of the same size as @var{A} containing the lowest index in ## @var{B} for each element of @var{A} that is a member of @var{B} and 0 ## otherwise. If the @qcode{'rows'} optional argument is used, then the ## returning index is a column vector with the same rows as @var{A} and it ## contains the lowest index in @var{B} for each row of @var{A} that is a ## member of @var{B} and 0 otherwise. If the @qcode{'legacy'} optional ## argument is specified, then the highest index of matched elements is ## returned. Unless multiple matches exist, the @qcode{'legacy'} option has ## no effect on the returned @var{index}. ## ## @end deftypefn function varargout = ismember (A, B, varargin) if (iscategorical (B)) A = categorical (A); if (nargout > 1) [varargout{1}, varargout{2}] = ismember (A, B, varargin{:}); else varargout{1} = ismember (A, B, varargin{:}); endif return; endif if (iscellstr (A) || ischar (A)) A = string (A); elseif (iscellstr (B) || ischar (B)) B = string (B); endif if (! isstring (A)) error ("string.ismember: first input argument must be text."); endif if (! isstring (B)) error ("string.ismember: second input argument must be text."); endif if (nargin > 2 && any (strcmp (varargin, 'rows'))) if (columns (A) != columns (B)) error (strcat ("string.ismember: A and B must have the same", ... " number of columns with the 'rows' option.")); endif endif ## Handle empty input array if (isempty (A) || isempty (B)) sz = size (A); varargout{1} = false (sz); if (nargout > 1) varargout{2} = zeros (sz); endif else if (nargout > 1) [TF, index] = __ismember__ (A.strs, B.strs, varargin{:}); TF(A.isMissing) = false; index(A.isMissing) = 0; varargout{1} = TF; varargout{2} = index; else TF = __ismember__ (A.strs, B.strs, varargin{:}); TF(A.isMissing) = false; varargout{1} = TF; endif endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} ismissing (@var{str}) ## @deftypefnx {string} {@var{TF} =} ismissing (@var{str}, @var{indicator}) ## ## Find missing elements in string array. ## ## @code{@var{TF} = ismissing (@var{str})} returns a logical array, ## @var{TF}, with any @qcode{true} values corresponding to missing elements ## in the input string array @var{str}. ## ## @code{@var{TF} = ismissing (@var{str}, @var{indicator})} also returns a ## logical array, @var{TF}, with any @qcode{true} values corresponding to ## elements in the input string array @var{str}, which are lexicographically ## equal to the values in @var{indicator}. ## ## @var{indicator} must be either a character vector or a string vector ## or a cell vector of character vectors. A @qcode{} element of ## a string @var{indicator} matches the @qcode{} elements of ## @var{str}, and so does @code{missing}. ## ## The output array @var{TF} has the same size as the input array @var{str}. ## ## @end deftypefn function TF = ismissing (this, varargin) if (nargin > 2) error ("string.ismissing: too many input arguments."); endif if (! isempty (varargin)) indicator = varargin{1}; TF = false (size (this)); if (isa (indicator, 'missing')) TF = this.isMissing; elseif (isvector (indicator)) if (ischar (indicator)) TF(this == indicator) = true; elseif (isstring (indicator)) for idx = 1:numel (indicator) if (indicator.isMissing(idx)) TF(this.isMissing) = true; else TF(this == indicator.strs(idx)) = true; endif endfor elseif (iscellstr (indicator)) for idx = 1:numel (indicator) TF(this == indicator(idx)) = true; endfor else error ("string.ismissing: INDICATOR must be a text array."); endif else error ("string.ismissing: INDICATOR must be a vector."); endif else TF = this.isMissing; endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} isrow (@var{str}) ## ## Test if string array is a row vector. ## ## @var{TF} is @qcode{true}, if string array @var{str} is a row vector. ## ## @end deftypefn function TF = isrow (this) TF = isrow (this.isMissing); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} isscalar (@var{str}) ## ## Test if string array is a scalar. ## ## @var{TF} is @qcode{true}, if string array @var{str} is a scalar. ## ## @end deftypefn function TF = isscalar (this) TF = isscalar (this.isMissing); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} issorted (@var{str}) ## @deftypefnx {string} {@var{TF} =} issorted (@var{str}, @var{dim}) ## @deftypefnx {string} {@var{TF} =} issorted (@var{str}, @var{direction}) ## @deftypefnx {string} {@var{TF} =} issorted (@var{str}, @var{dim}, @var{direction}) ## @deftypefnx {string} {@var{TF} =} issorted (@dots{}, @qcode{'MissingPlacement'}, @var{MP}) ## ## Return true if string array is sorted. ## ## @code{@var{TF} = issorted (@var{str})} returns a logical scalar @var{TF}, ## which is @qcode{true}, if the string array @var{str} is sorted in ## ascending order, and @qcode{false} otherwise. ## ## @code{@var{TF} = issorted (@var{str}, @var{dim})} returns a logical ## scalar @var{TF}, which is @qcode{true}, if the string array @var{str} ## is sorted in ascending order along the dimension @var{dim}, and ## @qcode{false} otherwise. ## ## @code{@var{TF} = issorted (@var{str}, @var{direction})} returns a logical ## scalar @var{TF}, which is @qcode{true}, if the string array @var{str} is ## sorted in the direction specified by @var{direction}, and @qcode{false} ## otherwise. @var{direction} can be any of the following options: ## ## @itemize ## @item @qcode{'ascend'}, which is the default, checks if elements are in ## ascending order. ## @item @qcode{'descend'} checks if elements are in descending order. ## @item @qcode{'monotonic'} checks if elements are either in ascending or ## descending order. ## @item @qcode{'strictascend'} checks if elements are in ascending order ## and there are no duplicate or missing elements. ## @item @qcode{'strictdescend'} checks if elements are in descending order ## and there are no duplicate or missing elements. ## @item @qcode{'strictmonotonic'} checks if elements are either in ## ascending or descending order and there are no duplicate or missing ## elements. ## @end itemize ## ## @code{@var{TF} = issorted (@dots{}, @qcode{'MissingPlacement'}, ## @var{MP})} specifies where missing elements (@qcode{}) are ## placed with one of the following options specified in @var{MP}: ## ## @itemize ## @item @qcode{'auto'}, which is the default, places missing elements last ## for ascending sort and first for descending sort. ## @item @qcode{'first'} places missing elements first. ## @item @qcode{'last'} places missing elements last. ## @end itemize ## ## @end deftypefn function TF = issorted (this, varargin) ## Single input argument if (nargin == 1) TF = isequaln (this, sort (this)); return; endif ## Get operating dimension cid = cellfun (@isnumeric, varargin); if (any (cid)) dim = varargin{cid}; else sz = size (this); dim = find (sz != 1, 1); if (isempty (dim)) # scalar dim = 1; endif endif ## Parse and validate optional 'MissingPlacement' paired argument optNames = {'MissingPlacement'}; dfValues = {'auto'}; [MP, args] = parsePairedArguments (optNames, dfValues, varargin(:)); ## The values are matched without regard to case, as MATLAB does. MP = lower (MP); if (! ismember (MP, {'auto', 'first', 'last'})) error ("string.issorted: invalid value for 'MissingPlacement'."); endif ## Force strings to character vectors [args{:}] = convertStringsToChars (args{:}); ## Get direction cid = cellfun (@(x) ischar (x), args); if (any (cid)) direction = lower (args{cid}); ## Check for type of direction valid_direction = {'ascend', 'descend', 'monotonic', 'strictascend', ... 'strictdescend', 'strictmonotonic'}; if (! ismember (direction, valid_direction)) error ("string.issorted: invalid DIRECTION value."); endif switch (direction) case {'ascend', 'descend'} TF = isequaln (this, sort (this, args{:}, 'MissingPlacement', MP)); case {'strictascend', 'strictdescend'} ## Check for missing values first (fast) if (any (this.isMissing, 'all')) TF = false; return; endif args{cid} = strrep (direction, 'strict', ''); sorted = sort (this, args{:}, 'MissingPlacement', MP); ## Detect duplicate adjacent elements through a numeric rank proxy ## (unique ranks preserve order); equal strings share a rank. [~, ~, ic] = unique (sorted.strs); ranks = reshape (ic, size (sorted)); if (any (diff (ranks, 1, dim) == 0, 'all')) TF = false; return; endif TF = isequal (this, sorted); case 'monotonic' ## Check for either ascending or descending args{cid} = 'ascend'; TF = isequaln (this, sort (this, args{:}, 'MissingPlacement', MP)); if (TF) return; endif args{cid} = 'descend'; TF = isequaln (this, sort (this, args{:}, 'MissingPlacement', MP)); case 'strictmonotonic' ## Check missing values first (fast) if (any (this.isMissing, 'all')) TF = false; return; endif ## Check for either ascending or descending args{cid} = 'ascend'; sorted = sort (this, args{:}, 'MissingPlacement', MP); [~, ~, ic] = unique (sorted.strs); ranks = reshape (ic, size (sorted)); if (any (diff (ranks, 1, dim) == 0, 'all')) TF = false; return; endif TF = isequal (this, sorted); if (TF) return; endif args{cid} = 'descend'; sorted = sort (this, args{:}, 'MissingPlacement', MP); [~, ~, ic] = unique (sorted.strs); ranks = reshape (ic, size (sorted)); if (any (diff (ranks, 1, dim) == 0, 'all')) TF = false; return; endif TF = isequal (this, sorted); endswitch else ## No DIRECTION input argument TF = isequaln (this, sort (this, args{:}, 'MissingPlacement', MP)); endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} issortedrows (@var{str}) ## @deftypefnx {string} {@var{TF} =} issortedrows (@var{str}, @var{col}) ## @deftypefnx {string} {@var{TF} =} issortedrows (@var{str}, @var{direction}) ## @deftypefnx {string} {@var{TF} =} issortedrows (@var{str}, @var{col}, @var{direction}) ## @deftypefnx {string} {@var{TF} =} issortedrows (@dots{}, @qcode{'MissingPlacement'}, @var{MP}) ## ## Return true if string matrix rows are sorted. ## ## @code{@var{TF} = issortedrows (@var{str})} returns a logical scalar ## @var{TF}, which is @qcode{true}, if the rows in the 2-D string array ## @var{str} are sorted in ascending order, and @qcode{false} otherwise. ## ## @code{@var{TF} = issortedrows (@var{str}, @var{col})} returns a logical ## scalar @var{TF}, which is @qcode{true}, if the string array @var{str} is ## sorted according to the columns specified by the vector @var{col}, and ## @qcode{false} otherwise. @var{col} must explicitly contain non-zero ## integers whose absolute values index existing columns in @var{str}. ## Positive elements sort the corresponding columns in ascending order, ## while negative elements sort the corresponding columns in descending ## order. ## ## @code{@var{TF} = issortedrows (@var{str}, @var{direction})} checks if the ## rows in @var{str} are sorted according to the specified direction, which ## can be one of the following options: ## ## @itemize ## @item @qcode{'ascend'}, which is the default, checks if elements are in ## ascending order. ## @item @qcode{'descend'} checks if elements are in descending order. ## @item @qcode{'monotonic'} checks if elements are either in ascending or ## descending order. ## @item @qcode{'strictascend'} checks if elements are in ascending order ## and there are no duplicate or missing elements. ## @item @qcode{'strictdescend'} checks if elements are in descending order ## and there are no duplicate or missing elements. ## @item @qcode{'strictmonotonic'} checks if elements are either in ## ascending or descending order and there are no duplicate or missing ## elements. ## @end itemize ## ## Alternatively, @var{direction} can be a cell array of character vectors ## specifying the sorting direction for each individual column of @var{str}, ## in which case the number of elements in @var{direction} must equal the ## number of columns in @var{str}. ## ## @code{@var{TF} = issortedrows (@var{str}, @var{col}, @var{direction})} ## checks if the rows in the string array @var{str} are sorted according to ## the columns specified in @var{col} using the corresponding sorting ## direction specified in @var{direction}. In this case, the sign of the ## values in @var{col} is ignored. @var{col} and @var{direction} must have ## the same length, but not necessarily the same number of elements as the ## columns in @var{str}. ## ## @code{@var{TF} = issortedrows (@dots{}, @qcode{'MissingPlacement'}, ## @var{MP})} specifies where missing elements (@qcode{}) are ## placed with one of the following options specified in @var{MP}: ## ## @itemize ## @item @qcode{'auto'}, which is the default, places missing elements last ## for ascending sort and first for descending sort. ## @item @qcode{'first'} places missing elements first. ## @item @qcode{'last'} places missing elements last. ## @end itemize ## ## @end deftypefn function TF = issortedrows (this, varargin) ## Single input argument if (nargin == 1) TF = isequaln (this, sortrows (this)); return; endif ## Force strings to character vectors or cell arrays of character vectors [varargin{:}] = convertStringsToChars (varargin{:}); ## Get valid direction(s) from input argument list valid = {'ascend', 'descend', 'monotonic', 'strictascend', ... 'strictdescend', 'strictmonotonic'}; fcn = @(x) (ischar (x) && ismember (x, valid)) || iscellstr (x); cid = cellfun (fcn, varargin); if (! any (cid)) ## No DIRECTION input argument TF = isequaln (this, sortrows (this, varargin{:})); return; endif direction = lower (cellstr (varargin{cid})); ## Check for valid type of directions in cellstring if (! all (cellfun (@(x) ismember (x, valid), direction))) error ("string.issortedrows: invalid DIRECTION value."); endif ## Handle purely non-strict directions (ascend/descend) if (all (cellfun (@(x) ismember (x, {'ascend', 'descend'}), direction))) TF = isequaln (this, sortrows (this, varargin{:})); return; endif ## ... and the simple set including 'monotonic' but no strict modes simple_types = {'ascend', 'descend', 'monotonic'}; if (all (cellfun (@(x) ismember (x, simple_types), direction))) idx = strcmp (direction, 'monotonic'); direction(idx) = {'ascend'}; varargin{cid} = direction; TF = isequaln (this, sortrows (this, varargin{:})); if (TF) return; endif direction(idx) = {'descend'}; varargin{cid} = direction; TF = isequaln (this, sortrows (this, varargin{:})); return; endif ## Handle strict modes. Determine the sort-key column order and the ## sort position holding the first strict direction, then derive the ## prefix (the columns sorted up to and including that position, on ## which the duplicate test operates) and the binding strict column ## (the single column on which the missing test operates). A missing ## value in the binding column, or two consecutive sorted rows equal ## across the prefix columns, rules out a strict ordering; strict ## positions after the first are subsumed by it. strict_types = {'strictascend', 'strictdescend', 'strictmonotonic'}; sflag = cellfun (@(x) ismember (x, strict_types), direction); nc = size (this, 2); nummask = cellfun (@isnumeric, varargin); if (any (nummask)) col = varargin{nummask}; ocols = abs (col(:)'); if (isscalar (direction)) sflag = repmat (sflag, 1, numel (col)); elseif (numel (direction) != numel (col)) error ("string.issortedrows: COL and DIRECTION mismatch."); endif else ocols = 1:nc; if (isscalar (direction)) sflag = repmat (sflag, 1, nc); endif endif K = find (sflag, 1); prefix = ocols(1:K); bindcol = ocols(K); ## A missing value in the binding strict column rules out strictness. if (any (this.isMissing(:,bindcol), 'all')) TF = false; return; endif ## Replace strict modes with their plain counterparts for the sort. direction = strrep (direction, 'strict', ''); varargin{cid} = direction; if (all (cellfun (@(x) ismember (x, {'ascend', 'descend'}), direction))) sorted = sortrows (this, varargin{:}); ## No two consecutive rows may tie across the prefix columns; a numeric ## rank proxy (unique ranks) gives equal strings an equal rank. [~, ~, ic] = unique (sorted.strs); ranks = reshape (ic, size (sorted)); tmpcol = ranks(:, prefix); if (any (all (diff (tmpcol, 1, 1) == 0, 2))) TF = false; return; endif TF = isequaln (this, sorted); else # a 'monotonic' position also exists idx = strcmp (direction, 'monotonic'); direction(idx) = {'ascend'}; varargin{cid} = direction; sorted = sortrows (this, varargin{:}); [~, ~, ic] = unique (sorted.strs); ranks = reshape (ic, size (sorted)); tmpcol = ranks(:, prefix); if (any (all (diff (tmpcol, 1, 1) == 0, 2))) TF = false; return; endif TF = isequaln (this, sorted); if (TF) return; endif direction(idx) = {'descend'}; varargin{cid} = direction; sorted = sortrows (this, varargin{:}); [~, ~, ic] = unique (sorted.strs); ranks = reshape (ic, size (sorted)); tmpcol = ranks(:, prefix); if (any (all (diff (tmpcol, 1, 1) == 0, 2))) TF = false; return; endif TF = isequaln (this, sorted); endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} isstring (@var{str}) ## ## Test for string array. ## ## @var{TF} is @qcode{true} for @code{string} inputs. ## ## @end deftypefn function TF = isstring (this) TF = true; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} isvector (@var{str}) ## ## Test if string array is a vector. ## ## @var{TF} is @qcode{true}, if string array @var{str} is a vector. ## ## @end deftypefn function TF = isvector (this) TF = isvector (this.isMissing); endfunction endmethods ################################################################################ ## ** Relational Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'eq' 'ge' 'gt' 'le' ## ## 'lt' 'ne' 'strcmp' 'strcmpi' ## ## 'strncmp' 'strncmpi' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} eq (@var{A}, @var{B}) ## ## Test for equality. ## ## @var{TF} is @qcode{true}, if string @var{A} is lexicographically equal to ## @var{B}. If one input is a string array, the other input can be a string ## array, a character vector, or a cell array of character vectors. This is ## equivalent to the @code{strcmp} function. ## ## @end deftypefn function TF = eq (A, B) ## Overload methods for certain data types if (any (isa (B, {'categorical', 'duration'}))) TF = eq (cellstr (A), B); return; endif if (iscellstr (A) || ischar (A)) A = string (A); elseif (iscellstr (B) || ischar (B)) B = string (B); elseif (! isa (A, 'string') || ! isa (B, 'string')) error (strcat ("string.eq: comparison between '%s' and '%s'", ... " is not supported."), class (A), class (B)); endif szA = size (A); szB = size (B); if (isscalar (A)) A = repmat (A, szB); elseif (isscalar (B)) B = repmat (B, szA); elseif (! isequal (szA, szB)) ## Force size dim vectors to equal length ndA = numel (szA); ndB = numel (szB); if (ndA > ndB) szB = [szB, ones(1, ndA - ndB)]; elseif (ndB > ndA) szA = [szA, ones(1, ndB - ndA)]; endif ## Check for compatible dimensions A_1 = szA != 1 & szA != szB; B_1 = szB != 1 & szA != szB; A_B = A_1 & B_1; if (any (A_B)) error ("string.eq: incompatible dimensions."); endif ## Expand arrays to same size eqAB = szA == szB; szA(eqAB) = 1; szB(eqAB) = 1; A = repmat (A, szB); B = repmat (B, szA); endif TF = strcmp (A.strs, B.strs); TF(A.isMissing | B.isMissing) = false; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} ge (@var{A}, @var{B}) ## ## Test for greater than or equal to. ## ## @var{TF} is @qcode{true}, if string @var{A} is lexicographically greater ## than or equal to @var{B}. If one input is a string array, the other ## input can be a string array, a character vector, or a cell array of ## character vectors. ## ## @end deftypefn function TF = ge (A, B) ## Overload methods for certain data types if (any (isa (B, {'categorical', 'duration'}))) TF = ge (cellstr (A), B); return; endif if (iscellstr (A) || ischar (A)) A = string (A); elseif (iscellstr (B) || ischar (B)) B = string (B); elseif (! isa (A, 'string') || ! isa (B, 'string')) error (strcat ("string.ge: comparison between '%s' and '%s'", ... " is not supported."), class (A), class (B)); endif szA = size (A); szB = size (B); if (isscalar (A)) A = repmat (A, szB); elseif (isscalar (B)) B = repmat (B, szA); elseif (! isequal (szA, szB)) ## Force size dim vectors to equal length ndA = numel (szA); ndB = numel (szB); if (ndA > ndB) szB = [szB, ones(1, ndA - ndB)]; elseif (ndB > ndA) szA = [szA, ones(1, ndB - ndA)]; endif ## Check for compatible dimensions A_1 = szA != 1 & szA != szB; B_1 = szB != 1 & szA != szB; A_B = A_1 & B_1; if (any (A_B)) error ("string.ge: incompatible dimensions."); endif ## Expand arrays to same size eqAB = szA == szB; szA(eqAB) = 1; szB(eqAB) = 1; A = repmat (A, szB); B = repmat (B, szA); endif out = sign_strings (A.strs, B.strs); TF = false (size (out)); TF(out >= 0) = true; TF(A.isMissing | B.isMissing) = false; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} gt (@var{A}, @var{B}) ## ## Test for greater than. ## ## @var{TF} is @qcode{true}, if string @var{A} is lexicographically greater ## than @var{B}. If one input is a string array, the other input can be a ## string array, a character vector, or a cell array of character vectors. ## ## @end deftypefn function TF = gt (A, B) ## Overload methods for certain data types if (any (isa (B, {'categorical', 'duration'}))) TF = gt (cellstr (A), B); return; endif if (iscellstr (A) || ischar (A)) A = string (A); elseif (iscellstr (B) || ischar (B)) B = string (B); elseif (! isa (A, 'string') || ! isa (B, 'string')) error (strcat ("string.gt: comparison between '%s' and '%s'", ... " is not supported."), class (A), class (B)); endif szA = size (A); szB = size (B); if (isscalar (A)) A = repmat (A, szB); elseif (isscalar (B)) B = repmat (B, szA); elseif (! isequal (szA, szB)) ## Force size dim vectors to equal length ndA = numel (szA); ndB = numel (szB); if (ndA > ndB) szB = [szB, ones(1, ndA - ndB)]; elseif (ndB > ndA) szA = [szA, ones(1, ndB - ndA)]; endif ## Check for compatible dimensions A_1 = szA != 1 & szA != szB; B_1 = szB != 1 & szA != szB; A_B = A_1 & B_1; if (any (A_B)) error ("string.gt: incompatible dimensions."); endif ## Expand arrays to same size eqAB = szA == szB; szA(eqAB) = 1; szB(eqAB) = 1; A = repmat (A, szB); B = repmat (B, szA); endif out = sign_strings (A.strs, B.strs); TF = false (size (out)); TF(out > 0) = true; TF(A.isMissing | B.isMissing) = false; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} le (@var{A}, @var{B}) ## ## Test for less than or equal to. ## ## @var{TF} is @qcode{true}, if string @var{A} is lexicographically less ## than or equal to @var{B}. If one input is a string array, the other ## input can be a string array, a character vector, or a cell array of ## character vectors. ## ## @end deftypefn function TF = le (A, B) ## Overload methods for certain data types if (any (isa (B, {'categorical', 'duration'}))) TF = le (cellstr (A), B); return; endif if (iscellstr (A) || ischar (A)) A = string (A); elseif (iscellstr (B) || ischar (B)) B = string (B); elseif (! isa (A, 'string') || ! isa (B, 'string')) error (strcat ("string.le: comparison between '%s' and '%s'", ... " is not supported."), class (A), class (B)); endif szA = size (A); szB = size (B); if (isscalar (A)) A = repmat (A, szB); elseif (isscalar (B)) B = repmat (B, szA); elseif (! isequal (szA, szB)) ## Force size dim vectors to equal length ndA = numel (szA); ndB = numel (szB); if (ndA > ndB) szB = [szB, ones(1, ndA - ndB)]; elseif (ndB > ndA) szA = [szA, ones(1, ndB - ndA)]; endif ## Check for compatible dimensions A_1 = szA != 1 & szA != szB; B_1 = szB != 1 & szA != szB; A_B = A_1 & B_1; if (any (A_B)) error ("string.le: incompatible dimensions."); endif ## Expand arrays to same size eqAB = szA == szB; szA(eqAB) = 1; szB(eqAB) = 1; A = repmat (A, szB); B = repmat (B, szA); endif out = sign_strings (A.strs, B.strs); TF = false (size (out)); TF(out <= 0) = true; TF(A.isMissing | B.isMissing) = false; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} lt (@var{A}, @var{B}) ## ## Test for less than. ## ## @var{TF} is @qcode{true}, if string @var{A} is lexicographically less ## than @var{B}. If one input is a string array, the other input can be a ## string array, a character vector, or a cell array of character vectors. ## ## @end deftypefn function TF = lt (A, B) ## Overload methods for certain data types if (any (isa (B, {'categorical', 'duration'}))) TF = lt (cellstr (A), B); return; endif if (iscellstr (A) || ischar (A)) A = string (A); elseif (iscellstr (B) || ischar (B)) B = string (B); elseif (! isa (A, 'string') || ! isa (B, 'string')) error (strcat ("string.lt: comparison between '%s' and '%s'", ... " is not supported."), class (A), class (B)); endif szA = size (A); szB = size (B); if (isscalar (A)) A = repmat (A, szB); elseif (isscalar (B)) B = repmat (B, szA); elseif (! isequal (szA, szB)) ## Force size dim vectors to equal length ndA = numel (szA); ndB = numel (szB); if (ndA > ndB) szB = [szB, ones(1, ndA - ndB)]; elseif (ndB > ndA) szA = [szA, ones(1, ndB - ndA)]; endif ## Check for compatible dimensions A_1 = szA != 1 & szA != szB; B_1 = szB != 1 & szA != szB; A_B = A_1 & B_1; if (any (A_B)) error ("string.lt: incompatible dimensions."); endif ## Expand arrays to same size eqAB = szA == szB; szA(eqAB) = 1; szB(eqAB) = 1; A = repmat (A, szB); B = repmat (B, szA); endif out = sign_strings (A.strs, B.strs); TF = false (size (out)); TF(out < 0) = true; TF(A.isMissing | B.isMissing) = false; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} ne (@var{A}, @var{B}) ## ## Test for inequality. ## ## @var{TF} is @qcode{true}, if string @var{A} is lexicographically not ## equal to @var{B}. If one input is a string array, the other input can be ## a string array, a character vector, or a cell array of character vectors. ## @code{@var{TF} = ne (@var{A}, @var{B})} is equivalent to ## @code{@var{TF} = ! strcmp (@var{A}, @var{B})}. ## ## @end deftypefn function TF = ne (A, B) ## Overload methods for certain data types if (any (isa (B, {'categorical', 'duration'}))) TF = ne (cellstr (A), B); return; endif if (iscellstr (A) || ischar (A)) A = string (A); elseif (iscellstr (B) || ischar (B)) B = string (B); elseif (! isa (A, 'string') || ! isa (B, 'string')) error (strcat ("string.ne: comparison between '%s' and '%s'", ... " is not supported."), class (A), class (B)); endif szA = size (A); szB = size (B); if (isscalar (A)) A = repmat (A, szB); elseif (isscalar (B)) B = repmat (B, szA); elseif (! isequal (szA, szB)) ## Force size dim vectors to equal length ndA = numel (szA); ndB = numel (szB); if (ndA > ndB) szB = [szB, ones(1, ndA - ndB)]; elseif (ndB > ndA) szA = [szA, ones(1, ndB - ndA)]; endif ## Check for compatible dimensions A_1 = szA != 1 & szA != szB; B_1 = szB != 1 & szA != szB; A_B = A_1 & B_1; if (any (A_B)) error ("string.ne: incompatible dimensions."); endif ## Expand arrays to same size eqAB = szA == szB; szA(eqAB) = 1; szB(eqAB) = 1; A = repmat (A, szB); B = repmat (B, szA); endif TF = ! strcmp (A.strs, B.strs); TF(A.isMissing | B.isMissing) = true; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} strcmp (@var{A}, @var{B}) ## ## Compare strings. ## ## @var{TF} is @qcode{true}, if string @var{A} is lexicographically equal to ## @var{B}. If one input is a string array, the other input can be a string ## array, a character vector, or a cell array of character vectors. ## ## If either @var{A} or @var{B} is a string array or a cell array of ## character vectors, then a logical array @var{TF} of the same size is ## returned, containing the values described above for every member of the ## array. In this case, the other argument may also be a string array or a ## cell array of character vectors (of the same size or scalar), or a ## character vector. ## ## @end deftypefn function TF = strcmp (A, B) if (iscellstr (A) || ischar (A)) A = string (A); elseif (iscellstr (B) || ischar (B)) B = string (B); elseif (! isa (A, 'string') || ! isa (B, 'string')) error (strcat ("string.strcmp: comparison between '%s' and '%s'", ... " is not supported."), class (A), class (B)); endif if (isscalar (A)) A = repmat (A, size (B)); elseif (isscalar (B)) B = repmat (B, size (A)); elseif (! isequal (size (A), size (B))) error ("string.strcmp: inconsistent dimensions."); endif TF = strcmp (A.strs, B.strs); TF(A.isMissing | B.isMissing) = false; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} strcmpi (@var{A}, @var{B}) ## ## Compare strings (case insensitive). ## ## @var{TF} is @qcode{true}, if string @var{A} is lexicographically equal to ## @var{B}, disregarding case of alphabetic characters. If one input is a ## string array, the other input can be a string array, a character vector, ## or a cell array of character vectors. ## ## If either @var{A} or @var{B} is a string array or a cell array of ## character vectors, then a logical array @var{TF} of the same size is ## returned, containing the values described above for every member of the ## array. In this case, the other argument may also be a string array or a ## cell array of character vectors (of the same size or scalar), or a ## character vector. ## ## @end deftypefn function TF = strcmpi (A, B) if (iscellstr (A) || ischar (A)) A = string (A); elseif (iscellstr (B) || ischar (B)) B = string (B); elseif (! isa (A, 'string') || ! isa (B, 'string')) error (strcat ("string.strcmpi: comparison between '%s' and '%s'", ... " is not supported."), class (A), class (B)); endif if (isscalar (A)) A = repmat (A, size (B)); elseif (isscalar (B)) B = repmat (B, size (A)); elseif (! isequal (size (A), size (B))) error ("string.strcmpi: inconsistent dimensions."); endif TF = strcmpi (A.strs, B.strs); TF(A.isMissing | B.isMissing) = false; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} strncmp (@var{A}, @var{B}, @var{n}) ## ## Compare first @var{n} characters of strings. ## ## @var{TF} is @qcode{true}, if the first @var{n} characters of strings ## @var{A} and @var{B} are lexicographically equal. If one input is a ## string array, the other input can be a string array, a character vector, ## or a cell array of character vectors. ## ## If either @var{A} or @var{B} is a string array or a cell array of ## character vectors, then a logical array @var{TF} of the same size is ## returned, containing the values described above for every member of the ## array. In this case, the other argument may also be a string array or a ## cell array of character vectors (of the same size or scalar), or a ## character vector. ## ## @end deftypefn function TF = strncmp (A, B, n) if (iscellstr (A) || ischar (A)) A = string (A); elseif (iscellstr (B) || ischar (B)) B = string (B); elseif (! isa (A, 'string') || ! isa (B, 'string')) error (strcat ("string.strncmp: comparison between '%s' and '%s'", ... " is not supported."), class (A), class (B)); endif if (isscalar (A)) A = repmat (A, size (B)); elseif (isscalar (B)) B = repmat (B, size (A)); elseif (! isequal (size (A), size (B))) error ("string.strncmp: inconsistent dimensions."); endif TF = strncmp (A.strs, B.strs, n); TF(A.isMissing | B.isMissing) = false; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{TF} =} strncmpi (@var{A}, @var{B}, @var{n}) ## ## Compare first @var{n} characters of strings (case insensitive). ## ## @var{TF} is @qcode{true}, if the first @var{n} characters of strings ## @var{A} and @var{B} are lexicographically equal, disregarding case of ## alphabetic characters. If one input is a string array, the other input ## can be a string array, a character vector, or a cell array of character ## vectors. ## ## If either @var{A} or @var{B} is a string array or a cell array of ## character vectors, then a logical array @var{TF} of the same size is ## returned, containing the values described above for every member of the ## array. In this case, the other argument may also be a string array or a ## cell array of character vectors (of the same size or scalar), or a ## character vector. ## ## @end deftypefn function TF = strncmpi (A, B, n) if (iscellstr (A) || ischar (A)) A = string (A); elseif (iscellstr (B) || ischar (B)) B = string (B); elseif (! isa (A, 'string') || ! isa (B, 'string')) error (strcat ("string.strncmpi: comparison between '%s' and '%s'", ... " is not supported."), class (A), class (B)); endif if (isscalar (A)) A = repmat (A, size (B)); elseif (isscalar (B)) B = repmat (B, size (A)); elseif (! isequal (size (A), size (B))) error ("string.strncmpi: inconsistent dimensions."); endif TF = strncmpi (A.strs, B.strs, n); TF(A.isMissing | B.isMissing) = false; endfunction endmethods ################################################################################ ## ** String Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'append' 'compose' 'erase' 'eraseBetween' ## ## 'extract' 'extractAfter' 'extractBefore' 'extractBetween' ## ## 'insertAfter' 'insertBefore' 'replace' 'replaceBetween' ## ## 'reverse' 'sort' 'sortrows' 'split' ## ## 'splitlines' 'strcat' 'strip' 'pad' ## ## 'join' 'plus' 'lower' 'upper' ## ## 'unique' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} append (@var{str1}, @dots{}, @var{strN}) ## ## Combine string arrays. ## ## @code{@var{newstr} = append (@var{str1}, @dots{}, @var{strN})} combines ## the text from each input argument, @var{str1}, @dots{}, @var{strN}), ## which must be either string arrays, cell arrays of character vectors, or ## character vectors or matrices. All input arguments must be of compatible ## sizes. Character vectors are treated as a single text element and ## character matrices are treated as a column of elements. @code{append} ## preserves any trailing white spaces, unlike the @code{strcat} function. ## ## @end deftypefn function out = append (varargin) ## Check input for valid types fcn = @(x) iscellstr (x) || ischar (x) || isa (x, 'string'); dtypes = cellfun (fcn, varargin); if (! all (dtypes)) error (strcat ("string.append: input arguments must be string", ... " arrays, cell arrays of character vectors, or", ... " character matrices.")); endif ## Convert all inputs to string arrays ctypes = cellfun (@(x) ! isa (x, 'string'), varargin); if (any (ctypes)) varargin(ctypes) = cellfun (@(x) string (char (x)), ... varargin(ctypes), "UniformOutput", false); endif ## A single argument is returned unchanged out = varargin{1}; if (numel (varargin) == 1) return; endif ## Handle compatible dimensions. An element is missing whenever any input ## contributes a missing value at that position, since a missing string ## propagates like NaN through concatenation. in_sz = cellfun (@size, varargin, "UniformOutput", false); strArgs = cellfun (@(x) x.strs, varargin, 'UniformOutput', false); ismArgs = cellfun (@(x) x.isMissing, varargin, 'UniformOutput', false); if (isequal (in_sz{:})) out.strs = strcat (strArgs{:}); out.isMissing = or (ismArgs{:}); else try out.isMissing = or (ismArgs{:}); szo = size (out.isMissing); catch error ("string.append: inputs have incompatible sizes."); end_try_catch for n = 1:numel (strArgs) strArg = strArgs{n}; newsz = szo ./ size (strArg); strArgs{n} = repmat (strArg, newsz); endfor out.strs = strcat (strArgs{:}); endif ## Missing elements carry no text, as elsewhere in the class out.strs(out.isMissing) = {''}; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{str} =} compose (@var{formatSpec}, @var{A}) ## @deftypefnx {string} {@var{str} =} compose (@var{formatSpec}, @var{A1}, @dots{}, @var{AN}) ## @deftypefnx {string} {@var{str} =} compose (@var{txt}) ## ## Format data into a string array or translate escape-character sequences. ## ## @code{@var{str} = compose (@var{formatSpec}, @var{A})} formats the data ## in the array @var{A} according to the formatting operators in ## @var{formatSpec}, which must be a string scalar or character vector, and ## returns the result in the string array @var{str}. The formatting ## operators are the same as those accepted by the @code{sprintf} function. ## Unlike @code{sprintf}, which returns a single character vector, ## @code{compose} returns a string array whose elements correspond to the ## rows of @var{A}. ## ## @code{compose} applies @var{formatSpec} to each row of @var{A} so that ## @var{str} has the same number of rows as @var{A}. The size of @var{str} ## is further determined as follows: ## ## @itemize ## @item If the number of columns in @var{A} exceeds the number of ## formatting operators in @var{formatSpec}, then @var{formatSpec} is ## applied repeatedly along each row of @var{A}, adding columns to ## @var{str}. ## @item If the number of columns in @var{A} is less than the number of ## formatting operators, then the operators left without a corresponding ## value appear unchanged in @var{str}. ## @item If @var{A} has zero columns, then @var{str} has the same size as ## @var{A} and no formatting operators are applied. ## @end itemize ## ## @code{@var{str} = compose (@var{formatSpec}, @var{A1}, @dots{}, ## @var{AN})} formats the data from the arrays @var{A1}, @dots{}, @var{AN}. ## The formatting operators are assigned to the input arrays in order: once ## an operator has consumed a value from an input array, it becomes ## unavailable to the following arrays. All input arrays must be of ## compatible sizes. ## ## @code{@var{str} = compose (@var{txt})} translates escape-character ## sequences, such as @qcode{'\n'} and @qcode{'\t'}, in @var{txt} and ## returns the result in @var{str}, which has the same size as @var{txt}. ## Any formatting operators in @var{txt} are left unchanged. ## ## In all syntaxes, escape-character sequences appearing in literal text are ## translated and each @qcode{'%%'} literal is converted to a single ## @qcode{'%'} character, following the same rules as the @code{sprintf} ## function. The only difference from @code{sprintf} is that a formatting ## operator left without a corresponding value is emitted unchanged rather ## than dropped. ## ## @end deftypefn function out = compose (this, varargin) ## Escape-sequence translation syntax: compose (TXT) if (nargin == 1) out = this; cstr = this.strs; for k = 1:numel (cstr) if (this.isMissing(k)) continue; endif cstr{k} = compose_apply (compose_tokenize (cstr{k}), {}); endfor out.strs = cstr; return; endif ## Formatting syntax: compose (FORMATSPEC, A1, ..., AN) if (! isscalar (this)) error (strcat ("string.compose: FORMATSPEC must be a", ... " string scalar or a character vector.")); endif if (this.isMissing) error ("string.compose: FORMATSPEC cannot be a missing value."); endif tok = compose_tokenize (this.strs{1}); ## Count the values consumed by one full pass of FORMATSPEC vpa = 0; for t = 1:numel (tok) if (strcmp (tok{t}.type, 'op')) vpa += tok{t}.nval; endif endfor ## Normalize each input array to a 2-D cell matrix of values N = numel (varargin); C = cell (1, N); nrows = ones (1, N); for n = 1:N A = varargin{n}; if (ischar (A)) Cn = cellstr (A); elseif (isa (A, 'string')) Cn = cellstr (A); elseif (isnumeric (A) || islogical (A)) Cn = num2cell (A); else error ("string.compose: unsupported input type: '%s'", class (A)); endif Cn = reshape (Cn, size (Cn, 1), []); C{n} = Cn; nrows(n) = size (Cn, 1); endfor ## Determine the common number of rows (singletons expand) R = max (nrows); for n = 1:N rn = size (C{n}, 1); if (rn == R) continue; elseif (rn == 1) C{n} = repmat (C{n}, R, 1); else error ("string.compose: input arrays must be of compatible sizes."); endif endfor ## Combine all values per row, in column order across input arrays vals = [C{:}]; V = columns (vals); ## With zero data columns no formatting is applied (size preserved) if (V == 0) out = string (cell (R, 0)); return; endif ## Number of times FORMATSPEC is applied per row if (vpa == 0) nApp = 1; else nApp = max (1, ceil (V / vpa)); endif ## Apply FORMATSPEC, consuming VPA values at a time outc = cell (R, nApp); for r = 1:R for a = 1:nApp lo = (a - 1) * vpa + 1; if (vpa == 0 || lo > V) slice = {}; else slice = vals(r, lo:min (a * vpa, V)); endif outc{r,a} = compose_apply (tok, slice); endfor endfor out = string (outc); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} erase (@var{str}, @var{match}) ## ## Remove content from string array. ## ## @code{@var{newstr} = erase (@var{str}, @var{match})} removes the ## occurrences of @var{match} from each element of the string array ## @var{str}. @var{match} can be a string array, a character vector, or a ## cell array of character vectors. When @var{match} contains more than one ## piece of text, every occurrence of every element of @var{match} is ## removed. @var{newstr} is a string array of the same size as @var{str}; ## the size of @var{match} need not match the size of @var{str}. Missing ## values in @var{str} are preserved. ## ## @end deftypefn function out = erase (this, match) if (isa (match, 'string')) pats = cellstr (match); elseif (ischar (match) || iscellstr (match)) ## Route char/cellstr through the constructor, which keeps trailing ## whitespace that bare 'cellstr' would deblank pats = cellstr (string (match)); else error (strcat ("string.erase: MATCH must be a string array, a", ... " character vector, or a cell array of character", ... " vectors.")); endif out = this; for i = 1:numel (pats) out.strs = strrep (out.strs, pats{i}, ''); endfor endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} eraseBetween (@var{str}, @var{startPat}, @var{endPat}) ## @deftypefnx {string} {@var{newstr} =} eraseBetween (@var{str}, @var{startPos}, @var{endPos}) ## @deftypefnx {string} {@var{newstr} =} eraseBetween (@dots{}, @qcode{"Boundaries"}, @var{bounds}) ## ## Erase content between start and end boundaries. ## ## @code{@var{newstr} = eraseBetween (@var{str}, @var{startPat}, ## @var{endPat})} removes from each element of the string array @var{str} ## the text that occurs between the substrings @var{startPat} and ## @var{endPat}, keeping the boundary substrings themselves. @var{startPat} ## and @var{endPat} can be string arrays, character vectors, or cell arrays ## of character vectors. For each element, the first occurrence of ## @var{startPat} is matched and then the first occurrence of @var{endPat} ## that begins after it; if either boundary is not found, the element is ## returned unchanged. ## ## @code{@var{newstr} = eraseBetween (@var{str}, @var{startPos}, ## @var{endPos})} removes the text between the character positions ## @var{startPos} and @var{endPos}, inclusive of the characters at those ## positions. @var{startPos} and @var{endPos} must be positive integers ## with @var{startPos} not exceeding @var{endPos} and both within the ## length of the corresponding element of @var{str}. ## ## @code{@var{newstr} = eraseBetween (@dots{}, @qcode{"Boundaries"}, ## @var{bounds})} specifies whether the boundaries are included in or ## excluded from the erased text. @var{bounds} can be either ## @qcode{"inclusive"} or @qcode{"exclusive"}. When boundaries are given ## as substrings, the default is @qcode{"exclusive"} and the boundary ## substrings are preserved; when given as positions, the default is ## @qcode{"inclusive"} and the characters at those positions are erased. ## ## @var{startPat}/@var{endPat} and @var{startPos}/@var{endPos} must either ## be scalars, applied to every element of @var{str}, or be of the same size ## as @var{str} and applied element-wise. @var{newstr} is a string array of ## the same size as @var{str}. Missing values in @var{str} are preserved. ## ## @end deftypefn function out = eraseBetween (this, start, stop, varargin) if (nargin < 3) error ("string.eraseBetween: not enough input arguments."); endif ## Position boundaries are numeric, pattern boundaries are text; the two ## modes cannot be mixed and have different default 'Boundaries' values. istxt = @(x) ischar (x) || iscellstr (x) || isa (x, 'string'); if (isnumeric (start) && isnumeric (stop)) posMode = true; dfBounds = 'inclusive'; elseif (istxt (start) && istxt (stop)) posMode = false; dfBounds = 'exclusive'; else error (strcat ("string.eraseBetween: START and STOP must be either", ... " both numeric positions or both text patterns.")); endif if (mod (numel (varargin), 2) != 0) error ("string.eraseBetween: name-value arguments must be in pairs."); endif ## Parse optional paired arguments [bounds, args] = parsePairedArguments ({'Boundaries'}, {dfBounds}, ... varargin(:)); if (! isempty (args)) error ("string.eraseBetween: invalid optional paired argument."); endif if (isa (bounds, 'string')) bounds = char (bounds); endif if (! ischar (bounds) || ... ! any (strcmpi (bounds, {'inclusive', 'exclusive'}))) error (strcat ("string.eraseBetween: BOUNDARIES must be", ... " 'inclusive' or 'exclusive'.")); endif inclusive = strcmpi (bounds, 'inclusive'); ## Normalize text boundaries to cell arrays of character vectors. Route ## char/cellstr through the constructor, which keeps trailing whitespace ## that bare 'cellstr' would deblank. if (! posMode) if (isa (start, 'string')) start = cellstr (start); else start = cellstr (string (start)); endif if (isa (stop, 'string')) stop = cellstr (stop); else stop = cellstr (string (stop)); endif endif ## Broadcast scalar boundaries; otherwise sizes must match STR sz = size (this.strs); [start, errmsg] = eb_expand (start, sz, 'START'); if (! isempty (errmsg)) error ("string.eraseBetween: %s", errmsg); endif [stop, errmsg] = eb_expand (stop, sz, 'STOP'); if (! isempty (errmsg)) error ("string.eraseBetween: %s", errmsg); endif out = this; cstr = this.strs; for k = 1:numel (cstr) if (this.isMissing(k)) continue; endif if (posMode) [cstr{k}, errmsg] = eb_between (cstr{k}, start(k), stop(k), true, ... inclusive); else [cstr{k}, errmsg] = eb_between (cstr{k}, start{k}, stop{k}, false, ... inclusive); endif if (! isempty (errmsg)) error ("string.eraseBetween: %s", errmsg); endif endfor out.strs = cstr; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} extract (@var{str}, @var{pat}) ## @deftypefnx {string} {@var{newstr} =} extract (@var{str}, @var{pos}) ## ## Extract substrings from a string array. ## ## @code{@var{newstr} = extract (@var{str}, @var{pat})} returns the ## substrings of @var{str} that match @var{pat}. @var{pat} can be a string ## array, a character vector, or a cell array of character vectors; when it ## contains more than one piece of text, any of them may match. Within each ## element of @var{str} the matches are found left to right and do not ## overlap; where alternatives match at the same position, the first listed ## in @var{pat} is taken. ## ## The matches of an element occupy a row of @var{newstr}, so the matches ## run along the second dimension and every element of @var{str} must yield ## the same number of matches. For a string scalar with @var{n} matches, ## @var{newstr} is @code{1x@var{n}}; for a non-scalar @var{str}, ## @var{newstr} has one row per element (taken in column-major order) and ## one column per match. Elements with no match, including missing values, ## are treated as having zero matches. ## ## @code{@var{newstr} = extract (@var{str}, @var{pos})} returns the single ## character located at position @var{pos} in each element of @var{str}. ## @var{pos} must be a positive integer that is either a scalar, applied to ## every element, or the same size as @var{str}, applied element-wise. In ## this syntax @var{newstr} has the same size as @var{str} and missing ## values are preserved. ## ## @end deftypefn function out = extract (this, pat) if (nargin < 2) error ("string.extract: not enough input arguments."); endif ## Numeric POS selects a single character per element; text PAT extracts ## substring matches. if (isnumeric (pat)) pos = pat; sz = size (this.strs); if (isscalar (pos)) pos = repmat (pos, sz); elseif (! isequal (size (pos), sz)) error ("string.extract: POS must be scalar or the same size as STR."); endif if (any (pos(:) != fix (pos(:))) || any (pos(:) < 1)) error ("string.extract: POS must be a positive integer."); endif out = this; cstr = this.strs; for k = 1:numel (cstr) if (this.isMissing(k)) continue; endif cp = str2cp (cstr{k}); if (pos(k) > numel (cp)) error ("string.extract: POS exceeds the length of the string."); endif cstr{k} = cp2str (cp(pos(k))); endfor out.strs = cstr; return; endif ## Text pattern form if (isa (pat, 'string')) pats = cellstr (pat); elseif (ischar (pat) || iscellstr (pat)) ## Route char/cellstr through the constructor, which keeps trailing ## whitespace that bare 'cellstr' would deblank pats = cellstr (string (pat)); else error (strcat ("string.extract: PAT must be a string array, a", ... " character vector, or a cell array of character", ... " vectors.")); endif ## Collect the matches of each element (missing elements yield none) ne = numel (this.strs); matches = cell (ne, 1); counts = zeros (ne, 1); for k = 1:ne if (this.isMissing(k)) matches{k} = {}; else matches{k} = extract_matches (this.strs{k}, pats); endif counts(k) = numel (matches{k}); endfor ## Matches run along the columns, so every element must match the same ## number of times to form a rectangular string array. if (ne > 0 && any (counts != counts(1))) error (strcat ("string.extract: all elements of STR must have the", ... " same number of matches.")); endif ncol = 0; if (ne > 0) ncol = counts(1); endif outc = cell (ne, ncol); for e = 1:ne outc(e,:) = matches{e}; endfor out = string (outc); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} extractAfter (@var{str}, @var{pat}) ## @deftypefnx {string} {@var{newstr} =} extractAfter (@var{str}, @var{pos}) ## ## Extract the substring after a position or pattern. ## ## @code{@var{newstr} = extractAfter (@var{str}, @var{pat})} returns, for ## each element of @var{str}, the part of the text that follows the first ## occurrence of @var{pat}, excluding @var{pat} itself. @var{pat} can be a ## string array, a character vector, or a cell array of character vectors, ## and must either be a scalar, applied to every element of @var{str}, or ## be of the same size as @var{str} and applied element-wise. If @var{pat} ## is not found in an element, the corresponding element of @var{newstr} is ## a missing value. ## ## @code{@var{newstr} = extractAfter (@var{str}, @var{pos})} returns the ## part of each element of @var{str} that follows the character position ## @var{pos}, that is, from @code{@var{pos}+1} to the end. @var{pos} must ## be a positive integer that is either a scalar or the same size as ## @var{str}. ## ## @var{newstr} is a string array of the same size as @var{str}. Missing ## values in @var{str} are preserved. ## ## @end deftypefn function out = extractAfter (this, pat) if (nargin < 2) error ("string.extractAfter: not enough input arguments."); endif [out, errmsg] = extract_side (this, pat, true); if (! isempty (errmsg)) error ("string.extractAfter: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} extractBefore (@var{str}, @var{pat}) ## @deftypefnx {string} {@var{newstr} =} extractBefore (@var{str}, @var{pos}) ## ## Extract the substring before a position or pattern. ## ## @code{@var{newstr} = extractBefore (@var{str}, @var{pat})} returns, for ## each element of @var{str}, the part of the text that precedes the first ## occurrence of @var{pat}, excluding @var{pat} itself. @var{pat} can be a ## string array, a character vector, or a cell array of character vectors, ## and must either be a scalar, applied to every element of @var{str}, or ## be of the same size as @var{str} and applied element-wise. If @var{pat} ## is not found in an element, the corresponding element of @var{newstr} is ## a missing value. ## ## @code{@var{newstr} = extractBefore (@var{str}, @var{pos})} returns the ## part of each element of @var{str} that precedes the character position ## @var{pos}, that is, from the start up to @code{@var{pos}-1}. @var{pos} ## must be a positive integer that is either a scalar or the same size as ## @var{str}. ## ## @var{newstr} is a string array of the same size as @var{str}. Missing ## values in @var{str} are preserved. ## ## @end deftypefn function out = extractBefore (this, pat) if (nargin < 2) error ("string.extractBefore: not enough input arguments."); endif [out, errmsg] = extract_side (this, pat, false); if (! isempty (errmsg)) error ("string.extractBefore: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} extractBetween (@var{str}, @var{startPat}, @var{endPat}) ## @deftypefnx {string} {@var{newstr} =} extractBetween (@var{str}, @var{startPos}, @var{endPos}) ## @deftypefnx {string} {@var{newstr} =} extractBetween (@dots{}, @qcode{"Boundaries"}, @var{bounds}) ## ## Extract the substrings between start and end boundaries. ## ## @code{@var{newstr} = extractBetween (@var{str}, @var{startPat}, ## @var{endPat})} returns the text that occurs between the substrings ## @var{startPat} and @var{endPat} in each element of @var{str}. ## @var{startPat} and @var{endPat} can be string arrays, character vectors, ## or cell arrays of character vectors. Within each element the boundary ## pairs are matched from left to right and do not overlap, so an element ## may yield several substrings; these run along the second dimension, and ## every element of @var{str} must yield the same number of matches. For a ## string scalar with @var{n} matches, @var{newstr} is @code{1x@var{n}}; ## for a non-scalar @var{str}, @var{newstr} has one row per element (taken ## in column-major order) and one column per match. Elements with no match, ## including missing values, are treated as having zero matches. ## ## @code{@var{newstr} = extractBetween (@var{str}, @var{startPos}, ## @var{endPos})} returns the substring between the character positions ## @var{startPos} and @var{endPos}, inclusive of the characters at those ## positions. This syntax extracts a single substring per element, so ## @var{newstr} has the same size as @var{str}. ## ## @code{@var{newstr} = extractBetween (@dots{}, @qcode{"Boundaries"}, ## @var{bounds})} specifies whether the boundaries are included in or ## excluded from the extracted text. @var{bounds} can be either ## @qcode{"inclusive"} or @qcode{"exclusive"}. When boundaries are given ## as substrings, the default is @qcode{"exclusive"} and the boundary ## substrings are not included; when given as positions, the default is ## @qcode{"inclusive"} and the characters at those positions are included. ## ## @var{startPat}/@var{endPat} and @var{startPos}/@var{endPos} must either ## be scalars, applied to every element of @var{str}, or be of the same size ## as @var{str}. Missing values in @var{str} are preserved. ## ## @end deftypefn function out = extractBetween (this, start, stop, varargin) if (nargin < 3) error ("string.extractBetween: not enough input arguments."); endif istxt = @(x) ischar (x) || iscellstr (x) || isa (x, 'string'); if (isnumeric (start) && isnumeric (stop)) posMode = true; dfBounds = 'inclusive'; elseif (istxt (start) && istxt (stop)) posMode = false; dfBounds = 'exclusive'; else error (strcat ("string.extractBetween: START and STOP must be", ... " either both numeric positions or both text", ... " patterns.")); endif if (mod (numel (varargin), 2) != 0) error ("string.extractBetween: name-value arguments must be in pairs."); endif ## Parse optional paired arguments [bounds, args] = parsePairedArguments ({'Boundaries'}, {dfBounds}, ... varargin(:)); if (! isempty (args)) error ("string.extractBetween: invalid optional paired argument."); endif if (isa (bounds, 'string')) bounds = char (bounds); endif if (! ischar (bounds) || ... ! any (strcmpi (bounds, {'inclusive', 'exclusive'}))) error (strcat ("string.extractBetween: BOUNDARIES must be", ... " 'inclusive' or 'exclusive'.")); endif inclusive = strcmpi (bounds, 'inclusive'); sz = size (this.strs); ne = numel (this.strs); if (posMode) [start, errmsg] = eb_expand (start, sz, 'START'); if (! isempty (errmsg)) error ("string.extractBetween: %s", errmsg); endif [stop, errmsg] = eb_expand (stop, sz, 'STOP'); if (! isempty (errmsg)) error ("string.extractBetween: %s", errmsg); endif ## One span per element, so the output matches the size of STR out = this; cstr = this.strs; for k = 1:ne if (this.isMissing(k)) continue; endif [cstr{k}, errmsg] = eb_span (cstr{k}, start(k), stop(k), inclusive); if (! isempty (errmsg)) error ("string.extractBetween: %s", errmsg); endif endfor out.strs = cstr; return; endif ## Pattern mode: each element may contribute several substrings. Route ## char/cellstr through the constructor, which keeps trailing whitespace ## that bare 'cellstr' would deblank. if (isa (start, 'string')) start = cellstr (start); else start = cellstr (string (start)); endif if (isa (stop, 'string')) stop = cellstr (stop); else stop = cellstr (string (stop)); endif [start, errmsg] = eb_expand (start, sz, 'START'); if (! isempty (errmsg)) error ("string.extractBetween: %s", errmsg); endif [stop, errmsg] = eb_expand (stop, sz, 'STOP'); if (! isempty (errmsg)) error ("string.extractBetween: %s", errmsg); endif matches = cell (ne, 1); counts = zeros (ne, 1); for k = 1:ne if (this.isMissing(k)) matches{k} = {}; else matches{k} = extract_between_matches (this.strs{k}, start{k}, ... stop{k}, inclusive); endif counts(k) = numel (matches{k}); endfor if (ne > 0 && any (counts != counts(1))) error (strcat ("string.extractBetween: all elements of STR must", ... " have the same number of matches.")); endif ncol = 0; if (ne > 0) ncol = counts(1); endif outc = cell (ne, ncol); for e = 1:ne outc(e,:) = matches{e}; endfor out = string (outc); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} insertAfter (@var{str}, @var{pat}, @var{newtext}) ## @deftypefnx {string} {@var{newstr} =} insertAfter (@var{str}, @var{pos}, @var{newtext}) ## ## Insert text after a pattern or position. ## ## @code{@var{newstr} = insertAfter (@var{str}, @var{pat}, @var{newtext})} ## inserts the text @var{newtext} into each element of @var{str} after ## every non-overlapping occurrence of the substring @var{pat}. @var{pat} ## can be a string array, a character vector, or a cell array of character ## vectors, and must either be a scalar, applied to every element of ## @var{str}, or be of the same size as @var{str} and applied ## element-wise. If @var{pat} is not found in an element, that element is ## returned unchanged. ## ## @code{@var{newstr} = insertAfter (@var{str}, @var{pos}, @var{newtext})} ## inserts @var{newtext} after the character position @var{pos}, that is, ## between the characters at positions @var{pos} and @code{@var{pos}+1}. ## @var{pos} must be a positive integer not exceeding the length of the ## corresponding element of @var{str}, and must be either a scalar or the ## same size as @var{str}. ## ## @var{newtext} can be a string array, a character vector, or a cell array ## of character vectors, and must be either a scalar, inserted at every ## position, or of the same size as @var{str}. @var{newstr} is a string ## array of the same size as @var{str}. Missing values in @var{str} are ## preserved, and a missing value in @var{newtext} makes the corresponding ## element of @var{newstr} missing. ## ## @end deftypefn function out = insertAfter (this, pat, new) if (nargin < 3) error ("string.insertAfter: not enough input arguments."); endif [out, errmsg] = insert_side (this, pat, new, true); if (! isempty (errmsg)) error ("string.insertAfter: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} insertBefore (@var{str}, @var{pat}, @var{newtext}) ## @deftypefnx {string} {@var{newstr} =} insertBefore (@var{str}, @var{pos}, @var{newtext}) ## ## Insert text before a pattern or position. ## ## @code{@var{newstr} = insertBefore (@var{str}, @var{pat}, @var{newtext})} ## inserts the text @var{newtext} into each element of @var{str} before ## every non-overlapping occurrence of the substring @var{pat}. @var{pat} ## can be a string array, a character vector, or a cell array of character ## vectors, and must either be a scalar, applied to every element of ## @var{str}, or be of the same size as @var{str} and applied ## element-wise. If @var{pat} is not found in an element, that element is ## returned unchanged. ## ## @code{@var{newstr} = insertBefore (@var{str}, @var{pos}, @var{newtext})} ## inserts @var{newtext} before the character position @var{pos}, that is, ## between the characters at positions @code{@var{pos}-1} and @var{pos}. ## @var{pos} must be a positive integer not exceeding the length of the ## corresponding element of @var{str}, and must be either a scalar or the ## same size as @var{str}. ## ## @var{newtext} can be a string array, a character vector, or a cell array ## of character vectors, and must be either a scalar, inserted at every ## position, or of the same size as @var{str}. @var{newstr} is a string ## array of the same size as @var{str}. Missing values in @var{str} are ## preserved, and a missing value in @var{newtext} makes the corresponding ## element of @var{newstr} missing. ## ## @end deftypefn function out = insertBefore (this, pat, new) if (nargin < 3) error ("string.insertBefore: not enough input arguments."); endif [out, errmsg] = insert_side (this, pat, new, false); if (! isempty (errmsg)) error ("string.insertBefore: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} replace (@var{str}, @var{old}, @var{new}) ## ## Replace substrings in string array. ## ## @code{@var{newstr} = replace (@var{str}, @var{old}, @var{new})} replaces ## every occurrence of the substring @var{old} with @var{new} in each ## element of the string array @var{str}. @var{old} and @var{new} can be ## string arrays, character vectors, or cell arrays of character vectors. ## ## When @var{old} contains several substrings, @var{new} must either be a ## single substring, used to replace all of them, or be of the same size as ## @var{old}, replacing each substring of @var{old} with the corresponding ## element of @var{new}. All substrings are replaced in a single left to ## right pass over each element: at each position the substrings of ## @var{old} are tried in order, the first that matches is replaced, and ## the scan resumes past the inserted text, so replacements are not ## themselves re-scanned. @var{old} and @var{new} are applied to every ## element of @var{str}; their size need not match the size of @var{str}. ## ## @var{newstr} is a string array of the same size as @var{str}. A ## substring of @var{old} that does not occur leaves the text unchanged, and ## missing values in @var{str} are preserved. ## ## @end deftypefn function out = replace (this, old, new) if (nargin < 3) error ("string.replace: not enough input arguments."); endif ## Normalize OLD and NEW to cell arrays of character vectors. Route ## char/cellstr through the constructor, which keeps trailing whitespace ## that bare 'cellstr' would deblank. if (isa (old, 'string')) olds = cellstr (old); elseif (ischar (old) || iscellstr (old)) olds = cellstr (string (old)); else error (strcat ("string.replace: OLD must be a string array, a", ... " character vector, or a cell array of character", ... " vectors.")); endif if (isa (new, 'string')) news = cellstr (new); elseif (ischar (new) || iscellstr (new)) news = cellstr (string (new)); else error (strcat ("string.replace: NEW must be a string array, a", ... " character vector, or a cell array of character", ... " vectors.")); endif ## NEW must be a single substring or pair up with the OLD substrings if (numel (news) == 1) news = repmat (news, size (olds)); elseif (numel (news) != numel (olds)) error (strcat ("string.replace: NEW must be a single substring or", ... " the same size as OLD.")); endif out = this; if (numel (olds) == 1) ## Single pattern: a vectorised strrep replaces every occurrence and ## leaves missing elements (stored as '') untouched out.strs = strrep (this.strs, olds{1}, news{1}); else ## Multiple patterns are applied simultaneously, in one pass per element cstr = this.strs; for k = 1:numel (cstr) if (this.isMissing(k)) continue; endif cstr{k} = replace_pairs (cstr{k}, olds, news); endfor out.strs = cstr; endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} replaceBetween (@var{str}, @var{startPat}, @var{endPat}, @var{newtext}) ## @deftypefnx {string} {@var{newstr} =} replaceBetween (@var{str}, @var{startPos}, @var{endPos}, @var{newtext}) ## @deftypefnx {string} {@var{newstr} =} replaceBetween (@dots{}, @qcode{"Boundaries"}, @var{bounds}) ## ## Replace content between start and end boundaries. ## ## @code{@var{newstr} = replaceBetween (@var{str}, @var{startPat}, ## @var{endPat}, @var{newtext})} replaces, in each element of the string ## array @var{str}, the text that occurs between the substrings ## @var{startPat} and @var{endPat} with @var{newtext}, keeping the boundary ## substrings themselves. @var{startPat} and @var{endPat} can be string ## arrays, character vectors, or cell arrays of character vectors. For each ## element, the first occurrence of @var{startPat} is matched and then the ## first occurrence of @var{endPat} that begins after it; if either boundary ## is not found, the element is returned unchanged. ## ## @code{@var{newstr} = replaceBetween (@var{str}, @var{startPos}, ## @var{endPos}, @var{newtext})} replaces the text between the character ## positions @var{startPos} and @var{endPos}, inclusive of the characters at ## those positions, with @var{newtext}. @var{startPos} and @var{endPos} ## must be positive integers with @var{startPos} not exceeding @var{endPos} ## and both within the length of the corresponding element of @var{str}. ## ## @code{@var{newstr} = replaceBetween (@dots{}, @qcode{"Boundaries"}, ## @var{bounds})} specifies whether the boundaries are included in or ## excluded from the replaced text. @var{bounds} can be either ## @qcode{"inclusive"} or @qcode{"exclusive"}. When boundaries are given ## as substrings, the default is @qcode{"exclusive"} and the boundary ## substrings are preserved; when given as positions, the default is ## @qcode{"inclusive"} and the characters at those positions are replaced. ## ## @var{startPat}/@var{endPat} and @var{startPos}/@var{endPos} must either ## be scalars, applied to every element of @var{str}, or be of the same size ## as @var{str} and applied element-wise. @var{newtext} can be a string ## array, a character vector, or a cell array of character vectors, and must ## be either a scalar or the same size as @var{str}. @var{newstr} is a ## string array of the same size as @var{str}. Missing values in @var{str} ## are preserved, and a missing value in @var{newtext} makes the ## corresponding element of @var{newstr} missing. ## ## @end deftypefn function out = replaceBetween (this, start, stop, new, varargin) if (nargin < 4) error ("string.replaceBetween: not enough input arguments."); endif ## Position boundaries are numeric, pattern boundaries are text; the two ## modes cannot be mixed and have different default 'Boundaries' values. istxt = @(x) ischar (x) || iscellstr (x) || isa (x, 'string'); if (isnumeric (start) && isnumeric (stop)) posMode = true; dfBounds = 'inclusive'; elseif (istxt (start) && istxt (stop)) posMode = false; dfBounds = 'exclusive'; else error (strcat ("string.replaceBetween: START and STOP must be", ... " either both numeric positions or both text", ... " patterns.")); endif if (mod (numel (varargin), 2) != 0) error ("string.replaceBetween: name-value arguments must be in pairs."); endif ## Parse optional paired arguments [bounds, args] = parsePairedArguments ({'Boundaries'}, {dfBounds}, ... varargin(:)); if (! isempty (args)) error ("string.replaceBetween: invalid optional paired argument."); endif if (isa (bounds, 'string')) bounds = char (bounds); endif if (! ischar (bounds) || ... ! any (strcmpi (bounds, {'inclusive', 'exclusive'}))) error (strcat ("string.replaceBetween: BOUNDARIES must be", ... " 'inclusive' or 'exclusive'.")); endif inclusive = strcmpi (bounds, 'inclusive'); ## Normalize text boundaries to cell arrays of character vectors. Route ## char/cellstr through the constructor, which keeps trailing whitespace ## that bare 'cellstr' would deblank. if (! posMode) if (isa (start, 'string')) start = cellstr (start); else start = cellstr (string (start)); endif if (isa (stop, 'string')) stop = cellstr (stop); else stop = cellstr (string (stop)); endif endif ## Broadcast scalar boundaries and the replacement; otherwise match STR sz = size (this.strs); [start, errmsg] = eb_expand (start, sz, 'START'); if (! isempty (errmsg)) error ("string.replaceBetween: %s", errmsg); endif [stop, errmsg] = eb_expand (stop, sz, 'STOP'); if (! isempty (errmsg)) error ("string.replaceBetween: %s", errmsg); endif [newc, newMiss, errmsg] = norm_new (new, sz); if (! isempty (errmsg)) error ("string.replaceBetween: %s", errmsg); endif out = this; cstr = this.strs; isMiss = this.isMissing; for k = 1:numel (cstr) if (isMiss(k)) continue; endif if (newMiss(k)) cstr{k} = ''; isMiss(k) = true; continue; endif if (posMode) [cstr{k}, errmsg] = rb_between (cstr{k}, start(k), stop(k), true, ... inclusive, newc{k}); else [cstr{k}, errmsg] = rb_between (cstr{k}, start{k}, stop{k}, false, ... inclusive, newc{k}); endif if (! isempty (errmsg)) error ("string.replaceBetween: %s", errmsg); endif endfor out.strs = cstr; out.isMissing = isMiss; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} reverse (@var{str}) ## ## Reverse order of characters in string array. ## ## @code{@var{newstr} = reverse (@var{str})} reverses the order of the ## characters in every each element of the string array @var{str}. ## @var{newstr} is a string array of the same size as @var{str}. ## ## @end deftypefn function out = reverse (this) enc = utf32 (); fu2n = @(x) typecast (unicode2native (x, enc), 'uint32'); fn2u = @(x) native2unicode (typecast (x, 'uint8'), enc); frev = @(x) x(end:-1:1); notempty = ! cellfun (@isempty, this.strs); out = this; code = cellfun (fu2n, this.strs(notempty), "UniformOutput", false); code = cellfun (frev, code, "UniformOutput", false); out.strs(notempty) = cellfun (fn2u, code, "UniformOutput", false); out.isMissing = this.isMissing; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{B} =} sort (@var{A}) ## @deftypefnx {string} {@var{B} =} sort (@var{A}, @var{dim}) ## @deftypefnx {string} {@var{B} =} sort (@var{A}, @var{direction}) ## @deftypefnx {string} {@var{B} =} sort (@var{A}, @var{dim}, @var{direction}) ## @deftypefnx {string} {@var{B} =} sort (@dots{}, @qcode{'MissingPlacement'}, @var{MP}) ## @deftypefnx {string} {[@var{B}, @var{index}] =} sort (@var{A}, @dots{}) ## ## Sort elements in a string array. ## ## @code{@var{B} = sort (@var{A})} sorts the elements of the string array ## @var{A} in ascending order. Elements are compared lexicographically by ## their Unicode code points, with the empty string @qcode{""} sorting ## before any non-empty string. If @var{A} is a matrix, ## @code{sort (@var{A})} sorts each column of @var{A} in ascending order. ## For multidimensional arrays, @code{sort (@var{A})} sorts along the first ## non-singleton dimension. ## ## @code{@var{B} = sort (@var{A}, @var{dim})} sorts along the dimension ## specified by @var{dim}. ## ## @code{@var{B} = sort (@var{A}, @var{direction})} also specifies the ## sorting direction, which can be either @qcode{'ascend'} (default) or ## @qcode{'descend'}. ## ## @code{@var{B} = sort (@dots{}, @qcode{'MissingPlacement'}, @var{MP})} ## specifies where to place the missing elements (@qcode{}) ## returned in @var{B} with any of the following options specified in ## @var{MP}: ## ## @itemize ## @item @qcode{'auto'}, which is the default, places missing elements last ## for ascending sort and first for descending sort. ## @item @qcode{'first'} places missing elements first. ## @item @qcode{'last'} places missing elements last. ## @end itemize ## ## @code{[@var{B}, @var{index}] = sort (@var{A}, @dots{})} also returns a ## sorting index containing the original indices of the elements in the ## sorted array. ## ## @itemize ## @item If @var{A} is a vector, then @var{index} contains the original ## linear indices of the elements in the sorted vector @var{B} such that ## @code{@var{B} = @var{A}(@var{index})}. ## @item If @var{A} is an @math{M*N} matrix and @qcode{@var{dim} = 1}, then ## @var{index} contains the original row indices of the elements in the ## sorted vector @var{B} such that for @qcode{j = 1:N}, ## @code{@var{B}(:,j) = @var{A}(@var{index}(:,j),j)}. ## @end itemize ## ## @end deftypefn function [B, index] = sort (A, varargin) ## Parse and validate optional 'MissingPlacement' paired argument optNames = {'MissingPlacement'}; dfValues = {'auto'}; [MP, args] = parsePairedArguments (optNames, dfValues, varargin(:)); ## The values are matched without regard to case, as MATLAB does. MP = lower (MP); if (! ismember (MP, {'auto', 'first', 'last'})) error ("string.sort: invalid value for 'MissingPlacement'."); endif ## Force strings to character vectors [args{:}] = convertStringsToChars (args{:}); ## Only a dimension and a direction may remain, once each for k = 1:numel (args) arg = args{k}; if (isnumeric (arg)) if (! (isscalar (arg) && arg >= 1 && arg == fix (arg))) error ("string.sort: DIM must be a positive integer."); endif elseif (ischar (arg) && isrow (arg)) if (! any (strcmpi (arg, {'ascend', 'descend'}))) error ("string.sort: invalid DIRECTION '%s'.", arg); endif else error ("string.sort: invalid input argument."); endif endfor if (sum (cellfun (@isnumeric, args)) > 1 || sum (cellfun (@ischar, args)) > 1) error ("string.sort: invalid input argument."); endif if (numel (args) == 2 && ischar (args{1})) error ("string.sort: DIM must precede DIRECTION."); endif ## Get direction cid = cellfun (@ischar, args); if (any (cid)) dir = lower (args{cid}); ## Put it back, since args is forwarded to the core sort further down. args(cid) = {dir}; else dir = 'ascend'; endif ## Get operating dimension szA = size (A); cid = cellfun (@isnumeric, args); if (any (cid)) dim = args{cid}; else dim = find (szA != 1, 1); if (isempty (dim)) # scalar dim = 1; endif endif ## Encode the text as numeric ranks that preserve lexicographic order, so ## that the missing elements (stored internally as '') can be told apart ## from genuine empty strings and placed according to 'MissingPlacement'. ## Missing elements are flagged as NaN, mirroring the numeric-backed ## classes, which lets the workaround below reuse the same logic. [~, ~, ic] = unique (A.strs); data = reshape (ic, szA); data(A.isMissing) = NaN; ## Special handling for missing elements when missing placement overrides ## default behavior (only if missing data actually exist). is_nan = isnan (data); if (any (is_nan, 'all')) ## FIX ME: this workaround will be removed once the 'sort' function ## in core Octave supports the 'MissingPlacement' optional argument. ## The default core behavior already matches 'auto' (missing last for ## ascending, first for descending), so only the overriding cases are ## handled here. Ranks are positive integers, so -Inf never collides. if ((strcmp (dir, 'ascend') && strcmp (MP, 'first')) || (strcmp (dir, 'descend') && strcmp (MP, 'last'))) ## Convert missing values to -Inf so that they are placed first ## (ascending) or last (descending), overriding the default. data(is_nan) = -Inf; endif endif ## Sort the numeric proxy [~, index] = sort (data, args{:}); ## Calculate linear index n_dims = ndims (A); dimarg = cell (1, n_dims); for i = 1:n_dims if (i == dim) dimarg{i} = index; else dim_sz = szA(i); tmpvec = ones (1, n_dims); tmpvec(i) = dim_sz; tmp_sz = szA; tmp_sz(i) = 1; dimarg{i} = repmat (reshape ([1:dim_sz], tmpvec), tmp_sz); endif endfor ## Return sorted string array B = subset (A, sub2ind (szA, dimarg{:})); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{B} =} sortrows (@var{A}) ## @deftypefnx {string} {@var{B} =} sortrows (@var{A}, @var{col}) ## @deftypefnx {string} {@var{B} =} sortrows (@var{A}, @var{direction}) ## @deftypefnx {string} {@var{B} =} sortrows (@var{A}, @var{col}, @var{direction}) ## @deftypefnx {string} {@var{B} =} sortrows (@dots{}, @qcode{'MissingPlacement'}, @var{MP}) ## @deftypefnx {string} {[@var{B}, @var{index}] =} sortrows (@var{A}, @dots{}) ## ## Sort rows in a string array. ## ## @code{@var{B} = sortrows (@var{A})} sorts the rows of the 2-D string ## array @var{A} in ascending order. The sorted array @var{B} has the same ## size as @var{A}. ## ## @code{@var{B} = sortrows (@var{A}, @var{col})} sorts @var{A} according to ## the columns specified by the numeric vector @var{col}, which must ## explicitly contain non-zero integers whose absolute values index existing ## columns in @var{A}. Positive elements sort the corresponding columns in ## ascending order, while negative elements sort the corresponding columns ## in descending order. ## ## @code{@var{B} = sortrows (@var{A}, @var{direction})} also specifies the ## sorting direction, which can be either @qcode{'ascend'} (default) or ## @qcode{'descend'} applying to all columns in @var{A}. Alternatively, ## @var{direction} can be either a string array or a cell array of character ## vectors specifying the sorting direction for each individual column of ## @var{A}, in which case the number of elements in @var{direction} must ## equal the number of columns in @var{A}. ## ## @code{@var{B} = sortrows (@var{A}, @var{col}, @var{direction})} sorts the ## string array @var{A} according to the columns specified in @var{col} ## using the corresponding sorting direction specified in @var{direction}. ## In this case, the sign of the values in @var{col} is ignored. @var{col} ## and @var{direction} must have the same number of elements, but not ## necessarily equal to the columns of @var{A}. ## ## @code{@var{B} = sortrows (@dots{}, @qcode{'MissingPlacement'}, @var{MP})} ## specifies where to place the missing elements (@qcode{}) ## returned in @var{B} with any of the following options specified in ## @var{MP}: ## ## @itemize ## @item @qcode{'auto'}, which is the default, places missing elements last ## for ascending sort and first for descending sort. ## @item @qcode{'first'} places missing elements first. ## @item @qcode{'last'} places missing elements last. ## @end itemize ## ## @code{[@var{B}, @var{index}] = sortrows (@var{A}, @dots{})} also returns ## an index vector containing the original row indices of @var{A} in the ## sorted matrix @var{B} such that @code{@var{B} = @var{A}(@var{index},:)}. ## ## @end deftypefn function [B, index] = sortrows (A, varargin) ## Input array must be a matrix if (ndims (A) != 2) error ("string.sortrows: A must be a 2-D matrix."); endif ## Parse and validate optional 'MissingPlacement' paired argument optNames = {'MissingPlacement'}; dfValues = {'auto'}; [MP, args] = parsePairedArguments (optNames, dfValues, varargin(:)); ## The values are matched without regard to case, as MATLAB does. MP = lower (MP); if (! any (strcmp (MP, {'auto', 'first', 'last'}))) error ("string.sortrows: invalid value for 'MissingPlacement'."); endif ## Parse COL / DIRECTION input nc = size (A, 2); col = [1:nc]; # default ascending direction dir_flag = false; if (numel (args) > 2) error ("string.sortrows: too many input arguments."); endif if (numel (args) > 0) col = args{1}; if (isnumeric (col)) if (! isvector (col) || any (fix (col) != col) || any (col == 0)) error (strcat ("string.sortrows: COL must be a vector", ... " of nonzero integers indexing columns in A.")); endif if (max (abs (col)) > nc) error ("string.sortrows: COL indexes non-existing column."); endif elseif (isvector (col) && (ischar (col) || iscellstr (col) || isa (col, 'string'))) direction = lower (cellstr (col)); if (! all (ismember (direction, {'ascend', 'descend'}))) error (strcat ("string.sortrows: DIRECTION input must", ... " contain either 'ascend' or 'descend' values.")); endif ## Apply scalar expansion if (isscalar (direction)) direction = repmat (direction, 1, nc); endif if (numel (direction) != nc) error (strcat ("string.sortrows: DIRECTION", ... " does not match the columns in A.")); endif ## Assign DIRECTION to COL col = [1:nc]; idx = strcmp (direction, 'descend'); col(idx) = - col(idx); dir_flag = true; else error ("string.sortrows: invalid type for COL argument."); endif endif if (numel (args) > 1) if (dir_flag) error ("string.sortrows: invalid third input argument."); endif if ((isvector (args{2}) && ischar (args{2})) || isa (args{2}, 'string')) direction = lower (cellstr (args{2})); elseif (isvector (args{2}) && iscellstr (args{2})) direction = args{2}; else error ("string.sortrows: invalid type for DIRECTION argument."); endif if (! all (ismember (direction, {'ascend', 'descend'}))) error (strcat ("string.sortrows: DIRECTION input must", ... " contain either 'ascend' or 'descend' values.")); endif ## Assign DIRECTION to COL if (isscalar (direction) && strcmp (direction, 'ascend')) col = abs (col); elseif (isscalar (direction) && strcmp (direction, 'descend')) col = - abs (col); else if (numel (direction) != numel (col)) error (strcat ("string.sortrows: DIRECTION does", ... " not match the elements in COL.")); endif col = abs (col); idx = strcmp (direction, 'descend'); col(idx) = - col(idx); endif endif ## Encode the text as numeric ranks that preserve lexicographic order, so ## that the missing elements (stored internally as '') can be told apart ## from genuine empty strings and placed according to 'MissingPlacement'. ## Missing elements are flagged as NaN, mirroring the numeric-backed ## classes, which lets the workaround below reuse the same logic. [~, ~, ic] = unique (A.strs); data = reshape (ic, size (A)); data(A.isMissing) = NaN; ## FIX ME: this workaround will be removed once the 'sortrows' function ## in core Octave supports the 'MissingPlacement' optional argument. ## The default core behavior already matches 'auto' (missing last for ## ascending columns, first for descending columns), so only the ## overriding cases are handled here. Ranks are positive integers, so ## -Inf never collides with an existing value. pos_dir = col > 0; neg_dir = col < 0; fix_pos_dir = any (pos_dir) && strcmp (MP, 'first'); fix_neg_dir = any (neg_dir) && strcmp (MP, 'last'); if (fix_pos_dir || fix_neg_dir) if (fix_pos_dir) col_idx = col(pos_dir); else # must be fix_neg_dir col_idx = - col(neg_dir); endif cdata = data(:,col_idx); is_nan = isnan (cdata); if (any (is_nan, 'all')) ## Convert missing values to -Inf so that they are placed first ## (ascending) or last (descending), overriding the default. cdata(is_nan) = -Inf; data(:,col_idx) = cdata; endif endif ## Sort the numeric proxy [~, index] = sortrows (data, col); ## Return sorted string array B = subset (A, index, ':'); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} split (@var{str}) ## @deftypefnx {string} {@var{newstr} =} split (@var{str}, @var{delimiter}) ## @deftypefnx {string} {@var{newstr} =} split (@var{str}, @var{delimiter}, @var{dim}) ## @deftypefnx {string} {[@var{newstr}, @var{match}] =} split (@dots{}) ## ## Split string array at delimiters. ## ## @code{@var{newstr} = split (@var{str})} divides each element of @var{str} ## at whitespace characters and returns the pieces as a string array. The ## whitespace characters are the space, tab, newline, carriage return, form ## feed, and vertical tab. ## ## @code{@var{newstr} = split (@var{str}, @var{delimiter})} divides each ## element at the substrings specified by @var{delimiter}, which can be a ## string array, a character vector, or a cell array of character vectors. ## When @var{delimiter} contains several substrings they are all used; at a ## given position the substrings are tried in order and the first that ## matches is taken. Delimiters are not collapsed, so consecutive ## delimiters yield empty strings in @var{newstr}. ## ## The pieces of each element are laid out along a new dimension. For a ## string scalar that splits into @var{n} pieces, @var{newstr} is ## @code{@var{n}x1}; for an @code{Mx1} column it is @code{Mx@var{n}}; for a ## @code{1xM} row it is @code{1xMx@var{n}}; and in general the pieces extend ## the first trailing singleton dimension. Every element of @var{str} must ## split into the same number of pieces. Missing values in @var{str} are ## preserved and count as a single piece. ## ## @code{@var{newstr} = split (@var{str}, @var{delimiter}, @var{dim})} lays ## the pieces out along dimension @var{dim}, which must be a dimension along ## which @var{str} has size 1. ## ## @code{[@var{newstr}, @var{match}] = split (@dots{})} also returns the ## delimiters matched between the pieces. @var{match} has the same layout ## as @var{newstr} but with one fewer element along the split dimension. ## ## @end deftypefn function [newstr, matchstr] = split (this, varargin) if (numel (varargin) > 2) error ("string.split: too many input arguments."); endif ## Determine the delimiters (default: whitespace characters) if (numel (varargin) >= 1) delim = varargin{1}; if (isa (delim, 'string')) dlms = cellstr (delim); elseif (ischar (delim) || iscellstr (delim)) dlms = cellstr (string (delim)); else error (strcat ("string.split: DELIMITER must be a string array,", ... " a character vector, or a cell array of", ... " character vectors.")); endif dlms = dlms(:).'; else dlms = {" ", sprintf("\t"), sprintf("\n"), sprintf("\r"), ... sprintf("\f"), sprintf("\v")}; endif ## Validate an explicit split dimension (auto-selected otherwise) dim = []; if (numel (varargin) >= 2) dim = varargin{2}; if (! (isnumeric (dim) && isscalar (dim) && dim == fix (dim) ... && dim >= 1)) error ("string.split: DIM must be a positive integer scalar."); endif if (size (this.strs, dim) != 1) error ("string.split: STR must have size 1 along dimension DIM."); endif endif [newstr, matchstr, errmsg] = split_core (this, dlms, dim); if (! isempty (errmsg)) error ("string.split: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} splitlines (@var{str}) ## ## Split string array at newline characters. ## ## @code{@var{newstr} = splitlines (@var{str})} divides each element of ## @var{str} at its newline characters and returns the lines as a string ## array. The recognized newline characters are the line feed, the carriage ## return, a carriage return followed by a line feed (treated as a single ## boundary), the vertical tab, the form feed, and the Unicode next-line ## (@code{U+0085}), line-separator (@code{U+2028}), and paragraph-separator ## (@code{U+2029}) characters. ## ## The lines are laid out along a new dimension exactly as for @code{split}: ## a string scalar with @var{n} lines yields an @code{@var{n}x1} array, and ## an @code{Mx1} column yields an @code{Mx@var{n}} array. Every element of ## @var{str} must contain the same number of newlines. Missing values in ## @var{str} are preserved and count as a single line. ## ## @end deftypefn function out = splitlines (this) cr = sprintf ("\r"); nl = sprintf ("\n"); ## A carriage return + line feed is tried first so it is consumed as a ## single boundary rather than as two separate ones. dlms = {[cr, nl], nl, cr, sprintf("\v"), sprintf("\f"), ... native2unicode(uint8 ([194, 133]), "UTF-8"), ... native2unicode(uint8 ([226, 128, 168]), "UTF-8"), ... native2unicode(uint8 ([226, 128, 169]), "UTF-8")}; [out, ~, errmsg] = split_core (this, dlms, []); if (! isempty (errmsg)) error ("string.splitlines: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} strcat (@var{str1}, @var{str2}, @dots{}) ## ## Horizontal concatenation of texts in string array. ## ## @code{@var{newstr} = strcat (@var{str1}, @var{str2}, @dots{})} merges ## horizontally all the input arguments into a string array, as long as any ## of the input arguments is a string array. All inputs must be of common ## size or scalars. All inputs must be character vectors, cell arrays of ## character vectors, or string arrays. ## ## @end deftypefn function out = strcat (varargin) args = cell (size (varargin)); for i = 1:numel (args) if (ischar (varargin{i})) args{i} = cellstr (varargin{i}); elseif (isstring (varargin{i})) args{i} = cellstr (varargin{i}); elseif (iscellstr (varargin{i})) args{i} = varargin{i}; else error ("string.strcat: incompatible data type: '%s'", ... class (varargin{i})); endif endfor out = string (strcat (args{:})); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} strip (@var{str}) ## @deftypefnx {string} {@var{newstr} =} strip (@var{str}, @var{side}) ## @deftypefnx {string} {@var{newstr} =} strip (@var{str}, @var{stripchar}) ## @deftypefnx {string} {@var{newstr} =} strip (@var{str}, @var{side}, @var{stripchar}) ## ## Remove leading and trailing characters from string array. ## ## @code{@var{newstr} = strip (@var{str})} removes all consecutive ## whitespace characters from the beginning and end of each element of ## @var{str}. The whitespace characters are the space, tab, newline, ## carriage return, form feed, and vertical tab. ## ## @code{@var{newstr} = strip (@var{str}, @var{side})} removes whitespace ## from the side given by @var{side}, which can be @qcode{"left"}, ## @qcode{"right"}, or @qcode{"both"} (the default). ## ## @code{@var{newstr} = strip (@var{str}, @var{stripchar})} removes the ## single character @var{stripchar} instead of whitespace. Because the side ## keywords are longer than one character, a single-character second ## argument is always treated as @var{stripchar}. ## ## @code{@var{newstr} = strip (@var{str}, @var{side}, @var{stripchar})} ## removes @var{stripchar} from the given @var{side}. ## ## @var{newstr} is a string array of the same size as @var{str}. Missing ## values in @var{str} are preserved. ## ## @end deftypefn function out = strip (this, varargin) if (numel (varargin) > 2) error ("string.strip: too many input arguments."); endif istext = @(x) ischar (x) || (isa (x, 'string') && isscalar (x)); side = 'both'; stripchar = ''; haveChar = false; if (numel (varargin) == 1) a = varargin{1}; if (! istext (a)) error (strcat ("string.strip: the second argument must be a side", ... " ('left', 'right', or 'both') or a single", ... " character.")); endif a = char (a); if (numel (str2cp (a)) == 1) stripchar = a; haveChar = true; elseif (any (strcmpi (a, {'left', 'right', 'both'}))) side = a; else error ("string.strip: SIDE must be 'left', 'right', or 'both'."); endif elseif (numel (varargin) == 2) s = varargin{1}; c = varargin{2}; if (! istext (s) || ... ! any (strcmpi (char (s), {'left', 'right', 'both'}))) error ("string.strip: SIDE must be 'left', 'right', or 'both'."); endif if (! istext (c)) error ("string.strip: STRIPCHAR must be a single character."); endif side = char (s); stripchar = char (c); haveChar = true; endif if (haveChar) cp = str2cp (stripchar); if (numel (cp) != 1) error ("string.strip: STRIPCHAR must be a single character."); endif stripcps = cp; else stripcps = uint32 ([9, 10, 11, 12, 13, 32]); endif sl = any (strcmpi (side, {'left', 'both'})); sr = any (strcmpi (side, {'right', 'both'})); out = this; cstr = this.strs; for k = 1:numel (cstr) if (this.isMissing(k)) continue; endif cstr{k} = strip_one (cstr{k}, sl, sr, stripcps); endfor out.strs = cstr; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} pad (@var{str}) ## @deftypefnx {string} {@var{newstr} =} pad (@var{str}, @var{nchars}) ## @deftypefnx {string} {@var{newstr} =} pad (@var{str}, @var{side}) ## @deftypefnx {string} {@var{newstr} =} pad (@var{str}, @var{nchars}, @var{side}) ## @deftypefnx {string} {@var{newstr} =} pad (@dots{}, @var{padchar}) ## ## Add leading or trailing characters to string array. ## ## @code{@var{newstr} = pad (@var{str})} pads each element of @var{str} with ## trailing spaces so that every element is as long as the longest one. ## ## @code{@var{newstr} = pad (@var{str}, @var{nchars})} pads each element to ## @var{nchars} characters. Elements that already have more than ## @var{nchars} characters are left unchanged; @code{pad} never truncates. ## ## @code{@var{newstr} = pad (@var{str}, @var{side})} and ## @code{@var{newstr} = pad (@var{str}, @var{nchars}, @var{side})} add the ## padding on the side given by @var{side}, which can be @qcode{"left"}, ## @qcode{"right"}, or @qcode{"both"}. The default is @qcode{"right"}; for ## @qcode{"both"}, an odd number of pad characters places the extra one on ## the right. ## ## @code{@var{newstr} = pad (@dots{}, @var{padchar})} pads with the single ## character @var{padchar} instead of a space. As @var{padchar} is the last ## argument and a single character, it is told apart from @var{side} by its ## length. ## ## Length is measured in characters, not bytes. @var{newstr} is a string ## array of the same size as @var{str}, and missing values in @var{str} are ## preserved. ## ## @end deftypefn function out = pad (this, varargin) if (numel (varargin) > 3) error ("string.pad: too many input arguments."); endif istext = @(x) ischar (x) || (isa (x, 'string') && isscalar (x)); N = []; side = 'right'; padchar = ' '; args = varargin; i = 1; ## Optional NCHARS comes first when numeric if (i <= numel (args) && isnumeric (args{i})) N = args{i}; if (! (isscalar (N) && isreal (N) && N == fix (N) && N >= 0)) error (strcat ("string.pad: NCHARS must be a nonnegative integer", ... " scalar.")); endif i += 1; endif ## A multi-character text argument is the SIDE if (i <= numel (args) && istext (args{i}) ... && numel (str2cp (char (args{i}))) != 1) a = char (args{i}); if (! any (strcmpi (a, {'left', 'right', 'both'}))) error ("string.pad: SIDE must be 'left', 'right', or 'both'."); endif side = a; i += 1; endif ## A single-character text argument is the PADCHAR if (i <= numel (args) && istext (args{i})) padchar = char (args{i}); if (numel (str2cp (padchar)) != 1) error ("string.pad: PADCHAR must be a single character."); endif i += 1; endif if (i <= numel (args)) error ("string.pad: invalid combination of input arguments."); endif ## Measure element lengths in code points; default NCHARS is the longest ne = numel (this.strs); lens = zeros (ne, 1); for k = 1:ne if (! this.isMissing(k)) lens(k) = numel (str2cp (this.strs{k})); endif endfor if (isempty (N)) if (any (! this.isMissing(:))) N = max (lens(! this.isMissing(:))); else N = 0; endif endif padcp = str2cp (padchar); side = lower (side); out = this; cstr = this.strs; for k = 1:ne if (this.isMissing(k)) continue; endif cstr{k} = pad_one (cstr{k}, N, side, padcp); endfor out.strs = cstr; endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} join (@var{str}) ## @deftypefnx {string} {@var{newstr} =} join (@var{str}, @var{delimiter}) ## @deftypefnx {string} {@var{newstr} =} join (@var{str}, @var{dim}) ## @deftypefnx {string} {@var{newstr} =} join (@var{str}, @var{delimiter}, @var{dim}) ## ## Combine string array elements. ## ## @code{@var{newstr} = join (@var{str})} combines the elements of @var{str} ## along the last dimension whose size is not 1, placing a single space ## between consecutive elements. That dimension is reduced to size 1. ## ## @code{@var{newstr} = join (@var{str}, @var{delimiter})} uses ## @var{delimiter} instead of a space. @var{delimiter} can be a string ## array, a character vector, or a cell array of character vectors. A ## scalar @var{delimiter} is placed between every pair of elements. An ## array @var{delimiter} supplies the text placed between consecutive ## elements and must have one fewer element than @var{str} along the joined ## dimension; its other dimensions must be 1 or match @var{str}. ## ## @code{@var{newstr} = join (@var{str}, @var{dim})} and ## @code{@var{newstr} = join (@var{str}, @var{delimiter}, @var{dim})} ## combine the elements along dimension @var{dim}. ## ## @var{newstr} has the size of @var{str} with the joined dimension reduced ## to 1. If any element being combined, or any delimiter placed between ## them, is a missing value, the corresponding element of @var{newstr} is a ## missing value. ## ## @end deftypefn function newstr = join (this, varargin) if (numel (varargin) > 2) error ("string.join: too many input arguments."); endif istext = @(x) ischar (x) || iscellstr (x) || isa (x, 'string'); delim = []; dim = []; if (numel (varargin) == 1) if (isnumeric (varargin{1})) dim = varargin{1}; elseif (istext (varargin{1})) delim = varargin{1}; else error (strcat ("string.join: the second argument must be a", ... " delimiter or a dimension.")); endif elseif (numel (varargin) == 2) delim = varargin{1}; dim = varargin{2}; if (! istext (delim)) error (strcat ("string.join: DELIMITER must be a string array, a", ... " character vector, or a cell array of character", ... " vectors.")); endif endif sz = size (this.strs); if (isempty (dim)) d = find (sz != 1, 1, 'last'); if (isempty (d)) dim = 2; else dim = d; endif elseif (! (isscalar (dim) && isreal (dim) && dim == fix (dim) ... && dim >= 1)) error ("string.join: DIM must be a positive integer scalar."); endif N = size (this.strs, dim); ## Normalize the delimiter to a cell array DC + missing mask DM sized like ## STR but with N-1 elements along DIM (broadcasting on the other dims) tsz = sz; tsz(dim) = max (N - 1, 0); if (isempty (delim)) Dc = repmat ({' '}, tsz); Dm = false (tsz); else if (isa (delim, 'string')) dc0 = cellstr (delim); dm0 = ismissing (delim); else dc0 = cellstr (string (delim)); dm0 = false (size (dc0)); endif if (isscalar (dc0)) Dc = repmat (dc0, tsz); Dm = repmat (dm0, tsz); else [Dc, Dm, ok] = join_bcast (dc0, dm0, tsz, dim); if (! ok) error (strcat ("string.join: DELIMITER must have one fewer", ... " element than STR along the joined dimension.")); endif endif endif ## Permute the joined dimension to the front and join each column nd = max (numel (sz), dim); perm = [dim, setdiff(1:nd, dim)]; cperm = permute (this.strs, perm); mperm = permute (this.isMissing, perm); pszc = size (cperm); R = prod (pszc(2:end)); C = reshape (cperm, N, R); Mi = reshape (mperm, N, R); D = reshape (permute (Dc, perm), max (N - 1, 0), R); Dm = reshape (permute (Dm, perm), max (N - 1, 0), R); outc = cell (1, R); outm = false (1, R); for r = 1:R if (any (Mi(:,r)) || (N >= 2 && any (Dm(:,r)))) outm(r) = true; outc{r} = ''; elseif (N == 0) outc{r} = ''; else pieces = cell (1, 2 * N - 1); p = 0; for j = 1:N p += 1; pieces{p} = C{j,r}; if (j < N) p += 1; pieces{p} = D{j,r}; endif endfor outc{r} = [pieces{:}]; endif endfor opsz = pszc; opsz(1) = 1; newstr = this; newstr.strs = ipermute (reshape (outc, opsz), perm); newstr.isMissing = ipermute (reshape (outm, opsz), perm); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} plus (@var{str1}, @var{str2}) ## ## Append strings. ## ## @code{@var{newstr} = lower (@var{str})} is the equivalent of the syntax ## @code{@var{newstr} = @var{str1} + @var{str2}} and appends @var{str2} to ## @var{str1}. Both input arguments must be string arrays of compatible ## size. ## ## @end deftypefn function out = plus (str1, str2) if (isa (str1, 'string') && isa (str2, 'string')) out = append (str1, str2); else error ("string.plus: both STR1 and STR2 must be string arrays."); endif endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} lower (@var{str}) ## ## Convert contents of string array to lower case. ## ## @code{@var{newstr} = lower (@var{str})} converts all upper case ## characters in every element of the string array @var{str} to lower case. ## @var{newstr} is a string array of the same size as @var{str}. ## ## @end deftypefn function out = lower (this) out = this; out.strs = lower (this.strs); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{newstr} =} upper (@var{str}) ## ## Convert contents of string array to upper case. ## ## @code{@var{newstr} = upper (@var{str})} converts all lower case ## characters in every element of the string array @var{str} to upper case. ## @var{newstr} is a string array of the same size as @var{str}. ## ## @end deftypefn function out = upper (this) out = this; out.strs = upper (this.strs); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{B} =} unique (@var{A}) ## @deftypefnx {string} {@var{B} =} unique (@var{A}, @qcode{'rows'}) ## @deftypefnx {string} {[@var{B}, @var{ixA}, @var{ixB}] =} unique (@dots{}) ## @deftypefnx {string} {@dots{} =} unique (@dots{}, @var{order}) ## @deftypefnx {string} {@dots{} =} unique (@dots{}, @var{occurrence}) ## ## Unique values in a string array. ## ## @code{@var{B} = unique (@var{A})} returns the unique values of the string ## array @var{A} in the string vector @var{B} sorted lexicographically. If ## If @var{A} is a column vector, then @var{B} is also a column vector, ## otherwise @code{unique} returns a row vector. ## ## @code{@var{B} = unique (@var{A}, @qcode{'rows'})} returns the unique rows ## of the string matrix @var{A} in the string matrix @var{B} sorted in ## lexicographical order. ## ## @code{[@var{B}, @var{ixA}, @var{ixB}] = unique (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} such that ## @code{@var{B} = @var{A}(@var{ixA})} and ## @code{@var{A} = @var{B}(@var{ixB})}, unless the @qcode{'rows'} optional ## argument is given, in which case @code{@var{B} = @var{A}(@var{ixA},:)} ## and @code{@var{A} = @var{B}(@var{ixB},:)}. ## ## @code{@dots{} = unique (@dots{}, @var{order})} also specifies the order ## of the returned unique values. @var{order} may be either ## @qcode{'sorted'}, which is the default behavior, or @qcode{'stable'}, in ## which case the unique values are returned in order of appearance. ## ## @code{@dots{} = unique (@dots{}, @var{occurrence})} also specifies the ## which index is returned in @var{ixA}, where there are repeated values or ## rows (if opted) in the input categorical array. @var{occurrence} may be ## either @qcode{'first'}, which is the default and returns the index of the ## first occurrence of each unique value, or @qcode{'last'}, in which case ## the last occurrence of each unique value is returned. ## ## @end deftypefn function [B, ixA, ixB] = unique (A, varargin) ## Handle 'rows' option do_rows = false; if (! isempty (varargin)) idx = strcmpi ('rows', varargin(:)); if (any (idx)) do_rows = true; varargin(idx) = []; if (ndims (A) != 2) error ("string.unique: 'rows' applies only to 2-D matrices."); endif endif endif ## Handle 'setOrder' and 'occurrence' options opt = "sorted"; if (! isempty (varargin)) if (any (strcmp (varargin{1}, {"sorted", "stable", "first", "last"}))) opt = varargin{1}; else error ("string.unique: invalid option '%s'.", varargin{1}); endif endif ## Find unique if (do_rows) is_nm = ! any (A.isMissing, 2); A = subset (A, is_nm, ':'); [~, ixA, ixB] = __unique__ (A.strs, 'rows', opt); B = subset (A, ixA, ':'); is_missing = ! is_nm; if (any (is_missing)) w = size (A, 2); B = [B; repmat(missing, sum (is_missing), w)]; endif else is_nm = ! A.isMissing; A = subset (A, is_nm); [~, ixA, ixB] = __unique__ (A.strs, opt); B = subset (A, ixA); is_missing = ! is_nm(:); if (any (is_missing)) if (isrow (A)) mstr = repmat (missing, 1, sum (is_missing)); B = [B, mstr]; else mstr = repmat (missing, sum (is_missing), 1); B = [B; mstr]; endif endif endif endfunction endmethods ################################################################################ ## ** Array Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'cat' 'horzcat' 'vertcat' 'repmat' ## ## 'repelem' 'repelems' 'reshape' 'circshift' ## ## 'permute' 'ipermute' 'transpose' 'ctranspose' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {string} {@var{C} =} cat (@var{dim}, @var{A}, @var{B}, @dots{}) ## ## Concatenate string arrays. ## ## @code{@var{C} = cat (@var{dim}, @var{A}, @var{B}, @dots{})} concatenates ## string arrays @var{A}, @var{B}, @dots{} along dimension @var{dim}. All ## input arrays must have the same size except along the operating dimension ## @var{dim}. Any of the input arrays may also be character matrixes, cell ## arrays of character vectors, numeric arrays, or logical arrays of ## compatible size. ## ## @end deftypefn function out = cat (dim, varargin) ## For categorical, datetime, and duration arrays being present in the ## input arguments, call their constructor for the first input array and ## forward all input to their respective concatenation method. is_datetime = cellfun ('isdatetime', varargin); is_duration = cellfun ('isduration', varargin); if (any (cellfun ('iscategorical', varargin))) varargin{1} = categorical (varargin{1}); out = cat (dim, varargin{:}); return; elseif (any (is_datetime)) idx = find (is_datetime, 1); tmp = varargin{idx}; varargin{1} = datetime (varargin{1}, 'Format', tmp.Format); out = cat (dim, varargin{:}); return; elseif (any (cellfun ('isduration', varargin))) idx = find (is_duration, 1); tmp = varargin{idx}; varargin{1} = duration (varargin{1}, 'Format', tmp.Format); out = cat (dim, varargin{:}); return; endif ## For everything else, try converting it a to string array idx = find (cellfun (@(x) ! isstring (x), varargin)); if (! isempty (idx)) for i = idx varargin{i} = string (varargin{i}); endfor endif ## Concatenate strings out = string; tmp = cellfun (@(obj) obj.strs, varargin, 'UniformOutput', false); out.strs = cat (dim, tmp{:}); tmp = cellfun (@(obj) obj.isMissing, varargin, 'UniformOutput', false); out.isMissing = cat (dim, tmp{:}); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{C} =} horzcat (@var{A}, @var{B}, @dots{}) ## ## Horizontal concatenation of string arrays. ## ## @code{@var{C} = horzcat (@var{A}, @var{B}, @dots{})} is the equivalent of ## the syntax @code{@var{C} = [@var{A}, @var{B}, @dots{}]} and horizontally ## concatenates the string arrays @var{A}, @var{B}, @dots{}. All input ## arrays must have the same size except along the second dimension. Any of ## the input arrays may also be character matrixes, cell arrays of character ## vectors, numeric arrays, or logical arrays of compatible size. ## ## @end deftypefn function out = horzcat (varargin) out = cat (2, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{C} =} vertcat (@var{A}, @var{B}, @dots{}) ## ## Vertical concatenation of string arrays. ## ## @code{@var{C} = vertcat (@var{A}, @var{B}, @dots{})} is the equivalent of ## the syntax @code{@var{C} = [@var{A}; @var{B}; @dots{}]} and vertically ## concatenates the string arrays @var{A}, @var{B}, @dots{}. All input ## arrays must have the same size except along the first dimension. Any of ## the input arrays may also be character matrixes, cell arrays of character ## vectors, numeric arrays, or logical arrays of compatible size. ## ## @end deftypefn function out = vertcat (varargin) out = cat (1, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{B} =} repmat (@var{A}, @var{n}) ## @deftypefnx {string} {@var{B} =} repmat (@var{A}, @var{d1}, @dots{}, @var{dN}) ## @deftypefnx {string} {@var{B} =} repmat (@var{A}, @var{dimvec}) ## ## Repeat copies of a string array. ## ## @code{@var{B} = repmat (@var{A}, @var{n})} returns a string array @var{B} ## containing @var{n} copies of the input string array @var{A} along every ## dimension of @var{A}. ## ## @code{@var{B} = repmat (@var{A}, @var{d1}, @dots{}, @var{dN})} returns an ## array @var{B} containing copies of @var{A} along the dimensions specified ## by the list of scalar integer values @var{d1}, @dots{}, @var{dN}, which ## specify how many copies of @var{A} are made in each dimension. ## ## @code{@var{B} = repmat (@var{A}, @var{dimvec})} is equivalent to the ## previous syntax with @code{@var{dimvec} = [@var{d1}, @dots{}, @var{dN}]}. ## ## @end deftypefn function this = repmat (this, varargin) this.strs = repmat (this.strs, varargin{:}); this.isMissing = repmat (this.isMissing, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{B} =} repelem (@var{A}, @var{n}) ## @deftypefnx {string} {@var{B} =} repelem (@var{A}, @var{d1}, @dots{}, @var{dN}) ## ## Repeat copies of string array elements. ## ## @code{@var{B} = repelem (@var{A}, @var{n})} returns a string vector ## @var{B} containing repeated elements of the input @var{A}, which must be ## a string vector. If @var{n} is a scalar, each element of @var{A} is ## repeated @var{n} times along the non-singleton dimension of @var{A}. If ## @var{n} is a vector, it must have the same elements as @var{A}, in which ## case it specifies the number of times to repeat each corresponding ## element of @var{A}. ## ## @code{@var{B} = repelem (@var{A}, @var{d1}, @dots{}, @var{dN})} returns ## an array @var{B} with each element of @var{A} repeated according to the ## the list of input arguments @code{@var{d1}, @dots{}, @var{dN}} each ## corresponding to a different dimension @code{1:ndims (@var{A})} of the ## input array @var{A}. @var{d1}, @dots{}, @var{dN} must be either scalars ## or vectors with the same length as the corresponding dimension of ## @var{A} containing non-negative integer values specifying the number of ## repetitions of each element along the corresponding dimension. ## ## @end deftypefn function this = repelem (this, varargin) this.strs = repelem (this.strs, varargin{:}); this.isMissing = repelem (this.isMissing, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{B} =} repelems (@var{A}, @var{R}) ## ## Construct a vector of repeated elements from a string array. ## ## @code{@var{B} = repelems (@var{A}, @var{R})} returns a string vector ## @var{B} containing repeated elements of the input @var{A}, which must be ## a string vector. @var{R} must be a @math{2*N} matrix of integers. ## Entries in the first row of @var{R} correspond to the linear indexing of ## the elements in @var{A} to be repeated. The corresponding entries in the ## second row of @var{R} specify the repeat count of each element. ## ## @end deftypefn function this = repelems (this, R) this.strs = repelems (this.strs, R); this.isMissing = repelems (this.isMissing, R); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{B} =} reshape (@var{A}, @var{d1}, @dots{}, @var{dN}) ## @deftypefnx {string} {@var{B} =} reshape (@var{A}, @dots{}, @qcode{[]}, @dots{}) ## @deftypefnx {string} {@var{B} =} reshape (@var{A}, @var{dimvec}) ## ## Reshape string array. ## ## @code{@var{B} = reshape (@var{A}, @var{d1}, @dots{}, @var{dN})} returns ## a string array @var{B} with specified dimensions @var{d1}, @dots{}, ## @var{dN}, whose elements are taken columnwise from the string array ## @var{A}. The product of @var{d1}, @dots{}, @var{dN} must equal the total ## number of elements in @var{A}. ## ## @code{@var{B} = reshape (@var{A}, @dots{}, @qcode{[]}, @dots{})} returns ## a string array @var{B} with one dimension unspecified which is calculated ## automatically so that the product of dimensions in @var{B} matches the ## total elements in @var{A}, which must be divisible the product of ## specified dimensions. An empty matrix @qcode{([])} is used to flag the ## unspecified dimension. ## ## @end deftypefn function this = reshape (this, varargin) this.strs = reshape (this.strs, varargin{:}); this.isMissing = reshape (this.isMissing, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{B} =} circshift (@var{A}, @var{n}) ## @deftypefnx {string} {@var{B} =} circshift (@var{A}, @var{n}, @var{dim}) ## ## Circularly shift the elements in a string array. ## ## @code{@var{B} = circshift (@var{A}, @var{n})} circularly shifts the ## elements of the string array @var{A} according to @var{n}. If @var{n} ## is a nonzero integer scalar, then the elements of @var{A} are shifted by ## @var{n} elements along the first non-singleton dimension of @var{A}. If ## @var{n} is a vector, it must not be longer that the number of dimensions ## of @var{A} with each value of @var{n} corresponding to a dimension in ## @var{A}. The sign of the value(s) in @var{n} specify the direction in ## the elements of @var{A} are shifted. ## ## @code{@var{B} = circshift (@var{A}, @var{n}, @var{dim})} circularly ## shifts the elements of the string array @var{A} along the dimension ## specified by @var{dim}. In this case, @var{n} must be a scalar integer ## value. ## ## @end deftypefn function this = circshift (this, varargin) this.strs = circshift (this.strs, varargin{:}); this.isMissing = circshift (this.isMissing, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{B} =} permute (@var{A}, @var{dims}) ## ## Generalized transpose for a string N-D array. ## ## @code{@var{B} = permute (@var{A}, @var{dims})} returns the generalized ## transpose of the string array @var{A} by rearranging its dimensions ## according to the permutation vector specified in @var{dims}. ## ## @var{dims} must index all the dimensions @code{1:ndims (@var{A})} of the ## input array @var{A}, in any order, but only once. The @var{N}th ## dimension of @var{A} gets remapped to the dimension in @var{B} specified ## by @code{@var{dims}(@var{N})}. ## ## @end deftypefn function this = permute (this, varargin) this.strs = permute (this.strs, varargin{:}); this.isMissing = permute (this.isMissing, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{A} =} ipermute (@var{B}, @var{dims}) ## ## Inverse of the generalized transpose for a string N-D array. ## ## @code{@var{A} = ipermute (@var{B}, @var{dims})} returns the inverse of ## the generalized transpose performed by the @code{permute} function. The ## expression @code{ipermute (permute (@var{A}, @var{dims}), @var{dims})} ## returns the original array @var{A}. ## ## @var{dims} must index all the dimensions @code{1:ndims (@var{B})} of the ## input array @var{B}, in any order, but only once. The dimension of ## @var{B} specified in @code{@var{dims}(@var{N})} gets remapped to the ## @var{N}th dimension of @var{A}. ## ## @end deftypefn function this = ipermute (this, varargin) this.strs = ipermute (this.strs, varargin{:}); this.isMissing = ipermute (this.isMissing, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{B} =} transpose (@var{A}) ## ## Transpose a string matrix. ## ## @code{@var{B} = transpose (@var{A})} is the equivalent of the syntax ## @code{@var{B} = @var{A}.'} and returns the transpose of the string ## matrix @var{A}. ## ## @end deftypefn function this = transpose (this, varargin) this.strs = transpose (this.strs, varargin{:}); this.isMissing = transpose (this.isMissing, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {string} {@var{B} =} ctranspose (@var{A}) ## ## Transpose a string matrix. ## ## @code{@var{B} = ctranspose (@var{A})} is the equivalent of the syntax ## @code{@var{B} = @var{A}'} and returns the transpose of the string matrix ## @var{A}. For string arrays, @code{ctranspose} is identical to ## @code{transpose}. ## ## @end deftypefn function this = ctranspose (this, varargin) this.strs = ctranspose (this.strs, varargin{:}); this.isMissing = ctranspose (this.isMissing, varargin{:}); endfunction endmethods ################################################################################ ## ** Reference and Assignment Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'end' 'subsref' 'subsasgn' ## ## ## ################################################################################ methods (Hidden) ## Overloaded end keyword function last_index = end (this, end_dim, ndim_obj) lastdim = ndims (this); if (end_dim == ndim_obj && ndim_obj == 1) last_index = prod (size (this)); elseif (end_dim == ndim_obj && end_dim < lastdim) last_index = prod (size (this)(end_dim:lastdim)); else last_index = size (this, end_dim); endif endfunction ## Class specific subscripted reference function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case '()' out = this; out.strs = this.strs(s.subs{:}); out.isMissing = this.isMissing(s.subs{:}); case '{}' out = this.strs(s.subs{:}); case '.' error (strcat ("string.subsref: '.' invalid indexing", ... " for referencing values. Use '()' instead.")); endswitch ## Chained references if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction ## Class specific subscripted assignment function this = subsasgn (this, s, val) ## Chained subscripts chain_s = s(2:end); s = s(1); if (! isempty (chain_s)) rhs_in = subsref (this, s); rhs = subsasgn (rhs_in, chain_s, val); else rhs = val; endif switch (s.type) case '()' if (isempty (rhs)) this.strs(s.subs{:}) = []; this.isMissing(s.subs{:}) =[]; return; elseif (! isa (rhs, "string")) rhs = string (rhs); endif oldnum = numel (this.strs); this.strs(s.subs{:}) = rhs.strs; this.isMissing(s.subs{:}) = rhs.isMissing; ## Growing past the end auto-fills gap cells with [] (and isMissing ## false); MATLAB fills such gaps with , so mark them. A ## genuine missing element stores '' (char), so 'ischar' separates ## real entries from the numeric-empty gaps. if (numel (this.strs) > oldnum) gaps = ! cellfun ('ischar', this.strs); this.strs(gaps) = {''}; this.isMissing(gaps) = true; endif case '{}' if (! ischar (rhs) || ! isvector (rhs)) error (strcat ("string.subsasgn: '{}' indexed assignment", ... " requires a character vector.")); endif if (numel (this.strs(s.subs{:})) != 1) error (strcat ("string.subsasgn: '{}' indexing can", ... " only be used for simple assignment.")); endif this.strs(s.subs{:}) = {rhs}; this.isMissing(s.subs{:}) = false; case '.' error (strcat ("string.subsasgn: '.' invalid indexing for", ... " assigning values. Use '()' or '{}' instead.")); endswitch endfunction endmethods ################################################################################ ## ** Overloaded methods for duration and categorical classes ** ## ################################################################################ ## Available Methods ## ## ## ## 'colon' 'linspace' 'intersect' 'setdiff' ## ## 'setxor' 'union' 'isequal' 'isequaln' ## ## ## ################################################################################ methods (Hidden) ## Overload colon for duration support function R = colon (varargin) ## Get properties from first duration input idx = find (cellfun ('isduration', varargin), 1); if (isempty (idx)) error ("string.colon: unsupported input types."); endif A = varargin{idx}; ## Convert first input (string) to duration varargin{1} = duration (varargin{1}, 'Format', A.Format); ## Call duration overloaded method R = colon (varargin{:}); endfunction ## Overload linspace for duration support function R = linspace (A, B, n = 100) ## Check for duration input if (! isduration (B)) error ("string.linspace: unsupported input types."); endif ## Convert first input (string) to duration A = duration (A, 'Format', B.Format); ## Call duration overloaded method R = linspace (A, B, n); endfunction ## Overload intersect for duration or categorical support function [C, ixA, ixB] = intersect (A, B, varargin) ## Check for duration input if (! (isduration (B) || iscategorical (B))) error ("string.intersect: unsupported input types."); endif ## Convert first input (string) to cellstr A = cellstr (A); ## Call overloaded method [C, ixA, ixB] = intersect (A, B, varargin{:}); endfunction ## Overload setdiff for duration or categorical support function [C, index] = setdiff (A, B, varargin) ## Check for duration input if (! (isduration (B) || iscategorical (B))) error ("string.setdiff: unsupported input types."); endif ## Convert first input (string) to cellstr A = cellstr (A); ## Call duration overloaded method [C, index] = setdiff (A, B, varargin{:}); endfunction ## Overload setxor for duration or categorical support function [C, ixA, ixB] = setxor (A, B, varargin) ## Check for duration input if (! (isduration (B) || iscategorical (B))) error ("string.setxor: unsupported input types."); endif ## Convert first input (string) to cellstr A = cellstr (A); ## Call duration overloaded method [C, ixA, ixB] = setxor (A, B, varargin{:}); endfunction ## Overload union for duration or categorical support function [C, ixA, ixB] = union (A, B, varargin) ## Check for duration input if (! (isduration (B) || iscategorical (B))) error ("string.union: unsupported input types."); endif ## Convert first input (string) to cellstr A = cellstr (A); ## Call duration overloaded method [C, ixA, ixB] = union (A, B, varargin{:}); endfunction ## Overload isequal for categorical support function TF = isequal (varargin) ## Check for categorical input idx = find (cellfun ('iscategorical', varargin), 1); if (isempty (idx)) [varargin, istext] = coerce_text (varargin); if (! istext) ## An operand that is not text can never equal a string, which is ## answered rather than raised. TF = false; return; endif tmp1 = cellfun (@(x) x.strs, varargin, 'UniformOutput', false); tmp2 = cellfun (@(x) x.isMissing, varargin, 'UniformOutput', false); if (any (cellfun (@(x) any (x, 'all'), tmp2))) TF = false; else TF = isequal (tmp1{:}) && isequal (tmp2{:}); endif else ## Convert first input (string) to categorical varargin{1} = categorical (varargin{1}); ## Call categorical overloaded method TF = isequal (varargin{:}); endif endfunction ## Overload isequaln for categorical support function TF = isequaln (varargin) ## Check for categorical input idx = find (cellfun ('iscategorical', varargin), 1); if (isempty (idx)) [varargin, istext] = coerce_text (varargin); if (! istext) ## An operand that is not text can never equal a string, which is ## answered rather than raised. TF = false; return; endif tmp1 = cellfun (@(x) x.strs, varargin, 'UniformOutput', false); tmp2 = cellfun (@(x) x.isMissing, varargin, 'UniformOutput', false); TF = isequal (tmp1{:}) && isequal (tmp2{:}); else ## Convert first input (string) to categorical varargin{1} = categorical (varargin{1}); ## Call categorical overloaded method TF = isequaln (varargin{:}); endif endfunction endmethods methods (Access = private) ## Return a subset of the array function this = subset (this, varargin) this = this; this.strs = this.strs(varargin{:}); this.isMissing = this.isMissing(varargin{:}); endfunction ## Shared implementation of extractAfter (AFTER true) and extractBefore ## (AFTER false). PAT may be numeric positions or text boundaries; an ## unmatched text boundary yields a missing value. On invalid input returns ## a non-empty ERRMSG describing the fault, leaving OUT unchanged, so the ## calling method can emit the error under its own name. function [out, errmsg] = extract_side (this, pat, after) out = this; errmsg = ''; cstr = this.strs; isMiss = this.isMissing; sz = size (cstr); if (isnumeric (pat)) [pos, errmsg] = eb_expand (pat, sz, 'POS'); if (! isempty (errmsg)) return; endif if (any (pos(:) != fix (pos(:))) || any (pos(:) < 1)) errmsg = "POS must be a positive integer."; return; endif sel = ! isMiss; cps = cell (size (cstr)); cps(sel) = cellfun (@str2cp, cstr(sel), 'UniformOutput', false); lens = cellfun (@numel, cps); # 0 for missing entries if (any (pos(sel) > lens(sel))) errmsg = "POS exceeds the length of the string."; return; endif if (after) cps(sel) = cellfun (@(cp, p) cp((p + 1):end), ... cps(sel), num2cell (pos(sel)), ... 'UniformOutput', false); else cps(sel) = cellfun (@(cp, p) cp(1:(p - 1)), ... cps(sel), num2cell (pos(sel)), ... 'UniformOutput', false); endif cstr(sel) = cellfun (@cp2str, cps(sel), 'UniformOutput', false); else if (isa (pat, 'string')) pat = cellstr (pat); elseif (ischar (pat) || iscellstr (pat)) ## Route char/cellstr through the constructor, which keeps trailing ## whitespace that bare 'cellstr' would deblank pat = cellstr (string (pat)); else errmsg = strcat ("PAT must be a string array, a character", ... " vector, or a cell array of character vectors."); return; endif [pat, errmsg] = eb_expand (pat, sz, 'PAT'); if (! isempty (errmsg)) return; endif for k = 1:numel (cstr) if (isMiss(k)) continue; endif s = cstr{k}; p = pat{k}; i = []; if (! isempty (p)) i = strfind (s, p); endif if (isempty (i)) cstr{k} = ''; # PAT not found -> missing isMiss(k) = true; continue; endif i = i(1); if (after) cstr{k} = s((i + numel (p)):end); else cstr{k} = s(1:(i - 1)); endif endfor endif out.strs = cstr; out.isMissing = isMiss; endfunction ## Shared implementation of insertAfter (AFTER true) and insertBefore ## (AFTER false). PAT may be numeric positions or text boundaries; text ## boundaries are matched at every non-overlapping occurrence (an unmatched ## boundary leaves the element unchanged). NEW is the text to insert, ## scalar or the same size as STR; a missing NEW makes the corresponding ## element missing. On invalid input returns a non-empty ERRMSG, leaving ## OUT unchanged, so the calling method can emit the error under its own ## name. function [out, errmsg] = insert_side (this, pat, new, after) out = this; cstr = this.strs; isMiss = this.isMissing; sz = size (cstr); [newc, newMiss, errmsg] = norm_new (new, sz); if (! isempty (errmsg)) return; endif if (isnumeric (pat)) [pos, errmsg] = eb_expand (pat, sz, 'POS'); if (! isempty (errmsg)) return; endif if (any (pos(:) != fix (pos(:))) || any (pos(:) < 1)) errmsg = "POS must be a positive integer."; return; endif proc = ! isMiss; # elements to act on gone = proc & newMiss; # missing NEW -> element missing do_ = proc & ! newMiss; # elements actually spliced cstr(gone) = {''}; isMiss(gone) = true; cps = cell (size (cstr)); cps(do_) = cellfun (@str2cp, cstr(do_), 'UniformOutput', false); lens = cellfun (@numel, cps); # 0 outside do_ if (any (pos(do_) > lens(do_))) errmsg = "POS exceeds the length of the string."; return; endif ncps = cellfun (@str2cp, newc(do_), 'UniformOutput', false); if (after) cps(do_) = cellfun (@(cp, ncp, p) [cp(1:p), ncp, cp((p + 1):end)], ... cps(do_), ncps, num2cell (pos(do_)), ... 'UniformOutput', false); else cps(do_) = cellfun (@(cp, ncp, p) [cp(1:(p - 1)), ncp, cp(p:end)], ... cps(do_), ncps, num2cell (pos(do_)), ... 'UniformOutput', false); endif cstr(do_) = cellfun (@cp2str, cps(do_), 'UniformOutput', false); else if (isa (pat, 'string')) pat = cellstr (pat); elseif (ischar (pat) || iscellstr (pat)) ## Route char/cellstr through the constructor, which keeps trailing ## whitespace that bare 'cellstr' would deblank pat = cellstr (string (pat)); else errmsg = strcat ("PAT must be a string array, a character", ... " vector, or a cell array of character vectors."); return; endif [pat, errmsg] = eb_expand (pat, sz, 'PAT'); if (! isempty (errmsg)) return; endif for k = 1:numel (cstr) if (isMiss(k)) continue; endif if (newMiss(k)) cstr{k} = ''; isMiss(k) = true; continue; endif p = pat{k}; if (isempty (p)) continue; # an empty boundary matches nothing endif if (after) cstr{k} = strrep (cstr{k}, p, [p, newc{k}]); else cstr{k} = strrep (cstr{k}, p, [newc{k}, p]); endif endfor endif out.strs = cstr; out.isMissing = isMiss; endfunction ## Shared engine for split/splitlines. Splits every element at the DLMS ## delimiters and lays the pieces out along dimension DIM (auto-selected ## when DIM is empty: the first trailing singleton dimension, or dimension 1 ## for a scalar). Returns the split array NEWSTR, the matched delimiters ## MATCHSTR (one fewer along DIM), and a non-empty ERRMSG if the elements do ## not all split into the same number of pieces; the caller emits the error ## under its own name. Missing elements count as a single (missing) piece. function [newstr, matchstr, errmsg] = split_core (this, dlms, dim) newstr = this; matchstr = this; errmsg = ''; sz = size (this.strs); ne = numel (this.strs); if (isempty (dim)) if (isscalar (this.strs)) dim = 1; else dim = 2; while (size (this.strs, dim) != 1) dim += 1; endwhile endif endif ## Split every element, requiring a common piece count pieces = cell (ne, 1); matches = cell (ne, 1); counts = zeros (ne, 1); for k = 1:ne if (this.isMissing(k)) pieces{k} = {''}; matches{k} = cell (1, 0); counts(k) = 1; else [pieces{k}, matches{k}] = split_one (this.strs{k}, dlms); counts(k) = numel (pieces{k}); endif endfor if (ne > 0 && any (counts != counts(1))) errmsg = strcat ("each element of STR must split into the same", ... " number of substrings."); return; endif N = 0; if (ne > 0) N = counts(1); endif ## Assemble the pieces (and matches) into element x count matrices if (ne > 0) pc = vertcat (pieces{:}); # ne x N mc = vertcat (matches{:}); # ne x (N - 1) else pc = cell (0, N); mc = cell (0, max (N - 1, 0)); endif pm = false (ne, N); if (ne > 0) pm(:,1) = this.isMissing(:); endif [newstr.strs, newstr.isMissing] = split_place (pc, pm, sz, dim, N); Nm = max (N - 1, 0); [matchstr.strs, matchstr.isMissing] = ... split_place (mc, false (ne, Nm), sz, dim, Nm); endfunction endmethods endclassdef ## Coerce the operands of an equality comparison to string arrays. Text in ## any form is comparable, so a character array or a cell array of character ## vectors becomes a string. Anything else is not text at all and ISTEXT is ## returned false, so the caller answers false instead of raising. function [args, istext] = coerce_text (args) istext = true; for i = 1:numel (args) if (isa (args{i}, 'string')) continue; elseif (ischar (args{i}) || iscellstr (args{i})) args{i} = string (args{i}); else istext = false; return; endif endfor endfunction ## Broadcast a scalar boundary argument to the size of STR, or verify that it ## already matches. Works for both numeric arrays and cell arrays of patterns. ## On a size mismatch returns a non-empty ERRMSG naming the offending argument; ## the calling method emits the error under its own name. function [arg, errmsg] = eb_expand (arg, sz, name) errmsg = ''; if (numel (arg) == 1) arg = repmat (arg, sz); elseif (! isequal (size (arg), sz)) errmsg = sprintf ("%s must be scalar or the same size as STR.", name); endif endfunction ## Erase the content of a single character vector S between two boundaries. ## When ISPOS is true, A and B are numeric character positions; otherwise they ## are the start and end boundary substrings. INCLUSIVE selects whether the ## boundaries themselves are erased. On an out-of-range position returns a ## non-empty ERRMSG; the calling method emits the error under its own name. function [s, errmsg] = eb_between (s, a, b, isPos, inclusive) errmsg = ''; if (isPos) cp = str2cp (s); n = numel (cp); if (a != fix (a) || b != fix (b) || a < 1 || a > b || b > n) errmsg = "position indices out of range."; return; endif if (inclusive) cp(a:b) = []; elseif (a + 1 <= b - 1) cp((a + 1):(b - 1)) = []; endif s = cp2str (cp); else if (isempty (a) || isempty (b)) return; # an empty boundary matches nothing endif i = strfind (s, a); if (isempty (i)) return; # start boundary not found endif i = i(1); aEnd = i + numel (a) - 1; j = strfind (s, b); j = j(j >= aEnd + 1); # end boundary must follow the start match if (isempty (j)) return; # end boundary not found endif j = j(1); if (inclusive) s(i:(j + numel (b) - 1)) = []; elseif (aEnd + 1 <= j - 1) s((aEnd + 1):(j - 1)) = []; endif endif endfunction ## Replace the content of a single character vector S between two boundaries ## with the text NEW. When ISPOS is true, A and B are numeric character ## positions; otherwise they are the start and end boundary substrings, matched ## at their first occurrence (an unmatched boundary leaves S unchanged). ## INCLUSIVE selects whether the boundaries themselves are replaced. The ## replaced span may be empty, in which case NEW is inserted between the ## boundaries. On an out-of-range position returns a non-empty ERRMSG; the ## calling method emits the error under its own name. function [s, errmsg] = rb_between (s, a, b, isPos, inclusive, new) errmsg = ''; if (isPos) cp = str2cp (s); n = numel (cp); if (a != fix (a) || b != fix (b) || a < 1 || a > b || b > n) errmsg = "position indices out of range."; return; endif ncp = str2cp (new); if (inclusive) cp = [cp(1:(a - 1)), ncp, cp((b + 1):end)]; else cp = [cp(1:a), ncp, cp(b:end)]; endif s = cp2str (cp); else if (isempty (a) || isempty (b)) return; # an empty boundary matches nothing endif i = strfind (s, a); if (isempty (i)) return; # start boundary not found endif i = i(1); aEnd = i + numel (a) - 1; j = strfind (s, b); j = j(j >= aEnd + 1); # end boundary must follow the start match if (isempty (j)) return; # end boundary not found endif j = j(1); if (inclusive) s = [s(1:(i - 1)), new, s((j + numel (b)):end)]; else s = [s(1:aEnd), new, s(j:end)]; endif endif endfunction ## Normalize the inserted-text argument NEW to a cell array of character vectors ## NEWC plus a logical missing mask NEWMISS, each broadcast to the size SZ of ## STR. Mirrors the boundary-broadcasting rules of the insert/extract family. ## On invalid input returns a non-empty ERRMSG; the calling method emits the ## error under its own name. function [newc, newMiss, errmsg] = norm_new (new, sz) newc = {}; newMiss = []; errmsg = ''; if (isa (new, 'string')) newc = cellstr (new); newMiss = ismissing (new); elseif (ischar (new) || iscellstr (new)) ## Route char/cellstr through the constructor, which keeps trailing ## whitespace that bare 'cellstr' would deblank newc = cellstr (string (new)); newMiss = false (size (newc)); else errmsg = strcat ("NEW must be a string array, a character", ... " vector, or a cell array of character vectors."); return; endif [newc, errmsg] = eb_expand (newc, sz, 'NEW'); if (! isempty (errmsg)) return; endif newMiss = eb_expand (newMiss, sz, 'NEW'); endfunction ## Extract a single span of S between positions A and B. INCLUSIVE selects ## whether the characters at A and B are kept. On an out-of-range position ## returns a non-empty ERRMSG; the calling method emits the error under its own ## name. function [s, errmsg] = eb_span (s, a, b, inclusive) errmsg = ''; cp = str2cp (s); n = numel (cp); if (a != fix (a) || b != fix (b) || a < 1 || a > b || b > n) errmsg = "position indices out of range."; return; endif if (inclusive) s = cp2str (cp(a:b)); elseif (a + 1 <= b - 1) s = cp2str (cp((a + 1):(b - 1))); else s = ''; endif endfunction ## Collect the non-overlapping substrings of S that lie between successive START ## and END boundary substrings, scanning left to right. INCLUSIVE selects ## whether the boundary substrings themselves are included. function m = extract_between_matches (s, sp, ep, inclusive) m = {}; if (isempty (sp) || isempty (ep)) return; endif i = 1; n = numel (s); while (i <= n) si = strfind (s(i:end), sp); if (isempty (si)) break; endif si = i + si(1) - 1; # absolute start of START match aEnd = si + numel (sp) - 1; ei = strfind (s((aEnd + 1):end), ep); if (isempty (ei)) break; endif ei = aEnd + ei(1); # absolute start of END match if (inclusive) m{end+1} = s(si:(ei + numel (ep) - 1)); elseif (aEnd + 1 <= ei - 1) m{end+1} = s((aEnd + 1):(ei - 1)); else m{end+1} = ''; endif i = ei + numel (ep); # resume past the END match endwhile endfunction ## The text of each element of the numeric array X, with the precision ## MATLAB gives the whole array REF: four more than the base-10 logarithm of ## its largest finite magnitude, rounded up, from 5 to 16 significant digits. function c = num2strs (x, ref) if (isinteger (x)) c = arrayfun (@__intstr__, x, "UniformOutput", false); return; elseif (iscomplex (x)) c = arrayfun (@num2str, x, "UniformOutput", false); return; endif x = double (x); x(x == 0) = 0; m = max (abs (double (ref(isfinite (ref))))); p = 5; if (! isempty (m) && m > 0) p = min (16, max (5, ceil (log10 (m)) + 4)); endif fmt = sprintf ("%%.%dg", p); c = arrayfun (@(v) sprintf (fmt, v), x, "UniformOutput", false); endfunction ## Return the name of the UTF-32 encoding in the byte order of this machine, so ## that typecasting its bytes to uint32 yields the Unicode code points. function enc = utf32 () persistent native = ''; if (isempty (native)) [~, ~, endian] = computer (); native = ['UTF-32', endian, 'E']; endif enc = native; endfunction ## Convert a character vector to/from a row of Unicode code points (uint32), so ## that character positions index whole characters rather than UTF-8 bytes. function cp = str2cp (s) cp = typecast (unicode2native (s, utf32 ()), 'uint32'); endfunction function s = cp2str (cp) if (isempty (cp)) s = ''; else s = native2unicode (typecast (uint32 (cp), 'uint8'), utf32 ()); endif endfunction ## Collect the non-overlapping matches of PATS in the character vector S, ## scanning left to right. At each position the patterns are tried in order and ## the first that matches is taken; the scan then resumes past that match. function m = extract_matches (s, pats) m = {}; i = 1; n = numel (s); while (i <= n) hit = 0; for p = 1:numel (pats) L = numel (pats{p}); if (L > 0 && i + L - 1 <= n && strncmp (s(i:i+L-1), pats{p}, L)) hit = L; m{end+1} = pats{p}; break; endif endfor if (hit > 0) i += hit; else i += 1; endif endwhile endfunction ## Replace, in the character vector S, every occurrence of the OLDS substrings ## with the corresponding NEWS substrings, in a single left to right pass. At ## each position the OLDS are tried in order and the first that matches is ## replaced; the scan resumes past the inserted NEWS text, so replacements are ## not re-scanned. Unmatched runs are flushed in bulk to avoid quadratic ## growth. function s = replace_pairs (s, olds, news) parts = {}; i = 1; n = numel (s); last = 1; # start of the current unmatched run while (i <= n) hit = 0; for p = 1:numel (olds) L = numel (olds{p}); if (L > 0 && i + L - 1 <= n && strncmp (s(i:i+L-1), olds{p}, L)) parts{end+1} = s(last:(i - 1)); parts{end+1} = news{p}; hit = L; break; endif endfor if (hit > 0) i += hit; last = i; else i += 1; endif endwhile parts{end+1} = s(last:end); s = [parts{:}]; endfunction ## Split the character vector S at the DLMS delimiters, scanning left to right. ## At each position the delimiters are tried in order and the first that matches ## is taken; the scan resumes past it. Delimiters are not collapsed. Returns ## the PIECES (a 1xK row) and the MATCHES between them (a 1x(K-1) row). function [pieces, matches] = split_one (s, dlms) pieces = {}; matches = {}; i = 1; n = numel (s); last = 1; # start of the current piece while (i <= n) hit = 0; for d = 1:numel (dlms) L = numel (dlms{d}); if (L > 0 && i + L - 1 <= n && strncmp (s(i:i+L-1), dlms{d}, L)) pieces{end+1} = s(last:(i - 1)); matches{end+1} = dlms{d}; i += L; last = i; hit = 1; break; endif endfor if (! hit) i += 1; endif endwhile pieces{end+1} = s(last:end); endfunction ## Lay out the per-element pieces (the columns of cell matrix PC, with logical ## missing mask PM) along dimension DIM of an array whose other dimensions match ## the input size SZ. N is the number of pieces. Returns the cellstr OC and ## logical missing mask OM of the resulting array. function [oc, om] = split_place (pc, pm, sz, dim, N) nd = max (numel (sz), dim); outsz = [sz, ones(1, nd - numel (sz))]; outsz(dim) = N; oc = cell (outsz); om = false (outsz); idx = repmat ({':'}, 1, nd); shp = outsz; shp(dim) = 1; for n = 1:N idx{dim} = n; oc(idx{:}) = reshape (pc(:,n), shp); om(idx{:}) = reshape (pm(:,n), shp); endfor endfunction ## Strip the leading (when SL is true) and trailing (when SR is true) characters ## of the character vector S whose code points are listed in STRIPCPS. Works in ## code-point space so a multibyte strip character is matched as a whole. function s = strip_one (s, sl, sr, stripcps) cp = str2cp (s); lo = 1; hi = numel (cp); if (sl) while (lo <= hi && any (cp(lo) == stripcps)) lo += 1; endwhile endif if (sr) while (hi >= lo && any (cp(hi) == stripcps)) hi -= 1; endwhile endif s = cp2str (cp(lo:hi)); endfunction ## Pad the character vector S to N code points with the PADCP code point, on the ## given SIDE ('left', 'right', or 'both'). S is returned unchanged when it is ## already at least N long. For 'both', an odd count puts the extra on the ## right. function s = pad_one (s, N, side, padcp) cp = str2cp (s); total = max (N, numel (cp)) - numel (cp); if (total <= 0) return; endif switch (side) case 'left' lpad = total; rpad = 0; case 'right' lpad = 0; rpad = total; otherwise lpad = floor (total / 2); rpad = total - lpad; endswitch s = cp2str ([repmat(padcp, 1, lpad), cp, repmat(padcp, 1, rpad)]); endfunction ## Broadcast a non-scalar delimiter (cell array DC0 with missing mask DM0) to ## the target size TSZ (the size of STR with N-1 elements along DIM). The delim ## must already have N-1 along DIM; other dimensions of size 1 are replicated to ## match. Returns the broadcast DC/DM and a logical OK (false on a size clash). function [Dc, Dm, ok] = join_bcast (dc0, dm0, tsz, dim) Dc = {}; Dm = []; ok = true; nd = numel (tsz); dsz = [size(dc0), ones(1, nd - ndims (dc0))]; if (dsz(dim) != tsz(dim)) ok = false; return; endif reps = ones (1, nd); for d = 1:nd if (d == dim) continue; elseif (dsz(d) == tsz(d)) ## already matches elseif (dsz(d) == 1) reps(d) = tsz(d); else ok = false; return; endif endfor Dc = repmat (dc0, reps); Dm = repmat (dm0, reps); endfunction function out = cmp_uint32 (Acode, Bcode) A_n = numel (Acode); B_n = numel (Bcode); len = min (A_n, B_n); A_d = double (Acode(1:len)); B_d = double (Bcode(1:len)); out = sign (A_d - B_d); out = out(find (out, 1)); if (isempty (out)) out = sign (A_n - B_n); endif endfunction function out = sign_strings (A, B) enc = utf32 (); fcn = @(x) typecast (unicode2native (x, enc), 'uint32'); Acode = cellfun (fcn, A, "UniformOutput", false); Acode(cellfun ('isempty', Acode)) = 0; Bcode = cellfun (fcn, B, "UniformOutput", false); Bcode(cellfun ('isempty', Bcode)) = 0; out = cellfun (@cmp_uint32, Acode, Bcode); endfunction ## Split a format specifier into an ordered list of literal and operator tokens. ## Each token is a scalar struct with the fields: ## 'type' : 'lit' for literal text or 'op' for a conversion operator ## 'text' : the corresponding piece of text from the format specifier ## 'nval' : number of values an operator consumes (1, plus 1 per '*'), or 0 ## The '%%' literal is collapsed to a single '%' and kept as a 'lit' token. function tok = compose_tokenize (fmt) pat = '%%|%[-+ #0]*(?:\d+|\*)?(?:\.(?:\d+|\*))?[diouxXeEfgGcs]'; [mt, sp] = regexp (fmt, pat, 'match', 'split'); tok = {}; for k = 1:numel (mt) if (! isempty (sp{k})) tok{end+1} = struct ('type', 'lit', 'text', sp{k}, 'nval', 0); endif if (strcmp (mt{k}, '%%')) tok{end+1} = struct ('type', 'lit', 'text', '%', 'nval', 0); else nstar = numel (strfind (mt{k}, '*')); tok{end+1} = struct ('type', 'op', 'text', mt{k}, 'nval', 1 + nstar); endif endfor if (! isempty (sp{end})) tok{end+1} = struct ('type', 'lit', 'text', sp{end}, 'nval', 0); endif endfunction ## Format a single string from a token list and a cell of input values. Each ## operator consumes its values in order; operators left without a ## corresponding value are emitted unchanged. Literal text has its escape ## sequences translated following the same rules as the 'sprintf' function. function out = compose_apply (tok, vals) parts = cell (1, numel (tok)); vi = 1; nv = numel (vals); for t = 1:numel (tok) if (strcmp (tok{t}.type, 'op') && vi + tok{t}.nval - 1 <= nv) args = vals(vi:vi + tok{t}.nval - 1); parts{t} = sprintf (tok{t}.text, args{:}); vi += tok{t}.nval; elseif (strcmp (tok{t}.type, 'op')) parts{t} = tok{t}.text; elseif (strcmp (tok{t}.text, '%')) parts{t} = '%'; else parts{t} = do_string_escapes (tok{t}.text); endif endfor out = ['', parts{:}]; endfunction ## 'pkg test' reads the BISTs from the file it is testing, so a class ## whose suite lives in inst/tests/ reads as carrying none. This block ## answers for it and checks nothing itself. %!test %! ## The suite for this class is inst/tests/string.m-tst. pr0m1th3as-datatypes-9450b29/inst/strings.m000066400000000000000000000121461527370255000205260ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{str} =} strings ## @deftypefnx {datatypes} {@var{str} =} strings (@var{n}) ## @deftypefnx {datatypes} {@var{str} =} strings (@var{sz1}, @dots{}, @var{szN}) ## @deftypefnx {datatypes} {@var{str} =} strings (@var{sz}) ## ## Create a string array of strings with no characters. ## ## @code{@var{str} = strings} returns a string scalar with no characters, ## @qcode{""}. ## ## @code{@var{str} = strings (@var{n})} returns an @math{N*N} string array ## whose every element has no characters. ## ## @code{@var{str} = strings (@var{sz1}, @dots{}, @var{szN})} returns a string ## array of size @var{sz1}-by-@dots{}-by-@var{szN}, and ## @code{@var{str} = strings (@var{sz})} takes the same sizes as the row vector ## @var{sz}. ## ## A size may be numeric or logical, and must be a real, finite integer. A ## negative size counts as zero, as does an empty size argument, and an empty ## size vector gives a @math{0*0} array. ## ## The elements are strings with no characters, not missing strings, so ## @code{ismissing} is false and @code{strlength} is zero for every one of ## them. @code{string (cell (@var{sz}))} gives missing strings instead. ## ## @seealso{string, string.empty} ## @end deftypefn function str = strings (varargin) if (nargin == 0) sz = [1, 1]; elseif (nargin == 1) sz = varargin{1}; errmsg = size_errmsg (sz); if (! isempty (errmsg)) error ("strings: %s", errmsg); endif if (isempty (sz)) sz = [0, 0]; elseif (isscalar (sz)) sz = [sz, sz]; elseif (! isrow (sz)) error ("strings: size vector must be a row vector."); endif else for i = 1:nargin errmsg = size_errmsg (varargin{i}); if (! isempty (errmsg)) error ("strings: %s", errmsg); endif if (numel (varargin{i}) > 1) error (strcat ("strings: each size must be a scalar when more than", ... " one is given.")); endif endfor ## An empty size argument counts as zero, as in MATLAB sz = cellfun (@(x) sum (double (x)), varargin); endif sz = max (double (sz), 0); str = string (repmat ({''}, sz)); endfunction ## The problem with the size argument SZ, or empty when there is none. function errmsg = size_errmsg (sz) errmsg = ''; if (! (isnumeric (sz) || islogical (sz))) errmsg = "size must be numeric or logical."; elseif (! isreal (sz)) errmsg = "size must be real."; elseif (! all (isfinite (sz(:)))) errmsg = "size must be finite."; elseif (any (sz(:) != fix (sz(:)))) errmsg = "size must be an integer."; endif endfunction %!test %! str = strings; %! assert_equal (size (str), [1, 1]); %! assert_equal (ismissing (str), false); %! assert_equal (strlength (str), 0); %!assert_equal (size (strings (3)), [3, 3]) %!assert_equal (size (strings (2, 3)), [2, 3]) %!assert_equal (size (strings ([2, 3])), [2, 3]) %!assert_equal (size (strings (2, 3, 4)), [2, 3, 4]) %!assert_equal (size (strings ([2, 3, 4])), [2, 3, 4]) %!assert_equal (size (strings (2, 3, 1, 1)), [2, 3]) %!assert_equal (size (strings ([2, 3, 0])), [2, 3, 0]) %!assert_equal (size (strings (0)), [0, 0]) %!assert_equal (size (strings (0, 3)), [0, 3]) %!assert_equal (size (strings (-1)), [0, 0]) %!assert_equal (size (strings (2, -1)), [2, 0]) %!assert_equal (size (strings ([])), [0, 0]) %!assert_equal (size (strings (zeros (1, 0))), [0, 0]) %!assert_equal (size (strings (2, [])), [2, 0]) %!assert_equal (size (strings (int8 (2))), [2, 2]) %!assert_equal (size (strings (uint64 (2), 3)), [2, 3]) %!assert_equal (size (strings (true)), [1, 1]) %!assert_equal (class (strings (2)), 'string') %!assert_equal (ismissing (strings (2, 3)), false (2, 3)) %!assert_equal (strlength (strings (2, 3)), zeros (2, 3)) %!assert_equal (cellstr (strings (1, 2)), {'', ''}) %!error strings (2.5) %!error strings ('2') %!error strings (string ('2')) %!error strings (2, 'like', 'a') %!error strings (NaN) %!error strings (Inf) %!error strings (1 + 2i) %!error strings ([2; 3]) %!error ... %! strings (1, [2, 3]) pr0m1th3as-datatypes-9450b29/inst/struct2ods.m000066400000000000000000000270121527370255000211470ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {} struct2ods (@var{filename}, @var{s}) ## ## Write a scalar structure of tables to a multi-sheet OpenDocument spreadsheet. ## ## @code{struct2ods (@var{filename}, @var{s})} writes each field of the scalar ## structure @var{s} to its own sheet in the OpenDocument spreadsheet named by ## @var{filename}. Every field of @var{s} must hold a @code{table} or a ## @code{timetable}; the field name becomes the sheet name. A timetable's row ## times lead its sheet and are tagged as row times, so @code{ods2struct} ## returns a timetable for that sheet and a table for the others. A timetable ## carrying an event table gets a second sheet holding it, named ## @qcode{_Events}, and a @qcode{## Events crossref:} line above the ## first sheet marker of the hidden sheet tying the two together and carrying ## the event table's three variable designations. Both the ## compressed @qcode{.ods} and the flat @qcode{.fods} formats are supported, ## selected by the file extension. ## ## Full type fidelity is preserved through a hidden, sectioned ## @qcode{__datatypes_meta__} sheet, exactly as for the single-table ## @code{table2ods} method; the workbook round-trips through @code{ods2struct}. ## ## A field whose table carries an @qcode{'ActualSheetName'} custom property ## (see @code{addprop}) uses that value as the sheet name instead of the field ## name, which lets a sheet name that is not a valid identifier (for example ## @qcode{'Sales 2024'}) round-trip. Sheet names must be non-empty and must not ## contain any of the characters @qcode{[ ] * ? : \ /}, and the ## resolved names must be unique. ## ## @seealso{ods2struct, table.table2ods, ods2table, table.writetable} ## @end deftypefn function struct2ods (filename, s) if (nargin != 2) print_usage (); endif if (! ((ischar (filename) && isvector (filename)) ... || iscellstr (filename) || isa (filename, 'string'))) error (strcat ("struct2ods: FILENAME must be a character vector,", ... " cellstr, or string.")); endif file = char (cellstr (filename)); [~, ~, ext] = fileparts (file); if (strcmpi (ext, '.fods')) is_flat = true; elseif (strcmpi (ext, '.ods')) is_flat = false; else error (strcat ("struct2ods: FILENAME must have a '.ods' or '.fods'", ... " extension.")); endif if (! (isstruct (s) && isscalar (s))) error ("struct2ods: S must be a scalar structure."); endif fields = fieldnames (s); if (isempty (fields)) error ("struct2ods: S must have at least one field."); endif K = numel (fields); names = cell (1, K); datas = cell (1, K); vtypes = cell (1, K); headers = cell (1, K); metablocks = cell (1, K); for k = 1:K T = s.(fields{k}); if (! isa (T, 'tabular')) error (strcat ("struct2ods: field '%s' is not a table or a", ... " timetable."), fields{k}); endif ## Resolve the sheet name: an 'ActualSheetName' custom property wins over ## the field name, so non-identifier sheet names can round-trip. sheetName = fields{k}; cp = T.Properties.CustomProperties; if (isfield (cp, 'ActualSheetName') && ! isempty (cp.ActualSheetName)) sheetName = char (cp.ActualSheetName); endif if (! (ischar (sheetName) && isrow (sheetName) && ! isempty (sheetName))) error (strcat ("struct2ods: sheet name for field '%s' must be a", ... " non-empty character vector."), fields{k}); endif if (any (ismember (sheetName, '[]*?:/\'))) error (strcat ("struct2ods: sheet name '%s' contains an invalid", ... " character ([ ] * ? : / \\\\)."), sheetName); endif if (any (strcmp (sheetName, names(1:k-1)))) error ("struct2ods: duplicate sheet name '%s'.", sheetName); endif names{k} = sheetName; [datas{k}, vtypes{k}, metablocks{k}, headers{k}] = ... __ods_parts__ (T, 'struct2ods'); endfor ## A timetable carrying events gets a second sheet of its own holding the ## event table, and a line in the preamble tying the two together. An event ## table cannot carry one, so nothing here recurses. xfrom = {}; xto = {}; xlbl = {}; xlen = {}; xend = {}; for k = 1:K T = s.(fields{k}); if (! (isa (T, 'timetable') && ! isa (T, 'eventtable'))) continue; endif ev = T.Properties.Events; if (! isa (ev, 'eventtable')) continue; endif evName = [names{k} '_Events']; j = 1; while (any (strcmp (evName, names))) evName = sprintf ('%s_Events_%d', names{k}, j); j += 1; endwhile names{end+1} = evName; [datas{end+1}, vtypes{end+1}, metablocks{end+1}, headers{end+1}] = ... __ods_parts__ (ev, 'struct2ods'); ## The three designations are the event table's own metadata and no ## sheet carries them, so they ride on the cross-reference beside the ## two sheet names. P = ev.Properties; xfrom{end+1} = names{k}; xto{end+1} = evName; xlbl{end+1} = char_or_blank (P.EventLabelsVariable); xlen{end+1} = char_or_blank (P.EventLengthsVariable); xend{end+1} = char_or_blank (P.EventEndsVariable); endfor K = numel (names); ## Assemble the sectioned metadata grid: each table's metadata block preceded ## by a "## Sheet: " marker row, all padded to a common width. sections = cell (1, K); maxcols = 0; for k = 1:K mb = metablocks{k}; marker = [{['## Sheet: ' names{k}]}, repmat({''}, 1, max (0, ... columns (mb) - 1))]; sections{k} = [marker; mb]; maxcols = max (maxcols, columns (sections{k})); endfor ## The cross-references lead the grid, above the first marker, where they ## belong to no sheet and no reader that predates them ever meets them: a ## section runs from its marker to the next, so a line placed at the end ## would be read as part of the last sheet's block. The two names go in ## cells of their own because a sheet name may contain spaces. maxcols = max (maxcols, 6 * (numel (xfrom) > 0)); metagrid = cell (0, maxcols); for x = 1:numel (xfrom) row = [{'## Events crossref:'}, xfrom(x), xto(x), xlbl(x), xlen(x), ... xend(x), repmat({''}, 1, max (0, maxcols - 6))]; metagrid = [metagrid; row]; endfor for k = 1:K sec = sections{k}; if (columns (sec) < maxcols) sec = [sec, repmat({''}, rows (sec), maxcols - columns (sec))]; endif metagrid = [metagrid; sec]; endfor opts = struct (); opts.sheets = struct ('name', names, 'data', datas, 'vtype', vtypes, ... 'header', headers); opts.meta = metagrid; msg = __table2ods__ (file, {}, {}, {}, is_flat, opts); if (! isequal (msg, 0)) error ("struct2ods: %s", msg); endif endfunction ## A variable designation as text, blank when it is unset. function out = char_or_blank (v) out = ''; if (! isempty (v)) out = char (v); endif endfunction %!demo %! ## `struct2ods` writes a whole workbook at once: each field of a scalar %! ## struct of tables becomes its own sheet, and the field name becomes the %! ## sheet name. %! %! wb.Patients = table ({'Li'; 'Diaz'}, [38; 40], 'VariableNames', {'Name', ... %! 'Age'}); %! wb.Visits = table ([1; 2; 3], 'VariableNames', {'Visit'}); %! filename = fullfile (tempdir (), 'clinic.ods'); %! struct2ods (filename, wb); %! %! ## The two fields are now two sheets, recoverable with `ods2struct`. %! fieldnames (ods2struct (filename)) %! %! delete (filename); %!test # round-trip a two-table workbook through ods2struct %! s.alpha = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); %! s.beta = table ([10.5; 20.5], 'VariableNames', {'v'}); %! fn = [tempname() '.ods']; %! unwind_protect %! struct2ods (fn, s); %! r = ods2struct (fn); %! assert_equal (sort (fieldnames (r)), {'alpha'; 'beta'}); %! assert_equal (r.alpha.x, [1; 2; 3]); %! assert_equal (r.alpha.y, {'a'; 'b'; 'c'}); %! assert_equal (r.beta.v, [10.5; 20.5]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!test # native types (datetime, duration, int) round-trip per sheet %! s.one = table (datetime (2024, 1, [1; 2]), int32 ([5; 6]), ... %! 'VariableNames', {'d', 'n'}); %! s.two = table (seconds ([30; 90; 120]), 'VariableNames', {'t'}); %! fn = [tempname() '.fods']; %! unwind_protect %! struct2ods (fn, s); %! r = ods2struct (fn); %! assert_equal (class (r.one.d), 'datetime'); %! assert_equal (class (r.one.n), 'int32'); %! assert_equal (r.one.n, int32 ([5; 6])); %! assert_equal (class (r.two.t), 'duration'); %! assert_equal (isequaln (seconds (r.two.t), [30; 90; 120]), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!test # a non-identifier sheet name round-trips via ActualSheetName %! T = table ([1; 2], 'VariableNames', {'v'}); %! T = addprop (T, 'ActualSheetName', 'table'); %! T.Properties.CustomProperties.ActualSheetName = 'Sales 2024'; %! s.sheet1 = T; %! fn = [tempname() '.ods']; %! unwind_protect %! struct2ods (fn, s); %! names = __ods2table__ (fn); # smoke: file is readable %! r = ods2struct (fn); %! fn2 = fieldnames (r); %! assert_equal (numel (fn2), 1); %! cp = r.(fn2{1}).Properties.CustomProperties; %! assert_equal (cp.ActualSheetName, 'Sales 2024'); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!error ... %! struct2ods ('bad.txt', struct ('a', table (1))) %!error ... %! struct2ods ([tempname() '.ods'], struct ('a', {table(1), table(2)})) %!error ... %! struct2ods ([tempname() '.ods'], struct ('a', table (inputParser ()))) %!error ... %! struct2ods ([tempname() '.ods'], struct ('a', table (1), 'b', 5)) %!error ... %! struct2ods ([tempname() '.ods'], struct ()) %!error ... %! struct2ods (42, struct ('a', table (1))) %!error ... %! T = addprop (table (1), 'ActualSheetName', 'table'); %! T.Properties.CustomProperties.ActualSheetName = ['A'; 'B']; %! struct2ods ([tempname() '.ods'], struct ('a', T)); %!error ... %! T = addprop (table (1), 'ActualSheetName', 'table'); %! T.Properties.CustomProperties.ActualSheetName = 'a*b'; %! struct2ods ([tempname() '.ods'], struct ('a', T)); %!error ... %! T = addprop (table (1), 'ActualSheetName', 'table'); %! T.Properties.CustomProperties.ActualSheetName = 'S'; %! struct2ods ([tempname() '.ods'], struct ('a', T, 'b', T)); pr0m1th3as-datatypes-9450b29/inst/struct2table.m000066400000000000000000000210541527370255000214510ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{tbl} =} struct2table (@var{S}) ## @deftypefnx {datatypes} {@var{tbl} =} struct2table (@var{S}, @var{Name}, @var{Value}) ## ## Convert a structure array to a table. ## ## @code{@var{tbl} = struct2table (@var{S})} converts a structure array @var{S} ## to the table @var{tbl}, where each field of the input structure becomes a ## variable in the output table. For a scalar structure with @math{N} fields, ## all of which have @math{M} rows, or an @math{M*1} or @math{1*M} structure ## array with @math{N} fields, the output is an @math{M*N} table. A field of ## function handles, @code{dictionary}, @code{timeseries} or @code{tscollection} ## objects becomes a cell variable, since no other variable can hold one. ## ## @code{@var{tbl} = struct2table (@var{S}, @var{Name}, @var{Value})} specifies ## optional parameters for creating the table @var{tbl} with the following ## Name-Value paired arguments. ## ## @multitable @columnfractions 0.23 0.75 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'AsArray'} @tab A logical scalar specifying whether to ## treat a scalar input as a structure array, which allows the fields containing ## data of different sizes. ## ## @item @qcode{'RowNames'} @tab A cell array of character vectors or ## a string array defining the row names of @var{tbl}. The names must be unique ## but not necessarily valid variable names. ## ## @item @qcode{'DimensionNames'} @tab A cell array of character vectors or ## a string array defining the dimension names of @var{tbl}. The names must be ## unique and not in conflict with variable names. By default, dimension names ## are @qcode{'Row', 'Variables'}. ## @end multitable ## ## @seealso{array2table, cell2table, table} ## @end deftypefn function tbl = struct2table (S, varargin) ## Check input is a struct scalar of vector if (! isstruct (S)) error ("struct2table: input array must be a structure."); endif if (mod (numel (varargin), 2) != 0) error ("struct2table: name-value arguments must be in pairs."); endif ## Handle default value for AsArray if (isscalar (S)) default = false; else default = true; endif ## Parse optional paired arguments; 'AsArray' defaults to true for a ## structure array and to false for a scalar structure optNames = {'AsArray', 'RowNames', 'DimensionNames'}; dfValues = {default, {}, {"Row", "Variables"}}; [AsArray, rowNames, dimNames, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error ("struct2table: invalid optional paired argument."); endif ## Get variable names from structure fields varNames = fieldnames (S); optArgs = {'VariableNames', varNames}; ## Get variable data from structure varN = numel (varNames); varValues = cell (varN, 1); if (AsArray) for i = 1:varN try ## A field of what only a cell can hold becomes a cell variable if (any (cellfun (@cellOnly, {S(:).(varNames{i})}))) error (); endif varValues{i} = cat (1, S(:).(varNames{i})); catch varValues{i} = cat (1, {S(:).(varNames{i})})'; end_try_catch endfor nrows = cellfun (@rows, varValues); if (! all (ismember (nrows, numel (S)))) for i = 1:varN varValues{i} = cat (1, {S(:).(varNames{i})})'; endfor endif else vals = struct2cell (S); for i = 1:varN try if (any (cellfun (@cellOnly, vals(i,1,:)))) error (); endif varValues{i} = cat (1, vals{i,1,:}); catch varValues{i} = cat (1, vals(i,1,:)); end_try_catch endfor nrows = cellfun (@rows, varValues); if (! all (ismember (nrows, nrows(1)))) error ("struct2table: fields have different rows. Use 'AsArray' option."); endif endif ## Handle remaining paired arguments if (! isempty (rowNames)) if (numel (rowNames) != nrows) error (strcat ("struct2table: 'RowNames' must match the rows", ... " in input structure.")); endif optArgs = [optArgs {'RowNames', rowNames}]; endif if (! isempty (dimNames)) if (numel (dimNames) != 2) error ("struct2table: 'DimensionNames' must be a two-element vector."); endif optArgs = [optArgs {'DimensionNames', dimNames}]; endif ## Construct table tbl = table (varValues{:}, optArgs{:}); endfunction ## True for a value only a cell variable can hold function tf = cellOnly (x) tf = (is_function_handle (x) || isa (x, 'dictionary') || isa (x, 'timeseries') || isa (x, 'tscollection')); endfunction %!demo %! ## `struct2table` maps a struct to a table. A struct *array* becomes one row %! ## per element; a *scalar* struct whose fields are equal-length columns %! ## becomes one row per element of those columns. Both give the same table %! ## here. %! %! S = struct ('Name', {'Li'; 'Diaz'; 'Brown'}, 'Age', {38; 40; 49}); %! struct2table (S) %!demo %! ## When a scalar struct holds fields of *unequal* length, use `'AsArray', %! ## true` to wrap the whole struct as a single table row, each field one %! ## variable. %! %! S.Values = [1, 2, 3]; %! S.Label = 'demo'; %! struct2table (S, 'AsArray', true) %!test %! S(1).A = [1, 2]; %! S(2).A = [3, 4]; %! S(1).B = 5; %! S(2).B = 6; %! tbl = struct2table (S); %! assert_equal (tbl.A, [1, 2; 3, 4]); %! assert_equal (tbl.B, [5; 6]); %! assert_equal (numel (S), rows (tbl)); %!test %! S(1).A = [1, 2]; %! S(2).A = [3, 4, 5, 6]; %! S(1).B = 7; %! S(2).B = 8; %! tbl = struct2table (S); %! assert_equal (isa (tbl.A, 'cell'), true); %! assert_equal (isa (tbl.B, 'double'), true); %! assert_equal (numel (S), rows (tbl)); %! assert_equal (tbl.A(1){1}, [1, 2]); %! assert_equal (tbl.A(2){1}, [3, 4, 5, 6]); %! assert_equal (tbl.B, [7; 8]); %!test %! S.A = [1; 2; 3]; %! S.B = [4, 5; 6, 7; 8, 9]; %! tbl = struct2table (S); %! assert_equal (isa (tbl.A, 'double'), true); %! assert_equal (isa (tbl.B, 'double'), true); %! assert_equal (tbl.A, [1; 2; 3]); %! assert_equal (tbl.B, [4, 5; 6, 7; 8, 9]); %!test %! tbl = struct2table (struct ('A', 1, 'B', [1; 2]), 'AsArray', true); %! assert_equal (iscell (tbl.A), true); %! assert_equal (iscell (tbl.B), true); %! assert_equal (size (tbl.B{1}), [2, 1]); %!test # a field of function handles becomes a cell variable %! tbl = struct2table (struct ('a', @sin, 'b', 1)); %! assert_equal (class (tbl.a), 'cell'); %! assert_equal (func2str (tbl.a{1}), 'sin'); %!test %! tbl = struct2table (struct ('a', {@sin; @cos})); %! assert_equal (class (tbl.a), 'cell'); %! assert_equal (func2str (tbl.a{2}), 'cos'); %!test # a field of time series becomes a cell variable, never one series %! tbl = struct2table (struct ('t', {timeseries([1; 2]); timeseries([3; 4])})); %! assert_equal (class (tbl.t), 'cell'); %! assert_equal (size (tbl.t), [2, 1]); %! assert_equal (tbl.t{2}.Data, [3; 4]); %!test %! tbl = struct2table (struct ('t', timeseries (5))); %! assert_equal (class (tbl.t), 'cell'); %!test %! tbl = struct2table (struct ('d', dictionary (1, 2))); %! assert_equal (class (tbl.d), 'cell'); %!test %! tbl = struct2table (struct ('c', tscollection ([0; 1]))); %! assert_equal (class (tbl.c), 'cell'); %!error struct2table ({1}); %!error ... %! struct2table (struct ('a', 1), 'AsArray') %!error ... %! struct2table (struct ('a', 1), 'Bogus', 1) %!error ... %! struct2table (struct ('A', 1, 'B', [1; 2])); %!error ... %! struct2table (struct ('A', {'a';, 'b'}, 'B', [1; 2]), 'RowNames', 'q'); %!error ... %! struct2table (struct ('A', {'a';, 'b'}, 'B', [1; 2]), 'DimensionNames', 'q'); pr0m1th3as-datatypes-9450b29/inst/struct2xlsx.m000066400000000000000000000176561527370255000213750ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {} struct2xlsx (@var{filename}, @var{s}) ## ## Write a scalar structure of tables to a multi-sheet Excel workbook. ## ## @code{struct2xlsx (@var{filename}, @var{s})} writes each field of the scalar ## structure @var{s} to its own sheet in the Office Open XML workbook named by ## @var{filename} (@qcode{.xlsx} or @qcode{.xlsm}). Every field must hold a ## @code{table}; the field name becomes the sheet name. ## ## This is the Excel counterpart of @code{struct2ods}. Like @code{writetable}, ## it writes the MATLAB-interoperable format (a variable-name header row ## followed by the data, with no hidden type metadata); read it back with ## @code{xlsx2struct}. A field whose table carries an @qcode{'ActualSheetName'} ## custom property uses that value as the sheet name instead of the field name. ## Sheet names must be non-empty, at most 31 characters, and must not contain ## any of the characters @qcode{[ ] * ? : \ /}; the resolved ## names must be unique. ## ## @seealso{xlsx2struct, struct2ods, table.writetable, readtable} ## @end deftypefn function struct2xlsx (filename, s) if (nargin != 2) print_usage (); endif if (! ((ischar (filename) && isvector (filename)) ... || iscellstr (filename) || isa (filename, 'string'))) error (strcat ("struct2xlsx: FILENAME must be a character vector,", ... " cellstr, or string.")); endif file = char (cellstr (filename)); [~, ~, ext] = fileparts (file); if (! any (strcmpi (ext, {'.xlsx', '.xlsm'}))) error ("struct2xlsx: FILENAME must have a '.xlsx' or '.xlsm' extension."); endif if (! (isstruct (s) && isscalar (s))) error ("struct2xlsx: S must be a scalar structure."); endif fields = fieldnames (s); if (isempty (fields)) error ("struct2xlsx: S must have at least one field."); endif K = numel (fields); names = cell (1, K); datas = cell (1, K); vtypes = cell (1, K); headers = cell (1, K); for k = 1:K T = s.(fields{k}); if (! isa (T, 'table')) error ("struct2xlsx: field '%s' is not a table.", fields{k}); endif sheetName = fields{k}; cp = T.Properties.CustomProperties; if (isfield (cp, 'ActualSheetName') && ! isempty (cp.ActualSheetName)) sheetName = char (cp.ActualSheetName); endif if (! (ischar (sheetName) && isrow (sheetName) && ! isempty (sheetName))) error (strcat ("struct2xlsx: sheet name for field '%s' must be a", ... " non-empty character vector."), fields{k}); endif if (numel (sheetName) > 31) error (strcat ("struct2xlsx: sheet name '%s' exceeds the", ... " 31-character", ... " Excel limit."), sheetName); endif if (any (ismember (sheetName, '[]*?:/\'))) error (strcat ("struct2xlsx: sheet name '%s' contains an invalid", ... " character ([ ] * ? : / \\\\)."), sheetName); endif if (any (strcmp (sheetName, names(1:k-1)))) error ("struct2xlsx: duplicate sheet name '%s'.", sheetName); endif names{k} = sheetName; [headers{k}, datas{k}, vtypes{k}] = __interop_parts__ (T, 'struct2xlsx'); endfor opts = struct (); opts.sheets = struct ('name', names, 'data', datas, 'vtype', vtypes, ... 'header', headers); opts.macro = strcmpi (ext, '.xlsm'); msg = __table2xlsx__ (file, {}, {}, opts); if (! isequal (msg, 0)) error ("struct2xlsx: %s", msg); endif endfunction %!demo %! ## `struct2xlsx` is the Excel counterpart of `struct2ods`: each field of a %! ## scalar struct of tables is written as its own worksheet in an `.xlsx` %! ## file. %! %! wb.Patients = table ({'Li'; 'Diaz'}, [38; 40], 'VariableNames', {'Name', ... %! 'Age'}); %! wb.Visits = table ([1; 2; 3], 'VariableNames', {'Visit'}); %! filename = fullfile (tempdir (), 'clinic.xlsx'); %! struct2xlsx (filename, wb); %! %! ## Read it back with `xlsx2struct` to recover the same field names. %! fieldnames (xlsx2struct (filename)) %! %! delete (filename); %!test # round-trip a two-table workbook through xlsx2struct %! s.alpha = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); %! s.beta = table ([10.5; 20.5], 'VariableNames', {'v'}); %! fn = [tempname() '.xlsx']; %! unwind_protect %! struct2xlsx (fn, s); %! r = xlsx2struct (fn); %! assert_equal (fieldnames (r), {'alpha'; 'beta'}); %! assert_equal (r.alpha.x, [1; 2; 3]); %! assert_equal (r.alpha.y, {'a'; 'b'; 'c'}); %! assert_equal (r.beta.v, [10.5; 20.5]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!test # native datetime/duration round-trip per sheet %! s.one = table (datetime (2024, 1, [1; 2]), 'VariableNames', {'d'}); %! s.two = table (seconds ([30; 90]), 'VariableNames', {'t'}); %! fn = [tempname() '.xlsx']; %! unwind_protect %! struct2xlsx (fn, s); %! r = xlsx2struct (fn); %! assert_equal (class (r.one.d), 'datetime'); %! assert_equal (isequaln (datevec (r.one.d), datevec (s.one.d)), true); %! assert_equal (class (r.two.t), 'duration'); %! assert_equal (isequaln (seconds (r.two.t), [30; 90]), true); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!error ... %! struct2xlsx ('bad.ods', struct ('a', table (1))) %!error ... %! struct2xlsx ([tempname() '.xlsx'], struct ('a', {table(1), table(2)})) %!error ... %! struct2xlsx ([tempname() '.xlsx'], struct ('a', table (inputParser ()))) %!error ... %! struct2xlsx ([tempname() '.xlsx'], struct ('a', table (1), 'b', 5)) %!error ... %! struct2xlsx (42, struct ('a', table (1))) %!error ... %! struct2xlsx ([tempname() '.xlsx'], struct ()) %!error ... %! T = addprop (table (1), 'ActualSheetName', 'table'); %! T.Properties.CustomProperties.ActualSheetName = ['A'; 'B']; %! struct2xlsx ([tempname() '.xlsx'], struct ('a', T)); %!error ... %! T = addprop (table (1), 'ActualSheetName', 'table'); %! T.Properties.CustomProperties.ActualSheetName = repmat ('x', 1, 32); %! struct2xlsx ([tempname() '.xlsx'], struct ('a', T)); %!error ... %! T = addprop (table (1), 'ActualSheetName', 'table'); %! T.Properties.CustomProperties.ActualSheetName = 'a*b'; %! struct2xlsx ([tempname() '.xlsx'], struct ('a', T)); %!error ... %! T = addprop (table (1), 'ActualSheetName', 'table'); %! T.Properties.CustomProperties.ActualSheetName = 'S'; %! struct2xlsx ([tempname() '.xlsx'], struct ('a', T, 'b', T)); %!error ... %! T = table ([1; 2], 'VariableNames', {'a'}); %! T.nest = table ([3; 4], [5; 6], 'VariableNames', {'p', 'q'}); %! struct2xlsx ([tempname() '.xlsx'], struct ('a', T)); pr0m1th3as-datatypes-9450b29/inst/table.m000066400000000000000000006777451527370255000201520ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef table < tabular ## -*- texinfo -*- ## @deftp {datatypes} table ## ## Array of tabular data containing multiple columnar variables. ## ## A table is a 2-dimensional data structure that collects heterogeneous data ## and metadata into a single container. Tables are suitable for storing ## columnar data much like spreadsheets but they can also be used for storing ## more complex data including multicolumnar variables and nested tables. ## ## Tables can be subscripted using parentheses like ordinary numeric arrays, ## but in addition to indexing with numeric and logical vectors, you can also ## use the table's variable or row names much like indexing a structure field ## as well as using a @qcode{vartype} class object to make a selection of ## variable types. While these methods will return a subset of the original ## table, you can also use curly brackets much like cell arrays to retrieve ## the contents of the table. In this case, the original data types of the ## selected variables are returned. ## ## An empty numeric variable subscript selects no variables, so ## @code{@var{tbl}(:,[])} is a table of the same height with nothing in ## it. An empty cell is not a subscript and is refused, as it is on any ## other array. ## ## Assigning an empty matrix to a subscripted table deletes rows or ## variables. @code{@var{tbl}(@var{rows},:) = []} removes the referenced ## rows, @code{@var{tbl}(:,@var{vars}) = []} removes the referenced ## variables, and @code{@var{tbl}.@var{varname} = []} removes a single ## variable by name. One of the two subscripts must be a colon, and when ## both of them are, the rows are removed. A table that has lost its last ## variable keeps its height, so a five-row table displays as ## @qcode{5x0} rather than as empty. ## ## Assigning to a subscripted table with parentheses takes another table ## or a cell array with one element per row and variable assigned. A ## variable name the table does not carry, or the index after its last ## variable, creates a new variable, named @qcode{Var@var{N}} when it is ## given by index; an index that would leave a gap is refused. Rows and ## variables the assignment adds but does not fill take the default value ## of their type, with a warning. A table with neither rows nor variables ## takes the height of what is assigned to it. ## ## Assigning with curly brackets writes the contents of the variables, as ## reading with them returns them: an array whose columns are those of the ## variables assigned, side by side, and whose rows are the rows assigned, ## or a scalar that fills them all. A single new variable also takes one ## row as every one of its rows. Each variable converts what it is given ## as its own type does, so a cell variable takes a cell array and a ## @qcode{double} variable reads text as numbers. Variables and rows are ## created as with parentheses, a table with neither rows nor variables ## takes one variable for each column, and assigning an empty matrix is ## refused, since curly brackets never delete. ## ## Assigning to @code{@var{tbl}.Variables}, the second dimension name, ## writes every variable as curly brackets over the whole table do, but ## takes only a value with one row for each row of the table. ## ## Besides the @code{table} constructor, you can also use @code{array2table}, ## @code{cell2table}, and @code{struct2table} to create tables from the ## respective data types. ## ## Besides all numeric data types, other supported data types that can be ## stored in a table array are @qcode{logical}, @qcode{categorical}, ## @qcode{cell}, (including @qcode{cellstr}), @qcode{calendarDuration}, ## @qcode{duration}, @qcode{datetime}, @qcode{string}, and @qcode{struct} ## arrays, as well as @qcode{table} itself. ## ## A variable cannot be a function handle, a @code{dictionary}, a ## @code{timeseries} or a @code{tscollection}; only a cell variable can ## hold one. Every way of creating or replacing a variable refuses them. ## For a function handle, the conversions @code{cell2table} and ## @code{struct2table}, as well as assignment of a cell array with ## parentheses, give a cell variable instead. MATLAB refuses a function ## handle or a @code{dictionary} only in the constructors and ## @code{addvars}, and lets @code{@var{tbl}.@var{varname}} assignment, ## @code{struct2table} and @code{convertvars} create one; it accepts a ## @code{timeseries} or a @code{tscollection} everywhere. ## ## @seealso{vartype, array2table, cell2table, struct2table} ## @end deftp properties ## -*- texinfo -*- ## @deftp {table} {property} RowNames ## ## Row names ## ## Row names, specified as a cell array of character vectors or a string ## array. If specified as a string array, it is converted and stored ## internally as a cell array of character vectors. If not empty (default), ## it must contain the same number of elements as the number of rows in the ## table. All elements must be nonempty and distinct. You can access the ## rows of the table @var{tbl} by specifying one or more row names within ## parentheses or curly braces. You can also set @qcode{RowNames} by ## dot name assignment to an existing variable. ## ## @end deftp RowNames = {} endproperties ################################################################################ ## ** Subclass hooks ** ## ################################################################################ ## ## ## The eleven hooks 'tabular' declares, implemented for a table, whose rows ## ## are labelled by 'RowNames' and may carry no labels at all. ## ## ## ################################################################################ methods (Access = protected) ## True when this table has 'RowNames'. They are optional, and a table ## created without them keeps the empty cell array the property defaults ## to, so emptiness is the whole test. function tf = hasRowLabels (this) tf = ! isempty (this.RowNames); endfunction ## The 'RowNames' cell array exactly as stored: a column cellstr of ## unique, nonempty names, one per row, or empty when the table has none. function out = getRowLabels (this) out = this.RowNames; endfunction ## The constant 'RowNames', which is the property name. It is not the ## row dimension name: 'DimensionNames{1}' also reaches the labels, but it ## defaults to 'Row' and the user may rename it. function out = rowLabelName (this) out = 'RowNames'; endfunction ## The 'RowNames' unchanged. A table's labels are already character data, ## so displaying and exporting them needs no rendering step and this hook ## has nothing to do beyond what 'getRowLabels' does. function out = rowLabelStrings (this) out = this.RowNames; endfunction ## The one property a table publishes about its row labels, under the ## name it declares them with. It is also what 'rowLabelName' answers ## here, but the two are not the same question and a timetable answers ## them differently. function out = rowLabelProperties (this) out = struct (); out.RowNames = this.RowNames; endfunction ## Nothing. A table's row names are printed with no heading over them ## and no rule under them, not even the row dimension name, which may be ## renamed without changing the display at all. function out = rowLabelHeader (this) out = ''; endfunction ## A table orders by its row names under either spelling. function out = rowLabelKeyNames (this) out = {'RowNames', this.DimensionNames{1}}; endfunction ## Row names given as a cellstr, or cleared when there are none. function this = setRowLabels (this, labels) this.RowNames = labels; endfunction ## A table is already a plain table; only its row names go. function out = plainTable (this) out = this; out.RowNames = {}; endfunction ## Row names are unique, so grouping by them would put every row in a ## group of its own; they are not a grouping key. function tf = groupsByLabels (this) tf = false; endfunction ## A bare 'sortrows (tbl)' orders by every variable, not by the names. function tf = sortsByLabelsByDefault (this) tf = false; endfunction ## Row names do not tell two otherwise equal rows apart. function tf = uniqueIncludesLabels (this) tf = false; endfunction ## A row name never disqualifies its row. function tf = usableRowLabels (this) tf = true (height (this), 1); endfunction ## A table has only the order of its rows to interpolate against. function [x, ownPoints, errmsg] = fillSamplePoints (this) x = (1:height (this))'; ownPoints = false; errmsg = ''; endfunction ## The one row label property a table has. 'RowNames' is the only name ## it recognises; anything else is not a table property at all and the ## caller says so. function [this, handled] = setRowLabelProperty (this, name, val, chain_s) handled = isequal (name, 'RowNames'); if (! handled) return; endif ## Check for empty input to remove RowNames from table. if (isempty (val)) this.RowNames = {}; return; endif ## Check for valid input: cellstring scalar, char row vector,or ## string scalar matching an existing VariableName of appropriate ## type, or a numeric scalar referencing an existing VariableName ## of appropriate type. if ((ischar (val) && size (val, 1) == 1) || ((iscellstr (val) || isa (val, 'string') || isnumeric (val)) && numel (val) == 1)) if (ischar (val) || isa (val, 'string')) val = cellstr (val); endif [ixVar, ~] = resolveVarRef (this, val, 'lenient'); ## ## If variable name exists check for appropriate varTypes if (ixVar != 0) selvar = this.VariableValues{ixVar}; if (iscellstr (selvar) || ischar (selvar) || isa (selvar, 'string')) if (ischar (selvar) || isa (selvar, 'string')) selvar = cellstr (selvar); endif ## RowNames must be unique, just as for the array-form ## assignment and the constructor. The referenced ## variable may contain duplicates, so guard here. if (numel (__unique__ (selvar)) != numel (selvar)) error (strcat ("table.subsasgn: elements in", ... " 'RowNames' must be unique.")); endif ## When RowNames as set this way, the referenced Variable ## is removed. Octave specific behavior. MATLAB does not ## support this feature. this.RowNames = selvar; this = removevars (this, ixVar); return endif endif endif ## Check for valid input: cellstring, char, or string array ## with as many distinct elements as the table has rows. if (ischar (val) || isa (val, 'string')) val = cellstr (val); endif if (! iscellstr (val) || numel (val) != height (this)) error (strcat ("table.subsasgn: the number of 'RowNames'", ... " must equal the number of rows.")); elseif (numel (__unique__ (val)) != numel (val)) error (strcat ("table.subsasgn: elements in 'RowNames'", ... " must be unique.")); endif this.RowNames = val(:); endfunction ## Keeps the 'RowNames' entries picked out by IXROWS, in the order given, ## so that reordering or repeating rows carries their names along. A ## table with no row names is left alone: indexing the empty cell array ## would raise instead of staying empty. function this = subsetRowLabels (this, ixRows) if (! isempty (this.RowNames)) this.RowNames = this.RowNames(ixRows); endif endfunction ## Drops the 'RowNames' outright, putting the property back to the empty ## cell array that marks a table carrying no row labels. function this = clearRowLabels (this) this.RowNames = {}; endfunction ## Row names must stay unique, so a repeated name takes a numbered one: ## 'r1' repeated becomes 'r1', 'r1_1'. A table with no row names has ## nothing to repeat. function this = growRowLabels (this, n) if (isempty (this.RowNames)) this.RowCount = n; return; endif ## A row grown into has no name of its own, so it takes the default ## one for its place, which is what MATLAB writes there. for k = (numel (this.RowNames) + 1):n this.RowNames{k,1} = sprintf ('Row%d', k); endfor endfunction function this = repeatRowLabels (this, n, elementwise) if (isempty (this.RowNames)) return; endif base = this.RowNames; nrow = numel (base); if (elementwise) ix = repelem ((1:nrow)', n, 1); else ix = repmat ((1:nrow)', n, 1); endif names = base(ix); seen = zeros (1, nrow); for k = 1:numel (ix) v = ix(k); seen(v)++; if (seen(v) > 1) names{k} = sprintf ('%s_%d', base{v}, seen(v) - 1); endif endfor this.RowNames = names; endfunction ## A table built from an apply method's output. Row names are carried ## only where the method has some to give, which the mapping methods do ## and the reducing ones do not; ROWIX names input rows and means nothing ## to a class whose labels do not follow them. function out = assembleApply (this, vars, names, rowLabels, rowIx, ... caller) if (isempty (rowLabels)) out = table (vars{:}, 'VariableNames', names); else out = table (vars{:}, 'VariableNames', names, 'RowNames', rowLabels); endif endfunction ## Wraps the metadata struct that 'getProperties' assembles in a ## 'datatypes.tabular.TableProperties', the class that adds 'RowNames' to ## the shared properties and fixes the order the whole set displays in. function out = makeProperties (this) out = datatypes.tabular.TableProperties (getProperties (this), ... this.CustomPropTypes); endfunction ## Matches ROWREF, a cellstr of row names, against the 'RowNames' and ## returns their positions. Raises when the table has no row names at all ## to match against, and again naming every reference that is not one of ## them. Row names are unique, so each match is a single row. function [ixRows, newLabels] = resolveRowRef (this, rowRef) newLabels = {}; ## A table's rows are labelled by name, so a row time is no more a ## reference to one of them than a struct is. if (! iscellstr (rowRef)) error ("table: unsupported row indexing operand type: '%s'", ... class (rowRef)); endif if (isempty (this.RowNames)) error ("table: this table has no RowNames."); endif [tf, ixRows] = ismember (rowRef, this.RowNames); ixRows = ixRows(:); tf = tf(:); if (! all (tf)) ## A name the table does not carry is a name to add when a row is ## being assigned, and nothing at all when one is being read. The ## caller says which by asking for the second output. A NaN marks ## the place the reference named, so the row added lands there. if (nargout < 2) error ("table: no such named row in table: '%s'", ... strjoin (rowRef(! tf), ", ")); endif newLabels = rowRef(! tf); newLabels = newLabels(:); ixRows(! tf) = NaN; endif endfunction endmethods ################################################################################ ## ** Create Table and Convert Type ** ## ################################################################################ ## Available Methods ## ## ## ## 'table' 'table2array' 'table2cell' 'table2struct' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {table} {@var{tbl} =} table (@var{var1}, @var{var2}, @dots{}, @var{varN}) ## @deftypefnx {table} {@var{tbl} =} table (@qcode{'Size'}, @var{sz}, @qcode{'VariableTypes'}, @var{varTypes}) ## @deftypefnx {table} {@var{tbl} =} table (@dots{}, @qcode{'VariableNames'}, @var{varNames}) ## @deftypefnx {table} {@var{tbl} =} table (@dots{}, @qcode{'RowNames'}, @var{rowNames}) ## @deftypefnx {table} {@var{tbl} =} table (@dots{}, @qcode{'DimensionNames'}, @var{dimNames}) ## ## Create a new table. ## ## @code{@var{tbl} = table (@var{var1}, @var{var2}, @dots{}, @var{varN})} ## creates a new table with the given variables. The variables passed as ## input arguments become the variables of the table. Their names are ## automatically detected from the input variable names that you used. ## ## @code{@var{tbl} = table (@qcode{'Size'}, @var{sz}, ## @qcode{'VariableTypes'}, @var{varTypes})} creates a new table of the ## given size, @var{sz}, and with the given variable types, @var{varTypes}. ## @var{sz} must be a two-element numeric array, where @qcode{@var{sz}(1)} ## specifies the number of rows and @qcode{@var{sz}(2)} specifies the ## number of variables. The variables will contain the default value for ## elements of that type. ## ## @code{@var{tbl} = table (@dots{}, @qcode{'VariableNames'}, ## @var{varNames})} specifies the variable names to use in the constructed ## table. @var{varNames} must be either a cell array of character vectors ## or a string array with the same number of nonempty and unique elements as ## the number of table variables. ## ## @code{@var{tbl} = table (@dots{}, @qcode{'RowNames'}, @var{rowNames})} ## specifies the row names to use in the constructed table. @var{rowNames} ## must be either a cell array of character vectors or a string array with ## the same number of nonempty and unique elements as the number of rows in ## the table. ## ## @code{@var{tbl} = table (@dots{}, @qcode{'DimensionNames'}, ## @var{dimNames})} specifies the dimension names to use in the constructed ## table. @var{dimNames} must be either a two-element cell array of ## character vectors or a two-element string array with nonempty and unique ## elements. ## ## @code{@var{tbl} = table ()} returns an empty table with 0 rows and 0 ## variables. ## ## @end deftypefn function this = table (varargin) ## Return an empty table object if (nargin == 0) return endif ## Parse optional Name-Value paired arguments optNames = {'VariableNames', 'RowNames', 'DimensionNames'}; dfValues = {{}, {}, {'Row', 'Variables'}}; [VariableNames, RowNames, DimensionNames, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Check optional Name-Value paired arguments if (! isempty (VariableNames)) if (! (iscellstr (VariableNames) || isa (VariableNames, 'string'))) error (strcat ("table: 'VariableNames' must be either a cell", ... " array of character vectors or a string array.")); endif VariableNames = cellstr (VariableNames); if (any (cellfun (@isempty, VariableNames))) error ("table: 'VariableNames' must contain nonempty names."); endif endif if (! isempty (RowNames)) if (! (iscellstr (RowNames) || isa (RowNames, 'string'))) error (strcat ("table: 'RowNames' must be either a cell array", ... " of character vectors or a string array.")); endif RowNames = cellstr (RowNames); endif if (! (iscellstr (DimensionNames) || isa (DimensionNames, 'string')) || numel (DimensionNames) != 2) error (strcat ("table: 'DimensionNames' must be either a", ... " two-element cell array of character vectors or", ... " a two-element string array.")); endif this.DimensionNames = cellstr (DimensionNames); ## Dimension names cannot match reserved table identifiers reserved = {'Properties', 'RowNames', 'VariableNames', ':'}; idr = ismember (this.DimensionNames, reserved); if (any (idr)) error (strcat ("table: 'DimensionNames' cannot include the", ... " reserved name: '%s'"), this.DimensionNames{idr}); endif ## Check for conflict between VariableNames and DimensionNames idx = ismember (this.DimensionNames, VariableNames); if (any (idx)) error ("table: duplicate dimension and variable name: '%s'", ... this.DimensionNames{idx}); endif ## Construct a preallocated table with default values. SIZEROWS holds ## the requested row count, which is the only record of it when no ## variable is asked for. sizeRows = []; if ((numel (args) == 2 || numel (args) == 4) && strcmpi (args{1}, 'Size') && (numel (args) == 2 || strcmpi (args{3}, 'VariableTypes'))) ## Validate the size specifier if (! isnumeric (args{2}) || numel (args{2}) != 2) error ("table: 'Size' must be a two-element numeric vector."); endif ## Get number of rows and variables nr = args{2}(1); nv = args{2}(2); sizeRows = nr; ## Get variable types. 'Size' on its own gives every variable a ## double, which is what a size with no variables needs and what a ## preallocation without a stated type is taken to mean. if (numel (args) == 4) varTypes = args{4}; else varTypes = repmat ({'double'}, 1, nv); endif if (! iscellstr (varTypes) || numel (varTypes) != nv) error (strcat ("table: 'VariableTypes' must be a cellstring", ... " array of the same number of elements as", ... " defined in SZ(2).")); endif ## Check optional arguments if (! isempty (VariableNames) && numel (VariableNames) != nv) error (strcat ("table: inconsistent number of 'VariableNames'", ... " and 'VariableTypes'.")); elseif (isempty (VariableNames)) VariableNames = cell (1, nv); for i = 1:nv VariableNames{i} = sprintf ("Var%d", i); endfor endif if (! isempty (RowNames) && numel (RowNames) != nr) error (strcat ("table: inconsistent number of 'RowNames' and", ... " rows defined in SZ(1).")); endif ## Populate variables with defaults VariableTypes = cell (1, nv); VariableValues = cell (1, nv); for i = 1:nv VariableTypes{i} = varTypes{i}; switch (varTypes{i}) case {'double', 'single', 'int8', 'uint8', 'int16', 'uint16', ... 'int32', 'uint32', 'int64', 'uint64'} VariableValues{i} = zeros (nr, 1, varTypes{i}); case {'doublenan', 'doubleNaN'} VariableValues{i} = NaN (nr, 1, 'double'); case {'singlenan', 'singleNaN'} VariableValues{i} = NaN (nr, 1, 'single'); case 'logical' VariableValues{i} = logical (zeros (nr, 1)); case 'categorical' VariableValues{i} = categorical (NaN (nr, 1)); case 'datetime' VariableValues{i} = NaT (nr, 1); case 'duration' VariableValues{i} = seconds (zeros (nr, 1)); case 'calendarDuration' VariableValues{i} = calendarDuration (zeros (nr, 3)); case 'string' VariableValues{i} = string (NaN (nr, 1)); case {'cellstr', 'char'} VariableValues{i} = repmat (cellstr (""), nr, 1); case 'cell' VariableValues{i} = cell (nr, 1); case 'struct' VariableValues{i} = repmat (struct, nr, 1); case 'table' VariableValues{i} = table([]); case 'timetable' error ("table: 'timetable' variable type not supported yet."); otherwise error ("table: unsupported variable type: '%s'", varTypes{i}); endswitch endfor ## Construct a table with data from input arguments else ## Get variable names from input arguments if (isempty (VariableNames)) VariableNames = cell (size (args)); for i = 1:numel (args) VariableNames{i} = inputname (i); if (isempty (VariableNames{i})) VariableNames{i} = sprintf ("Var%d", i); endif endfor endif ## Check for unique names in input arguments [uqNames, ix] = __unique__ (VariableNames); if (numel (uqNames) < numel (VariableNames)) ixBad = 1:numel (VariableNames); ixBad(ix) = []; error ("table: duplicate variable names: %s", ... strjoin (VariableNames(ixBad), ", ")); endif ## Check number of variable names and input arguments if (numel (VariableNames) != numel (args)) error (strcat ("table: inconsistent number of variable names", ... " (%d) and variable values (%d)."), ... numel (VariableNames), numel (args)); endif ## Only a cell variable can hold a function handle, a dictionary or ## a time series for i = 1:numel (args) errmsg = variableRefusal (this, args{i}); if (! isempty (errmsg)) error ("table: %s", errmsg); endif endfor ## Check size of input variables if (! isempty (args)) nrows = size (args{1}, 1); if (ndims (args{1}) > 2) error (strcat ("table: variable values must not have more", ... " than 2 dimensions: input 1 '%s' has %d."), ... VariableNames{1}, ndims (args{1})); endif for i = 2:numel (args) if (ndims (args{i}) > 2) error (strcat ("table: variable values must not have more", ... " than 2 dimensions: input %d '%s' has %d."), ... i, VariableNames{i}, ndims (args{i})); endif nrows2 = size (args{i}, 1); if (nrows != nrows2) error (strcat ("table: inconsistent sizes between", ... " variables: var '%s' has %d rows; var '%s'", ... " has %d rows."), ... VariableNames{1}, nrows, VariableNames{i}, nrows2); endif endfor endif VariableValues = args(:)'; endif ## Construction ## Descriptions and units stay unset until they are given: an empty ## property says 'none', which is not the same as one blank per ## variable and is what an empty assignment returns them to. this.VariableDescriptions = {}; this.VariableUnits = {}; this.VariableNames = VariableNames(:)'; this.VariableValues = VariableValues; this.VariableTypes = cellfun ('class', VariableValues, ... 'UniformOutput', false); if (! isempty (VariableValues)) this.RowCount = 0; elseif (! isempty (sizeRows)) this.RowCount = sizeRows; endif if (! isempty (RowNames)) if (! isempty (VariableValues)) nrows = size (VariableValues{1}, 1); elseif (! isempty (sizeRows)) nrows = sizeRows; else nrows = 0; endif if (numel (RowNames) != nrows) error (strcat ("table: the number of 'RowNames' (%d) must", ... " equal the number of rows (%d)."), ... numel (RowNames), nrows); elseif (numel (__unique__ (RowNames)) != numel (RowNames)) error ("table: elements in 'RowNames' must be unique."); endif this.RowNames = RowNames(:); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{A} =} table2array (@var{tbl}) ## ## Converts a table to an array. ## ## @code{@var{A} = table2array (@var{tbl})} places the variables of ## @var{tbl} side by side and joins them into one array @var{A}, as ## concatenation with @code{[]} joins them in MATLAB. A multicolumn ## variable gives as many columns. The row names and every other property ## of @var{tbl} are left out. @code{@var{tbl}@{:,:@}} and ## @code{@var{tbl}.Variables} return the same array. ## ## The variables join as follows: ## ## @itemize ## @item Numeric and logical variables join as numbers do. An integer type ## takes precedence over @qcode{single}, @qcode{double} and ## @qcode{logical}, the leftmost integer type over any other integer type, ## @qcode{single} over @qcode{double}, and @qcode{double} over ## @qcode{logical}. Values outside the range of the resulting integer type ## are clipped to its limits. ## @item A numeric variable beside a @qcode{char} variable becomes the ## characters of its values. A logical variable does not join a ## @qcode{char} variable. ## @item Cell variables join only with other cell variables. ## @item A @qcode{duration} variable joins @qcode{double} and logical ## variables, whose values count days, and a @qcode{calendarDuration} ## variable joins all three. ## @item @qcode{categorical} variables join only if all or none of them are ## ordinal, and @qcode{datetime} variables only if all or none of them have ## a time zone. Structures join only with the same fields, and nested ## tables only with different variable names. ## @item A @qcode{missing} variable joins @qcode{double}, @qcode{single}, ## @qcode{categorical}, @qcode{datetime}, @qcode{duration} and ## @qcode{calendarDuration} variables, as the missing value of their type. ## @end itemize ## ## Where the variables cannot form one array, @var{A} is the cell array ## that @code{table2cell} returns; MATLAB raises an error instead. ## ## The exception is a @qcode{string} variable. Numeric, logical, ## @qcode{char}, cell and @qcode{missing} variables beside it are converted ## to strings, and @var{A} is a string array instead of a cell array. Each ## row of a @qcode{char} variable becomes one string, each cell one string, ## and a logical value the string @qcode{"true"} or @qcode{"false"}. ## ## @end deftypefn function A = table2array (this) A = varsAsArray (this, 'table2array'); endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{C} =} table2cell (@var{tbl}) ## ## Converts a table to a cell array. ## ## @code{@var{C} = table2cell (@var{tbl})} returns a cell array @var{C} of ## the same size as @var{tbl}, in which each cell holds one row of one ## variable, in the variable's own type. A multicolumn variable gives a row ## in each cell, a @qcode{char} matrix one row of text, and a nested table a ## table of one row. A cell variable of one column gives the contents of ## its cells. The row names and every other property of @var{tbl} are left ## out. ## ## @end deftypefn function C = table2cell (this) C = varsAsCell (this, 'table2cell'); endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{S} =} table2struct (@var{tbl}) ## @deftypefnx {table} {@var{S} =} table2struct (@var{tbl}, @qcode{'ToScalar'}, @qcode{true}) ## ## Converts a table to a scalar structure or structure array. ## ## @code{@var{S} = table2struct (@var{tbl})} returns a structure array with ## the same fields as the variables in @var{tbl}. The length of @var{S} is ## the same as the height of @var{tbl}. ## ## @code{@var{S} = table2struct (@var{tbl}, @qcode{'ToScalar'}, ## @qcode{true})} returns a scalar structure with the same fields as the ## variables in @var{tbl}. Each field has the same rows as the @var{tbl}. ## ## The output @var{S} does not include any of the table's properties. This ## also applies to row names. ## ## @end deftypefn function S = table2struct (this, varargin) [S, errmsg] = table2structResult (this, varargin); if (! isempty (errmsg)) error ("table.table2struct: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tt} =} table2timetable (@var{tbl}) ## @deftypefnx {table} {@var{tt} =} table2timetable (@var{tbl}, @qcode{'RowTimes'}, @var{rowTimes}) ## @deftypefnx {table} {@var{tt} =} table2timetable (@var{tbl}, @qcode{'TimeStep'}, @var{dt}) ## @deftypefnx {table} {@var{tt} =} table2timetable (@var{tbl}, @qcode{'SampleRate'}, @var{fs}) ## @deftypefnx {table} {@var{tt} =} table2timetable (@dots{}, @qcode{'StartTime'}, @var{t0}) ## ## Convert a table to a timetable. ## ## @code{@var{tt} = table2timetable (@var{tbl})} converts the table ## @var{tbl} to a timetable, taking its row times from the @strong{first} ## variable that is a @code{datetime} or a @code{duration}. That ## variable stops being a variable and becomes the row times, and the row ## dimension is named after it. A table with no such variable cannot be ## converted this way. ## ## @code{@var{tt} = table2timetable (@var{tbl}, @qcode{'RowTimes'}, ## @var{rowTimes})} says which times to use. @var{rowTimes} may be a ## @code{datetime} or @code{duration} vector with one element per row, in ## which case every variable of @var{tbl} is kept and the row dimension is ## named @qcode{'Time'}; or it may name one of the variables, by name or by ## index, which is then taken as the row times exactly as above. ## ## @code{@var{tt} = table2timetable (@var{tbl}, @qcode{'TimeStep'}, ## @var{dt})} and @code{@var{tt} = table2timetable (@var{tbl}, ## @qcode{'SampleRate'}, @var{fs})} generate the row times instead, keeping ## every variable. @qcode{'StartTime'} sets the time of the first row for ## either, and the row dimension is named @qcode{'Time'}. ## ## The row names of @var{tbl}, if it has any, are not carried over: a ## timetable labels its rows by time and by nothing else. ## ## @seealso{timetable.timetable2table, array2timetable, timetable, table} ## @end deftypefn function TT = table2timetable (this, varargin) if (mod (numel (varargin), 2) != 0) error (strcat ("table.table2timetable: name-value arguments must", ... " be in pairs.")); endif ## Parse optional paired arguments optNames = {'RowTimes', 'TimeStep', 'SampleRate', 'StartTime'}; dfValues = {missing, missing, missing, missing}; [RowTimes, TimeStep, SampleRate, StartTime, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error ("table.table2timetable: invalid optional paired argument."); endif given = [! isa(RowTimes, 'missing'), ! isa(TimeStep, 'missing'), ... ! isa(SampleRate, 'missing')]; if (sum (given) > 1) error (strcat ("table.table2timetable: only one of 'RowTimes',", ... " 'TimeStep' and 'SampleRate' may be given.")); endif ## 'Properties' is synthesised by 'subsref' and a method's dot access ## to its own class does not go through it, so the metadata is read off ## the properties themselves. varNames = this.VariableNames; ## Decide where the row times come from, and which variable if any stops ## being one. A vector of times takes no variable and leaves the row ## dimension at its default; a variable, whether named outright or found ## by type, gives the dimension its own name. ixTime = 0; dimName = 'Time'; timeArgs = {}; if (given(1)) if (isdatetime (RowTimes) || isduration (RowTimes)) timeArgs = {'RowTimes', RowTimes}; else ixTime = resolveTimeVar (RowTimes, varNames); endif elseif (given(2)) timeArgs = {'TimeStep', TimeStep}; elseif (given(3)) timeArgs = {'SampleRate', SampleRate}; else for i = 1:numel (varNames) v = getvar (this, varNames{i}); if (isdatetime (v) || isduration (v)) ixTime = i; break; endif endfor if (ixTime == 0) error (strcat ("table.table2timetable: TBL must contain a", ... " datetime or a duration variable to use as row", ... " times, or the row times must be given.")); endif endif if (ixTime > 0) rt = getvar (this, varNames{ixTime}); if (! (isdatetime (rt) || isduration (rt))) error (strcat ("table.table2timetable: the variable '%s' is a", ... " %s; row times must be a datetime or a", ... " duration."), ... varNames{ixTime}, class (rt)); endif timeArgs = {'RowTimes', rt}; dimName = varNames{ixTime}; varNames(ixTime) = []; endif if (! isa (StartTime, 'missing')) timeArgs = [timeArgs, {'StartTime', StartTime}]; endif ## Build the timetable empty and fill it by name. The variables cannot be ## handed to the constructor positionally: a surviving datetime variable ## would be read as the row times. TT = timetable ('Size', [height(this), 0], 'VariableTypes', {}, ... timeArgs{:}); for i = 1:numel (varNames) TT.(varNames{i}) = getvar (this, varNames{i}); endfor TT.Properties.DimensionNames = {dimName, this.DimensionNames{2}}; ## Carry the metadata of the variables that survived, and of the table. keep = true (1, numel (this.VariableNames)); if (ixTime > 0) keep(ixTime) = false; endif if (! isempty (this.VariableDescriptions)) TT.Properties.VariableDescriptions = this.VariableDescriptions(keep); endif if (! isempty (this.VariableUnits)) TT.Properties.VariableUnits = this.VariableUnits(keep); endif if (! isempty (this.VariableContinuity)) TT.Properties.VariableContinuity = this.VariableContinuity(keep); endif if (! isempty (this.Description)) TT.Properties.Description = this.Description; endif if (! isempty (this.UserData)) TT.Properties.UserData = this.UserData; endif ## A custom property describing the variables loses the entry of the one ## that became the row times, which is no longer a variable. TT = tabular.carryCustomProps (TT, this, find (keep)); endfunction endmethods ################################################################################ ## ** Save to Files ** ## ################################################################################ ## Available Methods ## ## ## ## 'table2csv' 'table2ods' 'writetable' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {table} {} table2csv (@var{tbl}, @var{file}) ## @deftypefnx {table} {} table2csv (@var{tbl}, @var{file}, @var{Name}, @var{Value}) ## ## Write a table to a comma-separated-value (CSV) file. ## ## @code{table2csv (@var{tbl}, @var{file})} writes the table @var{tbl} to ## @var{file}, which may be a character vector, a cellstr, or a string ## scalar. The resulting file can be read back with @code{csv2table}. ## ## The file begins with a comment line reporting how many consecutive rows ## hold the variable types, names, descriptions, and units, in that order. ## Those header rows are followed by one row of data per table row. ## ## Variables are serialized as follows: ## ## @itemize ## @item ## Numeric and logical variables are written as numbers (logicals as ## @code{0}/@code{1}). Missing and infinite values are written as the ## tokens @qcode{NaN}, @qcode{NA}, @qcode{Inf}, and @qcode{-Inf}. ## ## @item ## Character, cellstr, and @code{string} variables are written as quoted ## text. A @code{char} matrix is one column of text, each row written ## whole so that its padding survives. A missing @code{string}, and every ## entry of a @code{missing} variable, is written as an empty unquoted ## field, which says that there is no value where @qcode{""} says the value ## is an empty string. ## ## @item ## @code{datetime} and @code{duration} variables are written in ISO 8601 ## form, which is exact whatever their display format; the format itself is ## recorded alongside the variable type, so that it is restored on read. ## @code{calendarDuration} and @code{categorical} variables are written as ## their display strings. ## ## @item ## A multicolumn variable is split into consecutive columns that share the ## same variable name. ## ## @item ## A nested table is split into columns tagged with both the outer and the ## nested variable name. A structure is split into one column per field, ## tagged with the variable name and the field name. ## @end itemize ## ## When @var{tbl} has row names they are written under a leading ## @qcode{RowNames} column. A row of variable descriptions and a row of ## variable units are written whenever the corresponding property is set, ## or a nested variable carries one; a property that was never set writes ## no row at all, so that it reads back unset. ## ## The following @var{Name}-@var{Value} options are supported: ## ## @multitable @columnfractions 0.30 0.70 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'WriteVariableNames'} @tab A logical scalar specifying ## whether the variable names are written (default @qcode{true}). When ## @qcode{false} the file carries none, so @code{csv2table} numbers the ## variables on read and can no longer group the columns: a multicolumn ## variable comes back as separate variables and a nested table as flat ## columns. ## @item @qcode{'WriteRowNames'} @tab A logical scalar specifying whether ## the row labels are written as a leading column (default @qcode{true}). ## @end multitable ## ## Note the following round-trip limitation when reading the file back ## with @code{csv2table}: @code{calendarDuration} and @code{categorical} ## variables are returned as cell arrays of character vectors and their ## values are not reconstructed. ## ## @end deftypefn function table2csv (this, file, varargin) file = char (cellstr (file)); if (mod (numel (varargin), 2) != 0) error ("table.table2csv: name-value arguments must be in pairs."); endif ## Parse optional paired arguments optNames = {'WriteVariableNames', 'WriteRowNames'}; dfValues = {true, true}; [writeVarNames, writeRowNames, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error ("table.table2csv: invalid optional paired argument."); endif if (! (islogical (writeVarNames) && isscalar (writeVarNames))) error (strcat ("table.table2csv: 'WriteVariableNames' must be a", ... " logical scalar.")); endif if (! (islogical (writeRowNames) && isscalar (writeRowNames))) error (strcat ("table.table2csv: 'WriteRowNames' must be a logical", ... " scalar.")); endif [csv, errmsg] = __csv_parts__ (this, writeVarNames, writeRowNames); if (! isempty (errmsg)) error ("table.table2csv: %s", errmsg); endif ## Write to file msg = __table2csv__ (file, csv); if (msg) error ("table.table2csv: %s", msg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {} table2ods (@var{tbl}, @var{file}) ## @deftypefnx {table} {} table2ods (@var{tbl}, @var{file}, @var{Name}, @var{Value}) ## ## Write a table to an OpenDocument spreadsheet file. ## ## @code{table2ods (@var{tbl}, @var{file})} writes the table @var{tbl} to ## @var{file}, which may be a character vector, a cellstr, or a string ## scalar. When @var{file} ends in @qcode{.ods} a compressed (ZIP-packaged) ## OpenDocument spreadsheet is written; when it ends in @qcode{.fods} a flat ## (single-XML) OpenDocument spreadsheet is written instead. The resulting ## file can be read back with @code{ods2table}. ## ## The data sheet (named @qcode{Sheet1} by default) is headed by the ## variable names and then carries one natively typed cell per value, and a ## hidden @qcode{__datatypes_meta__} sheet carries the variable types, ## descriptions and units needed to restore the exact Octave types on ## read-back. The names are on the data sheet, where anyone opening the ## file can see them, and a nested table takes one header row per nesting ## level. Variables map to ODS cell types as follows: ## ## @itemize ## @item ## Numeric variables become @code{float} cells and logical variables become ## @code{boolean} cells. Integers are written with their exact digits. ## ## @item ## @code{datetime} variables become native @code{date} cells and ## @code{duration} variables become native @code{time} cells, both encoded ## as ISO 8601 strings. ## ## @item ## Character, cellstr, @code{string}, @code{categorical}, and ## @code{calendarDuration} variables become @code{string} cells. ## ## @item ## A multicolumn variable is split into consecutive columns that share the ## same variable name. ## @end itemize ## ## Missing values (@code{NaN}, @code{NaT}, and missing strings) are written ## as empty cells. When @var{tbl} has row names they are written under a ## leading @qcode{RowNames} column. A row of variable descriptions and a ## row of variable units are written whenever the corresponding property is ## set; a property that was never set writes no row at all, so that it ## reads back unset. A zone-aware @code{datetime} ## variable keeps its @code{TimeZone} on read-back. ## ## The following @var{Name}-@var{Value} options are supported: ## ## @multitable @columnfractions 0.30 0.70 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'Sheet'} @tab The name of the sheet to write (default ## @qcode{'Sheet1'}). When @var{file} already exists the named sheet is ## added or replaced while every other sheet is preserved, so a workbook can ## be built up one table at a time. ## @item @qcode{'WriteVariableNames'} @tab A logical scalar specifying ## whether the variable names are written (default @qcode{true}). When ## @qcode{false} the file carries none at all, the hidden metadata sheet ## included, so @code{ods2table} numbers the variables on read and can no ## longer group the columns: a multicolumn variable comes back as separate ## variables and a nested table as flat columns. ## @item @qcode{'WriteRowNames'} @tab A logical scalar specifying whether ## the row labels are written as a leading column (default @qcode{true}). ## @item @qcode{'WriteMode'} @tab @qcode{'overwritesheet'} or ## @qcode{'inplace'} replace the sheet (the default when the sheet exists), ## @qcode{'append'} appends the table's rows to it, and ## @qcode{'replacefile'} discards any existing file. ## @end multitable ## ## A nested table is split into columns tagged with both the outer and the ## nested variable name, and a structure into one column per field, exactly ## as @code{table2csv} does; the tagging rows live on the hidden metadata ## sheet, so the data sheet stays flat. Note the following round-trip ## limitation when reading the file back with @code{ods2table}: ## @code{calendarDuration} and @code{categorical} variables are returned as ## cell arrays of character vectors and their values are not reconstructed. ## ## @end deftypefn function table2ods (this, file, varargin) file = char (cellstr (file)); ## A '.fods' file is written as flat XML, a '.ods' file as a ZIP package. [~, ~, ext] = fileparts (file); if (strcmpi (ext, '.fods')) is_flat = true; elseif (strcmpi (ext, '.ods')) is_flat = false; else error (strcat ("table.table2ods: FILE must have a '.ods' or", ... " '.fods' extension.")); endif if (mod (numel (varargin), 2) != 0) error ("table.table2ods: name-value arguments must be in pairs."); endif ## Parse optional paired arguments optNames = {'Sheet', 'WriteMode', 'WriteVariableNames', ... 'WriteRowNames'}; dfValues = {'Sheet1', '', true, true}; [sheet, writeMode, writeVarNames, writeRowNames, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! (islogical (writeVarNames) && isscalar (writeVarNames))) error (strcat ("table.table2ods: 'WriteVariableNames' must be a", ... " logical scalar.")); endif if (! (islogical (writeRowNames) && isscalar (writeRowNames))) error (strcat ("table.table2ods: 'WriteRowNames' must be a logical", ... " scalar.")); endif if (! isempty (args)) error ("table.table2ods: invalid optional paired argument."); endif if (isa (sheet, 'string')) sheet = char (sheet); endif if (! (ischar (sheet) && isrow (sheet))) error ("table.table2ods: 'Sheet' must be a sheet name."); endif writeMode = lower (char (writeMode)); switch (writeMode) case {'', 'replacefile', 'overwritesheet', 'inplace', 'append'} ## supported write modes otherwise error ("table.table2ods: 'WriteMode' '%s' is not valid.", writeMode); endswitch ## Merge into an existing workbook (preserving other sheets) by reading it ## back, modifying the struct of tables, and rewriting the whole file. if (exist (file, 'file') && ! strcmp (writeMode, 'replacefile')) s = ods2struct (file); s = __mergesheet__ (s, this, sheet, writeMode); struct2ods (file, s); return; endif ## Fresh single-sheet write. [V, vtype, meta, hdr] = __ods_parts__ (this, 'table.table2ods', ... writeVarNames, writeRowNames); msg = __table2ods__ (file, V, vtype, meta, is_flat, ... struct ('sheetname', sheet, 'header', {hdr})); if (! isequal (msg, 0)) error ("table.table2ods: %s", msg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {} writetable (@var{tbl}, @var{filename}) ## @deftypefnx {table} {} writetable (@var{tbl}, @var{filename}, @var{Name}, @var{Value}) ## ## Write a table to a file in a MATLAB-compatible format. ## ## @code{writetable (@var{tbl}, @var{filename})} writes the table @var{tbl} ## to @var{filename}. The file type is inferred from the extension: ## @qcode{.txt}, @qcode{.csv}, and @qcode{.dat} are written as delimited ## text; @qcode{.ods} as an OpenDocument spreadsheet; and @qcode{.xlsx} and ## @qcode{.xlsm} as Excel spreadsheets. Use the @qcode{'FileType'} option ## to override the inferred type. ## ## Unlike @code{table2csv}/@code{table2ods}, no type metadata is written: ## the file holds only an optional variable-name header row followed by the ## data, so it can be read by other applications. Type information is ## recovered by @code{readtable} through automatic detection (text) or the ## native cell types (spreadsheet). ## ## @qcode{.ods} is the OpenDocument spreadsheet format, an open standard ## that LibreOffice and other applications read and write on every ## platform. @strong{MATLAB is the exception and cannot be given one.} ## Its @code{writetable} refuses the extension, and its readers open an ## OpenDocument file only by handing it to Excel, so a MATLAB user without ## Excel cannot read one at all. A file written for a MATLAB user should ## therefore be @qcode{.csv} or @qcode{.xlsx}. Everywhere else the format ## is a good choice, and @code{table2ods} writes it losslessly. ## ## The following options are supported: ## ## @multitable @columnfractions 0.28 0.72 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'FileType'} @tab @qcode{'text'} or @qcode{'spreadsheet'}. ## @item @qcode{'WriteVariableNames'} @tab Logical; write the variable names ## as the first row (default @qcode{true}). ## @item @qcode{'WriteRowNames'} @tab Logical; write the row names as the ## first column (default @qcode{false}). ## @item @qcode{'Delimiter'} @tab Field delimiter for text files: a single ## character or one of @qcode{'comma'}, @qcode{'space'}, @qcode{'tab'}, ## @qcode{'semi'}, @qcode{'bar'} (default @qcode{','}). ## @item @qcode{'QuoteStrings'} @tab @qcode{'minimal'}, @qcode{'all'}, or ## @qcode{'none'} for text files (default @qcode{'minimal'}). ## @item @qcode{'Sheet'} @tab Spreadsheet only: the name of the sheet to ## write. The default is the first sheet of an existing workbook, or ## @qcode{'Sheet1'} for a new file. ## @item @qcode{'Range'} @tab Spreadsheet only: an A1-style anchor such as ## @qcode{'C5'} at which to place the top-left corner of the data (fresh ## writes only). ## @item @qcode{'WriteMode'} @tab For text: @qcode{'overwrite'} (default) or ## @qcode{'append'}. For spreadsheets: @qcode{'overwritesheet'} / ## @qcode{'inplace'} (replace the target sheet), @qcode{'append'} (append ## rows to it), or @qcode{'replacefile'} (overwrite the whole file). ## @end multitable ## ## When the target spreadsheet already exists, the sheet named by ## @qcode{'Sheet'} (defaulting to the first existing sheet) is added or ## replaced while every other sheet is preserved, unless @qcode{'WriteMode'} ## is @qcode{'replacefile'}. For ODS, existing foreign spreadsheets (for ## example those written by LibreOffice) are updated in place, keeping their ## other parts; for Excel (@qcode{.xlsx}, @qcode{.xlsm}) the workbook is ## read back and rewritten, so only its cell values are preserved. ## ## Dates written to Excel use its 1900 serial date system, which counts a ## 29 February 1900 that never existed: 1900-03-01 is serial 61, and every ## earlier date is one less than its plain day count. That system has no ## serial below 0 (1899-12-31), so any earlier date is written as text ## rather than as a date, as MATLAB does. MATLAB spells that text in the ## datetime display format; here it is written in ISO 8601 form. ## ## Nested tables and structures are not supported, and the legacy binary ## formats @qcode{.xls} and @qcode{.xlsb} are not supported either; use ## @qcode{.xlsx}, @qcode{.ods}, or a text format. ## ## @end deftypefn function writetable (this, filename, varargin) if (! ((ischar (filename) && isvector (filename)) ... || (isa (filename, 'string') && isscalar (filename)))) error (strcat ("table.writetable: FILENAME must be a character", ... " vector or string scalar.")); endif file = char (filename); if (mod (numel (varargin), 2) != 0) error ("table.writetable: name-value arguments must be in pairs."); endif ## Parse optional paired arguments optNames = {'FileType', 'WriteVariableNames', 'WriteRowNames', ... 'Delimiter', 'QuoteStrings', 'Sheet', 'Range', 'WriteMode'}; dfValues = {'', true, false, ',', 'minimal', '', '', ''}; [fileType, writeVarNames, writeRowNames, delim, quoteStrings, sheet, ... range, writeMode, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error ("table.writetable: invalid optional paired argument."); endif if (isa (sheet, 'string')) sheet = char (sheet); endif if (isa (range, 'string')) range = char (range); endif writeMode = lower (char (writeMode)); ## Resolve the file type from the option or the extension [~, ~, ext] = fileparts (file); if (isempty (fileType)) switch (lower (ext)) case {'.txt', '.csv', '.dat'} fileType = 'text'; case {'.ods', '.fods', '.xlsx', '.xlsm'} fileType = 'spreadsheet'; case {'.xls', '.xlsb'} error (strcat ("table.writetable: '%s' Excel files are not", ... " supported; use '.xlsx', '.ods', or a text", ... " format."), ext); otherwise error (strcat ("table.writetable: cannot infer the file type", ... " from '%s'; specify 'FileType'."), ext); endswitch endif switch (lower (fileType)) case 'text' fmt = 'display'; case 'spreadsheet' fmt = 'iso'; otherwise error (strcat ("table.writetable: 'FileType' must be 'text'", ... " or 'spreadsheet'.")); endswitch ## Office Open XML (.xlsx/.xlsm) uses a separate courier from ODS. isXlsx = any (strcmpi (ext, {'.xlsx', '.xlsm'})); ## Validate 'Sheet', 'Range', and 'WriteMode' against the resolved type. appendMode = false; if (strcmp (fmt, 'display')) # text if (! isempty (sheet) || ! isempty (range)) error (strcat ("table.writetable: 'Sheet' and 'Range' are not", ... " supported for text files.")); endif switch (writeMode) case {'', 'overwrite'} appendMode = false; case 'append' appendMode = true; otherwise error (strcat ("table.writetable: 'WriteMode' '%s' is not", ... " valid for text files; use 'overwrite' or", ... " 'append'."), ... writeMode); endswitch else # spreadsheet if (! isempty (sheet) && ! (ischar (sheet) && isrow (sheet))) error ("table.writetable: 'Sheet' must be a sheet name."); endif switch (writeMode) case {'', 'replacefile', 'overwritesheet', 'inplace', 'append'} ## supported spreadsheet write modes otherwise error (strcat ("table.writetable: 'WriteMode' '%s' is not", ... " valid for spreadsheet files."), writeMode); endswitch ## A 'Range' anchors a fresh write; it has no meaning when merging into ## an existing workbook. if (! isempty (range)) if (strcmp (writeMode, 'append')) error (strcat ("table.writetable: 'Range' is not supported", ... " with 'WriteMode' 'append'.")); endif if (exist (file, 'file') && ! strcmp (writeMode, 'replacefile')) error (strcat ("table.writetable: 'Range' is not supported", ... " when writing into an existing file.")); endif endif endif __interop_write__ (this, 'table.writetable', file, ... struct ('ext', ext, 'isXlsx', isXlsx, 'fmt', fmt, ... 'appendMode', appendMode, ... 'writeVarNames', writeVarNames, ... 'writeRowLabels', writeRowNames, ... 'rowLabelHeader', 'Row', 'delim', delim, ... 'fname', 'writetable', ... 'quoteStrings', quoteStrings, ... 'sheet', sheet, 'range', range, ... 'writeMode', writeMode)); endfunction endmethods ################################################################################ ## ** Summary Information ** ## ################################################################################ ## Available Methods ## ## ## ## 'summary' 'height' 'width' 'head' ## ## 'tail' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {table} {} summary (@var{tbl}) ## @deftypefnx {table} {@var{s} =} summary (@var{tbl}) ## ## Print a summary of a table. ## ## @code{summary (@var{tbl})} prints the description from ## @qcode{@var{tbl}.Properties.Description} followed by a summary of each ## table variable's values and their properties as defined in ## @qcode{@var{tbl}.Properties.VariableUnits} and ## @qcode{@var{tbl}.Properties.VariableDescriptions}. ## ## @code{@var{s} = summary (@var{tbl})} returns a structure, @var{s}, that ## contains a summary of the input table, @var{tbl}. Each field of @var{s} ## is a structure that summarizes the values in the corresponding variable ## of @var{tbl}. Where applicable, the number of missing values is reported ## in a @qcode{NumMissing} field and printed when it is greater than zero. ## ## @itemize ## @item For numerical variables of @qcode{double}, @qcode{single} or any ## @qcode{int} type, it prints the minimum, median, and maximum values. For ## multicolumnar numerical variables it prints the minimum, median, and ## maximum values for each column separately. ## ## @item For variables of @qcode{logical} type, it prints the occurrences ## of @qcode{True} and @qcode{False}. ## ## @item For variables of type @qcode{datetime} and @qcode{duration} it ## prints the minimum, median, and maximum values, computed after excluding ## any missing (@qcode{NaT} or @qcode{NaN}) elements. ## ## @item For variables of type @qcode{calendarDuration}, which are not ## totally ordered, only the size, the type, and the number of missing ## values are reported. ## ## @item For variables of type @qcode{cellstr}, @qcode{cell}, ## @qcode{string}, @qcode{categorical}, and @qcode{struct} it prints the ## size and the type of variable. ## @end itemize ## ## @end deftypefn function [varargout] = summary (this, varargin) if (! isempty (varargin)) error ("table.summary: too many input arguments."); endif s = summaryOf (this); if (nargout == 0) summaryPrint (this, s, inputname (1)); elseif (nargout == 1) varargout{1} = s; else error ("table.summary: invalid number of output arguments."); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{H} =} height (@var{tbl}) ## ## Number of rows in table. ## ## @code{@var{H} = height (@var{tbl})} returns the number of rows in the ## table @var{tbl} as a scalar. It is the equivalent of ## @qcode{size (@var{tbl}, 1)}. ## ## For an empty table, or a table created with zero rows, @code{height} ## returns 0. The presence of row names does not affect the result. ## ## @end deftypefn function out = height (this) if (! isempty (this.VariableValues)) out = size (this.VariableValues{1}, 1); elseif (! isempty (this.RowNames)) ## The row names outlive the variables and count the rows themselves. out = numel (this.RowNames); else out = this.RowCount; endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{W} =} width (@var{tbl}) ## ## Number of variables in table. ## ## @code{@var{W} = width (@var{tbl})} returns the number of variables in the ## table @var{tbl} as a scalar. It is the equivalent of ## @qcode{size (@var{tbl}, 2)}. ## ## Note that this is the number of table variables, not the total number of ## columns. A single variable may itself contain several columns (for ## example, a matrix-valued variable), but it still counts as one towards ## the table width. ## ## For a table with no variables, @code{width} returns 0. ## ## @end deftypefn function out = width (this) out = numel (this.VariableNames); endfunction ## -*- texinfo -*- ## @deftypefn {table} {} head (@var{tbl}) ## @deftypefnx {table} {} head (@var{tbl}, @var{k}) ## @deftypefnx {table} {@var{out} =} head (@var{tbl}, @var{k}) ## ## Display or return the first @var{k} rows of a table. ## ## @code{head (@var{tbl})} displays the first eight rows of the table ## @var{tbl}. If @var{tbl} has fewer than eight rows, then all rows are ## displayed. ## ## @code{head (@var{tbl}, @var{k})} displays the first @var{k} rows of the ## table @var{tbl}. @var{k} must be a real, nonnegative, integer scalar ## value. If @var{tbl} has fewer than @var{k} rows, then all rows are ## displayed, and @var{k} of zero displays none. ## ## @code{@var{out} = head (@var{tbl}, @var{k})} returns the first @var{k} ## rows in a new table @var{out} instead of displaying them. If @var{k} is ## omitted or empty, then it defaults to eight. If @var{tbl} has fewer than ## @var{k} rows, then all available rows are returned. ## ## The returned table preserves the variable names, row names, and all other ## properties of @var{tbl}. ## ## @end deftypefn function [varargout] = head (this, k) if (nargin < 2) k = []; endif [ixRows, errmsg] = headTailRows (this, k, false); if (! isempty (errmsg)) error ("table.head: %s", errmsg); endif out = subsetrows (this, ixRows); if (nargout == 0) print_table (out); elseif (nargout == 1) varargout{1} = out; else error ("table.head: invalid number of output arguments."); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {} tail (@var{tbl}) ## @deftypefnx {table} {} tail (@var{tbl}, @var{k}) ## @deftypefnx {table} {@var{out} =} tail (@var{tbl}, @var{k}) ## ## Display or return the last @var{k} rows of a table. ## ## @code{tail (@var{tbl})} displays the last eight rows of the table ## @var{tbl}. If @var{tbl} has fewer than eight rows, then all rows are ## displayed. ## ## @code{tail (@var{tbl}, @var{k})} displays the last @var{k} rows of the ## table @var{tbl}. @var{k} must be a real, nonnegative, integer scalar ## value. If @var{tbl} has fewer than @var{k} rows, then all rows are ## displayed, and @var{k} of zero displays none. ## ## @code{@var{out} = tail (@var{tbl}, @var{k})} returns the last @var{k} ## rows in a new table @var{out} instead of displaying them. If @var{k} is ## omitted or empty, then it defaults to eight. If @var{tbl} has fewer than ## @var{k} rows, then all available rows are returned. ## ## The returned table preserves the variable names, row names, and all other ## properties of @var{tbl}. ## ## @end deftypefn function [varargout] = tail (this, k) if (nargin < 2) k = []; endif [ixRows, errmsg] = headTailRows (this, k, true); if (! isempty (errmsg)) error ("table.tail: %s", errmsg); endif out = subsetrows (this, ixRows); if (nargout == 0) print_table (out); elseif (nargout == 1) varargout{1} = out; else error ("table.tail: invalid number of output arguments."); endif endfunction endmethods ################################################################################ ## ** Sort, Filter, and Rearrange ** ## ################################################################################ ## Available Methods ## ## ## ## 'sortrows' 'unique' 'issortedrows' 'topkrows' ## ## 'addvars' 'renamevars' 'movevars' 'removevars' ## ## 'splitvars' 'mergevars' 'convertvars' 'rows2vars' ## ## 'stack' 'unstack' 'inner2outer' 'addprop' ## ## 'rmprop' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} sortrows (@var{tblA}) ## @deftypefnx {table} {@var{tblB} =} sortrows (@var{tblA}, @qcode{'RowNames'}) ## @deftypefnx {table} {@var{tblB} =} sortrows (@var{tblA}, @var{rowDimName}) ## @deftypefnx {table} {@var{tblB} =} sortrows (@var{tblA}, @var{vars}) ## @deftypefnx {table} {@var{tblB} =} sortrows (@var{tblA}, @dots{}, @var{direction}) ## @deftypefnx {table} {@var{tblB} =} sortrows (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {table} {[@var{tblB}, @var{index}] =} sortrows (@dots{}) ## ## Sort the rows of a table. ## ## @code{@var{tblB} = sortrows (@var{tblA})} sorts the rows in @var{tblA} in ## ascending order based on the values in the first variable. If elements ## in the first variable are repeated, then @code{sortrows} sorts by the ## elements in the second variable, and so on. ## ## @code{@var{tblB} = sortrows (@var{tblA}, 'RowNames')} sorts the ## table @var{tblA} according to its row names. If @var{tblA} does not ## have row names, i.e. @qcode{tblA.Properties.RowNames} is empty, then it ## returns @var{tblA}. ## ## @code{@var{tblB} = sortrows (@var{tblA}, @var{rowDimName})} also sorts ## the table @var{tblA} along the first dimension, @var{rowDimName}, which ## is the equivalent to the previous syntax, i.e. according to its row ## names. If @var{tblA} does not have row names, that is ## @qcode{tblA.Properties.RowNames} is empty, then it returns @var{tblA}. ## For this syntax to work, @var{rowDimName} must match the first element in ## @qcode{tblA.Properties.DimensionNames}, otherwise @var{rowDimName} is ## considered a variable name, as in the following syntax. ## ## @code{@var{tblB} = sortrows (@var{tblA}, @var{vars})} sorts the rows in ## table @var{tblA} by the elements in the variables specified by ## @var{vars}, which can be a character vector (for a single variable) or a ## cell array of character vectors or a string array (specifying a single or ## multiple variables). If @var{tblA} has row names, then @var{vars} can ## include the row names. Alternatively, @var{vars} can be a logical vector ## or a numeric vector of real integers indexing the desired variables. ## Positive integers specify an ascending order, whereas negative integers ## specify a descending order for the referenced variables. You can also ## index all available variables in @var{tblA} by passing a semicolon ## character argument. This Octave-specific syntax facilitates the use of ## the @var{direction} input argument when no particular variable needs to ## be selected to sort on. Additionally, @var{vars} can be a ## @qcode{vartype} object used to create a subscript that selects variables ## of a specified type. ## ## @code{@var{tblB} = sortrows (@var{tblA}, @dots{}, @var{direction})} sorts ## the rows in table @var{tblA} in the order specified by @var{direction} ## for any of the previous syntaxes. @var{direction} can be ## @qcode{'ascend'} or @qcode{'descend'}, which is applied to all specified ## variables or row names that @code{sortrows} operates on. @var{direction} ## can also be a cell array of character vectors, whose elements are ## @qcode{'ascend'} and @qcode{'descend'}, where each element corresponds to ## the specified variables and/or row names used for sorting the table. ## The order specified by @var{direction} always takes precedence over the ## order defined by a numerical vector of integers in @var{vars}. ## @var{direction} must always be the 3rd input argument. If you want to ## omit passing selected variables and allow @code{sortrows} to work on ## consecutive variables until all ties are resolved, then you can leave the ## second input argument empty, as in ## @code{sortrows (@var{tblA}, @{[]@}, @var{direction})} or pass a ## colon argument for @var{vars} as in ## @code{sortrows (@var{tblA}, @{':'@}, @var{direction})}. ## ## @code{@var{tblB} = sortrows (@dots{}, @var{Name}, @var{Value})} specifies ## additional parameters for sorting rows of a table with the following ## Name-Value paired arguments. ## ## @itemize ## @item @qcode{'MissingPlacement'} specifies the placement of missing ## values with one of the following options: @qcode{'auto'} places the ## missing elements at the bottom for ascending order and at the top for ## descending order; @qcode{'first'} places missing elements at the top; ## @qcode{'last'} places missing elements at the bottom. ## @item @qcode{'ComparisonMethod'} specifies the element comparison method ## with one of the following options: @qcode{'auto'} sorts rows using the ## real part for real numbers and the magnitude for complex numbers; ## @qcode{'real'} sorts rows using the real part for both real and complex ## numbers; @qcode{'abs'} sorts rows using the magnitude for both real and ## complex numbers. For complex numbers with equal magnitude, the phase ## angle in the interval @math{(-π, π]} is further used to break ties. ## @end itemize ## ## @code{[@var{tblB}, @var{index}] = sortrows (@dots{})} also returns an ## index vector such that @qcode{@var{tblB} = @var{tblA}(@var{index},:)}. ## ## @end deftypefn function [tbl, index] = sortrows (this, varargin) [index, errmsg] = sortrowsIndex (this, varargin); if (! isempty (errmsg)) error ("table.sortrows: %s", errmsg); endif tbl = subsetrows (this, index); endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} unique (@var{tblA}) ## @deftypefnx {table} {@var{tblB} =} unique (@var{tblA}, @var{setOrder}) ## @deftypefnx {table} {@var{tblB} =} unique (@var{tblA}, @var{occurrence}) ## @deftypefnx {table} {[@var{tblB}, @var{ixA}, @var{ixB}] =} unique (@dots{}) ## ## Unique rows in a table. ## ## @code{@var{tblB} = unique (@var{tblA})} returns the unique rows of table ## @var{tblA} in sorted order. A row holding a missing value ## (@qcode{NaN}, @qcode{NaT}, @qcode{} or @qcode{}) ## equals no other row, so each such row is kept; empty text is a value. ## ## @code{@var{tblB} = unique (@var{tblA}, @var{setOrder})} returns the ## unique rows of table @var{tblA} in a specified order. @var{setOrder} can ## be either @qcode{'sorted'} (default) or @qcode{'stable'}. ## ## @itemize ## @item @qcode{'sorted'} returns the unique rows sorted in ascending order. ## @item @qcode{'stable'} returns the unique rows according to their order ## of occurrence. ## @end itemize ## ## @code{@var{tblB} = unique (@var{tblA}, @var{occurrence})} returns the ## unique rows of table @var{tblA} according to their order of occurrence. ## @var{occurrence} can be either @qcode{'first'} (default) or ## @qcode{'last'}. ## ## @itemize ## @item @qcode{'first'} returns the first occurrence of each unique row, ## i.e. the lowest possible indices are returned. ## @item @qcode{'last'} returns the last occurrence of each unique row, i.e. ## the highest possible indices are returned. ## @end itemize ## ## @code{[@var{tblB}, @var{ixA}, @var{ixB}] = unique (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} using any of the previous syntaxes. ## @var{ixA} and @var{ixB} map the tables @var{tblA} and @var{tblB} to one ## another such that @qcode{@var{tblB} = @var{tblA}(@var{ixA},:)} and ## @qcode{@var{tblA} = @var{tblB}(@var{ixB},:)}. ## ## @end deftypefn function [tbl, ia, ic] = unique (this, varargin) [ia, ic, errmsg] = uniqueIndex (this, varargin); if (! isempty (errmsg)) error ("table.unique: %s", errmsg); endif tbl = subsetrows (this, ia); endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{TF} =} issortedrows (@var{tblA}) ## @deftypefnx {table} {@var{TF} =} issortedrows (@var{tblA}, @qcode{'RowNames'}) ## @deftypefnx {table} {@var{TF} =} issortedrows (@var{tblA}, @var{rowDimName}) ## @deftypefnx {table} {@var{TF} =} issortedrows (@var{tblA}, @var{vars}) ## @deftypefnx {table} {@var{TF} =} issortedrows (@var{tblA}, @dots{}, @var{direction}) ## @deftypefnx {table} {@var{TF} =} issortedrows (@dots{}, @var{Name}, @var{Value}) ## ## Check if table rows are sorted accordingly. ## ## @code{@var{TF} = issortedrows (@var{tblA})} determines if the rows in ## @var{tblA} are sorted in ascending order based on the values in the first ## variable or subsequent variables if elements of the former are repeated. ## @var{TF} is a logical scalar and it is @qcode{true} when ## @code{@var{tblA} == sortrows (@var{tblA})} or @qcode{false} otherwise. ## ## @code{@var{TF} = issortedrows (@var{tblA}, 'RowNames')} ## determines if the rows in @var{tblA} are sorted according to its row ## names. @var{TF} is @qcode{true} when @code{@var{tblA} == sortrows ## (@var{tblA}, 'RowNames')} or @qcode{false} otherwise. If ## @var{tblA} does not have row names, i.e. @qcode{tblA.Properties.RowNames} ## is empty, then @var{TF} is @qcode{true}. ## ## @code{@var{TF} = issortedrows (@var{tblA}, @var{rowDimName})} determines ## if the rows in table @var{tblA} are sorted along the first dimension, ## @var{rowDimName}, which is the equivalent to the previous syntax, i.e. ## according to its row names. For this syntax to work, @var{rowDimName} ## must match the first element in @qcode{tblA.Properties.DimensionNames}, ## otherwise @var{rowDimName} is considered a variable name, as in the ## following syntax. @var{TF} is @qcode{true} when @code{@var{tblA} == ## sortrows (@var{tblA}, @var{rowDimName})} or @qcode{false} otherwise. If ## @var{tblA} does not have row names, i.e. @qcode{tblA.Properties.RowNames} ## is empty, then @var{TF} is @qcode{true}. ## ## @code{@var{TF} = issortedrows (@var{tblA}, @var{vars})} determines if the ## rows in @var{tblA} are sorted by the elements in the variables specified ## by @var{vars}, which can be a character vector (for a single variable) or ## a cell array of character vectors or a string array (specifying a single ## or multiple variables). If @var{tblA} has row names, then @var{vars} can ## include the row names. Alternatively, @var{vars} can be a logical vector ## or a numeric vector of real integers indexing the desired variables. ## Positive integers specify an ascending order, whereas negative integers ## specify a descending order for the referenced variables. You can also ## index all available variables in @var{tblA} by passing a semicolon ## character argument. This Octave-specific syntax facilitates the use of ## the @var{direction} input argument when no particular variable needs to ## be selected to sort on. Additionally, @var{vars} can be a ## @qcode{vartype} object used to create a subscript that selects variables ## of a specified type. ## ## @code{@var{TF} = issortedrows (@var{tblA}, @dots{}, @var{direction})} ## determines if the rows in @var{tblA} are sorted in the order specified by ## @var{direction} for any of the previous syntaxes. @var{direction} can be ## @qcode{'ascend'} or @qcode{'descend'}, which is applied to all specified ## variables or row names that @code{sortrows} operates on. @var{direction} ## can also be a cell array of character vectors, whose elements are ## @qcode{'ascend'} and @qcode{'descend'}, where each element corresponds to ## the specified variables and/or row names used for sorting the table. ## The order specified by @var{direction} always takes precedence over the ## order defined by a numerical vector of integers in @var{vars}. ## @var{direction} must always be the 3rd input argument. If you want to ## omit passing selected variables and allow @code{sortrows} to work on ## consecutive variables until all ties are resolved, then you can leave the ## second input argument empty, as in ## @code{sortrows (@var{tblA}, @{[]@}, @var{direction})} or pass a ## colon argument for @var{vars} as in ## @code{sortrows (@var{tblA}, @{':'@}, @var{direction})}. ## ## @code{@var{TF} = issortedrows (@dots{}, @var{Name}, @var{Value})} ## determines if the rows in @var{tblA} are sorted according the additional ## parameters specifying the sorting of rows of a table with the following ## Name-Value paired arguments. ## ## @itemize ## @item @qcode{'MissingPlacement'} specifies the placement of missing ## values with one of the following options: @qcode{'auto'} places the ## missing elements at the bottom for ascending order and at the top for ## descending order; @qcode{'first'} places missing elements at the top; ## @qcode{'last'} places missing elements at the bottom. ## @item @qcode{'ComparisonMethod'} specifies the element comparison method ## with one of the following options: @qcode{'auto'} sorts rows using the ## real part for real numbers and the magnitude for complex numbers; ## @qcode{'real'} sorts rows using the real part for both real and complex ## numbers; @qcode{'abs'} sorts rows using the magnitude for both real and ## complex numbers. For complex numbers with equal magnitude, the phase ## angle in the interval @math{(-π, π]} is further used to break ties. ## @end itemize ## ## @end deftypefn function TF = issortedrows (this, varargin) [TF, errmsg] = issortedrowsCheck (this, varargin); if (! isempty (errmsg)) error ("table.issortedrows: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} topkrows (@var{tblA}, @var{k}) ## @deftypefnx {table} {@var{tblB} =} topkrows (@var{tblA}, @var{k}, @qcode{'RowNames'}) ## @deftypefnx {table} {@var{tblB} =} topkrows (@var{tblA}, @var{k}, @var{rowDimName}) ## @deftypefnx {table} {@var{tblB} =} topkrows (@var{tblA}, @var{k}, @var{vars}) ## @deftypefnx {table} {@var{tblB} =} topkrows (@var{tblA}, @var{k}, @dots{}, @var{direction}) ## @deftypefnx {table} {@var{tblB} =} topkrows (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {table} {[@var{tblB}, @var{index}] =} topkrows (@dots{}) ## ## Return the top rows of a table. ## ## @code{@var{tblB} = topkrows (@var{tblA}, @var{k})} returns the top ## @var{k} rows from table @var{tblA} sorted in descending order based on ## all of its variables. If elements in the first variable are repeated, ## then @code{topkrows} sorts by the elements in the second variable, and so ## on. ## ## @code{@var{tblB} = topkrows (@var{tblA}, @var{k}, 'RowNames')} returns ## the top @var{k} rows from table @var{tblA} sorted according to its row ## names. If @var{tblA} does not have row names, i.e. ## @qcode{tblA.Properties.RowNames} is empty, then it returns @var{tblA}. ## ## @code{@var{tblB} = topkrows (@var{tblA}, @var{k}, @var{rowDimName})} also ## returns the top @var{k} rows from table @var{tblA} sorted along its first ## dimension, @var{rowDimName}, which is the equivalent to the previous ## syntax, i.e. according to its row names. If @var{tblA} does not have row ## names, i.e. @qcode{tblA.Properties.RowNames} is empty, then it returns ## @var{tblA}. For this syntax to work, @var{rowDimName} must match the ## first element in @qcode{tblA.Properties.DimensionNames}, otherwise ## @var{rowDimName} is considered a variable name, as in the following ## syntax. ## ## @code{@var{tblB} = topkrows (@var{tblA}, @var{k}, @var{vars})} returns ## the top @var{k} rows from table @var{tblA} sorted by the elements in the ## variables specified by @var{vars}, which can be a character vector (for a ## single variable) or a cell array of character vectors or a string array ## (specifying a single or multiple variables). If @var{tblA} has row ## names, then @var{vars} can include the row names. Alternatively, ## @var{vars} can be a logical vector or a numeric vector of real integers ## indexing the desired variables. Unlike @code{sortrows}, positive ## integers specify a descending order, whereas negative integers specify an ## ascending order for the referenced variables, consistent with the ## descending default of @code{topkrows}. You can also index all available ## variables in @var{tblA} by passing a semicolon character argument. This ## Octave-specific syntax facilitates the use of the @var{direction} input ## argument when no particular variable needs to be selected to sort on. ## Additionally, @var{vars} can be a @qcode{vartype} object used to create a ## subscript that selects variables of a specified type. ## ## @code{@var{tblB} = topkrows (@var{tblA}, @var{k}, @dots{}, ## @var{direction})} returns the top @var{k} rows from table @var{tblA} ## sorted in the order specified by @var{direction} for any of the previous ## syntaxes. @var{direction} can be @qcode{'ascend'} or @qcode{'descend'}, ## which is applied to all specified variables or row names that ## @code{sortrows} operates on. @var{direction} can also be a cell array of ## character vectors, whose elements are @qcode{'ascend'} and ## @qcode{'descend'}, where each element corresponds to the specified ## variables and/or row names used for sorting the table. The order ## specified by @var{direction} always takes precedence over the order ## defined by a numerical vector of integers in @var{vars}. @var{direction} ## must always be the 3rd input argument. If you want to omit passing ## selected variables and allow @code{sortrows} to work on consecutive ## variables until all ties are resolved, then you can leave the second ## input argument empty, as in ## @code{sortrows (@var{tblA}, @{[]@}, @var{direction})} or pass a ## colon argument for @var{vars} as in ## @code{sortrows (@var{tblA}, @{':'@}, @var{direction})}. ## ## @code{@var{tblB} = topkrows (@dots{}, @var{k}, @var{Name}, @var{Value})} ## returns the top @var{k} rows from table @var{tblA} sorted with any of the ## previous syntaxes and further specified by additional parameters for ## sorting rows of a table with the following Name-Value paired arguments. ## ## @itemize ## @item @qcode{'MissingPlacement'} specifies the placement of missing ## values with one of the following options: @qcode{'auto'} places the ## missing elements at the bottom for ascending order and at the top for ## descending order; @qcode{'first'} places missing elements at the top; ## @qcode{'last'} places missing elements at the bottom. ## @item @qcode{'ComparisonMethod'} specifies the element comparison method ## with one of the following options: @qcode{'auto'} sorts rows using the ## real part for real numbers and the magnitude for complex numbers; ## @qcode{'real'} sorts rows using the real part for both real and complex ## numbers; @qcode{'abs'} sorts rows using the magnitude for both real and ## complex numbers. For complex numbers with equal magnitude, the phase ## angle in the interval @math{(-π, π]} is further used to break ties. ## @end itemize ## ## @code{[@var{tblB}, @var{index}] = topkrows (@dots{})} also returns an ## index vector such that @qcode{@var{tblB} = @var{tblA}(@var{index},:)}. ## ## @end deftypefn function [tbl, ix] = topkrows (this, k, varargin) [ix, errmsg] = topkrowsIndex (this, k, varargin); if (! isempty (errmsg)) error ("table.topkrows: %s", errmsg); endif tbl = subsetrows (this, ix); endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} addvars (@var{tblA}, @var{var1}, @dots{}, @var{varN}) ## @deftypefnx {table} {@var{tblB} =} addvars (@dots{}, @qcode{'After'}, @var{location}) ## @deftypefnx {table} {@var{tblB} =} addvars (@dots{}, @qcode{'Before'}, @var{location}) ## @deftypefnx {table} {@var{tblB} =} addvars (@dots{}, @qcode{'NewVariableNames'}, @var{newNames}) ## ## Add new variables to a table. ## ## @code{@var{tblB} = addvars (@var{tblA}, @var{var1}, @dots{}, @var{varN})} ## adds new variables to the right of the last variable in table @var{tblA}. ## Each of the arrays specified by the input arguments @qcode{@var{var1}, ## @dots{}, @var{varN}} becomes a new variable and its name is derived from ## the input argument's variable name or a default is created if the input ## argument is not a variable itself. The input arrays can be of any data ## type including a table as long as they have the same number of rows as ## @var{tblA}. ## ## @code{@var{tblB} = addvars (@dots{}, @code{'After'}, @var{location})} ## adds the new variables after (i.e. to the right of) the table variable ## specified in @var{location}, which can be a character vector, a string ## scalar, a scalar integer value, or even a logical vector with ## @qcode{width (@var{tblA})} elements, as long as it indexes a single ## variable in @var{tblA}. ## ## @code{@var{tblB} = addvars (@dots{}, @code{'Before'}, @var{location})} ## adds the new variables before (i.e. to the left of) the table variable ## specified in @var{location}, which can be a character vector, a string ## scalar, a scalar integer value, or even a logical vector with ## @qcode{width (@var{tblA})} elements, as long as it indexes a single ## variable in @var{tblA}. ## ## @code{@var{tblB} = addvars (@dots{}, @code{'NewVariableNames'}, ## @var{newNames})} renames the new variables added from the previous ## syntaxes according to the names specified by @var{newNames}, which can be ## a character vector, a cell array of character vectors or a string array. ## The number of names in @var{newNames} must be the same as the number of ## added variables. ## ## @end deftypefn function tbl = addvars (this, varargin) ## Only the public method can read the caller's names. argNames = cell (1, numel (varargin)); for i = 1:numel (varargin) argNames{i} = inputname (i + 1); endfor [tbl, errmsg] = addvarsResult (this, argNames, varargin{:}); if (! isempty (errmsg)) error ("table.addvars: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} renamevars (@var{tblA}, @var{vars}, @var{newNames}) ## ## Rename variables in a table. ## ## @code{@var{tblB} = renamevars (@var{tblA}, @var{vars}, @var{newNames})} ## renames the selected variables in the table @var{tblA} specified by ## @var{vars} using the names in @var{newNames}. ## ## @var{vars} can be any of the following types. ## @itemize ## @item a character vector specifying a single variable. ## @item a cell array of character vectors specifying a single or multiple ## variables. ## @item a string array specifying a single or multiple variables. ## @item a numeric array of integer values indexing the variables to be ## renamed. ## @item a logical vector of the same length as the width of the table ## @var{tblA} indexing as @qcode{true} the variables to be renamed. ## @item a @qcode{vartype} object used to create a subscript that selects ## variables of a specified type. ## @end itemize ## ## @var{newNames} can either be a character vector (when renaming a single ## variable) or a cell array of character vectors or a string array. The ## number of names specified by @var{newNames} must match the number of ## variables specified by @var{vars}. ## ## @end deftypefn function tbl = renamevars (this, varargin) [tbl, errmsg] = renamevarsResult (this, varargin{:}); if (! isempty (errmsg)) error ("table.renamevars: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} movevars (@var{tblA}, @var{vars}) ## @deftypefnx {table} {@var{tblB} =} movevars (@dots{}, @qcode{'After'}, @var{location}) ## @deftypefnx {table} {@var{tblB} =} movevars (@dots{}, @qcode{'Before'}, @var{location}) ## ## Move variables in a table. ## ## @code{@var{tblB} = movevars (@var{tblA}, @var{vars})} moves the variables ## specified by @var{vars} to the end of the input table @var{tblA}. ## ## @var{vars} can be any of the following types. ## @itemize ## @item a character vector specifying a single variable. ## @item a cell array of character vectors specifying a single or multiple ## variables. ## @item a string array specifying a single or multiple variables. ## @item a numeric array of integer values indexing the variables to be ## moved. ## @item a logical vector of the same length as the width of the table ## @var{tblA} indexing as @qcode{true} the variables to be moved. ## @item a @qcode{vartype} object used to create a subscript that selects ## variables of a specified type. ## @end itemize ## ## @code{@var{tblB} = movevars (@dots{}, @code{'After'}, @var{location})} ## moves the selected variables after (i.e. to the right of) the table ## variable specified in @var{location}, which can be a character vector, a ## string scalar, a scalar integer value, or even a logical vector with ## @qcode{width (@var{tblA})} elements, as long as it indexes a single ## variable in @var{tblA} which is not selected by @var{vars}. ## ## @code{@var{tblB} = movevars (@dots{}, @code{'Before'}, @var{location})} ## moves the selected variables before (i.e. to the left of) the table ## variable specified in @var{location}, which can be a character vector, a ## string scalar, a scalar integer value, or even a logical vector with ## @qcode{width (@var{tblA})} elements, as long as it indexes a single ## variable in @var{tblA} which is not selected by @var{vars}. ## ## @end deftypefn function tbl = movevars (this, varargin) [tbl, errmsg] = movevarsResult (this, varargin{:}); if (! isempty (errmsg)) error ("table.movevars: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} removevars (@var{tblA}, @var{vars}) ## ## Remove variables from a table. ## ## @code{@var{tblB} = removevars (@var{tblA}, @var{vars})} removes the ## variables specified by @var{vars} from the input table @var{tblA}. ## ## @var{vars} can be any of the following types. ## @itemize ## @item a character vector specifying a single variable. ## @item a cell array of character vectors specifying a single or multiple ## variables. ## @item a string array specifying a single or multiple variables. ## @item a numeric array of integer values indexing the variables to be ## removed. ## @item a logical vector of the same length as the width of the table ## @var{tblA} indexing as @qcode{true} the variables to be removed. ## @item a @qcode{vartype} object used to create a subscript that selects ## variables of a specified type. ## @end itemize ## ## @end deftypefn function tbl = removevars (this, varargin) [tbl, errmsg] = removevarsResult (this, varargin{:}); if (! isempty (errmsg)) error ("table.removevars: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} splitvars (@var{tblA}) ## @deftypefnx {table} {@var{tblB} =} splitvars (@var{tblA}, @var{vars}) ## @deftypefnx {table} {@var{tblB} =} splitvars (@dots{}, @qcode{'NewVariableNames'}, @var{NewNames}) ## ## Split multicolumn variables in a table. ## ## @code{@var{tblB} = splitvars (@var{tblA})} splits multicolumn variables ## in @var{tblA} so that they are single-column variables in @var{tblB}, ## while all single-column variables in @var{tblA} are copied to @var{tblB} ## unaltered. Each newly created single-column variable in @var{tblB} is ## uniquely named by joining the name of its parent multicolumn variable in ## @var{tblA} and the corresponding column number. If a variable in ## @var{tblA} contains a table, then each variable of this nested table is ## returned as a newly created variable in @var{tblB}. By default, these ## variables retain their original name in the nested table, unless there ## are duplicate names, in which case the name of the nested table is also ## used. If the nested table in @var{tblA} contains a multicolumn variable, ## then the newly created variable in @var{tblB} is also multicolumnar. ## ## @code{@var{tblB} = splitvars (@var{tblA}, @var{vars})} splits only the ## variables in @var{tblA} specified by @var{vars}. If left empty, it ## defaults to all variables that can be split. Single-column variables ## specified in @var{vars} are copied unaltered. ## ## @var{vars} can be any of the following types. ## @itemize ## @item a character vector specifying a single variable. ## @item a cell array of character vectors specifying a single or multiple ## variables. ## @item a string array specifying a single or multiple variables. ## @item a numeric array of integer values indexing the variables to be ## split. ## @item a logical vector of the same length as the width of the table ## @var{tblA} indexing as @qcode{true} the variables to be split. ## @item a @qcode{vartype} object used to create a subscript that selects ## variables of a specified type. ## @end itemize ## ## @code{@var{tblB} = splitvars (@dots{}, @qcode{'NewVariableNames'}, ## @var{NewNames})} assigns new names to the variables that are split out of ## @var{tblA} and copied to @var{tblB}. @var{NewNames} can be specified as ## a cell array of character vectors and/or string arrays. ## ## @end deftypefn function tbl = splitvars (this, varargin) [tbl, errmsg] = splitvarsResult (this, varargin{:}); if (! isempty (errmsg)) error ("table.splitvars: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} mergevars (@var{tblA}, @var{vars}) ## @deftypefnx {table} {@var{tblB} =} mergevars (@var{tblA}, @var{vars}, @var{Name}, @var{Value}) ## ## Merge table variables into a single multicolumn variable. ## ## @code{@var{tblB} = mergevars (@var{tblA}, @var{vars})} combines the table ## variables in @var{tblA} specified by @var{vars} to create a new ## multicolumn variable in @var{tblB}. All other variables in @var{tblA} ## are copied to @var{tblB} unaltered. By default, the name of the merged ## variable in @var{tblB} takes the form @math{VarN}, where @math{N} is the ## position of the first variable in @var{tblA} among those to be merged, ## which is also the location of the merged variable in @var{tblB}. ## ## Note that merging variables with a @qcode{'string'} data type variable ## will result in a multicolumn variable of @qcode{'string'} data type, by ## initially converting all other to-be-merged variables into ## @qcode{'string'} data type. ## ## @var{vars} can be any of the following types. ## @itemize ## @item a character vector specifying a single variable. ## @item a cell array of character vectors specifying a single or multiple ## variables. ## @item a string array specifying a single or multiple variables. ## @item a numeric array of integer values indexing the variables to be ## merged. ## @item a logical vector of the same length as the width of the table ## @var{tblA} indexing as @qcode{true} the variables to be merged. ## @item a @qcode{vartype} object used to create a subscript that selects ## variables of a specified type. ## @end itemize ## ## @code{@var{tblB} = mergevars (@dots{}, @var{Name}, @var{Value})} further ## specifies additional parameters for merging table variables with the ## following Name-Value paired arguments. ## ## @itemize ## @item @qcode{'NewVariableName'} specifies the name of the merged variable ## in @var{tblB}, which must be unique. @qcode{'NewVariableName'} must be ## either a cellstr or string scalar or a character vector. ## @item @qcode{'MergeAsTable'} specifies whether the selected variables ## should be merged into a multicolumn variable (default) or into a table ## nested into a variable, which is useful for variables that cannot be ## concatenated due to incompatible variable types. @qcode{'MergeAsTable'} ## must be either a boolean scalar or a numeric scalar value of @qcode{1} ## (@qcode{true}) or @qcode{0} (@qcode{false}). ## @end itemize ## ## Nothing the merged variables carried about themselves survives: one ## variable carries one unit, one description, one continuity and one ## entry of a custom property describing the variables, and there is no ## saying which of them the merged variable should take, so it takes ## none: where the continuity is set at all, the merged variable's ## reads @qcode{'unset'}, and a property that was never set stays ## unset. The remaining variables keep theirs. ## ## @end deftypefn function tbl = mergevars (this, varargin) [tbl, errmsg] = mergevarsResult (this, varargin{:}); if (! isempty (errmsg)) error ("table.mergevars: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} convertvars (@var{tblA}, @var{vars}, @var{dataType}) ## ## Convert table variables to specified data type. ## ## @code{@var{tblB} = convertvars (@var{tblA}, @var{vars}, @var{dataType})} ## converts the variables in @var{tblA} specified by @var{vars} to the ## specified data type. ## ## @var{vars} can be any of the following types. ## @itemize ## @item a character vector specifying a single variable. ## @item a cell array of character vectors specifying a single or multiple ## variables. ## @item a string array specifying a single or multiple variables. ## @item a numeric array of integer values indexing the variables to be ## converted. ## @item a logical vector of the same length as the width of the table ## @var{tblA} indexing as @qcode{true} the variables to be converted. ## @item a @qcode{vartype} object used to create a subscript that selects ## variables of a specified type. ## @end itemize ## ## @var{dataType} specifies the data type to convert those variables to. It ## can either be a character vector defining the name of the data type to ## convert to or a function handle, which will perform the conversion. ## When specifying a name for data type conversion, ## it can either be a one-argument constructor for the specified data type, ## which must accept the selected variables' current data types as input, or ## an available method, which can be applied on selected variables' current ## data types. When specifying a function handle for applying a conversion ## on selected variables, this function handle must accept a single input ## argument and return in its output the same rows as the input argument. ## ## Either way, each resulting variable must have the same number of rows as ## the respective variable selected for conversion. However, depending on ## the chosen type of conversion, the columns of the converted variable(s) ## might differ. It is up to the user to ensure that the appropriate type ## of conversion is performed. @code{convertvars} only checks the custom ## function handles for returning the correct number of rows, which must ## equal the number of rows of the input table, @var{tblA}. ## ## @end deftypefn function tbl = convertvars (this, varargin) [tbl, errmsg] = convertvarsResult (this, varargin{:}); if (! isempty (errmsg)) error ("table.convertvars: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} rows2vars (@var{tblA}) ## @deftypefnx {table} {@var{tblB} =} rows2vars (@var{tblA}, @var{Name}, @var{Value}) ## ## Reorient table by swapping rows into variables. ## ## @code{@var{tblB} = rows2vars (@var{tblA})} reorients the input table ## @var{tblA} so that its rows become variables in the output table ## @var{tblB} and the variables are swapped into rows and their names ## are stored into a new variable at the beginning of the output table. If ## the contents of @var{tblA} can be concatenated, then the corresponding ## variables of @var{tblB} are arrays, otherwise they are cell arrays. If ## the input table @var{tblA} contains @qcode{RowNames}, then those names ## become the variable names of the output table @var{tblB}, otherwise the ## variable names of @var{tblB} are generated automatically. ## @code{rows2vars} cannot handle multicolumn variables or nested tables. ## ## @code{@var{tblB} = rows2vars (@dots{}, @var{Name}, @var{Value})} further ## specifies additional parameters for reorienting the table with the ## following Name-Value paired arguments. ## ## @itemize ## @item @qcode{'DataVariables'} specifies the variables from input table ## @var{tblA} which will be reoriented. @qcode{'DataVariables'} can be any ## of the following types: a character vector specifying a single variable; ## a cell array of character vectors or a string array specifying a single ## or multiple variables; a numeric array of integer values specifying a ## single or multiple variables; a logical vector of the same length as the ## width of the input table specifying a single or multiple variables. ## @item @qcode{'VariableNamesSource'} specifies a single variable that ## contains the variable names for the output table. The values of the ## selected variable must have a data type which can be converted to strings ## and the number of unique names in the selected variable must match the ## number of rows of the input table. @qcode{'VariableNamesSource'} accepts ## the same data types supported by @qcode{'DataVariables'} as long as they ## index a single variable, which, however, must not be specified by the ## @qcode{'DataVariables'} Name-Value paired argument. ## @item @qcode{'VariableNamingRule'} must be a character vector specifying ## the rule for naming variables in the output table @var{tblB}. When set ## to @qcode{'modify'} (default), the variable names are modified so that ## they are valid variable identifiers. When set to @qcode{'preserve'}, the ## original names are preserved. ## @end itemize ## ## @end deftypefn function tbl = rows2vars (this, varargin) [tbl, errmsg] = rows2varsResult (this, varargin); if (! isempty (errmsg)) error ("table.rows2vars: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} stack (@var{tblA}, @var{vars}) ## @deftypefnx {table} {@var{tblB} =} stack (@var{tblA}, @{@var{vars1}, @dots{}, @var{varsN}@}) ## @deftypefnx {table} {@var{tblB} =} stack (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {table} {[@var{tblB}, @var{idxA}] =} stack (@dots{}) ## ## Stack multiple table variables into a single table variable. ## ## @code{@var{tblB} = stack (@var{tblA}, @var{vars})} stacks the values from ## the variables @var{vars} in input @var{tblA} into a single variable in ## output table @var{tblB}. By default, the stacked variable in @var{tblB} ## is named by joining the names of the variables in @var{tblA} as defined ## by @var{vars}, and it inherits the units and description of the first ## variable in @var{vars}. Additionally, a new categorical variable is ## included in @var{tblB} that indicates which variable in @var{tblA} the ## stacked data in each row of @var{tblB} comes from. By default, this ## categorical variable is named by appending @qcode{'_Indicator'} to the ## name of the stacked variable. Variables in @var{tblA} that are not ## defined in @var{vars} for stacking are replicated in @var{tblB}. If ## @var{tblA} contains @qcode{RowNames}, these are not stacked. ## ## The stacked variables are joined as @code{table2array} joins them, and ## must each have one column; variables that cannot join, such as a ## @code{categorical} and a numeric variable, are refused. ## ## @code{@var{tblB} = stack (@var{tblA}, @{@var{vars1}, @dots{}, ## @var{varsN}@})} stacks multiple groups of variables, given as a cell ## array of variable references, producing one stacked data variable in ## @var{tblB} per group (each named and metadata-inherited from its own ## group). All groups must contain the same number of variables. In this ## case a single indicator variable, named @qcode{'Indicator'} by default, ## holds the numeric position within each group of the source variable for ## each stacked value. ## ## @var{vars} can be any of the following types. ## @itemize ## @item a character vector specifying a single variable. ## @item a cell array of character vectors specifying a single or multiple ## variables. ## @item a string array specifying a single or multiple variables. ## @item a numeric array of integer values indexing the variables to be ## stacked. ## @item a logical vector of the same length as the width of the table ## @var{tblA} indexing as @qcode{true} the variables to be stacked. ## @item a @qcode{vartype} object used to create a subscript that selects ## variables of a specified type. ## @end itemize ## ## @code{@var{tblB} = stack (@dots{}, @var{Name}, @var{Value})} further ## specifies additional parameters for stacking table variables with the ## following Name-Value paired arguments. ## ## @itemize ## @item @qcode{'ConstantVariables'} specifies the variables other than ## @var{vars} to include in the output table. By default, all remaining ## variables not specified by @var{vars} are included in the output table. ## Specifying @qcode{'ConstantVariables'} allows you to select specific ## variables to replicate in @var{tblB}. Row names in @var{tblA} are always ## replicated in @var{tblB}. You can specify @qcode{'ConstantVariables'} in ## the same manner as with @var{vars}. ## @item @qcode{'NewDataVariableName'} specifies the name for the new data ## variable in the output table @var{tblB}. It can be a character vector, ## a string scalar, or a cellstring scalar. ## @item @qcode{'IndexVariableName'} specifies the name for the new ## indicator variable in the output table @var{tblB}. It can be a character ## vector, a string scalar, or a cellstring scalar. ## @end itemize ## ## @code{[@var{tblB}, @var{idxA}] = stack (@dots{})} also returns an index ## vector, @var{idxA}, indicating the correspondence between the rows in ## @var{tblB} and the rows in @var{tblA}. ## ## @end deftypefn function [tbl, idxA] = stack (this, vars, varargin) if (nargin < 2) vars = []; endif [tbl, idxA, errmsg] = stackResult (this, vars, varargin); if (! isempty (errmsg)) error ("table.stack: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} unstack (@var{tblA}, @var{vars}, @var{ivar}) ## @deftypefnx {table} {@var{tblB} =} unstack (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {table} {[@var{tblB}, @var{idxA}] =} unstack (@dots{}) ## ## Unstack a single table variable into multiple table variables. ## ## @code{@var{tblB} = unstack (@var{tblA}, @var{vars}, @var{ivar})} unstacks ## the values from the variables @var{vars} according to the indicator ## variable @var{ivar} in input @var{tblA} into multiple variables in output ## table @var{tblB}. The new (unstacked) variables in @var{tblB} are named ## according to the unique values of the indicator variable and the rows ## with matching indicator values are aggregated into the new (unstacked) ## variables. By default, @qcode{numeric} and @qcode{duration} data types ## are aggregated by summation, whereas from other data types the first ## unique element of each group is returned. ## ## @var{vars} may specify one or more variables of any data type supported ## by the @qcode{table} class except for nested tables, whereas @var{ivar} ## must only specify a single variable, which must be numeric, logical, ## categorical, string, or cellstring. Both @var{vars} and @var{ivar} can ## be specified as follows: ## @itemize ## @item a character vector specifying a single variable. ## @item a cell array of character vectors specifying a single or multiple ## variables. ## @item a string array specifying a single or multiple variables. ## @item a numeric array of integer values indexing the variables to be ## unstacked. ## @item a logical vector of the same length as the width of the table ## @var{tblA} indexing as @qcode{true} the variables to be unstacked. ## @item a @qcode{vartype} object used to create a subscript that selects ## variables of a specified type. ## @end itemize ## ## By default, all remaining variables in @var{tblA} which are not specified ## by @var{vars} and @var{ivar} are treated as grouping variables, in which ## case each unique combination of values in the grouping variables ## identifies a group of rows in @var{tblA} that is unstacked into one row ## of @var{tblB}. ## ## @code{@var{tblB} = unstack (@dots{}, @var{Name}, @var{Value})} further ## specifies additional parameters for unstacking table variables with the ## following Name-Value paired arguments. ## ## @itemize ## @item @qcode{'GroupingVariables'} specifies the variables that should be ## used as grouping variables. All valid schemes for indexing a table ## variable can be used. If grouping variables have missing values, the ## data from corresponding rows are not aggregated in the output table. ## Table row names cannot be assigned as a grouping variable, since these ## must be unique for each row, which would defeat the purpose of unstacking ## a table onto itself. ## @item @qcode{'ConstantVariables'} specifies the variables that are ## constant within each group. All valid schemes for indexing a table ## variable can be used. The values for these variables in the output are ## taken from the first row in each group in the input. By default, no ## variable is treated as constant unless specified. However, if the input ## table has row names, these effectively are treated as constant variables. ## @item @qcode{'NewDataVariableNames'} specifies the names for the new data ## variables in the output table @var{tblB}. It can be a character vector, ## a string scalar, or a cellstring scalar. By default, the names of the ## new unstacked data variables are based on the string representation of ## the unique values in the indicator variable @var{ivar}. If multiple ## variables are unstacked, then @code{unstack} generates composite names ## using both the values from the indicator variable and the name of the ## variable being unstacked. The number of names must match the number of ## unique values in the indicator variable. ## @item @qcode{'AggregationFunction'} specifies a function handle used to ## aggregate each group's data into a single value. By default, ## @code{@@sum} is applied on numeric data, whereas @code{@@unique} is ## applied on all other supported data types, including @code{duration} and ## @code{calendarDuration}. In the latter case, if a group contains more ## than one distinct value for the same indicator value, the default ## aggregation errors, and an explicit @qcode{'AggregationFunction'} that ## returns a scalar must be specified. ## @item @qcode{'VariableNamingRule'}, specified as either @qcode{'modify'} ## or @qcode{'preserve'}, defines the rule for naming the new unstacked ## variables in the output table @var{tblB}. @qcode{'modify'} (default) ## forces all variable names to be valid Octave variable names. ## @qcode{'preserve'} preserves the original names taken from the input ## table, which can have any Unicode characters, including spaces and ## non-ASCII characters. ## @end itemize ## ## @code{[@var{tblB}, @var{idxA}] = unstack (@dots{})} also returns an index ## vector, @var{idxA}, indicating the correspondence between the rows in ## @var{tblB} and the rows in @var{tblA}. ## ## @end deftypefn function [tbl, idxA] = unstack (this, vars, ivar, varargin) if (nargin < 3) vars = []; ivar = []; endif [tbl, idxA, errmsg] = unstackResult (this, vars, ivar, varargin); if (! isempty (errmsg)) error ("table.unstack: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} inner2outer (@var{tblA}) ## ## Invert the nested hierarchy of nested tables in a table. ## ## @code{@var{tblB} = inner2outer (@var{tblA})} finds the variables in ## @var{tblA} that are themselves tables (nested tables) and returns a table ## @var{tblB} in which the inner and outer levels of nesting are transposed. ## The variables of the nested tables in @var{tblA} become the variables of ## @var{tblB}, and the variables of @var{tblA} that contain the nested ## tables become the variables of the nested tables in @var{tblB}. Any ## variable in @var{tblA} that is not a nested table is copied unaltered ## into @var{tblB}. ## ## For example, if @var{tblA} has two variables @var{A} and @var{B} that ## both contain nested tables with the variables @var{X} and @var{Y}, then ## @var{tblB} has two variables @var{X} and @var{Y}, each containing a ## nested table with the variables @var{A} and @var{B}. As a result, the ## table variables @code{tblA.A.X} and @code{tblA.B.X} are regrouped into ## @code{tblB.X.A} and @code{tblB.X.B}, while @code{tblA.A.Y} and ## @code{tblA.B.Y} are regrouped into @code{tblB.Y.A} and @code{tblB.Y.B}. ## ## The new variables of @var{tblB} are the union of the variable names of ## the nested tables in @var{tblA}, placed at the position of the first ## nested table. An inner variable name shared by more than one nested ## table becomes a nested table in @var{tblB} grouping the corresponding ## variables; an inner variable name held by a single nested table becomes ## a plain variable carrying that column. ## ## @end deftypefn function tbl = inner2outer (this) [tbl, errmsg] = inner2outerResult (this); if (! isempty (errmsg)) error ("table.inner2outer: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{T} =} addprop (@var{T}, @var{propertyNames}, @var{propertyTypes}) ## ## Add custom properties to a table. ## ## @code{@var{T} = addprop (@var{T}, @var{propertyNames}, ## @var{propertyTypes})} adds properties that contain custom metadata to the ## table @var{T}. The input argument @var{propertyNames} specifies the ## names of the custom properties to be added and @var{propertyTypes} the ## type of each corresponding custom property, that is whether the metadata ## values contained in the property apply to table @var{T} as a whole, or ## to the variables of @var{T}. Both @var{propertyNames} and ## @var{propertyTypes} can be character vectors, cell arrays of character ## vectors, or strings. When defined as cell arrays of character vectors or ## strings, they must have the same number of elements. ## ## Valid @var{propertyTypes} are either @qcode{'table'} or ## @qcode{'variable'}. When defined as @qcode{'table'}, the custom property ## can contain any value of any type and size, which applies as metadata to ## the table as a whole and is stored exactly as it is given. When defined ## as @qcode{'variable'}, the custom property contains a vector with one ## element per variable in the table. ## ## A @qcode{'variable'} property is cleared by assigning an empty 0-by-0 ## value, such as @code{[]} or @code{@{@}}, whatever the width of the ## table; empty values of any other size are not accepted. A character ## vector is not a valid value for a @qcode{'variable'} property: use a ## cell array of character vectors or a string array instead. ## ## After adding custom properties using @code{addprop}, metadata values can ## be assigned to the properties using dot syntax. ## ## @end deftypefn function tbl = addprop (this, Names, Types) if (nargin < 3) error ("table.addprop: too few input arguments."); endif [tbl, errmsg] = addpropResult (this, Names, Types); if (! isempty (errmsg)) error ("table.addprop: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{T} =} rmprop (@var{T}, @var{propertyNames}) ## ## Remove custom properties from a table. ## ## @code{@var{T} = rmprop (@var{T}, @var{propertyNames})} removes properties ## that contain custom metadata from the table @var{T}. The input argument ## @var{propertyNames} specifies the names of the custom properties to be ## removed and it can either be a character vector, a cell array of ## character vectors, or a string array. Names that do not match any ## existing custom property are silently ignored. ## ## @end deftypefn function tbl = rmprop (this, Names) if (nargin < 2) error ("table.rmprop: too few input arguments."); endif [tbl, errmsg] = rmpropResult (this, Names); if (! isempty (errmsg)) error ("table.rmprop: %s", errmsg); endif endfunction endmethods ################################################################################ ## ** Join and Set Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'join' 'innerjoin' 'outerjoin' 'union' ## ## 'intersect' 'ismember' 'setdiff' 'setxor' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {table} {@var{tbl} =} join (@var{tblL}, @var{tblR}) ## @deftypefnx {table} {@var{tbl} =} join (@var{tblL}, @var{tblR}, @var{Name}, @var{Value}) ## @deftypefnx {table} {[@var{tbl}, @var{ixR}] =} join (@dots{}) ## ## Combine two tables by rows using key variables. ## ## @code{@var{tbl} = join (@var{tblL}, @var{tblR})} combines @var{tblL} and ## @var{tblR} by matching the values of their @emph{key variables}, which by ## default are the variables that share the same name in both tables. ## @var{tbl} contains one row for each row of @var{tblL}, in the same order; ## each is completed with the single row of @var{tblR} whose key variables ## match. The key variables of @var{tblR} must contain unique combinations ## of values, and every key combination in @var{tblL} must be present in ## @var{tblR}. ## ## A key holding a missing value (@qcode{NaN}, @qcode{NaT}, ## @qcode{} or @qcode{}) matches no other key. Empty ## text, an empty character vector in a cellstr or a blank row of a ## character matrix, is a value and matches empty text, where MATLAB ## matches it with nothing; @code{ismissing} still flags it as missing. ## ## By default @var{tbl} contains all the variables of @var{tblL} followed by ## the non-key variables of @var{tblR}. Whenever a non-key variable name ## appears in both tables, a suffix derived from each input's argument name ## is appended to the conflicting names (for inputs named @var{tblL} and ## @var{tblR}, the suffixes @qcode{'_tblL'} and @qcode{'_tblR'}; when an ## input has no name, @qcode{'_left'} and @qcode{'_right'} are used). ## When both inputs have the same name, as when a table is joined with ## itself, the left names are numbered as well (@qcode{'_T_1'} beside ## @qcode{'_T'}), as in MATLAB. ## The row names of @var{tblL}, if any, are preserved. ## ## @code{@var{tbl} = join (@var{tblL}, @var{tblR}, @var{Name}, @var{Value})} ## customizes the join with the following options: ## ## @table @asis ## @item @qcode{'Keys'} ## Variables to use as keys in both tables, given as variable names or ## indices. It cannot be combined with @qcode{'LeftKeys'} or ## @qcode{'RightKeys'}. ## ## @item @qcode{'LeftKeys'}, @qcode{'RightKeys'} ## Variables to use as keys in @var{tblL} and @var{tblR}, respectively, when ## the key variables have different names. They must be specified together ## and reference the same number of variables. ## ## @item @qcode{'LeftVariables'}, @qcode{'RightVariables'} ## Variables of @var{tblL} and @var{tblR} to include in @var{tbl}. By ## default @qcode{'LeftVariables'} is all the variables of @var{tblL} and ## @qcode{'RightVariables'} is the non-key variables of @var{tblR}. ## ## @item @qcode{'KeepOneCopy'} ## Names of non-key variables that occur in both tables for which only the ## copy from @var{tblL} is kept (no suffix is added and the @var{tblR} copy ## is dropped). ## @end table ## ## @code{[@var{tbl}, @var{ixR}] = join (@dots{})} also returns the index ## vector @var{ixR} that identifies, for each row of @var{tbl}, the matching ## row of @var{tblR}. ## ## @end deftypefn function [tbl, ixR] = join (tblL, tblR, varargin) if (nargin < 2) error ("table.join: too few input arguments."); endif ## The caller's own names for the operands are read here, before ## the shared body, which cannot see them. [tbl, ixR, errmsg] = joinResult (tblL, tblR, varargin, ... inputname (1), inputname (2)); if (! isempty (errmsg)) error ("table.join: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tbl} =} innerjoin (@var{tblL}, @var{tblR}) ## @deftypefnx {table} {@var{tbl} =} innerjoin (@var{tblL}, @var{tblR}, @var{Name}, @var{Value}) ## @deftypefnx {table} {[@var{tbl}, @var{ixL}, @var{ixR}] =} innerjoin (@dots{}) ## ## Inner join between two tables by rows using key variables. ## ## @code{@var{tbl} = innerjoin (@var{tblL}, @var{tblR})} combines the tables ## @var{tblL} and @var{tblR} by matching the values of their @emph{key ## variables}, which by default are the variables that share the same name ## in both tables. Each row of @var{tbl} is formed by horizontally ## concatenating a row of @var{tblL} with a row of @var{tblR} whose key ## variables share the same combination of values. If @math{m} rows in ## @var{tblL} and @math{n} rows in @var{tblR} share the same key ## combination, then @var{tbl} contains all @math{m*n} pairings for that ## combination. The rows of @var{tbl} are sorted by the values of the key ## variables, and any row names are dropped. ## ## A key holding a missing value (@qcode{NaN}, @qcode{NaT}, ## @qcode{} or @qcode{}) matches no other key. Empty ## text, an empty character vector in a cellstr or a blank row of a ## character matrix, is a value and matches empty text, where MATLAB ## matches it with nothing; @code{ismissing} still flags it as missing. ## ## By default @var{tbl} contains all the variables of @var{tblL} followed by ## the non-key variables of @var{tblR}. Whenever a non-key variable name ## appears in both tables, a suffix derived from each input's argument name ## is appended to the conflicting names in @var{tbl} (for inputs named ## @var{tblL} and @var{tblR}, the suffixes @qcode{'_tblL'} and ## @qcode{'_tblR'}; when an input has no name, @qcode{'_left'} and ## @qcode{'_right'} are used). ## When both inputs have the same name, as when a table is joined with ## itself, the left names are numbered as well (@qcode{'_T_1'} beside ## @qcode{'_T'}), as in MATLAB. ## ## @code{@var{tbl} = innerjoin (@var{tblL}, @var{tblR}, @var{Name}, ## @var{Value})} customizes the join with the following options: ## ## @table @asis ## @item @qcode{'Keys'} ## Variables to use as keys in both tables, given as variable names or ## indices. It cannot be combined with @qcode{'LeftKeys'} or ## @qcode{'RightKeys'}. ## ## @item @qcode{'LeftKeys'}, @qcode{'RightKeys'} ## Variables to use as keys in @var{tblL} and @var{tblR}, respectively, when ## the key variables have different names. They must be specified together ## and reference the same number of variables. ## ## @item @qcode{'LeftVariables'}, @qcode{'RightVariables'} ## Variables of @var{tblL} and @var{tblR} to include in @var{tbl}. They may ## include or exclude key variables. By default @qcode{'LeftVariables'} is ## all the variables of @var{tblL} and @qcode{'RightVariables'} is the ## non-key variables of @var{tblR}. ## @end table ## ## @code{[@var{tbl}, @var{ixL}, @var{ixR}] = innerjoin (@dots{})} also ## returns the row-index vectors @var{ixL} and @var{ixR} such that @var{tbl} ## is the horizontal concatenation of @code{@var{tblL}(@var{ixL}, leftVars)} ## and @code{@var{tblR}(@var{ixR}, rightVars)}. ## ## @end deftypefn function [tbl, ixL, ixR] = innerjoin (tblL, tblR, varargin) if (nargin < 2) error ("table.innerjoin: too few input arguments."); endif [tbl, ixL, ixR, errmsg] = innerjoinResult (tblL, tblR, ... varargin, inputname (1), ... inputname (2)); if (! isempty (errmsg)) error ("table.innerjoin: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tbl} =} outerjoin (@var{tblL}, @var{tblR}) ## @deftypefnx {table} {@var{tbl} =} outerjoin (@var{tblL}, @var{tblR}, @var{Name}, @var{Value}) ## @deftypefnx {table} {[@var{tbl}, @var{ixL}, @var{ixR}] =} outerjoin (@dots{}) ## ## Outer join between two tables by rows using key variables. ## ## @code{@var{tbl} = outerjoin (@var{tblL}, @var{tblR})} combines the tables ## @var{tblL} and @var{tblR} by matching the values of their @emph{key ## variables}, which by default are the variables that share the same name ## in both tables. Unlike @code{innerjoin}, an outer join also keeps the ## rows of each table that have no match in the other table, filling the ## variables taken from the non-matching table with missing values ## (@qcode{NaN}, @qcode{NaT}, @qcode{}, empty string, etc., as ## appropriate). If @math{m} rows in @var{tblL} and @math{n} rows in ## @var{tblR} share the same key combination, then @var{tbl} contains all ## @math{m*n} pairings for that combination. The rows of @var{tbl} are ## sorted by the values of the key variables and any row names are dropped. ## ## A key holding a missing value (@qcode{NaN}, @qcode{NaT}, ## @qcode{} or @qcode{}) matches no other key. Empty ## text, an empty character vector in a cellstr or a blank row of a ## character matrix, is a value and matches empty text, where MATLAB ## matches it with nothing; @code{ismissing} still flags it as missing. ## ## By default @var{tbl} contains all the variables of @var{tblL} followed ## by all the variables of @var{tblR}. Because the key variables are kept ## from both tables, conflicting names receive a suffix derived from each ## input's argument name (for inputs named @var{tblL} and @var{tblR}, the ## suffixes @qcode{'_tblL'} and @qcode{'_tblR'}; when an input has no name, ## @qcode{'_left'} and @qcode{'_right'} are used). ## When both inputs have the same name, as when a table is joined with ## itself, the left names are numbered as well (@qcode{'_T_1'} beside ## @qcode{'_T'}), as in MATLAB. ## See @qcode{'MergeKeys'} to combine the keys into single columns ## instead. ## ## @code{@var{tbl} = outerjoin (@var{tblL}, @var{tblR}, @var{Name}, ## @var{Value})} customizes the join with the following options: ## ## @table @asis ## @item @qcode{'Type'} ## The type of outer join: @qcode{'full'} (default) keeps unmatched rows ## from both tables, @qcode{'left'} keeps all rows of @var{tblL} and only ## matching rows of @var{tblR}, and @qcode{'right'} keeps all rows of ## @var{tblR} and only matching rows of @var{tblL}. ## ## @item @qcode{'MergeKeys'} ## A logical scalar (default @qcode{false}). When @qcode{true}, each pair ## of key variables is merged into a single variable that takes the value ## from @var{tblL} where a matching left row exists and from @var{tblR} ## otherwise. The merged variable is named after the left key when both ## keys share the same name, or @qcode{'leftName_rightName'} when their ## names differ. ## ## @item @qcode{'Keys'} ## Variables to use as keys in both tables, given as variable names or ## indices. It cannot be combined with @qcode{'LeftKeys'} or ## @qcode{'RightKeys'}. ## ## @item @qcode{'LeftKeys'}, @qcode{'RightKeys'} ## Variables to use as keys in @var{tblL} and @var{tblR}, respectively, ## when the key variables have different names. They must be specified ## together and reference the same number of variables. ## ## @item @qcode{'LeftVariables'}, @qcode{'RightVariables'} ## Variables of @var{tblL} and @var{tblR} to include in @var{tbl}. By ## default all the variables of each table are included. ## @end table ## ## @code{[@var{tbl}, @var{ixL}, @var{ixR}] = outerjoin (@dots{})} also ## returns the row-index vectors @var{ixL} and @var{ixR} that identify the ## row of @var{tblL} and @var{tblR}, respectively, corresponding to each ## row of @var{tbl}. A zero indicates a row of @var{tbl} that has no ## corresponding row in that table. ## ## @end deftypefn function [tbl, ixL, ixR] = outerjoin (tblL, tblR, varargin) if (nargin < 2) error ("table.outerjoin: too few input arguments."); endif [tbl, ixL, ixR, errmsg] = outerjoinResult (tblL, tblR, ... varargin, inputname (1), ... inputname (2)); if (! isempty (errmsg)) error ("table.outerjoin: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tbl} =} union (@var{tblA}, @var{tblB}) ## @deftypefnx {table} {@var{tbl} =} union (@var{tblA}, @var{tblB}, @var{setOrder}) ## @deftypefnx {table} {[@var{tbl}, @var{ixA}, @var{ixB}] =} union (@dots{}) ## ## Union of two tables by rows. ## ## @code{@var{tbl} = union (@var{tblA}, @var{tblB})} returns the combined ## set of rows of @var{tblA} and @var{tblB}, with duplicate rows removed. ## Both tables must have the same variable names, although not necessarily ## in the same order; @var{tbl} keeps the variable order of @var{tblA}. ## Rows are compared by their variable values only (row names are ignored), ## and by default @var{tbl} is sorted by those values. ## A row holding a missing value (@qcode{NaN}, @qcode{NaT}, ## @qcode{} or @qcode{}) equals no other row, itself ## included, and missing values sort last. ## ## @code{@var{tbl} = union (@var{tblA}, @var{tblB}, @var{setOrder})} ## controls the ordering of @var{tbl}. @var{setOrder} is either ## @qcode{'sorted'} (default) for ascending order, or @qcode{'stable'} to ## keep the order in which the rows appear in @var{tblA} and @var{tblB}. ## ## @code{[@var{tbl}, @var{ixA}, @var{ixB}] = union (@dots{})} also returns ## the index vectors @var{ixA} and @var{ixB} such that @var{tbl} is the ## vertical concatenation of @code{@var{tblA}(@var{ixA},:)} and ## @code{@var{tblB}(@var{ixB},:)}. ## ## @end deftypefn function [tbl, ixA, ixB] = union (tblA, tblB, varargin) if (nargin < 2) error ("table.union: too few input arguments."); endif if (! istable (tblA) || ! istable (tblB)) error ("table.union: both inputs must be tables."); endif [order, emsg] = parse_set_order (varargin); if (! isempty (emsg)) error ("table.union: %s", emsg); endif [proxyA, proxyB, emsg] = rowProxies (tblA, tblB); if (! isempty (emsg)) error ("table.union: %s", emsg); endif [~, ixA, ixB] = union (proxyA, proxyB, 'rows', order); ## ixA, ixB list A's then B's contributions, but the result row order ## interleaves them per SETORDER, so reorder the assembled rows to the ## result's own order. Row names are dropped: rows are drawn from both ## tables and cannot be attributed to a single input (like MATLAB). sA = subsetrows (tblA, ixA); sB = subsetrows (tblB, ixB); sA.RowNames = {}; sB.RowNames = {}; sel = vertcat (sA, sB); perm = setop_order ([proxyA(ixA,:); proxyB(ixB,:)], order); tbl = subsetrows (sel, perm); endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tbl} =} intersect (@var{tblA}, @var{tblB}) ## @deftypefnx {table} {@var{tbl} =} intersect (@var{tblA}, @var{tblB}, @var{setOrder}) ## @deftypefnx {table} {[@var{tbl}, @var{ixA}, @var{ixB}] =} intersect (@dots{}) ## ## Intersection of two tables by rows. ## ## @code{@var{tbl} = intersect (@var{tblA}, @var{tblB})} returns the set of ## rows common to both @var{tblA} and @var{tblB}, with duplicate rows ## removed. Both tables must have the same variable names, although not ## necessarily in the same order; @var{tbl} keeps the variable order of ## @var{tblA}. Rows are compared by their variable values only (row names ## are ignored), and by default @var{tbl} is sorted by those values. ## A row holding a missing value (@qcode{NaN}, @qcode{NaT}, ## @qcode{} or @qcode{}) equals no other row, itself ## included, and missing values sort last. ## ## @code{@var{tbl} = intersect (@var{tblA}, @var{tblB}, @var{setOrder})} ## controls the ordering of @var{tbl}, either @qcode{'sorted'} (default) or ## @qcode{'stable'}. ## ## @code{[@var{tbl}, @var{ixA}, @var{ixB}] = intersect (@dots{})} also ## returns index vectors @var{ixA} and @var{ixB} such that @var{tbl} equals ## @code{@var{tblA}(@var{ixA},:)} and @code{@var{tblB}(@var{ixB},:)}. ## ## @end deftypefn function [tbl, ixA, ixB] = intersect (tblA, tblB, varargin) if (nargin < 2) error ("table.intersect: too few input arguments."); endif if (! istable (tblA) || ! istable (tblB)) error ("table.intersect: both inputs must be tables."); endif [order, emsg] = parse_set_order (varargin); if (! isempty (emsg)) error ("table.intersect: %s", emsg); endif [proxyA, proxyB, emsg] = rowProxies (tblA, tblB); if (! isempty (emsg)) error ("table.intersect: %s", emsg); endif [~, ixA, ixB] = intersect (proxyA, proxyB, 'rows', order); tbl = subsetrows (tblA, ixA); endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{TF} =} ismember (@var{tblA}, @var{tblB}) ## @deftypefnx {table} {[@var{TF}, @var{ixB}] =} ismember (@var{tblA}, @var{tblB}) ## ## Find set members between two tables by rows. ## ## @code{@var{TF} = ismember (@var{tblA}, @var{tblB})} returns a logical ## column vector @var{TF} with one element per row of @var{tblA}, where ## @code{@var{TF}(i)} is @qcode{true} when the @math{i}-th row of @var{tblA} ## also appears as a row of @var{tblB}. Both tables must have the same ## variable names, although not necessarily in the same order, and rows are ## compared by their variable values only (row names are ignored). ## A row holding a missing value (@qcode{NaN}, @qcode{NaT}, ## @qcode{} or @qcode{}) is a member of nothing. ## ## @code{[@var{TF}, @var{ixB}] = ismember (@var{tblA}, @var{tblB})} also ## returns a column vector @var{ixB} containing, for each row of @var{tblA}, ## the index of the lowest matching row in @var{tblB}, or @qcode{0} if there ## is no match. ## ## @end deftypefn function [TF, ixB] = ismember (tblA, tblB) if (nargin < 2) error ("table.ismember: too few input arguments."); endif if (! istable (tblA) || ! istable (tblB)) error ("table.ismember: both inputs must be tables."); endif [proxyA, proxyB, emsg] = rowProxies (tblA, tblB); if (! isempty (emsg)) error ("table.ismember: %s", emsg); endif [TF, ixB] = ismember (proxyA, proxyB, 'rows'); endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tbl} =} setdiff (@var{tblA}, @var{tblB}) ## @deftypefnx {table} {@var{tbl} =} setdiff (@var{tblA}, @var{tblB}, @var{setOrder}) ## @deftypefnx {table} {[@var{tbl}, @var{ixA}] =} setdiff (@dots{}) ## ## Difference between two tables by rows. ## ## @code{@var{tbl} = setdiff (@var{tblA}, @var{tblB})} returns the set of ## rows that are present in @var{tblA} but not in @var{tblB}, with duplicate ## rows removed. Both tables must have the same variable names, although ## not necessarily in the same order; @var{tbl} keeps the variable order of ## @var{tblA}. Rows are compared by their variable values only (row names ## are ignored), and by default @var{tbl} is sorted by those values. ## A row holding a missing value (@qcode{NaN}, @qcode{NaT}, ## @qcode{} or @qcode{}) equals no other row, itself ## included, and missing values sort last. ## ## @code{@var{tbl} = setdiff (@var{tblA}, @var{tblB}, @var{setOrder})} ## controls the ordering of @var{tbl}, either @qcode{'sorted'} (default) or ## @qcode{'stable'}. ## ## @code{[@var{tbl}, @var{ixA}] = setdiff (@dots{})} also returns the index ## vector @var{ixA} such that @var{tbl} equals ## @code{@var{tblA}(@var{ixA},:)}. ## ## @end deftypefn function [tbl, ixA] = setdiff (tblA, tblB, varargin) if (nargin < 2) error ("table.setdiff: too few input arguments."); endif if (! istable (tblA) || ! istable (tblB)) error ("table.setdiff: both inputs must be tables."); endif [order, emsg] = parse_set_order (varargin); if (! isempty (emsg)) error ("table.setdiff: %s", emsg); endif [proxyA, proxyB, emsg] = rowProxies (tblA, tblB); if (! isempty (emsg)) error ("table.setdiff: %s", emsg); endif [~, ixA] = setdiff (proxyA, proxyB, 'rows', order); tbl = subsetrows (tblA, ixA); endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tbl} =} setxor (@var{tblA}, @var{tblB}) ## @deftypefnx {table} {@var{tbl} =} setxor (@var{tblA}, @var{tblB}, @var{setOrder}) ## @deftypefnx {table} {[@var{tbl}, @var{ixA}, @var{ixB}] =} setxor (@dots{}) ## ## Exclusive OR of two tables by rows. ## ## @code{@var{tbl} = setxor (@var{tblA}, @var{tblB})} returns the set of ## rows that are present in either @var{tblA} or @var{tblB} but not in both, ## with duplicate rows removed. Both tables must have the same variable ## names, although not necessarily in the same order; @var{tbl} keeps the ## variable order of @var{tblA}. Rows are compared by their variable values ## only (row names are ignored), and by default @var{tbl} is sorted by those ## values. ## A row holding a missing value (@qcode{NaN}, @qcode{NaT}, ## @qcode{} or @qcode{}) equals no other row, itself ## included, and missing values sort last. ## ## @code{@var{tbl} = setxor (@var{tblA}, @var{tblB}, @var{setOrder})} ## controls the ordering of @var{tbl}, either @qcode{'sorted'} (default) or ## @qcode{'stable'}. ## ## @code{[@var{tbl}, @var{ixA}, @var{ixB}] = setxor (@dots{})} also returns ## index vectors @var{ixA} and @var{ixB} such that @var{tbl} is the ## vertical concatenation of @code{@var{tblA}(@var{ixA},:)} and ## @code{@var{tblB}(@var{ixB},:)}. ## ## @end deftypefn function [tbl, ixA, ixB] = setxor (tblA, tblB, varargin) if (nargin < 2) error ("table.setxor: too few input arguments."); endif if (! istable (tblA) || ! istable (tblB)) error ("table.setxor: both inputs must be tables."); endif [order, emsg] = parse_set_order (varargin); if (! isempty (emsg)) error ("table.setxor: %s", emsg); endif [proxyA, proxyB, emsg] = rowProxies (tblA, tblB); if (! isempty (emsg)) error ("table.setxor: %s", emsg); endif [~, ixA, ixB] = setxor (proxyA, proxyB, 'rows', order); ## ixA, ixB list A's then B's contributions, but the result row order ## interleaves them per SETORDER, so reorder the assembled rows to the ## result's own order. Row names are dropped: rows are drawn from both ## tables and cannot be attributed to a single input (like MATLAB). sA = subsetrows (tblA, ixA); sB = subsetrows (tblB, ixB); sA.RowNames = {}; sB.RowNames = {}; sel = vertcat (sA, sB); perm = setop_order ([proxyA(ixA,:); proxyB(ixB,:)], order); tbl = subsetrows (sel, perm); endfunction endmethods ################################################################################ ## ** Missing Values ** ## ################################################################################ ## Available Methods ## ## ## ## 'anymissing' 'ismissing' 'rmmissing' 'fillmissing' ## ## 'standardizeMissing' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {table} {@var{TF} =} anymissing (@var{tblA}) ## ## Determine if any table element is missing. ## ## @code{@var{TF} = anymissing (@var{tblA})} returns @qcode{true} if at ## least one element in table @var{tblA} is missing, otherwise it returns ## @qcode{false}. @var{TF} is a logical scalar value. ## ## Missing values are defined according to the data type of each variable in ## @var{tblA}: ## ## @itemize ## @item @qcode{NaN} - double, single, duration and calendarDuration ## @item @qcode{NaT} - datetime ## @item @qcode{} - string ## @item @qcode{} - categorical ## @item @qcode{@{''@}} - cell arrays of character vectors ## @item @qcode{''} - character arrays ## @end itemize ## ## @end deftypefn function TF = anymissing (this) TF = any (any (ismissing (this))); endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{TF} =} ismissing (@var{tbl}) ## @deftypefnx {table} {@var{TF} =} ismissing (@var{tbl}, @var{indicator}) ## @deftypefnx {table} {@var{TF} =} ismissing (@dots{}, @qcode{'OutputFormat'}, @var{outFmt}) ## ## Find missing values in table. ## ## @code{@var{TF} = ismissing (@var{tbl})} returns a logical array, ## @var{TF}, with any @qcode{true} values corresponding to missing elements ## in the input table @var{tbl}. ## ## Missing values are defined according to the data type of each variable in ## @var{tbl}: ## ## @itemize ## @item @qcode{NaN} - double, single, duration and calendarDuration ## @item @qcode{NaT} - datetime ## @item @qcode{} - string ## @item @qcode{} - categorical ## @item @qcode{@{''@}} - cell arrays of character vectors ## @item @qcode{''} - character arrays ## @end itemize ## ## @code{@var{TF} = ismissing (@var{tbl}, @var{indicator})} also returns a ## logical array, @var{TF}, with any @qcode{true} values corresponding to ## elements in the input table @var{tbl}, which are equal to the values in ## @var{indicator}. When specifying an @var{indicator}, all default missing ## values are ignored. If you want to keep them, you need to define them in ## @var{indicator}. ## ## @var{indicator} can be either a vector of specific data type, in which ## case all other data types in table @var{tbl} are ignored, or a cell array ## containing mixed types of data types, in which case they match the data ## types of the variables in table @var{tbl}. ## ## A nested table variable is never missing, whatever it holds, with or ## without @var{indicator}, as in MATLAB. ## ## Besides the explicit data type match between @var{indicator} and ## @var{tbl}, the following additional data types matches apply. ## @itemize ## @item @qcode{double} indicators match numeric and logical variables. ## @item @qcode{single}, integer, and @qcode{logical} indicators match ## variables of their own class and @qcode{double} variables. Any other ## numeric or logical variable is not matched, and it is not an error. ## @item @qcode{char} and @qcode{cellstr} indicators match string variables. ## @item @qcode{char}, @qcode{cellstr}, and @qcode{string} indicators match ## categorical variables, as @code{categorical.ismissing} reads them: an ## empty or blank character vector matches @code{} elements, ## while strings match category names only, so neither @qcode{""} nor ## @code{} matches @code{}; @code{missing} does. ## @item @code{missing}, alone or as an element of a cell array ## @var{indicator}, matches the standard missing values of every variable, ## in addition to any other values in @var{indicator}. ## @item @qcode{calendarDuration} indicators match calendarDuration ## variables, as @code{calendarDuration.ismissing} does; MATLAB accepts no ## calendarDuration indicator, so this is an Octave extension. ## @end itemize ## ## The output array @var{TF} has the same size as the input table @var{tbl}. ## ## @code{@var{TF} = ismissing (@dots{}, @qcode{'OutputFormat'}, ## @var{outFmt})} specifies whether @var{TF} is returned as a logical array ## or as a table, which maintains the variable names and all other ## information of the input table @var{tbl}. Specifying @var{outFmt} as ## @qcode{'logical'} (default) returns a logical array. Specifying ## @var{outFmt} as @qcode{'tabular'} returns a table. ## ## @end deftypefn function TF = ismissing (this, varargin) [TF, errmsg] = ismissingResult (this, varargin{:}); if (! isempty (errmsg)) error ("table.ismissing: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tbl} =} rmmissing (@var{tblA}) ## @deftypefnx {table} {@var{tbl} =} rmmissing (@var{tblA}, @var{dim}) ## @deftypefnx {table} {@var{tbl} =} rmmissing (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {table} {[@var{tbl}, @var{TF}] =} rmmissing (@dots{}) ## ## Remove missing table elements by rows or by variables. ## ## @code{@var{tbl} = rmmissing (@var{tblA})} returns a table with the rows ## of @var{tblA} that contain at least one missing value removed. Missing ## values are determined per variable according to its data type ## (@code{NaN} for numeric, @code{NaT} for @code{datetime}, @code{} ## for @code{string}, @code{} for @code{categorical}, ## @code{@{''@}} for cellstr, etc.), as reported by @code{ismissing}. A ## variable with several columns counts once in a row where any of its ## columns is missing. ## ## @code{@var{tbl} = rmmissing (@var{tblA}, @var{dim})} removes rows when ## @var{dim} is 1, the default, and variables when @var{dim} is 2. With ## @var{dim} equal to 2, a variable is removed when it holds a missing ## value, and the options below count down each variable instead of ## across each row. ## ## @code{@var{tbl} = rmmissing (@dots{}, @var{Name}, @var{Value})} ## customizes the operation with the following options: ## ## @table @asis ## @item @qcode{'MinNumMissing'} ## A positive integer @var{n} (default @code{1}). A row is removed only ## when it has at least @var{n} variables with a missing value, and with ## @var{dim} equal to 2 a variable only when it is missing in at least ## @var{n} rows. ## ## @item @qcode{'DataVariables'} ## Restrict the search for missing values to the indicated subset of table ## variables, using the same variable referencing as the other @code{table} ## methods. Variables outside the subset are not inspected, so they are ## kept in the output whatever they hold. ## ## @item @qcode{'MissingLocations'} ## Supply the missing-value locations explicitly instead of deriving them ## with @code{ismissing}; the values the table holds are then not ## consulted. The value is either a logical matrix with one row per row of ## the input and one column per variable of @var{tblA} or per inspected ## variable, or a @code{table} of logical variables whose names and sizes ## match the inspected variables. Where both sizes agree, the columns are ## taken in the order of the variables of @var{tblA}. ## @end table ## ## @code{[@var{tbl}, @var{TF}] = rmmissing (@dots{})} also returns a logical ## vector @var{TF} that is @qcode{true} for each removed row, a column with ## one element per row of @var{tblA}, or with @var{dim} equal to 2 for each ## removed variable, a row with one element per variable of @var{tblA}. ## ## @end deftypefn function [tbl, TF] = rmmissing (this, varargin) [tbl, TF, errmsg] = rmmissingResult (this, varargin{:}); if (! isempty (errmsg)) error ("table.rmmissing: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} fillmissing (@var{tblA}, @qcode{'constant'}, @var{val}) ## @deftypefnx {table} {@var{tblB} =} fillmissing (@var{tblA}, @var{method}) ## @deftypefnx {table} {@var{tblB} =} fillmissing (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {table} {[@var{tblB}, @var{TF}] =} fillmissing (@dots{}) ## ## Fill missing entries of a table, variable by variable. ## ## @code{@var{tblB} = fillmissing (@var{tblA}, @qcode{'constant'}, ## @var{val})} replaces the missing entries of each table variable with the ## fill value @var{val}. @var{val} can be a scalar that is broadcast to ## every targeted variable, a vector with one element per targeted ## variable, or a cell array with one fill value per targeted variable. ## The fill value of each variable must be compatible with that variable's ## data type. ## ## @code{@var{tblB} = fillmissing (@var{tblA}, @var{method})} fills missing ## entries using the gap-filling method @var{method}, which can be one of: ## ## @table @asis ## @item @qcode{'previous'} ## Fill with the previous non-missing entry along each column. ## ## @item @qcode{'next'} ## Fill with the next non-missing entry along each column. ## ## @item @qcode{'nearest'} ## Fill with the nearest non-missing entry along each column. When two ## non-missing entries are equidistant, the later (next) one is used. ## ## @item @qcode{'linear'} ## Fill by linear interpolation of the neighbouring entries that are not ## missing. Numeric, logical, @code{datetime} and @code{duration} ## variables can be interpolated; a targeted variable of any other type ## raises an error. ## @end table ## ## The @qcode{'previous'}, @qcode{'next'}, and @qcode{'nearest'} methods ## operate on variables of any data type. Leading or trailing missing ## entries that cannot be reached by the method are left missing. ## ## A variable with nothing @code{ismissing} counts as missing, such as a ## cell array that is not a cell array of character vectors, a structure ## or a nested table, is left as it is. MATLAB refuses the whole call ## when such a variable is among the data variables. ## ## The following @var{Name}/@var{Value} pairs are supported: ## ## @table @asis ## @item @qcode{'DataVariables'} ## Restrict the operation to the indicated subset of table variables. The ## value uses the same variable referencing as the rest of the @code{table} ## methods. By default, every variable is targeted. ## ## @item @qcode{'EndValues'} ## Control how leading and trailing missing entries are filled, whatever ## the fill method is. Leading entries are those before the first entry ## that is not missing, and trailing entries those after the last. Valid ## values are @qcode{'extrap'} (default), which leaves them to the fill ## method itself, @qcode{'none'}, which leaves them missing, ## @qcode{'previous'}, @qcode{'next'} and @qcode{'nearest'}, which take ## the value of the nearest entry that is not missing on the side they ## name and leave the other side missing, or a scalar constant, which must ## be assignable to the variable it fills. ## @end table ## ## @code{[@var{tblB}, @var{TF}] = fillmissing (@dots{})} also returns a ## logical array @var{TF} with @code{height (@var{tblA})} rows and one ## column per table variable. @code{@var{TF}(i,j)} is @qcode{true} when an ## entry of the j-th variable in the i-th row was missing and has been ## filled. ## ## Not yet supported: the @qcode{'spline'}, @qcode{'pchip'}, ## @qcode{'makima'}, @qcode{'movmean'}, @qcode{'movmedian'}, ## @qcode{'mean'}, @qcode{'median'}, @qcode{'mode'}, and @qcode{'knn'} ## methods, as well as the @qcode{'ReplaceValues'}, @qcode{'MaxGap'}, ## @qcode{'SamplePoints'}, and @qcode{'MissingLocations'} options. ## ## @end deftypefn function [tbl, TF] = fillmissing (tblA, varargin) [tbl, TF, errmsg] = fillmissingResult (tblA, varargin{:}); if (! isempty (errmsg)) error ("table.fillmissing: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} standardizeMissing (@var{tblA}, @var{indicator}) ## @deftypefnx {table} {@var{tblB} =} standardizeMissing (@dots{}, @var{Name}, @var{Value}) ## ## Insert standard missing values into a table. ## ## @code{@var{tblB} = standardizeMissing (@var{tblA}, @var{indicator})} ## replaces every entry of @var{tblA} that matches a value in ## @var{indicator} with the standard missing value of that variable's data ## type (@code{NaN} for @code{double}/@code{single} and @code{duration}, ## @qcode{''} for cell arrays of character vectors, @code{} for ## @code{string}, @code{} for @code{categorical}, and ## @code{NaT} for @code{datetime}). ## ## @var{indicator} may be a numeric scalar or vector, a character vector, a ## @code{string} array, a cell array of character vectors, a ## @code{duration} or @code{datetime} array, or a cell array mixing any of ## these. Each indicator is applied only to the variables whose type is ## compatible with it: numeric indicators match @code{double} and ## @code{single} variables, text indicators (char, @code{string}, or ## cellstr) match cell-array-of-character-vector, @code{string}, and ## @code{categorical} variables, @code{duration} indicators match ## @code{duration} variables, and @code{datetime} indicators match ## @code{datetime} variables. A @code{datetime} indicator matches the same ## instant in any time zone, but indicators with and without a time zone ## cannot be mixed, nor compared with a variable that differs from them in ## having one. ## ## The @qcode{'DataVariables'} @var{Name}/@var{Value} pair restricts the ## operation to a subset of variables, using the same variable referencing ## as the other @code{table} methods. Variables not selected pass through ## unchanged. ## ## Logical and integer variables (which have no standard missing value) and ## @code{calendarDuration} variables pass through unchanged. ## ## @end deftypefn function tbl = standardizeMissing (tblA, varargin) [tbl, errmsg] = standardizeMissingResult (tblA, varargin{:}); if (! isempty (errmsg)) error ("table.standardizeMissing: %s", errmsg); endif endfunction endmethods ################################################################################ ## ** Apply Functions to Table Contents ** ## ################################################################################ ## Available Methods ## ## ## ## 'pivot' 'groupcounts' 'groupfilter' 'groupsummary' ## ## 'grouptransform' 'findgroups' 'splitapply' 'rowfun' ## ## 'varfun' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {table} {@var{G} =} findgroups (@var{T}) ## @deftypefnx {table} {[@var{G}, @var{TID}] =} findgroups (@var{T}) ## ## Find groups defined by the variables of a table. ## ## @code{@var{G} = findgroups (@var{T})} returns @var{G}, a column vector of ## positive integer group numbers, with one element for each row of the ## table @var{T}. Each variable of @var{T} acts as a grouping variable, and ## the groups are the unique combinations of values across those variables, ## sorted in ascending order. If @var{N} groups are found, every integer ## between 1 and @var{N} labels a group. Rows holding a missing value ## (@code{NaN}, @code{NaT}, @code{}, @code{''}, or ## @code{}) in any grouping variable are labelled @code{NaN} in ## @var{G}. ## ## @code{[@var{G}, @var{TID}] = findgroups (@var{T})} also returns ## @var{TID}, a table whose rows are the sorted unique combinations ## identifying each group, with the same variables as @var{T}. ## ## @end deftypefn function [G, TID] = findgroups (this) if (nargin != 1) error ("table.findgroups: too few input arguments."); endif [G, TID, errmsg] = findgroupsResult (this); if (! isempty (errmsg)) error ("table.findgroups: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{Y} =} splitapply (@var{func}, @var{T}, @var{G}) ## @deftypefnx {table} {[@var{Y1}, @dots{}, @var{YM}] =} splitapply (@var{func}, @var{T}, @var{G}) ## ## Split table data into groups and apply a function to each group. ## ## @code{@var{Y} = splitapply (@var{func}, @var{T}, @var{G})} splits the ## rows of the table @var{T} into groups according to the group numbers ## @var{G} (typically produced by @code{findgroups}), applies the function ## handle @var{func} to each group, and concatenates the per-group results ## into the output @var{Y}. @var{G} must be a column vector of positive ## integers with one element per row of @var{T}; if it identifies @var{N} ## groups, every integer between 1 and @var{N} must occur at least once. ## Rows for which @var{G} is @code{NaN} are omitted. Each variable of ## @var{T} is passed to @var{func} as a separate input argument, so ## @var{func} must accept as many arguments as @var{T} has variables. ## ## @code{[@var{Y1}, @dots{}, @var{YM}] = splitapply (@dots{})} returns the ## multiple outputs of @var{func}, each concatenated across groups. ## ## @end deftypefn function varargout = splitapply (func, this, G) if (nargin != 3) error ("table.splitapply: too few input arguments."); endif nout = max (nargout, 1); [results, N, errmsg] = splitapplyResult (this, func, G, nout); if (! isempty (errmsg)) error ("table.splitapply: %s", errmsg); endif varargout = cell (1, nout); for k = 1:nout varargout{k} = vertcat (results{:,k}); endfor endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{B} =} varfun (@var{func}, @var{A}) ## @deftypefnx {table} {@var{B} =} varfun (@var{func}, @var{A}, @var{Name}, @var{Value}, @dots{}) ## ## Apply a function to each variable of a table. ## ## @code{@var{B} = varfun (@var{func}, @var{A})} applies the function ## handle @var{func} separately to each variable of the table @var{A} and ## returns the results in the table @var{B}. @var{func} is called once per ## variable with that variable as its single input argument. By default ## each output variable of @var{B} is named @qcode{@var{f}_@var{v}}, where ## @var{f} is the name of @var{func} (or @qcode{Fun} when @var{func} is ## anonymous) and @var{v} is the name of the corresponding variable of ## @var{A}. ## ## @code{@var{B} = varfun (@var{func}, @var{A}, @var{Name}, @var{Value}, ## @dots{})} modifies the operation through the following ## @var{Name}/@var{Value} pairs: ## ## @table @asis ## @item @qcode{'InputVariables'} ## The variables of @var{A} to which @var{func} is applied, given as ## variable names, indices, a logical vector, or a function handle that ## returns @code{true} for the variables to include. By default @var{func} ## is applied to every variable of @var{A} that is not a grouping variable. ## ## @item @qcode{'GroupingVariables'} ## One or more variables of @var{A} that define groups of rows. When ## grouping variables are given, @var{func} is applied to the values of each ## input variable within each group, @var{B} has one row per group, and ## @var{B} also includes the grouping variables and a @qcode{GroupCount} ## variable holding the number of rows in each group. Rows with a missing ## value in any grouping variable are omitted. ## ## @item @qcode{'OutputFormat'} ## The format of @var{B}, one of @qcode{'auto'} (the default, equivalent to ## @qcode{'table'}), @qcode{'table'}, @qcode{'uniform'}, or @qcode{'cell'}. ## For @qcode{'uniform'}, @var{func} must return a scalar on each call and ## the results are concatenated into an array. For @qcode{'cell'} the ## results are returned in a cell array. The @qcode{'uniform'} and ## @qcode{'cell'} formats return only the results of @var{func}, without the ## grouping variables or @qcode{GroupCount}. ## ## @item @qcode{'ErrorHandler'} ## A function handle that is called when @var{func} throws an error. It ## receives a structure with fields @qcode{identifier}, @qcode{message}, and ## @qcode{index}, followed by the same inputs that were passed to ## @var{func}, and its outputs are used in place of the outputs of ## @var{func}. ## @end table ## ## @end deftypefn function B = varfun (func, A, varargin) if (nargin < 2) error ("table.varfun: too few input arguments."); endif [B, errmsg] = varfunResult (A, func, varargin); if (! isempty (errmsg)) error ("table.varfun: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{B} =} rowfun (@var{func}, @var{A}) ## @deftypefnx {table} {@var{B} =} rowfun (@var{func}, @var{A}, @var{Name}, @var{Value}, @dots{}) ## ## Apply a function to each row of a table. ## ## @code{@var{B} = rowfun (@var{func}, @var{A})} applies the function ## handle @var{func} to each row of the table @var{A} and returns the ## results in the table @var{B}, which has one row for each row of @var{A}. ## By default the value of each variable in the row is passed to @var{func} ## as a separate input argument, and the output variables of @var{B} are ## named @qcode{Var1}, @qcode{Var2}, and so on. ## ## @code{@var{B} = rowfun (@var{func}, @var{A}, @var{Name}, @var{Value}, ## @dots{})} modifies the operation through the following ## @var{Name}/@var{Value} pairs: ## ## @table @asis ## @item @qcode{'InputVariables'} ## The variables of @var{A} that are passed to @var{func}, given as variable ## names, indices, a logical vector, or a function handle. By default every ## variable of @var{A} that is not a grouping variable is used. ## ## @item @qcode{'GroupingVariables'} ## One or more variables of @var{A} that define groups of rows. When ## grouping variables are given, @var{func} is applied once to each group, ## receiving the values of each input variable across the rows of the group; ## @var{B} has one row per group and also includes the grouping variables ## and a @qcode{GroupCount} variable. Rows with a missing value in any ## grouping variable are omitted. ## ## @item @qcode{'OutputVariableNames'} ## The names of the output variables of @var{B}, one per output of ## @var{func}. ## ## @item @qcode{'NumOutputs'} ## The number of output arguments to request from @var{func}. It defaults ## to the number of @qcode{'OutputVariableNames'} if those are given, ## otherwise to @code{1}. ## ## @item @qcode{'SeparateInputs'} ## A logical scalar. When @code{true} (the default), the value of each ## input variable is passed to @var{func} as a separate argument. When ## @code{false}, the values of the row are joined into one array, as ## @code{table2array} joins the variables, and passed as a single ## argument. Variables that cannot form one array are refused. ## ## @item @qcode{'ExtractCellContents'} ## A logical scalar. When @code{true}, the contents of cell-valued ## variables are extracted before being passed to @var{func}. It defaults ## to @code{false}. ## ## @item @qcode{'OutputFormat'} ## The format of @var{B}, one of @qcode{'auto'} (the default, equivalent to ## @qcode{'table'}), @qcode{'table'}, @qcode{'uniform'}, or @qcode{'cell'}. ## For @qcode{'uniform'}, every call to @var{func} must return scalars of ## the same type, which are concatenated into an array. For @qcode{'cell'} ## the results are returned in a cell array. The @qcode{'uniform'} and ## @qcode{'cell'} formats return only the results of @var{func}. ## ## @item @qcode{'ErrorHandler'} ## A function handle that is called when @var{func} throws an error, ## receiving a structure with fields @qcode{identifier}, @qcode{message}, ## and @qcode{index} followed by the inputs passed to @var{func}. ## @end table ## ## @end deftypefn function B = rowfun (func, A, varargin) if (nargin < 2) error ("table.rowfun: too few input arguments."); endif [B, errmsg] = rowfunResult (A, func, varargin); if (! isempty (errmsg)) error ("table.rowfun: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{G} =} groupsummary (@var{T}, @var{groupvars}) ## @deftypefnx {table} {@var{G} =} groupsummary (@var{T}, @var{groupvars}, @var{groupbins}) ## @deftypefnx {table} {@var{G} =} groupsummary (@var{T}, @var{groupvars}, @var{groupbins}, @var{method}) ## @deftypefnx {table} {@var{G} =} groupsummary (@var{T}, @var{groupvars}, @var{groupbins}, @var{method}, @var{datavars}) ## @deftypefnx {table} {@var{G} =} groupsummary (@dots{}, @var{Name}, @var{Value}) ## ## Compute summary statistics by group for the variables of a table. ## ## @code{@var{G} = groupsummary (@var{T}, @var{groupvars})} groups the rows ## of the table @var{T} by the grouping variables @var{groupvars} and ## returns the table @var{G} with one row per group, holding the grouping ## variables and a @qcode{GroupCount} variable counting the rows in each ## group. @var{groupvars} selects the grouping variables by name, index, ## logical vector, function handle, or @code{vartype} subscript. ## ## @code{@var{G} = groupsummary (@var{T}, @var{groupvars}, @var{method})} ## also applies @var{method} to each data variable within each group and ## appends the results to @var{G}. @var{method} is one of the method names ## below, a function handle, or a cell array of method names and@/or ## function handles: ## ## @table @asis ## @item @qcode{'sum'}, @qcode{'mean'}, @qcode{'median'}, @qcode{'mode'} ## @itemx @qcode{'var'}, @qcode{'std'}, @qcode{'min'}, @qcode{'max'} ## @itemx @qcode{'range'}, @qcode{'nnz'} ## Standard statistics, computed over numeric or logical data variables. ## @code{NaN} values are omitted (as in MATLAB) for every named method ## except @qcode{'nummissing'}. ## ## @item @qcode{'nummissing'} ## The number of missing values in the group, supported for data variables ## of any type. ## ## @item @qcode{'numunique'} ## The number of unique non-missing values in the group, supported for data ## variables of any type. ## @end table ## ## A function handle is applied to each group's slice of each data variable ## and must return a single row (its first dimension must be @code{1}); it ## receives the values with @code{NaN} included. ## ## @code{@var{G} = groupsummary (@var{T}, @var{groupvars}, @var{method}, ## @var{datavars})} applies @var{method} only to the data variables selected ## by @var{datavars} (named, indexed, logical, function handle, or ## @code{vartype} subscript). By default every variable that is not a ## grouping variable is a data variable. ## ## The computed variables of @var{G} are named @code{_}, ## e.g.@: @qcode{mean_X}; results from a function handle are named ## @code{fun_}, where @var{n} is the position of the handle ## among the requested methods. When several methods are requested the ## computed variables are ordered method first, then data variable. ## ## The optional @var{groupbins} argument bins the grouping variables before ## grouping. A binning scheme is one of: a vector of bin edges; a positive ## integer number of equal-width bins spanning the data range; a ## @code{duration} scalar giving a fixed bin width (for a datetime or ## duration grouping variable); or, for a datetime grouping variable, a ## calendar-unit keyword (@qcode{'second'}, @qcode{'minute'}, ## @qcode{'hour'}, @qcode{'day'}, @qcode{'week'}, @qcode{'month'}, ## @qcode{'quarter'}, @qcode{'year'}, @qcode{'decade'}, or ## @qcode{'century'}) that bins by that calendar period. A binned grouping ## variable becomes a categorical and is renamed @code{disc_} for edge, ## bin-count, or width binning, or @code{_} for calendar-unit ## binning. Pass a cell array with one scheme per grouping variable to bin ## them differently, or @qcode{'none'} to leave a variable unbinned. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @table @asis ## @item @qcode{'IncludeMissingGroups'} ## A logical scalar. When @code{true} (the default), rows holding a missing ## value in a grouping variable form their own groups, sorted after the ## non-missing groups. When @code{false}, such rows are excluded. ## ## @item @qcode{'IncludeEmptyGroups'} ## A logical scalar, @code{false} by default. When @code{true}, the unused ## categories of a categorical or binned grouping variable contribute empty ## groups (@qcode{GroupCount} @code{0}, @code{0} for @qcode{'sum'} and ## @qcode{'nnz'}, @code{NaN} otherwise). ## ## @item @qcode{'IncludedEdge'} ## Either @qcode{'left'} (the default) or @qcode{'right'}, selecting which ## edge of each bin is inclusive when @var{groupbins} is given. ## @end table ## ## @end deftypefn function G = groupsummary (T, groupvars, varargin) if (nargin < 2) error ("table.groupsummary: too few input arguments."); endif [G, errmsg] = groupsummaryResult (T, groupvars, varargin); if (! isempty (errmsg)) error ("table.groupsummary: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{G} =} groupcounts (@var{T}, @var{groupvars}) ## @deftypefnx {table} {@var{G} =} groupcounts (@var{T}, @var{groupvars}, @var{groupbins}) ## @deftypefnx {table} {@var{G} =} groupcounts (@dots{}, @var{Name}, @var{Value}) ## ## Count the number of rows in each group of a table. ## ## @code{@var{G} = groupcounts (@var{T}, @var{groupvars})} groups the rows ## of the table @var{T} by the grouping variables @var{groupvars} and ## returns the table @var{G} with one row per group, holding the grouping ## variables, a @qcode{GroupCount} variable counting the rows in each group, ## and a @qcode{Percent} variable giving each group's count as a percentage ## of the total. @var{groupvars} selects the grouping variables by name, ## index, logical vector, function handle, or @code{vartype} subscript. ## ## The optional @var{groupbins} argument bins the grouping variables before ## grouping, using bin edges, a number of equal-width bins, a ## @code{duration} bin width, or a datetime calendar-unit keyword, or a cell ## array with one scheme per grouping variable. A binned grouping variable ## becomes a categorical and is renamed @code{disc_} or ## @code{_}. See @code{groupsummary} for details. ## ## Groups are the sorted unique combinations of grouping values. The ## following @var{Name}/@var{Value} pairs are accepted: ## ## @table @asis ## @item @qcode{'IncludeMissingGroups'} ## A logical scalar. When @code{true} (the default), rows holding a missing ## value in a grouping variable form their own groups, sorted after the ## non-missing groups. When @code{false}, such rows are excluded. ## ## @item @qcode{'IncludeEmptyGroups'} ## A logical scalar, @code{false} by default. When @code{true}, the unused ## categories of a categorical or binned grouping variable contribute empty ## groups with a @qcode{GroupCount} of @code{0}. ## ## @item @qcode{'IncludedEdge'} ## Either @qcode{'left'} (the default) or @qcode{'right'}, selecting which ## edge of each bin is inclusive when @var{groupbins} is given. ## @end table ## ## @end deftypefn function G = groupcounts (T, groupvars, varargin) if (nargin < 2) error ("table.groupcounts: too few input arguments."); endif [G, errmsg] = groupcountsResult (T, groupvars, varargin); if (! isempty (errmsg)) error ("table.groupcounts: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{G} =} groupfilter (@var{T}, @var{groupvars}, @var{method}) ## @deftypefnx {table} {@var{G} =} groupfilter (@var{T}, @var{groupvars}, @var{groupbins}, @var{method}) ## @deftypefnx {table} {@var{G} =} groupfilter (@dots{}, @var{method}, @var{datavars}) ## ## Filter the rows of a table by a per-group condition. ## ## @code{@var{G} = groupfilter (@var{T}, @var{groupvars}, @var{method})} ## groups the rows of the table @var{T} by the grouping variables ## @var{groupvars}, applies the filter function @var{method} to each group, ## and returns the table @var{G} holding the rows that satisfy the ## condition, in their original order and with all the variables of ## @var{T}. @var{groupvars} selects the grouping variables by name, index, ## logical vector, function handle, or @code{vartype} subscript. ## ## @var{method} is a function handle applied to each group's slice of every ## data variable. It must return either a logical scalar, which keeps or ## drops the whole group, or a logical vector with one element per row of ## the group, which keeps or drops the individual rows. A row is kept only ## when the condition holds for it across all data variables. ## ## @code{@var{G} = groupfilter (@var{T}, @var{groupvars}, @var{method}, ## @var{datavars})} applies @var{method} only to the data variables selected ## by @var{datavars} (named, indexed, logical, function handle, or ## @code{vartype} subscript). By default every variable that is not a ## grouping variable is a data variable. ## ## Rows holding a missing value in a grouping variable form their own ## groups, to which @var{method} is applied like any other group. ## ## The optional @var{groupbins} argument bins the grouping variables before ## grouping, using bin edges, a number of equal-width bins, a ## @code{duration} bin width, or a datetime calendar-unit keyword, or a cell ## array with one scheme per grouping variable; see @code{groupsummary} for ## details. The @qcode{'IncludedEdge'} Name-Value pair (@qcode{'left'} by ## default, or @qcode{'right'}) selects which bin edge is inclusive. ## ## @end deftypefn function G = groupfilter (T, groupvars, varargin) if (nargin < 3) error ("table.groupfilter: too few input arguments."); endif [G, errmsg] = groupfilterResult (T, groupvars, varargin); if (! isempty (errmsg)) error ("table.groupfilter: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{G} =} grouptransform (@var{T}, @var{groupvars}, @var{method}) ## @deftypefnx {table} {@var{G} =} grouptransform (@var{T}, @var{groupvars}, @var{groupbins}, @var{method}) ## @deftypefnx {table} {@var{G} =} grouptransform (@dots{}, @var{method}, @var{datavars}) ## @deftypefnx {table} {@var{G} =} grouptransform (@dots{}, @var{Name}, @var{Value}) ## ## Transform the data variables of a table group by group. ## ## @code{@var{G} = grouptransform (@var{T}, @var{groupvars}, @var{method})} ## groups the rows of the table @var{T} by the grouping variables ## @var{groupvars}, applies @var{method} to each data variable within each ## group, and returns the table @var{G} with the transformed values, one ## row per row of @var{T} and in the original order. @var{groupvars} ## selects the grouping variables by name, index, logical vector, function ## handle, or @code{vartype} subscript. ## ## @var{method} is one of the transform names below or a function handle: ## ## @table @asis ## @item @qcode{'zscore'} ## Center and scale each group to zero mean and unit standard deviation. ## ## @item @qcode{'norm'} ## Divide each group by its 2-norm. ## ## @item @qcode{'meancenter'} ## Subtract the group mean. ## ## @item @qcode{'rescale'} ## Rescale each group to the range @code{[0, 1]}. ## ## @item @qcode{'meanfill'} ## Replace missing values with the group mean. ## ## @item @qcode{'linearfill'} ## Fill missing values by linear interpolation within the group; leading and ## trailing missing values are left unchanged. ## @end table ## ## For the named methods @code{NaN} values are omitted when computing the ## group statistics. A function handle is applied to each group's slice of ## each data variable and must return either a single row (broadcast to all ## the group's rows) or a result with one row per row of the group. ## ## @code{@var{G} = grouptransform (@var{T}, @var{groupvars}, @var{method}, ## @var{datavars})} transforms only the data variables selected by ## @var{datavars} (named, indexed, logical, function handle, or ## @code{vartype} subscript). By default every variable that is not a ## grouping variable is a data variable. ## ## The following @var{Name}/@var{Value} pair is accepted: ## ## @table @asis ## @item @qcode{'ReplaceValues'} ## A logical scalar. When @code{true} (the default), each data variable is ## replaced by its transformed values. When @code{false}, the transformed ## values are appended as new variables named @code{_} ## (@code{fun1_} for a function handle), leaving the originals in ## place. ## ## @item @qcode{'IncludedEdge'} ## Either @qcode{'left'} (the default) or @qcode{'right'}, selecting which ## edge of each bin is inclusive when @var{groupbins} is given. ## @end table ## ## Rows holding a missing value in a grouping variable form their own ## groups, which are transformed like any other group. The optional ## @var{groupbins} argument bins the grouping variables before grouping, ## using bin edges, a number of equal-width bins, a @code{duration} bin ## width, or a datetime calendar-unit keyword, or a cell array with one ## scheme per grouping variable; see @code{groupsummary} for details. ## ## @end deftypefn function G = grouptransform (T, groupvars, varargin) if (nargin < 3) error ("table.grouptransform: too few input arguments."); endif [G, errmsg] = grouptransformResult (T, groupvars, varargin); if (! isempty (errmsg)) error ("table.grouptransform: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{P} =} pivot (@var{T}, @qcode{'Columns'}, @var{colvars}) ## @deftypefnx {table} {@var{P} =} pivot (@var{T}, @qcode{'Rows'}, @var{rowvars}) ## @deftypefnx {table} {@var{P} =} pivot (@dots{}, @var{Name}, @var{Value}) ## ## Summarize tabular data in a pivoted table. ## ## @code{@var{P} = pivot (@var{T}, 'Columns', @var{colvars}, 'Rows', ## @var{rowvars})} reshapes the table @var{T} into the pivoted table ## @var{P}. The unique combinations of the grouping variables @var{colvars} ## become the variables (columns) of @var{P}, the unique combinations of the ## grouping variables @var{rowvars} become its rows, and each cell holds one ## statistic computed over the rows of @var{T} that fall into that ## row-and-column group. At least one of @qcode{'Columns'} or ## @qcode{'Rows'} is required; an omitted dimension collapses to a single ## group. Each of @var{colvars} and @var{rowvars} selects variables by ## name, index, or logical vector, and may name several variables. ## ## Groups are the sorted unique combinations of the grouping values, with ## the first variable varying slowest; a categorical variable groups by its ## category order. Column variable names are taken from the grouping values ## (e.g.@: @qcode{'true'}/@qcode{'false'} for a logical variable), joined ## with @qcode{'_'} when several variables define the columns. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @table @asis ## @item @qcode{'DataVariable'} ## The single variable whose values are aggregated. When omitted, the cells ## hold group counts. ## ## @item @qcode{'Method'} ## The aggregation applied to @qcode{'DataVariable'}: one of ## @qcode{'count'}, @qcode{'sum'}, @qcode{'mean'}, @qcode{'median'}, ## @qcode{'mode'}, @qcode{'std'}, @qcode{'var'}, @qcode{'min'}, ## @qcode{'max'}, @qcode{'range'}, @qcode{'nummissing'}, ## @qcode{'numunique'}, @qcode{'nnz'}, @qcode{'percentage'}, ## @qcode{'none'}, or a function handle. Named methods omit missing ## values. The default is @qcode{'count'} when no data variable is given ## or the data variable is non-numeric, and @qcode{'sum'} when it is ## numeric. @qcode{'none'} rearranges the data without aggregating and ## requires at most one value per cell. ## ## @item @qcode{'IncludeMissingGroups'} ## A logical scalar, @code{true} by default. When @code{true}, rows ## holding a missing value in a grouping variable form their own group, ## sorted last; when @code{false}, such rows are excluded. ## ## @item @qcode{'IncludeEmptyGroups'} ## A logical scalar, @code{false} by default. When @code{true}, every ## category of a categorical grouping variable contributes a group even ## if it is unused in the data, so unused combinations appear as empty ## cells. ## ## @item @qcode{'IncludeTotals'} ## A logical scalar, @code{false} by default. When @code{true}, a ## @qcode{'Total'} marginal row and/or column holding the same statistic ## computed over each margin is appended. Row labels are then placed in the ## row names. ## ## @item @qcode{'RowLabelPlacement'} ## Either @qcode{'variable'} (the default), which keeps the row grouping ## variables as the leftmost variables of @var{P}, or @qcode{'rownames'}, ## which places the row group labels in the @code{RowNames} property. ## ## @item @qcode{'ColumnsBinMethod'}, @qcode{'RowsBinMethod'} ## A binning scheme applied to the @qcode{'Columns'} or @qcode{'Rows'} ## grouping variables before pivoting: a vector of bin edges, a number of ## equal-width bins, a @code{duration} bin width, or a datetime ## calendar-unit keyword (see @code{groupsummary}), or a cell array with one ## scheme per variable. Each binned variable becomes a categorical. The ## default @qcode{'none'} applies no binning. ## ## @item @qcode{'IncludedEdge'} ## Either @qcode{'left'} (the default) or @qcode{'right'}, selecting which ## edge of each bin is inclusive when a binning scheme is given. ## ## @item @qcode{'OutputFormat'} ## @qcode{'flat'} (default) names each output column after the joined column ## grouping values (@qcode{@var{lvl}_@var{lvl}}). @qcode{'nested'} instead ## groups two or more @qcode{'Columns'} variables into nested tables: one ## outer variable per level of the first column grouping variable, each a ## nested @code{table} whose variables are the next grouping variable's ## levels (recursively). A marginal-total column, if any, stays a flat ## outer variable. ## @end table ## ## @end deftypefn function P = pivot (T, varargin) [P, errmsg] = pivotResult (T, varargin); if (! isempty (errmsg)) error ("table.pivot: %s", errmsg); endif endfunction endmethods ################################################################################ ## ** Auxiliary Methods ** ## ################################################################################ ## Available Methods ## ## ## ## 'cat' 'horzcat' 'iscolumn' 'isempty' ## ## 'isequal' 'isequaln' 'ismatrix' 'isrow' ## ## 'isscalar' 'istable' 'isvector' 'ndims' ## ## 'numel' 'repelem' 'repmat' 'size' ## ## 'squeeze' 'vertcat' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {table} {@var{tbl} =} cat (@var{dim}, @var{tbl1}, @var{tbl2}, @dots{}) ## ## Concatenate tables along the given dimension. ## ## @code{@var{tbl} = cat (@var{dim}, @var{tbl1}, @var{tbl2}, @dots{})} ## concatenates the input tables along dimension @var{dim}, which must be ## either 1 or 2, since a table always has exactly two dimensions. ## ## @code{cat (1, @dots{})} concatenates vertically and is equivalent to ## @code{vertcat}, whereas @code{cat (2, @dots{})} concatenates ## horizontally and is equivalent to @code{horzcat}. The same ## requirements on variable names, row names and size apply, and a ## @qcode{0x0} operand that is not a character array takes no part in the ## concatenation. ## ## @end deftypefn function tbl = cat (dim, varargin) if (nargin < 1) error ("table.cat: too few input arguments."); endif if (! (isnumeric (dim) && isscalar (dim) && any (dim == [1, 2]))) error ("table.cat: DIM must be 1 or 2 for a 2-D table."); endif if (dim == 1) tbl = vertcat (varargin{:}); else tbl = horzcat (varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tbl} =} horzcat (@var{tbl1}, @var{tbl2}, @dots{}) ## ## Horizontal concatenation for tables. ## ## @code{@var{tbl} = horzcat (@var{tbl1}, @var{tbl2}, @dots{})} merges ## tables by horizontally concatenating them, provided that all input tables ## have collectively unique variable names and the same number of rows. ## ## Input tables that have row names must share the same unique set of row ## names but not necessarily in the same order. When row names are present ## in multiple input tables, their position is matched to the row names of ## the first input table. Input tables without row names are concatenated ## by position without re-indexing. Output table's @qcode{Description} and ## @qcode{UserData} properties are assigned using the first non-empty value. ## ## @end deftypefn function tbl = horzcat (varargin) ## Null operands take no part in concatenation varargin = tabular.drop_null_operands (varargin); if (isempty (varargin)) tbl = table (); return; elseif (numel (varargin) == 1 && istable (varargin{1})) tbl = varargin{1}; return; endif ## All inputs must be tables are_tables = cellfun (@istable, varargin); if (! all (are_tables)) error ("table.horzcat: all inputs must be tables."); endif ## All tables must have unique variable names varNames = cellfun (@(obj) obj.VariableNames, varargin, ... 'UniformOutput', false); varNames = [varNames{:}]; if (numel (varNames) != numel (unique (varNames))) error (strcat ("table.horzcat: all input tables must have unique", ... " variable names.")); endif ## All tables must have the same rows (height). A table with rows but ## no variables counts: it knows its height, and a 0-by-0 one is gone ## already, having been dropped as a null operand. numRows = cellfun (@height, varargin); if (numel (unique (numRows)) != 1) error ("table.horzcat: all input tables must have the same height."); endif numRows = numRows(1); ## Check for RowNames has_RowNames = ! cellfun (@(obj) isempty (obj.RowNames), varargin); if (! any (has_RowNames)) # no RowNames in any table (easy) tbl = varargin{1}; tbl.VariableNames = varNames; for i = 2:numel (varargin) in = varargin{i}; nA = numel (tbl.VariableValues); nB = numel (in.VariableValues); tbl.VariableContinuity = tabular.merge_continuity ( ... tbl.VariableContinuity, nA, in.VariableContinuity, nB); tbl.VariableDescriptions = tabular.merge_meta ( ... tbl.VariableDescriptions, nA, ... in.VariableDescriptions, nB); tbl.VariableUnits = tabular.merge_meta (tbl.VariableUnits, nA, ... in.VariableUnits, nB); tbl.VariableValues = [tbl.VariableValues, in.VariableValues]; if (isempty (tbl.Description)) tbl.Description = in.Description; endif if (isempty (tbl.UserData)) tbl.UserData = in.UserData; endif endfor elseif (sum (has_RowNames) == 1) # only one input table has RowNames (ok) tbl = varargin{1}; tbl.VariableNames = varNames; for i = 2:numel (varargin) in = varargin{i}; nA = numel (tbl.VariableValues); nB = numel (in.VariableValues); tbl.VariableContinuity = tabular.merge_continuity ( ... tbl.VariableContinuity, nA, in.VariableContinuity, nB); tbl.VariableDescriptions = tabular.merge_meta ( ... tbl.VariableDescriptions, nA, ... in.VariableDescriptions, nB); tbl.VariableUnits = tabular.merge_meta (tbl.VariableUnits, nA, ... in.VariableUnits, nB); tbl.VariableValues = [tbl.VariableValues, in.VariableValues]; if (! isempty (in.RowNames)) tbl.RowNames = in.RowNames; endif if (isempty (tbl.Description)) tbl.Description = in.Description; endif if (isempty (tbl.UserData)) tbl.UserData = in.UserData; endif endfor else # multiple tables has rowNames (we are screwed) ## First we need to ensure that all tables with RowNames share the ## same unique RowNames (in any order) rowNames = cellfun (@(obj) obj.RowNames, varargin(has_RowNames), ... 'UniformOutput', false); sortedRowNames = cellfun (@sort, rowNames, 'UniformOutput', false); if (! isequal (sortedRowNames{:})) error ("table.horzcat: input tables must have identical RowNames."); endif ## We need to figure out some indexing for every other table with ## RowNames so that we now how to merge them with the first table ## with RowNames tbl_withRowNames = find (has_RowNames); index = [1:numRows]'; # first table is reindexed to itself for i = 2:numel (rowNames) ## For each row of the first table, find the matching row in the ## i-th table (the inverse map), so subsetrows aligns it to the first. fcn = @(x) find (ismember (rowNames{i}, x)); index(:,i) = cellfun (fcn, rowNames{1}); endfor ## Start merging tables and re-index every other table with RowNames ## before merging tbl = varargin{1}; tbl.VariableNames = varNames; tbl.VariableValues = {}; tbl.VariableDescriptions = {}; tbl.VariableUnits = {}; tbl.RowNames = {}; add_row_names = true; # only once for i = 1:numel (varargin) in = varargin{i}; if (ismember (i, tbl_withRowNames)) # this table has RowNames ixRows = index(:,1); index(:,1) = []; in = subsetrows (in, ixRows); if (add_row_names) tbl.RowNames = in.RowNames(ixRows); add_row_names = false; endif nA = numel (tbl.VariableValues); nB = numel (in.VariableValues); tbl.VariableContinuity = tabular.merge_continuity ( ... tbl.VariableContinuity, nA, in.VariableContinuity, nB); tbl.VariableDescriptions = tabular.merge_meta ( ... tbl.VariableDescriptions, nA, ... in.VariableDescriptions, nB); tbl.VariableUnits = tabular.merge_meta (tbl.VariableUnits, nA, ... in.VariableUnits, nB); tbl.VariableValues = [tbl.VariableValues, in.VariableValues]; else nA = numel (tbl.VariableValues); nB = numel (in.VariableValues); tbl.VariableContinuity = tabular.merge_continuity ( ... tbl.VariableContinuity, nA, in.VariableContinuity, nB); tbl.VariableDescriptions = tabular.merge_meta ( ... tbl.VariableDescriptions, nA, ... in.VariableDescriptions, nB); tbl.VariableUnits = tabular.merge_meta (tbl.VariableUnits, nA, ... in.VariableUnits, nB); tbl.VariableValues = [tbl.VariableValues, in.VariableValues]; endif if (isempty (tbl.Description)) tbl.Description = in.Description; endif if (isempty (tbl.UserData)) tbl.UserData = in.UserData; endif endfor endif ## Assign variable types in the new table new_types = cellfun ('class', tbl.VariableValues, 'UniformOutput', false); tbl.VariableTypes = new_types; ## Merge custom properties across all inputs: table-scoped properties are ## unioned (the first input wins on a name clash) and variable-scoped ## properties are concatenated across the inputs' variable blocks, NaN- ## filling the block of any input that lacks the property. [cp, cpTypes] = merge_hcat_props (tbl, varargin); tbl.CustomProperties = cp; tbl.CustomPropTypes = cpTypes; tbl = setRowCount (tbl, height (tbl)); endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{TF} =} iscolumn (@var{tbl}) ## ## Test input table for being a column vector. ## ## @qcode{@var{TF} = iscolumn (@var{tbl})} returns @qcode{true} if the input ## table @var{tbl} has a single variable. The number of columns within that ## variable does not matter. ## ## @end deftypefn function TF = iscolumn (this) TF = width (this) == 1; endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{TF} =} isempty (@var{tbl}) ## ## Test input table for being empty. ## ## For tables, @code{isempty} is true if the number of rows is 0 or the ## number of variables is 0. ## ## @end deftypefn function TF = isempty (this) TF = prod (size (this)) == 0; endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{TF} =} isequal (@var{A}, @var{B}) ## @deftypefnx {table} {@var{TF} =} isequal (@var{A}, @var{B}, @dots{}) ## ## Test tables for equality. ## ## @code{@var{TF} = isequal (@var{A}, @var{B})} returns a logical scalar ## @var{TF}, which is @qcode{true} when the tables @var{A} and @var{B} are ## the same size, carry the same variable names, row names and metadata, ## and each pair of corresponding variables holds equal values, and ## @qcode{false} otherwise. ## ## Variables are compared by value and not by class, exactly as ## @code{isequal} compares arrays elsewhere, so a table holding ## @code{int8 ([1; 2])} equals one holding @code{[1; 2]}. The ## @qcode{VariableTypes} property, which only restates those classes, ## takes no part in the comparison. Every other property does: two ## tables differing only in @qcode{Description}, @qcode{UserData}, ## @qcode{VariableUnits}, @qcode{VariableDescriptions} or a custom ## property are not equal. ## ## As with @qcode{NaN}, missing values are never equal, so a missing ## element anywhere in either table makes the result @qcode{false}; use ## @code{isequaln} to treat missing values as equal. ## ## Further tables may be supplied, as in @code{isequal (@var{A}, @var{B}, ## @var{C}, @dots{})}, in which case @var{TF} is @qcode{true} only when ## all of them are equal to one another. Any argument that is not a ## table, a timetable included, makes the result @qcode{false} rather ## than raising an error. ## ## @end deftypefn function TF = isequal (varargin) if (nargin < 2) error ("table.isequal: too few input arguments."); endif TF = false; if (all (cellfun (@(x) isa (x, 'table'), varargin))) TF = isequalResult (varargin{1}, varargin(2:end), false); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{TF} =} isequaln (@var{A}, @var{B}) ## @deftypefnx {table} {@var{TF} =} isequaln (@var{A}, @var{B}, @dots{}) ## ## Test tables for equality, treating missing values as equal. ## ## @code{@var{TF} = isequaln (@var{A}, @var{B})} is identical to ## @code{isequal (@var{A}, @var{B})} except that missing values are ## treated as equal to one another, in the same way that @code{isequaln} ## treats @qcode{NaN}. It returns a logical scalar @var{TF}, which is ## @qcode{true} when the tables are the same size, carry the same ## variable names, row names and metadata, and each pair of corresponding ## elements is either equal or missing in both, and @qcode{false} ## otherwise. ## ## Further tables may be supplied, as in @code{isequaln (@var{A}, ## @var{B}, @var{C}, @dots{})}, in which case @var{TF} is @qcode{true} ## only when all of them are equal to one another. Any argument that is ## not a table, a timetable included, makes the result @qcode{false} ## rather than raising an error. ## ## @end deftypefn function TF = isequaln (varargin) if (nargin < 2) error ("table.isequaln: too few input arguments."); endif TF = false; if (all (cellfun (@(x) isa (x, 'table'), varargin))) TF = isequalResult (varargin{1}, varargin(2:end), true); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{TF} =} ismatrix (@var{tbl}) ## ## Test input table for being a matrix. ## ## For tables, @code{ismatrix} is always true, by definition. ## ## @end deftypefn function TF = ismatrix (this) TF = true; endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{TF} =} isrow (@var{tbl}) ## ## Test input table for being a row vector. ## ## @qcode{@var{TF} = isrow (@var{tbl})} returns @qcode{true} if the input ## table @var{tbl} has a single row. ## ## @end deftypefn function TF = isrow (this) TF = height (this) == 1; endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{TF} =} isscalar (@var{tbl}) ## ## Test input table for being a scalar. ## ## @qcode{@var{TF} = isscalar (@var{tbl})} returns @qcode{true} if the input ## table @var{tbl} has a single row and a single variable. ## ## @end deftypefn function TF = isscalar (this) TF = height (this) == 1 && width (this) == 1; endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tf} =} istable (@var{tbl}) ## ## Return @qcode{true} if input is a table. ## ## @end deftypefn function TF = istable (this) TF = true; endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{TF} =} isvector (@var{tbl}) ## ## Test input table for being a vector. ## ## @qcode{@var{TF} = isvector (@var{tbl})} returns @qcode{true} if the input ## table @var{tbl} has a single row or a single column. ## ## @end deftypefn function TF = isvector (this) TF = isrow (this) || iscolumn (this); endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{out} =} ndims (@var{tbl}) ## ## Number of table dimensions. ## ## For tables, @code{ndims (tbl)} is always 2. ## ## @end deftypefn function out = ndims (this) out = 2; endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{out} =} numel (@var{tbl}) ## ## Total number of elements in table. ## ## @code{@var{out} = numel (@var{tbl})} returns the number of elements in ## the table, @var{tbl}, equivalent to @qcode{prod (size (@var{tbl}))}. A ## table is treated as a two-dimensional container, so this is the number of ## rows times the number of variables. Variables may themselves span ## multiple columns, but @code{numel} only accounts for the number of rows ## and the number of variables, not the underlying columns. ## ## @end deftypefn function out = numel (this, varargin) out = prod (size (this)); endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} repelem (@var{tblA}, @var{rows}, @var{columns}) ## ## Replicate elements of a table. ## ## Replicates each row of the input table @var{tblA} @var{rows} times and ## each variable @var{columns} times, keeping the repeats of a row ## together, in a similar fashion to how @code{repelem} applies to a ## matrix. Each repeated variable takes a numbered name, @qcode{x} ## becoming @qcode{x}, @qcode{x_1}, and so does each repeated row name. ## ## Either count may be a vector with one count per row, or per variable, ## as in @code{repelem (@var{tblA}, [1; 2; 0], 1)}, and a count of zero ## drops that row or variable. Both counts must be given. A table has ## exactly two dimensions, so a lone count is not read as applying to both, ## as it is for a matrix. ## ## @end deftypefn function tbl = repelem (this, varargin) [tbl, errmsg] = repeatResult (this, varargin, true); if (! isempty (errmsg)) error ("table.repelem: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} repmat (@var{tblA}, @var{sz}) ## @deftypefnx {table} {@var{tblB} =} repmat (@var{tblA}, @var{rows}, @var{columns}) ## ## Repeat copies of a table. ## ## Repeats copies of the input table @var{tblA} in a similar fashion ## to how @code{repmat} applies to a matrix. Only two dimensions are ## supported for tables. ## ## @end deftypefn function tbl = repmat (this, varargin) [tbl, errmsg] = repeatResult (this, varargin, false); if (! isempty (errmsg)) error ("table.repmat: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{sz} =} size (@var{tbl}) ## @deftypefnx {table} {@var{dim_sz} =} size (@var{tbl}, @var{dim}) ## @deftypefnx {table} {@var{dim_sz} =} size (@var{tbl}, @var{vecdim}) ## @deftypefnx {table} {[@var{rows}, @var{columns}] =} size (@var{tbl}) ## @deftypefnx {table} {[@var{rows}, @var{columns}, @dots{}] =} size (@var{tbl}) ## ## Return the size of a table. ## ## For tables, the size is [number-of-rows x number-of-variables]. ## This is the same as @code{[height(obj), width(obj)]}. ## ## @code{size (@var{tbl}, @var{dim})} returns the size along dimension ## @var{dim}; dimensions greater than 2 have size 1. @var{dim} may be a ## vector @var{vecdim}, in which case a row vector of the corresponding ## sizes is returned. ## ## @end deftypefn function varargout = size (this, dim) sz = [height(this), width(this)]; if (nargin == 2) ## Sizes along the requested dimension(s); dimensions above 2 are 1. dim_sz = ones (1, numel (dim)); valid = dim <= 2; dim_sz(valid) = sz(dim(valid)); if (nargout > 1) varargout = num2cell (dim_sz); else varargout{1} = dim_sz; endif elseif (nargout <= 1) varargout{1} = sz; else varargout{1} = sz(1); varargout{2} = sz(2); [varargout{3:nargout}] = deal (1); endif endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tblB} =} squeeze (@var{tblA}) ## ## Remove singleton dimensions. ## ## For tables, this is always a no-op that returns the input table ## unmodified, because tables always have exactly 2 dimensions. ## ## @end deftypefn function tbl = squeeze (this) tbl = this; endfunction ## -*- texinfo -*- ## @deftypefn {table} {@var{tbl} =} vertcat (@var{tbl1}, @var{tbl2}, @dots{}) ## ## Vertical concatenation for tables. ## ## @code{@var{tbl} = vertcat (@var{tbl1}, @var{tbl2}, @dots{})} merges ## tables by vertically concatenating them, provided that all input tables ## have the same variable names but not necessarily in the same order. The ## positions of the variable names are matched to those of the first input ## table. ## ## When any input table has row names, they must be unique across all input ## tables. In such a case, rows coming from input tables without row names ## are assigned default @qcode{Row@var{N}} names, where @var{N} is the row's ## position in the output table. Output table's @qcode{Description} and ## @qcode{UserData} properties are assigned using the first non-empty value. ## ## @end deftypefn function tbl = vertcat (varargin) ## Null operands take no part in concatenation varargin = tabular.drop_null_operands (varargin); if (isempty (varargin)) tbl = table (); return; elseif (numel (varargin) == 1 && istable (varargin{1})) tbl = varargin{1}; return; endif ## All inputs must be tables are_tables = cellfun (@istable, varargin); if (! all (are_tables)) error ("table.vertcat: all inputs must be tables."); endif ## All tables must have the same variable names varNames = cellfun (@(obj) obj.VariableNames, varargin, ... 'UniformOutput', false); is_empty = cellfun (@isempty, varNames); sortedVarNames = cellfun (@sort, varNames(! is_empty), ... 'UniformOutput', false); if (numel (sortedVarNames) > 1 && ! isequal (sortedVarNames{:})) error (strcat ("table.vertcat: input tables must have identical", ... " variable names.")); endif ## Identical variable names force identical widths, the sorted lists ## just compared being the same length as the widths they come from. numCols = width (varargin{1}); ## With no variables to stack, nothing else carries the row count. totalRows = sum (cellfun (@height, varargin)); ## We need to figure out some indexing for the variables of every other ## table so we can re-index to the variables of the first table. index = [1:numCols]; # first table is reindexed to itself for i = 2:numel (varNames) fcn = @(x) find (ismember (varNames{1}, x)); index(i,:) = cellfun (fcn, varNames{i}); endfor ## Check for RowNames has_RowNames = ! cellfun (@(obj) isempty (obj.RowNames), varargin); ## Check that all RowNames are unique across tables rowNames = cellfun (@(obj) obj.RowNames, varargin(has_RowNames), ... 'UniformOutput', false); rowNames = [rowNames{:}]; if (numel (rowNames) != numel (unique (rowNames))) error (strcat ("table.vertcat: all input tables must have unique", ... " row names.")); endif ## Start vertical concatenation if (! any (has_RowNames)) # no RowNames in any table (easy) tbl = varargin{1}; for i = 2:numel (varargin) in = varargin{i}; ixVars = index(i,:); in = subsetvars (in, ixVars); for v = 1:numCols tbl.VariableValues{v} = [tbl.VariableValues{v}; ... in.VariableValues{v}]; endfor if (isempty (tbl.VariableContinuity)) tbl.VariableContinuity = in.VariableContinuity; endif if (isempty (tbl.Description)) tbl.Description = in.Description; endif if (isempty (tbl.UserData)) tbl.UserData = in.UserData; endif ## Rows stack over identical variables, so the first table's custom ## properties already describe the result; adopt a later table's only ## when the first table has none. if (isempty (tbl.CustomProperties) && ! isempty (in.CustomProperties)) tbl.CustomProperties = in.CustomProperties; tbl.CustomPropTypes = in.CustomPropTypes; endif endfor else # at least one input table has RowNames ## Input tables without row names get default 'Row' names, where N ## is the row's position in the output table (MATLAB-compatible). tbl = varargin{1}; fcn = @(x) {sprintf("Row%d", x)}; ## If first input table does not have row names, add them here if (isempty (tbl.RowNames)) tbl.RowNames = arrayfun (fcn, 1:height (tbl))'; endif pos = height (tbl); for i = 2:numel (varargin) in = varargin{i}; ixVars = index(i,:); in = subsetvars (in, ixVars); for v = 1:numCols tbl.VariableValues{v} = [tbl.VariableValues{v}; ... in.VariableValues{v}]; endfor ## Handle row names here if (isempty (in.RowNames)) in.RowNames = arrayfun (fcn, pos + (1:height (in)))'; endif tbl.RowNames = [tbl.RowNames; in.RowNames]; pos += height (in); ## Handle remaining stuff if (isempty (tbl.VariableContinuity)) tbl.VariableContinuity = in.VariableContinuity; endif if (isempty (tbl.Description)) tbl.Description = in.Description; endif if (isempty (tbl.UserData)) tbl.UserData = in.UserData; endif ## As above: adopt a later table's custom properties only when the ## first table has none (rows stack over identical variables). if (isempty (tbl.CustomProperties) && ! isempty (in.CustomProperties)) tbl.CustomProperties = in.CustomProperties; tbl.CustomPropTypes = in.CustomPropTypes; endif endfor endif if (numCols == 0) tbl.RowCount = totalRows; endif endfunction endmethods methods (Static) ## -*- texinfo -*- ## @deftypefn {table} {@var{tbl} =} table.empty () ## @deftypefnx {table} {@var{tbl} =} table.empty (@var{n}) ## @deftypefnx {table} {@var{tbl} =} table.empty (@var{r}, @var{v}) ## @deftypefnx {table} {@var{tbl} =} table.empty (@var{sz}) ## ## Create an empty table. ## ## @code{@var{tbl} = table.empty ()} returns a 0-by-0 table. ## ## @code{@var{tbl} = table.empty (@var{r}, @var{v})} returns a table with ## @var{r} rows and @var{v} variables, at least one of which must be ## zero. A table with rows but no variables keeps its height, so ## @code{table.empty (5, 0)} answers 5 to @code{height}. A table with ## variables but no rows names them @qcode{Var1} to @qcode{VarN} and ## gives each of them a @qcode{double} value, exactly as ## @code{table (@qcode{'Size'}, @dots{})} does. ## ## @code{@var{tbl} = table.empty (@var{sz})} takes the two dimensions ## from the two-element vector @var{sz}, and @code{table.empty (@var{n})} ## is the same as @code{table.empty (@var{n}, @var{n})}. ## ## @seealso{table, isempty, height, width} ## @end deftypefn function tbl = empty (varargin) [sz, errmsg] = tabular.emptySize ('table', varargin); if (! isempty (errmsg)) error ('table.empty: %s', errmsg); endif tbl = table ('Size', sz, 'VariableTypes', ... repmat ({'double'}, 1, sz(2))); endfunction endmethods endclassdef ## Validate the optional SETORDER argument shared by the set operations. ## Returns the lower-cased order ('sorted' default) and an errmsg body (empty on ## success) emitted by the caller under its own name. ## The order of the result rows of a set operation, whose row values are P ## as its two inputs contributed them: already in order when 'stable', and ## sorted otherwise. The rows are the result, so they are ordered, not ## looked up; a row with a missing value matches no other. function perm = setop_order (P, order) if (strcmp (order, 'stable')) perm = (1:rows (P))'; else [~, perm] = sortrows (P); endif endfunction function [order, errmsg] = parse_set_order (args) order = 'sorted'; errmsg = ''; if (! isempty (args)) if (numel (args) > 1) errmsg = "too many input arguments."; elseif (! (ischar (args{1}) && isrow (args{1}) && any (strcmpi (args{1}, {'sorted', 'stable'})))) errmsg = "SETORDER must be either 'sorted' or 'stable'."; else order = lower (args{1}); endif endif endfunction ## Add, replace, or append table T to the struct of tables S (read from an ## existing house workbook) for the sheet named SHEET, per WRITEMODE. The ## struct is later written back with 'struct2ods'; sheet names that are not ## The index of the variable named by REF, which may be its name or its ## position. Only one variable may be named: the row times are one vector. function ix = resolveTimeVar (ref, varNames) if (isnumeric (ref) && isscalar (ref) && ref == fix (ref)) ix = ref; if (ix < 1 || ix > numel (varNames)) error ("table.table2timetable: 'RowTimes' index out of bound."); endif return endif if (isa (ref, 'string') && isscalar (ref)) ref = char (ref); endif if (ischar (ref) && isrow (ref)) ref = {ref}; endif if (! (iscellstr (ref) && isscalar (ref))) error (strcat ("table.table2timetable: 'RowTimes' must be a datetime", ... " or duration vector, or the name or index of a single", ... " variable.")); endif ix = find (strcmp (varNames, ref{1})); if (isempty (ix)) error ("table.table2timetable: no such variable in table: '%s'", ref{1}); endif endfunction ## 'pkg test' reads the BISTs from the file it is testing, so a class ## whose suite lives in inst/tests/ reads as carrying none. This block ## answers for it and checks nothing itself. %!test %! ## The suite for this class is inst/tests/table.m-tst. pr0m1th3as-datatypes-9450b29/inst/tabular.m000066400000000000000000015636451527370255000205070ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftp {datatypes} {} tabular ## ## Abstract superclass of @code{table} and @code{timetable}. ## ## @code{tabular} holds everything the two tabular classes share: their ## common metadata properties, their variable storage, and the machinery ## that does not depend on how rows are labelled. It is abstract and cannot ## be instantiated; @code{isa (@var{obj}, "tabular")} is @code{true} for both ## @code{table} and @code{timetable}, and neither derives from the other. ## ## Everything that does depend on row labels is reached through a small set ## of hooks that each subclass implements: @code{table} labels its rows with ## @code{RowNames} and @code{timetable} with @code{RowTimes}. ## ## @end deftp classdef (Abstract) tabular properties ## -*- texinfo -*- ## @deftp {tabular} {property} Description ## ## Table description ## ## Table description specified as a character vector or a string scalar. ## If specified as a string scalar, it is converted and stored internally ## as a character vector. You can access the @qcode{Description} property ## of a table @var{tbl} with @qcode{@var{tbl}.Properties.Description}. ## ## @end deftp Description = '' ## -*- texinfo -*- ## @deftp {tabular} {property} UserData ## ## Additional table information ## ## Additional table information, specified as an array. Any type of data ## can be attached using this property. You can access the @qcode{UserData} ## property of a table @var{tbl} with @qcode{@var{tbl}.Properties.UserData}. ## ## @end deftp UserData = [] ## -*- texinfo -*- ## @deftp {tabular} {property} DimensionNames ## ## Dimension names ## ## Dimension names specified as a two-element cell array of character ## vectors or a two-element string array. If specified as a string array, ## it is converted and stored internally as a cell array of character ## vectors. You can access the @qcode{DimensionNames} property of a table ## @var{tbl} with @qcode{@var{tbl}.Properties.DimensionNames}. ## ## By default, @qcode{DimensionNames} is specified as ## @qcode{'Row', 'Variables'}. You can access table data per rows or per ## columns by using either one of the two dimension names, respectively. ## However, if the table contains row names, then the first element of the ## @qcode{DimensionNames} corresponds to the row names. The second name ## returns the variables joined into one array, as @code{table2array} joins ## them, and assigning to it writes them all, as curly brackets over the ## whole table do. ## ## @end deftp DimensionNames = {'Row', 'Variables'} ## -*- texinfo -*- ## @deftp {tabular} {property} VariableNames ## ## Variable names ## ## Variable names, specified as a cell array of character vectors or a ## string array. If specified as a string array, it is converted and stored ## internally as a cell array of character vectors. All elements must be ## nonempty and distinct, and their number must equal the number of ## variables. You can access the data type of a specific variable by using ## dot name assignment, as in @qcode{@var{tbl}.@var{varname}}, where ## @var{varname} is the name of the variable in table @var{tbl}. If the ## variable name does not exist, a new one is created. ## ## @end deftp VariableNames = {} ## -*- texinfo -*- ## @deftp {tabular} {property} VariableTypes ## ## Variable data types ## ## The class of the data of each variable, defined as a cell array of ## character vectors or a string array with the same number of elements as ## the number of variables in the table. If specified as a string array, ## it is converted and stored internally as a cell array of character ## vectors. You can access the @qcode{VariableTypes} property of a table ## @var{tbl} with @qcode{@var{tbl}.Properties.VariableTypes}. You can ## further index specific variables to access their data type. Modifying ## the elements of the @qcode{VariableTypes} property automatically converts ## the underlying data of the corresponding variable into the specified ## data types provided that a valid conversion is requested. ## ## @end deftp VariableTypes = {} ## -*- texinfo -*- ## @deftp {tabular} {property} VariableDescriptions ## ## Variable descriptions ## ## Variable descriptions, specified as a cell array of character vectors or ## a string array. If specified as a string array, it is converted and ## stored internally as a cell array of character vectors. If not empty ## (default), it must contain the same number of elements as the number of ## variables. If a specific variable does not have a description, this can ## be specified with an individual empty character vector or an empty ## string. You can access the @qcode{VariableDescriptions} property of a ## table @var{tbl} with @qcode{@var{tbl}.Properties.VariableDescriptions}. ## You can further index specific variables to access their description. ## Assigning an empty cell or string array returns the property to its ## default, and describing a single variable of a table that has no ## descriptions gives the remaining variables an empty description. ## ## @end deftp VariableDescriptions = {} ## -*- texinfo -*- ## @deftp {tabular} {property} VariableUnits ## ## Variable units ## ## Variable units, specified as a cell array of character vectors or a ## string array. If specified as a string array, it is converted and stored ## internally as a cell array of character vectors. If not empty (default), ## it must contain the same number of elements as the number of variables. ## If a specific variable does not have a unit, this can be specified with ## an individual empty character vector or an empty string. You can access ## the @qcode{VariableUnits} property of a table @var{tbl} with ## @qcode{@var{tbl}.Properties.VariableUnits}. You can further index ## specific variables to access their unit. Assigning an empty cell or ## string array returns the property to its default, and giving a unit to ## a single variable of a table that has none gives the remaining ## variables an empty unit. ## ## @end deftp VariableUnits = {} ## -*- texinfo -*- ## @deftp {tabular} {property} VariableContinuity ## ## Variable continuity ## ## Continuity of each variable, specified as a cell array of character ## vectors or a string array carrying one element per variable, each of ## them @qcode{'unset'}, @qcode{'continuous'}, @qcode{'step'} or ## @qcode{'event'}. It is empty by default, and assigning @code{@{@}} or ## @code{[]} clears it. If specified as a string array, it is converted ## and stored internally as a cell array of character vectors. You can ## access it with @qcode{@var{tbl}.Properties.VariableContinuity} and you ## can index individual variables to read or assign their continuity. ## ## A @code{table} carries the property but does not act on it, which is ## also how MATLAB behaves. A @code{timetable} uses it to choose the ## default fill method of each variable when resampling. ## ## MATLAB stores this property as a @qcode{matlab.tabular.Continuity} ## enumeration. Octave has no enumeration classes, so it is stored and ## returned here as a cell array of character vectors, as ## @qcode{VariableNames} and @qcode{VariableUnits} are. ## ## @end deftp VariableContinuity = [] ## -*- texinfo -*- ## @deftp {tabular} {property} CustomProperties ## ## Customized metadata of table and its variables ## ## Custom properties that contain metadata of a table and its variables. ## By default, this is an empty container. Each custom property holds ## either table metadata or per-variable metadata, according to the property ## type (@qcode{'table'} or @qcode{'variable'}) specified when the property ## is created with the @code{addprop} method. A variable-scoped property ## holds one element per variable. ## ## You can add an individual custom property only by using the ## @code{addprop} method and you can only remove a custom property with the ## @code{rmprop} method. To access existing custom properties use dot name ## structure assignment as in ## @qcode{@var{tbl}.Properties.CustomProperties.@var{PropertyName}}, where ## @var{PropertyName} is the name used with the @code{addprop} method. ## ## The whole set may also be taken from another table by assigning that ## table's @qcode{CustomProperties} to this one, which replaces every ## custom property with those of the other table, their types included. A ## variable-scoped property arriving that way must hold one element for ## each variable of the receiving table, or be a 0-by-0 empty. Nothing ## else can be assigned there. ## ## @end deftp CustomProperties = [] endproperties properties (Access = protected) CustomPropTypes = struct () VariableValues = {} ## The row count, which outlives the last variable. While an object has ## variables its first one is authoritative and this is not read, so it ## only has to be right at the moment the variables go: every path that ## drops one records it first. A class whose row labels are mandatory ## reads its count off them and never reaches this. RowCount = 0 endproperties ################################################################################ ## ** Subclass hooks ** ## ################################################################################ ## ## ## Every subclass must implement all twenty-one. Octave's classdef has no ## ## 'methods (Abstract)' block, so the contract cannot be declared; these ## ## raising defaults stand in for it, and name the subclass that is missing ## ## one because 'class (this)' resolves downwards. ## ## ## ## Nineteen of them concern row labels, which is the whole of what ## ## separates one tabular class from another; the other two name the ## ## properties object and build the result of an apply method. ## ## ## ## 'hasRowLabels' whether the object carries row labels at all ## ## 'getRowLabels' the labels themselves, in their own type ## ## 'rowLabelName' the name the labels are known by ## ## 'rowLabelStrings' the labels rendered for display ## ## 'rowLabelHeader' the heading printed over them, if any ## ## 'rowLabelKeyNames' the names that mean "by the labels" rather than a var ## ## 'sortsByLabelsByDefault' whether a bare 'sortrows' orders by them ## ## 'uniqueIncludesLabels' whether they make a row distinct ## ## 'usableRowLabels' which rows carry a label that can be used ## ## 'fillSamplePoints' the points the variables are sampled at ## ## 'groupsByLabels' whether a grouping reference may name them ## ## 'rowLabelProperties' the row label metadata, named as it is published ## ## 'setRowLabelProperty' one of those properties assigned ## ## 'subsetRowLabels' the object with its labels subset by an index ## ## 'clearRowLabels' the object with its labels removed ## ## 'resolveRowRef' a row reference resolved to row indices ## ## 'makeProperties' the properties object this class's metadata lives in ## ## 'assembleApply' an object built from an apply method's output ## ## 'repeatRowLabels' the labels with each row repeated N times ## ## 'plainTable' the variables as a table, the labels dropped ## ## 'setRowLabels' the object given a set of labels of its own type ## ## ## ################################################################################ methods (Access = protected) ## Whether this object carries row labels at all. A 'table' answers false ## whenever it has no 'RowNames', which is the common case; a class whose ## labels are mandatory answers true always. function tf = hasRowLabels (this) error ("%s: subclass must implement hasRowLabels.", class (this)); endfunction ## The labels themselves, in their own type: a cellstr for a 'table', a ## datetime or duration for a class that labels its rows by time. Callers ## that need text ask 'rowLabelStrings' instead. function out = getRowLabels (this) error ("%s: subclass must implement getRowLabels.", class (this)); endfunction ## The name the labels are known by, 'RowNames' for a 'table'. Used ## wherever they have to be named rather than shown: the properties ## listing, the summary, and the header of an exported file. function out = rowLabelName (this) error ("%s: subclass must implement rowLabelName.", class (this)); endfunction ## The labels rendered for display, as a column cellstr. Separate from ## 'getRowLabels' because labels that are not already text must go through ## their own format before anything can print or export them. function out = rowLabelStrings (this) error ("%s: subclass must implement rowLabelStrings.", class (this)); endfunction ## The heading printed over the row label column, or empty for a class ## whose labels carry none. A table's row names are headed by nothing at ## all, not even the row dimension name; a class whose labels are a ## dimension in their own right names them. function out = rowLabelHeader (this) error ("%s: subclass must implement rowLabelHeader.", class (this)); endfunction ## The names that mean "order by the row labels" rather than by a ## variable. A table answers to 'RowNames' and to its row dimension ## name, a timetable to its row dimension name alone. function out = rowLabelKeyNames (this) error ("%s: subclass must implement rowLabelKeyNames.", class (this)); endfunction ## What the row-label column of an exported file is tagged with, which ## says what kind of labels it holds and not merely what they are called. ## Text labels are tagged 'RowNames', as they always were. Row times are ## tagged 'RowTimes' followed by their own type string, so that a reader ## knows to rebuild a timetable and knows the type, zone and format to ## rebuild it with; their name is a dimension name and travels in the ## variable-name row beside them. function out = rowLabelTag (this) lbl = getRowLabels (this); if (isa (lbl, 'datetime') || isa (lbl, 'duration')) out = ['RowTimes|', tabular.value_type_string(lbl)]; else out = rowLabelName (this); endif endfunction ## Whether a bare 'sortrows (obj)' orders by the row labels. A table ## orders by every variable it has, a timetable by its row times. function tf = sortsByLabelsByDefault (this) error (strcat ("%s: subclass must implement", ... " sortsByLabelsByDefault."), class (this)); endfunction ## Whether the row labels are part of what makes a row distinct. A ## timetable's row times are, a table's row names are not. function tf = uniqueIncludesLabels (this) error (strcat ("%s: subclass must implement", ... " uniqueIncludesLabels."), class (this)); endfunction ## Whether the row labels are still usable, one logical per row. A ## table's row names never disqualify a row; a timetable's row times do ## when they are missing, there being no placing such a row in time. function tf = usableRowLabels (this) error (strcat ("%s: subclass must implement", ... " usableRowLabels."), class (this)); endfunction ## Whether a grouping reference may name the row labels instead of a ## variable. A timetable's row times are an ordinary grouping key, ## named by the row dimension; a table's row names are not one, being ## unique and so grouping nothing together. function tf = groupsByLabels (this) error (strcat ("%s: subclass must implement", ... " groupsByLabels."), class (this)); endfunction ## The points the variables are sampled at, for the methods that ## interpolate or measure a distance. A table has only the row order to ## go on; a timetable has its row times, which is why the same gap fills ## differently in the two. function [x, ownPoints, errmsg] = fillSamplePoints (this) error (strcat ("%s: subclass must implement", ... " fillSamplePoints."), class (this)); endfunction ## The row label metadata as a struct, keyed by the names the properties ## object publishes it under. Separate from 'rowLabelName', which names ## the labels for a file header and for a timetable is the row dimension ## name: a class may publish its labels under a different name than that, ## and may publish more than one property describing them. function out = rowLabelProperties (this) error ("%s: subclass must implement rowLabelProperties.", class (this)); endfunction ## One row label property assigned. 'subsasgn' has already handled the ## nine shared properties and every name left over is one of these, so ## HANDLED reports whether NAME was recognised and an unrecognised name ## is refused by the caller. CHAIN_S carries any further subscripts. function [this, handled] = setRowLabelProperty (this, name, val, chain_s) error ("%s: subclass must implement setRowLabelProperty.", class (this)); endfunction ## This object's metadata re-resolved after its variable names changed ## without any of them being renamed: a designation naming a variable ## that is no longer there stops meaning anything, and is dropped. The ## default has nothing that names a variable. function this = varsChanged (this) endfunction ## This object's metadata carried across a rename. OLDNAMES and NEWNAMES ## are the same length and pair up, and every other name is unchanged. A ## designation naming a renamed variable follows it, which is where a ## rename parts company with a removal: the variable is still there. function this = varsRenamed (this, oldNames, newNames) endfunction ## TBL rebuilt as this object's class, where this object's class is the ## more derived of the two. A binary operation takes the most derived ## class among its operands rather than the class of the first, so each ## operand is offered the result in turn and all but the most derived ## decline. The default declines. function tbl = promoteResult (this, tbl) endfunction ## This object with any attached event table detached. It is the seam a ## shared body uses where an intermediate result must not carry events ## onward, and it is not the property route: assigning to 'Events' is ## refused for a class that has no such property, while detaching what ## was never there is simply nothing to do. function this = detachEvents (this) endfunction ## The event table attached to this object, or empty where the class has ## no such thing. Only a 'timetable' carries one, and an 'eventtable' ## carries none however much it inherits, so the property is reached ## through here and never named by a shared body. function out = eventsOf (this) out = []; endfunction ## This object with its labels subset by IXROWS, the same index the caller ## has just applied to the variables. A class whose labels are optional ## leaves them alone when there are none to subset. function this = subsetRowLabels (this, ixRows) error ("%s: subclass must implement subsetRowLabels.", class (this)); endfunction ## This object with its labels removed. Used where a result cannot carry ## them, as in the side of an outer join whose unmatched rows have no ## label to inherit. function this = clearRowLabels (this) error ("%s: subclass must implement clearRowLabels.", class (this)); endfunction ## A row reference resolved to row indices. ROWREF is a cellstr of label ## names for a 'table'; a class that labels rows by time resolves a time. ## Raises when the object carries no labels to match against. function ixRows = resolveRowRef (this, rowRef) error ("%s: subclass must implement resolveRowRef.", class (this)); endfunction ## This object with LABELS as its row labels, given in the class's own ## type. The counterpart of 'getRowLabels', for the few places that build ## a set of labels rather than subsetting the ones there are. function this = setRowLabels (this, labels) error (strcat ("%s: subclass must implement", ... " setRowLabels."), class (this)); endfunction ## The variables of this object as a plain table, its row labels dropped. ## A join lays the right operand's variables beside the left's, and only ## the left's labels survive, so the right side is reduced to this first. function out = plainTable (this) error (strcat ("%s: subclass must implement", ... " plainTable."), class (this)); endfunction ## This object with its row labels repeated N times, ELEMENTWISE placing ## each row's repeats together as 'repelem' does and otherwise repeating ## the whole block as 'repmat' does. A class whose labels must be unique ## numbers the repeats; one whose labels may repeat leaves them alone. function this = repeatRowLabels (this, n, elementwise) error (strcat ("%s: subclass must implement", ... " repeatRowLabels."), class (this)); endfunction ## This object with its row labels extended to N rows. An assignment to ## a row past the end grows the variables by padding them, and the labels ## have to grow with them or the object is left counting two heights at ## once. Each class says what a row added this way is labelled by. function this = growRowLabels (this, n) error (strcat ("%s: subclass must implement", ... " growRowLabels."), class (this)); endfunction ## An object of this class assembled from the output of an apply method: ## VARS holds the variable values and NAMES their names. Each class takes ## the row labels from the argument that means something to it and ignores ## the other: ROWLABELS holds them in the class's own type, empty where ## the result carries none, and ROWIX indexes the input row each output ## row takes its label from, empty where the caller has none to give. function out = assembleApply (this, vars, names, rowLabels, rowIx, ... caller) error (strcat ("%s: subclass must implement", ... " assembleApply."), class (this)); endfunction ## The properties object this class's metadata lives in, which is what ## 'tbl.Properties' returns. Not a row label hook: it is here because it ## is the other thing that differs per subclass, each class having its own ## subclass of 'datatypes.tabular.TabularProperties'. function out = makeProperties (this) error ("%s: subclass must implement makeProperties.", class (this)); endfunction endmethods ################################################################################ ## ** Display, reference and assignment ** ## ################################################################################ ## ## ## The display path, the two reference overloads and the assignment ## ## overload. Only the last branch of 'subsasgn' is row-label aware, and ## ## that one asks the subclass. ## ## ## ################################################################################ methods (Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif disp (this); endfunction ## Custom display. The header names the class rather than saying ## 'table', so that a timetable says what it is. ## ## An object with no rows still has columns to show, so the body is ## printed whenever there is a column: any variable will do, and for a ## class that heads its row label column, so will a row. That is what ## leaves a table with no variables printing nothing at all while a ## timetable with none still shows the times it labels its rows by. function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif if (isempty (this)) fprintf (" %dx%d empty %s\n\n", size (this), class (this)); else fprintf (" %dx%d %s\n\n", height (this), width (this), class (this)); endif if (width (this) > 0 || (height (this) > 0 && ! isempty (rowLabelHeader (this)))) print_table (this); endif endfunction ## Overload 'end' keyword function last_index = end (this, end_dim, ndim_obj) lastdim = ndims (this); if (end_dim == ndim_obj && ndim_obj == 1) last_index = prod (size (this)); elseif (end_dim == ndim_obj && end_dim < lastdim) last_index = prod (size (this)(end_dim:lastdim)); else last_index = size (this, end_dim); endif endfunction ## Class specific subscripted reference function varargout = subsref (this, s) clstype = class (this); chain_s = s(2:end); s = s(1); switch (s.type) case '()' if (numel (s.subs) != 2) error (strcat ("%s.subsref: '()' indexing of %s requires", ... " exactly two arguments."), clstype, clstype); endif [ixRow, ixVar] = resolveRowVarRefs (this, s.subs{1}, s.subs{2}); tbl = this; tbl = subsetrows (tbl, ixRow); tbl = subsetvars (tbl, ixVar); case '{}' if (numel (s.subs) != 2) error (strcat ("%s.subsref: '{}' indexing of %s requires", ... " exactly two arguments."), clstype, clstype); endif [ixRow, ixVar] = resolveRowVarRefs (this, s.subs{1}, s.subs{2}); tbl = this; tbl = subsetrows (tbl, ixRow); tbl = subsetvars (tbl, ixVar); tbl = varsAsArray (tbl, 'subsref'); case '.' ## A field name may be given as a string scalar, as in MATLAB. if (isstring (s.subs) && isscalar (s.subs)) s.subs = char (s.subs); endif if (! (ischar (s.subs) && isrow (s.subs))) error (strcat ("%s.subsref: '.' index argument must be a", ... " character vector or a string scalar."), clstype); endif ## Handle special cases: "Properties" and "DimensionNames" if (isequal (s.subs, 'Properties')) tbl = makeProperties (this); elseif (isequal (s.subs, this.DimensionNames{1})) tbl = getRowLabels (this); elseif (isequal (s.subs, this.DimensionNames{2})) tbl = varsAsArray (this, 'subsref'); ## Everything else is indexing an existing variable name else tbl = getvar (this, s.subs); endif endswitch ## Chained references if (! isempty (chain_s)) tbl = subsref (tbl, chain_s); endif varargout{1} = tbl; endfunction ## Class specific subscripted assignment. Every branch is shared but ## the last: a property that is none of the nine belongs to the ## subclass's own row labels, and 'setRowLabelProperty' assigns it. function tbl = subsasgn (this, s, val) clstype = class (this); ## Chained subscripts chain_s = s(2:end); s = s(1); if (! isempty (chain_s) && ! isequal (s.subs, 'Properties')) rhs_in = single_subref (this, s); rhs = subsasgn (rhs_in, chain_s, val); else rhs = val; endif tbl = this; switch (s.type) case '()' if (numel (s.subs) != 2) error (strcat ("%s.subsasgn: '()' indexing of %s", ... " requires exactly two arguments."), ... clstype, clstype); endif ## Assigning [] deletes rows or variables. MATLAB's trigger is ## the literal empty matrix, which Octave cannot tell apart from ## any other 0-by-0 double, so any of those deletes. if (isempty (chain_s) && isa (rhs, 'double') && isequal (size (rhs), [0, 0])) tbl = deleteSubs (this, s.subs{1}, s.subs{2}); return; endif tbl = assignSubs (this, s.subs, rhs, false); case '{}' if (numel (s.subs) != 2) error (strcat ("%s.subsasgn: '{}' indexing of %s", ... " requires exactly two arguments."), ... clstype, clstype); endif ## '{}' assigns contents and never deletes, as in MATLAB. if (isempty (chain_s) && isa (rhs, 'double') && isequal (size (rhs), [0, 0])) error (strcat ("%s.subsasgn: '{}' indexing cannot delete; use", ... " '()' to delete rows or variables."), clstype); endif tbl = assignSubs (this, s.subs, rhs, true); case '.' ## A field name may be given as a string scalar, as in MATLAB. if (isstring (s.subs) && isscalar (s.subs)) s.subs = char (s.subs); endif if (! (ischar (s.subs) && isrow (s.subs))) error (strcat ("%s.subsasgn: '.' index argument must be a", ... " character vector or a string scalar."), clstype); endif ## Grab Properties if (isequal (s.subs, 'Properties')) ## no further recursion, everything is handled here if (isempty (chain_s)) error ("%s.subsasgn: cannot assign new properties.", clstype); endif s = chain_s(1); ## A property name may be given as a string scalar, as in MATLAB. if (isstring (s.subs) && isscalar (s.subs)) s.subs = char (s.subs); endif if (! (ischar (s.subs) && isrow (s.subs))) error (strcat ("%s.subsasgn: '.' index argument must be a", ... " character vector or a string scalar."), clstype); endif ## Handle table properties if (isequal (s.subs, 'Description')) ## Check for valid input: character vector of string if (isa (val, 'string')) if (numel (val) > 1) error (strcat ("%s.subsasgn: %s description must", ... " be a character vector or a string", ... " scalar."), clstype, clstype); endif val = cellstr (val){1}; elseif (! ischar (val)) error (strcat ("%s.subsasgn: %s description must be", ... " a character vector or a string scalar."), ... clstype, clstype); endif this.Description = val; tbl = this; elseif (isequal (s.subs, 'UserData')) ## Any kind !! this.UserData = val; tbl = this; elseif (isequal (s.subs, 'DimensionNames')) ## Check for further indexing of specific variable(s) if (numel (chain_s) > 1) idx = chain_s(2).subs; if (numel (idx) > 1) error (strcat ("%s.subsasgn: cannot index", ... " DimensionNames with more than one", ... " dimension. Use a vector to index", ... " multiple DimensionNames at once."), clstype); endif idx = cell2mat (idx); if (isequal (idx, ':')) idx = [1:2]; endif if (! all (ismember (idx, [1:2]))) error (strcat ("%s.subsasgn: out of bound index for", ... " DimensionNames."), clstype); endif if (ischar (val) || isa (val, 'string')) val = cellstr (val); endif if (! (iscellstr (val) && numel (val) == numel (idx))) error (strcat ("%s.subsasgn: DimensionNames must be", ... " a cell array of character vectors or", ... " a string array matching the number of", ... " indexed variables."), clstype); endif this.DimensionNames(idx) = val; tbl = this; return endif ## Check for valid input: two-element cellstring or string array if (ischar (val) || isa (val, 'string')) val = cellstr (val); endif if (! (iscellstr (val) && numel (val) == 2)) error (strcat ("%s.subsasgn: DimensionNames must be a", ... " two-element cell array of character", ... " vectors or string array."), clstype); endif this.DimensionNames = val; tbl = this; elseif (isequal (s.subs, 'VariableNames')) ## Check for further indexing of specific variable(s) if (numel (chain_s) > 1) idx = chain_s(2).subs; if (numel (idx) > 1) error (strcat ("%s.subsasgn: cannot index", ... " VariableNames with more than one", ... " dimension. Use a vector to index", ... " multiple VariableNames at once."), clstype); endif idx = cell2mat (idx); if (isequal (idx, ':')) idx = [1:width(this)]; endif if (! all (ismember (idx, [1:width(this)]))) error (strcat ("%s.subsasgn: out of bound index for", ... " VariableNames."), clstype); endif if (ischar (val) || isa (val, 'string')) val = cellstr (val); endif if (! (iscellstr (val) && numel (val) == numel (idx))) error (strcat ("%s.subsasgn: VariableNames must be", ... " a cell array of character vectors or", ... " a string array matching the number of", ... " indexed variables."), clstype); endif oldNames = this.VariableNames(idx); this.VariableNames(idx) = val; tbl = varsRenamed (this, oldNames, val); return endif ## Check for valid input: cellstring or string array matching ## the number of variables in the table if (ischar (val) || isa (val, 'string')) val = cellstr (val); endif if (! (iscellstr (val) && numel (val) == width (this))) error (strcat ("%s.subsasgn: VariableNames must be a", ... " cell array of character vectors or a", ... " string array matching the number of", ... " variables."), clstype); endif oldNames = this.VariableNames; this.VariableNames = val; tbl = varsRenamed (this, oldNames, val); elseif (isequal (s.subs, 'VariableTypes')) ## Check for further indexing of specific variable(s) if (numel (chain_s) > 1) idx = chain_s(2).subs; if (numel (idx) > 1) error (strcat ("%s.subsasgn: cannot index", ... " VariableTypes with more than one", ... " dimension. Use a vector to index", ... " multiple VariableTypes at once."), clstype); endif idx = cell2mat (idx); if (isequal (idx, ':')) idx = [1:width(this)]; endif if (! all (ismember (idx, [1:width(this)]))) error (strcat ("%s.subsasgn: out of bound index for", ... " VariableTypes."), clstype); endif if (ischar (val) || isa (val, 'string')) val = cellstr (val); endif if (! (iscellstr (val) && numel (val) == numel (idx))) error (strcat ("%s.subsasgn: VariableTypes must be", ... " a cell array of character vectors or", ... " a string array matching the number of", ... " indexed variables."), clstype); endif ## Convert each selected variable to its new data type; ## convertvars updates both the data and the VariableTypes ## entry for the corresponding variable. tbl = this; for k = 1:numel (idx) tbl = convertvars (tbl, idx(k), val{k}); endfor return endif ## Check for valid input: cellstring or string array matching ## the number of variables in the table if (ischar (val) || isa (val, 'string')) val = cellstr (val); endif if (! (iscellstr (val) && numel (val) == width (this))) error (strcat ("%s.subsasgn: VariableTypes must be a", ... " cell array of character vectors or a", ... " string array matching the number of", ... " variables."), clstype); endif ## Convert each variable to its new data type; convertvars ## updates both the data and the VariableTypes entry for the ## corresponding variable. tbl = this; for k = 1:width (this) tbl = convertvars (tbl, k, val{k}); endfor elseif (isequal (s.subs, 'VariableDescriptions')) ## Check for further indexing of specific variable(s) if (numel (chain_s) > 1) idx = chain_s(2).subs; if (numel (idx) > 1) error (strcat ("%s.subsasgn: cannot index", ... " VariableDescriptions with more than", ... " one dimension. Use a vector to index", ... " multiple VariableDescriptions at", ... " once."), clstype); endif idx = cell2mat (idx); if (isequal (idx, ':')) idx = [1:width(this)]; endif if (! all (ismember (idx, [1:width(this)]))) error (strcat ("%s.subsasgn: out of bound index for", ... " VariableDescriptions."), clstype); endif if (ischar (val) || isa (val, 'string')) val = cellstr (val); endif if (! (iscellstr (val) && numel (val) == numel (idx))) error (strcat ("%s.subsasgn: VariableDescriptions", ... " must be a cell array of character", ... " vectors or a string array matching", ... " the number of indexed variables."), clstype); endif ## Naming one entry of an unset property sets them all, the ## rest blank: the property is either unset or one entry per ## variable, never anything in between. if (isempty (this.VariableDescriptions)) this.VariableDescriptions = repmat ({''}, 1, width (this)); endif this.VariableDescriptions(idx) = val; tbl = this; return endif ## An empty returns the property to unset. if ((iscell (val) || isa (val, 'string')) && isempty (val)) this.VariableDescriptions = {}; tbl = this; return endif ## Check for valid input: cellstring or string array matching ## the number of variables in the table if (ischar (val) || isa (val, 'string')) val = cellstr (val); endif if (! (iscellstr (val) && numel (val) == width (this))) error (strcat ("%s.subsasgn: VariableDescriptions", ... " must be a cell array of character", ... " vectors or a string array matching the", ... " number of variables."), clstype); endif this.VariableDescriptions = val; tbl = this; elseif (isequal (s.subs, 'VariableUnits')) ## Check for further indexing of specific variable(s) if (numel (chain_s) > 1) idx = chain_s(2).subs; if (numel (idx) > 1) error (strcat ("%s.subsasgn: cannot index", ... " VariableUnits with more than one", ... " dimension. Use a vector to index", ... " multiple VariableUnits at once."), clstype); endif idx = cell2mat (idx); if (isequal (idx, ':')) idx = [1:width(this)]; endif if (! all (ismember (idx, [1:width(this)]))) error (strcat ("%s.subsasgn: out of bound index for", ... " VariableUnits."), clstype); endif if (ischar (val) || isa (val, 'string')) val = cellstr (val); endif if (! (iscellstr (val) && numel (val) == numel (idx))) error (strcat ("%s.subsasgn: VariableUnits must be", ... " a cell array of character vectors or", ... " a string array matching the number of", ... " indexed variables."), clstype); endif ## Naming one entry of an unset property sets them all, the ## rest blank: the property is either unset or one entry per ## variable, never anything in between. if (isempty (this.VariableUnits)) this.VariableUnits = repmat ({''}, 1, width (this)); endif this.VariableUnits(idx) = val; tbl = this; return endif ## An empty returns the property to unset. if ((iscell (val) || isa (val, 'string')) && isempty (val)) this.VariableUnits = {}; tbl = this; return endif ## Check for valid input: cellstring or string array matching ## the number of variables in the table if (ischar (val) || isa (val, 'string')) val = cellstr (val); endif if (! (iscellstr (val) && numel (val) == width (this))) error (strcat ("%s.subsasgn: VariableUnits must be a", ... " cell array of character vectors or a", ... " string array matching the number of", ... " variables."), clstype); endif this.VariableUnits = val; tbl = this; elseif (isequal (s.subs, 'VariableContinuity')) ## Check for further indexing of specific variable(s) if (numel (chain_s) > 1) idx = chain_s(2).subs; if (numel (idx) > 1) error (strcat ("%s.subsasgn: cannot index", ... " VariableContinuity with more than one", ... " dimension. Use a vector to index", ... " multiple variables at once."), clstype); endif idx = cell2mat (idx); if (isequal (idx, ':')) idx = [1:width(this)]; endif if (! all (ismember (idx, [1:width(this)]))) error (strcat ("%s.subsasgn: out of bound index for", ... " VariableContinuity."), clstype); endif if (isempty (this.VariableContinuity)) this.VariableContinuity = repmat ({'unset'}, ... [1, width(this)]); endif val = check_continuity (val, numel (idx), true, clstype); this.VariableContinuity(idx) = val; tbl = this; return endif ## An empty value of any type clears the property if (isempty (val)) this.VariableContinuity = []; tbl = this; return; endif this.VariableContinuity = check_continuity (val, width (this), ... false, clstype); tbl = this; elseif (isequal (s.subs, 'CustomProperties')) ## Assigning the store itself replaces it whole, as in MATLAB, ## and only a store taken from a table can be assigned. The ## types travel with it, so a variable property stays one. if (numel (chain_s) < 2) if (! isa (val, 'datatypes.tabular.CustomProperties')) error (strcat ("%s.subsasgn: the value assigned to", ... " 'CustomProperties' must be a", ... " datatypes.tabular.CustomProperties", ... " object."), clstype); endif [cpVals, cpTypes] = unpack (val); cpNames = fieldnames (cpVals); for i = 1:numel (cpNames) cpVal = cpVals.(cpNames{i}); if (! strcmp (cpTypes.(cpNames{i}), 'variable')) continue; endif ## A 0-by-0 empty fits a table of any width. if (ndims (cpVal) == 2 && all (size (cpVal) == 0)) continue; endif if (numel (cpVal) != width (this)) error (strcat ("%s.subsasgn: custom property '%s'", ... " must have one element for each", ... " variable in the table, or be a 0-by-0", ... " empty."), clstype, cpNames{i}); endif endfor ## An empty store is [] with no types, as everywhere else. if (isempty (cpNames)) this.CustomProperties = []; this.CustomPropTypes = struct (); else this.CustomProperties = cpVals; this.CustomPropTypes = cpTypes; endif tbl = this; return; endif ## Check for valid indexing a custom property if (! strcmp (chain_s(2).type, '.')) error (strcat ("%s.subsasgn: use '.' notation to", ... " index a custom property."), clstype); endif cpName = chain_s(2).subs; ## A property name may be given as a string scalar, as in MATLAB. if (isstring (cpName) && isscalar (cpName)) cpName = char (cpName); endif if (! (ischar (cpName) && isrow (cpName))) error (strcat ("%s.subsasgn: indexing a custom property", ... " requires a character vector or a string", ... " scalar."), clstype); endif ## Check that referenced custom property exists if (isempty (this.CustomProperties)) error (strcat ("%s.subsasgn: custom property '%s'", ... " does not exist, use 'addprop' to add", ... " it."), clstype, ... cpName); endif existingNames = fieldnames (this.CustomProperties); if (! ismember (cpName, existingNames)) error (strcat ("%s.subsasgn: custom property '%s'", ... " does not exist, use 'addprop' to add", ... " it."), clstype, ... cpName); endif ## Get type of custom property cpType = this.CustomPropTypes.(cpName); if (strcmp (cpType, 'table')) ## A 'table' property is metadata the class never reads, so ## it holds any value of any type and size, stored as given. if (numel (chain_s) > 2) error (strcat ("%s.subsasgn: custom property '%s'", ... " is a table property and cannot be", ... " indexed any further."), clstype, ... cpName); endif this.CustomProperties.(cpName) = val; else maxIdx = width (this); ## Get further indexing (if available) if (numel (chain_s) > 2) if (strcmp (chain_s(3).type, '.')) error (strcat ("%s.subsasgn: custom property '%s'", ... " is a variable property and cannot", ... " be indexed any further with '.'", ... " notation."), clstype, ... cpName); endif cpIdx = chain_s(3).subs; if (numel (cpIdx) > 1) error (strcat ("%s.subsasgn: cannot index a", ... " custom variable property in more", ... " than one dimension."), clstype); endif cpIdx = cell2mat (cpIdx); if (isequal (cpIdx, ':')) cpIdx = [1:maxIdx]; elseif (islogical (cpIdx)) ## A logical mask selects the variables it marks. It may ## be shorter than the table but never longer. if (numel (cpIdx) > maxIdx) error (strcat ("%s.subsasgn: out of bound index", ... " for custom variable property", ... " '%s'."), clstype, cpName); endif cpIdx = find (cpIdx); endif if (! all (ismember (cpIdx, [1:maxIdx]))) error (strcat ("%s.subsasgn: out of bound index", ... " for custom variable property", ... " '%s'."), clstype, cpName); endif ## Check input is a vector if (! isvector (val)) error (strcat ("%s.subsasgn: assigned value to a", ... " custom variable property must be a", ... " vector."), clstype); endif if (numel (val) != numel (cpIdx)) error (strcat ("%s.subsasgn: input vector does", ... " not match the number of indexed", ... " variables in the custom variable", ... " property '%s'."), clstype, ... cpName); endif ## A cleared property is re-expanded to the table's width ## before the assignment, padded the way merging pads it, ## so indexing can never leave a short vector behind. tmp = this.CustomProperties.(cpName); if (numel (tmp) != maxIdx) if (iscell (tmp) || iscell (val)) tmp = cell (1, maxIdx); else tmp = NaN (1, maxIdx); endif endif tmp(cpIdx) = val; this.CustomProperties.(cpName) = tmp; else ## A character vector is never a per-variable value, so the ## type is checked before the size: '' is 0-by-0 and would ## otherwise read as a clearing value. if (ischar (val)) error (strcat ("%s.subsasgn: a character vector is", ... " not a valid value for a custom", ... " variable property, use a cell array", ... " of character vectors or a string", ... " array."), clstype); endif ## A 0-by-0 empty clears the property whatever the table's ## width and is stored as []; empties of any other size are ## not accepted. if (ndims (val) == 2 && all (size (val) == 0)) val = []; elseif (! isvector (val)) error (strcat ("%s.subsasgn: assigned value to a", ... " custom variable property must be a", ... " vector."), clstype); elseif (numel (val) != maxIdx) error (strcat ("%s.subsasgn: input vector does", ... " not match the number of", ... " variables."), clstype); endif this.CustomProperties.(cpName) = val; endif endif tbl = this; else ## Whatever is left names the subclass's own row labels, and ## only the subclass knows how to assign it. [tbl, handled] = setRowLabelProperty (this, s.subs, val, ... chain_s); if (! handled) error ("%s.subsasgn: unknown %s property '%s'.", ... clstype, clstype, s.subs); endif endif elseif (isequal (s.subs, this.DimensionNames{2})) ## The variable dimension name writes the contents of every ## variable, as '{}' over all of them does, but a value that is ## not one row for each row of the object is refused. if (size (rhs, 1) != height (this) && ! (height (this) == 0 && width (this) == 0)) error (strcat ("%s.subsasgn: the value assigned to '%s' must", ... " have %d rows, one for each row of the %s."), ... clstype, s.subs, height (this), clstype); endif tbl = assignSubs (this, {':', ':'}, rhs, true); else ## Everything else is indexing a variable name (existing of new), ## except that assigning [] to one deletes it. if (isempty (chain_s) && isa (rhs, 'double') && isequal (size (rhs), [0, 0])) if (isequal (s.subs, this.DimensionNames{1})) error (strcat ("%s.subsasgn: cannot delete the row labels", ... " of a %s."), clstype, clstype); endif tbl = deleteVars (this, s.subs); else tbl = setvar (this, s.subs, rhs); endif endif endswitch endfunction endmethods ################################################################################ ## ** Forbidden methods and flatteners ** ## ################################################################################ ## ## ## The shape shims, which no tabular class supports, and the two ## ## flatteners the spreadsheet exporters share. ## ## ## ################################################################################ methods (Hidden) ## A table has two dimensions that mean different things, and the larger ## of them is not a useful answer about either, so 'length' is refused, ## as in MATLAB. function out = length (this, varargin) cls = class (this); if (any (cls(1) == 'aeiou')) article = 'an'; else article = 'a'; endif error (strcat ("%s.length: 'length' is not defined for %s %s; use", ... " 'height', 'width' or 'size'."), cls, article, cls); endfunction function out = repelems (this, varargin) error ("%s.repelems: 'repelems' is not supported.", class (this)); endfunction function out = reshape (this, varargin) error ("%s.reshape: 'reshape' is not supported.", class (this)); endfunction function out = resize (this, varargin) error ("%s.resize: 'resize' is not supported.", class (this)); endfunction function out = shiftdims (this, varargin) error ("%s.shiftdims: 'shiftdims' is not supported.", class (this)); endfunction function out = vec (this, varargin) error ("%s.vec: 'vec' is not supported.", class (this)); endfunction ## Build the house-format CSV grid for THIS object: the descriptive ## comment row, the header block carrying the variable types, names, ## descriptions and units, and the data. Shared by 'table.table2csv' and ## 'timetable.timetable2csv', which differ only in what their row-label ## column holds. function [csv, errmsg] = __csv_parts__ (this, writeVarNames = true, ... writeRowLabels = true) ## A datetime or duration is written in its ISO 8601 form rather than as ## its display string: a display format may round or omit components, and ## a day-first one is indistinguishable from a month-first one on read. [V, N, T, D, U, errmsg] = table2cellarrays (this, 'iso'); if (! isempty (errmsg)) csv = {}; return; endif ## The row labels lead the block when the object has them; dropping the ## column here keeps them out of the file entirely. if (! writeRowLabels && hasRowLabels (this)) V(:,1) = []; N(:,1) = []; T(:,1) = []; D(:,1) = []; U(:,1) = []; endif ## Get columns for final cell array Ccols = size (V, 2); ## Get rows for variable types, names, descriptions, and units Trows = cellfun (@(x) size (x, 1), T); Tmaxr = max (Trows); Nrows = cellfun (@(x) size (x, 1), N); isvar = cellfun (@(x) ! isempty (x), N(1,:)); ## Suppressing the names leaves the block without them; the count in the ## comment says so, and the reader then numbers the variables. if (writeVarNames) Nmaxr = max (Nrows); else Nmaxr = 0; Nrows = zeros (size (Nrows)); endif ## Descriptions and units are written when the property is set, or when a ## nested variable carries one; nested variables expand them to as many ## rows as varNames/varTypes. A nested variable's entry is a column of ## one entry per nesting level, so the test has to look through it at the ## text rather than at the column itself, which is never empty. ## The block is written when any level of any variable has the property ## set, a set-but-blank entry being a character vector where an unset one ## is an empty. hasText = @(x) any (cellfun (@ischar, tabular.header_entry (x))); Drows = cellfun (@(x) max (1, size (x, 1)), D); if (! isempty (this.VariableDescriptions) || any (cellfun (hasText, ... D(isvar)))) Dmaxr = max (Drows(isvar)); else Dmaxr = 0; endif Urows = cellfun (@(x) max (1, size (x, 1)), U); if (! isempty (this.VariableUnits) || any (cellfun (hasText, U(isvar)))) Umaxr = max (Urows(isvar)); else Umaxr = 0; endif ## Initialize header Header = repmat ({''}, Nmaxr + Tmaxr + Dmaxr + Umaxr, Ccols); ## Populate header for c = 1:Ccols if (isvar(c)) # variable if (Trows(c) == 1) Header{1,c} = T{c}; else for tr = 1:Trows(c) Header{tr,c} = T{c}{tr}; endfor endif if (Nmaxr) if (Nrows(c) == 1) Header{1 + Tmaxr,c} = N{c}; else for nr = 1:Nrows(c) Header{nr + Tmaxr,c} = N{c}{nr}; endfor endif endif if (Dmaxr) if (Drows(c) == 1) Header{1 + Tmaxr + Nmaxr,c} = D{c}; else for dr = 1:Drows(c) Header{dr + Tmaxr + Nmaxr,c} = D{c}{dr}; endfor endif endif if (Umaxr) if (Urows(c) == 1) Header{1 + Tmaxr + Nmaxr + Dmaxr,c} = U{c}; else for ur = 1:Urows(c) Header{ur + Tmaxr + Nmaxr + Dmaxr,c} = U{c}{ur}; endfor endif endif else # the row-label column ## The tag says what kind of labels the column holds; a dimension ## name, which only row times have, travels in the name row beside ## it exactly as a variable's name does. Header{1,c} = rowLabelTag (this); if (Nmaxr && ! strcmp (Header{1,c}, rowLabelName (this))) Header{1 + Tmaxr,c} = rowLabelName (this); endif endif endfor ## Generate descriptive comment for header contents cmt = cell (1, Ccols); txt = strcat ("# varTypes %d rows; varNames %d rows;", ... " varDescriptions %d rows; varUnits %d rows."); cmt{1} = sprintf (txt, Tmaxr, Nmaxr, Dmaxr, Umaxr); ## Merge cell arrays into a single cell array for saving to csv file csv = [cmt; Header; V]; endfunction ## Shared helper for the house-format ODS exporters ('table2ods' and the ## standalone 'struct2ods'). Hidden rather than private so 'struct2ods' ## can reuse the exact flattening + metadata assembly. ## ## Build the house-format ODS parts for THIS table: the data grid V (with ## ISO-formatted datetime/duration values), the per-column ODS value types, ## and the metadata block (a descriptive comment row followed by the ## variable types, names, descriptions, and units, mirroring the header ## block that 'table2csv' writes so 'ods2table' can reuse its parser). ## CALLER names the function for error reporting. function [V, vtype, meta, hdr] = __ods_parts__ (this, caller, ... writeVarNames = true, ... writeRowNames = true) [V, N, T, D, U, errmsg] = table2cellarrays (this, 'iso'); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif ## The row labels lead the block when the object has them; dropping the ## column here keeps them out of the file entirely. if (! writeRowNames && hasRowLabels (this)) V(:,1) = []; N(:,1) = []; T(:,1) = []; D(:,1) = []; U(:,1) = []; endif Ccols = size (V, 2); ## A nested table or a struct carries a column of type entries, one per ## nesting level; the cells themselves hold the innermost variable's ## values, so the value type comes from the innermost entry. vtype = cell (1, Ccols); for c = 1:Ccols if (iscell (T{c})) vtype{c} = tabular.ods_value_type (T{c}{end}); else vtype{c} = tabular.ods_value_type (T{c}); endif endfor txt = strcat ("# varTypes %d rows; varNames %d rows;", ... " varDescriptions %d rows; varUnits %d rows."); ## A table with no variables carries only the descriptive comment if (Ccols == 0) meta = {sprintf(txt, 0, 0, 0, 0)}; hdr = {}; return; endif Trows = cellfun (@(x) size (x, 1), T); Tmaxr = max (Trows); Nrows = cellfun (@(x) size (x, 1), N); isvar = cellfun (@(x) ! isempty (x), N(1,:)); ## Suppressing the names leaves the block without them; the count in the ## comment says so, and the reader then numbers the variables. if (writeVarNames) Nmaxr = max (Nrows); else Nmaxr = 0; Nrows = zeros (size (Nrows)); endif ## The block is written when any level of any variable has the property ## set, a set-but-blank entry being a character vector where an unset one ## is an empty. hasText = @(x) any (cellfun (@ischar, tabular.header_entry (x))); Drows = cellfun (@(x) max (1, size (x, 1)), D); if (! isempty (this.VariableDescriptions) || any (cellfun (hasText, ... D(isvar)))) Dmaxr = max (Drows(isvar)); else Dmaxr = 0; endif Urows = cellfun (@(x) max (1, size (x, 1)), U); if (! isempty (this.VariableUnits) || any (cellfun (hasText, U(isvar)))) Umaxr = max (Urows(isvar)); else Umaxr = 0; endif ## The variable names go on the data sheet, where a reader of the file ## can see them, and everything else on the hidden one. A nested ## variable's entry is a column of one entry per nesting level and is ## written down its block; a plain variable fills its first row. Header = repmat ({''}, Tmaxr + Dmaxr + Umaxr, Ccols); hdr = repmat ({''}, Nmaxr, Ccols); for c = 1:Ccols if (isvar(c)) Header(1:Trows(c),c) = tabular.header_entry (T{c}); if (Nmaxr) hdr(1:Nrows(c),c) = tabular.header_entry (N{c}); endif if (Dmaxr) Header(1 + Tmaxr:Drows(c) + Tmaxr,c) = tabular.header_entry (D{c}); endif if (Umaxr) base = Tmaxr + Dmaxr; Header(1 + base:Urows(c) + base,c) = tabular.header_entry (U{c}); endif else ## The type row says what kind of labels the column holds, and the ## data sheet's own header names them, where a reader opening the ## file in a spreadsheet application sees the name. Header{1,c} = rowLabelTag (this); if (Nmaxr) hdr{1,c} = rowLabelName (this); endif endif endfor cmt = repmat ({''}, 1, Ccols); cmt{1} = sprintf (txt, Tmaxr, Nmaxr, Dmaxr, Umaxr); meta = [cmt; Header]; endfunction ## Write THIS object to a file in the MATLAB-compatible interop form: no ## hidden metadata, one header row of names, one row per row. O carries ## the options the public method has already resolved and validated, so ## that 'table.writetable' and 'timetable.writetimetable' share the ## writing and differ only in what they accept and what they call ## themselves. function __interop_write__ (this, caller, file, o) ## Flatten the table; nested tables and structs (multi-row type entries) ## are refused, as MATLAB does. [V, N, T, ~, ~, errmsg] = table2cellarrays (this, o.fmt); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif if (any (cellfun (@iscell, T))) error (strcat ("%s: %s does not support writing nested tables.", ... " Use splitvars to split multicolumn variables", ... " into single-column variables before writing."), ... caller, o.fname); endif ## Deviation D7: a zoned datetime row-time column goes out in RFC 9557 ## form on the text route. MATLAB writes the display string, which ## carries the zone only when the format asks for it and which its own ## reader cannot recover in any form, so nothing is lost by writing the ## standard form instead and the zone and the fold are both kept. if (o.writeRowLabels && hasRowLabels (this) && strcmp (o.fmt, 'display')) lbl = getRowLabels (this); if (isa (lbl, 'datetime') && ! isempty (lbl.TimeZone)) V(:,1) = __dt2rfc9557__ (lbl); elseif (isa (lbl, 'duration')) ## A duration's own display format may be '1 hr', which no reader ## parses back, MATLAB's included. The clock form is what MATLAB ## writes by default and what both readers take. l2 = lbl(:); if (any (mod (seconds (l2), 1) != 0)) l2.Format = 'hh:mm:ss.SSS'; else l2.Format = 'hh:mm:ss'; endif V(:,1) = cellstr (l2); endif endif [names, V, T] = tabular.writetable_prep (V, N, T, o.writeRowLabels); ## 'writetable_prep' heads the row-label column 'Row' and types it ## 'cellstr', which is what a table's row names are. A timetable heads ## it with its row dimension name, and its row times are written as the ## native date or time cells of their own type. if (o.writeRowLabels && hasRowLabels (this)) names{1} = o.rowLabelHeader; lbl = getRowLabels (this); if (isa (lbl, 'datetime') || isa (lbl, 'duration')) T{1} = class (lbl); endif endif if (strcmp (o.fmt, 'display')) ## In append mode MATLAB writes the data rows only, never a header. if (o.writeVarNames && ! o.appendMode) grid = [names; V]; else grid = V; endif d = wt_resolve_delimiter (o.delim, caller); msg = __table2csv__ (file, grid, d, lower (o.quoteStrings), ... o.appendMode); if (msg) error ("%s: %s", caller, msg); endif else vtype = cell (1, numel (T)); for c = 1:numel (T) vtype{c} = tabular.ods_value_type (T{c}); endfor opts = struct (); ## Append mode writes data rows only, never a header. if (o.writeVarNames && ! strcmp (o.writeMode, 'append')) opts.header = names; else opts.header = {}; endif ## Writing into an existing workbook with no explicit 'Sheet' takes ## the first existing sheet, as MATLAB does, not a new one. if (isempty (o.sheet) && exist (file, 'file') ... && ! strcmp (o.writeMode, 'replacefile')) if (o.isXlsx) [~, ~, ~, exNames] = __xlsx2table__ (file); else [~, ~, ~, exNames] = __ods2table__ (file); endif if (iscell (exNames) && ! isempty (exNames)) o.sheet = exNames{1}; endif endif if (! isempty (o.sheet)) opts.sheetname = o.sheet; endif if (o.isXlsx) if (exist (file, 'file') && ! strcmp (o.writeMode, 'replacefile')) ## Merge into an existing workbook by reading it back, modifying the ## struct of tables, and rewriting (interop re-encode, like the ## incremental table2ods path). s = xlsx2struct (file); s = __mergesheet__ (s, this, o.sheet, o.writeMode); struct2xlsx (file, s); msg = 0; else ## A fresh single-sheet write. if (! isempty (o.range)) [r1, c1] = __a1ref__ (o.range); opts.roff = r1 - 1; opts.coff = c1 - 1; endif opts.macro = strcmpi (o.ext, '.xlsm'); msg = __table2xlsx__ (file, V, vtype, opts); endif else is_flat = strcmpi (o.ext, '.fods'); ## Merge into an existing workbook, keeping its other sheets, ## unless the file is new or 'replacefile' overwrites it. if (exist (file, 'file') && ! strcmp (o.writeMode, 'replacefile')) opts.merge = true; opts.writemode = o.writeMode; elseif (! isempty (o.range)) [r1, c1] = __a1ref__ (o.range); opts.roff = r1 - 1; opts.coff = c1 - 1; endif msg = __table2ods__ (file, V, vtype, {}, is_flat, opts); endif if (! isequal (msg, 0)) error ("%s: %s", caller, msg); endif endif endfunction ## Build the MATLAB-interop spreadsheet parts for THIS table: the variable ## names (a header row), the flat data grid V with ISO-formatted ## datetime/duration values, and the per-column ODS value types. No hidden ## metadata (interop format). Shared by 'writetable' and 'struct2xlsx'. ## CALLER names the function for error reporting. function [names, V, vtype] = __interop_parts__ (this, caller) [V, N, T, ~, ~, errmsg] = table2cellarrays (this, 'iso'); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif if (any (cellfun (@iscell, T))) error (strcat ("%s: nested tables and structs are not supported;", ... " flatten multicolumn variables with splitvars", ... " before writing."), caller); endif [names, V, T] = tabular.writetable_prep (V, N, T, false); vtype = cell (1, numel (T)); for c = 1:numel (T) vtype{c} = tabular.ods_value_type (T{c}); endfor endfunction endmethods ################################################################################ ## ** Reference and assignment internals ** ## ################################################################################ ## ## ## Private in 'table' before the split. They are protected here because ## ## private access does not reach a subclass, and every one of them reaches ## ## the row labels through the hooks rather than directly. ## ## ## ################################################################################ methods (Access = protected) ## The body of the complaint against X as the value of a variable, or '' ## when a variable can hold it. A cell variable can hold anything. function errmsg = variableRefusal (this, x) errmsg = ''; if (is_function_handle (x)) errmsg = strcat ("a variable cannot be a function handle; only a", ... " cell array can hold one."); elseif (isa (x, 'dictionary') || isa (x, 'timeseries') || isa (x, 'tscollection')) errmsg = sprintf (strcat ("a variable cannot be a '%s'; only a", ... " cell array can hold one."), class (x)); endif endfunction ## The names of the custom properties of one scope, in the order they were ## added. TYPE is 'table' or 'variable'. A property's type is held under ## the property's own name, so nothing here depends on two containers ## agreeing on an order. -*- texinfo -*- @deftypefn {tabular} {[@var{index}, ## @var{errmsg}] =} sortrowsIndex (@var{obj}, @var{args}) ## ## Work out the row order @code{sortrows} asks for. ## ## @var{args} is the cell of arguments the public method was given, less the ## object itself. The permutation comes back in @var{index}, and ## @var{errmsg} carries the body of any complaint so that the calling class ## can raise it under its own name. The caller applies the order with ## @code{subsetrows}, which is where a subclass maintains whatever its row ## labels oblige it to. ## ## @end deftypefn function [index, errmsg] = sortrowsIndex (this, args_in) index = []; errmsg = ''; varargin = args_in; ## Bound before use: a call inside [] or {} is split by the space ## before its paren and would run with no arguments. labelNames = rowLabelKeyNames (this); keyNames = [this.VariableNames, labelNames]; ## Add defaults. A class whose rows carry labels may sort by them ## when nothing else is asked for; a table sorts by every variable. if (sortsByLabelsByDefault (this)) varRef = []; inLabels = 1; else varRef = ':'; inLabels = 0; endif direction = {'ascend'}; dir_given = false; ## Parse optional paired arguments optNames = {'MissingPlacement', 'ComparisonMethod'}; dfValues = {'auto', 'auto'}; [MP, CM, args] = parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! ismember (MP, {'auto', 'first', 'last'})) errmsg = strcat ("'MissingPlacement' parameter can", ... " be either 'auto', 'first', or 'last'."); return endif if (! ismember (CM, {'auto', 'real', 'abs'})) errmsg = strcat ("'ComparisonMethod' parameter can", ... " be either 'auto', 'real', or 'abs'."); return endif ## Only VARS and DIRECTION may remain; anything after them is an ## option this method does not know. nargs = numel (args); if (nargs > 2) errmsg = "invalid optional paired argument."; return endif if (nargs > 1) ## Matched without regard to case, as MATLAB does. direction = lower (cellstr (args{2})); dir_given = true; if (! all (ismember (direction, {'ascend', 'descend'}))) errmsg = "invalid value for DIRECTION argument."; return endif endif if (nargs > 0) ## Given keys of its own, the object no longer falls back on its row ## labels; they take part only if named among those keys. inLabels = 0; ## RowNames and rowDimName take precedence over variable names arg1 = args{1}; if (ischar (arg1) && isvector (arg1) && ismember (arg1, labelNames)) ## Check user's direction is scalar if (dir_given && numel (direction) != 1) errmsg = strcat ("DIRECTION must be a scalar", ... " input when 'RowNames' or 'rowDimNames' are", ... " selected."); return endif ## Handle special case here if (! hasRowLabels (this)) index = [1:height(this)]'; return else index = labelOrder (getRowLabels (this), direction{1}, MP); return endif endif ## At this point, VARS must be variable name(s) if (islogical (arg1)) varRef = arg1; if (! (isvector (varRef) && numel (varRef) == width (this))) errmsg = strcat ("logical indexing vector does", ... " not match table width."); return endif ## Check user's direction matches selected variables if (! isscalar (direction)) if (dir_given && sum (varRef) != numel (direction)) errmsg = "invalid size for DIRECTION argument."; return endif endif elseif (isnumeric (arg1)) if (isempty (arg1)) arg1 = [1:width(this)]; endif if (! isvector (arg1) || any (fix (arg1) != arg1) || any (arg1 == 0)) errmsg = strcat ("numerical indexing must be a", ... " vector of nonzero integers."); return endif if (any (abs (arg1) > width (this))) errmsg = "numerical index exceeds table dimensions."; return endif varRef = arg1; ## If direction was given, ignore sign of numerical indexing if (dir_given) varRef = abs (varRef); ## Check user's direction matches selected variables if (! isscalar (direction)) if (! isequal (size (varRef), size (direction))) errmsg = "invalid size for DIRECTION argument."; return endif endif else direction = cell (1, numel (varRef)); direction(sign (varRef) > 0) = 'ascend'; direction(sign (varRef) < 0) = 'descend'; varRef = abs (varRef); endif elseif (ischar (arg1) || iscellstr (arg1) || isa (arg1, 'string')) varRef = cellstr (arg1); if (isscalar (varRef) && strcmp (varRef, ':')) varRef = ':'; elseif (! all (ismember (varRef, keyNames))) errmsg = "VARS indexes non-existing variable names."; return endif ## Check user's direction matches selected variables if (! isscalar (direction)) if (strcmp (varRef, ':') && numel (direction) != width (this)) errmsg = "invalid size for DIRECTION argument."; return elseif (! isequal (size (varRef), size (direction))) errmsg = "invalid size for DIRECTION argument."; return endif endif ## Check whether the row labels are among the indexed variables isLabel = ismember (varRef, labelNames); if (any (isLabel)) inLabels = find (isLabel); varRef(isLabel) = []; endif elseif (isa (arg1, 'vartype')) varRef = arg1; ## Check user's direction is scalar if (dir_given && numel (direction) != 1) errmsg = strcat ("DIRECTION must be a scalar", ... " input when variables are indexed with a", ... " 'vartype' object."); return endif else errmsg = strcat ("VARS must be a numeric or logical index, a", ... " variable name, a cell array or string array", ... " of variable names, or a vartype object."); return endif endif ## Resolve varRef to variables' indices ixVars = resolveVarRef (this, varRef); ## With nothing to sort by the order cannot change, and the sort ## itself has no key to build. if (isempty (ixVars) && inLabels == 0) index = (1:height (this))'; return; endif ## Build a cell array for the selected variables to be used in sorting if (inLabels == 0) varVal = cell (1, numel (ixVars)); else varVal = cell (1, numel (ixVars) + 1); endif ## Expand direction if it is a scalar if (isscalar (direction)) direction = repmat (direction, 1, numel (varVal)); endif ## Populate cell array for sorting offset = 0; for ix = 1:numel (varVal) if (inLabels == ix) labels = getRowLabels (this); varVal(ix) = {labels}; offset = 1; else varVal(ix) = this.VariableValues(ixVars(ix - offset)); endif endfor ## Prepare a proxy array by converting every key to numeric proxies. ## The direction vector is widened by the proxy's own column count, so ## a key that yields no columns contributes no direction either. varValIdx = []; varValDir = []; for ix = 1:numel (varVal) if (strcmpi (direction{ix}, 'ascend')) tmpDir = 1; else tmpDir = -1; endif ## Calendar durations have no order: a month is no fixed number of ## days, so they are refused, as in MATLAB. if (isa (varVal{ix}, 'calendarDuration')) errmsg = sortBadType ('calendarDuration'); return endif [p, badtype] = valueProxy (varVal{ix}, CM); if (! isempty (badtype)) errmsg = sortBadType (badtype); return endif dirCols = repmat (tmpDir, 1, size (p, 2)); varValIdx = [varValIdx, p]; varValDir = [varValDir, dirCols]; endfor ## Fix direction vector varValDir = [1:numel(varValDir)] .* varValDir; ## Do the actual sorting here [~, index] = sortrows (varValIdx, varValDir); index = index(:); ## Fix missing placement. The first key decides it, and that key may ## be the row labels, so the mask is read off the key itself rather ## than off the object: a subclass need not implement 'ismissing'. mask = keyMissingMask (varVal{1}); TFvec = mask(index); if (any (TFvec) && ! all (TFvec)) is_nan = index(TFvec); no_nan = index(! TFvec); if (any (find (TFvec) == 1) && strcmpi (MP, 'last')) index = [no_nan; is_nan]; elseif (any (find (TFvec) == numel (index)) && strcmpi (MP, 'first')) index = [is_nan; no_nan]; endif endif endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{ia}, @var{ic}, @var{errmsg}] ## =} uniqueIndex (@var{obj}, @var{args}) ## ## Work out which rows @code{unique} keeps. ## ## @var{args} is the cell of arguments the public method was given, less the ## object itself. @var{ia} indexes the rows to keep and @var{ic} maps every ## original row onto one of them, as @code{unique} returns them. ## @var{errmsg} carries the body of any complaint so that the calling class ## can raise it under its own name. ## ## @end deftypefn function [ia, ic, errmsg] = uniqueIndex (this, args_in) ia = []; ic = []; errmsg = ''; ## Check max number of input arguments if (numel (args_in) > 1) errmsg = "too many input arguments."; return endif ## Handle 'setOrder' and 'occurrence' options opt = 'sorted'; if (! isempty (args_in)) opts = {'sorted', 'stable', 'first', 'last', 'rows'}; if (any (strcmp (args_in{1}, opts))) opt = args_in{1}; else errmsg = sprintf ("invalid option '%s'.", args_in{1}); return endif endif ## Rows are the only thing a tabular object compares, so naming them ## changes nothing. if (strcmp (opt, 'rows')) opt = 'sorted'; endif ## What makes a row distinct. A timetable is told apart by its row ## times as well as by its variables; a table by its variables alone. keyVals = {}; if (uniqueIncludesLabels (this) && hasRowLabels (this)) labels = getRowLabels (this); keyVals(end+1) = {labels}; endif for ix = 1:width (this) keyVals(end+1) = this.VariableValues(ix); endfor ## Every row of an object with nothing to compare is the same empty ## row, so they reduce to one; there is no proxy to build from. if (isempty (keyVals) && height (this) > 0) ia = 1; ic = ones (height (this), 1); return endif ## Prepare a proxy array by converting every key to numeric proxies proxy = []; for ix = 1:numel (keyVals) [p, badtype] = valueProxy (keyVals{ix}, 'auto'); if (! isempty (badtype)) errmsg = uniqueBadType (badtype); return endif proxy = [proxy, p]; endfor ## Find unique rows in the proxy [~, ia, ic] = __unique__ (proxy, opt, 'rows'); endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{ixRows}, @var{errmsg}] =} ## headTailRows (@var{obj}, @var{k}, @var{fromEnd}) ## ## The rows @code{head} or @code{tail} returns. ## ## @var{k} is the count asked for, empty for the default of eight, and ## @var{fromEnd} says which end to count from. Asking for more rows than ## there are yields all of them, and asking for none yields none. ## @var{errmsg} carries the body of any complaint so that the calling class ## can raise it under its own name. ## ## @end deftypefn function [ixRows, errmsg] = headTailRows (this, k, fromEnd) ixRows = []; errmsg = ''; if (isempty (k)) k = 8; endif if (! (isnumeric (k) && isscalar (k) && isreal (k) && isfinite (k)) || fix (k) != k || k < 0) errmsg = strcat ("K must be a real, nonnegative, integer scalar", ... " value."); return endif n = height (this); k = min (k, n); if (fromEnd) ixRows = ((n - k + 1):n)'; else ixRows = (1:k)'; endif endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{ixRows}, @var{errmsg}] =} ## topkrowsIndex (@var{obj}, @var{k}, @var{args}) ## ## The rows @code{topkrows} keeps, in the order it reports them. ## ## @code{topkrows} is @code{sortrows} with a descending default and a count, ## so the ordering is worked out by @code{sortrowsIndex} and only the ## defaults differ: sorting runs downwards unless a direction is given, and ## missing keys go last rather than wherever the direction would put them. ## @var{errmsg} carries the body of any complaint so that the calling class ## can raise it under its own name. ## ## @end deftypefn function [ixRows, errmsg] = topkrowsIndex (this, k, args_in) ixRows = []; errmsg = ''; ## Check for valid k if (! (isnumeric (k) && isscalar (k) && isreal (k) && isfinite (k) && fix (k) == k && k >= 0)) errmsg = "K must be a nonnegative integer scalar."; return endif ## Split off any trailing Name-Value pairs, so that the positional ## arguments can be adjusted without disturbing them. optNames = {'MissingPlacement', 'ComparisonMethod'}; nvStart = numel (args_in) + 1; for ii = 1:numel (args_in) if (ischar (args_in{ii}) && isrow (args_in{ii}) && any (strcmp (args_in{ii}, optNames))) nvStart = ii; break; endif endfor pos = args_in(1:nvStart-1); nv = args_in(nvStart:end); ## With no explicit DIRECTION, that is with fewer than two positional ## arguments, enforce the descending default. if (numel (pos) < 2) if (numel (pos) == 0) ## No VARS. A class whose rows carry labels ranks by them, as it ## does when sorting; every other ranks by all of its variables. if (sortsByLabelsByDefault (this)) labelNames = rowLabelKeyNames (this); pos = {labelNames{1}, 'descend'}; else pos = {':', 'descend'}; endif elseif (isnumeric (pos{1}) && ! isempty (pos{1})) ## Signed numeric index: flip the sign convention relative to ## 'sortrows' so that a positive index ranks downwards and a ## negative index upwards. pos = {-pos{1}}; else ## Named, logical, vartype, ':' or [] selection. pos = [pos, {'descend'}]; endif endif ## Missing keys rank last however the sort runs, which is where ## 'topkrows' parts company with 'sortrows' and its 'auto'. if (! any (strcmp (nv, 'MissingPlacement'))) nv = [nv, {'MissingPlacement', 'last'}]; endif [index, errmsg] = sortrowsIndex (this, [pos, nv]); if (! isempty (errmsg)) return endif if (k < numel (index)) index = index(1:k); endif ixRows = index; endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{TF}, @var{errmsg}] =} ## issortedrowsCheck (@var{obj}, @var{args}) ## ## Whether the rows are already in the order asked for. ## ## @var{args} is the cell of arguments the public method was given, less the ## object itself, and takes the same forms @code{sortrows} does. The sort ## is stable, so the rows are in that order exactly when sorting them would ## leave every one where it is. @var{errmsg} carries the body of any ## complaint so that the calling class can raise it under its own name. ## ## @end deftypefn function [TF, errmsg] = issortedrowsCheck (this, args_in) TF = false; [index, errmsg] = sortrowsIndex (this, args_in); if (! isempty (errmsg)) return endif TF = isequal (index(:)', 1:height (this)); endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{tbl}, @var{errmsg}] =} ## addvarsResult (@dots{}) ## ## The object with variables added. ## ## Variables belong to a tabular object whatever labels its rows, so the ## work is the same for either class. @var{errmsg} carries the body of any ## complaint so that the calling class can raise it under its own name. ## @var{argNames} carries the caller's own names for the new variables, ## which only the public method can read. ## ## @end deftypefn function [tbl, errmsg] = addvarsResult (this, argNames, varargin) tbl = this; errmsg = ''; ## Add defaults tbl_width = width (this); ix_insert = tbl_width; AB_insert = true; # after by default ## The new variables end at the first character vector, as in MATLAB; ## a string scalar is a variable isName = cellfun (@(x) ischar (x) && isrow (x), varargin); nvars = find ([isName, true], 1) - 1; args = varargin(1:nvars); pairs = varargin(nvars+1:end); if (mod (numel (pairs), 2) != 0) errmsg = "name-value arguments must be in pairs."; return endif ## Parse optional paired arguments; an empty 'After' and 'Before' ## append the new variables after the last one optNames = {'After', 'Before', 'NewVariableNames'}; dfValues = {[], [], []}; [After, Before, newVarNames, pairs] = ... parsePairedArguments (optNames, dfValues, pairs(:)); ## Validate optional paired arguments if (! isempty (After) && ! isempty (Before)) errmsg = "cannot use both 'After' and 'Before' options."; return endif ## All other errors will be handled by 'resolveVarRef' for invalid input msg_error1 = "LOCATION must index a single variable."; msg_error2 = strcat ("LOCATION must be either a", ... " scalar integer, a character vector, or a", ... " logical vector indexing a single table variable."); if (! isempty (After)) if ((isnumeric (After) && isscalar (After)) || ischar (After) || ... (isa (After, 'string') && isscalar (After))) ix_insert = resolveVarRef (this, After); elseif (isvector (After) && islogical (After)) ix_insert = resolveVarRef (this, After); if (numel (ix_insert) > 1) errmsg = msg_error1; return endif else errmsg = msg_error2; return endif elseif (! isempty (Before)) if ((isnumeric (Before) && isscalar (Before)) || ischar (Before) || ... (isa (Before, 'string') && isscalar (Before))) ix_insert = resolveVarRef (this, Before); AB_insert = false; elseif (isvector (Before) && islogical (Before)) ix_insert = resolveVarRef (this, Before); AB_insert = false; if (numel (ix_insert) > 1) errmsg = msg_error1; return endif else errmsg = msg_error2; return endif endif if (! isempty (pairs)) errmsg = "invalid optional paired argument."; return endif ## Every new variable spans the rows of the table, as in MATLAB, where ## 'setvar' would repeat a scalar down them. A table with neither rows ## nor variables takes the height of the first. if (! isempty (args)) nrows = height (this); if (nrows == 0 && width (this) == 0) nrows = size (args{1}, 1); endif for i = 1:numel (args) if (size (args{i}, 1) != nrows) errmsg = sprintf (strcat ("each new variable must have as", ... " many rows as the %s."), class (this)); return endif endfor endif if (isempty (newVarNames)) ## Create names for new variables ## The place the first new variable takes in the result, which is ## what a generated name counts. offset = ix_insert; if (! AB_insert) offset = ix_insert - 1; endif newVarNames = cell (size (args)); for i = 1:numel (args) newVarNames{i} = argNames{i}; ## A name taken from the caller's workspace is deconflicted rather ## than refused: the dimension names share the variables' namespace. if (any (strcmp (newVarNames{i}, this.DimensionNames))) suffix = 1; while (any (strcmp (sprintf ("%s_%d", argNames{i}, suffix), ... [this.VariableNames, this.DimensionNames]))) suffix++; endwhile newVarNames{i} = sprintf ("%s_%d", argNames{i}, suffix); endif if (isempty (newVarNames{i})) ## A generated name counts the place the variable takes in the ## result and not how many variables there were, so one put ## before the first is 'Var1' whatever it is put before. A name ## already in use is deconflicted with a suffix, as a name taken ## from the caller's workspace is. newVarNames{i} = sprintf ("Var%d", offset + i); if (ismember (newVarNames{i}, this.VariableNames)) base = newVarNames{i}; suffix = 1; while (ismember (sprintf ("%s_%d", base, suffix), ... [this.VariableNames, this.DimensionNames])) suffix++; endwhile newVarNames{i} = sprintf ("%s_%d", base, suffix); endif endif endfor else ## Force to cellstr (in case of string array) newVarNames = cellstr (newVarNames); clash = newVarNames(ismember (newVarNames, this.DimensionNames)); if (! isempty (clash)) errmsg = sprintf ("duplicate dimension and variable name: '%s'.", ... clash{1}); return endif if (numel (args) != numel (newVarNames)) errmsg = strcat ("NEWNAMES does not match the", ... " number of new variables."); return endif if (numel (__unique__ (newVarNames)) != numel (newVarNames)) errmsg = "NEWNAMES contains duplicate names."; return endif idx = ismember (newVarNames, this.VariableNames); if (any (idx)) if (sum (idx) == 1) errmsg = sprintf ("new variable name '%s' already exists.", ... newVarNames{idx}); return else msg_error3 = sprintf ("'%s', ", newVarNames{idx}); msg_error3(end-1:end) = []; errmsg = sprintf ("new variable names %s already exist.", ... msg_error3); return endif endif endif ## Append the new variables for i = 1:numel (args) errmsg = variableRefusal (this, args{i}); if (! isempty (errmsg)) return endif endfor tbl = this; for i = 1:numel (args) tbl = setvar (tbl, newVarNames{i}, args{i}); endfor ## Relocate new variables (if necessary) if (AB_insert) # after if (ix_insert < tbl_width) ix_L = [1:ix_insert]; ix_M = [tbl_width+1:tbl_width+numel(args)]; ix_R = [ix_insert+1:tbl_width]; ixVars = [ix_L, ix_M, ix_R]; tbl = subsetvars (tbl, ixVars); endif else # before if (ix_insert > 1) ix_L = [1:ix_insert-1]; ix_M = [tbl_width+1:tbl_width+numel(args)]; ix_R = [ix_insert:tbl_width]; ixVars = [ix_L, ix_M, ix_R]; else ixVars = [tbl_width+1:tbl_width+numel(args), 1:tbl_width]; endif tbl = subsetvars (tbl, ixVars); endif endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{tbl}, @var{errmsg}] =} ## movevarsResult (@dots{}) ## ## The object with a variable moved. ## ## Variables belong to a tabular object whatever labels its rows, so the ## work is the same for either class. @var{errmsg} carries the body of any ## complaint so that the calling class can raise it under its own name. ## ## @end deftypefn function [tbl, errmsg] = movevarsResult (this, vars, varargin) tbl = this; errmsg = ''; ## Check input argument if (nargin < 2 || isempty (vars)) errmsg = "too few input arguments."; return endif if (mod (numel (varargin), 2) != 0) errmsg = "name-value arguments must be in pairs."; return endif ## Add defaults tbl_width = width (this); ix_insert = tbl_width; AB_insert = true; # after by default ## Parse optional paired arguments; an empty 'After' and 'Before' ## move the variables after the last one optNames = {'After', 'Before'}; dfValues = {[], []}; [After, Before, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! isempty (After) && ! isempty (Before)) errmsg = "cannot use both 'After' and 'Before' options."; return endif ## All other errors will be handled by 'resolveVarRef' for invalid input msg_error1 = "LOCATION must index a single variable."; msg_error2 = strcat ("LOCATION must be either a", ... " scalar integer, a character vector, or a", ... " logical vector indexing a single table variable."); msg_error3 = strcat ("LOCATION does not index an", ... " existing variable."); if (! isempty (After) || ! isempty (Before)) if (! isempty (Before)) AB_insert = false; After = Before; endif if ((isnumeric (After) && isscalar (After)) || ischar (After) || ... (isa (After, 'string') && isscalar (After))) ix_insert = resolveVarRef (this, After, 'lenient'); elseif (isvector (After) && islogical (After)) ix_insert = resolveVarRef (this, After, 'lenient'); if (numel (ix_insert) > 1) errmsg = msg_error1; return endif else errmsg = msg_error2; return endif ## Grab silent errors returned by 'resolveVarRef' if (any (ix_insert == 0)) errmsg = msg_error3; return endif endif if (! isempty (args)) errmsg = "invalid optional paired argument."; return endif ## Get variables to be moved mvVar = resolveVarRef (this, vars, 'lenient'); if (any (mvVar == 0)) vars = cellstr (vars); errmsg = sprintf ("cannot index non-existing variable: '%s'", ... vars{find (mvVar == 0)}); return endif ## Get variables that remain static stVar = 1:tbl_width; stVar(mvVar) = []; ## Construct remapping vector if (AB_insert) # after if (ix_insert < tbl_width) ## Check LOCATION variable is a static one if (ismember (ix_insert, mvVar)) errmsg = "LOCATION variable cannot be moved."; return endif ix_L = stVar(stVar <= ix_insert); ix_R = stVar(stVar > ix_insert); ixVars = [ix_L, mvVar, ix_R]; else ixVars = [stVar, mvVar]; endif else # before if (ix_insert > 1) ## Check LOCATION variable is a static one if (ismember (ix_insert, mvVar)) errmsg = "LOCATION variable cannot be moved."; return endif ix_L = stVar(stVar < ix_insert); ix_R = stVar(stVar >= ix_insert); ixVars = [ix_L, mvVar, ix_R]; else ixVars = [mvVar, stVar]; endif endif ## Return remapped table tbl = subsetvars (this, ixVars); endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{tbl}, @var{errmsg}] =} ## renamevarsResult (@dots{}) ## ## The object with variables renamed. ## ## Variables belong to a tabular object whatever labels its rows, so the ## work is the same for either class. @var{errmsg} carries the body of any ## complaint so that the calling class can raise it under its own name. ## ## @end deftypefn function [tbl, errmsg] = renamevarsResult (this, vars, newNames) tbl = this; errmsg = ''; ## Check input arguments if (nargin < 3 || isempty (vars) || isempty (newNames)) errmsg = "too few input arguments."; return endif if (! iscellstr (newNames) && ! isa (newNames, 'string') && ! (ischar (newNames) && isvector (newNames))) errmsg = strcat ("NEWNAMES must be either a", ... " character vector, a cell array of character", ... " vectors, or a string array."); return endif ## Force to cellstring and get indices newNames = cellstr (newNames); ## The dimension names share the variables' namespace. clash = newNames(ismember (newNames, this.DimensionNames)); if (! isempty (clash)) errmsg = sprintf ("duplicate dimension and variable name: '%s'.", ... clash{1}); return endif if (numel (__unique__ (newNames)) != numel (newNames)) errmsg = "NEWNAMES contains duplicate names."; return endif ixVars = resolveVarRef (this, vars, 'lenient'); ## Check selected variables if (any (ixVars == 0)) errmsg = sprintf ("cannot index non-existing variable: '%s'",... vars{find (ixVars == 0)}); return elseif (numel (ixVars) != numel (newNames)) errmsg = strcat ("number of names in NEWNAMES do", ... " not match the selected variables specified by", ... " VARS."); return endif ## Rename the indexed variables tbl = this; oldNames = this.VariableNames(ixVars); tbl.VariableNames(ixVars) = newNames; ## Check for duplicate names if (numel (__unique__ (tbl.VariableNames)) != numel (tbl.VariableNames)) errmsg = strcat ("newly assigned variable name", ... " already exists."); return endif tbl = varsRenamed (tbl, oldNames, newNames); endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{tbl}, @var{errmsg}] =} ## removevarsResult (@dots{}) ## ## The object with variables removed. ## ## Variables belong to a tabular object whatever labels its rows, so the ## work is the same for either class. @var{errmsg} carries the body of any ## complaint so that the calling class can raise it under its own name. ## ## @end deftypefn function [tbl, errmsg] = removevarsResult (this, vars) tbl = this; errmsg = ''; ## Check input argument if (nargin < 2 || isempty (vars)) errmsg = "too few input arguments."; return endif ## Resolve variables to be removed ixVar = resolveVarRef (this, vars); ## Remove selected variables nrows = height (this); tbl = this; tbl.VariableTypes(ixVar) = []; tbl.VariableNames(ixVar) = []; tbl.VariableValues(ixVar) = []; if (! isempty (tbl.VariableDescriptions)) tbl.VariableDescriptions(ixVar) = []; endif if (! isempty (tbl.VariableUnits)) tbl.VariableUnits(ixVar) = []; endif if (! isempty (this.VariableContinuity)) tbl.VariableContinuity(ixVar) = []; if (isempty (tbl.VariableContinuity)) tbl.VariableContinuity = []; endif endif ## Check for custom variable properties and remove accordingly if (! isempty (this.CustomProperties)) cpNames = customPropsOfType (this, 'variable'); if (! isempty (cpNames)) ## Remove referenced variable values from custom variable properties for i = 1:numel (cpNames) tmp = this.CustomProperties.(cpNames{i}); if (! isempty (tmp)) tmp(ixVar) = []; tbl.CustomProperties.(cpNames{i}) = tmp; endif endfor endif endif tbl = setRowCount (tbl, nrows); tbl = varsChanged (tbl); endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{tbl}, @var{errmsg}] =} ## convertvarsResult (@dots{}) ## ## The object with variables converted. ## ## Variables belong to a tabular object whatever labels its rows, so the ## work is the same for either class. @var{errmsg} carries the body of any ## complaint so that the calling class can raise it under its own name. ## ## @end deftypefn function [tbl, errmsg] = convertvarsResult (this, vars, dataType) tbl = this; errmsg = ''; ## Check input arguments if (nargin < 3 || isempty (vars) || isempty (dataType)) errmsg = "too few input arguments."; return endif if (ischar (dataType)) if (! isvector (dataType)) errmsg = "DATATYPE must be a character vector."; return endif elseif (! isa (dataType, 'function_handle')) errmsg = sprintf (strcat ("DATATYPE must be either a", ... " character vector or a function handle; got a", ... " '%s'."), ... class (dataType)); return endif ## Get variables to convert (input validation is done by 'resolveVarRef') [ixVars, varNames] = resolveVarRef (this, vars); tbl = this; ## Apply conversion for i = 1:numel (ixVars) try newVarValue = feval (dataType, this.VariableValues{ixVars(i)}); catch errmsg = sprintf (strcat ("specified DATATYPE", ... " conversion cannot be applied on selected", ... " variable '%s'."), ... varNames{i}); return end_try_catch errmsg = variableRefusal (this, newVarValue); if (! isempty (errmsg)) return endif if (size (newVarValue, 1) != height (this)) errmsg = sprintf (strcat ("specified DATATYPE", ... " conversion on '%s' does not return the", ... " appropriate amount of rows."), ... varNames{i}); return endif ## Write output tbl.VariableTypes{ixVars(i)} = class (newVarValue); tbl.VariableValues{ixVars(i)} = newVarValue; endfor endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{tbl}, @var{errmsg}] =} ## mergevarsResult (@dots{}) ## ## The object with variables merged. ## ## Variables belong to a tabular object whatever labels its rows, so the ## work is the same for either class. @var{errmsg} carries the body of any ## complaint so that the calling class can raise it under its own name. ## ## @end deftypefn function [tbl, errmsg] = mergevarsResult (this, vars, varargin) tbl = this; errmsg = ''; ## Check input argument if (nargin < 2 || isempty (vars)) errmsg = "too few input arguments."; return endif if (mod (numel (varargin), 2) != 0) errmsg = "name-value arguments must be in pairs."; return endif ## Parse optional paired arguments; an empty 'NewVariableName' names ## the merged variable after the place it takes optNames = {'NewVariableName', 'MergeAsTable'}; dfValues = {[], false}; [newVarName, mergeAsTable, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! isscalar (mergeAsTable)) errmsg = "invalid input for 'MergeAsTable'."; return endif if (! (isbool (mergeAsTable) || ismember (mergeAsTable, [0, 1]))) errmsg = "invalid input for 'MergeAsTable'."; return endif if (! isempty (args)) errmsg = "invalid optional paired argument."; return endif ## Resolve variables to be removed [ixVars, varNames] = resolveVarRef (this, vars); if (isscalar (ixVars)) tbl = this; return; endif ## Get name and location for new variable [ixVars, ixSorted] = sort (ixVars); varNames = varNames(ixSorted); location = ixVars(1); if (isempty (newVarName)) newVarName = cellstr (sprintf ("Var%d", location)); else ## Check user input for 'NewVariableName' if (isa (newVarName, 'string') && isscalar (newVarName)) newVarName = cellstr (newVarName); elseif (ischar (newVarName) && isvector (newVarName)) newVarName = cellstr (newVarName); elseif (! (iscellstr (newVarName) && isscalar (newVarName))) errmsg = "invalid input for 'NewVariableName'."; return endif endif ## Gather remaining variables to be copied unaltered ixRem = 1:width (this); ixRem(ixVars) = []; tbl = subsetvars (this, ixRem); ## Check that new variable name does not conflict any existing variable if (ismember (newVarName, tbl.VariableNames)) errmsg = "assigned 'NewVariableName' already exists."; return endif ## Merge as a table (easy, custom properties are handled by 'subsetvars') if (mergeAsTable) newVarTable = subsetvars (this, ixVars); tbl = addvars (tbl, newVarTable, 'Before', location, ... 'NewVariableNames', newVarName); return; endif ## Merge into multicolumn variable. (keep the custom properties of the ## first variable that is to be merged) if (! mergeAsTable) ## Use the first to-be-merged variable for copying custom properties ixRem = 1:width (this); ixRem(ixVars(2:end)) = []; tbl = subsetvars (this, ixRem); ## Add a try...catch block instead of heuristics to check how ## selected variables can be merged try newVarValue = cat (2, this.VariableValues{ixVars}); catch errmsg = strcat ("selected variables cannot be", ... " merged into a multicolumn variable due to", ... " incompatible variable types."); return end_try_catch tbl.VariableTypes{location} = class (newVarValue); tbl.VariableValues{location} = newVarValue; tbl.VariableNames(location) = newVarName; ## The merged variable is a new one, so its continuity is unset even ## though the first merged variable's slot is being reused. ## Units and descriptions do not survive a merge: one variable ## carries one of each and there is no saying which it should be. if (! isempty (tbl.VariableUnits)) tbl.VariableUnits{location} = ''; endif if (! isempty (tbl.VariableDescriptions)) tbl.VariableDescriptions{location} = ''; endif if (! isempty (tbl.VariableContinuity)) tbl.VariableContinuity{location} = 'unset'; endif ## A custom property describing the variables says nothing about the ## merged one either, for the same reason. if (! isempty (tbl.CustomProperties)) cpNames = fieldnames (tbl.CustomProperties); for k = 1:numel (cpNames) if (! strcmp (tbl.CustomPropTypes.(cpNames{k}), 'variable')) continue endif cpVal = tbl.CustomProperties.(cpNames{k}); if (numel (cpVal) >= location) if (iscell (cpVal)) cpVal(location) = {[]}; else cpVal(location) = NaN; endif tbl.CustomProperties.(cpNames{k}) = cpVal; endif endfor endif endif tbl = varsChanged (tbl); endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{tbl}, @var{errmsg}] =} ## splitvarsResult (@dots{}) ## ## The object with a variable split. ## ## Variables belong to a tabular object whatever labels its rows, so the ## work is the same for either class. @var{errmsg} carries the body of any ## complaint so that the calling class can raise it under its own name. ## ## @end deftypefn function [tbl, errmsg] = splitvarsResult (this, varargin) tbl = this; errmsg = ''; ## Check max number of input arguments if (nargin > 4) errmsg = "too many input arguments."; return endif ## Parse optional Name-Value paired arguments optNames = {'NewVariableNames'}; dfValues = {[]}; [newNames, vars] = parsePairedArguments (optNames, dfValues, varargin(:)); ## Get vars to actually split if (isempty (vars)) vars_to_split = []; for ix = 1:width (this) if (size (this.VariableValues{ix}, 2) > 1) # multicolumn variable vars_to_split(end+1) = ix; elseif (istable (this.VariableValues{ix})) # nested table vars_to_split(end+1) = ix; endif endfor [ixVars, oldNames] = resolveVarRef (this, vars_to_split); else [ixVars, oldNames] = resolveVarRef (this, vars{1}); [ixVars, ixSorted] = sort (ixVars); oldNames = oldNames(ixSorted); ## Ignore referenced variables that cannot be split for ix = numel (ixVars):-1:1 if (size (this.VariableValues{ixVars(ix)}, 2) == 1) ixVars(ix) = []; oldNames(ix) = []; endif endfor endif ## Return input table if there's nothing to split if (isempty (ixVars)) tbl = this; return; endif ## Create a remapping vector along with the corresponding variable names ixCols = []; ix_remap = []; ix_names = {}; for ix = 1:width (this) if (ismember (ix, ixVars)) tmp = this.VariableValues{ix}; col = size (tmp, 2); ix_remap = [ix_remap, repmat(ix, 1, col)]; if (istable (tmp)) ix_names = [ix_names, tmp.VariableNames]; else fcn = @(x) sprintf ("%s_%d", this.VariableNames{ix}, x); newnames = arrayfun (fcn, 1:col, 'UniformOutput', false); ix_names = [ix_names, newnames]; endif ixCols(end+1) = col; else ix_remap(end+1) = ix; ix_names{end+1} = this.VariableNames{ix}; endif endfor ## If there are duplicate variable names, this means that there are ## nested tables with identical variable names. Switch to optional ## 'nestedTableName_varName' naming convention applied only on tables ## with duplicated variable names. if (numel (__unique__ (ix_names)) != numel (ix_names)) dup_N = arrayfun (@(k) sum (arrayfun (@(j) isequal (ix_names{k}, ... ix_names{j}), 1:numel (ix_names))), ... 1:numel (ix_names)); dup_names = ix_names (dup_N > 1); ixCols = 0; ix_remap = []; ix_names = {}; for ix = 1:width (this) if (ismember (ix, ixVars)) tmp = this.VariableValues{ix}; col = size (tmp, 2); ix_remap = [ix_remap, repmat(ix, 1, col)]; if (istable (tmp)) if (any (ismember (dup_names, tmp.VariableNames))) fcn = @(x) sprintf ("%s_%s", this.VariableNames{ix}, x); newnames = cellfun (fcn, tmp.VariableNames, ... 'UniformOutput', false); ix_names = [ix_names, newnames]; else ix_names = [ix_names, tmp.VariableNames]; endif else fcn = @(x) sprintf ("%s_%d", this.VariableNames{ix}, x); newnames = arrayfun (fcn, 1:col, 'UniformOutput', false); ix_names = [ix_names, newnames]; endif ixCols(end+1) = col; else ix_remap(end+1) = ix; ix_names{end+1} = this.VariableNames{ix}; endif endfor endif ## Create the new table by duplicating splitable variables ## and set new variable names tbl = subsetvars (this, ix_remap); tbl.VariableNames = ix_names; ## Split the multicolumn data into separate variables idx = 1; # variable index idc = 1; # new name index for ix = 1:width (this) if (ismember (ix, ixVars)) tmp = this.VariableValues{ix}; col = size (tmp, 2); ## Check for user defined new variable names if (! isempty (newNames)) if (! iscellstr (newNames) && iscell (newNames)) if (iscellstr (newNames{idc})) varNames = newNames{idc}; elseif (isa (newNames{idc}, 'string')) varNames = cellstr (newNames{idc}); else errmsg = strcat ("invalid input for", ... " 'NewVariableNames'."); return endif idc += 1; elseif (iscellstr (newNames) && idc == 1) varNames = newNames; else errmsg = "invalid input for 'NewVariableNames'."; return endif if (numel (varNames) != col) errmsg = "wrong number of 'NewVariableNames'."; return endif change_newNames = true; else change_newNames = false; endif ## Change variable data here if (istable (tmp)) tmpD = tabular.meta_or_blank (tmp.VariableDescriptions, col); tmpU = tabular.meta_or_blank (tmp.VariableUnits, col); for i = 1:col ## Copy new variable name if given if (change_newNames) tbl.VariableNames{idx} = varNames{i}; endif ## Copy data from each separate column tbl.VariableValues{idx} = tmp.VariableValues{i}; ## Copy variable properties from nested table tbl.VariableTypes{idx} = tmp.VariableTypes{i}; if (! isempty (tmpD{i})) if (isempty (tbl.VariableDescriptions)) tbl.VariableDescriptions = repmat ({''}, 1, ... numel (tbl.VariableNames)); endif tbl.VariableDescriptions{idx} = tmpD{i}; endif if (! isempty (tmpU{i})) if (isempty (tbl.VariableUnits)) tbl.VariableUnits = repmat ({''}, 1, ... numel (tbl.VariableNames)); endif tbl.VariableUnits{idx} = tmpU{i}; endif ## Variable-scoped custom properties are already replicated to the ## split columns by subsetvars via the repeated indices in ## 'ix_remap', so no further handling is needed here. idx += 1; endfor else for i = 1:col ## Copy new variable name if given if (change_newNames) tbl.VariableNames{idx} = varNames{i}; endif ## Copy data from each separate column tbl.VariableValues{idx} = tmp(:,i); tbl.VariableTypes{idx} = class (tmp(:,1)); idx += 1; endfor endif else idx += 1; endif endfor endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{TF}, @var{errmsg}] =} ## ismissingResult (@dots{}) ## ## Which elements of the variables are missing. ## ## Missing values belong to the variables, which a tabular object holds the ## same way whatever labels its rows. @var{errmsg} carries the body of any ## complaint so that the calling class can raise it under its own name. ## ## @end deftypefn function [TF, errmsg] = ismissingResult (this, varargin) TF = []; errmsg = ''; ## Parse optional Name-Value paired arguments optNames = {'OutputFormat'}; dfValues = {'logical'}; [outFmt, indicator] = parsePairedArguments (optNames, dfValues, ... varargin(:)); if (! any (strcmpi (outFmt, {'logical', 'tabular'}))) errmsg = "invalid value for 'OutputFormat'."; return endif ## Process each table variable with default missing values if (isempty (indicator)) for i = 1:width (this) tmpVar = this.VariableValues{i}; if (isa (tmpVar, 'tabular')) ## A nested table is one value per row and never missing, what ## it holds notwithstanding, as in MATLAB. this.VariableValues{i} = false (rows (tmpVar), 1); elseif (any (isa (tmpVar, {'calendarDuration', 'categorical', ... 'datetime', 'duration', 'string'}))) varTF = ismissing (tmpVar); varTF = any (varTF, 2); this.VariableValues{i} = varTF; elseif (ischar (tmpVar)) varTF = __ismissing__ (tmpVar); varTF = all (varTF, 2); this.VariableValues{i} = varTF; elseif (isa (tmpVar, 'missing')) this.VariableValues{i} = true (rows (tmpVar), 1); else # numeric, logical, and cellstr arrays varTF = __ismissing__ (tmpVar); varTF = any (varTF, 2); this.VariableValues{i} = varTF; endif endfor else ## Remove nested cell caused by parsing with paredArgs indicator = indicator{1}; ## Indicator must be a vector in any case, or '' if (! (isvector (indicator) || (ischar (indicator) && isempty (indicator)))) errmsg = "INDICATOR must be a vector."; return endif ## 'missing' marks the standard missing values of every variable idx_missing = false; idx_calendarDuration = false; ## Preprocess indicator if it is a cell array if (iscell (indicator) && ! iscellstr (indicator)) ## Elements in indicator vector must be scalars (except char vectors) fcn = @(x) isscalar (x) | isempty (x) | (ischar (x) & isvector (x)); all_scalar = all (cellfun (fcn, indicator)); if (! all_scalar) errmsg = strcat ("INDICATOR must explicitly", ... " contain scalar elements or character", ... " vectors."); return endif ## categorical arrays, each kind of indicator kept apart, since ## categorical.ismissing reads text and strings differently categorical_indicator = {}; fcn = @(x) isa (x, 'categorical'); ids_categorical = cellfun (fcn, indicator); if (any (ids_categorical)) categorical_indicator{end+1} = [indicator{ids_categorical}]; endif fcn = @(x) isa (x, 'string'); ids_categorical = cellfun (fcn, indicator); if (any (ids_categorical)) categorical_indicator{end+1} = [indicator{ids_categorical}]; endif ids_categorical = cellfun ('ischar', indicator); if (any (ids_categorical)) categorical_indicator{end+1} = indicator(ids_categorical); endif idx_categorical = ! isempty (categorical_indicator); ## datetime arrays fcn = @(x) isa (x, 'datetime'); idx_datetime = cellfun (fcn, indicator); if (any (idx_datetime)) datetime_indicator = [indicator{idx_datetime}]; idx_datetime = true; endif ## duration arrays fcn = @(x) isa (x, 'duration'); idx_duration = cellfun (fcn, indicator); if (any (idx_duration)) duration_indicator = [indicator{idx_duration}]; idx_duration = true; endif ## string arrays fcn = @(x) isa (x, 'string') || ischar (x) || iscellstr (x); idx_string = cellfun (fcn, indicator); if (any (idx_string)) string_indicator = string (indicator(idx_string)); idx_string = true; endif ## cell arrays of character vectors fcn = @(x) iscellstr (x); idx_iscstr = cellfun (fcn, indicator); if (any (idx_iscstr)) iscstr_indicator = indicator{idx_iscstr}; idx_iscstr = true; endif ## char arrays, each also a text value for cellstr variables idx_ischar = cellfun ('ischar', indicator); if (any (idx_ischar)) if (! idx_iscstr) iscstr_indicator = {}; endif iscstr_indicator = [iscstr_indicator(:); indicator(idx_ischar)(:)]; idx_iscstr = true; ischar_indicator = [indicator{idx_ischar}]; idx_ischar = true; endif ## numeric and logical arrays, each kept in its own class, since ## the class decides which variables it applies to fcn = @(x) isnumeric (x) || islogical (x); idx_numlog = cellfun (fcn, indicator); if (any (idx_numlog)) numlog_indicator = indicator(idx_numlog); idx_numlog = true; endif ## calendarDuration arrays fcn = @(x) isa (x, 'calendarDuration'); idx_calDur = cellfun (fcn, indicator); if (any (idx_calDur)) calendarDuration_indicator = [indicator{idx_calDur}]; idx_calendarDuration = true; endif ## missing fcn = @(x) isa (x, 'missing'); idx_missing = any (cellfun (fcn, indicator)); elseif (iscellstr (indicator)) ## cell arrays of character vectors, string and categorical arrays ## are searched idx_iscstr = true; iscstr_indicator = indicator; idx_string = true; string_indicator = indicator; idx_categorical = true; categorical_indicator = {indicator}; ## all other arrays are ignored idx_datetime = false; idx_duration = false; idx_ischar = false; idx_numlog = false; else # single data type indicator idx_categorical = false; idx_datetime = false; idx_duration = false; idx_string = false; idx_iscstr = false; idx_ischar = false; idx_numlog = false; if (isa (indicator, 'categorical')) idx_categorical = true; categorical_indicator = {indicator}; elseif (isa (indicator, 'datetime')) idx_datetime = true; datetime_indicator = indicator; elseif (isa (indicator, 'duration')) idx_duration = true; duration_indicator = indicator; elseif (isa (indicator, 'calendarDuration')) idx_calendarDuration = true; calendarDuration_indicator = indicator; elseif (isa (indicator, 'missing')) idx_missing = true; elseif (isa (indicator, 'string')) idx_string = true; string_indicator = indicator; idx_categorical = true; categorical_indicator = {indicator}; elseif (iscellstr (indicator)) idx_iscstr = true; iscstr_indicator = indicator; idx_string = true; string_indicator = string (indicator); elseif (ischar (indicator)) idx_ischar = true; ischar_indicator = cellstr (indicator); ## A character vector names one text value for cellstr variables, ## blanks kept, since cellstr strips them idx_iscstr = true; if (isrow (indicator) || isempty (indicator)) iscstr_indicator = {indicator}; else iscstr_indicator = cellstr (indicator); endif idx_string = true; string_indicator = string (indicator); idx_categorical = true; categorical_indicator = {indicator}; else # numeric and logical arrays idx_numlog = true; numlog_indicator = {indicator}; endif endif ## 'missing' adds each variable's standard missing values, which are ## what the call without an indicator reports if (idx_missing) TF_standard = ismissingResult (this, 'OutputFormat', 'tabular'); endif ## Return false TF vector for any datatypes that are not ## represented in the indicator and should be ignored TF_false = false (rows (this), 1); for i = 1:width (this) tmpVar = this.VariableValues{i}; if (isa (tmpVar, 'tabular')) varTF = TF_false; elseif (isa (tmpVar, 'calendarDuration')) if (idx_calendarDuration) varTF = ismissing (tmpVar, calendarDuration_indicator); else varTF = TF_false; endif elseif (isa (tmpVar, 'categorical')) if (idx_categorical) varTF = false (size (tmpVar)); for j = 1:numel (categorical_indicator) varTF |= ismissing (tmpVar, categorical_indicator{j}); endfor else varTF = TF_false; endif elseif (isa (tmpVar, 'datetime')) if (idx_datetime) varTF = ismissing (tmpVar, datetime_indicator); else varTF = TF_false; endif elseif (isa (tmpVar, 'duration')) if (idx_duration) varTF = ismissing (tmpVar, duration_indicator); else varTF = TF_false; endif elseif (isa (tmpVar, 'string')) if (idx_string) varTF = ismissing (tmpVar, string_indicator); else varTF = TF_false; endif elseif (iscellstr (tmpVar)) if (idx_iscstr) varTF = __ismissing__ (tmpVar, iscstr_indicator); else varTF = TF_false; endif elseif (ischar (tmpVar)) if (idx_ischar) varTF = __ismissing__ (cellstr (tmpVar), ischar_indicator); else varTF = TF_false; endif else # numeric and logical arrays ## An indicator applies to a variable of its own class, and a ## double on either side applies to every numeric and logical ## class; a variable no indicator applies to is not missing varTF = TF_false; if (idx_numlog) for j = 1:numel (numlog_indicator) ind = numlog_indicator{j}; if (isa (tmpVar, 'double') || isa (ind, 'double') || strcmp (class (tmpVar), class (ind))) varTF = varTF | __ismissing__ (tmpVar, ind); endif endfor endif endif varTF = any (varTF, 2); if (idx_missing) varTF = varTF | TF_standard.VariableValues{i}; endif this.VariableValues{i} = varTF; endfor endif ## Return appropriate OutputFormat. The array comes from the shared ## accessor rather than from 'table2array', which only a table has. if (strcmpi (outFmt, 'logical')) TF = varsAsArray (this, 'ismissing'); else TF = this; endif endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{tbl}, @var{TF}, @var{errmsg}] ## =} rmmissingResult (@dots{}) ## ## The object with its incomplete rows or variables removed. ## ## Missing values belong to the variables, which a tabular object holds the ## same way whatever labels its rows. @var{errmsg} carries the body of any ## complaint so that the calling class can raise it under its own name. ## ## @end deftypefn function [tbl, TF, errmsg] = rmmissingResult (this, varargin) tbl = this; TF = []; errmsg = ''; ## An optional DIM comes before any Name-Value pair dim = 1; if (! isempty (varargin) && (isnumeric (varargin{1}) || islogical (varargin{1}))) dim = varargin{1}; varargin(1) = []; if (! (isscalar (dim) && (dim == 1 || dim == 2))) errmsg = "DIM must be 1 or 2."; return endif endif if (mod (numel (varargin), 2) != 0) errmsg = "name-value arguments must be in pairs."; return endif ## Parse optional paired arguments; an empty 'DataVariables' stands ## for every variable and an empty 'MissingLocations' for what ## 'ismissing' finds optNames = {'MinNumMissing', 'DataVariables', 'MissingLocations'}; dfValues = {1, [], []}; [minNum, dVars, mLocs, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! isscalar (minNum) || fix (minNum) != minNum || minNum <= 0) errmsg = "'MinNumMissing' must be a positive integer."; return endif if (! isempty (dVars)) dIxVars = resolveVarRef (this, dVars, 'lenient'); if (any (dIxVars == 0)) badpos = find (dIxVars == 0)(1); dv = dVars; if (isa (dv, 'string')) dv = cellstr (dv); endif if (ischar (dv)) badname = dv; elseif (iscellstr (dv)) badname = dv{badpos}; else badname = ""; endif errmsg = sprintf (strcat ("'DataVariables' index a", ... " non-existing variable: '%s'"), badname); return endif dIxVars = dIxVars(:)'; else dIxVars = 1:width (this); endif tmpT = subsetvars (this, dIxVars); if (! isempty (args)) errmsg = "invalid optional paired argument."; return endif ## One column per variable operated on, true where a row is missing if (isempty (mLocs)) isMiss = ismissing (tmpT); elseif (islogical (mLocs)) ## Sized as the whole table, as in MATLAB, or as the variables ## operated on if (isequal (size (mLocs), size (this))) isMiss = mLocs(:,dIxVars); elseif (isequal (size (mLocs), size (tmpT))) isMiss = mLocs; else errmsg = strcat ("'MissingLocations' must be", ... " a logical matrix of the same size as the", ... " input table or the part of it referenced by", ... " 'DataVariables'."); return endif elseif (isa (mLocs, 'table')) if (! all (ismember (tmpT.VariableNames, mLocs.VariableNames))) errmsg = strcat ("'MissingLocations' must be", ... " a table with the same variable names as the", ... " input table or the part of it referenced by", ... " 'DataVariables'."); return endif isMiss = false (rows (this), width (tmpT)); for jx = 1:width (tmpT) kx = find (strcmp (tmpT.VariableNames{jx}, mLocs.VariableNames), 1); varTF = mLocs.VariableValues{kx}; if (! islogical (varTF)) errmsg = strcat ("'MissingLocations' must", ... " be a table with logical variables."); return endif if (! isequal (size (varTF), size (tmpT.VariableValues{jx}))) errmsg = strcat ("'MissingLocations' must", ... " be a table with the same variable sizes", ... " as the input table or the part of it", ... " referenced by 'DataVariables'."); return endif isMiss(:,jx) = any (varTF, 2); endfor else errmsg = "invalid data type for 'MissingLocations'."; return endif if (dim == 1) ## A row whose label disqualifies it goes whatever the data ## says, and whatever 'DataVariables' or 'MinNumMissing' say. TF = sum (isMiss, 2) >= minNum; TF = TF | ! usableRowLabels (this); tbl = subsetrows (this, ! TF); else ## Row labels are not a variable and never remove one TF = false (1, width (this)); TF(dIxVars) = sum (isMiss, 1) >= minNum; tbl = subsetvars (this, find (! TF)); endif endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{tbl}, @var{TF}, @var{errmsg}] ## =} fillmissingResult (@dots{}) ## ## The object with its missing values filled. ## ## Missing values belong to the variables, which a tabular object holds the ## same way whatever labels its rows. @var{errmsg} carries the body of any ## complaint so that the calling class can raise it under its own name. ## ## @end deftypefn function [tbl, TF, errmsg] = fillmissingResult (tblA, varargin) tbl = tblA; TF = []; errmsg = ''; ## Check input arguments if (nargin < 2) errmsg = "too few input arguments."; return endif ## Resolve the fill method (and the value for the 'constant' method) method = varargin{1}; if (isa (method, 'string') && isscalar (method)) method = char (method); endif if (! (ischar (method) && isrow (method))) errmsg = "METHOD must be a character vector."; return endif method = lower (method); rest = varargin(2:end); constVal = []; switch (method) case 'constant' if (isempty (rest)) errmsg = strcat ("the 'constant' method", ... " requires a fill value."); return endif constVal = rest{1}; rest = rest(2:end); case {'previous', 'next', 'nearest', 'linear', 'spline', ... 'pchip', 'makima'} ## supported; no extra positional argument case {'movmean', 'movmedian', 'knn', 'mean', 'median', 'mode'} errmsg = sprintf (strcat ("method '%s' is not supported", ... " yet."), method); return otherwise errmsg = sprintf ("unknown method '%s'.", method); return endswitch ## Where the rows sit, which decides how far apart a gap's neighbours ## are and what an interpolation runs against. Read before anything is ## filled, so a timetable that cannot say is refused whatever the method. [x, ownPoints, errmsg] = fillSamplePoints (tblA); if (! isempty (errmsg)) return endif if (mod (numel (rest), 2) != 0) errmsg = "name-value arguments must be in pairs."; return endif ## Parse optional paired arguments optNames = {'DataVariables', 'EndValues', 'ReplaceValues'}; dfValues = {[], 'extrap', true}; [dVars, endVals, replace, extra] = parsePairedArguments (optNames, ... dfValues, rest(:)); ## Anything left over was asked for and not understood. Saying so ## beats filling to a rule the caller did not ask for, which is what a ## misspelt option would otherwise get. if (! isempty (extra)) name = extra{1}; if (isa (name, 'string') && isscalar (name)) name = char (name); endif if (ischar (name) && isrow (name) && strcmpi (name, 'SamplePoints')) if (ownPoints) errmsg = strcat ("'SamplePoints' is not accepted here; the", ... " row times are the sample points."); else errmsg = "'SamplePoints' is not supported yet."; endif else errmsg = "invalid optional paired argument."; endif return endif if (! (islogical (replace) && isscalar (replace))) errmsg = "'ReplaceValues' must be a logical scalar."; return endif if (! replace) errmsg = strcat ("'ReplaceValues' set to false is", ... " not supported yet."); return endif ## 'EndValues' is checked here rather than where the end gaps are ## filled, so that a bad value is refused whatever the data looks like. ## A keyword names how the ends are filled; anything else is a constant, ## which must be a scalar and is type-checked against each variable when ## it is written. endKeys = {'extrap', 'none', 'previous', 'next', 'nearest'}; if (ischar (endVals) || (isa (endVals, 'string') && isscalar (endVals))) endVals = lower (char (endVals)); if (! any (strcmp (endVals, endKeys))) errmsg = strcat ("'EndValues' must be 'extrap',", ... " 'previous', 'next', 'nearest', 'none', or a", ... " scalar constant."); return endif elseif (! isscalar (endVals)) errmsg = "'EndValues' constant must be a scalar."; return endif ## Resolve targeted variables if (isempty (dVars)) ixVars = 1:width (tblA); else ixVars = resolveVarRef (tblA, dVars, 'lenient'); if (any (ixVars == 0)) badpos = find (ixVars == 0)(1); dv = dVars; if (isa (dv, 'string')) dv = cellstr (dv); endif if (ischar (dv)) badname = dv; elseif (iscellstr (dv)) badname = dv{badpos}; else badname = ""; endif errmsg = sprintf (strcat ("'DataVariables' index a", ... " non-existing variable: '%s'"), badname); return endif endif ## Every targeted variable must have a type the 'linear' method can ## interpolate, and that is settled before anything is filled: the call ## is refused whether or not the variable has an entry missing. if (strcmp (method, 'linear')) for k = 1:numel (ixVars) v = tblA.VariableValues{ixVars(k)}; if (isnumeric (v) || islogical (v) || isdatetime (v) ... || isduration (v)) continue; endif errmsg = sprintf (strcat ("the 'linear' method does not", ... " support the type of table variable '%s'."), ... tblA.VariableNames{ixVars(k)}); return endfor endif ## Resolve per-variable fill values for the 'constant' method if (strcmp (method, 'constant')) fillVals = resolve_const_values (constVal, numel (ixVars), ... class (tblA)); endif ## Initialize outputs (TF has one column per table variable) tbl = tblA; TF = false (height (tblA), width (tblA)); ## Fill each targeted variable for k = 1:numel (ixVars) iv = ixVars(k); v = tbl.VariableValues{iv}; M = __varmissing__ (v); if (! any (M(:))) continue; endif filled = false (size (M)); v0 = v; if (strcmp (method, 'constant')) [v, filled] = fill_constant (v, M, fillVals{k}, ... tbl.VariableNames{iv}, class (tblA)); else for c = 1:columns (M) m = M(:,c); if (! any (m)) continue; endif if (any (strcmp (method, {'linear', 'spline', 'pchip', ... 'makima'}))) [v(:,c), filled(:,c)] = fill_interp (v(:,c), m, x, method); else si = fill_neighbor_idx (m, method, x); rows = m & si > 0; v(rows,c) = v(si(rows),c); filled(:,c) = rows; endif endfor endif ## Every method has now filled the end gaps the way it extrapolates, ## which is what 'extrap' asks for. Any other 'EndValues' overrides ## them, whatever the method was. if (! (ischar (endVals) && strcmp (endVals, 'extrap'))) for c = 1:columns (M) if (! any (M(:,c))) continue; endif [v(:,c), filled(:,c)] = apply_end_values (v(:,c), v0(:,c), ... M(:,c), filled(:,c), endVals, ... tbl.VariableNames{iv}, class (tblA)); endfor endif tbl.VariableValues{iv} = v; TF(:,iv) = any (filled, 2); endfor endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{tbl}, @var{errmsg}] =} ## standardizeMissingResult (@dots{}) ## ## The object with the given values made missing. ## ## Missing values belong to the variables, which a tabular object holds the ## same way whatever labels its rows. @var{errmsg} carries the body of any ## complaint so that the calling class can raise it under its own name. ## ## @end deftypefn function [tbl, errmsg] = standardizeMissingResult (tblA, indicator, ... varargin) tbl = tblA; errmsg = ''; ## Check input arguments if (nargin < 2) errmsg = "too few input arguments."; return endif if (mod (numel (varargin), 2) != 0) errmsg = "name-value arguments must be in pairs."; return endif ## Parse optional paired arguments; an empty 'DataVariables' stands ## for every variable optNames = {'DataVariables'}; dfValues = {[]}; [dVars, args] = parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments and resolve targeted variables if (isempty (dVars)) ixVars = 1:width (tblA); else ixVars = resolveVarRef (tblA, dVars, 'lenient'); if (any (ixVars == 0)) badpos = find (ixVars == 0)(1); dv = dVars; if (isa (dv, 'string')) dv = cellstr (dv); endif if (ischar (dv)) badname = dv; elseif (iscellstr (dv)) badname = dv{badpos}; else badname = ""; endif errmsg = sprintf (strcat ("'DataVariables' index", ... " a non-existing variable: '%s'"), badname); return endif endif if (! isempty (args)) errmsg = "invalid optional paired argument."; return endif ## Split the indicator by the type of variable each value applies to ind = std_normalize_indicator (indicator, class (tblA)); ## Standardize each targeted variable tbl = tblA; for k = 1:numel (ixVars) iv = ixVars(k); v = tbl.VariableValues{iv}; if (isa (v, 'datetime') && ! isempty (ind.dt) && isempty (v.TimeZone) != isempty (ind.dt.TimeZone)) errmsg = sprintf (strcat ("cannot compare the datetime variable", ... " '%s' with a datetime indicator,", ... " because only one of them has a", ... " time zone."), tbl.VariableNames{iv}); return endif tbl.VariableValues{iv} = std_apply_indicator (v, ind); endfor endfunction ## The comparison behind 'isequal' and 'isequaln' on both classes. It ## compares the properties it names and nothing else, so the exclusions ## are the ones absent from the list: 'VariableTypes', which only ## restates the class of each variable and would make a table holding ## int8 unequal to one holding the same values as double, where the ## values themselves compare equal; and a timetable's 'TimeStep', ## 'SampleRate', 'StartTime' and step provenance, which describe the row ## times rather than adding to them, so a timetable told its step equals ## one that read the same step off the times it was given. An attached ## event table is state a user set rather than a description of the row ## times, so it is compared, and reached through 'eventsOf' because a ## shared body may not name the property. OTHERS holds ## the remaining arguments, already known to be of this same class. ## NANEQUAL selects 'isequaln' wherever a user value can sit. function TF = isequalResult (this, others, nanEqual) if (nanEqual) eqf = @isequaln; else eqf = @isequal; endif TF = false; for i = 1:numel (others) B = others{i}; if (! isequal (size (this), size (B))) return; endif if (! (isequal (this.VariableNames, B.VariableNames) && isequal (this.DimensionNames, B.DimensionNames) && isequal (this.Description, B.Description) && isequal (this.VariableDescriptions, B.VariableDescriptions) && isequal (this.VariableUnits, B.VariableUnits) && isequal (this.VariableContinuity, B.VariableContinuity) && isequal (this.CustomPropTypes, B.CustomPropTypes))) return; endif if (! (eqf (this.UserData, B.UserData) && eqf (this.CustomProperties, B.CustomProperties) && eqf (eventsOf (this), eventsOf (B)))) return; endif if (hasRowLabels (this) != hasRowLabels (B)) return; endif if (hasRowLabels (this) && ! eqf (getRowLabels (this), getRowLabels (B))) return; endif for j = 1:numel (this.VariableValues) if (! eqf (this.VariableValues{j}, B.VariableValues{j})) return; endif endfor endfor TF = true; endfunction ## The body behind 'varfun' on both classes. Returns an errmsg body ## (empty on success) for the caller to raise under its own name. function [B, errmsg] = varfunResult (this, func, args_in) B = []; errmsg = ''; scope = sprintf ('%s.varfun', class (this)); if (! is_function_handle (func)) errmsg = 'FUNC must be a function handle.'; return; endif if (mod (numel (args_in), 2) != 0) errmsg = "name-value arguments must be in pairs."; return; endif ## Parse optional paired arguments; empty 'InputVariables' and ## 'GroupingVariables' are resolved below, and an empty ## 'ErrorHandler' lets an error in FUNC through optNames = {'InputVariables', 'GroupingVariables', 'OutputFormat', ... 'ErrorHandler'}; dfValues = {[], [], 'auto', []}; [inVars, grpVars, outFmt, errHandler, args] = ... parsePairedArguments (optNames, dfValues, args_in(:)); ## Validate optional paired arguments outFmt = tabular.check_output_format (scope, outFmt, ... class (this)); if (! isempty (errHandler) && ! is_function_handle (errHandler)) errmsg = "'ErrorHandler' must be a function handle."; return; endif if (! isempty (args)) errmsg = "invalid optional paired argument."; return; endif ## Resolve grouping variables and input variables (default input is ## every variable that is not a grouping variable). byLabels = false; gIx = []; if (! isempty (grpVars)) [gIx, byLabels] = resolveGroupRef (this, grpVars); endif if (isempty (inVars)) iIx = 1:width (this); iIx(ismember (iIx, gIx)) = []; else iIx = resolveVarRef (this, inVars)(:)'; endif if (isempty (iIx)) errmsg = 'there are no variables to which to apply FUNC.'; return; endif ## Build the output variable names from the function and variable names. inNames = this.VariableNames(iIx); fname = tabular.apply_func_name (func); outNames = strcat (fname, '_', inNames); if (isempty (gIx) && ! byLabels) ## Ungrouped: apply FUNC to each whole variable. res = cell (1, numel (iIx)); for k = 1:numel (iIx) col = this.VariableValues{iIx(k)}; what = sprintf ("the variable '%s'", inNames{k}); out = tabular.apply_func (func, errHandler, k, 1, {col}, scope, ... what); res{1,k} = out{1}; endfor B = build_apply_result (this, scope, outFmt, res, outNames, {}, ... {}, [], {}, []); else ## Grouped: apply FUNC to each group's slice of each variable. The ## row labels group as a column of their own when they were named, ## and are reported as row labels of the result rather than as a ## grouping variable of it. inCols = this.VariableValues(iIx); grpCols = this.VariableValues(gIx); if (byLabels) labels = getRowLabels (this); grpCols = [{labels}, grpCols]; endif [G, ng, repRows, gerr] = tabular.group_table_rows (grpCols); if (! isempty (gerr)) errmsg = gerr; return; endif res = cell (ng, numel (iIx)); for g = 1:ng rows = (G == g); for k = 1:numel (iIx) col = inCols{k}; what = sprintf ("the variable '%s'", inNames{k}); out = tabular.apply_func (func, errHandler, g, 1, ... {rowsof(col, rows)}, scope, what); res{g,k} = out{1}; endfor endfor [gcols, gcount] = tabular.group_output_cols ( ... this.VariableValues(gIx), G, repRows); B = build_grouped_apply_result (this, scope, outFmt, res, outNames, ... gcols, this.VariableNames(gIx), ... gcount, repRows); endif endfunction ## The body behind 'repelem' and 'repmat' on both classes. ELEMENTWISE ## selects between them: 'repelem' repeats each row and each variable in ## place, 'repmat' repeats the whole block. Returns an errmsg body (empty ## on success) for the caller to raise under its own name. function [tbl, errmsg] = repeatResult (this, args, elementwise) tbl = []; errmsg = ''; nargs = numel (args); if (elementwise) ## 'repelem' takes a count per dimension and nothing else: neither a ## lone count nor a size vector, a tabular object having exactly two ## dimensions and both being named. Each count is a scalar or one per ## row or variable, checked in the order MATLAB checks them. if (nargs != 2) errmsg = 'exactly three input arguments are required.'; return; endif rows = args{1}; cols = args{2}; counts = {rows, cols}; dims = [height(this), width(this)]; for j = 1:2 k = counts{j}; if (! isnumeric (k)) errmsg = 'replication factors must be numeric.'; return; elseif (! (isscalar (k) || (isvector (k) && numel (k) == dims(j)))) errmsg = sprintf (strcat ("in repelem (A, N1, N2), N%d must be", ... " a scalar or a vector of length", ... " size (A, %d)."), j, j); return; elseif (! (isreal (k) && all (k(:) >= 0) && all (k(:) == fix (k(:))))) errmsg = strcat ("replication factors must be nonnegative", ... " integer-valued scalars or vectors."); return; endif endfor else ## 'repmat' takes a lone count for both dimensions, a count for each, ## or the two of them as a size vector. if (nargs < 1) errmsg = 'too few input arguments.'; return; endif if (nargs > 2) errmsg = 'only 2 dimensions are supported.'; return; endif if (nargs == 1) sz = args{1}; if (! isnumeric (sz)) errmsg = strcat ("replication factors must be a row vector of", ... " integers or integer scalars."); return; endif if (isscalar (sz)) rows = cols = sz; elseif (numel (sz) == 2) rows = sz(1); cols = sz(2); else errmsg = 'only 2 dimensions are supported.'; return; endif else rows = args{1}; cols = args{2}; endif for v = {rows, cols} k = v{1}; if (! (isnumeric (k) && isscalar (k) && isreal (k) && k == fix (k))) errmsg = strcat ("replication factors must be a row vector of", ... " integers or integer scalars."); return; endif endfor ## A negative count repeats nothing, as it does for an array. rows = max (rows, 0); cols = max (cols, 0); endif tbl = this; ## Replicate the rows by the index they come from, then hand the row ## labels to the subclass, which alone knows whether a repeated label ## needs a name of its own. A count may be one per row, so the test is ## whether any of them repeats or drops something. if (any (rows(:) != 1)) nrow = height (this); if (elementwise) ixRows = repelem ((1:nrow)', rows, 1); else ixRows = repmat ((1:nrow)', rows, 1); endif if (width (this) == 0) ## No variable carries the height, so the stored count does. tbl.RowCount = numel (ixRows); endif for i = 1:width (this) col = this.VariableValues{i}; tbl.VariableValues{i} = rowsof (col, ixRows); endfor tbl = repeatRowLabels (tbl, rows, elementwise); endif ## Replicate the variables the same way, numbering the repeats in the ## order they come out. The height is carried across, since a result ## with no variables left has nothing else to carry it. if (any (cols(:) != 1)) nrowsOut = height (tbl); nvar = width (this); if (elementwise) ixVars = repelem ((1:nvar)', cols, 1)'; else ixVars = repmat ((1:nvar)', cols, 1)'; endif tbl.VariableTypes = tbl.VariableTypes(ixVars); tbl.VariableValues = tbl.VariableValues(ixVars); tbl.VariableDescriptions = tabular.subset_meta ( ... tbl.VariableDescriptions, ixVars); tbl.VariableUnits = tabular.subset_meta (tbl.VariableUnits, ixVars); if (! isempty (tbl.VariableContinuity)) tbl.VariableContinuity = tbl.VariableContinuity(ixVars); endif newNames = cell (1, numel (ixVars)); seen = zeros (1, nvar); for k = 1:numel (ixVars) v = ixVars(k); seen(v)++; if (seen(v) == 1) newNames{k} = this.VariableNames{v}; else newNames{k} = sprintf ('%s_%d', this.VariableNames{v}, ... seen(v) - 1); endif endfor tbl.VariableNames = newNames; ## Handle custom variable properties if (! isempty (this.CustomProperties)) cp_names = customPropsOfType (this, 'variable'); for i = 1:numel (cp_names) nm = cp_names{i}; val = tbl.CustomProperties.(nm); tbl.CustomProperties.(nm) = val(ixVars); endfor endif tbl = setRowCount (tbl, nrowsOut); endif endfunction ## The body behind 'groupsummary' on both classes. Returns an errmsg ## body (empty on success) for the caller to raise under its own name. ## Like 'groupcounts' the result is a table whatever the input was, its ## rows describing groups rather than the rows they came from. function [G, errmsg] = groupsummaryResult (this, groupvars, args_in) G = []; errmsg = ''; scope = sprintf ('%s.groupsummary', class (this)); ## Split the trailing arguments into the optional positional METHOD and ## DATAVARS arguments and any Name-Value pairs. A Name-Value region ## starts at the first char-vector/string that names a known option. optNames = {'IncludeMissingGroups', 'IncludeEmptyGroups', ... 'IncludedEdge'}; nvStart = numel (args_in) + 1; for k = 1:numel (args_in) a = args_in{k}; if ((ischar (a) && isrow (a)) || (isa (a, 'string') && isscalar (a))) if (any (strcmpi (char (a), optNames))) nvStart = k; break; endif endif endfor posArgs = args_in(1:nvStart-1); nvArgs = args_in(nvStart:end); ## An optional GROUPBINS positional argument precedes METHOD. hasGroupbins = false; groupbins = []; if (! isempty (posArgs) && __groupbins__ ('is_spec', posArgs{1})) hasGroupbins = true; groupbins = posArgs{1}; posArgs = posArgs(2:end); endif if (numel (posArgs) > 2) errmsg = 'too many positional arguments.'; return; endif if (numel (posArgs) >= 1) method = posArgs{1}; else method = {}; endif if (numel (posArgs) >= 2) datavars = posArgs{2}; hasDataVars = true; else datavars = []; hasDataVars = false; endif ## Parse Name-Value options. dfValues = {true, false, 'left'}; [incMiss, incEmpty, incEdge] = ... parsePairedArguments (optNames, dfValues, nvArgs(:)); if (! (isscalar (incMiss) && (islogical (incMiss) || isnumeric (incMiss)))) errmsg = "'IncludeMissingGroups' must be a logical scalar."; return; endif incMiss = logical (incMiss); if (! (isscalar (incEmpty) && (islogical (incEmpty) || isnumeric (incEmpty)))) errmsg = "'IncludeEmptyGroups' must be a logical scalar."; return; endif incEmpty = logical (incEmpty); incEdge = tabular.check_included_edge (scope, incEdge); ## Normalise METHOD into parallel cell arrays of method specs and the ## display names used to build output variable names. [methods, methNames, errmsg] = tabular.gs_normalise_methods (method); if (! isempty (errmsg)) return; endif ## Resolve grouping and data variables. The default data variables are ## all variables that are not grouping variables. [gIx, byLabels] = resolveGroupRef (this, groupvars); if (isempty (gIx) && ! byLabels) errmsg = 'at least one grouping variable is required.'; return; endif if (hasDataVars) dIx = resolveVarRef (this, datavars)(:)'; else dIx = 1:width (this); dIx(ismember (dIx, gIx)) = []; endif ## Bin the grouping variables when a GROUPBINS argument was given. The ## row labels group as a column of their own when they were named. grpCols = this.VariableValues(gIx); grpNames = this.VariableNames(gIx); if (byLabels) labels = getRowLabels (this); lname = rowLabelName (this); grpCols = [{labels}, grpCols]; grpNames = [{lname}, grpNames]; endif if (hasGroupbins) [grpCols, grpNames, errmsg] = __groupbins__ ('bin', grpCols, ... grpNames, groupbins, incEdge, ... 'groupsummary'); if (! isempty (errmsg)) return; endif endif ## Group the rows, treating missing grouping values as their own groups ## (sorted last) when IncludeMissingGroups is true; IncludeEmptyGroups ## adds the unused categories of a categorical or binned grouping ## variable as empty groups. [Grp, ng, gcols, errmsg] = tabular.gs_grouping (grpCols, incMiss, ... incEmpty); if (! isempty (errmsg)) return; endif gcount = accumarray (Grp(! isnan (Grp)), 1, [ng, 1]); ## Compute each method over each data variable. Output columns are ## ordered data variable first, then method, to match MATLAB's order. datNames = this.VariableNames(dIx); rescols = {}; resNames = {}; for di = 1:numel (dIx) for mi = 1:numel (methods) col = this.VariableValues{dIx(di)}; vals = cell (ng, 1); for g = 1:ng rows = (Grp == g); [v, merr] = tabular.gs_apply_method (methods{mi}, ... rowsof (col, rows)); if (! isempty (merr)) errmsg = sprintf ("variable '%s': %s", datNames{di}, merr); return; endif vals{g} = v; endfor try rescols{end+1} = vertcat (vals{:}); catch errmsg = sprintf (strcat ("the '%s' results for variable '%s'", ... " cannot be concatenated into a", ... " column."), methNames{mi}, ... datNames{di}); return; end_try_catch resNames{end+1} = sprintf ("%s_%s", methNames{mi}, datNames{di}); endfor endfor vars = [gcols, {gcount}, rescols]; names = [grpNames, {'GroupCount'}, resNames]; G = table (vars{:}, 'VariableNames', names); endfunction ## The body behind 'join' on both classes. NAMEL and NAMER are the ## caller's own names for the two operands, which name a collision. ## Returns an errmsg body (empty on success) for the caller to raise ## under its own name. The result is of the left operand's class: its ## rows and its labels are kept and the right side contributes variables. function [tbl, ixR, errmsg] = joinResult (tblL, tblR, args_in, ... nameL, nameR) tbl = []; ixR = []; errmsg = ''; if (! isa (tblR, 'tabular')) errmsg = 'both inputs must be tables or timetables.'; return; endif if (mod (numel (args_in), 2) != 0) errmsg = "name-value arguments must be in pairs."; return; endif ## Parse optional paired arguments optNames = {'Keys', 'LeftKeys', 'RightKeys', 'LeftVariables', ... 'RightVariables', 'KeepOneCopy'}; dfValues = {[], [], [], [], [], []}; [Keys, LeftKeys, RightKeys, LeftVariables, RightVariables, ... KeepOneCopy, args] = parsePairedArguments (optNames, dfValues, ... args_in(:)); if (! isempty (args)) errmsg = "invalid optional paired argument."; return; endif ## Resolve key columns on each side. A class that groups by its row ## labels answers to their name, so a timetable joins on its row times. [lCols, rCols, ~, rKeyIdx, errmsg] = joinKeys (tblL, tblR, Keys, ... LeftKeys, RightKeys); if (! isempty (errmsg)) return; endif ## Resolve output variables on each side if (isempty (LeftVariables)) lVarIdx = 1:width (tblL); else lVarIdx = resolveVarRef (tblL, LeftVariables); endif if (isempty (RightVariables)) rVarIdx = setdiff (1:width (tblR), rKeyIdx(rKeyIdx > 0)); else rVarIdx = resolveVarRef (tblR, RightVariables); endif ## Drop the right copy of a 'KeepOneCopy' variable shared with the left if (! isempty (KeepOneCopy)) keepNames = cellstr (KeepOneCopy); rNames = tblR.VariableNames(rVarIdx); lNames = tblL.VariableNames(lVarIdx); dropMask = ismember (rNames, keepNames) & ismember (rNames, lNames); rVarIdx(dropMask) = []; endif ## Build consistent numeric key proxies for both sides [leftProxy, rightProxy, errmsg] = tabular.joinProxies (lCols, ... rCols); if (! isempty (errmsg)) return; endif ## The right key combinations must be unique if (rows (unique (rightProxy, 'rows')) != rows (rightProxy)) errmsg = strcat ("the key variables of TBLR must contain unique", ... " combinations of values."); return; endif ## Match each left row to its unique right row [tf, ixR] = ismember (leftProxy, rightProxy, 'rows'); if (! all (tf)) errmsg = strcat ("the key variables of TBLR must contain all", ... " values of the key variables of TBLL."); return; endif ## Assemble the output: the left rows keep their labels, the right side ## contributes variables and nothing else. Lpart = subsetvars (tblL, lVarIdx); Rpart = subsetrows (subsetvars (tblR, rVarIdx), ixR); Rpart = plainTable (Rpart); [Lpart, Rpart] = suffixShared (Lpart, Rpart, nameL, nameR); tbl = horzcat (Lpart, Rpart); endfunction ## The body behind 'innerjoin' on both classes. NAMEL and NAMER are the ## caller's own names for the two operands, which name a collision. ## Returns an errmsg body (empty on success) for the caller to raise ## under its own name. The result is of the left operand's class and ## carries the labels of the left rows it matched, where those labels are ## a dimension of their own; a table's row names are dropped, the rows ## being pairs rather than the rows they came from. function [tbl, ixL, ixR, errmsg] = innerjoinResult (tblL, tblR, ... args_in, nameL, nameR) tbl = []; ixL = []; ixR = []; errmsg = ''; if (! isa (tblR, 'tabular')) errmsg = 'both inputs must be tables or timetables.'; return; endif if (mod (numel (args_in), 2) != 0) errmsg = "name-value arguments must be in pairs."; return; endif ## Parse optional paired arguments optNames = {'Keys', 'LeftKeys', 'RightKeys', 'LeftVariables', ... 'RightVariables'}; dfValues = {[], [], [], [], []}; [Keys, LeftKeys, RightKeys, LeftVariables, RightVariables, args] = ... parsePairedArguments (optNames, dfValues, args_in(:)); if (! isempty (args)) errmsg = "invalid optional paired argument."; return; endif [lCols, rCols, ~, rKeyIdx, errmsg] = joinKeys (tblL, tblR, Keys, ... LeftKeys, RightKeys); if (! isempty (errmsg)) return; endif ## Resolve output variables on each side if (isempty (LeftVariables)) lVarIdx = 1:width (tblL); else lVarIdx = resolveVarRef (tblL, LeftVariables); endif if (isempty (RightVariables)) rVarIdx = setdiff (1:width (tblR), rKeyIdx(rKeyIdx > 0)); else rVarIdx = resolveVarRef (tblR, RightVariables); endif ## Build consistent numeric key proxies for both sides [leftProxy, rightProxy, errmsg] = tabular.joinProxies (lCols, ... rCols); if (! isempty (errmsg)) return; endif ## Match key rows and lay out the Cartesian product, key-sorted Nl = height (tblL); [uKeys, ~, ic] = unique ([leftProxy; rightProxy], 'rows'); icL = ic(1:Nl); icR = ic(Nl+1:end); for g = 1:rows (uKeys) lr = find (icL == g); rr = find (icR == g); if (! isempty (lr) && ! isempty (rr)) ixL = [ixL; repelem(lr(:), numel (rr), 1)]; ixR = [ixR; repmat(rr(:), numel (lr), 1)]; endif endfor ## Assemble the output Lpart = subsetrows (subsetvars (tblL, lVarIdx), ixL); if (isempty (rowLabelHeader (tblL))) Lpart = clearRowLabels (Lpart); endif Rpart = plainTable (subsetrows (subsetvars (tblR, rVarIdx), ixR)); [Lpart, Rpart] = suffixShared (Lpart, Rpart, nameL, nameR); tbl = horzcat (Lpart, Rpart); endfunction ## The key columns of a join, resolved on both sides from whichever of ## 'Keys', 'LeftKeys'/'RightKeys' or the default was given. RKEYIDX ## indexes the right variables the keys used, 0 where a key is the row ## labels on either side. Returns an errmsg body (empty on success). function [lCols, rCols, lKeyIdx, rKeyIdx, errmsg] = joinKeys (tblL, ... tblR, Keys, LeftKeys, RightKeys) lCols = {}; rCols = {}; lKeyIdx = []; rKeyIdx = []; errmsg = ''; if (! isempty (Keys)) if (! isempty (LeftKeys) || ! isempty (RightKeys)) errmsg = strcat ("'Keys' cannot be combined with 'LeftKeys' or", ... " 'RightKeys'."); return; endif [lCols, ~, lKeyIdx] = keyColumns (tblL, Keys); [rCols, ~, rKeyIdx] = keyColumns (tblR, Keys); elseif (! isempty (LeftKeys) || ! isempty (RightKeys)) if (isempty (LeftKeys) || isempty (RightKeys)) errmsg = strcat ("'LeftKeys' and 'RightKeys' must be specified", ... " together."); return; endif [lCols, ~, lKeyIdx] = keyColumns (tblL, LeftKeys); [rCols, ~, rKeyIdx] = keyColumns (tblR, RightKeys); if (numel (lCols) != numel (rCols)) errmsg = strcat ("'LeftKeys' and 'RightKeys' must reference the", ... " same number of variables."); return; endif elseif (groupsByLabels (tblL) && groupsByLabels (tblR)) ## Two classes labelled by the same kind of thing join on the labels. lCols = {getRowLabels(tblL)}; rCols = {getRowLabels(tblR)}; lKeyIdx = 0; rKeyIdx = 0; else isCommon = ismember (tblL.VariableNames, tblR.VariableNames); lKeyIdx = find (isCommon); if (isempty (lKeyIdx)) errmsg = strcat ("cannot find any common key variables between", ... " the two tables."); return; endif [~, rKeyIdx] = ismember (tblL.VariableNames(lKeyIdx), ... tblR.VariableNames); lCols = tblL.VariableValues(lKeyIdx); rCols = tblR.VariableValues(rKeyIdx); endif endfunction ## The two sides of a join with any name they share suffixed by the ## caller's own name for each operand. function [Lpart, Rpart] = suffixShared (Lpart, Rpart, nameL, nameR) shared = intersect (Lpart.VariableNames, Rpart.VariableNames); if (isempty (shared)) return; endif [lsuf, rsuf] = tabular.join_suffixes (nameL, nameR); ## Both operands under one name still clash; MATLAB numbers the left. if (strcmp (lsuf, rsuf)) lsuf = [lsuf, '_1']; endif lNames = Lpart.VariableNames; rNames = Rpart.VariableNames; for i = find (ismember (lNames, shared)) lNames{i} = [lNames{i}, lsuf]; endfor for i = find (ismember (rNames, shared)) rNames{i} = [rNames{i}, rsuf]; endfor Lpart.VariableNames = lNames; Rpart.VariableNames = rNames; endfunction ## The body behind 'outerjoin' on both classes. NAMEL and NAMER are the ## caller's own names for the two operands, which name a collision. ## Returns an errmsg body (empty on success) for the caller to raise ## under its own name. The result is of the left operand's class; a row ## with no left row to come from carries no label of its own. function [tbl, ixL, ixR, errmsg] = outerjoinResult (tblL, tblR, ... args_in, nameL, nameR) tbl = []; ixL = []; ixR = []; errmsg = ''; if (! isa (tblR, 'tabular')) errmsg = 'both inputs must be tables or timetables.'; return; endif if (mod (numel (args_in), 2) != 0) errmsg = "name-value arguments must be in pairs."; return; endif ## Parse optional paired arguments optNames = {'Keys', 'LeftKeys', 'RightKeys', 'LeftVariables', ... 'RightVariables', 'Type', 'MergeKeys'}; dfValues = {[], [], [], [], [], 'full', false}; [Keys, LeftKeys, RightKeys, LeftVariables, RightVariables, Type, ... MergeKeys, args] = parsePairedArguments (optNames, dfValues, args_in(:)); if (! isempty (args)) errmsg = "invalid optional paired argument."; return; endif ## Validate 'Type' and 'MergeKeys' if (! (ischar (Type) && isrow (Type)) || ! any (strcmpi (Type, {'full', 'left', 'right'}))) errmsg = "'Type' must be 'full', 'left', or 'right'."; return; endif Type = lower (Type); if (! (islogical (MergeKeys) && isscalar (MergeKeys))) errmsg = "'MergeKeys' must be a logical scalar."; return; endif ## Resolve key columns on each side. A class that groups by its row ## labels answers to their name, so a timetable joins on its row times. [lCols, rCols, lKeyIdx, rKeyIdx, errmsg] = joinKeys (tblL, tblR, ... Keys, LeftKeys, RightKeys); if (! isempty (errmsg)) return; endif ## Resolve output variables (defaults: all variables of each table) if (isempty (LeftVariables)) lVarIdx = 1:width (tblL); else lVarIdx = resolveVarRef (tblL, LeftVariables); endif if (isempty (RightVariables)) rVarIdx = 1:width (tblR); else rVarIdx = resolveVarRef (tblR, RightVariables); endif ## Build consistent numeric key proxies for both sides [leftProxy, rightProxy, errmsg] = tabular.joinProxies (lCols, rCols); if (! isempty (errmsg)) return; endif ## Match key rows, producing zero-filled index vectors per join type Nl = height (tblL); [uKeys, ~, ic] = unique ([leftProxy; rightProxy], 'rows'); icL = ic(1:Nl); icR = ic(Nl+1:end); keepL = any (strcmp (Type, {'full', 'left'})); keepR = any (strcmp (Type, {'full', 'right'})); ixL = []; ixR = []; for g = 1:rows (uKeys) lr = find (icL == g); rr = find (icR == g); nl = numel (lr); nr = numel (rr); if (nl > 0 && nr > 0) ixL = [ixL; repelem(lr(:), nr, 1)]; ixR = [ixR; repmat(rr(:), nl, 1)]; elseif (nl > 0 && keepL) ixL = [ixL; lr(:)]; ixR = [ixR; zeros(nl, 1)]; elseif (nr > 0 && keepR) ixL = [ixL; zeros(nr, 1)]; ixR = [ixR; rr(:)]; endif endfor ## Assemble each side, filling unmatched rows with missing values [Lout, errmsg] = joinBuildSide (subsetvars (tblL, lVarIdx), ixL); if (! isempty (errmsg)) return; endif [Rout, errmsg] = joinBuildSide (subsetvars (tblR, rVarIdx), ixR); if (! isempty (errmsg)) return; endif Rout = plainTable (Rout); ## A row with no row of its own on the left has no label from there. ## Where the key is the labels themselves, the right side knows it: the ## row matched nothing precisely because its key, which is its label, ## was not on the left. Where the key is an ordinary variable, nothing ## says when such a row happened and its label stays missing. if (any (lKeyIdx == 0) && ! isempty (rowLabelHeader (tblL))) fill = (ixL == 0); if (any (fill)) labels = getRowLabels (Lout); rlabels = getRowLabels (tblR); labels(fill) = rlabels(ixR(fill)); Lout = setRowLabels (Lout, labels); endif endif ## Optionally merge each key pair into a single variable. A merged key ## keeps the left position; its name is the left key name when both keys ## share it, or 'leftName_rightName' when they differ. if (MergeKeys) [tfL, posL] = ismember (lKeyIdx, lVarIdx); [tfR, posR] = ismember (rKeyIdx, rVarIdx); tfL = tfL & (lKeyIdx > 0); tfR = tfR & (rKeyIdx > 0); dropR = []; fillRows = (ixL == 0); lNames = Lout.VariableNames; for k = 1:numel (lKeyIdx) if (tfL(k) && tfR(k)) mcol = Lout.VariableValues{posL(k)}; rcol = Rout.VariableValues{posR(k)}; mcol(fillRows,:) = rcol(fillRows,:); Lout.VariableValues{posL(k)} = mcol; lkn = tblL.VariableNames{lKeyIdx(k)}; rkn = tblR.VariableNames{rKeyIdx(k)}; if (! strcmp (lkn, rkn)) lNames{posL(k)} = [lkn, '_', rkn]; endif dropR = [dropR, posR(k)]; endif endfor Lout.VariableNames = lNames; if (! isempty (dropR)) Rout = subsetvars (Rout, setdiff (1:width (Rout), dropR)); endif endif ## Suffix any variable names shared by both sides [Lout, Rout] = suffixShared (Lout, Rout, nameL, nameR); tbl = horzcat (Lout, Rout); endfunction ## The body behind 'findgroups' on both classes. Returns an errmsg body ## (empty on success) for the caller to raise under its own name. The ## groups are read from the variables alone; row labels take no part, and ## TID is a plain table, its rows being groups rather than rows of the ## input. function [G, TID, errmsg] = findgroupsResult (this) G = []; TID = []; errmsg = ''; nvar = width (this); n = height (this); if (nvar == 0) errmsg = 'T must have at least one variable.'; return; endif ## Build the combined proxy matrix and the overall missing-row mask. P = []; miss = false (n, 1); for j = 1:nvar [p, m, errmsg] = tabular.group_col_proxy (this.VariableValues{j}); if (! isempty (errmsg)) return; endif P = [P, p]; miss = miss | m; endfor ## Label the non-missing rows by sorted unique combination. G = NaN (n, 1); keep = find (! miss); ia = []; if (! isempty (keep)) [~, ia, ic] = unique (P(keep,:), "rows"); G(keep) = ic; endif if (isempty (keep)) TID = plainTable (subsetrows (this, [])); else repRows = keep(ia); idcols = cell (1, nvar); for j = 1:nvar col = this.VariableValues{j}; idcols{j} = rowsof (col, repRows); endfor TID = table (idcols{:}, "VariableNames", this.VariableNames); endif endfunction ## The body behind 'splitapply' on both classes. Returns an errmsg body ## (empty on success) for the caller to raise under its own name. The ## function receives one argument per variable, holding the rows of one ## group; row labels take no part. function [results, N, errmsg] = splitapplyResult (this, func, G, nout) results = {}; N = 0; errmsg = ''; if (! is_function_handle (func)) errmsg = 'FUNC must be a function handle.'; return; endif n = height (this); if (! (isnumeric (G) && isvector (G) && numel (G) == n)) errmsg = strcat ("G must be a numeric vector with one element per", ... " row of T."); return; endif G = G(:); gv = G(! isnan (G)); if (any (gv != fix (gv)) || any (gv < 1)) errmsg = 'G must contain positive integers.'; return; endif if (! isempty (gv)) N = max (gv); if (! isequal (unique (gv), (1:N)')) errmsg = strcat ("G must contain every integer between 1 and the", ... " number of groups."); return; endif endif nvar = width (this); results = cell (N, nout); for g = 1:N rows = (G == g); args = cell (1, nvar); for j = 1:nvar col = this.VariableValues{j}; args{j} = rowsof (col, rows); endfor [results{g,:}] = func (args{:}); endfor endfunction ## The body behind 'unstack' on both classes. Returns an errmsg body ## (empty on success) for the caller to raise under its own name. A class ## whose row labels make a row distinct groups by them, so a timetable ## with no grouping variable still answers with one row per row time, and ## the result is of the calling class. function [tbl, idxA, errmsg] = unstackResult (this, vars, ivar, args_in) tbl = []; idxA = []; errmsg = ''; scope = sprintf ('%s.unstack', class (this)); ## Check input argument if (isempty (vars)) errmsg = "too few input arguments."; return; endif ## Define allowed vartypes (cellstr + numeric are checked in place) allowed = {'logical', 'string', 'categorical'}; if (mod (numel (args_in), 2) != 0) errmsg = "name-value arguments must be in pairs."; return; endif ## Parse optional paired arguments; empty 'GroupingVariables', ## 'ConstantVariables', 'NewDataVariableNames' and ## 'AggregationFunction' are resolved below optNames = {'GroupingVariables', 'ConstantVariables', ... 'NewDataVariableNames', 'AggregationFunction', ... 'VariableNamingRule'}; dfValues = {[], [], [], [], 'modify'}; [groupVars, constVars, newVarNames, aggrFcn, rule, args] = ... parsePairedArguments (optNames, dfValues, args_in(:)); if (! isempty (args)) errmsg = "invalid optional paired argument."; return; endif ## Get variables to unstack [ixVars, ~] = resolveVarRef (this, vars, 'lenient'); if (any (ixVars == 0)) vars = cellstr (vars); errmsg = sprintf ("VARS index a non-existing variable: '%s'", ... vars{find (ixVars == 0)}); return; endif ## Check that variables to unstack do not contain nested tables for i = ixVars if (isa (this.VariableValues{i}, 'table')) errmsg = "VARS must not index nested tables."; return; endif endfor ## Move variables to unstack into a new table VarsTable = subsetvars (this, ixVars); ## Get indicator variable [ixIvar, ~] = resolveVarRef (this, ivar, 'lenient'); if (! isscalar (ixIvar)) errmsg = "IVAR must index a single variable."; return; elseif (ixIvar == 0) ivar = cellstr (ivar); errmsg = sprintf ("IVAR indexes a non-existing variable: '%s'", ... ivar{find (ixIvar == 0)}); return; endif ## Check indicator variable is not a multicolumn variable ## or member of the variables to be unstacked IvarValues = this.VariableValues{ixIvar}; if (! isvector (IvarValues)) errmsg = "IVAR must index a single column variable."; return; endif if (ismember (ixIvar, ixVars)) errmsg = strcat ("IVAR cannot be any of the", ... " variables to be unstacked as specified by VARS."); return; endif ## Check indicator variable is of a valid type if (! (iscellstr (IvarValues) || isnumeric (IvarValues))) if (! ismember (class (IvarValues), allowed)) errmsg = sprintf (strcat ("IVAR indexes a variable of", ... " invalid type: '%s'"), ... class (IvarValues)); return; endif IvarValues = cellstr (string (IvarValues)); endif ## Get default names for new unstacked variables IvarNames = __unique__ (IvarValues); ## Force both names and values to cellstr if (! iscellstr (IvarNames)) IvarNames = cellstr (string (IvarNames)); IvarValues = cellstr (string (IvarValues)); endif ## Get constant variables if (! isempty (constVars)) cIxVars = resolveVarRef (this, constVars, 'lenient'); if (any (cIxVars == 0)) constVars = cellstr (constVars); errmsg = sprintf (strcat ("'ConstantVariables' index a", ... " non-existing variable: '%s'"), ... constVars{find (cIxVars == 0)}); return; endif if (any (ismember (cIxVars, ixVars))) errmsg = strcat ("'ConstantVariables' cannot", ... " contain any variables to be unstacked as", ... " specified by VARS."); return; endif if (any (ismember (cIxVars, ixIvar))) errmsg = strcat ("'ConstantVariables' cannot", ... " contain the indicator variable as specified", ... " by IVAR."); return; endif else cIxVars = []; endif ## Get grouping variables if (isempty (groupVars)) gIxVars = setdiff (1:width (this), [ixVars, ixIvar, cIxVars]); else gIxVars = resolveVarRef (this, groupVars, 'lenient'); if (any (gIxVars == 0)) groupVars = cellstr (groupVars); errmsg = sprintf (strcat ("'GroupingVariables' index a", ... " non-existing variable: '%s'"), ... groupVars{find (gIxVars == 0)}); return; endif if (any (ismember (gIxVars, ixVars))) errmsg = strcat ("'GroupingVariables' cannot", ... " contain any variables to be unstacked as", ... " specified by VARS."); return; endif if (any (ismember (gIxVars, ixIvar))) errmsg = strcat ("'GroupingVariables' cannot", ... " contain the indicator variable as specified", ... " by IVAR."); return; endif endif ## Exclude variables of invalid type as grouping variables (emit warning) for i = numel (gIxVars):-1:1 GvarValues = this.VariableValues{gIxVars(i)}; if (! (iscellstr (GvarValues) || isnumeric (GvarValues))) if (! ismember (class (GvarValues), allowed)) invalid = this.VariableNames{gIxVars(i)}; gIxVars(i) = []; warning (strcat ("%s: 'GroupingVariables' index a variable", ... " of invalid type: '%s', which is", ... " ignored."), scope, invalid); endif endif endfor ## Move grouping variables into a new table removeVar = setdiff (1:width (this), gIxVars); GvarTable = removevars (this, removeVar); ## Move constant variables into a new table if (! isempty (cIxVars)) if (any (ismember (cIxVars, gIxVars)) && ! isempty (groupVars)) errmsg = strcat ("'ConstantVariables' cannot", ... " contain any grouping variables as specified", ... " by 'GroupingVariables'."); return; endif CvarTable = subsetvars (this, cIxVars); else CvarTable = table; endif ## Get new data variable names if (isempty (newVarNames)) newVarNames = IvarNames'; else if (! (iscellstr (newVarNames) && ! (isa (newVarNames, 'string')))) errmsg = strcat ("'NewDataVariableNames' must be", ... " either a cell array of character vectors, or", ... " a string array."); return; endif if (numel (newVarNames) != numel (IvarNames)) errmsg = strcat ("'NewDataVariableNames' do not", ... " match the number of unique values in the", ... " indicator variable."); return; endif endif ## Check user-defined aggregation function if (! isempty (aggrFcn)) if (! is_function_handle (aggrFcn)) errmsg = strcat ("'AggregationFunction' must be a", ... " function handle."); return; endif endif ## Create table containing unique instances of grouping variables, ## otherwise use unique instances of the indicator variable. Rows whose ## grouping variables contain missing values are excluded from unstacking, ## together with the corresponding indicator, data, and constant values, ## while the original row indices are retained for the returned index. ## A class whose row labels make a row distinct groups by them, so a ## timetable with no grouping variable still has one row per row time. if (! isempty (GvarTable) || uniqueIncludesLabels (this)) [GvarTable, rmRows] = rmmissing (GvarTable); validRows = ! rmRows; origIdx = find (validRows); IvarValues = IvarValues(validRows); VarsTable = subsetrows (VarsTable, origIdx); if (! isempty (CvarTable)) CvarTable = subsetrows (CvarTable, origIdx); endif [GvarTable, I, J] = unique (GvarTable, 'stable'); nrows = numel (I); rowIdx = origIdx(I); else [~, I, J] = __unique__ (IvarValues, 'stable', 'rows'); nrows = 1; rowIdx = 1; ## With no grouping variable every row collapses into one, and the ## grouping table is the empty half of that single row. GvarTable = subsetrows (GvarTable, 1:min (1, height (GvarTable))); endif ## Start unstacking here if (isscalar (ixVars)) # single variable to unstack ## Handle variable naming rule ncols = numel (newVarNames); if (strcmpi (rule, 'modify')) for i = 1:ncols if (! isvarname (newVarNames{i})) newVarNames{i} = matlab.lang.makeValidName (newVarNames{i}); endif endfor elseif (! strcmpi (rule, 'preserve')) errmsg = "invalid input for 'VariableNamingRule'."; return; endif ## Create table with unstacked variables vvals = VarsTable.VariableValues{:,:}; if (iscellstr (vvals)) vtype = 'cellstr'; else vtype = class (vvals); endif vtype = repmat ({vtype}, 1, ncols); UvarTable = table ('Size', [nrows, ncols], 'VariableTypes', vtype, ... 'VariableNames', newVarNames); ## Copy descriptions and units to unstacked variables if (! isempty (this.VariableDescriptions)) vd = this.VariableDescriptions{ixVars}; UvarTable.VariableDescriptions = repmat ({vd}, 1, ncols); endif if (! isempty (this.VariableUnits)) vu = this.VariableUnits{ixVars}; UvarTable.VariableUnits = repmat ({vu}, 1, ncols); endif ## Replicate the unstacked variable's variable-scoped custom properties ## onto each new column (MATLAB copies them); table-scoped properties ## are carried by the constant and grouping variables through the final ## horzcat that assembles the output. if (! isempty (this.CustomProperties)) cpNames = customPropsOfType (this, 'variable'); for ci = 1:numel (cpNames) srcval = this.CustomProperties.(cpNames{ci})(ixVars); UvarTable.CustomProperties.(cpNames{ci}) = ... repmat (srcval, 1, ncols); UvarTable.CustomPropTypes.(cpNames{ci}) = 'variable'; endfor endif ## Add type-specific NaN values and handle multicolumn variables ## Check that aggregation function returns suitable output [mcvec, aggrFcn] = tabular.get_default_aggrFcn (vvals, nrows, ... aggrFcn, scope); if (ischar (aggrFcn)) errmsg = aggrFcn; return; endif ## Process each unstacked variable for i = 1:ncols UvarTable.VariableValues{i} = mcvec; ix = strcmp (IvarNames{i}, IvarValues); if (nrows == 1) aggrVal = aggrFcn (rowsof (vvals, ix)); UvarTable.VariableValues{i} = aggrVal; CixRows = 1; else CixRows = []; for j = 1:nrows tmpIvarNames = IvarValues(J == j); ix = strcmp (IvarNames{i}, tmpIvarNames); if (any (ix)) aggrVec = ismember (tmpIvarNames, IvarNames{i}); aggrVal = aggrFcn (rowsof (rowsof (vvals, J == j), aggrVec)); UvarTable.VariableValues{i}(j,:) = aggrVal; endif CixRows = [CixRows, find(J == j, 1)]; endfor endif endfor ## Keep corresponding rows from ConstantVariables if (! isempty (CvarTable)) CvarTable = subsetrows (CvarTable, CixRows); endif else # multiple variables to unstack nvars = numel (ixVars); ncols = numel (newVarNames); expVarNames = cell (1, nvars * ncols); expVarTypes = expVarNames; ## Create composite variable names and get vartypes ij = 1; for i = 1:nvars vvals = VarsTable.VariableValues{i}; if (iscellstr (vvals)) vtype = 'cellstr'; else vtype = class (vvals); endif for j = 1:ncols expVarNames{ij} = sprintf ('%s_%s', VarsTable.VariableNames{i}, ... newVarNames{j}); expVarTypes{ij} = vtype; ij++; endfor endfor ## Handle variable naming rule if (strcmpi (rule, 'modify')) for i = 1:numel (expVarNames) if (! isvarname (expVarNames{i})) expVarNames{i} = matlab.lang.makeValidName (expVarNames{i}); endif endfor elseif (! strcmpi (rule, 'preserve')) errmsg = "invalid input for 'VariableNamingRule'."; return; endif ## Create table for each unstacked variable UvarTable = table ('Size', [nrows, ncols*nvars], ... 'VariableTypes', expVarTypes, ... 'VariableNames', expVarNames); ## Copy descriptions and units to unstacked variables VD = {}; VU = {}; for i = 1:nvars if (! isempty (this.VariableDescriptions)) vd = this.VariableDescriptions{ixVars(i)}; VD = [VD, repmat({vd}, 1, ncols)]; endif if (! isempty (this.VariableUnits)) vu = this.VariableUnits{ixVars(i)}; VU = [VU, repmat({vu}, 1, ncols)]; endif endfor if (! isempty (VD)) UvarTable.VariableDescriptions = VD; endif if (! isempty (VU)) UvarTable.VariableUnits = VU; endif ## Replicate each unstacked variable's variable-scoped custom properties ## onto its new columns (MATLAB copies them); table-scoped properties ## are carried by the constant and grouping variables through the final ## horzcat that assembles the output. if (! isempty (this.CustomProperties)) cpNames = customPropsOfType (this, 'variable'); for ci = 1:numel (cpNames) blk = []; for i = 1:nvars srcval = this.CustomProperties.(cpNames{ci})(ixVars(i)); blk = [blk, repmat(srcval, 1, ncols)]; endfor UvarTable.CustomProperties.(cpNames{ci}) = blk; UvarTable.CustomPropTypes.(cpNames{ci}) = 'variable'; endfor endif ## Process each separate variable to be unstacked vi = 1; for v = 1:nvars ## Get values of selected variable vvals = VarsTable.VariableValues{v}; ## Add type-specific NaN values and handle multicolumn variables. ## Resolve the aggregation per variable into THISAGGR so that the ## original AGGRFCN (or its default placeholder) is not overwritten ## between variables of different types. [mcvec, thisAggr] = tabular.get_default_aggrFcn (vvals, nrows, ... aggrFcn, scope); if (ischar (thisAggr)) errmsg = thisAggr; return; endif ## Process each unstacked variable for i = 1:ncols UvarTable.VariableValues{vi} = mcvec; ix = strcmp (IvarNames{i}, IvarValues); if (nrows == 1) aggrVal = thisAggr (rowsof (vvals, ix)); UvarTable.VariableValues{vi} = aggrVal; CixRows = 1; else CixRows = []; for j = 1:nrows tmpIvarNames = IvarValues(J == j); ix = strcmp (IvarNames{i}, tmpIvarNames); if (any (ix)) aggrVec = ismember (tmpIvarNames, IvarNames{i}); aggrVal = thisAggr (rowsof (rowsof (vvals, J == j), aggrVec)); UvarTable.VariableValues{vi}(j,:) = aggrVal; endif if (v == 1) CixRows = [CixRows, find(J == j, 1)]; endif endfor endif vi++; endfor endfor ## Keep corresponding rows from ConstantVariables if (! isempty (CvarTable)) CvarTable = subsetrows (CvarTable, CixRows); endif endif ## Merge output table and return index tbl = [GvarTable, CvarTable, UvarTable]; idxA = rowIdx; endfunction ## The body behind 'pivot' on both classes. Returns an errmsg body ## (empty on success) for the caller to raise under its own name. The ## result is a table whatever the input was, its rows and columns being ## groups rather than the rows they came from. function [P, errmsg] = pivotResult (T, args_in) P = []; errmsg = ''; scope = sprintf ('%s.pivot', class (T)); if (mod (numel (args_in), 2) != 0) errmsg = "name-value arguments must be in pairs."; return; endif ## Parse optional paired arguments optNames = {'Columns', 'Rows', 'DataVariable', 'Method', ... 'IncludeMissingGroups', 'IncludeEmptyGroups', ... 'IncludeTotals', 'RowLabelPlacement', 'OutputFormat', ... 'ColumnsBinMethod', 'RowsBinMethod', 'IncludedEdge'}; dfValues = {[], [], [], [], true, false, false, 'variable', 'flat', ... 'none', 'none', 'left'}; [colvars, rowvars, datavar, method, incMiss, incEmpty, incTot, ... rowPlace, outFmt, colBin, rowBin, incEdge, args] = ... parsePairedArguments (optNames, dfValues, args_in(:)); if (! isempty (args)) errmsg = "invalid optional paired argument."; return; endif ## Validate the IncludedEdge binning and OutputFormat options. incEdge = tabular.check_included_edge (scope, incEdge); if (isa (outFmt, 'string')) outFmt = char (outFmt); endif if (! (ischar (outFmt) && isrow (outFmt) ... && any (strcmpi (outFmt, {'flat', 'nested'})))) errmsg = "'OutputFormat' must be 'flat' or 'nested'."; return; endif nested = strcmpi (outFmt, 'nested'); ## Validate the logical-scalar and RowLabelPlacement options. incMiss = tabular.pivot_logical_opt ('IncludeMissingGroups', incMiss, ... class (T)); incEmpty = tabular.pivot_logical_opt ('IncludeEmptyGroups', incEmpty, ... class (T)); incTot = tabular.pivot_logical_opt ('IncludeTotals', incTot, ... class (T)); if (isa (rowPlace, 'string')) rowPlace = char (rowPlace); endif if (! (ischar (rowPlace) && isrow (rowPlace) ... && any (strcmpi (rowPlace, {'variable', 'rownames'})))) errmsg = strcat ("'RowLabelPlacement' must be 'variable'", ... " or 'rownames'."); return; endif rowPlace = lower (rowPlace); ## At least one grouping dimension is required. if (isempty (colvars) && isempty (rowvars)) errmsg = strcat ("specify at least one of 'Columns' or", ... " 'Rows'."); return; endif ## Resolve grouping and data variables. ## A class that groups by its row labels answers to their name here ## too, so a timetable pivots on its row times. IXVAR reports 0 for ## such a key, there being no variable behind it. colGcols = {}; colGnames = {}; colIx = []; if (! isempty (colvars)) [colGcols, colGnames, colIx] = keyColumns (T, colvars); endif rowGcols = {}; rowGnames = {}; rowIx = []; if (! isempty (rowvars)) [rowGcols, rowGnames, rowIx] = keyColumns (T, rowvars); endif hasDV = ! isempty (datavar); if (hasDV) dvIx = resolveVarRef (T, datavar)(:)'; if (! isscalar (dvIx)) errmsg = strcat ("'DataVariable' must specify a single", ... " variable."); return; endif dataVals = T.VariableValues{dvIx}; else dvIx = []; dataVals = []; endif ## Resolve the aggregation method and its default. reqDataMethods = {'sum', 'mean', 'median', 'mode', 'std', 'var', ... 'min', 'max', 'range', 'nnz'}; knownMethods = [{'count', 'percentage', 'nummissing', 'numunique', ... 'none'}, reqDataMethods]; if (isempty (method)) if (! hasDV || ! (isnumeric (dataVals) || islogical (dataVals))) method = 'count'; else method = 'sum'; endif elseif (isa (method, 'string') && isscalar (method)) method = char (method); endif if (ischar (method)) method = lower (method); if (! (isrow (method) && any (strcmp (method, knownMethods)))) errmsg = sprintf ("unknown Method '%s'.", method); return; endif if (! hasDV && (any (strcmp (method, reqDataMethods)) ... || strcmp (method, 'none'))) errmsg = sprintf ("Method '%s' requires a 'DataVariable'.", method); return; endif elseif (! is_function_handle (method)) errmsg = strcat ("'Method' must be a method name or a", ... " function handle."); return; endif isNone = ischar (method) && strcmp (method, 'none'); if (isNone && incTot) errmsg = strcat ("'IncludeTotals' is not supported with", ... " Method 'none'."); return; endif ## The display name of the method labels the single output variable when ## 'Columns' is omitted and the 'Overall_' totals row and column. if (ischar (method)) methodName = method; else methodName = tabular.apply_func_name (method); endif totalLabel = ['Overall_', methodName]; ## Bin the row/column grouping variables when a 'RowsBinMethod' or ## 'ColumnsBinMethod' was given (a per-variable cell or a single scheme). rowNone = ((ischar (rowBin) && isrow (rowBin)) ... || (isa (rowBin, 'string') && isscalar (rowBin))) ... && strcmpi (char (rowBin), 'none'); colNone = ((ischar (colBin) && isrow (colBin)) ... || (isa (colBin, 'string') && isscalar (colBin))) ... && strcmpi (char (colBin), 'none'); if (! rowNone && ! isempty (rowIx)) [rowGcols, rowGnames, emsg] = __groupbins__ ('bin', rowGcols, ... rowGnames, rowBin, incEdge, 'pivot'); if (! isempty (emsg)) errmsg = sprintf ("%s", emsg); return; endif endif if (! colNone && ! isempty (colIx)) [colGcols, colGnames, emsg] = __groupbins__ ('bin', colGcols, ... colGnames, colBin, incEdge, 'pivot'); if (! isempty (emsg)) errmsg = sprintf ("%s", emsg); return; endif endif ## Group the rows along each dimension. n = height (T); [rGid, nR, rLvlOf, rLevVals, ~, emsg] = ... tabular.pivot_dimension (rowGcols, n, incMiss, incEmpty); if (! isempty (emsg)) errmsg = sprintf ("%s", emsg); return; endif [cGid, nC, cLvlOf, cLevVals, cMissLvls, emsg] = ... tabular.pivot_dimension (colGcols, n, incMiss, incEmpty); if (! isempty (emsg)) errmsg = sprintf ("%s", emsg); return; endif assigned = ! isnan (rGid) & ! isnan (cGid); totalAssigned = sum (assigned); ## Build one output data column per column group. dataCols = cell (1, nC); for c = 1:nC if (isNone) [col, emsg] = tabular.pivot_none_column (dataVals, rGid, cGid, c, nR); if (! isempty (emsg)) errmsg = sprintf ("%s", emsg); return; endif else col = NaN (nR, 1); for r = 1:nR rows = find (rGid == r & cGid == c); [v, emsg] = ... tabular.pivot_cell_value (method, hasDV, dataVals, ... rows, totalAssigned); if (! isempty (emsg)) errmsg = sprintf ("%s", emsg); return; endif col(r) = v; endfor endif dataCols{c} = col; endfor ## Column variable names. With no 'Columns', a single variable named for ## the method ('_', or just 'count' with no data ## variable); otherwise one per column group, from the grouping values (a ## missing group becomes '>'). if (isempty (colIx)) if (hasDV) colNames = {[methodName, '_', T.VariableNames{dvIx}]}; else colNames = {methodName}; endif else colNames = cell (1, nC); for c = 1:nC parts = cell (1, numel (colIx)); for j = 1:numel (colIx) lvl = cLvlOf(c,j); if (cMissLvls{j}(lvl)) parts{j} = sprintf ("", colGnames{j}); else parts{j} = tabular.pivot_value_name (cLevVals{j}, lvl); endif endfor colNames{c} = strjoin (parts, '_'); endfor endif ## Row label columns (one per row grouping variable). if (isempty (rowIx)) rowLabelCols = {}; rowLabelNames = {}; else rowLabelCols = cell (1, numel (rowIx)); for j = 1:numel (rowIx) rowLabelCols{j} = rLevVals{j}(rLvlOf(:,j), :); endfor rowLabelNames = rowGnames; endif ## Marginal totals, recomputed over each margin from the raw data. addTotalCol = incTot && ! isempty (colIx); addTotalRow = incTot && ! isempty (rowIx); if (incTot) colMargin = NaN (nR, 1); for r = 1:nR rows = find (rGid == r & assigned); [colMargin(r), emsg] = ... tabular.pivot_cell_value (method, hasDV, dataVals, ... rows, totalAssigned); if (! isempty (emsg)) errmsg = sprintf ("%s", emsg); return; endif endfor rowMargin = NaN (1, nC); for c = 1:nC rows = find (cGid == c & assigned); [rowMargin(c), emsg] = ... tabular.pivot_cell_value (method, hasDV, dataVals, ... rows, totalAssigned); if (! isempty (emsg)) errmsg = sprintf ("%s", emsg); return; endif endfor rows = find (assigned); [grand, emsg] = ... tabular.pivot_cell_value (method, hasDV, dataVals, ... rows, totalAssigned); if (! isempty (emsg)) errmsg = sprintf ("%s", emsg); return; endif if (addTotalRow) for c = 1:nC dataCols{c} = [dataCols{c}; rowMargin(c)]; endfor endif if (addTotalCol) tcol = colMargin; if (addTotalRow) tcol = [tcol; grand]; endif dataCols{end+1} = tcol; colNames{end+1} = totalLabel; endif endif ## Column variables for the output: flat (the '_' data columns) ## or, for 'OutputFormat','nested' with two or more 'Columns' variables, ## nested tables grouped by the column hierarchy (the marginal-total ## column, if any, stays a flat outer variable). if (nested && numel (colIx) >= 2) [colVars, finalNames] = tabular.pivot_nest (dataCols(1:nC), cLvlOf, ... cLevVals, cMissLvls, colGnames); if (addTotalCol) colVars{end+1} = dataCols{nC + 1}; finalNames{end+1} = totalLabel; endif else colVars = dataCols; finalNames = colNames; endif ## Assemble the output table. Totals keep the row grouping variables and ## label the marginal row with 'Overall_', appended to the single ## row-label variable; a multi-variable or non-text row label falls back ## to row-name labelling instead. useRowNames = strcmp (rowPlace, 'rownames'); if (incTot && ! useRowNames && addTotalRow) if (numel (rowIx) == 1) [lc, ok] = tabular.pivot_append_label (rowLabelCols{1}, totalLabel); if (ok) rowLabelCols{1} = lc; else useRowNames = true; endif else useRowNames = true; endif endif if (useRowNames) if (isempty (rowIx)) P = table (colVars{:}, 'VariableNames', finalNames); else rn = tabular.pivot_row_names (rowLabelCols); if (addTotalRow) rn = [rn; {totalLabel}]; endif P = table (colVars{:}, 'VariableNames', finalNames, 'RowNames', rn); endif else allVars = [rowLabelCols, colVars]; allNames = [rowLabelNames, finalNames]; P = table (allVars{:}, 'VariableNames', allNames); endif endfunction ## The body behind 'inner2outer' on both classes. Returns an errmsg body ## (empty on success) for the caller to raise under its own name. The ## result is of the calling class, keeping its rows and their labels; only ## the nesting is turned inside out. function [tbl, errmsg] = inner2outerResult (this) errmsg = ''; ## Identify the variables that are themselves tables (nested tables) isNested = cellfun (@istable, this.VariableValues); ixNest = find (isNested); if (isempty (ixNest)) ## Nothing is nested, so there is nothing to turn inside out. tbl = this; return; endif ## The names of the nested-table variables become the variable names of ## the nested tables in the output. nestNames = this.VariableNames(ixNest); ## The union of the inner variable names (ordered by first appearance ## across the nested tables) becomes the outer variable names of the ## output. Nested tables need not share the same set of names. allNames = {}; for j = 1:numel (ixNest) allNames = [allNames, this.VariableValues{ixNest(j)}.VariableNames]; endfor innerNames = __unique__ (allNames, 'stable'); ## Build one outer variable per inner variable name. An inner name held ## by more than one nested table becomes a nested table grouping those ## source variables (named by the source nested-table variable names), ## inheriting each source variable's description and units. An inner name ## held by a single nested table becomes a plain variable carrying that ## column and its metadata. newVals = cell (1, numel (innerNames)); newTypes = cell (1, numel (innerNames)); newDesc = cell (1, numel (innerNames)); newUnits = cell (1, numel (innerNames)); for k = 1:numel (innerNames) srcJ = []; srcP = []; for j = 1:numel (ixNest) nt = this.VariableValues{ixNest(j)}; p = find (strcmp (nt.VariableNames, innerNames{k}), 1); if (! isempty (p)) srcJ(end+1) = j; srcP(end+1) = p; endif endfor if (numel (srcJ) == 1) nt = this.VariableValues{ixNest(srcJ)}; ntD = tabular.meta_or_blank (nt.VariableDescriptions, width (nt)); ntU = tabular.meta_or_blank (nt.VariableUnits, width (nt)); newVals{k} = nt.VariableValues{srcP}; newTypes{k} = nt.VariableTypes{srcP}; newDesc{k} = ntD{srcP}; newUnits{k} = ntU{srcP}; else cols = cell (1, numel (srcJ)); descs = cell (1, numel (srcJ)); units = cell (1, numel (srcJ)); for m = 1:numel (srcJ) nt = this.VariableValues{ixNest(srcJ(m))}; ntD = tabular.meta_or_blank (nt.VariableDescriptions, width (nt)); ntU = tabular.meta_or_blank (nt.VariableUnits, width (nt)); cols{m} = nt.VariableValues{srcP(m)}; descs{m} = ntD{srcP(m)}; units{m} = ntU{srcP(m)}; endfor nt2 = table (cols{:}, 'VariableNames', nestNames(srcJ)); if (any (! cellfun ("isempty", descs))) nt2.VariableDescriptions = descs; endif if (any (! cellfun ("isempty", units))) nt2.VariableUnits = units; endif newVals{k} = nt2; newTypes{k} = 'table'; newDesc{k} = ''; newUnits{k} = ''; endif endfor ## Assemble the output variable order: the new outer block sits at the ## position of the first nested variable, the other nested variables drop ## out, and the non-nested variables keep their relative position. outNames = {}; outVals = {}; outTypes = {}; outDesc = {}; outUnits = {}; thisD = tabular.meta_or_blank (this.VariableDescriptions, width (this)); thisU = tabular.meta_or_blank (this.VariableUnits, width (this)); emitted = false; for ix = 1:width (this) if (ismember (ix, ixNest)) if (! emitted) for k = 1:numel (innerNames) outNames{end+1} = innerNames{k}; outVals{end+1} = newVals{k}; outTypes{end+1} = newTypes{k}; outDesc{end+1} = newDesc{k}; outUnits{end+1} = newUnits{k}; endfor emitted = true; endif else outNames{end+1} = this.VariableNames{ix}; outVals{end+1} = this.VariableValues{ix}; outTypes{end+1} = this.VariableTypes{ix}; outDesc{end+1} = thisD{ix}; outUnits{end+1} = thisU{ix}; endif endfor ## A new outer variable name must not clash with a kept non-nested one. if (numel (__unique__ (outNames)) != numel (outNames)) errmsg = strcat ("an inner variable name clashes with an existing", ... " variable name in TBLA."); tbl = []; return; endif ## Build the output: preserve table-level metadata and row names; drop ## variable-scoped custom properties since the variable identities change. tbl = this; tbl.VariableNames = outNames; tbl.VariableValues = outVals; tbl.VariableTypes = outTypes; ## Leave them unset unless something was inherited, or the input had ## them set to blanks in the first place. if (! isempty (this.VariableDescriptions) || any (! cellfun ("isempty", outDesc))) tbl.VariableDescriptions = outDesc; endif if (! isempty (this.VariableUnits) || any (! cellfun ("isempty", outUnits))) tbl.VariableUnits = outUnits; endif if (! isempty (this.VariableContinuity)) ## The variables are not the ones that carried it. tbl.VariableContinuity = repmat ({'unset'}, 1, numel (outNames)); endif if (! isempty (this.CustomProperties)) cpNames = customPropsOfType (this, 'variable'); if (! isempty (cpNames)) tbl.CustomProperties = rmfield (tbl.CustomProperties, cpNames); tbl.CustomPropTypes = rmfield (tbl.CustomPropTypes, cpNames); endif endif endfunction ## The body behind 'rows2vars' on both classes. Returns an errmsg body ## (empty on success) for the caller to raise under its own name. The ## result is a table whatever the input was: its rows are the variables of ## the input and its columns the rows, so nothing is left in the order the ## row labels described. function [tbl, errmsg] = rows2varsResult (this, args_in) tbl = []; errmsg = ''; dimName = ''; if (mod (numel (args_in), 2) != 0) errmsg = "name-value arguments must be in pairs."; return; endif ## Parse optional paired arguments; an empty 'DataVariables' stands ## for every variable and an empty 'VariableNamesSource' names the ## new variables after the rows optNames = {'DataVariables', 'VariableNamesSource', 'VariableNamingRule'}; dfValues = {[], [], 'modify'}; [varRef, source, rule, args] = ... parsePairedArguments (optNames, dfValues, args_in(:)); if (! isempty (args)) errmsg = "invalid optional paired argument."; return; endif ## Check user input for 'DataVariables' if (! isempty (varRef)) ixVar = resolveVarRef (this, varRef, 'lenient'); if (any (ixVar == 0)) varRef = cellstr (varRef); bad = find (ixVar == 0); errmsg = sprintf (strcat ("'DataVariables' index a", ... " non-existing variable: '%s'"), ... varRef{bad(1)}); return; endif tbl = subsetvars (this, ixVar); else tbl = this; endif ## Check user input for 'VariableNamesSource' if (! isempty (source)) srcVar = resolveVarRef (this, source, 'lenient'); if (! isscalar (srcVar)) errmsg = strcat ("'VariableNamesSource' must index a single", ... " variable."); return; elseif (any (srcVar == 0)) source = cellstr (source); bad = find (srcVar == 0); errmsg = sprintf (strcat ("'VariableNamesSource' indexes a", ... " non-existing variable: '%s'"), ... source{bad(1)}); return; endif ## One name per row of the input, in the order the rows are in. ## Repeats are numbered below rather than refused: the source is an ## ordinary variable and nothing obliges it to hold distinct values. newVarNames = this.VariableValues{srcVar}; if (! iscellstr (newVarNames)) newVarNames = cellstr (string (newVarNames)); endif if (numel (newVarNames) != height (this)) errmsg = strcat ("the number of names taken from the variable", ... " specified in 'VariableNamesSource' does not", ... " match the number of rows in input table."); return; endif dimName = this.VariableNames{srcVar}; ## Check that 'VariableNamesSource' does not specify a variable ## that is specified by 'DataVariables', otherwise remove it from ## returning table if (! isempty (varRef)) if (ismember (srcVar, ixVar)) errmsg = strcat ("'VariableNamesSource' cannot specify a", ... " variable that is also specified by", ... " 'DataVariables'."); return; endif else tbl = removevars (tbl, srcVar); endif elseif (hasRowLabels (this)) ## The row labels name the new variables, rendered as the class ## displays them. A class whose labels are a dimension of their own ## lends that dimension's name to the result; a table's row names are ## not a dimension and lend nothing. newVarNames = rowLabelStrings (this); dimName = rowLabelHeader (this); else rows = height (tbl); newVarNames = cell (1, rows); for i = 1:rows newVarNames{i} = sprintf ("Var%d", i); endfor endif newVarNames = newVarNames(:)'; ## Handle variable naming rule if (strcmpi (rule, 'modify')) for i = 1:numel (newVarNames) if (! isvarname (newVarNames{i})) newVarNames{i} = matlab.lang.makeValidName (newVarNames{i}); endif endfor elseif (! strcmpi (rule, 'preserve')) errmsg = "invalid input for 'VariableNamingRule'."; return; endif ## Number a repeated name rather than refusing it. seen = struct (); for i = 1:numel (newVarNames) key = matlab.lang.makeValidName (newVarNames{i}); if (isfield (seen, key)) seen.(key)++; newVarNames{i} = sprintf ('%s_%d', newVarNames{i}, seen.(key) - 1); else seen.(key) = 1; endif endfor ## Check for multicolumn variables and nested tables for i = 1:width (tbl) if (isa (tbl.VariableValues{i}, 'table')) errmsg = 'input table must not contain nested tables.'; return; elseif (size (tbl.VariableValues{i}, 2) > 1) errmsg = 'the input must not contain multicolumn variables.'; return; endif endfor ## Each new variable is a row of the variables joined as 'table2array' ## joins them, and a cell array where they cannot join. [joined, ok] = tabular.join_vars (tbl.VariableValues); if (ok) new_var_values = cell (1, height (tbl)); for i = 1:height (tbl) col = joined(i,:); new_var_values{i} = col(:); endfor out = table (new_var_values{:}, 'VariableNames', newVarNames); else cols_as_cells = cell (1, width (tbl)); for i = 1:width (tbl) if (iscellstr (tbl.VariableValues{i})) cols_as_cells{i} = tbl.VariableValues{i}; elseif (iscell (tbl.VariableValues{i})) cols_as_cells{i} = tbl.VariableValues{i}; else cols_as_cells{i} = num2cell (tbl.VariableValues{i}); endif endfor matrix = cat (2, cols_as_cells{:})'; out = table (); for i = 1:height (tbl) tmp = table (matrix(:,i), 'VariableNames', newVarNames(i)); out = [out tmp]; endfor endif ## Merge original variable names into the table OriginalVariableNames = tbl.VariableNames(:); OriginalVariableNames = table (OriginalVariableNames); tbl = [OriginalVariableNames, out]; ## The output variables are new ones, carrying no description or units. tbl.VariableDescriptions = {}; tbl.VariableUnits = {}; ## Assign variable types in the new table new_types = cellfun ('class', tbl.VariableValues, 'UniformOutput', false); tbl.VariableTypes = new_types; ## The columns now stand for whatever named them. if (! isempty (dimName)) dn = tbl.DimensionNames; dn{2} = dimName; tbl.DimensionNames = dn; endif ## Remove any custom variable properties if (! isempty (tbl.CustomProperties)) cp_names = customPropsOfType (this, 'variable'); ## Remove custom variable properties only for i = 1:numel (cp_names) tbl = rmprop (tbl, cp_names{i}); endfor endif endfunction ## The body behind 'stack' on both classes. Returns an errmsg body ## (empty on success) for the caller to raise under its own name. The ## constant part is subset and repeated, which carries whatever row ## labels the class has, and the stacked part is laid beside it, so the ## result is of the calling class. function [tbl, idxA, errmsg] = stackResult (this, vars, args_in) tbl = []; idxA = []; errmsg = ''; ## Check input argument if (isempty (vars)) errmsg = 'too few input arguments.'; return; endif if (mod (numel (args_in), 2) != 0) errmsg = "name-value arguments must be in pairs."; return; endif ## Parse optional paired arguments; empty 'ConstantVariables', ## 'NewDataVariableName' and 'IndexVariableName' are resolved below optNames = {'ConstantVariables', 'NewDataVariableName', ... 'IndexVariableName'}; dfValues = {[], [], []}; [constVars, newVarName, idxVarName, args] = ... parsePairedArguments (optNames, dfValues, args_in(:)); if (! isempty (args)) errmsg = "invalid optional paired argument."; return; endif ## Determine single- vs multi-group stacking. Multiple groups of ## variables to stack are passed as a cell array of variable references ## (each a cellstr, string, numeric, or logical index), producing one ## stacked data variable per group; a single group is any other valid ## variable reference. isMulti = iscell (vars) && ! iscellstr (vars); if (isMulti) groups = vars; else groups = {vars}; endif nGroup = numel (groups); ## Resolve each group of variables to stack grpIx = cell (1, nGroup); grpNames = cell (1, nGroup); for g = 1:nGroup [ix, nm] = resolveVarRef (this, groups{g}, 'lenient'); if (any (ix == 0)) gv = cellstr (groups{g}); bad = find (ix == 0); errmsg = sprintf (strcat ("VARS index a non-existing", ... " variable: '%s'"), gv{bad(1)}); return; endif grpIx{g} = ix(:)'; grpNames{g} = nm; endfor ## All groups must contain the same number of variables grpSize = numel (grpIx{1}); if (any (cellfun (@numel, grpIx) != grpSize)) errmsg = strcat ("all groups of variables to stack must be", ... " the same size."); return; endif allStackIx = [grpIx{:}]; ## Get constant variables to include if (isempty (constVars)) cIxVars = setdiff (1:width (this), allStackIx); else cIxVars = resolveVarRef (this, constVars, 'lenient'); if (any (cIxVars == 0)) constVars = cellstr (constVars); bad = find (cIxVars == 0); errmsg = sprintf (strcat ("'ConstantVariables' index a", ... " non-existing variable: '%s'"), ... constVars{bad(1)}); return; endif if (any (ismember (cIxVars, allStackIx))) errmsg = strcat ("'ConstantVariables' cannot contain any", ... " variables to be stacked as specified by", ... " VARS."); return; endif endif ## Get new data variable name(s), one per group if (isempty (newVarName)) newVarName = cellfun (@(nm) strjoin (nm, '_'), grpNames, ... 'UniformOutput', false); else if (! ((ischar (newVarName) && isvector (newVarName)) || ((iscellstr (newVarName) || isa (newVarName, 'string')) && ! isempty (newVarName)))) errmsg = strcat ("'NewDataVariableName' must be a character", ... " vector, or a cellstring or string array."); return; endif newVarName = cellstr (newVarName); if (numel (newVarName) != nGroup) errmsg = strcat ("the number of 'NewDataVariableName' names", ... " must equal the number of variable groups to", ... " stack."); return; endif endif ## Get index (indicator) variable name if (isempty (idxVarName)) if (isMulti) idxVarName = 'Indicator'; else idxVarName = strcat (newVarName{1}, '_Indicator'); endif else if (! ((ischar (idxVarName) && isvector (idxVarName)) || ((iscellstr (idxVarName) || isa (idxVarName, 'string')) && isscalar (idxVarName)))) errmsg = strcat ("'IndexVariableName' must be either a", ... " character vector, or a cellstring or string", ... " scalar."); return; endif idxVarName = char (idxVarName); endif ## Handle the constant variables first; 'subsetvars' carries whatever ## row labels the class has, and 'repelem' repeats them. constTable = subsetvars (this, cIxVars); constTable = repelem (constTable, grpSize, 1); ## Build the indicator variable values. For a single group these are the ## categorical names of the stacked variables; for multiple groups they ## are the numeric position within each group, since the variable names ## differ between groups. nRow = height (this); if (isMulti) idVarValues = repmat ((1:grpSize)', nRow, 1); else idVarValues = repmat (categorical (grpNames{1})', nRow, 1); endif ## Build one stacked data column per group. The variables join as ## 'table2array' joins them, and a group that cannot is refused. ndCols = cell (1, nGroup); for g = 1:nGroup gvals = this.VariableValues(grpIx{g}); gnames = this.VariableNames(grpIx{g}); multi = find (cellfun (@(x) columns (x) != 1, gvals), 1); if (! isempty (multi)) errmsg = sprintf ("cannot stack the multicolumn variable '%s'.", ... gnames{multi}); return; endif [joined, ok] = tabular.join_vars (gvals); if (! ok) [i, j] = stack_bad_pair (gvals); errmsg = sprintf (strcat ("cannot stack the variables '%s' and", ... " '%s', because their types are %s and", ... " %s."), gnames{i}, gnames{j}, ... class (gvals{i}), class (gvals{j})); return; endif ndCols{g} = vec (joined'); endfor ## Assemble the stacked table (indicator followed by the data columns) stackVals = [{idVarValues}, ndCols]; stackNames = [{idxVarName}, newVarName]; stackedTable = table (stackVals{:}, 'VariableNames', stackNames); ## Inherit units and descriptions for the new data variables from the ## first variable of each group; the indicator carries a fixed ## description and no units. ndDescr = repmat ({''}, 1, nGroup); if (! isempty (this.VariableUnits)) ndUnits = cell (1, nGroup); for g = 1:nGroup ndUnits{g} = this.VariableUnits{grpIx{g}(1)}; endfor stackedTable.VariableUnits = [{''}, ndUnits]; endif if (! isempty (this.VariableDescriptions)) for g = 1:nGroup ndDescr{g} = this.VariableDescriptions{grpIx{g}(1)}; endfor endif stackedTable.VariableDescriptions = [{'Data indicator'}, ndDescr]; ## The indicator has no continuity of its own; each stacked variable ## takes the continuity of the first variable of its group. if (! isempty (this.VariableContinuity)) ndCont = cell (1, nGroup); for g = 1:nGroup ndCont{g} = this.VariableContinuity{grpIx{g}(1)}; endfor stackedTable.VariableContinuity = [{'unset'}, ndCont]; endif ## Merge tables tbl = [constTable, stackedTable]; ## Assign variable types in the new table new_types = cellfun ('class', tbl.VariableValues, 'UniformOutput', false); tbl.VariableTypes = new_types; ## The index of the input row each output row came from. idxA = repelem ((1:nRow)', grpSize, 1); endfunction ## The body behind 'groupfilter' on both classes. Returns an errmsg body ## (empty on success) for the caller to raise under its own name. The ## result is of the calling class: the rows that survive keep their order ## and whatever labels they carried. function [G, errmsg] = groupfilterResult (this, groupvars, args_in) G = []; errmsg = ''; scope = sprintf ('%s.groupfilter', class (this)); ## Split off a trailing 'IncludedEdge' Name-Value option, then an ## optional GROUPBINS positional argument that precedes the filter ## function METHOD. optNames = {'IncludedEdge'}; args = args_in; nvStart = numel (args) + 1; for k = 1:numel (args) a = args{k}; if (((ischar (a) && isrow (a)) || (isa (a, 'string') && isscalar (a))) && any (strcmpi (char (a), optNames))) nvStart = k; break; endif endfor nvArgs = args(nvStart:end); args = args(1:nvStart-1); incEdge = parsePairedArguments (optNames, {'left'}, nvArgs(:)); incEdge = tabular.check_included_edge (scope, incEdge); hasGroupbins = false; groupbins = []; if (! isempty (args) && __groupbins__ ('is_spec', args{1})) hasGroupbins = true; groupbins = args{1}; args = args(2:end); endif ## The filter function METHOD is the first remaining argument. if (isempty (args)) errmsg = 'a filter function METHOD is required.'; return; endif method = args{1}; if (! is_function_handle (method)) errmsg = 'METHOD must be a function handle.'; return; endif ## An optional DATAVARS argument may follow the filter function. rest = args(2:end); if (numel (rest) > 1) errmsg = 'too many positional arguments.'; return; endif if (numel (rest) == 1) datavars = rest{1}; hasDataVars = true; else datavars = []; hasDataVars = false; endif ## Resolve grouping and data variables. The default data variables are ## all variables that are not grouping variables. [gIx, byLabels] = resolveGroupRef (this, groupvars); if (isempty (gIx) && ! byLabels) errmsg = 'at least one grouping variable is required.'; return; endif if (hasDataVars) dIx = resolveVarRef (this, datavars)(:)'; else dIx = 1:width (this); dIx(ismember (dIx, gIx)) = []; endif ## Bin the grouping variables when a GROUPBINS argument was given, then ## group the rows, treating missing grouping values as their own groups ## so that every row belongs to exactly one group. The row labels group ## as a column of their own when they were named. grpCols = this.VariableValues(gIx); grpNames = this.VariableNames(gIx); if (byLabels) labels = getRowLabels (this); lname = rowLabelName (this); grpCols = [{labels}, grpCols]; grpNames = [{lname}, grpNames]; endif if (hasGroupbins) [grpCols, ~, errmsg] = __groupbins__ ('bin', grpCols, grpNames, ... groupbins, incEdge, ... 'groupfilter'); if (! isempty (errmsg)) return; endif endif [Grp, ng, ~, errmsg] = tabular.gs_group_rows (grpCols, true); if (! isempty (errmsg)) return; endif ## Build the row keep-mask by applying METHOD to each data variable. [keep, errmsg] = tabular.gf_keep_mask (method, ... this.VariableValues(dIx), Grp, ng); if (! isempty (errmsg)) return; endif G = subsetrows (this, find (keep)); endfunction ## The body behind 'groupcounts' on both classes. Returns an errmsg body ## (empty on success) for the caller to raise under its own name. The ## result is a table whatever the input was, the counts describing groups ## rather than rows, so there is nothing left for row labels to label. function [G, errmsg] = groupcountsResult (this, groupvars, args_in) G = []; errmsg = ''; scope = sprintf ('%s.groupcounts', class (this)); ## An optional GROUPBINS positional argument may precede the Name-Value ## options; anything else after GROUPVARS must be a recognised option. optNames = {'IncludeMissingGroups', 'IncludeEmptyGroups', ... 'IncludedEdge'}; hasGroupbins = false; groupbins = []; args = args_in; if (! isempty (args)) a = args{1}; isOpt = ((ischar (a) && isrow (a)) ... || (isa (a, 'string') && isscalar (a))) ... && any (strcmpi (char (a), optNames)); if (! isOpt) if (__groupbins__ ('is_spec', a)) hasGroupbins = true; groupbins = a; args = args(2:end); else errmsg = strcat ("invalid argument; expected a GROUPBINS", ... " binning scheme or a Name-Value option."); return; endif endif endif ## Parse Name-Value options. dfValues = {true, false, 'left'}; [incMiss, incEmpty, incEdge] = ... parsePairedArguments (optNames, dfValues, args(:)); if (! (isscalar (incMiss) && (islogical (incMiss) || isnumeric (incMiss)))) errmsg = "'IncludeMissingGroups' must be a logical scalar."; return; endif incMiss = logical (incMiss); if (! (isscalar (incEmpty) && (islogical (incEmpty) || isnumeric (incEmpty)))) errmsg = "'IncludeEmptyGroups' must be a logical scalar."; return; endif incEmpty = logical (incEmpty); incEdge = tabular.check_included_edge (scope, incEdge); ## Resolve grouping variables. The row labels count as a grouping key ## on a class that groups by them, named by the row dimension. [gIx, byLabels] = resolveGroupRef (this, groupvars); if (isempty (gIx) && ! byLabels) errmsg = 'at least one grouping variable is required.'; return; endif ## Bin the grouping variables when a GROUPBINS argument was given. grpCols = this.VariableValues(gIx); grpNames = this.VariableNames(gIx); if (byLabels) labels = getRowLabels (this); lname = rowLabelName (this); grpCols = [{labels}, grpCols]; grpNames = [{lname}, grpNames]; endif if (hasGroupbins) [grpCols, grpNames, errmsg] = __groupbins__ ('bin', grpCols, ... grpNames, groupbins, incEdge, ... 'groupcounts'); if (! isempty (errmsg)) return; endif endif ## Group the rows, treating missing grouping values as their own groups ## (sorted last) when IncludeMissingGroups is true; IncludeEmptyGroups ## adds the unused categories of a categorical or binned grouping ## variable as empty groups. [Grp, ng, gcols, errmsg] = tabular.gs_grouping (grpCols, incMiss, ... incEmpty); if (! isempty (errmsg)) return; endif gcount = accumarray (Grp(! isnan (Grp)), 1, [ng, 1]); pcent = 100 * gcount / sum (gcount); vars = [gcols, {gcount, pcent}]; names = [grpNames, {'GroupCount', 'Percent'}]; G = table (vars{:}, 'VariableNames', names); endfunction ## The body behind 'grouptransform' on both classes. Returns an errmsg ## body (empty on success) for the caller to raise under its own name. function [G, errmsg] = grouptransformResult (this, groupvars, args_in) G = []; errmsg = ''; scope = sprintf ('%s.grouptransform', class (this)); ## An optional GROUPBINS positional argument precedes the transform ## METHOD (a known method name or a function handle). args = args_in; hasGroupbins = false; groupbins = []; if (! isempty (args) && __groupbins__ ('is_spec', args{1})) hasGroupbins = true; groupbins = args{1}; args = args(2:end); endif if (isempty (args)) errmsg = 'a transform METHOD is required.'; return; endif method = args{1}; knownMethods = {'zscore', 'norm', 'meancenter', 'rescale', ... 'meanfill', 'linearfill'}; if (is_function_handle (method)) methDisp = 'fun1'; elseif (((ischar (method) && isrow (method)) || (isa (method, 'string') && isscalar (method))) && any (strcmpi (char (method), knownMethods))) method = lower (char (method)); methDisp = method; else errmsg = strcat ("METHOD must be one of 'zscore', 'norm',", ... " 'meancenter', 'rescale', 'meanfill',", ... " 'linearfill', or a function handle."); return; endif ## Split the remaining arguments into the optional positional DATAVARS ## and any Name-Value pairs (a Name-Value region starts at the first ## option). rest = args(2:end); optNames = {'ReplaceValues', 'IncludedEdge'}; nvStart = numel (rest) + 1; for k = 1:numel (rest) a = rest{k}; if (((ischar (a) && isrow (a)) || (isa (a, 'string') && isscalar (a))) && any (strcmpi (char (a), optNames))) nvStart = k; break; endif endfor posArgs = rest(1:nvStart-1); nvArgs = rest(nvStart:end); if (numel (posArgs) > 1) errmsg = 'too many positional arguments.'; return; endif if (numel (posArgs) == 1) datavars = posArgs{1}; hasDataVars = true; else datavars = []; hasDataVars = false; endif dfValues = {true, 'left'}; [replaceVals, incEdge] = ... parsePairedArguments (optNames, dfValues, nvArgs(:)); if (! (isscalar (replaceVals) && (islogical (replaceVals) || isnumeric (replaceVals)))) errmsg = "'ReplaceValues' must be a logical scalar."; return; endif replaceVals = logical (replaceVals); incEdge = tabular.check_included_edge (scope, incEdge); ## Resolve grouping and data variables. The default data variables are ## all variables that are not grouping variables. [gIx, byLabels] = resolveGroupRef (this, groupvars); if (isempty (gIx) && ! byLabels) errmsg = 'at least one grouping variable is required.'; return; endif if (hasDataVars) dIx = resolveVarRef (this, datavars)(:)'; else dIx = 1:width (this); dIx(ismember (dIx, gIx)) = []; endif ## Bin the grouping variables when a GROUPBINS argument was given, then ## group the rows, treating missing grouping values as their own groups ## so that every row belongs to exactly one group. The row labels group ## as a column of their own when they were named. grpCols = this.VariableValues(gIx); grpNames = this.VariableNames(gIx); if (byLabels) labels = getRowLabels (this); lname = rowLabelName (this); grpCols = [{labels}, grpCols]; grpNames = [{lname}, grpNames]; endif if (hasGroupbins) [grpCols, ~, errmsg] = __groupbins__ ('bin', grpCols, grpNames, ... groupbins, incEdge, ... 'grouptransform'); if (! isempty (errmsg)) return; endif endif [Grp, ng, ~, errmsg] = tabular.gs_group_rows (grpCols, true); if (! isempty (errmsg)) return; endif ## Transform each data variable, group by group. transCols = cell (1, numel (dIx)); for i = 1:numel (dIx) col = this.VariableValues{dIx(i)}; [tc, terr] = tabular.gt_transform_col (method, col, Grp, ng); if (! isempty (terr)) errmsg = sprintf ("variable '%s': %s", ... this.VariableNames{dIx(i)}, terr); return; endif transCols{i} = tc; endfor if (replaceVals) G = this; for i = 1:numel (dIx) G.VariableValues{dIx(i)} = transCols{i}; endfor else newNames = cell (1, numel (dIx)); for i = 1:numel (dIx) newNames{i} = sprintf ("%s_%s", methDisp, ... this.VariableNames{dIx(i)}); endfor G = addvars (this, transCols{:}, 'NewVariableNames', newNames); endif endfunction ## The body behind 'rowfun' on both classes. Returns an errmsg body ## (empty on success) for the caller to raise under its own name. function [B, errmsg] = rowfunResult (this, func, args_in) B = []; errmsg = ''; scope = sprintf ('%s.rowfun', class (this)); if (! is_function_handle (func)) errmsg = 'FUNC must be a function handle.'; return; endif if (mod (numel (args_in), 2) != 0) errmsg = "name-value arguments must be in pairs."; return; endif ## Parse optional paired arguments; empty 'InputVariables', ## 'GroupingVariables', 'OutputVariableNames' and 'NumOutputs' are ## resolved below, and an empty 'ErrorHandler' lets an error in FUNC ## through optNames = {'InputVariables', 'GroupingVariables', ... 'OutputVariableNames', 'NumOutputs', 'SeparateInputs', ... 'ExtractCellContents', 'OutputFormat', 'ErrorHandler'}; dfValues = {[], [], [], [], true, false, 'auto', []}; [inVars, grpVars, outNames, numOut, sepIn, extractCell, outFmt, ... errHandler, extra] = parsePairedArguments (optNames, dfValues, ... args_in(:)); ## Validate optional paired arguments outFmt = tabular.check_output_format (scope, outFmt, ... class (this)); if (! (isscalar (sepIn) && (islogical (sepIn) || isnumeric (sepIn)))) errmsg = "'SeparateInputs' must be a logical scalar."; return; endif sepIn = logical (sepIn); if (! (isscalar (extractCell) && (islogical (extractCell) || isnumeric (extractCell)))) errmsg = "'ExtractCellContents' must be a logical scalar."; return; endif extractCell = logical (extractCell); if (! isempty (errHandler) && ! is_function_handle (errHandler)) errmsg = "'ErrorHandler' must be a function handle."; return; endif if (! isempty (extra)) errmsg = "invalid optional paired argument."; return; endif ## Resolve grouping variables and input variables (default input is ## every variable that is not a grouping variable). byLabels = false; gIx = []; if (! isempty (grpVars)) [gIx, byLabels] = resolveGroupRef (this, grpVars); endif grouped = ! isempty (gIx) || byLabels; if (isempty (inVars)) iIx = 1:width (this); iIx(ismember (iIx, gIx)) = []; else iIx = resolveVarRef (this, inVars)(:)'; endif if (isempty (iIx)) errmsg = 'there are no variables to which to apply FUNC.'; return; endif ## Determine the number of outputs requested from FUNC. if (! isempty (numOut)) if (! (isnumeric (numOut) && isscalar (numOut) && numOut >= 0 && numOut == fix (numOut))) errmsg = "'NumOutputs' must be a nonnegative integer."; return; endif nout = numOut; if (! isempty (outNames) && numel (cellstr (outNames)) != nout) errmsg = strcat ("the number of 'OutputVariableNames' must", ... " equal 'NumOutputs'."); return; endif elseif (! isempty (outNames)) nout = numel (cellstr (outNames)); else nout = 1; endif ## Build the output variable names. Default names are 'Var'; for ## grouped output the numbering continues past the grouping variables ## and the GroupCount column, so the first result is 'Var'. if (isempty (outNames)) if (grouped) base = numel (gIx) + 1; else base = 0; endif resNames = arrayfun (@(k) sprintf ("Var%d", base + k), 1:nout, ... "UniformOutput", false); else resNames = cellstr (outNames)(:)'; endif inCols = this.VariableValues(iIx); ## A row laid side by side into one argument joins as 'table2array' ## joins the variables, but where they cannot form one array it is ## refused rather than given as a cell array. The types do not change ## row by row, so it is asked once. if (! sepIn) [~, ok] = tabular.join_vars (inCols); if (! ok) inNames = this.VariableNames(iIx); errmsg = "cannot concatenate the specified table variables."; for j = 2:numel (inCols) for i = 1:j-1 [~, ok] = tabular.join_vars (inCols([i, j])); if (! ok) errmsg = sprintf (strcat ("cannot concatenate the table", ... " variables '%s' and '%s',", ... " because their types are %s", ... " and %s."), inNames{i}, ... inNames{j}, class (inCols{i}), ... class (inCols{j})); return; endif endfor endfor return; endif endif if (! grouped) ## Ungrouped: apply FUNC to each row. The result maps the input row ## for row, so it carries the labels of the rows it was built from. n = height (this); res = cell (n, max (nout, 1)); for r = 1:n rows = false (n, 1); rows(r) = true; args = tabular.build_row_args (inCols, rows, sepIn, extractCell); res(r,:) = tabular.apply_func (func, errHandler, r, nout, args); ## An ungrouped 'rowfun' answers one row with one row, so a ## function returning more leaves the result with rows the input ## has nothing to match them to. for c = 1:max (nout, 1) if (size (res{r,c}, 1) != 1) errmsg = sprintf (strcat ("the function returned %d rows for", ... " row %d; an ungrouped 'rowfun' takes one", ... " row and returns one."), ... size (res{r,c}, 1), r); return; endif endfor endfor labels = {}; if (hasRowLabels (this)) labels = getRowLabels (this); endif B = build_apply_result (this, scope, outFmt, res(:,1:nout), ... resNames, {}, {}, [], labels, (1:n)'); else ## Grouped: apply FUNC to the rows of each group. grpCols = this.VariableValues(gIx); if (byLabels) labels = getRowLabels (this); grpCols = [{labels}, grpCols]; endif [G, ng, repRows, gerr] = tabular.group_table_rows (grpCols); if (! isempty (gerr)) errmsg = gerr; return; endif res = cell (ng, max (nout, 1)); for g = 1:ng rows = (G == g); args = tabular.build_row_args (inCols, rows, sepIn, extractCell); res(g,:) = tabular.apply_func (func, errHandler, g, nout, args); endfor [gcols, gcount] = tabular.group_output_cols ( ... this.VariableValues(gIx), G, repRows); B = build_grouped_apply_result (this, scope, outFmt, ... res(:,1:nout), resNames, gcols, ... this.VariableNames(gIx), gcount, ... repRows, G); endif endfunction ## Assemble the output of an apply method from the R-by-C cell array of ## per-row (or per-group) results RES with output names OUTNAMES. For ## grouped output the grouping columns GCOLS (named GNAMES) and the GCOUNT ## counts are prepended; for ungrouped output these are empty. FMT selects ## the 'table', 'uniform', or 'cell' return format; CALLER names the method ## for error messages. function out = build_apply_result (this, caller, fmt, res, outNames, ... gcols, gnames, gcount, rowLabels, rowIx) if (nargin < 9) rowLabels = {}; endif if (nargin < 10) rowIx = []; endif C = size (res, 2); switch (fmt) case {'table', 'plaintable'} rescols = cell (1, C); for c = 1:C rescols{c} = vertcat (res{:,c}); endfor if (isempty (gcols) && isempty (gcount)) vars = rescols; names = outNames; else vars = [gcols, {gcount}, rescols]; names = [gnames, {'GroupCount'}, outNames]; endif ## An index that does not describe the rows just built cannot ## label them; the class falls back on what it does for a result ## with no index at all. if (! isempty (rowIx) && numel (rowIx) != size (rescols{1}, 1)) rowIx = []; endif if (strcmp (fmt, 'plaintable')) out = table (vars{:}, 'VariableNames', names); else out = assembleApply (this, vars, names, rowLabels, ... rowIx, caller); endif case 'uniform' out = []; for c = 1:C colvals = res(:,c); if (! all (cellfun (@isscalar, colvals))) error (strcat ("%s: OutputFormat 'uniform' requires FUNC", ... " to return a scalar for each call."), caller); endif out = [out, vertcat(colvals{:})]; endfor case 'cell' out = res; endswitch endfunction ## Assemble the output of a grouped 'rowfun' or 'varfun' from the NG-by-C ## cell array of per-group results RES. Unlike an aggregating apply, FUNC ## may return several rows for a group; each group g therefore contributes ## 'size (RES{g,1}, 1)' rows and the grouping columns GCOLS (named GNAMES) ## and the GCOUNT counts are replicated to match before the per-group ## results are stacked. FMT selects the 'table', 'uniform', or 'cell' ## return format; CALLER names the method for error messages. function out = build_grouped_apply_result (this, caller, fmt, res, ... outNames, gcols, gnames, ... gcount, rowIx, gvec) if (nargin < 9) rowIx = []; endif if (nargin < 10) gvec = []; endif ng = size (res, 1); C = size (res, 2); switch (fmt) case {'table', 'plaintable'} repIdx = []; for g = 1:ng repIdx = [repIdx; repmat(g, size (res{g,1}, 1), 1)]; endfor rescols = cell (1, C); for c = 1:C rescols{c} = vertcat (res{:,c}); endfor gcolsR = cell (1, numel (gcols)); for p = 1:numel (gcols) gcolsR{p} = gcols{p}(repIdx,:); endfor vars = [gcolsR, {gcount(repIdx)}, rescols]; names = [gnames, {'GroupCount'}, outNames]; if (isempty (rowIx)) outIx = []; elseif (isempty (gvec)) outIx = rowIx(repIdx); else ## Each output row answers for a row of the group it came from, ## taken from the top as the ungrouped path takes them: a group ## reduced to one row answers for its first, and one that keeps ## its rows answers for each in turn. Only a group returning ## more rows than it holds runs out, and the last of them stands ## in for the rest. outIx = []; for g = 1:ng src = find (gvec == g); k = size (res{g,1}, 1); take = min (k, numel (src)); ix = src(1:take); if (k > take) ## The group has fewer rows than the function returned, so ## these output rows answer for no row of it. The class ## decides what that means: a timetable has no row time for ## them, a table does not label its rows by them at all. ix = [ix; NaN(k - take, 1)]; endif outIx = [outIx; ix]; endfor endif if (strcmp (fmt, 'plaintable')) out = table (vars{:}, 'VariableNames', names); else out = assembleApply (this, vars, names, {}, outIx, caller); endif case 'uniform' out = []; for c = 1:C colvals = res(:,c); if (! all (cellfun (@isscalar, colvals))) error (strcat ("%s: OutputFormat 'uniform' requires FUNC", ... " to return a scalar for each call."), caller); endif out = [out, vertcat(colvals{:})]; endfor case 'cell' out = res; endswitch endfunction function names = customPropsOfType (this, type) names = {}; if (isempty (this.CustomProperties)) return; endif names = fieldnames (this.CustomProperties); keep = cellfun (@(n) strcmp (this.CustomPropTypes.(n), type), names); names = names(keep); endfunction ## The key columns a join reference names, in the order it names them. ## A class that groups by its row labels answers to their name here too, ## and contributes the labels themselves as a key column, for which IXVAR ## reports 0, there being no variable to exclude from the result. function [cols, names, ixVar] = keyColumns (this, ref) isText = ischar (ref) || iscellstr (ref) || isa (ref, 'string'); if (! isText) ix = resolveVarRef (this, ref); ixVar = ix(:)'; cols = this.VariableValues(ixVar); names = this.VariableNames(ixVar); return; endif if (ischar (ref)) want = {ref}; else want = cellstr (ref); endif want = want(:)'; if (groupsByLabels (this)) keys = rowLabelKeyNames (this); else keys = {}; endif cols = cell (1, numel (want)); ixVar = zeros (1, numel (want)); names = want; for k = 1:numel (want) if (any (strcmp (want{k}, keys))) cols{k} = getRowLabels (this); else ix = resolveVarRef (this, want(k)); ixVar(k) = ix; cols{k} = this.VariableValues{ix}; endif endfor endfunction ## Resolve a 'GroupingVariables' reference. IXVAR indexes the variables ## named and BYLABELS says the row labels were named too, which only a ## class that groups by them answers to, under any of its label key ## names. The rows are then grouped by the label column followed by ## those variables, while only the variables are reported back as ## grouping variables of the result. function [ixVar, byLabels] = resolveGroupRef (this, grpRef) byLabels = false; isText = ischar (grpRef) || iscellstr (grpRef) ... || isa (grpRef, 'string'); if (groupsByLabels (this) && isText) if (ischar (grpRef)) names = {grpRef}; else names = cellstr (grpRef); endif names = names(:)'; keys = rowLabelKeyNames (this); isLabel = ismember (names, keys); byLabels = any (isLabel); grpRef = names(! isLabel); endif if (byLabels && isempty (grpRef)) ixVar = zeros (1, 0); return; endif ixVar = resolveVarRef (this, grpRef); ixVar = ixVar(:)'; endfunction ## Resolve variable references to indices and variable names. ## Returns: ## @var{ixVar} - numeric indices of the variables in @var{tbl} ## @var{varNames} - a cellstr of the names of the indexed variables ## ## Raises an error if any of the specified variables could not be resolved, ## unless strictness is 'lenient', in which case it will return 0 for the ## index and '' for the name for each variable which could not be resolved. function [ixVar, varNames] = resolveVarRef (this, varRef, strictness) clstype = class (this); if (nargin < 3 || isempty (strictness)) strictness = 'strict'; endif if ((isnumeric (varRef) || islogical (varRef)) && isempty (varRef)) ## An empty numeric or logical subscript selects no variables, as it ## does on any array. An empty cell is not an index at all and is ## refused below, which is what indexing an ordinary array with one ## does. ixVar = zeros (1, 0); if (nargout > 1) varNames = cell (1, 0); endif return; endif if (! isvector (varRef)) error ("%s: variable index must be a vector.", clstype); endif nvars = width (this); if (islogical (varRef)) vec = numel (varRef); if (nvars != vec) error ("%s: variable logical index does not match %s width.", ... clstype, clstype); endif ixVar = 1:nvars; ixVar(! varRef) = []; elseif (isnumeric (varRef)) ixVar = varRef; ix_bad = find (ixVar > nvars | ixVar < 1); if (! isempty (ix_bad)) error (strcat ("%s: variable index out of bounds: requested", ... " index %d; %s has %d variables."), ... clstype, ixVar(ix_bad(1)), clstype, nvars); endif elseif (ischar (varRef) && isequal (varRef, ':')) ixVar = 1:nvars; elseif (ischar (varRef) || iscellstr (varRef) || isa (varRef, 'string')) varRef = cellstr (varRef); [tf, ixVar] = ismember (varRef, this.VariableNames); if (isequal (strictness, 'strict')) if (! all (tf)) if (sum (! tf) == 1) error ("%s: no such variable in %s: '%s'", ... clstype, clstype, varRef{! tf}); else missing_vars = strjoin (varRef(! tf), ', '); error ("%s: no such variables in %s: '%s'", ... clstype, clstype, missing_vars); endif endif else ixVar(! tf) = 0; endif elseif (isa (varRef, 'vartype')) ixVar = []; for i = 1:nvars if (varRef.varMatch (this.VariableValues{i})) ixVar(end+1) = i; endif endfor elseif (is_function_handle (varRef)) ixVar = []; for i = 1:nvars if (varRef (this.VariableValues{i})) ixVar(end+1) = i; endif endfor else error ("%s: unsupported variable indexing operand type: '%s'", ... clstype, class (varRef)); endif if (nargout > 1) varNames = repmat ({''}, size (ixVar)); varNames(ixVar != 0) = this.VariableNames(ixVar(ixVar != 0)); endif endfunction ## Resolve the variable reference of an assignment, which may name ## variables to create. A name the object does not carry, an index past ## the last variable and a logical mask longer than the width all name ## new ones, which take the indices after the last variable in the order ## they are named. An index leaving a gap is refused, as in MATLAB. ## NEWNAMES holds the names of the variables to create, in index order, ## a new index taking the default name for its place. function [ixVar, newNames] = resolveAssignVarRef (this, varRef) clstype = class (this); nvars = width (this); newNames = cell (1, 0); if (islogical (varRef) && isvector (varRef) && numel (varRef) > nvars) varRef = find (varRef); endif if (isnumeric (varRef) && isvector (varRef) && ! isempty (varRef)) ixVar = varRef(:)'; if (any (ixVar < 1 | fix (ixVar) != ixVar)) ixVar = resolveVarRef (this, varRef); return; endif ixNew = unique (ixVar(ixVar > nvars))(:)'; if (! isequal (ixNew, nvars + (1:numel (ixNew)))) error ("%s: cannot create a variable with a discontiguous index.", ... clstype); endif for k = ixNew name = sprintf ('Var%d', k); while (any (strcmp (name, [this.VariableNames, newNames, ... this.DimensionNames]))) name = [name, '_1']; endwhile newNames{end+1} = name; endfor elseif (((ischar (varRef) && ! isequal (varRef, ':')) || iscellstr (varRef) || isa (varRef, 'string')) && isvector (cellstr (varRef))) names = cellstr (varRef); [tf, ixVar] = ismember (names, this.VariableNames); ixVar = ixVar(:)'; for k = find (! tf(:)') j = find (strcmp (names{k}, newNames), 1); if (isempty (j)) if (any (strcmp (names{k}, this.DimensionNames))) error ("%s: duplicate dimension and variable name: '%s'.", ... clstype, names{k}); endif newNames{end+1} = names{k}; j = numel (newNames); endif ixVar(k) = nvars + j; endfor else ixVar = resolveVarRef (this, varRef); endif endfunction ## Resolve both row and variable references to indices. function [ixRow, ixVar, newLabels] = resolveRowVarRefs (this, rowRef, ... varRef) clstype = class (this); newLabels = []; ## A caller that asks for NEWLABELS is assigning, and takes the labels ## the object does not carry so that it can add the rows they name. ## One that does not is reading, and the class says whether a label it ## does not carry is nothing or is an error. wantNew = nargout > 2; if (isnumeric (rowRef) || islogical (rowRef)) ixRow = rowRef; elseif ((ischar (rowRef) || isa (rowRef, 'string')) && isequal (rowRef, ':')) ## The type is checked before the comparison: 'isequal' against a ## row time would dispatch to the row time's own, which reads the ## colon as a time string and fails on it. ixRow = 1:height (this); elseif (ischar (rowRef) || iscellstr (rowRef) || isa (rowRef, 'string')) rowRef = cellstr (rowRef); if (wantNew) [ixRow, newLabels] = resolveRowRef (this, rowRef); else ixRow = resolveRowRef (this, rowRef); endif elseif (isa (rowRef, 'rowfilter')) ## A filter is a condition on the variables and reads no row labels, ## so it selects from any tabular class the same way. ixRow = rowIndices (rowRef, this); elseif (isa (rowRef, 'eventfilter')) ## An event filter is a condition on the attached event table, and ## selects the rows its matching events cover. ixRow = rowIndices (rowRef, this); else ## Anything else is a row reference of a kind only the subclass can ## read: a row time, a range of them, a tolerant match. A class that ## does not take the kind offered refuses it in its own hook. if (wantNew) [ixRow, newLabels] = resolveRowRef (this, rowRef); else ixRow = resolveRowRef (this, rowRef); endif endif ixVar = resolveVarRef (this, varRef); endfunction ## Return a subset of rows defined by the numerical or logical vector ixRows function tbl = subsetrows (this, ixRows) tbl = this; if (width (this) == 0) ## With no variable to index, nothing would raise on an out-of-range ## row and nothing would carry the new count, so both happen here. ixRows = validateRowIndex (this, ixRows); tbl.RowCount = numel (ixRows); endif s = struct ('type', '()', 'subs', {{ixRows,':'}}); for i = 1:width (this) tbl.VariableValues{i} = subsref (tbl.VariableValues{i}, s); endfor tbl = subsetRowLabels (tbl, ixRows); endfunction ## Keep the stored row count normalised: an object carries it only while ## it has no variable to carry the height for it, and carries zero ## otherwise. Without that, two objects equal in every observable way ## could still differ in this, and 'isequal' compares it. function tbl = setRowCount (this, nrows) tbl = this; if (width (this) == 0) tbl.RowCount = nrows; else tbl.RowCount = 0; endif endfunction ## Resolve a row subscript to a validated vector of row indices. The ## variables normally raise on a bad row when they are indexed; this is ## for the paths where there is no variable left to do it. function ixRows = validateRowIndex (this, ixRows) clstype = class (this); nrows = height (this); if (ischar (ixRows) && isequal (ixRows, ':')) ixRows = 1:nrows; elseif (islogical (ixRows)) if (numel (ixRows) > nrows) error ("%s: row logical index does not match %s height.", ... clstype, clstype); endif ixRows = find (ixRows(:))'; elseif (isnumeric (ixRows)) ixRows = ixRows(:)'; if (any (ixRows < 1 | ixRows != fix (ixRows))) error ("%s: row index must be a positive integer.", clstype); endif ix_bad = find (ixRows > nrows, 1); if (! isempty (ix_bad)) error (strcat ("%s: row index out of bounds: requested index", ... " %d; %s has %d rows."), clstype, ... ixRows(ix_bad), clstype, nrows); endif endif endfunction ## Delete rows or variables, which is what assigning [] means. One ## subscript must be ':' and the other names what goes; with both of ## them ':' the rows go. function tbl = deleteSubs (this, rowRef, varRef) rowColon = is_colon_ref (rowRef); varColon = is_colon_ref (varRef); if (! rowColon && ! varColon) error (strcat ("%s.subsasgn: deleting rows or variables by", ... " assigning [] requires one subscript to be ':'."), ... class (this)); endif if (rowColon && ! varColon) tbl = deleteVars (this, varRef); else tbl = deleteRows (this, rowRef); endif endfunction ## Delete the referenced variables. An empty reference removes nothing. function tbl = deleteVars (this, varRef) if (isempty (varRef)) tbl = this; return; endif ixVar = resolveVarRef (this, varRef); tbl = subsetvars (this, setdiff (1:width (this), ixVar(:)')); endfunction ## Delete the referenced rows. An empty reference removes nothing. function tbl = deleteRows (this, rowRef) if (isempty (rowRef) && ! is_colon_ref (rowRef)) tbl = this; return; endif ixRow = resolveRowVarRefs (this, rowRef, ':'); ixRow = validateRowIndex (this, ixRow); tbl = subsetrows (this, setdiff (1:height (this), ixRow)); endfunction ## Build consistent numeric row proxies for two tables sharing the same set ## of variable names, so that equal rows (compared by variable value, in the ## variable order of TBLA) map to equal proxy rows. Returns an errmsg body ## (empty on success) emitted by the caller under its own name. function [proxyA, proxyB, errmsg] = rowProxies (tblA, tblB) proxyA = []; proxyB = []; errmsg = ''; if (width (tblA) != width (tblB) || ! isempty (setdiff (tblA.VariableNames, tblB.VariableNames))) errmsg = "the two tables must have the same variable names."; return; endif for ix = 1:width (tblA) jx = find (strcmp (tblA.VariableNames{ix}, tblB.VariableNames), 1); [pa, pb, e] = tabular.key_col_proxy (tblA.VariableValues{ix}, ... tblB.VariableValues{jx}); if (! isempty (e)) errmsg = e; return; endif proxyA = [proxyA, pa]; proxyB = [proxyB, pb]; endfor endfunction ## Build one side of an outer join from a row-index vector IDX (zeros mark ## rows with no match, filled with missing values). Returns an errmsg body ## (empty on success) emitted by the caller under its own name. ## The body behind 'addprop' on both classes. NAMES and TYPES are the ## caller's arguments, TYPES being 'table' or 'variable' for each. A ## custom property is stored the same way whatever labels the rows, so ## only the name a complaint is raised under differs between them. ## Returns an errmsg body for the caller to raise under its own name. function [tbl, errmsg] = addpropResult (this, Names, Types) tbl = this; errmsg = ''; if (! (any (isa (Names, {'string', 'char'})) || iscellstr (Names))) errmsg = "invalid input type for 'propertyNames'."; return elseif (! (any (isa (Types, {'string', 'char'})) || iscellstr (Types))) errmsg = "invalid input type for 'propertyTypes'."; return endif Names = cellstr (Names); Types = cellstr (Types); if (numel (Names) != numel (Types)) errmsg = strcat ("the number of 'propertyTypes' must equal the", ... " number of 'propertyNames'."); return endif if (numel (unique (Names)) != numel (Names)) errmsg = "'propertyNames' cannot contain duplicate names."; return endif if (! isempty (this.CustomProperties)) existingNames = fieldnames (this.CustomProperties); idx = ismember (Names, existingNames); if (any (idx)) errmsg = sprintf ("custom property '%s' already exists.", ... Names{find (idx)(1)}); return endif endif for idx = 1:numel (Names) if (! isvarname (Names{idx})) errmsg = sprintf (strcat ("custom property '%s' does not have", ... " a valid name."), Names{idx}); return endif if (! any (strcmp (Types{idx}, {'table', 'variable'}))) errmsg = "invalid value for 'propertyTypes'."; return endif this.CustomProperties.(Names{idx}) = []; this.CustomPropTypes.(Names{idx}) = Types{idx}; endfor tbl = this; endfunction ## The body behind 'rmprop' on both classes. A name matching no custom ## property is ignored rather than refused, repeated names included, as ## MATLAB ignores it. Returns an errmsg body for the caller to raise. function [tbl, errmsg] = rmpropResult (this, Names) tbl = this; errmsg = ''; if (! (any (isa (Names, {'string', 'char'})) || iscellstr (Names))) errmsg = "invalid input type for 'propertyNames'."; return endif Names = cellstr (Names); if (! isempty (this.CustomProperties)) existingNames = fieldnames (this.CustomProperties); tf = ismember (existingNames, Names); if (any (tf)) this.CustomProperties = rmfield (this.CustomProperties, ... existingNames(tf)); this.CustomPropTypes = rmfield (this.CustomPropTypes, ... existingNames(tf)); endif endif tbl = this; endfunction function [out, errmsg] = joinBuildSide (this, idx) out = this; errmsg = ''; nout = numel (idx); pos = (idx > 0); for j = 1:width (this) p = this.VariableValues{j}; if (any (pos)) src = idx; src(! pos) = idx(find (pos, 1)); col = rowsof (p, src); [col, errmsg] = set_var_missing (col, ! pos); else [col, errmsg] = missing_rows (p, nout); endif if (! isempty (errmsg)) return; endif out.VariableValues{j} = col; endfor ## Labels that are a dimension of their own follow the rows they came ## from, and a row that came from none carries a missing label; a ## table's row names are dropped, the rows being pairs rather than the ## rows they came from. if (isempty (rowLabelHeader (this)) || ! any (pos)) out = clearRowLabels (out); else src = idx; src(! pos) = idx(find (pos, 1)); labels = getRowLabels (this); lab = labels(src); [lab, errmsg] = set_var_missing (lab, ! pos); if (! isempty (errmsg)) return; endif out = setRowLabels (out, lab); endif endfunction ## Merge the custom properties of a set of horizontally-combined tables ## (a cell array TABLES whose variables are concatenated in order). Table- ## scoped properties are unioned with the first table winning on a name ## clash; variable-scoped properties are concatenated across the tables' ## variable blocks, filling the block of any table lacking the property with ## NaN (numeric) or an empty cell. Table-scoped properties are listed ## before variable-scoped ones, matching MATLAB. function [cp, cpTypes] = merge_hcat_props (this, tables) widths = cellfun (@width, tables); cp = struct (); cpTypes = struct (); ## Pass 1: table-scoped properties (union, first table wins). for t = 1:numel (tables) T = tables{t}; if (isempty (T.CustomProperties)) continue; endif nm = fieldnames (T.CustomProperties); for i = 1:numel (nm) if (strcmp (T.CustomPropTypes.(nm{i}), 'table') ... && ! isfield (cp, nm{i})) cp.(nm{i}) = T.CustomProperties.(nm{i}); cpTypes.(nm{i}) = 'table'; endif endfor endfor ## Pass 2: variable-scoped properties (union of names; per-table blocks ## concatenated, missing blocks filled to match the property's variables). seen = {}; for t = 1:numel (tables) T = tables{t}; if (isempty (T.CustomProperties)) continue; endif nm = fieldnames (T.CustomProperties); for i = 1:numel (nm) if (! strcmp (T.CustomPropTypes.(nm{i}), 'variable') ... || any (strcmp (nm{i}, seen))) continue; endif seen{end+1} = nm{i}; proto = T.CustomProperties.(nm{i}); vec = []; for tt = 1:numel (tables) Tt = tables{tt}; if (! isempty (Tt.CustomProperties) ... && isfield (Tt.CustomProperties, nm{i})) blk = reshape (Tt.CustomProperties.(nm{i}), 1, []); elseif (iscell (proto)) blk = cell (1, widths(tt)); else blk = NaN (1, widths(tt)); endif vec = [vec, blk]; endfor cp.(nm{i}) = vec; cpTypes.(nm{i}) = 'variable'; endfor endfor if (isempty (fieldnames (cp))) cp = []; cpTypes = struct (); endif endfunction ## Return a subset of variables defined by the numerical vector ixVars function tbl = subsetvars (this, ixVars) nrows = height (this); tbl = this; ## Copy selected variables tbl.VariableTypes = this.VariableTypes(ixVars); tbl.VariableNames = this.VariableNames(ixVars); tbl.VariableValues = this.VariableValues(ixVars); tbl.VariableDescriptions = tabular.subset_meta ( ... this.VariableDescriptions, ixVars); tbl.VariableUnits = tabular.subset_meta (this.VariableUnits, ixVars); if (! isempty (this.VariableContinuity)) tbl.VariableContinuity = this.VariableContinuity(ixVars); endif ## Check for custom variable properties if (! isempty (this.CustomProperties)) cpNames = customPropsOfType (this, 'variable'); if (! isempty (cpNames)) ## Copy custom variable properties from selected variables for i = 1:numel (cpNames) tmp = this.CustomProperties.(cpNames{i}); if (isempty (tmp)) tbl.CustomProperties.(cpNames{i}) = tmp; else tbl.CustomProperties.(cpNames{i}) = tmp(ixVars); endif endfor endif endif tbl = setRowCount (tbl, nrows); tbl = varsChanged (tbl); endfunction ## The variables laid side by side as one array, and as a cell array. ## Neither reads the row labels, so both serve every tabular class; ## CALLER names the method reporting a refusal, which is the public ## conversion for a table and the brace reference otherwise. function A = varsAsArray (this, caller) ## An object with rows but no variables still reports its height, so ## the array it becomes keeps it too. if (width (this) == 0) A = zeros (height (this), 0); return; endif ## Variables that cannot form one array give a cell array instead. [A, ok] = tabular.join_vars (this.VariableValues); if (! ok) A = varsAsCell (this, caller); endif endfunction function C = varsAsCell (this, caller) ## Each cell holds one row of one variable, of the variable's own type. C = cell (size (this)); for i = 1:width (this) varVal = this.VariableValues{i}; if (iscell (varVal) && columns (varVal) == 1) C(:,i) = varVal; elseif (isnumeric (varVal) || islogical (varVal) || ischar (varVal) || iscell (varVal)) C(:,i) = num2cell (varVal, 2); elseif (isa (varVal, 'tabular')) ## Indexing inside a class method bypasses its own 'subsref'. for r = 1:size (varVal, 1) C{r,i} = subsetrows (varVal, r); endfor else ## A struct or an object, provided '()' selects its rows. try for r = 1:size (varVal, 1) C{r,i} = varVal(r,:); endfor catch error ("%s.%s: cannot convert variables of '%s' type.", ... class (this), caller, class (varVal)); end_try_catch endif endfor endfunction ## The body behind 'table2struct' on both classes. Neither form reads the ## row labels, so row names and row times are left out alike. Returns an ## errmsg body for the caller to raise. function [S, errmsg] = table2structResult (this, args) S = []; errmsg = ''; toScalar = false; if (! isempty (args)) if (numel (args) != 2) errmsg = "wrong number of input arguments."; return; elseif (! strcmpi (args{1}, 'ToScalar')) errmsg = "wrong optional input argument."; return; endif toScalar = isequal (args{2}, 1); endif if (toScalar) S = struct; for i = 1:width (this) S.(this.VariableNames{i}) = this.VariableValues{i}; endfor else C = varsAsCell (this, 'table2struct'); S = cell2struct (C, this.VariableNames(:), 2); endif endfunction ## The stored variable values, and the types of the custom properties, ## reachable from a sibling class. Dot access on an object of another ## class goes through that class's 'subsref' and is read as a variable ## name whatever the property's access, so an operation that mixes the ## two classes cannot read them directly and asks here instead. function out = varValues (this) out = this.VariableValues; endfunction function out = customPropTypes (this) out = this.CustomPropTypes; endfunction ## Get table properties as a struct for internal use called by subsasgn function out = getProperties (this) out = struct; out.Description = this.Description; out.UserData = this.UserData; out.DimensionNames = this.DimensionNames; out.VariableTypes = this.VariableTypes; out.VariableNames = this.VariableNames; out.VariableDescriptions = this.VariableDescriptions; out.VariableUnits = this.VariableUnits; out.VariableContinuity = this.VariableContinuity; rowProps = rowLabelProperties (this); rowNames = fieldnames (rowProps); for i = 1:numel (rowNames) out.(rowNames{i}) = rowProps.(rowNames{i}); endfor out.CustomProperties = this.CustomProperties; endfunction ## Get values from a single referenced variable function out = getvar (this, var_ref) [ix_var, ~] = resolveVarRef (this, var_ref); out = this.VariableValues{ix_var}; endfunction ## -*- texinfo -*- @deftypefn {table} {@var{out} =} setvar (@var{tbl}, ## @var{varRef}, @var{value}) ## ## Set values to an existing or a new variable in table. ## ## This sets (adds or replaces) the value for a variable in @var{tbl}. It ## may be used to change the value of an existing variable, or add a new ## variable. ## ## @var{varRef} is a variable reference, either its index or its name. If ## you are adding a new variable, it must be a name, and not an index. ## ## @var{value} is the value to set the variable to. If it is a scalar, it ## is scalar-expanded to match the number of rows in @var{tbl}. ## ## @end deftypefn function tbl = setvar (this, varRef, value) clstype = class (this); errmsg = variableRefusal (this, value); if (! isempty (errmsg)) error ("%s.subsasgn: %s", clstype, errmsg); endif ## An object with neither rows nor variables takes its height from ## the first variable given it, as in MATLAB, so nothing is expanded; ## otherwise do scalar expansion if necessary n_rows = height (this); if (n_rows == 0 && width (this) == 0) n_rows = size (value, 1); this = growRowLabels (this, n_rows); elseif (n_rows != 1 && (isscalar (value) || (ischar (value) && (size (value, 1) == 1 || isequal (size (value), [0 0]))))) if (ischar (value)) value = {value}; endif value = repmat (value, [n_rows, 1]); endif ## Check input matches table height if (size (value, 1) != n_rows) error ("%s.subsasgn: input value and %s height mismatch.", ... clstype, clstype); endif ## Resolve variable index ixVar = resolveVarRef (this, varRef, 'lenient'); tbl = this; if (ixVar == 0) ## Add new variable ix_new_var = width (this) + 1; tbl.VariableNames{ix_new_var} = varRef; tbl.VariableTypes{ix_new_var} = class (value); tbl.VariableValues{ix_new_var} = value; if (! isempty (tbl.VariableDescriptions)) tbl.VariableDescriptions{ix_new_var} = ''; endif if (! isempty (tbl.VariableUnits)) tbl.VariableUnits{ix_new_var} = ''; endif if (! isempty (this.VariableContinuity)) tbl.VariableContinuity{ix_new_var} = 'unset'; endif ## Check for custom variable properties if (! isempty (this.CustomProperties)) cpNames = customPropsOfType (this, 'variable'); if (! isempty (cpNames)) ## Add default values to custom variable properties for new variable for i = 1:numel (cpNames) tmp = this.CustomProperties.(cpNames{i}); if (! isempty (tmp)) if (isnumeric (tmp)) tmp(end+1) = NaN; elseif (islogical (tmp)) tmp(end+1) = false; elseif (iscell (tmp)) tmp{end+1} = []; elseif (isstruct (tmp)) f = fieldnames (tmp); tmp(end+1) = cell2struct (cell (numel (f), 1), f, 1); else ## Any other type takes its own missing value, as in MATLAB try tmp(end+1) = missing; end_try_catch endif tbl.CustomProperties.(cpNames{i}) = tmp; endif endfor endif endif else ## Set existing variable tbl.VariableTypes{ixVar} = class (value); tbl.VariableValues{ixVar} = value; endif tbl = setRowCount (tbl, n_rows); endfunction ## The assignment behind '()' and '{}' indexing. SUBS holds the row and ## variable subscripts, which may name rows and variables to create. ## '()' takes a tabular object, a cell array with one element per row and ## variable, or an array with one column per variable; '{}' takes the ## contents of the variables side by side, as '{}' indexing returns them. function tbl = assignSubs (this, subs, rhs, isBrace) clstype = class (this); tbl = this; [ixRow, ~, newLabels] = resolveRowVarRefs (this, subs{1}, ':'); [ixVar, newNames] = resolveAssignVarRef (this, subs{2}); if (islogical (ixRow)) ixRow = find (ixRow); endif oldHeight = height (this); oldWidth = width (this); ## An object with neither rows nor variables takes its height from ## what is assigned to all of its rows. if (oldHeight == 0 && oldWidth == 0 && ischar (subs{1}) && strcmp (subs{1}, ':')) ixRow = 1:size (rhs, 1); ## and its variables, one for each column, from what '{}' assigns ## to all of them. if (isBrace && ischar (subs{2}) && strcmp (subs{2}, ':')) ixVar = 1:size (rhs, 2); newNames = arrayfun (@(k) sprintf ("Var%d", k), ixVar, ... "UniformOutput", false); endif endif ## A row index past the end grows the object. The variables grow ## by being assigned into, but the row labels are not indexed here ## and would be left behind, so the height they carry is worked out ## now and the labels are extended once the variables are in. newHeight = growthHeight (this, ixRow); ## A label the object does not carry names a row to add. The rows ## go on the end, in the order the reference named them, and take ## the places the resolver left marked. if (! isempty (newLabels)) nNew = size (newLabels, 1); base = height (this); ixRow(isnan (ixRow)) = (base + 1):(base + nNew); labels = getRowLabels (this); this = setRowLabels (this, [labels(:); newLabels(:)]); tbl = this; newHeight = max (newHeight, base + nNew); labelsAdded = true; else labelsAdded = false; endif if (isBrace) errmsg = variableRefusal (this, rhs); if (! isempty (errmsg)) error ("%s.subsasgn: %s", clstype, errmsg); endif endif if (isBrace) ## The value is the contents of the variables side by side, as ## '{}' indexing returns them, so it is split by their columns. A ## variable named anew takes all of it when it is the only one ## assigned and one column otherwise. nv = numel (ixVar); w = ones (1, nv); for i = 1:nv if (ixVar(i) <= oldWidth) w(i) = size (this.VariableValues{ixVar(i)}, 2); elseif (nv == 1) w(i) = size (rhs, 2); endif endfor if (isa (rhs, 'tabular') && nv != 1) error (strcat ("%s.subsasgn: input data type mismatch", ... " indexed variable type."), clstype); endif if (! isscalar (rhs) || isa (rhs, 'tabular')) if (size (rhs, 2) != sum (w)) error (strcat ("%s.subsasgn: the value assigned must have", ... " %d columns, one for each column of the", ... " variables assigned."), clstype, sum (w)); endif ## A single variable named anew takes a row as every one of its ## rows, as in MATLAB; anywhere else the rows must match. if (size (rhs, 1) == 1 && numel (ixRow) != 1 && nv == 1 && ixVar(1) > oldWidth) rhs = repmat (rhs, numel (ixRow), 1); elseif (size (rhs, 1) != numel (ixRow)) error (strcat ("%s.subsasgn: the value assigned must have", ... " %d rows, one for each row assigned."), ... clstype, numel (ixRow)); endif endif else ## Check input data matches referenced elements if (! isequal (size (rhs), [numel(ixRow), numel(ixVar)])) error (strcat ("%s.subsasgn: input data mismatch indexed", ... " dimensions."), clstype); endif if (isa (rhs, 'timetable') && ! isa (this, 'timetable')) error (strcat ("%s.subsasgn: input data type mismatch", ... " indexed variable type."), clstype); endif endif if (newHeight > 0) ## The variables grow before anything is written into them, so ## that the rows the assignment passes over carry the fill the ## class gives rather than whatever indexed growth leaves, and a ## variable the assignment does not name grows with the rest. for i = 1:numel (this.VariableValues) [v, errmsg] = padVariable (this.VariableValues{i}, newHeight); if (! isempty (errmsg)) error ("%s.subsasgn: %s", clstype, errmsg); endif this.VariableValues{i} = v; tbl.VariableValues{i} = v; endfor endif ## The values for each variable assigned vals = cell (1, numel (ixVar)); for i = 1:numel (ixVar) if (isBrace) if (numel (ixVar) == 1 || (isscalar (rhs) && ! isa (rhs, 'tabular'))) vals{i} = rhs; else c = [0, cumsum(w)]; vals{i} = rhs(:,(c(i)+1):c(i+1)); endif elseif (isa (rhs, 'tabular')) # MATLAB compatible vals{i} = rhs.VariableValues{i}; elseif (iscell (rhs)) # MATLAB compatible ## A cell variable takes each element as it is, or unwrapped ## once where it comes in a cell of its own, as in MATLAB; any ## other variable takes the elements stacked. col = rhs(:,i); if (ixVar(i) <= oldWidth) isCellVar = iscell (this.VariableValues{ixVar(i)}); else isCellVar = any (cellfun (@(x) ischar (x) || iscell (x) ... || is_function_handle (x), col)); endif if (isCellVar) for k = 1:numel (col) if (iscell (col{k}) && isscalar (col{k})) col{k} = col{k}{1}; endif endfor vals{i} = col; else try vals{i} = vertcat (col{:}); catch error (strcat ("%s.subsasgn: input data type mismatch", ... " indexed variable type."), clstype); end_try_catch endif else # Octave specific vals{i} = rhs(:,i); endif endfor ## The variables there are take their rows by assignment. The ## call is explicit because a nested table indexed here would take ## the built-in assignment, which makes an array of tables. sRows = struct ('type', '()', 'subs', {{ixRow, ':'}}); for i = find (ixVar <= oldWidth) varData = this.VariableValues{ixVar(i)}; ## The contents of a cell variable are cells, and Octave would put ## anything else into every cell assigned, where MATLAB refuses. if (isBrace && iscell (varData) && ! iscell (vals{i})) error (strcat ("%s.subsasgn: input data type mismatch", ... " indexed variable type."), clstype); endif ## Text converts to the numbers it reads as, and only into a double. if (isBrace && isa (varData, 'double') && isa (vals{i}, 'string')) vals{i} = double (vals{i}); endif try varData = subsasgn (varData, sRows, vals{i}); catch error (strcat ("%s.subsasgn: input data type mismatch", ... " indexed variable type."), clstype); end_try_catch tbl.VariableValues{ixVar(i)} = varData; endfor if (newHeight > 0 && ! labelsAdded) tbl = growRowLabels (tbl, newHeight); endif ## A timetable assigned into a timetable brings row times, which ## must be those of the rows assigned; MATLAB pastes by position ## and ignores them, which would put values at the wrong times. if (! isBrace && isa (rhs, 'timetable')) labels = getRowLabels (tbl); rhsLabels = getRowLabels (rhs); if (! strcmp (class (labels), class (rhsLabels)) || ! isequaln (labels(ixRow), rhsLabels(:))) error (strcat ("%s.subsasgn: the row times of the assigned", ... " timetable do not match the rows assigned;", ... " use synchronize to align them first."), ... clstype); endif endif ## The variables named anew are built at full height, the rows not ## assigned taking the fill their class gives, and added as any ## new variable is, so that their properties follow. H = height (tbl); for j = 1:numel (newNames) idx = find (ixVar == oldWidth + j); proto = subsref (vals{idx(1)}, struct ('type', '()', ... 'subs', {{[], ':'}})); [col, errmsg] = padVariable (proto, H); if (! isempty (errmsg)) error ("%s.subsasgn: %s", clstype, errmsg); endif try for i = idx col = subsasgn (col, sRows, vals{i}); endfor catch error (strcat ("%s.subsasgn: input data type mismatch", ... " indexed variable type."), clstype); end_try_catch tbl = setvar (tbl, newNames{j}, col); endfor ## Padding with defaults is reported, as in MATLAB if (H > oldHeight && oldWidth > 0 && ! all (ismember (1:oldWidth, ixVar))) warning (strcat ("%s.subsasgn: new rows were padded with", ... " default values in the variables not", ... " assigned."), clstype); elseif (! isempty (newNames) && numel (unique (ixRow)) < H) warning (strcat ("%s.subsasgn: new variables were padded with", ... " default values in the rows not assigned."), ... clstype); endif endfunction ## Resolve subscripted reference for internal use called by subsasgn function out = single_subref (this, s) clstype = class (this); switch s.type case '{}' out = subsref (this, s); case '()' if (numel (s.subs) != 2) error (strcat ("%s.subsasgn: ()-indexing of %s requires", ... " exactly two arguments."), clstype, clstype); endif [ixRow, ixVar] = resolveRowVarRefs (this, s.subs{1}, s.subs{2}); out = this; out = subsetrows (out, ixRow); out = subsetvars (out, ixVar); case '.' ## A field name may be given as a string scalar, as in MATLAB. if (isstring (s.subs) && isscalar (s.subs)) s.subs = char (s.subs); endif if (! (ischar (s.subs) && isrow (s.subs))) error (strcat ("%s.subsasgn: .-index argument must be a", ... " character vector or a string scalar."), clstype); endif ## Handle special cases: "Properties" and "DimensionNames" if isequal (s.subs, 'Properties') out = getProperties (this); elseif isequal (s.subs, this.DimensionNames{1}) out = getRowLabels (this); elseif isequal (s.subs, this.DimensionNames{2}) out = varsAsArray (this, 'subsref'); ## Everything else is indexing an existing variable name else out = getvar (this, s.subs); endif endswitch endfunction endmethods ################################################################################ ## ** Printing internals ** ## ################################################################################ ## ## ## The rendering path: what turns a table's content into text, or into ## ## the flat cell arrays that both the display and the exporters consume. ## ## ## ################################################################################ methods (Access = protected) ## Display table internal function function print_table (this) ## Get VariableNames and VariableNames for optimal length of each column var_num = width (this); colData = {}; rowSpat = ""; T.parentV = []; T.nestedV = []; T.varName = {}; T.varNLen = []; T.optLen = []; colgap = " "; [colData, rowSpat, T] = resolve_table_for_printing ... (this, colData, rowSpat, T); ## Check for nested tables if (numel (T.nestedV) > 0) nested = true; ## Prepare nested table header varL_idx = 1; varN_idx = 1; varT_idx = 1; strhead1 = ""; strline1 = ""; strhead2 = ""; strline2 = ""; for v = 1:var_num ## Check for nested table in each variable if (ismember (v, T.parentV)) ## Get name length of variable containing the table and remove it ## from T.varNLen so it is aligned with data columns' T.optLen parVarNLen = T.varNLen(varL_idx); parVarName = T.varName{varN_idx}; T.varNLen(varL_idx) = []; ## Go through variables of nested table sum_optLen = 0; for nv = 1:T.nestedV(varT_idx) varN_idx += 1; pad_nB = floor ((T.optLen(varL_idx) - T.varNLen(varL_idx)) / 2); pad_nA = T.optLen(varL_idx) - (T.varNLen(varL_idx) + pad_nB); srtVarName = sprintf ("%s", T.varName{varN_idx}); strhead2 = [strhead2, repmat(" ", [1, pad_nB]), ... srtVarName, repmat(" ", [1, pad_nA]), colgap]; strline2 = [strline2, repmat("_", [1, T.optLen(varL_idx)]), ... colgap]; sum_optLen += T.optLen(varL_idx) + 4; varL_idx += 1; endfor ## Keep track of indexing sum_optLen -= 4; varL_idx -= 1; ## Fix continuous line for all nested table variables strline1 = [strline1, repmat("_", [1, sum_optLen]), colgap]; ## Position parent variable in the middle of the top header pad_nB = floor ((sum_optLen - parVarNLen) / 2); pad_nA = sum_optLen - (parVarNLen + pad_nB); srtVarName = sprintf ("%s", parVarName); strhead1 = [strhead1, repmat(" ", [1, pad_nB]), ... srtVarName, repmat(" ", [1, pad_nA]), colgap]; ## Increment index for nested table variables varT_idx += 1; else ## Oridinary variable (no nested table) pad_nB = floor ((T.optLen(varL_idx) - T.varNLen(varL_idx)) / 2); pad_nA = T.optLen(varL_idx) - (T.varNLen(varL_idx) + pad_nB); srtVarName = sprintf ("%s", T.varName{varN_idx}); strhead1 = [strhead1, repmat(" ", [1, pad_nB]), ... srtVarName, repmat(" ", [1, pad_nA]), colgap]; strhead2 = [strhead2, repmat(" ", [1, T.optLen(varL_idx)]), colgap]; strline1 = [strline1, repmat("_", [1, T.optLen(varL_idx)]), colgap]; strline2 = [strline2, repmat(" ", [1, T.optLen(varL_idx)]), colgap]; endif varL_idx += 1; varN_idx += 1; endfor else ## No nested table nested = false; ## Prepare table header strhead1 = ""; strline1 = ""; for v = 1:var_num pad_nB = floor ((T.optLen(v) - T.varNLen(v)) / 2); pad_nA = T.optLen(v) - (T.varNLen(v) + pad_nB); srtVarName = sprintf ("%s", T.varName{v}); strhead1 = [strhead1, repmat(" ", [1, pad_nB]), ... srtVarName, repmat(" ", [1, pad_nA]), colgap]; strline1 = [strline1, repmat("_", [1, T.optLen(v)]), colgap]; endfor endif ## Check whether RowNames are available in table and construct ## cell array of strings of size [height(table), 1] to prepad ## the displayed table with if (hasRowLabels (this)) rowLabels = rowLabelStrings (this); rowHead = rowLabelHeader (this); ## The column is as wide as the wider of its labels and its heading, ## the labels sit at the left of it, and the heading is centred over ## it exactly as a variable's name is centred over its own column. rnWidth = max ([cellfun(@length, rowLabels)(:); length(rowHead)]); rnLen = rnWidth + 4; padPT = sprintf ("%%-%ds", rnLen); padfn = @(x) sprintf (padPT, x); rowNM = cellfun (padfn, rowLabels, 'UniformOutput', false); if (isempty (rowHead)) rowHeadStr = repmat (" ", [1, rnLen]); rowLineStr = repmat (" ", [1, rnLen]); else padB = floor ((rnWidth - length (rowHead)) / 2); padA = rnWidth - length (rowHead) - padB; rowHeadStr = [repmat(" ", [1, padB]), rowHead, ... repmat(" ", [1, padA]), colgap]; rowLineStr = [repmat("_", [1, rnWidth]), colgap]; endif ## Print table header fprintf (" %s%s\n", rowHeadStr, strhead1); fprintf (" %s%s\n\n", rowLineStr, strline1); if (nested) fprintf (" %s%s\n", repmat (" ", [1, rnLen]), strhead2); fprintf (" %s%s\n\n", repmat (" ", [1, rnLen]), strline2); endif ## Print table rows. With no variables there is nothing to lay out ## beside the labels, and the row is the label alone. for iRow = 1:height (this) if (var_num > 0) strrow = sprintf (rowSpat, colData{iRow,:}); else strrow = ""; endif fprintf (" %s%s\n", rowNM{iRow}, strrow); endfor fprintf ("\n"); else ## Print table header fprintf (" %s\n", strhead1); fprintf (" %s\n\n", strline1); if (nested) fprintf (" %s\n", strhead2); fprintf (" %s\n\n", strline2); endif ## Print table rows for iRow = 1:height (this) strrow = sprintf (rowSpat, colData{iRow,:}); fprintf (" %s\n", strrow); endfor fprintf ("\n"); endif endfunction ## Prepare table for printing. Each variable becomes one or more display ## columns: 'display_texts' says what each cell reads and how it aligns, ## and the widths and the row pattern are worked out here, the same way ## for every type. A nested table or timetable is laid out under its ## variable's name at the first level, with a timetable's row times first. function [colData, rowSpat, T] = resolve_table_for_printing ... (this, colData, rowSpat, T) ## Recursing into a nested object, every column is at least as wide as ## the name of the variable holding it. if (numel (T.nestedV) > 0) nested = true; minLen = T.varNLen(end); else nested = false; minLen = 1; endif colgap = " "; ## The row times of a nested timetable lead its variables. if (nested && ! isempty (rowLabelHeader (this))) head = rowLabelHeader (this); texts = rowLabelStrings (this); T.varName = [T.varName, {head}]; T.varNLen = [T.varNLen, length(head)]; optLen = max ([length(head); cellfun(@length, texts(:)); minLen]); T.optLen = [T.optLen, optLen]; colData = [colData, texts(:)]; rowSpat = [rowSpat, sprintf("%%-%ds", optLen), colgap]; endif for v = 1:width (this) varNLen = length (this.VariableNames{v}); T.varName = [T.varName, this.VariableNames(v)]; T.varNLen = [T.varNLen, varNLen]; data = this.VariableValues{v}; if (isa (data, 'tabular') && ! nested) nsub = width (data) + ! isempty (rowLabelHeader (data)); T.nestedV = [T.nestedV, nsub]; T.parentV = [T.parentV, v]; [colData, rowSpat, T] = resolve_table_for_printing ... (data, colData, rowSpat, T); continue; endif [texts, align, W] = display_texts (data, varNLen); colData = [colData, texts]; if (columns (texts) == 1) optLen = max ([varNLen, W, minLen]); rowSpat = [rowSpat, cellpattern(align, optLen), colgap]; else dataLen = sum (W + 4) - 4; optLen = max ([varNLen, dataLen, minLen]); pats = arrayfun (@(w) [cellpattern(align, w), colgap], W, ... "UniformOutput", false); rowSpat = [rowSpat, repmat(" ", [1, optLen-dataLen]), pats{:}]; endif T.optLen = [T.optLen, optLen]; endfor endfunction ## Summary internal function ## -*- texinfo -*- ## @deftypefn {tabular} {@var{s} =} summaryOf (@var{obj}) ## ## The summary structure, row labels first where the class has them. ## ## @end deftypefn function s = summaryOf (this) s = struct (); [lname, lentry] = summaryLabelEntry (this); if (! isempty (lname)) s.(lname) = lentry; endif vs = summary_for_variables (this); f = fieldnames (vs); for i = 1:numel (f) s.(f{i}) = vs.(f{i}); endfor endfunction ## -*- texinfo -*- ## @deftypefn {tabular} {} summaryPrint (@var{obj}, @var{s}, @var{name}) ## ## Print the summary of an object. ## ## @var{s} is the structure @code{summaryOf} built and @var{name} the ## caller's own name for the object, which only the public method can ## read and which is omitted where there is none. ## ## @end deftypefn function summaryPrint (this, s, name) sz = size (this); fprintf ('\n'); if (isempty (name)) fprintf ('%dx%d %s\n', sz(1), sz(2), class (this)); else fprintf ('%s: %dx%d %s\n', name, sz(1), sz(2), class (this)); endif if (! isempty (this.Description)) fprintf ('Description: %s\n', this.Description); endif [lname, ~] = summaryLabelEntry (this); hasLabel = ! isempty (lname); if (hasLabel) fprintf ('Row Times:\n'); fprintf (' %s: %s\n', lname, s.(lname).Type); endif if (width (this) > 0) fprintf ('Variables:\n'); for v = 1:width (this) nm = this.VariableNames{v}; fprintf ('%s\n', summaryVarLine (nm, this.VariableValues{v}, ... s.(nm))); endfor endif ## The statistics of everything that has any, one row per column of a ## multi-column variable. A nested table is left out: its statistics ## are tables themselves and there is no rendering them in a cell. names = {}; if (hasLabel) names = {lname}; endif for v = 1:width (this) if (! (isa (this.VariableValues{v}, 'table') || isa (this.VariableValues{v}, 'timetable'))) names{end+1} = this.VariableNames{v}; endif endfor ## With no rows there is nothing to report statistics about. if (sz(1) == 0) names = {}; endif [labels, cells, cols] = summaryStatRows (s, names); if (isempty (labels)) fprintf ('\n'); return endif if (hasLabel) fprintf ('Statistics for applicable variables and row times:\n'); else fprintf ('Statistics for applicable variables:\n'); endif summaryStatTable (labels, cells, cols); fprintf ('\n'); endfunction function s = summary_for_variables (this) ## An object with no variables summarises to no fields, not to nothing. s = struct (); sumD = tabular.meta_or_blank (this.VariableDescriptions, width (this)); sumU = tabular.meta_or_blank (this.VariableUnits, width (this)); for v = 1:width (this) varName = this.VariableNames{v}; val = this.VariableValues{v}; e = struct (); e.Size = size (val); e.Type = class (val); e.Description = ''; if (! isempty (sumD{v})) e.Description = sumD{v}; endif e.Units = ''; if (! isempty (sumU{v})) e.Units = sumU{v}; endif e.Continuity = []; if (! isempty (this.VariableContinuity)) e.Continuity = this.VariableContinuity{v}; endif s.(varName) = tabular.summaryStats (e, val); endfor endfunction ## -*- texinfo -*- @deftypefn {tabular} {[@var{name}, @var{entry}] =} ## summaryLabelEntry (@var{obj}) ## ## The summary entry for the row labels, where the class has one. ## ## @var{name} is the field it is filed under and @var{entry} the entry ## itself. A class whose row labels are not summarised answers with an ## empty name. ## ## @end deftypefn function [name, entry] = summaryLabelEntry (this) name = ''; entry = []; endfunction ## Export table to cell arrays function [V, N, T, D, U, errmsg] = table2cellarrays (this, fmt = 'display') V = {}; # variable values N = {}; # variable names T = {}; # variable types D = {}; # variable descriptions U = {}; # variable units errmsg = ''; ## Every exported column needs a description and a units slot. An unset ## property gives empties, which a file writes as value-less fields and ## so distinguishes from the empty strings a set-but-blank one gives. VD = tabular.meta_or_bare (this.VariableDescriptions, width (this)); VU = tabular.meta_or_bare (this.VariableUnits, width (this)); dBare = {[]}; if (! isempty (this.VariableDescriptions)) dBare = {''}; endif uBare = {[]}; if (! isempty (this.VariableUnits)) uBare = {''}; endif ## Process the row labels. Labels that are row times are exported ## exactly as a variable of their type is, in ISO form and under their ## own type string, so that a display format cannot round or reorder ## them on the way out; labels that are already text are text. if (hasRowLabels (this)) lbl = getRowLabels (this); if (isa (lbl, 'datetime') || isa (lbl, 'duration')) ltype = ['RowTimes|', tabular.value_type_string(lbl)]; if (strcmp (fmt, 'iso')) if (isa (lbl, 'datetime')) lval = datetime2iso (lbl); else lval = duration2iso (lbl); endif else lval = rowLabelStrings (this); endif else ltype = 'cellstr'; lval = rowLabelStrings (this); endif V = [V, lval]; N = [N, {''}]; T = [T, ltype]; D = [D, dBare]; U = [U, uBare]; endif ## Process variables for ix = 1:width (this) var_V = this.VariableValues{ix}; ncols = size (var_V, 2); ## Handle each variable type if (iscell (var_V)) for col = 1:ncols V = [V, var_V(:,col)]; N = [N, this.VariableNames{ix}]; T = [T, 'cell']; D = [D, VD(ix)]; U = [U, VU(ix)]; endfor elseif (islogical (var_V)) for col = 1:ncols V = [V, num2cell(var_V(:,col))]; N = [N, this.VariableNames{ix}]; T = [T, 'logical']; D = [D, VD(ix)]; U = [U, VU(ix)]; endfor elseif (ischar (var_V)) ## A char matrix is one column of text and its rows are kept whole, ## padding included; splitting it by column would split it between ## characters. V = [V, num2cell(var_V, 2)]; N = [N, this.VariableNames{ix}]; T = [T, 'char']; D = [D, VD(ix)]; U = [U, VU(ix)]; elseif (isa (var_V, 'missing')) ## Every entry is missing, so the values carry nothing and the ## variable type alone restores the variable. for col = 1:ncols V = [V, repmat({[]}, size (var_V, 1), 1)]; N = [N, this.VariableNames{ix}]; T = [T, 'missing']; D = [D, VD(ix)]; U = [U, VU(ix)]; endfor elseif (isnumeric (var_V)) for col = 1:ncols V = [V, num2cell(var_V(:,col))]; N = [N, this.VariableNames{ix}]; T = [T, class(var_V(:,col))]; D = [D, VD(ix)]; U = [U, VU(ix)]; endfor elseif (isa (var_V, 'calendarDuration')) for col = 1:ncols V = [V, cellstr(var_V(:,col))]; N = [N, this.VariableNames{ix}]; T = [T, 'calendarDuration']; D = [D, VD(ix)]; U = [U, VU(ix)]; endfor elseif (isa (var_V, 'categorical')) for col = 1:ncols V = [V, cellstr(var_V(:,col))]; N = [N, this.VariableNames{ix}]; T = [T, 'categorical']; D = [D, VD(ix)]; U = [U, VU(ix)]; endfor elseif (isa (var_V, 'datetime')) ## Carry a non-empty TimeZone in the type string ('datetime ') so ## the house readers can restore a zone-aware datetime, and the ## display format after a '|', so that it survives the round trip. dttype = tabular.value_type_string (var_V); for col = 1:ncols if (strcmp (fmt, 'iso')) V = [V, datetime2iso(var_V(:,col))]; else V = [V, cellstr(var_V(:,col))]; endif N = [N, this.VariableNames{ix}]; T = [T, dttype]; D = [D, VD(ix)]; U = [U, VU(ix)]; endfor elseif (isa (var_V, 'duration')) dutype = tabular.value_type_string (var_V); for col = 1:ncols if (strcmp (fmt, 'iso')) V = [V, duration2iso(var_V(:,col))]; else V = [V, cellstr(var_V(:,col))]; endif N = [N, this.VariableNames{ix}]; T = [T, dutype]; D = [D, VD(ix)]; U = [U, VU(ix)]; endfor elseif (isa (var_V, 'string')) for col = 1:ncols ## A missing string carries no value, which is not the same as an ## empty one; it is written as nothing rather than as an empty ## string. sv = cellstr (var_V(:,col)); sv(ismissing (var_V(:,col))) = {[]}; V = [V, sv]; N = [N, this.VariableNames{ix}]; T = [T, 'string']; D = [D, VD(ix)]; U = [U, VU(ix)]; endfor elseif (isa (var_V, 'table')) [tmpV, tmpN, tmpT tmpD, tmpU, errmsg] = table2cellarrays (var_V, fmt); if (! isempty (errmsg)) return; endif V = [V, tmpV]; nestedN = {}; nestedT = {}; nestedD = {}; nestedU = {}; for col = 1:size (tmpV, 2) nestedN = [nestedN, {{this.VariableNames{ix}; tmpN{col}}}]; nestedT = [nestedT, {{'table'; tmpT{col}}}]; nestedD = [nestedD, {{VD{ix}; tmpD{col}}}]; nestedU = [nestedU, {{VU{ix}; tmpU{col}}}]; endfor N = [N, nestedN]; T = [T, nestedT]; D = [D, nestedD]; U = [U, nestedU]; elseif (isa (var_V, 'struct')) tmpV = squeeze (struct2cell (var_V(:)))'; tmpN = fieldnames (var_V(:))'; tmpT = cellfun ('class', tmpV(1,:), 'UniformOutput', false); V = [V, tmpV]; nestedN = {}; nestedT = {}; nestedD = {}; nestedU = {}; for col = 1:size (tmpV, 2) nestedN = [nestedN, {{this.VariableNames{ix}; tmpN{col}}}]; nestedT = [nestedT, {{'struct'; tmpT{col}}}]; nestedD = [nestedD, {{VD{ix}; []}}]; nestedU = [nestedU, {{VU{ix}; []}}]; endfor N = [N, nestedN]; T = [T, nestedT]; D = [D, nestedD]; U = [U, nestedU]; else errmsg = sprintf ("cannot write variables of '%s' type.", ... class (var_V)); return; endif endfor endfunction endmethods ################################################################################ ## ** Shared static helpers ** ## ################################################################################ ## ## ## Local functions before the split, called from both this class and its ## ## subclasses. They take no tabular object, so ordinary dispatch would never ## ## find them; they are called as 'tabular. (...)'. ## ## ## ## They are Hidden rather than protected because a local function carries no ## ## class context, so protected access is refused there, and several of the ## ## callers are local functions. Hidden keeps them out of 'methods' just as ## ## protected would, so they stay off the class pages either way. ## ## ## ################################################################################ methods (Static, Hidden) ## Carry the custom properties of SRC onto DST. IXVARS maps each ## variable of DST to the variable of SRC it came from, 0 for one that ## came from none, which is how a conversion that gains or loses a ## variable keeps a 'variable' property in step: a timetable turned into ## a table gains the row times as its first variable, and that variable ## has no entry to inherit. A 'table' property is carried whole. function dst = carryCustomProps (dst, src, ixVars) if (isempty (src.CustomProperties)) return endif names = fieldnames (src.CustomProperties); for k = 1:numel (names) name = names{k}; ty = src.CustomPropTypes.(name); val = src.CustomProperties.(name); if (strcmp (ty, 'variable') && ! isempty (val)) if (iscell (val)) out = cell (1, numel (ixVars)); for j = 1:numel (ixVars) if (ixVars(j) > 0 && ixVars(j) <= numel (val)) out(j) = val(ixVars(j)); endif endfor val = out; elseif (all (ixVars > 0) && max (ixVars) <= numel (val)) val = val(ixVars); else ## A variable the source did not have takes the missing value of ## the property's own type rather than turning it into a cell. out = NaN (1, numel (ixVars)); for j = 1:numel (ixVars) if (ixVars(j) > 0 && ixVars(j) <= numel (val)) out(j) = val(ixVars(j)); endif endfor val = out; endif endif dst.CustomProperties.(name) = val; dst.CustomPropTypes.(name) = ty; endfor endfunction ## -*- texinfo -*- ## @deftypefn {tabular} {@var{e} =} summaryStats (@var{e}, @var{val}) ## ## Add the statistics half of a summary entry, chosen by the value's type. ## ## The order the fields are added in is the order they are reported in, ## and which of them a type gets was measured rather than reasoned: a ## logical carries counts and nothing else, an integer has no standard ## deviation, and only an ordinal categorical can be ordered. ## ## @end deftypefn function e = summaryStats (e, val) if (islogical (val)) e.True = sum (val, 1); e.False = sum (! val, 1); elseif (isa (val, 'categorical')) e.Categories = categories (val); e.Counts = countcats (val); e.NumMissing = sum (ismissing (val), 1); if (isordinal (val)) d = double (val); cats = categories (val); e.Min = catOf (cats, __nanmin__ (d), val); e.Median = catOf (cats, ceil (median (d, 'omitnan')), val); e.Max = catOf (cats, __nanmax__ (d), val); endif elseif (isa (val, 'duration')) sec = seconds (val); fmt = val.Format; e.NumMissing = sum (isnan (sec), 1); e.Min = durOf (__nanmin__ (sec), fmt); e.Median = durOf (median (sec, 'omitnan'), fmt); e.Max = durOf (__nanmax__ (sec), fmt); e.Mean = durOf (colmean (sec), fmt); e.Std = durOf (colstd (sec), fmt); elseif (isa (val, 'datetime')) ## Measured from the first known instant so that the time zone, which ## the arithmetic carries, survives into every statistic. e.TimeZone = val.TimeZone; nm = ismissing (val); e.NumMissing = sum (nm, 1); ix = find (! nm(:), 1); if (isempty (ix) && isempty (val)) ## Nothing at all to measure from, not even a zero point. e.Min = val; e.Median = val; e.Max = val; e.Mean = val; e.Std = durOf (NaN, 'hh:mm:ss'); return elseif (isempty (ix)) sec = NaN (size (val)); origin = val(1); else origin = val(ix); sec = seconds (val - origin); endif e.Min = origin + seconds (__nanmin__ (sec)); e.Median = origin + seconds (median (sec, 'omitnan')); e.Max = origin + seconds (__nanmax__ (sec)); e.Mean = origin + seconds (colmean (sec)); e.Std = durOf (colstd (sec), 'hh:mm:ss'); elseif (isa (val, 'calendarDuration')) ## Not totally ordered: months and days are not interconvertible, so ## there is nothing to report but the count. e.NumMissing = sum (ismissing (val), 1); elseif (isinteger (val)) ## An integer median rounds to the type, and there is no deviation. e.NumMissing = zeros (1, columns (val)); e.Min = min (val, [], 1); e.Median = cast (round (median (double (val), 1)), class (val)); e.Max = max (val, [], 1); e.Mean = mean (double (val), 1); elseif (isnumeric (val)) e.NumMissing = sum (isnan (val), 1); if (size (val, 1) == 0) ## With no rows there is no smallest and no largest, while the ## averages still answer, as NaN. e.Min = val; e.Median = NaN (1, columns (val)); e.Max = val; e.Mean = NaN (1, columns (val)); e.Std = NaN (1, columns (val)); return endif e.Min = __nanmin__ (val); e.Median = median (val, 'omitnan'); e.Max = __nanmax__ (val); ## A single stays single through its own statistics. e.Mean = cast (colmean (val), class (val)); e.Std = cast (colstd (val), class (val)); elseif (isa (val, 'table') || isa (val, 'timetable')) e.NumMissing = sum (__varmissing__ (val), 1); else ## Text, cells, structs and anything else carry a count alone. m = __varmissing__ (val); if (size (m, 1) != size (val, 1)) m = false (size (val, 1), 1); endif e.NumMissing = sum (m, 1); endif endfunction ## A 0x0 non-char operand takes no part in concatenation and is dropped, ## as in MATLAB: [], zeros (0, 0), {} and an empty table all vanish. A ## 0x2 double and '' are not 0x0-and-non-char, so they survive here and ## are rejected by the caller's "all inputs must be tabular" check. function args = drop_null_operands (args) keep = true (1, numel (args)); for i = 1:numel (args) arg = args{i}; if (! ischar (arg) && ndims (arg) == 2 && all (size (arg) == 0)) keep(i) = false; endif endfor args = args(keep); endfunction ## Merge the VariableContinuity of two horizontally combined operands. ## An operand carrying none contributes 'unset' for each of its ## variables, and the result is empty only when neither carries any. ## A variable-scoped metadata property carried onto a derived object by ## the index IX. An unset property stays unset: the derived object was ## told nothing either. function out = subset_meta (v, ix) if (isempty (v)) out = {}; else out = v(ix); endif endfunction ## The same property read as one entry per variable, an unset one giving N ## blanks. For the places that need a slot per variable whether or not ## anything was ever set, such as the display and export paths. function out = meta_or_blank (v, n) if (isempty (v)) out = repmat ({''}, 1, n); else out = v; endif endfunction ## The same property as one entry per variable for the export paths, an ## unset one giving N empties rather than N blanks. A file writes an empty ## as a value-less field and a blank as an empty string, so a block written ## for one nesting level can still say that another level has none. function out = meta_or_bare (v, n) if (isempty (v)) out = repmat ({[]}, 1, n); else out = v; endif endfunction ## The concatenation of two such properties, over objects of NA and NB ## variables. Unset on both sides stays unset; unset on one side fills ## with blanks so the result still holds one entry per variable. function m = merge_meta (a, na, b, nb) if (isempty (a) && isempty (b)) m = {}; return; endif m = [tabular.meta_or_blank(a, na), tabular.meta_or_blank(b, nb)]; endfunction function vc = merge_continuity (a, na, b, nb) if (isempty (a) && isempty (b)) vc = []; return; endif if (isempty (a)) a = repmat ({'unset'}, [1, na]); endif if (isempty (b)) b = repmat ({'unset'}, [1, nb]); endif vc = [a, b]; endfunction ## Convert a datetime array to datenum-valued doubles of the same size, ## mapping NaT to NaN. Used by 'summary'. Core 'datenum' cannot process ## the NaN date components of a NaT, so those rows are substituted with a ## valid placeholder before conversion and set back to NaN afterwards. function dn = datetime_to_datenum (v) sz = size (v); DV = datevec (v); # (numel)-by-6 in column-major order nat = any (isnan (DV), 2); DV(nat,:) = 0; DV(nat,2:3) = 1; # valid month/day placeholder dn = datenum (DV); dn(nat) = NaN; dn = reshape (dn, sz); endfunction ## Map a key variable kind to a comparison category. Returns an empty ## character vector for types that cannot be used as keys. function k = key_kind (col) if (isa (col, 'categorical') || isa (col, 'string') || iscellstr (col) || ischar (col)) k = 'text'; elseif (isa (col, 'datetime')) k = 'datetime'; elseif (isa (col, 'duration')) k = 'duration'; elseif (isa (col, 'calendarDuration')) k = 'calendarDuration'; elseif (isnumeric (col) || islogical (col)) k = 'numeric'; elseif (isa (col, 'tabular')) k = 'tabular'; else k = ''; endif endfunction ## Encode two cellstr key columns into consistent integer codes so that ## equal strings (across both columns) map to the same code. function [lp, rp] = text_codes (lc, rc) nl = numel (lc); [~, ~, ic] = unique ([lc(:); rc(:)]); lp = ic(1:nl); rp = ic(nl+1:end); endfunction ## Build consistent numeric key proxies for the same key variable taken from ## two tables, so that equal key values map to equal proxy rows. Returns an ## errmsg body (empty on success) emitted by the caller under its own name. function [lp, rp, errmsg] = key_col_proxy (lcol, rcol) lp = []; rp = []; errmsg = ''; kl = tabular.key_kind (lcol); kr = tabular.key_kind (rcol); if (isempty (kl)) errmsg = sprintf ("unsupported key variable type '%s'.", class (lcol)); return; elseif (isempty (kr)) errmsg = sprintf ("unsupported key variable type '%s'.", class (rcol)); return; elseif (! strcmp (kl, kr)) errmsg = "key variables have incompatible types."; return; endif switch (kl) case 'text' if (ischar (lcol) || columns (lcol) == 1 || columns (lcol) != columns (rcol)) [lp, rp] = tabular.text_codes (cellstr (lcol), cellstr (rcol)); else ## A multicolumn key is coded column by column, as a numeric one ## is compared. lp = zeros (rows (lcol), columns (lcol)); rp = zeros (rows (rcol), columns (rcol)); for c = 1:columns (lcol) [lp(:,c), rp(:,c)] = tabular.text_codes (cellstr (lcol(:,c)), ... cellstr (rcol(:,c))); endfor endif ## A missing categorical or string value equals nothing, itself ## included, as a NaN does; a cellstr's '' is text like any other. lp = double (lp); rp = double (rp); lp(text_missing (lcol)) = NaN; rp(text_missing (rcol)) = NaN; case 'datetime' lp = tabular.datetime_to_datenum (lcol); rp = tabular.datetime_to_datenum (rcol); case 'duration' lp = days (lcol); rp = days (rcol); case 'calendarDuration' lp = lcol.proxyArray; rp = rcol.proxyArray; case 'numeric' lp = double (lcol); rp = double (rcol); case 'tabular' ## A nested table keys on its own variables, matched by name lnames = subsref (lcol, substruct ('.', 'Properties')).VariableNames; rnames = subsref (rcol, substruct ('.', 'Properties')).VariableNames; if (! isequal (sort (lnames), sort (rnames))) errmsg = "key variables have incompatible types."; return; endif for j = 1:numel (lnames) [a, b, errmsg] = tabular.key_col_proxy ( ... subsref (lcol, substruct ('.', lnames{j})), ... subsref (rcol, substruct ('.', lnames{j}))); if (! isempty (errmsg)) lp = []; rp = []; return; endif lp = [lp, a]; rp = [rp, b]; endfor endswitch if (size (lp, 2) != size (rp, 2)) lp = []; rp = []; errmsg = "key variables have incompatible sizes."; endif endfunction ## Validate an 'empty' size specification and return it as [rows, vars]. ## CLSTYPE names the class for the one message that has to say it. ## Returns an errmsg body (empty on success) emitted by the caller under ## its own name. function [sz, errmsg] = emptySize (clstype, args) sz = [0, 0]; errmsg = ''; if (numel (args) == 1) sz = args{1}; if (! (isnumeric (sz) && isvector (sz) && ! isempty (sz))) errmsg = 'SZ must be a numeric vector.'; return; endif sz = sz(:)'; if (isscalar (sz)) sz = [sz, sz]; endif elseif (numel (args) > 1) isnum = cellfun (@(x) isnumeric (x) && isscalar (x), args); if (! all (isnum)) errmsg = 'each dimension must be a numeric scalar.'; return; endif sz = cell2mat (args(:)'); endif if (numel (sz) > 2) if (any (sz(3:end) != 1)) errmsg = sprintf ('a %s has only two dimensions.', clstype); return; endif sz = sz(1:2); endif if (any (sz < 0) || any (sz != fix (sz))) errmsg = 'each dimension must be a non-negative integer.'; return; endif if (all (sz != 0)) errmsg = 'at least one dimension must be zero.'; return; endif endfunction ## The variables joined side by side as MATLAB's concatenation joins ## them. OK is false for a combination MATLAB refuses, and for one that ## core Octave would join only by changing a type MATLAB keeps apart, ## such as a logical beside a char or a number beside a categorical. function [A, ok] = join_vars (vals) A = []; kinds = cellfun (@tabular.join_kind, vals, 'UniformOutput', false); has = @(k) any (strcmp (kinds, k)); only = @(ks) all (ismember (kinds, ks)); if (has ('string')) ## Numbers, logicals, text and cells become strings. ok = only ({'string', 'float', 'integer', 'logical', 'char', ... 'cell', 'missing'}); elseif (has ('cell')) ok = only ({'cell'}); elseif (has ('calendarDuration')) ok = only ({'calendarDuration', 'duration', 'float', 'logical', ... 'missing'}) ... && ! any (cellfun (@(x) isa (x, 'single'), vals)); elseif (has ('duration')) ok = only ({'duration', 'float', 'logical', 'missing'}) ... && ! any (cellfun (@(x) isa (x, 'single'), vals)); elseif (has ('categorical')) ok = only ({'categorical', 'missing'}); if (ok) iscat = strcmp (kinds, 'categorical'); ord = cellfun (@isordinal, vals(iscat)); ok = all (ord) || ! any (ord); endif elseif (has ('datetime')) ok = only ({'datetime', 'missing'}); if (ok) isdt = strcmp (kinds, 'datetime'); zoned = cellfun (@(x) ! isempty (x.TimeZone), vals(isdt)); ok = all (zoned) || ! any (zoned); endif elseif (has ('char')) ok = only ({'char', 'float', 'integer'}); elseif (has ('missing')) ok = only ({'missing', 'float'}); elseif (only ({'float', 'integer', 'logical'})) ok = true; else ## Structs, nested tables, and any other class join only with their ## own kind. ok = all (strcmp (kinds, kinds{1})); endif if (! ok) return; endif ## A logical beside a duration counts days, as a number does. if (has ('duration') || has ('calendarDuration')) islg = strcmp (kinds, 'logical'); vals(islg) = cellfun (@double, vals(islg), 'UniformOutput', false); endif ## A number beside a char becomes its character, silently as in MATLAB. warning ('off', 'Octave:num-to-str', 'local'); try A = cat (2, vals{:}); catch A = []; ok = false; end_try_catch endfunction ## The kind of a variable's type, as 'join_vars' groups them. function kind = join_kind (val) if (isfloat (val)) kind = 'float'; elseif (isinteger (val)) kind = 'integer'; else kind = class (val); endif endfunction ## One header block entry as a column cell, whether it came in as a single ## character vector or as a column of them (a nested table or a struct). function out = header_entry (e) if (iscell (e)) out = e(:); else out = {e}; endif endfunction ## Map a variable type name to the ODS cell value type used by 'table2ods'. ## Numeric types become 'float', logical becomes 'boolean', datetime and ## duration map to the native 'date' and 'time' types, and everything else ## (text, categorical, calendarDuration, cell) is written as a 'string'. ## -*- texinfo -*- ## The variable-type string a 'datetime' or 'duration' is exported under. ## ## A zone-aware 'datetime' carries its 'TimeZone' after a space and either ## carries its display format after a '|', so that both survive the round ## trip. Any other value is named by its class alone. It is one function ## because the row-label column and the variables must agree on the ## spelling, and they are built in different places. function typestr = value_type_string (val) typestr = class (val); if (isa (val, 'datetime') && ! isempty (val.TimeZone)) typestr = [typestr, ' ', val.TimeZone]; endif if ((isa (val, 'datetime') || isa (val, 'duration')) ... && ! isempty (val.Format)) typestr = [typestr, '|', val.Format]; endif endfunction function vt = ods_value_type (typestr) ## Row times are tagged by kind ahead of their own type, and are written ## as the native date or time cells that variable would be: a ## spreadsheet showing the row times as text beside a datetime variable ## shown as a date would be the odd outcome. if (strncmp (typestr, 'RowTimes|', 9)) typestr = typestr(10:end); endif ## A zone-aware datetime carries its TimeZone in the type ## ('datetime '), and either carries its display format after a '|'. if (strncmp (typestr, 'datetime', 8)) vt = 'date'; return; endif if (strncmp (typestr, 'duration', 8)) vt = 'time'; return; endif switch (typestr) case 'logical' vt = 'boolean'; case 'missing' vt = 'string'; # every cell is bare, the type restores it case {'double', 'single', 'int8', 'int16', 'int32', 'int64', ... 'uint8', 'uint16', 'uint32', 'uint64'} vt = 'float'; otherwise vt = 'string'; endswitch endfunction ## Prepare the flat value/name/type cell arrays produced by ## 'table2cellarrays' for the MATLAB-compatible 'writetable' output: strip ## or keep the leading row names column (which carries an empty variable ## name) per WRITEROWNAMES, and de-duplicate the shared names of a ## multicolumn variable with _1, _2, ... suffixes, matching MATLAB. function [names, V, T] = writetable_prep (V, N, T, writeRowNames) hasRN = (! isempty (N) && isempty (N{1})); rnCol = {}; if (hasRN) rnCol = V(:,1); V(:,1) = []; N(:,1) = []; T(:,1) = []; endif names = {}; c = 1; n = numel (N); while (c <= n) c2 = c; while (c2 < n && strcmp (N{c2+1}, N{c})) c2 += 1; endwhile k = c2 - c + 1; if (k == 1) names{end+1} = N{c}; else for j = 1:k names{end+1} = sprintf ("%s_%d", N{c}, j); endfor endif c = c2 + 1; endwhile if (writeRowNames && hasRN) V = [rnCol, V]; names = [{'Row'}, names]; T = [{'cellstr'}, T]; endif endfunction ## Build a single-column grouping proxy for one grouping variable COL: a ## numeric matrix P (one row per element) whose sort order matches COL's ## value order, so that 'unique (P, "rows")' recovers the sorted unique ## groups, together with a logical MISS mask flagging the elements that ## findgroups treats as missing (NaN/NaT//''/). Returns ## an errmsg body (empty on success) emitted by the caller under its own ## name. function [p, miss, errmsg] = group_col_proxy (col) p = []; miss = []; errmsg = ''; ## A group is one value per row, as in MATLAB; a character matrix holds ## one text value per row and groups by it. if (columns (col) != 1 && ! ischar (col)) errmsg = strcat ("a grouping variable must have one column; use", ... " splitvars to group by its columns."); return; endif if (isa (col, 'categorical')) ## Categorical groups follow category order (ordinal or reordered), ## which the underlying category codes encode; maps to NaN. p = double (col)(:); miss = isnan (p); return; endif k = tabular.key_kind (col); ## A nested table keys a join or a set operation, but does not group, ## as in MATLAB. if (isempty (k) || strcmp (k, 'tabular')) errmsg = sprintf ("unsupported grouping variable type '%s'.", ... class (col)); return; endif switch (k) case 'text' c = cellstr (col); c = c(:); miss = cellfun (@isempty, c); [~, ~, ic] = unique (c); p = ic(:); case 'datetime' p = tabular.datetime_to_datenum (col)(:); miss = isnan (p); case 'duration' p = days (col)(:); miss = isnan (p); case 'calendarDuration' p = col.proxyArray; miss = any (isnan (p), 2); case 'numeric' p = double (col)(:); miss = isnan (p); endswitch endfunction ## Group table rows by the grouping-variable columns GRPCOLS (a cell array ## of variable values, one per grouping variable), using 'group_col_proxy' ## on each. Returns G, an n-by-1 vector of group numbers (NaN for rows ## holding a missing value in any grouping variable), NGROUPS, the number of ## groups, REPROWS, a representative row index per group in sorted group ## order, and an errmsg body (empty on success) emitted by the caller. function [G, ngroups, repRows, errmsg] = group_table_rows (grpCols) errmsg = ''; ngroups = 0; repRows = []; n = size (grpCols{1}, 1); P = []; miss = false (n, 1); for j = 1:numel (grpCols) [p, m, e] = tabular.group_col_proxy (grpCols{j}); if (! isempty (e)) G = []; errmsg = e; return; endif P = [P, p]; miss = miss | m; endfor G = NaN (n, 1); keep = find (! miss); if (! isempty (keep)) [~, ia, ic] = unique (P(keep,:), "rows"); G(keep) = ic; repRows = keep(ia); ngroups = numel (ia); endif endfunction ## Build the grouping-variable columns of a grouped apply output from the ## grouping-variable values GRPCOLS: GCOLS holds the value of each grouping ## variable at the representative rows REPROWS, and GCOUNT the number of ## rows in each group, derived from the group-number vector G. function [gcols, gcount] = group_output_cols (grpCols, G, repRows) ngroups = numel (repRows); gcols = cell (1, numel (grpCols)); for p = 1:numel (grpCols) gcols{p} = grpCols{p}(repRows,:); endfor gcount = accumarray (G(! isnan (G)), 1, [ngroups, 1]); endfunction ## Validate an 'IncludedEdge' binning option VAL for method CALLER, ## returning it lowercased as 'left' or 'right'. function e = check_included_edge (caller, val) if (isa (val, 'string') && isscalar (val)) val = char (val); endif if (! (ischar (val) && isrow (val) ... && any (strcmpi (val, {'left', 'right'})))) error ("%s: 'IncludedEdge' must be 'left' or 'right'.", caller); endif e = lower (val); endfunction ## Group table rows for 'groupsummary' by the grouping-variable values ## GRPCOLS, treating each grouping variable's missing values as a single ## group value. Returns G, an n-by-1 vector of group numbers (1..NGROUPS); ## NGROUPS; REPROWS, a representative row index per group; and an errmsg ## body emitted by the caller. Groups are sorted by grouping value with ## missing groups last. When INCMISS is false, rows holding a missing ## grouping value are dropped (labelled NaN in G and excluded from ## NGROUPS/REPROWS). function [G, ngroups, repRows, errmsg] = gs_group_rows (grpCols, incMiss) errmsg = ''; G = []; ngroups = 0; repRows = []; n = size (grpCols{1}, 1); KEY = []; SORT = []; anyMiss = false (n, 1); for j = 1:numel (grpCols) [p, m, e] = tabular.group_col_proxy (grpCols{j}); if (! isempty (e)) errmsg = e; return; endif pc = p; ## collapse all missing values of this variable pc(m,:) = 0; KEY = [KEY, pc, double(m)]; sp = p; sp(m,:) = Inf; # sort missing groups last SORT = [SORT, sp]; anyMiss = anyMiss | m; endfor [~, ia, ic] = unique (KEY, "rows"); ng = numel (ia); grpMiss = anyMiss(ia); [~, ord] = sortrows (SORT(ia,:)); reps = ia(ord); grpMiss = grpMiss(ord); pos = zeros (ng, 1); pos(ord) = 1:ng; G = pos(ic); if (! incMiss && any (grpMiss)) keep = find (! grpMiss); newId = NaN (ng, 1); newId(keep) = 1:numel (keep); G = newId(G); reps = reps(keep); ng = numel (keep); endif ngroups = ng; repRows = reps; endfunction ## Transform one data variable COL group by group for 'grouptransform', ## applying METHOD (a transform-name char vector or a function handle) to ## each group's slice and returning OUT, the transformed values the same ## size as COL. G is the n-by-1 group-number vector (1..NG), every row ## assigned to a group. A function handle must return a single row ## (broadcast) or one row per group row. Returns an errmsg body (empty on ## success) emitted by the caller. function [out, errmsg] = gt_transform_col (method, col, G, ng) out = []; errmsg = ''; if (! (isnumeric (col) || islogical (col))) errmsg = sprintf (strcat ("grouptransform requires numeric or", ... " logical data; got '%s'."), class (col)); return; endif x = double (col); out = x; for g = 1:ng rows = find (G == g); if (isempty (rows)) continue; endif slice = rowsof (x, rows); if (is_function_handle (method)) r = method (slice); if (! (isnumeric (r) || islogical (r))) errmsg = "the transform function must return a numeric result."; out = []; return; endif if (size (r, 1) == 1) r = repmat (r, numel (rows), 1); endif if (! isequal (size (r), size (slice))) errmsg = strcat ("the transform function must return a", ... " result the same size as the group, or a", ... " single row."); out = []; return; endif out(rows,:) = r; else for c = 1:columns (slice) out(rows,c) = tabular.gt_apply_named (method, slice(:,c)); endfor endif endfor endfunction ## Apply a single named transform METHOD to the column vector X (a group's ## slice of one data variable), returning the transformed values V the same ## size as X. NaN values are omitted when computing the group statistics; ## the centring and scaling methods leave NaN in place, while ## 'meanfill'/'linearfill' fill them. function v = gt_apply_named (method, x) nan = isnan (x); xo = x(! nan); switch (method) case 'meancenter' v = x - mean (xo); case 'zscore' v = (x - mean (xo)) / std (xo); case 'norm' v = x / norm (xo); case 'rescale' mn = min (xo); mx = max (xo); v = (x - mn) / (mx - mn); case 'meanfill' v = x; v(nan) = mean (xo); case 'linearfill' v = tabular.gt_linearfill (x); endswitch endfunction ## Fill the missing values of the column vector X by linear interpolation ## over the non-missing positions, leaving leading and trailing missing ## values (and any group with fewer than two non-missing values) unchanged. function v = gt_linearfill (x) v = x; idx = find (! isnan (x)); if (numel (idx) >= 2) pos = (1:numel (x))'; vi = interp1 (idx, x(idx), pos, "linear"); fill = isnan (x) & pos > idx(1) & pos < idx(end); v(fill) = vi(fill); endif endfunction ## Group the rows of 'groupsummary'/'groupcounts' by the grouping-variable ## values GRPCOLS (already binned when a GROUPBINS argument was given). ## Returns G, the n-by-1 group numbers (NaN for an excluded row), NG the ## number of groups, GCOLS a 1-by-K cell of the typed grouping-variable ## output columns (one value per group), and an errmsg body emitted by the ## caller. When INCEMPTY is true the unused categories of a categorical (or ## binned) grouping variable contribute empty groups, built from the full ## level machinery; otherwise only the observed groups are returned, in ## ascending grouping-value order with missing groups last. function [G, ng, gcols, errmsg] = gs_grouping (grpCols, incMiss, incEmpty) errmsg = ''; gcols = {}; K = numel (grpCols); n = size (grpCols{1}, 1); if (incEmpty) [G, ng, lvlOf, levVals, ~, errmsg] = ... tabular.pivot_dimension (grpCols, n, incMiss, true); if (! isempty (errmsg)) G = []; ng = 0; return; endif gcols = cell (1, K); for j = 1:K gcols{j} = levVals{j}(lvlOf(:,j), :); endfor else [G, ng, repRows, errmsg] = tabular.gs_group_rows (grpCols, incMiss); if (! isempty (errmsg)) return; endif gcols = cell (1, K); for j = 1:K gcols{j} = grpCols{j}(repRows, :); endfor endif endfunction ## Group the rows of one 'pivot' dimension (rows or columns) defined by the ## grouping-variable columns GRPCOLS (a cell array, empty for an omitted ## dimension). N is the table height. Returns GID, an n-by-1 group index ## per row (NaN when the row is excluded), NG, the number of groups, LVLOF, ## an ng-by-K matrix of per-variable level indices for each group, LEVVALS, ## a 1-by-K cell of the per-variable typed level values from 'pivot_levels', ## MISSLVLS, a 1-by-K cell of the per-variable missing-level logical flags, ## and an errmsg body (empty on success). When INCEMPTY is true the groups ## span the full Cartesian product of the variables' levels (so unused ## combinations appear as empty groups); otherwise only the observed ## combinations are kept, sorted in ascending level order with the first ## variable varying slowest. function [gid, ng, lvlOf, levVals, missLvls, errmsg] = ... pivot_dimension (grpCols, n, incMiss, incEmpty) errmsg = ''; K = numel (grpCols); if (K == 0) ## An omitted dimension is a single group holding every row. gid = ones (n, 1); ng = 1; lvlOf = zeros (1, 0); levVals = {}; missLvls = {}; return; endif idxAll = NaN (n, K); levVals = cell (1, K); missLvls = cell (1, K); sizes = zeros (1, K); for j = 1:K [idx, lv, ml, errmsg] = tabular.pivot_levels (grpCols{j}, ... incMiss, incEmpty); if (! isempty (errmsg)) gid = []; ng = 0; lvlOf = []; return; endif idxAll(:,j) = idx; levVals{j} = lv; missLvls{j} = ml; sizes(j) = size (lv, 1); endfor gid = NaN (n, 1); if (incEmpty) ## Full Cartesian product, first variable slowest (most significant). ng = prod (sizes); lvlOf = ones (ng, K); period = 1; for j = K:-1:1 lvlOf(:,j) = mod (floor ((0:ng-1)' / period), sizes(j)) + 1; period = period * sizes(j); endfor valid = all (! isnan (idxAll), 2); lin = zeros (n, 1); period = 1; for j = K:-1:1 col = idxAll(:,j); col(isnan (col)) = 1; lin = lin + (col - 1) * period; period = period * sizes(j); endfor gid(valid) = lin(valid) + 1; else ## Observed combinations only, in ascending level order. valid = all (! isnan (idxAll), 2); if (! any (valid)) ng = 0; lvlOf = zeros (0, K); return; endif [u, ~, ic] = unique (idxAll(valid,:), "rows"); ng = size (u, 1); lvlOf = u; gid(valid) = ic; endif endfunction ## Build the level structure of one 'pivot' grouping variable COL. Returns ## IDX, an n-by-1 vector of level indices (1..L) for the rows of COL (NaN ## for a row holding a missing value when INCMISS is false, so that row is ## excluded from every group), LEVVALS, a typed column vector with one ## representative value per level used to build row labels and column names, ## MISSLVL, a 1-by-L logical flagging the missing level, and an errmsg body ## (empty on success). Levels are the sorted unique values of COL; a ## categorical variable uses its category order, and when INCEMPTY is true ## every category is a level even if unused in the data. A missing value ## forms one extra level, sorted last, when INCMISS. function [idx, levVals, missLvl, errmsg] = pivot_levels (col, ... incMiss, incEmpty) idx = []; levVals = []; missLvl = []; errmsg = ''; n = size (col, 1); [p, miss, errmsg] = tabular.group_col_proxy (col); if (! isempty (errmsg)) return; endif if (isa (col, 'categorical') && incEmpty) ## Every category is a level, in category order; codes are the proxy. cats = categories (col); L = numel (cats); idx = double (col)(:); levVals = categorical (cats(:), cats, 'Ordinal', isordinal (col)); missLvl = false (1, L); else ## Observed levels only, sorted by proxy value ascending. idx = NaN (n, 1); keep = find (! miss); if (isempty (keep)) levVals = rowsof (col, []); L = 0; missLvl = []; else [~, ia, ic] = unique (p(keep,:), "rows"); idx(keep) = ic; levVals = rowsof (col, keep(ia)); L = numel (ia); missLvl = false (1, L); endif endif ## A missing value forms one extra level, sorted last, when included. if (any (miss)) if (incMiss) L = L + 1; idx(miss) = L; mrow = find (miss, 1); levVals = [levVals; rowsof(col, mrow)]; missLvl = [missLvl, true]; else idx(miss) = NaN; endif endif endfunction ## Normalise the METHOD argument of 'groupsummary' into parallel cell ## arrays of method specs and the display names used to build output ## variable names. A name is matched without regard to case and may be ## abbreviated to any unambiguous prefix; 'all' stands for every named ## method, in the order they are reported. Returns an errmsg body (empty ## on success). function [methods, methNames, errmsg] = gs_normalise_methods (method) methods = {}; methNames = {}; errmsg = ''; if (isempty (method) && ! iscell (method) && ! ischar (method) && ! is_function_handle (method)) return; # no method requested: counts only endif if (is_function_handle (method) || (ischar (method) && isrow (method)) || isa (method, 'string')) items = {method}; elseif (iscell (method)) items = method(:)'; else errmsg = strcat ("METHOD must be a method name, a function", ... " handle, or a cell array of method names and", ... " function handles."); return; endif ## The order 'all' reports its methods in. known = {'mean', 'sum', 'min', 'max', 'range', 'median', 'mode', ... 'var', 'std', 'nummissing', 'nnz', 'numunique'}; nfun = 0; for k = 1:numel (items) it = items{k}; if (is_function_handle (it)) nfun++; methods{end+1} = it; methNames{end+1} = sprintf ("fun%d", nfun); elseif ((ischar (it) && isrow (it)) || (isa (it, 'string') && isscalar (it))) [nm, errmsg] = tabular.gs_match_method (char (it), known); if (! isempty (errmsg)) return; endif if (strcmp (nm, 'all')) methods = [methods, known]; methNames = [methNames, known]; else methods{end+1} = nm; methNames{end+1} = nm; endif else errmsg = strcat ("each method must be a method name or a", ... " function handle."); return; endif endfor endfunction ## One 'groupsummary' method name resolved against the known list plus ## 'all', matched without regard to case and accepting any unambiguous ## abbreviation, as MATLAB does. function [nm, errmsg] = gs_match_method (name, known) nm = ''; errmsg = ''; cand = [known, {'all'}]; lname = lower (name); if (isempty (lname)) errmsg = "a method name cannot be empty."; return; endif hit = find (strcmp (lname, cand)); if (isempty (hit)) hit = find (strncmp (lname, cand, numel (lname))); endif if (numel (hit) == 1) nm = cand{hit}; elseif (isempty (hit)) errmsg = sprintf ("'%s' is not a supported method name.", name); else errmsg = sprintf (strcat ("'%s' matches more than one method", ... " name."), name); endif endfunction ## Apply a single 'groupsummary' method M (a method-name char vector or a ## function handle) to the column slice X of one group, returning a row ## result V. Named methods omit missing values (except 'nummissing'); a ## function handle receives X unchanged and must return a single row. ## Returns an errmsg body (empty on success) emitted by the caller. function [v, errmsg] = gs_apply_method (m, x) v = []; errmsg = ''; if (is_function_handle (m)) v = m (x); if (size (v, 1) != 1) errmsg = "a function handle method must return a single row."; endif return; endif ## Type-agnostic counting methods. if (strcmp (m, 'nummissing')) v = sum (tabular.gs_missing_mask (x), 1); return; endif if (strcmp (m, 'numunique')) miss = tabular.gs_missing_mask (x); if (size (x, 2) == 1) v = numel (unique (x(! miss,:))); else v = zeros (1, size (x, 2)); for c = 1:size (x, 2) col = x(:,c); v(c) = numel (unique (col(! miss(:,c)))); endfor endif return; endif ## 'std' and 'var' are defined on floating point data, as they are ## everywhere else in Octave, so an integer variable is refused rather ## than silently widened to double. if (any (strcmp (m, {'std', 'var'})) && isinteger (x)) errmsg = sprintf (strcat ("named method '%s' is not supported for", ... " variables of type '%s'."), m, class (x)); return; endif ## The remaining named methods require numeric or logical data. if (! (isnumeric (x) || islogical (x))) errmsg = sprintf (strcat ("named method '%s' is not supported for", ... " variables of type '%s'; use a function", ... " handle."), m, class (x)); return; endif x = double (x); ## An empty group (e.g. an unused IncludeEmptyGroups category) takes the ## method's empty value: 0 for the additive 'sum'/'nnz', NaN otherwise. if (rows (x) == 0) if (any (strcmp (m, {'sum', 'nnz'}))) v = zeros (1, columns (x)); else v = NaN (1, columns (x)); endif return; endif nan = isnan (x); cnt = sum (! nan, 1); z = x; z(nan) = 0; switch (m) case 'sum' v = sum (z, 1); case 'mean' v = sum (z, 1) ./ cnt; case 'min' v = min (x, [], 1); case 'max' v = max (x, [], 1); case 'range' v = max (x, [], 1) - min (x, [], 1); case 'nnz' v = sum (x != 0 & ! nan, 1); case {'median', 'mode', 'var', 'std'} v = NaN (1, size (x, 2)); for c = 1:size (x, 2) col = x(! nan(:,c), c); if (! isempty (col)) switch (m) case 'median' v(c) = median (col); case 'mode' v(c) = mode (col); case 'var' v(c) = var (col); case 'std' v(c) = std (col); endswitch endif endfor endswitch endfunction ## Return a logical mask the size of X flagging its missing elements, used ## by the type-agnostic 'groupsummary' methods. Supports the numeric, ## logical, text, datetime, duration, calendarDuration, and categorical ## variable types. function mask = gs_missing_mask (x) if (isa (x, 'datetime')) mask = isnan (tabular.datetime_to_datenum (x)); elseif (isa (x, 'duration')) mask = isnan (days (x)); elseif (isa (x, 'calendarDuration')) mask = any (isnan (x.proxyArray), 2); elseif (isa (x, 'categorical') || isa (x, 'string')) mask = ismissing (x); elseif (iscellstr (x)) mask = cellfun (@isempty, x); elseif (islogical (x)) mask = false (size (x)); elseif (isnumeric (x)) mask = isnan (x); else mask = false (size (x)); endif endfunction ## Build the row keep-mask for 'groupfilter' by applying the filter function ## METHOD to each data variable's per-group slice. DATACOLS is a cell array ## of data-variable values; G the n-by-1 group numbers (1..NG), every row ## assigned to a group. For each group METHOD receives the variable's slice ## and must return a logical scalar (keep/drop the whole group) or a logical ## vector with one element per group row. The per-variable masks are ## combined with logical AND, so a row is kept only when the condition holds ## across all data variables. Returns KEEP (n-by-1 logical) and an errmsg ## body emitted by the caller. function [keep, errmsg] = gf_keep_mask (method, dataCols, G, ng) errmsg = ''; n = numel (G); keep = true (n, 1); for d = 1:numel (dataCols) col = dataCols{d}; for g = 1:ng rows = find (G == g); if (isempty (rows)) continue; endif r = method (rowsof (col, rows)); if (! (islogical (r) || isnumeric (r))) errmsg = "the filter function must return a logical result."; return; endif r = logical (r(:)); if (isscalar (r)) m = repmat (r, numel (rows), 1); elseif (numel (r) == numel (rows)) m = r; else errmsg = strcat ("the filter function must return a logical", ... " scalar or a logical vector with one element", ... " per group", ... " row."); return; endif keep(rows) = keep(rows) & m; endfor endfor endfunction ## Build the disambiguation suffixes used by the join methods when a ## variable name is shared by both tables. MATLAB derives them from the ## input argument names (e.g. inputs L and R give '_L'/'_R'); fall back to ## '_left'/'_right' when an input has no workspace name. function [lsuf, rsuf] = join_suffixes (leftName, rightName) if (isempty (leftName)) leftName = 'left'; endif if (isempty (rightName)) rightName = 'right'; endif lsuf = ['_', leftName]; rsuf = ['_', rightName]; endfunction ## The numeric proxies a join matches its key columns by. function [leftProxy, rightProxy, errmsg] = joinProxies (lCols, rCols) leftProxy = []; rightProxy = []; errmsg = ''; for k = 1:numel (lCols) [lp, rp, errmsg] = tabular.key_col_proxy (lCols{k}, rCols{k}); if (! isempty (errmsg)) return; endif leftProxy = [leftProxy, lp]; rightProxy = [rightProxy, rp]; endfor endfunction ## Return a logical mask, the same size as a table variable V, that flags ## the missing entries. Used by 'fillmissing'. Char arrays have no ## standard missing value and nested tables are treated as non-missing. ## Expand the 'constant' fill value into a 1-by-NVARS cell, one value per ## targeted variable (scalar broadcast, per-variable vector, or per-variable ## cell). Used by 'fillmissing'. function [mcvec, aggrFcn] = get_default_aggrFcn (vvals, nrows, ... aggrFcn, scope) ## Get columns of stacked variable vcols = size (vvals, 2); ## Handle each specific data type if (any (isa (vvals, {'single', 'double'}))) mcvec = NaN (nrows, vcols, 'like', vvals); if (isempty (aggrFcn)) # add default aggrevation function aggrFcn = @sum; else # check that it produces correct output tmpval = 1:5; try tmpval = aggrFcn (tmpval); catch aggrFcn = strcat ("invalid 'AggregationFunction'", ... " for numeric data."); end_try_catch if (! isscalar (tmpval)) aggrFcn = strcat ("'AggregationFunction'", ... " must return a scalar value."); endif endif elseif (isnumeric (vvals)) # integer types have no missing value, use 0 mcvec = zeros (nrows, vcols, 'like', vvals); if (isempty (aggrFcn)) # add default aggrevation function aggrFcn = @sum; else # check that it produces correct output tmpval = 1:5; try tmpval = aggrFcn (tmpval); catch aggrFcn = strcat ("invalid 'AggregationFunction'", ... " for numeric data."); end_try_catch if (! isscalar (tmpval)) aggrFcn = strcat ("'AggregationFunction'", ... " must return a scalar value."); endif endif elseif (isa (vvals, 'calendarDuration')) mcvec = repmat (calendarDuration ([0, 0, 0]), nrows, vcols); if (isempty (aggrFcn)) # add default aggrevation function aggrFcn = @unique; else # check that it produces correct output tmpval = calendarDuration (1:5, 0, 0); try tmpval = aggrFcn (tmpval); catch aggrFcn = strcat ("invalid 'AggregationFunction'", ... " for calendarDuration data."); end_try_catch if (! isscalar (tmpval)) aggrFcn = strcat ("'AggregationFunction'", ... " must return a scalar value."); endif endif elseif (isa (vvals, 'duration')) mcvec = repmat (duration ([0, 0, 0]), nrows, vcols); if (isempty (aggrFcn)) # add default aggrevation function aggrFcn = @unique; else # check that it produces correct output tmpval = duration (1:5, 0, 0); try tmpval = aggrFcn (tmpval); catch aggrFcn = strcat ("invalid 'AggregationFunction'", ... " for duration data."); end_try_catch if (! isscalar (tmpval)) aggrFcn = strcat ("'AggregationFunction'", ... " must return a scalar value."); endif endif elseif (islogical (vvals)) mcvec = false (nrows, vcols); if (isempty (aggrFcn)) # add default aggrevation function aggrFcn = @unique; else # check that it produces correct output tmpval = [false, false, true, true, false]; try tmpval = aggrFcn (tmpval); catch aggrFcn = strcat ("invalid 'AggregationFunction'", ... " for logical data."); end_try_catch if (! isscalar (tmpval)) aggrFcn = strcat ("'AggregationFunction'", ... " must return a scalar value."); endif endif elseif (isa (vvals, 'categorical')) mcvec = repmat (categorical (NaN), nrows, vcols); if (isempty (aggrFcn)) # add default aggrevation function aggrFcn = @unique; else # check that it produces correct output tmpval = categorical (1:5); try tmpval = aggrFcn (tmpval); catch aggrFcn = strcat ("invalid 'AggregationFunction'", ... " for categorical data."); end_try_catch if (! isscalar (tmpval)) aggrFcn = strcat ("'AggregationFunction'", ... " must return a scalar value."); endif endif else # all other data types (string, cellstr, datetime, ...) if (iscellstr (vvals)) vt = 'cellstr'; else vt = class (vvals); endif tmpl = table ('Size', [nrows, 1], 'VariableTypes', {vt}); ## Inside the class a dot reads a property, so the column is taken ## from where it is stored rather than by its name. mcvec = repmat (tmpl.VariableValues{1}, 1, vcols); if (isempty (aggrFcn)) # add default aggrevation function aggrFcn = @unique; endif endif ## Enforce a scalar aggregation result, erroring on e.g. conflicting ## non-numeric values under the default @unique, matching MATLAB. if (! ischar (aggrFcn)) baseFcn = aggrFcn; aggrFcn = @(x) tabular.enforce_scalar_aggr (baseFcn (x), scope); endif endfunction ## Error out when an unstack aggregation function returns a non-scalar ## value. function val = enforce_scalar_aggr (val, scope) if (size (val, 1) > 1) error (strcat ("%s: 'AggregationFunction' must return", ... " a scalar value."), scope); endif endfunction ## Append the 'Overall_' totals label LABEL as one extra element of ## the row-label column COL for 'pivot' IncludeTotals, keeping its type. OK ## is false when COL cannot hold the text label (a numeric, datetime, or ## duration row label), so the caller falls back to row-name labelling. function [col, ok] = pivot_append_label (col, label) ok = true; if (isa (col, 'categorical')) if (! iscategory (col, label)) col = addcats (col, label); endif col(end+1, 1) = label; elseif (isa (col, 'string')) col(end+1, 1) = label; elseif (iscellstr (col)) col(end+1, 1) = {label}; else ok = false; endif endfunction ## Compute one 'pivot' cell value: apply METHOD to the data of the rows ROWS ## of one row-and-column group. HASDV flags whether a data variable is ## given; DATAVALS holds its values. 'count' counts rows (or, with a data ## variable, the non-missing data values), 'percentage' is the row count as ## a percentage of TOTALASSIGNED, an empty cell takes the named method's ## empty value, and every other named method or function handle is applied ## through 'gs_apply_method'. Returns an errmsg body (empty on success) ## emitted by the caller. function [v, errmsg] = pivot_cell_value (method, hasDV, dataVals, rows, ... totalAssigned) errmsg = ''; if (ischar (method) && strcmp (method, 'count')) if (! hasDV) v = numel (rows); else m = tabular.gs_missing_mask (rowsof (dataVals, rows)); v = sum (! any (m, 2)); endif elseif (ischar (method) && strcmp (method, 'percentage')) v = 100 * numel (rows) / totalAssigned; elseif (isempty (rows)) ## An empty cell takes the method's empty value; a function handle is ## not invoked on an empty slice, the cell is left missing. if (ischar (method)) v = tabular.pivot_empty_value (method); else v = NaN; endif else [v, errmsg] = tabular.gs_apply_method (method, rowsof (dataVals, rows)); endif endfunction ## Return the value 'pivot' places in an empty cell for the named method M. function val = pivot_empty_value (m) if (any (strcmp (m, {'sum', 'nnz', 'nummissing', 'numunique'}))) val = 0; else val = NaN; endif endfunction ## Validate a logical-scalar 'pivot' option VAL named NAME, returning it as ## a logical scalar. function tf = pivot_logical_opt (name, val, cls) if (! (isscalar (val) && (islogical (val) || isnumeric (val)))) error ("%s.pivot: '%s' must be a logical scalar.", cls, name); endif tf = logical (val); endfunction ## Return a one-row missing value MV of the same type as X for 'pivot' ## Method 'none' empty cells; OK is false when the type has no missing ## value. function [mv, ok] = pivot_missing_scalar (x) ok = true; mv = []; mm = any (tabular.gs_missing_mask (x), 2); if (any (mm)) mv = rowsof (x, find (mm, 1)); elseif (isnumeric (x)) mv = nan (1, size (x, 2)); else ok = false; endif endfunction ## Build nested-table column variables for pivot 'OutputFormat','nested': ## group the flat data columns COLS (1-by-nC) by the column-grouping level ## hierarchy in LVLOF (nC-by-K level indices), producing one outer variable ## per level of the first column grouping variable -- each a nested table ## (recursively) whose variables are the next grouping variable's levels; ## the innermost level holds the data columns. LEVVALS/MISSLVLS/GNAMES ## supply the level labels. function [vars, names] = pivot_nest (cols, lvlOf, levVals, missLvls, gnames) [vars, names] = tabular.pivot_nest_level (cols, lvlOf, 1, levVals, ... missLvls, gnames); endfunction function [vars, names] = pivot_nest_level (cols, lvlOf, p, levVals, ... missLvls, gnames) K = size (lvlOf, 2); vars = {}; names = {}; for L = unique (lvlOf(:,p)(:)') mask = lvlOf(:,p) == L; if (missLvls{p}(L)) nm = sprintf ("", gnames{p}); else nm = tabular.pivot_value_name (levVals{p}, L); endif if (p == K) sub = cols(mask); ## leaf: one data column per full tuple vars{end+1} = sub{1}; else [sv, sn] = tabular.pivot_nest_level (cols(mask), ... lvlOf(mask,:), p + 1, ... levVals, missLvls, gnames); vars{end+1} = table (sv{:}, 'VariableNames', sn); endif names{end+1} = nm; endfor endfunction ## Build one 'pivot' output column for Method 'none' (rearrange without ## aggregating): column C of the pivot, length NR, of the same type as ## DATAVALS. Each cell must hold at most one value; an empty cell is filled ## with a missing value. RGID and CGID are the per-row group indices. ## Returns an errmsg body (empty on success) emitted by the caller. function [col, errmsg] = pivot_none_column (dataVals, rGid, cGid, c, nR) errmsg = ''; col = []; srcRow = ones (nR, 1); hasVal = false (nR, 1); for r = 1:nR rows = find (rGid == r & cGid == c); if (numel (rows) > 1) errmsg = strcat ("Method 'none' allows at most one value per", ... " cell, but a group holds several; specify an", ... " aggregating method."); return; elseif (numel (rows) == 1) srcRow(r) = rows; hasVal(r) = true; endif endfor col = rowsof (dataVals, srcRow); if (! all (hasVal)) [mv, ok] = tabular.pivot_missing_scalar (dataVals); if (! ok) errmsg = sprintf (strcat ("Method 'none' with empty cells is not", ... " supported for data of type '%s'."), ... class (dataVals)); col = []; return; endif col(! hasVal, :) = mv; endif endfunction ## Build the cellstr RowNames for a 'pivot' with RowLabelPlacement ## 'rownames' from the row-label columns ROWLABELCOLS (a 1-by-K cell of ## typed columns), joining the per-variable displayed values of each row ## with '_'. function rn = pivot_row_names (rowLabelCols) K = numel (rowLabelCols); nR = size (rowLabelCols{1}, 1); rn = cell (nR, 1); for r = 1:nR parts = cell (1, K); for j = 1:K parts{j} = tabular.pivot_value_name (rowLabelCols{j}, r); endfor rn{r} = strjoin (parts, '_'); endfor endfunction ## Map a scalar grouping value VAL to the character vector used as a 'pivot' ## column variable name (or part of one). Logical values render as 'true' ## or 'false', numeric values through 'num2str', and text, categorical, ## datetime, duration, and calendarDuration values through their displayed ## text. A missing value renders as '' so the resulting name is ## never empty. function s = pivot_value_name (col, k) ## A time is rendered as the whole set of levels renders it, not as the ## one value does on its own: midnight alone loses its time of day, ## which would name one column differently from its neighbours. if (isa (col, 'datetime') || isa (col, 'duration') || isa (col, 'calendarDuration')) c = cellstr (col); s = c{k}; if (isempty (s)) s = ''; endif return; endif val = rowsof (col, k); if (isa (val, 'categorical')) if (ismissing (val)) s = ''; else c = cellstr (val); s = c{1}; endif elseif (isa (val, 'string')) if (ismissing (val)) s = ''; else s = char (val); endif elseif (iscellstr (val)) s = val{1}; elseif (ischar (val)) s = val; elseif (islogical (val)) if (val) s = 'true'; else s = 'false'; endif elseif (isnumeric (val)) s = num2str (val); else s = char (val); endif if (isempty (s)) s = ''; endif endfunction ## Build the cell array of input arguments passed to FUNC for the rows ## selected by the logical mask ROWS, taken from the input-variable values ## INCOLS (a cell array of variable values). When SEPIN is true each ## variable's selected rows form a separate argument; otherwise they are ## joined into a single argument by 'join_vars'. When EXTRACTCELL is ## true the contents of cell-valued variables are extracted. function args = build_row_args (inCols, rows, sepIn, extractCell) vals = cell (1, numel (inCols)); for k = 1:numel (inCols) col = inCols{k}; ## Extraction unwraps a cell variable for the argument it becomes on ## its own. It says nothing about a row laid side by side into one ## argument, which is the variables as they are stored. if (sepIn && extractCell && iscell (col)) sub = col(rows); if (numel (sub) == 1) vals{k} = sub{1}; else vals{k} = vertcat (sub{:}); endif else vals{k} = rowsof (col, rows); endif endfor if (sepIn) args = vals; else args = {tabular.join_vars(vals)}; endif endfunction ## Return the name used to prefix 'varfun' output variables: the name of ## FUNC, or 'Fun' when FUNC is an anonymous function handle. function fname = apply_func_name (func) fstr = func2str (func); if (isempty (fstr) || fstr(1) == '@') fname = 'Fun'; else fname = fstr; endif endfunction ## Validate and normalise an OutputFormat value for the apply methods: map ## 'auto' to 'table' and accept 'table', 'uniform', and 'cell'. CALLER ## names the method for error messages. function fmt = check_output_format (caller, fmt, clsname) if (nargin < 3) clsname = 'table'; endif if (isa (fmt, 'string')) fmt = char (fmt); endif if (! (ischar (fmt) && isrow (fmt))) error ("%s: 'OutputFormat' must be a character vector.", caller); endif low = lower (fmt); ## A class answers to 'auto' and to its own name with an object of that ## class; the other class's name does not, there being no row times to ## invent. 'table' asked of a timetable means a plain table, which is ## how a result too long to carry row times is asked for. if (any (strcmp (low, {'auto', lower(clsname)}))) fmt = 'table'; elseif (strcmp (low, 'table')) if (strcmp (lower (clsname), 'table')) fmt = 'table'; else fmt = 'plaintable'; endif elseif (strcmp (low, 'uniform')) fmt = 'uniform'; elseif (strcmp (low, 'cell')) fmt = 'cell'; elseif (any (strcmp (low, {'table', 'timetable'}))) error ("%s: '%s' OutputFormat is not supported.", caller, low); else error ("%s: invalid 'OutputFormat' value '%s'.", caller, fmt); endif endfunction ## Call FUNC with the arguments ARGS, requesting NOUT outputs, and return ## them in a 1-by-max(NOUT,1) cell row. When ERRHANDLER is non-empty it is ## called with a struct describing any error thrown by FUNC (fields ## 'identifier', 'message', and 'index' set to IDX) followed by ARGS, and ## its outputs are used instead. WHAT names what FUNC was applied to, as ## in "the variable 'x'", and where it is given a failure is reported ## against it under SCOPE rather than being left to speak for itself. function out = apply_func (func, errHandler, idx, nout, args, scope, what) out = cell (1, max (nout, 1)); if (isempty (errHandler)) if (nargin < 7 || isempty (what)) [out{1:nout}] = func (args{:}); else try [out{1:nout}] = func (args{:}); catch error (strcat ("%s: applying the function '%s' to %s", ... " generated an error."), scope, ... tabular.apply_func_name (func), what); end_try_catch endif else try [out{1:nout}] = func (args{:}); catch err S = struct ('identifier', err.identifier, 'message', err.message, ... 'index', idx); [out{1:nout}] = errHandler (S, args{:}); end_try_catch endif endfunction endmethods endclassdef ## Whether a subscript is the colon that selects a whole dimension. ## Order row labels on their own, honouring the placement asked for the ## missing ones. Octave's sort already puts them last ascending and first ## descending, which is what 'auto' means, so only 'first' and 'last' move ## anything. ## The numeric stand-in that one variable's values sort and compare by. ## Callers phrase their own complaint, so an unsupported type comes back ## named rather than as a message. CM is the comparison method asked of ## numeric data, and is 'auto' where none applies. function [p, badtype] = valueProxy (v, CM) p = []; badtype = ''; if (isa (v, 'categorical')) p = double (v); elseif (isa (v, 'calendarDuration')) p = v.proxyArray; elseif (isa (v, 'datetime')) p = tabular.datetime_to_datenum (v); elseif (isa (v, 'duration')) p = days (v); elseif (isa (v, 'string')) c = cellstr (v); [~, ~, p] = __unique__ (c, 'rows'); ## A missing string has no value, so it keys as NaN: it equals nothing ## and sorts as missing values do. An empty string is a value. miss = any (ismissing (v), 2); if (any (miss)) p = double (p); p(miss) = NaN; endif elseif (iscellstr (v)) [~, ~, p] = __unique__ (v, 'rows'); elseif (ischar (v)) [~, ~, p] = __unique__ (v, 'rows'); elseif (iscell (v)) badtype = 'cell'; elseif (isnumeric (v)) if (strcmpi (CM, 'real') && iscomplex (v)) p = real (v); elseif (strcmpi (CM, 'abs') && isreal (v)) p = abs (v); else p = v; endif elseif (islogical (v)) p = v; elseif (isstruct (v)) badtype = 'struct'; elseif (isa (v, 'table') || isa (v, 'timetable')) ## Variables that cannot form one array come back as a cell array. try p = table2array (v); catch badtype = 'nested'; end_try_catch if (iscell (p)) p = []; badtype = 'nested'; endif else badtype = class (v); endif endfunction ## How 'sortrows' names a type it cannot compare. function msg = sortBadType (badtype) if (strcmp (badtype, 'nested')) msg = "cannot sort nested tables with mixed data types."; else msg = sprintf ("cannot sort variables of '%s' type.", badtype); endif endfunction ## How 'unique' names a type it cannot compare. function msg = uniqueBadType (badtype) if (strcmp (badtype, 'nested')) msg = strcat ("cannot find unique rows for nested tables with", ... " mixed data types."); else msg = sprintf (strcat ("cannot find unique rows for variables of", ... " '%s' type."), badtype); endif endfunction ## Impose 'EndValues' on the end gaps of a column. COL is the column after ## the fill method has run and COL0 the column before it, so a value the ## method placed in an end gap can be taken back out. A keyword takes the ## value of the anchor on the side it names and leaves the other side missing; ## a constant is written directly and must be assignable to the variable. function [col, filled] = apply_end_values (col, col0, m, filled, ... endVals, vname, cls) [head, tail] = end_gaps (m); if (! any (head | tail)) return; endif known = find (! m(:)); if (ischar (endVals)) switch (endVals) case 'none' col(head | tail) = col0(head | tail); filled(head | tail) = false; case 'previous' col(head) = col0(head); filled(head) = false; if (! isempty (known)) col(tail) = col(known(end)); filled(tail) = true; endif case 'next' col(tail) = col0(tail); filled(tail) = false; if (! isempty (known)) col(head) = col(known(1)); filled(head) = true; endif case 'nearest' if (isempty (known)) col(head | tail) = col0(head | tail); filled(head | tail) = false; else col(head) = col(known(1)); col(tail) = col(known(end)); filled(head | tail) = true; endif otherwise error (strcat ("%s.fillmissing: unsupported 'EndValues'", ... " option '%s'."), cls, endVals); endswitch else try col(head | tail) = endVals; catch error (strcat ("%s.fillmissing: the 'EndValues' constant", ... " cannot be", ... " assigned to table variable '%s'."), cls, vname); end_try_catch filled(head | tail) = true; endif endfunction ## Split a 'standardizeMissing' indicator into a numeric row vector NUMIND and a ## cellstr row TXTIND of text indicators. Used by 'standardizeMissing'. function [head, tail] = end_gaps (m) m = m(:); known = find (! m); if (isempty (known)) head = m; tail = false (size (m)); return; endif x = (1:numel (m))'; head = m & x < known(1); tail = m & x > known(end); endfunction ## Fill every missing entry of variable V (mask M) with the constant FV. Used ## by 'fillmissing'. VARNAME names the variable for error reporting. function [v, filled] = fill_constant (v, M, fv, varname, cls) filled = M; try if (iscellstr (v)) if (ischar (fv)) fv = {fv}; elseif (! (iscellstr (fv) && isscalar (fv))) error ("incompatible"); endif v(M) = fv; else v(M) = fv; endif catch error (strcat ("%s.fillmissing: the fill value is incompatible", ... " with variable '%s'."), cls, varname); end_try_catch endfunction ## For a column with logical missing mask M, return the source row index SI for ## each row: SI(i) is the row whose value should fill row i (0 when none is ## reachable). METHOD is 'previous', 'next', or 'nearest'. Used by ## 'fillmissing'. function [col, filled] = fill_interp (col, m, x, method) m = m(:); n = numel (m); filled = false (n, 1); known = ! m; if (sum (known) < 2) return; # need at least two anchors to interpolate endif x = x(:); xk = x(known); ## A datetime or a duration is interpolated in seconds and put back through ## the class's own arithmetic, so its type, format and time zone survive. A ## datetime has no natural zero, so it is measured from its first known ## entry. origin = []; if (isdatetime (col)) ## Measured from the first known entry, which is a row of the column and ## so is found by position. The sample points are values, not positions: ## a table's happen to run 1:n and a timetable's are seconds from its ## first row time, which start at zero and index nothing. kidx = find (known); origin = col(kidx(1)); yk = seconds (col(known) - origin); elseif (isduration (col)) yk = seconds (col(known)); else yk = double (col(known)); endif lo = xk(1); hi = xk(end); vals = NaN (n, 1); ## Interior gaps are interpolated; the end gaps are extrapolated, which is ## what 'EndValues' 'extrap' asks of this method. Any other value overrides ## them in the caller. ## Core 'interp1' has no 'makima', so that one method is served by the ## package's own copy, which extrapolates from the boundary polynomials ## and so answers both calls the same way. interior = m & x > lo & x < hi; if (any (interior)) if (strcmp (method, 'makima')) vals(interior) = __makima__ (xk, yk, x(interior)); else vals(interior) = interp1 (xk, yk, x(interior), method); endif endif ends = m & (x < lo | x > hi); if (any (ends)) if (strcmp (method, 'makima')) vals(ends) = __makima__ (xk, yk, x(ends)); else vals(ends) = interp1 (xk, yk, x(ends), method, 'extrap'); endif endif if (isdatetime (col)) col(m) = origin + seconds (vals(m)); elseif (isduration (col)) col(m) = seconds (vals(m)); else col(m) = vals(m); endif filled(m) = true; endfunction ## The leading and trailing runs of missing entries of a column with missing ## mask M. Everything before the first known entry is a leading gap and ## everything after the last is a trailing one. When nothing is known there ## is no anchor to sit between, so every entry is a leading gap, which is what ## a constant end value fills and what every keyword leaves alone. function si = fill_neighbor_idx (m, method, x) m = m(:); n = numel (m); idx = (1:n)'; vp = idx; vp(m) = 0; sp = cummax (vp); # previous non-missing index (0 if none) vn = idx; vn(m) = n + 1; sn = flipud (cummin (flipud (vn))); sn(sn == n + 1) = 0; # next non-missing index (0 if none) switch (method) case 'previous' si = sp; case 'next' si = sn; case 'nearest' si = idx; for i = find (m)' if (sp(i) == 0 && sn(i) == 0) si(i) = 0; elseif (sp(i) == 0) si(i) = sn(i); elseif (sn(i) == 0) si(i) = sp(i); elseif (x(sn(i)) - x(i) <= x(i) - x(sp(i))) si(i) = sn(i); # tie favors the later (next) value else si(i) = sp(i); endif endfor endswitch endfunction ## Linearly interpolate the missing entries of numeric column COL (mask M). ## ENDVALS controls leading/trailing gaps ('extrap', 'none', or a numeric ## scalar). Returns the filled column and a logical mask of filled rows. Used ## by 'fillmissing'. function fvals = resolve_const_values (constVal, nvars, cls) if (iscell (constVal)) if (isscalar (constVal)) fvals = repmat (constVal, 1, nvars); elseif (numel (constVal) == nvars) fvals = reshape (constVal, 1, nvars); else error (strcat ("%s.fillmissing: a cell array of fill values must", ... " have one element per targeted variable."), cls); endif elseif (ischar (constVal) || isscalar (constVal)) fvals = repmat ({constVal}, 1, nvars); elseif (isvector (constVal) && numel (constVal) == nvars) fvals = num2cell (reshape (constVal, 1, nvars)); else error (strcat ("%s.fillmissing: the fill value must be a scalar, a", ... " vector with one element per targeted variable, or a", ... " cell array of per-variable values."), cls); endif endfunction ## Replace entries of variable V that match an indicator of its own type with ## the standard missing value of V's class. Numbers apply to floating-point ## variables, text to text variables, durations to durations and datetimes to ## datetimes. Used by 'standardizeMissing'. function v = std_apply_indicator (v, ind) if (isfloat (v)) if (! isempty (ind.num)) v(ismember (v, ind.num)) = NaN; endif elseif (iscellstr (v)) if (! isempty (ind.txt)) v(ismember (v, ind.txt)) = {''}; endif elseif (isa (v, 'string')) if (! isempty (ind.txt)) v(ismember (cellstr (v), ind.txt)) = string (missing); endif elseif (isa (v, 'categorical')) if (! isempty (ind.txt)) v(ismember (cellstr (v), ind.txt)) = categorical (missing); endif elseif (isa (v, 'duration')) if (! isempty (ind.dur)) v(ismember (v, ind.dur)) = NaN; endif elseif (isa (v, 'datetime')) if (! isempty (ind.dt)) v(ismember (v, ind.dt)) = NaT; endif endif ## logical, integer, calendarDuration, and nested table variables have no ## compatible standard missing value here; pass through. endfunction ## Split the indicator of 'standardizeMissing' by type into the fields 'num', ## 'txt', 'dur' and 'dt' of IND. Datetime indicators with and without a time ## zone cannot be compared with the same variable, so they are refused ## together. function ind = std_normalize_indicator (indicator, cls) if (iscell (indicator) && ! iscellstr (indicator)) elems = indicator(:)'; else elems = {indicator}; endif num = txt = dur = dt = {}; for i = 1:numel (elems) e = elems{i}; if (ischar (e)) txt{end+1} = {e}; elseif (isa (e, 'string') || iscellstr (e)) txt{end+1} = cellstr (e)(:)'; elseif (isnumeric (e) || islogical (e)) num{end+1} = double (e(:)'); elseif (isa (e, 'duration')) dur{end+1} = e(:); elseif (isa (e, 'datetime')) dt{end+1} = e(:); elseif (iscell (indicator)) error (strcat ("%s.standardizeMissing: unsupported indicator", ... " element of class '%s'."), cls, class (e)); else error (strcat ("%s.standardizeMissing: invalid INDICATOR of class", ... " '%s'."), cls, class (e)); endif endfor ind.num = [num{:}]; ind.txt = [txt{:}]; ind.dur = vertcat (dur{:}); if (isempty (dt)) ind.dt = []; else zoned = cellfun (@(x) ! isempty (x.TimeZone), dt); if (any (zoned) && ! all (zoned)) error (strcat ("%s.standardizeMissing: cannot combine datetime", ... " indicators with and without a time zone."), cls); endif ind.dt = vertcat (dt{:}); endif endfunction ## One category value per column statistic, taken by its code. function c = catOf (cats, code, proto) c = proto([]); for i = 1:numel (code) if (isnan (code(i)) || code(i) < 1 || code(i) > numel (cats)) c(i) = proto(1); c(i) = missing; else c(i) = categorical (cats(code(i)), cats, 'Ordinal', isordinal (proto)); endif endfor endfunction ## A duration of X seconds carrying the format asked for. function d = durOf (x, fmt) d = seconds (x); d.Format = fmt; endfunction ## Column means and sample deviations with the missing entries left out. function m = colmean (x) m = NaN (1, columns (x)); for c = 1:columns (x) k = x(! isnan (x(:,c)), c); if (! isempty (k)) m(c) = sum (k) / numel (k); endif endfor endfunction function sd = colstd (x) sd = NaN (1, columns (x)); for c = 1:columns (x) k = x(! isnan (x(:,c)), c); if (numel (k) > 1) mu = sum (k) / numel (k); sd(c) = sqrt (sum ((k - mu) .^ 2) / (numel (k) - 1)); elseif (numel (k) == 1) ## One observation deviates from itself by nothing. sd(c) = 0; endif endfor endfunction ## How one variable is described on its own line of a summary. function out = summaryVarLine (name, val, e) t = class (val); nr = size (val, 1); nc = size (val, 2); ## What a type says about itself joins the units and the description inside ## one parenthesis, in that order: 'categorical (none, 2 categories, a ## label)' rather than a bracket apiece. note = ''; if (isa (val, 'table') || isa (val, 'timetable') || nr == 0) d = sprintf ('%dx%d %s', nr, nc, t); elseif (nc > 1) d = sprintf ('%d-column %s', nc, t); elseif (islogical (val)) d = 'logical'; note = sprintf ('%d true', sum (val)); elseif (isa (val, 'categorical')) d = 'categorical'; if (isordinal (val)) d = 'ordinal categorical'; endif note = sprintf ('%d categories', numel (categories (val))); elseif (iscellstr (val)) d = 'cell array of character vectors'; else d = t; endif extra = {}; if (! isempty (e.Units)) extra{end+1} = e.Units; endif if (! isempty (note)) extra{end+1} = note; endif if (! isempty (e.Description)) extra{end+1} = e.Description; endif if (! isempty (extra)) d = sprintf ('%s (%s)', d, strjoin (extra, ', ')); endif out = sprintf (' %s: %s', name, d); endfunction ## The rows of the statistics block. A multi-column variable contributes one ## row per column, named the way a subscript would reach it, and a column of ## the block is kept only where something reports that statistic. function [labels, cells, cols] = summaryStatRows (s, names) stats = {'NumMissing', 'Min', 'Median', 'Max', 'Mean', 'Std'}; keep = false (1, numel (stats)); labels = {}; cells = {}; for i = 1:numel (names) e = s.(names{i}); if (! isfield (e, 'NumMissing')) continue; endif nc = max (1, numel (e.NumMissing)); for c = 1:nc if (nc == 1) labels{end+1} = names{i}; else labels{end+1} = sprintf ('%s(:,%d)', names{i}, c); endif row = cell (1, numel (stats)); for k = 1:numel (stats) row{k} = ''; if (isfield (e, stats{k})) v = e.(stats{k}); if (numel (v) >= c) row{k} = summaryCell (v(c)); keep(k) = true; endif endif endfor cells{end+1} = row; endfor endfor cols = stats(keep); for i = 1:numel (cells) cells{i} = cells{i}(keep); endfor endfunction ## One statistic rendered for the block: a whole number bare, anything else ## to four decimals, and a typed value through its own display. function out = summaryCell (v) try if (isnumeric (v)) if (isreal (v) && ! isnan (v) && ! isinf (v) && v == fix (v)) out = sprintf ('%d', v); else out = strtrim (sprintf ('%.4f', v)); if (isnan (v)) out = 'NaN'; elseif (isinf (v)) out = sprintf ('%g', v); elseif (! isreal (v)) out = strtrim (num2str (v)); endif endif elseif (islogical (v)) out = sprintf ('%d', v); elseif (ischar (v)) out = v; else out = char (string (v)); endif catch out = ''; end_try_catch endfunction ## Lay the statistics block out in columns, each as wide as the widest thing ## in it and its heading, values to the right. function summaryStatTable (labels, cells, cols) lw = 0; for i = 1:numel (labels) lw = max (lw, numel (labels{i})); endfor ## The field is as wide as the widest value in it and no wider: a heading ## wider than that overhangs rather than stretching the field. cw = zeros (1, numel (cols)); for k = 1:numel (cols) for i = 1:numel (cells) cw(k) = max (cw(k), numel (cells{i}{k})); endfor endfor ## A column is as wide as the wider of its heading and its values. The ## values are right-aligned in their own field and the heading is centred ## over it, so a heading wider than anything under it, 'NumMissing' over a ## column of single digits, overhangs on both sides rather than one. bw = zeros (1, numel (cols)); for k = 1:numel (cols) bw(k) = max (cw(k), numel (cols{k})); endfor gap = 4; line = blanks (4 + lw); for k = 1:numel (cols) line = [line, blanks(gap), padcentre(cols{k}, bw(k))]; gap = 8; endfor fprintf ('%s\n', deblank (line)); for i = 1:numel (cells) line = [' ', padright(labels{i}, lw)]; gap = 4; for k = 1:numel (cols) field = padleft (cells{i}{k}, cw(k)); line = [line, blanks(gap), padcentre(field, bw(k))]; gap = 8; endfor fprintf ('%s\n', deblank (line)); endfor endfunction function out = padleft (str, w) out = [blanks(w - numel (str)), str]; endfunction ## Centre a field in a wider block, the extra space favouring the left where ## it cannot be split evenly. function out = padcentre (str, w) pad = w - numel (str); if (pad <= 0) out = str; return; endif left = floor (pad / 2); out = [blanks(left), str, blanks(pad - left)]; endfunction function out = padright (str, w) out = [str, blanks(w - numel (str))]; endfunction function index = labelOrder (labels, direction, MP) [~, index] = sort (labels, direction); index = index(:); mask = keyMissingMask (labels); TFvec = mask(index); if (any (TFvec) && ! all (TFvec)) is_nan = index(TFvec); no_nan = index(! TFvec); if (strcmpi (MP, 'first')) index = [is_nan; no_nan]; elseif (strcmpi (MP, 'last')) index = [no_nan; is_nan]; endif endif endfunction ## One logical per row saying whether the sort key is missing there. Read ## off the key itself rather than off the object, so that a class without an ## 'ismissing' method can still be sorted, and so that a key which cannot be ## missing at all simply answers false. function mask = keyMissingMask (v) m = __varmissing__ (v); if (size (m, 2) > 1) m = any (m, 2); endif mask = m(:); endfunction function tf = is_colon_ref (ref) tf = false; if (isa (ref, 'string') && isscalar (ref)) ref = char (ref); endif if (ischar (ref) && isrow (ref)) tf = isequal (ref, ':'); endif endfunction ## Validate a VariableContinuity assignment of N elements and return it as a ## cell array of character vectors. A bare character vector is only valid ## when indexing individual variables, which is what ALLOWCHAR marks. ## CLSTYPE names the class the assignment was made on. function val = check_continuity (val, n, allowchar, clstype) if (ischar (val) && ! allowchar) error (strcat ("%s.subsasgn: to assign to the VariableContinuity", ... " property, use a string array or a cell array of", ... " character vectors. A character vector can be assigned", ... " only to an individual element of the property."), clstype); endif if (ischar (val) || isa (val, 'string')) val = cellstr (val); endif if (! iscellstr (val)) error (strcat ("%s.subsasgn: VariableContinuity must be a cell array", ... " of character vectors or a string array."), clstype); endif if (numel (val) != n) error (strcat ("%s.subsasgn: VariableContinuity property must have", ... " one element for each variable in the table."), clstype); endif if (! all (ismember (val, {'continuous', 'step', 'event', 'unset'}))) error (strcat ("%s.subsasgn: VariableContinuity property must be", ... " specified with 'continuous', 'step', 'event', or", ... " 'unset'."), clstype); endif val = val(:)'; endfunction ## Special function to convert a mixed cell array to cellstr array ## that keeps MATLAB like formatting for each type of element ## The first pair of the values VALS that cannot join, or the first and the ## last where every pair can but the whole set cannot. function [i, j] = stack_bad_pair (vals) n = numel (vals); for i = 1:n-1 for j = i+1:n [~, ok] = tabular.join_vars (vals([i, j])); if (! ok) return; endif endfor endfor i = 1; j = n; endfunction ## Which elements of the text key V are missing: undefined categorical ## values and missing strings. Character data and cellstr have none. function tf = text_missing (v) if (isa (v, 'categorical')) tf = isundefined (v); elseif (isa (v, 'string')) tf = ismissing (v); else tf = false (rows (v), 1); endif endfunction ## The rows IDX of the variable V. Indexing a tabular variable inside the ## class takes the built-in indexing, which sees one object, so it goes ## through the class's own 'subsref'. function out = rowsof (v, idx) if (isa (v, 'tabular')) out = subsref (v, substruct ('()', {idx, ':'})); else out = v(idx,:); endif endfunction ## The text of each cell of the variable DATA in a table display, one ## column of TEXTS for each display column, with how they align and how wide ## each column's widest text is. VARLEN is the width of the variable's name, ## which a cell array's texts are padded to. A value with no text form is ## shown as one placeholder per row naming its size and class. function [texts, align, W] = display_texts (data, varLen) nc = columns (data); texts = cell (rows (data), 0); W = []; if (isnumeric (data)) align = 'right'; for c = 1:nc if (isinteger (data)) t = arrayfun (@__intstr__, data(:,c), "UniformOutput", false); elseif (iscomplex (data)) ## A zero part is written unsigned re = real (data(:,c)); re(re == 0) = 0; im = imag (data(:,c)); im(im == 0) = 0; t = arrayfun (@(a, b) sprintf ("%g%+gi", a, b), re, im, ... "UniformOutput", false); else t = arrayfun (@(x) sprintf ("%g", x), data(:,c), ... "UniformOutput", false); endif [texts, W] = addtexts (texts, W, t); endfor elseif (islogical (data)) align = 'left'; for c = 1:nc t = repmat ({'false'}, size (data(:,c))); t(data(:,c)) = {'true'}; texts = [texts, t]; W = [W, 5]; endfor elseif (ischar (data)) align = 'left'; t = cell (rows (data), 1); for r = 1:rows (data) t{r} = sprintf ("'%s'", data(r,:)); endfor [texts, W] = addtexts (texts, W, t); elseif (iscellstr (data)) align = 'brace'; for c = 1:nc t = cellfun (@(x) sprintf ("'%s'", x), data(:,c), "UniformOutput", false); [texts, W] = addtexts (texts, W, t); W(end) += 2 * (rows (data) > 0); endfor elseif (iscell (data)) align = 'left'; for c = 1:nc [t, w] = mixedcell2str (data(:,c), varLen); texts = [texts, t]; W = [W, w]; endfor elseif (isobject (data) && ! isa (data, 'tabular') && hasdispstrings (data)) if (any (isa (data, {'categorical', 'datetime', 'duration', ... 'calendarDuration'}))) align = 'right'; else align = 'left'; endif for c = 1:nc [texts, W] = addtexts (texts, W, dispstrings (data(:,c))); endfor else ## Structures, nested objects beyond the first level, and any class with ## no text form align = 'left'; if (isa (data, 'tabular')) nc = width (data); endif t = repmat ({sprintf("<1x%d %s>", nc, class (data))}, [rows(data), 1]); [texts, W] = addtexts (texts, W, t); endif endfunction ## Append the column of texts T to TEXTS and its widest text to W, which is ## zero with no rows. function [texts, W] = addtexts (texts, W, t) texts = [texts, t(:)]; W = [W, max([0; cellfun(@length, t(:))])]; endfunction ## Whether the class of the object X defines a 'dispstrings' method, hidden ## or not. Octave 11 lists the methods in a cell array; an Octave whose meta ## classes follow MATLAB lists them as an array of 'meta.method' objects. function tf = hasdispstrings (x) ml = metaclass (x).MethodList; if (iscell (ml)) names = cellfun (@(m) m.Name, ml, 'UniformOutput', false); else names = {ml.Name}; endif tf = any (strcmp (names, 'dispstrings')); endfunction ## The row pattern for one display column of width W: numbers and the like ## align right, text left, and a cellstr's texts sit in braces. function pat = cellpattern (align, w) switch (align) case 'right' pat = sprintf ("%%+%ds", w); case 'left' pat = sprintf ("%%-%ds", w); otherwise pat = sprintf ("{%%-%ds}", w - 2); endswitch endfunction function [outData, optLen] = mixedcell2str (data, varLen) out_str = repmat ({''}, size (data)); ## Preallocate indexes to avoid truncation when last elements are 0 idx_cell = logical (zeros (size (data))); idx_charvec = idx_cell; idx_logical = idx_cell; idx_numeric = idx_cell; idx_object = idx_cell; idx_string = idx_cell; idx_struct = idx_cell; ## Find scalars or row vectors se = cell2mat (cellfun (@(x) numel (x), data, 'UniformOutput', false)) == 1; ve = cell2mat (cellfun (@(x) size (x,1), data, 'UniformOutput', false)) == 1; ## Catch 'cell' scalars tmp = cell2mat (cellfun (@iscell, data(se), 'UniformOutput', false)) == 1; idx_cell(se) = tmp; sf = @(x) sprintf ("1x1 cell"); out_str(idx_cell) = (cellfun (sf, data(idx_cell), ... 'UniformOutput', false)); ## Catch 'char' scalars or row vectors tmp = cell2mat (cellfun (@ischar, data(ve), 'UniformOutput', false)); idx_charvec(ve) = tmp; sf = @(x) sprintf ("'%s'", x); out_str(idx_charvec) = (cellfun (sf, data(idx_charvec), ... 'UniformOutput', false)); ## Catch 'logical' scalars or row vectors tmp = cell2mat (cellfun (@islogical, data(ve), 'UniformOutput', false)) == 1; idx_logical(ve) = tmp; sf = @(x) sprintf ("[%s]", strtrim (sprintf ("%d ", x))); out_str(idx_logical) = (cellfun (sf, data(idx_logical), ... 'UniformOutput', false)); ## Catch 'numeric' scalars or row vectors tmp = cell2mat (cellfun (@isnumeric, data(ve), 'UniformOutput', false)) == 1; idx_numeric(ve) = tmp; sf = @(x) sprintf ("[%s]", strtrim (sprintf ("%g ", x))); out_str(idx_numeric) = (cellfun (sf, data(idx_numeric), ... 'UniformOutput', false)); ## Catch 'object' scalars tmp = cell2mat (cellfun (@isobject, data(se), 'UniformOutput', false)) == 1; idx_struct(se) = tmp; sf = @(x) sprintf ("1x1 %s", class (x)); out_str(idx_struct) = (cellfun (sf, data(idx_struct), ... 'UniformOutput', false)); ## Catch 'string' scalars or row vectors tmp = cell2mat (cellfun (@isstring, data(ve), 'UniformOutput', false)) == 1; idx_string(ve) = tmp; sf = @(x) sprintf ("[%s]", strtrim (sprintf ("%s ", dispstrings (x){:}))); out_str(idx_string) = (cellfun (sf, data(idx_string), ... 'UniformOutput', false)); ## Catch scalar elements of struct type tmp = cell2mat (cellfun (@isstruct, data(se), 'UniformOutput', false)) == 1; idx_struct(se) = tmp; sf = @(x) sprintf ("1x1 struct"); out_str(idx_struct) = (cellfun (sf, data(idx_struct), ... 'UniformOutput', false)); ## Keep indexes for numerical and logical values to right alignment pad_B = idx_numeric | idx_logical; # pad before: sprintf("{%%-%ds}" pad_A = ! pad_B; # pad after: sprintf("{%%+%ds}" ## Catch remaining elements me = ! (idx_cell | idx_charvec | idx_logical | idx_numeric | ... idx_object | idx_string | idx_struct); ## Preallocate indexes to avoid truncation when last elements are 0 idx_cell = logical (zeros (size (data))); idx_charvec = idx_cell; idx_logical = idx_cell; idx_numeric = idx_cell; idx_object = idx_cell; idx_string = idx_cell; idx_struct = idx_cell; if (any (me)) ## 'cell' arrays tmp = cell2mat (cellfun (@iscell, data(me), 'UniformOutput', false)) == 1; idx_cell(me) = tmp; sf = @(x) sprintf (strcat ([strjoin(repmat ({'%d'}, 1, ndims (x)), 'x'), ... ' cell']), size (x)); out_str(idx_cell) = (cellfun (sf, data(idx_cell), ... 'UniformOutput', false)); ## 'char' arrays tmp = cell2mat (cellfun (@ischar, data(me), 'UniformOutput', false)); idx_charvec(me) = tmp; sf = @(x) sprintf (strcat (strjoin (repmat ({'%d'}, 1, ndims (x)), 'x'), ... ' char'), size (x)); out_str(idx_charvec) = (cellfun (sf, data(idx_charvec), ... 'UniformOutput', false)); ## 'logical' arrays tmp = cell2mat (cellfun (@islogical, data(me), ... 'UniformOutput', false)) == 1; idx_logical(me) = tmp; sf = @(x) sprintf (strcat (strjoin (repmat ({'%d'}, 1, ndims (x)), 'x'), ... ' logical'), size (x)); out_str(idx_logical) = (cellfun (sf, data(idx_logical), ... 'UniformOutput', false)); ## 'numeric' arrays tmp = cell2mat (cellfun (@isnumeric, data(me), ... 'UniformOutput', false)) == 1; idx_numeric(me) = tmp; sf = @(x) sprintf (strcat (strjoin (repmat ({'%d'}, 1, ndims (x)), 'x'), ... ' array'), size (x)); out_str(idx_numeric) = (cellfun (sf, data(idx_numeric), ... 'UniformOutput', false)); ## 'object' arrays tmp = cell2mat (cellfun (@isobject, data(me), 'UniformOutput', false)) == 1; idx_object(me) = tmp; sf = @(x) sprintf (strcat (strjoin (repmat ({'%d'}, 1, ndims (x)), 'x'), ... ' %s'), size (x), class (x)); out_str(idx_object) = (cellfun (sf, data(idx_object), ... 'UniformOutput', false)); ## 'string' arrays tmp = cell2mat (cellfun (@isstring, data(me), 'UniformOutput', false)) == 1; idx_string(me) = tmp; sf = @(x) sprintf (strcat (strjoin (repmat ({'%d'}, 1, ndims (x)), 'x'), ... ' string'), size (x)); out_str(idx_string) = (cellfun (sf, data(idx_string), ... 'UniformOutput', false)); ## 'struct' arrays tmp = cell2mat (cellfun (@isstruct, data(me), 'UniformOutput', false)) == 1; idx_struct(me) = tmp; sf = @(x) sprintf (strcat (strjoin (repmat ({'%d'}, 1, ndims (x)), 'x'), ... ' struct'), size (x)); out_str(idx_struct) = (cellfun (sf, data(idx_struct), ... 'UniformOutput', false)); endif ## Whatever is left is named by its size and class, and a function handle ## by its text. for k = find (cellfun (@isempty, out_str(:)))' x = data{k}; if (is_function_handle (x)) out_str{k} = func2str (x); if (out_str{k}(1) != '@') out_str{k} = ['@', out_str{k}]; endif else out_str{k} = sprintf ("%s %s", strjoin (arrayfun (@num2str, size (x), ... "UniformOutput", false), 'x'), class (x)); endif endfor ## Get optimal length strLen = max (cellfun (@length, out_str)) + 2; optLen = max ([varLen, strLen]); ## Pad data according to optimal length ## numeric and logical is right aligned, everything else is left aligned Ra = sprintf ("{%%+%ds}", optLen - 2); La = sprintf ("{%%-%ds}", optLen - 2); fcn = @(x) sprintf (Ra, x); outData(pad_B) = cellfun (fcn, out_str(pad_B), 'UniformOutput', false); fcn = @(x) sprintf (La, x); outData(pad_A) = cellfun (fcn, out_str(pad_A), 'UniformOutput', false); outData = outData(:); endfunction ## Set the rows of a variable V selected by the logical MASK to the standard ## missing value for V's type. Returns an errmsg body for unsupported types. ## The height an assignment grows the object to, or 0 where it grows nothing. ## Only a numeric row index reaches past the end; a logical mask is read ## against the rows there are. function n = growthHeight (this, ixRow) n = 0; if (isnumeric (ixRow) && ! isempty (ixRow)) m = max (ixRow(:)); if (m > height (this)) n = m; endif endif endfunction ## PROTO grown to N rows, the rows added taking the fill the class gives an ## array grown by indexed assignment: zero for a numeric, duration or ## calendarDuration variable and false for a logical one, as MATLAB does, ## and the class's own missing value for one that has such a thing. A ## cellstr variable takes empty text, where MATLAB puts an empty double, so ## that it stays a cellstr. Returns an errmsg body for a type with neither. function [v, errmsg] = padVariable (v, n) errmsg = ''; nrow = size (v, 1); if (nrow >= n) return; endif k = n - nrow; w = size (v, 2); if (islogical (v)) pad = false (k, w); elseif (isnumeric (v)) pad = zeros (k, w, class (v)); elseif (iscellstr (v)) pad = repmat ({''}, k, w); elseif (iscell (v)) pad = cell (k, w); elseif (ischar (v)) pad = repmat (' ', k, w); elseif (isa (v, 'duration')) pad = seconds (zeros (k, w)); elseif (isa (v, 'calendarDuration')) pad = calmonths (zeros (k, w)); elseif (isa (v, 'tabular')) ## A nested table grows by rows of its own variables' defaults [pad, errmsg] = tabular_rows (v, k, @default_rows); if (! isempty (errmsg)) return; endif v = vertcat (v, pad); return; else [pad, errmsg] = missing_rows (v, k); if (! isempty (errmsg)) return; endif endif v = [v; pad]; endfunction function [v, errmsg] = set_var_missing (v, mask) errmsg = ''; if (! any (mask)) return; endif if (isa (v, 'string')) v(mask) = string (missing); elseif (isa (v, 'categorical')) v(mask) = categorical (missing); elseif (isa (v, 'datetime')) ## 'missing' rather than a bare 'NaT', which carries no time zone and ## cannot be assigned into a zoned array; assigning 'missing' blanks the ## element in the array's own zone. v(mask) = missing; elseif (isa (v, 'duration')) v(mask) = missing; elseif (isa (v, 'calendarDuration')) v(mask,:) = NaN; elseif (iscellstr (v)) v(mask) = {''}; elseif (islogical (v)) v(mask,:) = false; elseif (isinteger (v)) v(mask,:) = 0; elseif (isfloat (v)) v(mask,:) = NaN; elseif (ischar (v)) v(mask,:) = ' '; elseif (isstruct (v)) v(mask) = empty_struct (v); elseif (isa (v, 'missing')) v(mask) = missing; elseif (isa (v, 'tabular')) [rows_missing, errmsg] = tabular_rows (v, sum (mask), @missing_rows); if (isempty (errmsg)) v = subsasgn (v, substruct ('()', {find(mask), ':'}), rows_missing); endif else errmsg = sprintf (strcat ("cannot create missing values for a variable", ... " of type '%s'."), class (v)); endif endfunction ## Create an N-row array of standard missing values matching the type and width ## of PROTO. Used when one input table has no rows to replicate from. Returns ## an errmsg body for unsupported types. function [col, errmsg] = missing_rows (proto, n) errmsg = ''; col = []; w = max (size (proto, 2), 1); if (isa (proto, 'string')) col = repmat (string (missing), n, w); elseif (isa (proto, 'categorical')) col = repmat (categorical (missing), n, w); elseif (isa (proto, 'datetime')) ## The column is built rather than assigned into, so it takes the ## prototype's zone here or the result would be an unzoned column beside ## zoned ones. col = NaT (n, w); if (! isempty (proto.TimeZone)) col.TimeZone = proto.TimeZone; endif elseif (isa (proto, 'duration')) col = hours (NaN (n, w)); elseif (isa (proto, 'calendarDuration')) col = calmonths (NaN (n, w)); elseif (iscellstr (proto)) col = repmat ({''}, n, w); elseif (islogical (proto)) col = false (n, w); elseif (isinteger (proto)) col = zeros (n, w, class (proto)); elseif (isfloat (proto)) col = NaN (n, w); elseif (ischar (proto)) col = repmat (' ', n, w); elseif (isstruct (proto)) col = repmat (empty_struct (proto), n, 1); elseif (isa (proto, 'missing')) col = repmat (missing, n, w); elseif (isa (proto, 'tabular')) [col, errmsg] = tabular_rows (proto, n, @missing_rows); else errmsg = sprintf (strcat ("cannot create missing values for a variable", ... " of type '%s'."), class (proto)); endif endfunction ## N rows shaped as the nested table PROTO, each of its variables filled by ## FILLFN (VAR, N): missing values or defaults. The rows are built through ## the table's own indexing, which indexing inside the class bypasses. function [col, errmsg] = tabular_rows (proto, n, fillfn) errmsg = ''; col = []; if (height (proto) == 0) errmsg = sprintf (strcat ("cannot create rows for an empty nested", ... " %s."), class (proto)); return; endif col = subsref (proto, substruct ('()', {ones(n, 1), ':'})); props = subsref (proto, substruct ('.', 'Properties')); names = props.VariableNames; for j = 1:numel (names) v = subsref (col, substruct ('.', names{j})); [v, errmsg] = fillfn (v, n); if (! isempty (errmsg)) return; endif col = subsasgn (col, substruct ('.', names{j}), v); endfor endfunction ## N rows of the default value of the variable V, as growing it gives. function [pad, errmsg] = default_rows (v, n) [p, errmsg] = padVariable (v, rows (v) + n); pad = []; if (isempty (errmsg)) pad = rowsof (p, rows (v) + (1:n)); endif endfunction ## A struct with the fields of S, each empty. function e = empty_struct (s) f = fieldnames (s); e = cell2struct (cell (numel (f), 1), f, 1); endfunction ## Format a datetime column as a column cell of ISO 8601 strings for ## 'table2ods'. NaT values yield an empty string, which the writer records as a ## missing (empty) cell. The wall-clock components are used; any TimeZone is ## not encoded in the value (mirroring the datetime display round-trip of the ## CSV path). function C = datetime2iso (dt) [Y, M, D] = ymd (dt(:)); [h, m, s] = hms (dt(:)); n = numel (Y); C = cell (n, 1); for i = 1:n if (isnan (Y(i))) C{i} = ''; else C{i} = sprintf ("%04d-%02d-%02dT%02d:%02d:%s", ... Y(i), M(i), D(i), h(i), m(i), iso_seconds (s(i))); endif endfor endfunction ## Format a duration column as a column cell of ISO 8601 duration strings ## (@code{PTnHnMnS}) for 'table2ods'. NaN values yield an empty string (written ## as a missing cell). Hours are not wrapped at 24, so durations of any ## magnitude are preserved; negative durations carry a leading minus sign. function C = duration2iso (du) tot = seconds (du(:)); n = numel (tot); C = cell (n, 1); for i = 1:n if (isnan (tot(i))) C{i} = ''; else a = abs (tot(i)); H = floor (a / 3600); MI = floor (mod (a, 3600) / 60); S = mod (a, 60); sgn = ''; if (tot(i) < 0) sgn = '-'; endif C{i} = sprintf ("%sPT%dH%dM%sS", sgn, H, MI, iso_seconds (S)); endif endfor endfunction ## Format a seconds value for an ISO 8601 string: a two-digit integer when ## whole, otherwise the shortest fraction that reads back as the same double, so ## that a duration of any resolution survives the round trip. function str = iso_seconds (s) si = floor (s); if (s == si) str = sprintf ("%02d", si); return; endif for prec = 15:17 str = sprintf ("%.*g", prec, s); if (str2double (str) == s) break; endif endfor if (si < 10) str = ['0', str]; endif endfunction ## Every subclass must implement all eleven hooks, and the message names the ## subclass that is missing one. The hooks are protected, so they can only be ## reached from inside a subclass: the fixture implements none of them and ## exposes one public wrapper per hook. %!shared fixdir %! fixdir = tempname (); %! mkdir (fixdir); %! src = {'classdef notable < tabular', ... %! ' methods', ... %! ' function out = call_hasRowLabels (this)', ... %! ' out = hasRowLabels (this);', ... %! ' endfunction', ... %! ' function out = call_getRowLabels (this)', ... %! ' out = getRowLabels (this);', ... %! ' endfunction', ... %! ' function out = call_rowLabelName (this)', ... %! ' out = rowLabelName (this);', ... %! ' endfunction', ... %! ' function out = call_rowLabelStrings (this)', ... %! ' out = rowLabelStrings (this);', ... %! ' endfunction', ... %! ' function out = call_rowLabelHeader (this)', ... %! ' out = rowLabelHeader (this);', ... %! ' endfunction', ... %! ' function out = call_rowLabelProperties (this)', ... %! ' out = rowLabelProperties (this);', ... %! ' endfunction', ... %! ' function out = call_setRowLabelProperty (this)', ... %! ' out = setRowLabelProperty (this, "x", 1, []);', ... %! ' endfunction', ... %! ' function out = call_subsetRowLabels (this)', ... %! ' out = subsetRowLabels (this, 1);', ... %! ' endfunction', ... %! ' function out = call_clearRowLabels (this)', ... %! ' out = clearRowLabels (this);', ... %! ' endfunction', ... %! ' function out = call_resolveRowRef (this)', ... %! ' out = resolveRowRef (this, 1);', ... %! ' endfunction', ... %! ' function out = call_makeProperties (this)', ... %! ' out = makeProperties (this);', ... %! ' endfunction', ... %! ' endmethods', ... %! 'endclassdef'}; %! fid = fopen (fullfile (fixdir, 'notable.m'), 'w'); %! fprintf (fid, '%s\n', src{:}); %! fclose (fid); %! addpath (fixdir); ## Test 'hasRowLabels' raises until the subclass implements it %!error ... %! call_hasRowLabels (notable ()); ## Test 'getRowLabels' raises until the subclass implements it %!error ... %! call_getRowLabels (notable ()); ## Test 'rowLabelName' raises until the subclass implements it %!error ... %! call_rowLabelName (notable ()); ## Test 'rowLabelStrings' raises until the subclass implements it %!error ... %! call_rowLabelStrings (notable ()); ## Test 'rowLabelHeader' raises until the subclass implements it %!error ... %! call_rowLabelHeader (notable ()); ## Test 'rowLabelProperties' raises until the subclass implements it %!error ... %! call_rowLabelProperties (notable ()); ## Test 'setRowLabelProperty' raises until the subclass implements it %!error ... %! call_setRowLabelProperty (notable ()); ## Test 'subsetRowLabels' raises until the subclass implements it %!error ... %! call_subsetRowLabels (notable ()); ## Test 'clearRowLabels' raises until the subclass implements it %!error ... %! call_clearRowLabels (notable ()); ## Test 'resolveRowRef' raises until the subclass implements it %!error ... %! call_resolveRowRef (notable ()); ## Test 'makeProperties' raises until the subclass implements it %!error ... %! call_makeProperties (notable ()); ## Test the fixture is removed again %!test %! rmpath (fixdir); %! delete (fullfile (fixdir, 'notable.m')); %! rmdir (fixdir); %! assert_equal (exist (fixdir, 'dir'), 0); ## Translate a MATLAB delimiter (named or literal) into a single character for ## 'writetable'. function d = wt_resolve_delimiter (delim, caller) if (isa (delim, 'string')) delim = char (delim); endif if (! ischar (delim)) error ("%s: 'Delimiter' must be a character vector or string.", caller); endif switch (lower (delim)) case {'comma', ','} d = ','; case {'space', ' '} d = ' '; case {'tab', "\t"} d = "\t"; case {'semi', ';'} d = ';'; case {'bar', '|'} d = '|'; otherwise if (isscalar (delim)) d = delim; else error ("%s: unsupported 'Delimiter' value '%s'.", caller, delim); endif endswitch endfunction pr0m1th3as-datatypes-9450b29/inst/tests/000077500000000000000000000000001527370255000200155ustar00rootroot00000000000000pr0m1th3as-datatypes-9450b29/inst/tests/calendarDuration.m-tst000066400000000000000000004105721527370255000242730ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ################################################################################ ## ** Create and convert 'calendarDuration' type ** ## ################################################################################ ## Available Methods ## ## ## ## 'duration' 'dispstrings' 'cellstr' 'char' ## ## 'datevec' 'time' 'split' 'caldays' ## ## 'calweeks' 'calmonths' 'calquarters' 'calyears' ## ## ## ################################################################################ %!assert_equal (isscalar (calendarDuration ()), true) %!assert_equal (isempty (calendarDuration ()), false) %!assert_equal (class (calendarDuration), 'calendarDuration') %!assert_equal (dispstrings (calendarDuration ()), {'0d'}) %!assert_equal (size (calendarDuration (zeros (4, 3))), [4, 1]) %!assert_equal (size (calendarDuration (ones (5, 6))), [5, 1]) %!test %! X = calendarDuration ([0, 0, 1]); %! assert_equal (caldays (X), 1); %! assert_equal (dispstrings (X), {'1d'}); %! assert_equal (char (X), '1d'); %!test %! X = calendarDuration ([0, 2, 14]); %! assert_equal (calmonths (X), 2); %! assert_equal (char (X), '2mo 14d'); %!test %! X = calendarDuration ([1, 5, 15]); %! assert_equal (calmonths (X), 17); %! assert_equal (calquarters (X), 5); %! assert_equal (dispstrings (X), {'1y 5mo 15d'}); %! assert_equal (char (X, 'mdt'), '17mo 15d'); %! assert_equal (cellstr (X, 'mwdt'), {'17mo 2w 1d'}); %!test %! X = calendarDuration ([1, 0, 0, 12, 30, 6.5]); %! assert_equal (calmonths (X), 12); %! assert_equal (calquarters (X), 4); %! assert_equal (dispstrings (X), {'1y 12h 30m 6.5s'}); %! assert_equal (class (time (X)), 'duration'); %! assert_equal (hours (time (X)), 12.5018, 1e-4); %! assert_equal (minutes (time (X)), 750.1083, 1e-4); %! assert_equal (seconds (time (X)), 45006.5); %!assert_equal (size (calendarDuration ([1, 2; 9, 8], 0, 1)), [2, 2]) %!assert_equal (size (calendarDuration (1, zeros (2, 3, 4), 1)), [2, 3, 4]) %!test %! X = calendarDuration (0, 0, 1); %! assert_equal (caldays (X), 1); %! assert_equal (dispstrings (X), {'1d'}); %! assert_equal (char (X), '1d'); %!test %! X = calendarDuration ([0, 2, 14]); %! assert_equal (calmonths (X), 2); %! assert_equal (char (X), '2mo 14d'); %!test %! X = calendarDuration (1, 5, 15); %! assert_equal (calmonths (X), 17); %! assert_equal (calquarters (X), 5); %! assert_equal (dispstrings (X), {'1y 5mo 15d'}); %! assert_equal (char (X, 'mdt'), '17mo 15d'); %! assert_equal (cellstr (X, 'mwdt'), {'17mo 2w 1d'}); %!test %! X = calendarDuration ([1;NaN], 2, 3); %! assert_equal (char (X(1)), '1y 2mo 3d'); %! assert_equal (char (X(2)), 'NaN'); %!assert_equal (size (calendarDuration (1, 2, 1, days (ones (2, 3, ... %! 4)))), [2, 3, 4]) %!test %! X = calendarDuration (1, 0, 0, duration (12, 30, 6.5)); %! assert_equal (calmonths (X), 12); %! assert_equal (calquarters (X), 4); %! assert_equal (dispstrings (X), {'1y 12h 30m 6.5s'}); %! assert_equal (class (time (X)), 'duration'); %! assert_equal (hours (time (X)), 12.5018, 1e-4); %! assert_equal (minutes (time (X)), 750.1083, 1e-4); %! assert_equal (seconds (time (X)), 45006.5); %!test %! X = calendarDuration (1, 0, 0, 12, 30, 6.5); %! assert_equal (calmonths (X), 12); %! assert_equal (calquarters (X), 4); %! assert_equal (dispstrings (X), {'1y 12h 30m 6.5s'}); %! assert_equal (class (time (X)), 'duration'); %! assert_equal (hours (time (X)), 12.5018, 1e-4); %! assert_equal (minutes (time (X)), 750.1083, 1e-4); %! assert_equal (seconds (time (X)), 45006.5); %!test %! X = calendarDuration ([1, NaN], 0, 0, 12, 30, 6.5); %! assert_equal (calmonths (X(1)), 12); %! assert_equal (calquarters (X(1)), 4); %! assert_equal (dispstrings (X(1)), {'1y 12h 30m 6.5s'}); %! assert_equal (class (time (X(1))), 'duration'); %! assert_equal (hours (time (X(1))), 12.5018, 1e-4); %! assert_equal (minutes (time (X(1))), 750.1083, 1e-4); %! assert_equal (seconds (time (X(1))), 45006.5); %! assert_equal (calmonths (X(2)), NaN); %! assert_equal (calquarters (X(2)), NaN); %! assert_equal (char (X(2)), 'NaN'); %! assert_equal (class (time (X(2))), 'duration'); %! assert_equal (hours (time (X(2))), NaN); %! assert_equal (minutes (time (X(2))), NaN); %! assert_equal (seconds (time (X(2))), NaN); %!error ... %! calendarDuration ([0, 0, 1], 'Format', 1) %!error ... %! calendarDuration ([0, 0, 1], 'Format', {'YY-MM-DD'}) %!error ... %! calendarDuration ([0, 0, 1], 'Format', 'yymd') %!error ... %! calendarDuration ([0, 0, 1], 'Format', 'ymd') %!error ... %! calendarDuration ([0, 0, 1], 'Format', 'mtd') %!error calendarDuration ({0, 0}) %!error calendarDuration ('asd') %!error calendarDuration (1+i) %!error ... %! calendarDuration ([0.5, 1, 1]) %!error ... %! calendarDuration ([1, 0.5, 1]) %!error ... %! calendarDuration ([1, 1, 0.5]) %!error ... %! calendarDuration ([0.5, 0, 0, 0, 0, 0]) %!error ... %! calendarDuration ([0, 0.5, 0, 0, 0, 0]) %!error ... %! calendarDuration ([0, 0, 0.5, 0, 0, 0]) %!error ... %! calendarDuration ([0, 0, 0, 0.5, 0, 0]) %!error ... %! calendarDuration ([0, 0, 0, 0, 0.5, 0]) %!error ... %! calendarDuration ([1, 1]) %!error ... %! calendarDuration ([1, 1, 1, 0.5, 0]) %!error ... %! calendarDuration ('a', 2, 3) %!error ... %! calendarDuration (1, {2}, 3) %!error ... %! calendarDuration (1, 2, {'s'}) %!error calendarDuration (i, 2, 3) %!error calendarDuration (1, i, 3) %!error calendarDuration (1, 2, i) %!error ... %! calendarDuration ([1, 2], 2, [1; 2]) %!error ... %! calendarDuration (1.2, 2, 3) %!error ... %! calendarDuration (1, 2.5, 3) %!error ... %! calendarDuration (1, 2, 0.3) %!error ... %! calendarDuration ('a', 2, 3, days (1)) %!error ... %! calendarDuration (1, {2}, 3, days (1)) %!error ... %! calendarDuration (1, 2, {'s'}, days (1)) %!error ... %! calendarDuration (i, 2, 3, days (1)) %!error ... %! calendarDuration (1, i, 3, days (1)) %!error ... %! calendarDuration (1, 2, i, days (1)) %!error ... %! calendarDuration (1, 2, 3, 2) %!error ... %! calendarDuration ([1, 2], 2, 1, days ([1; 2])) %!error ... %! calendarDuration (1.2, 2, 3, days (1)) %!error ... %! calendarDuration (1, 2.5, 3, days (1)) %!error ... %! calendarDuration (1, 2, 0.3, days (1)) %!error ... %! calendarDuration ('a', 0, 0, 12, 30, 6.5) %!error ... %! calendarDuration (1, {0}, 0, 12, 30, 6.5) %!error ... %! calendarDuration (1, 0, string (0), 12, 30, 6.5) %!error ... %! calendarDuration (1, 0, 0, {12}, 30, 6.5) %!error ... %! calendarDuration (1, 0, 0, 12, '30', 6.5) %!error ... %! calendarDuration (1, 0, 0, 12, 30, struct ('s', 6.5)) %!error ... %! calendarDuration (i, 0, 0, 12, 30, 6.5) %!error ... %! calendarDuration (1, i, 0, 12, 30, 6.5) %!error ... %! calendarDuration (1, 0, i, 12, 30, 6.5) %!error ... %! calendarDuration (1, 0, 0, i, 30, 6.5) %!error ... %! calendarDuration (1, 0, 0, 12, i, 6.5) %!error ... %! calendarDuration (1, 0, 0, 12, 30, i) %!error ... %! calendarDuration ([1, 2], 0, [0; 0], 12, 30, 6.5) %!error ... %! calendarDuration (1.2, 0, 1, 12, 30, 6.5) %!error ... %! calendarDuration (1, [0.1, NaN], 1, 12, 30, 6.5) %!error ... %! calendarDuration (1, 0, 1.5, 12, 30, 6.5) %!error ... %! calendarDuration (1, 0, 1, 1.2, 30, 6.5) %!error ... %! calendarDuration (1, 0, 1, 12, 29.5, 6.5) %!error ... %! calendarDuration (1, [0.1, NaN]) %!error ... %! calendarDuration (1, [0.1, NaN], 1, 12, 30) %!test %! X = calendarDuration (1, 2, 3, 'Format', 'qmdt'); %! Y = calendarDuration (X); %! assert_equal (class (Y), 'calendarDuration'); %! assert_equal (Y.Format, 'qmdt'); %! assert_equal (calmonths (Y), 14); %!test %! X = calendarDuration (ones (2, 3), 2, 3, 'Format', 'mdt'); %! Y = calendarDuration (X); %! assert_equal (size (Y), [2, 3]); %! assert_equal (Y.Format, 'mdt'); %!test %! X = calendarDuration (1, 2, 3, 'Format', 'qmdt'); %! Y = calendarDuration (X, 'Format', 'mdt'); %! assert_equal (Y.Format, 'mdt'); %! assert_equal (X.Format, 'qmdt'); %!test %! X = calendarDuration ([], [], []); %! Y = calendarDuration (X); %! assert_equal (size (Y), [0, 0]); %! assert_equal (isempty (Y), true); %!test %! X = calendarDuration (1, 2, 3, 'Format', string ('mdt')); %! assert_equal (class (X.Format), 'char'); %! assert_equal (X.Format, 'mdt'); %! assert_equal (dispstrings (X), {'14mo 3d'}); %!test %! X = calendarDuration (1, 2, 3); %! X.Format = string ('mdt'); %! assert_equal (class (X.Format), 'char'); %! assert_equal (X.Format, 'mdt'); %!test %! X = calendarDuration ('Format', 'mdt'); %! assert_equal (X.Format, 'mdt'); %! assert_equal (dispstrings (X), {'0d'}); %!assert_equal (cellstr (calendarDuration (1, 2, 3), ... %! string ('mdt')), {'14mo 3d'}) %!assert_equal (char (calendarDuration (1, 2, 3), string ('mdt')), '14mo 3d') %!error ... %! calendarDuration (1, 2, 3, 'Format', []) %!error ... %! calendarDuration (1, 2, 3, 'Format', string ({'ymdt', 'mdt'})) %!error ... %! calendarDuration (1, 2, 3, 'Format', ['ymdt'; 'ymdt']) %!error ... %! calendarDuration (1, 2, 3, 'Format', 'ymdtz') %!error ... %! calendarDuration (1, 2, 3, 'Format', 'YMDT') %!error ... %! calendarDuration (1, 2, 3, 'Format', '') %!error ... %! calendarDuration (duration (1, 2, 3)) %!error ... %! calendarDuration (calendarDuration (1, 2, 3), 1, 2) %!assert_equal (dispstrings (calendarDuration ([0, 0, NaN])), {'NaN'}) %!assert_equal (dispstrings (calendarDuration ([0, 0, Inf])), {'Inf'}) %!assert_equal (dispstrings (calendarDuration ([0, NaN, Inf])), {'NaN'}) %!assert_equal (dispstrings (calendarDuration ([0, 0, -Inf])), {'-Inf'}) %!assert_equal (dispstrings (calendarDuration ([0, Inf, -Inf])), {'NaN'}) %!assert_equal (char (calendarDuration (0, 0, NaN)), 'NaN') %!assert_equal (char (calendarDuration (0, 0, Inf)), 'Inf') %!assert_equal (char (calendarDuration (0, NaN, Inf)), 'NaN') %!assert_equal (char (calendarDuration (0, 0, -Inf)), '-Inf') %!assert_equal (char (calendarDuration (0, Inf, -Inf)), 'NaN') %!assert_equal (dispstrings (calendarDuration (1, 3, 1, 'Format', ... %! 'mwdt')), {'15mo 1d'}) %!assert_equal (dispstrings (calendarDuration (1, 3, 1, 'Format', ... %! 'ymwdt')), {'1y 3mo 1d'}) %!assert_equal (dispstrings (calendarDuration (1, 3, 15, 'Format', ... %! 'ymwdt')), {'1y 3mo 2w 1d'}) %!assert_equal (dispstrings (calendarDuration (1, 3, 15, 'Format', ... %! 'mwdt')), {'15mo 2w 1d'}) %!assert_equal (dispstrings (calendarDuration (1, 3, 15, 'Format', ... %! 'ymdt')), {'1y 3mo 15d'}) %!assert_equal (dispstrings (calendarDuration (1, 3, 15, 'Format', ... %! 'mdt')), {'15mo 15d'}) %!assert_equal (dispstrings (calendarDuration (0, 15, 15, 'Format', ... %! 'ymdt')), {'1y 3mo 15d'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0)), {'0d'}) %!assert_equal (dispstrings (calendarDuration (1, 0, 0)), {'1y'}) %!assert_equal (dispstrings (calendarDuration (0, 1, 0)), {'1mo'}) %!assert_equal (dispstrings (calendarDuration (0, 12, 0)), {'1y'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 1)), {'1d'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 14)), {'14d'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 14, 'Format', ... %! 'mwdt')), {'2w'}) %!assert_equal (dispstrings (calendarDuration (0, 1, 0, ... %! minutes (5))), {'1mo 0h 5m 0s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, ... %! minutes (5))), {'0h 5m 0s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, ... %! minutes (50.05))), {'0h 50m 3s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, ... %! minutes (5.005))), {'0h 5m 0.3s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, ... %! minutes (5.000005))), {'0h 5m 0.0003s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, ... %! minutes (5.0000005))), {'0h 5m 3e-05s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, ... %! minutes (-5.000005))), {'0h -5m -0.0003s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, duration (3, 5, ... %! 0.3))), {'3h 5m 0.3s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, 0, 0, ... %! -0.5)), {'0h 0m -0.5s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, 0, 0, ... %! -1.5)), {'0h 0m -1.5s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, 0, 0, ... %! -0.25)), {'0h 0m -0.25s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, 0, 0, ... %! -0.001)), {'0h 0m -0.001s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, 0, 0, ... %! -1e-9)), {'0h 0m -1e-09s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, -1, -2, ... %! -3.5)), {'-1h -2m -3.5s'}) %!assert_equal (dispstrings (calendarDuration (1, 2, 3, 0, 0, ... %! -0.5)), {'1y 2mo 3d 0h 0m -0.5s'}) %!assert_equal (dispstrings (calendarDuration (-1, -2, -3, -4, -5, ... %! -6.5)), {'-1y -2mo -3d -4h -5m -6.5s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, 0, 0, ... %! 0.0005)), {'0h 0m 0.0005s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, 0, 0, ... %! 0.9995)), {'0h 0m 0.9995s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, 0, 0, ... %! 6.0005)), {'0h 0m 6.0005s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, 0, 0, ... %! 6.123456789)), {'0h 0m 6.1235s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, 0, 0, ... %! 0.00012345678)), {'0h 0m 0.00012346s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, 0, 0, ... %! 2.5e-05)), {'0h 0m 2.5e-05s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, 0, 0, ... %! 59.9999)), {'0h 0m 60s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, 0, 0, ... %! 12345.6789)), {'3h 25m 45.679s'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 999999)), {'999999d'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 1000000)), {'1e+06d'}) %!assert_equal (dispstrings (calendarDuration (0, 0, ... %! 1234567)), {'1.23457e+06d'}) %!assert_equal (dispstrings (calendarDuration (0, 0, ... %! -1234567)), {'-1.23457e+06d'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 1e10)), {'1e+10d'}) %!assert_equal (dispstrings (calendarDuration (1000000, 0, 0)), {'1e+06y'}) %!assert_equal (dispstrings (calendarDuration (0, 1234567, 0, 'Format', ... %! 'mdt')), {'1.23457e+06mo'}) %!assert_equal (dispstrings (calendarDuration (0, 2^53, 0, 'Format', ... %! 'mdt')), {'9.0072e+15mo'}) %!assert_equal (dispstrings (calendarDuration (0, 0, 0, 1000000, 0, ... %! 0)), {'1e+06h 0m 0s'}) %!assert_equal (cellstr (calendarDuration (1, 3, 1), 'mwdt'), {'15mo 1d'}) %!assert_equal (cellstr (calendarDuration (1, 3, 1), 'ymwdt'), {'1y 3mo 1d'}) %!assert_equal (cellstr (calendarDuration (1, 3, 15), ... %! 'ymwdt'), {'1y 3mo 2w 1d'}) %!assert_equal (cellstr (calendarDuration (1, 3, 15), 'mwdt'), {'15mo 2w 1d'}) %!assert_equal (cellstr (calendarDuration (1, 3, 15), 'ymdt'), {'1y 3mo 15d'}) %!assert_equal (cellstr (calendarDuration (1, 3, 15), 'mdt'), {'15mo 15d'}) %!assert_equal (cellstr (calendarDuration (0, 15, 15), 'ymdt'), {'1y 3mo 15d'}) %!assert_equal (cellstr (calendarDuration (0, 0, 0)), {'0d'}) %!assert_equal (cellstr (calendarDuration (1, 0, 0)), {'1y'}) %!assert_equal (cellstr (calendarDuration (0, 1, 0)), {'1mo'}) %!assert_equal (cellstr (calendarDuration (0, 12, 0)), {'1y'}) %!assert_equal (cellstr (calendarDuration (0, 0, 1)), {'1d'}) %!assert_equal (cellstr (calendarDuration (0, 0, 14)), {'14d'}) %!assert_equal (cellstr (calendarDuration (0, 0, 14), 'mwdt'), {'2w'}) %!assert_equal (cellstr (calendarDuration (0, 1, 0, ... %! minutes (5))), {'1mo 0h 5m 0s'}) %!assert_equal (cellstr (calendarDuration (0, 0, 0, minutes (5))), {'0h 5m 0s'}) %!assert_equal (cellstr (calendarDuration (0, 0, 0, ... %! minutes (50.05))), {'0h 50m 3s'}) %!assert_equal (cellstr (calendarDuration (0, 0, 0, ... %! minutes (5.005))), {'0h 5m 0.3s'}) %!assert_equal (cellstr (calendarDuration (0, 0, 0, ... %! minutes (5.000005))), {'0h 5m 0.0003s'}) %!assert_equal (cellstr (calendarDuration (0, 0, 0, duration (3, 5, ... %! 0.3))), {'3h 5m 0.3s'}) %!error ... %! cellstr (calendarDuration ([0, 0, 1]), 1) %!error ... %! cellstr (calendarDuration ([0, 0, 1]), {'YY-MM-DD'}) %!error ... %! cellstr (calendarDuration ([0, 0, 1]), 'yymd') %!error ... %! cellstr (calendarDuration ([0, 0, 1]), 'ymd') %!error ... %! cellstr (calendarDuration ([0, 0, 1]), 'mtd') %!error ... %! cellstr (calendarDuration ([0, 0, 1]), 'ymdtz') %!assert_equal (cellstr (calendarDuration (1, 2, 3), ''), {'1y 2mo 3d'}) %!test %! X = calendarDuration ([1, 2; 3, 4], 2, 3); %! assert_equal (cellstr (X), {'1y 2mo 3d', '2y 2mo 3d'; '3y 2mo 3d', ... %! '4y 2mo 3d'}); %!test %! X = calendarDuration (ones (2, 2, 2), 0, 0); %! assert_equal (size (cellstr (X)), [2, 2, 2]); %!test %! X = calendarDuration ([], [], []); %! assert_equal (size (cellstr (X)), [0, 0]); %! assert_equal (class (cellstr (X)), 'cell'); %!test %! X = calendarDuration ([1, 1000000], 0, [3, 0]); %! assert_equal (cellstr (X), {'1y 3d', '1e+06y'}); %!assert_equal (cellstr (calendarDuration (1, 2, 3, 'Format', ... %! 'qmdt')), {'4q 2mo 3d'}) %!assert_equal (char (calendarDuration (1, 3, 1), 'mwdt'), '15mo 1d') %!assert_equal (char (calendarDuration (1, 3, 1), 'ymwdt'), '1y 3mo 1d') %!assert_equal (char (calendarDuration (1, 3, 15), 'ymwdt'), '1y 3mo 2w 1d') %!assert_equal (char (calendarDuration (1, 3, 15), 'mwdt'), '15mo 2w 1d') %!assert_equal (char (calendarDuration (1, 3, 15), 'ymdt'), '1y 3mo 15d') %!assert_equal (char (calendarDuration (1, 3, 15), 'mdt'), '15mo 15d') %!assert_equal (char (calendarDuration (0, 15, 15), 'ymdt'), '1y 3mo 15d') %!assert_equal (char (calendarDuration (0, 0, 0)), '0d') %!assert_equal (char (calendarDuration (1, 0, 0)), '1y') %!assert_equal (char (calendarDuration (0, 1, 0)), '1mo') %!assert_equal (char (calendarDuration (0, 12, 0)), '1y') %!assert_equal (char (calendarDuration (0, 0, 1)), '1d') %!assert_equal (char (calendarDuration (0, 0, 14)), '14d') %!assert_equal (char (calendarDuration (0, 0, 14), 'mwdt'), '2w') %!assert_equal (char (calendarDuration (0, 1, 0, minutes (5))), '1mo 0h 5m 0s') %!assert_equal (char (calendarDuration (0, 0, 0, minutes (5))), '0h 5m 0s') %!assert_equal (char (calendarDuration (0, 0, 0, minutes (50.05))), '0h 50m 3s') %!assert_equal (char (calendarDuration (0, 0, 0, ... %! minutes (5.005))), '0h 5m 0.3s') %!assert_equal (char (calendarDuration (0, 0, 0, ... %! minutes (5.000005))), '0h 5m 0.0003s') %!assert_equal (char (calendarDuration (0, 0, 0, duration (3, 5, ... %! 0.3))), '3h 5m 0.3s') %!error ... %! char (calendarDuration ([0, 0, 1]), 1) %!error ... %! char (calendarDuration ([0, 0, 1]), {'YY-MM-DD'}) %!error ... %! char (calendarDuration ([0, 0, 1]), 'yymd') %!error ... %! char (calendarDuration ([0, 0, 1]), 'ymd') %!error ... %! char (calendarDuration ([0, 0, 1]), 'mtd') %!error ... %! char (calendarDuration ([0, 0, 1]), 'ymdtz') %!assert_equal (char (calendarDuration (1, 2, 3), ''), '1y 2mo 3d') %!assert_equal (char (calendarDuration (1, 2, 3), string ('mdt')), '14mo 3d') %!test %! X = calendarDuration ([1, 1000000], 0, [3, 0]); %! assert_equal (char (X), [' 1y 3d'; '1e+06y']); %!test %! X = calendarDuration ([1, NaN], 2, 3); %! assert_equal (char (X), ['1y 2mo 3d'; ' NaN']); %!test %! X = calendarDuration ([1, 2; 3, 4], 2, 3); %! cmat = char (X); %! assert_equal (size (cmat), [4, 9]); %! assert_equal (cmat(2,:), '3y 2mo 3d'); %!test %! X = calendarDuration (ones (2, 2, 2), 0, 0); %! assert_equal (size (char (X)), [8, 2]); %!test %! X = calendarDuration ([], [], []); %! assert_equal (size (char (X)), [0, 0]); %! assert_equal (class (char (X)), 'char'); %!test %! X = calendarDuration ([1:5]', 3, 15); %! DV = datevec (X); %! assert_equal (size (X), [5, 1]); %! assert_equal (size (DV), [5, 6]); %! assert_equal (DV(:,1:3), [1, 3, 15; 2, 3, 15; 3, 3, 15; 4, 3, 15; 5, 3, 15]); %! assert_equal (DV(:,4:6), zeros (5, 3)); %!test %! X = calendarDuration ([1:5]', 3, 15); %! [Y, MO, D] = datevec (X); %! assert_equal (size (Y), [5, 1]); %! assert_equal (size (MO), [5, 1]); %! assert_equal (size (D), [5, 1]); %! assert_equal (Y, [1:5]'); %! assert_equal (MO, [3; 3; 3; 3; 3]); %! assert_equal (D, 15 * ones (5, 1)); %!test %! X = calendarDuration ([1:5]', 3, 15); %! [~, ~, ~, H, MI, S] = datevec (X); %! assert_equal (size (H), [5, 1]); %! assert_equal (size (MI), [5, 1]); %! assert_equal (size (S), [5, 1]); %! assert_equal (H, zeros (5, 1)); %! assert_equal (MI, zeros (5, 1)); %! assert_equal (S, zeros (5, 1)); %!test %! X = calendarDuration (reshape (1:24, 2, 3, 4), 0, reshape (25:48, 2, 3, 4)); %! DV = datevec (X); %! assert_equal (size (X), [2, 3, 4]); %! assert_equal (size (DV), [24, 6]); %! assert_equal (DV(:,1), [1:24]'); %! assert_equal (DV(:,2), zeros (24, 1)); %! assert_equal (DV(:,3), [25:48]'); %! assert_equal (DV(:,4:6), zeros (24, 3)); %!test %! X = calendarDuration (reshape (1:24, 2, 3, 4), 0, reshape (25:48, 2, 3, 4)); %! [Y, MO, D] = datevec (X); %! assert_equal (size (Y), [2, 3, 4]); %! assert_equal (size (MO), [2, 3, 4]); %! assert_equal (size (D), [2, 3, 4]); %! assert_equal (Y, reshape (1:24, 2, 3, 4)); %! assert_equal (MO, zeros (2, 3, 4)); %! assert_equal (D, reshape (25:48, 2, 3, 4)); %!test %! X = calendarDuration (reshape (1:24, 2, 3, 4), 0, reshape (25:48, 2, 3, 4)); %! [~, ~, ~, H, MI, S] = datevec (X); %! assert_equal (size (H), [2, 3, 4]); %! assert_equal (size (MI), [2, 3, 4]); %! assert_equal (size (S), [2, 3, 4]); %! assert_equal (H, zeros (2, 3, 4)); %! assert_equal (MI, zeros (2, 3, 4)); %! assert_equal (S, zeros (2, 3, 4)); %!test %! D = days (0.51); %! X = calendarDuration (1, 0, 0, D); %! [Y, ~, ~, H, MI, S] = datevec (X); %! [h, mi, s] = hms (D); %! assert_equal (Y, 1); %! assert_equal (H, h); %! assert_equal (MI, mi); %! assert_equal (S, s); %!test %! D = days (repmat ([0.51, 0.46; 0.2, 1.23], 1, 2, 3)); %! X = calendarDuration (1, 0, 0, D); %! [Y, ~, ~, H, MI, S] = datevec (X); %! [h, mi, s] = hms (D); %! assert_equal (Y, ones (2, 4, 3)); %! assert_equal (H, h); %! assert_equal (MI, mi); %! assert_equal (S, s); %!test %! X = calendarDuration (1, 0, 1, 0, reshape (1:24, 2, 3, 4), 0); %! [Y, MO, D, H, MI, S] = datevec (X); %! assert_equal (Y, ones (2, 3, 4)); %! assert_equal (MO, zeros (2, 3, 4)); %! assert_equal (D, ones (2, 3, 4)); %! assert_equal (H, zeros (2, 3, 4)); %! assert_equal (MI, reshape (1:24, 2, 3, 4)); %! assert_equal (S, zeros (2, 3, 4)); %! DV = datevec (X); %! assert_equal (DV(:,[1, 3]), ones (24, 2)); %! assert_equal (DV(:,[2, 4, 6]), zeros (24, 3)); %! assert_equal (DV(:,5), [1:24]'); %!error ... %! [Y, MO, D, H, MI, S, MS] = datevec (calendarDuration) %!assert_equal (datevec (calendarDuration (Inf, 0, 0)), Inf (1, 6)) %!assert_equal (datevec (calendarDuration (-Inf, 0, 0)), -Inf (1, 6)) %!assert_equal (datevec (calendarDuration (0, 0, Inf)), Inf (1, 6)) %!assert_equal (datevec (calendarDuration (0, 0, 0, 0, 0, Inf)), Inf (1, 6)) %!assert_equal (datevec (calendarDuration (NaN, 0, 0)), NaN (1, 6)) %!test %! [Y, MO, D, H, MI, S] = datevec (calendarDuration (Inf, 0, 0)); %! assert_equal ([Y, MO, D, H, MI, S], Inf (1, 6)); %!test %! DV = datevec (calendarDuration ([1, NaN, Inf], 2, 3)); %! assert_equal (size (DV), [3, 6]); %! assert_equal (DV(1,:), [1, 2, 3, 0, 0, 0]); %! assert_equal (DV(2,:), NaN (1, 6)); %! assert_equal (DV(3,:), Inf (1, 6)); %!assert_equal (datevec (calendarDuration (-1, -2, -3, -4, -5, -6.5)), [-1, ... %! -2, -3, -4, -5, -6.5]) %!assert_equal (datevec (calendarDuration (0, 0, 0, 0, 0, -0.5)), [0, 0, 0, ... %! 0, 0, -0.5]) %!assert_equal (datevec (calendarDuration (0, 0, 0, 0, 0, 6.123456789)), [0, ... %! 0, 0, 0, 0, 6.123456789], 1e-9) %!assert_equal (datevec (calendarDuration (0, 25, 0)), [2, 1, 0, 0, 0, 0]) %!assert_equal (datevec (calendarDuration (0, -25, 0)), [-2, -1, 0, 0, 0, 0]) %!assert_equal (datevec (calendarDuration (0, 0, 0, 25, 0, 0)), [0, 0, 0, ... %! 25, 0, 0]) %!assert_equal (datevec (calendarDuration (0, 0, 0, 0, 90, 0)), [0, 0, 0, 1, ... %! 30, 0]) %!assert_equal (size (datevec (calendarDuration ([], [], []))), [0, 6]) %!test %! DV = datevec (calendarDuration (ones (2, 2, 2), 2, 3)); %! assert_equal (size (DV), [8, 6]); %!test %! [Y, MO, D, H, MI, S] = datevec (calendarDuration (ones (2, 2, 2), 2, 3)); %! assert_equal (size (Y), [2, 2, 2]); %! assert_equal (size (S), [2, 2, 2]); %!assert_equal (size (time (calendarDuration (1, 1, 1))), [1, 1]) %!assert_equal (size (time (calendarDuration (ones (2, 3, 4), 1, 1))), [2, ... %! 3, 4]) %!assert_equal (size (time (calendarDuration (1, 1, 1, days (1)))), [1, 1]) %!assert_equal (size (time (calendarDuration (0, 0, 0, ones (2, 3, 4), 1, ... %! 1))), [2, 3, 4]) %!test %! t = time (calendarDuration (1, 1, 1)); %! assert_equal (class (t), 'duration'); %! [h, m, s] = hms (t); %! assert_equal (h, 0); %! assert_equal (m, 0); %! assert_equal (s, 0); %!test %! t = time (calendarDuration (1, ones (2), 1, hours (1.5))); %! assert_equal (size (t), [2, 2]); %! [h, m, s] = hms (t); %! assert_equal (h, ones (2)); %! assert_equal (m, 30 * ones (2)); %! assert_equal (s, zeros (2)); %!test %! t = time (calendarDuration (1, ones (2), 1, 0, 0, 0.0251)); %! assert_equal (size (t), [2, 2]); %! [h, m, s] = hms (t); %! assert_equal (s, [0.0251, 0.0251; 0.0251, 0.0251]); %!assert_equal (seconds (time (calendarDuration (0, 0, 0, 1, 0, 1))), 3601) %!assert_equal (seconds (time (calendarDuration (0, 0, 1, 1, 0, 1))), 3601) %!assert_equal (seconds (time (calendarDuration (0, 0, 0, 1, 30, 2.5))), 5402.5) %!assert_equal (seconds (time (calendarDuration (0, 1, 0, 1, 30, 2.5))), 5402.5) %!assert_equal (minutes (time (calendarDuration (0, 0, 0, 1, 0, ... %! 1))), 60.017, 1e-3) %!assert_equal (minutes (time (calendarDuration (0, 1, 1, 1, 0, ... %! 1))), 60.017, 1e-3) %!assert_equal (minutes (time (calendarDuration (0, 0, 1, 1, 30, ... %! 2.5))), 90.042, 1e-3) %!assert_equal (minutes (time (calendarDuration (1, 1, 0, 1, 30, ... %! 62.5))), 91.042, 1e-3) %!assert_equal (fix (minutes (time (calendarDuration (0, 0, 0, 1, 0, 1)))), 60) %!assert_equal (fix (minutes (time (calendarDuration (0, 1, 1, 1, 0, 1)))), 60) %!assert_equal (fix (minutes (time (calendarDuration (0, 0, 1, 1, 30, ... %! 2.5)))), 90) %!assert_equal (fix (minutes (time (calendarDuration (1, 1, 0, 1, 30, ... %! 62.5)))), 91) %!assert_equal (hours (time (calendarDuration (0, 0, 1, 1, 0, 0))), 1) %!assert_equal (hours (time (calendarDuration (0, 0, 1, 1, 30, 0))), 1.5) %!assert_equal (hours (time (calendarDuration (0, 0, 1, 1, 0, 1800))), 1.5) %!assert_equal (hours (time (calendarDuration (0, [1, 2], 1, 1, 0, 0))), [1, 1]) %!assert_equal (hours (time (calendarDuration (0, 0, 1, [1; 2], 30, ... %! 0))), [1.5; 2.5]) %!assert_equal (hours (time (calendarDuration (0, 0, 1, 1, [0, 0], ... %! 1800))), [1.5, 1.5]) %!assert_equal (seconds (time (calendarDuration (Inf, 0, 0))), Inf) %!assert_equal (seconds (time (calendarDuration (-Inf, 0, 0))), -Inf) %!assert_equal (seconds (time (calendarDuration (NaN, 0, 0))), NaN) %!assert_equal (size (time (calendarDuration ([], [], []))), [0, 0]) %!assert_equal (hours (time (calendarDuration (0, 0, 0, 25, 0, 0))), 25) %!assert_equal (seconds (time (calendarDuration (-1, -14, -40, -4, -5, ... %! -6.5))), -14706.5) %!test %! X = calendarDuration (1, 0, 15); %! assert_equal (split (X, 'years'), 1); %! [Y, Q, W, D] = split (X, {'years', 'quarters', 'weeks', 'days'}); %! assert_equal (Y, 1); %! assert_equal (Q, 0); %! assert_equal (W, 2); %! assert_equal (D, 1); %!test %! X = calendarDuration (1, 0, 15); %! T = split (X, 'time'); %! assert_equal (isduration (T), true); %! assert_equal (hours (T), 0); %! assert_equal (minutes (T), 0); %! assert_equal (seconds (T), 0); %! assert_equal (dispstrings (T), {'00:00:00'}); %!test %! X = calendarDuration (1, ones (2), 15); %! T = split (X, 'time'); %! assert_equal (isduration (T), true); %! assert_equal (hours (T), zeros (2)); %! assert_equal (minutes (T), zeros (2)); %! assert_equal (seconds (T), zeros (2)); %! assert_equal (dispstrings (T), repmat ({'00:00:00'}, 2, 2)); %!test %! X = calendarDuration (1, 0, 15); %! assert_equal (split (X, 'years'), 1); %! [Q, W, D] = split (X, {'quarters', 'weeks', 'days'}); %! assert_equal (Q, 4); %! assert_equal (W, 2); %! assert_equal (D, 1); %!test %! X = calendarDuration (1, zeros (2, 3, 4), 15); %! assert_equal (split (X, 'years'), ones (2, 3, 4)); %! [Q, W, D] = split (X, {'quarters', 'weeks', 'days'}); %! assert_equal (Q, 4 * ones (2, 3, 4)); %! assert_equal (W, 2 * ones (2, 3, 4)); %! assert_equal (D, 1 * ones (2, 3, 4)); %!test %! X = calendarDuration (1, 0, 15); %! assert_equal (split (X, 'years'), 1); %! [Q, D] = split (X, {'quarters', 'days'}); %! assert_equal (Q, 4); %! assert_equal (D, 15); %!test %! X = calendarDuration (1, 4, 64); %! assert_equal (split (X, 'years'), 1); %! [Q, M, D] = split (X, {'quarters', 'months', 'days'}); %! assert_equal (Q, 5); %! assert_equal (M, 1); %! assert_equal (D, 64); %!test %! X = calendarDuration (1, 4, 64:69); %! assert_equal (split (X, 'years'), ones (1, 6)); %! [Q, M, D] = split (X, {'quarters', 'months', 'days'}); %! assert_equal (Q, [5, 5, 5, 5, 5, 5]); %! assert_equal (M, [1, 1, 1, 1, 1, 1]); %! assert_equal (D, [64:69]); %!test %! X = calendarDuration (1, 4, 64, 0, 35, 30); %! [Q, M, W, T] = split (X, {'quarters', 'months', 'weeks', 'time'}); %! assert_equal (Q, 5); %! assert_equal (M, 1); %! assert_equal (W, 9); %! assert_equal (minutes (T), 35.5); %! assert_equal (seconds (T), 2130); %!test %! X = calendarDuration (1, 4, 64, 0, 35, [30; 60]); %! [Q, M, W, T] = split (X, {'quarters', 'months', 'weeks', 'time'}); %! assert_equal (Q, [5; 5]); %! assert_equal (M, [1; 1]); %! assert_equal (W, [9; 9]); %! assert_equal (minutes (T), [35.5; 36]); %! assert_equal (seconds (T), [2130; 2160]); %!error split (calendarDuration) %!error ... %! split (calendarDuration, 1) %!error ... %! split (calendarDuration, {1}) %!error ... %! split (calendarDuration, 'ert') %!error ... %! split (calendarDuration, {'time', 'ert'}) %!error ... %! split (calendarDuration, string ({'time', 'ert'})) %!error ... %! split (calendarDuration, {'time', 'years'}) %!error ... %! split (calendarDuration, string ({'months', 'weeks', 'years'})) %!error ... %! [Y, M, D] = split (calendarDuration, {'years', 'months'}) %!error ... %! [Y, M] = split (calendarDuration, {'years', 'years'}) %!error ... %! split (calendarDuration, {}) %!error ... %! split (calendarDuration, '') %!error ... %! split (calendarDuration, 'years ') %!error ... %! split (calendarDuration, 'yearss') %!error ... %! split (calendarDuration, 'times') %!test %! X = calendarDuration (1, 14, 40, 4, 5, 6.5); %! assert_equal (split (X, 'y'), 2); %! assert_equal (split (X, 'Year'), 2); %! assert_equal (split (X, 'YEARS'), 2); %! assert_equal (split (X, 'q'), 8); %! assert_equal (split (X, 'm'), 26); %! assert_equal (split (X, 'mo'), 26); %! assert_equal (split (X, 'w'), 5); %! assert_equal (split (X, 'd'), 40); %! assert_equal (seconds (split (X, 't')), 14706.5); %! assert_equal (seconds (split (X, 'tim')), 14706.5); %!test %! X = calendarDuration (1, 14, 40, 4, 5, 6.5); %! Y = split (X, {'years', 'quarters', 'months'}); %! assert_equal (Y, 2); %! [Y, Q] = split (X, {'years', 'quarters', 'months'}); %! assert_equal ([Y, Q], [2, 0]); %! [Y, Q, MO] = split (X, {'years', 'quarters', 'months'}); %! assert_equal ([Y, Q, MO], [2, 0, 2]); %!test %! X = calendarDuration (1, 14, 40, 4, 5, 6.5); %! assert_equal (split (X, {'years', 'years'}), 2); %! assert_equal (split (X, {'months', 'months'}), 26); %! [Y, MO] = split (X, {'year', 'month'}); %! assert_equal ([Y, MO], [2, 2]); %!test %! X = calendarDuration (Inf, 0, 0); %! [Y, Q, MO, W, D, T] = split (X, ... %! {'years', 'quarters', 'months', 'weeks', 'days', ... %! 'time'}); %! assert_equal ([Y, Q, MO, W, D], Inf (1, 5)); %! assert_equal (seconds (T), Inf); %!test %! X = calendarDuration (-Inf, 0, 0); %! [Y, MO, D] = split (X, {'years', 'months', 'days'}); %! assert_equal ([Y, MO, D], -Inf (1, 3)); %!test %! X = calendarDuration (NaN, 0, 0); %! [Y, MO, D] = split (X, {'years', 'months', 'days'}); %! assert_equal ([Y, MO, D], NaN (1, 3)); %!assert_equal (caldays (caldays (5)), 5) %!assert_equal (caldays (caldays (1:10)), 1:10) %!assert_equal (caldays (caldays (ones (2, 3, 4))), ones (2, 3, 4)) %!assert_equal (caldays (calendarDuration (0, 0, [1, 2; 3, 4])), [1, 2; 3, 4]) %!test %! x = randi (20, 2, 3); %! assert_equal (caldays (calendarDuration (0, 0, x)), x); %!assert_equal (calweeks (calweeks (5)), 5) %!assert_equal (calweeks (calweeks (1:10)), 1:10) %!assert_equal (calweeks (calweeks (ones (2, 3, 4))), ones (2, 3, 4)) %!assert_equal (calweeks (calendarDuration (0, 0, 7 * [1, 2; 3, 4])), [1, ... %! 2; 3, 4]) %!test %! x = randi (20, 2, 3, 4); %! assert_equal (calweeks (calendarDuration (0, 0, x * 7)), x); %!assert_equal (calmonths (calmonths (5)), 5) %!assert_equal (calmonths (calmonths (1:10)), 1:10) %!assert_equal (calmonths (calmonths (ones (2, 3, 4))), ones (2, 3, 4)) %!assert_equal (calmonths (calendarDuration (0, [1, 2; 3, 4], 0)), [1, 2; 3, 4]) %!test %! x = randi (20, 2, 3); %! assert_equal (calmonths (calendarDuration (0, x, 0)), x); %!assert_equal (calquarters (calquarters (5)), 5) %!assert_equal (calquarters (calquarters (1:10)), 1:10) %!assert_equal (calquarters (calquarters (ones (2, 3, 4))), ones (2, 3, 4)) %!assert_equal (calquarters (calendarDuration (0, 3 * [1, 2; 3, 4], 0)), [1, ... %! 2; 3, 4]) %!test %! x = randi (20, 2, 3, 4); %! assert_equal (calquarters (calendarDuration (0, x * 3, 0)), x); %!assert_equal (calyears (calyears (5)), 5) %!assert_equal (calyears (calyears (1:10)), 1:10) %!assert_equal (calyears (calyears (ones (2, 3, 4))), ones (2, 3, 4)) %!assert_equal (calyears (calendarDuration ([1, 2; 3, 4], 0, 0)), [1, 2; 3, 4]) %!test %! x = randi (20, 2, 3); %! assert_equal (calyears (calendarDuration (x, 0, 0)), x); %!assert_equal (calweeks (calendarDuration (0, 0, ... %! -21:21)), floor ((-21:21) / 7)) %!assert_equal (calweeks (calendarDuration (0, 0, -15)), -3) %!assert_equal (calweeks (calendarDuration (0, 0, -13)), -2) %!assert_equal (calweeks (calendarDuration (0, 0, -8)), -2) %!assert_equal (calweeks (calendarDuration (0, 0, -7)), -1) %!assert_equal (calweeks (calendarDuration (0, 0, -1)), -1) %!assert_equal (calquarters (calendarDuration (0, -13:13, ... %! 0)), floor ((-13:13) / 3)) %!assert_equal (calquarters (calendarDuration (0, -1, 0)), -1) %!assert_equal (calquarters (calendarDuration (0, -4, 0)), -2) %!assert_equal (calyears (calendarDuration (0, -13:13, ... %! 0)), floor ((-13:13) / 12)) %!assert_equal (calyears (calendarDuration (0, -1, 0)), -1) %!assert_equal (calyears (calendarDuration (0, -13, 0)), -2) %!assert_equal (calquarters (calendarDuration (0, Inf, 0)), Inf) %!assert_equal (calyears (calendarDuration (0, -Inf, 0)), -Inf) %!assert_equal (caldays (calendarDuration ([], [], [])), []) %!assert_equal (calweeks (calendarDuration ([], [], [])), []) %!assert_equal (caldays (calendarDuration (0, 0, 15, 5, 0, 0)), 15) %!assert_equal (calweeks (calendarDuration (0, 0, 15, 5, 0, 0)), 2) %!assert_equal (caldays (calendarDuration (0, [0, 0], [3, 4])), [3, 4]) %!assert_equal (calmonths (calendarDuration (0, [0, 2], [3, 4])), [0, 2]) %!assert_equal (calyears (calendarDuration (0, [0, 2], [3, 4])), [0, 0]) %!assert_equal (calquarters (calendarDuration (0, [0, 2], [3, 4])), [0, 0]) %!error ... %! caldays (calendarDuration (0, 2, 14)) %!error ... %! calweeks (calendarDuration (0, 2, 14)) %!error ... %! caldays (calendarDuration (1, 0, 0)) %!assert_equal (caldays (calendarDuration (NaN, 0, 0)), NaN) %!assert_equal (caldays (calendarDuration (Inf, 0, 0)), Inf) %!assert_equal (calweeks (calendarDuration (0, 0, Inf)), Inf) %!error ... %! caldays (calendarDuration (0, [0, 2], [3, 4])) %!error ... %! caldays (calendarDuration (0, -2, 14)) %!error ... %! caldays (calendarDuration (-1, 2, 0)) %!error ... %! caldays (calendarDuration (-1, 0, 14)) %!error ... %! caldays (calendarDuration (0, -1, 0, 5, 0, 0)) %!assert_equal (caldays (calendarDuration (0, -Inf, 0)), -Inf) %!assert_equal (caldays (calendarDuration (0, [0, NaN], [3, 4])), [3, NaN]) %!assert_equal (caldays (calendarDuration (0, [0, Inf], [3, 4])), [3, Inf]) %!error ... %! calweeks (calendarDuration (0, -2, -14)) %!assert_equal (calweeks (calendarDuration (0, 0, NaN)), NaN) %!assert_equal (caldays (calendarDuration (0, 0, -15, 5, 0, 0)), -15) %!assert_equal (caldays (calendarDuration (0, 0, -15, -5, 0, 0)), -15) %!assert_equal (caldays (calendarDuration (0, 0, 0, -5, 0, 0)), 0) %!assert_equal (caldays (-calendarDuration (0, 0, 3, 4, 5, 6.5)), -3) %!assert_equal (calweeks (calendarDuration (0, 0, -15, -5, 0, 0)), -3) %!assert_equal (calmonths (calendarDuration (-1, 2, 0)), -10) %!assert_equal (calmonths (calendarDuration (1, -2, 0)), 10) %!assert_equal (calmonths (calendarDuration (0, -14, 0)), -14) %!assert_equal (calmonths (calyears (1) - calmonths (14)), -2) %!assert_equal (calmonths (calendarDuration (1, 2, 3) - calendarDuration (0, ... %! 0, 10)), 14) %!error ... %! caldays (calendarDuration (1, 2, 3) - calendarDuration (0, 0, 10)) %!assert_equal (caldays (caldays (3) - caldays (10)), -7) %!assert_equal (calweeks (caldays (3) - caldays (10)), -1) %!assert_equal (caldays (caldays ([1, 2, NaN, 4])), [1, 2, NaN, 4]) %!assert_equal (calweeks (calweeks ([1, 2, NaN, 4])), [1, 2, NaN, 4]) %!assert_equal (caldays (caldays ([1, Inf])), [1, Inf]) %!assert_equal (calmonths (calendarDuration (0, [0, NaN], [3, 4])), [0, NaN]) ################################################################################ ## ** Summary Information ** ## ################################################################################ ## Available Methods ## ## ## ## 'size' 'ndims' 'numel' 'nnz' ## ## 'length' 'keyHash' ## ## ## ################################################################################ %!assert_equal (size (calendarDuration), [1, 1]) %!assert_equal (size (calendarDuration ([], [], [])), [0, 0]) %!assert_equal (size (calendarDuration (ones (0, 3))), [0, 1]) %!assert_equal (size (calendarDuration (ones (0, 1), ones (0, 1), ones (0, ... %! 1))), [0, 1]) %!assert_equal (size (calendarDuration (ones (0, 3), ones (0, 3), ones (0, ... %! 3))), [0, 3]) %!assert_equal (size (calendarDuration (ones (1, 0), ones (1, 0), ones (1, ... %! 0))), [1, 0]) %!assert_equal (size (calendarDuration (ones (5, 3))), [5, 1]) %!assert_equal (size (calendarDuration (ones (5, 6))), [5, 1]) %!assert_equal (size (calendarDuration (ones (2, 3, 4), 0, 0)), [2, 3, 4]) %!assert_equal (size (calendarDuration (ones (1:10), 0, 0)), 1:10) %!test %! X = calendarDuration (ones (2, 3, 4), 0, 0); %! [d1, d2, d3] = size (X); %! assert_equal (d1, 2); %! assert_equal (d2, 3); %! assert_equal (d3, 4); %!test %! X = calendarDuration (0, 0, 0, minutes (randi (30, 2, 3, 4))); %! [d1, d2, d3] = size (X); %! assert_equal (d1, 2); %! assert_equal (d2, 3); %! assert_equal (d3, 4); %!test %! X = calendarDuration (0, 0, 0, minutes (randi (30, 2, 3, 4))); %! [d1, d2, d3] = size (X); %! assert_equal (size (X, 1), 2); %! assert_equal (size (X, 2), 3); %! assert_equal (size (X, 3), 4); %!test %! [d1, d2] = size (calendarDuration (ones (2, 3, 4), 0, 0)); %! assert_equal ([d1, d2], [2, 12]); %!assert_equal (ndims (calendarDuration ([], [], [])), 2) %!assert_equal (ndims (calendarDuration (1, 1, 1)), 2) %!assert_equal (ndims (calendarDuration), 2) %!assert_equal (ndims (calendarDuration (ones (2), 1, 1)), 2) %!assert_equal (ndims (calendarDuration (ones (2, 3, 4), 1, 1)), 3) %!assert_equal (ndims (calendarDuration (ones (1:10), 1, 1)), 10) %!assert_equal (numel (calendarDuration ([], [], [])), 0) %!assert_equal (numel (calendarDuration (1, 1, 1)), 1) %!assert_equal (numel (calendarDuration), 1) %!assert_equal (numel (calendarDuration (ones (2), 1, 1)), 4) %!assert_equal (numel (calendarDuration (ones (2, 3, 4), 1, 1)), 24) %!assert_equal (numel (calendarDuration (ones (1, 10), 1, 1)), 10) %!assert_equal (nnz (calendarDuration (1, 1, 1)), 1) %!assert_equal (nnz (calendarDuration (0, 0, 0)), 0) %!assert_equal (nnz (calendarDuration (0:5, 0, 0)), 5) %!assert_equal (nnz (calendarDuration ([0, 0, 0, 1, 1], 0, 0)), 2) %!assert_equal (nnz (calendarDuration (ones (2, 3, 4), 0, 0)), 24) %!assert_equal (length (calendarDuration (1, 1, 1)), 1) %!assert_equal (length (calendarDuration (0, 0, 0)), 1) %!assert_equal (length (calendarDuration (0:5, 0, 0)), 6) %!assert_equal (length (calendarDuration ([0, 0, 0, 1, 1], 0, 0)), 5) %!assert_equal (length (calendarDuration (ones (2, 3, 4), 0, 0)), 4) %!assert_equal (length (calendarDuration (ones (2, 10, 4), 0, 0)), 10) %!assert_equal (keyHash (calendarDuration), uint64 (9404037721690811995)) %!assert_equal (keyHash (calendarDuration (0, 0, ... %! 0)), uint64 (9404037721690811995)) %!assert_equal (keyHash (calendarDuration ([], [], ... %! [])), uint64 (10790283023733344549)) %!assert_equal (keyHash (calendarDuration (1, 1, ... %! 1)), uint64 (11245130726649642670)) %!test %! key = keyHash (calendarDuration ([1, 2; 3, 4], 1, 1)); %! assert_equal (key, uint64 (3011565863366139772)); %!test %! key = keyHash (calendarDuration (0, 0, 0, days ([1, 2, 3]))); %! assert_equal (key, uint64 (6673583750979954156)); %!test %! base_key = uint64 (2342124352342344234); %! key = keyHash (calendarDuration (0, 0, 0, days ([1, 2, 3])), base_key); %! assert_equal (key, uint64 (17128873702567354935)); %!error ... %! keyHash (calendarDuration (0, 0, 0), uint64 ([1, 2])) %!error ... %! keyHash (calendarDuration (0, 0, 0), 2231107818551636405) ## A zero offset basis is a basis, not the absence of one, and a falsy base ## must still be validated rather than silently replaced by the default. %!test %! a = keyHash (calmonths (1)); %! assert_equal (isequal (a, keyHash (calmonths (1), uint64 (0))), false); %! assert_equal (keyHash (calmonths (1), []), a); %!error ... %! keyHash (calendarDuration (0, 0, 0), 0) %!error ... %! keyHash (calendarDuration (0, 0, 0), uint64 ([0, 1])) ## Calendar units are merged where they are interchangeable and kept apart ## where they are not, which already matched MATLAB. These hash codes are ## unchanged from 1.3.0. %!test %! assert_equal (keyMatch (calmonths (12), calyears (1)), true); %! assert_equal (keyHash (calmonths (12)), keyHash (calyears (1))); %! assert_equal (keyMatch (calweeks (1), caldays (7)), true); %! assert_equal (keyHash (calweeks (1)), keyHash (caldays (7))); %! assert_equal (keyMatch (calmonths (1), caldays (30)), false); %! assert_equal (keyMatch (calmonths (1), calmonths (2)), false); ## size with no dimension named pads with ones when more outputs are asked for ## than the array has dimensions, and folds the trailing dimensions into the ## last output when fewer are asked for. Naming a dimension instead fixes how ## many outputs there may be. %!assert_equal (size ([calmonths(1), calmonths(2), calmonths(3)]), [1, 3]) %!assert_equal (size (calendarDuration ([], [], [])), [0, 0]) %!test %! A = calmonths (1); A(1) = []; %! assert_equal (size (A), [1, 0]); %!test %! A = [calmonths(1), calmonths(2), calmonths(3); ... %! calmonths(4), calmonths(5), calmonths(6)]; %! assert_equal (size (A), [2, 3]); %! assert_equal (size (A, 1), 2); %! assert_equal (size (A, 2), 3); %! assert_equal (size (A, 3), 1); %! assert_equal (size (A, 4), 1); %! assert_equal (size (A, [1, 2]), [2, 3]); %! assert_equal (size (A, [2, 3]), [3, 1]); %!test %! A = [calmonths(1), calmonths(2), calmonths(3); ... %! calmonths(4), calmonths(5), calmonths(6)]; %! [r, c] = size (A); %! assert_equal ([r, c], [2, 3]); %!test %! A = [calmonths(1), calmonths(2), calmonths(3); ... %! calmonths(4), calmonths(5), calmonths(6)]; %! [r, c, p] = size (A); %! assert_equal ([r, c, p], [2, 3, 1]); %!test %! A = [calmonths(1), calmonths(2), calmonths(3); ... %! calmonths(4), calmonths(5), calmonths(6)]; %! [r, c, p, q] = size (A); %! assert_equal ([r, c, p, q], [2, 3, 1, 1]); %!test %! A = reshape (repmat (calmonths (1), 2, 4), 2, 2, 2); %! assert_equal (size (A), [2, 2, 2]); %! [r, c] = size (A); %! assert_equal ([r, c], [2, 4]); %!test %! A = reshape (repmat (calmonths (1), 2, 4), 2, 2, 2); %! [r, c, p] = size (A); %! assert_equal ([r, c, p], [2, 2, 2]); %!error ... %! [r, c] = size (calmonths ([1, 2, 3]), 1) ## ndims, numel and length. length is the longest dimension, and zero for an ## empty array however it is shaped. %!assert_equal (ndims ([calmonths(1), calmonths(2)]), 2) %!assert_equal (ndims (calendarDuration ([], [], [])), 2) %!assert_equal (ndims (reshape (repmat (calmonths (1), 2, 4), 2, 2, 2)), 3) %!assert_equal (numel ([calmonths(1), calmonths(2), calmonths(3)]), 3) %!assert_equal (numel (calmonths (1)), 1) %!assert_equal (numel (calendarDuration ([], [], [])), 0) %!assert_equal (numel (reshape (repmat (calmonths (1), 2, 4), 2, 2, 2)), 8) %!test %! A = [calmonths(1), calmonths(2), calmonths(3); ... %! calmonths(4), calmonths(5), calmonths(6)]; %! assert_equal (numel (A), 6); %! assert_equal (length (A), 3); %!assert_equal (length ([calmonths(1), calmonths(2), calmonths(3)]), 3) %!assert_equal (length (reshape (repmat (calmonths (1), 2, 4), 2, 2, 2)), 2) %!assert_equal (length (calendarDuration ([], [], [])), 0) %!test %! A = calmonths (1); A(1) = []; %! assert_equal (length (A), 0); ## nnz counts elements that are not a zero span; a time-only span counts, and ## so does a missing one, neither being zero. %!assert_equal (nnz ([calmonths(1), calendarDuration(0, 0, 0), ... %! calendarDuration(0, 0, 2)]), 2) %!assert_equal (nnz (calendarDuration ([0, 0], [0, 0], [0, 0])), 0) %!assert_equal (nnz (calendarDuration (0, 0, 0, 5, 0, 0)), 1) %!assert_equal (nnz (calendarDuration (NaN, 0, 0)), 1) ## keyHash returns a uint64 for the whole array. Its value is not a contract ## and is never asserted -- MATLAB's varies between sessions -- only that equal ## spans hash alike and unequal ones do not. %!assert_equal (class (keyHash (calmonths (1))), 'uint64') %!assert_equal (size (keyHash ([calmonths(1), calmonths(2)])), [1, 1]) %!assert_equal (isequal (keyHash (calmonths (1)), ... %! keyHash (calmonths (1))), true) %!assert_equal (isequal (keyHash (calmonths (1)), ... %! keyHash (calmonths (2))), false) ################################################################################ ## ** Query Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'iscolumn' 'isempty' 'isequal' 'isequaln' ## ## 'isfinite' 'isinf' 'ismatrix' 'ismissing' ## ## 'isnan' 'isreal' 'isrow' 'isscalar' ## ## 'isvector' ## ## ## ################################################################################ %!assert_equal (iscolumn (calendarDuration (0, 0, 0)), true) %!assert_equal (iscolumn (calendarDuration ([], [], [])), false) %!assert_equal (iscolumn (calendarDuration (ones (1, 0), ones (1, 0), ... %! ones (1, 0))), false) %!assert_equal (iscolumn (calendarDuration (ones (0, 1), ones (0, 1), ... %! ones (0, 1))), true) %!assert_equal (iscolumn (calendarDuration ([1, 1], 1, 1)), false) %!assert_equal (iscolumn (calendarDuration ([1; 1], 1, 1)), true) %!assert_equal (iscolumn (calendarDuration (ones (2), 1, 1)), false) %!assert_equal (iscolumn (calendarDuration (1, 1, zeros (2, 3, 4))), false) %!assert_equal (isempty (calendarDuration (0, 0, 0)), false) %!assert_equal (isempty (calendarDuration ([], [], [])), true) %!assert_equal (isempty (calendarDuration (ones (1, 0), ones (1, 0), ... %! ones (1, 0))), true) %!assert_equal (isempty (calendarDuration (ones (0, 1), ones (0, 1), ... %! ones (0, 1))), true) %!test %! x = ones ([2, 3, 0, 2]); %! assert_equal (isempty (calendarDuration (x, x, x)), true); %! assert_equal (isempty (calendarDuration (x, x, x, x, x, x)), true) %! assert_equal (isempty (calendarDuration (x, x, x, hours (x))), true); %!test %! x = ones ([2, 3, 1, 2]); %! assert_equal (isempty (calendarDuration (x, x, x)), false); %! assert_equal (isempty (calendarDuration (x, x, x, x, x, x)), false) %! assert_equal (isempty (calendarDuration (x, x, x, hours (x))), false); %!assert_equal (isequal (caldays (5), caldays (5)), true) %!assert_equal (isequal (caldays (NaN), caldays (NaN)), false) %!assert_equal (isequal (caldays (5), caldays (6)), false) %!assert_equal (isequal (caldays ([5, NaN]), caldays ([5, NaN])), false) %!assert_equal (isequal (caldays (5), caldays (5), caldays (5)), true) %!assert_equal (isequal (caldays (5), caldays (6), calweeks (1)), false) %!assert_equal (isequal (caldays (7), caldays (7), calweeks (1)), true) %!assert_equal (isequal (caldays ([1:3]), caldays ([1, 2, 3])), true) %!assert_equal (isequal (caldays ([1:3]), caldays ([1, NaN, 3])), false) %!assert_equal (isequal (calmonths (5), calmonths (5)), true) %!assert_equal (isequal (calmonths (5), calmonths (6)), false) %!assert_equal (isequal (calmonths (NaN), calmonths (NaN)), false) %!assert_equal (isequal (calmonths (5), calmonths (5), calmonths (5)), true) %!assert_equal (isequal (calmonths (5), calmonths (6), calquarters (2)), false) %!assert_equal (isequal (calmonths (6), calmonths (6), calquarters (2)), true) %!assert_equal (isequal (calmonths ([1:3]), calmonths ([1, 2, 3])), true) %!assert_equal (isequal (calmonths ([1:3]), calmonths ([1, NaN, 3])), false) %!assert_equal (isequal (calmonths ([1, NaN, 3]), calmonths ([1, NaN, ... %! 3])), false) %!assert_equal (isequal (calendarDuration (0, 0, 0, hours (ones (3))), ... %! calendarDuration (0, 0, 0, hours (ones (3)))), true) %!assert_equal (isequal (calendarDuration (0, 0, 0, hours (nan (3))), ... %! calendarDuration (0, 0, 0, hours (nan (3)))), false) %!assert_equal (isequal (calendarDuration (0, 0, 0, hours (ones (3, 3, 2))), ... %! calendarDuration (0, 0, 0, hours (ones (3, 3, 3)))), false) %!error isequal (caldays (3)) %!assert_equal (isequal (caldays (3), 1), false) %!assert_equal (isequal (caldays (3), caldays (3), hours (1)), false) %!assert_equal (isequal (caldays (3), caldays (3), days (1)), false) %!assert_equal (isequaln (caldays (5), caldays (5)), true) %!assert_equal (isequaln (caldays (NaN), caldays (NaN)), true) %!assert_equal (isequaln (caldays (5), caldays (6)), false) %!assert_equal (isequaln (caldays ([5, NaN]), caldays ([5, NaN])), true) %!assert_equal (isequaln (caldays (5), caldays (5), caldays (5)), true) %!assert_equal (isequaln (caldays (5), caldays (6), calweeks (1)), false) %!assert_equal (isequaln (caldays (7), caldays (7), calweeks (1)), true) %!assert_equal (isequaln (caldays ([1:3]), caldays ([1, 2, 3])), true) %!assert_equal (isequaln (caldays ([1:3]), caldays ([1, NaN, 3])), false) %!assert_equal (isequaln (calmonths (5), calmonths (5)), true) %!assert_equal (isequaln (calmonths (NaN), calmonths (NaN)), true) %!assert_equal (isequaln (calmonths (5), calmonths (6)), false) %!assert_equal (isequaln (calmonths (5), calmonths (5), calmonths (5)), true) %!assert_equal (isequaln (calmonths (5), calmonths (6), calquarters (2)), false) %!assert_equal (isequaln (calmonths (6), calmonths (6), calquarters (2)), true) %!assert_equal (isequaln (calmonths ([1:3]), calmonths ([1, 2, 3])), true) %!assert_equal (isequaln (calmonths ([1:3]), calmonths ([1, NaN, 3])), false) %!assert_equal (isequaln (calmonths ([1, NaN, 3]), calmonths ([1, NaN, ... %! 3])), true) %!assert_equal (isequaln (calendarDuration (0, 0, 0, hours (ones (3))), ... %! calendarDuration (0, 0, 0, hours (ones (3)))), true) %!assert_equal (isequaln (calendarDuration (0, 0, 0, hours (nan (3))), ... %! calendarDuration (0, 0, 0, hours (nan (3)))), true) %!assert_equal (isequaln (calendarDuration (0, 0, 0, hours (ones (3, 3, ... %! 2))), ... %! calendarDuration (0, 0, 0, hours (ones (3, 3, 3)))), false) %!assert_equal (isequaln (calendarDuration (0, NaN, 0, hours (ones (3))), ... %! calendarDuration (0, NaN, 0, hours (ones (3)))), true) %!assert_equal (isequaln (calendarDuration (0, 0, NaN, hours (nan (3, 3, ... %! 3))), ... %! calendarDuration (0, 0, NaN, hours (nan (3, 3, 3)))), true) %!error isequaln (caldays (3)) %!assert_equal (isequaln (caldays (3), 1), false) %!assert_equal (isequaln (caldays (3), caldays (3), hours (1)), false) %!assert_equal (isequaln (caldays (3), caldays (3), days (1)), false) %!assert_equal (isfinite (caldays (1)), true) %!assert_equal (isfinite (caldays (NaN)), false) %!assert_equal (isfinite (caldays (Inf)), false) %!assert_equal (isfinite (caldays (-Inf)), false) %!assert_equal (isfinite (calendarDuration ([1, 0, 1])), true) %!assert_equal (isfinite (calendarDuration (nan (2), 1, 0)), false (2)) %!assert_equal (isfinite (calendarDuration (Inf (3), 0, 0)), false (3)) %!assert_equal (isfinite (calendarDuration (1, -Inf, [0; 0])), false (2, 1)) %!assert_equal (size (isfinite (caldays ([]))), [0, 0]) %!assert_equal (size (isfinite (caldays (ones (1, 2, 0, 2)))), [1, 2, 0, 2]) %!assert_equal (isinf (caldays (1)), false) %!assert_equal (isinf (caldays (NaN)), false) %!assert_equal (isinf (caldays (Inf)), true) %!assert_equal (isinf (caldays (-Inf)), true) %!assert_equal (isinf (calendarDuration ([1, 0, NaN])), false) %!assert_equal (isinf (calendarDuration ([1, Inf, -Inf])), false) %!assert_equal (isinf (calendarDuration (nan (2), 1, 0)), false (2)) %!assert_equal (isinf (calendarDuration (Inf (3), 0, 0)), true (3)) %!assert_equal (isinf (calendarDuration (1, -Inf, [0; 0])), true (2, 1)) %!assert_equal (size (isinf (caldays ([]))), [0, 0]) %!assert_equal (size (isinf (caldays (Inf (1, 2, 0, 2)))), [1, 2, 0, 2]) %!assert_equal (ismatrix (caldays (3)), true) %!assert_equal (ismatrix (caldays ([3, 3; 2, 1])), true) %!assert_equal (ismatrix (caldays ([3, 3, 2, 1])), true) %!assert_equal (ismatrix (caldays (ones (3, 3, 2))), false) %!assert_equal (ismatrix (calmonths (3)), true) %!assert_equal (ismatrix (calmonths ([3, 3; 2, 1])), true) %!assert_equal (ismatrix (calmonths ([3, 3, 2, 1])), true) %!assert_equal (ismatrix (calmonths (ones (3, 3, 2))), false) %!assert_equal (ismatrix (calendarDuration (0, 0, 3)), true) %!assert_equal (ismatrix (calendarDuration ([3, 3; 2, 1], 0, 0)), true) %!assert_equal (ismatrix (calendarDuration ([3, 3, 2, 1], 0, 1)), true) %!assert_equal (ismatrix (calendarDuration (ones (3, 3, 2), 0, 0)), false) %!assert_equal (ismatrix (calendarDuration (0, 0, 1, hours (ones (3, ... %! 3)))), true) %!assert_equal (ismatrix (calendarDuration (0, 0, 1, hours (ones (3, 3, ... %! 2)))), false) %!assert_equal (ismissing (caldays (1)), false) %!assert_equal (ismissing (caldays (NaN)), true) %!assert_equal (ismissing (caldays (Inf)), false) %!assert_equal (ismissing (caldays (-Inf)), false) %!assert_equal (ismissing (calendarDuration ([1, 0, NaN])), true) %!assert_equal (ismissing (calendarDuration ([1, Inf, -Inf])), true) %!assert_equal (ismissing (calendarDuration (nan (2), 1, 0)), true (2)) %!assert_equal (ismissing (calendarDuration (Inf (3), 0, 0)), false (3)) %!assert_equal (ismissing (calendarDuration (1, -Inf, [0; NaN])), [false; true]) %!assert_equal (size (ismissing (caldays ([]))), [0, 0]) %!assert_equal (ismissing (caldays (nan (1, 2, 0, 2))), true (1, 2, 0, 2)) %!assert_equal (size (ismissing (caldays (nan (1, 2, 0, 2)))), [1, 2, 0, 2]) %!assert_equal (ismissing (caldays ([1, NaN, 3]), missing), ... %! [false, true, false]) %!assert_equal (ismissing (caldays ([1, NaN, 3]), caldays (NaN)), ... %! [false, true, false]) %!assert_equal (ismissing (caldays ([1, NaN, 3]), caldays ([NaN, 3])), ... %! [false, true, true]) %!assert_equal (ismissing (calmonths ([1, 2]), caldays (31)), [false, false]) %!assert_equal (ismissing ([calweeks(1), caldays(8)], caldays (7)), ... %! [true, false]) %!error ... %! ismissing (caldays (3), caldays (3), 3) %!error ... %! ismissing (caldays (3), 3) %!error ... %! ismissing (caldays (3), caldays (ones (2))) %!assert_equal (isnan (caldays (1)), false) %!assert_equal (isnan (caldays (NaN)), true) %!assert_equal (isnan (caldays (Inf)), false) %!assert_equal (isnan (caldays (-Inf)), false) %!assert_equal (isnan (calendarDuration ([1, 0, NaN])), true) %!assert_equal (isnan (calendarDuration ([1, Inf, -Inf])), true) %!assert_equal (isnan (calendarDuration (nan (2), 1, 0)), true (2)) %!assert_equal (isnan (calendarDuration (Inf (3), 0, 0)), false (3)) %!assert_equal (isnan (calendarDuration (1, -Inf, [0; NaN])), [false; true]) %!assert_equal (size (isnan (caldays ([]))), [0, 0]) %!assert_equal (isnan (caldays (nan (1, 2, 0, 2))), true (1, 2, 0, 2)) %!assert_equal (size (isnan (caldays (nan (1, 2, 0, 2)))), [1, 2, 0, 2]) %!assert_equal (isscalar (isreal (caldays (ones (2, 2, 2)))), true) %!assert_equal (isreal (calendarDuration ([1, 1, 1])), true) %!assert_equal (isrow (calendarDuration (0, 0, 0)), true) %!assert_equal (isrow (calendarDuration ([], [], [])), false) %!assert_equal (isrow (calendarDuration (ones (1, 0), ones (1, 0), ones (1, ... %! 0))), true) %!assert_equal (isrow (calendarDuration (ones (0, 1), ones (0, 1), ones (0, ... %! 1))), false) %!assert_equal (isrow (calmonths (1)), true) %!assert_equal (isrow (calmonths ([1, 2, 3])), true) %!assert_equal (isrow (calyears ([1; 2; 3])), false) %!assert_equal (isrow (calendarDuration (1, 1, 1, 1, 1, 1)), true) %!assert_equal (isrow (calendarDuration (1, 1, 1, 1, 1, [1, 2, 3])), true) %!assert_equal (isrow (calendarDuration (1, 1, 1, 1, 1, ones (2))), false) %!assert_equal (isrow (calendarDuration (1, 1, ones (1, 1, 2))), false) %!assert_equal (isrow (calendarDuration (1, 1, ones (1, 2, 1))), true) %!assert_equal (isscalar (calmonths (1)), true) %!assert_equal (isscalar (calmonths ([1, 2, 3])), false) %!assert_equal (isscalar (calyears ([1; 2; 3])), false) %!assert_equal (isscalar (calendarDuration (0, 0, 0)), true) %!assert_equal (isscalar (calendarDuration ([], [], [])), false) %!assert_equal (isscalar (calendarDuration (1, 1, 1, 1, 1, 1)), true) %!assert_equal (isscalar (calendarDuration (1, 1, 1, 1, 1, [1, 2, 3])), false) %!assert_equal (isscalar (calendarDuration (1, 1, 1, 1, 1, ones (2))), false) %!assert_equal (isscalar (calendarDuration (1, 1, ones (1, 1, 2))), false) %!assert_equal (isscalar (calendarDuration (1, 1, ones (1, 2, 1))), false) %!assert_equal (isscalar (calendarDuration (1, 1, ones (1, 1, 1))), true) %!assert_equal (isscalar (calendarDuration (1, 1, 1, hours (5))), true) %!assert_equal (isvector (calendarDuration (0, 0, 0)), true) %!assert_equal (isvector (calendarDuration ([], [], [])), false) %!assert_equal (isvector (calendarDuration (ones (1, 0), ones (1, 0), ... %! ones (1, 0))), true) %!assert_equal (isvector (calendarDuration (ones (0, 1), ones (0, 1), ... %! ones (0, 1))), true) %!assert_equal (isvector (calmonths (1)), true) %!assert_equal (isvector (calmonths ([1, 2, 3])), true) %!assert_equal (isvector (calyears ([1; 2; 3])), true) %!assert_equal (isvector (calendarDuration (1, 1, 1, 1, 1, 1)), true) %!assert_equal (isvector (calendarDuration (1, 1, 1, 1, 1, [1, 2, 3])), true) %!assert_equal (isvector (calendarDuration (1, 1, 1, 1, 1, ones (2))), false) %!assert_equal (isvector (calendarDuration (1, 1, ones (1, 1, 2))), false) %!assert_equal (isvector (calendarDuration (1, 1, ones (2, 1, 2))), false) %!assert_equal (isvector (calendarDuration (1, 1, ones (1, 2, 1))), true) %!assert_equal (isvector (calendarDuration (1, 1, ones (2, 1, 1))), true) ################################################################################ ## ** Mathematical Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'minus' 'uminus' 'plus' 'uplus' ## ## 'times' 'mtimes' ## ## ## ################################################################################ %!test %! A = caldays ([1, 2, 3]); %! B = caldays (1); %! assert_equal (caldays (A - B), [0, 1, 2]); %! assert_equal (caldays (B - A), [0, -1, -2]); %!test %! A = calweeks (1); %! B = days ([2, 2]); %! [c1, c2] = split (A - B, {'days', 'time'}); %! assert_equal (c1, [7, 7]); %! assert_equal (days (c2), [-2, -2]); %!test %! A = calweeks (1); %! B = [2, 2]; %! [c1, c2] = split (A - B, {'days', 'time'}); %! assert_equal (c1, [7, 7]); %! assert_equal (days (c2), [-2, -2]); %!test %! A = days ([2, 2]); %! B = calweeks (1); %! [c1, c2] = split (A - B, {'days', 'time'}); %! assert_equal (c1, [-7, -7]); %! assert_equal (days (c2), [2, 2]); %!test %! A = [2, 2]; %! B = calweeks (1); %! [c1, c2] = split (A - B, {'days', 'time'}); %! assert_equal (c1, [-7, -7]); %! assert_equal (days (c2), [2, 2]); %!error ... %! calweeks (1) - 'a' %!error ... %! calweeks (1) - {1} %!error ... %! calweeks (1) - struct ('a', 1) %!test %! A = caldays ([1, 2, 3]); %! assert_equal (caldays (-A), [-1, -2, -3]); %!test %! A = randi (10, 3, 4, 5); %! B = caldays (A); %! C = - caldays (A); %! assert_equal (caldays (B + C), zeros (3, 4, 5)); %!test %! A = caldays ([1, 2, 3]); %! B = caldays (1); %! assert_equal (caldays (A + B), [2, 3, 4]); %! assert_equal (caldays (B + A), [2, 3, 4]); %!test %! A = calweeks (1); %! B = days ([2, 2]); %! [c1, c2] = split (A + B, {'days', 'time'}); %! assert_equal (c1, [7, 7]); %! assert_equal (days (c2), [2, 2]); %!test %! A = calweeks (1); %! B = [2, 2]; %! [c1, c2] = split (A + B, {'days', 'time'}); %! assert_equal (c1, [7, 7]); %! assert_equal (days (c2), [2, 2]); %!test %! A = days ([2, 2]); %! B = calweeks (1); %! [c1, c2] = split (A + B, {'days', 'time'}); %! assert_equal (c1, [7, 7]); %! assert_equal (days (c2), [2, 2]); %!test %! A = [2, 2]; %! B = calweeks (1); %! [c1, c2] = split (A + B, {'days', 'time'}); %! assert_equal (c1, [7, 7]); %! assert_equal (days (c2), [2, 2]); %!error ... %! calweeks (1) + 'a' %!error ... %! calweeks (1) + {1} %!error ... %! calweeks (1) + struct ('a', 1) %!test %! A = calendarDuration ([1, 1, 5, 4, 30, 2]); %! B = A .* [1, 3]; %! [y, m, d, t] = split (B, {'years', 'months', 'days', 'time'}); %! assert_equal (y, [1, 3]); %! assert_equal (m, [1, 3]); %! assert_equal (d, [5, 15]); %! assert_equal (seconds (t), [16202, 48606], 4e-12); %!test %! A = calendarDuration ([1, 1, 5, 4, 30, 2]); %! B = A .* [1; 3]; %! [y, m, d, t] = split (B, {'years', 'months', 'days', 'time'}); %! assert_equal (y, [1; 3]); %! assert_equal (m, [1; 3]); %! assert_equal (d, [5; 15]); %! assert_equal (seconds (t), [16202; 48606], 4e-12); %!test %! A = calendarDuration ([1, 1, 5, 4, 30, 2]); %! B = [1; 3] .* A; %! [y, m, d, t] = split (B, {'years', 'months', 'days', 'time'}); %! assert_equal (y, [1; 3]); %! assert_equal (m, [1; 3]); %! assert_equal (d, [5; 15]); %! assert_equal (seconds (t), [16202; 48606], 4e-12); %!test %! A = calendarDuration ([1, 1, 5, 4, 30, 2]); %! B = 2 .* A; %! [y, m, d, t] = split (B, {'years', 'months', 'days', 'time'}); %! assert_equal (y, 2); %! assert_equal (m, 2); %! assert_equal (d, 10); %! assert_equal (seconds (t), 32404, 4e-12); %!error ... %! calweeks (1) .* 'a' %!error ... %! calweeks (1) .* {1} %!error ... %! calweeks (1) .* struct ('a', 1) %!test %! A = calendarDuration ([1, 1, 5, 4, 30, 2]); %! B = 2 * A; %! [y, m, d, t] = split (B, {'years', 'months', 'days', 'time'}); %! assert_equal (y, 2); %! assert_equal (m, 2); %! assert_equal (d, 10); %! assert_equal (seconds (t), 32404, 4e-12); %!test %! A = calendarDuration ([1, 2], 0, [4, 5]); %! B = A * [1; 2]; %! [y, d] = split (B, {'years', 'days'}); %! assert_equal (y, [1, 2] * [1; 2]); %! assert_equal (d, [4, 5] * [1; 2]); %!test %! A = calendarDuration ([1, 2], 0, [4, 5]); %! B = A * [4; 2]; %! [y, d] = split (B, {'years', 'days'}); %! assert_equal (y, [1, 2] * [4; 2]); %! assert_equal (d, [4, 5] * [4; 2]); %!test %! A = calendarDuration ([1, 2], 0, [4, 5]); %! B = [4; 2] * A; %! [y, d] = split (B, {'years', 'days'}); %! assert_equal (y, [4; 2] * [1, 2]); %! assert_equal (d, [4; 2] * [4, 5]); %!error ... %! calweeks (1) * 'a' %!error ... %! calweeks (1) * {1} %!error ... %! calweeks (1) * struct ('a', 1) ## A zero component stays a positive zero through negation and through scaling ## by a negative number. 1/(+0) is Inf and 1/(-0) is -Inf, which is the only ## way an equality test can see the sign of a zero. %!assert_equal (1 / seconds (time (-calendarDuration (1, 2, 3))), Inf) %!assert_equal (1 / calmonths (-calendarDuration (0, 0, 3, 4, 5, 6.5)), Inf) %!assert_equal (1 / caldays (-calendarDuration (0, 0, 0, 4, 5, 6.5)), Inf) %!assert_equal (1 / seconds (time (calendarDuration (1, 2, 3) * -1)), Inf) %!assert_equal (1 / seconds (time (-1 * calendarDuration (1, 2, 3))), Inf) %!assert_equal (1 / seconds (time (calendarDuration (1, 2, 3) .* -1)), Inf) %!assert_equal (1 / calmonths (calendarDuration (0, 0, 3) .* -1), Inf) ## Addition and subtraction are component-wise: months to months, days to days, ## time to time. No component ever rolls into another -- a time part beyond 24 ## hours stays in the time part, and days never become months. %!test %! C = calendarDuration (1, 2, 3) + calendarDuration (0, 1, 2); %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal (mo, 15); %! assert_equal (d, 5); %! assert_equal (seconds (t), 0); %!test %! C = calendarDuration (1, 2, 3) - calendarDuration (0, 1, 2); %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal (mo, 13); %! assert_equal (d, 1); %! assert_equal (seconds (t), 0); %!test %! C = calendarDuration (0, 2, -14) + calendarDuration (0, -1, 20); %! [mo, d] = split (C, {'months', 'days'}); %! assert_equal (mo, 1); %! assert_equal (d, 6); %!test %! C = calendarDuration (0, 1, 2) - calendarDuration (1, 2, 3); %! [mo, d] = split (C, {'months', 'days'}); %! assert_equal (mo, -13); %! assert_equal (d, -1); %!test %! C = calendarDuration (0, 0, 1, 5, 0, 0) + calendarDuration (0, 0, 1, 20, ... %! 0, 0); %! [d, t] = split (C, {'days', 'time'}); %! assert_equal (d, 2); %! assert_equal (seconds (t), 90000); %! assert_equal (char (C), '2d 25h 0m 0s'); %!test %! C = calendarDuration (0, 0, 0, 13, 0, 0) + calendarDuration (0, 0, 0, 13, ... %! 0, 0); %! assert_equal (seconds (split (C, 'time')), 93600); %! assert_equal (char (C), '26h 0m 0s'); %!assert_equal (char (calendarDuration (0, 0, 0, 25, 0, 0)), '25h 0m 0s') %!assert_equal (char (caldays (1) + hours (25)), '1d 25h 0m 0s') %!assert_equal (char (calendarDuration (0, 0, 1, -25, 0, 0)), '1d -25h 0m 0s') %!assert_equal (char (calendarDuration (0, 0, 0, 0, 90, 0)), '1h 30m 0s') %!assert_equal (char (calendarDuration (0, 0, 40)), '40d') %!assert_equal (char (calendarDuration (0, 13, 0)), '1y 1mo') ## A duration addend goes to the time component whatever unit it was written ## in, and so does a bare numeric, which counts days. %!test %! C = calendarDuration (0, 1, 2) + hours (5); %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d], [1, 2]); %! assert_equal (seconds (t), 18000); %!assert_equal (seconds (split (calendarDuration (0, 1, 2) + hours (25), ... %! 'time')), 90000) %!assert_equal (seconds (split (calendarDuration (0, 1, 2) + days (3), ... %! 'time')), 259200) %!assert_equal (seconds (split (hours (5) + calendarDuration (0, 1, 2), ... %! 'time')), 18000) %!assert_equal (seconds (split (calendarDuration (0, 1, 2) - hours (5), ... %! 'time')), -18000) %!test %! C = hours (5) - calendarDuration (0, 1, 2); %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d], [-1, -2]); %! assert_equal (seconds (t), 18000); %!assert_equal (seconds (split (calendarDuration (0, 1, 2) + 3, ... %! 'time')), 259200) %!assert_equal (seconds (split (3 + calendarDuration (0, 1, 2), ... %! 'time')), 259200) %!assert_equal (seconds (split (calendarDuration (0, 1, 2) - 3, ... %! 'time')), -259200) %!assert_equal (seconds (split (calendarDuration (0, 1, 2) + (-3), ... %! 'time')), -259200) %!assert_equal (seconds (split (calendarDuration (0, 1, 2) + 1.5, ... %! 'time')), 129600) %!test %! C = 3 - calendarDuration (0, 1, 2); %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d], [-1, -2]); %! assert_equal (seconds (t), 259200); %!test %! C = calendarDuration (0, 1, 2) + [1, 2]; %! assert_equal (size (C), [1, 2]); %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal (mo, [1, 1]); %! assert_equal (d, [2, 2]); %! assert_equal (seconds (t), [86400, 172800]); %!test %! C = calendarDuration (0, [1, 2], 0) + calendarDuration (0, 0, [10; 20]); %! assert_equal (size (C), [2, 2]); %! [mo, d] = split (C, {'months', 'days'}); %! assert_equal (mo, [1, 2; 1, 2]); %! assert_equal (d, [10, 10; 20, 20]); %!assert_equal (size (calendarDuration (0, 1, 2) + calendarDuration ([], [], ... %! [])), [0, 0]) ## A logical operand counts as one, and no other numeric type is accepted. %!assert_equal (seconds (split (calendarDuration (1, 2, 3) + true, ... %! 'time')), 86400) %!error ... %! calendarDuration (1, 2, 3) + single (2) %!error ... %! calendarDuration (1, 2, 3) + int8 (2) %!error ... %! calendarDuration (1, 2, 3) + 'a' %!error ... %! calendarDuration (1, 2, 3) + {1} ## A non-finite operand takes over every component of its element. %!test %! C = calendarDuration (0, 1, 2) + NaN; %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [NaN, NaN, NaN]); %! assert_equal (char (C), 'NaN'); %!test %! C = calendarDuration (0, 1, 2) + Inf; %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [Inf, Inf, Inf]); %! assert_equal (char (C), 'Inf'); %!test %! C = calendarDuration (0, 1, 2) + calendarDuration (0, NaN, 0); %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [NaN, NaN, NaN]); ## Unary minus negates every component; unary plus copies. %!test %! C = -calendarDuration (1, 2, 3); %! [mo, d] = split (C, {'months', 'days'}); %! assert_equal ([mo, d], [-14, -3]); %! assert_equal (char (C), '-1y -2mo -3d'); %!test %! C = +calendarDuration (1, 2, 3); %! [mo, d] = split (C, {'months', 'days'}); %! assert_equal ([mo, d], [14, 3]); %!test %! C = -calendarDuration (0, 2, -14); %! [mo, d] = split (C, {'months', 'days'}); %! assert_equal ([mo, d], [-2, 14]); %! assert_equal (char (C), '-2mo 14d'); %!assert_equal (char (-calendarDuration (0, 0, 0)), '0d') ## Scaling multiplies every component, and only by a whole number. %!test %! C = calendarDuration (1, 2, 3) * 2; %! [mo, d] = split (C, {'months', 'days'}); %! assert_equal ([mo, d], [28, 6]); %! assert_equal (char (C), '2y 4mo 6d'); %!test %! C = 2 * calendarDuration (1, 2, 3); %! assert_equal (split (C, 'months'), 28); %!test %! C = calendarDuration (1, 2, 3) * (-2); %! [mo, d] = split (C, {'months', 'days'}); %! assert_equal ([mo, d], [-28, -6]); %!assert_equal (char (calendarDuration (1, 2, 3) * 0), '0d') %!assert_equal (char (calendarDuration (1, 2, 3) * 2.0), '2y 4mo 6d') %!assert_equal (char (calendarDuration (1, 2, 3) * true), '1y 2mo 3d') %!test %! C = calendarDuration (1, 2, 3) .* [1, 2, 3]; %! assert_equal (size (C), [1, 3]); %! [mo, d] = split (C, {'months', 'days'}); %! assert_equal (mo, [14, 28, 42]); %! assert_equal (d, [3, 6, 9]); %!test %! C = [1, 2, 3] .* calendarDuration (1, 2, 3); %! assert_equal (split (C, 'months'), [14, 28, 42]); %!test %! C = calendarDuration (1, 2, 3) * [1; 2]; %! assert_equal (size (C), [2, 1]); %! assert_equal (split (C, 'months'), [14; 28]); %!assert_equal (split (calendarDuration (0, [1, 2], 0) * 2, 'months'), [2, 4]) %!assert_equal (size (calendarDuration (1, 2, 3) * []), [0, 0]) %!error ... %! calendarDuration (1, 2, 3) * 0.5 %!error ... %! calendarDuration (0, 1, 0) * 1.5 %!error ... %! calendarDuration (0, 0, 0, 1, 0, 0) * 0.5 %!error ... %! calendarDuration (1, 2, 3) .* [1, 0.5] %!error ... %! calendarDuration (1, 2, 3) * single (2) %!error ... %! calendarDuration (1, 2, 3) * uint8 (2) %!error ... %! calendarDuration (1, 2, 3) .* single (2) %!error ... %! calendarDuration (1, 2, 3) * calendarDuration (1, 2, 3) ## A non-finite factor scales the span as a whole, by its sign: a zero ## component does not poison the result, but a span whose components disagree ## in sign, or that is entirely zero, has no sign and gives NaN. %!test %! C = calendarDuration (1, 2, 3) * Inf; %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [Inf, Inf, Inf]); %!test %! C = calendarDuration (1, 2, 3) * (-Inf); %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [-Inf, -Inf, -Inf]); %!test %! C = calendarDuration (-1, -2, -3) * Inf; %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [-Inf, -Inf, -Inf]); %!test %! C = calendarDuration (-1, -2, -3) * (-Inf); %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [Inf, Inf, Inf]); %!test %! C = calendarDuration (0, 0, 3) * Inf; %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [Inf, Inf, Inf]); %!test %! C = calendarDuration (0, 0, 0, 5, 0, 0) * Inf; %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [Inf, Inf, Inf]); %!test %! C = calendarDuration (0, 0, 0, -5, 0, 0) * Inf; %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [-Inf, -Inf, -Inf]); %!test %! C = calendarDuration (0, -2, 14) * Inf; %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [NaN, NaN, NaN]); %!test %! C = calendarDuration (0, 2, -14) * (-Inf); %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [NaN, NaN, NaN]); %!test %! C = calendarDuration (0, 0, 0) * Inf; %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [NaN, NaN, NaN]); %!test %! C = calendarDuration (1, 2, 3) * NaN; %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [NaN, NaN, NaN]); %!test %! C = calendarDuration (0, Inf, 0) * 2; %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [Inf, Inf, Inf]); %!test %! C = calendarDuration (0, Inf, 0) * (-2); %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [-Inf, -Inf, -Inf]); %!test %! C = calendarDuration (0, Inf, 0) * 0; %! [mo, d, t] = split (C, {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [NaN, NaN, NaN]); %!test %! C = calendarDuration (0, [1, -2], [3, -4]) * Inf; %! assert_equal (split (C, 'months'), [Inf, -Inf]); %!test %! C = calendarDuration (1, 2, 3) .* [Inf, -Inf]; %! assert_equal (split (C, 'months'), [Inf, -Inf]); %!test %! C = calendarDuration (1, 2, 3) .* [1, NaN]; %! assert_equal (split (C, 'months'), [14, NaN]); ## Adding or subtracting two spans displays either one's units: the format is ## the character union in 'yqmwdt' order, the same rule concatenation follows. ## A duration or numeric addend names no units, so the span keeps its own, and ## scaling and negation keep it too. %!assert_equal ((calquarters (1) + calweeks (1)).Format, 'yqmwdt') %!assert_equal ((calquarters (1) - calweeks (1)).Format, 'yqmwdt') %!assert_equal ((calweeks (1) + calquarters (1)).Format, 'yqmwdt') %!assert_equal ((calweeks (1) + calmonths (1)).Format, 'ymwdt') %!assert_equal ((calquarters (1) + calquarters (2)).Format, 'qmdt') %!assert_equal ((calweeks (1) + calendarDuration (0, 0, 0)).Format, 'ymwdt') %!assert_equal ((calweeks (1) + hours (5)).Format, 'ymwdt') %!assert_equal ((calweeks (1) + 5).Format, 'ymwdt') %!assert_equal ((calquarters (1) * 2).Format, 'qmdt') %!assert_equal ((-calweeks (1)).Format, 'ymwdt') %!test %! C = calquarters (1) + calweeks (1); %! assert_equal (calmonths (C), 3); %! assert_equal (split (C, 'days'), 7); %!test %! C = calquarters (1) - calweeks (1); %! assert_equal (calmonths (C), 3); %! assert_equal (split (C, 'days'), -7); ################################################################################ ## ** Equality and Filter Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'eq' 'ne' 'unique' ## ## ## ################################################################################ %!assert_equal (caldays (3) == caldays (3), true) %!assert_equal (caldays (3) == caldays (4), false) %!assert_equal (caldays (14) == calweeks (2), true) %!assert_equal (calmonths (15) == calquarters (5), true) %!assert_equal (calmonths ([15, 12, NaN]) == calquarters ([5, 4, 3]), [true, ... %! true, false]) %!assert_equal (calyears (ones (2)) == calmonths ([12, 15]), logical ([1, ... %! 0; 1, 0])) %!assert_equal (calyears (ones (2)) == calmonths ([12; 15]), logical ([1, ... %! 1; 0, 0])) %!assert_equal (calmonths ([3, 4]) == calmonths ([3, 4]), [true, true]) %!assert_equal (calmonths ([3, NaN]) == calmonths ([3, 4]), [true, false]) %!assert_equal (calmonths ([3; NaN]) == calmonths ([3; NaN]), [true; false]) %!assert_equal (calendarDuration ([1; 2], 0, 0) == calyears ([1, ... %! 2]), logical (eye (2))) %!error ... %! caldays (3) == NaN %!error ... %! calendarDuration (0, 0, 0, hours (5)) == hours (5) %!error ... %! 5 == calyears (5) %!error ... %! '1y' == calyears(1) %!assert_equal (caldays (3) != caldays (3), false) %!assert_equal (caldays (3) != caldays (4), true) %!assert_equal (caldays (14) != calweeks (2), false) %!assert_equal (calmonths (15) != calquarters (5), false) %!assert_equal (calmonths ([15, 12, NaN]) != calquarters ([5, 4, ... %! 3]), [false, false, true]) %!assert_equal (calyears (ones (2)) != calmonths ([12, 15]), logical ([0, ... %! 1; 0, 1])) %!assert_equal (calyears (ones (2)) != calmonths ([12; 15]), logical ([0, ... %! 0; 1, 1])) %!assert_equal (calmonths ([3, 4]) != calmonths ([3, 4]), [false, false]) %!assert_equal (calmonths ([3, NaN]) != calmonths ([3, 4]), [false, true]) %!assert_equal (calmonths ([3; NaN]) != calmonths ([3; NaN]), [false; true]) %!assert_equal (calendarDuration ([1; 2], 0, 0) != calyears ([2, ... %! 1]), logical (eye (2))) %!error ... %! caldays (3) != NaN %!error ... %! calendarDuration (0, 0, 0, hours (5)) != hours (5) %!error ... %! 5 != calyears (5) %!error ... %! '1y' != calyears(1) %!test %! U = unique (caldays ([1, 2, 2, 3])); %! assert_equal (caldays (U), [1, 2, 3]); %!test %! U = unique (caldays ([1, 2; 2, 3; 1, 2]), 'rows'); %! assert_equal (caldays (U), [1, 2; 2, 3]); %!test %! U = unique (caldays ([1, 3, 2, 2])); %! assert_equal (caldays (U), [1, 2, 3]); %!test %! U = unique (caldays ([1, 3, 2, 2]), 'sorted'); %! assert_equal (caldays (U), [1, 2, 3]); %!test %! U = unique (caldays ([1, 3, 2, 2]), 'stable'); %! assert_equal (caldays (U), [1, 3, 2]); %!test %! [U, idx] = unique (caldays ([1, 3, 2, 2]), 'first'); %! assert_equal (caldays (U), [1, 2, 3]); %! assert_equal (idx, [1; 3; 2]); %!test %! [U, idx] = unique (caldays ([1, 3, 2, 2]), 'last'); %! assert_equal (caldays (U), [1, 2, 3]); %! assert_equal (idx, [1; 4; 2]); %!test %! [U, idx] = unique (caldays ([1, 3, 2, 2]), 'sorted', 'first'); %! assert_equal (caldays (U), [1, 2, 3]); %! assert_equal (idx, [1; 3; 2]); %!test %! [U, idx] = unique (caldays ([1, 3, 2, 2]), 'stable', 'first'); %! assert_equal (caldays (U), [1, 3, 2]); %! assert_equal (idx, [1; 2; 3]); %!test %! [U, idx] = unique (caldays ([1, 3, 2, 2]), 'sorted', 'last'); %! assert_equal (caldays (U), [1, 2, 3]); %! assert_equal (idx, [1; 4; 2]); %!test %! [U, idx] = unique (caldays ([1, 3, 2, 2]), 'stable', 'last'); %! assert_equal (caldays (U), [1, 3, 2]); %! assert_equal (idx, [1; 2; 4]); %!test %! [B, ixA, ixB] = unique (caldays ([1, 2; 2, 3; 1, 2]), 'rows', 'sorted', ... %! 'first'); %! assert_equal (caldays (B), [1, 2; 2, 3]); %! assert_equal (ixA, [1; 2]); %! assert_equal (ixB, [1; 2; 1]); %!test %! [B, ixA, ixB] = unique (caldays ([1, 2; 2, 3; 1, 2]), 'rows', 'stable', ... %! 'first'); %! assert_equal (caldays (B), [1, 2; 2, 3]); %! assert_equal (ixA, [1; 2]); %! assert_equal (ixB, [1; 2; 1]); %!test %! [B, ixA, ixB] = unique (caldays ([1, 2; 2, 3; 1, 2]), 'rows', 'sorted', ... %! 'last'); %! assert_equal (caldays (B), [1, 2; 2, 3]); %! assert_equal (ixA, [3; 2]); %! assert_equal (ixB, [1; 2; 1]); %!test %! [B, ixA, ixB] = unique (caldays ([1, 2; 2, 3; 1, 2]), 'rows', 'stable', ... %! 'last'); %! assert_equal (caldays (B), [2, 3; 1, 2]); %! assert_equal (ixA, [2; 3]); %! assert_equal (ixB, [2; 1; 2]); %!error ... %! unique (caldays ([1, 2; 2, 3; 1, 2]), 'legacy') ## MATLAB defines no equality for calendarDuration arrays -- '==' and '~=' ## raise "Undefined function 'eq'" there -- so eq and ne are Octave extensions. ## They compare component by component, which is what MATLAB's own isequal ## does, so the two agree element for element. %!assert_equal (calendarDuration (1, 2, 3) == calendarDuration (1, 2, 3), true) %!assert_equal (calendarDuration (1, 2, 3) == calendarDuration (1, 2, 4), false) %!assert_equal (calendarDuration (1, 0, 0) == calendarDuration (0, 12, 0), true) %!assert_equal (calweeks (1) == caldays (7), true) %!assert_equal (calmonths (1) == caldays (30), false) %!assert_equal (calmonths (1) == caldays (31), false) %!assert_equal (caldays (1) == calendarDuration (0, 0, 0, 24, 0, 0), false) %!assert_equal (calendarDuration (0, 0, 0, 24, 0, 0) == calendarDuration (0, ... %! 0, 0, 0, 1440, 0), true) %!assert_equal (calendarDuration (0, 0, 0) == calendarDuration (0, 0, 0, 0, ... %! 0, 0), true) %!assert_equal (calendarDuration (NaN, 0, 0) == calendarDuration (NaN, 0, ... %! 0), false) %!assert_equal (calendarDuration (Inf, 0, 0) == calendarDuration (Inf, 0, ... %! 0), true) %!assert_equal (calendarDuration (Inf, 0, 0) == calendarDuration (-Inf, 0, ... %! 0), false) %!assert_equal (calendarDuration (0, [1, 2, 3], 0) == calmonths (2), [false, ... %! true, false]) %!assert_equal (calendarDuration ([], [], []) == calmonths (1), logical ([])) %!assert_equal (calendarDuration (1, 2, 3) != calendarDuration (1, 2, 3), false) %!assert_equal (calendarDuration (NaN, 0, 0) != calendarDuration (NaN, 0, ... %! 0), true) %!assert_equal (calendarDuration (0, [1, 2, 3], 0) != calmonths (2), [true, ... %! false, true]) %!error ... %! caldays (1) == days (1) %!error ... %! caldays (1) == 1 %!error ... %! caldays (1) != 'a' ## isequal compares component by component and answers about any pair of ## values: an argument of another class is simply not equal, never an error. ## isequal holds a NaN unequal to itself where isequaln holds it equal. %!assert_equal (isequal (calendarDuration (1, 0, 0), calendarDuration (0, ... %! 12, 0)), true) %!assert_equal (isequal (caldays (1), calendarDuration (0, 0, 0, 24, 0, ... %! 0)), false) %!assert_equal (isequal (calendarDuration (NaN, 0, 0), ... %! calendarDuration (NaN, 0, 0)), false) %!assert_equal (isequaln (calendarDuration (NaN, 0, 0), ... %! calendarDuration (NaN, 0, 0)), true) %!assert_equal (isequal (caldays (1), days (1)), false) %!assert_equal (isequal (caldays (1), 1), false) %!assert_equal (isequal (caldays (1), 'a'), false) %!assert_equal (isequal (caldays (1), {1}), false) %!assert_equal (isequaln (caldays (1), days (1)), false) %!assert_equal (isequal (caldays (1), caldays (1), days (1)), false) %!error isequal (caldays (1)) %!error isequaln (caldays (1)) ## unique sorts lexicographically by months, then days, then time, so a span ## of 30 days precedes one of 1 month and 24 hours precedes 1 day. Every NaN ## is distinct from every other and sorts last. B keeps the orientation of ## the input while ixA and ixB are always columns. %!test %! A = calmonths ([3, 1, 2, 1]); %! [B, ixA, ixB] = unique (A); %! assert_equal (size (B), [1, 3]); %! assert_equal (calmonths (B), [1, 2, 3]); %! assert_equal (ixA, [2; 3; 1]); %! assert_equal (ixB, [3; 1; 2; 1]); %!test %! A = calmonths ([3, 1, 2, 1]); %! [B, ixA, ixB] = unique (A, 'stable'); %! assert_equal (calmonths (B), [3, 1, 2]); %! assert_equal (ixA, [1; 2; 3]); %! assert_equal (ixB, [1; 2; 3; 2]); %!test %! A = calmonths ([3, 1, 2, 1]); %! [B, ixA, ixB] = unique (A, 'last'); %! assert_equal (calmonths (B), [1, 2, 3]); %! assert_equal (ixA, [4; 3; 1]); %! assert_equal (ixB, [3; 1; 2; 1]); %!test %! A = [calmonths(1), caldays(30), calmonths(1)]; %! [B, ixA, ixB] = unique (A); %! assert_equal (size (B), [1, 2]); %! [mo, d] = split (B, {'months', 'days'}); %! assert_equal (mo, [0, 1]); %! assert_equal (d, [30, 0]); %! assert_equal (ixA, [2; 1]); %! assert_equal (ixB, [2; 1; 2]); %!test %! A = [calmonths(1), caldays(30), calmonths(1)]; %! [B, ixA, ixB] = unique (A, 'stable'); %! [mo, d] = split (B, {'months', 'days'}); %! assert_equal (mo, [1, 0]); %! assert_equal (d, [0, 30]); %! assert_equal (ixA, [1; 2]); %! assert_equal (ixB, [1; 2; 1]); %!test %! A = [calmonths(1), caldays(30), calmonths(1)]; %! [B, ixA, ixB] = unique (A, 'last'); %! assert_equal (ixA, [2; 3]); %! assert_equal (ixB, [2; 1; 2]); %!test %! A = [calendarDuration(1, 0, 0), calendarDuration(0, 12, 0)]; %! [B, ixA, ixB] = unique (A); %! assert_equal (size (B), [1, 1]); %! assert_equal (calmonths (B), 12); %! assert_equal (ixA, 1); %! assert_equal (ixB, [1; 1]); %!test %! A = [calendarDuration(1, 0, 0), calendarDuration(0, 12, 0)]; %! [B, ixA] = unique (A, 'last'); %! assert_equal (calmonths (B), 12); %! assert_equal (ixA, 2); %!test %! A = [calmonths(1), calendarDuration(NaN, 0, 0), calmonths(1), ... %! calendarDuration(NaN, 0, 0)]; %! [B, ixA, ixB] = unique (A); %! assert_equal (size (B), [1, 3]); %! assert_equal (calmonths (B), [1, NaN, NaN]); %! assert_equal (ixA, [1; 2; 4]); %! assert_equal (ixB, [1; 2; 1; 3]); %!test %! A = [calmonths(1), calendarDuration(NaN, 0, 0), calmonths(1), ... %! calendarDuration(NaN, 0, 0)]; %! [B, ixA, ixB] = unique (A, 'stable'); %! assert_equal (calmonths (B), [1, NaN, NaN]); %! assert_equal (ixA, [1; 2; 4]); %! assert_equal (ixB, [1; 2; 1; 3]); %!test %! A = [calmonths(1), calendarDuration(NaN, 0, 0), calmonths(1), ... %! calendarDuration(NaN, 0, 0)]; %! [~, ixA] = unique (A, 'last'); %! assert_equal (ixA, [3; 2; 4]); %!test %! A = [caldays(1), calendarDuration(0, 0, 0, 24, 0, 0), caldays(1)]; %! [B, ixA, ixB] = unique (A); %! assert_equal (size (B), [1, 2]); %! [d, t] = split (B, {'days', 'time'}); %! assert_equal (d, [0, 1]); %! assert_equal (seconds (t), [86400, 0]); %! assert_equal (ixA, [2; 1]); %! assert_equal (ixB, [2; 1; 2]); %!test %! A = [caldays(1), calendarDuration(0, 0, 0, 24, 0, 0), caldays(1)]; %! [B, ixA] = unique (A, 'stable'); %! [d, t] = split (B, {'days', 'time'}); %! assert_equal (d, [1, 0]); %! assert_equal (seconds (t), [0, 86400]); %! assert_equal (ixA, [1; 2]); %!test %! A = calmonths ([3; 1; 2; 1]); %! [B, ixA, ixB] = unique (A); %! assert_equal (size (B), [3, 1]); %! assert_equal (calmonths (B), [1; 2; 3]); %! assert_equal (ixA, [2; 3; 1]); %! assert_equal (ixB, [3; 1; 2; 1]); %!test %! A = calmonths ([3; 1; 2; 1]); %! [B, ixA, ixB] = unique (A, 'stable'); %! assert_equal (calmonths (B), [3; 1; 2]); %! assert_equal (ixA, [1; 2; 3]); %! assert_equal (ixB, [1; 2; 3; 2]); %!test %! A = [calendarDuration(0, 0, 0), calendarDuration(0, 0, 0)]; %! [B, ixA, ixB] = unique (A); %! assert_equal (size (B), [1, 1]); %! assert_equal (calmonths (B), 0); %! assert_equal (ixA, 1); %! assert_equal (ixB, [1; 1]); %!test %! A = [calendarDuration(0, 0, 0), calendarDuration(0, 0, 0)]; %! [~, ixA] = unique (A, 'last'); %! assert_equal (ixA, 2); ################################################################################ ## ** Array Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'cat' 'horzcat' 'vertcat' 'repmat' ## ## 'repelem' 'repelems' 'reshape' 'circshift' ## ## 'permute' 'ipermute' 'transpose' 'ctranspose' ## ## ## ################################################################################ ## Test 'cat' -- a missing value becomes an all-NaN calendarDuration ## (vs MATLAB R2024a) %!test %! cD = [caldays(1), missing]; %! assert_equal (size (cD), [1, 2]); %! assert_equal (ismissing (cD), [false, true]); %!test %! cD = [calmonths(2), missing]; %! assert_equal (cellstr (cD), {'2mo', 'NaN'}); %!assert_equal (size ([caldays(1); missing]), [2, 1]) ## Test 'subsasgn' -- assigning a missing value gives NaN, while a gap opened ## by growing past the end is zero, not missing (vs MATLAB R2024a) %!test %! cD = caldays (1); %! cD(3) = missing; %! assert_equal (ismissing (cD), [false, false, true]); %!test %! cD = cat (1, 2, caldays ([3; 4])); %! assert_equal (caldays (cD), [0; 3; 4]); %! assert_equal (days (time (cD)), [2; 0; 0]); %!test %! cD = cat (1, caldays ([3; 4]), 5); %! assert_equal (caldays (cD), [3; 4; 0]); %! assert_equal (days (time (cD)), [0; 0; 5]); %!test %! cD = cat (2, 2, caldays ([3, 4])); %! assert_equal (caldays (cD), [0, 3, 4]); %! assert_equal (days (time (cD)), [2, 0, 0]); %!test %! cD = cat (2, caldays ([3, 4]), 5); %! assert_equal (caldays (cD), [3, 4, 0]); %! assert_equal (days (time (cD)), [0, 0, 5]); %!test %! cD = cat (1, [0; 1; 2], caldays ([3; 4])); %! assert_equal (caldays (cD), [0; 0; 0; 3; 4]); %! assert_equal (days (time (cD)), [0; 1; 2; 0; 0]); %!test %! cD = cat (1, caldays ([3; 4]), [0; 2; 5]); %! assert_equal (caldays (cD), [3; 4; 0; 0; 0]); %! assert_equal (days (time (cD)), [0; 0; 0; 2; 5]); %!test %! cD = cat (2, [0, 1, 2], caldays ([3, 4])); %! assert_equal (caldays (cD), [0, 0, 0, 3, 4]); %! assert_equal (days (time (cD)), [0, 1, 2, 0, 0]); %!test %! cD = cat (2, caldays ([3, 4]), [0, 2, 5]); %! assert_equal (caldays (cD), [3, 4, 0, 0, 0]); %! assert_equal (days (time (cD)), [0, 0, 0, 2, 5]); %!test %! cD = cat (1, caldays ([1, 2]), caldays ([3, 4])); %! assert_equal (caldays (cD), [1, 2; 3, 4]); %!test %! cD = cat (2, caldays ([1, 2]), caldays ([3, 4])); %! assert_equal (caldays (cD), [1, 2, 3, 4]); %!test %! cD = cat (3, caldays ([1, 2]), caldays ([3, 4])); %! assert_equal (caldays (cD), reshape ([1, 2, 3, 4], 1, 2, 2)); %!test %! cD = cat (4, caldays ([1, 2]), caldays ([3, 4])); %! assert_equal (caldays (cD), reshape ([1, 2, 3, 4], 1, 2, 1, 2)); %!error cat (1, caldays ([3, 4]), [0; 2; 5]) %!error cat (1, caldays ([3; 4]), [0, 2; 5, 2]) %!error cat (2, caldays ([3; 4]), [0; 2; 5]) %!error cat (2, caldays ([3, 4]), [0, 2; 5, 2]) %!error ... %! cat (1, caldays ([3; 4]), {}) %!error ... %! cat (1, caldays ([3; 4]), {'o'}) %!error ... %! cat (1, caldays ([3; 4]), 'o') %!error ... %! cat (1, caldays ([3; 4]), string ('o')) %!test %! cD = [days(1), caldays(2)]; %! assert_equal (caldays (cD), [0, 2]); %! assert_equal (days (time (cD)), [1, 0]); %!test %! cD = [days(1), 1, caldays(2)]; %! assert_equal (caldays (cD), [0, 0, 2]); %! assert_equal (days (time (cD)), [1, 1, 0]); %!test %! cD = [caldays(1), days(2)]; %! assert_equal (caldays (cD), [1, 0]); %! assert_equal (days (time (cD)), [0, 2]); %!test %! cD = [caldays(1), 1, days(2)]; %! assert_equal (caldays (cD), [1, 0, 0]); %! assert_equal (days (time (cD)), [0, 1, 2]); %!test %! cD = [days(1), [], caldays(2)]; %! assert_equal (caldays (cD), [0, 2]); %! assert_equal (days (time (cD)), [1, 0]); %!test %! cD = [caldays(1), [], days(2)]; %! assert_equal (caldays (cD), [1, 0]); %! assert_equal (days (time (cD)), [0, 2]); %!test %! cD = [days(1), ones(0, 1), caldays(2)]; %! assert_equal (caldays (cD), [0, 2]); %! assert_equal (days (time (cD)), [1, 0]); %!test %! cD = [caldays(1), ones(0, 1), days(2)]; %! assert_equal (caldays (cD), [1, 0]); %! assert_equal (days (time (cD)), [0, 2]); %!test %! cD = [days(1), ones(0, 1), caldays(2), ones(1, 0)]; %! assert_equal (caldays (cD), [0, 2]); %! assert_equal (days (time (cD)), [1, 0]); %!test %! cD = [caldays(1), ones(0, 1), days(2), ones(1, 0)]; %! assert_equal (caldays (cD), [1, 0]); %! assert_equal (days (time (cD)), [0, 2]); %!test %! cD = [days(1), ones(2, 0, 1), caldays(2)]; %! assert_equal (caldays (cD), [0, 2]); %! assert_equal (days (time (cD)), [1, 0]); %!test %! cD = [caldays(1), ones(2, 0, 1), days(2)]; %! assert_equal (caldays (cD), [1, 0]); %! assert_equal (days (time (cD)), [0, 2]); %!test %! cD = [days(1); caldays(2)]; %! assert_equal (caldays (cD), [0; 2]); %! assert_equal (days (time (cD)), [1; 0]); %!test %! cD = [days(1); 1; caldays(2)]; %! assert_equal (caldays (cD), [0; 0; 2]); %! assert_equal (days (time (cD)), [1; 1; 0]); %!test %! cD = [caldays(1); days(2)]; %! assert_equal (caldays (cD), [1; 0]); %! assert_equal (days (time (cD)), [0; 2]); %!test %! cD = [caldays(1); 1; days(2)]; %! assert_equal (caldays (cD), [1; 0; 0]); %! assert_equal (days (time (cD)), [0; 1; 2]); %!test %! cD = [days(1); []; caldays(2)]; %! assert_equal (caldays (cD), [0; 2]); %! assert_equal (days (time (cD)), [1; 0]); %!test %! cD = [caldays(1); []; days(2)]; %! assert_equal (caldays (cD), [1; 0]); %! assert_equal (days (time (cD)), [0; 2]); %!test %! cD = [days(1); ones(0, 1); caldays(2)]; %! assert_equal (caldays (cD), [0; 2]); %! assert_equal (days (time (cD)), [1; 0]); %!test %! cD = [caldays(1); ones(0, 1); days(2)]; %! assert_equal (caldays (cD), [1; 0]); %! assert_equal (days (time (cD)), [0; 2]); %!test %! cD = [days(1); ones(0, 1); caldays(2); ones(1, 0)]; %! assert_equal (caldays (cD), [0; 2]); %! assert_equal (days (time (cD)), [1; 0]); %!test %! cD = [caldays(1); ones(0, 1); days(2); ones(1, 0)]; %! assert_equal (caldays (cD), [1; 0]); %! assert_equal (days (time (cD)), [0; 2]); %!test %! cD = [days(1); ones(2, 0, 1); caldays(2)]; %! assert_equal (caldays (cD), [0; 2]); %! assert_equal (days (time (cD)), [1; 0]); %!test %! cD = [caldays(1); ones(2, 0, 1); days(2)]; %! assert_equal (caldays (cD), [1; 0]); %! assert_equal (days (time (cD)), [0; 2]); %!shared x1, x2 %! x1 = [2, 3]; %! x2 = magic (3); %!assert_equal (caldays (repmat (caldays (x1), 3)), repmat (x1, 3)) %!assert_equal (caldays (repmat (caldays (x1), 2, 3)), repmat (x1, 2, 3)) %!assert_equal (caldays (repmat (caldays (x1), 1, 1, 3)), repmat (x1, 1, 1, 3)) %!assert_equal (caldays (repmat (caldays (x1), [1, 1, 3])), repmat (x1, [1, ... %! 1, 3])) %!assert_equal (caldays (repmat (caldays (x1), 1, 0, 3)), repmat (x1, 1, 0, 3)) %!assert_equal (caldays (repmat (caldays (x1), [1, 0, 3])), repmat (x1, 1, ... %! 0, 3)) %!assert_equal (caldays (repelem (caldays (x1), 3)), repelem (x1, 3)) %!assert_equal (caldays (repelem (caldays (x1), 2, 3)), repelem (x1, 2, 3)) %!assert_equal (caldays (repelem (caldays (x1), [1, 3])), repelem (x1, [1, 3])) %!assert_equal (caldays (repelem (caldays (x2), 1, [1, 1, 3])), repelem (x2, ... %! 1, [1, 1, 3])) %!assert_equal (caldays (repelem (caldays (x2), [2, 3, 4], [1, 0, ... %! 3])), repelem (x2, [2, 3, 4], [1, 0, 3])) %!assert_equal (caldays (repelem (caldays (x2), 2, [1, 0, 3])), repelem (x2, ... %! 2, [1, 0, 3])) %!assert_equal (caldays (repelems (caldays (x2), [1, 3, 4; 4, 2, ... %! 5])), repelems (x2, [1, 3, 4; 4, 2, 5])) %!assert_equal (isempty (repelems (caldays (x2), [1, 3, 4; 0, 0, 0])), true) %!assert_equal (size (reshape (caldays (x2), 1, 9)), [1, 9]) %!assert_equal (caldays (reshape (caldays (x2), 1, 9)), reshape (x2, 1, 9)) %!assert_equal (caldays (reshape (caldays (x2), 1, 1, 9)), reshape (x2, 1, ... %! 1, 9)) %!assert_equal (caldays (reshape (caldays (x2), 1, 1, 3, 1, ... %! 3)), reshape (x2, 1, 1, 3, 1, 3)) %!assert_equal (caldays (circshift (caldays (x2), 1)), circshift (x2, 1)) %!assert_equal (caldays (circshift (caldays (x2), -2)), circshift (x2, -2)) %!assert_equal (caldays (circshift (caldays (x2), [1, -2])), circshift (x2, ... %! [1, -2])) %!assert_equal (caldays (circshift (caldays (x2), -1, 2)), circshift (x2, ... %! -1, 2)) %!test %! cD = permute (caldays (x2), [2, 1]); %! assert_equal (caldays (cD), permute (x2, [2, 1])); %!test %! cD = ipermute (permute (caldays (x2), [2, 1]), [2, 1]); %! assert_equal (caldays (cD), x2); %!assert_equal (caldays (caldays (x2)'), x2'); %!assert_equal (caldays (caldays (x2).'), x2'); %!assert_equal (caldays (ctranspose (caldays (x2))), x2'); %!assert_equal (caldays (ctranspose (caldays (x2))), ... %! caldays (transpose (caldays (x2)))); ## Concatenation takes the union of the operands' formats, so units that only ## one side displays survive. A duration or a bare numeric may be concatenated ## and lands in the time component, the numeric counting days. %!test %! A = [calmonths(1), calendarDuration(0, 0, 2), calendarDuration(0, 0, 0, ... %! 3, 0, 0)]; %! B = [A, calmonths(9)]; %! assert_equal (size (B), [1, 4]); %! [mo, d, t] = split (B, {'months', 'days', 'time'}); %! assert_equal (mo, [1, 0, 0, 9]); %! assert_equal (d, [0, 2, 0, 0]); %! assert_equal (seconds (t), [0, 0, 10800, 0]); %!test %! B = [calmonths(1); calmonths(2)]; %! assert_equal (size (B), [2, 1]); %! assert_equal (calmonths (B), [1; 2]); %!assert_equal (size (cat (1, calmonths (1), calmonths (2))), [2, 1]) %!assert_equal (size (cat (2, calmonths (1), calmonths (2))), [1, 2]) %!test %! B = cat (3, calmonths (1), calmonths (2)); %! assert_equal (size (B), [1, 1, 2]); %! assert_equal (calmonths (B)(:).', [1, 2]); %!test %! B = [calweeks(1), calquarters(1)]; %! assert_equal (B.Format, 'yqmwdt'); %! assert_equal (calmonths (B), [0, 3]); %! assert_equal (split (B, 'days'), [7, 0]); %!test %! B = [calquarters(1), calweeks(1)]; %! assert_equal (B.Format, 'yqmwdt'); %! assert_equal (calmonths (B), [3, 0]); %! assert_equal (split (B, 'days'), [0, 7]); %!assert_equal ([calmonths(1), calmonths(2)].Format, 'ymdt') %!test %! B = [calmonths(1), hours(5)]; %! assert_equal (calmonths (B), [1, 0]); %! assert_equal (seconds (split (B, 'time')), [0, 18000]); %!test %! B = [calmonths(1), 5]; %! assert_equal (calmonths (B), [1, 0]); %! assert_equal (seconds (split (B, 'time')), [0, 432000]); %!test %! B = [calmonths(1), calendarDuration([], [], [])]; %! assert_equal (size (B), [1, 1]); %! assert_equal (calmonths (B), 1); %!assert_equal (size ([calendarDuration([],[],[]), calendarDuration([],[], ... %! [])]), [0, 0]) ## repmat and repelem tile and expand while keeping the format. %!test %! B = repmat (calmonths (1), 2); %! assert_equal (size (B), [2, 2]); %! assert_equal (calmonths (B), [1, 1; 1, 1]); %!test %! B = repmat (calmonths ([1, 2]), 2, 3); %! assert_equal (size (B), [2, 6]); %! assert_equal (calmonths (B), [1, 2, 1, 2, 1, 2; 1, 2, 1, 2, 1, 2]); %!assert_equal (calmonths (repmat (calmonths ([1, 2]), [2, 3])), ... %! [1, 2, 1, 2, 1, 2; 1, 2, 1, 2, 1, 2]) %!assert_equal (size (repmat (calmonths (1), [2, 2, 2])), [2, 2, 2]) %!assert_equal (size (repmat (calmonths (1), 0)), [0, 0]) %!assert_equal (calmonths (repelem (calmonths ([1, 2]), 2)), [1, 1, 2, 2]) %!assert_equal (calmonths (repelem (calmonths ([1, 2, 3]), [1, 2, 3])), ... %! [1, 2, 2, 3, 3, 3]) %!test %! B = repelem (calmonths ([1, 2]), 2, 2); %! assert_equal (size (B), [2, 4]); %! assert_equal (calmonths (B), [1, 1, 2, 2; 1, 1, 2, 2]); %!assert_equal (size (repelem (calmonths ([1, 2]), 0)), [1, 0]) %!assert_equal (repmat (calweeks (1), 2).Format, 'ymwdt') ## reshape, circshift, permute and the transposes reorder elements without ## disturbing the format. %!test %! A = [calmonths(1), calmonths(2), calmonths(3); ... %! calmonths(4), calmonths(5), calmonths(6)]; %! B = reshape (A, 3, 2); %! assert_equal (size (B), [3, 2]); %! assert_equal (calmonths (B), [1, 5; 4, 3; 2, 6]); %!test %! A = [calmonths(1), calmonths(2), calmonths(3); ... %! calmonths(4), calmonths(5), calmonths(6)]; %! assert_equal (calmonths (reshape (A, [2, 3])), [1, 2, 3; 4, 5, 6]); %! assert_equal (calmonths (reshape (A, 3, [])), [1, 5; 4, 3; 2, 6]); %!assert_equal (size (reshape (repmat (calmonths (1), 2, 4), 2, 2, 2)), [2, ... %! 2, 2]) %!test %! A = [calmonths(1), calendarDuration(0, 0, 2), calendarDuration(0, 0, 0, ... %! 3, 0, 0)]; %! B = circshift (A, 1); %! [mo, d, t] = split (B, {'months', 'days', 'time'}); %! assert_equal (mo, [0, 1, 0]); %! assert_equal (d, [0, 0, 2]); %! assert_equal (seconds (t), [10800, 0, 0]); %!test %! A = [calmonths(1), calendarDuration(0, 0, 2), calendarDuration(0, 0, 0, ... %! 3, 0, 0)]; %! B = circshift (A, -1); %! [mo, d, t] = split (B, {'months', 'days', 'time'}); %! assert_equal (mo, [0, 0, 1]); %! assert_equal (d, [2, 0, 0]); %! assert_equal (seconds (t), [0, 10800, 0]); %!test %! A = [calmonths(1), calmonths(2), calmonths(3); ... %! calmonths(4), calmonths(5), calmonths(6)]; %! assert_equal (calmonths (circshift (A, 1, 2)), [3, 1, 2; 6, 4, 5]); %!test %! A = [calmonths(1), calendarDuration(0, 0, 2), calendarDuration(0, 0, 0, ... %! 3, 0, 0)]; %! assert_equal (calmonths (circshift (A, 0)), [1, 0, 0]); %! assert_equal (split (circshift (A, 0), 'days'), [0, 2, 0]); %!test %! A = [calmonths(1), calmonths(2), calmonths(3); ... %! calmonths(4), calmonths(5), calmonths(6)]; %! B = permute (A, [2, 1]); %! assert_equal (size (B), [3, 2]); %! assert_equal (calmonths (B), [1, 4; 2, 5; 3, 6]); %! assert_equal (calmonths (ipermute (B, [2, 1])), [1, 2, 3; 4, 5, 6]); %!assert_equal (size (permute (reshape (repmat (calmonths (1), 2, 4), 2, 2, ... %! 2), ... %! [3, 1, 2])), [2, 2, 2]) %!test %! A = [calmonths(1), calmonths(2), calmonths(3); ... %! calmonths(4), calmonths(5), calmonths(6)]; %! assert_equal (calmonths (A.'), [1, 4; 2, 5; 3, 6]); %! assert_equal (calmonths (A'), [1, 4; 2, 5; 3, 6]); %! assert_equal (A.'.Format, 'ymdt'); %!assert_equal (calmonths (circshift (calweeks ([1, 2]), 1)), [0, 0]) %!assert_equal (circshift (calweeks ([1, 2]), 1).Format, 'ymwdt') %!assert_equal (calweeks (1).Format, 'ymwdt') %!assert_equal (reshape (calweeks ([1, 2, 3, 4]), 2, 2).Format, 'ymwdt') %!assert_equal (repelem (calweeks ([1, 2]), 2).Format, 'ymwdt') %!assert_equal (calquarters ([1, 2]).'.Format, 'qmdt') ## repelems has no MATLAB counterpart and follows Octave's own function: the ## first row of the index matrix names elements, the second how many of each. %!assert_equal (calmonths (repelems (calmonths ([1, 2]), [1, 2; 2, 1])), [1, ... %! 1, 2]) %!assert_equal (calmonths (repelems (calmonths ([1, 2, 3]), [1, 3; 1, ... %! 2])), [1, 3, 3]) ################################################################################ ## ** Reference and Assignment Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'end' 'subsref' 'subsasgn' ## ## ## ################################################################################ %!test %! cD = caldays (x2); %! assert_equal (caldays (cD(end)), 2); %! assert_equal (caldays (cD(1:end)), x2(:)'); %! assert_equal (caldays (cD(1:end)'), x2(:)); %!test %! cD = caldays ([x2, x2]); %! assert_equal (caldays (cD(1, 2:end-1)), [1, 6, 8, 1]); %! assert_equal (caldays (cD(1:end-1, 2:end-1)), [1, 6, 8, 1; 5, 7, 3, 5]); %! assert_equal (size (cD(1:end, 2:end-1)), [3, 4]); %!test %! cD = caldays ([x2, x2]); %! assert_equal (class (cD(1)), 'calendarDuration'); %! assert_equal (length (cD(1)), 1); %! assert_equal (class (cD(1,:)), 'calendarDuration'); %! assert_equal (length (cD(1,:)), 6); %! assert_equal (length (cD(:,2)), 3); %!test %! cD = caldays (x1); %! assert_equal (cD.proxyArray, [0, 2, 0, 0, 3, 0]); %! assert_equal (cD.Format, 'ymdt'); %!test %! cD = caldays (x1'); %! assert_equal (cD.proxyArray, [0, 2, 0; 0, 3, 0]); %! assert_equal (cD.Format, 'ymdt'); %!test %! cD = calendarDuration ([1; 2], 4, [2; 4]); %! assert_equal (cD.proxyArray, [16, 2, 0; 28, 4, 0]); %! assert_equal (cD.Format, 'ymdt'); %!test %! cD = calendarDuration ([1; 2], 4, [2; 4], hours ([8.5; 14.2]), 'Format', ... %! 'qmdt'); %! assert_equal (cD.proxyArray, [16, 2, 8.5; 28, 4, 14.2]); %! assert_equal (cD.Format, 'qmdt'); %!error ... %! caldays (3){1} %!error ... %! subsref (caldays (3), struct ('type', '.', 'subs', 123)); %!error ... %! caldays (3).fdasd %!test %! cD = calendarDuration ([1; 2], 4, [2; 4]); %! cD(1) = calyears (2) + calmonths (1) + caldays (14); %! assert_equal (calyears (cD), [2; 2]); %! assert_equal (calmonths (cD), [25; 28]); %! [~, D_] = split (cD, {'months', 'days'}); %! assert_equal (D_, [14; 4]); %! assert_equal (days (time (cD)), [0; 0]) %!test %! cD = calendarDuration ([1; 2], 4, [2; 4]); %! cD(end) = 180; %! assert_equal (calyears (cD), [1; 0]); %! assert_equal (calmonths (cD), [16; 0]); %! [~, D_] = split (cD, {'months', 'days'}); %! assert_equal (D_, [2; 0]); %! assert_equal (days (time (cD)), [0; 180]) %!test %! cD = calendarDuration ([1; 2], 4, [2; 4]); %! cD(end) = hours (12); %! assert_equal (calyears (cD), [1; 0]); %! assert_equal (calmonths (cD), [16; 0]); %! [~, D_] = split (cD, {'months', 'days'}); %! assert_equal (D_, [2; 0]); %! assert_equal (days (time (cD)), [0; 0.5]) %!test %! cD = calendarDuration ([1; 2], 4, [2; 4]); %! cD(1) = []; %! assert_equal (calyears (cD), 2); %! assert_equal (calmonths (cD), 28); %! [~, D_] = split (cD, {'months', 'days'}); %! assert_equal (D_, 4); %!test %! cD = calendarDuration ([1, 3; 2, 2], 4, 0); %! cD(1,:) = []; %! assert_equal (calyears (cD), [2, 2]); %! assert_equal (calmonths (cD), [28, 28]); %! [~, D_] = split (cD, {'months', 'days'}); %! assert_equal (D_, [0, 0]); %! cD(end+1, :) = caldays ([2; 3]); %! assert_equal (caldays (cD(2,:)), [2, 3]); %!test %! cD = calendarDuration ([1, 3; 2, 2], 4, 0); %! cD(:, 2) = []; %! assert_equal (calyears (cD), [1; 2]); %! assert_equal (calmonths (cD), [16; 28]); %! [~, D_] = split (cD, {'months', 'days'}); %! assert_equal (D_, [0; 0]); %! cD(:, end+1) = calmonths ([2; 3]); %! assert_equal (calmonths (cD(:,2)), [2; 3]); %! cD(:, end+1) = calmonths ([2, 3]); %! assert_equal (calmonths (cD(2,:)), [28, 3, 3]); %! assert_equal (calmonths (cD(:,[2,3])), [2, 2; 3, 3]); %!test %! cD = calendarDuration ([1; 2], 4, [2; 4]); %! cD(2) = []; %! assert_equal (calyears (cD), 1); %! assert_equal (calmonths (cD), 16); %! [~, D_] = split (cD, {'months', 'days'}); %! assert_equal (D_, 2); %!test %! cD = calendarDuration ([1; 2], 4, [2; 4]); %! assert_equal (cD.Format, 'ymdt'); %! cD.Format = 'qmdt'; %! assert_equal (cD.Format, 'qmdt'); %! cD.Format = 'qmwdt'; %! assert_equal (cD.Format, 'qmwdt'); %!error ... %! cD = caldays([1:5]); cD(2) = {1} %!error ... %! cD = caldays([1:5]); cD{2} = 1 %!error ... %! subsasgn (caldays (3), struct ('type', '.', 'subs', 123)); %!error ... %! cD = caldays([1:5]); cD.Format = ['asd';'ert'] %!error ... %! cD = caldays([1:5]); cD.Format = 'mmdt' %!error ... %! cD = caldays([1:5]); cD.Format = 'ymd' %!error ... %! cD = caldays([1:5]); cD.Format = 'mydt' %!error ... %! subsasgn (calendarDuration (1, 0, 0), substruct ('()', {1}, '.', 'x'), 1); %!error ... %! cD = caldays([1:5]); cD.fdasd = 1 ## Test 'subsref' names a property with a string scalar %!test %! cD = calendarDuration (1, 2, 3); %! s = struct ('type', '.', 'subs', string ('Format')); %! assert_equal (subsref (cD, s), cD.Format); ## Test 'subsasgn' names a property with a string scalar %!test %! cD = calendarDuration (1, 2, 3); %! s = struct ('type', '.', 'subs', string ('Format')); %! assert_equal (subsasgn (cD, s, 'ymdt').Format, 'ymdt'); ## Test 'subsref' refuses a property name that is not a single row %!error ... %! subsref (calendarDuration (1, 2, 3), struct ('type', '.', 'subs', ... %! ['ab'; 'cd'])); ## Test 'subsasgn' refuses a property name that is not a single row %!error ... %! subsasgn (calendarDuration (1, 2, 3), struct ('type', '.', 'subs', ''), 5); ## Test 'calendarDuration.empty' -- empty calendarDuration arrays %!assert_equal (size (calendarDuration.empty), [0, 0]) %!assert_equal (class (calendarDuration.empty), 'calendarDuration') %!assert_equal (isempty (calendarDuration.empty), true) %!assert_equal (calendarDuration.empty.Format, 'ymdt') %!assert_equal (size (calendarDuration.empty (2, 0)), [2, 0]) %!assert_equal (size (calendarDuration.empty (0, 3)), [0, 3]) %!error ... %! calendarDuration.empty (3) ## Test 'calendarDuration.empty' -- size arguments follow 'zeros' rules: a ## negative dimension counts as zero, a size vector holding none names no size %!assert_equal (size (calendarDuration.empty (0)), [0, 0]) %!assert_equal (size (calendarDuration.empty (-1)), [0, 0]) %!assert_equal (size (calendarDuration.empty (-2, -3)), [0, 0]) %!assert_equal (size (calendarDuration.empty ([])), [0, 0]) %!assert_equal (size (calendarDuration.empty (2, 0, 3)), [2, 0, 3]) %!assert_equal (size (calendarDuration.empty ([0 2 3])), [0, 2, 3]) %!error ... %! calendarDuration.empty (1.5) ## A wholly zero element takes its unit from the rest of the array: the ## smallest unit any element uses, days when none does. Values below are ## MATLAB R2024a. %!assert_equal (cellstr ([calendarDuration(0,0,0), calyears(1), ... %! calmonths(2)]), ... %! {'0mo', '1y', '2mo'}) %!assert_equal (cellstr ([calendarDuration(0,0,0), calyears(1), ... %! caldays(3)]), ... %! {'0d', '1y', '3d'}) %!assert_equal (cellstr ([calendarDuration(0,0,0), calmonths(2), ... %! caldays(3)]), ... %! {'0d', '2mo', '3d'}) %!assert_equal (cellstr ([calendarDuration(0,0,0), calyears(1), ... %! calendarDuration(0,0,0) + hours(5)]), ... %! {'0s', '1y', '5h 0m 0s'}) %!assert_equal (cellstr ([calendarDuration(0,0,0), calmonths(2), ... %! calendarDuration(0,0,0) + hours(5)]), ... %! {'0s', '2mo', '5h 0m 0s'}) %!assert_equal (cellstr ([calendarDuration(0,0,0), caldays(3), ... %! calendarDuration(0,0,0) + hours(5)]), ... %! {'0s', '3d', '5h 0m 0s'}) ## With nothing to take a unit from, a zero renders in days -- scalar or not. %!assert_equal (cellstr ([calendarDuration(0,0,0), calendarDuration(0,0, ... %! 0)]), ... %! {'0d', '0d'}) %!assert_equal (char (calendarDuration (0, 0, 0)), '0d') %!assert_equal (char (calmonths (0)), '0d') %!assert_equal (char (calyears (0)), '0d') ## A zero alongside an element that uses every unit reads in the smallest one. %!assert_equal (cellstr ([calendarDuration(0,0,0), ... %! calyears(1) + calmonths(2) + caldays(3)]){1}, '0d') %!assert_equal (cellstr ([calendarDuration(0,0,0), ... %! calmonths(2) + hours(5)]){1}, '0s') ## Concatenation displays in the union of the inputs' formats, so weeks and ## quarters survive rather than widening back to days and months. Values ## below are MATLAB R2024a. %!assert_equal (cellstr ([calendarDuration(0,0,0), calweeks(2)]), {'0w', '2w'}) %!assert_equal (cellstr ([calendarDuration(0,0,0), calquarters(3)]), ... %! {'0q', '3q'}) %!assert_equal (cellstr ([calendarDuration(0,0,0), calyears(1), ... %! calquarters(3)]), {'0q', '1y', '3q'}) %!assert_equal (cellstr ([calendarDuration(0,0,0), calweeks(2), ... %! calquarters(3)]), {'0w', '2w', '3q'}) %!assert_equal (cellstr ([calendarDuration(0,0,0), calmonths(2), ... %! calquarters(3)]), {'0mo', '2mo', '3q'}) %!assert_equal (cellstr ([calendarDuration(0,0,0), caldays(3), ... %! calweeks(2)]), {'0d', '3d', '2w'}) %!assert_equal (cellstr ([calendarDuration(0,0,0), calyears(1), ... %! calweeks(2)]), {'0w', '1y', '2w'}) %!assert_equal (cellstr ([calendarDuration(0,0,0), caldays(3), ... %! calquarters(3)]), {'0d', '3d', '3q'}) %!assert_equal (cellstr ([calendarDuration(0,0,0), ... %! calendarDuration(0,0,0) + hours(5), ... %! calweeks(2)]), {'0s', '5h 0m 0s', '2w'}) ## A format keeps its own units when nothing else is concatenated onto it. %!assert_equal (char (calweeks (2)), '2w') %!assert_equal (char (calquarters (3)), '3q') %!assert_equal ([calweeks(1), calquarters(1)](1).Format, 'yqmwdt') %!assert_equal ([calweeks(1); calquarters(1)].Format, 'yqmwdt') %!assert_equal (calquarters (1).Format, 'qmdt') %!assert_equal (calmonths (1).Format, 'ymdt') %!assert_equal (calyears (1).Format, 'ymdt') %!assert_equal (caldays (1).Format, 'ymdt') ## Indexing reads a subset without disturbing the Format. A(:) returns a ## column whatever the input's shape, an empty index an empty array, and a ## trailing singleton index is allowed. %!shared A, A2 %! A = [calmonths(1), calendarDuration(0, 0, 2), calendarDuration(0, 0, 0, ... %! 3, 0, 0)]; %! A2 = [calmonths(1), calmonths(2), calmonths(3); ... %! calmonths(4), calmonths(5), calmonths(6)]; %!test %! [mo, d, t] = split (A(2), {'months', 'days', 'time'}); %! assert_equal ([mo, d, seconds(t)], [0, 2, 0]); %!assert_equal (seconds (split (A(end), 'time')), 10800) %!assert_equal (split (A(end-1), 'days'), 2) %!test %! B = A(:); %! assert_equal (size (B), [3, 1]); %! [mo, d, t] = split (B, {'months', 'days', 'time'}); %! assert_equal (mo, [1; 0; 0]); %! assert_equal (d, [0; 2; 0]); %! assert_equal (seconds (t), [0; 0; 10800]); %!test %! B = A([1, 3]); %! assert_equal (size (B), [1, 2]); %! assert_equal (calmonths (B), [1, 0]); %! assert_equal (seconds (split (B, 'time')), [0, 10800]); %!test %! B = A(logical ([1, 0, 1])); %! assert_equal (calmonths (B), [1, 0]); %! assert_equal (seconds (split (B, 'time')), [0, 10800]); %!assert_equal (calmonths (A2(2,3)), 6) %!assert_equal (calmonths (A2(end,end)), 6) %!test %! B = A2(:,2); %! assert_equal (size (B), [2, 1]); %! assert_equal (calmonths (B), [2; 5]); %!test %! B = A2(2,:); %! assert_equal (size (B), [1, 3]); %! assert_equal (calmonths (B), [4, 5, 6]); %!assert_equal (size (A([])), [0, 0]) %!assert_equal (calmonths (A(1,1,1)), 1) %!assert_equal (A.Format, 'ymdt') %!assert_equal (A(2).Format, 'ymdt') ## Deleting elements. A(:) = [] empties an array to 0x0 while deleting the ## only element of a scalar leaves 1x0, and a null assignment takes one ## non-colon index at most. %!test %! B = A; B(2) = []; %! assert_equal (size (B), [1, 2]); %! assert_equal (calmonths (B), [1, 0]); %! assert_equal (seconds (split (B, 'time')), [0, 10800]); %!test %! B = A; B([1, 3]) = []; %! assert_equal (size (B), [1, 1]); %! assert_equal (split (B, 'days'), 2); %!test %! B = A; B(end) = []; %! assert_equal (calmonths (B), [1, 0]); %! assert_equal (split (B, 'days'), [0, 2]); %!test %! B = A; B(logical ([1, 0, 1])) = []; %! assert_equal (size (B), [1, 1]); %! assert_equal (split (B, 'days'), 2); %!test %! B = A; B(:) = []; %! assert_equal (size (B), [0, 0]); %!test %! B = A2; B(:,1) = []; %! assert_equal (size (B), [2, 2]); %! assert_equal (calmonths (B), [2, 3; 5, 6]); %!test %! B = A2; B(1,:) = []; %! assert_equal (size (B), [1, 3]); %! assert_equal (calmonths (B), [4, 5, 6]); %!test %! B = calmonths (1); B(1) = []; %! assert_equal (size (B), [1, 0]); ## Assignment. Growing past the end fills the gap with zero spans, not with ## missing values. A duration or a bare numeric may be assigned, the numeric ## counting days, and both land in the time component. %!test %! B = A; B(2) = calmonths (9); %! [mo, d, t] = split (B, {'months', 'days', 'time'}); %! assert_equal (mo, [1, 9, 0]); %! assert_equal (d, [0, 0, 0]); %! assert_equal (seconds (t), [0, 0, 10800]); %!test %! B = A; B(5) = calmonths (9); %! assert_equal (size (B), [1, 5]); %! [mo, d, t] = split (B, {'months', 'days', 'time'}); %! assert_equal (mo, [1, 0, 0, 0, 9]); %! assert_equal (d, [0, 2, 0, 0, 0]); %! assert_equal (seconds (t), [0, 0, 10800, 0, 0]); %!test %! B = A2; B(2,4) = calmonths (9); %! assert_equal (size (B), [2, 4]); %! assert_equal (calmonths (B), [1, 2, 3, 0; 4, 5, 6, 9]); %!test %! B = A; B(:) = calmonths (9); %! assert_equal (calmonths (B), [9, 9, 9]); %!test %! B = A; B(end+1) = calmonths (9); %! assert_equal (size (B), [1, 4]); %! assert_equal (calmonths (B), [1, 0, 0, 9]); %! assert_equal (seconds (split (B, 'time')), [0, 0, 10800, 0]); %!test %! B = A; B(2) = hours (5); %! [mo, d, t] = split (B, {'months', 'days', 'time'}); %! assert_equal (mo, [1, 0, 0]); %! assert_equal (d, [0, 0, 0]); %! assert_equal (seconds (t), [0, 18000, 10800]); %!test %! B = A; B(2) = 5; %! assert_equal (seconds (split (B, 'time')), [0, 432000, 10800]); %!test %! B = A; B(logical ([1, 0, 1])) = calmonths (9); %! [mo, d, t] = split (B, {'months', 'days', 'time'}); %! assert_equal (mo, [9, 0, 9]); %! assert_equal (d, [0, 2, 0]); %! assert_equal (seconds (t), [0, 0, 0]); %!test %! B = A; B(1:2) = [calmonths(8), calmonths(9)]; %! assert_equal (calmonths (B), [8, 9, 0]); %! assert_equal (seconds (split (B, 'time')), [0, 0, 10800]); %!test %! B = calendarDuration ([], [], []); B(2) = calmonths (9); %! assert_equal (size (B), [1, 2]); %! assert_equal (calmonths (B), [0, 9]); %!test %! B = A; B(2) = calendarDuration (NaN, 0, 0); %! [mo, d, t] = split (B, {'months', 'days', 'time'}); %! assert_equal (mo, [1, NaN, 0]); %! assert_equal (d, [0, NaN, 0]); %! assert_equal (seconds (t), [0, NaN, 10800]); %!error ... %! A(2) = 'a' ## A Format must name 'm', 'd' and 't' and use only the 'yqmwdt' characters. ## R2024a accepts a format missing them, warns, and falls back to 'ymdt', but ## it then ignores the format it accepted, so the documented contract is kept ## here instead. %!error ... %! A.Format = 'ymd' %!error ... %! A.Format = 'zz' pr0m1th3as-datatypes-9450b29/inst/tests/categorical.m-tst000066400000000000000000006507721527370255000233010ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ################################################################################ ## ** Create and convert 'categorical' type ** ## ################################################################################ ## Available Methods ## ## ## ## 'categorical' 'dispstrings' 'cellstr' 'char' ## ## 'double' 'single' 'int64' 'int32' ## ## 'int16' 'int8' 'uint64' 'uint32' ## ## 'uint16' 'uint8' ## ## ## ################################################################################ %!assert_equal (isscalar (categorical ()), false) %!assert_equal (isempty (categorical ()), true) %!assert_equal (class (categorical), 'categorical') %!assert_equal (dispstrings (categorical ()), {}) %!assert_equal (size (categorical), [0, 0]) ## size with no dimension named folds the trailing dimensions into the last ## output when fewer outputs are asked for than the array has dimensions, and ## pads with ones when more are asked for, as core Octave and MATLAB both do. %!test %! A = categorical (repmat ({'a'}, 2, 2, 2)); %! [r, c] = size (A); %! assert_equal ([r, c], [2, 4]); %!test %! A = categorical (repmat ({'a'}, 2, 2)); %! [a, b, c, d] = size (A); %! assert_equal ([a, b, c, d], [2, 2, 1, 1]); %!assert_equal (size (categorical (NaN (2, 3, 4))), [2, 3, 4]) %!assert_equal (size (categorical (cell (0, 1))), [0, 1]) %!assert_equal (size (categorical (cell (1, 0))), [1, 0]) %!assert_equal (size (categorical (cell (0, 3))), [0, 3]) %!assert_equal (size (categorical (cell (2, 0))), [2, 0]) %!assert_equal (size (categorical (cell (0, 1), {'a', 'b'})), [0, 1]) %!assert_equal (categories (categorical), {}) %!assert_equal (categories (categorical (NaN)), {}) %!assert_equal (categories (categorical (NaN (3))), {}) %!assert_equal (double (categorical), []) %!assert_equal (double (categorical (NaN)), NaN) %!assert_equal (double (categorical (NaN (3))), NaN (3)) %!test %! stations = categorical (string ({'S1', 'S2', 'S1', 'S3', 'S2'})); %! assert_equal (stations, categorical ({'S1', 'S2', 'S1', 'S3', 'S2'})); %! assert_equal (dispstrings (stations), {'S1', 'S2', 'S1', 'S3', 'S2'}); %! assert_equal (categories (stations), {'S1'; 'S2'; 'S3'}); %!test %! stations = categorical ({'S1', 'S2', 'S1'; 'S2', 'S3', 'S2'}); %! assert_equal (categories (stations), {'S1'; 'S2'; 'S3'}); %! assert_equal (double (stations), [1, 2, 1; 2, 3, 2]); %! assert_equal (size (stations), [2, 3]); %!test %! stations = categorical ({'S1', 'S2', ''; 'S2', 'S3', 'S2'}); %! assert_equal (categories (stations), {'S1'; 'S2'; 'S3'}); %! assert_equal (dispstrings (stations), {'S1', 'S2', ''; 'S2', ... %! 'S3', 'S2'}); %! assert_equal (ismissing (stations), isundefined (stations)); %! assert_equal (double (stations), [1, 2, NaN; 2, 3, 2]); %!test %! stations = categorical ({'S1', 'S2', 'S1'; 'S2', 'S3', 'S2'}, 'Ordinal', ... %! true); %! assert_equal (categories (stations), {'S1'; 'S2'; 'S3'}); %! assert_equal (double (stations), [1, 2, 1; 2, 3, 2]); %!test %! stations = categorical ({'S3', 'S2', 'S1'; 'S2', 'S3', 'S2'}, 'Ordinal', ... %! true); %! assert_equal (categories (stations), {'S1'; 'S2'; 'S3'}); %! assert_equal (double (stations), [3, 2, 1; 2, 3, 2]); %!test %! C = categorical ([3, 2, 3; 2, 1, 2]); %! assert_equal (dispstrings (C), {'3', '2', '3'; '2', '1', '2'}); %! assert_equal (categories (C), {'1'; '2'; '3'}); %! assert_equal (double (C), [3, 2, 3; 2, 1, 2]); %!test %! C = categorical ([1, 2, 3; 2, 1, 2]); %! assert_equal (dispstrings (C), {'1', '2', '3'; '2', '1', '2'}); %! assert_equal (categories (C), {'1'; '2'; '3'}); %! assert_equal (double (C), [1, 2, 3; 2, 1, 2]); %!test %! C = categorical ([3, 2, 3; 2, 1, 2], 'Ordinal', true); %! assert_equal (dispstrings (C), {'3', '2', '3'; '2', '1', '2'}); %! assert_equal (categories (C), {'1'; '2'; '3'}); %! assert_equal (double (C), [3, 2, 3; 2, 1, 2]); %!test %! C = categorical ([1, 2, 3; 2, 1, 2], 'Ordinal', true); %! assert_equal (dispstrings (C), {'1', '2', '3'; '2', '1', '2'}); %! assert_equal (categories (C), {'1'; '2'; '3'}); %! assert_equal (double (C), [1, 2, 3; 2, 1, 2]); %!test %! C = categorical ([3, NaN, 3; 2, 1, 2]); %! assert_equal (dispstrings (C), {'3', '', '3'; '2', '1', '2'}); %! assert_equal (categories (C), {'1'; '2'; '3'}); %! assert_equal (ismissing (C), isundefined (C)); %! assert_equal (double (C), [3, NaN, 3; 2, 1, 2]); %!test %! C = categorical ([1, NaN, 3; 5, 1, 2]); %! assert_equal (dispstrings (C), {'1', '', '3'; '5', '1', '2'}); %! assert_equal (categories (C), {'1'; '2'; '3'; '5'}); %! assert_equal (ismissing (C), isundefined (C)); %! assert_equal (double (C), [1, NaN, 3; 4, 1, 2]); %!test %! C = categorical ([3, NaN, 3; 2, 1, 2], 'Ordinal', true); %! assert_equal (dispstrings (C), {'3', '', '3'; '2', '1', '2'}); %! assert_equal (categories (C), {'1'; '2'; '3'}); %! assert_equal (ismissing (C), isundefined (C)); %! assert_equal (double (C), [3, NaN, 3; 2, 1, 2]); %!test %! C = categorical ([1, NaN, 3; 2, 1, 2], 'Ordinal', true); %! assert_equal (dispstrings (C), {'1', '', '3'; '2', '1', '2'}); %! assert_equal (categories (C), {'1'; '2'; '3'}); %! assert_equal (ismissing (C), isundefined (C)); %! assert_equal (double (C), [1, NaN, 3; 2, 1, 2]); %!test %! C = categorical ({'medium', 'small', 'large'}, 'Ordinal', true); %! assert_equal (dispstrings (C), {'medium', 'small', 'large'}); %! assert_equal (categories (C), {'large'; 'medium'; 'small'}); %! assert_equal (double (C), [2, 3, 1]); %!test %! S = {'medium', 'small', 'large'; 'large', 'medium', 'small'}; %! C = categorical (S, 'Ordinal', true); %! assert_equal (dispstrings (C), S); %! assert_equal (categories (C), {'large'; 'medium'; 'small'}); %! assert_equal (double (C), [2, 3, 1; 1, 2, 3]); %!test %! allsizes = {'medium', 'large', 'small', 'small', 'medium', 'large', ... %! 'medium', 'small'}; %! valueset = {'small', 'medium', 'large'}; %! C = categorical (allsizes, valueset, 'Ordinal', true); %! assert_equal (categories (C), {'small'; 'medium'; 'large'}); %! assert_equal (double (C), [2, 3, 1, 1, 2, 3, 2, 1]); %!test %! allsizes = {'medium', 'large', 'small', 'small', 'medium', 'large', ... %! 'medium', 'small'}; %! valueset = string ({'small', 'medium', 'large'}); %! C = categorical (allsizes, valueset, 'Ordinal', true); %! assert_equal (categories (C), {'small'; 'medium'; 'large'}); %! assert_equal (double (C), [2, 3, 1, 1, 2, 3, 2, 1]); %!test %! allsizes = string ({'medium', 'large', 'small', 'small', ... %! 'medium', 'large', 'medium', 'small'}); %! valueset = string ({'small', 'medium', 'large'}); %! C = categorical (allsizes, valueset, 'Ordinal', true); %! assert_equal (categories (C), {'small'; 'medium'; 'large'}); %! assert_equal (double (C), [2, 3, 1, 1, 2, 3, 2, 1]); %!test %! allsizes = string ({'medium', 'large', 'small', 'small', ... %! 'medium', 'large', 'medium', 'small'}); %! valueset = {'small', 'medium', 'large'}; %! C = categorical (allsizes, valueset, 'Ordinal', true); %! assert_equal (categories (C), {'small'; 'medium'; 'large'}); %! assert_equal (double (C), [2, 3, 1, 1, 2, 3, 2, 1]); %!test %! C = categorical (days ([3, NaN, 3; 2, 1, 2])); %! assert_equal (dispstrings (C), {'3 days', '', ... %! '3 days'; '2 days', '1 day', '2 days'}); %! assert_equal (categories (C), {'1 day'; '2 days'; '3 days'}); %! assert_equal (double (C), [3, NaN, 3; 2, 1, 2]); %!test %! C = categorical (days ([3, NaN, 3; 2, 1, 2]), 'Ordinal', true); %! assert_equal (dispstrings (C), {'3 days', '', ... %! '3 days'; '2 days', '1 day', '2 days'}); %! assert_equal (categories (C), {'1 day'; '2 days'; '3 days'}); %! assert_equal (double (C), [3, NaN, 3; 2, 1, 2]); %!test %! C = categorical (days ([3, NaN, 3; 2, 1, 2]), days (1:3), 'Ordinal', true); %! assert_equal (dispstrings (C), {'3 days', '', ... %! '3 days'; '2 days', '1 day', '2 days'}); %! assert_equal (categories (C), {'1 day'; '2 days'; '3 days'}); %! assert_equal (double (C), [3, NaN, 3; 2, 1, 2]); %!test %! C = categorical (days ([3, NaN, 3; 2, 1, 2]), days ([3:-1:1]), 'Ordinal', ... %! true); %! assert_equal (dispstrings (C), {'3 days', '', ... %! '3 days'; '2 days', '1 day', '2 days'}); %! assert_equal (categories (C), {'3 days'; '2 days'; '1 day'}); %! assert_equal (double (C), [1, NaN, 1; 2, 3, 2]); %!test %! C = categorical (days ([3, NaN, 3; 2, 1, 2]), days ([3, 1, 2]), ... %! 'Ordinal', true); %! assert_equal (dispstrings (C), {'3 days', '', ... %! '3 days'; '2 days', '1 day', '2 days'}); %! assert_equal (categories (C), {'3 days'; '1 day'; '2 days'}); %! assert_equal (double (C), [1, NaN, 1; 3, 2, 3]); %!test %! C = categorical (days ([3, NaN, 3; 2, 1, 2]), days ([2, 3, 1]), ... %! 'Ordinal', true); %! assert_equal (dispstrings (C), {'3 days', '', ... %! '3 days'; '2 days', '1 day', '2 days'}); %! assert_equal (categories (C), {'2 days'; '3 days'; '1 day'}); %! assert_equal (double (C), [2, NaN, 2; 1, 3, 1]); %!test %! C = categorical ([1:5], [1:5], {'A', 'B', 'C', 'D', 'E'}); %! assert_equal (dispstrings (C), {'A', 'B', 'C', 'D', 'E'}); %! assert_equal (categories (C), {'A'; 'B'; 'C'; 'D'; 'E'}); %! assert_equal (double (C), 1:5); %!test %! C = categorical ([1:5], [5:-1:1], {'A', 'B', 'C', 'D', 'E'}); %! assert_equal (dispstrings (C), {'E', 'D', 'C', 'B', 'A'}); %! assert_equal (categories (C), {'A'; 'B'; 'C'; 'D'; 'E'}); %! assert_equal (double (C), [5, 4, 3, 2, 1]); %!test %! C = categorical ([1:5], [1:5], {'E', 'D', 'C', 'B', 'A'}); %! assert_equal (dispstrings (C), {'E', 'D', 'C', 'B', 'A'}); %! assert_equal (categories (C), {'E'; 'D'; 'C'; 'B'; 'A'}); %! assert_equal (double (C), 1:5); %!test %! C = categorical ([1:5], [5:-1:1], {'E', 'D', 'C', 'B', 'A'}); %! assert_equal (dispstrings (C), {'A', 'B', 'C', 'D', 'E'}); %! assert_equal (categories (C), {'E'; 'D'; 'C'; 'B'; 'A'}); %! assert_equal (double (C), [5, 4, 3, 2, 1]); %!test %! C = categorical (categorical (1:5), categorical (1:5), {'A', 'B', 'C', ... %! 'D', 'E'}); %! assert_equal (dispstrings (C), {'A', 'B', 'C', 'D', 'E'}); %! assert_equal (categories (C), {'A'; 'B'; 'C'; 'D'; 'E'}); %! assert_equal (double (C), 1:5); %!test %! C = categorical (categorical (1:5), categorical ([5:-1:1]), {'A', 'B', ... %! 'C', 'D', 'E'}); %! assert_equal (dispstrings (C), {'E', 'D', 'C', 'B', 'A'}); %! assert_equal (categories (C), {'A'; 'B'; 'C'; 'D'; 'E'}); %! assert_equal (double (C), [5, 4, 3, 2, 1]); %!test %! C = categorical (categorical (1:5), categorical (1:5), {'E', 'D', 'C', ... %! 'B', 'A'}); %! assert_equal (dispstrings (C), {'E', 'D', 'C', 'B', 'A'}); %! assert_equal (categories (C), {'E'; 'D'; 'C'; 'B'; 'A'}); %! assert_equal (double (C), 1:5); %!test %! C = categorical (categorical (1:5), categorical ([5:-1:1]), {'E', 'D', ... %! 'C', 'B', 'A'}); %! assert_equal (dispstrings (C), {'A', 'B', 'C', 'D', 'E'}); %! assert_equal (categories (C), {'E'; 'D'; 'C'; 'B'; 'A'}); %! assert_equal (double (C), [5, 4, 3, 2, 1]); %!test %! C = categorical (categorical (1:5), categorical (2:4), {'B', 'C', 'D'}); %! assert_equal (dispstrings (C), {'', 'B', 'C', 'D', ... %! ''}); %! assert_equal (categories (C), {'B'; 'C'; 'D'}); %! assert_equal (double (C), [NaN, 1, 2, 3, NaN]); %!test %! C = categorical (categorical ([5:-1:1]), categorical (1:5), {'A', 'B', ... %! 'C', 'D', 'E'}); %! assert_equal (dispstrings (C), {'E', 'D', 'C', 'B', 'A'}); %! assert_equal (categories (C), {'A'; 'B'; 'C'; 'D'; 'E'}); %! assert_equal (double (C), [5, 4, 3, 2, 1]); %!test %! C = categorical (categorical ([5:-1:1]), categorical ([5:-1:1]), {'A', ... %! 'B', 'C', 'D', 'E'}); %! assert_equal (dispstrings (C), {'A', 'B', 'C', 'D', 'E'}); %! assert_equal (categories (C), {'A'; 'B'; 'C'; 'D'; 'E'}); %! assert_equal (double (C), 1:5); %!test %! C = categorical (categorical ([5:-1:1]), categorical (1:5), {'E', 'D', ... %! 'C', 'B', 'A'}); %! assert_equal (dispstrings (C), {'A', 'B', 'C', 'D', 'E'}); %! assert_equal (categories (C), {'E'; 'D'; 'C'; 'B'; 'A'}); %! assert_equal (double (C), [5, 4, 3, 2, 1]); %!test %! C = categorical (categorical ([5:-1:1]), categorical ([5:-1:1]), {'E', ... %! 'D', 'C', 'B', 'A'}); %! assert_equal (dispstrings (C), {'E', 'D', 'C', 'B', 'A'}); %! assert_equal (categories (C), {'E'; 'D'; 'C'; 'B'; 'A'}); %! assert_equal (double (C), 1:5); %!test %! C = categorical ([1, 2, 1, NaN, 3, 2, 3]); %! assert_equal (dispstrings (C), ({'1', '2', '1', '', '3', '2', ... %! '3'})); %! C = categorical ([1, 2, 1, NaN, 3, 2, 3], [1, 2, 3, NaN], {'a', 's', 'd', ... %! 'f'}); %! assert_equal (dispstrings (C), {'a', 's', 'a', 'f', 'd', 's', 'd'}); %! assert_equal (categories (C), {'a'; 's'; 'd'; 'f'}); %! assert_equal (double (C), [1, 2, 1, 4, 3, 2, 3]); %!test %! C = categorical (categorical ([1, 2, 1, NaN, 3, 2, 3])); %! assert_equal (dispstrings (C), ({'1', '2', '1', '', '3', '2', ... %! '3'})); %! C = categorical (categorical ([1, 2, 1, NaN, 3, 2, 3]), ... %! categorical ([1, 2, 3, NaN]), {'a', 's', 'd', 'f'}); %! assert_equal (dispstrings (C), {'a', 's', 'a', 'f', 'd', 's', 'd'}); %! assert_equal (categories (C), {'a'; 's'; 'd'; 'f'}); %! assert_equal (double (C), [1, 2, 1, 4, 3, 2, 3]); %!test %! C = categorical (1:5, [1, 2, 3], {'a', 'b', 'b'}); %! assert_equal (dispstrings (C), {'a', 'b', 'b', '', ... %! ''}); %! assert_equal (categories (C), {'a'; 'b'}); %! assert_equal (double (C), [1, 2, 2, NaN, NaN]); %!test %! C = categorical (categorical (1:5), categorical ([1, 2, 3]), {'a', 'b', ... %! 'b'}); %! assert_equal (dispstrings (C), {'a', 'b', 'b', '', ... %! ''}); %! assert_equal (categories (C), {'a'; 'b'}); %! assert_equal (double (C), [1, 2, 2, NaN, NaN]); %!test %! C = categorical ({'1', '2', '1', '2'}, {' ', '1', '2'}, {'a','s','d'}); %! assert_equal (dispstrings (C), {'s', 'd', 's', 'd'}); %! assert_equal (categories (C), {'a'; 's'; 'd'}); %! assert_equal (double (C), [2, 3, 2, 3]); %!test %! C = categorical ({'1', '2', '', '2'}, {' ', '1', '2'}, {'a','s','d'}); %! assert_equal (dispstrings (C), {'s', 'd', 'a', 'd'}); %! assert_equal (categories (C), {'a'; 's'; 'd'}); %! assert_equal (double (C), [2, 3, 1, 3]); %!test %! C = categorical ({'1', '2', ' ', '2'}, {' ', '1', '2'}, {'a','s','d'}); %! assert_equal (dispstrings (C), {'s', 'd', 'a', 'd'}); %! assert_equal (categories (C), {'a'; 's'; 'd'}); %! assert_equal (double (C), [2, 3, 1, 3]); %!test %! C = categorical (string ({'1', '2', '1', '2'}), string ({' ', '1', '2'}), ... %! {'a','s','d'}); %! assert_equal (dispstrings (C), {'s', 'd', 's', 'd'}); %! assert_equal (categories (C), {'a'; 's'; 'd'}); %! assert_equal (double (C), [2, 3, 2, 3]); %!test %! C = categorical (string ({'1', '2', '', '2'}), string ({' ', '1', '2'}), ... %! {'a','s','d'}); %! assert_equal (dispstrings (C), {'s', 'd', 'a', 'd'}); %! assert_equal (categories (C), {'a'; 's'; 'd'}); %! assert_equal (double (C), [2, 3, 1, 3]); %!test %! C = categorical (string ({'1', '2', ' ', '2'}), string ({' ', '1', '2'}), ... %! {'a','s','d'}); %! assert_equal (dispstrings (C), {'s', 'd', 'a', 'd'}); %! assert_equal (categories (C), {'a'; 's'; 'd'}); %! assert_equal (double (C), [2, 3, 1, 3]); %!test %! C = categorical (string ({'1', '2', ' ', '2'}), string ({missing, '1', ... %! '2'}), {'a','s','d'}); %! assert_equal (dispstrings (C), {'s', 'd', 'a', 'd'}); %! assert_equal (categories (C), {'a'; 's'; 'd'}); %! assert_equal (double (C), [2, 3, 1, 3]); %!test %! C = categorical (days ([1, 2, 1, 3, NaN])); %! assert_equal (dispstrings (C), {'1 day', '2 days', '1 day', '3 days', ... %! ''}); %! assert_equal (categories (C), {'1 day'; '2 days'; '3 days'}); %! assert_equal (double (C), [1, 2, 1, 3, NaN]); %!test %! C = categorical (days ([1, 2, 1, 3, NaN]), days ([1, 2, 3, NaN]), {'a', ... %! 'b', 'c', 'd'}); %! assert_equal (dispstrings (C), {'a', 'b', 'a', 'c', 'd'}); %! assert_equal (categories (C), {'a'; 'b'; 'c'; 'd'}); %! assert_equal (double (C), [1, 2, 1, 3, 4]); %!test %! C = categorical (days ([1, 2, 1, 3, NaN]), days ([1, 2, 3, NaN]), {'a', ... %! 'c', 'c', 'd'}); %! assert_equal (dispstrings (C), {'a', 'c', 'a', 'c', 'd'}); %! assert_equal (categories (C), {'a'; 'c'; 'd'}); %! assert_equal (double (C), [1, 2, 1, 2, 3]); %!test %! DT = [datetime('yesterday'), datetime('today'), datetime('tomorrow')]; %! C = categorical (DT); %! assert_equal (dispstrings (C), dispstrings (DT)); %! assert_equal (categories (C), dispstrings (DT')); %! assert_equal (double (C), [1, 2, 3]); %!test %! DT = [datetime('yesterday'), datetime('today'), datetime('tomorrow')]; %! C = categorical (DT, DT, {'a', 'b', 'c'}); %! assert_equal (dispstrings (C), {'a', 'b', 'c'}); %! assert_equal (categories (C), {'a'; 'b'; 'c'}); %! assert_equal (double (C), [1, 2, 3]); %!test %! DT = [datetime('yesterday'), datetime('today'), NaT, datetime('tomorrow')]; %! C = categorical (DT, DT, {'a', 'b', 'c', 'd'}); %! assert_equal (dispstrings (C), {'a', 'b', 'c', 'd'}); %! assert_equal (categories (C), {'a'; 'b'; 'c'; 'd'}); %! assert_equal (double (C), [1, 2, 3, 4]); %!test %! DT = [datetime('yesterday'), datetime('today'), NaT, datetime('tomorrow')]; %! C = categorical (DT, DT, {'a', 'b', 'c', 'c'}); %! assert_equal (dispstrings (C), {'a', 'b', 'c', 'c'}); %! assert_equal (categories (C), {'a'; 'b'; 'c'}); %! assert_equal (double (C), [1, 2, 3, 3]); %!test %! C = categorical ([false, true, true, false, true]); %! assert_equal (dispstrings (C), {'false', 'true', 'true', 'false', 'true'}); %! assert_equal (categories (C), {'false'; 'true'}); %! assert_equal (double (C), [1, 2, 2, 1, 2]); %!error ... %! categorical (1, 'Ordinal', 3) %!error ... %! categorical (1, 'Ordinal', 'true') %!error ... %! categorical (1, 'Protected', 3) %!error ... %! categorical (1, 'Protected', 'true') %!error ... %! categorical (categorical (1:5), categorical ([1, NaN, NaN])) %!error ... %! categorical (categorical (1:5), categorical ([1, 1, 1])) %!error ... %! categorical (categorical (1:5), categorical ([1, 2, NaN])) %!error ... %! categorical (categorical (1:5), categorical ([1, 2, NaN]), [1, 2]) %!error ... %! categorical (categorical (1:5), categorical ([1, 2, NaN]), {'1', '2'}) %!error ... %! categorical (categorical (1:5), categorical ([1, 2, NaN]), {'1', '2', ''}) %!error ... %! categorical (categorical (1:5), categorical ([1, 2, 3]), [1, 2]) %!error ... %! categorical (categorical (1:5), categorical ([1, 2, 3]), {'1', '2'}) %!error ... %! categorical (categorical (1:5), categorical ([1, 2, 3]), {'1', '2', ''}) %!error ... %! categorical (categorical (1:5), [1, 2, 3]) %!error categorical ({1}) %!error ... %! categorical ({'1', '2', '3'}, [1, 2, 3]) %!error ... %! categorical (days ([1, 2, 3]), [1, 2, 3]) %!error ... %! categorical (1:5, [1, 2, NaN]) %!error ... %! categorical (1:5, [1, NaN, NaN], {'a', 'b', 'c'}) %!error ... %! categorical (1:5, [1, 2, 2]) %!error ... %! categorical (1:5, [2, 2, NaN], {'a', 'b', 'c'}) %!error ... %! categorical (1:5, [1, 2, NaN], ['a'; 'b'; 'c']) %!error ... %! categorical (1:5, [1, 2, NaN], days ([1, 2, 3])) %!error ... %! categorical (1:5, [1, 2, NaN], {'a', 'b'}) %!error ... %! categorical (1:5, [1, 2, NaN], {'a', 'b', ''}) %!test %! C = categorical ([1, 2, 1, NaN, 3, 2, 3]); %! assert_equal (cellstr (C), dispstrings (C)); %!test %! C = categorical ([1, 2, 1, NaN, 3, 2, 3]); %! assert_equal (char (C), char (cellstr (C))); %! assert_equal (size (char (C)), [7, 11]); %! assert_equal (class (char (C)), 'char'); %!test %! C = categorical ([1, 2, 1, NaN, 3, 2, 3]); %! assert_equal (double (C), [1, 2, 1, NaN, 3, 2, 3]); %! assert_equal (single (C), single ([1, 2, 1, NaN, 3, 2, 3])); %! assert_equal (int64 (C), int64 ([1, 2, 1, 0, 3, 2, 3])); %! assert_equal (int32 (C), int32 ([1, 2, 1, 0, 3, 2, 3])); %! assert_equal (int16 (C), int16 ([1, 2, 1, 0, 3, 2, 3])); %! assert_equal (int8 (C), int8 ([1, 2, 1, 0, 3, 2, 3])); %! assert_equal (uint64 (C), uint64 ([1, 2, 1, 0, 3, 2, 3])); %! assert_equal (uint32 (C), uint32 ([1, 2, 1, 0, 3, 2, 3])); %! assert_equal (uint16 (C), uint16 ([1, 2, 1, 0, 3, 2, 3])); %! assert_equal (uint8 (C), uint8 ([1, 2, 1, 0, 3, 2, 3])); %!test %! C = categorical (1:256); %! assert_equal (int8 (C), int8 ([1:126, 127*ones(1, 130)])); %! assert_equal (uint8 (C), uint8 ([1:255, 255])); ################################################################################ ## ** Summary Information ** ## ################################################################################ ## Available Methods ## ## ## ## 'summary' 'categories' 'countcats' 'length' ## ## 'size' 'ndims' 'numel' 'keyHash' ## ## ## ################################################################################ %!test %! C = categorical (repmat ([1, 3; 4, 2], 3, 3, 2)); %! s = summary (C, 1); %! assert_equal (s.Size, [6, 6, 2]); %! assert_equal (s.Type, 'categorical'); %! assert_equal (s.Categories, {'1'; '2'; '3'; '4'}); %! assert_equal (s.Counts, countcats (C, 1)); %! assert_equal (size (s.Counts), [4, 6, 2]); %! assert_equal (s.NumMissing, sum (isundefined (C), 1)); %! assert_equal (size (s.NumMissing), [1, 6, 2]); %!test %! C = categorical (repmat ([1, 3; 4, 2], 3, 3, 2)); %! s = summary (C, 2); %! assert_equal (s.Size, [6, 6, 2]); %! assert_equal (s.Type, 'categorical'); %! assert_equal (s.Categories, {'1'; '2'; '3'; '4'}); %! assert_equal (s.Counts, countcats (C, 2)); %! assert_equal (size (s.Counts), [6, 4, 2]); %! assert_equal (s.NumMissing, sum (isundefined (C), 2)); %! assert_equal (size (s.NumMissing), [6, 1, 2]); %!test %! C = categorical (repmat ([1, 3; 4, 2], 3, 3, 2)); %! s = summary (C, 3); %! assert_equal (s.Size, [6, 6, 2]); %! assert_equal (s.Type, 'categorical'); %! assert_equal (s.Categories, {'1'; '2'; '3'; '4'}); %! assert_equal (s.Counts, countcats (C, 3)); %! assert_equal (size (s.Counts), [6, 6, 4]); %! assert_equal (s.NumMissing, sum (isundefined (C), 3)); %! assert_equal (size (s.NumMissing), [6, 6]); %!test %! C = categorical (repmat ([1, 3; 4, 2], 3, 3, 2)); %! s = summary (C, 4); %! assert_equal (s.Size, [6, 6, 2]); %! assert_equal (s.Type, 'categorical'); %! assert_equal (s.Categories, {'1'; '2'; '3'; '4'}); %! assert_equal (s.Counts, countcats (C, 4)); %! assert_equal (size (s.Counts), [6, 6, 2, 4]); %! assert_equal (s.NumMissing, sum (isundefined (C), 4)); %! assert_equal (size (s.NumMissing), [6, 6, 2]); %!test %! C = categorical (string ({'1', '2', ' ', '2'}), string ({' ', '1', '2'}), ... %! {'a', 's', 'd'}); %! s = summary (C); %! assert_equal (s.Size, [1, 4]); %! assert_equal (s.Type, 'categorical'); %! assert_equal (s.Categories, {'a'; 's'; 'd'}); %! assert_equal (s.Counts, [1, 1, 2]); %! assert_equal (s.NumMissing, 0); %!test %! C = categorical ({'A'; 'B'; 'C'; ''; 'C'; ''; 'C'; 'A'; ''}); %! C = repmat (C, 1, 4, 3); %! s = summary (C); %! assert_equal (s.Size, [9, 4, 3]); %! assert_equal (s.Type, 'categorical'); %! assert_equal (s.Categories, {'A'; 'B'; 'C'}); %! assert_equal (s.Counts, countcats (C)); %! assert_equal (size (s.Counts), [3, 4, 3]); %! assert_equal (s.NumMissing, sum (isundefined (C))); %! assert_equal (size (s.NumMissing), [1, 4, 3]); %!test %! C = categorical ({'A'; 'B'; 'C'; ''; 'C'; ''; 'C'; 'A'; ''}); %! C = repmat (C, 1, 4, 3); %! s = summary (C, 2); %! assert_equal (s.Size, [9, 4, 3]); %! assert_equal (s.Type, 'categorical'); %! assert_equal (s.Categories, {'A'; 'B'; 'C'}); %! assert_equal (s.Counts, countcats (C, 2)); %! assert_equal (size (s.Counts), [9, 3, 3]); %! assert_equal (s.NumMissing, sum (isundefined (C), 2)); %! assert_equal (size (s.NumMissing), [9, 1, 3]); %!test %! C = categorical ({'A'; 'B'; 'C'; ''; 'C'; ''; 'C'; 'A'; ''}); %! C = repmat (C, 1, 4, 3); %! s = summary (C, 3); %! assert_equal (s.Size, [9, 4, 3]); %! assert_equal (s.Type, 'categorical'); %! assert_equal (s.Categories, {'A'; 'B'; 'C'}); %! assert_equal (s.Counts, countcats (C, 3)); %! assert_equal (size (s.Counts), [9, 4, 3]); %! assert_equal (s.NumMissing, sum (isundefined (C), 3)); %! assert_equal (size (s.NumMissing), [9, 4]); %!test %! C = categorical ({'A'; 'B'; 'C'; ''; 'C'; ''; 'C'; 'A'; ''}); %! C = repmat (C, 1, 4, 3); %! s = summary (C, 4); %! assert_equal (s.Size, [9, 4, 3]); %! assert_equal (s.Type, 'categorical'); %! assert_equal (s.Categories, {'A'; 'B'; 'C'}); %! assert_equal (s.Counts, countcats (C, 4)); %! assert_equal (size (s.Counts), [9, 4, 3, 3]); %! assert_equal (s.NumMissing, sum (isundefined (C), 4)); %! assert_equal (size (s.NumMissing), [9, 4, 3]); %!test %! s = summary (categorical ([1, 2, 1, 3, 2, 3, 2, 1, 3, 2], [1, 2, 3], ... %! {'l','m','h'}, 'Ordinal', true), 1, ... %! 'Statistics', {'min', 'nummissing', 'counts', 'max', 'median'}); %! assert_equal (s.Size, [1, 10]); %! assert_equal (s.Type, 'categorical'); %! assert_equal (s.Categories, {'l'; 'm'; 'h'}); %! assert_equal (size (s.Counts), [3, 10]); %! C = categorical ({'l','m','l','h','m','h','m','l','h','m'}, ... %! {'l','m','h'}, {'l','m','h'}, 'Ordinal', true); %! assert_equal (s.Min, C); %! assert_equal (s.NumMissing, zeros (1, 10)); %! assert_equal (s.Max, C); %! assert_equal (s.Median, C); %!test %! s = summary (categorical ([1, 2, 1, 3, 2, 3, 2, 1, 3, NaN], [1, 2, 3], ... %! {'l','m','h'}, 'Ordinal', true), 1, ... %! 'Statistics', {'min', 'nummissing', 'counts', 'max', 'median'}); %! assert_equal (s.Size, [1, 10]); %! assert_equal (s.Type, 'categorical'); %! assert_equal (s.Categories, {'l'; 'm'; 'h'}); %! assert_equal (size (s.Counts), [3, 10]); %! C = categorical ({'l','m','l','h','m','h','m','l','h',''}, ... %! {'l','m','h'}, {'l','m','h'}, 'Ordinal', true); %! assert_equal (isequaln (s.Min, C), true); %! assert_equal (s.NumMissing, [0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); %! assert_equal (isequaln (s.Max, C), true); %! assert_equal (isequaln (s.Median, C), true); %!test # a category name too wide for the terminal is cut, the others are not %! out = evalc ('summary (categorical ({repmat(''a'', 1, 500); ''b''}))'); %! assert_equal (isempty (strfind (out, 'a ... ')), false); %! assert_equal (isempty (regexp (out, '\', 'once')), false); %!test %! s = summary (categorical ([1, 2, 1, 3, 2, 3, 2, 1, 3, NaN], [1, 2, 3], ... %! {'l','m','h'}, 'Ordinal', true), 1, ... %! 'Statistics', {'min', 'nummissing', 'max', 'median', 'mode'}); %! assert_equal (s.Size, [1, 10]); %! assert_equal (s.Type, 'categorical'); %! assert_equal (s.Categories, {'l'; 'm'; 'h'}); %! assert_equal (size (s.Min), [1, 10]); %! assert_equal (s.NumMissing, [0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); %! assert_equal (size (s.Max), [1, 10]); %! assert_equal (size (s.Median), [1, 10]); %! assert_equal (size (s.Mode), [1, 10]); %!test %! s = summary (categorical ([1, 2, 1, 3, 2, 3, 2, 1, 3, NaN], [1, 2, 3], ... %! {'l','m','h'}, 'Ordinal', true), 1, ... %! 'Statistics', {'min', 'counts', 'max', 'median', 'mode'}); %! assert_equal (s.Size, [1, 10]); %! assert_equal (s.Type, 'categorical'); %! assert_equal (s.Categories, {'l'; 'm'; 'h'}); %! assert_equal (size (s.Counts), [3, 10]); %! assert_equal (size (s.Min), [1, 10]); %! assert_equal (size (s.Max), [1, 10]); %! assert_equal (size (s.Median), [1, 10]); %! assert_equal (size (s.Mode), [1, 10]); %!error ... %! summary (categorical ([1:3]), 2, 3) %!error ... %! summary (categorical ([1:3]), 0) %!error ... %! summary (categorical ([1:3]), [1, 2]) %!error ... %! summary (categorical ([1:3]), 'a') %!assert_equal (categories (categorical), {}) %!assert_equal (categories (categorical (NaN (3), [1, 2, 3])), {'1'; '2'; '3'}) %!assert_equal (categories (categorical (1:5)), {'1'; '2'; '3'; '4'; '5'}) %!assert_equal (countcats (categorical), []) %!assert_equal (countcats (categorical ([NaN, 1;NaN,2;NaN, NaN])), [0, 1; 0, 1]) %!assert_equal (countcats (categorical (NaN (3))), zeros (0, 3)) %!assert_equal (countcats (categorical (NaN (3), [1, 2, 3])), zeros (3)) %!assert_equal (countcats (categorical (NaN (2, 0, 3))), zeros (0, 0, 3)) %!assert_equal (countcats (categorical (NaN (2, 0, 3), [1, 2, ... %! 3])), zeros (3, 0, 3)) %!assert_equal (countcats (categorical (NaN (2, 0, 3), [1, 2, 3]), ... %! 2), zeros (2, 3, 3)) %!assert_equal (countcats (categorical (ones (3, 2, 0, 2))), ones (0, 2, 0, 2)) %!assert_equal (countcats (categorical (ones (3, 2, 0, 2)), 3), ones (3, 2, ... %! 0, 2)) %!error ... %! countcats (categorical (ones (3, 2, 0, 2)), 0) %!error ... %! countcats (categorical (ones (3, 2, 0, 2)), [1, 2]) %!error ... %! countcats (categorical (ones (3, 2, 0, 2)), 'a') %!assert_equal (length (categorical), 0) %!assert_equal (length (categorical (ones (3, 2, 0, 2))), 0) %!assert_equal (length (categorical (ones (2, 3, 4))), 4) %!assert_equal (length (categorical (ones (3, 4, 2))), 4) %!assert_equal (length (categorical (ones (3))), 3) %!assert_equal (size (categorical), [0, 0]) %!assert_equal (size (categorical ([1:5])), [1, 5]) %!assert_equal (size (categorical (ones (2, 3, 4))), [2, 3, 4]) %!assert_equal (size (categorical (ones (2, 3, 4)), 1), 2) %!assert_equal (size (categorical (ones (2, 3, 4)), 2), 3) %!assert_equal (size (categorical (ones (2, 3, 4)), 3), 4) %!assert_equal (size (categorical (ones (2, 3, 4)), 4), 1) %!test %! [d1, d2, d3] = size (categorical (ones (2, 3, 4))); %! assert_equal (d1, 2); %! assert_equal (d2, 3); %! assert_equal (d3, 4); %!test %! [d1, d2] = size (categorical (ones (2, 3, 4)), 1, 2); %! assert_equal (d1, 2); %! assert_equal (d2, 3); %!test %! [d2, d3] = size (categorical (ones (2, 3, 4)), 2, 3); %! assert_equal (d2, 3); %! assert_equal (d3, 4); %!test %! [d2, d3] = size (categorical (ones (2, 3, 4)), [2, 3]); %! assert_equal (d2, 3); %! assert_equal (d3, 4); %!error ... %! [d2, d3] = size (categorical (ones (2, 3, 4)), [2, 3, 4]) %!assert_equal (ndims (categorical), 2) %!assert_equal (ndims (categorical (ones (2, 3, 4))), 3) %!assert_equal (ndims (categorical (ones (2, 3, 0, 4))), 4) %!assert_equal (numel (categorical), 0) %!assert_equal (numel (categorical (1:5)), 5) %!assert_equal (numel (categorical (ones (2, 3, 4))), 24) ## Hash codes changed in 1.3.1: the element labels are framed, the Protected ## flag is no longer part of the key, and the category list is hashed only ## for an ordinal array. These literals double as the cross-session ## determinism guard, having been computed in an earlier session. %!assert_equal (keyHash (categorical), uint64 (9221239794199714935)) %!assert_equal (keyHash (categorical (1:5)), uint64 (7961966267264693254)) %!assert_equal (keyHash (categorical (ones (3))), uint64 (17608104484830113158)) %!assert_equal (keyHash (categorical (ones (2, 3, ... %! 4))), uint64 (8739279001166458266)) %!assert_equal (keyHash (categorical (NaN (3))), uint64 (12122200442661984653)) %!test %! key = keyHash (categorical ([1, 2; 3, 4])); %! assert_equal (key, uint64 (14079952376186860569)); %!test %! key = keyHash (categorical ([1, 2; 3, 4], [1, 2, 3, 4])); %! assert_equal (key, uint64 (14079952376186860569)); %!test %! key = keyHash (categorical ([1, 2; 3, 4], [1, 2, 3, 4], {'a', 'b', 'c', ... %! 'd'})); %! assert_equal (key, uint64 (1290798791422273017)); %!test %! key = keyHash (categorical (1:3, [1, 2, 3])); %! assert_equal (key, uint64 (10691546085044606033)); %!test %! base_key = uint64 (2342124352342344234); %! key = keyHash (categorical (1:3, [1, 2, 3]), base_key); %! assert_equal (key, uint64 (4813383759399324834)); ## Identity is the label, plus the ordering only when ordinal. Neither the ## Protected flag nor a non-ordinal array's category set is part of the key. %!test %! a = categorical ({'a'}, {'a', 'b'}); %! assert_equal (keyMatch (a, categorical ({'a'}, {'a', 'c'})), true); %! assert_equal (keyHash (a), keyHash (categorical ({'a'}, {'a', 'c'}))); %! assert_equal (keyMatch (a, categorical ({'a'}, {'b', 'a'})), true); %! assert_equal (keyHash (a), keyHash (categorical ({'a'}, {'b', 'a'}))); %! assert_equal (keyMatch (a, categorical ({'a'}, {'a', 'b'}, 'Protected', ... %! true)), ... %! true); %! assert_equal (keyHash (a), ... %! keyHash (categorical ({'a'}, {'a', 'b'}, 'Protected', true))); %!test %! o = categorical ({'a'}, {'a', 'b'}, 'Ordinal', true); %! assert_equal (keyMatch (o, categorical ({'a'}, {'a', 'b'})), false); %! assert_equal (keyMatch (o, categorical ({'a'}, {'b', 'a'}, 'Ordinal', ... %! true)), ... %! false); %! assert_equal (keyMatch (o, categorical ({'a'}, {'a', 'b'}, 'Ordinal', ... %! true)), ... %! true); %! assert_equal (keyHash (o), ... %! keyHash (categorical ({'a'}, {'a', 'b'}, 'Ordinal', true))); %!error ... %! keyHash (categorical (1:5), uint64 ([1, 2])) %!error ... %! keyHash (categorical (1:5), 2231107818551636405) ## A zero offset basis is a basis, not the absence of one, and a falsy base ## must still be validated rather than silently replaced by the default. %!test %! a = keyHash (categorical (1:3)); %! assert_equal (isequal (a, keyHash (categorical (1:3), uint64 (0))), false); %! assert_equal (keyHash (categorical (1:3), []), a); %!error ... %! keyHash (categorical (1:5), 0) %!error ... %! keyHash (categorical (1:5), uint64 ([0, 1])) ################################################################################ ## ** Query Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'iscategory' 'iscolumn' 'isempty' 'isequal' ## ## 'isequaln' 'ismatrix' 'ismember' 'ismissing' ## ## 'isordinal' 'isprotected' 'isrow' 'isscalar' ## ## 'issorted' 'issortedrows' 'isundefined' 'isvector' ## ## ## ################################################################################ %!assert_equal (iscategory (categorical, ''), false) %!assert_equal (iscategory (categorical (1:3), '1'), true) %!assert_equal (iscategory (categorical (1:3), '4'), false) %!assert_equal (iscategory (categorical (1:3), string ('1')), true) %!assert_equal (iscategory (categorical (1:3), string ('4')), false) %!assert_equal (iscategory (categorical (1:3), {'1', '2'}), [true, true]) %!assert_equal (iscategory (categorical (1:3), {'1'; '2'}), [true; true]) %!assert_equal (iscategory (categorical (1:3), {'1', '4', ... %! '3'}), logical ([1, 0, 1])) %!assert_equal (size (iscategory (categorical (1:3), cell (0, 1))), [0, 1]) %!assert_equal (size (iscategory (categorical (1:3), cell (1, 0))), [1, 0]) %!test %! C = categorical ([], [1, 2, 3], {'A', 'B', 'C'}); %! assert_equal (iscategory (C, 'A'), true); %! assert_equal (iscategory (C, '1'), false); %! assert_equal (iscategory (C, string ('A')), true); %! assert_equal (iscategory (C, string (1:3)), [false, false, false]); %!error ... %! iscategory (categorical) %!assert_equal (iscolumn (categorical), false) %!assert_equal (iscolumn (categorical (1)), true) %!assert_equal (iscolumn (categorical (1:2)), false) %!assert_equal (iscolumn (categorical ([1:2]')), true) %!assert_equal (iscolumn (categorical (ones (2))), false) %!assert_equal (isempty (categorical), true) %!assert_equal (isempty (categorical (1)), false) %!assert_equal (isempty (categorical (ones (3, 2, 0, 3))), true) %!assert_equal (isempty (categorical ([], [1, 2, 3], {'A', 'B', 'C'})), true) %!assert_equal (isempty (categorical (NaN (2, 0), [1, 2, 3], {'A', 'B', ... %! 'C'})), true) %!assert_equal (isequal (categorical ([1, 1, 1]), ... %! categorical ([2, 2, 2], [1, 2], {'2', '1'})), true) %!assert_equal (isequal (categorical ([1, NaN, 1]), ... %! categorical ([2, NaN, 2], [1, 2], {'2', '1'})), false) %!assert_equal (isequal (categorical ([1, 1, NaN]), ... %! categorical ([2, 2, NaN], [1, 2], {'2', '1'})), false) %!assert_equal (isequal (categorical ([1, 1, 1]), ... %! categorical ([1, 1, 1], [1, 2], {'2', '1'})), false) %!assert_equal (isequal (categorical ([1, 1, 1], [1, 2]), ... %! categorical ([2, 2, 2], [1, 2], {'2', '1'})), true) %!assert_equal (isequal (categorical ([NaN, 1, 1], [1, 2]), ... %! categorical ([NaN, 2, 2], [1, 2], {'2', '1'})), false) %!assert_equal (isequal (categorical ([1, NaN, 1], [1, 2]), ... %! categorical ([2, NaN, 2], [1, 2], {'2', '1'})), false) %!assert_equal (isequal (categorical ([1, 1, 1], [1, 2]), ... %! categorical ([1, 1, 1], [1, 2], {'2', '1'})), false) %!assert_equal (isequal ({'1', '1', '1'}, categorical ([2, 2, 2], [1, 2], ... %! {'2', '1'})), true) %!assert_equal (isequal (categorical ([1, 1, 1]), {'1', '1', '1'}), true) %!assert_equal (isequal (string ({'1', '1', '1'}), ... %! categorical ([2, 2, 2], [1, 2], {'2', '1'})), true) %!assert_equal (isequal (categorical ([2, 2, 2], [1, 2], {'2', '1'}), ... %! string ({'1', '1', '1'})), true) %!assert_equal (isequal (missing, categorical), false) %!assert_equal (isequal (missing, categorical (NaN)), false) %!assert_equal (isequal (categorical (1), string (1)), true) %!assert_equal (isequal (string (1), categorical (1)), true) %!assert_equal (isequal (categorical (NaN), string (missing)), false) %!test %! C1 = categorical ([1, 1, 1], [1, 2], {'A', 'B'}); %! C2 = categorical ([1, 1, 1], [1, 2], {'A', 'B'}); %! assert_equal (isequal (C1, C2), true); %!test %! C1 = categorical ([1, 1, 1], [1, 2], {'A', 'B'}); %! C2 = categorical ([2, 2, 2], [1, 2], {'B', 'A'}); %! assert_equal (isequal (C1, C2), true); %! assert_equal (isequal (C1, C2, {'A', 'A', 'A'}), true); %! assert_equal (isequal (C1, C2, {'A', '', 'A'}), false); %!test %! C1 = categorical ([1, 1, 1], [1, 2], {'A', 'B'}); %! C2 = categorical ([1, 1, 1], [1, 2], {'A', 'B'}, 'Ordinal', true); %! assert_equal (isequal (C1, C2), false); %!test %! C1 = categorical ([1, 1, 1], [1, 2], {'A', 'B'}, 'Ordinal', true); %! C2 = categorical ([1, 1, 1], [1, 2], {'A', 'B'}); %! assert_equal (isequal (C1, C2), false); %!test %! C1 = categorical ([1, 1, 1], [1, 2], {'A', 'B'}, 'Ordinal', true); %! C2 = categorical ([1, 1, 1], [1, 2], {'A', 'B'}, 'Ordinal', true); %! assert_equal (isequal (C1, C2), true); %!test %! C1 = categorical ([1, 1, 1], [1, 2], {'A', 'B'}, 'Ordinal', true); %! C2 = categorical ([2, 2, 2], [1, 2], {'B', 'A'}, 'Ordinal', true); %! assert_equal (isequal (C1, C2), false); %!assert_equal (isequaln (categorical ([1, 1, 1]), ... %! categorical ([2, 2, 2], [1, 2], {'2', '1'})), true) %!assert_equal (isequaln (categorical ([1, NaN, 1]), ... %! categorical ([2, NaN, 2], [1, 2], {'2', '1'})), true) %!assert_equal (isequaln (categorical ([1, 1, NaN]), ... %! categorical ([2, 2, NaN], [1, 2], {'2', '1'})), true) %!assert_equal (isequaln (categorical ([1, 1, 1], [1, 2]), ... %! categorical ([2, 2, 2], [1, 2], {'2', '1'})), true) %!assert_equal (isequaln (categorical ([NaN, 1, 1], [1, 2]), ... %! categorical ([NaN, 2, 2], [1, 2], {'2', '1'})), true) %!assert_equal (isequaln (categorical ([1, NaN, 1], [1, 2]), ... %! categorical ([2, NaN, 2], [1, 2], {'2', '1'})), true) %!assert_equal (isequaln (missing, categorical), false) %!assert_equal (isequaln (missing, categorical (NaN)), true) %!assert_equal (isequaln (categorical (NaN), string (missing)), true) %!assert_equal (isequaln (repmat (missing, 2), categorical (NaN (2))), true) ## A non-scalar missing operand keeps its shape whichever side it sits on. ## 'promote' took the size of a fresh 'missing', always 1x1, so it silently ## collapsed and the two operand orders disagreed with each other. %!assert_equal (isequaln (categorical (NaN (1, 3)), repmat (missing, 1, ... %! 3)), true) %!assert_equal (isequaln (repmat (missing, 1, 3), categorical (NaN (1, ... %! 3))), true) %!assert_equal (isequaln (categorical (NaN (2, 3)), repmat (missing, 2, ... %! 3)), true) %!assert_equal (isequaln (categorical (NaN (1, 3)), repmat (missing, 1, ... %! 2)), false) %!assert_equal (isequal (categorical (NaN (1, 3)), repmat (missing, 1, ... %! 3)), false) %!test %! C1 = categorical ([1, 1, NaN], [1, 2], {'A', 'B'}); %! C2 = categorical ([1, 1, NaN], [1, 2], {'A', 'B'}); %! assert_equal (isequaln (C1, C2), true); %!test %! C1 = categorical ([1, NaN, 1], [1, 2], {'A', 'B'}); %! C2 = categorical ([2, NaN, 2], [1, 2], {'B', 'A'}); %! assert_equal (isequaln (C1, C2), true); %! assert_equal (isequaln (C1, C2, {'A', 'A', 'A'}), false); %! assert_equal (isequaln (C1, C2, {'A', '', 'A'}), true); %!test %! C1 = categorical ([1, NaN, 1], [1, 2], {'A', 'B'}); %! C2 = categorical ([1, NaN, 1], [1, 2], {'A', 'B'}, 'Ordinal', true); %! assert_equal (isequaln (C1, C2), false); %!test %! C1 = categorical ([1, NaN, 1], [1, 2], {'A', 'B'}, 'Ordinal', true); %! C2 = categorical ([1, NaN, 1], [1, 2], {'A', 'B'}); %! assert_equal (isequaln (C1, C2), false); %!test %! C1 = categorical ([1, NaN, 1], [1, 2], {'A', 'B'}, 'Ordinal', true); %! C2 = categorical ([1, NaN, 1], [1, 2], {'A', 'B'}, 'Ordinal', true); %! assert_equal (isequaln (C1, C2), true); %!test %! C1 = categorical ([1, 1, 1], [1, 2], {'A', 'B'}, 'Ordinal', true); %! C2 = categorical ([2, 2, 2], [1, 2], {'B', 'A'}, 'Ordinal', true); %! assert_equal (isequaln (C1, C2), false); %!test %! C1 = categorical ([1, NaN, 1], [1, 2], {'A', 'B'}, 'Ordinal', true); %! C2 = categorical ([2, NaN, 2], [1, 2], {'B', 'A'}, 'Ordinal', true); %! assert_equal (isequaln (C1, C2), false); %!assert_equal (ismatrix (categorical), true) %!assert_equal (ismatrix (categorical (NaN)), true) %!assert_equal (ismatrix (categorical (ones (1, 0))), true) %!assert_equal (ismatrix (categorical (ones (2, 1))), true) %!assert_equal (ismatrix (categorical (ones (2, 2, 1))), true) %!assert_equal (ismatrix (categorical (ones (2, 1, 2))), false) %!assert_equal (ismatrix (categorical (ones (2, 0, 2))), false) %!assert_equal (ismember (categorical (1:3), '1'), [true, false, false]) %!assert_equal (ismember (categorical (1:3), string ('1')), [true, false, ... %! false]) %!assert_equal (ismember (categorical (1:3), {'1', '2'}), [true, true, false]) %!assert_equal (ismember (categorical (1:3), string ({'1', '2'})), [true, ... %! true, false]) %!assert_equal (ismember ('1', categorical (1:3)), true) %!assert_equal (ismember ({'1', '2'}, categorical (1:3)), [true, true]) %!assert_equal (ismember ({'1', '2'}, categorical ([1:3]')), [true, true]) %!assert_equal (ismember ({'1'; '2'}, categorical (1:3)), [true; true]) %!assert_equal (ismember ({'1', '2'}, categorical ([1, 2, 1, 3, 2, 3, 2, ... %! 1])), ... %! [true, true]) %!assert_equal (ismember (categorical ([1, 2, 1, 3, 2, 3, 2, 1]), {'1', ... %! '2'}), ... %! logical ([1, 1, 1, 0, 1, 0, 1, 1])) %!assert_equal (ismember (categorical ([1, 2, 1, 3, 2, 3, 2, 1]), ... %! categorical (1:3)), ... %! true (1, 8)) %!assert_equal (ismember (categorical (1:3), categorical([1, 2, 1, 3, 2, 3, ... %! 2, 1])), ... %! true (1, 3)) %!assert_equal (ismember (categorical ([1, 2, 1, 3, 2, 3, 2, 1]), ... %! categorical (1:2)), ... %! logical ([1, 1, 1, 0, 1, 0, 1, 1])) %!assert_equal (ismember (categorical ([1; 2]), categorical ([1, 2, 1, 3, 2, ... %! 3, 2, 1])), ... %! [true; true]) %!test %! C1 = categorical ([1, 2, 1, 3, 2, 3, 2, NaN]); %! C2 = categorical ([1, NaN, 3]); %! assert_equal (ismember (C1, C2), logical ([1, 0, 1, 1, 0, 1, 0, 0])); %! assert_equal (ismember (C2, C1), [true, false, true]); %! assert_equal (ismember (C1', C2), logical ([1; 0; 1; 1; 0; 1; 0; 0])); %! assert_equal (ismember (C2', C1), [true; false; true]); %!test %! C1 = categorical ([1, 2, 1, 3, 2, 3, 2, NaN], [1, 2, 3], 'Ordinal', true); %! C2 = categorical ([1, NaN, 3], [1, 2, 3], 'Ordinal', true); %! assert_equal (ismember (C1, C2), logical ([1, 0, 1, 1, 0, 1, 0, 0])); %! assert_equal (ismember (C2, C1), [true, false, true]); %! assert_equal (ismember (C1', C2), logical ([1; 0; 1; 1; 0; 1; 0; 0])); %! assert_equal (ismember (C2', C1), [true; false; true]); %!test %! C1 = categorical ([1, 2, 1, 3, 2, 3, 2, NaN], [3, 2, 1], 'Ordinal', true); %! C2 = categorical ([1, NaN, 3], [3, 2, 1], 'Ordinal', true); %! assert_equal (ismember (C1, C2), logical ([1, 0, 1, 1, 0, 1, 0, 0])); %! assert_equal (ismember (C2, C1), [true, false, true]); %! assert_equal (ismember (C1', C2), logical ([1; 0; 1; 1; 0; 1; 0; 0])); %! assert_equal (ismember (C2', C1), [true; false; true]); %!test %! C1 = categorical ([1, 2, 1, 3, 2, 3, 2, NaN], [1, 2, 3], {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! C2 = categorical ([1, NaN, 3], [3, 2, 1], {'a', 'b', 'c'}, 'Ordinal', true); %! assert_equal (ismember (C1, C2), logical ([1, 0, 1, 1, 0, 1, 0, 0])); %! assert_equal (ismember (C2, C1), [true, false, true]); %! assert_equal (ismember (C1', C2), logical ([1; 0; 1; 1; 0; 1; 0; 0])); %! assert_equal (ismember (C2', C1), [true; false; true]); %!assert_equal (ismember ({'1', '2', '2'}, categorical (1:3), 'rows'), false) %!assert_equal (ismember ({'1', '2', '2'; '1', '2', '3'}, ... %! categorical (1:3), 'rows'), [false; true]) %!assert_equal (ismember ({'1', '2', '3'}, categorical (1:3), 'rows'), true) %!assert_equal (ismember ({'1', '2', '3'}, ... %! categorical ([1, 2, 3; 2, 2, 3]), 'rows'), true) %!assert_equal (ismember (string ({'1', '2', '2'}), categorical (1:3), ... %! 'rows'), ... %! false) %!assert_equal (ismember (string ({'1', '2', '2'; '1', '2', '3'}), ... %! categorical (1:3), 'rows'), [false; true]) %!assert_equal (ismember (string ({'1', '2', '3'}), ... %! categorical (1:3), 'rows'), true) %!assert_equal (ismember (string ({'1', '2', '3'}), ... %! categorical ([1, 2, 3; 2, 2, 3]), 'rows'), true) %!assert_equal (ismember (categorical (1:3), {'1', '2', '2'}, 'rows'), false) %!assert_equal (ismember (categorical (1:3), {'1', '2', '2'; '1', '2', '3'}, ... %! 'rows'), true) %!assert_equal (ismember (categorical (1:3), {'1', '2', '3'}, 'rows'), true) %!assert_equal (ismember (categorical ([1, 2, 3; 2, 2, 3]), ... %! {'1', '2', '3'}, 'rows'), [true; false]) %!error ... %! ismember (1, categorical) %!error ... %! ismember (categorical, {1}) %!error ... %! ismember (categorical, categorical, 1) %!error ... %! ismember (categorical, categorical, 'option') %!error ... %! ismember (categorical (ones (2, 3, 4)), categorical, 'rows') %!error ... %! ismember ({'1', '2'}, categorical (1:3), 'rows') %!error ... %! ismember (categorical (1:3), {'1', '2'}, 'rows') %!error ... %! ismember (categorical (1, 'Ordinal', true), categorical (2, 'Ordinal', true)) %!error ... %! ismember (categorical (1, 'Ordinal', true), categorical (1)) %!assert_equal (ismissing (categorical ([1, 2, NaN, 4]), '2'), logical ([0, ... %! 1, 0, 0])) %!assert_equal (ismissing (categorical ([1, 2, NaN, 4]), ... %! {'2'}), logical ([0, 1, 0, 0])) %!assert_equal (ismissing (categorical ([1, 2, NaN, 4]), string ('2')), ... %! logical ([0, 1, 0, 0])) %!assert_equal (ismissing (categorical ([1, 2, NaN, 4]), {'2', ... %! '4'}), logical ([0, 1, 0, 1])) %!assert_equal (ismissing (categorical ([1, 2, NaN, 4]), string ({'2', ... %! '4'})), ... %! logical ([0, 1, 0, 1])) %!assert_equal (ismissing (categorical ([1; 2; NaN; 4]), {'2', ... %! '4'}), logical ([0; 1; 0; 1])) %!assert_equal (ismissing (categorical ([1, 2; NaN, 4]), ... %! string ({'2'; '4'})), ... %! logical ([0, 1; 0, 1])) %!assert_equal (ismissing (categorical ([1, 2; NaN, 3], [1:3], {'A', 'B', ... %! 'C'}), {'A', 'B'}), ... %! logical ([1, 1; 0, 0])) %!assert_equal (ismissing (categorical ([1, 2, NaN, 3], [1:3], {'A', 'B', ... %! 'C'}), {'A', 'C'}), ... %! logical ([1, 0, 0, 1])) %!assert_equal (ismissing (categorical ([1, 2, NaN, 3], [1:3], {'A', 'B', ... %! 'C'}), {'A', ''}), ... %! logical ([1, 0, 1, 0])) %!assert_equal (ismissing (categorical ([1, 2, NaN, 4]), categorical (NaN)), ... %! logical ([0, 0, 1, 0])) %!assert_equal (ismissing (categorical ([1, 2, NaN, 4]), categorical ([NaN, ... %! 5, 2])), ... %! logical ([0, 1, 1, 0])) %!assert_equal (ismissing (categorical ([1, 2, 5, 4]), categorical ([NaN, 5, ... %! 2])), ... %! logical ([0, 1, 1, 0])) %!assert_equal (ismissing (categorical ([1, 2; 5, 4]), categorical ([NaN, 5, ... %! 2])), ... %! logical ([0, 1; 1, 0])) %!assert_equal (ismissing (categorical ([1, 2, 5, 4]), ... %! categorical ([NaN; 5; 2])), ... %! logical ([0, 1, 1, 0])) %!assert_equal (ismissing (categorical ([1, 2; 5, 4]), ... %! categorical ([NaN; 5; 2])), ... %! logical ([0, 1; 1, 0])) %!assert_equal (ismissing (categorical ({'a', ''}), missing), [false, true]) %!assert_equal (ismissing (categorical ({'a', ''}), ''), [false, true]) %!assert_equal (ismissing (categorical ({'a', ''}), char (zeros (1, 0))), ... %! [false, true]) %!assert_equal (ismissing (categorical ({'a', ''}), ' '), [false, true]) %!assert_equal (ismissing (categorical ({'a', ''}), {''}), [false, true]) %!assert_equal (ismissing (categorical ({'a', ''}), {'a', ''}), [true, true]) %!assert_equal (ismissing (categorical ({'a', 'b'}), ''), [false, false]) %!assert_equal (ismissing (categorical ({'a', ''}), string ('')), ... %! [false, false]) %!assert_equal (ismissing (categorical ({'a', ''}), string (missing)), ... %! [false, false]) %!assert_equal (ismissing (categorical ({'a', ''}), ... %! [string('a'), string('')]), [true, false]) %!error ... %! ismissing (categorical ([1, 2, 5, 4]), 1, 2) %!error ... %! ismissing (categorical ([1, 2, 5, 4]), ['1';'2']) %!error ... %! ismissing (categorical ([1, 2, 5, 4]), 1) %!error ... %! ismissing (categorical ([1, 2, 5, 4]), {'a', 'f'; 'e', 'r'}) %!assert_equal (isordinal (categorical (1)), false) %!assert_equal (isordinal (categorical (1, 'Ordinal', true)), true) %!assert_equal (isordinal (categorical (1, 'Protected', true)), false) %!assert_equal (isordinal (categorical (1:3, 1:3, {'a', 'b', 'c'})), false) %!assert_equal (isordinal (categorical (1:3, 1:3, {'a', 'b', 'c'}, ... %! 'Ordinal', true)), true) %!assert_equal (isordinal (categorical (1:3, 1:3, {'a', 'b', 'c'}, ... %! 'Protected', true)), false) %!assert_equal (isprotected (categorical (1)), false) %!assert_equal (isprotected (categorical (1, 'Ordinal', true)), true) %!assert_equal (isprotected (categorical (1, 'Protected', true)), true) %!assert_equal (isprotected (categorical (1:3, 1:3, {'a', 'b', 'c'})), false) %!assert_equal (isprotected (categorical (1:3, 1:3, {'a', 'b', 'c'}, ... %! 'Ordinal', true)), true) %!assert_equal (isprotected (categorical (1:3, 1:3, {'a', 'b', 'c'}, ... %! 'Protected', true)), true) %!assert_equal (isrow (categorical), false) %!assert_equal (isrow (categorical (NaN)), true) %!assert_equal (isrow (categorical (1)), true) %!assert_equal (isrow (categorical (1:4)), true) %!assert_equal (isrow (categorical (ones (2))), false) %!assert_equal (isrow (categorical (ones (4, 1))), false) %!assert_equal (isrow (categorical (ones (0, 1))), false) %!assert_equal (isrow (categorical (ones (1, 0))), true) %!assert_equal (isscalar (categorical), false) %!assert_equal (isscalar (categorical (NaN)), true) %!assert_equal (isscalar (categorical (1)), true) %!assert_equal (isscalar (categorical (1:4)), false) %!assert_equal (isscalar (categorical (ones (2))), false) %!assert_equal (isscalar (categorical (ones (4, 1))), false) %!assert_equal (isscalar (categorical (ones (0, 1))), false) %!assert_equal (isscalar (categorical (ones (1, 0))), false) %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issorted (sort (C)), true); %! assert_equal (issorted (sort (C), 'ascend'), true); %! assert_equal (issorted (sort (C), 'descend'), false); %! assert_equal (issorted (sort (C), 'monotonic'), true); %! assert_equal (issorted (sort (C), 'strictascend'), false); %! assert_equal (issorted (sort (C), 'strictdescend'), false); %! assert_equal (issorted (sort (C), 'strictmonotonic'), false); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issorted (sort (C, 2), 2), true); %! assert_equal (issorted (sort (C, 2), 2, 'ascend'), true); %! assert_equal (issorted (sort (C, 2), 2, 'descend'), false); %! assert_equal (issorted (sort (C, 2), 2, 'monotonic'), true); %! assert_equal (issorted (sort (C, 2), 2, 'strictascend'), true); %! assert_equal (issorted (sort (C, 2), 2, 'strictdescend'), false); %! assert_equal (issorted (sort (C, 2), 2, 'strictmonotonic'), true); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issorted (sort (C, 2), 'ascend'), false); %! assert_equal (issorted (sort (C, 2), 'descend'), false); %! assert_equal (issorted (sort (C, 2), 'monotonic'), false); %! assert_equal (issorted (sort (C, 2), 'strictascend'), false); %! assert_equal (issorted (sort (C, 2), 'strictdescend'), false); %! assert_equal (issorted (sort (C, 2), 'strictmonotonic'), false); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issorted (sort (C, 'descend'), 'ascend'), false); %! assert_equal (issorted (sort (C, 'descend'), 'descend'), true); %! assert_equal (issorted (sort (C, 'descend'), 'monotonic'), true); %! assert_equal (issorted (sort (C, 'descend'), 'strictascend'), false); %! assert_equal (issorted (sort (C, 'descend'), 'strictdescend'), false); %! assert_equal (issorted (sort (C, 'descend'), 'strictmonotonic'), false); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issorted (sort (C, 2, 'descend'), 2, 'ascend'), false); %! assert_equal (issorted (sort (C, 2, 'descend'), 2, 'descend'), true); %! assert_equal (issorted (sort (C, 2, 'descend'), 2, 'monotonic'), true); %! assert_equal (issorted (sort (C, 2, 'descend'), 2, 'strictascend'), false); %! assert_equal (issorted (sort (C, 2, 'descend'), 2, 'strictdescend'), true); %! assert_equal (issorted (sort (C, 2, 'descend'), 2, 'strictmonotonic'), true); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issorted (sort (C, 2, 'descend'), 'ascend'), false); %! assert_equal (issorted (sort (C, 2, 'descend'), 'descend'), false); %! assert_equal (issorted (sort (C, 2, 'descend'), 'monotonic'), false); %! assert_equal (issorted (sort (C, 2, 'descend'), 'strictascend'), false); %! assert_equal (issorted (sort (C, 2, 'descend'), 'strictdescend'), false); %! assert_equal (issorted (sort (C, 2, 'descend'), 'strictmonotonic'), false); %!test %! C = categorical ([1, 2; 2, 3; 3, 4; 4, 5]); %! assert_equal (issorted (sort (C), 'ascend'), true); %! assert_equal (issorted (sort (C), 'descend'), false); %! assert_equal (issorted (sort (C), 'monotonic'), true); %! assert_equal (issorted (sort (C), 'strictascend'), true); %! assert_equal (issorted (sort (C), 'strictdescend'), false); %! assert_equal (issorted (sort (C), 'strictmonotonic'), true); %!test %! C = categorical ([1, 2; 2, 3; 3, 4; 4, 5]); %! assert_equal (issorted (sort (C, 'descend'), 'ascend'), false); %! assert_equal (issorted (sort (C, 'descend'), 'descend'), true); %! assert_equal (issorted (sort (C, 'descend'), 'monotonic'), true); %! assert_equal (issorted (sort (C, 'descend'), 'strictascend'), false); %! assert_equal (issorted (sort (C, 'descend'), 'strictdescend'), true); %! assert_equal (issorted (sort (C, 'descend'), 'strictmonotonic'), true); %!test %! C = categorical ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (C)), true); %! assert_equal (issorted (sort (C), 'ascend'), true); %! assert_equal (issorted (sort (C), 'descend'), false); %! assert_equal (issorted (sort (C), 'monotonic'), true); %! assert_equal (issorted (sort (C), 'strictascend'), false); %! assert_equal (issorted (sort (C), 'strictdescend'), false); %! assert_equal (issorted (sort (C), 'strictmonotonic'), false); %!test %! C = categorical ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (C, 'descend'), 'ascend'), false); %! assert_equal (issorted (sort (C, 'descend'), 'descend'), true); %! assert_equal (issorted (sort (C, 'descend'), 'monotonic'), true); %! assert_equal (issorted (sort (C, 'descend'), 'strictascend'), false); %! assert_equal (issorted (sort (C, 'descend'), 'strictdescend'), false); %! assert_equal (issorted (sort (C, 'descend'), 'strictmonotonic'), false); %!test %! C = categorical ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (C, 'MissingPlacement', 'auto')), true); %! assert_equal (issorted (sort (C, 'MissingPlacement', 'first')), false); %! assert_equal (issorted (sort (C, 'MissingPlacement', 'first'), ... %! 'MissingPlacement', 'first'), true); %!test %! C = categorical ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (C, 'descend', 'MissingPlacement', 'auto'), ... %! 'descend'), true); %! assert_equal (issorted (sort (C, 'descend', 'MissingPlacement', 'last'), ... %! 'descend'), false); %! assert_equal (issorted (sort (C, 'descend', 'MissingPlacement', 'first'), ... %! 'descend', 'MissingPlacement', 'first'), true); %!test %! C = categorical ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (C, 2, 'MissingPlacement', 'auto'), 2), true); %! assert_equal (issorted (sort (C, 2, 'MissingPlacement', 'first'), 2), false); %! assert_equal (issorted (sort (C, 2, 'MissingPlacement', 'first'), 2, ... %! 'MissingPlacement', 'first'), true); %!test %! C = categorical ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (C, 2, 'descend', 'MissingPlacement', ... %! 'auto'), 2, 'descend'), true); %! assert_equal (issorted (sort (C, 2, 'descend', 'MissingPlacement', ... %! 'last'), 2, 'descend'), false); %! assert_equal (issorted (sort (C, 2, 'descend', 'MissingPlacement', ... %! 'first'), 2, ... %! 'descend', 'MissingPlacement', 'first'), true); %!test %! C = categorical ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (C, 2, string ('ascend'), ... %! 'MissingPlacement', 'auto'), 2), true); %! assert_equal (issorted (sort (C, 2, string ('ascend'), ... %! 'MissingPlacement', 'first'), 2), false); %! assert_equal (issorted (sort (C, 2, string ('ascend'), ... %! 'MissingPlacement', 'first'), 2, ... %! 'MissingPlacement', 'first'), true); %!test %! C = categorical ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (C, 2, string ('descend'), ... %! 'MissingPlacement', 'auto'), 2, 'descend'), true); %! assert_equal (issorted (sort (C, 2, string ('descend'), ... %! 'MissingPlacement', 'last'), 2, 'descend'), false); %! assert_equal (issorted (sort (C, 2, string ('descend'), ... %! 'MissingPlacement', 'first'), 2, ... %! 'descend', 'MissingPlacement', 'first'), true); %!test ## strictmonotonic honours an explicit MissingPlacement argument %! assert_equal (issorted (categorical ([3, 2, 1]), 'strictmonotonic', ... %! 'MissingPlacement', 'last'), true); %! assert_equal (issorted (categorical ([3, 2, 1]), 'strictmonotonic', ... %! 'MissingPlacement', 'first'), true); %! assert_equal (issorted (categorical ([1, 1, 2]), 'strictmonotonic', ... %! 'MissingPlacement', 'last'), false); %!error ... %! issorted (sort (categorical), 'MissingPlacement', 'some') %!error ... %! issorted (sort (categorical), 'asd') %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issortedrows (sortrows (C)), true); %! assert_equal (issortedrows (sortrows (C, 1)), true); %! assert_equal (issortedrows (sortrows (C, 1), -1), false); %! assert_equal (issortedrows (sortrows (C, 1), 1), true); %! assert_equal (issortedrows (sortrows (C, 2)), false); %! assert_equal (issortedrows (sortrows (C, 2), 2), true); %! assert_equal (issortedrows (sortrows (C, 2), -2), false); %! assert_equal (issortedrows (sortrows (C, -2), 2), false); %! assert_equal (issortedrows (sortrows (C, -2), -2), true); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! assert_equal (issortedrows (sortrows (C)), true); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! assert_equal (issortedrows (sortrows (C, -2, 'MissingPlacement', ... %! 'first')), false); %! assert_equal (issortedrows (sortrows (C, -2, 'MissingPlacement', ... %! 'first'), -2), true); %! assert_equal (issortedrows (sortrows (C, -2, 'MissingPlacement', 'last'), ... %! -2), false); %! assert_equal (issortedrows (sortrows (C, -2, 'MissingPlacement', 'last'), ... %! -2, ... %! 'MissingPlacement', 'last'), true); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! assert_equal (issortedrows (sortrows (C, 2, 'MissingPlacement', 'last')), ... %! false); %! assert_equal (issortedrows (sortrows (C, 2, 'MissingPlacement', 'last'), ... %! 2), true); %! assert_equal (issortedrows (sortrows (C, 2, 'MissingPlacement', 'first'), ... %! 2), false); %! assert_equal (issortedrows (sortrows (C, 2, 'MissingPlacement', 'first'), ... %! 2, ... %! 'MissingPlacement', 'first'), true); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! assert_equal (issortedrows (sortrows (C, [1, -2, 3]), [1, -2, 3]), true); %! assert_equal (issortedrows (sortrows (C, [1, 2, -3]), [1, -2, 3]), false); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! assert_equal (issortedrows (sortrows (C, string ({'ascend', 'descend', ... %! 'ascend'})), ... %! string ({'ascend', 'descend', 'ascend'})), true); %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! assert_equal (issortedrows (sortrows (C, string ({'ascend', 'descend', ... %! 'ascend'})), ... %! string ({'descend', 'ascend', 'ascend'})), false); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! assert_equal (issortedrows (sortrows (C, [2, 3], string ({'descend', ... %! 'ascend'})), ... %! [2, 3], string ({'descend', 'ascend'})), true); %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! assert_equal (issortedrows (sortrows (C, [2, 3], string ({'ascend', ... %! 'descend'})), ... %! [2, -3]), true); %! assert_equal (issortedrows (sortrows (C, [2, 3], string ({'descend', ... %! 'descend'})), ... %! [2, -3], string ({'descend', 'ascend'})), false); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! assert_equal (issortedrows (sortrows (C, [1, -2, 3], 'MissingPlacement', ... %! 'first'), ... %! [1, -2, 3], 'MissingPlacement', 'first'), true); %! assert_equal (issortedrows (sortrows (C, [-1, -2, -3], ... %! 'MissingPlacement', 'first'), ... %! [-1, -2, -3]), true); %! assert_equal (issortedrows (sortrows (C, [-1, -2, -3], ... %! 'MissingPlacement', 'first'), ... %! [1, -2,-3], string ({'descend', 'descend', 'descend'})), true); %!test ## strict issortedrows semantics, verified against MATLAB R2026a %! assert_equal (issortedrows (categorical ([1, 3; 1, 4]), 'strictascend'), ... %! false); %! assert_equal (issortedrows (categorical ([1, 3; 1, 3]), 'strictascend'), ... %! false); %! assert_equal (issortedrows (categorical ([1, 3; 1, 4]), ... %! {'strictascend', 'ascend'}), false); %! assert_equal (issortedrows (categorical ([1, 3; 2, 3]), ... %! {'ascend', 'strictascend'}), true); %! assert_equal (issortedrows (categorical ([2, 4; 1, 3]), 'monotonic'), true); %! assert_equal (issortedrows (categorical ([2, 4; 1, 3]), 'descend'), true); %! assert_equal (issortedrows (categorical ([1, 4; 2, 3]), 2, ... %! 'strictascend'), false); %! assert_equal (issortedrows (categorical ([2, 3; 1, 4]), 2, ... %! 'strictascend'), true); %!test ## strict issortedrows with missing values (vs MATLAB R2026a) %! assert_equal (issortedrows (categorical ([1, 3; NaN, 4]), ... %! {'strictascend', 'ascend'}), false); %! assert_equal (issortedrows (categorical ([1, 3; NaN, 4]), ... %! {'ascend', 'strictascend'}), true); %! assert_equal (issortedrows (categorical ([NaN, 3; 2, 4]), 2, ... %! 'strictascend'), true); %! assert_equal (issortedrows (categorical ([1, NaN; 2, 4]), ... %! 'strictascend'), true); %!assert_equal (isundefined (categorical ([1:10])), false (1, 10)) %!assert_equal (isundefined (categorical ([1, 2, NaN, 4, ... %! NaN])), logical ([0, 0, 1, 0, 1])) %!assert_equal (isundefined (categorical (NaN (3, 4, 5))), true (3, 4, 5)) %!assert_equal (isundefined (categorical (zeros (3, 4, 5))), false (3, 4, 5)) %!assert_equal (isvector (categorical ([1, 2, 3, 4])), true) %!assert_equal (isvector (categorical ([1; 2; 3; 4])), true) %!assert_equal (isvector (categorical ([1, 2; 3, 4])), false) %!assert_equal (isvector (categorical ([])), false) %!assert_equal (isvector (categorical (ones (1, 0))), true) %!assert_equal (isvector (categorical (ones (0, 1))), true) %!assert_equal (isvector (categorical (1)), true) %!assert_equal (isvector (categorical (ones (2, 3, 4))), false) ################################################################################ ## ** Category Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'addcats' 'mergecats' 'removecats' 'renamecats' ## ## 'reordercats' 'setcats' 'times' ## ## ## ################################################################################ %!test %! C = categorical ({'red', 'blue', 'red', 'blue', 'red', 'blue'}); %! B = addcats (C, {'green', 'black'}); %! assert_equal (C, B); %! assert_equal (categories (B), {'blue'; 'red'; 'green'; 'black'}); %! assert_equal (categories (C), {'blue'; 'red'}); %!test %! C = categorical ({'red', 'blue', 'red', 'blue', 'red', 'blue'}); %! B = addcats (C, string ({'green', 'black'})); %! assert_equal (C, B); %! assert_equal (categories (B), {'blue'; 'red'; 'green'; 'black'}); %! assert_equal (categories (C), {'blue'; 'red'}); %!test %! S = {'medium', 'large', 'small', 'xlarge', 'large', 'medium'}; %! C = categorical (S, {'small', 'medium', 'large', 'xlarge'}, 'Ordinal', true); %! B = addcats (C, 'xsmall', 'Before', 'small'); %! cats = categories (B); %! assert_equal (cats, {'xsmall'; 'small'; 'medium'; 'large'; 'xlarge'}); %! assert_equal (double (B), [3, 4, 2, 5, 4, 3]); %! assert_equal (cats (double (B)), S(:)); %!test %! S = {'medium', 'large', 'small', 'xlarge', 'large', 'medium'}; %! C = categorical (S, {'small', 'medium', 'large', 'xlarge'}, 'Ordinal', true); %! B = addcats (C, 'xsmall', 'Before', 'medium'); %! cats = categories (B); %! assert_equal (cats, {'small'; 'xsmall'; 'medium'; 'large'; 'xlarge'}); %! assert_equal (double (B), [3, 4, 1, 5, 4, 3]); %! assert_equal (cats (double (B)), S(:)); %!test %! S = {'medium', 'large', 'small', 'xlarge', 'large', 'medium'}; %! C = categorical (S, {'small', 'medium', 'large', 'xlarge'}, 'Ordinal', true); %! B = addcats (C, 'xsmall', 'Before', 'large'); %! cats = categories (B); %! assert_equal (cats, {'small'; 'medium'; 'xsmall'; 'large'; 'xlarge'}); %! assert_equal (double (B), [2, 4, 1, 5, 4, 2]); %! assert_equal (cats (double (B)), S(:)); %!test %! S = {'medium', 'large', 'small', 'xlarge', 'large', 'medium'}; %! C = categorical (S, {'small', 'medium', 'large', 'xlarge'}, 'Ordinal', true); %! B = addcats (C, 'xsmall', 'Before', 'xlarge'); %! cats = categories (B); %! assert_equal (cats, {'small'; 'medium'; 'large'; 'xsmall'; 'xlarge'}); %! assert_equal (double (B), [2, 3, 1, 5, 3, 2]); %! assert_equal (cats (double (B)), S(:)); %!test %! S = {'medium', 'large', 'small', 'xlarge', 'large', 'medium'}; %! C = categorical (S, {'small', 'medium', 'large', 'xlarge'}, 'Ordinal', true); %! B = addcats (C, {'xxsmall', 'xsmall'}, 'Before', 'small'); %! cats = categories (B); %! assert_equal (cats, ... %! {'xxsmall'; 'xsmall'; 'small'; 'medium'; 'large'; 'xlarge'}); %! assert_equal (double (B), [4, 5, 3, 6, 5, 4]); %! assert_equal (cats (double (B)), S(:)); %!test %! S = {'medium', 'large', 'small', 'xlarge', 'large', 'medium'}; %! C = categorical (S, {'small', 'medium', 'large', 'xlarge'}, 'Ordinal', true); %! B = addcats (C, {'xxsmall', 'xsmall'}, 'Before', 'xlarge'); %! cats = categories (B); %! assert_equal (cats, ... %! {'small'; 'medium'; 'large'; 'xxsmall'; 'xsmall'; 'xlarge'}); %! assert_equal (double (B), [2, 3, 1, 6, 3, 2]); %! assert_equal (cats (double (B)), S(:)); %!test %! S = {'medium', 'large', 'small', 'xsmall', 'large', 'medium'}; %! C = categorical (S, {'xsmall', 'small', 'medium', 'large'}, 'Ordinal', true); %! B = addcats (C, 'xlarge', 'After', 'large'); %! cats = categories (B); %! assert_equal (cats, {'xsmall'; 'small'; 'medium'; 'large'; 'xlarge'}); %! assert_equal (double (B), [3, 4, 2, 1, 4, 3]); %! assert_equal (cats (double (B)), S(:)); %!test %! S = {'medium', 'large', 'small', 'xsmall', 'large', 'medium'}; %! C = categorical (S, {'xsmall', 'small', 'medium', 'large'}, 'Ordinal', true); %! B = addcats (C, 'xlarge', 'After', 'medium'); %! cats = categories (B); %! assert_equal (cats, {'xsmall'; 'small'; 'medium'; 'xlarge'; 'large'}); %! assert_equal (double (B), [3, 5, 2, 1, 5, 3]); %! assert_equal (cats (double (B)), S(:)); %!test %! S = {'medium', 'large', 'small', 'xsmall', 'large', 'medium'}; %! C = categorical (S, {'xsmall', 'small', 'medium', 'large'}, 'Ordinal', true); %! B = addcats (C, 'xlarge', 'After', 'small'); %! cats = categories (B); %! assert_equal (cats, {'xsmall'; 'small'; 'xlarge'; 'medium'; 'large'}); %! assert_equal (double (B), [4, 5, 2, 1, 5, 4]); %! assert_equal (cats (double (B)), S(:)); %!test %! S = {'medium', 'large', 'small', 'xsmall', 'large', 'medium'}; %! C = categorical (S, {'xsmall', 'small', 'medium', 'large'}, 'Ordinal', true); %! B = addcats (C, 'xlarge', 'After', 'xsmall'); %! cats = categories (B); %! assert_equal (cats, {'xsmall'; 'xlarge'; 'small'; 'medium'; 'large'}); %! assert_equal (double (B), [4, 5, 3, 1, 5, 4]); %! assert_equal (cats (double (B)), S(:)); %!test %! S = {'medium', 'large', 'small', 'xsmall', 'large', 'medium'}; %! C = categorical (S, {'xsmall', 'small', 'medium', 'large'}, 'Ordinal', true); %! B = addcats (C, {'xlarge', 'xxlarge'}, 'After', 'large'); %! cats = categories (B); %! assert_equal (cats, ... %! {'xsmall'; 'small'; 'medium'; 'large'; 'xlarge'; 'xxlarge'}); %! assert_equal (double (B), [3, 4, 2, 1, 4, 3]); %! assert_equal (cats (double (B)), S(:)); %!test %! S = {'medium', 'large', 'small', 'xsmall', 'large', 'medium'}; %! C = categorical (S, {'xsmall', 'small', 'medium', 'large'}, 'Ordinal', true); %! B = addcats (C, {'xlarge', 'xxlarge'}, 'After', 'xsmall'); %! cats = categories (B); %! assert_equal (cats, ... %! {'xsmall'; 'xlarge'; 'xxlarge'; 'small'; 'medium'; 'large'}); %! assert_equal (double (B), [5, 6, 4, 1, 6, 5]); %! assert_equal (cats (double (B)), S(:)); %!error addcats (categorical) %!error addcats (categorical, '') %!error addcats (categorical, []) %!error addcats (categorical, {}) %!error ... %! addcats (categorical, 1) %!error ... %! addcats (categorical, false) %!error ... %! addcats (categorical ({'a', 'b'}), 'c', 'After') %!error ... %! addcats (categorical, struct ('a', 2)) %!error ... %! addcats (categorical, {'a', 'a'}) %!error ... %! addcats (categorical ({'a', 'b', 'a'}), {'a', 'c'}) %!error ... %! addcats (categorical ({'a', 'b', 'a'}), 'c', 'After', 'a', 'Before', 'b') %!error ... %! addcats (categorical ({'a', 'b'}), 'c', 'Bogus', 1) %!error ... %! addcats (categorical ({'a', 'b', 'a'}), 'c', 'After', 'd') %!error ... %! addcats (categorical ({'a', 'b', 'a'}), 'c', 'Before', 'c') %!error ... %! mergecats (categorical, {'a'}, 1) %!error ... %! mergecats (categorical, {'a'}, []) %!error ... %! mergecats (categorical, {'a'}, {'w', 'e'}) %!error ... %! mergecats (categorical, {'a'}, ['w'; 'e']) %!error ... %! mergecats (categorical, {'a'}, string ({'w', 'e'})) %!error ... %! mergecats (categorical, {'a'}, missing) %!error ... %! mergecats (categorical, {'a'}, '') %!error ... %! mergecats (categorical, {'a'}, {''}) %!error ... %! mergecats (categorical, {'a'}, string ({''})) %!error ... %! mergecats (categorical, {'a'}, string (missing)) %!test %! A = categorical ({'pink', 'blue', 'pink', 'red', 'blue', 'red'}); %! B = mergecats (A, {'red', 'pink'}); %! assert_equal (B, categorical ({'red', 'blue', 'red', 'red', 'blue', 'red'})); %! assert_equal (categories (B), {'blue'; 'red'}); %! assert_equal (double (B), [2, 1, 2, 2, 1, 2]); %!test %! A = categorical ({'pink', 'blue', 'pink', 'red', 'blue', 'red'}); %! B = mergecats (A, {'pink', 'red'}); %! assert_equal (B, categorical ({'pink', 'blue', 'pink', 'pink', 'blue', ... %! 'pink'})); %! assert_equal (categories (B), {'blue'; 'pink'}); %! assert_equal (double (B), [2, 1, 2, 2, 1, 2]); %!test %! A = categorical ({'pink', 'blue', 'pink', 'red', 'blue', 'red'}); %! B = mergecats (A, {'pink', 'blue'}); %! assert_equal (B, categorical ({'pink', 'pink', 'pink', 'red', 'pink', ... %! 'red'})); %! assert_equal (categories (B), {'pink'; 'red'}); %! assert_equal (double (B), [1, 1, 1, 2, 1, 2]); %!test %! A = categorical ({'pink', 'blue', 'pink', 'red', 'blue', 'red'}); %! B = mergecats (A, {'red', 'pink'}, 'gold'); %! assert_equal (B, categorical ({'gold', 'blue', 'gold', 'gold', 'blue', ... %! 'gold'})); %! assert_equal (categories (B), {'blue'; 'gold'}); %! assert_equal (double (B), [2, 1, 2, 2, 1, 2]); %!test %! A = categorical ({'pink', 'blue', 'pink', 'red', 'blue', 'red'}); %! B = mergecats (A, string ({'pink', 'red'}), 'gold'); %! assert_equal (B, categorical ({'gold', 'blue', 'gold', 'gold', 'blue', ... %! 'gold'})); %! assert_equal (categories (B), {'blue'; 'gold'}); %! assert_equal (double (B), [2, 1, 2, 2, 1, 2]); %!test %! A = categorical ({'pink', 'blue', 'pink', 'red', 'blue', 'red'}); %! B = mergecats (A, {'pink', 'blue'}, string ('gold')); %! assert_equal (B, categorical ({'gold', 'gold', 'gold', 'red', 'gold', ... %! 'red'})); %! assert_equal (categories (B), {'gold'; 'red'}); %! assert_equal (double (B), [1, 1, 1, 2, 1, 2]); %!test %! A = categorical ({'shirt', 'pants', 'shoes', 'shirt', 'dress', 'belt'}); %! assert_equal (categories (A), {'belt'; 'dress'; 'pants'; 'shirt'; 'shoes'}); %! B = mergecats (A, {'belt', 'shoes'}, 'hat'); %! assert_equal (categories (B), {'hat'; 'dress'; 'pants'; 'shirt'}); %! assert_equal (double (B), [4, 3, 1, 4, 2, 1]); %!test %! A = categorical ({'shirt', 'pants', 'shoes', 'shirt', 'dress', 'belt'}); %! assert_equal (categories (A), {'belt'; 'dress'; 'pants'; 'shirt'; 'shoes'}); %! B = mergecats (A, {'dress', 'shoes'}, 'hat'); %! assert_equal (categories (B), {'belt'; 'hat'; 'pants'; 'shirt'}); %! assert_equal (double (B), [4, 3, 2, 4, 2, 1]); %!test %! A = categorical ({'shirt', 'pants', 'shoes', 'shirt', 'dress', 'belt'}); %! assert_equal (categories (A), {'belt'; 'dress'; 'pants'; 'shirt'; 'shoes'}); %! B = mergecats (A, {'dress', 'shirt'}, 'hat'); %! assert_equal (categories (B), {'belt'; 'hat'; 'pants'; 'shoes'}); %! assert_equal (double (B), [2, 3, 4, 2, 2, 1]); %!test %! A = categorical ({'shirt', 'pants', 'shoes', 'shirt', 'dress', 'belt'}); %! assert_equal (categories (A), {'belt'; 'dress'; 'pants'; 'shirt'; 'shoes'}); %! B = mergecats (A, {'belt', 'shirt', 'shoes'}, 'hat'); %! assert_equal (categories (B), {'hat'; 'dress'; 'pants'}); %! assert_equal (double (B), [1, 3, 1, 1, 2, 1]); %!test %! A = categorical ({'shirt', 'pants', 'shoes', 'shirt', 'dress', 'belt'}); %! B = mergecats (A, {'dresss', 'shoes'}, 'hat'); %! assert_equal (categories (B), {'belt'; 'dress'; 'pants'; 'shirt'; 'hat'}); %! assert_equal (double (B), [4, 3, 5, 4, 2, 1]); %!test %! A = categorical ({'shirt', 'pants', 'shoes', 'shirt', 'dress', 'belt'}); %! B = mergecats (A, {'dresss', 'shirt'}, 'hat'); %! assert_equal (categories (B), {'belt'; 'dress'; 'pants'; 'hat'; 'shoes'}); %! assert_equal (double (B), [4, 3, 5, 4, 2, 1]); %!test %! A = categorical ({'shirt', 'pants', 'shoes', 'shirt', 'dress', 'belt', ''}); %! B = mergecats (A, {'belt', 'shoes'}, 'other'); %! cats = categories (B); %! assert_equal (cats, {'other'; 'dress'; 'pants'; 'shirt'}); %! codes = double (B); %! assert_equal (codes, [4, 3, 1, 4, 2, 1, NaN]); %! S = {'shirt'; 'pants'; 'other'; 'shirt'; 'dress'; 'other'}; %! assert_equal (cats(codes(! isnan (codes))), S); %!test %! A = categorical ([1, 2, 3, 2, 1], 1:3, {'poor', 'fair', 'good'}, ... %! 'Ordinal', true); %! B = mergecats (A, {'fair', 'poor'}, "bad"); %! assert_equal (categories (B), {'bad'; 'good'}); %! assert_equal (double (B), [1, 1, 2, 1, 1]); %!error mergecats (categorical) %!error mergecats (categorical, '') %!error mergecats (categorical, []) %!error mergecats (categorical, {}) %!error ... %! mergecats (categorical, 1) %!error ... %! mergecats (categorical, false) %!error ... %! mergecats (categorical ({'a'}), 'a') %!error ... %! mergecats (categorical, struct ('a', 2)) %!test %! A = categorical ({'red', 'blue', 'blue', 'blue', 'red', 'blue'}, ... %! {'black', 'blue', 'red', 'green'}); %! assert_equal (categories (A), {'black'; 'blue'; 'red'; 'green'}); %! B = removecats (A); %! assert_equal (categories (B), {'blue'; 'red'}); %! assert_equal (A, B); %!test %! A = categorical ({'red', 'blue', 'blue', 'blue', 'red', 'blue'}, ... %! {'black', 'blue', 'red', 'green'}); %! B = removecats (A, cell (0, 1)); %! assert_equal (categories (B), {'black'; 'blue'; 'red'; 'green'}); %! assert_equal (A, B); %!test %! A = categorical ({'red', 'blue', 'blue', 'blue', 'red', 'blue'}, ... %! {'black', 'blue', 'red', 'green'}); %! B = removecats (A, {}); %! assert_equal (categories (B), {'black'; 'blue'; 'red'; 'green'}); %! assert_equal (A, B); %!test %! A = categorical ({'plane', 'car', 'train', 'car', 'plane', 'car'}); %! B = removecats (A, 'car'); %! assert_equal (categories (B), {'plane'; 'train'}); %! assert_equal (double (B), [1, NaN, 2, NaN, 1, NaN]); %!test %! A = categorical ({'plane', 'car', 'train', 'car', 'plane', 'car'}); %! B = removecats (A, string ('plane')); %! assert_equal (categories (B), {'car'; 'train'}); %! assert_equal (double (B), [NaN, 1, 2, 1, NaN, 1]); %!test %! A = categorical ({'plane', 'car', 'train', 'car', 'plane', 'car'}); %! B = removecats (A, {'car', 'train'}); %! assert_equal (categories (B), {'plane'}); %! assert_equal (double (B), [1, NaN, NaN, NaN, 1, NaN]); %!error ... %! removecats (categorical, []) %!error ... %! removecats (categorical, '') %!error ... %! removecats (categorical, 1) %!error ... %! removecats (categorical, true) %!error ... %! removecats (categorical, struct ('a', 1)) %!test %! A = categorical ({'purple', 'blue', 'purple', 'red', 'red', 'blue'}); %! B = renamecats (A, {'b', 'p', 'r'}); %! cats = categories (B); %! assert_equal (cats, {'b'; 'p'; 'r'}); %! assert_equal (cats(double (B)), {'p'; 'b'; 'p'; 'r'; 'r'; 'b'}); %!test %! A = categorical ({'purple', 'blue', 'purple', 'red', 'red', 'blue'}); %! B = renamecats (A, string ({'b', 'p', 'r'})); %! cats = categories (B); %! assert_equal (cats, {'b'; 'p'; 'r'}); %! assert_equal (cats(double (B)), {'p'; 'b'; 'p'; 'r'; 'r'; 'b'}); %!test %! A = categorical ({'purple', 'blue', 'purple', 'red', 'red', 'blue'}); %! B = renamecats (A, 'purple', 'gold'); %! cats = categories (B); %! assert_equal (cats, {'blue'; 'gold'; 'red'}); %! assert_equal (cats(double (B)), ... %! {'gold'; 'blue'; 'gold'; 'red'; 'red'; 'blue'}); %!test %! A = categorical (); %! B = renamecats (A, {}); %! assert_equal (A, B); %!test %! A = categorical ({'purple', 'blue', 'purple', 'red', 'red', 'blue'}); %! B = renamecats (A, {}, {}); %! assert_equal (A, B); %!error ... %! renamecats (categorical) %!error ... %! renamecats (categorical, '') %!error ... %! renamecats (categorical, 1) %!error ... %! renamecats (categorical, true) %!error ... %! renamecats (categorical, struct ('a', 'b')) %!error ... %! renamecats (categorical, string (missing)) %!error ... %! renamecats (categorical ({'a'}), '', 'a') %!error ... %! renamecats (categorical ({'a'}), 1, 'a') %!error ... %! renamecats (categorical ({'a'}), true, 'a') %!error ... %! renamecats (categorical ({'a'}), struct ('a', 'a'), 'a') %!error ... %! renamecats (categorical ({'a'}), 'a', '') %!error ... %! renamecats (categorical ({'a'}), 'a', 1) %!error ... %! renamecats (categorical ({'a'}), 'a', true) %!error ... %! renamecats (categorical ({'a'}), 'a', struct ('a', 'a')) %!error ... %! renamecats (categorical ({'a'}), 'a', {}) %!error ... %! renamecats (categorical ({'a'}), {}, 'a') %!error ... %! renamecats (categorical ({'a'}), 'b', 'a') %!test %! X = categorical ({'Frog', 'Cat', 'Cat', 'Ant', 'Frog'}); %! Xcats = categories (X); %! assert_equal (Xcats, {'Ant'; 'Cat'; 'Frog'}); %! Y = categorical ({'Deer', 'Bear', 'Eagle', 'Deer'}); %! Ycats = categories (Y); %! assert_equal (Ycats, {'Bear'; 'Deer'; 'Eagle'}); %! A = [X, Y]; %! Acats = categories (A); %! S = {'Frog'; 'Cat'; 'Cat'; 'Ant'; 'Frog'; 'Deer'; 'Bear'; 'Eagle'; 'Deer'}; %! assert_equal (Acats, {'Ant'; 'Cat'; 'Frog'; 'Bear'; 'Deer'; 'Eagle'}); %! assert_equal (Acats(double (A)), S); %! B = reordercats (A); %! Bcats = categories (B); %! assert_equal (Bcats, {'Ant'; 'Bear'; 'Cat'; 'Deer'; 'Eagle'; 'Frog'}); %! assert_equal (Bcats(double (B)), S); %!test %! A = categorical ({'red', 'green', 'blue', 'red', 'green', 'red', 'blue', ... %! 'blue'}); %! assert_equal (categories (A), {'blue'; 'green'; 'red'}); %! B = reordercats (A, string ({'red', 'green', 'blue'})); %! assert_equal (categories (B), {'red'; 'green'; 'blue'}); %!test %! A = categorical ({'plane', 'car', 'train', 'car', 'plane', 'car'}, ... %! {'car', 'train', 'plane'}, 'Ordinal', true); %! assert_equal (categories (A), {'car'; 'train'; 'plane'}); %! A_out = categorical ({'plane', 'train', 'plane'}, ... %! {'car', 'train', 'plane'}, 'Ordinal', true); %! assert_equal (A(A > "car"), A_out); %! B = reordercats (A, {'train', 'car', 'plane'}); %! assert_equal (categories (B), {'train'; 'car'; 'plane'}); %! B_out = categorical ({'plane', 'plane'}, ... %! {'train', 'car', 'plane'}, 'Ordinal', true); %! assert_equal (B(B > "car"), B_out); %!test %! A = categorical ({'plane', 'car', 'train', 'car', 'car', 'plane', 'car'}); %! assert_equal (categories (A), {'car'; 'plane'; 'train'}); %! assert_equal (countcats (A), [4, 2, 1]); %! [~, neworder] = sort (countcats (A)); %! B = reordercats (A, neworder); %! assert_equal (categories (B), {'train'; 'plane'; 'car'}); %! assert_equal (A, B); %!error ... %! reordercats (categorical, 1.5) %!error ... %! reordercats (categorical, [0, 1, 2]) %!error ... %! reordercats (categorical, []) %!error ... %! reordercats (categorical ({'a', 'b', 'c'}), [1, 2, 2]) %!error ... %! reordercats (categorical ({'a', 'b', 'c'}), [1, 2, 4]) %!error ... %! reordercats (categorical, '') %!error ... %! reordercats (categorical, true) %!error ... %! reordercats (categorical, 'a') %!error ... %! reordercats (categorical, struct ('a', 1)) %!error ... %! reordercats (categorical ({'a', 'b', 'c'}), {'a', 'd', 'c'}) %!test %! A = categorical ({'blue', 'black', 'red', 'red', 'blue', 'black', ... %! 'black', 'red', 'blue'}); %! B = setcats (A, string ({'red', 'black'})); %! assert_equal (categories (B), {'red'; 'black'}); %! Bout = categorical ({'', 'black', 'red', 'red', '', 'black', 'black', ... %! 'red', ''}, ... %! {'red', 'black'}); %! assert_equal (isequaln (B, Bout), true); %!test %! A = categorical ({'blue', 'black', 'red', 'red', 'blue', 'black', ... %! 'black', 'red', 'blue'}); %! B = setcats (A, {'red', 'pink', 'blue'}); %! assert_equal (categories (B), {'red'; 'pink'; 'blue'}); %! Bout = categorical ({'blue', '', 'red', 'red', 'blue', '', '', 'red', ... %! 'blue'}, ... %! {'red', 'pink', 'blue'}); %! assert_equal (isequaln (B, Bout), true); %!test %! A = setcats (categorical, {}); %! assert_equal (categorical, categorical); %! assert_equal (isempty (A), true); %! assert_equal (categories (A), {}); %!test %! A = setcats (categorical ({'a', 'b', 'b'}), {}); %! assert_equal (double (A), [NaN, NaN, NaN]); %! assert_equal (categories (A), {}); %!test %! A = setcats (categorical ({'a','b'}, 'Protected', true), {'e','a'}); %! assert_equal (categories (A), {'e'; 'a'}); %! assert_equal (double (A), [2, NaN]); %! assert_equal (isprotected (A), true); %!error setcats (categorical) %!error setcats (categorical, []) %!error setcats (categorical, '') %!error ... %! setcats (categorical, 1) %!error ... %! setcats (categorical, true) %!error ... %! setcats (categorical, struct ('a', 1)) %!test %! colors = categorical ({'blue', 'red', 'green', 'black'}); %! signs = categorical ({'+', '-', '-', '+'}); %! C = colors .* signs; %! cats = categories (C); %! assert_equal (cats, {'black +'; 'black -'; 'blue +' ; 'blue -'; ... %! 'green +'; 'green -'; 'red +'; 'red -'}); %! C_out = categorical ({'blue +', 'red -', 'green -', 'black +'}, cats); %! assert_equal (C, C_out); %!error times (categorical) %!error ... %! categorical .* 1 %!error ... %! categorical .* string ('a') %!error ... %! 'cat' .* categorical %!error ... %! {'cat'} .* categorical ################################################################################ ## ** Relational Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'eq' 'ge' 'gt' 'le' ## ## 'lt' 'ne' ## ## ## ################################################################################ %!assert_equal (categorical ({'a'}) == 'a', true) %!assert_equal (string ('1') == categorical ({'a'}), false) %!assert_equal (categorical ({'a', ''}) == {'a'}, [true, false]) %!test %! A = categorical ({'a', 'b', 'c'}); %! B = categorical ({'a'; 'b'; 'c'}); %! assert_equal (A == B, logical (eye (3))); %!test %! A = categorical ({'a', 'b', 'c'}); %! B = categorical ({'a'; 'b'; 'c'}); %! B = repmat (B, 1, 3); %! assert_equal (A == B, logical (eye (3))); %!test %! X = categorical ([1, 2, NaN], [1, 2, 3], 'Ordinal', true); %! assert_equal (X == X, [true, true, false]); %! assert_equal (X == '1', [true, false, false]); %!error ... %! categorical == {'a', 'b'} %!error ... %! categorical ({'e', 'r'}) == string ({'e', 'r'}) %!error ... %! string ({'e', 'r'}) == categorical ({'e', 'r'}) %!error ... %! {'a', 'b'} == categorical %!error ... %! categorical ({'e', 'r'}, {'e', 'r'}, 'Ordinal', ... %! true) == categorical ({'e', 'r'}, {'e'}, 'Ordinal', true) %!error ... %! categorical ({'e', 'r'}, {'e', 'r'}, 'Ordinal', ... %! true) == categorical ({'e', 'r'}) %!error ... %! categorical ([1, 2, 3]) == categorical ([1, 2, 3, 4]) %!error ... %! categorical ([1, 2, 3]) == 1 %!error ... %! 1 == categorical ([1, 2, 3]) %!test %! X = categorical ([1, 2, NaN], [1, 2, 3], 'Ordinal', true); %! assert_equal (X >= '2', [false, true, false]); %! assert_equal (X >= '1', [true, true, false]); %!test %! X = categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true); %! assert_equal (X >= {'2'}, [false, true, true]); %! assert_equal (X >= string ('1'), [true, true, true]); %!test %! X = categorical ([1, 2, NaN], [1, 2, 3], 'Ordinal', true); %! assert_equal ('2' >= X, [true, true, false]); %! assert_equal ('1' >= X, [true, false, false]); %!test %! X = categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true); %! assert_equal ({'2'} >= X, [true, true, false]); %! assert_equal (string ('1') >= X, [true, false, false]); %!test %! X = categorical ([1, 2, 4], [1, 2, 3], 'Ordinal', true); %! assert_equal (X >= X', logical ([1, 1, 0; 0, 1, 0; 0, 0, 0])); %!test %! X = categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true); %! assert_equal (X >= X', logical ([1, 1, 1; 0, 1, 1; 0, 0, 1])); %!error categorical >= '1' %!error {'1'} >= categorical %!error ... %! categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) >= {'1', '2'} %!error ... %! string ({'1', '2'}) >= categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) %!error ... %! categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) >= {'4'} %!error ... %! '4' >= categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) %!error ... %! categorical >= 1 %!error ... %! 1 >= categorical %!error ... %! categorical >= categorical %!test %! X = categorical ([1, 2, NaN], [1, 2, 3], 'Ordinal', true); %! assert_equal (X > '2', [false, false, false]); %! assert_equal (X > '1', [false, true, false]); %!test %! X = categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true); %! assert_equal (X > {'2'}, [false, false, true]); %! assert_equal (X > string ('1'), [false, true, true]); %!test %! X = categorical ([1, 2, NaN], [1, 2, 3], 'Ordinal', true); %! assert_equal ('2' > X, [true, false, false]); %! assert_equal ('1' > X, [false, false, false]); %!test %! X = categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true); %! assert_equal ({'2'} > X, [true, false, false]); %! assert_equal (string ('1') > X, [false, false, false]); %!test %! X = categorical ([1, 2, 4], [1, 2, 3], 'Ordinal', true); %! assert_equal (X > X', logical ([0, 1, 0; 0, 0, 0; 0, 0, 0])); %!test %! X = categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true); %! assert_equal (X > X', logical ([0, 1, 1; 0, 0, 1; 0, 0, 0])); %!error categorical > '1' %!error {'1'} > categorical %!error ... %! categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) > {'1', '2'} %!error ... %! string ({'1', '2'}) > categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) %!error ... %! categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) > {'4'} %!error ... %! '4' > categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) %!error ... %! categorical > 1 %!error ... %! 1 > categorical %!error ... %! categorical > categorical %!test %! X = categorical ([1, 2, NaN], [1, 2, 3], 'Ordinal', true); %! assert_equal (X <= '2', [true, true, false]); %! assert_equal (X <= '1', [true, false, false]); %!test %! X = categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true); %! assert_equal (X <= {'2'}, [true, true, false]); %! assert_equal (X <= string ('1'), [true, false, false]); %!test %! X = categorical ([1, 2, NaN], [1, 2, 3], 'Ordinal', true); %! assert_equal ('2' <= X, [false, true, false]); %! assert_equal ('1' <= X, [true, true, false]); %!test %! X = categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true); %! assert_equal ({'2'} <= X, [false, true, true]); %! assert_equal (string ('1') <= X, [true, true, true]); %!test %! X = categorical ([1, 2, 4], [1, 2, 3], 'Ordinal', true); %! assert_equal (X <= X', logical ([1, 0, 0; 1, 1, 0; 0, 0, 0])); %!test %! X = categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true); %! assert_equal (X <= X', logical ([1, 0, 0; 1, 1, 0; 1, 1, 1])); %!error categorical <= '1' %!error {'1'} <= categorical %!error ... %! categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) <= {'1', '2'} %!error ... %! string ({'1', '2'}) <= categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) %!error ... %! categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) <= {'4'} %!error ... %! '4' <= categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) %!error ... %! categorical <= 1 %!error ... %! 1 <= categorical %!error ... %! categorical <= categorical %!test %! X = categorical ([1, 2, NaN], [1, 2, 3], 'Ordinal', true); %! assert_equal (X < '2', [true, false, false]); %! assert_equal (X < '1', [false, false, false]); %!test %! X = categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true); %! assert_equal (X < {'2'}, [true, false, false]); %! assert_equal (X < string ('1'), [false, false, false]); %!test %! X = categorical ([1, 2, NaN], [1, 2, 3], 'Ordinal', true); %! assert_equal ('2' < X, [false, false, false]); %! assert_equal ('1' < X, [false, true, false]); %!test %! X = categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true); %! assert_equal ({'2'} < X, [false, false, true]); %! assert_equal (string ('1') < X, [false, true, true]); %!test %! X = categorical ([1, 2, 4], [1, 2, 3], 'Ordinal', true); %! assert_equal (X < X', logical ([0, 0, 0; 1, 0, 0; 0, 0, 0])); %!test %! X = categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true); %! assert_equal (X < X', logical ([0, 0, 0; 1, 0, 0; 1, 1, 0])); %!error categorical < '1' %!error {'1'} < categorical %!error ... %! categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) < {'1', '2'} %!error ... %! string ({'1', '2'}) < categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) %!error ... %! categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) < {'4'} %!error ... %! '4' < categorical ([1, 2, 3], [1, 2, 3], 'Ordinal', true) %!error ... %! categorical < 1 %!error ... %! 1 < categorical %!error ... %! categorical < categorical %!assert_equal (categorical ({'a'}) != 'a', false) %!assert_equal (string ('1') != categorical ({'a'}), true) %!assert_equal (categorical ({'a', ''}) != {'a'}, [false, true]) %!test %! A = categorical ({'a', 'b', 'c'}); %! B = categorical ({'a'; 'b'; 'c'}); %! assert_equal (A != B, ! logical (eye (3))); %!test %! A = categorical ({'a', 'b', 'c'}); %! B = categorical ({'a'; 'b'; 'c'}); %! B = repmat (B, 1, 3); %! assert_equal (A != B, ! logical (eye (3))); %!test %! X = categorical ([1, 2, NaN], [1, 2, 3], 'Ordinal', true); %! assert_equal (X != X, [false, false, true]); %! assert_equal (X != '1', [false, true, true]); %!error ... %! categorical != {'a', 'b'} %!error ... %! categorical ({'e', 'r'}) != string ({'e', 'r'}) %!error ... %! string ({'e', 'r'}) != categorical ({'e', 'r'}) %!error ... %! {'a', 'b'} != categorical %!error ... %! categorical ({'e', 'r'}, {'e', 'r'}, 'Ordinal', ... %! true) != categorical ({'e', 'r'}, {'e'}, 'Ordinal', true) %!error ... %! categorical ({'e', 'r'}, {'e', 'r'}, 'Ordinal', ... %! true) != categorical ({'e', 'r'}) %!error ... %! categorical ([1, 2, 3]) != categorical ([1, 2, 3, 4]) %!error ... %! categorical ([1, 2, 3]) != 1 %!error ... %! 1 != categorical ([1, 2, 3]) ################################################################################ ## ** Arithmetic Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'min' 'mink' 'max' 'maxk' ## ## 'median' 'mode' 'histcounts' ## ## ## ################################################################################ %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = min (X); %! M_out = categorical (1, [1,2,3], 'Ordinal', true); %! assert_equal (M, M_out); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = min (X, [], 1); %! M_out = categorical ([1, 2, NaN], [1,2,3], 'Ordinal', true); %! assert_equal (isequaln (M, X), true); %! assert_equal (isequaln (M, M_out), true); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = min (X, [], 'includenan'); %! M_out = categorical (NaN, [1,2,3], 'Ordinal', true); %! assert_equal (isequaln (M, M_out), true); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = min (X, [], 1, 'includenan'); %! M_out = categorical ([1, 2, NaN], [1,2,3], 'Ordinal', true); %! assert_equal (isequaln (M, X), true); %! assert_equal (isequaln (M, M_out), true); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = min (X, [], 2, 'omitnan'); %! M_out = categorical (1, [1,2,3], 'Ordinal', true); %! assert_equal (M, M_out); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = min (X, [], 1, 'omitnan'); %! M_out = categorical ([1, 2, NaN], [1,2,3], 'Ordinal', true); %! assert_equal (isequaln (M, X), true); %! assert_equal (isequaln (M, M_out), true); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = min (X, [], 'includeundefined'); %! M_out = categorical (NaN, [1,2,3], 'Ordinal', true); %! assert_equal (isequaln (M, M_out), true); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = min (X, [], 1, 'includeundefined'); %! M_out = categorical ([1, 2, NaN], [1,2,3], 'Ordinal', true); %! assert_equal (isequaln (M, X), true); %! assert_equal (isequaln (M, M_out), true); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = min (X, [], 2, 'omitundefined'); %! M_out = categorical (1, [1,2,3], 'Ordinal', true); %! assert_equal (M, M_out); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = min (X, [], 1, 'omitundefined'); %! M_out = categorical ([1, 2, NaN], [1,2,3], 'Ordinal', true); %! assert_equal (isequaln (M, X), true); %! assert_equal (isequaln (M, M_out), true); %!test %! X = categorical (magic (3), [1:9], 'Ordinal', true); %! M = min (X); %! M_out = categorical ([3, 1, 2], [1:9], 'Ordinal', true); %! assert_equal (M, M_out); %!test %! X = categorical (magic (3), [1:9], 'Ordinal', true); %! M = min (X, [], 2); %! M_out = categorical ([1; 3; 2], [1:9], 'Ordinal', true); %! assert_equal (M, M_out); %!test %! x = categorical (reshape (1:8, [2, 2, 2]), 1:8, 'Ordinal', true); %! out = reshape ([1, 3, 5, 7], [1, 2, 2]); %! assert_equal (min (x, [], 1), categorical (out, 1:8, 'Ordinal', true)); %! out = reshape ([1, 2, 5, 6], [2, 1, 2]); %! assert_equal (min (x, [], 2), categorical (out, 1:8, 'Ordinal', true)); %! [y, i] = min (x, [], 3); %! assert_equal (ndims (y), 2); %! assert_equal (y, categorical ([1, 3; 2, 4], 1:8, 'Ordinal', true)); %! assert_equal (ndims (i), 2); %! assert_equal (i, ones (2)); %! [~, i] = min (x, [], 3, 'linear'); %! assert_equal (i, [1, 3; 2, 4]); %!test %! x = categorical (reshape (1:8, [2, 2, 2]), 1:8, 'Ordinal', true); %! assert_equal (min (x, [], [1, 3]), categorical ([1, 3], 1:8, 'Ordinal', ... %! true)); %! assert_equal (min (x, [], [2, 3]), categorical ([1; 2], 1:8, 'Ordinal', ... %! true)); %! out = reshape ([1, 5], [1, 1, 2]); %! assert_equal (min (x, [], [1, 2]), categorical (out, 1:8, 'Ordinal', true)); %! assert_equal (min (x, [], [1, 2, 3]), min (x, [], "all")); %!test %! X = categorical ([1, 2, 3, 4], 1:4, 'Ordinal', true); %! Y = categorical ([4, 3, 2, 1], 1:4, 'Ordinal', true); %! assert_equal (min (X, Y), categorical ([1, 2, 2, 1], 1:4, 'Ordinal', true)); %!test %! X = categorical ([1, 2, 3, 4], 1:4, 'Ordinal', true); %! Y = categorical (3, 1:4, 'Ordinal', true); %! assert_equal (min (X, Y), categorical ([1, 2, 3, 3], 1:4, 'Ordinal', true)); %!test %! X = categorical ([1, 2, 3, 4], 1:4, 'Ordinal', true); %! Y = categorical (2, 1:4, 'Ordinal', true); %! assert_equal (min (Y, X), categorical ([1, 2, 2, 2], 1:4, 'Ordinal', true)); %!test %! X = categorical ([1, 2, 3, 4], 1:4, 'Ordinal', true); %! Y = categorical ([4, 3, 2, 1], 1:4, 'Ordinal', true); %! out = min ([1, 2, 3, 4], [4; 3; 2; 1]); %! assert_equal (min (X, Y'), categorical (out, 1:4, 'Ordinal', true)); %!test %! X = categorical ([1, 2, 3, 4], 1:4, 'Ordinal', true); %! Y = categorical ([4, 3, 2, 1], 1:4, 'Ordinal', true); %! out = min ([1; 2; 3; 4], [4, 3, 2, 1]); %! assert_equal (min (X', Y), categorical (out, 1:4, 'Ordinal', true)); %!test %! x = repmat ([4, 3, 2, 4, 1, 5, 3, 2], 3, 2, 5, 2); %! [m, i] = min (categorical (x, 1:5, 'Ordinal', true), [], [2, 3], 'linear'); %! assert_equal (m, categorical (ones (3, 1, 1, 2), 1:5, 'Ordinal', true)); %! assert_equal (i(:,:,1,1), [13; 14; 15]); %! assert_equal (i(:,:,1,2), [253; 254; 255]); %!test %! x = repmat ([4, 3, 2, 4, 1, 5, 3, 2], 3, 2, 5, 2); %! [m, i] = min (categorical (x, 1:5, 'Ordinal', true), [], [1, 3], 'linear'); %! assert_equal (m, categorical (x(1,:,1,:), 1:5, 'Ordinal', true)); %! assert_equal (i(:,:,1,1), [1:3:46]); %! assert_equal (i(:,:,1,2), [241:3:286]); %!test %! x = repmat ([4, 3, 2, 4, 1, 5, 3, 2], 3, 2, 5, 2); %! [m, i] = min (categorical (x, 1:5, 'Ordinal', true), [], [2, 4], 'linear'); %! assert_equal (m, categorical (ones (3, 1, 5), 1:5, 'Ordinal', true)); %! assert_equal (i(:,:,1), [13; 14; 15]); %! assert_equal (i(:,:,2), [61; 62; 63]); %! assert_equal (i(:,:,3), [109; 110; 111]); %! assert_equal (i(:,:,4), [157; 158; 159]); %! assert_equal (i(:,:,5), [205; 206; 207]); %!test %! x = repmat ([4, 3, 2, 4, 1, 5, 3, 2], 3, 2, 5, 2); %! [m, i] = min (categorical (x, 1:5, 'Ordinal', true), [], [1, 4], 'linear'); %! assert_equal (m, categorical (x(1,:,:,1), 1:5, 'Ordinal', true)); %! assert_equal (i(:,:,1), [1:3:46]); %! assert_equal (i(:,:,2), [49:3:94]); %! assert_equal (i(:,:,3), [97:3:142]); %! assert_equal (i(:,:,4), [145:3:190]); %! assert_equal (i(:,:,5), [193:3:238]); %!test %! x = repmat ([4, 3, 2, 4, 1, 5, 3, 2], 3, 2, 5, 2); %! [m, i] = min (categorical (x, 1:5, 'Ordinal', true), [], [1, 2, 3], ... %! 'linear'); %! assert_equal (m, categorical (ones (1, 1, 1, 2), 1:5, 'Ordinal', true)); %! assert_equal (i(:,:,1,1), 13); %! assert_equal (i(:,:,1,2), 253); %!test %! x = repmat ([4, 3, 2, 4, 1, 5, 3, 2], 3, 2, 5, 2); %! [m, i] = min (categorical (x, 1:5, 'Ordinal', true), [], [2, 3, 4], ... %! 'linear'); %! assert_equal (m, categorical ([1; 1; 1], 1:5, 'Ordinal', true)); %! assert_equal (i, [13; 14; 15]); %!error ... %! min (categorical) %!error ... %! min (categorical ([], 1, 'Ordinal', true), [], 1, 2, 3, 4) %!error ... %! [m, i] = min (categorical ([], 1, 'Ordinal', true), categorical) %!error ... %! min (categorical ([], 1, 'Ordinal', true), categorical) %!error ... %! min (categorical ([], 1, 'Ordinal', true), categorical (1, 2, 'Ordinal', ... %! true)) %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = max (X); %! M_out = categorical (2, [1,2,3], 'Ordinal', true); %! assert_equal (M, M_out); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = max (X, [], 1); %! M_out = categorical ([1, 2, NaN], [1,2,3], 'Ordinal', true); %! assert_equal (isequaln (M, X), true); %! assert_equal (isequaln (M, M_out), true); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = max (X, [], 'includenan'); %! M_out = categorical (NaN, [1,2,3], 'Ordinal', true); %! assert_equal (isequaln (M, M_out), true); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = max (X, [], 1, 'includenan'); %! M_out = categorical ([1, 2, NaN], [1,2,3], 'Ordinal', true); %! assert_equal (isequaln (M, X), true); %! assert_equal (isequaln (M, M_out), true); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = max (X, [], 2, 'omitnan'); %! M_out = categorical (2, [1,2,3], 'Ordinal', true); %! assert_equal (M, M_out); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = max (X, [], 1, 'omitnan'); %! M_out = categorical ([1, 2, NaN], [1,2,3], 'Ordinal', true); %! assert_equal (isequaln (M, X), true); %! assert_equal (isequaln (M, M_out), true); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = max (X, [], 'includeundefined'); %! M_out = categorical (NaN, [1,2,3], 'Ordinal', true); %! assert_equal (isequaln (M, M_out), true); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = max (X, [], 1, 'includeundefined'); %! M_out = categorical ([1, 2, NaN], [1,2,3], 'Ordinal', true); %! assert_equal (isequaln (M, X), true); %! assert_equal (isequaln (M, M_out), true); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = max (X, [], 2, 'omitundefined'); %! M_out = categorical (2, [1,2,3], 'Ordinal', true); %! assert_equal (M, M_out); %!test %! X = categorical ([1,2,3], [1,2,3], 'Ordinal', true); %! M = max (X, [], 2, 'omitundefined'); %! M_out = categorical (3, [1,2,3], 'Ordinal', true); %! assert_equal (M, M_out); %!test %! X = categorical ([1,2,4], [1,2,3], 'Ordinal', true); %! M = max (X, [], 1, 'omitundefined'); %! M_out = categorical ([1, 2, NaN], [1,2,3], 'Ordinal', true); %! assert_equal (isequaln (M, X), true); %! assert_equal (isequaln (M, M_out), true); %!test %! X = categorical (magic (3), [1:9], 'Ordinal', true); %! M = max (X); %! M_out = categorical ([8, 9, 7], [1:9], 'Ordinal', true); %! assert_equal (M, M_out); %!test %! X = categorical (magic (3), [1:9], 'Ordinal', true); %! M = max (X, [], 2); %! M_out = categorical ([8; 7; 9], [1:9], 'Ordinal', true); %! assert_equal (M, M_out); %!test %! x = categorical (reshape (1:8, [2, 2, 2]), 1:8, 'Ordinal', true); %! out = reshape ([2, 4, 6, 8], [1, 2, 2]); %! assert_equal (max (x, [], 1), categorical (out, 1:8, 'Ordinal', true)); %! out = reshape ([3, 4, 7, 8], [2, 1, 2]); %! assert_equal (max (x, [], 2), categorical (out, 1:8, 'Ordinal', true)); %! [y, i] = max (x, [], 3); %! assert_equal (ndims (y), 2); %! assert_equal (y, categorical ([5, 7; 6, 8], 1:8, 'Ordinal', true)); %! assert_equal (ndims (i), 2); %! assert_equal (i, 2 * ones (2)); %! [~, i] = max (x, [], 3, 'linear'); %! assert_equal (i, [5, 7; 6, 8]); %!test %! x = categorical (reshape (1:8, [2, 2, 2]), 1:8, 'Ordinal', true); %! assert_equal (max (x, [], [1, 3]), categorical ([6, 8], 1:8, 'Ordinal', ... %! true)); %! assert_equal (max (x, [], [2, 3]), categorical ([7; 8], 1:8, 'Ordinal', ... %! true)); %! out = reshape ([4, 8], [1, 1, 2]); %! assert_equal (max (x, [], [1, 2]), categorical (out, 1:8, 'Ordinal', true)); %! assert_equal (max (x, [], [1, 2, 3]), max (x, [], "all")); %!test %! X = categorical ([1, 2, 3, 4], 1:4, 'Ordinal', true); %! Y = categorical ([4, 3, 2, 1], 1:4, 'Ordinal', true); %! assert_equal (max (X, Y), categorical ([4, 3, 3, 4], 1:4, 'Ordinal', true)); %!test %! X = categorical ([1, 2, 3, 4], 1:4, 'Ordinal', true); %! Y = categorical (3, 1:4, 'Ordinal', true); %! assert_equal (max (X, Y), categorical ([3, 3, 3, 4], 1:4, 'Ordinal', true)); %!test %! X = categorical ([1, 2, 3, 4], 1:4, 'Ordinal', true); %! Y = categorical (2, 1:4, 'Ordinal', true); %! assert_equal (max (Y, X), categorical ([2, 2, 3, 4], 1:4, 'Ordinal', true)); %!test %! X = categorical ([1, 2, 3, 4], 1:4, 'Ordinal', true); %! Y = categorical ([4, 3, 2, 1], 1:4, 'Ordinal', true); %! out = max ([1, 2, 3, 4], [4; 3; 2; 1]); %! assert_equal (max (X, Y'), categorical (out, 1:4, 'Ordinal', true)); %!test %! X = categorical ([1, 2, 3, 4], 1:4, 'Ordinal', true); %! Y = categorical ([4, 3, 2, 1], 1:4, 'Ordinal', true); %! out = max ([1; 2; 3; 4], [4, 3, 2, 1]); %! assert_equal (max (X', Y), categorical (out, 1:4, 'Ordinal', true)); %!test %! x = repmat ([4, 3, 2, 4, 1, 5, 3, 2], 3, 2, 5, 2); %! [m, i] = max (categorical (x, 1:5, 'Ordinal', true), [], [2, 3], 'linear'); %! assert_equal (m, categorical (5 * ones (3, 1, 1, 2), 1:5, 'Ordinal', true)); %! assert_equal (i(:,:,1,1), [16; 17; 18]); %! assert_equal (i(:,:,1,2), [256; 257; 258]); %!test %! x = repmat ([4, 3, 2, 4, 1, 5, 3, 2], 3, 2, 5, 2); %! [m, i] = max (categorical (x, 1:5, 'Ordinal', true), [], [1, 3], 'linear'); %! assert_equal (m, categorical (x(1,:,1,:), 1:5, 'Ordinal', true)); %! assert_equal (i(:,:,1,1), [1:3:46]); %! assert_equal (i(:,:,1,2), [241:3:286]); %!test %! x = repmat ([4, 3, 2, 4, 1, 5, 3, 2], 3, 2, 5, 2); %! [m, i] = max (categorical (x, 1:5, 'Ordinal', true), [], [2, 4], 'linear'); %! assert_equal (m, categorical (5 * ones (3, 1, 5), 1:5, 'Ordinal', true)); %! assert_equal (i(:,:,1), [16; 17; 18]); %! assert_equal (i(:,:,2), [64; 65; 66]); %! assert_equal (i(:,:,3), [112; 113; 114]); %! assert_equal (i(:,:,4), [160; 161; 162]); %! assert_equal (i(:,:,5), [208; 209; 210]); %!test %! x = repmat ([4, 3, 2, 4, 1, 5, 3, 2], 3, 2, 5, 2); %! [m, i] = max (categorical (x, 1:5, 'Ordinal', true), [], [1, 4], 'linear'); %! assert_equal (m, categorical (x(1,:,:,1), 1:5, 'Ordinal', true)); %! assert_equal (i(:,:,1), [1:3:46]); %! assert_equal (i(:,:,2), [49:3:94]); %! assert_equal (i(:,:,3), [97:3:142]); %! assert_equal (i(:,:,4), [145:3:190]); %! assert_equal (i(:,:,5), [193:3:238]); %!test %! x = repmat ([4, 3, 2, 4, 1, 5, 3, 2], 3, 2, 5, 2); %! [m, i] = max (categorical (x, 1:5, 'Ordinal', true), [], [1, 2, 3], ... %! 'linear'); %! assert_equal (m, categorical (5 * ones (1, 1, 1, 2), 1:5, 'Ordinal', true)); %! assert_equal (i(:,:,1,1), 16); %! assert_equal (i(:,:,1,2), 256); %!test %! x = repmat ([4, 3, 2, 4, 1, 5, 3, 2], 3, 2, 5, 2); %! [m, i] = max (categorical (x, 1:5, 'Ordinal', true), [], [2, 3, 4], ... %! 'linear'); %! assert_equal (m, categorical ([5; 5; 5], 1:5, 'Ordinal', true)); %! assert_equal (i, [16; 17; 18]); %!error ... %! max (categorical) %!error ... %! max (categorical ([], 1, 'Ordinal', true), [], 1, 2, 3, 4) %!error ... %! [m, i] = max (categorical ([], 1, 'Ordinal', true), categorical) %!error ... %! max (categorical ([], 1, 'Ordinal', true), categorical) %!error ... %! max (categorical ([], 1, 'Ordinal', true), categorical (1, 2, 'Ordinal', ... %! true)) %!test %! M = median (categorical ([2, 4, 6, 8], 1:8, 'Ordinal', true)); %! assert_equal (M, categorical (5, 1:8, 'Ordinal', true)); %!test %! M = median (categorical ([8, 2, 6, 4], 1:8, 'Ordinal', true)); %! assert_equal (M, categorical (5, 1:8, 'Ordinal', true)); %!test %! M = median (categorical ([1, 2, 3, 4], 1:4, 'Ordinal', true)); %! assert_equal (M, categorical (3, 1:4, 'Ordinal', true)); %!test %! M = median (categorical ([1, 2, 3, 4], 1:3, 'Ordinal', true)); %! assert_equal (isequaln (M, categorical (NaN, 1:3, 'Ordinal', true)), true); %!test %! M = median (categorical ([1, 2, 3, 4], 1:3, 'Ordinal', true), ... %! 'omitundefined'); %! assert_equal (M, categorical (2, 1:3, 'Ordinal', true)); %!test %! M = median (categorical (magic (3), 1:9, 'Ordinal', true)); %! assert_equal (M, categorical ([4, 5, 6], 1:9, 'Ordinal', true)); %!test %! M = median (categorical (magic (3), 1:9, 'Ordinal', true), 2); %! assert_equal (M, categorical ([6; 5; 4], 1:9, 'Ordinal', true)); %!test %! M = median (categorical (magic (3), 1:9, 'Ordinal', true), 3); %! assert_equal (M, categorical (magic (3), 1:9, 'Ordinal', true)); %!test %! M = median (categorical (magic (3), 1:9, 'Ordinal', true), [1, 2]); %! assert_equal (M, categorical (5, 1:9, 'Ordinal', true)); %!test %! M = median (categorical (magic (3), 1:9, 'Ordinal', true), 'all'); %! assert_equal (M, categorical (5, 1:9, 'Ordinal', true)); %!test %! M = median (categorical (magic (4), 1:16, 'Ordinal', true)); %! assert_equal (M, categorical ([7, 9, 8, 10], 1:16, 'Ordinal', true)); %!test %! M = median (categorical (magic (4), 1:16, 'Ordinal', true), 2); %! assert_equal (M, categorical ([8; 9; 8; 9], 1:16, 'Ordinal', true)); %!test %! M = median (categorical (magic (4), 1:16, 'Ordinal', true), 3); %! assert_equal (M, categorical (magic (4), 1:16, 'Ordinal', true)); %!test %! M = median (categorical (magic (4), 1:16, 'Ordinal', true), [1, 2]); %! assert_equal (M, categorical (9, 1:16, 'Ordinal', true)); %!test %! M = median (categorical (magic (4), 1:16, 'Ordinal', true), 'all'); %! assert_equal (M, categorical (9, 1:16, 'Ordinal', true)); %!test %! M = median (categorical (magic (4), 1:14, 'Ordinal', true)); %! assert_equal (isequaln (M, categorical ([NaN, 9, NaN, 10], 1:14, ... %! 'Ordinal', true)), true); %!test %! M = median (categorical (magic (4), 1:14, 'Ordinal', true), 2); %! assert_equal (isequaln (M, categorical ([NaN; 9; 8; NaN], 1:14, ... %! 'Ordinal', true)), true); %!test %! M = median (categorical (magic (4), 1:14, 'Ordinal', true), 3); %! assert_equal (isequaln (M, categorical (magic (4), 1:14, 'Ordinal', ... %! true)), true); %!test %! M = median (categorical (magic (4), 1:14, 'Ordinal', true), [1, 2]); %! assert_equal (isequaln (M, categorical (NaN, 1:14, 'Ordinal', true)), true); %!test %! M = median (categorical (magic (4), 1:14, 'Ordinal', true), 'all'); %! assert_equal (isequaln (M, categorical (NaN, 1:14, 'Ordinal', true)), true); %!test %! M = median (categorical (magic (4), 1:14, 'Ordinal', true), 'omitundefined'); %! assert_equal (M, categorical ([5, 9, 6, 10], 1:14, 'Ordinal', true)); %!test %! M = median (categorical (magic (4), 1:14, 'Ordinal', true), 2, ... %! 'omitundefined'); %! assert_equal (M, categorical ([3; 9; 8; 4], 1:14, 'Ordinal', true)); %!test %! M = median (categorical (magic (4), 1:14, 'Ordinal', true), 3, ... %! 'omitundefined'); %! assert_equal (isequaln (M, categorical (magic (4), 1:14, 'Ordinal', ... %! true)), true); %!test %! M = median (categorical (magic (4), 1:14, 'Ordinal', true), [1, 2], ... %! 'omitundefined'); %! assert_equal (M, categorical (8, 1:14, 'Ordinal', true)); %!test %! M = median (categorical (magic (4), 1:14, 'Ordinal', true), 'all', ... %! 'omitundefined'); %! assert_equal (M, categorical (8, 1:14, 'Ordinal', true)); %!error ... %! median (categorical) %!error ... %! median (categorical (1, 1, 'Ordinal', true), 1, 2, 3) %!test %! [m, f, c] = mode (categorical (toeplitz (1:5))); %! assert_equal (m, categorical ([1, 2, 2, 2, 1], 1:5)); %! assert_equal (f, [1, 2, 2, 2, 1]); %! assert_equal (size (c), [1, 5]); %! assert_equal (c{1}, categorical ([1:5]', 1:5)); %! assert_equal (c{2}, categorical (2, 1:5)); %! assert_equal (c{3}, categorical ([2; 3], 1:5)); %! assert_equal (c{4}, categorical (2, 1:5)); %! assert_equal (c{5}, categorical ([1:5]', 1:5)); %!test %! [m, f, c] = mode (categorical (toeplitz (1:5)), 2); %! assert_equal (m, categorical ([1; 2; 2; 2; 1], 1:5)); %! assert_equal (f, [1; 2; 2; 2; 1]); %! assert_equal (size (c), [5, 1]); %! assert_equal (c{1}, categorical ([1:5]', 1:5)); %! assert_equal (c{2}, categorical (2, 1:5)); %! assert_equal (c{3}, categorical ([2; 3], 1:5)); %! assert_equal (c{4}, categorical (2, 1:5)); %! assert_equal (c{5}, categorical ([1:5]', 1:5)); %!test %! [m, f, c] = mode (categorical (toeplitz (1:5)), 3); %! assert_equal (m, categorical (toeplitz (1:5), 1:5)); %! assert_equal (f, ones (5)); %! assert_equal (size (c), [5, 5]); %! C = arrayfun (@(x) categorical (x, 1:5), toeplitz (1:5), 'UniformOutput', ... %! false); %! assert_equal (c, C); %!test %! [m, f, c] = mode (categorical (toeplitz (1:5)), [1, 2]); %! assert_equal (m, categorical (2, 1:5)); %! assert_equal (f, 8); %! assert_equal (isscalar (c), true); %! assert_equal (c{1}, categorical (2, 1:5)); %!test %! [m, f, c] = mode (categorical (toeplitz (1:5)), 'all'); %! assert_equal (m, categorical (2, 1:5)); %! assert_equal (f, 8); %! assert_equal (isscalar (c), true); %! assert_equal (c{1}, categorical (2, 1:5)); %!test %! A = [0 0 1 1 1 0 0 0 0 NaN NaN 1 0 0 0 1 0 1 0 1 0 0 0 1 1 1 1]; %! C = categorical (A, [1, 0, NaN], {'yes', 'no', 'undecided'}); %! [N, Cats] = histcounts (C); %! assert_equal (N, [11, 14, 2]); %! assert_equal (Cats, {'yes', 'no', 'undecided'}); %! N = histcounts (C, 'Normalization', 'countdensity'); %! assert_equal (N, [11, 14, 2]); %! N = histcounts (C, 'Normalization', 'probability'); %! assert_equal (N, [0.4074, 0.5185, 0.0741], 1e-4); %! N = histcounts (C, 'Normalization', 'pdf'); %! assert_equal (N, [0.4074, 0.5185, 0.0741], 1e-4); %! N = histcounts (C, 'Normalization', 'cumcount'); %! assert_equal (N, [11, 25, 27]); %! N = histcounts (C, 'Normalization', 'cdf'); %! assert_equal (N, [0.4074, 0.9259, 1.0000], 1e-4); %!test %! A = [0 0 1 1 1 0 0 0 0 NaN NaN 1 0 0 0 1 0 1 0 1 0 0 0 1 1 1 1]; %! C = categorical (A, [1, 0, NaN], {'yes', 'no', 'undecided'}); %! [N, Cats] = histcounts (C, {'yes', 'no'}); %! assert_equal (N, [11, 14]); %! assert_equal (Cats, {'yes', 'no'}); %! N = histcounts (C, {'yes', 'no'}, 'Normalization', 'countdensity'); %! assert_equal (N, [11, 14]); %! N = histcounts (C, {'yes', 'no'}, 'Normalization', 'probability'); %! assert_equal (N, [0.4074, 0.5185], 1e-4); %! N = histcounts (C, {'yes', 'no'}, 'Normalization', 'pdf'); %! assert_equal (N, [0.4074, 0.5185], 1e-4); %! N = histcounts (C, {'yes', 'no'}, 'Normalization', 'cumcount'); %! assert_equal (N, [11, 25]); %! N = histcounts (C, {'yes', 'no'}, 'Normalization', 'cdf'); %! assert_equal (N, [0.4074, 0.9259], 1e-4); %!test %! A = [0 0 1 1 1 0 0 0 0 NaN NaN 1 0 0 0 1 0 1 0 1 0 0 0 1 1 1 1]; %! C = categorical (A, [1, 0, NaN], {'yes', 'no', 'undecided'}); %! [N, Cats] = histcounts (C, {'no', 'yes'}); %! assert_equal (N, [14, 11]); %! assert_equal (Cats, {'no', 'yes'}); %! [N, Cats] = histcounts (C, {'undecided', 'yes', 'no'}); %! assert_equal (N, [2, 11, 14]); %! assert_equal (Cats, {'undecided', 'yes', 'no'}); %! [N, Cats] = histcounts (C, {'undecided'}); %! assert_equal (N, 2); %! assert_equal (Cats, {'undecided'}); %! N = histcounts (C, {'no', 'yes'}, 'Normalization', 'cumcount'); %! assert_equal (N, [14, 25]); %! N = histcounts (C, {'undecided', 'yes', 'no'}, 'Normalization', 'cumcount'); %! assert_equal (N, [2, 13, 27]); %!test %! A = [0 0 1 1 1 0 0 0 0 NaN NaN 1 0 0 0 1 0 1 0 1 0 0 0 1 1 1 1]; %! C = categorical (A, [1, 0, NaN], {'yes', 'no', 'undecided'}); %! [N, Cats] = histcounts (C, categorical ({'yes', 'no'})); %! assert_equal (N, [11, 14]); %! assert_equal (Cats, {'yes', 'no'}); %! [N, Cats] = histcounts (C, categorical ({'yes', 'undecided'})); %! assert_equal (N, [11, 2]); %! assert_equal (Cats, {'yes', 'undecided'}); %! [N, Cats] = histcounts (C, string ({'no', 'yes'})); %! assert_equal (N, [14, 11]); %! assert_equal (Cats, {'no', 'yes'}); %! [N, Cats] = histcounts (C, string ({'yes', 'undecided'})); %! assert_equal (N, [11, 2]); %! assert_equal (Cats, {'yes', 'undecided'}); %!test %! A = [0 0 1 1 1 0 0 0 0 NaN NaN 1 0 0 0 1 0 1 0 1 0 0 0 1 1 1 1]; %! C = categorical (A, [1, 0, NaN], {'yes', 'no', 'undecided'}); %! N = histcounts (C, 'Normalization', 'percentage'); %! assert_equal (N, [40.7407, 51.8519, 7.4074], 1e-4); %! N = histcounts (C, {'no', 'yes'}, 'Normalization', 'percentage'); %! assert_equal (N, [51.8519, 40.7407], 1e-4); %! N = histcounts (C, {'undecided'}, 'Normalization', 'percentage'); %! assert_equal (N, 7.4074, 1e-4); %! assert_equal (sum (histcounts (C, 'Normalization', 'percentage')), 100, ... %! 1e-10); %!test %! E = categorical ({'b', 'a', '', 'c'}); %! assert_equal (histcounts (E), [1, 1, 1]); %! assert_equal (histcounts (E, 'Normalization', 'percentage'), [25, 25, 25]); %! assert_equal (histcounts (E, 'Normalization', 'probability'), ... %! [0.25, 0.25, 0.25]); %! assert_equal (histcounts (E, 'Normalization', 'cdf'), [0.25, 0.5, 0.75]); %!test %! assert_equal (histcounts (categorical ({'a'}), 'Normalization', ... %! 'percentage'), 100); %! assert_equal (class (histcounts (categorical ({'a', 'b'}), ... %! 'Normalization', 'percentage')), 'double'); %!test %! C = categorical ({'b', 'a', 'c', 'a', 'b', 'a'}); %! assert_equal (histcounts (C, 'Normalization', 'Count'), [3, 2, 1]); %! assert_equal (histcounts (C, 'Normalization', 'COUNT'), [3, 2, 1]); %! assert_equal (histcounts (C, 'Normalization', 'CountDensity'), [3, 2, 1]); %! assert_equal (histcounts (C, 'Normalization', 'CumCount'), [3, 5, 6]); %! assert_equal (histcounts (C, 'Normalization', 'PERCENTAGE'), ... %! [50, 33.3333, 16.6667], 1e-4); %! assert_equal (histcounts (C, 'Normalization', 'Probability'), ... %! [0.5, 1/3, 1/6], 1e-12); %! assert_equal (histcounts (C, 'Normalization', 'PDF'), [0.5, 1/3, 1/6], ... %! 1e-12); %! assert_equal (histcounts (C, 'Normalization', 'CDF'), ... %! [0.5, 0.8333, 1], 1e-4); %! assert_equal (histcounts (C, 'Normalization', string ('Count')), [3, 2, 1]); %!test %! A = [0 0 1 1 1 0 0 0 0 NaN NaN 1 0 0 0 1 0 1 0 1 0 0 0 1 1 1 1]; %! C = categorical (A); %! [N, Cats] = histcounts (C); %! assert_equal (N, [14, 11]); %! assert_equal (Cats, {'0', '1'}); %! N = histcounts (C, 'Normalization', 'countdensity'); %! assert_equal (N, [14, 11]); %! N = histcounts (C, 'Normalization', 'probability'); %! assert_equal (N, [0.5185, 0.4074], 1e-4); %! N = histcounts (C, 'Normalization', 'pdf'); %! assert_equal (N, [0.5185, 0.4074], 1e-4); %! N = histcounts (C, 'Normalization', 'cumcount'); %! assert_equal (N, [14, 25]); %! N = histcounts (C, 'Normalization', 'cdf'); %! assert_equal (N, [0.5185, 0.9259], 1e-4); %!test %! [N, Cats] = histcounts (categorical, {}); %! assert_equal (N, ones (1, 0)); %! assert_equal (Cats, cell (1, 0)); %!error ... %! histcounts (categorical, 'Normalization', 'some') %!error ... %! histcounts (categorical, 1) %!error ... %! histcounts (categorical, '1') %!error ... %! histcounts (categorical, {1}) %!error ... %! histcounts (categorical ({'a', 'b'}), {'a', 'a'}) ## Test 'histcounts' -- a name that is not a category of A counts zero rather ## than erroring, and elements are never credited to it %!test %! C = categorical ({'b', 'a', 'c', 'a', 'b', 'a'}); %! [n, c] = histcounts (C, {'a', 'zzz'}); %! assert_equal (n, [3 0]); %! assert_equal (c, {'a', 'zzz'}); %!test %! C = categorical ({'b', 'a', 'c', 'a', 'b', 'a'}); %! [n, c] = histcounts (C, {'zzz', 'yyy'}); %! assert_equal (n, [0 0]); %! assert_equal (c, {'zzz', 'yyy'}); %!test %! E = categorical ({'b', 'a', '', 'c'}); %! assert_equal (histcounts (E, {'a', 'zzz'}), [1 0]); %! assert_equal (histcounts (E, {'zzz'}), 0); %!test %! C = categorical ({'b', 'a', 'c', 'a', 'b', 'a'}); %! assert_equal (histcounts (C, {'zzz', 'a', 'yyy'}), [0 3 0]); %! assert_equal (histcounts (C, {'a', 'zzz'}, 'Normalization', ... %! 'probability'), ... %! [0.5 0]); %! assert_equal (histcounts (C, {'a', 'zzz'}, 'Normalization', 'cumcount'), ... %! [3 3]); %! assert_equal (histcounts (C, {''}), 0); %!test %! C = categorical ({'b', 'a', 'c', 'a', 'b', 'a'}); %! [n, c] = histcounts (C, string ({'a', 'zzz'})); %! assert_equal (n, [3 0]); %! assert_equal (class (c), 'cell') ################################################################################ ## ** Sort, Filter, and Set Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'maxk' 'mink' 'sort' 'sortrows' ## ## 'topkrows' 'unique' 'intersect' 'setdiff' ## ## 'setxor' 'union' ## ## ## ################################################################################ %!test %! C = categorical ({'b', 'a', 'c', 'a'}, {'a', 'b', 'c'}, 'Ordinal', true); %! [B, I] = maxk (C, 2); %! assert_equal (cellstr (B), {'c', 'b'}); %! assert_equal (I, [3, 1]); %! assert_equal (categories (B), {'a'; 'b'; 'c'}); %! assert_equal (isordinal (B), true); %!test %! C = categorical ({'b', '', 'c', 'a'}, {'a', 'b', 'c'}, 'Ordinal', true); %! [B, I] = maxk (C, 3); %! assert_equal (cellstr (B), {'c', 'b', 'a'}); %! assert_equal (I, [3, 1, 4]); %!test %! C = categorical ({'b', '', 'c', 'a'}, {'a', 'b', 'c'}, 'Ordinal', true); %! [B, I] = maxk (C, 10); %! assert_equal (isundefined (B), logical ([0, 0, 0, 1])); %! assert_equal (I, [3, 1, 4, 2]); %!test %! C = categorical ({'b', 'a', 'b', 'a', 'b'}, {'a', 'b'}, 'Ordinal', true); %! [B, I] = maxk (C, 3); %! assert_equal (cellstr (B), {'b', 'b', 'b'}); %! assert_equal (I, [1, 3, 5]); %!test %! C = categorical ({'b', 'a'; 'c', 'a'; 'a', 'b'}, {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! [B, I] = maxk (C, 2); %! assert_equal (cellstr (B), {'c', 'b'; 'b', 'a'}); %! assert_equal (I, [2, 3; 1, 1]); %!test %! C = categorical ({'b', 'a'; 'c', 'a'; 'a', 'b'}, {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! [B, I] = maxk (C, 2, 2); %! assert_equal (cellstr (B), {'b', 'a'; 'c', 'a'; 'b', 'a'}); %! assert_equal (I, [1, 2; 1, 2; 2, 1]); %!test %! C = categorical ({'b', 'a'; 'c', 'a'; 'a', 'b'}, {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! [B, I] = maxk (C, 2, 3); %! assert_equal (cellstr (B), {'b', 'a'; 'c', 'a'; 'a', 'b'}); %! assert_equal (I, ones (3, 2)); %!test %! C = categorical ({'', '', ''}, {'a', 'b', 'c'}, 'Ordinal', true); %! [B, I] = maxk (C, 2); %! assert_equal (isundefined (B), logical ([1, 1])); %! assert_equal (I, [1, 2]); %!test %! C = categorical ({'b', 'a', 'c'}, {'a', 'b', 'c'}, 'Ordinal', true); %! assert_equal (size (maxk (C, 0)), [1, 0]); %! assert_equal (class (maxk (C, 2)), 'categorical'); %! assert_equal (cellstr (maxk (C, 10)), {'c', 'b', 'a'}); %!error ... %! maxk (categorical ({'a'}, {'a'}, 'Ordinal', true)) %!error ... %! maxk (categorical ({'b', 'a'}), 1) %!error ... %! maxk (categorical ({'b', 'a'}, {'a', 'b'}, 'Ordinal', true), 2.5) %!error ... %! maxk (categorical ({'b', 'a'}, {'a', 'b'}, 'Ordinal', true), -1) %!error ... %! maxk (categorical ({'b', 'a'}, {'a', 'b'}, 'Ordinal', true), [2, 3]) %!error ... %! maxk (categorical ({'b', 'a'}, {'a', 'b'}, 'Ordinal', true), 2, 0) %!error ... %! maxk (categorical ({'b', 'a'}, {'a', 'b'}, 'Ordinal', true), 2, ... %! 'ComparisonMethod', 'abs') %!error ... %! maxk (categorical ({'b', 'a'}, {'a', 'b'}, 'Ordinal', true), 2, 1, 2) %!test %! C = categorical ({'b', 'a', 'c', 'a'}, {'a', 'b', 'c'}, 'Ordinal', true); %! [B, I] = mink (C, 2); %! assert_equal (cellstr (B), {'a', 'a'}); %! assert_equal (I, [2, 4]); %! assert_equal (categories (B), {'a'; 'b'; 'c'}); %! assert_equal (isordinal (B), true); %!test %! C = categorical ({'b', '', 'c', 'a'}, {'a', 'b', 'c'}, 'Ordinal', true); %! [B, I] = mink (C, 3); %! assert_equal (cellstr (B), {'a', 'b', 'c'}); %! assert_equal (I, [4, 1, 3]); %!test %! C = categorical ({'b', '', 'c', 'a'}, {'a', 'b', 'c'}, 'Ordinal', true); %! [B, I] = mink (C, 10); %! assert_equal (isundefined (B), logical ([0, 0, 0, 1])); %! assert_equal (I, [4, 1, 3, 2]); %!test %! C = categorical ({'b', 'a', 'b', 'a', 'b'}, {'a', 'b'}, 'Ordinal', true); %! [B, I] = mink (C, 3); %! assert_equal (cellstr (B), {'a', 'a', 'b'}); %! assert_equal (I, [2, 4, 1]); %!test %! C = categorical ({'b', 'a'; 'c', 'a'; 'a', 'b'}, {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! [B, I] = mink (C, 2); %! assert_equal (cellstr (B), {'a', 'a'; 'b', 'a'}); %! assert_equal (I, [3, 1; 1, 2]); %!test %! C = categorical ({'', '', ''}, {'a', 'b', 'c'}, 'Ordinal', true); %! [B, I] = mink (C, 2); %! assert_equal (isundefined (B), logical ([1, 1])); %! assert_equal (I, [1, 2]); %!test %! C = categorical ({'b', 'a', 'c'}, {'a', 'b', 'c'}, 'Ordinal', true); %! assert_equal (size (mink (C, 0)), [1, 0]); %! assert_equal (class (mink (C, 2)), 'categorical'); %! assert_equal (cellstr (mink (C, 10)), {'a', 'b', 'c'}); %!error ... %! mink (categorical ({'a'}, {'a'}, 'Ordinal', true)) %!error ... %! mink (categorical ({'b', 'a'}), 1) %!error ... %! mink (categorical ({'b', 'a'}, {'a', 'b'}, 'Ordinal', true), 2.5) %!error ... %! mink (categorical ({'b', 'a'}, {'a', 'b'}, 'Ordinal', true), Inf) %!error ... %! mink (categorical ({'b', 'a'}, {'a', 'b'}, 'Ordinal', true), 2, 1.5) %!error ... %! mink (categorical ({'b', 'a'}, {'a', 'b'}, 'Ordinal', true), 2, ... %! 'MissingPlacement', 'first') %!error ... %! mink (categorical ({'b', 'a'}, {'a', 'b'}, 'Ordinal', true), 2, 1, 2) %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (C); %! assert_equal (B, categorical ([1, -4; 1, 1; 2, 2; 2, 3])); %! assert_equal (I, [1, 3; 2, 4; 3, 1; 4, 2]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (C, 1); %! assert_equal (B, categorical ([1, -4; 1, 1; 2, 2; 2, 3])); %! assert_equal (I, [1, 3; 2, 4; 3, 1; 4, 2]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (C, 2); %! assert_equal (B, categorical ([1, 2; 1, 3; -4, 2; 1, 2])); %! assert_equal (I, [1, 2; 1, 2; 2, 1; 2, 1]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (C, 'descend'); %! assert_equal (B, categorical ([2, 3; 2, 2; 1, 1; 1, -4])); %! assert_equal (I, [3, 2; 4, 1; 1, 4; 2, 3]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (C, 1, 'descend'); %! assert_equal (B, categorical ([2, 3; 2, 2; 1, 1; 1, -4])); %! assert_equal (I, [3, 2; 4, 1; 1, 4; 2, 3]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (C, 2, 'descend'); %! assert_equal (B, categorical ([2, 1; 3, 1; 2, -4; 2, 1])); %! assert_equal (I, [2, 1; 2, 1; 1, 2; 1, 2]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! [B, I] = sort (C); %! X = [-2, -4, 1; 1, 1, 2; 1, 2, 2; 1, 3, 2; 2, 3, 3; 2, NaN, NaN]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [5, 3, 2; 1, 4, 1; 2, 1, 3; 6, 2, 6; 3, 6, 5; 4, 5, 4]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! [B, I] = sort (C, 1); %! X = [-2, -4, 1; 1, 1, 2; 1, 2, 2; 1, 3, 2; 2, 3, 3; 2, NaN, NaN]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [5, 3, 2; 1, 4, 1; 2, 1, 3; 6, 2, 6; 3, 6, 5; 4, 5, 4]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! [B, I] = sort (C, 2); %! X = [1, 2, 2; 1, 1, 3; -4, 2, 2; 1, 2, NaN; -2, 3, NaN; 1, 2, 3]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [1, 2, 3; 1, 3, 2; 2, 1, 3; 2, 1, 3; 1, 3, 2; 1, 3, 2]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! [B, I] = sort (C, 1, 'MissingPlacement', 'first'); %! X = [-2, NaN, NaN; 1, -4, 1; 1, 1, 2; 1, 2, 2; 2, 3, 2; 2, 3, 3]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [5, 5, 4; 1, 3, 2; 2, 4, 1; 6, 1, 3; 3, 2, 6; 4, 6, 5]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! [B, I] = sort (C, 2, 'MissingPlacement', 'first'); %! X = [1, 2, 2; 1, 1, 3; -4, 2, 2; NaN, 1, 2; NaN, -2, 3; 1, 2, 3]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [1, 2, 3; 1, 3, 2; 2, 1, 3; 3, 2, 1; 2, 1, 3; 1, 3, 2]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! [B1, I1] = sort (C, 1); %! [B2, I2] = sort (C, 1, 'MissingPlacement', 'last'); %! assert_equal (isequaln (B1, B2), true); %! assert_equal (I1, I2); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! [B1, I1] = sort (C, 2); %! [B2, I2] = sort (C, 2, 'MissingPlacement', 'last'); %! assert_equal (isequaln (B1, B2), true); %! assert_equal (I1, I2); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! [B, I] = sort (C, 'descend'); %! X = [2, NaN, NaN; 2, 3, 3; 1, 3, 2; 1, 2, 2; 1, 1, 2; -2, -4, 1]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [3, 5, 4; 4, 2, 5; 1, 6, 1; 2, 1, 3; 6, 4, 6; 5, 3, 2]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! [B, I] = sort (C, 1, 'descend'); %! X = [2, NaN, NaN; 2, 3, 3; 1, 3, 2; 1, 2, 2; 1, 1, 2; -2, -4, 1]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [3, 5, 4; 4, 2, 5; 1, 6, 1; 2, 1, 3; 6, 4, 6; 5, 3, 2]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! [B, I] = sort (C, 2, 'descend'); %! X = [2, 2, 1; 3, 1, 1; 2, 2, -4; NaN, 2, 1; NaN, 3, -2; 3, 2, 1]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [2, 3, 1; 2, 1, 3; 1, 3, 2; 3, 1, 2; 2, 3, 1; 2, 3, 1]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! [B1, I1] = sort (C, 1, 'descend'); %! [B2, I2] = sort (C, 1, 'descend', 'MissingPlacement', 'first'); %! assert_equal (isequaln (B1, B2), true); %! assert_equal (I1, I2); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! [B1, I1] = sort (C, 2, 'descend'); %! [B2, I2] = sort (C, 2, 'descend', 'MissingPlacement', 'first'); %! assert_equal (isequaln (B1, B2), true); %! assert_equal (I1, I2); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! [B, I] = sort (C, 1, 'descend', 'MissingPlacement', 'last'); %! X = [2, 3, 3; 2, 3, 2; 1, 2, 2; 1, 1, 2; 1, -4, 1; -2, NaN, NaN]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [3, 2, 5; 4, 6, 1; 1, 1, 3; 2, 4, 6; 6, 3, 2; 5, 5, 4]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, ... %! 3, 2]); %! [B, I] = sort (C, 2, 'descend', 'MissingPlacement', 'last'); %! X = [2, 2, 1; 3, 1, 1; 2, 2, -4; 2, 1, NaN; 3, -2, NaN; 3, 2, 1]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [2, 3, 1; 2, 1, 3; 1, 3, 2; 1, 2, 3; 3, 1, 2; 2, 3, 1]); %!test %! C = categorical ([1, 2; 1, 3; 2, 1; 2, 1], [3, 2, 1], {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! [B, I] = sort (C); %! X = categorical ([2, 3; 2, 2; 1, 1; 1, 1], [3, 2, 1], {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! assert_equal (isequaln (B, X), true); %! assert_equal (I, [3, 2; 4, 1; 1, 3; 2, 4]); %!test %! C = categorical ([1, 2; 1, 3; 2, 1; 2, 1], [3, 2, 1], {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! [B, I] = sort (C, 1); %! X = categorical ([2, 3; 2, 2; 1, 1; 1, 1], [3, 2, 1], {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! assert_equal (isequaln (B, X), true); %! assert_equal (I, [3, 2; 4, 1; 1, 3; 2, 4]); %!test %! C = categorical ([1, 2; 1, 3; 2, 1; 2, 1], [3, 2, 1], {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! [B, I] = sort (C, 2); %! X = categorical ([2, 1; 3, 1; 2, 1; 2, 1], [3, 2, 1], {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! assert_equal (isequaln (B, X), true); %! assert_equal (I, [2, 1; 2, 1; 1, 2; 1, 2]); %!test %! C = categorical ([1, 2; 1, 3; 2, NaN; 2, 1], [3, 2, 1], {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! [B, I] = sort (C, 'descend'); %! X = categorical ([1, NaN; 1, 1; 2, 2; 2, 3], [3, 2, 1], {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! assert_equal (isequaln (B, X), true); %! assert_equal (I, [1, 3; 2, 4; 3, 1; 4, 2]); %!test %! C = categorical ([1, 2; 1, 3; 2, NaN; 2, 1], [3, 2, 1], {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! [B, I] = sort (C, 1, 'descend'); %! X = categorical ([1, NaN; 1, 1; 2, 2; 2, 3], [3, 2, 1], {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! assert_equal (isequaln (B, X), true); %! assert_equal (I, [1, 3; 2, 4; 3, 1; 4, 2]); %!test %! C = categorical ([1, 2; 1, 3; 2, NaN; 2, 1], [3, 2, 1], {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! [B, I] = sort (C, 2, 'descend'); %! X = categorical ([1, 2; 1, 3; NaN, 2; 1, 2], [3, 2, 1], {'a', 'b', 'c'}, ... %! 'Ordinal', true); %! assert_equal (isequaln (B, X), true); %! assert_equal (I, [1, 2; 1, 2; 2, 1; 2, 1]); %!test %! X = [1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]; %! C = categorical (X, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! [B, I] = sort (C); %! Xs = [2, 3, 3; 2, 3, 2; 2, 2, 2; 1, 1, 2; 1, 4, 1; 1, NaN, NaN]; %! Cs = categorical (Xs, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! assert_equal (isequaln (B, Cs), true); %! assert_equal (I, [3, 2, 5; 4, 6, 1; 5, 1, 3; 1, 4, 6; 2, 3, 2; 6, 5, 4]); %!test %! X = [1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]; %! C = categorical (X, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! [B, I] = sort (C, 1); %! Xs = [2, 3, 3; 2, 3, 2; 2, 2, 2; 1, 1, 2; 1, 4, 1; 1, NaN, NaN]; %! Cs = categorical (Xs, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! assert_equal (isequaln (B, Cs), true); %! assert_equal (I, [3, 2, 5; 4, 6, 1; 5, 1, 3; 1, 4, 6; 2, 3, 2; 6, 5, 4]); %!test %! X = [1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]; %! C = categorical (X, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! [B, I] = sort (C, 2); %! Xs = [2, 2, 1; 3, 1, 1; 2, 2, 4; 2, 1, NaN; 3, 2, NaN; 3, 2, 1]; %! Cs = categorical (Xs, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! assert_equal (isequaln (B, Cs), true); %! assert_equal (I, [2, 3, 1; 2, 1, 3; 1, 3, 2; 1, 2, 3; 3, 1, 2; 2, 3, 1]); %!test %! X = [1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]; %! C = categorical (X, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! [B, I] = sort (C, 1, 'MissingPlacement', 'first'); %! Xs = [2, NaN, NaN; 2, 3, 3; 2, 3, 2; 1, 2, 2; 1, 1, 2; 1, 4, 1]; %! Cs = categorical (Xs, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! assert_equal (isequaln (B, Cs), true); %! assert_equal (I, [3, 5, 4; 4, 2, 5; 5, 6, 1; 1, 1, 3; 2, 4, 6; 6, 3, 2]); %!test %! X = [1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]; %! C = categorical (X, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! [B, I] = sort (C, 2, 'MissingPlacement', 'first'); %! Xs = [2, 2, 1; 3, 1, 1; 2, 2, 4; NaN, 2, 1; NaN, 3, 2; 3, 2, 1]; %! Cs = categorical (Xs, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! assert_equal (isequaln (B, Cs), true); %! assert_equal (I, [2, 3, 1; 2, 1, 3; 1, 3, 2; 3, 1, 2; 2, 3, 1; 2, 3, 1]); %!test %! X = [1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]; %! C = categorical (X, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! [B1, I1] = sort (C, 1); %! [B2, I2] = sort (C, 1, 'MissingPlacement', 'last'); %! assert_equal (isequaln (B1, B2), true); %! assert_equal (I1, I2); %!test %! X = [1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]; %! C = categorical (X, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! [B1, I1] = sort (C, 2); %! [B2, I2] = sort (C, 2, 'MissingPlacement', 'last'); %! assert_equal (isequaln (B1, B2), true); %! assert_equal (I1, I2); %!test %! X = [1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]; %! C = categorical (X, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! [B, I] = sort (C, 'descend'); %! Xs = [1, NaN, NaN; 1, 4, 1; 1, 1, 2; 2, 2, 2; 2, 3, 2; 2, 3, 3]; %! Cs = categorical (Xs, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! assert_equal (isequaln (B, Cs), true); %! assert_equal (I, [1, 5, 4; 2, 3, 2; 6, 4, 1; 3, 1, 3; 4, 2, 6; 5, 6, 5]); %!test %! X = [1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]; %! C = categorical (X, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! [B, I] = sort (C, 1, 'descend'); %! Xs = [1, NaN, NaN; 1, 4, 1; 1, 1, 2; 2, 2, 2; 2, 3, 2; 2, 3, 3]; %! Cs = categorical (Xs, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! assert_equal (isequaln (B, Cs), true); %! assert_equal (I, [1, 5, 4; 2, 3, 2; 6, 4, 1; 3, 1, 3; 4, 2, 6; 5, 6, 5]); %!test %! X = [1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]; %! C = categorical (X, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! [B, I] = sort (C, 2, 'descend'); %! Xs = [1, 2, 2; 1, 1, 3; 4, 2, 2; NaN, 1, 2; NaN, 2, 3; 1, 2, 3]; %! Cs = categorical (Xs, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! assert_equal (isequaln (B, Cs), true); %! assert_equal (I, [1, 2, 3; 1, 3, 2; 2, 1, 3; 3, 2, 1; 2, 1, 3; 1, 3, 2]); %!test %! X = [1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]; %! C = categorical (X, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! [B1, I1] = sort (C, 1, 'descend'); %! [B2, I2] = sort (C, 1, 'descend', 'MissingPlacement', 'first'); %! assert_equal (isequaln (B1, B2), true); %! assert_equal (I1, I2); %!test %! X = [1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]; %! C = categorical (X, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! [B1, I1] = sort (C, 2, 'descend'); %! [B2, I2] = sort (C, 2, 'descend', 'MissingPlacement', 'first'); %! assert_equal (isequaln (B1, B2), true); %! assert_equal (I1, I2); %!test %! X = [1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]; %! C = categorical (X, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! [B, I] = sort (C, 1, 'descend', 'MissingPlacement', 'last'); %! Xs = [1, 4, 1; 1, 1, 2; 1, 2, 2; 2, 3, 2; 2, 3, 3; 2, NaN, NaN]; %! Cs = categorical (Xs, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! assert_equal (isequaln (B, Cs), true); %! assert_equal (I, [1, 3, 2; 2, 4, 1; 6, 1, 3; 3, 2, 6; 4, 6, 5; 5, 5, 4]); %!test %! X = [1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]; %! C = categorical (X, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! [B, I] = sort (C, 2, 'descend', 'MissingPlacement', 'last'); %! Xs = [1, 2, 2; 1, 1, 3; 4, 2, 2; 1, 2, NaN; 2, 3, NaN; 1, 2, 3]; %! Cs = categorical (Xs, [3, 2, 1, 4], {'a', 'b', 'c', 'd'}, 'Ordinal', true); %! assert_equal (isequaln (B, Cs), true); %! assert_equal (I, [1, 2, 3; 1, 3, 2; 2, 1, 3; 2, 1, 3; 1, 3, 2; 1, 3, 2]); %!error ... %! sort (categorical, 'MissingPlacement', 'on') %!error ... %! sort (categorical ({'b'; 'a'}), 'Bogus', 1) %!error ... %! sort (categorical ({'b'; 'a'}), 0) %!error ... %! sort (categorical ({'b'; 'a'}), {1}) %!error ... %! sort (categorical ({'b'; 'a'}), 1, 2) %!error ... %! sort (categorical ({'b'; 'a'}), 'descend', 1) %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sortrows (C); %! assert_equal (B, categorical ([1, 2; 1, 3; 2, -4; 2, 1])); %! assert_equal (I, [1; 2; 3; 4]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sortrows (C, 1); %! assert_equal (B, categorical ([1, 2; 1, 3; 2, -4; 2, 1])); %! assert_equal (I, [1; 2; 3; 4]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sortrows (C, -1); %! assert_equal (B, categorical ([2, -4; 2, 1; 1, 2; 1, 3])); %! assert_equal (I, [3; 4; 1; 2]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sortrows (C, 2); %! assert_equal (B, categorical ([2, -4; 2, 1; 1, 2; 1, 3])); %! assert_equal (I, [3; 4; 1; 2]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sortrows (C, -2); %! assert_equal (B, categorical ([1, 3; 1, 2; 2, 1; 2, -4])); %! assert_equal (I, [2; 1; 4; 3]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sortrows (C, [1, -2]); %! assert_equal (B, categorical ([1, 3; 1, 2; 2, 1; 2, -4])); %! assert_equal (I, [2; 1; 4; 3]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sortrows (C, [-1, -2]); %! assert_equal (B, categorical ([2, 1; 2, -4; 1, 3; 1, 2])); %! assert_equal (I, [4; 3; 2; 1]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C); %! X = [1, 2, 2; 1, 3, 1; 1, 3, 2; 2, 1, NaN; 2, 4, 2; 2, NaN, 3]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [1; 2; 6; 4; 3; 5]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, 'MissingPlacement', 'first'); %! X = [1, 2, 2; 1, 3, 1; 1, 3, 2; 2, NaN, 3; 2, 1, NaN; 2, 4, 2]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [1; 2; 6; 5; 4; 3]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, 'MissingPlacement', 'last'); %! X = [1, 2, 2; 1, 3, 1; 1, 3, 2; 2, 1, NaN; 2, 4, 2; 2, NaN, 3]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [1; 2; 6; 4; 3; 5]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, [-1, -2], 'MissingPlacement', 'first'); %! X = [2, NaN, 3; 2, 4, 2; 2, 1, NaN; 1, 3, 1; 1, 3, 2; 1, 2, 2]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [5; 3; 4; 2; 6; 1]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, [-1, -2], 'MissingPlacement', 'last'); %! X = [2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 1; 1, 3, 2; 1, 2, 2]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [3; 4; 5; 2; 6; 1]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, 2, 'MissingPlacement', 'first'); %! X = [2, NaN, 3; 2, 1, NaN; 1, 2, 2; 1, 3, 1; 1, 3, 2; 2, 4, 2]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [5; 4; 1; 2; 6; 3]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, 2, 'MissingPlacement', 'last'); %! X = [2, 1, NaN; 1, 2, 2; 1, 3, 1; 1, 3, 2; 2, 4, 2; 2, NaN, 3]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [4; 1; 2; 6; 3; 5]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, -2, 'MissingPlacement', 'first'); %! X = [2, NaN, 3; 2, 4, 2; 1, 3, 1; 1, 3, 2; 1, 2, 2; 2, 1, NaN]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [5; 3; 2; 6; 1; 4]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, -2, 'MissingPlacement', 'last'); %! X = [2, 4, 2; 1, 3, 1; 1, 3, 2; 1, 2, 2; 2, 1, NaN; 2, NaN, 3]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [3; 2; 6; 1; 4; 5]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, [1, -2, 3]); %! X = [1, 3, 1; 1, 3, 2; 1, 2, 2; 2, NaN, 3; 2, 4, 2; 2, 1, NaN]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [2; 6; 1; 5; 3; 4]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, [-1, -2, 3]); %! X = [2, NaN, 3; 2, 4, 2; 2, 1, NaN; 1, 3, 1; 1, 3, 2; 1, 2, 2]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [5; 3; 4; 2; 6; 1]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, [1, -2], {'ascend', 'descend'}); %! X = [1, 3, 1; 1, 3, 2; 1, 2, 2; 2, NaN, 3; 2, 4, 2; 2, 1, NaN]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [2; 6; 1; 5; 3; 4]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, [1, -2], {'descend', 'ascend'}); %! X = [2, 1, NaN; 2, 4, 2; 2, NaN, 3; 1, 2, 2; 1, 3, 1; 1, 3, 2]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [4; 3; 5; 1; 2; 6]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, [-1, -2, 3], {'ascend', 'ascend', 'descend'}); %! X = [1, 2, 2; 1, 3, 2; 1, 3, 1; 2, 1, NaN; 2, 4, 2; 2, NaN, 3]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [1; 6; 2; 4; 3; 5]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, [1, -2, 3], {'ascend', 'ascend', 'descend'}); %! X = [1, 2, 2; 1, 3, 2; 1, 3, 1; 2, 1, NaN; 2, 4, 2; 2, NaN, 3]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [1; 6; 2; 4; 3; 5]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, [1, -2, 3], 'MissingPlacement', 'first'); %! X = [1, 3, 1; 1, 3, 2; 1, 2, 2; 2, NaN, 3; 2, 4, 2; 2, 1, NaN]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [2; 6; 1; 5; 3; 4]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, [-1, -2, 3], 'MissingPlacement', 'first'); %! X = [2, NaN, 3; 2, 4, 2; 2, 1, NaN; 1, 3, 1; 1, 3, 2; 1, 2, 2]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [5; 3; 4; 2; 6; 1]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, [1, -2, -3], 'MissingPlacement', 'first'); %! X = [1, 3, 2; 1, 3, 1; 1, 2, 2; 2, NaN, 3; 2, 4, 2; 2, 1, NaN]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [6; 2; 1; 5; 3; 4]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, [1, -2, 3], 'MissingPlacement', 'last'); %! X = [1, 3, 1; 1, 3, 2; 1, 2, 2; 2, 4, 2; 2, 1, NaN; 2, NaN, 3]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [2; 6; 1; 3; 4; 5]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, [-1, -2, 3], 'MissingPlacement', 'last'); %! X = [2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 1; 1, 3, 2; 1, 2, 2]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [3; 4; 5; 2; 6; 1]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = sortrows (C, [1, -2, -3], 'MissingPlacement', 'last'); %! X = [1, 3, 2; 1, 3, 1; 1, 2, 2; 2, 4, 2; 2, 1, NaN; 2, NaN, 3]; %! assert_equal (isequaln (B, categorical (X)), true); %! assert_equal (I, [6; 2; 1; 3; 4; 5]); %!test %! C = categorical ([1, 2, 3; 2, 3, 4], [4, 3, 2, 1], 'Ordinal', true); %! [B, I] = sortrows (C); %! out = categorical ([2, 3, 4; 1, 2, 3], [4, 3, 2, 1], 'Ordinal', true); %! assert_equal (B, out); %! assert_equal (I, [2; 1]); %!test %! C = categorical ([1, 2, 3; 2, 3, 4], [1, 2, 3, 4], 'Ordinal', true); %! [B, I] = sortrows (C); %! out = categorical ([1, 2, 3; 2, 3, 4], [1, 2, 3, 4], 'Ordinal', true); %! assert_equal (B, out); %! assert_equal (I, [1; 2]); %!test %! C = categorical ([1, 2, 3; 2, 3, 4; 1, 2, NaN], [1:4], 'Ordinal', true); %! [B, I] = sortrows (C); %! assert_equal (double (B), [1, 2, 3; 1, 2, NaN; 2, 3, 4]); %! assert_equal (I, [1; 3; 2]); %!test %! C = categorical ([1, 2, 3; 2, 3, 4; 1, 2, NaN], [1:4], 'Ordinal', true); %! [B, I] = sortrows (C, 'MissingPlacement', 'first'); %! assert_equal (double (B), [1, 2, NaN; 1, 2, 3; 2, 3, 4]); %! assert_equal (I, [3; 1; 2]); %!test %! C = categorical ([1, 2, 3; 2, 3, 4; 1, 2, NaN], [1:4], 'Ordinal', true); %! [B, I] = sortrows (C, 'descend'); %! assert_equal (double (B), [2, 3, 4; 1, 2, NaN; 1, 2, 3]); %! assert_equal (I, [2; 3; 1]); %!test %! C = categorical ([1, 2, 3; 2, 3, 4; 1, 2, NaN], [1:4], 'Ordinal', true); %! [B, I] = sortrows (C, 'descend', 'MissingPlacement', 'last'); %! assert_equal (double (B), [2, 3, 4; 1, 2, 3; 1, 2, NaN]); %! assert_equal (I, [2; 1; 3]); %!error ... %! sortrows (categorical (ones (2, 3, 4))) %!error ... %! sortrows (categorical (ones (2)), 'MissingPlacement', 2) %!error ... %! sortrows (categorical (1), 1, 'ascend', 2) %!error ... %! sortrows (categorical (1), 1.5) %!error ... %! sortrows (categorical (1), ones (2)) %!error ... %! sortrows (categorical (1), 2) %!error ... %! sortrows (categorical (1), {'a'}) %!error ... %! sortrows (categorical (1), 1, {'ascend', 'descend'}) %!error ... %! sortrows (categorical (1), {1}) %!error ... %! sortrows (categorical (1), {'ascend'}, 'ascend') %!error ... %! sortrows (categorical (1), {'ascend'}, 1) %!error ... %! sortrows (categorical (1), 1, 1) %!error ... %! sortrows (categorical (1), 1, {'a'}) %!error ... %! sortrows (categorical (1), 1, {'ascend', 'descend'}) %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = topkrows (C, 2); %! assert_equal (B, categorical ([2, 1; 2, -4])); %! assert_equal (I, [4; 3]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = topkrows (C, 5); %! assert_equal (B, sortrows (C, 'descend', 'MissingPlacement', 'last')); %! assert_equal (I, [4; 3; 2; 1]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = topkrows (C, 2, 'ascend'); %! assert_equal (B, categorical ([1, 2; 1, 3])); %! assert_equal (I, [1; 2]); %!test # a column list ranks in descending order, as a bare K does %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = topkrows (C, 2, 2); %! assert_equal (B, categorical ([1, 3; 1, 2])); %! assert_equal (I, [2; 1]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = topkrows (C, 2, 2, 'ascend'); %! assert_equal (B, categorical ([2, -4; 2, 1])); %! assert_equal (I, [3; 4]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = topkrows (C, 3, [1, 2], {'ascend', 'descend'}); %! assert_equal (B, categorical ([1, 3; 1, 2; 2, 1])); %! assert_equal (I, [2; 1; 4]); %!test # undefined rows are not ranked and go last whatever the direction %! C = categorical ({'b','a'; 'a','c'; 'b','c'; '','b'; 'a','a'}, ... %! {'a','b','c'}, 'Ordinal', true); %! [B, I] = topkrows (C, 5); %! assert_equal (I, [3; 1; 2; 5; 4]); %! assert_equal (isundefined (B(:, 1))', logical ([0, 0, 0, 0, 1])); %! [B, I] = topkrows (C, 5, 1, 'ascend'); %! assert_equal (I, [2; 5; 1; 3; 4]); %! assert_equal (isundefined (B(:, 1))', logical ([0, 0, 0, 0, 1])); %!test # an undefined element outside the sort columns does not demote its row %! C = categorical ({'b','a'; 'a','c'; 'b','c'; '','b'; 'a','a'}, ... %! {'a','b','c'}, 'Ordinal', true); %! [B, I] = topkrows (C, 5, 2); %! assert_equal (I, [2; 3; 4; 1; 5]); %!test # undefined rows only surface once K passes the ranked rows %! C = categorical ({'b','a'; 'a','c'; 'b','c'; '','b'; 'a','a'}, ... %! {'a','b','c'}, 'Ordinal', true); %! [B, I] = topkrows (C, 4); %! assert_equal (I, [3; 1; 2; 5]); %! assert_equal (any (isundefined (B(:, 1))), false); %!test # 'MissingPlacement' is an Octave extension; the default is 'last' %! C = categorical ({'b','a'; 'a','c'; 'b','c'; '','b'; 'a','a'}, ... %! {'a','b','c'}, 'Ordinal', true); %! [~, I] = topkrows (C, 5, 'MissingPlacement', 'last'); %! assert_equal (I, [3; 1; 2; 5; 4]); %! [~, I] = topkrows (C, 5, 'MissingPlacement', 'first'); %! assert_equal (I, [4; 3; 1; 2; 5]); %! [~, I] = topkrows (C, 5, 'MissingPlacement', 'auto'); %! assert_equal (I, [4; 3; 1; 2; 5]); %! [~, I] = topkrows (C, 5, 1, 'ascend', 'MissingPlacement', 'first'); %! assert_equal (I, [4; 2; 5; 1; 3]); %!test # the array need not be ordinal %! C = categorical ({'b','a'; 'a','c'}); %! [B, I] = topkrows (C, 1); %! assert_equal (cellstr (B), {'b', 'a'}); %! assert_equal (I, 1); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = topkrows (C, 3); %! assert_equal (double (B), [2, 4, 2; 2, 1, NaN; 2, NaN, 3]); %! assert_equal (I, [3; 4; 5]); %!test %! C = categorical ([1, 2, 2; 1, 3, 1; 2, 4, 2; 2, 1, NaN; 2, NaN, 3; 1, 3, 2]); %! [B, I] = topkrows (C, 3, 'ascend'); %! assert_equal (B, categorical ([1, 2, 2; 1, 3, 1; 1, 3, 2])); %! assert_equal (I, [1; 2; 6]); %!test %! C = categorical ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (size (topkrows (C, 0)), [0, 2]); %! assert_equal (categories (topkrows (C, 2)), categories (C)); %! assert_equal (size (topkrows (categorical ([]), 2)), [0, 0]); %! assert_equal (size (topkrows (categorical ([3, 1, 2]), 1)), [1, 3]); %!test %! C = categorical ([3; 1; 2]); %! [B, I] = topkrows (C, 2); %! assert_equal (B, categorical ([3; 2])); %! assert_equal (I, [1; 3]); %!error ... %! topkrows (categorical (1)) %!error ... %! topkrows (categorical (1), 2.5) %!error ... %! topkrows (categorical (1), -1) %!error ... %! topkrows (categorical (1), [2, 3]) %!error ... %! topkrows (categorical ([1, 2; 3, 4]), 1, -1) %!error ... %! topkrows (categorical ([1, 2; 3, 4]), 1, 0) %!error ... %! topkrows (categorical ([1, 2; 3, 4]), 1, 5) %!error ... %! topkrows (categorical ([1, 2; 3, 4]), 1, 1.5) %!error ... %! topkrows (categorical ([1, 2; 3, 4]), 1, 1, 'bogus') %!error ... %! topkrows (categorical ([1, 2; 3, 4]), 1, 'MissingPlacement', 'bogus') %!error ... %! topkrows (categorical ([1, 2; 3, 4]), 1, 'MissingPlacement') %!error ... %! topkrows (categorical (reshape (1:8, 2, 2, 2)), 1) %!assert_equal (unique (categorical ([1, 2, 2, 3, 4, 4])), categorical ([1:4])) %!assert_equal (unique (categorical ([1, 2, 2, 3, 4, ... %! 4]')), categorical ([1:4]')) %!test %! C = categorical ([1, 2, 2, 3, 4, 4]); %! [c, i, j] = unique (C); %! assert_equal (c, C(i)); %! assert_equal (c(j), C); %!assert_equal (unique (categorical ([1, 4, 2, 2, 3]), ... %! 'sorted'), categorical (1:4)) %!test %! C = categorical ([1, 4, 2, 2, 3]); %! [c, i, j] = unique (C, 'sorted'); %! assert_equal (c, categorical (1:4)); %! assert_equal (c, C(i)); %! assert_equal (c(j), C); %!test %! C = categorical ([1, 4, 2, 2, 3]); %! [c, i, j] = unique (C, 'stable'); %! assert_equal (c, categorical ([1, 4, 2, 3])); %! assert_equal (c, C(i)); %! assert_equal (c(j), C); %!test %! C = categorical ([1, 4, 2, 2, 3]); %! [c, i, j] = unique (C, 'sorted', 'last'); %! assert_equal (i, [1; 4; 5; 2]); %! assert_equal (c, C(i)); %! assert_equal (c(j), C); %!test %! C = categorical ([1, 4, 2, 2, 3]); %! [c, i, j] = unique (C, 'stable', 'last'); %! assert_equal (i, [1; 2; 4; 5]); %! assert_equal (c, C(i)); %! assert_equal (c(j), C); %!test %! C = categorical ([3, 1, 3; 3, 3, 1; 1, 3, 3; 3, 2, 3; 2, 3, 3; ... %! 1, 1, 3; 1, 2, 3; 2, 3, 2; 3, 3, 2; 3, 3, 1]); %! [c, i, j] = unique (C(:,[1:2]), 'rows'); %! assert_equal (c, categorical ([1, 1; 1, 2; 1, 3; 2, 3; 3, 1; 3, 2; 3, 3])); %! assert_equal (i, [6; 7; 3; 5; 1; 4; 2]); %! assert_equal (j, [5; 7; 3; 6; 4; 1; 2; 4; 7; 7]); %! assert_equal (C(i,[1:2]), categorical ([1, 1; 1, 2; 1, 3; 2, 3; 3, 1; 3, ... %! 2; 3, 3])); %!test %! [B, ia, ib] = unique (categorical ([1, 2; 2, 3; 1, 2]), 'rows', 'sorted', ... %! 'first'); %! assert_equal (B, categorical ([1, 2; 2, 3])); %! assert_equal (ia, [1; 2]); %! assert_equal (ib, [1; 2; 1]); %!test %! [B, ia, ib] = unique (categorical ([1, 2; 2, 3; 1, 2]), 'rows', 'sorted', ... %! 'last'); %! assert_equal (B, categorical ([1, 2; 2, 3])); %! assert_equal (ia, [3; 2]); %! assert_equal (ib, [1; 2; 1]); %!test %! [B, ia, ib] = unique (categorical ([1, 2; 2, 3; 1, 2]), 'rows', 'stable', ... %! 'first'); %! assert_equal (B, categorical ([1, 2; 2, 3])); %! assert_equal (ia, [1; 2]); %! assert_equal (ib, [1; 2; 1]); %!test %! [B, ia, ib] = unique (categorical ([1, 2; 2, 3; 1, 2]), 'rows', 'stable', ... %! 'last'); %! assert_equal (B, categorical ([2, 3; 1, 2])); %! assert_equal (ia, [2; 3]); %! assert_equal (ib, [2; 1; 2]); %!error ... %! unique (categorical ([1:3]), 'legacy') %!test %! A = categorical ([7 1 7 7 4]); %! B = categorical ([7 0 4 4 0]); %! [C, ixA, ixB] = intersect (A, B); %! assert_equal (C, categorical ([4, 7])); %! assert_equal (ixA, [5; 1]); %! assert_equal (ixB, [3; 1]); %!test %! A = categorical ([7 1 7 7 4]); %! B = categorical ([7 0 4 4 0]); %! [C, ixA, ixB] = intersect (A, B, 'sorted'); %! assert_equal (C, categorical ([4, 7])); %! assert_equal (ixA, [5; 1]); %! assert_equal (ixB, [3; 1]); %!test %! A = categorical ([7 1 7 7 4]); %! B = categorical ([7 0 4 4 0]); %! [C, ixA, ixB] = intersect (A, B, 'stable'); %! assert_equal (C, categorical ([7, 4])); %! assert_equal (ixA, [1; 5]); %! assert_equal (ixB, [1; 3]); %!test %! A = categorical ([2, 2, 2; 0, 0, 1; 1, 2, 3; 1, 1, 1]); %! B = categorical ([1, 2, 3; 2, 2, 2; 2, 2, 0]); %! [C, ixA, ixB] = intersect (A, B, 'rows'); %! assert_equal (C, categorical ([1, 2, 3; 2, 2, 2])); %! assert_equal (ixA, [3; 1]); %! assert_equal (ixB, [1; 2]); %!test %! A = categorical ([2, 2, 2; 0, 0, 1; 1, 2, 3; 1, 1, 1]); %! B = categorical ([1, 2, 3; 2, 2, 2; 2, 2, 0]); %! [C, ixA, ixB] = intersect (A, B, 'sorted', 'rows'); %! assert_equal (C, categorical ([1, 2, 3; 2, 2, 2])); %! assert_equal (ixA, [3; 1]); %! assert_equal (ixB, [1; 2]); %!test %! A = categorical ([2, 2, 2; 0, 0, 1; 1, 2, 3; 1, 1, 1]); %! B = categorical ([1, 2, 3; 2, 2, 2; 2, 2, 0]); %! [C, ixA, ixB] = intersect (A, B, 'stable', 'rows'); %! assert_equal (C, categorical ([2, 2, 2; 1, 2, 3])); %! assert_equal (ixA, [1; 3]); %! assert_equal (ixB, [2; 1]); %!assert_equal (intersect (categorical ([1, 2]), '2'), categorical (2, [1, 2])) %!assert_equal (intersect (categorical ([1, 2]), {'1'}), categorical (1, [1, ... %! 2])) %!assert_equal (intersect (categorical ([1, 2]), ... %! string ('2')), categorical (2, [1, 2])) %!assert_equal (intersect ('2', categorical ([1, 2])), categorical (2, [1, 2])) %!assert_equal (intersect ({'1'}, categorical ([1, 2])), categorical (1, [1, ... %! 2])) %!assert_equal (intersect (string ('2'), categorical ([1, ... %! 2])), categorical (2, [1, 2])) %!error ... %! intersect (categorical, categorical, 'legacy') %!error ... %! intersect (categorical, 1) %!error ... %! intersect (categorical, categorical (1, 1, 'Ordinal', true)) %!error ... %! intersect (categorical (1, [1, 2], 'Ordinal', true), categorical (1, 1, ... %! 'Ordinal', true)) %!test %! A = categorical ([3, 6, 2, 1, 5, 1, 1]); %! B = categorical ([2, 4, 6]); %! [C, index] = setdiff (A, B); %! assert_equal (C, categorical ([1, 3, 5])); %! assert_equal (index, [4; 1; 5]); %!test %! A = categorical ([3, 6, 2, 1, 5, 1, 1]); %! B = categorical ([2, 4, 6]); %! [C, index] = setdiff (A, B, 'sorted'); %! assert_equal (C, categorical ([1, 3, 5])); %! assert_equal (index, [4; 1; 5]); %!test %! A = categorical ([3, 6, 2, 1, 5, 1, 1]); %! B = categorical ([2, 4, 6]); %! [C, index] = setdiff (A, B, 'stable'); %! assert_equal (C, categorical ([3, 1, 5])); %! assert_equal (index, [1; 4; 5]); %!test %! A = categorical ([7, 9, 7; 0, 0, 0; 7, 9, 7; 5, 5, 5; 1, 4, 5]); %! B = categorical ([0, 0, 0; 5, 5, 5]); %! [C, index] = setdiff (A, B, 'rows'); %! assert_equal (C, categorical ([1, 4, 5; 7, 9, 7])); %! assert_equal (index, [5; 1]); %!test %! A = categorical ([7, 9, 7; 0, 0, 0; 7, 9, 7; 5, 5, 5; 1, 4, 5]); %! B = categorical ([0, 0, 0; 5, 5, 5]); %! [C, index] = setdiff (A, B, 'sorted', 'rows'); %! assert_equal (C, categorical ([1, 4, 5; 7, 9, 7])); %! assert_equal (index, [5; 1]); %!test %! A = categorical ([7, 9, 7; 0, 0, 0; 7, 9, 7; 5, 5, 5; 1, 4, 5]); %! B = categorical ([0, 0, 0; 5, 5, 5]); %! [C, index] = setdiff (A, B, 'stable', 'rows'); %! assert_equal (C, categorical ([7, 9, 7; 1, 4, 5])); %! assert_equal (index, [1; 5]); %!assert_equal (setdiff (categorical ([1, 2]), '2'), categorical (1, [1, 2])) %!assert_equal (setdiff (categorical ([1, 2]), {'1'}), categorical (2, [1, 2])) %!assert_equal (setdiff (categorical ([1, 2]), ... %! string ('2')), categorical (1, [1, 2])) %!assert_equal (size (setdiff (categorical ([1, 2]), string ({'2', ... %! 1}))), [1, 0]) %!assert_equal (class (setdiff (categorical ([1, 2]), string ({'2', ... %! 1}))), 'categorical') %!assert_equal (size (setdiff ('2', categorical ([1, 2]))), [1, 0]) %!assert_equal (class (setdiff ('2', categorical ([1, 2]))), 'categorical') %!assert_equal (size (setdiff ({'1'}, categorical ([1, 2]))), [1, 0]) %!assert_equal (class (setdiff ({'1'}, categorical ([1, 2]))), 'categorical') %!assert_equal (size (setdiff (string ({'1', '2'}), categorical ([1, ... %! 2]))), [1, 0]) %!assert_equal (class (setdiff (string ({'1', '2'}), categorical ([1, ... %! 2]))), 'categorical') %!error ... %! setdiff (categorical, categorical, 'legacy') %!error ... %! setdiff (categorical, 1) %!error ... %! setdiff (categorical, categorical (1, 1, 'Ordinal', true)) %!error ... %! setdiff (categorical (1, [1, 2], 'Ordinal', true), categorical (1, 1, ... %! 'Ordinal', true)) %!test %! A = categorical ([5, 1, 3, 3, 3]); %! B = categorical ([4, 1, 2]); %! [C, ixA, ixB] = setxor (A, B); %! assert_equal (C, categorical ([2, 3, 4, 5])); %! assert_equal (ixA, [3; 1]); %! assert_equal (ixB, [3; 1]); %!test %! A = categorical ([5, 1, 3, 3, 3]); %! B = categorical ([4, 1, 2]); %! [C, ixA, ixB] = setxor (A, B, 'sorted'); %! assert_equal (C, categorical ([2, 3, 4, 5])); %! assert_equal (ixA, [3; 1]); %! assert_equal (ixB, [3; 1]); %!test %! A = categorical ([5, 1, 3, 3, 3]); %! B = categorical ([4, 1, 2]); %! [C, ixA, ixB] = setxor (A, B, 'stable'); %! assert_equal (C, categorical ([5, 3, 4, 2])); %! assert_equal (ixA, [1; 3]); %! assert_equal (ixB, [1; 3]); %!test %! A = categorical ([7, 8, 9; 7, 7, 1; 7, 7, 1; 1, 2, 3; 4, 5, 6]); %! B = categorical ([1, 2, 3; 4, 5, 6; 7, 7, 2]); %! [C, ixA, ixB] = setxor (A, B, 'rows'); %! assert_equal (C, categorical ([7, 7, 1; 7, 7, 2; 7, 8, 9])); %! assert_equal (ixA, [2; 1]); %! assert_equal (ixB, [3]); %!test %! A = categorical ([7, 8, 9; 7, 7, 1; 7, 7, 1; 1, 2, 3; 4, 5, 6]); %! B = categorical ([1, 2, 3; 4, 5, 6; 7, 7, 2]); %! [C, ixA, ixB] = setxor (A, B, 'sorted', 'rows'); %! assert_equal (C, categorical ([7, 7, 1; 7, 7, 2; 7, 8, 9])); %! assert_equal (ixA, [2; 1]); %! assert_equal (ixB, [3]); %!test %! A = categorical ([7, 8, 9; 7, 7, 1; 7, 7, 1; 1, 2, 3; 4, 5, 6]); %! B = categorical ([1, 2, 3; 4, 5, 6; 7, 7, 2]); %! [C, ixA, ixB] = setxor (A, B, 'stable', 'rows'); %! assert_equal (C, categorical ([7, 8 ,9; 7, 7, 1; 7, 7, 2])); %! assert_equal (ixA, [1; 2]); %! assert_equal (ixB, [3]); %!assert_equal (setxor (categorical ([1, 2]), '2'), categorical (1, [1, 2])) %!assert_equal (setxor (categorical ([1, 2]), {'1'}), categorical (2, [1, 2])) %!assert_equal (setxor (categorical ([1, 2]), string ('2')), categorical (1, ... %! [1, 2])) %!assert_equal (size (setxor (categorical ([1, 2]), string ({'2', ... %! '1'}))), [1, 0]) %!assert_equal (class (setxor (categorical ([1, 2]), string ({'2', ... %! '1'}))), 'categorical') %!assert_equal (setxor ('2', categorical ([1, 2])), categorical (1, [1, 2])) %!assert_equal (setxor ({'1'}, categorical ([1, 2])), categorical (2, [1, 2])) %!assert_equal (setxor (string ('2'), categorical ([1, 2])), categorical (1, ... %! [1, 2])) %!error ... %! setxor (categorical, categorical, 'legacy') %!error ... %! setxor (categorical, 1) %!error ... %! setxor (categorical, categorical (1, 1, 'Ordinal', true)) %!error ... %! setxor (categorical (1, [1, 2], 'Ordinal', true), categorical (1, 1, ... %! 'Ordinal', true)) %!test %! A = categorical ([5, 7, 1]); %! B = categorical ([3, 1, 1]); %! [C, ixA, ixB] = union (A, B); %! assert_equal (C, categorical ([1, 3, 5, 7])); %! assert_equal (ixA, [3; 1; 2]); %! assert_equal (ixB, 1); %!test %! A = categorical ([5, 7, 1]); %! B = categorical ([3, 1, 1]); %! [C, ixA, ixB] = union (A, B, 'sorted'); %! assert_equal (C, categorical ([1, 3, 5, 7])); %! assert_equal (ixA, [3; 1; 2]); %! assert_equal (ixB, 1); %!test %! A = categorical ([5, 7, 1]); %! B = categorical ([3, 1, 1]); %! [C, ixA, ixB] = union (A, B, 'stable'); %! assert_equal (C, categorical ([5, 7, 1, 3])); %! assert_equal (ixA, [1; 2; 3]); %! assert_equal (ixB, 1); %!test %! A = categorical ([2, 2, 2; 0, 0, 1]); %! B = categorical ([1, 2, 3; 2, 2, 2; 2, 2, 2]); %! [C, ixA, ixB] = union (A, B, 'rows'); %! assert_equal (C, categorical ([0, 0, 1; 1, 2, 3; 2, 2, 2])); %! assert_equal (ixA, [2; 1]); %! assert_equal (ixB, 1); %!test %! A = categorical ([2, 2, 2; 0, 0, 1]); %! B = categorical ([1, 2, 3; 2, 2, 2; 2, 2, 2]); %! [C, ixA, ixB] = union (A, B, 'sorted', 'rows'); %! assert_equal (C, categorical ([0, 0, 1; 1, 2, 3; 2, 2, 2])); %! assert_equal (ixA, [2; 1]); %! assert_equal (ixB, 1); %!test %! A = categorical ([2, 2, 2; 0, 0, 1]); %! B = categorical ([1, 2, 3; 2, 2, 2; 2, 2, 2]); %! [C, ixA, ixB] = union (A, B, 'stable', 'rows'); %! assert_equal (C, categorical ([2, 2, 2; 0, 0, 1; 1, 2, 3])); %! assert_equal (ixA, [1; 2]); %! assert_equal (ixB, 1); %!assert_equal (union (categorical ([1, 2]), '2'), categorical ([1, 2], [1, 2])) %!assert_equal (union (categorical ([1, 2]), {'1'}), categorical ([1, 2], ... %! [1, 2])) %!assert_equal (union (categorical ([1, 2]), string ('3')), categorical ([1, ... %! 2, 3])) %!assert_equal (union ('2', categorical ([1, 2])), categorical ([1, 2])) %!assert_equal (union ({'1'}, categorical ([1, 2])), categorical ([1, 2])) %!assert_equal (union (string ('4'), categorical ([1, 2])), categorical ([1, ... %! 2, 4])) %!assert_equal (union (string ({'4', '5'}), categorical ([1, ... %! 2])), categorical ([1, 2, 4, 5])) %!error ... %! union (categorical, categorical, 'legacy') %!error ... %! union (categorical, 1) %!error ... %! union (categorical, categorical (1, 1, 'Ordinal', true)) %!error ... %! union (categorical (1, [1, 2], 'Ordinal', true), categorical (1, 1, ... %! 'Ordinal', true)) ################################################################################ ## ** Array Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'cat' 'horzcat' 'vertcat' 'repmat' ## ## 'repelem' 'repelems' 'reshape' 'circshift' ## ## 'permute' 'ipermute' 'transpose' 'ctranspose' ## ## ## ################################################################################ ## Test 'cat' -- a non-scalar missing operand keeps its own shape. 'promote' ## sized it from a fresh 'missing', always 1x1, so elements vanished with no ## error and the two operand orders disagreed. %!assert_equal (size ([categorical(NaN(1, 3)), repmat(missing, 1, 3)]), [1, 6]) %!assert_equal (size ([repmat(missing, 1, 3), categorical(NaN(1, 3))]), [1, 6]) %!assert_equal (size ([categorical(NaN(1, 1)); repmat(missing, 3, 1)]), [4, 1]) %!assert_equal (size ([categorical(NaN(2, 3)), repmat(missing, 2, 3)]), [2, 6]) %!assert_equal (ismissing ([categorical({'a'}), repmat(missing, 1, 2)]), ... %! [false, true, true]) %!assert_equal (size (cat (1, categorical, categorical)), [0, 0]) %!assert_equal (size (cat (2, categorical, categorical)), [0, 0]) %!assert_equal (size (cat (1, categorical (ones (1, 0)), ... %! categorical (ones (1, 0)))), [2, 0]) %!assert_equal (size (cat (2, categorical (ones (0, 1)), ... %! categorical (ones (0, 1)))), [0, 2]) %!test %! C = cat (1, categorical (1), categorical ([1; 2])); %! assert_equal (categories (C), {'1'; '2'}); %! assert_equal (C, categorical ([1; 1; 2])); %!test %! C = cat (2, categorical (1), categorical ([1, 2])); %! assert_equal (categories (C), {'1'; '2'}); %! assert_equal (C, categorical ([1, 1, 2])); %!test %! C = cat (3, categorical (1), categorical (2)); %! assert_equal (categories (C), {'1'; '2'}); %! assert_equal (C, categorical (reshape ([1, 2], 1, 1, 2))); %!test %! C = cat (2, {'1', '3'}, categorical ([2, 1])); %! assert_equal (categories (C), {'1'; '2'; '3'}); %! assert_equal (C, categorical ([1, 3, 2, 1])); %! assert_equal (double (C), [1, 3, 2, 1]); %!test %! C = cat (2, '1', categorical ([2, 1])); %! assert_equal (categories (C), {'1'; '2'}); %! assert_equal (C, categorical ([1, 2, 1])); %! assert_equal (double (C), [1, 2, 1]); %!test %! C = cat (2, categorical ([2, 1]), '1'); %! assert_equal (categories (C), {'1'; '2'}); %! assert_equal (C, categorical ([2, 1, 1])); %! assert_equal (double (C), [2, 1, 1]); %!test %! C = cat (1, categorical ([2, 1]), {'1', '3'}); %! assert_equal (categories (C), {'1'; '2'; '3'}); %! assert_equal (C, categorical ([2, 1; 1, 3])); %! assert_equal (double (C), [2, 1; 1, 3]); %!test %! C = cat (1, categorical ([2; 1]), '13'); %! assert_equal (categories (C), {'1'; '2'; '13'}); %! assert_equal (C, categorical ([2; 1; 13])); %! assert_equal (double (C), [2; 1; 3]); %!test %! C = cat (2, categorical ([2, 1]), '13'); %! assert_equal (categories (C), {'1'; '2'; '13'}); %! assert_equal (C, categorical ([2, 1, 13])); %! assert_equal (double (C), [2, 1, 3]); %!test %! C = cat (1, '13', categorical ([2; 1])); %! assert_equal (categories (C), {'1'; '2'; '13'}); %! assert_equal (C, categorical ([13; 2; 1])); %! assert_equal (double (C), [3; 2; 1]); %!test %! C = cat (1, categorical ([3; 1]), categorical ([2; 1])); %! assert_equal (categories (C), {'1'; '3'; '2'}); %! assert_equal (C, categorical ([3; 1; 2; 1])); %! assert_equal (double (C), [2; 1; 3; 1]); %!test %! C = cat (2, categorical ([3, 1]), categorical ([2, 1])); %! assert_equal (categories (C), {'1'; '3'; '2'}); %! assert_equal (C, categorical ([3, 1, 2, 1])); %! assert_equal (double (C), [2, 1, 3, 1]); %!test %! C = cat (2, categorical (1, [1, 2], 'Ordinal', true), ... %! categorical ([1, 2, 2], [1, 2], 'Ordinal', true)); %! assert_equal (categories (C), {'1'; '2'}); %! assert_equal (C, categorical ([1, 1, 2, 2], [1, 2], 'Ordinal', true)); %! assert_equal (double (C), [1, 1, 2, 2]); %!test %! C = cat (2, categorical (1, [2, 1], 'Ordinal', true), ... %! categorical ([1, 2, 2], [2, 1], 'Ordinal', true)); %! assert_equal (categories (C), {'2'; '1'}); %! assert_equal (C, categorical ([1, 1, 2, 2], [2, 1], 'Ordinal', true)); %! assert_equal (double (C), [2, 2, 1, 1]); %!test %! C = cat (1, categorical ([1, 2], [1, 2], 'Ordinal', true), ... %! categorical ([2, 2], [1, 2], 'Ordinal', true)); %! assert_equal (categories (C), {'1'; '2'}); %! assert_equal (C, categorical ([1, 2; 2, 2], [1, 2], 'Ordinal', true)); %! assert_equal (double (C), [1, 2; 2, 2]); %!test %! C = cat (1, categorical ([1, 1], [2, 1], 'Ordinal', true), ... %! categorical ([1, 2], [2, 1], 'Ordinal', true)); %! assert_equal (categories (C), {'2'; '1'}); %! assert_equal (C, categorical ([1, 1; 1, 2], [2, 1], 'Ordinal', true)); %! assert_equal (double (C), [2, 2; 2, 1]); %!test %! C = cat (2, categorical (1, [2, 1], 'Protected', true), ... %! categorical ([1, 2, 2], [1, 2], 'Protected', true)); %! assert_equal (categories (C), {'2'; '1'}); %! assert_equal (C, categorical ([1, 1, 2, 2], [2, 1], 'Protected', true)); %! assert_equal (double (C), [2, 2, 1, 1]); %!test %! C = cat (2, categorical (1, [1, 2], 'Protected', true), ... %! categorical ([1, 2, 2], [1, 2], 'Protected', true)); %! assert_equal (categories (C), {'1'; '2'}); %! assert_equal (C, categorical ([1, 1, 2, 2], [2, 1], 'Protected', true)); %! assert_equal (double (C), [1, 1, 2, 2]); ## Text and missing values join a categorical array, and empties add nothing %!assert_equal (vertcat (categorical ({'lo'}), 'x'), categorical ({'lo'; 'x'})) %!assert_equal (vertcat (categorical ({'lo'}), {'x'}), ... %! categorical ({'lo'; 'x'})) %!assert_equal (vertcat (categorical ({'lo'}), string ('x')), ... %! categorical ({'lo'; 'x'})) %!assert_equal (ismissing (vertcat (categorical ({'lo'}), missing)), ... %! [false; true]) %!assert_equal (vertcat (categorical ({'lo'}), []), categorical ({'lo'})) %!assert_equal (vertcat (categorical ({'lo'}), zeros (0, 1)), ... %! categorical ({'lo'})) %!error ... %! cat (0, categorical, categorical) %!error ... %! cat (1, categorical (ones (2, 3, 4)), categorical (ones (1, 0))) %!error ... %! cat (1, categorical (ones (0, 1)), categorical (ones (1, 0))) %!error ... %! cat (1, categorical (1, 1, 'Ordinal', true), categorical (1, 2, ... %! 'Ordinal', true)) %!error ... %! cat (1, categorical (1, 1, 'Ordinal', true), categorical (1, 1, ... %! 'Ordinal', false)) %!error ... %! cat (1, categorical (1, 1, 'Ordinal', true), {'1'}) %!error ... %! cat (1, categorical (1, 1, 'Protected', true), categorical (1, 2, ... %! 'Protected', true)) %!error ... %! vertcat (categorical ({'lo'}), 1) %!error ... %! vertcat (1, categorical ({'lo'})) %!error ... %! vertcat (categorical ({'lo'}), NaN) %!error ... %! vertcat (categorical ({'lo'}), int8 (1)) %!error ... %! vertcat (categorical ({'lo'}), true) %!error ... %! vertcat (categorical ({'lo'}), datetime (2020, 1, 1)) %!error ... %! vertcat (categorical ({'lo'}), ['ab'; 'cd']) %!test %! C = [categorical(3), categorical([4, 2]), categorical([0, 1, 0])]; %! assert_equal (C, categorical ([3, 4, 2, 0, 1, 0])); %! assert_equal (categories (C), {'3'; '2'; '4'; '0'; '1'}); %! assert_equal (double (C), [1, 3, 2, 4, 5, 4]); %!test %! C = [{'1', '2'}, categorical(3), categorical([4, 2]), categorical([0, 1, ... %! 0])]; %! assert_equal (C, categorical ([1, 2, 3, 4, 2, 0, 1, 0])); %! assert_equal (categories (C), {'3'; '1'; '2'; '4'; '0'}); %! assert_equal (double (C), [2, 3, 1, 4, 3, 5, 2, 5]); %!test %! C = [{'5', '3'}, categorical(3), categorical([4, 2]), '8', ... %! categorical([0, 1, 0])]; %! assert_equal (C, categorical ([5, 3, 3, 4, 2, 8, 0, 1, 0])); %! assert_equal (categories (C), {'3'; '5'; '2'; '4'; '8'; '0'; '1'}); %! assert_equal (double (C), [2, 1, 1, 4, 3, 5, 6, 7, 6]); %!test %! C = [categorical(3), categorical([4, 2]), {'1', '3'}, categorical([0, 1, ... %! 0])]; %! assert_equal (C, categorical ([3, 4, 2, 1, 3, 0, 1, 0])); %! assert_equal (categories (C), {'3'; '2'; '4'; '1'; '0'}); %! assert_equal (double (C), [1, 3, 2, 4, 1, 5, 4, 5]); %!test %! C = [{'5', '2'}, categorical(3), categorical([4, 2]), '8', ... %! categorical([0, 1, 0])]; %! assert_equal (C, categorical ([5, 2, 3, 4, 2, 8, 0, 1, 0])); %! assert_equal (categories (C), {'3'; '2'; '5'; '4'; '8'; '0'; '1'}); %! assert_equal (double (C), [3, 2, 1, 4, 2, 5, 6, 7, 6]); %!test %! C = [{'5'}, {'2'}, categorical(3), categorical([4, 2]), '8', ... %! categorical([0, 1, 0])]; %! assert_equal (C, categorical ([5, 2, 3, 4, 2, 8, 0, 1, 0])); %! assert_equal (categories (C), {'3'; '5'; '2'; '4'; '8'; '0'; '1'}); %! assert_equal (double (C), [2, 3, 1, 4, 3, 5, 6, 7, 6]); %!test %! C = [{'5', '2'}, {'8'}, categorical(3), categorical([4, 2]), '8', ... %! categorical([0, 1, 0])]; %! assert_equal (C, categorical ([5, 2, 8, 3, 4, 2, 8, 0, 1, 0])); %! assert_equal (categories (C), {'3'; '2'; '5'; '8'; '4'; '0'; '1'}); %! assert_equal (double (C), [3, 2, 4, 1, 5, 2, 4, 6, 7, 6]); %!test %! C = [{'5', '2'}, {'8'}, categorical([5, 3]), categorical([4, 2]), '8', ... %! categorical([0, 1, 0])]; %! assert_equal (C, categorical ([5, 2, 8, 5, 3, 4, 2, 8, 0, 1, 0])); %! assert_equal (categories (C), {'3'; '5'; '2'; '8'; '4'; '0'; '1'}); %! assert_equal (double (C), [2, 3, 4, 2, 1, 5, 3, 4, 6, 7, 6]); %!test %! C = [categorical([3, 1]); categorical([4, 2]); categorical([0, 1])]; %! assert_equal (C, categorical ([3, 1; 4, 2; 0, 1])); %! assert_equal (categories (C), {'1'; '3'; '2'; '4'; '0'}); %! assert_equal (double (C), [2, 1; 4, 3; 5, 1]); %!test %! C = [{'1', '2'}; categorical([3, 1]); categorical([4, ... %! 2]); categorical([1, 0])]; %! assert_equal (C, categorical ([1, 2; 3, 1; 4, 2; 1, 0])); %! assert_equal (categories (C), {'1'; '3'; '2'; '4'; '0'}); %! assert_equal (double (C), [1, 3; 2, 1; 4, 3; 1, 5]); %!test %! C = [{'5', '3'}; categorical([3, 2]); categorical([4, 2]); {'8', ... %! '2'}; categorical([0, 1])]; %! assert_equal (C, categorical ([5, 3; 3, 2; 4, 2; 8, 2; 0, 1])); %! assert_equal (categories (C), {'2'; '3'; '5'; '4'; '8'; '0'; '1'}); %! assert_equal (double (C), [3, 2; 2, 1; 4, 1; 5, 1; 6, 7]); %!test %! C = [categorical([3, 2]); categorical([4, 2]); {'1', ... %! '3'}; categorical([0, 0])]; %! assert_equal (C, categorical ([3, 2; 4, 2; 1, 3; 0, 0])); %! assert_equal (categories (C), {'2'; '3'; '4'; '1'; '0'}); %! assert_equal (double (C), [2, 1; 3, 1; 4, 2; 5, 5]); %!test %! C = [{'5', '2'}; categorical([1, 3]); categorical([4, 2]); {'8', ... %! '8'}; categorical([1, 1])]; %! assert_equal (C, categorical ([5, 2; 1, 3; 4, 2; 8, 8; 1, 1])); %! assert_equal (categories (C), {'1'; '3'; '2'; '5'; '4'; '8'}); %! assert_equal (double (C), [4, 3; 1, 2; 5, 3; 6, 6; 1, 1]); %!test %! C = [{'5', '4'}; {'2', '5'}; categorical([2, 3]); categorical([4, ... %! 2]); {'9', '8'}; categorical([0, 0])]; %! assert_equal (C, categorical ([5, 4; 2, 5; 2, 3; 4, 2; 9, 8; 0, 0])); %! assert_equal (categories (C), {'2'; '3'; '4'; '5'; '8'; '9'; '0'}); %! assert_equal (double (C), [4, 3; 1, 4; 1, 2; 3, 1; 6, 5; 7, 7]); %!test %! C = [{'5', '2'}; {'8', '8'}; categorical([3, 3]); categorical([4, ... %! 2]); {'8', '8'}; categorical([0, 1])]; %! assert_equal (C, categorical ([5, 2; 8, 8; 3, 3; 4, 2; 8, 8; 0, 1])); %! assert_equal (categories (C), {'3'; '2'; '5'; '8'; '4'; '0'; '1'}); %! assert_equal (double (C), [3, 2; 4, 4; 1, 1; 5, 2; 4, 4; 6, 7]); %!test %! C = [{'5', '2'}; {'8', '6'}; categorical([5, 3]); categorical([4, ... %! 2]); {'8', '8'}; categorical([0, 0])]; %! assert_equal (C, categorical ([5, 2; 8, 6; 5, 3; 4, 2; 8, 8; 0, 0])); %! assert_equal (categories (C), {'3'; '5'; '2'; '6'; '8'; '4'; '0'}); %! assert_equal (double (C), [2, 3; 5, 4; 2, 1; 6, 3; 5, 5; 7, 7]); %!assert_equal (repmat (categorical ([1, 2]), 4), categorical (repmat ([1, ... %! 2], 4))) %!assert_equal (repmat (categorical ([1, 2]), 2, 3, ... %! 4), categorical (repmat ([1, 2], 2, 3, 4))) %!assert_equal (repmat (categorical ([1, 2]), [2, ... %! 3]), categorical (repmat ([1, 2], [2, 3]))) %!assert_equal (repelem (categorical ([1:3]), ... %! 4), categorical (repelem ([1:3], 4))) %!assert_equal (repelem (categorical ([1:3]), 2, 3, ... %! 4), categorical (repelem ([1:3], 2, 3, 4))) %!assert_equal (repelem (categorical ([1:3]), [2, 3, ... %! 3]), categorical (repelem ([1:3], [2, 3, 3]))) %!assert_equal (repelems (categorical ([1:5]), [1, 3, 5; 2, 1, ... %! 2]), categorical ([1, 1, 3, 5, 5])) %!assert_equal (reshape (categorical (magic (4)), 2, 2, ... %! 4), categorical (reshape (magic (4), 2, 2, 4))) %!assert_equal (reshape (categorical (magic (4)), [2, 2, ... %! 4]), categorical (reshape (magic (4), [2, 2, 4]))) %!assert_equal (reshape (categorical (magic (4)), [], 2, ... %! 2), categorical (reshape (magic (4), [], 2, 2))) %!assert_equal (circshift (categorical (magic (4)), ... %! 2), categorical (circshift (magic (4), 2))) %!assert_equal (circshift (categorical (magic (4)), 1, ... %! 2), categorical (circshift (magic (4), 1, 2))) %!assert_equal (circshift (categorical (magic (4)), 1, ... %! 1), categorical (circshift (magic (4), 1, 1))) %!assert_equal (circshift (categorical (magic (4)), 1, ... %! 1), categorical (circshift (magic (4), 1))) %!test %! X = randi (10, 2, 3, 4); %! D = categorical (X); %! assert_equal (size (permute (D, [3, 1, 2])), [4, 2, 3]); %! assert_equal (permute (D, [3, 1, 2]), categorical (permute (X, [3, 1, 2]))); %! assert_equal (D, ipermute (permute (D, [3, 1, 2]), [3, 1, 2])); %!assert_equal (transpose (categorical ([1, 2; 3, 4])), categorical ([1, ... %! 3; 2, 4])) %!assert_equal (transpose (categorical ([1, 2; 3, ... %! 4])), ctranspose (categorical ([1, 2; 3, 4]))) %!error ... %! transpose (categorical (ones (2, 2, 2))) %!assert_equal (ctranspose (categorical ([1, 2; 3, 4])), categorical ([1, ... %! 3; 2, 4])) %!assert_equal (ctranspose (categorical ([1, 2; 3, ... %! 4])), transpose (categorical ([1, 2; 3, 4]))) %!error ... %! ctranspose (categorical (ones (2, 2, 2))) ################################################################################ ## ** Reference and Assignment Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'end' 'subsref' 'subsasgn' ## ## ## ################################################################################ %!test %! X = magic (3); %! C = categorical (X); %! assert_equal (C(:, end), categorical (X(:,end))); %!test %! X = magic (3); %! C = categorical (X); %! assert_equal (C([1:end-1], end), categorical ([6; 7])); %!test %! C = categorical ([1:5]); %! out = C([2,3]); %! assert_equal (size (out), [1, 2]); %! assert_equal (out, categorical ([2, 3])); %!error ... %! C = categorical ([1:5]); C{1} %!error ... %! C = categorical ([1:5]); C.prop %!test %! C = categorical (1:5); %! C([1,3]) = []; %! assert_equal (C, categorical ([2, 4, 5], [1, 2, 3, 4, 5])); %! assert_equal (categories (C), {'1'; '2'; '3'; '4'; '5'}); %! assert_equal (double (C), [2, 4, 5]); %!test %! C = categorical (1:5); %! C([1,3]) = missing; %! assert_equal (C, categorical ([NaN, 2, NaN, 4, 5], [1, 2, 3, 4, 5])); %! assert_equal (categories (C), {'1'; '2'; '3'; '4'; '5'}); %! assert_equal (double (C), [NaN, 2, NaN, 4, 5]); %!test %! C = categorical (1:5); %! C(2) = ''; %! assert_equal (isequaln (C, categorical ([1, NaN, 3, 4, 5], [1, 2, 3, 4, ... %! 5])), true); %! assert_equal (categories (C), {'1'; '2'; '3'; '4'; '5'}); %! assert_equal (double (C), [1, NaN, 3, 4, 5]); %!test %! C = categorical (1:5); %! C(2) = {''}; %! assert_equal (isequaln (C, categorical ([1, NaN, 3, 4, 5], [1, 2, 3, 4, ... %! 5])), true); %! assert_equal (categories (C), {'1'; '2'; '3'; '4'; '5'}); %! assert_equal (double (C), [1, NaN, 3, 4, 5]); %!test %! C = categorical (1:5); %! C(2) = string (''); %! assert_equal (isequaln (C, categorical ([1, NaN, 3, 4, 5], [1, 2, 3, 4, ... %! 5])), true); %! assert_equal (categories (C), {'1'; '2'; '3'; '4'; '5'}); %! assert_equal (double (C), [1, NaN, 3, 4, 5]); %!test %! C = categorical (1:5); %! C(2) = string (missing); %! assert_equal (isequaln (C, categorical ([1, NaN, 3, 4, 5], [1, 2, 3, 4, ... %! 5])), true); %! assert_equal (categories (C), {'1'; '2'; '3'; '4'; '5'}); %! assert_equal (double (C), [1, NaN, 3, 4, 5]); %!test %! C = categorical (1:5); %! C(1) = '5'; %! assert_equal (C, categorical ([5, 2, 3, 4, 5])); %!test %! C = categorical (1:5); %! C([1,3]) = {'5', '6'}; %! assert_equal (C, categorical ([5, 2, 6, 4, 5])); %! assert_equal (categories (C), {'1'; '2'; '3'; '4'; '5'; '6'}); %! assert_equal (double (C), [5, 2, 6, 4, 5]); %!test %! C = categorical (1:5); %! C(3) = {'0'}; %! assert_equal (C, categorical ([1, 2, 0, 4, 5])); %! C([1,3]) = {'5', '6'}; %! assert_equal (C, categorical ([5, 2, 6, 4, 5])); %! assert_equal (categories (C), {'1'; '2'; '3'; '4'; '5'; '0'; '6'}); %!test %! C = categorical (1:5, 1:5, 'Ordinal', true); %! C([1,3]) = categorical ([5, 5], 1:5, 'Ordinal', true); %! assert_equal (C, categorical ([5, 2, 5, 4, 5], 1:5, 'Ordinal', true)); %! assert_equal (categories (C), {'1'; '2'; '3'; '4'; '5'}); %! assert_equal (double (C), [5, 2, 5, 4, 5]); %!test %! C = categorical (1:5, 1:5, 'Protected', true); %! C([1,3]) = categorical ([5, 5], 'Protected', true); %! assert_equal (C, categorical ([5, 2, 5, 4, 5], 1:5, 'Protected', true)); %! assert_equal (categories (C), {'1'; '2'; '3'; '4'; '5'}); %! assert_equal (double (C), [5, 2, 5, 4, 5]); %!test %! C = categorical (1:5, 1:5, 'Protected', true); %! C([1,3]) = categorical ([5, 5]); %! assert_equal (C, categorical ([5, 2, 5, 4, 5], 1:5, 'Protected', true)); %! assert_equal (categories (C), {'1'; '2'; '3'; '4'; '5'}); %! assert_equal (double (C), [5, 2, 5, 4, 5]); %!test %! C = categorical (1:5); %! C([1,3]) = categorical ({'5', '6'}); %! assert_equal (C, categorical ([5, 2, 6, 4, 5])); %! assert_equal (categories (C), {'1'; '2'; '3'; '4'; '5'; '6'}); %! assert_equal (double (C), [5, 2, 6, 4, 5]); %!test %! C = categorical (1:5); %! C([1,3]) = categorical ([5, 6]); %! assert_equal (C, categorical ([5, 2, 6, 4, 5])); %! assert_equal (categories (C), {'1'; '2'; '3'; '4'; '5'; '6'}); %! assert_equal (double (C), [5, 2, 6, 4, 5]); %!test %! C = categorical ([1, 2, 4, 5]); %! C([1,3]) = categorical ([2, 6]); %! assert_equal (C, categorical ([2, 2, 6, 5], [1, 2, 4, 5, 6])); %! assert_equal (categories (C), {'1'; '2'; '4'; '5'; '6'}); %! assert_equal (double (C), [2, 2, 5, 4]); %!test %! C = categorical ([1, 2, 4, 5]); %! C([1,3]) = categorical ([2, 3]); %! assert_equal (C, categorical ([2, 2, 3, 5], [1, 2, 4, 5, 3])); %! assert_equal (categories (C), {'1'; '2'; '4'; '5'; '3'}); %! assert_equal (double (C), [2, 2, 5, 4]); %!test # only the categories of the values assigned are added %! C = categorical ({'a'; 'b'}); %! C(1:2) = categorical ({'x'; 'z'}, {'w', 'x', 'y', 'z'}); %! assert_equal (categories (C), {'a'; 'b'; 'x'; 'z'}); %! assert_equal (cellstr (C), {'x'; 'z'}); %!error ... %! C = categorical (1:5); C(1:3)(2) = {'3'} %!error ... %! C = categorical (1:5); C(2) = {} %!error ... %! C = categorical (1:5); C(2) = 1 %!error ... %! C = categorical (1:5, 1:5, 'Ordinal', true); C(2) = '1' %!error ... %! C = categorical (1:5, 1:5, 'Ordinal', true); C(2) = categorical (1) %!error ... %! C = categorical (1:5, 1:5, 'Ordinal', false); C(2) = categorical (1, 1:5, ... %! 'Ordinal', true) %!error ... %! C = categorical (1:5, 1:5, 'Protected', true); C(2) = '6' %!error ... %! C = categorical ([1:5]); C{2} = '1' %!error ... %! C = categorical ([1:5]); C.prop = 1 ## Test 'categorical.empty' -- empty categorical arrays %!assert_equal (size (categorical.empty), [0, 0]) %!assert_equal (class (categorical.empty), 'categorical') %!assert_equal (isempty (categorical.empty), true) %!assert_equal (numel (categories (categorical.empty)), 0) %!assert_equal (size (categorical.empty (2, 0)), [2, 0]) %!assert_equal (size (categorical.empty (0, 3)), [0, 3]) %!error ... %! categorical.empty (3) ## Test 'categorical.empty' -- size arguments follow 'zeros' rules: a ## negative dimension counts as zero, a size vector holding none names no size %!assert_equal (size (categorical.empty (0)), [0, 0]) %!assert_equal (size (categorical.empty (-1)), [0, 0]) %!assert_equal (size (categorical.empty (-2, -3)), [0, 0]) %!assert_equal (size (categorical.empty ([])), [0, 0]) %!assert_equal (size (categorical.empty (2, 0, 3)), [2, 0, 3]) %!assert_equal (size (categorical.empty ([0 2 3])), [0, 2, 3]) %!error ... %! categorical.empty (1.5) ## Test 'sort' -- option VALUES are matched without regard to case %!assert_equal (cellstr (sort (categorical ({'b', 'a', 'c'}), 'Ascend')), ... %! {'a', 'b', 'c'}) %!assert_equal (cellstr (sort (categorical ({'b', 'a', 'c'}), 'DESCEND')), ... %! {'c', 'b', 'a'}) ## size: more outputs than the requested dimensions yield %!error ... %! [r, c] = size (categorical ({'a', 'b'}), 1); ## ismember: the operands must be categorical, or nameable as categories %!error ... %! ismember (categorical ({'b', 'a'; 'd', 'c'}), 5); ## issortedrows: an unknown direction, and a strict direction list that does ## not match COL (the simple directions delegate to sortrows before this) %!error ... %! issortedrows (categorical ({'b', 'a'; 'd', 'c'}), {'nope'}); %!error ... %! issortedrows (categorical ({'b', 'a'; 'd', 'c'}), [1, 2], ... %! {'strictascend', 'strictdescend', 'strictascend'}); ## sortrows: DIRECTION must match the columns of A %!error ... %! sortrows (categorical ({'b', 'a'; 'd', 'c'}), {'ascend', 'descend', ... %! 'ascend'}); ## mergecats on an ordinal array only merges consecutive categories %!error ... %! mergecats (categorical ({'a'; 'b'; 'c'}, 'Ordinal', true), {'a', 'c'}); ## min and max compare against another categorical only %!error ... %! min (categorical ({'a'; 'b'; 'c'}, 'Ordinal', true), 5); %!error ... %! max (categorical ({'a'; 'b'; 'c'}, 'Ordinal', true), 5); ## assigning into an ordinal array needs the identical ordered category set %!error ... %! O = categorical ({'a'; 'b'; 'c'}, 'Ordinal', true); %! O(1) = categorical ({'z'}, 'Ordinal', true); ## concatenation takes only text and missing values beside a categorical %!error ... %! vertcat (categorical ({'b', 'a'; 'd', 'c'}), {1}); pr0m1th3as-datatypes-9450b29/inst/tests/datetime.m-tst000066400000000000000000017266511527370255000226210ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ################################################################################ ## ** Create and convert 'datetime' type ** ## ################################################################################ ## Available Methods ## ## ## ## 'datetime' 'dispstrings' 'cellstr' 'char' ## ## 'ymd' 'hms' 'year' 'quarter' ## ## 'month' 'week' 'day' 'hour' ## ## 'minute' 'second' 'timeofday' 'tzoffset' ## ## ## ################################################################################ ## The default display formats set by 'datetime.setDefaultFormats' persist ## across sessions, so a user who has set one would otherwise break every ## assertion below that renders a datetime. Save whatever is stored, run the ## file at the factory defaults, and put it back in the closing block. A format ## that was never set is restored as absent, not as its factory value, so that ## running the tests leaves no preference behind. %!shared savedDefaultFormat, savedDefaultDateFormat %! savedDefaultFormat = ''; %! savedDefaultDateFormat = ''; %! if (ispref ('datatypes', 'datetime_DefaultFormat')) %! savedDefaultFormat = getpref ('datatypes', 'datetime_DefaultFormat'); %! endif %! if (ispref ('datatypes', 'datetime_DefaultDateFormat')) %! savedDefaultDateFormat = getpref ('datatypes', ... %! 'datetime_DefaultDateFormat'); %! endif %! datetime.setDefaultFormats ('reset'); ## Test 'datetime' constructor -- no-argument and basic construction %!assert_equal (class (datetime), 'datetime') %!assert_equal (isa (datetime, 'datetime'), true) %!assert_equal (isscalar (datetime ()), true) %!assert_equal (isscalar (datetime (2020, 1, 1)), true) %!assert_equal (class (datetime (2020, 1, 1)), 'datetime') ## Test 'datetime' constructor -- Y,M,D components and broadcasting %!assert_equal (year (datetime (2020, 1, 1)), 2020) %!assert_equal (month (datetime (2020, 6, 1)), 6) %!assert_equal (day (datetime (2020, 6, 15)), 15) %!assert_equal (size (datetime (2020, 1, [1; 2; 3])), [3, 1]) %!assert_equal (size (datetime (2020, 1, [1, 2, 3])), [1, 3]) ## size with no dimension named folds the trailing dimensions into the last ## output when fewer outputs are asked for than the array has dimensions, and ## pads with ones when more are asked for, as core Octave and MATLAB both do. %!test %! A = datetime (2024, 1, ones (2, 2, 2)); %! [r, c] = size (A); %! assert_equal ([r, c], [2, 4]); %!test %! A = datetime (2024, 1, ones (2, 2)); %! [a, b, c, d] = size (A); %! assert_equal ([a, b, c, d], [2, 2, 1, 1]); %!assert_equal (size (datetime (2020, [1, 2; 3, 4], 1)), [2, 2]) %!assert_equal (year (datetime (2020, 1, [1; 2])), [2020; 2020]) %!assert_equal (day (datetime (2020, 1, [1; 2; 3])), [1; 2; 3]) %!assert_equal (size (datetime ([2019, 2020], 1, 1)), [1, 2]) ## Test 'datetime' constructor -- month/day rollover normalisation %!assert_equal (year (datetime (2020, 13, 1)), 2021) %!assert_equal (month (datetime (2020, 13, 1)), 1) %!assert_equal (month (datetime (2020, 0, 1)), 12) %!assert_equal (year (datetime (2020, 0, 1)), 2019) %!assert_equal (month (datetime (2020, 1, 32)), 2) %!assert_equal (day (datetime (2020, 1, 32)), 1) %!assert_equal (day (datetime (2020, 3, 0)), 29) %!assert_equal (month (datetime (2020, 3, 0)), 2) ## Test 'datetime' constructor -- H,MI,S components %!assert_equal (hour (datetime (2020, 1, 1, 13, 0, 0)), 13) %!assert_equal (minute (datetime (2020, 1, 1, 13, 45, 0)), 45) %!assert_equal (second (datetime (2020, 1, 1, 13, 45, 30)), 30) %!assert_equal (second (datetime (2020, 1, 1, 0, 0, 30.5)), 30.5) %!assert_equal (hour (datetime (2020, 1, 1, 25, 0, 0)), 1) %!assert_equal (day (datetime (2020, 1, 1, 25, 0, 0)), 2) %!assert_equal (minute (datetime (2020, 1, 1, 0, 75, 0)), 15) %!assert_equal (hour (datetime (2020, 1, 1, 0, 75, 0)), 1) ## Test 'datetime' constructor -- milliseconds component %!assert_equal (second (datetime (2020, 1, 1, 0, 0, 0, 500)), 0.5) %!assert_equal (second (datetime (2020, 1, 1, 0, 0, 1, 250)), 1.25) ## Test 'datetime' constructor -- microsecond precision (stored precision is ## microseconds; construction must not truncate to milliseconds). %!assert_equal (second (datetime (2020, 1, 1, 0, 0, ... %! 45.123456)), 45.123456, 1e-9) %!assert_equal (second (datetime (2020, 1, 1, 0, 0, ... %! 45.000001)), 45.000001, 1e-9) %!assert_equal (second (datetime (2020, 1, 1, 0, 0, ... %! 45.123456789)), 45.123457, 1e-9) %!assert_equal (second (datetime (2020, 1, 1, 0, 0, ... %! 45.123456) + seconds (0)), ... %! 45.123456, 1e-9) %!assert_equal (second (datetime (2020, 1, 1, 0, 0, 45.123456, ... %! 'TimeZone', 'America/New_York')), 45.123456, 1e-9) %!assert_equal (second (datetime (1704067200.123456, ... %! 'ConvertFrom', 'posixtime')), 0.123456, 1e-9) ## Test 'datetime' constructor -- from datevec matrix (3 or 6 columns) %!assert_equal (year (datetime ([2019, 6, 1; 2020, 7, 2])), [2019; 2020]) %!assert_equal (month (datetime ([2019, 6, 1; 2020, 7, 2])), [6; 7]) %!assert_equal (hour (datetime ([2020, 1, 1, 5, 0, 0])), 5) %!assert_equal (size (datetime ([2019, 6, 1; 2020, 7, 2])), [2, 1]) ## Test 'datetime' constructor -- from date strings %!assert_equal (year (datetime ('2020-06-15')), 2020) %!assert_equal (month (datetime ('2020-06-15')), 6) %!assert_equal (day (datetime ('2020-06-15')), 15) %!assert_equal (year (datetime ({'2019-01-01'; '2020-01-01'})), [2019; 2020]) ## Test 'datetime' constructor -- ConvertFrom posixtime/datenum/excel %!assert_equal (cellstr (datetime (0, 'ConvertFrom', ... %! 'posixtime')), {'01-Jan-1970'}) %!assert_equal (cellstr (datetime (86400, 'ConvertFrom', ... %! 'posixtime')), {'02-Jan-1970'}) %!assert_equal (cellstr (datetime (719529, 'ConvertFrom', ... %! 'datenum')), {'01-Jan-1970'}) %!assert_equal (year (datetime (0, 'ConvertFrom', 'posixtime')), 1970) %!assert_equal (isnat (datetime (NaN, 'ConvertFrom', 'datenum')), true) ## Test 'datetime' constructor -- NaT and Inf propagation %!assert_equal (isnat (datetime (NaN, NaN, NaN)), true) %!assert_equal (isnat (datetime (2020, 1, 1)), false) %!assert_equal (isinf (datetime (Inf, Inf, Inf)), true) %!assert_equal (year (datetime (Inf, Inf, Inf)), Inf) ## Test that an infinite datetime is infinite in EVERY component, whichever one ## carried the infinity and whichever sign it had. R2026a instead reports the ## year alone as infinite and NaN for the other five, which does not survive its ## own round trip: the components it gives for an infinite datetime rebuild as ## NaT there, while these rebuild the value they came from. Deliberate ## deviation. %!assert_equal (datevec (datetime (Inf, 1, 1)), [Inf, Inf, Inf, Inf, Inf, Inf]) %!assert_equal (datevec (datetime (-Inf, 1, 1)), ... %! [-Inf, -Inf, -Inf, -Inf, -Inf, -Inf]) %!assert_equal (datevec (datetime (2024, Inf, 1)), ... %! [Inf, Inf, Inf, Inf, Inf, Inf]) %!assert_equal (datevec (datetime (2024, 1, -Inf)), ... %! [-Inf, -Inf, -Inf, -Inf, -Inf, -Inf]) %!assert_equal (datevec (datetime (2024, 1, 1, 0, 0, Inf)), ... %! [Inf, Inf, Inf, Inf, Inf, Inf]) %!assert_equal (month (datetime (Inf, 1, 1)), Inf) %!assert_equal (second (datetime (-Inf, 1, 1)), -Inf) %!test %! t = datetime (Inf, 1, 1); %! assert_equal (datetime (datevec (t)), t); %!test %! t = datetime (-Inf, 1, 1); %! assert_equal (datetime (datevec (t)), t); ## Test that Not-a-Number beats an infinity, and that two infinities of opposite ## sign name no moment at all. R2026a agrees on both. %!assert_equal (isnat (datetime (Inf, NaN, 1)), true) %!assert_equal (isnat (datetime (NaN, Inf, 1)), true) %!assert_equal (isnat (datetime (Inf, -Inf, 1)), true) %!assert_equal (isnat (datetime (-Inf, Inf, 1)), true) %!assert_equal (year (datetime (-Inf, -Inf, -Inf)), -Inf) ## Test 'datetime' constructor -- second underflow/overflow normalisation %!assert_equal (char (datetime (2024, 3, 1, 0, 0, -1), ... %! 'yyyy-MM-dd HH:mm:ss'), ... %! '2024-02-29 23:59:59') %!assert_equal (char (datetime (2024, 1, 1, 0, 0, 90), ... %! 'yyyy-MM-dd HH:mm:ss'), ... %! '2024-01-01 00:01:30') %!assert_equal (char (datetime (2024, 14, 32, 25, 61, 61), ... %! 'yyyy-MM-dd HH:mm:ss'), ... %! '2025-03-05 02:02:01') ## Test 'datetime' constructor -- leap-day validity and non-leap rollover %!assert_equal (char (datetime (2024, 2, 29), 'yyyy-MM-dd'), '2024-02-29') %!assert_equal (char (datetime (2023, 2, 29), 'yyyy-MM-dd'), '2023-03-01') ## Test 'datetime' constructor -- year 0, negative, and far-future years %!assert_equal (datevec (datetime (0, 1, 1)), [0, 1, 1, 0, 0, 0]) %!assert_equal (datevec (datetime (-1, 6, 15)), [-1, 6, 15, 0, 0, 0]) %!assert_equal (datevec (datetime (10000, 1, 1)), [10000, 1, 1, 0, 0, 0]) ## Test 'datetime' constructor -- date strings with and without a time %!assert_equal (char (datetime ('2024-03-09 14:30:15'), ... %! 'yyyy-MM-dd HH:mm:ss'), ... %! '2024-03-09 14:30:15') %!assert_equal (char (datetime ('15-Jun-2024'), 'yyyy-MM-dd'), '2024-06-15') ## Test 'datetime' constructor -- datevec matrix with a time-of-day row %!assert_equal (char (datetime ([2024, 3, 9, 14, 30, 15]), ... %! 'yyyy-MM-dd HH:mm:ss'), ... %! '2024-03-09 14:30:15') ## Test 'datetime' constructor -- ConvertFrom excel and datenum values %!assert_equal (char (datetime (45360, 'ConvertFrom', 'excel'), ... %! 'yyyy-MM-dd'), ... %! '2024-03-09') %!assert_equal (char (datetime (739320, 'ConvertFrom', 'datenum'), ... %! 'yyyy-MM-dd'), ... %! '2024-03-09') %!assert_equal (class (datetime (Inf, 'ConvertFrom', 'datenum')), 'datetime') ## Test 'datetime' constructor -- ConvertFrom posixtime honours the time zone %!assert_equal (char (datetime (0, 'ConvertFrom', 'posixtime', 'TimeZone', ... %! 'America/New_York'), ... %! 'yyyy-MM-dd HH:mm:ss'), '1969-12-31 19:00:00') %!assert_equal (char (datetime (0, 'ConvertFrom', 'posixtime', 'TimeZone', ... %! 'Asia/Tokyo'), 'yyyy-MM-dd HH:mm:ss'), '1970-01-01 09:00:00') %!assert_equal (isnat (datetime (NaN, 'ConvertFrom', 'posixtime', ... %! 'TimeZone', 'UTC')), true) ## Test 'datetime.empty' -- empty datetime arrays %!assert_equal (size (datetime.empty), [0, 0]) %!assert_equal (class (datetime.empty), 'datetime') %!assert_equal (isempty (datetime.empty), true) %!assert_equal (size (datetime.empty (2, 0)), [2, 0]) %!assert_equal (size (datetime.empty (0, 3)), [0, 3]) %!error ... %! datetime.empty (3) ## Test 'datetime.empty' -- size arguments follow the rules 'zeros' follows: a ## negative dimension counts as zero, a size vector holding none names no size %!assert_equal (size (datetime.empty (0)), [0, 0]) %!assert_equal (size (datetime.empty (-1)), [0, 0]) %!assert_equal (size (datetime.empty (0, -1)), [0, 0]) %!assert_equal (size (datetime.empty (-2, -3)), [0, 0]) %!assert_equal (size (datetime.empty ([])), [0, 0]) %!assert_equal (size (datetime.empty (2, 0, 3)), [2, 0, 3]) %!error ... %! datetime.empty (1.5) ## Test 'datetime' constructor -- year/month/day/hour/minute must be integer %!error ... %! datetime (2024, 1, 1, 0, 30.5, 0) %!error ... %! datetime (2024.5, 1, 1) %!error ... %! datetime ([2024, 1, 15.5]) %!assert_equal (second (datetime ([2024, 1, 1, 0, 0, 30.5])), 30.5) ## Test that the civil calendar carries any year. It is 64-bit in 'civil.h', ## so the 16 bits 'date::year' holds no longer bound an unzoned datetime; the ## values are R2026a's, from 'dtnorm_probe'. %!assert_equal (year (datetime (32767, 1, 1)), 32767) %!assert_equal (year (datetime (32767, 12, 31)), 32767) %!assert_equal (year (datetime (-32767, 12, 31)), -32767) %!assert_equal (year (datetime (32766, 13, 1)), 32767) %!assert_equal (datevec (datetime (32768, 1, 1)), [32768, 1, 1, 0, 0, 0]) %!assert_equal (datevec (datetime (-32767, 1, 1)), [-32767, 1, 1, 0, 0, 0]) %!assert_equal (datevec (datetime (32767, 13, 1)), [32768, 1, 1, 0, 0, 0]) %!assert_equal (datevec (datetime (32766, 12, 400)), [32768, 1, 4, 0, 0, 0]) %!assert_equal (datevec (datetime (-32766, -12, 1)), [-32768, 12, 1, 0, 0, 0]) %!assert_equal (datevec (datetime ([32768, 1, 1])), [32768, 1, 1, 0, 0, 0]) %!assert_equal (datevec (datetime (32767, 12, 31, 24, 0, 0)), ... %! [32768, 1, 1, 0, 0, 0]) %!assert_equal (datevec (datetime (1e6, 1, 1)), [1e6, 1, 1, 0, 0, 0]) %!assert_equal (datevec (datetime (1e6, 3, 4, 5, 6, 7.25)), ... %! [1e6, 3, 4, 5, 6, 7.25]) %!assert_equal (year (datetime (1e9, 1, 1)), 1e9) %!assert_equal (year (datetime (-1e6, 1, 1)), -1e6) %!assert_equal (datevec (datetime (1e6, 13, 1)), [1000001, 1, 1, 0, 0, 0]) %!assert_equal (datevec (datetime (1e6, 1, 400)), [1000001, 2, 3, 0, 0, 0]) %!assert_equal (datevec (datetime ('100000-01-01', ... %! 'InputFormat', 'yyyy-MM-dd')), [100000, 1, 1, 0, 0, 0]) %!assert_equal (isnat (datetime (NaN, 1, 1)), true) %!assert_equal (year (datetime (Inf, 1, 1)), Inf) ## Test that instants, arithmetic and ordering hold up out there too, the whole ## instant path being counted in 64-bit days and seconds rather than built as a ## chrono time point, whose microseconds run out near year 292277. R2026a's ## values again. %!assert_equal (posixtime (datetime (1e6, 1, 1)), 31494784780800) %!assert_equal (datenum (datetime (1e6, 1, 1)), 365242501) %!assert_equal (char (datetime (1e6, 1, 2) - datetime (1e6, 1, 1)), '24:00:00') %!assert_equal (datetime (1e6, 1, 1) < datetime (1e6, 1, 2), true) %!assert_equal (year (min ([datetime(1e6, 1, 2), datetime(1e6, 1, 1)])), 1e6) %!assert_equal (datevec (datetime (1e6, 1, 1) + days (1)), ... %! [1e6, 1, 2, 0, 0, 0]) %!assert_equal (datevec (datetime (1e6, 1, 1) + calyears (1)), ... %! [1000001, 1, 1, 0, 0, 0]) %!assert_equal (datevec (datetime (32000, 1, 1) + calyears (1000)), ... %! [33000, 1, 1, 0, 0, 0]) %!assert_equal (year (datetime (32000, 1, 1) + years (1000)), 32999) %!assert_equal (year (dateshift (datetime (32767, 6, 1), 'end', 'year') ... %! + calyears (1)), 32768) %!test %! t = datetime (2024, 3, 15); %! t.Year = 100000; %! assert_equal (year (t), 100000); ## Test that UTC and the leap-second timeline carry a wide year as well: their ## offset is zero at every instant, so neither needs the timezone database. ## R2026a exempts UTC in the same way. %!assert_equal (year (datetime (1e6, 1, 1, 'TimeZone', 'UTC')), 1e6) %!assert_equal (datevec (datetime (30000, 1, 1, 'TimeZone', 'UTC') ... %! + calyears (2768)), [32768, 1, 1, 0, 0, 0]) %!assert_equal (posixtime (datetime (1e6, 1, 1, 'TimeZone', 'UTC')), ... %! 31494784780800) ## Test that a REAL zone resolves at any year. There is no zone DATA out ## beyond the database's range, only the rule it carries for all future time -- ## and that rule is written as an nth weekday of a month, so it repeats exactly ## every 400 years, which is what the offset is taken from. Every value here is ## R2026a's, which extrapolates the same way. %!assert_equal (year (datetime (1e6, 1, 1, 'TimeZone', ... %! 'America/New_York')), 1e6) %!assert_equal (year (datetime (32767, 1, 1, 'TimeZone', ... %! 'America/New_York')), ... %! 32767) %!assert_equal (hours (tzoffset (datetime (40000, 1, 15, 12, 0, 0, ... %! 'TimeZone', 'America/New_York'))), -5) %!assert_equal (hours (tzoffset (datetime (40000, 7, 15, 12, 0, 0, ... %! 'TimeZone', 'America/New_York'))), -4) %!assert_equal (isdst (datetime (40000, 7, 15, 12, 0, 0, ... %! 'TimeZone', 'America/New_York')), true) %!assert_equal (hours (tzoffset (datetime (100000, 7, 15, 12, 0, 0, ... %! 'TimeZone', 'America/New_York'))), -4) %!assert_equal (hours (tzoffset (datetime (40000, 1, 15, 12, 0, 0, ... %! 'TimeZone', 'Australia/Sydney'))), 11) %!assert_equal (hours (tzoffset (datetime (40000, 7, 15, 12, 0, 0, ... %! 'TimeZone', 'Australia/Sydney'))), 10) %!assert_equal (hours (tzoffset (datetime (40000, 7, 15, 12, 0, 0, ... %! 'TimeZone', 'Europe/Athens'))), 3) %!assert_equal (hours (tzoffset (datetime (40000, 7, 15, 12, 0, 0, ... %! 'TimeZone', 'Asia/Kolkata'))), 5.5) %!assert_equal (year (datetime ('32768-01-01', 'InputFormat', 'yyyy-MM-dd', ... %! 'TimeZone', 'America/New_York')), 32768) %!assert_equal (year (datetime (11274307, 'ConvertFrom', 'excel', ... %! 'TimeZone', 'America/New_York')), 32768) %!assert_equal (year (datetime (32000, 1, 1, 'TimeZone', 'America/New_York') ... %! + calyears (768)), 32768) %!assert_equal (datevec (datetime (2020, 480000, 1, ... %! 'TimeZone', 'Europe/Athens')), [42019, 12, 1, 0, 0, 0]) %!assert_equal (datevec (datetime (2020, -480000, 1, ... %! 'TimeZone', 'Europe/Athens')), [-37981, 12, 1, 0, 0, 0]) %!assert_equal (datevec (datetime (100000, -1176000, 1, ... %! 'TimeZone', 'Europe/Athens')), [1999, 12, 1, 0, 0, 0]) %!assert_equal (datevec (datetime (2020, 480006, 15, 12, 0, 0, ... %! 'TimeZone', 'Europe/Athens')), [42020, 6, 15, 12, 0, 0]) %!assert_equal (hours (tzoffset (datetime (2020, 480006, 15, 12, 0, 0, ... %! 'TimeZone', 'Europe/Athens'))), 3) %!test %! [~, dst] = tzoffset (datetime (2020, 480006, 15, 12, 0, 0, ... %! 'TimeZone', 'Europe/Athens')); %! assert_equal (hours (dst), 1); %!assert_equal (isdst (datetime (2020, 480006, 15, 12, 0, 0, ... %! 'TimeZone', 'Europe/Athens')), true) %!assert_equal (posixtime (datetime (2020, 480006, 15, 12, 0, 0, ... %! 'TimeZone', 'Europe/Athens')), 1263870291600) %!test %! t = datetime (2020, 1, 1, 'TimeZone', 'Europe/Athens'); %! t.Month = 480000; %! assert_equal (datevec (t), [42019, 12, 1, 0, 0, 0]); %!test %! t = datetime (2020, 480006, 15, 12, 0, 0, 'TimeZone', 'Europe/Athens'); %! t.TimeZone = 'America/New_York'; %! assert_equal (datevec (t), [42020, 6, 15, 5, 0, 0]); %!test %! t = datetime (1e6, 3, 15); %! t.TimeZone = 'America/New_York'; %! assert_equal (year (t), 1e6); %!test %! c = cat (2, datetime (2024, 3, 15, 'TimeZone', 'America/New_York'), ... %! datetime (1e6, 3, 15, 'TimeZone', 'UTC')); %! assert_equal (year (c), [2024, 1e6]); %!test %! t = datetime (2024, 3, 15, 'TimeZone', 'America/New_York'); %! t(2) = datetime (1e6, 3, 15, 'TimeZone', 'UTC'); %! assert_equal (year (t), [2024, 1e6]); ## Test that the extrapolated rule falls on the same day of the month 400 years ## apart, which is what makes it an extrapolation rather than a guess. R2026a's ## March transition is on day 11 at 2040 and 2440, and on day 12 at 40000 and ## 40400; its November one on day 4 at 2040 and day 5 at 40000. %!assert_equal (hours (tzoffset (datetime (40000, 3, 11, 12, 0, 0, ... %! 'TimeZone', 'America/New_York'))), -5) %!assert_equal (hours (tzoffset (datetime (40000, 3, 12, 12, 0, 0, ... %! 'TimeZone', 'America/New_York'))), -4) %!assert_equal (hours (tzoffset (datetime (40400, 3, 11, 12, 0, 0, ... %! 'TimeZone', 'America/New_York'))), -5) %!assert_equal (hours (tzoffset (datetime (40400, 3, 12, 12, 0, 0, ... %! 'TimeZone', 'America/New_York'))), -4) %!assert_equal (hours (tzoffset (datetime (40000, 11, 4, 12, 0, 0, ... %! 'TimeZone', 'America/New_York'))), -4) %!assert_equal (hours (tzoffset (datetime (40000, 11, 5, 12, 0, 0, ... %! 'TimeZone', 'America/New_York'))), -5) %!test %! t = datetime (1e6, 3, 15); %! t.TimeZone = 'UTC'; %! assert_equal (year (t), 1e6); %! assert_equal (t.TimeZone, 'UTC'); %!test %! c = [datetime(1e6, 3, 15, 'TimeZone', 'UTC'), ... %! datetime(2024, 3, 15, 'TimeZone', 'America/New_York')]; %! assert_equal (year (c), [1e6, 2024]); %! assert_equal (c.TimeZone, 'UTC'); ## Test that the serial conversions carry a wide year too: they decompose ## through the same 64-bit calendar. The excel value is R2026a's, and is the ## serial that reached this class from a spreadsheet through 'readtable'. %!assert_equal (datevec (datetime (11274307, 'ConvertFrom', 'excel')), ... %! [32768, 1, 1, 0, 0, 0]) %!assert_equal (exceltime (datetime (11274307, 'ConvertFrom', 'excel')), ... %! 11274307) %!assert_equal (datenum (datetime (4e9, 'ConvertFrom', 'datenum')), 4e9) %!assert_equal (posixtime (datetime (1e15, 'ConvertFrom', 'posixtime')), 1e15) %!assert_equal (year (datetime (1e15, 'ConvertFrom', 'epochtime')), ... %! year (datetime (1e15, 'ConvertFrom', 'posixtime'))) %!assert_equal (year (datetime (30000, 1, 1, 'TimeZone', 'UTCLeapSeconds') ... %! + calyears (2768)), 32768) %!assert_equal (year (datetime (1e6, 1, 1, 'TimeZone', 'UTCLeapSeconds')), 1e6) ## Test that a serial conversion into a zone lands on the same date whichever ## zone is asked for, the serial naming a wall clock rather than an instant. %!assert_equal (year (datetime (11274307, 'ConvertFrom', 'excel', ... %! 'TimeZone', 'America/New_York')), 32768) %!assert_equal (year (datetime (11274307, 'ConvertFrom', 'excel', ... %! 'TimeZone', 'UTC')), 32768) ## Test that the year-range check leaves every representable date alone, ## including the serials and the boundary the constructor already accepted. %!assert_equal (year (datetime (738000, 'ConvertFrom', 'datenum')), 2020) %!assert_equal (year (datetime (45000, 'ConvertFrom', 'excel')), 2023) %!assert_equal (year (datetime (59, 'ConvertFrom', 'excel')), 1900) %!assert_equal (year (datetime (61, 'ConvertFrom', 'excel')), 1900) %!assert_equal (year (datetime (1.7e9, 'ConvertFrom', 'posixtime')), 2023) %!assert_equal (year (datetime (2460000.5, 'ConvertFrom', 'juliandate')), 2023) %!assert_equal (year (datetime (NaN, 'ConvertFrom', 'datenum')), NaN) %!assert_equal (year (datetime (Inf, 'ConvertFrom', 'posixtime')), Inf) %!assert_equal (year (datetime (32767, 6, 1) + calyears (0)), 32767) %!assert_equal (year (datetime (32766, 6, 1) + calyears (1)), 32767) %!assert_equal (year (datetime (-32766, 6, 1) - calyears (1)), -32767) ## Test that an unzoned wall clock is never resolved against a timezone. These ## are spring-forward instants that do not exist in the zones named; an unzoned ## datetime must return each unchanged wherever the test is run, and must agree ## with the matrix spelling of the same call, which never consulted a zone. %!assert_equal (datevec (datetime (2024, 3, 31, 3, 30, 0)), ... %! [2024, 3, 31, 3, 30, 0]) %!assert_equal (datevec (datetime (2024, 3, 10, 2, 30, 0)), ... %! [2024, 3, 10, 2, 30, 0]) %!assert_equal (datevec (datetime (2024, 3, 31, 1, 30, 0)), ... %! [2024, 3, 31, 1, 30, 0]) %!assert_equal (datevec (datetime (2024, 10, 6, 2, 30, 0)), ... %! [2024, 10, 6, 2, 30, 0]) %!assert_equal (datevec (datetime (2024, 9, 29, 2, 30, 0)), ... %! [2024, 9, 29, 2, 30, 0]) %!assert_equal (datevec (datetime (2024, 3, 31, 3, 30, 0)), ... %! datevec (datetime ([2024, 3, 31, 3, 30, 0]))) %!assert_equal (datevec (datetime (2024, 3, 10, 2, 30, 0)), ... %! datevec (datetime ([2024, 3, 10, 2, 30, 0]))) ## Test that naming a zone still resolves the wall clock against it: a time the ## local clock skips moves ahead by the gap, and a repeated one keeps its clock. %!assert_equal (datevec (datetime (2024, 3, 31, 3, 30, 0, ... %! 'TimeZone', 'Europe/Athens')), [2024, 3, 31, 4, 30, 0]) %!assert_equal (datevec (datetime (2024, 3, 10, 2, 30, 0, ... %! 'TimeZone', 'America/New_York')), [2024, 3, 10, 3, 30, 0]) %!assert_equal (datevec (datetime (2024, 11, 3, 1, 30, 0, ... %! 'TimeZone', 'America/New_York')), [2024, 11, 3, 1, 30, 0]) ## Test the whole days carried by negative time components. An exact multiple ## of a day used to take one day too many, and the matrix spelling took none at ## all, so the two disagreed for every negative time. %!assert_equal (datevec (datetime (2024, 3, 15, -24, 0, 0)), ... %! [2024, 3, 14, 0, 0, 0]) %!assert_equal (datevec (datetime (2024, 3, 15, -48, 0, 0)), ... %! [2024, 3, 13, 0, 0, 0]) %!assert_equal (datevec (datetime (2024, 3, 15, 0, 0, -86400)), ... %! [2024, 3, 14, 0, 0, 0]) %!assert_equal (datevec (datetime (2024, 3, 15, 0, -1440, 0)), ... %! [2024, 3, 14, 0, 0, 0]) %!assert_equal (datevec (datetime (2024, 3, 15, 0, 0, -1)), ... %! [2024, 3, 14, 23, 59, 59]) %!assert_equal (datevec (datetime (2024, 3, 15, 0, 0, -86401)), ... %! [2024, 3, 13, 23, 59, 59]) %!assert_equal (datevec (datetime ([2024, 3, 15, 0, 0, -1])), ... %! [2024, 3, 14, 23, 59, 59]) %!assert_equal (datevec (datetime ([2024, 3, 15, -24, 0, 0])), ... %! [2024, 3, 14, 0, 0, 0]) %!assert_equal (datevec (datetime (2024, 3, 15, 24, 0, 0)), ... %! [2024, 3, 16, 0, 0, 0]) %!assert_equal (datevec (datetime (2024, 3, 15, 0, 0, 86400)), ... %! [2024, 3, 16, 0, 0, 0]) ## Test that time components survive at the top of the representable range, ## where the wall clock used to be read back through the machine's own zone and ## came out short by its offset. %!assert_equal (datevec (datetime (32767, 12, 31, 23, 59, 59)), ... %! [32767, 12, 31, 23, 59, 59]) %!assert_equal (datevec (datetime (-32767, 12, 31, 23, 59, 59)), ... %! [-32767, 12, 31, 23, 59, 59]) ## Test 'datetime' constructor -- ConvertFrom yyyymmdd %!assert_equal (char (datetime (20240309, 'ConvertFrom', 'yyyymmdd'), ... %! 'yyyy-MM-dd'), '2024-03-09') %!assert_equal (cellstr (datetime ([20240309, 20241231], ... %! 'ConvertFrom', 'yyyymmdd')), {'09-Mar-2024', '31-Dec-2024'}) %!assert_equal (isnat (datetime (NaN, 'ConvertFrom', 'yyyymmdd')), true) %!error ... %! datetime (20240309.5, 'ConvertFrom', 'yyyymmdd') %!error ... %! datetime (true, 'ConvertFrom', 'yyyymmdd') ## Test 'datetime' constructor -- TimeZone and Format properties %!assert_equal (datetime (2020, 1, 1, 'TimeZone', 'UTC').TimeZone, 'UTC') %!assert_equal (datetime (2020, 1, 1, 'Format', ... %! 'default').Format, 'dd-MMM-uuuu') %!assert_equal (datetime (2020, 1, 1).TimeZone, '') ## Test 'datetime' constructor -- error branches %!error ... %! datetime (2020, 1, 1, 'Format', 5) %!error ... %! datetime ('2020', 'InputFormat', 5) %!error ... %! datetime ('2020', 'PivotYear', 50) %!error ... %! datetime ('20', 'InputFormat', 'yy', 'PivotYear', 'x') %!error ... %! datetime ('20', 'InputFormat', 'yy', 'PivotYear', 50.5) %!error ... %! datetime ('2020-01-01', 'Bogus', 1) %!error ... %! datetime ('2020-01-01', 'Bogus') %!error ... %! datetime (2020, 1, 1, 'Bogus', 1) ## Test that a datetime column survives the trip out to a file and back, at any ## year the calendar carries. 'writetable' emits the display format, whose year ## ran past what the parser would read: a year of 1e6 came back as NaT, so the ## file this class wrote was one it could not read. %!test %! f = [tempname(), '.csv']; %! unwind_protect %! T = table ([datetime(1e6, 1, 1); datetime(1e6, 6, 15)]); %! writetable (T, f); %! R = readtable (f); %! assert_equal (isa (R{:,1}, 'datetime'), true); %! assert_equal (R{:,1}, T{:,1}); %! unwind_protect_cleanup %! unlink (f); %! end_unwind_protect %!test %! f = [tempname(), '.csv']; %! unwind_protect %! T = table ([datetime(-1e6, 1, 1); datetime(1e9, 6, 15)]); %! writetable (T, f); %! R = readtable (f); %! assert_equal (R{:,1}, T{:,1}); %! unwind_protect_cleanup %! unlink (f); %! end_unwind_protect ## Test text detection of a year wider than four digits, and of a signed one. ## R2026a-verified apart from the width: leading zeros cost nothing, and where ## MATLAB stops at six significant digits we read as far as the calendar goes, ## so that a file this class writes is one it can read back. %!assert_equal (datevec (datetime ('32767-03-15')), [32767, 3, 15, 0, 0, 0]) %!assert_equal (datevec (datetime ('0032767-03-15')), [32767, 3, 15, 0, 0, 0]) %!assert_equal (datevec (datetime ('00032767-03-15')), [32767, 3, 15, 0, 0, 0]) %!assert_equal (datevec (datetime ('10000-03-15')), [10000, 3, 15, 0, 0, 0]) %!assert_equal (datevec (datetime ('-1000-03-15')), [-1000, 3, 15, 0, 0, 0]) %!assert_equal (datevec (datetime ('-32767-03-15')), [-32767, 3, 15, 0, 0, 0]) %!assert_equal (datevec (datetime ('32767-03-15 12:34:56')), ... %! [32767, 3, 15, 12, 34, 56]) %!assert_equal (datevec (datetime ('32767-03-15T12:34:56')), ... %! [32767, 3, 15, 12, 34, 56]) %!assert_equal (datevec (datetime ('15-Mar-32767')), [32767, 3, 15, 0, 0, 0]) %!assert_equal (datevec (datetime ('March 15, 32767')), [32767, 3, 15, 0, 0, 0]) %!assert_equal (datevec (datetime ('15 March 32767')), [32767, 3, 15, 0, 0, 0]) %!assert_equal (datevec (datetime ('03/15/32767')), [32767, 3, 15, 0, 0, 0]) %!assert_equal (year (datetime ('1000000-03-15')), 1e6) %!assert_equal (year (datetime ('1000000000-03-15')), 1e9) %!assert_equal (year (datetime ('01-Jan--1000000')), -1e6) ## Test the two shapes whose year stays narrow, because MATLAB refuses them too: ## a year-first slash date and a bare year. %!error ... %! datetime ('32767/03/15') %!error ... %! datetime ('32767') ## Test the year field under an explicit 'InputFormat'. Its width is set by the ## text, not by the count of pattern letters, and the six-digit bound applies ## whatever that count is. %!assert_equal (datevec (datetime ('32767-03-15', 'InputFormat', ... %! 'yyyy-MM-dd')), ... %! [32767, 3, 15, 0, 0, 0]) %!assert_equal (datevec (datetime ('00002024-03-15', ... %! 'InputFormat', 'uuuuuuuu-MM-dd')), [2024, 3, 15, 0, 0, 0]) %!assert_equal (datevec (datetime ('00032767-03-15', ... %! 'InputFormat', 'uuuuuuuu-MM-dd')), [32767, 3, 15, 0, 0, 0]) %!assert_equal (datevec (datetime ('2024-03-15', ... %! 'InputFormat', 'uuuuuuuu-MM-dd')), [2024, 3, 15, 0, 0, 0]) %!assert_equal (datevec (datetime ('-1000-03-15', 'InputFormat', ... %! 'uuuu-MM-dd')), ... %! [-1000, 3, 15, 0, 0, 0]) %!assert_equal (datevec (datetime ('-32767-03-15', ... %! 'InputFormat', 'uuuuuuuu-MM-dd')), [-32767, 3, 15, 0, 0, 0]) %!assert_equal (year (datetime ('1000000-03-15', 'InputFormat', ... %! 'yyyy-MM-dd')), ... %! 1e6) %!assert_equal (year (datetime ('1000000-03-15', ... %! 'InputFormat', 'yyyyyyy-MM-dd')), 1e6) %!error ... %! datetime ('-1000-03-15', 'InputFormat', 'yyyy-MM-dd') ## Test that a year butted against another numeric field still takes exactly the ## pattern's width, which is what keeps 'yyyyMMdd' from eating its neighbours. %!assert_equal (datevec (datetime ('000100000315', ... %! 'InputFormat', 'uuuuuuuuMMdd')), [10000, 3, 15, 0, 0, 0]) %!assert_equal (datevec (datetime ('20240315', 'InputFormat', 'yyyyMMdd')), ... %! [2024, 3, 15, 0, 0, 0]) %!error ... %! datetime ('100000315', 'InputFormat', 'yyyyMMdd') ## Test that a rendered year can be read back, which a padding width counting ## the sign, and a parser counting characters, each used to prevent. %!assert_equal (char (datetime (2024, 3, 15), ... %! 'uuuuuuuu-MM-dd'), '00002024-03-15') %!assert_equal (char (datetime (32767, 3, 15), ... %! 'uuuuuuuu-MM-dd'), '00032767-03-15') %!assert_equal (char (datetime (-32767, 3, 15), 'uuuuuuuu-MM-dd'), ... %! '-00032767-03-15') %!assert_equal (year (datetime (char (datetime (2024, 3, 15), ... %! 'uuuuuuuu-MM-dd'), ... %! 'InputFormat', 'uuuuuuuu-MM-dd')), 2024) %!assert_equal (year (datetime (char (datetime (32767, 3, 15), ... %! 'uuuuuuuu-MM-dd'), ... %! 'InputFormat', 'uuuuuuuu-MM-dd')), 32767) %!assert_equal (year (datetime (char (datetime (-32766, 3, 15), ... %! 'uuuuuuuu-MM-dd'), ... %! 'InputFormat', 'uuuuuuuu-MM-dd')), -32766) %!assert_equal (year (datetime (char (datetime (32767, 3, 15)))), 32767) ## Test that an element the format cannot read becomes NaT without costing the ## rest of the array, as it does for any other unreadable text. %!test %! t = datetime ({'32767-03-15', 'not-a-date-here'}, 'InputFormat', ... %! 'uuuu-MM-dd'); %! assert_equal (isnat (t), [false, true]); %! assert_equal (year (t(1)), 32767); ## Test 'datetime' constructor -- 'InputFormat' LDML parsing (MATLAB-verified) %!assert_equal (datevec (datetime ('09.03.2024', 'InputFormat', ... %! 'dd.MM.yyyy')), ... %! [2024, 3, 9, 0, 0, 0]) %!assert_equal (datevec (datetime ('2024/03/09 14:30', ... %! 'InputFormat', 'yyyy/MM/dd HH:mm')), [2024, 3, 9, 14, 30, 0]) %!assert_equal (datevec (datetime ('09-Mar-2024', ... %! 'InputFormat', 'dd-MMM-yyyy')), [2024, 3, 9, 0, 0, 0]) %!assert_equal (datevec (datetime ('March 9, 2024', ... %! 'InputFormat', 'MMMM d, yyyy')), [2024, 3, 9, 0, 0, 0]) %!assert_equal (datevec (datetime ('9/3/24', 'InputFormat', 'd/M/yy')), ... %! [2024, 3, 9, 0, 0, 0]) %!assert_equal (datevec (datetime ('02:30 PM 09-Mar-2024', ... %! 'InputFormat', 'hh:mm a dd-MMM-yyyy')), [2024, 3, 9, 14, 30, 0]) %!assert_equal (datevec (datetime ('12:00 AM 09-Mar-2024', ... %! 'InputFormat', 'hh:mm a dd-MMM-yyyy')), [2024, 3, 9, 0, 0, 0]) %!assert_equal (second (datetime ('2024-03-09T14:30:45.678', ... %! 'InputFormat', 'yyyy-MM-dd''T''HH:mm:ss.SSS')), 45.678, 1e-9) %!assert_equal (datevec (datetime ('9.3.2024', 'InputFormat', ... %! 'dd.MM.yyyy')), ... %! [2024, 3, 9, 0, 0, 0]) %!assert_equal (datevec (datetime ('49', 'InputFormat', 'yy', ... %! 'PivotYear', 2000)), [2049, 1, 1, 0, 0, 0]) %!assert_equal (year (datetime ('99', 'InputFormat', 'yy', ... %! 'PivotYear', 1950)), 1999) %!assert_equal (month (datetime ('49', 'InputFormat', 'yy')), 1) %!error ... %! datetime ('09/03/2024', 'InputFormat', 'dd.MM.yyyy') %!error ... %! datetime ('Foo 9, 2024', 'InputFormat', 'MMMM d, yyyy') %!error ... %! datetime ('09.03.2024 extra', 'InputFormat', 'dd.MM.yyyy') ## Test 'datetime' constructor -- 'InputFormat' rejects a date that does not ## exist. A lone string errors; within an array only that element is lost, ## becoming NaT (MATLAB-verified against R2024a). %!error ... %! datetime ('2024-04-31', 'InputFormat', 'yyyy-MM-dd') %!error ... %! datetime ('2023-02-29', 'InputFormat', 'yyyy-MM-dd') %!error ... %! datetime ('2024-13-01', 'InputFormat', 'yyyy-MM-dd') %!error ... %! datetime ('2024-01-00', 'InputFormat', 'yyyy-MM-dd') %!error ... %! datetime ({'2024-04-31'}, 'InputFormat', 'yyyy-MM-dd') %!error ... %! datetime ('2024-01-01 25:00:00', 'InputFormat', 'yyyy-MM-dd HH:mm:ss') %!error ... %! datetime ('2024-01-01 12:60:00', 'InputFormat', 'yyyy-MM-dd HH:mm:ss') %!error ... %! datetime ('2024-01-01 12:00:60', 'InputFormat', 'yyyy-MM-dd HH:mm:ss') %!assert_equal (char (datetime ('2024-02-29', 'InputFormat', 'yyyy-MM-dd')), ... %! '29-Feb-2024') %!test %! d = datetime ({'2024-01-15', '2024-04-31'}, 'InputFormat', 'yyyy-MM-dd'); %! assert_equal (isnat (d), [false, true]) %! assert_equal (cellstr (d), {'15-Jan-2024', 'NaT'}) %!test # an unreadable element beside a blank is still an array element %! d = datetime ({'garbage', ''}, 'InputFormat', 'uuuu-MM-dd'); %! assert_equal (isnat (d), [true, true]) %!test %! d = datetime ({'2024-02-30', ''}, 'InputFormat', 'uuuu-MM-dd'); %! assert_equal (isnat (d), [true, true]) %!test %! d = datetime ({''; '2024-04-31'; ''}, 'InputFormat', 'yyyy-MM-dd'); %! assert_equal (isnat (d), [true; true; true]) %!error ... %! datetime (['garbage '; ' '], 'InputFormat', 'uuuu-MM-dd') %!error ... %! datetime ({'2024-13-45', ''}) ## Test an array none of whose text 'InputFormat' can read is refused, unless ## it holds a blank element; a character matrix always is (MATLAB R2024a) %!error ... %! datetime ({'garbage', 'garbage'}, 'InputFormat', 'uuuu-MM-dd') %!error ... %! datetime (['garbage '; 'garbage '], 'InputFormat', 'uuuu-MM-dd') %!error ... %! datetime (['garbage '; ' '; 'garbage '], 'InputFormat', ... %! 'uuuu-MM-dd') %!test %! d = datetime ({'garbage', 'garbage', ''}, 'InputFormat', 'uuuu-MM-dd'); %! assert_equal (isnat (d), [true, true, true]); %!test %! d = datetime (['2024-01-01'; 'garbage '], 'InputFormat', 'uuuu-MM-dd'); %! assert_equal (isnat (d), [false; true]); %!test %! d = datetime (['garbage '; '2024-01-01'], 'InputFormat', 'uuuu-MM-dd'); %! assert_equal (isnat (d), [true; false]); ## Test detected text whose first piece that is not blank cannot be read is ## refused, whatever follows (MATLAB R2024a) %!error ... %! datetime ({'2024-13-45', '2024-01-01'}) %!error ... %! datetime ({'', '2024-13-45', '2024-01-01'}) %!error ... %! datetime (['2024-13-45'; '2024-01-01']) %!error ... %! datetime ([' '; '2024-13-45'; '2024-01-01']) %!error ... %! datetime ({'2024-13-45', '2024-02-30'}) %!test %! d = datetime ({'', '2024-01-01', '2024-13-45'}); %! assert_equal (isnat (d), [true, false, true]); %!test %! d = datetime (['2024-01-01'; '2024-13-45']); %! assert_equal (isnat (d), [false; true]); ## Test 'InputFormat' offset fields read an instant into 'TimeZone' ## (every value measured against MATLAB R2024a) %!test %! d = datetime ('2024-03-09T22:00:00-0500', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssZ', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('2024-03-09T22:00:00Z', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssZ', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 9, 22, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssZZZZZ', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00Z', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssZZZZZ', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 9, 22, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00 GMT-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ss ZZZZ', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00 UTC-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ss ZZZZ', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00 -05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ss ZZZZ', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00 GMT-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ss Z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00Z', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 9, 22, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00z', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 9, 22, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00Z', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssxxx', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 9, 22, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00-05', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssX', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00-5', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssX', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00-0530', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssX', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 30, 0]); %!test %! d = datetime ('2024-03-09T22:00:00-0500', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXX', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXX', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00-05', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00-05:00:30', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXXXX', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 30]); %!test %! d = datetime ('2024-03-09T22:00:00-050030', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXXX', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 30]); %!test %! d = datetime ('2024-03-09T22:00:00-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssx', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00+23:59', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 8, 22, 1, 0]); %!test %! d = datetime ('-05:00 2024-03-09 22:00:00', 'InputFormat', ... %! 'XXX uuuu-MM-dd HH:mm:ss', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09 22:00 -0500', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm Z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00.5-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ss.SXXX', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0.5]); %!test %! d = datetime ('2024-03-09T22:00:00-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'UTC', ... %! 'Locale', 'de_DE'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime (['2024-03-09T22:00:00-05:00'; '2024-03-09T22:00:00+02:00'], ... %! 'InputFormat', 'uuuu-MM-dd''T''HH:mm:ssXXX', ... %! 'TimeZone', 'UTC'); %! assert_equal (hour (d), [3; 20]); %!test %! d = datetime ('12:30 -0500', 'InputFormat', 'HH:mm Z', 'TimeZone', 'UTC'); %! assert_equal (hour (d), 17); ## Test the 'z' field reads abbreviations and numeric offsets %!test %! d = datetime ('2024-03-09 22:00:00 EST', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09 22:00:00 est', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09 22:00:00 EDT', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 2, 0, 0]); %!test %! d = datetime ('2024-03-09 22:00:00 CST', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 4, 0, 0]); %!test %! d = datetime ('2024-03-09 22:00:00EST', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ssz', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09 22:00:00 UTC', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'Europe/Athens'); %! assert_equal (char (d), '10-Mar-2024'); %!test %! d = datetime ('2024-03-09 22:00:00 GMT-05:00', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09 22:00:00 utc+3', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 9, 19, 0, 0]); %!test %! d = datetime ('2024-03-09 22:00:00 +3', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 9, 19, 0, 0]); %!test %! d = datetime ('2024-03-09 22:00:00 +0530', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 9, 16, 30, 0]); %!test %! d = datetime ('2024-03-09 22:00:00 UTC+03:00:30', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 9, 18, 59, 30]); %!test %! d = datetime ('2024-03-09 22:00:00 MEZ', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC', 'Locale', 'de_DE'); %! assert_equal (datevec (d), [2024, 3, 9, 21, 0, 0]); ## Test abbreviations with two meanings are read only under their locale %!test %! d = datetime ('2024-03-09 22:00:00 BST', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC', 'Locale', 'en_GB'); %! assert_equal (datevec (d), [2024, 3, 9, 21, 0, 0]); %!test %! d = datetime ('2024-01-15 12:00:00 BST', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC', 'Locale', 'en_CA'); %! assert_equal (datevec (d), [2024, 1, 15, 6, 0, 0]); %!test %! d = datetime ('2024-01-15 12:00:00 IST', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC', 'Locale', 'en_IE'); %! assert_equal (datevec (d), [2024, 1, 15, 11, 0, 0]); %!test %! d = datetime ('2024-01-15 12:00:00 IST', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC', 'Locale', 'en_IN'); %! assert_equal (datevec (d), [2024, 1, 15, 6, 30, 0]); %!test %! d = datetime ('2024-01-15 12:00:00 ACST', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC', 'Locale', 'en_AU'); %! assert_equal (datevec (d), [2024, 1, 15, 2, 30, 0]); %!test %! d = datetime ('2024-01-15 12:00:00 ACST', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC', 'Locale', 'pt_BR'); %! assert_equal (datevec (d), [2024, 1, 15, 16, 0, 0]); %!test %! d = datetime ('2024-01-15 12:00:00 AMT', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC', 'Locale', 'pt_BR'); %! assert_equal (datevec (d), [2024, 1, 15, 16, 0, 0]); %!error ... %! datetime ('2024-01-15 12:00:00 IST', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC') %!error ... %! datetime ('2024-01-15 12:00:00 AMT', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC', 'Locale', 'en_US') ## Test the names added beyond MATLAB's table are read under any locale %!test %! d = datetime ('2024-01-15 12:00:00 CET', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 1, 15, 11, 0, 0]); %!test %! d = datetime ('2024-01-15 12:00:00 MSK', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 1, 15, 9, 0, 0]); %!test %! d = datetime ('2024-01-15 12:00:00 TRT', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 1, 15, 9, 0, 0]); %!test %! d = datetime ('2024-01-15 12:00:00 GET', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 1, 15, 8, 0, 0]); ## Test unreadable offset text is lost like any other unreadable text %!error ... %! datetime ('2024-03-09T22:00:00+24:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'UTC') %!error ... %! datetime ('2024-03-09T22:00:00-05:0', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'UTC') %!error ... %! datetime ('2024-03-09T22:00:00-05:00:3', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'UTC') %!error ... %! datetime ('2024-03-09 22:00:00 GMT+5:3', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC') %!error ... %! datetime ('2024-03-09 22:00:00 America/New_York', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC') %!error ... %! datetime ('2024-03-09T22:00:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'UTC') %!test %! d = datetime ({'2024-03-09T22:00:00-05:00', '2024-03-09T22:00:00+02:00'}, ... %! 'InputFormat', 'uuuu-MM-dd''T''HH:mm:ssXXX', ... %! 'TimeZone', 'UTC'); %! assert_equal (hour (d), [3, 20]); %!test %! d = datetime ({'2024-03-09T22:00:00-05:00', '2024-03-09T22:00:00'}, ... %! 'InputFormat', 'uuuu-MM-dd''T''HH:mm:ssXXX', ... %! 'TimeZone', 'UTC'); %! assert_equal (isnat (d), [false, true]); %!test %! d = datetime ({'2024-03-09T22:00:00-05:00', ''}, ... %! 'InputFormat', 'uuuu-MM-dd''T''HH:mm:ssXXX', ... %! 'TimeZone', 'UTC'); %! assert_equal (isnat (d), [false, true]); ## Test the offset settles a repeated or skipped wall clock %!test %! d = datetime ('2024-03-10T02:30:00-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'America/New_York'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 30, 0]); %!test %! a = datetime ('2024-11-03T01:30:00-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'America/New_York'); %! b = datetime ('2024-11-03T01:30:00-04:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'America/New_York'); %! assert_equal (hours (a - b), 1); %!test %! d = datetime ('2024-11-03T01:30:00-04:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'America/New_York'); %! assert_equal (char (d, 'uuuu-MM-dd HH:mm XXX'), '2024-11-03 01:30 -04:00'); ## Test the zones an offset is read into %!test %! d = datetime ('2024-03-09T22:00:00-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', '+03:00'); %! assert_equal (datevec (d), [2024, 3, 10, 6, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'Europe/Athens'); %! assert_equal (datevec (d), [2024, 3, 10, 5, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'local'); %! assert_equal (d.TimeZone, datetime (2024, 1, 1, 'TimeZone', ... %! datetime.SystemTimeZone).TimeZone); %! assert_equal (posixtime (d), posixtime (datetime (2024, 3, 10, 3, 0, 0, ... %! 'TimeZone', 'UTC'))); %!test %! d = datetime ('2024-03-09T22:00:00-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'UTC', ... %! 'Format', 'uuuu-MM-dd''T''HH:mm:ssXXX'); %! assert_equal (char (d), '2024-03-10T03:00:00Z'); %!test %! d = datetime ('2024-03-09T22:00:00-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'UTC', ... %! 'Format', 'preserveinput'); %! assert_equal (char (d), '2024-03-10T03:00:00Z'); %!test %! d = datetime ('2024-03-09T22:00:00Z', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ss''Z'''); %! assert_equal (d.TimeZone, ''); %! assert_equal (datevec (d), [2024, 3, 9, 22, 0, 0]); ## Test an offset field requires a zone, and a leap-second array its pattern %!error ... %! datetime ('2024-03-09T22:00:00-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX') %!error ... %! datetime ('2024-03-09T22:00:00', 'InputFormat', 'uuuu-MM-dd''T''HH:mm:ssXXX') %!error ... %! datetime ('2024-03-09T22:00:00-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', '') %!error ... %! datetime ('2024-03-09T22:00:00-0500', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssZ') %!error ... %! datetime ('2024-03-09 22:00:00 EST', 'InputFormat', 'uuuu-MM-dd HH:mm:ss z') %!error ... %! datetime ('2024-03-09T22:00:00 GMT-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ss ZZZZ') %!error ... %! datetime ('2016-12-31T18:59:60-05:00', 'InputFormat', ... %! 'uuuu-MM-dd''T''HH:mm:ssXXX', 'TimeZone', 'UTCLeapSeconds') ## Test ISO text carrying an offset is detected without 'InputFormat' ## (an Octave extension: MATLAB R2024a refuses every one of these) %!test %! d = datetime ('2024-03-09T22:00:00Z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 9, 22, 0, 0]); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('2024-03-09T22:00:00-05:00', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09 22:00:00-05:00', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00+0530', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 9, 16, 30, 0]); %!test %! d = datetime ('2024-03-09T22:00:00-05', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 10, 3, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00Z', 'TimeZone', 'UTC'); %! assert_equal (datevec (d), [2024, 3, 9, 22, 0, 0]); %!test %! d = datetime ('2024-03-09T22:00:00.123Z', 'TimeZone', 'UTC'); %! assert_equal (round (second (d) * 1000), 123); %!test %! d = datetime ('2024-03-09T22:00:00-05:00', 'TimeZone', 'Europe/Athens'); %! assert_equal (datevec (d), [2024, 3, 10, 5, 0, 0]); %!test %! d = datetime ({'2024-03-09T22:00:00-05:00', '2024-03-09T22:00:00+02:00'}, ... %! 'TimeZone', 'UTC'); %! assert_equal (hour (d), [3, 20]); %!test %! d = datetime ({'2024-03-09T22:00:00-05:00', '2024-03-09T22:00:00', ''}, ... %! 'TimeZone', 'UTC'); %! assert_equal (isnat (d), [false, true, true]); %!test %! a = datetime ('2024-11-03T01:30:00-05:00', 'TimeZone', 'America/New_York'); %! b = datetime ('2024-11-03T01:30:00-04:00', 'TimeZone', 'America/New_York'); %! assert_equal (hours (a - b), 1); %!error ... %! datetime ('2024-03-09T22:00:00Z') %!error ... %! datetime ('2024-03-09T22:00:00-05:00', 'TimeZone', '') %!error ... %! datetime ('2024-02-30T22:00:00Z', 'TimeZone', 'UTC') %!error ... %! datetime ('2016-12-31T18:59:60-05:00', 'TimeZone', 'UTCLeapSeconds') %!test %! d = datetime ({'xx', '2024-01-15'}, 'InputFormat', 'yyyy-MM-dd'); %! assert_equal (isnat (d), [true, false]) %!test %! ## A zone does not change the verdict. %! d = datetime ({'2024-01-15', '2024-04-31'}, 'InputFormat', 'yyyy-MM-dd', ... %! 'TimeZone', 'America/New_York'); %! assert_equal (isnat (d), [false, true]) %!test %! ## A NaT carries no time of day, so a date-only array keeps its date-only %! ## rendering when one sits beside it. %! assert_equal (cellstr ([datetime(2024, 1, 15); NaT]), ... %! {'15-Jan-2024'; 'NaT'}) %! assert_equal (cellstr ([datetime(2024, 1, 15, 13, 30, 0); NaT]), ... %! {'15-Jan-2024 13:30:00'; 'NaT'}) %!test %! ## An infinite datetime carries no time of day either, on the same grounds. %! pinf = datetime (Inf, 'ConvertFrom', 'datenum'); %! ninf = datetime (-Inf, 'ConvertFrom', 'datenum'); %! assert_equal (cellstr ([datetime(2024, 1, 15); pinf]), ... %! {'15-Jan-2024'; 'Inf'}) %! assert_equal (cellstr ([datetime(2024, 1, 15); ninf]), ... %! {'15-Jan-2024'; '-Inf'}) %! assert_equal (cellstr ([datetime(2024, 1, 15, 13, 30, 0); pinf]), ... %! {'15-Jan-2024 13:30:00'; 'Inf'}) %! assert_equal ([datetime(2024, 1, 15); pinf].Format, 'dd-MMM-uuuu') %!test %! ## Beside a NaT as well, and an array of nothing but non-finite elements %! ## says nothing either way, keeping the date and time pattern. %! pinf = datetime (Inf, 'ConvertFrom', 'datenum'); %! assert_equal (cellstr ([datetime(2024, 1, 15); NaT; pinf]), ... %! {'15-Jan-2024'; 'NaT'; 'Inf'}) %! assert_equal ([pinf; pinf].Format, 'dd-MMM-uuuu HH:mm:ss') %! assert_equal ([NaT; pinf].Format, 'dd-MMM-uuuu HH:mm:ss') ## Test 'datetime' constructor -- a datetime input is copied, components, time ## zone and display format all carrying over %!test %! d = datetime (2024, 1, 1, 12, 0, 0); %! r = datetime (d); %! assert_equal (datevec (r), datevec (d)); %! assert_equal (isempty (r.TimeZone), true); %! assert_equal (r.Format, d.Format); %!test %! z = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York'); %! r = datetime (z); %! assert_equal (r.TimeZone, 'America/New_York'); %! assert_equal (posixtime (r), posixtime (z)); %!test %! ls = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! r = datetime (ls); %! assert_equal (r.Second, 60); %! assert_equal (r.TimeZone, 'UTCLeapSeconds'); %! assert_equal (r.Format, "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'"); %!assert_equal (isnat (datetime (NaT)), true) %!assert_equal (size (datetime (datetime (2024, 1, [1, 2, 3]))), [1, 3]) ## Test 'datetime' constructor -- copying accepts 'Format' and 'TimeZone', the ## zone attaching to an unzoned array and converting a zoned one %!test %! d = datetime (2024, 1, 1, 12, 0, 0); %! r = datetime (d, 'Format', 'uuuu-MM-dd'); %! assert_equal (r.Format, 'uuuu-MM-dd'); %! assert_equal (char (r), '2024-01-01'); %!test %! d = datetime (2024, 1, 1, 12, 0, 0); %! r = datetime (d, 'TimeZone', 'UTC'); %! assert_equal (r.TimeZone, 'UTC'); %! assert_equal (r.Hour, 12); %! assert_equal (posixtime (r), 1704110400); %!test %! z = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York'); %! r = datetime (z, 'TimeZone', 'UTC'); %! assert_equal (r.Hour, 17); %! assert_equal (posixtime (r), 1704128400); %!test %! z = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York'); %! r = datetime (z, 'TimeZone', ''); %! assert_equal (isempty (r.TimeZone), true); %! assert_equal (r.Hour, 12); ## Test 'datetime' constructor -- a change of zone while copying follows the ## same rule as assigning the property, so the leap-second zone may be both ## entered and left (MATLAB's constructor and property disagree here) %!test %! ls = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! r = datetime (ls, 'TimeZone', 'UTC'); %! assert_equal (datevec (r), [2016, 12, 31, 23, 59, 59]); %! assert_equal (r.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime (2024, 1, 1, 12, 0, 0); %! r = datetime (d, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (r.TimeZone, 'UTCLeapSeconds'); %! assert_equal (r.Format, "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'"); ## Test 'datetime' constructor -- the text-reading options have nothing to act ## on when copying and are ignored, while 'ConvertFrom' is an error %!test %! d = datetime (2024, 1, 1, 12, 0, 0); %! assert_equal (datevec (datetime (d, 'InputFormat', 'uuuu-MM-dd')), ... %! datevec (d)); %! assert_equal (datevec (datetime (d, 'Locale', 'fr_FR')), datevec (d)); %! assert_equal (datevec (datetime (d, 'PivotYear', 1900)), datevec (d)); %!error ... %! datetime (datetime (2024, 1, 1), 'ConvertFrom', 'datenum') %!error ... %! datetime (datetime (2024, 1, 1), datetime (2024, 1, 2)) %!error ... %! datetime (20240101, 'ConvertFrom', 'yyyymmdd', 'InputFormat', 'yyyyMMdd') ## Test 'datetime' constructor -- elapsed time names no instant %!error ... %! datetime (days (3)) %!error ... %! datetime (calmonths (3)) ## Test 'datetime' constructor -- empty text names no date and becomes NaT, ## whereas a container holding no text at all gives an empty datetime %!test %! assert_equal (size (datetime ('')), [1, 1]); %! assert_equal (isnat (datetime ('')), true); %! assert_equal (isnat (datetime ({''})), true); %! assert_equal (isnat (datetime (string (''))), true); %! assert_equal (isnat (datetime (char (zeros (1, 0)))), true); %!test %! assert_equal (size (datetime ({})), [0, 0]); %! assert_equal (isempty (datetime ({})), true); ## Test 'datetime' constructor -- a blank among readable dates costs only ## itself, so a blank field in imported data does not lose the column %!test %! d = datetime ({'2024-01-01', '', '2024-01-03'}); %! assert_equal (size (d), [1, 3]); %! assert_equal (isnat (d), logical ([0, 1, 0])); %! assert_equal (d.Day, [1, NaN, 3]); %!test %! d = datetime ({'2024-01-01'; ''}); %! assert_equal (size (d), [2, 1]); %! assert_equal (isnat (d), logical ([0; 1])); %!test %! d = datetime ({'2024-01-01', ''}, 'InputFormat', 'uuuu-MM-dd'); %! assert_equal (isnat (d), logical ([0, 1])); %!test %! d = datetime ({'2024-01-01', ''}, 'TimeZone', 'America/New_York'); %! assert_equal (isnat (d), logical ([0, 1])); %! assert_equal (d.TimeZone, 'America/New_York'); %!test %! d = datetime ({'2016-12-31T23:59:60Z', ''}, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (isnat (d), logical ([0, 1])); %! assert_equal (d.Second(1), 60); ## Test 'datetime' constructor -- a character matrix holds one date per row, ## giving a column, with trailing blanks trimmed %!test %! d = datetime (['2024-01-01'; '2024-01-02']); %! assert_equal (size (d), [2, 1]); %! assert_equal (d.Day, [1; 2]); %!test %! d = datetime (['01-Jan-2024'; '02-Jan-2024'; '03-Jan-2024']); %! assert_equal (size (d), [3, 1]); %! assert_equal (d.Day, [1; 2; 3]); %!assert_equal (size (datetime (['2024-01-01'])), [1, 1]) %!test %! d = datetime (['2024-01-01 '; '2024-01-02 ']); %! assert_equal (d.Day, [1; 2]); %!test %! d = datetime (['2024-01-01'; '2024-01-02'], 'InputFormat', 'uuuu-MM-dd'); %! assert_equal (d.Day, [1; 2]); %!test %! d = datetime (['2024-01-01'; '2024-01-02'], 'TimeZone', 'UTC'); %! assert_equal (size (d), [2, 1]); %! assert_equal (d.TimeZone, 'UTC'); ## Test 'datetime' constructor -- a blank row of a character matrix names no ## date, while a matrix with no rows holds no text at all %!test %! d = datetime (['2024-01-01'; ' ']); %! assert_equal (size (d), [2, 1]); %! assert_equal (isnat (d), logical ([0; 1])); %!assert_equal (size (datetime (char (zeros (0, 10)))), [0, 1]) %!assert_equal (size (datetime ('')), [1, 1]) ## Test 'datetime' constructor -- a character array of more than two dimensions ## is refused %!error ... %! datetime (reshape (repmat ('a', 1, 8), 2, 2, 2)) ## Test 'cat' -- empty text adds one missing element, an empty container none %!test %! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York'); %! assert_equal (isnat ([ny, '']), logical ([0, 1])); %! assert_equal (isnat ([ny, {''}]), logical ([0, 1])); %! assert_equal ([ny, ''].TimeZone, 'America/New_York'); %! assert_equal (size ([ny, {}]), [1, 1]); %! assert_equal (size ([ny, []]), [1, 1]); ## Test 'datetime' constructor -- format auto-detection (no 'InputFormat') %!assert_equal (datevec (datetime ('2024-03-09')), [2024, 3, 9, 0, 0, 0]) %!assert_equal (datevec (datetime ('2024-03-09 14:30:45')), ... %! [2024, 3, 9, 14, 30, 45]) %!assert_equal (datevec (datetime ('2024/03/09')), [2024, 3, 9, 0, 0, 0]) %!assert_equal (datevec (datetime ('09-Mar-2024')), [2024, 3, 9, 0, 0, 0]) %!assert_equal (datevec (datetime ('09-March-2024')), [2024, 3, 9, 0, 0, 0]) %!assert_equal (datevec (datetime ('09-Mar-2024 14:30:45')), ... %! [2024, 3, 9, 14, 30, 45]) %!assert_equal (datevec (datetime ('2024-2-9')), [2024, 2, 9, 0, 0, 0]) %!assert_equal (datevec (datetime ({'2024-03-09'; '2024-12-31'})), ... %! [2024, 3, 9, 0, 0, 0; 2024, 12, 31, 0, 0, 0]) %!error ... %! datetime ('2024-13-09') %!error ... %! datetime ('2024-03-32') %!error ... %! datetime ('2024-03-09 25:00:00') ## Test 'datetime' constructor -- the auto-detected repertoire, widened toward ## MATLAB's: a 'T' separator, fractional seconds, a time without seconds, and ## the 'Month d, yyyy' form are read %!assert_equal (datevec (datetime ('2024-03-09T14:30:45')), ... %! [2024, 3, 9, 14, 30, 45]) %!assert_equal (datevec (datetime ('2024-03-09T14:30:45.678')), ... %! [2024, 3, 9, 14, 30, 45.678]) %!assert_equal (datevec (datetime ('2024-03-09 13:45:00.123')), ... %! [2024, 3, 9, 13, 45, 0.123]) %!assert_equal (datevec (datetime ('2024-03-09 14:30')), ... %! [2024, 3, 9, 14, 30, 0]) %!assert_equal (datevec (datetime ('March 15, 2024')), [2024, 3, 15, 0, 0, 0]) ## Test 'datetime' constructor -- one format is detected and read for the whole ## array, so text in another format is lost rather than read on its own terms %!test %! d = datetime ({'2024-03-09'; '09-Mar-2024'}); %! assert_equal (isnat (d), logical ([0; 1])); %! assert_equal (d.Day(1), 9); %!test %! d = datetime ({'2024-03-09 14:30:45'; '2024-03-10'}); %! assert_equal (isnat (d), logical ([0; 1])); %!assert_equal (datevec (datetime ({'2024-03-09'; '2024-12-31'})), ... %! [2024, 3, 9, 0, 0, 0; 2024, 12, 31, 0, 0, 0]) ## Test 'datetime' constructor -- the format is detected from the first string ## that is not blank, and text none can be detected from is refused %!test %! d = datetime ({''; '2024-03-09'}); %! assert_equal (isnat (d), logical ([1; 0])); %! assert_equal (d.Day(2), 9); %!error ... %! datetime ({'not-a-date'; '2024-03-09'}) ## Test 'datetime' constructor -- a date that does not exist is refused under ## the detected format, as it is under an explicit one %!error ... %! datetime ('2024-04-31') %!error ... %! datetime ('2024-02-30') %!error ... %! datetime ('2024-03-09 13:45:60') ## Test 'datetime' constructor -- 'MixedFormats' restores per-string detection, ## an Octave extension for text gathered from several sources %!test %! d = datetime ({'2024-03-09'; '09-Mar-2024'}, 'MixedFormats', true); %! assert_equal (isnat (d), logical ([0; 0])); %! assert_equal (datevec (d), [2024, 3, 9, 0, 0, 0; 2024, 3, 9, 0, 0, 0]); %!assert_equal (datevec (datetime ('2024-04-31', 'MixedFormats', true)), ... %! [2024, 5, 1, 0, 0, 0]) %!assert_equal (datevec (datetime ('2024-02-30', 'MixedFormats', true)), ... %! [2024, 3, 1, 0, 0, 0]) %!assert_equal (datevec (datetime ('2024-03-09 13:45:60', ... %! 'MixedFormats', true)), [2024, 3, 9, 13, 46, 0]) %!error ... %! datetime ('2024-03-09', 'MixedFormats', 'yes') ## Test 'datetime' constructor -- a slash date is ambiguous unless the text ## settles it, and the whole array is consulted %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! assert_equal (datevec (datetime ('03/09/2024')), [2024, 3, 9, 0, 0, 0]); %! assert_equal (datevec (datetime ({'01/02/2024'; '15/03/2024'})), ... %! [2024, 2, 1, 0, 0, 0; 2024, 3, 15, 0, 0, 0]); %! assert_equal (datevec (datetime ({'15/03/2024'; '01/02/2024'})), ... %! [2024, 3, 15, 0, 0, 0; 2024, 2, 1, 0, 0, 0]); %!assert_equal (datevec (datetime ('13/09/2024')), [2024, 9, 13, 0, 0, 0]) %!warning ... %! datetime ('03/09/2024'); ## Test 'datetime' constructor -- 'Locale' localized month names ## (MATLAB-verified) %!assert_equal (month (datetime ('9 février 2024', ... %! 'InputFormat', 'd MMMM yyyy', 'Locale', 'fr_FR')), 2) %!assert_equal (month (datetime ('9 janv. 2024', ... %! 'InputFormat', 'd MMM yyyy', 'Locale', 'fr_FR')), 1) %!assert_equal (month (datetime ('9 März 2024', ... %! 'InputFormat', 'd MMMM yyyy', 'Locale', 'de_DE')), 3) %!assert_equal (month (datetime ('9 Sept. 2024', ... %! 'InputFormat', 'd MMM yyyy', 'Locale', 'de_DE')), 9) %!assert_equal (month (datetime ('9 septiembre 2024', ... %! 'InputFormat', 'd MMMM yyyy', 'Locale', 'es_ES')), 9) %!assert_equal (month (datetime ('9 sept 2024', ... %! 'InputFormat', 'd MMM yyyy', 'Locale', 'es_ES')), 9) %!assert_equal (month (datetime ('9 maggio 2024', ... %! 'InputFormat', 'd MMMM yyyy', 'Locale', 'it_IT')), 5) %!assert_equal (month (datetime ('9 set 2024', ... %! 'InputFormat', 'd MMM yyyy', 'Locale', 'it_IT')), 9) %!assert_equal (month (datetime ('9 março 2024', ... %! 'InputFormat', 'd MMMM yyyy', 'Locale', 'pt_BR')), 3) %!assert_equal (month (datetime ('9 dez. 2024', ... %! 'InputFormat', 'd MMM yyyy', 'Locale', 'pt_BR')), 12) %!assert_equal (month (datetime ('9 mars 2024', ... %! 'InputFormat', 'd MMMM yyyy', 'Locale', 'fr-FR')), 3) %!test %! d = datetime ('2024-03-09 22:00:00 BST', 'InputFormat', ... %! 'uuuu-MM-dd HH:mm:ss z', 'TimeZone', 'UTC', 'Locale', 'en-GB'); %! assert_equal (datevec (d), [2024, 3, 9, 21, 0, 0]); %!assert_equal (month (datetime ('MARS', 'InputFormat', 'MMMM', ... %! 'Locale', 'fr_FR')), 3) %!assert_equal (month (datetime ('March', 'InputFormat', 'MMMM', ... %! 'Locale', 'system')), 3) %!error ... %! datetime ('9 mars 2024', 'InputFormat', 'd MMMM yyyy') %!error ... %! datetime ('9 marzo 2024', 'InputFormat', 'd MMMM yyyy', 'Locale', 'ja_JP') %!error ... %! datetime ('2024', 'InputFormat', 'yyyy', 'Locale', 5) %!test %! ## Locale name tables are memoized, so alternating locales within a session %! ## must keep resolving against their own tables. %! a = month (datetime ('9 mars 2024', 'InputFormat', 'd MMMM yyyy', ... %! 'Locale', 'fr')); %! b = month (datetime ('9 März 2024', 'InputFormat', 'd MMMM yyyy', ... %! 'Locale', 'de')); %! c = month (datetime ('9 mars 2024', 'InputFormat', 'd MMMM yyyy', ... %! 'Locale', 'fr')); %! d = month (datetime ('9 March 2024', 'InputFormat', 'd MMMM yyyy')); %! assert_equal ([a, b, c, d], [3, 3, 3, 3]) ## Test 'datetime' constructor -- the format detected under a 'Locale' ## (MATLAB-verified on R2024a; where a ruling differs, it is named) %!test %! d = datetime ('01-Mar-2024'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-01 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('01-Mär-2024', 'Locale', 'de_DE'); %!test %! d = datetime ('01-März-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-01 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('01-Mär-2024 10:30:00', 'Locale', 'de_DE'); %!error ... %! datetime ('15 März 2024', 'Locale', 'de_DE'); %!error ... %! datetime ('März 15, 2024', 'Locale', 'de_DE'); %!test %! d = datetime ('1. Januar 2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-01 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('01.03.2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-01 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('01/03/2024', 'Locale', 'de_DE'); %!error ... %! datetime ('13/03/2024', 'Locale', 'de_DE'); %!error ... %! datetime ('01-Mar-2024', 'Locale', 'de_DE'); %!test %! d = datetime ('01-Okt-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-01 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ({'01-Mär-2024', '15-Okt-2024'}, 'Locale', 'de_DE'); %!error ... %! datetime ({'01-Mär-2024', '15-Oct-2024'}, 'Locale', 'de_DE'); %!error ... %! datetime ('01-Mär-2024', 'Locale', 'de'); %!error ... %! datetime ('01-Mär-2024'); %!test %! d = datetime ('01-mars-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-01 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('01-janv.-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-01 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('01-janv-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-01 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('01-déc.-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-01 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('15 mars 2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-15 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('01-Jan-2024', 'Locale', 'fr_FR'); %!test %! d = datetime ('01-ene-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-01 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('01-gen-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-01 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('01-fev-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-01 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('01-Μα�-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-01 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('01-Μα�-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-01 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('15 Μα�τίου 2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-15 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('01-Mär-2024', 'Locale', 'de_DE'); %!error ... %! datetime ('15 März 2024', 'Locale', 'de_DE'); %!error ... %! datetime ('01-Mär-2024', 'Locale', 'xx_YY'); %!test %! d = datetime ('05-Jan-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-January-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Feb-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-February-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Mar-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-March-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Apr-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-April-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-May-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-May-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Jun-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-June-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Jul-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-July-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Aug-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-August-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Sep-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-September-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Oct-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-October-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Nov-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-November-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Dec-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-December-2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-janv-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-janv.-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-janvier-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-févr-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-févr.-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-février-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-mars-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-mars-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-avr-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-avr.-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-avril-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-mai-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-mai-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-juin-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-juin-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-juil-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-juil.-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-juillet-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-août-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-août-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-sept-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-sept.-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-septembre-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-oct-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-oct.-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-octobre-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-nov-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-nov.-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-novembre-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-déc-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-déc.-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-décembre-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Jan-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Jan.-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Januar-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Feb-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Feb.-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Februar-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-März-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-März-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Apr-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Apr.-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-April-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Mai-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Mai-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Juni-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Juni-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Juli-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Juli-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Aug-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Aug.-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-August-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Sept-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Sept.-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-September-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Okt-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Okt.-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Oktober-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Nov-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Nov.-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-November-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Dez-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Dez.-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Dezember-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-ene-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (c) Spanish abbreviations may carry a point %!test %! d = datetime ('05-ene.-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-enero-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-feb-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (c) Spanish abbreviations may carry a point %!test %! d = datetime ('05-feb.-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-febrero-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-mar-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (c) Spanish abbreviations may carry a point %!test %! d = datetime ('05-mar.-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-marzo-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-abr-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (c) Spanish abbreviations may carry a point %!test %! d = datetime ('05-abr.-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-abril-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-may-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (c) Spanish abbreviations may carry a point %!test %! d = datetime ('05-may.-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-mayo-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-jun-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (c) Spanish abbreviations may carry a point %!test %! d = datetime ('05-jun.-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-junio-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-jul-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (c) Spanish abbreviations may carry a point %!test %! d = datetime ('05-jul.-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-julio-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-ago-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (c) Spanish abbreviations may carry a point %!test %! d = datetime ('05-ago.-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-agosto-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-sept-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (c) Spanish abbreviations may carry a point %!test %! d = datetime ('05-sept.-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-septiembre-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-oct-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (c) Spanish abbreviations may carry a point %!test %! d = datetime ('05-oct.-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-octubre-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-nov-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (c) Spanish abbreviations may carry a point %!test %! d = datetime ('05-nov.-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-noviembre-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-dic-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (c) Spanish abbreviations may carry a point %!test %! d = datetime ('05-dic.-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-diciembre-2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-gen-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-gennaio-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-feb-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-febbraio-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-mar-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-marzo-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-apr-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-aprile-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-mag-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-maggio-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-giu-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-giugno-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-lug-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-luglio-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-ago-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-agosto-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-set-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-settembre-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-ott-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-ottobre-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-nov-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-novembre-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-dic-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-dicembre-2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-jan-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-jan.-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-janeiro-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-fev-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-fev.-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-fevereiro-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-mar-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-mar.-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-março-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-abr-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-abr.-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-abril-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-mai-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-mai.-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-maio-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-jun-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-jun.-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-junho-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-jul-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-jul.-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-julho-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-ago-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-ago.-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-agosto-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-set-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-set.-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-setembro-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-out-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-out.-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-outubro-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-nov-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-nov.-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-novembro-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-dez-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-dez.-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-dezembro-2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Ιαν-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Ιανουα�ίου-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Φεβ-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Φεβ�ουα�ίου-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-02-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Μα�-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Μα�τίου-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Απ�-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Απ�ιλίου-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-04-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Μα�-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Μα�ου-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Ιουν-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Ιουνίου-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-06-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Ιουλ-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Ιουλίου-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-07-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Αυγ-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Αυγο�στου-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-08-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Σεπ-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Σεπτεμβ�ίου-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-09-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Οκτ-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Οκτωβ�ίου-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-10-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-�οε-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-�οεμβ�ίου-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-11-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Δεκ-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Δεκεμβ�ίου-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-12-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('2024-03-05', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('2024-03-05 14:30:00', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05-Mar-2024 14:30:00', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05-Mar-2024 14:30', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('March 5, 2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## An Octave extension: 'dd MMMM uuuu' is read in English. %!test %! d = datetime ('5 March 2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## An Octave extension: 'dd MMMM uuuu' is read in English. %!test %! d = datetime ('05 March 2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('5. March 2024', 'Locale', 'en_US'); %!error ... %! datetime ('5 de March de 2024', 'Locale', 'en_US'); ## An Octave extension: 'dd MMMM uuuu' is read in English. %!test %! d = datetime ('5 Mar 2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## An Octave extension: 'dd MMMM uuuu' is read in English. %!test %! d = datetime ('5 Mar 2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('5. Mar 2024', 'Locale', 'en_US'); %!error ... %! datetime ('5 de Mar de 2024', 'Locale', 'en_US'); %!test %! d = datetime ('Mar 5, 2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05.03.2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5.3.2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!test %! d = datetime ('05.03.24', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!warning ... %! datetime ('05/03/2024', 'Locale', 'en_US'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-03 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('5/3/24', 'Locale', 'en_US'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('5/3/24', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-03 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('05/03/24', 'Locale', 'en_US'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/24', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-03 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!warning ... %! datetime ('03/05/2024', 'Locale', 'en_US'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('03/05/2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('3/5/24', 'Locale', 'en_US'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('3/5/24', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('05-03-2024', 'Locale', 'en_US'); %!test %! d = datetime ('05.03.2024 14:30', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05.03.2024 14:30:00', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!warning ... %! datetime ('05/03/2024 14:30', 'Locale', 'en_US'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/2024 14:30', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-03 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); ## An Octave extension: 'dd MMMM uuuu' is read in English. %!test %! d = datetime ('5 March 2024 14:30', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('5. March 2024 14:30', 'Locale', 'en_US'); %!error ... %! datetime ('5 de March de 2024 14:30', 'Locale', 'en_US'); %!test %! d = datetime ('March 5, 2024 14:30', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('Tuesday, 5. March 2024', 'Locale', 'en_US'); %!error ... %! datetime ('Tuesday 5 March 2024', 'Locale', 'en_US'); %!error ... %! datetime ('Tuesday, 5 de March de 2024', 'Locale', 'en_US'); %!error ... %! datetime ('Tuesday, 5 March 2024', 'Locale', 'en_US'); %!error ... %! datetime ('Tuesday, March 5, 2024', 'Locale', 'en_US'); %!test %! d = datetime ('2024-03-05', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('2024-03-05 14:30:00', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05-mars-2024 14:30:00', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05-mars-2024 14:30', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('mars 5, 2024', 'Locale', 'fr_FR'); %!test %! d = datetime ('5 mars 2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05 mars 2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('5. mars 2024', 'Locale', 'fr_FR'); %!error ... %! datetime ('5 de mars de 2024', 'Locale', 'fr_FR'); %!test %! d = datetime ('5 mars 2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5 mars 2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('5. mars 2024', 'Locale', 'fr_FR'); %!error ... %! datetime ('5 de mars de 2024', 'Locale', 'fr_FR'); %!error ... %! datetime ('mars 5, 2024', 'Locale', 'fr_FR'); %!test %! d = datetime ('05.03.2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5.3.2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!test %! d = datetime ('05.03.24', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!warning ... %! datetime ('05/03/2024', 'Locale', 'fr_FR'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('5/3/24', 'Locale', 'fr_FR'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('5/3/24', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('05/03/24', 'Locale', 'fr_FR'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/24', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!warning ... %! datetime ('03/05/2024', 'Locale', 'fr_FR'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('03/05/2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-03 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('3/5/24', 'Locale', 'fr_FR'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('3/5/24', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-03 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('05-03-2024', 'Locale', 'fr_FR'); %!test %! d = datetime ('05.03.2024 14:30', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05.03.2024 14:30:00', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!warning ... %! datetime ('05/03/2024 14:30', 'Locale', 'fr_FR'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/2024 14:30', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('5 mars 2024 14:30', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('5. mars 2024 14:30', 'Locale', 'fr_FR'); %!error ... %! datetime ('5 de mars de 2024 14:30', 'Locale', 'fr_FR'); %!error ... %! datetime ('mars 5, 2024 14:30', 'Locale', 'fr_FR'); %!error ... %! datetime ('mardi, 5. mars 2024', 'Locale', 'fr_FR'); %!error ... %! datetime ('mardi 5 mars 2024', 'Locale', 'fr_FR'); %!error ... %! datetime ('mardi, 5 de mars de 2024', 'Locale', 'fr_FR'); %!error ... %! datetime ('mardi, 5 mars 2024', 'Locale', 'fr_FR'); %!error ... %! datetime ('mardi, mars 5, 2024', 'Locale', 'fr_FR'); %!test %! d = datetime ('2024-03-05', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('2024-03-05 14:30:00', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05-März-2024 14:30:00', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05-März-2024 14:30', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('März 5, 2024', 'Locale', 'de_DE'); %!error ... %! datetime ('5 März 2024', 'Locale', 'de_DE'); %!error ... %! datetime ('05 März 2024', 'Locale', 'de_DE'); %!test %! d = datetime ('5. März 2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('5 de März de 2024', 'Locale', 'de_DE'); %!error ... %! datetime ('5 März 2024', 'Locale', 'de_DE'); %!error ... %! datetime ('5 März 2024', 'Locale', 'de_DE'); %!test %! d = datetime ('5. März 2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('5 de März de 2024', 'Locale', 'de_DE'); %!error ... %! datetime ('März 5, 2024', 'Locale', 'de_DE'); %!test %! d = datetime ('05.03.2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5.3.2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!test %! d = datetime ('05.03.24', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('05/03/2024', 'Locale', 'de_DE'); %!error ... %! datetime ('5/3/24', 'Locale', 'de_DE'); %!error ... %! datetime ('05/03/24', 'Locale', 'de_DE'); %!error ... %! datetime ('03/05/2024', 'Locale', 'de_DE'); %!error ... %! datetime ('3/5/24', 'Locale', 'de_DE'); %!error ... %! datetime ('05-03-2024', 'Locale', 'de_DE'); %!test %! d = datetime ('05.03.2024 14:30', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05.03.2024 14:30:00', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('05/03/2024 14:30', 'Locale', 'de_DE'); %!error ... %! datetime ('5 März 2024 14:30', 'Locale', 'de_DE'); %!test %! d = datetime ('5. März 2024 14:30', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('5 de März de 2024 14:30', 'Locale', 'de_DE'); %!error ... %! datetime ('März 5, 2024 14:30', 'Locale', 'de_DE'); %!error ... %! datetime ('Dienstag, 5. März 2024', 'Locale', 'de_DE'); %!error ... %! datetime ('Dienstag 5 März 2024', 'Locale', 'de_DE'); %!error ... %! datetime ('Dienstag, 5 de März de 2024', 'Locale', 'de_DE'); %!error ... %! datetime ('Dienstag, 5 März 2024', 'Locale', 'de_DE'); %!error ... %! datetime ('Dienstag, März 5, 2024', 'Locale', 'de_DE'); %!test %! d = datetime ('2024-03-05', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('2024-03-05 14:30:00', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05-mar-2024 14:30:00', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05-mar-2024 14:30', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('marzo 5, 2024', 'Locale', 'es_ES'); %!test %! d = datetime ('5 marzo 2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05 marzo 2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('5. marzo 2024', 'Locale', 'es_ES'); ## (b) the 'de' form is read for every Spanish and Portuguese locale %!test %! d = datetime ('5 de marzo de 2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5 mar 2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (c) Spanish abbreviations may carry a point %!test %! d = datetime ('5 mar. 2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('5. mar. 2024', 'Locale', 'es_ES'); ## (b) the 'de' form is read for every Spanish and Portuguese locale %!test %! d = datetime ('5 de mar. de 2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('mar 5, 2024', 'Locale', 'es_ES'); %!test %! d = datetime ('05.03.2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5.3.2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!test %! d = datetime ('05.03.24', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!warning ... %! datetime ('05/03/2024', 'Locale', 'es_ES'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('5/3/24', 'Locale', 'es_ES'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('5/3/24', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('05/03/24', 'Locale', 'es_ES'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/24', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!warning ... %! datetime ('03/05/2024', 'Locale', 'es_ES'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('03/05/2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-03 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('3/5/24', 'Locale', 'es_ES'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('3/5/24', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-03 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('05-03-2024', 'Locale', 'es_ES'); %!test %! d = datetime ('05.03.2024 14:30', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05.03.2024 14:30:00', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!warning ... %! datetime ('05/03/2024 14:30', 'Locale', 'es_ES'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/2024 14:30', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('5 marzo 2024 14:30', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('5. marzo 2024 14:30', 'Locale', 'es_ES'); ## (b) the 'de' form is read for every Spanish and Portuguese locale %!test %! d = datetime ('5 de marzo de 2024 14:30', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('marzo 5, 2024 14:30', 'Locale', 'es_ES'); %!error ... %! datetime ('martes, 5. marzo 2024', 'Locale', 'es_ES'); %!error ... %! datetime ('martes 5 marzo 2024', 'Locale', 'es_ES'); %!error ... %! datetime ('martes, 5 de marzo de 2024', 'Locale', 'es_ES'); %!error ... %! datetime ('martes, 5 marzo 2024', 'Locale', 'es_ES'); %!error ... %! datetime ('martes, marzo 5, 2024', 'Locale', 'es_ES'); %!test %! d = datetime ('2024-03-05', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('2024-03-05 14:30:00', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05-mar-2024 14:30:00', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05-mar-2024 14:30', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('marzo 5, 2024', 'Locale', 'it_IT'); %!test %! d = datetime ('5 marzo 2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05 marzo 2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('5. marzo 2024', 'Locale', 'it_IT'); %!error ... %! datetime ('5 de marzo de 2024', 'Locale', 'it_IT'); %!test %! d = datetime ('5 mar 2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5 mar 2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('5. mar 2024', 'Locale', 'it_IT'); %!error ... %! datetime ('5 de mar de 2024', 'Locale', 'it_IT'); %!error ... %! datetime ('mar 5, 2024', 'Locale', 'it_IT'); %!test %! d = datetime ('05.03.2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5.3.2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!test %! d = datetime ('05.03.24', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!warning ... %! datetime ('05/03/2024', 'Locale', 'it_IT'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('5/3/24', 'Locale', 'it_IT'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('5/3/24', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('05/03/24', 'Locale', 'it_IT'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/24', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!warning ... %! datetime ('03/05/2024', 'Locale', 'it_IT'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('03/05/2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-03 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('3/5/24', 'Locale', 'it_IT'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('3/5/24', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-03 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('05-03-2024', 'Locale', 'it_IT'); %!test %! d = datetime ('05.03.2024 14:30', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05.03.2024 14:30:00', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!warning ... %! datetime ('05/03/2024 14:30', 'Locale', 'it_IT'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/2024 14:30', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('5 marzo 2024 14:30', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('5. marzo 2024 14:30', 'Locale', 'it_IT'); %!error ... %! datetime ('5 de marzo de 2024 14:30', 'Locale', 'it_IT'); %!error ... %! datetime ('marzo 5, 2024 14:30', 'Locale', 'it_IT'); %!error ... %! datetime ('martedì, 5. marzo 2024', 'Locale', 'it_IT'); %!error ... %! datetime ('martedì 5 marzo 2024', 'Locale', 'it_IT'); %!error ... %! datetime ('martedì, 5 de marzo de 2024', 'Locale', 'it_IT'); %!error ... %! datetime ('martedì, 5 marzo 2024', 'Locale', 'it_IT'); %!error ... %! datetime ('martedì, marzo 5, 2024', 'Locale', 'it_IT'); %!test %! d = datetime ('2024-03-05', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('2024-03-05 14:30:00', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05-mar-2024 14:30:00', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05-mar-2024 14:30', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('março 5, 2024', 'Locale', 'pt_PT'); %!error ... %! datetime ('5 março 2024', 'Locale', 'pt_PT'); %!error ... %! datetime ('05 março 2024', 'Locale', 'pt_PT'); %!error ... %! datetime ('5. março 2024', 'Locale', 'pt_PT'); ## (b) the 'de' form is read for every Spanish and Portuguese locale %!test %! d = datetime ('5 de março de 2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('5 mar 2024', 'Locale', 'pt_PT'); %!error ... %! datetime ('5 mar. 2024', 'Locale', 'pt_PT'); %!error ... %! datetime ('5. mar. 2024', 'Locale', 'pt_PT'); ## (b) the 'de' form is read for every Spanish and Portuguese locale %!test %! d = datetime ('5 de mar. de 2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('mar 5, 2024', 'Locale', 'pt_PT'); %!test %! d = datetime ('05.03.2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5.3.2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!test %! d = datetime ('05.03.24', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05/03/2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!test %! d = datetime ('5/3/24', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!test %! d = datetime ('05/03/24', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('03/05/2024', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-03 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!test %! d = datetime ('3/5/24', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-03 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('05-03-2024', 'Locale', 'pt_PT'); %!test %! d = datetime ('05.03.2024 14:30', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05.03.2024 14:30:00', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05/03/2024 14:30', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('5 março 2024 14:30', 'Locale', 'pt_PT'); %!error ... %! datetime ('5. março 2024 14:30', 'Locale', 'pt_PT'); ## (b) the 'de' form is read for every Spanish and Portuguese locale %!test %! d = datetime ('5 de março de 2024 14:30', 'Locale', 'pt_PT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('março 5, 2024 14:30', 'Locale', 'pt_PT'); %!error ... %! datetime ('terça-feira, 5. março 2024', 'Locale', 'pt_PT'); %!error ... %! datetime ('terça-feira 5 março 2024', 'Locale', 'pt_PT'); %!error ... %! datetime ('terça-feira, 5 de março de 2024', 'Locale', 'pt_PT'); %!error ... %! datetime ('terça-feira, 5 março 2024', 'Locale', 'pt_PT'); %!error ... %! datetime ('terça-feira, março 5, 2024', 'Locale', 'pt_PT'); %!test %! d = datetime ('2024-03-05', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('2024-03-05 14:30:00', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05-Μα�-2024 14:30:00', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05-Μα�-2024 14:30', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('Μα�τίου 5, 2024', 'Locale', 'el_GR'); %!test %! d = datetime ('5 Μα�τίου 2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05 Μα�τίου 2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('5. Μα�τίου 2024', 'Locale', 'el_GR'); %!error ... %! datetime ('5 de Μα�τίου de 2024', 'Locale', 'el_GR'); %!test %! d = datetime ('5 Μα� 2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5 Μα� 2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('5. Μα� 2024', 'Locale', 'el_GR'); %!error ... %! datetime ('5 de Μα� de 2024', 'Locale', 'el_GR'); %!error ... %! datetime ('Μα� 5, 2024', 'Locale', 'el_GR'); %!test %! d = datetime ('05.03.2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5.3.2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!test %! d = datetime ('05.03.24', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!warning ... %! datetime ('05/03/2024', 'Locale', 'el_GR'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('5/3/24', 'Locale', 'el_GR'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('5/3/24', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('05/03/24', 'Locale', 'el_GR'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/24', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!warning ... %! datetime ('03/05/2024', 'Locale', 'el_GR'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('03/05/2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-03 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('3/5/24', 'Locale', 'el_GR'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('3/5/24', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-05-03 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('05-03-2024', 'Locale', 'el_GR'); %!test %! d = datetime ('05.03.2024 14:30', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('05.03.2024 14:30:00', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!warning ... %! datetime ('05/03/2024 14:30', 'Locale', 'el_GR'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/2024 14:30', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! d = datetime ('5 Μα�τίου 2024 14:30', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 14:30:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %!error ... %! datetime ('5. Μα�τίου 2024 14:30', 'Locale', 'el_GR'); %!error ... %! datetime ('5 de Μα�τίου de 2024 14:30', 'Locale', 'el_GR'); %!error ... %! datetime ('Μα�τίου 5, 2024 14:30', 'Locale', 'el_GR'); %!error ... %! datetime ('Τ�ίτη, 5. Μα�τίου 2024', 'Locale', 'el_GR'); %!error ... %! datetime ('Τ�ίτη 5 Μα�τίου 2024', 'Locale', 'el_GR'); %!error ... %! datetime ('Τ�ίτη, 5 de Μα�τίου de 2024', 'Locale', 'el_GR'); %!error ... %! datetime ('Τ�ίτη, 5 Μα�τίου 2024', 'Locale', 'el_GR'); %!error ... %! datetime ('Τ�ίτη, Μα�τίου 5, 2024', 'Locale', 'el_GR'); %!test %! d = datetime ('5 March 2024', 'Locale', 'en_GB'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!warning ... %! datetime ('05/03/2024', 'Locale', 'en_GB'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/2024', 'Locale', 'en_GB'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('13/03/2024', 'Locale', 'en_GB'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-13 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('13/03/2024', 'Locale', 'en_US'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-13 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Jänner-2024', 'Locale', 'de_AT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5. Jänner 2024', 'Locale', 'de_AT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-01-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05.03.2024', 'Locale', 'de_AT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (b) the 'de' form is read for every Spanish and Portuguese locale %!test %! d = datetime ('5 de marzo de 2024', 'Locale', 'es_MX'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!warning ... %! datetime ('05/03/2024', 'Locale', 'es_MX'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/2024', 'Locale', 'es_MX'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5 de março de 2024', 'Locale', 'pt_BR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!warning ... %! datetime ('05/03/2024', 'Locale', 'pt_BR'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('05/03/2024', 'Locale', 'pt_BR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('2024-03-05', 'Locale', 'fr_CA'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5 mars 2024', 'Locale', 'fr_CA'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('13/03/2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-13 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('13.03.2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-13 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('13/03/2024', 'Locale', 'it_IT'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-13 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('13/03/2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-13 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (a) a two-digit year takes the pivot; R2024a reads the year 24 %!warning ... %! datetime ('5/3/24', 'Locale', 'el_GR'); %!test %! warning ('off', 'Octave:datetime:ambiguous-format', 'local'); %! d = datetime ('5/3/24', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-MÄRZ-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-märz-2024', 'Locale', 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-MARS-2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5 MARS 2024', 'Locale', 'fr_FR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (b) the 'de' form is read for every Spanish and Portuguese locale %!test %! d = datetime ('5 de MARZO de 2024', 'Locale', 'es_ES'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-ΜΑΡ-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## (d) Greek capitals are read, as with 'InputFormat' %!test %! d = datetime ('5 ΜΑΡΤΙΟΥ 2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('5 Μά�τιος 2024', 'Locale', 'el_GR'); %!test %! d = datetime ('05-μα�-2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5 μα�τίου 2024', 'Locale', 'el_GR'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!error ... %! datetime ('05-Μά�τιος-2024', 'Locale', 'el_GR'); %!test %! d = datetime ({'05.03.2024', '13.03.2024'}, 'Locale', 'de_DE'); %! assert_equal (cellstr (char (d, 'yyyy-MM-dd HH:mm:ss')), ... %! {'2024-03-05 00:00:00'; '2024-03-13 00:00:00'}); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ({'05.03.2024', '5. März 2024'}, 'Locale', 'de_DE'); %! assert_equal (cellstr (char (d, 'yyyy-MM-dd HH:mm:ss')), ... %! {'2024-03-05 00:00:00'; 'NaT'}); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## A full name is read where the first entry had a short one. %!test %! d = datetime ({'5 mars 2024', '6 avril 2024'}, 'Locale', 'fr_FR'); %! assert_equal (cellstr (char (d, 'yyyy-MM-dd HH:mm:ss')), ... %! {'2024-03-05 00:00:00'; '2024-04-06 00:00:00'}); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ({'05/03/2024', '13/03/2024'}, 'Locale', 'fr_FR'); %! assert_equal (cellstr (char (d, 'yyyy-MM-dd HH:mm:ss')), ... %! {'2024-03-05 00:00:00'; '2024-03-13 00:00:00'}); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('05-Mar-2024', 'Locale', 'system'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); %!test %! d = datetime ('5. März 2024', 'InputFormat', 'd. MMMM yyyy', 'Locale', ... %! 'de_DE'); %! assert_equal (char (d, 'yyyy-MM-dd HH:mm:ss'), '2024-03-05 00:00:00'); %! assert_equal (d.Format, 'dd-MMM-uuuu'); ## Test 'datetime' constructor -- weekday name fields (consumed, but validated) %!assert_equal (day (datetime ('7 09-Mar-2024', 'InputFormat', ... %! 'e dd-MMM-yyyy')), 9) %!assert_equal (day (datetime ('07 09-Mar-2024', ... %! 'InputFormat', 'ee dd-MMM-yyyy')), 9) %!assert_equal (day (datetime ('Saturday, 09-Mar-2024', ... %! 'InputFormat', 'eeee, dd-MMM-yyyy')), 9) %!assert_equal (day (datetime ('Sat, 09-Mar-2024', ... %! 'InputFormat', 'eee, dd-MMM-yyyy')), 9) %!assert_equal (day (datetime ('vendredi 09.03.2024', ... %! 'InputFormat', 'eeee dd.MM.yyyy', 'Locale', 'fr_FR')), 9) %!assert_equal (day (datetime ('Sa. 09.03.2024', ... %! 'InputFormat', 'eee dd.MM.yyyy', 'Locale', 'de_DE')), 9) %!assert_equal (day (datetime ('sábado 09.03.2024', ... %! 'InputFormat', 'eeee dd.MM.yyyy', 'Locale', 'es_ES')), 9) %!assert_equal (day (datetime ('venerdì 09.03.2024', ... %! 'InputFormat', 'eeee dd.MM.yyyy', 'Locale', 'it_IT')), 9) %!assert_equal (day (datetime ('segunda-feira, 11.03.2024', ... %! 'InputFormat', 'eeee, dd.MM.yyyy', 'Locale', 'pt_BR')), 11) %!error ... %! datetime ('Xyzzy, 09-Mar-2024', 'InputFormat', 'eeee, dd-MMM-yyyy') ## Test 'datetime' constructor -- localized day-period ('a') markers. Most ## locales use 'AM'/'PM'; Spanish uses "a. m."/"p. m." with a UTF-8 no-break ## space (U+00A0 = bytes 194 160). %!assert_equal (hour (datetime ('02:30 PM 09-Mar-2024', ... %! 'InputFormat', 'hh:mm a dd-MMM-yyyy')), 14) %!assert_equal (hour (datetime ('02:30 PM 09.03.2024', ... %! 'InputFormat', 'hh:mm a dd.MM.yyyy', 'Locale', 'fr_FR')), 14) %!assert_equal (hour (datetime ('02:30 AM 09.03.2024', ... %! 'InputFormat', 'hh:mm a dd.MM.yyyy', 'Locale', 'de_DE')), 2) %!assert_equal (hour (datetime (['02:30 p.', char([194, 160]), ... %! 'm. 09.03.2024'], ... %! 'InputFormat', 'hh:mm a dd.MM.yyyy', 'Locale', 'es_ES')), 14) %!assert_equal (hour (datetime (['02:30 a.', char([194, 160]), ... %! 'm. 09.03.2024'], ... %! 'InputFormat', 'hh:mm a dd.MM.yyyy', 'Locale', 'es_ES')), 2) %!error ... %! datetime ('02:30 PM 09.03.2024', 'InputFormat', 'hh:mm a dd.MM.yyyy', ... %! 'Locale', 'es_ES') ## Test 'datetime' constructor -- Greek locale ('el'). Month names are the ## genitive (format) forms; day-period is 'π.μ.'/'μ.μ.'. %!assert_equal (month (datetime ('9 Μα�τίου 2024', ... %! 'InputFormat', 'd MMMM yyyy', 'Locale', 'el_GR')), 3) %!assert_equal (month (datetime ('9 Μα�ου 2024', ... %! 'InputFormat', 'd MMMM yyyy', 'Locale', 'el_GR')), 5) %!assert_equal (month (datetime ('9 Αυγ 2024', ... %! 'InputFormat', 'd MMM yyyy', 'Locale', 'el_GR')), 8) %!assert_equal (day (datetime ('Σάββατο, 09.03.2024', ... %! 'InputFormat', 'eeee, dd.MM.yyyy', 'Locale', 'el_GR')), 9) %!assert_equal (day (datetime ('Κυ�, 10.03.2024', ... %! 'InputFormat', 'eee, dd.MM.yyyy', 'Locale', 'el_GR')), 10) %!assert_equal (hour (datetime ('02:30 μ.μ. 09.03.2024', ... %! 'InputFormat', 'hh:mm a dd.MM.yyyy', 'Locale', 'el_GR')), 14) %!assert_equal (hour (datetime ('02:30 π.μ. 09.03.2024', ... %! 'InputFormat', 'hh:mm a dd.MM.yyyy', 'Locale', 'el_GR')), 2) %!error ... %! datetime ('9 Μά�τιος 2024', 'InputFormat', 'd MMMM yyyy', 'Locale', 'el_GR') ## Test 'datetime' constructor -- case/accent folding of names. Latin names are ## case-insensitive; Greek names are case- AND accent-insensitive, so the ## accentless all-caps spelling (idiomatic in Greek) is accepted. %!assert_equal (month (datetime ('9 MARCH 2024', 'InputFormat', ... %! 'd MMMM yyyy')), 3) %!assert_equal (month (datetime ('9 février 2024', ... %! 'InputFormat', 'd MMMM yyyy', 'Locale', 'fr_FR')), 2) %!assert_equal (month (datetime ('9 μα�τίου 2024', ... %! 'InputFormat', 'd MMMM yyyy', 'Locale', 'el_GR')), 3) %!assert_equal (month (datetime ('9 ΜΑΡΤΊΟΥ 2024', ... %! 'InputFormat', 'd MMMM yyyy', 'Locale', 'el_GR')), 3) %!assert_equal (month (datetime ('9 ΜΑΡΤΙΟΥ 2024', ... %! 'InputFormat', 'd MMMM yyyy', 'Locale', 'el_GR')), 3) %!assert_equal (day (datetime ('ΣΑΒΒΑΤΟ, 09.03.2024', ... %! 'InputFormat', 'eeee, dd.MM.yyyy', 'Locale', 'el_GR')), 9) %!assert_equal (hour (datetime ('02:30 Μ.Μ. 09.03.2024', ... %! 'InputFormat', 'hh:mm a dd.MM.yyyy', 'Locale', 'el_GR')), 14) %!error ... %! datetime ('ΞΞΞ, 09.03.2024', 'InputFormat', 'eeee, dd.MM.yyyy', 'Locale', ... %! 'el_GR') ## Test 'dispstrings' and 'cellstr' %!assert_equal (dispstrings (datetime (2020, 1, 1)), {'01-Jan-2020'}) %!assert_equal (dispstrings (datetime (2020, 1, 1, 12, 30, ... %! 0)), {'01-Jan-2020 12:30:00'}) %!assert_equal (dispstrings (datetime (2020, 1, 1, 12, 30, ... %! 45.5)), {'01-Jan-2020 12:30:45'}) %!assert_equal (cellstr (datetime (2020, 1, 1)), {'01-Jan-2020'}) %!assert_equal (cellstr (datetime (2020, 12, 25)), {'25-Dec-2020'}) %!assert_equal (cellstr (NaT), {'NaT'}) %!assert_equal (class (cellstr (datetime (2020, 1, 1))), 'cell') %!assert_equal (size (cellstr (datetime (2020, 1, [1; 2; 3]))), [3, 1]) ## Test 'char' %!assert_equal (char (datetime (2020, 1, 1)), '01-Jan-2020') %!assert_equal (class (char (datetime (2020, 1, 1))), 'char') %!assert_equal (rows (char (datetime (2020, 1, [1; 2]))), 2) ## Test Format seam -- 'default' is a data-dependent sentinel (date-only when ## every element sits at midnight, date+time when any time-of-day is nonzero) %!assert_equal (datetime (2026, 3, 9).Format, 'dd-MMM-uuuu') %!assert_equal (datetime (2026, 3, 9, 0, 0, 0).Format, 'dd-MMM-uuuu') %!assert_equal (datetime (2026, 3, 9, 0, 0, 0.5).Format, 'dd-MMM-uuuu HH:mm:ss') %!assert_equal (datetime (2026, 3, 9, 13, 0, 0).Format, 'dd-MMM-uuuu HH:mm:ss') %!assert_equal (datetime (NaN, 1, 1).Format, 'dd-MMM-uuuu HH:mm:ss') %!test %! m = [datetime(2026,3,9,0,0,0); datetime(2026,3,9,13,0,0)]; %! assert_equal (m.Format, 'dd-MMM-uuuu HH:mm:ss'); %! assert_equal (dispstrings (m), ... %! {'09-Mar-2026 00:00:00'; '09-Mar-2026 13:00:00'}); %!test %! m = [datetime(2026,3,9,0,0,0); datetime(2026,3,10,0,0,0)]; %! assert_equal (m.Format, 'dd-MMM-uuuu'); ## Test Format seam -- 'default' re-resolves after arithmetic; 'defaultdate' ## is always date-only regardless of the data %!test %! d = datetime (2026, 3, 9) + hours (5); %! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss'); %! assert_equal (char (d), '09-Mar-2026 05:00:00'); %!test %! d = datetime (2026, 3, 9, 1, 2, 3); %! d.Format = 'defaultdate'; %! assert_equal (d.Format, 'dd-MMM-uuuu'); %! assert_equal (char (d), '09-Mar-2026'); ## Test Format seam -- default display omits sub-seconds (whole second shown) %!assert_equal (char (datetime (2026, 3, 9, 14, 5, ... %! 3.75)), '09-Mar-2026 14:05:03') ## Test Format seam -- LDML year/month/day fields %!assert_equal (char (datetime (2026, 3, 9), 'y'), '2026') %!assert_equal (char (datetime (2026, 3, 9), 'yy'), '26') %!assert_equal (char (datetime (2026, 3, 9), 'yyyy'), '2026') %!assert_equal (char (datetime (26, 3, 9), 'yyyy'), '0026') %!assert_equal (char (datetime (2026, 3, 9), 'uuuuu'), '02026') %!assert_equal (char (datetime (2026, 3, 9), 'M'), '3') %!assert_equal (char (datetime (2026, 3, 9), 'MM'), '03') %!assert_equal (char (datetime (2026, 3, 9), 'MMM'), 'Mar') %!assert_equal (char (datetime (2026, 3, 9), 'MMMM'), 'March') %!assert_equal (char (datetime (2026, 3, 9), 'MMMMM'), 'M') %!assert_equal (char (datetime (2026, 3, 9), 'd'), '9') %!assert_equal (char (datetime (2026, 3, 9), 'dd'), '09') %!assert_equal (char (datetime (2026, 3, 9), 'D'), '68') %!assert_equal (char (datetime (2026, 3, 9), 'DDD'), '068') ## Test Format seam -- LDML time, fractional, and meridiem fields %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), 'H'), '14') %!assert_equal (char (datetime (2026, 3, 9, 9, 5, 3), 'H'), '9') %!assert_equal (char (datetime (2026, 3, 9, 9, 5, 3), 'HH'), '09') %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), 'h'), '2') %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), 'hh'), '02') %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), 'mm'), '05') %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), 'ss'), '03') %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3.75), 'ss'), '03') %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3.5), 'ss.SSS'), '03.500') %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), 'a'), 'PM') %!assert_equal (char (datetime (2026, 3, 9, 9, 5, 3), 'a'), 'AM') ## Test Format seam -- 12-hour clock edges (midnight and noon) %!assert_equal (char (datetime (2026, 3, 9, 0, 5, 3), 'hh:mm a'), '12:05 AM') %!assert_equal (char (datetime (2026, 3, 9, 12, 5, 3), 'hh:mm a'), '12:05 PM') %!assert_equal (char (datetime (2026, 3, 9, 23, 5, 3), 'hh:mm a'), '11:05 PM') ## Test Format seam -- weekday (Sunday = 1), quarter, era, week of month %!assert_equal (char (datetime (2026, 3, 9), 'e'), '2') %!assert_equal (char (datetime (2026, 3, 9), 'ee'), '02') %!assert_equal (char (datetime (2026, 3, 9), 'eee'), 'Mon') %!assert_equal (char (datetime (2026, 3, 9), 'eeee'), 'Monday') %!assert_equal (char (datetime (2026, 3, 8), 'eee'), 'Sun') %!assert_equal (char (datetime (2026, 3, 9), 'Q'), '1') %!assert_equal (char (datetime (2026, 7, 9), 'Q'), '3') %!assert_equal (char (datetime (2026, 3, 9), 'QQQ'), 'Q1') %!assert_equal (char (datetime (2026, 3, 9), 'QQQQ'), '1st quarter') %!assert_equal (char (datetime (2026, 3, 9), 'G'), 'CE') %!assert_equal (char (datetime (2026, 3, 9), 'W'), '2') ## Test Format seam -- quoted literals and full mixed patterns %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), ... %! 'yyyy-MM-dd''T''HH:mm:ss'), ... %! '2026-03-09T14:05:03') %!assert_equal (char (datetime (2026, 3, 9), 'MM/dd/yyyy'), '03/09/2026') %!assert_equal (char (datetime (2026, 3, 9), ... %! '''Day ''d'' of ''MMMM'), 'Day 9 of March') %!assert_equal (char (datetime (2026, 3, 9), ... %! 'eeee, MMMM d, yyyy'), 'Monday, March 9, 2026') ## Test Format seam -- time-zone offset fields (DST-aware; unzoned -> asterisks) %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3, 'TimeZone', ... %! 'America/New_York'), 'Z'), '-0400') %!assert_equal (char (datetime (2026, 1, 9, 14, 5, 3, 'TimeZone', ... %! 'America/New_York'), 'Z'), '-0500') %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3, 'TimeZone', ... %! 'America/New_York'), 'ZZZZ'), 'UTC-04:00') %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3, 'TimeZone', ... %! 'America/New_York'), 'XXX'), '-04:00') %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3, 'TimeZone', 'UTC'), ... %! 'X'), 'Z') %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3, 'TimeZone', 'UTC'), ... %! 'x'), '+00') %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3, 'TimeZone', 'UTC'), ... %! 'Z'), '+0000') %!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), 'ZZZZ'), '****') ## Test Format seam -- unsupported format symbols are rejected %!error ... %! datetime (2026, 3, 9, 'Format', 'yyyy-QQ-EE') %!error ... %! d = datetime (2026, 3, 9); d.Format = 'bad'; ## Test __datetime__ 'zoneabbrev' builtin mode -- the compiled foundation for ## the datetime 'z' format field: DST-aware IANA zone abbreviations, empty for ## NaT/Inf, and a cell array matching the input size. %!assert_equal (__datetime__ (2026, 3, 9, 14, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'America/New_York', ... %! 'Precision', 'microseconds'), {'EDT'}) %!assert_equal (__datetime__ (2026, 1, 9, 14, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'America/New_York', ... %! 'Precision', 'microseconds'), {'EST'}) %!assert_equal (__datetime__ (2026, 7, 9, 12, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'Europe/Athens', ... %! 'Precision', 'microseconds'), {'EEST'}) %!assert_equal (__datetime__ (2026, 1, 9, 12, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'Europe/Athens', ... %! 'Precision', 'microseconds'), {'EET'}) %!assert_equal (__datetime__ (2026, 3, 9, 14, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'UTC', ... %! 'Precision', 'microseconds'), {'UTC'}) %!assert_equal (__datetime__ (2026, [3; 7], 9, 14, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'America/New_York', ... %! 'Precision', 'microseconds'), {'EDT'; 'EDT'}) %!assert_equal (__datetime__ (NaN, 1, 1, 0, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'UTC', ... %! 'Precision', 'microseconds'), {''}) %!assert_equal (__datetime__ (Inf, 1, 1, 0, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'UTC', ... %! 'Precision', 'microseconds'), {''}) ## Test __datetime__ 'zoneabbrev' builtin mode -- southern-hemisphere daylight ## saving, half-hour zones, the numeric abbreviations tzdata writes itself, ## historic names, and the shape of the returned cell array. %!assert_equal (__datetime__ (2026, 1, 15, 12, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'Australia/Sydney', ... %! 'Precision', 'microseconds'), {'AEDT'}) %!assert_equal (__datetime__ (2026, 7, 15, 12, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'Australia/Sydney', ... %! 'Precision', 'microseconds'), {'AEST'}) %!assert_equal (__datetime__ (2026, 7, 15, 12, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'America/St_Johns', ... %! 'Precision', 'microseconds'), {'NDT'}) %!assert_equal (__datetime__ (2026, 1, 15, 12, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'America/St_Johns', ... %! 'Precision', 'microseconds'), {'NST'}) %!assert_equal (__datetime__ (2026, 7, 15, 12, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'Asia/Kolkata', ... %! 'Precision', 'microseconds'), {'IST'}) %!assert_equal (__datetime__ (2026, 7, 15, 12, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'Asia/Kathmandu', ... %! 'Precision', 'microseconds'), {'+0545'}) %!assert_equal (__datetime__ (1944, 7, 15, 12, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'America/New_York', ... %! 'Precision', 'microseconds'), {'EWT'}) %!assert_equal (__datetime__ (1900, 7, 15, 12, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'Europe/Athens', ... %! 'Precision', 'microseconds'), {'AMT'}) %!assert_equal (__datetime__ (2026, [1, 7; 1, 7], 15, 12, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'Australia/Sydney', ... %! 'Precision', 'microseconds'), ... %! {'AEDT', 'AEST'; 'AEDT', 'AEST'}) %!assert_equal (__datetime__ ([], [], [], [], [], [], 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', 'UTC', ... %! 'Precision', 'microseconds'), {}) ## Test Format seam -- time-zone name style: default is the Octave-specific ## 'iana', unzoned renders asterisks, invalid style is rejected. %!assert_equal (datetime.zoneNameStyle (), 'iana') %!assert_equal (char (datetime (2026, 3, 9), 'z'), '*') %!error ... %! datetime.zoneNameStyle ('bogus') ## Test Format seam -- 'iana' style shows the IANA abbreviation for every zone. %!test %! old = datetime.zoneNameStyle (); %! unwind_protect %! datetime.zoneNameStyle ('iana'); %! assert_equal (datetime.zoneNameStyle (), 'iana'); %! assert_equal (char (datetime (2026, 3, 9, 14, 0, 0, 'TimeZone', ... %! 'America/New_York'), 'z'), 'EDT'); %! assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ... %! 'Europe/Athens'), 'z'), 'EEST'); %! assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ... %! 'Asia/Tokyo'), 'z'), 'JST'); %! assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ... %! 'Asia/Shanghai'), 'z'), 'CST'); %! unwind_protect_cleanup %! datetime.zoneNameStyle (old); %! end_unwind_protect ## Test Format seam -- 'matlab' style: named abbreviation only for North ## American zones (plus GMT/UTC), numeric UTC offset elsewhere, with the ## (abbrev, offset) key resolving collisions (Shanghai/Havana). %!test %! old = datetime.zoneNameStyle (); %! unwind_protect %! datetime.zoneNameStyle ('matlab'); %! assert_equal (char (datetime (2026, 3, 9, 14, 0, 0, 'TimeZone', ... %! 'America/New_York'), 'z'), 'EDT'); %! assert_equal (char (datetime (2026, 1, 9, 14, 0, 0, 'TimeZone', ... %! 'America/New_York'), 'z'), 'EST'); %! assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ... %! 'America/Mexico_City'), 'z'), 'CST'); %! assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ... %! 'Pacific/Honolulu'), 'z'), 'HST'); %! assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ... %! 'Europe/Athens'), 'z'), 'UTC+3'); %! assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ... %! 'Asia/Tokyo'), 'z'), 'UTC+9'); %! assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ... %! 'Asia/Kolkata'), 'z'), 'UTC+5:30'); %! assert_equal (char (datetime (2026, 1, 15, 12, 0, 0, 'TimeZone', ... %! 'America/St_Johns'), 'z'), 'UTC-3:30'); %! assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ... %! 'Asia/Shanghai'), 'z'), 'UTC+8'); %! assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ... %! 'America/Havana'), 'z'), 'UTC-4'); %! assert_equal (char (datetime (2026, 3, 9, 14, 0, 0, 'TimeZone', ... %! 'UTC'), 'z'), 'UTC'); %! unwind_protect_cleanup %! datetime.zoneNameStyle (old); %! end_unwind_protect ## Test Format seam -- per-format overrides ignore the session style: 'zzzz' ## forces 'iana', 'zzzzz' forces 'matlab'. %!test %! old = datetime.zoneNameStyle (); %! unwind_protect %! ath = datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', 'Europe/Athens'); %! datetime.zoneNameStyle ('matlab'); %! assert_equal (char (ath, 'zzzz'), 'EEST'); %! datetime.zoneNameStyle ('iana'); %! assert_equal (char (ath, 'zzzzz'), 'UTC+3'); %! unwind_protect_cleanup %! datetime.zoneNameStyle (old); %! end_unwind_protect ## Test Format seam -- a NaT in the array renders under every field, including ## the weekday fields, which used to abort the whole array %!test %! dt = [datetime(2024, 3, 9); NaT]; %! assert_equal (cellstr (dt, 'eeee'), {'Saturday'; 'NaT'}) %! assert_equal (cellstr (dt, 'eee, dd MMM yyyy'), ... %! {'Sat, 09 Mar 2024'; 'NaT'}) %! assert_equal (cellstr (dt, 'e'), {'7'; 'NaT'}) %! assert_equal (cellstr (dt, 'D'), {'69'; 'NaT'}) %! assert_equal (cellstr (dt, 'W'), {'2'; 'NaT'}) %!test %! dt = [datetime(2024, 3, 9); NaT; datetime(2024, 12, 25)]; %! assert_equal (cellstr (dt, 'eeee'), {'Saturday'; 'NaT'; 'Wednesday'}) ## Test Format seam -- every LDML date/time field renders MATLAB-exactly %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'yyyy'), '2024') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'yy'), '24') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'MMMM'), 'March') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'MMM'), 'Mar') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'MMMMM'), 'M') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'dd'), '09') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'D'), '69') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'eeee'), 'Saturday') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'eee'), 'Sat') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'a'), 'PM') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'hh:mm a'), '02:30 PM') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'HH'), '14') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'QQQ'), 'Q1') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'G'), 'CE') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'ss'), '45') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), ... %! 'eee, dd MMM yyyy HH:mm:ss'), 'Sat, 09 Mar 2024 14:30:45') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45.678), 'ss.S'), '45.6') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45.678), 'ss.SS'), '45.67') %!assert_equal (char (datetime (2024, 1, 1, 0, 0, 45.678), 'ss.SSS'), '45.678') %!assert_equal (char (datetime (2024, 1, 1, 0, 0, 45.001), 'ss.SSS'), '45.001') %!assert_equal (char (datetime (2024, 1, 1, 0, 0, 45.999), 'ss.SSS'), '45.999') %!assert_equal (char (datetime (2024, 1, 1, 0, 0, 12.5), 'ss.SSS'), '12.500') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'h:mm a'), '2:30 PM') %!assert_equal (char (datetime (2024, 1, 1, 0, 0, 0), 'h a'), '12 AM') %!assert_equal (char (datetime (2024, 1, 1, 12, 0, 0), 'h a'), '12 PM') %!assert_equal (char (datetime (2024, 1, 1, 0, 0, 0), 'HH'), '00') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'e'), '7') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'ee'), '07') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'u'), '2024') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'QQQQ'), '1st quarter') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'QQ'), '01') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'MM'), '03') %!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), ... %! 'yyyy/MM/dd''T''HH:mm'), '2024/03/09T14:30') ## Test 'ymd' %!test %! [y, m, d] = ymd (datetime (2020, 7, 4)); %! assert_equal (y, 2020); %! assert_equal (m, 7); %! assert_equal (d, 4); %!test %! [y, m, d] = ymd (datetime (2020, 1, [1; 2])); %! assert_equal (y, [2020; 2020]); %! assert_equal (d, [1; 2]); ## Test 'hms' %!test %! [h, m, s] = hms (datetime (2020, 1, 1, 13, 45, 30.5)); %! assert_equal (h, 13); %! assert_equal (m, 45); %! assert_equal (s, 30.5); ## Test 'year' %!assert_equal (year (datetime (2020, 1, 1)), 2020) %!assert_equal (year (datetime ([1999, 2, 1; 2021, 3, 1])), [1999; 2021]) %!assert_equal (year (NaT), NaN) ## Test 'quarter' %!assert_equal (quarter (datetime (2020, 1, 1)), 1) %!assert_equal (quarter (datetime (2020, 4, 1)), 2) %!assert_equal (quarter (datetime (2020, 7, 1)), 3) %!assert_equal (quarter (datetime (2020, 10, 1)), 4) %!assert_equal (quarter (datetime (2020, 12, 31)), 4) %!assert_equal (quarter (datetime (2020, [1, 4, 7, 10], 1)), [1, 2, 3, 4]) ## Test 'month' with type options %!assert_equal (month (datetime (2020, 6, 1)), 6) %!assert_equal (month (datetime (2020, 6, 1), 'monthofyear'), 6) %!assert_equal (month (datetime (2020, 7, 1), 'name'), {'July'}) %!assert_equal (month (datetime (2020, 7, 1), 'shortname'), {'Jul'}) %!assert_equal (month (datetime (2020, 1, 1), 'name'), {'January'}) %!assert_equal (month (datetime (2020, 12, 1), 'shortname'), {'Dec'}) %!error month (datetime (2020, 1, 1), 'bogus') ## Test 'day' with dayofmonth (default) %!assert_equal (day (datetime (2020, 6, 15)), 15) %!assert_equal (day (datetime (2020, 6, 15), 'dayofmonth'), 15) %!assert_equal (day (datetime (2020, 1, [1; 15; 31])), [1; 15; 31]) ## Test 'day' with dayofweek (1=Sunday..7=Saturday) %!assert_equal (day (datetime (2020, 1, 1), 'dayofweek'), 4) %!assert_equal (day (datetime (2000, 1, 1), 'dayofweek'), 7) %!assert_equal (day (datetime (2020, 2, 29), 'dayofweek'), 7) %!assert_equal (day (datetime (2024, 2, 29), 'dayofweek'), 5) %!assert_equal (day (datetime (1900, 1, 1), 'dayofweek'), 2) %!assert_equal (day (datetime (2020, 3, 1), 'dayofweek'), 1) %!assert_equal (day (datetime (2020, 1, [1; 8; 15]), 'dayofweek'), [4; 4; 4]) %!assert_equal (day (datetime (2020, [1, 2], [1, 29]), 'dayofweek'), [4, 7]) ## Test 'day' with iso-dayofweek (1=Monday..7=Sunday) %!assert_equal (day (datetime (2020, 1, 1), 'iso-dayofweek'), 3) %!assert_equal (day (datetime (2020, 1, 5), 'iso-dayofweek'), 7) %!assert_equal (day (datetime (2020, 1, 6), 'iso-dayofweek'), 1) ## Test 'day' with name/shortname %!assert_equal (day (datetime (2020, 1, 1), 'name'), {'Wednesday'}) %!assert_equal (day (datetime (2000, 1, 1), 'name'), {'Saturday'}) %!assert_equal (day (datetime (2020, 1, 1), 'shortname'), {'Wed'}) %!assert_equal (day (datetime (2020, 3, 1), 'shortname'), {'Sun'}) ## Test 'day' with dayofyear (regression: shape + leap-year borrow) %!assert_equal (day (datetime (2020, 1, 1), 'dayofyear'), 1) %!assert_equal (day (datetime (2020, 12, 31), 'dayofyear'), 366) %!assert_equal (day (datetime (2021, 12, 31), 'dayofyear'), 365) %!assert_equal (day (datetime (2020, 3, 1), 'dayofyear'), 61) %!assert_equal (day (datetime (2021, 3, 1), 'dayofyear'), 60) %!assert_equal (day (datetime (2020, 2, 29), 'dayofyear'), 60) %!assert_equal (day (datetime (2020, 3, [1; 2]), 'dayofyear'), [61; 62]) %!assert_equal (day (datetime (2020, [1, 3; 6, 12], [1, 1; 1, 31]), ... %! 'dayofyear'), [1, 61; 153, 366]) %!assert_equal (day (NaT, 'dayofyear'), NaN) %!error day (datetime (2020, 1, 1), 'bogus') ## Test component accessors -- year-boundary, leap, and quarter/dow edges %!assert_equal (day (datetime (2024, 12, 31), 'dayofyear'), 366) %!assert_equal (day (datetime (2023, 12, 31), 'dayofyear'), 365) %!assert_equal (week (datetime (2024, 12, 31)), 53) %!assert_equal (week (datetime (2025, 1, 1)), 1) %!assert_equal (quarter (datetime (2024, [3 4 6 7 9 10 12], ... %! 1)), [1 2 2 3 3 4 4]) %!assert_equal (second (datetime (2024, 1, 1, 23, 59, 59), ... %! 'secondofday'), 86399) %!assert_equal (day (datetime ([2024 2000 1900], 1, 1), 'dayofweek'), [2 7 2]) ## Test component accessors -- NaT yields NaN, arrays preserve shape %!assert_equal (isnan (year (NaT)), true) %!assert_equal (isnan (quarter (NaT)), true) %!assert_equal (isnan (week (NaT)), true) %!assert_equal (year (datetime ([2020 2021 2022], 1, 1)), [2020 2021 2022]) %!assert_equal (month (datetime (2024, [1 6 12], 1)), [1 6 12]) %!assert_equal (hour (datetime (2024, 1, 1, [0 6 12 18], 0, 0)), [0 6 12 18]) %!assert_equal (ismissing ([datetime(2024, 1, 1), NaT]), logical ([0 1])) ## Test 'dateshift' -- 'dayofweek' next and previous occurrences %!assert_equal (char (dateshift (datetime (2024, 3, 13), 'dayofweek', ... %! 'Sunday'), 'yyyy-MM-dd'), '2024-03-17') %!assert_equal (char (dateshift (datetime (2024, 3, 13), 'dayofweek', ... %! 'Sunday', 'previous'), 'yyyy-MM-dd'), '2024-03-10') ## Test conversions and calendar -- array shapes and further edges %!assert_equal (yyyymmdd (datetime (2024, [3 12], [9 31])), [20240309 20241231]) %!assert_equal (datenum (datetime (2024, 3, 9, 12, 0, 0)), 739320.5) %!assert_equal (hours (diff (datetime (2024, 1, [1 3 7]), 1)), [48 96]) %!assert_equal (cellstr (between (datetime (2020, 3, 15), ... %! datetime (2024, 3, 15), 'years')), {'4y'}) %!assert_equal (isregular (datetime (2020:2:2026, 1, 1), 'years'), true) %!assert_equal (posixtime (datetime (1970, 1, [1 2], 'TimeZone', ... %! 'UTC')), [0 86400]) %!assert_equal (exceltime (datetime (1900, 1, [1 2])), [1 2]) ## Test 'caldiff' -- single-component differences over a series %!assert_equal (cellstr (caldiff (datetime ([2020 2022 2025], 1, 1), ... %! 'Years')), ... %! {'2y', '3y'}) %!assert_equal (cellstr (caldiff (datetime (2024, 1, [1 5 12]), ... %! 'Days')), {'4d', '7d'}) %!assert_equal (cellstr (caldiff (datetime (2024, 1, [1 8 22]), ... %! 'Weeks')), {'1w', '2w'}) ## Test 'caldiff' -- month-end clamp counts match MATLAB (values, not display; ## a zero single component renders as '0d' here vs MATLAB '0mo' -- a documented ## calendarDuration rendering deviation, the counts are identical) %!assert_equal (calmonths (caldiff (datetime (2024, [1 2 3], 31), ... %! 'Months')), [1 0]) %!assert_equal (split (caldiff (datetime (2024, [1 2 3], 31), 'Months'), ... %! 'days'), [0 0]) %!assert_equal (calmonths (caldiff (datetime (2024, [2 3], [29 31]), ... %! 'Months')), 1) %!assert_equal (calmonths (caldiff (datetime (2024, [3 4], [31 30]), ... %! 'Months')), 1) %!assert_equal (calmonths (caldiff (datetime (2024, [1 3], [31 31]), ... %! 'Months')), 2) ## Test 'dateshift' -- 'dayofweek' with next/previous/nth-occurrence rules %!assert_equal (char (dateshift (datetime (2024, 3, 13), 'dayofweek', ... %! 'Monday', ... %! 'next'), 'yyyy-MM-dd'), '2024-03-18') %!assert_equal (char (dateshift (datetime (2024, 3, 13), 'dayofweek', ... %! 'Friday', ... %! 'previous'), 'yyyy-MM-dd'), '2024-03-08') %!assert_equal (char (dateshift (datetime (2024, 3, 1), 'dayofweek', ... %! 'Sunday', ... %! 2), 'yyyy-MM-dd'), '2024-03-10') ## Test 'colon' -- hour and calendar-week steps count correctly %!assert_equal (numel (datetime (2024, 1, 1):hours (12):datetime (2024, 1, ... %! 2, ... %! 12, 0, 0)), 4) %!assert_equal (numel (datetime (2024, 1, 1):calweeks (1):datetime (2024, 1, ... %! 22)), 4) ## Test arithmetic -- chained calendar addition and duration subtraction %!assert_equal (char (datetime (2024, 1, 31) + calmonths (1) + caldays (5), ... %! 'yyyy-MM-dd'), '2024-03-05') %!assert_equal (char (datetime (2024, 1, 1) - minutes (90), ... %! 'yyyy-MM-dd HH:mm:ss'), '2023-12-31 22:30:00') ## Test conversions -- round-trip through posixtime and datenum %!assert_equal (char (datetime (posixtime (datetime (2024, 6, 15, 10, 30, 0, ... %! 'TimeZone', 'UTC')), 'ConvertFrom', 'posixtime', 'TimeZone', ... %! 'UTC'), 'yyyy-MM-dd HH:mm:ss'), '2024-06-15 10:30:00') %!assert_equal (char (datetime (datenum (datetime (2024, 6, 15)), ... %! 'ConvertFrom', 'datenum'), 'yyyy-MM-dd'), '2024-06-15') ## Test 'week' -- week 1 always contains January 1; year-end weeks %!assert_equal (week (datetime (2020, 1, 1)), 1) %!assert_equal (week (datetime (2021, 1, 1)), 1) %!assert_equal (week (datetime (2022, 1, 1)), 1) %!assert_equal (week (datetime (2023, 1, 1)), 1) %!assert_equal (week (datetime (2025, 1, 1)), 1) %!assert_equal (week (datetime (2026, 1, 1)), 1) %!assert_equal (week (datetime (2021, 12, 31)), 53) %!assert_equal (week (datetime (2022, 12, 31)), 53) %!assert_equal (week (datetime (2023, 12, 31)), 53) ## Test 'tzoffset' -- whole-hour, half-hour, and southern-hemisphere zones %!assert_equal (minutes (tzoffset (datetime (2024, 6, 15, ... %! 'TimeZone', 'Asia/Kolkata'))), 330) %!assert_equal (minutes (tzoffset (datetime (2024, 6, 15, ... %! 'TimeZone', 'Asia/Tokyo'))), 540) %!assert_equal (minutes (tzoffset (datetime (2024, 1, 15, ... %! 'TimeZone', 'Australia/Sydney'))), 660) %!assert_equal (minutes (tzoffset (datetime (2024, 7, 15, ... %! 'TimeZone', 'Australia/Sydney'))), 600) %!assert_equal (minutes (tzoffset (datetime (2024, 6, 15, ... %! 'TimeZone', 'Pacific/Marquesas'))), -570) ## Test 'isdst' -- southern-hemisphere and European DST windows %!assert_equal (isdst (datetime (2024, 1, 15, 'TimeZone', ... %! 'Australia/Sydney')), true) %!assert_equal (isdst (datetime (2024, 7, 15, 'TimeZone', ... %! 'Europe/London')), true) %!assert_equal (isdst (datetime (2024, 1, 15, 'TimeZone', ... %! 'Europe/London')), false) ## Test DST transitions -- a wall clock that is ambiguous (the hour the clock ## repeats) resolves to the later, standard-time offset; one that does not ## exist (the gap the clock skips) moves ahead by the length of that gap. ## Both used to abort Octave with an uncaught date.h exception. ## All values verified against MATLAB R2024a. %!assert_equal (char (datetime (2026, 11, 1, 1, 30, 0, 'TimeZone', ... %! 'America/New_York'), 'yyyy-MM-dd HH:mm:ss z'), ... %! '2026-11-01 01:30:00 EST') %!assert_equal (posixtime (datetime (2026, 11, 1, 1, 30, 0, 'TimeZone', ... %! 'America/New_York')), 1793514600) %!assert_equal (isdst (datetime (2026, 11, 1, 1, 30, 0, 'TimeZone', ... %! 'America/New_York')), false) %!assert_equal (seconds (tzoffset (datetime (2026, 11, 1, 1, 30, 0, ... %! 'TimeZone', 'America/New_York'))), -5 * 3600) %!assert_equal (char (datetime (2026, 3, 8, 2, 30, 0, 'TimeZone', ... %! 'America/New_York'), 'yyyy-MM-dd HH:mm:ss z'), ... %! '2026-03-08 03:30:00 EDT') %!assert_equal (posixtime (datetime (2026, 3, 8, 2, 30, 0, 'TimeZone', ... %! 'America/New_York')), 1772955000) %!assert_equal (isdst (datetime (2026, 3, 8, 2, 30, 0, 'TimeZone', ... %! 'America/New_York')), true) %!assert_equal (seconds (tzoffset (datetime (2026, 3, 8, 2, 30, 0, ... %! 'TimeZone', 'America/New_York'))), -4 * 3600) %!assert_equal (char (datetime (2026, 3, 8, 2, 0, 0, 'TimeZone', ... %! 'America/New_York')), '08-Mar-2026 03:00:00') %!assert_equal (char (datetime (2026, 3, 8, 2, 59, 59, 'TimeZone', ... %! 'America/New_York')), '08-Mar-2026 03:59:59') %!assert_equal (char (datetime (2026, 3, 29, 3, 30, 0, 'TimeZone', ... %! 'Europe/Athens')), '29-Mar-2026 04:30:00') %!assert_equal (seconds (tzoffset (datetime (2026, 10, 25, 3, 30, 0, ... %! 'TimeZone', 'Europe/Athens'))), 2 * 3600) %!assert_equal (char (datetime (2026, 10, 4, 2, 30, 0, 'TimeZone', ... %! 'Australia/Sydney')), '04-Oct-2026 03:30:00') %!assert_equal (seconds (tzoffset (datetime (2026, 4, 5, 2, 30, 0, ... %! 'TimeZone', 'Australia/Sydney'))), 10 * 3600) %!test %! ## Lord Howe shifts the clock by half an hour, so the gap is half an hour %! ## and 02:15 moves to 02:45 rather than to the transition itself. %! d = datetime (2026, 10, 4, 2, 15, 0, 'TimeZone', 'Australia/Lord_Howe'); %! assert_equal (char (d), '04-Oct-2026 02:45:00') %! assert_equal (seconds (tzoffset (d)), 11 * 3600) %!test %! ## Attaching a zone to an unzoned array applies the same rules. %! d = datetime (2026, 3, 8, 2, 30, 0); %! d.TimeZone = 'America/New_York'; %! assert_equal (char (d), '08-Mar-2026 03:30:00') %! a = datetime (2026, 11, 1, 1, 30, 0); %! a.TimeZone = 'America/New_York'; %! assert_equal (char (a, 'yyyy-MM-dd HH:mm:ss z'), '2026-11-01 01:30:00 EST') %!test %! ## A NaT alongside a skipped wall clock stays NaT. %! d = [datetime(2026, 3, 8, 2, 30, 0); NaT]; %! d.TimeZone = 'America/New_York'; %! assert_equal (cellstr (d), {'08-Mar-2026 03:30:00'; 'NaT'}) ## Test DST transitions -- text naming a wall clock its zone never shows is ## rejected, whereas the same wall clock given as numeric components is moved ## ahead by the gap. MATLAB draws exactly this distinction (R2024a). Under an ## 'InputFormat' a lone string errors and an array loses only that element to ## NaT; without one the whole input is rejected, as when no format is detected. %!error ... %! datetime ('2026-03-08 02:30:00', 'InputFormat', 'yyyy-MM-dd HH:mm:ss', ... %! 'TimeZone', 'America/New_York') %!error ... %! datetime ('2026-10-04 02:15:00', 'InputFormat', 'yyyy-MM-dd HH:mm:ss', ... %! 'TimeZone', 'Australia/Lord_Howe') %!error ... %! datetime ('2026-03-08 02:30:00', 'TimeZone', 'America/New_York') %!error ... %! datetime ({'2026-03-08 02:30:00', '2026-06-01 12:00:00'}, 'TimeZone', ... %! 'America/New_York') %!test %! d = datetime ({'2026-03-08 02:30:00', '2026-06-01 12:00:00'}, ... %! 'InputFormat', 'yyyy-MM-dd HH:mm:ss', ... %! 'TimeZone', 'America/New_York'); %! assert_equal (isnat (d), [true, false]) %! assert_equal (cellstr (d), {'NaT', '01-Jun-2026 12:00:00'}) %!test %! ## A repeated hour is not skipped, so it stays acceptable, and the boundaries %! ## of the gap itself are real wall clocks. %! a = datetime ('2026-11-01 01:30:00', 'InputFormat', ... %! 'yyyy-MM-dd HH:mm:ss', ... %! 'TimeZone', 'America/New_York'); %! assert_equal (char (a, 'yyyy-MM-dd HH:mm:ss z'), '2026-11-01 01:30:00 EST') %! b = datetime ('2026-03-08 01:59:59', 'InputFormat', ... %! 'yyyy-MM-dd HH:mm:ss', ... %! 'TimeZone', 'America/New_York'); %! assert_equal (char (b), '08-Mar-2026 01:59:59') %! c = datetime ('2026-03-08 03:00:00', 'InputFormat', ... %! 'yyyy-MM-dd HH:mm:ss', ... %! 'TimeZone', 'America/New_York'); %! assert_equal (char (c), '08-Mar-2026 03:00:00') %!test %! ## Unzoned text names no instant, so nothing is rejected. %! d = datetime ('2026-03-08 02:30:00', 'InputFormat', 'yyyy-MM-dd HH:mm:ss'); %! assert_equal (char (d), '08-Mar-2026 02:30:00') ## Test 'month'/'day' -- full names %!assert_equal (month (datetime (2024, 7, 1), 'name'), {'July'}) %!assert_equal (day (datetime (2024, 3, 10), 'name'), {'Sunday'}) ## Test Format seam -- numeric zone-offset fields (Z, x, X) %!assert_equal (char (datetime (2024, 7, 15, 14, 30, 0, 'TimeZone', ... %! 'America/New_York'), 'Z'), '-0400') %!assert_equal (char (datetime (2024, 7, 15, 14, 30, 0, 'TimeZone', ... %! 'America/New_York'), 'x'), '-04') %!assert_equal (char (datetime (2024, 7, 15, 14, 30, 0, 'TimeZone', ... %! 'America/New_York'), 'xxx'), '-04:00') %!assert_equal (char (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 'X'), 'Z') %!assert_equal (char (datetime (2024, 1, 1, 'TimeZone', 'Asia/Kolkata'), ... %! 'xxx'), '+05:30') ## Test Format seam -- ISO-8601 zone doublings and the UTC 'UTC'/'Z' forms %!assert_equal (char (datetime (2024, 1, 15, 'TimeZone', 'UTC'), 'ZZZZ'), 'UTC') %!assert_equal (char (datetime (2024, 1, 15, 'TimeZone', 'UTC'), 'ZZZZZ'), 'Z') %!assert_equal (char (datetime (2024, 1, 15, 'TimeZone', ... %! 'America/New_York'), ... %! 'ZZZZZ'), '-05:00') %!assert_equal (char (datetime (2024, 1, 15, 'TimeZone', 'Asia/Kolkata'), ... %! 'ZZZZ'), 'UTC+05:30') %!assert_equal (char (datetime (2024, 1, 15, 'TimeZone', 'UTC'), 'XXXXX'), 'Z') %!assert_equal (char (datetime (2024, 1, 15, 'TimeZone', 'UTC'), ... %! 'xxxxx'), '+00:00') %!assert_equal (char (datetime (2024, 1, 15, 'TimeZone', ... %! 'America/New_York'), ... %! 'xxxx'), '-0500') ## Test 'string'/'cellstr'/'char' -- array shapes %!assert_equal (cellstr (string (datetime (2024, 1, [1 2]), 'yyyy-MM-dd')), ... %! {'2024-01-01', '2024-01-02'}) %!assert_equal (cellstr (datetime (2024, 1, [1 2])), {'01-Jan-2024', ... %! '02-Jan-2024'}) %!assert_equal (size (char (datetime (2024, 1, [1; 2]), 'yyyy-MM-dd')), [2, 10]) ## Test component accessors -- N-D array preserves shape %!assert_equal (year (datetime (2024, 1, reshape (1:4, 2, 2))), ... %! [2024 2024; 2024 2024]) %!assert_equal (size (day (datetime (2024, 1, reshape (1:8, 2, 2, ... %! 2)))), [2 2 2]) ## Test 'convertTo' -- every numeric type on one fractional-day value %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), ... %! 'datenum'), 739320.25) %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), 'excel'), 45360.25) %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), ... %! 'excel1904'), 43898.25) %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), ... %! 'juliandate'), 2460378.75) %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), ... %! 'modifiedjuliandate'), ... %! 60378.25) %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), ... %! 'yyyymmdd'), 20240309) ## Test 'min'/'max' -- the returned index of the extreme element %!test %! [m, i] = min (datetime (2024, 3, [9 2 20])); %! assert_equal (i, 2); %!test %! [m, i] = max (datetime (2024, 3, [9 2 20])); %! assert_equal (i, 3); ## Test 'unique'/'sort' -- stable order and column-wise matrix sort %!assert_equal (cellstr (unique (datetime (2024, 1, [3 1 3 2]), 'stable')), ... %! {'03-Jan-2024', '01-Jan-2024', '02-Jan-2024'}) %!assert_equal (cellstr (sort (datetime (2024, 1, [3 1; 2 4]))), ... %! {'02-Jan-2024', '01-Jan-2024'; '03-Jan-2024', '04-Jan-2024'}) ## Test 'diff'/'caldiff' -- second-order and Time-component differences %!assert_equal (hours (diff (datetime (2024, 1, [1 2 4 8]), 2)), [24 48]) %!assert_equal (cellstr (caldiff (datetime (2024, 1, 1, [0 6 12], 0, 0), ... %! 'Time')), ... %! {'6h 0m 0s', '6h 0m 0s'}) ## Test conversions -- pre-epoch second and array datevec shape %!assert_equal (posixtime (datetime (1969, 12, 31, 23, 59, 59, 'TimeZone', ... %! 'UTC')), -1) %!assert_equal (size (datevec (datetime (2024, 1, [1;2;3]))), [3 6]) ## Test calendar diffs -- mixed default, day counts across leap February %!assert_equal (cellstr (caldiff (datetime ([2020 2024], [1 3], [15 20]))), ... %! {'4y 2mo 5d'}) %!assert_equal (cellstr (between (datetime (2024, 2, 28), ... %! datetime (2024, 3, 1), 'days')), {'2d'}) %!assert_equal (cellstr (between (datetime (2023, 2, 28), ... %! datetime (2023, 3, 1), 'days')), {'1d'}) ## Test 'dateshift' -- end of February respects leap years %!assert_equal (char (dateshift (datetime (2024, 2, 15), 'end', 'month'), ... %! 'yyyy-MM-dd'), '2024-02-29') %!assert_equal (char (dateshift (datetime (2023, 2, 15), 'end', 'month'), ... %! 'yyyy-MM-dd'), '2023-02-28') ## Test 'week'/'quarter' -- week-of-month span and all twelve months %!assert_equal (week (datetime (2024, 3, 1), 'weekofmonth'), 1) %!assert_equal (week (datetime (2024, 3, 31), 'weekofmonth'), 6) %!assert_equal (quarter (datetime (2024, 1:12, 1)), [1 1 1 2 2 2 3 3 3 4 4 4]) ## Test reductions -- DST-aware time of day, even-count median, N-normalisation %!assert_equal (hours (timeofday (datetime (2024, 3, 10, 3, 30, 0, ... %! 'TimeZone', ... %! 'America/New_York'))), 2.5) %!assert_equal (char (mean (datetime (2024, 1, [1 3])), ... %! 'yyyy-MM-dd'), '2024-01-02') %!assert_equal (char (median (datetime (2024, 1, [1 2 3 4])), ... %! 'yyyy-MM-dd HH:mm:ss'), '2024-01-02 12:00:00') %!assert_equal (char (std (datetime (2024, 1, [1 3 5]), 1)), '39:11:30') ## Test arithmetic -- fixed durations and calendar-unit additions %!assert_equal (char (datetime (2024, 1, 1) + hours (25), ... %! 'yyyy-MM-dd HH:mm:ss'), '2024-01-02 01:00:00') %!assert_equal (char (datetime (2024, 1, 1) + seconds (3661), ... %! 'yyyy-MM-dd HH:mm:ss'), '2024-01-01 01:01:01') %!assert_equal (char (datetime (2024, 2, 29) + calyears (4), 'yyyy-MM-dd'), ... %! '2028-02-29') %!assert_equal (char (datetime (2024, 1, 15) + calquarters (2), ... %! 'yyyy-MM-dd'), ... %! '2024-07-15') %!assert_equal (char (datetime (2024, 1, 1) + calweeks (3), 'yyyy-MM-dd'), ... %! '2024-01-22') ## Test conversions -- array outputs and the numeric-year construction %!assert_equal (exceltime (datetime (2024, [1 6 12], 1)), [45292 45444 45627]) %!assert_equal (juliandate (datetime ([2000 2024], 1, ... %! 1)), [2451544.5 2460310.5]) %!assert_equal (datenum (datetime ([2020 2024], 1, 1)), [737791 739252]) %!assert_equal (char (datetime (49, 1, 1), 'yyyy-MM-dd'), '0049-01-01') ## Test 'isbetween'/'min'/'max' -- text bounds and matrix reductions %!assert_equal (isbetween (datetime (2024, 6, 15), '2024-06-01', ... %! '2024-06-30'), true) %!assert_equal (cellstr (min (datetime (2024, 1, [3 1; 2 4]))), ... %! {'02-Jan-2024', '01-Jan-2024'}) %!assert_equal (cellstr (max (datetime (2024, 1, [3 1; 2 4]), [], 2)), ... %! {'03-Jan-2024'; '04-Jan-2024'}) ## Test 'caldiff'/'dateshift'/'string' -- quarters, week start, literal format %!assert_equal (cellstr (caldiff (datetime (2024, [1 4 7], 1), 'Quarters')), ... %! {'1q', '1q'}) %!assert_equal (char (dateshift (datetime (2024, 3, 13), 'start', 'week'), ... %! 'yyyy-MM-dd'), '2024-03-10') %!assert_equal (char (datetime (2024, 3, 9), '''Day'' d ''of'' MMMM'), ... %! 'Day 9 of March') ## Test 'between'/'isregular' -- quarter counts and weekly regularity %!assert_equal (cellstr (between (datetime (2024, 1, 1), ... %! datetime (2025, 1, 1), 'quarters')), {'4q'}) %!assert_equal (isregular (datetime (2024, 1, 1:7:22), 'weeks'), true) ## Test 'dateshift' -- start of quarter and end of day %!assert_equal (char (dateshift (datetime (2024, 5, 15), 'start', ... %! 'quarter'), ... %! 'yyyy-MM-dd'), '2024-04-01') %!assert_equal (char (dateshift (datetime (2024, 11, 15), 'start', ... %! 'quarter'), ... %! 'yyyy-MM-dd'), '2024-10-01') %!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 0, 0), 'end', ... %! 'day'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-16 00:00:00') ## Test conversions/components -- fractional posix, elapsed day, quarter edges %!assert_equal (posixtime (datetime (2024, 1, 1, 0, 0, 0.5, 'TimeZone', ... %! 'UTC')), ... %! 1704067200.5) %!assert_equal (seconds (timeofday (datetime (2024, 1, 1, 1, 1, 1))), 3661) %!assert_equal (exceltime (datetime (1904, 1, 1), '1904'), 0) %!assert_equal (quarter (datetime (2024, [3 4], 31)), [1 2]) %!assert_equal (second (datetime (2024, 1, 1, 0, 1, 30)), 30) %!assert_equal (minute (datetime (2024, 1, 1, 5, 45, 0)), 45) ## Test 'mode'/'diff'/'isweekend' -- most frequent, column diff, weekend run %!assert_equal (char (mode (datetime (2024, 1, [5 5 2 2 2])), 'yyyy-MM-dd'), ... %! '2024-01-02') %!assert_equal (hours (diff (datetime (2024, 1, [1;3;7]))), [48; 96]) %!assert_equal (isweekend (datetime (2024, 3, [16 17 18])), logical ([1 1 0])) ## Test component accessors -- assorted fixed values across the calendar %!assert_equal (year (datetime ([1999 2000 2001], 6, 15)), [1999 2000 2001]) %!assert_equal (char (datetime (2024, 12, 1), 'MMM'), 'Dec') %!assert_equal (char (datetime (2024, 3, 10), 'eee'), 'Sun') %!assert_equal (char (datetime (50, 1, 1), 'yyyy'), '0050') %!assert_equal (day (datetime (2024, 1, [1 15 31])), [1 15 31]) %!assert_equal (day (datetime (2024, 3, 1), 'dayofyear'), 61) %!assert_equal (day (datetime (2023, 3, 1), 'dayofyear'), 60) %!assert_equal (week (datetime (2024, 6, 15)), 24) %!assert_equal (quarter (datetime (2024, 7, 1)), 3) %!assert_equal (second (datetime (2024, 1, 1, 12, 0, 0), 'secondofday'), 43200) %!assert_equal (day (datetime (2024, 3, 11), 'iso-dayofweek'), 1) %!assert_equal (month (datetime (2024, [1 6 12], 1), 'shortname'), {'Jan', ... %! 'Jun', 'Dec'}) ## Test 'hour' %!assert_equal (hour (datetime (2020, 1, 1, 23, 0, 0)), 23) %!assert_equal (hour (datetime (2020, 1, 1, 0, 0, 0)), 0) %!assert_equal (hour (NaT), NaN) ## Test 'minute' %!assert_equal (minute (datetime (2020, 1, 1, 0, 59, 0)), 59) %!assert_equal (minute (NaT), NaN) ## Test 'second' with type options %!assert_equal (second (datetime (2020, 1, 1, 0, 0, 45)), 45) %!assert_equal (second (datetime (2020, 1, 1, 0, 0, 45.75)), 45.75) %!assert_equal (second (datetime (2020, 1, 1, 0, 0, 0), 'secondofminute'), 0) %!assert_equal (second (datetime (2020, 1, 1, 2, 3, 4), 'secondofday'), 7384) %!assert_equal (second (datetime (2020, 1, 1, 1, 0, 0), 'secondofday'), 3600) %!error ... %! second (datetime (2020, 1, 1), 'bogus') ## Test 'week' (weekofyear default; Sunday-Saturday weeks, week 1 holds Jan 1) %!assert_equal (week (datetime (2023, 1, 1)), 1) %!assert_equal (week (datetime (2023, 1, 8)), 2) %!assert_equal (week (datetime (2023, 1, 31)), 5) %!assert_equal (week (datetime (2024, 1, 1)), 1) %!assert_equal (week (datetime (2024, 1, 7)), 2) %!assert_equal (week (datetime (2024, 12, 29)), 53) %!assert_equal (week (datetime (2024, 12, 31)), 53) %!assert_equal (week (datetime (2025, 1, 1)), 1) %!assert_equal (week (datetime (2020, 2, 29)), 9) %!assert_equal (week (datetime (2020, 3, 1)), 10) %!assert_equal (week (datetime (2023, 1, [1, 8, 31])), [1, 2, 5]) %!assert_equal (week (datetime (2020, 1, 1), 'weekofyear'), 1) ## Test 'week' with weekofmonth %!assert_equal (week (datetime (2023, 1, 1), 'weekofmonth'), 1) %!assert_equal (week (datetime (2023, 1, 31), 'weekofmonth'), 5) %!assert_equal (week (datetime (2024, 12, 29), 'weekofmonth'), 5) %!assert_equal (week (datetime (2020, 2, 29), 'weekofmonth'), 5) %!assert_equal (week (datetime (2020, 3, 1), 'weekofmonth'), 1) %!assert_equal (week (datetime (2023, 1, [1, 8, 15, 22, 29]), ... %! 'weekofmonth'), [1, 2, 3, 4, 5]) ## Test 'week' NaT and error branch %!assert_equal (week (NaT), NaN) %!assert_equal (week ([datetime(2020, 1, 1); NaT]), [1; NaN]) %!error ... %! week (datetime (2020, 1, 1), 'bogus') ## Test 'timeofday' -- elapsed time since midnight, returned as a duration %!assert_equal (seconds (timeofday (datetime (2024, 6, 15, 14, 45, 30))), 53130) %!assert_equal (seconds (timeofday (datetime (2024, 6, 15, 14, 45, ... %! 30.5))), 53130.5) %!assert_equal (seconds (timeofday (datetime (2024, 3, 10, 0, 0, 0))), 0) %!assert_equal (hours (timeofday (datetime (2024, 3, 10, 3, 30, 0))), 3.5) %!assert_equal (seconds (timeofday ([datetime(2024, 6, 15, 1, 0, 0), ... %! datetime(2024, 6, 15, 2, 0, ... %! 0)])), [3600, 7200]) %!assert_equal (isnan (seconds (timeofday (NaT))), true) %!test %! d = timeofday (datetime (2024, 6, 15, 5, 0, 0)); %! assert_equal (class (d), 'duration'); %! assert_equal (d.Format, 'hh:mm:ss'); ## Test 'timeofday' -- zoned result is DST-aware (physical time since midnight) %!assert_equal (hours (timeofday (datetime (2024, 3, 10, 3, 30, 0, ... %! 'TimeZone', 'America/New_York'))), 2.5) %!assert_equal (seconds (timeofday (datetime (2024, 6, 15, 14, 45, 30.5, ... %! 'TimeZone', 'America/New_York'))), 53130.5) ## Test 'tzoffset' -- offset from UTC as a duration (positive east, DST-aware) %!assert_equal (minutes (tzoffset (datetime (2024, 1, 15, ... %! 'TimeZone', 'America/New_York'))), -300) %!assert_equal (minutes (tzoffset (datetime (2024, 7, 15, ... %! 'TimeZone', 'America/New_York'))), -240) %!assert_equal (minutes (tzoffset (datetime (2024, 7, 15, ... %! 'TimeZone', 'Asia/Kolkata'))), 330) %!assert_equal (minutes (tzoffset (datetime (2024, 7, 15, 'TimeZone', ... %! 'UTC'))), 0) %!assert_equal (minutes (tzoffset (datetime (2024, 7, 15, ... %! 'TimeZone', 'Pacific/Marquesas'))), -570) %!assert_equal (minutes (tzoffset (datetime (2024, 1, [15, NaN], 'TimeZone', ... %! 'America/New_York'))), [-300, NaN]) %!assert_equal (isnan (minutes (tzoffset (datetime (2024, 7, 15)))), true) %!test %! d = tzoffset (datetime (2024, 1, 15, 'TimeZone', 'America/New_York')); %! assert_equal (class (d), 'duration'); %! assert_equal (d.Format, 'hh:mm'); ## Test 'isdst' -- logical DST flag, false when unzoned or NaT %!assert_equal (class (isdst (datetime (2024, 7, 15, ... %! 'TimeZone', 'America/New_York'))), 'logical') %!assert_equal (isdst (datetime (2024, 1, 15, ... %! 'TimeZone', 'America/New_York')), false) %!assert_equal (isdst (datetime (2024, 7, 15, ... %! 'TimeZone', 'America/New_York')), true) %!assert_equal (isdst (datetime (2024, 7, 15, 'TimeZone', 'UTC')), false) %!assert_equal (isdst (datetime (2024, 1, 15, ... %! 'TimeZone', 'Australia/Sydney')), true) %!assert_equal (isdst (datetime (2024, 7, 15, ... %! 'TimeZone', 'Australia/Sydney')), false) %!assert_equal (isdst (datetime (2024, 7, 15, 'TimeZone', ... %! 'Asia/Kolkata')), false) %!assert_equal (isdst (datetime (2024, [1, 7], 15, ... %! 'TimeZone', 'America/New_York')), [false, true]) %!assert_equal (isdst (datetime (2024, [1, NaN], 15, ... %! 'TimeZone', 'America/New_York')), [false, false]) %!assert_equal (isdst (datetime (2024, 7, 15)), false) %!assert_equal (isdst (NaT), false) ## Test 'isweekend' -- logical, true on Saturday or Sunday, false for NaT %!assert_equal (class (isweekend (datetime (2024, 1, 6))), 'logical') %!assert_equal (isweekend (datetime (2024, 1, 6)), true) %!assert_equal (isweekend (datetime (2024, 1, 7)), true) %!assert_equal (isweekend (datetime (2024, 1, 8)), false) %!assert_equal (isweekend (datetime (2024, 1, 1:7)), ... %! logical ([0, 0, 0, 0, 0, 1, 1])) %!assert_equal (isweekend (datetime (2024, 1, [6, 7; 8, 13])), ... %! logical ([1, 1; 0, 1])) %!assert_equal (isweekend (datetime (2024, 1, 6, 'TimeZone', 'UTC')), true) %!assert_equal (isweekend (NaT), false) %!assert_equal (isweekend ([datetime(2024, 1, 6); NaT]), logical ([1; 0])) ## Test 'isregular' -- a vector that does not move has no spacing, so repeated ## instants are not regularly spaced, for time or for any calendar unit %!test %! d = datetime (2024, 1, [1, 1, 1]); %! [tf, dt] = isregular (d); %! assert_equal (tf, false); %! assert_equal (isnan (dt), true); %! assert_equal (isregular (d, 'days'), false); %! assert_equal (isregular (d, 'months'), false); %! assert_equal (isregular (d, 'years'), false); %!assert_equal (isregular (datetime (2024, [1, 1, 1], 1), 'months'), false) %!assert_equal (isregular (datetime (2024, 1, [5, 5]), 'time'), false) ## Test 'isregular' -- a moving vector is still regular, in either direction %!assert_equal (isregular (datetime (2024, 1, [1, 2, 3]), 'days'), true) %!assert_equal (isregular (datetime (2024, 1, [3, 2, 1]), 'days'), true) %!assert_equal (isregular (datetime (2024, [1, 2, 3], 1), 'months'), true) %!assert_equal (isregular (datetime (2024, [3, 2, 1], 1), 'months'), true) ## Test 'isregular' -- time-based (default) regularity %!assert_equal (isregular (datetime (2024, 1, 1) + days (0:3)), true) %!assert_equal (isregular (datetime (2024, 1, 1) + hours (0:3)), true) %!assert_equal (isregular (datetime (2024, 1, [1 2 4])), false) %!assert_equal (isregular (datetime (2024, 1, [1 5])), true) %!assert_equal (isregular (fliplr (datetime (2024, 1, 1) + days (0:3))), true) %!assert_equal (isregular ((datetime (2024, 1, 1) + days (0:3))'), true) %!assert_equal (isregular (datetime (2024, 1, 1)), false) %!assert_equal (isregular ([datetime(2024, 1, 1), NaT, datetime(2024, 1, ... %! 3)]), false) %!test %! e = datetime (2024, 1, 1); %! e(1) = []; %! assert_equal (isregular (e), false); ## Test 'isregular' -- monthly data is irregular in time but regular in months %!assert_equal (isregular (datetime (2024, 1, ... %! 1):calmonths (1):datetime (2024, 6, 1)), false) %!assert_equal (isregular (datetime (2024, 1, ... %! 1):calmonths (1):datetime (2024, 6, 1), 'months'), true) ## Test 'isregular' -- calendar units %!assert_equal (isregular (datetime (2024, 1, 1) + days (0:3), 'days'), true) %!assert_equal (isregular (datetime (2024, 1, 1) + days (0:3), 'weeks'), false) %!assert_equal (isregular (datetime (2024, 1, 1:7:22), 'weeks'), true) %!assert_equal (isregular (datetime (2024, 1:4, 15), 'months'), true) %!assert_equal (isregular (datetime (2024, [1 3 5], 31), 'months'), true) %!assert_equal (isregular (datetime (2024, 1, [1 2 3]), 'months'), false) %!assert_equal (isregular (datetime (2024, 1, 1) + calquarters (0:3), ... %! 'quarters'), true) %!assert_equal (isregular (datetime (2020, 2, 29) + calyears (0:3), ... %! 'years'), true) ## Test 'isregular' -- clamp series with varying day-of-month is irregular %!assert_equal (isregular (datetime (2024, 1, 31) + calmonths (0:3), ... %! 'months'), false) ## Test 'isregular' -- calendar-day spacing across DST is regular in days, not ## time %!assert_equal (isregular (datetime (2024, 3, 9, ... %! 'TimeZone', 'America/New_York') + caldays (0:3), 'days'), true) %!assert_equal (isregular (datetime (2024, 3, 9, ... %! 'TimeZone', 'America/New_York') + caldays (0:3), 'time'), false) ## Test 'isregular' -- second output DT (common step) %!test %! [tf, dt] = isregular (datetime (2024, 1, 1) + days (0:3)); %! assert_equal (tf, true); %! assert_equal (class (dt), 'duration'); %! assert_equal (seconds (dt), 86400); %!test %! [tf, dt] = isregular (datetime (2024, [1 3 5], 31), 'months'); %! assert_equal (class (dt), 'calendarDuration'); %! assert_equal (calmonths (dt), 2); %!test %! [tf, dt] = isregular (datetime (2024, 1, 1:7:22), 'weeks'); %! assert_equal (caldays (dt), 7); %!test %! [tf, dt] = isregular (datetime (2020, 2, 29) + calyears (0:3), 'years'); %! assert_equal (calmonths (dt), 12); %!test %! [tf, dt] = isregular (datetime (2024, 1, [1 2 4])); %! assert_equal (tf, false); %! assert_equal (class (dt), 'duration'); %! assert_equal (isnan (seconds (dt)), true); %!test %! [tf, dt] = isregular (datetime (2024, 1, [1 2 5]), 'months'); %! assert_equal (class (dt), 'calendarDuration'); %! assert_equal (isnan (calmonths (dt)), true); %!test %! [tf, dt] = isregular (datetime (2024, 1, 1), 'months'); %! assert_equal (tf, false); %! assert_equal (isnan (calmonths (dt)), true); ## Test 'isregular' -- errors %!error ... %! isregular (datetime (2024, 1, 1) + days (0:3), 'bogus') %!error ... %! isregular (reshape (datetime (2024, 1, 1) + days (0:5), 2, 3)) ################################################################################ ## ** Summary Information ** ## ################################################################################ ## Test 'size' %!assert_equal (size (datetime (2020, 1, 1)), [1, 1]) %!assert_equal (size (datetime (2020, 1, [1; 2; 3])), [3, 1]) %!assert_equal (size (datetime (2020, 1, [1, 2, 3])), [1, 3]) %!assert_equal (size (datetime (2020, ones (2, 4, 3), 1)), [2, 4, 3]) %!test %! [r, c] = size (datetime (2020, 1, [1, 2, 3])); %! assert_equal (r, 1); %! assert_equal (c, 3); %!assert_equal (size (datetime (2020, 1, [1; 2; 3]), 1), 3) %!assert_equal (size (datetime (2020, 1, [1; 2; 3]), 2), 1) ## Test 'ndims' %!assert_equal (ndims (datetime (2020, 1, 1)), 2) %!assert_equal (ndims (datetime (2020, 1, [1; 2])), 2) %!assert_equal (ndims (datetime (2020, ones (2, 3, 4), 1)), 3) ## Test indexing -- linear, logical, end, and N-D element access %!assert_equal (char (datetime (2024, 1, 1:5)(2), 'yyyy-MM-dd'), '2024-01-02') %!assert_equal (char (datetime (2024, 1, 1:5)(end), 'yyyy-MM-dd'), '2024-01-05') %!assert_equal (numel (datetime (2024, 1, 1:5)(logical ([1 0 1 0 1]))), 3) %!assert_equal (size (datetime (2024, 1, reshape (1:8, 2, 2, 2))), [2, 2, 2]) %!assert_equal (char (datetime (2024, 1, reshape (1:8, 2, 2, 2))(2, 2, 2), ... %! 'yyyy-MM-dd'), '2024-01-08') ## Test 'subsasgn' -- assignment grows the array, and deletion shrinks it %!test %! t = datetime (2024, 1, 1:3); %! t(5) = datetime (2024, 1, 10); %! assert_equal (numel (t), 5); %!test %! t = datetime (2024, 1, 1:5); %! t(2) = []; %! assert_equal (numel (t), 4); %! assert_equal (char (t(2), 'yyyy-MM-dd'), '2024-01-03'); ## Test that an element assigned from another zone is CONVERTED into the array's ## own, as concatenation already did: assignment changes which array an element ## belongs to, not the instant it names. It used to be copied across whole, so ## the array held an element carrying another zone's offset -- it displayed in a ## zone it was not in and compared unequal to the value assigned from it. All ## R2026a-verified. %!test %! n = datetime (2024, 3, 15, 12, 0, 0, 'TimeZone', 'America/New_York'); %! u = datetime (2024, 3, 15, 12, 0, 0, 'TimeZone', 'UTC'); %! n(2) = u; %! assert_equal (char (n(2), 'HH:mm'), '08:00'); %! assert_equal (posixtime (n(2)), posixtime (u)); %! assert_equal (hours (tzoffset (n(2))), -4); %! assert_equal (n(2) == u, true); %!test %! n = datetime (2024, 3, 15, 12, 0, 0, 'TimeZone', 'America/New_York'); %! u = datetime (2024, 3, 15, 12, 0, 0, 'TimeZone', 'UTC'); %! u(2) = n; %! assert_equal (char (u(2), 'HH:mm'), '16:00'); %! assert_equal (u.TimeZone, 'UTC'); %!test %! n = datetime (2024, 3, 15, 12, 0, 0, 'TimeZone', 'America/New_York'); %! n(2) = n(1); %! assert_equal (char (n(2), 'HH:mm'), '12:00'); %!test %! n = datetime (2024, 3, 15, 12, 0, 0, 'TimeZone', 'America/New_York'); %! u = datetime (2024, 3, 15, 12, 0, 0, 'TimeZone', 'UTC'); %! n(4) = u; %! assert_equal (char (n(4), 'HH:mm'), '08:00'); %! assert_equal (isnat (n), [false, true, true, false]); %!test %! n = datetime (2024, 3, 15, 12, 0, 0, 'TimeZone', 'America/New_York'); %! u = datetime (2024, 3, 15, 12, 0, 0, 'TimeZone', 'UTC'); %! n(2:3) = [u, u]; %! assert_equal (char (n(3), 'HH:mm'), '08:00'); ## A 'NaT' names no instant and so carries no zone to disagree with the ## array's own: it is assigned into a zoned array as readily as 'missing' is, ## and the two are indistinguishable afterwards. Measured on MATLAB R2026a, ## which refuses the real datetime below on the same fixture. %!test %! v = datetime (2024, 6, 1, 'TimeZone', 'America/New_York') + hours (0:3)'; %! v(2) = NaT; %! assert_equal (v.TimeZone, 'America/New_York'); %! assert_equal (ismissing (v)', [false, true, false, false]); %! ny = 'America/New_York'; %! assert_equal (v(3), datetime (2024, 6, 1, 2, 0, 0, 'TimeZone', ny)); %!test %! v = datetime (2024, 6, 1, 'TimeZone', 'America/New_York') + hours (0:3)'; %! w = v; %! v(2) = NaT; %! w(2) = missing; %! assert_equal (isequaln (v, w), true); %!test %! v = datetime (2024, 6, 1, 'TimeZone', 'America/New_York') + hours (0:3)'; %! v(2:3) = NaT (1, 2); %! assert_equal (v.TimeZone, 'America/New_York'); %! assert_equal (ismissing (v)', [false, true, true, false]); ## Test an unzoned array takes a 'NaT' as it always did %!test %! u = datetime (2024, 6, 1) + hours (0:3)'; %! u(2) = NaT; %! assert_equal (u.TimeZone, ''); %! assert_equal (ismissing (u)', [false, true, false, false]); ## Test that the two mixtures with no answer are refused rather than silently ## admitted, which is what concatenation already did and what MATLAB does here. ## A datetime that does name an instant is refused whichever way it goes, on ## R2024a and R2026a alike; only a missing one is exempt. %!error ... %! n = datetime (2024, 3, 15, 'TimeZone', 'America/New_York'); %! n(2) = datetime (2024, 3, 15); %!error ... %! p = datetime (2024, 3, 15); %! p(2) = datetime (2024, 3, 15, 'TimeZone', 'America/New_York'); %!error ... %! n = datetime (2024, 3, 15, 'TimeZone', 'America/New_York'); %! n(2) = datetime (2024, 3, 15, 'TimeZone', 'UTCLeapSeconds'); %!error ... %! l = datetime (2024, 3, 15, 'TimeZone', 'UTCLeapSeconds'); %! l(2) = datetime (2024, 3, 15, 'TimeZone', 'UTC'); ## Test 'subsasgn' -- an out-of-range assignment fills the gap with NaT %!test %! t = datetime (2024, 1, 1:3); %! t(5) = datetime (2024, 1, 10); %! assert_equal (isnat (t(4)), true); %! assert_equal (char (t(3), 'yyyy-MM-dd'), '2024-01-03'); %! assert_equal (char (t(5), 'yyyy-MM-dd'), '2024-01-10'); %! assert_equal (cellstr (t(4)), {'NaT'}); %!test %! m = datetime (2024, 1, 1:3); %! m(3, 3) = datetime (2024, 1, 20); %! assert_equal (size (m), [3, 3]); %! assert_equal (sum (isnat (m(:))), 5); %!test %! z = datetime (2024, 1, 1:2, 'TimeZone', 'UTC'); %! z(4) = datetime (2024, 1, 9, 'TimeZone', 'UTC'); %! assert_equal (isnat (z(3)), true); %! assert_equal (z.TimeZone, 'UTC'); ## Test 'numel' %!assert_equal (numel (datetime (2020, 1, 1)), 1) %!assert_equal (numel (datetime (2020, 1, [1; 2; 3])), 3) %!assert_equal (numel (datetime (2020, ones (2, 3), 1)), 6) ## Test 'length' %!assert_equal (length (datetime (2020, 1, 1)), 1) %!assert_equal (length (datetime (2020, 1, [1; 2; 3])), 3) %!assert_equal (length (datetime (2020, ones (2, 5), 1)), 5) ## Test 'keyHash' (regression: previously read a non-existent property) %!assert_equal (class (keyHash (datetime (2020, 1, 1))), 'uint64') %!assert_equal (isscalar (keyHash (datetime (2020, 1, [1; 2; 3]))), true) %!assert_equal (keyHash (datetime (2020, 1, 1)), keyHash (datetime (2020, 1, ... %! 1))) %!assert_equal (keyHash (datetime (2020, 1, 1)) != keyHash (datetime (2020, ... %! 1, 2)), true) %!assert_equal (class (keyHash (datetime (2020, 1, 1), uint64 (7))), 'uint64') %!error ... %! keyHash (datetime (2020, 1, 1), 7) ## A zero offset basis is a basis, not the absence of one, and a falsy base ## must still be validated rather than silently replaced by the default. %!test %! a = keyHash (datetime (2020, 1, 1)); %! b = keyHash (datetime (2020, 1, 1), uint64 (0)); %! assert_equal (isequal (a, b), false); %! assert_equal (keyHash (datetime (2020, 1, 1), []), a); %!error ... %! keyHash (datetime (2020, 1, 1), 0) %!error ... %! keyHash (datetime (2020, 1, 1), uint64 ([0, 1])) ## A datetime is keyed on the INSTANT, not on the wall clock and zone name, ## so the same moment named in two zones is one key. %!test %! a = datetime (2024, 3, 5, 12, 0, 0, 'TimeZone', 'UTC'); %! b = datetime (2024, 3, 5, 7, 0, 0, 'TimeZone', 'America/New_York'); %! assert_equal (keyMatch (a, b), true); %! assert_equal (keyHash (a), keyHash (b)); %! c = datetime (2024, 3, 5, 14, 0, 0, 'TimeZone', 'Europe/Athens'); %! assert_equal (keyMatch (a, c), true); %! assert_equal (keyHash (a), keyHash (c)); %! assert_equal (keyMatch (a, a + seconds (1)), false); ## The frame has three states, and no two of them are ever one key however ## the instants line up: an unzoned array names no instant, and a ## leap-second array counts seconds no other zone counts. %!test %! u = datetime (2024, 3, 5, 12, 0, 0); %! z = datetime (2024, 3, 5, 12, 0, 0, 'TimeZone', 'UTC'); %! l = datetime (2024, 3, 5, 12, 0, 0, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (keyMatch (u, z), false); %! assert_equal (keyMatch (z, l), false); %! assert_equal (keyMatch (u, l), false); %! assert_equal (keyMatch (u, datetime (2024, 3, 5, 12, 0, 0)), true); %! assert_equal (keyHash (u), keyHash (datetime (2024, 3, 5, 12, 0, 0))); %! assert_equal (keyMatch (l, datetime (2024, 3, 5, 12, 0, 0, ... %! 'TimeZone', 'UTCLeapSeconds')), true); ## The two instants that share a wall clock across a fall-back stay apart, ## which is what the stored offset is for. %!test %! a = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC'); %! b = a + hours (1); %! an = a; an.TimeZone = 'America/New_York'; %! bn = b; bn.TimeZone = 'America/New_York'; %! assert_equal (datestr (an, 'HH:MM'), datestr (bn, 'HH:MM')); %! assert_equal (keyMatch (an, bn), false); %! assert_equal (keyMatch (an, a), true); %! assert_equal (keyHash (an), keyHash (a)); ## NaT matches itself, whatever produced it. %!test %! assert_equal (keyMatch (NaT, NaT), true); %! assert_equal (keyHash (NaT), keyHash (NaT)); %! assert_equal (keyMatch (NaT, datetime (NaN, NaN, NaN)), true); %! assert_equal (keyHash (NaT), keyHash (datetime (NaN, NaN, NaN))); %! assert_equal (keyMatch (NaT, datetime (2020, 1, 1)), false); ################################################################################ ## ** Convert to other Numeric Representations ** ## ################################################################################ ## Test 'datevec' %!assert_equal (datevec (datetime (2020, 7, 4, 1, 2, 3)), [2020, 7, 4, 1, 2, 3]) %!assert_equal (size (datevec (datetime (2020, 1, [1; 2]))), [2, 6]) %!test %! [y, m, d, h, mi, s] = datevec (datetime (2020, 7, 4, 1, 2, 3)); %! assert_equal ([y, m, d, h, mi, s], [2020, 7, 4, 1, 2, 3]); ## Test 'datestr' -- legacy string conversion delegating to core datestr %!assert_equal (datestr (datetime (2024, 3, 9, 14, 5, ... %! 7)), '09-Mar-2024 14:05:07') %!assert_equal (datestr (datetime (2024, 3, 9, 14, 5, 7), ... %! 'yyyy-mm-dd HH:MM:SS'), '2024-03-09 14:05:07') %!assert_equal (datestr (datetime (2024, 3, 9), 'dd-mmm-yyyy'), '09-Mar-2024') %!assert_equal (datestr (datetime (2024, 3, 9, 14, 5, 7, ... %! 'TimeZone', 'America/New_York')), '09-Mar-2024 14:05:07') %!test %! s = datestr ([datetime(2024, 3, 9, 14, 5, 7); datetime(2024, 3, 10, 14, ... %! 5, 7)]); %! assert_equal (size (s), [2, 20]); %! assert_equal (s(1,:), '09-Mar-2024 14:05:07'); %! assert_equal (s(2,:), '10-Mar-2024 14:05:07'); ## Test 'string' -- datetime to string array via the Format seam %!assert_equal (class (string (datetime (2024, 3, 9))), 'string') %!assert_equal (cellstr (string (datetime (2024, 3, 9, 14, 5, 7))), ... %! {'09-Mar-2024 14:05:07'}) %!assert_equal (cellstr (string (datetime (2024, 3, 9), 'yyyy-MM-dd')), ... %! {'2024-03-09'}) %!assert_equal (cellstr (string (datetime (2024, 3, 9, 'TimeZone', ... %! 'America/New_York'), 'yyyy-MM-dd')), {'2024-03-09'}) %!assert_equal (ismissing (string (NaT)), true) ## Test 'string' -- Format property, shape, and NaT -> missing %!test %! t = datetime (2024, 3, 9); %! t.Format = 'yyyy-MM-dd'; %! assert_equal (cellstr (string (t)), {'2024-03-09'}); %!test %! t = [datetime(2024, 3, 9, 14, 5, 7), NaT]; %! s = string (t); %! assert_equal (size (s), [1, 2]); %! assert_equal (ismissing (s), logical ([0, 1])); %!test %! t = [datetime(2024, 1, 1, 5, 0, 0); datetime(2024, 2, 2, 6, 0, 0)]; %! assert_equal (cellstr (string (t)), cellstr (t)); ## Test 'yyyymmdd' -- YYYYMMDD numeric date, time ignored, NaT -> NaN %!assert_equal (yyyymmdd (datetime (2024, 3, 9)), 20240309) %!assert_equal (yyyymmdd (datetime (2024, 3, 9, 14, 5, 7)), 20240309) %!assert_equal (yyyymmdd (datetime (2024, 12, 31)), 20241231) %!assert_equal (yyyymmdd (datetime (1999, 1, 1)), 19990101) %!assert_equal (class (yyyymmdd (datetime (2024, 1, 1))), 'double') %!assert_equal (yyyymmdd (datetime (2024, 3, 9, 23, 0, 0, ... %! 'TimeZone', 'America/New_York')), 20240309) %!assert_equal (yyyymmdd (datetime (2024, 1, [1; 2])), [20240101; 20240102]) %!assert_equal (isnan (yyyymmdd (NaT)), true) ## Test 'datenum' -- serial date number, wall-clock, NaT -> NaN, Inf preserved %!assert_equal (datenum (datetime (2024, 3, 9)), 739320) %!assert_equal (datenum (datetime (2024, 1, 1)), 739252) %!assert_equal (datenum (datetime (2024, 3, 9, 6, 0, 0)), 739320.25) %!assert_equal (datenum (datetime (2024, 3, 9, 6, 30, 15.5)), ... %! datenum (2024, 3, 9, 6, 30, 15.5)) %!assert_equal (class (datenum (datetime (2024, 1, 1))), 'double') %!assert_equal (datenum (datetime (2024, 3, 9, 23, 0, 0, 'TimeZone', ... %! 'America/New_York')), datenum (2024, 3, 9, 23, 0, 0)) %!assert_equal (datenum (datetime (2024, 1, [1; 2])), [739252; 739253]) %!assert_equal (isnan (datenum (NaT)), true) %!assert_equal (datenum ([datetime(2024, 1, 1), NaT]), [739252, NaN]) %!assert_equal (datenum (datetime (Inf, 'ConvertFrom', 'datenum')), Inf) %!assert_equal (datenum (datetime (-Inf, 'ConvertFrom', 'datenum')), -Inf) ## Test that a day-count serial converts back to the time it was made from. ## Such a serial resolves only as finely as its magnitude allows -- about ten ## microseconds for a datenum at 2024, sixteen times finer for an Excel serial, ## whose epoch is nearer -- so the conversion is taken at that resolution and no ## finer. Reporting the raw product answered 10:30:44.999997 for a datetime ## built from 10:30:45. Every value below is R2026a's. %!assert_equal (second (datetime (datenum (datetime (2024, 6, 15, 10, 30, ... %! 45)), 'ConvertFrom', 'datenum')), 45) %!assert_equal (second (datetime (datenum (datetime (2024, 6, 15, 10, 30, ... %! 45.5)), 'ConvertFrom', 'datenum')), 45.5) %!assert_equal (second (datetime (datenum (datetime (2024, 6, 15, 10, 30, ... %! 45.001)), 'ConvertFrom', 'datenum')), 45.001) %!assert_equal (second (datetime (739418.4380208333, 'ConvertFrom', ... %! 'datenum')), 45) %!assert_equal (second (datetime (exceltime (datetime (2024, 6, 15, 10, 30, ... %! 45)), 'ConvertFrom', 'excel')), 45) %!assert_equal (second (datetime (exceltime (datetime (2024, 6, 15, 10, 30, ... %! 45.001)), 'ConvertFrom', 'excel')), 45.001) %!assert_equal (second (datetime (exceltime (datetime (2024, 6, 15, 10, 30, ... %! 45.000123)), 'ConvertFrom', 'excel')), 45.000123) %!assert_equal (second (datetime (datenum (datetime (1600, 6, 15, 10, 30, ... %! 45)), 'ConvertFrom', 'datenum')), 45) %!assert_equal (second (datetime (datenum (datetime (1e6, 6, 15, 10, 30, ... %! 45)), 'ConvertFrom', 'datenum')), 45) %!assert_equal (second (datetime (datenum (datetime (1e6, 6, 15, 10, 30, ... %! 45.001)), 'ConvertFrom', 'datenum')), 45) %!test %! t = datetime (2024, 6, 15, 10, 30, 45); %! assert_equal (datetime (datenum (t), 'ConvertFrom', 'datenum'), t); %! assert_equal (datetime (exceltime (t), 'ConvertFrom', 'excel'), t); ## Test 'exceltime' -- Excel serial date; 1900 (default) and 1904 systems %!assert_equal (exceltime (datetime (2024, 3, 9)), 45360) %!assert_equal (exceltime (datetime (2024, 3, 9, 6, 0, 0)), 45360.25) %!assert_equal (exceltime (datetime (1900, 1, 1)), 1) %!assert_equal (exceltime (datetime (1900, 2, 28)), 59) %!assert_equal (exceltime (datetime (1900, 3, 1)), 61) %!assert_equal (exceltime (datetime (1899, 12, 30)), -1) %!assert_equal (exceltime (datetime (2024, 3, 9), '1904'), 43898) ## The 1904 system has no phantom 1900-02-29, so these are plain day counts and ## MATLAB returns one less; ours converts back exactly, MATLAB's does not. %!assert_equal (exceltime (datetime (1900, 1, 1), '1904'), -1460) %!assert_equal (exceltime (datetime (1899, 6, 15), '1904'), -1660) %!assert_equal (datetime (exceltime (datetime (1899, 6, 15), '1904'), ... %! 'ConvertFrom', 'excel1904'), datetime (1899, 6, 15)) %!assert_equal (class (exceltime (datetime (2024, 1, 1))), 'double') %!assert_equal (exceltime (datetime (2024, 3, 9, 23, 0, 0, ... %! 'TimeZone', 'America/New_York')), ... %! exceltime (datetime (2024, 3, 9, 23, 0, 0))) %!assert_equal (exceltime (datetime (2024, 1, [1; 2])), [45292; 45293]) %!assert_equal (isnan (exceltime (NaT)), true) %!assert_equal (exceltime ([datetime(2024, 1, 1), NaT]), [45292, NaN]) %!error ... %! exceltime (datetime (2024, 1, 1), 'bogus') ## Test 'juliandate' -- Julian date and modified variant, instant-based %!assert_equal (juliandate (datetime (2000, 1, 1, 12, 0, 0)), 2451545) %!assert_equal (juliandate (datetime (2000, 1, 1, 12, 0, 0, ... %! 'TimeZone', 'UTC')), 2451545) %!assert_equal (juliandate (datetime (2024, 3, 9)), 2460378.5) %!assert_equal (juliandate (datetime (2024, 3, 9), 'juliandate'), 2460378.5) %!assert_equal (juliandate (datetime (2024, 3, 9), 'modifiedjuliandate'), 60378) %!assert_equal (class (juliandate (datetime (2024, 1, 1))), 'double') %!assert_equal (juliandate (datetime (2024, 1, [1, 2])), [2460310.5, 2460311.5]) %!assert_equal (isnan (juliandate (NaT)), true) %!assert_equal (juliandate (datetime (Inf, 'ConvertFrom', 'datenum')), Inf) ## Test 'juliandate' -- zoned result uses the absolute UTC instant %!assert_equal (juliandate (datetime (2000, 1, 1, 12, 0, 0, ... %! 'TimeZone', 'America/New_York')), ... %! juliandate (datetime (2000, 1, 1, 17, 0, 0, 'TimeZone', 'UTC'))) ## Test 'juliandate' -- error branch %!error ... %! juliandate (datetime (2024, 1, 1), 'bogus') ## Test conversions -- pre-epoch, epoch, Excel epoch, and instant-based edges %!assert_equal (posixtime (datetime (1969, 12, 31, 23, 0, 0)), -3600) %!assert_equal (datenum (datetime (1970, 1, 1)), 719529) %!assert_equal (exceltime (datetime (1900, 1, 1)), 1) %!assert_equal (juliandate (datetime (2000, 1, 1, 7, 0, 0, ... %! 'TimeZone', 'America/New_York')), 2451545) ## Test 'convertTo' -- umbrella dispatcher to the numeric conversions %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', ... %! 'UTC'), ... %! 'datenum'), 739320.25) %!assert_equal (convertTo (datetime (2024, 3, 9), 'excel'), 45360) %!assert_equal (convertTo (datetime (2024, 3, 9), 'excel1900'), 45360) %!assert_equal (convertTo (datetime (2024, 3, 9), 'excel1904'), 43898) %!assert_equal (convertTo (datetime (2024, 3, 9), 'juliandate'), 2460378.5) %!assert_equal (convertTo (datetime (2024, 3, 9), 'modifiedjuliandate'), 60378) %!assert_equal (convertTo (datetime (2024, 1, 1, 'TimeZone', 'UTC'), ... %! 'posixtime'), 1704067200) %!assert_equal (convertTo (datetime (2024, 3, 9), 'yyyymmdd'), 20240309) %!assert_equal (convertTo (datetime (2024, 3, 9), 'DATENUM'), 739320) %!assert_equal (isnan (convertTo (NaT, 'datenum')), true) ## Test 'convertTo' -- delegation matches the standalone methods %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), 'datenum'), ... %! datenum (datetime (2024, 3, 9, 6, 0, 0))) %!assert_equal (convertTo (datetime (2024, 3, 9), 'excel1904'), ... %! exceltime (datetime (2024, 3, 9), '1904')) ## Test 'convertTo' -- error branches %!error ... %! convertTo (datetime (2024, 1, 1)) %!error ... %! convertTo (datetime (2024, 1, 1), 'bogus') %!error ... %! convertTo (datetime (2024, 1, 1), 'datenum', 5) ## Test 'convertTo' -- ntp/ntfs/.net integer epochs (uint64), instant-based %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', ... %! 'UTC'), ... %! 'ntp'), uint64 (3918952800) * uint64 (4294967296)) %!assert_equal (class (convertTo (datetime (2024, 3, 9), 'ntp')), 'uint64') %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0.5, 'TimeZone', ... %! 'UTC'), ... %! 'ntp'), ... %! uint64 (3918952800) * uint64 (4294967296) + uint64 (2147483648)) %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, ... %! 'TimeZone', 'America/New_York'), 'ntp'), ... %! uint64 (3918952800 + 18000) * uint64 (4294967296)) %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), 'ntp'), ... %! uint64 (3918952800) * uint64 (4294967296)) %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', ... %! 'UTC'), ... %! 'ntfs'), uint64 (13354437600) * uint64 (10000000)) %!assert_equal (class (convertTo (datetime (2024, 3, 9), 'ntfs')), 'uint64') %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0.001, 'TimeZone', ... %! 'UTC'), ... %! 'ntfs'), uint64 (13354437600) * uint64 (10000000) + uint64 (10000)) %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', ... %! 'UTC'), ... %! '.net'), uint64 (63845560800) * uint64 (10000000)) %!assert_equal (class (convertTo (datetime (2024, 3, 9), '.net')), 'uint64') ## Test 'convertTo' -- ntp/ntfs array preserves shape and values %!test %! t = datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', 'UTC'); %! a = convertTo ([t, t + hours(1)], 'ntfs'); %! assert_equal (size (a), [1, 2]); %! assert_equal (a(1), uint64 (13354437600) * uint64 (10000000)); %! assert_equal (a(2), uint64 (13354437600 + 3600) * uint64 (10000000)); ## Test 'convertTo' -- epochtime (int64) with Epoch and TicksPerSecond %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', ... %! 'UTC'), ... %! 'epochtime'), int64 (1709964000)) %!assert_equal (class (convertTo (datetime (2024, 3, 9), 'epochtime')), 'int64') %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', ... %! 'UTC'), ... %! 'epochtime', 'TicksPerSecond', 1000), int64 (1709964000000)) %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', ... %! 'UTC'), ... %! 'epochtime', 'Epoch', datetime (2000, 1, 1, 'TimeZone', ... %! 'UTC'), ... %! 'TicksPerSecond', 1000), int64 (763279200000)) %!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0.5, 'TimeZone', ... %! 'UTC'), ... %! 'epochtime'), int64 (1709964001)) ## Test 'convertTo' -- integer epochs reject NaT, Inf, and out-of-range %!error ... %! convertTo (NaT, 'ntp') %!error ... %! convertTo (NaT, 'ntfs') %!error ... %! convertTo (datetime (-1, 1, 1), '.net') %!error ... %! convertTo (datetime (1850, 1, 1, 'TimeZone', 'UTC'), 'ntp') %!error ... %! convertTo (datetime (2040, 1, 1, 'TimeZone', 'UTC'), 'ntp') %!error ... %! convertTo ([datetime(2024, 1, 1, 'TimeZone', 'UTC'), ... %! NaT('TimeZone', 'UTC')], 'ntp') %!error ... %! convertTo (NaT, 'epochtime') %!error ... %! convertTo (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 'epochtime', ... %! 'Epoch', datetime (2000, 1, 1)) %!error ... %! convertTo (datetime (2024, 1, 1), 'epochtime', 'TicksPerSecond', -5) %!error ... %! convertTo (datetime (2024, 1, 1), 'epochtime', 'Epoch') %!error ... %! convertTo (datetime (2024, 1, 1), 'epochtime', 'Bogus', 1) %!error ... %! convertTo (datetime (2024, 1, 1), 'tt2000') ## Test 'posixtime' -- seconds since the Unix epoch, unzoned treated as UTC %!assert_equal (posixtime (datetime (1970, 1, 1, 0, 0, 0)), 0) %!assert_equal (posixtime (datetime (2024, 1, 1, 0, 0, 0)), 1704067200) %!assert_equal (posixtime (datetime (2024, 1, 1, 0, 0, 0, 'TimeZone', ... %! 'UTC')), ... %! 1704067200) %!assert_equal (posixtime (datetime (2024, 1, 1, 0, 0, 0, ... %! 'TimeZone', 'America/New_York')), 1704085200) %!assert_equal (posixtime (datetime (2024, 1, 1, 0, 0, 0.5)), 1704067200.5) %!assert_equal (class (posixtime (datetime (2024, 1, 1))), 'double') %!assert_equal (posixtime (datetime (2024, 1, ... %! [1; 2])), [1704067200; 1704153600]) %!assert_equal (isnan (posixtime (NaT)), true) ## Test 'interp1' -- datetime sample points, numeric values %!test %! t = datetime (2024, 1, 1) + days (0:4); %! v = [10 20 30 40 50]; %! tq = datetime (2024, 1, 1) + days ([1.5 3.25]); %! assert_equal (interp1 (t, v, tq), [25 42.5]); %! assert_equal (class (interp1 (t, v, tq)), 'double'); %! assert_equal (interp1 (t, v, tq, 'nearest'), [30 40]); %! assert_equal (interp1 (t, v, tq, 'previous'), [20 40]); %!test %! t = datetime (2024, 1, 1) + days (0:4); %! v = [10 20 30 40 50]; %! assert_equal (isnan (interp1 (t, v, datetime (2024, 1, 10))), true); %! assert_equal (interp1 (t, v, datetime (2024, 1, 10), 'linear', 'extrap'), ... %! 100); %! assert_equal (interp1 (t, v, datetime (2024, 1, 10), 'linear', -1), -1); %! assert_equal (isnan (interp1 (t, v, NaT)), true); ## Test 'interp1' -- datetime values yield a datetime result (zone preserved) %!test %! V = datetime (2024, 1, 1) + days (0:4); %! r = interp1 (0:4, V, [1.5 3.25]); %! assert_equal (class (r), 'datetime'); %! assert_equal (cellstr (r), {'02-Jan-2024 12:00:00', '04-Jan-2024 06:00:00'}); %! assert_equal (isnat (interp1 (0:4, V, 9)), true); %!test %! t = datetime (2024, 1, 1) + days (0:4); %! V = datetime (2024, 1, 1) + days (0:4); %! tq = datetime (2024, 1, 1) + days ([1.5 3.25]); %! assert_equal (cellstr (interp1 (t, V, tq)), ... %! {'02-Jan-2024 12:00:00', '04-Jan-2024 06:00:00'}); %!test %! Vz = datetime (2024, 1, 1, 'TimeZone', 'America/New_York') + days (0:4); %! r = interp1 (0:4, Vz, 1.5); %! assert_equal (r.TimeZone, 'America/New_York'); %! assert_equal (char (r, 'yyyy-MM-dd HH:mm:ss'), '2024-01-02 12:00:00'); ## Test 'interp1' -- sample and query points must be the same kind %!error ... %! interp1 (datetime (2024, 1, 1) + days (0:4), [1 2 3 4 5], 2) %!error ... %! interp1 (0:4, [1 2 3 4 5], datetime (2024, 1, 2)) ################################################################################ ## ** Query Operations ** ## ################################################################################ ## Test 'iscolumn' / 'isrow' / 'isscalar' / 'isvector' / 'ismatrix' %!assert_equal (iscolumn (datetime (2020, 1, [1; 2; 3])), true) %!assert_equal (iscolumn (datetime (2020, 1, [1, 2, 3])), false) %!assert_equal (isrow (datetime (2020, 1, [1, 2, 3])), true) %!assert_equal (isrow (datetime (2020, 1, [1; 2; 3])), false) %!assert_equal (isscalar (datetime (2020, 1, 1)), true) %!assert_equal (isscalar (datetime (2020, 1, [1; 2])), false) %!assert_equal (isvector (datetime (2020, 1, [1; 2; 3])), true) %!assert_equal (isvector (datetime (2020, ones (2, 2), 1)), false) %!assert_equal (ismatrix (datetime (2020, ones (2, 2), 1)), true) ## Test 'isempty' %!assert_equal (isempty (datetime (2020, 1, 1)), false) %!assert_equal (isempty (datetime (zeros (0, 3))), true) ## Test 'isfinite' / 'isinf' / 'isnat' %!assert_equal (isfinite (datetime (2020, 1, 1)), true) %!assert_equal (isfinite (NaT), false) %!assert_equal (isfinite (datetime (Inf, Inf, Inf)), false) %!assert_equal (isinf (datetime (Inf, Inf, Inf)), true) %!assert_equal (isinf (datetime (2020, 1, 1)), false) %!assert_equal (isnat (NaT), true) %!assert_equal (isnat (datetime (2020, 1, 1)), false) %!assert_equal (isnat (datetime (2020, 1, [1; 2])), [false; false]) ## Test 'ismissing' %!assert_equal (ismissing (datetime (2020, 1, 1)), false) %!assert_equal (ismissing (NaT), true) %!test %! d = datetime (2020, 1, [1; 2; 3]); %! d(2) = datetime (NaN, NaN, NaN); %! assert_equal (ismissing (d), [false; true; false]); %!assert_equal (ismissing (datetime (2020, 1, [1; 2; 3]), datetime (2020, 1, ... %! 3)), [false; false; true]) %!test %! D = [datetime(2020, 1, 1), NaT, datetime(2020, 1, 3)]; %! assert_equal (ismissing (D, NaT), [false, true, false]); %! assert_equal (ismissing (D, [NaT, datetime(2020, 1, 3)]), ... %! [false, true, true]); %! assert_equal (ismissing (D, missing), [false, true, false]); %!error ... %! ismissing (datetime (2020, 1, 1), datetime (2020, 1, 1), 3) %!error ... %! ismissing (datetime (2020, 1, 1), 5) ## Test 'isbetween' -- the default interval is closed and returns a logical %!test %! x = datetime (2026, 1, [1 2 3 4 5]); %! tf = isbetween (x, datetime (2026, 1, 2), datetime (2026, 1, 4)); %! assert_equal (class (tf), 'logical'); %! assert_equal (tf, logical ([0 1 1 1 0])); ## Test 'isbetween' -- the four interval types include the right endpoints %!test %! x = datetime (2026, 1, [1 2 3 4 5]); %! lo = datetime (2026, 1, 2); %! hi = datetime (2026, 1, 4); %! assert_equal (isbetween (x, lo, hi, 'closed'), logical ([0 1 1 1 0])); %! assert_equal (isbetween (x, lo, hi, 'open'), logical ([0 0 1 0 0])); %! assert_equal (isbetween (x, lo, hi, 'openleft'), logical ([0 0 1 1 0])); %! assert_equal (isbetween (x, lo, hi, 'openright'), logical ([0 1 1 0 0])); ## Test 'isbetween' -- Not-A-Time in any input is false; inverted bounds are ## false %!assert_equal (isbetween ([NaT datetime(2026,1,3)], datetime (2026, 1, 2), ... %! datetime (2026, 1, 4)), logical ([0 1])) %!assert_equal (isbetween (datetime (2026, 1, [1 2 3 4 5]), NaT, ... %! datetime (2026, 1, 4)), logical ([0 0 0 0 0])) %!assert_equal (isbetween (datetime (2026, 1, [1 2 3 4 5]), datetime (2026, ... %! 1, 4), ... %! datetime (2026, 1, 2)), logical ([0 0 0 0 0])) ## Test 'isbetween' -- bounds broadcast and may be date/time text %!assert_equal (isbetween (datetime (2026, 1, [1 2 3 4 5]), ... %! datetime (2026, 1, [1 2 3 4 5]), datetime (2026, 1, 4)), ... %! logical ([1 1 1 1 0])) %!assert_equal (isbetween (datetime (2026, 1, [1 2 3 4 5]), '02-Jan-2026', ... %! '04-Jan-2026'), logical ([0 1 1 1 0])) ## Test 'isbetween' -- comparison is by absolute instant across time zones %!assert_equal (isbetween (datetime (2026, 1, 1, 5, 0, 0, 'TimeZone', ... %! 'UTC'), ... %! datetime (2026, 1, 1, 0, 0, 0, 'TimeZone', ... %! 'America/New_York'), ... %! datetime (2026, 1, 1, 1, 0, 0, 'TimeZone', ... %! 'America/New_York')), true) ## Test 'isbetween' -- invalid arguments are rejected %!error ... %! isbetween (datetime (2026, 1, 1), datetime (2026, 1, 2)); %!error ... %! isbetween (datetime (2026, 1, 1), datetime (2026, 1, 1), datetime (2026, ... %! 1, 2), 'closed', 1); %!error ... %! isbetween (datetime (2026, 1, 1), datetime (2026, 1, 1), datetime (2026, ... %! 1, 2), 'bogus'); %!error ... %! isbetween (datetime (2026, 1, 2), datetime (2026, 1, 1, 'TimeZone', ... %! 'UTC'), ... %! datetime (2026, 1, 3, 'TimeZone', 'UTC')); %!error ... %! isbetween (datetime (2026, 1, 1), days (1), days (2)); ## Test 'isbetween' -- the four interval types at the boundaries %!assert_equal (isbetween (datetime (2024, 1, [5 10 15]), datetime (2024, 1, ... %! 5), ... %! datetime (2024, 1, 15)), logical ([1 1 1])) %!assert_equal (isbetween (datetime (2024, 1, [5 10 15]), datetime (2024, 1, ... %! 5), ... %! datetime (2024, 1, 15), 'open'), logical ([0 1 0])) %!assert_equal (isbetween (datetime (2024, 1, [5 10 15]), datetime (2024, 1, ... %! 5), ... %! datetime (2024, 1, 15), 'openleft'), logical ([0 1 1])) %!assert_equal (isbetween (datetime (2024, 1, [5 10 15]), datetime (2024, 1, ... %! 5), ... %! datetime (2024, 1, 15), 'openright'), logical ([1 1 0])) ## Test array ops -- concatenation and reshape preserve the time zone %!test %! c = [datetime(2024, 1, 1, 'TimeZone', 'UTC'), ... %! datetime(2024, 1, 2, 'TimeZone', 'UTC')]; %! assert_equal (c.TimeZone, 'UTC'); %!test %! r = reshape (datetime (2024, 1, 1:4, 'TimeZone', 'Asia/Tokyo'), 2, 2); %! assert_equal (r.TimeZone, 'Asia/Tokyo'); %! assert_equal (size (r), [2, 2]); ## Test 'colon' -- a negative step counts down %!assert_equal (cellstr (datetime (2024, 1, 5):-caldays (1):datetime (2024, ... %! 1, 1)), ... %! {'05-Jan-2024', '04-Jan-2024', '03-Jan-2024', '02-Jan-2024', ... %! '01-Jan-2024'}) ## Test 'unique' -- sorted order with the mapping indices %!test %! [b, ia, ib] = unique (datetime (2021, 1, [1 1 2])); %! assert_equal (b.Day, [1 2]); %! assert_equal (ia, [1; 3]); %! assert_equal (ib, [1; 1; 2]); ## Test 'unique' -- round-trips B = A(ia) and A = B(ib) %!test %! a = datetime (2021, 1, [3 1 2 1]); %! [b, ia, ib] = unique (a); %! assert_equal (isequaln (b, a(ia)), true); %! assert_equal (isequaln (a, b(ib)), true); ## Test 'unique' -- each Not-A-Time is kept as a distinct value, sorted last %!test %! a = datetime (2021, 1, [3 1 NaN 1 NaN 2]); %! [b, ia, ib] = unique (a); %! assert_equal (size (b), [1 5]); %! assert_equal (b.Day(1:3), [1 2 3]); %! assert_equal (isnat (b), logical ([0 0 0 1 1])); %! assert_equal (ia, [2; 6; 1; 3; 5]); %! assert_equal (ib, [3; 1; 4; 1; 5; 2]); ## Test 'unique' -- an all-NaT array keeps every element %!assert_equal (sum (isnat (unique (datetime ([NaN NaN NaN], 1, 1)))), 3) ## Test 'unique' -- 'stable' keeps first-seen order, NaT included %!test %! a = datetime (2021, 1, [3 1 NaN 1 NaN 2]); %! [b, ia, ib] = unique (a, 'stable'); %! assert_equal (b.Day([1 2 5]), [3 1 2]); %! assert_equal (isnat (b), logical ([0 0 1 1 0])); %! assert_equal (ia, [1; 2; 3; 5; 6]); %! assert_equal (ib, [1; 2; 3; 2; 4; 5]); ## Test 'unique' -- 'first' (default) and 'last' occurrence indices %!test %! [~, ia] = unique (datetime (2020, 1, [2 1 2 1]), 'first'); %! assert_equal (ia, [2; 1]); %! [~, ia] = unique (datetime (2020, 1, [2 1 2 1]), 'last'); %! assert_equal (ia, [4; 3]); ## Test 'unique' -- 'rows' treats each row as a single value %!test %! [b, ia, ib] = unique (datetime (2021, 1, [1 2; 3 4; 1 2]), 'rows'); %! assert_equal (b.Day, [1 2; 3 4]); %! assert_equal (ia, [1; 2]); %! assert_equal (ib, [1; 2; 1]); ## Test 'unique' -- orientation follows the input, empties give a column %!assert_equal (size (unique (datetime (2021, 1, [3 1 2]))), [1 3]) %!assert_equal (size (unique (datetime (2021, 1, [3; 1; 2]))), [3 1]) %!assert_equal (size (unique (datetime (2021, 1, [3 1; 2 1]))), [3 1]) %!assert_equal (size (unique (datetime (2021, 1, []))), [0 1]) ## Test 'unique' -- values are compared by absolute instant; the zone is kept %!test %! z = unique (datetime (2021, 1, [2 1 2], 'TimeZone', 'America/New_York')); %! assert_equal (z.Day, [1 2]); %! assert_equal (z.TimeZone, 'America/New_York'); ## Test 'unique' -- the 'legacy' option is rejected %!error ... %! unique (datetime (2021, 1, [1 2 1]), 'legacy'); ## Test 'intersect' -- common values, sorted, carrying A's format %!test %! a = datetime (2026, 1, [1 2 3 4]); a.Format = 'dd/MM/uuuu'; %! b = datetime (2026, 1, [3 4 5 6]); b.Format = 'uuuu-MM-dd'; %! c = intersect (a, b); %! assert_equal (day (c), [3 4]); %! assert_equal (c.Format, 'dd/MM/uuuu'); %! assert_equal (intersect (b, a).Format, 'uuuu-MM-dd'); ## Test 'intersect' -- index outputs satisfy C = A(ixA) = B(ixB) %!test %! a = datetime (2026, 1, [1 2 3 4]); %! b = datetime (2026, 1, [3 4 5 6]); %! [c, ixA, ixB] = intersect (a, b); %! assert_equal (isequaln (c, a(ixA)), true); %! assert_equal (isequaln (c, b(ixB)), true); ## Test 'intersect' -- membership is by absolute instant, result keeps A's zone %!test %! a = datetime (2026, 1, 1, 12, 0, 0, 'TimeZone', 'UTC'); %! b = datetime (2026, 1, 1, 7, 0, 0, 'TimeZone', 'America/New_York'); %! c = intersect (a, b); %! assert_equal (numel (c), 1); %! assert_equal (c.TimeZone, 'UTC'); %! assert_equal (hour (c), 12); ## Test 'intersect' -- two zones whose wall clocks never share an instant %!test %! z1 = datetime (2026, 1, [1 2 3], 'TimeZone', 'America/New_York'); %! z2 = datetime (2026, 1, [2 3 4], 'TimeZone', 'Europe/Athens'); %! c = intersect (z1, z2); %! assert_equal (numel (c), 0); %! assert_equal (c.TimeZone, 'America/New_York'); ## Test 'intersect' -- each NaT is distinct and never matches %!test %! n1 = [datetime(2026,1,1), NaT, datetime(2026,1,2), NaT]; %! n2 = [NaT, datetime(2026,1,2)]; %! c = intersect (n1, n2); %! assert_equal (day (c), 2); %! assert_equal (numel (c), 1); ## Test 'intersect' -- orientation: column unless both inputs are rows %!assert_equal (size (intersect (datetime(2026,1,(1:4)'), datetime(2026,1, ... %! [3 4 5 6]))), [2 1]) %!assert_equal (size (intersect (datetime(2026,1,[1 2 3 4]), datetime(2026, ... %! 1,(3:6)'))), [2 1]) %!assert_equal (size (intersect (datetime(2026,1,[1 2 3 4]), datetime(2026, ... %! 1,[3 4 5 6]))), [1 2]) ## Test 'intersect' -- 'stable' returns values in order of appearance in A %!test %! a = datetime (2026, 1, [4 3 2 1]); %! b = datetime (2026, 1, [3 4 5 6]); %! assert_equal (day (intersect (a, b, 'stable')), [4 3]); ## Test 'intersect' -- 'rows' matches whole rows %!test %! m1 = datetime (2026, 1, [1 2; 3 4]); %! m2 = datetime (2026, 1, [3 4; 5 6]); %! assert_equal (day (intersect (m1, m2, 'rows')), [3 4]); ## Test 'intersect' -- empty operand gives a 0-by-1 result %!assert_equal (size (intersect (datetime (2026, 1, []), datetime (2026, 1, ... %! [3 4]))), [0 1]) ## Test 'intersect' -- infinite elements compare as ordinary sortable values %!test %! p = datetime (2026, 1, 1) + days (Inf); %! a = [datetime(2026,1,1), p, datetime(2026,1,1) - days(Inf)]; %! b = [p, datetime(2026,1,9)]; %! c = intersect (a, b); %! assert_equal (numel (c), 1); %! assert_equal (isinf (c) & ! isnat (c), true); ## Test 'intersect' -- text operands are promoted to datetime %!assert_equal (day (intersect (datetime (2026, 1, [1 2 3 4]), ... %! '02-Jan-2026')), 2) %!assert_equal (day (intersect (datetime (2026, 1, [1 2 3 4]), ... %! {'02-Jan-2026', '03-Jan-2026'})), [2 3]) ## Test 'intersect' -- invalid operands and options are rejected %!error ... %! intersect (datetime (2026, 1, [1 2]), [736000 736001]); %!error ... %! intersect (datetime (2026, 1, [1 2]), days (1)); %!error ... %! intersect (datetime (2026, 1, 1), datetime (2026, 1, 1, 'TimeZone', 'UTC')); %!error ... %! intersect (datetime (2026, 1, [1 2]), datetime (2026, 1, [2 3]), 'legacy'); ## Test set-ops -- cross-zone results are instant-based and NaT never collapses %!assert_equal (cellstr (intersect (datetime (2024, 1, 1, 12, 0, 0, ... %! 'TimeZone', 'America/New_York'), datetime (2024, 1, 1, 17, ... %! 0, 0, 'TimeZone', 'UTC'))), {'01-Jan-2024 12:00:00'}) %!assert_equal (numel (union (datetime (2024, 1, 1, [12 13], 0, 0, ... %! 'TimeZone', ... %! 'America/New_York'), datetime (2024, 1, 1, [17 20], 0, 0, ... %! 'TimeZone', 'UTC'))), 3) %!assert_equal (numel (setdiff (datetime (2024, 1, 1, [12 13], 0, 0, ... %! 'TimeZone', ... %! 'America/New_York'), datetime (2024, 1, 1, [17 20], 0, 0, ... %! 'TimeZone', 'UTC'))), 1) %!assert_equal (numel (unique ([datetime(2024, 1, 1), NaT, NaT])), 3) ## Test 'union' -- all unique values, sorted, carrying A's format %!test %! a = datetime (2026, 1, [1 2 3 4]); a.Format = 'dd/MM/uuuu'; %! b = datetime (2026, 1, [3 4 5 6]); %! c = union (a, b); %! assert_equal (day (c), [1 2 3 4 5 6]); %! assert_equal (c.Format, 'dd/MM/uuuu'); ## Test 'union' -- cross-zone values are interleaved by absolute instant %!test %! z1 = datetime (2026, 1, [1 2 3], 'TimeZone', 'America/New_York'); %! z2 = datetime (2026, 1, [2 3 4], 'TimeZone', 'Europe/Athens'); %! c = union (z1, z2); %! assert_equal (day (c), [1 1 2 2 3 3]); %! assert_equal (hour (c), [0 17 0 17 0 17]); %! assert_equal (c.TimeZone, 'America/New_York'); ## Test 'union' -- every distinct NaT is retained, sorted last %!test %! n1 = [datetime(2026,1,1), NaT, datetime(2026,1,2), NaT]; %! n2 = [NaT, datetime(2026,1,2)]; %! c = union (n1, n2); %! assert_equal (numel (c), 5); %! assert_equal (day (c)(1:2), [1 2]); %! assert_equal (isnat (c), logical ([0 0 1 1 1])); ## Test 'union' -- 'stable' keeps A's values then B's new values in order %!assert_equal (day (union (datetime (2026, 1, [4 1]), ... %! datetime (2026, 1, [1 7]), 'stable')), [4 1 7]) ## Test 'union' -- 'rows' combines unique rows %!test %! m1 = datetime (2026, 1, [1 2; 3 4]); %! m2 = datetime (2026, 1, [3 4; 5 6]); %! assert_equal (day (union (m1, m2, 'rows')), [1 2; 3 4; 5 6]); ## Test 'union' -- orientation and an empty operand %!assert_equal (size (union (datetime(2026,1,(1:2)'), datetime(2026,1, ... %! [2 3]))), [3 1]) %!assert_equal (size (union (datetime (2026, 1, []), datetime (2026, 1, ... %! [3 4]))), [2 1]) ## Test 'union' -- index outputs and text promotion %!assert_equal (day (union (datetime (2026, 1, [1 2]), '03-Jan-2026')), [1 2 3]) %!error ... %! union (datetime (2026, 1, 1), datetime (2026, 1, 1, 'TimeZone', 'UTC')); ## Test 'setdiff' -- values in A not in B, with the index output %!test %! a = datetime (2026, 1, [1 2 3 4]); %! b = datetime (2026, 1, [3 4 5 6]); %! [c, ixA] = setdiff (a, b); %! assert_equal (day (c), [1 2]); %! assert_equal (isequaln (c, a(ixA)), true); ## Test 'setdiff' -- A's NaT elements are all retained %!test %! n1 = [datetime(2026,1,1), NaT, datetime(2026,1,2), NaT]; %! n2 = [NaT, datetime(2026,1,2)]; %! c = setdiff (n1, n2); %! assert_equal (numel (c), 3); %! assert_equal (day (c)(1), 1); %! assert_equal (isnat (c), logical ([0 1 1])); ## Test 'setdiff' -- 'stable', 'rows', orientation %!assert_equal (day (setdiff (datetime (2026, 1, [4 1 2]), ... %! datetime (2026, 1, 2), 'stable')), [4 1]) %!test %! m1 = datetime (2026, 1, [1 2; 3 4]); %! m2 = datetime (2026, 1, [3 4; 5 6]); %! assert_equal (day (setdiff (m1, m2, 'rows')), [1 2]); %!assert_equal (size (setdiff (datetime (2026, 1, []), datetime (2026, 1, ... %! [3 4]))), [0 1]) ## Test 'setxor' -- values in exactly one input, sorted %!test %! a = datetime (2026, 1, [1 2 3 4]); %! b = datetime (2026, 1, [3 4 5 6]); %! assert_equal (day (setxor (a, b)), [1 2 5 6]); ## Test 'setxor' -- distinct NaT elements from both inputs are retained %!test %! n1 = [datetime(2026,1,1), NaT, datetime(2026,1,2), NaT]; %! n2 = [NaT, datetime(2026,1,2)]; %! c = setxor (n1, n2); %! assert_equal (numel (c), 4); %! assert_equal (day (c)(1), 1); %! assert_equal (isnat (c), logical ([0 1 1 1])); ## Test 'setxor' -- 'stable' and 'rows' %!assert_equal (day (setxor (datetime (2026, 1, [4 1]), ... %! datetime (2026, 1, [1 7]), 'stable')), [4 7]) %!test %! m1 = datetime (2026, 1, [1 2; 3 4]); %! m2 = datetime (2026, 1, [3 4; 5 6]); %! assert_equal (day (setxor (m1, m2, 'rows')), [1 2; 5 6]); ## Test 'ismember' -- membership, index, and same-instant across zones %!assert_equal (ismember (datetime (2020, 1, [1; 2; 3]), datetime (2020, 1, ... %! [2; 3])), ... %! [false; true; true]) %!test %! [tf, idx] = ismember (datetime (2020, 1, [1; 2; 3]), datetime (2020, 1, ... %! [2; 3])); %! assert_equal (tf, [false; true; true]); %! assert_equal (idx, [0; 1; 2]); %!assert_equal (ismember (datetime (2026, 1, 1, 12, 0, 0, 'TimeZone', ... %! 'UTC'), ... %! datetime (2026, 1, 1, 7, 0, 0, 'TimeZone', ... %! 'America/New_York')), true) ## Test 'ismember' -- NaT never matches; text first operand is promoted %!test %! n1 = [datetime(2026,1,1), NaT, datetime(2026,1,2), NaT]; %! n2 = [NaT, datetime(2026,1,2)]; %! [tf, idx] = ismember (n1, n2); %! assert_equal (tf, logical ([0 0 1 0])); %! assert_equal (idx, [0 0 2 0]); %!assert_equal (ismember ('02-Jan-2026', datetime (2026, 1, [1 2 3 4])), true) ## Test 'ismember' -- 'rows' and an empty first operand (TF follows A's size) %!test %! m1 = datetime (2026, 1, [1 2; 3 4]); %! m2 = datetime (2026, 1, [3 4; 5 6]); %! [tf, idx] = ismember (m1, m2, 'rows'); %! assert_equal (tf, [false; true]); %! assert_equal (idx, [0; 1]); %!assert_equal (size (ismember (datetime (2026, 1, []), datetime (2026, 1, ... %! [3 4]))), [0 0]) ## Test 'ismember' -- invalid options and zone mismatch are rejected %!error ... %! ismember (datetime (2026, 1, reshape (1:8, [2 2 2])), ... %! datetime (2026, 1, [1 2; 3 4]), 'rows'); %!error ... %! ismember (datetime (2026, 1, [1 2; 3 4]), ... %! datetime (2026, 1, [1 2 3; 4 5 6]), 'rows'); %!error ... %! ismember (datetime (2026, 1, [1 2]), datetime (2026, 1, [2 3]), 'bogus'); %!error ... %! ismember (datetime (2026, 1, 1), datetime (2026, 1, 1, 'TimeZone', 'UTC')); ################################################################################ ## ** Array Operations ** ## ################################################################################ ## Test 'cat' / 'horzcat' / 'vertcat' %!assert_equal (size ([datetime(2020,1,1), datetime(2020,1,2)]), [1, 2]) %!assert_equal (size ([datetime(2020,1,1); datetime(2020,1,2)]), [2, 1]) %!assert_equal (day ([datetime(2020,1,1), datetime(2020,1,2)]), [1, 2]) %!assert_equal (size (cat (1, datetime (2020, 1, [1; 2]), datetime (2020, 1, ... %! 3))), [3, 1]) %!assert_equal (day (vertcat (datetime (2020, 1, 1), datetime (2020, 1, ... %! 2))), [1; 2]) %!error ... %! cat (2, datetime (2020, 1, 1), 5) ## Test 'cat' -- a 0-by-0 [] or {} contributes nothing and is dropped, so an ## array can be accumulated from an empty start %!test %! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York'); %! assert_equal (size ([ny, []]), [1, 1]); %! assert_equal ([ny, []].TimeZone, 'America/New_York'); %! assert_equal (size ([[], ny]), [1, 1]); %! assert_equal ([[], ny].TimeZone, 'America/New_York'); %! assert_equal (size ([ny, {}]), [1, 1]); %! assert_equal (size ([ny, [], ny]), [1, 2]); %! assert_equal (size (cat (2, [], ny)), [1, 1]); %!test %! d = datetime.empty; %! d = [d, datetime(2024, 1, 1)]; %! d = [d, datetime(2024, 1, 2)]; %! d = [d, datetime(2024, 1, 3)]; %! assert_equal (d.Day, [1, 2, 3]); %!test %! d = []; %! d = [d, datetime(2024, 1, 1)]; %! d = [d, datetime(2024, 1, 2)]; %! d = [d, datetime(2024, 1, 3)]; %! assert_equal (d.Day, [1, 2, 3]); %!assert_equal (size ([datetime(2024, 1, 1), []]), [1, 1]) ## Test 'cat' -- date/time text is promoted, and is read as a wall clock in the ## time zone of the first datetime operand rather than converted into it %!test %! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York'); %! r = [ny, '2024-01-01']; %! assert_equal (size (r), [1, 2]); %! assert_equal (r.TimeZone, 'America/New_York'); %! assert_equal (r.Hour, [12, 0]); %! assert_equal (posixtime (r), [1704128400, 1704085200]); %!test %! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York'); %! r = [ny, '2024-01-01 06:00:00']; %! assert_equal (posixtime (r), [1704128400, 1704106800]); %!test %! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York'); %! assert_equal ([ny, {'2024-01-01'}].Hour, [12, 0]); %! assert_equal ([ny, string('2024-01-01')].Hour, [12, 0]); %! assert_equal (size ([ny, {'2024-01-01', '2024-01-02'}]), [1, 3]); %! assert_equal ([ny; '2024-01-02'].Day, [1; 2]); %!test %! un = datetime (2024, 1, 1, 12, 0, 0); %! r = [un, '2024-01-01']; %! assert_equal (isempty (r.TimeZone), true); %! assert_equal (r.Hour, [12, 0]); ## Test 'cat' -- text may come first, the first datetime operand still giving ## the result its time zone %!test %! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York'); %! r = ['2024-01-01', ny]; %! assert_equal (r.TimeZone, 'America/New_York'); %! assert_equal (r.Hour, [0, 12]); %!test %! un = datetime (2024, 1, 1, 12, 0, 0); %! r = ['2024-01-01', un]; %! assert_equal (isempty (r.TimeZone), true); %! assert_equal (r.Hour, [0, 12]); ## Test 'cat' -- text promoted into the leap-second zone can name an inserted ## second %!test %! d = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! r = [d, '2016-12-31T23:59:60Z']; %! assert_equal (r.Second, [60, 60]); ## Test 'cat' -- an empty [] is dropped before the time-zone test, so a mismatch ## behind it is still caught %!error ... %! horzcat (datetime (2024, 1, 1, 'TimeZone', 'UTC'), [], datetime (2024, 1, 1)) ## Test 'cat' -- a missing value becomes NaT, taking the zone and format of ## the first datetime operand (vs MATLAB R2024a) %!test %! D = [datetime(2020, 1, 1), missing]; %! assert_equal (size (D), [1, 2]); %! assert_equal (isnat (D), [false, true]); %!test %! D = [datetime(2020, 1, 1, 'TimeZone', 'UTC'), missing]; %! assert_equal (D.TimeZone, 'UTC'); %! assert_equal (isnat (D), [false, true]); %!test %! D = [datetime(2020, 1, 1, 'Format', 'yyyy'), missing]; %! assert_equal (cellstr (D), {'2020', 'NaT'}); %!assert_equal (size ([datetime(2020, 1, 1); missing]), [2, 1]) ## Test 'subsasgn' -- a missing value assigns NaT, and a gap opened by growing ## past the end is NaT as well (vs MATLAB R2024a) %!test %! D = datetime (2020, 1, 1); %! D(3) = missing; %! assert_equal (isnat (D), [false, true, true]); %!test %! D = datetime (2020, 1, 1, 'TimeZone', 'UTC'); %! D(2) = missing; %! assert_equal (D.TimeZone, 'UTC'); %! assert_equal (isnat (D), [false, true]); ## Test 'cat' -- anything that is neither a datetime nor date/time text is ## refused, an empty numeric of non-zero size included %!error ... %! horzcat (datetime (2024, 1, 1), zeros (0, 3)) %!error ... %! horzcat (datetime (2024, 1, 1), NaN) %!error ... %! horzcat (datetime (2024, 1, 1), true) %!error ... %! horzcat (datetime (2024, 1, 1), int32 (5)) %!error ... %! horzcat (datetime (2024, 1, 1), days (1)) %!error ... %! horzcat (datetime (2024, 1, 1), calmonths (1)) ## Test 'cat' -- zoned operands need not share a zone; each is converted into ## the zone of the first, so the instant each element names is preserved %!test %! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York'); %! ut = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'UTC'); %! r = [ny, ut]; %! assert_equal (r.TimeZone, 'America/New_York'); %! assert_equal (r.Hour, [12, 7]); %! assert_equal (posixtime (r), [posixtime(ny), posixtime(ut)]); %!test %! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York'); %! ut = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'UTC'); %! r = [ut, ny]; %! assert_equal (r.TimeZone, 'UTC'); %! assert_equal (r.Hour, [12, 17]); %! assert_equal (posixtime (r), [posixtime(ut), posixtime(ny)]); %!test %! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York'); %! lh = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'Australia/Lord_Howe'); %! r = [ny, lh]; %! assert_equal (datevec (r)(2,:), [2023, 12, 31, 20, 0, 0]); %! assert_equal (posixtime (r), [posixtime(ny), posixtime(lh)]); ## Test 'cat' -- the conversion applies along any dimension and to every operand %!test %! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York'); %! ut = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'UTC'); %! assert_equal (cat (1, ny, ut).Hour, [12; 7]); %! assert_equal (vertcat (ny, ut).Hour, [12; 7]); %! assert_equal ([ny, ny, ut].Hour, [12, 12, 7]); %!test %! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York'); %! ut = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'UTC'); %! r = cat (3, ny, ut); %! assert_equal (size (r), [1, 1, 2]); %! assert_equal (r.TimeZone, 'America/New_York'); ## Test 'cat' -- Format and TimeZone come from the first operand, and a NaT in ## another zone stays a NaT %!test %! ny = datetime (2024, 1, 1, 'Format', 'uuuu-MM-dd HH:mm:ss', ... %! 'TimeZone', 'America/New_York'); %! ut = datetime (2024, 1, 1, 'Format', 'dd-MMM-uuuu', 'TimeZone', 'UTC'); %! assert_equal ([ny, ut].Format, 'uuuu-MM-dd HH:mm:ss'); %! assert_equal ([ut, ny].Format, 'dd-MMM-uuuu'); %!test %! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York'); %! r = [ny, NaT('TimeZone', 'UTC')]; %! assert_equal (r.TimeZone, 'America/New_York'); %! assert_equal (isnat (r), logical ([0, 1])); ## Test 'cat' -- a zoned and an unzoned array cannot be concatenated, in either ## order, and a bare NaT or empty datetime is unzoned %!error ... %! horzcat (datetime (2024, 1, 1, 'TimeZone', 'UTC'), datetime (2024, 1, 1)) %!error ... %! horzcat (datetime (2024, 1, 1), datetime (2024, 1, 1, 'TimeZone', 'UTC')) %!error ... %! cat (1, datetime (2024, 1, 1, 'TimeZone', 'UTC'), datetime (2024, 1, 1)) %!error ... %! vertcat (datetime (2024, 1, 1, 'TimeZone', 'UTC'), datetime (2024, 1, 1)) %!error ... %! horzcat (datetime (2024, 1, 1, 'TimeZone', 'UTC'), ... %! datetime (2024, 1, 1, 'TimeZone', 'America/New_York'), ... %! datetime (2024, 1, 1)) %!error ... %! horzcat (datetime (2024, 1, 1, 'TimeZone', 'UTC'), NaT) %!error ... %! horzcat (datetime (2024, 1, 1, 'TimeZone', 'UTC'), datetime.empty) %!assert_equal (isempty ([datetime(2024, 1, 1), datetime(2024, 1, ... %! 2)].TimeZone), ... %! true) ## Test 'cat' -- the zone check is made before the leap-second one, as MATLAB ## does, and two leap-second arrays concatenate unchanged %!error ... %! horzcat (datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'), ... %! datetime (2017, 1, 1, 'TimeZone', 'UTC')) %!error ... %! horzcat (datetime (2017, 1, 1, 'TimeZone', 'UTC'), ... %! datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds')) %!error ... %! horzcat (datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'), ... %! datetime (2017, 1, 1)) %!test %! d = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! r = [d, d]; %! assert_equal (r.Second, [60, 60]); %! assert_equal (r.TimeZone, 'UTCLeapSeconds'); ## Test 'repmat' / 'repelem' %!assert_equal (size (repmat (datetime (2020, 1, 1), 2, 3)), [2, 3]) %!assert_equal (day (repmat (datetime (2020, 1, 5), 1, 3)), [5, 5, 5]) %!assert_equal (day (repelem (datetime (2020, 1, [1, 2]), 1, 2)), [1, 1, 2, 2]) ## Test 'reshape' %!assert_equal (size (reshape (datetime (2020, 1, 1:6), 2, 3)), [2, 3]) %!assert_equal (size (reshape (datetime (2020, 1, 1:6), 3, [])), [3, 2]) %!assert_equal (day (reshape (datetime (2020, 1, 1:4), 2, 2)), [1, 3; 2, 4]) ## Test 'circshift' %!assert_equal (day (circshift (datetime (2020, 1, [1; 2; 3]), 1)), [3; 1; 2]) ## Test 'permute' / 'ipermute' %!assert_equal (size (permute (datetime (2020, 1, [1, 2, 3]), [2, 1])), [3, 1]) %!assert_equal (day (permute (datetime (2020, 1, [1, 2]), [2, 1])), [1; 2]) %!test %! d = datetime (2020, ones (2, 3), 1); %! assert_equal (size (ipermute (permute (d, [2, 1]), [2, 1])), [2, 3]); ## Test 'transpose' / 'ctranspose' %!assert_equal (size (transpose (datetime (2020, 1, [1; 2; 3]))), [1, 3]) %!assert_equal (day (datetime (2020, 1, [1; 2; 3]).'), [1, 2, 3]) %!assert_equal (day (datetime (2020, 1, [1; 2; 3])'), [1, 2, 3]) ################################################################################ ## ** Reference and Assignment Operations ** ## ################################################################################ ## Test 'end' and subscripted reference %!assert_equal (day (datetime (2020, 1, [1; 2; 3])(end)), 3) %!assert_equal (day (datetime (2020, 1, [1; 2; 3])(2)), 2) %!assert_equal (day (datetime (2020, 1, [1, 2, 3])(1:2)), [1, 2]) %!assert_equal (class (datetime (2020, 1, [1; 2; 3])(2)), 'datetime') ## Test property reference via subsref %!assert_equal (datetime (2020, 6, 15).Year, 2020) %!assert_equal (datetime (2020, 6, 15).Month, 6) %!assert_equal (datetime (2020, 6, 15).Day, 15) %!assert_equal (datetime (2020, 1, 1, 3, 4, 5).Hour, 3) %!assert_equal (datetime (2020, 1, 1).Format, 'dd-MMM-uuuu') %!error datetime (2020, 1, 1).bogus %!error ... %! datetime (2020, 1, 1){1} ## Test subscripted assignment -- element assignment %!test %! d = datetime (2020, 1, [1; 2; 3]); %! d(2) = datetime (2020, 1, 20); %! assert_equal (day (d), [1; 20; 3]); %!test %! d = datetime (2020, 1, [1; 2; 3]); %! d(2) = []; %! assert_equal (day (d), [1; 3]); %!error ... %! d = datetime (2020, 1, 1); d(1) = 5; ## Test subscripted assignment -- component setters re-canonicalise (rollover) %!test # d.Month = 13 rolls into the following year (MATLAB parity) %! d = datetime (2020, 1, 15); %! d.Month = 13; %! assert_equal ([d.Year, d.Month, d.Day], [2021, 1, 15]); %!test # d.Day = 32 rolls into the next month %! d = datetime (2020, 1, 15); %! d.Day = 32; %! assert_equal ([d.Year, d.Month, d.Day], [2020, 2, 1]); %!test # d.Hour = 25 rolls into the next day %! d = datetime (2020, 1, 1); %! d.Hour = 25; %! assert_equal ([d.Day, d.Hour], [2, 1]); %!test # setting a component preserves sub-second precision %! d = datetime (2020, 1, 1, 0, 0, 0.25); %! d.Minute = 30; %! assert_equal (second (d), 0.25); %!test # direct component assignment on a single element of a vector %! d = datetime (2020, 1, [1; 2; 3]); %! d.Year(2) = 1999; %! assert_equal (year (d), [2020; 1999; 2020]); ## Test 'proxyArray' (regression: was undefined, breaking table interop) %!assert_equal (datetime (2020, 6, 15).proxyArray, [2020, 6, 15, 0, 0, 0, 0]) %!assert_equal (size (datetime (2020, 1, [1; 2; 3]).proxyArray), [3, 7]) %!test %! d = datetime (2020, 1, [1; 2]); %! assert_equal (d.proxyArray, [2020, 1, 1, 0, 0, 0, 0; 2020, 1, 2, 0, 0, 0, ... %! 0]); %!test # NaT maps to NaN across its component columns %! d = datetime (2020, 1, [1; 2]); %! d(1) = NaT; %! p = d.proxyArray; %! assert_equal (p(1, :), [NaN, NaN, NaN, NaN, NaN, NaN, NaN]); ## The seventh column is the negated UTC offset, which keeps the two moments ## sharing a wall clock across a fall-back apart and in chronological order. %!test %! t = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC'); %! t.TimeZone = 'America/New_York'; %! u = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC'); %! u.TimeZone = 'America/New_York'; %! p = [t; u].proxyArray; %! assert_equal (p(:, 1:6), [2024, 11, 3, 1, 30, 0; 2024, 11, 3, 1, 30, 0]); %! assert_equal (p(:, 7), [14400; 18000]); ################################################################################ ## ** TimeZone handling ** ## ################################################################################ ## Test attaching a zone to an unzoned array does NOT shift the wall clock %!test %! d = datetime (2020, 6, 1, 12, 0, 0); %! d.TimeZone = 'America/New_York'; %! assert_equal ([d.Hour, d.Minute], [12, 0]); %! assert_equal (d.TimeZone, 'America/New_York'); ## Test converting between zones preserves the instant (wall clock shifts) %!test # UTC noon in January is 07:00 EST (UTC-5) %! d = datetime (2020, 1, 1, 12, 0, 0, 'TimeZone', 'UTC'); %! d.TimeZone = 'America/New_York'; %! assert_equal ([d.Hour, d.Minute], [7, 0]); %!test # UTC noon in July is 08:00 EDT (UTC-4, daylight saving) %! d = datetime (2020, 7, 1, 12, 0, 0, 'TimeZone', 'UTC'); %! d.TimeZone = 'America/New_York'; %! assert_equal ([d.Hour, d.Minute], [8, 0]); ## Test dropping the zone keeps the current wall-clock values %!test %! d = datetime (2020, 1, 1, 12, 0, 0, 'TimeZone', 'UTC'); %! d.TimeZone = ''; %! assert_equal ([d.Hour, d.Minute], [12, 0]); %! assert_equal (d.TimeZone, ''); ## Test invalid TimeZone assignment errors %!error ... %! d = datetime (2020, 1, 1); d.TimeZone = 5; ## Test SystemTimeZone is a zone name, not an abbreviation, stored as 'local' ## stores it %!test %! d = datetime (2024, 1, 1, 'TimeZone', datetime.SystemTimeZone); %! assert_equal (d.TimeZone, ... %! datetime (2024, 1, 1, 'TimeZone', 'local').TimeZone); ## Test 'local' names the system time zone, in any letter case, and is stored ## as naming that zone stores it %!test %! d = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'local'); %! assert_equal (d.TimeZone, datetime (2024, 1, 1, 'TimeZone', ... %! datetime.SystemTimeZone).TimeZone); %! assert_equal ([d.Hour, d.Minute], [12, 0]); %!test %! d = datetime (2024, 1, 1, 'TimeZone', 'LOCAL'); %! assert_equal (d.TimeZone, datetime (2024, 1, 1, 'TimeZone', ... %! datetime.SystemTimeZone).TimeZone); %!test %! d = datetime ('2024-01-01 12:00:00', 'TimeZone', 'Local'); %! assert_equal (d.TimeZone, datetime (2024, 1, 1, 'TimeZone', ... %! datetime.SystemTimeZone).TimeZone); %! assert_equal ([d.Hour, d.Minute], [12, 0]); %!test %! d = datetime (0, 'ConvertFrom', 'posixtime', 'TimeZone', 'local'); %! assert_equal (d.TimeZone, datetime (2024, 1, 1, 'TimeZone', ... %! datetime.SystemTimeZone).TimeZone); %! assert_equal (posixtime (d), 0); %!test %! u = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'UTC'); %! d = datetime (u, 'TimeZone', 'local'); %! assert_equal (d.TimeZone, datetime (2024, 1, 1, 'TimeZone', ... %! datetime.SystemTimeZone).TimeZone); %! assert_equal (posixtime (d), posixtime (u)); %!test %! d = datetime (2024, 1, 1, 12, 0, 0); %! d.TimeZone = 'local'; %! assert_equal (d.TimeZone, datetime (2024, 1, 1, 'TimeZone', ... %! datetime.SystemTimeZone).TimeZone); %! assert_equal ([d.Hour, d.Minute], [12, 0]); %!test %! u = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'UTC'); %! d = u; %! d.TimeZone = 'Local'; %! assert_equal (d.TimeZone, datetime (2024, 1, 1, 'TimeZone', ... %! datetime.SystemTimeZone).TimeZone); %! assert_equal (posixtime (d), posixtime (u)); ## Test a fixed-offset zone is stored as '+HH:MM' whatever its spelling ## (every value measured against MATLAB R2024a) %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '-0500'); %! assert_equal (d.TimeZone, '-05:00'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '+3'); %! assert_equal (d.TimeZone, '+03:00'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '+03'); %! assert_equal (d.TimeZone, '+03:00'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '+033'); %! assert_equal (d.TimeZone, '+00:33'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '+0330'); %! assert_equal (d.TimeZone, '+03:30'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '+05:45'); %! assert_equal (d.TimeZone, '+05:45'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '+030030'); %! assert_equal (d.TimeZone, '+03:00:30'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '+03:00:30'); %! assert_equal (d.TimeZone, '+03:00:30'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '-00:00'); %! assert_equal (d.TimeZone, '+00:00'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '+00'); %! assert_equal (d.TimeZone, '+00:00'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'Z'); %! assert_equal (d.TimeZone, '+00:00'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'UTC+03:00'); %! assert_equal (d.TimeZone, '+03:00'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'utc+03:00'); %! assert_equal (d.TimeZone, '+03:00'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'UTC+3:30'); %! assert_equal (d.TimeZone, '+03:30'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'UTC+1:30'); %! assert_equal (d.TimeZone, '+01:30'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '+03:00'); %! d.TimeZone = '+3'; %! assert_equal (d.TimeZone, '+03:00'); ## Test a fixed offset may be up to 23:59 either way %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '+23:59'); %! assert_equal (d.TimeZone, '+23:59'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '-23:59'); %! assert_equal (d.TimeZone, '-23:59'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '+15:00'); %! assert_equal (d.TimeZone, '+15:00'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '-13:00'); %! assert_equal (d.TimeZone, '-13:00'); ## Test the database's fixed-offset zones are stored as offsets %!test %! d = datetime (2024, 3, 9, 'TimeZone', 'Etc/GMT-3'); %! assert_equal (d.TimeZone, '+03:00'); %!test %! d = datetime (2024, 3, 9, 'TimeZone', 'Etc/GMT'); %! assert_equal (d.TimeZone, '+00:00'); %!test %! d = datetime (2024, 3, 9, 'TimeZone', 'Etc/UTC'); %! assert_equal (d.TimeZone, '+00:00'); %!test %! d = datetime (2024, 3, 9, 'TimeZone', 'Europe/Athens'); %! d.TimeZone = 'Etc/GMT-2'; %! assert_equal (d.TimeZone, '+02:00'); %!test %! d = datetime (2024, 3, 9, 'TimeZone', '+03:00'); %! d.TimeZone = 'Europe/Athens'; %! assert_equal (d.TimeZone, 'Europe/Athens'); %!test %! d = datetime (2024, 3, 9, 'TimeZone', 'Etc/GMT+5'); %! assert_equal (d.TimeZone, '-05:00'); %!test %! d = datetime (2024, 3, 9, 'TimeZone', 'Etc/GMT-14'); %! assert_equal (d.TimeZone, '+14:00'); %!test %! d = datetime (2024, 3, 9, 'TimeZone', 'Etc/GMT+0'); %! assert_equal (d.TimeZone, '+00:00'); %!test %! d = datetime (2024, 3, 9, 'TimeZone', 'Etc/GMT0'); %! assert_equal (d.TimeZone, '+00:00'); %!test %! d = datetime (2024, 3, 9, 'TimeZone', 'Etc/UCT'); %! assert_equal (d.TimeZone, '+00:00'); %!test %! d = datetime (2024, 3, 9, 'TimeZone', 'Etc/Zulu'); %! assert_equal (d.TimeZone, '+00:00'); %!test %! d = datetime (2024, 3, 9, 'TimeZone', 'Etc/Greenwich'); %! assert_equal (d.TimeZone, '+00:00'); %!test %! d = datetime (2024, 3, 9, 'TimeZone', 'Etc/Universal'); %! assert_equal (d.TimeZone, '+00:00'); %!error ... %! datetime (2024, 3, 9, 'TimeZone', 'Etc/GMT-15') ## Test the bare zero-offset aliases warn and are stored as offsets %!warning ... %! datetime (2024, 3, 9, 'TimeZone', 'Zulu'); %!test %! warning ('off', 'Octave:datetime:nonstandard-time-zone', 'local'); %! d = datetime (2024, 3, 9, 'TimeZone', 'GMT0'); %! assert_equal (d.TimeZone, '+00:00'); %!test %! warning ('off', 'Octave:datetime:nonstandard-time-zone', 'local'); %! d = datetime (2024, 3, 9, 'TimeZone', 'UCT'); %! assert_equal (d.TimeZone, '+00:00'); %!test %! warning ('off', 'Octave:datetime:nonstandard-time-zone', 'local'); %! d = datetime (2024, 3, 9, 'TimeZone', 'Universal'); %! assert_equal (d.TimeZone, '+00:00'); %!test %! warning ('off', 'Octave:datetime:nonstandard-time-zone', 'local'); %! d = datetime (2024, 3, 9, 'TimeZone', 'Greenwich'); %! assert_equal (d.TimeZone, '+00:00'); %!test %! warning ('off', 'Octave:datetime:nonstandard-time-zone', 'local'); %! d = datetime (2024, 3, 9, 'TimeZone', 'GMT+03'); %! assert_equal (d.TimeZone, '+03:00'); %!test %! d = datetime (2024, 3, 9, 'TimeZone', 'UTC+03:00:30'); %! assert_equal (d.TimeZone, '+03:00:30'); ## Test a 'GMT' prefix warns and is stored as an offset of the same sign %!warning ... %! datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'GMT+2'); %!test %! warning ('off', 'Octave:datetime:nonstandard-time-zone', 'local'); %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'gmt+2'); %! assert_equal (d.TimeZone, '+02:00'); %!test %! warning ('off', 'Octave:datetime:nonstandard-time-zone', 'local'); %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'GMT-0'); %! assert_equal (d.TimeZone, '+00:00'); %!test %! lastwarn (''); %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'UTC+2'); %! assert_equal (lastwarn (), ''); ## Test a malformed or out-of-range fixed offset is refused %!error ... %! datetime (2024, 3, 9, 'TimeZone', '+1:30') %!error ... %! datetime (2024, 3, 9, 'TimeZone', '+3:5') %!error ... %! datetime (2024, 3, 9, 'TimeZone', '+24:00') %!error ... %! datetime (2024, 3, 9, 'TimeZone', '-24:00') %!error ... %! datetime (2024, 3, 9, 'TimeZone', '+03:60') %!error ... %! datetime (2024, 3, 9, 'TimeZone', '+03:00:60') %!error ... %! datetime (2024, 3, 9, 'TimeZone', '+03:00:5') %!error ... %! datetime (2024, 3, 9, 'TimeZone', ' +03:00') %!error ... %! datetime (2024, 3, 9, 'TimeZone', '+03:00 ') %!error ... %! datetime (2024, 3, 9, 'TimeZone', 'UTC +03:00') %!error ... %! datetime (2024, 3, 9, 'TimeZone', 'UTC+5:3') %!error ... %! datetime (2024, 3, 9, 'TimeZone', 'UTC+033') %!error ... %! datetime (2024, 3, 9, 'TimeZone', 'UTC+0330') %!error ... %! datetime (2024, 3, 9, 'TimeZone', 'UTC+030030') %!error ... %! d = datetime (2024, 3, 9, 'TimeZone', 'UTC'); d.TimeZone = '+24:00'; ## Test a fixed-offset zone has no daylight saving %!assert_equal (isdst (datetime (2024, 7, 1, 'TimeZone', '+03:00')), false) %!assert_equal (hours (tzoffset (datetime (2024, 7, 1, 'TimeZone', ... %! '+03:00'))), 3) %!assert_equal (isnan (hours (tzoffset (NaT ('TimeZone', '+03:00')))), true) %!assert_equal (isnan (hours (tzoffset (datetime (Inf, 1, 1, 'TimeZone', ... %! '+03:00')))), true) ## Test the offset of a Not-a-Time or infinite element is NaN in every zone %!assert_equal (isnan (hours (tzoffset (NaT ('TimeZone', 'UTC')))), true) %!assert_equal (isnan (hours (tzoffset (NaT ('TimeZone', ... %! 'Europe/Athens')))), true) %!assert_equal (isnan (hours (tzoffset (datetime (Inf, 1, 1, 'TimeZone', ... %! 'Europe/Athens')))), true) ## Test the second output of 'tzoffset' is the daylight saving part ## (every value measured against MATLAB R2024a) %!test %! [dt, dst] = tzoffset (datetime (2024, [1, 7], 1, 12, 0, 0, ... %! 'TimeZone', 'Europe/Athens')); %! assert_equal (hours (dt), [2, 3]); %! assert_equal (hours (dst), [0, 1]); %! assert_equal (class (dst), 'duration'); %! assert_equal (dst.Format, 'hh:mm'); %!test %! [dt, dst] = tzoffset (datetime (2024, 1, 15, 12, 0, 0, ... %! 'TimeZone', 'Australia/Lord_Howe')); %! assert_equal (hours (dt), 11); %! assert_equal (hours (dst), 0.5); %!test %! t1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC'); %! t1.TimeZone = 'America/New_York'; %! t2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC'); %! t2.TimeZone = 'America/New_York'; %! [dt, dst] = tzoffset ([t1; t2]); %! assert_equal (hours (dt), [-4; -5]); %! assert_equal (hours (dst), [1; 0]); %!test %! [~, dst] = tzoffset (datetime (2024, 7, 1, 'TimeZone', '+03:00')); %! assert_equal (hours (dst), 0); %!test %! [~, dst] = tzoffset (datetime (2024, [1, 7], 1, 'TimeZone', 'UTC')); %! assert_equal (hours (dst), [0, 0]); %!test %! [~, dst] = tzoffset (datetime (2024, 7, 15, 12, 0, 0, ... %! 'TimeZone', 'UTCLeapSeconds')); %! assert_equal (hours (dst), 0); %!test %! [dt, dst] = tzoffset (datetime (2024, 7, 1, 12, 0, 0)); %! assert_equal (isnan (hours ([dt, dst])), [true, true]); %!test %! [~, dst] = tzoffset (NaT ('TimeZone', 'Europe/Athens')); %! assert_equal (isnan (hours (dst)), true); %!test %! [~, dst] = tzoffset (datetime (Inf, 1, 1, 'TimeZone', 'Europe/Athens')); %! assert_equal (isnan (hours (dst)), true); ## Test a zone the database gives a negative saving counts its summer as the ## daylight saving time, as MATLAB R2024a counts it %!test %! d = datetime (2024, [1, 7], 15, 12, 0, 0, 'TimeZone', 'Europe/Dublin'); %! [dt, dst] = tzoffset (d); %! assert_equal (hours (dt), [0, 1]); %! assert_equal (hours (dst), [0, 1]); %! assert_equal (isdst (d), [false, true]); %!test %! d = datetime (2024, [1, 7], 15, 12, 0, 0, 'TimeZone', 'Europe/London'); %! [dt, dst] = tzoffset (d); %! assert_equal (hours (dt), [0, 1]); %! assert_equal (hours (dst), [0, 1]); %! assert_equal (isdst (d), [false, true]); %!test %! d = datetime (2024, [1, 3], [15, 20], 12, 0, 0, ... %! 'TimeZone', 'Africa/Casablanca'); %! [dt, dst] = tzoffset (d); %! assert_equal (hours (dt), [1, 0]); %! assert_equal (hours (dst), [1, 0]); %! assert_equal (isdst (d), [true, false]); %!test %! d = datetime (1970, 6, 15, 12, 0, 0, 'TimeZone', 'Europe/Dublin'); %! [dt, dst] = tzoffset (d); %! assert_equal (hours (dt), 1); %! assert_equal (hours (dst), 0); %! assert_equal (isdst (d), false); %!test %! d = datetime (2024, 3, 30, 12, 0, 0, 'TimeZone', '+02:00') + days (1); %! assert_equal ([day(d), hour(d)], [31, 12]); %!test %! d = datetime (2024, 3, 30, 12, 0, 0, 'TimeZone', '+02:00') + caldays (1); %! assert_equal ([day(d), hour(d)], [31, 12]); ## Test a fixed-offset zone names instants like any other zone %!assert_equal (posixtime (datetime (1970, 1, 1, 3, 0, 0, 'TimeZone', ... %! '+03:00')), 0) %!test %! d = datetime (0, 'ConvertFrom', 'posixtime', 'TimeZone', '+03:00'); %! assert_equal (hour (d), 3); %!test %! d = datetime (2024, 3, 9, 12, 0, 0, 'TimeZone', 'UTC'); %! d.TimeZone = '+05:30'; %! assert_equal ([hour(d), minute(d)], [17, 30]); %!test %! d = datetime (2024, 3, 9, 12, 0, 0); %! d.TimeZone = '+05:30'; %! assert_equal ([hour(d), minute(d)], [12, 0]); %!test %! d = datetime (2024, 3, 9, 12, 0, 0, 'TimeZone', '+05:30'); %! d.TimeZone = ''; %! assert_equal ([hour(d), minute(d)], [12, 0]); %!test %! d = datetime (2024, 3, 9, 12, 0, 0, 'TimeZone', '+05:30'); %! d.TimeZone = 'UTCLeapSeconds'; %! assert_equal (char (d), '2024-03-09T06:30:00.000Z'); %!test %! d = datetime ('2024-03-09 12:00:00', 'TimeZone', '+03:00'); %! assert_equal ([hour(d), minute(d)], [12, 0]); %!test %! d = datetime ('now', 'TimeZone', '+03:00'); %! u = datetime ('now', 'TimeZone', 'UTC'); %! assert_equal (d.TimeZone, '+03:00'); %! assert_equal (abs (seconds (u - d)) < 5, true); %!test %! d = datetime ('2024-03-09 22:30', 'InputFormat', 'uuuu-MM-dd HH:mm', ... %! 'TimeZone', '+05:30'); %! assert_equal (char (d, 'uuuu-MM-dd HH:mm XXX'), '2024-03-09 22:30 +05:30'); ## Test comparing and combining fixed-offset zones with other zones %!assert_equal (datetime (2024, 3, 9, 12, 0, 0, 'TimeZone', '+03:00') == ... %! datetime (2024, 3, 9, 9, 0, 0, 'TimeZone', 'UTC'), true) %!assert_equal (isequal (datetime (2024, 3, 9, 12, 0, 0, 'TimeZone', ... %! '+03:00'), datetime (2024, 3, 9, 9, 0, 0, 'TimeZone', ... %! 'UTC')), true) %!assert_equal (hours (datetime (2024, 3, 9, 12, 0, 0, 'TimeZone', ... %! '+03:00') - datetime (2024, 3, 9, 12, 0, 0, 'TimeZone', ... %! '+05:30')), 2.5) %!test %! d = [datetime(2024, 3, 9, 12, 0, 0, 'TimeZone', '+03:00'), ... %! datetime(2024, 3, 9, 12, 0, 0, 'TimeZone', 'Europe/Athens')]; %! assert_equal (d.TimeZone, '+03:00'); %! assert_equal (hour (d), [12, 13]); %!test %! d = [datetime(2024, 3, 9, 12, 0, 0, 'TimeZone', 'Europe/Athens'), ... %! datetime(2024, 3, 9, 12, 0, 0, 'TimeZone', '+03:00')]; %! assert_equal (d.TimeZone, 'Europe/Athens'); %! assert_equal (hour (d), [12, 11]); ## Test a fixed-offset zone holds years the timezone database cannot %!test %! d = datetime (40000, 1, 1, 12, 0, 0, 'TimeZone', '+03:00'); %! assert_equal ([year(d), hour(d)], [40000, 12]); %!test %! d = datetime (-40000, 1, 1, 12, 0, 0, 'TimeZone', '+03:00'); %! assert_equal ([year(d), hour(d)], [-40000, 12]); %!test %! d = datetime (40000, 1, 1, 12, 0, 0, 'TimeZone', 'UTC'); %! d.TimeZone = '+03:00'; %! assert_equal ([year(d), hour(d)], [40000, 15]); ## Test day-level operations on a fixed-offset zone %!assert_equal (char (dateshift (datetime (2024, 3, 9, 22, 30, 0, ... %! 'TimeZone', '+05:30'), 'start', 'day')), '09-Mar-2024') %!assert_equal (hours (timeofday (datetime (2024, 3, 9, 22, 30, 0, ... %! 'TimeZone', '+05:30'))), 22.5) ## Test rendering a fixed-offset zone (MATLAB-style names via 'zzzzz') %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '+05:30'); %! assert_equal (char (d, 'zzzzz Z ZZZZ XXX'), ... %! 'UTC+5:30 +0530 UTC+05:30 +05:30'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '-03:30'); %! assert_equal (char (d, 'zzzzz x'), 'UTC-3:30 -0330'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '-05:00'); %! assert_equal (char (d, 'zzzzz ZZZZ XXX'), 'UTC-5 UTC-05:00 -05:00'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '+03:00:30'); %! assert_equal (char (d, 'zzzzz ZZZZ XXXXX x'), ... %! 'UTC+3:00:30 UTC+03:00:30 +03:00:30 +03'); %!test %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', '+00:00'); %! assert_equal (char (d, 'zzzzz XXX Z ZZZZ x ZZZZZ'), 'GMT Z +0000 UTC +00 Z'); %!test %! warning ('off', 'Octave:datetime:nonstandard-time-zone', 'local'); %! d = datetime (2024, 3, 9, 22, 0, 0, 'TimeZone', 'GMT+2'); %! assert_equal (char (d, 'zzzzz'), 'UTC+2'); ## Test the two instants sharing a wall clock across a fall-back stay apart. ## On 3 November 2024 America/New_York puts its clock back at 02:00, so ## 01:30 comes round twice: once at UTC-4 and once at UTC-5. Every value ## below was measured against MATLAB R2024a. %!test # the pair is built from UTC, which names each of them unambiguously %! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC'); %! b1.TimeZone = 'America/New_York'; %! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC'); %! b2.TimeZone = 'America/New_York'; %! assert_equal (posixtime ([b1, b2]), [1730611800, 1730615400]); %! assert_equal (hours (tzoffset ([b1, b2])), [-4, -5]); %! assert_equal (cellstr ([b1, b2]), ... %! {'03-Nov-2024 01:30:00', '03-Nov-2024 01:30:00'}); %! assert_equal (isdst ([b1, b2]), [true, false]); %!test # concatenation carries each element's own offset, in either order %! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC'); %! b1.TimeZone = 'America/New_York'; %! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC'); %! b2.TimeZone = 'America/New_York'; %! assert_equal (hours (tzoffset ([b2, b1])), [-5, -4]); %! assert_equal (posixtime ([b2, b1]), [1730615400, 1730611800]); %!test # a round trip through another zone comes back to the same instant %! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC'); %! b1.TimeZone = 'America/New_York'; %! d = b1; d.TimeZone = 'UTC'; d.TimeZone = 'America/New_York'; %! assert_equal (posixtime (d), 1730611800); %! assert_equal (hours (tzoffset (d)), -4); %! e = b1; e.TimeZone = 'Europe/Athens'; e.TimeZone = 'America/New_York'; %! assert_equal (posixtime (e), 1730611800); %! assert_equal (hours (tzoffset (e)), -4); %!test # they are two values, not one, to every comparison and set operation %! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC'); %! b1.TimeZone = 'America/New_York'; %! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC'); %! b2.TimeZone = 'America/New_York'; %! assert_equal (b1 == b2, false); %! assert_equal (b1 < b2, true); %! assert_equal (isequal (b1, b2), false); %! assert_equal (numel (unique ([b1, b2])), 2); %! assert_equal (ismember (b1, b2), false); %! assert_equal (numel (intersect (b1, b2)), 0); %! assert_equal (numel (union (b1, b2)), 2); %! assert_equal (keyHash (b1) == keyHash (b2), false); %!test # sorting is by instant, so the pair comes back in the other order %! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC'); %! b1.TimeZone = 'America/New_York'; %! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC'); %! b2.TimeZone = 'America/New_York'; %! [sv, si] = sort ([b2, b1]); %! assert_equal (si, [2, 1]); %! assert_equal (posixtime (sv), [1730611800, 1730615400]); %! assert_equal (hours (diff ([b1, b2])), 1); %!test # the zone fields of a display format read the element's own offset %! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC'); %! b1.TimeZone = 'America/New_York'; %! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC'); %! b2.TimeZone = 'America/New_York'; %! p = [b1, b2]; %! p.Format = 'yyyy-MM-dd HH:mm:ss z'; %! assert_equal (cellstr (p), {'2024-11-03 01:30:00 EDT', ... %! '2024-11-03 01:30:00 EST'}); %! p.Format = 'yyyy-MM-dd HH:mm:ss Z'; %! assert_equal (cellstr (p), {'2024-11-03 01:30:00 -0400', ... %! '2024-11-03 01:30:00 -0500'}); %!test # the wall-clock numeric views stay deliberately non-injective %! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC'); %! b1.TimeZone = 'America/New_York'; %! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC'); %! b2.TimeZone = 'America/New_York'; %! assert_equal (datenum (b1), datenum (b2)); %! assert_equal (exceltime (b1), exceltime (b2)); %! assert_equal (datevec (b1), datevec (b2)); %! assert (juliandate (b1) != juliandate (b2)); %!test # a calendar difference of none still leaves the hour between them %! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC'); %! b1.TimeZone = 'America/New_York'; %! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC'); %! b2.TimeZone = 'America/New_York'; %! assert_equal (char (between (b1, b2)), '1h 0m 0s'); %! assert_equal (char (between (b2, b1)), '-1h 0m 0s'); %! assert_equal (char (caldiff ([b1, b2])), '1h 0m 0s'); %!test # a shift within the day stays in the element's own fold %! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC'); %! b1.TimeZone = 'America/New_York'; %! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC'); %! b2.TimeZone = 'America/New_York'; %! p = [b1, b2]; %! assert_equal (posixtime (dateshift (p, 'start', 'hour')), ... %! [1730610000, 1730613600]); %! assert_equal (hours (tzoffset (dateshift (p, 'start', 'hour'))), [-4, -5]); %! assert_equal (posixtime (dateshift (p, 'end', 'hour')), ... %! [1730613600, 1730617200]); %! assert_equal (posixtime (dateshift (p, 'start', 'day')), ... %! [1730606400, 1730606400]); %! assert_equal (posixtime (dateshift (p, 'end', 'day')), ... %! [1730696400, 1730696400]); %!test # a calendar step onto a repeated clock lands on the source's fold %! z = 'America/New_York'; %! a = datetime (2024, 11, 2, 1, 30, 0, 'TimeZone', z); %! r = a : caldays (1) : a + caldays (2); %! assert_equal (posixtime (r), [1730525400, 1730611800, 1730701800]); %! assert_equal (hours (tzoffset (r)), [-4, -4, -5]); %!test # stepping the instant crosses the fold rather than resolving to it %! z = 'America/New_York'; %! a = datetime (2024, 11, 3, 0, 30, 0, 'TimeZone', z); %! L = linspace (a, a + hours (3), 4); %! assert_equal (posixtime (L), ... %! [1730608200, 1730611800, 1730615400, 1730619000]); %! assert_equal (hours (tzoffset (L)), [-4, -4, -5, -5]); %!test # a POSIX instant names one of the pair, and says which %! z = 'America/New_York'; %! p = datetime (1730611800, 'ConvertFrom', 'posixtime', 'TimeZone', z); %! q = datetime (1730615400, 'ConvertFrom', 'posixtime', 'TimeZone', z); %! assert_equal (posixtime ([p, q]), [1730611800, 1730615400]); %! assert_equal (hours (tzoffset ([p, q])), [-4, -5]); %! assert_equal (isdst ([p, q]), [true, false]); ## Test the same on a zone whose transition is half an hour, which is why the ## offset is stored in seconds rather than as a one-hour fold flag. Australia ## /Lord_Howe puts its clock back 30 minutes on 7 April 2024; measured against ## MATLAB R2024a. %!test %! g1 = datetime (2024, 4, 6, 14, 45, 0, 'TimeZone', 'UTC'); %! g1.TimeZone = 'Australia/Lord_Howe'; %! g2 = datetime (2024, 4, 6, 15, 15, 0, 'TimeZone', 'UTC'); %! g2.TimeZone = 'Australia/Lord_Howe'; %! assert_equal (posixtime ([g1, g2]), [1712414700, 1712416500]); %! assert_equal (hours (tzoffset ([g1, g2])), [11, 10.5]); %! assert_equal (cellstr ([g1, g2]), ... %! {'07-Apr-2024 01:45:00', '07-Apr-2024 01:45:00'}); %! assert_equal (g1 == g2, false); %! assert_equal (numel (unique ([g1, g2])), 2); ## Test the compiled regime cache never answers for a neighbouring regime. ## The tz database is consulted once per regime rather than once per element, ## so an array crossing a transition is where a stale answer would show: the ## same clocks are therefore read in one call, in the reverse order, and one at ## a time, and all three must agree. Australia/Lord_Howe shifts by half an ## hour and Europe/Dublin's entries count winter as the saving period, so both ## the size of the step and the sense of the flag are covered. %!function check_regime (z) %! mi = 0:239; %! t = datetime (2024, 4, 7, 0, mi, 0, 'TimeZone', z); %! r = datetime (2024, 4, 7, 0, fliplr (mi), 0, 'TimeZone', z); %! assert_equal (posixtime (t), fliplr (posixtime (r))); %! assert_equal (hours (tzoffset (t)), fliplr (hours (tzoffset (r)))); %! assert_equal (isdst (t), fliplr (isdst (r))); %! ## The same clocks one at a time, every tenth minute, built singly so that %! ## no call sees more than one of them. %! ix = 1:10:numel (mi); %! c = arrayfun (@(m) datetime (2024, 4, 7, 0, m, 0, 'TimeZone', z), ... %! mi(ix), 'UniformOutput', false); %! one = [c{:}]; %! assert_equal (posixtime (one), posixtime (t(ix))); %! assert_equal (hours (tzoffset (one)), hours (tzoffset (t(ix)))); %! assert_equal (isdst (one), isdst (t(ix))); %!endfunction ## A half-hour shift %!test %! check_regime ('Australia/Lord_Howe'); ## Winter counted as the saving period %!test %! check_regime ('Europe/Dublin'); ## The ordinary case %!test %! check_regime ('America/New_York'); ## Test a wall clock the spring-forward gap skips still moves ahead by the gap %!test %! z = 'America/New_York'; %! assert_equal (posixtime (datetime (2024, 3, 10, 2, 30, 0, 'TimeZone', ... %! z)), ... %! 1710055800); %! assert_equal (hours (tzoffset (datetime (2024, 3, 10, 2, 30, 0, ... %! 'TimeZone', z))), -4); ################################################################################ ## ** Arithmetic Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'minus' ## ## ## ################################################################################ ## Test that a fixed-length shift and a difference keep the microsecond this ## class stores, at any year. Both went through a count of seconds from 1970, ## which is one double: past about year 3000 it no longer resolves a ## microsecond, so 't + milliseconds (0.001)' did not move the value and two ## clocks a microsecond apart subtracted to zero. The 1e9 rows are R2026a's. %!assert_equal ((datetime (3000, 6, 15, 10, 30, 45) + milliseconds (0.001)) ... %! - milliseconds (0.001), datetime (3000, 6, 15, 10, 30, 45)) %!assert_equal ((datetime (1e6, 6, 15, 10, 30, 45) + milliseconds (0.001)) ... %! - milliseconds (0.001), datetime (1e6, 6, 15, 10, 30, 45)) %!assert_equal ((datetime (1e9, 6, 15, 10, 30, 45) + milliseconds (0.001)) ... %! - milliseconds (0.001), datetime (1e9, 6, 15, 10, 30, 45)) %!assert_equal ((datetime (1e9, 6, 15, 10, 30, 45) + seconds (1)) ... %! - datetime (1e9, 6, 15, 10, 30, 45), seconds (1)) %!assert_equal ((datetime (1e9, 6, 15, 10, 30, 45) + hours (5)) - hours (5), ... %! datetime (1e9, 6, 15, 10, 30, 45)) %!assert_equal ((datetime (1e6, 6, 15, 10, 30, 45) + milliseconds (0.001)) ... %! > datetime (1e6, 6, 15, 10, 30, 45), true) %!assert_equal (datetime (1e6, 6, 15, 10, 30, 45.000001) ... %! > datetime (1e6, 6, 15, 10, 30, 45), true) %!assert_equal ((datetime (3000, 1, 1) + milliseconds (0.001)) ... %! > datetime (3000, 1, 1), true) ## Test that a non-finite shift is still carried: the day term takes it whole, ## the remainder of an infinite shift being Inf - Inf. %!assert_equal (isinf (datetime (2026, 1, 1) + days (Inf)), true) %!assert_equal (isnat (datetime (2026, 1, 1) + days (Inf)), false) %!assert_equal (isinf (datetime (2026, 1, 1) - days (Inf)), true) %!assert_equal (isnat (datetime (2026, 1, 1) + days (NaN)), true) %!assert_equal (seconds ((datetime (2026, 1, 1) + days (Inf)) ... %! - datetime (2026, 1, 1)), Inf) %!assert_equal (seconds (datetime (2026, 1, 1) ... %! - (datetime (2026, 1, 1) + days (Inf))), -Inf) ## Test that a zone still governs a fixed-length shift, which is applied to the ## instant and not to the wall clock: the component route above is taken only ## where there are no transitions to cross. %!assert_equal (char (datetime (2024, 3, 10, 1, 30, 0, ... %! 'TimeZone', 'America/New_York') + hours (1), 'HH:mm'), '03:30') %!assert_equal (hours (tzoffset (datetime (2024, 11, 3, 0, 30, 0, ... %! 'TimeZone', 'America/New_York') + hours (1))), -4) %!assert_equal (hours (tzoffset (datetime (2024, 11, 3, 0, 30, 0, ... %! 'TimeZone', 'America/New_York') + hours (2))), -5) %!assert_equal (char (datetime (2016, 12, 31, 23, 59, 59, ... %! 'TimeZone', 'UTCLeapSeconds') + seconds (1), 'HH:mm:ss'), ... %! '23:59:60') %!assert_equal (char (datetime (2017, 1, 1, 0, 0, 0, ... %! 'TimeZone', 'UTCLeapSeconds') - datetime (2016, 12, 31, ... %! 23, 59, 59, 'TimeZone', 'UTCLeapSeconds')), '00:00:02') ## Test 'minus' -- datetime minus datetime returns a duration %!assert_equal (class (datetime (2021, 1, 2) - datetime (2021, 1, ... %! 1)), 'duration') %!assert_equal (seconds (datetime (2021, 1, 2) - datetime (2021, 1, 1)), 86400) %!assert_equal (seconds (datetime (2021, 1, 2, 6, 30, 15) ... %! - datetime (2021, 1, 1, 0, 0, 0)), 109815) %!assert_equal (days (datetime (2021, 1, 2) - datetime (2021, 1, 1)), 1) %!assert_equal (seconds (datetime (2021, 1, 1) - datetime (2021, 1, 2)), -86400) ## Test 'minus' -- datetime difference is element-wise and size compatible %!assert_equal (seconds (datetime (2021, 1, [2, 3, 4]) - datetime (2021, 1, ... %! 1)), ... %! [86400, 172800, 259200]) %!assert_equal (size (datetime (2021, 1, [1, 2; 3, 4]) - datetime (2021, 1, ... %! 1)), ... %! [2, 2]) ## Test 'minus' -- zoned difference uses absolute instants across zones %!assert_equal (hours (datetime (2021, 1, 1, 'TimeZone', 'Europe/London') ... %! - datetime (2021, 1, 1, 'TimeZone', ... %! 'America/New_York')), -5) %!assert_equal (seconds (datetime (2021, 7, 1, 12, 0, 0, 'TimeZone', 'UTC') ... %! - datetime (2021, 7, 1, 12, 0, 0, 'TimeZone', ... %! 'UTC')), 0) ## Test 'minus' -- mixing a zoned and an unzoned datetime errors %!error ... %! datetime (2021, 1, 1, 'TimeZone', 'UTC') - datetime (2021, 1, 1); %!error ... %! datetime (2021, 1, 1) - datetime (2021, 1, 1, 'TimeZone', 'UTC'); ## Test 'minus' -- datetime minus duration returns a shifted datetime %!assert_equal (class (datetime (2021, 1, 1) - hours (30)), 'datetime') %!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) - hours (30)), ... %! [2020, 12, 30, 18, 0, 0]) %!assert_equal (datevec (datetime (2021, 1, 1) - days (1)), [2020, 12, 31, ... %! 0, 0, 0]) %!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) - minutes (90)), ... %! [2020, 12, 31, 22, 30, 0]) ## Test 'minus' -- duration subtraction on a zoned array is instant-based (DST) %!assert_equal (datevec (datetime (2021, 3, 14, 3, 0, 0, ... %! 'TimeZone', 'America/New_York') - hours (2)), ... %! [2021, 3, 14, 0, 0, 0]) ## Test 'minus' -- numeric and logical operands are a number of 24-hour days %!assert_equal (class (datetime (2021, 1, 1) - 5), 'datetime') %!assert_equal (datevec (datetime (2021, 1, 1) - 5), [2020, 12, 27, 0, 0, 0]) %!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) - 1.5), ... %! [2020, 12, 30, 12, 0, 0]) %!assert_equal (datevec (datetime (2021, 1, 1) - true), [2020, 12, 31, 0, 0, 0]) %!assert_equal (datevec (datetime (2021, 1, 1) - [1, 2, 3]), ... %! [2020, 12, 31, 0, 0, 0; 2020, 12, 30, 0, 0, 0; 2020, 12, 29, ... %! 0, 0, 0]) ## Test 'minus' -- calendarDuration subtracts whole months with day clamping %!assert_equal (class (datetime (2021, 1, 1) - calmonths (1)), 'datetime') %!assert_equal (datevec (datetime (2021, 1, 1) - calmonths (1)), [2020, 12, ... %! 1, 0, 0, 0]) %!assert_equal (datevec (datetime (2021, 3, 31) - calmonths (1)), [2021, 2, ... %! 28, 0, 0, 0]) %!assert_equal (datevec (datetime (2021, 3, 30) - calmonths (1)), [2021, 2, ... %! 28, 0, 0, 0]) %!assert_equal (datevec (datetime (2020, 3, 31) - calmonths (13)), [2019, 2, ... %! 28, 0, 0, 0]) %!assert_equal (datevec (datetime (2020, 2, 29) - calyears (1)), [2019, 2, ... %! 28, 0, 0, 0]) ## Test 'minus' -- calendarDuration applies months, then days, then time %!assert_equal (datevec (datetime (2021, 1, 1) ... %! - calendarDuration (0, 1, 15, 0, 0, ... %! 0)), [2020, 11, 16, 0, 0, 0]) %!assert_equal (datevec (datetime (2021, 1, ... %! 31) - calmonths (1) - caldays (1)), ... %! [2020, 12, 30, 0, 0, 0]) %!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) ... %! - calendarDuration (0, 0, 0, 6, 0, ... %! 0)), [2020, 12, 31, 18, 0, 0]) ## Test 'minus' -- Not-A-Time and infinite elements propagate %!assert_equal (isnan (seconds (NaT - datetime (2021, 1, 1))), true) %!assert_equal (isnat (NaT - hours (1)), true) %!assert_equal (isnat (datetime (2021, 1, 1) - days (NaN)), true) %!assert_equal (isinf (year (datetime (2021, 1, 1) - days (Inf))), true) %!assert_equal (sign (year (datetime (2021, 1, 1) - days (Inf))), -1) %!assert_equal (sign (year (datetime (2021, 1, 1) - days (-Inf))), 1) ## Test 'minus' -- empty arrays return empty %!assert_equal (isempty (datetime (2021, 1, []) - datetime (2021, 1, [])), true) %!assert_equal (isempty (datetime (2021, 1, []) - 1), true) ## Test 'minus' -- integer, char, string, and cell operands error %!error ... %! datetime (2021, 1, 1) - int8 (5); %!error ... %! datetime (2021, 1, 1) - 'A'; %!error ... %! datetime (2021, 1, 1) - string ("A"); %!error ... %! datetime (2021, 1, 1) - {1}; ## Test 'minus' -- subtracting a datetime from a non-datetime left operand ## errors %!error ... %! 5 - datetime (2021, 1, 1); %!error ... %! int8 (5) - datetime (2021, 1, 1); %!error ... %! true - datetime (2021, 1, 1); %!error ... %! 'A' - datetime (2021, 1, 1); ## Test arithmetic -- fixed vs calendar shifts across a DST spring-forward %!assert_equal (char (datetime (2024, 3, 10, 1, 30, 0, 'TimeZone', ... %! 'America/New_York') + hours (1), 'yyyy-MM-dd HH:mm:ss'), ... %! '2024-03-10 03:30:00') %!assert_equal (char (datetime (2024, 3, 9, 12, 0, 0, 'TimeZone', ... %! 'America/New_York') + days (1), 'yyyy-MM-dd HH:mm:ss'), ... %! '2024-03-10 13:00:00') %!assert_equal (char (datetime (2024, 3, 9, 12, 0, 0, 'TimeZone', ... %! 'America/New_York') + caldays (1), 'yyyy-MM-dd HH:mm:ss'), ... %! '2024-03-10 12:00:00') ## Test arithmetic -- elapsed time spans 23 h / 25 h on DST transition days %!assert_equal (hours (datetime (2024, 3, 11, 'TimeZone', ... %! 'America/New_York') ... %! - datetime (2024, 3, 10, 'TimeZone', 'America/New_York')), 23) %!assert_equal (hours (datetime (2024, 11, 4, 'TimeZone', ... %! 'America/New_York') ... %! - datetime (2024, 11, 3, 'TimeZone', 'America/New_York')), 25) ## Test arithmetic -- calendar month/year addition clamps to month end %!assert_equal (char (datetime (2024, 1, 31) + calmonths (1), 'yyyy-MM-dd'), ... %! '2024-02-29') %!assert_equal (char (datetime (2023, 1, 31) + calmonths (1), 'yyyy-MM-dd'), ... %! '2023-02-28') %!assert_equal (char (datetime (2024, 2, 29) + calyears (1), 'yyyy-MM-dd'), ... %! '2025-02-28') ## Test 'diff' -- successive differences return a duration in 'hh:mm:ss' %!test %! d = diff (datetime (2026, 1, [1 2 4 8])); %! assert_equal (class (d), 'duration'); %! assert_equal (hours (d), [24 48 96]); %! assert_equal (d.Format, 'hh:mm:ss'); ## Test 'diff' -- N-th order difference is still a duration %!assert_equal (hours (diff (datetime (2026, 1, [1 2 4 8]), 2)), [24 48]) %!assert_equal (hours (diff (datetime (2026, 1, [1 2 4 8]), 3)), 24) %!assert_equal (class (diff (datetime (2026, 1, [1 2 4 8]), 2)), 'duration') ## Test 'diff' -- default operates along dim 1, DIM selects another %!assert_equal (hours (diff (datetime (2026, 1, [1 2; 4 8]))), [72 144]) %!assert_equal (hours (diff (datetime (2026, 1, [1 2; 4 8]), 1, 2)), [24; 96]) ## Test 'diff' -- a scalar yields a 0-by-0 empty duration %!assert_equal (size (diff (datetime (2026, 1, 1))), [0 0]) %!assert_equal (class (diff (datetime (2026, 1, 1))), 'duration') ## Test 'diff' -- Not-A-Time propagates as a NaN duration %!assert_equal (isnan (hours (diff ([datetime(2026,1,1), NaT, datetime(2026, ... %! 1,5)]))), ... %! [true true]) ## Test 'diff' -- differencing along a trailing singleton dimension is empty %!assert_equal (size (diff (datetime (2026, 1, [1 2 4 8]), 1, 3)), [1 4 0]) ## Test 'diff' -- zoned differences are daylight-saving aware (spring forward) %!test %! zs = datetime (2026, 3, 28, 12, 0, 0, 'TimeZone', 'Europe/Athens'); %! ze = datetime (2026, 3, 30, 12, 0, 0, 'TimeZone', 'Europe/Athens'); %! assert_equal (hours (diff ([zs ze])), 47); ## Test 'diff' -- N must be a positive integer scalar %!error ... %! diff (datetime (2026, 1, [1 2 3]), -1); %!error ... %! diff (datetime (2026, 1, [1 2 3]), 0); %!error ... %! diff (datetime (2026, 1, [1 2 3]), 1.5); %!error ... %! diff (datetime (2026, 1, [1 2 3]), 1, 0); %!error ... %! diff (datetime (2026, 1, [1 2 3]), 1, 2, 3); ## Test 'caldiff' -- consecutive calendar differences as a calendarDuration %!test %! d = caldiff ([datetime(2026,1,31) datetime(2026,3,31) datetime(2027,5,1)]); %! assert_equal (class (d), 'calendarDuration'); %! assert_equal (dispstrings (d), {'2mo', '1y 1mo 1d'}); ## Test 'caldiff' -- a single requested component expresses the whole difference %!test %! tt = [datetime(2026,1,31) datetime(2026,3,31) datetime(2027,5,1)]; %! assert_equal (dispstrings (caldiff (tt, 'Time')), {'1416h 0m 0s', ... %! '9504h 0m 0s'}); %! assert_equal (dispstrings (caldiff (tt, 'Day')), {'59d', '396d'}); %! assert_equal (dispstrings (caldiff (tt, 'Month')), {'2mo', '13mo'}); %! assert_equal (dispstrings (caldiff (tt, 'q')), {'0q', '4q'}); %! assert_equal (calmonths (caldiff (tt, 'Year')), [0 12]); ## Test 'caldiff' -- an empty COMPONENTS string selects the default set %!assert_equal (dispstrings (caldiff ([datetime(2026,1,31) datetime(2027,5, ... %! 1)], '')), ... %! {'1y 3mo 1d'}) ## Test 'caldiff' -- whole months honour month-end day clamping %!assert_equal (dispstrings (caldiff ([datetime(2026,1,31) datetime(2026,2, ... %! 28)])), {'1mo'}) %!assert_equal (dispstrings (caldiff ([datetime(2026,1,31), datetime(2026,2, ... %! 28), ... %! datetime(2026,3, ... %! 31)])), {'1mo', '1mo 3d'}) ## Test 'caldiff' -- a time-of-day remainder appears as the Time component %!assert_equal (dispstrings (caldiff ([datetime(2026,1,1,6,0,0), ... %! datetime(2026,3,15,18,30, ... %! 0)])), {'2mo 14d 12h 30m 0s'}) %!assert_equal (dispstrings (caldiff ([datetime(2026,1,1,0,0,0.25), ... %! datetime(2026,1,1,0,0, ... %! 1.75)])), {'0h 0m 1.5s'}) ## Test 'caldiff' -- a scalar yields a 0-by-1 empty, Not-A-Time yields NaN %!assert_equal (size (caldiff (datetime (2026, 1, 1))), [0 1]) %!assert_equal (class (caldiff (datetime (2026, 1, 1))), 'calendarDuration') %!assert_equal (dispstrings (caldiff ([datetime(2026,1,1), NaT, ... %! datetime(2026,5,5)])), ... %! {'NaN', 'NaN'}) ## Test 'caldiff' -- a matrix differences along dimension 1 by default %!assert_equal (dispstrings (caldiff ([datetime(2026,1,31) datetime(2026,3, ... %! 31); ... %! datetime(2026,5,1) datetime(2027,1,1)])), {'3mo 1d', '9mo 1d'}) ## Test 'caldiff' -- an invalid COMPONENTS is rejected %!error ... %! caldiff ([datetime(2026,1,1) datetime(2026,2,1)], 'ym'); ## Test 'between' -- signed calendar difference from A to B %!test %! d = between (datetime (2026, 1, 31), datetime (2026, 3, 30)); %! assert_equal (class (d), 'calendarDuration'); %! assert_equal (dispstrings (d), {'1mo 30d'}); %!assert_equal (dispstrings (between (datetime (2026, 3, 30), ... %! datetime (2026, 1, 31))), ... %! {'-1mo -28d'}) %!assert_equal (dispstrings (between (datetime (2026, 1, 31), ... %! datetime (2026, 1, 31))), {'0d'}) ## Test 'between' -- month-end clamping and leap-day cases %!assert_equal (dispstrings (between (datetime (2026, 1, 31), ... %! datetime (2026, 2, 28))), {'1mo'}) %!assert_equal (dispstrings (between (datetime (2026, 3, 31), ... %! datetime (2026, 2, 28))), {'-1mo'}) %!assert_equal (dispstrings (between (datetime (2026, 1, 31), ... %! datetime (2026, 3, 1))), {'1mo 1d'}) %!assert_equal (dispstrings (between (datetime (2024, 2, 29), ... %! datetime (2025, 2, 28))), {'1y'}) ## Test 'between' -- the whole-unit borrow is gated by the time of day %!assert_equal (dispstrings (between (datetime (2026, 1, 31, 10, 0, 0), ... %! datetime (2026, 2, 28, 8, 0, ... %! 0))), {'27d 22h 0m 0s'}) %!assert_equal (dispstrings (between (datetime (2024, 2, 29, 6, 0, 0), ... %! datetime (2027, 5, 10, 20, 30, ... %! 15))), {'3y 2mo 12d 14h 30m 15s'}) ## Test 'between' -- broadcasting a scalar against a vector %!assert_equal (dispstrings (between ([datetime(2026,1,1) datetime(2026,6, ... %! 1)], ... %! datetime (2026, 3, ... %! 15))), {'2mo 14d', '-2mo -17d'}) ## Test 'between' -- Not-A-Time yields NaN %!assert_equal (dispstrings (between (datetime (2026, 1, 1), ... %! [datetime(2026,3, ... %! 15) NaT])), {'2mo 14d', 'NaN'}) ## Test 'between' -- calendar differences ignore daylight saving; Time does not %!test %! zs = datetime (2026, 3, 28, 12, 0, 0, 'TimeZone', 'Europe/Athens'); %! ze = datetime (2026, 3, 30, 12, 0, 0, 'TimeZone', 'Europe/Athens'); %! assert_equal (dispstrings (between (zs, ze)), {'2d'}); %! assert_equal (dispstrings (between (zs, ze, 'Time')), {'47h 0m 0s'}); ## Test 'between' -- invalid operands and options are rejected %!error ... %! between (datetime (2026, 1, [1 2 3]), datetime (2026, 1, [1 2])); %!error ... %! between (datetime (2026, 1, 1), datetime (2026, 1, 1, 'TimeZone', 'UTC')); %!error ... %! between (datetime (2026, 1, 31), days (1)); %!error ... %! between (datetime (2026, 1, 31), datetime (2026, 3, 30), 'bogus'); ## Test 'mean' -- average of the instants as a datetime %!test %! m = mean (datetime (2026, 1, [1 2 4 8])); %! assert_equal (class (m), 'datetime'); %! assert_equal (dispstrings (m), {'03-Jan-2026 18:00:00'}); %!assert_equal (dispstrings (mean (datetime (2026, 1, ... %! [1 2]))), {'01-Jan-2026 12:00:00'}) %!assert_equal (dispstrings (mean (datetime (2026, 1, 5))), {'05-Jan-2026'}) ## Test 'mean' -- Not-A-Time is kept by default and dropped with 'omitnat' %!test %! rn = [datetime(2026,1,1) NaT datetime(2026,1,5)]; %! assert_equal (isnat (mean (rn)), true); %! assert_equal (dispstrings (mean (rn, 'omitnat')), {'03-Jan-2026'}); %! assert_equal (dispstrings (mean (rn, 'omitmissing')), {'03-Jan-2026'}); %! assert_equal (isnat (mean (rn, 'includenat')), true); ## Test 'mean' -- dimension arguments and an empty array %!test %! rm = datetime (2026, 1, [1 2; 4 8]); %! assert_equal (dispstrings (mean (rm)), ... %! {'02-Jan-2026 12:00:00', '05-Jan-2026 00:00:00'}); %! assert_equal (dispstrings (mean (rm, 2)), ... %! {'01-Jan-2026 12:00:00'; '06-Jan-2026 00:00:00'}); %! assert_equal (dispstrings (mean (rm, 'all')), {'03-Jan-2026 18:00:00'}); %!assert_equal (isnat (mean (datetime (2026, 1, []))), true) ## Test 'mean' -- the Format and TimeZone are inherited; zoned mean is DST-aware %!test %! rf = datetime (2026, 1, [1 2 4 8]); %! rf.Format = 'dd/MM/uuuu HH:mm'; %! assert_equal (mean (rf).Format, 'dd/MM/uuuu HH:mm'); %!test %! z = mean (datetime (2026, 1, [1 2 4], 'TimeZone', 'Europe/Athens')); %! assert_equal (z.TimeZone, 'Europe/Athens'); %! assert_equal (dispstrings (z), {'02-Jan-2026 08:00:00'}); ## Test 'mean' -- infinite instants propagate %!assert_equal (isinf (mean ([datetime(2026,1,1), datetime(2026,1, ... %! 1) + days(Inf)])), true) %!assert_equal (isnat (mean ([datetime(2026,1,1) + days(Inf), ... %! datetime(2026,1,1) - days(Inf)])), true) ## Test 'median' -- middle instant, averaging the two middle for an even count %!assert_equal (dispstrings (median (datetime (2026, 1, ... %! [1 2 4 8]))), {'03-Jan-2026'}) %!assert_equal (dispstrings (median (datetime (2026, 1, [1 2 3 4]))), ... %! {'02-Jan-2026 12:00:00'}) %!assert_equal (dispstrings (median (datetime (2026, 1, ... %! [1 2 3]))), {'02-Jan-2026'}) %!assert_equal (class (median (datetime (2026, 1, [1 2 3]))), 'datetime') %!test %! rn = [datetime(2026,1,1) NaT datetime(2026,1,5)]; %! assert_equal (isnat (median (rn)), true); %! assert_equal (dispstrings (median (rn, 'omitnat')), {'03-Jan-2026'}); ## Test 'mode' -- smallest most-frequent value, ignoring Not-A-Time %!assert_equal (dispstrings (mode (datetime (2026, 1, ... %! [1 2 4 8]))), {'01-Jan-2026'}) %!assert_equal (dispstrings (mode ([datetime(2026,1,1) NaT datetime(2026,1, ... %! 5)])), ... %! {'01-Jan-2026'}) %!assert_equal (class (mode (datetime (2026, 1, [1 2 2]))), 'datetime') ## Test 'mode' -- the frequency and the list of all modal values %!test %! [m, f, c] = mode (datetime (2026, 1, [1 1 2 2 3])); %! assert_equal (dispstrings (m), {'01-Jan-2026'}); %! assert_equal (f, 2); %! assert_equal (dispstrings (c{1}), {'01-Jan-2026'; '02-Jan-2026'}); ## Test 'std' -- standard deviation of the instants as a duration %!test %! s = std (datetime (2026, 1, [1 2 4 8])); %! assert_equal (class (s), 'duration'); %! assert_equal (dispstrings (s), {'74:17:48'}); ## Test 'std' -- the weight selects N-1 (default) or N normalisation %!assert_equal (dispstrings (std (datetime (2026, 1, [1 2 4 8]), ... %! 1)), {'64:20:34'}) %!assert_equal (dispstrings (std (datetime (2026, 1, [1 2; 4 8]), 0, 2)), ... %! {'16:58:14'; '67:52:56'}) ## Test 'std' -- 'omitnat' skips missing values and the mean is a second output %!assert_equal (dispstrings (std ([datetime(2026,1,1) NaT datetime(2026,1, ... %! 5)], ... %! 'omitnat')), {'67:52:56'}) %!test %! [s, m] = std (datetime (2026, 1, [1 2 4 8])); %! assert_equal (class (s), 'duration'); %! assert_equal (class (m), 'datetime'); %! assert_equal (dispstrings (m), {'03-Jan-2026 18:00:00'}); ## Test 'mean'/'median'/'mode' -- omit-flags, and mean along a chosen dimension %!assert_equal (isnat (mean ([datetime(2024, 3, [1 9 20]), NaT])), true) %!assert_equal (char (mean ([datetime(2024, 3, [1 9 20]), NaT], 'omitnat'), ... %! 'yyyy-MM-dd'), '2024-03-10') %!assert_equal (char (mean ([datetime(2024, 3, [1 9 20]), NaT], ... %! 'omitmissing'), ... %! 'yyyy-MM-dd'), '2024-03-10') %!assert_equal (char (median (datetime (2024, 3, [1 9 20])), 'yyyy-MM-dd'), ... %! '2024-03-09') %!assert_equal (char (mode (datetime (2024, 1, [1 1 2 3])), 'yyyy-MM-dd'), ... %! '2024-01-01') %!assert_equal (cellstr (mean (datetime (2024, 1, [10 20; 30 40]))), ... %! {'20-Jan-2024', '30-Jan-2024'}) %!assert_equal (cellstr (mean (datetime (2024, 1, [10 20; 30 40]), 2)), ... %! {'15-Jan-2024'; '04-Feb-2024'}) ## Test 'dateshift' -- start of each calendar unit (a week starts on Sunday) %!test %! sh = datetime (2026, 7, 17, 13, 45, 30); %! assert_equal (class (dateshift (sh, 'start', 'year')), 'datetime'); %! assert_equal (dispstrings (dateshift (sh, 'start', 'year')), ... %! {'01-Jan-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'start', 'quarter')), ... %! {'01-Jul-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'start', 'month')), ... %! {'01-Jul-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'start', 'week')), ... %! {'12-Jul-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'start', 'day')), {'17-Jul-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'start', 'hour')), ... %! {'17-Jul-2026 13:00:00'}); %! assert_equal (dispstrings (dateshift (sh, 'start', 'minute')), ... %! {'17-Jul-2026 13:45:00'}); %! assert_equal (dispstrings (dateshift (sh, 'start', 'second')), ... %! {'17-Jul-2026 13:45:30'}); ## Test 'dateshift' -- end of each unit (next boundary below a day, last day ## above) %!test %! sh = datetime (2026, 7, 17, 13, 45, 30); %! assert_equal (dispstrings (dateshift (sh, 'end', 'year')), {'31-Dec-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'end', 'quarter')), ... %! {'30-Sep-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'end', 'month')), {'31-Jul-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'end', 'week')), {'18-Jul-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'end', 'day')), {'18-Jul-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'end', 'hour')), ... %! {'17-Jul-2026 14:00:00'}); %! assert_equal (dispstrings (dateshift (sh, 'end', 'minute')), ... %! {'17-Jul-2026 13:46:00'}); %! assert_equal (dispstrings (dateshift (sh, 'end', 'second')), ... %! {'17-Jul-2026 13:45:31'}); ## Test 'dateshift' -- end of a day is the following midnight %!assert_equal (dispstrings (dateshift (datetime (2026, 1, 1), 'end', ... %! 'day')), {'02-Jan-2026'}) %!assert_equal (dispstrings (dateshift (datetime (2026, 1, 1), 'end', ... %! 'year')), {'31-Dec-2026'}) ## Test 'dateshift' -- a rule shifts by whole units before truncating %!test %! sh = datetime (2026, 7, 17, 13, 45, 30); %! assert_equal (dispstrings (dateshift (sh, 'start', 'week', 'next')), ... %! {'19-Jul-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'start', 'week', 'previous')), ... %! {'05-Jul-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'start', 'month', 'next')), ... %! {'01-Aug-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'end', 'month', 'next')), ... %! {'31-Aug-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'start', 'day', 2)), ... %! {'19-Jul-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'start', 'day', -3)), ... %! {'14-Jul-2026'}); %! assert_equal (dispstrings (dateshift (sh, 'start', 'month', 0)), ... %! {'01-Jul-2026'}); ## Test 'dateshift' -- 'dayofweek' moves to a weekday, keeping the time of day %!test %! sh = datetime (2026, 7, 17, 13, 45, 30); %! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 1)), ... %! {'19-Jul-2026 13:45:30'}); %! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 2)), ... %! {'20-Jul-2026 13:45:30'}); %! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 'Monday')), ... %! {'20-Jul-2026 13:45:30'}); %! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 2, 'previous')), ... %! {'13-Jul-2026 13:45:30'}); %! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 2, 'nearest')), ... %! {'20-Jul-2026 13:45:30'}); %! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 6, 'current')), ... %! {'17-Jul-2026 13:45:30'}); %! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 6, 2)), ... %! {'24-Jul-2026 13:45:30'}); %! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 6, -1)), ... %! {'17-Jul-2026 13:45:30'}); ## Test 'dateshift' -- across a week vector, and matrix / empty shapes %!test %! ww = datetime (2026, 7, [13 14 15 16 17 18 19]); %! assert_equal (day (dateshift (ww, 'start', 'week')), [12 12 12 12 12 12 19]); %! assert_equal (day (dateshift (ww, 'end', 'week')), [18 18 18 18 18 18 25]); %!assert_equal (day (dateshift (datetime (2026, 1, [15 20; 25 30]), 'start', ... %! 'month')), ... %! [1 1; 1 1]) %!assert_equal (size (dateshift (datetime (2026, 1, []), 'start', ... %! 'month')), [0 0]) ## Test 'dateshift' -- Not-A-Time and infinite elements pass through %!assert_equal (isnat (dateshift (NaT, 'start', 'month')), true) %!assert_equal (isinf (dateshift (datetime (2026, 1, 1) + days (Inf), ... %! 'start', 'month')), true) %!test %! v = dateshift ([datetime(2026,1,15) NaT], 'start', 'month'); %! assert_equal (day (v(1)), 1); %! assert_equal (isnat (v(2)), true); ## Test 'dateshift' -- month-end respects leap years, and zoned start keeps the ## zone %!assert_equal (dispstrings (dateshift (datetime (2024, 2, 10), 'end', ... %! 'month')), {'29-Feb-2024'}) %!assert_equal (dispstrings (dateshift (datetime (2026, 2, 10), 'end', ... %! 'month')), {'28-Feb-2026'}) %!test %! z = dateshift (datetime (2026, 3, 29, 12, 0, 0, 'TimeZone', ... %! 'Europe/Athens'), ... %! 'start', 'day'); %! assert_equal (z.TimeZone, 'Europe/Athens'); %! assert_equal (dispstrings (z), {'29-Mar-2026'}); ## Test 'dateshift' -- invalid arguments are rejected %!error ... %! dateshift (datetime (2026, 1, 1), 'bogus', 'month'); %!error ... %! dateshift (datetime (2026, 1, 1), 'start', 'bogus'); %!error ... %! dateshift (datetime (2026, 1, 1), 'start'); %!error ... %! dateshift (datetime (2026, 1, 1), 'dayofweek', 8); %!error ... %! dateshift (datetime (2026, 1, 1), 'dayofweek', 'Funday'); %!error ... %! dateshift (datetime (2026, 1, 1), 'start', 'month', 'bogus'); %!error ... %! dateshift (datetime (2026, 1, 1), 'start', 'week', 1.5); %!error ... %! dateshift (datetime (2026, 1, 1), 'end', 'dayofweek', 2); ## Test 'dateshift' -- start of every calendar/time unit %!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ... %! 'start', 'year'), 'yyyy-MM-dd HH:mm:ss'), '2024-01-01 00:00:00') %!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ... %! 'start', 'quarter'), ... %! 'yyyy-MM-dd HH:mm:ss'), '2024-01-01 00:00:00') %!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ... %! 'start', 'month'), ... %! 'yyyy-MM-dd HH:mm:ss'), '2024-03-01 00:00:00') %!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ... %! 'start', 'week'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-10 00:00:00') %!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ... %! 'start', 'hour'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-15 10:00:00') %!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ... %! 'start', 'minute'), ... %! 'yyyy-MM-dd HH:mm:ss'), '2024-03-15 10:30:00') ## Test 'dateshift' -- end of every calendar/time unit %!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ... %! 'end', 'year'), 'yyyy-MM-dd HH:mm:ss'), '2024-12-31 00:00:00') %!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), 'end', ... %! 'quarter'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-31 00:00:00') %!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ... %! 'end', 'month'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-31 00:00:00') %!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ... %! 'end', 'week'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-16 00:00:00') %!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ... %! 'end', 'day'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-16 00:00:00') %!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ... %! 'end', 'hour'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-15 11:00:00') ## Test 'between' -- month-end, negative, and leap-edge calendar differences %!assert_equal (cellstr (between (datetime (2024, 1, 31), ... %! datetime (2024, 3, 31))), {'2mo'}) %!assert_equal (cellstr (between (datetime (2024, 1, 31), ... %! datetime (2024, 2, 29))), {'1mo'}) %!assert_equal (cellstr (between (datetime (2025, 3, 20), ... %! datetime (2024, 1, 15))), {'-1y -2mo -5d'}) %!assert_equal (cellstr (between (datetime (2020, 2, 29), ... %! datetime (2021, 2, 28))), {'1y'}) %!assert_equal (cellstr (between (datetime (2024, 1, 15), ... %! datetime (2025, 3, 20), 'days')), {'430d'}) %!assert_equal (cellstr (between (datetime (2024, 1, 15), ... %! datetime (2025, 3, 20), 'months')), {'14mo'}) ## Test 'plus' -- datetime plus duration returns a shifted datetime %!assert_equal (class (datetime (2021, 1, 1) + hours (30)), 'datetime') %!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) + hours (30)), ... %! [2021, 1, 2, 6, 0, 0]) %!assert_equal (datevec (datetime (2021, 1, 1) + days (1)), [2021, 1, 2, 0, ... %! 0, 0]) %!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) + minutes (90)), ... %! [2021, 1, 1, 1, 30, 0]) ## Test 'plus' -- addition commutes with the datetime on either side %!assert_equal (datevec (hours (5) + datetime (2021, 1, 1, 0, 0, 0)), ... %! [2021, 1, 1, 5, 0, 0]) %!assert_equal (datevec (calmonths (1) + datetime (2021, 1, 1)), [2021, 2, ... %! 1, 0, 0, 0]) %!assert_equal (datevec (caldays (3) + datetime (2021, 1, 1)), [2021, 1, 4, ... %! 0, 0, 0]) %!assert_equal (datevec (5 + datetime (2021, 1, 1)), [2021, 1, 6, 0, 0, 0]) %!assert_equal (datevec (5 + datetime (2021, 1, ... %! 1)), datevec (datetime (2021, 1, 1) + 5)) ## Test 'plus' -- numeric and logical operands are a number of 24-hour days %!assert_equal (datevec (datetime (2021, 1, 1) + 5), [2021, 1, 6, 0, 0, 0]) %!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) + 1.5), [2021, 1, 2, ... %! 12, 0, 0]) %!assert_equal (datevec (datetime (2021, 1, 1) + true), [2021, 1, 2, 0, 0, 0]) %!assert_equal (datevec (datetime (2021, 1, 1) + [1, 2, 3]), ... %! [2021, 1, 2, 0, 0, 0; 2021, 1, 3, 0, 0, 0; 2021, 1, 4, 0, 0, 0]) ## Test 'plus' -- calendarDuration adds whole months with day clamping %!assert_equal (datevec (datetime (2021, 1, 1) + calmonths (1)), [2021, 2, ... %! 1, 0, 0, 0]) %!assert_equal (datevec (datetime (2021, 1, 1) + calmonths (13)), [2022, 2, ... %! 1, 0, 0, 0]) %!assert_equal (datevec (datetime (2021, 1, 31) + calmonths (1)), [2021, 2, ... %! 28, 0, 0, 0]) %!assert_equal (datevec (datetime (2021, 3, 31) + calmonths (1)), [2021, 4, ... %! 30, 0, 0, 0]) %!assert_equal (datevec (datetime (2020, 2, 29) + calyears (1)), [2021, 2, ... %! 28, 0, 0, 0]) %!assert_equal (datevec (datetime (2021, 1, 1) + caldays (40)), [2021, 2, ... %! 10, 0, 0, 0]) ## Test 'plus' -- calendarDuration applies months, then days, then time %!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) ... %! + calendarDuration (0, 1, 15, 6, 0, ... %! 0)), [2021, 2, 16, 6, 0, 0]) %!assert_equal (datevec (datetime (2021, 1, ... %! 31) + calmonths (1) + caldays (1)), ... %! [2021, 3, 1, 0, 0, 0]) ## Test 'plus' -- duration addition on a zoned array is instant-based (DST) %!assert_equal (datevec (datetime (2021, 3, 14, 0, 0, 0, ... %! 'TimeZone', 'America/New_York') + hours (2)), ... %! [2021, 3, 14, 3, 0, 0]) %!assert_equal (datevec (datetime (2021, 3, 14, 0, 0, 0, ... %! 'TimeZone', 'America/New_York') + caldays (1)), ... %! [2021, 3, 15, 0, 0, 0]) ## Test 'plus' -- adding two datetime arrays is not defined %!error ... %! datetime (2021, 1, 1) + datetime (2021, 1, 2); ## Test 'plus' -- Not-A-Time and infinite elements propagate %!assert_equal (isnat (NaT + hours (1)), true) %!assert_equal (isnat (datetime (2021, 1, 1) + days (NaN)), true) %!assert_equal (isnat (datetime (2021, 1, 1) + calmonths (NaN)), true) %!assert_equal (isinf (year (datetime (2021, 1, 1) + days (Inf))), true) %!assert_equal (sign (year (datetime (2021, 1, 1) + days (Inf))), 1) %!assert_equal (sign (year (datetime (2021, 1, 1) + days (-Inf))), -1) ## Test 'plus' -- empty arrays return empty %!assert_equal (isempty (datetime (2021, 1, []) + 1), true) %!assert_equal (isempty (1 + datetime (2021, 1, [])), true) ## Test 'plus' -- integer, char, string, and cell operands error %!error ... %! datetime (2021, 1, 1) + int8 (5); %!error ... %! datetime (2021, 1, 1) + 'A'; %!error ... %! datetime (2021, 1, 1) + string ("A"); %!error ... %! datetime (2021, 1, 1) + {1}; %!error ... %! int8 (5) + datetime (2021, 1, 1); ################################################################################ ## ** Relational Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'lt' 'le' 'gt' 'ge' ## ## 'eq' 'ne' ## ## ## ################################################################################ ## Test 'lt' -- earlier is less than later %!assert_equal (datetime (2021, 1, 1) < datetime (2021, 6, 1), true) %!assert_equal (datetime (2021, 6, 1) < datetime (2021, 1, 1), false) %!assert_equal (datetime (2021, 1, 1) < datetime (2021, 1, 1), false) %!assert_equal (datetime (2021, 1, 1, 0, 0, 0) < datetime (2021, 1, 1, 0, 0, ... %! 1), true) %!assert_equal (class (datetime (2021, 1, 1) < datetime (2021, 6, ... %! 1)), 'logical') ## Test 'le' -- earlier or equal %!assert_equal (datetime (2021, 1, 1) <= datetime (2021, 1, 1), true) %!assert_equal (datetime (2021, 1, 1) <= datetime (2021, 6, 1), true) %!assert_equal (datetime (2021, 6, 1) <= datetime (2021, 1, 1), false) ## Test 'gt' -- later is greater %!assert_equal (datetime (2021, 6, 1) > datetime (2021, 1, 1), true) %!assert_equal (datetime (2021, 1, 1) > datetime (2021, 6, 1), false) %!assert_equal (datetime (2021, 1, 1) > datetime (2021, 1, 1), false) ## Test 'ge' -- later or equal %!assert_equal (datetime (2021, 1, 1) >= datetime (2021, 1, 1), true) %!assert_equal (datetime (2021, 6, 1) >= datetime (2021, 1, 1), true) %!assert_equal (datetime (2021, 1, 1) >= datetime (2021, 6, 1), false) ## Test 'eq' and 'ne' -- equality of the same instant %!assert_equal (datetime (2021, 1, 1) == datetime (2021, 1, 1), true) %!assert_equal (datetime (2021, 1, 1) == datetime (2021, 6, 1), false) %!assert_equal (datetime (2021, 1, 1) != datetime (2021, 6, 1), true) %!assert_equal (datetime (2021, 1, 1) != datetime (2021, 1, 1), false) %!assert_equal (datetime (2021, 1, 1, 12, 0, 0) == datetime (2021, 1, 1, 12, ... %! 0, 0), true) ## Test relational -- element-wise over arrays with broadcasting %!assert_equal (datetime (2021, 1, [1, 2, 3]) < datetime (2021, 1, 2), ... %! [true, false, false]) %!assert_equal (datetime (2021, 1, [1, 2, 3]) == datetime (2021, 1, 2), ... %! [false, true, false]) %!assert_equal (datetime (2021, 1, [1, 2, 3]) >= datetime (2021, 1, 2), ... %! [false, true, true]) %!assert_equal (size (datetime (2021, 1, [1, 2; 3, 4]) < datetime (2021, 1, ... %! 3)), ... %! [2, 2]) ## Test relational -- Not-A-Time never compares less/greater/equal %!assert_equal (NaT < datetime (2021, 1, 1), false) %!assert_equal (NaT > datetime (2021, 1, 1), false) %!assert_equal (NaT <= datetime (2021, 1, 1), false) %!assert_equal (NaT >= datetime (2021, 1, 1), false) %!assert_equal (NaT == datetime (2021, 1, 1), false) %!assert_equal (NaT == NaT, false) %!assert_equal (NaT != NaT, true) %!assert_equal (NaT != datetime (2021, 1, 1), true) %!assert_equal ([datetime(2021,1,1), NaT] != NaT, [true, true]) ## Test relational -- zoned arrays compare by absolute instant across zones %!assert_equal (datetime (2021, 1, 1, 'TimeZone', 'America/New_York') ... %! == datetime (2021, 1, 1, 'TimeZone', 'Europe/London'), false) %!assert_equal (datetime (2021, 1, 1, 'TimeZone', 'America/New_York') ... %! > datetime (2021, 1, 1, 'TimeZone', 'Europe/London'), true) %!assert_equal (datetime (2021, 1, 1, 12, 0, 0, 'TimeZone', 'UTC') ... %! == datetime (2021, 1, 1, 7, 0, 0, 'TimeZone', ... %! 'America/New_York'), true) ## Test relational -- infinite datetimes order correctly %!assert_equal (datetime (2021, 1, 1) - days (-Inf) > datetime (2021, 6, ... %! 1), true) %!assert_equal (datetime (2021, 1, 1) - days (Inf) < datetime (2021, 1, ... %! 1), true) %!assert_equal (datetime (2021, 1, 1) - days (Inf) == datetime (2021, 1, ... %! 1) - days (Inf), true) ## Test relational -- explicit cross-zone instant comparison and NaT inequality %!assert_equal (datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', ... %! 'America/New_York') ... %! == datetime (2024, 1, 1, 17, 0, 0, 'TimeZone', 'UTC'), true) %!assert_equal (datetime (2024, 1, 1, 11, 0, 0, 'TimeZone', ... %! 'America/New_York') ... %! < datetime (2024, 1, 1, 17, 0, 0, 'TimeZone', 'UTC'), true) %!assert_equal (NaT != NaT, true) %!assert_equal (isbetween (datetime (2024, 1, 1, 14, 0, 0, 'TimeZone', ... %! 'America/New_York'), datetime (2024, 1, 1, 18, 0, 0, ... %! 'TimeZone', 'UTC'), datetime (2024, 1, 1, 20, 0, 0, ... %! 'TimeZone', 'UTC')), true) ## Test relational -- empty arrays return empty %!assert_equal (isempty (datetime (2021, 1, []) < datetime (2021, 1, [])), true) ## Test relational -- comparing a zoned and an unzoned datetime errors %!error ... %! datetime (2021, 1, 1, 'TimeZone', 'UTC') == datetime (2021, 1, 1); %!error ... %! datetime (2021, 1, 1, 'TimeZone', 'UTC') < datetime (2021, 1, 1); ## Test relational -- a non-datetime operand errors %!error ... %! datetime (2021, 1, 1) < 5; %!error ... %! 5 > datetime (2021, 1, 1); %!error ... %! datetime (2021, 1, 1) == 'x'; ## Test relational -- broadcasting holds for all six operators %!assert_equal (size (datetime (2021, 1, [1, 2, 3]) < datetime (2021, 1, ... %! [1; 2; 4])), [3, 3]) %!assert_equal (size (datetime (2021, 1, [1, 2, 3]) <= datetime (2021, 1, ... %! [1; 2; 4])), [3, 3]) %!assert_equal (size (datetime (2021, 1, [1, 2, 3]) > datetime (2021, 1, ... %! [1; 2; 4])), [3, 3]) %!assert_equal (size (datetime (2021, 1, [1, 2, 3]) >= datetime (2021, 1, ... %! [1; 2; 4])), [3, 3]) %!assert_equal (size (datetime (2021, 1, [1, 2, 3]) == datetime (2021, 1, ... %! [1; 2; 4])), [3, 3]) %!assert_equal (size (datetime (2021, 1, [1, 2, 3]) != datetime (2021, 1, ... %! [1; 2; 4])), [3, 3]) %!assert_equal (datetime (2021, 1, [1, 2, 3]) == datetime (2021, 1, ... %! [1; 2; 4]), ... %! logical ([1, 0, 0; 0, 1, 0; 0, 0, 0])) %!assert_equal (datetime (2021, 1, [1, 2, 3]) < datetime (2021, 1, ... %! [1; 2; 4]), ... %! logical ([0, 0, 0; 1, 0, 0; 1, 1, 1])) %!assert_equal (datetime (2021, 1, 2) <= datetime (2021, 1, [1, 2, 3]), ... %! [false, true, true]) %!assert_equal (datetime (2021, 1, [1, 2; 3, 4]) > datetime (2021, 1, 2), ... %! logical ([0, 0; 1, 1])) ## Test 'isequal' -- equal and unequal datetime arrays %!assert_equal (isequal (datetime (2021, 1, 1), datetime (2021, 1, 1)), true) %!assert_equal (isequal (datetime (2021, 1, 1), datetime (2021, 6, 1)), false) %!assert_equal (isequal (datetime (2021, 1, 1, 12, 0, 0), ... %! datetime (2021, 1, 1, 12, 0, 0)), true) %!assert_equal (isequal (datetime (2021, 1, [1, 2, 3]), datetime (2021, 1, ... %! [1, 2, 3])), true) %!assert_equal (class (isequal (datetime (2021, 1, 1), datetime (2021, 1, ... %! 1))), 'logical') %!assert_equal (isscalar (isequal (datetime (2021, 1, [1, 2]), ... %! datetime (2021, 1, [1, 2]))), true) ## Test 'isequal' -- Not-A-Time is never equal (unlike isequaln) %!assert_equal (isequal (NaT, NaT), false) %!assert_equal (isequal (NaT, datetime (2021, 1, 1)), false) %!assert_equal (isequal ([datetime(2021,1,1), NaT], [datetime(2021,1,1), ... %! NaT]), false) ## Test 'isequal' -- multiple arguments are all compared to the first %!assert_equal (isequal (datetime (2021, 1, 1), datetime (2021, 1, 1), ... %! datetime (2021, 1, 1)), true) %!assert_equal (isequal (datetime (2021, 1, 1), datetime (2021, 1, 1), ... %! datetime (2021, 6, 1)), false) ## Test 'isequal' -- size and orientation mismatches return false %!assert_equal (isequal (datetime (2021, 1, [1, 2, 3]), datetime (2021, 1, ... %! [1, 2])), false) %!assert_equal (isequal (datetime (2021, 1, [1, 2, 3]), datetime (2021, 1, ... %! [1; 2; 3])), false) ## Test 'isequal' -- empties of equal size are equal %!assert_equal (isequal (datetime (2021, 1, []), datetime (2021, 1, [])), true) ## Test 'isequal' -- a non-datetime operand returns false, never errors %!assert_equal (isequal (datetime (2021, 1, 1), 738522), false) %!assert_equal (isequal (datetime (2021, 1, 1), 'x'), false) %!assert_equal (isequal (datetime (2021, 1, 1), {1}), false) ## Test 'isequal' -- time zones compare by instant; a zone mismatch is false %!assert_equal (isequal (datetime (2021, 1, 1, 'TimeZone', 'UTC'), ... %! datetime (2021, 1, 1)), false) %!assert_equal (isequal (datetime (2021, 1, 1, 12, 0, 0, 'TimeZone', 'UTC'), ... %! datetime (2021, 1, 1, 7, 0, 0, 'TimeZone', ... %! 'America/New_York')), true) %!assert_equal (isequal (datetime (2021, 1, 1, 'TimeZone', 'UTC'), ... %! datetime (2021, 1, 1, 'TimeZone', ... %! 'America/New_York')), false) ## Test 'isequal' -- infinite elements compare equal by sign %!assert_equal (isequal (datetime (2021, 1, 1) - days (Inf), ... %! datetime (2021, 1, 1) - days (Inf)), true) %!assert_equal (isequal (datetime (2021, 1, 1) - days (Inf), ... %! datetime (2021, 1, 1) - days (-Inf)), false) ## Test 'isequaln' -- like isequal but Not-A-Time compares equal %!assert_equal (isequaln (NaT, NaT), true) %!assert_equal (isequaln (NaT, datetime (2021, 1, 1)), false) %!assert_equal (isequaln ([datetime(2021,1,1), NaT], [datetime(2021,1,1), ... %! NaT]), true) %!assert_equal (isequaln ([datetime(2021,1,1), NaT], [datetime(2021,1,1), ... %! datetime(2021,6,1)]), false) %!assert_equal (isequaln (datetime (2021, 1, 1), datetime (2021, 1, 1)), true) %!assert_equal (isequaln (datetime (2021, 1, 1), datetime (2021, 6, 1)), false) %!assert_equal (class (isequaln (NaT, NaT)), 'logical') ## Test 'isequaln' -- multiple arguments are all compared to the first %!assert_equal (isequaln (NaT, NaT, NaT), true) %!assert_equal (isequaln (datetime (2021, 1, 1), NaT, datetime (2021, 1, ... %! 1)), false) ## Test 'isequaln' -- size, type, and time-zone mismatches return false %!assert_equal (isequaln (datetime (2021, 1, [1, 2, 3]), datetime (2021, 1, ... %! [1, 2])), false) %!assert_equal (isequaln (datetime (2021, 1, 1), 738522), false) %!assert_equal (isequaln (datetime (2021, 1, 1, 'TimeZone', 'UTC'), ... %! datetime (2021, 1, 1)), false) %!assert_equal (isequaln (datetime (2021, 1, 1, 12, 0, 0, 'TimeZone', ... %! 'UTC'), ... %! datetime (2021, 1, 1, 7, 0, 0, 'TimeZone', ... %! 'America/New_York')), true) ## Test 'isequaln' -- infinite elements are equal but distinct from Not-A-Time %!assert_equal (isequaln (datetime (2021, 1, 1) - days (Inf), ... %! datetime (2021, 1, 1) - days (Inf)), true) %!assert_equal (isequaln (NaT, datetime (2021, 1, 1) - days (Inf)), false) ## Test 'colon' -- issue #2: ranges must not blow the stack %!assert_equal (numel (datetime (2025, 5, 7) : datetime (2025, 5, 8)), 2) %!assert_equal (class (datetime (2025, 5, 7) : datetime (2025, 5, ... %! 8)), 'datetime') %!assert_equal (numel (datetime (2025, 5, 7, 0, 0, 0) : hours (1) ... %! : datetime (2025, 5, 8, 0, 0, 0)), 25) ## Test 'colon' -- default step is one calendar day and yields a row vector %!assert_equal (cellstr (datetime (2021, 1, 1) : datetime (2021, 1, 4)), ... %! {'01-Jan-2021', '02-Jan-2021', '03-Jan-2021', '04-Jan-2021'}) %!assert_equal (size (datetime (2021, 1, 1) : datetime (2021, 1, 5)), [1, 5]) %!assert_equal (isscalar (datetime (2021, 1, 1) : datetime (2021, 1, 1)), true) ## Test 'colon' -- duration and numeric steps advance by fixed elapsed time %!assert_equal (numel (datetime (2021, 1, 1, 0, 0, 0) : hours (6) ... %! : datetime (2021, 1, 2, 0, 0, 0)), 5) %!assert_equal (cellstr (datetime (2021, 1, 1) : 2 : datetime (2021, 1, 5)), ... %! {'01-Jan-2021', '03-Jan-2021', '05-Jan-2021'}) %!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) : minutes (30) ... %! : datetime (2021, 1, 1, 1, 0, 0)), ... %! [2021, 1, 1, 0, 0, 0; 2021, 1, 1, 0, 30, 0; 2021, 1, 1, 1, ... %! 0, 0]) ## Test 'colon' -- calendarDuration steps clamp the day per element %!assert_equal (cellstr (datetime (2021, 1, ... %! 31) : calmonths (1) : datetime (2021, 5, 31)), ... %! {'31-Jan-2021', '28-Feb-2021', '31-Mar-2021', '30-Apr-2021', ... %! '31-May-2021'}) %!assert_equal (cellstr (datetime (2021, 1, ... %! 31) : calmonths (2) : datetime (2021, 5, 31)), ... %! {'31-Jan-2021', '31-Mar-2021', '31-May-2021'}) %!assert_equal (numel (datetime (2021, 1, ... %! 1) : calyears (1) : datetime (2025, 1, 1)), 5) %!assert_equal (cellstr (datetime (2021, 1, ... %! 1) : calquarters (1) : datetime (2021, 10, 1)), ... %! {'01-Jan-2021', '01-Apr-2021', '01-Jul-2021', '01-Oct-2021'}) ## Test 'colon' -- default calendar-day step preserves the wall clock over DST %!assert_equal ((datetime (2021, 3, 12, 'TimeZone', 'America/New_York') ... %! : datetime (2021, 3, 16, 'TimeZone', ... %! 'America/New_York')).Hour, ... %! [0, 0, 0, 0, 0]) ## Test 'colon' -- a fixed 24-hour step drifts across a DST change %!assert_equal ((datetime (2021, 3, 12, 'TimeZone', 'America/New_York') ... %! : days (1) : datetime (2021, 3, 16, 'TimeZone', ... %! 'America/New_York')).Hour, ... %! [0, 0, 0, 1]) ## Test 'colon' -- reversed ranges need a negative step %!assert_equal (isempty (datetime (2021, 1, 5) : datetime (2021, 1, 1)), true) %!assert_equal (size (datetime (2021, 1, 5) : datetime (2021, 1, 1)), [1, 0]) %!assert_equal (isempty (datetime (2021, 1, ... %! 5) : caldays (1) : datetime (2021, 1, 1)), true) %!assert_equal (cellstr (datetime (2021, 1, ... %! 5) : caldays (-1) : datetime (2021, 1, 3)), ... %! {'05-Jan-2021', '04-Jan-2021', '03-Jan-2021'}) %!assert_equal (numel (datetime (2021, 1, 1, 12, 0, 0) : hours (-6) ... %! : datetime (2021, 1, 1, 0, 0, 0)), 3) ## Test 'colon' -- the range inherits the endpoints' time zone %!assert_equal ((datetime (2021, 1, 1, 'TimeZone', 'UTC') ... %! : datetime (2021, 1, 3, 'TimeZone', 'UTC')).TimeZone, 'UTC') ## Test 'colon' -- error branches %!error ... %! datetime (2021, 1, 1) : 5; %!error ... %! datetime (2021, 1, [1, 2]) : datetime (2021, 1, 5); %!error ... %! datetime (2021, 1, 1) : datetime (2021, 1, 5, 'TimeZone', 'UTC'); %!error ... %! datetime (2021, 1, 1) : NaT; %!error ... %! datetime (2021, 1, 1) : caldays (0) : datetime (2021, 1, 5); %!error ... %! datetime (2021, 1, 1) : {1} : datetime (2021, 1, 5); %!error ... %! datetime (2021, 1, 1) : caldays ([1, 2]) : datetime (2021, 1, 5); ## Test 'linspace' -- both endpoints are included and 100 values by default %!test %! r = linspace (datetime (2024, 1, 1), datetime (2024, 1, 11), 3); %! assert_equal (size (r), [1, 3]); %! assert_equal (r.Day, [1, 6, 11]); %!test %! a = datetime (2024, 1, 1); %! b = datetime (2024, 1, 11); %! assert_equal (size (linspace (a, b)), [1, 100]); %! assert_equal (linspace (a, b, 2).Day, [1, 11]); ## Test 'linspace' -- N of one returns the end point, N below one is empty, and ## a non-integer N is floored %!test %! a = datetime (2024, 1, 1); %! b = datetime (2024, 1, 11); %! assert_equal (linspace (a, b, 1).Day, 11); %! assert_equal (size (linspace (a, b, 0)), [1, 0]); %! assert_equal (class (linspace (a, b, 0)), 'datetime'); %! assert_equal (size (linspace (a, b, -3)), [1, 0]); %! assert_equal (linspace (a, b, 3.7).Day, [1, 6, 11]); %! assert_equal (linspace (a, b, int32 (3)).Day, [1, 6, 11]); ## Test 'linspace' -- a reversed range descends, and equal endpoints repeat %!test %! a = datetime (2024, 1, 1); %! assert_equal (linspace (datetime (2024, 1, 11), a, 3).Day, [11, 6, 1]); %! assert_equal (linspace (a, a, 3).Day, [1, 1, 1]); ## Test 'linspace' -- the result carries the Format and TimeZone of the first ## endpoint %!test %! a = datetime (2024, 1, 1, 'Format', 'uuuu-MM-dd HH:mm:ss'); %! b = datetime (2024, 1, 11, 'Format', 'dd-MMM-uuuu'); %! assert_equal (linspace (a, b, 3).Format, 'uuuu-MM-dd HH:mm:ss'); %! assert_equal (linspace (b, a, 3).Format, 'dd-MMM-uuuu'); %!test %! tz = 'America/New_York'; %! r = linspace (datetime (2024, 1, 1, 'TimeZone', tz), ... %! datetime (2024, 1, 11, 'TimeZone', tz), 3); %! assert_equal (r.TimeZone, tz); ## Test 'linspace' -- spacing is by absolute instant, so it is daylight saving ## aware and the wall-clock readings are not equally spaced %!test %! a = datetime (2024, 3, 9, 0, 0, 0, 'TimeZone', 'America/New_York'); %! b = datetime (2024, 3, 11, 0, 0, 0, 'TimeZone', 'America/New_York'); %! r = linspace (a, b, 5); %! assert_equal (r.Day, [9, 9, 9, 10, 11]); %! assert_equal (r.Hour, [0, 11, 23, 12, 0]); %! assert_equal (r.Minute, [0, 45, 30, 15, 0]); ## Test 'linspace' -- the two endpoints may sit in different time zones, the ## result being expressed in the zone of the first %!test %! a = datetime (2024, 1, 1, 0, 0, 0, 'TimeZone', 'America/New_York'); %! b = datetime (2024, 1, 11, 0, 0, 0, 'TimeZone', 'UTC'); %! r = linspace (a, b, 3); %! assert_equal (r.TimeZone, 'America/New_York'); %! assert_equal (r.Day, [1, 5, 10]); %! assert_equal (r.Hour, [0, 21, 19]); %! assert_equal (r.Minute, [0, 30, 0]); ## Test 'linspace' -- sub-second spacing %!test %! r = linspace (datetime (2024, 1, 1, 0, 0, 0), ... %! datetime (2024, 1, 1, 0, 0, 1), 3); %! assert_equal (r.Second, [0, 0.5, 1]); ## Test 'linspace' -- an inserted leap second is stepped through, the span from ## 23:59:59 to the next midnight being two SI seconds %!test %! tz = 'UTCLeapSeconds'; %! a = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', tz); %! b = datetime (2017, 1, 1, 0, 0, 0, 'TimeZone', tz); %! r = linspace (a, b, 3); %! assert_equal (r.Second, [59, 60, 0]); %! assert_equal (r.Format, "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'"); ## Test 'linspace' -- a Not-A-Time endpoint carries through every value that ## depends on it, leaving only the opposite endpoint %!test %! r = linspace (datetime (2024, 1, 1), NaT, 3); %! assert_equal (isnat (r), logical ([0, 1, 1])); %! assert_equal (r.Day(1), 1); %!test %! r = linspace (NaT, datetime (2024, 1, 11), 3); %! assert_equal (isnat (r), logical ([1, 1, 0])); %! assert_equal (r.Day(3), 11); %!assert_equal (isnat (linspace (NaT, NaT, 3)), logical ([1, 1, 1])) %!assert_equal (isnat (linspace (datetime (2024, 1, 1), NaT, 1)), true) ## Test 'linspace' -- an infinite endpoint carries its infinity forward, while ## stepping back from an infinite start reaches nothing definite %!test %! a = datetime (2024, 1, 1); %! pinf = datetime (Inf, 'ConvertFrom', 'datenum'); %! ninf = datetime (-Inf, 'ConvertFrom', 'datenum'); %! assert_equal (linspace (a, pinf, 3).Year, [2024, Inf, Inf]); %! assert_equal (linspace (a, ninf, 3).Year, [2024, -Inf, -Inf]); %! assert_equal (linspace (pinf, a, 3).Year, [Inf, NaN, 2024]); %! assert_equal (linspace (ninf, a, 3).Year, [-Inf, NaN, 2024]); %!test %! pinf = datetime (Inf, 'ConvertFrom', 'datenum'); %! ninf = datetime (-Inf, 'ConvertFrom', 'datenum'); %! assert_equal (linspace (pinf, pinf, 3).Year, [Inf, NaN, Inf]); %! assert_equal (linspace (ninf, pinf, 4).Year, [-Inf, NaN, NaN, Inf]); %! assert_equal (linspace (pinf, ninf, 4).Year, [Inf, NaN, NaN, -Inf]); %! assert_equal (isnat (linspace (pinf, ninf, 4)), logical ([0, 1, 1, 0])); ## Test 'linspace' -- error branches %!error ... %! linspace (datetime (2024, 1, 1)) %!error ... %! linspace (datetime (2024, 1, 1), 5, 3) %!error ... %! linspace (5, datetime (2024, 1, 11), 3) %!error ... %! linspace (datetime (2024, 1, 1), days (3), 3) %!error ... %! linspace (datetime (2024, 1, [1, 2]), datetime (2024, 1, 11), 3) %!error ... %! linspace (datetime (2024, 1, 1), datetime (2024, 1, [11, 12]), 3) %!error ... %! linspace (datetime (2024, 1, 1), datetime (2024, 1, 11), 'x') %!error ... %! linspace (datetime (2024, 1, 1, 'TimeZone', 'UTC'), ... %! datetime (2024, 1, 11), 3) %!error ... %! linspace (datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'), ... %! datetime (2017, 1, 2, 'TimeZone', 'UTC'), 3) ## Test 'sort'/'min'/'max' -- an array of more than two dimensions is reduced ## along the dimension asked for, checked against the numeric answer %!test %! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]); %! a = datetime (2024, 1, x); %! [b, i] = sort (a, 1); %! [w, wi] = sort (x, 1); %! assert_equal (size (b), size (w)); %! assert_equal (b.Day, w); %! assert_equal (i, wi); %!test %! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]); %! a = datetime (2024, 1, x); %! [b, i] = sort (a, 2); %! [w, wi] = sort (x, 2); %! assert_equal (size (b), size (w)); %! assert_equal (b.Day, w); %! assert_equal (i, wi); %!test %! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]); %! a = datetime (2024, 1, x); %! [b, i] = sort (a, 3); %! [w, wi] = sort (x, 3); %! assert_equal (size (b), size (w)); %! assert_equal (b.Day, w); %! assert_equal (i, wi); %!test %! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]); %! a = datetime (2024, 1, x); %! [m, i] = min (a, [], 1); %! [wm, wi] = min (x, [], 1); %! assert_equal (size (m), size (wm)); %! assert_equal (m.Day, wm); %! assert_equal (i, wi); %!test %! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]); %! a = datetime (2024, 1, x); %! [M, j] = max (a, [], 1); %! [wM, wj] = max (x, [], 1); %! assert_equal (M.Day, wM); %! assert_equal (j, wj); %!test %! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]); %! a = datetime (2024, 1, x); %! [m, i] = min (a, [], 2); %! [wm, wi] = min (x, [], 2); %! assert_equal (size (m), size (wm)); %! assert_equal (m.Day, wm); %! assert_equal (i, wi); %!test %! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]); %! a = datetime (2024, 1, x); %! [M, j] = max (a, [], 2); %! [wM, wj] = max (x, [], 2); %! assert_equal (M.Day, wM); %! assert_equal (j, wj); %!test %! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]); %! a = datetime (2024, 1, x); %! [m, i] = min (a, [], 3); %! [wm, wi] = min (x, [], 3); %! assert_equal (size (m), size (wm)); %! assert_equal (m.Day, wm); %! assert_equal (i, wi); %!test %! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]); %! a = datetime (2024, 1, x); %! [M, j] = max (a, [], 3); %! [wM, wj] = max (x, [], 3); %! assert_equal (M.Day, wM); %! assert_equal (j, wj); %!test %! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]); %! a = datetime (2024, 1, x); %! [b, i] = sort (a, 2, 'descend'); %! [w, wi] = sort (x, 2, 'descend'); %! assert_equal (b.Day, w); %! assert_equal (i, wi); %!test %! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]); %! z = datetime (2024, 1, x, 'TimeZone', 'America/New_York'); %! assert_equal (sort (z, 3).Day, sort (x, 3)); %! assert_equal (min (z, [], 3).Day, min (x, [], 3)); %! assert_equal (sort (z, 3).TimeZone, 'America/New_York'); ## Test 'sort' -- ascending order is the default and ties are stable %!test %! [b, i] = sort (datetime (2021, 1, [3, 1, 2, 1])); %! assert_equal (b.Day, [1, 1, 2, 3]); %! assert_equal (i, [2, 4, 3, 1]); ## Test 'sort' -- descending order keeps the original order of equal elements %!test %! [b, i] = sort (datetime (2021, 1, [3, 1, 2, 1]), 'descend'); %! assert_equal (b.Day, [3, 2, 1, 1]); %! assert_equal (i, [1, 3, 2, 4]); ## Test 'sort' -- Not-A-Time goes last for ascending, first for descending %!test %! n = datetime ([2021, 2020, NaN, 2022], 1, 1); %! [b, i] = sort (n); %! assert_equal (isnat (b), logical ([0, 0, 0, 1])); %! assert_equal (b.Year(1:3), [2020, 2021, 2022]); %! assert_equal (i, [2, 1, 4, 3]); %! [b, i] = sort (n, 'descend'); %! assert_equal (isnat (b), logical ([1, 0, 0, 0])); %! assert_equal (i, [3, 4, 1, 2]); ## Test 'sort' -- MissingPlacement overrides the direction default %!test %! n = datetime ([2021, 2020, NaN, 2022], 1, 1); %! b = sort (n, 'MissingPlacement', 'first'); %! assert_equal (isnat (b), logical ([1, 0, 0, 0])); %! assert_equal (b.Year(2:4), [2020, 2021, 2022]); %! b = sort (n, 'MissingPlacement', 'last'); %! assert_equal (isnat (b), logical ([0, 0, 0, 1])); %! b = sort (n, 'descend', 'MissingPlacement', 'first'); %! assert_equal (isnat (b), logical ([1, 0, 0, 0])); %! assert_equal (b.Year(2:4), [2022, 2021, 2020]); ## Test 'sort' -- a matrix sorts down each column by default %!test %! [b, i] = sort (datetime (2021, 1, [3, 1; 2, 4])); %! assert_equal (b.Day, [2, 1; 3, 4]); %! assert_equal (i, [2, 1; 1, 2]); ## Test 'sort' -- dimension two sorts along each row %!assert_equal (sort (datetime (2021, 1, [3, 1; 2, 4]), 2).Day, [1, 3; 2, 4]) ## Test 'sort' -- descending sort along the first dimension of a matrix %!test %! [b, i] = sort (datetime (2021, 1, [3, 1; 2, 4]), 1, 'descend'); %! assert_equal (b.Day, [3, 4; 2, 1]); %! assert_equal (i, [1, 2; 2, 1]); ## Test 'sort' -- an empty array is returned unchanged %!assert_equal (size (sort (datetime (2021, 1, []))), [0, 0]) ## Test 'sort' -- a zoned array sorts by absolute instant %!test %! [b, i] = sort (datetime (2021, 1, [3, 1, 2], ... %! 'TimeZone', 'America/New_York'), 'descend'); %! assert_equal (b.Day, [3, 2, 1]); %! assert_equal (i, [1, 3, 2]); ## Test 'sort' -- error branches %!error ... %! sort (datetime (2021, 1, 1), 0); %!error ... %! sort (datetime (2021, 1, 1), 'up'); %!error ... %! sort (datetime (2021, 1, 1), 'MissingPlacement', 'middle'); %!error ... %! sort (datetime (2021, 1, 1), 'MissingPlacement'); %!error ... %! sort (datetime (2021, 1, 1), {1}); ## Test 'maxk' -- the K latest elements come back in descending order %!test %! [b, i] = maxk (datetime (2024, 1, [3, 1, 2, 5]), 3); %! assert_equal (b.Day, [5, 3, 2]); %! assert_equal (i, [4, 1, 3]); ## Test 'maxk' -- ties keep their original order %!test %! [b, i] = maxk (datetime (2024, 1, [2, 1, 2, 1, 2]), 3); %! assert_equal (b.Day, [2, 2, 2]); %! assert_equal (i, [1, 3, 5]); ## Test 'mink' -- the K earliest elements come back in ascending order %!test %! [b, i] = mink (datetime (2024, 1, [3, 1, 2, 5]), 3); %! assert_equal (b.Day, [1, 2, 3]); %! assert_equal (i, [2, 3, 1]); ## Test 'mink' -- ties keep their original order %!test %! [b, i] = mink (datetime (2024, 1, [2, 1, 2, 1, 2]), 3); %! assert_equal (b.Day, [1, 1, 2]); %! assert_equal (i, [2, 4, 1]); ## Test 'maxk'/'mink' -- Not-A-Time is never ranked %!test %! t = datetime (2024, 1, [3, NaN, 1, NaN, 5]); %! [b, i] = maxk (t, 3); %! assert_equal (b.Day, [5, 3, 1]); %! assert_equal (i, [5, 1, 3]); %! [b, i] = mink (t, 3); %! assert_equal (b.Day, [1, 3, 5]); %! assert_equal (i, [3, 1, 5]); ## Test 'maxk'/'mink' -- Not-A-Time is appended last in either direction, and ## only once K passes the number of non-missing elements %!test %! t = datetime (2024, 1, [3, NaN, 1, NaN, 5]); %! [b, i] = maxk (t, 10); %! assert_equal (isnat (b), logical ([0, 0, 0, 1, 1])); %! assert_equal (b.Day(1:3), [5, 3, 1]); %! assert_equal (i, [5, 1, 3, 2, 4]); %! [b, i] = mink (t, 10); %! assert_equal (isnat (b), logical ([0, 0, 0, 1, 1])); %! assert_equal (b.Day(1:3), [1, 3, 5]); %! assert_equal (i, [3, 1, 5, 2, 4]); ## Test 'maxk'/'mink' -- an all-missing array returns missing elements %!test %! t = datetime ([NaN, NaN, NaN], 1, 1); %! [b, i] = maxk (t, 2); %! assert_equal (isnat (b), logical ([1, 1])); %! assert_equal (i, [1, 2]); %! [b, i] = mink (t, 2); %! assert_equal (isnat (b), logical ([1, 1])); %! assert_equal (i, [1, 2]); ## Test 'maxk'/'mink' -- a matrix ranks down each column by default %!test %! a = datetime (2024, 1, [1, 5; 4, 2; 3, 6]); %! [b, i] = maxk (a, 2); %! assert_equal (b.Day, [4, 6; 3, 5]); %! assert_equal (i, [2, 3; 3, 1]); %! [b, i] = mink (a, 2); %! assert_equal (b.Day, [1, 2; 3, 5]); %! assert_equal (i, [1, 2; 3, 1]); ## Test 'maxk' -- dimension two ranks along each row %!test %! a = datetime (2024, 1, [1, 5; 4, 2; 3, 6]); %! [b, i] = maxk (a, 2, 2); %! assert_equal (b.Day, [5, 1; 4, 2; 6, 3]); %! assert_equal (i, [2, 1; 1, 2; 2, 1]); ## Test 'maxk' -- a dimension beyond the array leaves it unchanged %!test %! a = datetime (2024, 1, [1, 5; 4, 2; 3, 6]); %! [b, i] = maxk (a, 2, 3); %! assert_equal (b.Day, [1, 5; 4, 2; 3, 6]); %! assert_equal (i, ones (3, 2)); ## Test 'maxk' -- an array of more than two dimensions is ranked along the ## dimension asked for, checked against the numeric answer %!test %! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]); %! a = datetime (2024, 1, x); %! w = sort (x, 1, 'descend'); %! assert_equal (maxk (a, 2, 1).Day, w(1:2,:,:)); %! w = sort (x, 2, 'descend'); %! assert_equal (maxk (a, 2, 2).Day, w(:,1:2,:)); %! w = sort (x, 3, 'descend'); %! assert_equal (maxk (a, 2, 3).Day, w(:,:,1:2)); ## Test 'maxk'/'mink' -- K may be zero, and empties keep their orientation %!assert_equal (size (maxk (datetime (2024, 1, [1, 2, 3]), 0)), [1, 0]) %!assert_equal (size (mink (datetime (2024, 1, [1, 2, 3]), 0)), [1, 0]) %!assert_equal (size (maxk (datetime (2024, 1, [1, 5; 4, 2; 3, 6]), 0)), [0, 2]) %!assert_equal (size (maxk (datetime.empty (0, 0), 2)), [0, 0]) %!assert_equal (class (maxk (datetime (2024, 1, 1), 1)), 'datetime') ## Test 'maxk' -- a zoned array ranks by absolute instant and keeps its zone %!test %! z = datetime (2024, 1, [3, 1, 5], 'TimeZone', 'America/New_York'); %! [b, i] = maxk (z, 2); %! assert_equal (b.Day, [5, 3]); %! assert_equal (i, [3, 1]); %! assert_equal (b.TimeZone, 'America/New_York'); ## Test 'maxk' -- the display format is carried through %!test %! t = datetime (2024, 1, [3, 1, 5]); %! t.Format = 'yyyy-MM-dd'; %! assert_equal (maxk (t, 2).Format, 'yyyy-MM-dd'); ## Test 'maxk' -- an inserted leap second ranks as the latest instant %!test %! L = datetime (2016, 12, 31, 23, 59, [58, 59, 60], ... %! 'TimeZone', 'UTCLeapSeconds'); %! [b, i] = maxk (L, 2); %! assert_equal (b.Second, [60, 59]); %! assert_equal (i, [3, 2]); ## Test 'maxk'/'mink' -- infinite datetimes rank as ordinary values %!test %! pinf = datetime (Inf, 'ConvertFrom', 'datenum'); %! ninf = datetime (-Inf, 'ConvertFrom', 'datenum'); %! t = [pinf, ninf, datetime(2024, 1, 1), NaT]; %! [b, i] = maxk (t, 4); %! assert_equal (i, [1, 3, 2, 4]); %! assert_equal (isnat (b), logical ([0, 0, 0, 1])); %! [b, i] = mink (t, 4); %! assert_equal (i, [2, 3, 1, 4]); %! assert_equal (isnat (b), logical ([0, 0, 0, 1])); ## Test 'maxk'/'mink' -- error branches %!error maxk (datetime (2024, 1, 1)) %!error mink (datetime (2024, 1, 1)) %!error ... %! maxk (datetime (2024, 1, [1, 2, 3]), 2.5) %!error ... %! maxk (datetime (2024, 1, [1, 2, 3]), -1) %!error ... %! maxk (datetime (2024, 1, [1, 2, 3]), [2, 3]) %!error ... %! mink (datetime (2024, 1, [1, 2, 3]), Inf) %!error ... %! maxk (datetime (2024, 1, [1, 2, 3]), 2, 0) %!error ... %! mink (datetime (2024, 1, [1, 2, 3]), 2, 1.5) %!error ... %! maxk (datetime (2024, 1, [1, 2, 3]), 2, 'ComparisonMethod', 'abs') %!error ... %! mink (datetime (2024, 1, [1, 2, 3]), 2, 'MissingPlacement', 'first') %!error ... %! maxk (datetime (2024, 1, [1, 2, 3]), 2, 1, 2) ## Test 'issorted' -- ascending (non-decreasing) is the default %!assert_equal (issorted (datetime (2021, 1, [1, 2, 3])), true) %!assert_equal (issorted (datetime (2021, 1, [3, 2, 1])), false) %!assert_equal (issorted (datetime (2021, 1, [1, 2, 2, 3])), true) ## Test 'issorted' -- descending order %!assert_equal (issorted (datetime (2021, 1, [3, 2, 1]), 'descend'), true) ## Test 'issorted' -- strict variants reject equal neighbours %!assert_equal (issorted (datetime (2021, 1, [1, 2, 2, 3]), ... %! 'strictascend'), false) %!assert_equal (issorted (datetime (2021, 1, [1, 2, 3]), 'strictascend'), true) %!assert_equal (issorted (datetime (2021, 1, [1, 2, 3]), ... %! 'strictmonotonic'), true) ## Test 'issorted' -- monotonic accepts either direction %!assert_equal (issorted (datetime (2021, 1, [1, 2, 3]), 'monotonic'), true) %!assert_equal (issorted (datetime (2021, 1, [3, 2, 1]), 'monotonic'), true) ## Test 'issorted' -- Not-A-Time counts as greater than any value %!assert_equal (issorted (datetime ([2021, 2022, NaN], 1, 1)), true) %!assert_equal (issorted (datetime ([NaN, 2021, 2022], 1, 1)), false) %!assert_equal (issorted (datetime ([2021, 2022, NaN], 1, 1), 'descend'), false) ## Test 'issorted' -- every column (or row) of a matrix must be sorted %!assert_equal (issorted (datetime (2021, 1, [3, 1; 2, 4])), false) %!assert_equal (issorted (datetime (2021, 1, [3, 1; 2, 4]), 2), false) %!assert_equal (issorted (datetime (2021, 1, [1, 3; 2, 4])), true) ## Test 'issorted' -- scalars and empties are trivially sorted %!assert_equal (issorted (datetime (2021, 1, 1)), true) %!assert_equal (issorted (datetime (2021, 1, [])), true) ## Test 'issorted' -- error branches %!error ... %! issorted (datetime (2021, 1, 1), 1.5); %!error ... %! issorted (datetime (2021, 1, 1), 'up'); %!error ... %! issorted (datetime (2021, 1, 1), {1}); ## Test 'sortrows' -- rows sort lexicographically in ascending order %!test %! [b, i] = sortrows (datetime (2021, 1, [3 1; 3 2; 1 5])); %! assert_equal (b.Day, [1 5; 3 1; 3 2]); %! assert_equal (i, [3; 1; 2]); ## Test 'sortrows' -- a bare direction applies to every column %!test %! [b, i] = sortrows (datetime (2021, 1, [3 1; 3 2; 1 5]), 'descend'); %! assert_equal (b.Day, [3 2; 3 1; 1 5]); %! assert_equal (i, [2; 1; 3]); ## Test 'sortrows' -- a column list selects and orders the sort keys %!assert_equal (sortrows (datetime (2021, 1, [3 1; 3 2; 1 5]), 2).Day, ... %! [3 1; 3 2; 1 5]) %!assert_equal (sortrows (datetime (2021, 1, [3 1; 3 2; 1 5]), [2 1]).Day, ... %! [3 1; 3 2; 1 5]) ## Test 'sortrows' -- a negative column sorts that key in descending order %!assert_equal (sortrows (datetime (2021, 1, [3 1; 3 2; 1 5]), [1 -2]).Day, ... %! [1 5; 3 2; 3 1]) ## Test 'sortrows' -- a cell array gives one direction per sort column %!assert_equal (sortrows (datetime (2021, 1, [3 1; 3 2; 1 5]), ... %! {'ascend', 'descend'}).Day, [1 5; 3 2; 3 1]) %!assert_equal (sortrows (datetime (2021, 1, [3 1; 3 2; 1 5]), [1 2], ... %! {'ascend', 'descend'}).Day, [1 5; 3 2; 3 1]) ## Test 'sortrows' -- Not-A-Time sorts as the largest value by default %!test %! N = datetime (2021, 1, [2 1; NaN 3; 2 5; 1 1; 2 1]); %! [b, i] = sortrows (N); %! assert_equal (i, [4; 1; 5; 3; 2]); %! assert_equal (isnat (b)(:, 1), logical ([0; 0; 0; 0; 1])); %! [b, i] = sortrows (N, 'descend'); %! assert_equal (i, [2; 3; 1; 5; 4]); ## Test 'sortrows' -- MissingPlacement overrides the NaT position %!test %! N = datetime (2021, 1, [2 1; NaN 3; 2 5; 1 1; 2 1]); %! bf = sortrows (N, 'MissingPlacement', 'first'); %! assert_equal (isnat (bf)(1, 1), true); %! bl = sortrows (N, 'MissingPlacement', 'last'); %! assert_equal (isnat (bl)(5, 1), true); ## Test 'sortrows' -- a single sort column keeps ties in original order %!test %! N = datetime (2021, 1, [2 1; NaN 3; 2 5; 1 1; 2 1]); %! [b, i] = sortrows (N, 1); %! assert_equal (i, [4; 1; 3; 5; 2]); ## Test 'sortrows' -- a column vector sorts as a single-column matrix %!test %! [b, i] = sortrows (datetime (2021, 1, [3; 1; 2])); %! assert_equal (i, [2; 3; 1]); ## Test 'sortrows' -- error branches %!error ... %! sortrows (datetime (2021, 1, [1 2]), 3); %!error ... %! sortrows (datetime (2021, 1, [1 2]), 'up'); %!error ... %! sortrows (datetime (2021, 1, [1 2]), [1 2], {'ascend'}); %!error ... %! sortrows (datetime (2021, 1, [1 2]), 'ascend', 'descend'); %!error ... %! sortrows (datetime (2021, 1, [1 2]), 'MissingPlacement', 'middle'); ## Test 'issortedrows' -- ascending lexicographic order is the default %!assert_equal (issortedrows (datetime (2021, 1, [3 1; 3 2; 1 5])), false) %!assert_equal (issortedrows (datetime (2021, 1, [1 5; 3 1; 3 2])), true) ## Test 'issortedrows' -- ties are resolved by later columns %!assert_equal (issortedrows (datetime (2021, 1, [1 1; 1 2; 2 0])), true) %!assert_equal (issortedrows (datetime (2021, 1, [1 2; 1 1])), false) ## Test 'issortedrows' -- a column list selects and orients the sort keys %!assert_equal (issortedrows (datetime (2021, 1, [3 1; 3 2; 1 5]), 2), true) %!assert_equal (issortedrows (datetime (2021, 1, [3 1; 3 2; 1 5]), ... %! [1 -2]), false) ## Test 'issortedrows' -- descend and monotonic directions %!assert_equal (issortedrows (datetime (2021, 1, [3 2; 3 1; 1 5]), ... %! 'descend'), true) %!assert_equal (issortedrows (datetime (2021, 1, [3; 2; 1]), 'monotonic'), true) %!assert_equal (issortedrows (datetime (2021, 1, [1; 2; 3]), 'monotonic'), true) ## Test 'issortedrows' -- strict checks only the first sort column %!assert_equal (issortedrows (datetime (2021, 1, [1 5; 2 5; 3 5]), ... %! 'strictascend'), true) %!assert_equal (issortedrows (datetime (2021, 1, [1 5; 2 5; 2 5]), ... %! 'strictascend'), false) %!assert_equal (issortedrows (datetime (2021, 1, [1 1; 1 2; 2 0]), ... %! 'strictascend'), false) %!assert_equal (issortedrows (datetime (2021, 1, [3 5; 2 5; 1 5]), ... %! 'strictdescend'), true) %!assert_equal (issortedrows (datetime (2021, 1, [1; 2; 3]), ... %! 'strictmonotonic'), true) ## Test 'issortedrows' -- strict is false when the first column has NaT %!assert_equal (issortedrows (datetime ([2021; 2022; NaN], 1, 1), ... %! 'strictascend'), false) %!assert_equal (issortedrows (datetime ([2021; 2022; NaN], 1, 1), ... %! 'ascend'), true) ## Test 'issortedrows' -- a cell array gives one direction per column %!assert_equal (issortedrows (datetime (2021, 1, [1 5; 3 2; 3 1]), ... %! {'ascend', 'descend'}), true) ## Test 'issortedrows' -- error branches %!error ... %! issortedrows (datetime (2021, 1, [1 2]), 3); %!error ... %! issortedrows (datetime (2021, 1, [1 2]), 'up'); %!error ... %! issortedrows (datetime (2021, 1, [1 2]), {'ascend'}); %!error ... %! issortedrows (datetime (2021, 1, [1 2]), 'ascend', 'descend'); ## Test 'topkrows' -- the top K rows come back in descending order, with ## missing elements ranked last within each sort column %!test %! T = datetime (2024, 1, [3, 1; 1, 2; 3, NaN; NaN, 5; 1, 1]); %! [B, I] = topkrows (T, 5); %! assert_equal (I, [1; 3; 2; 5; 4]); %! assert_equal (B.Day, [3, 1; 3, NaN; 1, 2; 1, 1; NaN, 5]); ## Test 'topkrows' -- a column list and a direction %!test %! T = datetime (2024, 1, [3, 1; 1, 2; 3, NaN; NaN, 5; 1, 1]); %! [B, I] = topkrows (T, 5, 1, 'ascend'); %! assert_equal (I, [2; 5; 1; 3; 4]); %! [B, I] = topkrows (T, 5, 2); %! assert_equal (I, [4; 2; 1; 5; 3]); ## Test 'topkrows' -- K past the number of rows, K zero, and empty input %!test %! T = datetime (2024, 1, [3, 1; 1, 2; 3, NaN; NaN, 5; 1, 1]); %! assert_equal (topkrows (T, 10).Day, topkrows (T, 5).Day); %! assert_equal (size (topkrows (T, 0)), [0, 2]); %! assert_equal (size (topkrows (datetime.empty (0, 0), 2)), [0, 0]); ## Test 'topkrows' -- the display format and the time zone are carried through %!test %! T = datetime (2024, 1, [3, 1; 1, 2; 3, NaN; NaN, 5; 1, 1]); %! assert_equal (topkrows (T, 2).Format, 'dd-MMM-uuuu'); %! z = datetime (2024, 1, [3; 1; 5], 'TimeZone', 'America/New_York'); %! [B, I] = topkrows (z, 2); %! assert_equal (I, [3; 1]); %! assert_equal (B.TimeZone, 'America/New_York'); ## Test 'topkrows' -- 'MissingPlacement' is an Octave extension defaulting to ## 'last', which is what MATLAB always does %!test %! T = datetime (2024, 1, [3, 1; 1, 2; 3, NaN; NaN, 5; 1, 1]); %! [~, I] = topkrows (T, 5, 'MissingPlacement', 'last'); %! assert_equal (I, [1; 3; 2; 5; 4]); %! [~, I] = topkrows (T, 5, 'MissingPlacement', 'first'); %! assert_equal (I, [4; 3; 1; 2; 5]); ## Test 'topkrows' -- error branches %!error ... %! topkrows (datetime (2024, 1, 1)) %!error ... %! topkrows (datetime (2024, 1, reshape (1:8, 2, 2, 2)), 1) %!error ... %! topkrows (datetime (2024, 1, [1, 2; 3, 4]), 2.5) %!error ... %! topkrows (datetime (2024, 1, [1, 2; 3, 4]), -1) %!error ... %! topkrows (datetime (2024, 1, [1, 2; 3, 4]), 1, -1) %!error ... %! topkrows (datetime (2024, 1, [1, 2; 3, 4]), 1, 5) %!error ... %! topkrows (datetime (2024, 1, [1, 2; 3, 4]), 1, 1, 'bogus') %!error ... %! topkrows (datetime (2024, 1, [1, 2; 3, 4]), 1, 'MissingPlacement', 'bogus') ## Test 'min'/'max' -- reduce along the first non-singleton dimension %!test %! [m, i] = min (datetime (2021, 1, [3 1 2])); %! assert_equal (m.Day, 1); %! assert_equal (i, 2); %!test %! [m, i] = max (datetime (2021, 1, [3 1 2])); %! assert_equal (m.Day, 3); %! assert_equal (i, 1); ## Test 'min' -- NaT is omitted by default; ties return the first index %!test %! [m, i] = min (datetime (2021, 1, [3 NaN 1 2])); %! assert_equal (m.Day, 1); %! assert_equal (i, 3); %!test %! [m, i] = min (datetime (2021, 1, [2 1 1 3])); %! assert_equal (i, 2); ## Test 'min' -- 'includenan' propagates NaT %!assert_equal (isnat (min (datetime (2021, 1, [3 NaN 1]), [], ... %! 'includenan')), true) %!assert_equal (max (datetime (2021, 1, [3 NaN 1]), [], 'omitnan').Day, 3) ## Test 'min' -- a reduction over only NaT yields NaT at index 1 %!test %! [m, i] = min (datetime ([NaN NaN], 1, 1)); %! assert_equal (isnat (m), true); %! assert_equal (i, 1); ## Test 'min' -- matrix reduction along each dimension and over all elements %!assert_equal (min (datetime (2021, 1, [3 1; 2 4])).Day, [2 1]) %!assert_equal (min (datetime (2021, 1, [3 1; 2 4]), [], 2).Day, [1; 2]) %!assert_equal (min (datetime (2021, 1, [3 1; 2 4]), [], 'all').Day, 1) %!test %! [m, i] = min (datetime (2021, 1, [3 1; 2 4])); %! assert_equal (i, [2 1]); ## Test 'min'/'max' -- element-wise comparison of two arrays with broadcasting %!assert_equal (min (datetime (2021, 1, [1 5 3]), datetime (2021, 1, ... %! [4 2 3])).Day, [1 2 3]) %!assert_equal (max (datetime (2021, 1, [1 5 3]), datetime (2021, 1, ... %! [4 2 3])).Day, [4 5 3]) %!assert_equal (min (datetime (2021, 1, [1 5 3]), datetime (2021, 1, ... %! 3)).Day, [1 3 3]) ## Test 'min' -- element-wise NaT handling (omit versus include) %!assert_equal (min (datetime (2021, 1, [1 5 3]), ... %! datetime (2021, 1, [NaN 2 NaN])).Day, [1 2 3]) %!test %! m = min (datetime (2021, 1, [1 5 3]), ... %! datetime (2021, 1, [NaN 2 NaN]), 'includenan'); %! assert_equal (isnat (m), logical ([1 0 1])); %! assert_equal (m.Day(2), 2); ## Test 'min' -- reductions are by absolute instant and keep the time zone %!test %! z = datetime (2021, 1, [3 1 2], 'TimeZone', 'America/New_York'); %! [m, i] = min (z); %! assert_equal (m.Day, 1); %! assert_equal (i, 2); %! assert_equal (m.TimeZone, 'America/New_York'); ## Test 'min' -- an empty array reduces to an empty array %!assert_equal (size (min (datetime (2021, 1, []))), [0 0]) ## Test 'min' -- error branches %!error ... %! min (datetime (2021, 1, 1), 5); %!error ... %! [m, i] = min (datetime (2021, 1, 1), datetime (2021, 1, 2)); %!error ... %! min (datetime (2021, 1, 1), [], 0); %!error ... %! min (datetime (2021, 1, 1), [], 'foo'); %!error ... %! max (datetime (2021, 1, 1, 'TimeZone', 'UTC'), datetime (2021, 1, 2)); ################################################################################ ## ** Leap seconds ('UTCLeapSeconds') ** ## ################################################################################ ## Test 'UTCLeapSeconds' -- the 60th second of a minute is representable %!test %! a = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (datevec (a), [2016, 12, 31, 23, 59, 60]); %! assert_equal (a.Second, 60); %! assert_equal (a.Minute, 59); %! assert_equal (a.Hour, 23); %! assert_equal (a.TimeZone, 'UTCLeapSeconds'); %!assert_equal (datetime (2016, 12, 31, 23, 59, 60.25, ... %! 'TimeZone', 'UTCLeapSeconds').Second, 60.25) %!assert_equal (datetime (1972, 6, 30, 23, 59, 60, ... %! 'TimeZone', 'UTCLeapSeconds').Second, 60) ## Test 'UTCLeapSeconds' -- every second of the shipped table is recognized %!test %! L = [1972 6 30; 1972 12 31; 1973 12 31; 1974 12 31; 1975 12 31; 1976 12 31; %! 1977 12 31; 1978 12 31; 1979 12 31; 1981 6 30; 1982 6 30; 1983 6 30; %! 1985 6 30; 1987 12 31; 1989 12 31; 1990 12 31; 1992 6 30; 1993 6 30; %! 1994 6 30; 1995 12 31; 1997 6 30; 1998 12 31; 2005 12 31; 2008 12 31; %! 2012 6 30; 2015 6 30; 2016 12 31]; %! V = [L, repmat([23 59 60], rows (L), 1)]; %! d = datetime (V, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (d.Second, repmat (60, rows (L), 1)); ## Test 'UTCLeapSeconds' -- a 60th second that was never inserted rolls over %!test %! d = datetime ([2000 6 30 23 59 60; 2019 6 30 23 59 60; ... %! 2020 12 31 23 59 60], ... %! 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (d.Second, [0; 0; 0]); %! assert_equal (d.Day, [1; 1; 1]); %!assert_equal (datevec (datetime (2016, 12, 30, 23, 59, 60, ... %! 'TimeZone', 'UTCLeapSeconds')), [2016 12 31 0 0 0]) %!assert_equal (datevec (datetime (2016, 12, 31, 23, 58, 60, ... %! 'TimeZone', 'UTCLeapSeconds')), [2016 12 31 23 59 0]) %!assert_equal (datevec (datetime (1971, 12, 31, 23, 59, 60, ... %! 'TimeZone', 'UTCLeapSeconds')), [1972 1 1 0 0 0]) ## Test 'UTCLeapSeconds' -- the seconds field is counted on the leap-second ## timeline, while a minute or hour that overflows steps over the insertion %!assert_equal (datevec (datetime (2016, 12, 31, 23, 59, 61, ... %! 'TimeZone', 'UTCLeapSeconds')), [2017 1 1 0 0 0]) %!assert_equal (datevec (datetime (2016, 12, 31, 23, 59, 61.5, ... %! 'TimeZone', 'UTCLeapSeconds')), [2017 1 1 0 0 0.5]) %!assert_equal (datevec (datetime (2016, 12, 31, 23, 59, 120, ... %! 'TimeZone', 'UTCLeapSeconds')), [2017 1 1 0 0 59]) %!assert_equal (datevec (datetime (2016, 12, 31, 23, 60, 0, ... %! 'TimeZone', 'UTCLeapSeconds')), [2017 1 1 0 0 0]) %!assert_equal (datevec (datetime (2017, 1, 1, 0, 0, -1, ... %! 'TimeZone', 'UTCLeapSeconds')), [2016 12 31 23 59 60]) %!assert_equal (datevec (datetime (2017, 1, 1, 0, 0, -0.5, ... %! 'TimeZone', 'UTCLeapSeconds')), [2016 12 31 23 59 60.5]) ## Test 'UTCLeapSeconds' -- arithmetic counts the inserted second %!test %! a1 = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds'); %! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! a3 = datetime (2017, 1, 1, 0, 0, 0, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (seconds (a2 - a1), 1); %! assert_equal (seconds (a3 - a2), 1); %! assert_equal (seconds (a3 - a1), 2); %! assert_equal (seconds (a3 - datetime (1970, 1, 1, 0, 0, 0, ... %! 'TimeZone', 'UTCLeapSeconds')), ... %! 1483228827); %!assert_equal (seconds (datetime (1972, 1, 1, 0, 0, 0, ... %! 'TimeZone', 'UTCLeapSeconds') ... %! - datetime (1971, 12, 31, 23, 59, 59, ... %! 'TimeZone', 'UTCLeapSeconds')), 1) ## Test 'UTCLeapSeconds' -- a fixed-length day lands on the inserted second, ## a calendar day steps past it %!test %! d = datetime (2016, 12, 31, 0, 0, 0, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (datevec (d + days (1)), [2016 12 31 23 59 60]); %! assert_equal (datevec (d + caldays (1)), [2017 1 1 0 0 0]); ## Test 'UTCLeapSeconds' -- calendar arithmetic clamps a 60th second onto a ## minute that never had one, keeping any fractional part %!test %! q = datetime (2016, 12, 31, 23, 59, 60.25, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (datevec (q + caldays (1)), [2017 1 1 23 59 59.25]); %! assert_equal (datevec (q + calmonths (1)), [2017 1 31 23 59 59.25]); %! assert_equal (datevec (q + calyears (1)), [2017 12 31 23 59 59.25]); %!assert_equal (datevec (datetime (2016, 12, 31, 23, 59, 60, ... %! 'TimeZone', 'UTCLeapSeconds') - caldays (1)), ... %! [2016 12 30 23 59 59]) %!assert_equal (datevec (datetime (2015, 6, 30, 23, 59, 60, ... %! 'TimeZone', 'UTCLeapSeconds') + calmonths (18)), ... %! [2016 12 30 23 59 59]) ## Test 'UTCLeapSeconds' -- 'posixtime' folds forward onto the next second %!test %! a1 = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds'); %! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! a3 = datetime (2017, 1, 1, 0, 0, 0, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (posixtime (a1), 1483228799); %! assert_equal (posixtime (a2), 1483228800); %! assert_equal (posixtime (a3), 1483228800); %!assert_equal (posixtime (datetime (2016, 12, 31, 23, 59, 60.25, ... %! 'TimeZone', 'UTCLeapSeconds')), ... %! 1483228800.25) %!assert_equal (posixtime (datetime (1972, 6, 30, 23, 59, 60, ... %! 'TimeZone', 'UTCLeapSeconds')), 78796800) ## Test 'UTCLeapSeconds' -- 'datenum', 'exceltime' and 'datestr' fold backward %!test %! a1 = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds'); %! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (datenum (a2), datenum (a1)); %! assert_equal (exceltime (a2), exceltime (a1)); %! assert_equal (exceltime (a2, '1904'), exceltime (a1, '1904')); %! assert_equal (datestr (a2), '31-Dec-2016 23:59:59'); %!test %! q = datetime (2016, 12, 31, 23, 59, 60.25, 'TimeZone', 'UTCLeapSeconds'); %! a1 = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal ((datenum (q) - datenum (a1)) * 86400, 0.25, 1e-4); ## Test 'UTCLeapSeconds' -- both Julian scales stretch the day that holds an ## inserted second over its true length of 86401 seconds %!test %! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (juliandate (a2), 2457754 + 43200 / 86401, 1e-9); %! assert_equal (juliandate (a2, 'modifiedjuliandate'), ... %! 57753 + 86400 / 86401, ... %! 1e-9); %!test %! d = datetime (2016, 12, 31, 6, 0, 0, 'TimeZone', 'UTCLeapSeconds'); %! gap = (juliandate (d, 'modifiedjuliandate') ... %! - (juliandate (d) - 2400000.5)) * 86400; %! assert_equal (gap, -0.25, 1e-4); %!test %! d = datetime (2016, 12, 31, 0, 0, 0, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (juliandate (d, 'modifiedjuliandate'), ... %! juliandate (d) - 2400000.5, 1e-9); %!assert_equal (juliandate (datetime (1960, 1, 1, 0, 0, 0, ... %! 'TimeZone', 'UTCLeapSeconds')), 2436934.5) ## Test 'UTCLeapSeconds' -- the integer fixed-epoch conversions fold backward %!test %! q = datetime (2016, 12, 31, 23, 59, 60.25, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (convertTo (q, 'epochtime'), int64 (1483228799)); %! assert_equal (convertTo (q, '.net'), uint64 (636188255992500000)); %! assert_equal (convertTo (q, 'ntp'), uint64 (15857953838494384128)); %!assert_equal (convertTo (datetime (2016, 12, 31, 23, 59, 60, ... %! 'TimeZone', 'UTCLeapSeconds'), 'epochtime', ... %! 'TicksPerSecond', 1000), int64 (1483228799000)) %!assert_equal (convertTo (datetime (2017, 1, 1, ... %! 'TimeZone', 'UTCLeapSeconds'), 'epochtime'), ... %! int64 (1483228800)) ## Test 'UTCLeapSeconds' -- 'yyyymmdd' and the component readers are unaffected %!test %! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (yyyymmdd (a2), 20161231); %! assert_equal ([hour(a2), minute(a2), second(a2)], [23, 59, 60]); %! assert_equal (second (a2, 'secondofday'), 86400); %! assert_equal (week (a2), 53); %! assert_equal (week (a2, 'weekofmonth'), 5); %! assert_equal (day (a2, 'dayofyear'), 366); %! assert_equal (day (a2, 'name'), {'Saturday'}); %! assert_equal (char (timeofday (a2)), '24:00:00'); %! assert_equal (isdst (a2), false); %!assert_equal (second (datetime (2016, 12, 31, 23, 59, 60.25, ... %! 'TimeZone', 'UTCLeapSeconds'), 'secondofday'), 86400.25) ## Test 'convertTo' -- 'tt2000' is nanoseconds since the J2000 TT epoch %!test %! tz = 'UTCLeapSeconds'; %! assert_equal (convertTo (datetime (1972, 1, 1, 0, 0, 0, 'TimeZone', tz), ... %! 'tt2000'), int64 (-883655957816000000)); %! assert_equal (convertTo (datetime (1980, 1, 6, 0, 0, 0, 'TimeZone', tz), ... %! 'tt2000'), int64 (-630763148816000000)); %! assert_equal (convertTo (datetime (1999, 1, 1, 0, 0, 0, 'TimeZone', tz), ... %! 'tt2000'), int64 (-31579135816000000)); %! assert_equal (convertTo (datetime (2017, 1, 1, 0, 0, 0, 'TimeZone', tz), ... %! 'tt2000'), int64 (536500869184000000)); %! assert_equal (convertTo (datetime (2030, 1, 1, 0, 0, 0, 'TimeZone', tz), ... %! 'tt2000'), int64 (946728069184000000)); %!test %! tz = 'UTCLeapSeconds'; %! assert_equal (convertTo (datetime (2016, 12, 31, 23, 59, 60, ... %! 'TimeZone', tz), 'tt2000'), int64 (536500868184000000)); %! assert_equal (convertTo (datetime (2016, 12, 31, 23, 59, 60.123456, ... %! 'TimeZone', tz), 'tt2000'), int64 (536500868307456000)); %! assert_equal (convertTo (datetime (1972, 6, 30, 23, 59, 60, ... %! 'TimeZone', tz), 'tt2000'), int64 (-867931157816000000)); %!test %! tz = 'UTCLeapSeconds'; %! assert_equal (convertTo (datetime (1900, 1, 1, 0, 0, 0, 'TimeZone', tz), ... %! 'tt2000'), int64 (-3155716767816000000)); %! assert_equal (convertTo (datetime (1707, 9, 22, 12, 13, 15, ... %! 'TimeZone', tz), 'tt2000'), int64 (-9223371972816000000)); %! assert_equal (convertTo (datetime (2292, 4, 11, 11, 46, 7, ... %! 'TimeZone', tz), 'tt2000'), int64 (9223372036184000000)); ## Test 'convertTo' -- the tt2000 epoch itself, and round trips through it %!test %! tz = 'UTCLeapSeconds'; %! assert_equal (datevec (datetime (int64 (0), 'ConvertFrom', 'tt2000', ... %! 'TimeZone', tz)), [2000 1 1 11 58 55.816]); %! assert_equal (datevec (datetime (int64 (536500868307456000), ... %! 'ConvertFrom', 'tt2000', 'TimeZone', tz)), ... %! [2016 12 31 23 59 60.123456]); %! assert_equal (datevec (datetime (int64 ([0; 1000000000]), ... %! 'ConvertFrom', 'tt2000', 'TimeZone', tz)), ... %! [2000 1 1 11 58 55.816; 2000 1 1 11 58 56.816]); %!test %! tz = 'UTCLeapSeconds'; %! d = datetime (2016, 12, 31, 23, 59, 60.25, 'TimeZone', tz); %! assert_equal (datevec (datetime (convertTo (d, 'tt2000'), ... %! 'ConvertFrom', 'tt2000', 'TimeZone', tz)), datevec (d)); %!assert_equal (datetime (int64 (0), 'ConvertFrom', 'tt2000', ... %! 'TimeZone', 'UTCLeapSeconds').TimeZone, 'UTCLeapSeconds') ## Test 'convertTo' -- 'tt2000' error branches %!error ... %! convertTo (datetime (2016, 12, 31, 'TimeZone', 'UTC'), 'tt2000') %!error ... %! convertTo (NaT ('TimeZone', 'UTCLeapSeconds'), 'tt2000') %!error ... %! convertTo (datetime (1707, 9, 22, 12, 12, 10, 'TimeZone', ... %! 'UTCLeapSeconds'), 'tt2000') %!error ... %! convertTo (datetime (2292, 4, 11, 11, 46, 8, 'TimeZone', ... %! 'UTCLeapSeconds'), 'tt2000') %!error ... %! datetime (0, 'ConvertFrom', 'tt2000', 'TimeZone', 'UTCLeapSeconds') %!error ... %! datetime (int64 (0), 'ConvertFrom', 'tt2000', 'TimeZone', 'UTC') %!error ... %! datetime (int64 (0), 'ConvertFrom', 'tt2000') %!error ... %! datetime (int64 (-9223372036854775806), 'ConvertFrom', 'tt2000', ... %! 'TimeZone', 'UTCLeapSeconds') ## Test 'UTCLeapSeconds' -- the display format is locked to the ISO 8601 UTC ## pattern, with the fractional-second digits optional %!test %! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (a2.Format, "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'"); %! assert_equal (char (a2), '2016-12-31T23:59:60.000Z'); %! assert_equal (char (string (a2)), '2016-12-31T23:59:60.000Z'); %!test %! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! a2.Format = "uuuu-MM-dd'T'HH:mm:ss'Z'"; %! assert_equal (char (a2), '2016-12-31T23:59:60Z'); %!test %! q = datetime (2016, 12, 31, 23, 59, 60.25, 'TimeZone', 'UTCLeapSeconds'); %! q.Format = "uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'"; %! assert_equal (char (q), '2016-12-31T23:59:60.250000000Z'); %!assert_equal (char (datetime (2016, 12, 31, 23, 59, 60, ... %! 'TimeZone', 'UTCLeapSeconds', ... %! 'Format', "uuuu-MM-dd'T'HH:mm:ss'Z'")), '2016-12-31T23:59:60Z') %!assert_equal (NaT ('TimeZone', 'UTCLeapSeconds').Format, ... %! "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'") %!assert_equal (size (NaT ('TimeZone', 'UTCLeapSeconds')), [1 1]) ## Test 'UTCLeapSeconds' -- any other display format is rejected %!error ... %! d = datetime (2016, 12, 31, 'TimeZone', ... %! 'UTCLeapSeconds'); d.Format = 'uuuu-MM-dd HH:mm:ss'; %!error ... %! d = datetime (2016, 12, 31, 'TimeZone', 'UTCLeapSeconds'); %! d.Format = "uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSSS'Z'"; %!error ... %! d = datetime (2016, 12, 31, 'TimeZone', ... %! 'UTCLeapSeconds'); d.Format = 'default'; %!error ... %! datetime (2016, 12, 31, 'TimeZone', 'UTCLeapSeconds', 'Format', ... %! 'dd-MMM-uuuu') ## Test 'UTCLeapSeconds' -- leaving the zone folds the inserted second back and ## restores the data-dependent display format %!test %! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! a2.TimeZone = 'UTC'; %! assert_equal (datevec (a2), [2016 12 31 23 59 59]); %! assert_equal (a2.Format, 'dd-MMM-uuuu HH:mm:ss'); %!test %! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! a2.TimeZone = ''; %! assert_equal (datevec (a2), [2016 12 31 23 59 59]); %!test %! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! a2.TimeZone = 'America/New_York'; %! assert_equal (datevec (a2), [2016 12 31 18 59 59]); ## Test 'UTCLeapSeconds' -- entering the zone keeps the instant and takes the ## locked display format, and re-assigning the same zone changes nothing %!test %! d = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTC'); %! d.TimeZone = 'UTCLeapSeconds'; %! assert_equal (datevec (d), [2016 12 31 23 59 59]); %! assert_equal (d.Format, "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'"); %!test %! d = datetime (2016, 12, 31, 23, 59, 59, 'Format', 'dd-MMM-uuuu HH:mm:ss'); %! d.TimeZone = 'UTCLeapSeconds'; %! assert_equal (datevec (d), [2016 12 31 23 59 59]); %! assert_equal (d.Format, "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'"); %!test %! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! a2.TimeZone = 'UTCLeapSeconds'; %! assert_equal (datevec (a2), [2016 12 31 23 59 60]); ## Test 'UTCLeapSeconds' -- assigning a component re-canonicalizes on the ## leap-second timeline %!test %! d = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds'); %! d.Second = 60; %! assert_equal (datevec (d), [2016 12 31 23 59 60]); %! d.Second = 60.5; %! assert_equal (datevec (d), [2016 12 31 23 59 60.5]); %!test %! d = datetime (2016, 12, 30, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds'); %! d.Second = 60; %! assert_equal (datevec (d), [2016 12 31 0 0 0]); %!test %! d = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds'); %! d.Minute = 58; %! assert_equal (datevec (d), [2016 12 31 23 59 0]); ## Test 'UTCLeapSeconds' -- an array with leap seconds cannot be combined with ## one without them, a UTC array included %!error ... %! datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds') == datetime (2017, 1, ... %! 1, 'TimeZone', 'UTC') %!error ... %! datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds') < datetime (2017, 1, ... %! 1, 'TimeZone', 'UTC') %!error ... %! datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds') - datetime (2017, 1, ... %! 1, 'TimeZone', 'UTC') %!error ... %! ismember (datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'), ... %! datetime (2017, 1, 1, 'TimeZone', 'UTC')) %!error ... %! isbetween (datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'), ... %! datetime (2016, 12, 31, 'TimeZone', 'UTC'), datetime (2017, 1, ... %! 2, 'TimeZone', 'UTC')) %!error ... %! datetime (2017, 1, 1, 'TimeZone', ... %! 'UTCLeapSeconds'):seconds (1):datetime (2017, 1, 2, 'TimeZone', ... %! 'UTC') %!error ... %! horzcat (datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'), ... %! datetime (2017, 1, 1, 'TimeZone', 'UTC')) %!error ... %! convertTo (datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'), ... %! 'epochtime', 'Epoch', datetime (2017, 1, 1, 'TimeZone', ... %! 'UTC')) %!assert_equal (isequal (datetime (2017, 1, 1, 'TimeZone', ... %! 'UTCLeapSeconds'), ... %! datetime (2017, 1, 1, 'TimeZone', 'UTC')), false) %!assert_equal (isequal (datetime (2016, 12, 31, 23, 59, 60, ... %! 'TimeZone', 'UTCLeapSeconds'), ... %! datetime (2016, 12, 31, 23, 59, 60, ... %! 'TimeZone', 'UTCLeapSeconds')), true) ## Test 'UTCLeapSeconds' -- ordering, reductions and interpolation step through ## the inserted second %!test %! tz = 'UTCLeapSeconds'; %! a1 = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', tz); %! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', tz); %! a3 = datetime (2017, 1, 1, 0, 0, 0, 'TimeZone', tz); %! assert_equal (datevec (sort ([a3; a2; a1])), datevec ([a1; a2; a3])); %! assert_equal (datevec (max ([a1 a2 a3])), datevec (a3)); %! assert_equal (datevec (unique ([a2 a2 a1])), datevec ([a1; a2])); %! assert_equal (isbetween (a2, a1, a3), true); %! assert_equal (seconds (diff ([a1; a2; a3])), [1; 1]); %! assert_equal (isregular ([a1; a2; a3]), true); %! assert_equal (datevec (mean ([a1 a3])), datevec (a2)); %! assert_equal (datevec (a1:seconds (1):a3), datevec ([a1 a2 a3])); %! assert_equal (interp1 ([a1 a3], [0 2], a2), 1); %! assert_equal (char (std ([a1 a2 a3])), '00:00:01'); ## Test 'UTCLeapSeconds' -- 'dateshift' treats the inserted second as the last ## second of its day %!test %! tz = 'UTCLeapSeconds'; %! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', tz); %! assert_equal (datevec (dateshift (a2, 'start', 'minute')), ... %! [2016 12 31 23 59 0]); %! assert_equal (datevec (dateshift (a2, 'start', 'day')), [2016 12 31 0 0 0]); %! assert_equal (datevec (dateshift (a2, 'start', 'year')), [2016 1 1 0 0 0]); %! assert_equal (datevec (dateshift (a2, 'end', 'day')), [2017 1 1 0 0 0]); %! assert_equal (datevec (dateshift (a2, 'end', 'minute')), [2017 1 1 0 0 0]); %! assert_equal (datevec (dateshift (a2, 'start', 'minute', 'nearest')), ... %! [2017 1 1 0 0 0]); %! assert_equal (datevec (dateshift (a2, 'dayofweek', 'Monday')), ... %! [2017 1 3 0 0 0]); ## Test 'UTCLeapSeconds' -- 'caldiff' and 'between' across the inserted second %!test %! tz = 'UTCLeapSeconds'; %! a1 = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', tz); %! a3 = datetime (2017, 1, 1, 0, 0, 0, 'TimeZone', tz); %! assert_equal (char (caldiff ([a1 a3])), '0h 0m 2s'); %! assert_equal (char (between (a1, a3)), '0h 0m 2s'); ## Test 'UTCLeapSeconds' -- text is read in the one shape the array also writes %!test %! tz = 'UTCLeapSeconds'; %! assert_equal (datevec (datetime ('2016-12-31T23:59:60Z', 'TimeZone', ... %! tz)), ... %! [2016 12 31 23 59 60]); %! assert_equal (datevec (datetime ('2016-12-31T23:59:60.250Z', ... %! 'TimeZone', tz)), [2016 12 31 23 59 60.25]); %! assert_equal (datevec (datetime ('2016-12-31T23:59:59Z', 'TimeZone', ... %! tz)), ... %! [2016 12 31 23 59 59]); %!error ... %! datetime ('31-Dec-2016 23:59:59', 'TimeZone', 'UTCLeapSeconds') %!error ... %! datetime ('2016-12-31', 'TimeZone', 'UTCLeapSeconds') ## Test 'UTCLeapSeconds' -- an 'InputFormat' admits a 60th second only where one ## was really inserted %!test %! tz = 'UTCLeapSeconds'; %! F = "uuuu-MM-dd'T'HH:mm:ss'Z'"; %! assert_equal (datevec (datetime ('2016-12-31T23:59:60Z', 'InputFormat', ... %! F, ... %! 'TimeZone', tz)), [2016 12 31 23 59 60]); %! F3 = "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'"; %! assert_equal (datevec (datetime ('2016-12-31T23:59:60.250Z', ... %! 'InputFormat', F3, 'TimeZone', tz)), ... %! [2016 12 31 23 59 60.25]); %!test %! F = "uuuu-MM-dd'T'HH:mm:ss'Z'"; %! d = datetime ({'2016-12-31T23:59:60Z'; '2016-12-30T23:59:60Z'}, ... %! 'InputFormat', F, 'TimeZone', 'UTCLeapSeconds'); %! assert_equal (isnat (d), logical ([0; 1])); %! assert_equal (d.Second(1), 60); %!error ... %! datetime ('2016-12-30T23:59:60Z', 'InputFormat', ... %! "uuuu-MM-dd'T'HH:mm:ss'Z'", 'TimeZone', 'UTCLeapSeconds') %!error ... %! datetime ('2016-12-31T23:59:60Z', 'InputFormat', "uuuu-MM-dd'T'HH:mm:ss'Z'") %!error ... %! datetime ('2016-12-31T23:59:61Z', 'InputFormat', ... %! "uuuu-MM-dd'T'HH:mm:ss'Z'", 'TimeZone', 'UTCLeapSeconds') ## Test 'UTCLeapSeconds' -- before 1972 no second had been inserted, so the zone ## reads exactly as UTC %!test %! tz = 'UTCLeapSeconds'; %! assert_equal (posixtime (datetime (1960, 1, 1, 0, 0, 0, 'TimeZone', tz)), ... %! posixtime (datetime (1960, 1, 1, 0, 0, 0, 'TimeZone', 'UTC'))); %! assert_equal (posixtime (datetime (1971, 12, 31, 23, 59, 59, ... %! 'TimeZone', tz)), 63071999); %! assert_equal (posixtime (datetime (1972, 1, 1, 0, 0, 0, 'TimeZone', tz)), ... %! 63072000); ## Test 'setDefaultFormats' -- each default governs the arrays it applies to %!test %! datetime.setDefaultFormats ('default', 'yyyy-MM-dd HH:mm:ss'); %! datetime.setDefaultFormats ('defaultdate', 'yyyy/MM/dd'); %! assert_equal (char (datetime (2024, 1, 1)), '2024/01/01'); %! assert_equal (char (datetime (2024, 1, 1, 12, 30, 0)), ... %! '2024-01-01 12:30:00'); %! datetime.setDefaultFormats ('reset'); ## Test 'setDefaultFormats' -- the values choose the default, not the ## constructor call, so components given as an explicit midnight still take the ## date-only default, while a mixed array takes the date-and-time one %!test %! datetime.setDefaultFormats ('default', 'yyyy-MM-dd HH:mm:ss'); %! datetime.setDefaultFormats ('defaultdate', 'yyyy/MM/dd'); %! assert_equal (char (datetime (2024, 1, 1, 0, 0, 0)), '2024/01/01'); %! assert_equal (datetime (2024, 1, 1, 0, 0, 0).Format, 'yyyy/MM/dd'); %! m = datetime (2024, 1, [1, 2], [0, 12], 0, 0); %! assert_equal (m.Format, 'yyyy-MM-dd HH:mm:ss'); %! datetime.setDefaultFormats ('reset'); ## Test 'setDefaultFormats' -- the choice is remade on every display, so an ## array moves between the two defaults when its components change %!test %! datetime.setDefaultFormats ('default', 'yyyy-MM-dd HH:mm:ss'); %! datetime.setDefaultFormats ('defaultdate', 'yyyy/MM/dd'); %! d = datetime (2024, 1, 1); %! assert_equal (d.Format, 'yyyy/MM/dd'); %! d.Hour = 12; %! assert_equal (d.Format, 'yyyy-MM-dd HH:mm:ss'); %! d.Hour = 0; %! assert_equal (d.Format, 'yyyy/MM/dd'); %! datetime.setDefaultFormats ('reset'); ## Test 'setDefaultFormats' -- an explicitly set Format is never touched %!test %! k = datetime (2024, 1, 1, 'Format', 'dd.MM.yyyy'); %! datetime.setDefaultFormats ('default', 'yyyy-MM-dd HH:mm:ss'); %! datetime.setDefaultFormats ('defaultdate', 'yyyy/MM/dd'); %! assert_equal (k.Format, 'dd.MM.yyyy'); %! assert_equal (char (k), '01.01.2024'); %! datetime.setDefaultFormats ('reset'); ## Test 'setDefaultFormats' -- 'reset' restores both factory formats %!test %! datetime.setDefaultFormats ('default', 'yyyy-MM-dd HH:mm:ss'); %! datetime.setDefaultFormats ('defaultdate', 'yyyy/MM/dd'); %! datetime.setDefaultFormats ('reset'); %! assert_equal (char (datetime (2024, 1, 1)), '01-Jan-2024'); %! assert_equal (char (datetime (2024, 1, 1, 12, 30, 0)), ... %! '01-Jan-2024 12:30:00'); ## Test 'setDefaultFormats' -- a set format is stored as a preference so that it ## survives the session, and a factory format is stored as no preference at all %!test %! datetime.setDefaultFormats ('reset'); %! assert_equal (ispref ('datatypes', 'datetime_DefaultFormat'), false); %! assert_equal (ispref ('datatypes', 'datetime_DefaultDateFormat'), false); %! datetime.setDefaultFormats ('default', 'yyyy-MM-dd HH:mm:ss'); %! assert_equal (ispref ('datatypes', 'datetime_DefaultFormat'), true); %! assert_equal (getpref ('datatypes', 'datetime_DefaultFormat'), ... %! 'yyyy-MM-dd HH:mm:ss'); %! assert_equal (ispref ('datatypes', 'datetime_DefaultDateFormat'), false); %! datetime.setDefaultFormats ('reset'); %! assert_equal (ispref ('datatypes', 'datetime_DefaultFormat'), false); ## Test 'setDefaultFormats' -- a string scalar is accepted for both arguments %!test %! datetime.setDefaultFormats (string ('defaultdate'), string ('yyyy/MM/dd')); %! assert_equal (char (datetime (2024, 1, 1)), '2024/01/01'); %! datetime.setDefaultFormats ('reset'); ## Test 'setDefaultFormats' -- error branches %!error ... %! datetime.setDefaultFormats () %!error ... %! datetime.setDefaultFormats ('default') %!error ... %! datetime.setDefaultFormats ('bogus', 'yyyy') %!error ... %! datetime.setDefaultFormats ('preserveinput', 'yyyy') %!error ... %! datetime.setDefaultFormats (42, 'yyyy') %!error ... %! datetime.setDefaultFormats ('default', 42) %!error ... %! datetime.setDefaultFormats ('default', 'default') %!error ... %! datetime.setDefaultFormats ('default', 'yyyy-VV') ## Test 'setDefaultFormats' -- unlike MATLAB, which accepts anything in the ## second slot after 'reset' and silently resets both defaults regardless %!error ... %! datetime.setDefaultFormats ('reset', 'default') %!error ... %! datetime.setDefaultFormats ('reset', 42) ## Test 'discretize' -- explicit datetime edges, half open with the last bin ## closed at both ends %!test %! t = datetime (2024, 1, 1) + days (0:9); %! assert_equal (discretize (t, t([1 4 8 10])), [1 1 1 2 2 2 2 3 3 3]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [b, e] = discretize (t, t([1 5 10])); %! assert_equal (class (b), 'double'); %! assert_equal (class (e), 'datetime'); %! assert_equal (posixtime (e), [1704067200 1704412800 1704844800]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! assert_equal (discretize (t, t([1 5 10]), 'IncludedEdge', 'right'), ... %! [1 1 1 1 1 2 2 2 2 2]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! assert_equal (discretize (t, t([1 5 10]), [10 20]), ... %! [10 10 10 10 20 20 20 20 20 20]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! c = discretize (t, t([1 5 10]), 'categorical'); %! assert_equal (categories (c), ... %! {'[01-Jan-2024, 05-Jan-2024)'; '[05-Jan-2024, 10-Jan-2024)'}); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! c = discretize (t, t([1 5 10]), 'categorical', {'early', 'late'}); %! assert_equal (categories (c), {'early'; 'late'}); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [b, e] = discretize (t, 3); %! assert_equal (b, [1 1 1 2 2 2 2 3 3 3]); %! assert_equal (posixtime (e), ... %! [1703980800 1704326400 1704672000 1705017600]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = discretize (t, 1); %! assert_equal (posixtime (e), [1704067200 1704931200]); %!test %! u = datetime (2024, 1, 1) + hours (0:47); %! [~, e] = discretize (u, 7); %! assert_equal (posixtime (e), 1704067200 + (-1 + (0:7) * 7) * 3600); %!test %! t = reshape (datetime (2024, 1, 1) + days (0:11), 2, 3, 2); %! b = discretize (t, 3); %! assert_equal (size (b), [2 3 2]); %!test %! t = datetime (2024, 1, 1) + days ([0 0]); %! [~, e] = discretize (t, 3); %! ref = 1704067200 + [-0.5 -1/6 1/6 0.5]; %! assert_equal (max (abs (posixtime (e) - ref)) < 1e-6, true); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! t(6) = NaT; %! b = discretize (t, 3); %! assert_equal (isnan (b(6)), true); %! assert_equal (isnan (b([1 10])), [false false]); ## Test 'discretize' -- a duration or calendarDuration bin width %!test %! u = datetime (2024, 1, 1) + hours (0:47); %! [~, e] = discretize (u, days (1)); %! assert_equal (posixtime (e), [1704067200 1704153600 1704240000]); %!test %! u = datetime (2024, 1, 1) + hours (0:47); %! [~, e1] = discretize (u, days (1)); %! [~, e2] = discretize (u, 'day'); %! assert_equal (posixtime (e1), posixtime (e2)); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! assert_equal (discretize (t, calmonths (1)), ones (1, 10)); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = discretize (t, caldays (7)); %! assert_equal (posixtime (e), [1704067200 1704672000 1705276800]); ## Test 'discretize' -- named units on real calendar boundaries %!test %! u = datetime (2024, 1, 1) + hours (0:47); %! [~, e] = discretize (u, 'hour'); %! assert_equal (numel (e), 49); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = discretize (t, 'week'); %! assert_equal (posixtime (e), [1703980800 1704585600 1705190400]); %! assert_equal (weekday (datenum (2023, 12, 31)), 1); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = discretize (t, 'month'); %! assert_equal (posixtime (e), [1704067200 1706745600]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = discretize (t, 'quarter'); %! assert_equal (posixtime (e), [1704067200 1711929600]); %!test %! t = datetime (2024, 5, 17) + days (0:9); %! [~, e] = discretize (t, 'quarter'); %! [Y, M, D] = ymd (e); %! assert_equal ([Y(:), M(:), D(:)], [2024 4 1; 2024 7 1]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = discretize (t, 'year'); %! assert_equal (posixtime (e), [1704067200 1735689600]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = discretize (t, 'decade'); %! assert_equal (posixtime (e), [1577836800 1893456000]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = discretize (t, 'century'); %! [Y, M, D] = ymd (e); %! assert_equal ([Y(:), M(:), D(:)], [2000 1 1; 2100 1 1]); %!test %! t = datetime (2024, 1, 1) + minutes (0:59); %! [~, e] = discretize (t, 'minute'); %! assert_equal (numel (e), 61); ## Test 'discretize' -- zoned data bins on local midnight, so a bin holding a ## daylight-saving transition really is 23 or 25 hours long %!test %! t0 = datetime (2024, 3, 5, 'TimeZone', 'America/New_York'); %! [~, e] = discretize (t0 + days ([0 5]), 3); %! [H, MI, S] = hms (e); %! assert_equal ([H(:).', MI(:).', S(:).'], zeros (1, 12)); %! assert_equal (hours (diff (e)), [48 48 47]); %!test %! t0 = datetime (2024, 10, 30, 'TimeZone', 'America/New_York'); %! [~, e] = discretize (t0 + days ([0 9]), 3); %! assert_equal (hours (diff (e)), [72 73 72]); %!test %! t0 = datetime (2024, 3, 5); %! [~, e] = discretize (t0 + days ([0 5]), 3); %! assert_equal (hours (diff (e)), [48 48 48]); %!test %! t0 = datetime (2024, 3, 9, 12, 0, 0, 'TimeZone', 'America/New_York'); %! [~, e] = discretize (t0 + days ([0 1]), 3); %! assert_equal (hours (diff (e)), [9 9 9]); %! H = hms (e); %! assert_equal (H(:).', [11 20 6 15]); ## Test 'histcounts' -- automatic binning, counts, and the third output %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [n, e] = histcounts (t); %! assert_equal (n, [5 5]); %! assert_equal (posixtime (e), [1704067200 1704499200 1704931200]); %!test %! u = datetime (2024, 1, 1) + hours (0:47); %! [n, e] = histcounts (u); %! assert_equal (n, [12 12 12 12]); %! assert_equal (posixtime (e), 1704067200 + (0:4) * 43200); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = histcounts (t, 3); %! assert_equal (posixtime (e), ... %! [1703980800 1704326400 1704672000 1705017600]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [n, e, b] = histcounts (t, t([1 5 10])); %! assert_equal (n, [4 6]); %! assert_equal (posixtime (e), [1704067200 1704412800 1704844800]); %! assert_equal (b, [1 1 1 1 2 2 2 2 2 2]); %!test %! t = reshape (datetime (2024, 1, 1) + days (0:11), 2, 3, 2); %! [n, ~, b] = histcounts (t, 3); %! assert_equal (size (b), [2 3 2]); %! assert_equal (isrow (n), true); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! assert_equal (class (histcounts (t, 3)), 'double'); ## Test 'histcounts' -- the Name/Value options %!test %! u = datetime (2024, 1, 1) + hours (0:47); %! [~, e] = histcounts (u, 'BinWidth', days (1)); %! assert_equal (posixtime (e), [1704067200 1704153600 1704240000]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = histcounts (t, 'BinEdges', t([1 5 10])); %! assert_equal (posixtime (e), [1704067200 1704412800 1704844800]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = histcounts (t, 'NumBins', 3); %! assert_equal (posixtime (e), ... %! [1703980800 1704326400 1704672000 1705017600]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = histcounts (t, 'BinMethod', 'day'); %! assert_equal (numel (e), 11); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = histcounts (t, 'BinMethod', 'month'); %! assert_equal (posixtime (e), [1704067200 1706745600]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! assert_equal (histcounts (t, 'BinMethod', 'decade'), 10); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! assert_equal (histcounts (t, 'BinMethod', 'scott'), [5 5]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = histcounts (t, 'BinMethod', 'sturges'); %! assert_equal (posixtime (e), 1704067200 + (0:5) * 172800); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = histcounts (t, 'BinMethod', 'sqrt'); %! assert_equal (posixtime (e), ... %! [1703980800 1704240000 1704499200 1704758400 1705017600]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = histcounts (t, 'BinMethod', 'fd'); %! assert_equal (posixtime (e), [1704067200 1704499200 1704931200]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! [~, e] = histcounts (t, 'BinLimits', t([3 7])); %! assert_equal (posixtime (e), [1704240000 1704412800 1704585600]); ## Test 'histcounts' -- the accepted normalizations %!test %! t = datetime (2024, 1, 1) + days (0:9); %! assert_equal (histcounts (t, 3, 'Normalization', 'count'), [3 4 3]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! assert_equal (histcounts (t, 3, 'Normalization', 'probability'), ... %! [0.3 0.4 0.3]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! assert_equal (histcounts (t, 3, 'Normalization', 'percentage'), [30 40 30]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! assert_equal (histcounts (t, 3, 'Normalization', 'cdf'), [0.3 0.7 1]); %!test %! t = datetime (2024, 1, 1) + days (0:9); %! assert_equal (histcounts (t, 'Normalization', 'cumcount'), [5 10]); ## Test 'discretize' and 'histcounts' -- error branches %!error ... %! discretize (datetime (2024, 1, 1)) %!error ... %! discretize (datetime (2024, 1, 1) + days (0:9), 0) %!error ... %! discretize (datetime (2024, 1, 1) + days (0:9), 2.5) %!error ... %! discretize (datetime (2024, 1, 1) + days (0:9), Inf) %!error ... %! discretize (datetime (2024, 1, 1) + days (0:9), [1 2 3]) %!error ... %! discretize (datetime (2024, 1, 1) + days (0:9), {1}) %!error ... %! discretize (datetime (2024, 1, 1) + days (0:9), 'fortnight') %!error ... %! discretize (datetime (2024, 1, 1) + days (0:9), days (0)) %!error ... %! discretize (datetime (2024, 1, 1) + days (0:9), caldays (0)) %!error ... %! discretize (datetime (2024, 1, 1) + days (0:9), caldays (-1)) %!error ... %! discretize (datetime (2024, 1, 1) + days (0:9), caldays ([1, 2])) %!error ... %! histcounts (datetime (2024, 1, 1) + days (0:9), 'BinWidth', 3) %!error ... %! histcounts (datetime (2024, 1, 1) + days (0:9), 'BinMethod', 'integers') %!error ... %! histcounts (datetime (2024, 1, 1) + days (0:9), 3, 'Normalization', ... %! 'countdensity') %!error ... %! histcounts (datetime (2024, 1, 1) + days (0:9), 3, 'Normalization', 'pdf') %!error ... %! histcounts (datetime (2024, 1, 1) + days (0:9), 'BinEdges', [1 2 3]) %!error ... %! histcounts (datetime (2024, 1, 1) + days (0:9), 'BinLimits', [1 2]) %!error ... %! histcounts (datetime (2024, 1, 1) + days (0:9), 'NumBins', 0) ## Test 'discretize' -- the fixed units are anchored on LOCAL midnight and then ## stepped by a fixed span of seconds. Flooring the instant itself would anchor ## on absolute time, which lands the edges on the half hour in a zone offset by ## one. Values captured from R2024a. %!test %! t = datetime (2024, 1, 1, 'TimeZone', 'Asia/Kolkata') + hours (0:24); %! [~, e] = discretize (t, 3); %! [Y, M, D] = ymd (e); %! [H, MI, S] = hms (e); %! assert_equal ([Y(:), M(:), D(:), H(:), MI(:), S(:)], ... %! [2023 12 31 23 0 0; 2024 1 1 8 0 0; ... %! 2024 1 1 17 0 0; 2024 1 2 2 0 0]); %!test %! t = datetime (2024, 1, 1, 'TimeZone', 'Asia/Kolkata') + hours (0:5); %! [~, e] = discretize (t, 'hour'); %! [H, MI, S] = hms (e); %! assert_equal (H(:).', 0:6); %! assert_equal ([MI(:).', S(:).'], zeros (1, 14)); ## Test 'discretize' -- across a daylight-saving transition a named 'day' keeps ## every edge on local midnight, while a fixed width of days (1) is anchored on ## local midnight and then outruns the clock, its later edges reading 01:00. %!test %! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + days ([0 5]); %! [~, e] = discretize (t, 'day'); %! [Y, M, D] = ymd (e); %! [H, MI, S] = hms (e); %! assert_equal ([D(:).'], 8:14); %! assert_equal ([H(:).'], zeros (1, 7)); %!test %! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + days ([0 5]); %! [~, e] = discretize (t, days (1)); %! [Y, M, D] = ymd (e); %! [H, MI, S] = hms (e); %! assert_equal ([Y(1), M(1), D(1), H(1), MI(1), S(1)], [2024 3 8 0 0 0]); %! assert_equal (H(1:3)(:).', [0 0 0]); %! assert_equal (H(4:6)(:).', [1 1 1]); ## Test 'histcounts' -- the same daylight-saving contract on the Name/Value ## side, and it is the KIND of width that decides it, not the length: days (1), ## hours (24), minutes (1440) and seconds (86400) all bin alike as a fixed ## 24-hour span whose edges outrun the clock, while a named unit follows the ## calendar and yields a 23-hour day. Values captured from R2024a. The last ## test covers the Octave extension of taking a unit name through 'BinWidth', ## which MATLAB refuses; it agrees with 'BinMethod' by construction. %!test %! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5); %! [n, e] = histcounts (t, 'BinWidth', days (1)); %! [Y, M, D] = ymd (e); %! [H, MI, S] = hms (e); %! assert_equal (n, [1, 1, 2, 1, 1]); %! assert_equal (D(:).', 8:13); %! assert_equal (H(:).', [0, 0, 0, 1, 1, 1]); %! assert_equal (hours (diff (e)), [24, 24, 24, 24, 24]); %!test %! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5); %! [~, e1] = histcounts (t, 'BinWidth', days (1)); %! [~, e2] = histcounts (t, 'BinWidth', hours (24)); %! [~, e3] = histcounts (t, 'BinWidth', minutes (1440)); %! [~, e4] = histcounts (t, 'BinWidth', seconds (86400)); %! assert_equal (posixtime (e2), posixtime (e1)); %! assert_equal (posixtime (e3), posixtime (e1)); %! assert_equal (posixtime (e4), posixtime (e1)); %!test %! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5); %! [n, e] = histcounts (t, 'BinMethod', 'day'); %! [Y, M, D] = ymd (e); %! [H, MI, S] = hms (e); %! assert_equal (n, ones (1, 6)); %! assert_equal (D(:).', 8:14); %! assert_equal (H(:).', zeros (1, 7)); %! assert_equal (hours (diff (e)), [24, 24, 23, 24, 24, 24]); %!test %! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5); %! [n, e] = histcounts (t, 'BinMethod', 'week'); %! assert_equal (n, [2, 4]); %! assert_equal (hours (diff (e)), [168, 167]); %!test %! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5); %! [n, e] = histcounts (t, 'BinMethod', 'month'); %! assert_equal (n, 6); %! assert_equal (hours (diff (e)), 743); %!test %! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5); %! [n, e] = histcounts (t, 'BinMethod', 'auto'); %! [Y, M, D] = ymd (e); %! assert_equal (n, [3, 3]); %! assert_equal (D(:).', [8, 11, 14]); %! assert_equal (hours (diff (e)), [71, 72]); %!test %! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5); %! [n1, e1] = histcounts (t, 'BinWidth', 'day'); %! [n2, e2] = histcounts (t, 'BinMethod', 'day'); %! assert_equal (n1, n2); %! assert_equal (posixtime (e1), posixtime (e2)); ## Test 'discretize' / 'histcounts' -- a MIXED calendarDuration bin width is ## accepted, as MATLAB accepts it. The grid is anchored on 1 January of the ## smallest element's year when the width carries months and on the 1st of its ## month when it does not, and steps by the whole width, time of day included. ## Values captured from R2024a. %!test %! t = datetime (2024, 3, 1) + hours (0:6:120); %! [b, e] = discretize (t, caldays (1) + hours (2)); %! [Y, M, D] = ymd (e); %! [H, MI, S] = hms (e); %! assert_equal (D(:).', [1, 2, 3, 4, 5, 6]); %! assert_equal (H(:).', [0, 2, 4, 6, 8, 10]); %! assert_equal (b([1, 6, 10, 21]), [1, 2, 3, 5]); %!test %! t = datetime (2024, 5, 10) + caldays (0:60); %! [~, e] = discretize (t, calmonths (1) + caldays (1)); %! [Y, M, D] = ymd (e); %! assert_equal (M(:).', [5, 6, 7, 8]); %! assert_equal (D(:).', [5, 6, 7, 8]); %!test %! t = datetime (2024, 12, 10) + caldays (0:60); %! [~, e] = discretize (t, calmonths (1) + caldays (1)); %! [Y, M, D] = ymd (e); %! assert_equal (Y(:).', [2024, 2024, 2025, 2025]); %! assert_equal (M(:).', [11, 12, 1, 2]); %! assert_equal (D(:).', [11, 12, 13, 14]); %!test %! t = datetime (2024, 5, 10) + caldays (0:90); %! [~, e] = discretize (t, calmonths (1) + caldays (3)); %! [Y, M, D] = ymd (e); %! assert_equal (M(:).', [4, 5, 6, 7, 8]); %! assert_equal (D(:).', [10, 13, 16, 19, 22]); %!test %! t = datetime (2024, 5, 10, 0, 0, 0) + hours (0:6:96); %! [~, e] = discretize (t, caldays (1) + hours (2)); %! [Y, M, D] = ymd (e); %! [H, MI, S] = hms (e); %! assert_equal (D(:).', [9, 10, 11, 12, 14]); %! assert_equal (H(:).', [16, 18, 20, 22, 0]); %!test %! t = datetime (2024, 3, 1) + hours (0:6:120); %! [~, e] = discretize (t, calyears (1) + calmonths (1)); %! [Y, M, D] = ymd (e); %! assert_equal (Y(:).', [2024, 2025]); %! assert_equal (M(:).', [1, 2]); %!test %! t = datetime (2024, 3, 1) + hours (0:6:120); %! [~, e] = discretize (t, calendarDuration (0, 0, 0, 6, 0, 0)); %! [H, MI, S] = hms (e); %! assert_equal (H(1:5), [0, 6, 12, 18, 0]); ## Test 'discretize' / 'histcounts' -- a duration bin width is anchored at ## local midnight on the 1st of the month holding the smallest element and ## stepped from there in elapsed time, not from that element's own midnight. ## The two agree unless a daylight-saving transition falls between the 1st and ## the data: in Pacific/Chatham (+12:45, and a 25-hour 7 April 2024) seven ## 24-hour steps from the 1st land on the 7th at 23:00. In a sub-hour-offset ## zone without a transition in the way, every edge is still on the hour. ## Values captured from R2024a. %!test %! t = datetime (2024, 4, 8, 'TimeZone', 'Pacific/Chatham') + hours (0:23); %! [n, e] = histcounts (t, 'BinWidth', days (1)); %! [Y, M, D] = ymd (e); %! [H, MI, S] = hms (e); %! assert_equal (n, 24); %! assert_equal (D(:).', [7, 8]); %! assert_equal (H(:).', [23, 23]); %! assert_equal (hours (diff (e)), 24); %!test %! t = datetime (2024, 6, 10, 'TimeZone', 'Asia/Kolkata') + hours (0:23); %! [~, e] = histcounts (t, 'BinWidth', hours (6)); %! [H, MI, S] = hms (e); %! assert_equal (H(:).', [0, 6, 12, 18, 0]); %! assert_equal (MI(:).', zeros (1, 5)); %!test %! t = datetime (2024, 6, 10, 'TimeZone', 'Asia/Kathmandu') + hours (0:23); %! [~, e] = histcounts (t, 'BinMethod', 'hour'); %! [H, MI, S] = hms (e); %! assert_equal (MI, zeros (size (MI))); %! assert_equal (H(1), 0); %!test %! t = datetime (2024, 6, 10, 'TimeZone', 'Australia/Eucla') + hours (0:23); %! [~, e] = histcounts (t, 'BinWidth', hours (12)); %! [H, MI, S] = hms (e); %! assert_equal (H(:).', [0, 12, 0]); %! assert_equal (MI(:).', zeros (1, 3)); ## Test 'discretize' / 'histcounts' -- a bin width of several days lands on a ## grid anchored at the 1st of the month holding the smallest element, not on ## that element's own date. caldays (2) opens on an odd day of the month, ## caldays (3) on the 1st, 4th, 7th ..., and calweeks (1) seven days from the ## 1st, which is NOT the Sunday the named 'week' unit uses. A duration width ## of whole days shares the same grid. The time of day of the data does not ## move the anchor. Values captured from R2024a. %!test %! t = datetime (2024, 6, 10) + caldays (0:5); %! [n, e] = histcounts (t, 'BinWidth', caldays (2)); %! [Y, M, D] = ymd (e); %! assert_equal (n, [1, 2, 3]); %! assert_equal (D(:).', [9, 11, 13, 15]); %! assert_equal (M(:).', 6 * ones (1, 4)); %!test %! t = datetime (2024, 6, 10, 17, 0, 0) + caldays (0:5); %! [~, e] = histcounts (t, 'BinWidth', caldays (2)); %! [Y, M, D] = ymd (e); %! assert_equal (D(1), 9); %!test %! t = datetime (2024, 6, 10) + caldays (0:5); %! [n, e] = histcounts (t, 'BinWidth', caldays (3)); %! [Y, M, D] = ymd (e); %! assert_equal (n, [3, 3]); %! assert_equal (D(:).', [10, 13, 16]); %!test %! t = datetime (2024, 6, 10) + caldays (0:5); %! [n, e] = histcounts (t, 'BinWidth', days (2)); %! [Y, M, D] = ymd (e); %! assert_equal (n, [1, 2, 3]); %! assert_equal (D(:).', [9, 11, 13, 15]); %!test %! t = datetime (2024, 6, 10) + caldays ([0 7]); %! [n, e] = histcounts (t, 'BinWidth', calweeks (1)); %! [Y, M, D] = ymd (e); %! assert_equal (n, [1, 1]); %! assert_equal (D(:).', [8, 15, 22]); %!test %! t = datetime (2024, 1, 20) + calmonths ([0 1]); %! [n, e] = histcounts (t, 'BinWidth', calmonths (2)); %! [Y, M, D] = ymd (e); %! assert_equal (n, 2); %! assert_equal (M(:).', [1, 3]); %! assert_equal (D(:).', [1, 1]); %!test %! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5); %! [n, e] = histcounts (t, 'BinWidth', caldays (2)); %! [Y, M, D] = ymd (e); %! assert_equal (n, [1, 2, 3]); %! assert_equal (D(:).', [7, 9, 11, 13]); %! assert_equal (hours (diff (e)), [48, 47, 48]); ## Test 'discretize' / 'histcounts' -- a calendarDuration width covers the ## largest element even when it does not sit on an edge. The bin count was ## taken from the gap between the enclosing boundaries (midnight to midnight, or ## month index to month index), which discards the largest element's time of ## day, so a width dividing that gap exactly closed on an edge the data had ## already passed and the last element fell outside every bin. ## Zone-independent. The first test's values are captured from R2024a. %!test %! t = datetime (2024, 3, 8, 7, 0, 0, 'TimeZone', 'America/New_York') ... %! + caldays (0:5); %! [n, e, b] = histcounts (t, 'BinWidth', caldays (1)); %! [Y, M, D] = ymd (e); %! [H, MI, S] = hms (e); %! assert_equal (n, ones (1, 6)); %! assert_equal (D(:).', 8:14); %! assert_equal (H(:).', zeros (1, 7)); %! assert_equal (hours (diff (e)), [24, 24, 23, 24, 24, 24]); %! assert_equal (b, 1:6); %!test %! t = datetime (2024, 3, 8, 7, 0, 0) + caldays (0:5); %! [n, ~, b] = histcounts (t, 'BinWidth', caldays (1)); %! assert_equal (sum (n), 6); %! assert_equal (any (b == 0), false); %! assert_equal (discretize (t, caldays (1)), 1:6); %!test %! t = datetime (2024, 3, 8, 7, 0, 0) + caldays (0:13); %! [n, ~, b] = histcounts (t, 'BinWidth', calweeks (1)); %! assert_equal (sum (n), 14); %! assert_equal (any (b == 0), false); %!test %! t = datetime (2024, 3, 8) + caldays ([0 36]); %! [n, e, b] = histcounts (t, 'BinWidth', calmonths (1)); %! [Y, M, D] = ymd (e); %! assert_equal (M(:).', [3, 4, 5]); %! assert_equal (sum (n), 2); %! assert_equal (b, [1, 2]); %!test %! t = datetime (2024, 1, 15) + caldays ([0 200]); %! [n, ~, b] = histcounts (t, 'BinWidth', calquarters (1)); %! assert_equal (sum (n), 2); %! assert_equal (any (b == 0), false); %!test %! t = datetime (2024, 3, 8) + caldays (0:5); %! [~, ew] = histcounts (t, 'BinWidth', caldays (1)); %! [~, en] = histcounts (t, 'BinMethod', 'day'); %! assert_equal (numel (ew), 6); %! assert_equal (numel (en), 7); ## Test 'discretize' -- a unit given by NAME opens one more bin than the same ## unit given as a WIDTH, but only when the data end exactly on an edge. The ## two agree otherwise, which is why the difference went unseen. A duration ## array makes no such distinction; this is a datetime behaviour. Values ## captured from R2024a. %!test %! t = datetime (2024, 1, 1) + hours ([0 5]); %! [~, ew] = discretize (t, hours (1)); %! [~, en] = discretize (t, 'hour'); %! assert_equal (numel (ew), 6); %! assert_equal (numel (en), 7); %!test %! t = datetime (2024, 1, 1) + days ([0 3]); %! [~, ew] = discretize (t, days (1)); %! [~, en] = discretize (t, 'day'); %! assert_equal (numel (ew), 4); %! assert_equal (numel (en), 5); %!test %! t = datetime (2024, 1, 1) + days ([0 2.5]); %! [~, ew] = discretize (t, days (1)); %! [~, en] = discretize (t, 'day'); %! assert_equal (numel (ew), 4); %! assert_equal (numel (en), 4); %!test %! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + days ([0 5]); %! [~, ew] = discretize (t, days (1)); %! [~, en] = discretize (t, 'day'); %! assert_equal (numel (ew), 6); %! assert_equal (numel (en), 7); %!test %! d = [seconds(0), seconds(5 * 3600)]; %! [~, ew] = discretize (d, hours (1)); %! [~, en] = discretize (d, 'hour'); %! assert_equal (numel (ew), 6); %! assert_equal (numel (en), 6); ## Test 'discretize' / 'histcounts' -- the extra bin a NAMED unit opens is ## conditioned on the largest element sitting on a unit boundary of the WALL ## CLOCK, not on the elapsed span dividing exactly. The bins step in elapsed ## time, so a zone shifting by less than one unit -- Australia/Lord_Howe moves ## its clock half an hour against an hour bin -- carries the grid off the wall ## clock past the transition and the element then lands mid-unit. A whole-hour ## zone such as America/New_York keeps the grid aligned and keeps its extra ## bin, and so does a day with no transition, which is why only a sub-unit ## shift tells the two readings apart. Values captured from R2024a. %!test %! t = datetime (2024, 10, 5, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [n, e] = histcounts (t + hours (0:5), 'BinMethod', 'hour'); %! [H, MI] = hms (e); %! assert_equal (n, [1, 1, 1, 1, 1, 1]); %! assert_equal (H(:).', [20, 21, 22, 23, 0, 1, 2]); %! assert_equal (MI(:).', [0, 0, 0, 0, 0, 0, 30]); %!test %! t = datetime (2024, 10, 5, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [n, e] = histcounts (t + hours (0:6), 'BinMethod', 'hour'); %! [H, MI] = hms (e); %! assert_equal (n, [1, 1, 1, 1, 1, 2]); %! assert_equal (H(:).', [20, 21, 22, 23, 0, 1, 2]); %! assert_equal (MI(:).', [0, 0, 0, 0, 0, 0, 30]); %!test %! t = datetime (2024, 10, 5, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [n, e] = histcounts (t + hours (0:12), 'BinMethod', 'hour'); %! [H, MI] = hms (e); %! assert_equal (n, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2]); %! assert_equal (H(:).', [20, 21, 22, 23, 0, 1, 2, 3, 4, 5, 6, 7, 8]); %! assert_equal (MI(:).', [0, 0, 0, 0, 0, 0, 30, 30, 30, 30, 30, 30, 30]); %!test %! t = datetime (2024, 4, 6, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [n, e] = histcounts (t + hours (0:12), 'BinMethod', 'hour'); %! [H, MI] = hms (e); %! assert_equal (n, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2]); %! assert_equal (H(:).', [20, 21, 22, 23, 0, 1, 1, 2, 3, 4, 5, 6, 7]); %! assert_equal (MI(:).', [0, 0, 0, 0, 0, 0, 30, 30, 30, 30, 30, 30, 30]); %!test %! t = datetime (2024, 11, 2, 20, 0, 0, 'TimeZone', 'America/New_York'); %! [n, e] = histcounts (t + hours (0:12), 'BinMethod', 'hour'); %! [H, MI] = hms (e); %! assert_equal (n, ones (1, 13)); %! assert_equal (H(:).', [20, 21, 22, 23, 0, 1, 1, 2, 3, 4, 5, 6, 7, 8]); %! assert_equal (MI(:).', zeros (1, 14)); %!test %! t = datetime (2024, 10, 5, 20, 30, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [n, e] = histcounts (t + hours (0:12), 'BinMethod', 'hour'); %! [H, MI] = hms (e); %! assert_equal (n, ones (1, 13)); %! assert_equal (H(end), 9); %! assert_equal (MI(end), 30); %!test %! t = datetime (2024, 10, 5, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [b, e] = discretize (t + hours (0:6), 'hour'); %! assert_equal (b, [1, 2, 3, 4, 5, 6, 6]); %! assert_equal (numel (e), 7); ## Test 'histcounts' -- DELIBERATE DEVIATION. Where a zone shifts by less than ## one bin unit and the largest element lands mid-unit past the transition, ## MATLAB R2024a closes its last edge short of that element and drops it: 13 ## points over Australia/Lord_Howe from 20:01 return 12 bins summing to 12. ## Every element of a finite input must fall in a bin when no 'BinLimits' was ## given, so we open the bin that covers it and count all 13. See ## PENDING_NEWS.md. %!test %! t = datetime (2024, 10, 5, 20, 1, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [n, e] = histcounts (t + hours (0:12), 'BinMethod', 'hour'); %! [H, MI] = hms (e); %! assert_equal (numel (n), 13); %! assert_equal (sum (n), 13); %! assert_equal (H(end), 9); %! assert_equal (MI(end), 30); ## Test 'histcounts' / 'discretize' with a BIN COUNT -- the grid of a fixed ## sub-day unit is anchored on the WALL CLOCK, and only the step is elapsed. ## Anchoring on the seconds elapsed since local midnight put every edge half an ## hour out whenever a transition fell between that midnight and the data: ## noon on the day Australia/Lord_Howe moves its clock forward half an hour is ## 11.5 elapsed hours after midnight. Note there is no transition inside the ## data here -- it lies behind them. The span, by contrast, stays elapsed: a ## fall-back makes it an hour longer than the wall span, which is what the ## America/New_York row pins down. Values captured from R2024a. %!test %! t = datetime (2024, 10, 6, 12, 0, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [~, e] = histcounts (t + hours (0:7), 3); %! [H, MI] = hms (e); %! assert_equal (H(:).', [11, 14, 17, 20]); %! assert_equal (MI(:).', zeros (1, 4)); %! assert_equal (hours (diff (e)), [3, 3, 3]); %!test %! t = datetime (2024, 11, 2, 20, 0, 0, 'TimeZone', 'America/New_York'); %! [~, e] = histcounts (t + hours (0:9), 2); %! [H, MI] = hms (e); %! assert_equal (H(:).', [20, 1, 5]); %! assert_equal (MI(:).', zeros (1, 3)); %! assert_equal (hours (diff (e)), [5, 5]); %!test %! t = datetime (2024, 6, 10, 20, 0, 0, 'TimeZone', 'Asia/Kolkata'); %! [~, e] = histcounts (t + hours (0:7), 3); %! [H, MI] = hms (e); %! assert_equal (H(:).', [19, 22, 1, 4]); %! assert_equal (MI(:).', zeros (1, 4)); %!test %! t = datetime (2024, 9, 28, 20, 0, 0, 'TimeZone', 'Pacific/Chatham'); %! [~, e] = histcounts (t + hours (0:7), 3); %! [H, MI] = hms (e); %! assert_equal (H(:).', [19, 22, 1, 5]); %! assert_equal (MI(:).', zeros (1, 4)); ## Test 'histcounts' with a BIN COUNT -- where the zone's shift is not a whole ## number of grid units, the span is read off the WALL clock rather than the ## elapsed one. Australia/Lord_Howe moved its clock a whole hour until 1985 ## and half an hour since, so the same zone over the same dates and spans takes ## the elapsed reading in 1983 and the wall reading today. Values captured ## from R2024a. %!test %! t = datetime (1983, 10, 29, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [~, e] = histcounts (t + hours (0:7), 2); %! [H, MI] = hms (e); %! assert_equal (H(:).', [20, 0, 5]); %! assert_equal (MI(:).', zeros (1, 3)); %! assert_equal (hours (diff (e)), [4, 4]); %!test %! t = datetime (2024, 10, 5, 20, 30, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [~, e] = histcounts (t + hours (0:7), 2); %! [H, MI] = hms (e); %! assert_equal (H(:).', [19, 0, 5]); %! assert_equal (MI(:).', [0, 0, 30]); %! assert_equal (hours (diff (e)), [5, 5]); %!test %! t = datetime (2024, 3, 24, 0, 0, 0, 'TimeZone', 'Antarctica/Troll'); %! [~, e] = histcounts (t + hours (0:7), 2); %! [H, MI] = hms (e); %! assert_equal (MI(:).', zeros (1, 3)); %! assert_equal (hours (diff (e)), [4, 4]); ## Test 'histcounts' with a BIN COUNT -- where the grid point that anchors the ## bins names a wall time inside the gap a spring-forward opens, it steps BACK ## out of the gap rather than forward, and the remaining edges are counted from ## there in elapsed time. Data opening at 02:30 on Australia/Lord_Howe's ## spring-forward day anchor on 02:00, which does not exist; that resolves to ## 01:30, and the edge one unit earlier is 00:30, not the 01:00 its own grid ## index would name. Values captured from R2024a. %!test %! t = datetime (2024, 10, 6, 2, 30, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [~, e] = histcounts (t + hours (0:7), 2); %! [H, MI] = hms (e); %! assert_equal (H(:).', [1, 6, 10]); %! assert_equal (MI(:).', [30, 0, 0]); %! assert_equal (hours (diff (e)), [4, 4]); %!test %! t = datetime (2024, 10, 6, 2, 30, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [~, e] = histcounts (t + hours (0:7), 3); %! [H, MI] = hms (e); %! assert_equal (H(:).', [0, 4, 7, 10]); %! assert_equal (MI(:).', [30, 0, 0, 0]); %! assert_equal (hours (diff (e)), [3, 3, 3]); ## Test 'histcounts' with a BIN COUNT -- a transition INSIDE the data whose ## shift is not a whole number of grid units moves the whole grid one unit ## earlier. A whole-unit shift does not, which is why the Chatham and ## New_York rows above keep their place. The move is conditioned on the parity ## of the span and of the grid: the s = 7 row takes it and the s = 10 row does ## not. This is MATLAB's placement, established empirically by sweeping the ## data window across a spring-forward and a fall-back alike; see STATUS. ## Values captured from R2024a. %!test %! t = datetime (2024, 10, 5, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [~, e] = histcounts (t + hours (0:7), 3); %! [H, MI] = hms (e); %! assert_equal (H(:).', [18, 21, 0, 3]); %! assert_equal (MI(:).', [0, 0, 0, 30]); %! assert_equal (hours (diff (e)), [3, 3, 3]); %!test %! t = datetime (2024, 10, 5, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [~, e] = histcounts (t + hours (0:10), 2); %! [H, MI] = hms (e); %! assert_equal (H(:).', [19, 1, 7]); %! assert_equal (MI(:).', [0, 0, 30]); %! assert_equal (hours (diff (e)), [6, 6]); ## Test 'ConvertFrom' -- the six conversions that 'convertTo' performs and the ## constructor could not invert. Tick zero of each scale, captured from R2024a. %!test %! [Y, M, D] = ymd (datetime (0, 'ConvertFrom', 'excel1904')); %! assert_equal ([Y, M, D], [1904, 1, 1]); %!test %! [Y, M, D] = ymd (datetime (0, 'ConvertFrom', 'juliandate')); %! [h, m, s] = hms (datetime (0, 'ConvertFrom', 'juliandate')); %! assert_equal ([Y, M, D, h, m, s], [-4713, 11, 24, 12, 0, 0]); %!test %! [Y, M, D] = ymd (datetime (0, 'ConvertFrom', 'modifiedjuliandate')); %! assert_equal ([Y, M, D], [1858, 11, 17]); ## Test 'ConvertFrom' -- each new conversion inverts 'convertTo' of the same ## name. Reference values captured from R2024a for 2024-03-15T12:34:56.789Z. %!test %! t = datetime (2024, 3, 15, 12, 34, 56, 'TimeZone', ... %! 'UTC') + milliseconds (789); %! assert_equal (convertTo (t, 'excel1904'), 43904.52426839120017, 1e-9); %! assert_equal (convertTo (t, 'juliandate'), 2460385.024268391076, 1e-9); %! assert_equal (convertTo (t, 'ntfs'), uint64 (133549796967890000)); ## Each scale inverts its own 'convertTo' %!test %! t = datetime (2024, 3, 15, 12, 34, 56, 'TimeZone', ... %! 'UTC') + milliseconds (789); %! r = datetime (convertTo (t, 'excel1904'), 'ConvertFrom', 'excel1904', ... %! 'TimeZone', 'UTC'); %! assert_equal (posixtime (r), posixtime (t), 1e-4); %!test %! t = datetime (2024, 3, 15, 12, 34, 56, 'TimeZone', ... %! 'UTC') + milliseconds (789); %! r = datetime (convertTo (t, 'juliandate'), 'ConvertFrom', 'juliandate', ... %! 'TimeZone', 'UTC'); %! assert_equal (posixtime (r), posixtime (t), 1e-4); %!test %! t = datetime (2024, 3, 15, 12, 34, 56, 'TimeZone', ... %! 'UTC') + milliseconds (789); %! r = datetime (convertTo (t, 'modifiedjuliandate'), 'ConvertFrom', ... %! 'modifiedjuliandate', 'TimeZone', 'UTC'); %! assert_equal (posixtime (r), posixtime (t), 1e-4); %!test %! t = datetime (2024, 3, 15, 12, 34, 56, 'TimeZone', ... %! 'UTC') + milliseconds (789); %! r = datetime (convertTo (t, 'ntp'), 'ConvertFrom', 'ntp', 'TimeZone', 'UTC'); %! assert_equal (posixtime (r), posixtime (t), 1e-4); %!test %! t = datetime (2024, 3, 15, 12, 34, 56, 'TimeZone', ... %! 'UTC') + milliseconds (789); %! r = datetime (convertTo (t, 'ntfs'), 'ConvertFrom', 'ntfs', 'TimeZone', ... %! 'UTC'); %! assert_equal (posixtime (r), posixtime (t), 1e-4); %!test %! t = datetime (2024, 3, 15, 12, 34, 56, 'TimeZone', ... %! 'UTC') + milliseconds (789); %! r = datetime (convertTo (t, '.net'), 'ConvertFrom', '.net', 'TimeZone', ... %! 'UTC'); %! assert_equal (posixtime (r), posixtime (t), 1e-4); ## Test 'ConvertFrom' -- the fixed-epoch counts require uint64, as MATLAB ## requires, rather than rounding a double: an NTP stamp runs past the 2^53 a ## double holds exactly. %!error ... %! datetime (0, 'ConvertFrom', 'ntp') %!error ... %! datetime (0, 'ConvertFrom', 'ntfs') %!error ... %! datetime (0, 'ConvertFrom', '.net') ## Test 'ConvertFrom' -- a conversion that folded an inserted leap second away ## cannot recover it and returns the preceding second, while the Julian scales ## measure against the day's own length and do return 23:59:60. Values ## captured from R2024a, except the Julian scales on the leap day, where ## MATLAB does not invert its own convertTo -- see the deviation note in the ## datetime docstring. %!test %! t = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds') ... %! + seconds ([0 1 2]); %! r = datetime (convertTo (t, 'ntp'), 'ConvertFrom', 'ntp', ... %! 'TimeZone', 'UTCLeapSeconds'); %! [~, ~, ~, ~, ~, sec] = datevec (r); %! assert_equal (round (sec(:).'), [59, 59, 0]); %!test %! t = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds') ... %! + seconds ([0 1 2]); %! r = datetime (convertTo (t, 'ntfs'), 'ConvertFrom', 'ntfs', ... %! 'TimeZone', 'UTCLeapSeconds'); %! [~, ~, ~, ~, ~, sec] = datevec (r); %! assert_equal (round (sec(:).'), [59, 59, 0]); %!test %! t = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds') ... %! + seconds ([0 1 2]); %! r = datetime (convertTo (t, '.net'), 'ConvertFrom', '.net', ... %! 'TimeZone', 'UTCLeapSeconds'); %! [~, ~, ~, ~, ~, sec] = datevec (r); %! assert_equal (round (sec(:).'), [59, 59, 0]); %!test %! t = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds') ... %! + seconds ([0 1 2]); %! r = datetime (convertTo (t, 'excel1904'), 'ConvertFrom', 'excel1904', ... %! 'TimeZone', 'UTCLeapSeconds'); %! [~, ~, ~, ~, ~, sec] = datevec (r); %! assert_equal (round (sec(:).'), [59, 59, 0]); %!test %! t = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds') ... %! + seconds ([0 1 2]); %! r = datetime (convertTo (t, 'modifiedjuliandate'), 'ConvertFrom', ... %! 'modifiedjuliandate', 'TimeZone', 'UTCLeapSeconds'); %! [~, ~, ~, ~, ~, sec] = datevec (r); %! assert_equal (sec(1:2).', [59, 60], 1e-4); ## Test 'ConvertFrom' -- the Modified Julian scale is counted from its own ## epoch, not by subtracting 2400000.5 from the Julian one, which would lose ## about 14 microseconds to cancellation. The round-trip is exact. %!test %! t = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC'); %! assert_equal (convertTo (t, 'modifiedjuliandate'), ... %! 40587 + 1730611800 / 86400, 0); %! r = datetime (convertTo (t, 'modifiedjuliandate'), 'ConvertFrom', ... %! 'modifiedjuliandate', 'TimeZone', 'UTC'); %! assert_equal (posixtime (r), posixtime (t), 0); ## Test 'discretize' / 'histcounts' -- a fixed bin width is anchored on the ## start of the largest clock unit that does not exceed it: a whole number of ## days on the 1st of the month, an hour or more on local midnight, a minute or ## more on the hour, and anything shorter on the minute. A width that divides ## its unit lands on the same edges either way, so only one that does not -- ## minutes (50) or minutes (25) against an hour -- shows the rung. Values ## captured from R2024a. %!test %! t = datetime (2024, 6, 15, 7, 0, 0) + hours (0:5); %! [~, e] = histcounts (t, 'BinWidth', minutes (50)); %! [H, MI] = hms (e); %! assert_equal (numel (e), 7); %! assert_equal ([H(1), MI(1)], [7, 0]); %!test %! t = datetime (2024, 6, 15, 7, 37, 0) + minutes (0:10); %! [~, e] = histcounts (t, 'BinWidth', minutes (25)); %! [H, MI] = hms (e); %! assert_equal ([H(1), MI(1)], [7, 25]); %!test %! t = datetime (2024, 6, 15, 23, 40, 0) + minutes (0:10); %! [~, e] = histcounts (t, 'BinWidth', minutes (40)); %! [H, MI] = hms (e); %! assert_equal ([H(1), MI(1)], [23, 40]); %!test %! t = datetime (2024, 6, 15, 7, 23, 17) + minutes (0:10); %! [~, e] = histcounts (t, 'BinWidth', seconds (7)); %! [H, MI, S] = hms (e); %! assert_equal ([H(1), MI(1), S(1)], [7, 23, 14]); %!test %! t = datetime (2024, 6, 15, 7, 0, 0) + hours (0:5); %! [~, e] = histcounts (t, 'BinWidth', minutes (90)); %! [H, MI] = hms (e); %! assert_equal ([H(1), MI(1)], [6, 0]); %!test %! t = datetime (2024, 6, 15, 7, 0, 0) + hours (0:5); %! [~, eh] = histcounts (t, 'BinWidth', hours (5)); %! [~, ed] = histcounts (datetime (2024, 6, 2, 7, 0, 0) + hours (0:5), ... %! 'BinWidth', hours (5)); %! H1 = hms (eh); %! H2 = hms (ed); %! assert_equal ([H1(1), H2(1)], [5, 5]); ## Test 'histcounts' with a BIN COUNT of one -- both edges of a single bin are ## grid points of the WALL clock, so the bin spans a whole number of units of ## the local clock and its elapsed length absorbs whatever the zone did in ## between. Two wall hours across Australia/Lord_Howe's spring forward are 90 ## elapsed minutes; seven wall hours across its fall-back are 450. From two ## bins up the interior edges step in elapsed time instead and leave the wall ## clock, which the rows above pin. Values captured from R2024a. %!test %! t = datetime (2024, 10, 6, 1, 40, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [~, e] = histcounts (t + minutes ([0 20]), 1); %! [H, MI] = hms (e); %! assert_equal (H(:).', [1, 3]); %! assert_equal (MI(:).', [0, 0]); %! assert_equal (minutes (diff (e)), 90); %!test %! t = datetime (2024, 4, 7, 0, 0, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [~, e] = histcounts (t + hours ([0 7]), 1); %! [H, MI] = hms (e); %! assert_equal (H(:).', [0, 7]); %! assert_equal (MI(:).', [0, 0]); %! assert_equal (minutes (diff (e)), 450); %!test %! t = datetime (2024, 10, 6, 0, 0, 0, 'TimeZone', 'Australia/Lord_Howe'); %! [~, e] = histcounts (t + hours ([0 7]), 1); %! [H, MI] = hms (e); %! assert_equal (H(:).', [0, 8]); %! assert_equal (MI(:).', [0, 0]); %! assert_equal (minutes (diff (e)), 450); ## Test that the second argument is a bin count or a set of edges, never a bin ## width. A duration left standing there dispatched the internal numeric call ## to 'duration.histcounts' and answered with an error from inside it. %!error ... %! histcounts (datetime (2024, 1, 1) + days ((0:9)'), days (2)) %!error ... %! histcounts (datetime (2024, 1, 1) + days ((0:9)'), [1, 2, 3]) %!assert_equal (histcounts (datetime (2024, 1, 1) + days ((0:9)'), ... %! 'BinWidth', days (2)), [2, 2, 2, 2, 2]) %!assert_equal (histcounts (datetime (2024, 1, 1) + days ((0:9)'), 5), ... %! [2, 2, 2, 2, 2]) %!assert_equal (discretize (datetime (2024, 1, 1) + days ((0:9)'), ... %! days (2)), ... %! [1; 1; 2; 2; 3; 3; 4; 4; 5; 5]) ## Test 'histcounts' with a BIN COUNT -- the empirical placement correction is ## applied only where a zone's shift is exactly HALF the bin unit, the domain it ## was measured on (Australia/Lord_Howe's half hour against an hour bin). At ## any other fraction the plain rule stands. Asia/Singapore moved +07:00 to ## +07:20 on 1933-01-01, a third of an hour, and firing the correction there is ## measurably wrong -- confining it recovers 60 rows of the probe corpus. These ## two agree with R2024a; others across that transition still do not, which is ## a documented deviation and STATUS item 0h. %!test %! t = datetime (1932, 12, 31, 20, 0, 0, 'TimeZone', 'Asia/Singapore'); %! [~, e] = histcounts (t + hours ([0 7]), 2); %! [H, MI] = hms (e); %! assert_equal (H(:).', [20, 0, 4]); %! assert_equal (MI(:).', [0, 20, 20]); %! assert_equal (minutes (diff (e)), [240, 240]); %!test %! t = datetime (1932, 12, 31, 20, 0, 0, 'TimeZone', 'Asia/Singapore'); %! [~, e] = histcounts (t + hours ([0 7]), 3); %! [H, MI] = hms (e); %! assert_equal (H(:).', [19, 22, 1, 4]); %! assert_equal (MI(:).', [0, 0, 20, 20]); %! assert_equal (minutes (diff (e)), [180, 180, 180]); ## Test 'ConvertFrom' 'epochtime' -- 'Epoch' and 'TicksPerSecond'. Whether the ## count measures elapsed or wall-clock time follows from the epoch: both sides ## zoned makes it elapsed from a known instant, so a UTC epoch agrees with ## 'posixtime'; otherwise the epoch is a wall clock, its zone dropped, which is ## why the default (an unzoned 1970-01-01) does not agree. Values captured ## from R2024a. %!test %! p = 1730611800; %! r = datetime (p, 'ConvertFrom', 'epochtime', 'Epoch', ... %! datetime (1970, 1, 1, 'TimeZone', 'UTC'), ... %! 'TimeZone', 'America/New_York'); %! assert_equal (posixtime (r), p); %! [H, MI] = hms (r); %! assert_equal ([H, MI], [1, 30]); %!test %! p = 1730611800; %! r = datetime (p, 'ConvertFrom', 'epochtime', 'Epoch', ... %! datetime (1970, 1, 1), 'TimeZone', 'America/New_York'); %! assert_equal (posixtime (r), p + 18000); %! [H, MI] = hms (r); %! assert_equal ([H, MI], [5, 30]); %!test %! r = datetime (1730611800, 'ConvertFrom', 'epochtime', 'Epoch', ... %! datetime (2000, 1, 1)); %! [Y, M, D] = ymd (r); %! assert_equal ([Y, M, D], [2054, 11, 3]); %!test %! r = datetime (1730611800123, 'ConvertFrom', 'epochtime', ... %! 'TicksPerSecond', 1000); %! assert_equal (posixtime (r), 1730611800.123, 1e-6); %!test %! r = datetime (1730611800, 'ConvertFrom', 'epochtime', 'Epoch', ... %! datetime (1970, 1, 1, 'TimeZone', 'America/New_York')); %! assert_equal (posixtime (r), 1730611800); %! assert_equal (isempty (r.TimeZone), true); ## Test 'ConvertFrom' -- 'Epoch' and 'TicksPerSecond' bear on 'epochtime' alone ## and are ignored by every other conversion, as MATLAB ignores them. %!test %! r = datetime (739559, 'ConvertFrom', 'datenum', 'Epoch', datetime (1970, ... %! 1, 1)); %! [Y, M, D] = ymd (r); %! assert_equal ([Y, M, D], [2024, 11, 3]); %!test %! r = datetime (1730611800, 'ConvertFrom', 'posixtime', 'TicksPerSecond', ... %! 1000); %! assert_equal (posixtime (r), 1730611800); ## Test 'ConvertFrom' 'epochtime' -- DELIBERATE DEVIATION on a leap-second ## array. Both operands are counted on the same timeline here; MATLAB folds the ## array onto the POSIX one and the epoch onto the leap-second one, so its ## answer runs long by the seconds inserted before that epoch -- it returns ## 2017-01-01T00:00:25 where both consistent readings give 23:59:59. The ## mirror of the deviation already documented for convertTo. %!test %! E = datetime (2016, 12, 31, 'TimeZone', 'UTCLeapSeconds'); %! r = datetime (int64 (86399), 'ConvertFrom', 'epochtime', 'Epoch', E, ... %! 'TimeZone', 'UTCLeapSeconds'); %! [Y, M, D] = ymd (r); %! [H, MI, S] = hms (r); %! assert_equal ([Y, M, D, H, MI, S], [2016, 12, 31, 23, 59, 59]); %!error ... %! datetime (0, 'ConvertFrom', 'epochtime', 'Epoch', 5) %!error ... %! datetime (0, 'ConvertFrom', 'epochtime', 'TicksPerSecond', -1) ## Restore whatever default formats the user had before this file ran. %!test %! datetime.setDefaultFormats ('reset'); %! if (! isempty (savedDefaultFormat)) %! datetime.setDefaultFormats ('default', savedDefaultFormat); %! endif %! if (! isempty (savedDefaultDateFormat)) %! datetime.setDefaultFormats ('defaultdate', savedDefaultDateFormat); %! endif ## the constructor: 'ConvertFrom' kinds that need a numeric input, and the ## mutual exclusion with 'InputFormat' %!error ... %! datetime ('abc', 'ConvertFrom', 'epochtime', 'TicksPerSecond', 1); %!error ... %! datetime ('abc', 'ConvertFrom', 'juliandate'); %!error ... %! datetime (1, 'ConvertFrom', 'posixtime', 'InputFormat', 'yyyy'); ## cellstr, size and datevec validate their own arguments %!error ... %! D = datetime (2020, 1, 1); %! cellstr (D, 5); %!error ... %! V = datetime (2020, 1, 1) + days ([0; 1]); %! [r, c] = size (V, 1); %!error ... %! D = datetime (2020, 1, 1); %! [a, b, c, e, f, g, h] = datevec (D); ## convertTo takes a character DATETYPE and a scalar datetime Epoch %!error ... %! D = datetime (2020, 1, 1); %! convertTo (D, 5); %!error ... %! D = datetime (2020, 1, 1); %! convertTo (D, 'epochtime', 'Epoch', 5); ## issortedrows and sortrows share their shape and argument checks %!error ... %! issortedrows (reshape (datetime (2020, 1, 1) + days (0:7), 2, 2, 2)); %!error ... %! M = datetime (2020, 1, 1) + days ([0, 1; 2, 3]); %! issortedrows (M, 1, {'ascend'}, 5); %!error ... %! M = datetime (2020, 1, 1) + days ([0, 1; 2, 3]); %! issortedrows (M, 1, 5); %!error ... %! sortrows (reshape (datetime (2020, 1, 1) + days (0:7), 2, 2, 2)); %!error ... %! M = datetime (2020, 1, 1) + days ([0, 1; 2, 3]); %! sortrows (M, 'MissingPlacement'); %!error ... %! M = datetime (2020, 1, 1) + days ([0, 1; 2, 3]); %! sortrows (M, 1, {'ascend'}, 5); %!error ... %! M = datetime (2020, 1, 1) + days ([0, 1; 2, 3]); %! sortrows (M, 1, 5); ## ismissing takes a vector indicator only %!error ... %! V = datetime (2020, 1, 1) + days ([0; 1]); %! M = datetime (2020, 1, 1) + days ([0, 1; 2, 3]); %! ismissing (V, M); ## the set operations do not implement the 'legacy' option %!error ... %! V = datetime (2020, 1, 1) + days ([0; 1]); %! union (V, V, 'legacy'); %!error ... %! V = datetime (2020, 1, 1) + days ([0; 1]); %! setdiff (V, V, 'legacy'); %!error ... %! V = datetime (2020, 1, 1) + days ([0; 1]); %! setxor (V, V, 'legacy'); ## colon, caldiff, between and dateshift validate their own arguments %!error ... %! D = datetime (2020, 1, 1); %! colon (NaT, days (1), D); %!error ... %! V = datetime (2020, 1, 1) + days ([0; 1]); %! caldiff (V, {1}); %!error ... %! D = datetime (2020, 1, 1); %! between (D, D, 'y', 5); %!error ... %! D = datetime (2020, 1, 1); %! dateshift (D, 'start', 'day', 1, 2); ## subsasgn: only '()' reaches the values, and the assigned value and the ## named properties are checked %!error ... %! D = datetime (2020, 1, 1); %! subsasgn (D, substruct ('.', 'Format', '.', 'x'), 1); %!error ... %! D = datetime (2020, 1, 1); %! subsasgn (D, substruct ('{}', {1}), D); %!error ... %! D = datetime (2020, 1, 1); %! subsasgn (D, substruct ('()', {1}), {1}); %!error ... %! D = datetime (2020, 1, 1); %! subsasgn (D, substruct ('.', 'Format'), 5); %!error ... %! D = datetime (2020, 1, 1); %! subsasgn (D, substruct ('.', 'Nope'), 5); ## Test 'subsref' names a property with a string scalar %!test %! D = datetime (2020, 1, 1); %! s = struct ('type', '.', 'subs', string ('Format')); %! assert_equal (subsref (D, s), D.Format); ## Test 'subsasgn' names a property with a string scalar %!test %! D = datetime (2020, 1, 1); %! s = struct ('type', '.', 'subs', string ('Format')); %! assert_equal (subsasgn (D, s, 'yyyy').Format, 'yyyy'); ## Test 'subsref' refuses a property name that is not a single row %!error ... %! subsref (datetime (2020, 1, 1), struct ('type', '.', 'subs', ['ab'; 'cd'])); ## Test 'subsasgn' refuses a property name that is not a single row %!error ... %! subsasgn (datetime (2020, 1, 1), struct ('type', '.', 'subs', ''), 5); ## min and max share one implementation; its two-array and option checks %!error ... %! V = datetime (2020, 1, 1) + days ([0; 1]); %! [m, i] = min (V, V); %!error ... %! V = datetime (2020, 1, 1) + days ([0; 1]); %! min (V, {1}); %!error ... %! D = datetime (2020, 1, 1); %! min (D, datetime (2020, 1, 1, 'TimeZone', 'UTC')); %!error ... %! V = datetime (2020, 1, 1) + days ([0; 1]); %! min (V, V, 'nope'); %!error ... %! V = datetime (2020, 1, 1) + days ([0; 1]); %! min (V, [], -1); %!error ... %! V = datetime (2020, 1, 1) + days ([0; 1]); %! min (V, [], 'nope'); %!error ... %! V = datetime (2020, 1, 1) + days ([0; 1]); %! min (V, [], {1}); ## a 'UTCLeapSeconds' array accepts only its own display format %!error ... %! L = datetime (2020, 1, 1, 'TimeZone', 'UTCLeapSeconds'); %! subsasgn (L, substruct ('.', 'Format'), 'yyyy'); pr0m1th3as-datatypes-9450b29/inst/tests/dictionary.m-tst000066400000000000000000011132251527370255000231550ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## Expected values measured on MATLAB R2024a. ################################################################################ ## ** Create and describe 'dictionary' ** ## ################################################################################ ## Available Methods ## ## ## ## 'dictionary' 'types' 'isConfigured' 'numEntries' ## ## 'numel' 'length' 'size' 'ndims' ## ## 'isempty' 'dispstrings' ## ## ## ################################################################################ %!shared U, C %! U = dictionary (); %! C = configureDictionary ('string', 'double'); %!assert_equal (class (dictionary ()), 'dictionary') %!assert_equal (isConfigured (dictionary ()), false) %!test %! d = dictionary (string ({'delta'; 'alpha'; 'charlie'; 'bravo'}), ... %! [4; 1; 3; 2]); %! assert_equal (keys (d), string ({'delta'; 'alpha'; 'charlie'; 'bravo'})); %! assert_equal (values (d), [4; 1; 3; 2]); %!test %! d = dictionary ([5, 3, 9, 1], [50, 30, 90, 10]); %! assert_equal (keys (d), [5; 3; 9; 1]); %!test %! ## A repeated key keeps its first position and takes its last value %! d = dictionary (string ({'a'; 'b'; 'a'}), [1; 2; 3]); %! assert_equal (keys (d), string ({'a'; 'b'})); %! assert_equal (values (d), [3; 2]); %!test %! d = dictionary (string ({'a', 'b', 'c'}), [1, 2, 3]); %! assert_equal (keys (d), string ({'a'; 'b'; 'c'})); %! assert_equal (values (d), [1; 2; 3]); %!test %! d = dictionary (string ({'a', 'b'; 'c', 'd'}), [1, 2; 3, 4]); %! assert_equal (keys (d), string ({'a'; 'c'; 'b'; 'd'})); %! assert_equal (values (d), [1; 3; 2; 4]); %!test %! d = dictionary (string ({'a'; 'b'}), 1); %! assert_equal (values (d), [1; 1]); %!test %! d = dictionary ('a', 1, 'b', 2); %! assert_equal (keys (d), string ({'a'; 'b'})); %! assert_equal (values (d), [1; 2]); %!test %! d = dictionary (string ('a'), 1, 'b', int8 (2)); %! [kt, vt] = types (d); %! assert_equal (kt, string ('string')); %! assert_equal (vt, string ('double')); %! assert_equal (values (d), [1; 2]); %!test %! d = dictionary (1, 2, string ('a'), 3); %! assert_equal (keys (d), [1; NaN]); %!test %! d = dictionary (string ({'a'; 'b'}), [1; 2], string ({'c'; 'a'}), [3; 4]); %! assert_equal (keys (d), string ({'a'; 'b'; 'c'})); %! assert_equal (values (d), [4; 2; 3]); %!test %! d = dictionary ('abc', 1); %! assert_equal (keys (d), string ('abc')); %!test %! d = dictionary (['ab'; 'cd'], [1; 2]); %! assert_equal (keys (d), string ({'ab'; 'cd'})); %!test %! d = dictionary ('abc', 'xyz'); %! [kt, vt] = types (d); %! assert_equal (vt, string ('string')); %! assert_equal (values (d), string ('xyz')); %!test %! d = dictionary (['ab'; 'cd'], ['xy'; 'zw']); %! assert_equal (values (d), string ({'xy'; 'zw'})); %!test %! d = dictionary (string ({'a'; 'b'}), 'xy'); %! assert_equal (values (d), string ({'xy'; 'xy'})); %!test %! d = dictionary ('', 1); %! assert_equal (keys (d), string ('')); %!test %! d = dictionary ([], 1); %! [kt, vt] = types (d); %! assert_equal (kt, string ('double')); %! assert_equal (vt, string ('double')); %! assert_equal (numEntries (d), 0); %!test %! d = dictionary (string (cell (0, 0)), 5); %! assert_equal (isConfigured (d), true); %! assert_equal (numEntries (d), 0); %!test %! d = dictionary ({'a'; 'b'}, [1; 2]); %! assert_equal (types (d), string ('cell')); %! assert_equal (keys (d), {'a'; 'b'}); %!test %! d = dictionary (string ({'a'; 'b'}), {1; 2}); %! assert_equal (values (d), {1; 2}); %!test %! d = dictionary (string ({'a'; 'b'}), {1}); %! assert_equal (values (d), {1; 1}); %!test %! d = dictionary ([0, -0], [1, 2]); %! assert_equal (numEntries (d), 1); %! assert_equal (values (d), 2); %!test %! d = dictionary ([1, NaN, 2, NaN], [10, 20, 30, 40]); %! assert_equal (values (d), [10; 40; 30]); %!test %! d = dictionary ([1, 1+2i], [5, 6]); %! assert_equal (keys (d), [1; 1+2i]); %!test %! d = dictionary (sparse ([1, 2]), [5, 6]); %! assert_equal (issparse (keys (d)), false); %! assert_equal (keys (d), [1; 2]); %!test %! d = dictionary (string ('t'), table (1)); %! assert_equal (types (d), string ('string')); %! assert_equal (isequal (values (d), table (1)), true); %!test %! ## A table of any size is one value, given here to both keys %! d = dictionary (string ({'a'; 'b'}), table ([1; 2])); %! v = values (d, 'cell'); %! assert_equal (numEntries (d), 2); %! assert_equal (isequal (v{2}, table ([1; 2])), true); %!test %! d = dictionary (string ('t'), timetable (seconds ([1; 2]), [1; 2])); %! assert_equal (types (d), string ('string')); %! assert_equal (numEntries (d), 1); %!test %! d = dictionary (@sin, 1); %! assert_equal (types (d), string ('function_handle')); %! assert_equal (numEntries (d), 1); %!test %! d = dictionary (string ({'a'; 'b'}), struct ('x', {1; 2})); %! assert_equal (values (d), struct ('x', {1; 2})); %!test %! d = dictionary (datetime (2024, 1, [1; 2], 'TimeZone', 'Europe/Athens'), ... %! [1; 2]); %! k = keys (d); %! assert_equal (k.TimeZone, 'Europe/Athens'); %!test %! d = dictionary (categorical ({'a'; 'b'}), [1; 2], ... %! categorical ({'c'}, {'c', 'z'}), 3); %! assert_equal (categories (keys (d)), {'a'; 'b'; 'c'; 'z'}); %!error dictionary (1) %!error dictionary (1, 2, 3) %!error ... %! dictionary (string ({'a'; 'b'; 'c'}), [1; 2]) %!error ... %! dictionary (string ({'a', 'b'}), [1; 2]) %!error ... %! dictionary (string ('a'), ['xy'; 'zw']) %!error ... %! dictionary ([], 1:3) %!error ... %! dictionary (string ('a'), {1, 2}) %!error dictionary (table (1), 1) %!error ... %! dictionary (1, 2, {3}, 4) %!error dictionary ({table(1)}, 1) %!error ... %! dictionary (string ('a'), 1, string ('b'), {2}) %!test %! [kt, vt] = types (U); %! assert_equal (class (kt), 'string'); %! assert_equal (ismissing (kt), true); %! assert_equal (class (vt), 'string'); %! assert_equal (ismissing (vt), true); %!test %! [kt, vt] = types (C); %! assert_equal (kt, string ('string')); %! assert_equal (vt, string ('double')); %!assert_equal (types (C), string ('string')) %!assert_equal (isConfigured (U), false) %!assert_equal (isConfigured (C), true) %!assert_equal (numEntries (U), 0) %!assert_equal (numEntries (C), 0) %!assert_equal (numel (U), 1) %!assert_equal (numel (C), 1) %!assert_equal (length (U), 1) %!assert_equal (length (C), 1) %!assert_equal (size (U), [1, 1]) %!assert_equal (size (C), [1, 1]) %!assert_equal (size (C, 1), 1) %!assert_equal (size (C, 2), 1) %!assert_equal (size (C, 3), 1) %!assert_equal (size (C, [1, 2]), [1, 1]) %!assert_equal (size (C, 1, 2), [1, 1]) %!test %! [r, c] = size (C); %! assert_equal ([r, c], [1, 1]); %!test %! [r, c, p] = size (C); %! assert_equal ([r, c, p], [1, 1, 1]); %!test %! [r, c] = size (C, [1, 2]); %! assert_equal ([r, c], [1, 1]); %!error size (C, 0) %!error size (C, 1.5) %!error size (C, []) %!error size (C, 'a') %!error size (C, 1, 0) %!error size (C, 1, [1, 2]) %!assert_equal (ndims (U), 2) %!assert_equal (ndims (C), 2) %!assert_equal (isempty (U), false) %!assert_equal (isempty (C), false) %!assert_equal (dispstrings (U), {'dictionary with unset key and value types'}) %!assert_equal (dispstrings (C), ... %! {'dictionary (string ⟼ double) with no entries'}) %!assert_equal (dispstrings (dictionary (1, 2)), ... %! {'dictionary (double ⟼ double) with 1 entry'}) %!assert_equal (dispstrings (dictionary ([1, 2], {3, 4})), ... %! {'dictionary (double ⟼ cell) with 2 entries'}) %!assert_equal (dispstrings (dictionary (string ('a'), dictionary (1, 2))), ... %! {'dictionary (string ⟼ dictionary) with 1 entry'}) %!assert_equal (U.dispstrings (), {'dictionary with unset key and value types'}) %!assert_equal (properties (U), cell (0, 1)) %!assert_equal (properties (C), cell (0, 1)) %!assert_equal (fieldnames (C), cell (0, 1)) %!test %! s = struct ('d', C); %! assert_equal (isdictionary (s.d), true); %!test %! s = struct ('a', {C, 5}); %! assert_equal (size (s), [1, 2]); %! assert_equal (isdictionary (s(1).a), true); %!error ... %! struct (C) %!error ... %! jsonencode (C) %!error ... %! jsonencode (U, 'PrettyPrint', true) %!error ... %! dictionary.empty () %!error ... %! dictionary.empty (0, 0) ## Construction, measured on R2024a %!test %! r = dictionary (1, 2); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! assert_equal (values (r, 'cell'), {2}); %!test %! r = dictionary ([1, 2, 3], [4, 5, 6]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {1; 2; 3}); %! assert_equal (values (r, 'cell'), {4; 5; 6}); %!error ... %! dictionary ([1; 2; 3], [4, 5, 6]); %!test %! r = dictionary ([1, 2; 3, 4], [5, 6; 7, 8]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 4); %! assert_equal (keys (r, 'cell'), {1; 3; 2; 4}); %! assert_equal (values (r, 'cell'), {5; 7; 6; 8}); %!error ... %! dictionary ([1, 2; 3, 4], [5, 6, 7, 8]); %!test %! r = dictionary ([1, 2, 3], 7); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {1; 2; 3}); %! assert_equal (values (r, 'cell'), {7; 7; 7}); %!error ... %! dictionary (7, [1, 2, 3]); %!error ... %! dictionary ([1, 2], [1, 2, 3]); %!test %! r = dictionary ([], []); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 0); %!test %! r = dictionary (zeros (1, 0), zeros (1, 0)); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 0); %!test %! r = dictionary (zeros (0, 3), zeros (0, 3)); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 0); %!test %! r = dictionary ([1, 2, 1], [10, 20, 30]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 2}); %! assert_equal (values (r, 'cell'), {30; 20}); %!test %! r = dictionary ([NaN, NaN, 1], [1, 2, 3]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {NaN; 1}); %! assert_equal (values (r, 'cell'), {2; 3}); %!test %! r = dictionary ([0, -0], [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {0}); %! assert_equal (values (r, 'cell'), {2}); %!test %! r = dictionary ([Inf, -Inf], [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {Inf; -Inf}); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary (single ([1, 2]), [3, 4]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'single', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {single(1); single(2)}); %! assert_equal (values (r, 'cell'), {3; 4}); %!test %! r = dictionary (int8 ([1, 2]), [3, 4]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'int8', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {int8(1); int8(2)}); %! assert_equal (values (r, 'cell'), {3; 4}); %!test %! r = dictionary (intmax ('uint64'), 1); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'uint64', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {intmax('uint64')}); %! assert_equal (values (r, 'cell'), {1}); %!test %! r = dictionary ([intmax('uint64') - 1, intmax('uint64')], [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'uint64', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {intmax('uint64') - 1; intmax('uint64')}); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary ([intmin('int64'), intmin('int64') + 1], [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'int64', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {intmin('int64'); intmin('int64') + 1}); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary ([true, false, true], [1, 2, 3]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'logical', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {true; false}); %! assert_equal (values (r, 'cell'), {3; 2}); %!test %! r = dictionary ('a', 1); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {1}); %!test %! r = dictionary ('abc', 1); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('abc')}); %! assert_equal (values (r, 'cell'), {1}); %!error ... %! dictionary (['ab'; 'cd'], [1, 2]); %!test %! r = dictionary (['ab'; 'cd'], [1; 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('ab'); string('cd')}); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary (['ab'; 'cd'], 1); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('ab'); string('cd')}); %! assert_equal (values (r, 'cell'), {1; 1}); %!test %! r = dictionary ('', 1); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('')}); %! assert_equal (values (r, 'cell'), {1}); %!test %! r = dictionary (string ({'a', 'b'}), [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary (string ({'a', 'b'}), string ({'x', 'y'})); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {string('x'); string('y')}); %!test %! r = dictionary (string ({'a', 'b'}), 'xy'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {string('xy'); string('xy')}); %!error ... %! dictionary (string ({'a', 'b'}), ['xy'; 'zw']); %!test %! r = dictionary (string ('a'), ''); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {string('')}); %!test %! r = dictionary (string (''), 1); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('')}); %! assert_equal (values (r, 'cell'), {1}); %!test %! r = dictionary (string ({'a', ''}), [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('')}); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary ([string('a'), string(missing)], [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! assert_equal (kc3{1}, string ('a')); %! assert_equal (class (kc3{2}), 'string'); %! assert_equal (ismissing (kc3{2}), true); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary (string (missing), 1); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'string'); %! assert_equal (ismissing (kc3{1}), true); %! assert_equal (values (r, 'cell'), {1}); %!test %! r = dictionary ([string(missing), string(missing)], [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'string'); %! assert_equal (ismissing (kc3{1}), true); %! assert_equal (values (r, 'cell'), {2}); %!test %! r = dictionary ({'a', 'b'}, [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'cell', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {{'a'}; {'b'}}); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary ({'a', 1}, [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'cell', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {{'a'}; {1}}); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary ({'a', 'a'}, [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'cell', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {{'a'}}); %! assert_equal (values (r, 'cell'), {2}); %!test %! r = dictionary ({[1, 2], [1, 2]}, [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'cell', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {{[1, 2]}}); %! assert_equal (values (r, 'cell'), {2}); %!test %! r = dictionary ({}, []); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'cell', 'double'})); %! assert_equal (numEntries (r), 0); %!test %! r = dictionary (1, {'x'}); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! assert_equal (values (r, 'cell'), {{'x'}}); %!test %! r = dictionary ([1, 2], {'x', [1, 2, 3]}); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 2}); %! assert_equal (values (r, 'cell'), {{'x'}; {[1, 2, 3]}}); %!test %! r = dictionary ([1, 2], {'x'}); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 2}); %! assert_equal (values (r, 'cell'), {{'x'}; {'x'}}); %!error ... %! dictionary (1, {}); %!test %! r = dictionary (1, {{}}); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! assert_equal (values (r, 'cell'), {{{}}}); %!test %! r = dictionary ([1, 2], struct ('a', {1, 2})); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'struct'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 2}); %! assert_equal (values (r, 'cell'), {struct('a', 1); struct('a', 2)}); %!test %! r = dictionary (1, struct ('a', 1)); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'struct'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! assert_equal (values (r, 'cell'), {struct('a', 1)}); %!test %! r = dictionary (1, struct ()); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'struct'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! assert_equal (values (r, 'cell'), {struct()}); %!test %! r = dictionary (struct ('a', {1, 2}), [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'struct', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {struct('a', 1); struct('a', 2)}); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary (struct ('a', 1), 5); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'struct', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {struct('a', 1)}); %! assert_equal (values (r, 'cell'), {5}); %!test %! r = dictionary (@sin, 1); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'function_handle', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {@sin}); %! assert_equal (values (r, 'cell'), {1}); %!test %! r = dictionary (1, @sin); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'function_handle'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! assert_equal (values (r, 'cell'), {@sin}); %!test %! r = dictionary ([1, 2], {@sin, @cos}); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'cell'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 2}); %! assert_equal (values (r, 'cell'), {{@sin}; {@cos}}); %!test %! r = dictionary (datetime (2024, 1, [1, 2]), [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'datetime', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! x5 = kc3{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, ''); %! x6 = kc3{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'2024-01-02 00:00:00.000'}); %! assert_equal (x6.TimeZone, ''); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary (datetime (2024, 1, [1, 1]), [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'datetime', 'double'})); %! assert_equal (numEntries (r), 1); %! kc3 = keys (r, 'cell'); %! x5 = kc3{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, ''); %! assert_equal (values (r, 'cell'), {2}); %!test %! r = dictionary ([datetime(2024, 1, 1), NaT, NaT], [1, 2, 3]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'datetime', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! x5 = kc3{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, ''); %! x6 = kc3{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'NaT'}); %! assert_equal (x6.TimeZone, ''); %! assert_equal (values (r, 'cell'), {1; 3}); %!test %! r = dictionary (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 1); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'datetime', 'double'})); %! assert_equal (numEntries (r), 1); %! kc3 = keys (r, 'cell'); %! x5 = kc3{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, 'UTC'); %! assert_equal (values (r, 'cell'), {1}); %!test %! r = dictionary (datetime (2024, 1, 1, 'TimeZone', 'Europe/Athens'), 1); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'datetime', 'double'})); %! assert_equal (numEntries (r), 1); %! kc3 = keys (r, 'cell'); %! x5 = kc3{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, 'Europe/Athens'); %! assert_equal (values (r, 'cell'), {1}); %!test %! r = dictionary (1, datetime (2024, 1, 1, 'TimeZone', 'UTC')); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'datetime'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! vc4 = values (r, 'cell'); %! x5 = vc4{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, 'UTC'); %!test %! r = dictionary (hours ([1, 2]), [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'duration', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'duration'); %! assert_equal (seconds (kc3{1}), 3600); %! assert_equal (class (kc3{2}), 'duration'); %! assert_equal (seconds (kc3{2}), 7200); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary (hours (1), 1, minutes (60), 2); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'duration', 'double'})); %! assert_equal (numEntries (r), 1); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'duration'); %! assert_equal (seconds (kc3{1}), 3600); %! assert_equal (values (r, 'cell'), {2}); %!test %! r = dictionary ([hours(1), seconds(3600)], [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'duration', 'double'})); %! assert_equal (numEntries (r), 1); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'duration'); %! assert_equal (seconds (kc3{1}), 3600); %! assert_equal (values (r, 'cell'), {2}); %!test %! r = dictionary ([hours(NaN), hours(NaN)], [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'duration', 'double'})); %! assert_equal (numEntries (r), 1); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'duration'); %! assert_equal (seconds (kc3{1}), NaN); %! assert_equal (values (r, 'cell'), {2}); %!test %! r = dictionary (calmonths ([1, 2]), [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'calendarDuration', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! [m5, dd6, t7] = split (kc3{1}, {'months', 'days', 'time'}); %! assert_equal ([m5(:), dd6(:), seconds(t7(:))], [1, 0, 0]); %! [m8, dd9, t10] = split (kc3{2}, {'months', 'days', 'time'}); %! assert_equal ([m8(:), dd9(:), seconds(t10(:))], [2, 0, 0]); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary ([calmonths(12), calyears(1)], [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'calendarDuration', 'double'})); %! assert_equal (numEntries (r), 1); %! kc3 = keys (r, 'cell'); %! [m5, dd6, t7] = split (kc3{1}, {'months', 'days', 'time'}); %! assert_equal ([m5(:), dd6(:), seconds(t7(:))], [12, 0, 0]); %! assert_equal (values (r, 'cell'), {2}); %!test %! r = dictionary ([caldays(7), calweeks(1)], [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'calendarDuration', 'double'})); %! assert_equal (numEntries (r), 1); %! kc3 = keys (r, 'cell'); %! [m5, dd6, t7] = split (kc3{1}, {'months', 'days', 'time'}); %! assert_equal ([m5(:), dd6(:), seconds(t7(:))], [0, 7, 0]); %! assert_equal (values (r, 'cell'), {2}); %!test %! r = dictionary (categorical ({'r', 'g', 'r'}), [1, 2, 3]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'categorical', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'categorical'); %! assert_equal (size (kc3{1}), [1, 1]); %! assert_equal (cellstr (kc3{1}), {'r'}); %! assert_equal (categories (kc3{1}), {'g'; 'r'}); %! assert_equal (isordinal (kc3{1}), false); %! assert_equal (class (kc3{2}), 'categorical'); %! assert_equal (size (kc3{2}), [1, 1]); %! assert_equal (cellstr (kc3{2}), {'g'}); %! assert_equal (categories (kc3{2}), {'g'; 'r'}); %! assert_equal (isordinal (kc3{2}), false); %! assert_equal (values (r, 'cell'), {3; 2}); %!test %! r = dictionary (categorical ({'r', 'g'}, {'r', 'g', 'b'}), [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'categorical', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'categorical'); %! assert_equal (size (kc3{1}), [1, 1]); %! assert_equal (cellstr (kc3{1}), {'r'}); %! assert_equal (categories (kc3{1}), {'r'; 'g'; 'b'}); %! assert_equal (isordinal (kc3{1}), false); %! assert_equal (class (kc3{2}), 'categorical'); %! assert_equal (size (kc3{2}), [1, 1]); %! assert_equal (cellstr (kc3{2}), {'g'}); %! assert_equal (categories (kc3{2}), {'r'; 'g'; 'b'}); %! assert_equal (isordinal (kc3{2}), false); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary (categorical ({'lo', 'hi'}, {'lo', 'hi'}, 'Ordinal', ... %! true), [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'categorical', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'categorical'); %! assert_equal (size (kc3{1}), [1, 1]); %! assert_equal (cellstr (kc3{1}), {'lo'}); %! assert_equal (categories (kc3{1}), {'lo'; 'hi'}); %! assert_equal (isordinal (kc3{1}), true); %! assert_equal (class (kc3{2}), 'categorical'); %! assert_equal (size (kc3{2}), [1, 1]); %! assert_equal (cellstr (kc3{2}), {'hi'}); %! assert_equal (categories (kc3{2}), {'lo'; 'hi'}); %! assert_equal (isordinal (kc3{2}), true); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary (categorical ({'r', ''}), [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'categorical', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'categorical'); %! assert_equal (size (kc3{1}), [1, 1]); %! assert_equal (cellstr (kc3{1}), {'r'}); %! assert_equal (categories (kc3{1}), {'r'}); %! assert_equal (isordinal (kc3{1}), false); %! assert_equal (class (kc3{2}), 'categorical'); %! assert_equal (size (kc3{2}), [1, 1]); %! assert_equal (isundefined (kc3{2}), true); %! assert_equal (categories (kc3{2}), {'r'}); %! assert_equal (isordinal (kc3{2}), false); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary (categorical ({'', ''}), [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'categorical', 'double'})); %! assert_equal (numEntries (r), 1); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'categorical'); %! assert_equal (size (kc3{1}), [1, 1]); %! assert_equal (isundefined (kc3{1}), true); %! assert_equal (categories (kc3{1}), cell (0, 0)); %! assert_equal (isordinal (kc3{1}), false); %! assert_equal (values (r, 'cell'), {2}); %!test %! r = dictionary (1, categorical ({'x'})); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'categorical'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! vc4 = values (r, 'cell'); %! assert_equal (class (vc4{1}), 'categorical'); %! assert_equal (size (vc4{1}), [1, 1]); %! assert_equal (cellstr (vc4{1}), {'x'}); %! assert_equal (categories (vc4{1}), {'x'}); %! assert_equal (isordinal (vc4{1}), false); %!test %! r = dictionary (missing, 1); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'missing', 'double'})); %! assert_equal (numEntries (r), 1); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'missing'); %! assert_equal (size (kc3{1}), [1, 1]); %! assert_equal (values (r, 'cell'), {1}); %!test %! r = dictionary ([missing, missing], [1, 2]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'missing', 'double'})); %! assert_equal (numEntries (r), 1); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'missing'); %! assert_equal (size (kc3{1}), [1, 1]); %! assert_equal (values (r, 'cell'), {2}); %!test %! r = dictionary (1, missing); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'missing'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! vc4 = values (r, 'cell'); %! assert_equal (class (vc4{1}), 'missing'); %! assert_equal (size (vc4{1}), [1, 1]); %!test %! r = dictionary ([1, 2], [missing, missing]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'missing'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 2}); %! vc4 = values (r, 'cell'); %! assert_equal (class (vc4{1}), 'missing'); %! assert_equal (size (vc4{1}), [1, 1]); %! assert_equal (class (vc4{2}), 'missing'); %! assert_equal (size (vc4{2}), [1, 1]); %!test %! r = dictionary (string ('a'), missing); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'missing'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! vc4 = values (r, 'cell'); %! assert_equal (class (vc4{1}), 'missing'); %! assert_equal (size (vc4{1}), [1, 1]); %!test %! r = dictionary (dictionary (1, 2), 3); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'dictionary', 'double'})); %! assert_equal (numEntries (r), 1); %! kc3 = keys (r, 'cell'); %! dc5 = kc3{1}; %! [kt6, vt7] = types (dc5); %! assert_equal ([kt6, vt7], string ({'double', 'double'})); %! assert_equal (numEntries (dc5), 1); %! assert_equal (keys (dc5, 'cell'), {1}); %! assert_equal (values (dc5, 'cell'), {2}); %! assert_equal (values (r, 'cell'), {3}); %!test %! r = dictionary (1, dictionary (1, 2)); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'dictionary'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! vc4 = values (r, 'cell'); %! dc5 = vc4{1}; %! [kt6, vt7] = types (dc5); %! assert_equal ([kt6, vt7], string ({'double', 'double'})); %! assert_equal (numEntries (dc5), 1); %! assert_equal (keys (dc5, 'cell'), {1}); %! assert_equal (values (dc5, 'cell'), {2}); %!test %! r = dictionary (1, table (1)); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'table'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! vc4 = values (r, 'cell'); %! tb5 = vc4{1}; %! assert_equal (size (tb5), [1, 1]); %! assert_equal (tb5.Properties.VariableNames, {'Var1'}); %! assert_equal (tb5.Var1, 1); ## D3: a table of any size is one value, where MATLAB takes only 1-by-1. %!test %! r = dictionary ([1, 2], [table(1); table(2)]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'table'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 2}); %! vc4 = values (r, 'cell'); %! tb5 = vc4{1}; %! assert_equal (size (tb5), [2, 1]); %! assert_equal (tb5.Properties.VariableNames, {'Var1'}); %! assert_equal (tb5.Var1, [1; 2]); %! tb6 = vc4{2}; %! assert_equal (size (tb6), [2, 1]); %! assert_equal (tb6.Properties.VariableNames, {'Var1'}); %! assert_equal (tb6.Var1, [1; 2]); %!error ... %! dictionary (table (1), 1); %!error ... %! dictionary (1, 2, 3); %!test %! r = dictionary (1, 2, 3, 4); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 3}); %! assert_equal (values (r, 'cell'), {2; 4}); %!test %! r = dictionary (1, 2, 1, 4); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! assert_equal (values (r, 'cell'), {4}); %!test %! r = dictionary (1, 2, string ('3'), 4); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 3}); %! assert_equal (values (r, 'cell'), {2; 4}); %!test %! r = dictionary (string ('a'), 1, 2, 3); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('2')}); %! assert_equal (values (r, 'cell'), {1; 3}); %!test %! r = dictionary (string ('a'), 1, string ('b'), 'x'); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('b')}); %! assert_equal (values (r, 'cell'), {1; 120}); %!test %! r = dictionary (1, 2, [], []); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! assert_equal (values (r, 'cell'), {2}); %!test %! r = dictionary (1, 2, [3, 4], [5, 6]); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 3); %! assert_equal (keys (r, 'cell'), {1; 3; 4}); %! assert_equal (values (r, 'cell'), {2; 5; 6}); %!test %! r = dictionary (int8 (1), 1, 2.7, 3); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'int8', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {int8(1); int8(3)}); %! assert_equal (values (r, 'cell'), {1; 3}); %!test %! r = dictionary (int8 (1), 1, 300, 3); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'int8', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {int8(1); int8(127)}); %! assert_equal (values (r, 'cell'), {1; 3}); %!test %! r = dictionary (true, 1, 2, 3); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'logical', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {true}); %! assert_equal (values (r, 'cell'), {3}); %!test %! r = dictionary (1, true, 2, 3); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'logical'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 2}); %! assert_equal (values (r, 'cell'), {true; true}); %!test %! r = dictionary (1, int8 (5), 2, 3.6); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'int8'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 2}); %! assert_equal (values (r, 'cell'), {int8(5); int8(4)}); %!test %! r = dictionary (1, 2, 'a', 3); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 97}); %! assert_equal (values (r, 'cell'), {2; 3}); %!test %! r = dictionary (string ('a'), 1, 7, 2); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('7')}); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary (string ('a'), 1, true, 2); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('true')}); %! assert_equal (values (r, 'cell'), {1; 2}); %!test %! r = dictionary (datetime (2024, 1, 1), 1, '2024-01-02', 2); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'datetime', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! x5 = kc3{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, ''); %! x6 = kc3{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'2024-01-02 00:00:00.000'}); %! assert_equal (x6.TimeZone, ''); %! assert_equal (values (r, 'cell'), {1; 2}); %!error ... %! dictionary (datetime (2024, 1, 1), 1, 5, 2); %!test %! r = dictionary (categorical ({'r'}), 1, string ('g'), 2); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'categorical', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'categorical'); %! assert_equal (size (kc3{1}), [1, 1]); %! assert_equal (cellstr (kc3{1}), {'r'}); %! assert_equal (categories (kc3{1}), {'r'}); %! assert_equal (isordinal (kc3{1}), false); %! assert_equal (class (kc3{2}), 'categorical'); %! assert_equal (size (kc3{2}), [1, 1]); %! assert_equal (cellstr (kc3{2}), {'g'}); %! assert_equal (categories (kc3{2}), {'g'}); %! assert_equal (isordinal (kc3{2}), false); %! assert_equal (values (r, 'cell'), {1; 2}); %!error ... %! dictionary (categorical ({'r'}), 1, 'g', 2); %!test %! r = dictionary (1, string ('x'), 2, 5); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'string'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 2}); %! assert_equal (values (r, 'cell'), {string('x'); string('5')}); %!test %! r = dictionary (1, string ('x'), 2, missing); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'string'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 2}); %! vc4 = values (r, 'cell'); %! assert_equal (vc4{1}, string ('x')); %! assert_equal (class (vc4{2}), 'string'); %! assert_equal (ismissing (vc4{2}), true); %!test %! r = dictionary (1, 2, 3, missing); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 3}); %! assert_equal (values (r, 'cell'), {2; NaN}); %!test %! r = dictionary (string ('a'), 1, missing, 2); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! assert_equal (kc3{1}, string ('a')); %! assert_equal (class (kc3{2}), 'string'); %! assert_equal (ismissing (kc3{2}), true); %! assert_equal (values (r, 'cell'), {1; 2}); %!assert_equal (isConfigured (dictionary ()), false) %!assert_equal (isConfigured (dictionary (1, 2)), true) %!assert_equal (isConfigured (configureDictionary ('double', 'double')), true) %!assert_equal (numEntries (dictionary ()), 0) %!assert_equal (numEntries (configureDictionary ('double', 'double')), 0) %!test %! r = types (dictionary ()); %! assert_equal (class (r), 'string'); %! assert_equal (ismissing (r), true); %!assert_equal (types (dictionary (1, string ('x'))), string ('double')) %!test %! [kt, vt] = types (dictionary (1, string ('x'))); %! assert_equal (vt, string ('string')); %!test %! [kt, vt] = types (dictionary ()); %! r = vt; %! assert_equal (class (r), 'string'); %! assert_equal (ismissing (r), true); ################################################################################ ## ** Keys and values ** ## ################################################################################ ## Available Methods ## ## ## ## 'keys' 'values' 'isKey' ## ## ## ################################################################################ %!shared D, C %! D = dictionary (string ({'a'; 'b'; 'c'}), [1; 2; 3]); %! C = configureDictionary ('string', 'double'); %!assert_equal (keys (D), string ({'a'; 'b'; 'c'})) %!assert_equal (keys (D, 'uniform'), string ({'a'; 'b'; 'c'})) %!assert_equal (keys (D, 'cell'), {string('a'); string('b'); string('c')}) %!assert_equal (keys (D, string ('cell')), ... %! {string('a'); string('b'); string('c')}) %!assert_equal (keys (C), string (cell (0, 1))) %!assert_equal (keys (C, 'cell'), cell (0, 1)) %!assert_equal (keys (dictionary ({'a'; 'b'}, [1; 2]), 'cell'), {{'a'}; {'b'}}) %!assert_equal (keys (dictionary (@sin, 1)), @sin) %!test %! k = keys (configureDictionary ('datetime', 'double')); %! assert_equal (class (k), 'datetime'); %! assert_equal (size (k), [0, 1]); %!assert_equal (keys (D, 'c'), {string('a'); string('b'); string('c')}) %!assert_equal (keys (D, 'UNIFORM'), string ({'a'; 'b'; 'c'})) %!error keys (dictionary ()) %!error ... %! keys (D, 1) %!error keys (D, 'foo') %!error ... %! keys (dictionary (@sin, 1, @cos, 2)) %!assert_equal (values (D), [1; 2; 3]) %!assert_equal (values (D, 'uniform'), [1; 2; 3]) %!assert_equal (values (D, 'cell'), {1; 2; 3}) %!assert_equal (values (C), zeros (0, 1)) %!assert_equal (values (dictionary (string ('f'), @sin)), @sin) %!test %! d = dictionary (string ({'f'; 'g'}), {@sin; @cos}); %! assert_equal (values (d), {@sin; @cos}); %!test %! d = dictionary (string ('a'), struct ('x', 1), ... %! string ('b'), struct ('y', 2)); %! v = values (d, 'cell'); %! assert_equal (v{2}, struct ('y', 2)); %!test %! d = dictionary (string ('a'), table (1), string ('b'), table (2)); %! assert_equal (isequal (values (d), table ([1; 2])), true); %!assert_equal (values (D, 'u'), [1; 2; 3]) %!assert_equal (values (D, 'Cell'), {1; 2; 3}) %!error values (dictionary ()) %!error ... %! values (D, string ({'b'; 'a'})) %!error values (D, 'foo') %!error ... %! values (dictionary (string ('a'), struct ('x', 1), ... %! string ('b'), struct ('y', 2))) %!error ... %! values (dictionary (string ('a'), table ([1; 2]), string ('b'), table (3))) %!error ... %! values (dictionary (string ('f'), @sin, string ('g'), @cos)) %!error ... %! values (dictionary (string ('a'), dictionary (1, 2), ... %! string ('b'), dictionary (3, 4))) %!assert_equal (isKey (D, string ('a')), true) %!assert_equal (isKey (D, string ('z')), false) %!assert_equal (isKey (D, string ({'a'; 'z'})), [true; false]) %!assert_equal (isKey (D, string ({'a', 'z'; 'c', 'b'})), ... %! [true, false; true, true]) %!assert_equal (isKey (D, string (cell (0, 0))), false (0, 0)) %!assert_equal (isKey (D, []), false (0, 0)) %!assert_equal (isKey (D, 'a'), true) %!assert_equal (isKey (D, {'a'}), true) %!assert_equal (isKey (D, 1), false) %!assert_equal (isKey (D, datetime (2024, 1, 1)), false) %!assert_equal (isKey (dictionary ([1; 2], [3; 4]), string ('1')), true) %!assert_equal (isKey (dictionary ([1; 2], [3; 4]), string ('abc')), false) %!assert_equal (isKey (dictionary ([1; 2], [3; 4]), 'a'), false) %!assert_equal (isKey (dictionary ([1; 2], [3; 4]), NaN), false) %!assert_equal (isKey (dictionary ({'a'; 'b'}, [1; 2]), {'a'; 'z'}), ... %! [true; false]) %!assert_equal (isKey (dictionary (int8 ([1, 2]), [5, 6]), 1.4), true) %!assert_equal (isKey (dictionary (int8 ([1, 2]), [5, 6]), 300), false) %!assert_equal (isKey (C, string ('a')), false) %!error isKey (D) %!error isKey (dictionary (), string ('a')) %!error ... %! isKey (dictionary ([1; 2], [3; 4]), {1}) %!error ... %! isKey (dictionary ({1}, 1), {table(1)}) %!error ... %! isKey (dictionary ({'a'; 'b'}, [1; 2]), 'a') ################################################################################ ## ** Entries ** ## ################################################################################ ## Available Methods ## ## ## ## 'insert' 'lookup' 'remove' 'entries' ## ## ## ################################################################################ %!shared D, CV %! D = dictionary (string ({'a'; 'b'; 'c'}), [1; 2; 3]); %! CV = dictionary (string ({'a'; 'b'}), {1; 'two'}); %!assert_equal (values (insert (D, string ('a'), 9)), [9; 2; 3]) %!assert_equal (values (insert (D, string ('z'), 9)), [1; 2; 3; 9]) %!assert_equal (values (insert (D, string ('a'), 9, 'Overwrite', false)), ... %! [1; 2; 3]) %!assert_equal (values (insert (D, string ('a'), 9, 'overwrite', false)), ... %! [1; 2; 3]) %!assert_equal (values (insert (D, string ('a'), 9, ... %! string ('Overwrite'), 0)), ... %! [1; 2; 3]) %!assert_equal (values (insert (D, string ('a'), 9, 'Overwrite', -1)), ... %! [9; 2; 3]) %!assert_equal (values (insert (D, string ('a'), 9, 'Overwrite', true, ... %! 'Overwrite', false)), [1; 2; 3]) %!assert_equal (values (insert (D, string ({'z'; 'a'}), [8; 9], ... %! 'Overwrite', false)), [1; 2; 3; 8]) %!assert_equal (values (insert (D, string ({'z'; 'z'}), [8; 9])), [1; 2; 3; 9]) %!assert_equal (values (insert (D, string ({'z'; 'z'}), [8; 9], ... %! 'Overwrite', false)), [1; 2; 3; 8]) %!assert_equal (values (insert (D, string ({'x'; 'y'}), 7)), [1; 2; 3; 7; 7]) %!assert_equal (numEntries (insert (D, string (cell (0, 1)), 5)), 3) %!assert_equal (keys (insert (D, {1}, 4)), string ({'a'; 'b'; 'c'; '1'})) %!assert_equal (values (insert (D, string ('q'), string ('7'))), [1; 2; 3; 7]) %!test %! d = insert (dictionary (), string ('k'), {1}); %! [kt, vt] = types (d); %! assert_equal (kt, string ('string')); %! assert_equal (vt, string ('cell')); %!assert_equal (numEntries (D.insert (string ('q'), 1)), 4) %!error ... %! insert (D, string ('a')) %!error ... %! insert (D, string ('a'), 9, 'Overwrite') %!error ... %! insert (D, string ('a'), 9, 5) %!error ... %! insert (D, string ('a'), 9, 'Foo', true) %!error ... %! insert (dictionary (), table (1), 2) %!error ... %! insert (dictionary (1, 2), {1}, 2) %!error ... %! insert (D, string ('a'), 9, 'Overwrite', [true, false]) %!error ... %! insert (D, string ('a'), 9, 'Overwrite', 'yes') %!error ... %! insert (D, string ('a'), 9, 'Overwrite', []) %!error ... %! insert (D, string ('a'), 9, 'Overwrite', NaN) %!error ... %! insert (D, string ('a'), []) %!error ... %! insert (D, string ('a'), {1}) %!assert_equal (lookup (D, string ('b')), 2) %!assert_equal (lookup (D, string ({'c'; 'a'})), [3; 1]) %!assert_equal (lookup (D, string ('z'), 'FallbackValue', 0), 0) %!assert_equal (lookup (D, string ('z'), 'FALLBACKVALUE', 0), 0) %!assert_equal (lookup (D, string ({'a', 'z'; 'z', 'b'}), ... %! 'FallbackValue', -1), ... %! [1, -1; -1, 2]) %!assert_equal (lookup (D, [], 'FallbackValue', -1), zeros (0, 0)) %!assert_equal (lookup (D, string (cell (0, 1))), zeros (0, 1)) %!assert_equal (lookup (D, string ('z'), 'FallbackValue', missing), NaN) %!assert_equal (lookup (D, string ('z'), 'FallbackValue', '7'), 55) %!assert_equal (lookup (D, string ('z'), 'FallbackValue', string ('7')), 7) %!assert_equal (lookup (D, string ('z'), 'FallbackValue', int8 (4)), 4) %!test %! ## The fallback is not checked when every key is found %! assert_equal (lookup (D, string ('a'), 'FallbackValue', {1}), 1); %! assert_equal (lookup (D, string ('a'), 'FallbackValue', [1, 2]), 1); %!assert_equal (lookup (CV, string ('a')), {1}) %!assert_equal (lookup (CV, string ({'a'; 'z'}), 'FallbackValue', {5}), {1; 5}) %!test %! d = dictionary (string ('f'), @sin); %! assert_equal (lookup (d, string ('z'), 'FallbackValue', @cos), @cos); %!test %! d = dictionary ([1, 2], string ({'a', 'b'})); %! v = lookup (d, [1, 3], 'FallbackValue', 'none'); %! assert_equal (v, string ({'a', 'none'})); %!test %! d = dictionary ([1, 2], datetime (2024, 1, [1, 2])); %! v = lookup (d, [1, 3], 'FallbackValue', '2021-01-01'); %! assert_equal (datenum (v), datenum ([2024, 2021], 1, 1)); %!error lookup (D) %!error ... %! lookup (D, string ('a'), 5) %!error ... %! lookup (D, string ('a'), 'Foo', 1) %!error lookup (D, string ('z')) %!error ... %! lookup (D, string ({'a'; 'z'})) %!error lookup (D, 1) %!error ... %! lookup (D, ':') %!error ... %! lookup (dictionary (1, 2), {1}) %!error ... %! lookup (dictionary ({1}, 1), {table(1)}) %!error ... %! lookup (D, string ('z'), 'FallbackValue', []) %!error ... %! lookup (D, string ('z'), 'FallbackValue', [-1; -2]) %!error ... %! lookup (D, string ('z'), 'FallbackValue', {1}) %!error ... %! lookup (CV, string ('z'), 'FallbackValue', 5) %!error ... %! lookup (dictionary (), string ('z'), 'FallbackValue', 1) %!error ... %! lookup (dictionary (string ('f'), @sin), string ({'f'; 'z'}), ... %! 'FallbackValue', @cos) %!assert_equal (keys (remove (D, string ('b'))), string ({'a'; 'c'})) %!assert_equal (keys (remove (D, string ({'a', 'c'}))), string ('b')) %!assert_equal (numEntries (remove (D, string ('zz'))), 3) %!assert_equal (numEntries (remove (D, [])), 3) %!assert_equal (keys (remove (D, {'b'})), string ({'a'; 'c'})) %!assert_equal (numEntries (remove (D, 1)), 3) %!test %! d = remove (D, string ('a')); %! d = insert (d, string ('a'), 1); %! assert_equal (keys (d), string ({'b'; 'c'; 'a'})); %!error remove (D) %!error ... %! remove (dictionary (), string ('a')) %!error ... %! remove (dictionary ([1; 2], [3; 4]), {1}) %!error ... %! remove (dictionary ({1}, 1), {table(1)}) %!test %! d = dictionary (datetime (2024, 1, [1, 2, 3]), [1, 2, 3]); %! d = remove (d, datetime (2024, 1, 1)); %! d(datetime (2024, 1, 1)) = 4; %! assert_equal (values (d), [2; 3; 4]); %!test %! d = dictionary (categorical ({'r', 'g'}), [1, 2]); %! d = insert (d, categorical ({'g', 'b'}), [20, 3], 'Overwrite', false); %! assert_equal (values (d), [1; 2; 3]); %!test %! d = dictionary (hours ([1, 2]), [10, 20]); %! assert_equal (lookup (d, minutes (120)), 20); %!test %! s = entries (dictionary (missing, 1), 'struct'); %! assert_equal (class (s.Key), 'missing'); %! assert_equal (s.Value, 1); %!test %! e = entries (D); %! assert_equal (istable (e), true); %! assert_equal (e.Properties.VariableNames, {'Key', 'Value'}); %! assert_equal (e.Key, string ({'a'; 'b'; 'c'})); %! assert_equal (e.Value, [1; 2; 3]); %!test %! e = entries (D, 'TABLE'); %! assert_equal (size (e), [3, 2]); %!test %! e = entries (D, 'struct'); %! assert_equal (size (e), [3, 1]); %! assert_equal (e(2).Key, string ('b')); %! assert_equal (e(2).Value, 2); %!test %! e = entries (D, 's'); %! assert_equal (fieldnames (e), {'Key'; 'Value'}); %!test %! e = entries (D, 'Cell'); %! assert_equal (e, {string('a'), 1; string('b'), 2; string('c'), 3}); %!test %! e = entries (configureDictionary ('string', 'double')); %! assert_equal (size (e), [0, 2]); %! assert_equal (e.Key, string (cell (0, 1))); %! assert_equal (e.Value, zeros (0, 1)); %!test %! e = entries (configureDictionary ('string', 'double'), 'struct'); %! assert_equal (size (e), [0, 1]); %! assert_equal (fieldnames (e), {'Key'; 'Value'}); %!assert_equal (entries (configureDictionary ('string', 'double'), 'cell'), ... %! cell (0, 2)) %!test %! e = entries (dictionary ({'a'; 'b'}, [1; 2])); %! assert_equal (e.Key, {'a'; 'b'}); %!test %! e = entries (CV); %! assert_equal (e.Value, {1; 'two'}); %!test %! e = entries (CV, 'struct'); %! assert_equal (e(2).Value, {'two'}); %!test %! e = entries (CV, 'cell'); %! assert_equal (e{2,2}, {'two'}); %!test %! e = entries (dictionary (string ({'f'; 'g'}), {@sin; @cos})); %! assert_equal (e.Value, {@sin; @cos}); %!test %! e = entries (dictionary (string ('t'), table (1))); %! assert_equal (istable (e.Value), true); %!test %! e = entries (dictionary (string ('f'), @sin), 'struct'); %! assert_equal (e.Value, @sin); %!error ... %! entries (D, 1) %!error entries (D, 'foo') %!error entries (dictionary ()) %!error ... %! entries (dictionary (string ('f'), @sin)) %!error ... %! entries (dictionary (@sin, 1)) %!error ... %! entries (dictionary (string ('d'), dictionary (1, 2))) %!error ... %! entries (dictionary (string ('a'), struct ('x', 1), ... %! string ('b'), struct ('y', 2))) ## The entry methods, measured on R2024a %!shared k %! k = dictionary (string ({'b', 'a', 'c'}), [2, 1, 3]); %!assert_equal (keys (k), string ({'b'; 'a'; 'c'})) %!assert_equal (values (k), [2; 1; 3]) %!assert_equal (keys (k, 'cell'), {string('b'); string('a'); string('c')}) %!assert_equal (values (k, 'cell'), {2; 1; 3}) %!assert_equal (keys (k, 'Cell'), {string('b'); string('a'); string('c')}) %!assert_equal (keys (k, 'c'), {string('b'); string('a'); string('c')}) %!assert_equal (keys (k, 'uniform'), string ({'b'; 'a'; 'c'})) %!assert_equal (keys (k, 'u'), string ({'b'; 'a'; 'c'})) %!error ... %! keys (k, 'x'); %!error ... %! keys (k, 5); %!assert_equal (values (k, 'un'), [2; 1; 3]) %!test %! r = entries (k); %! tb1 = r; %! assert_equal (size (tb1), [3, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! assert_equal (tb1.Key, string ({'b'; 'a'; 'c'})); %! assert_equal (tb1.Value, [2; 1; 3]); %!test %! r = entries (k, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (r(1).Key, string ('b')); %! assert_equal (r(1).Value, 2); %! assert_equal (r(2).Key, string ('a')); %! assert_equal (r(2).Value, 1); %! assert_equal (r(3).Key, string ('c')); %! assert_equal (r(3).Value, 3); %!test %! r = entries (k, 'cell'); %! assert_equal (r, {string('b'), 2; string('a'), 1; string('c'), 3}); %!test %! r = entries (k, 'table'); %! tb1 = r; %! assert_equal (size (tb1), [3, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! assert_equal (tb1.Key, string ({'b'; 'a'; 'c'})); %! assert_equal (tb1.Value, [2; 1; 3]); %!test %! r = entries (k, 'S'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (r(1).Key, string ('b')); %! assert_equal (r(1).Value, 2); %! assert_equal (r(2).Key, string ('a')); %! assert_equal (r(2).Value, 1); %! assert_equal (r(3).Key, string ('c')); %! assert_equal (r(3).Value, 3); %!error ... %! entries (k, 'x'); %!assert_equal (isKey (k, string ('a')), true) %!test %! r = isKey (k, string ({'a', 'z'; 'c', 'b'})); %! assert_equal (r, [true, false; true, true]); %!assert_equal (isKey (k, 'a'), true) %!assert_equal (isKey (k, {'a', 'z'}), [true, false]) %!assert_equal (isKey (k, 1), false) %!assert_equal (isKey (k, []), false (0, 0)) %!assert_equal (isKey (k, missing), false) %!assert_equal (isKey (k, string (missing)), false) %!error ... %! isKey (k); %!assert_equal (types (k), string ('string')) %!assert_equal (numEntries (k), 3) %!assert_equal (lookup (k, string ('a')), 1) %!assert_equal (lookup (k, string ({'c', 'a'})), [3, 1]) %!assert_equal (lookup (k, string ({'c', 'a'; 'b', 'b'})), [3, 1; 2, 2]) %!error ... %! lookup (k, string ('z')); %!assert_equal (lookup (k, string ({'a', 'z'}), 'FallbackValue', -1), [1, -1]) %!assert_equal (lookup (k, string ({'a', 'z'}), 'fallbackvalue', -1), [1, -1]) %!test %! r = lookup (k, string ({'z', 'y'}), 'FallbackValue', NaN); %! assert_equal (r, [NaN, NaN]); %!assert_equal (lookup (k, string ('a'), 'FallbackValue', 'x'), 1) %!assert_equal (lookup (k, string ('z'), 'FallbackValue', 'x'), 120) %!error ... %! lookup (k, string ('z'), 'FallbackValue', [1, 2]); %!error ... %! lookup (k, string ('z'), 'FallbackValue', []); %!assert_equal (lookup (k, string ('z'), 'FallbackValue', int8 (4)), 4) %!error ... %! lookup (k, string ('z'), 'FallbackValue'); %!error ... %! lookup (k, string ('z'), 'Foo', 1); %!assert_equal (lookup (k, []), zeros (0, 0)) %!assert_equal (lookup (k, 'a'), 1) %!error ... %! lookup (k); %!error ... %! lookup (k, ':'); %!test %! cv = dictionary (string ({'a', 'b'}), {1, 'two'}); %! r = lookup (cv, string ({'a', 'z'}), 'FallbackValue', {0}); %! assert_equal (r, {1, 0}); %!error ... %! cv = dictionary (string ({'a', 'b'}), {1, 'two'}); %! lookup (cv, string ('z'), 'FallbackValue', 0); %!test %! k2 = insert (k, string ('d'), 4); %! assert_equal (keys (k2), string ({'b'; 'a'; 'c'; 'd'})); %!test %! k2 = insert (k, string ('a'), 100); %! assert_equal (values (k2), [2; 100; 3]); %!test %! k2 = insert (k, string ({'a', 'e'}), [100, 5], 'Overwrite', false); %! assert_equal (values (k2), [2; 1; 3; 5]); %!test %! k2 = insert (k, string ({'e', 'e'}), [5, 6], 'Overwrite', false); %! assert_equal (values (k2), [2; 1; 3; 5]); %!test %! k2 = insert (k, string ({'e', 'e'}), [5, 6]); %! assert_equal (values (k2), [2; 1; 3; 6]); %!test %! k2 = insert (k, string ({'e', 'f'}), 7); %! assert_equal (values (k2), [2; 1; 3; 7; 7]); %!error ... %! k2 = insert (k, string ({'e', 'f'}), [1, 2, 3]); %!test %! k2 = insert (k, string ('e'), 1, 'overwrite', 0); %! assert_equal (keys (k2), string ({'b'; 'a'; 'c'; 'e'})); %!error ... %! k2 = insert (k, string ('e'), 1, 'Overwrite', 'no'); %!error ... %! k2 = insert (k, string ('e'), 1, 'Overwrite', [true, true]); %!error ... %! k2 = insert (k, string ('e')); %!test %! k2 = insert (k, [], []); %! assert_equal (numEntries (k2), 3); %!test %! k2 = insert (dictionary (), string ('a'), 1); %! r = k2; %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {1}); %!test %! k2 = insert (k, 5, 6); %! assert_equal (keys (k2), string ({'b'; 'a'; 'c'; '5'})); %!test %! k2 = remove (k, string ('a')); %! assert_equal (keys (k2), string ({'b'; 'c'})); %!test %! k2 = remove (k, string ({'a', 'z'})); %! assert_equal (keys (k2), string ({'b'; 'c'})); %!test %! k2 = remove (k, string ({'a', 'b', 'c'})); %! r = k2; %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 0); %!test %! k2 = remove (k, []); %! assert_equal (numEntries (k2), 3); %!test %! k2 = remove (k, string ({'a', 'a'})); %! assert_equal (keys (k2), string ({'b'; 'c'})); %!test %! k2 = remove (k, 'b'); %! assert_equal (keys (k2), string ({'a'; 'c'})); %!error ... %! k2 = remove (k); %!error ... %! k2 = remove (dictionary (), 1); %!test %! k2 = remove (configureDictionary ('double', 'double'), 1); %! r = k2; %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 0); %!test %! k2 = remove (k, string ('b')); %! k2 = insert (k2, string ('b'), 9); %! assert_equal (keys (k2), string ({'a'; 'c'; 'b'})); %!test %! k2 = remove (k, string ({'a', 'b', 'c'})); %! k2 = insert (k2, string ('z'), 1); %! r = k2; %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('z')}); %! assert_equal (values (r, 'cell'), {1}); ################################################################################ ## ** Files ** ## ################################################################################ ## Available Methods ## ## ## ## 'writedictionary' ## ## ## ################################################################################ ## Write D to a temporary file, return its text, and delete the file %!function s = wj (d, varargin) %! s = wjx (d, '.json', varargin{:}); %!endfunction %!function s = wjx (d, ext, varargin) %! f = [tempname() ext]; %! unwind_protect %! writedictionary (d, f, varargin{:}); %! s = fileread (f); %! unwind_protect_cleanup %! if (exist (f, 'file')) %! delete (f); %! endif %! end_unwind_protect %!endfunction ## Measured on MATLAB Online R2026b %!test %! s = wj (dictionary (string ({'a', 'b'}), [1, 2])); %! assert_equal (s, ... %! strjoin ({'{', ' "a": 1.0,', ' "b": 2.0', '}'}, "\n")); %!test %! s = wj (dictionary (string ({'a', 'b'}), [1, 2]), 'PrettyPrint', false); %! assert_equal (s, '{"a":1.0,"b":2.0}'); %!test %! s = wj (dictionary (string ({'a', 'b', 'c', 'd'}), [1.5, pi, 0.1, 1e20]), ... %! 'PrettyPrint', false); %! assert_equal (s, ... %! ['{"a":1.5,"b":3.141592653589793,"c":0.1,', ... %! '"d":100000000000000000000.0}']); %!test %! s = wj (dictionary (string ({'a', 'b', 'c'}), [-0, 1, -7]), ... %! 'PrettyPrint', false); %! assert_equal (s, '{"a":-0.0,"b":1.0,"c":-7.0}'); %!test %! s = wj (dictionary (string ({'a', 'b', 'c'}), [NaN, Inf, -Inf]), ... %! 'PrettyPrint', false); %! assert_equal (s, '{"a":NaN,"b":Infinity,"c":-Infinity}'); %!test %! s = wj (dictionary (string ({'a', 'b', 'c'}), [NaN, Inf, -Inf]), ... %! 'PrettyPrint', false, 'PreserveInfAndNaN', false); %! assert_equal (s, '{"a":null,"b":null,"c":null}'); %!test %! s = wj (dictionary (string ({'a', 'b'}), [string('x'), string(missing)]), ... %! 'PrettyPrint', false); %! assert_equal (s, '{"a":"x","b":null}'); %!test %! s = wj (dictionary (string ({'a', 'b'}), [true, false]), 'PrettyPrint', ... %! false); %! assert_equal (s, '{"a":true,"b":false}'); %!test %! s = wj (dictionary (string ({'a', 'b'}), int8 ([5, -3])), 'PrettyPrint', ... %! false); %! assert_equal (s, '{"a":5,"b":-3}'); %!test %! s = wj (dictionary ('a', intmax ('int64')), 'PrettyPrint', false); %! assert_equal (s, '{"a":9223372036854775807}'); %!test %! s = wj (dictionary ('a', intmax ('uint64')), 'PrettyPrint', false); %! assert_equal (s, '{"a":18446744073709551615}'); %!test %! s = wj (dictionary (string ({'a', 'b'}), single ([1.5, 0.1])), ... %! 'PrettyPrint', false); %! assert_equal (s, '{"a":1.5,"b":0.10000000149011612}'); %!test %! s = wj (dictionary (string ({'a', 'b'}), datetime (2024, 1, [1, 2])), ... %! 'PrettyPrint', false); %! assert_equal (s, '{"a":"01-Jan-2024","b":"02-Jan-2024"}'); %!test %! s = wj (dictionary ('a', datetime (2024, 1, 1, 'TimeZone', 'UTC')), ... %! 'PrettyPrint', false); %! assert_equal (s, '{"a":"01-Jan-2024"}'); %!assert_equal (wj (dictionary ('a', NaT), 'PrettyPrint', false), '{"a":null}') %!test %! s = wj (dictionary (string ({'a', 'b'}), hours ([1, 2])), 'PrettyPrint', ... %! false); %! assert_equal (s, '{"a":"1 hr","b":"2 hr"}'); %!error ... %! wj (dictionary ('a', calmonths (1)), 'PrettyPrint', false); %!error ... %! wj (dictionary (string ({'a', 'b'}), categorical (string ({'r', 'g'}))), ... %! 'PrettyPrint', false); %!test %! s = wj (dictionary (string ({'a', 'b', 'c', 'd', 'e'}), ... %! {1, 'x', [1, 2, 3], {3, 'y'}, []}), 'PrettyPrint', ... %! false); %! assert_equal (s, '{"a":1.0,"b":"x","c":[1.0,2.0,3.0],"d":[3.0,"y"],"e":[]}'); %!error ... %! wj (dictionary ('a', {[1, 2;, 3, 4]}), 'PrettyPrint', false); %!test %! s = wj (dictionary ('a', {true}), 'PrettyPrint', false); %! assert_equal (s, '{"a":true}'); %!test %! s = wj (dictionary ('a', {missing}), 'PrettyPrint', false); %! assert_equal (s, '{"a":null}'); %!assert_equal (wj (dictionary ('a', {NaN}), 'PrettyPrint', false), '{"a":NaN}') %!error ... %! wj (dictionary ('a', struct ('x', 1, 'y', 's')), 'PrettyPrint', false); %!error ... %! wj (dictionary ('a', {struct('x', {1, 2})}), 'PrettyPrint', false); %!test %! s = wj (dictionary ('a', dictionary (string ({'x', 'y'}), [1, 2])), ... %! 'PrettyPrint', false); %! assert_equal (s, '{"a":{"x":1.0,"y":2.0}}'); %!test %! s = wj (dictionary ('a', dictionary (1, 2)), 'PrettyPrint', false); %! assert_equal (s, '{"a":{"1":2.0}}'); %!test %! s = wj (dictionary ('a', {dictionary('x', 1)}), 'PrettyPrint', false); %! assert_equal (s, '{"a":{"x":1.0}}'); %!test %! s = wj (dictionary ('a', missing), 'PrettyPrint', false); %! assert_equal (s, '{"a":null}'); %!error ... %! wj (dictionary ('a', @sin), 'PrettyPrint', false); %!error ... %! wj (dictionary ('a', table (1)), 'PrettyPrint', false); %!test %! s = wj (dictionary ([1, 2], [3, 4]), 'PrettyPrint', false); %! assert_equal (s, '{"1":3.0,"2":4.0}'); %!test %! s = wj (dictionary ([1.5, -2], [3, 4]), 'PrettyPrint', false); %! assert_equal (s, '{"1.5":3.0,"-2":4.0}'); %!test %! s = wj (dictionary ([true, false], [3, 4]), 'PrettyPrint', false); %! assert_equal (s, '{"true":3.0,"false":4.0}'); %!test %! s = wj (dictionary (int8 ([1, 2]), [3, 4]), 'PrettyPrint', false); %! assert_equal (s, '{"1":3.0,"2":4.0}'); %!test %! s = wj (dictionary (datetime (2024, 1, [1, 2]), [3, 4]), 'PrettyPrint', ... %! false); %! assert_equal (s, '{"01-Jan-2024":3.0,"02-Jan-2024":4.0}'); %!test %! s = wj (dictionary (hours ([1, 2]), [3, 4]), 'PrettyPrint', false); %! assert_equal (s, '{"1 hr":3.0,"2 hr":4.0}'); %!error ... %! wj (dictionary (categorical (string ({'r', 'g'})), [3, 4]), ... %! 'PrettyPrint', false); %!test %! s = wj (dictionary ({'a' 'b'}, [3, 4]), 'PrettyPrint', false); %! assert_equal (s, '{"a":3.0,"b":4.0}'); %!error ... %! wj (dictionary (missing, 1), 'PrettyPrint', false); %!error ... %! wj (dictionary ([string('a'), string(missing)], [1, 2]), 'PrettyPrint', ... %! false); %!test %! s = wj (dictionary (string ({'', 'a'}), [1, 2]), 'PrettyPrint', false); %! assert_equal (s, '{"":1.0,"a":2.0}'); %!test %! s = wj (dictionary ([string('a"b'), string(sprintf ('c\nd')), ... %! string('é')], [1, 2, 3]), 'PrettyPrint', false); %! assert_equal (s, '{"a\"b":1.0,"c\nd":2.0,"é":3.0}'); %!assert_equal (wj (dictionary, 'PrettyPrint', false), '{}') %!test %! s = wj (configureDictionary ('string', 'double'), 'PrettyPrint', false); %! assert_equal (s, '{}'); %!test %! s = wj (dictionary (string ({'a', 'b'}), {1, {}})); %! assert_equal (s, strjoin ({'{', ' "a": 1.0,', ' "b": []', '}'}, "\n")); %!test %! s = wj (dictionary ('a', dictionary ('b', dictionary ('c', 1)))); %! assert_equal (s, ... %! strjoin ({'{', ' "a": {', ' "b": {', ... %! ' "c": 1.0', ' }', ' }', ... %! '}'}, "\n")); %!error ... %! wjx (dictionary (string ({'a', 'b'}), [1, 2]), '.txt'); %!test %! s = wjx (dictionary (string ({'a', 'b'}), [1, 2]), '.txt', 'FileType', ... %! 'json'); %! assert_equal (s, ... %! strjoin ({'{', ' "a": 1.0,', ' "b": 2.0', '}'}, "\n")); %!test %! s = wj (dictionary (string ({'a', 'b'}), [1, 2]), 'prettyprint', false); %! assert_equal (s, '{"a":1.0,"b":2.0}'); %!error ... %! wj (dictionary (string ({'a', 'b'}), [1, 2]), 'PrettyPrint', 'yes'); %!error ... %! wj (dictionary (string ({'a', 'b'}), [1, 2]), 'Foo', 1); %!test %! s = wj (dictionary ('a', ... %! datetime (2024, 1, 1, 10, 0, 0, 'Format', ... %! 'yyyy-MM-dd''T''HH:mm:ss')), 'PrettyPrint', ... %! false); %! assert_equal (s, '{"a":"2024-01-01T10:00:00"}'); %!test %! s = wj (dictionary ('a', seconds (90)), 'PrettyPrint', false); %! assert_equal (s, '{"a":"90 sec"}'); %!test %! s = wj (dictionary ('a', {{'x', 1, true}}), 'PrettyPrint', false); %! assert_equal (s, '{"a":["x",1.0,true]}'); %!test %! s = wj (dictionary ('a', {[1, 2, 3]'}), 'PrettyPrint', false); %! assert_equal (s, '{"a":[1.0,2.0,3.0]}'); %!test %! s = wj (dictionary ('a', {string({'x', 'y'})}), 'PrettyPrint', false); %! assert_equal (s, '{"a":["x","y"]}'); %!test %! s = wj (dictionary ('a', {{'x' 'y'}}), 'PrettyPrint', false); %! assert_equal (s, '{"a":["x","y"]}'); %!test %! s = wj (dictionary ('a', {zeros(1, 0)}), 'PrettyPrint', false); %! assert_equal (s, '{"a":[]}'); %!test %! s = wj (dictionary ('a', {int8([1, 2])}), 'PrettyPrint', false); %! assert_equal (s, '{"a":[1,2]}'); %!test %! s = wj (dictionary ('a', {datetime(2024, 1, [1, 2])}), 'PrettyPrint', false); %! assert_equal (s, '{"a":["01-Jan-2024","02-Jan-2024"]}'); ## Complex values are refused wherever they are (R2026b crashes) %!error ... %! wj (dictionary ('a', 1+2i)); %!error ... %! wj (dictionary ('a', {1+2i})); %!error ... %! wj (dictionary ('a', {{1, 2i}})); %!error ... %! wj (dictionary ('a', dictionary ('b', 3i))); %!error ... %! wj (dictionary (1+2i, 1)); ## Ours: the number layout beyond what R2026b showed, after RapidJSON %!test %! d = dictionary (string ({'a', 'b', 'c', 'd', 'e', 'f'}), ... %! [1e21, 1e-7, 1e-6, 1.5e300, -1e-5, 2^53]); %! s = wj (d, 'PrettyPrint', false); %! assert_equal (s, ['{"a":1e21,"b":1e-7,"c":0.000001,"d":1.5e300,', ... %! '"e":-0.00001,"f":9007199254740992.0}']); %!test %! s = wj (dictionary ('a', intmin ('int64')), 'PrettyPrint', false); %! assert_equal (s, '{"a":-9223372036854775808}'); %!test %! s = wj (dictionary ('a', uint8 (255)), 'PrettyPrint', false); %! assert_equal (s, '{"a":255}'); %!test %! d = dictionary (string ({'a', 'b'}), seconds ([90, NaN])); %! assert_equal (wj (d, 'PrettyPrint', false), '{"a":"90 sec","b":null}'); %!test %! s = wj (dictionary ('a', [char(1), 'b', char(8), char(12), '/']), ... %! 'PrettyPrint', false); %! assert_equal (s, '{"a":"\u0001b\b\f/"}'); ## Ours: arrays and empty objects when indented %!test %! s = wj (dictionary ('a', {[1, 2]})); %! assert_equal (s, strjoin ({'{', ' "a": [', ' 1.0,', ... %! ' 2.0', ' ]', '}'}, "\n")); %!test %! s = wj (dictionary ('a', {{1, {}}})); %! assert_equal (s, strjoin ({'{', ' "a": [', ' 1.0,', ... %! ' []', ' ]', '}'}, "\n")); %!test %! s = wj (dictionary ('a', dictionary ())); %! assert_equal (s, strjoin ({'{', ' "a": {}', '}'}, "\n")); %!assert_equal (wj (dictionary ()), '{}') ## Ours: keys of NaN, Inf and NaT %!test %! s = wj (dictionary ([NaN, Inf], [1, 2]), 'PrettyPrint', false); %! assert_equal (s, '{"NaN":1.0,"Inf":2.0}'); %!assert_equal (wj (dictionary (NaT, 1), 'PrettyPrint', false), '{"NaT":1.0}') %!test %! s = wj (dictionary (single (0.5), 1), 'PrettyPrint', false); %! assert_equal (s, '{"0.5":1.0}'); %!error ... %! wj (dictionary ({1}, 2)); ## Ours: the file is replaced, and the method can be called with '.' %!test %! f = [tempname() '.json']; %! unwind_protect %! writedictionary (dictionary (string ({'a', 'b'}), [1, 2]), f); %! d = dictionary ('z', 9); %! d.writedictionary (f, 'PrettyPrint', false); %! assert_equal (fileread (f), '{"z":9.0}'); %! unwind_protect_cleanup %! delete (f); %! end_unwind_protect ## Ours: options and locations %!error ... %! writedictionary (dictionary ('a', 1), 5); %!error ... %! writedictionary (dictionary ('a', 1)); %!error ... %! wj (dictionary ('a', 1), 'PrettyPrint'); %!error ... %! wj (dictionary ('a', 1), 'PreserveInfAndNaN', 'yes'); %!error ... %! wj (dictionary ('a', 1), 'FileType', 'xml'); %!error ... %! wj (dictionary ('a', 1), 'FileType', 5); %!error ... %! writedictionary (dictionary ('a', 1), 's3://bucket/x.json'); %!error ... %! writedictionary (dictionary ('a', 1), 'https://example.org/x.json'); ## Round trips through readdictionary, as on R2026b %!test %! d0 = dictionary ('a', dictionary (string ({'x', 'y'}), {1, 'x'})); %! f = [tempname() '.json']; %! unwind_protect %! writedictionary (d0, f); %! assert_equal (isequal (d0, readdictionary (f)), false); %! unwind_protect_cleanup %! delete (f); %! end_unwind_protect %!test %! d0 = dictionary (string ({'a', 'b'}), {[1, 2, 3], 's'}); %! f = [tempname() '.json']; %! unwind_protect %! writedictionary (d0, f); %! v = values (readdictionary (f), 'cell'); %! assert_equal (v{1}, {{1; 2; 3}}); %! unwind_protect_cleanup %! delete (f); %! end_unwind_protect %!test %! d0 = dictionary (string ({'a', 'b'}), datetime (2024, 1, [1, 2])); %! f = [tempname() '.json']; %! unwind_protect %! writedictionary (d0, f); %! d1 = readdictionary (f); %! assert_equal (datenum (values (d1)), datenum (2024, 1, [1; 2])); %! unwind_protect_cleanup %! delete (f); %! end_unwind_protect ## OpenDocument spreadsheets, an extension of this package %!test %! d = dictionary (string ({'Sales', 'Q 2024'}), ... %! {table([1; 2]), timetable(hours ([1; 2]), [3; 4])}); %! f = [tempname() '.ods']; %! unwind_protect %! writedictionary (d, f); %! r = readdictionary (f); %! assert_equal (keys (r), string ({'Sales'; 'Q 2024'})); %! assert_equal (r{string('Sales')}.Var1, [1; 2]); %! assert_equal (class (r{string('Q 2024')}), 'timetable'); %! unwind_protect_cleanup %! delete (f); %! end_unwind_protect %!test %! d = dictionary ('t', table (1)); %! f = [tempname() '.fods']; %! unwind_protect %! writedictionary (d, f); %! [~, vt] = types (readdictionary (f)); %! assert_equal (vt, string ('table')); %! unwind_protect_cleanup %! delete (f); %! end_unwind_protect %!error ... %! wjx (dictionary ('x', 1), '.ods'); %!error ... %! wjx (dictionary (1, table (1)), '.ods'); %!error ... %! wjx (configureDictionary ('string', 'table'), '.ods'); %!error ... %! wjx (dictionary ('x', table (1)), '.ods', 'PrettyPrint', false); %!error ... %! wjx (dictionary ('x', table (1)), '.ods', 'PreserveInfAndNaN', false); ################################################################################ ## ** Compare and hash ** ## ################################################################################ ## Available Methods ## ## ## ## 'isequal' 'isequaln' 'keyHash' ## ## ## ################################################################################ %!shared D1, D2, D4, D5, D6 %! D1 = dictionary (string ({'a'; 'b'}), [1; 2]); %! D2 = dictionary (string ({'b'; 'a'}), [2; 1]); %! D4 = dictionary (string ({'a'; 'b'}), [1; 2]); %! D5 = dictionary (string ({'a'; 'b'}), [1; 3]); %! D6 = dictionary (string ({'a'; 'b'}), int8 ([1; 2])); %!assert_equal (isequal (D1, D2), true) %!assert_equal (isequal (D1, D4, D2), true) %!assert_equal (isequal (D1, D5), false) %!assert_equal (isequal (D1, D6), true) %!assert_equal (isequal (D1, 5), false) %!assert_equal (isequal (5, D1), false) %!assert_equal (isequal (dictionary (string ('a'), 1), ... %! dictionary (1, 1)), false) %!assert_equal (isequal (dictionary (string ('a'), NaN), ... %! dictionary (string ('a'), NaN)), false) %!assert_equal (isequal (dictionary (string ({'a'; 'b'}), {1; 2}), ... %! dictionary (string ({'b'; 'a'}), {2; 1})), true) %!assert_equal (isequal (dictionary (), dictionary ()), true) %!assert_equal (isequal (dictionary (), ... %! configureDictionary ('string', 'double')), false) %!assert_equal (isequal (configureDictionary ('string', 'double'), ... %! configureDictionary ('string', 'int8')), true) %!assert_equal (isequal (configureDictionary ('double', 'double'), ... %! configureDictionary ('string', 'double')), false) %!assert_equal (isequal (dictionary (string (cell (0, 0)), []), ... %! configureDictionary ('string', 'double')), true) %!error isequal (D1) %!assert_equal (isequaln (dictionary (string ('a'), NaN), ... %! dictionary (string ('a'), NaN)), true) %!assert_equal (isequaln (D1, D2), true) %!assert_equal (isequaln (D1, D4, D5), false) %!error isequaln (D1) %!assert_equal (class (keyHash (D1)), 'uint64') %!assert_equal (keyHash (D1) == keyHash (D4), true) %!assert_equal (keyHash (D1) == keyHash (D2), false) %!assert_equal (keyHash (D1) == keyHash (D5), false) %!assert_equal (keyHash (D1) == keyHash (D6), false) %!assert_equal (keyHash (D1, uint64 (5)) == keyHash (D1), false) %!assert_equal (keyMatch (D1, D4), true) %!assert_equal (keyMatch (D1, D2), false) %!assert_equal (keyMatch (D1, D6), false) %!assert_equal (keyMatch (dictionary (string ('a'), NaN), ... %! dictionary (string ('a'), NaN)), true) %!assert_equal (keyMatch (dictionary (), dictionary ()), true) %!assert_equal (keyMatch (dictionary (), ... %! configureDictionary ('string', 'double')), false) %!assert_equal (keyMatch (configureDictionary ('string', 'double'), ... %! configureDictionary ('string', 'int8')), false) %!assert_equal (class (keyHash (dictionary (string ('f'), @sin))), 'uint64') %!assert_equal (D1.keyHash () == keyHash (D1), true) %!error keyHash (D1, 5) %!error ... %! keyHash (dictionary (string ('t'), table (1))) ## Comparison and hashing, measured on R2024a %!shared q1, q2, q3, q4, q5 %! q1 = dictionary (string ({'a', 'b'}), [1, 2]); %! q2 = dictionary (string ({'b', 'a'}), [2, 1]); %! q3 = dictionary (string ({'a', 'b'}), int8 ([1, 2])); %! q4 = dictionary (string ({'a', 'b'}), [1, NaN]); %! q5 = dictionary (string ({'a', 'b'}), [1, NaN]); %!assert_equal (isequal (q1, q2), true) %!assert_equal (isequal (q1, q3), true) %!assert_equal (isequal (q4, q5), false) %!assert_equal (isequaln (q4, q5), true) %!assert_equal (isequal (q1, q1, q2), true) %!assert_equal (isequal (q1, q2, q4), false) %!assert_equal (isequal (q1, 5), false) %!assert_equal (isequal (5, q1), false) %!error ... %! isequal (q1); %!assert_equal (isequaln (q1, q2, q3), true) %!error ... %! isequaln (q1); %!assert_equal (keyHash (q1) == keyHash (q2), false) %!assert_equal (keyHash (q1) == keyHash (q1), true) %!assert_equal (keyHash (q1) == keyHash (q3), false) %!assert_equal (keyMatch (q1, q2), false) %!assert_equal (keyMatch (q1, q1), true) %!assert_equal (keyMatch (q4, q5), true) ## keyHash takes a BASE, an extension of this package. %!assert_equal (keyHash (q1, uint64 (5)) == keyHash (q1, uint64 (5)), true) ## keyHash takes a BASE, an extension of this package. %!assert_equal (keyHash (q1, uint64 (5)) == keyHash (q1), false) %!error ... %! keyHash (q1, 5); %!assert_equal (isequal (dictionary (), dictionary ()), true) %!test %! r = isequal (dictionary (), configureDictionary ('double', 'double')); %! assert_equal (r, false); %!test %! r = isequal (configureDictionary ('double', 'double'), ... %! configureDictionary ('string', 'double')); %! assert_equal (r, false); %!test %! r = isequal (configureDictionary ('string', 'double'), ... %! configureDictionary ('string', 'int8')); %! assert_equal (r, true); %!test %! r = isequal (dictionary ([1, 2], [3, 4]), ... %! dictionary (string ({'1', '2'}), [3, 4])); %! assert_equal (r, false); %!assert_equal (isequal (dictionary (1, {1}), dictionary (1, {1})), true) %!test %! r = isequal (dictionary (1, dictionary (1, 2)), ... %! dictionary (1, dictionary (1, 2))); %! assert_equal (r, true); %!assert_equal (isequal (dictionary (1, 2), dictionary (int8 (1), 2)), false) %!assert_equal (keyHash (dictionary ()) == keyHash (dictionary ()), true) %!test %! r = keyHash (dictionary ()) == keyHash (configureDictionary ('double', ... %! 'double')); %! assert_equal (r, false); %!error ... %! keyHash (dictionary (string ('t'), table (1))); %!test %! r = keyHash (dictionary (string ('f'), ... %! @sin)) == keyHash (dictionary (string ('f'), @sin)); %! assert_equal (r, true); %!test %! r = keyMatch (dictionary ([1, 2], [3, 4]), ... %! dictionary ([1, 2], int8 ([3, 4]))); %! assert_equal (r, false); ################################################################################ ## ** Indexing and concatenation ** ## ################################################################################ ## Available Methods ## ## ## ## 'subsref' 'subsasgn' 'end' ## ## 'horzcat' 'vertcat' 'cat' ## ## ## ################################################################################ %!shared D, CV, C %! D = dictionary (string ({'a'; 'b'; 'c'}), [1; 2; 3]); %! CV = dictionary (string ({'a'; 'b'}), {[1, 2, 3]; [4, 5]}); %! C = configureDictionary ('string', 'double'); %!assert_equal (D(string ('b')), 2) %!assert_equal (D(string ({'c'; 'a'})), [3; 1]) %!assert_equal (D(string ({'a', 'b'; 'c', 'a'})), [1, 2; 3, 1]) %!assert_equal (D('a'), 1) %!assert_equal (D({'a'}), 1) %!assert_equal (D([]), zeros (0, 0)) %!assert_equal (D(string (cell (0, 1))), zeros (0, 1)) %!assert_equal (D(string ('a'))(1), 1) %!assert_equal (CV{string('a')}, [1, 2, 3]) %!assert_equal (CV{string('a')}(2), 2) %!test %! [p, q] = CV{string({'a'; 'b'})}; %! assert_equal (p, [1, 2, 3]); %! assert_equal (q, [4, 5]); %!test %! x = {CV{string({'b'; 'a'})}}; %! assert_equal (x, {[4, 5], [1, 2, 3]}); %!test %! d = dictionary (string ({'a'; 'b'}), struct ('x', {1; 2})); %! assert_equal (d(string ('b')).x, 2); %!assert_equal (D.keys (), string ({'a'; 'b'; 'c'})) %!assert_equal (D.keys ('cell'), {string('a'); string('b'); string('c')}) %!assert_equal (D.isKey (string ('a')), true) %!assert_equal (D.numEntries, 3) %!test %! [kt, vt] = D.types (); %! assert_equal (kt, string ('string')); %! assert_equal (vt, string ('double')); %!assert_equal (keys (D'), string ({'a'; 'b'; 'c'})) ## Key types %!test %! d = dictionary (missing, 1); %! assert_equal (d(missing), 1); %! assert_equal (class (keys (d)), 'missing'); %!test %! d = dictionary ([missing, missing], [1, 2]); %! assert_equal (numEntries (d), 1); %! assert_equal (values (d), 2); %!test %! d = configureDictionary ('missing', 'double'); %! d(missing) = 4; %! assert_equal (values (d), 4); %!test %! d = dictionary (dictionary (1, 2), 5); %! assert_equal (d(dictionary (1, 2)), 5); %! d(dictionary (1, 2)) = 6; %! assert_equal (numEntries (d), 1); %! assert_equal (values (d), 6); %!test %! d = dictionary (dictionary (1, 2), 5, dictionary (3, 4), 6); %! assert_equal (size (keys (d, 'cell')), [2, 1]); %!test %! ## Dates with and without a time zone are different keys %! z = dictionary (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 1); %! assert_equal (isKey (z, datetime (2024, 1, 1)), false); %! z(datetime (2024, 1, 2)) = 2; %! assert_equal (numEntries (z), 2); %!test %! u = dictionary (datetime (2024, 1, 1), 1); %! assert_equal (isKey (u, datetime (2024, 1, 1, 'TimeZone', 'UTC')), false); %!test %! ## Text is read in the time zone of the keys, where MATLAB reads it in none %! z = dictionary (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 1); %! assert_equal (z(string ('2024-01-01')), 1); %! z(string ('2024-01-02')) = 2; %! k = keys (z); %! assert_equal (k.TimeZone, 'UTC'); %! assert_equal (numEntries (z), 2); %!test %! z = dictionary (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 1); %! ny = datetime (2023, 12, 31, 19, 0, 0, 'TimeZone', 'America/New_York'); %! z(ny) = 5; %! assert_equal (numEntries (z), 1); %! assert_equal (values (z), 5); %!test %! d = dictionary (datetime (2024, 1, 1), 1, ... %! datetime (2024, 1, 1, 'Format', 'yyyy-MM-dd'), 2); %! assert_equal (values (d), 2); %!assert_equal (dictionary (single (1), 1)(int8 (1)), 1) %!test %! d = dictionary (int8 (1), 1); %! d(NaN) = 2; %! assert_equal (keys (d), int8 ([1; 0])); %!test %! d = dictionary (seconds (1), 1); %! d(seconds (NaN)) = 2; %! d(seconds (NaN)) = 3; %! assert_equal (numEntries (d), 2); %!test %! d = dictionary (calmonths (1), 1); %! d(calmonths (NaN)) = 2; %! d(calmonths (NaN)) = 3; %! assert_equal (numEntries (d), 2); ## Octave cannot tell d(:) from d(':'), so ':' is the key ":" %!test %! d = dictionary (string (':'), 1); %! assert_equal (d(':'), 1); %!test %! d = dictionary (string ({'a', 'b'}), [1, 2]); %! d(:) = 5; %! assert_equal (keys (d), string ({'a'; 'b'; ':'})); %!test %! d = dictionary ([1, 2], [3, 4]); %! d(:) = 9; %! assert_equal (keys (d), [1; 2; 58]); ## Assigning into the contents of a new key starts from empty %!test %! d = dictionary (string ('a'), {1}); %! d{string('new')}(2) = 7; %! assert_equal (d{string('new')}, [0, 7]); %!test %! d = dictionary (string ('a'), {1}); %! d{string('a')}(3) = 7; %! assert_equal (d{string('a')}, [1, 0, 7]); ## A date value keeps its own time zone; a date key moves to that of the keys %!test %! d = dictionary (1, datetime (2024, 1, 1, 'TimeZone', 'UTC')); %! d(2) = datetime (2024, 1, 2, 'TimeZone', 'Europe/Athens'); %! v = values (d, 'cell'); %! assert_equal (v{2}.TimeZone, 'Europe/Athens'); %!test %! d = dictionary (1, datetime (2024, 1, 1, 'TimeZone', 'UTC')); %! d(2) = datetime (2024, 1, 2); %! v = values (d, 'cell'); %! assert_equal (v{2}.TimeZone, ''); %!error ... %! d = dictionary (1, datetime (2024, 1, 1, 'TimeZone', 'UTC')); %! d(2) = datetime (2024, 1, 2); %! values (d); ## An array of objects of another class holds one value per element %!test %! d = dictionary ([1, 2], [timeseries([1; 2]), timeseries([3; 4; 5])]); %! assert_equal (d(2).Data, [3; 4; 5]); %!test %! d = dictionary ([1, 2], tscollection (timeseries ([1; 2; 3]))); %! assert_equal (numEntries (d), 2); %! assert_equal (class (d(2)), 'tscollection'); ## Conversion by the function of the type's name, as in MATLAB %!test %! d = configureDictionary ('double', 'table'); %! d(1) = 5; %! assert_equal (d(1).Var1, 5); %!test %! d = configureDictionary ('double', 'categorical'); %! d(1) = 5; %! assert_equal (cellstr (d(1)), {'5'}); %!test %! d = configureDictionary ('double', 'categorical'); %! d(1) = missing; %! assert_equal (isundefined (d(1)), true); %!test %! d = configureDictionary ('double', 'string'); %! d([1, 2, 3]) = [hours(1), hours(2), hours(3)]; %! assert_equal (d(1), string ('1 hr')); %!test %! d = configureDictionary ('double', 'string'); %! d(1) = categorical ({'x'}); %! assert_equal (d(1), string ('x')); %!test %! d = configureDictionary ('double', 'string'); %! d(1) = calmonths (1); %! assert_equal (d(1), string ('1mo')); %!test %! d = configureDictionary ('double', 'double'); %! d(1) = categorical ({'x'}); %! assert_equal (d(1), 1); %!test %! d = configureDictionary ('double', 'int8'); %! d(1) = categorical ({'x'}); %! assert_equal (d(1), int8 (1)); %!test %! d = configureDictionary ('double', 'logical'); %! d(1) = 'x'; %! assert_equal (d(1), true); %!test %! d = configureDictionary ('double', 'datetime'); %! d(1) = missing; %! assert_equal (isnat (d(1)), true); %!test %! d = configureDictionary ('double', 'duration'); %! d(1) = missing; %! assert_equal (isnan (d(1)), true); %!test %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = missing; %! assert_equal (isnan (d(1)), true); %!test %! d = configureDictionary ('string', 'double'); %! d(hours (1)) = 1; %! assert_equal (keys (d), string ('1 hr')); %!test %! d = configureDictionary ('categorical', 'double'); %! d(5) = 1; %! assert_equal (cellstr (keys (d)), {'5'}); %!test %! d = configureDictionary ('logical', 'double'); %! d('x') = 1; %! assert_equal (keys (d), true); %!test %! d = configureDictionary ('datetime', 'double'); %! d(missing) = 1; %! assert_equal (isnat (keys (d)), true); %!error ... %! d = configureDictionary ('double', 'table'); d(1) = 'x'; %!error ... %! d = configureDictionary ('double', 'single'); d(1) = string ('5'); %!error ... %! d = configureDictionary ('double', 'int8'); d(1) = string ('5'); %!error ... %! d = configureDictionary ('double', 'int8'); d(1) = missing; %!error ... %! d = configureDictionary ('int8', 'double'); d(string ('5')) = 1; %!error ... %! d = configureDictionary ('double', 'struct'); d(1) = hours (1); %!error ... %! d = configureDictionary ('double', 'cell'); d(1) = 5; %!error ... %! d = configureDictionary ('double', 'logical'); d(1) = NaN; %!error D(string ('z')) %!error D(true) %!error ... %! D(string ({'a'; 'z'})) %!error D(1, 2) %!error D(:) %!error ... %! D(':') %!error ... %! d = dictionary (1, struct ('f', 1)); d(2).f = 5; %!error ... %! d = dictionary (datetime (2020, 1, 1), 1); d(string ('garbage')) %!error ... %! isKey (dictionary (datetime (2020, 1, 1), 1), string ('garbage')) %!error ... %! d = dictionary (int8 ([0, 1]), [10, 11]); d(string ('abc')) %!error ... %! d = dictionary (); d(string ('a')) %!error ... %! d = dictionary ([1; 2], [3; 4]); d({1}) %!error ... %! d = dictionary ({1}, 1); d({table(1)}) %!error ... %! d = dictionary ({1}, 1); d({table(1)}) = 2; %!error ... %! d = dictionary (1, struct ('f', 1)); d(':').f = 2; %!error ... %! d = dictionary (string ('f'), @sin); d(string ({'f'; 'f'})) %!error ... %! D{string('a')} %!error CV{string('z')} %!error D.foo %!error ... %! d = dictionary (missing, 1); d(NaN) %!error ... %! d = dictionary (true, 1); d(NaN) %!error ... %! d = dictionary (dictionary (1, 2), 5); d(dictionary (1, 3)) %!error ... %! z = dictionary (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 1); %! z(datetime (2024, 1, 2)) = 2; keys (z) %!test %! d = D; %! d(string ('z')) = 26; %! assert_equal (keys (d), string ({'a'; 'b'; 'c'; 'z'})); %! assert_equal (values (d), [1; 2; 3; 26]); %!test %! ## Updating a value keeps the key's position %! d = D; %! d(string ('a')) = 11; %! assert_equal (keys (d), string ({'a'; 'b'; 'c'})); %! assert_equal (values (d), [11; 2; 3]); %!test %! d = D; %! d(string ({'x', 'y'})) = [7, 8]; %! assert_equal (values (d), [1; 2; 3; 7; 8]); %!test %! d = D; %! d(string ({'x'; 'y'})) = 9; %! assert_equal (values (d), [1; 2; 3; 9; 9]); %!test %! d = D; %! d(string ('e')) = string ('12'); %! assert_equal (d(string ('e')), 12); %!test %! d = D; %! d(5) = 1; %! assert_equal (keys (d), string ({'a'; 'b'; 'c'; '5'})); %!test %! d = D; %! d(string (cell (0, 1))) = 5; %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d(string ({'a'; 'nope'})) = []; %! assert_equal (keys (d), string ({'b'; 'c'})); %!test %! d = D; %! d({'b'}) = []; %! assert_equal (keys (d), string ({'a'; 'c'})); %!test %! d = D; %! d([]) = []; %! assert_equal (numEntries (d), 3); %!test %! ## A key removed and added again goes to the end %! d = D; %! d(string ('b')) = []; %! d(string ('b')) = 22; %! assert_equal (keys (d), string ({'a'; 'c'; 'b'})); %! assert_equal (values (d), [1; 3; 22]); %!test %! d = D; %! e = d; %! e(string ('a')) = []; %! assert_equal (numEntries (d), 3); %! assert_equal (numEntries (e), 2); %!test %! d = dictionary (string ({'a'; 'b'}), string ({'x'; 'y'})); %! d(string ('a')) = ''; %! assert_equal (values (d), string ({''; 'y'})); %!test %! d = dictionary (); %! d(string (cell (0, 1))) = 5; %! assert_equal (isConfigured (d), true); %!test %! d = dictionary (); %! d('abc') = 'xyz'; %! assert_equal (types (d), string ('string')); %! assert_equal (values (d), string ('xyz')); %!test %! d = CV; %! d{string('a')}(2) = 20; %! assert_equal (d{string('a')}, [1, 20, 3]); %!test %! d = CV; %! d{string('c')} = 7; %! assert_equal (values (d), {[1, 2, 3]; [4, 5]; 7}); %!test %! ## '{} = []' stores an empty value, '() = []' deletes %! d = CV; %! d{string('b')} = []; %! assert_equal (values (d), {[1, 2, 3]; []}); %! d(string ('a')) = []; %! assert_equal (keys (d), string ('b')); %!test %! d = dictionary (); %! d{string('a')} = 1; %! assert_equal (types (d), string ('string')); %! assert_equal (values (d), {1}); %!test %! d = dictionary (string ({'a'; 'b'}), struct ('x', {1; 2})); %! d(string ('a')).x = 10; %! assert_equal (d(string ('a')), struct ('x', 10)); %!error ... %! d = D; d(string ({'p', 'q'})) = [7; 8]; %!error ... %! d = D; d(string ('m')) = zeros (0, 0, 'single'); %!error ... %! d = CV; d(string ('a')) = {}; %!error ... %! d = D; d(string ('a')) = {1}; %!error ... %! d = D; d(1, 2) = 3; %!error ... %! d = dictionary (); d(string ('a')) = []; %!error ... %! d = dictionary (); d(string ('a')).x = 1; %!error ... %! d = D; d{string('a')} = 1; %!error ... %! d = CV; d{string({'a'; 'b'})} = 5; %!error ... %! d = D; d.foo = 1; %!error ... %! d = dictionary (); d(table (1)) = 1; %!error D(end) ## The bracket forms raise core's own 'cat: dictionary/horzcat method failed' ## wrapper, which is not ours to test, so call the methods directly. %!error horzcat (C, C) %!error horzcat (C, 1) %!error horzcat (1, C) %!error horzcat (C) %!error vertcat (C, C) %!error vertcat (C, 1) %!error vertcat (C) %!error cat (1, C, C) %!error cat (3, C) ## Indexing, measured on R2024a %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! assert_equal (d([3, 1]), [30, 10]); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! assert_equal (d([3; 1]), [30; 10]); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! assert_equal (d([1, 2; 3, 1]), [10, 20; 30, 10]); %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(4); %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d([1, 4]); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! assert_equal (d([]), zeros (0, 0)); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! assert_equal (d(zeros (0, 3)), zeros (0, 3)); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! assert_equal (d(int8 (2)), 20); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! assert_equal (d(true), 10); %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d('a'); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! assert_equal (d(string ('2')), 20); %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(1, 2); %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(NaN); %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(:); %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d{1}; %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! assert_equal (d.numEntries, 3); %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d.Foo; %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! assert_equal (d.keys, [1; 2; 3]); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! assert_equal (d.keys (), [1; 2; 3]); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! assert_equal (d.isConfigured, true); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! assert_equal (d.types, string ('double')); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! r = d.entries ('struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (r(1).Key, 1); %! assert_equal (r(1).Value, 10); %! assert_equal (r(2).Key, 2); %! assert_equal (r(2).Value, 20); %! assert_equal (r(3).Key, 3); %! assert_equal (r(3).Value, 30); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(4) = 40; %! assert_equal (keys (d), [1; 2; 3; 4]); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(2) = 200; %! assert_equal (values (d), [10; 200; 30]); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d([5, 6]) = [50, 60]; %! assert_equal (keys (d), [1; 2; 3; 5; 6]); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d([7, 8]) = 0; %! assert_equal (values (d), [10; 20; 30; 0; 0]); %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d([9, 10]) = [1, 2, 3]; %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(11) = []; %! assert_equal (numEntries (d), 3); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(2) = []; %! assert_equal (keys (d), [1; 3]); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(2) = []; %! d(2) = 2; %! assert_equal (keys (d), [1; 3; 2]); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d([1, 3]) = []; %! assert_equal (keys (d), 2); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(NaN) = 1; %! d(NaN) = 2; %! assert_equal (d(NaN), 2); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(NaN) = 1; %! d(NaN) = 2; %! assert_equal (numEntries (d), 4); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(-0) = 5; %! assert_equal (d(0), 5); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(-0) = 5; %! assert_equal (keys (d), [1; 2; 3; -0]); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(string ('3')) = 33; %! assert_equal (d(3), 33); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d('a') = 1; %! assert_equal (d(97), 1); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(int8 (4)) = int8 (9); %! assert_equal (d(4), 9); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(12) = string ('5'); %! assert_equal (d(12), 5); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(12) = string ('x'); %! assert_equal (d(12), NaN); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(13) = 'x'; %! assert_equal (d(13), 120); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(13) = true; %! assert_equal (d(13), 1); %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(14) = {1}; %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(20) = 1; %! d(21) = d; %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(:) = 9; %! assert_equal (keys (d), [1; 2; 3; 58]); %!test %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(':') = 9; %! assert_equal (d(':'), 9); %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(1, 2) = 9; %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d{1} = 9; %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d.x = 9; %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(9).x = 9; %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d(4) = [1, 2]; %!error ... %! d = dictionary ([1, 2, 3], [10, 20, 30]); %! d([4, 5]) = [1; 2]; %!test %! c = dictionary (string ({'a', 'b'}), {1, 'two'}); %! assert_equal (c{string('a')}, 1); %!test %! c = dictionary (string ({'a', 'b'}), {1, 'two'}); %! assert_equal (c(string ('a')), {1}); %!test %! c = dictionary (string ({'a', 'b'}), {1, 'two'}); %! assert_equal (c(string ({'b', 'a'})), {'two', 1}); %!error ... %! c = dictionary (string ({'a', 'b'}), {1, 'two'}); %! c{string('z')}; %!test %! c = dictionary (string ({'a', 'b'}), {1, 'two'}); %! c{string('c')} = 3; %! assert_equal (c(string ('c')), {3}); %!test %! c = dictionary (string ({'a', 'b'}), {1, 'two'}); %! c{string('d')} = []; %! assert_equal (c(string ('d')), {zeros(0, 0)}); %!error ... %! c = dictionary (string ({'a', 'b'}), {1, 'two'}); %! c{string({'e', 'f'})} = 5; %!test %! c = dictionary (string ({'a', 'b'}), {1, 'two'}); %! c(string ('g')) = {[1, 2, 3]}; %! assert_equal (c{string('g')}(2), 2); %!test %! c = dictionary (string ({'a', 'b'}), {1, 'two'}); %! c(string ('g')) = {[1, 2, 3]}; %! c{string('g')}(2) = 9; %! assert_equal (c{string('g')}, [1, 9, 3]); %!error ... %! c = dictionary (string ({'a', 'b'}), {1, 'two'}); %! c(string ('h')) = 7; %!error ... %! c = dictionary (string ({'a', 'b'}), {1, 'two'}); %! c(string ('i')) = {}; %!test %! c = dictionary (string ({'a', 'b'}), {1, 'two'}); %! c(string ('a')) = []; %! assert_equal (keys (c), string ('b')); %!test %! c = dictionary (string ({'a', 'b'}), {1, 'two'}); %! c{string('new')}(3) = 7; %! assert_equal (c{string('new')}, [0, 0, 7]); %!test %! c = dictionary (string ({'a', 'b'}), {1, 'two'}); %! c{string('new')}.f = 7; %! assert_equal (c{string('new')}, struct ('f', 7)); %!test %! c = dictionary (string ({'a', 'b'}), {1, 'two'}); %! c{:} = 5; %! assert_equal (keys (c), string ({'a'; 'b'; ':'})); %!error ... %! n = dictionary (1, 2); %! n{1}; %!error ... %! n = dictionary (1, 2); %! n{1} = 3; %!test %! s = dictionary ([1, 2], struct ('a', {10, 20})); %! assert_equal (s(2).a, 20); %!test %! s = dictionary ([1, 2], struct ('a', {10, 20})); %! s(2).a = 5; %! assert_equal (s(2), struct ('a', 5)); %!error ... %! s = dictionary ([1, 2], struct ('a', {10, 20})); %! s(3).a = 7; %!test %! s = dictionary ([1, 2], struct ('a', {10, 20})); %! r = s([1, 2]); %! assert_equal (size (r), [1, 2]); %! assert_equal (fieldnames (r), {'a'}); %! assert_equal (r(1).a, 10); %! assert_equal (r(2).a, 20); %!test %! s = dictionary ([1, 2], struct ('a', {10, 20})); %! s(3) = struct ('b', 1); %! assert_equal (s(3), struct ('b', 1)); %!test %! s = dictionary ([1, 2], struct ('a', {10, 20})); %! s(3) = struct ('b', 1); %! r = values (s, 'cell'); %! assert_equal (r, {struct('a', 10); struct('a', 20); struct('b', 1)}); %!error ... %! s = dictionary ([1, 2], struct ('a', {10, 20})); %! s(3) = struct ('b', 1); %! values (s); %!error ... %! s = dictionary ([1, 2], struct ('a', {10, 20})); %! s(3) = struct ('b', 1); %! s([1, 3]); %!test %! s = dictionary ([1, 2], struct ('a', {10, 20})); %! s(3) = struct ('b', 1); %! r = entries (s, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (r(1).Key, 1); %! assert_equal (r(1).Value, struct ('a', 10)); %! assert_equal (r(2).Key, 2); %! assert_equal (r(2).Value, struct ('a', 20)); %! assert_equal (r(3).Key, 3); %! assert_equal (r(3).Value, struct ('b', 1)); %!error ... %! s = dictionary ([1, 2], struct ('a', {10, 20})); %! s(3) = struct ('b', 1); %! entries (s); %!test %! z = dictionary (1, datetime (2024, 1, 1, 'TimeZone', 'UTC')); %! z(2) = datetime (2024, 1, 2); %! r = values (z, 'cell'); %! assert_equal (size (r), [2, 1]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-01 00:00:00.000'}); %! assert_equal (x1.TimeZone, 'UTC'); %! x2 = r{2}; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), {'2024-01-02 00:00:00.000'}); %! assert_equal (x2.TimeZone, ''); %!error ... %! z = dictionary (1, datetime (2024, 1, 1, 'TimeZone', 'UTC')); %! z(2) = datetime (2024, 1, 2); %! values (z); %!test %! z = dictionary (1, datetime (2024, 1, 1, 'TimeZone', 'UTC')); %! z(3) = datetime (2024, 1, 3, 'TimeZone', 'Europe/Athens'); %! r = values (z, 'cell'); %! assert_equal (size (r), [2, 1]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-01 00:00:00.000'}); %! assert_equal (x1.TimeZone, 'UTC'); %! x2 = r{2}; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), {'2024-01-03 00:00:00.000'}); %! assert_equal (x2.TimeZone, 'Europe/Athens'); %!test %! z = dictionary (1, datetime (2024, 1, 1)); %! z(2) = datetime (2024, 1, 2, 'TimeZone', 'UTC'); %! r = values (z, 'cell'); %! assert_equal (size (r), [2, 1]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-01 00:00:00.000'}); %! assert_equal (x1.TimeZone, ''); %! x2 = r{2}; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), {'2024-01-02 00:00:00.000'}); %! assert_equal (x2.TimeZone, 'UTC'); %!test %! z = dictionary (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 1); %! assert_equal (isKey (z, datetime (2024, 1, 1)), false); %!test %! z = dictionary (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 1); %! z(datetime (2024, 1, 2)) = 2; %! r = keys (z, 'cell'); %! assert_equal (size (r), [2, 1]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-01 00:00:00.000'}); %! assert_equal (x1.TimeZone, 'UTC'); %! x2 = r{2}; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), {'2024-01-02 00:00:00.000'}); %! assert_equal (x2.TimeZone, ''); %!error ... %! z = dictionary (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 1); %! z(datetime (2024, 1, 2)) = 2; %! keys (z); %!test %! z = dictionary (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 1); %! z(datetime (2023, 12, 31, 19, 0, 0, 'TimeZone', 'America/New_York')) = 5; %! assert_equal (values (z), 5); %!test %! z = dictionary (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 1); %! z(datetime (2024, 1, 1, 0, 0, 0, 'TimeZone', 'Europe/Athens')) = 6; %! r = keys (z); %! x1 = r; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), ... %! {'2024-01-01 00:00:00.000'; '2023-12-31 22:00:00.000'}); %! assert_equal (x1.TimeZone, 'UTC'); ## D4: text is read in the time zone of the keys, where MATLAB reads none. %!test %! z = dictionary (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 1); %! z('2024-01-02') = 2; %! r = keys (z); %! x1 = r; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), ... %! {'2024-01-01 00:00:00.000'; '2024-01-02 00:00:00.000'}); %! assert_equal (x1.TimeZone, 'UTC'); %!test %! z = dictionary (datetime (2024, 1, 1), 1); %! assert_equal (isKey (z, datetime (2024, 1, 1, 'TimeZone', 'UTC')), false); %!error ... %! g = dictionary (datetime (2020, 1, 1), 1); %! g(string ('garbage')); %!error ... %! g = dictionary (datetime (2020, 1, 1), 1); %! isKey (g, string ('garbage')); %!test %! k = dictionary (string (':'), 1); %! assert_equal (k(':'), 1); %!test %! k = dictionary (string (':'), 1); %! assert_equal (isKey (k, ':'), true); %!test %! v = dictionary ([1, 2], string ({'a', 'b'})); %! r = lookup (v, [1, 3], 'FallbackValue', 'none'); %! assert_equal (r, string ({'a', 'none'})); %!test %! v = dictionary ([1, 2], datetime (2024, 1, [1, 2])); %! r = lookup (v, [1, 3], 'FallbackValue', '2021-01-01'); %! x1 = r; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), ... %! {'2024-01-01 00:00:00.000', '2021-01-01 00:00:00.000'}); %! assert_equal (x1.TimeZone, ''); %!test %! w = dictionary (string ({'x', 'y'}), [1, 2]); %! w2 = w; %! w2(string ('x')) = 100; %! assert_equal (w(string ('x')), 1); %!test %! r = dictionary (1:1000, 1:1000); %! r = remove (r, 2:2:1000); %! assert_equal (numEntries (r), 500); %!test %! r = dictionary (1:1000, 1:1000); %! r = remove (r, 2:2:1000); %! assert_equal (r([999, 1]), [999, 1]); %!test %! r = dictionary (1:1000, 1:1000); %! r = remove (r, 2:2:1000); %! assert_equal (isKey (r, [1, 2, 3, 4]), [true, false, true, false]); %!test %! r = dictionary (1:1000, 1:1000); %! r = remove (r, 2:2:1000); %! r(2) = 2; %! assert_equal (sum (values (r)), 250002); %!test %! p = dictionary (string ({'a', 'b', 'c', 'd', 'e'}), 1:5); %! p(string ({'b', 'd'})) = []; %! p(string ('b')) = 20; %! p(string ('a')) = 10; %! assert_equal (keys (p), string ({'a'; 'c'; 'e'; 'b'})); %!test %! p = dictionary (string ({'a', 'b', 'c', 'd', 'e'}), 1:5); %! p(string ({'b', 'd'})) = []; %! p(string ('b')) = 20; %! p(string ('a')) = 10; %! assert_equal (values (p), [10; 3; 5; 20]); %!test %! p = dictionary (string ({'a', 'b', 'c', 'd', 'e'}), 1:5); %! p = insert (p, string ({'f', 'b'}), [6, 200]); %! assert_equal (values (p), [1; 200; 3; 4; 5; 6]); %!test %! u = dictionary (); %! u(1) = 2; %! r = u; %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! assert_equal (values (r, 'cell'), {2}); %!test %! u = dictionary (); %! u(string ('a')) = 'x'; %! r = u; %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'string'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {string('x')}); %!test %! u = dictionary (); %! u('ab') = 1; %! r = u; %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('ab')}); %! assert_equal (values (r, 'cell'), {1}); %!error ... %! u = dictionary (); %! u(['ab'; 'cd']) = [1, 2]; %!test %! u = dictionary (); %! u(['ab'; 'cd']) = [1; 2]; %! r = u; %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('ab'); string('cd')}); %! assert_equal (values (r, 'cell'), {1; 2}); %!error ... %! u = dictionary (); %! u([]) = []; %!test %! u = dictionary (); %! u([]) = 5; %! r = u; %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 0); %!test %! u = dictionary (); %! u{1} = 2; %! r = u; %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! assert_equal (values (r, 'cell'), {{2}}); %!error ... %! u = dictionary (); %! u(1) = []; %!error ... %! u = dictionary (); %! u(1); %!test %! u = dictionary (); %! u(string ('a')) = {1}; %! r = u; %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {{1}}); %!test %! u = dictionary (); %! u(datetime (2024, 1, 1)) = hours (1); %! r = u; %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'datetime', 'duration'})); %! assert_equal (numEntries (r), 1); %! kc3 = keys (r, 'cell'); %! x5 = kc3{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, ''); %! vc4 = values (r, 'cell'); %! assert_equal (class (vc4{1}), 'duration'); %! assert_equal (seconds (vc4{1}), 3600); %!error ... %! u = dictionary (); %! u(table (1)) = 1; ## D3: a table of any size is one value, where MATLAB takes only 1-by-1. %!test %! u = dictionary (); %! u(1) = table ([1; 2]); %! r = u; %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'table'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {1}); %! vc4 = values (r, 'cell'); %! tb5 = vc4{1}; %! assert_equal (size (tb5), [2, 1]); %! assert_equal (tb5.Properties.VariableNames, {'Var1'}); %! assert_equal (tb5.Var1, [1; 2]); %!test %! u = dictionary (); %! u{string('a')}(2) = 7; %! r = u; %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'cell'})); %! assert_equal (numEntries (r), 1); %! assert_equal (keys (r, 'cell'), {string('a')}); %! assert_equal (values (r, 'cell'), {{[0, 7]}}); ## Keys of type 'double', measured on R2024a %!shared D, R, CV %! D = dictionary (1, 10); %! D(2) = 20; %! D(3) = 30; %! R = dictionary (3, 30); %! R(2) = 20; %! R(1) = 10; %! CV = dictionary (1, {1}); %! CV(2) = {'two'}; %! CV(3) = {[3, 4]}; %!assert_equal (keys (D), [1; 2; 3]) %!assert_equal (values (D), [10; 20; 30]) %!assert_equal (keys (D, 'cell'), {1; 2; 3}) %!assert_equal (numEntries (D), 3) %!assert_equal (D(2), 20) %!assert_equal (isKey (D, 1), true) %!assert_equal (lookup (D, 2), 20) %!assert_equal (isKey (D, 9), false) %!assert_equal (lookup (D, 9, 'FallbackValue', 0), 0) %!error ... %! D(9); %!test %! d = D; %! d(9) = []; %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d = remove (d, 9); %! assert_equal (numEntries (d), 3); %!assert_equal (D([1, 3]), [10, 30]) %!assert_equal (isKey (D, [1, 3]), [true, true]) %!test %! r = dictionary ([1, 3], 7); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'double', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {1; 3}); %! assert_equal (values (r, 'cell'), {7; 7}); %!test %! d = D; %! d(4) = 40; %! assert_equal (keys (d, 'cell'), {1; 2; 3; 4}); %!test %! d = D; %! d = insert (d, 4, 40); %! assert_equal (values (d), [10; 20; 30; 40]); %!test %! d = D; %! d(1) = 99; %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, 1, 99); %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, 1, 99, 'Overwrite', false); %! assert_equal (values (d), [10; 20; 30]); %!test %! d = D; %! d = remove (d, 1); %! assert_equal (keys (d, 'cell'), {2; 3}); %!test %! d = D; %! d(1) = []; %! assert_equal (values (d), [20; 30]); %!test %! d = D; %! d = remove (d, 1); %! d(1) = 11; %! assert_equal (values (d), [20; 30; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (r(1).Key, 1); %! assert_equal (r(1).Value, 10); %! assert_equal (r(2).Key, 2); %! assert_equal (r(2).Value, 20); %! assert_equal (r(3).Key, 3); %! assert_equal (r(3).Value, 30); %!assert_equal (entries (D, 'cell'), {1, 10; 2, 20; 3, 30}) %!test %! r = entries (D); %! tb1 = r; %! assert_equal (size (tb1), [3, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! assert_equal (tb1.Key, [1; 2; 3]); %! assert_equal (tb1.Value, [10; 20; 30]); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!assert_equal (CV{2}, 'two') %!test %! c = CV; %! c{1} = 5; %! assert_equal (c(1), {5}); %!test %! c = CV; %! c{2}(2) = 7; %! r = c{2}; %! assert_equal (double (r), [116, 7, 111]); %!error ... %! CV{9}; ## Keys of type 'single', measured on R2024a %!shared D, R, CV %! D = dictionary (single (1), 10); %! D(single (2)) = 20; %! D(single (3)) = 30; %! R = dictionary (single (3), 30); %! R(single (2)) = 20; %! R(single (1)) = 10; %! CV = dictionary (single (1), {1}); %! CV(single (2)) = {'two'}; %! CV(single (3)) = {[3, 4]}; %!assert_equal (keys (D), single ([1; 2; 3])) %!assert_equal (values (D), [10; 20; 30]) %!assert_equal (keys (D, 'cell'), {single(1); single(2); single(3)}) %!assert_equal (numEntries (D), 3) %!assert_equal (D(single (2)), 20) %!assert_equal (isKey (D, single (1)), true) %!assert_equal (lookup (D, single (2)), 20) %!assert_equal (isKey (D, single (9)), false) %!assert_equal (lookup (D, single (9), 'FallbackValue', 0), 0) %!error ... %! D(single (9)); %!test %! d = D; %! d(single (9)) = []; %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d = remove (d, single (9)); %! assert_equal (numEntries (d), 3); %!assert_equal (D(single ([1, 3])), [10, 30]) %!assert_equal (isKey (D, single ([1, 3])), [true, true]) %!test %! r = dictionary (single ([1, 3]), 7); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'single', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {single(1); single(3)}); %! assert_equal (values (r, 'cell'), {7; 7}); %!test %! d = D; %! d(single (4)) = 40; %! r = keys (d, 'cell'); %! assert_equal (r, {single(1); single(2); single(3); single(4)}); %!test %! d = D; %! d = insert (d, single (4), 40); %! assert_equal (values (d), [10; 20; 30; 40]); %!test %! d = D; %! d(single (1)) = 99; %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, single (1), 99); %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, single (1), 99, 'Overwrite', false); %! assert_equal (values (d), [10; 20; 30]); %!test %! d = D; %! d = remove (d, single (1)); %! assert_equal (keys (d, 'cell'), {single(2); single(3)}); %!test %! d = D; %! d(single (1)) = []; %! assert_equal (values (d), [20; 30]); %!test %! d = D; %! d = remove (d, single (1)); %! d(single (1)) = 11; %! assert_equal (values (d), [20; 30; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (r(1).Key, single (1)); %! assert_equal (r(1).Value, 10); %! assert_equal (r(2).Key, single (2)); %! assert_equal (r(2).Value, 20); %! assert_equal (r(3).Key, single (3)); %! assert_equal (r(3).Value, 30); %!test %! r = entries (D, 'cell'); %! assert_equal (r, {single(1), 10; single(2), 20; single(3), 30}); %!test %! r = entries (D); %! tb1 = r; %! assert_equal (size (tb1), [3, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! assert_equal (tb1.Key, single ([1; 2; 3])); %! assert_equal (tb1.Value, [10; 20; 30]); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!assert_equal (CV{single(2)}, 'two') %!test %! c = CV; %! c{single(1)} = 5; %! assert_equal (c(single (1)), {5}); %!test %! c = CV; %! c{single(2)}(2) = 7; %! r = c{single(2)}; %! assert_equal (double (r), [116, 7, 111]); %!error ... %! CV{single(9)}; ## Keys of type 'int8', measured on R2024a %!shared D, R, CV %! D = dictionary (int8 (1), 10); %! D(int8 (2)) = 20; %! D(int8 (3)) = 30; %! R = dictionary (int8 (3), 30); %! R(int8 (2)) = 20; %! R(int8 (1)) = 10; %! CV = dictionary (int8 (1), {1}); %! CV(int8 (2)) = {'two'}; %! CV(int8 (3)) = {[3, 4]}; %!assert_equal (keys (D), int8 ([1; 2; 3])) %!assert_equal (values (D), [10; 20; 30]) %!assert_equal (keys (D, 'cell'), {int8(1); int8(2); int8(3)}) %!assert_equal (numEntries (D), 3) %!assert_equal (D(int8 (2)), 20) %!assert_equal (isKey (D, int8 (1)), true) %!assert_equal (lookup (D, int8 (2)), 20) %!assert_equal (isKey (D, int8 (9)), false) %!assert_equal (lookup (D, int8 (9), 'FallbackValue', 0), 0) %!error ... %! D(int8 (9)); %!test %! d = D; %! d(int8 (9)) = []; %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d = remove (d, int8 (9)); %! assert_equal (numEntries (d), 3); %!assert_equal (D(int8 ([1, 3])), [10, 30]) %!assert_equal (isKey (D, int8 ([1, 3])), [true, true]) %!test %! r = dictionary (int8 ([1, 3]), 7); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'int8', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {int8(1); int8(3)}); %! assert_equal (values (r, 'cell'), {7; 7}); %!test %! d = D; %! d(int8 (4)) = 40; %! assert_equal (keys (d, 'cell'), {int8(1); int8(2); int8(3); int8(4)}); %!test %! d = D; %! d = insert (d, int8 (4), 40); %! assert_equal (values (d), [10; 20; 30; 40]); %!test %! d = D; %! d(int8 (1)) = 99; %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, int8 (1), 99); %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, int8 (1), 99, 'Overwrite', false); %! assert_equal (values (d), [10; 20; 30]); %!test %! d = D; %! d = remove (d, int8 (1)); %! assert_equal (keys (d, 'cell'), {int8(2); int8(3)}); %!test %! d = D; %! d(int8 (1)) = []; %! assert_equal (values (d), [20; 30]); %!test %! d = D; %! d = remove (d, int8 (1)); %! d(int8 (1)) = 11; %! assert_equal (values (d), [20; 30; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (r(1).Key, int8 (1)); %! assert_equal (r(1).Value, 10); %! assert_equal (r(2).Key, int8 (2)); %! assert_equal (r(2).Value, 20); %! assert_equal (r(3).Key, int8 (3)); %! assert_equal (r(3).Value, 30); %!assert_equal (entries (D, 'cell'), {int8(1), 10; int8(2), 20; int8(3), 30}) %!test %! r = entries (D); %! tb1 = r; %! assert_equal (size (tb1), [3, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! assert_equal (tb1.Key, int8 ([1; 2; 3])); %! assert_equal (tb1.Value, [10; 20; 30]); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!assert_equal (CV{int8(2)}, 'two') %!test %! c = CV; %! c{int8(1)} = 5; %! assert_equal (c(int8 (1)), {5}); %!test %! c = CV; %! c{int8(2)}(2) = 7; %! r = c{int8(2)}; %! assert_equal (double (r), [116, 7, 111]); %!error ... %! CV{int8(9)}; ## Keys of type 'uint64', measured on R2024a %!shared D, R, CV %! D = dictionary (intmax ('uint64') - 2, 10); %! D(intmax ('uint64') - 1) = 20; %! D(intmax ('uint64')) = 30; %! R = dictionary (intmax ('uint64'), 30); %! R(intmax ('uint64') - 1) = 20; %! R(intmax ('uint64') - 2) = 10; %! CV = dictionary (intmax ('uint64') - 2, {1}); %! CV(intmax ('uint64') - 1) = {'two'}; %! CV(intmax ('uint64')) = {[3, 4]}; %!test %! r = keys (D); %! assert_equal (r, ... %! [intmax('uint64') - 2; intmax('uint64') - 1; ... %! intmax('uint64')]); %!assert_equal (values (D), [10; 20; 30]) %!test %! r = keys (D, 'cell'); %! assert_equal (r, ... %! {intmax('uint64') - 2; intmax('uint64') - 1; ... %! intmax('uint64')}); %!assert_equal (numEntries (D), 3) %!assert_equal (D(intmax ('uint64') - 1), 20) %!assert_equal (isKey (D, intmax ('uint64') - 2), true) %!assert_equal (lookup (D, intmax ('uint64') - 1), 20) %!assert_equal (isKey (D, uint64 (9)), false) %!assert_equal (lookup (D, uint64 (9), 'FallbackValue', 0), 0) %!error ... %! D(uint64 (9)); %!test %! d = D; %! d(uint64 (9)) = []; %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d = remove (d, uint64 (9)); %! assert_equal (numEntries (d), 3); %!assert_equal (D([intmax('uint64') - 2, intmax('uint64')]), [10, 30]) %!test %! r = isKey (D, [intmax('uint64') - 2, intmax('uint64')]); %! assert_equal (r, [true, true]); %!test %! r = dictionary ([intmax('uint64') - 2, intmax('uint64')], 7); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'uint64', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {intmax('uint64') - 2; intmax('uint64')}); %! assert_equal (values (r, 'cell'), {7; 7}); %!test %! d = D; %! d(uint64 (4)) = 40; %! r = keys (d, 'cell'); %! assert_equal (r, ... %! {intmax('uint64') - 2; intmax('uint64') - 1; ... %! intmax('uint64'); uint64(4)}); %!test %! d = D; %! d = insert (d, uint64 (4), 40); %! assert_equal (values (d), [10; 20; 30; 40]); %!test %! d = D; %! d(intmax ('uint64') - 2) = 99; %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, intmax ('uint64') - 2, 99); %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, intmax ('uint64') - 2, 99, 'Overwrite', false); %! assert_equal (values (d), [10; 20; 30]); %!test %! d = D; %! d = remove (d, intmax ('uint64') - 2); %! assert_equal (keys (d, 'cell'), {intmax('uint64') - 1; intmax('uint64')}); %!test %! d = D; %! d(intmax ('uint64') - 2) = []; %! assert_equal (values (d), [20; 30]); %!test %! d = D; %! d = remove (d, intmax ('uint64') - 2); %! d(intmax ('uint64') - 2) = 11; %! assert_equal (values (d), [20; 30; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (r(1).Key, intmax ('uint64') - 2); %! assert_equal (r(1).Value, 10); %! assert_equal (r(2).Key, intmax ('uint64') - 1); %! assert_equal (r(2).Value, 20); %! assert_equal (r(3).Key, intmax ('uint64')); %! assert_equal (r(3).Value, 30); %!test %! r = entries (D, 'cell'); %! assert_equal (r, ... %! {intmax('uint64') - 2, 10; intmax('uint64') - 1, 20; ... %! intmax('uint64'), 30}); %!test %! r = entries (D); %! tb1 = r; %! assert_equal (size (tb1), [3, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! assert_equal (tb1.Key, ... %! [intmax('uint64') - 2; intmax('uint64') - 1; ... %! intmax('uint64')]); %! assert_equal (tb1.Value, [10; 20; 30]); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!assert_equal (CV{intmax('uint64') - 1}, 'two') %!test %! c = CV; %! c{intmax('uint64') - 2} = 5; %! assert_equal (c(intmax ('uint64') - 2), {5}); %!test %! c = CV; %! c{intmax('uint64') - 1}(2) = 7; %! r = c{intmax('uint64') - 1}; %! assert_equal (double (r), [116, 7, 111]); %!error ... %! CV{uint64(9)}; ## Keys of type 'string', measured on R2024a %!shared D, R, CV %! D = dictionary (string ('a'), 10); %! D(string ('b')) = 20; %! D(string ('c')) = 30; %! R = dictionary (string ('c'), 30); %! R(string ('b')) = 20; %! R(string ('a')) = 10; %! CV = dictionary (string ('a'), {1}); %! CV(string ('b')) = {'two'}; %! CV(string ('c')) = {[3, 4]}; %!assert_equal (keys (D), string ({'a'; 'b'; 'c'})) %!assert_equal (values (D), [10; 20; 30]) %!assert_equal (keys (D, 'cell'), {string('a'); string('b'); string('c')}) %!assert_equal (numEntries (D), 3) %!assert_equal (D(string ('b')), 20) %!assert_equal (isKey (D, string ('a')), true) %!assert_equal (lookup (D, string ('b')), 20) %!assert_equal (isKey (D, string ('z')), false) %!assert_equal (lookup (D, string ('z'), 'FallbackValue', 0), 0) %!error ... %! D(string ('z')); %!test %! d = D; %! d(string ('z')) = []; %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d = remove (d, string ('z')); %! assert_equal (numEntries (d), 3); %!assert_equal (D(string ({'a', 'c'})), [10, 30]) %!assert_equal (isKey (D, string ({'a', 'c'})), [true, true]) %!test %! r = dictionary (string ({'a', 'c'}), 7); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'string', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {string('a'); string('c')}); %! assert_equal (values (r, 'cell'), {7; 7}); %!test %! d = D; %! d(string ('d')) = 40; %! r = keys (d, 'cell'); %! assert_equal (r, {string('a'); string('b'); string('c'); string('d')}); %!test %! d = D; %! d = insert (d, string ('d'), 40); %! assert_equal (values (d), [10; 20; 30; 40]); %!test %! d = D; %! d(string ('a')) = 99; %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, string ('a'), 99); %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, string ('a'), 99, 'Overwrite', false); %! assert_equal (values (d), [10; 20; 30]); %!test %! d = D; %! d = remove (d, string ('a')); %! assert_equal (keys (d, 'cell'), {string('b'); string('c')}); %!test %! d = D; %! d(string ('a')) = []; %! assert_equal (values (d), [20; 30]); %!test %! d = D; %! d = remove (d, string ('a')); %! d(string ('a')) = 11; %! assert_equal (values (d), [20; 30; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (r(1).Key, string ('a')); %! assert_equal (r(1).Value, 10); %! assert_equal (r(2).Key, string ('b')); %! assert_equal (r(2).Value, 20); %! assert_equal (r(3).Key, string ('c')); %! assert_equal (r(3).Value, 30); %!test %! r = entries (D, 'cell'); %! assert_equal (r, {string('a'), 10; string('b'), 20; string('c'), 30}); %!test %! r = entries (D); %! tb1 = r; %! assert_equal (size (tb1), [3, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! assert_equal (tb1.Key, string ({'a'; 'b'; 'c'})); %! assert_equal (tb1.Value, [10; 20; 30]); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!assert_equal (CV{string('b')}, 'two') %!test %! c = CV; %! c{string('a')} = 5; %! assert_equal (c(string ('a')), {5}); %!test %! c = CV; %! c{string('b')}(2) = 7; %! r = c{string('b')}; %! assert_equal (double (r), [116, 7, 111]); %!error ... %! CV{string('z')}; ## Keys of type 'cell', measured on R2024a %!shared D, R, CV %! D = dictionary ({'a'}, 10); %! D({1}) = 20; %! D({[1, 2]}) = 30; %! R = dictionary ({[1, 2]}, 30); %! R({1}) = 20; %! R({'a'}) = 10; %! CV = dictionary ({'a'}, {1}); %! CV({1}) = {'two'}; %! CV({[1, 2]}) = {[3, 4]}; %!assert_equal (keys (D), {'a'; 1; [1, 2]}) %!assert_equal (values (D), [10; 20; 30]) %!assert_equal (keys (D, 'cell'), {{'a'}; {1}; {[1, 2]}}) %!assert_equal (numEntries (D), 3) %!assert_equal (D({1}), 20) %!assert_equal (isKey (D, {'a'}), true) %!assert_equal (lookup (D, {1}), 20) %!assert_equal (isKey (D, {'z'}), false) %!assert_equal (lookup (D, {'z'}, 'FallbackValue', 0), 0) %!error ... %! D({'z'}); %!test %! d = D; %! d({'z'}) = []; %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d = remove (d, {'z'}); %! assert_equal (numEntries (d), 3); %!assert_equal (D({'a', [1, 2]}), [10, 30]) %!assert_equal (isKey (D, {'a', [1, 2]}), [true, true]) %!test %! r = dictionary ({'a', [1, 2]}, 7); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'cell', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {{'a'}; {[1, 2]}}); %! assert_equal (values (r, 'cell'), {7; 7}); %!test %! d = D; %! d({'d'}) = 40; %! assert_equal (keys (d, 'cell'), {{'a'}; {1}; {[1, 2]}; {'d'}}); %!test %! d = D; %! d = insert (d, {'d'}, 40); %! assert_equal (values (d), [10; 20; 30; 40]); %!test %! d = D; %! d({'a'}) = 99; %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, {'a'}, 99); %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, {'a'}, 99, 'Overwrite', false); %! assert_equal (values (d), [10; 20; 30]); %!test %! d = D; %! d = remove (d, {'a'}); %! assert_equal (keys (d, 'cell'), {{1}; {[1, 2]}}); %!test %! d = D; %! d({'a'}) = []; %! assert_equal (values (d), [20; 30]); %!test %! d = D; %! d = remove (d, {'a'}); %! d({'a'}) = 11; %! assert_equal (values (d), [20; 30; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (r(1).Key, {'a'}); %! assert_equal (r(1).Value, 10); %! assert_equal (r(2).Key, {1}); %! assert_equal (r(2).Value, 20); %! assert_equal (r(3).Key, {[1, 2]}); %! assert_equal (r(3).Value, 30); %!assert_equal (entries (D, 'cell'), {{'a'}, 10; {1}, 20; {[1, 2]}, 30}) %!test %! r = entries (D); %! tb1 = r; %! assert_equal (size (tb1), [3, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! assert_equal (tb1.Key, {'a'; 1; [1, 2]}); %! assert_equal (tb1.Value, [10; 20; 30]); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!assert_equal (CV{{1}}, 'two') %!test %! c = CV; %! c{{'a'}} = 5; %! assert_equal (c({'a'}), {5}); %!test %! c = CV; %! c{{1}}(2) = 7; %! r = c{{1}}; %! assert_equal (double (r), [116, 7, 111]); %!error ... %! CV{{'z'}}; ## Keys of type 'struct', measured on R2024a %!shared D, R, CV %! D = dictionary (struct ('a', 1), 10); %! D(struct ('a', 2)) = 20; %! D(struct ('a', 3)) = 30; %! R = dictionary (struct ('a', 3), 30); %! R(struct ('a', 2)) = 20; %! R(struct ('a', 1)) = 10; %! CV = dictionary (struct ('a', 1), {1}); %! CV(struct ('a', 2)) = {'two'}; %! CV(struct ('a', 3)) = {[3, 4]}; %!test %! r = keys (D); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'a'}); %! assert_equal (r(1).a, 1); %! assert_equal (r(2).a, 2); %! assert_equal (r(3).a, 3); %!assert_equal (values (D), [10; 20; 30]) %!test %! r = keys (D, 'cell'); %! assert_equal (r, {struct('a', 1); struct('a', 2); struct('a', 3)}); %!assert_equal (numEntries (D), 3) %!assert_equal (D(struct ('a', 2)), 20) %!assert_equal (isKey (D, struct ('a', 1)), true) %!assert_equal (lookup (D, struct ('a', 2)), 20) %!assert_equal (isKey (D, struct ('a', 9)), false) %!assert_equal (lookup (D, struct ('a', 9), 'FallbackValue', 0), 0) %!error ... %! D(struct ('a', 9)); %!test %! d = D; %! d(struct ('a', 9)) = []; %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d = remove (d, struct ('a', 9)); %! assert_equal (numEntries (d), 3); %!assert_equal (D(struct ('a', {1, 3})), [10, 30]) %!assert_equal (isKey (D, struct ('a', {1, 3})), [true, true]) %!test %! r = dictionary (struct ('a', {1, 3}), 7); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'struct', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {struct('a', 1); struct('a', 3)}); %! assert_equal (values (r, 'cell'), {7; 7}); %!test %! d = D; %! d(struct ('a', 4)) = 40; %! r = keys (d, 'cell'); %! assert_equal (r, ... %! {struct('a', 1); struct('a', 2); struct('a', 3); ... %! struct('a', 4)}); %!test %! d = D; %! d = insert (d, struct ('a', 4), 40); %! assert_equal (values (d), [10; 20; 30; 40]); %!test %! d = D; %! d(struct ('a', 1)) = 99; %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, struct ('a', 1), 99); %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, struct ('a', 1), 99, 'Overwrite', false); %! assert_equal (values (d), [10; 20; 30]); %!test %! d = D; %! d = remove (d, struct ('a', 1)); %! assert_equal (keys (d, 'cell'), {struct('a', 2); struct('a', 3)}); %!test %! d = D; %! d(struct ('a', 1)) = []; %! assert_equal (values (d), [20; 30]); %!test %! d = D; %! d = remove (d, struct ('a', 1)); %! d(struct ('a', 1)) = 11; %! assert_equal (values (d), [20; 30; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (r(1).Key, struct ('a', 1)); %! assert_equal (r(1).Value, 10); %! assert_equal (r(2).Key, struct ('a', 2)); %! assert_equal (r(2).Value, 20); %! assert_equal (r(3).Key, struct ('a', 3)); %! assert_equal (r(3).Value, 30); %!test %! r = entries (D, 'cell'); %! assert_equal (r, ... %! {struct('a', 1), 10; struct('a', 2), 20; struct('a', 3), 30}); %!test %! r = entries (D); %! tb1 = r; %! assert_equal (size (tb1), [3, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! st2 = tb1.Key; %! assert_equal (size (st2), [3, 1]); %! assert_equal (fieldnames (st2), {'a'}); %! assert_equal (st2(1).a, 1); %! assert_equal (st2(2).a, 2); %! assert_equal (st2(3).a, 3); %! assert_equal (tb1.Value, [10; 20; 30]); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!assert_equal (CV{struct('a', 2)}, 'two') %!test %! c = CV; %! c{struct('a', 1)} = 5; %! assert_equal (c(struct ('a', 1)), {5}); %!test %! c = CV; %! c{struct('a', 2)}(2) = 7; %! r = c{struct('a', 2)}; %! assert_equal (double (r), [116, 7, 111]); %!error ... %! CV{struct('a', 9)}; ## Keys of type 'handle', measured on R2024a %!shared D, R, CV %! D = dictionary (@sin, 10); %! D(@cos) = 20; %! D(@tan) = 30; %! R = dictionary (@tan, 30); %! R(@cos) = 20; %! R(@sin) = 10; %! CV = dictionary (@sin, {1}); %! CV(@cos) = {'two'}; %! CV(@tan) = {[3, 4]}; %!error ... %! keys (D); %!assert_equal (values (D), [10; 20; 30]) %!assert_equal (keys (D, 'cell'), {@sin; @cos; @tan}) %!assert_equal (numEntries (D), 3) %!assert_equal (D(@cos), 20) %!assert_equal (isKey (D, @sin), true) %!assert_equal (lookup (D, @cos), 20) %!assert_equal (isKey (D, @log), false) %!assert_equal (lookup (D, @log, 'FallbackValue', 0), 0) %!error ... %! D(@log); %!test %! d = D; %! d(@log) = []; %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d = remove (d, @log); %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d(@exp) = 40; %! assert_equal (keys (d, 'cell'), {@sin; @cos; @tan; @exp}); %!test %! d = D; %! d = insert (d, @exp, 40); %! assert_equal (values (d), [10; 20; 30; 40]); %!test %! d = D; %! d(@sin) = 99; %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, @sin, 99); %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, @sin, 99, 'Overwrite', false); %! assert_equal (values (d), [10; 20; 30]); %!test %! d = D; %! d = remove (d, @sin); %! assert_equal (keys (d, 'cell'), {@cos; @tan}); %!test %! d = D; %! d(@sin) = []; %! assert_equal (values (d), [20; 30]); %!test %! d = D; %! d = remove (d, @sin); %! d(@sin) = 11; %! assert_equal (values (d), [20; 30; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (r(1).Key, @sin); %! assert_equal (r(1).Value, 10); %! assert_equal (r(2).Key, @cos); %! assert_equal (r(2).Value, 20); %! assert_equal (r(3).Key, @tan); %! assert_equal (r(3).Value, 30); %!assert_equal (entries (D, 'cell'), {@sin, 10; @cos, 20; @tan, 30}) %!error ... %! entries (D); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!assert_equal (CV{@cos}, 'two') %!test %! c = CV; %! c{@sin} = 5; %! assert_equal (c(@sin), {5}); %!test %! c = CV; %! c{@cos}(2) = 7; %! r = c{@cos}; %! assert_equal (double (r), [116, 7, 111]); %!error ... %! CV{@log}; ## Keys of type 'datetime', measured on R2024a %!shared D, R, CV %! D = dictionary (datetime (2024, 1, 1), 10); %! D(datetime (2024, 1, 2)) = 20; %! D(datetime (2024, 1, 3)) = 30; %! R = dictionary (datetime (2024, 1, 3), 30); %! R(datetime (2024, 1, 2)) = 20; %! R(datetime (2024, 1, 1)) = 10; %! CV = dictionary (datetime (2024, 1, 1), {1}); %! CV(datetime (2024, 1, 2)) = {'two'}; %! CV(datetime (2024, 1, 3)) = {[3, 4]}; %!test %! r = keys (D); %! x1 = r; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), ... %! {'2024-01-01 00:00:00.000'; '2024-01-02 00:00:00.000'; ... %! '2024-01-03 00:00:00.000'}); %! assert_equal (x1.TimeZone, ''); %!assert_equal (values (D), [10; 20; 30]) %!test %! r = keys (D, 'cell'); %! assert_equal (size (r), [3, 1]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-01 00:00:00.000'}); %! assert_equal (x1.TimeZone, ''); %! x2 = r{2}; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), {'2024-01-02 00:00:00.000'}); %! assert_equal (x2.TimeZone, ''); %! x3 = r{3}; %! x3.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x3), {'2024-01-03 00:00:00.000'}); %! assert_equal (x3.TimeZone, ''); %!assert_equal (numEntries (D), 3) %!assert_equal (D(datetime (2024, 1, 2)), 20) %!assert_equal (isKey (D, datetime (2024, 1, 1)), true) %!assert_equal (lookup (D, datetime (2024, 1, 2)), 20) %!assert_equal (isKey (D, datetime (2025, 1, 1)), false) %!assert_equal (lookup (D, datetime (2025, 1, 1), 'FallbackValue', 0), 0) %!error ... %! D(datetime (2025, 1, 1)); %!test %! d = D; %! d(datetime (2025, 1, 1)) = []; %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d = remove (d, datetime (2025, 1, 1)); %! assert_equal (numEntries (d), 3); %!assert_equal (D(datetime (2024, 1, [1, 3])), [10, 30]) %!assert_equal (isKey (D, datetime (2024, 1, [1, 3])), [true, true]) %!test %! r = dictionary (datetime (2024, 1, [1, 3]), 7); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'datetime', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! x5 = kc3{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, ''); %! x6 = kc3{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'2024-01-03 00:00:00.000'}); %! assert_equal (x6.TimeZone, ''); %! assert_equal (values (r, 'cell'), {7; 7}); %!test %! d = D; %! d(datetime (2024, 1, 4)) = 40; %! r = keys (d, 'cell'); %! assert_equal (size (r), [4, 1]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-01 00:00:00.000'}); %! assert_equal (x1.TimeZone, ''); %! x2 = r{2}; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), {'2024-01-02 00:00:00.000'}); %! assert_equal (x2.TimeZone, ''); %! x3 = r{3}; %! x3.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x3), {'2024-01-03 00:00:00.000'}); %! assert_equal (x3.TimeZone, ''); %! x4 = r{4}; %! x4.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x4), {'2024-01-04 00:00:00.000'}); %! assert_equal (x4.TimeZone, ''); %!test %! d = D; %! d = insert (d, datetime (2024, 1, 4), 40); %! assert_equal (values (d), [10; 20; 30; 40]); %!test %! d = D; %! d(datetime (2024, 1, 1)) = 99; %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, datetime (2024, 1, 1), 99); %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, datetime (2024, 1, 1), 99, 'Overwrite', false); %! assert_equal (values (d), [10; 20; 30]); %!test %! d = D; %! d = remove (d, datetime (2024, 1, 1)); %! r = keys (d, 'cell'); %! assert_equal (size (r), [2, 1]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-02 00:00:00.000'}); %! assert_equal (x1.TimeZone, ''); %! x2 = r{2}; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), {'2024-01-03 00:00:00.000'}); %! assert_equal (x2.TimeZone, ''); %!test %! d = D; %! d(datetime (2024, 1, 1)) = []; %! assert_equal (values (d), [20; 30]); %!test %! d = D; %! d = remove (d, datetime (2024, 1, 1)); %! d(datetime (2024, 1, 1)) = 11; %! assert_equal (values (d), [20; 30; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! x1 = r(1).Key; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-01 00:00:00.000'}); %! assert_equal (x1.TimeZone, ''); %! assert_equal (r(1).Value, 10); %! x2 = r(2).Key; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), {'2024-01-02 00:00:00.000'}); %! assert_equal (x2.TimeZone, ''); %! assert_equal (r(2).Value, 20); %! x3 = r(3).Key; %! x3.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x3), {'2024-01-03 00:00:00.000'}); %! assert_equal (x3.TimeZone, ''); %! assert_equal (r(3).Value, 30); %!test %! r = entries (D, 'cell'); %! assert_equal (size (r), [3, 2]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-01 00:00:00.000'}); %! assert_equal (x1.TimeZone, ''); %! x2 = r{2}; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), {'2024-01-02 00:00:00.000'}); %! assert_equal (x2.TimeZone, ''); %! x3 = r{3}; %! x3.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x3), {'2024-01-03 00:00:00.000'}); %! assert_equal (x3.TimeZone, ''); %! assert_equal (r{4}, 10); %! assert_equal (r{5}, 20); %! assert_equal (r{6}, 30); %!test %! r = entries (D); %! tb1 = r; %! assert_equal (size (tb1), [3, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! x2 = tb1.Key; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), ... %! {'2024-01-01 00:00:00.000'; '2024-01-02 00:00:00.000'; ... %! '2024-01-03 00:00:00.000'}); %! assert_equal (x2.TimeZone, ''); %! assert_equal (tb1.Value, [10; 20; 30]); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!assert_equal (CV{datetime(2024, 1, 2)}, 'two') %!test %! c = CV; %! c{datetime(2024, 1, 1)} = 5; %! assert_equal (c(datetime (2024, 1, 1)), {5}); %!test %! c = CV; %! c{datetime(2024, 1, 2)}(2) = 7; %! r = c{datetime(2024, 1, 2)}; %! assert_equal (double (r), [116, 7, 111]); %!error ... %! CV{datetime(2025, 1, 1)}; ## Keys of type 'zoned', measured on R2024a %!shared D, R, CV %! D = dictionary (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 10); %! D(datetime (2024, 1, 2, 'TimeZone', 'UTC')) = 20; %! D(datetime (2024, 1, 3, 'TimeZone', 'UTC')) = 30; %! R = dictionary (datetime (2024, 1, 3, 'TimeZone', 'UTC'), 30); %! R(datetime (2024, 1, 2, 'TimeZone', 'UTC')) = 20; %! R(datetime (2024, 1, 1, 'TimeZone', 'UTC')) = 10; %! CV = dictionary (datetime (2024, 1, 1, 'TimeZone', 'UTC'), {1}); %! CV(datetime (2024, 1, 2, 'TimeZone', 'UTC')) = {'two'}; %! CV(datetime (2024, 1, 3, 'TimeZone', 'UTC')) = {[3, 4]}; %!test %! r = keys (D); %! x1 = r; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), ... %! {'2024-01-01 00:00:00.000'; '2024-01-02 00:00:00.000'; ... %! '2024-01-03 00:00:00.000'}); %! assert_equal (x1.TimeZone, 'UTC'); %!assert_equal (values (D), [10; 20; 30]) %!test %! r = keys (D, 'cell'); %! assert_equal (size (r), [3, 1]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-01 00:00:00.000'}); %! assert_equal (x1.TimeZone, 'UTC'); %! x2 = r{2}; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), {'2024-01-02 00:00:00.000'}); %! assert_equal (x2.TimeZone, 'UTC'); %! x3 = r{3}; %! x3.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x3), {'2024-01-03 00:00:00.000'}); %! assert_equal (x3.TimeZone, 'UTC'); %!assert_equal (numEntries (D), 3) %!assert_equal (D(datetime (2024, 1, 2, 'TimeZone', 'UTC')), 20) %!assert_equal (isKey (D, datetime (2024, 1, 1, 'TimeZone', 'UTC')), true) %!assert_equal (lookup (D, datetime (2024, 1, 2, 'TimeZone', 'UTC')), 20) %!assert_equal (isKey (D, datetime (2025, 1, 1, 'TimeZone', 'UTC')), false) %!test %! r = lookup (D, datetime (2025, 1, 1, 'TimeZone', 'UTC'), 'FallbackValue', 0); %! assert_equal (r, 0); %!error ... %! D(datetime (2025, 1, 1, 'TimeZone', 'UTC')); %!test %! d = D; %! d(datetime (2025, 1, 1, 'TimeZone', 'UTC')) = []; %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d = remove (d, datetime (2025, 1, 1, 'TimeZone', 'UTC')); %! assert_equal (numEntries (d), 3); %!assert_equal (D(datetime (2024, 1, [1, 3], 'TimeZone', 'UTC')), [10, 30]) %!test %! r = isKey (D, datetime (2024, 1, [1, 3], 'TimeZone', 'UTC')); %! assert_equal (r, [true, true]); %!test %! r = dictionary (datetime (2024, 1, [1, 3], 'TimeZone', 'UTC'), 7); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'datetime', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! x5 = kc3{1}; %! x5.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x5), {'2024-01-01 00:00:00.000'}); %! assert_equal (x5.TimeZone, 'UTC'); %! x6 = kc3{2}; %! x6.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x6), {'2024-01-03 00:00:00.000'}); %! assert_equal (x6.TimeZone, 'UTC'); %! assert_equal (values (r, 'cell'), {7; 7}); %!test %! d = D; %! d(datetime (2024, 1, 4, 'TimeZone', 'UTC')) = 40; %! r = keys (d, 'cell'); %! assert_equal (size (r), [4, 1]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-01 00:00:00.000'}); %! assert_equal (x1.TimeZone, 'UTC'); %! x2 = r{2}; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), {'2024-01-02 00:00:00.000'}); %! assert_equal (x2.TimeZone, 'UTC'); %! x3 = r{3}; %! x3.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x3), {'2024-01-03 00:00:00.000'}); %! assert_equal (x3.TimeZone, 'UTC'); %! x4 = r{4}; %! x4.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x4), {'2024-01-04 00:00:00.000'}); %! assert_equal (x4.TimeZone, 'UTC'); %!test %! d = D; %! d = insert (d, datetime (2024, 1, 4, 'TimeZone', 'UTC'), 40); %! assert_equal (values (d), [10; 20; 30; 40]); %!test %! d = D; %! d(datetime (2024, 1, 1, 'TimeZone', 'UTC')) = 99; %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, datetime (2024, 1, 1, 'TimeZone', 'UTC'), 99); %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, datetime (2024, 1, 1, 'TimeZone', 'UTC'), 99, 'Overwrite', ... %! false); %! assert_equal (values (d), [10; 20; 30]); %!test %! d = D; %! d = remove (d, datetime (2024, 1, 1, 'TimeZone', 'UTC')); %! r = keys (d, 'cell'); %! assert_equal (size (r), [2, 1]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-02 00:00:00.000'}); %! assert_equal (x1.TimeZone, 'UTC'); %! x2 = r{2}; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), {'2024-01-03 00:00:00.000'}); %! assert_equal (x2.TimeZone, 'UTC'); %!test %! d = D; %! d(datetime (2024, 1, 1, 'TimeZone', 'UTC')) = []; %! assert_equal (values (d), [20; 30]); %!test %! d = D; %! d = remove (d, datetime (2024, 1, 1, 'TimeZone', 'UTC')); %! d(datetime (2024, 1, 1, 'TimeZone', 'UTC')) = 11; %! assert_equal (values (d), [20; 30; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! x1 = r(1).Key; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-01 00:00:00.000'}); %! assert_equal (x1.TimeZone, 'UTC'); %! assert_equal (r(1).Value, 10); %! x2 = r(2).Key; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), {'2024-01-02 00:00:00.000'}); %! assert_equal (x2.TimeZone, 'UTC'); %! assert_equal (r(2).Value, 20); %! x3 = r(3).Key; %! x3.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x3), {'2024-01-03 00:00:00.000'}); %! assert_equal (x3.TimeZone, 'UTC'); %! assert_equal (r(3).Value, 30); %!test %! r = entries (D, 'cell'); %! assert_equal (size (r), [3, 2]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-01 00:00:00.000'}); %! assert_equal (x1.TimeZone, 'UTC'); %! x2 = r{2}; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), {'2024-01-02 00:00:00.000'}); %! assert_equal (x2.TimeZone, 'UTC'); %! x3 = r{3}; %! x3.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x3), {'2024-01-03 00:00:00.000'}); %! assert_equal (x3.TimeZone, 'UTC'); %! assert_equal (r{4}, 10); %! assert_equal (r{5}, 20); %! assert_equal (r{6}, 30); %!test %! r = entries (D); %! tb1 = r; %! assert_equal (size (tb1), [3, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! x2 = tb1.Key; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), ... %! {'2024-01-01 00:00:00.000'; '2024-01-02 00:00:00.000'; ... %! '2024-01-03 00:00:00.000'}); %! assert_equal (x2.TimeZone, 'UTC'); %! assert_equal (tb1.Value, [10; 20; 30]); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!assert_equal (CV{datetime(2024, 1, 2, 'TimeZone', 'UTC')}, 'two') %!test %! c = CV; %! c{datetime(2024, 1, 1, 'TimeZone', 'UTC')} = 5; %! assert_equal (c(datetime (2024, 1, 1, 'TimeZone', 'UTC')), {5}); %!test %! c = CV; %! c{datetime(2024, 1, 2, 'TimeZone', 'UTC')}(2) = 7; %! r = c{datetime(2024, 1, 2, 'TimeZone', 'UTC')}; %! assert_equal (double (r), [116, 7, 111]); %!error ... %! CV{datetime(2025, 1, 1, 'TimeZone', 'UTC')}; ## Keys of type 'duration', measured on R2024a %!shared D, R, CV %! D = dictionary (hours (1), 10); %! D(hours (2)) = 20; %! D(hours (3)) = 30; %! R = dictionary (hours (3), 30); %! R(hours (2)) = 20; %! R(hours (1)) = 10; %! CV = dictionary (hours (1), {1}); %! CV(hours (2)) = {'two'}; %! CV(hours (3)) = {[3, 4]}; %!test %! r = keys (D); %! assert_equal (class (r), 'duration'); %! assert_equal (seconds (r), [3600; 7200; 10800]); %!assert_equal (values (D), [10; 20; 30]) %!test %! r = keys (D, 'cell'); %! assert_equal (size (r), [3, 1]); %! assert_equal (class (r{1}), 'duration'); %! assert_equal (seconds (r{1}), 3600); %! assert_equal (class (r{2}), 'duration'); %! assert_equal (seconds (r{2}), 7200); %! assert_equal (class (r{3}), 'duration'); %! assert_equal (seconds (r{3}), 10800); %!assert_equal (numEntries (D), 3) %!assert_equal (D(hours (2)), 20) %!assert_equal (isKey (D, hours (1)), true) %!assert_equal (lookup (D, hours (2)), 20) %!assert_equal (isKey (D, hours (9)), false) %!assert_equal (lookup (D, hours (9), 'FallbackValue', 0), 0) %!error ... %! D(hours (9)); %!test %! d = D; %! d(hours (9)) = []; %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d = remove (d, hours (9)); %! assert_equal (numEntries (d), 3); %!assert_equal (D(hours ([1, 3])), [10, 30]) %!assert_equal (isKey (D, hours ([1, 3])), [true, true]) %!test %! r = dictionary (hours ([1, 3]), 7); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'duration', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'duration'); %! assert_equal (seconds (kc3{1}), 3600); %! assert_equal (class (kc3{2}), 'duration'); %! assert_equal (seconds (kc3{2}), 10800); %! assert_equal (values (r, 'cell'), {7; 7}); %!test %! d = D; %! d(hours (4)) = 40; %! r = keys (d, 'cell'); %! assert_equal (size (r), [4, 1]); %! assert_equal (class (r{1}), 'duration'); %! assert_equal (seconds (r{1}), 3600); %! assert_equal (class (r{2}), 'duration'); %! assert_equal (seconds (r{2}), 7200); %! assert_equal (class (r{3}), 'duration'); %! assert_equal (seconds (r{3}), 10800); %! assert_equal (class (r{4}), 'duration'); %! assert_equal (seconds (r{4}), 14400); %!test %! d = D; %! d = insert (d, hours (4), 40); %! assert_equal (values (d), [10; 20; 30; 40]); %!test %! d = D; %! d(hours (1)) = 99; %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, hours (1), 99); %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, hours (1), 99, 'Overwrite', false); %! assert_equal (values (d), [10; 20; 30]); %!test %! d = D; %! d = remove (d, hours (1)); %! r = keys (d, 'cell'); %! assert_equal (size (r), [2, 1]); %! assert_equal (class (r{1}), 'duration'); %! assert_equal (seconds (r{1}), 7200); %! assert_equal (class (r{2}), 'duration'); %! assert_equal (seconds (r{2}), 10800); %!test %! d = D; %! d(hours (1)) = []; %! assert_equal (values (d), [20; 30]); %!test %! d = D; %! d = remove (d, hours (1)); %! d(hours (1)) = 11; %! assert_equal (values (d), [20; 30; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (class (r(1).Key), 'duration'); %! assert_equal (seconds (r(1).Key), 3600); %! assert_equal (r(1).Value, 10); %! assert_equal (class (r(2).Key), 'duration'); %! assert_equal (seconds (r(2).Key), 7200); %! assert_equal (r(2).Value, 20); %! assert_equal (class (r(3).Key), 'duration'); %! assert_equal (seconds (r(3).Key), 10800); %! assert_equal (r(3).Value, 30); %!test %! r = entries (D, 'cell'); %! assert_equal (size (r), [3, 2]); %! assert_equal (class (r{1}), 'duration'); %! assert_equal (seconds (r{1}), 3600); %! assert_equal (class (r{2}), 'duration'); %! assert_equal (seconds (r{2}), 7200); %! assert_equal (class (r{3}), 'duration'); %! assert_equal (seconds (r{3}), 10800); %! assert_equal (r{4}, 10); %! assert_equal (r{5}, 20); %! assert_equal (r{6}, 30); %!test %! r = entries (D); %! tb1 = r; %! assert_equal (size (tb1), [3, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! assert_equal (class (tb1.Key), 'duration'); %! assert_equal (seconds (tb1.Key), [3600; 7200; 10800]); %! assert_equal (tb1.Value, [10; 20; 30]); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!assert_equal (CV{hours(2)}, 'two') %!test %! c = CV; %! c{hours(1)} = 5; %! assert_equal (c(hours (1)), {5}); %!test %! c = CV; %! c{hours(2)}(2) = 7; %! r = c{hours(2)}; %! assert_equal (double (r), [116, 7, 111]); %!error ... %! CV{hours(9)}; ## Keys of type 'calendarDuration', measured on R2024a %!shared D, R, CV %! D = dictionary (calmonths (1), 10); %! D(calmonths (2)) = 20; %! D(calmonths (3)) = 30; %! R = dictionary (calmonths (3), 30); %! R(calmonths (2)) = 20; %! R(calmonths (1)) = 10; %! CV = dictionary (calmonths (1), {1}); %! CV(calmonths (2)) = {'two'}; %! CV(calmonths (3)) = {[3, 4]}; %!test %! r = keys (D); %! [m1, dd2, t3] = split (r, {'months', 'days', 'time'}); %! assert_equal ([m1(:), dd2(:), seconds(t3(:))], [1, 0, 0; 2, 0, 0; 3, 0, 0]); %!assert_equal (values (D), [10; 20; 30]) %!test %! r = keys (D, 'cell'); %! assert_equal (size (r), [3, 1]); %! [m1, dd2, t3] = split (r{1}, {'months', 'days', 'time'}); %! assert_equal ([m1(:), dd2(:), seconds(t3(:))], [1, 0, 0]); %! [m4, dd5, t6] = split (r{2}, {'months', 'days', 'time'}); %! assert_equal ([m4(:), dd5(:), seconds(t6(:))], [2, 0, 0]); %! [m7, dd8, t9] = split (r{3}, {'months', 'days', 'time'}); %! assert_equal ([m7(:), dd8(:), seconds(t9(:))], [3, 0, 0]); %!assert_equal (numEntries (D), 3) %!assert_equal (D(calmonths (2)), 20) %!assert_equal (isKey (D, calmonths (1)), true) %!assert_equal (lookup (D, calmonths (2)), 20) %!assert_equal (isKey (D, calmonths (9)), false) %!assert_equal (lookup (D, calmonths (9), 'FallbackValue', 0), 0) %!error ... %! D(calmonths (9)); %!test %! d = D; %! d(calmonths (9)) = []; %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d = remove (d, calmonths (9)); %! assert_equal (numEntries (d), 3); %!assert_equal (D(calmonths ([1, 3])), [10, 30]) %!assert_equal (isKey (D, calmonths ([1, 3])), [true, true]) %!test %! r = dictionary (calmonths ([1, 3]), 7); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'calendarDuration', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! [m5, dd6, t7] = split (kc3{1}, {'months', 'days', 'time'}); %! assert_equal ([m5(:), dd6(:), seconds(t7(:))], [1, 0, 0]); %! [m8, dd9, t10] = split (kc3{2}, {'months', 'days', 'time'}); %! assert_equal ([m8(:), dd9(:), seconds(t10(:))], [3, 0, 0]); %! assert_equal (values (r, 'cell'), {7; 7}); %!test %! d = D; %! d(calmonths (4)) = 40; %! r = keys (d, 'cell'); %! assert_equal (size (r), [4, 1]); %! [m1, dd2, t3] = split (r{1}, {'months', 'days', 'time'}); %! assert_equal ([m1(:), dd2(:), seconds(t3(:))], [1, 0, 0]); %! [m4, dd5, t6] = split (r{2}, {'months', 'days', 'time'}); %! assert_equal ([m4(:), dd5(:), seconds(t6(:))], [2, 0, 0]); %! [m7, dd8, t9] = split (r{3}, {'months', 'days', 'time'}); %! assert_equal ([m7(:), dd8(:), seconds(t9(:))], [3, 0, 0]); %! [m10, dd11, t12] = split (r{4}, {'months', 'days', 'time'}); %! assert_equal ([m10(:), dd11(:), seconds(t12(:))], [4, 0, 0]); %!test %! d = D; %! d = insert (d, calmonths (4), 40); %! assert_equal (values (d), [10; 20; 30; 40]); %!test %! d = D; %! d(calmonths (1)) = 99; %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, calmonths (1), 99); %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, calmonths (1), 99, 'Overwrite', false); %! assert_equal (values (d), [10; 20; 30]); %!test %! d = D; %! d = remove (d, calmonths (1)); %! r = keys (d, 'cell'); %! assert_equal (size (r), [2, 1]); %! [m1, dd2, t3] = split (r{1}, {'months', 'days', 'time'}); %! assert_equal ([m1(:), dd2(:), seconds(t3(:))], [2, 0, 0]); %! [m4, dd5, t6] = split (r{2}, {'months', 'days', 'time'}); %! assert_equal ([m4(:), dd5(:), seconds(t6(:))], [3, 0, 0]); %!test %! d = D; %! d(calmonths (1)) = []; %! assert_equal (values (d), [20; 30]); %!test %! d = D; %! d = remove (d, calmonths (1)); %! d(calmonths (1)) = 11; %! assert_equal (values (d), [20; 30; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! [m1, dd2, t3] = split (r(1).Key, {'months', 'days', 'time'}); %! assert_equal ([m1(:), dd2(:), seconds(t3(:))], [1, 0, 0]); %! assert_equal (r(1).Value, 10); %! [m4, dd5, t6] = split (r(2).Key, {'months', 'days', 'time'}); %! assert_equal ([m4(:), dd5(:), seconds(t6(:))], [2, 0, 0]); %! assert_equal (r(2).Value, 20); %! [m7, dd8, t9] = split (r(3).Key, {'months', 'days', 'time'}); %! assert_equal ([m7(:), dd8(:), seconds(t9(:))], [3, 0, 0]); %! assert_equal (r(3).Value, 30); %!test %! r = entries (D, 'cell'); %! assert_equal (size (r), [3, 2]); %! [m1, dd2, t3] = split (r{1}, {'months', 'days', 'time'}); %! assert_equal ([m1(:), dd2(:), seconds(t3(:))], [1, 0, 0]); %! [m4, dd5, t6] = split (r{2}, {'months', 'days', 'time'}); %! assert_equal ([m4(:), dd5(:), seconds(t6(:))], [2, 0, 0]); %! [m7, dd8, t9] = split (r{3}, {'months', 'days', 'time'}); %! assert_equal ([m7(:), dd8(:), seconds(t9(:))], [3, 0, 0]); %! assert_equal (r{4}, 10); %! assert_equal (r{5}, 20); %! assert_equal (r{6}, 30); %!test %! r = entries (D); %! tb1 = r; %! assert_equal (size (tb1), [3, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! [m2, dd3, t4] = split (tb1.Key, {'months', 'days', 'time'}); %! assert_equal ([m2(:), dd3(:), seconds(t4(:))], [1, 0, 0; 2, 0, 0; 3, 0, 0]); %! assert_equal (tb1.Value, [10; 20; 30]); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!assert_equal (CV{calmonths(2)}, 'two') %!test %! c = CV; %! c{calmonths(1)} = 5; %! assert_equal (c(calmonths (1)), {5}); %!test %! c = CV; %! c{calmonths(2)}(2) = 7; %! r = c{calmonths(2)}; %! assert_equal (double (r), [116, 7, 111]); %!error ... %! CV{calmonths(9)}; ## Keys of type 'categorical', measured on R2024a %!shared D, R, CV %! D = dictionary (categorical ({'a'}), 10); %! D(categorical ({'b'})) = 20; %! D(categorical ({'c'})) = 30; %! R = dictionary (categorical ({'c'}), 30); %! R(categorical ({'b'})) = 20; %! R(categorical ({'a'})) = 10; %! CV = dictionary (categorical ({'a'}), {1}); %! CV(categorical ({'b'})) = {'two'}; %! CV(categorical ({'c'})) = {[3, 4]}; %!test %! r = keys (D); %! assert_equal (class (r), 'categorical'); %! assert_equal (size (r), [3, 1]); %! assert_equal (cellstr (r), {'a'; 'b'; 'c'}); %! assert_equal (categories (r), {'a'; 'b'; 'c'}); %! assert_equal (isordinal (r), false); %!assert_equal (values (D), [10; 20; 30]) %!test %! r = keys (D, 'cell'); %! assert_equal (size (r), [3, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'a'}); %! assert_equal (categories (r{1}), {'a'}); %! assert_equal (isordinal (r{1}), false); %! assert_equal (class (r{2}), 'categorical'); %! assert_equal (size (r{2}), [1, 1]); %! assert_equal (cellstr (r{2}), {'b'}); %! assert_equal (categories (r{2}), {'b'}); %! assert_equal (isordinal (r{2}), false); %! assert_equal (class (r{3}), 'categorical'); %! assert_equal (size (r{3}), [1, 1]); %! assert_equal (cellstr (r{3}), {'c'}); %! assert_equal (categories (r{3}), {'c'}); %! assert_equal (isordinal (r{3}), false); %!assert_equal (numEntries (D), 3) %!assert_equal (D(categorical ({'b'})), 20) %!assert_equal (isKey (D, categorical ({'a'})), true) %!assert_equal (lookup (D, categorical ({'b'})), 20) %!assert_equal (isKey (D, categorical ({'z'})), false) %!assert_equal (lookup (D, categorical ({'z'}), 'FallbackValue', 0), 0) %!error ... %! D(categorical ({'z'})); %!test %! d = D; %! d(categorical ({'z'})) = []; %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d = remove (d, categorical ({'z'})); %! assert_equal (numEntries (d), 3); %!assert_equal (D(categorical ({'a', 'c'})), [10, 30]) %!assert_equal (isKey (D, categorical ({'a', 'c'})), [true, true]) %!test %! r = dictionary (categorical ({'a', 'c'}), 7); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'categorical', 'double'})); %! assert_equal (numEntries (r), 2); %! kc3 = keys (r, 'cell'); %! assert_equal (class (kc3{1}), 'categorical'); %! assert_equal (size (kc3{1}), [1, 1]); %! assert_equal (cellstr (kc3{1}), {'a'}); %! assert_equal (categories (kc3{1}), {'a'; 'c'}); %! assert_equal (isordinal (kc3{1}), false); %! assert_equal (class (kc3{2}), 'categorical'); %! assert_equal (size (kc3{2}), [1, 1]); %! assert_equal (cellstr (kc3{2}), {'c'}); %! assert_equal (categories (kc3{2}), {'a'; 'c'}); %! assert_equal (isordinal (kc3{2}), false); %! assert_equal (values (r, 'cell'), {7; 7}); %!test %! d = D; %! d(categorical ({'d'})) = 40; %! r = keys (d, 'cell'); %! assert_equal (size (r), [4, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'a'}); %! assert_equal (categories (r{1}), {'a'}); %! assert_equal (isordinal (r{1}), false); %! assert_equal (class (r{2}), 'categorical'); %! assert_equal (size (r{2}), [1, 1]); %! assert_equal (cellstr (r{2}), {'b'}); %! assert_equal (categories (r{2}), {'b'}); %! assert_equal (isordinal (r{2}), false); %! assert_equal (class (r{3}), 'categorical'); %! assert_equal (size (r{3}), [1, 1]); %! assert_equal (cellstr (r{3}), {'c'}); %! assert_equal (categories (r{3}), {'c'}); %! assert_equal (isordinal (r{3}), false); %! assert_equal (class (r{4}), 'categorical'); %! assert_equal (size (r{4}), [1, 1]); %! assert_equal (cellstr (r{4}), {'d'}); %! assert_equal (categories (r{4}), {'d'}); %! assert_equal (isordinal (r{4}), false); %!test %! d = D; %! d = insert (d, categorical ({'d'}), 40); %! assert_equal (values (d), [10; 20; 30; 40]); %!test %! d = D; %! d(categorical ({'a'})) = 99; %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, categorical ({'a'}), 99); %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, categorical ({'a'}), 99, 'Overwrite', false); %! assert_equal (values (d), [10; 20; 30]); %!test %! d = D; %! d = remove (d, categorical ({'a'})); %! r = keys (d, 'cell'); %! assert_equal (size (r), [2, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'b'}); %! assert_equal (categories (r{1}), {'b'}); %! assert_equal (isordinal (r{1}), false); %! assert_equal (class (r{2}), 'categorical'); %! assert_equal (size (r{2}), [1, 1]); %! assert_equal (cellstr (r{2}), {'c'}); %! assert_equal (categories (r{2}), {'c'}); %! assert_equal (isordinal (r{2}), false); %!test %! d = D; %! d(categorical ({'a'})) = []; %! assert_equal (values (d), [20; 30]); %!test %! d = D; %! d = remove (d, categorical ({'a'})); %! d(categorical ({'a'})) = 11; %! assert_equal (values (d), [20; 30; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (class (r(1).Key), 'categorical'); %! assert_equal (size (r(1).Key), [1, 1]); %! assert_equal (cellstr (r(1).Key), {'a'}); %! assert_equal (categories (r(1).Key), {'a'}); %! assert_equal (isordinal (r(1).Key), false); %! assert_equal (r(1).Value, 10); %! assert_equal (class (r(2).Key), 'categorical'); %! assert_equal (size (r(2).Key), [1, 1]); %! assert_equal (cellstr (r(2).Key), {'b'}); %! assert_equal (categories (r(2).Key), {'b'}); %! assert_equal (isordinal (r(2).Key), false); %! assert_equal (r(2).Value, 20); %! assert_equal (class (r(3).Key), 'categorical'); %! assert_equal (size (r(3).Key), [1, 1]); %! assert_equal (cellstr (r(3).Key), {'c'}); %! assert_equal (categories (r(3).Key), {'c'}); %! assert_equal (isordinal (r(3).Key), false); %! assert_equal (r(3).Value, 30); %!test %! r = entries (D, 'cell'); %! assert_equal (size (r), [3, 2]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'a'}); %! assert_equal (categories (r{1}), {'a'}); %! assert_equal (isordinal (r{1}), false); %! assert_equal (class (r{2}), 'categorical'); %! assert_equal (size (r{2}), [1, 1]); %! assert_equal (cellstr (r{2}), {'b'}); %! assert_equal (categories (r{2}), {'b'}); %! assert_equal (isordinal (r{2}), false); %! assert_equal (class (r{3}), 'categorical'); %! assert_equal (size (r{3}), [1, 1]); %! assert_equal (cellstr (r{3}), {'c'}); %! assert_equal (categories (r{3}), {'c'}); %! assert_equal (isordinal (r{3}), false); %! assert_equal (r{4}, 10); %! assert_equal (r{5}, 20); %! assert_equal (r{6}, 30); %!test %! r = entries (D); %! tb1 = r; %! assert_equal (size (tb1), [3, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! assert_equal (class (tb1.Key), 'categorical'); %! assert_equal (size (tb1.Key), [3, 1]); %! assert_equal (cellstr (tb1.Key), {'a'; 'b'; 'c'}); %! assert_equal (categories (tb1.Key), {'a'; 'b'; 'c'}); %! assert_equal (isordinal (tb1.Key), false); %! assert_equal (tb1.Value, [10; 20; 30]); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!assert_equal (CV{categorical({'b'})}, 'two') %!test %! c = CV; %! c{categorical({'a'})} = 5; %! assert_equal (c(categorical ({'a'})), {5}); %!test %! c = CV; %! c{categorical({'b'})}(2) = 7; %! r = c{categorical({'b'})}; %! assert_equal (double (r), [116, 7, 111]); %!error ... %! CV{categorical({'z'})}; ## Keys of type 'ordinal', measured on R2024a %!shared D, R, CV %! D = dictionary (categorical ({'lo'}, {'lo', 'mid', 'hi'}, 'Ordinal', ... %! true), 10); %! D(categorical ({'mid'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)) = 20; %! D(categorical ({'hi'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)) = 30; %! R = dictionary (categorical ({'hi'}, {'lo', 'mid', 'hi'}, 'Ordinal', ... %! true), 30); %! R(categorical ({'mid'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)) = 20; %! R(categorical ({'lo'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)) = 10; %! CV = dictionary (categorical ({'lo'}, {'lo', 'mid', 'hi'}, 'Ordinal', ... %! true), {1}); %! CV(categorical ({'mid'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)) = {'two'}; %! CV(categorical ({'hi'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)) = {[3, 4]}; %!test %! r = keys (D); %! assert_equal (class (r), 'categorical'); %! assert_equal (size (r), [3, 1]); %! assert_equal (cellstr (r), {'lo'; 'mid'; 'hi'}); %! assert_equal (categories (r), {'lo'; 'mid'; 'hi'}); %! assert_equal (isordinal (r), true); %!assert_equal (values (D), [10; 20; 30]) %!test %! r = keys (D, 'cell'); %! assert_equal (size (r), [3, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'lo'}); %! assert_equal (categories (r{1}), {'lo'; 'mid'; 'hi'}); %! assert_equal (isordinal (r{1}), true); %! assert_equal (class (r{2}), 'categorical'); %! assert_equal (size (r{2}), [1, 1]); %! assert_equal (cellstr (r{2}), {'mid'}); %! assert_equal (categories (r{2}), {'lo'; 'mid'; 'hi'}); %! assert_equal (isordinal (r{2}), true); %! assert_equal (class (r{3}), 'categorical'); %! assert_equal (size (r{3}), [1, 1]); %! assert_equal (cellstr (r{3}), {'hi'}); %! assert_equal (categories (r{3}), {'lo'; 'mid'; 'hi'}); %! assert_equal (isordinal (r{3}), true); %!assert_equal (numEntries (D), 3) %!test %! r = D(categorical ({'mid'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)); %! assert_equal (r, 20); %!test %! r = isKey (D, categorical ({'lo'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)); %! assert_equal (r, true); %!test %! r = lookup (D, categorical ({'mid'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)); %! assert_equal (r, 20); %!test %! d = D; %! d(categorical ({'lo'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)) = 99; %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, ... %! categorical ({'lo'}, {'lo', 'mid', 'hi'}, 'Ordinal', true), 99); %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, ... %! categorical ({'lo'}, {'lo', 'mid', 'hi'}, 'Ordinal', ... %! true), 99, 'Overwrite', false); %! assert_equal (values (d), [10; 20; 30]); %!test %! d = D; %! d = remove (d, categorical ({'lo'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)); %! r = keys (d, 'cell'); %! assert_equal (size (r), [2, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'mid'}); %! assert_equal (categories (r{1}), {'lo'; 'mid'; 'hi'}); %! assert_equal (isordinal (r{1}), true); %! assert_equal (class (r{2}), 'categorical'); %! assert_equal (size (r{2}), [1, 1]); %! assert_equal (cellstr (r{2}), {'hi'}); %! assert_equal (categories (r{2}), {'lo'; 'mid'; 'hi'}); %! assert_equal (isordinal (r{2}), true); %!test %! d = D; %! d(categorical ({'lo'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)) = []; %! assert_equal (values (d), [20; 30]); %!test %! d = D; %! d = remove (d, categorical ({'lo'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)); %! d(categorical ({'lo'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)) = 11; %! assert_equal (values (d), [20; 30; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (class (r(1).Key), 'categorical'); %! assert_equal (size (r(1).Key), [1, 1]); %! assert_equal (cellstr (r(1).Key), {'lo'}); %! assert_equal (categories (r(1).Key), {'lo'; 'mid'; 'hi'}); %! assert_equal (isordinal (r(1).Key), true); %! assert_equal (r(1).Value, 10); %! assert_equal (class (r(2).Key), 'categorical'); %! assert_equal (size (r(2).Key), [1, 1]); %! assert_equal (cellstr (r(2).Key), {'mid'}); %! assert_equal (categories (r(2).Key), {'lo'; 'mid'; 'hi'}); %! assert_equal (isordinal (r(2).Key), true); %! assert_equal (r(2).Value, 20); %! assert_equal (class (r(3).Key), 'categorical'); %! assert_equal (size (r(3).Key), [1, 1]); %! assert_equal (cellstr (r(3).Key), {'hi'}); %! assert_equal (categories (r(3).Key), {'lo'; 'mid'; 'hi'}); %! assert_equal (isordinal (r(3).Key), true); %! assert_equal (r(3).Value, 30); %!test %! r = entries (D, 'cell'); %! assert_equal (size (r), [3, 2]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'lo'}); %! assert_equal (categories (r{1}), {'lo'; 'mid'; 'hi'}); %! assert_equal (isordinal (r{1}), true); %! assert_equal (class (r{2}), 'categorical'); %! assert_equal (size (r{2}), [1, 1]); %! assert_equal (cellstr (r{2}), {'mid'}); %! assert_equal (categories (r{2}), {'lo'; 'mid'; 'hi'}); %! assert_equal (isordinal (r{2}), true); %! assert_equal (class (r{3}), 'categorical'); %! assert_equal (size (r{3}), [1, 1]); %! assert_equal (cellstr (r{3}), {'hi'}); %! assert_equal (categories (r{3}), {'lo'; 'mid'; 'hi'}); %! assert_equal (isordinal (r{3}), true); %! assert_equal (r{4}, 10); %! assert_equal (r{5}, 20); %! assert_equal (r{6}, 30); %!test %! r = entries (D); %! tb1 = r; %! assert_equal (size (tb1), [3, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! assert_equal (class (tb1.Key), 'categorical'); %! assert_equal (size (tb1.Key), [3, 1]); %! assert_equal (cellstr (tb1.Key), {'lo'; 'mid'; 'hi'}); %! assert_equal (categories (tb1.Key), {'lo'; 'mid'; 'hi'}); %! assert_equal (isordinal (tb1.Key), true); %! assert_equal (tb1.Value, [10; 20; 30]); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!test %! r = CV{categorical({'mid'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)}; %! assert_equal (r, 'two'); %!test %! c = CV; %! c{categorical({'lo'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)} = 5; %! r = c(categorical ({'lo'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)); %! assert_equal (r, {5}); %!test %! c = CV; %! c{categorical({'mid'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)}(2) = 7; %! r = c{categorical({'mid'}, {'lo', 'mid', 'hi'}, 'Ordinal', true)}; %! assert_equal (double (r), [116, 7, 111]); ## Keys of type 'dictionary', measured on R2024a %!shared D, R, CV %! D = dictionary (dictionary (1, 2), 10); %! D(dictionary (3, 4)) = 20; %! D(dictionary (5, 6)) = 30; %! R = dictionary (dictionary (5, 6), 30); %! R(dictionary (3, 4)) = 20; %! R(dictionary (1, 2)) = 10; %! CV = dictionary (dictionary (1, 2), {1}); %! CV(dictionary (3, 4)) = {'two'}; %! CV(dictionary (5, 6)) = {[3, 4]}; %!error ... %! keys (D); %!assert_equal (values (D), [10; 20; 30]) %!test %! r = keys (D, 'cell'); %! assert_equal (size (r), [3, 1]); %! dc1 = r{1}; %! [kt2, vt3] = types (dc1); %! assert_equal ([kt2, vt3], string ({'double', 'double'})); %! assert_equal (numEntries (dc1), 1); %! assert_equal (keys (dc1, 'cell'), {1}); %! assert_equal (values (dc1, 'cell'), {2}); %! dc6 = r{2}; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'double', 'double'})); %! assert_equal (numEntries (dc6), 1); %! assert_equal (keys (dc6, 'cell'), {3}); %! assert_equal (values (dc6, 'cell'), {4}); %! dc11 = r{3}; %! [kt12, vt13] = types (dc11); %! assert_equal ([kt12, vt13], string ({'double', 'double'})); %! assert_equal (numEntries (dc11), 1); %! assert_equal (keys (dc11, 'cell'), {5}); %! assert_equal (values (dc11, 'cell'), {6}); %!assert_equal (numEntries (D), 3) %!assert_equal (D(dictionary (3, 4)), 20) %!assert_equal (isKey (D, dictionary (1, 2)), true) %!assert_equal (lookup (D, dictionary (3, 4)), 20) %!assert_equal (isKey (D, dictionary (9, 9)), false) %!assert_equal (lookup (D, dictionary (9, 9), 'FallbackValue', 0), 0) %!error ... %! D(dictionary (9, 9)); %!test %! d = D; %! d(dictionary (9, 9)) = []; %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d = remove (d, dictionary (9, 9)); %! assert_equal (numEntries (d), 3); %!test %! d = D; %! d(dictionary (7, 8)) = 40; %! r = keys (d, 'cell'); %! assert_equal (size (r), [4, 1]); %! dc1 = r{1}; %! [kt2, vt3] = types (dc1); %! assert_equal ([kt2, vt3], string ({'double', 'double'})); %! assert_equal (numEntries (dc1), 1); %! assert_equal (keys (dc1, 'cell'), {1}); %! assert_equal (values (dc1, 'cell'), {2}); %! dc6 = r{2}; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'double', 'double'})); %! assert_equal (numEntries (dc6), 1); %! assert_equal (keys (dc6, 'cell'), {3}); %! assert_equal (values (dc6, 'cell'), {4}); %! dc11 = r{3}; %! [kt12, vt13] = types (dc11); %! assert_equal ([kt12, vt13], string ({'double', 'double'})); %! assert_equal (numEntries (dc11), 1); %! assert_equal (keys (dc11, 'cell'), {5}); %! assert_equal (values (dc11, 'cell'), {6}); %! dc16 = r{4}; %! [kt17, vt18] = types (dc16); %! assert_equal ([kt17, vt18], string ({'double', 'double'})); %! assert_equal (numEntries (dc16), 1); %! assert_equal (keys (dc16, 'cell'), {7}); %! assert_equal (values (dc16, 'cell'), {8}); %!test %! d = D; %! d = insert (d, dictionary (7, 8), 40); %! assert_equal (values (d), [10; 20; 30; 40]); %!test %! d = D; %! d(dictionary (1, 2)) = 99; %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, dictionary (1, 2), 99); %! assert_equal (values (d), [99; 20; 30]); %!test %! d = D; %! d = insert (d, dictionary (1, 2), 99, 'Overwrite', false); %! assert_equal (values (d), [10; 20; 30]); %!test %! d = D; %! d = remove (d, dictionary (1, 2)); %! r = keys (d, 'cell'); %! assert_equal (size (r), [2, 1]); %! dc1 = r{1}; %! [kt2, vt3] = types (dc1); %! assert_equal ([kt2, vt3], string ({'double', 'double'})); %! assert_equal (numEntries (dc1), 1); %! assert_equal (keys (dc1, 'cell'), {3}); %! assert_equal (values (dc1, 'cell'), {4}); %! dc6 = r{2}; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'double', 'double'})); %! assert_equal (numEntries (dc6), 1); %! assert_equal (keys (dc6, 'cell'), {5}); %! assert_equal (values (dc6, 'cell'), {6}); %!test %! d = D; %! d(dictionary (1, 2)) = []; %! assert_equal (values (d), [20; 30]); %!test %! d = D; %! d = remove (d, dictionary (1, 2)); %! d(dictionary (1, 2)) = 11; %! assert_equal (values (d), [20; 30; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [3, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! dc1 = r(1).Key; %! [kt2, vt3] = types (dc1); %! assert_equal ([kt2, vt3], string ({'double', 'double'})); %! assert_equal (numEntries (dc1), 1); %! assert_equal (keys (dc1, 'cell'), {1}); %! assert_equal (values (dc1, 'cell'), {2}); %! assert_equal (r(1).Value, 10); %! dc6 = r(2).Key; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'double', 'double'})); %! assert_equal (numEntries (dc6), 1); %! assert_equal (keys (dc6, 'cell'), {3}); %! assert_equal (values (dc6, 'cell'), {4}); %! assert_equal (r(2).Value, 20); %! dc11 = r(3).Key; %! [kt12, vt13] = types (dc11); %! assert_equal ([kt12, vt13], string ({'double', 'double'})); %! assert_equal (numEntries (dc11), 1); %! assert_equal (keys (dc11, 'cell'), {5}); %! assert_equal (values (dc11, 'cell'), {6}); %! assert_equal (r(3).Value, 30); %!test %! r = entries (D, 'cell'); %! assert_equal (size (r), [3, 2]); %! dc1 = r{1}; %! [kt2, vt3] = types (dc1); %! assert_equal ([kt2, vt3], string ({'double', 'double'})); %! assert_equal (numEntries (dc1), 1); %! assert_equal (keys (dc1, 'cell'), {1}); %! assert_equal (values (dc1, 'cell'), {2}); %! dc6 = r{2}; %! [kt7, vt8] = types (dc6); %! assert_equal ([kt7, vt8], string ({'double', 'double'})); %! assert_equal (numEntries (dc6), 1); %! assert_equal (keys (dc6, 'cell'), {3}); %! assert_equal (values (dc6, 'cell'), {4}); %! dc11 = r{3}; %! [kt12, vt13] = types (dc11); %! assert_equal ([kt12, vt13], string ({'double', 'double'})); %! assert_equal (numEntries (dc11), 1); %! assert_equal (keys (dc11, 'cell'), {5}); %! assert_equal (values (dc11, 'cell'), {6}); %! assert_equal (r{4}, 10); %! assert_equal (r{5}, 20); %! assert_equal (r{6}, 30); %!error ... %! entries (D); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!assert_equal (CV{dictionary(3, 4)}, 'two') %!test %! c = CV; %! c{dictionary(1, 2)} = 5; %! assert_equal (c(dictionary (1, 2)), {5}); %!test %! c = CV; %! c{dictionary(3, 4)}(2) = 7; %! r = c{dictionary(3, 4)}; %! assert_equal (double (r), [116, 7, 111]); %!error ... %! CV{dictionary(9, 9)}; ## Keys of type 'logical', measured on R2024a %!shared D, R, CV %! D = dictionary (true, 10); %! D(false) = 20; %! R = dictionary (false, 20); %! R(true) = 10; %! CV = dictionary (true, {1}); %! CV(false) = {'two'}; %!assert_equal (keys (D), [true; false]) %!assert_equal (values (D), [10; 20]) %!assert_equal (keys (D, 'cell'), {true; false}) %!assert_equal (numEntries (D), 2) %!assert_equal (D(false), 20) %!assert_equal (isKey (D, true), true) %!assert_equal (lookup (D, false), 20) %!assert_equal (D([false, true]), [20, 10]) %!assert_equal (isKey (D, [false, true]), [true, true]) %!test %! r = dictionary ([false, true], 7); %! [kt1, vt2] = types (r); %! assert_equal ([kt1, vt2], string ({'logical', 'double'})); %! assert_equal (numEntries (r), 2); %! assert_equal (keys (r, 'cell'), {false; true}); %! assert_equal (values (r, 'cell'), {7; 7}); %!test %! d = D; %! d(true) = 99; %! assert_equal (values (d), [99; 20]); %!test %! d = D; %! d = insert (d, true, 99); %! assert_equal (values (d), [99; 20]); %!test %! d = D; %! d = insert (d, true, 99, 'Overwrite', false); %! assert_equal (values (d), [10; 20]); %!test %! d = D; %! d = remove (d, true); %! assert_equal (keys (d, 'cell'), {false}); %!test %! d = D; %! d(true) = []; %! assert_equal (values (d), 20); %!test %! d = D; %! d = remove (d, true); %! d(true) = 11; %! assert_equal (values (d), [20; 11]); %!test %! r = entries (D, 'struct'); %! assert_equal (size (r), [2, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (r(1).Key, true); %! assert_equal (r(1).Value, 10); %! assert_equal (r(2).Key, false); %! assert_equal (r(2).Value, 20); %!assert_equal (entries (D, 'cell'), {true, 10; false, 20}) %!test %! r = entries (D); %! tb1 = r; %! assert_equal (size (tb1), [2, 2]); %! assert_equal (tb1.Properties.VariableNames, {'Key', 'Value'}); %! assert_equal (tb1.Key, [true; false]); %! assert_equal (tb1.Value, [10; 20]); %!assert_equal (isequal (D, R), true) %!assert_equal (isequaln (D, R), true) %!assert_equal (keyHash (D) == keyHash (R), false) %!assert_equal (keyHash (D) == keyHash (D), true) %!assert_equal (keyMatch (D, R), false) %!assert_equal (CV{false}, 'two') %!test %! c = CV; %! c{true} = 5; %! assert_equal (c(true), {5}); %!test %! c = CV; %! c{false}(2) = 7; %! r = c{false}; %! assert_equal (double (r), [116, 7, 111]); ## Keys of type 'missing', measured on R2024a %!test %! d = dictionary (missing, 10); %! r = keys (d); %! assert_equal (class (r), 'missing'); %! assert_equal (size (r), [1, 1]); %!test %! d = dictionary (missing, 10); %! assert_equal (d(missing), 10); %!test %! d = dictionary (missing, 10); %! assert_equal (isKey (d, missing), true); %!test %! d = dictionary (missing, 10); %! d(missing) = 20; %! assert_equal (values (d), 20); %!test %! d = dictionary (missing, 10); %! d = remove (d, missing); %! assert_equal (numEntries (d), 0); %!test %! d = dictionary (missing, 10); %! r = entries (d, 'struct'); %! assert_equal (size (r), [1, 1]); %! assert_equal (fieldnames (r), {'Key'; 'Value'}); %! assert_equal (class (r(1).Key), 'missing'); %! assert_equal (size (r(1).Key), [1, 1]); %! assert_equal (r(1).Value, 10); %!error ... %! d = dictionary (missing, 10); %! d(NaN); %!test %! d = dictionary (missing, 10); %! assert_equal (keyHash (d) == keyHash (dictionary (missing, 10)), true); ## Conversion to a value of type 'double', measured on R2024a %!test %! d = configureDictionary ('double', 'double'); %! d(1) = 5; %! assert_equal (values (d, 'cell'), {5}); %!test %! d = configureDictionary ('double', 'double'); %! d(1) = int8 (5); %! assert_equal (values (d, 'cell'), {5}); %!test %! d = configureDictionary ('double', 'double'); %! d(1) = true; %! assert_equal (values (d, 'cell'), {1}); %!test %! d = configureDictionary ('double', 'double'); %! d(1) = 'x'; %! assert_equal (values (d, 'cell'), {120}); %!test %! d = configureDictionary ('double', 'double'); %! d(1) = '5'; %! assert_equal (values (d, 'cell'), {53}); %!test %! d = configureDictionary ('double', 'double'); %! d(1) = string ('x'); %! assert_equal (values (d, 'cell'), {NaN}); %!test %! d = configureDictionary ('double', 'double'); %! d(1) = string ('5'); %! assert_equal (values (d, 'cell'), {5}); %!error ... %! d = configureDictionary ('double', 'double'); %! d(1) = {'x'}; %!error ... %! d = configureDictionary ('double', 'double'); %! d(1) = {5}; %!test %! d = configureDictionary ('double', 'double'); %! d(1) = categorical ({'x'}); %! assert_equal (values (d, 'cell'), {1}); %!error ... %! d = configureDictionary ('double', 'double'); %! d(1) = datetime (2024, 1, 1); %!error ... %! d = configureDictionary ('double', 'double'); %! d(1) = hours (1); %!error ... %! d = configureDictionary ('double', 'double'); %! d(1) = calmonths (1); %!test %! d = configureDictionary ('double', 'double'); %! d(1) = missing; %! assert_equal (values (d, 'cell'), {NaN}); %!error ... %! d = configureDictionary ('double', 'double'); %! d(1) = struct ('a', 1); %!error ... %! d = configureDictionary ('double', 'double'); %! d(1) = table (1); %!error ... %! d = configureDictionary ('double', 'double'); %! d(1) = @sin; ## Conversion to a value of type 'single', measured on R2024a %!test %! d = configureDictionary ('double', 'single'); %! d(1) = 5; %! assert_equal (values (d, 'cell'), {single(5)}); %!test %! d = configureDictionary ('double', 'single'); %! d(1) = int8 (5); %! assert_equal (values (d, 'cell'), {single(5)}); %!test %! d = configureDictionary ('double', 'single'); %! d(1) = true; %! assert_equal (values (d, 'cell'), {single(1)}); %!test %! d = configureDictionary ('double', 'single'); %! d(1) = 'x'; %! assert_equal (values (d, 'cell'), {single(120)}); %!test %! d = configureDictionary ('double', 'single'); %! d(1) = '5'; %! assert_equal (values (d, 'cell'), {single(53)}); %!error ... %! d = configureDictionary ('double', 'single'); %! d(1) = string ('x'); %!error ... %! d = configureDictionary ('double', 'single'); %! d(1) = string ('5'); %!error ... %! d = configureDictionary ('double', 'single'); %! d(1) = {'x'}; %!error ... %! d = configureDictionary ('double', 'single'); %! d(1) = {5}; %!test %! d = configureDictionary ('double', 'single'); %! d(1) = categorical ({'x'}); %! assert_equal (values (d, 'cell'), {single(1)}); %!error ... %! d = configureDictionary ('double', 'single'); %! d(1) = datetime (2024, 1, 1); %!error ... %! d = configureDictionary ('double', 'single'); %! d(1) = hours (1); %!error ... %! d = configureDictionary ('double', 'single'); %! d(1) = calmonths (1); %!test %! d = configureDictionary ('double', 'single'); %! d(1) = missing; %! assert_equal (values (d, 'cell'), {single(NaN)}); %!error ... %! d = configureDictionary ('double', 'single'); %! d(1) = struct ('a', 1); %!error ... %! d = configureDictionary ('double', 'single'); %! d(1) = table (1); %!error ... %! d = configureDictionary ('double', 'single'); %! d(1) = @sin; ## Conversion to a value of type 'int8', measured on R2024a %!test %! d = configureDictionary ('double', 'int8'); %! d(1) = 5; %! assert_equal (values (d, 'cell'), {int8(5)}); %!test %! d = configureDictionary ('double', 'int8'); %! d(1) = int8 (5); %! assert_equal (values (d, 'cell'), {int8(5)}); %!test %! d = configureDictionary ('double', 'int8'); %! d(1) = true; %! assert_equal (values (d, 'cell'), {int8(1)}); %!test %! d = configureDictionary ('double', 'int8'); %! d(1) = 'x'; %! assert_equal (values (d, 'cell'), {int8(120)}); %!test %! d = configureDictionary ('double', 'int8'); %! d(1) = '5'; %! assert_equal (values (d, 'cell'), {int8(53)}); %!error ... %! d = configureDictionary ('double', 'int8'); %! d(1) = string ('x'); %!error ... %! d = configureDictionary ('double', 'int8'); %! d(1) = string ('5'); %!error ... %! d = configureDictionary ('double', 'int8'); %! d(1) = {'x'}; %!error ... %! d = configureDictionary ('double', 'int8'); %! d(1) = {5}; %!test %! d = configureDictionary ('double', 'int8'); %! d(1) = categorical ({'x'}); %! assert_equal (values (d, 'cell'), {int8(1)}); %!error ... %! d = configureDictionary ('double', 'int8'); %! d(1) = datetime (2024, 1, 1); %!error ... %! d = configureDictionary ('double', 'int8'); %! d(1) = hours (1); %!error ... %! d = configureDictionary ('double', 'int8'); %! d(1) = calmonths (1); %!error ... %! d = configureDictionary ('double', 'int8'); %! d(1) = missing; %!error ... %! d = configureDictionary ('double', 'int8'); %! d(1) = struct ('a', 1); %!error ... %! d = configureDictionary ('double', 'int8'); %! d(1) = table (1); %!error ... %! d = configureDictionary ('double', 'int8'); %! d(1) = @sin; ## Conversion to a value of type 'logical', measured on R2024a %!test %! d = configureDictionary ('double', 'logical'); %! d(1) = 5; %! assert_equal (values (d, 'cell'), {true}); %!test %! d = configureDictionary ('double', 'logical'); %! d(1) = int8 (5); %! assert_equal (values (d, 'cell'), {true}); %!test %! d = configureDictionary ('double', 'logical'); %! d(1) = true; %! assert_equal (values (d, 'cell'), {true}); %!test %! d = configureDictionary ('double', 'logical'); %! d(1) = 'x'; %! assert_equal (values (d, 'cell'), {true}); %!test %! d = configureDictionary ('double', 'logical'); %! d(1) = '5'; %! assert_equal (values (d, 'cell'), {true}); %!error ... %! d = configureDictionary ('double', 'logical'); %! d(1) = string ('x'); %!error ... %! d = configureDictionary ('double', 'logical'); %! d(1) = string ('5'); %!error ... %! d = configureDictionary ('double', 'logical'); %! d(1) = {'x'}; %!error ... %! d = configureDictionary ('double', 'logical'); %! d(1) = {5}; %!error ... %! d = configureDictionary ('double', 'logical'); %! d(1) = categorical ({'x'}); %!error ... %! d = configureDictionary ('double', 'logical'); %! d(1) = datetime (2024, 1, 1); %!error ... %! d = configureDictionary ('double', 'logical'); %! d(1) = hours (1); %!error ... %! d = configureDictionary ('double', 'logical'); %! d(1) = calmonths (1); %!error ... %! d = configureDictionary ('double', 'logical'); %! d(1) = missing; %!error ... %! d = configureDictionary ('double', 'logical'); %! d(1) = struct ('a', 1); %!error ... %! d = configureDictionary ('double', 'logical'); %! d(1) = table (1); %!error ... %! d = configureDictionary ('double', 'logical'); %! d(1) = @sin; ## Conversion to a value of type 'string', measured on R2024a %!test %! d = configureDictionary ('double', 'string'); %! d(1) = 5; %! assert_equal (values (d, 'cell'), {string('5')}); %!test %! d = configureDictionary ('double', 'string'); %! d(1) = int8 (5); %! assert_equal (values (d, 'cell'), {string('5')}); %!test %! d = configureDictionary ('double', 'string'); %! d(1) = true; %! assert_equal (values (d, 'cell'), {string('true')}); %!test %! d = configureDictionary ('double', 'string'); %! d(1) = 'x'; %! assert_equal (values (d, 'cell'), {string('x')}); %!test %! d = configureDictionary ('double', 'string'); %! d(1) = '5'; %! assert_equal (values (d, 'cell'), {string('5')}); %!test %! d = configureDictionary ('double', 'string'); %! d(1) = string ('x'); %! assert_equal (values (d, 'cell'), {string('x')}); %!test %! d = configureDictionary ('double', 'string'); %! d(1) = string ('5'); %! assert_equal (values (d, 'cell'), {string('5')}); %!test %! d = configureDictionary ('double', 'string'); %! d(1) = {'x'}; %! assert_equal (values (d, 'cell'), {string('x')}); %!test %! d = configureDictionary ('double', 'string'); %! d(1) = {5}; %! assert_equal (values (d, 'cell'), {string('5')}); %!test %! d = configureDictionary ('double', 'string'); %! d(1) = categorical ({'x'}); %! assert_equal (values (d, 'cell'), {string('x')}); %!test %! d = configureDictionary ('double', 'string'); %! d(1) = datetime (2024, 1, 1); %! assert_equal (values (d, 'cell'), {string('01-Jan-2024')}); %!test %! d = configureDictionary ('double', 'string'); %! d(1) = hours (1); %! assert_equal (values (d, 'cell'), {string('1 hr')}); %!test %! d = configureDictionary ('double', 'string'); %! d(1) = calmonths (1); %! assert_equal (values (d, 'cell'), {string('1mo')}); %!test %! d = configureDictionary ('double', 'string'); %! d(1) = missing; %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'string'); %! assert_equal (ismissing (r{1}), true); %!error ... %! d = configureDictionary ('double', 'string'); %! d(1) = struct ('a', 1); %!error ... %! d = configureDictionary ('double', 'string'); %! d(1) = table (1); %!error ... %! d = configureDictionary ('double', 'string'); %! d(1) = @sin; ## Conversion to a value of type 'cell', measured on R2024a %!error ... %! d = configureDictionary ('double', 'cell'); %! d(1) = 5; %!error ... %! d = configureDictionary ('double', 'cell'); %! d(1) = int8 (5); %!error ... %! d = configureDictionary ('double', 'cell'); %! d(1) = true; %!error ... %! d = configureDictionary ('double', 'cell'); %! d(1) = 'x'; %!error ... %! d = configureDictionary ('double', 'cell'); %! d(1) = '5'; %!error ... %! d = configureDictionary ('double', 'cell'); %! d(1) = string ('x'); %!error ... %! d = configureDictionary ('double', 'cell'); %! d(1) = string ('5'); %!test %! d = configureDictionary ('double', 'cell'); %! d(1) = {'x'}; %! assert_equal (values (d, 'cell'), {{'x'}}); %!test %! d = configureDictionary ('double', 'cell'); %! d(1) = {5}; %! assert_equal (values (d, 'cell'), {{5}}); %!error ... %! d = configureDictionary ('double', 'cell'); %! d(1) = categorical ({'x'}); %!error ... %! d = configureDictionary ('double', 'cell'); %! d(1) = datetime (2024, 1, 1); %!error ... %! d = configureDictionary ('double', 'cell'); %! d(1) = hours (1); %!error ... %! d = configureDictionary ('double', 'cell'); %! d(1) = calmonths (1); %!error ... %! d = configureDictionary ('double', 'cell'); %! d(1) = missing; %!error ... %! d = configureDictionary ('double', 'cell'); %! d(1) = struct ('a', 1); %!error ... %! d = configureDictionary ('double', 'cell'); %! d(1) = table (1); %!error ... %! d = configureDictionary ('double', 'cell'); %! d(1) = @sin; ## Conversion to a value of type 'struct', measured on R2024a %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = 5; %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = int8 (5); %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = true; %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = 'x'; %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = '5'; %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = string ('x'); %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = string ('5'); %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = {'x'}; %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = {5}; ## D6: an object is never a structure; MATLAB takes its private properties. %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = categorical ({'x'}); ## D6: an object is never a structure; MATLAB takes its private properties. %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = datetime (2024, 1, 1); ## D6: an object is never a structure; MATLAB takes its private properties. %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = hours (1); ## D6: an object is never a structure; MATLAB takes its private properties. %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = calmonths (1); %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = missing; %!test %! d = configureDictionary ('double', 'struct'); %! d(1) = struct ('a', 1); %! assert_equal (values (d, 'cell'), {struct('a', 1)}); ## D6: an object is never a structure; MATLAB takes its private properties. %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = table (1); %!error ... %! d = configureDictionary ('double', 'struct'); %! d(1) = @sin; ## Conversion to a value of type 'function_handle', measured on R2024a %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = 5; %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = int8 (5); %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = true; %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = 'x'; %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = '5'; %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = string ('x'); %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = string ('5'); %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = {'x'}; %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = {5}; %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = categorical ({'x'}); %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = datetime (2024, 1, 1); %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = hours (1); %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = calmonths (1); %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = missing; %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = struct ('a', 1); %!error ... %! d = configureDictionary ('double', 'function_handle'); %! d(1) = table (1); %!test %! d = configureDictionary ('double', 'function_handle'); %! d(1) = @sin; %! assert_equal (values (d, 'cell'), {@sin}); ## Conversion to a value of type 'datetime', measured on R2024a %!error ... %! d = configureDictionary ('double', 'datetime'); %! d(1) = 5; %!error ... %! d = configureDictionary ('double', 'datetime'); %! d(1) = int8 (5); %!error ... %! d = configureDictionary ('double', 'datetime'); %! d(1) = true; %!error ... %! d = configureDictionary ('double', 'datetime'); %! d(1) = 'x'; %!error ... %! d = configureDictionary ('double', 'datetime'); %! d(1) = '5'; %!error ... %! d = configureDictionary ('double', 'datetime'); %! d(1) = string ('x'); %!error ... %! d = configureDictionary ('double', 'datetime'); %! d(1) = string ('5'); %!error ... %! d = configureDictionary ('double', 'datetime'); %! d(1) = {'x'}; %!error ... %! d = configureDictionary ('double', 'datetime'); %! d(1) = {5}; %!error ... %! d = configureDictionary ('double', 'datetime'); %! d(1) = categorical ({'x'}); %!test %! d = configureDictionary ('double', 'datetime'); %! d(1) = datetime (2024, 1, 1); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-01 00:00:00.000'}); %! assert_equal (x1.TimeZone, ''); %!error ... %! d = configureDictionary ('double', 'datetime'); %! d(1) = hours (1); %!error ... %! d = configureDictionary ('double', 'datetime'); %! d(1) = calmonths (1); %!test %! d = configureDictionary ('double', 'datetime'); %! d(1) = missing; %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'NaT'}); %! assert_equal (x1.TimeZone, ''); %!error ... %! d = configureDictionary ('double', 'datetime'); %! d(1) = struct ('a', 1); %!error ... %! d = configureDictionary ('double', 'datetime'); %! d(1) = table (1); %!error ... %! d = configureDictionary ('double', 'datetime'); %! d(1) = @sin; ## Conversion to a value of type 'duration', measured on R2024a %!error ... %! d = configureDictionary ('double', 'duration'); %! d(1) = 5; %!error ... %! d = configureDictionary ('double', 'duration'); %! d(1) = int8 (5); %!error ... %! d = configureDictionary ('double', 'duration'); %! d(1) = true; %!error ... %! d = configureDictionary ('double', 'duration'); %! d(1) = 'x'; %!error ... %! d = configureDictionary ('double', 'duration'); %! d(1) = '5'; %!error ... %! d = configureDictionary ('double', 'duration'); %! d(1) = string ('x'); %!error ... %! d = configureDictionary ('double', 'duration'); %! d(1) = string ('5'); %!error ... %! d = configureDictionary ('double', 'duration'); %! d(1) = {'x'}; %!error ... %! d = configureDictionary ('double', 'duration'); %! d(1) = {5}; %!error ... %! d = configureDictionary ('double', 'duration'); %! d(1) = categorical ({'x'}); %!error ... %! d = configureDictionary ('double', 'duration'); %! d(1) = datetime (2024, 1, 1); %!test %! d = configureDictionary ('double', 'duration'); %! d(1) = hours (1); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'duration'); %! assert_equal (seconds (r{1}), 3600); %!error ... %! d = configureDictionary ('double', 'duration'); %! d(1) = calmonths (1); %!test %! d = configureDictionary ('double', 'duration'); %! d(1) = missing; %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'duration'); %! assert_equal (seconds (r{1}), NaN); %!error ... %! d = configureDictionary ('double', 'duration'); %! d(1) = struct ('a', 1); %!error ... %! d = configureDictionary ('double', 'duration'); %! d(1) = table (1); %!error ... %! d = configureDictionary ('double', 'duration'); %! d(1) = @sin; ## Conversion to a value of type 'calendarDuration', measured on R2024a %!error ... %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = 5; %!error ... %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = int8 (5); %!error ... %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = true; %!error ... %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = 'x'; %!error ... %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = '5'; %!error ... %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = string ('x'); %!error ... %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = string ('5'); %!error ... %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = {'x'}; %!error ... %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = {5}; %!error ... %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = categorical ({'x'}); %!error ... %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = datetime (2024, 1, 1); %!error ... %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = hours (1); %!test %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = calmonths (1); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! [m1, dd2, t3] = split (r{1}, {'months', 'days', 'time'}); %! assert_equal ([m1(:), dd2(:), seconds(t3(:))], [1, 0, 0]); %!test %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = missing; %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! [m1, dd2, t3] = split (r{1}, {'months', 'days', 'time'}); %! assert_equal ([m1(:), dd2(:), seconds(t3(:))], [NaN, NaN, NaN]); %!error ... %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = struct ('a', 1); %!error ... %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = table (1); %!error ... %! d = configureDictionary ('double', 'calendarDuration'); %! d(1) = @sin; ## Conversion to a value of type 'categorical', measured on R2024a %!test %! d = configureDictionary ('double', 'categorical'); %! d(1) = 5; %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'5'}); %! assert_equal (categories (r{1}), {'5'}); %! assert_equal (isordinal (r{1}), false); %!test %! d = configureDictionary ('double', 'categorical'); %! d(1) = int8 (5); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'5'}); %! assert_equal (categories (r{1}), {'5'}); %! assert_equal (isordinal (r{1}), false); %!test %! d = configureDictionary ('double', 'categorical'); %! d(1) = true; %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'true'}); %! assert_equal (categories (r{1}), {'true'}); %! assert_equal (isordinal (r{1}), false); %!error ... %! d = configureDictionary ('double', 'categorical'); %! d(1) = 'x'; %!error ... %! d = configureDictionary ('double', 'categorical'); %! d(1) = '5'; %!test %! d = configureDictionary ('double', 'categorical'); %! d(1) = string ('x'); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'x'}); %! assert_equal (categories (r{1}), {'x'}); %! assert_equal (isordinal (r{1}), false); %!test %! d = configureDictionary ('double', 'categorical'); %! d(1) = string ('5'); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'5'}); %! assert_equal (categories (r{1}), {'5'}); %! assert_equal (isordinal (r{1}), false); %!test %! d = configureDictionary ('double', 'categorical'); %! d(1) = {'x'}; %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'x'}); %! assert_equal (categories (r{1}), {'x'}); %! assert_equal (isordinal (r{1}), false); %!error ... %! d = configureDictionary ('double', 'categorical'); %! d(1) = {5}; %!test %! d = configureDictionary ('double', 'categorical'); %! d(1) = categorical ({'x'}); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'x'}); %! assert_equal (categories (r{1}), {'x'}); %! assert_equal (isordinal (r{1}), false); %!test %! d = configureDictionary ('double', 'categorical'); %! d(1) = datetime (2024, 1, 1); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'01-Jan-2024'}); %! assert_equal (categories (r{1}), {'01-Jan-2024'}); %! assert_equal (isordinal (r{1}), false); %!test %! d = configureDictionary ('double', 'categorical'); %! d(1) = hours (1); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'1 hr'}); %! assert_equal (categories (r{1}), {'1 hr'}); %! assert_equal (isordinal (r{1}), false); ## Our categorical takes no calendarDuration, where MATLAB's makes '1mo'. %!error ... %! d = configureDictionary ('double', 'categorical'); %! d(1) = calmonths (1); %!test %! d = configureDictionary ('double', 'categorical'); %! d(1) = missing; %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (isundefined (r{1}), true); %! assert_equal (categories (r{1}), cell (0, 0)); %! assert_equal (isordinal (r{1}), false); %!error ... %! d = configureDictionary ('double', 'categorical'); %! d(1) = struct ('a', 1); %!error ... %! d = configureDictionary ('double', 'categorical'); %! d(1) = table (1); %!error ... %! d = configureDictionary ('double', 'categorical'); %! d(1) = @sin; ## Conversion to a value of type 'missing', measured on R2024a %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = 5; %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = int8 (5); %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = true; %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = 'x'; %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = '5'; %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = string ('x'); %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = string ('5'); %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = {'x'}; %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = {5}; %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = categorical ({'x'}); %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = datetime (2024, 1, 1); %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = hours (1); %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = calmonths (1); %!test %! d = configureDictionary ('double', 'missing'); %! d(1) = missing; %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'missing'); %! assert_equal (size (r{1}), [1, 1]); %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = struct ('a', 1); %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = table (1); %!error ... %! d = configureDictionary ('double', 'missing'); %! d(1) = @sin; ## Conversion to a value of type 'table', measured on R2024a %!test %! d = configureDictionary ('double', 'table'); %! d(1) = 5; %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! tb1 = r{1}; %! assert_equal (size (tb1), [1, 1]); %! assert_equal (tb1.Properties.VariableNames, {'Var1'}); %! assert_equal (tb1.Var1, 5); %!test %! d = configureDictionary ('double', 'table'); %! d(1) = int8 (5); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! tb1 = r{1}; %! assert_equal (size (tb1), [1, 1]); %! assert_equal (tb1.Properties.VariableNames, {'Var1'}); %! assert_equal (tb1.Var1, int8 (5)); %!test %! d = configureDictionary ('double', 'table'); %! d(1) = true; %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! tb1 = r{1}; %! assert_equal (size (tb1), [1, 1]); %! assert_equal (tb1.Properties.VariableNames, {'Var1'}); %! assert_equal (tb1.Var1, true); %!error ... %! d = configureDictionary ('double', 'table'); %! d(1) = 'x'; %!error ... %! d = configureDictionary ('double', 'table'); %! d(1) = '5'; %!test %! d = configureDictionary ('double', 'table'); %! d(1) = string ('x'); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! tb1 = r{1}; %! assert_equal (size (tb1), [1, 1]); %! assert_equal (tb1.Properties.VariableNames, {'Var1'}); %! assert_equal (tb1.Var1, string ('x')); %!test %! d = configureDictionary ('double', 'table'); %! d(1) = string ('5'); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! tb1 = r{1}; %! assert_equal (size (tb1), [1, 1]); %! assert_equal (tb1.Properties.VariableNames, {'Var1'}); %! assert_equal (tb1.Var1, string ('5')); %!test %! d = configureDictionary ('double', 'table'); %! d(1) = {'x'}; %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! tb1 = r{1}; %! assert_equal (size (tb1), [1, 1]); %! assert_equal (tb1.Properties.VariableNames, {'Var1'}); %! assert_equal (tb1.Var1, {'x'}); %!test %! d = configureDictionary ('double', 'table'); %! d(1) = {5}; %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! tb1 = r{1}; %! assert_equal (size (tb1), [1, 1]); %! assert_equal (tb1.Properties.VariableNames, {'Var1'}); %! assert_equal (tb1.Var1, {5}); %!test %! d = configureDictionary ('double', 'table'); %! d(1) = categorical ({'x'}); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! tb1 = r{1}; %! assert_equal (size (tb1), [1, 1]); %! assert_equal (tb1.Properties.VariableNames, {'Var1'}); %! assert_equal (class (tb1.Var1), 'categorical'); %! assert_equal (size (tb1.Var1), [1, 1]); %! assert_equal (cellstr (tb1.Var1), {'x'}); %! assert_equal (categories (tb1.Var1), {'x'}); %! assert_equal (isordinal (tb1.Var1), false); %!test %! d = configureDictionary ('double', 'table'); %! d(1) = datetime (2024, 1, 1); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! tb1 = r{1}; %! assert_equal (size (tb1), [1, 1]); %! assert_equal (tb1.Properties.VariableNames, {'Var1'}); %! x2 = tb1.Var1; %! x2.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x2), {'2024-01-01 00:00:00.000'}); %! assert_equal (x2.TimeZone, ''); %!test %! d = configureDictionary ('double', 'table'); %! d(1) = hours (1); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! tb1 = r{1}; %! assert_equal (size (tb1), [1, 1]); %! assert_equal (tb1.Properties.VariableNames, {'Var1'}); %! assert_equal (class (tb1.Var1), 'duration'); %! assert_equal (seconds (tb1.Var1), 3600); %!test %! d = configureDictionary ('double', 'table'); %! d(1) = calmonths (1); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! tb1 = r{1}; %! assert_equal (size (tb1), [1, 1]); %! assert_equal (tb1.Properties.VariableNames, {'Var1'}); %! [m2, dd3, t4] = split (tb1.Var1, {'months', 'days', 'time'}); %! assert_equal ([m2(:), dd3(:), seconds(t4(:))], [1, 0, 0]); %!test %! d = configureDictionary ('double', 'table'); %! d(1) = missing; %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! tb1 = r{1}; %! assert_equal (size (tb1), [1, 1]); %! assert_equal (tb1.Properties.VariableNames, {'Var1'}); %! assert_equal (class (tb1.Var1), 'missing'); %! assert_equal (size (tb1.Var1), [1, 1]); %!test %! d = configureDictionary ('double', 'table'); %! d(1) = struct ('a', 1); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! tb1 = r{1}; %! assert_equal (size (tb1), [1, 1]); %! assert_equal (tb1.Properties.VariableNames, {'Var1'}); %! assert_equal (tb1.Var1, struct ('a', 1)); %!test %! d = configureDictionary ('double', 'table'); %! d(1) = table (1); %! r = values (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! tb1 = r{1}; %! assert_equal (size (tb1), [1, 1]); %! assert_equal (tb1.Properties.VariableNames, {'Var1'}); %! assert_equal (tb1.Var1, 1); %!error ... %! d = configureDictionary ('double', 'table'); %! d(1) = @sin; ## Conversion to a value of type 'dictionary', measured on R2024a %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = 5; %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = int8 (5); %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = true; %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = 'x'; %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = '5'; %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = string ('x'); %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = string ('5'); %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = {'x'}; %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = {5}; %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = categorical ({'x'}); %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = datetime (2024, 1, 1); %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = hours (1); %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = calmonths (1); %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = missing; %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = struct ('a', 1); %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = table (1); %!error ... %! d = configureDictionary ('double', 'dictionary'); %! d(1) = @sin; ## Conversion to a key of type 'double', measured on R2024a %!test %! d = configureDictionary ('double', 'double'); %! d(5) = 1; %! assert_equal (keys (d, 'cell'), {5}); %!test %! d = configureDictionary ('double', 'double'); %! d(int8 (5)) = 1; %! assert_equal (keys (d, 'cell'), {5}); %!test %! d = configureDictionary ('double', 'double'); %! d(true) = 1; %! assert_equal (keys (d, 'cell'), {1}); %!test %! d = configureDictionary ('double', 'double'); %! d('x') = 1; %! assert_equal (keys (d, 'cell'), {120}); %!test %! d = configureDictionary ('double', 'double'); %! d('5') = 1; %! assert_equal (keys (d, 'cell'), {53}); %!test %! d = configureDictionary ('double', 'double'); %! d(string ('x')) = 1; %! assert_equal (keys (d, 'cell'), {NaN}); %!test %! d = configureDictionary ('double', 'double'); %! d(string ('5')) = 1; %! assert_equal (keys (d, 'cell'), {5}); %!error ... %! d = configureDictionary ('double', 'double'); %! d({'x'}) = 1; %!error ... %! d = configureDictionary ('double', 'double'); %! d({5}) = 1; %!test %! d = configureDictionary ('double', 'double'); %! d(categorical ({'x'})) = 1; %! assert_equal (keys (d, 'cell'), {1}); %!error ... %! d = configureDictionary ('double', 'double'); %! d(datetime (2024, 1, 1)) = 1; %!error ... %! d = configureDictionary ('double', 'double'); %! d(hours (1)) = 1; %!error ... %! d = configureDictionary ('double', 'double'); %! d(calmonths (1)) = 1; %!test %! d = configureDictionary ('double', 'double'); %! d(missing) = 1; %! assert_equal (keys (d, 'cell'), {NaN}); %!error ... %! d = configureDictionary ('double', 'double'); %! d(struct ('a', 1)) = 1; %!error ... %! d = configureDictionary ('double', 'double'); %! d(table (1)) = 1; %!error ... %! d = configureDictionary ('double', 'double'); %! d(@sin) = 1; ## Conversion to a key of type 'int8', measured on R2024a %!test %! d = configureDictionary ('int8', 'double'); %! d(5) = 1; %! assert_equal (keys (d, 'cell'), {int8(5)}); %!test %! d = configureDictionary ('int8', 'double'); %! d(int8 (5)) = 1; %! assert_equal (keys (d, 'cell'), {int8(5)}); %!test %! d = configureDictionary ('int8', 'double'); %! d(true) = 1; %! assert_equal (keys (d, 'cell'), {int8(1)}); %!test %! d = configureDictionary ('int8', 'double'); %! d('x') = 1; %! assert_equal (keys (d, 'cell'), {int8(120)}); %!test %! d = configureDictionary ('int8', 'double'); %! d('5') = 1; %! assert_equal (keys (d, 'cell'), {int8(53)}); %!error ... %! d = configureDictionary ('int8', 'double'); %! d(string ('x')) = 1; %!error ... %! d = configureDictionary ('int8', 'double'); %! d(string ('5')) = 1; %!error ... %! d = configureDictionary ('int8', 'double'); %! d({'x'}) = 1; %!error ... %! d = configureDictionary ('int8', 'double'); %! d({5}) = 1; %!test %! d = configureDictionary ('int8', 'double'); %! d(categorical ({'x'})) = 1; %! assert_equal (keys (d, 'cell'), {int8(1)}); %!error ... %! d = configureDictionary ('int8', 'double'); %! d(datetime (2024, 1, 1)) = 1; %!error ... %! d = configureDictionary ('int8', 'double'); %! d(hours (1)) = 1; %!error ... %! d = configureDictionary ('int8', 'double'); %! d(calmonths (1)) = 1; %!error ... %! d = configureDictionary ('int8', 'double'); %! d(missing) = 1; %!error ... %! d = configureDictionary ('int8', 'double'); %! d(struct ('a', 1)) = 1; %!error ... %! d = configureDictionary ('int8', 'double'); %! d(table (1)) = 1; %!error ... %! d = configureDictionary ('int8', 'double'); %! d(@sin) = 1; ## Conversion to a key of type 'logical', measured on R2024a %!test %! d = configureDictionary ('logical', 'double'); %! d(5) = 1; %! assert_equal (keys (d, 'cell'), {true}); %!test %! d = configureDictionary ('logical', 'double'); %! d(int8 (5)) = 1; %! assert_equal (keys (d, 'cell'), {true}); %!test %! d = configureDictionary ('logical', 'double'); %! d(true) = 1; %! assert_equal (keys (d, 'cell'), {true}); %!test %! d = configureDictionary ('logical', 'double'); %! d('x') = 1; %! assert_equal (keys (d, 'cell'), {true}); %!test %! d = configureDictionary ('logical', 'double'); %! d('5') = 1; %! assert_equal (keys (d, 'cell'), {true}); %!error ... %! d = configureDictionary ('logical', 'double'); %! d(string ('x')) = 1; %!error ... %! d = configureDictionary ('logical', 'double'); %! d(string ('5')) = 1; %!error ... %! d = configureDictionary ('logical', 'double'); %! d({'x'}) = 1; %!error ... %! d = configureDictionary ('logical', 'double'); %! d({5}) = 1; %!error ... %! d = configureDictionary ('logical', 'double'); %! d(categorical ({'x'})) = 1; %!error ... %! d = configureDictionary ('logical', 'double'); %! d(datetime (2024, 1, 1)) = 1; %!error ... %! d = configureDictionary ('logical', 'double'); %! d(hours (1)) = 1; %!error ... %! d = configureDictionary ('logical', 'double'); %! d(calmonths (1)) = 1; %!error ... %! d = configureDictionary ('logical', 'double'); %! d(missing) = 1; %!error ... %! d = configureDictionary ('logical', 'double'); %! d(struct ('a', 1)) = 1; %!error ... %! d = configureDictionary ('logical', 'double'); %! d(table (1)) = 1; %!error ... %! d = configureDictionary ('logical', 'double'); %! d(@sin) = 1; ## Conversion to a key of type 'string', measured on R2024a %!test %! d = configureDictionary ('string', 'double'); %! d(5) = 1; %! assert_equal (keys (d, 'cell'), {string('5')}); %!test %! d = configureDictionary ('string', 'double'); %! d(int8 (5)) = 1; %! assert_equal (keys (d, 'cell'), {string('5')}); %!test %! d = configureDictionary ('string', 'double'); %! d(true) = 1; %! assert_equal (keys (d, 'cell'), {string('true')}); %!test %! d = configureDictionary ('string', 'double'); %! d('x') = 1; %! assert_equal (keys (d, 'cell'), {string('x')}); %!test %! d = configureDictionary ('string', 'double'); %! d('5') = 1; %! assert_equal (keys (d, 'cell'), {string('5')}); %!test %! d = configureDictionary ('string', 'double'); %! d(string ('x')) = 1; %! assert_equal (keys (d, 'cell'), {string('x')}); %!test %! d = configureDictionary ('string', 'double'); %! d(string ('5')) = 1; %! assert_equal (keys (d, 'cell'), {string('5')}); %!test %! d = configureDictionary ('string', 'double'); %! d({'x'}) = 1; %! assert_equal (keys (d, 'cell'), {string('x')}); %!test %! d = configureDictionary ('string', 'double'); %! d({5}) = 1; %! assert_equal (keys (d, 'cell'), {string('5')}); %!test %! d = configureDictionary ('string', 'double'); %! d(categorical ({'x'})) = 1; %! assert_equal (keys (d, 'cell'), {string('x')}); %!test %! d = configureDictionary ('string', 'double'); %! d(datetime (2024, 1, 1)) = 1; %! assert_equal (keys (d, 'cell'), {string('01-Jan-2024')}); %!test %! d = configureDictionary ('string', 'double'); %! d(hours (1)) = 1; %! assert_equal (keys (d, 'cell'), {string('1 hr')}); %!test %! d = configureDictionary ('string', 'double'); %! d(calmonths (1)) = 1; %! assert_equal (keys (d, 'cell'), {string('1mo')}); %!test %! d = configureDictionary ('string', 'double'); %! d(missing) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'string'); %! assert_equal (ismissing (r{1}), true); %!error ... %! d = configureDictionary ('string', 'double'); %! d(struct ('a', 1)) = 1; %!error ... %! d = configureDictionary ('string', 'double'); %! d(table (1)) = 1; %!error ... %! d = configureDictionary ('string', 'double'); %! d(@sin) = 1; ## Conversion to a key of type 'cell', measured on R2024a %!error ... %! d = configureDictionary ('cell', 'double'); %! d(5) = 1; %!error ... %! d = configureDictionary ('cell', 'double'); %! d(int8 (5)) = 1; %!error ... %! d = configureDictionary ('cell', 'double'); %! d(true) = 1; %!error ... %! d = configureDictionary ('cell', 'double'); %! d('x') = 1; %!error ... %! d = configureDictionary ('cell', 'double'); %! d('5') = 1; %!error ... %! d = configureDictionary ('cell', 'double'); %! d(string ('x')) = 1; %!error ... %! d = configureDictionary ('cell', 'double'); %! d(string ('5')) = 1; %!test %! d = configureDictionary ('cell', 'double'); %! d({'x'}) = 1; %! assert_equal (keys (d, 'cell'), {{'x'}}); %!test %! d = configureDictionary ('cell', 'double'); %! d({5}) = 1; %! assert_equal (keys (d, 'cell'), {{5}}); %!error ... %! d = configureDictionary ('cell', 'double'); %! d(categorical ({'x'})) = 1; %!error ... %! d = configureDictionary ('cell', 'double'); %! d(datetime (2024, 1, 1)) = 1; %!error ... %! d = configureDictionary ('cell', 'double'); %! d(hours (1)) = 1; %!error ... %! d = configureDictionary ('cell', 'double'); %! d(calmonths (1)) = 1; %!error ... %! d = configureDictionary ('cell', 'double'); %! d(missing) = 1; %!error ... %! d = configureDictionary ('cell', 'double'); %! d(struct ('a', 1)) = 1; %!error ... %! d = configureDictionary ('cell', 'double'); %! d(table (1)) = 1; %!error ... %! d = configureDictionary ('cell', 'double'); %! d(@sin) = 1; ## Conversion to a key of type 'datetime', measured on R2024a %!error ... %! d = configureDictionary ('datetime', 'double'); %! d(5) = 1; %!error ... %! d = configureDictionary ('datetime', 'double'); %! d(int8 (5)) = 1; %!error ... %! d = configureDictionary ('datetime', 'double'); %! d(true) = 1; %!error ... %! d = configureDictionary ('datetime', 'double'); %! d('x') = 1; %!error ... %! d = configureDictionary ('datetime', 'double'); %! d('5') = 1; %!error ... %! d = configureDictionary ('datetime', 'double'); %! d(string ('x')) = 1; %!error ... %! d = configureDictionary ('datetime', 'double'); %! d(string ('5')) = 1; %!error ... %! d = configureDictionary ('datetime', 'double'); %! d({'x'}) = 1; %!error ... %! d = configureDictionary ('datetime', 'double'); %! d({5}) = 1; %!error ... %! d = configureDictionary ('datetime', 'double'); %! d(categorical ({'x'})) = 1; %!test %! d = configureDictionary ('datetime', 'double'); %! d(datetime (2024, 1, 1)) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'2024-01-01 00:00:00.000'}); %! assert_equal (x1.TimeZone, ''); %!error ... %! d = configureDictionary ('datetime', 'double'); %! d(hours (1)) = 1; %!error ... %! d = configureDictionary ('datetime', 'double'); %! d(calmonths (1)) = 1; %!test %! d = configureDictionary ('datetime', 'double'); %! d(missing) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! x1 = r{1}; %! x1.Format = 'yyyy-MM-dd HH:mm:ss.SSS'; %! assert_equal (cellstr (x1), {'NaT'}); %! assert_equal (x1.TimeZone, ''); %!error ... %! d = configureDictionary ('datetime', 'double'); %! d(struct ('a', 1)) = 1; %!error ... %! d = configureDictionary ('datetime', 'double'); %! d(table (1)) = 1; %!error ... %! d = configureDictionary ('datetime', 'double'); %! d(@sin) = 1; ## Conversion to a key of type 'duration', measured on R2024a %!error ... %! d = configureDictionary ('duration', 'double'); %! d(5) = 1; %!error ... %! d = configureDictionary ('duration', 'double'); %! d(int8 (5)) = 1; %!error ... %! d = configureDictionary ('duration', 'double'); %! d(true) = 1; %!error ... %! d = configureDictionary ('duration', 'double'); %! d('x') = 1; %!error ... %! d = configureDictionary ('duration', 'double'); %! d('5') = 1; %!error ... %! d = configureDictionary ('duration', 'double'); %! d(string ('x')) = 1; %!error ... %! d = configureDictionary ('duration', 'double'); %! d(string ('5')) = 1; %!error ... %! d = configureDictionary ('duration', 'double'); %! d({'x'}) = 1; %!error ... %! d = configureDictionary ('duration', 'double'); %! d({5}) = 1; %!error ... %! d = configureDictionary ('duration', 'double'); %! d(categorical ({'x'})) = 1; %!error ... %! d = configureDictionary ('duration', 'double'); %! d(datetime (2024, 1, 1)) = 1; %!test %! d = configureDictionary ('duration', 'double'); %! d(hours (1)) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'duration'); %! assert_equal (seconds (r{1}), 3600); %!error ... %! d = configureDictionary ('duration', 'double'); %! d(calmonths (1)) = 1; %!test %! d = configureDictionary ('duration', 'double'); %! d(missing) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'duration'); %! assert_equal (seconds (r{1}), NaN); %!error ... %! d = configureDictionary ('duration', 'double'); %! d(struct ('a', 1)) = 1; %!error ... %! d = configureDictionary ('duration', 'double'); %! d(table (1)) = 1; %!error ... %! d = configureDictionary ('duration', 'double'); %! d(@sin) = 1; ## Conversion to a key of type 'calendarDuration', measured on R2024a %!error ... %! d = configureDictionary ('calendarDuration', 'double'); %! d(5) = 1; %!error ... %! d = configureDictionary ('calendarDuration', 'double'); %! d(int8 (5)) = 1; %!error ... %! d = configureDictionary ('calendarDuration', 'double'); %! d(true) = 1; %!error ... %! d = configureDictionary ('calendarDuration', 'double'); %! d('x') = 1; %!error ... %! d = configureDictionary ('calendarDuration', 'double'); %! d('5') = 1; %!error ... %! d = configureDictionary ('calendarDuration', 'double'); %! d(string ('x')) = 1; %!error ... %! d = configureDictionary ('calendarDuration', 'double'); %! d(string ('5')) = 1; %!error ... %! d = configureDictionary ('calendarDuration', 'double'); %! d({'x'}) = 1; %!error ... %! d = configureDictionary ('calendarDuration', 'double'); %! d({5}) = 1; %!error ... %! d = configureDictionary ('calendarDuration', 'double'); %! d(categorical ({'x'})) = 1; %!error ... %! d = configureDictionary ('calendarDuration', 'double'); %! d(datetime (2024, 1, 1)) = 1; %!error ... %! d = configureDictionary ('calendarDuration', 'double'); %! d(hours (1)) = 1; %!test %! d = configureDictionary ('calendarDuration', 'double'); %! d(calmonths (1)) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! [m1, dd2, t3] = split (r{1}, {'months', 'days', 'time'}); %! assert_equal ([m1(:), dd2(:), seconds(t3(:))], [1, 0, 0]); %!test %! d = configureDictionary ('calendarDuration', 'double'); %! d(missing) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! [m1, dd2, t3] = split (r{1}, {'months', 'days', 'time'}); %! assert_equal ([m1(:), dd2(:), seconds(t3(:))], [NaN, NaN, NaN]); %!error ... %! d = configureDictionary ('calendarDuration', 'double'); %! d(struct ('a', 1)) = 1; %!error ... %! d = configureDictionary ('calendarDuration', 'double'); %! d(table (1)) = 1; %!error ... %! d = configureDictionary ('calendarDuration', 'double'); %! d(@sin) = 1; ## Conversion to a key of type 'categorical', measured on R2024a %!test %! d = configureDictionary ('categorical', 'double'); %! d(5) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'5'}); %! assert_equal (categories (r{1}), {'5'}); %! assert_equal (isordinal (r{1}), false); %!test %! d = configureDictionary ('categorical', 'double'); %! d(int8 (5)) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'5'}); %! assert_equal (categories (r{1}), {'5'}); %! assert_equal (isordinal (r{1}), false); %!test %! d = configureDictionary ('categorical', 'double'); %! d(true) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'true'}); %! assert_equal (categories (r{1}), {'true'}); %! assert_equal (isordinal (r{1}), false); %!error ... %! d = configureDictionary ('categorical', 'double'); %! d('x') = 1; %!error ... %! d = configureDictionary ('categorical', 'double'); %! d('5') = 1; %!test %! d = configureDictionary ('categorical', 'double'); %! d(string ('x')) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'x'}); %! assert_equal (categories (r{1}), {'x'}); %! assert_equal (isordinal (r{1}), false); %!test %! d = configureDictionary ('categorical', 'double'); %! d(string ('5')) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'5'}); %! assert_equal (categories (r{1}), {'5'}); %! assert_equal (isordinal (r{1}), false); %!test %! d = configureDictionary ('categorical', 'double'); %! d({'x'}) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'x'}); %! assert_equal (categories (r{1}), {'x'}); %! assert_equal (isordinal (r{1}), false); %!error ... %! d = configureDictionary ('categorical', 'double'); %! d({5}) = 1; %!test %! d = configureDictionary ('categorical', 'double'); %! d(categorical ({'x'})) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'x'}); %! assert_equal (categories (r{1}), {'x'}); %! assert_equal (isordinal (r{1}), false); %!test %! d = configureDictionary ('categorical', 'double'); %! d(datetime (2024, 1, 1)) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'01-Jan-2024'}); %! assert_equal (categories (r{1}), {'01-Jan-2024'}); %! assert_equal (isordinal (r{1}), false); %!test %! d = configureDictionary ('categorical', 'double'); %! d(hours (1)) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (cellstr (r{1}), {'1 hr'}); %! assert_equal (categories (r{1}), {'1 hr'}); %! assert_equal (isordinal (r{1}), false); ## Our categorical takes no calendarDuration, where MATLAB's makes '1mo'. %!error ... %! d = configureDictionary ('categorical', 'double'); %! d(calmonths (1)) = 1; %!test %! d = configureDictionary ('categorical', 'double'); %! d(missing) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'categorical'); %! assert_equal (size (r{1}), [1, 1]); %! assert_equal (isundefined (r{1}), true); %! assert_equal (categories (r{1}), cell (0, 0)); %! assert_equal (isordinal (r{1}), false); %!error ... %! d = configureDictionary ('categorical', 'double'); %! d(struct ('a', 1)) = 1; %!error ... %! d = configureDictionary ('categorical', 'double'); %! d(table (1)) = 1; %!error ... %! d = configureDictionary ('categorical', 'double'); %! d(@sin) = 1; ## Conversion to a key of type 'missing', measured on R2024a %!error ... %! d = configureDictionary ('missing', 'double'); %! d(5) = 1; %!error ... %! d = configureDictionary ('missing', 'double'); %! d(int8 (5)) = 1; %!error ... %! d = configureDictionary ('missing', 'double'); %! d(true) = 1; %!error ... %! d = configureDictionary ('missing', 'double'); %! d('x') = 1; %!error ... %! d = configureDictionary ('missing', 'double'); %! d('5') = 1; %!error ... %! d = configureDictionary ('missing', 'double'); %! d(string ('x')) = 1; %!error ... %! d = configureDictionary ('missing', 'double'); %! d(string ('5')) = 1; %!error ... %! d = configureDictionary ('missing', 'double'); %! d({'x'}) = 1; %!error ... %! d = configureDictionary ('missing', 'double'); %! d({5}) = 1; %!error ... %! d = configureDictionary ('missing', 'double'); %! d(categorical ({'x'})) = 1; %!error ... %! d = configureDictionary ('missing', 'double'); %! d(datetime (2024, 1, 1)) = 1; %!error ... %! d = configureDictionary ('missing', 'double'); %! d(hours (1)) = 1; %!error ... %! d = configureDictionary ('missing', 'double'); %! d(calmonths (1)) = 1; %!test %! d = configureDictionary ('missing', 'double'); %! d(missing) = 1; %! r = keys (d, 'cell'); %! assert_equal (size (r), [1, 1]); %! assert_equal (class (r{1}), 'missing'); %! assert_equal (size (r{1}), [1, 1]); %!error ... %! d = configureDictionary ('missing', 'double'); %! d(struct ('a', 1)) = 1; %!error ... %! d = configureDictionary ('missing', 'double'); %! d(table (1)) = 1; %!error ... %! d = configureDictionary ('missing', 'double'); %! d(@sin) = 1; ## Conversion to a key of type 'struct', measured on R2024a %!error ... %! d = configureDictionary ('struct', 'double'); %! d(5) = 1; %!error ... %! d = configureDictionary ('struct', 'double'); %! d(int8 (5)) = 1; %!error ... %! d = configureDictionary ('struct', 'double'); %! d(true) = 1; %!error ... %! d = configureDictionary ('struct', 'double'); %! d('x') = 1; %!error ... %! d = configureDictionary ('struct', 'double'); %! d('5') = 1; %!error ... %! d = configureDictionary ('struct', 'double'); %! d(string ('x')) = 1; %!error ... %! d = configureDictionary ('struct', 'double'); %! d(string ('5')) = 1; %!error ... %! d = configureDictionary ('struct', 'double'); %! d({'x'}) = 1; %!error ... %! d = configureDictionary ('struct', 'double'); %! d({5}) = 1; ## D6: an object is never a structure; MATLAB takes its private properties. %!error ... %! d = configureDictionary ('struct', 'double'); %! d(categorical ({'x'})) = 1; ## D6: an object is never a structure; MATLAB takes its private properties. %!error ... %! d = configureDictionary ('struct', 'double'); %! d(datetime (2024, 1, 1)) = 1; ## D6: an object is never a structure; MATLAB takes its private properties. %!error ... %! d = configureDictionary ('struct', 'double'); %! d(hours (1)) = 1; ## D6: an object is never a structure; MATLAB takes its private properties. %!error ... %! d = configureDictionary ('struct', 'double'); %! d(calmonths (1)) = 1; %!error ... %! d = configureDictionary ('struct', 'double'); %! d(missing) = 1; %!test %! d = configureDictionary ('struct', 'double'); %! d(struct ('a', 1)) = 1; %! assert_equal (keys (d, 'cell'), {struct('a', 1)}); ## D6: an object is never a structure; MATLAB takes its private properties. %!error ... %! d = configureDictionary ('struct', 'double'); %! d(table (1)) = 1; %!error ... %! d = configureDictionary ('struct', 'double'); %! d(@sin) = 1; ## Conversion to a key of type 'function_handle', measured on R2024a %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d(5) = 1; %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d(int8 (5)) = 1; %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d(true) = 1; %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d('x') = 1; %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d('5') = 1; %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d(string ('x')) = 1; %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d(string ('5')) = 1; %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d({'x'}) = 1; %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d({5}) = 1; %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d(categorical ({'x'})) = 1; %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d(datetime (2024, 1, 1)) = 1; %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d(hours (1)) = 1; %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d(calmonths (1)) = 1; %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d(missing) = 1; %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d(struct ('a', 1)) = 1; %!error ... %! d = configureDictionary ('function_handle', 'double'); %! d(table (1)) = 1; %!test %! d = configureDictionary ('function_handle', 'double'); %! d(@sin) = 1; %! assert_equal (keys (d, 'cell'), {@sin}); ## A fixed sequence of 80 edits, measured on R2024a %!test %! t = configureDictionary ('string', 'double'); %! t(string ('d')) = 87; %! t(string ('h')) = 52; %! t(string ('a')) = 52; %! t = insert (t, string ('f'), 44); %! t(string ('e')) = []; %! t = insert (t, string ('f'), 7); %! t(string ('f')) = []; %! t(string ('h')) = []; %! t = insert (t, string ('g'), 99, 'Overwrite', false); %! t = remove (t, string ('d')); %! t = insert (t, string ('g'), 42, 'Overwrite', false); %! t = remove (t, string ('b')); %! t = insert (t, string ('c'), 55); %! t = insert (t, string ('e'), 75); %! t(string ({'f', 'f'})) = [60, 61]; %! t(string ('a')) = []; %! t(string ('c')) = []; %! t(string ({'d', 'a'})) = [97, 98]; %! t = insert (t, string ('g'), 15, 'Overwrite', false); %! t = insert (t, string ('e'), 81, 'Overwrite', false); %! t(string ('d')) = 9; %! t(string ({'c', 'c'})) = [51, 52]; %! t(string ('e')) = 93; %! t = remove (t, string ('b')); %! t(string ('e')) = 38; %! t(string ({'b', 'b'})) = [53, 54]; %! t = remove (t, string ('a')); %! t = insert (t, string ('b'), 73); %! t = remove (t, string ('a')); %! t(string ('b')) = 46; %! t(string ('h')) = 46; %! t = insert (t, string ('h'), 5, 'Overwrite', false); %! t(string ('b')) = []; %! t = remove (t, string ('f')); %! t(string ({'g', 'f'})) = [23, 24]; %! t = remove (t, string ('g')); %! t = insert (t, string ('g'), 67, 'Overwrite', false); %! t(string ({'b', 'f'})) = [18, 19]; %! t = insert (t, string ('b'), 88); %! t(string ('h')) = 99; %! t = insert (t, string ('f'), 25); %! t = remove (t, string ('e')); %! t = remove (t, string ('e')); %! t = insert (t, string ('f'), 91); %! t = insert (t, string ('g'), 59, 'Overwrite', false); %! t(string ('f')) = []; %! t = insert (t, string ('c'), 86); %! t = remove (t, string ('h')); %! t(string ('g')) = []; %! t = insert (t, string ('g'), 19); %! t(string ({'e', 'h'})) = [80, 81]; %! t = insert (t, string ('d'), 85); %! t = insert (t, string ('d'), 25, 'Overwrite', false); %! t(string ('e')) = 84; %! t = insert (t, string ('c'), 87); %! t(string ('f')) = 53; %! t(string ({'e', 'b'})) = [53, 54]; %! t(string ('e')) = 15; %! t(string ({'h', 'c'})) = [70, 71]; %! t(string ('g')) = 78; %! t(string ('b')) = 8; %! t(string ('h')) = 29; %! t = insert (t, string ('a'), 28); %! t = insert (t, string ('e'), 18); %! t(string ('a')) = 73; %! t(string ('d')) = []; %! t(string ('f')) = 99; %! t(string ({'c', 'b'})) = [10, 11]; %! t = insert (t, string ('g'), 45, 'Overwrite', false); %! t(string ({'g', 'f'})) = [10, 11]; %! t(string ({'d', 'g'})) = [63, 64]; %! t = insert (t, string ('h'), 11, 'Overwrite', false); %! t(string ('a')) = 28; %! t(string ('a')) = 65; %! t(string ('f')) = 2; %! t(string ('c')) = 75; %! t = insert (t, string ('d'), 51, 'Overwrite', false); %! t(string ('c')) = 90; %! t = insert (t, string ('e'), 67); %! t(string ({'b', 'f'})) = [58, 59]; %! assert_equal (keys (t), string ({'c'; 'b'; 'g'; 'e'; 'h'; 'f'; 'a'; 'd'})); %!test %! t = configureDictionary ('string', 'double'); %! t(string ('d')) = 87; %! t(string ('h')) = 52; %! t(string ('a')) = 52; %! t = insert (t, string ('f'), 44); %! t(string ('e')) = []; %! t = insert (t, string ('f'), 7); %! t(string ('f')) = []; %! t(string ('h')) = []; %! t = insert (t, string ('g'), 99, 'Overwrite', false); %! t = remove (t, string ('d')); %! t = insert (t, string ('g'), 42, 'Overwrite', false); %! t = remove (t, string ('b')); %! t = insert (t, string ('c'), 55); %! t = insert (t, string ('e'), 75); %! t(string ({'f', 'f'})) = [60, 61]; %! t(string ('a')) = []; %! t(string ('c')) = []; %! t(string ({'d', 'a'})) = [97, 98]; %! t = insert (t, string ('g'), 15, 'Overwrite', false); %! t = insert (t, string ('e'), 81, 'Overwrite', false); %! t(string ('d')) = 9; %! t(string ({'c', 'c'})) = [51, 52]; %! t(string ('e')) = 93; %! t = remove (t, string ('b')); %! t(string ('e')) = 38; %! t(string ({'b', 'b'})) = [53, 54]; %! t = remove (t, string ('a')); %! t = insert (t, string ('b'), 73); %! t = remove (t, string ('a')); %! t(string ('b')) = 46; %! t(string ('h')) = 46; %! t = insert (t, string ('h'), 5, 'Overwrite', false); %! t(string ('b')) = []; %! t = remove (t, string ('f')); %! t(string ({'g', 'f'})) = [23, 24]; %! t = remove (t, string ('g')); %! t = insert (t, string ('g'), 67, 'Overwrite', false); %! t(string ({'b', 'f'})) = [18, 19]; %! t = insert (t, string ('b'), 88); %! t(string ('h')) = 99; %! t = insert (t, string ('f'), 25); %! t = remove (t, string ('e')); %! t = remove (t, string ('e')); %! t = insert (t, string ('f'), 91); %! t = insert (t, string ('g'), 59, 'Overwrite', false); %! t(string ('f')) = []; %! t = insert (t, string ('c'), 86); %! t = remove (t, string ('h')); %! t(string ('g')) = []; %! t = insert (t, string ('g'), 19); %! t(string ({'e', 'h'})) = [80, 81]; %! t = insert (t, string ('d'), 85); %! t = insert (t, string ('d'), 25, 'Overwrite', false); %! t(string ('e')) = 84; %! t = insert (t, string ('c'), 87); %! t(string ('f')) = 53; %! t(string ({'e', 'b'})) = [53, 54]; %! t(string ('e')) = 15; %! t(string ({'h', 'c'})) = [70, 71]; %! t(string ('g')) = 78; %! t(string ('b')) = 8; %! t(string ('h')) = 29; %! t = insert (t, string ('a'), 28); %! t = insert (t, string ('e'), 18); %! t(string ('a')) = 73; %! t(string ('d')) = []; %! t(string ('f')) = 99; %! t(string ({'c', 'b'})) = [10, 11]; %! t = insert (t, string ('g'), 45, 'Overwrite', false); %! t(string ({'g', 'f'})) = [10, 11]; %! t(string ({'d', 'g'})) = [63, 64]; %! t = insert (t, string ('h'), 11, 'Overwrite', false); %! t(string ('a')) = 28; %! t(string ('a')) = 65; %! t(string ('f')) = 2; %! t(string ('c')) = 75; %! t = insert (t, string ('d'), 51, 'Overwrite', false); %! t(string ('c')) = 90; %! t = insert (t, string ('e'), 67); %! t(string ({'b', 'f'})) = [58, 59]; %! assert_equal (values (t), [90; 58; 64; 67; 29; 59; 65; 63]); %!test %! t = configureDictionary ('string', 'double'); %! t(string ('d')) = 87; %! t(string ('h')) = 52; %! t(string ('a')) = 52; %! t = insert (t, string ('f'), 44); %! t(string ('e')) = []; %! t = insert (t, string ('f'), 7); %! t(string ('f')) = []; %! t(string ('h')) = []; %! t = insert (t, string ('g'), 99, 'Overwrite', false); %! t = remove (t, string ('d')); %! t = insert (t, string ('g'), 42, 'Overwrite', false); %! t = remove (t, string ('b')); %! t = insert (t, string ('c'), 55); %! t = insert (t, string ('e'), 75); %! t(string ({'f', 'f'})) = [60, 61]; %! t(string ('a')) = []; %! t(string ('c')) = []; %! t(string ({'d', 'a'})) = [97, 98]; %! t = insert (t, string ('g'), 15, 'Overwrite', false); %! t = insert (t, string ('e'), 81, 'Overwrite', false); %! t(string ('d')) = 9; %! t(string ({'c', 'c'})) = [51, 52]; %! t(string ('e')) = 93; %! t = remove (t, string ('b')); %! t(string ('e')) = 38; %! t(string ({'b', 'b'})) = [53, 54]; %! t = remove (t, string ('a')); %! t = insert (t, string ('b'), 73); %! t = remove (t, string ('a')); %! t(string ('b')) = 46; %! t(string ('h')) = 46; %! t = insert (t, string ('h'), 5, 'Overwrite', false); %! t(string ('b')) = []; %! t = remove (t, string ('f')); %! t(string ({'g', 'f'})) = [23, 24]; %! t = remove (t, string ('g')); %! t = insert (t, string ('g'), 67, 'Overwrite', false); %! t(string ({'b', 'f'})) = [18, 19]; %! t = insert (t, string ('b'), 88); %! t(string ('h')) = 99; %! t = insert (t, string ('f'), 25); %! t = remove (t, string ('e')); %! t = remove (t, string ('e')); %! t = insert (t, string ('f'), 91); %! t = insert (t, string ('g'), 59, 'Overwrite', false); %! t(string ('f')) = []; %! t = insert (t, string ('c'), 86); %! t = remove (t, string ('h')); %! t(string ('g')) = []; %! t = insert (t, string ('g'), 19); %! t(string ({'e', 'h'})) = [80, 81]; %! t = insert (t, string ('d'), 85); %! t = insert (t, string ('d'), 25, 'Overwrite', false); %! t(string ('e')) = 84; %! t = insert (t, string ('c'), 87); %! t(string ('f')) = 53; %! t(string ({'e', 'b'})) = [53, 54]; %! t(string ('e')) = 15; %! t(string ({'h', 'c'})) = [70, 71]; %! t(string ('g')) = 78; %! t(string ('b')) = 8; %! t(string ('h')) = 29; %! t = insert (t, string ('a'), 28); %! t = insert (t, string ('e'), 18); %! t(string ('a')) = 73; %! t(string ('d')) = []; %! t(string ('f')) = 99; %! t(string ({'c', 'b'})) = [10, 11]; %! t = insert (t, string ('g'), 45, 'Overwrite', false); %! t(string ({'g', 'f'})) = [10, 11]; %! t(string ({'d', 'g'})) = [63, 64]; %! t = insert (t, string ('h'), 11, 'Overwrite', false); %! t(string ('a')) = 28; %! t(string ('a')) = 65; %! t(string ('f')) = 2; %! t(string ('c')) = 75; %! t = insert (t, string ('d'), 51, 'Overwrite', false); %! t(string ('c')) = 90; %! t = insert (t, string ('e'), 67); %! t(string ({'b', 'f'})) = [58, 59]; %! r = lookup (t, string ({'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}), ... %! 'FallbackValue', 0); %! assert_equal (r, [65, 58, 90, 63, 67, 59, 64, 29]); pr0m1th3as-datatypes-9450b29/inst/tests/duration.m-tst000066400000000000000000007340341527370255000226430ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ################################################################################ ## ** Create and convert 'duration' type ** ## ################################################################################ ## Available Methods ## ## ## ## 'duration' 'dispstrings' 'cellstr' 'char' ## ## 'datevec' 'datenum' 'hms' 'years' ## ## 'days' ## ## 'hours' 'minutes' 'seconds' 'milliseconds' ## ## ## ################################################################################ %!assert_equal (isscalar (duration ()), true) %!assert_equal (isempty (duration ()), false) %!assert_equal (class (duration), 'duration') %!assert_equal (dispstrings (duration ()), {'00:00:00'}) %!assert_equal (size (duration (zeros (4, 3))), [4, 1]) ## size with no dimension named folds the trailing dimensions into the last ## output when fewer outputs are asked for than the array has dimensions, and ## pads with ones when more are asked for, as core Octave and MATLAB both do. %!test %! A = duration (ones (2, 2, 2), 0, 0); %! [r, c] = size (A); %! assert_equal ([r, c], [2, 4]); %!test %! A = duration (ones (2, 2), 0, 0); %! [a, b, c, d] = size (A); %! assert_equal ([a, b, c, d], [2, 2, 1, 1]); %!assert_equal (isempty (duration (ones (0, 3))), true) %!assert_equal (seconds (duration ([0, 0, 15])), 15) %!assert_equal (minutes (duration ([0, 50, 30])), 50.5) %!assert_equal (hours (duration ([1, 0, 3600; 5, 60, 0])), [2; 6]) %!assert_equal (seconds (duration ('30:15', 'InputFormat', 'mm:ss')), 1815) %!assert_equal (hours (duration ({'01:00', '02:30'}, 'InputFormat', ... %! 'hh:mm')), [1, 2.5]) %!assert_equal (minutes (duration ({'01:00:30'}, 'InputFormat', ... %! 'hh:mm:ss')), 60.5) %!assert_equal (minutes (duration ({'01:00:45'}, 'InputFormat', ... %! 'hh:mm:ss')), 60.75) %!assert_equal (days (duration ({'24:00:00'}, 'InputFormat', 'hh:mm:ss')), 1) %!assert_equal (days (duration ({'1:12:00:00'}, 'InputFormat', ... %! 'dd:hh:mm:ss')), 1.5) %!assert_equal (seconds (duration (0, 0, 15)), 15) %!assert_equal (seconds (duration (0, zeros (4), 15)), ones (4) * 15) %!assert_equal (minutes (duration (0, 50, 30)), 50.5) %!assert_equal (minutes (duration (0, 40:50, 30)), [40:50] + 0.5) %!assert_equal (seconds (duration (0, 0, 0, 15)), 0.015) %!assert_equal (size (duration (ones (2, 4, 3), 0, 0)), [2, 4, 3]) %!assert_equal (size (duration (0, ones (2, 3, 4), 0)), [2, 3, 4]) %!assert_equal (size (duration (zeros (4), zeros (4), 0, 0)), [4, 4]) %!assert_equal (size (duration (zeros (4), 0, 0, zeros (4))), [4, 4]) %!error ... %! duration (0, 0, 0, 'Format', 5) %!error ... %! duration (0, 0, 0, 'Format', {'mm:ss'}) %!error ... %! duration (0, 0, 0, 'Format', string ({'mm:ss', 'hh:mm:ss'})) %!error duration (0, 0, 0, 'Format', 't') %!error duration (0, 0, 0, 'Format', 'd.h.S') %!error duration (0, 0, 0, 'Format', 's.SS') %!error ... %! duration (0, 0, 0, 'Format', 'hh:mm.SS') %!error ... %! duration (0, 0, 0, 'Format', 'hh:mm.SM') %!error ... %! duration (0, 0, 0, 'Format', 'hh:mm:ss.SSSSSSSSSS') %!error ... %! duration (0, 0, 0, 'InputFormat', {'hh:mm'}) %!error ... %! duration (0, 0, 0, 'InputFormat', string ({'hh:mm', 'hh:mm'})) %!error duration (0, 0, 0, 'InputFormat', 'hh:mmm') %!error duration (0, 0, 0, 'InputFormat', string ('hh:mmm')) %!error ... %! duration (0, 0, 0, 'InputFormat', 'hh:mm.SSS') %!error ... %! duration (0, 0, 0, 'InputFormat', string ('hh:mm:ss.SSM')) %!error ... %! duration (0, 0, 0, 'InputFormat', string ('hh:mm:ss.SSSSSSSSSS')) %!error ... %! duration (0, 0, 0, 'InputFormat', 'hh:mm:ss.SSSSSSSSSS') %!error duration (ones (2, 2, 2)) %!error duration (ones (2, 3) * i) %!error duration (ones (2, 4)) %!error duration ({1}) %!error duration (struct ('a', 1)) %!error duration ('a', 0, 0) %!error duration (0, 'a', 0) %!error duration (0, 0, 'a') %!error duration (i, 0, 0) %!error duration (0, i, 0) %!error duration (0, 0, i) %!error ... %! duration (0, [0, 0], [0; 0]) %!error duration ('a', 0, 0, 0) %!error duration (0, 'a', 0, 0) %!error duration (0, 0, 'a', 0) %!error duration (0, 0, 0, 'a') %!error duration (i, 0, 0, 0) %!error duration (0, i, 0, 0) %!error duration (0, 0, i, 0) %!error duration (0, 0, 0, i) %!error ... %! duration (0, [0, 0], [0; 0], 0) %!error duration (1, 2, 3, 4, 5) %!warning ... %! duration ([0, 0, 0], 'InputFormat', 'hh:mm'); %!warning ... %! duration (0, 0, 0, 'InputFormat', 'hh:mm'); %!warning ... %! duration (0, 0, 0, 0, 'InputFormat', 'hh:mm'); %!assert_equal (dispstrings (years (1)), {'1 yr'}) %!assert_equal (dispstrings (years (2)), {'2 yrs'}) %!assert_equal (dispstrings (years ([1, 2, 2.4])), {'1 yr', '2 yrs', '2.4 yrs'}) %!assert_equal (dispstrings (years ([1; 2; 2.4])), {'1 yr'; '2 yrs'; '2.4 yrs'}) %!assert_equal (dispstrings (days (1)), {'1 day'}) %!assert_equal (dispstrings (days (2)), {'2 days'}) %!assert_equal (dispstrings (days ([1, 2, 2.4])), {'1 day', '2 days', ... %! '2.4 days'}) %!assert_equal (dispstrings (days ([1; 2; 2.4])), ... %! {'1 day'; '2 days'; '2.4 days'}) %!assert_equal (dispstrings (duration (36, 0, 0)), {'36:00:00'}) %!assert_equal (dispstrings (duration (-36, 0, 0)), {'-36:00:00'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', ... %! 'y')), {'0.00410686 yrs'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', 'd')), {'1.5 days'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', 'h')), {'36 hr'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', 'm')), {'2160 min'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', ... %! 's')), {'129600 sec'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', ... %! 'dd:hh:mm:ss.SSSSS')), {'01:12:00:00.00000'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', ... %! 'hh:mm:ss.SSSSS')), {'36:00:00.00000'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', ... %! 'hh:mm:ss.SSSS')), {'36:00:00.0000'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', ... %! 'hh:mm:ss.SSS')), {'36:00:00.000'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', ... %! 'hh:mm:ss.SS')), {'36:00:00.00'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', ... %! 'hh:mm:ss.S')), {'36:00:00.0'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', 'hh:mm')), {'36:00'}) %!assert_equal (dispstrings (duration (36, 0, 0.025, 'Format', ... %! 'hh:mm')), {'36:00'}) %!assert_equal (dispstrings (duration (36, 0, 55.025, 'Format', ... %! 'hh:mm')), {'36:00'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', ... %! 'mm:ss.SSSSS')), {'2160:00.00000'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', ... %! 'mm:ss.SSSSSS')), {'2160:00.000000'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', ... %! 'mm:ss.SSSSSSS')), {'2160:00.0000000'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', ... %! 'mm:ss.SSSSSSSS')), {'2160:00.00000000'}) %!assert_equal (dispstrings (duration (36, 0, 0, 'Format', ... %! 'mm:ss.SSSSSSSSS')), {'2160:00.000000000'}) %!assert_equal (dispstrings (duration (0, 36, 0)), {'00:36:00'}) %!assert_equal (dispstrings (duration (0, 36, 0, 'Format', ... %! 'y')), {'6.84477e-05 yrs'}) %!assert_equal (dispstrings (duration (0, 36, 0, 'Format', ... %! 'd')), {'0.025 days'}) %!assert_equal (dispstrings (duration (0, 36, 0, 'Format', 'h')), {'0.6 hr'}) %!assert_equal (dispstrings (duration (0, 36, 0, 'Format', 'm')), {'36 min'}) %!assert_equal (dispstrings (duration (0, 36, 0, 'Format', 's')), {'2160 sec'}) %!assert_equal (dispstrings (duration (0, 36, 0.025, 'Format', ... %! 'dd:hh:mm:ss.SSSSS')), {'00:36:00.02500'}) %!assert_equal (dispstrings (duration (0, 36, 0.025, 'Format', ... %! 'hh:mm:ss.SSSSS')), {'00:36:00.02500'}) %!assert_equal (dispstrings (duration (0, 36, 0.025, 'Format', ... %! 'hh:mm:ss.SSSS')), {'00:36:00.0250'}) %!assert_equal (dispstrings (duration (0, 36, 0.025, 'Format', ... %! 'hh:mm:ss.SSS')), {'00:36:00.025'}) %!assert_equal (dispstrings (duration (0, 36, 0.025, 'Format', ... %! 'hh:mm:ss.SS')), {'00:36:00.02'}) %!assert_equal (dispstrings (duration (0, 36, 0.025, 'Format', ... %! 'hh:mm:ss.S')), {'00:36:00.0'}) %!assert_equal (dispstrings (duration (0, 36, 0.025, 'Format', ... %! 'hh:mm')), {'00:36'}) %!assert_equal (dispstrings (duration (0, 36, 5.025, 'Format', ... %! 'hh:mm')), {'00:36'}) %!assert_equal (dispstrings (duration (0, 36, 0.025, 'Format', ... %! 'mm:ss.SSSSS')), {'36:00.02500'}) %!assert_equal (dispstrings (duration (0, 36, 0.025, 'Format', ... %! 'mm:ss.SSSSSS')), {'36:00.025000'}) %!assert_equal (dispstrings (duration (0, 36, 0.025, 'Format', ... %! 'mm:ss.SSSSSSS')), {'36:00.0250000'}) %!assert_equal (dispstrings (duration (0, 36, 0.025, 'Format', ... %! 'mm:ss.SSSSSSSS')), {'36:00.02500000'}) %!assert_equal (dispstrings (duration (0, 36, 0.025, 'Format', ... %! 'mm:ss.SSSSSSSSS')), {'36:00.025000000'}) %!test %! D = duration ([1, 2.3, NaN, Inf, -Inf], 0, 0, 'Format', 'y'); %! assert_equal (dispstrings (D), {'0.000114079 yrs', '0.000262383 yrs', ... %! 'NaN yrs', 'Inf yrs', '-Inf yrs'}); %!test %! D = duration ([1, 2.3, NaN, Inf, -Inf], 0, 0, 'Format', 'd'); %! assert_equal (dispstrings (D), {'0.0416667 days', '0.0958333 days', ... %! 'NaN days', 'Inf days', '-Inf days'}); %!test %! D = duration ([1, 2.3, NaN, Inf, -Inf], 0, 0, 'Format', 'h'); %! assert_equal (dispstrings (D), {'1 hr', '2.3 hr', 'NaN hr', 'Inf hr', ... %! '-Inf hr'}); %!test %! D = duration ([1, 2.3, NaN, Inf, -Inf], 0, 0, 'Format', 'm'); %! assert_equal (dispstrings (D), {'60 min', '138 min', 'NaN min', ... %! 'Inf min', '-Inf min'}); %!test %! D = duration ([1, 2.3, NaN, Inf, -Inf], 0, 0, 'Format', 's'); %! assert_equal (dispstrings (D), {'3600 sec', '8280 sec', 'NaN sec', ... %! 'Inf sec', '-Inf sec'}); %!test %! D = duration ([1, 2.3, NaN, Inf, -Inf], 0, 0, 'Format', 'hh:mm'); %! assert_equal (dispstrings (D), {'01:00', '02:18', 'NaN', 'Inf', '-Inf'}); %!test %! D = duration ([1, 2.3, NaN, Inf, -Inf], 0, 0, 'Format', 'mm:ss'); %! assert_equal (dispstrings (D), {'60:00', '138:00', 'NaN', 'Inf', '-Inf'}); %!test %! D = duration ([1, 2.3, NaN, Inf, -Inf], 0, 0, 'Format', 'hh:mm:ss'); %! assert_equal (dispstrings (D), {'01:00:00', '02:18:00', 'NaN', 'Inf', ... %! '-Inf'}); %!test %! D = duration ([1, 2.3, NaN, Inf, -Inf], 0, 0, 'Format', 'hh:mm:ss.SS'); %! assert_equal (dispstrings (D), {'01:00:00.00', '02:18:00.00', 'NaN', ... %! 'Inf', '-Inf'}); %!test %! D = duration ([1, 2.3, NaN, Inf, -Inf], 0, 0, 'Format', 'dd:hh:mm:ss'); %! assert_equal (dispstrings (D), {'01:00:00', '02:18:00', 'NaN', 'Inf', ... %! '-Inf'}); %!test %! D = duration ([1, 2.3, NaN, Inf, -Inf], 0, 0, 'Format', 'dd:hh:mm:ss.SS'); %! assert_equal (dispstrings (D), {'01:00:00.00', '02:18:00.00', 'NaN', ... %! 'Inf', '-Inf'}); %!test %! D = duration ([24.001, 2.3001, NaN, Inf, -Inf], 0, 0, 'Format', ... %! 'dd:hh:mm:ss'); %! assert_equal (dispstrings (D), {'01:00:00:03', '02:18:00', 'NaN', 'Inf', ... %! '-Inf'}); %!test %! D = duration ([24.001, 2.3001, NaN, Inf, -Inf], 0, 0, 'Format', ... %! 'dd:hh:mm:ss.SSS'); %! assert_equal (dispstrings (D), {'01:00:00:03.600', '02:18:00.360', 'NaN', ... %! 'Inf', '-Inf'}); %!assert_equal (cellstr (years (1)), {'1 yr'}) %!assert_equal (cellstr (years (1), 'd'), {'365.243 days'}) %!assert_equal (cellstr (years (2)), {'2 yrs'}) %!assert_equal (cellstr (years (2), 'd'), {'730.485 days'}) %!assert_equal (cellstr (days (1)), {'1 day'}) %!assert_equal (cellstr (days (1), 'y'), {'0.00273791 yrs'}) %!assert_equal (cellstr (days (2), 'm'), {'2880 min'}) %!assert_equal (cellstr (duration (36, 0, 0)), {'36:00:00'}) %!assert_equal (cellstr (duration (36, 0, 0), 'y'), {'0.00410686 yrs'}) %!assert_equal (cellstr (duration (36, 0, 0), 'd'), {'1.5 days'}) %!assert_equal (cellstr (duration (36, 0, 0), 'h'), {'36 hr'}) %!assert_equal (cellstr (duration (36, 0, 0), 'm'), {'2160 min'}) %!assert_equal (cellstr (duration (36, 0, 0), 's'), {'129600 sec'}) %!assert_equal (cellstr (duration (36, 0, 0), ... %! 'dd:hh:mm:ss.SSSSS'), {'01:12:00:00.00000'}) %!assert_equal (cellstr (duration (36, 0, 0), ... %! 'hh:mm:ss.SSSSS'), {'36:00:00.00000'}) %!assert_equal (cellstr (duration (36, 0, 0), ... %! 'hh:mm:ss.SSSS'), {'36:00:00.0000'}) %!assert_equal (cellstr (duration (36, 0, 0), 'hh:mm:ss.SSS'), {'36:00:00.000'}) %!assert_equal (cellstr (duration (36, 0, 0), 'hh:mm:ss.SS'), {'36:00:00.00'}) %!assert_equal (cellstr (duration (36, 0, 0), 'hh:mm:ss.S'), {'36:00:00.0'}) %!assert_equal (cellstr (duration (36, 0, 0), 'hh:mm'), {'36:00'}) %!assert_equal (cellstr (duration (36, 0, 0.025), 'hh:mm'), {'36:00'}) %!assert_equal (cellstr (duration (36, 0, 55.025), 'hh:mm'), {'36:00'}) %!assert_equal (cellstr (duration (36, 0, 0), 'mm:ss.SSSSS'), {'2160:00.00000'}) %!assert_equal (cellstr (duration (36, 0, 0), ... %! 'mm:ss.SSSSSS'), {'2160:00.000000'}) %!assert_equal (cellstr (duration (36, 0, 0), ... %! 'mm:ss.SSSSSSS'), {'2160:00.0000000'}) %!assert_equal (cellstr (duration (36, 0, 0), ... %! 'mm:ss.SSSSSSSS'), {'2160:00.00000000'}) %!assert_equal (cellstr (duration (36, 0, 0), ... %! 'mm:ss.SSSSSSSSS'), {'2160:00.000000000'}) %!assert_equal (cellstr (duration (0, 36, 0)), {'00:36:00'}) %!assert_equal (cellstr (duration (0, 36, 0), 'y'), {'6.84477e-05 yrs'}) %!assert_equal (cellstr (duration (0, 36, 0), 'd'), {'0.025 days'}) %!assert_equal (cellstr (duration (0, 36, 0), 'h'), {'0.6 hr'}) %!assert_equal (cellstr (duration (0, 36, 0), 'm'), {'36 min'}) %!assert_equal (cellstr (duration (0, 36, 0), 's'), {'2160 sec'}) %!assert_equal (cellstr (duration (0, 36, 0.025), ... %! 'dd:hh:mm:ss.SSSSS'), {'00:36:00.02500'}) %!assert_equal (cellstr (duration (0, 36, 0.025), ... %! 'hh:mm:ss.SSSSS'), {'00:36:00.02500'}) %!assert_equal (cellstr (duration (0, 36, 0.025), ... %! 'hh:mm:ss.SSSS'), {'00:36:00.0250'}) %!assert_equal (cellstr (duration (0, 36, 0.025), ... %! 'hh:mm:ss.SSS'), {'00:36:00.025'}) %!assert_equal (cellstr (duration (0, 36, 0.025), ... %! 'hh:mm:ss.SS'), {'00:36:00.02'}) %!assert_equal (cellstr (duration (0, 36, 0.025), 'hh:mm:ss.S'), {'00:36:00.0'}) %!assert_equal (cellstr (duration (0, 36, 0.025), 'hh:mm'), {'00:36'}) %!assert_equal (cellstr (duration (0, 36, 5.025), 'hh:mm'), {'00:36'}) %!assert_equal (cellstr (duration (0, 36, 0.025), ... %! 'mm:ss.SSSSS'), {'36:00.02500'}) %!assert_equal (cellstr (duration (0, 36, 0.025), ... %! 'mm:ss.SSSSSS'), {'36:00.025000'}) %!assert_equal (cellstr (duration (0, 36, 0.025), ... %! 'mm:ss.SSSSSSS'), {'36:00.0250000'}) %!assert_equal (cellstr (duration (0, 36, 0.025), ... %! 'mm:ss.SSSSSSSS'), {'36:00.02500000'}) %!assert_equal (cellstr (duration (0, 36, 0.025), ... %! 'mm:ss.SSSSSSSSS'), {'36:00.025000000'}) %!error ... %! cellstr (days (2), string ({'m', 'y'})) %!error ... %! cellstr (days (2), ['m'; 'y']) %!error ... %! cellstr (days (2), {'m'}) %!error ... %! cellstr (days (2), 2) %!error cellstr (duration (0, 0, 0), 't') %!error cellstr (duration (0, 0, 0), 'd.h.S') %!error cellstr (duration (0, 0, 0), 's.SS') %!error ... %! cellstr (duration (0, 0, 0), 'hh:mm.SS') %!error ... %! cellstr (duration (0, 0, 0), 'hh:mm.SM') %!error ... %! cellstr (duration (0, 0, 0), 'hh:mm:ss.SSSSSSSSSS') %!assert_equal (char (years (1)), '1 yr') %!assert_equal (char (years ([1, 2])), [' 1 yr'; '2 yrs']) %!assert_equal (char (days ([1, 20])), [' 1 day'; '20 days']) %!assert_equal (char (years (1), 'd'), '365.243 days') %!assert_equal (char (years (2)), '2 yrs') %!assert_equal (char (years (2), 'd'), '730.485 days') %!assert_equal (char (days (1)), '1 day') %!assert_equal (char (days ([1; 2])), [' 1 day'; '2 days']) %!assert_equal (char (days (1), 'y'), '0.00273791 yrs') %!assert_equal (char (days (2), 'm'), '2880 min') %!assert_equal (char (duration (36, 0, 0)), '36:00:00') %!assert_equal (char (duration (36, 0, 0), 'y'), '0.00410686 yrs') %!assert_equal (char (duration (36, 0, 0), 'd'), '1.5 days') %!assert_equal (char (duration (36, 0, 0), 'h'), '36 hr') %!assert_equal (char (duration (36, 0, 0), 'm'), '2160 min') %!assert_equal (char (duration (36, 0, 0), 's'), '129600 sec') %!assert_equal (char (duration (36, 0, 0), ... %! 'dd:hh:mm:ss.SSSSS'), '01:12:00:00.00000') %!assert_equal (char (duration (36, 0, 0), 'hh:mm:ss.SSSSS'), '36:00:00.00000') %!assert_equal (char (duration (36, 0, 0), 'hh:mm:ss.SSSS'), '36:00:00.0000') %!assert_equal (char (duration (36, 0, 0), 'hh:mm:ss.SSS'), '36:00:00.000') %!assert_equal (char (duration (36, 0, 0), 'hh:mm:ss.SS'), '36:00:00.00') %!assert_equal (char (duration (36, 0, 0), 'hh:mm:ss.S'), '36:00:00.0') %!assert_equal (char (duration (36, 0, 0), 'hh:mm'), '36:00') %!assert_equal (char (duration (36, 0, 0.025), 'hh:mm'), '36:00') %!assert_equal (char (duration (36, 0, 55.025), 'hh:mm'), '36:00') %!assert_equal (char (duration (36, 0, 0), 'mm:ss.SSSSS'), '2160:00.00000') %!assert_equal (char (duration (36, 0, 0), 'mm:ss.SSSSSS'), '2160:00.000000') %!assert_equal (char (duration (36, 0, 0), 'mm:ss.SSSSSSS'), '2160:00.0000000') %!assert_equal (char (duration (36, 0, 0), ... %! 'mm:ss.SSSSSSSS'), '2160:00.00000000') %!assert_equal (char (duration (36, 0, 0), ... %! 'mm:ss.SSSSSSSSS'), '2160:00.000000000') %!assert_equal (char (duration (0, 36, 0)), '00:36:00') %!assert_equal (char (duration (0, 36, 0), 'y'), '6.84477e-05 yrs') %!assert_equal (char (duration (0, 36, 0), 'd'), '0.025 days') %!assert_equal (char (duration (0, 36, 0), 'h'), '0.6 hr') %!assert_equal (char (duration (0, 36, 0), 'm'), '36 min') %!assert_equal (char (duration (0, 36, 0), 's'), '2160 sec') %!assert_equal (char (duration (0, 36, 0.025), ... %! 'dd:hh:mm:ss.SSSSS'), '00:36:00.02500') %!assert_equal (char (duration (0, 36, 0.025), ... %! 'hh:mm:ss.SSSSS'), '00:36:00.02500') %!assert_equal (char (duration (0, 36, 0.025), ... %! 'hh:mm:ss.SSSS'), '00:36:00.0250') %!assert_equal (char (duration (0, 36, 0.025), 'hh:mm:ss.SSS'), '00:36:00.025') %!assert_equal (char (duration (0, 36, 0.025), 'hh:mm:ss.SS'), '00:36:00.02') %!assert_equal (char (duration (0, 36, 0.025), 'hh:mm:ss.S'), '00:36:00.0') %!assert_equal (char (duration (0, 36, 0.025), 'hh:mm'), '00:36') %!assert_equal (char (duration (0, 36, 5.025), 'hh:mm'), '00:36') %!assert_equal (char (duration (0, 36, 0.025), 'mm:ss.SSSSS'), '36:00.02500') %!assert_equal (char (duration (0, 36, 0.025), 'mm:ss.SSSSSS'), '36:00.025000') %!assert_equal (char (duration (0, 36, 0.025), ... %! 'mm:ss.SSSSSSS'), '36:00.0250000') %!assert_equal (char (duration (0, 36, 0.025), ... %! 'mm:ss.SSSSSSSS'), '36:00.02500000') %!assert_equal (char (duration (0, 36, 0.025), ... %! 'mm:ss.SSSSSSSSS'), '36:00.025000000') %!error ... %! char (days (2), string ({'m', 'y'})) %!error ... %! char (days (2), ['m'; 'y']) %!error ... %! char (days (2), {'m'}) %!error ... %! char (days (2), 2) %!error char (duration (0, 0, 0), 't') %!error char (duration (0, 0, 0), 'd.h.S') %!error char (duration (0, 0, 0), 's.SS') %!error ... %! char (duration (0, 0, 0), 'hh:mm.SS') %!error ... %! char (duration (0, 0, 0), 'hh:mm.SM') %!error ... %! char (duration (0, 0, 0), 'hh:mm:ss.SSSSSSSSSS') %!test %! cmat = char (duration ([1; 100], 0, 0)); %! assert_equal (size (cmat), [2, 9]); %! assert_equal (cmat(1,:), ' 01:00:00'); %! assert_equal (cmat(2,:), '100:00:00'); %!test %! cmat = char (duration ([1; NaN], 0, 0)); %! assert_equal (size (cmat), [2, 8]); %! assert_equal (cmat(1,:), '01:00:00'); %! assert_equal (cmat(2,:), ' NaN'); %!test %! cmat = char (duration ([-1; 100], 0, 0)); %! assert_equal (cmat(1,:), '-01:00:00'); %! assert_equal (cmat(2,:), '100:00:00'); %!test %! cmat = char (duration ([], [], [])); %! assert_equal (size (cmat), [0, 0]); %!test %! [y, mo, d, h, mi, s] = datevec (years (1)); %! assert_equal (y, 1); %! assert_equal (mo, 0); %! assert_equal (d, 0); %! assert_equal (h, 0); %! assert_equal (mi, 0); %! assert_equal (s, 0); %!test %! [y, mo, d, h, mi, s] = datevec (years (1.0258123)); %! assert_equal (y, 1); %! assert_equal (mo, 0); %! assert_equal (d, 9); %! assert_equal (h, 10); %! assert_equal (mi, 15); %! assert_equal (s, 57.5121, 1E-4); %!test %! [y, mo, d, h, mi, s] = datevec (years (1.5)); %! assert_equal (y, 1); %! assert_equal (mo, 0); %! assert_equal (d, 182); %! assert_equal (h, 14); %! assert_equal (mi, 54); %! assert_equal (s, 36, 1e-8); %!test %! [y, mo, d, h, mi, s] = datevec (days (1)); %! assert_equal (y, 0); %! assert_equal (mo, 0); %! assert_equal (d, 1); %! assert_equal (h, 0); %! assert_equal (mi, 0); %! assert_equal (s, 0); %!test %! [y, mo, d, h, mi, s] = datevec (days (1.0258123)); %! assert_equal (y, 0); %! assert_equal (mo, 0); %! assert_equal (d, 1); %! assert_equal (h, 0); %! assert_equal (mi, 37); %! assert_equal (s, 10.1827, 1E-4); %!test %! [y, mo, d, h, mi, s] = datevec (days (1.5)); %! assert_equal (y, 0); %! assert_equal (mo, 0); %! assert_equal (d, 1); %! assert_equal (h, 12); %! assert_equal (mi, 0); %! assert_equal (s, 0, 1e-8); %!assert_equal (datevec (years (1)), [1, 0, 0, 0, 0, 0]) %!assert_equal (datevec (years (1.0258123)), [1, 0, 9, 10, 15, 57.5121], 1e-4) %!assert_equal (datevec (years (1.5)), [1, 0, 182, 14, 54, 36], 1e-8) %!assert_equal (datevec (days (1)), [0, 0, 1, 0, 0, 0]) %!assert_equal (datevec (days (1.0258123)), [0, 0, 1, 0, 37, 10.1827], 1e-4) %!assert_equal (datevec (days (1.5)), [0, 0, 1, 12, 0, 0]) %!test %! [y, mo, d, h, mi, s] = datevec (days (magic (3))); %! assert_equal (y, zeros (3)); %! assert_equal (mo, zeros (3)); %! assert_equal (d, magic (3)); %! assert_equal (h, zeros (3)); %! assert_equal (mi, zeros (3)); %! assert_equal (s, zeros (3)); %!test %! DV = datevec (days (magic (3))); %! out = zeros (9, 6); %! out(:,3) = magic (3)(:); %! assert_equal (DV, out); %!test %! [y, mo, d, h, mi, s] = datevec (days (ones (2, 3, 4))); %! assert_equal (y, zeros (2, 3, 4)); %! assert_equal (mo, zeros (2, 3, 4)); %! assert_equal (d, ones (2, 3, 4)); %! assert_equal (h, zeros (2, 3, 4)); %! assert_equal (mi, zeros (2, 3, 4)); %! assert_equal (s, zeros (2, 3, 4)); %!test %! DV = datevec (days (ones (2, 3, 4))); %! out = zeros (24, 6); %! out(:,3) = ones (24, 1); %! assert_equal (DV, out); %!test %! DV = datevec (years (reshape ([1:24], 2, 3, 4))); %! out = zeros (24, 6); %! out(:,1) = [1:24]'; %! assert_equal (DV, out); %!test %! DV = datevec (days (reshape ([1:24], 2, 3, 4))); %! out = zeros (24, 6); %! out(:,3) = [1:24]'; %! assert_equal (DV, out); %!test %! DV = datevec (hours (reshape ([1:24], 2, 3, 4))); %! out = zeros (24, 6); %! out(:,4) = [1:23,0]'; %! out(24,3) = 1; %! assert_equal (DV, out); %!test %! DV = datevec (minutes (reshape ([1:24], 2, 3, 4))); %! out = zeros (24, 6); %! out(:,5) = [1:24]'; %! assert_equal (DV, out); %!test %! DV = datevec (seconds (reshape ([1:24], 2, 3, 4))); %! out = zeros (24, 6); %! out(:,6) = [1:24]'; %! assert_equal (DV, out, 2e-15); %!test %! DV = datevec (milliseconds (reshape ([1:24], 2, 3, 4))); %! out = zeros (24, 6); %! out(:,6) = 1e-3 * [1:24]'; %! assert_equal (DV, out, 2e-15); %!test %! X = reshape ([1:24], 2, 3, 4); %! X([12,17]) = NaN; %! DV = datevec (days (X)); %! out = zeros (24, 6); %! out(:, 3) = [1:24]'; %! out([12,17],:) = NaN; %! assert_equal (DV, out); %!test %! X = reshape ([1:24], 2, 3, 4); %! X([12,17]) = NaN; %! [y, mo, d, h, mi, s] = datevec (days (X)); %! y_out = mo_out = h_out = mi_out = s_out = zeros (2, 3, 4); %! d_out = X; %! y_out([12,17]) = mo_out([12,17]) = NaN; %! h_out([12,17]) = mi_out([12,17]) = s_out([12,17]) = NaN; %! assert_equal (y, y_out); %! assert_equal (mo, mo_out); %! assert_equal (d, d_out); %! assert_equal (h, h_out); %! assert_equal (mi, mi_out); %! assert_equal (s, s_out); %!assert_equal (hms (hours ([1:5])), [1:5]) %!assert_equal (hms (days ([1:5])), 24 * [1:5]) %!assert_equal (hms (hours (reshape ([1:24], 2, 3, 4))), reshape ([1:24], 2, ... %! 3, 4)) %!test %! [h, m, s] = hms (days (0.25 * reshape ([1:24], 2, 3, 4))); %! assert_equal (h, 6 * reshape ([1:24], 2, 3, 4)); %! assert_equal (m, zeros (2, 3, 4)); %! assert_equal (s, zeros (2, 3, 4)); %!test %! [h, m, s] = hms (years (0.25 * reshape ([1:24], 2, 3, 4))); %! h_out = fix (hours (years (0.25 * reshape ([1:24], 2, 3, 4)))); %! assert_equal (h, h_out); %! m_out = fix (minutes (years (0.25 * reshape ([1:24], 2, 3, ... %! 4)))) - h_out * 60; %! assert_equal (m, m_out); %! s_out = round (seconds (years (0.25 * reshape ([1:24], 2, 3, 4)))); %! assert_equal (s, s_out - (h_out * 3600 + m_out * 60), 5e-8); %!test %! [h, m, s] = hms (hours ([1:5])); %! assert_equal (m, zeros (1, 5)); %! assert_equal (s, zeros (1, 5)); %!test %! [h, m, s] = hms (minutes ([1:5])); %! assert_equal (h, zeros (1, 5)); %! assert_equal (m, [1:5]); %! assert_equal (s, zeros (1, 5)); %!test %! [h, m, s] = hms (seconds ([1:5])); %! assert_equal (h, zeros (1, 5)); %! assert_equal (m, zeros (1, 5)); %! assert_equal (s, [1:5]); %!error [h, m, s, ms] = hms (days (1)) %!test %! [h, m, s] = hms (duration (-4, -5, -6.5)); %! assert_equal ([h, m, s], [-4, -5, -6.5]); %!test %! [h, m, s] = hms (duration (0, 0, -0.5)); %! assert_equal ([h, m, s], [0, 0, -0.5]); %!test %! [h, m, s] = hms (duration ([Inf, -Inf, NaN], 0, 0)); %! assert_equal (h, [Inf, -Inf, NaN]); %! assert_equal (m, [Inf, -Inf, NaN]); %! assert_equal (s, [Inf, -Inf, NaN]); %!test %! [h, m, s] = hms (duration (0, 0, -3599.9999999999995)); %! assert_equal ([h, m], [0, -59]); %! assert_equal (s, -59.999999999999545, 1e-12); %!test %! [h, m, s] = hms (duration (0, 0, 3599.9999999999995)); %! assert_equal ([h, m], [0, 59]); %! assert_equal (s, 59.999999999999545, 1e-12); %!test %! [h, m, s] = hms (duration (0, 0, -1e-13)); %! assert_equal ([h, m], [0, 0]); %! assert_equal (s, -1e-13); %!test %! [h, m, s] = hms (duration ([1, -1], 0, [0.5, -0.5])); %! assert_equal (h, [1, -1]); %! assert_equal (m, [0, 0]); %! assert_equal (s, [0.5, -0.5]); ################################################################################ ## ** Summary Information ** ## ################################################################################ ## Available Methods ## ## ## ## 'size' 'ndims' 'numel' 'nnz' ## ## 'length' 'keyHash' ## ## ## ################################################################################ %!assert_equal (size (duration), [1, 1]) %!assert_equal (size (days ([])), [0, 0]) %!assert_equal (size (days (ones (2, 0, 3, 4))), [2, 0, 3, 4]) %!assert_equal (size (duration (ones (2, 4), 0, 0)), [2, 4]) %!assert_equal (size (duration (ones (2, 4, 1, 1), 0, 0)), [2, 4]) %!assert_equal (size (duration (ones (2, 4, 1, 0), 0, 0)), [2, 4, 1, 0]) %!assert_equal (size (duration (zeros (6, 3))), [6, 1]) %!assert_equal (ndims (duration), 2) %!assert_equal (ndims (days ([])), 2) %!assert_equal (ndims (days (ones (2, 0, 3, 4))), 4) %!assert_equal (ndims (duration (ones (2, 4), 0, 0)), 2) %!assert_equal (ndims (duration (zeros (6, 3))), 2) %!assert_equal (numel (duration), 1) %!assert_equal (numel (days ([])), 0) %!assert_equal (numel (days (ones (2, 0, 3, 4))), 0) %!assert_equal (numel (duration (ones (2, 4), 0, 0)), 8) %!assert_equal (numel (duration (zeros (6, 3))), 6) %!assert_equal (nnz (days ([])), 0) %!assert_equal (nnz (duration), 0) %!assert_equal (nnz (days (ones (2, 0, 3, 4))), 0) %!assert_equal (nnz (days (zeros (2, 0, 3, 4))), 0) %!assert_equal (nnz (days (zeros (2, 1, 3, 4))), 0) %!assert_equal (nnz (days (ones (2, 1, 3, 4))), 24) %!assert_equal (nnz (days ([1, 4, 0, 9, 0, 8])), 4) %!assert_equal (nnz (days ([1, 4, 0; 9, 0, 8])), 4) %!assert_equal (length (duration (1, 1, 1)), 1) %!assert_equal (length (duration (0, 0, 0)), 1) %!assert_equal (length (duration (0:5, 0, 0)), 6) %!assert_equal (length (duration ([0, 0, 0, 1, 1], 0, 0)), 5) %!assert_equal (length (duration (ones (2, 3, 4), 0, 0)), 4) %!assert_equal (length (duration (ones (2, 10, 4), 0, 0)), 10) %!assert_equal (keyHash (duration), uint64 (8095976253982599603)) %!assert_equal (keyHash (duration (0, 0, 0)), uint64 (8095976253982599603)) %!assert_equal (keyHash (duration ([], [], [])), uint64 (14859204025010592219)) %!assert_equal (keyHash (duration (1, 1, 1)), uint64 (9684925284775797661)) %!test %! key = keyHash (duration ([1, 2; 3, 4], 1, 1)); %! assert_equal (key, uint64 (6302809889869477948)); %!test %! key = keyHash (duration (0, 0, 0, [1, 2, 3])); %! assert_equal (key, uint64 (11458397730511227888)); %!test %! base_key = uint64 (2342124352342344234); %! key = keyHash (duration (0, 0, 0, [1, 2, 3]), base_key); %! assert_equal (key, uint64 (14450751741045872781)); %!error ... %! keyHash (duration (0, 0, 0), uint64 ([1, 2])) %!error ... %! keyHash (duration (0, 0, 0), 2231107818551636405) ## A zero offset basis is a basis, not the absence of one, and a falsy base ## must still be validated rather than silently replaced by the default. %!test %! a = keyHash (seconds (1)); %! b = keyHash (seconds (1), uint64 (0)); %! assert_equal (isequal (a, b), false); %! assert_equal (keyHash (seconds (1), []), a); %!error ... %! keyHash (duration (0, 0, 0), 0) %!error ... %! keyHash (duration (0, 0, 0), uint64 ([0, 1])) ## The count of milliseconds is the key, so the unit a duration was written ## in is not part of it. These hash codes are unchanged from 1.3.0. %!test %! assert_equal (keyMatch (seconds (1), milliseconds (1000)), true); %! assert_equal (keyHash (seconds (1)), keyHash (milliseconds (1000))); %! assert_equal (keyMatch (minutes (1), seconds (60)), true); %! assert_equal (keyHash (minutes (1)), keyHash (seconds (60))); %! assert_equal (keyMatch (seconds (1), seconds (2)), false); ## A NaN duration is one key however the NaN arose. Storing raw bytes made ## seconds (NaN) and seconds (0/0) different keys, because 0/0 sets the sign ## bit of the NaN on x86-64 and the literal NaN does not. %!test %! assert_equal (keyMatch (seconds (NaN), seconds (0/0)), true); %! assert_equal (keyHash (seconds (NaN)), keyHash (seconds (0/0))); %! assert_equal (keyMatch (seconds (NaN), seconds (Inf - Inf)), true); %! assert_equal (keyHash (seconds (NaN)), keyHash (seconds (Inf - Inf))); %! assert_equal (keyMatch (duration (NaN, 0, 0), seconds (0/0)), true); %! assert_equal (keyMatch (seconds (NaN), seconds (1)), false); %! assert_equal (keyMatch (seconds (0), seconds (-0)), true); %! assert_equal (keyHash (seconds (0)), keyHash (seconds (-0))); ################################################################################ ## ** Query Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'isbetween' 'iscolumn' 'isempty' 'isequal' ## ## 'isequaln' 'isfinite' 'isinf' 'ismatrix' ## ## 'ismember' 'ismissing' 'isnan' 'isregular' ## ## 'isrow' 'isscalar' 'issorted' 'issortedrows' ## ## 'isvector' ## ## ## ################################################################################ %!test %! TF = isbetween (days ([0:5]), days (2), days (3)); %! assert_equal (TF, logical ([0, 0, 1, 1, 0, 0,])); %!test %! TF = isbetween (days ([0:5]), duration ('02:00:00:00'), days (3)); %! assert_equal (TF, logical ([0, 0, 1, 1, 0, 0,])); %!test %! TF = isbetween (days ([0:5]), duration ("1:00:00:00"), days (3)); %! assert_equal (TF, logical ([0, 1, 1, 1, 0, 0,])); %!test %! TF = isbetween (days ([0:5]), duration ("1:00:00:00"), days (4)); %! assert_equal (TF, logical ([0, 1, 1, 1, 1, 0,])); %!test %! TF = isbetween (days ([0:5]), days (2), days (3), 'open'); %! assert_equal (TF, false (1, 6)); %!test %! TF = isbetween (days ([0:5]), duration ('02:00:00:00'), days (3), 'open'); %! assert_equal (TF, false (1, 6)); %!test %! TF = isbetween (days ([0:5]), duration ('01:00:00:00'), days (3), 'open'); %! assert_equal (TF, logical ([0, 0, 1, 0, 0, 0,])); %!test %! TF = isbetween (days ([0:5]), duration ('01:00:00:00'), days (4), 'open'); %! assert_equal (TF, logical ([0, 0, 1, 1, 0, 0,])); %!test %! TF = isbetween (days ([0:5]), days (2), hours (72), 'open'); %! assert_equal (TF, false (1, 6)); %!test %! TF = isbetween (days ([0:5]), days (2), days (3), 'openleft'); %! assert_equal (TF, logical ([0, 0, 0, 1, 0, 0,])); %!test %! TF = isbetween (days ([0:5]), duration ('02:00:00:00'), days (3), ... %! 'openleft'); %! assert_equal (TF, logical ([0, 0, 0, 1, 0, 0,])); %!test %! TF = isbetween (days ([0:5]), duration ('01:00:00:00'), days (3), ... %! 'openleft'); %! assert_equal (TF, logical ([0, 0, 1, 1, 0, 0,])); %!test %! TF = isbetween (days ([0:5]), duration ('01:00:00:00'), days (4), ... %! 'openleft'); %! assert_equal (TF, logical ([0, 0, 1, 1, 1, 0,])); %!test %! TF = isbetween (days ([0:5]), days (2), hours (72), 'openleft'); %! assert_equal (TF, logical ([0, 0, 0, 1, 0, 0,])); %!test %! TF = isbetween (days ([0:5]), days (2), days (3), 'closedleft'); %! assert_equal (TF, logical ([0, 0, 1, 0, 0, 0,])); %!test %! TF = isbetween (days ([0:5]), duration ('02:00:00:00'), days (3), ... %! 'closedleft'); %! assert_equal (TF, logical ([0, 0, 1, 0, 0, 0,])); %!test %! TF = isbetween (days ([0:5]), duration ('01:00:00:00'), days (3), ... %! 'closedleft'); %! assert_equal (TF, logical ([0, 1, 1, 0, 0, 0,])); %!test %! TF = isbetween (days ([0:5]), duration ('01:00:00:00'), days (4), ... %! 'closedleft'); %! assert_equal (TF, logical ([0, 1, 1, 1, 0, 0,])); %!test %! TF = isbetween (days ([0:5]), days (2), hours (72), 'closedleft'); %! assert_equal (TF, logical ([0, 0, 1, 0, 0, 0,])); %!test %! TF = isbetween (days ([0:5]),'1:23:00:00', hours (72)); %! assert_equal (TF, logical ([0, 0, 1, 1, 0, 0,])); %!assert_equal (isbetween (years (1), years (1), years (1)), true) %!error ... %! isbetween (days (1), days (1)) %!error ... %! isbetween (days (1), days (1), days (1), 'open', 'OutputFormat', 'logical') %!error ... %! isbetween (days (1), 1, 2) %!error ... %! isbetween (days (1), days (1), days (1), 'asd') %!assert_equal (iscolumn (days ([1, 2, 3, 4])), false) %!assert_equal (iscolumn (days ([1; 2; 3; 4])), true) %!assert_equal (iscolumn (days ([1, 2; 3, 4])), false) %!assert_equal (iscolumn (days ([])), false) %!assert_equal (iscolumn (days (ones (1, 0))), false) %!assert_equal (iscolumn (days (ones (0, 1))), true) %!assert_equal (iscolumn (days (1)), true) %!assert_equal (iscolumn (days (ones (2, 3, 4))), false) %!assert_equal (isempty (days ([])), true) %!assert_equal (isempty (days (ones (2, 0, 3))), true) %!assert_equal (isempty (days (ones (2, 1, 3))), false) %!assert_equal (isempty (days (1)), false) %!assert_equal (isempty (duration), false) %!assert_equal (isequal (duration (24, 0, 0), days (1)), true) %!assert_equal (isequal (duration (24, 0, 0), '24:00:00'), true) %!assert_equal (isequal (duration (24, 0, 0), '24:00:01'), false) %!assert_equal (isequal (duration (24, 0, 1), {'24:00:01'}), true) %!assert_equal (isequal (duration (24, 0, 1), {'24:00:00'}), false) %!assert_equal (isequal (days ([1, 2]), string ({'24:00:00', ... %! '48:00:00'})), true) %!assert_equal (isequal (days ([1, 2]), ... %! string ({'24:00:00'; '48:00:00'})), false) %!assert_equal (isequal (days ([1; 2]), ... %! string ({'24:00:00'; '48:00:00'})), true) %!assert_equal (isequal (days ([1; 2]), string ({'24:00:00', ... %! '48:00:00'})), false) %!assert_equal (isequal (days (ones (2, 3, 4)), days (ones (2, 3, 4))), true) %!assert_equal (isequal (duration (ones (2, 3, 4) * 24, 0, 0), ... %! days (ones (2, 3, 4))), true) %!assert_equal (isequal (repmat ({'24:00:00'}, 2, 3, 4), days (ones (2, 3, ... %! 4))), true) %!assert_equal (isequal (repmat ({'24:00:05'}, 2, 3, 4), ... %! duration (24 * ones (2, 3, 4), 0, 5)), true) %!assert_equal (isequal (days (1), duration (24, 0, 0), '24:00:00'), true) %!assert_equal (isequal (days ([1, NaN]), duration ([24, NaN], 0, 0)), false) %!assert_equal (isequaln (duration (24, 0, 0), days (1)), true) %!assert_equal (isequaln (duration (24, 0, 0), '24:00:00'), true) %!assert_equal (isequaln (duration (24, 0, 0), '24:00:01'), false) %!assert_equal (isequaln (duration (24, 0, 1), {'24:00:01'}), true) %!assert_equal (isequaln (duration (24, 0, 1), {'24:00:00'}), false) %!assert_equal (isequaln (days ([1, 2]), string ({'24:00:00', ... %! '48:00:00'})), true) %!assert_equal (isequaln (days ([1, 2]), ... %! string ({'24:00:00'; '48:00:00'})), false) %!assert_equal (isequaln (days ([1; 2]), ... %! string ({'24:00:00'; '48:00:00'})), true) %!assert_equal (isequaln (days ([1; 2]), string ({'24:00:00', ... %! '48:00:00'})), false) %!assert_equal (isequaln (days (ones (2, 3, 4)), days (ones (2, 3, 4))), true) %!assert_equal (isequaln (duration (ones (2, 3, 4) * 24, 0, 0), ... %! days (ones (2, 3, 4))), true) %!assert_equal (isequaln (repmat ({'24:00:00'}, 2, 3, 4), days (ones (2, 3, ... %! 4))), true) %!assert_equal (isequaln (repmat ({'24:00:05'}, 2, 3, 4), ... %! duration (24 * ones (2, 3, 4), 0, 5)), true) %!assert_equal (isequaln (days (1), duration (24, 0, 0), '24:00:00'), true) %!assert_equal (isequaln (days ([1, NaN]), duration ([24, NaN], 0, 0)), true) %!assert_equal (isfinite (days ([1, -Inf, NaN, Inf])), [true, false, false, ... %! false]) %!assert_equal (isfinite (days ([1, -Inf; NaN, Inf])), [true, false; false, ... %! false]) %!assert_equal (isfinite (days (ones (0, 1))), true (0, 1)) %!assert_equal (isfinite (days (ones (1, 0))), true (1, 0)) %!assert_equal (isfinite (days ([])), true ([])) %!assert_equal (isfinite (duration (ones (2, 3, 4), 0, 0)), true (2, 3, 4)) %!assert_equal (isfinite (duration (Inf (2, 3, 4), 0, 0)), false (2, 3, 4)) %!assert_equal (isfinite (duration (-Inf (2, 3, 4), 0, 0)), false (2, 3, 4)) %!assert_equal (isfinite (duration (NaN (2, 3, 4), 0, 0)), false (2, 3, 4)) %!assert_equal (isinf (days ([1, -Inf, NaN, Inf])), [false, true, false, true]) %!assert_equal (isinf (days ([1, -Inf; NaN, Inf])), [false, true; false, true]) %!assert_equal (isinf (days (ones (0, 1))), true (0, 1)) %!assert_equal (isinf (days (ones (1, 0))), true (1, 0)) %!assert_equal (isinf (days ([])), true ([])) %!assert_equal (isinf (duration (ones (2, 3, 4), 0, 0)), false (2, 3, 4)) %!assert_equal (isinf (duration (Inf (2, 3, 4), 0, 0)), true (2, 3, 4)) %!assert_equal (isinf (duration (-Inf (2, 3, 4), 0, 0)), true (2, 3, 4)) %!assert_equal (isinf (duration (NaN (2, 3, 4), 0, 0)), false (2, 3, 4)) %!assert_equal (ismatrix (duration), true) %!assert_equal (ismatrix (duration ([], [], [])), true) %!assert_equal (ismatrix (duration (ones (2, 4), 0, 0)), true) %!assert_equal (ismatrix (duration (ones (2, 4, 1), 0, 0)), true) %!assert_equal (ismatrix (duration (ones (2, 1, 4), 0, 0)), false) %!assert_equal (ismatrix (duration (ones (2, 3, 4), 0, 0)), false) %!assert_equal (ismatrix (days (ones (0, 1))), true) %!assert_equal (ismatrix (days (ones (1, 0))), true) %!assert_equal (ismember (days (1), days ([1, 2])), true) %!assert_equal (ismember (days ([1, 2]), days (1)), [true, false]) %!assert_equal (ismember (days ([1; 2]), days (1)), [true; false]) %!assert_equal (ismember (days (1), days ([1; 2])), true) %!test %! [TF, index] = ismember (days (1), days (ones (2, 3, 4))); %! assert_equal (TF, true); %! assert_equal (index, 1); %!test %! [TF, index] = ismember (days (1), days (ones (2, 3, 4)), 'legacy'); %! assert_equal (TF, true); %! assert_equal (index, 24); %!test %! x = ones (2, 3, 4); %! x(1) = 2; %! [TF, index] = ismember (days (1), days (x)); %! assert_equal (TF, true); %! assert_equal (index, 2); # This is MATLAB incompatible %!test %! x = ones (2, 3, 4); %! x(1) = 2; %! [TF, index] = ismember (days (1), days (x), 'legacy'); %! assert_equal (TF, true); %! assert_equal (index, 24); %!test %! [TF, index] = ismember (days ([0:4]), days ([1, 2])); %! assert_equal (TF, [false, true, true, false, false]); %! assert_equal (index, [0, 1, 2, 0, 0]); %!test %! [TF, index] = ismember (days ([0:4]), days ([-1, 2])); %! assert_equal (TF, [false, false, true, false, false]); %! assert_equal (index, [0, 0, 2, 0, 0]); %!test %! [TF, index] = ismember (days ([0:4]), [-1, 2]); %! assert_equal (TF, [false, false, true, false, false]); %! assert_equal (index, [0, 0, 2, 0, 0]); %!test %! [TF, index] = ismember (days ([0:4]), {'24:00:00', '18:00:00'}); %! assert_equal (TF, [false, true, false, false, false]); %! assert_equal (index, [0, 1, 0, 0, 0]); %!test %! [TF, index] = ismember (days ([2,3;1,2;-1,2;4,5]), days ([-1, 2]), 'rows'); %! assert_equal (TF, [false; false; true; false]); %! assert_equal (index, [0; 0; 1; 0]); %!error ... %! ismember (days (1), days (ones (2, 3, 4)), 'rows') %!error ... %! ismember (days (1), days (ones (2, 3)), 'rows') %!error ... %! ismember (days (1), days (1), 3) %!error ... %! ismember (days (1), days (1), 'row') %!assert_equal (ismissing (days ([1, 2, 3, NaN, 5])), logical ([0, 0, 0, 1, 0])) %!assert_equal (ismissing (days ([1, 2, 3, NaN, 5]), days (1)), logical ([1, ... %! 0, 0, 0, 0])) %!assert_equal (ismissing (days ([1, 2, 3, NaN, 5]), duration (24, 0, ... %! 0)), logical ([1, 0, 0, 0, 0])) %!assert_equal (ismissing (days ([1, 2, 3, NaN, 5]), ... %! duration ('48:00:00')), logical ([0, 1, 0, 0, 0])) %!assert_equal (ismissing (seconds ([1:5]), duration (0, 0, ... %! [1:3])), logical ([1, 1, 1, 0, 0])) %!assert_equal (ismissing (days ([1, 2, 3, NaN, 5]), days ([1, ... %! 3])), logical ([1, 0, 1, 0, 0])) %!assert_equal (ismissing (days ([1, 2, 3, NaN, 5]), days ([1, ... %! 6])), logical ([1, 0, 0, 0, 0])) %!assert_equal (ismissing (days ([1, 2, 3, NaN, 5]), days ([1, 5, ... %! 6])), logical ([1, 0, 0, 0, 1])) %!assert_equal (ismissing (seconds ([1, NaN, 3]), seconds (NaN)), ... %! logical ([0, 1, 0])) %!assert_equal (ismissing (seconds ([1, NaN, 3]), seconds ([NaN, 3])), ... %! logical ([0, 1, 1])) %!assert_equal (ismissing (seconds ([1, NaN, 3]), missing), logical ([0, 1, 0])) %!assert_equal (ismissing (seconds ([1, NaN, 3]), [missing, seconds(3)]), ... %! logical ([0, 1, 1])) %!error ... %! ismissing (days (1), days (1), days (1)) %!error ... %! ismissing (seconds ([1, NaN]), NaN) %!error ... %! ismissing (days ([1, 2, 3, NaN, 5]), 5) %!error ... %! ismissing (days ([1, 2, 3, NaN, 5]), '24:00:00') %!error ... %! ismissing (days ([1, 2, 3, NaN, 5]), string ('24:00:00')) %!assert_equal (isnan (minutes ([1:10])), false (1, 10)) %!assert_equal (isnan (days ([1, 2, NaN, 4, NaN])), logical ([0, 0, 1, 0, 1])) %!assert_equal (isnan (duration (NaN (3, 4, 5), 0, 0)), true (3, 4, 5)) %!assert_equal (isnan (duration (zeros (3, 4, 5), 0, 0)), false (3, 4, 5)) %!assert_equal (isrow (days ([1, 2, 3, 4])), true) %!assert_equal (isrow (days ([1; 2; 3; 4])), false) %!assert_equal (isrow (days ([1, 2; 3, 4])), false) %!assert_equal (isrow (days ([])), false) %!assert_equal (isrow (days (ones (1, 0))), true) %!assert_equal (isrow (days (ones (0, 1))), false) %!assert_equal (isrow (days (1)), true) %!assert_equal (isrow (days (ones (2, 3, 4))), false) %!assert_equal (isscalar (days ([])), false) %!assert_equal (isscalar (days (ones (1, 0))), false) %!assert_equal (isscalar (days (ones (0, 1))), false) %!assert_equal (isscalar (days (1)), true) %!assert_equal (isscalar (days (NaN)), true) %!assert_equal (isscalar (days ([1, 2])), false) %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issorted (sort (D)), true); %! assert_equal (issorted (sort (D), 'ascend'), true); %! assert_equal (issorted (sort (D), 'descend'), false); %! assert_equal (issorted (sort (D), 'monotonic'), true); %! assert_equal (issorted (sort (D), 'strictascend'), false); %! assert_equal (issorted (sort (D), 'strictdescend'), false); %! assert_equal (issorted (sort (D), 'strictmonotonic'), false); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issorted (sort (D, 2), 2), true); %! assert_equal (issorted (sort (D, 2), 2, 'ascend'), true); %! assert_equal (issorted (sort (D, 2), 2, 'descend'), false); %! assert_equal (issorted (sort (D, 2), 2, 'monotonic'), true); %! assert_equal (issorted (sort (D, 2), 2, 'strictascend'), true); %! assert_equal (issorted (sort (D, 2), 2, 'strictdescend'), false); %! assert_equal (issorted (sort (D, 2), 2, 'strictmonotonic'), true); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issorted (sort (D, 2), 'ascend'), false); %! assert_equal (issorted (sort (D, 2), 'descend'), false); %! assert_equal (issorted (sort (D, 2), 'monotonic'), false); %! assert_equal (issorted (sort (D, 2), 'strictascend'), false); %! assert_equal (issorted (sort (D, 2), 'strictdescend'), false); %! assert_equal (issorted (sort (D, 2), 'strictmonotonic'), false); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issorted (sort (D, 'descend'), 'ascend'), false); %! assert_equal (issorted (sort (D, 'descend'), 'descend'), true); %! assert_equal (issorted (sort (D, 'descend'), 'monotonic'), true); %! assert_equal (issorted (sort (D, 'descend'), 'strictascend'), false); %! assert_equal (issorted (sort (D, 'descend'), 'strictdescend'), false); %! assert_equal (issorted (sort (D, 'descend'), 'strictmonotonic'), false); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issorted (sort (D, 2, 'descend'), 2, 'ascend'), false); %! assert_equal (issorted (sort (D, 2, 'descend'), 2, 'descend'), true); %! assert_equal (issorted (sort (D, 2, 'descend'), 2, 'monotonic'), true); %! assert_equal (issorted (sort (D, 2, 'descend'), 2, 'strictascend'), false); %! assert_equal (issorted (sort (D, 2, 'descend'), 2, 'strictdescend'), true); %! assert_equal (issorted (sort (D, 2, 'descend'), 2, 'strictmonotonic'), true); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issorted (sort (D, 2, 'descend'), 'ascend'), false); %! assert_equal (issorted (sort (D, 2, 'descend'), 'descend'), false); %! assert_equal (issorted (sort (D, 2, 'descend'), 'monotonic'), false); %! assert_equal (issorted (sort (D, 2, 'descend'), 'strictascend'), false); %! assert_equal (issorted (sort (D, 2, 'descend'), 'strictdescend'), false); %! assert_equal (issorted (sort (D, 2, 'descend'), 'strictmonotonic'), false); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issorted (sort (D, 'ComparisonMethod', 'real')), true); %! assert_equal (issorted (sort (D, 'ComparisonMethod', 'abs')), false); %! assert_equal (issorted (sort (D, 'ComparisonMethod', 'abs'), ... %! 'ComparisonMethod', 'abs'), true); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issorted (sort (D, 2, 'ComparisonMethod', 'real'), 2), true); %! assert_equal (issorted (sort (D, 2, 'ComparisonMethod', 'abs'), 2), false); %! assert_equal (issorted (sort (D, 2, 'ComparisonMethod', 'abs'), 2, ... %! 'ComparisonMethod', 'abs'), true); %!test %! D = days ([1, 2; 2, 3; 3, 4; 4, 5]); %! assert_equal (issorted (sort (D), 'ascend'), true); %! assert_equal (issorted (sort (D), 'descend'), false); %! assert_equal (issorted (sort (D), 'monotonic'), true); %! assert_equal (issorted (sort (D), 'strictascend'), true); %! assert_equal (issorted (sort (D), 'strictdescend'), false); %! assert_equal (issorted (sort (D), 'strictmonotonic'), true); %!test %! D = days ([1, 2; 2, 3; 3, 4; 4, 5]); %! assert_equal (issorted (sort (D, 'descend'), 'ascend'), false); %! assert_equal (issorted (sort (D, 'descend'), 'descend'), true); %! assert_equal (issorted (sort (D, 'descend'), 'monotonic'), true); %! assert_equal (issorted (sort (D, 'descend'), 'strictascend'), false); %! assert_equal (issorted (sort (D, 'descend'), 'strictdescend'), true); %! assert_equal (issorted (sort (D, 'descend'), 'strictmonotonic'), true); %!test %! D = days ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (D)), true); %! assert_equal (issorted (sort (D), 'ascend'), true); %! assert_equal (issorted (sort (D), 'descend'), false); %! assert_equal (issorted (sort (D), 'monotonic'), true); %! assert_equal (issorted (sort (D), 'strictascend'), false); %! assert_equal (issorted (sort (D), 'strictdescend'), false); %! assert_equal (issorted (sort (D), 'strictmonotonic'), false); %!test %! D = days ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (D, 'descend'), 'ascend'), false); %! assert_equal (issorted (sort (D, 'descend'), 'descend'), true); %! assert_equal (issorted (sort (D, 'descend'), 'monotonic'), true); %! assert_equal (issorted (sort (D, 'descend'), 'strictascend'), false); %! assert_equal (issorted (sort (D, 'descend'), 'strictdescend'), false); %! assert_equal (issorted (sort (D, 'descend'), 'strictmonotonic'), false); %!test %! D = days ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (D, 'MissingPlacement', 'auto')), true); %! assert_equal (issorted (sort (D, 'MissingPlacement', 'first')), false); %! assert_equal (issorted (sort (D, 'MissingPlacement', 'first'), ... %! 'MissingPlacement', 'first'), true); %!test %! D = days ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (D, 'descend', 'MissingPlacement', 'auto'), ... %! 'descend'), true); %! assert_equal (issorted (sort (D, 'descend', 'MissingPlacement', 'last'), ... %! 'descend'), false); %! assert_equal (issorted (sort (D, 'descend', 'MissingPlacement', 'first'), ... %! 'descend', 'MissingPlacement', 'first'), true); %!test %! D = days ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (D, 2, 'MissingPlacement', 'auto'), 2), true); %! assert_equal (issorted (sort (D, 2, 'MissingPlacement', 'first'), 2), false); %! assert_equal (issorted (sort (D, 2, 'MissingPlacement', 'first'), 2, ... %! 'MissingPlacement', 'first'), true); %!test %! D = days ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (D, 2, 'descend', 'MissingPlacement', ... %! 'auto'), 2, 'descend'), true); %! assert_equal (issorted (sort (D, 2, 'descend', 'MissingPlacement', ... %! 'last'), 2, 'descend'), false); %! assert_equal (issorted (sort (D, 2, 'descend', 'MissingPlacement', ... %! 'first'), 2, ... %! 'descend', 'MissingPlacement', 'first'), true); %!test %! D = days ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (D, 2, string ('ascend'), ... %! 'MissingPlacement', 'auto'), 2), true); %! assert_equal (issorted (sort (D, 2, string ('ascend'), ... %! 'MissingPlacement', 'first'), 2), false); %! assert_equal (issorted (sort (D, 2, string ('ascend'), ... %! 'MissingPlacement', 'first'), 2, ... %! 'MissingPlacement', 'first'), true); %!test %! D = days ([1, 2; 2, 3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (D, 2, string ('descend'), ... %! 'MissingPlacement', 'auto'), 2, 'descend'), true); %! assert_equal (issorted (sort (D, 2, string ('descend'), ... %! 'MissingPlacement', 'last'), 2, 'descend'), false); %! assert_equal (issorted (sort (D, 2, string ('descend'), ... %! 'MissingPlacement', 'first'), 2, ... %! 'descend', 'MissingPlacement', 'first'), true); %!test %! D = days ([1, 2; 2, -3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (D, 'MissingPlacement', 'auto', ... %! 'ComparisonMethod', 'real')), true); %! assert_equal (issorted (sort (D, 'MissingPlacement', 'auto', ... %! 'ComparisonMethod', 'abs')), false); %! assert_equal (issorted (sort (D, 'MissingPlacement', 'auto', ... %! 'ComparisonMethod', 'abs'), 'ComparisonMethod', 'abs'), true); %! assert_equal (issorted (sort (D, 'MissingPlacement', 'last', ... %! 'ComparisonMethod', 'abs')), false); %! assert_equal (issorted (sort (D, 'MissingPlacement', 'last', ... %! 'ComparisonMethod', 'abs'), 'ComparisonMethod', 'abs'), true); %! assert_equal (issorted (sort (D, 'MissingPlacement', 'first', ... %! 'ComparisonMethod', 'abs'), 'MissingPlacement', 'first', ... %! 'ComparisonMethod', 'abs'), true); %!test %! D = days ([1, 2; 2, -3; 3, 4; 4, NaN]); %! assert_equal (issorted (sort (D, 'descend', 'MissingPlacement', 'auto', ... %! 'ComparisonMethod', 'real'), 'descend'), true); %! assert_equal (issorted (sort (D, 'descend', 'MissingPlacement', 'auto', ... %! 'ComparisonMethod', 'abs'), 'descend'), false); %! assert_equal (issorted (sort (D, 'descend', 'MissingPlacement', 'auto', ... %! 'ComparisonMethod', 'abs'), ... %! 'descend', 'ComparisonMethod', 'abs'), true); %! assert_equal (issorted (sort (D, 'descend', 'MissingPlacement', 'last', ... %! 'ComparisonMethod', 'abs'), 'descend'), false); %! assert_equal (issorted (sort (D, 'descend', 'MissingPlacement', 'last', ... %! 'ComparisonMethod', 'abs'), ... %! 'descend', 'ComparisonMethod', 'abs'), false); %! assert_equal (issorted (sort (D, 'descend', 'MissingPlacement', 'first', ... %! 'ComparisonMethod', 'abs'), 'descend'), false); %! assert_equal (issorted (sort (D, 'descend', 'MissingPlacement', 'first', ... %! 'ComparisonMethod', 'abs'), ... %! 'descend', 'ComparisonMethod', 'abs'), true); %! assert_equal (issorted (sort (D, 'descend', 'MissingPlacement', 'first', ... %! 'ComparisonMethod', 'abs'), 'descend', 'MissingPlacement', 'first', ... %! 'ComparisonMethod', 'abs'), true); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issortedrows (sortrows (D)), true); %! assert_equal (issortedrows (sortrows (D, 1)), true); %! assert_equal (issortedrows (sortrows (D, 1), -1), false); %! assert_equal (issortedrows (sortrows (D, 1), 1), true); %! assert_equal (issortedrows (sortrows (D, 2)), false); %! assert_equal (issortedrows (sortrows (D, 2), 2), true); %! assert_equal (issortedrows (sortrows (D, 2), -2), false); %! assert_equal (issortedrows (sortrows (D, -2), 2), false); %! assert_equal (issortedrows (sortrows (D, -2), -2), true); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issortedrows (sortrows (D, 'ComparisonMethod', 'abs')), false); %! assert_equal (issortedrows (sortrows (D, 'ComparisonMethod', 'real')), true); %! assert_equal (issortedrows (sortrows (D, 'ComparisonMethod', 'abs'), ... %! 'ComparisonMethod', 'abs'), true); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issortedrows (sortrows (D, 2, 'ComparisonMethod', 'abs'), ... %! 2), false); %! assert_equal (issortedrows (sortrows (D, 2, 'ComparisonMethod', 'real'), ... %! 2), true); %! assert_equal (issortedrows (sortrows (D, 2, 'ComparisonMethod', 'abs'), ... %! 2, ... %! 'ComparisonMethod', 'abs'), true); %! assert_equal (issortedrows (sortrows (D, [1, 2], 'ComparisonMethod', ... %! 'abs'), ... %! {'ascend', 'ascend'}), false); %! assert_equal (issortedrows (sortrows (D, [1, 2], 'ComparisonMethod', ... %! 'abs'), ... %! {'ascend', ... %! 'ascend'}, 'ComparisonMethod', 'abs'), true); %! assert_equal (issortedrows (sortrows (D, 2, 'ComparisonMethod', 'real'), ... %! 2), true); %! assert_equal (issortedrows (sortrows (D, [1, 2], 'ComparisonMethod', ... %! 'real'), ... %! {'ascend', ... %! 'ascend'}, 'ComparisonMethod', 'auto'), true); %! assert_equal (issortedrows (sortrows (D, [-1, 2], 'ComparisonMethod', ... %! 'abs'), ... %! {'ascend', ... %! 'ascend'}, 'ComparisonMethod', 'abs'), false); %! assert_equal (issortedrows (sortrows (D, 2, 'ComparisonMethod', 'real'), ... %! -2), false); %! assert_equal (issortedrows (sortrows (D, [1, 2], 'ComparisonMethod', ... %! 'real'), ... %! {'ascend', ... %! 'descend'}, 'ComparisonMethod', 'auto'), false); %! assert_equal (issortedrows (sortrows (D, [1, -2], 'ComparisonMethod', ... %! 'real'), ... %! {'ascend', ... %! 'descend'}, 'ComparisonMethod', 'auto'), true); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! assert_equal (issortedrows (sortrows (D, [1, 2], 'ComparisonMethod', ... %! 'abs'), ... %! [1, 2], string ({'ascend', 'ascend'})), false); %! assert_equal (issortedrows (sortrows (D, [1, 2], 'ComparisonMethod', ... %! 'abs'), ... %! [1, 2], string ({'ascend', 'ascend'}), ... %! 'ComparisonMethod', 'abs'), true); %! assert_equal (issortedrows (sortrows (D, [1, 2], 'ComparisonMethod', ... %! 'abs'), ... %! [1, -2], string ({'ascend', 'ascend'}), ... %! 'ComparisonMethod', 'abs'), true); %! assert_equal (issortedrows (sortrows (D, [1, 2], 'ComparisonMethod', ... %! 'real'), ... %! [1, 2], string ({'ascend', 'ascend'}), ... %! 'ComparisonMethod', 'auto'), true); %! assert_equal (issortedrows (sortrows (D, [1, -2], string ({'ascend', ... %! 'ascend'}), ... %! 'ComparisonMethod', 'real'), ... %! [1, 2], string ({'ascend', 'ascend'}), ... %! 'ComparisonMethod', 'auto'), true); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! assert_equal (issortedrows (sortrows (D)), true); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! assert_equal (issortedrows (sortrows (D, -2, 'MissingPlacement', ... %! 'first')), false); %! assert_equal (issortedrows (sortrows (D, -2, 'MissingPlacement', ... %! 'first'), -2), true); %! assert_equal (issortedrows (sortrows (D, -2, 'MissingPlacement', 'last'), ... %! -2), false); %! assert_equal (issortedrows (sortrows (D, -2, 'MissingPlacement', 'last'), ... %! -2, ... %! 'MissingPlacement', 'last'), true); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! assert_equal (issortedrows (sortrows (D, 2, 'MissingPlacement', 'last')), ... %! false); %! assert_equal (issortedrows (sortrows (D, 2, 'MissingPlacement', 'last'), ... %! 2), true); %! assert_equal (issortedrows (sortrows (D, 2, 'MissingPlacement', 'first'), ... %! 2), false); %! assert_equal (issortedrows (sortrows (D, 2, 'MissingPlacement', 'first'), ... %! 2, ... %! 'MissingPlacement', 'first'), true); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! assert_equal (issortedrows (sortrows (D, 'ComparisonMethod', 'abs', ... %! 'MissingPlacement', 'first'), ... %! 'ComparisonMethod', 'abs', ... %! 'MissingPlacement', 'first'), true); %! assert_equal (issortedrows (sortrows (D, 'ComparisonMethod', 'abs', ... %! 'MissingPlacement', 'last'), ... %! 'ComparisonMethod', 'abs', ... %! 'MissingPlacement', 'first'), false); %! assert_equal (issortedrows (sortrows (D, 'ComparisonMethod', 'abs', ... %! 'MissingPlacement', 'last'), ... %! 'ComparisonMethod', 'abs', ... %! 'MissingPlacement', 'auto'), true); %! assert_equal (issortedrows (sortrows (D, 'ComparisonMethod', 'abs', ... %! 'MissingPlacement', 'last'), ... %! 'ComparisonMethod', 'abs'), true); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! assert_equal (issortedrows (sortrows (D, [1, -2, 3]), [1, -2, 3]), true); %! assert_equal (issortedrows (sortrows (D, [1, 2, -3]), [1, -2, 3]), false); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! assert_equal (issortedrows (sortrows (D, string ({'ascend', 'descend', ... %! 'ascend'})), ... %! string ({'ascend', 'descend', 'ascend'})), true); %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! assert_equal (issortedrows (sortrows (D, string ({'ascend', 'descend', ... %! 'ascend'})), ... %! string ({'descend', 'ascend', 'ascend'})), false); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! assert_equal (issortedrows (sortrows (D, [2, 3], string ({'descend', ... %! 'ascend'})), ... %! [2, 3], string ({'descend', 'ascend'})), true); %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! assert_equal (issortedrows (sortrows (D, [2, 3], string ({'ascend', ... %! 'descend'})), ... %! [2, -3]), true); %! assert_equal (issortedrows (sortrows (D, [2, 3], string ({'descend', ... %! 'descend'})), ... %! [2, -3], string ({'descend', 'ascend'})), false); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! assert_equal (issortedrows (sortrows (D, [1, -2, 3], 'MissingPlacement', ... %! 'first'), ... %! [1, -2, 3], 'MissingPlacement', 'first'), true); %! assert_equal (issortedrows (sortrows (D, [-1, -2, -3], ... %! 'MissingPlacement', 'first'), ... %! [-1, -2, -3]), true); %! assert_equal (issortedrows (sortrows (D, [-1, -2, -3], ... %! 'MissingPlacement', 'first'), ... %! [1, -2,-3], string ({'descend', 'descend', 'descend'})), true); %!test ## strict issortedrows semantics, verified against MATLAB R2026a %! assert_equal (issortedrows (days ([1, 3; 1, 4]), 'strictascend'), false); %! assert_equal (issortedrows (days ([1, 3; 1, 3]), 'strictascend'), false); %! assert_equal (issortedrows (days ([1, 3; 1, 4]), {'strictascend', ... %! 'ascend'}), false); %! assert_equal (issortedrows (days ([1, 3; 2, 3]), {'ascend', ... %! 'strictascend'}), true); %! assert_equal (issortedrows (days ([2, 4; 1, 3]), 'monotonic'), true); %! assert_equal (issortedrows (days ([2, 4; 1, 3]), 'descend'), true); %! assert_equal (issortedrows (days ([1, 4; 2, 3]), 2, 'strictascend'), false); %! assert_equal (issortedrows (days ([2, 3; 1, 4]), 2, 'strictascend'), true); %!test ## strict issortedrows with missing values (vs MATLAB R2026a) %! assert_equal (issortedrows (days ([1, 3; NaN, 4]), {'strictascend', ... %! 'ascend'}), false); %! assert_equal (issortedrows (days ([1, 3; NaN, 4]), {'ascend', ... %! 'strictascend'}), true); %! assert_equal (issortedrows (days ([NaN, 3; 2, 4]), 2, 'strictascend'), true); %! assert_equal (issortedrows (days ([1, NaN; 2, 4]), 'strictascend'), true); %!assert_equal (isvector (days ([1, 2, 3, 4])), true) %!assert_equal (isvector (days ([1; 2; 3; 4])), true) %!assert_equal (isvector (days ([1, 2; 3, 4])), false) %!assert_equal (isvector (days ([])), false) %!assert_equal (isvector (days (ones (1, 0))), true) %!assert_equal (isvector (days (ones (0, 1))), true) %!assert_equal (isvector (days (1)), true) %!assert_equal (isvector (days (ones (2, 3, 4))), false) ################################################################################ ## ** Relational Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'eq' 'ge' 'gt' 'le' ## ## 'lt' 'ne' ## ## ## ################################################################################ %!assert_equal (duration (1, 0, 0) == '01:00:00', true) %!assert_equal (duration (1, 0, 0) == '01:00:01', false) %!assert_equal (duration (1, 0, 0) == {'01:00:00'}, true) %!assert_equal (duration (1, 0, 1) == {'01:00:00'}, false) %!assert_equal (duration (1, 0, 0) == string ('01:00:00'), true) %!assert_equal (duration (NaN, 0, 0) == string ('01:00:00'), false) %!assert_equal (duration (24, 0, 0) == 1, true) %!assert_equal (duration (NaN, 0, 0) == NaN, false) %!assert_equal (duration (24, 0, 0) == days (1), true) %!assert_equal ('01:00:00' == duration (1, 0, 0), true) %!assert_equal ({'01:00:00'} == duration (1, 0, 0), true) %!assert_equal (string ('01:00:00') == duration (1, 0, 0), true) %!assert_equal (1 == duration (24, 0, 0), true) %!assert_equal (days (1) == duration (24, 0, 0), true) %!assert_equal (days (NaN) == duration (NaN, 0, 0), false) %!assert_equal (duration (1, 0, 0) == hours (1), true) %!assert_equal (duration (0, 1, 0) == minutes (1), true) %!assert_equal (duration (0, 0, 1) == seconds (1), true) %!assert_equal (days ([1, 2]) == 1, [true, false]) %!assert_equal (days ([1; 2]) == 1, [true; false]) %!assert_equal (days ([1, 2]) == [1, 2], [true, true]) %!assert_equal (days ([1, 2]) == [1; 2], logical (eye (2))) %!assert_equal (duration (1, 0, 0) >= '01:00:00', true) %!assert_equal (duration (1, 0, 0) >= '01:00:01', false) %!assert_equal (duration (1, 0, 0) >= {'01:00:00'}, true) %!assert_equal (duration (1, 0, 1) >= {'01:00:00'}, true) %!assert_equal (duration (1, 0, 0) >= string ('01:00:00'), true) %!assert_equal (duration (NaN, 0, 0) >= string ('01:00:00'), false) %!assert_equal (duration (24, 0, 0) >= 1, true) %!assert_equal (duration (NaN, 0, 0) >= NaN, false) %!assert_equal (duration (24, 0, 0) >= days (1), true) %!assert_equal ('01:00:00' >= duration (1, 0, 0), true) %!assert_equal ('01:00:00' >= duration (1, 0, 1), false) %!assert_equal ({'01:00:00'} >= duration (1, 0, 0), true) %!assert_equal (string ('01:00:00') >= duration (1, 0, 0), true) %!assert_equal (string ('01:00:01') >= duration (1, 0, 0), true) %!assert_equal (string ('01:00:00') >= duration (1, 1, 0), false) %!assert_equal (1 >= duration (24, 0, 0), true) %!assert_equal (days (1) >= duration (24, 0, 0), true) %!assert_equal (days (NaN) >= duration (NaN, 0, 0), false) %!assert_equal (duration (1, 0, 0) >= hours (1), true) %!assert_equal (duration (0, 1, 0) >= minutes (1), true) %!assert_equal (duration (0, 0, 1) >= seconds (1), true) %!assert_equal (days ([1, 2]) >= 1, [true, true]) %!assert_equal (days ([1; 2]) >= 1, [true; true]) %!assert_equal (days ([1, 2]) >= [1, 2], [true, true]) %!assert_equal (days ([1, 2]) >= [1; 2], [true, true; false, true]) %!assert_equal (duration (1, 0, 0) > '01:00:00', false) %!assert_equal (duration (1, 0, 0) > '01:00:01', false) %!assert_equal (duration (1, 0, 0) > {'01:00:00'}, false) %!assert_equal (duration (1, 0, 1) > {'01:00:00'}, true) %!assert_equal (duration (1, 0, 0) > string ('01:00:00'), false) %!assert_equal (duration (NaN, 0, 0) > string ('01:00:00'), false) %!assert_equal (duration (24, 0, 0) > 1, false) %!assert_equal (duration (NaN, 0, 0) > NaN, false) %!assert_equal (duration (24, 0, 0) > days (1), false) %!assert_equal (duration (24, 0, 1) > days (1), true) %!assert_equal ('01:00:00' > duration (1, 0, 0), false) %!assert_equal ('01:00:01' > duration (1, 0, 0), true) %!assert_equal ('01:00:00' > duration (1, 0, 1), false) %!assert_equal ({'01:00:00'} > duration (1, 0, 0), false) %!assert_equal (string ('01:00:00') > duration (1, 0, 0), false) %!assert_equal (string ('01:00:01') > duration (1, 0, 0), true) %!assert_equal (string ('01:00:00') > duration (1, 1, 0), false) %!assert_equal (1 > duration (24, 0, 0), false) %!assert_equal (days (1) > duration (24, 0, 0), false) %!assert_equal (days (1.05) > duration (24, 0, 0), true) %!assert_equal (days (NaN) > duration (NaN, 0, 0), false) %!assert_equal (duration (1, 0, 0) > hours (1), false) %!assert_equal (duration (0, 1, 0) > minutes (1), false) %!assert_equal (duration (0, 0, 1) > seconds (1), false) %!assert_equal (days ([1, 2]) > 1, [false, true]) %!assert_equal (days ([1; 2]) > 1, [false; true]) %!assert_equal (days ([1, 2]) > [1, 2], [false, false]) %!assert_equal (days ([1, 2]) > [1; 2], [false, true; false, false]) %!assert_equal (duration (1, 0, 0) <= '01:00:00', true) %!assert_equal (duration (1, 0, 0) <= '01:00:01', true) %!assert_equal (duration (1, 0, 0) <= {'01:00:00'}, true) %!assert_equal (duration (1, 0, 1) <= {'01:00:00'}, false) %!assert_equal (duration (1, 0, 0) <= string ('01:00:00'), true) %!assert_equal (duration (NaN, 0, 0) <= string ('01:00:00'), false) %!assert_equal (duration (24, 0, 0) <= 1, true) %!assert_equal (duration (NaN, 0, 0) <= NaN, false) %!assert_equal (duration (24, 0, 0) <= days (1), true) %!assert_equal ('01:00:00' <= duration (1, 0, 0), true) %!assert_equal ('01:00:00' <= duration (1, 0, 1), true) %!assert_equal ({'01:00:00'} <= duration (1, 0, 0), true) %!assert_equal (string ('01:00:00') <= duration (1, 0, 0), true) %!assert_equal (string ('01:00:01') <= duration (1, 0, 0), false) %!assert_equal (string ('01:00:00') <= duration (1, 1, 0), true) %!assert_equal (1 <= duration (24, 0, 0), true) %!assert_equal (days (1) <= duration (24, 0, 0), true) %!assert_equal (days (NaN) <= duration (NaN, 0, 0), false) %!assert_equal (duration (1, 0, 0) <= hours (1), true) %!assert_equal (duration (0, 1, 0) <= minutes (1), true) %!assert_equal (duration (0, 0, 1) <= seconds (1), true) %!assert_equal (days ([1, 2]) <= 1, [true, false]) %!assert_equal (days ([1; 2]) <= 1, [true; false]) %!assert_equal (days ([1, 2]) <= [1, 2], [true, true]) %!assert_equal (days ([1, 2]) <= [1; 2], [true, false; true, true]) %!assert_equal (duration (1, 0, 0) < '01:00:00', false) %!assert_equal (duration (1, 0, 0) < '01:00:01', true) %!assert_equal (duration (1, 0, 0) < {'01:00:00'}, false) %!assert_equal (duration (1, 0, 1) < {'01:00:00'}, false) %!assert_equal (duration (1, 0, 0) < string ('01:00:00'), false) %!assert_equal (duration (NaN, 0, 0) < string ('01:00:00'), false) %!assert_equal (duration (24, 0, 0) < 1, false) %!assert_equal (duration (NaN, 0, 0) < NaN, false) %!assert_equal (duration (24, 0, 0) < days (1), false) %!assert_equal (duration (24, 0, 1) < days (1), false) %!assert_equal ('01:00:00' < duration (1, 0, 0), false) %!assert_equal ('01:00:01' < duration (1, 0, 0), false) %!assert_equal ('01:00:00' < duration (1, 0, 1), true) %!assert_equal ({'01:00:00'} < duration (1, 0, 0), false) %!assert_equal (string ('01:00:00') < duration (1, 0, 0), false) %!assert_equal (string ('01:00:01') < duration (1, 0, 0), false) %!assert_equal (string ('01:00:00') < duration (1, 1, 0), true) %!assert_equal (1 < duration (24, 0, 0), false) %!assert_equal (days (1) < duration (24, 0, 0), false) %!assert_equal (days (1.05) < duration (24, 0, 0), false) %!assert_equal (days (NaN) < duration (NaN, 0, 0), false) %!assert_equal (duration (1, 0, 0) < hours (1), false) %!assert_equal (duration (0, 1, 0) < minutes (1), false) %!assert_equal (duration (0, 0, 1) < seconds (1), false) %!assert_equal (days ([1, 2]) < 1, [false, false]) %!assert_equal (days ([1; 2]) < 1, [false; false]) %!assert_equal (days ([1, 2]) < [1, 2], [false, false]) %!assert_equal (days ([1, 2]) < [1; 2], [false, false; true, false]) %!assert_equal (duration (1, 0, 0) != '01:00:00', false) %!assert_equal (duration (1, 0, 0) != '01:00:01', true) %!assert_equal (duration (1, 0, 0) != {'01:00:00'}, false) %!assert_equal (duration (1, 0, 1) != {'01:00:00'}, true) %!assert_equal (duration (1, 0, 0) != string ('01:00:00'), false) %!assert_equal (duration (NaN, 0, 0) != string ('01:00:00'), true) %!assert_equal (duration (24, 0, 0) != 1, false) %!assert_equal (duration (NaN, 0, 0) != NaN, true) %!assert_equal (duration (24, 0, 0) != days (1), false) %!assert_equal ('01:00:00' != duration (1, 0, 0), false) %!assert_equal ({'01:00:00'} != duration (1, 0, 0), false) %!assert_equal (string ('01:00:00') != duration (1, 0, 0), false) %!assert_equal (1 != duration (24, 0, 0), false) %!assert_equal (days (1) != duration (24, 0, 0), false) %!assert_equal (days (NaN) != duration (NaN, 0, 0), true) %!assert_equal (duration (1, 0, 0) != hours (1), false) %!assert_equal (duration (0, 1, 0) != minutes (1), false) %!assert_equal (duration (0, 0, 1) != seconds (1), false) %!assert_equal (days ([1, 2]) != 1, [false, true]) %!assert_equal (days ([1; 2]) != 1, [false; true]) %!assert_equal (days ([1, 2]) != [1, 2], [false, false]) %!assert_equal (days ([1, 2]) != [1; 2], [false, true; true, false]) ################################################################################ ## ** Mathematical Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'abs' 'plus' 'uplus' 'minus' ## ## 'uminus' 'times' 'mtimes' 'ldivide' ## ## 'rdivide' 'mldivide' 'mrdivide' 'mod' ## ## 'rem' 'eps' 'colon' 'linspace' ## ## 'sign' ## ## 'sum' 'cumsum' 'diff' 'min' ## ## 'max' 'cummin' 'cummax' 'floor' ## ## 'ceil' 'round' ## ## ## ################################################################################ %!assert_equal (days (abs (days ([1, -1, NaN, -Inf, Inf]))), [1, 1, NaN, ... %! Inf, Inf]) %!assert_equal (days (abs (days (- ones (2, 3, 4)))), ones (2, 3, 4)) %!assert_equal (days (days ([1, 2; NaN, Inf]) + [2, 3]), [3, 5; NaN, Inf]) %!assert_equal (days (days ([1, 2; NaN, Inf]) + days ([2, 3])), [3, 5; NaN, ... %! Inf]) %!assert_equal (days ([2, 3] + days ([1, 2; NaN, Inf])), [3, 5; NaN, Inf]) %!assert_equal (days (days ([2, 3]) + days ([1, 2; NaN, Inf])), [3, 5; NaN, ... %! Inf]) %!assert_equal (caldays (days (1) + calendarDuration), 0) %!assert_equal (caldays (days (1) + calendarDuration (0, 0, 1)), 1) %!assert_equal (hours (time (days (1) + calendarDuration)), 24) %!assert_equal (hours (time (days (1) + calendarDuration (0, 0, 1))), 24) %!assert_equal (class (days (1) + calendarDuration), 'calendarDuration') %!error ... %! days (1) + '24:00:00' %!error ... %! days (1) + {'24:00:00'} %!error ... %! days (1) + string('24:00:00') ## mod: remainder takes the sign of the divisor. Asserted in days, the unit a ## duration stores, so the arithmetic is exact; the same in hours carries the ## rounding of 7/24 and needs a tolerance. %!assert_equal (days (mod (days ([7, -7, 7, -7]), days ([2, 2, -2, -2]))), ... %! [1, 1, -1, -1]) %!assert_equal (days (mod (days ([1, 2, 3]), days (2))), [1, 0, 1]) %!assert_equal (hours (mod (hours ([7, -7, 7, -7]), hours ([2, 2, -2, ... %! -2]))), ... %! [1, 1, -1, -1], 1e-9) ## mod: a numeric operand counts days, on either side %!assert_equal (hours (mod (hours (7), 2)), 7, 1e-9) %!assert_equal (hours (mod (7, hours (2))), 0, 1e-9) %!assert_equal (hours (mod (hours (7), days (1))), 7, 1e-9) ## mod: a zero divisor leaves the dividend, and NaN carries through either side %!assert_equal (days (mod (days (7), days (0))), 7) %!assert_equal (days (mod (days (NaN), days (2))), NaN) %!assert_equal (days (mod (days (7), days (NaN))), NaN) ## mod: the display format comes from the duration operand %!assert_equal (mod (hours (7), hours (2)).Format, 'h') %!assert_equal (mod (7, hours (2)).Format, 'h') %!test %! x = days (3); %! x.Format = 'h'; %! assert_equal (mod (x, days (2)).Format, 'h'); ## mod: shape is kept for an array of any number of dimensions %!assert_equal (size (mod (hours (reshape (1:8, 2, 2, 2)), hours (3))), [2, ... %! 2, 2]) %!assert_equal (days (mod (days (reshape (1:8, 2, 2, 2)), days (3))), ... %! mod (reshape (1:8, 2, 2, 2), 3)) ## mod: anything that is not a duration or numeric is refused %!error ... %! mod (hours (7), calmonths (1)) %!error ... %! mod (hours (7), '2') %!error ... %! mod ({2}, hours (7)) ## rem: remainder takes the sign of the dividend, where mod takes the divisor's %!assert_equal (days (rem (days ([7, -7, 7, -7]), days ([2, 2, -2, -2]))), ... %! [1, -1, 1, -1]) %!assert_equal (days (rem (days ([1, 2, 3]), days (2))), [1, 0, 1]) %!assert_equal (hours (rem (hours ([7, -7, 7, -7]), hours ([2, 2, -2, ... %! -2]))), ... %! [1, -1, 1, -1], 1e-9) ## rem: the two agree only where the operands share a sign, and part ways on a ## zero divisor, where rem is NaN and mod is the dividend %!test %! a = days ([7, -7, 7, -7]); %! b = days ([2, 2, -2, -2]); %! assert_equal (days (mod (a, b)) == days (rem (a, b)), logical ([1, 0, 0, ... %! 1])); %!assert_equal (days (rem (days (7), days (0))), NaN) %!assert_equal (days (mod (days (7), days (0))), 7) ## rem: a numeric operand counts days, on either side %!assert_equal (hours (rem (hours (7), 2)), 7, 1e-9) %!assert_equal (hours (rem (7, hours (2))), 0, 1e-9) %!assert_equal (hours (rem (hours (7), days (1))), 7, 1e-9) ## rem: NaN carries through from either side %!assert_equal (days (rem (days (NaN), days (2))), NaN) %!assert_equal (days (rem (days (7), days (NaN))), NaN) ## rem: the display format comes from the duration operand %!assert_equal (rem (hours (7), hours (2)).Format, 'h') %!assert_equal (rem (7, hours (2)).Format, 'h') %!test %! x = days (3); %! x.Format = 'h'; %! assert_equal (rem (x, days (2)).Format, 'h'); ## rem: shape is kept for an array of any number of dimensions %!assert_equal (days (rem (days (reshape (1:8, 2, 2, 2)), days (3))), ... %! rem (reshape (1:8, 2, 2, 2), 3)) ## rem: anything that is not a duration or numeric is refused %!error ... %! rem (hours (7), calmonths (1)) %!error ... %! rem (hours (7), '2') %!error ... %! rem ({2}, hours (7)) ## mldivide: the denominator decides the result. A number scales the duration; ## a duration gives how many times one goes into the other, without a unit. %!assert_equal (days (mldivide (2, days (6))), 3) %!assert_equal (class (mldivide (2, days (6))), 'duration') %!assert_equal (mldivide (days (2), days (6)), 3) %!assert_equal (class (mldivide (days (2), days (6))), 'double') %!assert_equal (mldivide (days (2), days ([6, 8])), [3, 4]) %!assert_equal (days (2 \ days (6)), 3) ## mldivide: a zero denominator gives Inf, and NaN carries through %!assert_equal (mldivide (days (0), days (6)), Inf) %!assert_equal (days (mldivide (0, days (6))), Inf) %!assert_equal (days (mldivide (2, days (NaN))), NaN) ## mldivide: the format of a duration result is the numerator's %!assert_equal (mldivide (2, hours (6)).Format, 'h') ## mldivide: a logical or integer denominator is converted, an Octave extension %!assert_equal (days (mldivide (true, days (6))), 6) %!assert_equal (days (mldivide (int32 (2), days (6))), 3) ## mldivide: the denominator must be scalar and real, and dividing a number by ## a duration has no meaning %!error ... %! mldivide (days (2), 6) %!error ... %! mldivide ([1, 2], days ([6, 8])) %!error ... %! mldivide (days ([1, 2]), days ([6, 8])) %!error ... %! mldivide (2 + 1i, days (6)) %!error ... %! mldivide (days (6), calmonths (1)) %!error ... %! mldivide ({2}, days (6)) ## mrdivide: the mirror of mldivide, the denominator deciding the result %!assert_equal (days (mrdivide (days (6), 2)), 3) %!assert_equal (class (mrdivide (days (6), 2)), 'duration') %!assert_equal (mrdivide (days (6), days (2)), 3) %!assert_equal (class (mrdivide (days (6), days (2))), 'double') %!assert_equal (mrdivide (days ([6, 8]), days (2)), [3, 4]) %!assert_equal (days (days (6) / 2), 3) ## mrdivide: A / B is B \ A, which is what makes it the mirror %!test %! a = days ([6, -8, 0, NaN]); %! assert_equal (mrdivide (a, days (2)), mldivide (days (2), a)); %! assert_equal (days (mrdivide (a, 4)), days (mldivide (4, a))); ## mrdivide: a zero denominator gives Inf, and NaN carries through %!assert_equal (days (mrdivide (days (6), 0)), Inf) %!assert_equal (days (mrdivide (days (NaN), 2)), NaN) %!assert_equal (mrdivide (days (6), days (0)), Inf) ## mrdivide: format kept, and a logical or integer denominator is converted %!assert_equal (mrdivide (hours (6), 2).Format, 'h') %!assert_equal (days (mrdivide (days (6), true)), 6) %!assert_equal (days (mrdivide (days (6), int32 (2))), 3) ## mrdivide: the denominator must be scalar and real; use rdivide to divide by ## an array of divisors %!error ... %! mrdivide (6, days (2)) %!error ... %! mrdivide (days ([6, 8]), [1, 2]) %!error ... %! mrdivide (days (6), days ([1, 2])) %!error ... %! mrdivide (days (6), 2 + 1i) %!error ... %! mrdivide (days (6), {2}) ## eps: the spacing of the milliseconds a duration is stored in, which is what ## MATLAB stores too. Every figure below is bit-exact against R2024a. %!assert_equal (class (eps (hours (1))), 'duration') %!assert_equal (milliseconds (eps (days (1))), eps (86400000)) %!assert_equal (milliseconds (eps (hours (1))), eps (3600000)) %!assert_equal (milliseconds (eps (days ([1, 2, 4]))), ... %! eps ([1, 2, 4] * 86400000)) %!assert_equal (seconds (eps (seconds (1))), 1.1368683772161603e-16) %!assert_equal (seconds (eps (seconds (3600))), 4.6566128730773927e-13) %!assert_equal (seconds (eps (seconds (1e9))), 1.220703125e-07) ## eps: a negative duration has the spacing of its magnitude, and NaN gives NaN %!assert_equal (milliseconds (eps (days (-5))), eps (5 * 86400000)) %!assert_equal (days (eps (days (NaN))), NaN) ## eps: format and shape are kept %!assert_equal (eps (hours (1)).Format, 'h') %!assert_equal (size (eps (days (reshape (1:8, 2, 2, 2)))), [2, 2, 2]) %!assert_equal (milliseconds (eps (days (reshape (1:8, 2, 2, 2)))), ... %! eps (reshape (1:8, 2, 2, 2) * 86400000)) ## datenum: the length in days, the same value days() gives under the name the ## older date functions use %!assert_equal (datenum (hours (24)), 1) %!assert_equal (class (datenum (hours (24))), 'double') %!assert_equal (datenum (days ([1, 2.5])), [1, 2.5]) %!assert_equal (datenum (days (NaN)), NaN) %!assert_equal (datenum (hours ([1, 2, 3])), days (hours ([1, 2, 3]))) %!assert_equal (size (datenum (days (reshape (1:8, 2, 2, 2)))), [2, 2, 2]) ## datenum: it counts elapsed days, so adding it to a serial date shifts that ## date by the duration %!assert_equal (datenum (2024, 1, 1) + datenum (hours (36)), ... %! datenum (2024, 1, 2) + 0.5) ## isregular: evenly spaced, in either direction, with the step as second output %!test %! [tf, dt] = isregular (hours ([1, 2, 3])); %! assert_equal (tf, true); %! assert_equal (char (dt), '1 hr'); %!test %! [tf, dt] = isregular (hours ([3, 2, 1])); %! assert_equal (tf, true); %! assert_equal (char (dt), '-1 hr'); %!assert_equal (isregular (hours ([1, 2])), true) %!assert_equal (isregular (hours ([1; 2; 3])), true) %!assert_equal (isregular (hours ([-3, -2, -1])), true) %!assert_equal (isregular (hours ([1, 2, 4])), false) ## isregular: a vector that does not move has no spacing, so it is not regular %!test %! [tf, dt] = isregular (hours ([2, 2, 2])); %! assert_equal (tf, false); %! assert_equal (isnan (dt), true); %!assert_equal (isregular (hours ([5, 5])), false) ## isregular: a NaN or an infinite element makes it irregular, as does having ## fewer than two elements %!assert_equal (isregular (hours ([1, NaN, 3])), false) %!assert_equal (isregular (hours ([1, 2, NaN])), false) %!assert_equal (isregular (hours ([1, Inf, 3])), false) %!assert_equal (isregular (hours ([Inf, Inf])), false) %!assert_equal (isregular (hours (5)), false) %!assert_equal (isregular (hours ([])), false) %!assert_equal (isregular (hours (zeros (1, 0))), false) ## isregular: steps are compared within a few eps, since subtracting durations ## rounds; one, two and three hours differ in the last bit and are still regular %!assert_equal (isregular (hours ([1, 2, 3 + 1e-15])), true) %!assert_equal (isregular (hours ([1, 2, 3 + 1e-14])), false) %!assert_equal (isregular (hours ([1, 2, 3 + 1e-9])), false) %!assert_equal (isregular (hours ([0, 1/3, 2/3])), true) ## isregular: the step keeps the vector's own format, in either direction %!test %! [~, dt] = isregular (days ([1, 2, 3])); %! assert_equal (dt.Format, 'd'); %! assert_equal (char (dt), '1 day'); %!test %! [~, dt] = isregular (seconds ([3, 2, 1])); %! assert_equal (dt.Format, 's'); %! assert_equal (char (dt), '-1 sec'); ## isregular: only elapsed time can be regular, a duration having no calendar, ## so every calendar component is false however the vector is spaced %!assert_equal (isregular (hours ([1, 2, 3]), 'time'), true) %!assert_equal (isregular (hours ([1, 2, 3]), 'days'), false) %!assert_equal (isregular (days ([1, 2, 3]), 'days'), false) %!assert_equal (isregular (days ([1, 2, 3]), 'Time'), true) %!assert_equal (isregular (hours ([1, 2, 3]), 'months'), false) ## isregular: input must be a vector, and the component must be one MATLAB names %!error ... %! isregular (hours ([1, 2; 3, 4])) %!error ... %! isregular (hours ([1, 2, 3]), 'bogus') %!assert_equal (days (+ days (-3)), -3) %!assert_equal (days (+ days (magic (3))), magic (3)) %!assert_equal (days (days ([3, 5; NaN, Inf]) - [2, 3]), [1, 2; NaN, Inf]) %!assert_equal (days (days ([3, 5; NaN, Inf]) - days ([2, 3])), [1, 2; NaN, ... %! Inf]) %!assert_equal (days ([3, 5] - days ([2, 3; NaN, Inf])), [1, 2; NaN, -Inf]) %!assert_equal (days (days ([3, 5]) - days ([2, 3; NaN, Inf])), [1, 2; NaN, ... %! -Inf]) %!assert_equal (caldays (days (1) - calendarDuration), 0) %!assert_equal (caldays (days (1) - calendarDuration (0, 0, 1)), -1) %!assert_equal (hours (time (days (1) - calendarDuration)), 24) %!assert_equal (hours (time (days (1) - calendarDuration (0, 0, 1))), 24) %!assert_equal (class (days (1) - calendarDuration), 'calendarDuration') %!error ... %! days (1) - '24:00:00' %!error ... %! days (1) - {'24:00:00'} %!error ... %! days (1) - string('24:00:00') %!assert_equal (days (- days (-3)), 3) %!assert_equal (days (- days (3)), -3) %!assert_equal (days (- days (magic (3))), - magic (3)) %!assert_equal (days (days ([1:3]) .* [1:3]), [1, 4, 9]) %!assert_equal (days ([1:3] .* days ([1:3])), [1, 4, 9]) %!assert_equal (days (days ([1:3]) .* [1; 2]), [1, 2, 3; 2, 4, 6]) %!assert_equal (days ([1; 2] .* days ([1:3])), [1, 2, 3; 2, 4, 6]) %!assert_equal (class (1 .* duration), 'duration') %!assert_equal (class (duration .* magic (3)), 'duration') %!assert_equal (hours (duration .* magic (3)), zeros (3)) %!error ... %! days (1) .* days (1) %!error ... %! days (1) .* 'asd' %!error ... %! days (1) .* string ({'asd'}) %!error ... %! {'asd'} .* days (1) %!assert_equal (days (days ([1:3]) * [1:3]'), 14) %!assert_equal (days (days ([1:3]') * [1:3]), [1, 2, 3; 2, 4, 6; 3, 6, 9]) %!assert_equal (days ([1:3] * days ([1:3]')), 14) %!assert_equal (days ([1:3]' * days ([1:3])), [1, 2, 3; 2, 4, 6; 3, 6, 9]) %!assert_equal (class (1 * duration), 'duration') %!assert_equal (class (days (magic (3)) * magic (3)), 'duration') %!assert_equal (days (days (magic (3)) * magic (3)), magic (3) * magic (3)) %!assert_equal (days (magic (3) * days (magic (3))), magic (3) * magic (3)) %!error ... %! days (1) * days (1) %!error ... %! days (1) * 'asd' %!error ... %! days (1) * string ({'asd'}) %!error ... %! {'asd'} * days (1) %!assert_equal (days ([1:3]) .\ days ([1:3]), [1, 1, 1]) %!assert_equal (days ([1:3] .\ days ([1:3])), [1, 1, 1]) %!assert_equal (days ([1; 2] .\ days ([1:3])), [1, 2, 3; 0.5, 1, 1.5]) %!assert_equal (class (1 .\ duration), 'duration') %!assert_equal (class (duration .\ duration), 'double') %!error ... %! days (1) .\ 1 %!error ... %! 'asd' .\ days (1) %!assert_equal (days ([1:3]) ./ days ([1:3]), [1, 1, 1]) %!assert_equal (days (days ([1:3]) ./ [1:3]), [1, 1, 1]) %!assert_equal (days (days ([1; 2]) ./ [1:3]), [1, 0.5, 1/3; 2, 1, 2/3]) %!assert_equal (class (duration ./ 1), 'duration') %!assert_equal (class (duration ./ duration), 'double') %!error ... %! 1 ./ days (1) %!error ... %! days (1) ./ 'asd' %!assert_equal (days(1):days(10), days ([1:10])) %!assert_equal (days(1):2:days(10), days ([1:2:10])) %!assert_equal (days(1):days(2):days(10), days ([1:2:10])) %!test %! R = days(1):'72:00:00'; %! assert_equal (R, days ([1:3])) %! assert_equal (R.Format, 'd') %!test %! R = '00:00:00':days(1):'72:00:00'; %! assert_equal (R, days ([0:3])) %! assert_equal (R.Format, 'd') %!test %! R = days(1):'06:00:00':days(10); %! assert_equal (R, days ([1:0.25:10])) %! assert_equal (R.Format, 'd') %!assert_equal (days(1):{'72:00:00'}, days ([1:3])) %!assert_equal (days(1):string('72:00:00'), days ([1:3])) %!assert_equal (string('00:00:00'):days(1):string('72:00:00'), days ([0:3])) %!assert_equal (days(1):string({'06:00:00'}):days(10), days ([1:0.25:10])) %!assert_equal (days(1):{'06:00:00'}:days(10), days ([1:0.25:10])) %!error colon (days (1)) %!error colon (days (1), 2, 3, 4) %!error days([1, 2]):days(2) %!error days(1):days([1, 2]) %!error days(1):days(2):days([1, 2]) %!assert_equal (milliseconds (linspace (days (1), days (2))), ... %! linspace (86400000, 172800000)) %!assert_equal (milliseconds (linspace (days (1), days (2), 15)), ... %! linspace (86400000, 172800000, 15)) %!assert_equal (milliseconds (linspace (days (1), 2)), ... %! linspace (86400000, 172800000)) %!assert_equal (milliseconds (linspace (1, days (2))), ... %! linspace (86400000, 172800000)) %!assert_equal (milliseconds (linspace (1, days (2), 15)), ... %! linspace (86400000, 172800000, 15)) %!assert_equal (milliseconds (linspace (days (1), '48:00:00')), ... %! linspace (86400000, 172800000)) %!assert_equal (milliseconds (linspace (days (1), {'48:00:00'})), ... %! linspace (86400000, 172800000)) %!assert_equal (milliseconds (linspace (days (1), string ({'48:00:00'}))), ... %! linspace (86400000, 172800000)) %!assert_equal (milliseconds (linspace (days (1), '48:00:00', 15)), ... %! linspace (86400000, 172800000, 15)) %!assert_equal (milliseconds (linspace (days (1), {'48:00:00'}, 15)), ... %! linspace (86400000, 172800000, 15)) %!assert_equal (milliseconds ( ... %! linspace (days (1), string ({'48:00:00'}), 15)), ... %! %! linspace (86400000, 172800000, 15)) %!assert_equal (milliseconds (linspace ('48:00:00', days (1))), ... %! linspace (172800000, 86400000)) %!assert_equal (milliseconds (linspace ({'48:00:00'}, days (1))), ... %! linspace (172800000, 86400000)) %!assert_equal (milliseconds (linspace (string ({'48:00:00'}), days (1))), ... %! linspace (172800000, 86400000)) %!assert_equal (milliseconds (linspace ('48:00:00', days (1), 15)), ... %! linspace (172800000, 86400000, 15)) %!assert_equal (milliseconds (linspace ({'48:00:00'}, days (1), 15)), ... %! linspace (172800000, 86400000, 15)) %!assert_equal (milliseconds ( ... %! linspace (string ({'48:00:00'}), days (1), 15)), ... %! %! linspace (172800000, 86400000, 15)) %!error ... %! linspace (days (1)) %!assert_equal (days (diff (days (magic (3)))), diff (magic (3))) %!assert_equal (days (diff (days (magic (3)), 2)), diff (magic (3), 2)) %!assert_equal (days (diff (days (magic (3)), 1, 1)), diff (magic (3), 1, 1)) %!assert_equal (days (diff (days (magic (3)), 1, 2)), diff (magic (3), 1, 2)) %!assert_equal (sum (days ([1, 2, 3])), days (6)) %!assert_equal (sum (days ([-1; -2; -3])), days (-6)) %!assert_equal (sum (days ([1, 2; 3, 4]), 1), days ([4, 6])) %!assert_equal (sum (days ([1, 2; 3, 4]), 2), days ([3; 7])) %!assert_equal (sum (days ([])), days(0)) %!assert_equal (size (sum (days ([]), 1)), [1, 0]) %!assert_equal (size (sum (days ([]), 2)), [0, 1]) %!assert_equal (size (sum (days ([]), 3)), [0, 0]) %!assert_equal (sum (days (zeros (1, 0))), days (0)) %!assert_equal (sum (days (zeros (1, 0)), 1), days (zeros (1, 0))) %!assert_equal (sum (days (zeros (1, 0)), 2), days (0)) %!assert_equal (sum (days (zeros (0, 1))), days (0)) %!assert_equal (sum (days (zeros (0, 1)), 1), days (0)) %!assert_equal (sum (days (zeros (0, 1)), 2), days (zeros (0, 1))) %!test %! x = repmat (days ([1:20;6:25]), [5, 2, 6, 3]); %! assert_equal (size (sum (x, [3, 2])), [10, 1, 1, 3]); %! assert_equal (size (sum (x, [1, 2])), [1, 1, 6, 3]); %! assert_equal (size (sum (x, [1, 2, 4])), [1, 1, 6]); %! assert_equal (size (sum (x, [1, 4, 3])), [1, 40]); %! assert_equal (size (sum (x, [1, 2, 3, 4])), [1, 1]); %!assert_equal (sum (days (ones (2,2)), 3), days (ones (2,2))) %!assert_equal (sum (days (ones (2,2,2)), 99), days (ones (2,2,2))) %!assert_equal (sum (days (magic (3)), 3), days (magic (3))) %!test %! x = days (ones (3, 4, 5)); %! x(1) = NaN; %! assert_equal (isequaln (sum (x)(:,:,1), days ([NaN, 3, 3, 3])), true); %! assert_equal (isequaln (sum (x, "includenan")(:,:,1), days ([NaN, 3, 3, ... %! 3])), true); %! assert_equal (sum (x, "omitnan")(:,:,1), days ([2, 3, 3, 3])); %! assert_equal (sum (x, "omitmissing")(:,:,1), days ([2, 3, 3, 3])); %! assert_equal (isequaln (sum (x, [2 3]), days ([NaN; 20; 20])), true); %! assert_equal (sum (x, [2 3], "omitnan"), days ([19; 20; 20])); %!assert_equal (cumsum (days ([1, 2, 3])), days ([1, 3, 6])) %!assert_equal (cumsum (days ([-1; -2; -3])), days ([-1; -3; -6])) %!assert_equal (cumsum (days ([1, 2; 3, 4]), 1), days ([1, 2; 4, 6])) %!assert_equal (cumsum (days ([1, 2; 3, 4]), 2), days ([1, 3; 3, 7])) %!test %! x = reshape (days ([1:8]), 2, 2, 2); %! y = cumsum (x); %! assert_equal (y(:,:,1), days ([1, 3; 3, 7])); %! assert_equal (y(:,:,2), days ([5, 7; 11, 15])); %! assert_equal (flip (cumsum (flip (x))), cumsum (x, "reverse")); %! y = cumsum (x, 2); %! assert_equal (y(:,:,1), days ([1, 4; 2, 6])); %! assert_equal (y(:,:,2), days ([5, 12; 6, 14])); %! y = cumsum (x, 2, "reverse"); %! assert_equal (y(:,:,1), days ([4, 3; 6, 4])); %! assert_equal (y(:,:,2), days ([12, 7; 14, 8])); %! assert_equal (flip (cumsum (flip (x, 2), 2), 2), cumsum (x, 2, "reverse")); %! y = cumsum (x, [1, 2]); %! assert_equal (y(:,:,1), days ([1; 3; 6; 10])); %! assert_equal (y(:,:,2), days ([5; 11; 18; 26])); %! y = cumsum (x, [1, 3]); %! assert_equal (y(:,1), days ([1; 3; 8; 14])); %! assert_equal (y(:,2), days ([3; 7; 14; 22])); %! y = cumsum (x, [2, 3]); %! assert_equal (y(1,:), days ([1, 4, 9, 16])); %! assert_equal (y(2,:), days ([2, 6, 12, 20])); %! assert_equal (cumsum (x, [1, 2, 3]), cumsum (x, "all")); %!test %! x = reshape (days ([1:8]), 2, 2, 2); %! assert_equal (cumsum (x, 4), x); %! assert_equal (cumsum (x, 2), cumsum (x, [2, 4])); %! assert_equal (cumsum (x, 2, "reverse"), cumsum (x, [2, 4], "reverse")); %! x(3) = NaN; %! assert_equal (isequaln (cumsum (x, 4), x), true); %! y = x; %! y(3) = 0; %! assert_equal (cumsum (x, 4, "omitnan"), y); %! assert_equal (cumsum (x, 2, "omitnan"), cumsum (x, [2, 4], "omitnan")); %! assert_equal (cumsum (x, 2, "reverse", "omitnan"), %! cumsum (x, [2, 4], "reverse", "omitnan")); %!assert_equal (cumsum (days ([NaN, NaN]), "omitnan"), days ([0, 0])) %!assert_equal (min (days ([1, 4, 2, 3])), days (1)) %!assert_equal (min (days ([1; -10; 5; -2])), days (-10)) %!test %! x = reshape (days ([1:8]), [2,2,2]); %! assert_equal (min (x, [], 1), reshape (days ([1, 3, 5, 7]), [1, 2, 2])); %! assert_equal (min (x, [], 2), reshape (days ([1, 2, 5, 6]), [2, 1, 2])); %! [y, i] = min (x, [], 3); %! assert_equal (ndims (y), 2); %! assert_equal (y, days ([1, 3; 2, 4])); %! assert_equal (ndims (i), 2); %! assert_equal (i, [1, 1; 1, 1]); %!test %! x = reshape (days ([1:8]), [2, 2, 2]); %! assert_equal (min (x, [], [1, 3]), days ([1, 3])); %! assert_equal (min (x, [], [2, 3]), days ([1; 2])); %! assert_equal (min (x, [], [1, 2]), reshape (days ([1, 5]), [1, 1, 2])); %! assert_equal (min (x, [], [1, 2, 3]), min (x, [], "all")); %!test %! x = days ([1, 2, 3, 4]); %! y = flip (x); %! assert_equal (min (x, y), days ([1, 2, 2, 1])); %! assert_equal (min (x, 3), days ([1, 2, 3, 3])); %! assert_equal (min (2, x), days ([1, 2, 2, 2])); %!test %! [m, i] = min (days ([1, 2, 3; 4, 3, NaN; 4, 5, 6]), [], 2, "includenan"); %! assert_equal (isequaln (m, days ([1; NaN; 4])), true); %! assert_equal (i, [1; 3; 1]); %! [m,i] = min (days ([1, 2, 3; 4, NaN, NaN; 4, 5, 6]), [], 2, "includenan"); %! assert_equal (isequaln (m, [1; NaN; 4]), true); %! assert_equal (i, [1; 2; 1]); %!test %! x = days (magic (3)); %! x(2, 3) = NaN; %! assert_equal (isequaln (min (x, [], 2, "includenan"), ... %! days ([1; NaN; 2])), true); %!assert_equal (size (min (days (0), days (zeros (0, 1)))), [0, 1]) %!assert_equal (size (min (days ([]), days (zeros (0, 1)))), [0, 0]) %!assert_equal (size (min (days (zeros (0, 1)), days ([]))), [0, 0]) %!assert_equal (size (min (days ([]), days (zeros (1, 0)))), [0, 0]) %!assert_equal (size (min (days (zeros (1, 0)), days ([]))), [0, 0]) %!test %! x = repmat (days ([4, 3, 2, 4, 1, 5, 3, 2]), 3, 2, 5, 2); %! [m, i] = min (x, [], [2, 3], 'linear'); %! assert_equal (m, days (ones (3, 1, 1, 2))); %! assert_equal (i(:,:,1,1), [13; 14; 15]); %! assert_equal (i(:,:,1,2), [253; 254; 255]); %!test %! x = repmat (days ([4, 3, 2, 4, 1, 5, 3, 2]), 3, 2, 5, 2); %! [m, i] = min (x, [], [1, 3], 'linear'); %! assert_equal (m, x(1,:,1,:)); %! assert_equal (i(:,:,1,1), [1:3:46]); %! assert_equal (i(:,:,1,2), [241:3:286]); %!assert_equal (cummin (days ([1, 4, 2, 3])), days ([1, 1, 1, 1])) %!assert_equal (cummin (days ([1; -10; 5; -2])), days ([1; -10; -10; -10])) %!test %! x = reshape (days ([1:8]), [2, 2, 2]); %! assert_equal (cummin (x, 1), reshape (days ([1, 1, 3, 3, 5, 5, 7, 7]), ... %! [2, 2, 2])); %! assert_equal (cummin (x, 2), reshape (days ([1, 2, 1, 2, 5, 6, 5, 6]), ... %! [2, 2, 2])); %! [w, iw] = cummin (x, 3); %! assert_equal (ndims (w), 3); %! assert_equal (w, repmat (days ([1, 3; 2, 4]), [1, 1, 2])); %! assert_equal (ndims (iw), 3); %! assert_equal (iw, ones (2, 2, 2)); %!assert_equal (isequaln (cummin (days ([1; -10; NaN; -2]), 'includenan'), ... %! days ([1; -10; NaN; NaN])), true) %!assert_equal (isequaln (cummin (days ([1; -10; NaN; -2]), 'reverse', ... %! 'includenan'), days ([NaN(3,1); -2])), true) %!assert_equal (isequaln (cummin (days ([1, -10, NaN, -2]), 'includenan'), ... %! days ([1, -10, NaN, NaN])), true) %!assert_equal (isequaln (cummin (days ([1, -10, NaN, -2]), 'reverse', ... %! 'includenan'), days ([NaN(1,3), -2])), true) %!shared A %! A = days ([3, 5, NaN, 4, 2; 2, 6, 2, 9, 4; 1, 3, 0, NaN, 1; 5, 3, 4, 2, 0]); %!test %! [m, i] = cummin (A, 'includenan'); %! m_exp = days ([3, 5, NaN, 4, 2; 2, 5, NaN, 4, 2; ... %! 1, 3, NaN, NaN, 1; 1, 3, NaN, NaN, 0]); %! i_exp = [1, 1, 1, 1, 1; 2, 1, 1, 1, 1; 3, 3, 1, 3, 3; 3, 3, 1, 3, 4]; %! assert_equal (isequaln (m, m_exp), true); %! assert_equal (i, i_exp); %!test %! [m, i] = cummin (A, 'includenan', 'reverse'); %! m_exp = days ([1, 3, NaN, NaN, 0; 1, 3, 0, NaN, 0; ... %! 1, 3, 0, NaN, 0; 5, 3, 4, 2, 0]); %! i_exp = [3, 4, 1, 3, 4; 3, 4, 3, 3, 4; 3, 4, 3, 3, 4; 4, 4, 4, 4, 4]; %! assert_equal (isequaln (m, m_exp), true); %! assert_equal (i, i_exp); %!test %! [m, i] = cummin (A, 'reverse'); %! m_exp = days ([1, 3, 0, 2, 0; 1, 3, 0, 2, 0; 1, 3, 0, 2, 0; 5, 3, 4, 2, 0]); %! i_exp = [3, 4, 3, 4, 4; 3, 4, 3, 4, 4; 3, 4, 3, 4, 4; 4, 4, 4, 4, 4]; %! assert_equal (m, m_exp); %! assert_equal (i, i_exp); %!test %! [m, i] = cummin (A); %! m_exp = days ([3, 5, NaN, 4, 2; 2, 5, 2, 4, 2; 1, 3, 0, 4, 1; 1, 3, 0, 2, ... %! 0]); %! i_exp = [1, 1, 1, 1, 1; 2, 1, 2, 1, 1; 3, 3, 3, 1, 3; 3, 3, 3, 4, 4]; %! assert_equal (isequaln (m, m_exp), true); %! assert_equal (i, i_exp); %!test %! [m, i] = cummin (A, 2, 'includenan'); %! m_exp = days ([3, 3, NaN, NaN, NaN; 2, 2, 2, 2, 2; 1, 1, 0, NaN, NaN; 5, ... %! 3, 3, 2, 0]); %! i_exp = [1, 1, 3, 3, 3; 1, 1, 1, 1, 1; 1, 1, 3, 4, 4; 1, 2, 2, 4, 5]; %! assert_equal (isequaln (m, m_exp), true); %! assert_equal (i, i_exp); %!test %! [m, i] = cummin (A, 2, 'includenan', 'reverse'); %! m_exp = days ([NaN, NaN, NaN, 2, 2; 2, 2, 2, 4, 4; ... %! NaN, NaN, NaN, NaN, 1; 0, 0, 0, 0, 0]); %! i_exp = [3, 3, 3, 5, 5; 3, 3, 3, 5, 5; 4, 4, 4, 4, 5; 5, 5, 5, 5, 5]; %! assert_equal (isequaln (m, m_exp), true); %! assert_equal (i, i_exp); %!test %! [m, i] = cummin (A, 2, 'reverse'); %! m_exp = days ([2, 2, 2, 2, 2; 2, 2, 2, 4, 4; 0, 0, 0, 1, 1; 0, 0, 0, 0, 0]); %! i_exp = [5, 5, 5, 5, 5; 3, 3, 3, 5, 5; 3, 3, 3, 5, 5; 5, 5, 5, 5, 5]; %! assert_equal (m, m_exp); %! assert_equal (i, i_exp); %!test %! [m, i] = cummin (A, 2); %! m_exp = days ([3, 3, 3, 3, 2; 2, 2, 2, 2, 2; 1, 1, 0, 0, 0; 5, 3, 3, 2, 0]); %! i_exp = [1, 1, 1, 1, 5; 1, 1, 1, 1, 1; 1, 1, 3, 3, 3; 1, 2, 2, 4, 5]; %! assert_equal (m, m_exp); %! assert_equal (i, i_exp); %!test %! [~, i_lin] = cummin (A, 'includenan', 'linear'); %! assert_equal (isequaln (cummin (A, 'includenan'), A(i_lin)), true); %!test %! [~, i_lin] = cummin (A, 'includenan', 'reverse', 'linear'); %! assert_equal (isequaln (cummin (A, 'includenan', 'reverse'), A(i_lin)), ... %! true); %!test %! [~, i_lin] = cummin (A, 'reverse', 'linear'); %! assert_equal (isequaln (cummin (A, 'reverse'), A(i_lin)), true); %!test %! [~, i_lin] = cummin (A, 'linear'); %! assert_equal (isequaln (cummin (A), A(i_lin)), true); %!test %! [~, i_lin] = cummin (A, 2, 'includenan', 'linear'); %! assert_equal (isequaln (cummin (A, 2, 'includenan'), A(i_lin)), true); %!test %! [~, i_lin] = cummin (A, 2, 'includenan', 'reverse', 'linear'); %! assert_equal (isequaln (cummin (A, 2, 'includenan', 'reverse'), ... %! A(i_lin)), true); %!test %! [~, i_lin] = cummin (A, 2, 'reverse', 'linear'); %! assert_equal (isequaln (cummin (A, 2, 'reverse'), A(i_lin)), true); %!test %! [~, i_lin] = cummin (A, 2, 'linear'); %! assert_equal (isequaln (cummin (A, 2), A(i_lin)), true); %!shared x %! x = days (randi ([-10, 10], 3, 4, 5, 2)); %!test %! [m, i] = cummin (x, [2, 3], 'linear'); %! assert_equal (m, x(i)); %!test %! [m, i] = cummin (x, [1, 3], 'linear'); %! assert_equal (m, x(i)); %!test %! [m, i] = cummin (x, [2, 4], 'linear'); %! assert_equal (m, x(i)); %!test %! [m, i] = cummin (x, [1, 4], 'linear'); %! assert_equal (m, x(i)); %!test %! [m, i] = cummin (x, [1, 2, 3], 'linear'); %! assert_equal (m, x(i)); %!test %! [m, i] = cummin (x, [2, 3, 4], 'linear'); %! assert_equal (m, x(i)); %!assert_equal (max (days ([1, 4, 2, 3])), days (4)) %!assert_equal (max (days ([1; -10; 5; -2])), days (5)) %!test %! x = reshape (days ([1:8]), [2,2,2]); %! assert_equal (max (x, [], 1), reshape (days ([2, 4, 6, 8]), [1, 2, 2])); %! assert_equal (max (x, [], 2), reshape (days ([3, 4, 7, 8]), [2, 1, 2])); %! [y, i] = max (x, [], 3); %! assert_equal (ndims (y), 2); %! assert_equal (y, days ([5, 7; 6, 8])); %! assert_equal (ndims (i), 2); %! assert_equal (i, [2, 2; 2, 2]); %!test %! x = reshape (days ([1:8]), [2, 2, 2]); %! assert_equal (max (x, [], [1, 3]), days ([6, 8])); %! assert_equal (max (x, [], [2, 3]), days ([7; 8])); %! assert_equal (max (x, [], [1, 2]), reshape (days ([4, 8]), [1, 1, 2])); %! assert_equal (max (x, [], [1, 2, 3]), max (x, [], "all")); %!test %! x = days ([1, 2, 3, 4]); %! y = flip (x); %! assert_equal (max (x, y), days ([4, 3, 3, 4])); %! assert_equal (max (x, 3), days ([3, 3, 3, 4])); %! assert_equal (max (2, x), days ([2, 2, 3, 4])); %!test %! [m, i] = max (days ([1, 2, 3; 4, 3, NaN; 4, 5, 6]), [], 2, "includenan"); %! assert_equal (isequaln (m, days ([3; NaN; 6])), true); %! assert_equal (i, [3; 3; 3]); %! [m,i] = max (days ([1, 2, 3; 4, NaN, NaN; 4, 5, 6]), [], 2, "includenan"); %! assert_equal (isequaln (m, [3; NaN; 6]), true); %! assert_equal (i, [3; 2; 3]); %!test %! x = days (magic (3)); %! x(2, 3) = NaN; %! assert_equal (isequaln (max (x, [], 2, "includenan"), ... %! days ([8; NaN; 9])), true); %!assert_equal (size (max (days (0), days (zeros (0, 1)))), [0, 1]) %!assert_equal (size (max (days ([]), days (zeros (0, 1)))), [0, 0]) %!assert_equal (size (max (days (zeros (0, 1)), days ([]))), [0, 0]) %!assert_equal (size (max (days ([]), days (zeros (1, 0)))), [0, 0]) %!assert_equal (size (max (days (zeros (1, 0)), days ([]))), [0, 0]) %!test %! x = repmat (days ([4, 3, 2, 4, 1, 5, 3, 2]), 3, 2, 5, 2); %! [m, i] = max (x, [], [2, 3], 'linear'); %! assert_equal (m, days (5 * ones (3, 1, 1, 2))); %! assert_equal (i(:,:,1,1), [16; 17; 18]); %! assert_equal (i(:,:,1,2), [256; 257; 258]); %!test %! x = repmat (days ([4, 3, 2, 4, 1, 5, 3, 2]), 3, 2, 5, 2); %! [m, i] = max (x, [], [1, 3], 'linear'); %! assert_equal (m, x(1,:,1,:)); %! assert_equal (i(:,:,1,1), [1:3:46]); %! assert_equal (i(:,:,1,2), [241:3:286]); %!assert_equal (cummax (days ([1, 4, 2, 3])), days ([1, 4, 4, 4])) %!assert_equal (cummax (days ([1; -10; 5; -2])), days ([1; 1; 5; 5])) %!test %! x = reshape (days ([8:-1:1]), [2, 2, 2]); %! assert_equal (cummax (x, 1), reshape (days ([8, 8, 6, 6, 4, 4, 2, 2]), ... %! [2, 2, 2])); %! assert_equal (cummax (x, 2), reshape (days ([8, 7, 8, 7, 4, 3, 4, 3]), ... %! [2, 2, 2])); %! [w, iw] = cummax (x, 3); %! assert_equal (ndims (w), 3); %! assert_equal (w, repmat (days ([8, 6; 7, 5]), [1, 1, 2])); %! assert_equal (ndims (iw), 3); %! assert_equal (iw, ones (2, 2, 2)); %!assert_equal (isequaln (cummax (days ([1; -10; NaN; -2]), 'includenan'), ... %! days ([1; 1; NaN; NaN])), true) %!assert_equal (isequaln (cummax (days ([1; -10; NaN; -2]), 'reverse', ... %! 'includenan'), days ([NaN(3,1); -2])), true) %!assert_equal (isequaln (cummax (days ([1, -10, NaN, -2]), 'includenan'), ... %! days ([1, 1, NaN, NaN])), true) %!assert_equal (isequaln (cummax (days ([1, -10, NaN, -2]), 'reverse', ... %! 'includenan'), days ([NaN(1,3), -2])), true) %!shared A %! A = days ([3, 5, NaN, 4, 2; 2, 6, 2, 9, 4; 1, 3, 0, NaN, 1; 5, 3, 4, 2, 0]); %!test %! [m, i] = cummax (A, 'includenan'); %! m_exp = days ([3, 5, NaN, 4, 2; 3, 6, NaN, 9, 4; ... %! 3, 6, NaN, NaN, 4; 5, 6, NaN, NaN, 4]); %! i_exp = [1, 1, 1, 1, 1; 1, 2, 1, 2, 2; 1, 2, 1, 3, 2; 4, 2, 1, 3, 2]; %! assert_equal (isequaln (m, m_exp), true); %! assert_equal (i, i_exp); %!test %! [m, i] = cummax (A, 'includenan', 'reverse'); %! m_exp = days ([5, 6, NaN, NaN, 4; 5, 6, 4, NaN, 4; ... %! 5, 3, 4, NaN, 1; 5, 3, 4, 2, 0]); %! i_exp = [4, 2, 1, 3, 2; 4, 2, 4, 3, 2; 4, 4, 4, 3, 3; 4, 4, 4, 4, 4]; %! assert_equal (isequaln (m, m_exp), true); %! assert_equal (i, i_exp); %!test %! [m, i] = cummax (A, 'reverse'); %! m_exp = days ([5, 6, 4, 9, 4; 5, 6, 4, 9, 4; 5, 3, 4, 2, 1; 5, 3, 4, 2, 0]); %! i_exp = [4, 2, 4, 2, 2; 4, 2, 4, 2, 2; 4, 4, 4, 4, 3; 4, 4, 4, 4, 4]; %! assert_equal (m, m_exp); %! assert_equal (i, i_exp); %!test %! [m, i] = cummax (A); %! m_exp = days ([3, 5, NaN, 4, 2; 3, 6, 2, 9, 4; 3, 6, 2, 9, 4; 5, 6, 4, 9, ... %! 4]); %! i_exp = [1, 1, 1, 1, 1; 1, 2, 2, 2, 2; 1, 2, 2, 2, 2; 4, 2, 4, 2, 2]; %! assert_equal (isequaln (m, m_exp), true); %! assert_equal (i, i_exp); %!test %! [m, i] = cummax (A, 2, 'includenan'); %! m_exp = days ([3, 5, NaN, NaN, NaN; 2, 6, 6, 9, 9; 1, 3, 3, NaN, NaN; 5, ... %! 5, 5, 5, 5]); %! i_exp = [1, 2, 3, 3, 3; 1, 2, 2, 4, 4; 1, 2, 2, 4, 4; 1, 1, 1, 1, 1]; %! assert_equal (isequaln (m, m_exp), true); %! assert_equal (i, i_exp); %!test %! [m, i] = cummax (A, 2, 'includenan', 'reverse'); %! m_exp = days ([NaN, NaN, NaN, 4, 2; 9, 9, 9, 9, 4; ... %! NaN, NaN, NaN, NaN, 1; 5, 4, 4, 2, 0]); %! i_exp = [3, 3, 3, 4, 5; 4, 4, 4, 4, 5; 4, 4, 4, 4, 5; 1, 3, 3, 4, 5]; %! assert_equal (isequaln (m, m_exp), true); %! assert_equal (i, i_exp); %!test %! [m, i] = cummax (A, 2, 'reverse'); %! m_exp = days ([5, 5, 4, 4, 2; 9, 9, 9, 9, 4; 3, 3, 1, 1, 1; 5, 4, 4, 2, 0]); %! i_exp = [2, 2, 4, 4, 5; 4, 4, 4, 4, 5; 2, 2, 5, 5, 5; 1, 3, 3, 4, 5]; %! assert_equal (m, m_exp); %! assert_equal (i, i_exp); %!test %! [m, i] = cummax (A, 2); %! m_exp = days ([3, 5, 5, 5, 5; 2, 6, 6, 9, 9; 1, 3, 3, 3, 3; 5, 5, 5, 5, 5]); %! i_exp = [1, 2, 2, 2, 2; 1, 2, 2, 4, 4; 1, 2, 2, 2, 2; 1, 1, 1, 1, 1]; %! assert_equal (m, m_exp); %! assert_equal (i, i_exp); %!test %! [~, i_lin] = cummax (A, 'includenan', 'linear'); %! assert_equal (isequaln (cummax (A, 'includenan'), A(i_lin)), true); %!test %! [~, i_lin] = cummax (A, 'includenan', 'reverse', 'linear'); %! assert_equal (isequaln (cummax (A, 'includenan', 'reverse'), A(i_lin)), ... %! true); %!test %! [~, i_lin] = cummax (A, 'reverse', 'linear'); %! assert_equal (isequaln (cummax (A, 'reverse'), A(i_lin)), true); %!test %! [~, i_lin] = cummax (A, 'linear'); %! assert_equal (isequaln (cummax (A), A(i_lin)), true); %!test %! [~, i_lin] = cummax (A, 2, 'includenan', 'linear'); %! assert_equal (isequaln (cummax (A, 2, 'includenan'), A(i_lin)), true); %!test %! [~, i_lin] = cummax (A, 2, 'includenan', 'reverse', 'linear'); %! assert_equal (isequaln (cummax (A, 2, 'includenan', 'reverse'), ... %! A(i_lin)), true); %!test %! [~, i_lin] = cummax (A, 2, 'reverse', 'linear'); %! assert_equal (isequaln (cummax (A, 2, 'reverse'), A(i_lin)), true); %!test %! [~, i_lin] = cummax (A, 2, 'linear'); %! assert_equal (isequaln (cummax (A, 2), A(i_lin)), true); %!shared x %! x = days (randi ([-10, 10], 3, 4, 5, 2)); %!test %! [m, i] = cummax (x, [2, 3], 'linear'); %! assert_equal (m, x(i)); %!test %! [m, i] = cummax (x, [1, 3], 'linear'); %! assert_equal (m, x(i)); %!test %! [m, i] = cummax (x, [2, 4], 'linear'); %! assert_equal (m, x(i)); %!test %! [m, i] = cummax (x, [1, 4], 'linear'); %! assert_equal (m, x(i)); %!test %! [m, i] = cummax (x, [1, 2, 3], 'linear'); %! assert_equal (m, x(i)); %!test %! [m, i] = cummax (x, [2, 3, 4], 'linear'); %! assert_equal (m, x(i)); %!test %! D = hours(8) + minutes(29:31) + seconds(1.23); %! D.Format = 'hh:mm:ss.SS'; %! assert_equal (dispstrings (D), {'08:29:01.23', '08:30:01.23', ... %! '08:31:01.23'}); %! Y = dispstrings (floor (D)); %! assert_equal (Y, {'08:29:01.00', '08:30:01.00', '08:31:01.00'}); %! Y = dispstrings (floor (D, 'seconds')); %! assert_equal (Y, {'08:29:01.00', '08:30:01.00', '08:31:01.00'}); %! Y = dispstrings (floor (D, 'minutes')); %! assert_equal (Y, {'08:29:00.00', '08:30:00.00', '08:31:00.00'}); %! Y = dispstrings (floor (D, 'hours')); %! assert_equal (Y, {'08:00:00.00', '08:00:00.00', '08:00:00.00'}); %!test %! D = days (3) + hours (6.5); %! assert_equal (floor (D, 'days'), days (3)); %!test %! D = hours(8) + minutes(29:31) + seconds(1.23); %! D.Format = 'hh:mm:ss.SS'; %! assert_equal (dispstrings (D), {'08:29:01.23', '08:30:01.23', ... %! '08:31:01.23'}); %! Y = dispstrings (ceil (D)); %! assert_equal (Y, {'08:29:02.00', '08:30:02.00', '08:31:02.00'}); %! Y = dispstrings (ceil (D, 'seconds')); %! assert_equal (Y, {'08:29:02.00', '08:30:02.00', '08:31:02.00'}); %! Y = dispstrings (ceil (D, 'minutes')); %! assert_equal (Y, {'08:30:00.00', '08:31:00.00', '08:32:00.00'}); %! Y = dispstrings (ceil (D, 'hours')); %! assert_equal (Y, {'09:00:00.00', '09:00:00.00', '09:00:00.00'}); %!test %! D = days (3) + hours (6.5); %! assert_equal (ceil (D, 'days'), days (4)); %!test %! D = hours(8) + minutes(29:31) + seconds(1.23); %! D.Format = 'hh:mm:ss.SS'; %! assert_equal (dispstrings (D), {'08:29:01.23', '08:30:01.23', ... %! '08:31:01.23'}); %! Y = dispstrings (round (D)); %! assert_equal (Y, {'08:29:01.00', '08:30:01.00', '08:31:01.00'}); %! Y = dispstrings (round (D, 'seconds')); %! assert_equal (Y, {'08:29:01.00', '08:30:01.00', '08:31:01.00'}); %! Y = dispstrings (round (D, 'minutes')); %! assert_equal (Y, {'08:29:00.00', '08:30:00.00', '08:31:00.00'}); %! Y = dispstrings (round (D, 'hours')); %! assert_equal (Y, {'08:00:00.00', '09:00:00.00', '09:00:00.00'}); %!test %! D = days (3) + hours (6.5); %! assert_equal (round (D, 'days'), days (3)); %!assert_equal (sign (days ([-1, 0, 1, NaN])), [-1, 0, 1, NaN]) %!assert_equal (sign (seconds ([-2:2] .* ones (5))), repmat ([-1, -1, 0, 1, ... %! 1], 5, 1)) ################################################################################ ## ** Statistical Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'bounds' 'center' 'histc' 'iqr' ## ## 'kendal' 'kurtosis' 'mad' 'mape' ## ## 'mean' 'median' 'mode' 'prctile' ## ## 'quantile' 'range' 'rmse' 'skewness' ## ## 'spearman' 'statistics' 'std' 'var' ## ## ## ################################################################################ %!test %! [s, l] = bounds (days (1:10)); %! assert_equal ([s, l], days ([1, 10])); %!test %! [s, l] = bounds (days ([10:-1:1]')); %! assert_equal ([s, l], days ([1, 10])); %!assert_equal (bounds (days (magic (3))), days ([3, 1, 2])) %!assert_equal (bounds (days (magic (3)), 2), days ([1; 3; 2])) %!test %! x = days (magic (3)); %! x(2,3) = NaN; %! assert_equal (bounds (x), days ([3, 1, 2])); %! assert_equal (bounds (x, "omitnan"), days ([3, 1, 2])); %! assert_equal (isequaln (bounds (x, "includenan"), days ([3, 1, NaN])), true); %! assert_equal (bounds (x, 2), days ([1; 3; 2])); %! assert_equal (bounds (x, 2, "omitnan"), days ([1; 3; 2])); %! assert_equal (isequaln (bounds (x, 2, "includenan"), days ([1; NaN; 2])), ... %! true); %!test %! x = reshape (days (1:27), [3, 3, 3]); %! [s, l] = bounds (x, 3); %! assert_equal (s, x(:,:,1)); %! assert_equal (l, x(:,:,3)); %!test %! [s, l] = bounds (x, [2, 3]); %! assert_equal (s, x(:,1,1)); %! assert_equal (l, x(:,3,3)); %!test %! [s, l] = bounds (x, [1, 3]); %! assert_equal (s, x(1,:,1)); %! assert_equal (l, x(3,:,3)); %!test %! [s, l] = bounds (x, [1, 2, 3]); %! assert_equal (s, days (1)); %! assert_equal (l, days (27)); %!test %! [s, l] = bounds (x, "all"); %! assert_equal (s, days (1)); %! assert_equal (l, days (27)); %!test %! x(3) = NaN; %! [s, l] = bounds (x, "all"); %! assert_equal (s, days (1)); %! assert_equal (l, days (27)); %!test %! [s,l] = bounds (x, "all", "omitnan"); %! assert_equal (s, days (1)); %! assert_equal (l, days (27)); %!test %! [s,l] = bounds (x, "all", "includenan"); %! assert_equal (isnan (s), true); %! assert_equal (isnan (l), true); %!test %! [s,l] = bounds (x, "includenan"); %! assert_equal (isequaln (s, min (x, [], "includenan")), true); %! assert_equal (isequaln (l, max (x, [], "includenan")), true); %!assert_equal (center (days ([1, 2, 3])), days ([-1,0,1])) %!assert_equal (center (days (ones (3, 2, 0, 2))), days (zeros (3, 2, 0,2 ))) %!assert_equal (center (days (magic (3))), days ([3, -4, 1; -2, 0, 2; -1, 4, ... %! -3])) %!assert_equal (center (days ([1, 2, 3; 6, 5, 4]), 2), days ([-1, 0, 1; 1, ... %! 0, -1])) %!assert_equal (center (days (1), 3), days (0)) %!assert_equal (center (repmat (days ([1, 2, 3]), 5, 1)), days (zeros (5, 3))) %!assert_equal (center (repmat (days ([1, 2, 3]), 5, 1), ... %! 2), repmat (days ([-1, 0, 1]), 5, 1)) %!test %! out = center (days ([1, 3, 2; 2, NaN, 1])); %! assert_equal (isequaln (out, days ([-0.5, NaN, 0.5; 0.5, NaN, -0.5])), true); %!test %! out = center (days ([1, 3, 2; 2, NaN, 1]), 'omitnan'); %! assert_equal (isequaln (out, days ([-0.5, 0, 0.5; 0.5, NaN, -0.5])), true); %!test %! out = center (days ([1, 3, 2; 2, NaN, 1]), 2, 'omitnan'); %! assert_equal (isequaln (out, days ([-1, 1, 0; 0.5, NaN, -0.5])), true); %!test %! x = linspace (days (0), days (10), 1001); %! n = histc (x, 0:10); %! assert_equal (n, [repmat(100, 1, 10), 1]); %!test %! x = repmat (linspace (days (0), days (10), 1001), [2, 1, 3]); %! n = histc (x, 0:10, 2); %! assert_equal (n, repmat ([repmat(100, 1, 10), 1], [2, 1, 3])); %!assert_equal (iqr (days (17)), duration) %!assert_equal (iqr (days (17), 1), duration) %!assert_equal (iqr (days (17), 4), duration) %!assert_equal (iqr (days ([1:3])), days (1.5)) %!assert_equal (iqr (days ([1:4])), days (2)) %!assert_equal (iqr (days ([1:5])), days (2.5)) %!assert_equal (iqr (days ([1:10])), days (5)) %!assert_equal (iqr (days ([1:10]')), days (5)) %!assert_equal (iqr (days ([1:10]), 2), days (5)) %!assert_equal (iqr (days ([1:10]), 1), days (zeros (1, 10))) %!assert_equal (iqr (days ([1:10]), 3), days (zeros (1, 10))) %!assert_equal (iqr (days ([1:5; 2:6]), [1, 2]), days (3)) %!assert_equal (iqr (days ([1:5; 2:6]), 'all'), days (3)) %!test %! x = reshape (days (1:6), [1, 2, 3]); %! assert_equal (iqr (x), days (ones (1, 1, 3))); %! assert_equal (iqr (x, 1), days (zeros (1, 2, 3))); %! assert_equal (iqr (x, 2), days (ones (1, 1, 3))); %! assert_equal (iqr (x, 3), days ([3, 3])); %!test %! x = magic (4); x = cat (3,x, 2*x, 3*x); x = cat (4, x, 2*x); %! y = cat (3, 8*[1, 1, 1, 1], 16*[1, 1, 1, 1], 24*[1, 1, 1, 1]); %! x = days (x); y = days (y); %! assert_equal (iqr (x), cat (4, y, 2*y)); %! assert_equal (iqr (x, 1), cat (4, y, 2*y)); %! y = cat (3, 4*[3, 1, 1, 3].', 8*[3, 1, 1, 3].', 12*[3, 1, 1, 3].'); %! y = days (y); %! assert_equal (iqr (x, 2), cat (4, y, 2*y)); %! y = [24, 3, 4.5, 19.5; 7.5, 16.5, 15, 12; 13.5, 10.5, 9, 18; 6, 21, 22.5, ... %! 1.5]; %! y = days (y); %! assert_equal (iqr (x, 3), cat (4, y, 2*y)); %! y = [16, 2, 3, 13; 5, 11, 10, 8; 9, 7, 6, 12; 4, 14, 15, 1]; %! y = days (y); %! assert_equal (iqr (x, 4), cat (3, y, 2*y, 3*y)); %! assert_equal (iqr (x, 5), days (zeros (size (x)))); %!assert_equal (iqr (days (17), [1, 8]), duration) %!assert_equal (iqr (days ([1, 2, 5; 2, 5, 6]), [1, 2]), days (3)) %!test %! x = days (cat (3, [1, 2, 5; 2, 5, 6], [1, 2, 5; 2, 5, 6])); %! assert_equal (iqr (x, [1, 2]), days (cat (3, 3, 3))); %! assert_equal (iqr (x, [1, 2]'), days (cat (3, 3, 3))); %!test %! x = [-1; 0; 0; 0; 1]; %! y = days ([x, 2*x]); %! assert_equal (kurtosis (y), [2.5, 2.5], sqrt (eps)); %!assert_equal (kurtosis (days ([-3, 0, 1])), kurtosis (days ([-1, 0, 3]))) %!assert_equal (kurtosis (days (ones (3, 5))), NaN (1, 5)) %!assert_equal (kurtosis (days (1), [], 3), NaN) %!test %! clear x; %! x(:,:,1) = [0.5377, 0.3188, 3.5784; 1.8339, -1.3077, 2.7694; ... %! -2.2588, -0.4336, -1.3499; 0.8622, 0.3426, 3.0349]; %! x(:,:,2) = [0.7254, -0.1241, 0.6715; -0.0631, 1.4897, -1.2075; ... %! 0.7147 1.4090 0.7172; -0.2050, 1.4172, 1.6302]; %! y = kurtosis (days (x)); %! assert_equal (class (y), 'double'); %! assert_equal (y(:,:,1), [2.1350, 1.7060, 2.2789], 1e-4); %! assert_equal (y(:,:,2), [1.0542, 2.3278, 2.0996], 1e-4); %! y = kurtosis (days (x), 1, 2); %! assert_equal (y(:,:,1), [1.5; 1.5; 1.5; 1.5], 8 * eps); %! assert_equal (y(:,:,2), [1.5; 1.5; 1.5; 1.5], 8 * eps); %! y = kurtosis (days (x), 1, 3); %! assert_equal (y, ones (4, 3), 8 * eps); %!test %! x(:,:,1) = [0.5377, 0.3188, 3.5784; 1.8339, -1.3077, 2.7694; ... %! -2.2588, -0.4336, -1.3499; 0.8622, 0.3426, 3.0349]; %! x(:,:,2) = [0.7254, -0.1241, 0.6715; -0.0631, 1.4897, -1.2075; ... %! 0.7147 1.4090 0.7172; -0.2050, 1.4172, 1.6302]; %! y = kurtosis (days (x), 1, "all"); %! assert_equal (y, 2.8029, 1e-4); %! y = kurtosis (days (x), 1, [1, 2]); %! assert_equal (y(:,:,1), 1.9345, 1e-4); %! assert_equal (y(:,:,2), 2.5877, 1e-4); %! y = kurtosis (days (x), 1, [2, 3]); %! assert_equal (y, [3.8457; 1.4306; 1.7094; 2.3378], 1e-4); %!assert_equal (mad (days (123)), duration) %!assert_equal (isnan (mad (days (Inf))), true) %!assert_equal (mad (days ([3, Inf])), days (Inf)) %!assert_equal (mad (days ([0, 0, 1, 2, 100])), days (31.76)) %!assert_equal (mad (days ([0, 0, 1, 2, 100]')), days (31.76)) %!assert_equal (mad (days ([0, 0, 1, 2, 100]), 1), days (1)) %!assert_equal (mad (days ([0, 0, 1, 2, 100]'), 1), days (1)) %!assert_equal (mad (days (magic (4))), days ([4, 4, 4, 4])) %!assert_equal (mad (days (magic (4)), [], 2), days ([6; 2; 2; 6])) %!assert_equal (mad (days (magic (4)), 1), days ([2.5, 3.5, 3.5, 2.5])) %!assert_equal (mad (days (magic (4)), 1, 2), days ([5.5; 1.5; 1.5; 5.5])) %!assert_equal (mad (days (magic (4)), 0, 3), days (zeros (4))) %!assert_equal (mad (days (magic (4)), 1, 3), days (zeros (4))) %!assert_equal (mad (days (cat (3, magic (4), ... %! magic (4)))), days (4 * ones (1, 4, 2))) %!assert_equal (mad (days (magic (4)), 0, "all"), days (4)) %!assert_equal (mad (days (magic (4)), 1, "all"), days (4)) %!assert_equal (mad (days (magic (4)), 0, [1 2]), days (4)) %!assert_equal (mad (days (magic (4)), 0, [1 3]), mad (days (magic (4)), 0, 1)) %!assert_equal (mad (days (magic (4)), 0, [1 2 3]), days (4)) %!assert_equal (mad (days (magic (4)), 1, [1 2]), days (4)) %!assert_equal (mad (days (magic (4)), 1, [1 3]), mad (days (magic (4)), 1, 1)) %!assert_equal (mad (days (magic (4)), 1, [1 2 3]), days (4)) %!assert_equal (mad (days (magic (4)), 0, [3 4 99]), days (zeros (4))) %!assert_equal (mad (days (magic (4)), 1, [3 4 99]), days (zeros (4))) %!assert_equal (isequaln (mad (days (NaN)), days (NaN)), true) %!assert_equal (isequaln (mad (days (NaN (2))), days (NaN (1, 2))), true) %!assert_equal (mad (days ([1, 2; 3, NaN])), days ([1, 0])) %!assert_equal (isequaln (mad (days ([1, 2; 3, NaN]), [], 1), days ([1, ... %! 0])), true) %!assert_equal (isequaln (mad (days ([1, NaN; 3, NaN]), [], 1), days ([1, ... %! NaN])), true) %!assert_equal (mad (days ([1, NaN; 3, NaN]), [], 2), days ([0; 0])) %!test %! F = days ([2, 11, 6]); %! A = days ([3, 10, 8]); %! assert_equal (mape (F, A), 22.7778, 1e-4); %! assert_equal (mape (F, A, 1), [33.3333, 10.0000, 25.0000], 1e-4); %! assert_equal (mape (F, A, 2), 22.7778, 1e-4); %! assert_equal (mape (F, A, 3), [33.3333, 10.0000, 25.0000], 1e-4); %! assert_equal (mape (F, A, 'all'), 22.7778, 1e-4); %!test %! F = days ([3; 7; 4; NaN]); %! A = days ([4; 0; 6; 4]); %! assert_equal (mape (F, A), NaN); %! assert_equal (mape (F, A, 'omitnan'), Inf); %! assert_equal (mape (F, A, 'omitzero'), NaN); %! assert_equal (mape (F, A, 'omitnan', 'omitzero'), 29.1667, 1e-4); %! assert_equal (mape (F, A, 1, 'omitnan', 'omitzero'), 29.1667, 1e-4); %! E = mape (F, A, 2, 'omitnan', 'omitzero'); %! assert_equal (E, [25; NaN; 33.3333; NaN], 1e-4); %!test %! F = days ([17, 21; 1, 5; 13, 17]); %! A = days ([18, 19; 5, 4; 15, 13]); %! assert_equal (mape (F, A), [32.9630, 22.0985], 1e-4); %! assert_equal (mape (F, A, 1), [32.9630, 22.0985], 1e-4); %! assert_equal (mape (F, A, 2), [8.0409; 52.5000; 22.0513], 1e-4); %! assert_equal (mape (F, A, 'all'), 27.5307, 1e-4); %! assert_equal (mape (F, A, [1, 2]), 27.5307, 1e-4); %!test %! F = days ([3, 5; -1, 2]); %! F(:,:,2) = days ([4, 2; 7, -4]); %! A = days ([6, 5; 0, 3]); %! E = mape (F, A, [1, 2]); %! assert_equal (E(:,:,1), Inf); %! assert_equal (E(:,:,2), Inf); %! E = mape (F, A, [1, 2], 'omitzero'); %! assert_equal (E(:,:,1), 27.7778, 1e-4); %! assert_equal (E(:,:,2), 108.8889, 1e-4); %! assert_equal (mape (F, A, [1, 3]), [Inf, 81.6667], 1e-4); %! assert_equal (mape (F, A, [1, 3], 'omitzero'), [41.6667, 81.6667], 1e-4); %! assert_equal (mape (F, A, [2, 3]), [35.8333; Inf], 1e-4); %! assert_equal (mape (F, A, [2, 3], 'omitzero'), [35.8333; 133.3333], 1e-4); %!assert_equal (mape ([1:4], zeros (1, 4)), Inf) %!assert_equal (mape ([1:4], zeros (1, 4), "omitzero"), NaN) %!test %! F = days ([2, 11, 6]); %! A = days ([3, 10, 8]); %! W = [1, 2, 1]; %! assert_equal (mape (F, A, 'Weights', W), 19.5833, 1e-4); %! assert_equal (mape (F, A, 1, 'Weights', W), [33.3333, 10, 25], 1e-4); %! assert_equal (mape (F, A, 2, 'Weights', W), 19.5833, 1e-4); %! assert_equal (mape (F, A, 3, 'Weights', W), [33.3333, 10, 25], 1e-4); %!test %! F = days ([3; 7; 4; NaN]); %! A = days ([4; 0; 6; 4]); %! W = [1, 2, 1, 2]; %! assert_equal (mape (F, A, 'Weights', W), NaN); %! assert_equal (mape (F, A, 'omitnan', 'Weights', W), Inf); %! assert_equal (mape (F, A, 'omitzero', 'Weights', W), NaN); %! assert_equal (mape (F, A, 'omitnan', 'omitzero', 'Weights', W), 29.1667, ... %! 1e-4); %! assert_equal (mape (F, A, 1, 'omitnan', 'omitzero', 'Weights', W), ... %! 29.1667, 1e-4); %!test %! F = days ([3; 7; 4; NaN]); %! A = days ([4; 0; 6; 4]); %! W = [1; 2; 1; 2]; %! E = mape (F, A, 2, 'omitnan', 'omitzero', 'Weights', W); %! assert_equal (E, [25; NaN; 33.3333; NaN], 1e-4); %! E = mape (F, A, 3, 'omitnan', 'omitzero', 'Weights', W); %! assert_equal (E, [25; NaN; 33.3333; NaN], 1e-4); %!test %! F = days ([3, 5; -1, 2]); %! F(:,:,2) = days ([4, 2; 7, -4]); %! A = days ([6, 5; 0, 3]); %! W = [1, 1; 1, 1]; %! Ew = mape (F, A, [1, 2], 'omitzero', 'Weights', W); %! E = mape (F, A, [1, 2], 'omitzero'); %! assert_equal (Ew, E); %! Ew = mape (F, A, [1, 3], 'omitzero', 'Weights', W); %! E = mape (F, A, [1, 3], 'omitzero'); %! assert_equal (Ew, E); %! Ew = mape (F, A, [2, 3], 'omitzero', 'Weights', W); %! E = mape (F, A, [2, 3], 'omitzero'); %! assert_equal (Ew, E); %!test %! F = days ([3, 5; -1, 2]); %! F(:,:,2) = days ([4, NaN; 7, -4]); %! A = days ([6, 5; 2, 3]); %! W = [1, 1; 1, 1]; %! Ew = mape (F, A, [1, 2], 'omitnan', 'Weights', W); %! E = mape (F, A, [1, 2], 'omitnan'); %! assert_equal (Ew, E); %! Ew = mape (F, A, [1, 3], 'omitnan','Weights', W); %! E = mape (F, A, [1, 3], 'omitnan'); %! assert_equal (Ew, E); %! Ew = mape (F, A, [2, 3], 'omitnan','Weights', W); %! E = mape (F, A, [2, 3], 'omitnan'); %! assert_equal (Ew, E); %!error ... %! mape (days ([1, 2, 3]), 1:3) %!error ... %! mape (1:3, days ([1, 2, 3])) %!assert_equal (mean (days (magic (3)), 1), days ([5, 5, 5])) %!assert_equal (mean (days (magic (3)), 2), days ([5; 5; 5])) %!test %! x = days ([-10:10]); %! y = [x;x+5;x-5]; %! assert_equal (mean (x), days (0)); %! assert_equal (mean (y, 2), days ([0, 5, -5]')); %! assert_equal (mean (y, 'all'), days (0)); %! y(2,4) = NaN; %! assert_equal (all (mean (y', 'omitnan') - days ([0, 5.35, -5]) < 1e-14), ... %! true); %! z = y + 20; %! assert_equal (isequaln (mean (z, "all"), days (NaN)), true); %! assert_equal (isequaln (mean (z, "all", "includenan"), days (NaN)), true); %! assert_equal (days (mean (z, "all", "omitnan")), 20.03225806451613, 4e-14); %!test %! x = repmat ([1:20;6:25], [5 2 6 3]); %! assert_equal (size (mean (x, [3 2])), [10 1 1 3]); %! assert_equal (size (mean (x, [1 2])), [1 1 6 3]); %! assert_equal (size (mean (x, [1 2 4])), [1 1 6]); %! assert_equal (size (mean (x, [1 4 3])), [1 40]); %! assert_equal (size (mean (x, [1 2 3 4])), [1 1]); ## Test exceeding dimensions %!assert_equal (mean (days (ones (2, 2)), 3), days (ones (2, 2))) %!assert_equal (mean (days (ones (2, 2, 2)), 99), days (ones (2, 2, 2))) %!assert_equal (mean (days (magic (3)), 3), days (magic (3))) %!assert_equal (mean (days (magic (3)), [1, 3]), days ([5, 5, 5])) %!assert_equal (mean (days (magic (3)), [1, 99]), days ([5, 5, 5])) %!test %! x = days ([1, 1; 7, 9; 1, 9; 1, 9; 6, 2]); %! w = [1; 2; 1; 2; 3]; %! assert_equal (days (mean (x, 'Weights', w)), [4, 5.7778], 1e-4); %! assert_equal (days (mean (x, 'Weights', w')), [4, 5.7778], 1e-4); %! assert_equal (days (mean (x, 1, 'Weights', w)), [4, 5.7778], 1e-4); %! assert_equal (days (mean (x, 1, 'Weights', w')), [4, 5.7778], 1e-4); %! assert_equal (days (mean (x', 2, 'Weights', w)), [4; 5.7778], 1e-4); %! assert_equal (days (mean (x', 2, 'Weights', w')), [4; 5.7778], 1e-4); %!test %! x = days ([1, 1; 7, 9; 1, 9; 1, 9; 6, 2]); %! x = reshape (x', [1, 2, 5]); %! w = [1, 2, 1, 2, 3]; %! assert_equal (size (x)([1, 3]), size (w)); %! assert_equal (days (mean (x, [1, 3], 'Weights', w)), [4, 5.7778], 1e-4); %!test %! x = days ([1, 1; 7, 9; 1, 9; 1, 9; 6, 2]); %! x = reshape (x', [1, 2, 5]); %! w = ones (1, 2, 5); %! assert_equal (size (x), size (w)); %! assert_equal (days (mean (x, 'all', 'Weights', w)), days (mean (x, ... %! 'all')), 1e-4); %! assert_equal (days (mean (x, [1, 2, 3], 'Weights', w)), days (mean (x, ... %! [1, 2, 3])), 1e-4); %!assert_equal (median (days (1)), days (1)) %!assert_equal (median (days ([1, 2, 3])), days (2)) %!assert_equal (median (days ([1, 2, 3]')), days (2)) %!assert_equal (median (days (cat (3, 3, 1, 2))), days (2)) %!assert_equal (median (days ([3, 1, 2])), days (2)) %!assert_equal (median (days ([2, 4, 6, 8])), days (5)) %!assert_equal (median (days ([8, 2, 6, 4])), days (5)) %!assert_equal (median (days ([1, 2]), 3), days ([1, 2])) %!test %! x = days ([1, 2, 3, 4, 5, 6]); %! x2 = x'; %! y = days ([1, 2, 3, 4, 5, 6, 7]); %! y2 = y'; %! %! assert_equal (median (x) == median (x2) && median (x) == days (3.5), true); %! assert_equal (median (y) == median (y2) && median (y) == days (4), true); %! assert_equal (median ([x2, 2 * x2]), days ([3.5, 7])); %! assert_equal (median ([y2, 3 * y2]), days ([4, 12])); %!test %! x = repmat (days ([2 2.1 2.2 2 NaN; 3 1 2 NaN 5; 1 1.1 1.4 5 3]), [1, 1, 4]); %! y = repmat (days ([2 1.1 2 NaN NaN]), [1, 1, 4]); %! assert_equal (isequaln (median (x), y), true); %! assert_equal (isequaln (median (x, 1), y), true); %! y = repmat (days ([2, 1.1, 2, 3.5, 4]), [1, 1, 4]); %! assert_equal (median (x, "omitnan"), y); %! assert_equal (median (x, 1, "omitnan"), y); %! y = repmat (days ([2.05; 2.5; 1.4]), [1, 1, 4]); %! ## Averaging two stored values and building one from a day figure differ in %! ## the last bit, as they do in MATLAB, which stores milliseconds too. %! assert_equal (milliseconds (median (x, 2, "omitnan")), ... %! milliseconds (y), 1e-6); %! y = repmat (days ([NaN; NaN; 1.4]), [1, 1, 4]); %! assert_equal (isequaln (median (x, 2), y), true); %! assert_equal (isequaln (median (x, "all"), NaN), true); %! assert_equal (isequaln (median (x, "all", "omitnan"), 2), true); %!assert_equal (median (cat (3, 3, 1, NaN, 2), "omitnan"), 2) %!assert_equal (median (cat (3, 3, 1, NaN, 2), 3, "omitnan"), 2) %!test %! [m, f, c] = mode (days (toeplitz (1:5))); %! assert_equal (m, days ([1, 2, 2, 2, 1])); %! assert_equal (f, [1, 2, 2, 2, 1]); %! assert_equal (c, {days([1;2;3;4;5]), days(2), days([2;3]), days(2), ... %! days([1;2;3;4;5])}); %!test %! [m, f, c] = mode (days (toeplitz (1:5)), 2); %! assert_equal (m, days ([1; 2; 2; 2;1 ])); %! assert_equal (f, [1; 2; 2; 2; 1]); %! assert_equal (c, ... %! {days([1;2;3;4;5]); days(2); days([2;3]); days(2); days([1;2;3;4;5])}); %!assert_equal (mode (days ([2,3,1,2,3,4]), 1), days ([2,3,1,2,3,4])) %!assert_equal (mode (days ([2,3,1,2,3,4]), 2), days (2)) %!assert_equal (mode (days ([2,3,1,2,3,4])), days (2)) %!assert_equal (mode (days ([2;3;1;2;3;4]), 1), days (2)) %!assert_equal (mode (days ([2;3;1;2;3;4]), 2), days ([2;3;1;2;3;4])) %!assert_equal (mode (days ([2;3;1;2;3;4])), days (2)) %!shared %!shared x %! x(:,:,1) = toeplitz (1:3); %! x(:,:,2) = circshift (toeplitz (1:3), 1); %! x(:,:,3) = circshift (toeplitz (1:3), 2); %! x = days (x); %!test %! [m, f, c] = mode (x, 1); %! assert_equal (reshape (m, [3, 3]), days ([1, 1, 1; 2, 2, 2; 1, 1, 1])); %! assert_equal (reshape (f, [3, 3]), [1, 1, 1; 2, 2, 2; 1, 1, 1]); %! c = reshape (c, [3, 3]); %! assert_equal (c{1}, days ([1; 2; 3])); %! assert_equal (c{2}, days (2)); %! assert_equal (c{3}, days ([1; 2; 3])); %!test %! [m, f, c] = mode (x, 2); %! assert_equal (reshape (m, [3, 3]), days ([1, 1, 2; 2, 1, 1; 1, 2, 1])); %! assert_equal (reshape (f, [3, 3]), [1, 1, 2; 2, 1, 1; 1, 2, 1]); %! c = reshape (c, [3, 3]); %! assert_equal (c{1}, days ([1; 2; 3])); %! assert_equal (c{2}, days (2)); %! assert_equal (c{3}, days ([1; 2; 3])); %!test %! [m, f, c] = mode (x, 3); %! assert_equal (reshape (m, [3, 3]), days ([1, 2, 1; 1, 2, 1; 1, 2, 1])); %! assert_equal (reshape (f, [3, 3]), [1, 2, 1; 1, 2, 1; 1, 2, 1]); %! c = reshape (c, [3, 3]); %! assert_equal (c{1}, days ([1; 2; 3])); %! assert_equal (c{2}, days ([1; 2; 3])); %! assert_equal (c{3}, days ([1; 2; 3])); %!shared %!test %! pct = 50; %! q = prctile (days (1:4), pct); %! qa = days (2.5); %! assert_equal (q, qa); %! q = prctile (days (1:4), pct, 1); %! qa = days ([1, 2, 3, 4]); %! assert_equal (q, qa); %! q = prctile (days (1:4), pct, 2); %! qa = days (2.5); %! assert_equal (q, qa); %!test %! pct = [50 75]; %! q = prctile (days (1:4), pct); %! qa = days ([2.5, 3.5]); %! assert_equal (q, qa); %! q = prctile (days (1:4), pct, 1); %! qa = days ([1, 2, 3, 4; 1, 2, 3, 4]); %! assert_equal (q, qa); %! q = prctile (days (1:4), pct, 2); %! qa = days ([2.5, 3.5]); %! assert_equal (q, qa); %!test %! pct = 50; %! x = [0.1126, 0.1148, 0.0521, 0.2364, 0.1393 %! 0.1718, 0.7273, 0.2041, 0.4531, 0.1585 %! 0.2795, 0.7978, 0.3296, 0.5567, 0.7307 %! 0.4288, 0.8753, 0.6477, 0.6287, 0.8165 %! 0.9331, 0.9312, 0.9635, 0.7796, 0.8461]; %! tol = 0.0001; %! q = days (prctile (days (x), pct, 1)); %! qa = [0.2795, 0.7978, 0.3296, 0.5567, 0.7307]; %! assert_equal (q, qa, tol); %! q = days (prctile (days (x), pct, 2)); %! qa = [0.1148; 0.2041; 0.5567; 0.6477; 0.9312]; %! assert_equal (q, qa, tol); %!test %! pct = 50; %! tol = 0.0001; %! x = [0.1126, 0.1148, 0.0521, 0.2364, 0.1393 %! 0.1718, 0.7273, 0.2041, 0.4531, 0.1585 %! 0.2795, 0.7978, 0.3296, 0.5567, 0.7307 %! 0.4288, 0.8753, 0.6477, 0.6287, 0.8165 %! 0.9331, 0.9312, 0.9635, 0.7796, 0.8461]; %! x(5,5) = Inf; %! q = days (prctile (days (x), pct, 1)); %! qa = [0.2795, 0.7978, 0.3296, 0.5567, 0.7307]; %! assert_equal (q, qa, tol); %! x(5,5) = -Inf; %! q = days (prctile (days (x), pct, 1)); %! qa = [0.2795, 0.7978, 0.3296, 0.5567, 0.1585]; %! assert_equal (q, qa, tol); %! x(1,1) = Inf; %! q = days (prctile (days (x), pct, 1)); %! qa = [0.4288, 0.7978, 0.3296, 0.5567, 0.1585]; %! assert_equal (q, qa, tol); %!test %! pct = 50; %! tol = 0.0001; %! x = [0.1126, 0.1148, 0.0521, 0.2364, 0.1393 %! 0.1718, 0.7273, 0.2041, 0.4531, 0.1585 %! 0.2795, 0.7978, 0.3296, 0.5567, 0.7307 %! 0.4288, 0.8753, 0.6477, 0.6287, 0.8165 %! 0.9331, 0.9312, 0.9635, 0.7796, 0.8461]; %! x(3,3) = Inf; %! q = days (prctile (days (x), pct, 1)); %! qa = [0.2795, 0.7978, 0.6477, 0.5567, 0.7307]; %! assert_equal (q, qa, tol); %! q = days (prctile (days (x), pct, 2)); %! qa = [0.1148; 0.2041; 0.7307; 0.6477; 0.9312]; %! assert_equal (q, qa, tol); %!test %! pct = 50; %! tol = 0.0001; %! x = [0.1126, 0.1148, 0.0521, 0.2364, 0.1393 %! 0.1718, 0.7273, 0.2041, 0.4531, 0.1585 %! 0.2795, 0.7978, 0.3296, 0.5567, 0.7307 %! 0.4288, 0.8753, 0.6477, 0.6287, 0.8165 %! 0.9331, 0.9312, 0.9635, 0.7796, 0.8461]; %! x(5,5) = NaN; %! q = days (prctile (days (x), pct, 2)); %! qa = [0.1148; 0.2041; 0.5567; 0.6477; 0.9322]; %! assert_equal (q, qa, tol); %! x(1,1) = NaN; %! q = days (prctile (days (x), pct, 2)); %! qa = [0.1270; 0.2041; 0.5567; 0.6477; 0.9322]; %! assert_equal (q, qa, tol); %! x(3,3) = NaN; %! q = days (prctile (days (x), pct, 2)); %! qa = [0.1270; 0.2041; 0.6437; 0.6477; 0.9322]; %! assert_equal (q, qa, tol); %!assert_equal (prctile (days ([1:10]), 1, 3), days ([1:10])) %!test %! p = 0.50; %! q = quantile (days (1:4), p); %! qa = days (2.5); %! assert_equal (q, qa); %! q = quantile (days (1:4), p, 1); %! qa = days ([1, 2, 3, 4]); %! assert_equal (q, qa); %! q = quantile (days (1:4), p, 2); %! qa = days (2.5); %! assert_equal (q, qa); %!test %! p = [0.50 0.75]; %! q = quantile (days (1:4), p); %! qa = days ([2.5 3.5]); %! assert_equal (q, qa); %! q = quantile (days (1:4), p, 1); %! qa = days ([1, 2, 3, 4; 1, 2, 3, 4]); %! assert_equal (q, qa); %! q = quantile (days (1:4), p, 2); %! qa = days ([2.5 3.5]); %! assert_equal (q, qa); %!test %! p = 0.5; %! x = days (sort (rand (11))); %! q = quantile (x, p); %! assert_equal (q, x(6,:)); %! x = x.'; %! q = quantile (x, p, 2); %! assert_equal (q, x(:,6)); %!test %! sx = [2, 3, 4]; %! x = days (rand (sx)); %! dim = 2; %! p = 0.5; %! yobs = quantile (x, p, dim); %! yexp = median (x, dim); %! assert_equal (yobs, yexp); %!assert_equal (range (days (1:10)), days (9)) %!assert_equal (range (days (magic (3))), days ([5, 8, 5])) %!assert_equal (range (days (magic (3)), 2), days ([7; 4; 7])) %!assert_equal (range (days (2)), duration) %!test %! x = days (magic (3)); %! x(2,3) = NaN; %! assert_equal (range (x), days ([5, 8, 4])); %! assert_equal (range (x, "omitnan"), days ([5, 8, 4])); %! assert_equal (isequaln (range (x, "includenan"), days ([5, 8, NaN])), true); %! assert_equal (range (x, 2), days ([7; 2; 7])); %! assert_equal (range (x, 2, "omitnan"), days ([7; 2; 7])); %! assert_equal (isequaln (range (x, 2, "includenan"), days ([7; NaN; 7])), ... %! true); %!test %! x = reshape (days (1:27), [3, 3, 3]); %! assert_equal (range (x, 3), x(:,:,3) - x(:,:,1)); %!test %! x = reshape (days (1:27), [3, 3, 3]); %! assert_equal (range (x, [2, 3]), x(:,3,3) - x(:,1,1)); %!test %! x = reshape (days (1:27), [3, 3, 3]); %! assert_equal (range (x, [1, 3]), x(3,:,3) - x(1,:,1)); %!test %! x = reshape (days (1:27), [3, 3, 3]); %! assert_equal (range (x, [1, 2, 3]), days (26)); %!test %! x = reshape (days (1:27), [3, 3, 3]); %! assert_equal (range (x, "all"), days (26)); %!test %! x = reshape (days (1:27), [3, 3, 3]); %! x(3) = NaN; %! assert_equal (range (x, "all"), days (26)); %!test %! x = reshape (days (1:27), [3, 3, 3]); %! x(3) = NaN; %! assert_equal (range (x, "all", "omitnan"), days (26)); %!test %! x = reshape (days (1:27), [3, 3, 3]); %! x(3) = NaN; %! assert_equal (isequaln (range (x, "all", "includenan"), days (NaN)), true); %!test %! x = reshape (days (1:27), [3, 3, 3]); %! x(3) = NaN; %! s = min (x, [], "includenan"); %! l = max (x, [], "includenan"); %! assert_equal (isequaln (range (x, "includenan"), l - s), true); %!test %! x = reshape (days (1:27), [3, 3, 3]); %! x(3) = NaN; %! y = range (x, 3, "includenan"); %! assert_equal (isequaln (y(3), days (NaN)), true); %!test %! F = days ([2, 11, 6]); %! A = days ([3, 10, 8]); %! assert_equal (days (rmse (F, A)), sqrt (2), eps); %! assert_equal (days (rmse (F, A, 1)), [1, 1, 2], eps); %! assert_equal (days (rmse (F, A, 2)), sqrt (2), eps); %! assert_equal (days (rmse (F, A, 3)), [1, 1, 2], eps); %! assert_equal (days (rmse (F, A, 'all')), sqrt (2), eps); %!test %! F = days ([3; 7; 4; NaN]); %! A = days ([4; 0; 6; 4]); %! assert_equal (days (rmse (F, A)), NaN); %! assert_equal (days (rmse (F, A, 'omitnan')), 4.2426, 1e-4); %! assert_equal (days (rmse (F, A, 1, 'omitnan')), 4.2426, 1e-4); %! assert_equal (days (rmse (F, A, 2, 'omitnan')), [1; 7; 2; NaN]); %!test %! F = days ([17, 21; 1, 5; 13, 17]); %! A = days ([18, 19; 5, 4; 15, 13]); %! assert_equal (days (rmse (F, A)), [2.6458, 2.6458], 1e-4); %! assert_equal (days (rmse (F, A, 1)), [2.6458, 2.6458], 1e-4); %! assert_equal (days (rmse (F, A, 2)), [1.5811; 2.9155; 3.1623], 1e-4); %! assert_equal (days (rmse (F, A, 'all')), 2.6458, 1e-4); %! assert_equal (days (rmse (F, A, [1, 2])), 2.6458, 1e-4); %!test %! F = days ([3, 5; -1, 2]); %! F(:,:,2) = days ([4, 2; 7, -4]); %! A = days ([6, 5; NaN, 3]); %! E = days (rmse (F, A, [1, 2])); %! assert_equal (E(:,:,1), NaN); %! assert_equal (E(:,:,2), NaN); %! E = days (rmse (F, A, [1, 2], 'omitnan')); %! assert_equal (E(:,:,1), 1.8257, 1e-4); %! assert_equal (E(:,:,2), 4.5461, 1e-4); %! assert_equal (days (rmse (F, A, [1, 3])), [NaN, 3.8406], 1e-4); %! assert_equal (days (rmse (F, A, [1, 3], 'omitnan')), [2.5495, 3.8406], 1e-4); %! assert_equal (days (rmse (F, A, [2, 3])), [2.3452; NaN], 1e-4); %! assert_equal (days (rmse (F, A, [2, 3], 'omitnan')), [2.3452; 5], 1e-4); %!test %! F = days ([2, 11, 6]); %! A = days ([3, 10, 8]); %! W = [1, 2, 1]; %! assert_equal (days (rmse (F, A, 'Weights', W)), 1.3229, 1e-4); %! assert_equal (days (rmse (F, A, 1, 'Weights', W)), [1, 1, 2]); %! assert_equal (days (rmse (F, A, 2, 'Weights', W)), 1.3229, 1e-4); %! assert_equal (days (rmse (F, A, 3, 'Weights', W)), [1, 1, 2]); %!test %! F = days ([3; 7; 4; NaN]); %! A = days ([4; 0; 6; 4]); %! W = [1, 2, 1, 2]; %! assert_equal (days (rmse (F, A, 'Weights', W)), NaN); %! assert_equal (days (rmse (F, A, 'omitnan', 'Weights', W)), 5.0744, 1e-4); %! assert_equal (days (rmse (F, A, 1, 'omitnan', 'Weights', W)), 5.0744, 1e-4); %!test %! F = days ([3; 7; 4; NaN]); %! A = days ([4; 0; 6; 4]); %! W = [1; 2; 1; 2]; %! assert_equal (days (rmse (F, A, 2, 'Weights', W)), [1; 7; 2; NaN], 1e-4); %! assert_equal (days (rmse (F, A, 2, 'omitnan', 'Weights', W)), ... %! [1; 7; 2; NaN], 1e-4); %! assert_equal (days (rmse (F, A, 3, 'Weights', W)), [1; 7; 2; NaN], 1e-4); %! assert_equal (days (rmse (F, A, 3, 'omitnan', 'Weights', W)), ... %! [1; 7; 2; NaN], 1e-4); %!test %! F = days ([3, 4; 7, 5; 4, 3; NaN, 8]); %! A = days ([4; 0; 6; 4]); %! W = [3; 1; 2; 2]; %! E = days (rmse (F, A, 1, 'omitnan', 'Weights', W)); %! assert_equal (E, [3.1623, 3.0619], 1e-4); %!test %! F = days ([3, 4; 7, 5; 4, 3; NaN, 8]); %! A = days ([4; 0; 6; 4]); %! W = [3, 0.4; 1, 0.2; 2, 0.2; 2, 0.2]; %! assert_equal (days (rmse (F, A, 1, 'Weights', W)), [NaN, 3.1623], 1e-4); %! assert_equal (days (rmse (F, A, 1, 'omitnan', 'Weights', W)), [3.1623, ... %! 3.1623], 1e-4); %! assert_equal (days (rmse (F, A, 2, 'Weights', W)), ... %! [0.9393; 6.7082; 2.1106; NaN], 1e-4); %! E = rmse (F, A, 2, 'omitnan', 'Weights', W); %! assert_equal (days (E), [0.9393; 6.7082; 2.1106; 4], 1e-4); %!test %! F = days ([3, 5; -1, 2]); %! F(:,:,2) = days ([4, NaN; 7, -4]); %! A = days ([6, 5; 2, 3]); %! W = [1, 1; 1, 1]; %! Ew = rmse (F, A, [1, 2], 'omitnan', 'Weights', W); %! E = rmse (F, A, [1, 2], 'omitnan'); %! assert_equal (Ew, E); %! Ew = rmse (F, A, [1, 3], 'omitnan','Weights', W); %! E = rmse (F, A, [1, 3], 'omitnan'); %! assert_equal (Ew, E); %! Ew = rmse (F, A, [2, 3], 'omitnan','Weights', W); %! E = rmse (F, A, [2, 3], 'omitnan'); %! assert_equal (Ew, E); %!test %! F = days ([3, 5; -1, 2]); %! F(:,:,2) = days ([4, 2; 7, -4]); %! A = days ([6, 5; 0, 3]); %! W = [1, 2; 1, 1]; %! F1 = reshape (F, 1, 4, 2); %! A1 = reshape (A, 1, 4); %! W1 = reshape (W, 1, 4); %! E1w = days (rmse (F1, A1, 2, 'Weights', W1)); %! assert_equal (E1w(:,:,1), 1.4832, 1e-4); %! assert_equal (E1w(:,:,2), 4.8990, 1e-4); %! E1 = days (rmse (F1, A1, 2)); %! assert_equal (E1(:,:,1), 1.6583, 1e-4); %! assert_equal (E1(:,:,2), 5.2678, 1e-4); %! assert_equal (days (rmse (F, A, [1, 2], 'Weights', W)), E1w, 1e-12); %! assert_equal (days (rmse (F, A, [1, 2])), E1, 1e-12); %!test %! F = days ([3, 5; -1, 2]); %! F(:,:,2) = days ([4, 2; 7, -4]); %! A = days ([6, 5; 0, 3]); %! W = [0.5, 2; 1, 3]; %! F1 = reshape (F, 2, 4, 1); %! A1 = reshape (repmat (A, 1, 1, 2), 2, 4, 1); %! W1 = [0.5, 1, 2, 3; 0.5, 1, 2, 3]; %! E1w = rmse (F1, A1, 2, 'Weights', W1); %! assert_equal (days (E1w), [2.4651; 6.1582], 1e-4); %! E1 = rmse (F1, A1, 2); %! assert_equal (days (E1), [2.3452; 5], 1e-4); %! assert_equal (rmse (F, A, [2, 3], 'Weights', W), E1w); %! assert_equal (rmse (F, A, [2, 3]), E1); %!error ... %! rmse (days ([1, 2, 3]), 1:3) %!error ... %! rmse (1:3, days ([1, 2, 3])) %!assert_equal (skewness (days ([-1, 0, 1])), 0) %!assert_equal (skewness (days ([-2, 0, 1])) < 0, true) %!assert_equal (skewness (days ([-1, 0, 2])) > 0, true) ## Skewness is scale free, so the sign flips with the data; the two are ## equal to within a rounding of the stored milliseconds. %!assert_equal (skewness (days ([-3, 0, 1])), -1 * skewness ([-1, 0, 3]), 1e-12) %!assert_equal (skewness (days (ones (3, 5))), NaN (1, 5)) %!assert_equal (skewness (days (1), [], 3), NaN) %!test %! x = [0; 0; 0; 1]; %! y = days ([x, 2*x]); %! assert_equal (skewness (y), 1.154700538379251 * [1 1], 5*eps); %!assert_equal (skewness (days ([1:5, 10; 1:5, 10]), 0, ... %! 2), 1.439590274527954 * [1; 1], eps) %!assert_equal (skewness (days ([1:5, 10; 1:5, 10]), 1, ... %! 2), 1.051328089232020 * [1; 1], 2*eps) %!assert_equal (skewness (days ([1:5, 10; 1:5, 10]), [], ... %! 2), 1.051328089232020 * [1; 1], 2*eps) %!test %! x(:,:,1) = [0.5377, 0.3188, 3.5784; 1.8339, -1.3077, 2.7694; ... %! -2.2588, -0.4336, -1.3499; 0.8622, 0.3426, 3.0349]; %! x(:,:,2) = [0.7254, -0.1241, 0.6715; -0.0631, 1.4897, -1.2075; ... %! 0.7147 1.4090 0.7172; -0.2050, 1.4172, 1.6302]; %! y = skewness (days (x)); %! assert_equal (y(:,:,1), [-0.8084, -0.5578, -1.0772], 1e-4); %! assert_equal (y(:,:,2), [-0.0403, -1.1472, -0.6632], 1e-4); %! y = skewness (days (x), 1, 2); %! assert_equal (y(:,:,1), [0.6956; -0.5575; 0.0049; 0.6033], 1e-4); %! assert_equal (y(:,:,2), [-0.6969; 0.1828; 0.7071; -0.6714], 1e-4); %! y = skewness (days (x), 1, 3); %! assert_equal (y, zeros (4, 3), 8 * eps); %!test %! x(:,:,1) = [0.5377, 0.3188, 3.5784; 1.8339, -1.3077, 2.7694; ... %! -2.2588, -0.4336, -1.3499; 0.8622, 0.3426, 3.0349]; %! x(:,:,2) = [0.7254, -0.1241, 0.6715; -0.0631, 1.4897, -1.2075; ... %! 0.7147 1.4090 0.7172; -0.2050, 1.4172, 1.6302]; %! y = skewness (days (x), 1, "all"); %! assert_equal (y, 0.0916, 1e-4); %! y = skewness (days (x), 1, [1, 2]); %! assert_equal (y(:,:,1), 0.1070, 1e-4); %! assert_equal (y(:,:,2), -0.6263, 1e-4); %! y = skewness (days (x), 1, [1, 3]); %! assert_equal (y, [-1.0755, -0.3108, -0.2209], 1e-4); %!test %! x = days (ones (10, 2)); %! y = days ([1, 3]); %! assert_equal (std (x), days ([0, 0])); %! assert_equal (days (std (y)), sqrt (2), sqrt (eps)); %! assert_equal (std (x, 0, 2), days (zeros (10, 1))); %!assert_equal (std (days (ones (3, 1, 2)), 0, 2), days (zeros (3, 1, 2))) %!assert_equal (days (std (days ([1, 2]), 0)), sqrt (2)/2, 5*eps) %!assert_equal (days (std (days ([1, 2]), 1)), 0.5, 5*eps) %!assert_equal (std (days (1)), duration) %!assert_equal (std (days ([1, 2, 3]), [], 3), days ([0, 0, 0])) %!test %! x = days ([-10:10]); %! y = [x;x+5;x-5]; %! assert_equal (std (x), days (sqrt (38.5))); %! assert_equal (std (y, [], 2), days (sqrt ([38.5; 38.5; 38.5]))); %! assert_equal (std (y, 0, 2), days (sqrt ([38.5; 38.5; 38.5]))); %! assert_equal (days (std (y, 1, 2)), sqrt (ones (3, ... %! 1) * 36.66666666666666), 1e-14); %! assert_equal (days (std (y, "all")), sqrt (54.19354838709678), 1e-14); %! y(2,4) = NaN; %! assert_equal (isequaln (std (y, "all"), days (NaN)), true); %! assert_equal (isequaln (std (y, "all", "includenan"), days (NaN)), true); %! assert_equal (days (std (y, "all", "omitnan")), sqrt (55.01533580116342), ... %! 1e-14); %! assert_equal (isequaln (std (y, 0, 2, "includenan"), ... %! days (sqrt ([38.5; NaN; 38.5]))), true); %! assert_equal (isequaln (std (y, [], 2), days (sqrt ([38.5; NaN; 38.5]))), ... %! true); %! assert_equal (days (std (y, [], 2, "omitnan")), ... %! sqrt ([38.5; 37.81842105263158; 38.5]), 1e-14); %!assert_equal (days (std (days ([1, NaN, 3]), [1 2 3], ... %! "omitnan")), sqrt (0.75), eps) %!assert_equal (days (std (days ([1, 2, 3]), [1 NaN 3], ... %! "omitnan")), sqrt (0.75), eps) %!assert_equal (days (std (days (magic (3)), [1 NaN 3], ... %! "omitnan")), sqrt ([3, 12, 3]), eps) %!assert_equal (days (std (days ([1, NaN, 3]), [1 2 3], "omitnan", ... %! "all")), sqrt (0.75), eps) %!assert_equal (days (std (days ([1, NaN, 3]), [1 2 3], "all", ... %! "omitnan")), sqrt (0.75), eps) %!assert_equal (days (std (days ([1, 2, 3]), [1 NaN 3], "omitnan", ... %! "all")), sqrt (0.75), eps) %!assert_equal (days (std (days ([1, NaN, 3]), [1 2 3], 2, ... %! "omitnan")), sqrt (0.75), eps) %!assert_equal (days (std (days ([1, 2, 3]), [1 NaN 3], 2, ... %! "omitnan")), sqrt (0.75), eps) %!assert_equal (days (std (days (magic (3)), [1 NaN 3], 1, ... %! "omitnan")), sqrt ([3, 12, 3]), eps) %!assert_equal (days (std (days (magic (3)), [1 NaN 3], 2, ... %! "omitnan")), sqrt ([0.75;3;0.75]), eps) %!assert_equal (days (std (days ([4, 4; 4, 6; 6, 6]), [1 3], 2, ... %! 'omitnan')), sqrt ([0;0.75;0]), eps) %!assert_equal (days (std (days ([4, NaN; 4, 6; 6, 6]), [1, 2, 3], 1, ... %! 'omitnan')), sqrt ([1 0])) %!assert_equal (days (std (days ([4, NaN; 4, 6; 6, 6]), [1, 3], 2, ... %! 'omitnan')), sqrt ([0;0.75;0]), eps) %!assert_equal (days (std (days (3*reshape(1:18, [3, 3, 2])), [1, 2, 3], 1, ... %! 'omitnan')), ... %! sqrt (ones (1, 3, 2) * 5)) %!assert_equal (days (std (reshape (days (1:18), [3, 3, 2]), [1, 2, 3], 2, ... %! 'omitnan')), ... %! sqrt (5 * ones(3, 1, 2))) %!assert_equal (days (std (3 * reshape (days (1:18), [3, 3, 2]), ones (3,3, ... %! 2), [1 2], 'omitnan')), ... %! sqrt (60 * ones (1, 1, 2))) %!assert_equal (days (std (3*reshape(days (1:18), [3, 3, 2]), ones (3,3,2), ... %! [1 4], 'omitnan')), ... %! sqrt (6 * ones (1, 3, 2))) %!assert_equal (days (std (6 * reshape (days (1:18), [3, 3, 2]), ones (3,3, ... %! 2), [1:3], 'omitnan')), sqrt (969)) %!test %! x = reshape (days (1:18), [3, 3, 2]); %! x([2, 14]) = NaN; %! w = ones (3,3,2); %! assert_equal (std (16 * x, w, [1:3], 'omitnan'), days (sqrt (6519))); ################################################################################ ## ** Sort, Filter, and Set Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'maxk' 'mink' 'sort' 'sortrows' ## ## 'topkrows' 'unique' 'interp1' 'intersect' ## ## 'setdiff' 'setxor' 'union' ## ## ## ################################################################################ %!test %! D = days ([3, 1, NaN, 2, 1, NaN]); %! [B, I] = maxk (D, 3); %! assert_equal (days (B), [3, 2, 1]); %! assert_equal (I, [1, 4, 2]); %!test %! D = days ([3, 1, NaN, 2, 1, NaN]); %! [B, I] = maxk (D, 10); %! assert_equal (days (B), [3, 2, 1, 1, NaN, NaN]); %! assert_equal (I, [1, 4, 2, 5, 3, 6]); %!test %! D = days ([3; 1; NaN; 2]); %! [B, I] = maxk (D, 2); %! assert_equal (days (B), [3; 2]); %! assert_equal (I, [1; 4]); %!test %! D = days ([2, 1, 2, 1, 2]); %! [B, I] = maxk (D, 3); %! assert_equal (days (B), [2, 2, 2]); %! assert_equal (I, [1, 3, 5]); %!test %! D = days ([1, 5; 4, 2; 3, 6]); %! [B, I] = maxk (D, 2); %! assert_equal (days (B), [4, 6; 3, 5]); %! assert_equal (I, [2, 3; 3, 1]); %!test %! D = days ([1, 5; 4, 2; 3, 6]); %! [B, I] = maxk (D, 2, 2); %! assert_equal (days (B), [5, 1; 4, 2; 6, 3]); %! assert_equal (I, [2, 1; 1, 2; 2, 1]); %!test %! D = days ([1, 5; 4, 2; 3, 6]); %! [B, I] = maxk (D, 2, 3); %! assert_equal (days (B), [1, 5; 4, 2; 3, 6]); %! assert_equal (I, ones (3, 2)); %!test %! D = days ([3, 1, NaN, 2, 1, NaN]); %! [B, I] = maxk (D, 3, 1); %! assert_equal (days (B), [3, 1, NaN, 2, 1, NaN]); %! assert_equal (I, ones (1, 6)); %!test %! D = days ([1, NaN; NaN, 2; 3, 4]); %! [B, I] = maxk (D, 3); %! assert_equal (days (B), [3, 4; 1, 2; NaN, NaN]); %! assert_equal (I, [3, 3; 1, 2; 2, 1]); %!test %! D = days ([Inf, -Inf, 1, NaN]); %! [B, I] = maxk (D, 4); %! assert_equal (days (B), [Inf, 1, -Inf, NaN]); %! assert_equal (I, [1, 3, 2, 4]); %!test %! D = days (reshape (1:24, 2, 3, 4)); %! [B, I] = maxk (D, 2, 3); %! assert_equal (size (B), [2, 3, 2]); %! assert_equal (days (B), days (D(:,:,[4, 3]))); %! assert_equal (I, cat (3, 4 * ones (2, 3), 3 * ones (2, 3))); %!test %! D = days ([NaN, NaN, NaN]); %! [B, I] = maxk (D, 2); %! assert_equal (days (B), [NaN, NaN]); %! assert_equal (I, [1, 2]); %!test %! D = days ([3, 1, 2]); %! D.Format = 'hh:mm'; %! B = maxk (D, 2); %! assert_equal (B.Format, 'hh:mm'); %!assert_equal (class (maxk (days ([3, 1, 2]), 2)), 'duration') %!assert_equal (size (maxk (days ([3, 1, 2]), 0)), [1, 0]) %!assert_equal (size (maxk (days ([1, 5; 4, 2; 3, 6]), 0)), [0, 2]) %!assert_equal (size (maxk (days ([]), 2)), [0, 0]) %!error maxk (days (1)) %!error ... %! maxk (days ([1, 2, 3]), 2.5) %!error ... %! maxk (days ([1, 2, 3]), -1) %!error ... %! maxk (days ([1, 2, 3]), [2, 3]) %!error ... %! maxk (days ([1, 2, 3]), Inf) %!error ... %! maxk (days ([1, 2, 3]), 2, 0) %!error ... %! maxk (days ([1, 2, 3]), 2, 1.5) %!error ... %! maxk (days ([1, 2, 3]), 2, 'ComparisonMethod', 'abs') %!error ... %! maxk (days ([1, 2, 3]), 2, 'MissingPlacement', 'first') %!error ... %! maxk (days ([1, 2, 3]), 2, 1, 2) %!test %! D = days ([3, 1, NaN, 2, 1, NaN]); %! [B, I] = mink (D, 3); %! assert_equal (days (B), [1, 1, 2]); %! assert_equal (I, [2, 5, 4]); %!test %! D = days ([3, 1, NaN, 2, 1, NaN]); %! [B, I] = mink (D, 10); %! assert_equal (days (B), [1, 1, 2, 3, NaN, NaN]); %! assert_equal (I, [2, 5, 4, 1, 3, 6]); %!test %! D = days ([3, NaN, 1]); %! [B, I] = mink (D, 3); %! assert_equal (days (B), [1, 3, NaN]); %! assert_equal (I, [3, 1, 2]); %!test %! D = days ([2, 1, 2, 1, 2]); %! [B, I] = mink (D, 3); %! assert_equal (days (B), [1, 1, 2]); %! assert_equal (I, [2, 4, 1]); %!test %! D = days ([1, 5; 4, 2; 3, 6]); %! [B, I] = mink (D, 2); %! assert_equal (days (B), [1, 2; 3, 5]); %! assert_equal (I, [1, 2; 3, 1]); %!test %! D = days ([1, 5; 4, 2; 3, 6]); %! [B, I] = mink (D, 2, 2); %! assert_equal (days (B), [1, 5; 2, 4; 3, 6]); %! assert_equal (I, [1, 2; 2, 1; 1, 2]); %!test %! D = days ([1, NaN; NaN, 2; 3, 4]); %! [B, I] = mink (D, 3); %! assert_equal (days (B), [1, 2; 3, 4; NaN, NaN]); %! assert_equal (I, [1, 2; 3, 3; 2, 1]); %!test %! D = days ([Inf, -Inf, 1, NaN]); %! [B, I] = mink (D, 4); %! assert_equal (days (B), [-Inf, 1, Inf, NaN]); %! assert_equal (I, [2, 3, 1, 4]); %!test %! D = days ([3, 1, NaN, 2, 1, NaN]); %! [B, I] = mink (D, 3, 1); %! assert_equal (days (B), [3, 1, NaN, 2, 1, NaN]); %! assert_equal (I, ones (1, 6)); %!test %! D = days ([NaN, NaN, NaN]); %! [B, I] = mink (D, 2); %! assert_equal (days (B), [NaN, NaN]); %! assert_equal (I, [1, 2]); %!test %! D = days ([3, 1, 2]); %! D.Format = 'hh:mm'; %! B = mink (D, 2); %! assert_equal (B.Format, 'hh:mm'); %!assert_equal (class (mink (days ([3, 1, 2]), 2)), 'duration') %!assert_equal (size (mink (days ([3, 1, 2]), 0)), [1, 0]) %!assert_equal (size (mink (days ([1, 5; 4, 2; 3, 6]), 0)), [0, 2]) %!assert_equal (size (mink (days ([]), 2)), [0, 0]) %!error mink (days (1)) %!error ... %! mink (days ([1, 2, 3]), 2.5) %!error ... %! mink (days ([1, 2, 3]), -1) %!error ... %! mink (days ([1, 2, 3]), [2, 3]) %!error ... %! mink (days ([1, 2, 3]), Inf) %!error ... %! mink (days ([1, 2, 3]), 2, 0) %!error ... %! mink (days ([1, 2, 3]), 2, 1.5) %!error ... %! mink (days ([1, 2, 3]), 2, 'ComparisonMethod', 'abs') %!error ... %! mink (days ([1, 2, 3]), 2, 'MissingPlacement', 'first') %!error ... %! mink (days ([1, 2, 3]), 2, 1, 2) %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (D); %! assert_equal (days (B), [1, -4; 1, 1; 2, 2; 2, 3]); %! assert_equal (I, [1, 3; 2, 4; 3, 1; 4, 2]); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (D, 1); %! assert_equal (days (B), [1, -4; 1, 1; 2, 2; 2, 3]); %! assert_equal (I, [1, 3; 2, 4; 3, 1; 4, 2]); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (D, 2); %! assert_equal (days (B), [1, 2; 1, 3; -4, 2; 1, 2]); %! assert_equal (I, [1, 2; 1, 2; 2, 1; 2, 1]); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (D, 1, 'ComparisonMethod', 'abs'); %! assert_equal (days (B), [1, 1; 1, 2; 2, 3; 2, -4]); %! assert_equal (I, [1, 4; 2, 1; 3, 2; 4, 3]); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (D, 2, 'ComparisonMethod', 'abs'); %! assert_equal (days (B), [1, 2; 1, 3; 2, -4; 1, 2]); %! assert_equal (I, [1, 2; 1, 2; 1, 2; 2, 1]); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (D, 'descend'); %! assert_equal (days (B), [2, 3; 2, 2; 1, 1; 1, -4]); %! assert_equal (I, [3, 2; 4, 1; 1, 4; 2, 3]); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (D, 1, 'descend'); %! assert_equal (days (B), [2, 3; 2, 2; 1, 1; 1, -4]); %! assert_equal (I, [3, 2; 4, 1; 1, 4; 2, 3]); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (D, 2, 'descend'); %! assert_equal (days (B), [2, 1; 3, 1; 2, -4; 2, 1]); %! assert_equal (I, [2, 1; 2, 1; 1, 2; 1, 2]); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (D, 1, 'descend', 'ComparisonMethod', 'abs'); %! assert_equal (days (B), [2, -4; 2, 3; 1, 2; 1, 1]); %! assert_equal (I, [3, 3; 4, 2; 1, 1; 2, 4]); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! [B, I] = sort (D, 2, 'descend', 'ComparisonMethod', 'abs'); %! assert_equal (days (B), [2, 1; 3, 1; -4, 2; 2, 1]); %! assert_equal (I, [2, 1; 2, 1; 2, 1; 1, 2]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D); %! X = [-2, -4, 1; 1, 1, 2; 1, 2, 2; 1, 3, 2; 2, 3, 3; 2, NaN, NaN]; %! assert_equal (days (B), X); %! assert_equal (I, [5, 3, 2; 1, 4, 1; 2, 1, 3; 6, 2, 6; 3, 6, 5; 4, 5, 4]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 1); %! X = [-2, -4, 1; 1, 1, 2; 1, 2, 2; 1, 3, 2; 2, 3, 3; 2, NaN, NaN]; %! assert_equal (days (B), X); %! assert_equal (I, [5, 3, 2; 1, 4, 1; 2, 1, 3; 6, 2, 6; 3, 6, 5; 4, 5, 4]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 2); %! X = [1, 2, 2; 1, 1, 3; -4, 2, 2; 1, 2, NaN; -2, 3, NaN; 1, 2, 3]; %! assert_equal (days (B), X); %! assert_equal (I, [1, 2, 3; 1, 3, 2; 2, 1, 3; 2, 1, 3; 1, 3, 2; 1, 3, 2]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 1, 'ComparisonMethod', 'abs'); %! X = [1, 1, 1; 1, 2, 2; 1, 3, 2; 2, 3, 2; 2, -4, 3; -2, NaN, NaN]; %! assert_equal (days (B), X); %! assert_equal (I, [1, 4, 2; 2, 1, 1; 6, 2, 3; 3, 6, 6; 4, 3, 5; 5, 5, 4]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 2, 'ComparisonMethod', 'abs'); %! X = [1, 2, 2; 1, 1, 3; 2, 2, -4; 1, 2, NaN; -2, 3, NaN; 1, 2, 3]; %! assert_equal (days (B), X); %! assert_equal (I, [1, 2, 3; 1, 3, 2; 1, 3, 2; 2, 1, 3; 1, 3, 2; 1, 3, 2]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 1, 'MissingPlacement', 'first'); %! X = [-2, NaN, NaN; 1, -4, 1; 1, 1, 2; 1, 2, 2; 2, 3, 2; 2, 3, 3]; %! assert_equal (days (B), X); %! assert_equal (I, [5, 5, 4; 1, 3, 2; 2, 4, 1; 6, 1, 3; 3, 2, 6; 4, 6, 5]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 2, 'MissingPlacement', 'first'); %! X = [1, 2, 2; 1, 1, 3; -4, 2, 2; NaN, 1, 2; NaN, -2, 3; 1, 2, 3]; %! assert_equal (days (B), X); %! assert_equal (I, [1, 2, 3; 1, 3, 2; 2, 1, 3; 3, 2, 1; 2, 1, 3; 1, 3, 2]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 1, 'MissingPlacement', 'last'); %! X = [-2, -4, 1; 1, 1, 2; 1, 2, 2; 1, 3, 2; 2, 3, 3; 2, NaN, NaN]; %! assert_equal (days (B), X); %! assert_equal (I, [5, 3, 2; 1, 4, 1; 2, 1, 3; 6, 2, 6; 3, 6, 5; 4, 5, 4]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 2, 'MissingPlacement', 'last'); %! X = [1, 2, 2; 1, 1, 3; -4, 2, 2; 1, 2, NaN; -2, 3, NaN; 1, 2, 3]; %! assert_equal (days (B), X); %! assert_equal (I, [1, 2, 3; 1, 3, 2; 2, 1, 3; 2, 1, 3; 1, 3, 2; 1, 3, 2]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 'descend'); %! X = [2, NaN, NaN; 2, 3, 3; 1, 3, 2; 1, 2, 2; 1, 1, 2; -2, -4, 1]; %! assert_equal (days (B), X); %! assert_equal (I, [3, 5, 4; 4, 2, 5; 1, 6, 1; 2, 1, 3; 6, 4, 6; 5, 3, 2]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 1, 'descend'); %! X = [2, NaN, NaN; 2, 3, 3; 1, 3, 2; 1, 2, 2; 1, 1, 2; -2, -4, 1]; %! assert_equal (days (B), X); %! assert_equal (I, [3, 5, 4; 4, 2, 5; 1, 6, 1; 2, 1, 3; 6, 4, 6; 5, 3, 2]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 2, 'descend'); %! X = [2, 2, 1; 3, 1, 1; 2, 2, -4; NaN, 2, 1; NaN, 3, -2; 3, 2, 1]; %! assert_equal (days (B), X); %! assert_equal (I, [2, 3, 1; 2, 1, 3; 1, 3, 2; 3, 1, 2; 2, 3, 1; 2, 3, 1]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 1, 'descend', 'ComparisonMethod', 'abs'); %! X = [2, NaN, NaN; 2, -4, 3; -2, 3, 2; 1, 3, 2; 1, 2, 2; 1, 1, 1]; %! assert_equal (days (B), X); %! assert_equal (I, [3, 5, 4; 4, 3, 5; 5, 2, 1; 1, 6, 3; 2, 1, 6; 6, 4, 2]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 2, 'descend', 'ComparisonMethod', 'abs'); %! X = [2, 2, 1; 3, 1, 1; -4, 2, 2; NaN, 2, 1; NaN, 3, -2; 3, 2, 1]; %! assert_equal (days (B), X); %! assert_equal (I, [2, 3, 1; 2, 1, 3; 2, 1, 3; 3, 1, 2; 2, 3, 1; 2, 3, 1]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 1, 'descend', 'MissingPlacement', 'first'); %! X = [2, NaN, NaN; 2, 3, 3; 1, 3, 2; 1, 2, 2; 1, 1, 2; -2, -4, 1]; %! assert_equal (days (B), X); %! assert_equal (I, [3, 5, 4; 4, 2, 5; 1, 6, 1; 2, 1, 3; 6, 4, 6; 5, 3, 2]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 2, 'descend', 'MissingPlacement', 'first'); %! X = [2, 2, 1; 3, 1, 1; 2, 2, -4; NaN, 2, 1; NaN, 3, -2; 3, 2, 1]; %! assert_equal (days (B), X); %! assert_equal (I, [2, 3, 1; 2, 1, 3; 1, 3, 2; 3, 1, 2; 2, 3, 1; 2, 3, 1]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 1, 'descend', 'MissingPlacement', 'last'); %! X = [2, 3, 3; 2, 3, 2; 1, 2, 2; 1, 1, 2; 1, -4, 1; -2, NaN, NaN]; %! assert_equal (days (B), X); %! assert_equal (I, [3, 2, 5; 4, 6, 1; 1, 1, 3; 2, 4, 6; 6, 3, 2; 5, 5, 4]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 2, 'descend', 'MissingPlacement', 'last'); %! X = [2, 2, 1; 3, 1, 1; 2, 2, -4; 2, 1, NaN; 3, -2, NaN; 3, 2, 1]; %! assert_equal (days (B), X); %! assert_equal (I, [2, 3, 1; 2, 1, 3; 1, 3, 2; 1, 2, 3; 3, 1, 2; 2, 3, 1]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 1, 'MissingPlacement', 'first', 'ComparisonMethod', 'abs'); %! X = [1, NaN, NaN; 1, 1, 1; 1, 2, 2; 2, 3, 2; 2, 3, 2; -2, -4, 3]; %! assert_equal (days (B), X); %! assert_equal (I, [1, 5, 4; 2, 4, 2; 6, 1, 1; 3, 2, 3; 4, 6, 6; 5, 3, 5]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 2, 'MissingPlacement', 'first', 'ComparisonMethod', 'abs'); %! X = [1, 2, 2; 1, 1, 3; 2, 2, -4; NaN, 1, 2; NaN, -2, 3; 1, 2, 3]; %! assert_equal (days (B), X); %! assert_equal (I, [1, 2, 3; 1, 3, 2; 1, 3, 2; 3, 2, 1; 2, 1, 3; 1, 3, 2]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 1, 'MissingPlacement', 'last', 'ComparisonMethod', 'abs'); %! X = [1, 1, 1; 1, 2, 2; 1, 3, 2; 2, 3, 2; 2, -4, 3; -2, NaN, NaN]; %! assert_equal (days (B), X); %! assert_equal (I, [1, 4, 2; 2, 1, 1; 6, 2, 3; 3, 6, 6; 4, 3, 5; 5, 5, 4]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 2, 'MissingPlacement', 'last', 'ComparisonMethod', 'abs'); %! X = [1, 2, 2; 1, 1, 3; 2, 2, -4; 1, 2, NaN; -2, 3, NaN; 1, 2, 3]; %! assert_equal (days (B), X); %! assert_equal (I, [1, 2, 3; 1, 3, 2; 1, 3, 2; 2, 1, 3; 1, 3, 2; 1, 3, 2]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 1, 'descend', 'MissingPlacement', 'first', ... %! 'ComparisonMethod', 'abs'); %! X = [2, NaN, NaN; 2, -4, 3; -2, 3, 2; 1, 3, 2; 1, 2, 2; 1, 1, 1]; %! assert_equal (days (B), X); %! assert_equal (I, [3, 5, 4; 4, 3, 5; 5, 2, 1; 1, 6, 3; 2, 1, 6; 6, 4, 2]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 2, 'descend', 'MissingPlacement', 'first', ... %! 'ComparisonMethod', 'abs'); %! X = [2, 2, 1; 3, 1, 1; -4, 2, 2; NaN, 2, 1; NaN, 3, -2; 3, 2, 1]; %! assert_equal (days (B), X); %! assert_equal (I, [2, 3, 1; 2, 1, 3; 2, 1, 3; 3, 1, 2; 2, 3, 1; 2, 3, 1]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 1, 'descend', 'MissingPlacement', 'last', ... %! 'ComparisonMethod', 'abs'); %! X = [2, -4, 3; 2, 3, 2; -2, 3, 2; 1, 2, 2; 1, 1, 1; 1, NaN, NaN]; %! assert_equal (days (B), X); %! assert_equal (I, [3, 3, 5; 4, 2, 1; 5, 6, 3; 1, 1, 6; 2, 4, 2; 6, 5, 4]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! [B, I] = sort (D, 2, 'descend', 'MissingPlacement', 'last', ... %! 'ComparisonMethod', 'abs'); %! X = [2, 2, 1; 3, 1, 1; -4, 2, 2; 2, 1, NaN; 3, -2, NaN; 3, 2, 1]; %! assert_equal (days (B), X); %! assert_equal (I, [2, 3, 1; 2, 1, 3; 2, 1, 3; 1, 2, 3; 3, 1, 2; 2, 3, 1]); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! D = sortrows (D); %! assert_equal (days (D), [1, 2; 1, 3; 2, -4; 2, 1]); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! D = sortrows (D, 'ComparisonMethod', 'abs'); %! assert_equal (days (D), [1, 2; 1, 3; 2, 1; 2, -4]); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! D = sortrows (D, 1, 'ComparisonMethod', 'abs'); %! assert_equal (days (D), [1, 2; 1, 3; 2, -4; 2, 1]); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! D = sortrows (D, 2, 'ComparisonMethod', 'abs'); %! assert_equal (days (D), [2, 1; 1, 2; 1, 3; 2, -4]); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! D = sortrows (D, -1, 'ComparisonMethod', 'abs'); %! assert_equal (days (D), [2, -4; 2, 1; 1, 2; 1, 3]); %!test %! D = days ([1, 2; 1, 3; 2, -4; 2, 1]); %! D = sortrows (D, -2, 'ComparisonMethod', 'abs'); %! assert_equal (days (D), [2, -4; 1, 3; 1, 2; 2, 1]); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D); %! X = [-2, NaN, 3; 1, 2, 2; 1, 3, 1; 1, 3, 2; 2, -4, 2; 2, 1, NaN]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, -2, 'MissingPlacement', 'first'); %! X = [-2, NaN, 3; 1, 3, 1; 1, 3, 2; 1, 2, 2; 2, 1, NaN; 2, -4, 2]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, -2, 'MissingPlacement', 'last'); %! X = [1, 3, 1; 1, 3, 2; 1, 2, 2; 2, 1, NaN; 2, -4, 2; -2, NaN, 3]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, 2, 'MissingPlacement', 'first'); %! X = [-2, NaN, 3; 2, -4, 2; 2, 1, NaN; 1, 2, 2; 1, 3, 1; 1, 3, 2]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, 2, 'MissingPlacement', 'last'); %! X = [2, -4, 2; 2, 1, NaN; 1, 2, 2; 1, 3, 1; 1, 3, 2; -2, NaN, 3]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, 'ComparisonMethod', 'abs', 'MissingPlacement', 'first'); %! X = [1, 2, 2; 1, 3, 1; 1, 3, 2; -2, NaN, 3; 2, 1, NaN; 2, -4, 2]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, 'ComparisonMethod', 'abs', 'MissingPlacement', 'last'); %! X = [1, 2, 2; 1, 3, 1; 1, 3, 2; 2, 1, NaN; 2, -4, 2; -2, NaN, 3]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2, 3]); %! X = [-2, NaN, 3; 1, 3, 1; 1, 3, 2; 1, 2, 2; 2, 1, NaN; 2, -4, 2]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [-1, -2, 3]); %! X = [2, 1, NaN; 2, -4, 2; 1, 3, 1; 1, 3, 2; 1, 2, 2; -2, NaN, 3]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2, -3]); %! X = [-2, NaN, 3; 1, 3, 2; 1, 3, 1; 1, 2, 2; 2, 1, NaN; 2, -4, 2]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2], {'ascend', 'descend'}); %! X = [-2, NaN, 3; 1, 3, 1; 1, 3, 2; 1, 2, 2; 2, 1, NaN; 2, -4, 2]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2], {'descend', 'ascend'}); %! X = [2, -4, 2; 2, 1, NaN; 1, 2, 2; 1, 3, 1; 1, 3, 2; -2, NaN, 3]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [-1, -2, 3], {'ascend', 'ascend', 'descend'}); %! X = [-2, NaN, 3; 1, 2, 2; 1, 3, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2, -3], string ({'ascend', 'ascend', 'descend'})); %! X = [-2, NaN, 3; 1, 2, 2; 1, 3, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, string ({'ascend', 'ascend', 'descend'})); %! X = [-2, NaN, 3; 1, 2, 2; 1, 3, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, string ({'ascend', 'ascend', 'descend'})); %! X = [-2, NaN, 3; 1, 2, 2; 1, 3, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, 2, -3]); %! X = [-2, NaN, 3; 1, 2, 2; 1, 3, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2, 3], 'MissingPlacement', 'first'); %! X = [-2, NaN, 3; 1, 3, 1; 1, 3, 2; 1, 2, 2; 2, 1, NaN; 2, -4, 2]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [-1, -2, 3], 'MissingPlacement', 'first'); %! X = [2, 1, NaN; 2, -4, 2; 1, 3, 1; 1, 3, 2; 1, 2, 2; -2, NaN, 3]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2, -3], 'MissingPlacement', 'first'); %! X = [-2, NaN, 3; 1, 3, 2; 1, 3, 1; 1, 2, 2; 2, 1, NaN; 2, -4, 2]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2, 3], 'MissingPlacement', 'last'); %! X = [-2, NaN, 3; 1, 3, 1; 1, 3, 2; 1, 2, 2; 2, 1, NaN; 2, -4, 2]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [-1, -2, 3], 'MissingPlacement', 'last'); %! X = [2, 1, NaN; 2, -4, 2; 1, 3, 1; 1, 3, 2; 1, 2, 2; -2, NaN, 3]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2, -3], 'MissingPlacement', 'last'); %! X = [-2, NaN, 3; 1, 3, 2; 1, 3, 1; 1, 2, 2; 2, 1, NaN; 2, -4, 2]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2, 3], 'MissingPlacement', 'first', ... %! 'ComparisonMethod', 'abs'); %! X = [1, 3, 1; 1, 3, 2; 1, 2, 2; -2, NaN, 3; 2, -4, 2; 2, 1, NaN]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [-1, -2, 3], 'MissingPlacement', 'first', ... %! 'ComparisonMethod', 'abs'); %! X = [-2, NaN, 3; 2, -4, 2; 2, 1, NaN; 1, 3, 1; 1, 3, 2; 1, 2, 2]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2, -3], 'MissingPlacement', 'first', ... %! 'ComparisonMethod', 'abs'); %! X = [1, 3, 2; 1, 3, 1; 1, 2, 2; -2, NaN, 3; 2, -4, 2; 2, 1, NaN]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2, 3], 'MissingPlacement', 'last', ... %! 'ComparisonMethod', 'abs'); %! X = [1, 3, 1; 1, 3, 2; 1, 2, 2; 2, -4, 2; 2, 1, NaN; -2, NaN, 3]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [-1, -2, 3], 'MissingPlacement', 'last', ... %! 'ComparisonMethod', 'abs'); %! X = [2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 1; 1, 3, 2; 1, 2, 2]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2, -3], 'MissingPlacement', 'last', ... %! 'ComparisonMethod', 'abs'); %! X = [1, 3, 2; 1, 3, 1; 1, 2, 2; 2, -4, 2; 2, 1, NaN; -2, NaN, 3]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2], {'ascend', 'descend'}, 'MissingPlacement', ... %! 'first'); %! X = [-2, NaN, 3; 1, 3, 1; 1, 3, 2; 1, 2, 2; 2, 1, NaN; 2, -4, 2]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2], {'descend', 'ascend'}, 'MissingPlacement', 'last'); %! X = [2, -4, 2; 2, 1, NaN; 1, 2, 2; 1, 3, 1; 1, 3, 2; -2, NaN, 3]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [-1, -2, 3], {'ascend', 'ascend', 'descend'}, ... %! 'ComparisonMethod', 'abs', 'MissingPlacement', 'first'); %! X = [1, 2, 2; 1, 3, 2; 1, 3, 1; -2, NaN, 3; 2, 1, NaN; 2, -4, 2]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2, -3], {'ascend', 'ascend', 'descend'}, ... %! 'ComparisonMethod', 'abs', 'MissingPlacement', 'first'); %! X = [1, 2, 2; 1, 3, 2; 1, 3, 1; -2, NaN, 3; 2, 1, NaN; 2, -4, 2]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [-1, -2, 3], {'ascend', 'ascend', 'descend'}, ... %! 'ComparisonMethod', 'abs', 'MissingPlacement', 'last'); %! X = [1, 2, 2; 1, 3, 2; 1, 3, 1; 2, 1, NaN; 2, -4, 2; -2, NaN, 3]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, -2, -3], {'ascend', 'ascend', 'descend'}, ... %! 'ComparisonMethod', 'abs', 'MissingPlacement', 'last'); %! X = [1, 2, 2; 1, 3, 2; 1, 3, 1; 2, 1, NaN; 2, -4, 2; -2, NaN, 3]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, {'ascend', 'ascend', 'descend'}, 'ComparisonMethod', 'abs'); %! X = [1, 2, 2; 1, 3, 2; 1, 3, 1; 2, 1, NaN; 2, -4, 2; -2, NaN, 3]; %! assert_equal (days (D), X); %!test %! D = days ([1, 2, 2; 1, 3, 1; 2, -4, 2; 2, 1, NaN; -2, NaN, 3; 1, 3, 2]); %! D = sortrows (D, [1, 2, -3], 'ComparisonMethod', 'abs'); %! X = [1, 2, 2; 1, 3, 2; 1, 3, 1; 2, 1, NaN; 2, -4, 2; -2, NaN, 3]; %! assert_equal (days (D), X); %!error ... %! sortrows (days (ones (2, 3, 4))) %!error ... %! sortrows (days (2), 'MissingPlacement', 2) %!error ... %! sortrows (days (2), 'ComparisonMethod', 2) %!error ... %! sortrows (days (1), 1, 'ascend', 2) %!error ... %! sortrows (days (1), 1.5) %!error ... %! sortrows (days (1), ones (2)) %!error ... %! sortrows (days (1), 2) %!error ... %! sortrows (days (1), {'a'}) %!error ... %! sortrows (days (1), 1, {'ascend', 'descend'}) %!error ... %! sortrows (days (1), {1}) %!error ... %! sortrows (days (1), {'ascend'}, 'ascend') %!error ... %! sortrows (days (1), {'ascend'}, 1) %!error ... %! sortrows (days (1), 1, 1) %!error ... %! sortrows (days (1), 1, {'a'}) %!error ... %! sortrows (days (1), 1, {'ascend', 'descend'}) %!test %! D = hours ([3, 1; 1, 2; 3, 0; NaN, 5; 1, 1]); %! [B, I] = topkrows (D, 3); %! assert_equal (hours (B), [3, 1; 3, 0; 1, 2]); %! assert_equal (I, [1; 3; 2]); %!test %! D = hours ([3, 1; 1, 2; 3, 0; NaN, 5; 1, 1]); %! [B, I] = topkrows (D, 5); %! assert_equal (hours (B), [3, 1; 3, 0; 1, 2; 1, 1; NaN, 5]); %! assert_equal (I, [1; 3; 2; 5; 4]); %!test %! D = hours ([3, 1; 1, 2; 3, 0; NaN, 5; 1, 1]); %! [B, I] = topkrows (D, 5, 'ascend'); %! assert_equal (hours (B), [1, 1; 1, 2; 3, 0; 3, 1; NaN, 5]); %! assert_equal (hours (topkrows (D, 5, 'descend')), hours (topkrows (D, 5))); %!test %! D = hours ([3, 1; 1, 2; 3, 0; NaN, 5; 1, 1]); %! [B, I] = topkrows (D, 5, 1, 'ascend'); %! assert_equal (I, [2; 5; 1; 3; 4]); %!test %! D = hours ([3, 1; 1, 2; 3, 0; NaN, 5; 1, 1]); %! [B, I] = topkrows (D, 3, [2, 1]); %! assert_equal (hours (B), [NaN, 5; 1, 2; 3, 1]); %!test %! D = hours ([3, 1; 1, 2; 3, 0; NaN, 5; 1, 1]); %! assert_equal (hours (topkrows (D, 3, [1, 2], {'ascend', 'descend'})), ... %! [1, 2; 1, 1; 3, 1]); %! assert_equal (hours (topkrows (D, 3, [1, 2], {'descend', 'ascend'})), ... %! [3, 0; 3, 1; 1, 1]); %!test # a missing element demotes a row only in the column that decides it %! E = hours ([3, 1; 1, NaN; 3, 0; 2, 5; 1, 1]); %! [B, I] = topkrows (E, 5, 2); %! assert_equal (I, [4; 1; 5; 3; 2]); %! assert_equal (hours (topkrows (E, 5, 2, 'ascend')), ... %! [3, 0; 3, 1; 1, 1; 2, 5; 1, NaN]); %! [B, I] = topkrows (E, 5, [1, 2]); %! assert_equal (I, [1; 3; 4; 5; 2]); %!test # 'MissingPlacement' is an Octave extension; the default is 'last' %! D = hours ([3, 1; 1, 2; 3, 0; NaN, 5; 1, 1]); %! [~, I] = topkrows (D, 5, 'MissingPlacement', 'last'); %! assert_equal (I, [1; 3; 2; 5; 4]); %! [~, I] = topkrows (D, 5, 'MissingPlacement', 'first'); %! assert_equal (I, [4; 1; 3; 2; 5]); %!test %! D = hours ([3, 1; 1, 2; 3, 0; NaN, 5; 1, 1]); %! assert_equal (hours (topkrows (D, 10)), hours (topkrows (D, 5))); %! assert_equal (size (topkrows (D, 0)), [0, 2]); %! assert_equal (size (topkrows (hours ([]), 2)), [0, 0]); %! assert_equal (size (topkrows (hours ([3, 1, 2]), 1)), [1, 3]); %!test %! [B, I] = topkrows (hours ([3; 1; NaN; 2]), 2); %! assert_equal (hours (B), [3; 2]); %! assert_equal (I, [1; 4]); %!error topkrows (days (1)) %!error ... %! topkrows (days (reshape (1:8, 2, 2, 2)), 1) %!error ... %! topkrows (days ([1, 2; 3, 4]), 2.5) %!error ... %! topkrows (days ([1, 2; 3, 4]), -1) %!error ... %! topkrows (days ([1, 2; 3, 4]), 1, -1) %!error ... %! topkrows (days ([1, 2; 3, 4]), 1, 5) %!error ... %! topkrows (days ([1, 2; 3, 4]), 1, 1, 'bogus') %!error ... %! topkrows (days ([1, 2; 3, 4]), 1, 'MissingPlacement', 'bogus') %!error ... %! topkrows (days ([1, 2; 3, 4]), 1, 'MissingPlacement') %!assert_equal (unique (days ([1, 2, 2, 3, 4, 4])), days ([1:4])) %!assert_equal (unique (days ([1, 2, 2, 3, 4, 4]')), days ([1:4]')) %!test %! D = days ([1, 2, 2, 3, 4, 4]); %! [c, i, j] = unique (D); %! assert_equal (c, D(i)); %! assert_equal (c(j), D); %!assert_equal (unique (days ([1, 4, 2, 2, 3]), 'sorted'), days (1:4)) %!test %! D = days ([1, 4, 2, 2, 3]); %! [c, i, j] = unique (D, 'sorted'); %! assert_equal (c, days (1:4)); %! assert_equal (c, D(i)); %! assert_equal (c(j), D); %!test %! D = days ([1, 4, 2, 2, 3]); %! [c, i, j] = unique (D, 'stable'); %! assert_equal (c, days ([1, 4, 2, 3])); %! assert_equal (c, D(i)); %! assert_equal (c(j), D); %!test %! D = days ([1, 4, 2, 2, 3]); %! [c, i, j] = unique (D, 'sorted', 'last'); %! assert_equal (i, [1; 4; 5; 2]); %! assert_equal (c, D(i)); %! assert_equal (c(j), D); %!test %! D = days ([1, 4, 2, 2, 3]); %! [c, i, j] = unique (D, 'stable', 'last'); %! assert_equal (i, [1; 2; 4; 5]); %! assert_equal (c, D(i)); %! assert_equal (c(j), D); %!test %! D = days ([3, 1, 3; 3, 3, 1; 1, 3, 3; 3, 2, 3; 2, 3, 3; ... %! 1, 1, 3; 1, 2, 3; 2, 3, 2; 3, 3, 2; 3, 3, 1]); %! [c, i, j] = unique (D(:,[1:2]), 'rows'); %! assert_equal (c, days ([1, 1; 1, 2; 1, 3; 2, 3; 3, 1; 3, 2; 3, 3])); %! assert_equal (i, [6; 7; 3; 5; 1; 4; 2]); %! assert_equal (j, [5; 7; 3; 6; 4; 1; 2; 4; 7; 7]); %! assert_equal (D(i,[1:2]), days ([1, 1; 1, 2; 1, 3; 2, 3; 3, 1; 3, 2; 3, 3])); %!test %! [B, ia, ib] = unique (days ([1, 2; 2, 3; 1, 2]), 'rows', 'sorted', 'first'); %! assert_equal (B, days ([1, 2; 2, 3])); %! assert_equal (ia, [1; 2]); %! assert_equal (ib, [1; 2; 1]); %!test %! [B, ia, ib] = unique (days ([1, 2; 2, 3; 1, 2]), 'rows', 'sorted', 'last'); %! assert_equal (B, days ([1, 2; 2, 3])); %! assert_equal (ia, [3; 2]); %! assert_equal (ib, [1; 2; 1]); %!test %! [B, ia, ib] = unique (days ([1, 2; 2, 3; 1, 2]), 'rows', 'stable', 'first'); %! assert_equal (B, days ([1, 2; 2, 3])); %! assert_equal (ia, [1; 2]); %! assert_equal (ib, [1; 2; 1]); %!test %! [B, ia, ib] = unique (days ([1, 2; 2, 3; 1, 2]), 'rows', 'stable', 'last'); %! assert_equal (B, days ([2, 3; 1, 2])); %! assert_equal (ia, [2; 3]); %! assert_equal (ib, [2; 1; 2]); %!error ... %! unique (days ([1:3]), 'legacy') %!assert_equal (interp1 (days ([1:5]), 1:5), days (1:5)) %!assert_equal (interp1 (days ([1:5]), 1:6, 'linear', days(1)), days ([1:5,1])) %!assert_equal (interp1 (1:5, days ([1:5])), [1:5]) %!assert_equal (interp1 (1:5, days ([1:6]), 'linear', 1), [1:5,1]) %!assert_equal (interp1 (days ([1:5]), 1:5, days ([1.5, 2.5, 3.5, 4.5])), ... %! [1.5, 2.5, 3.5, 4.5]) %!assert_equal (interp1 (1:5, days ([1:5]), [1.5, 2.5, 3.5, 4.5]), ... %! days ([1.5, 2.5, 3.5, 4.5])) %!assert_equal (interp1 (days ([1:5]), days ([1:5]), days ([1.5, 2.5, 3.5, ... %! 4.5])), ... %! days ([1.5, 2.5, 3.5, 4.5])) %!assert_equal (isequaln (interp1 (1:5, days ([1:5]), [-1.5, 2.5, 3.5, ... %! 6.5]), ... %! days ([NaN, 2.5, 3.5, NaN])), true) %!assert_equal (class (interp1 (1:5, days ([1:5]), 'pp')), 'struct') %!assert_equal (isequaln (interp1 (1:5, days ([1:5]), [-1.5, 2.5, 3.5, 6.5], ... %! 'linear'), ... %! days ([NaN, 2.5, 3.5, NaN])), true) %!assert_equal (interp1 (1:5, days ([1:5]), [-1.5, 2.5, 3.5, 6.5], 'linear', ... %! days (2)), ... %! days ([2, 2.5, 3.5, 2])) %!assert_equal (interp1 (1:5, days ([1:5]), [-1.5, 2.5, 3.5, 6.5], 'linear', ... %! 'extrap'), ... %! days ([-1.5, 2.5, 3.5, 6.5])) %!test %! YI = interp1 ([1:5], hours (1:5), [1.5, 2.5, 3.5, 4.5]); %! assert_equal (round (YI), round (hours ([1.5, 2.5, 3.5, 4.5]))); %! assert_equal (YI.Format, 'h'); %!test %! YI = interp1 (days ([1:5]), hours (1:5), days ([1.5, 2.5, 3.5, 4.5])); %! assert_equal (round (YI), round (hours ([1.5, 2.5, 3.5, 4.5]))); %! assert_equal (YI.Format, 'h'); %!test %! YI = interp1 (hours ([1:5]), days (1:5), hours ([1.5, 2.5, 3.5, 4.5])); %! assert_equal (round (YI), round (days ([1.5, 2.5, 3.5, 4.5]))); %! assert_equal (YI.Format, 'd'); %!error ... %! interp1 (1:5, days ([1:5]), 'linear', days (2)) %!error ... %! interp1 (days ([1:5]), 1:5, 'linear', 2) %!error ... %! interp1 (days ([1:5]), 1:5, [-1.5, 2.5, 3.5, 6.5]) %!error ... %! interp1 (1:5, days ([1:5]), days ([-1.5, 2.5, 3.5, 6.5])) %!error ... %! interp1 (1:5, days ([1:5]), [-1.5, 2.5, 3.5, 6.5], 'linear', 2) %!error ... %! interp1 (days ([1:5]), 1:5, days ([-1.5, 2.5, 3.5, 6.5]), 'linear', days (2)) %!error ... %! interp1 (days ([1:5]), "asd", days ([1.5, 2.5, 3.5, 4.5])) %!error ... %! interp1 (days ([1:5]), {1:5}, days ([1.5, 2.5, 3.5, 4.5])) %!test %! A = days ([7 1 7 7 4]); %! B = days ([7 0 4 4 0]); %! [C, ixA, ixB] = intersect (A, B); %! assert_equal (C, days ([4, 7])); %! assert_equal (ixA, [5; 1]); %! assert_equal (ixB, [3; 1]); %!test %! A = days ([7 1 7 7 4]); %! B = days ([7 0 4 4 0]); %! [C, ixA, ixB] = intersect (A, B, 'sorted'); %! assert_equal (C, days ([4, 7])); %! assert_equal (ixA, [5; 1]); %! assert_equal (ixB, [3; 1]); %!test %! A = days ([7 1 7 7 4]); %! B = days ([7 0 4 4 0]); %! [C, ixA, ixB] = intersect (A, B, 'stable'); %! assert_equal (C, days ([7, 4])); %! assert_equal (ixA, [1; 5]); %! assert_equal (ixB, [1; 3]); %!test %! A = days ([2, 2, 2; 0, 0, 1; 1, 2, 3; 1, 1, 1]); %! B = days ([1, 2, 3; 2, 2, 2; 2, 2, 0]); %! [C, ixA, ixB] = intersect (A, B, 'rows'); %! assert_equal (C, days ([1, 2, 3; 2, 2, 2])); %! assert_equal (ixA, [3; 1]); %! assert_equal (ixB, [1; 2]); %!test %! A = days ([2, 2, 2; 0, 0, 1; 1, 2, 3; 1, 1, 1]); %! B = days ([1, 2, 3; 2, 2, 2; 2, 2, 0]); %! [C, ixA, ixB] = intersect (A, B, 'sorted', 'rows'); %! assert_equal (C, days ([1, 2, 3; 2, 2, 2])); %! assert_equal (ixA, [3; 1]); %! assert_equal (ixB, [1; 2]); %!test %! A = days ([2, 2, 2; 0, 0, 1; 1, 2, 3; 1, 1, 1]); %! B = days ([1, 2, 3; 2, 2, 2; 2, 2, 0]); %! [C, ixA, ixB] = intersect (A, B, 'stable', 'rows'); %! assert_equal (C, days ([2, 2, 2; 1, 2, 3])); %! assert_equal (ixA, [1; 3]); %! assert_equal (ixB, [2; 1]); %!assert_equal (intersect (days ([5, NaN, NaN]), days ([5, NaN, ... %! NaN])), days (5)) %!assert_equal (intersect (days ([1, 2]), '48:00:00'), days (2)) %!assert_equal (intersect (days ([1, 2]), {'48:00:00'}), days (2)) %!assert_equal (intersect (days ([1, 2]), string ({'48:00:00'})), days (2)) %!assert_equal (intersect (days ([1, 2]), 2), days (2)) %!assert_equal (intersect ('48:00:00', days ([1, 2])), days (2)) %!assert_equal (intersect ({'48:00:00'}, days ([1, 2])), days (2)) %!assert_equal (intersect (string ({'48:00:00'}), days ([1, 2])), days (2)) %!assert_equal (intersect (2, days ([1, 2])), days (2)) %!assert_equal (intersect (days ([2, 3]), 1:3), days ([2, 3])) %!error ... %! intersect (days ([1, 2]), days (2), 'legacy') %!error ... %! intersect ({1}, days ([1, 2])) %!error ... %! intersect (days ([1, 2]), caldays) %!error ... %! intersect (days ([1, 2]), datetime) %!test %! A = days ([3, 6, 2, 1, 5, 1, 1]); %! B = days ([2, 4, 6]); %! [C, index] = setdiff (A, B); %! assert_equal (C, days ([1, 3, 5])); %! assert_equal (index, [4; 1; 5]); %!test %! A = days ([3, 6, 2, 1, 5, 1, 1]); %! B = days ([2, 4, 6]); %! [C, index] = setdiff (A, B, 'sorted'); %! assert_equal (C, days ([1, 3, 5])); %! assert_equal (index, [4; 1; 5]); %!test %! A = days ([3, 6, 2, 1, 5, 1, 1]); %! B = days ([2, 4, 6]); %! [C, index] = setdiff (A, B, 'stable'); %! assert_equal (C, days ([3, 1, 5])); %! assert_equal (index, [1; 4; 5]); %!test %! A = days ([7, 9, 7; 0, 0, 0; 7, 9, 7; 5, 5, 5; 1, 4, 5]); %! B = days ([0, 0, 0; 5, 5, 5]); %! [C, index] = setdiff (A, B, 'rows'); %! assert_equal (C, days ([1, 4, 5; 7, 9, 7])); %! assert_equal (index, [5; 1]); %!test %! A = days ([7, 9, 7; 0, 0, 0; 7, 9, 7; 5, 5, 5; 1, 4, 5]); %! B = days ([0, 0, 0; 5, 5, 5]); %! [C, index] = setdiff (A, B, 'sorted', 'rows'); %! assert_equal (C, days ([1, 4, 5; 7, 9, 7])); %! assert_equal (index, [5; 1]); %!test %! A = days ([7, 9, 7; 0, 0, 0; 7, 9, 7; 5, 5, 5; 1, 4, 5]); %! B = days ([0, 0, 0; 5, 5, 5]); %! [C, index] = setdiff (A, B, 'stable', 'rows'); %! assert_equal (C, days ([7, 9, 7; 1, 4, 5])); %! assert_equal (index, [1; 5]); %!test %! C = setdiff (days ([5, NaN, NaN]), days ([5, NaN, NaN])); %! assert_equal (isequaln (C, days ([NaN, NaN])), true); %!assert_equal (setdiff (days ([1, 2]), days (2)), days (1)) %!assert_equal (setdiff (days ([1, 2]), '48:00:00'), days (1)) %!assert_equal (setdiff (days ([1, 2]), string ({'48:00:00'})), days (1)) %!assert_equal (size (setdiff (days ([1, 2]), string ({'48:00:00', ... %! '24:00:00'}))), [1, 0]) %!assert_equal (class (setdiff (days ([1, 2]), string ({'48:00:00', ... %! '24:00:00'}))), 'duration') %!assert_equal (setdiff (days ([1, 2, 3]), string ({'48:00:00', ... %! '24:00:00'})), days (3)) %!assert_equal (setdiff (string ({'24:00:00', '48:00:00'}), days (2)), days (1)) %!assert_equal (size (setdiff (string ({'24:00:00'}), days ([1, 2]))), [1, 0]) %!assert_equal (class (setdiff (string ({'24:00:00'}), days ([1, ... %! 2]))), 'duration') %!assert_equal (size (setdiff ('24:00:00', days ([1, 2]))), [1, 0]) %!assert_equal (class (setdiff ('24:00:00', days ([1, 2]))), 'duration') %!assert_equal (setdiff ([1, 2, 3], days (2)), days ([1, 3])) %!assert_equal (size (setdiff (days ([2, 3]), 1:3)), [1, 0]) %!error ... %! setdiff (days ([1, 2]), days (2), 'legacy') %!error ... %! setdiff ({1}, days ([1, 2])) %!error ... %! setdiff (days ([1, 2]), caldays) %!error ... %! setdiff (days ([1, 2]), datetime) %!test %! A = days ([5, 1, 3, 3, 3]); %! B = days ([4, 1, 2]); %! [C, ixA, ixB] = setxor (A, B); %! assert_equal (C, days ([2, 3, 4, 5])); %! assert_equal (ixA, [3; 1]); %! assert_equal (ixB, [3; 1]); %!test %! A = days ([5, 1, 3, 3, 3]); %! B = days ([4, 1, 2]); %! [C, ixA, ixB] = setxor (A, B, 'sorted'); %! assert_equal (C, days ([2, 3, 4, 5])); %! assert_equal (ixA, [3; 1]); %! assert_equal (ixB, [3; 1]); %!test %! A = days ([5, 1, 3, 3, 3]); %! B = days ([4, 1, 2]); %! [C, ixA, ixB] = setxor (A, B, 'stable'); %! assert_equal (C, days ([5, 3, 4, 2])); %! assert_equal (ixA, [1; 3]); %! assert_equal (ixB, [1; 3]); %!test %! A = days ([7, 8, 9; 7, 7, 1; 7, 7, 1; 1, 2, 3; 4, 5, 6]); %! B = days ([1, 2, 3; 4, 5, 6; 7, 7, 2]); %! [C, ixA, ixB] = setxor (A, B, 'rows'); %! assert_equal (C, days ([7, 7, 1; 7, 7, 2; 7, 8, 9])); %! assert_equal (ixA, [2; 1]); %! assert_equal (ixB, [3]); %!test %! A = days ([7, 8, 9; 7, 7, 1; 7, 7, 1; 1, 2, 3; 4, 5, 6]); %! B = days ([1, 2, 3; 4, 5, 6; 7, 7, 2]); %! [C, ixA, ixB] = setxor (A, B, 'sorted', 'rows'); %! assert_equal (C, days ([7, 7, 1; 7, 7, 2; 7, 8, 9])); %! assert_equal (ixA, [2; 1]); %! assert_equal (ixB, [3]); %!test %! A = days ([7, 8, 9; 7, 7, 1; 7, 7, 1; 1, 2, 3; 4, 5, 6]); %! B = days ([1, 2, 3; 4, 5, 6; 7, 7, 2]); %! [C, ixA, ixB] = setxor (A, B, 'stable', 'rows'); %! assert_equal (C, days ([7, 8 ,9; 7, 7, 1; 7, 7, 2])); %! assert_equal (ixA, [1; 2]); %! assert_equal (ixB, [3]); %!test %! C = setxor (days ([5, NaN, NaN]), days ([5, NaN, NaN])); %! assert_equal (isequaln (C, days ([NaN, NaN, NaN, NaN])), true); %!assert_equal (setxor (days ([1, 2]), days (2)), days (1)) %!assert_equal (setxor (days ([1, 2]), '48:00:00'), days (1)) %!assert_equal (setxor (days ([1, 2]), string ('48:00:00')), days (1)) %!assert_equal (setxor (days ([1, 2]), string ('24:00:00')), days (2)) %!assert_equal (size (setxor (days ([1, 2]), string ({'48:00:00', ... %! '24:00:00'}))), [1, 0]) %!assert_equal (class (setxor (days ([1, 2]), string ({'48:00:00', ... %! '24:00:00'}))), 'duration') %!assert_equal (setxor (days ([1, 2, 3]), string ({'48:00:00', ... %! '24:00:00'})), days (3)) %!assert_equal (setxor (days ([1, 2, 3]), {'48:00:00', '24:00:00'}), days (3)) %!assert_equal (setxor ({'24:00:00', '48:00:00'}, days (2)), days (1)) %!assert_equal (setxor (string ({'24:00:00', '48:00:00'}), days (2)), days (1)) %!assert_equal (setxor (string ('24:00:00'), days ([1, 2])), days (2)) %!assert_equal (setxor ('24:00:00', days ([1, 2])), days (2)) %!assert_equal (setxor ([1, 2, 3], days (2)), days ([1, 3])) %!assert_equal (setxor (days ([2, 3]), 1:3), days (1)) %!error ... %! setxor (days ([1, 2]), days (2), 'legacy') %!error ... %! setxor ({1}, days ([1, 2])) %!error ... %! setxor (days ([1, 2]), caldays) %!error ... %! setxor (days ([1, 2]), datetime) %!test %! A = days ([5, 7, 1]); %! B = days ([3, 1, 1]); %! [C, ixA, ixB] = union (A, B); %! assert_equal (C, days ([1, 3, 5, 7])); %! assert_equal (ixA, [3; 1; 2]); %! assert_equal (ixB, 1); %!test %! A = days ([5, 7, 1]); %! B = days ([3, 1, 1]); %! [C, ixA, ixB] = union (A, B, 'sorted'); %! assert_equal (C, days ([1, 3, 5, 7])); %! assert_equal (ixA, [3; 1; 2]); %! assert_equal (ixB, 1); %!test %! A = days ([5, 7, 1]); %! B = days ([3, 1, 1]); %! [C, ixA, ixB] = union (A, B, 'stable'); %! assert_equal (C, days ([5, 7, 1, 3])); %! assert_equal (ixA, [1; 2; 3]); %! assert_equal (ixB, 1); %!test %! A = days ([2, 2, 2; 0, 0, 1]); %! B = days ([1, 2, 3; 2, 2, 2; 2, 2, 2]); %! [C, ixA, ixB] = union (A, B, 'rows'); %! assert_equal (C, days ([0, 0, 1; 1, 2, 3; 2, 2, 2])); %! assert_equal (ixA, [2; 1]); %! assert_equal (ixB, 1); %!test %! A = days ([2, 2, 2; 0, 0, 1]); %! B = days ([1, 2, 3; 2, 2, 2; 2, 2, 2]); %! [C, ixA, ixB] = union (A, B, 'sorted', 'rows'); %! assert_equal (C, days ([0, 0, 1; 1, 2, 3; 2, 2, 2])); %! assert_equal (ixA, [2; 1]); %! assert_equal (ixB, 1); %!test %! A = days ([2, 2, 2; 0, 0, 1]); %! B = days ([1, 2, 3; 2, 2, 2; 2, 2, 2]); %! [C, ixA, ixB] = union (A, B, 'stable', 'rows'); %! assert_equal (C, days ([2, 2, 2; 0, 0, 1; 1, 2, 3])); %! assert_equal (ixA, [1; 2]); %! assert_equal (ixB, 1); %!test %! C = union (days ([5, NaN, 1]), days ([4, NaN, NaN])); %! assert_equal (isequaln (C, days ([1, 4, 5, NaN, NaN, NaN])), true); %!assert_equal (union (days ([1, 3]), days (2)), days ([1, 2, 3])) %!assert_equal (union (days ([1, 3]), '48:00:00'), days ([1, 2, 3])) %!assert_equal (union (days ([1, 4]), string ('48:00:00')), days ([1, 2, 4])) %!assert_equal (union (days ([1, 4]), {'48:00:00'}), days ([1, 2, 4])) %!assert_equal (union (days ([1, 1]), string ({'48:00:00', ... %! '24:00:00'})), days ([1, 2])) %!assert_equal (union (days ([1, 1]), {'48:00:00', '24:00:00'}), days ([1, 2])) %!assert_equal (union (days ([1, 3]), string ({'48:00:00', ... %! '24:00:00'})), days ([1, 2, 3])) %!assert_equal (union (string ({'24:00:00', '48:00:00'}), ... %! days (2)), days ([1, 2])) %!assert_equal (union (string ({'24:00:00'}), days ([1, 4])), days ([1, 4])) %!assert_equal (union ('24:00:00', days ([0, 2])), days ([0, 1, 2])) %!assert_equal (union ([1, 2, 3], days (2)), days ([1, 2, 3])) %!assert_equal (union (days ([2, 3]), 1:3), days ([1, 2, 3])) %!error ... %! union (days ([1, 2]), days (2), 'legacy') %!error ... %! union ({1}, days ([1, 2])) %!error ... %! union (days ([1, 2]), caldays) %!error ... %! union (days ([1, 2]), datetime) ################################################################################ ## ** Array Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'cat' 'horzcat' 'vertcat' 'repmat' ## ## 'repelem' 'repelems' 'reshape' 'circshift' ## ## 'permute' 'ipermute' 'transpose' 'ctranspose' ## ## ## ################################################################################ ## Test 'cat' -- a missing value on either side becomes an all-NaN duration, ## and the result keeps the real operand's format (vs MATLAB R2024a) %!test %! D = [duration(1, 0, 0), missing]; %! assert_equal (size (D), [1, 2]); %! assert_equal (ismissing (D), [false, true]); %!test %! D = [hours(1), missing]; %! assert_equal (D.Format, 'h'); %! assert_equal (cellstr (D), {'1 hr', 'NaN hr'}); %!test %! D = [duration(1, 0, 0), missing, duration(2, 0, 0)]; %! assert_equal (cellstr (D), {'01:00:00', 'NaN', '02:00:00'}); %!assert_equal (size ([duration(1, 0, 0); missing]), [2, 1]) %!assert_equal (size (cat (3, duration(1, 0, 0), missing)), [1, 1, 2]) ## Test 'subsasgn' -- assigning a missing value gives NaN, while a gap opened ## by growing past the end is zero, not missing (vs MATLAB R2024a) %!test %! D = duration (1, 0, 0); %! D(3) = missing; %! assert_equal (ismissing (D), [false, false, true]); %!test %! D = cat (2, days (2), hours (3)); %! assert_equal (D, days ([2, 0.125])); %! assert_equal (D.Format, 'd'); %!test %! D = cat (2, hours (3), days (3)); %! assert_equal (D, hours ([3, 72])); %! assert_equal (D.Format, 'h'); %!test %! D = cat (2, '24:00:00', hours (3), days (3)); %! assert_equal (D, hours ([24, 3, 72])); %! assert_equal (D.Format, 'h'); %!test %! D = cat (2, string ('24:00:00'), hours (3), days (3)); %! assert_equal (D, hours ([24, 3, 72])); %! assert_equal (D.Format, 'h'); %!test %! D = cat (2, string ('24:00:00:00'), hours (3), days (3)); %! assert_equal (D, hours ([576, 3, 72])); %! assert_equal (D.Format, 'h'); %!test %! D = cat (2, seconds (3), string ('24:00:00:00'), hours (3), days (3)); %! assert_equal (D, seconds ([3, 2.0736e+06, 10800, 2.592e+05])); %! assert_equal (D.Format, 's'); %!test %! D = cat (2, string ('24:00:00'), duration (3, 0, 0), days (3)); %! assert_equal (D, hours ([24, 3, 72])); %! assert_equal (D.Format, 'hh:mm:ss'); %!test %! D = cat (1, days (2), hours (3)); %! assert_equal (D, days ([2; 0.125])); %! assert_equal (D.Format, 'd'); %!test %! D = cat (1, hours (3), days (3)); %! assert_equal (D, hours ([3; 72])); %! assert_equal (D.Format, 'h'); %!test %! D = cat (1, '24:00:00', hours (3), days (3)); %! assert_equal (D, hours ([24; 3; 72])); %! assert_equal (D.Format, 'h'); %!test %! D = cat (1, string ('24:00:00'), hours (3), days (3)); %! assert_equal (D, hours ([24; 3; 72])); %! assert_equal (D.Format, 'h'); %!test %! D = cat (1, string ('24:00:00:00'), hours (3), days (3)); %! assert_equal (D, hours ([576; 3; 72])); %! assert_equal (D.Format, 'h'); %!test %! D = cat (1, seconds (3), string ('24:00:00:00'), hours (3), days (3)); %! assert_equal (D, seconds ([3; 2.0736e+06; 10800; 2.592e+05])); %! assert_equal (D.Format, 's'); %!test %! D = cat (1, string ('24:00:00'), duration (3, 0, 0), days (3)); %! assert_equal (D, hours ([24; 3; 72])); %! assert_equal (D.Format, 'hh:mm:ss'); %!test %! D = [days(2), hours(3)]; %! assert_equal (D, days ([2, 0.125])); %! assert_equal (D.Format, 'd'); %!test %! D = [hours(3), days(3)]; %! assert_equal (D, hours ([3, 72])); %! assert_equal (D.Format, 'h'); %!test %! D = ['24:00:00', hours(3), days(3)]; %! assert_equal (D, hours ([24, 3, 72])); %! assert_equal (D.Format, 'h'); %!test %! D = [string('24:00:00'), hours(3), days(3)]; %! assert_equal (D, hours ([24, 3, 72])); %! assert_equal (D.Format, 'h'); %!test %! D = [string('24:00:00:00'), hours(3), days(3)]; %! assert_equal (D, hours ([576, 3, 72])); %! assert_equal (D.Format, 'h'); %!test %! D = [seconds(3), string('24:00:00:00'), hours(3), days(3)]; %! assert_equal (D, seconds ([3, 2.0736e+06, 10800, 2.592e+05])); %! assert_equal (D.Format, 's'); %!test %! D = [string('24:00:00'), duration(3, 0, 0), days(3)]; %! assert_equal (D, hours ([24, 3, 72])); %! assert_equal (D.Format, 'hh:mm:ss'); %!test %! D = [days(2); hours(3)]; %! assert_equal (D, days ([2; 0.125])); %! assert_equal (D.Format, 'd'); %!test %! D = [hours(3); days(3)]; %! assert_equal (D, hours ([3; 72])); %! assert_equal (D.Format, 'h'); %!test %! D = ['24:00:00'; hours(3); days(3)]; %! assert_equal (D, hours ([24; 3; 72])); %! assert_equal (D.Format, 'h'); %!test %! D = [string('24:00:00'); hours(3); days(3)]; %! assert_equal (D, hours ([24; 3; 72])); %! assert_equal (D.Format, 'h'); %!test %! D = [string('24:00:00:00'); hours(3); days(3)]; %! assert_equal (D, hours ([576; 3; 72])); %! assert_equal (D.Format, 'h'); %!test %! D = [seconds(3); string('24:00:00:00'); hours(3); days(3)]; %! assert_equal (D, seconds ([3; 2.0736e+06; 10800; 2.592e+05])); %! assert_equal (D.Format, 's'); %!test %! D = [string('24:00:00'); duration(3, 0, 0); days(3)]; %! assert_equal (D, hours ([24; 3; 72])); %! assert_equal (D.Format, 'hh:mm:ss'); %!assert_equal (repmat (days ([1, 2]), 4), days (repmat ([1, 2], 4))) %!assert_equal (repmat (days ([1, 2]), 2, 3, 4), days (repmat ([1, 2], 2, 3, ... %! 4))) %!assert_equal (repmat (days ([1, 2]), [2, 3]), days (repmat ([1, 2], [2, 3]))) %!assert_equal (repelem (days ([1:3]), 4), days (repelem ([1:3], 4))) %!assert_equal (repelem (days ([1:3]), 2, 3, 4), days (repelem ([1:3], 2, 3, ... %! 4))) %!assert_equal (repelem (days ([1:3]), [2, 3, 3]), days (repelem ([1:3], [2, ... %! 3, 3]))) %!assert_equal (repelems (days ([1:5]), [1, 3, 5; 2, 1, 2]), days ([1, 1, 3, ... %! 5, 5])) %!assert_equal (reshape (days (magic (4)), 2, 2, ... %! 4), days (reshape (magic (4), 2, 2, 4))) %!assert_equal (reshape (days (magic (4)), [2, 2, ... %! 4]), days (reshape (magic (4), [2, 2, 4]))) %!assert_equal (reshape (days (magic (4)), [], 2, ... %! 2), days (reshape (magic (4), [], 2, 2))) %!assert_equal (circshift (days (magic (4)), 2), days (circshift (magic (4), ... %! 2))) %!assert_equal (circshift (days (magic (4)), 1, ... %! 2), days (circshift (magic (4), 1, 2))) %!assert_equal (circshift (days (magic (4)), 1, ... %! 1), days (circshift (magic (4), 1, 1))) %!assert_equal (circshift (days (magic (4)), 1, ... %! 1), days (circshift (magic (4), 1))) %!test %! X = randi (10, 2, 3, 4); %! D = days (X); %! assert_equal (size (permute (D, [3, 1, 2])), [4, 2, 3]); %! assert_equal (permute (D, [3, 1, 2]), days (permute (X, [3, 1, 2]))); %! assert_equal (D, ipermute (permute (D, [3, 1, 2]), [3, 1, 2])); %!assert_equal (transpose (days ([1, 2; 3, 4])), days ([1, 3; 2, 4])) %!assert_equal (transpose (days ([1, 2; 3, 4])), ctranspose (days ([1, 2; 3, ... %! 4]))) %!assert_equal (ctranspose (days ([1, 2; 3, 4])), days ([1, 3; 2, 4])) %!assert_equal (ctranspose (days ([1, 2; 3, 4])), transpose (days ([1, 2; 3, ... %! 4]))) ################################################################################ ## ** Reference and Assignment Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'end' 'subsref' 'subsasgn' ## ## ## ################################################################################ %!test %! X = magic (3); %! D = days (X); %! assert_equal (days (D(:, end)), X(:,end)); %!test %! X = magic (3); %! D = days (X); %! assert_equal (days (D([1:end-1], end)), [6; 7]); %!test %! D = duration ([1:5], 0, 0); %! out = D([2,3]); %! assert_equal (size (out), [1, 2]); %! assert_equal (hours (out), [2, 3]); %!test %! D = duration ([1:5], 0, 0); %! assert_equal (D.Format, 'hh:mm:ss'); %!test %! D = duration ([1:5], 0, 0, 'Format', 'hh:mm'); %! assert_equal (D.Format, 'hh:mm'); %!test %! D = duration ([1:5], 0, 0, 'Format', 'y'); %! assert_equal (D.Format, 'y'); %!error ... %! D = duration ([1:5], 0, 0); D{1} %!error ... %! D = duration ([1:5], 0, 0); D.form %!test %! D = duration ([1:5], 0, 0); %! D([2,3]) = [4, 5]; %! assert_equal (hours (D), [1, 96, 120, 4, 5]); %!test %! D = duration ([1:5], 0, 0); %! D([2,3]) = hours ([4, 5]); %! assert_equal (hours (D), [1, 4, 5, 4, 5]); %!test %! D = days ([1:5]); %! D([2,3]) = {'12:23:42', '00:12:23'}; %! assert_equal (days (D), [1, 0.51646, 0.0085995, 4, 5], 1e-5); %!test %! D = days ([1:5]); %! D([2,3]) = string ({'12:23:42', '00:12:23'}); %! assert_equal (days (D), [1, 0.51646, 0.0085995, 4, 5], 1e-5); %!test %! D = days ([1:5]); %! D([2,3]) = ['12:23:42'; '00:12:23']; %! assert_equal (days (D), [1, 0.51646, 0.0085995, 4, 5], 1e-5); %!test %! D = days ([1:5]); %! D([2,3]) = {'05:12:00:00', '01:00:00:00'}; %! assert_equal (days (D), [1, 5.5, 1, 4, 5]); %!test %! D = days ([1:5]); %! D([2,3]) = string ({'05:12:00:00', '01:00:00:00'}); %! assert_equal (days (D), [1, 5.5, 1, 4, 5]); %!test %! D = days ([1:5]); %! D([2,3]) = ['05:12:00:00'; '01:00:00:00']; %! assert_equal (days (D), [1, 5.5, 1, 4, 5]); %!test %! D = days ([1:5]); %! D([2,3]) = NaN; %! assert_equal (days (D), [1, NaN, NaN, 4, 5]); %! assert_equal (dispstrings (D), {'1 day', 'NaN days', 'NaN days', ... %! '4 days', '5 days'}); %!test %! D = days ([1:5]); %! D([1:3]) = missing; %! assert_equal (days (D), [NaN, NaN, NaN, 4, 5]); %! assert_equal (dispstrings (D), {'NaN days', 'NaN days', 'NaN days', ... %! '4 days', '5 days'}); %!test %! D = days ([1:5]); %! D([2,3]) = [Inf, -Inf]; %! assert_equal (days (D), [1, Inf, -Inf, 4, 5]); %! assert_equal (dispstrings (D), {'1 day', 'Inf days', '-Inf days', ... %! '4 days', '5 days'}); %!test %! D = days ([1:5]); %! D([1:3]) = 2; %! assert_equal (days (D), [2, 2, 2, 4, 5]); %!test %! D = days ([1:5]); %! D([1:3]) = [NaN, 2, 3]; %! assert_equal (days (D), [NaN, 2, 3, 4, 5]); %!test %! D = duration (0, [1:5], 0); %! D([1:3]) = [NaN, 2, 3]; %! assert_equal (minutes (D), [NaN, 48 * 60, 72 * 60, 4, 5]); %! assert_equal (dispstrings (D), {'NaN', '48:00:00', '72:00:00', ... %! '00:04:00', '00:05:00'}); %!error ... %! D = duration (); D(1)(1) = 1 %!error ... %! D = duration (); D(1) = {1} %!error ... %! D = duration ([1:5], 0, 0); D{1} = 1 %!error ... %! D = duration ([1:5], 0, 0); D.form = 'f' %!error ... %! D = duration ([1:5], 0, 0); D.Format = 3 %!error ... %! D = duration ([1:5], 0, 0); D.Format = ['y';'d'] %!error ... %! D = duration ([1:5], 0, 0); D.Format = string ({'mm:ss', 'hh:mm'}) %!error ... %! D = duration ([1:5], 0, 0); D.Format = 't' %!error ... %! D = duration ([1:5], 0, 0); D.Format = 'd.h.S' %!error ... %! D = duration ([1:5], 0, 0); D.Format = 's.SS' %!error ... %! D = duration ([1:5], 0, 0); D.Format = 'hh:mm.SS' %!error ... %! D = duration ([1:5], 0, 0); D.Format = 'hh:mm.SM' %!error ... %! D = duration ([1:5], 0, 0); D.Format = 'hh:mm:ss.SSSSSSSSSS' ## Test 'subsref' names a property with a string scalar %!test %! D = duration (1, 2, 3); %! s = struct ('type', '.', 'subs', string ('Format')); %! assert_equal (subsref (D, s), D.Format); ## Test 'subsasgn' names a property with a string scalar %!test %! D = duration (1, 2, 3); %! s = struct ('type', '.', 'subs', string ('Format')); %! assert_equal (subsasgn (D, s, 'hh:mm').Format, 'hh:mm'); ## Test 'subsref' refuses a property name that is not a single row %!error ... %! subsref (duration (1, 2, 3), struct ('type', '.', 'subs', ['ab'; 'cd'])); ## Test 'subsasgn' refuses a property name that is not a single row %!error ... %! subsasgn (duration (1, 2, 3), struct ('type', '.', 'subs', ''), 5); %!test %! D = hours (0:9); %! [b, e] = discretize (D, 3); %! assert_equal (b, [1, 1, 1, 2, 2, 2, 3, 3, 3, 3]); %! assert_equal (hours (e), [0, 3, 6, 9], 1e-12); %! [~, e] = discretize (seconds (0:5), 3); %! assert_equal (seconds (e), [-1, 1, 3, 5], 1e-12); %! [~, e] = discretize (seconds (0:600), 3); %! assert_equal (seconds (e), [-60, 180, 420, 660], 1e-9); %! [~, e] = discretize (hours (0:100), 3); %! assert_equal (seconds (e) / 86400, [-1, 1, 3, 5], 1e-12); %! [~, e] = discretize (days (0:40), 3); %! assert_equal (seconds (e) / 86400, [-1, 13, 27, 41], 1e-12); %!test %! D = hours (0:9); %! [~, e] = discretize (D, hours (2)); %! assert_equal (hours (e), [0, 2, 4, 6, 8, 10], 1e-12); %! [~, e] = discretize (D, 'hour'); %! assert_equal (hours (e), 0:9, 1e-12); %! b = discretize (D, hours ([0, 4, 9])); %! assert_equal (b, [1, 1, 1, 1, 2, 2, 2, 2, 2, 2]); %! b = discretize (D, hours ([0, 4, 9]), 'IncludedEdge', 'right'); %! assert_equal (b, [1, 1, 1, 1, 1, 2, 2, 2, 2, 2]); %! b = discretize (D, hours ([0, 4, 9]), [10, 20]); %! assert_equal (b, [10, 10, 10, 10, 20, 20, 20, 20, 20, 20]); %!test %! D = hours (0:9); %! [N, e] = histcounts (D, 3); %! assert_equal (N, [3, 3, 4]); %! assert_equal (hours (e), [0, 3, 6, 9], 1e-12); %! [N, e] = histcounts (D, 'NumBins', 3); %! assert_equal (N, [3, 3, 4]); %! [N, e] = histcounts (D, hours ([0, 4, 9])); %! assert_equal (N, [4, 6]); %! [~, e] = histcounts (D, 'BinWidth', hours (3)); %! assert_equal (hours (e), [0, 3, 6, 9], 1e-12); %! [~, e] = histcounts (D, 'BinMethod', 'hour'); %! assert_equal (hours (e), 0:9, 1e-12); %!test %! [~, e] = histcounts (seconds (0:90)); %! assert_equal (seconds (e), [0, 18, 36, 54, 72, 90], 1e-12); %! [~, e] = histcounts (seconds (0:600)); %! assert_equal (seconds (e), [-2, 65, 132, 199, 266, 333, 400, 467, 534, ... %! 601], 1e-9); %! [~, e] = histcounts (hours (0:9)); %! assert_equal (seconds (e), [-3600, 14400, 32400], 1e-9); %! [~, e] = histcounts (seconds (0:90), 'BinMethod', 'sturges'); %! assert_equal (seconds (e), [-3, 9, 21, 33, 45, 57, 69, 81, 93], 1e-12); %! [~, e] = histcounts (seconds (0:90), 'BinMethod', 'sqrt'); %! assert_equal (seconds (e), 0:9:90, 1e-12); %! [~, e] = histcounts (seconds (0:90), 'BinMethod', 'fd'); %! assert_equal (seconds (e), [0, 18, 36, 54, 72, 90], 1e-12); %!test %! D = hours (0:9); %! assert_equal (histcounts (D, 3, 'Normalization', 'cumcount'), [3, 6, 10]); %! assert_equal (histcounts (D, 3, 'Normalization', 'probability'), ... %! [0.3, 0.3, 0.4], 1e-12); %! assert_equal (histcounts (D, 3, 'Normalization', 'percentage'), ... %! [30, 30, 40], 1e-12); %! assert_equal (histcounts (D, 3, 'Normalization', 'cdf'), ... %! [0.3, 0.6, 1], 1e-12); %! [~, ~, b] = histcounts (D, 3); %! assert_equal (b, [1, 1, 1, 2, 2, 2, 3, 3, 3, 3]); %!error discretize (hours (1)); %!error ... %! discretize (hours (0:9), 0); %!error ... %! discretize (hours (0:9), 'fortnight'); %!error ... %! discretize (hours (0:9), [1, 2, 3]); %!error ... %! histcounts (hours (0:9), 'BinMethod', 'integers'); %!error ... %! histcounts (hours (0:9), 'Normalization', 'countdensity'); %!error ... %! histcounts (hours (0:9), 'Normalization', 'pdf'); %!error ... %! histcounts (hours (0:9), 'BinWidth', 3); ## Test 'histcounts' -- a single bin takes the width of the data, not of some ## far coarser unit. A unit is kept only when it both needs every bin and is ## no more than 3/4 of the raw width; keeping the first test alone returned a ## bin one YEAR wide for an hour of data. %!test %! [~, e] = histcounts (hours (0:1)); %! assert_equal (seconds (e), [0 3600]); %!test %! [~, e] = histcounts (hours (0:2)); %! assert_equal (seconds (e), [0 7200]); %!test %! [~, e] = histcounts (seconds (0:1)); %! assert_equal (seconds (e), [0 1]); %!test %! [~, e] = histcounts (seconds (0:2)); %! assert_equal (seconds (e), [0 2]); %!test %! [~, e] = histcounts (hours (0:100)); %! assert_equal (seconds (e), [0 72000 144000 216000 288000 360000]); ## This was once documented as a deliberate deviation, on the reading that ## R2024a left one of the seven bins unused. It does not: with the left edge ## placed correctly all seven are occupied, and we now agree with it exactly. %!test %! [~, e] = discretize (hours (0:24), 7); %! assert_equal (seconds (e), ... %! [-7200 7200 21600 36000 50400 64800 79200 93600]); %! assert_equal (unique (discretize (hours (0:24), 7)), 1:7); %!test %! [~, e] = discretize (hours (0:5), 3); %! assert_equal (seconds (e), [-3600 3600 10800 18000]); %!test %! [~, e] = discretize (days (0:40), 3); %! assert_equal (seconds (e) / 86400, [-1 13 27 41]); ## Test 'duration.empty' -- empty duration arrays %!assert_equal (size (duration.empty), [0, 0]) %!assert_equal (class (duration.empty), 'duration') %!assert_equal (isempty (duration.empty), true) %!assert_equal (duration.empty.Format, 'hh:mm:ss') %!assert_equal (size (duration.empty (2, 0)), [2, 0]) %!assert_equal (size (duration.empty (0, 3)), [0, 3]) %!error ... %! duration.empty (3) ## Test 'duration.empty' -- size arguments follow 'zeros' rules: a ## negative dimension counts as zero, a size vector holding none names no size %!assert_equal (size (duration.empty (0)), [0, 0]) %!assert_equal (size (duration.empty (-1)), [0, 0]) %!assert_equal (size (duration.empty (-2, -3)), [0, 0]) %!assert_equal (size (duration.empty ([])), [0, 0]) %!assert_equal (size (duration.empty (2, 0, 3)), [2, 0, 3]) %!assert_equal (size (duration.empty ([0 2 3])), [0, 2, 3]) %!error ... %! duration.empty (1.5) ## Test 'histcounts' and 'discretize' -- with no finite value to bin, the bins ## go where they would for a single zero, as MATLAB places them. The edges ## used to run 0, 1, ... N instead, which ignored the tick entirely. %!test %! [n, e] = histcounts (duration.empty); %! assert_equal (n, 0); %! assert_equal (seconds (e), [-0.0005 0.0005]); %!test %! [n, e] = histcounts (duration.empty, 3); %! assert_equal (n, [0 0 0]); %! assert_equal (max (abs (seconds (e) - (-0.0005 + (0:3) * 0.001 / 3))) ... %! < 1e-12, true); %!test %! [n, e] = histcounts (seconds ([NaN NaN])); %! assert_equal (n, 0); %! assert_equal (seconds (e), [-0.0005 0.0005]); %!test %! [b, e] = discretize (duration.empty, 3); %! assert_equal (isempty (b), true); %! assert_equal (max (abs (seconds (e) - (-0.0005 + (0:3) * 0.001 / 3))) ... %! < 1e-12, true); %!test %! [b, e] = discretize (seconds ([NaN NaN]), 3); %! assert_equal (isnan (b), [true true]); %!test %! [~, e] = histcounts (duration.empty, 'BinMethod', 'scott'); %! assert_equal (seconds (e), [-0.0005 0.0005]); ## Test 'histcounts' -- a named unit or an explicit width still tiles from zero ## when there is no data, rather than collapsing to the numeric [0 1] %!test %! [~, e] = histcounts (duration.empty, 'BinWidth', hours (1)); %! assert_equal (seconds (e), [0 3600]); %!test %! [~, e] = histcounts (duration.empty, 'BinMethod', 'hour'); %! assert_equal (seconds (e), [0 3600]); %!test %! [~, e] = histcounts (seconds ([NaN NaN]), 'BinMethod', 'hour'); %! assert_equal (seconds (e), [0 3600]); %!test %! [~, e] = discretize (duration.empty, hours (1)); %! assert_equal (seconds (e), [0 3600]); %!test %! [~, e] = histcounts (duration.empty, 'BinLimits', hours ([1 3])); %! assert_equal (seconds (e), [3600 10800]); ## Test that a lone duration in the bin-count position is refused. Passed on it ## became a bin COUNT of the milliseconds it held, so 'days (2)' asked for ## 172800000 bins and the array to hold them. %!error ... %! histcounts (days ((0:9)'), days (2)) %!assert_equal (histcounts (days ((0:9)'), days ((0:2:10)')), [2, 2, 2, 2, 2]) %!assert_equal (histcounts (days ((0:9)'), 5), [1, 2, 2, 2, 3]) %!assert_equal (histcounts (days ((0:9)'), 'BinWidth', days (2)), ... %! [2, 2, 2, 2, 2]) %!assert_equal (discretize (days ((0:9)'), days (2)), ... %! [1; 1; 2; 2; 3; 3; 4; 4; 5; 5]) ## Test the stored unit -- a duration counts milliseconds, as MATLAB's does, so ## a conversion out and back is exact rather than rounded to a picosecond grid. ## Every figure below is bit-exact against R2024a. %!assert_equal (seconds (seconds (4.9998333333333331)), 4.9998333333333331) %!assert_equal (seconds (seconds (1 / 3)), 1 / 3) %!assert_equal (seconds (seconds (1e5 / 3)), 1e5 / 3) %!assert_equal (seconds (seconds (1814.9999999999998)), 1814.9999999999998) %!assert_equal (milliseconds (milliseconds (1234.5678)), 1234.5678) %!assert_equal (minutes (minutes (1 / 7)), 1 / 7) %!assert_equal (hours (hours (1 / 7)), 1 / 7) %!assert_equal (days (days (1 / 7)), 1 / 7) ## Test the stored unit -- whole values stay whole through the conversions that ## used to need the picosecond snap to come out right %!assert_equal (seconds (duration ('30:15', 'InputFormat', 'mm:ss')), 1815) %!assert_equal (seconds (duration (0, 30, 15)), 1815) %!assert_equal (minutes (duration (0, 30, 15)), 30.25) %!assert_equal (seconds (minutes (1)), 60) %!assert_equal (seconds (hours (1)), 3600) %!assert_equal (seconds (days (1)), 86400) %!assert_equal (milliseconds (seconds (1)), 1000) %!assert_equal (seconds (milliseconds (1)), 0.001) %!assert_equal (days (seconds (1)), 1.1574074074074073e-05) ## Test 'discretize' -- bin edges are computed in the stored milliseconds, not ## in seconds. A width that is not a whole number of seconds cannot be held ## exactly in seconds but is a round integer in milliseconds, so binning in ## seconds left a last-bit error in every such edge. Values captured from ## R2024a; these are bit-exact and must stay so. %!test %! [~, e] = discretize (seconds ([0 3.6]), 3); %! assert_equal (seconds (e), [0 1.2 2.3999999999999999 3.6000000000000001]); %!test %! [~, e] = discretize (seconds ([0 3.72]), 3); %! assert_equal (seconds (e), [0 1.3 2.6000000000000001 3.8999999999999999]); %!test %! [~, e] = discretize (seconds ([0 1e-4]), 3); %! assert_equal (seconds (e), [0 4.0000000000000003e-05 ... %! 8.0000000000000007e-05 0.00011999999999999999]); %!test %! [~, e] = discretize (seconds ([0 10^-2.5]), 3); %! assert_equal (seconds (e), [0 0.0011000000000000001 ... %! 0.0022000000000000001 0.0033000000000000004]); ## Test 'discretize' -- the left edge is the unit holding the smallest value ## moved down a whole number of units, so it is constant across a unit and ## steps once. Sliding the start by an eighth of a second changes the width, ## because the span is counted with a ceiling at the top against a floor at the ## bottom. Values captured from R2024a. %!test %! [~, e] = discretize (seconds ([0 6]), 3); %! assert_equal (seconds (e), [0 2 4 6]); %!test %! [~, e] = discretize (seconds ([0.125 6.125]), 3); %! assert_equal (seconds (e), [-1 2 5 8]); %!test %! [~, e] = discretize (seconds ([0.5 10.5]), 3); %! assert_equal (seconds (e), [-1 3 7 11]); %!test %! [~, e] = discretize (seconds ([0.75 10.75]), 3); %! assert_equal (seconds (e), [-1 3 7 11]); ## Test 'discretize' -- a whole unit is taken when the excess is under one ## width, and also when it is exactly one width provided the bins are at least ## three units across; below that the finer decimal grid wins. R=12 at N=5 and ## N=7 are the pair that pins it: same excess-to-width ratio, different answer. %!test %! [~, e] = discretize (seconds ([0 12]), 5); %! assert_equal (seconds (e), [-2 1 4 7 10 13]); %!test %! [~, e] = discretize (seconds ([0 12]), 7); %! assert_equal (seconds (e), [0 1.8 3.6000000000000001 5.4000000000000004 ... %! 7.2000000000000002 9 10.800000000000001 12.6]); %!test %! [~, e] = discretize (seconds ([0 16]), 5); %! assert_equal (seconds (e), [-2 2 6 10 14 18]); %!test %! [~, e] = discretize (seconds ([0 8]), 5); %! assert_equal (seconds (e), [0 1.6000000000000001 3.2000000000000002 ... %! 4.7999999999999998 6.4000000000000004 8]); ## Test 'discretize' -- a whole unit is kept while the bins overshoot the data ## by less than twice one width less a unit, so the tolerance rises with the ## width: a 3-unit width takes an excess of 3 and a 5-unit width takes 7. The ## rows below are the accept side, all of which the earlier, tighter rule sent ## to the decimal grid. Values captured from R2024a. %!test %! [~, e] = discretize (seconds ([0 48]), 11); %! assert_equal (seconds (e), [-4 1 6 11 16 21 26 31 36 41 46 51]); %!test %! [~, e] = discretize (seconds ([0.125 28.125]), 7); %! assert_equal (seconds (e), [-3 2 7 12 17 22 27 32]); %!test %! [~, e] = discretize (seconds ([0.125 32.125]), 8); %! assert_equal (seconds (e), [-4 1 6 11 16 21 26 31 36]); %!test %! [~, e] = discretize (seconds ([0.125 40.125]), 8); %! assert_equal (seconds (e), [-4 2 8 14 20 26 32 38 44]); ## Test 'discretize' -- the reject side of the same bound, which must keep ## using the decimal rule. A 2-unit width refuses an excess of 2 and a 3-unit ## width refuses 4, one step past what each tolerates above. %!test %! [~, e] = discretize (seconds ([0 8]), 5); %! assert_equal (seconds (e), [0 1.6000000000000001 3.2000000000000002 ... %! 4.7999999999999998 6.4000000000000004 8]); %!test %! [~, e] = discretize (seconds ([0 14]), 6); %! assert_equal (seconds (e), [0 2.3999999999999999 4.7999999999999998 ... %! 7.2000000000000002 9.5999999999999996 12 14.4]); %!test %! [~, e] = discretize (seconds ([0 12]), 3); %! assert_equal (seconds (e), [0 4 8 12]); ## Test 'sort' -- option VALUES are matched without regard to case, as MATLAB ## matches them; the option names already were. %!assert_equal (hours (sort (hours ([3 1 NaN 2]), 'Ascend')), [1 2 3 NaN]) %!assert_equal (hours (sort (hours ([3 1 NaN 2]), 'DESCEND')), [NaN 3 2 1]) %!assert_equal (hours (sort (hours ([3 1 NaN 2]), ... %! 'MissingPlacement', 'First')), [NaN 1 2 3]) %!assert_equal (hours (sort (hours ([3 1 NaN 2]), ... %! 'missingplacement', 'last')), [1 2 3 NaN]) ## Adding or subtracting a zero duration is an identity, at any magnitude. ## A precision guard tuned for the old day-based storage used to snap results ## below 1e-15 ms to zero, destroying legitimately tiny durations. %!assert_equal (milliseconds (milliseconds (5e-16) + milliseconds (0)), 5e-16) %!assert_equal (seconds (seconds (1e-19) - seconds (0)), 1e-19) %!assert_equal (milliseconds (milliseconds (5e-16) * 1), 5e-16) %!assert_equal (seconds (abs (seconds (-1e-19))), 1e-19) ## the shared '__unique__' helper validates its options under its own name %!error ... %! unique (duration (1, 0, 0), 5); %!error ... %! unique (duration (1, 0, 0), 'first', 'last'); %!error ... %! unique (duration (1, 0, 0), 'sorted', 'stable'); %!error ... %! unique (duration (1, 0, 0), 'nope'); ## datevec and size refuse more outputs than they can fill %!error ... %! [a, b, c, d, e, f, g] = datevec (duration (1, 0, 0)); %!error ... %! [r, c] = size (duration ([1; 2], [0; 0], [0; 0]), 1); ## ismissing takes a vector indicator only %!error ... %! ismissing (duration ([1; 2], [0; 0], [0; 0]), duration ([1, 2; 3, 4], ... %! zeros (2), zeros (2))); ## issortedrows: an unknown direction, and a strict direction list that does ## not match COL (the simple directions delegate to sortrows before this) %!error ... %! issortedrows (duration ([1, 2; 3, 4], zeros (2), zeros (2)), {'nope'}); %!error ... %! issortedrows (duration ([1, 2; 3, 4], zeros (2), zeros (2)), [1, 2], ... %! {'strictascend', 'strictdescend', 'strictascend'}); ## the rounding methods share one unit vocabulary %!error ... %! floor (duration (1, 0, 0), 'nope'); %!error ... %! ceil (duration (1, 0, 0), 'nope'); %!error ... %! round (duration (1, 0, 0), 'nope'); ## bounds takes at most two inputs %!error ... %! bounds (duration (1, 0, 0), 1, 2, 3); ## sort validates both of its Name/Value options %!error ... %! sort (duration (1, 0, 0), 'MissingPlacement', 'nope'); %!error ... %! sort (duration (1, 0, 0), 'ComparisonMethod', 'nope'); %!error ... %! sort (hours ([2; 1]), 'Bogus', 1) %!error ... %! sort (hours ([2; 1]), 0) %!error ... %! sort (hours ([2; 1]), {1}) %!error ... %! sort (hours ([2; 1]), 1, 2) %!error ... %! sort (hours ([2; 1]), 'descend', 1) %!error ... %! sortrows (duration ([1, 2; 3, 4], zeros (2), zeros (2)), {'ascend', ... %! 'descend', 'ascend'}); ## concatenation promotes only the types the constructor accepts %!error ... %! vertcat (duration ([1; 2], [0; 0], [0; 0]), {1}); ## the text constructor: too many dots, too many or too few colons, a ## fractional part that is not a number, a field count that disagrees with ## the colons, and a field that is not a number %!error ... %! duration ({'1.2.3'}); %!error ... %! duration ({'1:2:3:4:5'}); %!error ... %! duration ({'1:2:3.xy'}); %!error ... %! duration ({'1.2:3:4'}); %!error ... %! duration ({'1:a:3.5'}); %!error ... %! duration ({'1:2'}); ## binning takes a duration width, a bin count, or a unit name %!error ... %! discretize (duration ([1; 2], [0; 0], [0; 0]), {1}); pr0m1th3as-datatypes-9450b29/inst/tests/eventtable.m-tst000066400000000000000000001761151527370255000231470ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## Every expected value below was measured against MATLAB R2026a, which is ## where 'eventtable' exists; R2024a has no such class. ################################################################################ ## ** Construction ** ## ################################################################################ ## Test the empty constructor %!test %! ET = eventtable (); %! assert_equal (class (ET), 'eventtable'); %! assert_equal (size (ET), [0, 0]); ## Test the empty constructor designates nothing %!test %! P = eventtable ().Properties; %! assert_equal (isempty (P.EventLabelsVariable), true); %! assert_equal (isempty (P.EventLengthsVariable), true); %! assert_equal (isempty (P.EventEndsVariable), true); ## Test construction from a datetime vector labels the events itself %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! assert_equal (ET.Properties.VariableNames, {'EventLabels'}); %! assert_equal (ET.Properties.EventLabelsVariable, 'EventLabels'); %! assert_equal (cellstr (ET.EventLabels), {'Event 1'; 'Event 2'}); ## Test the generated labels are numbered from one for each event table %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ((1:4))'); %! assert_equal (cellstr (ET.EventLabels), ... %! {'Event 1'; 'Event 2'; 'Event 3'; 'Event 4'}); ## Test construction from a duration vector %!test %! ET = eventtable (seconds ([1 3])'); %! assert_equal (class (ET), 'eventtable'); %! assert_equal (ET.Properties.RowTimes, seconds ([1; 3])); ## Test construction from a timetable keeps its variables %!test %! tv = datetime (2024, 1, 1) + hours ([1 3])'; %! TT = timetable (tv, string ({'a'; 'b'}), [10; 20], ... %! 'VariableNames', {'lab', 'val'}); %! ET = eventtable (TT); %! assert_equal (class (ET), 'eventtable'); %! assert_equal (ET.Properties.VariableNames, {'lab', 'val'}); %! assert_equal (ET.Properties.RowTimes, tv); ## Test construction from a timetable guesses no labels variable %!test %! tv = datetime (2024, 1, 1) + hours ([1 3])'; %! TT = timetable (tv, string ({'a'; 'b'}), 'VariableNames', {'lab'}); %! P = eventtable (TT).Properties; %! assert_equal (isempty (P.EventLabelsVariable), true); ## Test construction from a timetable carries its variable metadata %!test %! tv = datetime (2024, 1, 1) + hours ([1 3])'; %! TT = timetable (tv, [10; 20], 'VariableNames', {'val'}); %! TT.Properties.VariableUnits = {'kg'}; %! TT.Properties.Description = 'source'; %! ET = eventtable (TT); %! assert_equal (ET.Properties.VariableUnits, {'kg'}); %! assert_equal (ET.Properties.Description, 'source'); ## Test 'EventLabels' names the events and is added as a variable %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])', ... %! 'EventLabels', string ({'on'; 'off'})); %! assert_equal (ET.Properties.VariableNames, {'EventLabels'}); %! assert_equal (cellstr (ET.EventLabels), {'on'; 'off'}); ## Test a scalar 'EventLabels' labels every event alike %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])', ... %! 'EventLabels', "hit"); %! assert_equal (cellstr (ET.EventLabels), {'hit'; 'hit'}); ## Test a character matrix labels one event per row, held as a cellstr with ## the blanks trimmed as 'cellstr' trims them %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])', ... %! 'EventLabels', ['on '; 'off']); %! assert_equal (class (ET.EventLabels), 'cell'); %! assert_equal (ET.EventLabels, {'on'; 'off'}); ## Test the generated labels are a cellstr %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! assert_equal (class (ET.EventLabels), 'cell'); ## Test a string given as such is kept as one %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])', ... %! 'EventLabels', string ({'on'; 'off'})); %! assert_equal (class (ET.EventLabels), 'string'); ## Test 'EventLengths' makes the events intervals %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])', ... %! 'EventLengths', hours ([2; 1])); %! assert_equal (ET.Properties.EventLengthsVariable, 'EventLengths'); %! assert_equal (ET.EventLengths, hours ([2; 1])); ## Test 'EventLengths' takes a calendarDuration against datetime event times %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])', ... %! 'EventLengths', caldays ([1; 1])); %! assert_equal (class (ET.EventLengths), 'calendarDuration'); ## Test 'EventEnds' makes the events intervals %!test %! tv = datetime (2024, 1, 1) + hours ([1 3])'; %! ET = eventtable (tv, 'EventEnds', tv + hours (1)); %! assert_equal (ET.Properties.EventEndsVariable, 'EventEnds'); %! assert_equal (isempty (ET.Properties.EventLengthsVariable), true); ## Test 'EventLabelsVariable' points at a variable of the timetable %!test %! tv = datetime (2024, 1, 1) + hours ([1 3])'; %! TT = timetable (tv, string ({'a'; 'b'}), 'VariableNames', {'lab'}); %! ET = eventtable (TT, 'EventLabelsVariable', 'lab'); %! assert_equal (ET.Properties.EventLabelsVariable, 'lab'); %! assert_equal (ET.Properties.VariableNames, {'lab'}); ## Test 'EventLengthsVariable' points at a variable of the timetable %!test %! tv = datetime (2024, 1, 1) + hours ([1 3])'; %! TT = timetable (tv, hours ([2; 1]), 'VariableNames', {'dur'}); %! ET = eventtable (TT, 'EventLengthsVariable', 'dur'); %! assert_equal (ET.Properties.EventLengthsVariable, 'dur'); ################################################################################ ## ** Construction: refusals ** ## ################################################################################ ## Test a first input that is neither a timetable nor a time vector %!error ... %! eventtable (table (1)); ## Test the event times are required %!error ... %! eventtable ('EventLabels', "a"); ## Test an unknown option, and one without a value %!error ... %! eventtable (hours (1), 'Bogus', 1); %!error ... %! eventtable (hours (1), 'Bogus'); ## Test a length and an end together %!error ... %! eventtable (datetime (2024, 1, 1), 'EventLengths', hours (1), ... %! 'EventEnds', datetime (2024, 1, 2)); ## Test labels given both ways %!error ... %! eventtable (timetable (datetime (2024, 1, 1), 1, 'VariableNames', {'v'}), ... %! 'EventLabels', "a", 'EventLabelsVariable', 'v'); ## Test a variable-naming option against a vector of times %!error ... %! eventtable (datetime (2024, 1, 1), 'EventLabelsVariable', 'v'); ## Test labels of a type that describes when rather than what %!error ... %! eventtable (datetime (2024, 1, 1), 'EventLabels', hours (1)); ## Test a wrong number of labels %!error ... %! eventtable (datetime (2024, 1, 1) + hours ([1 3])', ... %! 'EventLabels', string ({'a'; 'b'; 'c'})); ## Test a calendar length against duration event times %!error ... %! eventtable (seconds ([1 3])', 'EventLengths', calmonths ([1; 1])); ## Test a length that is neither a duration nor a calendarDuration %!error ... %! eventtable (datetime (2024, 1, 1), 'EventLengths', 5); ## Test an end of a different type from the event times %!error ... %! eventtable (datetime (2024, 1, 1), 'EventEnds', hours (1)); ## Test an option value colliding with a variable the input already has %!error ... %! eventtable (timetable (datetime (2024, 1, 1), 1, ... %! 'VariableNames', {'EventLabels'}), ... %! 'EventLabels', "a"); ################################################################################ ## ** The three properties ** ## ################################################################################ ## Test a property may be pointed at another variable %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! ET = addvars (ET, [1; 2], 'NewVariableNames', {'val'}); %! ET.Properties.EventLabelsVariable = 'val'; %! assert_equal (ET.Properties.EventLabelsVariable, 'val'); ## Test a property is cleared by assigning an empty matrix %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! ET.Properties.EventLabelsVariable = []; %! assert_equal (isempty (ET.Properties.EventLabelsVariable), true); ## Test a property accepts a string scalar as a name %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! ET.Properties.EventLabelsVariable = "EventLabels"; %! assert_equal (ET.Properties.EventLabelsVariable, 'EventLabels'); ## Test an empty character vector is not a name and does not clear %!error ... %! ET = eventtable (datetime (2024, 1, 1)); %! ET.Properties.EventLabelsVariable = ''; ## Test a name that no variable answers to %!error ... %! ET = eventtable (datetime (2024, 1, 1)); %! ET.Properties.EventLabelsVariable = 'nosuch'; ## Test an end variable cannot be set while a length variable is %!error ... %! ET = eventtable (datetime (2024, 1, 1), 'EventLengths', hours (1)); %! ET.Properties.EventEndsVariable = 'EventLengths'; ## Test a length variable cannot be set while an end variable is %!error ... %! ET = eventtable (datetime (2024, 1, 1), 'EventEnds', datetime (2024, 1, 2)); %! ET.Properties.EventLengthsVariable = 'EventEnds'; ################################################################################ ## ** The properties object ** ## ################################################################################ ## Test 'Properties' returns an EventtableProperties object %!assert (class (eventtable (datetime (2024, 1, 1)).Properties), ... %! 'datatypes.tabular.EventtableProperties') ## Test the three event properties are listed first, ahead of 'Description' %!test %! names = properties (eventtable (datetime (2024, 1, 1)).Properties); %! assert_equal (names(1:3)', {'EventLabelsVariable', ... %! 'EventLengthsVariable', 'EventEndsVariable'}); ## Test the whole published order %!test %! names = properties (eventtable (datetime (2024, 1, 1)).Properties); %! assert_equal (names', {'EventLabelsVariable', 'EventLengthsVariable', ... %! 'EventEndsVariable', 'Description', 'UserData', ... %! 'DimensionNames', 'VariableNames', ... %! 'VariableTypes', 'VariableDescriptions', ... %! 'VariableUnits', 'VariableContinuity', ... %! 'RowTimes', 'StartTime', 'SampleRate', ... %! 'TimeStep', 'CustomProperties'}); ################################################################################ ## ** An event table is a timetable ** ## ################################################################################ ## Test the class hierarchy %!test %! ET = eventtable (datetime (2024, 1, 1)); %! assert_equal (isa (ET, 'timetable'), true); %! assert_equal (isa (ET, 'tabular'), true); %! assert_equal (isa (ET, 'table'), false); ## Test the type predicates single it out as a timetable %!test %! ET = eventtable (datetime (2024, 1, 1)); %! assert_equal (istimetable (ET), true); %! assert_equal (istabular (ET), true); %! assert_equal (istable (ET), false); ## Test a row subset is still an event table %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3 5])'); %! assert_equal (class (ET(1:2,:)), 'eventtable'); %! assert_equal (ET(1:2,:).Properties.EventLabelsVariable, 'EventLabels'); ## Test 'sortrows' returns an event table %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([3 1])'); %! assert_equal (class (sortrows (ET)), 'eventtable'); ## Test 'head' returns an event table %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! assert_equal (class (head (ET, 1)), 'eventtable'); ## Test 'addvars' returns an event table %!assert (class (addvars (eventtable (datetime (2024, 1, 1)), 1)), 'eventtable') ## Test 'removevars' returns an event table %!test %! ET = eventtable (datetime (2024, 1, 1)); %! ET = addvars (ET, 1, 'NewVariableNames', {'val'}); %! assert_equal (class (removevars (ET, 'val')), 'eventtable'); ## Test 'length' is not defined for an event table %!error ... %! length (eventtable (datetime (2024, 1, 1))); ################################################################################ ## ** The 'Events' property ** ## ################################################################################ ## Test an unattached timetable reports empty, of class double %!test %! TT = timetable (datetime (2024, 1, 1) + hours ((0:3))', (1:4)'); %! assert_equal (class (TT.Properties.Events), 'double'); %! assert_equal (isempty (TT.Properties.Events), true); ## Test an event table is attached and read back %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)'); %! TT.Properties.Events = eventtable (t([2 4])); %! assert_equal (class (TT.Properties.Events), 'eventtable'); %! assert_equal (height (TT.Properties.Events), 2); ## Test a plain timetable is converted on assignment %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)'); %! TT.Properties.Events = timetable (t([2 4]), string ({'a'; 'b'})); %! assert_equal (class (TT.Properties.Events), 'eventtable'); ## Test the conversion guesses no labels variable %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)'); %! TT.Properties.Events = timetable (t([2 4]), string ({'a'; 'b'})); %! P = TT.Properties.Events.Properties; %! assert_equal (isempty (P.EventLabelsVariable), true); ## Test assigning an empty matrix detaches the event table %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)'); %! TT.Properties.Events = eventtable (t([2 4])); %! TT.Properties.Events = []; %! assert_equal (isempty (TT.Properties.Events), true); ## Test events lying entirely outside the row times are accepted %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)'); %! TT.Properties.Events = eventtable (t([2 4]) + years (5)); %! assert_equal (height (TT.Properties.Events), 2); ## Test a duration-keyed timetable takes a duration-keyed event table %!test %! TT = timetable (seconds ((0:3))', (1:4)'); %! TT.Properties.Events = eventtable (seconds ([1 2])'); %! assert_equal (class (TT.Properties.Events), 'eventtable'); ## Test 'Events' is published between 'TimeStep' and 'CustomProperties' %!test %! names = properties (timetable ((1:3)', 'TimeStep', hours (1)).Properties); %! assert_equal (names(end-1:end)', {'Events', 'CustomProperties'}); ## Test a value that is neither a timetable nor empty %!error ... %! TT = timetable ((1:3)', 'TimeStep', hours (1)); %! TT.Properties.Events = table (1); ## Test event times of a different type from the row times %!error ... %! TT = timetable (datetime (2024, 1, 1) + hours ((0:3))', (1:4)'); %! TT.Properties.Events = eventtable (seconds ([1 2])'); ################################################################################ ## ** An event table carries no event table ** ## ################################################################################ ## Test reading 'Events' off an event table %!error ... %! eventtable (datetime (2024, 1, 1)).Properties.Events; ## Test assigning 'Events' to an event table %!error ... %! ET = eventtable (datetime (2024, 1, 1)); %! ET.Properties.Events = eventtable (datetime (2024, 1, 1)); ## Test 'Events' is among the properties of an event table, inherited %!assert (any (strcmp (properties ('eventtable'), 'Events')), true) ## Test 'Events' is among the properties of a timetable %!assert (any (strcmp (properties ('timetable'), 'Events')), true) ## Test 'Events' is not published by the properties object of an event table %!test %! names = properties (eventtable (datetime (2024, 1, 1)).Properties); %! assert_equal (any (strcmp (names, 'Events')), false); ################################################################################ ## ** A row-preserving operation carries it ** ## ################################################################################ ## Test a row subset carries the event table through unchanged %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)'); %! TT.Properties.Events = eventtable (t([2 4])); %! assert_equal (height (TT(1:2,:).Properties.Events), 2); ## Test 'sortrows' carries the event table through unchanged %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)'); %! TT.Properties.Events = eventtable (t([2 4])); %! assert_equal (height (sortrows (TT).Properties.Events), 2); ## Test 'head' carries the event table through unchanged %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)'); %! TT.Properties.Events = eventtable (t([2 4])); %! assert_equal (height (head (TT, 2).Properties.Events), 2); ## Test 'addvars' carries the event table through unchanged %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)'); %! TT.Properties.Events = eventtable (t([2 4])); %! TT = addvars (TT, (5:8)', 'NewVariableNames', {'z'}); %! assert_equal (height (TT.Properties.Events), 2); ## Test 'retime' carries the event table through unchanged %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)'); %! TT.Properties.Events = eventtable (t([2 4])); %! assert_equal (height (retime (TT, t, 'previous').Properties.Events), 2); ## Test the table round trip drops the event table %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)'); %! TT.Properties.Events = eventtable (t([2 4])); %! TT2 = table2timetable (timetable2table (TT)); %! assert_equal (isempty (TT2.Properties.Events), true); ## Test 'isequal' sees an attached event table %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)'); %! TTe = TT; %! TTe.Properties.Events = eventtable (t([2 4])); %! assert_equal (isequal (TT, TTe), false); %! assert_equal (isequal (TTe, TTe), true); ## Test 'isequal' distinguishes two different event tables %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! A = timetable (t, (1:4)'); %! B = A; %! A.Properties.Events = eventtable (t([2 4])); %! B.Properties.Events = eventtable (t([1 3])); %! assert_equal (isequal (A, B), false); ################################################################################ ## ** A binary operation merges the event tables ** ## ################################################################################ ## Test only the left operand carrying events hands them over %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! B = timetable (t, (5:8)', 'VariableNames', {'B'}); %! A.Properties.Events = eventtable (t([2 4]), 'EventLabels', ... %! string ({'on'; 'off'})); %! E = [A B].Properties.Events; %! assert_equal (height (E), 2); %! assert_equal (cellstr (E.EventLabels), {'on'; 'off'}); ## Test only the right operand carrying events hands them over %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! B = timetable (t, (5:8)', 'VariableNames', {'B'}); %! B.Properties.Events = eventtable (t([2 4])); %! assert_equal (height ([A B].Properties.Events), 2); ## Test neither operand carrying events leaves the result with none %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! B = timetable (t, (5:8)', 'VariableNames', {'B'}); %! assert_equal (isempty ([A B].Properties.Events), true); ## Test a key on both sides collapses to one row %!test %! t = datetime (2024, 1, 1) + hours ((0:5))'; %! A = timetable (t, (1:6)', 'VariableNames', {'A'}); %! B = timetable (t, (7:12)', 'VariableNames', {'B'}); %! A.Properties.Events = eventtable (t([2 4]), 'EventLabels', ... %! string ({'on'; 'off'})); %! B.Properties.Events = eventtable (t([4 6]), 'EventLabels', ... %! string ({'off'; 'down'})); %! E = [A B].Properties.Events; %! assert_equal (height (E), 3); %! assert_equal (cellstr (E.EventLabels), {'on'; 'off'; 'down'}); ## Test a shared event time with differing labels keeps both rows %!test %! t = datetime (2024, 1, 1) + hours ((0:5))'; %! A = timetable (t, (1:6)', 'VariableNames', {'A'}); %! B = timetable (t, (7:12)', 'VariableNames', {'B'}); %! A.Properties.Events = eventtable (t([2 4]), 'EventLabels', ... %! string ({'on'; 'off'})); %! B.Properties.Events = eventtable (t([4 6]), 'EventLabels', ... %! string ({'mid'; 'down'})); %! E = [A B].Properties.Events; %! assert_equal (height (E), 4); %! assert_equal (cellstr (E.EventLabels), {'on'; 'mid'; 'off'; 'down'}); ## Test the merge is ordered by the keys and not by operand order %!test %! t = datetime (2024, 1, 1) + hours ((0:5))'; %! A = timetable (t, (1:6)', 'VariableNames', {'A'}); %! B = timetable (t, (7:12)', 'VariableNames', {'B'}); %! A.Properties.Events = eventtable (t([2 4]), 'EventLabels', ... %! string ({'on'; 'off'})); %! B.Properties.Events = eventtable (t([4 6]), 'EventLabels', ... %! string ({'mid'; 'down'})); %! assert_equal (isequal ([A B].Properties.Events, ... %! [B A].Properties.Events), true); ## Test a variable only one side has is filled with missing on the other %!test %! t = datetime (2024, 1, 1) + hours ((0:5))'; %! A = timetable (t, (1:6)', 'VariableNames', {'A'}); %! B = timetable (t, (7:12)', 'VariableNames', {'B'}); %! EA = eventtable (t([2 4]), 'EventLabels', string ({'on'; 'off'})); %! EA = addvars (EA, [10; 20], 'NewVariableNames', {'val'}); %! A.Properties.Events = EA; %! B.Properties.Events = eventtable (t([4 6]), 'EventLabels', ... %! string ({'mid'; 'down'})); %! E = [A B].Properties.Events; %! assert_equal (E.Properties.VariableNames, {'EventLabels', 'val'}); %! assert_equal (E.val, [10; NaN; 20; NaN]); ## Test a key differing only in a further event variable keeps both rows %!test %! t = datetime (2024, 1, 1) + hours ((0:5))'; %! A = timetable (t, (1:6)', 'VariableNames', {'A'}); %! B = timetable (t, (7:12)', 'VariableNames', {'B'}); %! EA = addvars (eventtable (t([2 4]), 'EventLabels', ... %! string ({'on'; 'off'})), [10; 20], ... %! 'NewVariableNames', {'val'}); %! EB = addvars (eventtable (t([4 6]), 'EventLabels', ... %! string ({'off'; 'down'})), [99; 30], ... %! 'NewVariableNames', {'val'}); %! A.Properties.Events = EA; %! B.Properties.Events = EB; %! E = [A B].Properties.Events; %! assert_equal (height (E), 4); %! assert_equal (E.val, [10; 20; 99; 30]); ## Test 'vertcat' merges the event tables %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! t2 = datetime (2024, 1, 1) + hours ((4:7))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! C = timetable (t2, (9:12)', 'VariableNames', {'A'}); %! A.Properties.Events = eventtable (t([2 4]), 'EventLabels', ... %! string ({'on'; 'off'})); %! C.Properties.Events = eventtable (t2([1 3]), 'EventLabels', ... %! string ({'mid'; 'down'})); %! assert_equal (height ([A; C].Properties.Events), 4); ## Test 'vertcat' carries an attached event table holding no events %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! t2 = datetime (2024, 1, 1) + hours ((4:7))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! C = timetable (t2, (9:12)', 'VariableNames', {'A'}); %! E = eventtable (t([2 4]), 'EventLabels', string ({'on'; 'off'})); %! A.Properties.Events = E([], :); %! out = [A; C].Properties.Events; %! assert_equal (isa (out, 'eventtable'), true); %! assert_equal (height (out), 0); ## Test 'synchronize' merges the event tables %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! t2 = datetime (2024, 1, 1) + hours ((4:7))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! C = timetable (t2, (9:12)', 'VariableNames', {'C'}); %! A.Properties.Events = eventtable (t([2 4]), 'EventLabels', ... %! string ({'on'; 'off'})); %! C.Properties.Events = eventtable (t2([1 3]), 'EventLabels', ... %! string ({'mid'; 'down'})); %! assert_equal (height (synchronize (A, C).Properties.Events), 4); ## Test 'innerjoin' merges the event tables %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! B = timetable (t, (5:8)', 'VariableNames', {'B'}); %! A.Properties.Events = eventtable (t([2 4]), 'EventLabels', ... %! string ({'on'; 'off'})); %! B.Properties.Events = eventtable (t([1 3]), 'EventLabels', ... %! string ({'mid'; 'down'})); %! assert_equal (height (innerjoin (A, B).Properties.Events), 4); ## Test 'outerjoin' merges the event tables %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! B = timetable (t, (5:8)', 'VariableNames', {'B'}); %! A.Properties.Events = eventtable (t([2 4]), 'EventLabels', ... %! string ({'on'; 'off'})); %! B.Properties.Events = eventtable (t([1 3]), 'EventLabels', ... %! string ({'mid'; 'down'})); %! assert_equal (height (outerjoin (A, B).Properties.Events), 4); ## Test two event tables carrying identical events collapse to one %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! B = timetable (t, (5:8)', 'VariableNames', {'B'}); %! ET = eventtable (t([2 4]), 'EventLabels', string ({'on'; 'off'})); %! A.Properties.Events = ET; %! B.Properties.Events = ET; %! assert_equal (height ([A B].Properties.Events), 2); ## Test the merged event table keeps the event properties %!test %! t = datetime (2024, 1, 1) + hours ((0:5))'; %! A = timetable (t, (1:6)', 'VariableNames', {'A'}); %! B = timetable (t, (7:12)', 'VariableNames', {'B'}); %! A.Properties.Events = eventtable (t([2 4]), 'EventLabels', ... %! string ({'on'; 'off'}), ... %! 'EventLengths', hours ([1; 1])); %! B.Properties.Events = eventtable (t([4 6]), 'EventLabels', ... %! string ({'mid'; 'down'}), ... %! 'EventLengths', hours ([1; 1])); %! P = [A B].Properties.Events.Properties; %! assert_equal (P.EventLabelsVariable, 'EventLabels'); %! assert_equal (P.EventLengthsVariable, 'EventLengths'); ################################################################################ ## ** The merge refuses a mismatch ** ## ################################################################################ ## Test an interval event table against an instantaneous one %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! B = timetable (t, (5:8)', 'VariableNames', {'B'}); %! A.Properties.Events = eventtable (t([2 4]), 'EventLengths', hours ([1; 1])); %! B.Properties.Events = eventtable (t([1 3])); %! horzcat (A, B); ## Test two event tables whose label variables are named differently %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! B = timetable (t, (5:8)', 'VariableNames', {'B'}); %! A.Properties.Events = eventtable (t([2 4])); %! ET = eventtable (t([1 3])); %! ET = renamevars (ET, 'EventLabels', 'Tag'); %! ET.Properties.EventLabelsVariable = 'Tag'; %! B.Properties.Events = ET; %! horzcat (A, B); ## Test a length against an end %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! B = timetable (t, (5:8)', 'VariableNames', {'B'}); %! A.Properties.Events = eventtable (t([2 4]), 'EventLengths', hours ([1; 1])); %! B.Properties.Events = eventtable (t([1 3]), 'EventEnds', t([2 4])); %! horzcat (A, B); ## Test 'synchronize' refuses under its own name %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! t2 = datetime (2024, 1, 1) + hours ((4:7))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! C = timetable (t2, (9:12)', 'VariableNames', {'C'}); %! A.Properties.Events = eventtable (t([2 4]), 'EventLengths', hours ([1; 1])); %! C.Properties.Events = eventtable (t2([1 3])); %! synchronize (A, C); ## Test 'outerjoin' refuses under its own name %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! B = timetable (t, (5:8)', 'VariableNames', {'B'}); %! A.Properties.Events = eventtable (t([2 4]), 'EventLengths', hours ([1; 1])); %! B.Properties.Events = eventtable (t([1 3])); %! outerjoin (A, B); ## Test two unset label variables agree and merge %!test %! t = datetime (2024, 1, 1) + hours ((0:5))'; %! A = timetable (t, (1:6)', 'VariableNames', {'A'}); %! B = timetable (t, (7:12)', 'VariableNames', {'B'}); %! EA = eventtable (t([2 4])); %! EA.Properties.EventLabelsVariable = []; %! EB = eventtable (t([4 6])); %! EB.Properties.EventLabelsVariable = []; %! A.Properties.Events = EA; %! B.Properties.Events = EB; %! assert_equal (height ([A B].Properties.Events), 4); ################################################################################ ## ** The result takes the more derived class ** ## ################################################################################ ## Test 'horzcat' with the event table first %!test %! t = datetime (2024, 1, 1) + hours ([1 3])'; %! ET = eventtable (t, 'EventLabels', string ({'on'; 'off'})); %! TT = timetable (t, [7; 8], 'VariableNames', {'V'}); %! out = horzcat (ET, TT); %! assert_equal (class (out), 'eventtable'); %! assert_equal (out.Properties.VariableNames, {'EventLabels', 'V'}); %! assert_equal (out.Properties.EventLabelsVariable, 'EventLabels'); ## Test 'horzcat' with the plain timetable first %!test %! t = datetime (2024, 1, 1) + hours ([1 3])'; %! ET = eventtable (t, 'EventLabels', string ({'on'; 'off'})); %! TT = timetable (t, [7; 8], 'VariableNames', {'V'}); %! out = horzcat (TT, ET); %! assert_equal (class (out), 'eventtable'); %! assert_equal (out.Properties.VariableNames, {'V', 'EventLabels'}); %! assert_equal (out.Properties.EventLabelsVariable, 'EventLabels'); ## Test 'vertcat' with the event table first %!test %! t = datetime (2024, 1, 1) + hours ([1 3])'; %! ET = eventtable (t, 'EventLabels', string ({'on'; 'off'})); %! TT = timetable (t, string ({'p'; 'q'}), 'VariableNames', {'EventLabels'}); %! out = vertcat (ET, TT); %! assert_equal (class (out), 'eventtable'); %! assert_equal (height (out), 4); %! assert_equal (out.Properties.EventLabelsVariable, 'EventLabels'); ## Test 'vertcat' with the plain timetable first %!test %! t = datetime (2024, 1, 1) + hours ([1 3])'; %! ET = eventtable (t, 'EventLabels', string ({'on'; 'off'})); %! TT = timetable (t, string ({'p'; 'q'}), 'VariableNames', {'EventLabels'}); %! out = vertcat (TT, ET); %! assert_equal (class (out), 'eventtable'); %! assert_equal (height (out), 4); %! assert_equal (out.Properties.EventLabelsVariable, 'EventLabels'); ## Test a promoted result drops a designation naming no variable of its own %!test %! t = datetime (2024, 1, 1) + hours ([1 3])'; %! ET = eventtable (t, 'EventLabels', string ({'on'; 'off'})); %! ET = removevars (ET, 'EventLabels'); %! ET.Properties.EventLabelsVariable = []; %! TT = timetable (t, [7; 8], 'VariableNames', {'V'}); %! out = horzcat (TT, ET); %! assert_equal (class (out), 'eventtable'); %! assert_equal (isempty (out.Properties.EventLabelsVariable), true); ## Test two plain timetables are not promoted %!test %! t = datetime (2024, 1, 1) + hours ([1 3])'; %! A = timetable (t, [1; 2], 'VariableNames', {'A'}); %! B = timetable (t, [3; 4], 'VariableNames', {'B'}); %! assert_equal (class (horzcat (A, B)), 'timetable'); ## Test 'cat' promotes along both dimensions %!test %! t = datetime (2024, 1, 1) + hours ([1 3])'; %! ET = eventtable (t, 'EventLabels', string ({'on'; 'off'})); %! TT = timetable (t, [7; 8], 'VariableNames', {'V'}); %! TT2 = timetable (t, string ({'p'; 'q'}), 'VariableNames', {'EventLabels'}); %! assert_equal (class (cat (2, TT, ET)), 'eventtable'); %! assert_equal (class (cat (1, TT2, ET)), 'eventtable'); ################################################################################ ## ** 'eventtable.empty' ** ## ################################################################################ ## Test the no-argument form %!test %! ET = eventtable.empty (); %! assert_equal (class (ET), 'eventtable'); %! assert_equal (size (ET), [0, 0]); ## Test the two-argument form %!test %! ET = eventtable.empty (0, 3); %! assert_equal (class (ET), 'eventtable'); %! assert_equal (size (ET), [0, 3]); %! assert_equal (ET.Properties.VariableNames, {'Var1', 'Var2', 'Var3'}); ## Test the size-vector form %!assert (size (eventtable.empty ([0, 2])), [0, 2]) ## Test an empty event table designates nothing %!test %! P = eventtable.empty (0, 3).Properties; %! assert_equal (isempty (P.EventLabelsVariable), true); %! assert_equal (isempty (P.EventLengthsVariable), true); %! assert_equal (isempty (P.EventEndsVariable), true); ## Test neither dimension being zero %!error ... %! eventtable.empty (2, 3); ################################################################################ ## ** The designations follow the variables they name ** ## ################################################################################ ## Test removing the labels variable clears the designation %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! ET = addvars (ET, [10; 20], 'NewVariableNames', {'val'}); %! P = removevars (ET, 'EventLabels').Properties; %! assert_equal (isempty (P.EventLabelsVariable), true); ## Test removing another variable leaves the designation alone %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! ET = addvars (ET, [10; 20], 'NewVariableNames', {'val'}); %! P = removevars (ET, 'val').Properties; %! assert_equal (P.EventLabelsVariable, 'EventLabels'); ## Test deleting the labels variable by empty assignment clears it %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! ET = addvars (ET, [10; 20], 'NewVariableNames', {'val'}); %! ET(:,'EventLabels') = []; %! assert_equal (isempty (ET.Properties.EventLabelsVariable), true); ## Test removing the lengths variable clears only that designation %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])', ... %! 'EventLengths', hours ([1; 1])); %! P = removevars (ET, 'EventLengths').Properties; %! assert_equal (isempty (P.EventLengthsVariable), true); %! assert_equal (P.EventLabelsVariable, 'EventLabels'); ## Test moving a variable leaves the designation alone %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! ET = addvars (ET, [10; 20], 'NewVariableNames', {'val'}); %! P = movevars (ET, 'EventLabels', 'After', 'val').Properties; %! assert_equal (P.EventLabelsVariable, 'EventLabels'); ## Test converting a variable's type leaves the designation alone %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! P = convertvars (ET, 'EventLabels', 'categorical').Properties; %! assert_equal (P.EventLabelsVariable, 'EventLabels'); ## Test renaming the labels variable carries the designation with it %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! P = renamevars (ET, 'EventLabels', 'Tag').Properties; %! assert_equal (P.EventLabelsVariable, 'Tag'); %! assert_equal (P.VariableNames, {'Tag'}); ## Test renaming the lengths variable carries its designation with it %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])', ... %! 'EventLengths', hours ([1; 1])); %! P = renamevars (ET, 'EventLengths', 'Dur').Properties; %! assert_equal (P.EventLengthsVariable, 'Dur'); %! assert_equal (P.EventLabelsVariable, 'EventLabels'); ## Test renaming another variable leaves the designation alone %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! ET = addvars (ET, [10; 20], 'NewVariableNames', {'val'}); %! P = renamevars (ET, 'val', 'other').Properties; %! assert_equal (P.EventLabelsVariable, 'EventLabels'); ## Test renaming there and back restores the designation %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! ET = renamevars (ET, 'EventLabels', 'Tag'); %! ET = renamevars (ET, 'Tag', 'EventLabels'); %! assert_equal (ET.Properties.EventLabelsVariable, 'EventLabels'); ## Test assigning the whole variable name list carries the designation %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! ET = addvars (ET, [10; 20], 'NewVariableNames', {'val'}); %! ET.Properties.VariableNames = {'Tag', 'val'}; %! assert_equal (ET.Properties.EventLabelsVariable, 'Tag'); ## Test assigning one indexed variable name carries the designation %!test %! ET = eventtable (datetime (2024, 1, 1) + hours ([1 3])'); %! ET = addvars (ET, [10; 20], 'NewVariableNames', {'val'}); %! ET.Properties.VariableNames{1} = 'Tag'; %! assert_equal (ET.Properties.EventLabelsVariable, 'Tag'); ## Test a stripped event table no longer merges with a labelled one %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! B = timetable (t, (5:8)', 'VariableNames', {'B'}); %! EA = addvars (eventtable (t([2 4])), [10; 20], 'NewVariableNames', {'val'}); %! A.Properties.Events = removevars (EA, 'EventLabels'); %! B.Properties.Events = addvars (eventtable (t([1 3])), [10; 20], ... %! 'NewVariableNames', {'val'}); %! horzcat (A, B); ## Test a renamed event table no longer merges with an unrenamed one %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! A = timetable (t, (1:4)', 'VariableNames', {'A'}); %! B = timetable (t, (5:8)', 'VariableNames', {'B'}); %! A.Properties.Events = renamevars (eventtable (t([2 4])), ... %! 'EventLabels', 'Tag'); %! B.Properties.Events = eventtable (t([1 3])); %! horzcat (A, B); ## Test merging the labels variable into another clears the designation %!test %! t = datetime (2024, 1, 1) + hours ([1 3])'; %! ET = eventtable (t); %! ET.Properties.EventLabelsVariable = []; %! ET = removevars (ET, 'EventLabels'); %! ET = addvars (ET, [1; 2], [3; 4], 'NewVariableNames', {'a', 'b'}); %! ET.Properties.EventLabelsVariable = 'a'; %! M = mergevars (ET, {'a', 'b'}, 'NewVariableName', 'both'); %! assert_equal (M.Properties.VariableNames, {'both'}); %! assert_equal (isempty (M.Properties.EventLabelsVariable), true); ## Test merging variables the designation does not name leaves it alone %!test %! t = datetime (2024, 1, 1) + hours ([1 3])'; %! ET = eventtable (t); %! ET.Properties.EventLabelsVariable = []; %! ET = removevars (ET, 'EventLabels'); %! ET = addvars (ET, [1; 2], [3; 4], [5; 6], ... %! 'NewVariableNames', {'a', 'b', 'c'}); %! ET.Properties.EventLabelsVariable = 'a'; %! M = mergevars (ET, {'b', 'c'}, 'NewVariableName', 'both'); %! assert_equal (M.Properties.EventLabelsVariable, 'a'); ## Test a plain timetable is unaffected by either hook %!test %! t = datetime (2024, 1, 1) + hours ([1 3])'; %! TT = timetable (t, [1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! TT = renamevars (TT, 'a', 'z'); %! TT = removevars (TT, 'b'); %! assert_equal (TT.Properties.VariableNames, {'z'}); ################################################################################ ## ** 'extractevents' ** ## ################################################################################ ## Test row numbers pick the events out %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! ET = extractevents (TT, [2; 5]); %! assert_equal (class (ET), 'eventtable'); %! assert_equal (ET.Properties.RowTimes, t([2 5])); ## Test the rows form carries no variables and designates nothing %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! P = extractevents (TT, [2; 5]).Properties; %! assert_equal (isempty (P.VariableNames), true); %! assert_equal (isempty (P.EventLabelsVariable), true); ## Test a logical row subscript %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! ET = extractevents (TT, [true; false; true; false]); %! assert_equal (ET.Properties.RowTimes, t([1 3])); ## Test a 'timerange' subscript, which is half open on the right %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! ET = extractevents (TT, timerange (t(3), t(6))); %! assert_equal (height (ET), 3); %! assert_equal (ET.Properties.RowTimes, t(3:5)); ## Test the events come out in the order the subscript names them %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! assert_equal (extractevents (TT, [5; 2]).Properties.RowTimes, t([5 2])); ## Test a row named twice gives two events %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! assert_equal (height (extractevents (TT, [2; 2; 5])), 3); ## Test an empty subscript gives an event table with no events %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! assert_equal (height (extractevents (TT, zeros (0, 1))), 0); ## Test a colon takes every row %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! assert_equal (height (extractevents (TT, ':')), 8); ## Test a duration-keyed timetable %!test %! TD = timetable (seconds ((0:7))', (1:8)', 'VariableNames', {'v'}); %! ET = extractevents (TD, [2; 5]); %! assert_equal (ET.Properties.RowTimes, seconds ([1; 4])); ## Test the source timetable is left as it was %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! extractevents (TT, [2; 5]); %! assert_equal (width (TT), 1); %! assert_equal (isempty (TT.Properties.Events), true); ## Test the labels form makes one event per labelled row, ungrouped %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! g = categorical ([{'a'}; {'a'}; {'a'}; {''}; {'b'}; {'b'}; {''}; {''}]); %! ET = extractevents (TT, g); %! assert_equal (height (ET), 5); %! assert_equal (ET.Properties.RowTimes, t([1 2 3 5 6])); %! assert_equal (ET.Properties.EventLabelsVariable, 'EventLabels'); ## Test the labels form sets no lengths, so its events are instants %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! g = categorical ([{'a'}; {'a'}; {'b'}; {''}]); %! P = extractevents (TT, g).Properties; %! assert_equal (isempty (P.EventLengthsVariable), true); ## Test labels that are all missing give no events but still designate %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! ET = extractevents (TT, categorical (repmat ({''}, 4, 1))); %! assert_equal (height (ET), 0); %! assert_equal (ET.Properties.EventLabelsVariable, 'EventLabels'); ## Test a scalar 'EventLabels' labels every event alike %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! ET = extractevents (TT, [2; 5], 'EventLabels', "hit"); %! assert_equal (cellstr (ET.EventLabels), {'hit'; 'hit'}); ## Test 'EventLabelsVariable' keeps the source variable's own name %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! g = categorical ([{'a'}; {'a'}; {'a'}; {''}; {'b'}; {'b'}; {''}; {''}]); %! TT = timetable (t, (1:8)', g, 'VariableNames', {'v', 'g'}); %! ET = extractevents (TT, [2; 5], 'EventLabelsVariable', 'g'); %! assert_equal (ET.Properties.VariableNames, {'g'}); %! assert_equal (ET.Properties.EventLabelsVariable, 'g'); ## Test 'EventLengths' names its variable after the option %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! ET = extractevents (TT, [2; 5], 'EventLengths', hours ([2; 1])); %! assert_equal (ET.Properties.EventLengthsVariable, 'EventLengths'); %! assert_equal (ET.EventLengths, hours ([2; 1])); ## Test 'EventEnds' names its variable after the option %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! ET = extractevents (TT, [2; 5], 'EventEnds', t([4 7])); %! assert_equal (ET.Properties.EventEndsVariable, 'EventEnds'); %! assert_equal (isempty (ET.Properties.EventLengthsVariable), true); ## Test 'EventDataVariables' carries variables under their own names %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', (101:108)', 'VariableNames', {'v', 'q'}); %! ET = extractevents (TT, [2; 5], 'EventDataVariables', {'q', 'v'}); %! assert_equal (ET.Properties.VariableNames, {'q', 'v'}); %! assert_equal (ET.q, [102; 105]); ## Test the second output keeps every variable the event table did not take %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! g = categorical ([{'a'}; {'a'}; {'a'}; {''}; {'b'}; {'b'}; {''}; {''}]); %! TT = timetable (t, (1:8)', (101:108)', g, 'VariableNames', {'v', 'q', 'g'}); %! [~, TT2] = extractevents (TT, g); %! assert_equal (TT2.Properties.VariableNames, {'v', 'q', 'g'}); ## Test the second output loses a variable named as the labels %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! g = categorical ([{'a'}; {'a'}; {'a'}; {''}; {'b'}; {'b'}; {''}; {''}]); %! TT = timetable (t, (1:8)', (101:108)', g, 'VariableNames', {'v', 'q', 'g'}); %! [~, TT2] = extractevents (TT, [2; 5], 'EventLabelsVariable', 'g'); %! assert_equal (TT2.Properties.VariableNames, {'v', 'q'}); ## Test the second output loses a variable named as event data %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! g = categorical ([{'a'}; {'a'}; {'a'}; {''}; {'b'}; {'b'}; {''}; {''}]); %! TT = timetable (t, (1:8)', (101:108)', g, 'VariableNames', {'v', 'q', 'g'}); %! [~, TT2] = extractevents (TT, [2; 5], 'EventDataVariables', {'q'}); %! assert_equal (TT2.Properties.VariableNames, {'v', 'g'}); ## Test 'PreserveEventVariables' keeps them in the second output %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! g = categorical ([{'a'}; {'a'}; {'a'}; {''}; {'b'}; {'b'}; {''}; {''}]); %! TT = timetable (t, (1:8)', g, 'VariableNames', {'v', 'g'}); %! [~, TT2] = extractevents (TT, [2; 5], 'EventLabelsVariable', 'g', ... %! 'PreserveEventVariables', true); %! assert_equal (TT2.Properties.VariableNames, {'v', 'g'}); ## Test an event table is itself a timetable to extract from %!test %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! ET = eventtable (t); %! assert_equal (height (extractevents (ET, [1; 2])), 2); ################################################################################ ## ** 'extractevents' refusals ** ## ################################################################################ ## Test labels given both ways %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! g = categorical ([{'a'}; {'a'}; {'b'}; {''}]); %! TT = timetable (t, (1:4)', g, 'VariableNames', {'v', 'g'}); %! extractevents (TT, [2; 3], 'EventLabels', "x", 'EventLabelsVariable', 'g'); ## Test labels given alongside the labels form %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! g = categorical ([{'a'}; {'a'}; {'b'}; {''}]); %! TT = timetable (t, (1:4)', g, 'VariableNames', {'v', 'g'}); %! extractevents (TT, g, 'EventLabelsVariable', 'g'); ## Test a length and an end together %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! extractevents (TT, [2; 3], 'EventLengths', hours ([1; 1]), ... %! 'EventEnds', t([3 4])); ## Test a labels vector of the wrong length %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! extractevents (TT, categorical ([{'a'}; {'b'}])); ## Test a lengths variable of the wrong type %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)', (101:104)', 'VariableNames', {'v', 'q'}); %! extractevents (TT, [2; 3], 'EventLengthsVariable', 'q'); ## Test an ends variable of the wrong type %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)', (101:104)', 'VariableNames', {'v', 'q'}); %! extractevents (TT, [2; 3], 'EventEndsVariable', 'q'); ## Test 'PreserveEventVariables' must be a logical scalar %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! extractevents (TT, [2; 3], 'PreserveEventVariables', 'yes'); ## Test 'PreserveEventVariables' with nothing named to preserve %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! g = categorical ([{'a'}; {'a'}; {'b'}; {''}]); %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! extractevents (TT, g, 'PreserveEventVariables', true); ## Test an option name that is none of the eight, and one without a value %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! extractevents (TT, [2; 3], 'NoSuchOption', 1); %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! extractevents (TT, [2; 3], 'NoSuchOption'); ## Test too few input arguments %!error ... %! extractevents (timetable ((1:3)', 'TimeStep', hours (1))); ################################################################################ ## ** 'syncevents' ** ## ################################################################################ ## Test an interval event covers its own time and stops short of its end %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t([3 6]), 'EventLabels', ... %! string ({'rain'; 'snow'}), ... %! 'EventLengths', hours ([2; 1])); %! out = syncevents (TT); %! assert_equal (height (out), 8); %! assert_equal (cellstr (out.EventLabels(3:5)), {'rain'; 'rain'; ''}); ## Test an event given an end covers the same rows as one given a length %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! A = TT; %! A.Properties.Events = eventtable (t(3), 'EventLabels', "rain", ... %! 'EventLengths', hours (2)); %! B = TT; %! B.Properties.Events = eventtable (t(3), 'EventLabels', "rain", ... %! 'EventEnds', t(5)); %! assert_equal (cellstr (syncevents (A).EventLabels), ... %! cellstr (syncevents (B).EventLabels)); ## Test an instantaneous event covers only a row at its own time %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t([3 6]), 'EventLabels', ... %! string ({'rain'; 'snow'})); %! out = syncevents (TT); %! assert_equal (cellstr (out.EventLabels(3:4)), {'rain'; ''}); ## Test an event of zero length covers nothing %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t([3 6]), 'EventLabels', ... %! string ({'rain'; 'snow'}), ... %! 'EventLengths', hours ([0; 0])); %! assert_equal (all (ismissing (syncevents (TT).EventLabels)), true); ## Test an attached event table holding no events leaves every row missing %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! E = eventtable (t([3 6]), 'EventLabels', string ({'rain'; 'snow'})); %! TT.Properties.Events = E([], :); %! out = syncevents (TT); %! assert_equal (height (out), 8); %! assert_equal (all (ismissing (out.EventLabels)), true); ## Test events lying outside the row times leave every row missing %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t([3 6]) + years (5), 'EventLabels', ... %! string ({'rain'; 'snow'}), ... %! 'EventLengths', hours ([2; 1])); %! assert_equal (all (ismissing (syncevents (TT).EventLabels)), true); ## Test a row covered by two events is repeated once per event %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t([3 4]), 'EventLabels', ... %! string ({'rain'; 'snow'}), ... %! 'EventLengths', hours ([3; 3])); %! out = syncevents (TT); %! assert_equal (height (out), 10); %! assert_equal (out.v', [1, 2, 3, 4, 4, 5, 5, 6, 7, 8]); %! assert_equal (cellstr (out.EventLabels(4:5)), {'rain'; 'snow'}); ## Test the variable holding the lengths is not copied by default %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t([3 6]), 'EventLabels', ... %! string ({'rain'; 'snow'}), ... %! 'EventLengths', hours ([2; 1])); %! assert_equal (syncevents (TT).Properties.VariableNames, ... %! {'v', 'EventLabels'}); ## Test an event data variable is copied by default %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! ET = eventtable (t([3 6]), 'EventLabels', string ({'rain'; 'snow'}), ... %! 'EventLengths', hours ([2; 1])); %! TT.Properties.Events = addvars (ET, [7; 8], ... %! 'NewVariableNames', {'amount'}); %! out = syncevents (TT); %! assert_equal (out.Properties.VariableNames, {'v', 'EventLabels', 'amount'}); %! assert_equal (out.amount', [NaN, NaN, 7, 7, NaN, 8, NaN, NaN]); ## Test 'EventDataVariables' replaces the default set rather than adding %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! ET = eventtable (t([3 6]), 'EventLabels', string ({'rain'; 'snow'}), ... %! 'EventLengths', hours ([2; 1])); %! TT.Properties.Events = addvars (ET, [7; 8], ... %! 'NewVariableNames', {'amount'}); %! out = syncevents (TT, 'EventDataVariables', {'amount'}); %! assert_equal (out.Properties.VariableNames, {'v', 'amount'}); ## Test 'EventDataVariables' can ask for the lengths %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t([3 6]), 'EventLabels', ... %! string ({'rain'; 'snow'}), ... %! 'EventLengths', hours ([2; 1])); %! out = syncevents (TT, 'EventDataVariables', {'EventLengths'}); %! assert_equal (out.Properties.VariableNames, {'v', 'EventLengths'}); ## Test a default label answers for the rows no event covered %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t([3 6]), 'EventLabels', ... %! string ({'rain'; 'snow'}), ... %! 'EventLengths', hours ([2; 1])); %! out = syncevents (TT, "none"); %! assert_equal (cellstr (out.EventLabels'), ... %! {'none', 'none', 'rain', 'rain', 'none', 'snow', ... %! 'none', 'none'}); ## Test a default label of another type is converted to the labels' own %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t([3 6]), 'EventLabels', ... %! string ({'rain'; 'snow'}), ... %! 'EventLengths', hours ([2; 1])); %! out = syncevents (TT, 7); %! assert_equal (cellstr (out.EventLabels(1)), {'7'}); ## Test a name used on both sides has both of them moved %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! ET = eventtable (t([3 6]), 'EventLabels', string ({'rain'; 'snow'})); %! TT.Properties.Events = addvars (ET, [7; 8], 'NewVariableNames', {'v'}); %! out = syncevents (TT); %! assert_equal (out.Properties.VariableNames, ... %! {'v_tt', 'EventLabels', 'v_et'}); ## Test the result keeps the event table it was synchronised from %!test %! t = datetime (2024, 1, 1) + hours ((0:7))'; %! TT = timetable (t, (1:8)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t([3 6]), 'EventLabels', ... %! string ({'rain'; 'snow'})); %! assert_equal (class (syncevents (TT).Properties.Events), 'eventtable'); ## Test a duration-keyed timetable %!test %! TD = timetable (seconds ((0:7))', (1:8)', 'VariableNames', {'v'}); %! TD.Properties.Events = eventtable (seconds ([2 5])', 'EventLabels', ... %! string ({'rain'; 'snow'}), ... %! 'EventLengths', seconds ([2; 1])); %! out = syncevents (TD); %! assert_equal (cellstr (out.EventLabels(3:5)), {'rain'; 'rain'; ''}); ## Test a timetable with no event table attached %!error ... %! syncevents (timetable ((1:3)', 'TimeStep', hours (1))); ## Test more than one positional argument %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (t([2 3])); %! syncevents (TT, "a", "b"); ## Test a default label with no labels variable to put it in %!error ... %! t = datetime (2024, 1, 1) + hours ((0:3))'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! ET = eventtable (t([2 3])); %! ET.Properties.EventLabelsVariable = []; %! TT.Properties.Events = ET; %! syncevents (TT, "none"); pr0m1th3as-datatypes-9450b29/inst/tests/missing.m-tst000066400000000000000000000375051527370255000224660ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ################################################################################ ## ** Create and convert 'missing' type ** ## ################################################################################ ## Available Methods ## ## ## ## 'cellstr' 'dispstrings' 'isequal' 'isequaln' ## ## 'double' 'single' 'categorical' 'datetime' ## ## 'duration' 'calendarDuration' ## ## ## ################################################################################ ## every conversion keeps the shape of the missing array it was given %!assert_equal (size (double (repmat (missing, 2, 3))), [2, 3]) %!assert_equal (size (single (repmat (missing, 2, 3))), [2, 3]) %!assert_equal (size (categorical (repmat (missing, 2, 3))), [2, 3]) %!assert_equal (size (datetime (repmat (missing, 2, 3))), [2, 3]) %!assert_equal (size (duration (repmat (missing, 2, 3))), [2, 3]) %!assert_equal (size (calendarDuration (repmat (missing, 2, 3))), [2, 3]) ## duration: the numeric form is read as an Nx3 [h m s] matrix, so a scalar ## missing must not be handed a single-column NaN %!assert_equal (class (duration (missing)), 'duration') %!assert_equal (size (duration (missing)), [1, 1]) %!assert_equal (ismissing (duration (missing)), true) %!assert_equal (all (ismissing (duration (repmat (missing, 2, 3)))(:)), true) %!test ## the conversion carries duration's default format (vs MATLAB R2024a) %! D = duration (missing); %! assert_equal (D.Format, 'hh:mm:ss'); %! assert_equal (cellstr (D), {'NaN'}); ## datetime: the conversion takes the constructor's options, so a missing ## value can be turned into one that carries a zone or a display format ## (measured on MATLAB R2024a) %!test %! D = datetime (missing, 'TimeZone', 'America/New_York'); %! assert_equal (D.TimeZone, 'America/New_York'); %! assert_equal (D.Format, 'dd-MMM-uuuu HH:mm:ss'); %! assert_equal (ismissing (D), true); %!test %! D = datetime (missing, 'Format', 'uuuu-MM-dd'); %! assert_equal (D.Format, 'uuuu-MM-dd'); %! assert_equal (D.TimeZone, ''); %!test %! D = datetime (missing, 'TimeZone', 'America/New_York', ... %! 'Format', 'uuuu-MM-dd'); %! assert_equal (D.TimeZone, 'America/New_York'); %! assert_equal (D.Format, 'uuuu-MM-dd'); %!test %! assert_equal (datetime (missing, 'TimeZone', 'UTC').TimeZone, 'UTC'); ## 'InputFormat' describes how text is read and has nothing to act on here, ## so it is accepted and ignored, as it is for a datetime input %!test %! D = datetime (missing, 'InputFormat', 'uuuu'); %! assert_equal (D.Format, 'dd-MMM-uuuu HH:mm:ss'); %! assert_equal (D.TimeZone, ''); ## the shape is kept whether an option is given or not %!test %! D = datetime (repmat (missing, 2, 3), 'TimeZone', 'America/New_York'); %! assert_equal (size (D), [2, 3]); %! assert_equal (D.TimeZone, 'America/New_York'); %! assert_equal (all (ismissing (D)(:)), true); ## duration: the conversion takes the constructor's options (R2024a) %!test %! D = duration (missing, 'Format', 'hh:mm'); %! assert_equal (D.Format, 'hh:mm'); %! assert_equal (ismissing (D), true); %!test %! assert_equal (duration (missing, 'Format', 'dd:hh:mm:ss').Format, ... %! 'dd:hh:mm:ss'); %!test %! D = duration (repmat (missing, 2, 3), 'Format', 'hh:mm'); %! assert_equal (size (D), [2, 3]); %! assert_equal (D.Format, 'hh:mm'); ## calendarDuration: likewise, for a format that names every required ## character (R2024a). A format that omits one is refused here where MATLAB ## warns and repairs it, which the class docstring records. %!test %! assert_equal (calendarDuration (missing, 'Format', 'mdt').Format, 'mdt'); %!test %! assert_equal (calendarDuration (missing, 'Format', 'ymwdt').Format, ... %! 'ymwdt'); %!test %! D = calendarDuration (repmat (missing, 2, 3), 'Format', 'mdt'); %! assert_equal (size (D), [2, 3]); %! assert_equal (D.Format, 'mdt'); ## categorical: the category set and the ordinal flag reach the constructor ## (R2024a) %!test %! C = categorical (missing, [1 2], {'a', 'b'}); %! assert_equal (categories (C), {'a'; 'b'}); %! assert_equal (isordinal (C), false); %! assert_equal (ismissing (C), true); %!test %! C = categorical (missing, 'Ordinal', true); %! assert_equal (isordinal (C), true); %! assert_equal (isempty (categories (C)), true); %!test %! C = categorical (missing, [1 2], {'a', 'b'}, 'Ordinal', true); %! assert_equal (categories (C), {'a'; 'b'}); %! assert_equal (isordinal (C), true); %!test %! C = categorical (repmat (missing, 2, 3), 'Ordinal', true); %! assert_equal (size (C), [2, 3]); %! assert_equal (isordinal (C), true); ## cellstr: a missing value carries no text, so it cannot become one %!error cellstr (missing) %!error cellstr (repmat (missing, 2, 3)) ## dispstrings still yields the display placeholder, which is not a conversion %!assert_equal (dispstrings (missing), {''}) %!assert_equal (dispstrings (repmat (missing, 2, 2)), repmat ({''}, ... %! 2, 2)) ## isequal: a missing value equals nothing, itself included %!assert_equal (isequal (missing, missing), false) %!assert_equal (isequal (missing, NaN), false) %!assert_equal (isequal (missing, 1), false) %!assert_equal (isequal (missing, categorical (NaN)), false) %!assert_equal (isequal (missing, string (missing)), false) %!assert_equal (isequal (repmat (missing, 2, 2), repmat (missing, 2, 2)), false) %!assert_equal (isequal (missing, missing, missing), false) ## isequaln: missing values are alike, each type answering with its own marker %!assert_equal (isequaln (missing, missing), true) %!assert_equal (isequaln (missing, NaN), true) %!assert_equal (isequaln (missing, single (NaN)), true) %!assert_equal (isequaln (missing, categorical (NaN)), true) %!assert_equal (isequaln (missing, NaT), true) %!assert_equal (isequaln (missing, duration (NaN, 0, 0)), true) %!assert_equal (isequaln (missing, string (missing)), true) %!assert_equal (isequaln (missing, NaN, missing), true) %!assert_equal (isequaln (repmat (missing, 2, 2), NaN (2, 2)), true) ## isequaln: a non-missing element or a shape mismatch defeats it %!assert_equal (isequaln (missing, 1), false) %!assert_equal (isequaln (missing, repmat (missing, 2, 2)), false) %!assert_equal (isequaln (missing, NaN (2, 2)), false) %!assert_equal (isequaln (repmat (missing, 1, 2), [NaN, 1]), false) %!assert_equal (isequaln (missing, NaN, 1), false) ################################################################################ ## ** Summary Information ** ## ################################################################################ ## Available Methods ## ## ## ## 'size' 'keyHash' ## ## ## ################################################################################ ## size: asking for more outputs than the array has dimensions is refused %!error ... %! [r, c, p] = size (repmat (missing, 2, 3)); %!assert_equal (class (keyHash (missing)), 'uint64') %!assert_equal (keyHash (missing) == keyHash (missing), true) %!assert_equal (keyHash (missing) == keyHash ([missing, missing]), false) %!assert_equal (keyHash (missing) == keyHash (NaN), false) %!assert_equal (keyHash (missing) == keyHash (string (missing)), false) %!assert_equal (keyHash (missing, uint64 (5)) == keyHash (missing), false) %!assert_equal (keyMatch (missing, missing), true) %!assert_equal (keyMatch (missing, NaN), false) %!assert_equal (keyMatch (missing, string (missing)), false) %!error keyHash (missing, 5) ################################################################################ ## ** Relational Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'eq' 'ge' 'gt' 'le' ## ## 'lt' 'ne' ## ## ## ################################################################################ ## each comparison refuses operands that cannot be broadcast, the missing ## array standing in as all-NaN of its own size %!error ... %! repmat (missing, 2, 2) == ones (1, 3); %!error ... %! repmat (missing, 2, 2) >= ones (1, 3); %!error ... %! repmat (missing, 2, 2) > ones (1, 3); %!error ... %! repmat (missing, 2, 2) <= ones (1, 3); %!error ... %! repmat (missing, 2, 2) < ones (1, 3); %!error ... %! repmat (missing, 2, 2) != ones (1, 3); ################################################################################ ## ** Array Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'cat' ## ## ## ################################################################################ ## cat: a cell array carries no missing marker, so concatenation is refused. ## The bracket forms raise core's own 'cat: missing/horzcat method failed' ## wrapper, which is not ours to test, so call cat directly. %!error cat (2, missing, {'a'}) %!error cat (1, missing, {'a'}) %!error cat (2, {'a'}, missing) ## cat: the types that do carry one still concatenate, and the missing element ## survives as that type's own marker %!assert_equal (class ([missing, missing]), 'missing') %!assert_equal ([missing, 1], [NaN, 1]) %!assert_equal (class ([missing, single(1)]), 'single') %!assert_equal (class ([missing, categorical(1)]), 'categorical') %!assert_equal (class ([missing, NaT]), 'datetime') %!assert_equal (class ([missing, duration(1, 0, 0)]), 'duration') %!assert_equal (class ([missing, calendarDuration(1, 0, ... %! 0)]), 'calendarDuration') %!assert_equal (class ([missing, string('a')]), 'string') %!assert_equal (ismissing ([missing, duration(1, 0, 0)]), [true, false]) %!assert_equal (ismissing ([missing, datetime(2020, 1, 1)]), [true, false]) %!assert_equal (ismissing ([missing, categorical(1)]), [true, false]) ## ismissing: a missing indicator marks the standard missing values %!assert_equal (ismissing ([1, NaN, 3], missing), [false, true, false]) %!assert_equal (ismissing (single ([1, NaN, 3]), missing), [false, true, false]) %!assert_equal (ismissing ({'a', ''}, missing), [false, true]) %!assert_equal (ismissing (missing, missing), true) %!error ... %! ismissing (int8 ([1, 2, 3]), missing) %!error ... %! ismissing ([true, false], missing) %!error ... %! ismissing ('a b', missing) %!error ... %! ismissing ({1, 2}, missing) %!error ... %! ismissing (missing, 1) %!error ... %! ismissing (missing, missing, missing) ## cat: a datetime result keeps the real operand's zone and format -- an ## unzoned NaT cannot join a zoned array at all (vs MATLAB R2024a) %!test %! D = [missing, datetime(2020, 1, 1, 'TimeZone', 'UTC')]; %! assert_equal (D.TimeZone, 'UTC'); %! assert_equal (isnat (D), [true, false]); %!test %! D = [missing, datetime(2020, 1, 1, 'Format', 'yyyy')]; %! assert_equal (D.Format, 'yyyy'); %! assert_equal (cellstr (D), {'NaT', '2020'}); ## cat: a duration result takes its format from the real operand, not from the ## all-NaN filler (vs MATLAB R2024a) %!test %! D = [missing, hours(1)]; %! assert_equal (D.Format, 'h'); %! assert_equal (cellstr (D), {'NaN hr', '1 hr'}); %!test %! D = [missing, days(1)]; %! assert_equal (D.Format, 'd'); %! assert_equal (cellstr (D), {'NaN days', '1 day'}); %!test %! D = [missing, minutes(1)]; %! assert_equal (D.Format, 'm'); %! assert_equal (cellstr (D), {'NaN min', '1 min'}); %!test %! D = [missing, duration(1, 0, 0)]; %! assert_equal (D.Format, 'hh:mm:ss'); %! assert_equal (cellstr (D), {'NaN', '01:00:00'}); ################################################################################ ## ** Reference and Assignment Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'subsref' 'subsasgn' ## ## ## ################################################################################ ## subsref: only '()' reaches the values. The '{}' branch needs an explicit ## subsref call, the operator itself dying in core numel beforehand %!error ... %! subsref (missing, substruct ('{}', {1})); %!error ... %! m = missing; %! m.x; ## subsasgn: chained subscripts, a value carrying no missing marker, and the ## two invalid indexing types are each refused %!error ... %! m = missing; %! m(1).x = missing; %!error ... %! m = missing; %! m(1) = 5; %!error ... %! m = missing; %! m{1} = missing; %!error ... %! m = missing; %! m.x = missing; pr0m1th3as-datatypes-9450b29/inst/tests/string.m-tst000066400000000000000000003712621527370255000223240ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ################################################################################ ## ** Create String and Convert Type ** ## ################################################################################ ## Available Methods ## ## ## ## 'string' 'dispstrings' 'cellstr' 'cell' ## ## 'char' 'double' ## ## ## ################################################################################ %!assert_equal (string, string ('')) %!assert_equal (string, string ({''})) %!assert_equal (string (['a', 'b', 'c']), string ('abc')) %!assert_equal (string (['a', 'b', 'c']), string ({'abc'})) %!test %! str = string (["a";"b";"c"]); %! assert_equal (cellstr (str), {"a";"b";"c"}); ## Constructing from a char vector preserves trailing whitespace (the core ## 'cellstr' would deblank it); MATLAB's 'string' preserves it too. %!assert_equal (cellstr (string ('z ')), {'z '}) %!assert_equal (cellstr (string (' z ')), {' z '}) ## Each row of a padded char matrix keeps its trailing whitespace. %!test %! str = string (['ab '; 'c ']); %! assert_equal (cellstr (str), {'ab '; 'c '}); %! assert_equal (size (str), [2, 1]); ## size with no dimension named folds the trailing dimensions into the last ## output when fewer outputs are asked for than the array has dimensions, and ## pads with ones when more are asked for, as core Octave and MATLAB both do. %!test %! A = string (repmat ({'a'}, 2, 2, 2)); %! [r, c] = size (A); %! assert_equal ([r, c], [2, 4]); %!test %! A = string (repmat ({'a'}, 2, 2)); %! [a, b, c, d] = size (A); %! assert_equal ([a, b, c, d], [2, 2, 1, 1]); %!test %! str = string ({"a";"b";"c"}); %! assert_equal (cellstr (str), {"a";"b";"c"}); %!test %! str = string ({"a";"";"c"}); %! tfM = ismissing (str); %! assert_equal (cell (str), {"a";"";"c"}); %! assert_equal (tfM, logical ([0; 0; 0])); %!test %! str = string ([1 2 3 NaN 5]); %! tfM = ismissing (str); %! assert_equal (cellstr (str), {"1", "2", "3", "", "5"}); %! assert_equal (tfM, logical ([0 0 0 1 0])); ## One precision for the whole numeric array, set by its largest finite ## magnitude, as in MATLAB %!assert_equal (cellstr (string ([pi; 1e20])), {'3.141592653589793'; '1e+20'}) %!assert_equal (cellstr (string ([1/3; 10])), {'0.33333'; '10'}) %!assert_equal (cellstr (string ([1/3; 20])), {'0.333333'; '20'}) %!assert_equal (cellstr (string ([1/3; 12345])), {'0.333333333'; '12345'}) %!assert_equal (cellstr (string ([1/3; 1e10])), ... %! {'0.33333333333333'; '10000000000'}) %!assert_equal (cellstr (string ([1/3; 2e10])), ... %! {'0.333333333333333'; '20000000000'}) %!assert_equal (cellstr (string ([0.1; 123456789.123])), ... %! {'0.1'; '123456789.123'}) %!assert_equal (cellstr (string ([1/3, 1e10; 2, 3])), ... %! {'0.33333333333333', '10000000000'; '2', '3'}) %!assert_equal (cellstr (string ([1e15; 2])), {'1000000000000000'; '2'}) %!assert_equal (cellstr (string ([1e16; 2])), {'1e+16'; '2'}) %!assert_equal (cellstr (string ([1e-5; 2e-7])), {'1e-05'; '2e-07'}) %!assert_equal (cellstr (string ([-1/3; 2])), {'-0.33333'; '2'}) %!assert_equal (cellstr (string ([Inf; 1/3])), {'Inf'; '0.33333'}) %!assert_equal (cellstr (string ([Inf; -Inf])), {'Inf'; '-Inf'}) %!assert_equal (cellstr (string ([1/3; -0])), {'0.33333'; '0'}) %!assert_equal (ismissing (string ([NaN; 1/3])), [true; false]) %!assert_equal (cellstr (string (single ([1/3; 123456.7]))), ... %! {'0.3333333433'; '123456.7031'}) %!assert_equal (cellstr (string (single ([1/3; 1e10]))), ... %! {'0.33333334326744'; '10000000000'}) %!assert_equal (cellstr (string (uint64 ([18446744073709551615; 4]))), ... %! {'18446744073709551615'; '4'}) %!assert_equal (cellstr (string (intmin ('int64'))), ... %! {'-9223372036854775808'}) %!assert_equal (cellstr (string (int64 (2) ^ 53 + int64 (1))), ... %! {'9007199254740993'}) %!assert_equal (cellstr (string (int8 ([-128; 127]))), {'-128'; '127'}) %!assert_equal (cellstr (string ({1/3, 1e10})), {'0.33333', '10000000000'}) ## Concatenation converts each numeric argument as the constructor does %!assert_equal (cellstr ([[pi; 1e20], string({'s'; 's'})]), ... %! {'3.141592653589793', 's'; '1e+20', 's'}) %!assert_equal (cellstr ([string({'s'; 's'}), [1/3; 1e10]]), ... %! {'s', '0.33333333333333'; 's', '10000000000'}) %!assert_equal (cellstr ([1/3, 1e10, string('s')]), ... %! {'0.33333', '10000000000', 's'}) %!test %! str = string (duration ([3,4,5; NaN,NaN,NaN])); %! tfM = ismissing (str); %! assert_equal (cellstr (str), {"03:04:05"; ""}); %! assert_equal (tfM, logical ([0; 1])); %!test %! str = string (calendarDuration ([3,4,5; NaN,NaN,NaN])); %! tfM = ismissing (str); %! assert_equal (cellstr (str), {"3y 4mo 5d"; ""}); %! assert_equal (tfM, logical ([0; 1])); %!test %! str = string (repmat ('a', 3, 4, 5)); %! assert_equal (str, repmat (string ('aaaa'), 3, 5)); %! assert_equal (size (str), [3, 5]); %!error ... %! string ({[1 2], false}) %!error string ({"d", @(x)x}); %!error string (@(x)x); %!error string (struct ()) %!assert_equal (dispstrings (string ('a')), {'"a"'}) %!assert_equal (dispstrings (string ("a")), {'"a"'}) %!assert_equal (dispstrings (string ('a\na')), {'"a\na"'}) %!assert_equal (dispstrings (string (NaN)), {''}) %!assert_equal (cellstr (string ('a')), {'a'}) %!assert_equal (cellstr (string ("a")), {'a'}) %!assert_equal (cellstr (string ('a\na')), {'a\na'}) %!assert_equal (cellstr (string (NaN)), {''}) %!test %! str = string ({'3', 4, 'asd', [], NaN, hours(3)}); %! out = {3, 4, 'asd', [], [], '3 hr'}; %! assert_equal (cell (str), out); %!assert_equal (cell ([missing, string('')]), {[], ''}) %!assert_equal (cell ([NaN, string('')]), {[], ''}) %!assert_equal (cell (['', string('')]), {'', ''}) %!assert_equal (cell ([string, string('')]), {'', ''}) %!assert_equal (char (string), ''); %!assert_equal (char (string ('')), ''); %!assert_equal (char (string ({'a', 's', 'd'})), ['a'; 's'; 'd']) %!assert_equal (double (string ({'a', 's', 'd'})), [NaN, NaN, NaN]) %!assert_equal (double (string ({'a'; 's'; 'd'})), [NaN; NaN; NaN]) %!assert_equal (double (string ([1, 2, NaN])), [1, 2, NaN]) %!assert_equal (double (string ({1, 2, 'a', missing})), [1, 2, NaN, NaN]) %!assert_equal (double ([string([1, 2]) 'a', missing]), [1, 2, NaN, NaN]) ################################################################################ ## ** Summary Information ** ## ################################################################################ ## Available Methods ## ## ## ## 'size' 'ndims' 'numel' 'length' ## ## 'strlength' 'count' 'keyHash' ## ## ## ################################################################################ %!assert_equal (size (string), [1, 1]) %!assert_equal (size (char (string)), [0, 0]) %!assert_equal (size (char (string (''))), [0, 0]) %!assert_equal (size (string ('')), [1, 1]) %!assert_equal (size (string ([])), [0, 0]) %!assert_equal (size (string (ones (2, 0, 3, 4))), [2, 0, 3, 4]) %!assert_equal (size (string (ones (2, 4))), [2, 4]) %!assert_equal (size (string (ones (2, 4, 1, 1))), [2, 4]) %!assert_equal (size (string (ones (2, 4, 1, 0))), [2, 4, 1, 0]) %!assert_equal (size (string ('a')), [1, 1]) %!assert_equal (size (string ('abcd')), [1, 1]) %!assert_equal (size (string (char (0, 1))), [2, 1]) %!assert_equal (size (string (char (1, 0))), [2, 1]) %!assert_equal (size (string (repmat ('a', 3, 4))), [3, 1]) %!assert_equal (ndims (string), 2) %!assert_equal (ndims (string ([])), 2) %!assert_equal (ndims (string (ones (2, 0, 3, 4))), 4) %!assert_equal (ndims (string (ones (2, 4))), 2) %!assert_equal (ndims (string (zeros (6, 3))), 2) %!assert_equal (numel (string), 1) %!assert_equal (numel (string ([])), 0) %!assert_equal (numel (string (ones (2, 0, 3, 4))), 0) %!assert_equal (numel (string (ones (2, 4))), 8) %!assert_equal (numel (string (zeros (6, 3))), 18) %!assert_equal (length (string ('1')), 1) %!assert_equal (length (string ('asd')), 1) %!assert_equal (length (string (1:5)), 5) %!assert_equal (length (string (repmat ('a', 2, 3, 4))), 4) %!assert_equal (length (string (ones (5, 3, 4), 0, 0)), 5) %!assert_equal (length (string (ones (2, 10, 4), 0, 0)), 10) %!test %! str = string ({'asd', '', missing, 'a\na', "a\na"}); %! assert_equal (strlength (str), [3, 0, NaN, 4, 3]); %!assert_equal (strlength (string (char (0, 1))), [1; 1]) %!assert_equal (strlength (string (char (1, 0))), [1; 1]) %!test %! str = string ('Hello, World'); %! assert_equal (strlength (str), 12); %!test %! str = string ({'Amis', 'Chekhov', 'Joyce'; 'Stein', '', 'Proust'}); %! assert_equal (strlength (str), [4, 7, 5; 5, 0, 6]); %!assert_equal (count (string ({'paired with red shoes'}), 'red'), 2) %!test %! str = string ({'red green red red blue blue green'; ... %! 'green red blue green green blue'}); %! assert_equal (count (str, 'red'), [3; 1]); %!test %! str = string ({'red green red red blue blue green'; ... %! 'green red blue green green blue'}); %! assert_equal (count (str, string ('blue')), [2; 2]); %!test %! str = string ({'red green blue'; 'green red blue green blue'}); %! assert_equal (count (str, {'red', 'blue'}), [2; 3]); %!test %! str = string ({'Edgar Allan Poe', 'Louisa May Alcott'}); %! assert_equal (count (str, 'E', 'IgnoreCase',true), [2, 0]); %!test %! str = [string({'Andreas', 'Thomas'}), missing, string('Collin')]; %! assert_equal (count (str, 'a'), [1, 1, 0, 0]); %!test %! str = [string({'Andreas', 'Thomas'}), missing, string('Collin')]; %! assert_equal (count (str, 'a', 'IgnoreCase', true), [2, 1, 0, 0]); %!error count (string) %!error ... %! count (string ('a'), 1) %!error ... %! count (string ('a'), 'a', 'IgnoreCase', 1) %!error ... %! count (string ('a'), 'a', 'IgnoreCase', [true, true]) %!error ... %! count (string ('a'), 'a', 'whatever') %!error ... %! count (string ('a'), 'a', 'whatever', 1) ## Hash codes changed in 1.3.1: elements are now framed by their length, so ## that ["ab", "c"] is not the same key as ["a", "bc"]. These literals are ## also the cross-session determinism guard -- they were computed in an ## earlier session, so a run that reproduces them has reproduced them across ## sessions. %!assert_equal (keyHash (string), uint64 (3839404358835980713)) %!assert_equal (keyHash (string (1:5)), uint64 (15819870498579993191)) %!assert_equal (keyHash (string ({'A', 'B'})), uint64 (6917609306631486080)) %!assert_equal (keyHash (string ({'a', 'a'})), uint64 (13572976228165864193)) %!assert_equal (keyHash (string (['A', 'B'])), uint64 (1026459024852912674)) %!assert_equal (keyHash (string (['a', 'a'])), uint64 (17915091765270604921)) %!assert_equal (keyHash (string ('AB')), uint64 (1026459024852912674)) %!assert_equal (keyHash (string ('aa')), uint64 (17915091765270604921)) %!assert_equal (keyHash (string ({'A'; 'B'})), uint64 (8052465766455705784)) %!assert_equal (keyHash (string ({'a'; 'a'})), uint64 (5420354740993528185)) %!assert_equal (keyHash (string (['A'; 'B'])), uint64 (8052465766455705784)) %!assert_equal (keyHash (string (['a'; 'a'])), uint64 (5420354740993528185)) %!assert_equal (keyHash (string ('')), uint64 (3839404358835980713)) %!assert_equal (keyHash (string ({''})), uint64 (3839404358835980713)) %!assert_equal (keyHash (string (NaN)), uint64 (3839403259324352502)) %!assert_equal (keyHash (string (missing)), uint64 (3839403259324352502)) %!assert_equal (keyHash (string ([missing, ... %! missing])), uint64 (1774881411061845027)) %!assert_equal (keyHash (string ([missing; missing])), ... %! uint64 (15349247600549626907)) %!assert_equal (keyHash (string ('1')), uint64 (17490492431388450203)) ## Element boundaries are part of the key. %!assert_equal (keyMatch (string ({'ab', 'c'}), string ({'a', 'bc'})), false) %!assert_equal (isequal (keyHash (string ({'ab', 'c'})), ... %! keyHash (string ({'a', 'bc'}))), false) %!assert_equal (keyMatch (string ({'ab', 'c'}), string ({'ab', 'c'})), true) %!assert_equal (keyHash (string ({'ab', 'c'})), keyHash (string ({'ab', 'c'}))) %!test %! base_key = uint64 (14695981039346656037); %! key1 = keyHash (string ('1'), base_key); %! key2 = keyHash (string ('1')); %! assert_equal (key1, key2); %!test %! base_key = uint64 (2342124352342344234); %! key = keyHash (string ('1'), base_key); %! assert_equal (key, uint64 (17779550089123719118)); %!error ... %! keyHash (string (1:5), uint64 ([1, 2])) %!error ... %! keyHash (string (1:5), 2231107818551636405) ## A zero offset basis is a basis, not the absence of one, and a falsy base ## must still be validated rather than silently replaced by the default. %!test %! a = keyHash (string ('1')); %! assert_equal (isequal (a, keyHash (string ('1'), uint64 (0))), false); %! assert_equal (keyHash (string ('1'), []), a); %!error ... %! keyHash (string (1:5), 0) %!error ... %! keyHash (string (1:5), uint64 ([0, 1])) ################################################################################ ## ** Query Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'contains' 'endsWith' 'startsWith' 'matches' ## ## 'iscolumn' 'isempty' 'ismatrix' 'ismember' ## ## 'ismissing' 'isrow' 'isscalar' 'issorted' ## ## 'issortedrows' 'isstring' 'isvector' ## ## ## ################################################################################ %!test %! str = string ({'Mary Ann Jones', 'Paul Jay Burns', 'John Paul Smith'}); %! TF = contains (str, 'Paul'); %! assert_equal (TF, [false, true, true]); %!test %! str = string ({'Mary Ann Jones', 'Paul Jay Burns', 'John Paul Smith'}); %! TF = contains (str, string ('Paul')); %! assert_equal (TF, [false, true, true]); %!test %! str = string ({'Mary Ann Jones', 'Paul Jay Burns', 'John Paul Smith'}); %! TF = contains (str, 'paul'); %! assert_equal (TF, [false, false, false]); %!test %! str = string ({'Mary Ann Jones', 'Paul Jay Burns', 'John Paul Smith'}); %! TF = contains (str, 'paul', 'IgnoreCase', true); %! assert_equal (TF, [false, true, true]); %!test %! str = string ({'Anne', 'Elizabeth', 'Marianne', '"Tracy'}); %! TF = contains (str, 'anne', 'IgnoreCase', true); %! assert_equal (TF, [true, false, true, false]); %!test %! str = string ({'Anne', 'Elizabeth', 'Marianne', '"Tracy'}); %! TF = contains (str, {'anne', 'Anne'}, 'IgnoreCase', false); %! assert_equal (TF, [true, false, true, false]); %!error contains (string) %!error ... %! contains (string ('a'), 1) %!error ... %! contains (string ('a'), 'a', 'IgnoreCase', 1) %!error ... %! contains (string ('a'), 'a', 'IgnoreCase', [true, true]) %!error ... %! contains (string ('a'), 'a', 'whatever') %!error ... %! contains (string ('a'), 'a', 'whatever', 1) %!test %! str = string ({'a.doc', 'b.odt', 'c.doc', 'd.ods'}); %! TF = endsWith (str, 'odt'); %! assert_equal (TF, [false, true, false, false]); %!test %! str = string ({'a.doc', 'b.odt', 'c.doc', 'd.ods'}); %! TF = endsWith (str, string ('odt')); %! assert_equal (TF, [false, true, false, false]); %!test %! str = string ({'a.doc', 'b.odt', 'c.doc', 'd.ods'}); %! TF = endsWith (str, {'odt', 'ods'}); %! assert_equal (TF, [false, true, false, true]); %!test %! str = string ({'a.doc', 'b.odt', 'c.doc', 'd.ods'}); %! TF = endsWith (str, {'odt', 'ODS'}); %! assert_equal (TF, [false, true, false, false]); %!test %! str = string ({'a.doc', 'b.odt', 'c.doc', 'd.ods'}); %! TF = endsWith (str, {'odt', 'ODS'}, 'IgnoreCase', true); %! assert_equal (TF, [false, true, false, true]); %!error endsWith (string) %!error ... %! endsWith (string ('a'), 1) %!error ... %! endsWith (string ('a'), 'a', 'IgnoreCase', 1) %!error ... %! endsWith (string ('a'), 'a', 'IgnoreCase', [true, true]) %!error ... %! endsWith (string ('a'), 'a', 'whatever') %!error ... %! endsWith (string ('a'), 'a', 'whatever', 1) %!test %! str = string ({'ball', 'base', 'Balkan', 'bison'}); %! TF = startsWith (str, 'ba'); %! assert_equal (TF, [true, true, false, false]); %!test %! str = string ({'ball', 'base', 'Balkan', 'bison'}); %! TF = startsWith (str, string ('ba')); %! assert_equal (TF, [true, true, false, false]); %!test %! str = string ({'ball', 'base', 'Balkan', 'bison'}); %! TF = startsWith (str, {'ba', 'bi'}); %! assert_equal (TF, [true, true, false, true]); %!test %! str = string ({'ball', 'base', 'Balkan', 'bison'}); %! TF = startsWith (str, {'ba', 'Bi'}); %! assert_equal (TF, [true, true, false, false]); %!test %! str = string ({'ball', 'base', 'Balkan', 'bison'}); %! TF = startsWith (str, {'ba', 'Bi'}, 'IgnoreCase', true); %! assert_equal (TF, [true, true, true, true]); %!error startsWith (string) %!error ... %! startsWith (string ('a'), 1) %!error ... %! startsWith (string ('a'), 'a', 'IgnoreCase', 1) %!error ... %! startsWith (string ('a'), 'a', 'IgnoreCase', [true, true]) %!error ... %! startsWith (string ('a'), 'a', 'whatever') %!error ... %! startsWith (string ('a'), 'a', 'whatever', 1) %!test %! str = string ({'Mercury', 'Venus', 'Earth', 'Mars'}); %! TF = matches (str, 'Earth'); %! assert_equal (TF, [false, false, true, false]); %!test %! str = string ({'Mercury', 'Venus', 'Earth', 'Mars'}); %! TF = matches (str, string ('Earth')); %! assert_equal (TF, [false, false, true, false]); %!test %! str = string ({'Mercury', 'Venus', 'Earth', 'Mars'}); %! TF = matches (str, 'earth'); %! assert_equal (TF, [false, false, false, false]); %!test %! str = string ({'Mercury', 'Venus', 'Earth', 'Mars'}); %! TF = matches (str, 'earth', 'IgnoreCase', true); %! assert_equal (TF, [false, false, true, false]); %!test %! str = string ({'Mercury', 'Venus', 'Earth', 'Mars'}); %! TF = matches (str, {'Earth', 'Mercury'}); %! assert_equal (TF, [true, false, true, false]); %!test %! str = string ({'Mercury', 'Venus', 'Earth', 'Mars'}); %! TF = matches (str, {'earth', 'mercury'}); %! assert_equal (TF, [false, false, false, false]); %!test %! str = string ({'Mercury', 'Venus', 'Earth', 'Mars'}); %! TF = matches (str, {'earth', 'mercury'}, 'IgnoreCase', true); %! assert_equal (TF, [true, false, true, false]); %!error matches (string) %!error ... %! matches (string ('a'), 1) %!error ... %! matches (string ('a'), 'a', 'IgnoreCase', 1) %!error ... %! matches (string ('a'), 'a', 'IgnoreCase', [true, true]) %!error ... %! matches (string ('a'), 'a', 'whatever') %!error ... %! matches (string ('a'), 'a', 'whatever', 1) %!assert_equal (iscolumn (string ([1, 2, 3, 4])), false) %!assert_equal (iscolumn (string ([1; 2; 3; 4])), true) %!assert_equal (iscolumn (string ([1, 2; 3, 4])), false) %!assert_equal (iscolumn (string ({'1', '2', '3', '4'})), false) %!assert_equal (iscolumn (string ({'1'; '2'; '3'; '4'})), true) %!assert_equal (iscolumn (string ({'1', '2'; '3', '4'})), false) %!assert_equal (iscolumn (string ([])), false) %!assert_equal (iscolumn (string (ones (1, 0))), false) %!assert_equal (iscolumn (string (ones (0, 1))), true) %!assert_equal (iscolumn (string (1)), true) %!assert_equal (iscolumn (string (ones (2, 3, 4))), false) %!assert_equal (isempty (string ([])), true) %!assert_equal (isempty (string ('')), false) %!assert_equal (isempty (string (ones (2, 0, 3))), true) %!assert_equal (isempty (string (ones (2, 1, 3))), false) %!assert_equal (isempty (string (1)), false) %!assert_equal (isempty (string), false) %!assert_equal (ismatrix (string), true) %!assert_equal (ismatrix (string ([])), true) %!assert_equal (ismatrix (string (ones (2, 4))), true) %!assert_equal (ismatrix (string (ones (2, 4, 1))), true) %!assert_equal (ismatrix (string (ones (2, 1, 4))), false) %!assert_equal (ismatrix (string (ones (2, 3, 4))), false) %!assert_equal (ismatrix (string (ones (0, 1))), true) %!assert_equal (ismatrix (string (ones (1, 0))), true) %!assert_equal (ismatrix (string (char (0, 1))), true) %!assert_equal (ismatrix (string (char (1, 0))), true) %!test %! A = string ({'dog', 'cat', 'fish', 'horse'}); %! B = string ({'dog ', 'cat', 'fish ', 'horse'}); %! [TF, index] = ismember (A, B); %! assert_equal (TF, [false, true, false, true]); %! assert_equal (index, [0, 2, 0, 4]); %!test %! A = string ({'dog', 'cat', 'fish', 'horse'}); %! B = ['dog';'cat']; %! [TF, index] = ismember (A, B); %! assert_equal (TF, [true, true, false, false]); %! assert_equal (index, [1, 2, 0, 0]); %! [TF, index] = ismember (B, A); %! assert_equal (TF, [true; true]); %! assert_equal (index, [1; 2]); %!test %! A = string ({'dog', 'cat'; 'fish', 'horse'}); %! B = string ({'dog ', 'cat'}); %! [TF, index] = ismember (A, B); %! assert_equal (TF, [false, true; false, false]); %! assert_equal (index, [0, 2; 0, 0]); %! [TF, index] = ismember (A, B, 'rows'); %! assert_equal (TF, [false; false]); %! assert_equal (index, [0; 0]); %!test %! A = string ({'dog', 'cat'; 'fish', 'horse'}); %! B = string ({'dog', 'cat'}); %! [TF, index] = ismember (A, B); %! assert_equal (TF, [true, true; false, false]); %! assert_equal (index, [1, 2; 0, 0]); %! [TF, index] = ismember (A, B, 'rows'); %! assert_equal (TF, [true; false]); %! assert_equal (index, [1; 0]); %!error ismember (1, string) %!error ismember (string, 1) %!error ... %! ismember (string (1), string ([1, 2]), 'rows') %!assert_equal (ismissing (string ('a'), 'a'), true) %!assert_equal (ismissing (string ('a'), string ({'a', 'c'})), true) %!assert_equal (ismissing (string ({'a', 'b'}), string ({'a', 'c'})), [true, ... %! false]) %!test %! TF = ismissing (string ({'a', 'b', ''}), string ({'a', 'c'})); %! assert_equal (TF, logical ([1, 0, 0])); %!test %! TF = ismissing (string ({'a', 'b', ''}), string ({'a'; 'c'})); %! assert_equal (TF, logical ([1, 0, 0])); %!test %! TF = ismissing (string ({'a'; 'b'; ''}), string ({'a', 'c'})); %! assert_equal (TF, logical ([1; 0; 0])); %!test %! TF = ismissing ([string({'a', 'b', ''}), missing], string ({'a', 'c'})); %! assert_equal (TF, logical ([1, 0, 0, 0])); %!test %! S = string ({'a', 'b', ''}); %! S(2) = missing; %! assert_equal (ismissing (S, missing), [false, true, false]); %! assert_equal (ismissing (S, [missing, string('a')]), [true, true, false]); %! assert_equal (ismissing (S, string (missing)), [false, true, false]); %!error ... %! ismissing (string, string, string) %!error ... %! ismissing (string, 1) %!error ... %! ismissing (string, {1}) %!error ... %! ismissing (string, string (ones (2))) %!assert_equal (isrow (string ([1, 2, 3, 4])), true) %!assert_equal (isrow (string ([1; 2; 3; 4])), false) %!assert_equal (isrow (string ([1, 2; 3, 4])), false) %!assert_equal (isrow (string ([])), false) %!assert_equal (isrow (string ()), true) %!assert_equal (isrow (string (ones (1, 0))), true) %!assert_equal (isrow (string (ones (0, 1))), false) %!assert_equal (isrow (string (1)), true) %!assert_equal (isrow (string (ones (2, 3, 4))), false) %!assert_equal (isscalar (string ([])), false) %!assert_equal (isscalar (string ()), true) %!assert_equal (isscalar (string (ones (1, 0))), false) %!assert_equal (isscalar (string (ones (0, 1))), false) %!assert_equal (isscalar (string (1)), true) %!assert_equal (isscalar (string (NaN)), true) %!assert_equal (isscalar (string (missing)), true) %!assert_equal (isscalar (string ([1, 2])), false) %!test ## issorted, verified against MATLAB R2026a %! assert_equal (issorted (string ({'a', 'b', 'c'})), true); %! assert_equal (issorted (string ({'a', 'b', 'c'}), 'strictascend'), true); %! assert_equal (issorted (string ({'a', 'a', 'b'}), 'strictascend'), false); %! assert_equal (issorted (string ({'c', 'b', 'a'}), 'descend'), true); %! assert_equal (issorted (string ({'a', 'b', 'c'}), 'monotonic'), true); %! assert_equal (issorted (string ({'b', 'a', 'c'}), 'monotonic'), false); %! assert_equal (issorted ([string('a') string('b') missing], 'ascend'), true); %! assert_equal (issorted ([string('a') string('b') missing], ... %! 'strictascend'), false); %!error ... %! issorted (string ({'a', 'b'}), 'MissingPlacement', 'bad') %!error ... %! issorted (string ({'a', 'b'}), 'asd') %!test ## strict issortedrows semantics, verified against MATLAB R2026a %! assert_equal (issortedrows (string ({'a', 'x'; 'a', 'y'}), ... %! 'strictascend'), false); %! assert_equal (issortedrows (string ({'a', 'x'; 'a', 'x'}), ... %! 'strictascend'), false); %! assert_equal (issortedrows (string ({'a', 'p'; 'a', 'q'}), ... %! {'strictascend', 'ascend'}), false); %! assert_equal (issortedrows (string ({'a', 'p'; 'b', 'p'}), ... %! {'ascend', 'strictascend'}), true); %! assert_equal (issortedrows (string ({'b', 'z'; 'a', 'y'}), 'monotonic'), ... %! true); %! assert_equal (issortedrows (string ({'b', 'z'; 'a', 'y'}), 'descend'), true); %! assert_equal (issortedrows (string ({'a', 'q'; 'b', 'p'}), 2, ... %! 'strictascend'), false); %! assert_equal (issortedrows (string ({'b', 'p'; 'a', 'q'}), 2, ... %! 'strictascend'), true); %!test ## strict issortedrows with missing values (vs MATLAB R2026a) %! assert_equal (issortedrows ( ... %! [string('a') string('p'); missing string('q')], ... %! {'strictascend', 'ascend'}), false); %! assert_equal (issortedrows ( ... %! [string('a') string('p'); missing string('q')], ... %! {'ascend', 'strictascend'}), true); %! assert_equal (issortedrows ([missing string('p') string('z'); ... %! string('x') string('q') string('y')], ... %! 2, 'strictascend'), true); %! assert_equal (issortedrows ( ... %! [string('a') missing; string('b') string('y')], ... %! 'strictascend'), true); %!error ... %! issortedrows (string ({'a', 'b'}), {'asd'}) %!error ... %! issortedrows (string ({'a', 'b'; 'c', 'd'}), [1, 2], ... %! {'strictascend', 'ascend', 'descend'}) %!assert_equal (isstring (string), true) %!assert_equal (isstring (cellstr (string)), false) %!assert_equal (isvector (string ([1, 2, 3, 4])), true) %!assert_equal (isvector (string ([1; 2; 3; 4])), true) %!assert_equal (isvector (string ([1, 2; 3, 4])), false) %!assert_equal (isvector (string ([])), false) %!assert_equal (isvector (string ()), true) %!assert_equal (isvector (string ()), true) %!assert_equal (isvector (string (ones (1, 0))), true) %!assert_equal (isvector (string (ones (0, 1))), true) %!assert_equal (isvector (string (1)), true) %!assert_equal (isvector (string (ones (2, 3, 4))), false) ################################################################################ ## ** Relational Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'eq' 'ge' 'gt' 'le' ## ## 'lt' 'ne' 'strcmp' 'strcmpi' ## ## 'strncmp' 'strncmpi' ## ## ## ################################################################################ %!assert_equal (eq (string ('A'), string ('A')), true); %!assert_equal (eq (string ('A'), string ('b')), false); %!assert_equal (eq (string ('A'), {'A', 'b'}), [true, false]); %!assert_equal (eq ({'A', 'b'}, string ('A')), [true, false]); %!assert_equal (eq (string ({'A', 'b'}), 'A'), [true, false]); %!assert_equal (eq ('A', string ({"A", "b"})), [true, false]); %!assert_equal (eq (string ({'A', 'B'}), ... %! string ({'A'; 'B'})), logical (eye (2))) %!error ... %! eq (string ('A'), 2) %!error ... %! NaN == string ('A') %!error ... %! eq (string ({'A', 'B', 'A'; 'A', 'D', 'F'}), string ({'A', 'B'})) %!assert_equal (ge (string ('A'), string ('A')), true); %!assert_equal (ge (string ('A'), string ('b')), false); %!assert_equal (ge (string ('b'), {'A', 'b'}), [true, true]); %!assert_equal (ge ({'A', 'b'}, string ('b')), [false, true]); %!assert_equal (ge (string ({'A', 'b'}), 'A'), [true, true]); %!assert_equal (ge ('A', string ({'A', 'b'})), [true, false]); %!assert_equal (ge (string ({'A', 'B'}), string ({'A'; 'B'})), logical ([1, ... %! 1; 0, 1])) %!error ... %! ge (string ('A'), 2) %!error ... %! NaN >= string ('A') %!error ... %! ge (string ({'A', 'B', 'A'; 'A', 'D', 'F'}), string ({'A', 'B'})) %!assert_equal (gt (string ('A'), string ('A')), false); %!assert_equal (gt (string ('A'), string ('b')), false); %!assert_equal (gt (string ('b'), {'A', 'b'}), [true, false]); %!assert_equal (gt ({'A', 'b'}, string ('b')), [false, false]); %!assert_equal (gt (string ({'A', 'b'}), 'A'), [false, true]); %!assert_equal (gt ('A', string ({'A', 'b'})), [false, false]); %!assert_equal (gt (string ({'A', 'B'}), string ({'A'; 'B'})), logical ([0, ... %! 1; 0, 0])) %!error ... %! gt (string ('A'), 2) %!error ... %! NaN > string ('A') %!error ... %! gt (string ({'A', 'B', 'A'; 'A', 'D', 'F'}), string ({'A', 'B'})) %!assert_equal (le (string ('A'), string ('A')), true); %!assert_equal (le (string ('A'), string ('b')), true); %!assert_equal (le (string ('b'), {'A', 'b'}), [false, true]); %!assert_equal (le ({'A', 'b'}, string ('b')), [true, true]); %!assert_equal (le (string ({'A', 'b'}), 'A'), [true, false]); %!assert_equal (le ('A', string ({'A', 'b'})), [true, true]); %!assert_equal (le (string ({'A', 'B'}), string ({'A'; 'B'})), logical ([1, ... %! 0; 1, 1])) %!error ... %! le (string ('A'), 2) %!error ... %! NaN <= string ('A') %!error ... %! le (string ({'A', 'B', 'A'; 'A', 'D', 'F'}), string ({'A', 'B'})) %!assert_equal (lt (string ('A'), string ('A')), false); %!assert_equal (lt (string ('A'), string ('b')), true); %!assert_equal (lt (string ('b'), {'A', 'b'}), [false, false]); %!assert_equal (lt ({'A', 'b'}, string ("b")), [true, false]); %!assert_equal (lt (string ({'A', 'b'}), 'A'), [false, false]); %!assert_equal (lt ('A', string ({'A', 'b'})), [false, true]); %!assert_equal (lt (string ({'A', 'B'}), string ({'A'; 'B'})), logical ([0, ... %! 0; 1, 0])) %!assert_equal (lt (string ('ÿ'), string ('Ā')), true); %!error ... %! lt (string ('A'), 2) %!error ... %! NaN < string ('A') %!error ... %! lt (string ({'A', 'B', 'A'; 'A', 'D', 'F'}), string ({'A', 'B'})) %!assert_equal (ne (string ('A'), string ('A')), false); %!assert_equal (ne (string ('A'), string ('b')), true); %!assert_equal (ne (string ('A'), {'A', 'b'}), [false, true]); %!assert_equal (ne ({'A', 'b'}, string ('A')), [false, true]); %!assert_equal (ne (string ({'A', 'b'}), 'A'), [false, true]); %!assert_equal (ne ('A', string ({'A', 'b'})), [false, true]); %!assert_equal (ne (string ({'A', 'B'}), string ({'A'; 'B'})), logical ([0, ... %! 1; 1, 0])) %!error ... %! ne (string ('A'), 2) %!error ... %! NaN != string ('A') %!error ... %! ne (string ({'A', 'B', 'A'; 'A', 'D', 'F'}), string ({'A', 'B'})) %!assert_equal (strcmp (string ("A"), string ("A")), true); %!assert_equal (strcmp (string ("A"), string ("b")), false); %!assert_equal (strcmp (string ("A"), {"A", "b"}), [true, false]); %!assert_equal (strcmp ({"A", "b"}, string ("A")), [true, false]); %!assert_equal (strcmp (string ({'A', 'b'}), 'A'), [true, false]); %!assert_equal (strcmp ('A', string ({"A", "b"})), [true, false]); %!error ... %! strcmp (string ("A"), 2) %!error ... %! strcmp (2, string ("A")) %!error ... %! strcmp (string ({"A","B"}), string ({"A";"B"})) %!assert_equal (strcmpi (string ("A"), string ("a")), true); %!assert_equal (strcmpi (string ("A"), string ("b")), false); %!assert_equal (strcmpi (string ("a"), {"A", "b"}), [true, false]); %!assert_equal (strcmpi ({"A", "b"}, string ("a")), [true, false]); %!assert_equal (strcmpi (string ({'a', 'b'}), 'A'), [true, false]); %!assert_equal (strcmpi ('A', string ({"a", "b"})), [true, false]); %!error ... %! strcmpi (string ("A"), 2) %!error ... %! strcmpi (2, string ("A")) %!error ... %! strcmpi (string ({"A","B"}), string ({"a";"b"})) %!assert_equal (strncmp (string ("ASDFG"), "ASDER", 3), true); %!assert_equal (strncmp (string ("ASDFG"), "ASDER", 4), false); %!assert_equal (strncmp (string ("ASDFG"), {"ASDER","ASFGH"}, 3), [true, ... %! false]); %!assert_equal (strncmp (string ("ASDFG"), {"ASDER","ASFGH"}, 2), [true, true]); %!error ... %! strncmp (string ("A"), 2) %!error ... %! strncmp (2, string ("A")) %!error ... %! strncmp (string ({"A","B"}), string ({"a";"b"}), 1) %!assert_equal (strncmpi (string ("asDFG"), "ASDER", 3), true); %!assert_equal (strncmpi (string ("ASDFG"), "asDER", 4), false); %!assert_equal (strncmpi (string ("asdfg"), {"ASDER","ASFGH"}, 3), [true, ... %! false]); %!assert_equal (strncmpi (string ("asdfg"), {"ASDER","ASFGH"}, 2), [true, ... %! true]); %!error ... %! strncmpi (string ("A"), 2) %!error ... %! strncmpi (2, string ("A")) %!error ... %! strncmpi (string ({"A","B"}), string ({"a";"b"}), 1) ################################################################################ ## ** String Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'append' 'compose' 'erase' 'eraseBetween' ## ## 'extract' 'extractAfter' 'extractBefore' 'extractBetween' ## ## 'insertAfter' 'insertBefore' 'replace' 'replaceBetween' ## ## 'reverse' 'sort' 'sortrows' 'split' ## ## 'splitlines' 'strcat' 'strip' 'pad' ## ## 'join' 'plus' 'lower' 'upper' ## ## 'unique' ## ## ## ################################################################################ ## append: concatenates corresponding elements of its inputs %!assert_equal (append (string ('Hello'), ... %! string (' World')), string ('Hello World')) %!assert_equal (append (string ('a'), string ('b'), ... %! string ('c')), string ('abc')) ## append: preserves trailing whitespace (unlike 'strcat') %!assert_equal (append (string ('a '), string ('b')), string ('a b')) ## append: char vectors are one element, cellstr arguments are accepted %!assert_equal (append (string ('x'), '_', string ('y')), string ('x_y')) %!assert_equal (append (string ('x'), {'_'}, string ('y')), string ('x_y')) ## append: char matrices contribute one element per row (whitespace preserved) %!assert_equal (append (string ({'1'; '2'}), char ('a', ... %! 'bb')), string ({'1a '; '2bb'})) ## append: element-wise over equal-sized arrays %!assert_equal (append (string ({'a'; 'b'}), ... %! string ({'1'; '2'})), string ({'a1'; 'b2'})) ## append: scalar expands against a non-scalar argument %!assert_equal (append (string ('p_'), string ({'a'; 'b'; 'c'})), ... %! string ({'p_a'; 'p_b'; 'p_c'})) ## append: implicit expansion of a row and a column yields a matrix %!assert_equal (append (string ({'a', 'b'}), string ({'1'; '2'})), ... %! string ({'a1', 'b1'; 'a2', 'b2'})) ## append: a single argument is returned unchanged %!assert_equal (append (string ({'a'; 'b'})), string ({'a'; 'b'})) ## append: a missing value propagates like NaN (any missing input -> missing), ## and a missing element carries no text %!test %! str = append (string ('a'), string (NaN)); %! assert_equal (ismissing (str), true); %! assert_equal (cellstr (str), {''}); %!test %! str = append (string ({'a'; 'b'}), string ({missing; 'z'})); %! assert_equal (ismissing (str), [true; false]); %! assert_equal (cellstr (str), {''; 'bz'}); ## append: error branches %!error ... %! append (string ('a'), 5) %!error ... %! append (string ({'a'; 'b'; 'c'}), string ({'x'; 'y'})) ## compose: formatting syntax -- single operator %!assert_equal (compose (string ('%d apples'), 3), string ('3 apples')) %!assert_equal (compose (string ('%d'), [1 2 3]), string ({'1', '2', '3'})) %!assert_equal (compose (string ('%d'), [1; 2; 3]), string ({'1'; '2'; '3'})) %!assert_equal (compose (string ('%d apples'), [1; 2; 3]), ... %! string ({'1 apples'; '2 apples'; '3 apples'})) ## compose: formatting syntax -- multiple operators per row %!assert_equal (compose (string ('%d + %d = %d'), ... %! [1 2 3]), string ('1 + 2 = 3')) %!assert_equal (compose (string ('%d %d %d'), [1 2 3; 4 5 6]), ... %! string ({'1 2 3'; '4 5 6'})) ## compose: FORMATSPEC repeats when A has more columns than operators %!assert_equal (compose (string ('%d-%d '), [1 2 3 4]), string ({'1-2 ', ... %! '3-4 '})) ## compose: leftover operators are emitted unchanged %!assert_equal (compose (string ('%d and %d'), 5), string ('5 and %d')) ## compose: multiple input arrays consumed left to right %!assert_equal (compose (string ('%s = %d'), string ('a'), 1), string ('a = 1')) %!assert_equal (compose (string ('%s=%d'), string ({'a'; 'b'}), [1; 2]), ... %! string ({'a=1'; 'b=2'})) ## compose: scalar input expands against a column vector %!assert_equal (compose (string ('%s%d'), string ('row'), [1; 2; 3]), ... %! string ({'row1'; 'row2'; 'row3'})) ## compose: accepted input types %!assert_equal (compose (string ('%s!'), 'hi'), string ('hi!')) %!assert_equal (compose (string ('%s!'), string ('hi')), string ('hi!')) %!assert_equal (compose (string ('%d'), true), string ('1')) %!assert_equal (compose (string ('%5.2f'), pi), string (' 3.14')) ## compose: '*' dynamic field width consumes an extra value %!assert_equal (compose (string ('%*d'), [5 42]), string (' 42')) ## compose: literal '%%' becomes a single '%' %!assert_equal (compose (string ('%d%% done'), 50), string ('50% done')) ## compose: escape sequences in literal text of FORMATSPEC are translated %!assert_equal (compose (string ('Line %d\n'), [1; 2]), ... %! string ({sprintf('Line 1\n'); sprintf('Line 2\n')})) ## compose: zero-column input preserves size and applies no formatting %!test %! out = compose (string ('%d'), zeros (2, 0)); %! assert_equal (class (out), 'string'); %! assert_equal (size (out), [2, 0]); ## compose: escape-translation syntax -- compose (TXT) %!assert_equal (compose (string ('a\tb\nc')), string (sprintf ('a\tb\nc'))) %!assert_equal (compose (string ('100%% done')), string ('100% done')) ## compose (TXT) leaves formatting operators unchanged %!assert_equal (compose (string ('val=%d\n')), string (sprintf ('val=%%d\n'))) ## compose (TXT) preserves size over a multi-element array %!test %! out = compose (string ({'a\tb'; 'c'; 'd\ne'})); %! assert_equal (size (out), [3, 1]); %! assert_equal (cellstr (out), {sprintf('a\tb'); 'c'; sprintf('d\ne')}); ## compose: error branches %!error ... %! compose (string ({'%d', '%d'}), 1) %!error ... %! compose (string (NaN), 1) %!error ... %! compose (string ('%d'), {1, 2}) %!error ... %! compose (string ('%d%d'), [1; 2; 3], [1; 2]) ## erase: removes every occurrence of a single match from each element %!assert_equal (erase (string ('the quick brown fox'), 'o'), ... %! string ('the quick brwn fx')) %!assert_equal (erase (string ('Hello World'), ' World'), string ('Hello')) ## erase: a match that does not occur leaves the text unchanged %!assert_equal (erase (string ('abc'), 'z'), string ('abc')) ## erase: an empty match removes nothing %!assert_equal (erase (string ('abc'), ''), string ('abc')) ## erase: a char-vector match keeps trailing whitespace (bare cellstr deblanks) %!assert_equal (erase (string ('the quick brown'), ... %! 'quick '), string ('the brown')) ## erase: accepts char vectors, cell arrays of char vectors, and string scalars %!assert_equal (erase (string ('abcabc'), string ('bc')), string ('aa')) %!assert_equal (erase (string ('aXa'), {'X'}), string ('aa')) ## erase: applied element-wise across an array, preserving its size %!assert_equal (erase (string ({'aXa', 'bXb'}), 'X'), string ({'aa', 'bb'})) %!assert_equal (erase (string ({'a.b'; 'c.d'}), '.'), string ({'ab'; 'cd'})) ## erase: a multi-element match deletes every occurrence of every element %!assert_equal (erase (string ('The quick brown fox'), string ({'The ', ... %! 'quick '})), ... %! string ('brown fox')) %!assert_equal (erase (string ('aXbYc'), {'X', 'Y'}), string ('abc')) ## erase: MATCH size need not match STR size (every element is removed from all) %!assert_equal (erase (string ({'aXbYc', 'XYZ'}), string ({'X', 'Y'})), ... %! string ({'abc', 'Z'})) ## erase: erasing the entire content yields an empty, non-missing string %!test %! str = erase (string ('abc'), 'abc'); %! assert_equal (str, string ('')); %! assert_equal (ismissing (str), false); ## erase: missing values are preserved and carry no text %!test %! str = erase (string ({'a.b.c', missing}), '.'); %! assert_equal (ismissing (str), [false, true]); %! assert_equal (cellstr (str), {'abc', ''}); ## erase: error branch %!error ... %! erase (string ('ab'), 5) ## eraseBetween: pattern syntax erases between boundaries (default exclusive) %!assert_equal (eraseBetween (string ('The quick brown fox'), 'quick', ... %! ' fox'), ... %! string ('The quick fox')) ## eraseBetween: pattern syntax with explicit exclusive boundaries %!assert_equal (eraseBetween (string ('a[xyz]b'), '[', ']', 'Boundaries', ... %! 'exclusive'), ... %! string ('a[]b')) ## eraseBetween: pattern syntax with inclusive boundaries erases them too %!assert_equal (eraseBetween ( ... %! string ('The quick brown fox jumps over the lazy dog'), ... %! ' brown', 'lazy', 'Boundaries', 'inclusive'), ... %! string ('The quick dog')) %!assert_equal (eraseBetween (string ('a[xyz]b'), '[', ']', 'Boundaries', ... %! 'inclusive'), ... %! string ('ab')) ## eraseBetween: char boundaries keep trailing whitespace (cellstr deblanks) %!assert_equal (eraseBetween (string ('a quick b'), 'a ', ' b', ... %! 'Boundaries', 'inclusive'), string ('')) ## eraseBetween: boundary substrings accepted as char, cellstr, or string %!assert_equal (eraseBetween (string ('ab'), '<', '>'), string ('a<>b')) %!assert_equal (eraseBetween (string ('ab'), {'<'}, {'>'}), string ('a<>b')) %!assert_equal (eraseBetween (string ('ab'), string ('<'), string ('>')), ... %! string ('a<>b')) ## eraseBetween: a missing boundary leaves the element unchanged %!assert_equal (eraseBetween (string ('hello'), 'X', 'o'), string ('hello')) %!assert_equal (eraseBetween (string ('hello'), 'h', 'X'), string ('hello')) ## eraseBetween: the end boundary must follow the start match %!assert_equal (eraseBetween (string ('a|b|c'), 'b', 'a'), string ('a|b|c')) ## eraseBetween: a scalar boundary pair broadcasts over an array %!assert_equal (eraseBetween (string ({'1', '2'}), '<', '>', ... %! 'Boundaries', 'inclusive'), string ({'1', '2'})) ## eraseBetween: boundary pairs given element-wise, preserving size %!assert_equal (eraseBetween (string ({'aXbYc'; '1P2Q3'}), ... %! string ({'X'; 'P'}), ... %! string ({'Y'; 'Q'})), string ({'aXYc'; '1PQ3'})) ## eraseBetween: position syntax erases between positions (default inclusive) %!assert_equal (eraseBetween (string ('Edgar Allen Poe'), 6, ... %! 11), string ('Edgar Poe')) %!assert_equal (eraseBetween (string ('mushrooms, peppers, and onions'), 10, ... %! 19), ... %! string ('mushrooms and onions')) ## eraseBetween: position syntax with exclusive boundaries keeps the endpoints %!assert_equal (eraseBetween (string ('small|medium|large'), 6, 13, ... %! 'Boundaries', 'exclusive'), string ('small||large')) ## eraseBetween: equal positions erase a single character (inclusive) or none %!assert_equal (eraseBetween (string ('abcde'), 3, 3), string ('abde')) %!assert_equal (eraseBetween (string ('abcde'), 3, 3, 'Boundaries', ... %! 'exclusive'), ... %! string ('abcde')) ## eraseBetween: scalar positions broadcast; array positions map by element %!assert_equal (eraseBetween (string ({'abcdef'; 'ABCDEF'}), 2, 4), ... %! string ({'aef'; 'AEF'})) %!assert_equal (eraseBetween (string ({'abcdef'; 'ABCDEF'}), [2; 3], ... %! [4; 5]), ... %! string ({'aef'; 'ABF'})) ## eraseBetween: 'Boundaries' name and string option are case-insensitive %!assert_equal (eraseBetween (string ('small|medium|large'), 6, 13, ... %! 'boundaries', ... %! string ('exclusive')), string ('small||large')) ## eraseBetween: missing values are preserved and carry no text %!test %! str = eraseBetween (string ({'a[x]b', missing}), '[', ']', 'Boundaries', ... %! 'inclusive'); %! assert_equal (ismissing (str), [false, true]); %! assert_equal (cellstr (str), {'ab', ''}); ## eraseBetween: error branches %!error ... %! eraseBetween (string ('ab'), 1) %!error ... %! eraseBetween (string ('ab'), 1, 'x') %!error ... %! eraseBetween (string ('ab'), 'a', 'b', 'Boundaries', 'foo') %!error ... %! eraseBetween (string ('ab'), 1, 2, 'Foo', 'bar') %!error ... %! eraseBetween (string ('ab'), 1, 2, 'Foo') %!error ... %! eraseBetween (string ('abc'), 2, 9) %!error ... %! eraseBetween (string ('abc'), 0, 2) %!error ... %! eraseBetween (string ('abc'), 1.5, 2) %!error ... %! eraseBetween (string ('abcde'), 4, 2) %!error ... %! eraseBetween (string ({'a', 'b', 'c'}), [1, 2], [1, 2]) %!error ... %! eraseBetween (string ({'a', 'b', 'c'}), 1, [1, 2]) ## extract: pattern form returns each match of PAT along the columns %!assert_equal (extract (string ('xax ax'), 'ax'), string ({'ax', 'ax'})) %!assert_equal (extract (string ('hello world'), 'o'), string ({'o', 'o'})) ## extract: a multi-element PAT matches any alternative, first listed winning %!assert_equal (extract (string ('cabbage'), {'a', 'b'}), ... %! string ({'a', 'b', 'b', 'a'})) %!assert_equal (extract (string ('ababab'), string ({'ab', 'a'})), ... %! string ({'ab', 'ab', 'ab'})) ## extract: matches do not overlap and are taken left to right %!assert_equal (extract (string ('aaaa'), 'aa'), string ({'aa', 'aa'})) ## extract: a scalar with one match yields a 1x1 string %!assert_equal (extract (string ('a-b'), '-'), string ('-')) ## extract: no match yields an empty string array %!test %! str = extract (string ('hello'), 'z'); %! assert_equal (class (str), 'string'); %! assert_equal (size (str), [1, 0]); ## extract: a non-scalar STR gives one row per element and one column per match %!assert_equal (extract (string ({'p.q.r'; 'x.y.z'}), '.'), ... %! string ({'.', '.'; '.', '.'})) ## extract: elements that do not match are treated as having zero matches %!test %! str = extract (string ({'ab', missing}), '.'); %! assert_equal (class (str), 'string'); %! assert_equal (size (str), [2, 0]); ## extract: position form returns the single character at POS %!assert_equal (extract (string ("All's well that ends well"), 1), string ('A')) %!assert_equal (extract (string ("All's well that ends well"), ... %! 25), string ('l')) ## extract: position form is element-wise and preserves the size of STR %!assert_equal (extract (string ({'abc'; 'xyz'}), [1; 3]), string ({'a'; 'z'})) ## extract: a scalar position broadcasts over the array %!assert_equal (extract (string ({'abc', 'xyz'}), 2), string ({'b', 'y'})) ## extract: position form preserves missing values %!test %! str = extract (string ({'abc', missing}), 1); %! assert_equal (ismissing (str), [false, true]); %! assert_equal (cellstr (str), {'a', ''}); ## extract: error branches %!error ... %! extract (string ('ab')) %!error ... %! extract (string ('ab'), {1, 2}) %!error ... %! extract (string ({'a.b'; 'c.d.e'}), '.') %!error ... %! extract (string ({'a', 'b', 'c'}), [1, 2]) %!error ... %! extract (string ('ab'), 0) %!error ... %! extract (string ('ab'), 1.5) %!error ... %! extract (string ('ab'), 5) ## extractAfter: pattern form returns the text after the first match of PAT %!assert_equal (extractAfter (string ('The quick brown fox'), 'quick '), ... %! string ('brown fox')) %!assert_equal (extractAfter (string ('a-b-c'), '-'), string ('b-c')) ## extractAfter: boundary pattern given per element %!assert_equal (extractAfter ( ... %! string ({'The quick brown fox jumps'; ... %! 'over the lazy dog'}), ... %! string ({'quick '; 'the '})), ... %! string ({'brown fox jumps'; 'lazy dog'})) ## extractAfter: a scalar pattern broadcasts over the array %!assert_equal (extractAfter (string ({'a:1', 'a:2'}), 'a:'), string ({'1', ... %! '2'})) ## extractAfter: a pattern that is not found yields a missing value %!test %! str = extractAfter (string ('hello'), 'z'); %! assert_equal (ismissing (str), true); ## extractAfter: position form returns the text after POS %!assert_equal (extractAfter (string ('Edgar Allen Poe'), 12), string ('Poe')) %!assert_equal (extractAfter (string ({'abcd'; 'wxyz'}), ... %! [1; 2]), string ({'bcd'; 'yz'})) ## extractAfter: POS at the last character yields an empty string %!assert_equal (extractAfter (string ('abc'), 3), string ('')) ## extractAfter: missing values are preserved %!test %! str = extractAfter (string ({'a-b', missing}), '-'); %! assert_equal (ismissing (str), [false, true]); %! assert_equal (cellstr (str), {'b', ''}); ## extractAfter: error branches %!error ... %! extractAfter (string ('ab')) %!error ... %! extractAfter (string ('ab'), {1, 2}) %!error ... %! extractAfter (string ('ab'), string ({'a', 'b'})) %!error ... %! extractAfter (string ('ab'), 0) %!error ... %! extractAfter (string ('ab'), 5) ## extractBefore: pattern form returns the text before the first match of PAT %!assert_equal (extractBefore (string ('The quick brown fox'), ' brown'), ... %! string ('The quick')) %!assert_equal (extractBefore (string ('a-b-c'), '-'), string ('a')) ## extractBefore: a scalar pattern broadcasts over the array %!assert_equal (extractBefore (string ({'1:a', '2:a'}), ':a'), string ({'1', ... %! '2'})) ## extractBefore: a pattern that is not found yields a missing value %!test %! str = extractBefore (string ('hello'), 'z'); %! assert_equal (ismissing (str), true); ## extractBefore: position form returns the text before POS %!assert_equal (extractBefore (string ('Edgar Allen Poe'), 6), string ('Edgar')) %!assert_equal (extractBefore (string ({'abcd'; 'wxyz'}), ... %! [3; 2]), string ({'ab'; 'w'})) ## extractBefore: POS at the first character yields an empty string %!assert_equal (extractBefore (string ('abc'), 1), string ('')) ## extractBefore: missing values are preserved %!test %! str = extractBefore (string ({'a-b', missing}), '-'); %! assert_equal (ismissing (str), [false, true]); %! assert_equal (cellstr (str), {'a', ''}); ## extractBefore: error branches %!error ... %! extractBefore (string ('ab')) %!error ... %! extractBefore (string ('ab'), {1, 2}) %!error ... %! extractBefore (string ('ab'), 5) ## extractBetween: pattern form returns the text between boundaries (exclusive) %!assert_equal (extractBetween (string ('The quick brown fox'), 'quick ', ... %! ' fox'), ... %! string ('brown')) ## extractBetween: pattern form with inclusive boundaries keeps the boundaries %!assert_equal (extractBetween (string ('The quick brown fox jumps'), ... %! ' brown', 'fox', 'Boundaries', ... %! 'inclusive'), string (' brown fox')) ## extractBetween: position form returns the span (inclusive by default) %!assert_equal (extractBetween (string ('Edgar Allen Poe'), 7, ... %! 11), string ('Allen')) ## extractBetween: position form with exclusive boundaries drops the endpoints %!assert_equal (extractBetween (string ('small|medium|large'), 6, 13, ... %! 'Boundaries', 'exclusive'), string ('medium')) ## extractBetween: equal positions extract a single character (inclusive) %!assert_equal (extractBetween (string ('abcde'), 3, 3), string ('c')) ## extractBetween: a scalar string with several matches gives a 1xN row %!assert_equal (extractBetween (string ('a[1][2][3]b'), '[', ']'), ... %! string ({'1', '2', '3'})) ## extractBetween: one match per element preserves the size of STR %!assert_equal (extractBetween (string ({''; ''}), '<', '>'), ... %! string ({'a'; 'bb'})) %!assert_equal (extractBetween (string ({'(x)'; '(yy)'}), '(', ')'), ... %! string ({'x'; 'yy'})) ## extractBetween: no match yields an empty string array %!test %! str = extractBetween (string ('hello'), '[', ']'); %! assert_equal (class (str), 'string'); %! assert_equal (size (str), [1, 0]); ## extractBetween: 'Boundaries' name and string option are case-insensitive %!assert_equal (extractBetween (string ('small|medium|large'), 6, 13, ... %! 'boundaries', string ('exclusive')), string ('medium')) ## extractBetween: missing values are preserved (position form) %!test %! str = extractBetween (string ({'abcd', missing}), 2, 3); %! assert_equal (ismissing (str), [false, true]); %! assert_equal (cellstr (str), {'bc', ''}); ## extractBetween: error branches %!error ... %! extractBetween (string ('ab'), 1) %!error ... %! extractBetween (string ('ab'), 1, 'x') %!error ... %! extractBetween (string ('ab'), 'a', 'b', 'Boundaries', 'foo') %!error ... %! extractBetween (string ('ab'), 1, 2, 'Foo', 'bar') %!error ... %! extractBetween (string ('ab'), 1, 2, 'Foo') %!error ... %! extractBetween (string ({'[1]'; '[1][2]'}), '[', ']') %!error ... %! extractBetween (string ('abc'), 2, 9) %!error ... %! extractBetween (string ({'a', 'b', 'c'}), 1, [1, 2]) ## Code-point positions: numeric indexing counts whole characters, not UTF-8 ## bytes. The test text "abc" has 5 code points spanning ## 1-, 3-, and 4-byte UTF-8 sequences (built from raw bytes so this file stays ## ASCII): a(1) euro(2) b(3) emoji(4) c(5). %!test %! s = string (native2unicode ( ... %! uint8 ([97 226 130 172 98 240 159 152 128 99]), ... %! 'UTF-8')); %! euro = string (native2unicode (uint8 ([226 130 172]), 'UTF-8')); %! emoji = string (native2unicode (uint8 ([240 159 152 128]), 'UTF-8')); %! ## extract: the 4th character is the whole emoji, not a stray byte %! assert_equal (extract (s, 4), emoji); %! assert_equal (extract (s, 2), euro); %! ## extractBefore / extractAfter split on character boundaries %! assert_equal (extractBefore (s, 4), append (string ('a'), euro, ... %! string ('b'))); %! assert_equal (extractAfter (s, 2), append (string ('b'), emoji, ... %! string ('c'))); %! ## eraseBetween (inclusive removes chars 2..4; exclusive removes char 3) %! assert_equal (eraseBetween (s, 2, 4), string ('ac')); %! assert_equal (eraseBetween (s, 2, 4, 'Boundaries', 'exclusive'), ... %! append (string ('a'), euro, emoji, string ('c'))); %! ## extractBetween (inclusive keeps chars 2..4; exclusive keeps char 3) %! assert_equal (extractBetween (s, 2, 4), append (euro, string ('b'), emoji)); %! assert_equal (extractBetween (s, 2, 4, 'Boundaries', 'exclusive'), ... %! string ('b')); ## insertAfter: pattern form inserts after the match (single occurrence) %!assert_equal (insertAfter (string ('The quick fox'), 'quick', ' brown'), ... %! string ('The quick brown fox')) ## insertAfter: pattern form inserts after EVERY non-overlapping occurrence %!assert_equal (insertAfter (string ('a-b-c'), '-', 'X'), string ('a-Xb-Xc')) ## insertAfter: boundary pattern and NEW given per element %!assert_equal (insertAfter (string ({'foo'; 'bar'}), string ({'o'; 'r'}), ... %! string ({'1'; '2'})), string ({'fo1o1'; 'bar2'})) ## insertAfter: a scalar pattern and scalar NEW broadcast over the array %!assert_equal (insertAfter (string ({'a:1', 'a:2'}), 'a', '_'), ... %! string ({'a_:1', 'a_:2'})) ## insertAfter: a pattern that is not found leaves the element unchanged %!assert_equal (insertAfter (string ('hello'), 'z', 'X'), string ('hello')) ## insertAfter: NEW preserves trailing whitespace (bare cellstr would deblank) %!assert_equal (insertAfter (string ('ab'), 'a', 'X '), string ('aX b')) ## insertAfter: position form inserts after POS %!assert_equal (insertAfter (string ('abcde'), 3, '_'), string ('abc_de')) %!assert_equal (insertAfter (string ({'abcd'; 'wxyz'}), [1; 2], '_'), ... %! string ({'a_bcd'; 'wx_yz'})) ## insertAfter: POS at the last character appends NEW %!assert_equal (insertAfter (string ('abc'), 3, 'X'), string ('abcX')) ## insertAfter: missing values in STR are preserved %!test %! str = insertAfter (string ({'a-b', missing}), '-', 'X'); %! assert_equal (ismissing (str), [false, true]); %! assert_equal (cellstr (str), {'a-Xb', ''}); ## insertAfter: a missing NEW makes the corresponding element missing %!test %! str = insertAfter (string ('abc'), 'a', string (missing)); %! assert_equal (ismissing (str), true); ## insertAfter: error branches %!error ... %! insertAfter (string ('ab'), 'a') %!error ... %! insertAfter (string ('ab'), {1, 2}, 'x') %!error ... %! insertAfter (string ('ab'), string ({'a', 'b'}), 'x') %!error ... %! insertAfter (string ('ab'), 'a', {1, 2}) %!error ... %! insertAfter (string ({'a', 'b'}), 'x', string ({'1', '2', '3'})) %!error ... %! insertAfter (string ('ab'), 0, 'x') %!error ... %! insertAfter (string ('ab'), 5, 'x') ## insertBefore: pattern form inserts before the match (single occurrence) %!assert_equal (insertBefore (string ('The quick fox'), 'fox', 'brown '), ... %! string ('The quick brown fox')) ## insertBefore: pattern form inserts before EVERY non-overlapping occurrence %!assert_equal (insertBefore (string ('a-b-c'), '-', 'X'), string ('aX-bX-c')) ## insertBefore: a scalar pattern and scalar NEW broadcast over the array %!assert_equal (insertBefore (string ({'cat', 'dog'}), string ({'c', 'd'}), ... %! '_'), ... %! string ({'_cat', '_dog'})) ## insertBefore: a pattern that is not found leaves the element unchanged %!assert_equal (insertBefore (string ('hello'), 'z', 'X'), string ('hello')) ## insertBefore: position form inserts before POS %!assert_equal (insertBefore (string ('abcde'), 3, '_'), string ('ab_cde')) %!assert_equal (insertBefore (string ({'abcd'; 'wxyz'}), [3; 2], '_'), ... %! string ({'ab_cd'; 'w_xyz'})) ## insertBefore: POS at the first character prepends NEW %!assert_equal (insertBefore (string ('abc'), 1, 'X'), string ('Xabc')) ## insertBefore: missing values in STR are preserved %!test %! str = insertBefore (string ({'a-b', missing}), '-', 'X'); %! assert_equal (ismissing (str), [false, true]); %! assert_equal (cellstr (str), {'aX-b', ''}); ## insertBefore: a missing NEW makes the corresponding element missing %!test %! str = insertBefore (string ('abc'), 'c', string (missing)); %! assert_equal (ismissing (str), true); ## insertBefore: error branches %!error ... %! insertBefore (string ('ab'), 'a') %!error ... %! insertBefore (string ('ab'), {1, 2}, 'x') %!error ... %! insertBefore (string ('ab'), 'a', {1, 2}) %!error ... %! insertBefore (string ('ab'), 0, 'x') %!error ... %! insertBefore (string ('ab'), 5, 'x') ## Code-point positions for insertAfter / insertBefore: numeric indexing counts ## whole characters, not UTF-8 bytes (same multibyte text as above). %!test %! s = string (native2unicode ( ... %! uint8 ([97 226 130 172 98 240 159 152 128 99]), ... %! 'UTF-8')); %! euro = string (native2unicode (uint8 ([226 130 172]), 'UTF-8')); %! emoji = string (native2unicode (uint8 ([240 159 152 128]), 'UTF-8')); %! ## insertAfter after the euro (position 2) lands on a character boundary %! assert_equal (insertAfter (s, 2, string ('X')), ... %! append (string ('a'), euro, string ('Xb'), emoji, ... %! string ('c'))); %! ## insertBefore the emoji (position 4) lands on a character boundary %! assert_equal (insertBefore (s, 4, string ('X')), ... %! append (string ('a'), euro, string ('bX'), emoji, ... %! string ('c'))); ## replace: replaces every occurrence of a single substring %!assert_equal (replace (string ('a-b-c'), '-', '_'), string ('a_b_c')) ## replace: a substring that does not occur leaves the text unchanged %!assert_equal (replace (string ('abc'), 'z', 'Y'), string ('abc')) ## replace: applied element-wise across an array, preserving its size %!assert_equal (replace (string ({'a.b', 'c.d'}), '.', '/'), ... %! string ({'a/b', 'c/d'})) ## replace: OLD/NEW size is independent of STR size %!assert_equal (replace (string ('a-b_c'), {'-', '_'}, '/'), string ('a/b/c')) ## replace: a multi-element OLD pairs up with a same-size NEW %!assert_equal (replace (string ('a-b_c'), {'-', '_'}, {'+', '='}), ... %! string ('a+b=c')) ## replace: multiple patterns applied simultaneously (a swap, not sequential) %!assert_equal (replace (string ('ab'), {'a', 'b'}, {'b', 'a'}), string ('ba')) ## replace: at each position the OLD substrings are tried in order, first wins %!assert_equal (replace (string ('aaa'), {'aa', 'a'}, {'X', ... %! 'Y'}), string ('XY')) ## replace: an empty OLD element matches nothing (single and multi-pattern) %!assert_equal (replace (string ('abc'), '', 'X'), string ('abc')) %!assert_equal (replace (string ('abc'), {'', 'b'}, {'X', 'Y'}), string ('aYc')) ## replace: trailing whitespace is preserved in both OLD and NEW %!assert_equal (replace (string ('a b'), 'a ', 'Z'), string ('Zb')) %!assert_equal (replace (string ('ab'), 'a', 'X '), string ('X b')) ## replace: accepts char vectors, cell arrays of char vectors, and string arrays %!assert_equal (replace (string ('a-b'), string ('-'), ... %! string ('_')), string ('a_b')) ## replace: missing values are preserved and carry no text %!test %! str = replace (string ({'a-b', missing}), '-', '_'); %! assert_equal (ismissing (str), [false, true]); %! assert_equal (cellstr (str), {'a_b', ''}); ## replace: error branches %!error ... %! replace (string ('ab'), 'a') %!error ... %! replace (string ('ab'), {1, 2}, 'x') %!error ... %! replace (string ('ab'), 'a', {1, 2}) %!error ... %! replace (string ('ab'), {'a', 'b'}, {'1', '2', '3'}) ## replaceBetween: pattern replaces between boundaries (default exclusive) %!assert_equal (replaceBetween (string ('The quick brown fox'), 'quick ', ... %! ' fox', 'red'), ... %! string ('The quick red fox')) ## replaceBetween: pattern syntax with explicit exclusive boundaries %!assert_equal (replaceBetween (string ('a[x]b'), '[', ']', 'Y', ... %! 'Boundaries', 'exclusive'), ... %! string ('a[Y]b')) ## replaceBetween: pattern syntax with inclusive boundaries replaces them too %!assert_equal (replaceBetween (string ('a[x]b'), '[', ']', 'Y', ... %! 'Boundaries', 'inclusive'), ... %! string ('aYb')) ## replaceBetween: an empty exclusive span inserts NEW between boundaries %!assert_equal (replaceBetween (string ('ab'), 'a', 'b', 'X', 'Boundaries', ... %! 'exclusive'), ... %! string ('aXb')) ## replaceBetween: a scalar boundary broadcasts over the array %!assert_equal (replaceBetween (string ({'[1]', '[2]'}), '[', ']', '#'), ... %! string ({'[#]', '[#]'})) ## replaceBetween: boundaries and NEW given per element %!assert_equal (replaceBetween (string ({'(a)'; ''}), ... %! string ({'('; '<'}), ... %! string ({')'; '>'}), string ({'1'; '2'})), ... %! string ({'(1)'; '<2>'})) ## replaceBetween: an unmatched boundary leaves the element unchanged %!assert_equal (replaceBetween (string ('abc'), '[', ']', 'X'), string ('abc')) ## replaceBetween: position syntax replaces the span (inclusive by default) %!assert_equal (replaceBetween (string ('abcde'), 2, 4, 'X'), string ('aXe')) ## replaceBetween: position syntax with exclusive boundaries keeps the endpoints %!assert_equal (replaceBetween (string ('abcde'), 2, 4, 'X', 'Boundaries', ... %! 'exclusive'), ... %! string ('abXde')) ## replaceBetween: equal positions replace a single character (inclusive) %!assert_equal (replaceBetween (string ('abc'), 2, 2, 'XY'), string ('aXYc')) ## replaceBetween: 'Boundaries' name and string option are case-insensitive %!assert_equal (replaceBetween (string ('a[x]b'), '[', ']', 'Y', ... %! 'boundaries', string ('Inclusive')), ... %! string ('aYb')) ## replaceBetween: missing values in STR are preserved %!test %! str = replaceBetween (string ({'[x]', missing}), '[', ']', 'Y'); %! assert_equal (ismissing (str), [false, true]); %! assert_equal (cellstr (str), {'[Y]', ''}); ## replaceBetween: a missing NEW makes the corresponding element missing %!test %! str = replaceBetween (string ('a[x]b'), '[', ']', string (missing)); %! assert_equal (ismissing (str), true); ## replaceBetween: error branches %!error ... %! replaceBetween (string ('ab'), 'a', 'b') %!error ... %! replaceBetween (string ('ab'), 1, 'x', 'Y') %!error ... %! replaceBetween (string ('a[x]b'), '[', ']', 'Y', 'Boundaries', 'foo') %!error ... %! replaceBetween (string ('ab'), 1, 2, 'Y', 'Foo', 'bar') %!error ... %! replaceBetween (string ('ab'), 1, 2, 'Y', 'Foo') %!error ... %! replaceBetween (string ('abc'), 2, 9, 'Y') %!error ... %! replaceBetween (string ({'a', 'b', 'c'}), 1, [1, 2], 'Y') %!error ... %! replaceBetween (string ({'a', 'b'}), 1, 1, string ({'1', '2', '3'})) %!error ... %! replaceBetween (string ('a[x]b'), '[', ']', {1, 2}) ## Code-point positions for replaceBetween: numeric indexing counts whole ## characters, not UTF-8 bytes (same multibyte text as above): a euro b emoji c. %!test %! s = string (native2unicode ( ... %! uint8 ([97 226 130 172 98 240 159 152 128 99]), ... %! 'UTF-8')); %! euro = string (native2unicode (uint8 ([226 130 172]), 'UTF-8')); %! emoji = string (native2unicode (uint8 ([240 159 152 128]), 'UTF-8')); %! ## inclusive default replaces chars 2..4 (euro,b,emoji) with 'X' %! assert_equal (replaceBetween (s, 2, 4, string ('X')), string ('aXc')); %! ## exclusive keeps chars 2 and 4, replacing char 3 (b) with 'X' %! assert_equal (replaceBetween (s, 2, 4, string ('X'), 'Boundaries', ... %! 'exclusive'), ... %! append (string ('a'), euro, string ('X'), emoji, ... %! string ('c'))); ## reverse: reverses the order of the characters in a string scalar %!assert_equal (reverse (string ('hello')), string ('olleh')) ## reverse: the verbatim MATLAB documentation example %!assert_equal (reverse (string ({'airport', 'control tower', 'radar', ... %! 'runway'})), ... %! string ({'tropria', 'rewot lortnoc', 'radar', 'yawnur'})) ## reverse: a palindrome is returned unchanged %!assert_equal (reverse (string ('radar')), string ('radar')) ## reverse: applied element-wise across an array, preserving its size %!assert_equal (reverse (string ({'ab', 'cd'; 'ef', 'gh'})), ... %! string ({'ba', 'dc'; 'fe', 'hg'})) ## reverse: a column vector keeps its shape %!assert_equal (reverse (string ({'ab'; 'cde'})), string ({'ba'; 'edc'})) ## reverse: output is a string array of the same size as the input %!test %! r = reverse (string ({'ab', 'cd', 'ef'})); %! assert_equal (isa (r, 'string'), true); %! assert_equal (size (r), [1, 3]); ## reverse: a single character is unchanged %!assert_equal (reverse (string ('x')), string ('x')) ## reverse: an empty string stays empty %!assert_equal (reverse (string ('')), string ('')) ## reverse: an empty (0x0) string array is returned unchanged %!test %! r = reverse (string ([])); %! assert_equal (isa (r, 'string'), true); %! assert_equal (size (r), [0, 0]); ## reverse: leading and trailing whitespace is preserved (and moved) %!assert_equal (reverse (string ('ab ')), string (' ba')) %!assert_equal (reverse (string (' ab')), string ('ba ')) %!assert_equal (reverse (string ('a b c')), string ('c b a')) ## reverse: applying reverse twice restores the original %!test %! s = string ({'hello', 'world'; 'foo bar', 'baz'}); %! assert_equal (reverse (reverse (s)), s); ## reverse: missing values are preserved and carry no text %!test %! r = reverse (string ({'ab', missing, 'cd'})); %! assert_equal (ismissing (r), [false, true, false]); %! assert_equal (cellstr (r), {'ba', '', 'dc'}); ## reverse: an all-missing array stays all missing %!test %! r = reverse (string ({missing, missing})); %! assert_equal (ismissing (r), [true, true]); ## reverse: counts whole characters, not UTF-8 bytes (multibyte text ## a euro b emoji c reverses to c emoji b euro a) %!test %! s = string (native2unicode ( ... %! uint8 ([97 226 130 172 98 240 159 152 128 99]), ... %! 'UTF-8')); %! euro = string (native2unicode (uint8 ([226 130 172]), 'UTF-8')); %! emoji = string (native2unicode (uint8 ([240 159 152 128]), 'UTF-8')); %! assert_equal (reverse (s), ... %! append (string ('c'), emoji, string ('b'), euro, ... %! string ('a'))); %! ## reversing twice restores the multibyte original exactly %! assert_equal (reverse (reverse (s)), s); ## sort: a vector is sorted in ascending order by default %!assert_equal (sort (string ({'b', 'c', 'a'})), string ({'a', 'b', 'c'})) ## sort: a column vector keeps its shape %!assert_equal (sort (string ({'b'; 'a'; 'c'})), string ({'a'; 'b'; 'c'})) ## sort: the second output indexes the original elements (B = A(index)) %!test %! a = string ({'b', 'c', 'a'}); %! [b, i] = sort (a); %! assert_equal (i, [3, 1, 2]); %! assert_equal (b, a(i)); ## sort: 'descend' reverses the order %!assert_equal (sort (string ({'b', 'c', 'a'}), 'descend'), ... %! string ({'c', 'b', 'a'})) ## sort: a matrix is sorted column-wise by default %!assert_equal (sort (string ({'d', 'a'; 'b', 'c'})), ... %! string ({'b', 'a'; 'd', 'c'})) ## sort: DIM selects the operating dimension %!assert_equal (sort (string ({'d', 'a'; 'b', 'c'}), 2), ... %! string ({'a', 'd'; 'b', 'c'})) ## sort: DIM and DIRECTION can be combined %!assert_equal (sort (string ({'d', 'a'; 'b', 'c'}), 2, 'descend'), ... %! string ({'d', 'a'; 'c', 'b'})) ## sort: the empty string sorts before any non-empty string %!assert_equal (sort (string ({'b', '', 'a'})), string ({'', 'a', 'b'})) ## sort: a scalar is returned unchanged %!assert_equal (sort (string ('x')), string ('x')) ## sort: ties are stable -- equal elements keep their original order %!test %! [~, i] = sort (string ({'b', 'a', 'b'})); %! assert_equal (i, [2, 1, 3]); ## sort: missing values go last for ascending order ('auto' default) %!test %! r = sort (string ({'b', missing, 'a'})); %! assert_equal (ismissing (r), [false, false, true]); %! assert_equal (cellstr (r), {'a', 'b', ''}); ## sort: missing values go first for descending order ('auto' default) %!test %! r = sort (string ({'b', missing, 'a'}), 'descend'); %! assert_equal (ismissing (r), [true, false, false]); %! assert_equal (cellstr (r), {'', 'b', 'a'}); ## sort: 'MissingPlacement','first' forces missing to the front %!test %! r = sort (string ({'b', missing, 'a'}), 'MissingPlacement', 'first'); %! assert_equal (ismissing (r), [true, false, false]); %! assert_equal (cellstr (r), {'', 'a', 'b'}); ## sort: 'MissingPlacement','last' forces missing to the end (descending) %!test %! r = sort (string ({'b', missing, 'a'}), 'descend', ... %! 'MissingPlacement', 'last'); %! assert_equal (ismissing (r), [false, false, true]); %! assert_equal (cellstr (r), {'b', 'a', ''}); ## sort: error branches %!error ... %! sort (string ('a'), 'MissingPlacement', 'bogus') %!error ... %! sort (string ({'b'; 'a'}), 'Bogus', 1) %!error ... %! sort (string ({'b'; 'a'}), 0) %!error ... %! sort (string ({'b'; 'a'}), {1}) %!error ... %! sort (string ({'b'; 'a'}), 1, 2) %!error ... %! sort (string ({'b'; 'a'}), 'descend', 1) ## sortrows: rows are sorted ascending, ties broken by later columns %!assert_equal (sortrows (string ({'b', 'x'; 'a', 'z'; 'b', 'a'})), ... %! string ({'a', 'z'; 'b', 'a'; 'b', 'x'})) ## sortrows: the second output indexes the original rows (B = A(index,:)) %!test %! a = string ({'b', 'x'; 'a', 'z'; 'b', 'a'}); %! [b, i] = sortrows (a); %! assert_equal (i, [2; 3; 1]); %! assert_equal (b, a(i, :)); ## sortrows: COL selects columns; a negative index sorts that column descending %!assert_equal (sortrows (string ({'b', 'x'; 'a', 'z'; 'b', 'a'}), [1, -2]), ... %! string ({'a', 'z'; 'b', 'x'; 'b', 'a'})) ## sortrows: a single DIRECTION applies to all columns %!assert_equal (sortrows (string ({'b', 'x'; 'a', 'z'; 'b', 'a'}), ... %! 'descend'), ... %! string ({'b', 'x'; 'b', 'a'; 'a', 'z'})) ## sortrows: DIRECTION can be a cell array, one entry per column %!assert_equal (sortrows (string ({'b', 'x'; 'a', 'z'; 'b', 'a'}), ... %! {'ascend', 'descend'}), ... %! string ({'a', 'z'; 'b', 'x'; 'b', 'a'})) ## sortrows: COL and DIRECTION together (the sign of COL is ignored) %!assert_equal (sortrows (string ({'b', 'x'; 'a', 'z'; 'b', 'a'}), ... %! [1, 2], {'ascend', 'descend'}), ... %! string ({'a', 'z'; 'b', 'x'; 'b', 'a'})) ## sortrows: a missing element sends its row last for ascending ('auto') %!test %! a = string ({'b', 'x'; 'a', 'z'}); %! a(1,1) = string (missing); %! r = sortrows (a); %! assert_equal (cellstr (r), {'a', 'z'; '', 'x'}); %! assert_equal (ismissing (r), [false, false; true, false]); ## sortrows: 'MissingPlacement','first' sends the missing row first %!test %! a = string ({'b', 'x'; 'a', 'z'}); %! a(1,1) = string (missing); %! r = sortrows (a, 'MissingPlacement', 'first'); %! assert_equal (cellstr (r), {'', 'x'; 'a', 'z'}); ## sortrows: 'MissingPlacement','last' keeps the missing row last (descending) %!test %! a = string ({'b', 'x'; 'a', 'z'}); %! a(1,1) = string (missing); %! r = sortrows (a, 'descend', 'MissingPlacement', 'last'); %! assert_equal (cellstr (r), {'a', 'z'; '', 'x'}); ## sortrows: error branches %!error ... %! sortrows (string (reshape (1:8, [2, 2, 2]))) %!error ... %! sortrows (string ({'a', 'b'}), 'MissingPlacement', 'bogus') %!error ... %! sortrows (string ({'a', 'b'}), [1.5]) %!error ... %! sortrows (string ({'a', 'b'}), [0]) %!error ... %! sortrows (string ({'a', 'b'}), [3]) %!error ... %! sortrows (string ({'a', 'b'}), {'up', 'down'}) %!error ... %! sortrows (string ({'a', 'b'}), [1, 2], {'ascend', 'descend'}, 'x') %!error ... %! sortrows (string ({'a', 'b'}), {'ascend', 'descend'}, 'ascend') ## split: a scalar splits into an Nx1 column at the delimiter %!assert_equal (split (string ('a-b-c'), '-'), string ({'a'; 'b'; 'c'})) ## split: the default delimiter is whitespace %!assert_equal (split (string ('one two three')), ... %! string ({'one'; 'two'; 'three'})) ## split: delimiters are not collapsed -- consecutive ones yield empty strings %!assert_equal (split (string ('Jones,,,68,175'), ','), ... %! string ({'Jones'; ''; ''; '68'; '175'})) ## split: a trailing delimiter yields a trailing empty string %!assert_equal (split (string ('a-'), '-'), string ({'a'; ''})) ## split: an Mx1 column splits into an MxN array %!assert_equal (split (string ({'a-b'; 'c-d'}), '-'), string ({'a', ... %! 'b'; 'c', 'd'})) ## split: a 1xM row splits into a 1xMxN array %!test %! r = split (string ({'a-b', 'c-d'}), '-'); %! assert_equal (size (r), [1, 2, 2]); %! assert_equal (cellstr (r(1,1,:))(:), {'a'; 'b'}); %! assert_equal (cellstr (r(1,2,:))(:), {'c'; 'd'}); ## split: multiple delimiters are all used (first match wins at a position) %!assert_equal (split (string ('a, b and c'), {', ', ' and '}), ... %! string ({'a'; 'b'; 'c'})) ## split: the second output returns the matched delimiters (one fewer piece) %!test %! [s, m] = split (string ('a, b and c'), {', ', ' and '}); %! assert_equal (s, string ({'a'; 'b'; 'c'})); %! assert_equal (m, string ({', '; ' and '})); ## split: DIM lays the pieces out along the given dimension %!test %! r = split (string ('x.y.z'), '.', 2); %! assert_equal (size (r), [1, 3]); %! assert_equal (cellstr (r), {'x', 'y', 'z'}); ## split: a string with no delimiter match returns itself (single piece) %!assert_equal (split (string ('abc'), '-'), string ('abc')) ## split: missing values are preserved and count as a single piece %!test %! r = split (string (missing), '-'); %! assert_equal (ismissing (r), true); %! assert_equal (size (r), [1, 1]); %!test %! r = split (string ({'ab', missing}), '-'); %! assert_equal (ismissing (r), [false, true]); %! assert_equal (cellstr (r), {'ab', ''}); ## split: error branches %!error ... %! split (string ('a'), '-', 2, 3) %!error ... %! split (string ('a'), {1, 2}) %!error ... %! split (string ('a'), '-', 0) %!error ... %! split (string ({'a-b'; 'c-d'}), '-', 1) %!error ... %! split (string ({'a-b', 'c'}), '-') ## splitlines: a scalar splits into an Nx1 column at line feeds %!assert_equal (splitlines (string (sprintf ("a\nb\nc"))), ... %! string ({'a'; 'b'; 'c'})) ## splitlines: a CR+LF pair is a single boundary (no empty line between) %!assert_equal (splitlines (string (sprintf ("a\r\nb"))), string ({'a'; 'b'})) ## splitlines: a lone carriage return is also a boundary %!assert_equal (splitlines (string (sprintf ("a\rb"))), string ({'a'; 'b'})) ## splitlines: consecutive newlines are not collapsed (empty line between) %!assert_equal (splitlines (string (sprintf ("a\n\nb"))), ... %! string ({'a'; ''; 'b'})) ## splitlines: vertical tab and form feed are boundaries too %!assert_equal (splitlines (string (sprintf ("a\vb\fc"))), ... %! string ({'a'; 'b'; 'c'})) ## splitlines: the Unicode line separator (U+2028) is a boundary %!test %! ls = native2unicode (uint8 ([226, 128, 168]), "UTF-8"); %! assert_equal (splitlines (string (["a", ls, "b"])), string ({'a'; 'b'})); ## splitlines: an Mx1 column splits into an MxN array %!assert_equal (splitlines (string ({sprintf("a\nb"); sprintf("c\nd")})), ... %! string ({'a', 'b'; 'c', 'd'})) ## splitlines: a string with no newline returns itself (single line) %!assert_equal (splitlines (string ('abc')), string ('abc')) ## splitlines: missing values are preserved and count as a single line %!test %! r = splitlines (string ({'ab', missing})); %! assert_equal (ismissing (r), [false, true]); %! assert_equal (cellstr (r), {'ab', ''}); ## splitlines: error branch -- elements must have the same number of lines %!error ... %! splitlines (string ({sprintf("a\nb"), 'c'})) ## strcat: concatenates corresponding elements (verbatim MATLAB example) %!assert_equal (strcat (string ({'John ', 'Mary '}), string ({'Smith', ... %! 'Jones'})), ... %! string ({'John Smith', 'Mary Jones'})) ## strcat: string inputs preserve trailing whitespace (unlike char strcat) %!assert_equal (strcat (string ('a '), string ('b')), string ('a b')) ## strcat: a scalar argument expands, and char/cellstr may be mixed in %!assert_equal (strcat (string ({'x', 'y'}), '_', string ('Z')), ... %! string ({'x_Z', 'y_Z'})) %!assert_equal (strcat (string ({'a', 'b'}), {'1', '2'}), string ({'a1', 'b2'})) ## strcat: a single argument is returned as a string %!assert_equal (strcat (string ('hi')), string ('hi')) ## strcat: output is a string array %!test %! r = strcat (string ({'a', 'b'}), string ('!')); %! assert_equal (isa (r, 'string'), true); %! assert_equal (cellstr (r), {'a!', 'b!'}); ## strcat: error branch %!error ... %! strcat (string ('a'), 5) ## strip: removes leading and trailing whitespace from both sides by default %!assert_equal (strip (string (' Ann Marie ')), string ('Ann Marie')) ## strip: 'right' removes trailing whitespace only %!assert_equal (strip (string (' Ann Marie '), ... %! 'right'), string (' Ann Marie')) ## strip: 'left' removes leading whitespace only %!assert_equal (strip (string (' Ann Marie '), ... %! 'left'), string ('Ann Marie ')) ## strip: a custom strip character on a given side (verbatim MATLAB example) %!assert_equal (strip (string ('0095.36'), 'left', '0'), string ('95.36')) ## strip: a single-character second argument is the strip character %!assert_equal (strip (string ('xxhixx'), 'x'), string ('hi')) ## strip: 'both' with an explicit strip character %!assert_equal (strip (string ('--a--'), 'both', '-'), string ('a')) ## strip: the default whitespace set includes tab and newline %!assert_equal (strip (string (sprintf ("\t a \n"))), string ('a')) ## strip: an all-whitespace string becomes empty %!assert_equal (strip (string (' ')), string ('')) ## strip: an empty string stays empty %!assert_equal (strip (string ('')), string ('')) ## strip: applied element-wise across an array, preserving its size %!assert_equal (strip (string ({' a ', ' b '})), string ({'a', 'b'})) ## strip: a non-breaking space (char 160) is not treated as whitespace %!test %! nbsp = native2unicode (uint8 ([194, 160]), "UTF-8"); %! s = string ([nbsp, 'a', nbsp]); %! assert_equal (strip (s), s); ## strip: a multibyte strip character is matched as a whole code point %!test %! euro = native2unicode (uint8 ([226, 130, 172]), "UTF-8"); %! assert_equal (strip (string ([euro, euro, 'a', euro]), euro), string ('a')); ## strip: missing values are preserved and carry no text %!test %! r = strip (string ({' a ', missing})); %! assert_equal (ismissing (r), [false, true]); %! assert_equal (cellstr (r), {'a', ''}); ## strip: error branches %!error ... %! strip (string ('a'), 'left', '-', 'extra') %!error ... %! strip (string ('a'), 'middle') %!error ... %! strip (string ('a'), 'left', 'xy') ## pad: by default pads with trailing spaces to the longest element %!assert_equal (pad (string ({'a'; 'bbb'; 'cc'})), ... %! string ({'a '; 'bbb'; 'cc '})) ## pad: a single element is returned unchanged (it is the longest) %!assert_equal (pad (string ('abc')), string ('abc')) ## pad: NCHARS gives an explicit width, padding on the right by default %!assert_equal (pad (string ('ab'), 5), string ('ab ')) ## pad: 'left' adds the padding before the text %!assert_equal (pad (string ('ab'), 5, 'left'), string (' ab')) ## pad: 'both' splits the padding, putting an odd extra on the right %!assert_equal (pad (string ('ab'), 5, 'both'), string (' ab ')) %!assert_equal (pad (string ('ab'), 6, 'both'), string (' ab ')) ## pad: a custom pad character on a given side %!assert_equal (pad (string ('7'), 4, 'left', '0'), string ('0007')) ## pad: a single-character second argument is the pad character (to longest) %!assert_equal (pad (string ({'a'; 'bbb'}), '*'), string ({'a**'; 'bbb'})) ## pad: a SIDE-only second argument pads to the longest element %!assert_equal (pad (string ({'a'; 'bbb'}), 'left'), string ({' a'; 'bbb'})) ## pad: elements longer than NCHARS are not truncated %!assert_equal (pad (string ('abcdef'), 3), string ('abcdef')) ## pad: width is measured in characters (code points), not bytes %!test %! euro = native2unicode (uint8 ([226, 130, 172]), "UTF-8"); %! assert_equal (pad (string (euro), 3), string ([euro, ' '])); ## pad: missing values are preserved and ignored when finding the longest %!test %! r = pad (string ({'a', missing, 'ccc'})); %! assert_equal (ismissing (r), [false, true, false]); %! assert_equal (cellstr (r), {'a ', '', 'ccc'}); ## pad: error branches %!error ... %! pad (string ('a'), 1, 'left', '-', 'extra') %!error ... %! pad (string ('a'), -1) %!error ... %! pad (string ('a'), 'middle') %!error ... %! pad (string ('a'), 5, 'left', 'xy') ## join: combines along the last non-singleton dimension with a space (verbatim) %!assert_equal (join (string ({'Carlos', 'Sada'; 'Ella', 'Olsen'; 'Diana', ... %! 'Lee'})), ... %! string ({'Carlos Sada'; 'Ella Olsen'; 'Diana Lee'})) ## join: a single custom delimiter is placed between all elements (verbatim) %!assert_equal (join (string ({'x', 'y', 'z'; 'a', 'b', 'c'}), '-'), ... %! string ({'x-y-z'; 'a-b-c'})) ## join: an array of delimiters goes between consecutive elements (verbatim) %!assert_equal (join (string ({'x', 'y', 'z'; 'a', 'b', 'c'}), ... %! string ({' + ', ' = '; ' - ', ' = '})), ... %! string ({'x + y = z'; 'a - b = c'})) ## join: DIM selects the joined dimension (verbatim example, dim 1) %!assert_equal (join (string ({'Carlos', 'Sada'; 'Ella', 'Olsen'; 'Diana', ... %! 'Lee'}), 1), ... %! string ({'Carlos Ella Diana', 'Sada Olsen Lee'})) ## join: a row or column vector collapses to a string scalar %!assert_equal (join (string ({'a', 'b', 'c'})), string ('a b c')) %!assert_equal (join (string ({'a'; 'b'; 'c'})), string ('a b c')) ## join: a string scalar is returned unchanged %!assert_equal (join (string ('solo')), string ('solo')) ## join: a delimiter and a dimension together %!assert_equal (join (string ({'a', 'b'; 'c', 'd'}), '-', 1), ... %! string ({'a-c', 'b-d'})) ## join: join is the inverse of split for a single delimiter %!assert_equal (join (split (string ('a-b-c'), '-'), '-'), string ('a-b-c')) ## join: a missing element (or delimiter) propagates to the joined result %!test %! r = join (string ({'a', 'b'; 'c', missing})); %! assert_equal (ismissing (r), [false; true]); %! assert_equal (cellstr (r(1)), {'a b'}); ## join: error branches %!error ... %! join (string ('a'), '-', 2, 3) %!error ... %! join (string ('a'), {1, 2}) %!error ... %! join (string ({'a', 'b'}), string ({'-', '-'})) %!error ... %! join (string ('a'), '-', 0) ## plus: appends STR2 to STR1, equivalent to the '+' operator %!assert_equal (string ('foo') + string ('bar'), string ('foobar')) %!assert_equal (plus (string ('foo'), string ('bar')), string ('foobar')) ## plus: applied element-wise across equal-sized arrays %!assert_equal (string ({'a', 'b'}) + string ({'1', '2'}), string ({'a1', ... %! 'b2'})) ## plus: a scalar operand broadcasts over the array %!assert_equal (string ({'a', 'b'}) + string ('!'), string ({'a!', 'b!'})) ## plus: a missing value propagates (any missing operand -> missing) %!test %! r = string ({'a', missing}) + string ('X'); %! assert_equal (ismissing (r), [false, true]); %! assert_equal (cellstr (r), {'aX', ''}); ## plus: error branch -- both operands must be string arrays %!error ... %! string ('a') + 5 ## lower: converts uppercase characters to lowercase (verbatim MATLAB example) %!assert_equal (lower (string ('The SOONER,')), string ('the sooner,')) ## lower: digits, punctuation, and already-lowercase letters are unchanged %!assert_equal (lower (string ('aB3!Cd')), string ('ab3!cd')) ## lower: applied element-wise across an array, preserving its size %!assert_equal (lower (string ({'AB', 'Cd'; 'eF', 'GH'})), ... %! string ({'ab', 'cd'; 'ef', 'gh'})) ## lower: an empty string stays empty %!assert_equal (lower (string ('')), string ('')) ## lower: missing values are preserved and carry no text %!test %! r = lower (string ({'AB', missing})); %! assert_equal (ismissing (r), [false, true]); %! assert_equal (cellstr (r), {'ab', ''}); ## upper: converts lowercase characters to uppercase (verbatim MATLAB example) %!assert_equal (upper (string ('Hello, World.')), string ('HELLO, WORLD.')) ## upper: digits, punctuation, and already-uppercase letters are unchanged %!assert_equal (upper (string ('aB3!cD')), string ('AB3!CD')) ## upper: applied element-wise across an array, preserving its size %!assert_equal (upper (string ({'ab', 'cD'; 'Ef', 'gh'})), ... %! string ({'AB', 'CD'; 'EF', 'GH'})) ## upper: an empty string stays empty %!assert_equal (upper (string ('')), string ('')) ## upper: missing values are preserved and carry no text %!test %! r = upper (string ({'ab', missing})); %! assert_equal (ismissing (r), [false, true]); %! assert_equal (cellstr (r), {'AB', ''}); ## unique: returns sorted unique elements with index vectors B=A(ixA), A=B(ixB) %!test %! A = string ({'b', 'a', 'b', 'c', 'a'}); %! [B, ixA, ixB] = unique (A); %! assert_equal (B, string ({'a', 'b', 'c'})); %! assert_equal (ixA, [2; 1; 4]); %! assert_equal (ixB, [2; 1; 2; 3; 1]); ## unique: 'stable' keeps first-seen order %!assert_equal (unique (string ({'b', 'a', 'b', 'c', 'a'}), 'stable'), ... %! string ({'b', 'a', 'c'})) ## unique: 'first' and 'last' select which occurrence the index points to %!test %! [~, iaF] = unique (string ({'b', 'a', 'b', 'c', 'a'}), 'first'); %! [~, iaL] = unique (string ({'b', 'a', 'b', 'c', 'a'}), 'last'); %! assert_equal (iaF, [2; 1; 4]); %! assert_equal (iaL, [5; 3; 4]); ## unique: 'rows' returns the unique rows of a 2-D string matrix %!assert_equal (unique (string ({'a', 'b'; 'a', 'b'; 'c', 'd'}), 'rows'), ... %! string ({'a', 'b'; 'c', 'd'})) ## unique: missing values are not de-duplicated and are appended at the end %!test %! B = unique (string ({'b', missing, 'a', missing})); %! assert_equal (ismissing (B), [false, false, true, true]); %! assert_equal (cellstr (B(~ismissing (B))), {'a', 'b'}); ## unique: error branches %!error ... %! unique (string ('a'), 'bogus') %!error ... %! unique (reshape (string ({'a','b','c','d','e','f','g','h'}), 2, 2, 2), ... %! 'rows') ################################################################################ ## ** Array Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'cat' 'horzcat' 'vertcat' 'repmat' ## ## 'repelem' 'repelems' 'reshape' 'circshift' ## ## 'permute' 'ipermute' 'transpose' 'ctranspose' ## ## ## ################################################################################ ## cat: concatenates string arrays along the first dimension (rows) %!assert_equal (cat (1, string ({'a', 'b'}), string ({'c', 'd'})), ... %! string ({'a', 'b'; 'c', 'd'})) ## cat: concatenates string arrays along the second dimension (columns) %!assert_equal (cat (2, string ({'a'; 'b'}), string ({'c'; 'd'})), ... %! string ({'a', 'c'; 'b', 'd'})) ## cat: a higher dimension stacks the inputs into an N-D array %!test %! c = cat (3, string ({'a', 'b'}), string ({'c', 'd'})); %! assert_equal (size (c), [1, 2, 2]); %! assert_equal (cellstr (c(:,:,1)), {'a', 'b'}); %! assert_equal (cellstr (c(:,:,2)), {'c', 'd'}); ## cat: accepts more than two inputs %!assert_equal (cat (2, string ('a'), string ('b'), string ('c')), ... %! string ({'a', 'b', 'c'})) ## cat: converts char, cellstr, numeric, and logical arguments to string %!assert_equal (cat (2, string ('x'), 'yy', {'z'}), string ({'x', 'yy', 'z'})) %!assert_equal (cat (2, string ('n'), 5, true, false), ... %! string ({'n', '5', 'true', 'false'})) ## cat: numeric arrays keep their shape when converted %!assert_equal (cat (1, string ({'a'; 'b'}), [1; 2]), ... %! string ({'a'; 'b'; '1'; '2'})) ## cat: a char matrix contributes one element per row, preserving trailing ## whitespace (unlike the core 'cellstr', which would deblank it) %!assert_equal (cat (1, string ({'1'; '2'}), char ('a', 'bb')), ... %! string ({'1'; '2'; 'a '; 'bb'})) ## cat: an empty argument drops out of the concatenation %!assert_equal (cat (2, string ('a'), string ([])), string ('a')) %!assert_equal (cat (1, string ([]), string ({'a'; 'b'})), string ({'a'; 'b'})) ## cat: a single argument is returned unchanged %!assert_equal (cat (2, string ({'a'; 'b'})), string ({'a'; 'b'})) ## cat: missing values are carried through positionally %!test %! c = cat (2, string ('a'), string (NaN), string ('b')); %! assert_equal (ismissing (c), [false, true, false]); %! assert_equal (cellstr (c), {'a', '', 'b'}); ## cat: a categorical, datetime, or duration argument promotes the result to ## that class (the first input is forwarded to its constructor) %!assert_equal (class (cat (2, string ('a'), categorical ({'b'}))), ... %! 'categorical') %!assert_equal (class (cat (2, string ('2020-01-01'), ... %! datetime (2021, 1, 1))), 'datetime') %!assert_equal (class (cat (2, string ('1:00:00'), duration (2, 0, 0))), ... %! 'duration') ## horzcat: '[A, B]' equals 'horzcat (A, B)' equals 'cat (2, A, B)' %!assert_equal ([string({'a', 'b'}), string({'c', 'd'})], ... %! horzcat (string ({'a', 'b'}), string ({'c', 'd'}))) %!assert_equal (horzcat (string ({'a', 'b'}), string ({'c', 'd'})), ... %! cat (2, string ({'a', 'b'}), string ({'c', 'd'}))) ## horzcat: converts and concatenates non-string arguments %!assert_equal ([string('x'), 'yy', {'z'}, 5, true], ... %! string ({'x', 'yy', 'z', '5', 'true'})) ## horzcat: missing values are carried through positionally %!test %! h = [string('a'), string(NaN)]; %! assert_equal (ismissing (h), [false, true]); %! assert_equal (cellstr (h), {'a', ''}); ## vertcat: '[A; B]' equals 'vertcat (A, B)' equals 'cat (1, A, B)' %!assert_equal ([string({'a', 'b'}); string({'c', 'd'})], ... %! vertcat (string ({'a', 'b'}), string ({'c', 'd'}))) %!assert_equal (vertcat (string ({'a', 'b'}), string ({'c', 'd'})), ... %! cat (1, string ({'a', 'b'}), string ({'c', 'd'}))) ## vertcat: converts and concatenates non-string arguments %!assert_equal ([string('x'); 'yy'; {'z'}; 5; true], ... %! string ({'x'; 'yy'; 'z'; '5'; 'true'})) ## vertcat: missing values are carried through positionally %!test %! v = [string('a'); string(NaN)]; %! assert_equal (ismissing (v), [false; true]); %! assert_equal (cellstr (v), {'a'; ''}); ## repmat: a scalar count tiles the array along every dimension %!assert_equal (repmat (string ({'a', 'b'}), 2), ... %! string ({'a', 'b', 'a', 'b'; 'a', 'b', 'a', 'b'})) ## repmat: separate row and column counts %!assert_equal (repmat (string ('x'), 2, 3), ... %! string ({'x', 'x', 'x'; 'x', 'x', 'x'})) ## repmat: a dimension vector is equivalent to the scalar-list form %!assert_equal (repmat (string ('x'), [2, 3]), repmat (string ('x'), 2, 3)) ## repmat: a zero count yields an empty string array %!assert_equal (size (repmat (string ('a'), 0)), [0, 0]) %!assert_equal (class (repmat (string ('a'), 0)), 'string') ## repmat: missing values are preserved %!test %! r = repmat (string (NaN), 1, 3); %! assert_equal (ismissing (r), [true, true, true]); %! assert_equal (size (r), [1, 3]); ## repelem: a scalar count repeats each element that many times %!assert_equal (repelem (string ({'a', 'b'}), 2), ... %! string ({'a', 'a', 'b', 'b'})) ## repelem: a count vector repeats each element by its own count %!assert_equal (repelem (string ({'a', 'b', 'c'}), [1, 2, 3]), ... %! string ({'a', 'b', 'b', 'c', 'c', 'c'})) ## repelem: a zero count drops the corresponding element %!assert_equal (repelem (string ({'a', 'b'}), [0, 2]), string ({'b', 'b'})) ## repelem: separate per-dimension counts repeat a matrix block-wise %!test %! r = repelem (string ({'a', 'b'; 'c', 'd'}), 2, 3); %! assert_equal (size (r), [4, 6]); %! assert_equal (cellstr (r(1,:)), {'a', 'a', 'a', 'b', 'b', 'b'}); %! assert_equal (cellstr (r(3,:)), {'c', 'c', 'c', 'd', 'd', 'd'}); ## repelem: missing values are repeated like any other element %!test %! r = repelem (string ({'a', missing}), 2); %! assert_equal (ismissing (r), [false, false, true, true]); %! assert_equal (cellstr (r), {'a', 'a', '', ''}); ## repelems: the first row of R indexes elements, the second row repeats them %!assert_equal (repelems (string ({'a', 'b', 'c'}), [1, 3; 2, 2]), ... %! string ({'a', 'a', 'c', 'c'})) ## repelems: a single column selects and repeats one element %!assert_equal (repelems (string ({'x', 'y', 'z'}), [2; 3]), ... %! string ({'y', 'y', 'y'})) ## repelems: a selected missing element stays missing %!test %! r = repelems (string ({'a', missing, 'c'}), [2, 3; 1, 2]); %! assert_equal (ismissing (r), [true, false, false]); %! assert_equal (cellstr (r), {'', 'c', 'c'}); ## reshape: dimensions are filled column-major %!assert_equal (reshape (string ({'a', 'b', 'c', 'd', 'e', 'f'}), 2, 3), ... %! string ({'a', 'c', 'e'; 'b', 'd', 'f'})) ## reshape: an empty dimension '[]' is inferred from the element count %!assert_equal (reshape (string ({'a', 'b', 'c', 'd', 'e', 'f'}), 2, []), ... %! reshape (string ({'a', 'b', 'c', 'd', 'e', 'f'}), 2, 3)) %!assert_equal (reshape (string ({'a', 'b', 'c', 'd', 'e', 'f'}), [], 3), ... %! reshape (string ({'a', 'b', 'c', 'd', 'e', 'f'}), 2, 3)) ## reshape: a dimension vector is equivalent to the scalar-list form %!assert_equal (reshape (string ({'a', 'b', 'c', 'd', 'e', 'f'}), [3, 2]), ... %! reshape (string ({'a', 'b', 'c', 'd', 'e', 'f'}), 3, 2)) ## reshape: missing values follow their elements column-major %!test %! r = reshape (string ({'a', missing, 'c', missing}), 2, 2); %! assert_equal (ismissing (r), [false, false; true, true]); ## circshift: a positive scalar shifts along the first non-singleton dimension %!assert_equal (circshift (string ({'a', 'b', 'c', 'd'}), 1), ... %! string ({'d', 'a', 'b', 'c'})) ## circshift: a negative shift moves elements the other way %!assert_equal (circshift (string ({'a', 'b', 'c', 'd'}), -1), ... %! string ({'b', 'c', 'd', 'a'})) ## circshift: a trailing DIM argument selects the dimension to shift %!assert_equal (circshift (string ({'a', 'b'; 'c', 'd'}), 1, 2), ... %! string ({'b', 'a'; 'd', 'c'})) ## circshift: a vector shift applies one value per dimension %!assert_equal (circshift (string ({'a', 'b'; 'c', 'd'}), [1, 1]), ... %! string ({'d', 'c'; 'b', 'a'})) ## circshift: missing values are shifted like any other element %!test %! r = circshift (string ({'a', missing, 'c'}), 1); %! assert_equal (ismissing (r), [false, false, true]); %! assert_equal (cellstr (r), {'c', 'a', ''}); ## permute: rearranges the dimensions (here equivalent to a transpose) %!assert_equal (permute (string ({'a', 'b'; 'c', 'd'}), [2, 1]), ... %! string ({'a', 'c'; 'b', 'd'})) ## permute: missing values move with their elements %!test %! p = permute (string ({'a', missing; 'c', 'd'}), [2, 1]); %! assert_equal (ismissing (p), [false, false; true, false]); ## ipermute: inverts permute, so the round-trip returns the original array %!test %! A = string ({'a', 'b', 'c'; 'd', 'e', 'f'}); %! assert_equal (ipermute (permute (A, [2, 1]), [2, 1]), A); ## ipermute: a 2-element permutation is its own inverse (a transpose) %!assert_equal (ipermute (string ({'a', 'b'; 'c', 'd'}), [2, 1]), ... %! string ({'a', 'c'; 'b', 'd'})) ## transpose: turns a row into a column and vice versa %!assert_equal (transpose (string ({'a', 'b', 'c'})), string ({'a'; 'b'; 'c'})) ## transpose: flips a matrix across its diagonal %!assert_equal (transpose (string ({'a', 'b'; 'c', 'd'})), ... %! string ({'a', 'c'; 'b', 'd'})) ## transpose: the '.'' operator dispatches to this method %!assert_equal (string ({'a', 'b'}).', string ({'a'; 'b'})) ## transpose: missing values are preserved %!test %! t = transpose (string ({'a', missing})); %! assert_equal (ismissing (t), [false; true]); %! assert_equal (size (t), [2, 1]); ## ctranspose: identical to transpose for string arrays %!assert_equal (ctranspose (string ({'a', 'b', 'c'})), string ({'a'; 'b'; 'c'})) %!assert_equal (ctranspose (string ({'a', 'b'; 'c', 'd'})), ... %! transpose (string ({'a', 'b'; 'c', 'd'}))) ## ctranspose: the ''' operator dispatches to this method %!assert_equal (string ({'a', 'b'})', string ({'a'; 'b'})) ################################################################################ ## ** Reference and Assignment Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'end' 'subsref' 'subsasgn' ## ## ## ################################################################################ ## end: indexes the last element of a vector (and counts back from it) %!test %! a = string ({'a', 'b', 'c', 'd'}); %! assert_equal (a(end), string ('d')); %! assert_equal (a(end-1), string ('c')); ## end: resolves per-dimension and linearly within a matrix %!test %! m = string ({'a', 'b', 'c'; 'd', 'e', 'f'}); %! assert_equal (m(end, end), string ('f')); %! assert_equal (m(end), string ('f')); %! assert_equal (m(1, end), string ('c')); ## subsref: '()' returns a string sub-array (class and contents preserved) %!test %! a = string ({'a', 'b', 'c', 'd'}); %! assert_equal (class (a(2:3)), 'string'); %! assert_equal (a(2:3), string ({'b', 'c'})); ## subsref: '()' carries the missing mask along with the elements %!test %! a = string ({'a', missing, 'c'}); %! r = a([1, 2]); %! assert_equal (ismissing (r), [false, true]); %! assert_equal (cellstr (r), {'a', ''}); ## subsref: '{}' returns the backing cell-array slice (a cell, not a string) %!test %! a = string ({'a', 'b', 'c', 'd'}); %! v = a{2}; %! assert_equal (class (v), 'cell'); %! assert_equal (v, {'b'}); ## subsref: chained '()' references resolve left to right %!assert_equal (string ({'aa', 'bb', 'cc'})([1, 3])(2), string ('cc')) ## subsref: '.' indexing for reference is an error %!error ... %! (string ({'a', 'b'}).foo) ## subsasgn: '()' assignment from another string replaces the elements %!test %! a = string ({'a', 'b', 'c', 'd'}); %! a(2) = string ('Z'); %! assert_equal (a, string ({'a', 'Z', 'c', 'd'})); ## subsasgn: '()' converts a char, cellstr, or numeric right-hand side %!test %! a = string ({'a', 'b', 'c'}); %! a(1) = 'Y'; %! a(2) = {'CC'}; %! a(3) = 5; %! assert_equal (a, string ({'Y', 'CC', '5'})); ## subsasgn: '()' assignment of a missing value sets the missing mask %!test %! a = string ({'a', 'b', 'c'}); %! a(2) = string (NaN); %! assert_equal (ismissing (a), [false, true, false]); %! assert_equal (cellstr (a), {'a', '', 'c'}); ## subsasgn: '()' assignment past the end grows the array, filling the gap ## elements with (MATLAB-confirmed, forum probe) %!test %! a = string ({'a', 'b'}); %! a(4) = string ('x'); %! assert_equal (size (a), [1, 4]); %! assert_equal (ismissing (a), [false, false, true, false]); %! assert_equal (cellstr (a), {'a', 'b', '', 'x'}); ## subsasgn: growing with a char-vector right-hand side also fills %!test %! b = string ({'a', 'b', 'c'}); %! b(5) = 'z'; %! assert_equal (ismissing (b), [false, false, false, true, false]); %! assert_equal (cellstr (b), {'a', 'b', 'c', '', 'z'}); ## subsasgn: a two-dimensional grow fills every new cell with %!test %! c = string ({'a', 'b'; 'c', 'd'}); %! c(3, 3) = string ('z'); %! assert_equal (size (c), [3, 3]); %! assert_equal (ismissing (c), ... %! [false, false, true; false, false, true; true, true, false]); ## subsasgn: '()' assignment of '[]' deletes the indexed elements %!test %! a = string ({'a', 'b', 'c', 'd'}); %! a(2) = []; %! assert_equal (a, string ({'a', 'c', 'd'})); ## subsasgn: '{}' assigns a single element from a character vector %!test %! a = string ({'a', 'b', 'c'}); %! a{2} = 'QQ'; %! assert_equal (a, string ({'a', 'QQ', 'c'})); ## subsasgn: '{}' assignment clears a previously-missing element %!test %! a = string ({'a', missing}); %! a{2} = 'now'; %! assert_equal (ismissing (a), [false, false]); %! assert_equal (a, string ({'a', 'now'})); ## subsasgn: '{}' requires a character-vector right-hand side %!error ... %! (subsasgn (string ({'a', 'b'}), substruct ('{}', {2}), 5)) ## subsasgn: '{}' is restricted to a single (scalar) element %!error ... %! (subsasgn (string ({'a', 'b'}), substruct ('{}', {[1, 2]}), 'x')) ## subsasgn: '.' indexing for assignment is an error %!error ... %! (subsasgn (string ({'a', 'b'}), substruct ('.', 'foo'), 1)) ################################################################################ ## ** Overloaded methods for duration and categorical classes ** ## ################################################################################ ## Available Methods ## ## ## ## 'colon' 'linspace' 'intersect' 'setdiff' ## ## 'setxor' 'union' 'isequal' 'isequaln' ## ## ## ################################################################################ ## colon: a duration operand makes the string range delegate to duration %!assert_equal (class (string ('1:00:00'):hours (5)), 'duration') ## colon: without a duration (or categorical) operand it is an error %!error ... %! colon (string ('a'), string ('b')) ## linspace: a duration endpoint makes the result a duration array %!assert_equal (class (linspace (string ('0:00:00'), hours (5), 6)), 'duration') ## linspace: a non-duration second argument is an error %!error ... %! linspace (string ('a'), string ('b'), 5) ## intersect: with a categorical it returns the common elements (categorical) %!test %! C = intersect (string ({'a', 'b', 'c'}), categorical ({'b', 'c', 'd'})); %! assert_equal (class (C), 'categorical'); %! assert_equal (cellstr (C), {'b', 'c'}); ## intersect: a non-duration, non-categorical second argument is an error %!error ... %! intersect (string ({'a'}), string ({'b'})) ## setdiff: with a categorical it returns the elements of A that are not in B %!test %! C = setdiff (string ({'a', 'b', 'c'}), categorical ({'b'})); %! assert_equal (class (C), 'categorical'); %! assert_equal (cellstr (C), {'a', 'c'}); ## setdiff: a non-duration, non-categorical second argument is an error %!error ... %! setdiff (string ({'a'}), string ({'b'})) ## setxor: with a categorical it returns the symmetric difference %!test %! C = setxor (string ({'a', 'b'}), categorical ({'b', 'c'})); %! assert_equal (class (C), 'categorical'); %! assert_equal (cellstr (C), {'a', 'c'}); ## setxor: a non-duration, non-categorical second argument is an error %!error ... %! setxor (string ({'a'}), string ({'b'})) ## union: with a categorical it returns the combined unique elements %!test %! C = union (string ({'a', 'b'}), categorical ({'b', 'c'})); %! assert_equal (class (C), 'categorical'); %! assert_equal (cellstr (C), {'a', 'b', 'c'}); ## union: a non-duration, non-categorical second argument is an error %!error ... %! union (string ({'a'}), string ({'b'})) ## isequal: true for equal string arrays, false otherwise %!assert_equal (isequal (string ({'a', 'b'}), string ({'a', 'b'})), true) %!assert_equal (isequal (string ({'a', 'b'}), string ({'a', 'c'})), false) ## isequal: a missing element is never equal, even to another missing %!assert_equal (isequal (string ({'a', missing}), ... %! string ({'a', missing})), false) ## isequal: accepts more than two string arrays %!assert_equal (isequal (string ('a'), string ('a'), string ('a')), true) ## isequal: a categorical operand delegates to categorical isequal %!assert_equal (isequal (string ({'a', 'b'}), categorical ({'a', 'b'})), true) ## isequal: text in any form is comparable %!assert_equal (isequal (string ('a'), 'a'), true) %!assert_equal (isequal ('a', string ('a')), true) %!assert_equal (isequal (string ('a'), 'ab'), false) %!assert_equal (isequal (string ('a'), {'a'}), true) %!assert_equal (isequal (string ({'a', 'b'}), {'a', 'b'}), true) %!assert_equal (isequal (string (''), ''), true) ## isequal: an operand that is not text is false, not an error %!assert_equal (isequal (string ({'a'}), 5), false) ## isequaln: like isequal but treats same-position missing values as equal %!assert_equal (isequaln (string ({'a', missing}), ... %! string ({'a', missing})), true) %!assert_equal (isequaln (string ({'a', missing}), string ({'a', 'b'})), false) ## isequaln: true for equal string arrays %!assert_equal (isequaln (string ({'a', 'b'}), string ({'a', 'b'})), true) ## isequaln: a categorical operand delegates to categorical isequaln %!assert_equal (isequaln (string ({'a', 'b'}), categorical ({'a', 'b'})), true) ## isequaln: text in any form is comparable %!assert_equal (isequaln (string ('a'), 'a'), true) %!assert_equal (isequaln (string ('a'), {'a'}), true) ## isequaln: an operand that is not text is false, not an error %!assert_equal (isequaln (string ({'a'}), 5), false) ## empty: a 0-by-0 empty string array by default %!test %! e = string.empty; %! assert_equal (class (e), 'string'); %! assert_equal (size (e), [0, 0]); %! assert_equal (isempty (e), true); ## empty: a requested size, which may have any one dimension zero %!assert_equal (size (string.empty (0, 3)), [0, 3]) %!assert_equal (size (string.empty (3, 0)), [3, 0]) %!assert_equal (size (string.empty ([0, 3])), [0, 3]) %!assert_equal (size (string.empty (2, 0, 3)), [2, 0, 3]) ## empty: a lone dimension gives a square size, so only zero is an empty one %!assert_equal (size (string.empty (0)), [0, 0]) %!error ... %! string.empty (3) ## empty: a negative dimension counts as zero, as it does for 'zeros', and a ## size vector holding no dimensions names no size %!assert_equal (size (string.empty (-1)), [0, 0]) %!assert_equal (size (string.empty (0, -1)), [0, 0]) %!assert_equal (size (string.empty (-2, -3)), [0, 0]) %!assert_equal (size (string.empty ([])), [0, 0]) %!error ... %! string.empty (1.5) ## empty: the result behaves as an empty string array elsewhere %!assert_equal (size ([string('a'), string.empty]), [1, 1]) %!assert_equal (size (ismissing (string.empty)), [0, 0]) %!assert_equal (size (cellstr (string.empty)), [0, 0]) ## Test 'sort' -- option VALUES are matched without regard to case %!assert_equal (cellstr (sort (string ({'b', 'a', 'c'}), 'Descend')), ... %! {'c', 'b', 'a'}) %!assert_equal (cellstr (sort (string ({'b', 'a', 'c'}), 'ASCEND')), ... %! {'a', 'b', 'c'}) ## the shared '__ismember__' helper validates its options under its own name %!error ... %! ismember (string ('a'), string ('a'), 5); %!error ... %! ismember (string ('a'), string ('a'), 'nope'); ## an undefined categorical element becomes a missing string carrying no text %!test %! s = string (categorical ({'a'; ''; 'b'})); %! assert_equal (ismissing (s), [false; true; false]); %! assert_equal (cellstr (s), {'a'; ''; 'b'}); %! assert_equal (char (s(2)), ''); %! assert_equal (strlength (s), [1; NaN; 1]); ## an undefined categorical element is not the text '' %!assert_equal (string (categorical ({''})) == "", false) ## the constructor refuses a cell whose elements are neither scalars nor ## character vectors %!error ... %! string ({[1, 2]}); ## size: more outputs than the requested dimensions yield %!error ... %! [r, c] = size (string ({'a', 'b'}), 1); ## sortrows: COL and DIRECTION must agree with each other and with A %!error ... %! sortrows (string ({'b', 'a'; 'd', 'c'}), {'ascend', 'descend', 'ascend'}); %!error ... %! sortrows (string ({'b', 'a'; 'd', 'c'}), {1}); %!error ... %! sortrows (string ({'b', 'a'; 'd', 'c'}), 1, 5); %!error ... %! sortrows (string ({'b', 'a'; 'd', 'c'}), [1, 2], {'ascend', 'descend', ... %! 'ascend'}); ## strip, pad and join reject a malformed second argument %!error ... %! strip (string ('a'), 5); %!error ... %! pad (string ('a'), 'left', 'x', 'y'); %!error ... %! join (string ({'a', 'b'}), 5, 2); pr0m1th3as-datatypes-9450b29/inst/tests/table.m-tst000066400000000000000000016720371527370255000221120ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## A table may hold a datetime variable, and several assertions below render one ## through 'table2cell', 'csv2table', 'summary' and 'groupsummary'. Those ## renderings use the default display formats, which persist across sessions, so ## a user who has set one with 'datetime.setDefaultFormats' would otherwise see ## this file fail. Save whatever is stored, run at the factory defaults, and ## put it back in the closing block; a format that was never set is restored as ## absent rather than as its factory value. ## ## The saved formats live in a persistent inside this helper rather than in a ## '%!shared' variable, because the '%!shared' blocks further down clear any ## shared variable declared before them, and the assertions that need the guard ## sit on both sides of them. %!function datetimeFormatGuard (mode) %! persistent saved %! if (strcmp (mode, 'save')) %! saved = {'', ''}; %! if (ispref ('datatypes', 'datetime_DefaultFormat')) %! saved{1} = getpref ('datatypes', 'datetime_DefaultFormat'); %! endif %! if (ispref ('datatypes', 'datetime_DefaultDateFormat')) %! saved{2} = getpref ('datatypes', 'datetime_DefaultDateFormat'); %! endif %! endif %! datetime.setDefaultFormats ('reset'); %! if (strcmp (mode, 'restore')) %! if (! isempty (saved{1})) %! datetime.setDefaultFormats ('default', saved{1}); %! endif %! if (! isempty (saved{2})) %! datetime.setDefaultFormats ('defaultdate', saved{2}); %! endif %! endif %!endfunction %!test %! datetimeFormatGuard ('save'); ################################################################################ ## ** Create Table and Convert Type ** ## ################################################################################ ## Available Methods ## ## ## ## 'table' 'table2array' 'table2cell' 'table2struct' ## ## ## ################################################################################ ## Test the constructor %!test # variables become table columns; names auto-detected from inputname %! Name = {"John"; "Mary"; "Peter"; "Barbara"}; %! Age = [23; 34; 42; 28]; %! Height = [167; 163; 183; 178]; %! T = table (Name, Age, Height); %! assert_equal (size (T), [4, 3]); %! assert_equal (size (T.Age), [4, 1]); %! assert_equal (mean (T.Age), 31.75); %! assert_equal (T.Properties.VariableNames, {"Name", "Age", "Height"}); %!test # an empty constructor yields a 0x0 table %! T = table (); %! assert_equal (size (T), [0, 0]); %! assert_equal (height (T), 0); %! assert_equal (width (T), 0); %! assert_equal (class (T), "table"); %! assert_equal (istable (T), true); %!test # a single variable gives a one-variable table with an auto name %! x = [1; 2; 3]; %! T = table (x); %! assert_equal (size (T), [3, 1]); %! assert_equal (T.Properties.VariableNames, {"x"}); %! assert_equal (T.x, [1; 2; 3]); %!test # an unnamed expression falls back to 'Var' names %! T = table ([1; 2], [3; 4]); %! assert_equal (T.Properties.VariableNames, {"Var1", "Var2"}); %!test # a multicolumn variable counts as a single table variable %! T = table ([1, 2; 3, 4; 5, 6], 'VariableNames', {'bp'}); %! assert_equal (size (T), [3, 1]); %! assert_equal (width (T), 1); %! assert_equal (size (T.bp), [3, 2]); %!test # 'VariableNames' overrides the detected names %! Name = {"John"; "Mary"; "Peter"; "Barbara"}; %! Age = [23; 34; 42; 28]; %! Height = [167; 163; 183; 178]; %! T = table (Name, Age, Height, "VariableNames", {"A", "B", "C"}); %! assert_equal (T.Properties.VariableNames, {"A", "B", "C"}); %!test # 'VariableNames' may be given as a string array %! T = table (1, 2, 'VariableNames', string ({'AA', 'BB'})); %! assert_equal (T.Properties.VariableNames, {'AA', 'BB'}); %! assert_equal (class (T.Properties.VariableNames), 'cell'); %!test # a multicolumn variable is kept intact %! Name = {"John"; "Mary"; "Peter"; "Barbara"}; %! Age = [23; 34; 42; 28]; %! Height = [167; 163; 183; 178]; %! BloodPressure = [114, 83; 119, 75; 115, 73; 107, 80]; %! T = table (Name, Age, Height, BloodPressure); %! assert_equal (size (T), [4, 4]); %! assert_equal (size (T.BloodPressure), [4, 2]); %!test # 'RowNames' labels the rows without adding a variable %! T = table ([1; 2; 3], 'VariableNames', {'x'}, ... %! 'RowNames', {'a'; 'b'; 'c'}); %! assert_equal (size (T), [3, 1]); %! assert_equal (T.Properties.RowNames, {'a'; 'b'; 'c'}); %!test # 'RowNames' may be given as a string array %! T = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', string ({'a', 'b'})); %! assert_equal (T.Properties.RowNames, {'a'; 'b'}); %!test # 'DimensionNames' sets the row/variable dimension labels %! T = table ([1; 2], 'VariableNames', {'x'}, ... %! 'DimensionNames', {'Samples', 'Measures'}); %! assert_equal (T.Properties.DimensionNames, {'Samples', 'Measures'}); %!test # the default dimension names are 'Row' and 'Variables' %! T = table ([1; 2]); %! assert_equal (T.Properties.DimensionNames, {'Row', 'Variables'}); %!test # heterogeneous variable types are preserved %! T = table ([1; 2], logical ([1; 0]), {'a'; 'b'}, string ({'c'; 'd'}), ... %! 'VariableNames', {'n', 'l', 'c', 's'}); %! assert_equal (class (T.n), 'double'); %! assert_equal (class (T.l), 'logical'); %! assert_equal (class (T.c), 'cell'); %! assert_equal (class (T.s), 'string'); %!test # variables with zero rows are allowed %! T = table (zeros (0, 1), cell (0, 1), 'VariableNames', {'a', 'b'}); %! assert_equal (size (T), [0, 2]); %! assert_equal (height (T), 0); %! assert_equal (width (T), 2); %!test # 'VariableDescriptions' and 'VariableUnits' default to unset %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! assert_equal (T.Properties.VariableDescriptions, {}); %! assert_equal (T.Properties.VariableUnits, {}); %!test # blanks are a set property, not an unset one %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! T.Properties.VariableDescriptions = {'', ''}; %! assert_equal (T.Properties.VariableDescriptions, {'', ''}); %!test # naming one entry of an unset property blanks the rest %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! T.Properties.VariableUnits(2) = {'kg'}; %! assert_equal (T.Properties.VariableUnits, {'', 'kg'}); %!test # an empty returns a set property to unset %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! T.Properties.VariableUnits = {'m', 'kg'}; %! T.Properties.VariableUnits = {}; %! assert_equal (T.Properties.VariableUnits, {}); %!test # removing a variable leaves an unset property unset %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! T = removevars (T, 'a'); %! assert_equal (T.Properties.VariableUnits, {}); %!test # adding a variable leaves an unset property unset %! T = table ([1; 2], 'VariableNames', {'a'}); %! T.b = [3; 4]; %! assert_equal (T.Properties.VariableUnits, {}); %!test # adding a variable extends a set property with a blank %! T = table ([1; 2], 'VariableNames', {'a'}); %! T.Properties.VariableUnits = {'m'}; %! T.b = [3; 4]; %! assert_equal (T.Properties.VariableUnits, {'m', ''}); %!test # horzcat leaves the units unset when neither input has any %! T = [table([1; 2], 'VariableNames', {'a'}), ... %! table([3; 4], 'VariableNames', {'b'})]; %! assert_equal (T.Properties.VariableUnits, {}); %!test # horzcat blanks the units of the input that has none %! A = table ([1; 2], 'VariableNames', {'a'}); %! A.Properties.VariableUnits = {'m'}; %! B = table ([3; 4], 'VariableNames', {'b'}); %! T = [A, B]; %! assert_equal (T.Properties.VariableUnits, {'m', ''}); %!test # a nested table is accepted as a variable %! T = table ([1; 2], table ([3; 4], [5; 6]), 'VariableNames', {'a', 'b'}); %! assert_equal (width (T), 2); %! assert_equal (class (T.b), 'table'); ## Test the constructor -- 'Size'/'VariableTypes' preallocation %!test # preallocates a table of the requested size with default values %! T = table ('Size', [4, 3], 'VariableTypes', {'double', 'datetime', ... %! 'string'}); %! assert_equal (size (T), [4, 3]); %! assert_equal (T.Properties.VariableNames, {'Var1', 'Var2', 'Var3'}); %!test # 'VariableNames' names the preallocated variables %! T = table ('Size', [4, 3], 'VariableTypes', {'double', 'datetime', ... %! 'string'}, ... %! 'VariableNames', {'A', 'B', 'C'}); %! assert_equal (T.Properties.VariableNames, {'A', 'B', 'C'}); %!test # numeric and logical types default to zeros/false %! T = table ('Size', [2, 3], 'VariableTypes', {'double', 'int8', 'logical'}); %! assert_equal (T.Var1, [0; 0]); %! assert_equal (T.Var2, int8 ([0; 0])); %! assert_equal (class (T.Var2), 'int8'); %! assert_equal (T.Var3, [false; false]); %!test # the 'doublenan'/'singlenan' types default to NaN %! T = table ('Size', [3, 2], 'VariableTypes', {'doublenan', 'singlenan'}); %! assert_equal (all (isnan (T.Var1)), true); %! assert_equal (class (T.Var2), 'single'); %! assert_equal (all (isnan (T.Var2)), true); %!test # categorical, datetime, duration and string default to missing %! T = table ('Size', [2, 4], 'VariableTypes', ... %! {'categorical', 'datetime', 'duration', 'string'}); %! assert_equal (all (ismissing (T.Var1)), true); %! assert_equal (all (isnat (T.Var2)), true); %! assert_equal (seconds (T.Var3), [0; 0]); %! assert_equal (all (ismissing (T.Var4)), true); %!test # cellstr, cell and struct types preallocate accordingly %! T = table ('Size', [3, 3], 'VariableTypes', {'cellstr', 'cell', 'struct'}); %! assert_equal (iscellstr (T.Var1), true); %! assert_equal (T.Var1, {''; ''; ''}); %! assert_equal (class (T.Var2), 'cell'); %! assert_equal (class (T.Var3), 'struct'); %!test # the 'char' type preallocates a cell array of empty character vectors %! T = table ('Size', [3, 1], 'VariableTypes', {'char'}); %! assert_equal (class (T.Var1), 'cell'); %! assert_equal (iscellstr (T.Var1), true); %! assert_equal (T.Var1, {''; ''; ''}); %!test # a 'table' typed variable preallocates an empty nested table %! T = table ('Size', [4, 3], 'VariableTypes', {'double', 'datetime', ... %! 'table'}, ... %! 'VariableNames', {'A', 'B', 'C'}); %! assert_equal (size (T), [4, 3]); %! assert_equal (class (T.A), 'double'); %! assert_equal (class (T.B), 'datetime'); %! assert_equal (class (T.C), 'table'); %! assert_equal (size (T.C), [0, 1]); %!test # 'RowNames' labels the preallocated rows %! T = table ('Size', [3, 1], 'VariableTypes', {'double'}, ... %! 'RowNames', {'a'; 'b'; 'c'}); %! assert_equal (T.Properties.RowNames, {'a'; 'b'; 'c'}); ## Test the constructor -- an empty table built with 'Size' or 'table.empty' %!test # 'Size' with no variables keeps the requested row count %! T = table ('Size', [5, 0], 'VariableTypes', {}); %! assert_equal (size (T), [5, 0]); %! assert_equal (height (T), 5); %! assert_equal (T.Properties.VariableNames, cell (1, 0)); %!test # 'Size' with no variables takes row names for the rows it declares %! T = table ('Size', [3, 0], 'VariableTypes', {}, ... %! 'RowNames', {'a', 'b', 'c'}); %! assert_equal (size (T), [3, 0]); %! assert_equal (T.Properties.RowNames, {'a'; 'b'; 'c'}); %!test # 'table.empty' takes both dimensions, a size vector, or neither %! assert_equal (size (table.empty), [0, 0]); %! assert_equal (size (table.empty (5, 0)), [5, 0]); %! assert_equal (height (table.empty (5, 0)), 5); %! assert_equal (size (table.empty ([5, 0])), [5, 0]); %! assert_equal (size (table.empty (0, 0)), [0, 0]); %! assert_equal (class (table.empty), 'table'); %!test # 'table.empty' names and types the variables it is asked for %! T = table.empty (0, 3); %! assert_equal (size (T), [0, 3]); %! assert_equal (T.Properties.VariableNames, {'Var1', 'Var2', 'Var3'}); %! assert_equal (class (T.Var1), 'double'); %!test # 'Size' on its own gives every variable a double %! T = table ('Size', [5, 0]); %! assert_equal (size (T), [5, 0]); %! assert_equal (height (T), 5); %! U = table ('Size', [0, 3]); %! assert_equal (size (U), [0, 3]); %! assert_equal (U.Properties.VariableNames, {'Var1', 'Var2', 'Var3'}); %! assert_equal (class (U.Var1), 'double'); %!test # a cell variable holds a dictionary, which a variable cannot be %! T = table ({dictionary(1, 2)}); %! assert_equal (class (T.Var1), 'cell'); %! assert_equal (isdictionary (T.Var1{1}), true); ## Test input validation for 'table.empty' %!error table.empty (5, 3); %!error table.empty (5); %!error ... %! table.empty (-1, 0); %!error ... %! table.empty (2.5, 0); %!error table.empty ({1, 0}); %!error ... %! table.empty (5, 'a'); %!error table.empty (5, 0, 2); ## Test input validation for the constructor %!error ... %! table ([1; 2], [3; 4], "VariableNames", {2, 3}); %!error ... %! table ([1; 2], [3; 4], "VariableNames", [2, 3]); %!error ... %! table ([1; 2], [3; 4], "VariableNames", [true, true]); %!error ... %! table ([1; 2], [3; 4], "VariableNames", {"", "A"}); %!error ... %! table ([1; 2], [3; 4], "RowNames", {2, 3}); %!error ... %! table ([1; 2], [3; 4], "RowNames", [2, 3]); %!error ... %! table ([1; 2], [3; 4], "RowNames", [true, false]); %!error ... %! table ([1; 2], [3; 4], "DimensionNames", {2, 3}); %!error ... %! table ([1; 2], [3; 4], "DimensionNames", [2, 3]); %!error ... %! table ([1; 2], [3; 4], "DimensionNames", [true, false]); %!error ... %! table ([1; 2], [3; 4], "DimensionNames", {"A", "B", "C"}); %!error ... %! table (1, "DimensionNames", {"Properties", "Variables"}); %!error ... %! table (1, "DimensionNames", {"VariableNames", "Row"}); %!error ... %! table (1, 'VariableNames', {'Variables'}); %!error ... %! table ("Size", [4, 2], "VariableTypes", {"A", "B", "C"}); %!error ... %! table ("Size", [4, 3], "VariableTypes", ["A", "B", "C"]); %!error ... %! table ("Size", [4, 3], "VariableTypes", {1, 2, 3}); %!error ... %! table ("Size", [4, 3], "VariableTypes", {"double", "double", "logical"}, ... %! "VariableNames", {"A", "B"}); %!error ... %! table ("Size", [4, 3], "VariableTypes", {"double", "double", "logical"}, ... %! "RowNames", {"A", "B", "C"}); %!error ... %! table ("Size", [4, 1], "VariableTypes", {"uint128"}); %!error ... %! table ("Size", 3, "VariableTypes", {"double"}); %!error ... %! table ("Size", [3, 1, 2], "VariableTypes", {"double"}); %!error ... %! table ("Size", [4, 3], "VariableTypes", {"double", "double", ... %! "timetable"}, ... %! "VariableNames", {"A", "B", "C"}); %!error ... %! table ("Size", [4, 3], "VariableTypes", {"double", "double", "timetable"}); %!error ... %! Age = [1; 2]; %! table (Age, Age); %!error ... %! Age = [1; 2]; %! Height = [3; 4]; %! table (Age, Age, Height, Height); %!error ... %! table ([1; 2], [3; 4], "VariableNames", {"A"}); %!error ... %! table (@sin, 1); %!error ... %! table (1, @sin, 'VariableNames', {'a', 'b'}); %!error ... %! table (dictionary (1, 2)); %!error ... %! table (timeseries ([1; 2])); %!error ... %! table (tscollection ([0; 1])); %!error ... %! table (ones (2, 2, 2), [1; 2], "VariableNames", {"A", "B"}); %!error ... %! table ([1; 2], ones (2, 2, 2), "VariableNames", {"A", "B"}); %!error ... %! table (struct ("age", 1), [1; 2; 3; 4; 5], "VariableNames", {"A", "B"}); %!error ... %! table ([1; 2; 3; 4; 5], struct ("age", 1), "VariableNames", {"A", "B"}); %!error ... %! table ("Size", [4, 2], "VariableTypes", {"double", "double"}, ... %! "RowNames", {"A", "B", "B", "D"}); %!error ... %! table ([1; 2; 3], "RowNames", {"a", "b"}); %!shared LastName, Age, Smoker, Height, Weight, BloodPressure, T, tblA %! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"}; %! Age = [38; 43; 38; 40; 49]; %! Smoker = logical ([1; 0; 1; 0; 1]); %! Height = [71; 69; 64; 67; 64]; %! Weight = [176; 163; 131; 133; 119]; %! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80]; %! T = table (Age, Smoker, Height, Weight, BloodPressure); ## Test 'table2array' method %!test # the canonical patient table concatenates into a numeric matrix %! assert_equal (table2array (T), [Age, Smoker, Height, Weight, BloodPressure]); %!test # single-column numeric variables concatenate into a matrix %! A = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'a', 'b'}); %! assert_equal (table2array (A), [1, 4; 2, 5; 3, 6]); %!test # multicolumn variables expand into multiple columns %! A = table ([1, 2; 3, 4], [5; 6], 'VariableNames', {'a', 'b'}); %! M = table2array (A); %! assert_equal (size (M), [2, 3]); %! assert_equal (M, [1, 2, 5; 3, 4, 6]); %!test # a single variable is returned as-is %! M = table2array (table ([1; 2; 3], 'VariableNames', {'x'})); %! assert_equal (M, [1; 2; 3]); %! assert_equal (class (M), 'double'); %!test # the result takes the dominant numeric type (double + single -> single) %! M = table2array (table ([1; 2], single ([3; 4]), 'VariableNames', {'a', ... %! 'b'})); %! assert_equal (class (M), 'single'); %!test # an integer variable dominates a double variable %! M = table2array (table (int8 ([1; 2]), [3; 4], 'VariableNames', {'a', 'b'})); %! assert_equal (class (M), 'int8'); %!test # a logical variable widens to double %! M = table2array (table (logical ([1; 0]), [3; 4], 'VariableNames', {'a', ... %! 'b'})); %! assert_equal (class (M), 'double'); %!test # homogeneous string variables yield a string array %! M = table2array (table (string ({'a'; 'b'}), string ({'c'; 'd'}), ... %! 'VariableNames', {'x', 'y'})); %! assert_equal (class (M), 'string'); %! assert_equal (size (M), [2, 2]); %!test # homogeneous categorical variables yield a categorical array %! M = table2array (table (categorical ({'a'; 'b'}), ... %! categorical ({'c'; 'd'}), ... %! 'VariableNames', {'x', 'y'})); %! assert_equal (class (M), 'categorical'); %! assert_equal (size (M), [2, 2]); %!test # homogeneous datetime variables yield a datetime array %! d = datetime (2020, 1, [1; 2]); %! M = table2array (table (d, d, 'VariableNames', {'x', 'y'})); %! assert_equal (class (M), 'datetime'); %! assert_equal (size (M), [2, 2]); %!test # an empty table returns an empty array %! assert_equal (table2array (table ()), []); %!test # homogeneous cell variables yield a cell array %! M = table2array (table ({'a'; 'b'}, {'c'; 'd'}, 'VariableNames', {'x', ... %! 'y'})); %! assert_equal (class (M), 'cell'); %! assert_equal (size (M), [2, 2]); ## Test 'table2array' joins every pair of variable types as MATLAB joins them, ## and gives the cell array of 'table2cell' where MATLAB cannot %!shared d, s, i1, u1, i2, i8, u8, b, c, cs, ce, st, ca, dt, du, cd, sr, tb, ms %! d = [66.5; 67]; %! s = single ([68.5; 69]); %! i1 = int8 ([70; 71]); %! u1 = uint8 ([72; 73]); %! i2 = int16 ([74; 75]); %! i8 = int64 ([76; 77]); %! u8 = uint64 ([78; 79]); %! b = [true; false]; %! c = ['ab'; 'cd']; %! cs = {'ef'; 'g'}; %! ce = {1; 'h'}; %! st = string ({'x'; 'y'}); %! ca = categorical ({'lo'; 'hi'}); %! dt = datetime (2020, 1, [1; 2]); %! du = hours ([1; 2]); %! cd = calmonths ([1; 2]); %! sr = struct ('a', {1; 2}); %! tb = table ([1; 2], 'VariableNames', {'p'}); %! ms = [missing; missing]; %!assert_equal (table2array (table (d, d, 'VariableNames', {'a', 'b'})), ... %! [66.5, 66.5; 67, 67]) %!assert_equal (table2array (table (d, s)), single ([66.5, 68.5; 67, 69])) %!assert_equal (table2array (table (d, i1)), int8 ([67, 70; 67, 71])) %!assert_equal (table2array (table (d, u1)), uint8 ([67, 72; 67, 73])) %!assert_equal (table2array (table (d, i2)), int16 ([67, 74; 67, 75])) %!assert_equal (table2array (table (d, i8)), int64 ([67, 76; 67, 77])) %!assert_equal (table2array (table (d, u8)), uint64 ([67, 78; 67, 79])) %!assert_equal (table2array (table (d, b)), [66.5, 1; 67, 0]) %!assert_equal (table2array (table (d, c)), ['Bab'; 'Ccd']) %!assert_equal (table2array (table (d, cs)), table2cell (table (d, cs))) %!assert_equal (table2array (table (d, ce)), table2cell (table (d, ce))) %!assert_equal (table2array (table (d, st)), string ({'66.5', 'x'; '67', 'y'})) %!assert_equal (table2array (table (d, ca)), table2cell (table (d, ca))) %!assert_equal (table2array (table (d, dt)), table2cell (table (d, dt))) %!assert_equal (table2array (table (d, du)), hours ([1596, 1; 1608, 2])) %!assert_equal (table2array (table (d, cd)), ... %! calendarDuration (0, [0, 1; 0, 2], 0, ... %! [1596, 0; 1608, 0], 0, 0)) %!assert_equal (table2array (table (d, sr)), table2cell (table (d, sr))) %!assert_equal (isequaln (table2array (table (d, tb)), ... %! table2cell (table (d, tb))), true) %!assert_equal (table2array (table (d, ms)), [66.5, NaN; 67, NaN]) %!assert_equal (table2array (table (s, s, 'VariableNames', {'a', 'b'})), ... %! single ([68.5, 68.5; 69, 69])) %!assert_equal (table2array (table (s, i1)), int8 ([69, 70; 69, 71])) %!assert_equal (table2array (table (s, u1)), uint8 ([69, 72; 69, 73])) %!assert_equal (table2array (table (s, i2)), int16 ([69, 74; 69, 75])) %!assert_equal (table2array (table (s, i8)), int64 ([69, 76; 69, 77])) %!assert_equal (table2array (table (s, u8)), uint64 ([69, 78; 69, 79])) %!assert_equal (table2array (table (s, b)), single ([68.5, 1; 69, 0])) %!assert_equal (table2array (table (s, c)), ['Dab'; 'Ecd']) %!assert_equal (table2array (table (s, cs)), table2cell (table (s, cs))) %!assert_equal (table2array (table (s, ce)), table2cell (table (s, ce))) %!assert_equal (table2array (table (s, st)), string ({'68.5', 'x'; '69', 'y'})) %!assert_equal (table2array (table (s, ca)), table2cell (table (s, ca))) %!assert_equal (table2array (table (s, dt)), table2cell (table (s, dt))) %!assert_equal (table2array (table (s, du)), table2cell (table (s, du))) %!assert_equal (table2array (table (s, cd)), table2cell (table (s, cd))) %!assert_equal (table2array (table (s, sr)), table2cell (table (s, sr))) %!assert_equal (isequaln (table2array (table (s, tb)), ... %! table2cell (table (s, tb))), true) %!assert_equal (table2array (table (s, ms)), single ([68.5, NaN; 69, NaN])) %!assert_equal (table2array (table (i1, i1, 'VariableNames', {'a', 'b'})), ... %! int8 ([70, 70; 71, 71])) %!assert_equal (table2array (table (i1, u1)), int8 ([70, 72; 71, 73])) %!assert_equal (table2array (table (i1, i2)), int8 ([70, 74; 71, 75])) %!assert_equal (table2array (table (i1, i8)), int8 ([70, 76; 71, 77])) %!assert_equal (table2array (table (i1, u8)), int8 ([70, 78; 71, 79])) %!assert_equal (table2array (table (i1, b)), int8 ([70, 1; 71, 0])) %!assert_equal (table2array (table (i1, c)), ['Fab'; 'Gcd']) %!assert_equal (table2array (table (i1, cs)), table2cell (table (i1, cs))) %!assert_equal (table2array (table (i1, ce)), table2cell (table (i1, ce))) %!assert_equal (table2array (table (i1, st)), string ({'70', 'x'; '71', 'y'})) %!assert_equal (table2array (table (i1, ca)), table2cell (table (i1, ca))) %!assert_equal (table2array (table (i1, dt)), table2cell (table (i1, dt))) %!assert_equal (table2array (table (i1, du)), table2cell (table (i1, du))) %!assert_equal (table2array (table (i1, cd)), table2cell (table (i1, cd))) %!assert_equal (table2array (table (i1, sr)), table2cell (table (i1, sr))) %!assert_equal (isequaln (table2array (table (i1, tb)), ... %! table2cell (table (i1, tb))), true) %!assert_equal (isequaln (table2array (table (i1, ms)), ... %! table2cell (table (i1, ms))), true) %!assert_equal (table2array (table (u1, u1, 'VariableNames', {'a', 'b'})), ... %! uint8 ([72, 72; 73, 73])) %!assert_equal (table2array (table (u1, i2)), uint8 ([72, 74; 73, 75])) %!assert_equal (table2array (table (u1, i8)), uint8 ([72, 76; 73, 77])) %!assert_equal (table2array (table (u1, u8)), uint8 ([72, 78; 73, 79])) %!assert_equal (table2array (table (u1, b)), uint8 ([72, 1; 73, 0])) %!assert_equal (table2array (table (u1, c)), ['Hab'; 'Icd']) %!assert_equal (table2array (table (u1, cs)), table2cell (table (u1, cs))) %!assert_equal (table2array (table (u1, ce)), table2cell (table (u1, ce))) %!assert_equal (table2array (table (u1, st)), string ({'72', 'x'; '73', 'y'})) %!assert_equal (table2array (table (u1, ca)), table2cell (table (u1, ca))) %!assert_equal (table2array (table (u1, dt)), table2cell (table (u1, dt))) %!assert_equal (table2array (table (u1, du)), table2cell (table (u1, du))) %!assert_equal (table2array (table (u1, cd)), table2cell (table (u1, cd))) %!assert_equal (table2array (table (u1, sr)), table2cell (table (u1, sr))) %!assert_equal (isequaln (table2array (table (u1, tb)), ... %! table2cell (table (u1, tb))), true) %!assert_equal (isequaln (table2array (table (u1, ms)), ... %! table2cell (table (u1, ms))), true) %!assert_equal (table2array (table (i2, i2, 'VariableNames', {'a', 'b'})), ... %! int16 ([74, 74; 75, 75])) %!assert_equal (table2array (table (i2, i8)), int16 ([74, 76; 75, 77])) %!assert_equal (table2array (table (i2, u8)), int16 ([74, 78; 75, 79])) %!assert_equal (table2array (table (i2, b)), int16 ([74, 1; 75, 0])) %!assert_equal (table2array (table (i2, c)), ['Jab'; 'Kcd']) %!assert_equal (table2array (table (i2, cs)), table2cell (table (i2, cs))) %!assert_equal (table2array (table (i2, ce)), table2cell (table (i2, ce))) %!assert_equal (table2array (table (i2, st)), string ({'74', 'x'; '75', 'y'})) %!assert_equal (table2array (table (i2, ca)), table2cell (table (i2, ca))) %!assert_equal (table2array (table (i2, dt)), table2cell (table (i2, dt))) %!assert_equal (table2array (table (i2, du)), table2cell (table (i2, du))) %!assert_equal (table2array (table (i2, cd)), table2cell (table (i2, cd))) %!assert_equal (table2array (table (i2, sr)), table2cell (table (i2, sr))) %!assert_equal (isequaln (table2array (table (i2, tb)), ... %! table2cell (table (i2, tb))), true) %!assert_equal (isequaln (table2array (table (i2, ms)), ... %! table2cell (table (i2, ms))), true) %!assert_equal (table2array (table (i8, i8, 'VariableNames', {'a', 'b'})), ... %! int64 ([76, 76; 77, 77])) %!assert_equal (table2array (table (i8, u8)), int64 ([76, 78; 77, 79])) %!assert_equal (table2array (table (i8, b)), int64 ([76, 1; 77, 0])) %!assert_equal (table2array (table (i8, c)), ['Lab'; 'Mcd']) %!assert_equal (table2array (table (i8, cs)), table2cell (table (i8, cs))) %!assert_equal (table2array (table (i8, ce)), table2cell (table (i8, ce))) %!assert_equal (table2array (table (i8, st)), string ({'76', 'x'; '77', 'y'})) %!assert_equal (table2array (table (i8, ca)), table2cell (table (i8, ca))) %!assert_equal (table2array (table (i8, dt)), table2cell (table (i8, dt))) %!assert_equal (table2array (table (i8, du)), table2cell (table (i8, du))) %!assert_equal (table2array (table (i8, cd)), table2cell (table (i8, cd))) %!assert_equal (table2array (table (i8, sr)), table2cell (table (i8, sr))) %!assert_equal (isequaln (table2array (table (i8, tb)), ... %! table2cell (table (i8, tb))), true) %!assert_equal (isequaln (table2array (table (i8, ms)), ... %! table2cell (table (i8, ms))), true) %!assert_equal (table2array (table (u8, u8, 'VariableNames', {'a', 'b'})), ... %! uint64 ([78, 78; 79, 79])) %!assert_equal (table2array (table (u8, b)), uint64 ([78, 1; 79, 0])) %!assert_equal (table2array (table (u8, c)), ['Nab'; 'Ocd']) %!assert_equal (table2array (table (u8, cs)), table2cell (table (u8, cs))) %!assert_equal (table2array (table (u8, ce)), table2cell (table (u8, ce))) %!assert_equal (table2array (table (u8, st)), string ({'78', 'x'; '79', 'y'})) %!assert_equal (table2array (table (u8, ca)), table2cell (table (u8, ca))) %!assert_equal (table2array (table (u8, dt)), table2cell (table (u8, dt))) %!assert_equal (table2array (table (u8, du)), table2cell (table (u8, du))) %!assert_equal (table2array (table (u8, cd)), table2cell (table (u8, cd))) %!assert_equal (table2array (table (u8, sr)), table2cell (table (u8, sr))) %!assert_equal (isequaln (table2array (table (u8, tb)), ... %! table2cell (table (u8, tb))), true) %!assert_equal (isequaln (table2array (table (u8, ms)), ... %! table2cell (table (u8, ms))), true) %!assert_equal (table2array (table (b, b, 'VariableNames', {'a', 'b'})), ... %! [true, true; false, false]) %!assert_equal (table2array (table (b, c)), table2cell (table (b, c))) %!assert_equal (table2array (table (b, cs)), table2cell (table (b, cs))) %!assert_equal (table2array (table (b, ce)), table2cell (table (b, ce))) %!assert_equal (table2array (table (b, st)), ... %! string ({'true', 'x'; 'false', 'y'})) %!assert_equal (table2array (table (b, ca)), table2cell (table (b, ca))) %!assert_equal (table2array (table (b, dt)), table2cell (table (b, dt))) %!assert_equal (table2array (table (b, du)), hours ([24, 1; 0, 2])) %!assert_equal (table2array (table (b, cd)), ... %! calendarDuration (0, [0, 1; 0, 2], 0, [24, 0; 0, 0], 0, 0)) %!assert_equal (table2array (table (b, sr)), table2cell (table (b, sr))) %!assert_equal (isequaln (table2array (table (b, tb)), ... %! table2cell (table (b, tb))), true) %!assert_equal (isequaln (table2array (table (b, ms)), ... %! table2cell (table (b, ms))), true) %!assert_equal (table2array (table (c, c, 'VariableNames', {'a', 'b'})), ... %! ['abab'; 'cdcd']) %!assert_equal (table2array (table (c, cs)), table2cell (table (c, cs))) %!assert_equal (table2array (table (c, ce)), table2cell (table (c, ce))) %!assert_equal (table2array (table (c, st)), string ({'ab', 'x'; 'cd', 'y'})) %!assert_equal (table2array (table (c, ca)), table2cell (table (c, ca))) %!assert_equal (table2array (table (c, dt)), table2cell (table (c, dt))) %!assert_equal (table2array (table (c, du)), table2cell (table (c, du))) %!assert_equal (table2array (table (c, cd)), table2cell (table (c, cd))) %!assert_equal (table2array (table (c, sr)), table2cell (table (c, sr))) %!assert_equal (isequaln (table2array (table (c, tb)), ... %! table2cell (table (c, tb))), true) %!assert_equal (isequaln (table2array (table (c, ms)), ... %! table2cell (table (c, ms))), true) %!assert_equal (table2array (table (cs, cs, 'VariableNames', {'a', 'b'})), ... %! {'ef', 'ef'; 'g', 'g'}) %!assert_equal (table2array (table (cs, ce)), {'ef', 1; 'g', 'h'}) %!assert_equal (table2array (table (cs, st)), string ({'ef', 'x'; 'g', 'y'})) %!assert_equal (table2array (table (cs, ca)), table2cell (table (cs, ca))) %!assert_equal (table2array (table (cs, dt)), table2cell (table (cs, dt))) %!assert_equal (table2array (table (cs, du)), table2cell (table (cs, du))) %!assert_equal (table2array (table (cs, cd)), table2cell (table (cs, cd))) %!assert_equal (table2array (table (cs, sr)), table2cell (table (cs, sr))) %!assert_equal (isequaln (table2array (table (cs, tb)), ... %! table2cell (table (cs, tb))), true) %!assert_equal (isequaln (table2array (table (cs, ms)), ... %! table2cell (table (cs, ms))), true) %!assert_equal (table2array (table (ce, ce, 'VariableNames', {'a', 'b'})), ... %! {1, 1; 'h', 'h'}) %!assert_equal (table2array (table (ce, st)), string ({'1', 'x'; 'h', 'y'})) %!assert_equal (table2array (table (ce, ca)), table2cell (table (ce, ca))) %!assert_equal (table2array (table (ce, dt)), table2cell (table (ce, dt))) %!assert_equal (table2array (table (ce, du)), table2cell (table (ce, du))) %!assert_equal (table2array (table (ce, cd)), table2cell (table (ce, cd))) %!assert_equal (table2array (table (ce, sr)), table2cell (table (ce, sr))) %!assert_equal (isequaln (table2array (table (ce, tb)), ... %! table2cell (table (ce, tb))), true) %!assert_equal (isequaln (table2array (table (ce, ms)), ... %! table2cell (table (ce, ms))), true) %!assert_equal (table2array (table (st, st, 'VariableNames', {'a', 'b'})), ... %! string ({'x', 'x'; 'y', 'y'})) %!assert_equal (table2array (table (st, ca)), table2cell (table (st, ca))) %!assert_equal (table2array (table (st, dt)), table2cell (table (st, dt))) %!assert_equal (table2array (table (st, du)), table2cell (table (st, du))) %!assert_equal (table2array (table (st, cd)), table2cell (table (st, cd))) %!assert_equal (table2array (table (st, sr)), table2cell (table (st, sr))) %!assert_equal (isequaln (table2array (table (st, tb)), ... %! table2cell (table (st, tb))), true) %!assert_equal (isequaln (table2array (table (st, ms)), ... %! string ({'x', NaN; 'y', NaN})), true) %!assert_equal (table2array (table (ca, ca, 'VariableNames', {'a', 'b'})), ... %! categorical ({'lo', 'lo'; 'hi', 'hi'}, {'hi', 'lo'})) %!assert_equal (table2array (table (ca, dt)), table2cell (table (ca, dt))) %!assert_equal (table2array (table (ca, du)), table2cell (table (ca, du))) %!assert_equal (table2array (table (ca, cd)), table2cell (table (ca, cd))) %!assert_equal (table2array (table (ca, sr)), table2cell (table (ca, sr))) %!assert_equal (isequaln (table2array (table (ca, tb)), ... %! table2cell (table (ca, tb))), true) %!assert_equal (isequaln (table2array (table (ca, ms)), ... %! categorical ({'lo', ''; 'hi', ''}, {'hi', 'lo'})), true) %!assert_equal (table2array (table (dt, dt, 'VariableNames', {'a', 'b'})), ... %! datetime (2020, 1, [1, 1; 2, 2])) %!assert_equal (table2array (table (dt, du)), table2cell (table (dt, du))) %!assert_equal (table2array (table (dt, cd)), table2cell (table (dt, cd))) %!assert_equal (table2array (table (dt, sr)), table2cell (table (dt, sr))) %!assert_equal (isequaln (table2array (table (dt, tb)), ... %! table2cell (table (dt, tb))), true) %!assert_equal (isequaln (table2array (table (dt, ms)), ... %! datetime (2020, 1, [1, NaN; 2, NaN])), true) %!assert_equal (table2array (table (du, du, 'VariableNames', {'a', 'b'})), ... %! hours ([1, 1; 2, 2])) %!assert_equal (table2array (table (du, cd)), ... %! calendarDuration (0, [0, 1; 0, 2], 0, [1, 0; 2, 0], 0, 0)) %!assert_equal (table2array (table (du, sr)), table2cell (table (du, sr))) %!assert_equal (isequaln (table2array (table (du, tb)), ... %! table2cell (table (du, tb))), true) %!assert_equal (isequaln (table2array (table (du, ms)), ... %! hours ([1, NaN; 2, NaN])), true) %!assert_equal (table2array (table (cd, cd, 'VariableNames', {'a', 'b'})), ... %! calmonths ([1, 1; 2, 2])) %!assert_equal (table2array (table (cd, sr)), table2cell (table (cd, sr))) %!assert_equal (isequaln (table2array (table (cd, tb)), ... %! table2cell (table (cd, tb))), true) %!assert_equal (isequaln (table2array (table (cd, ms)), ... %! calmonths ([1, NaN; 2, NaN])), true) %!assert_equal (table2array (table (sr, sr, 'VariableNames', {'a', 'b'})), ... %! struct ('a', {1, 1; 2, 2})) %!assert_equal (isequaln (table2array (table (sr, tb)), ... %! table2cell (table (sr, tb))), true) %!assert_equal (isequaln (table2array (table (sr, ms)), ... %! table2cell (table (sr, ms))), true) %!assert_equal (isequaln (table2array (table (tb, tb, 'VariableNames', ... %! {'a', 'b'})), ... %! table2cell (table (tb, tb, 'VariableNames', ... %! {'a', 'b'}))), true) %!assert_equal (isequaln (table2array (table (tb, ms)), ... %! table2cell (table (tb, ms))), true) %!assert_equal (isequaln (table2array (table (ms, ms, 'VariableNames', ... %! {'a', 'b'})), ... %! repmat (missing, 2, 2)), true) %!assert_equal (table2array (table (u1, i1)), uint8 ([72, 70; 73, 71])) %!assert_equal (table2array (table (i2, i1)), int16 ([74, 70; 75, 71])) %!assert_equal (table2array (table (i8, i1)), int64 ([76, 70; 77, 71])) %!assert_equal (table2array (table (u8, i1)), uint64 ([78, 70; 79, 71])) %!assert_equal (table2array (table (i2, u1)), int16 ([74, 72; 75, 73])) %!assert_equal (table2array (table (i8, u1)), int64 ([76, 72; 77, 73])) %!assert_equal (table2array (table (u8, u1)), uint64 ([78, 72; 79, 73])) %!assert_equal (table2array (table (i8, i2)), int64 ([76, 74; 77, 75])) %!assert_equal (table2array (table (u8, i2)), uint64 ([78, 74; 79, 75])) %!assert_equal (table2array (table (u8, i8)), uint64 ([78, 76; 79, 77])) ## Test 'table2array' over more than two variables and other shapes %!assert_equal (table2array (table (int8 ([100; -1]), int16 ([1000; 2]))), ... %! int8 ([100, 127; -1, 2])) %!assert_equal (table2array (table (int64 (2) ^ 53 + 1, 2.5)), ... %! [int64(2) ^ 53 + 1, int64(3)]) %!assert_equal (table2array (table ([1.5; 2], {'ab'; 'c'}, st)), ... %! string ({'1.5', 'ab', 'x'; '2', 'c', 'y'})) %!assert_equal (table2array (table ([1.5; 2], {1; 'x'}, st)), ... %! string ({'1.5', '1', 'x'; '2', 'x', 'y'})) %!assert_equal (table2array (table ([2; 3], int8 ([100; -1]), ... %! int16 ([1000; 2]))), ... %! int8 ([2, 100, 127; 3, -1, 2])) %!assert_equal (table2array (table ([true; false], ['c'; 'd'], [1; 2])), ... %! table2cell (table ([true; false], ['c'; 'd'], [1; 2]))) %!assert_equal (table2array (table ([1, 2; 3, 4], st)), ... %! string ({'1', '2', 'x'; '3', '4', 'y'})) %!assert_equal (table2array (table ({'a', 'b'; 'c', 'd'}, st)), ... %! string ({'a', 'b', 'x'; 'c', 'd', 'y'})) %!assert_equal (table2array (table ({{'a'}; 'x'}, st)), ... %! string ({'a', 'x'; 'x', 'y'})) %!assert_equal (table2array (table ({[1, 2]; 'x'}, st)), ... %! table2cell (table ({[1, 2]; 'x'}, st))) %!assert_equal (table2array (table (['a'; 'b'], ['c'; 'd'])), ['ac'; 'bd']) %!assert_equal (table2array (table ([65; 66], c, ['e'; 'f'])), ... %! ['Aabe'; 'Bcdf']) %!assert_equal (table2array (table (ca, categorical ({'a'; 'b'}))), ... %! categorical ({'lo', 'a'; 'hi', 'b'}, {'hi', 'lo', 'a', 'b'})) %!assert_equal (table2array (table (categorical ({'lo'; 'hi'}, ... %! {'lo', 'hi'}, 'Ordinal', true), ca)), ... %! table2cell (table (categorical ({'lo'; 'hi'}, ... %! {'lo', 'hi'}, 'Ordinal', true), ca))) %!assert_equal (table2array (table (datetime (2020, 1, [1; 2], ... %! 'TimeZone', 'UTC'), dt)), ... %! table2cell (table (datetime (2020, 1, [1; 2], ... %! 'TimeZone', 'UTC'), dt))) %!assert_equal (table2array (table (datetime (2020, 1, [1; 2], ... %! 'TimeZone', 'UTC'), ... %! datetime (2020, 1, [1; 2], 'TimeZone', ... %! 'Europe/Athens'))), ... %! datetime (2020, 1, [1, 1; 2, 2], [0, -2; 0, -2], 0, 0, ... %! 'TimeZone', 'UTC')) %!assert_equal (table2array (table (sr, struct ('z', {1; 2}))), ... %! table2cell (table (sr, struct ('z', {1; 2})))) %!assert_equal (isequaln (table2array (table (tb, table ({'a'; 'b'}, ... %! 'VariableNames', {'r'}))), ... %! table ([1; 2], {'a'; 'b'}, ... %! 'VariableNames', {'p', 'r'})), true) %!assert_equal (table2array (table (zeros (0, 1), string (cell (0, 1)))), ... %! string (cell (0, 2))) %!assert_equal (table2array (table (zeros (0, 1), cell (0, 1))), cell (0, 2)) ## Test '{}' and the variable dimension name give what 'table2array' gives %!assert_equal (table (d, cs){:,:}, table2array (table (d, cs))) %!assert_equal (table (d, cs).Variables, table2array (table (d, cs))) %!assert_equal (table (d, cs){2,:}, {67, 'g'}) %!assert_equal (table (d, st){:,:}, table2array (table (d, st))) %!assert_equal (table (d, st).Variables, table2array (table (d, st))) %!shared LastName, Age, Smoker, Height, Weight, BloodPressure, T, tblA %! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"}; %! Age = [38; 43; 38; 40; 49]; %! Smoker = logical ([1; 0; 1; 0; 1]); %! Height = [71; 69; 64; 67; 64]; %! Weight = [176; 163; 131; 133; 119]; %! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80]; %! T = table (Age, Smoker, Height, Weight, BloodPressure); ## Test 'table2cell' method %!test # the output cell array has the same size as the table %! A = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); %! C = table2cell (A); %! assert_equal (size (C), [3, 2]); %! assert_equal (C{1, 1}, 1); %! assert_equal (C{1, 2}, 'a'); %!test # the patient table round-trips its numeric block %! C = table2cell (T); %! assert_equal (cell2mat (C(:, [1:4])), [Age, Smoker, Height, Weight]); %!test # a multicolumn variable becomes a row vector in a single cell %! C = table2cell (table ([1, 2; 3, 4], 'VariableNames', {'bp'})); %! assert_equal (size (C), [2, 1]); %! assert_equal (C{1}, [1, 2]); %! assert_equal (C{2}, [3, 4]); %!test # a cellstr variable keeps its char contents %! C = table2cell (table ({'a'; 'b'}, 'VariableNames', {'y'})); %! assert_equal (C{1}, 'a'); %! assert_equal (C{2}, 'b'); %!test # a logical variable is placed scalar-wise %! C = table2cell (table (logical ([1; 0]), 'VariableNames', {'l'})); %! assert_equal (C{1}, true); %! assert_equal (C{2}, false); %!test # a string variable gives one string per cell %! C = table2cell (table (string ({'a'; 'b'}), 'VariableNames', {'s'})); %! assert_equal (C, {string('a'); string('b')}); %!test # a categorical variable gives one categorical per cell %! C = table2cell (table (categorical ({'lo'; 'hi'}), 'VariableNames', {'c'})); %! assert_equal (C{2}, categorical ({'hi'}, {'hi', 'lo'})); %!test # a datetime variable gives one datetime per cell %! C = table2cell (table (datetime (2020, 1, [1; 2]), 'VariableNames', {'d'})); %! assert_equal (C, {datetime(2020, 1, 1); datetime(2020, 1, 2)}); %!test # a duration variable gives one duration per cell %! C = table2cell (table (days ([1.5; 2.5]), 'VariableNames', {'d'})); %! assert_equal (C, {days(1.5); days(2.5)}); %!test # a calendarDuration variable gives one per cell %! C = table2cell (table (calmonths ([1; 2]), 'VariableNames', {'m'})); %! assert_equal (C, {calmonths(1); calmonths(2)}); %!test # an integer variable keeps its class %! C = table2cell (table (int8 ([100; -1]), 'VariableNames', {'i'})); %! assert_equal (C, {int8(100); int8(-1)}); %!test # a missing variable gives one missing per cell %! C = table2cell (table ([missing; missing], 'VariableNames', {'m'})); %! assert_equal (class (C{2}), 'missing'); %!test # a two-column datetime variable gives a row per cell %! D = [datetime(2020, 1, [1; 2]), datetime(2021, 1, [1; 2])]; %! C = table2cell (table (D, 'VariableNames', {'d'})); %! assert_equal (C{2}, [datetime(2020, 1, 2), datetime(2021, 1, 2)]); %!test # a two-column cell variable gives a 1-by-2 cell per cell %! C = table2cell (table ({'a', 'b'; 'c', 'd'}, 'VariableNames', {'k'})); %! assert_equal (C, {{'a', 'b'}; {'c', 'd'}}); %!test # a nested table gives its row as a one-row table %! N = table (datetime (2020, 1, [1; 2]), [5; 6], 'VariableNames', {'d', 'x'}); %! C = table2cell (table (N, [7; 8], 'VariableNames', {'m', 'y'})); %! assert_equal (isequaln (C{2,1}, N(2,:)), true); %! assert_equal (C{2,2}, 8); %!test # a struct variable yields one struct per cell %! A = table ([struct('x', 1); struct('x', 2)], 'VariableNames', {'s'}); %! C = table2cell (A); %! assert_equal (size (C), [2, 1]); %! assert_equal (class (C{1}), 'struct'); %! assert_equal (C{2}.x, 2); %!test # row names are not included in the output %! A = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', {'r1'; 'r2'}); %! C = table2cell (A); %! assert_equal (size (C), [2, 1]); %! assert_equal (C{1}, 1); %!test # a char matrix variable gives one row per cell %! C = table2cell (table (['ab'; 'cd'], 'VariableNames', {'k'})); %! assert_equal (C, {'ab'; 'cd'}); %!test # an object of any other class is placed in its cell as it is %! C = table2cell (table (inputParser ())); %! assert_equal (class (C{1}), 'inputParser'); %!test # a cell variable of function handles gives the handles %! C = table2cell (table ({@sin; @cos})); %! assert_equal (func2str (C{2}), 'cos'); ## Test input validation for 'table2cell' method %!error ... %! table2cell (table (containers.Map ({'a', 'b'}, {1, 2}))); ## Test 'table2struct' method %!test # default output is an m-by-1 struct array, one element per row %! A = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'x', 'y'}); %! S = table2struct (A); %! assert_equal (size (S), [3, 1]); %! assert_equal (numel (S), 3); %! assert_equal (fieldnames (S), {'x'; 'y'}); %! assert_equal ([S.x], [1, 2, 3]); %! assert_equal (S(2).y, 5); %!test # the patient table maps each row to a struct element %! S = table2struct (T); %! assert_equal ([S.Age], Age'); %! assert_equal (reshape ([S.BloodPressure], 2, 5)', BloodPressure); %! assert_equal (numel (S), height (T)); %!test # a multicolumn variable becomes a row vector field %! A = table ([1; 2], [3, 4; 5, 6], 'VariableNames', {'a', 'bp'}); %! S = table2struct (A); %! assert_equal (S(1).bp, [3, 4]); %! assert_equal (S(2).bp, [5, 6]); %!test # the struct array preserves object variable types %! d = datetime (2020, 1, [1; 2]); %! c = categorical ({'lo'; 'hi'}); %! S = table2struct (table (d, c, 'VariableNames', {'D', 'C'})); %! assert_equal (class (S(1).D), 'datetime'); %! assert_equal (class (S(1).C), 'categorical'); %! assert_equal (cellstr (S(2).C), {'hi'}); %!test # 'ToScalar' true returns one struct with whole-column fields %! A = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'x', 'y'}); %! S = table2struct (A, 'ToScalar', true); %! assert_equal (numel (S), 1); %! assert_equal (S.x, [1; 2; 3]); %! assert_equal (S.y, [4; 5; 6]); %!test # 'ToScalar' preserves object variable types %! d = datetime (2020, 1, [1; 2]); %! S = table2struct (table (d, categorical ({'a'; 'b'}), ... %! 'VariableNames', {'D', 'C'}), 'ToScalar', true); %! assert_equal (class (S.D), 'datetime'); %! assert_equal (class (S.C), 'categorical'); %!test # 'ToScalar' also accepts a numeric 1 for true %! S = table2struct (table ([1; 2], 'VariableNames', {'x'}), 'ToScalar', 1); %! assert_equal (numel (S), 1); %! assert_equal (S.x, [1; 2]); %!test # 'ToScalar' false gives the default struct array %! S = table2struct (table ([1; 2], 'VariableNames', {'x'}), 'ToScalar', false); %! assert_equal (numel (S), 2); %!test # row names are not carried into the struct %! A = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', {'r1'; 'r2'}); %! S = table2struct (A); %! assert_equal (fieldnames (S), {'x'}); %!test # a char matrix variable gives one row per element %! S = table2struct (table (['ab'; 'cd'], 'VariableNames', {'k'})); %! assert_equal (S(2).k, 'cd'); %!test # an object of any other class is placed in its element as it is %! S = table2struct (table (inputParser (), 'VariableNames', {'p'})); %! assert_equal (class (S.p), 'inputParser'); %!test # a nested table gives its row as a one-row table %! N = table (datetime (2020, 1, [1; 2]), [5; 6], 'VariableNames', {'d', 'x'}); %! S = table2struct (table (N, [7; 8], 'VariableNames', {'m', 'y'})); %! assert_equal (isequaln (S(2).m, N(2,:)), true); ## Test input validation for 'table2struct' method %!error ... %! table2struct (table (1), "ToScalar"); %!error ... %! table2struct (table (1), "Scalar", 1); %!error ... %! table2struct (table (containers.Map ({'a', 'b'}, {1, 2}))); ################################################################################ ## ** Save to Files ** ## ################################################################################ ## Available Methods ## ## ## ## 'table2csv' ## ## ## ################################################################################ ## Test 'table2csv' method (round-trips through 'csv2table') %!test # numeric, cellstr, and double variables round-trip by value and name %! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, [1.5; 2.5; 3.5], ... %! 'VariableNames', {'id', 'name', 'val'}); %! T.Properties.VariableDescriptions = {'the id', 'the name', 'the value'}; %! T.Properties.VariableUnits = {'#', 'str', 'kg'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.Properties.VariableNames, {'id', 'name', 'val'}); %! assert_equal (R.id, [1; 2; 3]); %! assert_equal (R.val, [1.5; 2.5; 3.5]); %! assert_equal (R.name, {'a'; 'b'; 'c'}); %! assert_equal (R.Properties.VariableDescriptions, {'the id', 'the name', ... %! 'the value'}); %! assert_equal (R.Properties.VariableUnits, {'#', 'str', 'kg'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a double round-trips bit for bit %! x = [pi; 0.1; realmin; eps; 1e308; -1/3]; %! T = table (x, 'VariableNames', {'v'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.v, x); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a day-first datetime format neither loses nor swaps components %! d = datetime ({'2020-01-02 03:04:05'; '2021-03-04 05:06:07'}); %! d.Format = 'dd/MM/yyyy'; %! T = table (d, 'VariableNames', {'v'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (isequaln (R.v, d), true); %! assert_equal (R.v.Format, 'dd/MM/yyyy'); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a zone-aware datetime keeps its TimeZone and its format %! d = datetime ({'2020-01-02'; '2021-03-04'}, 'TimeZone', 'America/New_York'); %! d.Format = 'dd-MM-uuuu HH:mm'; %! T = table (d, 'VariableNames', {'v'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.v.TimeZone, 'America/New_York'); %! assert_equal (R.v.Format, 'dd-MM-uuuu HH:mm'); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a duration in a unit format round-trips with that format %! u = hours ([1; 2]); %! T = table (u, 'VariableNames', {'v'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (isequaln (R.v, u), true); %! assert_equal (R.v.Format, 'h'); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a duration finer than a microsecond round-trips exactly %! u = seconds ([1.000000123456; 2]); %! T = table (u, 'VariableNames', {'v'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (seconds (R.v), [1.000000123456; 2]); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a duration truncated by its format keeps every component %! u = duration ([1, 2, 3; 4, 5, 6]); %! u.Format = 'hh:mm'; %! T = table (u, 'VariableNames', {'v'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (isequaln (R.v, u), true); %! assert_equal (R.v.Format, 'hh:mm'); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # 'WriteVariableNames' false leaves no names in the file %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn, 'WriteVariableNames', false); %! R = csv2table (fn); %! assert_equal (R.Properties.VariableNames, {'Var1', 'Var2'}); %! assert_equal (R.Var1, [1; 2]); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # 'WriteRowNames' false leaves the row labels out %! T = table ([1; 2], 'VariableNames', {'n'}); %! T.Properties.RowNames = {'r1', 'r2'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn, 'WriteRowNames', false); %! R = csv2table (fn); %! assert_equal (R.Properties.RowNames, {}); %! assert_equal (R.Properties.VariableNames, {'n'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # 'ReadVariableNames' false numbers the variables of a named file %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 's'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn, 'ReadVariableNames', false); %! assert_equal (R.Properties.VariableNames, {'Var1', 'Var2'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a char matrix round-trips with its padding %! T = table (['ab '; 'c '], 'VariableNames', {'v'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.v, ['ab '; 'c ']); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a missing variable round-trips as a missing variable %! T = table ([missing; missing], 'VariableNames', {'v'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.v), 'missing'); %! assert_equal (size (R.v), [2, 1]); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a missing string stays missing and an empty one stays empty %! T = table ([string('a'); string(missing); string('')], 'VariableNames', ... %! {'v'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (ismissing (R.v), [false; true; false]); %! assert_equal (char (R.v(3)), ''); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a blank field in a declared numeric column reads as NaN %! fn = [tempname() '.csv']; %! unwind_protect %! fid = fopen (fn, 'w'); %! fputs (fid, "\"# varTypes 1 rows; varNames 1 rows;"); %! fputs (fid, " varDescriptions 0 rows; varUnits 0 rows.\",\n"); %! fputs (fid, "\"double\",\"double\"\n\"n\",\"m\"\n1,\n2,4\n"); %! fclose (fid); %! R = csv2table (fn); %! assert_equal (R.m, [NaN; 4]); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a units row of blanks round-trips as a set property %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! T.Properties.VariableUnits = {'', ''}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.Properties.VariableUnits, {'', ''}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a nested table with no descriptions round-trips as unset %! T = table ([1; 2], table ([3; 4], [5; 6]), 'VariableNames', {'a', 'v'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.Properties.VariableDescriptions, {}); %! assert_equal (R.Properties.VariableUnits, {}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # an inner description leaves the outer property unset %! inner = table ([3; 4], [5; 6], 'VariableNames', {'p', 'q'}); %! inner.Properties.VariableDescriptions = {'inner p', 'inner q'}; %! T = table ([1; 2], inner, 'VariableNames', {'a', 'v'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.Properties.VariableDescriptions, {}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # an outer property set to blanks stays set beside a set inner one %! inner = table ([3; 4], [5; 6], 'VariableNames', {'p', 'q'}); %! inner.Properties.VariableDescriptions = {'inner p', 'inner q'}; %! T = table ([1; 2], inner, 'VariableNames', {'a', 'v'}); %! T.Properties.VariableDescriptions = {'', ''}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.Properties.VariableDescriptions, {'', ''}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # inner units leave the outer units unset %! inner = table ([3; 4], [5; 6], 'VariableNames', {'p', 'q'}); %! inner.Properties.VariableUnits = {'m', 's'}; %! T = table ([1; 2], inner, 'VariableNames', {'a', 'v'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.Properties.VariableUnits, {}); %! assert_equal (R.v.Properties.VariableUnits, {'m', 's'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a nested table's own description survives the round-trip %! inner = table ([3; 4], [5; 6]); %! inner.Properties.VariableDescriptions = {'inner one', ''}; %! T = table ([1; 2], 'VariableNames', {'a'}); %! T.v = inner; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.v.Properties.VariableDescriptions, {'inner one', ''}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # units that were never set round-trip as unset %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.Properties.VariableUnits, {}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## integer and single variables preserve their class through the vartype row %!test %! T = table (int8 ([1; 2]), single ([1.5; 2.5]), uint16 ([10; 20]), ... %! 'VariableNames', {'a', 'b', 'c'}); %! T.Properties.VariableDescriptions = {'sbyte', 'real', 'ushort'}; %! T.Properties.VariableUnits = {'au', 'au', 'au'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.a), 'int8'); %! assert_equal (class (R.b), 'single'); %! assert_equal (class (R.c), 'uint16'); %! assert_equal (R.a, int8 ([1; 2])); %! assert_equal (R.b, single ([1.5; 2.5])); %! assert_equal (R.Properties.VariableDescriptions, {'sbyte', 'real', ... %! 'ushort'}); %! assert_equal (R.Properties.VariableUnits, {'au', 'au', 'au'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a logical variable round-trips as a logical column %! T = table ([true; false; true], 'VariableNames', {'flag'}); %! T.Properties.VariableDescriptions = {'a flag'}; %! T.Properties.VariableUnits = {'bool'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.flag), 'logical'); %! assert_equal (R.flag, [true; false; true]); %! assert_equal (R.Properties.VariableDescriptions, {'a flag'}); %! assert_equal (R.Properties.VariableUnits, {'bool'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a multicolumn variable splits into same-named columns and is rejoined %! T = table ([1, 2; 3, 4], 'VariableNames', {'M'}); %! T.Properties.VariableDescriptions = {'a pair'}; %! T.Properties.VariableUnits = {'cm'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (width (R), 1); %! assert_equal (R.M, [1, 2; 3, 4]); %! assert_equal (R.Properties.VariableDescriptions, {'a pair'}); %! assert_equal (R.Properties.VariableUnits, {'cm'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## Infinities are written as Inf and -Inf and round-trip through csv2table %!test %! T = table ([Inf; -Inf; 2], 'VariableNames', {'v'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! txt = fileread (fn); %! assert_equal (! isempty (regexp (txt, '^Inf$', 'lineanchors', 'once')), ... %! true); %! assert_equal (! isempty (regexp (txt, '^-Inf$', 'lineanchors', ... %! 'once')), true); %! R = csv2table (fn); %! assert_equal (R.v, [Inf; -Inf; 2]); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## NA and NaN are written as distinct tokens and round-trip through csv2table %!test %! T = table ([1; NA; NaN], 'VariableNames', {'v'}); %! T.Properties.VariableDescriptions = {'measurement'}; %! T.Properties.VariableUnits = {'mm'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! txt = fileread (fn); %! assert_equal (! isempty (regexp (txt, '^NA$', 'lineanchors', 'once')), ... %! true); %! assert_equal (! isempty (regexp (txt, '^NaN$', 'lineanchors', 'once')), ... %! true); %! R = csv2table (fn); %! assert_equal (class (R.v), 'double'); %! assert_equal (R.v(1), 1); %! assert_equal (isna (R.v'), [false, true, false]); %! assert_equal (isnan (R.v'), [false, true, true]); %! assert_equal (R.Properties.VariableDescriptions, {'measurement'}); %! assert_equal (R.Properties.VariableUnits, {'mm'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a string variable round-trips as a string column %! T = table (string ({'a'; 'b'}), 'VariableNames', {'s'}); %! T.Properties.VariableDescriptions = {'a label'}; %! T.Properties.VariableUnits = {'txt'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.s), 'string'); %! assert_equal (cellstr (R.s), {'a'; 'b'}); %! assert_equal (R.Properties.VariableDescriptions, {'a label'}); %! assert_equal (R.Properties.VariableUnits, {'txt'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a datetime variable round-trips through its display strings %! T = table (datetime (2020, 1, [1; 2; 3]), 'VariableNames', {'d'}); %! T.Properties.VariableDescriptions = {'a date'}; %! T.Properties.VariableUnits = {'cal'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.d), 'datetime'); %! assert_equal (cellstr (R.d), ... %! {'01-Jan-2020'; '02-Jan-2020'; '03-Jan-2020'}); %! assert_equal (R.Properties.VariableDescriptions, {'a date'}); %! assert_equal (R.Properties.VariableUnits, {'cal'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a duration variable round-trips through its display strings %! T = table (duration ([1; 2], [30; 45], [0; 0]), 'VariableNames', {'du'}); %! T.Properties.VariableDescriptions = {'elapsed'}; %! T.Properties.VariableUnits = {'hms'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.du), 'duration'); %! assert_equal (cellstr (R.du), {'01:30:00'; '02:45:00'}); %! assert_equal (R.Properties.VariableDescriptions, {'elapsed'}); %! assert_equal (R.Properties.VariableUnits, {'hms'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## a calendarDuration is written as text; csv2table reads it back as cellstr ## (lossy) %!test %! T = table (calendarDuration ([1; 2], [0; 1], [0; 0]), 'VariableNames', ... %! {'cd'}); %! T.Properties.VariableDescriptions = {'span'}; %! T.Properties.VariableUnits = {'ymd'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! txt = fileread (fn); %! assert_equal (! isempty (regexp (txt, '^"calendarDuration"$', ... %! 'lineanchors', 'once')), true); %! assert_equal (! isempty (regexp (txt, '^"1y"$', 'lineanchors', ... %! 'once')), true); %! assert_equal (! isempty (regexp (txt, '^"2y 1mo"$', 'lineanchors', ... %! 'once')), true); %! warning ('off', 'all', 'local'); %! R = csv2table (fn); %! assert_equal (R.cd, {'1y'; '2y 1mo'}); %! assert_equal (R.Properties.VariableDescriptions, {'span'}); %! assert_equal (R.Properties.VariableUnits, {'ymd'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## Test warning here %!warning ... %! T = table (calendarDuration ([1; 2], [0; 1], [0; 0]), 'VariableNames', ... %! {'cd'}); %! T.Properties.VariableDescriptions = {'span'}; %! T.Properties.VariableUnits = {'ymd'}; %! fn = [tempname() '.csv']; %! table2csv (T, fn); %! R = csv2table (fn); ## a categorical is written as labels; csv2table reads it back as cellstr ## (lossy) %!test %! T = table (categorical ({'lo'; 'hi'; 'lo'}), 'VariableNames', {'c'}); %! T.Properties.VariableDescriptions = {'level'}; %! T.Properties.VariableUnits = {'cat'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! txt = fileread (fn); %! assert_equal (! isempty (regexp (txt, '^"categorical"$', 'lineanchors', ... %! 'once')), true); %! assert_equal (! isempty (regexp (txt, '^"lo"$', 'lineanchors', ... %! 'once')), true); %! assert_equal (! isempty (regexp (txt, '^"hi"$', 'lineanchors', ... %! 'once')), true); %! warning ('off', 'all', 'local'); %! R = csv2table (fn); %! assert_equal (R.c, {'lo'; 'hi'; 'lo'}); %! assert_equal (R.Properties.VariableDescriptions, {'level'}); %! assert_equal (R.Properties.VariableUnits, {'cat'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## Test warning here %!warning ... %! T = table (categorical ({'lo'; 'hi'; 'lo'}), 'VariableNames', {'c'}); %! T.Properties.VariableDescriptions = {'level'}; %! T.Properties.VariableUnits = {'cat'}; %! fn = [tempname() '.csv']; %! table2csv (T, fn); %! R = csv2table (fn); %!test # row names are written under a 'RowNames' column and restored %! T = table ([1; 2], 'VariableNames', {'v'}, 'RowNames', {'r1'; 'r2'}); %! T.Properties.VariableDescriptions = {'a value'}; %! T.Properties.VariableUnits = {'kg'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.Properties.RowNames, {'r1'; 'r2'}); %! assert_equal (R.v, [1; 2]); %! assert_equal (R.Properties.VariableDescriptions, {'a value'}); %! assert_equal (R.Properties.VariableUnits, {'kg'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## a nested table is split with two vartype rows and rebuilt on round-trip %!test %! nt = table ([10; 20], [30; 40], 'VariableNames', {'p', 'q'}); %! T = table ([1; 2], nt, 'VariableNames', {'id', 'nest'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! assert_equal (! isempty (strfind (fileread (fn), 'varTypes 2 rows')), ... %! true); %! R = csv2table (fn); %! assert_equal (class (R.nest), 'table'); %! assert_equal (R.nest.p, [10; 20]); %! assert_equal (R.nest.q, [30; 40]); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # descriptions and units of a nested table round-trip on both levels %! nt = table ([10; 20], [30; 40], 'VariableNames', {'p', 'q'}); %! nt.Properties.VariableDescriptions = {'inner p', 'inner q'}; %! nt.Properties.VariableUnits = {'mm', 'cm'}; %! T = table ([1; 2], nt, 'VariableNames', {'id', 'nest'}); %! T.Properties.VariableDescriptions = {'an id', 'a nest'}; %! T.Properties.VariableUnits = {'#', 'box'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! txt = fileread (fn); %! assert_equal (! isempty (strfind (txt, ... %! 'varDescriptions 2 rows; varUnits 2 rows')), true); %! R = csv2table (fn); %! assert_equal (R.Properties.VariableDescriptions, {'an id', 'a nest'}); %! assert_equal (R.Properties.VariableUnits, {'#', 'box'}); %! assert_equal (R.nest.Properties.VariableDescriptions, {'inner p', ... %! 'inner q'}); %! assert_equal (R.nest.Properties.VariableUnits, {'mm', 'cm'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a struct variable is split per field and rebuilt on round-trip %! sv = struct ('x', {1; 2}, 'y', {3; 4}); %! T = table ([1; 2], sv, 'VariableNames', {'id', 's'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! s = R.s; %! assert_equal (class (s), 'struct'); %! assert_equal ([s.x], [1, 2]); %! assert_equal ([s.y], [3, 4]); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # the top-level description and units of a struct variable round-trip %! sv = struct ('x', {1; 2}, 'y', {3; 4}); %! T = table ([1; 2], sv, 'VariableNames', {'id', 's'}); %! T.Properties.VariableDescriptions = {'an id', 'a struct'}; %! T.Properties.VariableUnits = {'#', 'rec'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.Properties.VariableDescriptions, {'an id', 'a struct'}); %! assert_equal (R.Properties.VariableUnits, {'#', 'rec'}); %! assert_equal (class (R.s), 'struct'); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # descriptions are written when any variable carries one (partial) %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! T.Properties.VariableDescriptions = {'has one', ''}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! assert_equal (! isempty (strfind (fileread (fn), ... %! 'varDescriptions 1 rows')), true); %! R = csv2table (fn); %! assert_equal (R.Properties.VariableDescriptions, {'has one', ''}); %! assert_equal (R.a, [1; 2]); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # partial units round-trip through csv (any variable, not all) %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! T.Properties.VariableUnits = {'', 'kg'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.Properties.VariableUnits, {'', 'kg'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect %!test # a zone-aware datetime keeps its TimeZone through csv %! dt = datetime (2024, 6, [15; 16], 10, 30, 0, 'TimeZone', 'America/New_York'); %! T = table (dt, 'VariableNames', {'t'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.t.TimeZone, 'America/New_York'); %! assert_equal (cellstr (char (R.t)), cellstr (char (dt))); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect %!test # variable descriptions and units are written to the header and restored %! T = table ([1; 2], 'VariableNames', {'v'}); %! T.Properties.VariableDescriptions = {'mass'}; %! T.Properties.VariableUnits = {'kg'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! txt = fileread (fn); %! assert_equal (! isempty (strfind (txt, ... %! 'varDescriptions 1 rows; varUnits 1 rows')), true); %! assert_equal (! isempty (regexp (txt, '^"mass"$', 'lineanchors', ... %! 'once')), true); %! assert_equal (! isempty (regexp (txt, '^"kg"$', 'lineanchors', ... %! 'once')), true); %! R = csv2table (fn); %! assert_equal (R.v, [1; 2]); %! assert_equal (R.Properties.VariableDescriptions, {'mass'}); %! assert_equal (R.Properties.VariableUnits, {'kg'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # FILE accepts a char vector, a cellstr, and a string scalar %! T = table ([1; 2], 'VariableNames', {'v'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! assert_equal (exist (fn, 'file') > 0, true); %! delete (fn); %! table2csv (T, {fn}); %! assert_equal (exist (fn, 'file') > 0, true); %! delete (fn); %! table2csv (T, string (fn)); %! assert_equal (exist (fn, 'file') > 0, true); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## a complex table of many variable types round-trips with descriptions and ## units %!test %! T = table ([1; 2; 3], single ([1.5; 2.5; 3.5]), int16 ([10; 20; 30]), ... %! [true; false; true], string ({'x'; 'y'; 'z'}), ... %! datetime (2021, 3, [4; 5; 6]), ... %! duration ([1; 2; 3], [0; 30; 0], [0; 0; 0]), ... %! categorical ({'lo'; 'hi'; 'lo'}), [1, 2; 3, 4; 5, 6], ... %! 'VariableNames', ... %! {'id', 'score', 'cnt', 'ok', 'name', 'when', 'dur', 'grade', ... %! 'bp'}); %! T.Properties.VariableDescriptions = ... %! {'identifier', 'a score', 'a count', 'flag', 'a name', 'a date', ... %! 'elapsed', 'level', 'pair'}; %! T.Properties.VariableUnits = ... %! {'#', 'pts', 'n', 'bool', 'txt', 'cal', 'hms', 'cat', 'cm'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! warning ('off', 'all', 'local'); %! R = csv2table (fn); %! assert_equal (width (R), 9); %! assert_equal (R.id, [1; 2; 3]); %! assert_equal (class (R.score), 'single'); %! assert_equal (R.score, single ([1.5; 2.5; 3.5])); %! assert_equal (class (R.cnt), 'int16'); %! assert_equal (R.cnt, int16 ([10; 20; 30])); %! assert_equal (R.ok, [true; false; true]); %! assert_equal (class (R.name), 'string'); %! assert_equal (cellstr (R.name), {'x'; 'y'; 'z'}); %! assert_equal (class (R.when), 'datetime'); %! assert_equal (cellstr (R.when), ... %! {'04-Mar-2021'; '05-Mar-2021'; '06-Mar-2021'}); %! assert_equal (class (R.dur), 'duration'); %! assert_equal (cellstr (R.dur), {'01:00:00'; '02:30:00'; '03:00:00'}); %! assert_equal (R.grade, {'lo'; 'hi'; 'lo'}); %! assert_equal (R.bp, [1, 2; 3, 4; 5, 6]); %! assert_equal (R.Properties.VariableNames, ... %! {'id', 'score', 'cnt', 'ok', 'name', 'when', 'dur', ... %! 'grade', 'bp'}); %! assert_equal (R.Properties.VariableDescriptions, ... %! {'identifier', 'a score', 'a count', 'flag', 'a name', ... %! 'a date', 'elapsed', 'level', 'pair'}); %! assert_equal (R.Properties.VariableUnits, ... %! {'#', 'pts', 'n', 'bool', 'txt', 'cal', 'hms', 'cat', 'cm'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## a complex table with row names round-trips data, row names, descriptions, and ## units %!test %! T = table ([10; 20], string ({'aa'; 'bb'}), datetime (2022, 6, [1; 2]), ... %! 'VariableNames', {'n', 'tag', 'day'}, ... %! 'RowNames', {'first'; 'second'}); %! T.Properties.VariableDescriptions = {'a number', 'a tag', 'a day'}; %! T.Properties.VariableUnits = {'u1', 'u2', 'u3'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.Properties.RowNames, {'first'; 'second'}); %! assert_equal (R.n, [10; 20]); %! assert_equal (cellstr (R.tag), {'aa'; 'bb'}); %! assert_equal (class (R.day), 'datetime'); %! assert_equal (cellstr (R.day), {'01-Jun-2022'; '02-Jun-2022'}); %! assert_equal (R.Properties.VariableDescriptions, {'a number', 'a tag', ... %! 'a day'}); %! assert_equal (R.Properties.VariableUnits, {'u1', 'u2', 'u3'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## every integer class (full range, exact) and the float classes round-trip %!test %! T = table (int8 ([intmin('int8'); intmax('int8')]), ... %! int16 ([intmin('int16'); intmax('int16')]), ... %! int32 ([intmin('int32'); intmax('int32')]), ... %! int64 ([intmin('int64'); intmax('int64')]), uint8 ([0; intmax('uint8')]), ... %! uint16 ([0; intmax('uint16')]), uint32 ([0; intmax('uint32')]), ... %! uint64 ([0; intmax('uint64')]), single ([1.5; -2.5]), [1.25; -3.75], ... %! 'VariableNames', {'i8', 'i16', 'i32', 'i64', 'u8', 'u16', 'u32', 'u64', ... %! 'sg', 'db'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.i8), 'int8'); %! assert_equal (class (R.i16), 'int16'); %! assert_equal (class (R.i32), 'int32'); %! assert_equal (class (R.i64), 'int64'); %! assert_equal (class (R.u8), 'uint8'); %! assert_equal (class (R.u16), 'uint16'); %! assert_equal (class (R.u32), 'uint32'); %! assert_equal (class (R.u64), 'uint64'); %! assert_equal (class (R.sg), 'single'); %! assert_equal (class (R.db), 'double'); %! assert_equal (R.i8, int8 ([intmin('int8'); intmax('int8')])); %! assert_equal (R.i32, int32 ([intmin('int32'); intmax('int32')])); %! assert_equal (R.i64, int64 ([intmin('int64'); intmax('int64')])); %! assert_equal (R.u32, uint32 ([0; intmax('uint32')])); %! assert_equal (R.u64, uint64 ([0; intmax('uint64')])); %! assert_equal (R.sg, single ([1.5; -2.5])); %! assert_equal (R.db, [1.25; -3.75]); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a multicolumn string variable splits and rejoins as a 2-D string %! T = table (string ({'a', 'b'; 'c', 'd'}), 'VariableNames', {'s'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.s), 'string'); %! assert_equal (size (R.s), [2, 2]); %! assert_equal (cellstr (R.s), {'a', 'b'; 'c', 'd'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a multicolumn datetime variable round-trips as a 2-D datetime %! T = table ([datetime(2020, 1, [1; 2]), datetime(2021, 1, [1; 2])], ... %! 'VariableNames', {'d'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.d), 'datetime'); %! assert_equal (size (R.d), [2, 2]); %! assert_equal (cellstr (R.d(:, 1)), {'01-Jan-2020'; '02-Jan-2020'}); %! assert_equal (cellstr (R.d(:, 2)), {'01-Jan-2021'; '02-Jan-2021'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a multicolumn duration variable round-trips as a 2-D duration %! T = table ([duration(1, 0, 0) .* [1; 2], duration(2, 0, 0) .* [1; 2]], ... %! 'VariableNames', {'du'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.du), 'duration'); %! assert_equal (size (R.du), [2, 2]); %! assert_equal (cellstr (R.du(:, 1)), {'01:00:00'; '02:00:00'}); %! assert_equal (cellstr (R.du(:, 2)), {'02:00:00'; '04:00:00'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a multicolumn logical variable round-trips as a 2-D logical %! T = table (logical ([1, 0; 0, 1]), 'VariableNames', {'L'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.L), 'logical'); %! assert_equal (R.L, logical ([1, 0; 0, 1])); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a multicolumn integer variable preserves its class and shape %! T = table (int16 ([10, 20; 30, 40]), 'VariableNames', {'M'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.M), 'int16'); %! assert_equal (R.M, int16 ([10, 20; 30, 40])); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## a multicolumn categorical is written as labels and read back as 2-D cellstr ## (lossy) %!test %! T = table (categorical ({'x', 'y'; 'y', 'x'}), 'VariableNames', {'c'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! warning ('off', 'all', 'local'); %! R = csv2table (fn); %! assert_equal (R.c, {'x', 'y'; 'y', 'x'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## a datetime variable with a NaT round-trips, restoring the missing entry %!test %! T = table ([datetime(2020, 1, 1); NaT; datetime(2020, 1, 3)], ... %! 'VariableNames', {'d'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.d), 'datetime'); %! assert_equal (isnat (R.d'), [false, true, false]); %! assert_equal (cellstr (R.d([1, 3])), {'01-Jan-2020'; '03-Jan-2020'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## a duration variable with a NaN round-trips, restoring the missing entry %!test %! T = table (duration ([1; NaN; 3], [0; 0; 0], [0; 0; 0]), 'VariableNames', ... %! {'du'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.du), 'duration'); %! assert_equal (isnan (R.du'), [false, true, false]); %! assert_equal (cellstr (R.du([1, 3])), {'01:00:00'; '03:00:00'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## a missing string value is written as an empty field and read back as "" %!test %! T = table (string ({'a'; missing; 'c'}), 'VariableNames', {'s'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.s), 'string'); %! assert_equal (cellstr (R.s), {'a'; ''; 'c'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # text with embedded commas, double quotes, and padding survives quoting %! T = table ({'a,b'; 'he said "hi"'; ' pad '; 'plain'}, 'VariableNames', ... %! {'s'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! assert_equal (! isempty (strfind (fileread (fn), '"he said ""hi"""')), ... %! true); %! R = csv2table (fn); %! assert_equal (R.s, {'a,b'; 'he said "hi"'; ' pad '; 'plain'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a nested table of special types round-trips (categorical is lossy) %! inner = table (datetime(2020, 1, [1; 2]), duration ([1; 2], [0; 0], ... %! [0; 0]), ... %! string ({'p'; 'q'}), categorical ({'lo'; 'hi'}), ... %! 'VariableNames', {'dt', 'du', 'st', 'ct'}); %! T = table ([1; 2], inner, 'VariableNames', {'id', 'nest'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! warning ('off', 'all', 'local'); %! R = csv2table (fn); %! assert_equal (class (R.nest), 'table'); %! assert_equal (class (R.nest.dt), 'datetime'); %! assert_equal (cellstr (R.nest.dt), {'01-Jan-2020'; '02-Jan-2020'}); %! assert_equal (class (R.nest.du), 'duration'); %! assert_equal (cellstr (R.nest.du), {'01:00:00'; '02:00:00'}); %! assert_equal (class (R.nest.st), 'string'); %! assert_equal (cellstr (R.nest.st), {'p'; 'q'}); %! assert_equal (R.nest.ct, {'lo'; 'hi'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a nested table with a multicolumn inner variable round-trips %! inner = table ([1, 2; 3, 4], {'a'; 'b'}, 'VariableNames', {'mm', 'lab'}); %! T = table ([9; 8], inner, 'VariableNames', {'id', 'nest'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (R.nest.mm, [1, 2; 3, 4]); %! assert_equal (R.nest.lab, {'a'; 'b'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## a table mixing multicolumn, nested, and several types round-trips with ## metadata %!test %! inner = table (duration ([1; 2], [0; 0], [0; 0]), string ({'p'; 'q'}), ... %! categorical ({'lo'; 'hi'}), 'VariableNames', {'idu', ... %! 'ist', 'ict'}); %! inner.Properties.VariableDescriptions = {'inner dur', 'inner str', ... %! 'inner cat'}; %! inner.Properties.VariableUnits = {'h', 't', 'c'}; %! T = table ([duration(1, 0, 0) .* [1; 2], duration(2, 0, 0) .* [1; 2]], ... %! string ({'aa'; 'bb'}), categorical ({'x'; 'y'}), ... %! [10, 11, 12; 20, 21, 22], inner, ... %! 'VariableNames', {'mdur', 'tag', 'grade', 'mat', 'nest'}); %! T.Properties.VariableDescriptions = {'mc dur', 'a tag', 'a grade', ... %! 'a matrix', 'nested'}; %! T.Properties.VariableUnits = {'h', 't', 'g', 'm', 'n'}; %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! warning ('off', 'all', 'local'); %! R = csv2table (fn); %! assert_equal (R.Properties.VariableNames, {'mdur', 'tag', 'grade', ... %! 'mat', 'nest'}); %! assert_equal (class (R.mdur), 'duration'); %! assert_equal (size (R.mdur), [2, 2]); %! assert_equal (cellstr (R.tag), {'aa'; 'bb'}); %! assert_equal (R.grade, {'x'; 'y'}); %! assert_equal (R.mat, [10, 11, 12; 20, 21, 22]); %! assert_equal (class (R.nest), 'table'); %! assert_equal (cellstr (R.nest.idu), {'01:00:00'; '02:00:00'}); %! assert_equal (cellstr (R.nest.ist), {'p'; 'q'}); %! assert_equal (R.nest.ict, {'lo'; 'hi'}); %! assert_equal (R.Properties.VariableDescriptions, ... %! {'mc dur', 'a tag', 'a grade', 'a matrix', 'nested'}); %! assert_equal (R.Properties.VariableUnits, {'h', 't', 'g', 'm', 'n'}); %! assert_equal (R.nest.Properties.VariableDescriptions, {'inner dur', ... %! 'inner str', 'inner cat'}); %! assert_equal (R.nest.Properties.VariableUnits, {'h', 't', 'c'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## full-range 64-bit integers round-trip exactly (written/read without double) %!test %! ii = [intmin('int64'); intmin('int64') + 1; intmax('int64')]; %! uu = [uint64(0); intmax('uint64') - uint64(1); intmax('uint64')]; %! T = table (ii, uu, 'VariableNames', {'i', 'u'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! assert_equal (! isempty (strfind (fileread (fn), ... %! '18446744073709551615')), true); %! R = csv2table (fn); %! assert_equal (R.i, ii); %! assert_equal (R.u, uu); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a multicolumn 64-bit integer variable with extreme values round-trips %! M = int64 ([intmax('int64'), 1; -5, intmin('int64')]); %! T = table (M, 'VariableNames', {'M'}); %! fn = [tempname() '.csv']; %! unwind_protect %! table2csv (T, fn); %! R = csv2table (fn); %! assert_equal (class (R.M), 'int64'); %! assert_equal (R.M, M); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## Test input validation for 'table2csv' method %!error ... %! table2csv (table (1), fullfile (tempname (), 'x.csv')); ################################################################################ ## ** Save to Files ** ## ################################################################################ ## Available Methods ## ## ## ## 'table2ods' ## ## ## ################################################################################ ## Test 'table2ods' method (round-trips through 'ods2table') %!test # datetime and duration are written as native ODS date/time cells %! T = table (datetime (2024, 1, 15, 10, 30, 15), duration (25, 30, 0), ... %! 'VariableNames', {'d', 'e'}); %! fn = [tempname() '.fods']; %! unwind_protect %! table2ods (T, fn); %! txt = fileread (fn); %! assert_equal (! isempty (strfind (txt, 'office:value-type="date"')), true); %! assert_equal (! isempty (strfind (txt, ... %! 'office:date-value="2024-01-15T10:30:15"')), true); %! assert_equal (! isempty (strfind (txt, 'office:value-type="time"')), true); %! assert_equal (! isempty (strfind (txt, ... %! 'office:time-value="PT25H30M00S"')), true); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a full-range int64 is written with its exact digits %! T = table (int64 (9223372036854775807), 'VariableNames', {'i'}); %! fn = [tempname() '.fods']; %! unwind_protect %! table2ods (T, fn); %! txt = fileread (fn); %! assert_equal (! isempty (strfind (txt, '9223372036854775807')), true); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # the hidden metadata sheet records the variable types and names %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'v', 'g'}); %! fn = [tempname() '.fods']; %! unwind_protect %! table2ods (T, fn); %! txt = fileread (fn); %! assert_equal (! isempty (strfind (txt, '__datatypes_meta__')), true); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## a compressed '.ods' is a ZIP whose contents round-trip through ods2table %!test %! T = table ([1; 2; 3], datetime (2024, 1, [1; 2; 3]), {'a'; 'b'; 'c'}, ... %! 'VariableNames', {'n', 'd', 'g'}); %! fn = [tempname() '.ods']; %! unwind_protect %! table2ods (T, fn); %! R = ods2table (fn); %! assert_equal (R.Properties.VariableNames, {'n', 'd', 'g'}); %! assert_equal (R.n, [1; 2; 3]); %! assert_equal (R.g, {'a'; 'b'; 'c'}); %! assert_equal (class (R.d), 'datetime'); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # the ODS package leads with a compliant stored 'mimetype' entry %! T = table ([1; 2; 3], 'VariableNames', {'n'}); %! fn = [tempname() '.ods']; %! unwind_protect %! table2ods (T, fn); %! fid = fopen (fn, 'r'); hdr = fread (fid, 38, 'uint8')'; fclose (fid); %! flag = hdr(7) + hdr(8) * 256; # general-purpose bit flag %! method = hdr(9) + hdr(10) * 256; # compression method %! namelen = hdr(27) + hdr(28) * 256; %! assert_equal (hdr(1:4), [80, 75, 3, 4]); # ZIP magic "PK\x03\x04" %! assert_equal (method, 0); # stored, not deflated %! ## no data descriptor / extras %! assert_equal (flag, 0); %! assert_equal (char (hdr(31:30+namelen)), 'mimetype'); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## Incremental 'table2ods': build a multi-sheet workbook one table at a time, ## read back through 'ods2struct' %!test %! A = table ([1; 2; 3], 'VariableNames', {'a'}); %! B = table ({'x'; 'y'}, 'VariableNames', {'b'}); %! C = table ([9; 8], 'VariableNames', {'c'}); %! fn = [tempname() '.ods']; %! unwind_protect %! table2ods (A, fn, 'Sheet', 'first'); %! table2ods (B, fn, 'Sheet', 'second'); %! table2ods (C, fn, 'Sheet', 'third'); %! r = ods2struct (fn); %! assert_equal (fieldnames (r), {'first'; 'second'; 'third'}); %! assert_equal (r.first.a, [1; 2; 3]); %! assert_equal (r.second.b, {'x'; 'y'}); %! assert_equal (r.third.c, [9; 8]); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## Incremental 'table2ods' 'append' adds rows to a sheet, others preserved %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = table ([3; 4; 5], 'VariableNames', {'b'}); %! more = table ([6; 7], 'VariableNames', {'a'}); %! fn = [tempname() '.fods']; %! unwind_protect %! table2ods (A, fn, 'Sheet', 'first'); %! table2ods (B, fn, 'Sheet', 'second'); %! table2ods (more, fn, 'Sheet', 'first', 'WriteMode', 'append'); %! r = ods2struct (fn); %! assert_equal (r.first.a, [1; 2; 6; 7]); %! assert_equal (r.second.b, [3; 4; 5]); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## Incremental 'table2ods' 'overwritesheet' replaces one sheet, keeps the rest; ## native types survive the read-modify-rewrite %!test %! A = table (datetime (2024, 1, [1; 2]), 'VariableNames', {'d'}); %! B = table ([3; 4; 5], 'VariableNames', {'b'}); %! repl = table (seconds ([10; 20]), 'VariableNames', {'t'}); %! fn = [tempname() '.ods']; %! unwind_protect %! table2ods (A, fn, 'Sheet', 'first'); %! table2ods (B, fn, 'Sheet', 'second'); %! table2ods (repl, fn, 'Sheet', 'first', 'WriteMode', 'overwritesheet'); %! r = ods2struct (fn); %! assert_equal (class (r.first.t), 'duration'); %! assert_equal (isequaln (seconds (r.first.t), [10; 20]), true); %! assert_equal (r.second.b, [3; 4; 5]); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## 'table2ods' 'replacefile' discards the existing workbook %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = table ([3; 4], 'VariableNames', {'b'}); %! fn = [tempname() '.ods']; %! unwind_protect %! table2ods (A, fn, 'Sheet', 'first'); %! table2ods (B, fn, 'Sheet', 'second', 'WriteMode', 'replacefile'); %! [~, ~, ~, nm] = __ods2table__ (fn); %! assert_equal (nm(:), {'second'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## Test input validation for 'table2ods' method %!error ... %! table2ods (table (1), [tempname() '.ods'], 'WriteMode', 'bogus'); %!error ... %! table2ods (table (1), [tempname() '.ods'], 'Bogus', 1); %!error ... %! table2ods (table (1), [tempname() '.ods'], 'Bogus'); %!error ... %! table2csv (table (1), [tempname() '.csv'], 'WriteVariableNames', 'yes'); %!error ... %! table2csv (table (1), [tempname() '.csv'], 'WriteRowNames', 1); %!error ... %! table2csv (table (1), [tempname() '.csv'], 'Bogus', 1); %!error ... %! table2csv (table (1), [tempname() '.csv'], 'Bogus'); %!error ... %! table2csv (table (inputParser ()), [tempname() '.csv']); %!error ... %! table2ods (table (inputParser ()), [tempname() '.ods']); %!error ... %! table2ods (table (1), [tempname() '.ods'], 'WriteVariableNames', 'yes'); %!error ... %! table2ods (table (1), [tempname() '.ods'], 'WriteRowNames', 1); %!error ... %! table2ods (table (1), [tempname() '.txt']); ################################################################################ ## ** Save to Files ** ## ################################################################################ ## Available Methods ## ## ## ## 'writetable' ## ## ## ################################################################################ ## Test 'writetable' method (round-trips through 'readtable') %!test # text round-trip: names header, numeric/text detection %! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, [1.5; 2.5; 3.5], ... %! 'VariableNames', {'id', 'name', 'val'}); %! fn = [tempname() '.csv']; %! unwind_protect %! writetable (T, fn); %! R = readtable (fn); %! assert_equal (R.Properties.VariableNames, {'id', 'name', 'val'}); %! assert_equal (R.id, [1; 2; 3]); %! assert_equal (R.name, {'a'; 'b'; 'c'}); %! assert_equal (R.val, [1.5; 2.5; 3.5]); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # text: no type metadata is written, only a names row then data %! T = table ([1; 2], {'x'; 'y'}, 'VariableNames', {'a', 'b'}); %! fn = [tempname() '.csv']; %! unwind_protect %! writetable (T, fn); %! txt = fileread (fn); %! assert_equal (! isempty (regexp (txt, '^a,b$', 'lineanchors', 'once')), ... %! true); %! assert_equal (isempty (strfind (txt, 'varTypes')), true); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # 'WriteVariableNames' false omits the header row %! T = table ([1; 2], 'VariableNames', {'a'}); %! fn = [tempname() '.csv']; %! unwind_protect %! writetable (T, fn, 'WriteVariableNames', false); %! txt = fileread (fn); %! assert_equal (isempty (strfind (txt, 'a')), true); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # 'Delimiter' and 'QuoteStrings' control the text output %! T = table ({'x,y'; 'z'}, 'VariableNames', {'s'}); %! fn = [tempname() '.csv']; %! unwind_protect %! writetable (T, fn, 'Delimiter', 'semi', 'QuoteStrings', 'none'); %! txt = fileread (fn); %! assert_equal (! isempty (strfind (txt, 'x,y')), true); %! assert_equal (isempty (strfind (txt, '"')), true); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # 'WriteRowNames' writes the row names as a leading column %! T = table ([10; 20], 'VariableNames', {'v'}, 'RowNames', {'r1', 'r2'}); %! fn = [tempname() '.csv']; %! unwind_protect %! writetable (T, fn, 'WriteRowNames', true); %! R = readtable (fn, 'ReadRowNames', true); %! assert_equal (R.Properties.RowNames, {'r1'; 'r2'}); %! assert_equal (R.v, [10; 20]); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a multicolumn variable is split into name_1, name_2, ... %! T = table ([1 2; 3 4], {'p'; 'q'}, 'VariableNames', {'mat', 'tag'}); %! fn = [tempname() '.csv']; %! unwind_protect %! writetable (T, fn); %! R = readtable (fn); %! assert_equal (R.Properties.VariableNames, {'mat_1', 'mat_2', 'tag'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # spreadsheet round-trip preserves native datetime/duration types %! T = table ([1; 2], datetime (2024, 1, [1; 2]), seconds ([30; 90]), ... %! 'VariableNames', {'a', 'd', 'e'}); %! fn = [tempname() '.ods']; %! unwind_protect %! writetable (T, fn); %! R = readtable (fn); %! assert_equal (R.a, [1; 2]); %! assert_equal (class (R.d), 'datetime'); %! assert_equal (class (R.e), 'duration'); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a writetable '.ods' exposes a visible names row (read back as names) %! T = table ([1; 2], {'x'; 'y'}, 'VariableNames', {'aa', 'bb'}); %! fn = [tempname() '.ods']; %! unwind_protect %! writetable (T, fn); %! R = readtable (fn); %! assert_equal (R.Properties.VariableNames, {'aa', 'bb'}); %! assert_equal (R.aa, [1; 2]); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## 'writetable' to a named sheet, read back by that name %!test %! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'x', 'y'}); %! fn = [tempname() '.ods']; %! unwind_protect %! writetable (T, fn, 'Sheet', 'MySheet'); %! R = readtable (fn, 'Sheet', 'MySheet'); %! assert_equal (R.Properties.VariableNames, {'x', 'y'}); %! assert_equal (R.x, [1; 2; 3]); %! assert_equal (R.y, [10; 20; 30]); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## 'writetable' 'Range' anchors the block; readtable auto-trims to it, and an ## explicit read range recovers the same data. %!test %! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'x', 'y'}); %! fn = [tempname() '.ods']; %! unwind_protect %! writetable (T, fn, 'Range', 'C5'); %! R = readtable (fn); %! assert_equal (R.Properties.VariableNames, {'x', 'y'}); %! assert_equal (R.x, [1; 2; 3]); %! R2 = readtable (fn, 'Range', 'C5:D8'); %! assert_equal (R2.x, [1; 2; 3]); %! assert_equal (R2.y, [10; 20; 30]); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## 'writetable' flat '.fods' with a 'Range' offset round-trips %!test %! T = table ([5; 6], [50; 60], 'VariableNames', {'a', 'b'}); %! fn = [tempname() '.fods']; %! unwind_protect %! writetable (T, fn, 'Range', 'B3'); %! R = readtable (fn); %! assert_equal (R.a, [5; 6]); %! assert_equal (R.b, [50; 60]); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## text 'WriteMode' 'append' adds rows below without repeating the header %!test %! T = table ([1; 2], [10; 20], 'VariableNames', {'x', 'y'}); %! T2 = table ([3; 4], [30; 40], 'VariableNames', {'x', 'y'}); %! fn = [tempname() '.csv']; %! unwind_protect %! writetable (T, fn); %! writetable (T2, fn, 'WriteMode', 'append'); %! R = readtable (fn); %! assert_equal (R.x, [1; 2; 3; 4]); %! assert_equal (R.y, [10; 20; 30; 40]); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## 'writetable' to a new sheet of an existing workbook preserves other sheets %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = table ([3; 4; 5], 'VariableNames', {'b'}); %! fn = [tempname() '.ods']; %! unwind_protect %! writetable (A, fn, 'Sheet', 'first'); %! writetable (B, fn, 'Sheet', 'second'); %! assert_equal (readtable (fn, 'Sheet', 'first').a, [1; 2]); %! assert_equal (readtable (fn, 'Sheet', 'second').b, [3; 4; 5]); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## 'WriteMode' 'append' adds rows to an existing sheet; other sheets untouched %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = table ([3; 4; 5], 'VariableNames', {'b'}); %! more = table ([6; 7], 'VariableNames', {'a'}); %! fn = [tempname() '.ods']; %! unwind_protect %! writetable (A, fn, 'Sheet', 'first'); %! writetable (B, fn, 'Sheet', 'second'); %! writetable (more, fn, 'Sheet', 'first', 'WriteMode', 'append'); %! assert_equal (readtable (fn, 'Sheet', 'first').a, [1; 2; 6; 7]); %! assert_equal (readtable (fn, 'Sheet', 'second').b, [3; 4; 5]); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## 'WriteMode' 'overwritesheet' replaces one sheet, preserving the others %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = table ([3; 4; 5], 'VariableNames', {'b'}); %! repl = table ([9], 'VariableNames', {'z'}); %! fn = [tempname() '.fods']; %! unwind_protect %! writetable (A, fn, 'Sheet', 'first'); %! writetable (B, fn, 'Sheet', 'second'); %! writetable (repl, fn, 'Sheet', 'first', 'WriteMode', 'overwritesheet'); %! R = readtable (fn, 'Sheet', 'first'); %! assert_equal (R.Properties.VariableNames, {'z'}); %! assert_equal (R.z, 9); %! assert_equal (readtable (fn, 'Sheet', 'second').b, [3; 4; 5]); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## Writing to an existing workbook with no 'Sheet' targets the first sheet %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = table ([3; 4; 5], 'VariableNames', {'b'}); %! Z = table ([9; 9], 'VariableNames', {'z'}); %! fn = [tempname() '.ods']; %! unwind_protect %! writetable (A, fn, 'Sheet', 'first'); %! writetable (B, fn, 'Sheet', 'second'); %! writetable (Z, fn); # no 'Sheet' -> first sheet %! [~, ~, ~, nm] = __ods2table__ (fn); %! assert_equal (nm(:), {'first'; 'second'}); # no stray 'Sheet1' added %! assert_equal (readtable (fn, 'Sheet', ... %! 'first').Properties.VariableNames, {'z'}); %! assert_equal (readtable (fn, 'Sheet', 'second').b, [3; 4; 5]); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## 'WriteMode' 'replacefile' overwrites the whole workbook %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = table ([3; 4; 5], 'VariableNames', {'b'}); %! fn = [tempname() '.ods']; %! unwind_protect %! writetable (A, fn, 'Sheet', 'first'); %! writetable (B, fn, 'Sheet', 'second', 'WriteMode', 'replacefile'); %! [~, ~, ~, nm] = __ods2table__ (fn); %! assert_equal (nm(:), {'second'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## Test input validation for 'writetable' method %!error ... %! inner = table ([1; 2], 'VariableNames', {'a'}); ... %! T = table (inner, [3; 4], 'VariableNames', {'nested', 'x'}); ... %! writetable (T, [tempname() '.csv']); %!error ... %! writetable (table (1), [tempname() '.xls']); %!error ... %! writetable (table (1), [tempname() '.csv'], 'Sheet', 'S1'); %!error ... %! writetable (table (1), [tempname() '.csv'], 'Range', 'A1'); %!error ... %! writetable (table (1), [tempname() '.csv'], 'WriteMode', 'inplace'); %!error ... %! writetable (table (1), [tempname() '.ods'], 'WriteMode', 'bogus'); %!error ... %! writetable (table (1), [tempname() '.ods'], 'WriteMode', 'append', ... %! 'Range', 'B2'); %!error ... %! writetable (table (1), [tempname() '.csv'], 'Bogus', 1); %!error ... %! writetable (table (1), [tempname() '.csv'], 'Bogus'); ## '.xlsx' write into an existing workbook adds/preserves sheets and appends %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = table ([3; 4; 5], 'VariableNames', {'b'}); %! more = table ([6; 7], 'VariableNames', {'a'}); %! fn = [tempname() '.xlsx']; %! unwind_protect %! writetable (A, fn, 'Sheet', 'first'); %! writetable (B, fn, 'Sheet', 'second'); %! writetable (more, fn, 'Sheet', 'first', 'WriteMode', 'append'); %! r = xlsx2struct (fn); %! assert_equal (fieldnames (r), {'first'; 'second'}); %! assert_equal (r.first.a, [1; 2; 6; 7]); %! assert_equal (r.second.b, [3; 4; 5]); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## 'replacefile' overwrites the whole Excel workbook %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = table ([3; 4], 'VariableNames', {'b'}); %! fn = [tempname() '.xlsx']; %! unwind_protect %! writetable (A, fn, 'Sheet', 'first'); %! writetable (B, fn, 'Sheet', 'second', 'WriteMode', 'replacefile'); %! [~, ~, ~, nm] = __xlsx2table__ (fn); %! assert_equal (nm(:), {'second'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## The Excel serial scale counts a 1900-02-29 that never existed, so the tests ## below assert the serial written into the sheet rather than a round trip: ## reader and writer are exact inverses, and an epoch error is invisible from ## inside the package. 'xlsxWithSerials' does the converse, planting chosen ## serials in a date-formatted column so the reader can be measured on its own. %!function x = xlsxSheetXML (fn) %! dn = tempname (); %! unwind_protect %! unzip (fn, dn); %! x = fileread (fullfile (dn, 'xl', 'worksheets', 'sheet1.xml')); %! unwind_protect_cleanup %! if (exist (dn, 'dir')); rmdir (dn, 's'); endif %! end_unwind_protect %!endfunction %!function fn = xlsxWithSerials (serials) %! fn = [tempname() '.xlsx']; %! dn = tempname (); %! unwind_protect %! d = datetime (2020, 1, 1) + days ((1:numel (serials))' - 1); %! writetable (table (d, 'VariableNames', {'d'}), fn); %! unzip (fn, dn); %! wf = fullfile (dn, 'xl', 'worksheets', 'sheet1.xml'); %! x = fileread (wf); %! for i = 1:numel (serials) %! x = strrep (x, sprintf ('%d', 43830 + i), ... %! sprintf ('%.17g', serials(i))); %! endfor %! fid = fopen (wf, 'w'); %! fputs (fid, x); %! fclose (fid); %! delete (fn); %! ent = dir (dn); %! ent = ent(! ismember ({ent.name}, {'.', '..'})); %! zip (fn, {ent.name}, dn); %! unwind_protect_cleanup %! if (exist (dn, 'dir')); rmdir (dn, 's'); endif %! end_unwind_protect %!endfunction ## '.xlsx' dates go out on Excel's 1900 serial scale: 1900-03-01 is 61, and ## every earlier date is one less than its plain day count %!test %! d = [datetime(2020, 1, 1); datetime(1900, 3, 1); datetime(1900, 2, 28); ... %! datetime(1900, 1, 1); datetime(1899, 12, 31)]; %! fn = [tempname() '.xlsx']; %! unwind_protect %! writetable (table (d, 'VariableNames', {'d'}), fn); %! v = regexp (xlsxSheetXML (fn), '([^<]*)', 'tokens'); %! assert_equal (cellfun (@(c) c{1}, v, 'UniformOutput', false), ... %! {'43831', '61', '59', '1', '0'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## A date below serial 0 has no Excel serial at all and is written as text %!test %! d = [datetime(1899, 12, 30); datetime(1850, 6, 15); ... %! datetime(1899, 6, 15, 6, 30, 0)]; %! fn = [tempname() '.xlsx']; %! unwind_protect %! writetable (table (d, 'VariableNames', {'d'}), fn, ... %! 'WriteVariableNames', false); %! t = regexp (xlsxSheetXML (fn), ']*>([^<]*)', 'tokens'); %! assert_equal (cellfun (@(c) c{1}, t, 'UniformOutput', false), ... %! {'1899-12-30', '1850-06-15', '1899-06-15 06:30:00'}); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ## Reading applies the same scale: serials 60 and 61 are both 1900-03-01, since ## 60 is the phantom 1900-02-29, and 60.5 therefore lands a day before 60 %!test %! fn = xlsxWithSerials ([61, 60.5, 60, 59, 1, 0, -1]); %! unwind_protect %! R = readtable (fn); %! e = [datetime(1900, 3, 1); datetime(1900, 2, 28, 12, 0, 0); ... %! datetime(1900, 3, 1); datetime(1900, 2, 28); datetime(1900, 1, 1); ... %! datetime(1899, 12, 31); datetime(1899, 12, 30)]; %! assert_equal (R.d, e); %! unwind_protect_cleanup %! if (exist (fn, 'file')); delete (fn); endif %! end_unwind_protect ################################################################################ ## ** Summary Information ** ## ################################################################################ ## Available Methods ## ## ## ## 'summary' 'height' 'width' 'head' ## ## 'tail' ## ## ## ################################################################################ %!shared LastName, Age, Smoker, Height, Weight, BloodPressure, T, tblA %! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"}; %! Age = [38; 43; 38; 40; 49]; %! Smoker = logical ([1; 0; 1; 0; 1]); %! Height = [71; 69; 64; 67; 64]; %! Weight = [176; 163; 131; 133; 119]; %! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80]; %! T = table (Age, Smoker, Height, Weight, BloodPressure); ## Test 'summary' returns a struct with the expected per-variable fields %!test %! tt = table (Age, Smoker, BloodPressure); %! ss = summary (tt); %! assert_equal (isstruct (ss), true); %! assert_equal (fieldnames (ss), {'Age'; 'Smoker'; 'BloodPressure'}); %! assert_equal (ss.Age.Size, [5, 1]); %! assert_equal (ss.Age.Type, 'double'); %! assert_equal ([ss.Age.Min, ss.Age.Median, ss.Age.Max], [38, 40, 49]); %! assert_equal (ss.Age.NumMissing, 0); %! assert_equal (ss.Smoker.Size, [5, 1]); %! assert_equal (ss.Smoker.True, 3); %! assert_equal (ss.Smoker.False, 2); %! assert_equal (ss.BloodPressure.Size, [5, 2]); ## Test 'summary' numeric NaN handling and per-column statistics %!test %! ss = summary (table ([1; 2; NaN; 4], [NaN, 2; 3, NaN; 5, 6; 7, 8], ... %! 'VariableNames', {'a', 'b'})); %! assert_equal ([ss.a.Min, ss.a.Median, ss.a.Max], [1, 2, 4]); %! assert_equal (ss.a.NumMissing, 1); %! assert_equal (ss.b.Min, [3, 2]); %! assert_equal (ss.b.Median, [5, 6]); %! assert_equal (ss.b.Max, [7, 8]); %! assert_equal (ss.b.NumMissing, [1, 1]); ## Test 'summary' of logical, string, cell, and categorical variables %!test %! ss = summary (table (logical ([1; 1; 0]), string ({'a'; 'b'; 'c'}), ... %! {'x'; 'y'; 'z'}, categorical ({'p'; 'q'; 'p'}), ... %! 'VariableNames', {'L', 'S', 'C', 'Cat'})); %! assert_equal (ss.L.True, 2); %! assert_equal (ss.L.False, 1); %! assert_equal (isfield (ss.S, 'Min'), false); %! assert_equal (isfield (ss.C, 'Min'), false); %! assert_equal (isfield (ss.Cat, 'Min'), false); %! assert_equal (ss.Cat.Type, 'categorical'); ## Test 'summary' of a duration variable (typed stats, no TimeStep field) %!test %! Dv = duration ([1; 2; 3], [0; 30; 0], [0; 0; 0]); %! ss = summary (table (Dv, 'VariableNames', {'D'})); %! assert_equal (ss.D.Type, 'duration'); %! assert_equal (class (ss.D.Min), 'duration'); %! assert_equal (seconds (ss.D.Min), 3600); %! assert_equal (seconds (ss.D.Median), 9000); %! assert_equal (seconds (ss.D.Max), 10800); %! assert_equal (ss.D.NumMissing, 0); %! assert_equal (isfield (ss.D, 'TimeStep'), false); ## Test 'summary' duration omits NaN from statistics and counts missing %!test %! Dv = duration ([1; NaN; 3], [0; 0; 0], [0; 0; 0]); %! ss = summary (table (Dv, 'VariableNames', {'D'})); %! assert_equal (seconds (ss.D.Min), 3600); %! assert_equal (seconds (ss.D.Max), 10800); %! assert_equal (seconds (ss.D.Median), 7200); %! assert_equal (ss.D.NumMissing, 1); ## Test 'summary' of a datetime variable (typed stats, no TimeStep field) %!test %! Tv = datetime ([2020; 2021; 2022], 1, 1); %! ss = summary (table (Tv, 'VariableNames', {'T'})); %! assert_equal (ss.T.Type, 'datetime'); %! assert_equal (class (ss.T.Min), 'datetime'); %! assert_equal (datevec (ss.T.Min), [2020, 1, 1, 0, 0, 0]); %! assert_equal (datevec (ss.T.Median), [2021, 1, 1, 0, 0, 0]); %! assert_equal (datevec (ss.T.Max), [2022, 1, 1, 0, 0, 0]); %! assert_equal (ss.T.NumMissing, 0); %! assert_equal (isfield (ss.T, 'TimeStep'), false); ## Test 'summary' datetime excludes NaT and counts it as missing %!test %! Tv = datetime ([2020; NaN; 2022], 1, 1); %! ss = summary (table (Tv, 'VariableNames', {'T'})); %! assert_equal (datevec (ss.T.Min), [2020, 1, 1, 0, 0, 0]); %! assert_equal (datevec (ss.T.Max), [2022, 1, 1, 0, 0, 0]); %! assert_equal (ss.T.NumMissing, 1); ## Test 'summary' of calendarDuration reports only Size, Type, and NumMissing %!test %! Cv = calendarDuration ([1; 2; 3], [0; 1; 2], [0; 0; 0]); %! ss = summary (table (Cv, 'VariableNames', {'C'})); %! assert_equal (ss.C.Type, 'calendarDuration'); %! assert_equal (ss.C.Size, [3, 1]); %! assert_equal (isfield (ss.C, 'Min'), false); %! assert_equal (ss.C.NumMissing, 0); %! ss = summary (table (calendarDuration ([1; NaN], 0, 0), ... %! 'VariableNames', {'C'})); %! assert_equal (ss.C.NumMissing, 1); ## Test 'summary' captures variable Description and Units in the struct %!test %! tt = table ([1; 2; 3], 'VariableNames', {'v'}); %! tt.Properties.VariableUnits = {'kg'}; %! tt.Properties.VariableDescriptions = {'a weight'}; %! ss = summary (tt); %! assert_equal (ss.v.Units, 'kg'); %! assert_equal (ss.v.Description, 'a weight'); ## Test 'summary' of an empty table does not error %!test %! et = table ('Size', [0, 2], 'VariableTypes', {'double', 'double'}, ... %! 'VariableNames', {'a', 'b'}); %! ss = summary (et); %! assert_equal (fieldnames (ss), {'a'; 'b'}); %! str = evalc ("summary (et);"); %! assert_equal (! isempty (strfind (str, 'Variables:')), true); ## Test 'summary' printed output for numeric, logical, and missing values %!test %! str = evalc (["summary (table ([1; 2; NaN], logical ([1; 0; 1]), ", ... %! "'VariableNames', {'a', 'b'}));"]); %! assert_equal (! isempty (strfind (str, '3x2 table')), true); %! assert_equal (! isempty (strfind (str, 'Variables:')), true); %! assert_equal (! isempty (strfind (str, ' a: double')), true); %! assert_equal (! isempty (strfind (str, ' b: logical (2 true)')), true); %! assert_equal (! isempty ... %! (strfind (str, 'Statistics for applicable variables:')), true); %! assert_equal (! isempty (strfind (str, 'NumMissing')), true); %! assert_equal (! isempty (strfind (str, 'Median')), true); %! assert_equal (! isempty (strfind (str, 'Std')), true); ## Test the units, the type note and the description share one bracket %!test %! Tn = table ([1; 2; 3], categorical ({'x'; 'y'; 'x'}), ... %! 'VariableNames', {'num', 'cat'}); %! Tn.Properties.VariableUnits = {'kg', 'none'}; %! Tn.Properties.VariableDescriptions = {'a count', 'a label'}; %! str = evalc ("summary (Tn);"); %! assert_equal (! isempty (strfind (str, ... %! ' num: double (kg, a count)')), true); %! assert_equal (! isempty (strfind (str, ... %! ' cat: categorical (none, 2 categories, a label)')), true); ## Test a heading wider than its column is centred over it, not right-aligned %!test %! str = evalc ("summary (table ([1; 2; 3], 'VariableNames', {'v'}));"); %! c = strsplit (str, "\n"); %! ix = find (! cellfun (@isempty, strfind (c, 'NumMissing')), 1); %! head = c{ix}; %! row = c{ix + 1}; %! hs = strfind (head, 'NumMissing'); %! assert_equal (numel (hs), 1); %! ## the single-digit count sits at the middle of its heading, not its end %! vs = strfind (row, '0'); %! assert_equal (vs(1) > hs(1), true); %! assert_equal (vs(1) < hs(1) + 9, true); ## Test no line is padded past its last value %!test %! Tn = table ([1; 2; 3], categorical ({'x'; 'y'; 'x'}), ... %! 'VariableNames', {'num', 'cat'}); %! str = evalc ("summary (Tn);"); %! c = strsplit (str, "\n"); %! for k = 1:numel (c) %! assert_equal (isempty (c{k}) || ! isspace (c{k}(end)), true); %! endfor ## Test a logical variable is counted on its own line, not in the statistics %!test %! str = evalc (["summary (table (logical ([1; 0; 1]), ", ... %! "'VariableNames', {'b'}));"]); %! assert_equal (! isempty (strfind (str, ' b: logical (2 true)')), true); %! assert_equal (isempty (strfind (str, 'Statistics')), true); ## Test 'summary' printed output shows table and variable Description %!test %! tt = table ([1; 2; 3], 'VariableNames', {'v'}); %! tt.Properties.Description = 'My table'; %! tt.Properties.VariableDescriptions = {'a weight'}; %! str = evalc ("summary (tt);"); %! assert_equal (! isempty (strfind (str, 'Description: My table')), true); %! assert_equal (! isempty (strfind (str, ' v: double (a weight)')), true); ## Test 'summary' printed output for datetime and duration variables %!test %! sd = evalc (["summary (table (duration ([1; 2; 3], 0, 0), ", ... %! "'VariableNames', {'D'}));"]); %! assert_equal (! isempty (strfind (sd, '01:00:00')), true); %! assert_equal (! isempty (strfind (sd, '03:00:00')), true); %! assert_equal (! isempty (strfind (sd, ' D: duration')), true); %! st = evalc (["summary (table (datetime (2020, 1, [1; 2; 3]), ", ... %! "'VariableNames', {'T'}));"]); %! assert_equal (! isempty (strfind (st, '01-Jan-2020')), true); %! assert_equal (! isempty (strfind (st, '03-Jan-2020')), true); %! assert_equal (! isempty (strfind (st, ' T: datetime')), true); ## Test 'summary' printed output for calendarDuration shows only NumMissing %!test %! sc = evalc (["summary (table (calendarDuration ([1; NaN], 0, 0), ", ... %! "'VariableNames', {'C'}));"]); %! assert_equal (! isempty (strfind (sc, 'NumMissing')), true); %! assert_equal (! isempty (strfind (sc, ' C: calendarDuration')), true); %! assert_equal (isempty (strfind (sc, 'Median')), true); ## Test 'height': number of rows, equivalent to size (TBL, 1) %!assert_equal (height (T), 5); %!assert_equal (height (T), size (T, 1)); %!assert_equal (class (height (T)), 'double'); ## Test 'height' of a single-row table %!assert_equal (height (table (1, 2, 3)), 1); ## Test 'height' of an empty table with no variables %!assert_equal (height (table ()), 0); ## Test 'height' of a table with variables but zero rows %!assert_equal (height (table ('Size', [0, 3], ... %! 'VariableTypes', {'double', 'double', 'double'})), 0); ## Test 'height' of a preallocated table reports the requested row count %!assert_equal (height (table ('Size', [7, 2], ... %! 'VariableTypes', {'double', 'double'})), 7); ## Test 'height' is unaffected by multicolumn variables %!assert_equal (height (table (Age, BloodPressure)), 5); ## Test 'height' counts rows regardless of row names %!assert_equal (height (table (Age, Smoker, 'RowNames', LastName)), 5); ## Test 'width': number of variables, equivalent to size (TBL, 2) %!assert_equal (width (T), 5); %!assert_equal (width (T), size (T, 2)); %!assert_equal (class (width (T)), 'double'); ## Test 'width' counts variables, not the total number of columns %!assert_equal (width (T), 5); %!assert_equal (size (T.BloodPressure, 2), 2); ## Test 'width' of a single-variable table %!assert_equal (width (table (Age)), 1); ## Test 'width' of an empty table with no variables %!assert_equal (width (table ()), 0); ## Test 'width' of a table with zero rows still counts its variables %!assert_equal (width (table ('Size', [0, 3], ... %! 'VariableTypes', {'double', 'double', 'double'})), 3); ## Test 'head' returns the first K rows in a new table %!test %! h = head (T, 3); %! assert_equal (height (h), 3); %! assert_equal (h.Age, Age(1:3)); %! assert_equal (class (h), 'table'); ## Test 'head' default count is eight rows %!assert_equal (height (head (T)), 5); %!test %! big = table ((1:20)'); %! assert_equal (height (head (big)), 8); %! h = head (big); %! assert_equal (h.Var1, (1:8)'); ## Test 'head' with K equal to the number of rows returns the whole table %!assert_equal (height (head (T, 5)), 5); ## Test 'head' with K greater than the number of rows returns all rows %!assert_equal (height (head (T, 99)), 5); ## Test 'head' with K of one returns just the first row %!test %! h1 = head (T, 1); %! assert_equal (height (h1), 1); %! assert_equal (h1.Age, Age(1)); ## Test 'head' of an empty (zero-row) table returns an empty table %!test %! e = table ('Size', [0, 2], 'VariableTypes', {'double', 'double'}); %! assert_equal (size (head (e, 3)), [0, 2]); ## Test 'head' preserves variable names, row names, and properties %!test %! R = table (Age, Smoker, 'RowNames', LastName); %! R.Properties.VariableUnits = {'Yrs', ''}; %! R.Properties.Description = 'demo'; %! h = head (R, 2); %! assert_equal (h.Properties.VariableNames, {'Age', 'Smoker'}); %! assert_equal (h.Properties.RowNames, LastName(1:2)); %! assert_equal (h.Properties.VariableUnits, {'Yrs', ''}); %! assert_equal (h.Properties.Description, 'demo'); ## Test 'head' preserves multicolumn variables %!test %! h = head (table (Age, BloodPressure), 2); %! assert_equal (h.BloodPressure, BloodPressure(1:2, :)); ## Test 'head' preserves special data type variables %!test %! du = duration ((1:5)', 0, 0); %! ca = categorical ({'a'; 'b'; 'c'; 'd'; 'e'}); %! h = head (table (du, ca), 2); %! assert_equal (class (h.du), 'duration'); %! assert_equal (class (h.ca), 'categorical'); %! assert_equal (seconds (h.du), [3600; 7200]); %! assert_equal (cellstr (h.ca), {'a'; 'b'}); ## Test 'head' with no output argument displays the first K rows %!test %! R = table (Age, Smoker, 'RowNames', LastName); %! str = evalc ('head (R, 2);'); %! assert_equal (! isempty (strfind (str, 'Sanchez')), true); %! assert_equal (! isempty (strfind (str, 'Johnson')), true); %! assert_equal (isempty (strfind (str, 'Li')), true); ## Test 'head' displays a missing variable as %!test %! str = evalc ('head (table ([missing; missing]), 2);'); %! assert_equal (numel (strfind (str, '')), 2); ## Test 'head' displays a missing variable alongside other types %!test %! str = evalc ('head (table ([missing; missing], [1; 2]), 2);'); %! assert_equal (numel (strfind (str, '')), 2); %! assert_equal (! isempty (strfind (str, 'Var2')), true); ## Test 'head' displays a multicolumn missing variable %!test %! str = evalc ('head (table (repmat (missing, 2, 3)), 2);'); %! assert_equal (numel (strfind (str, '')), 6); ## Test 'head' with a K of zero returns a table with no rows %!test %! out = head (T, 0); %! assert_equal (height (out), 0); %! assert_equal (width (out), width (T)); %! assert_equal (class (out), 'table'); ## Test input validation for 'head' %!error ... %! head (T, 1.5); %!error ... %! head (T, -2); %!error ... %! head (T, [1, 2]); %!error ... %! [out1, out2] = head (T, 2); ## Test 'tail' returns the last K rows in a new table %!test %! t = tail (T, 3); %! assert_equal (height (t), 3); %! assert_equal (t.Age, Age(3:5)); %! assert_equal (class (t), 'table'); ## Test 'tail' default count is eight rows %!assert_equal (height (tail (T)), 5); %!test %! big = table ((1:20)'); %! assert_equal (height (tail (big)), 8); %! t = tail (big); %! assert_equal (t.Var1, (13:20)'); ## Test 'tail' with K equal to the number of rows returns the whole table %!assert_equal (height (tail (T, 5)), 5); ## Test 'tail' preserves the original row order %!test %! t = tail (T, 5); %! assert_equal (t.Age, Age); ## Test 'tail' with K greater than the number of rows returns all rows %!assert_equal (height (tail (T, 99)), 5); ## Test 'tail' with K of one returns just the last row %!test %! t1 = tail (T, 1); %! assert_equal (height (t1), 1); %! assert_equal (t1.Age, Age(5)); ## Test 'tail' of an empty (zero-row) table returns an empty table %!test %! e = table ('Size', [0, 2], 'VariableTypes', {'double', 'double'}); %! assert_equal (size (tail (e, 3)), [0, 2]); ## Test 'tail' preserves variable names, row names, and properties %!test %! R = table (Age, Smoker, 'RowNames', LastName); %! R.Properties.VariableUnits = {'Yrs', ''}; %! R.Properties.Description = 'demo'; %! t = tail (R, 2); %! assert_equal (t.Properties.VariableNames, {'Age', 'Smoker'}); %! assert_equal (t.Properties.RowNames, LastName(4:5)); %! assert_equal (t.Properties.VariableUnits, {'Yrs', ''}); %! assert_equal (t.Properties.Description, 'demo'); ## Test 'tail' preserves multicolumn variables %!test %! t = tail (table (Age, BloodPressure), 2); %! assert_equal (t.BloodPressure, BloodPressure(4:5, :)); ## Test 'tail' preserves special data type variables %!test %! du = duration ((1:5)', 0, 0); %! ca = categorical ({'a'; 'b'; 'c'; 'd'; 'e'}); %! t = tail (table (du, ca), 2); %! assert_equal (class (t.du), 'duration'); %! assert_equal (class (t.ca), 'categorical'); %! assert_equal (seconds (t.du), [14400; 18000]); %! assert_equal (cellstr (t.ca), {'d'; 'e'}); ## Test 'tail' with no output argument displays the last K rows %!test %! R = table (Age, Smoker, 'RowNames', LastName); %! str = evalc ('tail (R, 2);'); %! assert_equal (! isempty (strfind (str, 'Diaz')), true); %! assert_equal (! isempty (strfind (str, 'Brown')), true); %! assert_equal (isempty (strfind (str, 'Sanchez')), true); ## Test 'tail' with a K of zero returns a table with no rows %!test %! out = tail (T, 0); %! assert_equal (height (out), 0); %! assert_equal (width (out), width (T)); %! assert_equal (class (out), 'table'); ## Test input validation for 'tail' %!error ... %! tail (T, 1.5); %!error ... %! tail (T, -2); %!error ... %! tail (T, [1, 2]); %!error ... %! [out1, out2] = tail (T, 2); ################################################################################ ## ** Sort, Filter, and Rearrange ** ## ################################################################################ ## Available Methods ## ## ## ## 'sortrows' 'unique' 'issortedrows' 'topkrows' ## ## 'addvars' 'renamevars' 'movevars' 'removevars' ## ## 'splitvars' 'mergevars' 'convertvars' 'rows2vars' ## ## 'stack' 'unstack' 'inner2outer' 'addprop' ## ## 'rmprop' ## ## ## ################################################################################ %!shared LastName, Age, Smoker, Height, Weight, BloodPressure, T, tblA %! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"}; %! Age = [38; 43; 38; 40; 49]; %! Smoker = logical ([1; 0; 1; 0; 1]); %! Height = [71; 69; 64; 67; 64]; %! Weight = [176; 163; 131; 133; 119]; %! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80]; %! T = table (Age, Smoker, Height, Weight, BloodPressure); ## Test 'sortrows' and 'issortedrows' methods %!test %! tblA = table (Age, Height, Weight, BloodPressure, 'RowNames', LastName); %! assert_equal (issortedrows (tblA, "RowNames"), false); %! [sorted_tblA, index] = sortrows (tblA, "RowNames"); %! assert_equal (index, [5; 4; 2; 3; 1]); %! assert_equal (issortedrows (sorted_tblA, "RowNames"), true); %! [sorted_tblA, index] = sortrows (tblA, "RowNames", "descend"); %! assert_equal (index, [1; 3; 2; 4; 5]); %! assert_equal (issortedrows (sorted_tblA, "RowNames", "descend"), true); %!test # duration variable (regression: 'days' is a method, not a property) %! A = table (hours ([3;1;2]), {'a';'b';'c'}, 'VariableNames', {'D', 'S'}); %! [S, index] = sortrows (A); %! assert_equal (hours (S.D), [1;2;3]); %! assert_equal (S.S, {'b';'c';'a'}); %! assert_equal (index, [2;3;1]); %!test %! tblA.Properties.DimensionNames(1) = "Patients"; %! assert_equal (issortedrows (tblA, "Patients"), false); %! [sorted_tblA, index] = sortrows (tblA, "Patients"); %! assert_equal (index, [5; 4; 2; 3; 1]); %! assert_equal (issortedrows (sorted_tblA, "Patients"), true); %! [sorted_tblA, index] = sortrows (tblA, "Patients", "descend"); %! assert_equal (index, [1; 3; 2; 4; 5]); %! assert_equal (issortedrows (sorted_tblA, "Patients", "descend"), true); %!test %! [sorted_tblA, index] = sortrows (tblA); %! assert_equal (index, [3; 1; 4; 2; 5]); %! assert_equal (issortedrows (sorted_tblA), true); %! [sorted_tblA, index] = sortrows (tblA, [], "ascend"); %! assert_equal (index, [3; 1; 4; 2; 5]); %! assert_equal (issortedrows (sorted_tblA, [], "ascend"), true); %! [sorted_tblA, index] = sortrows (tblA, [], "descend"); %! assert_equal (index, [5; 2; 4; 1; 3]); %! assert_equal (issortedrows (sorted_tblA, [], "descend"), true); %!test %! [tblB, index] = sortrows (tblA, "Height"); %! assert_equal (index, [3; 5; 4; 2; 1]); %! assert_equal (tblB.Properties.RowNames, ... %! {"Li"; "Brown"; "Diaz"; "Johnson"; "Sanchez"}) %! assert_equal (issortedrows (tblB, "Height"), true); %!test %! LastName = {"Sweet"; "Jacobson"; "Wang"; "Joiner"; "Berger"}; %! tblA = table (Age, Height, Weight, BloodPressure, 'RowNames', LastName); %! [tblB, index] = sortrows (tblA, {"Height", "Weight"}, {"ascend", "descend"}); %! assert_equal (index, [3; 5; 4; 2; 1]); %! assert_equal (tblB.Properties.RowNames, ... %! {"Wang"; "Berger"; "Joiner"; "Jacobson"; "Sweet"}) %! assert_equal (issortedrows (tblB, {"Height", "Weight"}, ... %! {"ascend", "descend"}), true); %!test %! [tblB, index] = sortrows (tblA, {"BloodPressure"}, {"ascend"}); %! assert_equal (index, [2; 4; 5; 1; 3]); %! assert_equal (tblB.BloodPressure, sortrows (BloodPressure)); %! assert_equal (issortedrows (tblB, {"BloodPressure"}, {"ascend"}), true); %! [tblB, index] = sortrows (tblA, {"BloodPressure"}, {"descend"}); %! assert_equal (index, flip ([2; 4; 5; 1; 3])); %! assert_equal (tblB.BloodPressure, sortrows (BloodPressure, -1)); %! assert_equal (issortedrows (tblB, "BloodPressure", "descend"), true); %!test %! [tblB, index] = sortrows (tblA, {"Height", "RowNames"}, {"ascend"}); %! assert_equal (index, [5; 3; 4; 2; 1]); %! assert_equal (tblB.Properties.RowNames, ... %! {"Berger"; "Wang"; "Joiner"; "Jacobson"; "Sweet"}) %! assert_equal (tblB.Height, sortrows (Height)); %! assert_equal (issortedrows (tblB, {"Height", "RowNames"}, {"ascend"}), true); %! [tblB, index] = sortrows (tblA, {"Height", "RowNames"}, ... %! {"ascend", "descend"}); %! assert_equal (index, [3; 5; 4; 2; 1]); %! assert_equal (tblB.Properties.RowNames, ... %! {"Wang"; "Berger"; "Joiner"; "Jacobson"; "Sweet"}) %! assert_equal (issortedrows (tblB, {"Height", "RowNames"}, ... %! {"ascend", "descend"}), true); %!test %! [tblB, index] = sortrows (tblA, [true, true, false, false]); %! assert_equal (index, [3; 1; 4; 2; 5]); %! assert_equal (issortedrows (tblB, [true, true, false, false]), true); %! [tblB, index] = sortrows (tblA, [true, true, false, false], "ascend"); %! assert_equal (index, [3; 1; 4; 2; 5]); %! assert_equal (issortedrows (tblB, [true, true, false, false], "ascend"), ... %! true); %! [tblB, index] = sortrows (tblA, [true, true, false, false], "descend"); %! assert_equal (index, flip ([3; 1; 4; 2; 5])); %! assert_equal (issortedrows (tblB, [true, true, false, false], "descend"), ... %! true); %! [tblB, index] = sortrows (tblA, [true, true, false, false], ... %! {"ascend", "descend"}); %! assert_equal (index, [1; 3; 4; 2; 5]); %! assert_equal (issortedrows (tblB, [true, true, false, false], ... %! {"ascend", "descend"}), true); %!test %! [sorted_tblA, indexN] = sortrows (tblA, 1); %! assert_equal (issortedrows (sorted_tblA, 1), true); %! [sorted_tblA, indexV] = sortrows (tblA, "Age"); %! assert_equal (issortedrows (sorted_tblA, "Age"), true); %! assert_equal (indexN, indexV); %! [sorted_tblA, indexN] = sortrows (tblA, -1); %! assert_equal (issortedrows (sorted_tblA, -1), true); %! [sorted_tblA, indexV] = sortrows (tblA, "Age", "descend"); %! assert_equal (issortedrows (sorted_tblA, "Age", "descend"), true); %! assert_equal (indexN, indexV); %! [sorted_tblA, indexN] = sortrows (tblA, -1, "ascend"); %! assert_equal (issortedrows (sorted_tblA, -1, "ascend"), true); %! [~, indexV] = sortrows (tblA, "Age"); %! assert_equal (indexN, indexV); %! [sorted_tblA, indexN] = sortrows (tblA, [-1, 2]); %! assert_equal (issortedrows (sorted_tblA, [-1, 2]), true); %! [~, indexV] = sortrows (tblA, {"Age", "Height"}, {"descend", "ascend"}); %! assert_equal (indexN, indexV); %!test %! TT = table (Age, table (Age, Height), BloodPressure, 'RowNames', LastName); %! [sorted_TT, index] = sortrows (TT); %! assert_equal (index, [3; 1; 4; 2; 5]); %! assert_equal (issortedrows (sorted_TT), true); %! [sorted_TT, index] = sortrows (TT, [1, 2]); %! assert_equal (index, [3; 1; 4; 2; 5]); %! assert_equal (issortedrows (sorted_TT, [1, 2]), true); %! [sorted_TT, index] = sortrows (TT, [1, -2]); %! assert_equal (index, [1; 3; 4; 2; 5]); %! assert_equal (issortedrows (sorted_TT, [1, -2]), true); %!test %! Weights = [176;NaN;131;133;NaN]; %! tblN = table (Age, Height, Weights, BloodPressure, 'RowNames', LastName); %! [tblB, index] = sortrows (tblN, "Weights", "MissingPlacement", "first"); %! assert_equal (index, [2; 5; 3; 4; 1]); %! assert_equal (issortedrows (tblB, "Weights", "MissingPlacement", ... %! "first"), true); %! [tblB, index] = sortrows (tblN, "Weights"); %! assert_equal (index, [3; 4; 1; 2; 5]); %! assert_equal (issortedrows (tblB, "Weights"), true); %!test %! tblN = table (Age, Height, Weight, BloodPressure, LastName); %! [tblB, index] = sortrows (tblN, vartype ("numeric")); %! assert_equal (index, [3; 1; 4; 2; 5]); %! assert_equal (issortedrows (tblB, vartype ("numeric")), true); %! [tblB, index] = sortrows (tblN, vartype ("cellstr")); %! assert_equal (index, [5; 2; 4; 1; 3]); %! assert_equal (issortedrows (tblB, vartype ("cellstr")), true); ## special-type sort keys (datetime / categorical / string / calendarDuration) %!test %! S = {'b'; 'a'; 'c'; 'a'}; %! dt = datetime (2020, [3; 1; 2; 1], 1); %! A = table (dt, S, 'VariableNames', {'D', 'S'}); %! [tblB, index] = sortrows (A, 'D'); %! assert_equal (index, [2; 4; 3; 1]); %! assert_equal (issortedrows (tblB, 'D'), true); %! [tblB, index] = sortrows (A, 'D', 'descend'); %! assert_equal (index, [1; 3; 2; 4]); %! assert_equal (issortedrows (tblB, 'D', 'descend'), true); %! c = categorical ({'mid'; 'low'; 'high'; 'low'}, {'low', 'mid', 'high'}, ... %! 'Ordinal', true); %! [tblB, index] = sortrows (table (c, 'VariableNames', {'C'}), 'C'); %! assert_equal (index, [2; 4; 1; 3]); %! assert_equal (issortedrows (tblB, 'C'), true); %! st = string ({'banana'; 'apple'; 'cherry'; 'apple'}); %! [tblB, index] = sortrows (table (st, 'VariableNames', {'St'})); %! assert_equal (index, [2; 4; 1; 3]); %! assert_equal (issortedrows (tblB), true); %!test # 'ComparisonMethod' with complex data ('abs' and 'real') %! z = [3+4i; -1+0i; 0+2i; -5+0i]; %! A = table (z, 'VariableNames', {'Z'}); %! [tblB, index] = sortrows (A, 'Z', 'ComparisonMethod', 'abs'); %! assert_equal (index, [2; 3; 1; 4]); %! [tblB, index] = sortrows (A, 'Z', 'ComparisonMethod', 'real'); %! assert_equal (index, [4; 2; 3; 1]); %!test # 'MissingPlacement' 'last' and 'auto' %! W = [176; NaN; 131; 133; NaN]; %! tblN = table (Age, W, 'VariableNames', {'Age', 'W'}); %! [tblB, index] = sortrows (tblN, 'W', 'MissingPlacement', 'last'); %! assert_equal (index, [3; 4; 1; 2; 5]); %! assert_equal (issortedrows (tblN, 'W', 'MissingPlacement', 'last'), false); %! [tblB, index] = sortrows (tblN, 'W', 'MissingPlacement', 'auto'); %! assert_equal (index, [3; 4; 1; 2; 5]); ## Test 'sortrows' sorts by a logical variable %!test %! T = table ([3; 1; 2], logical ([1; 0; 1]), 'VariableNames', {'N', 'L'}); %! assert_equal (sortrows (T, 'L').L, logical ([0; 1; 1])); %! assert_equal (sortrows (T, 'L', 'descend').L, logical ([1; 1; 0])); ## Test a logical variable does not stop a table being sorted at all %!test %! T = table ([3; 1; 2], logical ([1; 0; 1]), 'VariableNames', {'N', 'L'}); %! assert_equal (sortrows (T).N, [1; 2; 3]); %! assert_equal (sortrows (T, {'N', 'L'}).N, [1; 2; 3]); ## Test 'sortrows' sorts by a char matrix variable %!test %! T = table (['cb'; 'aa'; 'ca'], [1; 2; 3], 'VariableNames', {'K', 'N'}); %! assert_equal (sortrows (T, 'K').N, [2; 3; 1]); %! assert_equal (sortrows (T, 'K', 'descend').N, [1; 3; 2]); ## Test 'sortrows' places a missing string as a missing value %!test # a missing string sorts as a missing value does, as in MATLAB %! s = [string('b'); string(missing); string('a')]; %! T = table (s, [1; 2; 3], 'VariableNames', {'S', 'K'}); %! assert_equal (sortrows (T).K, [3; 1; 2]); %! assert_equal (sortrows (T, 'S', 'descend').K, [2; 1; 3]); %! assert_equal (sortrows (T, 'S', 'MissingPlacement', 'first').K, [2; 3; 1]); ## Test input validation for 'sortrows' method %!error ... %! sortrows (table ([2; 1]), struct ('a', 1)); %!error ... %! sortrows (table ([2; 1]), {1}); %!error ... %! sortrows (table (calmonths ([2; 1]))); %!error ... %! sortrows (table (calmonths ([2; 1]), [1; 2]), 'Var1'); %!error ... %! issortedrows (table (calmonths ([2; 1]))); %!error ... %! sortrows (tblA, "MissingPlacement", "param"); %!error ... %! sortrows (tblA, "ComparisonMethod", "param"); %!error ... %! sortrows (tblA, [], "ascend", "param"); %!error ... %! sortrows (tblA, "Age", "Bogus", 1); %!error ... %! sortrows (tblA, [], "aaascend"); %!error ... %! sortrows (tblA, "RowNames", {"ascend", "ascend"}); %!error ... %! sortrows (tblA, "Row", {"ascend", "ascend"}); %!error ... %! sortrows (tblA, [true, true, false]); %!error ... %! sortrows (tblA, "Age", {"ascend", "ascend"}); %!error ... %! sortrows (tblA, [1, 0, 3]); %!error ... %! sortrows (tblA, [1, 6, 3]); %!error ... %! sortrows (tblA, [1, 3, 2], {"ascend", "ascend"}); %!error ... %! sortrows (tblA, {"Age", "whatever"}); %!error ... %! sortrows (tblA, {"Age", "Height", "Weight"}, {"ascend", "ascend"}); %!error ... %! sortrows (tblA, vartype ("numeric"), {"ascend", "ascend"}); %!error ... %! sortrows (table ({5; 4; 3})); %!error ... %! sortrows (table ([struct("s", 4); struct("s", 5); struct("s", 6)])); %!error ... %! sortrows (table (inputParser ())); %!error ... %! sortrows (table (Age, table (Age, LastName), "RowNames", LastName)) ## Test 'unique' method %!test %! Name = {'Fred'; 'Betty'; 'Bob'; 'George'; 'Jane'}; %! Age = [38; 43; 38; 40; 38]; %! Height = [71; 69; 64; 67; 64]; %! Weight = [176; 163; 131; 185; 131]; %! A = table (Age, Height, Weight, 'RowNames', Name); %! [C, ia, ic] = unique (A); %! assert_equal (ia, [3; 1; 4; 2]); %! assert_equal (ic, [2; 4; 1; 3; 1]); %! assert_equal (C.Age, [38; 38; 40; 43]); %! assert_equal (C.Height, [64; 71; 67; 69]); %! assert_equal (C.Weight, [131; 176; 185; 163]); %! assert_equal (C.Properties.RowNames, {'Bob'; 'Fred'; 'George'; 'Betty'}); %!test # duration variable (regression: 'days' is a method, not a property) %! A = table (hours ([2;1;2;3]), {'a';'b';'c';'a'}, 'VariableNames', {'D','S'}); %! C = unique (A); %! assert_equal (hours (C.D), [1;2;2;3]); %! assert_equal (C.S, {'b';'a';'c';'a'}); %!test %! Name = {'Fred'; 'Betty'; 'Bob'; 'George'; 'Jane'}; %! Age = [38; 43; 38; 40; 38]; %! Height = [71; 69; 64; 67; 64]; %! Weight = [176; 163; 131; 185; 131]; %! A = table (Age, Height, Weight, 'RowNames', Name); %! [C, ia, ic] = unique (A, "sorted"); %! assert_equal (ia, [3; 1; 4; 2]); %! assert_equal (ic, [2; 4; 1; 3; 1]); %! assert_equal (C.Age, [38; 38; 40; 43]); %! assert_equal (C.Height, [64; 71; 67; 69]); %! assert_equal (C.Weight, [131; 176; 185; 163]); %! assert_equal (C.Properties.RowNames, {'Bob'; 'Fred'; 'George'; 'Betty'}); %!test %! Name = {'Fred'; 'Betty'; 'Bob'; 'George'; 'Jane'}; %! Age = [38; 43; 38; 40; 38]; %! Height = [71; 69; 64; 67; 64]; %! Weight = [176; 163; 131; 185; 131]; %! A = table (Age, Height, Weight, 'RowNames', Name); %! [C, ia, ic] = unique (A, "stable"); %! assert_equal (ia, [1; 2; 3; 4]); %! assert_equal (ic, [1; 2; 3; 4; 3]); %! assert_equal (C.Age, [38; 43; 38; 40]); %! assert_equal (C.Height, [71; 69; 64; 67]); %! assert_equal (C.Weight, [176; 163; 131; 185]); %! assert_equal (C.Properties.RowNames, {'Fred'; 'Betty'; 'Bob'; 'George'}); %!test %! Name = {'Fred'; 'Betty'; 'Bob'; 'George'; 'Jane'}; %! Age = [38; 43; 38; 40; 38]; %! Height = [71; 69; 64; 67; 64]; %! Weight = [176; 163; 131; 185; 131]; %! A = table (Age, Height, Weight, 'RowNames', Name); %! [C, ia, ic] = unique (A, "first"); %! assert_equal (ia, [3; 1; 4; 2]); %! assert_equal (ic, [2; 4; 1; 3; 1]); %! assert_equal (C.Age, [38; 38; 40; 43]); %! assert_equal (C.Height, [64; 71; 67; 69]); %! assert_equal (C.Weight, [131; 176; 185; 163]); %! assert_equal (C.Properties.RowNames, {'Bob'; 'Fred'; 'George'; 'Betty'}); %!test %! Name = {'Fred'; 'Betty'; 'Bob'; 'George'; 'Jane'}; %! Age = [38; 43; 38; 40; 38]; %! Height = [71; 69; 64; 67; 64]; %! Weight = [176; 163; 131; 185; 131]; %! A = table (Age, Height, Weight, 'RowNames', Name); %! [C, ia, ic] = unique (A, "last"); %! assert_equal (ia, [5; 1; 4; 2]); %! assert_equal (ic, [2; 4; 1; 3; 1]); %! assert_equal (C.Age, [38; 38; 40; 43]); %! assert_equal (C.Height, [64; 71; 67; 69]); %! assert_equal (C.Weight, [131; 176; 185; 163]); %! assert_equal (C.Properties.RowNames, {'Jane'; 'Fred'; 'George'; 'Betty'}); %!test # special-type variables (datetime / categorical / calendarDuration) %! dt = datetime (2020, [2; 1; 2; 3], 1); %! [C, ia, ic] = unique (table (dt, 'VariableNames', {'D'})); %! assert_equal (ia, [2; 1; 4]); %! assert_equal (ic, [2; 1; 2; 3]); %! c = categorical ({'mid'; 'low'; 'mid'; 'high'}, {'low', 'mid', 'high'}, ... %! 'Ordinal', true); %! [C, ia, ic] = unique (table (c, 'VariableNames', {'C'})); %! assert_equal (ia, [2; 1; 4]); %! assert_equal (ic, [2; 1; 2; 3]); %! cd = calendarDuration ([0; 1; 0; 2], [3; 0; 3; 0], [0; 0; 0; 0]); %! [C, ia, ic] = unique (table (cd, 'VariableNames', {'CD'})); %! assert_equal (ia, [1; 2; 4]); %! assert_equal (ic, [1; 2; 1; 3]); %!test # text variables sort lexicographically, 'stable' keeps encounter order %! S = {'banana'; 'apple'; 'banana'; 'cherry'}; %! [C, ia, ic] = unique (table (S, 'VariableNames', {'S'})); %! assert_equal (C.S, {'apple'; 'banana'; 'cherry'}); %! assert_equal (ia, [2; 1; 4]); %! assert_equal (ic, [2; 1; 2; 3]); %! C = unique (table (S, 'VariableNames', {'S'}), 'stable'); %! assert_equal (C.S, {'banana'; 'apple'; 'cherry'}); %! St = string ({'banana'; 'apple'; 'banana'; 'cherry'}); %! C = unique (table (St, 'VariableNames', {'St'})); %! assert_equal (cellstr (C.St), {'apple'; 'banana'; 'cherry'}); %!test # a char matrix variable compares whole rows %! [C, ia, ic] = unique (table (['cc'; 'aa'; 'cc'], 'VariableNames', {'K'})); %! assert_equal (C.K, ['aa'; 'cc']); %! assert_equal (ia, [2; 1]); %! assert_equal (ic, [2; 1; 2]); %!test # nested numeric table variable (regression: brace-indexed extraction) %! Ag = [38; 43; 38]; Hi = [71; 69; 71]; %! [C, ia, ic] = unique (table (table (Ag, Hi), 'VariableNames', {'N'})); %! assert_equal (ia, [1; 2]); %! assert_equal (ic, [1; 2; 1]); ## Test input validation for 'unique' method %!test # missing strings equal no other value; empty text is a value %! s = [string('b'); string(missing); string('a')]; %! assert_equal (height (unique (table ([s; string(missing)]))), 4); %! assert_equal (height (unique (table ({''; ''; 'a'}))), 2); %!error unique (tblA, 1, 2); %!error unique (tblA, "some"); %!error ... %! unique (table ({2; 2; 3}), "sorted"); %!error ... %! unique (table (struct ("a", {2; 2; 3})), "sorted"); %!error ... %! unique (table (inputParser ())); %!error ... %! unique (table (table (Age, LastName)), "sorted"); ## Test 'topkrows' ranks missing values last whichever way it sorts %!test %! T = table ([1; NaN; 3; 2], 'VariableNames', {'A'}); %! assert_equal (topkrows (T, 2).A, [3; 2]); %! assert_equal (topkrows (T, 4).A, [3; 2; 1; NaN]); %! assert_equal (topkrows (T, 2, 'A', 'ascend').A, [1; 2]); %! assert_equal (topkrows (T, 4, 'A', 'MissingPlacement', 'first').A, ... %! [NaN; 3; 2; 1]); ## Test 'topkrows' method %!test %! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"}; %! Age = [38; 43; 38; 40; 49]; %! Smoker = logical ([1; 0; 1; 0; 1]); %! Height = [71; 69; 64; 67; 64]; %! Weight = [176; 163; 131; 133; 119]; %! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80]; %! tblA = table (Age, Height, Weight, BloodPressure, 'RowNames', LastName); %! [sorted_tblA, index] = topkrows (tblA, 3, "RowNames"); %! assert_equal (index, [1; 3; 2]); %! assert_equal (issortedrows (sorted_tblA, "RowNames", "descend"), true); %! [sorted_tblA, index] = topkrows (tblA, 4, "RowNames", "descend"); %! assert_equal (index, [1; 3; 2; 4]); %! assert_equal (issortedrows (sorted_tblA, "RowNames", "descend"), true); %!test %! tblA.Properties.DimensionNames(1) = "Patients"; %! assert_equal (issortedrows (tblA, "Patients"), false); %! [sorted_tblA, index] = topkrows (tblA, 2, "Patients"); %! assert_equal (index, [1; 3]); %! assert_equal (issortedrows (sorted_tblA, "Patients", "descend"), true); %! [sorted_tblA, index] = topkrows (tblA, 5, "Patients", "descend"); %! assert_equal (index, [1; 3; 2; 4; 5]); %! assert_equal (issortedrows (sorted_tblA, "Patients", "descend"), true); %!test %! [sorted_tblA, index] = topkrows (tblA, 3); %! assert_equal (index, [5; 2; 4]); %! assert_equal (issortedrows (sorted_tblA, [], "descend"), true); %! [sorted_tblA, index] = topkrows (tblA, 3, ":", "ascend"); %! assert_equal (index, [3; 1; 4]); %! assert_equal (issortedrows (sorted_tblA, [], "ascend"), true); %! [sorted_tblA, index] = topkrows (tblA, 3, [], "descend"); %! assert_equal (index, [5; 2; 4]); %! assert_equal (issortedrows (sorted_tblA, [], "descend"), true); %!test %! [tblB, index] = topkrows (tblA, 3, "Height"); %! assert_equal (index, [1; 2; 4]); %! assert_equal (tblB.Properties.RowNames, {"Sanchez"; "Johnson"; "Diaz"}); %! assert_equal (issortedrows (tblB, "Height", "descend"), true); %!test %! LastName = {"Sweet"; "Jacobson"; "Wang"; "Joiner"; "Berger"}; %! tblA = table (Age, Height, Weight, BloodPressure, 'RowNames', LastName); %! [tblB, index] = topkrows (tblA, 2, {"Height", "Weight"}, ... %! {"ascend", "descend"}); %! assert_equal (index, [3; 5]); %! assert_equal (tblB.Properties.RowNames, {"Wang"; "Berger"}); %! assert_equal (issortedrows (tblB, {"Height", "Weight"}, ... %! {"ascend", "descend"}), true); %!test %! [tblB, index] = topkrows (tblA, 4, {"BloodPressure"}, {"ascend"}); %! assert_equal (index, [2; 4; 5; 1]); %! assert_equal (issortedrows (tblB, {"BloodPressure"}, {"ascend"}), true); %! [tblB, index] = topkrows (tblA, 3, {"BloodPressure"}, {"descend"}); %! assert_equal (index, [3; 1; 5]); %! assert_equal (issortedrows (tblB, "BloodPressure", "descend"), true); %!test %! [tblB, index] = topkrows (tblA, 3, {"Height", "RowNames"}, {"ascend"}); %! assert_equal (index, [5; 3; 4]); %! assert_equal (tblB.Properties.RowNames, {"Berger"; "Wang"; "Joiner"}) %! assert_equal (issortedrows (tblB, {"Height", "RowNames"}, {"ascend"}), true); %! [tblB, index] = topkrows (tblA, 4, {"Height", "RowNames"}, ... %! {"ascend", "descend"}); %! assert_equal (index, [3; 5; 4; 2]); %! assert_equal (tblB.Properties.RowNames, ... %! {"Wang"; "Berger"; "Joiner"; "Jacobson"}); %! assert_equal (issortedrows (tblB, {"Height", "RowNames"}, ... %! {"ascend", "descend"}), true); %!test %! [tblB, index] = topkrows (tblA, 3, [true, true, false, false]); %! assert_equal (index, [5; 2; 4]); %! assert_equal (issortedrows (tblB, [true, true, false, false], "descend"), ... %! true); %! [tblB, index] = topkrows (tblA, 2, [true, true, false, false], "ascend"); %! assert_equal (index, [3; 1]); %! assert_equal (issortedrows (tblB, [true, true, false, false], "ascend"), ... %! true); %! [tblB, index] = topkrows (tblA, 5, [true, true, false, false], "descend"); %! assert_equal (index, flip ([3; 1; 4; 2; 5])); %! assert_equal (issortedrows (tblB, [true, true, false, false], "descend"), ... %! true); %! [tblB, index] = topkrows (tblA, 6, [true, true, false, false], ... %! {"ascend", "descend"}); %! assert_equal (index, [1; 3; 4; 2; 5]); %! assert_equal (issortedrows (tblB, [true, true, false, false], ... %! {"ascend", "descend"}), true); %!test %! ## A positive numeric index sorts descending (opposite of sortrows), so a %! ## positive index matches the descending default of a named variable. %! [sorted_tblA, indexN] = topkrows (tblA, 2, 1); %! assert_equal (issortedrows (sorted_tblA, "Age", "descend"), true); %! [sorted_tblA, indexV] = topkrows (tblA, 2, "Age"); %! assert_equal (issortedrows (sorted_tblA, "Age", "descend"), true); %! assert_equal (indexN, indexV); %! assert_equal (indexN, [5; 2]); %! ## A negative numeric index sorts ascending. %! [sorted_tblA, indexN] = topkrows (tblA, 3, -1); %! assert_equal (issortedrows (sorted_tblA, "Age", "ascend"), true); %! [sorted_tblA, indexV] = topkrows (tblA, 3, "Age", "ascend"); %! assert_equal (indexN, indexV); %! assert_equal (indexN, [1; 3; 4]); %! ## An explicit DIRECTION ignores the sign of a numeric index. %! [sorted_tblA, indexN] = topkrows (tblA, 5, -1, "ascend"); %! [~, indexV] = topkrows (tblA, 5, "Age", "ascend"); %! assert_equal (indexN, indexV); %! [sorted_tblA, indexN] = topkrows (tblA, 3, [-1, 2]); %! [~, indexV] = topkrows (tblA, 3, {"Age", "Height"}, {"ascend", "descend"}); %! assert_equal (indexN, indexV); %! assert_equal (indexN, [1; 3; 4]); %!test %! TT = table (Age, table (Age, Height), BloodPressure, 'RowNames', LastName); %! [sorted_TT, index] = topkrows (TT, 4); %! assert_equal (index, [5; 2; 4; 1]); %! assert_equal (issortedrows (sorted_TT, [], "descend"), true); %! [sorted_TT, index] = topkrows (TT, 3, [1, 2]); %! assert_equal (index, [5; 2; 4]); %! assert_equal (issortedrows (sorted_TT, [-1, -2]), true); %! [sorted_TT, index] = topkrows (TT, 2, [1, -2]); %! assert_equal (index, [5; 2]); %! assert_equal (issortedrows (sorted_TT, [-1, 2]), true); %!test %! Weights = [176;NaN;131;133;NaN]; %! tblN = table (Age, Height, Weights, BloodPressure, 'RowNames', LastName); %! [tblB, index] = topkrows (tblN, 4, "Weights", "MissingPlacement", "first"); %! assert_equal (index, [2; 5; 1; 4]); %! assert_equal (issortedrows (tblB, "Weights", "descend", ... %! "MissingPlacement", "first"), true); %! [tblB, index] = topkrows (tblN, 3, "Weights"); %! assert_equal (index, [1; 4; 3]); %! assert_equal (issortedrows (tblB, "Weights", "descend"), true); %!test %! tblN = table (Age, Height, Weight, BloodPressure, LastName); %! [tblB, index] = topkrows (tblN, 3, vartype ("numeric")); %! assert_equal (index, [5; 2; 4]); %! assert_equal (issortedrows (tblB, vartype ("numeric"), "descend"), true); %! [tblB, index] = topkrows (tblN, 3, vartype ("cellstr")); %! assert_equal (index, [3; 1; 4]); %! assert_equal (issortedrows (tblB, vartype ("cellstr"), "descend"), true); ## special-type sort keys (datetime / categorical / string / calendarDuration) %!test %! dt = datetime (2020, [3; 1; 2; 1], 1); %! [tblB, index] = topkrows (table (dt, 'VariableNames', {'D'}), 2); %! assert_equal (index, [1; 3]); %! assert_equal (issortedrows (tblB, 'D', 'descend'), true); %! c = categorical ({'mid'; 'low'; 'high'}, {'low', 'mid', 'high'}, ... %! 'Ordinal', true); %! [tblB, index] = topkrows (table (c, 'VariableNames', {'C'}), 2); %! assert_equal (index, [3; 1]); %! st = string ({'b'; 'a'; 'c'}); %! [tblB, index] = topkrows (table (st, 'VariableNames', {'S'}), 2); %! assert_equal (index, [3; 1]); %!test # K edge cases: zero, equal to height, larger than height %! Ag = [38; 43; 38]; %! A = table (Ag, 'VariableNames', {'A'}); %! tblB = topkrows (A, 0); %! assert_equal (height (tblB), 0); %! assert_equal (width (tblB), 1); %! [tblB, index] = topkrows (A, 3); %! assert_equal (index, [2; 1; 3]); %! [tblB, index] = topkrows (A, 10); %! assert_equal (index, [2; 1; 3]); %! assert_equal (height (tblB), 3); %!test # 'ComparisonMethod' passes through to sortrows %! z = [3+4i; -1+0i; 0+2i; -5+0i]; %! [tblB, index] = topkrows (table (z, 'VariableNames', {'Z'}), 2, 'Z', ... %! 'ComparisonMethod', 'abs'); %! assert_equal (index, [4; 1]); ## Test input validation for 'topkrows' method %!error ... %! topkrows (table (calmonths ([2; 1])), 1) %!error ... %! topkrows (tblA, -1) %!error ... %! topkrows (tblA, 2.5) ## Test 'addvars' method %!test %! load patients %! T1 = table (Age, Height, Weight); %! T2 = addvars (T1, ones (size (Age))); %! assert_equal (size (head (T2, 3)), [3, 4]); %! assert_equal (T2.Properties.VariableNames, {'Age', 'Height', 'Weight', ... %! 'Var4'}); %!test %! load patients %! T1 = table (LastName, Age, Height, Weight); %! assert_equal (size (head (T1, 3)), [3, 4]); %! assert_equal (T1.Properties.VariableNames, {'LastName', 'Age', 'Height', ... %! 'Weight'}); %! T2 = addvars (T1, Gender, Smoker); %! assert_equal (size (head (T2, 3)), [3, 6]); %! assert_equal (T2.Properties.VariableNames, ... %! {'LastName', 'Age', 'Height', 'Weight', 'Gender', 'Smoker'}); %!test %! load patients %! T1 = table (LastName, Gender); %! assert_equal (size (head (T1, 3)), [3, 2]); %! assert_equal (T1.Properties.VariableNames, {'LastName', 'Gender'}); %! T2 = addvars (T1, Age, 'Before', 'Gender'); %! assert_equal (size (head (T2, 5)), [5, 3]); %! assert_equal (T2.Properties.VariableNames, {'LastName', 'Age', 'Gender'}); %!test %! load patients %! T1 = table (LastName, Age, Gender); %! assert_equal (size (head (T1, 4)), [4, 3]); %! assert_equal (T1.Properties.VariableNames, {'LastName', 'Age', 'Gender'}); %! T2 = addvars (T1, Height, Weight, 'After', 'Age'); %! assert_equal (size (head (T2, 4)), [4, 5]); %! assert_equal (T2.Properties.VariableNames, ... %! {'LastName', 'Age', 'Height', 'Weight', 'Gender'}); %!test %! load patients %! T1 = table (LastName, Age, Height, Weight, Gender); %! assert_equal (size (head (T1, 3)), [3, 5]); %! assert_equal (T1.Properties.VariableNames, ... %! {'LastName', 'Age', 'Height', 'Weight', 'Gender'}); %! T2 = addvars (T1, Smoker, 'After', 1); %! assert_equal (size (head (T2, 3)), [3, 6]); %! assert_equal (T2.Properties.VariableNames, ... %! {'LastName', 'Smoker', 'Age', 'Height', 'Weight', 'Gender'}); %!test %! load patients %! T1 = table (LastName, Age, Gender, Smoker); %! assert_equal (size (head (T1, 3)), [3, 4]); %! assert_equal (T1.Properties.VariableNames, {'LastName', 'Age', 'Gender', ... %! 'Smoker'}); %! T2 = addvars (T1, [Diastolic Systolic], 'NewVariableNames', 'BloodPressure'); %! assert_equal (size (head (T2, 3)), [3, 5]); %! assert_equal (T2.Properties.VariableNames, ... %! {'LastName', 'Age', 'Gender', 'Smoker', 'BloodPressure'}); %!test %! load patients %! T1 = table (LastName, Age, Gender, Smoker); %! assert_equal (size (head (T1, 3)), [3, 4]); %! assert_equal (T1.Properties.VariableNames, {'LastName', 'Age', 'Gender', ... %! 'Smoker'}); %! T2 = addvars (T1, Height, Weight, 'Before', 'Smoker', ... %! 'NewVariableNames', {'Inches', 'Pounds'}); %! assert_equal (size (head (T2, 3)), [3, 6]); %! assert_equal (T2.Properties.VariableNames, ... %! {'LastName', 'Age', 'Gender', 'Inches', 'Pounds', 'Smoker'}); %!test # 'Before'/'After' with logical-vector and numeric LOCATION %! T = table ([1;2], [3;4], [5;6], 'VariableNames', {'A', 'B', 'C'}); %! nv = [7; 8]; %! R = addvars (T, nv, 'Before', [false, true, false], ... %! 'NewVariableNames', 'X'); %! assert_equal (R.Properties.VariableNames, {'A', 'X', 'B', 'C'}); %! R = addvars (T, nv, 'After', [false, true, false], ... %! 'NewVariableNames', 'X'); %! assert_equal (R.Properties.VariableNames, {'A', 'B', 'X', 'C'}); %! R = addvars (T, nv, 'Before', 2, 'NewVariableNames', 'X'); %! assert_equal (R.Properties.VariableNames, {'A', 'X', 'B', 'C'}); %! R = addvars (T, nv, 'Before', 1, 'NewVariableNames', 'X'); %! assert_equal (R.Properties.VariableNames, {'X', 'A', 'B', 'C'}); %! R = addvars (T, nv, 'After', 3, 'NewVariableNames', 'X'); %! assert_equal (R.Properties.VariableNames, {'A', 'B', 'C', 'X'}); %! R = addvars (T, nv, 'After', string ('B'), 'NewVariableNames', 'X'); %! assert_equal (R.Properties.VariableNames, {'A', 'B', 'X', 'C'}); %! R = addvars (T, nv, 'Before', string ('B'), 'NewVariableNames', 'X'); %! assert_equal (R.Properties.VariableNames, {'A', 'X', 'B', 'C'}); %!test # nested-table variable and string-array NewVariableNames %! T = table ([1;2], [3;4], 'VariableNames', {'A', 'B'}); %! inner = table ([7;8], [9;10], 'VariableNames', {'P', 'Q'}); %! R = addvars (T, inner, 'NewVariableNames', 'N'); %! assert_equal (R.Properties.VariableNames, {'A', 'B', 'N'}); %! assert_equal (istable (R.N), true); %! assert_equal (R.N.P, [7; 8]); %! R = addvars (T, [11;12], [13;14], 'NewVariableNames', string ({'X', 'Y'})); %! assert_equal (R.Properties.VariableNames, {'A', 'B', 'X', 'Y'}); ## Test a variable may not take a dimension's name %!test %! T = table ([1; 2], 'VariableNames', {'A'}); %! assert_equal (T.Properties.DimensionNames{1}, 'Row'); %! fail1 = 0; %! try %! addvars (T, [3; 4], 'NewVariableNames', {'Row'}); %! catch err1 %! fail1 = 1; %! end_try_catch %! assert_equal (fail1, 1); %! fail2 = 0; %! try %! renamevars (T, 'A', 'Row'); %! catch err2 %! fail2 = 1; %! end_try_catch %! assert_equal (fail2, 1); ## Test 'mergevars' discards the units of the variables it merges %!test %! T = table ([1; 2], [3; 4], 'VariableNames', {'A', 'B'}); %! T.Properties.VariableUnits = {'m', 's'}; %! M = mergevars (T, {'A', 'B'}); %! assert_equal (M.Properties.VariableUnits, {''}); ## Test a table with neither rows nor variables takes the height of the ## first variable given it %!test %! T = table (); %! T.x = [1; 2; 3]; %! assert_equal (isequal (T, table ([1; 2; 3], 'VariableNames', {'x'})), true); %!test %! T = table (); %! T.x = 5; %! assert_equal (size (T), [1, 1]); %!test %! T = table (); %! T.x = 'abc'; %! assert_equal (T.x, 'abc'); %!test %! T = table (); %! T.Properties.Description = 'd'; %! T.x = [1; 2]; %! assert_equal (T.Properties.Description, 'd'); %!test %! R = addvars (table (), [1; 2; 3], [4; 5; 6]); %! assert_equal (size (R), [3, 2]); %!error ... %! addvars (table (1), @sin); %!error ... %! addvars (table (1), dictionary (1, 2)); %!error ... %! addvars (table (), [1; 2; 3], [4; 5]); ## Test a table with rows and no variables takes a variable of its height %!test %! R = addvars (table ('Size', [3, 0], 'VariableTypes', {}), [1; 2; 3]); %! assert_equal (size (R), [3, 1]); ## Test a string scalar is added as a variable %!test %! T = table (1, 2, 'VariableNames', {'g', 'x'}); %! R = addvars (T, string ('abc'), 'NewVariableNames', 'n'); %! assert_equal (R.n, string ('abc')); ## Test input validation for 'addvars' method %!error ... %! addvars (tblA, Smoker, 'After'); %!error ... %! addvars (tblA, 'abc'); %!error ... %! addvars (tblA, Smoker, 'After', 1, 'Before', 2); %!error ... %! addvars (tblA, Smoker, 'After', [true, false, false, true]); %!error ... %! addvars (tblA, Smoker, 'Before', [true, false, false, true]); %!error ... %! addvars (tblA, Smoker, 'After', [1, 2]); %!error ... %! addvars (tblA, Smoker, 'Before', [1, 2]); %!error ... %! addvars (tblA, Smoker, 'Bogus', 1); %!error ... %! addvars (tblA, 'abc', Smoker); %!error ... %! addvars (tblA, 1); %!error ... %! addvars (tblA, true (5, 1), 1); %!error ... %! addvars (table ('Size', [3, 0], 'VariableTypes', {}), [1; 2]); %!error ... %! addvars (tblA, true (5, 1), 'NewVariableNames', {'A', 'B'}); %!error ... %! addvars (tblA, true (5, 1), true (5, 1), 'NewVariableNames', {'A', 'A'}); %!error ... %! addvars (tblA, true (5, 1), 'NewVariableNames', {'Height'}); %!error ... %! addvars (tblA, true (5, 1), true (5, 1), ... %! 'NewVariableNames', {'Height', 'Weight'}); ## Test 'rename' method %!test %! load patients %! T1 = table (LastName, Age, Gender, Smoker); %! assert_equal (T1.Properties.VariableNames, {'LastName', 'Age', 'Gender', ... %! 'Smoker'}); %! T2 = renamevars (T1, {'LastName', 'Gender'}, {'Name', 'Sex'}); %! assert_equal (T2.Properties.VariableNames, {'Name', 'Age', 'Sex', 'Smoker'}); %!test %! T1 = array2table (rand (5, 3)); %! assert_equal (T1.Properties.VariableNames, {'Var1', 'Var2', 'Var3'}); %! allVars = 1:width (T1); %! newNames = cellfun (@(x) sprintf ("Rand%i",x), num2cell (allVars), ... %! "UniformOutput", false); %! T2 = renamevars (T1, allVars, newNames); %! assert_equal (T2.Properties.VariableNames, {'Rand1', 'Rand2', 'Rand3'}); %!test # VARS forms: char, string array, logical vector, vartype %! T = table ([1;2], [3;4], [5;6], 'VariableNames', {'A', 'B', 'C'}); %! R = renamevars (T, 'B', 'X'); %! assert_equal (R.Properties.VariableNames, {'A', 'X', 'C'}); %! R = renamevars (T, string ({'A', 'C'}), string ({'X', 'Z'})); %! assert_equal (R.Properties.VariableNames, {'X', 'B', 'Z'}); %! R = renamevars (T, [true, false, true], {'X', 'Z'}); %! assert_equal (R.Properties.VariableNames, {'X', 'B', 'Z'}); %! R = renamevars (T, vartype ('numeric'), {'X', 'Y', 'Z'}); %! assert_equal (R.Properties.VariableNames, {'X', 'Y', 'Z'}); %!test # renaming preserves data and per-variable metadata %! T = table ([1;2], [3;4], 'VariableNames', {'A', 'B'}); %! T.Properties.VariableDescriptions = {'da', 'db'}; %! T.Properties.VariableUnits = {'ua', 'ub'}; %! R = renamevars (T, 'A', 'X'); %! assert_equal (R.X, [1; 2]); %! assert_equal (R.Properties.VariableDescriptions, {'da', 'db'}); %! assert_equal (R.Properties.VariableUnits, {'ua', 'ub'}); ## Test input validation for 'renamevars' method %!error ... %! renamevars (tblA, {"Age", "Smoker"}); %!error ... %! renamevars (tblA, {"Age"}, {}); %!error ... %! renamevars (tblA, {}, {"NewName"}); %!error ... %! renamevars (tblA, {"Age", "Smoker"}, {"Age", 4}); %!error ... %! renamevars (tblA, {"Age", "Smoker"}, {"Age", "Age"}); %!error ... %! renamevars (tblA, {"Age", "Smoker"}, {"Age", "User"}); %!error ... %! renamevars (tblA, {"Age", "Height"}, {"Age", "Smoker", "Height"}); %!error ... %! renamevars (tblA, {"Age", "Height"}, {"Age", "Weight"}); ## Test 'movevars' method %!test %! load patients %! T1 = table (LastName, Age, Gender, Smoker); %! assert_equal (T1.Properties.VariableNames, {'LastName', 'Age', 'Gender', ... %! 'Smoker'}); %! T2 = movevars (T1, {'LastName', 'Gender'}); %! assert_equal (T2.Properties.VariableNames, {'Age', 'Smoker', 'LastName', ... %! 'Gender'}); %!test %! load patients %! T1 = table (LastName, Age, Gender, Smoker); %! T2 = movevars (T1, vartype ("cellstr")); %! assert_equal (T2.Properties.VariableNames, {'Age', 'Smoker', 'LastName', ... %! 'Gender'}); %!test %! load patients %! T1 = table (LastName, Age, Gender, Smoker); %! T2 = movevars (T1, vartype ("numeric")); %! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', ... %! 'Smoker', 'Age'}); %!test %! load patients %! T1 = table (LastName, Age, Gender, Smoker); %! T2 = movevars (T1, {'LastName', 'Gender'}, 'After', 'Age'); %! assert_equal (T2.Properties.VariableNames, {'Age', 'LastName', 'Gender', ... %! 'Smoker'}); %!test %! load patients %! T1 = table (LastName, Age, Gender, Smoker); %! T2 = movevars (T1, [true, false, true, false], 'After', 'Age'); %! assert_equal (T2.Properties.VariableNames, {'Age', 'LastName', 'Gender', ... %! 'Smoker'}); %!test %! load patients %! T1 = table (LastName, Age, Gender, Smoker); %! T2 = movevars (T1, [3, 4], 'Before', 'Age'); %! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', ... %! 'Smoker', 'Age'}); %!test %! load patients %! T1 = table (LastName, Age, Gender, Smoker); %! T2 = movevars (T1, [3, 4], 'Before', 2); %! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', ... %! 'Smoker', 'Age'}); %!test %! load patients %! T1 = table (LastName, Age, Gender, Smoker); %! T2 = movevars (T1, [3, 4], 'Before', [false, true, false, false]); %! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', ... %! 'Smoker', 'Age'}); %!test # VARS forms (string array, char single) and LOCATION edges %! T = table ([1;2], [3;4], [5;6], [7;8], 'VariableNames', {'A', 'B', 'C', ... %! 'D'}); %! R = movevars (T, string ({'A', 'C'})); %! assert_equal (R.Properties.VariableNames, {'B', 'D', 'A', 'C'}); %! R = movevars (T, 'A'); %! assert_equal (R.Properties.VariableNames, {'B', 'C', 'D', 'A'}); %! R = movevars (T, {'C', 'D'}, 'Before', 1); %! assert_equal (R.Properties.VariableNames, {'C', 'D', 'A', 'B'}); %! R = movevars (T, 'C', 'Before', 'A'); %! assert_equal (R.Properties.VariableNames, {'C', 'A', 'B', 'D'}); %! R = movevars (T, 'A', 'After', 4); %! assert_equal (R.Properties.VariableNames, {'B', 'C', 'D', 'A'}); %! R = movevars (T, 'A', 'After', string ('C')); %! assert_equal (R.Properties.VariableNames, {'B', 'C', 'A', 'D'}); %! R = movevars (T, 'D', 'Before', string ('B')); %! assert_equal (R.Properties.VariableNames, {'A', 'D', 'B', 'C'}); ## Test input validation for 'movevars' method %!error movevars (tblA); %!error movevars (tblA, {}); %!error ... %! movevars (tblA, 1, 'After'); %!error ... %! movevars (tblA, 1, "After", "Height", "Before", "Weight"); %!error ... %! movevars (tblA, 1, "After", [true, true, false, false]); %!error ... %! movevars (tblA, 1, "After", [1, 2]); %!error ... %! movevars (tblA, 1, "After", {"Height"}); %!error ... %! movevars (tblA, 1, "After", "Smoker"); %!error ... %! movevars (tblA, 1, 'Bogus', 1); %!error ... %! movevars (tblA, "Smoker", "After", 2); %!error ... %! movevars (tblA, {"Age", "Height"}, "After", 2); %!error ... %! movevars (tblA, {"Height", "Weight"}, "Before", "Height"); ## Test 'removevars' method %!test %! load patients %! T1 = table (LastName, Age, Gender, Smoker); %! T2 = removevars (T1, {'LastName', 'Gender'}); %! assert_equal (T2.Properties.VariableNames, {'Age', 'Smoker'}); %!test %! load patients %! T1 = table (LastName, Age, Gender, Smoker); %! T2 = removevars (T1, [true, false, true, false]); %! assert_equal (T2.Properties.VariableNames, {'Age', 'Smoker'}); %!test %! load patients %! T1 = table (LastName, Age, Gender, Smoker); %! T2 = removevars (T1, [1, 3]); %! assert_equal (T2.Properties.VariableNames, {'Age', 'Smoker'}); %!test %! load patients %! T1 = table (LastName, Age, Gender, Smoker); %! T2 = removevars (T1, vartype ("numeric")); %! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', 'Smoker'}); %!test # VARS forms (string array, single char) and removing all variables %! T = table ([1;2], [3;4], [5;6], 'VariableNames', {'A', 'B', 'C'}); %! R = removevars (T, string ({'A', 'C'})); %! assert_equal (R.Properties.VariableNames, {'B'}); %! R = removevars (T, 'B'); %! assert_equal (R.Properties.VariableNames, {'A', 'C'}); %! R = removevars (T, {'A', 'B', 'C'}); %! assert_equal (width (R), 0); %!test # custom variable properties shrink with the removed variables %! T = table ([1;2], [3;4], [5;6], 'VariableNames', {'A', 'B', 'C'}); %! T = addprop (T, 'Note', 'variable'); %! T.Properties.CustomProperties.Note = {'na', 'nb', 'nc'}; %! R = removevars (T, 'B'); %! assert_equal (R.Properties.VariableNames, {'A', 'C'}); %! assert_equal (R.Properties.CustomProperties.Note, {'na', 'nc'}); %! R = removevars (T, [1, 3]); %! assert_equal (R.Properties.CustomProperties.Note, {'nb'}); ## Test input validation for 'removevars' method %!error removevars (tblA); %!error removevars (tblA, ones (2)); %!error ... %! removevars (tblA, [true, true, false]); %!error ... %! removevars (tblA, [1, 5]); %!error ... %! removevars (tblA, [1, 2, 7]); %!error ... %! removevars (tblA, "Smoker"); %!error ... %! removevars (tblA, {"Smoker", "Health"}); %!error ... %! removevars (tblA, {1}); %!error ... %! removevars (tblA, struct ("A", 1)); ## Test the row count of a table left with no variables %!test # removing every variable keeps the height %! T = table ([1;2;3;4;5], [6;7;8;9;10], 'VariableNames', {'A', 'B'}); %! R = removevars (T, {'A', 'B'}); %! assert_equal (size (R), [5, 0]); %! assert_equal (height (R), 5); %! assert_equal (width (R), 0); %! assert_equal (isempty (R), true); %!test # the row names count the rows once the variables are gone %! T = table ([1;2;3], 'VariableNames', {'A'}, 'RowNames', {'a', 'b', 'c'}); %! R = removevars (T, 'A'); %! assert_equal (size (R), [3, 0]); %! assert_equal (R.Properties.RowNames, {'a'; 'b'; 'c'}); %!test # rows subset out of a table with no variables %! T = table ([1;2;3;4;5], 'VariableNames', {'A'}); %! R = removevars (T, 'A'); %! assert_equal (size (R(2:3,:)), [2, 0]); %! assert_equal (height (R(:,:)), 5); %!test # the height survives conversion to an array or a cell %! T = table ([1;2;3;4;5], 'VariableNames', {'A'}); %! R = removevars (T, 'A'); %! assert_equal (size (table2array (R)), [5, 0]); %! assert_equal (size (table2cell (R)), [5, 0]); %! assert_equal (size (R{:,:}), [5, 0]); %!test # vertical concatenation adds the row counts up %! T = table ([1;2;3;4;5], 'VariableNames', {'A'}); %! R = removevars (T, 'A'); %! assert_equal (height ([R; R]), 10); %! assert_equal (height ([R; R(1:2,:)]), 7); %!test # a variable added back at the right height restores the table %! T = table ([1;2;3;4;5], 'VariableNames', {'A'}); %! R = removevars (T, 'A'); %! R.C = (1:5)'; %! assert_equal (size (R), [5, 1]); %! assert_equal (R.C, (1:5)'); %!test # sorting, deduplicating and summarising a table with no variables %! T = table ([1;2;3;4;5], 'VariableNames', {'A'}); %! R = removevars (T, 'A'); %! assert_equal (size (sortrows (R)), [5, 0]); %! assert_equal (size (unique (R)), [1, 0]); %! assert_equal (fieldnames (summary (R)), cell (0, 1)); %!test # the stored count never makes two otherwise-equal tables differ %! A = [1;2;3]; %! B = [4;5;6]; %! T = table (A, B, [7;8;9], 'VariableNames', {'A', 'B', 'C'}); %! assert_equal (isequal (removevars (T, 'C'), table (A, B)), true); ## Test an empty variable subscript %!test # an empty numeric subscript selects no variables and keeps the height %! T = table ([1;2;3;4;5], [6;7;8;9;10], 'VariableNames', {'A', 'B'}); %! assert_equal (size (T(:,[])), [5, 0]); %! assert_equal (height (T(:,[])), 5); %! assert_equal (size (T(:,zeros (0, 3))), [5, 0]); %! assert_equal (size (T(1:2,[])), [2, 0]); %!test # an empty variable reference keeps the row names %! T = table ([1;2;3], 'VariableNames', {'A'}, 'RowNames', {'a', 'b', 'c'}); %! assert_equal (T(:,[]).Properties.RowNames, {'a'; 'b'; 'c'}); %!test # an empty logical subscript selects no variables too %! T = table ([1;2;3;4;5], [6;7;8;9;10], 'VariableNames', {'A', 'B'}); %! assert_equal (size (T(:,logical ([]))), [5, 0]); %! assert_equal (height (T(:,logical ([]))), 5); ## Test an empty cell is not a variable subscript, as it is not on any array %!error ... %! T = table ([1;2], [3;4]); T(:,{}); ## Test input validation for a table left with no variables %!error ... %! T = table ([1;2;3;4;5]); R = removevars (T, 'Var1'); R(6,:); %!error ... %! T = table ([1;2;3;4;5]); R = removevars (T, 'Var1'); R(0,:); ## Test 'splitvars' method %!test %! A = [1:3]'; %! B = rand (3); %! C = {'a', 'XX'; 'b', 'YY'; 'c', 'ZZ'}; %! D = {"A"; "B"; "C"}; %! T1 = table (A, B, C, D); %! T2 = splitvars (T1); %! assert_equal (T2.Properties.VariableNames, ... %! {'A', 'B_1', 'B_2', 'B_3', 'C_1', 'C_2', 'D'}); %!test %! A = [1:3]'; %! B = rand (3); %! C = {'a', 'XX'; 'b', 'YY'; 'c', 'ZZ'}; %! D = {"A"; "B"; "C"}; %! T1 = table (A, B, C, D); %! T2 = addvars (T1, table ([5; 5; 5])); %! assert_equal (size (T2.Var5), [3, 1]); %! T3 = splitvars (T2); %! assert_equal (T3.Properties.VariableNames, ... %! {'A', 'B_1', 'B_2', 'B_3', 'C_1', 'C_2', 'D', 'Var1'}); %! assert_equal (size (T3.Var1), [3, 1]); %!test %! A = [1:3]'; %! B = rand (3); %! C = {'a', 'XX'; 'b', 'YY'; 'c', 'ZZ'}; %! D = {"A"; "B"; "C"}; %! T1 = table (A, B, C, D); %! T2 = addvars (T1, table ([5, 6; 5, 6; 5, 6])); %! assert_equal (size (T2.Var5), [3, 1]); %! T3 = splitvars (T2); %! assert_equal (T3.Properties.VariableNames, ... %! {'A', 'B_1', 'B_2', 'B_3', 'C_1', 'C_2', 'D', 'Var1'}); %! assert_equal (size (T3.Var1), [3, 2]); %!test %! A = [1:3]'; %! B = rand (3); %! C = {'a', 'XX'; 'b', 'YY'; 'c', 'ZZ'}; %! D = {"A"; "B"; "C"}; %! T1 = table (A, B, C, D); %! A = [5; 5; 5]; %! T2 = addvars (T1, table (A)); %! T3 = splitvars (T2); %! assert_equal (T3.Properties.VariableNames, ... %! {'A', 'B_1', 'B_2', 'B_3', 'C_1', 'C_2', 'D', 'Var5_A'}); %!test %! A = [1:3]'; %! B = rand (3); %! C = {'a', 'XX'; 'b', 'YY'; 'c', 'ZZ'}; %! D = {"A"; "B"; "C"}; %! T1 = table (A, B, C, D); %! A = [5; 5; 5]; %! T2 = addvars (T1, table ([5, 6; 5, 6; 5, 6]), table (A)); %! T3 = splitvars (T2); %! assert_equal (T3.Properties.VariableNames, ... %! {'A', 'B_1', 'B_2', 'B_3', 'C_1', 'C_2', 'D', 'Var1', 'Var6_A'}); %!test %! load patients %! Personal_Data = table (Gender, Age); %! BMI_Data = table (Height, Weight); %! BloodPressure = table (Systolic, Diastolic); %! T1 = table (LastName, Personal_Data, BMI_Data, BloodPressure); %! T2 = splitvars (T1, 'BloodPressure'); %! assert_equal (T2.Properties.VariableNames, ... %! {'LastName', 'Personal_Data', 'BMI_Data', 'Systolic', 'Diastolic'}); %!test %! load patients %! Personal_Data = table (Gender, Age); %! BMI_Data = table (Height, Weight); %! BloodPressure = table (Systolic, Diastolic); %! T1 = table (LastName, Personal_Data, BMI_Data, BloodPressure); %! T2 = splitvars (T1, {'BMI_Data', 'BloodPressure'}); %! assert_equal (T2.Properties.VariableNames, {'LastName', 'Personal_Data', ... %! 'Height', 'Weight', 'Systolic', 'Diastolic'}); %!test %! load patients %! Personal_Data = table (Gender, Age); %! BMI_Data = table (Height, Weight); %! BloodPressure = table (Systolic, Diastolic); %! T1 = table (LastName, Personal_Data, BMI_Data, BloodPressure); %! T2 = splitvars (T1, [2, 4]); %! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', 'Age', ... %! 'BMI_Data', 'Systolic', 'Diastolic'}); %!test %! load patients %! Personal_Data = table (Gender, Age); %! BMI_Data = table (Height, Weight); %! BloodPressure = table (Systolic, Diastolic); %! T1 = table (LastName, Personal_Data, BMI_Data, BloodPressure); %! T2 = splitvars (T1, [false, true, false, true]); %! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', 'Age', ... %! 'BMI_Data', 'Systolic', 'Diastolic'}); %!test %! load patients %! Personal_Data = [Age, Height, Weight]; %! BloodPressure = [Systolic, Diastolic]; %! T1 = table (LastName, Gender, Personal_Data, BloodPressure); %! T2 = splitvars (T1, 'BloodPressure', ... %! 'NewVariableNames', {'Systolic','Diastolic'}); %! assert_equal (T2.Properties.VariableNames, ... %! {'LastName', 'Gender', 'Personal_Data', 'Systolic', 'Diastolic'}); %!test %! load patients %! Personal_Data = [Age, Height, Weight]; %! BloodPressure = [Systolic, Diastolic]; %! T1 = table (LastName, Gender, Personal_Data, BloodPressure); %! T2 = splitvars (T1, {'Personal_Data', 'BloodPressure'}, ... %! 'NewVariableNames', {{'Age', 'Height', 'Weight'}, ... %! {'Systolic', 'Diastolic'}}); %! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', ... %! 'Age', 'Height', 'Weight', 'Systolic', 'Diastolic'}); %!test %! load patients %! Personal_Data = [Age, Height, Weight]; %! BloodPressure = [Systolic, Diastolic]; %! T1 = table (LastName, Gender, Personal_Data, BloodPressure); %! new_PD = string ({'Age', 'Height', 'Weight'}); %! T2 = splitvars (T1, {'Personal_Data', 'BloodPressure'}, ... %! 'NewVariableNames', {new_PD, {'Systolic', 'Diastolic'}}); %! assert_equal (T2.Properties.VariableNames, {'LastName', 'Gender', ... %! 'Age', 'Height', 'Weight', 'Systolic', 'Diastolic'}); %!test # duplicate inner names across nested tables get disambiguated %! n1 = table ([1;2], [3;4], 'VariableNames', {'X', 'Y'}); %! n2 = table ([5;6], [7;8], 'VariableNames', {'X', 'Y'}); %! T = table (n1, n2, 'VariableNames', {'P', 'Q'}); %! R = splitvars (T); %! assert_equal (R.Properties.VariableNames, {'P_X', 'P_Y', 'Q_X', 'Q_Y'}); %! assert_equal (R.P_X, [1; 2]); %! assert_equal (R.Q_Y, [7; 8]); %!test # splitting a multicolumn special-type variable preserves its class %! d = hours ([1, 2; 3, 4; 5, 6]); %! R = splitvars (table (d, 'VariableNames', {'D'})); %! assert_equal (R.Properties.VariableNames, {'D_1', 'D_2'}); %! assert_equal (class (R.D_1), 'duration'); %! assert_equal (hours (R.D_1), [1; 3; 5]); %! assert_equal (hours (R.D_2), [2; 4; 6]); %!test # string-array VARS, single-column var ignored, data preserved %! n = table ([1;2], [3;4], 'VariableNames', {'X', 'Y'}); %! T = table ([9;9], n, 'VariableNames', {'A', 'N'}); %! R = splitvars (T, string ('N')); %! assert_equal (R.Properties.VariableNames, {'A', 'X', 'Y'}); %! T = table ([1;2], [3, 4; 5, 6], 'VariableNames', {'S', 'M'}); %! R = splitvars (T, 'S'); %! assert_equal (R.Properties.VariableNames, {'S', 'M'}); %! R = splitvars (table ([10, 20; 30, 40], 'VariableNames', {'M'})); %! assert_equal (R.M_1, [10; 30]); %! assert_equal (R.M_2, [20; 40]); ## Test input validation for 'splitvars' method %!error splitvars (tblA, 1, 2, 3, 4); %!error ... %! splitvars (table ([5, 6; 5, 6; 5, 6]), 1, 'NewVariableNames', {1}) %!error ... %! splitvars (table ([5, 6; 5, 6; 5, 6]), 1, 'NewVariableNames', 1) %!error ... %! splitvars (table ([5, 6; 5, 6; 5, 6]), 1, 'NewVariableNames', "new_name") %!error ... %! splitvars (table ([5, 6; 5, 6; 5, 6], ones (3, 2)), ":", ... %! 'NewVariableNames', {{"A", "B"}, {"new_name", 1}}) %!error ... %! splitvars (table ([5, 6; 5, 6; 5, 6], ones (3, 2)), ":", ... %! 'NewVariableNames', {{"A", "B"}, {"C", "D", "E"}}) ## Test 'mergevars' method %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! D = string ({"a"; "b"; "c"}); %! T1 = table (A, B, C, D); %! T2 = mergevars (T1, [2, 3]); %! assert_equal (T2.Properties.VariableNames, {'A', 'Var2', 'D'}); %! assert_equal (isequal (T2.Var2, cat (2, B, C)), true); %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! D = string ({"a"; "b"; "c"}); %! T1 = table (A, B, C, D); %! T2 = mergevars (T1, [2, 3], 'NewVariableName', 'Merged'); %! assert_equal (T2.Properties.VariableNames, {'A', 'Merged', 'D'}); %! assert_equal (isequal (T2.Merged, cat (2, B, C)), true); %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! D = string ({"a"; "b"; "c"}); %! T1 = table (A, B, C, D); %! T2 = mergevars (T1, [2, 3], 'MergeAsTable', true); %! assert_equal (T2.Properties.VariableNames, {'A', 'Var2', 'D'}); %! assert_equal (isequal (T2.Var2, table (B, C)), true); %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! D = string ({"a"; "b"; "c"}); %! T1 = table (A, B, C, D); %! T2 = mergevars (T1, [2, 3], 'NewVariableName', 'Merged', 'MergeAsTable', 1); %! assert_equal (T2.Properties.VariableNames, {'A', 'Merged', 'D'}); %! assert_equal (isequal (T2.Merged, table (B, C)), true); %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! D = string ({"a"; "b"; "c"}); %! T1 = table (A, B, C, D); %! T2 = mergevars (T1, {"A", "B"}); %! assert_equal (T2.Properties.VariableNames, {'Var1', 'C', 'D'}); %! assert_equal (isequal (T2.Var1, cat (2, A, B)), true); %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! D = string ({"a"; "b"; "c"}); %! T1 = table (A, B, C, D); %! T2 = mergevars (T1, {"A", "B"}, 'NewVariableName', 'Merged'); %! assert_equal (T2.Properties.VariableNames, {'Merged', 'C', 'D'}); %! assert_equal (isequal (T2.Merged, cat (2, A, B)), true); %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! D = string ({"a"; "b"; "c"}); %! T1 = table (A, B, C, D); %! T2 = mergevars (T1, {"A", "B"}, 'MergeAsTable', true); %! assert_equal (T2.Properties.VariableNames, {'Var1', 'C', 'D'}); %! assert_equal (isequal (T2.Var1, table (A, B)), true); %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! D = string ({"a"; "b"; "c"}); %! T1 = table (A, B, C, D); %! T2 = mergevars (T1, {"A", "B"}, 'NewVariableName', 'Merged', ... %! 'MergeAsTable', true); %! assert_equal (T2.Properties.VariableNames, {'Merged', 'C', 'D'}); %! assert_equal (isequal (T2.Merged, table (A, B)), true); %!test # string-array VARS and string-scalar NewVariableName %! T = table ([1;2], [3;4], [5;6], [7;8], 'VariableNames', {'A', 'B', 'C', ... %! 'D'}); %! R = mergevars (T, string ({'B', 'C'}), 'NewVariableName', string ('M')); %! assert_equal (R.Properties.VariableNames, {'A', 'M', 'D'}); %! assert_equal (R.M, [3, 5; 4, 6]); %!test # logical and vartype VARS, and a single-variable merge is a no-op %! T = table ([1;2], [3;4], [5;6], [7;8], 'VariableNames', {'A', 'B', 'C', ... %! 'D'}); %! R = mergevars (T, [false, true, true, false]); %! assert_equal (R.Properties.VariableNames, {'A', 'Var2', 'D'}); %! R = mergevars (T, vartype ('numeric')); %! assert_equal (R.Properties.VariableNames, {'Var1'}); %! R = mergevars (T, 2); %! assert_equal (R.Properties.VariableNames, {'A', 'B', 'C', 'D'}); %!test # non-contiguous VARS merge keeps the first-merged position %! T = table ([1;2], [3;4], [5;6], [7;8], 'VariableNames', {'A', 'B', 'C', ... %! 'D'}); %! R = mergevars (T, [2, 4]); %! assert_equal (R.Properties.VariableNames, {'A', 'Var2', 'C'}); %! assert_equal (R.Var2, [3, 7; 4, 8]); %!test # merging special-type variables preserves their class %! T = table (hours ([1;2]), hours ([3;4]), 'VariableNames', {'P', 'Q'}); %! R = mergevars (T, [1, 2]); %! assert_equal (class (R.Var1), 'duration'); %! assert_equal (hours (R.Var1), [1, 3; 2, 4]); %!test # a merged variable has no custom property entry of its own %! T = table ([1;2], [3;4], [5;6], [7;8], 'VariableNames', {'A', 'B', 'C', ... %! 'D'}); %! T = addprop (T, 'Note', 'variable'); %! T.Properties.CustomProperties.Note = {'nA', 'nB', 'nC', 'nD'}; %! R = mergevars (T, [2, 3]); %! assert_equal (R.Properties.VariableNames, {'A', 'Var2', 'D'}); %! assert_equal (R.Properties.CustomProperties.Note, {'nA', [], 'nD'}); ## Test input validation for 'mergevars' method %!error mergevars (tblA); %!error ... %! mergevars (tblA, {'Height', 'Weight'}, 'NewVariableName'); %!error ... %! mergevars (tblA, {'Height', 'Weight'}, 'MergeAsTable', 'on'); %!error ... %! mergevars (tblA, {'Height', 'Weight'}, 'MergeAsTable', 2); %!error ... %! mergevars (tblA, {'Height', 'Weight'}, 'MergeAsTable', [true, true]); %!error ... %! mergevars (tblA, {'Height', 'Weight'}, 'Bogus', 1); %!error ... %! mergevars (tblA, {'Height', 'Weight'}, 'NewVariableName', 2) %!error ... %! mergevars (tblA, {'Height', 'Weight'}, 'NewVariableName', 'Age'); %!error ... %! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"}; %! mergevars (addvars (tblA, LastName), {'Height', 'LastName'}); ## Test 'convertvars' method %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! D = string ({"a"; "b"; "c"}); %! T1 = table (A, B, C, D); %! assert_equal (isa (T1.A, "numeric"), true); %! assert_equal (isa (T1.B, "numeric"), true); %! T2 = convertvars (T1, {"A", "B"}, "string"); %! assert_equal (isa (T2.A, "string"), true); %! assert_equal (isa (T2.B, "string"), true); %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! D = string ({"a"; "b"; "c"}); %! T1 = table (A, B, C, D); %! assert_equal (isa (T1.D, "string"), true); %! T2 = convertvars (T1, 4, "cellstr"); %! assert_equal (iscellstr (T2.D), true); %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! D = string ({"a"; "b"; "c"}); %! T1 = table (A, B, C, D); %! assert_equal (isa (T1.D, "string"), true); %! T2 = convertvars (T1, [1:3], @(x) sqrt (x)); %! assert_equal (isequal (T2.A, sqrt (A)), true); %! assert_equal (isequal (T2.B, sqrt (B)), true); %! assert_equal (isequal (T2.C, sqrt (C)), true); %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! D = string ({"a"; "b"; "c"}); %! T1 = table (A, B, C, D); %! assert_equal (isa (T1.D, "string"), true); %! T2 = convertvars (T1, [1:3], @(x) [x, x, x]); %! assert_equal (size (T2.A, 2), 3); %! assert_equal (size (T2.B, 2), 3); %! assert_equal (size (T2.C, 2), 3); %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! D = {"a"; "b"; "c"}; %! T1 = table (A, B, C, D); %! T2 = convertvars (T1, "D", @(x) cellstr (upper (char (x)))); %! assert_equal (T2.D, {"A"; "B"; "C"}); %!test # VARS forms: string array, logical vector, vartype %! T = table ([1;2;3], [4;5;6], {'x'; 'y'; 'z'}, 'VariableNames', {'A', 'B', ... %! 'C'}); %! R = convertvars (T, string ({'A', 'B'}), 'string'); %! assert_equal (isa (R.A, 'string'), true); %! assert_equal (isa (R.B, 'string'), true); %! R = convertvars (T, [true, false, false], 'single'); %! assert_equal (class (R.A), 'single'); %! R = convertvars (T, vartype ('numeric'), 'int32'); %! assert_equal (class (R.A), 'int32'); %! assert_equal (class (R.B), 'int32'); %!test # conversion to special data types (categorical, duration) %! T = table ([1;2;3], {'x'; 'y'; 'z'}, 'VariableNames', {'A', 'C'}); %! R = convertvars (T, 'C', 'categorical'); %! assert_equal (class (R.C), 'categorical'); %! R = convertvars (T, 'A', @hours); %! assert_equal (class (R.A), 'duration'); %! assert_equal (hours (R.A), [1; 2; 3]); ## Test input validation for 'convertvars' method %!error ... %! convertvars (table (1, 'VariableNames', {'a'}), 'a', @(x) @sin); %!error ... %! convertvars (table (1, 'VariableNames', {'a'}), 'a', ... %! @(x) dictionary (x, 2)); %!error convertvars (tblA); %!error ... %! convertvars (tblA, "Height"); %!error ... %! convertvars (tblA, "Height", ["c","h";"a","r"]); %!error ... %! convertvars (tblA, "Height", {"char"}); %!error ... %! convertvars (tblA, "Height", 25); %!error ... %! convertvars (tblA, "Height", "struct"); %!error ... %! convertvars (tblA, "Height", @(x) sum (x)); ## Test 'rows2vars' method %!test %! load patients %! T1 = table (LastName, Gender, Age, Height, Weight); %! T2 = rows2vars (T1); %! assert_equal (size (T1, 2), size (T2, 1)); %! assert_equal (size (T1, 1) + 1, size (T2, 2)); %! assert_equal (T2.OriginalVariableNames, ... %! {"LastName"; "Gender"; "Age"; "Height"; "Weight"}); %! assert_equal (T2.Var1, {"Smith"; "Male"; 38; 71; 176}); %!test %! load patients %! T1 = table (Gender, Age, Height, Weight, "RowNames", LastName); %! T2 = rows2vars (T1); %! assert_equal (T2.OriginalVariableNames, ... %! {"Gender"; "Age"; "Height"; "Weight"}); %! assert_equal (T2.Smith, {"Male"; 38; 71; 176}); %! assert_equal (T2.Johnson, {"Male"; 43; 69; 163}); %! assert_equal (T2.Williams, {"Female"; 38; 64; 131}); %!test %! load patients %! T1 = table (LastName, Gender, Age, Height, Weight); %! T2 = rows2vars (T1, "VariableNamesSource", "LastName"); %! assert_equal (T2.OriginalVariableNames, ... %! {"Gender"; "Age"; "Height"; "Weight"}); %! assert_equal (T2.Smith, {"Male"; 38; 71; 176}); %! assert_equal (T2.Johnson, {"Male"; 43; 69; 163}); %! assert_equal (T2.Williams, {"Female"; 38; 64; 131}) %!test %! load patients %! T1 = table (LastName, Gender, Age, Height, Weight); %! T2 = rows2vars (T1, "DataVariables", {"LastName", "Gender", "Age"}); %! assert_equal (size (T2, 1), 3); %! assert_equal (size (T1, 1) + 1, size (T2, 2)); %! assert_equal (T2.OriginalVariableNames, {"LastName"; "Gender"; "Age"}); %! assert_equal (T2.Var1, {"Smith"; "Male"; 38}); %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! T1 = table (A, B, C); %! T2 = rows2vars (T1); %! assert_equal (class (T2.Var1), "double"); %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! D = {"a"; "b"; "c"}; %! T1 = table (A, B, C, D); %! T2 = rows2vars (T1); %! assert_equal (class (T2.Var1), "cell"); %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! T1 = table (A, B, C); %! T2 = rows2vars (T1, "VariableNamesSource", "A"); %! assert_equal (size (T2), [2, 4]); %! assert_equal (T2.Properties.VariableNames(1), {"OriginalVariableNames"}); %! assert_equal (T2.Properties.VariableNames(2:4), {"x1", "x2", "x3"}); %! assert_equal (T2.x1, [5; 3.14]); %!test %! A = [1:3]'; %! B = [5; 11; 12]; %! C = [3.14; 2.72; 1.37]; %! T1 = table (A, B, C); %! T2 = rows2vars (T1, "VariableNamesSource", "A", ... %! "VariableNamingRule", "preserve"); %! assert_equal (size (T2), [2, 4]); %! assert_equal (T2.Properties.VariableNames(1), {"OriginalVariableNames"}); %! assert_equal (T2.Properties.VariableNames(2:4), {"1", "2", "3"}); %! assert_equal (T2.('1'), [5; 3.14]); %!test # DataVariables forms: numeric, logical, string array, vartype %! T = table ([1;2;3], [4;5;6], [7;8;9], 'VariableNames', {'A', 'B', 'C'}); %! R = rows2vars (T, 'DataVariables', [1, 2]); %! assert_equal (R.OriginalVariableNames, {'A'; 'B'}); %! R = rows2vars (T, 'DataVariables', [true, false, true]); %! assert_equal (R.OriginalVariableNames, {'A'; 'C'}); %! R = rows2vars (T, 'DataVariables', string ({'A', 'C'})); %! assert_equal (R.OriginalVariableNames, {'A'; 'C'}); %! R = rows2vars (T, 'DataVariables', vartype ('numeric')); %! assert_equal (R.OriginalVariableNames, {'A'; 'B'; 'C'}); %!test # VariableNamesSource by numeric index and from a string variable %! T = table ([1;2;3], [4;5;6], 'VariableNames', {'A', 'B'}); %! R = rows2vars (T, 'VariableNamesSource', 1); %! assert_equal (R.Properties.VariableNames, ... %! {'OriginalVariableNames', 'x1', 'x2', 'x3'}); %! assert_equal (R.OriginalVariableNames, {'B'}); %! T2 = table (string ({'p'; 'q'; 'r'}), [4;5;6], 'VariableNames', {'S', 'B'}); %! R = rows2vars (T2, 'VariableNamesSource', 'S'); %! assert_equal (R.Properties.VariableNames, ... %! {'OriginalVariableNames', 'p', 'q', 'r'}); %! assert_equal (R.OriginalVariableNames, {'B'}); %!test # each new variable joins its row as 'table2array' joins it %! R = rows2vars (table (int8 ([7; 3]), [1; 2], 'VariableNames', {'A', 'K'})); %! assert_equal (R.Var1, int8 ([7; 1])); %!test %! R = rows2vars (table (hours ([1; 2]), [3; 4], 'VariableNames', {'D', 'K'})); %! assert_equal (R.Var1, hours ([1; 72])); %!test %! R = rows2vars (table (string ({'x'; 'y'}), [3; 4], ... %! 'VariableNames', {'S', 'K'})); %! assert_equal (R.Var2, string ({'y'; '4'})); ## Test input validation for 'rows2vars' method %!error ... %! rows2vars (tblA, 'DataVariables'); %!error ... %! rows2vars (tblA, 'Bogus', 1); %!error ... %! rows2vars (tblA, "DataVariables", "Some"); %!error ... %! rows2vars (tblA, "VariableNamesSource", {"LastName", "Age"}); %!error ... %! rows2vars (tblA, "VariableNamesSource", {"Some"}); ## a repeated name from 'VariableNamesSource' is numbered, not refused, and ## the columns take the name of the variable that named them %!test %! age = [38; 43; 38; 40; 49]; %! hgt = [71; 69; 64; 67; 64]; %! T = table (age, hgt, 'VariableNames', {'Age', 'Height'}); %! R = rows2vars (T, 'VariableNamesSource', {'Age'}); %! assert_equal (R.Properties.VariableNames, ... %! {'OriginalVariableNames', 'x38', 'x43', 'x38_1', 'x40', ... %! 'x49'}); %! assert_equal (R.Properties.DimensionNames, {'Row', 'Age'}); %!error ... %! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"}; %! Age = [38; 43; 38; 40; 49]; %! rows2vars (table (LastName, Age), "DataVariables", "LastName", ... %! "VariableNamesSource", "LastName"); %!error ... %! rows2vars (tblA, "VariableNamingRule", "somerule"); %!error ... %! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"}; %! rows2vars (table (tblA, LastName)); %!error ... %! rows2vars (tblA); ## Test 'stack' method %!test %! Test1 = [93; 57; 87; 89]; %! Test2 = [89; 77; 92; 86]; %! Test3 = [95; 62; 89; 91]; %! U = table (Test1, Test2, Test3); %! S = stack (U, 1:3); %! assert_equal (size (S), [12, 2]); %! assert_equal (class (S.Test1_Test2_Test3_Indicator), "categorical"); %! assert_equal (cellstr (S.Test1_Test2_Test3_Indicator), ... %! repmat ({"Test1"; "Test2"; "Test3"}, 4, 1)); %! assert_equal (S.Test1_Test2_Test3, [Test1';Test2';Test3'](:)); %!test %! Test1 = [93; 57; 87; 89]; %! Test2 = [89; 77; 92; 86]; %! Test3 = [95; 62; 89; 91]; %! U = table (Test1, Test2, Test3); %! [S, idx] = stack (U, 1:2); %! assert_equal (idx, [1; 1; 2; 2; 3; 3; 4; 4]); %! assert_equal (cellstr (S.Test1_Test2_Indicator), ... %! repmat ({"Test1"; "Test2"}, 4, 1)); %! assert_equal (S.Test3, repelem (Test3, 2, 1)); %!test %! Test1 = [93; 57; 87; 89]; %! Test2 = [89; 77; 92; 86]; %! Test3 = [95; 62; 89; 91]; %! U = table (Test1, Test2, Test3); %! S = stack (U, 1:3, 'NewDataVariableName', 'NewData', ... %! 'IndexVariableName', 'Index'); %! assert_equal (size (S), [12, 2]); %! assert_equal (class (S.Index), "categorical"); %! assert_equal (cellstr (S.Index), repmat ({"Test1"; "Test2"; "Test3"}, 4, 1)); %! assert_equal (S.NewData, [Test1';Test2';Test3'](:)); %!test %! Test1 = [93; 57; 87; 89]; %! Test2 = [89; 77; 92; 86]; %! Test3 = [95; 62; 89; 91]; %! Test4 = [95; 62; 89; 91]; %! U = table (Test1, Test2, Test3, Test4); %! S = stack (U, 1:2, 'ConstantVariables', "Test4"); %! assert_equal (S.Properties.VariableNames, ... %! {"Test4", "Test1_Test2_Indicator", "Test1_Test2"}); %!test %! Test1 = [93; 57; 87; 89]; %! Test2 = [89; 77; 92; 86]; %! Test3 = [95; 62; 89; 91]; %! Test4 = [95; 62; 89; 91]; %! U = table (Test1, Test2, Test3, Test4); %! S = stack (U, {"Test1", "Test4"}, 'ConstantVariables', "Test2"); %! assert_equal (S.Properties.VariableNames, ... %! {"Test2", "Test1_Test4_Indicator", "Test1_Test4"}); %!test %! Test1 = [93; 57; 87; 89]; %! Test2 = [89; 77; 92; 86]; %! Test3 = [95; 62; 89; 91]; %! Test4 = [95; 62; 89; 91]; %! U = table (Test1, Test2, Test3, Test4); %! S = stack (U, {"Test1", "Test4"}, 'ConstantVariables', "Test2", ... %! 'NewDataVariableName', 'NewData', 'IndexVariableName', 'Index'); %! assert_equal (S.Properties.VariableNames, ... %! {"Test2", "Index", "NewData"}); %!test # units and descriptions are inherited from the first stacked variable %! Test1 = [93; 57]; %! Test2 = [89; 77]; %! U = table (Test1, Test2); %! U.Properties.VariableUnits = {'cm', 'kg'}; %! U.Properties.VariableDescriptions = {'height', 'weight'}; %! S = stack (U, 1:2); %! assert_equal (S.Properties.VariableUnits, {'', 'cm'}); %! assert_equal (S.Properties.VariableDescriptions, {'Data indicator', ... %! 'height'}); %!test # multiple variable groups stack into one data variable each %! T1 = [1; 2; 3]; %! T2 = [4; 5; 6]; %! T3 = [7; 8; 9]; %! T4 = [10; 11; 12]; %! U = table (T1, T2, T3, T4); %! [S, idx] = stack (U, {{'T1', 'T2'}, {'T3', 'T4'}}); %! assert_equal (S.Properties.VariableNames, {'Indicator', 'T1_T2', 'T3_T4'}); %! assert_equal (class (S.Indicator), 'double'); %! assert_equal (S.Indicator, [1; 2; 1; 2; 1; 2]); %! assert_equal (S.T1_T2, [1; 4; 2; 5; 3; 6]); %! assert_equal (S.T3_T4, [7; 10; 8; 11; 9; 12]); %! assert_equal (idx, [1; 1; 2; 2; 3; 3]); %!test # multi-group inherits per-group metadata; indicator stays generic %! T1 = [1; 2]; %! T2 = [3; 4]; %! T3 = [5; 6]; %! T4 = [7; 8]; %! U = table (T1, T2, T3, T4); %! U.Properties.VariableUnits = {'u1', 'u2', 'u3', 'u4'}; %! U.Properties.VariableDescriptions = {'d1', 'd2', 'd3', 'd4'}; %! S = stack (U, {{'T1', 'T2'}, {'T3', 'T4'}}); %! assert_equal (S.Properties.VariableUnits, {'', 'u1', 'u3'}); %! assert_equal (S.Properties.VariableDescriptions, ... %! {'Data indicator', 'd1', 'd3'}); %!test # multi-group accepts one new data name per group and an index name %! T1 = [1; 2]; %! T2 = [3; 4]; %! T3 = [5; 6]; %! T4 = [7; 8]; %! U = table (T1, T2, T3, T4); %! S = stack (U, {{'T1', 'T2'}, {'T3', 'T4'}}, ... %! 'NewDataVariableName', {'A', 'B'}, 'IndexVariableName', 'Idx'); %! assert_equal (S.Properties.VariableNames, {'Idx', 'A', 'B'}); %!test # the stacked variables join as 'table2array' joins them %! S = stack (table (hours ([1; 2]), [3; 4], 'VariableNames', {'A', 'K'}), ... %! {'A', 'K'}); %! assert_equal (S.A_K, hours ([1; 72; 2; 96])); %!test %! S = stack (table (string ({'x'; 'y'}), [3; 4], 'VariableNames', ... %! {'A', 'K'}), {'A', 'K'}); %! assert_equal (S.A_K, string ({'x'; '3'; 'y'; '4'})); %!test %! S = stack (table ([true; false], [3; 4], 'VariableNames', {'A', 'K'}), ... %! {'A', 'K'}); %! assert_equal (S.A_K, [1; 3; 0; 4]); %!test %! S = stack (table (int8 ([1; 2]), [3.7; 4], 'VariableNames', {'A', 'K'}), ... %! {'A', 'K'}); %! assert_equal (S.A_K, int8 ([1; 4; 2; 4])); ## Test input validation for 'stack' method %!error stack (tblA); %!error ... %! stack (tblA, {'Height', 'Weight'}, 'IndexVariableName'); %!error ... %! stack (tblA, {'Height', 'Weight'}, 'Bogus', 1); %!error ... %! stack (tblA, "Some"); %!error ... %! stack (tblA, {{'Height', 'Weight'}, {'Age'}}); %!error ... %! stack (tblA, 1:3, "ConstantVariables", "Some"); %!error ... %! stack (tblA, 1:3, "ConstantVariables", "Weight"); %!error ... %! stack (tblA, 1:3, "NewDataVariableName", 3); %!error ... %! stack (tblA, 1:2, "NewDataVariableName", {'A', 'B'}); %!error ... %! stack (tblA, 1:3, "IndexVariableName", 3); %!error ... %! stack (table (categorical ({'lo'; 'hi'}), [1; 2], 'VariableNames', ... %! {'A', 'K'}), {'A', 'K'}); %!error ... %! stack (table (datetime (2020, 1, [1; 2]), [1; 2], 'VariableNames', ... %! {'A', 'K'}), {'A', 'K'}); %!error ... %! stack (table ({'a'; 'b'}, [1; 2], 'VariableNames', {'A', 'K'}), ... %! {'A', 'K'}); %!error ... %! stack (table (struct ('a', {1; 2}), [1; 2], 'VariableNames', ... %! {'A', 'K'}), {'A', 'K'}); %!error ... %! stack (table (table ([1; 2]), [1; 2], 'VariableNames', {'A', 'K'}), ... %! {'A', 'K'}); %!error ... %! stack (table ([1, 2; 3, 4], [1; 2], 'VariableNames', {'A', 'K'}), ... %! {'A', 'K'}); ## Test 'unstack' method %!test # a single variable is unstacked by a categorical indicator %! Storm = [3; 3; 1; 3; 1; 1; 4; 2; 4; 2; 4; 2]; %! Town = categorical ({'Natick'; 'Worcester'; 'Natick'; 'Boston'; ... %! 'Boston'; 'Worcester'; 'Boston'; 'Natick'; ... %! 'Worcester'; 'Worcester'; 'Natick'; 'Boston'}); %! Snowfall = [0; 3; 5; 5; 9; 10; 12; 13; 15; 16; 17; 21]; %! S = table (Storm, Town, Snowfall); %! [U, idx] = unstack (S, 'Snowfall', 'Town'); %! assert_equal (U.Properties.VariableNames, ... %! {'Storm', 'Boston', 'Natick', 'Worcester'}); %! assert_equal (U.Storm, [3; 1; 4; 2]); %! assert_equal (U.Boston, [5; 9; 12; 21]); %! assert_equal (U.Natick, [0; 5; 17; 13]); %! assert_equal (U.Worcester, [3; 10; 15; 16]); %! assert_equal (idx, [1; 3; 7; 8]); ## duplicate group/indicator pairs are aggregated by @sum (numeric default) %!test %! Storm = [3; 3]; %! Town = categorical ({'Boston'; 'Boston'}); %! Snowfall = [5; 100]; %! U = unstack (table (Storm, Town, Snowfall), 'Snowfall', 'Town'); %! assert_equal (U.Boston, 105); %!test # a custom AggregationFunction overrides the default %! G = [1; 1; 1; 2]; %! T = categorical ({'a'; 'a'; 'b'; 'a'}); %! V = [3; 9; 5; 7]; %! U = unstack (table (G, T, V), 'V', 'T', 'AggregationFunction', @max); %! assert_equal (U.a, [9; 7]); %! assert_equal (U.b, [5; NaN]); %!test # a numeric indicator derives valid variable names %! G = [1; 1; 2; 2]; %! T = [10; 20; 10; 20]; %! V = [1; 2; 3; 4]; %! U = unstack (table (G, T, V), 'V', 'T'); %! assert_equal (U.Properties.VariableNames, {'G', 'x10', 'x20'}); %! assert_equal (U.x10, [1; 3]); %! assert_equal (U.x20, [2; 4]); %!test # ConstantVariables take their value from the first row of each group %! G = [1; 1; 2; 2]; %! T = categorical ({'a'; 'b'; 'a'; 'b'}); %! V = [1; 2; 3; 4]; %! C = [100; 101; 200; 201]; %! U = unstack (table (G, T, V, C), 'V', 'T', 'ConstantVariables', 'C'); %! assert_equal (U.Properties.VariableNames, {'G', 'C', 'a', 'b'}); %! assert_equal (U.C, [100; 200]); %!test # explicit GroupingVariables restrict the grouping set %! G = [1; 1; 2; 2]; %! T = categorical ({'a'; 'b'; 'a'; 'b'}); %! V = [1; 2; 3; 4]; %! C = [100; 101; 200; 201]; %! U = unstack (table (G, T, V, C), 'V', 'T', 'GroupingVariables', 'G'); %! assert_equal (U.Properties.VariableNames, {'G', 'a', 'b'}); %!test # NewDataVariableNames rename the unstacked variables %! G = [1; 1; 2; 2]; %! T = categorical ({'a'; 'b'; 'a'; 'b'}); %! V = [1; 2; 3; 4]; %! U = unstack (table (G, T, V), 'V', 'T', 'NewDataVariableNames', {'AA', ... %! 'BB'}); %! assert_equal (U.Properties.VariableNames, {'G', 'AA', 'BB'}); %!test # VariableNamingRule 'preserve' keeps invalid identifiers verbatim %! G = [1; 1; 2; 2]; %! T = categorical ({'New York'; 'New York'; 'Los Angeles'; 'Los Angeles'}); %! V = [1; 2; 3; 4]; %! U = unstack (table (G, T, V), 'V', 'T', 'VariableNamingRule', 'preserve'); %! assert_equal (U.Properties.VariableNames, {'G', 'Los Angeles', 'New York'}); %!test # the default 'modify' rule produces valid identifiers %! G = [1; 1; 2; 2]; %! T = categorical ({'New York'; 'New York'; 'Los Angeles'; 'Los Angeles'}); %! V = [1; 2; 3; 4]; %! U = unstack (table (G, T, V), 'V', 'T'); %! assert_equal (U.Properties.VariableNames, {'G', 'LosAngeles', 'NewYork'}); %!test # rows with missing grouping values are excluded from unstacking %! Storm = [3; 3; 1; 3; 1; 1; 4; 2; 4; 2; 4; 2]; %! Town = categorical ({'Natick'; 'Worcester'; 'Natick'; 'Boston'; ... %! 'Boston'; 'Worcester'; 'Boston'; 'Natick'; ... %! 'Worcester'; 'Worcester'; 'Natick'; 'Boston'}); %! Snowfall = [0; 3; 5; 5; 9; 10; 12; 13; 15; 16; 17; 21]; %! Storm(1) = NaN; %! S = table (Storm, Town, Snowfall); %! [U, idx] = unstack (S, 'Snowfall', 'Town'); %! assert_equal (U.Storm, [3; 1; 4; 2]); %! assert_equal (U.Natick, [NaN; 5; 17; 13]); %! assert_equal (U.Boston, [5; 9; 12; 21]); %! assert_equal (idx, [2; 3; 7; 8]); %!test # with no grouping variables every row collapses to a single output row %! Town = categorical ({'Boston'; 'Natick'; 'Worcester'; 'Boston'}); %! Snowfall = [5; 10; 15; 20]; %! [U, idx] = unstack (table (Town, Snowfall), 'Snowfall', 'Town'); %! assert_equal (size (U), [1, 3]); %! assert_equal (U.Boston, 25); %! assert_equal (idx, 1); %!test # categorical data are unstacked (default @unique, no conflict) %! G = [1; 1; 2; 2]; %! T = categorical ({'a'; 'b'; 'a'; 'b'}); %! V = categorical ({'p'; 'q'; 'r'; 's'}); %! U = unstack (table (G, T, V), 'V', 'T'); %! assert_equal (cellstr (U.a), {'p'; 'r'}); %! assert_equal (cellstr (U.b), {'q'; 's'}); %!test # logical data are unstacked (regression: default no longer crashes) %! G = [1; 1; 2; 2]; %! T = categorical ({'a'; 'b'; 'a'; 'b'}); %! B = logical ([1; 0; 1; 0]); %! U = unstack (table (G, T, B), 'B', 'T'); %! assert_equal (U.a, [true; true]); %! assert_equal (U.b, [false; false]); %!test # string data are unstacked (regression: default no longer crashes) %! G = [1; 1; 2; 2]; %! T = categorical ({'a'; 'b'; 'a'; 'b'}); %! V = string ({'p'; 'q'; 'r'; 's'}); %! U = unstack (table (G, T, V), 'V', 'T'); %! assert_equal (cellstr (U.a), {'p'; 'r'}); %! assert_equal (cellstr (U.b), {'q'; 's'}); %!test # cellstr data are unstacked (regression: default no longer crashes) %! G = [1; 1; 2; 2]; %! T = categorical ({'a'; 'b'; 'a'; 'b'}); %! V = {'p'; 'q'; 'r'; 's'}; %! U = unstack (table (G, T, V), 'V', 'T'); %! assert_equal (U.a, {'p'; 'r'}); %! assert_equal (U.b, {'q'; 's'}); %!test # duration data are unstacked (default @unique, no conflict) %! G = [1; 1; 2; 2]; %! T = categorical ({'a'; 'b'; 'a'; 'b'}); %! V = hours ([1; 2; 3; 4]); %! U = unstack (table (G, T, V), 'V', 'T'); %! assert_equal (hours (U.a), [1; 3]); %! assert_equal (hours (U.b), [2; 4]); %!test # missing duration cells fill with zero, identical conflicts collapse %! G = [1; 1; 1; 2]; %! T = categorical ({'a'; 'a'; 'b'; 'b'}); %! V = hours ([2; 2; 3; 4]); %! U = unstack (table (G, T, V), 'V', 'T'); %! assert_equal (hours (U.a), [2; 0]); %! assert_equal (hours (U.b), [3; 4]); %!test # calendarDuration data are unstacked (default @unique, no conflict) %! G = [1; 1; 2; 2]; %! T = categorical ({'a'; 'b'; 'a'; 'b'}); %! V = calmonths ([1; 2; 3; 4]); %! U = unstack (table (G, T, V), 'V', 'T'); %! assert_equal (calmonths (U.a), [1; 3]); %! assert_equal (calmonths (U.b), [2; 4]); ## missing calendarDuration cells fill with zero, identical conflicts collapse %!test %! G = [1; 1; 1; 2]; %! T = categorical ({'a'; 'a'; 'b'; 'b'}); %! V = calmonths ([1; 1; 2; 3]); %! U = unstack (table (G, T, V), 'V', 'T'); %! assert_equal (calmonths (U.a), [1; 0]); %! assert_equal (calmonths (U.b), [2; 3]); %!test # multiple variables are unstacked with composite names %! G = [1; 1; 2; 2]; %! T = categorical ({'a'; 'b'; 'a'; 'b'}); %! N = [10; 20; 30; 40]; %! M = [1; 2; 3; 4]; %! U = unstack (table (G, T, N, M), {'N', 'M'}, 'T'); %! assert_equal (U.Properties.VariableNames, {'G', 'N_a', 'N_b', 'M_a', 'M_b'}); %! assert_equal (U.N_a, [10; 30]); %! assert_equal (U.N_b, [20; 40]); %! assert_equal (U.M_a, [1; 3]); %! assert_equal (U.M_b, [2; 4]); %!test # multiple variables of different types are unstacked together %! G = [1; 1; 2; 2]; %! T = categorical ({'a'; 'b'; 'a'; 'b'}); %! N = [10; 20; 30; 40]; %! C = categorical ({'p'; 'q'; 'r'; 's'}); %! U = unstack (table (G, T, N, C), {'N', 'C'}, 'T'); %! assert_equal (U.N_a, [10; 30]); %! assert_equal (cellstr (U.C_a), {'p'; 'r'}); ## Test input validation for 'unstack' method %!error unstack (tblA); %!error unstack (tblA, 'Height'); %!error unstack (tblA, [], 'Height'); %!error ... %! unstack (tblA, 'Height', 'Age', 'GroupingVariables'); %!error ... %! unstack (tblA, 'Height', 'Age', 'Bogus', 1); %!error ... %! unstack (tblA, 'some', 'Height'); %!error ... %! Age = [38; 43; 38; 40; 49]; %! Smoker = logical ([1; 0; 1; 0; 1]); %! Height = [71; 69; 64; 67; 64]; %! unstack (table (table (Age), Smoker, Height), 'Var1', 'Height'); %!error ... %! unstack (tblA, 'Height', {'Smoker', 'Age'}); %!error ... %! unstack (tblA, 'Height', 'some'); %!error ... %! unstack (tblA, 'Height', 'BloodPressure'); %!error ... %! unstack (tblA, 'Height', 'Height'); %!error ... %! Smoker = logical ([1; 0; 1; 0; 1]); %! Height = [71; 69; 64; 67; 64]; %! unstack (table ({4;5;6;7;8}, Smoker, Height), 'Height', 'Var1'); %!error ... %! unstack (tblA, 'Height', 'Age', 'ConstantVariables', 'some'); %!error ... %! unstack (tblA, 'Height', 'Age', 'ConstantVariables', 'Height'); %!error ... %! unstack (tblA, 'Height', 'Age', 'ConstantVariables', 'Age'); %!error ... %! unstack (tblA, 'Height', 'Age', 'GroupingVariables', 'some'); %!error ... %! unstack (tblA, 'Height', 'Age', 'GroupingVariables', 'Height'); %!error ... %! unstack (tblA, 'Height', 'Age', 'GroupingVariables', 'Age'); %!warning ... %! Smoker = logical ([1; 0; 1; 0; 1]); %! Height = [71; 69; 64; 67; 64]; %! Age = [38; 43; 38; 40; 49]; %! unstack (table ({4;5;6;7;8}, Smoker, Height, Age), 'Height', 'Age', ... %! 'GroupingVariables', 'Var1'); %!error ... %! unstack (tblA, 'BloodPressure', 'Height', 'ConstantVariables', 'Age', ... %! 'GroupingVariables', 'Age'); %!error ... %! unstack (tblA, 'Height', 'Age', 'NewDataVariableNames', [1:5]); %!error ... %! unstack (tblA, 'Height', 'Age', 'NewDataVariableNames', {"A","B","C"}); %!error ... %! unstack (tblA, 'Height', 'Age', 'AggregationFunction', 1); %!error ... %! unstack (tblA, 'Height', 'Age', 'AggregationFunction', 'isnumeric'); %!error ... %! unstack (table ([1; 1; 2], categorical ({'a'; 'a'; 'b'}), ... %! categorical ({'x'; 'y'; 'z'})), 'Var3', 'Var2'); %!error ... %! unstack (table ([1; 1; 2], categorical ({'a'; 'a'; 'b'}), ... %! hours ([1; 2; 3])), 'Var3', 'Var2'); %!error ... %! unstack (tblA, {'Weight', 'Height'}, 'Age', 'VariableNamingRule', ... %! 'somerule'); ## Test 'inner2outer' method %!test # basic transpose: two nested tables sharing inner names %! A = table ([1;2;3], [4;5;6], 'VariableNames', {'X', 'Y'}); %! B = table ([7;8;9], [10;11;12], 'VariableNames', {'X', 'Y'}); %! T = inner2outer (table (A, B, 'VariableNames', {'A', 'B'})); %! assert_equal (T.Properties.VariableNames, {'X', 'Y'}); %! assert_equal (T.X.Properties.VariableNames, {'A', 'B'}); %! assert_equal (T.X.A, [1; 2; 3]); %! assert_equal (T.X.B, [7; 8; 9]); %! assert_equal (T.Y.A, [4; 5; 6]); %! assert_equal (T.Y.B, [10; 11; 12]); %!test # non-nested variables keep their relative position %! A = table ([1;2;3], [4;5;6], 'VariableNames', {'X', 'Y'}); %! B = table ([7;8;9], [10;11;12], 'VariableNames', {'X', 'Y'}); %! C = [100; 200; 300]; %! T = inner2outer (table (C, A, B, 'VariableNames', {'C', 'A', 'B'})); %! assert_equal (T.Properties.VariableNames, {'C', 'X', 'Y'}); %! assert_equal (T.C, [100; 200; 300]); %! T = inner2outer (table (A, C, B, 'VariableNames', {'A', 'C', 'B'})); %! assert_equal (T.Properties.VariableNames, {'X', 'Y', 'C'}); %!test # inner names are regrouped by name regardless of their order %! A = table ([1;2;3], [4;5;6], 'VariableNames', {'X', 'Y'}); %! Br = table ([10;11;12], [7;8;9], 'VariableNames', {'Y', 'X'}); %! T = inner2outer (table (A, Br, 'VariableNames', {'A', 'B'})); %! assert_equal (T.Properties.VariableNames, {'X', 'Y'}); %! assert_equal (T.X.A, [1; 2; 3]); %! assert_equal (T.X.B, [7; 8; 9]); %! assert_equal (T.Y.A, [4; 5; 6]); %! assert_equal (T.Y.B, [10; 11; 12]); %!test # mismatched inner names: union, single-source names become plain vars %! A = table ([1;2;3], [4;5;6], 'VariableNames', {'X', 'Y'}); %! Bz = table ([7;8;9], [10;11;12], 'VariableNames', {'X', 'Z'}); %! T = inner2outer (table (A, Bz, 'VariableNames', {'A', 'B'})); %! assert_equal (T.Properties.VariableNames, {'X', 'Y', 'Z'}); %! assert_equal (istable (T.X), true); %! assert_equal (T.X.Properties.VariableNames, {'A', 'B'}); %! assert_equal (istable (T.Y), false); %! assert_equal (T.Y, [4; 5; 6]); %! assert_equal (istable (T.Z), false); %! assert_equal (T.Z, [10; 11; 12]); %!test # three nested tables %! A = table ([1;2;3], [4;5;6], 'VariableNames', {'X', 'Y'}); %! B = table ([7;8;9], [10;11;12], 'VariableNames', {'X', 'Y'}); %! D = table ([13;14;15], [16;17;18], 'VariableNames', {'X', 'Y'}); %! T = inner2outer (table (A, B, D, 'VariableNames', {'A', 'B', 'D'})); %! assert_equal (T.X.Properties.VariableNames, {'A', 'B', 'D'}); %! assert_equal (T.X.D, [13; 14; 15]); %!test # inner variable metadata is inherited; parent row names are preserved %! A = table ([1;2;3], [4;5;6], 'VariableNames', {'X', 'Y'}); %! A.Properties.VariableUnits = {'kg', 'm'}; %! A.Properties.VariableDescriptions = {'mass', 'length'}; %! B = table ([7;8;9], [10;11;12], 'VariableNames', {'X', 'Y'}); %! B.Properties.VariableUnits = {'kg', 'm'}; %! B.Properties.VariableDescriptions = {'mass', 'length'}; %! T = inner2outer (table (A, B, 'VariableNames', {'A', 'B'}, ... %! 'RowNames', {'r1', 'r2', 'r3'})); %! assert_equal (T.Properties.RowNames, {'r1'; 'r2'; 'r3'}); %! assert_equal (T.X.Properties.VariableDescriptions, {'mass', 'mass'}); %! assert_equal (T.X.Properties.VariableUnits, {'kg', 'kg'}); %! assert_equal (T.Y.Properties.VariableDescriptions, {'length', 'length'}); %! assert_equal (T.Y.Properties.VariableUnits, {'m', 'm'}); %! assert_equal (isempty (T.X.Properties.RowNames), true); ## Test input validation for 'inner2outer' method ## a table holding nothing nested has nothing to turn inside out %!test %! T = table ([1; 2], [3; 4], 'VariableNames', {'A', 'B'}); %! assert_equal (isequal (inner2outer (T), T), true); %!error ... %! inner2outer (table (table ([1;2], 'VariableNames', {'A'}), [3;4], ... %! 'VariableNames', {'N', 'A'})); ## Test 'addprop' method %!test # custom properties of both kinds are added %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'x', 'y'}); %! T = addprop (T, {'Note', 'Flags'}, {'table', 'variable'}); %! assert_equal (fieldnames (T.Properties.CustomProperties), {'Note'; 'Flags'}); %!test # a single property is added with character-vector inputs %! T = table ([1; 2], 'VariableNames', {'x'}); %! T = addprop (T, 'Note', 'table'); %! assert_equal (fieldnames (T.Properties.CustomProperties), {'Note'}); %!test # values are assigned to a 'table' custom property with dot syntax %! T = table ([1; 2], 'VariableNames', {'x'}); %! T = addprop (T, 'Note', 'table'); %! T.Properties.CustomProperties.Note = 'hello'; %! assert_equal (T.Properties.CustomProperties.Note, 'hello'); %!test # a 'variable' custom property holds one value per variable %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'x', 'y'}); %! T = addprop (T, 'Flags', 'variable'); %! T.Properties.CustomProperties.Flags = {'f1', 'f2'}; %! assert_equal (T.Properties.CustomProperties.Flags, {'f1', 'f2'}); %!test # properties can be appended to a table that already has some %! T = table ([1; 2], 'VariableNames', {'x'}); %! T = addprop (T, 'Note', 'table'); %! T = addprop (T, 'Flags', 'variable'); %! assert_equal (fieldnames (T.Properties.CustomProperties), {'Note'; 'Flags'}); %!test # property names and types may be given as a string array %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'x', 'y'}); %! T = addprop (T, string ({'Note', 'Flags'}), string ({'table', 'variable'})); %! assert_equal (fieldnames (T.Properties.CustomProperties), {'Note'; 'Flags'}); ## Test input validation for 'addprop' method %!error ... %! addprop (table (1), {'a'}); %!error ... %! addprop (table (1), 5, 'table'); %!error ... %! addprop (table (1), 'a', 5); %!error ... %! addprop (table (1), {'a', 'b'}, {'table'}); %!error ... %! addprop (table (1), {'a', 'a'}, {'table', 'variable'}); %!error ... %! addprop (addprop (table (1), 'Note', 'table'), 'Note', 'table'); %!error ... %! addprop (addprop (table (1), 'Flags', 'table'), {'New', 'Flags'}, ... %! {'table', 'table'}); %!error ... %! addprop (table (1), '1bad', 'table'); %!error ... %! addprop (table (1), 'a', 'foo'); ## Custom-property merge semantics across multi-table operations %!test # horzcat: table-scoped first-wins, variable-scoped concatenate %! A = table ([1; 2], [3; 4], 'VariableNames', {'k', 'a'}); %! A = addprop (A, 'ts', 'table'); A.Properties.CustomProperties.ts = 'A'; %! A = addprop (A, 'vs', ... %! 'variable'); A.Properties.CustomProperties.vs = [10 11]; %! B = table ([7; 8], 'VariableNames', {'b'}); %! B = addprop (B, 'ts', 'table'); B.Properties.CustomProperties.ts = 'B'; %! B = addprop (B, 'vs', 'variable'); B.Properties.CustomProperties.vs = [20]; %! H = [A, B]; %! assert_equal (H.Properties.CustomProperties.ts, 'A'); %! assert_equal (H.Properties.CustomProperties.vs, [10 11 20]); ## horzcat: a table lacking a variable-scoped property NaN-fills its block %!test %! A = table ([1; 2], [3; 4], 'VariableNames', {'k', 'a'}); %! A = addprop (A, 'vs', ... %! 'variable'); A.Properties.CustomProperties.vs = [10 11]; %! C = table ([7; 8], 'VariableNames', {'c'}); %! H = [A, C]; %! assert_equal (H.Properties.CustomProperties.vs, [10 11 NaN]); ## horzcat: disjoint table-scoped props are unioned, table before variable %!test %! A = table ([1; 2], [3; 4], 'VariableNames', {'k', 'a'}); %! A = addprop (A, 'ts', 'table'); A.Properties.CustomProperties.ts = 'A'; %! A = addprop (A, 'vs', ... %! 'variable'); A.Properties.CustomProperties.vs = [10 11]; %! D = table ([7; 8], 'VariableNames', {'d'}); %! D = addprop (D, 'other', 'table'); D.Properties.CustomProperties.other = 'D'; %! H = [A, D]; %! assert_equal (fieldnames (H.Properties.CustomProperties), ... %! {'ts'; 'other'; 'vs'}); %! assert_equal (H.Properties.CustomProperties.other, 'D'); %! assert_equal (H.Properties.CustomProperties.vs, [10 11 NaN]); ## vertcat keeps the first table's custom properties (identical variables) %!test %! A = table ([1; 2], [3; 4], 'VariableNames', {'k', 'a'}); %! A = addprop (A, 'ts', 'table'); A.Properties.CustomProperties.ts = 'A'; %! A = addprop (A, 'vs', ... %! 'variable'); A.Properties.CustomProperties.vs = [10 11]; %! A2 = table ([5; 6], [7; 8], 'VariableNames', {'k', 'a'}); %! A2 = addprop (A2, 'ts', ... %! 'table'); A2.Properties.CustomProperties.ts = 'A2'; %! A2 = addprop (A2, 'vs', ... %! 'variable'); A2.Properties.CustomProperties.vs = [30 31]; %! V = [A; A2]; %! assert_equal (V.Properties.CustomProperties.ts, 'A'); %! assert_equal (V.Properties.CustomProperties.vs, [10 11]); %!test # splitvars replicates a split variable's variable-scoped entry %! S = table ([1; 2], [3 4; 5 6], 'VariableNames', {'x', 'm'}); %! S = addprop (S, 'vs', ... %! 'variable'); S.Properties.CustomProperties.vs = [100 200]; %! SS = splitvars (S, 'm'); %! assert_equal (SS.Properties.CustomProperties.vs, [100 200 200]); ## join maps variable-scoped props per source (key+left from left, right from ## right) %!test %! L = table ([1; 2], [3; 4], 'VariableNames', {'k', 'a'}); %! L = addprop (L, 'ts', 'table'); L.Properties.CustomProperties.ts = 'L'; %! L = addprop (L, 'vs', ... %! 'variable'); L.Properties.CustomProperties.vs = [10 11]; %! R = table ([1; 2], [5; 6], 'VariableNames', {'k', 'b'}); %! R = addprop (R, 'ts', 'table'); R.Properties.CustomProperties.ts = 'R'; %! R = addprop (R, 'vs', ... %! 'variable'); R.Properties.CustomProperties.vs = [20 21]; %! J = innerjoin (L, R); %! assert_equal (J.Properties.CustomProperties.ts, 'L'); %! assert_equal (J.Properties.CustomProperties.vs, [10 11 21]); %!test # unstack replicates the unstacked variable's entry onto its new columns %! T = table ([1; 1; 2; 2], categorical ({'x'; 'y'; 'x'; 'y'}), ... %! [10; 20; 30; 40], ... %! 'VariableNames', {'g', 'k', 'v'}); %! T = addprop (T, 'ts', 'table'); T.Properties.CustomProperties.ts = 'T'; %! T = addprop (T, 'vs', ... %! 'variable'); T.Properties.CustomProperties.vs = [1 2 3]; %! U = unstack (T, 'v', 'k'); %! assert_equal (U.Properties.VariableNames, {'g', 'x', 'y'}); %! assert_equal (U.Properties.CustomProperties.ts, 'T'); %! assert_equal (U.Properties.CustomProperties.vs, [1 3 3]); ## Test 'rmprop' method %!test # a custom property is removed, others are kept %! T = table ([1; 2], 'VariableNames', {'x'}); %! T = addprop (T, {'Note', 'Flags'}, {'table', 'variable'}); %! T = rmprop (T, 'Note'); %! assert_equal (fieldnames (T.Properties.CustomProperties), {'Flags'}); %!test # several custom properties are removed in one call %! T = table ([1; 2], 'VariableNames', {'x'}); %! T = addprop (T, {'Note', 'Flags', 'Tag'}, {'table', 'variable', 'table'}); %! T = rmprop (T, {'Note', 'Tag'}); %! assert_equal (fieldnames (T.Properties.CustomProperties), {'Flags'}); %!test # property names may be given as a string array %! T = table ([1; 2], 'VariableNames', {'x'}); %! T = addprop (T, {'Note', 'Flags'}, {'table', 'variable'}); %! T = rmprop (T, string ({'Note', 'Flags'})); %! assert_equal (fieldnames (T.Properties.CustomProperties), cell (0, 1)); %!test # duplicate names are tolerated (the property is removed once) %! T = table ([1; 2], 'VariableNames', {'x'}); %! T = addprop (T, {'Note', 'Flags'}, {'table', 'variable'}); %! T = rmprop (T, {'Note', 'Note'}); %! assert_equal (fieldnames (T.Properties.CustomProperties), {'Flags'}); %!test # names that do not match an existing property are silently ignored %! T = table ([1; 2], 'VariableNames', {'x'}); %! T = addprop (T, {'Note', 'Flags'}, {'table', 'variable'}); %! T = rmprop (T, {'Note', 'Missing'}); %! assert_equal (fieldnames (T.Properties.CustomProperties), {'Flags'}); %!test # removing from a table without custom properties is a no-op %! T = table ([1; 2], 'VariableNames', {'x'}); %! T = rmprop (T, 'Missing'); %! assert_equal (isempty (T.Properties.CustomProperties), true); ## Test input validation for 'rmprop' method %!error ... %! rmprop (table (1)); %!error ... %! rmprop (table (1), 5); ################################################################################ ## ** Join and Set Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'join' 'innerjoin' 'outerjoin' 'union' ## ## 'intersect' 'ismember' 'setdiff' 'setxor' ## ## ## ################################################################################ ## Test 'join' method %!test %! L = table ([3;1;2;1], {'a';'b';'c';'d'}, 'VariableNames', {'Key', 'L'}, ... %! 'RowNames', {'r1', 'r2', 'r3', 'r4'}); %! R = table ([1;2;3], {'x';'y';'z'}, 'VariableNames', {'Key', 'R'}); %! [T, ixR] = join (L, R); %! assert_equal (T.Properties.VariableNames, {'Key', 'L', 'R'}); %! assert_equal (T.Key, [3;1;2;1]); %! assert_equal (T.R, {'z';'x';'y';'x'}); %! assert_equal (ixR, [3;1;2;1]); %! assert_equal (T.Properties.RowNames, {'r1';'r2';'r3';'r4'}); ## 'join' with 'LeftKeys'/'RightKeys' %!test %! L = table ([1;2], (10:11)', 'VariableNames', {'KL', 'A'}); %! R = table ([2;1], {'x';'y'}, 'VariableNames', {'KR', 'B'}); %! T = join (L, R, 'LeftKeys', 'KL', 'RightKeys', 'KR'); %! assert_equal (T.Properties.VariableNames, {'KL', 'A', 'B'}); %! assert_equal (T.A, [10;11]); %! assert_equal (T.B, {'y';'x'}); ## 'join' with 'Keys' and 'RightVariables' %!test %! L = table ([1;2;3], (1:3)', 'VariableNames', {'K', 'A'}); %! R = table ([3;2;1], {'x';'y';'z'}, (4:6)', 'VariableNames', {'K', 'B', 'C'}); %! T = join (L, R, 'Keys', 'K', 'RightVariables', 'B'); %! assert_equal (T.Properties.VariableNames, {'K', 'A', 'B'}); %! assert_equal (T.B, {'z';'y';'x'}); ## 'join' suffixes shared non-key names with the input argument names %!test %! L = table ([1;2], {'a';'b'}, 'VariableNames', {'KeyL', 'V'}); %! R = table ([1;2], {'p';'q'}, 'VariableNames', {'KeyR', 'V'}); %! T = join (L, R, 'LeftKeys', 'KeyL', 'RightKeys', 'KeyR'); %! assert_equal (T.Properties.VariableNames, {'KeyL', 'V_L', 'V_R'}); ## 'join' with 'KeepOneCopy' drops the right copy of the named variable %!test %! L = table ([1;2], {'a';'b'}, 'VariableNames', {'KeyL', 'V'}); %! R = table ([1;2], {'p';'q'}, 'VariableNames', {'KeyR', 'V'}); %! T = join (L, R, 'LeftKeys', 'KeyL', 'RightKeys', 'KeyR', 'KeepOneCopy', 'V'); %! assert_equal (T.Properties.VariableNames, {'KeyL', 'V'}); %! assert_equal (T.V, {'a';'b'}); ## 'join' on a duration key variable %!test %! L = table (hours ([3;1;2]), {'a';'b';'c'}, 'VariableNames', {'K', 'L'}); %! R = table (hours ([1;2;3]), {'x';'y';'z'}, 'VariableNames', {'K', 'R'}); %! [T, ixR] = join (L, R); %! assert_equal (hours (T.K), [3;1;2]); %! assert_equal (T.R, {'z';'x';'y'}); %! assert_equal (ixR, [3;1;2]); ## 'join' carries a categorical key and a calendarDuration variable %!test %! L = table (categorical ({'a';'b'}), calmonths ([5;6]), ... %! 'VariableNames', {'K', 'M'}); %! R = table (categorical ({'b';'a'}), {'x';'y'}, 'VariableNames', {'K', 'R'}); %! T = join (L, R); %! assert_equal (cellstr (T.K), {'a';'b'}); %! assert_equal (calmonths (T.M), [5;6]); %! assert_equal (T.R, {'y';'x'}); ## 'join' on a datetime key variable %!test %! L = table (datetime (2020, 1, [3;1;2]), {'a';'b';'c'}, ... %! 'VariableNames', {'K', 'L'}); %! R = table (datetime (2020, 1, [1;2;3]), {'x';'y';'z'}, ... %! 'VariableNames', {'K', 'R'}); %! [T, ixR] = join (L, R); %! assert_equal (datevec (T.K)(:,3), [3;1;2]); %! assert_equal (T.R, {'z';'x';'y'}); %! assert_equal (ixR, [3;1;2]); ## 'join' with 'LeftVariables' restricts the left columns kept %!test %! L = table ([1;2;3], (10:12)', {'p';'q';'r'}, ... %! 'VariableNames', {'K', 'A', 'C'}); %! R = table ([3;2;1], {'x';'y';'z'}, 'VariableNames', {'K', 'B'}); %! T = join (L, R, 'LeftVariables', {'K', 'A'}); %! assert_equal (T.Properties.VariableNames, {'K', 'A', 'B'}); %! assert_equal (T.A, [10;11;12]); %! assert_equal (T.B, {'z';'y';'x'}); ## 'join' suffixes anonymous inputs with '_left'/'_right' %!test %! T = join (table ([1;2], {'a';'b'}, 'VariableNames', {'KeyL', 'V'}), ... %! table ([1;2], {'p';'q'}, 'VariableNames', {'KeyR', 'V'}), ... %! 'LeftKeys', 'KeyL', 'RightKeys', 'KeyR'); %! assert_equal (T.Properties.VariableNames, {'KeyL', 'V_left', 'V_right'}); %!error ... %! join (table ([1;2])); %!error ... %! join (table ([1;2]), 5); %!error ... %! join (table (1), table (1), 'Keys', 1, 'LeftKeys', 1); %!error ... %! join (table (1, 'VariableNames', {'a'}), ... %! table (1, 'VariableNames', {'b'}), 'LeftKeys', 'a'); %!error ... %! join (table ([1;2], [1;2], 'VariableNames', {'a', 'b'}), ... %! table ([1;2], 'VariableNames', {'c'}), ... %! 'LeftKeys', {'a', 'b'}, 'RightKeys', 'c'); %!error ... %! join (table ((1:3)', 'VariableNames', {'AA'}), ... %! table ((1:3)', 'VariableNames', {'BB'})); %!error ... %! join (table ([1;2], 'VariableNames', {'K'}), ... %! table ([1;2], 'VariableNames', {'K'}), 'BadOption', 1); %!error ... %! join (table ([1;2], 'VariableNames', {'K'}), ... %! table ([1;2], 'VariableNames', {'K'}), 'BadOption'); %!error ... %! join (table ([1;2], 'VariableNames', {'K'}), ... %! table ([1;1;2], {'x';'y';'z'}, 'VariableNames', {'K', 'R'})); %!error ... %! join (table ([1;9], 'VariableNames', {'K'}), ... %! table ([1;2], {'x';'y'}, 'VariableNames', {'K', 'R'})); ## Test 'innerjoin' method %!test %! L = table ([1;2;3;2], {'a';'b';'c';'d'}, 'VariableNames', {'Key', 'L'}); %! R = table ([2;3;5], {'x';'y';'z'}, 'VariableNames', {'Key', 'R'}); %! [T, ixL, ixR] = innerjoin (L, R); %! assert_equal (T.Properties.VariableNames, {'Key', 'L', 'R'}); %! assert_equal (T.Key, [2;2;3]); %! assert_equal (T.L, {'b';'d';'c'}); %! assert_equal (T.R, {'x';'x';'y'}); %! assert_equal (ixL, [2;4;3]); %! assert_equal (ixR, [1;1;2]); ## Inner join drops row names %!test %! L = table ([1;2], {'a';'b'}, 'VariableNames', {'Key', 'L'}, ... %! 'RowNames', {'r1', 'r2'}); %! R = table ([2;1], {'x';'y'}, 'VariableNames', {'Key', 'R'}); %! T = innerjoin (L, R); %! assert_equal (isempty (T.Properties.RowNames), true); ## Cartesian product on repeated keys (m*n rows) %!test %! L = table ([1;1], {'a';'b'}, 'VariableNames', {'K', 'L'}); %! R = table ([1;1], {'x';'y'}, 'VariableNames', {'K', 'R'}); %! [T, ixL, ixR] = innerjoin (L, R); %! assert_equal (height (T), 4); %! assert_equal (T.L, {'a';'a';'b';'b'}); %! assert_equal (T.R, {'x';'y';'x';'y'}); %! assert_equal (ixL, [1;1;2;2]); %! assert_equal (ixR, [1;2;1;2]); ## No matching keys yields an empty result with the correct variables %!test %! L = table ([1;2], {'a';'b'}, 'VariableNames', {'K', 'L'}); %! R = table ([8;9], {'x';'y'}, 'VariableNames', {'K', 'R'}); %! T = innerjoin (L, R); %! assert_equal (height (T), 0); %! assert_equal (T.Properties.VariableNames, {'K', 'L', 'R'}); ## Cellstr key variable %!test %! L = table ({'a';'b';'c'}, (1:3)', 'VariableNames', {'K', 'N'}); %! R = table ({'b';'a'}, {'Y';'X'}, 'VariableNames', {'K', 'M'}); %! T = innerjoin (L, R); %! assert_equal (T.K, {'a';'b'}); %! assert_equal (T.N, [1;2]); %! assert_equal (T.M, {'X';'Y'}); ## String key variable %!test %! L = table (string ({'a';'b'}), (1:2)', 'VariableNames', {'K', 'N'}); %! R = table (string ({'b'}), {'Z'}, 'VariableNames', {'K', 'M'}); %! T = innerjoin (L, R); %! assert_equal (cellstr (T.K), {'b'}); %! assert_equal (T.N, 2); %! assert_equal (T.M, {'Z'}); ## Categorical key variable, sorted by category code %!test %! L = table (categorical ({'a';'b';'c'}), (1:3)', 'VariableNames', {'K', 'N'}); %! R = table (categorical ({'c';'a'}), {'P';'Q'}, 'VariableNames', {'K', 'M'}); %! T = innerjoin (L, R); %! assert_equal (cellstr (T.K), {'a';'c'}); %! assert_equal (T.N, [1;3]); %! assert_equal (T.M, {'Q';'P'}); ## 'Keys' option selects the key explicitly %!test %! L = table ([1;2;3], (1:3)', 'VariableNames', {'K', 'A'}); %! R = table ([2;3], {'x';'y'}, 'VariableNames', {'K', 'B'}); %! T = innerjoin (L, R, 'Keys', 'K'); %! assert_equal (T.Properties.VariableNames, {'K', 'A', 'B'}); %! assert_equal (T.K, [2;3]); ## 'LeftKeys'/'RightKeys' for differently-named keys %!test %! L = table ([1;2;3], (10:12)', 'VariableNames', {'KL', 'A'}); %! R = table ([2;3], {'x';'y'}, 'VariableNames', {'KR', 'B'}); %! [T, ixL, ixR] = innerjoin (L, R, 'LeftKeys', 'KL', 'RightKeys', 'KR'); %! assert_equal (T.Properties.VariableNames, {'KL', 'A', 'B'}); %! assert_equal (T.KL, [2;3]); %! assert_equal (T.A, [11;12]); %! assert_equal (T.B, {'x';'y'}); %! assert_equal (ixL, [2;3]); %! assert_equal (ixR, [1;2]); ## 'LeftVariables' and 'RightVariables' restrict the output columns %!test %! L = table ([1;2;3], (10:12)', {'p';'q';'r'}, ... %! 'VariableNames', {'K', 'A', 'C'}); %! R = table ([2;3], {'x';'y'}, (5:6)', 'VariableNames', {'K', 'B', 'D'}); %! T = innerjoin (L, R, 'LeftVariables', 'A', 'RightVariables', 'B'); %! assert_equal (T.Properties.VariableNames, {'A', 'B'}); %! assert_equal (T.A, [11;12]); %! assert_equal (T.B, {'x';'y'}); ## Shared non-key variable names get '_Tleft'/'_Tright' suffixes %!test %! L = table ([1;2], {'a';'b'}, 'VariableNames', {'KeyL', 'V'}); %! R = table ([2;1], {'p';'q'}, 'VariableNames', {'KeyR', 'V'}); %! T = innerjoin (L, R, 'LeftKeys', 'KeyL', 'RightKeys', 'KeyR'); %! assert_equal (T.Properties.VariableNames, {'KeyL', 'V_L', 'V_R'}); %! assert_equal (T.V_L, {'a';'b'}); %! assert_equal (T.V_R, {'q';'p'}); ## Multiple key variables %!test %! L = table ([1;1;2], {'a';'b';'a'}, (1:3)', ... %! 'VariableNames', {'K1', 'K2', 'N'}); %! R = table ([1;2], {'b';'a'}, {'x';'y'}, ... %! 'VariableNames', {'K1', 'K2', 'M'}); %! T = innerjoin (L, R); %! assert_equal (T.Properties.VariableNames, {'K1', 'K2', 'N', 'M'}); %! assert_equal (T.K1, [1;2]); %! assert_equal (T.K2, {'b';'a'}); %! assert_equal (T.N, [2;3]); %! assert_equal (T.M, {'x';'y'}); ## 'innerjoin' on a duration key variable %!test %! L = table (hours ([3;1;2]), {'a';'b';'c'}, 'VariableNames', {'K', 'L'}); %! R = table (hours ([1;2;3]), {'x';'y';'z'}, 'VariableNames', {'K', 'R'}); %! [T, ixL, ixR] = innerjoin (L, R); %! assert_equal (hours (T.K), [1;2;3]); %! assert_equal (T.R, {'x';'y';'z'}); %! assert_equal (ixL, [2;3;1]); %! assert_equal (ixR, [1;2;3]); ## 'innerjoin' on a calendarDuration key variable %!test %! L = table (calmonths ([3;1;2]), {'a';'b';'c'}, 'VariableNames', {'K', 'L'}); %! R = table (calmonths ([2;1]), {'x';'y'}, 'VariableNames', {'K', 'R'}); %! [T, ixL, ixR] = innerjoin (L, R); %! assert_equal (calmonths (T.K), [1;2]); %! assert_equal (T.L, {'b';'c'}); %! assert_equal (T.R, {'y';'x'}); %! assert_equal (ixL, [2;3]); %! assert_equal (ixR, [2;1]); ## 'innerjoin' on a datetime key variable, sorted by key %!test %! L = table (datetime (2020, 1, [3;1;2]), {'a';'b';'c'}, ... %! 'VariableNames', {'K', 'L'}); %! R = table (datetime (2020, 1, [1;2;3]), {'x';'y';'z'}, ... %! 'VariableNames', {'K', 'R'}); %! [T, ixL, ixR] = innerjoin (L, R); %! assert_equal (datevec (T.K)(:,3), [1;2;3]); %! assert_equal (T.R, {'x';'y';'z'}); %! assert_equal (ixL, [2;3;1]); %! assert_equal (ixR, [1;2;3]); ## 'innerjoin' suffixes anonymous inputs with '_left'/'_right' %!test %! T = innerjoin (table ([1;2], {'a';'b'}, 'VariableNames', {'KeyL', 'V'}), ... %! table ([2;1], {'p';'q'}, 'VariableNames', {'KeyR', 'V'}), ... %! 'LeftKeys', 'KeyL', 'RightKeys', 'KeyR'); %! assert_equal (T.Properties.VariableNames, {'KeyL', 'V_left', 'V_right'}); %!test # an undefined key matches nothing, as in MATLAB %! T = table (categorical ({'lo'; ''}), [1; 1], 'VariableNames', {'C', 'K'}); %! U = table (categorical ({'lo'; ''}), [3; 4], 'VariableNames', {'C', 'W'}); %! assert_equal (innerjoin (T, U, 'Keys', 'C').W, 3); %!test # a multicolumn cellstr key is compared row by row %! C = table ({'a', 'b'; 'c', 'd'; 'a', 'b'}, [1; 2; 3], ... %! 'VariableNames', {'A', 'K'}); %! D = table ({'a', 'b'; 'x', 'y'}, [7; 8], 'VariableNames', {'A', 'W'}); %! J = innerjoin (C, D, 'Keys', 'A'); %! assert_equal (J.K, [1; 3]); %! assert_equal (J.W, [7; 7]); %!test # a nested table key is compared by its own variables %! T = table (table ([1; 2], 'VariableNames', {'p'}), [5; 6], ... %! 'VariableNames', {'N', 'K'}); %! U = table (table ([1; 3], 'VariableNames', {'p'}), [7; 8], ... %! 'VariableNames', {'N', 'W'}); %! J = innerjoin (T, U, 'Keys', 'N'); %! assert_equal ([J.K, J.W], [5, 7]); %!test # one table joined with itself numbers the left names, as in MATLAB %! T = table (categorical ({'lo'; 'hi'}), [1; 2], 'VariableNames', {'C', 'K'}); %! J = innerjoin (T, T, 'Keys', 'C'); %! assert_equal (J.Properties.VariableNames, {'C', 'K_T_1', 'K_T'}); %!test # an empty text key is a value and matches empty text %! T = table ({''; 'a'}, [1; 2], 'VariableNames', {'C', 'K'}); %! U = table ({''; 'b'}, [3; 4], 'VariableNames', {'C', 'W'}); %! assert_equal (innerjoin (T, U, 'Keys', 'C').W, 3); %!error ... %! innerjoin (table ([1;2])); %!error ... %! innerjoin (table ([1;2]), 5); %!error ... %! innerjoin (table (1), table (1), 'Keys', 1, 'LeftKeys', 1); %!error ... %! innerjoin (table (1, 'VariableNames', {'a'}), ... %! table (1, 'VariableNames', {'b'}), 'LeftKeys', 'a'); %!error ... %! innerjoin (table ([1;2], [1;2], 'VariableNames', {'a', 'b'}), ... %! table ([1;2], 'VariableNames', {'c'}), ... %! 'LeftKeys', {'a', 'b'}, 'RightKeys', 'c'); %!error ... %! innerjoin (table ((1:3)', 'VariableNames', {'AA'}), ... %! table ((1:3)', 'VariableNames', {'BB'})); %!error ... %! innerjoin (table ([1;2], 'VariableNames', {'K'}), ... %! table ([1;2], 'VariableNames', {'K'}), 'BadOption', 1); %!error ... %! innerjoin (table ([1;2], 'VariableNames', {'K'}), ... %! table ([1;2], 'VariableNames', {'K'}), 'BadOption'); %!error ... %! innerjoin (table ([1;2], 'VariableNames', {'K'}), ... %! table ({'1';'2'}, 'VariableNames', {'K'})); ## Test 'outerjoin' method %!test %! L = table ([1;2;3], {'a';'b';'c'}, 'VariableNames', {'Key', 'L'}); %! R = table ([2;3;5], {'x';'y';'z'}, 'VariableNames', {'Key', 'R'}); %! [T, ixL, ixR] = outerjoin (L, R); %! assert_equal (T.Properties.VariableNames, {'Key_L', 'L', 'Key_R', 'R'}); %! assert_equal (height (T), 4); %! assert_equal (T.Key_L, [1;2;3;NaN]); %! assert_equal (T.Key_R, [NaN;2;3;5]); %! assert_equal (T.L, {'a';'b';'c';''}); %! assert_equal (T.R, {'';'x';'y';'z'}); %! assert_equal (ixL, [1;2;3;0]); %! assert_equal (ixR, [0;1;2;3]); ## Outer join drops row names %!test %! L = table ([1;2], {'a';'b'}, 'VariableNames', {'Key', 'L'}, ... %! 'RowNames', {'r1', 'r2'}); %! R = table ([2;1], {'x';'y'}, 'VariableNames', {'Key', 'R'}); %! T = outerjoin (L, R); %! assert_equal (isempty (T.Properties.RowNames), true); ## 'Type' 'left' keeps all left rows and only matching right rows %!test %! L = table ([1;2;3], {'a';'b';'c'}, 'VariableNames', {'Key', 'L'}); %! R = table ([2;3;5], {'x';'y';'z'}, 'VariableNames', {'Key', 'R'}); %! [T, ixL, ixR] = outerjoin (L, R, 'Type', 'left'); %! assert_equal (height (T), 3); %! assert_equal (T.Key_L, [1;2;3]); %! assert_equal (ixL, [1;2;3]); %! assert_equal (ixR, [0;1;2]); ## 'Type' 'right' keeps all right rows and only matching left rows %!test %! L = table ([1;2;3], {'a';'b';'c'}, 'VariableNames', {'Key', 'L'}); %! R = table ([2;3;5], {'x';'y';'z'}, 'VariableNames', {'Key', 'R'}); %! [T, ixL, ixR] = outerjoin (L, R, 'Type', 'right'); %! assert_equal (height (T), 3); %! assert_equal (T.Key_R, [2;3;5]); %! assert_equal (ixL, [2;3;0]); %! assert_equal (ixR, [1;2;3]); ## 'MergeKeys' merges the key pair into a single column %!test %! L = table ([1;2;3], {'a';'b';'c'}, 'VariableNames', {'Key', 'L'}); %! R = table ([2;3;5], {'x';'y';'z'}, 'VariableNames', {'Key', 'R'}); %! T = outerjoin (L, R, 'MergeKeys', true); %! assert_equal (T.Properties.VariableNames, {'Key', 'L', 'R'}); %! assert_equal (T.Key, [1;2;3;5]); %! assert_equal (T.L, {'a';'b';'c';''}); %! assert_equal (T.R, {'';'x';'y';'z'}); ## 'Keys' with multiple key variables, merged %!test %! L = table ([1;1;2], {'a';'b';'a'}, (1:3)', ... %! 'VariableNames', {'K1', 'K2', 'N'}); %! R = table ([1;2], {'b';'a'}, {'x';'y'}, ... %! 'VariableNames', {'K1', 'K2', 'M'}); %! T = outerjoin (L, R, 'Keys', {'K1', 'K2'}, 'MergeKeys', true); %! assert_equal (T.Properties.VariableNames, {'K1', 'K2', 'N', 'M'}); %! assert_equal (height (T), 3); %! assert_equal (T.K1, [1;1;2]); %! assert_equal (T.K2, {'a';'b';'a'}); ## 'MergeKeys' with differently-named keys uses the left key name %!test %! L = table ([1;2], (10:11)', 'VariableNames', {'KL', 'A'}); %! R = table ([2;3], {'x';'y'}, 'VariableNames', {'KR', 'B'}); %! T = outerjoin (L, R, 'LeftKeys', 'KL', 'RightKeys', 'KR', ... %! 'MergeKeys', true); %! assert_equal (T.Properties.VariableNames, {'KL_KR', 'A', 'B'}); %! assert_equal (T.KL_KR, [1;2;3]); %! assert_equal (T.A, [10;11;NaN]); %! assert_equal (T.B, {'';'x';'y'}); ## String variables in unmatched rows are filled with %!test %! L = table ([1;2], string ({'a';'b'}), 'VariableNames', {'K', 'S'}); %! R = table ([2;3], {'p';'q'}, 'VariableNames', {'K', 'R'}); %! T = outerjoin (L, R); %! assert_equal (ismissing (T.S), [false;false;true]); %! assert_equal (T.R, {'';'p';'q'}); ## Categorical variables in unmatched rows are filled with %!test %! L = table ([1;2], {'a';'b'}, 'VariableNames', {'K', 'L'}); %! R = table ([2;3], categorical ({'p';'q'}), 'VariableNames', {'K', 'C'}); %! T = outerjoin (L, R); %! assert_equal (ismissing (T.C), [true;false;false]); ## Cartesian product on repeated keys in a full outer join %!test %! L = table ([1;1;9], {'a';'b';'z'}, 'VariableNames', {'K', 'L'}); %! R = table ([1;1], {'x';'y'}, 'VariableNames', {'K', 'R'}); %! T = outerjoin (L, R); %! assert_equal (height (T), 5); %! assert_equal (T.L, {'a';'a';'b';'b';'z'}); %! assert_equal (T.R, {'x';'y';'x';'y';''}); ## An empty input table yields all-missing fill on that side %!test %! E = table (zeros (0, 1), cell (0, 1), 'VariableNames', {'K', 'L'}); %! R = table ([1;2], {'x';'y'}, 'VariableNames', {'K', 'R'}); %! T = outerjoin (E, R); %! assert_equal (height (T), 2); %! assert_equal (T.K_R, [1;2]); %! assert_equal (T.L, {'';''}); %! assert_equal (T.R, {'x';'y'}); ## 'LeftVariables'/'RightVariables' restrict the output columns %!test %! L = table ([1;2;3], (10:12)', {'p';'q';'r'}, ... %! 'VariableNames', {'K', 'A', 'C'}); %! R = table ([2;3], {'x';'y'}, 'VariableNames', {'K', 'B'}); %! T = outerjoin (L, R, 'LeftVariables', 'A', 'RightVariables', 'B', ... %! 'Type', 'left'); %! assert_equal (T.Properties.VariableNames, {'A', 'B'}); %! assert_equal (T.A, [10;11;12]); %! assert_equal (T.B, {'';'x';'y'}); ## 'outerjoin' on a duration key fills unmatched keys with missing %!test %! L = table (hours ([3;1;2]), {'a';'b';'c'}, 'VariableNames', {'K', 'L'}); %! R = table (hours ([2;5]), {'p';'q'}, 'VariableNames', {'K', 'R'}); %! [T, ixL, ixR] = outerjoin (L, R); %! assert_equal (hours (T.K_L), [1;2;3;NaN]); %! assert_equal (ismissing (T.K_L), [false;false;false;true]); %! assert_equal (T.R, {'';'p';'';'q'}); %! assert_equal (ixL, [2;3;1;0]); %! assert_equal (ixR, [0;1;0;2]); ## 'outerjoin' fills an unmatched duration variable with missing %!test %! L = table ([1;2], hours ([10;20]), 'VariableNames', {'K', 'D'}); %! R = table ([2;3], {'x';'y'}, 'VariableNames', {'K', 'R'}); %! T = outerjoin (L, R); %! assert_equal (ismissing (T.D), [false;false;true]); %! assert_equal (hours (T.D), [10;20;NaN]); ## 'outerjoin' fills an unmatched calendarDuration variable with missing %!test %! L = table ([1;2], calmonths ([5;6]), 'VariableNames', {'K', 'CD'}); %! R = table ([2;3], {'x';'y'}, 'VariableNames', {'K', 'R'}); %! T = outerjoin (L, R); %! assert_equal (ismissing (T.CD), [false;false;true]); ## 'outerjoin' fills an unmatched string variable with %!test %! L = table ([1;2], string ({'a';'b'}), 'VariableNames', {'K', 'S'}); %! R = table ([2;3], {'x';'y'}, 'VariableNames', {'K', 'R'}); %! T = outerjoin (L, R); %! assert_equal (ismissing (T.S), [false;false;true]); ## 'outerjoin' on a datetime key fills unmatched keys with NaT %!test %! L = table (datetime (2020, 1, [3;1;2]), {'a';'b';'c'}, ... %! 'VariableNames', {'K', 'L'}); %! R = table (datetime (2020, 1, [2;5]), {'p';'q'}, 'VariableNames', {'K', ... %! 'R'}); %! [T, ixL, ixR] = outerjoin (L, R); %! assert_equal (datevec (T.K_L)(:,3), [1;2;3;NaN]); %! assert_equal (ismissing (T.K_L), [false;false;false;true]); %! assert_equal (T.R, {'';'p';'';'q'}); %! assert_equal (ixL, [2;3;1;0]); %! assert_equal (ixR, [0;1;0;2]); ## 'outerjoin' fills an unmatched datetime variable with NaT %!test %! L = table ([1;2], datetime (2021, 1, [10;20]), 'VariableNames', {'K', 'D'}); %! R = table ([2;3], {'x';'y'}, 'VariableNames', {'K', 'R'}); %! T = outerjoin (L, R); %! assert_equal (ismissing (T.D), [false;false;true]); %!test # a nested table variable is carried by its rows %! N = table ([1; 2; 3], 'VariableNames', {'p'}); %! T = table (N, [1; 1; 2], [5; 6; 7], 'VariableNames', {'A', 'G', 'V'}); %! J = outerjoin (T, table ([1; 2], [8; 9], 'VariableNames', {'G', 'W'}), ... %! 'Keys', 'G'); %! assert_equal (J.A.p, [1; 2; 3]); %!test # an undefined key matches nothing, as in MATLAB %! T = table (categorical ({'lo'; ''}), [1; 1], 'VariableNames', {'C', 'K'}); %! U = table (categorical ({'lo'; ''}), [3; 4], 'VariableNames', {'C', 'W'}); %! J = outerjoin (T, U, 'Keys', 'C'); %! assert_equal (J.W, [3; NaN; 4]); %!test # an unmatched row pads a struct and a nested table, as in MATLAB %! L = table ([1; 2], struct ('a', {1; 2}), ... %! table ([5; 6], 'VariableNames', {'p'}), ... %! 'VariableNames', {'K', 'S', 'N'}); %! R = table ([1; 3], [7; 8], 'VariableNames', {'K', 'W'}); %! J = outerjoin (L, R, 'Keys', 'K'); %! assert_equal (isempty (J.S(3).a), true); %! assert_equal (J.N.p, [5; 6; NaN]); %!test # one table joined with itself numbers the left names, as in MATLAB %! T = table (categorical ({'lo'; 'hi'}), [1; 2], 'VariableNames', {'C', 'K'}); %! J = outerjoin (T, T, 'Keys', 'C'); %! assert_equal (J.Properties.VariableNames, {'C_T_1', 'K_T_1', 'C_T', 'K_T'}); %!error ... %! outerjoin (table ([1;2])); %!error ... %! outerjoin (table ([1;2]), 5); %!error ... %! outerjoin (table ([1;2], 'VariableNames', {'K'}), ... %! table ([1;2], 'VariableNames', {'K'}), 'Type', 'bogus'); %!error ... %! outerjoin (table ([1;2], 'VariableNames', {'K'}), ... %! table ([1;2], 'VariableNames', {'K'}), 'MergeKeys', 3); %!error ... %! outerjoin (table (1), table (1), 'Keys', 1, 'LeftKeys', 1); %!error ... %! outerjoin (table (1, 'VariableNames', {'a'}), ... %! table (1, 'VariableNames', {'b'}), 'LeftKeys', 'a'); %!error ... %! outerjoin (table ([1;2], [1;2], 'VariableNames', {'a', 'b'}), ... %! table ([1;2], 'VariableNames', {'c'}), ... %! 'LeftKeys', {'a', 'b'}, 'RightKeys', 'c'); %!error ... %! outerjoin (table ((1:3)', 'VariableNames', {'AA'}), ... %! table ((1:3)', 'VariableNames', {'BB'})); %!error ... %! outerjoin (table ([1;2], 'VariableNames', {'K'}), ... %! table ([1;2], 'VariableNames', {'K'}), 'BadOption', 1); %!error ... %! outerjoin (table ([1;2], 'VariableNames', {'K'}), ... %! table ([1;2], 'VariableNames', {'K'}), 'BadOption'); %!error ... %! outerjoin (table ([1;2], 'VariableNames', {'K'}), ... %! table ({'1';'2'}, 'VariableNames', {'K'})); ## Test 'union' method %!test %! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'}); %! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'}); %! [U, ia, ib] = union (A, B); %! assert_equal (U.Properties.VariableNames, {'N', 'S'}); %! assert_equal (U.N, [1;2;3;5]); %! assert_equal (U.S, {'a';'b';'c';'e'}); %! assert_equal (ia, [2;3;1]); %! assert_equal (ib, 2); ## 'union' with 'stable' set order %!test %! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'}); %! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'}); %! U = union (A, B, 'stable'); %! assert_equal (U.N, [3;1;2;5]); %! assert_equal (U.S, {'c';'a';'b';'e'}); ## 'union' ignores variable order (output keeps the first table's order) %!test %! A = table ([3;1], {'c';'a'}, 'VariableNames', {'N', 'S'}); %! B = table ({'a';'e'}, [1;5], 'VariableNames', {'S', 'N'}); %! U = union (A, B); %! assert_equal (U.Properties.VariableNames, {'N', 'S'}); %! assert_equal (U.N, [1;3;5]); %! assert_equal (U.S, {'a';'c';'e'}); ## 'union' drops row names (rows come from both tables) %!test %! A = table ([3;1], {'c';'a'}, 'VariableNames', {'N', 'S'}, ... %! 'RowNames', {'r1', 'r2'}); %! B = table ([2;5], {'b';'e'}, 'VariableNames', {'N', 'S'}, ... %! 'RowNames', {'q1', 'q2'}); %! U = union (A, B); %! assert_equal (U.N, [1;2;3;5]); %! assert_equal (isempty (U.Properties.RowNames), true); ## 'union' keeps the result's own sorted order when A and B rows interleave %!test %! A = table ([3;1], {'c';'a'}, 'VariableNames', {'N', 'S'}); %! B = table ([4;2], {'d';'b'}, 'VariableNames', {'N', 'S'}); %! U = union (A, B); %! assert_equal (U.N, [1;2;3;4]); %! assert_equal (U.S, {'a';'b';'c';'d'}); ## 'union' 'stable' keeps appearance order when A and B rows interleave %!test %! A = table ([3;1], {'c';'a'}, 'VariableNames', {'N', 'S'}); %! B = table ([4;2], {'d';'b'}, 'VariableNames', {'N', 'S'}); %! U = union (A, B, 'stable'); %! assert_equal (U.N, [3;1;4;2]); %! assert_equal (U.S, {'c';'a';'d';'b'}); ## Test 'intersect' method %!test %! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'}); %! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'}); %! [I, ia, ib] = intersect (A, B); %! assert_equal (I.N, [1;2]); %! assert_equal (I.S, {'a';'b'}); %! assert_equal (ia, [2;3]); %! assert_equal (ib, [3;1]); ## 'intersect' with no common rows yields an empty table %!test %! A = table ([1;2], {'a';'b'}, 'VariableNames', {'N', 'S'}); %! B = table ([8;9], {'x';'y'}, 'VariableNames', {'N', 'S'}); %! I = intersect (A, B); %! assert_equal (height (I), 0); %! assert_equal (I.Properties.VariableNames, {'N', 'S'}); ## 'intersect' keeps the row names of the matched rows from the first table %!test %! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'}, ... %! 'RowNames', {'r1', 'r2', 'r3', 'r4'}); %! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'}); %! I = intersect (A, B); %! assert_equal (I.N, [1;2]); %! assert_equal (I.Properties.RowNames, {'r2';'r3'}); ## Test 'setdiff' method %!test %! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'}); %! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'}); %! [D, ia] = setdiff (A, B); %! assert_equal (D.N, 3); %! assert_equal (D.S, {'c'}); %! assert_equal (ia, 1); ## 'setdiff' with 'stable' set order %!test %! A = table ([5;3;5], {'e';'c';'e'}, 'VariableNames', {'N', 'S'}); %! B = table ([1], {'a'}, 'VariableNames', {'N', 'S'}); %! D = setdiff (A, B, 'stable'); %! assert_equal (D.N, [5;3]); ## 'setdiff' keeps the row names of the retained rows from the first table %!test %! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'}, ... %! 'RowNames', {'r1', 'r2', 'r3', 'r4'}); %! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'}); %! D = setdiff (A, B); %! assert_equal (D.N, 3); %! assert_equal (D.Properties.RowNames, {'r1'}); ## Test 'setxor' method %!test %! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'}); %! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'}); %! [X, ia, ib] = setxor (A, B); %! assert_equal (X.N, [3;5]); %! assert_equal (X.S, {'c';'e'}); %! assert_equal (ia, 1); %! assert_equal (ib, 2); ## 'setxor' drops row names (rows come from both tables) %!test %! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'}, ... %! 'RowNames', {'r1', 'r2', 'r3', 'r4'}); %! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'}, ... %! 'RowNames', {'q1', 'q2', 'q3'}); %! X = setxor (A, B); %! assert_equal (X.N, [3;5]); %! assert_equal (isempty (X.Properties.RowNames), true); ## 'setxor' keeps the result's own sorted order when A and B rows interleave %!test %! A = table ([3;1], {'c';'a'}, 'VariableNames', {'N', 'S'}); %! B = table ([4;2], {'d';'b'}, 'VariableNames', {'N', 'S'}); %! X = setxor (A, B); %! assert_equal (X.N, [1;2;3;4]); %! assert_equal (X.S, {'a';'b';'c';'d'}); ## Test 'ismember' method %!test %! A = table ([3;1;2;1], {'c';'a';'b';'a'}, 'VariableNames', {'N', 'S'}); %! B = table ([2;5;1], {'b';'e';'a'}, 'VariableNames', {'N', 'S'}); %! [TF, ixB] = ismember (A, B); %! assert_equal (TF, [false;true;true;true]); %! assert_equal (ixB, [0;3;1;3]); ## Set operations work on text and numeric keys via the row proxy %!test %! A = table (categorical ({'a';'b';'c'}), [1;2;3], 'VariableNames', {'C', ... %! 'N'}); %! B = table (categorical ({'b';'d'}), [2;4], 'VariableNames', {'C', 'N'}); %! I = intersect (A, B); %! assert_equal (cellstr (I.C), {'b'}); %! assert_equal (I.N, 2); ## 'union' over string, categorical, duration and calendarDuration variables %!test %! A = table (string ({'a';'b';'c'}), categorical ({'x';'y';'z'}), ... %! hours ([1;2;3]), calmonths ([1;2;3]), ... %! 'VariableNames', {'S', 'C', 'D', 'M'}); %! B = table (string ({'b';'d'}), categorical ({'y';'w'}), hours ([2;4]), ... %! calmonths ([2;4]), 'VariableNames', {'S', 'C', 'D', 'M'}); %! U = union (A, B); %! assert_equal (cellstr (U.S), {'a';'b';'c';'d'}); %! assert_equal (cellstr (U.C), {'x';'y';'z';'w'}); %! assert_equal (hours (U.D), [1;2;3;4]); %! assert_equal (calmonths (U.M), [1;2;3;4]); ## 'intersect' over string, categorical, duration and calendarDuration variables %!test %! A = table (string ({'a';'b';'c'}), categorical ({'x';'y';'z'}), ... %! hours ([1;2;3]), calmonths ([1;2;3]), ... %! 'VariableNames', {'S', 'C', 'D', 'M'}); %! B = table (string ({'b';'d'}), categorical ({'y';'w'}), hours ([2;4]), ... %! calmonths ([2;4]), 'VariableNames', {'S', 'C', 'D', 'M'}); %! I = intersect (A, B); %! assert_equal (cellstr (I.S), {'b'}); %! assert_equal (cellstr (I.C), {'y'}); %! assert_equal (hours (I.D), 2); %! assert_equal (calmonths (I.M), 2); ## 'setdiff' over string, categorical, duration and calendarDuration variables %!test %! A = table (string ({'a';'b';'c'}), categorical ({'x';'y';'z'}), ... %! hours ([1;2;3]), calmonths ([1;2;3]), ... %! 'VariableNames', {'S', 'C', 'D', 'M'}); %! B = table (string ({'b';'d'}), categorical ({'y';'w'}), hours ([2;4]), ... %! calmonths ([2;4]), 'VariableNames', {'S', 'C', 'D', 'M'}); %! D = setdiff (A, B); %! assert_equal (cellstr (D.S), {'a';'c'}); %! assert_equal (hours (D.D), [1;3]); ## 'setxor' over string, categorical, duration and calendarDuration variables %!test %! A = table (string ({'a';'b';'c'}), categorical ({'x';'y';'z'}), ... %! hours ([1;2;3]), calmonths ([1;2;3]), ... %! 'VariableNames', {'S', 'C', 'D', 'M'}); %! B = table (string ({'b';'d'}), categorical ({'y';'w'}), hours ([2;4]), ... %! calmonths ([2;4]), 'VariableNames', {'S', 'C', 'D', 'M'}); %! X = setxor (A, B); %! assert_equal (cellstr (X.S), {'a';'c';'d'}); %! assert_equal (hours (X.D), [1;3;4]); ## 'ismember' over string, categorical, duration and calendarDuration variables %!test %! A = table (string ({'a';'b';'c'}), categorical ({'x';'y';'z'}), ... %! hours ([1;2;3]), calmonths ([1;2;3]), ... %! 'VariableNames', {'S', 'C', 'D', 'M'}); %! B = table (string ({'b';'d'}), categorical ({'y';'w'}), hours ([2;4]), ... %! calmonths ([2;4]), 'VariableNames', {'S', 'C', 'D', 'M'}); %! [TF, ixB] = ismember (A, B); %! assert_equal (TF, [false;true;false]); %! assert_equal (ixB, [0;1;0]); ## Set operations over a datetime key variable (rowProxies datetime path) %!test %! A = table (datetime (2020, 1, [3;1;2]), {'c';'a';'b'}, ... %! 'VariableNames', {'D', 'S'}); %! B = table (datetime (2020, 1, [2;5]), {'b';'e'}, 'VariableNames', {'D', ... %! 'S'}); %! U = union (A, B); %! assert_equal (datevec (U.D)(:,3), [1;2;3;5]); %! I = intersect (A, B); %! assert_equal (I.S, {'b'}); %! D = setdiff (A, B); %! assert_equal (D.S, {'a';'c'}); %! X = setxor (A, B); %! assert_equal (X.S, {'a';'c';'e'}); %! [TF, ixB] = ismember (A, B); %! assert_equal (TF, [false;false;true]); %! assert_equal (ixB, [0;0;1]); %!test # rows with a missing value are never equal, as in MATLAB %! T = table ([1.5; NaN], [1; 2], 'VariableNames', {'A', 'K'}); %! U = table ([7; NaN], [1; 2], 'VariableNames', {'A', 'K'}); %! assert_equal (union (T, T).A, [1.5; NaN; NaN]); %! assert_equal (union (T, U, 'stable').A, [1.5; NaN; 7; NaN]); %!test # rows with an undefined value are never equal, and sort last %! T = table (categorical ({'lo'; ''}), [1; 1], 'VariableNames', {'C', 'K'}); %! U = union (T, T); %! assert_equal (isundefined (U.C), [false; true; true]); %!test # a multicolumn cellstr key is compared row by row %! C = table ({'a', 'b'; 'c', 'd'; 'a', 'b'}, 'VariableNames', {'A'}); %! D = table ({'a', 'b'; 'x', 'y'}, 'VariableNames', {'A'}); %! assert_equal (union (C, D).A, {'a', 'b'; 'c', 'd'; 'x', 'y'}); %! assert_equal (intersect (C, D).A, {'a', 'b'}); %! assert_equal (ismember (C, D), [true; false; true]); %! assert_equal (setdiff (C, D).A, {'c', 'd'}); %!test # a nested table key is compared by its own variables %! N = table ([1; 2; 1], 'VariableNames', {'p'}); %! T = table (N, [1; 2; 1], 'VariableNames', {'N', 'K'}); %! assert_equal (union (T, T).K, [1; 2]); %!error ... %! union (table ([1;2])); %!error ... %! union (table ([1;2]), 5); %!error ... %! union (table ([1;2], 'VariableNames', {'N'}), ... %! table ([1;2], 'VariableNames', {'Z'})); %!error ... %! union (table ([1;2], 'VariableNames', {'N'}), ... %! table ([1;2], 'VariableNames', {'N'}), 'bogus'); %!error ... %! union (table ([1;2], 'VariableNames', {'N'}), ... %! table ([1;2], 'VariableNames', {'N'}), 'sorted', 'extra'); %!test # a row with a missing value is in no intersection %! T = table (categorical ({'lo'; ''}), [1; 1], 'VariableNames', {'C', 'K'}); %! assert_equal (intersect (T, T).C, categorical ({'lo'}, {'lo'})); %!error ... %! intersect (table ([1;2])); %!error ... %! intersect (table ([1;2]), 5); %!error ... %! intersect (table ([1;2], 'VariableNames', {'N'}), ... %! table ([1;2], 'VariableNames', {'Z'})); %!error ... %! intersect (table ([1;2], 'VariableNames', {'N'}), ... %! table ([1;2], 'VariableNames', {'N'}), 'bogus'); %!error ... %! intersect (table ([1;2], 'VariableNames', {'N'}), ... %! table ([1;2], 'VariableNames', {'N'}), 'sorted', 'extra'); %!test # a row with a missing value is never removed %! T = table ([string('x'); string(missing)], [1; 1], ... %! 'VariableNames', {'S', 'K'}); %! D = setdiff (T, T); %! assert_equal (ismissing (D.S), true); %!error ... %! setdiff (table ([1;2])); %!error ... %! setdiff (table ([1;2]), 5); %!error ... %! setdiff (table ([1;2], 'VariableNames', {'N'}), ... %! table ([1;2], 'VariableNames', {'Z'})); %!error ... %! setdiff (table ([1;2], 'VariableNames', {'N'}), ... %! table ([1;2], 'VariableNames', {'N'}), 'bogus'); %!error ... %! setdiff (table ([1;2], 'VariableNames', {'N'}), ... %! table ([1;2], 'VariableNames', {'N'}), 'sorted', 'extra'); %!test # rows with a missing value are never equal, as in MATLAB %! T = table ([1.5; NaN], [1; 2], 'VariableNames', {'A', 'K'}); %! U = table ([7; NaN], [1; 2], 'VariableNames', {'A', 'K'}); %! assert_equal (setxor (T, U).A, [1.5; 7; NaN; NaN]); %! assert_equal (setxor (T, U, 'stable').A, [1.5; NaN; 7; NaN]); %!error ... %! setxor (table ([1;2])); %!error ... %! setxor (table ([1;2]), 5); %!error ... %! setxor (table ([1;2], 'VariableNames', {'N'}), ... %! table ([1;2], 'VariableNames', {'Z'})); %!error ... %! setxor (table ([1;2], 'VariableNames', {'N'}), ... %! table ([1;2], 'VariableNames', {'N'}), 'bogus'); %!error ... %! setxor (table ([1;2], 'VariableNames', {'N'}), ... %! table ([1;2], 'VariableNames', {'N'}), 'sorted', 'extra'); %!test # a row with a missing value is a member of nothing %! T = table (categorical ({'lo'; ''}), [1; 1], 'VariableNames', {'C', 'K'}); %! assert_equal (ismember (T, T), [true; false]); %!error ... %! ismember (table ([1;2])); %!error ... %! ismember (table ([1;2]), 5); %!error ... %! ismember (table ([1;2], 'VariableNames', {'N'}), ... %! table ([1;2], 'VariableNames', {'Z'})); ################################################################################ ## ** Missing Values ** ## ################################################################################ ## Available Methods ## ## ## ## 'anymissing' 'ismissing' 'rmmissing' 'fillmissing' ## ## 'standardizeMissing' ## ## ## ################################################################################ ## Test 'anymissing' method %!test %! T = table (categorical ({'1';'2';'';'4'})); %! assert_equal (anymissing (T), true); %!test %! T = table (categorical ({'1';'2';'3';'4'})); %! assert_equal (anymissing (T), false); %!test %! T = table (calyears ([1; 2; NaN; 4])); %! assert_equal (anymissing (T), true); %!test %! T = table (calyears ([1; 2; 3; 4])); %! assert_equal (anymissing (T), false); %!test %! T = table (days ([1; 2; NaN; 4])); %! assert_equal (anymissing (T), true); %!test %! T = table (days ([1; 2; 3; 4])); %! assert_equal (anymissing (T), false); %!test %! T = table (string ([1; 2; NaN; 4])); %! assert_equal (anymissing (T), true); %!test %! T = table (string ([1; 2; 3; 4])); %! assert_equal (anymissing (T), false); %!test %! T = table ({'1'; '2'; ''; '4'}); %! assert_equal (anymissing (T), true); %!test %! T = table ({'1'; '2'; 'NaN'; '4'}); %! assert_equal (anymissing (T), false); %!test %! T = table (['1'; '2'; ' '; '4']); %! assert_equal (anymissing (T), true); %!test %! T = table (['1'; '2'; 'NaN'; '4']); %! assert_equal (anymissing (T), false); %!test %! T = table (['1'; '2'; 'N'; '4']); %! assert_equal (anymissing (T), false); ## Test 'ismissing' method %!test %! TF = ismissing (table ([' '; ' 2 '; 'NaN'; '4 ']), ' 2'); %! assert_equal (TF, [false; true; false; false]); %!test %! TF = ismissing (table ([' '; ' 2 '; 'NaN'; '4 ']), '2'); %! assert_equal (TF, [false; false; false; false]); %!test %! TF = ismissing (table ([' '; '2 '; 'NaN'; '4 ']), '2'); %! assert_equal (TF, [false; true; false; false]); %!test %! TF = ismissing (table ([' '; ' 2 '; 'NaN'; '4 '])); %! assert_equal (TF, [true; false; false; false]); %!test %! TF = ismissing (table ({'1'; '2'; ''; '4'})); %! assert_equal (TF, [false; false; true; false]); %!test %! TF = ismissing (table ({'1'; '2'; ''; '4'}), 'OutputFormat', 'tabular'); %! assert_equal (class (TF), 'table'); %! assert_equal (TF.Var1, [false; false; true; false]); %!test %! TF = ismissing (table ({'1'; '2'; ''; '4'}), {'4'}, 'OutputFormat', ... %! 'tabular'); %! assert_equal (class (TF), 'table'); %! assert_equal (TF.Var1, [false; false; false; true]); %!test %! TF = ismissing (table ({'1'; '2'; ''; '4'}, string ([1; 2; 3; 4])), {'4', ... %! '3'}, 'OutputFormat', 'tabular'); %! assert_equal (class (TF), 'table'); %! assert_equal (TF.Var1, [false; false; false; true]); %! assert_equal (TF.Var2, [false; false; true; true]); %!test %! TF = ismissing (table ({'1'; '2'; ''; '4'}, string ([1; 2; 3; 4])), {'4', ... %! '3'}); %! assert_equal (class (TF), 'logical'); %! assert_equal (TF, [false, false; false, false; false, true; true, true]); %!test %! TF = ismissing (table (days ([1; 2; NaN; 4]), calweeks ([1; 2; NaN; 4]))); %! assert_equal (TF, [false, false; false, false; true, true; false, false]); %!test # a scalar duration indicator matches a duration variable %! TF = ismissing (table (hours ([1; 2; 3]), 'VariableNames', {'d'}), ... %! hours (2)); %! assert_equal (TF, [false; true; false]); %!test # a numeric NaN indicator does not apply to duration variables %! TF = ismissing (table (hours ([1; NaN; 3]), 'VariableNames', {'d'}), NaN); %! assert_equal (TF, [false; false; false]); %!test # a NaN duration indicator flags missing duration entries %! TF = ismissing (table (hours ([1; NaN; 3]), 'VariableNames', {'d'}), ... %! hours (NaN)); %! assert_equal (TF, [false; true; false]); %!test # missing marks the standard missing values of every variable %! T = table (seconds ([1; NaN]), caldays ([1; NaN]), [1; NaN]); %! assert_equal (ismissing (T, missing), logical ([0, 0, 0; 1, 1, 1])); %! assert_equal (ismissing (T, {missing}), logical ([0, 0, 0; 1, 1, 1])); %! assert_equal (ismissing (T, {missing, 1}), logical ([0, 0, 1; 1, 1, 1])); %! assert_equal (ismissing (T, {NaN, seconds(NaN)}), ... %! logical ([0, 0, 0; 1, 0, 1])); %!test # missing covers datetime, categorical, string, cellstr and char %! T = table ([datetime(2020, 1, 1); NaT], categorical ({'a'; ''}), ... %! string ({'a'; 'b'}), {'a'; ''}, ['a'; ' ']); %! T.Var3(2) = missing; %! assert_equal (ismissing (T, missing), ... %! logical ([0, 0, 0, 0, 0; 1, 1, 1, 1, 1])); %! assert_equal (ismissing (T, NaT), ... %! logical ([0, 0, 0, 0, 0; 1, 0, 0, 0, 0])); %!test # a string marks no categorical element %! T = table (categorical ({'a'; ''}), string ({'a'; 'b'})); %! T.Var2(2) = missing; %! assert_equal (ismissing (T, string (missing)), logical ([0, 0; 0, 1])); %! assert_equal (ismissing (T, {string(missing)}), logical ([0, 0; 0, 1])); %! assert_equal (ismissing (T, [string(missing), "a"]), ... %! logical ([1, 1; 0, 1])); %! assert_equal (ismissing (T, {string(missing), "a"}), ... %! logical ([1, 1; 0, 1])); %!test # text indicators on categorical variables %! T = table (categorical ({'a'; ''}), {'a'; ''}); %! assert_equal (ismissing (T, ''), logical ([0, 0; 1, 1])); %! assert_equal (ismissing (T, ' '), logical ([0, 0; 1, 0])); %! assert_equal (ismissing (T, {''}), logical ([0, 0; 1, 1])); %! assert_equal (ismissing (T, {'a', ''}), logical ([1, 1; 1, 1])); %! assert_equal (ismissing (T, {'a', 1}), logical ([1, 1; 0, 0])); %! assert_equal (ismissing (T, string ('')), logical ([0, 0; 0, 0])); %! assert_equal (ismissing (T, {string(''), 1}), logical ([0, 0; 0, 0])); %!test # a blank indicator matches only that exact text in a cellstr %! T = table ({'a'; ''; ' '; ' '}); %! assert_equal (ismissing (T, ' '), logical ([0; 0; 1; 0])); %! assert_equal (ismissing (T, ' '), logical ([0; 0; 0; 1])); %! assert_equal (ismissing (T, ''), logical ([0; 1; 0; 0])); %! assert_equal (ismissing (T, {' '}), logical ([0; 0; 1; 0])); %!test # character vectors in a cell indicator stay apart %! T = table (categorical ({'a'; 'b'; 'ab'})); %! assert_equal (ismissing (T, {'a', 'b', 1}), [true; true; false]); %!test # calendarDuration indicators match calendarDuration variables %! T = table (seconds ([1; NaN]), caldays ([1; NaN]), [1; NaN]); %! assert_equal (ismissing (T, caldays (NaN)), logical ([0, 0, 0; 0, 1, 0])); %! assert_equal (ismissing (T, {caldays(1), 1}), logical ([0, 1, 1; 0, 0, 0])); %!test # a missing variable is missing in every row %! T = table ([missing; missing], [1; 2], 'VariableNames', {'M', 'K'}); %! assert_equal (ismissing (T), [true, false; true, false]); %!test # a nested table variable is never missing, as in MATLAB %! T = table (table ([1; NaN]), [1; 2], 'VariableNames', {'N', 'K'}); %! assert_equal (ismissing (T), false (2, 2)); %! assert_equal (ismissing (T, NaN), false (2, 2)); %! assert_equal (size (rmmissing (T)), [2, 2]); %! assert_equal (anymissing (T), false); %!test # a character vector indicator is one text value for a cellstr %! assert_equal (ismissing (table ({'a'; 'b'}), 'a'), [true; false]); %! assert_equal (ismissing (table ({'a'; 'b'}, [5; 1]), {'b', 5}), ... %! [false, true; true, false]); %!test # a logical indicator matches double and logical variables only %! T = table ([1; 0; 2], single ([1; 0; 2]), int32 ([1; 0; 2]), ... %! [true; false; true]); %! assert_equal (ismissing (T, true), ... %! logical ([1, 0, 0, 1; 0, 0, 0, 0; 0, 0, 0, 1])); %! assert_equal (ismissing (T, [true, false]), ... %! logical ([1, 0, 0, 1; 1, 0, 0, 1; 0, 0, 0, 1])); %! assert_equal (ismissing (T, {true}), ... %! logical ([1, 0, 0, 1; 0, 0, 0, 0; 0, 0, 0, 1])); %!test # a double indicator matches every numeric and logical variable %! T = table ([1; 0; 2], single ([1; 0; 2]), int32 ([1; 0; 2]), ... %! [true; false; true]); %! assert_equal (ismissing (T, 1), ... %! logical ([1, 1, 1, 1; 0, 0, 0, 0; 0, 0, 0, 1])); %!test # single and integer indicators match their own class and double %! T = table ([1; 0; 2], single ([1; 0; 2]), int32 ([1; 0; 2]), ... %! [true; false; true]); %! assert_equal (ismissing (T, single (1)), ... %! logical ([1, 1, 0, 0; 0, 0, 0, 0; 0, 0, 0, 0])); %! assert_equal (ismissing (T, int32 ([1, 2])), ... %! logical ([1, 0, 1, 0; 0, 0, 0, 0; 1, 0, 1, 0])); %!test # each entry of a cell indicator keeps its own class %! T = table ([1; 0; 2], single ([1; 0; 2]), int32 ([1; 0; 2]), ... %! [true; false; true]); %! assert_equal (ismissing (T, {true, single(1)}), ... %! logical ([1, 1, 0, 1; 0, 0, 0, 0; 0, 0, 0, 1])); %! assert_equal (ismissing (T, {int32(1), 2.4}), ... %! logical ([1, 0, 1, 0; 0, 0, 0, 0; 0, 0, 0, 0])); %! assert_equal (ismissing (T, {true, 1}), ... %! logical ([1, 1, 1, 1; 0, 0, 0, 0; 0, 0, 0, 1])); %! assert_equal (ismissing (T, {int8(0), single(2)}), ... %! logical ([0, 0, 0, 0; 1, 0, 0, 0; 1, 1, 0, 0])); %! assert_equal (ismissing (T, {false, int32(2)}), ... %! logical ([0, 0, 0, 0; 1, 0, 0, 1; 1, 0, 1, 0])); %! assert_equal (ismissing (T, {single(0), 'a'}), ... %! logical ([0, 0, 0, 0; 1, 1, 0, 0; 0, 0, 0, 0])); %!test # a variable no indicator applies to is not missing, not an error %! assert_equal (ismissing (table (single ([1; 0; 2])), true), false (3, 1)); %! assert_equal (ismissing (table (int32 ([1; 0; 2])), single (1)), ... %! false (3, 1)); %! assert_equal (ismissing (table ([true; false; true]), int8 (1)), ... %! false (3, 1)); %! assert_equal (ismissing (table ([true; false]), true), [true; false]); %!test # the 'tabular' output follows the same rule %! T = table ([1; 0; 2], single ([1; 0; 2]), int32 ([1; 0; 2]), ... %! [true; false; true]); %! TF = ismissing (T, true, 'OutputFormat', 'tabular'); %! assert_equal (TF.Var1, [true; false; false]); %! assert_equal (TF.Var2, false (3, 1)); %! assert_equal (TF.Var3, false (3, 1)); %! assert_equal (TF.Var4, [true; false; true]); ## Test 'rmmissing' method %!test # removes every row that contains a missing value %! T = table ([1; NaN; 3], {'a'; 'b'; ''}, 'VariableNames', {'x', 'y'}); %! R = rmmissing (T); %! assert_equal (size (R), [1, 2]); %! assert_equal (R.x, 1); %! assert_equal (R.y, {'a'}); %!test # the second output is the removed-row mask %! T = table ([1; NaN; 3], {'a'; 'b'; ''}, 'VariableNames', {'x', 'y'}); %! [~, TF] = rmmissing (T); %! assert_equal (TF, [false; true; true]); %!test # a table with no missing values is returned unchanged %! T = table ([1; 2; 3], 'VariableNames', {'x'}); %! [R, TF] = rmmissing (T); %! assert_equal (isequal (R, T), true); %! assert_equal (TF, [false; false; false]); %!test # 'DataVariables' restricts which variables are inspected %! T = table ([1; NaN; 3], {'a'; 'b'; ''}, 'VariableNames', {'x', 'y'}); %! [R, TF] = rmmissing (T, 'DataVariables', 'x'); %! assert_equal (TF, [false; true; false]); %! assert_equal (R.x, [1; 3]); %!test # 'MinNumMissing' needs at least N missing values in a row %! T = table ([1; NaN; NaN], {'a'; 'b'; ''}, 'VariableNames', {'x', 'y'}); %! [R, TF] = rmmissing (T, 'MinNumMissing', 2); %! assert_equal (TF, [false; false; true]); %! assert_equal (height (R), 2); %!test # 'MissingLocations' as a logical matrix overrides the derived mask %! T = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'a', 'b'}); %! [R, TF] = rmmissing (T, 'MissingLocations', logical ([0, 0; 1, 0; 0, 1])); %! assert_equal (TF, [false; true; true]); %! assert_equal (R.a, 1); %!test # 'MissingLocations' as a table of logicals overrides the derived mask %! T = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'a', 'b'}); %! ML = table (logical ([0; 1; 0]), logical ([0; 0; 1]), ... %! 'VariableNames', {'a', 'b'}); %! [R, TF] = rmmissing (T, 'MissingLocations', ML); %! assert_equal (TF, [false; true; true]); %! assert_equal (R.a, 1); %!test # 'DataVariables' with 'MissingLocations' inspects only the named vars %! T = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'a', 'b'}); %! ML = table (logical ([0; 1; 0]), 'VariableNames', {'a'}); %! [R, TF] = rmmissing (T, 'DataVariables', 'a', 'MissingLocations', ML); %! assert_equal (TF, [false; true; false]); %! assert_equal (R.b, [4; 6]); %!test # DIM 2 removes the variables holding a missing value %! M = table ([1; 2; 3; 4], [5; 6; 7; 8], [9; 10; 11; NaN], ... %! 'VariableNames', {'a', 'b', 'c'}); %! [R, TF] = rmmissing (M, 2); %! assert_equal (R.Properties.VariableNames, {'a', 'b'}); %! assert_equal (TF, [false, false, true]); %!test # DIM 1 given explicitly, as a logical true %! M = table ([1; 2; 3; 4], [5; 6; 7; 8], [9; 10; 11; NaN], ... %! 'VariableNames', {'a', 'b', 'c'}); %! [R, TF] = rmmissing (M, true); %! assert_equal (TF, [false; false; false; true]); %! assert_equal (height (R), 3); %!test # DIM 2 with 'MissingLocations' ignores the values held %! M = table ([1; 2; 3; 4], [5; 6; 7; 8], [9; 10; 11; NaN], ... %! 'VariableNames', {'a', 'b', 'c'}); %! L = logical ([1, 0, 0; 0, 1, 0; 0, 1, 0; 0, 0, 0]); %! [R, TF] = rmmissing (M, 2, 'MissingLocations', L); %! assert_equal (R.Properties.VariableNames, {'c'}); %! assert_equal (TF, [true, true, false]); %!test # DIM 2 with 'MinNumMissing' counts down each variable %! M = table ([1; 2; 3; 4], [5; 6; 7; 8], [9; 10; 11; NaN], ... %! 'VariableNames', {'a', 'b', 'c'}); %! L = logical ([1, 0, 0; 0, 1, 0; 0, 1, 0; 0, 0, 0]); %! [R, TF] = rmmissing (M, 2, 'MissingLocations', L, 'MinNumMissing', 2); %! assert_equal (R.Properties.VariableNames, {'a', 'c'}); %! assert_equal (TF, [false, true, false]); %!test # DIM 1 with 'MissingLocations' ignores the values held %! M = table ([1; 2; 3; 4], [5; 6; 7; 8], [9; 10; 11; NaN], ... %! 'VariableNames', {'a', 'b', 'c'}); %! L = logical ([1, 0, 0; 0, 1, 0; 0, 1, 0; 0, 0, 0]); %! [R, TF] = rmmissing (M, 1, 'MissingLocations', L); %! assert_equal (TF, [true; true; true; false]); %! assert_equal (R.a, 4); %!test # 'MissingLocations' sized as the whole table with 'DataVariables' %! M = table ([1; 2; 3; 4], [5; 6; 7; 8], [9; 10; 11; NaN], ... %! 'VariableNames', {'a', 'b', 'c'}); %! L = logical ([1, 0, 0; 0, 1, 0; 0, 1, 0; 0, 0, 1]); %! [R, TF] = rmmissing (M, 2, 'MissingLocations', L, ... %! 'DataVariables', {'a', 'b'}); %! assert_equal (R.Properties.VariableNames, {'c'}); %! assert_equal (TF, [true, true, false]); %!test # 'MissingLocations' sized as the 'DataVariables' %! M = table ([1; 2; 3; 4], [5; 6; 7; 8], [9; 10; 11; NaN], ... %! 'VariableNames', {'a', 'b', 'c'}); %! L = logical ([0, 0; 0, 1; 0, 1; 0, 0]); %! [R, TF] = rmmissing (M, 2, 'MissingLocations', L, ... %! 'DataVariables', {'a', 'b'}); %! assert_equal (R.Properties.VariableNames, {'a', 'c'}); %! assert_equal (TF, [false, true, false]); %!test # a multicolumn variable counts once per row %! T = table ([1, NaN; 2, 3; NaN, NaN], [1; 2; 3], ... %! 'VariableNames', {'m', 'v'}); %! [~, TF] = rmmissing (T, 2, 'MinNumMissing', 2); %! assert_equal (TF, [true, false]); %! [~, TF] = rmmissing (T, 2, 'MinNumMissing', 3); %! assert_equal (TF, [false, false]); %! [~, TF] = rmmissing (T, 1, 'MinNumMissing', 2); %! assert_equal (TF, [false; false; false]); %!test # DIM 2 on an empty table %! T = table ('Size', [0, 2], 'VariableTypes', {'double', 'double'}); %! [R, TF] = rmmissing (T, 2); %! assert_equal (size (R), [0, 2]); %! assert_equal (TF, [false, false]); %!test # a missing variable is missing, as in MATLAB %! T = table ([missing; missing], [1; 2], 'VariableNames', {'M', 'K'}); %! assert_equal (size (rmmissing (T)), [0, 2]); ## Test input validation for 'rmmissing' method %!error ... %! rmmissing (table ([1; NaN]), 3); %!error ... %! rmmissing (table ([1; NaN]), [1, 2]); %!error ... %! rmmissing (table ([1; NaN]), 2, 'MinNumMissing'); %!error ... %! rmmissing (table ([1; NaN]), 'MinNumMissing'); %!error ... %! rmmissing (table ([1; NaN]), 'MinNumMissing', 1.5); %!error ... %! rmmissing (table ([1; NaN], 'VariableNames', {'x'}), 'DataVariables', 'zzz'); %!error ... %! rmmissing (table ([1; NaN]), 'Bogus', 1); %!error ... %! rmmissing (table ([1; 2; 3], 'VariableNames', {'a'}), ... %! 'MissingLocations', table ([1; 2; 3], 'VariableNames', {'a'})); %!error ... %! rmmissing (table ([1; 2; 3], 'VariableNames', {'a'}), ... %! 'MissingLocations', table (logical ([0; 1]), 'VariableNames', ... %! {'a'})); %!error ... %! rmmissing (table ([1; 2; 3], 'VariableNames', {'a'}), ... %! 'MissingLocations', table (logical ([0; 1; 0]), ... %! 'VariableNames', {'b'})); ## Test 'fillmissing' method -- 'constant' method %!test # a scalar fill value is broadcast to the targeted numeric variable %! T = table ([1; NaN; 3; NaN], 'VariableNames', {'x'}); %! [R, TF] = fillmissing (T, 'constant', 0); %! assert_equal (R.x, [1; 0; 3; 0]); %! assert_equal (TF, [false; true; false; true]); %!test # a cell array gives one fill value per variable %! T = table ([1; NaN; 3], {'a'; ''; 'c'}, 'VariableNames', {'x', 'y'}); %! R = fillmissing (T, 'constant', {0, 'z'}); %! assert_equal (R.x, [1; 0; 3]); %! assert_equal (R.y, {'a'; 'z'; 'c'}); %!test # a vector gives one fill value per variable %! T = table ([1; NaN], [NaN; 4], 'VariableNames', {'a', 'b'}); %! R = fillmissing (T, 'constant', [7, 9]); %! assert_equal (R.a, [1; 7]); %! assert_equal (R.b, [9; 4]); %!test # 'constant' fills a string variable %! s = string ({'a'; 'b'; 'c'}); %! s(2) = string (missing); %! R = fillmissing (table (s, 'VariableNames', {'s'}), 'constant', ... %! string ('Z')); %! assert_equal (cellstr (R.s), {'a'; 'Z'; 'c'}); %!test # 'constant' fills a categorical variable %! c = categorical ({'p'; ''; 'r'}); %! R = fillmissing (table (c, 'VariableNames', {'c'}), 'constant', ... %! categorical ({'q'})); %! assert_equal (cellstr (R.c), {'p'; 'q'; 'r'}); %!test # 'constant' fills a duration variable %! d = hours ([1; NaN; 3]); %! R = fillmissing (table (d, 'VariableNames', {'d'}), 'constant', hours (9)); %! assert_equal (hours (R.d), [1; 9; 3]); %!test # 'constant' fills a datetime variable %! t = datetime (2020, 1, [1; 1; 3]); %! t(2) = NaT; %! R = fillmissing (table (t, 'VariableNames', {'t'}), 'constant', ... %! datetime (2020, 1, 2)); %! assert_equal (datevec (R.t)(:,3), [1; 2; 3]); %!test # 'DataVariables' restricts the fill to a subset of variables %! T = table ([1; NaN], [NaN; 4], 'VariableNames', {'a', 'b'}); %! [R, TF] = fillmissing (T, 'constant', 0, 'DataVariables', 'a'); %! assert_equal (R.a, [1; 0]); %! assert_equal (R.b, [NaN; 4]); %! assert_equal (TF, [false, false; true, false]); ## Test 'fillmissing' method -- 'previous'/'next'/'nearest' methods %!test # 'previous' carries the last non-missing value forward %! T = table ([1; NaN; NaN; 4], 'VariableNames', {'x'}); %! [R, TF] = fillmissing (T, 'previous'); %! assert_equal (R.x, [1; 1; 1; 4]); %! assert_equal (TF, [false; true; true; false]); %!test # 'next' carries the next non-missing value backward %! T = table ([1; NaN; NaN; 4], 'VariableNames', {'x'}); %! R = fillmissing (T, 'next'); %! assert_equal (R.x, [1; 4; 4; 4]); %!test # 'nearest' uses the closest value; ties favor the next one %! T = table ([1; NaN; NaN; NaN; 5], 'VariableNames', {'x'}); %! R = fillmissing (T, 'nearest'); %! assert_equal (R.x, [1; 1; 5; 5; 5]); %!test # leading missing entries are left missing by 'previous' %! T = table ([NaN; NaN; 3], 'VariableNames', {'x'}); %! [R, TF] = fillmissing (T, 'previous'); %! assert_equal (R.x, [NaN; NaN; 3]); %! assert_equal (TF, [false; false; false]); %!test # the neighbor methods work on a string variable %! s = string ({'a'; 'b'; 'c'; 'd'}); %! s(2) = string (missing); %! s(3) = string (missing); %! R = fillmissing (table (s, 'VariableNames', {'s'}), 'previous'); %! assert_equal (cellstr (R.s), {'a'; 'a'; 'a'; 'd'}); %!test # the neighbor methods work on a categorical variable %! c = categorical ({'p'; ''; ''; 's'}); %! R = fillmissing (table (c, 'VariableNames', {'c'}), 'next'); %! assert_equal (cellstr (R.c), {'p'; 's'; 's'; 's'}); %!test # the neighbor methods work on a datetime variable %! t = datetime (2020, 1, [1; 1; 4]); %! t(2) = NaT; %! R = fillmissing (table (t, 'VariableNames', {'t'}), 'next'); %! assert_equal (datevec (R.t)(:,3), [1; 4; 4]); %!test # a multicolumn variable is filled column by column %! T = table ([1, 10; NaN, NaN; 3, 30]); %! [R, TF] = fillmissing (T, 'previous'); %! assert_equal (R.Var1, [1, 10; 1, 10; 3, 30]); %! assert_equal (TF, [false; true; false]); %!test # the method name is case-insensitive %! T = table ([1; NaN; 3], 'VariableNames', {'x'}); %! R = fillmissing (T, 'Previous'); %! assert_equal (R.x, [1; 1; 3]); ## Test 'fillmissing' method -- 'linear' method %!test # interior gaps are linearly interpolated %! T = table ([1; NaN; NaN; 4], 'VariableNames', {'x'}); %! [R, TF] = fillmissing (T, 'linear'); %! assert_equal (R.x, [1; 2; 3; 4]); %! assert_equal (TF, [false; true; true; false]); %!test # 'EndValues' defaults to 'extrap' (linear extrapolation) %! T = table ([NaN; 2; 4; NaN], 'VariableNames', {'x'}); %! R = fillmissing (T, 'linear'); %! assert_equal (R.x, [0; 2; 4; 6]); %!test # 'EndValues' 'none' leaves leading and trailing gaps missing %! T = table ([NaN; 2; 4; NaN], 'VariableNames', {'x'}); %! R = fillmissing (T, 'linear', 'EndValues', 'none'); %! assert_equal (R.x, [NaN; 2; 4; NaN]); %!test # 'EndValues' accepts a numeric constant for the end gaps %! T = table ([NaN; 2; 4; NaN], 'VariableNames', {'x'}); %! R = fillmissing (T, 'linear', 'EndValues', 0); %! assert_equal (R.x, [0; 2; 4; 0]); %!test # 'EndValues' 'previous' holds the last known value, leaving the head %! T = table ([NaN; 2; 3; NaN], 'VariableNames', {'x'}); %! R = fillmissing (T, 'linear', 'EndValues', 'previous'); %! assert_equal (R.x, [NaN; 2; 3; 3]); %!test # 'EndValues' 'next' holds the first known value, leaving the tail %! T = table ([NaN; 2; 3; NaN], 'VariableNames', {'x'}); %! R = fillmissing (T, 'linear', 'EndValues', 'next'); %! assert_equal (R.x, [2; 2; 3; NaN]); %!test # 'EndValues' 'nearest' fills both ends from the nearest known value %! T = table ([NaN; NaN; 3; 4; NaN; NaN], 'VariableNames', {'x'}); %! R = fillmissing (T, 'linear', 'EndValues', 'nearest'); %! assert_equal (R.x, [3; 3; 3; 4; 4; 4]); %!test # 'EndValues' governs the end gaps whatever the fill method is %! T = table ([NaN; 2; 3; NaN], 'VariableNames', {'x'}); %! R = fillmissing (T, 'previous', 'EndValues', 'next'); %! assert_equal (R.x, [2; 2; 3; NaN]); %!test # 'extrap' leaves the ends to the method, not to linear extrapolation %! T = table ([NaN; 2; 3; NaN], 'VariableNames', {'x'}); %! R = fillmissing (T, 'previous', 'EndValues', 'extrap'); %! assert_equal (R.x, [NaN; 2; 3; 3]); %!test # a constant overrides the ends under the 'constant' method too %! T = table ([NaN; 2; 3; NaN], 'VariableNames', {'x'}); %! R = fillmissing (T, 'constant', 0, 'EndValues', 9); %! assert_equal (R.x, [9; 2; 3; 9]); %!test # one known value is enough for a keyword %! T = table ([NaN; 5; NaN], 'VariableNames', {'x'}); %! R = fillmissing (T, 'linear', 'EndValues', 'nearest'); %! assert_equal (R.x, [5; 5; 5]); %!test # a constant fills a column with no known value at all %! T = table ([NaN; NaN; NaN], 'VariableNames', {'x'}); %! R = fillmissing (T, 'linear', 'EndValues', 9); %! assert_equal (R.x, [9; 9; 9]); %!test # a keyword has nothing to take when nothing is known %! T = table ([NaN; NaN; NaN], 'VariableNames', {'x'}); %! R = fillmissing (T, 'linear', 'EndValues', 'nearest'); %! assert_equal (R.x, [NaN; NaN; NaN]); %!test # 'EndValues' reaches a datetime variable, which 'linear' never did %! d = datetime (2024, 1, [1;2;3;4]); %! d([1 4]) = NaT; %! R = fillmissing (table (d), 'previous', 'EndValues', datetime (2000, 1, 1)); %! assert_equal (year (R.d), [2000; 2024; 2024; 2000]); %!test # 'linear' interpolates a datetime variable %! d = datetime (2024, 1, [1;2;3]); %! d(2) = NaT; %! R = fillmissing (table (d), 'linear'); %! assert_equal (day (R.d), [1; 2; 3]); %!test # 'linear' extrapolates a datetime variable past both ends %! d = datetime (2024, 1, [1;2;3;4]); %! d([1 4]) = NaT; %! R = fillmissing (table (d), 'linear'); %! assert_equal (day (R.d), [1; 2; 3; 4]); %!test # 'EndValues' governs a datetime variable's end gaps %! d = datetime (2024, 1, [1;2;3;4]); %! d([1 4]) = NaT; %! R = fillmissing (table (d), 'linear', 'EndValues', 'none'); %! assert_equal (isnat (R.d), [true; false; false; true]); %!test # 'linear' interpolates a duration variable and keeps its format %! u = hours ([1;NaN;3]); %! R = fillmissing (table (u), 'linear'); %! assert_equal (hours (R.u), [1; 2; 3]); %!test # a variable 'linear' cannot interpolate is untouched when not targeted %! T = table ([1; NaN; 3], {'a'; ''; 'c'}, 'VariableNames', {'x', 'y'}); %! R = fillmissing (T, 'linear', 'DataVariables', 'x'); %! assert_equal (R.x, [1; 2; 3]); %! assert_equal (R.y, {'a'; ''; 'c'}); %!test # fewer than two anchors cannot be interpolated %! T = table ([NaN; 5; NaN], 'VariableNames', {'x'}); %! R = fillmissing (T, 'linear', 'EndValues', 'none'); %! assert_equal (R.x, [NaN; 5; NaN]); %!test # a table without missing values is returned unchanged %! T = table ([1; 2; 3], 'VariableNames', {'x'}); %! [R, TF] = fillmissing (T, 'previous'); %! assert_equal (isequal (R, T), true); %! assert_equal (TF, [false; false; false]); ## Test an option that is not understood is reported, not ignored %!test %! T = table ([10; NaN; 30], 'VariableNames', {'A'}); %! fail = 0; %! try %! fillmissing (T, 'linear', 'EndValuess', 'extrap'); %! catch %! fail = 1; %! end_try_catch %! assert_equal (fail, 1); ## Test 'fillmissing' interpolates with the methods that were refused before %!test %! T = table ([10; NaN; 30], 'VariableNames', {'A'}); %! assert_equal (fillmissing (T, 'linear').A, [10; 20; 30]); %! assert_equal (fillmissing (T, 'spline').A, [10; 20; 30]); %!test # a variable with nothing missing to fill is left as it is %! T = table (struct ('a', {1; 2}), [NaN; 5], 'VariableNames', {'s', 'd'}); %! F = fillmissing (T, 'next'); %! assert_equal (F.s, T.s); %! assert_equal (F.d, [5; 5]); ## Test input validation for 'fillmissing' method %!error ... %! fillmissing (table (1)); %!error ... %! fillmissing (table (1), 5); %!error ... %! fillmissing (table (1), 'bogus'); %!error ... %! fillmissing (table (1), 'movmean', 3); %!error ... %! fillmissing (table (1), 'constant'); %!error ... %! fillmissing (table ([1; NaN], 'VariableNames', {'x'}), 'constant', {1, 2}); %!error ... %! fillmissing (table ([1; NaN], 'VariableNames', {'x'}), 'constant', [1, 2, ... %! 3]); %!error ... %! fillmissing (table ([1; NaN], {'a'; ''}, 'VariableNames', {'x', 'y'}), ... %! 'constant', 0); %!error ... %! fillmissing (table ([1; NaN], 'VariableNames', {'x'}), 'previous', ... %! 'DataVariables', 'zzz'); %!error ... %! fillmissing (table ([NaN; 2; 3], 'VariableNames', {'x'}), 'linear', ... %! 'EndValues', 'bogus'); ## the option is checked at the parse, so data without end gaps refuses it too %!error ... %! fillmissing (table ([1; 2; 3], 'VariableNames', {'x'}), 'linear', ... %! 'EndValues', 'bogus'); %!error ... %! fillmissing (table ([1; 2; 3], 'VariableNames', {'x'}), 'linear', ... %! 'EndValues', [1 2]); %!error ... %! fillmissing (table ([NaN; 2; 3], 'VariableNames', {'x'}), 'linear', ... %! 'EndValues', {1, 2}); ## 'linear' refuses a targeted variable whose type it cannot interpolate %!error ... %! T = table ([1; NaN; 3], {'a'; ''; 'c'}, 'VariableNames', {'x', 'y'}); %! fillmissing (T, 'linear'); %!error ... %! c = categorical ({'a'; 'b'; 'c'}); %! c(2) = missing; %! fillmissing (table ([1; NaN; 3], c, 'VariableNames', {'x', 'c'}), 'linear'); ## the type is checked before anything is filled, missing entries or not %!error ... %! T = table ([1; NaN; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); %! fillmissing (T, 'linear'); ## the constant must be assignable to the variable it fills %!error ... %! d = datetime (2024, 1, [1;2;3;4]); %! d([1 4]) = NaT; %! fillmissing (table (d), 'previous', 'EndValues', 9); %!error ... %! fillmissing (table (1), 'previous', 'ReplaceValues', false); %!error ... %! fillmissing (table ([1; NaN; 3]), 'previous', 'SamplePoints', (1:3)'); %!error ... %! fillmissing (table (1), 'previous', 5); %!error ... %! fillmissing (table (1), 'previous', 'Bogus', 1); ## Test 'standardizeMissing' method %!test # a numeric scalar indicator becomes NaN in numeric variables %! T = table ([1; 99; 3], 'VariableNames', {'x'}); %! R = standardizeMissing (T, 99); %! assert_equal (R.x, [1; NaN; 3]); %!test # a numeric vector indicator matches any of its values %! T = table ([1; -99; 3; 0], 'VariableNames', {'x'}); %! R = standardizeMissing (T, [-99, 0]); %! assert_equal (R.x, [1; NaN; 3; NaN]); %!test # a text indicator standardizes a cellstr variable to '' %! T = table ({'a'; 'NA'; 'c'}, 'VariableNames', {'y'}); %! R = standardizeMissing (T, 'NA'); %! assert_equal (R.y, {'a'; ''; 'c'}); %! assert_equal (cellfun (@isempty, R.y), [false; true; false]); %!test # a text indicator standardizes a string variable to %! T = table (string ({'a'; 'NA'; 'c'}), 'VariableNames', {'s'}); %! R = standardizeMissing (T, 'NA'); %! assert_equal (ismissing (R.s), [false; true; false]); %!test # a text indicator standardizes a categorical variable to %! T = table (categorical ({'a'; 'NA'; 'c'}), 'VariableNames', {'c'}); %! R = standardizeMissing (T, {'NA'}); %! assert_equal (ismissing (R.c), [false; true; false]); %!test # a cell indicator mixes numeric and text across a mixed table %! T = table ([1; 5; 99], {'a'; 'x'; 'c'}, 'VariableNames', {'n', 't'}); %! R = standardizeMissing (T, {99, 'x'}); %! assert_equal (R.n, [1; 5; NaN]); %! assert_equal (R.t, {'a'; ''; 'c'}); %!test # numeric and text indicators only touch compatible variables %! T = table ([1; 5; 99], {'a'; '99'; 'c'}, 'VariableNames', {'n', 't'}); %! R = standardizeMissing (T, 99); %! assert_equal (R.n, [1; 5; NaN]); %! assert_equal (R.t, {'a'; '99'; 'c'}); %!test # 'DataVariables' restricts the standardization to a subset %! T = table ([1; 99], [99; 2], 'VariableNames', {'a', 'b'}); %! R = standardizeMissing (T, 99, 'DataVariables', 'a'); %! assert_equal (R.a, [1; NaN]); %! assert_equal (R.b, [99; 2]); %!test # integer variables have no standard missing value; pass through %! T = table (int32 ([1; 99; 3]), 'VariableNames', {'x'}); %! R = standardizeMissing (T, 99); %! assert_equal (R.x, int32 ([1; 99; 3])); %!test # a duration indicator makes a duration NaN; numbers pass it by %! T = table (hours ([1; -99]), [1; -99], 'VariableNames', {'D', 'N'}); %! R = standardizeMissing (T, hours (-99)); %! assert_equal (hours (R.D), [1; NaN]); %! assert_equal (R.N, [1; -99]); %! R = standardizeMissing (T, -99); %! assert_equal (R.D, hours ([1; -99])); %! assert_equal (R.N, [1; NaN]); %! R = standardizeMissing (T, {hours(-99), -99}); %! assert_equal (hours (R.D), [1; NaN]); %! assert_equal (R.N, [1; NaN]); %!test # a duration vector indicator matches any of its values %! T = table (hours ([1; -99; -98]), string ({'a'; 'b'; 'c'}), ... %! 'VariableNames', {'D', 'S'}); %! R = standardizeMissing (T, hours ([-99, -98])); %! assert_equal (hours (R.D), [1; NaN; NaN]); %! assert_equal (R.S, string ({'a'; 'b'; 'c'})); %!test # a duration indicator leaves calendarDuration variables alone %! T = table (calmonths ([1; 2]), hours ([1; 2]), 'VariableNames', {'C', 'D'}); %! R = standardizeMissing (T, hours (2)); %! assert_equal (R.C, calmonths ([1; 2])); %! assert_equal (hours (R.D), [1; NaN]); %!test # a datetime indicator makes a datetime NaT; durations pass it by %! T = table (datetime (2020, 1, [1; 2]), [1; 2], 'VariableNames', {'D', 'N'}); %! R = standardizeMissing (T, datetime (2020, 1, 2)); %! assert_equal (isnat (R.D), [false; true]); %! assert_equal (R.N, [1; 2]); %! T = table (hours ([1; 2]), [1; 2], 'VariableNames', {'D', 'N'}); %! R = standardizeMissing (T, {hours(2), datetime(2020, 1, 2)}); %! assert_equal (hours (R.D), [1; NaN]); %!test # a datetime indicator matches the same instant in another zone %! D = datetime (2020, 1, [1; 2], 'TimeZone', 'UTC'); %! T = table (D, [1; 2], 'VariableNames', {'D', 'N'}); %! R = standardizeMissing (T, datetime (2020, 1, 2, 'TimeZone', ... %! 'Europe/Athens')); %! assert_equal (isnat (R.D), [false; false]); %! R = standardizeMissing (T, datetime (2020, 1, 2, 2, 0, 0, 'TimeZone', ... %! 'Europe/Athens')); %! assert_equal (isnat (R.D), [false; true]); ## Test input validation for 'standardizeMissing' method %!error ... %! standardizeMissing (table (1)); %!error ... %! standardizeMissing (table (1), 1, 'DataVariables'); %!error ... %! standardizeMissing (table (1), struct ('a', 1)); %!error ... %! standardizeMissing (table (1), {struct('a', 1)}); %!error ... %! standardizeMissing (table (1), calmonths (1)); %!error ... %! standardizeMissing (table (1), {datetime(2020, 1, 2), ... %! datetime(2020, 1, 3, 'TimeZone', 'UTC')}); %!error ... %! standardizeMissing (table (datetime (2020, 1, [1; 2], 'TimeZone', 'UTC'), ... %! 'VariableNames', {'D'}), datetime (2020, 1, 2)); %!error ... %! standardizeMissing (table ([1; 99], 'VariableNames', {'x'}), 99, ... %! 'DataVariables', 'zzz'); %!error ... %! standardizeMissing (table (1), 1, 'Bogus', 1); ################################################################################ ## ** Apply Functions to Table Contents ** ## ################################################################################ ## Available Methods ## ## ## ## 'pivot' 'groupcounts' 'groupfilter' 'groupsummary' ## ## 'grouptransform' 'findgroups' 'splitapply' 'rowfun' ## ## 'varfun' ## ## ## ################################################################################ ## 'findgroups' labels rows by sorted unique value; group k is the k-th value %!assert_equal (findgroups (table ([1; 3; 1; 2])), [1; 3; 1; 2]) %!assert_equal (findgroups (table ([30; 10; 10; 20])), [3; 1; 1; 2]) %!assert_equal (findgroups (table ({'b'; 'a'; 'b'; 'c'})), [2; 1; 2; 3]) %!assert_equal (findgroups (table (logical ([1; 0; 1; 0]))), [2; 1; 2; 1]) %!test %! ## Missing values map to NaN in G (numeric, text, datetime) %! assert_equal (findgroups (table ([1; NaN; 2; 1])), [1; NaN; 2; 1]); %! assert_equal (findgroups (table ({'b'; ''; 'a'})), [2; NaN; 1]); %! assert_equal (findgroups (table ([datetime(2026,1,2); NaT; ... %! datetime(2026,1,1)])), [2; NaN; 1]); %!test %! ## Multiple variables group by unique sorted combinations %! T = table ([1; 1; 2; 1], {'b'; 'a'; 'a'; 'b'}); %! assert_equal (findgroups (T), [2; 1; 3; 2]); %!test %! ## Second output is the sorted-unique identifier table %! [G, TID] = findgroups (table ([30; 10; 10; 20])); %! assert_equal (G, [3; 1; 1; 2]); %! assert_equal (TID.Var1, [10; 20; 30]); %! assert_equal (TID.Properties.VariableNames, {'Var1'}); %!error findgroups (table ()) ## 'splitapply' splits rows by group number and concatenates per-group results %!assert_equal (splitapply (@(x) mean (x), table ([1; 2; 3; 4]), ... %! [1; 1; 2; 2]), ... %! [1.5; 3.5]) %!assert_equal (splitapply (@(x) sum (x), table ([1; 2; 3]), [2; 1; 2]), [2; 4]) %!test %! ## Each table variable is passed to FUNC as a separate argument %! T = table ([1; 2; 3; 4], [10; 20; 30; 40]); %! y = splitapply (@(a, b) sum (a) + sum (b), T, [1; 1; 2; 2]); %! assert_equal (y, [33; 77]); %!test %! ## Multiple outputs are concatenated separately %! [lo, hi] = splitapply (@(x) deal (min (x), max (x)), ... %! table ([4; 1; 3; 2]), [1; 1; 2; 2]); %! assert_equal (lo, [1; 2]); %! assert_equal (hi, [4; 3]); %!test %! ## NaN group numbers omit the corresponding rows %! y = splitapply (@(x) mean (x), table ([1; 2; 99; 4]), [1; 1; NaN; 2]); %! assert_equal (y, [1.5; 4]); %!error splitapply (@mean, table ([1; 2]), [1; 1; 1]) %!error splitapply (@mean, table ([1; 2]), [1; 3]) %!error splitapply (1, table ([1; 2]), [1; 2]) ## 'varfun' applies FUNC to each variable; output names are FUNC_VARNAME %!test %! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'}); %! B = varfun (@sum, T); %! assert_equal (B.Properties.VariableNames, {'sum_a', 'sum_b'}); %! assert_equal (B.sum_a, 6); %! assert_equal (B.sum_b, 60); %!test %! ## Anonymous functions are named with the 'Fun' prefix %! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'}); %! B = varfun (@(x) max (x), T); %! assert_equal (B.Properties.VariableNames, {'Fun_a', 'Fun_b'}); %! assert_equal (B.Fun_b, 30); %!test %! ## 'OutputFormat' 'uniform' concatenates scalar results into an array %! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'}); %! assert_equal (varfun (@mean, T, 'OutputFormat', 'uniform'), [2, 20]); %!test %! ## 'OutputFormat' 'cell' returns the results in a cell array %! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'}); %! assert_equal (varfun (@sum, T, 'OutputFormat', 'cell'), {6, 60}); %!test %! ## 'InputVariables' restricts the variables passed to FUNC %! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'}); %! B = varfun (@sum, T, 'InputVariables', 'a'); %! assert_equal (B.Properties.VariableNames, {'sum_a'}); %! assert_equal (B.sum_a, 6); %!test %! ## 'GroupingVariables' add grouping columns and a GroupCount column %! T = table ([1; 1; 2; 2], [10; 20; 30; 40], 'VariableNames', {'g', 'x'}); %! B = varfun (@sum, T, 'GroupingVariables', 'g'); %! assert_equal (B.Properties.VariableNames, {'g', 'GroupCount', 'sum_x'}); %! assert_equal (B.g, [1; 2]); %! assert_equal (B.GroupCount, [2; 2]); %! assert_equal (B.sum_x, [30; 70]); %!test %! ## Grouped 'uniform' output holds only the per-group results %! T = table ([1; 1; 2; 2], [10; 20; 30; 40], 'VariableNames', {'g', 'x'}); %! v = varfun (@sum, T, 'GroupingVariables', 'g', 'OutputFormat', 'uniform'); %! assert_equal (v, [30; 70]); %!test %! ## A grouped FUNC may return several rows per group; the grouping and %! ## GroupCount columns are replicated to match the stacked results %! T = table ([1; 1; 2; 2], [10; 20; 30; 40], 'VariableNames', {'g', 'x'}); %! B = varfun (@(x) x * 2, T, 'GroupingVariables', 'g'); %! assert_equal (B.Properties.VariableNames, {'g', 'GroupCount', 'Fun_x'}); %! assert_equal (B.g, [1; 1; 2; 2]); %! assert_equal (B.GroupCount, [2; 2; 2; 2]); %! assert_equal (B.Fun_x, [20; 40; 60; 80]); %!test %! ## Grouped 'cell' output holds one cell per group with the full result %! T = table ([1; 1; 2; 2], [10; 20; 30; 40], 'VariableNames', {'g', 'x'}); %! C = varfun (@(x) x * 2, T, 'GroupingVariables', 'g', 'OutputFormat', 'cell'); %! assert_equal (size (C), [2, 1]); %! assert_equal (C{1}, [20; 40]); %! assert_equal (C{2}, [60; 80]); %!test # a nested table variable passes its rows to each group %! N = table ([1; 2; 3], 'VariableNames', {'p'}); %! T = table (N, [1; 1; 2], [5; 6; 7], 'VariableNames', {'A', 'G', 'V'}); %! R = varfun (@(x) height (x), T, 'GroupingVariables', 'G', ... %! 'InputVariables', 'A'); %! assert_equal (R{:,3}, [2; 1]); %!error ... %! T = table ([1, 2; 1, 2; 3, 4], [5; 6; 7], 'VariableNames', {'A', 'V'}); %! varfun (@sum, T, 'GroupingVariables', 'A'); %!error varfun (1, table ([1; 2])) %!error varfun (table ([1; 2])) %!error ... %! varfun (@sum, table ([1; 2]), 'OutputFormat') %!error ... %! varfun (@sum, table ([1; 2]), 'OutputFormat', 'bogus') %!error ... %! varfun (@sum, table ([1; 2], 'VariableNames', {'g'}), ... %! 'GroupingVariables', 'g') %!error ... %! varfun (@(x) x, table ([1; 2; 3]), 'OutputFormat', 'uniform') ## a failure names the function and the variable it was applied to ## a table does not answer to the other class's name as an 'OutputFormat' %!error ... %! varfun (@mean, table ([1; 2]), 'OutputFormat', 'timetable') %!test # a nested table variable passes its rows %! N = table ([1; 2; 3], 'VariableNames', {'p'}); %! T = table (N, [1; 1; 2], [5; 6; 7], 'VariableNames', {'A', 'G', 'V'}); %! R = rowfun (@(a, g, v) height (a) + v, T); %! assert_equal (R.Var1, [6; 7; 8]); %!error ... %! T = table ([1, 2; 1, 2; 3, 4], [5; 6; 7], 'VariableNames', {'A', 'V'}); %! rowfun (@(v) v, T, 'GroupingVariables', 'A', ... %! 'InputVariables', 'V'); %!error ... %! rowfun (@(a) a, table ([1; 2]), 'OutputFormat', 'timetable') ## a right table may hold several rows per key, and each one is matched %!test %! L = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'Key', 'Lv'}); %! R = table ([1; 1; 2], {'p'; 'q'; 'r'}, 'VariableNames', {'Key', 'Rv'}); %! C = innerjoin (L, R); %! assert_equal (C.Key, [1; 1; 2]); %! assert_equal (C.Lv, {'a'; 'a'; 'b'}); %! assert_equal (C.Rv, {'p'; 'q'; 'r'}); %!test %! L = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'Key', 'Lv'}); %! R = table ([1; 1; 2], {'p'; 'q'; 'r'}, 'VariableNames', {'Key', 'Rv'}); %! C = outerjoin (L, R); %! assert_equal (size (C), [3, 4]); %! assert_equal (C.Rv, {'p'; 'q'; 'r'}); ## a bin count picks the edges 'discretize' picks, not the data range split ## evenly %!test %! T = table ((1:6)', (1:6)', 'VariableNames', {'A', 'B'}); %! G = groupsummary (T, 'A', 2, 'sum', 'B'); %! assert_equal (categories (G.disc_A), {'[0, 3)'; '[3, 6]'}); %! assert_equal (G.GroupCount, [2; 4]); %!test %! T = table ((1:6)', (1:6)', 'VariableNames', {'A', 'B'}); %! G = groupsummary (T, 'A', 3, 'sum', 'B'); %! assert_equal (categories (G.disc_A), ... %! {'[1, 2.7)'; '[2.7, 4.4)'; '[4.4, 6.1]'}); ## and the same rule reaches the other methods that bin %!test %! T = table ((1:6)', 'VariableNames', {'A'}); %! G = groupcounts (T, 'A', 2); %! assert_equal (categories (G.disc_A), {'[0, 3)'; '[3, 6]'}); ## a large edge reads as an exponent, at six significant digits %!test %! T = table ((1:6)' * 1e6, (1:6)', 'VariableNames', {'A', 'B'}); %! G = groupsummary (T, 'A', 2, 'sum', 'B'); %! assert_equal (categories (G.disc_A), {'[0, 3e+06)'; '[3e+06, 6e+06]'}); ## 'all' names every method, in the order they are reported %!test %! T = table (categorical ({'a'; 'a'; 'b'}), [1; 2; 3], ... %! 'VariableNames', {'g', 'A'}); %! G = groupsummary (T, 'g', 'all', 'A'); %! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount', 'mean_A', ... %! 'sum_A', 'min_A', 'max_A', 'range_A', 'median_A', ... %! 'mode_A', 'var_A', 'std_A', 'nummissing_A', 'nnz_A', ... %! 'numunique_A'}); ## a method name may be abbreviated to any unambiguous prefix %!test %! T = table (categorical ({'a'; 'a'; 'b'}), [1; 2; 3], ... %! 'VariableNames', {'g', 'A'}); %! assert_equal (groupsummary (T, 'g', 'nummiss', ... %! 'A').Properties.VariableNames, ... %! {'g', 'GroupCount', 'nummissing_A'}); %! assert_equal (groupsummary (T, 'g', 'MEDI', ... %! 'A').Properties.VariableNames, ... %! {'g', 'GroupCount', 'median_A'}); ## an ambiguous abbreviation is refused %!test # a nested table variable passes its rows to a function %! N = table ([1; 2; 3], 'VariableNames', {'p'}); %! T = table (N, [1; 1; 2], [5; 6; 7], 'VariableNames', {'A', 'G', 'V'}); %! S = groupsummary (T, 'G', @(x) height (x), 'A'); %! assert_equal (S{:,3}, [2; 1]); %!error ... %! T = table (table ([1; 2]), [1; 2], 'VariableNames', {'N', 'K'}); %! groupsummary (T, 'N'); %!error ... %! T = table ([1, 2; 1, 2; 3, 4], [5; 6; 7], 'VariableNames', {'A', 'V'}); %! groupsummary (T, 'A'); %!error ... %! groupsummary (table (categorical ({'a'}), 1, 'VariableNames', {'g', ... %! 'A'}), ... %! 'g', 'm', 'A') ## 'std' and 'var' are not defined on an integer variable %!error ... %! groupsummary (table (categorical ({'a'; 'a'}), int8 ([1; 2]), ... %! 'VariableNames', {'g', 'A'}), 'g', 'std', 'A') %!error ... %! groupsummary (table (categorical ({'a'; 'a'}), int16 ([1; 2]), ... %! 'VariableNames', {'g', 'A'}), 'g', 'var', 'A') %!error ... %! varfun (@mean, table ([1; 2], string ({'p'; 'q'}), 'VariableNames', ... %! {'num', 'str'})) ## 'rowfun' applies FUNC to each row; default output names are Var1, Var2, ... %!test %! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'}); %! B = rowfun (@(x, y) x + y, T); %! assert_equal (B.Properties.VariableNames, {'Var1'}); %! assert_equal (B.Var1, [11; 22; 33]); %!test %! ## Multiple outputs with 'OutputVariableNames' %! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'}); %! B = rowfun (@(x, y) deal (x + y, x * y), T, ... %! 'OutputVariableNames', {'add', 'mul'}); %! assert_equal (B.Properties.VariableNames, {'add', 'mul'}); %! assert_equal (B.add, [11; 22; 33]); %! assert_equal (B.mul, [10; 40; 90]); %!test %! ## 'SeparateInputs' false passes the row as a single concatenated argument %! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'}); %! B = rowfun (@(row) sum (row), T, 'SeparateInputs', false); %! assert_equal (B.Var1, [11; 22; 33]); %!test %! ## The row joins as 'table2array' joins the variables %! T = table ([1; 2], hours ([1; 2])); %! v = rowfun (@(x) {x}, T, 'SeparateInputs', false, 'OutputFormat', 'cell'); %! assert_equal (v{2}, {hours([48, 2])}); %!test %! T = table ([1; 2], string ({'x'; 'y'})); %! v = rowfun (@(x) {x}, T, 'SeparateInputs', false, 'OutputFormat', 'cell'); %! assert_equal (v{2}, {string({'2', 'y'})}); %!test %! T = table (int8 ([1; 2]), [3.7; 4]); %! v = rowfun (@(x) {x}, T, 'SeparateInputs', false, 'OutputFormat', 'cell'); %! assert_equal (v{1}, {int8([1, 4])}); %!test %! T = table ([1; 2], logical ([1; 0]), string ({'x'; 'y'})); %! v = rowfun (@(x) {x}, T, 'SeparateInputs', false, 'OutputFormat', 'cell'); %! assert_equal (v{1}, {string({'1', 'true', 'x'})}); %!test %! ## 'InputVariables' restricts and orders the variables passed to FUNC %! T = table ([1; 2; 3], [10; 20; 30], [100; 200; 300], ... %! 'VariableNames', {'a', 'b', 'c'}); %! B = rowfun (@(x, y) x + y, T, 'InputVariables', {'a', 'c'}); %! assert_equal (B.Var1, [101; 202; 303]); %!test %! ## 'ExtractCellContents' unwraps cell-valued variables before the call %! T = table ({1; 2; 3}, 'VariableNames', {'a'}); %! B = rowfun (@(x) x * 2, T, 'ExtractCellContents', true); %! assert_equal (B.Var1, [2; 4; 6]); %!test %! ## 'OutputFormat' 'uniform' and 'cell' %! T = table ([1; 2; 3], [10; 20; 30], 'VariableNames', {'a', 'b'}); %! assert_equal (rowfun (@(x, y) x + y, T, 'OutputFormat', 'uniform'), ... %! [11; 22; 33]); %! assert_equal (rowfun (@(x, y) x + y, T, 'OutputFormat', 'cell'), ... %! {11; 22; 33}); %!test %! ## Ungrouped output preserves the row names of the input table %! T = table ([1; 2], [10; 20], 'VariableNames', {'a', 'b'}, ... %! 'RowNames', {'r1', 'r2'}); %! B = rowfun (@(x, y) x + y, T); %! assert_equal (B.Properties.RowNames, T.Properties.RowNames); %!test %! ## 'GroupingVariables' apply FUNC per group with a GroupCount column; the %! ## default output name continues past the grouping and GroupCount columns %! T = table ([1; 1; 2; 2], [10; 20; 30; 40], 'VariableNames', {'g', 'x'}); %! B = rowfun (@(x) sum (x), T, 'GroupingVariables', 'g'); %! assert_equal (B.Properties.VariableNames, {'g', 'GroupCount', 'Var3'}); %! assert_equal (B.g, [1; 2]); %! assert_equal (B.GroupCount, [2; 2]); %! assert_equal (B.Var3, [30; 70]); %!test %! ## A grouped FUNC may return several rows per group; the grouping and %! ## GroupCount columns are replicated to match the stacked results %! T = table ([1; 1; 2; 2], [10; 20; 30; 40], [5; 6; 7; 8], ... %! 'VariableNames', {'g', 'x', 'y'}); %! B = rowfun (@(x, y) x + y, T, 'GroupingVariables', 'g'); %! assert_equal (B.Properties.VariableNames, {'g', 'GroupCount', 'Var3'}); %! assert_equal (B.g, [1; 1; 2; 2]); %! assert_equal (B.GroupCount, [2; 2; 2; 2]); %! assert_equal (B.Var3, [15; 26; 37; 48]); %!error rowfun (1, table ([1; 2])) %!error rowfun (table ([1; 2])) %!error ... %! rowfun (@(x) x, table ([1; 2]), 'OutputFormat') %!error ... %! rowfun (@(x) x, table ([1; 2]), 'SeparateInputs', 'yes') %!error ... %! rowfun (@(x) x, table ([1; 2], {'a'; 'b'}), 'SeparateInputs', false) %!error ... %! rowfun (@(x) x, table (true, 'a'), 'SeparateInputs', false) %!error ... %! rowfun (@(x) x, table (categorical ({'a'}), 1), 'SeparateInputs', false) %!error ... %! rowfun (@(x) x, table (1, true, 'a'), 'SeparateInputs', false) %!error ... %! rowfun (@(x) x, table ([1; 2]), 'OutputVariableNames', {'p', 'q'}, ... %! 'NumOutputs', 1) ## 'groupsummary' with no method returns the grouping variables and GroupCount %!test %! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], 'VariableNames', {'g', ... %! 'x'}); %! G = groupsummary (T, 'g'); %! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount'}); %! assert_equal (G.g, [1; 2]); %! assert_equal (G.GroupCount, [3; 2]); ## A named method appends a '_' column for each data variable %!test %! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], [1; 2; 3; 4; 5], ... %! 'VariableNames', {'g', 'x', 'y'}); %! G = groupsummary (T, 'g', 'sum'); %! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount', 'sum_x', ... %! 'sum_y'}); %! assert_equal (G.GroupCount, [3; 2]); %! assert_equal (G.sum_x, [80; 70]); %! assert_equal (G.sum_y, [8; 7]); ## 'DataVars' restricts the variables the method is applied to %!test %! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], [1; 2; 3; 4; 5], ... %! 'VariableNames', {'g', 'x', 'y'}); %! G = groupsummary (T, 'g', 'mean', 'x'); %! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount', 'mean_x'}); %! assert_equal (G.mean_x, [80/3; 35]); ## Several methods and data variables are ordered data variable first, then ## method %!test %! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], [1; 2; 3; 4; 5], ... %! 'VariableNames', {'g', 'x', 'y'}); %! G = groupsummary (T, 'g', {'mean', 'sum'}, {'x', 'y'}); %! assert_equal (G.Properties.VariableNames, ... %! {'g', 'GroupCount', 'mean_x', 'sum_x', 'mean_y', 'sum_y'}); %! assert_equal (G.mean_x, [80/3; 35]); %! assert_equal (G.sum_x, [80; 70]); %! assert_equal (G.mean_y, [8/3; 3.5]); %! assert_equal (G.sum_y, [8; 7]); ## Function handles are named 'fun' by their position among the methods %!test %! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], 'VariableNames', {'g', ... %! 'x'}); %! G = groupsummary (T, 'g', @std); %! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount', 'fun1_x'}); %! assert_equal (G.fun1_x, [std([10; 20; 50]); std([30; 40])]); ## Multiple function handles keep their 'fun' index across data variables %!test %! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], [1; 2; 3; 4; 5], ... %! 'VariableNames', {'g', 'x', 'y'}); %! G = groupsummary (T, 'g', {@std, @(v) median(v)}, {'x', 'y'}); %! assert_equal (G.Properties.VariableNames, ... %! {'g', 'GroupCount', 'fun1_x', 'fun2_x', 'fun1_y', 'fun2_y'}); %! assert_equal (G.fun2_x, [median([10; 20; 50]); median([30; 40])]); ## A function handle may return a row, producing a multi-column result variable %!test %! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], 'VariableNames', {'g', ... %! 'x'}); %! G = groupsummary (T, 'g', @(v) [min(v), max(v)]); %! assert_equal (G.fun1_x, [10, 50; 30, 40]); ## 'min', 'max', 'range', 'std', 'var', 'median', 'mode' spot checks %!test %! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], 'VariableNames', {'g', ... %! 'x'}); %! G = groupsummary (T, 'g', {'min', 'max', 'range', 'var', 'median', 'mode'}); %! assert_equal (G.min_x, [10; 30]); %! assert_equal (G.max_x, [50; 40]); %! assert_equal (G.range_x, [40; 10]); %! assert_equal (G.var_x, [var([10; 20; 50]); var([30; 40])]); %! assert_equal (G.median_x, [20; 35]); %! assert_equal (G.mode_x, [10; 30]); ## 'nnz' counts the nonzero values in the group, omitting NaN %!test %! T = table ([1; 1; 2; 2], [0; 2; 0; 0], 'VariableNames', {'g', 'x'}); %! G = groupsummary (T, 'g', 'nnz'); %! assert_equal (G.nnz_x, [1; 0]); ## Named methods omit NaN in the data; 'nummissing' counts them %!test %! T = table ([1; 1; 2; 2], [NaN; 20; NaN; NaN], 'VariableNames', {'g', 'x'}); %! G = groupsummary (T, 'g', {'mean', 'nummissing'}); %! assert_equal (G.mean_x, [20; NaN]); %! assert_equal (G.nummissing_x, [1; 2]); ## 'numunique' counts the unique non-missing values, for any data type %!test %! T = table ([1; 1; 2; 2], {'a'; 'a'; 'b'; ''}, 'VariableNames', {'g', 's'}); %! G = groupsummary (T, 'g', {'numunique', 'nummissing'}); %! assert_equal (G.numunique_s, [1; 1]); %! assert_equal (G.nummissing_s, [0; 1]); ## Missing values in a grouping variable form their own group, sorted last %!test %! T = table ([1; 1; 2; NaN; 2], [10; 20; 30; 40; 50], 'VariableNames', ... %! {'g', 'x'}); %! G = groupsummary (T, 'g', 'mean'); %! assert_equal (G.g, [1; 2; NaN]); %! assert_equal (G.GroupCount, [2; 2; 1]); %! assert_equal (G.mean_x, [15; 40; 40]); ## 'IncludeMissingGroups' false drops the rows with a missing grouping value %!test %! T = table ([1; 1; 2; NaN; 2], [10; 20; 30; 40; 50], 'VariableNames', ... %! {'g', 'x'}); %! G = groupsummary (T, 'g', 'mean', 'IncludeMissingGroups', false); %! assert_equal (G.g, [1; 2]); %! assert_equal (G.GroupCount, [2; 2]); %! assert_equal (G.mean_x, [15; 40]); ## Several grouping variables group by their unique combinations, missing last %!test %! T = table ([1; 1; 2; NaN; 2], {'b'; 'a'; 'a'; 'a'; 'b'}, ... %! [10; 20; 30; 40; 50], ... %! 'VariableNames', {'g1', 'g2', 'x'}); %! G = groupsummary (T, {'g1', 'g2'}, 'sum'); %! assert_equal (G.g1, [1; 1; 2; 2; NaN]); %! assert_equal (G.g2, {'a'; 'b'; 'a'; 'b'; 'a'}); %! assert_equal (G.GroupCount, [1; 1; 1; 1; 1]); %! assert_equal (G.sum_x, [20; 10; 30; 50; 40]); ## A categorical grouping variable keeps its type in the output %!test %! T = table (categorical ({'a'; 'b'; 'a'; 'c'}), [1; 2; 3; 4], ... %! 'VariableNames', {'g', 'x'}); %! G = groupsummary (T, 'g', 'sum'); %! assert_equal (iscategorical (G.g), true); %! assert_equal (cellstr (G.g), {'a'; 'b'; 'c'}); %! assert_equal (G.sum_x, [4; 2; 4]); ## An ordinal categorical grouping variable groups by category order %!test %! c = categorical ({'medium'; 'low'; 'high'; 'low'}, ... %! {'low', 'medium', 'high'}, 'Ordinal', true); %! T = table (c, [1; 2; 3; 4], 'VariableNames', {'g', 'x'}); %! G = groupsummary (T, 'g', 'sum'); %! assert_equal (cellstr (G.g), {'low'; 'medium'; 'high'}); %! assert_equal (G.sum_x, [6; 1; 3]); %!error ... %! groupsummary (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', ... %! 'bogus') %!error ... %! groupsummary (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', ... %! 'sum', 'x', 5) %!error ... %! groupsummary (table ([1; 2], 'VariableNames', {'g'}), false) %!error ... %! groupsummary (table ([1; 2], 'VariableNames', {'g'})) %!error ... %! groupsummary (table ([1; 2], {'a'; 'b'}, 'VariableNames', {'g', 'x'}), ... %! 'g', 'mean') ## 'IncludeEmptyGroups' adds the unused categories of a categorical grouping ## variable as empty groups (GroupCount 0, sum 0, NaN for the rest) %!test %! c = categorical ({'a'; 'a'; 'b'}, {'a', 'b', 'c'}); %! T = table (c, [10; 20; 30], 'VariableNames', {'g', 'x'}); %! G = groupsummary (T, 'g', {'sum', 'mean', 'max', 'numunique'}, 'x', ... %! 'IncludeEmptyGroups', true); %! assert_equal (cellstr (G.g), {'a'; 'b'; 'c'}); %! assert_equal (G.GroupCount, [2; 1; 0]); %! assert_equal (G.sum_x, [30; 30; 0]); %! assert_equal (G.mean_x, [15; 30; NaN]); %! assert_equal (G.max_x, [20; 30; NaN]); %! assert_equal (G.numunique_x, [2; 1; 0]); ## A GROUPBINS edge vector groups a numeric variable by bin interval %!test %! T = table ([1; 3; 5; 7; 9; 11], (1:6)', 'VariableNames', {'g', 'x'}); %! G = groupsummary (T, 'g', [0 6 12], 'sum', 'x'); %! assert_equal (iscategorical (G.disc_g), true); %! assert_equal (cellstr (G.disc_g), {'[0, 6)'; '[6, 12]'}); %! assert_equal (G.GroupCount, [3; 3]); %! assert_equal (G.sum_x, [6; 15]); ## A GROUPBINS number of bins makes equal-width bins over the data range %!test %! T = table ([1; 2; 3; 4], (1:4)', 'VariableNames', {'g', 'x'}); %! G = groupsummary (T, 'g', 3, 'sum', 'x'); %! assert_equal (cellstr (G.disc_g), {'[1, 2)'; '[2, 3)'; '[3, 4]'}); %! assert_equal (G.GroupCount, [1; 1; 2]); ## 'IncludedEdge' 'right' makes the right bin edge the inclusive one %!test %! T = table ([0; 6; 12], (1:3)', 'VariableNames', {'g', 'x'}); %! G = groupsummary (T, 'g', [0 6 12], 'sum', 'x', 'IncludedEdge', 'right'); %! assert_equal (cellstr (G.disc_g), {'[0, 6]'; '(6, 12]'}); %! assert_equal (G.GroupCount, [2; 1]); ## Binning combined with 'IncludeEmptyGroups' keeps the empty bins %!test %! T = table ([1; 2; 3; 20], (1:4)', 'VariableNames', {'g', 'x'}); %! G = groupsummary (T, 'g', [0 5 10 25], 'sum', 'x', 'IncludeEmptyGroups', ... %! true); %! assert_equal (cellstr (G.disc_g), {'[0, 5)'; '[5, 10)'; '[10, 25]'}); %! assert_equal (G.GroupCount, [3; 0; 1]); %! assert_equal (G.sum_x, [6; 0; 4]); ## GROUPBINS time-unit 'month' bins a datetime by calendar month (month_) %!test %! t = datetime (2023, [1; 1; 2; 3; 3], [1; 2; 3; 4; 5]); %! T = table (t, (1:5)', 'VariableNames', {'t', 'x'}); %! G = groupsummary (T, 't', 'month', 'sum', 'x'); %! assert_equal (G.Properties.VariableNames, {'month_t', 'GroupCount', ... %! 'sum_x'}); %! assert_equal (iscategorical (G.month_t), true); %! assert_equal (cellstr (G.month_t), {'Jan-2023'; 'Feb-2023'; 'Mar-2023'}); %! assert_equal (G.GroupCount, [2; 1; 2]); %! assert_equal (G.sum_x, [3; 3; 9]); ## Time-unit 'year' point labels; empty periods appear with IncludeEmptyGroups %!test %! t = datetime ([2020; 2020; 2023], 1, 1); %! T = table (t, (1:3)', 'VariableNames', {'t', 'x'}); %! G = groupsummary (T, 't', 'year', 'sum', 'x', 'IncludeEmptyGroups', true); %! assert_equal (numel (categories (G.year_t)), 4); %! assert_equal (cellstr (G.year_t), {'2020'; '2021'; '2022'; '2023'}); %! assert_equal (G.GroupCount, [2; 0; 0; 1]); ## Time-unit 'day' and 'quarter' labels %!test %! t = datetime (2024, [1; 4], [3; 10]); %! T = table (t, (1:2)', 'VariableNames', {'t', 'x'}); %! assert_equal (cellstr (groupsummary (T, 't', 'day', 'sum', 'x').day_t), ... %! {'03-Jan-2024'; '10-Apr-2024'}); %! assert_equal (cellstr (groupsummary (T, 't', 'quarter', 'sum', ... %! 'x').quarter_t), ... %! {'Q1 2024'; 'Q2 2024'}); ## A duration bin width bins a duration variable (labelled in its unit, ## disc_) %!test %! d = minutes ([100; 200; 300]); %! T = table (d, (1:3)', 'VariableNames', {'d', 'x'}); %! G = groupsummary (T, 'd', minutes (90), 'sum', 'x'); %! assert_equal (G.Properties.VariableNames, {'disc_d', 'GroupCount', 'sum_x'}); %! assert_equal (cellstr (G.disc_d), ... %! {'[90 min, 180 min)'; '[180 min, 270 min)'; ... %! '[270 min, 360 min]'}); ## Time-unit binning of a numeric variable is rejected %!error ... %! groupsummary (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', ... %! 'month', 'sum') %!error ... %! groupsummary (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', ... %! 2, 'sum', 'IncludedEdge', 'mid') %!error ... %! groupsummary (table ({'a'; 'b'}, [3; 4], 'VariableNames', {'g', 'x'}), ... %! 'g', 2, 'sum') %!error ... %! groupsummary (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', ... %! 'month', 'sum') ## 'groupcounts' returns the grouping variables, GroupCount, and Percent %!test %! T = table ([1; 1; 2; 2; 1], [10; 20; 30; 40; 50], 'VariableNames', {'g', ... %! 'x'}); %! G = groupcounts (T, 'g'); %! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount', 'Percent'}); %! assert_equal (G.g, [1; 2]); %! assert_equal (G.GroupCount, [3; 2]); %! assert_equal (G.Percent, [60; 40]); ## Percent is each group's count as a percentage of the total row count %!test %! T = table ([1; 1; 1; 2], 'VariableNames', {'g'}); %! G = groupcounts (T, 'g'); %! assert_equal (G.GroupCount, [3; 1]); %! assert_equal (G.Percent, [75; 25]); ## Several grouping variables group by their unique combinations, missing last %!test %! T = table ([1; 1; 2; NaN; 2], {'b'; 'a'; 'a'; 'a'; 'b'}, ... %! 'VariableNames', {'g1', 'g2'}); %! G = groupcounts (T, {'g1', 'g2'}); %! assert_equal (G.g1, [1; 1; 2; 2; NaN]); %! assert_equal (G.g2, {'a'; 'b'; 'a'; 'b'; 'a'}); %! assert_equal (G.GroupCount, [1; 1; 1; 1; 1]); %! assert_equal (G.Percent, [20; 20; 20; 20; 20]); ## Missing values in a grouping variable form their own group, sorted last %!test %! T = table ([1; 1; 2; NaN; 2], 'VariableNames', {'g'}); %! G = groupcounts (T, 'g'); %! assert_equal (G.g, [1; 2; NaN]); %! assert_equal (G.GroupCount, [2; 2; 1]); %! assert_equal (G.Percent, [40; 40; 20]); ## 'IncludeMissingGroups' false drops the rows with a missing grouping value %!test %! T = table ([1; 1; 2; NaN; 2], 'VariableNames', {'g'}); %! G = groupcounts (T, 'g', 'IncludeMissingGroups', false); %! assert_equal (G.g, [1; 2]); %! assert_equal (G.GroupCount, [2; 2]); %! assert_equal (G.Percent, [50; 50]); ## A categorical grouping variable keeps its type and groups by category order %!test %! c = categorical ({'medium'; 'low'; 'high'; 'low'}, ... %! {'low', 'medium', 'high'}, 'Ordinal', true); %! T = table (c, 'VariableNames', {'g'}); %! G = groupcounts (T, 'g'); %! assert_equal (iscategorical (G.g), true); %! assert_equal (cellstr (G.g), {'low'; 'medium'; 'high'}); %! assert_equal (G.GroupCount, [2; 1; 1]); %!error ... %! T = table ([1, 2; 1, 2; 3, 4], [5; 6; 7], 'VariableNames', {'A', 'V'}); %! groupcounts (T, 'A'); %!error ... %! groupcounts (table ([1; 2], 'VariableNames', {'g'}), false) ## 'IncludeEmptyGroups' adds unused categorical categories with zero count %!test %! c = categorical ({'a'; 'a'; 'b'}, {'a', 'b', 'c'}); %! T = table (c, 'VariableNames', {'g'}); %! G = groupcounts (T, 'g', 'IncludeEmptyGroups', true); %! assert_equal (cellstr (G.g), {'a'; 'b'; 'c'}); %! assert_equal (G.GroupCount, [2; 1; 0]); %! assert_equal (G.Percent, [100*2/3; 100/3; 0]); ## A GROUPBINS edge vector groups a numeric variable by bin interval %!test %! T = table ([1; 3; 5; 7; 9; 11], 'VariableNames', {'g'}); %! G = groupcounts (T, 'g', [0 6 12]); %! assert_equal (cellstr (G.disc_g), {'[0, 6)'; '[6, 12]'}); %! assert_equal (G.GroupCount, [3; 3]); %! assert_equal (G.Percent, [50; 50]); ## Binning combined with 'IncludeEmptyGroups' keeps the empty bins %!test %! T = table ([1; 2; 3; 20], 'VariableNames', {'g'}); %! G = groupcounts (T, 'g', [0 5 10 25], 'IncludeEmptyGroups', true); %! assert_equal (cellstr (G.disc_g), {'[0, 5)'; '[5, 10)'; '[10, 25]'}); %! assert_equal (G.GroupCount, [3; 0; 1]); %!error ... %! groupcounts (table ([1; 2], 'VariableNames', {'g'}), 'g', 'bogus') %!error ... %! groupcounts (table ([1; 2], 'VariableNames', {'g'}), 'g', 2, ... %! 'IncludedEdge', 'x') ## 'groupfilter' with a scalar-returning method keeps or drops whole groups %!test %! T = table ([1; 1; 1; 2; 2; 3], [3; 1; 2; 9; 8; 5], ... %! 'VariableNames', {'g', 'x'}); %! G = groupfilter (T, 'g', @(x) numel (x) > 2); %! assert_equal (G.Properties.VariableNames, {'g', 'x'}); %! assert_equal (G.g, [1; 1; 1]); %! assert_equal (G.x, [3; 1; 2]); ## A vector-returning method keeps individual rows within each group %!test %! T = table ([1; 1; 1; 1; 2; 2; 2; 2], [67; 65; 71; 55; 61; 79; 58; 78], ... %! 'VariableNames', {'d', 't'}); %! G = groupfilter (T, 'd', @(x) x == max (x)); %! assert_equal (G.d, [1; 2]); %! assert_equal (G.t, [71; 79]); ## Output rows keep their original order, not group order %!test %! T = table ([2; 1; 2; 1], [1; 2; 3; 4], 'VariableNames', {'g', 'x'}); %! G = groupfilter (T, 'g', @(x) numel (x) >= 1); %! assert_equal (G.x, [1; 2; 3; 4]); ## A row is kept only when the condition holds across all data variables %!test %! T = table ([1; 1; 2; 2], [5; 1; 9; 2], [1; 9; 1; 9], ... %! 'VariableNames', {'g', 'x', 'y'}); %! G = groupfilter (T, 'g', @(v) v > 3); %! assert_equal (G.x, zeros (0, 1)); ## 'DataVars' restricts the variables the filter operates on %!test %! T = table ([1; 1; 2; 2], [5; 1; 9; 2], [1; 9; 1; 9], ... %! 'VariableNames', {'g', 'x', 'y'}); %! G = groupfilter (T, 'g', @(v) v > 3, 'x'); %! assert_equal (G.x, [5; 9]); %! assert_equal (G.y, [1; 1]); ## Rows with a missing grouping value form their own group and are filtered too %!test %! T = table ([1; 1; NaN; 2; 2], [10; 20; 30; 40; 50], ... %! 'VariableNames', {'g', 'x'}); %! G = groupfilter (T, 'g', @(v) mean (v) > 25); %! assert_equal (G.g, [NaN; 2; 2]); %! assert_equal (G.x, [30; 40; 50]); ## A categorical grouping variable groups by category %!test %! T = table (categorical ({'a'; 'b'; 'a'; 'b'; 'a'}), [1; 5; 2; 6; 9], ... %! 'VariableNames', {'g', 'x'}); %! G = groupfilter (T, 'g', @(x) x == max (x)); %! assert_equal (cellstr (G.g), {'b'; 'a'}); %! assert_equal (G.x, [6; 9]); ## A GROUPBINS edge vector filters within bins of a numeric grouping variable %!test %! T = table ([1; 3; 5; 7; 9; 11], (1:6)', 'VariableNames', {'g', 'x'}); %! G = groupfilter (T, 'g', [0 6 12], @(x) numel (x) > 2); %! assert_equal (G.x, (1:6)'); %! G2 = groupfilter (T, 'g', [0 6 12], @(x) mean (x) > 4, 'IncludedEdge', ... %! 'right'); %! assert_equal (G2.x, [4; 5; 6]); %!test # a nested table variable passes its rows %! N = table ([1; 2; 3], 'VariableNames', {'p'}); %! T = table (N, [1; 1; 2], [5; 6; 7], 'VariableNames', {'A', 'G', 'V'}); %! F = groupfilter (T, 'G', @(x) height (x) > 1, 'A'); %! assert_equal (F.V, [5; 6]); %!error ... %! T = table ([1, 2; 1, 2; 3, 4], [5; 6; 7], 'VariableNames', {'A', 'V'}); %! groupfilter (T, 'A', @(x) true (size (x, 1), 1)); %!error ... %! groupfilter (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', 'sum') %!error ... %! groupfilter (table ([1; 2], 'VariableNames', {'g'}), 'g') %!error ... %! groupfilter (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', ... %! 'month', @(x) x > 0) %!error ... %! groupfilter (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', ... %! @(x) x > 0, 'x', 5) %!error ... %! groupfilter (table ([1; 2], 'VariableNames', {'g'}), false, @(x) x > 0) %!error ... %! groupfilter (table ([1; 1], [3; 4], 'VariableNames', {'g', 'x'}), 'g', ... %! @(x) [true; true; true]) ## 'grouptransform' 'meancenter' subtracts the group mean, in place %!test %! T = table ([1; 1; 1; 2; 2], [10; 20; 30; 40; 60], ... %! 'VariableNames', {'g', 'x'}); %! G = grouptransform (T, 'g', 'meancenter'); %! assert_equal (G.Properties.VariableNames, {'g', 'x'}); %! assert_equal (G.g, [1; 1; 1; 2; 2]); %! assert_equal (G.x, [-10; 0; 10; -10; 10]); ## 'zscore', 'norm', and 'rescale' spot checks %!test %! T = table ([1; 1; 1; 2; 2], [10; 20; 30; 40; 60], ... %! 'VariableNames', {'g', 'x'}); %! assert_equal (grouptransform (T, 'g', 'zscore').x, ... %! [-1; 0; 1; -1/sqrt(2); 1/sqrt(2)], 8 * eps); %! assert_equal (grouptransform (T, 'g', 'norm').x, ... %! [[10; 20; 30] ./ norm([10; 20; 30]); ... %! [40; 60] ./ norm([40; 60])], 8 * eps); %! assert_equal (grouptransform (T, 'g', 'rescale').x, [0; 0.5; 1; 0; 1]); ## 'meanfill' replaces missing values with the group mean %!test %! T = table ([1; 1; 2; 2], [10; NaN; 30; 40], 'VariableNames', {'g', 'x'}); %! G = grouptransform (T, 'g', 'meanfill'); %! assert_equal (G.x, [10; 10; 30; 40]); ## 'zscore' omits NaN for the group statistics and leaves NaN in place %!test %! T = table ([1; 1; 2; 2], [10; NaN; 30; 40], 'VariableNames', {'g', 'x'}); %! G = grouptransform (T, 'g', 'zscore'); %! assert_equal (G.x, [NaN; NaN; -1/sqrt(2); 1/sqrt(2)], 8 * eps); ## 'linearfill' interpolates interior missing values, leaving the edges as NaN %!test %! T = table ([1; 1; 1; 1; 2; 2; 2], [1; NaN; NaN; 4; NaN; 10; NaN], ... %! 'VariableNames', {'g', 'x'}); %! G = grouptransform (T, 'g', 'linearfill'); %! assert_equal (G.x, [1; 2; 3; 4; NaN; 10; NaN]); ## A function handle is applied to each group's slice %!test %! T = table ([1; 1; 1; 2; 2], [10; 20; 30; 40; 60], ... %! 'VariableNames', {'g', 'x'}); %! G = grouptransform (T, 'g', @(v) v - mean (v)); %! assert_equal (G.x, [-10; 0; 10; -10; 10]); ## 'ReplaceValues' false appends a '_' column, keeping ## originals %!test %! T = table ([1; 1; 1; 2; 2], [10; 20; 30; 40; 60], ... %! 'VariableNames', {'g', 'x'}); %! G = grouptransform (T, 'g', 'meancenter', 'ReplaceValues', false); %! assert_equal (G.Properties.VariableNames, {'g', 'x', 'meancenter_x'}); %! assert_equal (G.x, [10; 20; 30; 40; 60]); %! assert_equal (G.meancenter_x, [-10; 0; 10; -10; 10]); ## 'DataVars' restricts the variables that are transformed %!test %! T = table ([1; 1; 1; 2; 2], [10; 20; 30; 40; 60], [1; 2; 3; 4; 5], ... %! 'VariableNames', {'g', 'y', 'z'}); %! G = grouptransform (T, 'g', 'meancenter', 'y'); %! assert_equal (G.y, [-10; 0; 10; -10; 10]); %! assert_equal (G.z, [1; 2; 3; 4; 5]); ## Rows with a missing grouping value form their own group, in original order %!test %! T = table ([1; 1; NaN; 2; 2], [10; 20; 30; 40; 60], ... %! 'VariableNames', {'g', 'x'}); %! G = grouptransform (T, 'g', 'meancenter'); %! assert_equal (G.g, [1; 1; NaN; 2; 2]); %! assert_equal (G.x, [-5; 5; 0; -10; 10]); ## A GROUPBINS edge vector transforms within bins of a numeric grouping variable %!test %! T = table ([1; 3; 5; 7; 9; 11], [10; 20; 30; 40; 50; 60], ... %! 'VariableNames', {'g', 'x'}); %! G = grouptransform (T, 'g', [0 6 12], 'meancenter', 'x'); %! assert_equal (G.x, [-10; 0; 10; -10; 0; 10]); %!error ... %! T = table ([1, 2; 1, 2; 3, 4], [5; 6; 7], 'VariableNames', {'A', 'V'}); %! grouptransform (T, 'A', @(x) x); %!error ... %! grouptransform (table ([1; 2], 'VariableNames', {'g'}), false, 'zscore') %!error ... %! grouptransform (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', ... %! 'bogus') %!error ... %! grouptransform (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', ... %! 'month', 'zscore') %!error ... %! grouptransform (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', ... %! 'zscore', 'ReplaceValues', 'yes') %!error ... %! grouptransform (table ([1; 2], [3; 4], 'VariableNames', {'g', 'x'}), 'g', ... %! 'zscore', 'x', 5) %!error ... %! grouptransform (table ([1; 2], {'a'; 'b'}, 'VariableNames', {'g', 'x'}), ... %! 'g', 'zscore') %!error ... %! grouptransform (table ([1; 2], 'VariableNames', {'g'}), 'g') ## 'pivot' counts the rows of each row-and-column group by default %!test %! T = table (categorical ({'a'; 'a'; 'b'; 'b'; 'b'}), ... %! categorical ({'x'; 'y'; 'x'; 'y'; 'y'}), ... %! 'VariableNames', {'g', 'h'}); %! P = pivot (T, 'Rows', 'g', 'Columns', 'h'); %! assert_equal (P.Properties.VariableNames, {'g', 'x', 'y'}); %! assert_equal (cellstr (P{:, 'g'}), {'a'; 'b'}); %! assert_equal (P{:, {'x', 'y'}}, [1, 1; 1, 2]); ## Column variable names come from the column grouping values, joined with '_' %!test %! T = table (categorical ({'a'; 'a'; 'b'}), categorical ({'x'; 'y'; 'x'}), ... %! [1; 2; 3], 'VariableNames', {'g', 'h', 'v'}); %! P = pivot (T, 'Columns', {'g', 'h'}, 'DataVariable', 'v', 'Method', 'sum'); %! assert_equal (P.Properties.VariableNames, {'a_x', 'a_y', 'b_x'}); %! assert_equal (P{:, :}, [1, 2, 3]); ## A logical column grouping variable names the variables 'false' and 'true' %!test %! T = table (logical ([0; 0; 1; 1]), [1; 2; 3; 4], ... %! 'VariableNames', {'b', 'v'}); %! P = pivot (T, 'Columns', 'b', 'DataVariable', 'v', 'Method', 'sum'); %! assert_equal (P.Properties.VariableNames, {'false', 'true'}); %! assert_equal (P{:, :}, [3, 7]); ## 'DataVariable' with 'Method' aggregates that variable; empty mean cells are ## NaN %!test %! T = table (categorical ({'a'; 'a'; 'b'; 'b'}), ... %! categorical ({'x'; 'y'; 'x'; 'x'}), [10; 20; 30; 50], ... %! 'VariableNames', {'g', 'h', 'v'}); %! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'DataVariable', 'v', ... %! 'Method', 'mean'); %! assert_equal (P{:, {'x', 'y'}}, [10, 20; 40, NaN]); ## A numeric data variable defaults to 'sum'; the empty cell sums to zero %!test %! T = table (categorical ({'a'; 'a'; 'b'}), categorical ({'x'; 'y'; 'x'}), ... %! [10; 20; 30], 'VariableNames', {'g', 'h', 'v'}); %! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'DataVariable', 'v'); %! assert_equal (P{:, {'x', 'y'}}, [10, 20; 30, 0]); ## A function handle is applied to each cell's data slice %!test %! T = table (categorical ({'a'; 'a'; 'b'; 'b'}), ... %! categorical ({'x'; 'x'; 'y'; 'y'}), [1; 3; 5; 9], ... %! 'VariableNames', {'g', 'h', 'v'}); %! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'DataVariable', 'v', ... %! 'Method', @(x) max (x) - min (x)); %! assert_equal (P{:, {'x', 'y'}}, [2, NaN; NaN, 4]); ## Method 'percentage' gives each cell's row count as a percentage of the total %!test %! T = table (categorical ({'a'; 'a'; 'b'; 'b'}), ... %! categorical ({'x'; 'y'; 'x'; 'y'}), ... %! 'VariableNames', {'g', 'h'}); %! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'Method', 'percentage'); %! assert_equal (P{:, {'x', 'y'}}, [25, 25; 25, 25]); ## Method 'none' rearranges the data without aggregating; empty cells are ## missing %!test %! T = table (categorical ({'a'; 'a'; 'b'}), categorical ({'x'; 'y'; 'x'}), ... %! [10; 20; 30], 'VariableNames', {'g', 'h', 'v'}); %! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'DataVariable', 'v', ... %! 'Method', 'none'); %! assert_equal (P{:, {'x', 'y'}}, [10, 20; 30, NaN]); ## An omitted 'Rows' dimension collapses to a single row %!test %! T = table (categorical ({'a'; 'a'; 'b'}), [1; 2; 3], ... %! 'VariableNames', {'g', 'v'}); %! P = pivot (T, 'Columns', 'g', 'DataVariable', 'v', 'Method', 'sum'); %! assert_equal (P.Properties.VariableNames, {'a', 'b'}); %! assert_equal (P{:, :}, [3, 3]); ## With no 'Columns', a single variable named '_' (or 'count') %!test %! T = table (categorical ({'a'; 'a'; 'b'}), [1; 2; 3], ... %! 'VariableNames', {'g', 'v'}); %! P = pivot (T, 'Rows', 'g', 'DataVariable', 'v', 'Method', 'sum'); %! assert_equal (P.Properties.VariableNames, {'g', 'sum_v'}); %! assert_equal (P{:, 'sum_v'}, [3; 3]); %! P2 = pivot (T, 'Rows', 'g'); %! assert_equal (P2.Properties.VariableNames, {'g', 'count'}); %! assert_equal (P2{:, 'count'}, [2; 1]); ## 'RowLabelPlacement' 'rownames' moves the row labels to the RowNames property %!test %! T = table (categorical ({'a'; 'a'; 'b'}), categorical ({'x'; 'y'; 'x'}), ... %! 'VariableNames', {'g', 'h'}); %! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'RowLabelPlacement', 'rownames'); %! assert_equal (P.Properties.VariableNames, {'x', 'y'}); %! assert_equal (P.Properties.RowNames, {'a'; 'b'}); ## 'IncludeMissingGroups' controls whether missing grouping values form a group %!test %! T = table (categorical ({'a'; 'a'; 'b'; ''}, {'a', 'b'}), ... %! [1; 2; 3; 4], 'VariableNames', {'g', 'v'}); %! P = pivot (T, 'Rows', 'g', 'DataVariable', 'v', 'Method', 'sum'); %! assert_equal (P{:, 'sum_v'}, [3; 3; 4]); %! Q = pivot (T, 'Rows', 'g', 'DataVariable', 'v', 'Method', 'sum', ... %! 'IncludeMissingGroups', false); %! assert_equal (cellstr (Q{:, 'g'}), {'a'; 'b'}); %! assert_equal (Q{:, 'sum_v'}, [3; 3]); ## A missing column group is named '>' %!test %! T = table (categorical ({'a'; 'a'; 'b'; ''}, {'a', 'b'}), ... %! [1; 2; 3; 4], 'VariableNames', {'g', 'v'}); %! P = pivot (T, 'Columns', 'g', 'DataVariable', 'v', 'Method', 'sum'); %! assert_equal (P.Properties.VariableNames, {'a', 'b', ''}); %! assert_equal (P{:, :}, [3, 3, 4]); ## 'IncludeEmptyGroups' adds unused categorical categories as empty groups %!test %! T = table (setcats (categorical ({'a'; 'a'; 'b'}), {'a', 'b', 'c'}), ... %! categorical ({'x'; 'y'; 'x'}), 'VariableNames', {'g', 'h'}); %! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'IncludeEmptyGroups', true); %! assert_equal (cellstr (P{:, 'g'}), {'a'; 'b'; 'c'}); %! assert_equal (P{:, {'x', 'y'}}, [1, 1; 1, 0; 0, 0]); ## 'IncludeTotals' appends an 'Overall_' marginal row and column %!test %! T = table (categorical ({'a'; 'a'; 'b'; 'b'}), ... %! categorical ({'x'; 'y'; 'x'; 'y'}), [1; 2; 3; 9], ... %! 'VariableNames', {'g', 'h', 'v'}); %! P = pivot (T, 'Rows', 'g', 'Columns', 'h', 'DataVariable', 'v', ... %! 'Method', 'sum', 'IncludeTotals', true); %! assert_equal (P.Properties.VariableNames, {'g', 'x', 'y', 'Overall_sum'}); %! assert_equal (cellstr (P{:, 'g'}), {'a'; 'b'; 'Overall_sum'}); %! assert_equal (P{:, {'x', 'y', 'Overall_sum'}}, [1, 2, 3; 3, 9, 12; 4, 11, ... %! 15]); %!test # a nested table variable is counted by its rows %! N = table ([1; 2; 3], 'VariableNames', {'p'}); %! T = table (N, [1; 1; 2], [5; 6; 7], 'VariableNames', {'A', 'G', 'V'}); %! P = pivot (T, 'Rows', 'G', 'DataVariable', 'A', 'Method', 'count'); %! assert_equal (P{:,2}, [2; 1]); %!error ... %! T = table ([1, 2; 1, 2; 3, 4], [5; 6; 7], 'VariableNames', {'A', 'V'}); %! pivot (T, 'Rows', 'A'); %!error ... %! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'})) %!error ... %! pivot (table ([1; 2], [3; 4], [5; 6], 'VariableNames', {'g', 'x', 'y'}), ... %! 'Rows', 'g', 'DataVariable', {'x', 'y'}) %!error ... %! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', ... %! 'g', 'Method', 'bogus') %!error ... %! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', ... %! 'g', 'Method', 'sum') %!error ... %! pivot (table (categorical ({'a'; 'a'}), categorical ({'x'; 'x'}), [1; 2], ... %! 'VariableNames', {'g', 'h', 'v'}), 'Rows', 'g', 'Columns', 'h', ... %! 'DataVariable', 'v', 'Method', 'none') %!error ... %! pivot (table (categorical ({'a'; 'b'}), [1; 2], 'VariableNames', {'g', ... %! 'v'}), 'Rows', 'g', 'DataVariable', 'v', 'Method', 'none', ... %! 'IncludeTotals', true) %!error ... %! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', ... %! 'g', 'RowLabelPlacement', 'foo') %!error ... %! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', ... %! 'g', 'IncludeMissingGroups', 'yes') %!error ... %! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', ... %! 'g', 'OutputFormat', 'bogus') ## 'OutputFormat','nested' nests multiple Columns variables into nested tables %!test %! r = [1; 1; 2; 2; 1; 2]; %! c1 = categorical ({'A'; 'A'; 'B'; 'B'; 'A'; 'B'}); %! c2 = categorical ({'X'; 'Y'; 'X'; 'Y'; 'X'; 'Y'}); %! v = [10; 20; 30; 40; 50; 60]; %! T = table (r, c1, c2, v, 'VariableNames', {'r', 'c1', 'c2', 'v'}); %! P = pivot (T, 'Rows', 'r', 'Columns', {'c1', 'c2'}, 'DataVariable', 'v', ... %! 'Method', 'sum', 'OutputFormat', 'nested'); %! assert_equal (P.Properties.VariableNames, {'r', 'A', 'B'}); %! assert_equal (isa (P.A, 'table'), true); %! assert_equal (P.A.Properties.VariableNames, {'X', 'Y'}); %! assert_equal (P.A.X, [60; 0]); %! assert_equal (P.A.Y, [20; 0]); %! assert_equal (P.B.X, [0; 30]); %! assert_equal (P.B.Y, [0; 100]); ## Nested with a single Columns variable collapses to flat columns %!test %! r = [1; 1; 2]; %! c = categorical ({'A'; 'B'; 'A'}); %! v = [10; 20; 30]; %! T = table (r, c, v, 'VariableNames', {'r', 'c', 'v'}); %! P = pivot (T, 'Rows', 'r', 'Columns', 'c', 'DataVariable', 'v', ... %! 'Method', 'sum', 'OutputFormat', 'nested'); %! assert_equal (P.Properties.VariableNames, {'r', 'A', 'B'}); %! assert_equal (isa (P.A, 'double'), true); ## Nested output with IncludeTotals keeps a flat Overall_ column %!test %! r = [1; 1; 2; 2; 1; 2]; %! c1 = categorical ({'A'; 'A'; 'B'; 'B'; 'A'; 'B'}); %! c2 = categorical ({'X'; 'Y'; 'X'; 'Y'; 'X'; 'Y'}); %! v = [10; 20; 30; 40; 50; 60]; %! T = table (r, c1, c2, v, 'VariableNames', {'r', 'c1', 'c2', 'v'}); %! P = pivot (T, 'Rows', 'r', 'Columns', {'c1', 'c2'}, 'DataVariable', 'v', ... %! 'Method', 'sum', 'OutputFormat', 'nested', 'IncludeTotals', true); %! assert_equal (any (strcmp (P.Properties.VariableNames, 'Overall_sum')), ... %! true); %! assert_equal (P.Overall_sum, [80; 130; 210]); %!error ... %! pivot (table ([1; 2], 'VariableNames', {'g'}), 'Rows', 'g', ... %! 'RowsBinMethod', 2, 'IncludedEdge', 'mid') %!error ... %! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', ... %! 'g', 'RowsBinMethod', 5) %!error ... %! pivot (table (categorical ({'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', ... %! 'g', 'Wibble', 1) ## 'RowsBinMethod' bins a numeric row grouping variable by bin interval %!test %! T = table ([1; 5; 1; 9; 5], [1; 1; 2; 2; 3], [10; 20; 30; 40; 50], ... %! 'VariableNames', {'r', 'c', 'v'}); %! P = pivot (T, 'Rows', 'r', 'Columns', 'c', 'RowsBinMethod', [0 4 10]); %! assert_equal (iscategorical (P.disc_r), true); %! assert_equal (cellstr (P.disc_r), {'[0, 4)'; '[4, 10]'}); %! assert_equal (P{:, 2:end}, [1, 1, 0; 1, 1, 1]); ## 'ColumnsBinMethod' names the output variables after the column bins %!test %! T = table ([1; 1; 2; 2; 3], [1; 5; 1; 9; 5], [10; 20; 30; 40; 50], ... %! 'VariableNames', {'c', 'r', 'v'}); %! P = pivot (T, 'Rows', 'c', 'Columns', 'r', 'ColumnsBinMethod', 2, ... %! 'DataVariable', 'v', 'Method', 'sum'); %! assert_equal (P.Properties.VariableNames, {'c', '[0, 5)', '[5, 10]'}); %! assert_equal (P{:, 2:end}, [10, 20; 30, 40; 0, 50]); ################################################################################ ## ** Auxiliary Methods ** ## ################################################################################ ## Available Methods ## ## ## ## 'horzcat' 'iscolumn' 'isempty' 'isequal' ## ## 'isequaln' 'ismatrix' 'isrow' 'isscalar' ## ## 'istable' 'isvector' 'ndims' 'numel' ## ## 'repelem' 'repmat' 'size' 'squeeze' ## ## 'vertcat' ## ## ## ################################################################################ %!assert_equal (iscolumn (table ([1; 2; 3])), true) %!assert_equal (iscolumn (table ([1, 2, 3])), true) %!assert_equal (iscolumn (table (1, 2, 3)), false) %!assert_equal (isempty (table ()), true) %!assert_equal (isempty (table ([])), true) %!assert_equal (isempty (table (1)), false) %!assert_equal (ismatrix (table ([])), true) %!assert_equal (ismatrix (table (1)), true) %!assert_equal (isrow (table ([1, 2, 3])), true) %!assert_equal (isrow (table (1, 2, 3)), true) %!assert_equal (isrow (table ([1; 2; 3])), false) %!assert_equal (isscalar (table (1)), true) %!assert_equal (isscalar (table ([1, 2, 3])), true) %!assert_equal (isscalar (table (1, 2, 3)), false) %!assert_equal (isscalar (table ([1; 2; 3])), false) %!assert_equal (istable (table ()), true) %!assert_equal (isvector (table (1)), true) %!assert_equal (isvector (table (1, 2, 3)), true) %!assert_equal (isvector (table ([1, 2, 3])), true) %!assert_equal (isvector (table ([1; 2; 3])), true) %!assert_equal (isvector (table ([1; 2], {'a'; 'b'})), false) %!error ... %! length (table ([1; 2; 3], [2; 3; 4])); %!assert_equal (ndims (table ()), 2) %!assert_equal (ndims (table ([])), 2) %!assert_equal (ndims (table ([1, 2, 3, 4])), 2) %!assert_equal (ndims (table ([1; 2], {'a'; 'b'})), 2) ## 'numel' returns rows times variables, i.e. prod (size (tbl)) %!assert_equal (numel (table ()), 0) %!assert_equal (numel (table (1)), 1) %!assert_equal (numel (table (1, 2, 3, 4)), 4) %!assert_equal (numel (table ([1; 2; 3], [4; 5; 6])), 6) %!assert_equal (numel (table (1, 2, 3, 4)), prod (size (table (1, 2, 3, 4)))) ## 'horzcat' combines variables of equal-height tables %!test %! T1 = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); %! T2 = table ([10; 20; 30], 'VariableNames', {'z'}); %! H = horzcat (T1, T2); %! assert_equal (size (H), [3, 3]); %! assert_equal (H.Properties.VariableNames, {'x', 'y', 'z'}); %! assert_equal (H.x, [1; 2; 3]); %! assert_equal (H.z, [10; 20; 30]); %!test # 'horzcat' refuses inputs of different heights %! T1 = table ([1; 2; 3], 'VariableNames', {'x'}); %! T2 = table ([10; 20], 'VariableNames', {'z'}); %! assert_equal (size (horzcat (T1, table ([4; 5; 6], ... %! 'VariableNames', {'w'}))), [3, 2]); %!test # a table with rows but no variables counts in the height check %! T = table ([1;2;3;4;5], 'VariableNames', {'A'}); %! Z = removevars (T, 'A'); %! assert_equal (size (horzcat (Z, table ((1:5)', ... %! 'VariableNames', {'C'}))), [5, 1]); %!error ... %! horzcat (table ([1; 2; 3], 'VariableNames', {'x'}), ... %! table ([10; 20], 'VariableNames', {'z'})); %!error ... %! T = table ([1;2;3;4;5], 'VariableNames', {'A'}); ... %! horzcat (removevars (T, 'A'), table ([1; 2; 3], 'VariableNames', {'C'})); %!test # '[ , ]' operator dispatches to horzcat %! T1 = table ([1; 2; 3], 'VariableNames', {'x'}); %! T2 = table ([10; 20; 30], 'VariableNames', {'z'}); %! H = [T1, T2]; %! assert_equal (size (H), [3, 2]); %! assert_equal (H.Properties.VariableNames, {'x', 'z'}); %!test # row names of an input table are carried over %! T1 = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', {'r1', 'r2'}); %! T2 = table ([3; 4], 'VariableNames', {'y'}); %! H = horzcat (T1, T2); %! assert_equal (H.Properties.VariableNames, {'x', 'y'}); %! assert_equal (H.Properties.RowNames, {'r1'; 'r2'}); %!test # multiple row-named inputs are aligned by row name, not position %! T1 = table ([1; 2; 3], 'VariableNames', {'x'}, 'RowNames', {'r1', 'r2', ... %! 'r3'}); %! T2 = table ([20; 30; 10], 'VariableNames', {'y'}, 'RowNames', {'r2', ... %! 'r3', 'r1'}); %! T3 = table ([300; 100; 200], 'VariableNames', {'z'}, 'RowNames', {'r3', ... %! 'r1', 'r2'}); %! H = horzcat (T1, T2, T3); %! assert_equal (H.Properties.RowNames, {'r1'; 'r2'; 'r3'}); %! assert_equal (H.x, [1; 2; 3]); %! assert_equal (H.y, [10; 20; 30]); %! assert_equal (H.z, [100; 200; 300]); ## Test 'cat' along dimension 1 concatenates vertically %!test %! A = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 't'}); %! B = table ([3; 4], {'c'; 'd'}, 'VariableNames', {'n', 't'}); %! C = cat (1, A, B); %! assert_equal (size (C), [4, 2]); %! assert_equal (C.Properties.VariableNames, {'n', 't'}); %! assert_equal (C.n, [1; 2; 3; 4]); %! assert_equal (C.t, {'a'; 'b'; 'c'; 'd'}); ## Test 'cat' along dimension 2 concatenates horizontally %!test %! A = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 't'}); %! Z = table ([5; 6], 'VariableNames', {'z'}); %! C = cat (2, A, Z); %! assert_equal (size (C), [2, 3]); %! assert_equal (C.Properties.VariableNames, {'n', 't', 'z'}); %! assert_equal (C.z, [5; 6]); ## Test 'cat' with a single table returns it unchanged %!test %! A = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 't'}); %! assert_equal (size (cat (1, A)), [2, 2]); %! assert_equal (size (cat (2, A)), [2, 2]); ## Test 'cat' with more than two tables %!test %! A = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 't'}); %! assert_equal (size (cat (1, A, A, A)), [6, 2]); ## Test 'cat' drops null operands %!test %! A = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 't'}); %! assert_equal (size (cat (1, A, [], A)), [4, 2]); %! assert_equal (size (cat (2, A, [])), [2, 2]); ## Test 'cat' of empty tables returns an empty table %!test %! C = cat (1, table (), table ()); %! assert_equal (istable (C), true); %! assert_equal (size (C), [0, 0]); ## Test 'cat' along dimension 1 preserves row names %!test %! R = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', {'r1'; 'r2'}); %! S = table ([3; 4], 'VariableNames', {'x'}, 'RowNames', {'r3'; 'r4'}); %! assert_equal (cat (1, R, S).Properties.RowNames, {'r1'; 'r2'; 'r3'; 'r4'}); ## Test input validation for 'cat' method %!error ... %! cat (3, table (1), table (2)); %!error ... %! cat (0, table (1), table (2)); %!error ... %! cat (1.5, table (1), table (2)); ## Test 'horzcat' with a single table returns it unchanged %!test %! A = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 't'}); %! H = horzcat (A); %! assert_equal (size (H), [2, 2]); %! assert_equal (H.Properties.VariableNames, {'n', 't'}); %! assert_equal (H.n, [1; 2]); %! assert_equal (H.t, {'a'; 'b'}); ## Test 'horzcat' drops null operands %!test %! A = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 't'}); %! H = horzcat (A, []); %! assert_equal (size (H), [2, 2]); %! assert_equal (H.Properties.VariableNames, {'n', 't'}); %! assert_equal (size (horzcat ([], A)), [2, 2]); %! assert_equal (size (horzcat (A, {})), [2, 2]); %! assert_equal (size (horzcat (A, table ())), [2, 2]); ## Test 'horzcat' of null operands only returns an empty table %!test %! H = horzcat (table (), table ()); %! assert_equal (istable (H), true); %! assert_equal (size (H), [0, 0]); ## Test input validation for 'horzcat' method %!error ... %! horzcat (table (1), 5); %!error ... %! horzcat (table (1, 'VariableNames', {'x'}), ... %! table (2, 'VariableNames', {'x'})); %!error ... %! horzcat (table ([1; 2], 'VariableNames', {'x'}), ... %! table (3, 'VariableNames', {'y'})); %!error ... %! horzcat (table (1, 'VariableNames', {'x'}, 'RowNames', {'r1'}), ... %! table (2, 'VariableNames', {'y'}, 'RowNames', {'q1'})); ## 'vertcat' stacks rows of tables sharing variable names %!test %! T1 = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); %! T2 = table ([4; 5], {'d'; 'e'}, 'VariableNames', {'x', 'y'}); %! V = [T1; T2]; %! assert_equal (size (V), [5, 2]); %! assert_equal (V.x, [1; 2; 3; 4; 5]); %! assert_equal (V.y, {'a'; 'b'; 'c'; 'd'; 'e'}); %!test # variables are matched by name, not position %! T1 = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); %! T2 = table ({'f'}, 6, 'VariableNames', {'y', 'x'}); %! V = vertcat (T1, T2); %! assert_equal (V.x, [1; 2; 3; 6]); %! assert_equal (V.y, {'a'; 'b'; 'c'; 'f'}); %!test # two row-named inputs concatenate, keeping their unique row names %! T1 = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', {'r1', 'r2'}); %! T2 = table ([3; 4], 'VariableNames', {'x'}, 'RowNames', {'r3', 'r4'}); %! V = [T1; T2]; %! assert_equal (V.Properties.RowNames, {'r1'; 'r2'; 'r3'; 'r4'}); %! assert_equal (V.x, [1; 2; 3; 4]); %!test # rows from an unnamed input get default 'Row' names by position %! T1 = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', {'r1', 'r2'}); %! T2 = table ([3; 4; 5], 'VariableNames', {'x'}); %! V = [T1; T2]; %! assert_equal (V.Properties.RowNames, {'r1'; 'r2'; 'Row3'; 'Row4'; 'Row5'}); %! assert_equal (V.x, [1; 2; 3; 4; 5]); %!test # default 'Row' numbering tracks the output position %! T1 = table ([1; 2; 3], 'VariableNames', {'x'}); %! T2 = table ([4; 5], 'VariableNames', {'x'}, 'RowNames', {'r1', 'r2'}); %! V = [T1; T2]; %! assert_equal (V.Properties.RowNames, {'Row1'; 'Row2'; 'Row3'; 'r1'; 'r2'}); %! assert_equal (V.x, [1; 2; 3; 4; 5]); %!test # object-typed variables retain their class through concatenation %! T1 = table (datetime (2020, 1, [1; 2]), categorical ({'a'; 'b'}), ... %! string ({'p'; 'q'}), 'VariableNames', {'d', 'c', 's'}); %! T2 = table (datetime (2020, 1, [3; 4]), categorical ({'b'; 'a'}), ... %! string ({'r'; 's'}), 'VariableNames', {'d', 'c', 's'}); %! V = vertcat (T1, T2); %! assert_equal (height (V), 4); %! assert_equal (class (V.d), 'datetime'); %! assert_equal (class (V.c), 'categorical'); %! assert_equal (class (V.s), 'string'); %! assert_equal (cellstr (V.c), {'a'; 'b'; 'b'; 'a'}); %! assert_equal (cellstr (V.s), {'p'; 'q'; 'r'; 's'}); ## Test 'vertcat' with a single table returns it unchanged %!test %! A = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 't'}); %! V = vertcat (A); %! assert_equal (size (V), [2, 2]); %! assert_equal (V.Properties.VariableNames, {'n', 't'}); %! assert_equal (V.n, [1; 2]); %! assert_equal (V.t, {'a'; 'b'}); ## Test 'vertcat' drops null operands %!test %! A = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 't'}); %! V = vertcat (A, []); %! assert_equal (size (V), [2, 2]); %! assert_equal (V.Properties.VariableNames, {'n', 't'}); %! assert_equal (size (vertcat ([], A)), [2, 2]); %! assert_equal (size (vertcat (A, zeros (0, 0))), [2, 2]); %! assert_equal (size (vertcat (A, table ())), [2, 2]); ## Test 'vertcat' of null operands only returns an empty table %!test %! V = vertcat (table (), table ()); %! assert_equal (istable (V), true); %! assert_equal (size (V), [0, 0]); ## Test 'vertcat' does not drop an empty operand that is not 0x0 %!error ... %! vertcat (table (1, 'VariableNames', {'x'}), zeros (0, 2)); ## Test 'vertcat' does not drop an empty char operand %!error ... %! vertcat (table (1, 'VariableNames', {'x'}), ''); ## Test input validation for 'vertcat' method %!error ... %! vertcat (table (1), 5); %!error ... %! vertcat (table (1, 'VariableNames', {'x'}), ... %! table (2, 'VariableNames', {'y'})); %!error ... %! vertcat (table (1, 'VariableNames', {'x'}, 'RowNames', {'r1'}), ... %! table (2, 'VariableNames', {'x'}, 'RowNames', {'r1'})); ## 'repelem' replicates rows and variables in place %!test # rows replicated consecutively (1 1 2 2 3 3) %! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); %! R = repelem (T, 2, 1); %! assert_equal (size (R), [6, 2]); %! assert_equal (R.x, [1; 1; 2; 2; 3; 3]); %! assert_equal (R.y, {'a'; 'a'; 'b'; 'b'; 'c'; 'c'}); %!test # columns replicated; copied variables get a '_N' suffix %! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); %! R = repelem (T, 1, 2); %! assert_equal (size (R), [3, 4]); %! assert_equal (R.Properties.VariableNames, {'x', 'x_1', 'y', 'y_1'}); %! assert_equal (R.x, [1; 2; 3]); %! assert_equal (R.x_1, [1; 2; 3]); %!test # a scalar count applies to both dimensions of 'repmat' alone %! T = table ([1; 2; 3], 'VariableNames', {'x'}); %! R = repmat (T, 2); %! assert_equal (size (R), [6, 2]); %! assert_equal (R.x, [1; 2; 3; 1; 2; 3]); %! assert_equal (R.Properties.VariableNames, {'x', 'x_1'}); %!test # repelem (T, 1, 1) leaves the table unchanged %! T = table ([1; 2; 3], 'VariableNames', {'x'}); %! assert_equal (isequal (repelem (T, 1, 1), T), true); %!test # a nested table variable is repeated by its rows %! N = table ([1; 2; 3], 'VariableNames', {'p'}); %! T = table (N, [1; 1; 2], [5; 6; 7], 'VariableNames', {'A', 'G', 'V'}); %! R = repelem (T, 2, 1); %! assert_equal (R.A.p, [1; 1; 2; 2; 3; 3]); ## Test input validation for 'repelem' method %!error ... %! repelem (table (1)); %!error ... %! repelem (table (1), 1, 2, 3); %!error ... %! repelem (table (1), 2); %!error ... %! repelem (table (1), 1.5, 1); %!error ... %! repelem (table (1), 2, 1.5); %!error ... %! repelem (table (1), -1, 1); %!error ... %! repelem (table (1), {1}, 1); %!error ... %! repelem (table ([1; 2; 3]), [1; 2], 1); %!error ... %! repelem (table ([1; 2; 3]), 1, [1, 2]); %!error ... %! repelem (table ([1; 2; 3]), [1; -1; 2], 1); %!error ... %! repelem (table ([1; 2; 3]), [1; 1.5; 2], 1); ## a count of zero leaves that dimension empty rather than being refused %!test %! T = table ([1; 2], [3; 4], 'VariableNames', {'A', 'B'}); %! assert_equal (size (repelem (T, 0, 1)), [0, 2]); %! assert_equal (size (repelem (T, 1, 0)), [2, 0]); ## a count vector repeats each row or variable by its own count (R2024a) %!test %! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'X', 'Y'}, ... %! 'RowNames', {'r1'; 'r2'; 'r3'}); %! R = repelem (T, [1; 2; 0], 1); %! assert_equal (R.X, [1; 2; 2]); %! assert_equal (R.Properties.RowNames, {'r1'; 'r2'; 'r2_1'}); %!test %! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'X', 'Y'}, ... %! 'RowNames', {'r1'; 'r2'; 'r3'}); %! R = repelem (T, [1, 2, 3], 1); %! assert_equal (R.Properties.RowNames, ... %! {'r1'; 'r2'; 'r2_1'; 'r3'; 'r3_1'; 'r3_2'}); %!test %! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'X', 'Y'}); %! R = repelem (T, 1, [2, 1]); %! assert_equal (R.Properties.VariableNames, {'X', 'X_1', 'Y'}); %! assert_equal (R.X_1, [1; 2; 3]); %!test %! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'X', 'Y'}); %! R = repelem (T, [2; 2; 2], [1, 3]); %! assert_equal (R.Properties.VariableNames, {'X', 'Y', 'Y_1', 'Y_2'}); %! assert_equal (height (R), 6); %!test %! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'X', 'Y'}); %! R = repelem (T, 1, [0, 1]); %! assert_equal (R.Properties.VariableNames, {'Y'}); %!test %! R = repelem (table ([1; 2]), [3; 0], 1); %! assert_equal (R.Var1, [1; 1; 1]); ## 'repmat' tiles whole rows and variables of a table %!test # rows tiled as a block (1 2 3 1 2 3) %! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); %! M = repmat (T, 2, 1); %! assert_equal (size (M), [6, 2]); %! assert_equal (M.x, [1; 2; 3; 1; 2; 3]); %! assert_equal (M.y, {'a'; 'b'; 'c'; 'a'; 'b'; 'c'}); %!test # columns tiled; the variable block is repeated then suffixed %! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); %! M = repmat (T, 1, 2); %! assert_equal (size (M), [3, 4]); %! assert_equal (M.Properties.VariableNames, {'x', 'y', 'x_1', 'y_1'}); %!test # a scalar count applies to both dimensions %! T = table ([1; 2], 'VariableNames', {'x'}); %! M = repmat (T, 2); %! assert_equal (size (M), [4, 2]); %! assert_equal (M.x, [1; 2; 1; 2]); %!test # repmat (T, 1, 1) leaves the table unchanged %! T = table ([1; 2; 3], 'VariableNames', {'x'}); %! assert_equal (isequal (repmat (T, 1, 1), T), true); %!test # a nested table variable is repeated by its rows %! N = table ([1; 2; 3], 'VariableNames', {'p'}); %! T = table (N, [1; 1; 2], [5; 6; 7], 'VariableNames', {'A', 'G', 'V'}); %! R = repmat (T, 2, 1); %! assert_equal (R.A.p, [1; 2; 3; 1; 2; 3]); ## Test input validation for 'repmat' method %!error ... %! repmat (table (1)); %!error ... %! repmat (table (1), 1, 2, 3); %!error ... %! repmat (table (1), 1.5); %!error ... %! repmat (table (1), 1.5, 1); %!error ... %! repmat (table (1), 2, 1.5); %!error ... %! repmat (table (1), [2 3 4]); ## a size vector names both counts, and a negative count repeats nothing %!test %! T = table ([1; 2], [3; 4], 'VariableNames', {'A', 'B'}); %! R = repmat (T, [2 3]); %! assert_equal (size (R), [4, 6]); %! assert_equal (R.Properties.VariableNames, ... %! {'A', 'B', 'A_1', 'B_1', 'A_2', 'B_2'}); %! assert_equal (size (repmat (T, 0, 1)), [0, 2]); %! assert_equal (size (repmat (T, 1, 0)), [2, 0]); %! assert_equal (size (repmat (T, -1, 1)), [0, 2]); ## 'size' returns [height, width] and supports dimension arguments %!test %! T = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'a', 'b'}); %! assert_equal (size (T), [3, 2]); %! assert_equal (size (T, 1), 3); %! assert_equal (size (T, 2), 2); %! assert_equal (size (T, 3), 1); %! assert_equal (size (T, [1, 2]), [3, 2]); %! assert_equal (size (T, [2, 1]), [2, 3]); %! assert_equal (size (T, [2, 3]), [2, 1]); %!test # multiple output forms %! T = table ([1; 2; 3], [4; 5; 6], 'VariableNames', {'a', 'b'}); %! [r, c] = size (T); %! assert_equal ([r, c], [3, 2]); %! [r, c, d] = size (T); %! assert_equal ([r, c, d], [3, 2, 1]); ## 'squeeze' is a no-op for tables (always two-dimensional) %!test %! T = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); %! assert_equal (isequal (squeeze (T), T), true); %!assert_equal (isequal (squeeze (table ()), table ()), true) ## 'isequal' compares variables by value, not by class %!assert_equal (isequal (table ([1; 2]), table ([1; 2])), true) %!assert_equal (isequal (table (int8 ([1; 2])), table ([1; 2])), true) %!assert_equal (isequal (table (single ([1; 2])), table ([1; 2])), true) %!assert_equal (isequal (table (logical ([1; 0])), table ([1; 0])), true) %!assert_equal (isequal (table ({'a'; 'b'}), table (string ({'a'; 'b'}))), true) ## a difference in any other property makes the tables unequal %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = A; %! B.Properties.VariableNames = {'b'}; %! assert_equal (isequal (A, B), false); %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = A; %! B.Properties.DimensionNames = {'Obs', 'Variables'}; %! assert_equal (isequal (A, B), false); %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = A; %! B.Properties.Description = 'note'; %! assert_equal (isequal (A, B), false); %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = A; %! B.Properties.UserData = 42; %! assert_equal (isequal (A, B), false); %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = A; %! B.Properties.VariableUnits = {'m'}; %! assert_equal (isequal (A, B), false); %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = A; %! B.Properties.VariableDescriptions = {'d'}; %! assert_equal (isequal (A, B), false); ## a custom property counts, whether it is absent on one side or differs %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = addprop (A, {'tp'}, {'table'}); %! assert_equal (isequal (A, B), false); %!test %! A = addprop (table ([1; 2], 'VariableNames', {'a'}), {'tp'}, {'table'}); %! B = A; %! A.Properties.CustomProperties.tp = 1; %! B.Properties.CustomProperties.tp = 2; %! assert_equal (isequal (A, B), false); ## so do the row names, absent on one side or different %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! B = A; %! B.Properties.RowNames = {'r1', 'r2'}; %! assert_equal (isequal (A, B), false); %!test %! A = table ([1; 2], 'VariableNames', {'a'}, 'RowNames', {'r1', 'r2'}); %! B = table ([1; 2], 'VariableNames', {'a'}, 'RowNames', {'q1', 'q2'}); %! assert_equal (isequal (A, B), false); ## order is part of the value, in both dimensions %!test %! A = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! assert_equal (isequal (A, A(:, {'b', 'a'})), false); %! assert_equal (isequal (A, A([2 1], :)), false); ## size is compared before anything else %!assert_equal (isequal (table ([1; 2]), table (1)), false) %!assert_equal (isequal (table (), table ([1; 2])([], :)), false) %!assert_equal (isequal (table ([1; 2])([], :), table ([1; 2])([], :)), true) %!assert_equal (isequal (table (), table ()), true) ## a missing value is never equal to itself, and a nested table is compared %!assert_equal (isequal (table ([NaN; 1]), table ([NaN; 1])), false) %!assert_equal (isequal (table (table ([1; 2])), table (table ([1; 2]))), true) ## more than two tables are all compared against the first %!test %! A = table ([1; 2], 'VariableNames', {'a'}); %! assert_equal (isequal (A, A, A), true); %! assert_equal (isequal (A, A, A(1, :)), false); ## anything that is not a table is unequal rather than an error %!assert_equal (isequal (table ([1; 2]), 5), false) %!assert_equal (isequal (table ([1; 2]), []), false) %!assert_equal (isequal (table ([1; 2]), struct ('a', 1)), false) %!test %! TT = timetable ([1; 2], 'RowTimes', datetime (2024, 1, 1) + hours (0:1)'); %! assert_equal (isequal (table ([1; 2]), TT), false); ## 'isequaln' treats missing values as equal %!assert_equal (isequaln (table ([NaN; 1]), table ([NaN; 1])), true) %!test %! s = string ({'a'; 'b'}); %! s(2) = missing; %! assert_equal (isequal (table (s), table (s)), false); %! assert_equal (isequaln (table (s), table (s)), true); ## it is otherwise 'isequal', so a metadata difference still tells %!test %! A = table ([NaN; 1], 'VariableNames', {'a'}); %! B = A; %! B.Properties.Description = 'note'; %! assert_equal (isequaln (A, B), false); ## and it compares variables by value, not by class %!assert_equal (isequaln (table (int8 ([1; 2])), table ([1; 2])), true) ## Test a comparison needs something to compare against %!error isequal (table ([1; 2])) %!error isequaln (table ([1; 2])) ################################################################################ ## ** Forbidden Methods ** ## ################################################################################ ## Available Methods ## ## ## ## 'repelems' 'reshape' 'resize' 'shiftdims' ## ## 'vec' ## ## ## ################################################################################ ## These methods are unsupported by design and only emit an error %!error ... %! repelems (table (1)); %!error ... %! reshape (table (1), 1, 1); %!error ... %! resize (table (1), 1); %!error ... %! shiftdims (table (1), 1); %!error ... %! vec (table (1)); ################################################################################ ## ** Reference and Assignment Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'end' 'subsref' 'subsasgn' ## ## ## ################################################################################ ## Test 'subref' and 'subsasgn' methods %!shared LastName, Age, Smoker, Height, Weight, BloodPressure, T, tblA %! LastName = {"Sanchez"; "Johnson"; "Li"; "Diaz"; "Brown"}; %! Age = [38; 43; 38; 40; 49]; %! Smoker = logical ([1; 0; 1; 0; 1]); %! Height = [71; 69; 64; 67; 64]; %! Weight = [176; 163; 131; 133; 119]; %! BloodPressure = [124, 93; 109, 77; 125, 83; 117, 75; 122, 80]; %! T = table (Age, Smoker, Height, Weight, BloodPressure); %!assert_equal (size (T(1,:)), [1, 5]); %!assert_equal (class (T(1,:)), "table"); %!assert_equal (size (T{1,:}), [1, 6]); %!assert_equal (T{1,:}, [38, 1, 71, 176, 124, 93]); %!assert_equal (T{:,2}, logical([1; 0; 1; 0; 1])); %!assert_equal (T{:,"Smoker"}, logical([1; 0; 1; 0; 1])); %!assert_equal (T{:,"Height"}, [71; 69; 64; 67; 64]); %!assert_equal (size(T{:,"Smoker"}), [5, 1]); %!assert_equal (size(T{:,"Height"}), [5, 1]); %!test # brace-indexing all-cell variables yields a cell array %! Tc = table ({'a'; 'b'}, {'c'; 'd'}, 'VariableNames', {'x', 'y'}); %! assert_equal (Tc{:,:}, {'a', 'c'; 'b', 'd'}); %!test # brace-indexing variables that cannot form one array gives a cell %! Tm = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); %! assert_equal (Tm{1,:}, {1, 'a'}); %! assert_equal (Tm{:,:}, {1, 'a'; 2, 'b'; 3, 'c'}); %!assert_equal (T.Variables, [Age, Smoker, Height, Weight, BloodPressure]); %!assert_equal (isempty (T.Properties.RowNames), true); %!assert_equal (numel (T.Properties.VariableNames), 5); %!test %! T.Properties.Description = "text"; %! assert_equal (T.Properties.Description, "text"); %!assert_equal (size (T(:,{"Age", "Smoker"})), [5, 2]); %!assert_equal (T{:,{"Age", "Smoker"}}, [Age, Smoker]); %!test %! T = table (Age, Smoker); %! T.("29-May-2019 Blood Pressure Reading") = BloodPressure; %! assert_equal (T.("29-May-2019 Blood Pressure Reading"), BloodPressure); %!test %! T = table (Age, Smoker); %! T.Height = Height; %! T.Weight = Weight; %! assert_equal (size (T), [5, 4]); %! assert_equal (T.Weight, Weight); %! assert_equal (T.Weight, T{:,"Weight"}); %! assert_equal (T.Weight, T{:,4}); %!test %! T.Weight(1) = 25; %! assert_equal (T.Weight, [25;Weight(2:end)]); %!test %! T.Weight([1,3]) = 25; %! assert_equal (T.Weight, [25;Weight(2);25;Weight(4:end)]); %!test %! T.Weight([1,3]) = 25; %! assert_equal (T.Weight, [25;Weight(2);25;Weight(4:end)]); %!test %! T.Weight([1:3,5]) = 25; %! assert_equal (T.Weight, [25;25;25;Weight(4);25]); %!test %! T = table (LastName, Age, Smoker, Height, Weight); %! T.Properties.RowNames = string (LastName); %!test %! T = table (LastName, Age, Smoker, Height, Weight); %! T.Properties.RowNames = 'LastName'; %!test %! T = table (LastName, Age, Smoker, Height, Weight); %! T.Properties.RowNames = {'LastName'}; %!test %! T = table (LastName, Age, Smoker, Height, Weight); %! T.Properties.RowNames = ['LastName']; %!test %! T = table (LastName, Age, Smoker, Height, Weight); %! assert_equal (isempty (T.Row), true); %! T.Properties.DimensionNames(1) = 'Patients'; %! assert_equal (isempty (T.Patients), true); %!test %! T.Properties.RowNames = "LastName"; %! assert_equal (isempty (T.Patients), false); %! T.Properties.DimensionNames(2) = 'Data'; %! assert_equal (T.Data, [Age,Smoker,Height,Weight]); %!test %! T = table (Age, Smoker, Height, Weight, BloodPressure); %! assert_equal (T{1, @isnumeric}, [38, 71, 176, 124, 93]); %! assert_equal (T{1, vartype ("numeric")}, [38, 71, 176, 124, 93]); %!test %! T1 = table (LastName, Age); %! assert_equal (T1{:, @iscellstr}, LastName); %! assert_equal (T1{:, @isnumeric}, Age); %! assert_equal (T1(:, @iscellstr).LastName, LastName) %! assert_equal (T1(:, @isnumeric).Age, Age) %!test %! T1 = table (LastName, Age); %! assert_equal (T1{:, @iscellstr}, LastName); %! assert_equal (T1{:, @isnumeric}, Age); %!test %! T1 = table (LastName, Age, Height); %! assert_equal (T1{:, @iscellstr}, LastName); %! assert_equal (class (T1(:, @isnumeric)), "table"); %! assert_equal (size (T1(:, @isnumeric)), [5, 2]); ## Test "end" keyword %!test %! t = table ([1;2;3], [4;5;6], [7;8;9], 'VariableNames', {'A', 'B', 'C'}, ... %! 'RowNames', {'Row1', 'Row2', 'Row3'}); %! out = t(:,end); %! assert_equal (out.Properties.VariableNames, {'C'}); %! assert_equal (out.Properties.RowNames, {'Row1'; 'Row2'; 'Row3'}); %! assert_equal (out.C, [7; 8; 9]); %! out = t{:,end}; %! assert_equal (out, [7; 8; 9]); %!test %! t = table ([1;2;3], [4;5;6], [7;8;9], 'VariableNames', {'A', 'B', 'C'}, ... %! 'RowNames', {'Row1', 'Row2', 'Row3'}); %! out = t(end, :); %! assert_equal (out.Properties.VariableNames, {'A', 'B', 'C'}); %! assert_equal (out.Properties.RowNames, {'Row3'}); %! assert_equal ([out.A, out.B, out.C], [3, 6, 9]); %! out = t{end,:}; %! assert_equal (out, [3, 6, 9]); ## 'subsref' with '()' returns a sub-table for every row/variable reference type %!test %! T = table ([1;2;3], {'a';'b';'c'}, [10;20;30], 'VariableNames', {'x','y', ... %! 'z'}); %! assert_equal (class (T(2,:)), 'table'); %! assert_equal (T(2,:).x, 2); %! assert_equal (size (T(1:2,[1 3])), [2, 2]); %! assert_equal (T(1:2,[1 3]).Properties.VariableNames, {'x','z'}); %! assert_equal (height (T(logical ([1 0 1]),:)), 2); %! assert_equal (T(:,logical ([1 0 1])).Properties.VariableNames, {'x','z'}); %! assert_equal (T(:,'y').y, {'a';'b';'c'}); %! assert_equal (T(:,{'x','z'}).Properties.VariableNames, {'x','z'}); %! assert_equal (T(:,@isnumeric).Properties.VariableNames, {'x','z'}); %! assert_equal (T(:,vartype ('numeric')).Properties.VariableNames, {'x','z'}); %! assert_equal (size (T(:,:)), [3, 3]); %!test # named row references require RowNames %! T = table ([1;2;3], [10;20;30], 'VariableNames', {'x','z'}, ... %! 'RowNames', {'r1','r2','r3'}); %! assert_equal (T('r2',:).x, 2); %! assert_equal (T({'r1','r3'},:).x, [1;3]); %! assert_equal (T('r2',:).Properties.RowNames, {'r2'}); ## Test input validation for 'subsref' with '()' indexing %!error ... %! T = table ([1;2;3]); T(1); %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); T(:,'q'); %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); T('r1',:); %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}, 'RowNames', {'r1','r2', ... %! 'r3'}); ... %! T('r9',:); ## 'subsref' with '{}' extracts raw data from concatenable variables %!test %! T = table ([1;2;3], [10;20;30], [100;200;300], 'VariableNames', {'x','y', ... %! 'z'}); %! assert_equal (T{2,:}, [2, 20, 200]); %! assert_equal (T{:,2}, [10;20;30]); %! assert_equal (T{1:2,[1 3]}, [1, 100; 2, 200]); %! assert_equal (T{:,'y'}, [10;20;30]); %! assert_equal (T{:,@isnumeric}, [1 10 100; 2 20 200; 3 30 300]); ## Test input validation for 'subsref' with '{}' indexing %!error ... %! T = table ([1;2;3]); T{1}; %!error ... %! T = table ([1; 2], containers.Map ({'a', 'b'}, {1, 2})); T.Variables; ## 'subsref' with '.' returns variable values, properties, and dimension data %!test %! T = table ([1;2;3], {'a';'b';'c'}, 'VariableNames', {'x','y'}); %! assert_equal (T.x, [1;2;3]); %! assert_equal (T.y, {'a';'b';'c'}); %! assert_equal (class (T.Properties), 'datatypes.tabular.TableProperties'); %! assert_equal (T.Properties.VariableNames, {'x','y'}); %! assert_equal (isempty (T.Row), true); %! assert_equal (T.Variables, {1, 'a'; 2, 'b'; 3, 'c'}); %!test # '.' on the RowNames dimension name returns the row names %! T = table ([1;2;3], 'VariableNames', {'x'}, 'RowNames', {'r1','r2','r3'}); %! assert_equal (T.Row, {'r1';'r2';'r3'}); %!test # chained '.' references resolve left to right %! T = table ([1;2;3], {'a';'b';'c'}, 'VariableNames', {'x','y'}); %! assert_equal (T.x(2), 2); %! assert_equal (T.Properties.VariableNames{1}, 'x'); %! assert_equal (T(2:3,:).x, [2;3]); ## Test input validation for 'subsref' with '.' indexing %!error ... %! T = table ([1;2;3]); subsref (T, struct ('type', '.', 'subs', 5)); %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); T.nope; ## 'subsasgn' with '()' writes numeric, cell, and table data element-wise %!test # numeric right-hand side %! T = table ([1;2;3], [10;20;30], 'VariableNames', {'x','z'}); %! T(1:2,[1 2]) = [70 700; 80 800]; %! assert_equal (T.x, [70;80;3]); %! assert_equal (T.z, [700;800;30]); %!test # cell right-hand side assigns element-wise (not broadcast) %! T = table ([1;2;3], [10;20;30], 'VariableNames', {'x','z'}); %! T(1:2,1) = {7; 8}; %! assert_equal (T.x, [7;8;3]); %! T(1:2,[1 2]) = {70 700; 80 800}; %! assert_equal (T.x, [70;80;3]); %! assert_equal (T.z, [700;800;30]); %!test # cellstr variable keeps its contents through cell assignment %! T = table ({'a';'b';'c'}, 'VariableNames', {'y'}); %! T(1:2,1) = {'P'; 'Q'}; %! assert_equal (T.y, {'P';'Q';'c'}); %!test # table right-hand side assigns element-wise across variables %! T = table ([1;2;3], {'a';'b';'c'}, 'VariableNames', {'x','y'}); %! T2 = table ([77;88], {'P';'Q'}, 'VariableNames', {'x','y'}); %! T(1:2,[1 2]) = T2; %! assert_equal (T.x, [77;88;3]); %! assert_equal (T.y, {'P';'Q';'c'}); %!test # single-element assignment %! T = table ([1;2;3], 'VariableNames', {'x'}); %! T(1,1) = {99}; %! assert_equal (T.x, [99;2;3]); ## Assigning to a row past the end grows the table, the rows skipped over ## taking the fill their variable's own type gives. Measured on MATLAB ## R2024a. %!test # growing pads a numeric variable with zero %! T = table ([1;2;3], [4;5;6]); %! T(5,:) = {9, 9}; %! assert_equal (height (T), 5); %! assert_equal (T.Var1, [1;2;3;0;9]); %! assert_equal (T.Var2, [4;5;6;0;9]); %!test # a variable the assignment does not name grows with the rest %! T = table ([1;2;3], [4;5;6]); %! T(5,1) = {9}; %! assert_equal (height (T), 5); %! assert_equal (T.Var2, [4;5;6;0;0]); %!test # row names grow with the rows, the added ones taking their place %! T = table ([1;2;3], 'RowNames', {'a';'b';'c'}); %! T(5,:) = {9}; %! assert_equal (height (T), 5); %! assert_equal (T.Properties.RowNames, {'a';'b';'c';'Row4';'Row5'}); %!test # a table with no row names grows without gaining any %! T = table ([1;2;3]); %! T(5,:) = {9}; %! assert_equal (T.Properties.RowNames, {}); %!test # a cellstr variable grows with a blank and stays a cellstr %! T = table ({'a';'b';'c'}, 'VariableNames', {'y'}); %! T(5,1) = {'z'}; %! assert_equal (iscellstr (T.y), true); %! assert_equal (T.y, {'a';'b';'c';'';'z'}); %!test # a logical variable grows with false %! T = table (logical ([1;0;1]), 'VariableNames', {'b'}); %! T(5,1) = {true}; %! assert_equal (T.b, logical ([1;0;1;0;1])); ## A row name is matched against the names there are. Reading one the table ## does not carry raises, but assigning to it asks for the row and adds it. ## Measured on MATLAB R2024a. %!test # assigning at an unknown name appends the row with that name %! T = table ([1;2;3], 'RowNames', {'a';'b';'c'}); %! T('z',:) = {9}; %! assert_equal (height (T), 4); %! assert_equal (T.Properties.RowNames, {'a';'b';'c';'z'}); %! assert_equal (T.Var1, [1;2;3;9]); %!test # a reference mixing a known and an unknown name does both %! T = table ([1;2;3], 'RowNames', {'a';'b';'c'}); %! T({'a','z'},:) = {7; 8}; %! assert_equal (T.Properties.RowNames, {'a';'b';'c';'z'}); %! assert_equal (T.Var1, [7;2;3;8]); %!test # two unknown names add two rows, in the order they were named %! T = table ([1;2;3], 'RowNames', {'a';'b';'c'}); %! T({'y','z'},:) = {7; 8}; %! assert_equal (T.Properties.RowNames, {'a';'b';'c';'y';'z'}); %! assert_equal (T.Var1, [1;2;3;7;8]); ## Test reading an unknown row name is still refused %!error ... %! T = table ([1;2;3], 'RowNames', {'a';'b';'c'}); T('z',:); %!error ... %! T = table ([1;2;3], 'RowNames', {'a';'b';'c'}); T{'z','Var1'}; ## 'subsasgn' with '()' creates the variables it names anew %!test %! P = table ([1; 2; 3], 'VariableNames', {'n'}); %! P(:,2) = {7; 8; 9}; %! assert_equal (P.Properties.VariableNames, {'n', 'Var2'}); %! assert_equal (P.Var2, [7; 8; 9]); %!test %! P = table ([1; 2; 3], 'VariableNames', {'n'}); %! P(:,[2, 3]) = table ([4; 5; 6], [7; 8; 9]); %! assert_equal (P.Properties.VariableNames, {'n', 'Var2', 'Var3'}); %!test %! P = table ([1; 2; 3], 'VariableNames', {'n'}); %! P(:,'z') = table ([4; 5; 6], 'VariableNames', {'q'}); %! assert_equal (P.Properties.VariableNames, {'n', 'z'}); %! assert_equal (P.z, [4; 5; 6]); %!test %! P = table ([1; 2; 3], 'VariableNames', {'n'}); %! P(:,[false, true]) = {7; 8; 9}; %! assert_equal (P.Properties.VariableNames, {'n', 'Var2'}); %!test %! P = table ([1; 2; 3], 'VariableNames', {'n'}); %! P.Properties.VariableUnits = {'m'}; %! P(:,'z') = {7; 8; 9}; %! assert_equal (P.Properties.VariableUnits, {'m', ''}); %!test %! P = table ([1; 2; 3], 'VariableNames', {'n'}); %! P = addprop (P, 'C', 'variable'); %! P.Properties.CustomProperties.C = 5; %! P(:,'z') = {7; 8; 9}; %! assert_equal (P.Properties.CustomProperties.C, [5, NaN]); %!test %! P = table (); %! P(:,'z') = table ([4; 5; 6]); %! assert_equal (size (P), [3, 1]); %!test %! warning ('off', 'all', 'local'); %! P = table ([1; 2; 3], 'VariableNames', {'n'}); %! P(2,'z') = {7}; %! assert_equal (P.z, [0; 7; 0]); %! P(2,'c') = {'txt'}; %! assert_equal (P.c, {''; 'txt'; ''}); %! P(2,'k') = {categorical({'k'})}; %! assert_equal (isundefined (P.k), [true; false; true]); %!test %! warning ('off', 'all', 'local'); %! P = table ([1; 2; 3], 'VariableNames', {'n'}); %! P(4,'z') = {1}; %! assert_equal (size (P), [4, 2]); %! assert_equal (P.n, [1; 2; 3; 0]); %! assert_equal (P.z, [0; 0; 0; 1]); %!test %! warning ('off', 'all', 'local'); %! P = table (hours ([1; 2]), calmonths ([1; 2]), 'VariableNames', {'d', 'c'}); %! P(4,'d') = {hours(9)}; %! assert_equal (P.d, hours ([1; 2; 0; 9])); %! assert_equal (P.c, calmonths ([1; 2; 0; 0])); %! P(2,'e') = {minutes(5)}; %! assert_equal (P.e, minutes ([0; 5; 0; 0])); %!test # rows added beside a struct, missing or nested variable are padded %! warning ('off', 'all', 'local'); %! T = table (struct ('a', {1; 2}), [1; 2], 'VariableNames', {'S', 'K'}); %! T{3,'K'} = 5; %! assert_equal (isempty (T.S(3).a), true); %!test %! warning ('off', 'all', 'local'); %! T = table ([missing; missing], [1; 2], 'VariableNames', {'M', 'K'}); %! T{3,'K'} = 5; %! assert_equal (ismissing (T.M), [true; true; true]); %!test # a nested table grows by rows of its own defaults, as in MATLAB %! warning ('off', 'all', 'local'); %! N = table ([1; 2], 'VariableNames', {'p'}); %! T = table (N, [1; 2], 'VariableNames', {'N', 'K'}); %! T{3,'K'} = 5; %! assert_equal (T.N.p, [1; 2; 0]); %!test # a whole row copied onto a new row needs no padding at all %! T = table (struct ('a', {1; 2}), [1; 2], 'VariableNames', {'S', 'K'}); %! T(3,:) = T(1,:); %! assert_equal (T.S(3).a, 1); %!warning ... %! P = table ([1; 2; 3], 'VariableNames', {'n'}); %! P(2,'z') = {7}; %!warning ... %! P = table ([1; 2; 3], 'VariableNames', {'n'}); %! P(4,'z') = {1}; ## 'subsasgn' with '()' takes a logical row mask %!test %! P = table ([1; 2; 3], 'VariableNames', {'n'}); %! P(logical ([1, 0, 1]),'n') = {10; 30}; %! assert_equal (P.n, [10; 2; 30]); ## 'subsasgn' with '()' and a cell array fills multicolumn, cell and nested ## table variables %!test %! P = table ([1; 2], {'a'; 'b'}, [4, 5; 6, 7], ... %! 'VariableNames', {'n', 's', 'm'}); %! P(end+1,:) = {3, 'c', [8, 9]}; %! assert_equal (P.m, [4, 5; 6, 7; 8, 9]); %! assert_equal (P.s, {'a'; 'b'; 'c'}); %!test %! P = table ({'a'; 'b'}, 'VariableNames', {'s'}); %! P(2,'s') = {{'x'}}; %! assert_equal (P.s, {'a'; 'x'}); %!test %! N = table ([3; 4], [5; 6], 'VariableNames', {'p', 'q'}); %! P = table ([1; 2], N, 'VariableNames', {'a', 'nt'}); %! P(2,'nt') = {table(8, 8, 'VariableNames', {'p', 'q'})}; %! N = P.nt; %! assert_equal (N.p, [3; 8]); %!test # a function handle assigned with '()' makes a cell variable %! P = table (1); %! P(:,'f') = {@sin}; %! assert_equal (class (P.f), 'cell'); %! assert_equal (func2str (P.f{1}), 'sin'); ## Test input validation for 'subsasgn' with '()' indexing %!error ... %! P = table ([1; 2; 3]); %! P(:,3) = {7; 8; 9}; %!error ... %! P = table ([1; 2; 3]); %! P(:,'Row') = {7; 8; 9}; %!error ... %! T = table ([1;2;3]); T(1) = 5; %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); T(1,1) = [9 9]; %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); T(1,1) = {[1 2 3]}; ## 'subsasgn' with '{}' assigns the contents of the variables %!test %! P = table ([1; 2; 3], {'a'; 'b'; 'c'}, [4, 5; 6, 7; 8, 9], ... %! 'VariableNames', {'n', 's', 'm'}); %! P{2,'n'} = 10; %! assert_equal (P.n, [1; 10; 3]); %! P{:,'n'} = 5; %! assert_equal (P.n, [5; 5; 5]); %! P{2,'s'} = {'x'}; %! assert_equal (P.s, {'a'; 'x'; 'c'}); %! P{2,{'n', 'm'}} = [1, 2, 3]; %! assert_equal (P.n, [5; 1; 5]); %! assert_equal (P.m, [4, 5; 2, 3; 8, 9]); %! P{[1, 3],{'n', 'm'}} = [1, 2, 3; 4, 5, 6]; %! assert_equal (P.m, [2, 3; 2, 3; 5, 6]); %!test %! P = table ([1; 2; 3], {'a'; 'b'; 'c'}, [4, 5; 6, 7; 8, 9], ... %! 'VariableNames', {'n', 's', 'm'}); %! P{2,'s'}{1} = 'y'; %! P{2,'m'}(2) = 99; %! assert_equal (P.s, {'a'; 'y'; 'c'}); %! assert_equal (P.m(2,:), [6, 99]); %!test %! P = table ([1, 2; 3, 4]); %! P{:,:} = [5, 6; 7, 8]; %! assert_equal (P.Var1, [5, 6; 7, 8]); %! P{:,:} = 0; %! assert_equal (P.Var1, zeros (2)); %!test %! P = table (categorical ({'p'; 'q'}), string ({'u'; 'v'}), ... %! hours ([1; 2]), [true; false], ... %! 'VariableNames', {'c', 'st', 'du', 'l'}); %! P{2,'c'} = {'new'}; %! assert_equal (cellstr (P.c), {'p'; 'new'}); %! P{2,'st'} = 'x'; %! assert_equal (P.st, string ({'u'; 'x'})); %! P{2,'du'} = 5; %! assert_equal (P.du, [hours(1); days(5)]); %! P{2,'l'} = 7; %! assert_equal (P.l, [true; true]); %!test %! P = table ([1; 2; 3], 'VariableNames', {'n'}); %! P{:,'z'} = [4; 5; 6]; %! P{:,3} = 'abc'; %! assert_equal (P.Properties.VariableNames, {'n', 'z', 'Var3'}); %! assert_equal (P.Var3, ['abc'; 'abc'; 'abc']); %!test %! warning ('off', 'all', 'local'); %! P = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'n', 's'}); %! P{2,'z'} = 5; %! assert_equal (P.z, [0; 5; 0]); %! P{end+1,'n'} = 4; %! assert_equal (P.n, [1; 2; 3; 4]); %! assert_equal (P.s, {'a'; 'b'; 'c'; ''}); %!test %! P = table (); %! P{:,'z'} = [1; 2; 3]; %! assert_equal (size (P), [3, 1]); %!test %! N = table ([3; 4], [5; 6], 'VariableNames', {'p', 'q'}); %! P = table ([1; 2], N, 'VariableNames', {'a', 'nt'}); %! P{2,'nt'} = table (9, 9, 'VariableNames', {'p', 'q'}); %! N = P.nt; %! assert_equal (N.q, [5; 9]); %!test # a single new variable takes a row as every one of its rows %! P = table ([1; 2; 3], 'VariableNames', {'n'}); %! P{:,'z'} = [4, 5]; %! assert_equal (P.z, [4, 5; 4, 5; 4, 5]); %!test %! P = table ([1; 2; 3], 'VariableNames', {'n'}); %! P{:,2} = {'ab'}; %! assert_equal (P.Var2, {'ab'; 'ab'; 'ab'}); %!test # text converts into a double variable %! P = table ([1; 2], [3; 4]); %! P{:,:} = string ({'5', 'abc'; '7', '8'}); %! assert_equal (P.Var1, [5; 7]); %! assert_equal (P.Var2, [NaN; 8]); %!test %! P = table ([1; 2], string ({'c'; 'd'})); %! P{:,:} = string ({'5', 'e'; '6', 'f'}); %! assert_equal (P.Var1, [5; 6]); %! assert_equal (P.Var2, string ({'e'; 'f'})); %!test # each variable gains only the categories assigned to it %! P = table (categorical ({'a'; 'b'}), categorical ({'c'; 'd'})); %! P{:,:} = categorical ({'x', 'y'; 'z', 'w'}); %! assert_equal (categories (P.Var1), {'a'; 'b'; 'x'; 'z'}); %! assert_equal (categories (P.Var2), {'c'; 'd'; 'w'; 'y'}); %!test # a table with neither rows nor variables takes both %! P = table (); %! P{:,:} = [1, 2; 3, 4]; %! assert_equal (P.Properties.VariableNames, {'Var1', 'Var2'}); %! assert_equal (P.Var2, [2; 4]); %!test %! P = table (); %! P{:,:} = {'a', 1; 'b', 2}; %! assert_equal (P.Var1, {'a'; 'b'}); %! assert_equal (P.Var2, {1; 2}); %!test %! P = table (); %! P{:,:} = string ({'a', 'b'; 'c', 'd'}); %! assert_equal (P.Var1, string ({'a'; 'c'})); %!warning ... %! P = table ([1; 2; 3], [4; 5; 6]); %! P{4,'Var1'} = 4; %!error ... %! P = table ([1; 2; 3]); %! P{2,:} = []; %!error ... %! P = table ([1; 2; 3]); %! P{1} = 5; %!error ... %! P = table ([1; 2], [3, 4; 5, 6]); %! P{2,:} = [1, 2]; %!error ... %! P = table (categorical ({'p'; 'q'})); %! P{2,1} = 'new'; %!error ... %! P = table ([1; 2; 3]); %! P{[1, 2],1} = [7; 8; 9]; %!error ... %! P = table ({'a'; 'b'}); %! P{2,1} = 5; %!error ... %! P = table ([1; 2]); %! P{2,1} = {5}; %!error ... %! P = table ([1; 2; 3]); %! P{:,3} = [1; 2; 3]; %!error ... %! P = table ([1; 2], [3; 4]); %! P{:,:} = [5, 6]; %!error ... %! P = table ([1; 2; 3], 'VariableNames', {'n'}); %! P{:,2:3} = [4, 5]; %!error ... %! P = table ([1; 2], [3, 4; 5, 6]); %! P{:,'Var2'} = [7, 8]; %!error ... %! P = table (int8 ([1; 2])); %! P{:,1} = string ({'5'; '6'}); %!error ... %! P = table ([true; false]); %! P{:,1} = string ({'1'; '0'}); %!error ... %! P = table ('Size', [2, 0], 'VariableTypes', {}); %! P{:,:} = [1, 2; 3, 4]; ## 'subsasgn' with '.' adds, replaces, and scalar-expands variables %!test # add a new variable %! T = table ([1;2;3], 'VariableNames', {'x'}); %! T.y = [4;5;6]; %! assert_equal (T.Properties.VariableNames, {'x','y'}); %! assert_equal (T.y, [4;5;6]); %!test # replace an existing variable %! T = table ([1;2;3], 'VariableNames', {'x'}); %! T.x = [7;8;9]; %! assert_equal (T.x, [7;8;9]); %!test # scalar right-hand side is expanded to the table height %! T = table ([1;2;3], 'VariableNames', {'x'}); %! T.x = 5; %! assert_equal (T.x, [5;5;5]); %!test # chained assignment into a single variable %! T = table ([1;2;3], 'VariableNames', {'x'}); %! T.x(2) = 77; %! assert_equal (T.x, [1;77;3]); %!test # the variable dimension name writes every variable %! T = table ([1; 2], [3; 4]); %! T.Variables = [5, 6; 7, 8]; %! assert_equal (T.Properties.VariableNames, {'Var1', 'Var2'}); %! assert_equal (T.Var1, [5; 7]); %! assert_equal (T.Var2, [6; 8]); %!test %! T = table ([1; 2], [3, 4; 5, 6]); %! T.Variables = [7, 8, 9; 10, 11, 12]; %! assert_equal (T.Var2, [8, 9; 11, 12]); %!test %! T = table ([1; 2], [3; 4], 'RowNames', {'r1'; 'r2'}); %! T.Variables = [5, 6; 7, 8]; %! assert_equal (T.Properties.RowNames, {'r1'; 'r2'}); %! assert_equal (T.Var1, [5; 7]); %!test %! T = table ([1; 2], [3; 4]); %! T.Variables(2,1) = 9; %! assert_equal (T.Var1, [1; 9]); %!test # each part converts to its variable's type %! T = table (int8 ([1; 2]), [3; 4]); %! T.Variables = [5.7, 6.5; 7.2, 300]; %! assert_equal (T.Var1, int8 ([6; 7])); %! assert_equal (T.Var2, [6.5; 300]); %!test %! T = table ([1; 2], [3; 4]); %! T.Variables = int8 ([5, 6; 7, 8]); %! assert_equal (T.Var1, [5; 7]); %!test %! T = table ([1; 2], [3; 4]); %! T.Variables = [true, false; false, true]; %! assert_equal (T.Var1, [1; 0]); %!test %! T = table ([1; 2], [true; false]); %! T.Variables = [5, 6; 7, 0]; %! assert_equal (T.Var2, [true; false]); %!test %! T = table ([1; 2], [3; 4]); %! T.Variables = string ({'5', '6'; '7', '8'}); %! assert_equal (T.Var2, [6; 8]); %!test %! T = table ([1; 2], [3; 4]); %! T.Variables = ['ab'; 'cd']; %! assert_equal (T.Var1, [97; 99]); %!test %! T = table ({'a'; 'b'}, {'c'; 'd'}); %! T.Variables = {'e', 'f'; 'g', 'h'}; %! assert_equal (T.Var2, {'f'; 'h'}); %!test %! T = table (string ({'a'; 'b'}), string ({'c'; 'd'})); %! T.Variables = {'e', 'f'; 'g', 'h'}; %! assert_equal (T.Var1, string ({'e'; 'g'})); %!test %! T = table (datetime (2020, 1, [1; 2]), datetime (2021, 1, [1; 2])); %! T.Variables = datetime (2022, 1, [1, 2; 3, 4]); %! assert_equal (T.Var2, datetime (2022, 1, [2; 4])); %!test %! T = table (categorical ({'a'; 'b'}), categorical ({'c'; 'd'})); %! T.Variables = {'a', 'c'; 'b', 'd'}; %! assert_equal (T.Var1, categorical ({'a'; 'b'})); %!test %! T = table (hours ([1; 2]), hours ([3; 4])); %! T.Variables = [5, 6; 7, 8]; %! assert_equal (T.Var1, hours ([120; 168])); %!test # a table with neither rows nor variables takes both %! T = table (); %! T.Variables = [1, 2; 3, 4]; %! assert_equal (T.Var1, [1; 3]); %! assert_equal (T.Var2, [2; 4]); ## Test input validation for 'subsasgn' with '.' indexing %!error ... %! T = table ([1;2;3]); subsasgn (T, struct ('type', '.', 'subs', 5), 1); %!error ... %! T = table (1); T.f = @sin; %!error ... %! T = table (1); T.Var1 = @sin; %!error ... %! T = table (1); T{:,'f'} = @sin; %!error ... %! T = table (1); T{1,'Var1'} = @sin; %!error ... %! T = table (1); T.d = dictionary (1, 2); %!error ... %! T = table (1); T.ts = timeseries (5); %!error ... %! T = table (1); T{1,'x'} = dictionary (1, 2); %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); T.y = [4;5]; %!error ... %! T = table ([1; 2], [3; 4]); T.Variables = 5; %!error ... %! T = table ([1; 2], [3; 4]); T.Variables = [5, 6]; %!error ... %! T = table ([1; 2], [3; 4]); T.Variables = [5, 6; 7, 8; 9, 10]; %!error ... %! T = table ([1; 2], [3; 4]); T.Variables = []; %!error ... %! T = table ([1; 2], [3; 4]); T.Variables = [5, 6, 7; 8, 9, 10]; %!error ... %! T = table ([1; 2], [3; 4]); T.Variables = [5; 7]; %!error ... %! T = table ([1; 2], [3; 4]); T.Variables = {5, 6; 7, 8}; %!error ... %! T = table ([1; 2], {'c'; 'd'}); T.Variables = {5, 'e'; 6, 'f'}; %!error ... %! T = table ('Size', [2, 0], 'VariableTypes', {}); T.Variables = [1, 2; 3, 4]; ## 'subsasgn' sets table-level Properties %!test # Description and UserData %! T = table ([1;2;3], 'VariableNames', {'x'}); %! T.Properties.Description = 'a table'; %! assert_equal (T.Properties.Description, 'a table'); %! T.Properties.UserData = {1, 'two', 3}; %! assert_equal (T.Properties.UserData, {1, 'two', 3}); %!test # DimensionNames, full and indexed %! T = table ([1;2;3], 'VariableNames', {'x'}); %! T.Properties.DimensionNames = {'Rows', 'Vars'}; %! assert_equal (T.Properties.DimensionNames, {'Rows', 'Vars'}); %! T.Properties.DimensionNames(1) = 'Patients'; %! assert_equal (T.Properties.DimensionNames, {'Patients', 'Vars'}); %!test # VariableNames, full and indexed %! T = table ([1;2;3], [4;5;6], 'VariableNames', {'x','y'}); %! T.Properties.VariableNames = {'a', 'b'}; %! assert_equal (T.Properties.VariableNames, {'a', 'b'}); %! T.Properties.VariableNames(2) = {'B'}; %! assert_equal (T.Properties.VariableNames, {'a', 'B'}); %!test # VariableDescriptions and VariableUnits, full and indexed %! T = table ([1;2;3], [4;5;6], 'VariableNames', {'x','y'}); %! T.Properties.VariableDescriptions = {'first', 'second'}; %! assert_equal (T.Properties.VariableDescriptions, {'first', 'second'}); %! T.Properties.VariableDescriptions(1) = {'1st'}; %! assert_equal (T.Properties.VariableDescriptions, {'1st', 'second'}); %! T.Properties.VariableUnits = {'m', 's'}; %! assert_equal (T.Properties.VariableUnits, {'m', 's'}); %! T.Properties.VariableUnits(2) = {'kg'}; %! assert_equal (T.Properties.VariableUnits, {'m', 'kg'}); %!test # VariableTypes converts the underlying data (whole table) %! T = table ([1;2;3], [10;20;30], 'VariableNames', {'x','z'}); %! T.Properties.VariableTypes = {'single', 'int32'}; %! assert_equal (class (T.x), 'single'); %! assert_equal (class (T.z), 'int32'); %! assert_equal (T.x, single ([1;2;3])); %! assert_equal (T.z, int32 ([10;20;30])); %! assert_equal (T.Properties.VariableTypes, {'single', 'int32'}); %! assert_equal (T.Properties.VariableDescriptions, {}); %!test # VariableTypes converts a single indexed variable %! T = table ([1;2;3], [10;20;30], 'VariableNames', {'x','z'}); %! T.Properties.VariableTypes(2) = {'single'}; %! assert_equal (class (T.x), 'double'); %! assert_equal (class (T.z), 'single'); %! assert_equal (T.Properties.VariableTypes, {'double', 'single'}); %!test # RowNames, set from an array and removed with [] %! T = table ([1;2;3], 'VariableNames', {'x'}); %! T.Properties.RowNames = {'r1', 'r2', 'r3'}; %! assert_equal (T.Properties.RowNames, {'r1';'r2';'r3'}); %! T.Properties.RowNames = {}; %! assert_equal (isempty (T.Properties.RowNames), true); ## Test 'Properties' of an empty table yields a value in argument position %!test %! assert_equal (class (table ().Properties), ... %! 'datatypes.tabular.TableProperties'); %! assert_equal (isa (table ().Properties, ... %! 'datatypes.tabular.TabularProperties'), true); ## Test 'Properties' of a zero-row table yields a value in argument position %!test %! Z = table (zeros (0, 1), 'VariableNames', {'v'}); %! assert_equal (class (Z.Properties), 'datatypes.tabular.TableProperties'); %! assert_equal (Z.Properties.VariableNames, {'v'}); ## Test 'Properties' of an empty table carries the same fields as a full one %!test %! assert_equal (properties (table ().Properties), ... %! properties (table ([1; 2]).Properties)); ## Test 'VariableContinuity' defaults to empty %!test %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! assert_equal (T.Properties.VariableContinuity, []); %! assert_equal (class (T.Properties.VariableContinuity), 'double'); ## Test 'VariableContinuity' accepts a cellstr and a string array %!test %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! T.Properties.VariableContinuity = {'continuous', 'step'}; %! assert_equal (T.Properties.VariableContinuity, {'continuous', 'step'}); %! T.Properties.VariableContinuity = string ({'event', 'unset'}); %! assert_equal (T.Properties.VariableContinuity, {'event', 'unset'}); ## Test 'VariableContinuity' is cleared by an empty value %!test %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! T.Properties.VariableContinuity = {'continuous', 'step'}; %! T.Properties.VariableContinuity = {}; %! assert_equal (T.Properties.VariableContinuity, []); %! T.Properties.VariableContinuity = {'continuous', 'step'}; %! T.Properties.VariableContinuity = []; %! assert_equal (T.Properties.VariableContinuity, []); ## Test 'VariableContinuity' follows its variables when they are subset %!test %! T = table ([1; 2], [3; 4], [5; 6], 'VariableNames', {'a', 'b', 'c'}); %! T.Properties.VariableContinuity = {'continuous', 'step', 'event'}; %! assert_equal (T(:,[1, 3]).Properties.VariableContinuity, ... %! {'continuous', 'event'}); %! assert_equal (removevars (T, 'a').Properties.VariableContinuity, ... %! {'step', 'event'}); %! assert_equal (movevars (T, 'a', 'After', ... %! 'b').Properties.VariableContinuity, ... %! {'step', 'continuous', 'event'}); ## Test 'VariableContinuity' gains 'unset' for a variable added by addvars %!test %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! T.Properties.VariableContinuity = {'continuous', 'step'}; %! A = addvars (T, [5; 6], 'NewVariableNames', {'c'}); %! assert_equal (A.Properties.VariableContinuity, {'continuous', 'step', ... %! 'unset'}); %! B = addvars (T, [5; 6], 'Before', 'a', 'NewVariableNames', {'z'}); %! assert_equal (B.Properties.VariableContinuity, {'unset', 'continuous', ... %! 'step'}); ## Test 'VariableContinuity' stays unset when the table has none %!test %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! A = addvars (T, [5; 6], 'NewVariableNames', {'c'}); %! assert_equal (A.Properties.VariableContinuity, []); ## Test 'VariableContinuity' gains 'unset' for horizontally added variables %!test %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! T.Properties.VariableContinuity = {'continuous', 'step'}; %! H = [T, table([5; 6], 'VariableNames', {'z'})]; %! assert_equal (H.Properties.VariableContinuity, {'continuous', 'step', ... %! 'unset'}); ## Test 'VariableContinuity' takes the first non-empty operand in vertcat %!test %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! S = T; %! S.Properties.VariableContinuity = {'continuous', 'step'}; %! assert_equal (vertcat (S, T).Properties.VariableContinuity, ... %! {'continuous', 'step'}); %! assert_equal (vertcat (T, S).Properties.VariableContinuity, ... %! {'continuous', 'step'}); ## Test 'VariableContinuity' is replicated by repmat and repelem %!test %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! T.Properties.VariableContinuity = {'continuous', 'step'}; %! assert_equal (repmat (T, 1, 2).Properties.VariableContinuity, ... %! {'continuous', 'step', 'continuous', 'step'}); %! assert_equal (repelem (T, 1, 2).Properties.VariableContinuity, ... %! {'continuous', 'continuous', 'step', 'step'}); ## Test 'VariableContinuity' of a merged variable is unset %!test %! T = table ([1; 2], [3; 4], [5; 6], 'VariableNames', {'a', 'b', 'c'}); %! T.Properties.VariableContinuity = {'continuous', 'step', 'event'}; %! M = mergevars (T, {'a', 'b'}); %! assert_equal (M.Properties.VariableContinuity, {'unset', 'event'}); ## Test 'VariableContinuity' is reported by summary %!test %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! T.Properties.VariableContinuity = {'continuous', 'step'}; %! assert_equal (isfield (summary (T).(T.Properties.VariableNames{1}), ... %! 'Continuity'), true); ## Test 'VariableContinuity' can be assigned per variable %!test %! T = table ([1; 2], [3; 4], 'VariableNames', {'a', 'b'}); %! T.Properties.VariableContinuity = {'continuous', 'step'}; %! T.Properties.VariableContinuity(2) = {'event'}; %! assert_equal (T.Properties.VariableContinuity, {'continuous', 'event'}); ## Test input validation for 'VariableContinuity' assignment %!error ... %! T = table ([1; 2], [3; 4]); T.Properties.VariableContinuity = {'step'}; %!error ... %! T = table ([1; 2], [3; 4]); T.Properties.VariableContinuity = {'bogus', ... %! 'step'}; %!error ... %! T = table ([1; 2], [3; 4]); T.Properties.VariableContinuity = 'continuous'; %!error ... %! T = table ([1; 2], [3; 4]); T.Properties.VariableContinuity(5) = {'step'}; ## Test 'Properties' is a TableProperties object %!test %! T = table ([1; 2], 'VariableNames', {'x'}); %! assert_equal (class (T.Properties), 'datatypes.tabular.TableProperties'); %! assert_equal (isa (T.Properties, 'datatypes.tabular.TabularProperties'), ... %! true); ## Test 'Properties' no longer exposes the internal variable storage %!test %! T = table ([1; 2], 'VariableNames', {'x'}); %! assert_equal (any (strcmp (properties (T.Properties), 'VariableValues')), ... %! false); ## Test the properties listing writes out a row of names %!test %! fv = @(v) datatypes.tabular.TabularProperties.formatValue (v); %! assert_equal (fv ({'a', 'b'}), "{'a' 'b'}"); ## Test the properties listing summarizes a column of names by its size %!test %! fv = @(v) datatypes.tabular.TabularProperties.formatValue (v); %! assert_equal (fv ({'a'; 'b'}), '{2x1 cell}'); ## Test the properties listing summarizes a 2-D cell array of names by its size %!test %! fv = @(v) datatypes.tabular.TabularProperties.formatValue (v); %! assert_equal (fv ({'a', 'b'; 'c', 'd'}), '{2x2 cell}'); ## Test 'RowNames' of a multi-row table is listed by its size %!test %! T = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', {'r1', 'r2'}); %! fv = @(v) datatypes.tabular.TabularProperties.formatValue (v); %! assert_equal (fv (T.Properties.RowNames), '{2x1 cell}'); ## Test the properties listing writes a non-cell array in square brackets %!test %! fv = @(v) datatypes.tabular.TabularProperties.formatValue (v); %! assert_equal (fv (hours (1:3)'), '[3x1 duration]'); ## Test 'Properties' lists its names in the order they are displayed %!test %! T = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', {'r1', 'r2'}); %! assert_equal (properties (T.Properties), ... %! {'Description'; 'UserData'; 'DimensionNames'; ... %! 'VariableNames'; 'VariableTypes'; 'VariableDescriptions'; ... %! 'VariableUnits'; 'VariableContinuity'; 'RowNames'; ... %! 'CustomProperties'}); ## Test 'fieldnames' of 'Properties' agrees with 'properties' %!test %! T = table ([1; 2], 'VariableNames', {'x'}, 'RowNames', {'r1', 'r2'}); %! assert_equal (fieldnames (T.Properties), properties (T.Properties)); ## Test a table property may be named with a string scalar %!test %! T = table ([1; 2], 'VariableNames', {'x'}); %! s = struct ('type', '.', 'subs', string ('VariableNames')); %! assert_equal (subsref (T.Properties, s), {'x'}); ## Test a table property name must be a character vector or a string scalar %!error ... %! T = table ([1; 2], 'VariableNames', {'x'}); %! subsref (T.Properties, struct ('type', '.', 'subs', 5)); ## Test a custom property may be named with a string scalar %!test %! T = addprop (table ([1; 2]), 'tp', 'table'); %! T.Properties.CustomProperties.tp = 7; %! s = struct ('type', '.', 'subs', string ('tp')); %! assert_equal (subsref (T.Properties.CustomProperties, s), 7); ## Test a custom property name must be a character vector or a string scalar %!error ... %! T = addprop (table ([1; 2]), 'tp', 'table'); %! subsref (T.Properties.CustomProperties, struct ('type', '.', 'subs', 5)); ## Test 'CustomProperties' is a CustomProperties object %!test %! T = table ([1; 2], 'VariableNames', {'x'}); %! assert_equal (class (T.Properties.CustomProperties), ... %! 'datatypes.tabular.CustomProperties'); %! assert_equal (isempty (T.Properties.CustomProperties), true); %! assert_equal (isempty (fieldnames (T.Properties.CustomProperties)), true); ## Test 'CustomProperties' is not empty once a property is added %!test %! T = addprop (table ([1; 2], 'VariableNames', {'x'}), 'Extra', 'table'); %! assert_equal (isempty (T.Properties.CustomProperties), false); %! assert_equal (fieldnames (T.Properties.CustomProperties), {'Extra'}); ## Test 'properties' of the store names the custom properties that are set %!test %! T = addprop (table ([1; 2], 'VariableNames', {'x'}), 'Extra', 'table'); %! assert_equal (properties (T.Properties.CustomProperties), {'Extra'}); ## Test 'properties' of an empty store is an empty column %!test %! T = table ([1; 2], 'VariableNames', {'x'}); %! assert_equal (properties (T.Properties.CustomProperties), cell (0, 1)); ## Test 'properties' of the store keeps the order the properties were added in %!test %! T = addprop (table ([1; 2], 'VariableNames', {'x'}), {'zz', 'aa'}, ... %! {'table', 'table'}); %! assert_equal (properties (T.Properties.CustomProperties), {'zz'; 'aa'}); ## Test a custom property round trips through the object %!test %! T = addprop (table ([1; 2], 'VariableNames', {'x'}), 'Extra', 'table'); %! T.Properties.CustomProperties.Extra = 'xyz'; %! assert_equal (T.Properties.CustomProperties.Extra, 'xyz'); %! assert_equal (fieldnames (T.Properties.CustomProperties), {'Extra'}); %! assert_equal (isempty (T.Properties.CustomProperties), false); ## Test input validation for 'CustomProperties' indexing %!error ... %! T = addprop (table ([1; 2]), 'Extra', ... %! 'table'); T.Properties.CustomProperties.Nope; %!error ... %! T = addprop (table ([1; 2]), 'Extra', ... %! 'table'); T.Properties.CustomProperties(1); ## Test input validation for 'subsasgn' Properties assignment %!error ... %! T = table ([1;2;3]); subsasgn (T, substruct ('.', 'Properties'), 5); %!error ... %! T = table ([1;2;3]); T.Properties.Description = 5; %!error ... %! T = table ([1;2;3]); T.Properties.Description = string ({'a', 'b'}); %!error ... %! T = table ([1;2;3], [4;5;6], 'VariableNames', {'x','y'}); ... %! T.Properties.VariableNames = {'only_one'}; %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); ... %! T.Properties.VariableNames(5) = {'a'}; %!error ... %! T = table ([1;2;3], [4;5;6], 'VariableNames', {'x','y'}); ... %! T.Properties.VariableTypes = {'double'}; %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); ... %! T.Properties.RowNames = {'r1', 'r2'}; %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); ... %! T.Properties.RowNames = {'r1', 'r1', 'r2'}; %!test # RowNames set from a variable with unique values moves and drops it %! T = table ({'x'; 'y'; 'z'}, [1; 2; 3], 'VariableNames', {'G', 'V'}); %! T.Properties.RowNames = 'G'; %! assert_equal (T.Properties.RowNames, {'x'; 'y'; 'z'}); %! assert_equal (T.Properties.VariableNames, {'V'}); %!error ... %! T = table ({'x'; 'y'; 'x'}, [1; 2; 3], 'VariableNames', {'G', 'V'}); ... %! T.Properties.RowNames = 'G'; ## 'subsasgn' sets custom properties through Properties.CustomProperties %!test # table-scoped and variable-scoped custom properties %! T = table ([1;2;3], [4;5;6], 'VariableNames', {'x','y'}); %! T = addprop (T, {'tp', 'vp'}, {'table', 'variable'}); %! T.Properties.CustomProperties.tp = 'meta'; %! assert_equal (T.Properties.CustomProperties.tp, 'meta'); %! T.Properties.CustomProperties.vp = [10 20]; %! assert_equal (T.Properties.CustomProperties.vp, [10 20]); %! T.Properties.CustomProperties.vp(2) = 99; %! assert_equal (T.Properties.CustomProperties.vp, [10 99]); ## a table-scoped custom property holds a matrix, stored as given %!test %! T = table ([1;2;3], 'VariableNames', {'x'}); %! T = addprop (T, 'tp', 'table'); %! T.Properties.CustomProperties.tp = magic (3); %! assert_equal (T.Properties.CustomProperties.tp, magic (3)); ## a table-scoped custom property holds a cell array of character vectors %!test %! T = table ([1;2;3], 'VariableNames', {'x'}); %! T = addprop (T, 'tp', 'table'); %! T.Properties.CustomProperties.tp = {'a', 'b', 'c'}; %! assert_equal (T.Properties.CustomProperties.tp, {'a', 'b', 'c'}); ## a table-scoped custom property holds another table %!test %! T = table ([1;2;3], 'VariableNames', {'x'}); %! T = addprop (T, 'tp', 'table'); %! T.Properties.CustomProperties.tp = table ([1; 2]); %! assert_equal (height (T.Properties.CustomProperties.tp), 2); ## a table-scoped custom property holds a multi-element string array %!test %! T = table ([1;2;3], 'VariableNames', {'x'}); %! T = addprop (T, 'tp', 'table'); %! T.Properties.CustomProperties.tp = string ({'a', 'b'}); %! assert_equal (numel (T.Properties.CustomProperties.tp), 2); %!test # a new variable pads each variable property with a missing value %! T = table ([1; 2], [3; 4]); %! T = addprop (T, {'c', 'd', 's'}, {'variable', 'variable', 'variable'}); %! T.Properties.CustomProperties.c = categorical ({'a', 'b'}); %! T.Properties.CustomProperties.d = datetime (2020, 1, [1, 2]); %! T.Properties.CustomProperties.s = struct ('f', {1, 2}); %! T.x = [5; 6]; %! assert_equal (isundefined (T.Properties.CustomProperties.c), ... %! [false, false, true]); %! assert_equal (isnat (T.Properties.CustomProperties.d), [false, false, true]); %! assert_equal (isempty (T.Properties.CustomProperties.s(3).f), true); ## Test input validation for 'subsasgn' custom property assignment %!error ... %! T = table ([1;2;3]); T.Properties.CustomProperties.nope = 5; ## only a custom property store can be assigned to CustomProperties %!error ... %! T = table ([1;2;3]); T.Properties.CustomProperties = 5; %!error ... %! T = table ([1;2;3]); T.Properties.CustomProperties = []; ## the custom property store is replaced whole, not merged %!test %! A = addprop (table ([1;2;3], 'VariableNames', {'x'}), 'tp', 'table'); %! A.Properties.CustomProperties.tp = 7; %! B = addprop (table ([1;2;3], 'VariableNames', {'x'}), 'vp', 'variable'); %! B.Properties.CustomProperties = A.Properties.CustomProperties; %! assert_equal (fieldnames (B.Properties.CustomProperties), {'tp'}); ## a copied variable property is still a variable property %!test %! A = addprop (table ([1;2;3], [4;5;6], [7;8;9]), 'vp', 'variable'); %! A.Properties.CustomProperties.vp = [1 2 3]; %! B = table ([1;2;3], [4;5;6], [7;8;9]); %! B.Properties.CustomProperties = A.Properties.CustomProperties; %! B = removevars (B, 2); %! assert_equal (B.Properties.CustomProperties.vp, [1 3]); ## a 0-by-0 empty variable property fits a table of any width %!test %! A = addprop (table ([1;2;3]), 'vp', 'variable'); %! A.Properties.CustomProperties.vp = []; %! B = table ([1;2;3], [4;5;6], [7;8;9]); %! B.Properties.CustomProperties = A.Properties.CustomProperties; %! assert_equal (B.Properties.CustomProperties.vp, []); ## assigning the store to itself changes nothing %!test %! A = addprop (table ([1;2;3], 'VariableNames', {'x'}), 'tp', 'table'); %! A.Properties.CustomProperties.tp = 7; %! A.Properties.CustomProperties = A.Properties.CustomProperties; %! assert_equal (A.Properties.CustomProperties.tp, 7); ## a variable property that does not fit the table is refused %!error ... %! A = addprop (table ([1;2;3]), 'vp', 'variable'); %! A.Properties.CustomProperties.vp = 1; %! B = table ([1;2;3], [4;5;6], [7;8;9]); %! B.Properties.CustomProperties = A.Properties.CustomProperties; ## Test deleting rows and variables by assigning [] %!test # a row goes by index, by range and by logical mask %! T = table ([1;2;3;4;5], [6;7;8;9;10], 'VariableNames', {'A', 'B'}); %! R = T; R(1,:) = []; %! assert_equal (R.A, [2;3;4;5]); %! R = T; R(2:3,:) = []; %! assert_equal (R.A, [1;4;5]); %! R = T; R(logical ([1, 0, 1, 0, 0]),:) = []; %! assert_equal (R.A, [2;4;5]); %!test # deleting a row shrinks the row names with it %! T = table ([1;2;3], 'VariableNames', {'A'}, 'RowNames', {'a', 'b', 'c'}); %! R = T; R('b',:) = []; %! assert_equal (R.Properties.RowNames, {'a'; 'c'}); %! assert_equal (R.A, [1;3]); %!test # an empty row reference removes nothing, a repeated one removes once %! T = table ([1;2;3;4;5], 'VariableNames', {'A'}); %! R = T; R([],:) = []; %! assert_equal (R.A, [1;2;3;4;5]); %! R = T; R([2, 2],:) = []; %! assert_equal (R.A, [1;3;4;5]); %!test # with both subscripts ':' the rows go and the variables stay %! T = table ([1;2;3], [4;5;6], 'VariableNames', {'A', 'B'}); %! R = T; R(:,:) = []; %! assert_equal (size (R), [0, 2]); %! assert_equal (R.Properties.VariableNames, {'A', 'B'}); %!test # a variable goes by index, by name, by cellstr and by logical mask %! T = table ([1;2], [3;4], 'VariableNames', {'A', 'B'}); %! R = T; R(:,1) = []; %! assert_equal (R.Properties.VariableNames, {'B'}); %! R = T; R(:,'A') = []; %! assert_equal (R.Properties.VariableNames, {'B'}); %! R = T; R(:,{'A'}) = []; %! assert_equal (R.Properties.VariableNames, {'B'}); %! R = T; R(:,logical ([1, 0])) = []; %! assert_equal (R.Properties.VariableNames, {'B'}); %!test # an empty variable reference removes nothing %! T = table ([1;2], [3;4], 'VariableNames', {'A', 'B'}); %! R = T; R(:,[]) = []; %! assert_equal (size (R), [2, 2]); %!test # a variable goes by dot assignment, and the last one leaves the height %! T = table ([1;2;3;4;5], [6;7;8;9;10], 'VariableNames', {'A', 'B'}); %! R = T; R.A = []; %! assert_equal (R.Properties.VariableNames, {'B'}); %! R.B = []; %! assert_equal (size (R), [5, 0]); %!test # per-variable metadata goes with the variable it describes %! T = table ([1;2], [3;4], 'VariableNames', {'A', 'B'}); %! T.Properties.VariableUnits = {'kg', 'm'}; %! T.Properties.VariableDescriptions = {'mass', 'length'}; %! T(:,'A') = []; %! assert_equal (T.Properties.VariableUnits, {'m'}); %! assert_equal (T.Properties.VariableDescriptions, {'length'}); ## Test input validation for deleting by assigning [] %!error ... %! T = table ([1;2], [3;4]); T(1,1) = []; %!error ... %! T = table ([1;2], [3;4]); T.Row = []; %!error ... %! T = table ([1;2], [3;4]); T.Q = []; %!error ... %! T = table ([1;2], [3;4]); T(6,:) = []; %!error ... %! T = table ([1;2], [3;4]); T(:,3) = []; ## Restore whatever default display formats the user had before this file ran. %!test %! datetimeFormatGuard ('restore'); ## Test 'sortrows' -- the DIRECTION value is matched without regard to case %!assert_equal (sortrows (table ([3; 1; 2]), 1, 'Descend').Var1', [3 2 1]) %!assert_equal (sortrows (table ([3; 1; 2]), 1, 'ASCEND').Var1', [1 2 3]) ## the shared 'tabular' machinery, reached through a table. Its messages ## take the class name from the subclass, so they read 'table' here %!test %! N = table ([1; 2], 'VariableNames', {'n'}); %! N.sub = table ([3; 4], [5; 6]); %! assert_equal (isequaln (N{:, :}, table2cell (N)), true); %!error ... %! T = table ([1;2], 'RowNames', {'a', 'b'}); T(datetime (2024, 1, 1),:); %!error ... %! T = table ([1; 2], 'VariableNames', {'n'}); %! T(struct ('a', 1), :); %!error ... %! T = table ([1; 2], 'VariableNames', {'n'}); %! subsasgn (T, substruct ('()', {1}, '.', 'n'), 5); ## Test variables of different types cannot form one array %!test # numbers, logicals and characters concatenate with one another %! T = table ([1;2], int8 ([3;4]), logical ([1;0]), ... %! 'VariableNames', {'a', 'b', 'c'}); %! assert_equal (size (table2array (T)), [2, 3]); %! assert_equal (class (table2array (T)), 'int8'); %!test # variables of one non-numeric type concatenate with each other %! T = table (categorical ({'x';'y'}), categorical ({'a';'b'})); %! assert_equal (class (table2array (T)), 'categorical'); %!test # a number and a categorical give a cell array %! T = table ([1; 2], categorical ({'x'; 'y'})); %! assert_equal (table2array (T), table2cell (T)); %!test # a number beside a duration counts days %! T = table ([1; 2], duration ([1; 2], 0, 0)); %! assert_equal (table2array (T), hours ([24, 1; 48, 2])); %!test # a string promotes a number beside it, as concatenation does %! T = table ([1;2;3], string ({'a';'b';'c'})); %! assert_equal (class (table2array (T)), 'string'); %! assert_equal (size (table2array (T)), [3, 2]); %!test # a string and a categorical give a cell array %! T = table (string ({'a'; 'b'}), categorical ({'x'; 'y'})); %! assert_equal (table2array (T), table2cell (T)); ## table2array gives a cell array for a nested table, writetable refuses it %!test %! N = table ([1; 2], 'VariableNames', {'n'}); %! N.sub = table ([3; 4], [5; 6]); %! assert_equal (isequaln (table2array (N), table2cell (N)), true); %!error ... %! N = table ([1; 2], 'VariableNames', {'n'}); %! N.sub = table ([3; 4], [5; 6]); %! writetable (N, [tempname() '.csv']); ## the spreadsheet writers validate their file name, type, sheet and delimiter %!error ... %! N = table ([1; 2], 'VariableNames', {'n'}); %! N.sub = table ([3; 4], [5; 6]); %! table2ods (N, [tempname() '.ods'], 'Sheet', 5); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! writetable (T, 42); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! writetable (T, 'x.zzz'); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! writetable (T, 'x.csv', 'FileType', 'nope'); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! writetable (T, [tempname() '.xlsx'], 'Sheet', 5); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! writetable (T, 'x.csv', 'Delimiter', 42); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! writetable (T, 'x.csv', 'Delimiter', 'nope'); ## ismissing, rmmissing and fillmissing validate their options %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! ismissing (T, 'OutputFormat', 'nope'); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! ismissing (T, ones (2, 2)); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! ismissing (T, {[1, 2]}); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! rmmissing (T, 'MissingLocations', true (3, 3)); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! rmmissing (T, 'MissingLocations', 'nope'); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! fillmissing (T, 'constant', 0, 'ReplaceValues', [1, 2]); ## findgroups and splitapply on a table %!error ... %! T = table ([1, 2; 1, 2; 3, 4], [5; 6; 7], 'VariableNames', {'A', 'V'}); %! findgroups (T(:, 'A')); %!error ... %! findgroups (table ()); %!error ... %! findgroups (table ({{1}; {2}}, 'VariableNames', {'v'})); %!test # a nested table variable passes its rows %! N = table ([1; 2; 3], 'VariableNames', {'p'}); %! C = splitapply (@(a) height (a), table (N), [1; 1; 2]); %! assert_equal (C, [2; 1]); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! splitapply (5, T, [1; 2]); %!error ... %! splitapply (@mean, table ([1; 2], 'VariableNames', {'n'})); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! splitapply (@(x) 1, T, [1; 2; 3]); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! splitapply (@(x) 1, T, [0; 1]); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! splitapply (@(x) 1, T, [1; 3]); ## varfun and rowfun validate their Name/Value options %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! varfun (@(x) x, T, 'InputVariables', 'n', 'ErrorHandler', 5); %!error ... %! varfun (@sum, table ([1; 2]), 'Bogus', 1) %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! varfun (@(x) x, T, 'InputVariables', 'n', 'OutputFormat', 5); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! varfun (@(x) x, T, 'InputVariables', 'n', 'OutputFormat', 'timetable'); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! rowfun (@(x) x, T, 'InputVariables', 'n', 'ExtractCellContents', [1, 2]); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! rowfun (@(x) x, T, 'InputVariables', 'n', 'ErrorHandler', 5); %!error ... %! rowfun (@(x) x, table ([1; 2]), 'Bogus', 1) %!error ... %! rowfun (@(x) 1, table ()); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! rowfun (@(x) x, T, 'InputVariables', 'n', 'NumOutputs', -1); ## the grouping methods validate their group options %!error ... %! G = table ([1; 1], [1; 2], 'VariableNames', {'g', 'v'}); %! groupsummary (G, 'g', 'mean', 'IncludeMissingGroups', [1, 2]); %!error ... %! G = table ([1; 1], [1; 2], 'VariableNames', {'g', 'v'}); %! groupsummary (G, 'g', 'mean', 'IncludeEmptyGroups', [1, 2]); %!error ... %! G = table ([1; 1; 2], [1; 2; 3], 'VariableNames', {'g', 'v'}); %! groupsummary (G, 'g', @(x) 1:numel (x), 'v'); %!error ... %! G = table ([1; 1], [1; 2], 'VariableNames', {'g', 'v'}); %! groupcounts (G, 'g', 'IncludeMissingGroups', [1, 2]); %!error ... %! groupcounts (table ([1; 1], 'VariableNames', {'g'})); %!error ... %! G = table ([1; 1], [1; 2], 'VariableNames', {'g', 'v'}); %! groupcounts (G, 'g', 'IncludeEmptyGroups', [1, 2]); %!error ... %! G = table ([1; 1], [1; 2], 'VariableNames', {'g', 'v'}); %! pivot (G, 5); %!error ... %! G = table ([1; 1], [1; 2], 'VariableNames', {'g', 'v'}); %! pivot (G, 'Rows', 'g', 'DataVariable', 'v', 'Method', 5); ## subsasgn validates every metadata property, its index shape and its type %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'Description'), 5); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'Description'), ... %! string ({'a', 'b'})); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'DimensionNames', '()', ... %! {1, 1}), {'a'}); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'DimensionNames', '()', ... %! {5}), {'a'}); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'DimensionNames', '()', ... %! {1}), 5); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'DimensionNames'), {'a'}); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableNames', '()', ... %! {1, 1}), {'a'}); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableNames'), 5); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableTypes', '()', ... %! {1, 1}), {'a'}); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableTypes', '()', ... %! {5}), {'a'}); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableTypes'), 5); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableDescriptions', ... %! '()', {1, 1}), {'a'}); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableDescriptions', ... %! '()', {5}), {'a'}); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableDescriptions', ... %! '()', {1}), 5); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableDescriptions'), 5); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableUnits', '()', ... %! {1, 1}), {'a'}); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableUnits', '()', ... %! {5}), {'a'}); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableUnits', '()', ... %! {1}), 5); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableUnits'), 5); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableContinuity', ... %! '()', {1, 1}), {'unset'}); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableContinuity'), 5); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! subsasgn (T, substruct ('.', 'Properties', '.', 'VariableContinuity'), ... %! "unset"); ## and the custom properties, which are scalar per table or one value ## per variable %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! T = addprop (T, 'tp', 'table'); %! subsasgn (T, substruct ('.', 'Properties', '.', 'CustomProperties', '()', ... %! {1}), 1); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! T = addprop (T, 'tp', 'table'); %! subsasgn (T, substruct ('.', 'Properties', '.', 'CustomProperties', '.', ... %! 'tp', '()', {1}), 1); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! T = addprop (T, 'vp', 'variable'); %! subsasgn (T, substruct ('.', 'Properties', '.', 'CustomProperties', '.', ... %! 'vp', '()', {1, 1}), {1}); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! T = addprop (T, 'vp', 'variable'); %! subsasgn (T, substruct ('.', 'Properties', '.', 'CustomProperties', '.', ... %! 'vp', '()', {5}), {1}); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! T = addprop (T, 'vp', 'variable'); %! subsasgn (T, substruct ('.', 'Properties', '.', 'CustomProperties', '.', ... %! 'vp', '()', {1}), {1, 2}); %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! T = addprop (T, 'vp', 'variable'); %! subsasgn (T, substruct ('.', 'Properties', '.', 'CustomProperties', '.', ... %! 'vp'), 5); ## writetable refuses 'Range' when merging into a file that already exists %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! fn = [tempname() '.xlsx']; %! unwind_protect %! writetable (T, fn); %! writetable (T, fn, 'Range', 'A1', 'WriteMode', 'inplace'); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect ## writetable refuses a variable of a type it cannot write, text or sheet %!error ... %! writetable (table (inputParser ()), [tempname() '.csv']); %!error ... %! writetable (table (inputParser ()), [tempname() '.xlsx']); ## a custom variable property takes one value per variable, as a vector %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! T = addprop (T, 'vp', 'variable'); %! subsasgn (T, substruct ('.', 'Properties', '.', 'CustomProperties', '.', ... %! 'vp'), ones (2, 2)); ## a 0-by-0 empty clears a custom variable property whatever the table's width %!test %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! T = addprop (T, 'vp', 'variable'); %! T.Properties.CustomProperties.vp = [10 20]; %! T.Properties.CustomProperties.vp = []; %! assert_equal (T.Properties.CustomProperties.vp, []); ## an empty of any other class clears it too, and is stored as [] %!test %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! T = addprop (T, 'vp', 'variable'); %! T.Properties.CustomProperties.vp = {}; %! assert_equal (T.Properties.CustomProperties.vp, []); ## a cleared custom variable property stays empty when a variable is added %!test %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! T = addprop (T, 'vp', 'variable'); %! T.Properties.CustomProperties.vp = []; %! T = addvars (T, [3; 4]); %! assert_equal (T.Properties.CustomProperties.vp, []); ## a character vector is not a per-variable value, whatever its size %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! T = addprop (T, 'vp', 'variable'); %! subsasgn (T, substruct ('.', 'Properties', '.', 'CustomProperties', '.', ... %! 'vp'), ''); ## an indexed assignment re-expands a cleared property to the table's width %!test %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! T = addprop (T, 'vp', 'variable'); %! T.Properties.CustomProperties.vp = []; %! T.Properties.CustomProperties.vp(2) = 5; %! assert_equal (T.Properties.CustomProperties.vp, [NaN, 5]); ## a cleared property re-expands as a cell when a cell value is assigned %!test %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! T = addprop (T, 'vp', 'variable'); %! T.Properties.CustomProperties.vp = []; %! T.Properties.CustomProperties.vp(2) = {'x'}; %! assert_equal (T.Properties.CustomProperties.vp, {[], 'x'}); ## a logical subscript assigns to the variables it marks %!test %! T = table ([1; 2], {'a'; 'b'}, [3; 4], 'VariableNames', {'n', 'c', 'd'}); %! T = addprop (T, 'vp', 'variable'); %! T.Properties.CustomProperties.vp = [1 2 3]; %! T.Properties.CustomProperties.vp(logical ([1 0 1])) = [9 9]; %! assert_equal (T.Properties.CustomProperties.vp, [9 2 9]); ## a logical mask longer than the table is out of bounds %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! T = addprop (T, 'vp', 'variable'); %! T.Properties.CustomProperties.vp = [1 2]; %! subsasgn (T, substruct ('.', 'Properties', '.', 'CustomProperties', '.', ... %! 'vp', '()', {logical([1 0 1])}), 5); ## how a property is indexed is settled before what is assigned to it %!error ... %! T = table ([1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! T = addprop (T, 'vp', 'variable'); %! subsasgn (T, substruct ('.', 'Properties', '.', 'CustomProperties', '.', ... %! 'vp', '.', 'x'), ones (2, 2)); ## a '.' subscript may name a variable with a string scalar %!test %! T = table ([1;2;3], [4;5;6], 'VariableNames', {'x', 'y'}); %! T = subsasgn (T, struct ('type', '.', 'subs', string ('x')), [7;8;9]); %! assert_equal (T.x, [7;8;9]); ## the same on the reading side %!test %! T = table ([1;2;3], [4;5;6], 'VariableNames', {'x', 'y'}); %! assert_equal (subsref (T, struct ('type', '.', 'subs', string ('x'))), ... %! [1;2;3]); ## a string scalar names 'Properties' %!test %! T = table ([1;2;3], 'VariableNames', {'x'}); %! s = struct ('type', {'.', '.'}, 'subs', {string('Properties'), ... %! 'Description'}); %! T = subsasgn (T, s, 'meta'); %! assert_equal (T.Properties.Description, 'meta'); ## a string scalar names a property field %!test %! T = table ([1;2;3], 'VariableNames', {'x'}); %! s = struct ('type', {'.', '.'}, 'subs', {'Properties', ... %! string('Description')}); %! T = subsasgn (T, s, 'meta'); %! assert_equal (T.Properties.Description, 'meta'); ## a string scalar names a custom property %!test %! T = addprop (table ([1;2;3], 'VariableNames', {'x'}), 'tp', 'table'); %! s = struct ('type', {'.', '.', '.'}, ... %! 'subs', {'Properties', 'CustomProperties', string('tp')}); %! T = subsasgn (T, s, 42); %! assert_equal (T.Properties.CustomProperties.tp, 42); ## a string scalar names a variable through a chained subscript %!test %! T = table ([1;2;3], 'VariableNames', {'x'}); %! s = struct ('type', {'.', '()'}, 'subs', {string('x'), {2}}); %! T = subsasgn (T, s, 99); %! assert_equal (T.x, [1;99;3]); ## a string array of more than one element is not a field name %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); %! subsasgn (T, struct ('type', '.', 'subs', string ({'x', 'y'})), 1); %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); %! subsref (T, struct ('type', '.', 'subs', string ({'x', 'y'}))); %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); %! subsasgn (T, struct ('type', {'.', '()'}, 'subs', {string({'x','y'}), ... %! {2}}), 1); %!error ... %! T = addprop (table ([1;2;3], 'VariableNames', {'x'}), 'tp', 'table'); %! s = struct ('type', {'.', '.', '.'}, ... %! 'subs', {'Properties', 'CustomProperties', string({'a','b'})}); %! subsasgn (T, s, 1); ## an empty character array is not a field name %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); %! subsasgn (T, struct ('type', '.', 'subs', ''), [1;2;3]); ## neither is a character matrix %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); %! subsref (T, struct ('type', '.', 'subs', ['er'; 'ty'])); ## a missing string names nothing, and is refused before it becomes '' %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); %! subsasgn (T, struct ('type', '.', 'subs', string (missing)), [1;2;3]); ## the custom property name is held to the same rule %!error ... %! T = addprop (table ([1;2;3], 'VariableNames', {'x'}), 'tp', 'table'); %! s = struct ('type', {'.', '.', '.'}, ... %! 'subs', {'Properties', 'CustomProperties', ''}); %! subsasgn (T, s, 1); ## an unknown table property is not assigned in silence %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); %! T.Properties.Nonsense = 5; ## a property name is held to the same rule as any other field name %!error ... %! T = table ([1;2;3], 'VariableNames', {'x'}); %! subsasgn (T, struct ('type', {'.', '.'}, 'subs', {'Properties', ''}), 5); ## Test 'fillmissing' interpolates with a modified Akima spline %!test %! T = table ([1; NaN; 3], 'VariableNames', {'v'}); %! assert (fillmissing (T, 'makima').v, [1; 2; 3], 1e-12); ## Test a merged variable has no entry in a variable-typed custom property %!test %! T = table ((1:3)', (11:13)', (21:23)', 'VariableNames', {'a', 'b', 'c'}); %! T = addprop (T, {'q'}, {'variable'}); %! T.Properties.CustomProperties.q = {'m', 'kg', 's'}; %! q = mergevars (T, {'a', 'b'}).Properties.CustomProperties.q; %! assert_equal (q, {[], 's'}); ################################################################################ ## ** 'table2timetable' ** ## ################################################################################ %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! T = table (tv, (1:3)', 'VariableNames', {'when', 'A'}); %! TT = table2timetable (T); %! assert_equal (class (TT), 'timetable'); %! assert_equal (TT.Properties.VariableNames, {'A'}); %! assert_equal (TT.Properties.DimensionNames, {'when', 'Variables'}); %! assert_equal (TT.Properties.RowTimes, tv); %! assert_equal (TT.Properties.TimeStep, hours (1)); ## Test the first time variable is the one taken %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! T = table (tv, tv + hours (1), (1:3)', ... %! 'VariableNames', {'first', 'second', 'A'}); %! TT = table2timetable (T); %! assert_equal (TT.Properties.VariableNames, {'second', 'A'}); %! assert_equal (TT.Properties.DimensionNames{1}, 'first'); ## Test a duration variable serves as row times %!test %! dv = hours (0:2)'; %! T = table (dv, (1:3)', 'VariableNames', {'elapsed', 'A'}); %! TT = table2timetable (T); %! assert_equal (class (TT.Properties.RowTimes), 'duration'); %! assert_equal (TT.Properties.DimensionNames{1}, 'elapsed'); ## Test an explicit vector keeps every variable %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! T = table ((1:3)', (4:6)'); %! TT = table2timetable (T, 'RowTimes', tv); %! assert_equal (TT.Properties.VariableNames, {'Var1', 'Var2'}); %! assert_equal (TT.Properties.DimensionNames, {'Time', 'Variables'}); ## Test a variable named by index becomes the row times %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! T = table (tv, tv + hours (1), (1:3)', ... %! 'VariableNames', {'first', 'second', 'A'}); %! TT = table2timetable (T, 'RowTimes', 2); %! assert_equal (TT.Properties.VariableNames, {'first', 'A'}); %! assert_equal (TT.Properties.DimensionNames{1}, 'second'); ## Test a variable named outright becomes the row times %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! T = table (tv, tv + hours (1), (1:3)', ... %! 'VariableNames', {'first', 'second', 'A'}); %! TT = table2timetable (T, 'RowTimes', 'second'); %! assert_equal (TT.Properties.VariableNames, {'first', 'A'}); ## Test a generated time step keeps every variable %!test %! T = table ((1:3)', (4:6)'); %! TT = table2timetable (T, 'TimeStep', hours (1)); %! assert_equal (TT.Properties.RowTimes, hours (0:2)'); %! assert_equal (TT.Properties.VariableNames, {'Var1', 'Var2'}); ## Test a generated sample rate %!test %! T = table ((1:3)', (4:6)'); %! TT = table2timetable (T, 'SampleRate', 2); %! assert_equal (TT.Properties.TimeStep, seconds (0.5)); ## Test row names are not carried over %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! T = table (tv, (1:3)', 'VariableNames', {'when', 'A'}, ... %! 'RowNames', {'a', 'b', 'c'}); %! TT = table2timetable (T); %! assert_equal (TT.Properties.DimensionNames{1}, 'when'); %! assert_equal (any (strcmp (properties (TT.Properties), 'RowNames')), false); ## Test the variable metadata of the survivors is carried over %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! T = table (tv, (1:3)', 'VariableNames', {'when', 'A'}); %! T.Properties.VariableUnits = {'', 'm'}; %! T.Properties.Description = 'a description'; %! TT = table2timetable (T); %! assert_equal (TT.Properties.VariableUnits, {'m'}); %! assert_equal (TT.Properties.Description, 'a description'); %!error ... %! table2timetable (table ((1:3)', (4:6)')); %!error ... %! table2timetable (table ((1:3)'), 'RowTimes', hours (0:2)', ... %! 'TimeStep', hours (1)); %!error ... %! table2timetable (table ((1:3)'), 'RowTimes', 'nope'); %!error ... %! table2timetable (table ((1:3)'), 'RowTimes', 7); %!error ... %! table2timetable (table ((1:3)'), 'RowTimes', 1); %!error ... %! table2timetable (table (hours ((1:3)')), 'Bogus', 1); %!error ... %! table2timetable (table (hours ((1:3)')), 'Bogus'); ## Test a custom property describing the table survives the conversion %!test %! T = table ((1:3)', 'VariableNames', {'A'}); %! T = addprop (T, {'p'}, {'table'}); %! T.Properties.CustomProperties.p = 7; %! tv = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = table2timetable (T, 'RowTimes', tv); %! assert_equal (TT.Properties.CustomProperties.p, 7); ## Test a custom property describing the variables survives the conversion %!test %! T = table ((1:3)', (11:13)', 'VariableNames', {'A', 'B'}); %! T = addprop (T, {'q'}, {'variable'}); %! T.Properties.CustomProperties.q = {'m', 'kg'}; %! tv = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = table2timetable (T, 'RowTimes', tv); %! assert_equal (TT.Properties.CustomProperties.q, {'m', 'kg'}); ## Test the variable that became the row times loses its entry %!test %! tv = datetime (2024, 1, 1) + hours ((0:2)'); %! T = table (tv, (1:3)', 'VariableNames', {'when', 'A'}); %! T = addprop (T, {'q'}, {'variable'}); %! T.Properties.CustomProperties.q = {'x', 'm'}; %! TT = table2timetable (T, 'RowTimes', 'when'); %! assert_equal (TT.Properties.CustomProperties.q, {'m'}); pr0m1th3as-datatypes-9450b29/inst/tests/timeseries.m-tst000066400000000000000000004412761527370255000231720ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## Expected values measured on MATLAB R2024a. ################################################################################ ## ** Create and describe 'timeseries' ** ## ################################################################################ ## Available Methods ## ## ## ## 'timeseries' 'get' 'set' ## ## ## ################################################################################ ## Test an empty series has no name, where every other form is 'unnamed' %!test %! ts = timeseries (); %! assert_equal (ts.Name, ''); %! assert_equal (ts.Data, []); %! assert_equal (ts.Time, zeros (0, 1)); %! assert_equal (ts.Length, 0); %! assert_equal (ts.IsTimeFirst, true); %! assert_equal (ts.Events, []); %! assert_equal (ts.Quality, []); %! assert_equal (ts.UserData, []); %! assert_equal (ts.TreatNaNasMissing, true); %!test %! ts = timeseries (); %! assert_equal (ts.TimeInfo.Units, 'seconds'); %! assert_equal (ts.TimeInfo.StartDate, ''); %! assert_equal (ts.TimeInfo.Format, ''); %! assert_equal (ts.TimeInfo.Length, 0); %! assert_equal (ts.TimeInfo.Start, []); %! assert_equal (ts.TimeInfo.End, []); %! assert_equal (ts.TimeInfo.Increment, []); %!test %! ts = timeseries (); %! assert_equal (ts.DataInfo.Units, ''); %! assert_equal (ts.DataInfo.Interpolation.Name, 'linear'); %! assert_equal (ts.DataInfo.InterpretSingleRowDataAs3D, false); %! assert_equal (ts.QualityInfo.Code, []); %! assert_equal (ts.QualityInfo.Description, []); ## Test a name alone %!test %! ts = timeseries ('myname'); %! assert_equal (ts.Name, 'myname'); %! assert_equal (ts.Length, 0); %!test %! ts = timeseries ('a b-c'); %! assert_equal (ts.Name, 'a b-c'); %!test %! ts = timeseries (string ('myname')); %! assert_equal (ts.Name, 'myname'); %! assert_equal (ts.Data, []); ## Test data alone takes default times %!test %! ts = timeseries ([1; 2; 3]); %! assert_equal (ts.Name, 'unnamed'); %! assert_equal (ts.Data, [1; 2; 3]); %! assert_equal (ts.Time, [0; 1; 2]); %! assert_equal (ts.Length, 3); %! assert_equal (ts.IsTimeFirst, true); %! assert_equal (ts.TimeInfo.Start, 0); %! assert_equal (ts.TimeInfo.End, 2); %! assert_equal (ts.TimeInfo.Increment, 1); %!test %! ts = timeseries (5); %! assert_equal (ts.Time, 0); %! assert_equal (ts.Length, 1); %! assert_equal (ts.TimeInfo.Increment, NaN); %!test %! ts = timeseries ([1, 2, 3; 4, 5, 6]); %! assert_equal (size (ts.Data), [2, 3]); %! assert_equal (ts.Time, [0; 1]); %! assert_equal (ts.IsTimeFirst, true); %!test %! ts = timeseries (ones (2, 3, 4)); %! assert_equal (ts.Time, [0; 1; 2; 3]); %! assert_equal (ts.Length, 4); %! assert_equal (ts.IsTimeFirst, false); %!test %! ts = timeseries (ones (3, 3, 3)); %! assert_equal (ts.Length, 3); %! assert_equal (ts.IsTimeFirst, false); %!test %! ts = timeseries (ones (1, 1, 3)); %! assert_equal (ts.Length, 3); %! assert_equal (ts.IsTimeFirst, false); ## Test a row vector is that many scalar samples %!test %! ts = timeseries ([1, 2, 3]); %! assert_equal (size (ts.Data), [1, 1, 3]); %! assert_equal (ts.Data(:), [1; 2; 3]); %! assert_equal (ts.Time, [0; 1; 2]); %! assert_equal (ts.IsTimeFirst, false); %!test %! ts = timeseries ([1, 2, 3], [1, 2, 3]); %! assert_equal (size (ts.Data), [1, 1, 3]); %! assert_equal (ts.Time, [1; 2; 3]); %!test %! ts = timeseries (1:3, (1:3)'); %! assert_equal (size (ts.Data), [1, 1, 3]); ## Test a row vector with a single time is one sample %!test %! ts = timeseries (ones (1, 3), 1); %! assert_equal (size (ts.Data), [1, 3]); %! assert_equal (ts.Length, 1); %! assert_equal (ts.IsTimeFirst, true); %! assert_equal (ts.TimeInfo.Increment, NaN); ## Test a matrix with a column per time is stored as an Rx1xN array %!test %! ts = timeseries (ones (2, 3), [5, 6, 7]); %! assert_equal (size (ts.Data), [2, 1, 3]); %! assert_equal (ts.IsTimeFirst, false); %! assert_equal (getdatasamplesize (ts), [2, 1]); %!test %! ts = timeseries (ones (2, 3), [5, 6]); %! assert_equal (size (ts.Data), [2, 3]); %! assert_equal (ts.IsTimeFirst, true); %! assert_equal (getdatasamplesize (ts), [1, 3]); ## Test a matrix with a single time is one sample %!test %! ts = timeseries (ones (2, 3), 5); %! assert_equal (size (ts.Data), [2, 3]); %! assert_equal (ts.Length, 1); %! assert_equal (ts.IsTimeFirst, false); %! assert_equal (getdatasamplesize (ts), [2, 3]); %!test %! ts = timeseries (ones (3, 1), 5); %! assert_equal (ts.Length, 1); %! assert_equal (ts.IsTimeFirst, false); %! assert_equal (getdatasamplesize (ts), [3, 1]); %!test %! ts = timeseries ([1, 2; 3, 4]); %! ts1 = getsamples (ts, 1); %! assert_equal (ts1.IsTimeFirst, true); %! assert_equal (getdatasamplesize (ts1), [1, 2]); ## Test empty data %!test %! ts = timeseries ([]); %! assert_equal (ts.Name, 'unnamed'); %! assert_equal (ts.Length, 0); %! assert_equal (ts.TimeInfo.Increment, []); %!test %! ts = timeseries (zeros (0, 1)); %! assert_equal (ts.Length, 0); %!test %! ts = timeseries (zeros (0, 3)); %! assert_equal (size (ts.Data), [0, 3]); %! assert_equal (ts.Length, 0); ## Test the data class is kept %!test %! ts = timeseries (int8 ([1; 2; 3])); %! assert_equal (ts.Data, int8 ([1; 2; 3])); %!test %! ts = timeseries (single ([1; 2; 3])); %! assert_equal (ts.Data, single ([1; 2; 3])); %!test %! ts = timeseries (logical ([1; 0; 1])); %! assert_equal (ts.Data, logical ([1; 0; 1])); %!test %! ts = timeseries ([1; 2; 3] + 1i); %! assert_equal (ts.Data, [1; 2; 3] + 1i); ## Test a time vector %!test %! ts = timeseries ([1; 2; 3], [10; 20; 30]); %! assert_equal (ts.Time, [10; 20; 30]); %! assert_equal (ts.TimeInfo.Start, 10); %! assert_equal (ts.TimeInfo.End, 30); %! assert_equal (ts.TimeInfo.Increment, 10); %!test %! ts = timeseries ([1; 2; 3], [10, 20, 30]); %! assert_equal (ts.Time, [10; 20; 30]); %!test %! ts = timeseries (ones (2, 3, 4), [1, 2, 3, 4]); %! assert_equal (ts.Time, [1; 2; 3; 4]); %! assert_equal (ts.TimeInfo.Start, 1); %!test %! ts = timeseries ([1; 2; 3], [0; 0.5; 1.5]); %! assert_equal (ts.TimeInfo.End, 1.5); %! assert_equal (ts.TimeInfo.Increment, NaN); %!test %! ts = timeseries ([1; 2; 3], [], []); %! assert_equal (ts.Time, [0; 1; 2]); %! assert_equal (ts.Quality, []); %!test %! ts = timeseries ([1; 2; 3], {1, 2, 3}); %! assert_equal (ts.Time, [1; 2; 3]); %! assert_equal (ts.TimeInfo.Units, 'seconds'); ## Test an unsorted time vector sorts the samples with it %!test %! ts = timeseries ([1; 2; 3], [3; 1; 2]); %! assert_equal (ts.Time, [1; 2; 3]); %! assert_equal (ts.Data, [2; 3; 1]); %!test %! ts = timeseries ([1; 2; 3], [3; 1; 2], [7; 8; 9]); %! assert_equal (ts.Quality, [8; 9; 7]); %!test %! ts = timeseries ([1, 2; 3, 4; 5, 6], [2; 0; 1]); %! assert_equal (ts.Data, [3, 4; 5, 6; 1, 2]); %!test %! ts = timeseries (cat (3, 1, 2, 3), [3; 1; 2]); %! assert_equal (ts.Data(:), [2; 3; 1]); ## Test repeated times are kept in order %!test %! ts = timeseries ([1; 2; 3], [1; 1; 2]); %! assert_equal (ts.Time, [1; 1; 2]); %! assert_equal (ts.Data, [1; 2; 3]); %! assert_equal (ts.TimeInfo.Increment, NaN); %!test %! ts = timeseries ([1; 2; 3], [2; 1; 1]); %! assert_equal (ts.Data, [2; 3; 1]); ## Test integer and single time vectors are stored as double %!test %! ts = timeseries ([1; 2; 3], int32 ([1; 2; 3])); %! assert_equal (ts.Time, [1; 2; 3]); %! assert_equal (ts.TimeInfo.Increment, 1); %! assert_equal (class (ts.TimeInfo.Start), 'double'); %!test %! ts = timeseries ([1; 2; 3], uint8 ([1; 2; 3])); %! assert_equal (class (ts.Time), 'double'); %!test %! ts = timeseries ([1; 2; 3], single ([0.5; 1; 1.5])); %! assert_equal (ts.Time, [0.5; 1; 1.5]); ## Test dates give a time vector in days from the first %!test %! ts = timeseries ([1; 2; 3], {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}); %! assert_equal (ts.Time, [0; 1; 2]); %! assert_equal (ts.TimeInfo.Units, 'days'); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %!test %! ts = timeseries ([1; 2; 3], {'03-Jan-2024', '01-Jan-2024', '02-Jan-2024'}); %! assert_equal (ts.Time, [0; 1; 2]); %! assert_equal (ts.Data, [2; 3; 1]); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %!test %! ts = timeseries ([1; 2; 3], {'2024-01-01', '2024-01-02', '2024-01-03'}); %! assert_equal (ts.Time, [0; 1; 2]); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %!test %! ts = timeseries ([1; 2; 3], string ({'01-Jan-2024', '02-Jan-2024', ... %! '03-Jan-2024'})); %! assert_equal (ts.Time, [0; 1; 2]); %! assert_equal (ts.TimeInfo.Units, 'days'); %!test %! ts = timeseries (1, {'01-Jan-2024'}); %! assert_equal (ts.Time, 0); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %!test %! ts = timeseries ([1; 2], {'01-Jan-2024 06:00:00', '01-Jan-2024 18:00:00'}); %! assert_equal (ts.Time, [0; 0.5]); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 06:00:00'); ## Test each date is read by its own format %!test %! ts = timeseries ([1; 2; 3], {'01-Jan-2024', '2024-01-02', '03-Jan-2024'}); %! assert_equal (ts.Time, [0; 1; 2]); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); ## Test a datetime array is read as its dates, which MATLAB refuses %!test %! ts = timeseries ([1; 2; 3], datetime (2024, 1, 1:3)); %! assert_equal (ts.Time, [0; 1; 2]); %! assert_equal (ts.TimeInfo.Units, 'days'); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %!test %! ts = timeseries ([1; 2; 3], datetime (2024, 1, [3, 1, 2], 6, 0, 0)); %! assert_equal (ts.Data, [2; 3; 1]); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 06:00:00'); %!test %! t = datetime (2024, 1, 1:2, 6, 0, 0, 'TimeZone', 'Europe/Athens'); %! ts = timeseries ([1; 2], t); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 06:00:00'); ## Test a duration array is relative time in the units of its format %!test %! ts = timeseries ([1; 2; 3], hours ([1, 2, 3])); %! assert_equal (ts.Time, [1; 2; 3]); %! assert_equal (ts.TimeInfo.Units, 'hours'); %! assert_equal (ts.TimeInfo.StartDate, ''); %!test %! ts = timeseries ([1; 2; 3], minutes ([0, 1, 2])); %! assert_equal (ts.TimeInfo.Units, 'minutes'); %!test %! ts = timeseries ([1; 2; 3], days ([0, 1, 2])); %! assert_equal (ts.TimeInfo.Units, 'days'); %!test %! ts = timeseries ([1; 2; 3], seconds ([3, 1, 2])); %! assert_equal (ts.Time, [1; 2; 3]); %! assert_equal (ts.Data, [2; 3; 1]); %! assert_equal (ts.TimeInfo.Units, 'seconds'); %!test %! ts = timeseries ([1; 2], duration ([0; 1], 30, 0)); %! assert_equal (ts.Time, [1800; 5400]); %! assert_equal (ts.TimeInfo.Units, 'seconds'); ## Test quality codes %!test %! ts = timeseries ([1; 2; 3], [10; 20; 30], [0; 1; 0]); %! assert_equal (ts.Quality, [0; 1; 0]); %!test %! ts = timeseries ([1; 2; 3], [1; 2; 3], int8 ([1; 2; 3])); %! assert_equal (ts.Quality, [1; 2; 3]); %! assert_equal (class (ts.Quality), 'double'); %!test %! ts = timeseries (ones (3, 2), [1; 2; 3], [1; 2; 3]); %! assert_equal (ts.Quality, [1; 2; 3]); %!test %! ts = timeseries (ones (3, 2), [1; 2; 3], ones (3, 2)); %! assert_equal (ts.Quality, ones (3, 2)); %!test %! ts = timeseries (ones (2, 3, 4), 1:4, ones (2, 3, 4)); %! assert_equal (size (ts.Quality), [2, 3, 4]); %!test %! ts = timeseries (ones (2, 3, 4), 1:4, [1, 2, 3, 4]); %! assert_equal (size (ts.Quality), [1, 1, 4]); %! assert_equal (ts.Quality(:), [1; 2; 3; 4]); ## Test the 'Name' option %!test %! ts = timeseries ([1; 2; 3], 'Name', 'abc'); %! assert_equal (ts.Name, 'abc'); %! assert_equal (ts.Time, [0; 1; 2]); %!test %! ts = timeseries ([1; 2; 3], [10; 20; 30], 'Name', 'abc'); %! assert_equal (ts.Name, 'abc'); %! assert_equal (ts.Time, [10; 20; 30]); %!test %! ts = timeseries ([1; 2; 3], [10; 20; 30], [0; 1; 0], 'Name', 'abc'); %! assert_equal (ts.Name, 'abc'); %! assert_equal (ts.Quality, [0; 1; 0]); %!test %! ts = timeseries ([1; 2; 3], [10; 20; 30], 'name', 'abc'); %! assert_equal (ts.Name, 'abc'); %!test %! ts = timeseries ([1; 2; 3], [1; 2; 3], 'Name', 'a', 'Name', 'b'); %! assert_equal (ts.Name, 'b'); %!test %! ts = timeseries ([1; 2; 3], [1; 2; 3], 'Name', string ('str')); %! assert_equal (ts.Name, 'str'); %!test %! ts = timeseries ([1; 2; 3], [1; 2; 3], 'Name', ''); %! assert_equal (ts.Name, ''); %!test %! ts = timeseries ([1; 2; 3], string ('Name'), 'abc'); %! assert_equal (ts.Name, 'abc'); ## Test a series is returned as it is %!test %! ts0 = timeseries ([1; 2], 'Name', 'x'); %! ts = timeseries (ts0); %! assert_equal (ts.Name, 'x'); %! assert_equal (ts.Data, [1; 2]); ## Test value semantics %!test %! a = timeseries ([1; 2; 3]); %! b = a; %! b.Name = 'changed'; %! assert_equal (a.Name, 'unnamed'); %!test %! a = timeseries ([1; 2; 3]); %! b = a; %! b.TimeInfo.Units = 'days'; %! assert_equal (a.TimeInfo.Units, 'seconds'); %!test %! a = timeseries ([1; 2; 3]); %! b = a; %! b.DataInfo.Interpolation = 'zoh'; %! assert_equal (a.DataInfo.Interpolation.Name, 'linear'); ## Test arrays of series %!test %! ts = timeseries ([1; 2]); %! assert_equal (size (ts), [1, 1]); %! assert_equal (numel (ts), 1); %! assert_equal (isempty (ts), false); %! assert_equal (isempty (timeseries ()), false); %!test %! ts = timeseries ([1; 2]); %! A = [ts, ts]; %! assert_equal (size (A), [1, 2]); %! assert_equal (class (A), 'timeseries'); %! assert_equal (A(2).Length, 2); %!test %! ts = timeseries ([1; 2]); %! assert_equal (size ([ts; ts]), [2, 1]); ## Test the property list %!test %! assert_equal (properties (timeseries ()), {'Events'; 'Name'; ... %! 'UserData'; 'Data'; 'DataInfo'; 'Time'; 'TimeInfo'; ... %! 'Quality'; 'QualityInfo'; 'IsTimeFirst'; ... %! 'TreatNaNasMissing'; 'Length'}); %!test %! assert_equal (fieldnames (timeseries ()), properties (timeseries ())); %!error timeseries ({1, 2}) %!error ... %! timeseries (struct ('a', 1)) %!error ... %! timeseries ('abc', [1, 2, 3]) %!error ... %! timeseries ('Name', 'abc') %!error ... %! timeseries ([1; 2; 3], [1; 2; 3], [1; 2; 3], [1; 2; 3]) %!error ... %! timeseries ([1; 2; 3], [10; 20; 30], 'Name') %!error ... %! timeseries (ones (3, 2), 'myname') %!error ... %! timeseries ([1; 2; 3], '01-Jan-2024') %!error ... %! timeseries ([1; 2; 3], [10; 20; 30], 'Bogus', 1) %!error ... %! timeseries ([1; 2; 3], 'UserData', 5) %!error ... %! timeseries ([1; 2; 3], [1; 2; 3], 'Name', 5) ## Test codes per element of data stored a sample per slice take its stored ## shape, as in MATLAB %!test %! ts = timeseries ([1, 2, 3; 4, 5, 6], [0, 1, 2], ... %! reshape ([1, 2, 3; 4, 5, 6], 2, 1, 3)); %! assert_equal (size (ts.Quality), [2, 1, 3]); %!error ... %! timeseries ([1, 2, 3; 4, 5, 6], [0, 1, 2], [1, 2, 3; 4, 5, 6]) ## Test no dates give the default times in seconds %!test %! ts = timeseries ([1; 2; 3], {}); %! assert_equal (ts.Time, [0; 1; 2]); %! assert_equal (ts.TimeInfo.Units, 'seconds'); %!error ... %! timeseries ([1; 2], datetime (12000, 1, 1:2)) %!error ... %! timeseries ([1; 2; 3], [1; 2]) %!error ... %! timeseries (ones (2, 3, 4), [1, 2]) %!error ... %! timeseries ([1; 2; 3], [1; NaN; 2]) %!error ... %! timeseries ([1; 2; 3], [1; Inf; 2]) %!error ... %! timeseries ([1; 2; 3], [datetime(2024, 1, 1), NaT, datetime(2024, 1, 3)]) %!error ... %! timeseries ([1; 2; 3], seconds ([1, NaN, 3])) %!error ... %! timeseries ([1; 2; 3], ones (3, 3)) %!error ... %! timeseries ([1; 2; 3], [1; 2; 3] + 1i) %!error ... %! timeseries ([1; 2; 3], {'bogus', '02-Jan-2024', '03-Jan-2024'}) %!error ... %! timeseries ([1; 2; 3], [1; 2; 3], [1; 2]) %!error ... %! timeseries ([1; 2; 3], [1; 2; 3], {1; 2; 3}) %!error ... %! timeseries ([1; 2; 3], [1; 2; 3], [0.5; 1; 2]) %!error ... %! timeseries ([1; 2; 3], [1; 2; 3], [200; 1; 2]) %!test %! ts = timeseries ([1; 2; 3; 4]); %! S = get (ts); %! assert_equal (fieldnames (S), properties (ts)); %! assert_equal (S.Length, 4); %! assert_equal (S.Name, 'unnamed'); %!test %! ts = timeseries ([1; 2; 3; 4]); %! assert_equal (get (ts, 'Name'), 'unnamed'); %! assert_equal (get (ts, 'Length'), 4); %!test %! ts = timeseries ([1; 2; 3; 4]); %! assert_equal (get (ts, 'name'), 'unnamed'); %! assert_equal (get (ts, string ('Name')), 'unnamed'); %!test %! ts = timeseries ([1; 2; 3; 4]); %! assert_equal (get (ts, {'Name', 'Length'}), {'unnamed', 4}); %! assert_equal (get (ts, {'Name'; 'Length'}), {'unnamed', 4}); %!test %! ts = timeseries ([1; 2; 3; 4]); %! assert_equal (get ([ts, ts], 'Name'), {'unnamed', 'unnamed'}); %!test %! A = [timeseries([1; 2], 'Name', 'a'), timeseries([1; 2], 'Name', 'b')]; %! assert_equal (get (A, {'Name', 'Length'}), {'a', 2; 'b', 2}); %!error ... %! get (timeseries ([1; 2]), 'Length', 'Name') %!error ... %! get (timeseries ([1; 2]), 5) %!error ... %! get (timeseries ([1; 2]), 'Bogus') %!error ... %! get (timeseries ([1; 2]), {'Name', 'Bogus'}) %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts2 = set (ts, 'Name', 'x'); %! assert_equal (ts2.Name, 'x'); %! assert_equal (ts.Name, 'unnamed'); %!test %! ts = set (timeseries ([1; 2; 3; 4]), 'name', 'x'); %! assert_equal (ts.Name, 'x'); ## Test 'set' with no output sets the caller's variable, as in MATLAB %!test %! ts = timeseries ([1; 2; 3; 4]); %! set (ts, 'Name', 'viaset'); %! assert_equal (ts.Name, 'viaset'); %!test %! ts = timeseries ([1; 2; 3; 4]); %! set (ts, 'Name', 'x', 'UserData', 3); %! assert_equal (ts.Name, 'x'); %! assert_equal (ts.UserData, 3); %!test %! A = [timeseries([1; 2]), timeseries([3; 4])]; %! set (A, 'Name', 'z'); %! assert_equal (A(2).Name, 'z'); %!test %! ts = timeseries ([1; 2; 3; 4]); %! f = @(x) set (x, 'Name', 'inner'); %! f (ts); %! assert_equal (ts.Name, 'unnamed'); %!test %! ts = set (timeseries ([1; 2; 3; 4]), 'Name', 'x', 'UserData', 3); %! assert_equal (ts.Name, 'x'); %! assert_equal (ts.UserData, 3); %!test %! ts = set (timeseries ([1; 2; 3; 4]), string ('Name'), 'q'); %! assert_equal (ts.Name, 'q'); %!test %! ts = timeseries ([1; 2; 3; 4]); %! S = set (ts); %! assert_equal (fieldnames (S), properties (ts)); %! assert_equal (S.Data, [1; 2; 3; 4]); %!test %! A = set ([timeseries([1; 2]), timeseries([3; 4])], 'Name', 'z'); %! assert_equal (A(1).Name, 'z'); %! assert_equal (A(2).Name, 'z'); %!error ... %! set (timeseries ([1; 2]), 'Name', 'x') %!error ... %! A = [timeseries([1; 2]), timeseries([3; 4])]; set (A(1), 'Name', 'x'); %!error ... %! set (timeseries ([1; 2]), 'Name') %!error ... %! set (timeseries ([1; 2]), 'Name', 'a', 'UserData') %!error ... %! set (timeseries ([1; 2]), 5, 1) %!error ... %! set (timeseries ([1; 2]), 'Bogus', 1) %!error ... %! set (timeseries ([1; 2]), 'Length', 3) ################################################################################ ## ** Data and sample operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'getdatasamples' 'getdatasamplesize' 'getsamples' 'getsampleusingtime' ## ## 'addsample' 'delsample' 'append' 'hasduplicatetimes' ## ## ## ################################################################################ %!shared A, M, T3, Q3, D, Ab %! A = timeseries ([10; 20; 30; 40; 50], [0; 1; 2; 3; 4], [1; 2; 3; 4; 5], ... %! 'Name', 'a'); %! A.DataInfo.Units = 'volts'; %! A.UserData = 7; %! A.Events = tsdata.event ('ev', 2); %! M = timeseries ([1, 2; 3, 4; 5, 6; 7, 8], [0; 1; 2; 3]); %! T3 = timeseries (reshape (1:24, 2, 3, 4), [0, 1, 2, 3]); %! Q3 = timeseries (reshape (1:24, 2, 3, 4), [0, 1, 2, 3], ... %! reshape (mod (1:24, 3), 2, 3, 4)); %! D = timeseries ([1; 2; 3; 4], [0; 1; 1; 2]); %! Ab = timeseries ([1; 2; 3], {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}); %!assert_equal (getdatasamples (A, 2), 20) %!assert_equal (getdatasamples (A, [1, 3]), [10; 30]) %!assert_equal (getdatasamples (A, [3, 1]), [30; 10]) %!assert_equal (getdatasamples (A, [2, 2]), [20; 20]) %!assert_equal (getdatasamples (A, [1; 2]), [10; 20]) %!assert_equal (getdatasamples (A, int8 (2)), 20) %!assert_equal (getdatasamples (A, logical ([1, 0, 1, 0, 0])), [10; 30]) %!assert_equal (getdatasamples (A, logical ([1, 0, 1])), [10; 30]) %!assert_equal (getdatasamples (A, []), []) %!assert_equal (getdatasamples (M, [1, 2]), [1, 2; 3, 4]) %!assert_equal (getdatasamples (T3, [1, 2]), reshape (1:12, 2, 3, 2)) %!assert_equal (getdatasamples (Q3, 2), [7, 9, 11; 8, 10, 12]) %!assert_equal (getdatasamples (timeseries ([1, 2, 3]), [1, 3]), cat (3, 1, 3)) %!error ... %! getdatasamples (A) %!error ... %! getdatasamples (A, 2, 3) %!error ... %! getdatasamples (A, 6) %!error ... %! getdatasamples (A, 0) %!error ... %! getdatasamples (A, 1.5) %!error ... %! getdatasamples (A, ':') %!error ... %! getdatasamples (timeseries (), 1) %!error ... %! getdatasamples (A, true (1, 6)) %!error ... %! getdatasamples ([A, A], 1) %!assert_equal (getdatasamplesize (A), [1, 1]) %!assert_equal (getdatasamplesize (M), [1, 2]) %!assert_equal (getdatasamplesize (T3), [2, 3]) %!assert_equal (getdatasamplesize (timeseries (ones (2, 3, 4, 5))), [2, 3, 4]) %!assert_equal (getdatasamplesize (timeseries (ones (1, 3), 1)), [1, 3]) %!assert_equal (getdatasamplesize (timeseries ([1, 2, 3])), [1, 1]) %!assert_equal (getdatasamplesize (timeseries ()), []) %!assert_equal (getdatasamplesize (timeseries ('n')), []) %!assert_equal (getdatasamplesize (timeseries (zeros (0, 3))), []) %!assert_equal (getdatasamplesize (getsamples (T3, 2)), [2, 3]) %!error ... %! getdatasamplesize ([A, A]) %!test %! ts = getsamples (A, [2, 4]); %! assert_equal (ts.Data, [20; 40]); %! assert_equal (ts.Time, [1; 3]); %! assert_equal (ts.Quality, [2; 4]); %! assert_equal (ts.TimeInfo.Increment, 2); %! assert_equal (ts.DataInfo.Units, 'volts'); %! assert_equal (ts.UserData, 7); %! assert_equal (ts.Events.Name, 'ev'); ## Test the name is kept, where MATLAB gives 'unnamed' %!test %! ts = getsamples (A, [2, 4]); %! assert_equal (ts.Name, 'a'); ## Test the samples are taken in time order, repeats kept %!test %! ts = getsamples (A, [4, 2]); %! assert_equal (ts.Data, [20; 40]); %!test %! ts = getsamples (A, [2, 2]); %! assert_equal (ts.Data, [20; 20]); %! assert_equal (ts.Time, [1; 1]); %! assert_equal (ts.TimeInfo.Increment, NaN); %!test %! ts = getsamples (A, logical ([0, 1, 0, 1, 0])); %! assert_equal (ts.Time, [1; 3]); %!test %! ts = getsamples (A, 3); %! assert_equal (ts.Data, 30); %! assert_equal (ts.TimeInfo.Increment, NaN); ## Test no samples keep every property, where MATLAB resets them %!test %! ts = getsamples (A, []); %! assert_equal (ts.Length, 0); %! assert_equal (ts.Name, 'a'); %! assert_equal (ts.DataInfo.Units, 'volts'); %! assert_equal (ts.UserData, 7); %!test %! ts = getsamples (T3, [1, 3]); %! assert_equal (ts.Data, reshape ([1:6, 13:18], 2, 3, 2)); %! assert_equal (ts.Time, [0; 2]); %! assert_equal (ts.IsTimeFirst, false); %!test %! ts = getsamples (Q3, [2, 3]); %! assert_equal (ts.Quality, reshape (mod (7:18, 3), 2, 3, 2)); %!test %! ts = getsamples (T3, 2); %! assert_equal (ts.Data, [7, 9, 11; 8, 10, 12]); %! assert_equal (ts.Length, 1); %! assert_equal (ts.IsTimeFirst, false); %!test %! ts = getsamples (Ab, 2); %! assert_equal (ts.Time, 1); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %! assert_equal (ts.TimeInfo.Units, 'days'); %!test %! ts = getsamples (D, [2, 3]); %! assert_equal (ts.Data, [2; 3]); %! assert_equal (ts.Time, [1; 1]); %!error getsamples (A) %!error ... %! getsamples (A, 6) %!error ... %! getsamples (A, 2.5) %!error ... %! getsamples (A, ':') %!error getsamples ([A, A], 1) %!test %! ts = getsampleusingtime (A, 2); %! assert_equal (ts.Data, 30); %! assert_equal (ts.Quality, 3); %! assert_equal (ts.Name, 'a'); %!test %! ts = getsampleusingtime (A, 1, 3); %! assert_equal (ts.Data, [20; 30; 40]); %! assert_equal (ts.Time, [1; 2; 3]); %!test %! ts = getsampleusingtime (A, 1.5, 3.5); %! assert_equal (ts.Data, [30; 40]); %!test %! ts = getsampleusingtime (A, 2, 2); %! assert_equal (ts.Data, 30); %!test %! ts = getsampleusingtime (A, -Inf, Inf); %! assert_equal (ts.Length, 5); %!test %! ts = getsampleusingtime (T3, 1, 2); %! assert_equal (ts.Data, reshape (7:18, 2, 3, 2)); ## Test a time or range no sample falls in gives no samples %!test %! ts = getsampleusingtime (A, 2.5); %! assert_equal (ts.Length, 0); %! assert_equal (ts.Name, 'a'); %! assert_equal (ts.DataInfo.Units, 'volts'); %!test %! assert_equal (getsampleusingtime (A, 3, 1).Length, 0); %! assert_equal (getsampleusingtime (A, 5, 9).Length, 0); %! assert_equal (getsampleusingtime (A, NaN).Length, 0); %! assert_equal (getsampleusingtime (timeseries (), 1).Length, 0); ## Test repeated times need 'AllowDuplicateTimes' in the single-time form %!test %! ts = getsampleusingtime (D, 1, 'AllowDuplicateTimes', true); %! assert_equal (ts.Data, [2; 3]); %!test %! ts = getsampleusingtime (D, 1, 'allowduplicatetimes', 1); %! assert_equal (ts.Data, [2; 3]); %!test %! ts = getsampleusingtime (D, 2, 'AllowDuplicateTimes', false); %! assert_equal (ts.Data, 4); %!test %! ts = getsampleusingtime (D, 1, 1); %! assert_equal (ts.Data, [2; 3]); %!test %! ts = getsampleusingtime (D, 0, 2); %! assert_equal (ts.Data, [1; 2; 3; 4]); ## Test dates on a series with a start date: text, datenum, datetime %!test %! ts = getsampleusingtime (Ab, '02-Jan-2024'); %! assert_equal (ts.Data, 2); %! assert_equal (ts.Time, 1); %!test %! ts = getsampleusingtime (Ab, string ('02-Jan-2024')); %! assert_equal (ts.Data, 2); %!test %! ts = getsampleusingtime (Ab, '01-Jan-2024', '02-Jan-2024'); %! assert_equal (ts.Data, [1; 2]); %!test %! ts = getsampleusingtime (Ab, '01-Jan-2024 12:00:00', '03-Jan-2024'); %! assert_equal (ts.Data, [2; 3]); %!test %! ts = getsampleusingtime (Ab, datenum (2024, 1, 2)); %! assert_equal (ts.Data, 2); %!test %! ts = getsampleusingtime (Ab, datenum (2024, 1, 1), datenum (2024, 1, 2)); %! assert_equal (ts.Data, [1; 2]); %!test %! ts = getsampleusingtime (Ab, datetime (2024, 1, 2)); %! assert_equal (ts.Data, 2); %!test %! assert_equal (getsampleusingtime (Ab, 1).Length, 0); %!error ... %! getsampleusingtime (D, 1) %!error ... %! getsampleusingtime (D, 1, 'AllowDuplicateTimes', false) %!error ... %! getsampleusingtime (A) %!error ... %! getsampleusingtime (A, 1, 3, 4, 5) %!error ... %! getsampleusingtime (A, 1, 3, 4) %!error ... %! getsampleusingtime (D, 1, 'Bogus', true) %!error ... %! getsampleusingtime (D, 1, 'AllowDuplicateTimes', 'yes') %!error ... %! getsampleusingtime (A, '02-Jan-2024') %!error ... %! getsampleusingtime (A, [1, 3]) %!error ... %! getsampleusingtime (A, 1, 'x') %!error ... %! getsampleusingtime (Ab, 'bogus') %!error ... %! getsampleusingtime ([A, A], 1) %!test %! ts = addsample (A, 'Data', 99, 'Time', 2.5); %! assert_equal (ts.Data, [10; 20; 30; 99; 40; 50]); %! assert_equal (ts.Time, [0; 1; 2; 2.5; 3; 4]); %! assert_equal (ts.Name, 'a'); %! assert_equal (ts.DataInfo.Units, 'volts'); ## Test a new sample without a code takes the nearest sample's code, ## the later on a tie %!test %! ts = addsample (A, 'Data', 99, 'Time', 2.5); %! assert_equal (ts.Quality, [1; 2; 3; 4; 4; 5]); %!test %! ts = addsample (timeseries ([1; 2; 3], [0; 1; 2], [7; 8; 9]), ... %! 'Data', 5, 'Time', 0.4); %! assert_equal (ts.Quality, [7; 7; 8; 9]); %!test %! ts = addsample (timeseries ([1; 2; 3], [0; 3; 6], [7; 8; 9]), ... %! 'Data', [5; 6], 'Time', [1; 2]); %! assert_equal (ts.Quality, [7; 7; 8; 8; 9]); %!test %! ts = addsample (A, 'Data', 99, 'Time', 10); %! assert_equal (ts.Quality(end), 5); %!test %! ts = addsample (A, 'Data', 99, 'Time', -1); %! assert_equal (ts.Time, [-1; 0; 1; 2; 3; 4]); %! assert_equal (ts.Quality(1), 1); %! assert_equal (ts.TimeInfo.Increment, 1); ## Test a sample at a time already present goes after the samples there %!test %! ts = addsample (A, 'Data', 99, 'Time', 2); %! assert_equal (ts.Data, [10; 20; 30; 99; 40; 50]); %! assert_equal (ts.Quality, [1; 2; 3; 3; 4; 5]); %!test %! ts = addsample (A, 'Data', [99; 98], 'Time', [2.5; 5]); %! assert_equal (ts.Data, [10; 20; 30; 99; 40; 50; 98]); %!test %! ts = addsample (A, 'Data', [1; 2], 'Time', [3.5, 2.5]); %! assert_equal (ts.Data, [10; 20; 30; 2; 40; 1; 50]); %!test %! ts = addsample (A, 'Data', 99, 'Time', 2.5, 'Quality', 9); %! assert_equal (ts.Quality, [1; 2; 3; 9; 4; 5]); %!test %! ts = addsample (A, 'Time', 2.5, 'Data', 99); %! assert_equal (ts.Data(4), 99); %!test %! ts = addsample (A, 'data', 99, 'time', 2.5); %! assert_equal (ts.Data(4), 99); %!test %! ts = addsample (A, 'Data', 99, 'Time', 2.5, 'Data', 98); %! assert_equal (ts.Data(4), 98); %!test %! ts = addsample (A, struct ('Data', 99, 'Time', 2.5)); %! assert_equal (ts.Data(4), 99); ## Test 'OverwriteFlag' replaces the samples at a time already present %!test %! ts = addsample (A, 'Data', 99, 'Time', 2, 'OverwriteFlag', true); %! assert_equal (ts.Data, [10; 20; 99; 40; 50]); %! assert_equal (ts.Quality, [1; 2; 3; 4; 5]); %!test %! ts = addsample (A, 'Data', 99, 'Time', 2, 'OverwriteFlag', true, ... %! 'Quality', 9); %! assert_equal (ts.Quality, [1; 2; 9; 4; 5]); %!test %! ts = addsample (A, 'Data', [99; 98], 'Time', [2; 2.5], ... %! 'OverwriteFlag', true); %! assert_equal (ts.Data, [10; 20; 99; 98; 40; 50]); %!test %! ts = addsample (D, 'Data', 99, 'Time', 1, 'OverwriteFlag', true); %! assert_equal (ts.Data, [1; 99; 99; 4]); %!test %! ts = addsample (A, 'Data', 99, 'Time', 2, 'OverwriteFlag', false); %! assert_equal (ts.Length, 6); %!test %! ts = addsample (A, 'Data', 99, 'Time', 2, 'OverwriteFlag', 1); %! assert_equal (ts.Length, 5); ## Test the samples of matrices and arrays %!test %! ts = addsample (M, 'Data', [9, 9], 'Time', 1.5); %! assert_equal (ts.Data, [1, 2; 3, 4; 9, 9; 5, 6; 7, 8]); %!test %! ts = addsample (M, 'Data', [9, 9; 8, 8], 'Time', [1.5; 2.5]); %! assert_equal (ts.Data, [1, 2; 3, 4; 9, 9; 5, 6; 8, 8; 7, 8]); %!test %! ts = addsample (T3, 'Data', ones (2, 3), 'Time', 1.5); %! assert_equal (size (ts.Data), [2, 3, 5]); %! assert_equal (ts.Data(:,:,3), ones (2, 3)); %!test %! ts = addsample (T3, 'Data', ones (2, 3, 2), 'Time', [1.5, 2.5]); %! assert_equal (ts.Time, [0; 1; 1.5; 2; 2.5; 3]); %!test %! ts = addsample (Q3, 'Data', ones (2, 3), 'Time', 1.5, ... %! 'Quality', zeros (2, 3)); %! assert_equal (ts.Quality(:,:,3), zeros (2, 3)); %!test %! ts = addsample (Q3, 'Data', ones (2, 3), 'Time', 1.5); %! assert_equal (ts.Quality(:,:,3), mod ([13, 15, 17; 14, 16, 18], 3)); %!test %! ts = addsample (timeseries ([1, 2, 3]), 'Data', 9, 'Time', 3); %! assert_equal (size (ts.Data), [1, 1, 4]); %! assert_equal (ts.Data(:), [1; 2; 3; 9]); %!test %! ts = addsample (timeseries (ones (1, 3), 1), 'Data', [2, 2, 2], 'Time', 2); %! assert_equal (ts.Data, [1, 1, 1; 2, 2, 2]); %!test %! ts = addsample (getsamples (T3, 2), 'Data', ones (2, 3), 'Time', 9); %! assert_equal (size (ts.Data), [2, 3, 2]); %! assert_equal (ts.Time, [1; 9]); ## Test an empty series takes any samples and is named 'unnamed' %!test %! ts = addsample (timeseries (), 'Data', 5, 'Time', 1); %! assert_equal (ts.Data, 5); %! assert_equal (ts.Name, 'unnamed'); %!test %! ts = addsample (timeseries (), 'Data', [1, 2, 3], 'Time', 1); %! assert_equal (ts.Data, [1, 2, 3]); %! assert_equal (ts.Length, 1); %!test %! ts = addsample (timeseries ('n'), 'Data', 5, 'Time', 1); %! assert_equal (ts.Name, 'n'); ## Test the class of the data follows concatenation %!test %! ts = addsample (A, 'Data', int8 (9), 'Time', 2.5); %! assert_equal (class (ts.Data), 'int8'); %!test %! ts = addsample (timeseries (int8 ([1; 2])), 'Data', 3.7, 'Time', 5); %! assert_equal (ts.Data, int8 ([1; 2; 4])); %!test %! ts = addsample (A, 'Data', true, 'Time', 2.5); %! assert_equal (ts.Data(4), 1); ## Test times on a series with a start date %!test %! ts = addsample (Ab, 'Data', 9, 'Time', '04-Jan-2024'); %! assert_equal (ts.Time, [0; 1; 2; 3]); %!test %! ts = addsample (Ab, 'Data', 9, 'Time', 3.5); %! assert_equal (ts.Time, [0; 1; 2; 3.5]); %!error ... %! addsample (A, 'Data', 99) %!error ... %! addsample (A, 'Time', 2.5) %!error ... %! addsample (A, 'Data', [], 'Time', []) %!error ... %! addsample (A, 'Data', {99}, 'Time', 2.5) %!error ... %! addsample (A, 'Data', [99, 98], 'Time', 2.5) %!error ... %! addsample (M, 'Data', [9; 9], 'Time', 1.5) ## Test one sample of a series of four dimensions %!test %! ts = timeseries (reshape (1:120, 2, 3, 4, 5)); %! ts = addsample (ts, 'Data', zeros (2, 3, 4), 'Time', 10); %! assert_equal (size (ts.Data), [2, 3, 4, 6]); %! assert_equal (ts.Time(end), 10); %!error ... %! addsample (A, 'Data', 99, 'Time', [2.5, 3.5]) %!error ... %! addsample (A, 'Data', 9, 'Time', '04-Jan-2024') %!error ... %! addsample (timeseries ([1; 2; 3], [0; 1; 2]), 'Data', 9, 'Time', NaN) %!error ... %! addsample (timeseries ([1; 2; 3], [0; 1; 2]), 'Data', 9, 'Time', Inf) %!error ... %! addsample (timeseries (), 'Data', 1, 'Time', NaN) %!error ... %! addsample (A, 'Data', 99, 'Time', 2.5, 'Bogus', 1) %!error ... %! addsample (A, 'Data', 99, 'Time') %!error ... %! addsample (A, 'Data', 99, 'Time', 2.5, 'OverwriteFlag', 'yes') %!error ... %! addsample (A, 'Data', 99, 'Time', 2.5, 'Quality', 1.5) %!error ... %! addsample (A, 'Data', 99, 'Time', 2.5, 'Quality', 200) %!error ... %! addsample (A, 'Data', 99, 'Time', 2.5, 'Quality', [9, 8]) %!error ... %! addsample (timeseries ([1; 2; 3]), 'Data', 9, 'Time', 1.5, 'Quality', 3) %!error ... %! addsample ([A, A], 'Data', 9, 'Time', 1.5) %!test %! ts = delsample (A, 'Index', 2); %! assert_equal (ts.Data, [10; 30; 40; 50]); %! assert_equal (ts.Quality, [1; 3; 4; 5]); %! assert_equal (ts.Name, 'a'); %!test %! ts = delsample (A, 'Index', [1, 3]); %! assert_equal (ts.Time, [1; 3; 4]); %!test %! ts = delsample (A, 'index', [2, 2]); %! assert_equal (ts.Data, [10; 30; 40; 50]); %!test %! ts = delsample (A, 'Index', []); %! assert_equal (ts.Length, 5); %!test %! ts = delsample (A, 'Index', 1:5); %! assert_equal (ts.Length, 0); %! assert_equal (size (ts.Data), [0, 1]); %! assert_equal (ts.DataInfo.Units, 'volts'); %!test %! ts = delsample (A, 'Value', 2); %! assert_equal (ts.Data, [10; 20; 40; 50]); %!test %! ts = delsample (A, 'Value', [1, 3]); %! assert_equal (ts.Time, [0; 2; 4]); %! assert_equal (ts.TimeInfo.Increment, 2); %!test %! ts = delsample (A, 'Value', 2.5); %! assert_equal (ts.Length, 5); %!test %! ts = delsample (A, 'Value', NaN); %! assert_equal (ts.Length, 5); ## Test 'Value' removes every sample at a repeated time %!test %! ts = delsample (D, 'Value', 1); %! assert_equal (ts.Data, [1; 4]); %!test %! ts = delsample (T3, 'Index', 2); %! assert_equal (ts.Data, reshape ([1:6, 13:24], 2, 3, 3)); ## Test codes per data element lose the removed slice, where MATLAB ## flattens them %!test %! ts = delsample (Q3, 'Index', 2); %! assert_equal (ts.Quality, reshape (mod ([1:6, 13:24], 3), 2, 3, 3)); %!test %! ts = delsample (M, 'Index', 1); %! assert_equal (ts.Data, [3, 4; 5, 6; 7, 8]); %!test %! ts = delsample (Ab, 'Value', '02-Jan-2024'); %! assert_equal (ts.Data, [1; 3]); %!test %! ts = delsample (Ab, 'Value', 1); %! assert_equal (ts.Data, [1; 3]); %!error delsample (A) %!error ... %! delsample (A, 'Index') %!error ... %! delsample (A, 'Index', 2, 'Value', 3) %!error ... %! delsample (A, 'Bogus', 2) %!error ... %! delsample (A, 'Index', 6) %!error ... %! delsample (A, 'Index', 0) %!error ... %! delsample (A, 'Index', 1.5) %!error ... %! delsample (A, 'Index', logical ([1, 0, 0, 0, 0])) %!error ... %! delsample (timeseries (), 'Index', 1) %!error ... %! delsample (A, 'Value', '02-Jan-2024') %!error ... %! delsample ([A, A], 'Index', 1) %!test %! ts = append (timeseries ([1; 2], [0; 1]), timeseries ([3; 4], [2; 3])); %! assert_equal (ts.Data, [1; 2; 3; 4]); %! assert_equal (ts.Time, [0; 1; 2; 3]); %! assert_equal (ts.TimeInfo.Increment, 1); ## Test series may meet at an equal time, and a gap is kept %!test %! ts = append (timeseries ([1; 2], [0; 1]), timeseries ([3; 4], [1; 2])); %! assert_equal (ts.Time, [0; 1; 1; 2]); %!test %! ts = append (timeseries ([1; 2], [0; 1]), timeseries ([3; 4], [10; 20])); %! assert_equal (ts.Time, [0; 1; 10; 20]); %!test %! ts = append (timeseries ([1; 2], [0; 1]), timeseries ([3; 4], [2; 3]), ... %! timeseries (5, 4)); %! assert_equal (ts.Data, [1; 2; 3; 4; 5]); %!test %! ts = append ([timeseries([1; 2], [0; 1]), timeseries([3; 4], [2; 3])]); %! assert_equal (ts.Data, [1; 2; 3; 4]); ## Test series with no samples are skipped, and one alone is returned as it is %!test %! ts = append (A, timeseries ()); %! assert_equal (ts.Name, 'a'); %! assert_equal (ts.Quality, [1; 2; 3; 4; 5]); %!test %! ts = append (timeseries (), A); %! assert_equal (ts.Data, [10; 20; 30; 40; 50]); %!test %! ts = append (A); %! assert_equal (ts.Name, 'a'); %!test %! ts = append (M, timeseries ([9, 9], 4)); %! assert_equal (ts.Data, [1, 2; 3, 4; 5, 6; 7, 8; 9, 9]); %!test %! ts = append (T3, timeseries (ones (2, 3, 2), [4, 5])); %! assert_equal (size (ts.Data), [2, 3, 6]); %! assert_equal (ts.Time, [0; 1; 2; 3; 4; 5]); %!test %! ts = append (getsamples (T3, 2), getsamples (T3, 3)); %! assert_equal (ts.Data, reshape (7:18, 2, 3, 2)); %! assert_equal (ts.IsTimeFirst, false); ## Test times are converted to the coarsest units %!test %! a = timeseries ([60; 70], [0; 1]); %! a.TimeInfo.Units = 'minutes'; %! ts = append (a, timeseries ([80; 90], [120; 180])); %! assert_equal (ts.Time, [0; 1; 2; 3]); %! assert_equal (ts.TimeInfo.Units, 'minutes'); %!test %! b = timeseries ([60; 70], [5; 6]); %! b.TimeInfo.Units = 'minutes'; %! ts = append (timeseries ([1; 2], [0; 60]), b); %! assert_equal (ts.Time, [0; 1; 5; 6]); %!test %! ts = append (Ab, timeseries ([4; 5], {'04-Jan-2024', '05-Jan-2024'})); %! assert_equal (ts.Time, [0; 1; 2; 3; 4]); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); ## Test quality codes are joined when every series has them %!test %! ts = append (timeseries ([1; 2], [0; 1], [1; 1]), ... %! timeseries ([3; 4], [2; 3], [2; 2])); %! assert_equal (ts.Quality, [1; 1; 2; 2]); ## Test the name, units and quality descriptions are kept where they agree %!test %! ts = append (timeseries ([1; 2], [0; 1], 'Name', 'x'), ... %! timeseries ([3; 4], [2; 3], 'Name', 'x')); %! assert_equal (ts.Name, 'x'); %!test %! ts = append (timeseries ([1; 2], [0; 1], 'Name', 'x'), ... %! timeseries ([3; 4], [2; 3])); %! assert_equal (ts.Name, 'unnamed'); %!test %! a = timeseries ([1; 2], [0; 1]); %! a.DataInfo.Units = 'volts'; %! a.QualityInfo.Code = [0, 1]; %! b = timeseries ([3; 4], [2; 3]); %! b.DataInfo.Units = 'volts'; %! b.QualityInfo.Code = [0, 1]; %! ts = append (a, b); %! assert_equal (ts.DataInfo.Units, 'volts'); %! assert_equal (ts.QualityInfo.Code, [0, 1]); %!test %! a = timeseries ([1; 2], [0; 1]); %! a.DataInfo.Units = 'volts'; %! a.QualityInfo.Code = [0, 1]; %! b = timeseries ([3; 4], [2; 3]); %! b.DataInfo.Units = 'amps'; %! ts = append (a, b); %! assert_equal (ts.DataInfo.Units, ''); %! assert_equal (ts.QualityInfo.Code, []); ## Test events are gathered and the rest comes from the first series %!test %! a = timeseries ([1; 2], [0; 1]); %! a.Events = tsdata.event ('ea', 0); %! a.UserData = 9; %! a.TreatNaNasMissing = false; %! b = timeseries ([3; 4], [2; 3]); %! b.Events = tsdata.event ('eb', 2); %! b.UserData = 5; %! ts = append (a, b); %! assert_equal ({ts.Events.Name}, {'ea', 'eb'}); %! assert_equal (ts.UserData, 9); %! assert_equal (ts.TreatNaNasMissing, false); ## Test the class of the data follows concatenation %!test %! ts = append (A, timeseries (int8 ([60; 70]), [5; 6], [1; 1])); %! assert_equal (class (ts.Data), 'int8'); %!test %! ts = append (timeseries (int8 ([1; 2]), [0; 1]), ... %! timeseries ([3.5; 4], [2; 3])); %! assert_equal (ts.Data, int8 ([1; 2; 4; 4])); ## Test dates read in a day-first format keep their meaning when joined %!test %! a = setabstime (timeseries ([1; 2]), {'02/01/2024', '03/01/2024'}, ... %! 'dd/mm/yyyy'); %! b = setabstime (timeseries ([3; 4]), {'04/01/2024', '05/01/2024'}, ... %! 'dd/mm/yyyy'); %! assert_equal (append (a, b).Time, [0; 86400; 172800; 259200]); %!error ... %! append (A, timeseries ([60; 70], [3; 5], [1; 1])) %!error ... %! append (timeseries ([60; 70], [5; 6]), timeseries ([1; 2], [0; 1])) %!error ... %! append (A, A) %!error ... %! append (timeseries ([1; 2], [0; 1]), timeseries ([1, 2; 3, 4], [5; 6])) %!error append (A, 5) %!error ... %! append (Ab, timeseries ([4; 5], [3; 4])) %!error ... %! append (A, timeseries ([60; 70], [5; 6])) %!error ... %! append (timeseries ([1; 2], [0; 1]), timeseries ([3; 4], [2; 3], [1; 2])) %!assert_equal (hasduplicatetimes (A), false) %!assert_equal (hasduplicatetimes (D), true) %!assert_equal (hasduplicatetimes (timeseries ()), false) %!assert_equal (hasduplicatetimes (timeseries (5)), false) %!assert_equal (hasduplicatetimes (timeseries (ones (2, 2, 3), ... %! [0, 0, 1])), true) %!assert_equal (hasduplicatetimes ([A, D]), [false, true]) ################################################################################ ## ** Time representation ** ## ################################################################################ ## Available Methods ## ## ## ## 'getabstime' 'setabstime' 'setuniformtime' ## ## ## ################################################################################ %!shared R, I %! R = timeseries ([10; 20; 30], [0; 1; 2], [7; 8; 9], 'Name', 'r'); %! I = timeseries ([1; 2; 3], [1; 2; 3]); %!test %! ts = timeseries ([1; 2; 3], {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}); %! assert_equal (getabstime (ts), {'01-Jan-2024 00:00:00'; ... %! '02-Jan-2024 00:00:00'; ... %! '03-Jan-2024 00:00:00'}); %!test %! ts = timeseries ([1; 2; 3], [0; 1.5; 3]); %! ts.TimeInfo.StartDate = '01-Jan-2024'; %! ts.TimeInfo.Units = 'hours'; %! assert_equal (getabstime (ts), {'01-Jan-2024 00:00:00'; ... %! '01-Jan-2024 01:30:00'; ... %! '01-Jan-2024 03:00:00'}); %!test %! ts = timeseries ([1; 2; 3], [-1; 0; 1]); %! ts.TimeInfo.StartDate = '02-Jan-2024'; %! ts.TimeInfo.Units = 'days'; %! assert_equal (getabstime (ts){1}, '01-Jan-2024 00:00:00'); %!test %! ts = timeseries ([1; 2], [0; 1]); %! ts.TimeInfo.StartDate = '01-Jan-2024'; %! ts.TimeInfo.Units = 'weeks'; %! assert_equal (getabstime (ts){2}, '08-Jan-2024 00:00:00'); %!test %! ts = timeseries ([1; 2], [0; 1500]); %! ts.TimeInfo.StartDate = '01-Jan-2024'; %! ts.TimeInfo.Units = 'milliseconds'; %! assert_equal (getabstime (ts){2}, '01-Jan-2024 00:00:01'); %!test %! ts = timeseries ([1; 2], [0; 1]); %! ts.TimeInfo.StartDate = '2024-01-01'; %! assert_equal (getabstime (ts){2}, '01-Jan-2024 00:00:01'); ## Test seconds are rounded to the millisecond, then cut %!test %! ts = timeseries ([1; 2; 3; 4], [0; 0.5; 0.9999; 59.9999]); %! ts.TimeInfo.StartDate = '01-Jan-2024'; %! assert_equal (getabstime (ts), {'01-Jan-2024 00:00:00'; ... %! '01-Jan-2024 00:00:00'; ... %! '01-Jan-2024 00:00:01'; ... %! '01-Jan-2024 00:01:00'}); ## Test 'TimeInfo.Format' writes the dates %!test %! ts = timeseries ([1; 2], [0; 1.25]); %! ts.TimeInfo.StartDate = '15-Mar-2024 13:45:30'; %! ts.TimeInfo.Units = 'days'; %! ts.TimeInfo.Format = 'dd-mmm-yyyy'; %! assert_equal (getabstime (ts), {'15-Mar-2024'; '16-Mar-2024'}); %! ts.TimeInfo.Format = 'mm/dd/yyyy'; %! assert_equal (getabstime (ts), {'03/15/2024'; '03/16/2024'}); %! ts.TimeInfo.Format = 'HH:MM'; %! assert_equal (getabstime (ts), {'13:45'; '19:45'}); %! ts.TimeInfo.Format = 'HH:MM PM'; %! assert_equal (getabstime (ts), {' 1:45 PM'; ' 7:45 PM'}); %! ts.TimeInfo.Format = 'dd-mmm-yyyy HH:MM:SS.FFF'; %! assert_equal (getabstime (ts){1}, '15-Mar-2024 13:45:30.000'); ## Test any format is used, where MATLAB writes the default for this one %!test %! ts = timeseries ([1; 2], [0; 1.25]); %! ts.TimeInfo.StartDate = '15-Mar-2024 13:45:30'; %! ts.TimeInfo.Units = 'days'; %! ts.TimeInfo.Format = 'yyyy-mm-dd'; %! assert_equal (getabstime (ts), {'2024-03-15'; '2024-03-16'}); %!test %! ts = timeseries ([1; 2; 3], {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}); %! ts.TimeInfo.Units = 'hours'; %! assert_equal (getabstime (ts){3}, '01-Jan-2024 02:00:00'); %!test %! ts = timeseries (); %! ts.TimeInfo.StartDate = '01-Jan-2024'; %! assert_equal (getabstime (ts), {}); %!error getabstime (R) %!error getabstime ([R, R]) %!test %! ts = setabstime (R, {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}); %! assert_equal (ts.Time, [0; 86400; 172800]); %! assert_equal (ts.TimeInfo.Units, 'seconds'); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %! assert_equal (ts.TimeInfo.Increment, 86400); %! assert_equal (ts.Data, [10; 20; 30]); %! assert_equal (ts.Name, 'r'); ## Test unsorted dates move the samples with them, where MATLAB does not %!test %! ts = setabstime (R, {'03-Jan-2024', '01-Jan-2024', '02-Jan-2024'}); %! assert_equal (ts.Data, [20; 30; 10]); %! assert_equal (ts.Quality, [8; 9; 7]); %! assert_equal (ts.Time, [0; 86400; 172800]); %!test %! ts = setabstime (timeseries (reshape (1:12, 2, 2, 3)), ... %! {'03-Jan-2024', '01-Jan-2024', '02-Jan-2024'}); %! assert_equal (ts.Data, reshape ([5:12, 1:4], 2, 2, 3)); %!test %! ts = setabstime (R, ['01-Jan-2024'; '02-Jan-2024'; '03-Jan-2024']); %! assert_equal (ts.Time, [0; 86400; 172800]); %!test %! ts = setabstime (R, string ({'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'})); %! assert_equal (ts.Time, [0; 86400; 172800]); %!test %! ts = setabstime (R, {'01-Jan-2024'; '02-Jan-2024'; '03-Jan-2024'}); %! assert_equal (ts.Time, [0; 86400; 172800]); %!test %! ts = setabstime (timeseries (5), '01-Jan-2024 06:00:00'); %! assert_equal (ts.Time, 0); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 06:00:00'); ## Test a datetime is read as its dates, where MATLAB drops them %!test %! ts = setabstime (R, datetime (2024, 1, 1:3)); %! assert_equal (ts.Time, [0; 86400; 172800]); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %!test %! ts = setabstime (R, {'01-Jan-2024 00:00:00', '01-Jan-2024 12:00:00', ... %! '02-Jan-2024 00:00:00'}); %! assert_equal (ts.Time, [0; 43200; 86400]); %!test %! ts = timeseries ([1; 2; 3], [0; 1.5; 3]); %! ts.TimeInfo.Units = 'hours'; %! ts = setabstime (ts, {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}); %! assert_equal (ts.Time, [0; 24; 48]); %! assert_equal (ts.TimeInfo.Units, 'hours'); %!test %! ts = timeseries ([1; 2; 3], {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}); %! ts = setabstime (ts, {'05-Jan-2024', '06-Jan-2024', '07-Jan-2024'}); %! assert_equal (ts.Time, [0; 1; 2]); %! assert_equal (ts.TimeInfo.StartDate, '05-Jan-2024 00:00:00'); %!test %! ts = setabstime (R, {'01-Jan-2024', '01-Jan-2024', '02-Jan-2024'}); %! assert_equal (ts.Time, [0; 0; 86400]); %!test %! ts = setabstime (R, {'01-Jan-2024', '02-Jan-2024', '04-Jan-2024'}); %! assert_equal (ts.TimeInfo.Increment, NaN); %!test %! ts = setabstime (R, {'01/02/2024', '01/03/2024', '01/04/2024'}); %! assert_equal (ts.TimeInfo.StartDate, '02-Jan-2024 00:00:00'); %!test %! ts = I; %! ts.Events = tsdata.event ('e', 2); %! ts = setabstime (ts, {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}); %! assert_equal (ts.Events.Time, 2); %! assert_equal (ts.Events.StartDate, ''); ## Test each date is read by its own format, where MATLAB misreads the second %!test %! ts = setabstime (R, {'01-Jan-2024', '2024-01-02', '03-Jan-2024'}); %! assert_equal (ts.Time, [0; 86400; 172800]); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %!test %! ts = setabstime (R, {'02-Jan-2024 06:00:00', '03-Jan-2024', '04-Jan-2024'}); %! assert_equal (ts.Time, [0; 64800; 151200]); ## Test a format reads the dates, and the start date is stored in one form; ## MATLAB writes it in the format, which a later reading must guess %!test %! ts = setabstime (R, {'2024-01-01', '2024-01-02', '2024-01-03'}, ... %! 'yyyy-mm-dd'); %! assert_equal (ts.Time, [0; 86400; 172800]); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %!test %! ts = setabstime (R, {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}, ... %! 'dd-mmm-yyyy'); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %! assert_equal (ts.TimeInfo.Format, ''); %! assert_equal (getabstime (ts){2}, '02-Jan-2024 00:00:00'); ## Test a start date as stored is read back as a date anywhere, and dates ## finer than a millisecond find their samples %!test %! ts = timeseries ((1:2)', [0, 1]); %! ts.TimeInfo.StartDate = datetime (2024, 1, 1, 0, 0, 0.5); %! assert_equal (getsampleusingtime (ts, ts.TimeInfo.StartDate).Data, 1); %! assert_equal (addsample (ts, 'Data', 3, 'Time', ... %! '01-Jan-2024 00:00:00.250').Time, [-0.25; 0; 1]); %!test %! d = datetime (2024, 1, 1, 0, 0, 0.0004) + seconds (0:2); %! ts = timeseries ((1:3)', d); %! assert_equal (getsampleusingtime (ts, d(2)).Data, 2); %! assert_equal (resample (ts, d(2:3)).Data, [2; 3]); ## Test a day-first format; stored in its own format the start date read ## back as another month %!test %! ts = setabstime (R, {'02/01/2024', '03/01/2024', '04/01/2024'}, ... %! 'dd/mm/yyyy'); %! assert_equal (ts.TimeInfo.StartDate, '02-Jan-2024 00:00:00'); %! assert_equal (getabstime (ts){3}, '04-Jan-2024 00:00:00'); %! assert_equal (getsampleusingtime (ts, '03-Jan-2024').Data, 20); %!test %! ts = setabstime (R, {'2024-01-02 10:30', '2024-01-03 10:30', ... %! '2024-01-04 10:30'}, 'yyyy-mm-dd HH:MM'); %! assert_equal (ts.TimeInfo.StartDate, '02-Jan-2024 10:30:00'); %! assert_equal (ts.Time, [0; 86400; 172800]); ## Test a start date keeps its milliseconds %!test %! ts = timeseries ([1; 2], [0; 1]); %! ts.TimeInfo.StartDate = datetime (2024, 1, 1, 0, 0, 0.5); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00.500'); %! ts.TimeInfo.Format = 'HH:MM:SS.FFF'; %! assert_equal (getabstime (ts), {'00:00:00.500'; '00:00:01.500'}); ## Test times of day alone give times since midnight and no start date %!test %! ts = setabstime (I, {'13:00:00', '14:00:00', '15:00:00'}); %! assert_equal (ts.Time, [46800; 50400; 54000]); %! assert_equal (ts.TimeInfo.StartDate, ''); %!test %! ts = setabstime (timeseries (), {}); %! assert_equal (ts.Length, 0); %!error setabstime (R) %!error ... %! setabstime (R, datenum (2024, 1, 1:3)) %!error ... %! setabstime (R, [1, 2, 3]) %!error ... %! setabstime (R, {1, 2, 3}) %!error ... %! setabstime (R, [datetime(2024, 1, 1), NaT, datetime(2024, 1, 3)]) %!error ... %! setabstime (timeseries ([1; 2]), datetime (12000, 1, 1:2)) %!error ... %! setabstime (R, {'01-Jan-2024', '02-Jan-2024'}) %!error ... %! setabstime (R, '01-Jan-2024') %!error ... %! setabstime (R, {'bogus', '02-Jan-2024', '03-Jan-2024'}) %!error ... %! setabstime (R, {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}, 'yyyy-mm-dd') %!error ... %! setabstime (I, {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}, ... %! 'dd-mmm-yyyy HH:MM:SS') %!error ... %! setabstime (R, {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}, 5) %!error ... %! setabstime ([R, R], {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}) %!assert_equal (setuniformtime (I, 'StartTime', 10).Time, [10; 11; 12]) %!assert_equal (setuniformtime (I, 'Interval', 0.5).Time, [0; 0.5; 1]) %!assert_equal (setuniformtime (I, 'EndTime', 20).Time, [0; 10; 20]) %!assert_equal (setuniformtime (I, 'StartTime', 10, 'Interval', 2).Time, ... %! [10; 12; 14]) %!assert_equal (setuniformtime (I, 'StartTime', 10, 'EndTime', 20).Time, ... %! [10; 15; 20]) %!assert_equal (setuniformtime (I, 'Interval', 2, 'EndTime', 20).Time, ... %! [16; 18; 20]) %!assert_equal (setuniformtime (I, 'EndTime', 20, 'Interval', 2).Time, ... %! [16; 18; 20]) %!assert_equal (setuniformtime (I, 'StartTime', 10, 'Interval', 2, ... %! 'EndTime', 14).Time, [10; 12; 14]) %!assert_equal (setuniformtime (I, 'Interval', 2, 'Interval', 3).Time, ... %! [0; 3; 6]) %!assert_equal (setuniformtime (I, 'interval', 2).Time, [0; 2; 4]) %!assert_equal (setuniformtime (I, 'StartTime', -4).Time, [-4; -3; -2]) %!assert_equal (setuniformtime (I, 'Interval', 0).Time, [0; 0; 0]) %!assert_equal (setuniformtime (I, 'StartTime', 10, 'EndTime', 10).Time, ... %! [10; 10; 10]) %!assert_equal (setuniformtime (I, 'Interval', 0.1).Time, [0; 0.1; 0.2]) %!assert_equal (setuniformtime (I, 'StartTime', 0.1, 'EndTime', 0.3).Time, ... %! [0.1; 0.2; 0.3]) %!assert_equal (setuniformtime (timeseries (5, 3), 'StartTime', 10).Time, 10) %!assert_equal (setuniformtime (timeseries (5, 3), 'Interval', 2).Time, 0) ## Test one sample with equal start and end, which MATLAB refuses %!assert_equal (setuniformtime (timeseries (5, 3), 'StartTime', 10, ... %! 'EndTime', 10).Time, 10) %!test %! ts = setuniformtime (timeseries ([1; 2; 3; 4], [0; 1; 3; 7]), ... %! 'StartTime', 10); %! assert_equal (ts.Time, [10; 11; 12; 13]); %! assert_equal (ts.TimeInfo.Increment, 1); %!test %! ts = setuniformtime (R, 'Interval', 0.5); %! assert_equal (ts.Data, [10; 20; 30]); %! assert_equal (ts.Quality, [7; 8; 9]); %! assert_equal (ts.Name, 'r'); %!test %! ts = timeseries ([1; 2; 3], {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}); %! ts = setuniformtime (ts, 'Interval', 2); %! assert_equal (ts.Time, [0; 2; 4]); %! assert_equal (ts.TimeInfo.Units, 'days'); %! assert_equal (getabstime (ts){3}, '05-Jan-2024 00:00:00'); %!test %! ts = I; %! ts.Events = tsdata.event ('e', 2); %! ts = setuniformtime (ts, 'Interval', 2); %! assert_equal (ts.Events.Time, 2); %!test %! ts = setuniformtime (timeseries (reshape (1:12, 2, 2, 3)), 'Interval', 5); %! assert_equal (ts.Time, [0; 5; 10]); %! assert_equal (size (ts.Data), [2, 2, 3]); %!error ... %! setuniformtime (I) %!error ... %! setuniformtime (I, 'Interval') %!error ... %! setuniformtime (I, 'Bogus', 2) %!error ... %! setuniformtime (I, 'Interval', [1, 2]) %!error ... %! setuniformtime (I, 'Interval', 'x') %!error ... %! setuniformtime (I, 'StartTime', '05-Jan-2024') %!error ... %! setuniformtime (I, 'Interval', NaN) %!error ... %! setuniformtime (I, 'StartTime', Inf) %!error ... %! setuniformtime (I, 'EndTime', Inf) %!error ... %! setuniformtime (I, 'Interval', -1) %!error ... %! setuniformtime (I, 'StartTime', 20, 'EndTime', 10) %!error ... %! setuniformtime (I, 'EndTime', -4) %!error ... %! setuniformtime (I, 'StartTime', 10, 'Interval', 2, 'EndTime', 20) %!error ... %! setuniformtime (timeseries (5, 3), 'EndTime', 20) %!error ... %! setuniformtime (timeseries (5, 3), 'StartTime', 10, 'EndTime', 20) %!error ... %! setuniformtime (timeseries (), 'Interval', 2) %!error ... %! setuniformtime ([I, I], 'Interval', 2) ################################################################################ ## ** Events ** ## ################################################################################ ## Available Methods ## ## ## ## 'addevent' 'delevent' 'gettsbeforeevent' ## ## 'gettsbeforeatevent' 'gettsatevent' ## ## 'gettsafteratevent' 'gettsafterevent' ## ## 'gettsbetweenevents' ## ## ## ################################################################################ %!shared T, EA, AbE %! T = timeseries ((10:10:50)', (0:4)', (1:5)', 'Name', 't'); %! T.DataInfo.Units = 'volts'; %! EA = addevent (addevent (addevent (T, 'a', 1), 'b', 2), 'a', 3); %! AbE = timeseries ([1; 2; 3; 4], {'01-Jan-2024', '02-Jan-2024', ... %! '03-Jan-2024', '04-Jan-2024'}); %!test %! ts = addevent (T, 'x', 2); %! assert_equal (ts.Events.Name, 'x'); %! assert_equal (ts.Events.Time, 2); %! assert_equal (ts.Events.Units, 'seconds'); %! assert_equal (ts.Events.StartDate, ''); %! assert_equal (ts.Data, T.Data); %!test %! ts = addevent (T, tsdata.event ('obj', 2)); %! assert_equal (ts.Events.Name, 'obj'); %!test %! ts = addevent (T, [tsdata.event('p', 3), tsdata.event('q', 1)]); %! assert_equal ({ts.Events.Name}, {'p', 'q'}); %!test %! ts = addevent (T, {'p', 'q'}, {3, 1}); %! assert_equal ({ts.Events.Name}, {'p', 'q'}); %! assert_equal ([ts.Events.Time], [3, 1]); %!test %! ts = addevent (T, {'p'; 'q'}, {3; 1}); %! assert_equal ([ts.Events.Time], [3, 1]); %!test %! ts = addevent (T, {'p', 'q'}, [3, 1]); %! assert_equal ([ts.Events.Time], [3, 1]); %!test %! ts = addevent (T, string ({'p', 'q'}), {3, 1}); %! assert_equal ({ts.Events.Name}, {'p', 'q'}); %!test %! ts = addevent (T, string ('x'), 2); %! assert_equal (ts.Events.Name, 'x'); %!test %! ts = addevent (T, '', 2); %! assert_equal (ts.Events.Name, ''); %!test %! ts = addevent (T, 'x', int8 (2)); %! assert_equal (ts.Events.Time, 2); ## Test events are appended in the order given, never sorted %!test %! ts = addevent (EA, 'a', 0.5); %! assert_equal ([ts.Events.Time], [1, 2, 3, 0.5]); ## Test an event matching one held is not added again %!test %! ts = addevent (addevent (T, 'dup', 1), 'dup', 1); %! assert_equal (numel (ts.Events), 1); %!test %! ts = addevent (addevent (T, 'dup', 1), 'dup', 2); %! assert_equal ([ts.Events.Time], [1, 2]); %!test %! ts = addevent (T, 'x', 10); %! assert_equal (ts.Events.Time, 10); ## Test a number is in the series' units and from its start date %!test %! ts = T; %! ts.TimeInfo.Units = 'minutes'; %! ts = addevent (ts, 'x', 2); %! assert_equal (ts.Events.Units, 'minutes'); %!test %! ts = addevent (AbE, 'x', 1); %! assert_equal (ts.Events.Time, 1); %! assert_equal (ts.Events.Units, 'days'); %! assert_equal (ts.Events.StartDate, '01-Jan-2024 00:00:00'); ## Test a date on a series with a start date %!test %! ts = addevent (AbE, 'x', '02-Jan-2024'); %! assert_equal (ts.Events.Time, 0); %! assert_equal (ts.Events.Units, 'days'); %! assert_equal (ts.Events.StartDate, '02-Jan-2024 00:00:00'); %!test %! ts = addevent (AbE, 'x', datetime (2024, 1, 2, 12, 0, 0)); %! assert_equal (ts.Events.StartDate, '02-Jan-2024 12:00:00'); %!test %! ts = addevent (AbE, {'p', 'q'}, string ({'02-Jan-2024', '03-Jan-2024'})); %! assert_equal ({ts.Events.StartDate}, {'02-Jan-2024 00:00:00', ... %! '03-Jan-2024 00:00:00'}); %!test %! e = tsdata.event ('m', 1); %! e.Units = 'minutes'; %! ts = addevent (T, e); %! assert_equal (ts.Events.Units, 'minutes'); ## Test a datetime event time keeps its milliseconds %!test %! ts = timeseries ((1:3)', 0:2); %! ts.TimeInfo.StartDate = '01-Jan-2024'; %! ts = addevent (ts, 'a', datetime (2024, 1, 1, 0, 0, 1.5)); %! assert_equal (gettsafteratevent (ts, 'a').Time, 2); %!error addevent (T) %!error ... %! addevent (T, 'x', 1, 2) %!error ... %! addevent (T, 'x') %!error ... %! addevent (T, 5) %!error ... %! addevent (T, 5, 1) %!error ... %! addevent (T, tsdata.event ('x', 2), 5) %!error ... %! addevent (T, {'p', 'q'}, {3}) %!error ... %! addevent (T, 'x', [1, 2]) %!error ... %! addevent (T, 'x', true) %!error ... %! addevent (T, 'x', NaN) %!error ... %! addevent (AbE, 'x', 'bogus') ## Test a date on a series without a start date, which MATLAB reads as time 0 %!error ... %! addevent (T, 'x', '02-Jan-2024') %!error ... %! addevent (T, tsdata.event ('x', '02-Jan-2024')) %!error addevent ([T, T], 'x', 1) %!test %! ts = delevent (addevent (addevent (T, 'dup', 1), 'dup', 3), 'dup'); %! assert_equal (ts.Events.Time, 3); %!test %! ts = delevent (addevent (addevent (T, 'dup', 1), 'dup', 3), 'dup', 2); %! assert_equal (ts.Events.Time, 1); %!test %! ts = delevent (EA, 'a', 2); %! assert_equal ([ts.Events.Time], [1, 2]); %!test %! ts = delevent (EA, {'a', 'b'}); %! assert_equal (ts.Events.Time, 3); %!test %! ts = delevent (EA, string ('b')); %! assert_equal ({ts.Events.Name}, {'a', 'a'}); %!test %! ts = delevent (addevent (T, 'k', 1), 'k'); %! assert_equal (ts.Events, []); %!test %! ts = delevent (EA, 'a'); %! assert_equal (ts.Data, T.Data); %! assert_equal (ts.Name, 't'); %!error delevent (EA) %!error ... %! delevent (EA, 'a', 1, 2) %!error delevent (T, 'a') ## Test names and occurrences no event has, which MATLAB ignores %!error delevent (EA, 'zz') %!error delevent (EA, 'A') %!error ... %! delevent (EA, 'a', 3) %!error ... %! delevent (EA, {'a', 'b'}, 2) %!error ... %! delevent (EA, 'a', 0) %!error ... %! delevent (EA, 'a', 1.5) %!error ... %! delevent (EA, 'a', [1, 2]) %!error ... %! delevent (EA, 5) %!error ... %! delevent (EA, tsdata.event ('a', 1)) %!error delevent ([EA, EA], 'a') %!assert_equal (gettsbeforeevent (EA, 'a').Time, 0) %!assert_equal (gettsbeforeevent (EA, 'a', 2).Time, [0; 1; 2]) %!test %! ts = gettsbeforeevent (EA, 'a'); %! assert_equal (ts.Data, 10); %! assert_equal (ts.Quality, 1); %! assert_equal (ts.Name, 't'); %! assert_equal (ts.DataInfo.Units, 'volts'); %! assert_equal (numel (ts.Events), 3); %!assert_equal (gettsbeforeevent (addevent (T, 'far', 10), 'far').Time, ... %! (0:4)') %!error ... %! gettsbeforeevent (EA, 'zz') %!error ... %! gettsbeforeevent ([EA, EA], 'a') %!assert_equal (gettsbeforeatevent (EA, 'a').Time, [0; 1]) %!assert_equal (gettsbeforeatevent (EA, 'a', 2).Time, [0; 1; 2; 3]) %!error ... %! gettsbeforeatevent (EA, 'zz') %!error ... %! gettsbeforeatevent ([EA, EA], 'a') %!assert_equal (gettsatevent (EA, 'a').Time, 1) %!assert_equal (gettsatevent (EA, 'a', 2).Time, 3) %!assert_equal (gettsatevent (EA, string ('b')).Time, 2) ## Test an event between samples meets none, and repeated times all meet it %!test %! ts = gettsatevent (addevent (T, 'h', 2.5), 'h'); %! assert_equal (ts.Length, 0); %! assert_equal (ts.Name, 't'); %! assert_equal (ts.Events.Name, 'h'); %!assert_equal (gettsatevent (addevent (timeseries ([1; 2; 3], [0; 1; 1]), ... %! 'd', 1), 'd').Data, [2; 3]) %!assert_equal (gettsatevent (addevent (AbE, 'x', '02-Jan-2024'), 'x').Data, 2) %!assert_equal (gettsatevent (addevent (AbE, 'x', 1), 'x').Data, 2) %!assert_equal (gettsatevent (addevent (AbE, 'x', ... %! '02-Jan-2024 12:00:00'), 'x').Length, 0) ## Test an event in other units finds the sample it names, as in MATLAB %!test %! ts = timeseries ((1:101)', 0:0.001:0.1); %! ts.TimeInfo.Units = 'minutes'; %! e = tsdata.event ('e', 2.58); %! e.Units = 'seconds'; %! assert_equal (gettsatevent (addevent (ts, e), 'e').Data, 44); %! assert_equal (gettsafteratevent (addevent (ts, e), 'e').Length, 58); ## Test a sample at exactly the event's time is found, whatever rounding a ## round trip through another unit gives %!test %! ts = timeseries ((1:101)', 0:0.001:0.1); %! ts = addevent (ts, 'e', ts.Time(44)); %! assert_equal (gettsatevent (ts, 'e').Data, 44); %! assert_equal (gettsafteratevent (ts, 'e').Time(1), ts.Time(44)); %!error ... %! gettsatevent (EA, 'zz') %!error ... %! gettsatevent ([EA, EA], 'a') %!assert_equal (gettsafteratevent (EA, 'a').Time, [1; 2; 3; 4]) %!assert_equal (gettsafteratevent (EA, 'a', 2).Time, [3; 4]) %!error ... %! gettsafteratevent (EA, 'zz') %!error ... %! gettsafteratevent ([EA, EA], 'a') %!assert_equal (gettsafterevent (EA, 'a').Time, [2; 3; 4]) %!assert_equal (gettsafterevent (EA, 'a', 2).Time, 4) %!assert_equal (gettsafterevent (addevent (T, 'h', 2.5), 'h').Time, [3; 4]) %!assert_equal (gettsafterevent (addevent (T, 'far', 10), 'far').Length, 0) ## Test an event object, which need not belong to the series %!assert_equal (gettsafterevent (EA, tsdata.event ('obj', 2)).Time, [3; 4]) %!assert_equal (gettsafterevent (T, tsdata.event ('obj', 2)).Time, [3; 4]) %!assert_equal (gettsafterevent (T, tsdata.event ('obj', 2), 1).Time, [3; 4]) ## Test the event's time is converted to the series' units and start date %!test %! e = tsdata.event ('m', 1); %! e.Units = 'minutes'; %! assert_equal (gettsafterevent (T, e).Length, 0); %! assert_equal (gettsafterevent (addevent (T, e), 'm').Length, 0); %!test %! h = timeseries ([1; 2; 3], [0; 1; 2]); %! h.TimeInfo.Units = 'hours'; %! assert_equal (gettsafterevent (h, tsdata.event ('s', 3600)).Time, 2); %! assert_equal (gettsafterevent (addevent (h, 'n', 1), 'n').Time, 2); %!assert_equal (gettsafterevent (AbE, tsdata.event ('y', 1)).Time, [1; 2; 3]) %!test %! ts = addevent (AbE, 'x', '02-Jan-2024 12:00:00'); %! assert_equal (gettsafterevent (ts, 'x').Data, [3; 4]); %!error gettsafterevent (T, 'a') ## Test names and occurrences no event has, where MATLAB returns an empty series %!error ... %! gettsafterevent (EA, 'zz') %!error ... %! gettsafterevent (EA, 'A') %!error ... %! gettsafterevent (EA, 'b', 2) %!error ... %! gettsafterevent (EA, 'a', 3) %!error ... %! gettsafterevent (EA, 'a', 0) %!error ... %! gettsafterevent (EA, 'a', 1.5) %!error ... %! gettsafterevent (EA, tsdata.event ('obj', 2), 2) %!error ... %! gettsafterevent (EA, 2) %!error ... %! gettsafterevent (EA, {'a'}) %!error ... %! gettsafterevent (EA) %!error ... %! gettsafterevent (EA, 'a', 1, 2) ## Test a dated event on a series without a start date, which MATLAB reads as 0 %!error ... %! gettsafterevent (T, tsdata.event ('x', '02-Jan-2024')) %!error ... %! gettsafterevent ([EA, EA], 'a') %!assert_equal (gettsbetweenevents (EA, 'a', 'b').Time, [1; 2]) %!assert_equal (gettsbetweenevents (EA, 'a', 'a').Time, 1) %!assert_equal (gettsbetweenevents (EA, 'a', 'a', 1, 2).Time, [1; 2; 3]) %!assert_equal (gettsbetweenevents (EA, 'b', 'a', 1, 2).Time, [2; 3]) %!assert_equal (gettsbetweenevents (EA, tsdata.event ('o', 2), 'a', ... %! 1, 2).Time, [2; 3]) ## Test the second event before the first gives no samples %!test %! ts = gettsbetweenevents (EA, 'b', 'a'); %! assert_equal (ts.Length, 0); %! assert_equal (ts.Name, 't'); %!assert_equal (gettsbetweenevents (EA, 'a', 'b', 2, 1).Length, 0) %!error ... %! gettsbetweenevents (EA, 'a') %!error ... %! gettsbetweenevents (EA, 'a', 'b', 2) %!error ... %! gettsbetweenevents (EA, 'a', 'b', 1, 1, 1) %!error ... %! gettsbetweenevents (EA, 'a', 'zz') %!error ... %! gettsbetweenevents ([EA, EA], 'a', 'b') ################################################################################ ## ** Interpolation, resampling, synchronising ** ## ################################################################################ ## Available Methods ## ## ## ## 'getinterpmethod' 'setinterpmethod' 'resample' 'synchronize' ## ## ## ################################################################################ %!shared TR, TQ %! TR = timeseries ([10; 20; 40; 80; 160], (0:4)', (1:5)', 'Name', 't'); %! TR.DataInfo.Units = 'volts'; %! TR.Events = tsdata.event ('e', 2); %! TQ = TR; %! TQ.QualityInfo.Code = 1:9; %!assert_equal (getinterpmethod (TR), 'linear') %!assert_equal (getinterpmethod (setinterpmethod (TR, 'zoh')), 'zoh') %!assert_equal (getinterpmethod (setinterpmethod (TR, @(a, b, c) c)), ... %! 'myFuncHandle') %!error ... %! getinterpmethod ([TR, TR]) %!assert_equal (getinterpmethod (setinterpmethod (TR, 'ZOH')), 'zoh') %!assert_equal (getinterpmethod (setinterpmethod (TR, string ('zoh'))), 'zoh') %!assert_equal (getinterpmethod (setinterpmethod (TR, ... %! tsdata.interpolation ('zoh'))), 'zoh') %!test %! ts = setinterpmethod (TR, @(nt, ot, od) nt); %! assert_equal (class (ts.DataInfo.Interpolation.Fhandle), 'function_handle'); %!error ... %! setinterpmethod (TR) %!error ... %! setinterpmethod (TR, 'cubic') %!error ... %! setinterpmethod (TR, 5) %!error ... %! setinterpmethod (TR, {@(a, b, c) c}) %!error ... %! setinterpmethod ([TR, TR], 'zoh') %!test %! ts = resample (TR, [0.5, 2.5]); %! assert_equal (ts.Data, [15; 60]); %! assert_equal (ts.Time, [0.5; 2.5]); %! assert_equal (ts.Quality, [2; 4]); %! assert_equal (ts.Name, 't'); %! assert_equal (ts.DataInfo.Units, 'volts'); %! assert_equal (ts.Events.Name, 'e'); %!assert_equal (resample (TR, [0.5; 2.5]).Data, [15; 60]) %!assert_equal (resample (TR, [2.5, 0.5]).Time, [0.5; 2.5]) %!test %! ts = resample (TR, [1, 1, 2]); %! assert_equal (ts.Data, [20; 20; 40]); %! assert_equal (ts.Quality, [2; 2; 3]); %!assert_equal (resample (TR, 0:4).Data, [10; 20; 40; 80; 160]) %!assert_equal (resample (TR, [0.5, 2.5], 'Linear').Data, [15; 60]) %!assert_equal (resample (TR, [0.5, 2.5], []).Data, [15; 60]) %!assert_equal (resample (timeseries ([1, 2; 3, 4; 5, 6], [0; 1; 2]), ... %! [0.5, 1.5]).Data, [2, 3; 4, 5]) %!assert_equal (resample (timeseries (reshape (1:12, 2, 2, 3), [0, 1, 2]), ... %! [0.5, 1.5]).Data, reshape (3:10, 2, 2, 2)) %!test %! ts = timeseries (reshape (1:12, 2, 2, 3), [0, 1, 2], ... %! reshape (mod (1:12, 3), 2, 2, 3)); %! ts = resample (ts, [0.5, 1.5]); %! assert_equal (ts.Quality(:)', [2, 0, 1, 2, 0, 1, 2, 0]); %!test %! ts = timeseries ([1; 2; 3], {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}); %! r = resample (ts, {'01-Jan-2024 12:00:00', '02-Jan-2024 12:00:00'}); %! assert_equal (r.Data, [1.5; 2.5]); %! assert_equal (r.Time, [0.5; 1.5]); %! assert_equal (r.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %! assert_equal (resample (ts, [0.5, 1.5]).Data, [1.5; 2.5]); %! assert_equal (resample (ts, datetime (2024, 1, 1:2, 12, 0, 0)).Data, ... %! [1.5; 2.5]); %!assert_equal (resample (timeseries ([1; 2; 3; 4], [0; 1; 1; 2]), ... %! [0.9, 1, 1.1]).Data, [1.9; 3; 3.1], 1e-14) %!assert_equal (resample (timeseries (int8 ([10; 20; 30]), [0; 1; 2]), ... %! [0.5, 1.5]).Data, int8 ([15; 25])) %!assert_equal (resample (timeseries (logical ([1; 0; 1]), [0; 1; 2]), ... %! [0.5, 1.5]).Data, [true; true]) ## Test NaN samples are left out, whatever 'TreatNaNasMissing' says %!test %! ts = timeseries ([1; NaN; 3; 4], [0; 1; 2; 3]); %! ts.TreatNaNasMissing = false; %! assert_equal (resample (ts, [0.5, 1.5, 2.5]).Data, [1.5; 2.5; 3.5]); %!assert_equal (resample (timeseries ([NaN; 2; 3; NaN], [0; 1; 2; 3]), ... %! [0, 0.5, 1.5, 2.5, 3]).Data, [NaN; NaN; 2.5; NaN; NaN]) ## Test 'zoh' holds each value, and its code, from the sample before %!test %! ts = resample (TR, [0.4, 0.6, 2.4, 2.6], 'zoh'); %! assert_equal (ts.Data, [10; 10; 40; 40]); %! assert_equal (ts.Quality, [1; 1; 3; 3]); %!test %! ts = resample (setinterpmethod (TR, 'zoh'), [0.5, 2.5]); %! assert_equal (ts.Data, [10; 40]); %! assert_equal (ts.Quality, [1; 3]); %!assert_equal (resample (TR, [0.4, 0.6, 2.4, 2.6]).Quality, [1; 2; 3; 4]) ## Test outside the series the data is NaN and the code the nearest sample's %!test %! ts = resample (TR, [-1, 0.5, 4, 5]); %! assert_equal (ts.Data, [NaN; 15; 160; NaN]); %! assert_equal (ts.Quality, [1; 2; 5; 5]); %!assert_equal (resample (setinterpmethod (TR, 'zoh'), ... %! [-1, 0.5, 4, 5]).Data, [NaN; 10; 160; NaN]) ## Test 'code' goes to the times the series did not hold %!assert_equal (resample (TQ, [0.5, 1, 2.5], 'linear', 7).Quality, [7; 2; 7]) %!assert_equal (resample (TQ, [0.5, 2.5], 'linear', []).Quality, [2; 4]) ## Test a user function is called as MATLAB calls it %!test %! fun = @(newtime, oldtime, olddata) ... %! interp1 (oldtime, olddata, newtime, 'linear', 'extrap'); %! ts = resample (setinterpmethod (TR, fun), [0.5, 2.5, 6]); %! assert_equal (ts.Data, [15; 60; 320]); %! assert_equal (ts.Quality, [2; 4; 5]); %!test %! f = @(nt, ot, od) interp1 (ot, od, nt); %! ts = resample (setinterpmethod (timeseries (reshape (1:12, 2, 2, 3), ... %! [0, 1, 2]), f), [0.5, 1.5]); %! assert_equal (ts.Data, reshape (3:10, 2, 2, 2)); %!assert_equal (resample (setinterpmethod (TR, @(nt, ot, od) ... %! 2 * ones (numel (nt), 1)), ... %! [0.5, 1.5]).Data, [2; 2]) %!test %! ts = resample (TR, []); %! assert_equal (ts.Length, 0); %! assert_equal (ts.Name, 't'); %!assert_equal (resample (timeseries (), [0, 1]).Length, 0) ## Test dates as text land exactly on the times they name %!test %! ts = timeseries ([1; 2; 3], [0; 1; 2], [1; 2; 3]); %! ts.TimeInfo.StartDate = '01-Jan-2020 00:00:00'; %! ts.QualityInfo.Code = [1, 2, 3, 9]; %! ts.QualityInfo.Description = {'a', 'b', 'c', 'n'}; %! r = resample (ts, {'01-Jan-2020 00:00:01', '01-Jan-2020 00:00:02'}, ... %! 'linear', 9); %! assert_equal (r.Time, [1; 2]); %! assert_equal (r.Data, [2; 3]); %! assert_equal (r.Quality, [2; 3]); ## Test a value held past a NaN sample takes the code of the one it is held ## from %!test %! ts = timeseries ([10; NaN; 30; 40], [0; 1; 2; 3], [1; 2; 3; 4]); %! ts.QualityInfo.Code = [1, 2, 3, 4]; %! ts.QualityInfo.Description = {'a', 'b', 'c', 'd'}; %! r = resample (ts, [1, 1.5], 'zoh'); %! assert_equal (r.Data, [10; 10]); %! assert_equal (r.Quality, [1; 1]); %!error resample (TR) ## Test samples all at one time resample right-continuously %!assert_equal (resample (timeseries ([2; 3], [1, 1]), [1, 2]).Data, [3; NaN]) %!assert_equal (resample (timeseries ([2; 3; NaN], [1, 1, 2]), ... %! [1, 1.5]).Data, [3; NaN]) %!error ... %! resample (timeseries (5, 1), [0, 1, 2]) %!error ... %! resample (TR, [0.5, 2.5], 'cubic') %!error ... %! resample (TR, [0.5, 2.5], @(nt, ot, od) nt) %!error ... %! resample (TR, 'x') %!error resample (TR, [0.5, NaN]) %!error resample (TR, Inf) %!error ... %! resample (TR, [0.5, 2.5], 'linear', 7) %!error ... %! resample (TQ, [0.5, 2.5], 'linear', 7.5) %!error ... %! resample (timeseries ((1:5)'), [0.5, 2.5], 'linear', 7) %!error ... %! resample (setinterpmethod (TR, @(nt, ot, od) [1, 2, 3]), [0.5, 1.5]) %!error ... %! resample ([TR, TR], [0.5, 1.5]) %!shared S1, S2 %! S1 = timeseries ([1; 2; 3; 4], [0; 1; 2; 3], 'Name', 'a'); %! S1.DataInfo.Units = 'volts'; %! S2 = timeseries ([10; 20; 30], [0.5; 1.5; 4], 'Name', 'b'); %!test %! [a, b] = synchronize (S1, S2, 'union'); %! assert_equal (a.Time, [0.5; 1; 1.5; 2; 3]); %! assert_equal (a.Data, [1.5; 2; 2.5; 3; 4]); %! assert_equal (b.Data, [10; 15; 20; 22; 26]); %! assert_equal (a.Name, 'a'); %! assert_equal (b.Name, 'b'); %! assert_equal (a.DataInfo.Units, 'volts'); %!test %! [a, b] = synchronize (S1, S2, 'Union'); %! assert_equal (b.Time, [0.5; 1; 1.5; 2; 3]); %!test %! [a, b] = synchronize (S1, S2, 'intersection'); %! assert_equal (a.Length, 0); %! assert_equal (b.Length, 0); %!test %! [a, b] = synchronize (S1, S2, 'uniform', 'Interval', 0.5); %! assert_equal (a.Time, [0.5; 1; 1.5; 2; 2.5; 3]); %! assert_equal (b.Data, [10; 15; 20; 22; 24; 26]); %!test %! [a, b] = synchronize (S1, S2, 'uniform'); %! assert_equal (a.Time, [0.5; 1.5; 2.5]); %!test %! [a, b] = synchronize (S1, S2, 'union', 'InterpMethod', 'zoh'); %! assert_equal (a.Data, [1; 2; 2; 3; 4]); %! assert_equal (b.Data, [10; 10; 20; 20; 20]); %!test %! [a, b] = synchronize (S1, S2, 'union', 'interpmethod', 'zoh'); %! assert_equal (b.Data, [10; 10; 20; 20; 20]); %!test %! [a, b] = synchronize (S1, timeseries ([5; 6], [10; 11]), 'union'); %! assert_equal (a.Length, 0); %!test %! [a, b] = synchronize (S1, S1, 'union'); %! assert_equal (a.Data, [1; 2; 3; 4]); %!test %! z = timeseries ([10; 20; 40; 80; 160], (0:4)', (1:5)'); %! z = setinterpmethod (z, 'zoh'); %! [a, b] = synchronize (z, S2, 'union'); %! assert_equal (a.Data, [10; 20; 20; 40; 80; 160]); %! assert_equal (a.Quality, [1; 2; 2; 3; 4; 5]); %! assert_equal (b.Data, [10; 15; 20; 22; 26; 30]); %!test %! [a, b] = synchronize (timeseries (reshape (1:12, 2, 2, 3), [0, 1, 2]), ... %! timeseries (reshape (1:8, 2, 2, 2), [0.5, 1.5]), ... %! 'union'); %! assert_equal (a.Time, [0.5; 1; 1.5]); %! assert_equal (a.Data(:)', [3, 4, 5, 6, 5, 6, 7, 8, 7, 8, 9, 10]); ## Test near-equal times match within the tolerance, the second's time kept %!test %! b2 = timeseries ([10; 20; 30], [1 + 1e-11; 2; 2.5]); %! [a, b] = synchronize (timeseries ([1; 2; 3; 4], [0; 1; 2; 3]), b2, ... %! 'intersection'); %! assert_equal (a.Time, [1 + 1e-11; 2]); %! [a, b] = synchronize (timeseries ([1; 2; 3; 4], [0; 1; 2; 3]), b2, ... %! 'intersection', 'tolerance', 1e-12); %! assert_equal (a.Time, 2); ## Test a larger tolerance widens the match, which MATLAB ignores %!test %! a4 = timeseries ([1; 2; 3; 4], [0; 1; 2; 3]); %! b4 = timeseries ([10; 20], [1.05; 2.05]); %! [a, b] = synchronize (a4, b4, 'intersection'); %! assert_equal (a.Length, 0); %! [a, b] = synchronize (a4, b4, 'intersection', 'tolerance', 0.1); %! assert_equal (a.Time, [1.05; 2.05]); %! assert_equal (a.Data, [2; 3]); %! assert_equal (b.Data, [10; 20]); %! [a, b] = synchronize (a4, b4, 'union', 'tolerance', 0.1); %! assert_equal (a.Time, [1.05; 2.05]); %! [a, b] = synchronize (a4, b4, 'union'); %! assert_equal (a.Time, [1.05; 2; 2.05]); ## Test each series gives its own sample at a time matched within the ## tolerance, even past its last sample %!test %! [x, y] = synchronize (timeseries ([1; 2; 3], [0; 1; 2]), ... %! timeseries ([10; 20], [2.05; 3]), 'union', ... %! 'tolerance', 0.1); %! assert_equal (x.Time, 2.05); %! assert_equal (x.Data, 3); %! assert_equal (y.Data, 10); ## Test the times a series holds stay exact, and are not given the code %!test %! ts = timeseries ((1:101)', 0:0.001:0.1, zeros (101, 1)); %! ts.QualityInfo.Code = [0, 9]; %! ts.QualityInfo.Description = {'ok', 'new'}; %! [a, b] = synchronize (ts, ts, 'union', 'QualityCode', 9); %! assert_equal (a.Time, ts.Time); %! assert_equal (a.Quality, ts.Quality); ## Test a series of one sample is evaluated at its own time, as in MATLAB %!test %! [x, y] = synchronize (timeseries (7, 1), timeseries ([1; 2; 3], ... %! [0; 1; 2]), 'union'); %! assert_equal ([x.Time, x.Data, y.Data], [1, 7, 2]); ## Test series that do not overlap come back with no samples, one sample ## each included %!test %! [x, y] = synchronize (timeseries (5, 10), timeseries ([1; 2; 3], ... %! [0; 1; 2]), 'union'); %! assert_equal ([x.Length, y.Length], [0, 0]); ## Test dated series that do not overlap still count from the earlier date %!test %! a = timeseries ([1; 2], [0; 1]); %! a.TimeInfo.Units = 'days'; %! a.TimeInfo.StartDate = '01-Jan-2024'; %! b = a; %! b.TimeInfo.StartDate = '10-Jan-2024'; %! [x, y] = synchronize (a, b, 'union'); %! assert_equal ({x.TimeInfo.StartDate, y.TimeInfo.StartDate}, ... %! {'01-Jan-2024 00:00:00', '01-Jan-2024 00:00:00'}); ## Test times in different units are converted exactly, where MATLAB is not %!test %! m = timeseries ([7; 8; 9], [0; 1; 2]); %! m.TimeInfo.Units = 'minutes'; %! [a, b] = synchronize (S1, m, 'union'); %! assert_equal (a.Time, [0; 1; 2; 3]); %! assert_equal (b.Time, [0; 1; 2; 3] / 60); %! assert_equal (b.TimeInfo.Units, 'minutes'); ## Test dated series count from the earlier start date, or keep their own %!test %! d1 = timeseries ([1; 2; 3], {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}); %! d2 = timeseries ([4; 5; 6], {'01-Jan-2024 12:00:00', ... %! '02-Jan-2024 12:00:00', ... %! '03-Jan-2024 12:00:00'}); %! [a, b] = synchronize (d2, d1, 'union'); %! assert_equal (a.Time, [0.5; 1; 1.5; 2]); %! assert_equal (a.Data, [4; 4.5; 5; 5.5]); %! assert_equal (b.Data, [1.5; 2; 2.5; 3]); %! assert_equal (a.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %! [a, b] = synchronize (d2, d1, 'union', 'KeepOriginalTimes', true); %! assert_equal (a.Time, [0; 0.5; 1; 1.5]); %! assert_equal (a.TimeInfo.StartDate, '01-Jan-2024 12:00:00'); %! assert_equal (b.Time, [0.5; 1; 1.5; 2]); %! [a, b] = synchronize (d2, d1, 'union', 'KeepOriginalTimes', 1); %! assert_equal (a.TimeInfo.StartDate, '01-Jan-2024 12:00:00'); ## Test quality codes follow the rules of 'resample' %!test %! q1 = timeseries ([1; 2; 3; 4], [0; 1; 2; 3], [1; 2; 3; 4]); %! q1.QualityInfo.Code = 1:9; %! q2 = timeseries ([10; 20; 30], [0.5; 1.5; 2.5], [5; 6; 7]); %! q2.QualityInfo.Code = 1:9; %! [a, b] = synchronize (q1, q2, 'union'); %! assert_equal (a.Quality, [2; 2; 3; 3; 4]); %! assert_equal (b.Quality, [5; 6; 6; 7; 7]); %! [a, b] = synchronize (q1, q2, 'union', 'QualityCode', 9); %! assert_equal (a.Quality, [9; 2; 9; 3; 9]); %! assert_equal (b.Quality, [5; 9; 6; 9; 7]); %! [a, b] = synchronize (q1, q2, 'union', 'InterpMethod', 'zoh'); %! assert_equal (a.Quality, [1; 2; 2; 3; 3]); %! assert_equal (b.Quality, [5; 5; 6; 6; 7]); %!error ... %! [a, b] = synchronize (S1, S2); %!error ... %! [a, b] = synchronize (S1, S2, 'bogus'); %!error ... %! [a, b] = synchronize (S1, S2, 'union', 'TimeTolerance', 1); %!error ... %! [a, b] = synchronize (S1, S2, 'union', 'Interval'); %!error ... %! [a, b] = synchronize (S1, S2, 'uniform', 'Interval', 0); %!error ... %! [a, b] = synchronize (S1, S2, 'uniform', 'Interval', -1); %!error ... %! [a, b] = synchronize (S1, S2, 'union', 'InterpMethod', 'cubic'); %!error ... %! [a, b] = synchronize (S1, S2, 'union', 'KeepOriginalTimes', 'yes'); %!error ... %! [a, b] = synchronize (S1, S2, 'union', 'tolerance', -1); %!error ... %! [a, b] = synchronize (S1, S2, 'union', 'QualityCode', 7); %!error ... %! synchronize (5, timeseries ([1; 2; 3], [0; 1; 2]), 'union') %!error ... %! [a, b] = synchronize (S1, 5, 'union'); %!error ... %! [a, b] = synchronize (S1, timeseries (), 'union'); ## Test a dated series with an undated one, which MATLAB reads as undated %!error ... %! [a, b] = synchronize (S1, timeseries ([1; 2], {'01-Jan-2024', ... %! '02-Jan-2024'}), 'union'); %!error ... %! [a, b] = synchronize ([S1, S1], S2, 'union'); ################################################################################ ## ** Statistics ** ## ################################################################################ ## Available Methods ## ## ## ## 'mean' 'median' 'mode' 'std' ## ## 'var' 'sum' 'min' 'max' ## ## 'iqr' 'getqualitydesc' ## ## ## ################################################################################ %!shared SV, SNU, SM, ST3, SN, SNF, SQ, SI8, SL %! SV = timeseries ([1; 2; 4; 8; 16], (0:4)'); %! SNU = timeseries ([1; 2; 4; 8; 16], [0; 1; 3; 7; 8]); %! SM = timeseries ([1, 10; 2, 20; 4, 40], [0; 1; 2]); %! ST3 = timeseries (reshape ([1 5 2 9 3 4 8 6 7 12 10 11], 2, 2, 3), ... %! [0, 1, 2]); %! SN = timeseries ([1; NaN; 3; 100], [0; 1; 2; 3]); %! SNF = SN; %! SNF.TreatNaNasMissing = false; %! SQ = timeseries ([10; 20; 30; 40; 50], (0:4)', [1; 1; 0; 0; 1]); %! SI8 = timeseries (int8 ([1; 2; 4; 9]), (0:3)'); %! SL = timeseries (logical ([1; 0; 1; 1]), (0:3)'); %!assert_equal (mean (SV), 6.2) %!assert_equal (mean (SM), [7, 70] / 3, 1e-14) %!assert_equal (mean (ST3), [11, 20; 21, 26] / 3, 1e-14) %!assert_equal (mean (SN), 104 / 3, 1e-14) %!assert_equal (mean (SNF), NaN) %!assert_equal (mean (timeseries ([NaN; 2; 3; NaN], [0; 1; 2; 3])), 2.5) %!assert_equal (mean (timeseries ()), []) %!assert_equal (mean (timeseries (5, 0)), 5) %!assert_equal (mean (SI8), 4) %!assert_equal (mean (SL), 0.75) %!assert_equal (mean (SQ, 'Quality', 1), 35) %!assert_equal (mean (SQ, 'quality', 1), 35) %!assert_equal (mean (SQ, 'Quality', [0, 1]), NaN) %!assert_equal (mean (SN, 'MissingData', 'interpolate'), 26.5) %!assert_equal (mean (SN, 'MissingData', 'remove'), 104 / 3, 1e-14) %!assert_equal (mean (SNU, 'Weighting', 'time'), 52 / 9, 1e-14) %!assert_equal (mean (SV, 'Weighting', 'time'), 6.2, 1e-14) %!assert_equal (mean (SNU, 'Weighting', 'none'), 6.2) %!assert_equal (mean (timeseries ([1; NaN; 4; 8], [0; 1; 3; 4]), ... %! 'MissingData', 'interpolate', 'Weighting', 'time'), ... %! 3.6, 1e-14) %!test %! Q3 = timeseries (reshape (1:12, 2, 2, 3), [0, 1, 2], ... %! reshape ([0 1 0 1 1 1 0 0 0 1 0 1], 2, 2, 3)); %! assert_equal (mean (Q3, 'Quality', 1), [5, 7; NaN, 8]); ## Test a value left without samples is NaN whatever the class of the data %!test %! ts = timeseries (int8 ([10; 20; 30]), [0; 1; 2], [1; 1; 1]); %! assert_equal (median (ts, 'Quality', 1), NaN); %! assert_equal (min (ts, 'Quality', 1), NaN); %! assert_equal (median (ts), int8 (20)); %!test %! ts = timeseries (logical ([1; 0; 1]), [0; 1; 2], [1; 1; 1]); %! assert_equal (max (ts, 'Quality', 1), NaN); %! assert_equal (max (ts), true); ## Test a code no sample has warns even where other codes match %!warning ... %! mean (timeseries ([10; 20; 30], [0; 1; 2], [1; 0; 0]), 'Quality', [1, 5]); %!warning ... %! mean (timeseries ([10; 20; 30], [0; 1; 2], [1; 0; 0]), 'Quality', ... %! [1, 5, 7]); %!warning ... %! mean (SQ, 'Quality', 7); %!error ... %! mean (SV, 'Bogus', 1) %!error mean (SV, 1) %!error ... %! mean (SV, 'MissingData', 'bogus') %!error ... %! mean (SV, 'Weighting', 'bogus') %!error ... %! mean (SQ, 'Quality', 1.5) %!error mean ([SV, SV]) %!assert_equal (median (SV), 4) %!assert_equal (median (SM), [2, 20]) %!assert_equal (median (ST3), [3, 8; 5, 9]) %!assert_equal (median (SN), 3) %!assert_equal (median (SNF), NaN) %!assert_equal (median (SI8), int8 (3)) %!assert_equal (median (SL), true) %!assert_equal (median (SQ, 'Quality', 1), 35) %!assert_equal (median (SN, 'MissingData', 'interpolate'), 2.5) ## Test the time-weighted median, where MATLAB gives 6.667 %!assert_equal (median (SNU, 'Weighting', 'time'), 52 / 11, 1e-14) %!assert_equal (median (SV, 'Weighting', 'time'), 4) %!error ... %! median (SV, 'Bogus', 1) %!assert_equal (mode (SV), 1) %!assert_equal (mode (SM), [1, 10]) %!assert_equal (mode (SNF), 1) %!assert_equal (mode (SI8), int8 (1)) %!assert_equal (mode (SL), true) %!assert_equal (mode (timeseries ([3; 1; 3; 1; 2], (0:4)')), 1) ## Test the value of most weight, where MATLAB gives 0.5556 %!assert_equal (mode (SNU, 'Weighting', 'time'), 4) %!error ... %! mode (SV, 'Bogus', 1) %!assert_equal (std (SV), sqrt (37.2), 1e-14) %!assert_equal (std (SM), sqrt ([7, 700] / 3), 1e-14) %!assert_equal (std (SNF), NaN) %!assert_equal (std (timeseries (5, 0)), 0) %!assert_equal (std (SI8), sqrt (38 / 3), 1e-14) %!assert_equal (std (SL), 0.5) %!assert_equal (std (SQ, 'Quality', 1), sqrt (50), 1e-14) ## Test the time-weighted spread, where MATLAB gives 4.558 %!assert_equal (std (SNU, 'Weighting', 'time'), sqrt (23.688271604938272), ... %! 1e-12) %!assert_equal (std (SV, 'Weighting', 'time'), sqrt (37.2), 1e-14) %!error std (SV, 'Bogus', 1) %!assert_equal (var (SV), 37.2, 1e-14) %!assert_equal (var (ST3), [28, 52; 57, 19] / 3, 1e-13) %!assert_equal (var (SN), 3202 + 1 / 3, 1e-10) %!assert_equal (var (timeseries (5, 0)), 0) %!assert_equal (var (SI8), 38 / 3, 1e-14) %!assert_equal (var (SNU, 'Weighting', 'time'), 23.688271604938272, 1e-12) %!error var (SV, 'Bogus', 1) %!assert_equal (sum (SV), 31) %!assert_equal (sum (SM), [7, 70]) %!assert_equal (sum (SNF), NaN) %!assert_equal (sum (SI8), 16) %!assert_equal (sum (SL), 3) %!assert_equal (sum (SQ, 'Quality', 1), 70) %!assert_equal (sum (SQ, 'Quality', [0, 1]), 0) %!assert_equal (sum (SN, 'MissingData', 'interpolate'), 106) %!assert_equal (sum (SNU, 'Weighting', 'time'), 260 / 9, 1e-13) %!error sum (SV, 'Bogus', 1) %!assert_equal (min (SV), 1) %!assert_equal (min (ST3), [1, 2; 4, 6]) %!assert_equal (min (SNF), 1) %!assert_equal (min (SI8), int8 (1)) %!assert_equal (min (SL), false) %!assert_equal (min (SQ, 'Quality', 1), 30) %!assert_equal (min (SQ, 'Quality', [0, 1]), NaN) ## Test weighting leaves the extremes, where MATLAB gives 0.5556 and 11.11 %!assert_equal (min (SNU, 'Weighting', 'time'), 1) %!error min (SV, 'Bogus', 1) %!assert_equal (max (SV), 16) %!assert_equal (max (SM), [4, 40]) %!assert_equal (max (SNF), 100) %!assert_equal (max (SI8), int8 (9)) %!assert_equal (max (SL), true) %!assert_equal (max (SNU, 'Weighting', 'time'), 16) %!error max (SV, 'Bogus', 1) %!assert_equal (iqr (SV), 8.25) %!assert_equal (iqr (SM), [2.25, 22.5]) %!assert_equal (iqr (ST3), [4.5, 6; 6, 3.75], 1e-14) %!assert_equal (iqr (SNF), 74.25, 1e-13) %!assert_equal (iqr (timeseries (5, 0)), 0) %!assert_equal (iqr (SI8), 5) %!assert_equal (iqr (SL), 0.5) %!assert_equal (iqr (SQ, 'Quality', 1), 10) %!assert_equal (iqr (SNU, 'Weighting', 'time'), 50 / 9, 1e-13) %!assert_equal (iqr (SV, 'Weighting', 'time'), 8.25) %!error iqr (SV, 'Bogus', 1) %!test %! G = timeseries ([1; 2; 3; 4], (0:3)', [0; 1; 1; 0]); %! G.QualityInfo.Code = [0, 1]; %! G.QualityInfo.Description = {'good', 'bad'}; %! assert_equal (getqualitydesc (G), {'good'; 'bad'; 'bad'; 'good'}); %!test %! G3 = timeseries (reshape (1:8, 2, 2, 2), [0, 1], ... %! reshape ([0 1 1 0 0 0 1 1], 2, 2, 2)); %! G3.QualityInfo.Code = [0, 1]; %! G3.QualityInfo.Description = {'good', 'bad'}; %! d = getqualitydesc (G3); %! assert_equal (size (d), [2, 2, 2]); %! assert_equal (d(:,:,2), {'good', 'bad'; 'good', 'bad'}); %!assert_equal (getqualitydesc (SV), {}) %!error ... %! G = timeseries ([1; 2], [0; 1], [0; 2]); G.QualityInfo.Code = [0, 1]; %! G.QualityInfo.Description = {'good', 'bad'}; getqualitydesc (G); %!error ... %! G = timeseries ([1; 2], [0; 1], [0; 1]); G.QualityInfo.Code = [0, 1]; %! G.QualityInfo.Description = {'good'}; getqualitydesc (G); %!error ... %! getqualitydesc ([SV, SV]) ################################################################################ ## ** Arithmetic and comparison ** ## ################################################################################ ## Available Methods ## ## ## ## 'plus' 'minus' 'times' 'mtimes' ## ## 'rdivide' 'ldivide' 'mrdivide' 'mldivide' ## ## 'eq' 'isequal' 'isequalwithequalnans' ## ## ## ################################################################################ %!shared XA, XB, XC, XA2, XA3, XB3 %! XA = timeseries ([1; 2; 3], [0; 1; 2], [1; 1; 0], 'Name', 'a'); %! XA.DataInfo.Units = 'volts'; %! XA.UserData = 7; %! XA.Events = tsdata.event ('ea', 1); %! XB = timeseries ([10; 20; 30], [0; 1; 2], [0; 0; 1], 'Name', 'b'); %! XB.DataInfo.Units = 'volts'; %! XB.UserData = 9; %! XB.Events = tsdata.event ('eb', 2); %! XB = setinterpmethod (XB, 'zoh'); %! XC = timeseries ([4; 5; 6], [0; 1; 2]); %! XC.DataInfo.Units = 'amps'; %! XA2 = timeseries ([1, 2; 3, 4; 5, 6], [0; 1; 2]); %! XA3 = timeseries (reshape (1:12, 2, 2, 3), [0, 1, 2]); %! XB3 = timeseries (reshape (12:-1:1, 2, 2, 3), [0, 1, 2]); %!test %! ts = XA + XB; %! assert_equal (ts.Data, [11; 22; 33]); %! assert_equal (ts.Name, 'unnamed'); %! assert_equal (ts.DataInfo.Units, 'volts'); %! assert_equal (ts.UserData, 7); %! assert_equal ({ts.Events.Name}, {'ea', 'eb'}); %! assert_equal (ts.Quality, []); %! assert_equal (getinterpmethod (ts), 'linear'); %!test %! ts = XB + XA; %! assert_equal (ts.UserData, 9); %! assert_equal ({ts.Events.Name}, {'eb', 'ea'}); %!test %! ts = XA + XC; %! assert_equal (ts.Data, [5; 7; 9]); %! assert_equal (ts.DataInfo.Units, ''); %!test %! ts = XA + 2; %! assert_equal (ts.Data, [3; 4; 5]); %! assert_equal (ts.Name, 'a'); %! assert_equal (ts.Quality, [1; 1; 0]); %! assert_equal (ts.DataInfo.Units, 'volts'); %!assert_equal ((2 + XA).Data, [3; 4; 5]) %!assert_equal ((XA2 + [10, 20]).Data, [11, 22; 13, 24; 15, 26]) %!assert_equal (([10, 20] + XA2).Data, [11, 22; 13, 24; 15, 26]) %!assert_equal ((XA3 + XB3).Data, 13 * ones (2, 2, 3)) %!assert_equal ((XA3 + [1, 2; 3, 4]).Data(:,:,1), [2, 5; 5, 8]) %!assert_equal ((XA + int8 (2)).Data, [3; 4; 5]) %!assert_equal (class ((timeseries (int8 ([1; 2; 3])) + 2.6).Data), 'double') %!assert_equal ((timeseries (logical ([1; 0; 1])) + ... %! timeseries (logical ([1; 1; 0]))).Data, [2; 1; 1]) ## Test undated times may differ by a constant offset, the left's kept %!test %! w = warning ('off', 'all'); %! ts = XA + timeseries ([4; 5; 6], [5; 6; 7]); %! warning (w); %! assert_equal (ts.Time, [0; 1; 2]); %! assert_equal (ts.Data, [5; 7; 9]); %!warning ... %! XA + timeseries ([4; 5; 6], [0.5; 1.5; 2.5]); %!test %! d1 = timeseries ([1; 2; 3], {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}); %! ts = d1 + d1; %! assert_equal (ts.Data, [2; 4; 6]); %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); ## Test dated series at the same instants, however the times round %!test %! a = timeseries ([1; 2], [1; 4.1]); %! a.TimeInfo.StartDate = '01-Jan-2020 00:00:00'; %! b = timeseries ([1; 2], [0; 3.1]); %! b.TimeInfo.StartDate = '01-Jan-2020 00:00:01'; %! assert_equal ((a + b).Data, [2; 4]); %!assert_equal ((timeseries () + timeseries ()).Length, 0) %!error ... %! XA + timeseries ([4; 5], [0; 1]) %!error ... %! XA + timeseries ([4; 5; 6], [0; 1; 3]) %!error ... %! XA + setfield (XC, 'TimeInfo', setfield (XC.TimeInfo, 'Units', 'minutes')) %!error ... %! x = XC; x.TimeInfo.StartDate = '01-Jan-2024'; XA + x; ## Test dated series a day apart, refused as in MATLAB %!error ... %! timeseries ([1; 2; 3], {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'}) + ... %! timeseries ([4; 5; 6], {'02-Jan-2024', '03-Jan-2024', '04-Jan-2024'}) %!error ... %! XA + [1; 2; 3] %!error ... %! XA2 + [1, 2; 3, 4] %!error ... %! XA + XA2 %!error ... %! XA + {1} %!error [XA, XA] + 1 %!assert_equal ((XA - XB).Data, [-9; -18; -27]) %!assert_equal ((XA - XB).DataInfo.Units, 'volts') %!assert_equal ((2 - XA).Data, [1; 0; -1]) %!assert_equal ((XA3 - XB3).Data(:)', -11:2:11) %!error ... %! XA - timeseries ([4; 5], [0; 1]) %!assert_equal ((XA .* XB).Data, [10; 40; 90]) ## Test units are dropped through a product, where MATLAB keeps them %!assert_equal ((XA .* XB).DataInfo.Units, '') %!assert_equal ((XA .* 2).DataInfo.Units, 'volts') %!assert_equal ((XA2 .* [10, 20]).Data, [10, 40; 30, 80; 50, 120]) %!error ... %! XA .* timeseries ([4; 5], [0; 1]) %!assert_equal ((XA * XB).Data, [10; 40; 90]) %!assert_equal ((XA * XB).DataInfo.Units, '') %!assert_equal ((XA * 2).Data, [2; 4; 6]) %!assert_equal ((2 * XA).Data, [2; 4; 6]) %!assert_equal ((XA2 * [1; 2]).Data, [5; 11; 17]) %!assert_equal ((XA2 * [1, 2; 3, 4]).Data, [7, 10; 15, 22; 23, 34]) %!assert_equal ((XA3 * XB3).Data(:)', [45 68 37 56 89 104 65 76 69 76 29 32]) ## Test a product that changes the sample shape lays the result out as its ## samples are %!test %! ts3 = timeseries (reshape (1:12, 2, 2, 3), [0, 1, 2]); %! r = ([1, 1] * ts3) * [1; 1]; %! assert_equal (r.Data, [10; 26; 42]); %! assert_equal (r.IsTimeFirst, true); %! assert_equal (mean (r), 26); %!test %! r = timeseries ([1, 2; 3, 4; 5, 6], [0, 1, 2]) * [1; 1]; %! assert_equal (r.Data, [3; 7; 11]); %! assert_equal (r.IsTimeFirst, true); ## Test a scalar sample is multiplied only by a number, on either side ## Test codes held per sample follow the samples when their shape changes %!test %! ts = timeseries (reshape (1:8, 4, 2), (0:3)', [0; 1; 0; 1]); %! r = [1; 2; 3] * ts; %! assert_equal (size (r.Quality), [1, 1, 4]); %! assert_equal (squeeze (getsamples (r, [2, 3]).Quality), [1; 0]); %!error ... %! [1; 2] * timeseries ([1; 2; 3], [0; 1; 2]) %!error ... %! [1, 2] * timeseries ([1; 2; 3], [0; 1; 2]) %!error ... %! [1, 2] / timeseries ([1; 2; 3], [0; 1; 2]) %!error ... %! timeseries ([1, 2; 3, 4], [0, 1], [1, 2; 3, 4]) * [1; 1] %!error ... %! XA * [1; 2; 3] %!error ... %! XA2 * XA3 %!assert_equal ((XA ./ XB).Data, [0.1; 0.1; 0.1]) %!assert_equal ((XA ./ XB).DataInfo.Units, '') %!assert_equal ((2 ./ XA).Data, [2; 1; 2 / 3]) %!assert_equal ((XA ./ 2).Data, [0.5; 1; 1.5]) %!assert_equal ((XA .\ XB).Data, [10; 10; 10]) %!assert_equal ((XA .\ 2).Data, [2; 1; 2 / 3]) %!assert_equal ((2 .\ XA).Data, [0.5; 1; 1.5]) %!assert_equal ((XA / XB).Data, [0.1; 0.1; 0.1], 1e-15) %!assert_equal ((XA / 2).Data, [0.5; 1; 1.5]) ## Test a number divided by a series works per sample, where MATLAB gives ## [0 0 0.667] %!assert_equal ((2 / XA).Data, [2; 1; 2 / 3]) %!assert_equal ((XA3 / XB3).Data(:,:,1), [12, -13; 13, -14], 1e-12) %!assert_equal ((XA2 / [1, 2; 3, 4]).Data, [1, 0; 0, 1; -1, 2], 1e-14) %!error ... %! XA / [1; 2; 3] %!error ... %! XA2 / timeseries ([1, 0; 0, 1; 2, 2], [0; 1; 2]) %!assert_equal ((XA \ XB).Data, [10; 10; 10], 1e-14) %!assert_equal ((2 \ XA).Data, [0.5; 1; 1.5]) %!assert_equal ((XA3 \ XB3).Data(:,:,1), [-7.5, -6.5; 6.5, 5.5], 1e-12) %!error ... %! XA \ [1; 2; 3] %!test %! assert_equal (XA == XA, true); %! assert_equal (XA == XB, false); %! assert_equal (XA == 2, false); %! assert_equal (XA == timeseries ([4; 5], [0; 1]), false); ## Test every property is compared but 'UserData' and the data class %!test %! x = XA; %! x.UserData = 1; %! assert_equal (XA == x, true); %! x = XA; %! x.Data = int8 ([1; 2; 3]); %! assert_equal (XA == x, true); %! x = XA; %! x.Name = 'z'; %! assert_equal (XA == x, false); %! x = XA; %! x.DataInfo.Units = 'amps'; %! assert_equal (XA == x, false); %! x = XA; %! x.DataInfo.UserData = 5; %! assert_equal (XA == x, false); %! x = XA; %! x.Quality = [0; 0; 0]; %! assert_equal (XA == x, false); %! x = XA; %! x.TimeInfo.Units = 'minutes'; %! assert_equal (XA == x, false); %! x = XA; %! x.TimeInfo.StartDate = '01-Jan-2024'; %! assert_equal (XA == x, false); %! x = XA; %! x.TimeInfo.Format = 'dd-mmm-yyyy'; %! assert_equal (XA == x, false); %! x = XA; %! x.TreatNaNasMissing = false; %! assert_equal (XA == x, false); %! x = XA; %! x.Events = []; %! assert_equal (XA == x, false); %! assert_equal (XA == setinterpmethod (XA, 'zoh'), false); %! assert_equal (XA == addevent (XA, 'eb', 2), false); %!assert_equal (isequal (XA, XA), true) %!assert_equal (isequal (XA, XB), false) %!assert_equal (isequal (XA, 5), false) %!assert_equal (isequal (timeseries ([1; NaN; 3]), ... %! timeseries ([1; NaN; 3])), false) %!assert_equal (isequalwithequalnans (timeseries ([1; NaN; 3]), ... %! timeseries ([1; NaN; 3])), true) %!assert_equal (isequalwithequalnans (XA, XB), false) ################################################################################ ## ** Filtering and detrending ** ## ################################################################################ ## Available Methods ## ## ## ## 'detrend' 'filter' 'idealfilter' ## ## ## ################################################################################ %!shared t, x1, FA, FN, FZ, FB3, FU, FD %! t = (0:15)'; %! x1 = 5 + sin (2 * pi * t / 8) + 0.5 * cos (2 * pi * t / 4) + 0.25 * t; %! FA = timeseries ([x1, 2 * x1 + 3], t, 'Name', 'sig'); %! FA.DataInfo.Units = 'volts'; %! FA.Quality = mod (t, 3); %! FA = addevent (FA, 'ev', 3); %! FA = setinterpmethod (FA, 'zoh'); %! FA.UserData = 42; %! FN = timeseries ([1; 2; NaN; 4; 5; 6], (0:5)'); %! FZ = setinterpmethod (FN, 'zoh'); %! FB3 = timeseries (reshape ([x1(1:8), -x1(1:8)]', 2, 1, 8), (0:7)'); %! FU = timeseries ([1; 3; 10; 15; 17; 25], [0; 1; 3; 7; 8; 12], (1:6)'); %! FU = addevent (FU, 'ev', 3); %! FD = timeseries ([1; 5; 3; 4], [0; 1; 1; 2]); %!test %! ts = detrend (FA, 'linear'); %! assert_equal (ts.Data(:,1), x1 - polyval (polyfit (t, x1, 1), t), 1e-13); %! assert_equal (ts.Data(:,2), 2 * ts.Data(:,1), 1e-13); %! assert_equal (ts.Name, 'sig'); %! assert_equal (ts.DataInfo.Units, 'volts'); %! assert_equal (ts.Quality, mod (t, 3)); %! assert_equal (ts.Events.Name, 'ev'); %! assert_equal (getinterpmethod (ts), 'zoh'); %! assert_equal (ts.UserData, 42); %!assert_equal (detrend (FA, 'constant').Data, [x1, 2 * x1 + 3] - ... %! mean ([x1, 2 * x1 + 3]), 1e-14) %!assert_equal (detrend (FA, 0).Data, detrend (FA, 'constant').Data) %!assert_equal (detrend (FA, 1).Data, detrend (FA, 'linear').Data) %!assert_equal (detrend (FA, 'Linear').Data, detrend (FA, 'linear').Data) %!assert_equal (detrend (FA, "linear").Data, detrend (FA, 'linear').Data) %!assert_equal (detrend (FA, string ('linear')).Data, ... %! detrend (FA, 'linear').Data) %!assert_equal (detrend (FA, 2).Data(1:3,1)', ... %! [0.079561016560287356, 0.31770392936398473, ... %! 0.14699522254633202], 1e-13) %!test %! ts = detrend (FA, 'linear', 2); %! assert_equal (ts.Data(:,1), x1); %! assert_equal (ts.Data(:,2), detrend (FA, 'linear').Data(:,2)); %!assert_equal (detrend (FA, 'linear', [2, 1]).Data, ... %! detrend (FA, 'linear').Data) %!assert_equal (detrend (FA, 'linear', []).Data, FA.Data) %!assert_equal (detrend (FN, 'constant').Data, ... %! [-2.6; -1.6; NaN; 0.4; 1.4; 2.4], 1e-14) %!assert_equal (detrend (timeseries ([1; 3; 2], (0:2)'), 'linear').Data, ... %! [-0.5; 1; -0.5], 1e-14) %!assert_equal (class (detrend (timeseries (single (x1), t), ... %! 'linear').Data), 'single') %!assert_equal (detrend (timeseries (), 'linear').Data, []) %!assert_equal (detrend (timeseries ([NaN; NaN], [0; 1]), 'linear').Data, ... %! [NaN; NaN]) ## Test the fit against 'Time', where MATLAB fits over sample positions %!assert_equal (detrend (timeseries (2 * [0; 1; 3; 7; 8; 12] + 1, ... %! [0; 1; 3; 7; 8; 12]), 'linear').Data, ... %! zeros (6, 1), 1e-13) %!assert_equal (detrend (FN, 'linear').Data, [0; 0; NaN; 0; 0; 0], 1e-14) %!assert_equal (detrend (FZ, 'linear').Data, [0; 0; NaN; 0; 0; 0], 1e-14) %!assert_equal (detrend (FD, 'linear').Data, [-3; 7; -1; -3] / 4, 1e-14) ## Test short series are detrended, where MATLAB returns them unchanged %!assert_equal (detrend (timeseries (3, 0), 'constant').Data, 0) %!assert_equal (detrend (timeseries ([1; 3], [0; 1]), 'constant').Data, [-1; 1]) %!assert_equal (detrend (timeseries ([1; 3], [0; 1]), 'linear').Data, ... %! [0; 0], 1e-15) %!assert_equal (detrend (timeseries ([1; NaN; 3], (0:2)'), 'linear').Data, ... %! [0; NaN; 0], 1e-15) %!error detrend (FA) %!error ... %! detrend (FA, 'quadratic') %!error ... %! detrend (FA, -1) %!error ... %! detrend (FA, 1.5) %!error ... %! detrend (FA, {1}) %!error ... %! detrend (FA, 'linear', 3) %!error ... %! detrend (FA, 'linear', 0) %!error ... %! detrend (FA, 'linear', 1.5) %!error ... %! detrend (FA, 'linear', true) %!error ... %! detrend (FA, 'linear', {1}) %!error ... %! detrend (FA, 'constant', 'x') %!error ... %! detrend (FB3, 'linear') %!error ... %! detrend (timeseries (int16 ([1; 2; 3]), (0:2)'), 'linear') %!error ... %! detrend (timeseries (logical ([1; 0; 1]), (0:2)'), 'constant') %!error ... %! detrend ([FA, FA], 'linear') %!test %! ts = filter (FA, [1, 1] / 2, 1); %! assert_equal (ts.Data, filter ([1, 1] / 2, 1, [x1, 2 * x1 + 3]), 1e-14); %! assert_equal (ts.Data(1:3,1)', ... %! [2.75, 5.728553390593274, 5.978553390593274], 1e-14); %! assert_equal (ts.Name, 'sig'); %! assert_equal (ts.DataInfo.Units, 'volts'); %! assert_equal (ts.Quality, mod (t, 3)); %! assert_equal (ts.Events.Name, 'ev'); %! assert_equal (getinterpmethod (ts), 'zoh'); %! assert_equal (ts.UserData, 42); %!assert_equal (filter (FA, [2, 2], [2, -1]).Data(1:3,1)', ... %! [5.5, 14.207106781186548, 19.060660171779823], 1e-13) %!assert_equal (filter (FA, [1; 1] / 2, 1).Data, ... %! filter (FA, [1, 1] / 2, 1).Data) %!test %! ts = filter (FA, [1, 1] / 2, 1, 2); %! assert_equal (ts.Data(:,1), x1); %! assert_equal (ts.Data(:,2), filter ([1, 1] / 2, 1, 2 * x1 + 3), 1e-14); %!assert_equal (filter (FA, [1, 1] / 2, 1, [2, 1]).Data, ... %! filter (FA, [1, 1] / 2, 1).Data) %!assert_equal (filter (FN, 1, 1).Data, [1; 2; 3; 4; 5; 6]) %!assert_equal (filter (FZ, 1, 1).Data, [1; 2; 2; 4; 5; 6]) %!assert_equal (filter (FZ, [1, 1] / 2, 1).Data, [0.5; 1.5; 2; 3; 4.5; 5.5]) %!assert_equal (filter (timeseries (3, 0), [1, 1] / 2, [1, -0.5]).Data, 1.5) %!assert_equal (filter (timeseries (), [1, 1] / 2, 1).Data, []) %!assert_equal (class (filter (timeseries (single (x1), t), [1, 1] / 2, ... %! 1).Data), 'single') ## Test 3-D data is filtered along time, where MATLAB filters across rows %!test %! ts = filter (FB3, [1, 1] / 2, 1); %! assert_equal (size (ts.Data), [2, 1, 8]); %! assert_equal (squeeze (ts.Data(1,1,:)), filter ([1, 1] / 2, 1, x1(1:8)), ... %! 1e-14); %! assert_equal (ts.Data(2,1,:), -ts.Data(1,1,:)); %!test %! ts = filter (FB3, [1, 1] / 2, 1, 1); %! assert_equal (squeeze (ts.Data(1,1,:))', [2.75, 5.728553390593274, ... %! 5.978553390593274, 6.228553390593274, 6.478553390593274, ... %! 6.021446609406726, 5.271446609406726, 5.521446609406726], ... %! 1e-14); %! assert_equal (squeeze (ts.Data(2,1,:)), -x1(1:8)); %!error ... %! filter (FA, [1, 1] / 2) %!error ... %! filter (timeseries ([1; 2; 3], [0; 1; 2]), 1, [0, 1]) %!error ... %! ts = setinterpmethod (timeseries ([1; NaN; NaN; 4], 0:3), ... %! @(nt, ot, od) [1; 2; 3]); ... %! filter (ts, 1, 1) %!error ... %! ts = setinterpmethod (timeseries ([1; NaN; 4], 0:2), @(nt, ot, od) 'a'); ... %! filter (ts, 1, 1) %!error ... %! filter (FA, {[1, 1] / 2}, {1}) %!error filter (FA, 'ab', 1) %!error filter (FA, [], 1) %!error ... %! filter (FA, [1, 1] / 2, ones (2)) ## Test irregular times are refused, where MATLAB filters them as uniform %!error ... %! filter (FU, [1, 1] / 2, 1) %!error ... %! filter (FD, [1, 1] / 2, 1) %!error ... %! filter (timeseries ([1; 2; 3; 4; 5; NaN], (0:5)'), 1, 1) %!error ... %! filter (timeseries ([NaN; 2; 3; 4; 5; 7], (0:5)'), 1, 1) %!error ... %! filter (timeseries (int16 ([1; 2; 3]), (0:2)'), [1, 1] / 2, 1) %!error ... %! filter (timeseries (logical ([1; 0; 1]), (0:2)'), [1, 1] / 2, 1) %!error ... %! filter (FA, [1, 1] / 2, 1, 3) %!error ... %! filter (FA, [1, 1] / 2, 1, true) %!error ... %! filter (FB3, [1, 1] / 2, 1, 3) %!error ... %! filter ([FA, FA], [1, 1] / 2, 1) %!test %! ts = idealfilter (FA, [0.2, 0.3], 'pass'); %! p = repmat ([0.25; -0.25; -0.25; 0.25], 4, 1); %! assert_equal (ts.Data, [p, 2 * p], 1e-14); %! assert_equal (ts.Time, t); %! assert_equal (ts.Name, 'sig'); %! assert_equal (ts.DataInfo.Units, 'volts'); %! assert_equal (ts.Quality, mod (t, 3)); %! assert_equal (ts.Events.Name, 'ev'); %! assert_equal (getinterpmethod (ts), 'zoh'); %! assert_equal (ts.UserData, 42); %!test %! ps = idealfilter (FA, [0.2, 0.3], 'pass'); %! nt = idealfilter (FA, [0.2, 0.3], 'notch'); %! assert_equal (ps.Data + nt.Data, detrend (FA, 'constant').Data, 1e-13); %!assert_equal (idealfilter (FA, [0.3, 0.2], 'pass').Data, ... %! idealfilter (FA, [0.2, 0.3], 'pass').Data) %!assert_equal (idealfilter (FA, [0.25, 0.25], 'pass').Data, zeros (16, 2)) %!assert_equal (idealfilter (FA, [0.2, 0.6], 'pass').Data(1:2,1)', ... %! [-0.62499999999999956, 0.13183487303146241], 1e-13) %!assert_equal (idealfilter (FA, [0, 0.1], 'pass').Data(1:3,1)', ... %! [-0.25, -0.71193976625564326, -1.065493156848917], 1e-13) %!assert_equal (idealfilter (FA, [0.1, 0.15; 0.2, 0.3], ... %! 'pass').Data(1:4,1)', ... %! [0, -0.14644660940672583, 0.14644660940672649, ... %! 0.70710678118654746], 1e-13) %!test %! ts = idealfilter (FA, [0.2, 0.3], 'pass', 2); %! assert_equal (ts.Data(:,1), x1); %! assert_equal (ts.Data(:,2), repmat ([0.5; -0.5; -0.5; 0.5], 4, 1), 1e-14); %!test %! th = (0:0.5:7.5)'; %! H = timeseries (cos (2 * pi * 0.25 * th) + 1, th); %! assert_equal (idealfilter (H, [0.2, 0.3], 'pass').Data, ... %! cos (2 * pi * 0.25 * th), 1e-14); %! assert_equal (idealfilter (H, [0.2, 0.3], 'notch').Data, zeros (16, 1), ... %! 1e-14); %! H.TimeInfo.Units = 'minutes'; %! assert_equal (idealfilter (H, [0.2, 0.3], 'pass').Data, ... %! cos (2 * pi * 0.25 * th), 1e-14); %!assert_equal (idealfilter (FN, [0, 0.6], 'pass').Data, ... %! [-2.5; -1.5; -0.5; 0.5; 1.5; 2.5], 1e-14) %!assert_equal (idealfilter (FZ, [0, 0.6], 'pass').Data, ... %! [-7; -4; -4; 2; 5; 8] / 3, 1e-14) %!test %! ts = idealfilter (timeseries (2 * [0; 1; 3; 7; 8; 12] + 1, ... %! [0; 1; 3; 7; 8; 12]), [0.2, 0.3], 'pass'); %! assert_equal (ts.Data, 2.4 * [-1; 1; -1; 1; -1; 1], 1e-14); %! assert_equal (ts.Time, [0; 2.4; 4.8; 7.2; 9.6; 12], 1e-14); %!test %! ts = idealfilter (FU, [0, 0.6], 'pass'); %! assert_equal (ts.Data, [-12.625; -5.725; -1.375; 1.775; 6.575; 11.375], ... %! 1e-13); %! assert_equal (ts.Time, [0; 2.4; 4.8; 7.2; 9.6; 12], 1e-14); %! assert_equal (ts.Quality, [1; 3; 3; 4; 5; 6]); %! assert_equal (ts.Events.Name, 'ev'); ## Test a held value keeps the code it is held from, where MATLAB gives ## [1 3 3 4 5 6] %!test %! ts = idealfilter (setinterpmethod (FU, 'zoh'), [0, 0.6], 'pass'); %! assert_equal (ts.Data, [-65; -53; -11; 19; 31; 79] / 6, 1e-13); %! assert_equal (ts.Quality, [1; 2; 3; 4; 5; 6]); %!test %! ts = idealfilter (FD, [0, 0.6], 'pass'); %! assert_equal (ts.Data, [-7; -1; 7; 1] / 6, 1e-14); %! assert_equal (ts.Time, [0; 2; 4; 6] / 3, 1e-15); %!assert_equal (class (idealfilter (timeseries (single (x1), t), [0.2, 0.3], ... %! 'pass').Data), 'single') %!assert_equal (idealfilter (timeseries (), [0.2, 0.3], 'pass').Data, []) ## Test short series lose their mean, where MATLAB keeps it %!assert_equal (idealfilter (timeseries (3, 0), [0, 0.6], 'notch').Data, 0) %!assert_equal (idealfilter (timeseries ([1; 3], [0; 1]), [0.1, 0.2], ... %! 'notch').Data, [-1; 1]) %!error ... %! idealfilter (FA, [0.2, 0.3]) %!error ... %! idealfilter (FA, 0.25, 'pass') %!error ... %! idealfilter (FA, [0.2; 0.3], 'pass') %!error ... %! idealfilter (FA, [0.2, 0.3, 0.4], 'pass') %!error ... %! idealfilter (FA, 'ab', 'pass') %!error ... %! idealfilter (FA, [NaN, 0.3], 'pass') %!error ... %! idealfilter (FA, zeros (0, 2), 'pass') %!error ... %! idealfilter (FA, [0.2, 0.3], 'Pass') %!error ... %! idealfilter (FA, [0.2, 0.3], 'bandpass') %!error ... %! idealfilter (FA, [0.2, 0.3], 1) %!error ... %! idealfilter (FB3, [0.2, 0.3], 'pass') %!error ... %! idealfilter (timeseries (int16 ([1; 2; 3]), (0:2)'), [0.2, 0.3], 'pass') %!error ... %! idealfilter (timeseries (logical ([1; 0; 1]), (0:2)'), [0.2, 0.3], 'pass') ## Test a leading or trailing NaN is refused, where MATLAB returns complex NaN %!error ... %! idealfilter (timeseries ([1; 2; 3; 4; 5; NaN], (0:5)'), [0, 0.6], 'pass') %!error ... %! idealfilter (timeseries ([NaN; 2; 3; 4; 5; 7], (0:5)'), [0, 0.6], 'pass') %!error ... %! idealfilter (FA, [0.2, 0.3], 'pass', 3) %!error ... %! idealfilter ([FA, FA], [0.2, 0.3], 'pass') ################################################################################ ## ** 'timeseries' properties ** ## ################################################################################ ## Test 'Events' %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.Events = tsdata.event ('a', 1); %! assert_equal (class (ts.Events), 'tsdata.event'); %! assert_equal (ts.Events.Name, 'a'); %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.Events = [tsdata.event('a', 1), tsdata.event('b', 2)]; %! assert_equal (numel (ts.Events), 2); %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.Events = tsdata.event ('a', 1); %! ts.Events = []; %! assert_equal (ts.Events, []); ## Test a column of events is kept as a column, as in MATLAB, and more can be ## added, where MATLAB fails %!test %! ts = timeseries ((1:3)', 1:3); %! ts.Events = [tsdata.event('a', 1); tsdata.event('b', 2)]; %! assert_equal (size (ts.Events), [2, 1]); %! ts = addevent (ts, 'c', 3); %! assert_equal (size (ts.Events), [3, 1]); %! assert_equal ({ts.Events.Name}, {'a', 'b', 'c'}); ## Test clearing the start date keeps the events on their instants %!test %! ts = timeseries ((1:4)', 0:3); %! ts.TimeInfo.StartDate = '01-Jan-2024'; %! ts = addevent (ts, 'ev', '01-Jan-2024 00:00:02'); %! ts.TimeInfo.StartDate = ''; %! assert_equal (ts.Events(1).StartDate, ''); %! assert_equal (gettsatevent (ts, 'ev').Data, 3); %!error ... %! setfield (timeseries ([1; 2]), 'Events', 5) %!error ... %! setfield (timeseries ([1; 2]), 'Events', {tsdata.event('a', 1)}) ## Test 'Name' %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.Name = 'str'; %! assert_equal (ts.Name, 'str'); %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.Name = string ('str'); %! assert_equal (ts.Name, 'str'); %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.Name = ''; %! assert_equal (ts.Name, ''); %!error ... %! setfield (timeseries ([1; 2]), 'Name', 5) %!error ... %! setfield (timeseries ([1; 2]), 'Name', {'a'}) ## Test 'UserData' %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.UserData = {1, 'a'}; %! assert_equal (ts.UserData, {1, 'a'}); ## Test 'Data' %!test %! ts = timeseries ([1; 2; 3; 4], [0; 1; 2; 3]); %! ts.Data = ones (4, 2); %! assert_equal (ts.Data, ones (4, 2)); %! assert_equal (ts.IsTimeFirst, true); %!test %! ts = timeseries ([1; 2; 3; 4], [0; 1; 2; 3]); %! ts.Data = ones (2, 2, 4); %! assert_equal (size (ts.Data), [2, 2, 4]); %! assert_equal (ts.IsTimeFirst, false); %!test %! ts = timeseries ([1; 2; 3; 4], [0; 1; 2; 3]); %! ts.Data = [1, 2, 3, 4]; %! assert_equal (size (ts.Data), [1, 1, 4]); %! assert_equal (ts.IsTimeFirst, false); %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.Data = int16 ([4; 3; 2; 1]); %! assert_equal (ts.Data, int16 ([4; 3; 2; 1])); ## Test an empty series is built by assigning 'Time' and 'Data' in turn %!test %! ts = timeseries (); %! ts.Time = [1; 2; 3]; %! assert_equal (ts.Length, 3); %! assert_equal (ts.Data, []); %! ts.Data = [4; 5; 6]; %! assert_equal (ts.Data, [4; 5; 6]); %!test %! ts = timeseries (); %! ts.Data = [1; 2; 3]; %! assert_equal (ts.Data, [1; 2; 3]); %! assert_equal (ts.Length, 0); %! ts.Time = [0; 1; 2]; %! assert_equal (ts.Length, 3); %!test %! ts = timeseries ('n'); %! ts.Data = 5; %! assert_equal (ts.Data, 5); %! assert_equal (ts.Length, 0); ## Test a series whose time runs last keeps it through 'Data' and 'Time' %!test %! ts = getsamples (timeseries (reshape (1:24, 2, 3, 4)), 2); %! ts.Data = ones (2, 3); %! assert_equal (ts.IsTimeFirst, false); %! assert_equal (ts.Length, 1); %!test %! ts = getsamples (timeseries (reshape (1:24, 2, 3, 4)), 2); %! ts.Time = 7; %! assert_equal (ts.IsTimeFirst, false); %! assert_equal (size (ts.Data), [2, 3]); ## Test one sample of four dimensions keeps its layout when assigned %!test %! ts = getsamples (timeseries (reshape (1:120, 2, 3, 4, 5)), 1); %! ts.Data = ts.Data; %! ts.Time = 7; %! assert_equal (size (ts.Data), [2, 3, 4]); %! assert_equal (ts.IsTimeFirst, false); %!test %! ts = getsamples (timeseries (reshape (1:12, 1, 3, 4), 0:3), 2); %! ts.Time = 5; %! assert_equal (ts.IsTimeFirst, false); ## Test data given as the constructor takes it, codes held per element %!test %! ts = timeseries ([1, 2, 3; 4, 5, 6], (0:2)', ... %! reshape ([0, 1, 0; 1, 0, 1], 2, 1, 3)); %! ts.Data = [7, 8, 9; 10, 11, 12]; %! assert_equal (size (ts.Data), [2, 1, 3]); %!error ... %! setfield (timeseries (ones (3, 2), [0, 1, 2], [1, 2; 3, 4; 5, 6]), ... %! 'Data', ones (3, 4)) %!error ... %! setfield (timeseries ([1; 2; 3; 4]), 'Data', [1; 2; 3]) %!error ... %! setfield (timeseries ([1; 2; 3; 4]), 'Data', ones (4, 3, 2)) %!error ... %! setfield (timeseries ([1; 2; 3; 4]), 'Data', {1; 2; 3; 4}) %!error ... %! setfield (timeseries ([1; 2; 3; 4]), 'Data', 'abcd') ## Test 'DataInfo' %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.DataInfo.Units = 'volts'; %! assert_equal (ts.DataInfo.Units, 'volts'); %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.DataInfo.Interpolation = tsdata.interpolation ('zoh'); %! assert_equal (ts.DataInfo.Interpolation.Name, 'zoh'); %!test %! ts = timeseries ([1; 2; 3; 4]); %! di = tsdata.datametadata (); %! di.Units = 'amps'; %! ts.DataInfo = di; %! assert_equal (ts.DataInfo.Units, 'amps'); ## Test 'InterpretSingleRowDataAs3D' never moves 'IsTimeFirst' %!test %! ts = timeseries (ones (1, 3), 1); %! ts.DataInfo.InterpretSingleRowDataAs3D = true; %! assert_equal (ts.DataInfo.InterpretSingleRowDataAs3D, true); %! assert_equal (ts.IsTimeFirst, true); %! assert_equal (ts.Length, 1); %!error ... %! setfield (timeseries ([1; 2]), 'DataInfo', 5) %!error ... %! setfield (timeseries ([1; 2]), 'DataInfo', tsdata.timemetadata ()) %!error ... %! ts = timeseries ([1; 2]); ts.DataInfo.Units = 5; ## Test 'Time' %!test %! ts = timeseries ([1; 2; 3; 4], [0; 1; 2; 3]); %! ts.Time = [0, 1, 2, 3]; %! assert_equal (ts.Time, [0; 1; 2; 3]); %!test %! ts = timeseries ([1; 2; 3; 4], [0; 1; 2; 3]); %! ts.Time = [0; 1; 1; 2]; %! assert_equal (ts.Time, [0; 1; 1; 2]); %! assert_equal (ts.TimeInfo.End, 2); %! assert_equal (ts.TimeInfo.Increment, NaN); %!test %! ts = timeseries ([1; 2; 3; 4], [0; 1; 2; 3]); %! ts.Time = [10; 11; 12; 13]; %! assert_equal (ts.TimeInfo.Start, 10); %! assert_equal (ts.TimeInfo.End, 13); %! assert_equal (ts.TimeInfo.Increment, 1); %!test %! ts = timeseries ([1; 2; 3; 4], [0; 1; 2; 3]); %! ts.Time = int32 ([0; 2; 4; 6]); %! assert_equal (class (ts.Time), 'double'); %! assert_equal (ts.TimeInfo.Increment, 2); %!test %! ts = timeseries ([1; 2; 3; 4], [0; 1; 3; 7]); %! assert_equal (ts.TimeInfo.Increment, NaN); %! assert_equal (ts.TimeInfo.End, 7); %!error ... %! setfield (timeseries ([1; 2; 3; 4]), 'Time', [3; 2; 1; 0]) %!error ... %! setfield (timeseries ([1; 2; 3; 4]), 'Time', [0; 1; 2]) %!error ... %! setfield (timeseries ([1; 2; 3; 4]), 'Time', [0; 1; NaN; 3]) %!error ... %! setfield (timeseries ([1; 2; 3; 4]), 'Time', datetime (2024, 1, 1:4)) %!error ... %! setfield (timeseries ([1; 2; 3; 4]), 'Time', {1; 2; 3; 4}) %!error ... %! ts = timeseries ('n'); ts.Time = [1; 2]; ts.Quality = [1; 2]; ts.Time = 1; ## Test 'TimeInfo' %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.TimeInfo.Units = 'minutes'; %! assert_equal (ts.TimeInfo.Units, 'minutes'); %! assert_equal (ts.Time, [0; 1; 2; 3]); %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.TimeInfo.StartDate = '01-Jan-2024'; %! assert_equal (ts.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.TimeInfo.Format = 'dd-mmm-yyyy'; %! assert_equal (ts.TimeInfo.Format, 'dd-mmm-yyyy'); ## Test a new 'TimeInfo' takes the series' own time vector %!test %! ts = timeseries ([1; 2; 3; 4]); %! ti = tsdata.timemetadata ([5, 6]); %! ti.Units = 'hours'; %! ts.TimeInfo = ti; %! assert_equal (ts.TimeInfo.Units, 'hours'); %! assert_equal (ts.TimeInfo.Length, 4); %! assert_equal (ts.TimeInfo.End, 3); %!error ... %! setfield (timeseries ([1; 2]), 'TimeInfo', 5) %!error ... %! ts = timeseries ([1; 2]); ts.TimeInfo.Units = 'bogus'; %!error ... %! ts = timeseries ([1; 2]); ts.TimeInfo.Start = 5; %!error ... %! ts = timeseries ([1; 2]); ts.TimeInfo.Increment = 2; %!error ... %! ts = timeseries ([1; 2]); ts.TimeInfo.End = 10; %!error ... %! ts = timeseries ([1; 2]); ts.TimeInfo.Length = 10; %!error ... %! ts = timeseries ([1; 2]); ts.TimeInfo.StartDate = 'bogus'; ## Test 'Quality' %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.Quality = int8 ([1; 2; 3; 4]); %! assert_equal (ts.Quality, [1; 2; 3; 4]); %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.Quality = logical ([1; 0; 1; 0]); %! assert_equal (ts.Quality, [1; 0; 1; 0]); %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.Quality = [1, 2, 3, 4]; %! assert_equal (ts.Quality, [1; 2; 3; 4]); %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.Quality = [1; 2; 3; 4]; %! ts.Quality = []; %! assert_equal (ts.Quality, []); %!error ... %! setfield (timeseries ([1; 2; 3; 4]), 'Quality', [1; 2]) %!error ... %! setfield (timeseries (), 'Quality', [1; 2; 3]) %!error ... %! setfield (timeseries ([1; 2; 3; 4]), 'Quality', 1.5 * [1; 2; 3; 4]) %!error ... %! setfield (timeseries ([1; 2; 3; 4]), 'Quality', {1; 2; 3; 4}) ## Test 'QualityInfo' %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.QualityInfo.Code = [0, 1]; %! ts.QualityInfo.Description = {'good', 'bad'}; %! assert_equal (ts.QualityInfo.Code, [0, 1]); %! assert_equal (ts.QualityInfo.Description, {'good', 'bad'}); %!error ... %! setfield (timeseries ([1; 2]), 'QualityInfo', 5) %!error ... %! ts = timeseries ([1; 2]); ts.QualityInfo.Code = [0, 0]; ## Test 'IsTimeFirst' follows the data %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.IsTimeFirst = true; %! assert_equal (ts.IsTimeFirst, true); %!test %! ts = timeseries (ones (2, 3, 4)); %! ts.IsTimeFirst = false; %! assert_equal (ts.IsTimeFirst, false); %!error ... %! setfield (timeseries (ones (2, 3, 4)), 'IsTimeFirst', true) %!error ... %! setfield (timeseries (ones (3, 3, 3)), 'IsTimeFirst', true) %!error ... %! setfield (timeseries ([1; 2; 3; 4]), 'IsTimeFirst', false) %!error ... %! setfield (timeseries (ones (2, 3), 5), 'IsTimeFirst', true) %!error ... %! setfield (timeseries ([1; 2; 3; 4]), 'IsTimeFirst', 'yes') ## Test 'TreatNaNasMissing' %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.TreatNaNasMissing = 0; %! assert_equal (ts.TreatNaNasMissing, false); %!test %! ts = timeseries ([1; 2; 3; 4]); %! ts.TreatNaNasMissing = false; %! ts.TreatNaNasMissing = true; %! assert_equal (ts.TreatNaNasMissing, true); %!error ... %! setfield (timeseries ([1; 2]), 'TreatNaNasMissing', 'yes') %!error ... %! setfield (timeseries ([1; 2]), 'TreatNaNasMissing', [true, false]) %!error ... %! setfield (timeseries ([1; 2]), 'TreatNaNasMissing', 2) ## Test 'Length' %!test %! assert_equal (timeseries ([1; 2; 3; 4]).Length, 4); %! assert_equal (timeseries (ones (2, 3, 4)).Length, 4); %!error ... %! setfield (timeseries ([1; 2]), 'Length', 5) pr0m1th3as-datatypes-9450b29/inst/tests/timetable.m-tst000066400000000000000000014765351527370255000227760ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## Every expected value below was measured against MATLAB R2024a. Row times ## and time steps are compared as objects rather than as rendered text, so ## that a user who has set a default display format does not see this file ## fail; the few assertions that do read text use duration row times, whose ## format travels with the value. ################################################################################ ## ** Construction ** ## ################################################################################ ## Test the empty constructor %!test %! TT = timetable (); %! assert_equal (class (TT), 'timetable'); %! assert_equal (size (TT), [0, 0]); %! assert_equal (height (TT), 0); %! assert_equal (width (TT), 0); ## Test the empty constructor keys its rows by datetime %!test %! P = timetable ().Properties; %! assert_equal (class (P.RowTimes), 'datetime'); %! assert_equal (isempty (P.RowTimes), true); %! assert_equal (P.DimensionNames, {'Time', 'Variables'}); ## Test 'length' is not defined for a timetable %!error ... %! length (timetable ((1:3)', 'TimeStep', hours (1))); ## Test a subscripted reference counts as one timetable %!test %! TT = timetable ((1:5)', (6:10)', 'TimeStep', hours (1)); %! assert_equal (numel (TT), 10); %! assert_equal (numel (TT, 1, 1), 1); ## Test assigning a time step to an empty timetable keeps the step %!test %! TT = timetable (); %! TT.Properties.TimeStep = hours (1); %! assert_equal (TT.Properties.TimeStep, hours (1)); ## Test construction from a leading row time vector %!test %! tv = datetime (2024, 1, 1) + hours (0:4)'; %! TT = timetable (tv, (1:5)'); %! assert_equal (size (TT), [5, 1]); %! assert_equal (TT.Properties.RowTimes, tv); ## Test construction from 'RowTimes' %!test %! tv = datetime (2024, 1, 1) + hours (0:4)'; %! TT = timetable ((1:5)', 'RowTimes', tv); %! assert_equal (TT.Properties.RowTimes, tv); ## Test construction from 'TimeStep' %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! assert_equal (TT.Properties.RowTimes, hours (0:4)'); ## Test construction from 'TimeStep' with a 'StartTime' %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! assert_equal (TT.Properties.RowTimes, t0 + hours (0:4)'); ## Test construction from 'SampleRate' %!test %! TT = timetable ((1:5)', 'SampleRate', 2); %! assert_equal (TT.Properties.TimeStep, seconds (0.5)); %! assert_equal (TT.Properties.SampleRate, 2); ## Test the row times are stored as a column whatever shape they are given in %!test %! TT = timetable ((1:3)', 'RowTimes', hours (0:2)); %! assert_equal (size (TT.Properties.RowTimes), [3, 1]); ## Test variable names are taken from the input variable names %!test %! tv = hours (0:2)'; %! alpha = [1; 2; 3]; %! beta = [4; 5; 6]; %! TT = timetable (tv, alpha, beta); %! assert_equal (TT.Properties.VariableNames, {'alpha', 'beta'}); ## Test variable names fall back to 'Var%d' for an unnamed input %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1)); %! assert_equal (TT.Properties.VariableNames, {'Var1'}); ## Test 'VariableNames' overrides the input variable names %!test %! TT = timetable ((1:3)', (4:6)', 'TimeStep', hours (1), ... %! 'VariableNames', {'a', 'b'}); %! assert_equal (TT.Properties.VariableNames, {'a', 'b'}); ## Test the row dimension is named after the leading row time argument %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! TT = timetable (tv, (1:6)'); %! assert_equal (TT.Properties.DimensionNames, {'tv', 'Variables'}); ## Test the row dimension defaults to 'Time' when the times are generated %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! assert_equal (TT.Properties.DimensionNames, {'Time', 'Variables'}); ## Test 'DimensionNames' overrides the default %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1), ... %! 'DimensionNames', {'When', 'What'}); %! assert_equal (TT.Properties.DimensionNames, {'When', 'What'}); ## Test preallocation by 'Size' and 'VariableTypes' %!test %! TT = timetable ('Size', [3, 2], 'VariableTypes', {'double', 'string'}, ... %! 'TimeStep', hours (1)); %! assert_equal (size (TT), [3, 2]); %! assert_equal (TT.Properties.VariableTypes, {'double', 'string'}); %! assert_equal (TT.Properties.RowTimes, hours (0:2)'); ## Test preallocation names its variables when none are given %!test %! TT = timetable ('Size', [2, 2], 'VariableTypes', {'double', 'double'}, ... %! 'TimeStep', hours (1)); %! assert_equal (TT.Properties.VariableNames, {'Var1', 'Var2'}); ## Test preallocation takes explicit row times %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! TT = timetable ('Size', [3, 1], 'VariableTypes', {'double'}, ... %! 'RowTimes', tv); %! assert_equal (TT.Properties.RowTimes, tv); ## Test a timetable with no variables still counts its rows %!test %! TT = timetable ('Size', [4, 0], 'VariableTypes', {}, 'TimeStep', hours (1)); %! assert_equal (size (TT), [4, 0]); %! assert_equal (height (TT), 4); ## Test an empty row time vector builds a timetable with no rows %!test %! TT = timetable (zeros (0, 1), 'RowTimes', datetime.empty (0, 1)); %! assert_equal (size (TT), [0, 1]); %! assert_equal (class (TT.Properties.RowTimes), 'datetime'); ################################################################################ ## ** The regularity law ** ## ################################################################################ ## Test an evenly spaced datetime vector implies its step %!test %! tv = datetime (2024, 1, 1) + hours (0:4)'; %! P = timetable ((1:5)', 'RowTimes', tv).Properties; %! assert_equal (P.TimeStep, hours (1)); %! assert_equal (class (P.TimeStep), 'duration'); %! assert_equal (P.SampleRate, 1 / 3600); %! assert_equal (P.StartTime, tv(1)); ## Test a generated duration-keyed timetable starts at a zero of its own unit %!test %! P = timetable ((1:5)', 'TimeStep', hours (1)).Properties; %! assert_equal (P.StartTime, hours (0)); %! assert_equal (class (P.StartTime), 'duration'); %! assert_equal (class (P.RowTimes), 'duration'); ## Test a step given as hours keeps its own units %!test %! P = timetable ((1:5)', 'TimeStep', hours (1)).Properties; %! assert_equal (cellstr (P.TimeStep), {'1 hr'}); ## Test a step read off a datetime vector comes out in clock units %!test %! tv = datetime (2024, 1, 1) + hours (0:4)'; %! P = timetable ((1:5)', 'RowTimes', tv).Properties; %! assert_equal (cellstr (P.TimeStep), {'01:00:00'}); ## Test a step given with a datetime start keeps its own units %!test %! P = timetable ((1:5)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 1, 1)).Properties; %! assert_equal (cellstr (P.TimeStep), {'1 hr'}); ## Test a sample rate implies the reciprocal step %!test %! P = timetable ((1:5)', 'SampleRate', 2).Properties; %! assert_equal (cellstr (P.TimeStep), {'0.5 sec'}); %! assert_equal (P.SampleRate, 2); ## Test duration row times imply a duration step %!test %! P = timetable ((1:5)', 'RowTimes', seconds (0:4)').Properties; %! assert_equal (P.TimeStep, seconds (1)); %! assert_equal (P.SampleRate, 1); %! assert_equal (P.StartTime, seconds (0)); %! assert_equal (isregular (timetable ((1:5)', 'RowTimes', seconds (0:4)')), ... %! true); ## Test a calendar step is kept as a calendarDuration %!test %! P = timetable ((1:5)', 'TimeStep', calmonths (1), ... %! 'StartTime', datetime (2024, 1, 1)).Properties; %! assert_equal (class (P.TimeStep), 'calendarDuration'); %! assert_equal (P.TimeStep, calmonths (1)); %! assert_equal (P.SampleRate, NaN); ## Test a calendar step generates calendar row times %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:5)', 'TimeStep', calmonths (1), 'StartTime', t0); %! P = TT.Properties; %! assert_equal (P.RowTimes, t0 + calmonths (0:4)'); ## Test a calendar step is inferred from calendar-regular row times %!test %! t0 = datetime (2024, 1, 1); %! P = timetable ((1:5)', 'RowTimes', t0 + calmonths (0:4)').Properties; %! assert_equal (class (P.TimeStep), 'calendarDuration'); %! assert_equal (P.TimeStep, calmonths (1)); %! assert_equal (P.SampleRate, NaN); ## Test a calendar step that cannot reproduce its own times is not kept %!test %! t0 = datetime (2024, 1, 31); %! TT = timetable ((1:5)', 'TimeStep', calmonths (1), 'StartTime', t0); %! P = TT.Properties; %! assert_equal (class (P.TimeStep), 'duration'); %! assert_equal (ismissing (P.TimeStep), true); %! assert_equal (P.RowTimes, t0 + calmonths (0:4)'); ## Test row times clamped by a short month are irregular in every sense %!test %! t0 = datetime (2024, 1, 31); %! TT = timetable ((1:5)', 'RowTimes', t0 + calmonths (0:4)'); %! assert_equal (ismissing (TT.Properties.TimeStep), true); %! assert_equal (TT.Properties.SampleRate, NaN); %! assert_equal (isregular (TT), false); %! assert_equal (isregular (TT, 'months'), false); %! assert_equal (isregular (TT, 'time'), false); ## Test unevenly spaced row times imply no step %!test %! tv = datetime (2024, 1, 1) + hours ([0 1 3 6 10])'; %! P = timetable ((1:5)', 'RowTimes', tv).Properties; %! assert_equal (class (P.TimeStep), 'duration'); %! assert_equal (ismissing (P.TimeStep), true); %! assert_equal (isempty (P.TimeStep), false); %! assert_equal (P.SampleRate, NaN); %! assert_equal (P.StartTime, tv(1)); ## Test a descending sequence is as regular as an ascending one %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! assert_equal (TT(end:-1:1,:).Properties.TimeStep, hours (-1)); %! assert_equal (isregular (TT(end:-1:1,:)), true); ## Test duplicate row times make a timetable irregular %!test %! tv = datetime (2024, 1, 1) + hours ([0 0 1])'; %! TT = timetable ((1:3)', 'RowTimes', tv); %! assert_equal (class (TT), 'timetable'); %! assert_equal (ismissing (TT.Properties.TimeStep), true); %! assert_equal (isregular (TT), false); ## Test unsorted row times are accepted and make a timetable irregular %!test %! TT = timetable ((1:3)', 'RowTimes', hours ([2 0 1])'); %! assert_equal (class (TT), 'timetable'); %! assert_equal (ismissing (TT.Properties.TimeStep), true); %! assert_equal (isregular (TT), false); ## Test a missing row time makes a timetable irregular %!test %! tv = [datetime(2024,1,1); NaT; datetime(2024,1,3)]; %! TT = timetable ((1:3)', 'RowTimes', tv); %! assert_equal (class (TT), 'timetable'); %! assert_equal (ismissing (TT.Properties.TimeStep), true); %! assert_equal (isregular (TT), false); ## Test a freshly built one-row timetable has no step %!test %! TT = timetable (1, 'RowTimes', datetime (2024, 1, 1)); %! assert_equal (ismissing (TT.Properties.TimeStep), true); %! assert_equal (isregular (TT), false); ## Test a freshly built one-row duration-keyed timetable has no step %!test %! TT = timetable (1, 'RowTimes', hours (0)); %! assert_equal (ismissing (TT.Properties.TimeStep), true); ## Test a freshly built zero-row timetable has no step %!test %! TT = timetable (zeros (0, 1), 'RowTimes', datetime.empty (0, 1)); %! assert_equal (ismissing (TT.Properties.TimeStep), true); ################################################################################ ## ** The step carried through a subset ** ## ################################################################################ ## Test a truncated subset keeps the step it implies %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! assert_equal (TT(1:4,:).Properties.TimeStep, hours (1)); %! assert_equal (TT(1:4,:).Properties.SampleRate, 1 / 3600); ## Test a subset with a gap in it loses the step %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! assert_equal (ismissing (TT([1 2 4 5],:).Properties.TimeStep), true); %! assert_equal (isregular (TT([1 2 4 5],:)), false); ## Test a two-row subset still implies its step %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! assert_equal (TT(1:2,:).Properties.TimeStep, hours (1)); ## Test a one-row subset keeps the step it came from %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! assert_equal (TT(1,:).Properties.TimeStep, hours (1)); %! assert_equal (isregular (TT(1,:)), true); ## Test a zero-row subset keeps the step and the start it came from %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! assert_equal (TT([],:).Properties.TimeStep, hours (1)); %! assert_equal (isregular (TT([],:)), true); %! assert_equal (TT([],:).Properties.StartTime, hours (0)); ## Test dropping the first row moves the start time to the new first row %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! assert_equal (TT(2:end,:).Properties.StartTime, t0 + hours (1)); ## Test a calendar-stepped timetable loses its step when a row is dropped %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:5)', 'TimeStep', calmonths (1), 'StartTime', t0); %! assert_equal (ismissing (TT([1 2 4 5],:).Properties.TimeStep), true); ## Test a timetable with all its variables removed keeps its rows %!test %! TT = timetable ('Size', [5, 0], 'VariableTypes', {}, 'TimeStep', hours (1)); %! assert_equal (class (TT), 'timetable'); %! assert_equal (size (TT), [5, 0]); %! assert_equal (TT.Properties.TimeStep, hours (1)); ## Test a declared step outlives a subset too short to imply one %!test %! t0 = datetime (2024, 1, 1); %! D = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! assert_equal (D(1,:).Properties.TimeStep, hours (1)); %! assert_equal (D([],:).Properties.TimeStep, hours (1)); %! assert_equal (isregular (D(1,:)), true); %! assert_equal (isregular (D([],:)), true); ## Test an inferred step does not, having nothing to fall back on %!test %! t0 = datetime (2024, 1, 1); %! I = timetable ((1:5)', 'RowTimes', t0 + hours (0:4)'); %! assert_equal (isnan (I(1,:).Properties.TimeStep), true); %! assert_equal (isnan (I([],:).Properties.TimeStep), true); %! assert_equal (isregular (I(1,:)), false); %! assert_equal (isregular (I([],:)), false); ## Test 'SampleRate' declares the step just as 'TimeStep' does %!test %! t0 = datetime (2024, 1, 1); %! R = timetable ((1:5)', 'SampleRate', 2, 'StartTime', t0); %! assert_equal (R(1,:).Properties.TimeStep, seconds (0.5)); %! assert_equal (R(1,:).Properties.SampleRate, 2); ## Test two rows or more imply their own step whatever it was declared %!test %! t0 = datetime (2024, 1, 1); %! D = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! assert_equal (D([1 4],:).Properties.TimeStep, hours (3)); %! assert_equal (D([1 3 5],:).Properties.TimeStep, hours (2)); ## Test an inferred calendar step resets its class along with its value %!test %! t0 = datetime (2024, 1, 1); %! K = timetable ((1:5)', 'RowTimes', t0 + calmonths (0:4)'); %! assert_equal (class (K.Properties.TimeStep), 'calendarDuration'); %! assert_equal (class (K(1,:).Properties.TimeStep), 'duration'); %! assert_equal (isnan (K(1,:).Properties.TimeStep), true); ## Test a declared calendar step survives the same subset %!test %! t0 = datetime (2024, 1, 1); %! C = timetable ((1:5)', 'TimeStep', calmonths (1), 'StartTime', t0); %! assert_equal (class (C(1,:).Properties.TimeStep), 'calendarDuration'); %! assert_equal (calmonths (C(1,:).Properties.TimeStep), 1); ## Test the start time goes missing when an inferred timetable is emptied %!test %! t0 = datetime (2024, 1, 1); %! I = timetable ((1:5)', 'RowTimes', t0 + hours (0:4)'); %! assert_equal (ismissing (I([],:).Properties.StartTime), true); %! D = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! assert_equal (D([],:).Properties.StartTime, t0); ## Test the rule holds through the methods that shorten a timetable %!test %! t0 = datetime (2024, 1, 1); %! I = timetable ((1:5)', 'RowTimes', t0 + hours (0:4)'); %! assert_equal (isnan (head (I, 1).Properties.TimeStep), true); %! assert_equal (isnan (tail (I, 1).Properties.TimeStep), true); %! assert_equal (isnan (unique (I(1,:)).Properties.TimeStep), true); %! assert_equal (isnan (sortrows (I(1,:)).Properties.TimeStep), true); %! D = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! assert_equal (head (D, 1).Properties.TimeStep, hours (1)); %! assert_equal (tail (D, 1).Properties.TimeStep, hours (1)); ## Test assigning row times to a declared timetable makes its step inferred %!test %! t0 = datetime (2024, 1, 1); %! D = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! D.Properties.RowTimes = t0 + hours (0:4)'; %! assert_equal (isnan (D(1,:).Properties.TimeStep), true); ## Test a declared step names the unit a recomputed step reads in %!test %! t0 = datetime (2024, 1, 1); %! D = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! I = timetable ((1:5)', 'RowTimes', t0 + hours (0:4)'); %! assert_equal (D([1 3],:).Properties.TimeStep.Format, 'h'); %! assert_equal (I([1 3],:).Properties.TimeStep.Format, 'hh:mm:ss'); %! assert_equal (D([1 3],:).Properties.TimeStep, hours (2)); ## Test assigning a step to an inferred timetable declares it %!test %! t0 = datetime (2024, 1, 1); %! I = timetable ((1:5)', 'RowTimes', t0 + hours (0:4)'); %! I.Properties.TimeStep = hours (2); %! assert_equal (I(1,:).Properties.TimeStep, hours (2)); ################################################################################ ## ** Property assignment ** ## ################################################################################ ## Test assigning the same row times leaves the step alone %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! TT.Properties.RowTimes = TT.Properties.RowTimes; %! assert_equal (TT.Properties.TimeStep, hours (1)); ## Test assigning irregular row times drops the step %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! TT.Properties.RowTimes = hours ([0 1 3 6 10])'; %! assert_equal (ismissing (TT.Properties.TimeStep), true); ## Test assigning row times recomputes the step %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! TT.Properties.RowTimes = hours (0:2:8)'; %! assert_equal (TT.Properties.TimeStep, hours (2)); ## Test assigning a step to an irregular timetable regenerates its row times %!test %! tv = datetime (2024, 1, 1) + hours ([0 1 3 6 10])'; %! TT = timetable ((1:5)', 'RowTimes', tv); %! TT.Properties.TimeStep = hours (1); %! assert_equal (TT.Properties.RowTimes, tv(1) + hours (0:4)'); %! assert_equal (TT.Properties.TimeStep, hours (1)); ## Test assigning a calendar step regenerates calendar row times %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! TT.Properties.TimeStep = calmonths (1); %! assert_equal (TT.Properties.RowTimes, t0 + calmonths (0:4)'); ## Test assigning a sample rate regenerates the row times and the step %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! TT.Properties.SampleRate = 4; %! assert_equal (TT.Properties.TimeStep, seconds (0.25)); %! assert_equal (TT.Properties.RowTimes, hours (0) + seconds ((0:4)' / 4)); ## Test assigning a start time shifts the row times and keeps the step %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! TT.Properties.StartTime = datetime (2030, 1, 1); %! assert_equal (TT.Properties.RowTimes, datetime (2030, 1, 1) + hours (0:4)'); %! assert_equal (TT.Properties.TimeStep, hours (1)); ## Test assigning a start time leaves an irregular timetable irregular %!test %! TT = timetable ((1:5)', 'RowTimes', hours ([0 1 3 6 10])'); %! TT.Properties.StartTime = hours (10); %! assert_equal (TT.Properties.RowTimes, hours ([10 11 13 16 20])'); %! assert_equal (ismissing (TT.Properties.TimeStep), true); ## Test the shared metadata properties are still assignable %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1)); %! TT.Properties.Description = 'a description'; %! TT.Properties.VariableNames = {'v'}; %! TT.Properties.VariableUnits = {'m'}; %! assert_equal (TT.Properties.Description, 'a description'); %! assert_equal (TT.Properties.VariableNames, {'v'}); %! assert_equal (TT.Properties.VariableUnits, {'m'}); ################################################################################ ## ** The properties object ** ## ################################################################################ ## Test 'Properties' returns a TimetableProperties %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1)); %! assert_equal (class (TT.Properties), ... %! 'datatypes.tabular.TimetableProperties'); ## Test the properties object is a TabularProperties %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1)); %! assert_equal (isa (TT.Properties, ... %! 'datatypes.tabular.TabularProperties'), true); ## Test a timetable has no 'RowNames' and a table's properties object differs %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1)); %! assert_equal (any (strcmp (properties (TT.Properties), 'RowNames')), false); %! assert_equal (isa (TT.Properties, ... %! 'datatypes.tabular.TableProperties'), false); ## Test the properties are enumerated in the order they are displayed in %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1)); %! assert_equal (properties (TT.Properties), ... %! {'Description'; 'UserData'; 'DimensionNames'; ... %! 'VariableNames'; 'VariableTypes'; 'VariableDescriptions'; ... %! 'VariableUnits'; 'VariableContinuity'; 'RowTimes'; ... %! 'StartTime'; 'SampleRate'; 'TimeStep'; 'Events'; ... %! 'CustomProperties'}); ## Test 'fieldnames' answers in the same order as 'properties' %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1)); %! assert_equal (fieldnames (TT.Properties), properties (TT.Properties)); ## Test the custom property store starts out empty %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1)); %! assert_equal (class (TT.Properties.CustomProperties), ... %! 'datatypes.tabular.CustomProperties'); %! assert_equal (isempty (TT.Properties.CustomProperties), true); ################################################################################ ## ** Size and shape ** ## ################################################################################ ## Test the size family %!test %! TT = timetable ((1:6)', (7:12)', 'TimeStep', hours (1)); %! assert_equal (size (TT), [6, 2]); %! assert_equal (size (TT, 1), 6); %! assert_equal (size (TT, 2), 2); %! assert_equal (size (TT, 3), 1); %! assert_equal (height (TT), 6); %! assert_equal (width (TT), 2); %! assert_equal (numel (TT), 12); %! assert_equal (ndims (TT), 2); %! assert_equal (isempty (TT), false); ## Test 'size' with two outputs %!test %! TT = timetable ((1:6)', (7:12)', 'TimeStep', hours (1)); %! [r, c] = size (TT); %! assert_equal ([r, c], [6, 2]); ## Test 'size' pads dimensions above the second with ones %!test %! TT = timetable ((1:6)', 'TimeStep', hours (1)); %! [r, c, p] = size (TT); %! assert_equal ([r, c, p], [6, 1, 1]); ## Test a timetable with no rows is empty %!test %! TT = timetable (zeros (0, 1), 'RowTimes', datetime.empty (0, 1)); %! assert_equal (isempty (TT), true); ## Test a timetable with no variables is empty although it has rows %!test %! TT = timetable ('Size', [5, 0], 'VariableTypes', {}, 'TimeStep', hours (1)); %! assert_equal (isempty (TT), true); %! assert_equal (height (TT), 5); ## Test 'repelem' repeats each row in place, its row time with it %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3], [4; 5; 6], 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'A', 'B'}); %! R = repelem (TT, 2, 1); %! assert_equal (class (R), 'timetable'); %! assert_equal (size (R), [6, 2]); %! assert_equal (R.A, [1; 1; 2; 2; 3; 3]); %! assert_equal (R.Properties.RowTimes, repelem (t0 + hours (0:2)', 2, 1)); ## Test 'repmat' repeats the whole timetable as a block %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:2)'; %! TT = timetable ([1; 2; 3], 'RowTimes', rt, 'VariableNames', {'A'}); %! R = repmat (TT, 2, 1); %! assert_equal (R.A, [1; 2; 3; 1; 2; 3]); %! assert_equal (R.Properties.RowTimes, [rt; rt]); ## Test repeating the variables numbers the repeats %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:1)'; %! TT = timetable ([1; 2], [3; 4], 'RowTimes', rt, ... %! 'VariableNames', {'A', 'B'}); %! assert_equal (repelem (TT, 1, 2).Properties.VariableNames, ... %! {'A', 'A_1', 'B', 'B_1'}); %! assert_equal (repmat (TT, 1, 2).Properties.VariableNames, ... %! {'A', 'B', 'A_1', 'B_1'}); ## Test a repeated timetable is no longer regular %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3], 'TimeStep', hours (1), 'StartTime', t0); %! assert_equal (isregular (TT), true); %! assert_equal (isregular (repelem (TT, 2, 1)), false); %! assert_equal (ismissing (repelem (TT, 2, 1).Properties.TimeStep), true); ## Test a single argument repeats both dimensions of 'repmat' alone %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A'}); %! assert_equal (size (repmat (TT, 2)), [4, 2]); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! repelem (timetable ([1; 2], 'RowTimes', rt), 2); ## Test the argument validation is the timetable's own %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! repelem (timetable ([1; 2], 'RowTimes', rt), -1, 1); ## Test a count vector repeats each row by its own count, row times with it ## (MATLAB R2024a) %!test %! TT = timetable ([1; 2; 3], 'RowTimes', hours ([1; 2; 3]), ... %! 'VariableNames', {'X'}); %! R = repelem (TT, [1; 2; 0], 1); %! assert_equal (hours (R.Properties.RowTimes), [1; 2; 2]); %! assert_equal (R.X, [1; 2; 2]); %!test %! TT = timetable ([1; 2; 3], 'RowTimes', hours ([1; 2; 3]), ... %! 'VariableNames', {'X'}); %! R = repelem (TT, 1, 3); %! assert_equal (R.Properties.VariableNames, {'X', 'X_1', 'X_2'}); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! repmat (timetable ([1; 2], 'RowTimes', rt), 2, 2, 2); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! repmat (timetable ([1; 2], 'RowTimes', rt), 1.5); ## Test a size vector names both counts and a zero count empties a dimension %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], [3; 4], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'B'}); %! assert_equal (size (repmat (TT, [2 3])), [4, 6]); %! assert_equal (size (repelem (TT, 0, 1)), [0, 2]); %! assert_equal (size (repelem (TT, 1, 0)), [2, 0]); ################################################################################ ## ** Regularity and type ** ## ################################################################################ ## Test 'isregular' returns a logical %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! assert_equal (class (isregular (TT)), 'logical'); ## Test 'isregular' and its 'time' unit agree %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! assert_equal (isregular (TT), true); %! assert_equal (isregular (TT, 'time'), true); ## Test a calendar-stepped timetable is regular on the calendar only %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:5)', 'TimeStep', calmonths (1), 'StartTime', t0); %! assert_equal (isregular (TT), false); %! assert_equal (isregular (TT, 'time'), false); %! assert_equal (isregular (TT, 'months'), true); ## Test an hourly timetable is regular in time and in no calendar unit %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! assert_equal (isregular (TT, 'time'), true); %! assert_equal (isregular (TT, 'days'), false); %! assert_equal (isregular (TT, 'weeks'), false); %! assert_equal (isregular (TT, 'months'), false); %! assert_equal (isregular (TT, 'quarters'), false); %! assert_equal (isregular (TT, 'years'), false); ## Test a daily timetable is regular in days and not in weeks %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:5)', 'TimeStep', caldays (1), 'StartTime', t0); %! assert_equal (TT.Properties.TimeStep, caldays (1)); %! assert_equal (isregular (TT), true); %! assert_equal (isregular (TT, 'days'), true); %! assert_equal (isregular (TT, 'weeks'), false); %! assert_equal (isregular (TT, 'months'), false); ## Test a weekly timetable is regular in days and in weeks %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:5)', 'TimeStep', calweeks (1), 'StartTime', t0); %! assert_equal (isregular (TT, 'days'), true); %! assert_equal (isregular (TT, 'weeks'), true); %! assert_equal (isregular (TT, 'months'), false); ## Test a whole number of days is inferred as a calendar step %!test %! t0 = datetime (2024, 1, 1); %! P = timetable ((1:5)', 'RowTimes', t0 + hours (0:24:96)').Properties; %! assert_equal (class (P.TimeStep), 'calendarDuration'); %! assert_equal (P.TimeStep, caldays (1)); %! assert_equal (P.SampleRate, NaN); ## Test a step given as a duration stays one, whole days or not %!test %! t0 = datetime (2024, 1, 1); %! P = timetable ((1:5)', 'TimeStep', hours (24), 'StartTime', t0).Properties; %! assert_equal (class (P.TimeStep), 'duration'); %! assert_equal (P.TimeStep, hours (24)); ## Test a spacing that is not whole days stays a duration %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + (0:4)' * (caldays (1) + hours (2)); %! P = timetable ((1:5)', 'RowTimes', rt).Properties; %! assert_equal (class (P.TimeStep), 'duration'); %! assert_equal (P.TimeStep, hours (26)); ## Test a calendar spacing of more than one unit is no calendar step %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + calmonths (0:4)' + caldays (0:4)'; %! P = timetable ((1:5)', 'RowTimes', rt).Properties; %! assert_equal (class (P.TimeStep), 'duration'); %! assert_equal (ismissing (P.TimeStep), true); ## Test a monthly timetable is regular in months and not in quarters %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:5)', 'TimeStep', calmonths (1), 'StartTime', t0); %! assert_equal (isregular (TT, 'quarters'), false); %! assert_equal (isregular (TT, 'years'), false); %! assert_equal (isregular (TT, 'days'), false); %! assert_equal (isregular (TT, 'weeks'), false); ## Test a quarterly timetable is regular in quarters and in months %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:5)', 'RowTimes', t0 + calmonths (0:3:12)'); %! assert_equal (isregular (TT, 'months'), true); %! assert_equal (isregular (TT, 'quarters'), true); %! assert_equal (isregular (TT, 'years'), false); ## Test days across a daylight saving change are regular in days only %!test %! z0 = datetime (2024, 3, 9, 12, 0, 0, 'TimeZone', 'America/New_York'); %! TT = timetable ((1:4)', 'RowTimes', z0 + caldays (0:3)'); %! assert_equal (ismissing (TT.Properties.TimeStep), true); %! assert_equal (isregular (TT), false); %! assert_equal (isregular (TT, 'days'), true); ## Test absolute days across the same change stay regular in absolute time %!test %! z0 = datetime (2024, 3, 9, 12, 0, 0, 'TimeZone', 'America/New_York'); %! TT = timetable ((1:4)', 'RowTimes', z0 + days (0:3)'); %! assert_equal (class (TT.Properties.TimeStep), 'duration'); %! assert_equal (TT.Properties.TimeStep, hours (24)); %! assert_equal (isregular (TT), true); %! assert_equal (isregular (TT, 'days'), false); ## Test 'isregular' on a one-row subset falls back on the step it remembers %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! assert_equal (isregular (TT(1,:), 'time'), true); %! assert_equal (isregular (TT(1,:), 'days'), false); ## Test 'isregular' accepts a string scalar unit %!test %! TT = timetable ((1:5)', 'TimeStep', caldays (1), ... %! 'StartTime', datetime (2024, 1, 1)); %! assert_equal (isregular (TT, "days"), true); ## Test the type predicates %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1)); %! assert_equal (istimetable (TT), true); %! assert_equal (istabular (TT), true); %! assert_equal (istable (TT), false); %! assert_equal (isa (TT, 'table'), false); %! assert_equal (isa (TT, 'tabular'), true); ################################################################################ ## ** Comparison ** ## ################################################################################ ## Test a declared step and an inferred one compare equal %!test %! t0 = datetime (2024, 1, 1); %! D = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! I = timetable ((1:5)', 'RowTimes', t0 + hours (0:4)'); %! assert_equal (isequal (D, I), true); %! assert_equal (isequaln (D, I), true); ## Test they stay equal where their reported step differs %!test %! t0 = datetime (2024, 1, 1); %! D = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! I = timetable ((1:5)', 'RowTimes', t0 + hours (0:4)'); %! ds = D(1,:).Properties.TimeStep; %! is = I(1,:).Properties.TimeStep; %! assert_equal (ismissing (is), true); %! assert_equal (ds, hours (1)); %! assert_equal (isequal (D(1,:), I(1,:)), true); ## Test a declared rate and a declared step compare equal %!test %! t0 = datetime (2024, 1, 1); %! R = timetable ((1:5)', 'SampleRate', 1 / 3600, 'StartTime', t0); %! D = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! assert_equal (isequal (R, D), true); ## Test sorting an already sorted timetable leaves it equal %!test %! t0 = datetime (2024, 1, 1); %! D = timetable ((1:5)', 'TimeStep', hours (1), 'StartTime', t0); %! I = timetable ((1:5)', 'RowTimes', t0 + hours (0:4)'); %! assert_equal (isequal (sortrows (D), I), true); ## Test the row times themselves are compared %!test %! t0 = datetime (2024, 1, 1); %! A = timetable ((1:3)', 'RowTimes', t0 + hours (0:2)'); %! B = timetable ((1:3)', 'RowTimes', t0 + hours (1:3)'); %! assert_equal (isequal (A, B), false); %! assert_equal (isequal (A, A([3 2 1],:)), false); ## Test a time zone is part of the row times %!test %! A = timetable ((1:3)', 'RowTimes', datetime (2024, 1, 1) + hours (0:2)'); %! z0 = datetime (2024, 1, 1, 'TimeZone', 'UTC'); %! B = timetable ((1:3)', 'RowTimes', z0 + hours (0:2)'); %! assert_equal (isequal (A, B), false); ## Test the display format of the row times is not %!test %! rt = datetime (2024, 1, 1) + hours (0:2)'; %! A = timetable ((1:3)', 'RowTimes', rt); %! rt.Format = 'yyyy-MM-dd HH:mm'; %! B = timetable ((1:3)', 'RowTimes', rt); %! assert_equal (isequal (A, B), true); ## Test 'VariableContinuity' is compared %!test %! t0 = datetime (2024, 1, 1); %! A = timetable ((1:3)', 'RowTimes', t0 + hours (0:2)'); %! B = A; %! B.Properties.VariableContinuity = {'continuous'}; %! assert_equal (isequal (A, B), false); ## Test a missing row time is never equal to itself, and 'isequaln' says it is %!test %! rt = [datetime(2024,1,1); NaT; datetime(2024,1,1)+hours(2)]; %! A = timetable ((1:3)', 'RowTimes', rt); %! assert_equal (isequal (A, A), false); %! assert_equal (isequaln (A, A), true); ## Test variables are compared by value, not by class %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:1)'; %! A = timetable (int8 ([1; 2]), 'RowTimes', rt); %! B = timetable ([1; 2], 'RowTimes', rt); %! assert_equal (isequal (A, B), true); ## Test anything that is not a timetable is unequal rather than an error %!test %! t0 = datetime (2024, 1, 1); %! A = timetable ((1:2)', 'RowTimes', t0 + hours (0:1)'); %! assert_equal (isequal (A, table ((1:2)')), false); %! assert_equal (isequal (A, 5), false); %! assert_equal (isequal (A, []), false); %! assert_equal (isequal (A, A, A), true); ## Test a comparison needs something to compare against %!error ... %! isequal (timetable ((1:2)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)')); %!error ... %! isequaln (timetable ((1:2)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)')); ################################################################################ ## ** Row subscripts ** ## ################################################################################ ## Test a datetime subscript selects the rows it names %!test %! tv = datetime (2024, 1, 1) + hours (0:4)'; %! TT = timetable ((1:5)', 'RowTimes', tv); %! assert_equal (class (TT(tv(2:3),:)), 'timetable'); %! assert_equal (TT(tv(2:3),:).Properties.RowTimes, tv(2:3)); ## Test a duration subscript selects the rows it names %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! assert_equal (TT(hours (3),:).Properties.RowTimes, hours (3)); ## Test a repeated row time is matched by every row carrying it %!test %! tv = hours ([0 0 1])'; %! TT = timetable ((1:3)', 'RowTimes', tv); %! assert_equal (height (TT(hours (0),:)), 2); ## Test the row dimension name reaches the row times %!test %! tv = datetime (2024, 1, 1) + hours (0:4)'; %! TT = timetable (tv, (1:5)'); %! assert_equal (TT.tv, tv); ## Test a negative sample rate steps backwards and stays regular %!test %! P = timetable ((1:5)', 'SampleRate', -1).Properties; %! assert_equal (P.TimeStep, seconds (-1)); %! assert_equal (P.SampleRate, -1); %! assert_equal (P.RowTimes, seconds (0:-1:-4)'); %! assert_equal (isregular (timetable ((1:5)', 'SampleRate', -1)), true); ## Test a negative time step steps backwards %!test %! P = timetable ((1:5)', 'TimeStep', hours (-1)).Properties; %! assert_equal (P.RowTimes, hours (0:-1:-4)'); %! assert_equal (P.SampleRate, -1 / 3600); ## Test a zero sample rate carries its arithmetic into the row times %!test %! P = timetable ((1:5)', 'SampleRate', 0).Properties; %! assert_equal (P.StartTime, seconds (0)); %! assert_equal (isinf (seconds (P.RowTimes(2:end))), true (4, 1)); %! assert_equal (ismissing (P.TimeStep), true); %! assert_equal (P.SampleRate, NaN); ## Test an infinite sample rate puts every row at the same time %!test %! P = timetable ((1:5)', 'SampleRate', Inf).Properties; %! assert_equal (P.RowTimes, seconds (zeros (5, 1))); %! assert_equal (ismissing (P.TimeStep), true); ## Test assigning a datetime start time re-anchors elapsed row times %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1)); %! TT.Properties.StartTime = datetime (2024, 1, 1); %! assert_equal (class (TT.Properties.RowTimes), 'datetime'); %! assert_equal (TT.Properties.RowTimes, datetime (2024, 1, 1) + hours (0:4)'); %! assert_equal (TT.Properties.TimeStep, hours (1)); %! assert_equal (isregular (TT), true); ## Test assigning a duration start time reduces dated row times %!test %! TT = timetable ((1:5)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 1, 1)); %! TT.Properties.StartTime = hours (10); %! assert_equal (class (TT.Properties.RowTimes), 'duration'); %! assert_equal (TT.Properties.RowTimes, hours (10:14)'); ## Test re-anchoring an irregular timetable keeps its spacing %!test %! TT = timetable ((1:5)', 'RowTimes', hours ([0 1 3 6 10])'); %! TT.Properties.StartTime = datetime (2024, 1, 1); %! assert_equal (TT.Properties.RowTimes, ... %! datetime (2024, 1, 1) + hours ([0 1 3 6 10])'); %! assert_equal (ismissing (TT.Properties.TimeStep), true); ## Test re-anchoring a calendar timetable keeps its calendar offsets %!test %! TT = timetable ((1:5)', 'TimeStep', calmonths (1), ... %! 'StartTime', datetime (2024, 1, 1)); %! TT.Properties.StartTime = datetime (2025, 6, 15); %! assert_equal (TT.Properties.RowTimes, ... %! datetime (2025, 6, 15) + calmonths (0:4)'); %! assert_equal (TT.Properties.TimeStep, calmonths (1)); ################################################################################ ## ** Row time subscripts ** ## ################################################################################ ## Test a time range selects the rows in it %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! V = timetable (tv, (1:6)', 'VariableNames', {'A'}); %! S = V(timerange (tv(2), tv(4)), :); %! assert_equal (class (S), 'timetable'); %! assert_equal (S.A, [2; 3]); %! assert_equal (S.Properties.RowTimes, tv(2:3)); ## Test a tolerant subscript selects the rows near the times asked for %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! V = timetable (tv, (1:6)', 'VariableNames', {'A'}); %! assert_equal (V(withtol (tv(3), hours (1)), :).A, [2; 3; 4]); %! assert_equal (V(withtol (tv([5 2]), minutes (1)), :).A, [5; 2]); ## Test a range subscript reaches the braces too %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! V = timetable (tv, (1:6)', 'VariableNames', {'A'}); %! assert_equal (V{timerange (tv(2), tv(4)), 1}, [2; 3]); ## Test a range subscript keeps the step where the rows stay evenly spaced %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! V = timetable (tv, (1:6)', 'VariableNames', {'A'}); %! assert_equal (V(timerange (tv(1), tv(4)), :).Properties.TimeStep, hours (1)); ################################################################################ ## ** Brace reference ** ## ################################################################################ ## Test braces return one variable in its own type %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! V = timetable (tv, (1:6)', 'VariableNames', {'A'}); %! assert_equal (class (V{:, 1}), 'double'); %! assert_equal (V{:, 1}, (1:6)'); ## Test braces take a subset of rows %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! V = timetable (tv, (1:6)', 'VariableNames', {'A'}); %! assert_equal (V{2:3, 1}, [2; 3]); ## Test braces take a variable by name %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! V = timetable (tv, (1:6)', 'VariableNames', {'A'}); %! assert_equal (V{:, 'A'}, (1:6)'); ## Test braces over every variable of one type %!test %! V = timetable (hours (0:5)', (1:6)', (7:12)', 'VariableNames', {'A', 'B'}); %! assert_equal (class (V{:, :}), 'double'); %! assert_equal (V{:, :}, [(1:6)', (7:12)']); ## Test the variable dimension name reaches the variable values %!test %! V = timetable (hours (0:2)', (1:3)', 'VariableNames', {'A'}); %! assert_equal (V.Variables, (1:3)'); ## Test the variable dimension name writes every variable %!test %! V = timetable (hours ([1; 2]), [1; 2], [3; 4]); %! V.Variables = [5, 6; 7, 8]; %! assert_equal (V.Var2, [6; 8]); %! assert_equal (V.Time, hours ([1; 2])); %!error ... %! V = timetable (hours ([1; 2]), [1; 2], [3; 4]); V.Variables = 5; ## Test braces give a cell array for variables that cannot form one array %!test %! V = timetable (hours (0:1)', (1:2)', {'a'; 'b'}, ... %! 'VariableNames', {'A', 'B'}); %! assert_equal (V{:, :}, {1, 'a'; 2, 'b'}); %! assert_equal (V.Variables, {1, 'a'; 2, 'b'}); %!test %! V = timetable (hours (0:1)', int8 ([100; -1]), int16 ([1000; 2])); %! assert_equal (V{:, :}, int8 ([100, 127; -1, 2])); %! assert_equal (V.Variables, int8 ([100, 127; -1, 2])); %!test %! V = timetable (hours (0:1)', [1.5; 2], string ({'x'; 'y'})); %! assert_equal (V{:, :}, string ({'1.5', 'x'; '2', 'y'})); %!test %! V = timetable (hours (0:1)', datetime (2020, 1, [1; 2]), hours ([1; 2])); %! assert_equal (class (V{:, :}), 'cell'); %! assert_equal (V{2, :}, {datetime(2020, 1, 2), hours(2)}); ################################################################################ ## ** Concatenation ** ## ################################################################################ ## Test vertical concatenation stacks rows in operand order %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! V = timetable (tv, (1:3)', 'VariableNames', {'A'}); %! X = [V; V]; %! assert_equal (size (X), [6, 1]); %! assert_equal (X.Properties.RowTimes, [tv; tv]); %! assert_equal (X.A, [1;2;3;1;2;3]); ## Test vertical concatenation never sorts the row times %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! V = timetable (tv, (1:3)', 'VariableNames', {'A'}); %! W = timetable (tv - hours (9), (4:6)', 'VariableNames', {'A'}); %! assert_equal ([V; W].Properties.RowTimes, [tv; tv - hours(9)]); ## Test blocks that meet exactly keep the step they share %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! V = timetable (tv, (1:3)', 'VariableNames', {'A'}); %! W = timetable (tv + hours (3), (4:6)', 'VariableNames', {'A'}); %! X = [V; W]; %! assert_equal (X.Properties.TimeStep, hours (1)); %! assert_equal (isregular (X), true); ## Test a gap between blocks loses the step %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! V = timetable (tv, (1:3)', 'VariableNames', {'A'}); %! W = timetable (tv + hours (9), (4:6)', 'VariableNames', {'A'}); %! assert_equal (ismissing ([V; W].Properties.TimeStep), true); ## Test three operands stack in order %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! V = timetable (tv, (1:3)', 'VariableNames', {'A'}); %! W = timetable (tv + hours (3), (4:6)', 'VariableNames', {'A'}); %! X = [V; W; V]; %! assert_equal (size (X), [9, 1]); %! assert_equal (X.A, [1;2;3;4;5;6;1;2;3]); ## Test a table stacked onto a timetable gets missing row times %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! V = timetable (tv, (1:3)', 'VariableNames', {'A'}); %! T = table ((4:6)', 'VariableNames', {'A'}); %! X = [V; T]; %! assert_equal (size (X), [6, 1]); %! assert_equal (class (X), 'timetable'); %! assert_equal (X.A, [1;2;3;4;5;6]); %! assert_equal (ismissing (X.Properties.RowTimes), [false(3,1); true(3,1)]); %! assert_equal (ismissing (X.Properties.TimeStep), true); ## Test the row dimension takes the first name that is not the default %!test %! aa = datetime (2024, 1, 1) + hours (0:2)'; %! A = timetable (aa, (1:3)', 'VariableNames', {'A'}); %! D = timetable ((4:6)', 'RowTimes', aa, 'VariableNames', {'A'}); %! assert_equal (D.Properties.DimensionNames{1}, 'Time'); %! assert_equal ([A; D].Properties.DimensionNames{1}, 'aa'); %! assert_equal ([D; A].Properties.DimensionNames{1}, 'aa'); %! assert_equal ([D; D].Properties.DimensionNames{1}, 'Time'); ## Test horizontal concatenation appends variables and keeps the times %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! V = timetable (tv, (1:3)', 'VariableNames', {'A'}); %! N = timetable (tv, (7:9)', 'VariableNames', {'B'}); %! X = [V N]; %! assert_equal (size (X), [3, 2]); %! assert_equal (X.Properties.VariableNames, {'A', 'B'}); %! assert_equal (X.Properties.RowTimes, tv); %! assert_equal (X.Properties.TimeStep, hours (1)); ## Test a table appended to a timetable %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! V = timetable (tv, (1:3)', 'VariableNames', {'A'}); %! X = [V table((7:9)', 'VariableNames', {'B'})]; %! assert_equal (class (X), 'timetable'); %! assert_equal (size (X), [3, 2]); %! assert_equal (X.Properties.RowTimes, tv); ## Test horizontal concatenation takes the first non-default dimension name %!test %! aa = datetime (2024, 1, 1) + hours (0:2)'; %! A = timetable (aa, (1:3)', 'VariableNames', {'A'}); %! D = timetable ((4:6)', 'RowTimes', aa, 'VariableNames', {'B'}); %! assert_equal ([A D].Properties.DimensionNames{1}, 'aa'); %! assert_equal ([D A].Properties.DimensionNames{1}, 'aa'); ## Test 'cat' along each dimension %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! V = timetable (tv, (1:3)', 'VariableNames', {'A'}); %! N = timetable (tv, (7:9)', 'VariableNames', {'B'}); %! assert_equal (size (cat (1, V, V)), [6, 1]); %! assert_equal (size (cat (2, V, N)), [3, 2]); ## Test a null operand takes no part and a single operand passes through %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! V = timetable (tv, (1:3)', 'VariableNames', {'A'}); %! assert_equal (size ([V; []]), [3, 1]); %! assert_equal (size ([V []]), [3, 1]); %! assert_equal (size ([V]), [3, 1]); ################################################################################ ## ** Summary ** ## ################################################################################ ## Test 'summary' files the row times first, under the row dimension's name %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! s = summary (TT); %! assert_equal (fieldnames (s), {'Time'; 'A'; 'B'}); %! TT.Properties.DimensionNames = {'Date', 'Vars'}; %! assert_equal (fieldnames (summary (TT)), {'Date'; 'A'; 'B'}); ## Test the row-times entry reports where they start, run and step %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'A'}); %! e = summary (TT).Time; %! assert_equal (fieldnames (e), {'Size'; 'Type'; 'TimeZone'; 'SampleRate'; ... %! 'StartTime'; 'NumMissing'; 'Min'; ... %! 'Median'; 'Max'; 'Mean'; 'Std'; 'TimeStep'}); %! assert_equal (e.StartTime, t0); %! assert_equal (e.TimeStep, hours (1)); %! assert_equal (e.Min, t0); %! assert_equal (e.Max, t0 + hours (5)); %! assert_equal (e.Mean, t0 + hours (2.5)); ## Test duration row times carry no time zone %!test %! Q = timetable ((1:4)', 'RowTimes', hours (0:3)', 'VariableNames', {'A'}); %! e = summary (Q).Time; %! assert_equal (any (strcmp (fieldnames (e), 'TimeZone')), false); %! assert_equal (e.StartTime, hours (0)); %! assert_equal (e.Mean, hours (1.5)); ## Test a zoned row time keeps its zone through every statistic %!test %! t0 = datetime (2024, 1, 1, 'TimeZone', 'America/New_York'); %! TT = timetable ((1:4)', 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A'}); %! e = summary (TT).Time; %! assert_equal (e.TimeZone, 'America/New_York'); %! assert_equal (e.Mean.TimeZone, 'America/New_York'); ## Test an irregular timetable reports no rate and no step %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:4)', 'RowTimes', t0 + hours ([0 1 2 9])', ... %! 'VariableNames', {'A'}); %! e = summary (TT).Time; %! assert_equal (isnan (e.SampleRate), true); %! assert_equal (isnan (e.TimeStep), true); ## Test a missing row time is counted and left out of the statistics %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:3)', 'RowTimes', [t0; NaT; t0 + hours(2)], ... %! 'VariableNames', {'A'}); %! e = summary (TT).Time; %! assert_equal (e.NumMissing, 1); %! assert_equal (e.Min, t0); %! assert_equal (e.Max, t0 + hours (2)); ## Test a variable entry carries the mean and the deviation %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([0; 2; 4], 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'A'}); %! e = summary (TT).A; %! assert_equal (fieldnames (e), {'Size'; 'Type'; 'Description'; 'Units'; ... %! 'Continuity'; 'NumMissing'; 'Min'; ... %! 'Median'; 'Max'; 'Mean'; 'Std'}); %! assert_equal (e.Mean, 2); %! assert_equal (e.Std, 2); ## Test the deviation of a lone observation is nothing at all %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable (7, 'RowTimes', t0, 'VariableNames', {'A'}); %! assert_equal (summary (TT).A.Std, 0); ## Test the printed form names the sections it has %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'A'}); %! str = evalc ('summary (TT);'); %! assert_equal (! isempty (strfind (str, 'Row Times:')), true); %! assert_equal (! isempty (strfind (str, ' Time: datetime')), true); %! assert_equal (! isempty (strfind (str, 'Variables:')), true); %! assert_equal (! isempty (strfind (str, ' A: double')), true); %! assert_equal (! isempty (strfind (str, ... %! 'Statistics for applicable variables and row times:')), true); ## Test a timetable with no rows reports no statistics %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'A'}); %! str = evalc ('summary (TT([], :));'); %! assert_equal (! isempty (strfind (str, 'Row Times:')), true); %! assert_equal (isempty (strfind (str, 'Statistics')), true); ## Test input validation for 'timetable.summary' %!error ... %! summary (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'all'); %!error ... %! [a, b] = summary (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)')); ################################################################################ ## ** Missing data ** ## ################################################################################ ## Test 'ismissing' reports the variables and not the row times %!test %! t0 = datetime (2024, 1, 1); %! N = timetable ([1; 2; NaN; 4], {'a'; ''; 'c'; 'd'}, 'RowTimes', ... %! [t0; NaT; t0 + hours(2); t0 + hours(3)], ... %! 'VariableNames', {'A', 'C'}); %! assert_equal (size (ismissing (N)), [4, 2]); %! assert_equal (ismissing (N), logical ([0 0; 0 1; 1 0; 0 0])); ## Test 'ismissing' takes an indicator and keeps its shape %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! assert_equal (ismissing (TT, 3), logical ([0 0; 0 0; 1 0; 0 0; 0 0; 0 0])); %! assert_equal (size (ismissing (TT([], :))), [0, 2]); %! Q = timetable ([1; 2; 3; 4], 'RowTimes', hours (0:3)'); %! assert_equal (size (ismissing (Q)), [4, 1]); ## Test 'ismissing' matches an indicator by the class of each variable %!test %! TT = timetable (single ([1; 0; 2]), [1; 0; 2], 'RowTimes', ... %! seconds ([1; 2; 3])); %! assert_equal (ismissing (TT, true), logical ([0, 1; 0, 0; 0, 0])); %! assert_equal (ismissing (TT, {single(2), int8(0)}), ... %! logical ([0, 0; 0, 1; 1, 1])); ## Test 'anymissing' reads the same thing 'ismissing' does %!test %! t0 = datetime (2024, 1, 1); %! N = timetable ([1; 2; NaN; 4], 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A'}); %! assert_equal (anymissing (N), true); %! TT = timetable ((1:4)', 'RowTimes', t0 + hours (0:3)'); %! assert_equal (anymissing (TT), false); ## Test a missing row time alone does not make 'anymissing' true %!test %! t0 = datetime (2024, 1, 1); %! T = timetable ([1; 2; 3], 'RowTimes', [t0; NaT; t0 + hours(2)], ... %! 'VariableNames', {'A'}); %! assert_equal (anymissing (T), false); %! assert_equal (any (ismissing (T)(:)), false); ## Test 'rmmissing' removes the rows holding a missing value %!test %! t0 = datetime (2024, 1, 1); %! N = timetable ([1; 2; NaN; 4], {'a'; ''; 'c'; 'd'}, 'RowTimes', ... %! [t0; NaT; t0 + hours(2); t0 + hours(3)], ... %! 'VariableNames', {'A', 'C'}); %! [R, TF] = rmmissing (N); %! assert_equal (R.Properties.RowTimes, [t0; t0 + hours(3)]); %! assert_equal (TF, [false; true; true; false]); ## Test a row whose time is missing goes although its data is sound %!test %! t0 = datetime (2024, 1, 1); %! T = timetable ([1; 2; 3], 'RowTimes', [t0; NaT; t0 + hours(2)], ... %! 'VariableNames', {'A'}); %! [R, TF] = rmmissing (T); %! assert_equal (R.A, [1; 3]); %! assert_equal (TF, [false; true; false]); ## Test 'DataVariables' does not reach the row times %!test %! t0 = datetime (2024, 1, 1); %! N = timetable ([1; 2; NaN; 4], {'a'; ''; 'c'; 'd'}, 'RowTimes', ... %! [t0; NaT; t0 + hours(2); t0 + hours(3)], ... %! 'VariableNames', {'A', 'C'}); %! R = rmmissing (N, 'DataVariables', 'A'); %! assert_equal (R.Properties.RowTimes, [t0; t0 + hours(3)]); ## Test 'MinNumMissing' does not reach the row times either %!test %! t0 = datetime (2024, 1, 1); %! T = timetable ([1; 2; 3], 'RowTimes', [t0; NaT; t0 + hours(2)], ... %! 'VariableNames', {'A'}); %! R = rmmissing (T, 'MinNumMissing', 2); %! assert_equal (R.A, [1; 3]); ## Test the time step is read afresh from the rows that survive %!test %! t0 = datetime (2024, 1, 1); %! N = timetable ([1; 2; NaN; 4], {'a'; ''; 'c'; 'd'}, 'RowTimes', ... %! [t0; NaT; t0 + hours(2); t0 + hours(3)], ... %! 'VariableNames', {'A', 'C'}); %! assert_equal (rmmissing (N).Properties.TimeStep, hours (3)); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! assert_equal (rmmissing (TT).Properties.TimeStep, hours (1)); ## Test 'standardizeMissing' makes the given values missing %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! S = standardizeMissing (TT, 3); %! assert_equal (S.A, [1; 2; NaN; 4; 5; 6]); %! assert_equal (S.Properties.RowTimes, t0 + hours (0:5)'); %! assert_equal (S.Properties.TimeStep, hours (1)); %! D = standardizeMissing (TT, 30, 'DataVariables', 'B'); %! assert_equal (D.B, [10; 20; NaN; 40; 50; 60]); %!test %! TT = timetable (hours ([1; 2]), hours ([1; -99]), [1; -99], ... %! 'VariableNames', {'D', 'N'}); %! S = standardizeMissing (TT, hours (-99)); %! assert_equal (hours (S.D), [1; NaN]); %! assert_equal (S.N, [1; -99]); %! assert_equal (S.Properties.RowTimes, hours ([1; 2])); ## Test 'fillmissing' interpolates against the row times, not the row order %!test %! t0 = datetime (2024, 1, 1); %! F = timetable ([10; NaN; 30], 'RowTimes', t0 + hours ([0 1 3])', ... %! 'VariableNames', {'A'}); %! assert_equal (fillmissing (F, 'linear').A, [10; 16.666666666666668; 30]); %! assert_equal (fillmissing (F, 'spline').A, [10; 16.666666666666668; 30]); ## Test 'nearest' measures the distance in time and not in rows %!test %! t0 = datetime (2024, 1, 1); %! F = timetable ([10; NaN; 30], 'RowTimes', t0 + hours ([0 1 3])', ... %! 'VariableNames', {'A'}); %! assert_equal (fillmissing (F, 'nearest').A, [10; 10; 30]); %! assert_equal (fillmissing (F, 'previous').A, [10; 10; 30]); %! assert_equal (fillmissing (F, 'next').A, [10; 30; 30]); ## Test a constant fill and an 'EndValues' override %!test %! t0 = datetime (2024, 1, 1); %! F = timetable ([10; NaN; 30], 'RowTimes', t0 + hours ([0 1 3])', ... %! 'VariableNames', {'A'}); %! assert_equal (fillmissing (F, 'constant', 0).A, [10; 0; 30]); %! assert_equal (fillmissing (F, 'linear', 'EndValues', 'nearest').A, ... %! [10; 16.666666666666668; 30]); ## Test the row times are never filled and the time step is unchanged %!test %! t0 = datetime (2024, 1, 1); %! F = timetable ([10; NaN; 30], 'RowTimes', t0 + hours ([0 1 3])', ... %! 'VariableNames', {'A'}); %! G = fillmissing (F, 'linear'); %! assert_equal (G.Properties.RowTimes, t0 + hours ([0 1 3])'); %! assert_equal (isnan (G.Properties.TimeStep), true); %! R = timetable ([10; NaN; 30], 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'A'}); %! assert_equal (fillmissing (R, 'linear').Properties.TimeStep, hours (1)); ## Test input validation for 'timetable.fillmissing' %!error ... %! fillmissing (timetable ([1; 2; 3], 'RowTimes', ... %! [datetime(2024,1,1); NaT; datetime(2024,1,1) + hours(2)]), ... %! 'previous'); %!error ... %! fillmissing (timetable ([10; NaN; 30], 'RowTimes', ... %! datetime (2024, 1, 1) + hours ([0 1 3])'), 'linear', ... %! 'SamplePoints', (1:3)'); %!error ... %! fillmissing (timetable ([10; NaN; 30], 'RowTimes', ... %! datetime (2024, 1, 1) + hours ([0 1 3])'), 'linear', ... %! 'Bogus', 1); ## Test the shared branches of 'fillmissing' name the timetable %!error ... %! fillmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 5); %!error ... %! fillmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'constant'); %!error ... %! fillmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'movmean', 3); %!error ... %! fillmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'previous', 5); %!error ... %! fillmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'previous', 5, 1); %!error ... %! fillmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'previous', ... %! 'ReplaceValues', 'yes'); %!error ... %! fillmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'previous', ... %! 'ReplaceValues', false); %!error ... %! fillmissing (timetable ([NaN; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'linear', ... %! 'EndValues', 'bogus'); %!error ... %! fillmissing (timetable ([1; NaN; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'linear', ... %! 'DataVariables', 'Some'); %!error ... %! fillmissing (timetable ([1; NaN; 3], {'a'; ''; 'c'}, 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)', ... %! 'VariableNames', {'x', 'y'}), 'linear'); ## Test the validation of 'ismissing' is the timetable's own %!error ... %! ismissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), ones (2, 2)); ## Test DIM 2 on a timetable keeps every row and its row time %!test %! TT = timetable (hours ([0; NaN; 2; 3]), [1; 2; 3; 4], [5; 6; 7; 8], ... %! [9; 10; 11; NaN], 'VariableNames', {'a', 'b', 'c'}); %! [R, TF] = rmmissing (TT, 2); %! assert_equal (R.Properties.VariableNames, {'a', 'b'}); %! assert_equal (hours (R.Properties.RowTimes), [0; NaN; 2; 3]); %! assert_equal (TF, [false, false, true]); %!test %! TT = timetable (hours ([0; NaN; 2; 3]), [1; 2; 3; 4], [5; 6; 7; 8], ... %! [9; 10; 11; NaN], 'VariableNames', {'a', 'b', 'c'}); %! L = logical ([1, 0, 0; 0, 1, 0; 0, 1, 0; 0, 0, 0]); %! [R, TF] = rmmissing (TT, 2, 'MissingLocations', L); %! assert_equal (R.Properties.VariableNames, {'c'}); %! assert_equal (height (R), 4); %! assert_equal (TF, [true, true, false]); ## Test the validation of 'rmmissing' is the timetable's own %!error ... %! rmmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 3); %!error ... %! rmmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'MinNumMissing', -1); ## Test the shared branches of 'ismissing' name the timetable %!error ... %! ismissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'OutputFormat', 'nope'); %!error ... %! ismissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), {[1, 2]}); ## Test the shared branches of 'rmmissing' name the timetable %!error ... %! rmmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'MinNumMissing'); %!error ... %! rmmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'Bogus', 1); %!error ... %! rmmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'DataVariables', 'Some'); %!error ... %! rmmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), ... %! 'MissingLocations', true (4, 4)); %!error ... %! rmmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)', 'VariableNames', {'a'}), ... %! 'MissingLocations', ... %! table (logical ([0; 1; 0]), 'VariableNames', {'b'})); %!error ... %! rmmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)', 'VariableNames', {'a'}), ... %! 'MissingLocations', table ([1; 2; 3], 'VariableNames', {'a'})); %!error ... %! rmmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)', 'VariableNames', {'a'}), ... %! 'MissingLocations', ... %! table (logical ([0; 1]), 'VariableNames', {'a'})); %!error ... %! rmmissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), ... %! 'MissingLocations', 'nope'); ## Test the shared branch of 'standardizeMissing' names the timetable %!error ... %! standardizeMissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 1, ... %! 'DataVariables', 'Some'); %!error ... %! standardizeMissing (timetable ([1; 2; 3], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 1, 'Bogus', 1); %!error ... %! standardizeMissing (timetable (hours (1), datetime (2020, 1, 1), ... %! 'VariableNames', {'D'}), ... %! datetime (2020, 1, 1, 'TimeZone', 'UTC')); ################################################################################ ## ** Variable management ** ## ################################################################################ ## Test 'addvars' appends and names, and leaves the row times alone %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! assert_equal (addvars (TT, (1:6)').Properties.VariableNames, ... %! {'A', 'B', 'Var3'}); %! assert_equal (addvars (TT, (1:6)', 'NewVariableNames', ... %! {'Z'}).Properties.VariableNames, {'A', 'B', 'Z'}); %! assert_equal (addvars (TT, (1:6)').Properties.TimeStep, hours (1)); ## Test 'addvars' places a variable before or after another %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! assert_equal (addvars (TT, (1:6)', 'Before', 'B', 'NewVariableNames', ... %! {'Z'}).Properties.VariableNames, {'A', 'Z', 'B'}); %! assert_equal (addvars (TT, (1:6)', 'Before', 1, 'NewVariableNames', ... %! {'Z'}).Properties.VariableNames, {'Z', 'A', 'B'}); ## Test a name taken from the workspace is deconflicted, not refused %!test %! t0 = datetime (2024, 1, 1); %! Time = t0 + hours (0:5)'; %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', Time, ... %! 'VariableNames', {'A', 'B'}); %! assert_equal (addvars (TT, Time).Properties.VariableNames, ... %! {'A', 'B', 'Time_1'}); ## Test 'removevars' can empty a timetable of variables but not of times %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! assert_equal (removevars (TT, 'B').Properties.VariableNames, {'A'}); %! E = removevars (TT, {'A', 'B'}); %! assert_equal (size (E), [6, 0]); %! assert_equal (E.Properties.RowTimes, t0 + hours (0:5)'); %! assert_equal (E.Properties.TimeStep, hours (1)); ## Test 'movevars' and 'renamevars' carry the metadata with the variable %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! TT.Properties.VariableUnits = {'m', 's'}; %! M = movevars (TT, 'B', 'Before', 'A'); %! assert_equal (M.Properties.VariableNames, {'B', 'A'}); %! assert_equal (M.Properties.VariableUnits, {'s', 'm'}); %! R = renamevars (TT, 'A', 'AA'); %! assert_equal (R.Properties.VariableNames, {'AA', 'B'}); %! assert_equal (R.Properties.VariableUnits, {'m', 's'}); ## Test the dimension names are left alone by a rename %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! assert_equal (renamevars (TT, 'B', 'BB').Properties.DimensionNames, ... %! {'Time', 'Variables'}); ## Test 'convertvars' converts a variable and keeps the row times %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'A'}); %! C = convertvars (TT, 'A', 'string'); %! assert_equal (class (C.A), 'string'); %! assert_equal (C.Properties.TimeStep, hours (1)); ## Test 'mergevars' combines variables and discards their units %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! TT.Properties.VariableUnits = {'m', 's'}; %! M = mergevars (TT, {'A', 'B'}); %! assert_equal (M.Properties.VariableNames, {'Var1'}); %! assert_equal (M.Properties.VariableUnits, {''}); %! assert_equal (size (M.Var1), [6, 2]); %! assert_equal (M.Properties.TimeStep, hours (1)); %! N = mergevars (TT, {'A', 'B'}, 'NewVariableName', 'AB'); %! assert_equal (N.Properties.VariableNames, {'AB'}); ## Test 'splitvars' splits a multicolumn variable into one each %!test %! t0 = datetime (2024, 1, 1); %! W = timetable ([1 2; 3 4; 5 6], (7:9)', 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'MC', 'S'}); %! assert_equal (splitvars (W).Properties.VariableNames, ... %! {'MC_1', 'MC_2', 'S'}); %! assert_equal (splitvars (W, 'MC', 'NewVariableNames', ... %! {'p', 'q'}).Properties.VariableNames, ... %! {'p', 'q', 'S'}); %! assert_equal (splitvars (W).Properties.TimeStep, hours (1)); ## Test the row times are not a variable for any of the family %!shared TT %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:5)', ... %! 'VariableNames', {'A', 'B'}); %!error ... %! removevars (TT, 'Time'); %!error ... %! convertvars (TT, 'Time', 'double'); %!error ... %! splitvars (TT, 'Time'); %!error ... %! mergevars (TT, {'Time', 'A'}); ## Test input validation for the variable-management family %!error ... %! addvars (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), (1:3)', 'Before', 'Time'); %!error ... %! addvars (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), (1:3)', ... %! 'NewVariableNames', {'Time'}); %!error ... %! renamevars (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)', ... %! 'VariableNames', {'A'}), 'A', 'Time'); %!error ... %! removevars (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'Time'); %!error ... %! movevars (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)', 'VariableNames', {'A'}), ... %! 'A', 'Before', 'Time'); ## Test the validation of the delegating methods is the timetable's own %!error ... %! removevars (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)')); %!error ... %! convertvars (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)')); %!error ... %! mergevars (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)')); %!error ... %! splitvars (timetable ([1 2; 3 4; 5 6], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)', ... %! 'VariableNames', {'m'}), 'm', 'NewVariableNames', {'x'}); ## Test a timetable with neither rows nor variables takes the height of the ## first variable given it, with missing row times %!test %! E = timetable (); %! E.x = [1; 2; 3]; %! assert_equal (size (E), [3, 1]); %! assert_equal (isnat (E.Properties.RowTimes), true (3, 1)); %!test %! E = addvars (timetable (), [1; 2; 3]); %! assert_equal (size (E), [3, 1]); ## Test the shared branches of 'addvars' name the timetable %!error ... %! addvars (TT, (1:6)', 'After'); %!error ... %! addvars (TT, (1:6)', 'After', 1, 'Before', 2); %!error ... %! addvars (TT, (1:6)', 'Bogus', 1); %!error ... %! addvars (TT, 5); %!error ... %! addvars (TT, (1:6)', 'NewVariableNames', {'X', 'Y'}); %!error ... %! addvars (TT, (1:6)', (1:6)', 'NewVariableNames', {'X', 'X'}); %!error ... %! addvars (TT, (1:6)', (1:6)', 'NewVariableNames', {'A', 'B'}); ## Test the shared branches of 'movevars' name the timetable %!error ... %! movevars (TT, 'A', 'After', 1, 'Before', 2); %!error ... %! movevars (TT, 'A', 'After'); %!error ... %! movevars (TT, 'A', 'Bogus', 1); %!error ... %! movevars (TT, 'Z', 'After', 1); %!error ... %! movevars (addvars (TT, (1:6)', 'NewVariableNames', {'C'}), ... %! {'A', 'B'}, 'After', 1); ## Test the shared branches of 'renamevars' name the timetable %!error ... %! renamevars (TT, {'A', 'B'}, {'A', 4}); %!error ... %! renamevars (TT, {'A', 'B'}, {'X', 'X'}); %!error ... %! renamevars (TT, {'A', 'Z'}, {'A', 'X'}); %!error ... %! renamevars (TT, {'A', 'B'}, {'X', 'Y', 'Z'}); %!error ... %! renamevars (TT, 'A', 'B'); ## Test the shared branches of 'convertvars' name the timetable %!error ... %! convertvars (TT, 'A', ['c', 'h'; 'a', 'r']); %!error ... %! convertvars (TT, 'A', {'char'}); %!error ... %! convertvars (TT, 'A', 'struct'); %!error ... %! convertvars (TT, 'A', @(x) sum (x)); ## Test the shared branches of 'mergevars' name the timetable %!error ... %! mergevars (TT, {'A', 'B'}, 'NewVariableName'); %!error ... %! mergevars (TT, {'A', 'B'}, 'Bogus', 1); %!error ... %! mergevars (TT, {'A', 'B'}, 'MergeAsTable', 'on'); %!error ... %! mergevars (TT, {'A', 'B'}, 'NewVariableName', 2); %!error ... %! mergevars (addvars (TT, (1:6)', 'NewVariableNames', {'C'}), ... %! {'A', 'B'}, 'NewVariableName', 'C'); %!error ... %! mergevars (addvars (TT, {'a'; 'b'; 'c'; 'd'; 'e'; 'f'}, ... %! 'NewVariableNames', {'C'}), {'A', 'C'}); ## Test the shared branch of 'splitvars' names the timetable %!error ... %! splitvars (timetable ([5, 6; 5, 6], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)'), 1, ... %! 'NewVariableNames', {1}); ## Test the shared branches of 'addprop' name the timetable %!error ... %! addprop (TT, 'p', 5); %!error ... %! addprop (TT, {'p', 'q'}, {'table'}); %!error ... %! addprop (TT, {'p', 'p'}, {'table', 'variable'}); ################################################################################ ## ** First and last rows ** ## ################################################################################ ## Test 'head' returns the first eight rows by default %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:12)', 'RowTimes', t0 + hours (0:11)', ... %! 'VariableNames', {'A'}); %! assert_equal (head (TT).A, (1:8)'); ## Test 'tail' returns the last eight rows by default %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:12)', 'RowTimes', t0 + hours (0:11)', ... %! 'VariableNames', {'A'}); %! assert_equal (tail (TT).A, (5:12)'); ## Test 'head' and 'tail' take the count asked for %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:12)', 'RowTimes', t0 + hours (0:11)', ... %! 'VariableNames', {'A'}); %! assert_equal (head (TT, 3).A, [1; 2; 3]); %! assert_equal (tail (TT, 3).A, [10; 11; 12]); ## Test asking for more rows than there are returns all of them %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'A'}); %! assert_equal (head (TT, 99).A, (1:6)'); %! assert_equal (tail (TT, 99).A, (1:6)'); ## Test a K of zero returns a timetable with no rows rather than raising %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'A'}); %! H = head (TT, 0); %! assert_equal (size (H), [0, 1]); %! assert_equal (class (H), 'timetable'); %! assert_equal (size (tail (TT, 0)), [0, 1]); ## Test 'head' takes the first rows and not the earliest ones %!test %! t0 = datetime (2024, 1, 1); %! U = timetable ([30; 10; 40; 20], 'RowTimes', t0 + hours ([2 0 3 1])', ... %! 'VariableNames', {'A'}); %! assert_equal (head (U, 2).Properties.RowTimes, t0 + hours ([2 0])'); %! assert_equal (head (U, 2).A, [30; 10]); ## Test the time step is read afresh from the rows that are kept %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'A'}); %! assert_equal (head (TT, 3).Properties.TimeStep, hours (1)); %! assert_equal (tail (TT, 3).Properties.TimeStep, hours (1)); %! G = timetable ((1:4)', 'RowTimes', t0 + hours ([0 1 2 9])', ... %! 'VariableNames', {'A'}); %! assert_equal (head (G, 3).Properties.TimeStep, hours (1)); %! assert_equal (isnan (tail (G, 3).Properties.TimeStep), true); ## Test duration row times come through unchanged %!test %! Q = timetable ([1; 2; 3; 4], 'RowTimes', hours (0:3)', ... %! 'VariableNames', {'A'}); %! assert_equal (head (Q, 2).Properties.RowTimes, hours (0:1)'); %! assert_equal (tail (Q, 2).Properties.RowTimes, hours (2:3)'); ## Test a timetable with no rows yields one with no rows %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'A'}); %! assert_equal (size (head (TT([], :), 3)), [0, 1]); %! assert_equal (size (tail (TT([], :), 3)), [0, 1]); ## Test input validation for 'head' and 'tail' %!error ... %! head (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), -2); %!error ... %! head (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 1.5); %!error ... %! head (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), [1, 2]); %!error ... %! tail (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), -2); %!error ... %! tail (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), Inf); %!error ... %! [a, b] = head (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 2); %!error ... %! [a, b] = tail (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 2); ################################################################################ ## ** Row ordering ** ## ################################################################################ ## Test 'sortrows' orders by the row times when nothing else is asked %!test %! t0 = datetime (2024, 1, 1); %! U = timetable ([30; 10; 40; 20], 'RowTimes', t0 + hours ([2 0 3 1])', ... %! 'VariableNames', {'A'}); %! S = sortrows (U); %! assert_equal (S.Properties.RowTimes, t0 + hours (0:3)'); %! assert_equal (S.A, [10; 20; 30; 40]); ## Test tied row times keep their order and the variables never break the tie %!test %! t0 = datetime (2024, 1, 1); %! D = timetable ([9; 3; 1; 7], 'RowTimes', t0 + hours ([1 0 1 0])', ... %! 'VariableNames', {'A'}); %! assert_equal (sortrows (D).A, [3; 7; 9; 1]); ## Test the second output is the permutation that produced the order %!test %! t0 = datetime (2024, 1, 1); %! U = timetable ([30; 10; 40; 20], 'RowTimes', t0 + hours ([2 0 3 1])', ... %! 'VariableNames', {'A'}); %! [S, index] = sortrows (U); %! assert_equal (index, [2; 4; 1; 3]); %! assert_equal (S.A, U.A(index)); ## Test the row dimension name orders by the row times %!test %! t0 = datetime (2024, 1, 1); %! U = timetable ([30; 10; 40; 20], 'RowTimes', t0 + hours ([2 0 3 1])', ... %! 'VariableNames', {'A'}); %! assert_equal (sortrows (U, 'Time').A, [10; 20; 30; 40]); %! assert_equal (sortrows (U, 'Time', 'descend').A, [40; 30; 20; 10]); ## Test renaming the row dimension renames the key with it %!test %! t0 = datetime (2024, 1, 1); %! U = timetable ([30; 10; 40; 20], 'RowTimes', t0 + hours ([2 0 3 1])', ... %! 'VariableNames', {'A'}); %! U.Properties.DimensionNames = {'Date', 'Vars'}; %! assert_equal (sortrows (U, 'Date').A, [10; 20; 30; 40]); ## Test sorting by a variable carries the row times along with it %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! S = sortrows (TT, 'B', 'descend'); %! assert_equal (S.A, [6; 5; 4; 3; 2; 1]); %! assert_equal (S.Properties.RowTimes, flipud (t0 + hours (0:5)')); ## Test the row dimension name may be one key among several %!test %! t0 = datetime (2024, 1, 1); %! D = timetable ([9; 3; 1; 7], 'RowTimes', t0 + hours ([1 0 1 0])', ... %! 'VariableNames', {'A'}); %! assert_equal (sortrows (D, {'Time', 'A'}).A, [3; 7; 1; 9]); %! assert_equal (sortrows (D, {'A', 'Time'}).A, [1; 3; 7; 9]); ## Test a numeric index counts the variables and never the row times %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! assert_equal (sortrows (TT, 1).A, (1:6)'); %! assert_equal (sortrows (TT, 2).A, (1:6)'); %! assert_equal (sortrows (TT, -1).A, (6:-1:1)'); ## Test missing row times sort last by default %!test %! t0 = datetime (2024, 1, 1); %! N = timetable ([1; 2; NaN; 4], 'RowTimes', ... %! [t0; NaT; t0 + hours(2); t0 + hours(3)], ... %! 'VariableNames', {'A'}); %! rt = sortrows (N).Properties.RowTimes; %! assert_equal (ismissing (rt), [false; false; false; true]); ## Test 'MissingPlacement' moves a missing key to the front %!test %! t0 = datetime (2024, 1, 1); %! N = timetable ([1; 2; NaN; 4], 'RowTimes', ... %! [t0; NaT; t0 + hours(2); t0 + hours(3)], ... %! 'VariableNames', {'A'}); %! assert_equal (sortrows (N, 'A', 'MissingPlacement', 'first').A, ... %! [NaN; 1; 2; 4]); ## Test the time step is read afresh from the sorted row times %!test %! t0 = datetime (2024, 1, 1); %! U = timetable ([30; 10; 40; 20], 'RowTimes', t0 + hours ([2 0 3 1])', ... %! 'VariableNames', {'A'}); %! assert_equal (isnan (U.Properties.TimeStep), true); %! assert_equal (sortrows (U).Properties.TimeStep, hours (1)); ## Test sorting a backwards timetable turns its step positive %!test %! t0 = datetime (2024, 1, 1); %! Z = timetable ((1:4)', 'RowTimes', t0 - hours (0:3)', ... %! 'VariableNames', {'A'}); %! assert_equal (Z.Properties.TimeStep, hours (-1)); %! S = sortrows (Z); %! assert_equal (S.Properties.TimeStep, hours (1)); %! assert_equal (S.Properties.RowTimes, flipud (t0 - hours (0:3)')); ## Test duplicate row times keep the order they were in %!test %! t0 = datetime (2024, 1, 1); %! D = timetable ([1; 2; 3; 4], 'RowTimes', t0 + hours ([0 1 1 3])', ... %! 'VariableNames', {'A'}); %! assert_equal (sortrows (D).A, [1; 2; 3; 4]); ## Test 'ComparisonMethod' is accepted after the keys %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'A'}); %! assert_equal (sortrows (TT, 'A', 'ComparisonMethod', 'abs').A, (1:6)'); ## Test sorting a timetable with no rows returns one with no rows %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'A'}); %! S = sortrows (TT([], :)); %! assert_equal (size (S), [0, 1]); %! assert_equal (class (S), 'timetable'); ## Test duration row times sort like any other %!test %! Q = timetable ([1; 2; 3; 4], 'RowTimes', hours ([2 0 3 1])', ... %! 'VariableNames', {'A'}); %! assert_equal (sortrows (Q).Properties.RowTimes, hours (0:3)'); ## Test 'sortrows' sorts a timetable by a logical variable %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable (logical ([1; 0; 1]), 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'L'}); %! assert_equal (sortrows (TT, 'L').L, logical ([0; 1; 1])); %! assert_equal (sortrows (TT, 'L').Properties.RowTimes, ... %! t0 + hours ([1 0 2])'); ## Test input validation for 'timetable.sortrows' %!error ... %! sortrows (timetable (hours ([1; 2]), calmonths ([2; 1])), 'Var1'); %!error ... %! sortrows (timetable (inputParser (), 'TimeStep', hours (1)), 'Var1'); %!error ... %! sortrows (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'descend'); %!error ... %! sortrows (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'Nope'); %!error ... %! sortrows (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'Variables'); %!error ... %! sortrows (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 0); %!error ... %! sortrows (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 1, ... %! 'MissingPlacement', 'middle'); ## Test the shared branches of 'sortrows' name the timetable %!error ... %! TS = timetable ([38; 43; 38; 40; 49], [71; 69; 64; 67; 64], ... %! [1; 2; 3; 4; 5], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:4)', ... %! 'VariableNames', {'Age', 'Height', 'Z'}); %! sortrows (TS, 'ComparisonMethod', 'param'); %!error ... %! TS = timetable ([38; 43; 38; 40; 49], [71; 69; 64; 67; 64], ... %! [1; 2; 3; 4; 5], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:4)', ... %! 'VariableNames', {'Age', 'Height', 'Z'}); %! sortrows (TS, [], 'ascend', 'param'); %!error ... %! TS = timetable ([38; 43; 38; 40; 49], [71; 69; 64; 67; 64], ... %! [1; 2; 3; 4; 5], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:4)', ... %! 'VariableNames', {'Age', 'Height', 'Z'}); %! sortrows (TS, [], 'aaascend'); %!error ... %! TS = timetable ([38; 43; 38; 40; 49], [71; 69; 64; 67; 64], ... %! [1; 2; 3; 4; 5], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:4)', ... %! 'VariableNames', {'Age', 'Height', 'Z'}); %! sortrows (TS, 'Time', {'ascend', 'ascend'}); %!error ... %! TS = timetable ([38; 43; 38; 40; 49], [71; 69; 64; 67; 64], ... %! [1; 2; 3; 4; 5], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:4)', ... %! 'VariableNames', {'Age', 'Height', 'Z'}); %! sortrows (TS, [true, true, false, true]); %!error ... %! TS = timetable ([38; 43; 38; 40; 49], [71; 69; 64; 67; 64], ... %! [1; 2; 3; 4; 5], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:4)', ... %! 'VariableNames', {'Age', 'Height', 'Z'}); %! sortrows (TS, 'Age', {'ascend', 'ascend'}); %!error ... %! TS = timetable ([38; 43; 38; 40; 49], [71; 69; 64; 67; 64], ... %! [1; 2; 3; 4; 5], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:4)', ... %! 'VariableNames', {'Age', 'Height', 'Z'}); %! sortrows (TS, [1, 6, 3]); %!error ... %! TS = timetable ([38; 43; 38; 40; 49], [71; 69; 64; 67; 64], ... %! [1; 2; 3; 4; 5], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:4)', ... %! 'VariableNames', {'Age', 'Height', 'Z'}); %! sortrows (TS, vartype ('numeric'), {'ascend', 'ascend'}); ## Test a row is its time together with its values, and both must agree %!test %! t0 = datetime (2024, 1, 1); %! V = timetable ([1; 2; 2; 5; 5; 8; 9], 'RowTimes', ... %! t0 + hours ([0 1 1 2 3 4 4])', 'VariableNames', {'A'}); %! U = unique (V); %! assert_equal (height (U), 6); %! assert_equal (U.A, [1; 2; 5; 5; 8; 9]); %! assert_equal (U.Properties.RowTimes, ... %! t0 + hours ([0 1 2 3 4 4])'); ## Test the index vectors map each way between the two timetables %!test %! t0 = datetime (2024, 1, 1); %! V = timetable ([1; 2; 2; 5; 5; 8; 9], 'RowTimes', ... %! t0 + hours ([0 1 1 2 3 4 4])', 'VariableNames', {'A'}); %! [U, ia, ic] = unique (V); %! assert_equal (ia, [1; 2; 4; 5; 6; 7]); %! assert_equal (ic, [1; 2; 2; 3; 4; 5; 6]); %! assert_equal (U.A, V.A(ia)); %! assert_equal (V.A, U.A(ic)); ## Test rows sharing a time but not their values are all kept %!test %! t0 = datetime (2024, 1, 1); %! D = timetable ([1; 2; 3; 4], 'RowTimes', t0 + hours ([0 1 1 3])', ... %! 'VariableNames', {'A'}); %! assert_equal (height (unique (D)), 4); ## Test rows sharing their values at different times are all kept %!test %! t0 = datetime (2024, 1, 1); %! S = timetable ([7; 7; 7], 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'A'}); %! assert_equal (height (unique (S)), 3); ## Test the result comes back ordered by row time %!test %! t0 = datetime (2024, 1, 1); %! U = timetable ([30; 10; 40; 20], 'RowTimes', t0 + hours ([2 0 3 1])', ... %! 'VariableNames', {'A'}); %! assert_equal (unique (U).A, [10; 20; 30; 40]); ## Test 'stable' keeps the rows in the order they were met %!test %! t0 = datetime (2024, 1, 1); %! U = timetable ([30; 10; 40; 20], 'RowTimes', t0 + hours ([2 0 3 1])', ... %! 'VariableNames', {'A'}); %! assert_equal (unique (U, 'stable').A, [30; 10; 40; 20]); ## Test 'rows' is accepted and changes nothing %!test %! t0 = datetime (2024, 1, 1); %! V = timetable ([1; 2; 2; 5], 'RowTimes', t0 + hours ([0 1 1 2])', ... %! 'VariableNames', {'A'}); %! assert_equal (unique (V, 'rows').A, unique (V).A); ## Test the time step is read afresh from the rows that survive %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'A'}); %! assert_equal (unique (TT).Properties.TimeStep, hours (1)); %! V = timetable ([1; 2; 2; 5; 5; 8; 9], 'RowTimes', ... %! t0 + hours ([0 1 1 2 3 4 4])', 'VariableNames', {'A'}); %! assert_equal (isnan (unique (V).Properties.TimeStep), true); ## Test a missing row time survives and comes last %!test %! t0 = datetime (2024, 1, 1); %! N = timetable ([1; 2; NaN; 4], 'RowTimes', ... %! [t0; NaT; t0 + hours(2); t0 + hours(3)], ... %! 'VariableNames', {'A'}); %! U = unique (N); %! assert_equal (height (U), 4); %! assert_equal (ismissing (U.Properties.RowTimes), ... %! [false; false; false; true]); ## Test a timetable with no variables is compared on its row times alone %!test %! t0 = datetime (2024, 1, 1); %! D = timetable ([1; 2; 3; 4], 'RowTimes', t0 + hours ([0 1 1 3])', ... %! 'VariableNames', {'A'}); %! U = unique (D(:, [])); %! assert_equal (size (U), [3, 0]); %! assert_equal (U.Properties.RowTimes, t0 + hours ([0 1 3])'); ## Test duration row times compare like any other %!test %! Q = timetable ([1; 2; 3; 4], 'RowTimes', hours ([2 0 3 1])', ... %! 'VariableNames', {'A'}); %! assert_equal (unique (Q).Properties.RowTimes, hours (0:3)'); ## Test input validation for 'timetable.unique' %!error ... %! unique (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 1, 2); %!error ... %! unique (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'some'); %!error ... %! unique (timetable ({1; 2; 3}, 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)')); %!error ... %! unique (timetable (inputParser (), 'TimeStep', hours (1))); ## Test 'topkrows' ranks by the row times, latest first %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'A'}); %! K = topkrows (TT, 3); %! assert_equal (K.A, [6; 5; 4]); %! assert_equal (K.Properties.RowTimes, t0 + hours ([5 4 3])'); ## Test the second output names the rows that were chosen %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'A'}); %! [K, index] = topkrows (TT, 3); %! assert_equal (index, [6; 5; 4]); %! assert_equal (K.A, TT.A(index)); ## Test ranking by a variable and by the row dimension name %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! assert_equal (topkrows (TT, 2, 'B').B, [60; 50]); %! assert_equal (topkrows (TT, 2, 'Time').A, [6; 5]); ## Test a direction of 'ascend' takes the bottom rows instead %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! assert_equal (topkrows (TT, 2, 'B', 'ascend').B, [10; 20]); %! assert_equal (topkrows (TT, 2, 'Time', 'ascend').Properties.RowTimes, ... %! t0 + hours (0:1)'); ## Test a numeric index counts variables and ranks downwards when positive %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! assert_equal (topkrows (TT, 2, 1).A, [6; 5]); %! assert_equal (topkrows (TT, 2, -1).A, [1; 2]); ## Test asking for more rows than there are returns all of them, ranked %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'A'}); %! K = topkrows (TT, 99); %! assert_equal (K.A, (6:-1:1)'); %! assert_equal (K.Properties.TimeStep, hours (-1)); ## Test a K of zero returns a timetable with no rows %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'A'}); %! K = topkrows (TT, 0); %! assert_equal (size (K), [0, 1]); %! assert_equal (isnan (K.Properties.TimeStep), true); ## Test the row times need not be sorted to be ranked %!test %! t0 = datetime (2024, 1, 1); %! U = timetable ([30; 10; 40; 20], 'RowTimes', t0 + hours ([2 0 3 1])', ... %! 'VariableNames', {'A'}); %! assert_equal (topkrows (U, 2).Properties.RowTimes, ... %! t0 + hours ([3 2])'); ## Test tied row times keep the order they were in %!test %! t0 = datetime (2024, 1, 1); %! D = timetable ([1; 2; 3; 4], 'RowTimes', t0 + hours ([0 1 1 3])', ... %! 'VariableNames', {'A'}); %! assert_equal (topkrows (D, 3).A, [4; 2; 3]); ## Test a missing row time ranks last however the sort runs %!test %! t0 = datetime (2024, 1, 1); %! N = timetable ([1; 2; NaN; 4], 'RowTimes', ... %! [t0; NaT; t0 + hours(2); t0 + hours(3)], ... %! 'VariableNames', {'A'}); %! assert_equal (topkrows (N, 2).Properties.RowTimes, ... %! t0 + hours ([3 2])'); ## Test ranking by time alone reaches a timetable holding a cell variable %!test %! t0 = datetime (2024, 1, 1); %! M = timetable ((1:4)', {1; 2; 3; 4}, 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'num', 'cel'}); %! K = topkrows (M, 2); %! assert_equal (size (K), [2, 2]); %! assert_equal (K.num, [4; 3]); ## Test input validation for 'timetable.topkrows' %!error ... %! topkrows (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), -1); %!error ... %! topkrows (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 1.5); %!error ... %! topkrows (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 2, 'Nope'); ## Test 'issorted' reads the row times and nothing else %!test %! t0 = datetime (2024, 1, 1); %! assert_equal (issorted (timetable ((1:4)', 'RowTimes', ... %! t0 + hours (0:3)')), true); %! assert_equal (issorted (timetable ((1:4)', 'RowTimes', ... %! t0 + hours ([2 0 3 1])')), false); %! assert_equal (issorted (timetable ((1:4)', 'RowTimes', ... %! t0 - hours (0:3)')), false); ## Test repeated row times are sorted, the ordering not being strict %!test %! t0 = datetime (2024, 1, 1); %! D = timetable ([1; 2; 3; 4], 'RowTimes', t0 + hours ([0 1 1 3])'); %! assert_equal (issorted (D), true); ## Test a missing row time leaves a timetable unsorted %!test %! t0 = datetime (2024, 1, 1); %! N = timetable ([1; 2; 3], 'RowTimes', [t0; NaT; t0 + hours(2)]); %! assert_equal (issorted (N), false); ## Test fewer than two rows have nothing out of order %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:4)', 'RowTimes', t0 + hours (0:3)'); %! assert_equal (issorted (TT(1,:)), true); %! assert_equal (issorted (TT([],:)), true); ## Test duration row times are read the same way %!test %! assert_equal (issorted (timetable ((1:4)', 'RowTimes', hours (0:3)')), true); %! assert_equal (issorted (timetable ((1:4)', 'RowTimes', ... %! hours ([2 0 3 1])')), false); ## Test 'issortedrows' asks about the row times by default %!test %! t0 = datetime (2024, 1, 1); %! assert_equal (issortedrows (timetable ((1:4)', 'RowTimes', ... %! t0 + hours (0:3)')), true); %! assert_equal (issortedrows (timetable ((1:4)', 'RowTimes', ... %! t0 + hours ([2 0 3 1])')), false); ## Test 'issortedrows' takes the row dimension name, renamed or not %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:4)', 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A'}); %! assert_equal (issortedrows (TT, 'Time'), true); %! TT.Properties.DimensionNames = {'Date', 'Vars'}; %! assert_equal (issortedrows (TT, 'Date'), true); ## Test 'issortedrows' asks about a variable and about a direction %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', [10;20;30;40;50;60], 'RowTimes', ... %! t0 + hours (0:5)', 'VariableNames', {'A', 'B'}); %! assert_equal (issortedrows (TT, 'B'), true); %! assert_equal (issortedrows (TT, 'B', 'descend'), false); %! assert_equal (issortedrows (TT, 1), true); %! assert_equal (issortedrows (TT, -1), false); ## Test 'issortedrows' takes several keys at once %!test %! t0 = datetime (2024, 1, 1); %! S = timetable ([9; 3; 1; 7], 'RowTimes', t0 + hours ([1 0 1 0])', ... %! 'VariableNames', {'A'}); %! assert_equal (issortedrows (S, {'Time', 'A'}), false); %! assert_equal (issortedrows (sortrows (S, {'Time', 'A'}), ... %! {'Time', 'A'}), true); ## Test a missing row time and an empty timetable %!test %! t0 = datetime (2024, 1, 1); %! N = timetable ([1; 2; 3], 'RowTimes', [t0; NaT; t0 + hours(2)]); %! assert_equal (issortedrows (N), false); %! TT = timetable ((1:4)', 'RowTimes', t0 + hours (0:3)'); %! assert_equal (issortedrows (TT([],:)), true); ## Test input validation for 'issorted' and 'issortedrows' %!error ... %! issorted (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'descend'); %!error ... %! issortedrows (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'descend'); %!error ... %! issortedrows (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), 'Nope'); ################################################################################ ## ** Range predicates ** ## ################################################################################ ## Test a range inside the timetable is contained and overlapped, not within %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! R = timerange (t0 + hours (1), t0 + hours (3)); %! assert_equal (containsrange (TT, R), true); %! assert_equal (overlapsrange (TT, R), true); %! assert_equal (withinrange (TT, R), false); ## Test the rows a range selects are the same for all three predicates %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! R = timerange (t0 + hours (1), t0 + hours (3)); %! [~, wc] = containsrange (TT, R); %! [~, wo] = overlapsrange (TT, R); %! [~, ww] = withinrange (TT, R); %! assert_equal (wc, [false; true; true; false; false; false]); %! assert_equal (wc, wo); %! assert_equal (wc, ww); ## Test the selected rows come back as a logical column %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! [~, w] = containsrange (TT, timerange (t0, t0 + hours (2))); %! assert_equal (class (w), 'logical'); %! assert_equal (size (w), [6, 1]); ## Test a half-open range ending on the last row time is not one to be within %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! R = timerange (t0, t0 + hours (5)); %! assert_equal (containsrange (TT, R), true); %! assert_equal (withinrange (TT, R), false); ## Test closing that same range turns 'withinrange' true %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! R = timerange (t0, t0 + hours (5), 'closed'); %! assert_equal (withinrange (TT, R), true); ## Test an open range excludes both of its own bounds %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! [tf, w] = withinrange (TT, timerange (t0, t0 + hours (5), 'open')); %! assert_equal (tf, false); %! assert_equal (w, [false; true; true; true; true; false]); ## Test 'openleft' excludes the lower bound and keeps the upper one %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! [~, w] = containsrange (TT, timerange (t0, t0 + hours (5), 'openleft')); %! assert_equal (w, [false; true; true; true; true; true]); ## Test a range straddling the start overlaps but is not contained %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! R = timerange (t0 - hours (2), t0 + hours (2)); %! assert_equal (containsrange (TT, R), false); %! assert_equal (overlapsrange (TT, R), true); %! assert_equal (withinrange (TT, R), false); ## Test a disjoint range is neither contained, overlapped nor within %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! R = timerange (t0 + hours (10), t0 + hours (12)); %! assert_equal (containsrange (TT, R), false); %! assert_equal (overlapsrange (TT, R), false); %! assert_equal (withinrange (TT, R), false); ## Test a range wider than the timetable is one it lies within %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! R = timerange (t0 - hours (1), t0 + hours (9)); %! assert_equal (containsrange (TT, R), false); %! assert_equal (withinrange (TT, R), true); ## Test a range starting exactly at the last row time still overlaps %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! R = timerange (t0 + hours (5), t0 + hours (9)); %! assert_equal (overlapsrange (TT, R), true); %! assert_equal (containsrange (TT, R), false); ## Test a single instant is contained and picks out the row holding it %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! [tf, w] = containsrange (TT, t0 + hours (2)); %! assert_equal (tf, true); %! assert_equal (w, [false; false; true; false; false; false]); ## Test an instant between two rows is contained although no row holds it %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! [tf, w] = containsrange (TT, t0 + minutes (90)); %! assert_equal (tf, true); %! assert_equal (any (w), false); ## Test an instant outside the timetable is contained by none of it %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! assert_equal (containsrange (TT, t0 + hours (9)), false); %! assert_equal (overlapsrange (TT, t0 + hours (9)), false); ## Test a timetable is within a single instant only when every row is at it %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! assert_equal (withinrange (TT, t0), false); %! assert_equal (withinrange (TT(1,:), t0), true); ## Test a missing instant names no range and answers false %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! assert_equal (containsrange (TT, NaT), false); %! assert_equal (overlapsrange (TT, NaT), false); %! assert_equal (withinrange (TT, NaT), false); ## Test a second timetable gives a range closed at both of its ends %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! assert_equal (containsrange (TT, TT), true); %! assert_equal (withinrange (TT, TT), true); ## Test the wider of two timetables contains the narrower %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! W = timetable ((1:12)', 'RowTimes', t0 + hours (0:11)'); %! assert_equal (containsrange (W, TT), true); %! assert_equal (containsrange (TT, W), false); %! assert_equal (withinrange (TT, W), true); ## Test only the two ends are read, so unsorted row times answer the same %!test %! t0 = datetime (2024, 1, 1); %! U = timetable ([30; 10; 40; 20], 'RowTimes', t0 + hours ([2 0 3 1])'); %! R = timerange (t0 + hours (1), t0 + hours (3)); %! [tf, w] = containsrange (U, R); %! assert_equal (tf, true); %! assert_equal (w, [true; false; false; true]); ## Test duplicate row times do not disturb the range %!test %! t0 = datetime (2024, 1, 1); %! D = timetable ([1; 2; 3; 4], 'RowTimes', t0 + hours ([0 1 1 3])'); %! [tf, w] = containsrange (D, timerange (t0 + hours (1), t0 + hours (3))); %! assert_equal (tf, true); %! assert_equal (w, [false; true; true; false]); ## Test missing row times take no part in the range and select no row %!test %! t0 = datetime (2024, 1, 1); %! N = timetable ([1; 2; 3; 4], 'RowTimes', ... %! [t0; NaT; t0 + hours(2); t0 + hours(3)]); %! [tf, w] = withinrange (N, timerange (t0 - hours (1), t0 + hours (9))); %! assert_equal (tf, true); %! assert_equal (w, [true; false; true; true]); ## Test a timetable left with no row times has no range at all %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! E = TT([], :); %! [tf, w] = containsrange (E, timerange (t0, t0 + hours (2))); %! assert_equal (tf, false); %! assert_equal (size (w), [0, 1]); ## Test duration row times work against a duration range %!test %! Q = timetable ([1; 2; 3; 4], 'RowTimes', hours (0:3)'); %! [tf, w] = containsrange (Q, timerange (hours (1), hours (3))); %! assert_equal (tf, true); %! assert_equal (w, [false; true; true; false]); ## Test a descending timetable spans the same range as an ascending one %!test %! t0 = datetime (2024, 1, 1); %! Z = timetable ([1; 2; 3; 4], 'RowTimes', t0 - hours (0:3)'); %! R = timerange (t0 - hours (2), t0); %! assert_equal (containsrange (Z, R), true); %! assert_equal (overlapsrange (Z, R), true); ## Test a range unbounded below is never contained and can still be within %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! R = timerange (-Inf, t0 + hours (9)); %! assert_equal (containsrange (TT, R), false); %! assert_equal (overlapsrange (TT, R), true); %! assert_equal (withinrange (TT, R), true); ## Test a range unbounded above stops the timetable being within it early %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ((1:6)', 'RowTimes', t0 + hours (0:5)'); %! assert_equal (withinrange (TT, timerange (t0, Inf)), true); %! assert_equal (withinrange (TT, timerange (t0 + hours (1), Inf)), false); %! assert_equal (containsrange (TT, timerange (t0, Inf)), false); ## Test input validation for the range predicates %!error ... %! containsrange (timetable ((1:3)', 'TimeStep', hours (1))); %!error ... %! overlapsrange (timetable ((1:3)', 'TimeStep', hours (1))); %!error ... %! withinrange (timetable ((1:3)', 'TimeStep', hours (1))); %!error ... %! containsrange (timetable ((1:3)', 'TimeStep', hours (1)), 3); %!error ... %! overlapsrange (timetable ((1:3)', 'TimeStep', hours (1)), {1}); %!error ... %! withinrange (timetable ((1:3)', 'TimeStep', hours (1)), ... %! datetime (2024, 1, 1) + hours (0:1)'); %!error ... %! containsrange (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), hours (2)); %!error ... %! containsrange (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), ... %! timetable ((1:3)', 'RowTimes', hours (0:2)')); %!error ... %! containsrange (timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:2)'), ... %! timerange (hours (1), hours (2))); ################################################################################ ## ** Apply functions ** ## ################################################################################ ## Test an ungrouped 'varfun' reduces to one row at the first row time %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3], 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'A'}); %! B = varfun (@mean, TT); %! assert_equal (class (B), 'timetable'); %! assert_equal (size (B), [1, 1]); %! assert_equal (B.Properties.VariableNames, {'mean_A'}); %! assert_equal (B.mean_A, 2); %! assert_equal (B.Properties.RowTimes, t0); ## A step the timetable was told at construction outlives a reduction to one ## row, which is too short to imply one of its own, exactly as a subset of ## that length does. Two rows or more say what their own step is, so a ## grouped apply reports the spacing of the groups. Measured on R2024a. %!test %! R2 = timetable ((1:6)', 'TimeStep', hours (1)); %! B = varfun (@mean, R2); %! assert_equal (height (B), 1); %! assert_equal (B.Properties.TimeStep, hours (1)); %! assert_equal (isregular (B), true); %!test %! R3 = timetable ((1:6)', 'SampleRate', 2); %! B = varfun (@mean, R3); %! assert_equal (B.Properties.SampleRate, 2); %! assert_equal (B.Properties.TimeStep, seconds (0.5)); %! assert_equal (isregular (B), true); %!test %! CM = timetable ((1:6)', 'TimeStep', calmonths (1), ... %! 'StartTime', datetime (2024, 6, 1)); %! B = varfun (@mean, CM); %! assert_equal (B.Properties.TimeStep, calmonths (1)); %! assert_equal (isregular (B), false); %!test %! assert_equal (varfun (@sum, timetable ((1:6)', ... %! 'TimeStep', hours (1))).Properties.TimeStep, hours (1)); ## Test a step only the row times implied does not outlive the reduction %!test %! R1 = timetable ((1:6)', 'RowTimes', ... %! datetime (2024, 6, 1) + hours (0:5)'); %! B = varfun (@mean, R1); %! assert_equal (isnan (B.Properties.TimeStep), true); %! assert_equal (isregular (B), false); ## Test a grouped apply reports the spacing of the groups it made %!test %! GD = timetable ([1; 1; 2; 2; 3; 3], (1:6)', 'TimeStep', hours (1), ... %! 'VariableNames', {'g', 'v'}); %! B = varfun (@mean, GD, 'GroupingVariables', 'g'); %! assert_equal (height (B), 3); %! assert_equal (B.Properties.TimeStep, hours (2)); %! assert_equal (isregular (B), true); ## Test a subset of every other row does the same %!test %! R2 = timetable ((1:6)', 'TimeStep', hours (1)); %! assert_equal (R2([1, 3, 5], :).Properties.TimeStep, hours (2)); ## Test a function returning several rows takes that many row times from the top %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:2)'; %! TT = timetable ([1; 2; 3], 'RowTimes', rt, 'VariableNames', {'A'}); %! B = varfun (@(x) [min(x); max(x)], TT); %! assert_equal (size (B), [2, 1]); %! assert_equal (B.Fun_A, [1; 3]); %! assert_equal (B.Properties.RowTimes, rt(1:2)); ## Test a timetable answers to its own name as an 'OutputFormat' %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:2)'; %! TT = timetable ([1; 2; 3], 'RowTimes', rt, 'VariableNames', {'A'}); %! B = varfun (@mean, TT, 'OutputFormat', 'timetable'); %! assert_equal (class (B), 'timetable'); %! assert_equal (B.mean_A, 2); %! C = rowfun (@(a) a, TT, 'OutputFormat', 'timetable'); %! assert_equal (class (C), 'timetable'); %! assert_equal (C.Properties.RowTimes, rt); ## Test grouping by a variable gives each group the row time of its first row %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A', 'g'}); %! B = varfun (@mean, TT, 'GroupingVariables', 'g', 'InputVariables', 'A'); %! assert_equal (B.Properties.VariableNames, {'g', 'GroupCount', 'mean_A'}); %! assert_equal (B.GroupCount, [2; 2]); %! assert_equal (B.mean_A, [2; 3]); %! assert_equal (B.Properties.RowTimes, [t0; t0 + hours(1)]); ## Test grouping by the row dimension name groups by the row times themselves %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A', 'g'}); %! B = varfun (@mean, TT, 'GroupingVariables', 'Time', 'InputVariables', 'A'); %! assert_equal (B.Properties.VariableNames, {'GroupCount', 'mean_A'}); %! assert_equal (B.GroupCount, [1; 1; 1; 1]); %! assert_equal (B.mean_A, [1; 2; 3; 4]); %! assert_equal (B.Properties.RowTimes, t0 + hours (0:3)'); ## Test duplicate row times group together under the row dimension name %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3], 'RowTimes', t0 + hours ([0 1 1])', ... %! 'VariableNames', {'A'}); %! B = varfun (@sum, TT, 'GroupingVariables', 'Time'); %! assert_equal (B.GroupCount, [1; 2]); %! assert_equal (B.sum_A, [1; 5]); %! assert_equal (B.Properties.RowTimes, [t0; t0 + hours(1)]); ## Test 'InputVariables' selects what the function is applied to %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], [10; 20], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'B'}); %! B = varfun (@sum, TT, 'InputVariables', 'B'); %! assert_equal (B.Properties.VariableNames, {'sum_B'}); %! assert_equal (B.sum_B, 30); ## Test the non-table output formats return arrays rather than timetables %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], [10; 20], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'B'}); %! assert_equal (varfun (@mean, TT, 'OutputFormat', 'uniform'), [1.5, 15]); %! assert_equal (varfun (@sum, TT, 'OutputFormat', 'cell'), {3, 30}); ## Test 'ErrorHandler' stands in for a function that fails %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A'}); %! B = varfun (@(x) error ('boom'), TT, 'ErrorHandler', @(s, varargin) NaN); %! assert_equal (B.Fun_A, NaN); ## Test a failure names the function and the variable it was applied to %!error ... %! TT = timetable ([1; 2], string ({'p'; 'q'}), 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)', ... %! 'VariableNames', {'num', 'str'}); %! varfun (@mean, TT); ## Test the option validation is the timetable's own %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! varfun (1, timetable ([1; 2], 'RowTimes', rt)); %!error ... %! varfun (timetable ([1; 2], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)')); %!error ... %! varfun (@sum, timetable ([1; 2], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)'), 'Bogus', 1); ## 'OutputFormat' asked of a timetable answers with a timetable under ## 'auto' and under its own name, and with a plain table under 'table', ## which is how a result is asked for without row times. Measured on ## MATLAB R2024a. %!test %! TT = timetable ((1:4)', 'RowTimes', datetime (2024, 6, 1) + hours (0:3)'); %! assert_equal (class (varfun (@mean, TT)), 'timetable'); %! assert_equal (class (varfun (@mean, TT, 'OutputFormat', 'auto')), ... %! 'timetable'); %! assert_equal (class (varfun (@mean, TT, 'OutputFormat', 'timetable')), ... %! 'timetable'); %!test %! TT = timetable ((1:4)', 'RowTimes', datetime (2024, 6, 1) + hours (0:3)'); %! B = varfun (@mean, TT, 'OutputFormat', 'table'); %! assert_equal (class (B), 'table'); %! assert_equal (size (B), [1, 1]); %! assert_equal (B.Properties.VariableNames, {'mean_Var1'}); %!test %! TT = timetable ((1:4)', 'RowTimes', datetime (2024, 6, 1) + hours (0:3)'); %! B = rowfun (@(a) a, TT, 'OutputFormat', 'table'); %! assert_equal (class (B), 'table'); %! assert_equal (size (B), [4, 1]); ## Test a grouped apply answers the same way %!test %! GG = timetable ([1; 1; 2; 2; 3; 3], (1:6)', 'RowTimes', ... %! datetime (2024, 6, 1) + hours (0:5)', ... %! 'VariableNames', {'g', 'v'}); %! B = rowfun (@(a) a, GG, 'GroupingVariables', 'g', 'OutputFormat', 'table'); %! assert_equal (class (B), 'table'); %! assert_equal (height (B), 6); %! assert_equal (B.Properties.VariableNames, {'g', 'GroupCount', 'Var3'}); %! assert_equal (isempty (B.Properties.RowNames), true); ## Test a table is unaffected, 'table' meaning what 'auto' does there %!test %! T = table ((1:4)', 'VariableNames', {'v'}); %! assert_equal (class (rowfun (@(a) a, T)), 'table'); %! assert_equal (class (rowfun (@(a) a, T, 'OutputFormat', 'table')), 'table'); %! assert_equal (class (varfun (@mean, T, 'OutputFormat', 'table')), 'table'); ## Every row of a timetable is labelled by a row time, and there are only as ## many of those as it has rows, so an apply returning more is refused and ## the result asked for as a table instead. A table labels its rows by no ## such thing and takes the same call. Measured on MATLAB R2024a. %!error ... %! GG = timetable ([1; 1; 2; 2; 3; 3], (1:6)', 'RowTimes', ... %! datetime (2024, 6, 1) + hours (0:5)', ... %! 'VariableNames', {'g', 'v'}); %! rowfun (@(a) [a; a], GG, 'GroupingVariables', 'g'); ## Test a group returning more rows than it holds is refused, though the ## whole result is no longer than the timetable %!error ... %! H = timetable ([1; 2; 2; 2], (1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + hours (0:3)', ... %! 'VariableNames', {'g', 'v'}); %! rowfun (@(a) [a(1); a(1)], H, 'GroupingVariables', 'g'); %!error ... %! varfun (@(x) [x; x], timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + hours (0:3)')); ## Test the same calls answer when the result is asked for as a table %!test %! GG = timetable ([1; 1; 2; 2; 3; 3], (1:6)', 'RowTimes', ... %! datetime (2024, 6, 1) + hours (0:5)', ... %! 'VariableNames', {'g', 'v'}); %! B = rowfun (@(a) [a; a], GG, 'GroupingVariables', 'g', ... %! 'OutputFormat', 'table'); %! assert_equal (class (B), 'table'); %! assert_equal (height (B), 12); %!test %! B = varfun (@(x) [x; x], timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + hours (0:3)'), ... %! 'OutputFormat', 'table'); %! assert_equal (class (B), 'table'); %! assert_equal (height (B), 8); ## Test an ungrouped 'rowfun' answers one row with one row, for either class %!error ... %! rowfun (@(a) [a; a], timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + hours (0:3)')); %!error ... %! rowfun (@(a) [a; a], table ((1:4)', 'VariableNames', {'v'})); ## Test a table takes a grouped apply longer than itself, having no row ## times to run out of %!test %! Tg = table ([1; 1; 2; 2], (1:4)', 'VariableNames', {'g', 'v'}); %! assert_equal (height (rowfun (@(a) [a; a], Tg, ... %! 'GroupingVariables', 'g')), 8); ## Test the shared branches of 'varfun' name the timetable %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! T2 = timetable (rt, [1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! varfun (@(x) x, T2, 'InputVariables', 'n', 'ErrorHandler', 5); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! varfun (@sum, timetable (rt, [1; 2], 'VariableNames', {'g'}), ... %! 'GroupingVariables', 'g'); ## Test an ungrouped 'rowfun' maps row for row and keeps every row time %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:2)'; %! TT = timetable ([1; 2; 3], 'RowTimes', rt, 'VariableNames', {'A'}); %! B = rowfun (@(a) a * 2, TT); %! assert_equal (class (B), 'timetable'); %! assert_equal (B.Properties.VariableNames, {'Var1'}); %! assert_equal (B.Var1, [2; 4; 6]); %! assert_equal (B.Properties.RowTimes, rt); ## Test a grouped 'rowfun' gives each group the row time of its first row %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A', 'g'}); %! B = rowfun (@(a) sum (a), TT, 'GroupingVariables', 'g', ... %! 'InputVariables', 'A'); %! assert_equal (B.Properties.VariableNames, {'g', 'GroupCount', 'Var3'}); %! assert_equal (B.GroupCount, [2; 2]); %! assert_equal (B.Var3, [4; 6]); %! assert_equal (B.Properties.RowTimes, [t0; t0 + hours(1)]); ## Test grouping by the row dimension name groups by the row times %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3], 'RowTimes', t0 + hours ([0 1 1])', ... %! 'VariableNames', {'A'}); %! B = rowfun (@(a) sum (a), TT, 'GroupingVariables', 'Time'); %! assert_equal (B.Properties.VariableNames, {'GroupCount', 'Var2'}); %! assert_equal (B.GroupCount, [1; 2]); %! assert_equal (B.Var2, [1; 5]); %! assert_equal (B.Properties.RowTimes, [t0; t0 + hours(1)]); ## Each row of a grouped apply's result answers for a row of the group it ## came from, taken from the top: a group reduced to one row answers for its ## first, and one that keeps its rows answers for each in turn. Measured on ## MATLAB R2024a. ## Test a grouped 'rowfun' that keeps its rows keeps their row times %!test %! t0 = datetime (2024, 6, 1); %! GG = timetable ([1; 1; 2; 2; 3; 3], (1:6)', ... %! 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'g', 'v'}); %! B = rowfun (@(a) a, GG, 'GroupingVariables', 'g'); %! assert_equal (height (B), 6); %! assert_equal (B.Properties.RowTimes, t0 + hours (0:5)'); %! assert_equal (B.Properties.TimeStep, hours (1)); %! assert_equal (isregular (B), true); %! assert_equal (B.Var3, (1:6)'); ## Test the rows follow the groups where the groups are not in row order %!test %! t0 = datetime (2024, 6, 1); %! H = timetable ([2; 1; 2; 1], (1:4)', 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'g', 'v'}); %! B = rowfun (@(a) a, H, 'GroupingVariables', 'g'); %! assert_equal (height (B), 4); %! assert_equal (B.Properties.RowTimes, ... %! t0 + hours ([1; 3; 0; 2])); %! assert_equal (B.Var3, [2; 4; 1; 3]); ## Test a grouped 'rowfun' that reduces still answers at the group's first row %!test %! t0 = datetime (2024, 6, 1); %! GG = timetable ([1; 1; 2; 2; 3; 3], (1:6)', ... %! 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'g', 'v'}); %! B = rowfun (@(a) sum (a), GG, 'GroupingVariables', 'g'); %! assert_equal (height (B), 3); %! assert_equal (B.Properties.RowTimes, t0 + hours ([0; 2; 4])); ## Test a grouped 'varfun' reduces and answers at each group's first row %!test %! t0 = datetime (2024, 6, 1); %! GG = timetable ([1; 1; 2; 2; 3; 3], (1:6)', ... %! 'RowTimes', t0 + hours (0:5)', ... %! 'VariableNames', {'g', 'v'}); %! B = varfun (@mean, GG, 'GroupingVariables', 'g'); %! assert_equal (height (B), 3); %! assert_equal (B.Properties.RowTimes, t0 + hours ([0; 2; 4])); ## Test 'NumOutputs' and 'OutputVariableNames' name the result %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:1)'; %! TT = timetable ([1; 2], [3; 4], 'RowTimes', rt, ... %! 'VariableNames', {'A', 'B'}); %! B = rowfun (@(a, b) deal (a + b, a - b), TT, 'NumOutputs', 2); %! assert_equal (B.Properties.VariableNames, {'Var1', 'Var2'}); %! assert_equal (B.Var1, [4; 6]); %! assert_equal (B.Var2, [-2; -2]); %! C = rowfun (@(a, b) a + b, TT, 'OutputVariableNames', {'sum'}); %! assert_equal (C.Properties.VariableNames, {'sum'}); %! assert_equal (C.Properties.RowTimes, rt); ## Test 'SeparateInputs' false hands the row over as one argument %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], [3; 4], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'B'}); %! B = rowfun (@(x) sum (x), TT, 'SeparateInputs', false); %! assert_equal (B.Var1, [4; 6]); %!test %! TT = timetable (hours ([1; 2]), [1; 2], hours ([1; 2])); %! v = rowfun (@(x) {x}, TT, 'SeparateInputs', false, 'OutputFormat', 'cell'); %! assert_equal (v{2}, {hours([48, 2])}); %!error ... %! rowfun (@(x) x, timetable (hours (1), 1, {'a'}), 'SeparateInputs', false) ## Test 'ExtractCellContents' unwraps a cell variable %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], {'pp'; 'qqq'}, 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'B'}); %! B = rowfun (@(a, b) numel (b), TT, 'ExtractCellContents', true); %! assert_equal (B.Var1, [2; 3]); %! C = rowfun (@(a, b) numel (b), TT); %! assert_equal (C.Var1, [1; 1]); ## Test the non-table output formats return arrays rather than timetables %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], [3; 4], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'B'}); %! assert_equal (rowfun (@(a, b) a + b, TT, 'OutputFormat', 'uniform'), ... %! [4; 6]); %! assert_equal (rowfun (@(a, b) a + b, TT, 'OutputFormat', 'cell'), ... %! {4; 6}); ## Test the option validation is the timetable's own %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! rowfun (1, timetable ([1; 2], 'RowTimes', rt)); %!error ... %! rowfun (timetable ([1; 2], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)')); %!error ... %! rowfun (@(x) x, timetable ([1; 2], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)'), 'Bogus', 1); ## Test the shared branches of 'rowfun' name the timetable %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! T2 = timetable (rt, [1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! rowfun (@(x) x, T2, 'SeparateInputs', 'yes'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! T2 = timetable (rt, [1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! rowfun (@(x) x, T2, 'InputVariables', 'n', ... %! 'ExtractCellContents', [1, 2]); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! T2 = timetable (rt, [1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! rowfun (@(x) x, T2, 'InputVariables', 'n', 'ErrorHandler', 5); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! rowfun (@sum, timetable (rt, [1; 2], 'VariableNames', {'g'}), ... %! 'GroupingVariables', 'g'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! T2 = timetable (rt, [1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! rowfun (@(x) x, T2, 'InputVariables', 'n', 'NumOutputs', -1); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! T2 = timetable (rt, [1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! rowfun (@(x) x, T2, 'InputVariables', 'n', ... %! 'OutputVariableNames', {'p', 'q'}, 'NumOutputs', 1); ## Test 'grouptransform' keeps every row, its order and its row times %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:3)'; %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', rt, 'VariableNames', {'A', 'g'}); %! G = grouptransform (TT, 'g', 'zscore', 'A'); %! assert_equal (class (G), 'timetable'); %! assert_equal (G.Properties.VariableNames, {'A', 'g'}); %! assert_equal (G.Properties.RowTimes, rt); %! assert_equal (G.A, [-1; -1; 1; 1] / sqrt (2)); ## Test 'ReplaceValues' false appends the result under its own name %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A', 'g'}); %! G = grouptransform (TT, 'g', 'zscore', 'A', 'ReplaceValues', false); %! assert_equal (G.Properties.VariableNames, {'A', 'g', 'zscore_A'}); %! assert_equal (G.A, [1; 2; 3; 4]); ## Test grouping by the row dimension name groups by the row times %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours ([0 1 1 3])'; %! TT = timetable ([1; 2; 4; 5], 'RowTimes', rt, 'VariableNames', {'A'}); %! G = grouptransform (TT, 'Time', 'meancenter'); %! assert_equal (G.Properties.RowTimes, rt); %! assert_equal (G.A, [0; -1; 1; 0]); ## Test a function handle is applied group by group %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A', 'g'}); %! G = grouptransform (TT, 'g', @(x) x - 1, 'A'); %! assert_equal (G.A, [0; 1; 2; 3]); ## Test 'meanfill' fills a gap from the group it belongs to %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; NaN; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A', 'g'}); %! G = grouptransform (TT, 'g', 'meanfill', 'A'); %! assert_equal (G.A, [1; 2; 1; 4]); ## Test the option validation is the timetable's own %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! grouptransform (timetable ([1; 2], 'RowTimes', rt), 'Time', 'bogus'); %!error ... %! grouptransform (timetable ([1; 2], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)'), 'Time'); ## Test the shared branches of 'grouptransform' name the timetable %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tg = timetable (rt, [1; 2], [3; 4], 'VariableNames', {'g', 'x'}); %! grouptransform (Tg, 'g', 'none'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tg = timetable (rt, [1; 2], [3; 4], 'VariableNames', {'g', 'x'}); %! grouptransform (Tg, 'g', 'zscore', 'x', 5); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tg = timetable (rt, [1; 2], [3; 4], 'VariableNames', {'g', 'x'}); %! grouptransform (Tg, [false, false], 'zscore'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! grouptransform (timetable (rt, [1; 2], {'a'; 'b'}, ... %! 'VariableNames', {'g', 'x'}), 'g', 'zscore'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tf = timetable (rt, [1; 1], [3; 4], 'VariableNames', {'g', 'x'}); %! grouptransform (Tf, 'g', @(x) {1; 2}); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tf = timetable (rt, [1; 1], [3; 4], 'VariableNames', {'g', 'x'}); %! grouptransform (Tf, 'g', @(x) [1; 2; 3]); ## Test 'groupcounts' returns a table, the counts describing groups not rows %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A', 'g'}); %! G = groupcounts (TT, 'g'); %! assert_equal (class (G), 'table'); %! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount', 'Percent'}); %! assert_equal (G.GroupCount, [2; 2]); %! assert_equal (G.Percent, [50; 50]); ## Test grouping by the row dimension name counts the row times %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3], 'RowTimes', t0 + hours ([0 1 1])', ... %! 'VariableNames', {'A'}); %! G = groupcounts (TT, 'Time'); %! assert_equal (G.Properties.VariableNames, {'Time', 'GroupCount', ... %! 'Percent'}); %! assert_equal (G.GroupCount, [1; 2]); %! assert_equal (G.Time, [t0; t0 + hours(1)]); ## Test 'IncludeMissingGroups' false drops the group of missing values %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3], categorical ({'a'; ''; 'a'}), ... %! 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'A', 'g'}); %! assert_equal (height (groupcounts (TT, 'g')), 2); %! G = groupcounts (TT, 'g', 'IncludeMissingGroups', false); %! assert_equal (height (G), 1); %! assert_equal (G.GroupCount, 2); ## Test 'IncludeEmptyGroups' true reports an unused category %!test %! t0 = datetime (2024, 1, 1); %! g = categorical ({'hi'; 'lo'}, {'lo', 'mid', 'hi'}, 'Ordinal', true); %! TT = timetable ([1; 2], g, 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'g'}); %! G = groupcounts (TT, 'g', 'IncludeEmptyGroups', true); %! assert_equal (height (G), 3); %! assert_equal (G.GroupCount, [1; 0; 1]); ## Test the option validation is the timetable's own %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! TT = timetable ([1; 2], 'RowTimes', rt, 'VariableNames', {'A'}); %! groupcounts (TT, 'Time', 'IncludeEmptyGroups', 'yes'); %!error ... %! groupcounts (timetable ([1; 2], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)')); ## A grouping variable binned by a width gets one bin per width across the ## span it covers, and the last of them is closed, so the largest value falls ## in it rather than in a bin of its own past the end. Measured on R2026a. %!test %! T = timetable ((1:6)', 'RowTimes', hours ([0; 0.5; 1; 25; 26; 200])); %! R = groupcounts (T, 'Time', hours (2)); %! assert_equal (height (R), 4); %! assert_equal (numel (categories (R.(R.Properties.VariableNames{1}))), 100); %!test %! T = timetable ((1:6)', 'RowTimes', hours ([0; 0.5; 1; 25; 26; 200])); %! R = groupcounts (T, 'Time', hours (2), 'IncludeEmptyGroups', true); %! assert_equal (height (R), 100); ## Test the value sitting on the top edge is binned rather than left out %!test %! T = timetable ((1:6)', 'RowTimes', hours ([0; 0.5; 1; 25; 26; 200])); %! R = groupcounts (T, 'Time', hours (2), 'IncludeEmptyGroups', true); %! assert_equal (any (ismissing (R.(R.Properties.VariableNames{1}))), false); %! assert_equal (sum (R.GroupCount), 6); ## Test the time-unit path over datetime row times is unchanged %!test %! T = timetable ((1:6)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours ([0; 0.5; 1; 25; 26; 200])); %! R = groupcounts (T, 'Time', 'hour'); %! assert_equal (height (R), 5); %! assert_equal (numel (categories (R.(R.Properties.VariableNames{1}))), 201); ## A duration is binned by the time units that have a fixed length of their ## own, which is to say those a duration can be built from; a week, a month ## and a quarter have none. Measured on MATLAB R2024a. %!test %! T = timetable ((1:6)', 'RowTimes', hours ([0; 0.5; 1; 25; 26; 200])); %! R = groupcounts (T, 'Time', 'hour'); %! assert_equal (class (R), 'table'); %! assert_equal (height (R), 5); %! assert_equal (R.Properties.VariableNames{1}, 'hour_Time'); %! assert_equal (numel (categories (R.hour_Time)), 200); %! assert_equal (R.GroupCount, [2; 1; 1; 1; 1]); %!test %! T = timetable ((1:6)', 'RowTimes', hours ([0; 0.5; 1; 25; 26; 200])); %! assert_equal (height (groupcounts (T, 'Time', 'day')), 3); %! assert_equal (height (groupcounts (T, 'Time', 'year')), 1); ## Test the bins are named for the row times' own unit, the last one closed %!test %! T = timetable ((1:6)', 'RowTimes', hours ([0; 0.5; 1; 25; 26; 200])); %! R = groupsummary (T, 'Time', 'hour', 'mean', 'Var1'); %! assert_equal (cellstr (R.hour_Time)', ... %! {'[0 hr, 1 hr)', '[1 hr, 2 hr)', '[25 hr, 26 hr)', ... %! '[26 hr, 27 hr)', '[199 hr, 200 hr]'}); ## Test a bin fine enough for thousands of categories counts only occupied bins %!test %! T = timetable ((1:6)', 'RowTimes', hours ([0; 0.5; 1; 25; 26; 200])); %! R = groupcounts (T, 'Time', 'minute'); %! assert_equal (height (R), 6); %! assert_equal (numel (categories (R.minute_Time)), 12000); ## Test the other apply families reach the same binning %!test %! T = timetable ((1:6)', 'RowTimes', hours ([0; 0.5; 1; 25; 26; 200])); %! assert_equal (height (groupsummary (T, 'Time', 'hour', 'mean', 'Var1')), 5); %! assert_equal (height (grouptransform (T, 'Time', 'hour', 'zscore')), 6); ## Test the units a duration cannot be built from are refused %!error ... %! groupcounts (timetable ((1:3)', 'RowTimes', hours ([0; 1; 2])), ... %! 'Time', 'week'); %!error ... %! groupcounts (timetable ((1:3)', 'RowTimes', hours ([0; 1; 2])), ... %! 'Time', 'month'); ## Test a bin too narrow for the span is refused rather than truncated %!error ... %! groupcounts (timetable ((1:6)', 'RowTimes', ... %! hours ([0; 0.5; 1; 25; 26; 200])), 'Time', 'second'); ## Test a datetime is still binned by every unit %!test %! T = timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + calmonths ((0:2)')); %! assert_equal (height (groupcounts (T, 'Time', 'month')), 3); %! assert_equal (height (groupcounts (T, 'Time', 'week')), 3); ## Test the shared branches of 'groupcounts' name the timetable %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tg = timetable (rt, [1; 2], [3; 4], 'VariableNames', {'g', 'x'}); %! groupcounts (Tg, 'g', 'bogus'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tg = timetable (rt, [1; 2], [3; 4], 'VariableNames', {'g', 'x'}); %! groupcounts (Tg, 'g', 'IncludeMissingGroups', 'yes'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tg = timetable (rt, [1; 2], [3; 4], 'VariableNames', {'g', 'x'}); %! groupcounts (Tg, [false, false]); ## Test 'groupsummary' returns a table, the summary describing groups %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A', 'g'}); %! G = groupsummary (TT, 'g', 'mean', 'A'); %! assert_equal (class (G), 'table'); %! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount', 'mean_A'}); %! assert_equal (G.GroupCount, [2; 2]); %! assert_equal (G.mean_A, [2; 3]); ## Test the counts alone are reported when no method is asked for %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A', 'g'}); %! G = groupsummary (TT, 'g'); %! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount'}); ## Test grouping by the row dimension name groups by the row times %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3], 'RowTimes', t0 + hours ([0 1 1])', ... %! 'VariableNames', {'A'}); %! G = groupsummary (TT, 'Time', 'sum', 'A'); %! assert_equal (G.Properties.VariableNames, {'Time', 'GroupCount', 'sum_A'}); %! assert_equal (G.GroupCount, [1; 2]); %! assert_equal (G.sum_A, [1; 5]); ## Test a time unit bins the row times before grouping %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A', 'g'}); %! G = groupsummary (TT, 'Time', 'hour', 'mean', 'A'); %! assert_equal (G.Properties.VariableNames, {'hour_Time', 'GroupCount', ... %! 'mean_A'}); %! assert_equal (G.GroupCount, [1; 1; 1; 1]); %! assert_equal (G.mean_A, [1; 2; 3; 4]); ## Test several methods are reported in the order they were asked for %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A', 'g'}); %! G = groupsummary (TT, 'g', {'mean', 'max'}, 'A'); %! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount', 'mean_A', ... %! 'max_A'}); %! assert_equal (G.max_A, [3; 4]); ## Test the method validation is the timetable's own %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! TT = timetable ([1; 2], 'RowTimes', rt, 'VariableNames', {'A'}); %! groupsummary (TT, 'Time', 'bogus', 'A'); %!error ... %! groupsummary (timetable ([1; 2], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)')); ## Test the shared branches of 'groupsummary' name the timetable %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tg = timetable (rt, [1; 2], [3; 4], 'VariableNames', {'g', 'x'}); %! groupsummary (Tg, 'g', 'sum', 'x', 5); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tg = timetable (rt, [1; 2], [3; 4], 'VariableNames', {'g', 'x'}); %! groupsummary (Tg, 'g', 'sum', 'x', 'IncludeMissingGroups', 'yes'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tg = timetable (rt, [1; 2], [3; 4], 'VariableNames', {'g', 'x'}); %! groupsummary (Tg, [false, false]); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tg = timetable (rt, [1; 2], [3; 4], 'VariableNames', {'g', 'x'}); %! groupsummary (Tg, 'g', '', 'x'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tg = timetable (rt, [1; 2], [3; 4], 'VariableNames', {'g', 'x'}); %! groupsummary (Tg, 'g', {''}, 'x'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tg = timetable (rt, [1; 2], [3; 4], 'VariableNames', {'g', 'x'}); %! groupsummary (Tg, 'g', {string('')}, 'x'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! groupsummary (timetable (rt, categorical ({'a'; 'a'}), int8 ([1; 2]), ... %! 'VariableNames', {'g', 'A'}), 'g', 'm', 'A'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! groupsummary (timetable (rt, categorical ({'a'; 'a'}), int8 ([1; 2]), ... %! 'VariableNames', {'g', 'A'}), 'g', 'std', 'A'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! groupsummary (timetable (rt, categorical ({'a'; 'a'}), {'p'; 'q'}, ... %! 'VariableNames', {'g', 'x'}), 'g', 'mean', 'x'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:2)'; %! groupsummary (timetable (rt, [1; 1; 2], [1; 2; 3], ... %! 'VariableNames', {'g', 'v'}), 'g', @(x) [1; 2], 'v'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:2)'; %! groupsummary (timetable (rt, [1; 1; 2], [1; 2; 3], ... %! 'VariableNames', {'g', 'v'}), 'g', @(x) 1:numel (x), 'v'); ## Test 'groupfilter' keeps the rows it accepts, in order, with their times %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:3)'; %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', rt, 'VariableNames', {'A', 'g'}); %! G = groupfilter (TT, 'g', @(x) numel (x) > 1); %! assert_equal (class (G), 'timetable'); %! assert_equal (G.Properties.VariableNames, {'A', 'g'}); %! assert_equal (G.A, [1; 2; 3; 4]); %! assert_equal (G.Properties.RowTimes, rt); ## Test a group that fails the test takes its rows with it %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:4)'; %! TT = timetable ([1; 2; 3; 4; 5], ... %! categorical ({'a'; 'b'; 'a'; 'b'; 'c'}), ... %! 'RowTimes', rt, 'VariableNames', {'A', 'g'}); %! G = groupfilter (TT, 'g', @(x) numel (x) > 1); %! assert_equal (G.A, [1; 2; 3; 4]); %! assert_equal (G.Properties.RowTimes, rt(1:4)); ## Test the filter may be applied to a named data variable %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], [10; 20; 30; 40], ... %! categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A', 'B', 'g'}); %! G = groupfilter (TT, 'g', @(x) mean (x) > 25, 'B'); %! assert_equal (G.B, [20; 40]); ## Test grouping by the row dimension name groups by the row times %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours ([0 1 1])'; %! TT = timetable ([1; 2; 3], 'RowTimes', rt, 'VariableNames', {'A'}); %! G = groupfilter (TT, 'Time', @(x) numel (x) > 1); %! assert_equal (G.A, [2; 3]); %! assert_equal (G.Properties.RowTimes, rt(2:3)); ## Test a filter that keeps nothing still answers with a timetable %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], categorical ({'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'g'}); %! G = groupfilter (TT, 'g', @(x) numel (x) > 99); %! assert_equal (class (G), 'timetable'); %! assert_equal (size (G), [0, 2]); ## Test the validation is the timetable's own %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! TT = timetable ([1; 2], 'RowTimes', rt, 'VariableNames', {'A'}); %! groupfilter (TT, 'Time', 'nope'); %!error ... %! groupfilter (timetable ([1; 2], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)'), 'Time'); ## Test the shared branches of 'groupfilter' name the timetable %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tg = timetable (rt, [1; 2], [3; 4], 'VariableNames', {'g', 'x'}); %! groupfilter (Tg, 'g', 'none'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tg = timetable (rt, [1; 2], [3; 4], 'VariableNames', {'g', 'x'}); %! groupfilter (Tg, 'g', @(x) x > 0, 'x', 5); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tg = timetable (rt, [1; 2], [3; 4], 'VariableNames', {'g', 'x'}); %! groupfilter (Tg, [false, false], @(x) x > 0); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tf = timetable (rt, [1; 1], [3; 4], 'VariableNames', {'g', 'x'}); %! groupfilter (Tf, 'g', @(x) {true}); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Tf = timetable (rt, [1; 1], [3; 4], 'VariableNames', {'g', 'x'}); %! groupfilter (Tf, 'g', @(x) [true; true; true]); ## Test 'stack' repeats each row time once per stacked variable %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:1)'; %! TT = timetable ([1; 2], [3; 4], 'RowTimes', rt, ... %! 'VariableNames', {'A', 'B'}); %! S = stack (TT, {'A', 'B'}); %! assert_equal (class (S), 'timetable'); %! assert_equal (S.Properties.VariableNames, {'A_B_Indicator', 'A_B'}); %! assert_equal (S.A_B, [1; 3; 2; 4]); %! assert_equal (S.Properties.RowTimes, repelem (rt, 2, 1)); ## Test one stacked variable leaves the row times as they were %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:2)'; %! TT = timetable ([1; 2; 3], 'RowTimes', rt, 'VariableNames', {'A'}); %! S = stack (TT, {'A'}); %! assert_equal (S.Properties.VariableNames, {'A_Indicator', 'A'}); %! assert_equal (S.A, [1; 2; 3]); %! assert_equal (S.Properties.RowTimes, rt); ## Test a constant variable is carried along, repeated with its row %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:1)'; %! TT = timetable (categorical ({'x'; 'y'}), [1; 2], [3; 4], ... %! 'RowTimes', rt, 'VariableNames', {'g', 'A', 'B'}); %! S = stack (TT, {'A', 'B'}, 'ConstantVariables', 'g'); %! assert_equal (S.Properties.VariableNames, {'g', 'A_B_Indicator', 'A_B'}); %! assert_equal (cellstr (S.g), {'x'; 'x'; 'y'; 'y'}); ## Test the indicator names which variable each value came from %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], [3; 4], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'B'}); %! S = stack (TT, {'A', 'B'}); %! assert_equal (categories (S.A_B_Indicator), {'A'; 'B'}); %! assert_equal (cellstr (S.A_B_Indicator), {'A'; 'B'; 'A'; 'B'}); ## Test the second output names the row each stacked row came from %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], [3; 4], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'B'}); %! [~, ix] = stack (TT, {'A', 'B'}); %! assert_equal (ix, [1; 1; 2; 2]); ## Test the names may be chosen %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], [3; 4], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'B'}); %! S = stack (TT, {'A', 'B'}, 'NewDataVariableName', 'val', ... %! 'IndexVariableName', 'which'); %! assert_equal (S.Properties.VariableNames, {'which', 'val'}); ## Test the validation is the timetable's own %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! stack (timetable ([1; 2], 'RowTimes', rt)); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! stack (timetable ([1; 2], [3; 4], 'RowTimes', rt), {'Var1', 'Var2'}, ... %! 'Bogus', 1); ## Test the shared branches of 'stack' name the timetable %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! T5 = timetable (rt, {'a'; 'b'; 'c'; 'd'; 'e'}, [38; 43; 38; 40; 49], ... %! [71; 69; 64; 67; 64], [176; 163; 131; 133; 119], ... %! 'VariableNames', {'Name', 'Age', 'Height', 'Weight'}); %! stack (T5, 'Some'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! T5 = timetable (rt, {'a'; 'b'; 'c'; 'd'; 'e'}, [38; 43; 38; 40; 49], ... %! [71; 69; 64; 67; 64], [176; 163; 131; 133; 119], ... %! 'VariableNames', {'Name', 'Age', 'Height', 'Weight'}); %! stack (T5, {{'Height', 'Weight'}, {'Age'}}); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! T5 = timetable (rt, {'a'; 'b'; 'c'; 'd'; 'e'}, [38; 43; 38; 40; 49], ... %! [71; 69; 64; 67; 64], [176; 163; 131; 133; 119], ... %! 'VariableNames', {'Name', 'Age', 'Height', 'Weight'}); %! stack (T5, 2:4, 'ConstantVariables', 'Some'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! T5 = timetable (rt, {'a'; 'b'; 'c'; 'd'; 'e'}, [38; 43; 38; 40; 49], ... %! [71; 69; 64; 67; 64], [176; 163; 131; 133; 119], ... %! 'VariableNames', {'Name', 'Age', 'Height', 'Weight'}); %! stack (T5, 2:4, 'ConstantVariables', 'Weight'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! T5 = timetable (rt, {'a'; 'b'; 'c'; 'd'; 'e'}, [38; 43; 38; 40; 49], ... %! [71; 69; 64; 67; 64], [176; 163; 131; 133; 119], ... %! 'VariableNames', {'Name', 'Age', 'Height', 'Weight'}); %! stack (T5, 2:4, 'NewDataVariableName', 3); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! T5 = timetable (rt, {'a'; 'b'; 'c'; 'd'; 'e'}, [38; 43; 38; 40; 49], ... %! [71; 69; 64; 67; 64], [176; 163; 131; 133; 119], ... %! 'VariableNames', {'Name', 'Age', 'Height', 'Weight'}); %! stack (T5, 2:3, 'NewDataVariableName', {'A', 'B'}); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! T5 = timetable (rt, {'a'; 'b'; 'c'; 'd'; 'e'}, [38; 43; 38; 40; 49], ... %! [71; 69; 64; 67; 64], [176; 163; 131; 133; 119], ... %! 'VariableNames', {'Name', 'Age', 'Height', 'Weight'}); %! stack (T5, 2:4, 'IndexVariableName', 3); ## Test 'rows2vars' names the new variables for the row times %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3], 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'A'}); %! R = rows2vars (TT); %! assert_equal (class (R), 'table'); %! assert_equal (size (R), [1, 4]); %! assert_equal (R.Properties.VariableNames, ... %! {'OriginalVariableNames', 'x01_Jan_202400_00_00', ... %! 'x01_Jan_202401_00_00', 'x01_Jan_202402_00_00'}); %! assert_equal (R.Properties.DimensionNames, {'Row', 'Time'}); %! assert_equal (R.OriginalVariableNames, {'A'}); ## Test the values are transposed into the new variables %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], [3; 4], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'B'}); %! R = rows2vars (TT); %! n = R.Properties.VariableNames; %! assert_equal (R.(n{2}), [1; 3]); %! assert_equal (R.(n{3}), [2; 4]); ## Test 'DataVariables' chooses which variables become rows %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], [3; 4], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'B'}); %! R = rows2vars (TT, 'DataVariables', 'B'); %! assert_equal (R.OriginalVariableNames, {'B'}); %! assert_equal (size (R), [1, 3]); ## Test a variable may name the new variables instead of the row times %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], categorical ({'p'; 'q'}), ... %! 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'g'}); %! R = rows2vars (TT, 'VariableNamesSource', 'g'); %! assert_equal (R.Properties.VariableNames, ... %! {'OriginalVariableNames', 'p', 'q'}); %! assert_equal (R.Properties.DimensionNames, {'Row', 'g'}); ## Test variables of different types come back as cell arrays %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], {'p'; 'q'}, 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'B'}); %! R = rows2vars (TT); %! n = R.Properties.VariableNames; %! assert_equal (class (R.(n{2})), 'cell'); ## Test the validation is the timetable's own %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! rows2vars (timetable ([1 2; 3 4], 'RowTimes', rt)); ## Test the shared branches of 'rows2vars' name the timetable %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! rows2vars (timetable ([1; 2], 'RowTimes', rt), 'Bogus', 1); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! T5 = timetable (rt, {'a'; 'b'; 'c'; 'd'; 'e'}, [38; 43; 38; 40; 49], ... %! [71; 69; 64; 67; 64], [176; 163; 131; 133; 119], ... %! 'VariableNames', {'Name', 'Age', 'Height', 'Weight'}); %! rows2vars (T5, 'VariableNamesSource', {'Name', 'Age'}); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! T5 = timetable (rt, {'a'; 'b'; 'c'; 'd'; 'e'}, [38; 43; 38; 40; 49], ... %! [71; 69; 64; 67; 64], [176; 163; 131; 133; 119], ... %! 'VariableNames', {'Name', 'Age', 'Height', 'Weight'}); %! rows2vars (T5, 'VariableNamesSource', {'Some'}); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! rows2vars (timetable (rt, [1 2; 3 4; 5 6; 7 8; 9 10], ... %! 'VariableNames', {'M'}), 'VariableNamesSource', 'M'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! T5 = timetable (rt, {'a'; 'b'; 'c'; 'd'; 'e'}, [38; 43; 38; 40; 49], ... %! [71; 69; 64; 67; 64], [176; 163; 131; 133; 119], ... %! 'VariableNames', {'Name', 'Age', 'Height', 'Weight'}); %! rows2vars (T5, 'DataVariables', 'Name', 'VariableNamesSource', 'Name'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! T5 = timetable (rt, {'a'; 'b'; 'c'; 'd'; 'e'}, [38; 43; 38; 40; 49], ... %! [71; 69; 64; 67; 64], [176; 163; 131; 133; 119], ... %! 'VariableNames', {'Name', 'Age', 'Height', 'Weight'}); %! rows2vars (T5, 'VariableNamingRule', 'somerule'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! rows2vars (timetable (rt, table ((1:5)'), {'a'; 'b'; 'c'; 'd'; 'e'}, ... %! 'VariableNames', {'N', 'Name'})); ## Test two timetables join on their row times with no key named %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:2)'; %! A = timetable ([1; 2; 3], 'RowTimes', rt, 'VariableNames', {'A'}); %! B = timetable ({'p'; 'q'; 'r'}, 'RowTimes', rt, 'VariableNames', {'B'}); %! C = join (A, B); %! assert_equal (class (C), 'timetable'); %! assert_equal (C.Properties.VariableNames, {'A', 'B'}); %! assert_equal (C.Properties.RowTimes, rt); %! assert_equal (C.B, {'p'; 'q'; 'r'}); ## Test a table joins onto a timetable when the row times are named as a key %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:2)'; %! TT = timetable ([1; 2; 3], 'RowTimes', rt, 'VariableNames', {'A'}); %! T = table (t0 + hours ([2; 0; 1]), {'X'; 'Y'; 'Z'}, ... %! 'VariableNames', {'Time', 'Rv'}); %! C = join (TT, T, 'LeftKeys', 'Time', 'RightKeys', 'Time'); %! assert_equal (class (C), 'timetable'); %! assert_equal (C.Properties.VariableNames, {'A', 'Rv'}); %! assert_equal (C.Properties.RowTimes, rt); %! assert_equal (C.Rv, {'Y'; 'Z'; 'X'}); ## Test the left operand decides the class of the result %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:2)'; %! TT = timetable ([1; 2; 3], 'RowTimes', rt, 'VariableNames', {'A'}); %! T = table (t0 + hours ([2; 0; 1]), {'X'; 'Y'; 'Z'}, ... %! 'VariableNames', {'Time', 'Rv'}); %! C = join (T, TT, 'LeftKeys', 'Time', 'RightKeys', 'Time'); %! assert_equal (class (C), 'table'); %! assert_equal (C.Properties.VariableNames, {'Time', 'Rv', 'A'}); %! assert_equal (C.A, [3; 1; 2]); ## Test the second output names the row of the right operand each row took %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:2)'; %! A = timetable ([1; 2; 3], 'RowTimes', rt, 'VariableNames', {'A'}); %! B = timetable ({'p'; 'q'; 'r'}, 'RowTimes', rt(end:-1:1), ... %! 'VariableNames', {'B'}); %! [~, ix] = join (A, B); %! assert_equal (ix, [3; 2; 1]); ## Test a timetable and a table share no key unless one is named %!error ... %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A'}); %! join (TT, table ([1; 2], 'VariableNames', {'Rv'})); %!error ... %! join (timetable ([1; 2], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)')); ## Test the shared branches of the joins name the timetable %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! K1 = timetable (rt, [1; 2], 'VariableNames', {'K'}); %! K2 = timetable (rt, [1; 2], {'x'; 'y'}, 'VariableNames', {'K', 'R'}); %! join (K1, 5); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! K1 = timetable (rt, [1; 2], 'VariableNames', {'K'}); %! K2 = timetable (rt, [1; 2], {'x'; 'y'}, 'VariableNames', {'K', 'R'}); %! join (K1, K2, 'BadOption', 1); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! K1 = timetable (rt, [1; 2], 'VariableNames', {'K'}); %! K2 = timetable (rt, [1; 2], {'x'; 'y'}, 'VariableNames', {'K', 'R'}); %! join (K1, table ([1; 1; 2], {'x'; 'y'; 'z'}, ... %! 'VariableNames', {'K', 'R'}), 'Keys', 'K'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! join (timetable (rt, [1; 9], 'VariableNames', {'K'}), ... %! table ([1; 2], {'x'; 'y'}, 'VariableNames', {'K', 'R'}), 'Keys', 'K'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! K1 = timetable (rt, [1; 2], 'VariableNames', {'K'}); %! K2 = timetable (rt, [1; 2], {'x'; 'y'}, 'VariableNames', {'K', 'R'}); %! join (K1, K2, 'Keys', 1, 'LeftKeys', 1); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! K1 = timetable (rt, [1; 2], 'VariableNames', {'K'}); %! K2 = timetable (rt, [1; 2], {'x'; 'y'}, 'VariableNames', {'K', 'R'}); %! join (K1, K2, 'LeftKeys', 'K'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! join (timetable (rt, [1; 2], [1; 2], 'VariableNames', {'a', 'b'}), ... %! timetable (rt, [1; 2], 'VariableNames', {'c'}), ... %! 'LeftKeys', {'a', 'b'}, 'RightKeys', 'c'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! K1 = timetable (rt, [1; 2], 'VariableNames', {'K'}); %! K2 = timetable (rt, [1; 2], {'x'; 'y'}, 'VariableNames', {'K', 'R'}); %! innerjoin (K1, 5); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! K1 = timetable (rt, [1; 2], 'VariableNames', {'K'}); %! K2 = timetable (rt, [1; 2], {'x'; 'y'}, 'VariableNames', {'K', 'R'}); %! innerjoin (K1, K2, 'BadOption', 1); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! K1 = timetable (rt, [1; 2], 'VariableNames', {'K'}); %! K2 = timetable (rt, [1; 2], {'x'; 'y'}, 'VariableNames', {'K', 'R'}); %! outerjoin (K1, 5); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! K1 = timetable (rt, [1; 2], 'VariableNames', {'K'}); %! K2 = timetable (rt, [1; 2], {'x'; 'y'}, 'VariableNames', {'K', 'R'}); %! outerjoin (K1, K2, 'BadOption', 1); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! K1 = timetable (rt, [1; 2], 'VariableNames', {'K'}); %! K2 = timetable (rt, [1; 2], {'x'; 'y'}, 'VariableNames', {'K', 'R'}); %! outerjoin (K1, K2, 'MergeKeys', 3); ## Test the shared branches that read the key columns name the timetable %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! C = timetable (rt, {@sin; @cos}, 'VariableNames', {'K'}); %! innerjoin (C, C, 'Keys', 'K'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! K1 = timetable (rt, [1; 2], 'VariableNames', {'K'}); %! K2 = timetable (rt, [1; 2], {'x'; 'y'}, 'VariableNames', {'K', 'R'}); %! innerjoin (K1, timetable (rt, {'1'; '2'}, 'VariableNames', {'K'}), ... %! 'Keys', 'K'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! innerjoin (timetable (rt, [1, 2; 3, 4], 'VariableNames', {'K'}), ... %! timetable (rt, [1, 2, 3; 4, 5, 6], 'VariableNames', {'K'}), ... %! 'Keys', 'K'); ## Test two timetables innerjoin on their row times with no key named %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:2)'; %! A = timetable ([1; 2; 3], 'RowTimes', rt, 'VariableNames', {'A'}); %! B = timetable ([4; 5; 6], categorical ({'x'; 'y'; 'z'}), ... %! 'RowTimes', rt, 'VariableNames', {'A', 'g'}); %! C = innerjoin (A, B); %! assert_equal (class (C), 'timetable'); %! assert_equal (C.Properties.VariableNames, {'A_A', 'A_B', 'g'}); %! assert_equal (C.Properties.RowTimes, rt); %! assert_equal (C.A_A, [1; 2; 3]); %! assert_equal (C.A_B, [4; 5; 6]); ## Test innerjoin keeps only the rows whose keys match %!test %! t0 = datetime (2024, 1, 1); %! A = timetable ([1; 2; 3], 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'A'}); %! B = timetable ([9; 8], 'RowTimes', t0 + hours ([1 3])', ... %! 'VariableNames', {'B'}); %! [C, iL, iR] = innerjoin (A, B); %! assert_equal (size (C), [1, 2]); %! assert_equal (C.Properties.RowTimes, t0 + hours (1)); %! assert_equal (iL, 2); %! assert_equal (iR, 1); ## Test outerjoin keeps the rows that matched nothing %!test %! t0 = datetime (2024, 1, 1); %! A = timetable ([1; 2], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A'}); %! B = timetable ([9; 8], 'RowTimes', t0 + hours ([1 3])', ... %! 'VariableNames', {'B'}); %! C = outerjoin (A, B); %! assert_equal (class (C), 'timetable'); %! assert_equal (size (C), [3, 2]); %! assert_equal (C.A, [1; 2; NaN]); %! assert_equal (C.B, [NaN; 9; 8]); ## Test an unmatched row keyed by the row times carries the time it has %!test %! t0 = datetime (2024, 1, 1); %! A = timetable ([1; 2], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A'}); %! B = timetable ([9; 8], 'RowTimes', t0 + hours ([1 3])', ... %! 'VariableNames', {'B'}); %! C = outerjoin (A, B); %! assert_equal (C.Properties.RowTimes, ... %! [t0; t0 + hours(1); t0 + hours(3)]); %! D = outerjoin (A, B, 'Type', 'right'); %! assert_equal (D.Properties.RowTimes, [t0 + hours(1); t0 + hours(3)]); ## Test an unmatched row keyed by a variable has no time of its own %!test %! t0 = datetime (2024, 1, 1); %! A = timetable ([1; 2], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'K'}); %! B = timetable ([2; 3], 'RowTimes', t0 + hours ([5 6])', ... %! 'VariableNames', {'K'}); %! C = outerjoin (A, B, 'Keys', 'K'); %! rt = C.Properties.RowTimes; %! assert_equal (rt(1:2), [t0; t0 + hours(1)]); %! assert_equal (ismissing (rt(3)), true); ## Test 'Type' keeps the unmatched rows of one side only %!test %! t0 = datetime (2024, 1, 1); %! A = timetable ([1; 2], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A'}); %! B = timetable ([9; 8], 'RowTimes', t0 + hours ([1 3])', ... %! 'VariableNames', {'B'}); %! assert_equal (height (outerjoin (A, B, 'Type', 'left')), 2); %! assert_equal (height (outerjoin (A, B, 'Type', 'right')), 2); ## Test a table may be joined onto a timetable by naming the row times %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:2)'; %! TT = timetable ([1; 2; 3], 'RowTimes', rt, 'VariableNames', {'A'}); %! T = table (t0 + hours ([2; 0; 1]), {'X'; 'Y'; 'Z'}, ... %! 'VariableNames', {'Time', 'Rv'}); %! C = innerjoin (TT, T, 'LeftKeys', 'Time', 'RightKeys', 'Time'); %! assert_equal (class (C), 'timetable'); %! assert_equal (C.Properties.VariableNames, {'A', 'Rv'}); %! assert_equal (C.Rv, {'Y'; 'Z'; 'X'}); ## Test a timetable and a table share no key unless one is named %!error ... %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A'}); %! innerjoin (TT, table ([1; 2], 'VariableNames', {'Rv'})); %!error ... %! innerjoin (timetable ([1; 2], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)')); %!error ... %! t0 = datetime (2024, 1, 1); %! A = timetable ([1; 2], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A'}); %! outerjoin (A, A, 'Type', 'inner'); %!error ... %! outerjoin (timetable ([1; 2], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)')); ## Test a type with no missing value of its own cannot fill an unmatched row %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! A = timetable (rt, [1; 2], 'VariableNames', {'K'}); %! B = timetable (rt, [1; 9], {@sin; @cos}, 'VariableNames', {'K', 'h'}); %! outerjoin (A, B, 'Keys', 'K'); ## and neither can it fill a side that matched nothing at all %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! A = timetable (rt, [1; 2], 'VariableNames', {'K'}); %! C = timetable (rt, [7; 8], {@sin; @cos}, 'VariableNames', {'K', 'h'}); %! outerjoin (A, C, 'Keys', 'K'); ## Test 'inner2outer' turns the nesting inside out and keeps the row times %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:1)'; %! i1 = table ([1; 2], [3; 4], 'VariableNames', {'p', 'q'}); %! i2 = table ([5; 6], [7; 8], 'VariableNames', {'p', 'q'}); %! TT = timetable (i1, i2, 'RowTimes', rt, 'VariableNames', {'i1', 'i2'}); %! R = inner2outer (TT); %! assert_equal (class (R), 'timetable'); %! assert_equal (R.Properties.VariableNames, {'p', 'q'}); %! assert_equal (R.Properties.RowTimes, rt); %! assert_equal (R.p.Properties.VariableNames, {'i1', 'i2'}); %! assert_equal (R.p.i1, [1; 2]); %! assert_equal (R.p.i2, [5; 6]); ## Test turning it inside out twice gives back what it started as %!test %! t0 = datetime (2024, 1, 1); %! i1 = table ([1; 2], [3; 4], 'VariableNames', {'p', 'q'}); %! i2 = table ([5; 6], [7; 8], 'VariableNames', {'p', 'q'}); %! TT = timetable (i1, i2, 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'i1', 'i2'}); %! assert_equal (isequal (inner2outer (inner2outer (TT)), TT), true); ## Test a timetable holding nothing nested comes back as it was %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A'}); %! assert_equal (isequal (inner2outer (TT), TT), true); ## Test the validation is the timetable's own %!error ... %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], table ([3; 4], 'VariableNames', {'A'}), ... %! 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'n'}); %! inner2outer (TT); ## Test 'findgroups' numbers the groups the variables fall into %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A', 'g'}); %! [G, TID] = findgroups (TT(:, {'g'})); %! assert_equal (G, [1; 2; 1; 2]); %! assert_equal (class (TID), 'table'); %! assert_equal (size (TID), [2, 1]); %! assert_equal (cellstr (TID.g), {'a'; 'b'}); ## Test the row times take no part in the grouping %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 1; 2], 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'A'}); %! assert_equal (findgroups (TT), [1; 1; 2]); ## Test a missing value puts its row in no group %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; NaN; 2], 'RowTimes', t0 + hours (0:2)', ... %! 'VariableNames', {'A'}); %! assert_equal (findgroups (TT), [1; NaN; 2]); ## Test the validation is the timetable's own %!error ... %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A'}); %! findgroups (TT(:, [])); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! findgroups (timetable (rt, {{1}; {2}}, 'VariableNames', {'v'})); ## Test 'splitapply' hands each group's rows over one variable at a time %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A', 'g'}); %! G = findgroups (TT(:, {'g'})); %! assert_equal (splitapply (@(a) sum (a), TT(:, {'A'}), G), [4; 6]); %! assert_equal (splitapply (@(a) numel (a), TT(:, {'A'}), G), [2; 2]); ## Test several outputs are each stacked %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'A'}); %! G = [1; 1; 2; 2]; %! [lo, hi] = splitapply (@(a) deal (min (a), max (a)), TT, G); %! assert_equal (lo, [1; 3]); %! assert_equal (hi, [2; 4]); ## Test the validation is the timetable's own %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! splitapply (1, timetable ([1; 2], 'RowTimes', rt), [1; 2]); %!error ... %! splitapply (@mean, timetable ([1; 2], 'RowTimes', ... %! datetime (2024, 1, 1) + hours (0:1)')); ## Test the shared branches of 'splitapply' name the timetable %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! T2 = timetable (rt, [1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! splitapply (@(x) 1, T2, [1; 2; 3]); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! T2 = timetable (rt, [1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! splitapply (@(x) 1, T2, [0; 1]); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! T2 = timetable (rt, [1; 2], {'a'; 'b'}, 'VariableNames', {'n', 'c'}); %! splitapply (@(x) 1, T2, [1; 3]); ## Test 'unstack' groups by the row times when no grouping variable is given %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:3)'; %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', rt, 'VariableNames', {'A', 'g'}); %! U = unstack (TT, 'A', 'g'); %! assert_equal (class (U), 'timetable'); %! assert_equal (U.Properties.VariableNames, {'a', 'b'}); %! assert_equal (U.Properties.RowTimes, rt); %! assert_equal (U.a, [1; NaN; 3; NaN]); %! assert_equal (U.b, [NaN; 2; NaN; 4]); ## Test rows sharing a row time come back as one %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours ([0 0 1 1])'; %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! 'RowTimes', rt, 'VariableNames', {'A', 'g'}); %! U = unstack (TT, 'A', 'g'); %! assert_equal (size (U), [2, 2]); %! assert_equal (U.Properties.RowTimes, [t0; t0 + hours(1)]); %! assert_equal (U.a, [1; 3]); %! assert_equal (U.b, [2; 4]); ## Test an aggregation function settles a group holding several values %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours ([0 0])'; %! TT = timetable ([1; 5], categorical ({'a'; 'a'}), 'RowTimes', rt, ... %! 'VariableNames', {'A', 'g'}); %! assert_equal (unstack (TT, 'A', 'g').a, 6); %! U = unstack (TT, 'A', 'g', 'AggregationFunction', @max); %! assert_equal (U.a, 5); ## Test 'NewDataVariableNames' names the new variables %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], categorical ({'a'; 'b'}), ... %! 'RowTimes', t0 + hours ([0 0])', ... %! 'VariableNames', {'A', 'g'}); %! U = unstack (TT, 'A', 'g', 'NewDataVariableNames', {'c1', 'c2'}); %! assert_equal (U.Properties.VariableNames, {'c1', 'c2'}); ## Test the validation is the timetable's own %!error ... %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], categorical ({'a'; 'b'}), [3; 4], ... %! 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'A', 'g', 'B'}); %! unstack (TT, 'A', {'g', 'B'}); ## Test the shared branches of 'unstack' name the timetable %!error ... %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], categorical ({'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:1)', 'VariableNames', {'A', 'g'}); %! unstack (TT, 'A', 'g', 'Bogus', 1); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! unstack (timetable (rt, table ([38; 43; 38; 40; 49]), ... %! [71; 69; 64; 67; 64], 'VariableNames', {'N', 'Height'}), ... %! 'N', 'Height'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! TU = timetable (rt, [38; 43; 38; 40; 49], logical ([1; 0; 1; 0; 1]), ... %! [71; 69; 64; 67; 64], ... %! [124, 93; 109, 77; 125, 83; 117, 75; 122, 80], ... %! 'VariableNames', {'Age', 'Smoker', 'Height', 'BP'}); %! unstack (TU, 'Height', 'some'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! TU = timetable (rt, [38; 43; 38; 40; 49], logical ([1; 0; 1; 0; 1]), ... %! [71; 69; 64; 67; 64], ... %! [124, 93; 109, 77; 125, 83; 117, 75; 122, 80], ... %! 'VariableNames', {'Age', 'Smoker', 'Height', 'BP'}); %! unstack (TU, 'Height', 'BP'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! TU = timetable (rt, [38; 43; 38; 40; 49], logical ([1; 0; 1; 0; 1]), ... %! [71; 69; 64; 67; 64], ... %! [124, 93; 109, 77; 125, 83; 117, 75; 122, 80], ... %! 'VariableNames', {'Age', 'Smoker', 'Height', 'BP'}); %! unstack (TU, 'Height', 'Height'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! unstack (timetable (rt, {4; 5; 6; 7; 8}, [71; 69; 64; 67; 64], ... %! 'VariableNames', {'C', 'Height'}), 'Height', 'C'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! TU = timetable (rt, [38; 43; 38; 40; 49], logical ([1; 0; 1; 0; 1]), ... %! [71; 69; 64; 67; 64], ... %! [124, 93; 109, 77; 125, 83; 117, 75; 122, 80], ... %! 'VariableNames', {'Age', 'Smoker', 'Height', 'BP'}); %! unstack (TU, 'Height', 'Age', 'ConstantVariables', 'Height'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! TU = timetable (rt, [38; 43; 38; 40; 49], logical ([1; 0; 1; 0; 1]), ... %! [71; 69; 64; 67; 64], ... %! [124, 93; 109, 77; 125, 83; 117, 75; 122, 80], ... %! 'VariableNames', {'Age', 'Smoker', 'Height', 'BP'}); %! unstack (TU, 'Height', 'Age', 'ConstantVariables', 'Age'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! TU = timetable (rt, [38; 43; 38; 40; 49], logical ([1; 0; 1; 0; 1]), ... %! [71; 69; 64; 67; 64], ... %! [124, 93; 109, 77; 125, 83; 117, 75; 122, 80], ... %! 'VariableNames', {'Age', 'Smoker', 'Height', 'BP'}); %! unstack (TU, 'Height', 'Age', 'GroupingVariables', 'some'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! TU = timetable (rt, [38; 43; 38; 40; 49], logical ([1; 0; 1; 0; 1]), ... %! [71; 69; 64; 67; 64], ... %! [124, 93; 109, 77; 125, 83; 117, 75; 122, 80], ... %! 'VariableNames', {'Age', 'Smoker', 'Height', 'BP'}); %! unstack (TU, 'Height', 'Age', 'GroupingVariables', 'Height'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! TU = timetable (rt, [38; 43; 38; 40; 49], logical ([1; 0; 1; 0; 1]), ... %! [71; 69; 64; 67; 64], ... %! [124, 93; 109, 77; 125, 83; 117, 75; 122, 80], ... %! 'VariableNames', {'Age', 'Smoker', 'Height', 'BP'}); %! unstack (TU, 'Height', 'Age', 'GroupingVariables', 'Age'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! TU = timetable (rt, [38; 43; 38; 40; 49], logical ([1; 0; 1; 0; 1]), ... %! [71; 69; 64; 67; 64], ... %! [124, 93; 109, 77; 125, 83; 117, 75; 122, 80], ... %! 'VariableNames', {'Age', 'Smoker', 'Height', 'BP'}); %! unstack (TU, 'BP', 'Height', 'ConstantVariables', 'Age', ... %! 'GroupingVariables', 'Age'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! TU = timetable (rt, [38; 43; 38; 40; 49], logical ([1; 0; 1; 0; 1]), ... %! [71; 69; 64; 67; 64], ... %! [124, 93; 109, 77; 125, 83; 117, 75; 122, 80], ... %! 'VariableNames', {'Age', 'Smoker', 'Height', 'BP'}); %! unstack (TU, 'Height', 'Age', 'NewDataVariableNames', 1:5); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! TU = timetable (rt, [38; 43; 38; 40; 49], logical ([1; 0; 1; 0; 1]), ... %! [71; 69; 64; 67; 64], ... %! [124, 93; 109, 77; 125, 83; 117, 75; 122, 80], ... %! 'VariableNames', {'Age', 'Smoker', 'Height', 'BP'}); %! unstack (TU, 'Height', 'Age', 'NewDataVariableNames', {'A', 'B', 'C'}); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:4)'; %! TU = timetable (rt, [38; 43; 38; 40; 49], logical ([1; 0; 1; 0; 1]), ... %! [71; 69; 64; 67; 64], ... %! [124, 93; 109, 77; 125, 83; 117, 75; 122, 80], ... %! 'VariableNames', {'Age', 'Smoker', 'Height', 'BP'}); %! unstack (TU, 'Height', 'Age', 'AggregationFunction', 1); ## Test 'pivot' returns a table, its rows and columns being groups %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! categorical ({'p'; 'p'; 'q'; 'q'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'v', 'g', 'k'}); %! P = pivot (TT, 'Rows', 'g'); %! assert_equal (class (P), 'table'); %! assert_equal (P.Properties.VariableNames, {'g', 'count'}); %! assert_equal (P.count, [2; 2]); ## Test grouping down the page and across it at once %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2; 3; 4], categorical ({'a'; 'b'; 'a'; 'b'}), ... %! categorical ({'p'; 'p'; 'q'; 'q'}), ... %! 'RowTimes', t0 + hours (0:3)', ... %! 'VariableNames', {'v', 'g', 'k'}); %! P = pivot (TT, 'Rows', 'g', 'Columns', 'k', 'DataVariable', 'v'); %! assert_equal (P.Properties.VariableNames, {'g', 'p', 'q'}); %! assert_equal (P.p, [1; 2]); %! assert_equal (P.q, [3; 4]); ## Test the row dimension name groups down the page by the row times %!test %! t0 = datetime (2024, 1, 1); %! rt = t0 + hours (0:3)'; %! TT = timetable ([1; 2; 3; 4], 'RowTimes', rt, 'VariableNames', {'v'}); %! P = pivot (TT, 'Rows', 'Time', 'DataVariable', 'v'); %! assert_equal (class (P), 'table'); %! assert_equal (P.Properties.VariableNames, {'Time', 'sum_v'}); %! assert_equal (P.Time, rt); %! assert_equal (P.sum_v, [1; 2; 3; 4]); ## Test the row dimension name names the variables when it groups across %!test %! t0 = datetime (2024, 1, 1); %! TT = timetable ([1; 2], categorical ({'a'; 'b'}), ... %! 'RowTimes', t0 + hours (0:1)', ... %! 'VariableNames', {'v', 'g'}); %! P = pivot (TT, 'Rows', 'g', 'Columns', 'Time', 'DataVariable', 'v'); %! assert_equal (P.Properties.VariableNames{1}, 'g'); %! assert_equal (numel (P.Properties.VariableNames), 3); ## Test the validation is the timetable's own %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! pivot (timetable ([1; 2], 'RowTimes', rt, 'VariableNames', {'v'})); ## Test the shared branches of the 'none' method name the timetable %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! P = timetable (rt, categorical ({'a'; 'a'}), categorical ({'x'; 'x'}), ... %! [1; 2], 'VariableNames', {'g', 'h', 'v'}); %! pivot (P, 'Rows', 'g', 'Columns', 'h', 'DataVariable', 'v', ... %! 'Method', 'none'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Q = timetable (rt, categorical ({'a'; 'b'}), categorical ({'x'; 'y'}), ... %! {@sin; @cos}, 'VariableNames', {'g', 'h', 'v'}); %! pivot (Q, 'Rows', 'g', 'Columns', 'h', 'DataVariable', 'v', ... %! 'Method', 'none'); ## Test the shared branches of 'pivot' name the timetable %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! G2 = timetable (rt, categorical ({'a'; 'b'}), [1; 2], ... %! 'VariableNames', {'g', 'v'}); %! pivot (G2, 'Rows', 'g', 'Wibble', 1); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Gn = timetable (rt, [1; 1], [1; 2], 'VariableNames', {'g', 'v'}); %! pivot (Gn, 5); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! G2 = timetable (rt, categorical ({'a'; 'b'}), [1; 2], ... %! 'VariableNames', {'g', 'v'}); %! pivot (G2, 'Rows', 'g', 'OutputFormat', 'bogus'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! G2 = timetable (rt, categorical ({'a'; 'b'}), [1; 2], ... %! 'VariableNames', {'g', 'v'}); %! pivot (G2, 'Rows', 'g', 'RowLabelPlacement', 'foo'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! pivot (timetable (rt, [1; 2], [3; 4], [5; 6], ... %! 'VariableNames', {'g', 'x', 'y'}), 'Rows', 'g', ... %! 'DataVariable', {'x', 'y'}); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! G2 = timetable (rt, categorical ({'a'; 'b'}), [1; 2], ... %! 'VariableNames', {'g', 'v'}); %! pivot (G2, 'Rows', 'g', 'Method', 'bogus'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! G2 = timetable (rt, categorical ({'a'; 'b'}), [1; 2], ... %! 'VariableNames', {'g', 'v'}); %! pivot (G2, 'Rows', 'g', 'Method', 'sum'); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Gn = timetable (rt, [1; 1], [1; 2], 'VariableNames', {'g', 'v'}); %! pivot (Gn, 'Rows', 'g', 'DataVariable', 'v', 'Method', 5); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! G2 = timetable (rt, categorical ({'a'; 'b'}), [1; 2], ... %! 'VariableNames', {'g', 'v'}); %! pivot (G2, 'Rows', 'g', 'DataVariable', 'v', 'Method', 'none', ... %! 'IncludeTotals', true); %!error ... %! rt = datetime (2024, 1, 1) + hours (0:1)'; %! Gn = timetable (rt, [1; 1], [1; 2], 'VariableNames', {'g', 'v'}); %! pivot (Gn, 'Rows', 'g', 'RowsBinMethod', 'bogus'); ################################################################################ ## ** Zoned row times ** ## ################################################################################ ## Every expectation in this section was measured on MATLAB R2024a. The ## fixtures straddle a daylight-saving change in America/New_York: 'sf' the ## spring-forward gap, where 02:00 never happens, and 'fb' the fall-back ## repeat, where 01:00 happens twice. 'pl' is the same zone away from either. ## Test the spring-forward gap is a gap on the wall clock and not in time %!test %! sf = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours (0:4)'; %! assert_equal (hour (sf), [0; 1; 3; 4; 5]); %! assert_equal (seconds (diff (sf)), [3600; 3600; 3600; 3600]); ## Test the fall-back hour repeats on the wall clock and not in time %!test %! fb = datetime (2024, 11, 3, 0, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours (0:4)'; %! assert_equal (hour (fb), [0; 1; 1; 2; 3]); %! assert_equal (seconds (diff (fb)), [3600; 3600; 3600; 3600]); ## Test a timetable over the gap is regular in time and not in days %!test %! sf = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours (0:4)'; %! TT = timetable ((1:5)', 'RowTimes', sf); %! assert_equal (isregular (TT, 'time'), true); %! assert_equal (isregular (TT, 'days'), false); %! assert_equal (TT.Properties.TimeStep, hours (1)); ## Test 'lag' leaves the row times and their zone alone %!test %! pl = datetime (2024, 6, 1, 0, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours (0:4)'; %! L = lag (timetable ((1:5)', 'RowTimes', pl), 1); %! assert_equal (L.Properties.RowTimes, pl); %! assert_equal (L.Properties.RowTimes.TimeZone, 'America/New_York'); %! assert_equal (L.Var1, [NaN; 1; 2; 3; 4]); ## Test 'lag' over the spring-forward gap %!test %! sf = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours (0:4)'; %! L = lag (timetable ((1:5)', 'RowTimes', sf), 1); %! assert_equal (L.Properties.RowTimes, sf); %! assert_equal (L.Var1, [NaN; 1; 2; 3; 4]); ## Test 'lag' over the fall-back repeat %!test %! fb = datetime (2024, 11, 3, 0, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours (0:4)'; %! L = lag (timetable ((1:5)', 'RowTimes', fb), 1); %! assert_equal (L.Properties.RowTimes, fb); %! assert_equal (L.Var1, [NaN; 1; 2; 3; 4]); ## Test 'retime' onto a finer grid holds the previous value %!test %! pl = datetime (2024, 6, 1, 0, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours (0:4)'; %! R = retime (timetable ((1:5)', 'RowTimes', pl), 'regular', 'previous', ... %! 'TimeStep', minutes (30)); %! assert_equal (height (R), 9); %! assert_equal (R.Var1, [1; 1; 2; 2; 3; 3; 4; 4; 5]); %! assert_equal (R.Properties.RowTimes(1), pl(1)); %! assert_equal (R.Properties.RowTimes(9), pl(5)); %! assert_equal (R.Properties.RowTimes.TimeZone, 'America/New_York'); ## Test the rows a finer grid adds are missing where nothing fills them %!test %! pl = datetime (2024, 6, 1, 0, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours (0:4)'; %! R = retime (timetable ((1:5)', 'RowTimes', pl), 'regular', ... %! 'fillwithmissing', 'TimeStep', minutes (30)); %! assert_equal (R.Var1, [1; NaN; 2; NaN; 3; NaN; 4; NaN; 5]); ## A unit measures duration row times only where a duration counts in it, ## which is where there is a duration constructor for the unit. A week, a ## month and a quarter have none; a year is the fixed 365.2425 days that ## 'years' builds. Measured on MATLAB R2024a. %!test %! D = timetable (hours (0:5)', (1:6)'); %! assert_equal (height (retime (D, 'secondly', 'sum')), 18001); %! assert_equal (height (retime (D, 'minutely', 'sum')), 301); %! assert_equal (retime (D, 'hourly', 'sum').Var1', [1, 2, 3, 4, 5, 6]); %!test %! D = timetable (hours (0:5)', (1:6)'); %! R = retime (D, 'daily', 'sum'); %! assert_equal (height (R), 1); %! assert_equal (R.Var1, 21); %!test %! D = timetable (hours (0:5)', (1:6)'); %! R = retime (D, 'yearly', 'sum'); %! assert_equal (height (R), 1); %! assert_equal (R.Var1, 21); %!error ... %! retime (timetable (hours (0:5)', (1:6)'), 'weekly', 'sum'); %!error ... %! retime (timetable (hours (0:5)', (1:6)'), 'quarterly', 'sum'); ## Test a datetime still takes every unit, the calendar being there for it %!test %! T = timetable (datetime (2024, 6, 1) + days ((0:2)'), (1:3)'); %! R = retime (T, 'weekly', 'sum'); %! assert_equal (height (R), 2); %! assert_equal (R.Properties.RowTimes, ... %! [datetime(2024, 5, 26); datetime(2024, 6, 2)]); %! assert_equal (R.Var1', [1, 5]); %!test %! T = timetable (datetime (2024, 6, 1) + calmonths ((0:2)'), (1:3)'); %! assert_equal (height (retime (T, 'monthly', 'sum')), 3); ## Test 'retime' by the hour over the gap keeps the rows it was given %!test %! sf = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours (0:4)'; %! R = retime (timetable ((1:5)', 'RowTimes', sf), 'hourly', 'previous'); %! assert_equal (R.Properties.RowTimes, sf); %! assert_equal (R.Var1, [1; 2; 3; 4; 5]); ## Test 'retime' by the hour over the fall-back repeat keeps both hours %!test %! fb = datetime (2024, 11, 3, 0, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours (0:4)'; %! R = retime (timetable ((1:5)', 'RowTimes', fb), 'hourly', 'previous'); %! assert_equal (R.Properties.RowTimes, fb); %! assert_equal (R.Var1, [1; 2; 3; 4; 5]); ## Test 'synchronize' over a zoned pair keeps the step and the zone %!test %! pl = datetime (2024, 6, 1, 0, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours (0:4)'; %! A = timetable ((1:5)', 'RowTimes', pl, 'VariableNames', {'v'}); %! B = timetable ((6:8)', 'RowTimes', pl([1 3 5]), 'VariableNames', {'W'}); %! S = synchronize (A, B); %! assert_equal (height (S), 5); %! assert_equal (S.Properties.TimeStep, hours (1)); %! assert_equal (isregular (S), true); %! assert_equal (S.Properties.RowTimes.TimeZone, 'America/New_York'); %! assert_equal (S.v, [1; 2; 3; 4; 5]); ## Test 'outerjoin' filling a zoned datetime keeps the zone %!test %! pl = datetime (2024, 6, 1, 'TimeZone', 'America/New_York') + hours (0:2)'; %! L = table ([1; 2; 3], 'VariableNames', {'K'}); %! R = table ([7; 8; 9], pl, 'VariableNames', {'K', 'z'}); %! C = outerjoin (L, R, 'Keys', 'K'); %! assert_equal (C.z.TimeZone, 'America/New_York'); %! assert_equal (height (C), 6); ## Test 'syncevents' answers over a zoned timetable %!test %! pl = datetime (2024, 6, 1, 'TimeZone', 'America/New_York') + hours (0:4)'; %! TT = timetable ((1:5)', 'RowTimes', pl, 'VariableNames', {'v'}); %! TT.Properties.Events = eventtable (pl(2), 'EventLabels', {'up'}); %! S = syncevents (TT); %! assert_equal (height (S), 5); %! assert_equal (S.Properties.RowTimes.TimeZone, 'America/New_York'); ## Test a time range over the gap picks the rows inside it %!test %! ny = 'America/New_York'; %! sf = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', ny) + hours (0:4)'; %! TT = timetable ((1:5)', 'RowTimes', sf); %! tr = timerange (datetime (2024, 3, 10, 1, 0, 0, 'TimeZone', ny), ... %! datetime (2024, 3, 10, 4, 0, 0, 'TimeZone', ny)); %! assert_equal (height (TT(tr,:)), 2); %! assert_equal (TT(tr,:).Properties.RowTimes, sf(2:3)); %! assert_equal (containsrange (TT, tr), true); %! assert_equal (overlapsrange (TT, tr), true); %! assert_equal (withinrange (TT, tr), false); ## Test 'sortrows' and 'unique' read the instants and not the wall clock %!test %! fb = datetime (2024, 11, 3, 0, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours (0:4)'; %! TT = timetable ((1:5)', 'RowTimes', fb); %! assert_equal (sortrows (TT).Properties.RowTimes, fb); %! assert_equal (height (unique (TT)), 5); ## Test stacking two zoned blocks keeps the zone and loses the step %!test %! ny = 'America/New_York'; %! sf = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', ny) + hours (0:4)'; %! fb = datetime (2024, 11, 3, 0, 0, 0, 'TimeZone', ny) + hours (0:4)'; %! A = timetable ((1:5)', 'RowTimes', sf); %! B = timetable ((1:5)', 'RowTimes', fb); %! V = [A; B]; %! assert_equal (height (V), 10); %! assert_equal (V.Properties.RowTimes.TimeZone, ny); %! assert_equal (isregular (V), false); %! assert_equal (isnan (V.Properties.TimeStep), true); ## Duration row times take the same methods a datetime does, and give a ## duration where a datetime gives a datetime. Measured on R2024a. %!test %! D = timetable (hours (0:5)', (1:6)'); %! assert_equal (D.Properties.TimeStep, hours (1)); %! assert_equal (D.Properties.SampleRate, 1/3600); %! assert_equal (isregular (D), true); %! assert_equal (isregular (D, 'time'), true); %! assert_equal (isregular (D, 'days'), false); %! assert_equal (D.Properties.RowTimes.Format, 'h'); %! assert_equal (D.Properties.DimensionNames{1}, 'Time'); ## Test a bare 'TimeStep' or 'SampleRate' gives duration row times %!test %! T = timetable ((1:6)', 'TimeStep', hours (1)); %! assert_equal (class (T.Properties.RowTimes), 'duration'); %! assert_equal (seconds (T.Properties.RowTimes)', [0, 3600, 7200, ... %! 10800, 14400, 18000]); %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! assert_equal (class (R.Properties.RowTimes), 'duration'); %! assert_equal (seconds (R.Properties.RowTimes)', [0, 0.5, 1, 1.5, 2, 2.5]); ## Test the ordering and selecting methods read duration row times %!test %! D = timetable (hours (0:5)', (1:6)'); %! assert_equal (issorted (D), true); %! assert_equal (height (unique (D)), 6); %! assert_equal (seconds (head (D, 2).Properties.RowTimes)', [0, 3600]); %! assert_equal (height (topkrows (D, 2, 'Var1')), 2); %! assert_equal (lag (D, 1).Var1', [NaN, 1, 2, 3, 4, 5]); %!test %! U = timetable (hours ([2; 0; 1]), (1:3)'); %! assert_equal (seconds (sortrows (U).Properties.RowTimes)', [0, 3600, 7200]); ## Test stacking two of them loses the step, as it does for a datetime %!test %! D = timetable (hours (0:5)', (1:6)'); %! V = [D; D]; %! assert_equal (height (V), 12); %! assert_equal (isnan (V.Properties.TimeStep), true); %! assert_equal (V.Properties.RowTimes.Format, 'h'); ## Test a duration time range selects and answers the predicates %!test %! D = timetable (hours (0:5)', (1:6)'); %! R = timerange (hours (1), hours (3)); %! assert_equal (height (D(R, :)), 2); %! assert_equal (seconds (D(R, :).Properties.RowTimes)', [3600, 7200]); %! assert_equal (containsrange (D, R), true); %! assert_equal (overlapsrange (D, R), true); %! assert_equal (withinrange (D, R), false); %! assert_equal (height (D(hours (2), :)), 1); %! assert_equal (height (D(withtol (hours (2.2), minutes (20)), :)), 1); ## Test 'synchronize' and the round trip through a table keep the type %!test %! D = timetable (hours (0:5)', (1:6)'); %! B = timetable (hours (0:5)', (7:12)', 'VariableNames', {'z'}); %! S = synchronize (D, B); %! assert_equal (S.Properties.TimeStep, hours (1)); %! assert_equal (isregular (S), true); %!test %! D = timetable (hours (0:5)', (1:6)'); %! W = synchronize (D, timetable (hours ([0.5; 2.5]), [1; 2], ... %! 'VariableNames', {'w'})); %! assert_equal (seconds (W.Properties.RowTimes)', ... %! [0, 1800, 3600, 7200, 9000, 10800, 14400, 18000]); %!test %! d = hours (0:5)'; %! D = timetable (d, (1:6)'); %! T = timetable2table (D); %! assert_equal (T.Properties.VariableNames, {'d', 'Var1'}); %! assert_equal (class (T.d), 'duration'); %! assert_equal (table2timetable (T).Properties.TimeStep, hours (1)); ################################################################################ ## A zoned timetable is spaced in absolute time, so an hourly grid crossing a ## clock change keeps its one-hour step while the wall clock skips an hour in ## spring and repeats one in autumn. Measured on MATLAB R2024a. ## Test the spring gap shows in the wall clock, not in the spacing %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! assert_equal (hour (t)', [0, 1, 3, 4, 5, 6]); %! assert_equal (diff (posixtime (t))', [3600, 3600, 3600, 3600, 3600]); %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! assert_equal (A.Properties.TimeStep, hours (1)); %! assert_equal (isregular (A, 'time'), true); %! assert_equal (isregular (A, 'days'), false); ## Test the autumn repeat shows the same way %!test %! z = 'America/New_York'; %! t = datetime (2024, 11, 3, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! assert_equal (hour (t)', [0, 1, 1, 2, 3, 4]); %! assert_equal (diff (posixtime (t))', [3600, 3600, 3600, 3600, 3600]); %!test %! z = 'America/New_York'; %! t = datetime (2024, 11, 3, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! assert_equal (A.Properties.TimeStep, hours (1)); %! assert_equal (isregular (A), true); ## Test 'head' and 'tail' carry the row times across the gap unchanged %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! assert_equal (head (A, 3).Properties.RowTimes, t(1:3)); %! assert_equal (tail (A, 3).Properties.RowTimes, t(4:6)); %! assert_equal (head (A, 3).Properties.TimeStep, hours (1)); ## Test descending order reverses the times and the step alike %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! D = sortrows (A, 'Time', 'descend'); %! assert_equal (D.Properties.RowTimes, flipud (t)); %! assert_equal (D.Properties.TimeStep, hours (-1)); %! assert_equal (isregular (D), true); ## Test 'topkrows' takes the largest and reports the step it is left with %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! K = topkrows (A, 2, 'v'); %! assert_equal (K.Properties.RowTimes, [t(6); t(5)]); %! assert_equal (K.v, [6; 5]); ## Test repeating rows repeats their times and gives up the step %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! R = repelem (A, 2, 1); %! assert_equal (height (R), 12); %! assert_equal (R.Properties.RowTimes, repelem (t, 2, 1)); %! assert_equal (isregular (R), false); %! assert_equal (ismissing (R.Properties.TimeStep), true); %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! R = repmat (A, 2, 1); %! assert_equal (R.Properties.RowTimes, [t; t]); %! assert_equal (isregular (R), false); ## Test a row added on the grid keeps the timetable regular %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! B = timetable (7, 'RowTimes', t(6) + hours (1), 'VariableNames', {'v'}); %! V = [A; B]; %! assert_equal (height (V), 7); %! assert_equal (V.Properties.TimeStep, hours (1)); %! assert_equal (isregular (V), true); ## Test 'unique' takes a doubled block back down to its rows %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! U = unique ([A; A]); %! assert_equal (U.Properties.RowTimes, t); %! assert_equal (isregular (U), true); ## Test 'rmmissing' drops the row and the time that labelled it %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! A.v(3) = NaN; %! R = rmmissing (A); %! assert_equal (R.Properties.RowTimes, t([1, 2, 4, 5, 6])); %! assert_equal (isregular (R), false); ## Test 'fillmissing' interpolates against absolute time, not the wall clock %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! A.v(3) = NaN; %! F = fillmissing (A, 'linear'); %! assert_equal (F.v, (1:6)'); %! assert_equal (F.Properties.TimeStep, hours (1)); ## Test 'lag' shifts the values and leaves the times where they were %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! L = lag (A, 1); %! assert_equal (L.Properties.RowTimes, t); %! assert_equal (L.v, [NaN; 1; 2; 3; 4; 5]); %! assert_equal (L.Properties.TimeStep, hours (1)); %!test %! z = 'America/New_York'; %! t = datetime (2024, 11, 3, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! assert_equal (lag (A, 1).v, [NaN; 1; 2; 3; 4; 5]); ## Test an hourly grid follows absolute hours, so the gap adds no row %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! R = retime (A, 'hourly', 'sum'); %! assert_equal (height (R), 6); %! assert_equal (R.Properties.RowTimes, t); ## Test the repeated wall-clock hour adds no row either %!test %! z = 'America/New_York'; %! t = datetime (2024, 11, 3, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! assert_equal (height (retime (A, 'hourly', 'sum')), 6); ## Test a daily grid gathers the whole short day into one row %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! R = retime (A, 'daily', 'sum'); %! assert_equal (height (R), 1); %! assert_equal (R.v, 21); ## Test the long day gathers the same way %!test %! z = 'America/New_York'; %! t = datetime (2024, 11, 3, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! assert_equal (retime (A, 'daily', 'sum').v, 21); ## Test 'synchronize' lays two zoned timetables on the union of their times %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! n = datetime (2024, 11, 3, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! B = timetable ((1:6)', 'RowTimes', n, 'VariableNames', {'w'}); %! S = synchronize (A, B, 'union', 'fillwithmissing'); %! assert_equal (height (S), 12); %! assert_equal (S.Properties.RowTimes, [t; n]); ## Test the apply methods carry the times and the step through %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! V = varfun (@(x) x * 2, A); %! assert_equal (V.Properties.RowTimes, t); %! assert_equal (V.Properties.TimeStep, hours (1)); %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! R = rowfun (@(x) x * 2, A, 'OutputFormat', 'timetable'); %! assert_equal (R.Properties.RowTimes, t); ## Test grouping by the row times bins them by the wall clock %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! G = groupsummary (A, 'Time', 'hour', 'sum'); %! assert_equal (height (G), 6); %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! assert_equal (height (groupsummary (A, 'Time', 'day', 'sum')), 1); %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! G = grouptransform (A, 'Time', 'hour', 'norm'); %! assert_equal (height (G), 6); %! assert_equal (G.v, ones (6, 1)); ## Test the zone survives every method that rebuilds the row times %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! D = sortrows (A, 'Time', 'descend'); %! assert_equal (D.Properties.RowTimes.TimeZone, z); %! assert_equal (lag (A, 1).Properties.RowTimes.TimeZone, z); %! assert_equal (retime (A, 'hourly', 'sum').Properties.RowTimes.TimeZone, z); ## Test moving and converting variables leaves the times alone %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! assert_equal (movevars (A, 'v', 'Before', 'v').Properties.TimeStep, ... %! hours (1)); %! assert_equal (class (convertvars (A, 'v', 'single').v), 'single'); ## Test 'stack' repeats the row times once per stacked variable %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', (11:16)', 'RowTimes', t, ... %! 'VariableNames', {'v', 'w'}); %! S = stack (A, {'v', 'w'}); %! assert_equal (height (S), 12); ## Test a join on a key keeps the left operand's zoned times %!test %! z = 'America/New_York'; %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', z) + hours ((0:5)'); %! A = timetable ((1:6)', (1:6)', 'RowTimes', t, 'VariableNames', {'v', 'k'}); %! B = timetable ((1:6)', (1:6)', 'RowTimes', t, 'VariableNames', {'u', 'k'}); %! J = innerjoin (A, B, 'Keys', 'k'); %! assert_equal (height (J), 6); %! assert_equal (J.Properties.RowTimes.TimeZone, z); ################################################################################ ## ** The sample rate kept or given up ** ## ################################################################################ ## A declared 'SampleRate' is metadata a row-touching method must either keep ## or give up, and it travels with 'TimeStep': the two report the same ## spacing, one in hertz and one as a duration. Measured on MATLAB R2024a. ## Test a declared rate reads back beside the step it means %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! assert_equal (R.Properties.SampleRate, 2); %! assert_equal (R.Properties.TimeStep, seconds (0.5)); %! assert_equal (isregular (R), true); %!test %! R = timetable ((1:6)', 'SampleRate', 2, 'StartTime', datetime (2024, 1, 1)); %! assert_equal (R.Properties.SampleRate, 2); %! assert_equal (R.Properties.TimeStep, seconds (0.5)); ## Test taking rows off either end keeps the rate %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! assert_equal (head (R, 3).Properties.SampleRate, 2); %! assert_equal (tail (R, 3).Properties.SampleRate, 2); %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! assert_equal (R(1:3, :).Properties.SampleRate, 2); %! assert_equal (R(2, :).Properties.SampleRate, 2); ## Test a subset taken every other row halves the rate %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! S = R([1, 3, 5], :); %! assert_equal (S.Properties.SampleRate, 1); %! assert_equal (S.Properties.TimeStep, seconds (1)); ## Test sorting keeps it, and descending order negates it %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! assert_equal (sortrows (R).Properties.SampleRate, 2); %! assert_equal (sortrows (R, 'Time', 'descend').Properties.SampleRate, -2); %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! assert_equal (topkrows (R, 2, 'Var1').Properties.SampleRate, -2); ## Test 'unique' over already unique rows keeps it %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! assert_equal (unique (R).Properties.SampleRate, 2); ## Test the methods that only touch variables keep it %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! assert_equal (addvars (R, (7:12)').Properties.SampleRate, 2); %! assert_equal (removevars (addvars (R, (7:12)'), 2).Properties.SampleRate, 2); %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! M = movevars (R, 'Var1', 'Before', 'Var1'); %! assert_equal (M.Properties.SampleRate, 2); %! assert_equal (convertvars (R, 'Var1', 'single').Properties.SampleRate, 2); %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! assert_equal (varfun (@(x) x * 2, R).Properties.SampleRate, 2); %! assert_equal (rowfun (@(x) x * 2, R, 'OutputFormat', ... %! 'timetable').Properties.SampleRate, 2); ## Test the methods that only touch values keep it %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! assert_equal (rmmissing (R).Properties.SampleRate, 2); %! assert_equal (fillmissing (R, 'linear').Properties.SampleRate, 2); %! assert_equal (lag (R, 1).Properties.SampleRate, 2); ## Test retiming onto the same grid keeps it and onto a coarser one resets it %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! S = retime (R, seconds ((0:0.5:2.5)'), 'sum'); %! assert_equal (S.Properties.SampleRate, 2); %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! S = retime (R, 'regular', 'sum', 'SampleRate', 1); %! assert_equal (S.Properties.SampleRate, 1); %! assert_equal (S.Properties.TimeStep, seconds (1)); %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! W = renamevars (R, 'Var1', 'w'); %! assert_equal (synchronize (R, W).Properties.SampleRate, 2); ## Test the methods that break the spacing give the rate up with the step %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! V = [R; R]; %! assert_equal (height (V), 12); %! assert_equal (V.Properties.SampleRate, NaN); %! assert_equal (ismissing (V.Properties.TimeStep), true); %! assert_equal (isregular (V), false); %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! assert_equal (repelem (R, 2, 1).Properties.SampleRate, NaN); %! assert_equal (repmat (R, 2, 1).Properties.SampleRate, NaN); %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! D = R; %! D(3, :) = []; %! assert_equal (height (D), 5); %! assert_equal (seconds (D.Properties.RowTimes), [0; 0.5; 1.5; 2; 2.5]); %! assert_equal (D.Properties.SampleRate, NaN); ## Test repeating the rows repeats their times %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! assert_equal (seconds (repelem (R, 2, 1).Properties.RowTimes)', ... %! [0, 0, 0.5, 0.5, 1, 1, 1.5, 1.5, 2, 2, 2.5, 2.5]); ## Test assigning the row times outright reads the rate off the new ones %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! R.Properties.RowTimes = seconds ((0:5)' * 3); %! assert_equal (R.Properties.TimeStep, seconds (3)); %! assert (R.Properties.SampleRate, 1/3, 1e-12); ## Test growing on the grid keeps the rate %!test %! R = timetable ((1:6)', 'SampleRate', 2); %! R(7, :) = {99}; %! assert_equal (height (R), 7); %! assert_equal (R.Properties.SampleRate, 2); ################################################################################ ## ** Time range and brace subscripts ** ## ################################################################################ ## A time range subscript reads the four interval types as it says, and the ## result is an ordinary timetable carrying whatever step is left. Measured ## on MATLAB R2024a. ## Test the default range takes the start and leaves the end out %!test %! t = datetime (2024, 1, 1) + hours ((0:5)'); %! C = timetable ((1:6)', (11:16)', 'RowTimes', t, ... %! 'VariableNames', {'v', 'w'}); %! R = C(timerange (t(2), t(5)), :); %! assert_equal (R.Properties.RowTimes, t(2:4)); %! assert_equal (R.v, [2; 3; 4]); %!test %! t = datetime (2024, 1, 1) + hours ((0:5)'); %! C = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! assert_equal (C(timerange (t(2), t(5), 'closed'), :).v, [2; 3; 4; 5]); %! assert_equal (C(timerange (t(2), t(5), 'open'), :).v, [3; 4]); %! assert_equal (C(timerange (t(2), t(5), 'openleft'), :).v, [3; 4; 5]); %! assert_equal (C(timerange (t(2), t(5), 'openright'), :).v, [2; 3; 4]); ## Test the range result keeps the step the rows it kept have %!test %! t = datetime (2024, 1, 1) + hours ((0:5)'); %! C = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! R = C(timerange (t(2), t(5)), :); %! assert_equal (R.Properties.TimeStep, hours (1)); %! assert_equal (isregular (R), true); ## Test a range running backwards selects nothing %!test %! t = datetime (2024, 1, 1) + hours ((0:5)'); %! C = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! assert_equal (height (C(timerange (t(5), t(2)), :)), 0); ## Test a vector of row times selects the rows carrying them %!test %! t = datetime (2024, 1, 1) + hours ((0:5)'); %! C = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! R = C([t(2); t(5)], :); %! assert_equal (R.Properties.RowTimes, [t(2); t(5)]); %! assert_equal (R.v, [2; 5]); ## Test one row time gives the one row %!test %! t = datetime (2024, 1, 1) + hours ((0:5)'); %! C = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! assert_equal (C(t(2), :).v, 2); ## Test naming a time twice takes its row twice %!test %! t = datetime (2024, 1, 1) + hours ((0:5)'); %! C = timetable ((1:6)', 'RowTimes', t, 'VariableNames', {'v'}); %! assert_equal (height (C([t(2); t(2)], :)), 2); ## Test duration row times are subscripted the same way %!test %! D = timetable ((1:6)', 'RowTimes', seconds ((0:5)'), ... %! 'VariableNames', {'v'}); %! assert_equal (D(seconds ([1; 3]), :).v, [2; 4]); %! assert_equal (D(timerange (seconds (1), seconds (4)), :).v, [2; 3; 4]); %! assert_equal (D(timerange (seconds (1), seconds (4), 'closed'), :).v, ... %! [2; 3; 4; 5]); %!test %! D = timetable ((1:6)', 'RowTimes', seconds ((0:5)'), ... %! 'VariableNames', {'v'}); %! R = D(timerange (seconds (1), seconds (4)), :); %! assert_equal (R.Properties.TimeStep, seconds (1)); ## Test brace reference brings back the values without the timetable %!test %! t = datetime (2024, 1, 1) + hours ((0:5)'); %! C = timetable ((1:6)', (11:16)', 'RowTimes', t, ... %! 'VariableNames', {'v', 'w'}); %! assert_equal (C{:, :}, [(1:6)', (11:16)']); %! assert_equal (C{:, 'v'}, (1:6)'); ## Test a brace reference takes a row time subscript too %!test %! t = datetime (2024, 1, 1) + hours ((0:5)'); %! C = timetable ((1:6)', (11:16)', 'RowTimes', t, ... %! 'VariableNames', {'v', 'w'}); %! assert_equal (C{[t(2); t(5)], 'v'}, [2; 5]); %! assert_equal (C{timerange (t(2), t(5)), :}, [(2:4)', (12:14)']); ## Test 'end' counts the rows in a brace reference %!test %! t = datetime (2024, 1, 1) + hours ((0:5)'); %! C = timetable ((1:6)', (11:16)', 'RowTimes', t, ... %! 'VariableNames', {'v', 'w'}); %! assert_equal (C{end, :}, [6, 16]); %!test %! D = timetable ((1:6)', 'RowTimes', seconds ((0:5)'), ... %! 'VariableNames', {'v'}); %! assert_equal (D{seconds ([1; 3]), 'v'}, [2; 4]); ## Test a variable type subscript takes the variables of that type %!test %! t = datetime (2024, 1, 1) + hours ((0:5)'); %! C = timetable ((1:6)', (11:16)', 'RowTimes', t, ... %! 'VariableNames', {'v', 'w'}); %! assert_equal (C(:, vartype ('numeric')).Properties.VariableNames, ... %! {'v', 'w'}); %! assert_equal (width (C(:, vartype ('duration'))), 0); %! assert_equal (width (C(:, vartype ('cell'))), 0); ## Test it sorts a mixed timetable into its types %!test %! t = datetime (2024, 1, 1) + hours ((0:5)'); %! C = timetable ((1:6)', (11:16)', 'RowTimes', t, ... %! 'VariableNames', {'v', 'w'}); %! C.d = seconds ((1:6)'); %! assert_equal (C(:, vartype ('numeric')).Properties.VariableNames, ... %! {'v', 'w'}); %! assert_equal (C(:, vartype ('duration')).Properties.VariableNames, {'d'}); ## Test a variable type subscript reads in a brace reference as well %!test %! t = datetime (2024, 1, 1) + hours ((0:5)'); %! C = timetable ((1:6)', (11:16)', 'RowTimes', t, ... %! 'VariableNames', {'v', 'w'}); %! assert_equal (C{:, vartype ('numeric')}, [(1:6)', (11:16)']); ## ** The file round trips ** ## ################################################################################ ## Each of the three file routes is asked to give back what it was handed. ## The row times and their type are the point: a format that loses them ## loses what the class is for. The house text and spreadsheet pairs are ## this package's own, so what they keep is settled here rather than ## against MATLAB; 'writetimetable' and 'readtimetable' write what MATLAB ## writes and were read on R2026a when the format was settled. ## Test the house text pair gives back the row times, their type and format %!test %! t = datetime (2024, 6, 1) + hours (0:3)'; %! TT = timetable (t, (1:4)', {'p'; 'q'; 'r'; 's'}, ... %! 'VariableNames', {'a', 'b'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! timetable2csv (TT, fname); %! B = csv2timetable (fname); %! assert_equal (class (B), 'timetable'); %! assert_equal (B.Properties.RowTimes, t); %! assert_equal (B.Properties.VariableNames, {'a', 'b'}); %! assert_equal (B.a, (1:4)'); %! assert_equal (B.b, {'p'; 'q'; 'r'; 's'}); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test infinities and NaN come back through the text pair %!test %! t = datetime (2024, 6, 1) + hours (0:2)'; %! TT = timetable (t, [Inf; -Inf; NaN], 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! timetable2csv (TT, fname); %! B = csv2timetable (fname); %! assert_equal (isequaln (B.v, [Inf; -Inf; NaN]), true); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test infinities and NaN come back through the OpenDocument pair %!test %! t = datetime (2024, 6, 1) + hours (0:2)'; %! TT = timetable (t, [Inf; -Inf; NaN], 'VariableNames', {'v'}); %! fname = [tempname(), '.fods']; %! unwind_protect %! timetable2ods (TT, fname); %! B = ods2timetable (fname); %! assert_equal (isequaln (B.v, [Inf; -Inf; NaN]), true); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test a zoned row time comes back zoned through the text pair %!test %! ny = 'America/New_York'; %! t = datetime (2024, 3, 10, 'TimeZone', ny) + hours (0:3)'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! timetable2csv (TT, fname); %! B = csv2timetable (fname); %! assert_equal (B.Properties.RowTimes.TimeZone, ny); %! assert_equal (B.Properties.RowTimes, t); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test duration row times come back a duration and not a number %!test %! TT = timetable (hours (0:3)', (1:4)', 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! timetable2csv (TT, fname); %! B = csv2timetable (fname); %! assert_equal (class (B.Properties.RowTimes), 'duration'); %! assert_equal (seconds (B.Properties.RowTimes)', [0, 3600, 7200, 10800]); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the house spreadsheet pair gives back the row times and the values %!test %! t = datetime (2024, 6, 1) + hours (0:3)'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! fname = [tempname(), '.fods']; %! unwind_protect %! timetable2ods (TT, fname); %! B = ods2timetable (fname); %! assert_equal (class (B), 'timetable'); %! assert_equal (B.Properties.RowTimes, t); %! assert_equal (B.v, (1:4)'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the spreadsheet pair is the one that carries an event table %!test %! t = datetime (2024, 6, 1) + hours (0:3)'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! ET = eventtable (timetable (t([2, 3]), {'x'; 'y'}, ... %! 'VariableNames', {'L'})); %! ET.EventLabelsVariable = 'L'; %! TT.Properties.Events = ET; %! fname = [tempname(), '.fods']; %! unwind_protect %! timetable2ods (TT, fname); %! B = ods2timetable (fname); %! assert_equal (class (B.Properties.Events), 'eventtable'); %! assert_equal (height (B.Properties.Events), 2); %! assert_equal (B.Properties.Events.Properties.RowTimes, t([2, 3])); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the MATLAB-compatible pair round trips through a text file %!test %! t = datetime (2024, 6, 1) + hours (0:3)'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! writetimetable (TT, fname); %! B = readtimetable (fname); %! assert_equal (class (B), 'timetable'); %! assert_equal (B.Properties.RowTimes, t); %! assert_equal (B.v, (1:4)'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test it round trips through a spreadsheet as well %!test %! t = datetime (2024, 6, 1) + hours (0:3)'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! fname = [tempname(), '.fods']; %! unwind_protect %! writetimetable (TT, fname); %! B = readtimetable (fname); %! assert_equal (B.Properties.RowTimes, t); %! assert_equal (B.v, (1:4)'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test a zoned row time survives it in the RFC 9557 form of deviation D7 %!test %! ny = 'America/New_York'; %! t = datetime (2024, 3, 10, 'TimeZone', ny) + hours (0:3)'; %! TT = timetable (t, (1:4)', 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! writetimetable (TT, fname); %! B = readtimetable (fname); %! assert_equal (B.Properties.RowTimes.TimeZone, ny); %! assert_equal (B.Properties.RowTimes, t); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ################################################################################ ## ** Regularity by unit ** ## ################################################################################ ## 'isregular' asks whether the rows are evenly spaced in the unit named. A ## calendar unit is answered by the calendar and a coarser one implies the ## finer ones it divides into; a duration carries no calendar, so it answers ## for time alone. Measured on MATLAB R2024a. ## Test an hourly timetable is regular in time and in nothing else %!test %! d0 = datetime (2024, 1, 1); %! T = timetable (d0 + hours (0:5)', (1:6)'); %! assert_equal (isregular (T), true); %! assert_equal (isregular (T, 'time'), true); %! assert_equal (isregular (T, 'days'), false); %! assert_equal (isregular (T, 'months'), false); %! assert_equal (isregular (T, 'years'), false); ## Test a daily one is regular in days but not in weeks %!test %! d0 = datetime (2024, 1, 1); %! T = timetable (d0 + days (0:5)', (1:6)'); %! assert_equal (isregular (T, 'time'), true); %! assert_equal (isregular (T, 'days'), true); %! assert_equal (isregular (T, 'weeks'), false); %! assert_equal (isregular (T, 'months'), false); ## Test a weekly one is regular in weeks and in the days they hold %!test %! d0 = datetime (2024, 1, 1); %! T = timetable (d0 + days (0:7:35)', (1:6)'); %! assert_equal (isregular (T, 'time'), true); %! assert_equal (isregular (T, 'days'), true); %! assert_equal (isregular (T, 'weeks'), true); %! assert_equal (isregular (T, 'months'), false); ## Test a monthly one is regular in months and in no fixed length of time %!test %! d0 = datetime (2024, 1, 1); %! T = timetable (d0 + calmonths ((0:5)'), (1:6)'); %! assert_equal (isregular (T), false); %! assert_equal (isregular (T, 'time'), false); %! assert_equal (isregular (T, 'days'), false); %! assert_equal (isregular (T, 'months'), true); %! assert_equal (isregular (T, 'quarters'), false); ## Test a coarser calendar unit implies the finer ones it divides into %!test %! d0 = datetime (2024, 1, 1); %! T = timetable (d0 + calquarters ((0:5)'), (1:6)'); %! assert_equal (isregular (T, 'months'), true); %! assert_equal (isregular (T, 'quarters'), true); %! assert_equal (isregular (T, 'years'), false); %!test %! d0 = datetime (2024, 1, 1); %! T = timetable (d0 + calyears ((0:5)'), (1:6)'); %! assert_equal (isregular (T, 'months'), true); %! assert_equal (isregular (T, 'quarters'), true); %! assert_equal (isregular (T, 'years'), true); %! assert_equal (isregular (T, 'time'), false); ## Test duration row times answer for time and for no calendar unit, a ## duration of whole days included %!test %! T = timetable (hours (0:5)', (1:6)'); %! assert_equal (isregular (T, 'time'), true); %! assert_equal (isregular (T, 'days'), false); %! assert_equal (isregular (T, 'months'), false); %!test %! T = timetable (days (0:5)', (1:6)'); %! assert_equal (isregular (T, 'time'), true); %! assert_equal (isregular (T, 'days'), false); %! assert_equal (isregular (T, 'weeks'), false); ## Test an irregular timetable is regular in nothing %!test %! d0 = datetime (2024, 1, 1); %! T = timetable (d0 + hours ([0, 1, 3, 6, 10, 15])', (1:6)'); %! assert_equal (isregular (T), false); %! assert_equal (isregular (T, 'time'), false); %! assert_equal (isregular (T, 'days'), false); %! assert_equal (isregular (T, 'months'), false); ## Test the three predicates single out what each is for %!test %! T = timetable (datetime (2024, 1, 1), 1); %! assert_equal (istimetable (T), true); %! assert_equal (istable (T), false); %! assert_equal (istabular (T), true); %! assert_equal (istimetable (table (1)), false); %! assert_equal (istabular (table (1)), true); %! assert_equal (istimetable (1), false); %! assert_equal (istabular (1), false); ## Test an event table is a timetable and still not a table %!test %! E = eventtable (timetable (datetime (2024, 1, 1), 1)); %! assert_equal (istimetable (E), true); %! assert_equal (istable (E), false); ################################################################################ ## ** Row selection and assignment ** ## ################################################################################ ## Measured on MATLAB R2024a. ## Test 'head' and 'tail' take eight rows by default and clamp to the height %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (height (head (A)), 6); %! assert_equal (height (tail (A)), 6); %! assert_equal (height (head (A, 99)), 6); %! assert_equal (height (tail (A, 99)), 6); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (height (head (A, 0)), 0); %! assert_equal (height (tail (A, 0)), 0); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (head (A, 2).Properties.RowTimes, t(1:2)); %! assert_equal (tail (A, 2).Properties.RowTimes, t(5:6)); ## Assigning to a row past the end grows the timetable. A declared step says ## what the row after the last one is, so the grid goes on; a step only read ## off the rows says nothing about a row that was never there, and the times ## added go missing. Measured on MATLAB R2024a. ## Test a declared 'TimeStep' carries the grid on %!test %! A = timetable ((1:6)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 1, 1)); %! A(8, :) = {99}; %! assert_equal (height (A), 8); %! assert_equal (A.Properties.RowTimes, ... %! datetime (2024, 1, 1) + hours ((0:7)')); %! assert_equal (A.Var1, [1; 2; 3; 4; 5; 6; 0; 99]); %! assert_equal (A.Properties.TimeStep, hours (1)); %! assert_equal (isregular (A), true); ## Test a declared 'SampleRate' does the same %!test %! A = timetable ((1:6)', 'SampleRate', 2); %! A(7, :) = {99}; %! assert_equal (height (A), 7); %! assert_equal (seconds (A.Properties.RowTimes), (0:0.5:3)'); %! assert_equal (A.Properties.SampleRate, 2); ## Test the rows skipped over are filled, a numeric variable with zero %!test %! A = timetable ((1:6)', 'SampleRate', 2); %! A(9, :) = {99}; %! assert_equal (height (A), 9); %! assert_equal (A.Var1, [1; 2; 3; 4; 5; 6; 0; 0; 99]); %! assert_equal (seconds (A.Properties.RowTimes), (0:0.5:4)'); ## Test a step only inferred leaves the added row times missing %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! A = timetable ((1:3)', 'RowTimes', t); %! A(5, :) = {99}; %! assert_equal (height (A), 5); %! assert_equal (ismissing (A.Properties.RowTimes), ... %! logical ([0; 0; 0; 1; 1])); %! assert_equal (isregular (A), false); %! assert_equal (ismissing (A.Properties.TimeStep), true); ## Test an irregular timetable grows the same way %!test %! A = timetable ((1:4)', 'RowTimes', seconds ([0; 1; 3; 7])); %! A(5, :) = {99}; %! assert_equal (height (A), 5); %! assert_equal (ismissing (A.Properties.RowTimes(5)), true); %! assert_equal (A.Var1, [1; 2; 3; 4; 99]); ## Test duration row times take a missing time too %!test %! A = timetable ((1:3)', 'RowTimes', seconds ((0:2)')); %! A(5, :) = {99}; %! assert_equal (seconds (A.Properties.RowTimes(1:3)), (0:2)'); %! assert_equal (ismissing (A.Properties.RowTimes(4:5)), [true; true]); %! assert_equal (isregular (A), false); ## Test a subsetted timetable carries on by the step it reports %!test %! A = timetable ((1:6)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 1, 1)); %! B = A([1, 3, 5], :); %! assert_equal (B.Properties.TimeStep, hours (2)); %! B(5, :) = {99}; %! assert_equal (B.Properties.RowTimes, ... %! datetime (2024, 1, 1) + hours ((0:2:8)')); %! assert_equal (B.Properties.TimeStep, hours (2)); ## Test a zoned timetable keeps its zone and blanks in its own zone %!test %! t = datetime (2024, 3, 10, 0, 0, 0, 'TimeZone', 'America/New_York') ... %! + hours ((0:2)'); %! A = timetable ((1:3)', 'RowTimes', t); %! A(5, :) = {99}; %! assert_equal (height (A), 5); %! assert_equal (A.Properties.RowTimes.TimeZone, 'America/New_York'); %! assert_equal (ismissing (A.Properties.RowTimes(4:5)), [true; true]); ## Test a variable the assignment did not name grows with the rest %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! A = timetable ((1:3)', (4:6)', 'RowTimes', t); %! A(5, :) = {9, 9}; %! assert_equal (A.Var1, [1; 2; 3; 0; 9]); %! assert_equal (A.Var2, [4; 5; 6; 0; 9]); %! assert_equal (numel (A.Properties.RowTimes), 5); ## Test a text variable grows with a blank rather than a broken cell %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! A = timetable ({'a'; 'b'; 'c'}, 'RowTimes', t); %! A(5, :) = {'z'}; %! assert_equal (A.Var1, {'a'; 'b'; 'c'; ''; 'z'}); ## Test a logical variable grows with false %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! A = timetable (logical ([1; 0; 1]), 'RowTimes', t); %! A(5, :) = {true}; %! assert_equal (A.Var1, logical ([1; 0; 1; 0; 1])); ## Test a datetime variable grows with a missing value %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! A = timetable ((1:3)', 'RowTimes', t); %! A.d = t; %! A(5, :) = {9, datetime(2024, 2, 1)}; %! assert_equal (ismissing (A.d), logical ([0; 0; 0; 1; 0])); ## Test growing an empty timetable gives it its first row %!test %! A = timetable ('Size', [0, 1], 'VariableTypes', {'double'}, ... %! 'RowTimes', datetime.empty (0, 1)); %! A(1, :) = {5}; %! assert_equal (height (A), 1); %! assert_equal (A.Var1, 5); %! assert_equal (ismissing (A.Properties.RowTimes), true); ## Test assigning the row times replaces them and reads the step afresh %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! B = A; %! B.Properties.RowTimes = t + hours (10); %! assert_equal (B.Properties.RowTimes, t + hours (10)); %! assert_equal (B.Properties.TimeStep, hours (1)); ## Test assigning the start time shifts every row and keeps the step %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! D = timetable ((1:6)', 'TimeStep', hours (1), 'StartTime', t(1)); %! B = D; %! B.Properties.StartTime = t(1) + hours (5); %! assert_equal (B.Properties.RowTimes, t + hours (5)); %! assert_equal (B.Properties.TimeStep, hours (1)); ## Test assigning the step regenerates the row times from the start %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! D = timetable ((1:6)', 'TimeStep', hours (1), 'StartTime', t(1)); %! B = D; %! B.Properties.TimeStep = hours (2); %! assert_equal (B.Properties.RowTimes, t(1) + hours (0:2:10)'); %! assert_equal (B.Properties.SampleRate, 1/7200); ## Test assigning the rate does the same, being the step's reciprocal %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! D = timetable ((1:6)', 'TimeStep', hours (1), 'StartTime', t(1)); %! B = D; %! B.Properties.SampleRate = 1; %! assert_equal (B.Properties.TimeStep, seconds (1)); %! assert_equal (B.Properties.RowTimes, t(1) + seconds (0:5)'); ## Test deleting rows takes their times with them and reads the step afresh %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! B = A; %! B(2, :) = []; %! assert_equal (height (B), 5); %! assert_equal (B.Properties.RowTimes, t([1, 3, 4, 5, 6])); %! assert_equal (isnan (B.Properties.TimeStep), true); ## Test a variable goes by assigning it away, and arrives by naming it %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! B = A; %! B.a = []; %! assert_equal (B.Properties.VariableNames, {'b'}); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! B = A; %! B.a = (21:26)'; %! assert_equal (B.a, (21:26)'); %! B.c = (21:26)'; %! assert_equal (B.Properties.VariableNames, {'a', 'b', 'c'}); ## Test assigning at a row time the timetable does not have adds the row %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! A(t(1) + hours (9), :) = {7, 7}; %! assert_equal (height (A), 7); %! assert_equal (A.Properties.RowTimes(7), t(1) + hours (9)); %! assert_equal (A.a, [1; 2; 3; 4; 5; 6; 7]); %! assert_equal (A.b, [11; 12; 13; 14; 15; 16; 7]); ## Test a time range given a unit widens to whole periods %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (height (A(timerange (t(2), t(4), 'hours'), :)), 3); %! assert_equal (height (A(timerange (t(2), t(4), 'days'), :)), 6); %!test %! t = datetime (2024, 6, 1) + minutes (0:30:150)'; %! A = timetable (t, (1:6)', 'VariableNames', {'a'}); %! assert_equal (A(timerange (t(2), t(4), 'hours'), :).a, [1; 2; 3; 4]); ## Test the four kinds of time range take their bounds as they say %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (height (A(timerange (t(2), t(4)), :)), 2); %! assert_equal (height (A(timerange (t(2), t(4), 'closed'), :)), 3); %! assert_equal (height (A(timerange (t(2), t(4), 'openleft'), :)), 2); %! assert_equal (height (A(timerange (t(2), t(4), 'open'), :)), 1); ## Test a tolerance takes the rows within it %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (height (A(withtol (t(3), minutes (0)), :)), 1); %! assert_equal (height (A(withtol (t(3), minutes (90)), :)), 3); %! assert_equal (height (A(withtol (t([2, 4]), minutes (10)), :)), 2); ## Test a variable type subscript selects by type %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (width (A(:, vartype ('numeric'))), 2); ## Test the range predicates read a closed range and one that misses %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (containsrange (A, timerange (t(2), t(4), 'closed')), true); %! assert_equal (withinrange (A, timerange (t(2), t(4), 'closed')), false); %! assert_equal (overlapsrange (A, timerange (t(1) - hours (5), ... %! t(1) - hours (2))), false); ## Test 'summary' keys its struct by the row time and variable names %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! S = summary (A); %! assert_equal (fieldnames (S)', {'t', 'a', 'b'}); %! assert_equal (S.a.Min, 1); %! assert_equal (S.a.Max, 6); ## Test a categorical variable is described by its categories %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! C = timetable (t, categorical ({'x'; 'y'; 'x'; 'y'; 'x'; 'y'}), ... %! 'VariableNames', {'g'}); %! S = summary (C); %! assert_equal (fieldnames (S.g)', {'Size', 'Type', 'Description', ... %! 'Units', 'Continuity', 'Categories', 'Counts', ... %! 'NumMissing'}); %! assert_equal (S.g.Categories', {'x', 'y'}); ################################################################################ ## ** Per method behaviour ** ## ################################################################################ ## Measured on MATLAB R2024a. These are the methods the other sections ## touch only in passing. ## Test the size queries answer for two dimensions %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (width (A), 2); %! assert_equal (ndims (A), 2); %! assert_equal (numel (A), 12); %! assert_equal (size (A, 2), 2); ## Test the concatenations called by name agree with the bracket syntax %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (height (vertcat (A, A)), 12); %! assert_equal (height (cat (1, A, A)), 12); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! B = renamevars (A, {'a', 'b'}, {'c', 'd'}); %! assert_equal (width (horzcat (A, B)), 4); %! assert_equal (width (cat (2, A, B)), 4); ## Test 'repelem' repeats each row where 'repmat' repeats the block %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (height (repelem (A, 2, 1)), 12); %! assert_equal (repelem (A(1:2, :), 2, 1).Properties.RowTimes, ... %! t([1, 1, 2, 2])); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (height (repmat (A, 2, 1)), 12); %! assert_equal (repmat (A(1:2, :), 2, 1).Properties.RowTimes, ... %! t([1, 2, 1, 2])); %! assert_equal (width (repmat (A, 1, 2)), 4); ## Test 'isequaln' counts two missing values alike where 'isequal' does not %!test %! t4 = datetime (2024, 6, 1) + hours (0:3)'; %! X = timetable (t4, [1; NaN; 3; NaN], 'VariableNames', {'v'}); %! assert_equal (isequaln (X, X), true); %! assert_equal (isequal (X, X), false); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (isequaln (A, A), true); %! assert_equal (anymissing (A), false); ## Test the missing-data family reads the variables and not the row times %!test %! t4 = datetime (2024, 6, 1) + hours (0:3)'; %! X = timetable (t4, [1; NaN; 3; NaN], 'VariableNames', {'v'}); %! assert_equal (anymissing (X), true); %! assert_equal (sum (ismissing (X)), 2); %! assert_equal (height (rmmissing (X)), 2); %!test %! t4 = datetime (2024, 6, 1) + hours (0:3)'; %! X = timetable (t4, [1; NaN; 3; NaN], 'VariableNames', {'v'}); %! [~, TF] = rmmissing (X); %! assert_equal (TF', [false, true, false, true]); %!test %! t4 = datetime (2024, 6, 1) + hours (0:3)'; %! X = timetable (t4, [1; NaN; 3; NaN], 'VariableNames', {'v'}); %! assert_equal (fillmissing (X, 'previous').v, [1; 1; 3; 3]); %! assert_equal (fillmissing (X, 'constant', 0).v, [1; 0; 3; 0]); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (standardizeMissing (A(:, 1), 3).a, [1; 2; NaN; 4; 5; 6]); ## Test the variable-management family names and orders as asked %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (removevars (A, 'a').Properties.VariableNames, {'b'}); %! assert_equal (movevars (A, 'a', 'After', 'b').Properties.VariableNames, ... %! {'b', 'a'}); %! assert_equal (renamevars (A, 'a', 'q').Properties.VariableNames, ... %! {'q', 'b'}); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (class (convertvars (A, 'a', 'single').a), 'single'); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! M = mergevars (A, {'a', 'b'}); %! assert_equal (M.Properties.VariableNames, {'Var1'}); %! assert_equal (size (M.Var1), [6, 2]); %!test %! t4 = datetime (2024, 6, 1) + hours (0:3)'; %! M = timetable (t4, [1, 2; 3, 4; 5, 6; 7, 8], 'VariableNames', {'m'}); %! S = splitvars (M); %! assert_equal (S.Properties.VariableNames, {'m_1', 'm_2'}); %! assert_equal (height (S), 4); ## A generated variable name counts the place the variable takes in the ## result and not how many variables there were, so one put before the first ## is 'Var1'. A name already in use is deconflicted with a suffix. ## Measured on MATLAB R2024a. %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (addvars (A, (1:6)').Properties.VariableNames, ... %! {'a', 'b', 'Var3'}); %! assert_equal (addvars (A, (1:6)', (7:12)').Properties.VariableNames, ... %! {'a', 'b', 'Var3', 'Var4'}); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! B1 = addvars (A, (1:6)', 'Before', 'a'); %! assert_equal (B1.Properties.VariableNames, {'Var1', 'a', 'b'}); %! assert_equal (addvars (A, (1:6)', (7:12)', ... %! 'Before', 'a').Properties.VariableNames, ... %! {'Var1', 'Var2', 'a', 'b'}); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! B2 = addvars (A, (1:6)', 'After', 'a'); %! assert_equal (B2.Properties.VariableNames, {'a', 'Var2', 'b'}); %! assert_equal (addvars (A, (1:6)', (7:12)', ... %! 'After', 'a').Properties.VariableNames, ... %! {'a', 'Var2', 'Var3', 'b'}); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! C = timetable (t, (1:6)', (11:16)', (21:26)', ... %! 'VariableNames', {'p', 'q', 'r'}); %! B3 = addvars (C, (1:6)', 'After', 'q'); %! assert_equal (B3.Properties.VariableNames, {'p', 'q', 'Var3', 'r'}); ## Test a generated name already in use is deconflicted, not renumbered %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! B = timetable (t, (1:6)', 'VariableNames', {'Var1'}); %! assert_equal (addvars (B, (1:6)', ... %! 'Before', 'Var1').Properties.VariableNames, ... %! {'Var1_1', 'Var1'}); %! assert_equal (addvars (B, (1:6)').Properties.VariableNames, ... %! {'Var1', 'Var2'}); ## Test a new variable gains an empty entry in the units %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! A.Properties.VariableUnits = {'m', 's'}; %! assert_equal (addvars (A, (1:6)').Properties.VariableUnits, {'m', 's', ''}); ## Test the sortedness queries read the variables, not the row times %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (issortedrows (A), true); %! assert_equal (issortedrows (A, 'a'), true); %! assert_equal (issortedrows (A, 'a', 'descend'), false); %! assert_equal (issorted (flipud (A)), false); %!test %! t4 = datetime (2024, 6, 1) + hours (0:3)'; %! C = timetable (t4, (1:4)', categorical ({'x'; 'y'; 'x'; 'y'}), ... %! 'VariableNames', {'v', 'g'}); %! assert_equal (sortrows (C, {'g', 'v'}).v, [1; 3; 2; 4]); ## Test the grouping family names and counts its result %!test %! t4 = datetime (2024, 6, 1) + hours (0:3)'; %! C = timetable (t4, (1:4)', categorical ({'x'; 'y'; 'x'; 'y'}), ... %! 'VariableNames', {'v', 'g'}); %! G = grouptransform (C, 'g', 'zscore'); %! assert_equal (height (G), 4); %! assert_equal (G.Properties.VariableNames, {'v', 'g'}); %!test %! t4 = datetime (2024, 6, 1) + hours (0:3)'; %! C = timetable (t4, (1:4)', categorical ({'x'; 'y'; 'x'; 'y'}), ... %! 'VariableNames', {'v', 'g'}); %! G = groupsummary (C, 'g', 'sum', 'v'); %! assert_equal (height (G), 2); %! assert_equal (G.Properties.VariableNames, {'g', 'GroupCount', 'sum_v'}); %!test %! t4 = datetime (2024, 6, 1) + hours (0:3)'; %! C = timetable (t4, (1:4)', categorical ({'x'; 'y'; 'x'; 'y'}), ... %! 'VariableNames', {'v', 'g'}); %! assert_equal (height (groupfilter (C, 'g', @(x) sum (x) > 3)), 4); %! assert_equal (groupcounts (C, 'g').Properties.VariableNames, ... %! {'g', 'GroupCount', 'Percent'}); %!test %! t4 = datetime (2024, 6, 1) + hours (0:3)'; %! C = timetable (t4, (1:4)', categorical ({'x'; 'y'; 'x'; 'y'}), ... %! 'VariableNames', {'v', 'g'}); %! assert_equal (findgroups (C(:, {'g'})), [1; 2; 1; 2]); %! assert_equal (splitapply (@(x) sum (x), C(:, {'v'}), [1; 2; 1; 2]), ... %! [4; 6]); ## Test the reshaping family names its result %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! S = stack (A, {'a', 'b'}); %! assert_equal (S.Properties.VariableNames, {'a_b_Indicator', 'a_b'}); %! assert_equal (height (S), 12); %!test %! t4 = datetime (2024, 6, 1) + hours (0:3)'; %! C = timetable (t4, (1:4)', categorical ({'x'; 'y'; 'x'; 'y'}), ... %! 'VariableNames', {'v', 'g'}); %! U = unstack (C, 'v', 'g'); %! assert_equal (U.Properties.VariableNames, {'x', 'y'}); %! assert_equal (height (U), 4); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! V = rows2vars (A); %! assert_equal (height (V), 2); %! assert_equal (V.Properties.VariableNames{1}, 'OriginalVariableNames'); %!test %! t4 = datetime (2024, 6, 1) + hours (0:3)'; %! C = timetable (t4, (1:4)', categorical ({'x'; 'y'; 'x'; 'y'}), ... %! 'VariableNames', {'v', 'g'}); %! assert_equal (pivot (C, 'Rows', 'g').Properties.VariableNames, ... %! {'g', 'count'}); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (inner2outer (A).Properties.VariableNames, {'a', 'b'}); ## Test the joins keep, rename and merge the keys as asked %!test %! t4 = datetime (2024, 6, 1) + hours (0:3)'; %! L = timetable (t4, [1; 2; 3; 4], 'VariableNames', {'K'}); %! R = timetable (t4, [1; 2; 3; 9], {'p'; 'q'; 'r'; 's'}, ... %! 'VariableNames', {'K', 'z'}); %! I = innerjoin (L, R, 'Keys', 'K'); %! assert_equal (height (I), 3); %! assert_equal (I.Properties.VariableNames, {'K', 'z'}); %!test %! t4 = datetime (2024, 6, 1) + hours (0:3)'; %! L = timetable (t4, [1; 2; 3; 4], 'VariableNames', {'K'}); %! R = timetable (t4, [1; 2; 3; 9], {'p'; 'q'; 'r'; 's'}, ... %! 'VariableNames', {'K', 'z'}); %! O = outerjoin (L, R, 'Keys', 'K'); %! assert_equal (height (O), 5); %! assert_equal (O.Properties.VariableNames, {'K_L', 'K_R', 'z'}); %!test %! t4 = datetime (2024, 6, 1) + hours (0:3)'; %! L = timetable (t4, [1; 2; 3; 4], 'VariableNames', {'K'}); %! R = timetable (t4, [1; 2; 3; 9], {'p'; 'q'; 'r'; 's'}, ... %! 'VariableNames', {'K', 'z'}); %! M = outerjoin (L, R, 'Keys', 'K', 'MergeKeys', true); %! assert_equal (M.Properties.VariableNames, {'K', 'z'}); ## Test a custom property comes and goes by name %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! P = addprop (A, {'q'}, {'table'}); %! assert_equal (fieldnames (P.Properties.CustomProperties)', {'q'}); %! assert_equal (isempty (fieldnames ( ... %! rmprop (P, {'q'}).Properties.CustomProperties)), true); ################################################################################ ## ** The table round trip ** ## ################################################################################ ## What survives 'timetable2table' and comes back through 'table2timetable'. ## Every expectation was measured on MATLAB R2024a. ## Test the row times become the first variable, under their own name %!test %! t = datetime (2024, 6, 1) + hours (0:3)'; %! TT = timetable (t, (1:4)', {'p'; 'q'; 'r'; 's'}, ... %! 'VariableNames', {'a', 'b'}); %! TT.Properties.DimensionNames = {'Stamp', 'Vars'}; %! TT.Properties.VariableUnits = {'m', ''}; %! TT.Properties.VariableDescriptions = {'first', 'second'}; %! TT.Properties.VariableContinuity = {'continuous', 'unset'}; %! TT.Properties.Description = 'a description'; %! TT = addprop (TT, {'tp', 'vp'}, {'table', 'variable'}); %! TT.Properties.CustomProperties.tp = 'table scoped'; %! TT.Properties.CustomProperties.vp = {'u', 'v'}; %! T = timetable2table (TT); %! assert_equal (T.Properties.VariableNames, {'Stamp', 'a', 'b'}); %! assert_equal (class (T.Stamp), 'datetime'); %! assert_equal (isempty (T.Properties.RowNames), true); ## Test the rows stop being labelled by time and the variables keep their name %!test %! t = datetime (2024, 6, 1) + hours (0:3)'; %! TT = timetable (t, (1:4)', {'p'; 'q'; 'r'; 's'}, ... %! 'VariableNames', {'a', 'b'}); %! TT.Properties.DimensionNames = {'Stamp', 'Vars'}; %! TT.Properties.VariableUnits = {'m', ''}; %! TT.Properties.VariableDescriptions = {'first', 'second'}; %! TT.Properties.VariableContinuity = {'continuous', 'unset'}; %! TT.Properties.Description = 'a description'; %! TT = addprop (TT, {'tp', 'vp'}, {'table', 'variable'}); %! TT.Properties.CustomProperties.tp = 'table scoped'; %! TT.Properties.CustomProperties.vp = {'u', 'v'}; %! T = timetable2table (TT); %! assert_equal (T.Properties.DimensionNames, {'Row', 'Vars'}); ## Test the variable metadata shifts along with the variables %!test %! t = datetime (2024, 6, 1) + hours (0:3)'; %! TT = timetable (t, (1:4)', {'p'; 'q'; 'r'; 's'}, ... %! 'VariableNames', {'a', 'b'}); %! TT.Properties.DimensionNames = {'Stamp', 'Vars'}; %! TT.Properties.VariableUnits = {'m', ''}; %! TT.Properties.VariableDescriptions = {'first', 'second'}; %! TT.Properties.VariableContinuity = {'continuous', 'unset'}; %! TT.Properties.Description = 'a description'; %! TT = addprop (TT, {'tp', 'vp'}, {'table', 'variable'}); %! TT.Properties.CustomProperties.tp = 'table scoped'; %! TT.Properties.CustomProperties.vp = {'u', 'v'}; %! T = timetable2table (TT); %! assert_equal (T.Properties.VariableUnits, {'', 'm', ''}); %! assert_equal (T.Properties.VariableDescriptions, {'', 'first', 'second'}); %! assert_equal (T.Properties.Description, 'a description'); ## Test a variable-scoped custom property gains an empty entry for the times %!test %! t = datetime (2024, 6, 1) + hours (0:3)'; %! TT = timetable (t, (1:4)', {'p'; 'q'; 'r'; 's'}, ... %! 'VariableNames', {'a', 'b'}); %! TT.Properties.DimensionNames = {'Stamp', 'Vars'}; %! TT.Properties.VariableUnits = {'m', ''}; %! TT.Properties.VariableDescriptions = {'first', 'second'}; %! TT.Properties.VariableContinuity = {'continuous', 'unset'}; %! TT.Properties.Description = 'a description'; %! TT = addprop (TT, {'tp', 'vp'}, {'table', 'variable'}); %! TT.Properties.CustomProperties.tp = 'table scoped'; %! TT.Properties.CustomProperties.vp = {'u', 'v'}; %! v = timetable2table (TT).Properties.CustomProperties.vp; %! assert_equal (numel (v), 3); %! assert_equal (isempty (v{1}), true); %! assert_equal (class (v{1}), 'double'); %! assert_equal (v(2:3), {'u', 'v'}); %! assert_equal (timetable2table (TT).Properties.CustomProperties.tp, ... %! 'table scoped'); ## Test the trip back restores everything it took %!test %! t = datetime (2024, 6, 1) + hours (0:3)'; %! TT = timetable (t, (1:4)', {'p'; 'q'; 'r'; 's'}, ... %! 'VariableNames', {'a', 'b'}); %! TT.Properties.DimensionNames = {'Stamp', 'Vars'}; %! TT.Properties.VariableUnits = {'m', ''}; %! TT.Properties.VariableDescriptions = {'first', 'second'}; %! TT.Properties.VariableContinuity = {'continuous', 'unset'}; %! TT.Properties.Description = 'a description'; %! TT = addprop (TT, {'tp', 'vp'}, {'table', 'variable'}); %! TT.Properties.CustomProperties.tp = 'table scoped'; %! TT.Properties.CustomProperties.vp = {'u', 'v'}; %! B = table2timetable (timetable2table (TT)); %! assert_equal (class (B), 'timetable'); %! assert_equal (B.Properties.VariableNames, {'a', 'b'}); %! assert_equal (B.Properties.DimensionNames, {'Stamp', 'Vars'}); %! assert_equal (B.Properties.VariableUnits, {'m', ''}); %! assert_equal (B.Properties.VariableDescriptions, {'first', 'second'}); %! assert_equal (cellstr (B.Properties.VariableContinuity), ... %! {'continuous', 'unset'}); %! assert_equal (B.Properties.Description, 'a description'); %! assert_equal (B.Properties.CustomProperties.tp, 'table scoped'); %! assert_equal (B.Properties.CustomProperties.vp, {'u', 'v'}); %! assert_equal (B.Properties.TimeStep, hours (1)); %! assert_equal (B.Properties.RowTimes, t); ## Test the round trip gives back what it was given %!test %! t = datetime (2024, 6, 1) + hours (0:3)'; %! TT = timetable (t, (1:4)', {'p'; 'q'; 'r'; 's'}, ... %! 'VariableNames', {'a', 'b'}); %! TT.Properties.DimensionNames = {'Stamp', 'Vars'}; %! TT.Properties.VariableUnits = {'m', ''}; %! TT.Properties.VariableDescriptions = {'first', 'second'}; %! TT.Properties.VariableContinuity = {'continuous', 'unset'}; %! TT.Properties.Description = 'a description'; %! TT = addprop (TT, {'tp', 'vp'}, {'table', 'variable'}); %! TT.Properties.CustomProperties.tp = 'table scoped'; %! TT.Properties.CustomProperties.vp = {'u', 'v'}; %! assert_equal (isequal (table2timetable (timetable2table (TT)), TT), true); ## Test 'ConvertRowTimes' false drops the times and keeps the rest %!test %! t = datetime (2024, 6, 1) + hours (0:3)'; %! TT = timetable (t, (1:4)', {'p'; 'q'; 'r'; 's'}, ... %! 'VariableNames', {'a', 'b'}); %! TT.Properties.DimensionNames = {'Stamp', 'Vars'}; %! TT.Properties.VariableUnits = {'m', ''}; %! TT.Properties.VariableDescriptions = {'first', 'second'}; %! TT.Properties.VariableContinuity = {'continuous', 'unset'}; %! TT.Properties.Description = 'a description'; %! TT = addprop (TT, {'tp', 'vp'}, {'table', 'variable'}); %! TT.Properties.CustomProperties.tp = 'table scoped'; %! TT.Properties.CustomProperties.vp = {'u', 'v'}; %! N = timetable2table (TT, 'ConvertRowTimes', false); %! assert_equal (N.Properties.VariableNames, {'a', 'b'}); %! assert_equal (N.Properties.DimensionNames, {'Row', 'Vars'}); %! assert_equal (height (N), 4); ## Test a zoned row time comes back with its zone %!test %! ny = 'America/New_York'; %! Z = timetable (datetime (2024, 6, 1, 'TimeZone', ny) + hours (0:2)', ... %! (1:3)'); %! B = table2timetable (timetable2table (Z)); %! assert_equal (B.Properties.RowTimes.TimeZone, ny); %! assert_equal (isequal (B, Z), true); ## Test duration row times come back whole, format included %!test %! D = timetable (hours (0:3)', (1:4)'); %! B = table2timetable (timetable2table (D)); %! assert_equal (seconds (B.Properties.RowTimes)', [0, 3600, 7200, 10800]); %! assert_equal (B.Properties.RowTimes.Format, 'h'); %! assert_equal (isequal (B, D), true); ## A step given at construction does not survive the trip: the table keeps ## no step, so the timetable reads one off the row times it is given and a ## subset too short to imply one has nothing to fall back on. %!test %! t = datetime (2024, 6, 1) + hours (0:3)'; %! F = timetable ((1:4)', 'TimeStep', hours (1), 'StartTime', t(1)); %! B = table2timetable (timetable2table (F)); %! assert_equal (B.Properties.TimeStep, hours (1)); %! assert_equal (F(1, :).Properties.TimeStep, hours (1)); %! assert_equal (isnan (B(1, :).Properties.TimeStep), true); ## Test the event table does not survive, a table having nowhere to put one %!test %! t = datetime (2024, 6, 1) + hours (0:3)'; %! E = timetable (t, (1:4)', 'VariableNames', {'v'}); %! E.Properties.Events = eventtable (timetable (t([2, 3]), {'x'; 'y'}, ... %! 'VariableNames', {'L'})); %! B = table2timetable (timetable2table (E)); %! assert_equal (class (B.Properties.Events), 'double'); %! assert_equal (isempty (B.Properties.Events), true); ################################################################################ ## ** Row time shapes ** ## ################################################################################ ## The row times need not be unique, sorted or present, and each of those ## merely makes the timetable irregular. Every expectation below was ## measured on MATLAB R2024a. ## Test duplicated row times leave the timetable irregular but whole %!test %! d0 = datetime (2024, 6, 1); %! DUP = timetable (d0 + hours ([0; 1; 1; 2; 3]), (1:5)'); %! assert_equal (height (DUP), 5); %! assert_equal (isnan (DUP.Properties.TimeStep), true); %! assert_equal (isregular (DUP), false); %! assert_equal (issorted (DUP), true); %! assert_equal (height (unique (DUP)), 5); %! assert_equal (DUP.Properties.RowTimes, sortrows (DUP).Properties.RowTimes); ## Test a row time naming two rows selects both %!test %! d0 = datetime (2024, 6, 1); %! DUP = timetable (d0 + hours ([0; 1; 1; 2; 3]), (1:5)'); %! assert_equal (height (DUP(d0 + hours (1), :)), 2); %! assert_equal (height (DUP(timerange (d0, d0 + hours (2)), :)), 3); ## Test descending row times are regular, with a step that says so %!test %! d0 = datetime (2024, 6, 1); %! DESC = timetable (d0 + hours ([5; 4; 3; 2; 1; 0]), (1:6)'); %! assert_equal (seconds (DESC.Properties.TimeStep), -3600); %! assert_equal (DESC.Properties.SampleRate, -1/3600); %! assert_equal (isregular (DESC), true); %! assert_equal (issorted (DESC), false); %! assert_equal (sortrows (DESC).Properties.RowTimes, d0 + hours (0:5)'); %! assert_equal (height (unique (DESC)), 6); ## Test row times out of order leave no step to read %!test %! d0 = datetime (2024, 6, 1); %! SHUF = timetable (d0 + hours ([0; 2; 1; 3]), (1:4)'); %! assert_equal (isnan (SHUF.Properties.TimeStep), true); %! assert_equal (isregular (SHUF), false); %! assert_equal (issorted (SHUF), false); %! assert_equal (sortrows (SHUF).Properties.RowTimes, d0 + hours (0:3)'); ## Test a missing row time is kept, and sorts last %!test %! d0 = datetime (2024, 6, 1); %! NAT = timetable ([d0; d0 + hours(1); NaT; d0 + hours(3)], (1:4)'); %! assert_equal (height (NAT), 4); %! assert_equal (isregular (NAT), false); %! assert_equal (issorted (NAT), false); %! assert_equal (ismissing (NAT.Properties.RowTimes)', ... %! [false, false, true, false]); %! assert_equal (ismissing (sortrows (NAT).Properties.RowTimes)', ... %! [false, false, false, true]); %! assert_equal (height (unique (NAT)), 4); ## Test the row whose time is missing goes with 'rmmissing' %!test %! d0 = datetime (2024, 6, 1); %! NAT = timetable ([d0; d0 + hours(1); NaT; d0 + hours(3)], (1:4)'); %! assert_equal (height (rmmissing (NAT)), 3); %! assert_equal (height (NAT(timerange (d0, d0 + hours (3)), :)), 2); ## Test one row implies no spacing of its own %!test %! ONE = timetable (datetime (2024, 6, 1), 1); %! assert_equal (height (ONE), 1); %! assert_equal (isnan (ONE.Properties.TimeStep), true); %! assert_equal (isregular (ONE), false); %! assert_equal (issorted (ONE), true); ## Test no rows at all answer without raising %!test %! Z = timetable (datetime.empty (0, 1), zeros (0, 1)); %! assert_equal (height (Z), 0); %! assert_equal (isregular (Z), false); %! assert_equal (issorted (Z), true); %! assert_equal (height (unique (Z)), 0); %! assert_equal (size (Z.Var1), [0, 1]); %! assert_equal (ismissing (Z.Properties.StartTime), true); ## A step given at construction outlives a subset too short to imply one, ## where a step only the row times implied does not. This is the ## fresh-versus-subsetted distinction. %!test %! DECL = timetable ((1:6)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 6, 1)); %! assert_equal (DECL.Properties.TimeStep, hours (1)); %! assert_equal (DECL(1, :).Properties.TimeStep, hours (1)); %! assert_equal (isregular (DECL(1, :)), true); %! assert_equal (DECL([], :).Properties.TimeStep, hours (1)); %! assert_equal (DECL([1, 3], :).Properties.TimeStep, hours (2)); %!test %! FRESH = timetable (datetime (2024, 6, 1) + hours (0:5)', (1:6)'); %! assert_equal (FRESH.Properties.TimeStep, hours (1)); %! assert_equal (isnan (FRESH(1, :).Properties.TimeStep), true); %! assert_equal (isregular (FRESH(1, :)), false); ## Test the start time follows the first row that is left %!test %! DECL = timetable ((1:6)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 6, 1)); %! assert_equal (DECL(2:end, :).Properties.StartTime, ... %! datetime (2024, 6, 1, 1, 0, 0)); %! assert_equal (isregular (DECL, 'days'), false); ## Test the range predicates read every shape %!test %! d0 = datetime (2024, 6, 1); %! R = timerange (d0, d0 + hours (2)); %! DUP = timetable (d0 + hours ([0; 1; 1; 2; 3]), (1:5)'); %! NAT = timetable ([d0; d0 + hours(1); NaT; d0 + hours(3)], (1:4)'); %! DESC = timetable (d0 + hours ([5; 4; 3; 2; 1; 0]), (1:6)'); %! Z = timetable (datetime.empty (0, 1), zeros (0, 1)); %! assert_equal (containsrange (DUP, R), true); %! assert_equal (containsrange (NAT, R), true); %! assert_equal (containsrange (DESC, R), true); %! assert_equal (containsrange (Z, R), false); %! assert_equal (withinrange (DUP, R), false); %! assert_equal (overlapsrange (DESC, R), true); ## Test 'retime' reads each shape the same way MATLAB does %!test %! d0 = datetime (2024, 6, 1); %! DUP = timetable (d0 + hours ([0; 1; 1; 2; 3]), (1:5)'); %! assert_equal (retime (DUP, 'hourly', 'sum').Var1', [1, 5, 4, 5]); %! assert_equal (retime (DUP, 'hourly', 'previous').Var1', [1, 3, 4, 5]); %!test %! d0 = datetime (2024, 6, 1); %! DESC = timetable (d0 + hours ([5; 4; 3; 2; 1; 0]), (1:6)'); %! assert_equal (retime (DESC, 'hourly', 'sum').Var1', [6, 5, 4, 3, 2, 1]); %!test %! ONE = timetable (datetime (2024, 6, 1), 1); %! assert_equal (height (retime (ONE, 'hourly', 'sum')), 1); %! Z = timetable (datetime.empty (0, 1), zeros (0, 1)); %! assert_equal (height (retime (Z, 'hourly', 'sum')), 0); ## Test 'synchronize' reads them too %!test %! d0 = datetime (2024, 6, 1); %! DUP = timetable (d0 + hours ([0; 1; 1; 2; 3]), (1:5)'); %! assert_equal (height (synchronize (DUP, renamevars (DUP, 'Var1', 'z'))), 4); %!test %! d0 = datetime (2024, 6, 1); %! DESC = timetable (d0 + hours ([5; 4; 3; 2; 1; 0]), (1:6)'); %! S = synchronize (DESC, renamevars (DESC, 'Var1', 'z')); %! assert_equal (S.Properties.RowTimes, d0 + hours (0:5)'); %!test %! Z = timetable (datetime.empty (0, 1), zeros (0, 1)); %! ONE = timetable (datetime (2024, 6, 1), 1); %! assert_equal (height (synchronize (Z, renamevars (ONE, 'Var1', 'z'))), 1); ################################################################################ ## ** Return types ** ## ################################################################################ ## What each method hands back, measured on MATLAB R2024a. A method that ## keeps the rows returns a timetable; one whose rows describe something ## else than instants degrades to a table, and the brace and dot references ## degrade further to the variable's own type. ## Test a timetable is tabular without being a table %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', categorical ({'a'; 'b'; 'a'; 'b'; 'a'; 'b'}), ... %! {'p'; 'q'; 'r'; 's'; 't'; 'u'}, ... %! 'VariableNames', {'a', 'g', 's'}); %! assert_equal (istimetable (A), true); %! assert_equal (istable (A), false); %! assert_equal (istabular (A), true); %! assert_equal (isa (A, 'table'), false); %! assert_equal (isa (A, 'tabular'), true); %! assert_equal (isa (A, 'timetable'), true); ## Test the size and shape queries answer with numbers, not objects %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', categorical ({'a'; 'b'; 'a'; 'b'; 'a'; 'b'}), ... %! {'p'; 'q'; 'r'; 's'; 't'; 'u'}, ... %! 'VariableNames', {'a', 'g', 's'}); %! assert_equal (class (height (A)), 'double'); %! assert_equal (class (width (A)), 'double'); %! assert_equal (class (size (A)), 'double'); %! assert_equal (class (numel (A)), 'double'); %! assert_equal (class (ndims (A)), 'double'); %! assert_equal (class (isempty (A)), 'logical'); ## Test concatenation keeps the class, whichever direction and however asked %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! B = timetable (t, (7:12)', 'VariableNames', {'z'}); %! assert_equal (class ([N; N]), 'timetable'); %! assert_equal (class ([N, B]), 'timetable'); %! assert_equal (class (cat (1, N, N)), 'timetable'); %! assert_equal (class (cat (2, N, B)), 'timetable'); ## Test a table stacked onto a timetable comes back a timetable %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! PT = table ((1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (class ([N; PT]), 'timetable'); ## Test the two repetitions keep the class %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', categorical ({'a'; 'b'; 'a'; 'b'; 'a'; 'b'}), ... %! {'p'; 'q'; 'r'; 's'; 't'; 'u'}, ... %! 'VariableNames', {'a', 'g', 's'}); %! assert_equal (class (repelem (A, 2, 1)), 'timetable'); %! assert_equal (class (repmat (A, 2, 1)), 'timetable'); ## Test the variable-management family keeps the class %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', categorical ({'a'; 'b'; 'a'; 'b'; 'a'; 'b'}), ... %! {'p'; 'q'; 'r'; 's'; 't'; 'u'}, ... %! 'VariableNames', {'a', 'g', 's'}); %! assert_equal (class (addvars (A, (1:6)')), 'timetable'); %! assert_equal (class (removevars (A, 'g')), 'timetable'); %! assert_equal (class (movevars (A, 'a', 'After', 'g')), 'timetable'); %! assert_equal (class (renamevars (A, 'a', 'q')), 'timetable'); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (class (convertvars (N, 'a', 'single')), 'timetable'); %! assert_equal (class (mergevars (N, {'a', 'b'})), 'timetable'); %! assert_equal (class (splitvars (mergevars (N, {'a', 'b'}))), 'timetable'); ## Test the row-selecting methods keep the class %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', categorical ({'a'; 'b'; 'a'; 'b'; 'a'; 'b'}), ... %! {'p'; 'q'; 'r'; 's'; 't'; 'u'}, ... %! 'VariableNames', {'a', 'g', 's'}); %! assert_equal (class (head (A, 2)), 'timetable'); %! assert_equal (class (tail (A, 2)), 'timetable'); ## Test a subscript that keeps rows keeps the class %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', categorical ({'a'; 'b'; 'a'; 'b'; 'a'; 'b'}), ... %! {'p'; 'q'; 'r'; 's'; 't'; 'u'}, ... %! 'VariableNames', {'a', 'g', 's'}); %! assert_equal (class (A(1:2, :)), 'timetable'); %! assert_equal (class (A(:, 1)), 'timetable'); %! assert_equal (class (A(1, 1)), 'timetable'); ## Test a brace reference degrades to the values themselves %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (class (N{:, 1}), 'double'); %! assert_equal (class (N{:, :}), 'double'); ## Test a dot reference gives the variable its own type %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', categorical ({'a'; 'b'; 'a'; 'b'; 'a'; 'b'}), ... %! {'p'; 'q'; 'r'; 's'; 't'; 'u'}, ... %! 'VariableNames', {'a', 'g', 's'}); %! assert_equal (class (A.a), 'double'); %! assert_equal (class (A.g), 'categorical'); %! assert_equal (class (A.s), 'cell'); ## Test the row time properties carry their own types %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', categorical ({'a'; 'b'; 'a'; 'b'; 'a'; 'b'}), ... %! {'p'; 'q'; 'r'; 's'; 't'; 'u'}, ... %! 'VariableNames', {'a', 'g', 's'}); %! assert_equal (class (A.Properties.RowTimes), 'datetime'); %! assert_equal (class (A.Properties.StartTime), 'datetime'); %! assert_equal (class (A.Properties.TimeStep), 'duration'); %! assert_equal (class (A.Properties.SampleRate), 'double'); ## Test duration row times give a duration everywhere a datetime gives one %!test %! D = timetable (hours (0:5)', (1:6)', 'VariableNames', {'a'}); %! assert_equal (class (D.Properties.RowTimes), 'duration'); %! assert_equal (class (D.Properties.StartTime), 'duration'); %! assert_equal (class (D.Properties.TimeStep), 'duration'); ## Test duration row times keep the class through the same methods %!test %! D = timetable (hours (0:5)', (1:6)', 'VariableNames', {'a'}); %! assert_equal (class ([D; D]), 'timetable'); %! assert_equal (class (sortrows (D)), 'timetable'); %! assert_equal (class (head (D, 2)), 'timetable'); %! assert_equal (class (retime (D, 'hourly', 'previous')), 'timetable'); ## Test they degrade the same way too %!test %! D = timetable (hours (0:5)', (1:6)', categorical ({'a'; 'b'; 'a'; 'b'; ... %! 'a'; 'b'}), 'VariableNames', {'a', 'g'}); %! assert_equal (class (timetable2table (D)), 'table'); %! assert_equal (class (groupcounts (D, 'g')), 'table'); %! assert_equal (class (summary (D)), 'struct'); ## Test the predicates answer with a logical %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (class (isregular (N)), 'logical'); %! assert_equal (class (isequal (N, N)), 'logical'); %! assert_equal (class (isequaln (N, N)), 'logical'); %! assert_equal (class (ismissing (N)), 'logical'); %! assert_equal (class (anymissing (N)), 'logical'); %! assert_equal (class (issorted (N)), 'logical'); %! assert_equal (class (issortedrows (N)), 'logical'); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', 'VariableNames', {'a'}); %! R = timerange (t(2), t(4)); %! assert_equal (class (overlapsrange (N, R)), 'logical'); %! assert_equal (class (withinrange (N, R)), 'logical'); %! [tf, w] = containsrange (N, R); %! assert_equal (class (tf), 'logical'); %! assert_equal (class (w), 'logical'); ## Test the missing-data family keeps the class, and says so alongside %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (class (standardizeMissing (N, 1)), 'timetable'); %! [B, TF] = rmmissing (N); %! assert_equal (class (B), 'timetable'); %! assert_equal (class (TF), 'logical'); %! [C, TF2] = fillmissing (N, 'previous'); %! assert_equal (class (C), 'timetable'); %! assert_equal (class (TF2), 'logical'); ## Test 'summary' answers with a struct and not a tabular object %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (class (summary (N)), 'struct'); ## Test the ordering family keeps the class and indexes with doubles %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! [B, ix] = sortrows (N, 'a'); %! assert_equal (class (B), 'timetable'); %! assert_equal (class (ix), 'double'); %! [C, ia, ic] = unique (N); %! assert_equal (class (C), 'timetable'); %! assert_equal (class (ia), 'double'); %! assert_equal (class (ic), 'double'); %! [D, ixk] = topkrows (N, 2, 'a'); %! assert_equal (class (D), 'timetable'); %! assert_equal (class (ixk), 'double'); ## Test an apply that keeps the rows keeps the class %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (class (varfun (@mean, N)), 'timetable'); %! assert_equal (class (rowfun (@(x, y) x + y, N)), 'timetable'); ## Test 'OutputFormat' answers with what it names %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (class (varfun (@mean, N, 'OutputFormat', 'uniform')), ... %! 'double'); %! assert_equal (class (varfun (@mean, N, 'OutputFormat', 'cell')), 'cell'); ## Test the grouping family degrades where its rows describe groups %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! G = timetable (t, categorical ({'a'; 'b'; 'a'; 'b'; 'a'; 'b'}), (1:6)', ... %! 'VariableNames', {'g', 'v'}); %! assert_equal (class (grouptransform (G, 'g', 'zscore')), 'timetable'); %! assert_equal (class (groupfilter (G, 'g', @(x) x > 0)), 'timetable'); %! assert_equal (class (groupcounts (G, 'g')), 'table'); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (class (groupsummary (N, 'a', 'sum', 'b')), 'table'); %! assert_equal (class (pivot (timetable (t, categorical ({'a'; 'b'; 'a'; ... %! 'b'; 'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', 'g')), ... %! 'table'); %! assert_equal (class (rows2vars (N)), 'table'); ## Test 'findgroups' numbers with doubles and describes with a table %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! G = timetable (t, categorical ({'a'; 'b'; 'a'; 'b'; 'a'; 'b'}), (1:6)', ... %! 'VariableNames', {'g', 'v'}); %! [gr, TID] = findgroups (G(:, {'g'})); %! assert_equal (class (gr), 'double'); %! assert_equal (class (TID), 'table'); %! assert_equal (class (splitapply (@(x, y) sum (x), ... %! timetable (t, (1:6)', (11:16)'), ones (6, 1))), 'double'); ## Test the reshaping and joining family keeps the class %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! [S, ixs] = stack (N, {'a', 'b'}); %! assert_equal (class (S), 'timetable'); %! assert_equal (class (ixs), 'double'); %! assert_equal (class (inner2outer (N)), 'timetable'); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! B = timetable (t, (7:12)', 'VariableNames', {'z'}); %! [C, ix] = join (N, B); %! assert_equal (class (C), 'timetable'); %! assert_equal (class (ix), 'double'); %! [E, iL, iR] = innerjoin (N, B); %! assert_equal (class (E), 'timetable'); %! assert_equal (class (iL), 'double'); %! assert_equal (class (iR), 'double'); %! [F, jL, jR] = outerjoin (N, B); %! assert_equal (class (F), 'timetable'); %! assert_equal (class (jL), 'double'); %! assert_equal (class (jR), 'double'); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! G = timetable (t, categorical ({'a'; 'b'; 'a'; 'b'; 'a'; 'b'}), (1:6)', ... %! 'VariableNames', {'g', 'v'}); %! [U, ixu] = unstack (G, 'v', 'g'); %! assert_equal (class (U), 'timetable'); %! assert_equal (class (ixu), 'double'); ## Test the row time family keeps the class %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (class (retime (N, 'hourly', 'previous')), 'timetable'); %! assert_equal (class (retime (N, 'daily', 'sum')), 'timetable'); %! assert_equal (class (lag (N, 1)), 'timetable'); %! assert_equal (class (synchronize (N, timetable (t, (7:12)', ... %! 'VariableNames', {'z'}))), 'timetable'); ## Test the property family keeps it, and 'timetable2table' does not %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! N = timetable (t, (1:6)', (11:16)', 'VariableNames', {'a', 'b'}); %! assert_equal (class (addprop (N, {'q'}, {'table'})), 'timetable'); %! assert_equal (class (rmprop (addprop (N, {'q'}, {'table'}), {'q'})), ... %! 'timetable'); %! assert_equal (class (timetable2table (N)), 'table'); %! assert_equal (class (timetable.empty (0, 2)), 'timetable'); ## Test the event methods answer with an eventtable and a timetable %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', 'VariableNames', {'a'}); %! [ET, TT2] = extractevents (A, [2; 4]); %! assert_equal (class (ET), 'eventtable'); %! assert_equal (class (TT2), 'timetable'); %!test %! t = datetime (2024, 6, 1) + hours (0:5)'; %! A = timetable (t, (1:6)', 'VariableNames', {'a'}); %! A.Properties.Events = eventtable (timetable (t([2, 4]), {'x'; 'y'}, ... %! 'VariableNames', {'L'})); %! assert_equal (class (A.Properties.Events), 'eventtable'); %! assert_equal (class (syncevents (A)), 'timetable'); ################################################################################ ## ** Input validation ** ## ################################################################################ ## Test the row times must be given %!error ... %! timetable ((1:3)'); ## Test the row times are given only once %!error ... %! timetable (hours (0:2)', (1:3)', 'TimeStep', hours (1)); ## Test only one way of saying when the rows are may be used %!error ... %! timetable ((1:3)', 'RowTimes', hours (0:2)', 'TimeStep', hours (1)); ## Test the row times must be a datetime or a duration %!error ... %! timetable ((1:3)', 'RowTimes', (1:3)'); ## Test the row times must be a vector %!error ... %! timetable ((1:4)', 'RowTimes', hours (reshape (0:3, 2, 2))); ## Test the row times must match the number of rows %!error ... %! timetable ((1:3)', 'RowTimes', hours (0:4)'); ## Test the time step must be a duration or a calendarDuration %!error ... %! timetable ((1:3)', 'TimeStep', 1); ## Test the time step must be a scalar %!error ... %! timetable ((1:3)', 'TimeStep', hours (1:2)); ## Test a calendar step names a single calendar unit %!error ... %! timetable ((1:3)', 'TimeStep', calmonths (1) + caldays (1), ... %! 'StartTime', datetime (2024, 1, 1)); ## Test a calendar step carries no time of day %!error ... %! timetable ((1:3)', 'TimeStep', caldays (1) + hours (2), ... %! 'StartTime', datetime (2024, 1, 1)); ## Test a calendar step needs a datetime start time %!error ... %! timetable ((1:3)', 'TimeStep', calmonths (1)); ## Test the start time must be a datetime or a duration %!error ... %! timetable ((1:3)', 'TimeStep', hours (1), 'StartTime', 0); ## Test the start time must be a scalar %!error ... %! timetable ((1:3)', 'TimeStep', hours (1), 'StartTime', hours (0:1)); ## Test the sample rate must be numeric %!error ... %! timetable ((1:3)', 'SampleRate', 'fast'); ## Test 'VariableNames' must be text %!error ... %! timetable ((1:3)', 'TimeStep', hours (1), 'VariableNames', 5); ## Test 'VariableNames' must be nonempty %!error ... %! timetable ((1:3)', 'TimeStep', hours (1), 'VariableNames', {''}); ## Test 'DimensionNames' must have two elements %!error ... %! timetable ((1:3)', 'TimeStep', hours (1), 'DimensionNames', {'One'}); ## Test 'DimensionNames' cannot use a reserved name %!error ... %! timetable ((1:3)', 'TimeStep', hours (1), ... %! 'DimensionNames', {'RowTimes', 'Variables'}); ## Test a dimension name cannot repeat a variable name %!error ... %! timetable ((1:3)', 'TimeStep', hours (1), 'VariableNames', {'v'}, ... %! 'DimensionNames', {'v', 'Variables'}); ## Test duplicate variable names are refused %!error ... %! timetable ((1:3)', (4:6)', 'TimeStep', hours (1), ... %! 'VariableNames', {'b', 'b'}); ## Test the number of variable names must match the number of variables %!error ... %! timetable ((1:3)', (4:6)', 'TimeStep', hours (1), 'VariableNames', {'a'}); ## Test variables must agree on their number of rows %!error ... %! timetable ((1:3)', (1:2)', 'TimeStep', hours (1)); ## Test a variable may not have more than two dimensions %!error ... %! timetable (hours (1), @sin); %!error ... %! timetable (hours (1), dictionary (1, 2)); %!error ... %! timetable (hours (1), timeseries (5)); %!error ... %! timetable (ones (2, 2, 2), 'TimeStep', hours (1)); ## Test a later variable may not have more than two dimensions %!error ... %! timetable ((1:2)', ones (2, 2, 2), 'TimeStep', hours (1)); ## Test 'Size' must be a two-element numeric vector %!error ... %! timetable ('Size', [3], 'VariableTypes', {'double'}, 'TimeStep', hours (1)); ## Test 'VariableTypes' must match the number of variables asked for %!error ... %! timetable ('Size', [3, 2], 'VariableTypes', {'double'}, ... %! 'TimeStep', hours (1)); ## Test 'VariableNames' must match 'VariableTypes' when preallocating %!error ... %! timetable ('Size', [3, 2], 'VariableTypes', {'double', 'double'}, ... %! 'TimeStep', hours (1), 'VariableNames', {'a'}); ## Test an unsupported variable type is refused %!error ... %! timetable ('Size', [3, 1], 'VariableTypes', {'nosuchtype'}, ... %! 'TimeStep', hours (1)); ## Test a nested timetable variable is refused %!error ... %! timetable ('Size', [3, 1], 'VariableTypes', {'timetable'}, ... %! 'TimeStep', hours (1)); ## Test 'isregular' refuses a unit it does not know %!error ... %! isregular (timetable ((1:3)', 'TimeStep', hours (1)), 'fortnights'); ## Test 'isregular' has no unit smaller than a day %!error ... %! isregular (timetable ((1:3)', 'TimeStep', hours (1)), 'hours'); ## Test 'isregular' refuses a unit that is not text %!error ... %! isregular (timetable ((1:3)', 'TimeStep', hours (1)), 5); ## Test assigning row times of the wrong length is refused %!error ... %! TT = timetable ((1:3)', 'TimeStep', hours (1)); ... %! TT.Properties.RowTimes = hours (0:5)'; ## Test assigning a calendar step to a duration-keyed timetable is refused %!error ... %! TT = timetable ((1:3)', 'TimeStep', hours (1)); ... %! TT.Properties.TimeStep = calmonths (1); ## Test an unknown property is refused %!error ... %! TT = timetable (hours (1), 1); TT.f = @sin; %!error ... %! TT = timetable (hours (1), 1); TT.c = tscollection (); %!error ... %! TT = timetable ((1:3)', 'TimeStep', hours (1)); ... %! TT.Properties.Nope = 1; ## Test a row time subscript of the wrong type is refused %!error ... %! TT = timetable ((1:3)', 'TimeStep', hours (1)); ... %! TT(datetime (2024, 1, 1),:); ## Test vertical concatenation requires identical variable names %!error ... %! tv = datetime (2024, 1, 1) + hours (0:2)'; ... %! vertcat (timetable (tv, (1:3)', 'VariableNames', {'A'}), ... %! timetable (tv, (4:6)', 'VariableNames', {'B'})); ## Test a table put before a timetable is refused, by 'table' since ## concatenation dispatches on the first operand %!error ... %! tv = datetime (2024, 1, 1) + hours (0:2)'; ... %! vertcat (table ((4:6)', 'VariableNames', {'A'}), ... %! timetable (tv, (1:3)', 'VariableNames', {'A'})); ## Test concatenation refuses an input that is not tabular %!error ... %! tv = datetime (2024, 1, 1) + hours (0:2)'; ... %! vertcat (timetable (tv, (1:3)', 'VariableNames', {'A'}), (1:3)'); %!error ... %! tv = datetime (2024, 1, 1) + hours (0:2)'; ... %! horzcat (timetable (tv, (1:3)', 'VariableNames', {'A'}), (1:3)'); ## Test horizontal concatenation requires unique variable names %!error ... %! tv = datetime (2024, 1, 1) + hours (0:2)'; ... %! horzcat (timetable (tv, (1:3)', 'VariableNames', {'A'}), ... %! timetable (tv, (4:6)', 'VariableNames', {'A'})); ## Test horizontal concatenation requires the same number of rows %!error ... %! tv = datetime (2024, 1, 1) + hours (0:2)'; ... %! horzcat (timetable (tv, (1:3)', 'VariableNames', {'A'}), ... %! timetable (tv(1:2), (4:5)', 'VariableNames', {'B'})); ## Test horizontal concatenation requires the same row times %!error ... %! tv = datetime (2024, 1, 1) + hours (0:2)'; ... %! horzcat (timetable (tv, (1:3)', 'VariableNames', {'A'}), ... %! timetable (tv + hours (9), (4:6)', 'VariableNames', {'B'})); ## Test 'cat' has no third dimension %!error ... %! tv = datetime (2024, 1, 1) + hours (0:2)'; ... %! V = timetable (tv, (1:3)', 'VariableNames', {'A'}); ... %! cat (3, V, V); ## Test a row subscript that is not a time is refused %!error ... %! TT = timetable ((1:3)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 1, 1)); ... %! TT('nonsense',:); ## Test a row subscript given as text carrying an offset is read in the zone of ## the row times, and refused when they have none %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1), 'StartTime', ... %! datetime (2024, 3, 10, 2, 0, 0, 'TimeZone', 'UTC'), ... %! 'VariableNames', {'A'}); %! R = TT('2024-03-09T22:00:00-05:00',:); %! assert_equal (R.A, 2); %!error ... %! TT = timetable ((1:3)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 1, 1)); ... %! TT('2024-03-09T22:00:00Z',:); ## A row time is matched against the times there are, not read as a position, ## so a time the timetable does not carry matches nothing rather than raising. ## Assigning to one asks for the row and adds it. Measured on MATLAB R2024a. ## Test reading a row time that matches no row gives no rows %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1)); %! assert_equal (height (TT(hours (9), :)), 0); %! assert_equal (size (TT{hours (9), 'Var1'}), [0, 1]); ## Test a reference that matches some of its times keeps only those %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1)); %! R = TT([hours(0); hours(9)], :); %! assert_equal (height (R), 1); %! assert_equal (R.Var1, 1); ## Test a tolerant match that reaches no row gives no rows %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1)); %! assert_equal (height (TT(withtol (hours (9), minutes (1)), :)), 0); ## Test assigning at an unmatched time appends the row at the end %!test %! TT = timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours ((0:2)')); %! TT(datetime (2024, 1, 2), :) = {9}; %! assert_equal (height (TT), 4); %! assert_equal (TT.Properties.RowTimes(4), datetime (2024, 1, 2)); %! assert_equal (TT.Var1, [1; 2; 3; 9]); ## Test an appended row leaves the step to be read afresh %!test %! TT = timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 1, 1) + hours ((0:2)')); %! TT(datetime (2024, 1, 2), :) = {9}; %! assert_equal (isregular (TT), false); %! assert_equal (ismissing (TT.Properties.TimeStep), true); ## Test a row appended on the grid keeps the timetable regular %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 1, 1)); %! TT(datetime (2024, 1, 1, 3, 0, 0), :) = {9}; %! assert_equal (height (TT), 4); %! assert_equal (isregular (TT), true); %! assert_equal (TT.Properties.TimeStep, hours (1)); ## Test a row appended off the grid does not %!test %! TT = timetable ((1:3)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 1, 1)); %! TT(datetime (2024, 1, 2), :) = {9}; %! assert_equal (isregular (TT), false); ## Test a row is added where the reference named it, the others assigned %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable ((1:3)', 'RowTimes', t); %! TT([t(1); datetime(2024, 1, 2)], :) = {7; 8}; %! assert_equal (TT.Var1, [7; 2; 3; 8]); %! assert_equal (TT.Properties.RowTimes(4), datetime (2024, 1, 2)); ## Test two unmatched times add two rows, in the order they were named %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable ((1:3)', 'RowTimes', t); %! TT([datetime(2024, 1, 2); datetime(2024, 1, 3)], :) = {7; 8}; %! assert_equal (TT.Var1, [1; 2; 3; 7; 8]); %! assert_equal (TT.Properties.RowTimes(4:5), ... %! [datetime(2024, 1, 2); datetime(2024, 1, 3)]); ## Test a time named twice assigns twice, the last value standing %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable ((1:3)', 'RowTimes', t); %! TT([t(1); t(1)], :) = {7; 8}; %! assert_equal (height (TT), 3); %! assert_equal (TT.Var1, [8; 2; 3]); ## Test duration row times take an appended row the same way %!test %! TT = timetable ((1:3)', 'RowTimes', seconds ((0:2)')); %! TT(seconds (1.5), :) = {9}; %! assert_equal (seconds (TT.Properties.RowTimes), [0; 1; 2; 1.5]); %! assert_equal (TT.Var1, [1; 2; 3; 9]); %! assert_equal (issorted (TT), false); ## Test deleting a row re-reads the time step from what is left %!test %! TT = timetable ((1:6)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 1, 1)); %! R = TT; R(1,:) = []; %! assert_equal (height (R), 5); %! assert_equal (R.Properties.TimeStep, hours (1)); %! assert_equal (isregular (R), true); %! R = TT; R(3,:) = []; %! assert_equal (height (R), 5); %! assert_equal (isregular (R), false); ## Test deleting every variable keeps the row times and the height %!test %! TT = timetable ((1:6)', (11:16)', 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 1, 1), ... %! 'VariableNames', {'A', 'B'}); %! R = TT; R(:,[1, 2]) = []; %! assert_equal (size (R), [6, 0]); %! assert_equal (numel (R.Properties.RowTimes), 6); %! assert_equal (isregular (R), true); %! assert_equal (size (R(2:3,:)), [2, 0]); ## Test a variable goes by dot assignment on a timetable too %!test %! TT = timetable ((1:3)', (4:6)', 'TimeStep', hours (1), ... %! 'VariableNames', {'A', 'B'}); %! TT.A = []; %! assert_equal (TT.Properties.VariableNames, {'B'}); %! assert_equal (height (TT), 3); ## Test '()' creates a variable on a timetable, whose row times must be ## those of the rows assigned when a timetable is assigned %!test %! E = timetable (hours ((0:2)'), [1; 2; 3], 'VariableNames', {'a'}); %! E(:,'z') = {4; 5; 6}; %! assert_equal (E.z, [4; 5; 6]); %!test %! E = timetable (hours ((0:2)'), [1; 2; 3], 'VariableNames', {'a'}); %! E(:,'z') = timetable (hours ((0:2)'), [4; 5; 6]); %! assert_equal (E.z, [4; 5; 6]); %!error ... %! E = timetable (hours ((0:2)'), [1; 2; 3], 'VariableNames', {'a'}); %! E(:,'z') = timetable (hours ((1:3)'), [4; 5; 6]); %!test %! warning ('off', 'all', 'local'); %! E = timetable (hours ((0:1)'), hours ([1; 2]), 'VariableNames', {'d'}); %! E(3,'z') = {5}; %! assert_equal (E.d, hours ([1; 2; 0])); %!error ... %! E = timetable (hours ((0:2)'), [1; 2; 3], 'VariableNames', {'a'}); %! E(:,'a') = timetable (hours ([2; 1; 0]), [4; 5; 6]); %!error ... %! E = timetable (hours ((0:2)'), [1; 2; 3], 'VariableNames', {'a'}); %! E(:,'a') = timetable (datetime (2024, 1, 1) + hours ((0:2)'), [4; 5; 6]); %!error ... %! E = timetable (hours ((0:2)'), [1; 2; 3], 'VariableNames', {'a'}); %! E(4,'a') = timetable (hours (3), 7); %!error ... %! E = timetable (hours ((0:2)'), [1; 2; 3], 'VariableNames', {'a'}); %! E(:,3) = {4; 5; 6}; ## Test '{}' assigns contents on a timetable and grows it with missing row ## times, or along a declared step %!test %! E = timetable (hours ((0:2)'), [1; 2; 3], 'VariableNames', {'a'}); %! E{2,'a'} = 9; %! assert_equal (E.a, [1; 9; 3]); %! E{4,'a'} = 7; %! assert_equal (hours (E.Properties.RowTimes), [0; 1; 2; NaN]); %!test %! E = timetable ([1; 2; 3], 'VariableNames', {'a'}, 'TimeStep', hours (1), ... %! 'StartTime', datetime (2024, 1, 1)); %! E{4,'a'} = 9; %! assert_equal (E.Properties.RowTimes(4), datetime (2024, 1, 1, 3, 0, 0)); %!error ... %! E = timetable (hours ((0:2)'), [1; 2; 3]); %! E{2,:} = []; ## Test the row times of a timetable cannot be deleted %!error ... %! TT = timetable ((1:3)', 'TimeStep', hours (1)); ... %! TT.Time = []; ## Test a calendar step names the coarsest whole unit it fills %!test %! t0 = datetime (2024, 1, 1); %! W = timetable (t0 + days ((0:3)' * 7), (1:4)'); %! assert_equal (char (string (W.Properties.TimeStep)), '1w'); %! assert_equal (caldays (W.Properties.TimeStep), 7); %! Q = timetable (t0 + calmonths ((0:3)' * 3), (1:4)'); %! assert_equal (char (string (Q.Properties.TimeStep)), '1q'); %! assert_equal (calmonths (Q.Properties.TimeStep), 3); ## Test a step filling no coarser unit keeps the one caldiff gives it %!test %! t0 = datetime (2024, 1, 1); %! D = timetable (t0 + days (0:3)', (1:4)'); %! assert_equal (char (string (D.Properties.TimeStep)), '1d'); %! M = timetable (t0 + calmonths ((0:3)'), (1:4)'); %! assert_equal (char (string (M.Properties.TimeStep)), '1mo'); %! Y = timetable (t0 + calmonths ((0:3)' * 12), (1:4)'); %! assert_equal (char (string (Y.Properties.TimeStep)), '1y'); ## Test 'timetable.empty' takes both dimensions, a size vector, or neither %!test %! assert_equal (size (timetable.empty), [0, 0]); %! assert_equal (size (timetable.empty (5, 0)), [5, 0]); %! assert_equal (height (timetable.empty (5, 0)), 5); %! assert_equal (size (timetable.empty ([5, 0])), [5, 0]); %! assert_equal (class (timetable.empty), 'timetable'); ## Test the row times of an empty timetable are missing, one per row %!test %! TT = timetable.empty (5, 0); %! assert_equal (numel (TT.Properties.RowTimes), 5); %! assert_equal (all (ismissing (TT.Properties.RowTimes)), true); ## Test 'timetable.empty' names and types the variables it is asked for %!test %! TT = timetable.empty (0, 3); %! assert_equal (size (TT), [0, 3]); %! assert_equal (TT.Properties.VariableNames, {'Var1', 'Var2', 'Var3'}); ## Test input validation for 'timetable.empty' %!error ... %! timetable.empty (5, 3); %!error ... %! timetable.empty (5, 0, 2); %!error ... %! timetable.empty ({1, 0}); %!error ... %! timetable.empty (5, 'a'); %!error ... %! timetable.empty (-1, 0); ## Test 'retime' onto an explicit time vector holds the previous value %!test %! ta = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (ta, [0; 1; 8; 27], 'VariableNames', {'v'}); %! tq = datetime (2024, 1, 1) + minutes (0:30:180)'; %! assert_equal (retime (TT, tq, 'previous').v, [0; 0; 1; 1; 8; 8; 27]); ## Test 'retime' takes the next value %!test %! ta = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (ta, [0; 1; 8; 27], 'VariableNames', {'v'}); %! tq = datetime (2024, 1, 1) + minutes (0:30:180)'; %! assert_equal (retime (TT, tq, 'next').v, [0; 1; 1; 8; 8; 27; 27]); ## Test 'retime' breaks a tie between two neighbours towards the later %!test %! ta = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (ta, [0; 1; 8; 27], 'VariableNames', {'v'}); %! tq = datetime (2024, 1, 1) + minutes (0:30:180)'; %! assert_equal (retime (TT, tq, 'nearest').v, [0; 1; 1; 8; 8; 27; 27]); ## Test 'retime' interpolates linearly against the row times %!test %! ta = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (ta, [0; 1; 8; 27], 'VariableNames', {'v'}); %! tq = datetime (2024, 1, 1) + minutes (0:30:180)'; %! assert_equal (retime (TT, tq, 'linear').v, [0; 0.5; 1; 4.5; 8; 17.5; 27]); ## Test 'retime' interpolates with a spline %!test %! ta = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (ta, [0; 1; 8; 27], 'VariableNames', {'v'}); %! tq = datetime (2024, 1, 1) + minutes (0:30:180)'; %! v = [0; 0.125; 1; 3.375; 8; 15.625; 27]; %! assert (retime (TT, tq, 'spline').v, v, 1e-12); ## Test 'retime' leaves an added row missing when asked to %!test %! ta = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (ta, [0; 1; 8; 27], 'VariableNames', {'v'}); %! tq = datetime (2024, 1, 1) + minutes (0:30:180)'; %! v = [0; NaN; 1; NaN; 8; NaN; 27]; %! assert_equal (retime (TT, tq, 'fillwithmissing').v, v); ## Test no method at all leaves an added row missing %!test %! ta = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (ta, [0; 1; 8; 27], 'VariableNames', {'v'}); %! tq = datetime (2024, 1, 1) + minutes (0:30:180)'; %! assert_equal (retime (TT, tq).v, [0; NaN; 1; NaN; 8; NaN; 27]); ## Test a target time past the data extrapolates under 'linear' %!test %! tb = datetime (2024, 1, 1) + hours (1:3)'; %! TT = timetable (tb, [10; 20; 40], 'VariableNames', {'v'}); %! tx = datetime (2024, 1, 1) + hours (0:4)'; %! assert_equal (retime (TT, tx, 'linear').v, [0; 10; 20; 40; 60]); ## Test 'previous' has nothing before the data and carries the last forward %!test %! tb = datetime (2024, 1, 1) + hours (1:3)'; %! TT = timetable (tb, [10; 20; 40], 'VariableNames', {'v'}); %! tx = datetime (2024, 1, 1) + hours (0:4)'; %! assert_equal (retime (TT, tx, 'previous').v, [NaN; 10; 20; 40; 40]); ## Test 'next' carries the first value back and has nothing after the data %!test %! tb = datetime (2024, 1, 1) + hours (1:3)'; %! TT = timetable (tb, [10; 20; 40], 'VariableNames', {'v'}); %! tx = datetime (2024, 1, 1) + hours (0:4)'; %! assert_equal (retime (TT, tx, 'next').v, [10; 10; 20; 40; NaN]); ## Test 'nearest' answers on both sides of the data %!test %! tb = datetime (2024, 1, 1) + hours (1:3)'; %! TT = timetable (tb, [10; 20; 40], 'VariableNames', {'v'}); %! tx = datetime (2024, 1, 1) + hours (0:4)'; %! assert_equal (retime (TT, tx, 'nearest').v, [10; 10; 20; 40; 40]); ## Test a constant 'EndValues' answers outside the data under 'linear' %!test %! tb = datetime (2024, 1, 1) + hours (1:3)'; %! TT = timetable (tb, [10; 20; 40], 'VariableNames', {'v'}); %! tx = datetime (2024, 1, 1) + hours (0:4)'; %! v = retime (TT, tx, 'linear', 'EndValues', 99).v; %! assert_equal (v, [99; 10; 20; 40; 99]); ## Test a constant 'EndValues' overrides what a neighbour method found %!test %! tb = datetime (2024, 1, 1) + hours (1:3)'; %! TT = timetable (tb, [10; 20; 40], 'VariableNames', {'v'}); %! tx = datetime (2024, 1, 1) + hours (0:4)'; %! v = retime (TT, tx, 'previous', 'EndValues', 99).v; %! assert_equal (v, [99; 10; 20; 40; 99]); ## Test a value that was already missing is filled like any other gap %!test %! tm = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tm, [10; NaN; 30; 40], 'VariableNames', {'v'}); %! tq = datetime (2024, 1, 1) + minutes (0:30:180)'; %! v = [10; 15; 20; 25; 30; 35; 40]; %! assert_equal (retime (TT, tq, 'linear').v, v); ## Test a neighbour method skips a value that was already missing %!test %! tm = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tm, [10; NaN; 30; 40], 'VariableNames', {'v'}); %! tq = datetime (2024, 1, 1) + minutes (0:30:180)'; %! v = [10; 10; 10; 10; 30; 30; 40]; %! assert_equal (retime (TT, tq, 'previous').v, v); ## Test a gap is filled even when the target adds no row at all %!test %! tm = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tm, [10; NaN; 30; 40], 'VariableNames', {'v'}); %! assert_equal (retime (TT, tm, 'linear').v, [10; 20; 30; 40]); ## Test 'fillwithmissing' is the one method that leaves such a value alone %!test %! tm = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tm, [10; NaN; 30; 40], 'VariableNames', {'v'}); %! v = retime (TT, tm, 'fillwithmissing').v; %! assert_equal (v, [10; NaN; 30; 40]); ## Test 'retime' holds the previous value of a logical variable %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, logical ([1; 0; 1]), 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (retime (TT, th, 'previous').v, logical ([1; 0; 0; 1])); ## Test a logical variable stays logical %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, logical ([1; 0; 1]), 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (class (retime (TT, th, 'previous').v), 'logical'); ## Test 'retime' holds the previous value of a categorical variable %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, categorical ({'x'; 'y'; 'x'}), 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! c = categorical ({'x'; 'y'; 'y'; 'x'}); %! assert_equal (retime (TT, th, 'previous').v, c); ## Test 'retime' holds the previous value of a cell variable %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, {1; 2; 3}, 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (retime (TT, th, 'previous').v, {1; 2; 2; 3}); ## Test 'retime' interpolates a datetime variable %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, ta, 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (retime (TT, th, 'linear').v, th); ## Test 'retime' interpolates a duration variable %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, seconds ([1; 2; 3]), 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (retime (TT, th, 'linear').v, seconds ([1; 2; 2.5; 3])); ## Test the time unit grid runs to the first boundary past the last row time %!test %! tn = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tn, [1; 2; 3; 4], 'VariableNames', {'v'}); %! rt = retime (TT, 'daily', 'previous').Properties.RowTimes; %! assert_equal (rt, [datetime(2024,1,1); datetime(2024,1,2)]); ## A grid of bins ends on the bin holding the last row time, an aggregation ## having nothing to put in one past it; a fill computes a value for every ## target time and runs on to the boundary at or after it, as the test above ## shows. Every expectation below was measured on MATLAB R2024a. ## Test each calendar unit gathers a day's worth of rows into one bin %!test %! TT = timetable ((1:5)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes (0:15:60)'); %! R = retime (TT, 'daily', 'sum'); %! assert_equal (height (R), 1); %! assert_equal (R.Properties.RowTimes, datetime (2024, 6, 1)); %! assert_equal (R.Var1, 15); %!test %! TT = timetable ((1:5)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes (0:15:60)'); %! R = retime (TT, 'weekly', 'sum'); %! assert_equal (height (R), 1); %! assert_equal (R.Properties.RowTimes, datetime (2024, 5, 26)); %!test %! TT = timetable ((1:5)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes (0:15:60)'); %! R = retime (TT, 'monthly', 'sum'); %! assert_equal (height (R), 1); %! assert_equal (R.Properties.RowTimes, datetime (2024, 6, 1)); %!test %! TT = timetable ((1:5)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes (0:15:60)'); %! R = retime (TT, 'quarterly', 'sum'); %! assert_equal (height (R), 1); %! assert_equal (R.Properties.RowTimes, datetime (2024, 4, 1)); %!test %! TT = timetable ((1:5)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes (0:15:60)'); %! R = retime (TT, 'yearly', 'sum'); %! assert_equal (height (R), 1); %! assert_equal (R.Properties.RowTimes, datetime (2024, 1, 1)); ## Test the same rows filled rather than gathered keep the boundary past them %!test %! TT = timetable ((1:5)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes (0:15:60)'); %! R = retime (TT, 'daily', 'previous'); %! assert_equal (height (R), 2); %! assert_equal (R.Properties.RowTimes, ... %! [datetime(2024, 6, 1); datetime(2024, 6, 2)]); ## Test an hourly bin grid whose last row time is not on the hour %!test %! TT = timetable ((1:5)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([10 40 70 100 130])'); %! R = retime (TT, 'hourly', 'sum'); %! assert_equal (height (R), 3); %! assert_equal (R.Properties.RowTimes(3), datetime (2024, 6, 1, 2, 0, 0)); %! assert_equal (R.Var1, [3; 7; 5]); %!test %! TT = timetable ((1:5)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([10 40 70 100 130])'); %! assert_equal (height (retime (TT, 'hourly', 'previous')), 4); ## Test a function handle gathers, and so ends the grid where a name does %!test %! TT = timetable ((1:5)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([10 40 70 100 130])'); %! assert_equal (height (retime (TT, 'hourly', @sum)), 3); %!test %! TT = timetable ((1:5)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes (0:15:60)'); %! assert_equal (height (retime (TT, 'daily', @sum)), 1); ## Test rows crossing midnight fall into the two days that hold them %!test %! TT = timetable ((1:5)', 'RowTimes', ... %! datetime (2024, 6, 1, 22, 0, 0) + hours (0:4)'); %! R = retime (TT, 'daily', 'sum'); %! assert_equal (height (R), 2); %! assert_equal (R.Var1, [3; 12]); ## Test a last row time landing on a boundary keeps the bin it names %!test %! TT = timetable ((1:3)', 'RowTimes', datetime (2024, 6, 1) + days ((0:2)')); %! R = retime (TT, 'daily', 'sum'); %! assert_equal (height (R), 3); %! assert_equal (R.Var1, [1; 2; 3]); ## Test a single row gathers into a single bin %!test %! R = retime (timetable (1, 'RowTimes', datetime (2024, 6, 1)), ... %! 'daily', 'sum'); %! assert_equal (height (R), 1); ## An aggregating method bins the rows, so 'IncludedEdge' says which bin a ## row time sitting on a boundary falls into, and the grid moves with it. ## Measured on MATLAB R2024a. ## Test bins closed on the left end where the last row time is %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([0 30 60 90])'); %! R = retime (TT, 'hourly', 'sum', 'IncludedEdge', 'left'); %! assert_equal (height (R), 2); %! assert_equal (R.Var1, [3; 7]); ## Test bins closed on the right reach one unit past it %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([0 30 60 90])'); %! R = retime (TT, 'hourly', 'sum', 'IncludedEdge', 'right'); %! assert_equal (height (R), 3); %! assert_equal (R.Properties.RowTimes(3), datetime (2024, 6, 1, 2, 0, 0)); %! assert_equal (R.Var1, [1; 5; 4]); ## Test counting the rows agrees with summing them %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([0 30 60 90])'); %! R = retime (TT, 'hourly', 'count', 'IncludedEdge', 'right'); %! assert_equal (R.Var1, [1; 2; 1]); ## Test bins closed on the right drop a leading label nothing can reach %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([30 60 90 120])'); %! R = retime (TT, 'hourly', 'sum', 'IncludedEdge', 'right'); %! assert_equal (height (R), 2); %! assert_equal (R.Properties.RowTimes(1), datetime (2024, 6, 1, 1, 0, 0)); %! assert_equal (R.Var1, [3; 7]); ## Test the left-closed grid over the same rows starts an hour earlier %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([30 60 90 120])'); %! R = retime (TT, 'hourly', 'sum', 'IncludedEdge', 'left'); %! assert_equal (height (R), 3); %! assert_equal (R.Properties.RowTimes(1), datetime (2024, 6, 1)); %! assert_equal (R.Var1, [1; 5; 4]); ## Test a daily grid closed on the right names the day the rows ran into %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([30 60 90 120])'); %! R = retime (TT, 'daily', 'sum', 'IncludedEdge', 'right'); %! assert_equal (height (R), 1); %! assert_equal (R.Properties.RowTimes, datetime (2024, 6, 2)); %! assert_equal (R.Var1, 10); %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([30 60 90 120])'); %! R = retime (TT, 'daily', 'sum', 'IncludedEdge', 'left'); %! assert_equal (R.Properties.RowTimes, datetime (2024, 6, 1)); %! assert_equal (R.Var1, 10); ## Test a row time on the boundary joins the earlier bin when closed right %!test %! TT = timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([0 30 60])'); %! R = retime (TT, 'daily', 'sum', 'IncludedEdge', 'right'); %! assert_equal (height (R), 2); %! assert_equal (R.Var1, [1; 5]); %!test %! TT = timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([0 30 60])'); %! R = retime (TT, 'daily', 'sum', 'IncludedEdge', 'left'); %! assert_equal (height (R), 1); %! assert_equal (R.Var1, 6); ## Test the same rule over a grid of days spanned by the rows %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + hours ([0 5 25 30])'); %! R = retime (TT, 'daily', 'sum', 'IncludedEdge', 'right'); %! assert_equal (height (R), 3); %! assert_equal (R.Var1, [1; 2; 7]); %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + hours ([0 5 25 30])'); %! R = retime (TT, 'daily', 'sum', 'IncludedEdge', 'left'); %! assert_equal (height (R), 2); %! assert_equal (R.Var1, [3; 7]); ## A method that fills the grid rather than gathering into it reads the rows ## one at a time, so 'IncludedEdge' leaves it alone. ## Test a filling method ignores the option %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([0 30 60 90])'); %! L = retime (TT, 'hourly', 'previous', 'IncludedEdge', 'left'); %! R = retime (TT, 'hourly', 'previous', 'IncludedEdge', 'right'); %! assert_equal (height (L), 3); %! assert_equal (L.Var1, R.Var1); %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([0 30 60 90])'); %! L = retime (TT, 'daily', 'fillwithmissing', 'IncludedEdge', 'left'); %! R = retime (TT, 'daily', 'fillwithmissing', 'IncludedEdge', 'right'); %! assert_equal (height (L), 2); %! assert_equal (L.Var1, R.Var1); ## Test a 'regular' grid is bounded by the same rule %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([0 30 60 90])'); %! R = retime (TT, 'regular', 'sum', 'TimeStep', hours (1), ... %! 'IncludedEdge', 'left'); %! assert_equal (height (R), 2); %! assert_equal (R.Var1, [3; 7]); %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([0 30 60 90])'); %! R = retime (TT, 'regular', 'sum', 'TimeStep', hours (1), ... %! 'IncludedEdge', 'right'); %! assert_equal (height (R), 3); %! assert_equal (R.Var1, [1; 5; 4]); ## Test a filling method over a 'regular' grid is unmoved by the option %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([0 30 60 90])'); %! L = retime (TT, 'regular', 'previous', 'TimeStep', hours (1), ... %! 'IncludedEdge', 'left'); %! R = retime (TT, 'regular', 'previous', 'TimeStep', hours (1), ... %! 'IncludedEdge', 'right'); %! assert_equal (height (L), 3); %! assert_equal (L.Var1, [1; 3; 4]); %! assert_equal (L.Var1, R.Var1); ## Test the ordered aggregations take the edge like the others %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([0 30 60 90])'); %! R = retime (TT, 'hourly', 'firstvalue', 'IncludedEdge', 'left'); %! assert_equal (height (R), 2); %! assert_equal (R.Var1, [1; 3]); %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([0 30 60 90])'); %! R = retime (TT, 'hourly', 'lastvalue', 'IncludedEdge', 'right'); %! assert_equal (height (R), 3); %! assert_equal (R.Var1, [1; 3; 4]); ## Test duration row times take the rule as datetimes do %!test %! TT = timetable ((1:4)', 'RowTimes', seconds ([0; 30; 90; 150])); %! R = retime (TT, 'hourly', 'sum', 'IncludedEdge', 'left'); %! assert_equal (height (R), 1); %! assert_equal (R.Var1, 10); %!test %! TT = timetable ((1:4)', 'RowTimes', seconds ([0; 30; 90; 150])); %! R = retime (TT, 'hourly', 'sum', 'IncludedEdge', 'right'); %! assert_equal (height (R), 2); %! assert_equal (seconds (R.Properties.RowTimes), [0; 3600]); %! assert_equal (R.Var1, [1; 9]); %!test %! TT = timetable ((1:4)', 'RowTimes', seconds ([0; 30; 90; 150])); %! R = retime (TT, 'daily', 'count', 'IncludedEdge', 'right'); %! assert_equal (seconds (R.Properties.RowTimes), [0; 86400]); %! assert_equal (R.Var1, [1; 3]); ## Test a grid given as a vector of times is used as it stands, both ways %!test %! TT = timetable ((1:4)', 'RowTimes', ... %! datetime (2024, 6, 1) + minutes ([0 30 60 90])'); %! tq = datetime (2024, 6, 1) + hours ((0:2)'); %! assert_equal (retime (TT, tq, 'sum', 'IncludedEdge', 'left').Var1, ... %! [3; 7; 0]); %! assert_equal (retime (TT, tq, 'sum', 'IncludedEdge', 'right').Var1, ... %! [1; 5; 4]); ## Test a calendar month and week grid over three monthly rows %!test %! TT = timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 6, 1) + calmonths ((0:2)')); %! R = retime (TT, 'monthly', 'sum'); %! assert_equal (height (R), 3); %! assert_equal (R.Var1, [1; 2; 3]); %!test %! TT = timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 6, 1) + calmonths ((0:2)')); %! R = retime (TT, 'weekly', 'sum'); %! assert_equal (height (R), 10); %! assert_equal (R.Properties.RowTimes(10), datetime (2024, 7, 28)); ## Test duration row times gather the same way %!test %! TT = timetable ((1:3)', 'RowTimes', seconds ([0; 30; 90])); %! R = retime (TT, 'hourly', 'sum'); %! assert_equal (height (R), 1); %! assert_equal (R.Var1, 6); %!test %! TT = timetable ((1:3)', 'RowTimes', seconds ([0; 30; 90])); %! R = retime (TT, 'minutely', 'sum'); %! assert_equal (height (R), 2); %! assert_equal (seconds (R.Properties.RowTimes), [0; 60]); %! assert_equal (R.Var1, [3; 3]); ## Asking for the rows by a unit says how to gather them, not how to print ## them, so the row times keep the display format they had rather than ## taking the unit's. Measured on MATLAB R2024a and R2026a. %!test %! TT = timetable ((1:3)', 'RowTimes', seconds ([0; 30; 90])); %! assert_equal (TT.Properties.RowTimes.Format, 's'); %! assert_equal (retime (TT, 'hourly', 'sum').Properties.RowTimes.Format, 's'); %! assert_equal (retime (TT, 'minutely', 'sum').Properties.RowTimes.Format, ... %! 's'); %!test %! TT = timetable ((1:3)', 'RowTimes', seconds ([0; 30; 90])); %! R = retime (TT, 'minutely', 'previous'); %! assert_equal (R.Properties.RowTimes.Format, 's'); %!test %! TT = timetable ((1:3)', 'RowTimes', hours ([0; 0.25; 0.75])); %! assert_equal (TT.Properties.RowTimes.Format, 'h'); %! R = retime (TT, 'minutely', 'sum'); %! assert_equal (R.Properties.RowTimes.Format, 'h'); ## Test a 'regular' grid keeps it too, as it always did %!test %! TT = timetable ((1:3)', 'RowTimes', seconds ([0; 30; 90])); %! R = retime (TT, 'regular', 'previous', 'TimeStep', seconds (60)); %! assert_equal (R.Properties.RowTimes.Format, 's'); ## Test 'synchronize' ends its grid by the same rule %!test %! d0 = datetime (2024, 6, 1); %! H = timetable ((1:5)', 'RowTimes', d0 + minutes ([10 40 70 100 130])'); %! B = timetable ((6:10)', 'RowTimes', d0 + minutes ([5 35 65 95 125])', ... %! 'VariableNames', {'W'}); %! assert_equal (height (synchronize (H, B, 'hourly', 'sum')), 3); %! assert_equal (height (synchronize (H, B, 'hourly', 'previous')), 4); %! assert_equal (height (synchronize (H, B, 'daily', 'sum')), 1); %! assert_equal (height (synchronize (H, B, 'daily', 'previous')), 2); ## A 'TimeStep' may be a calendarDuration, which is stepped one unit at a ## time from a boundary of that unit: the start of the month where it counts ## months, quarters or years, and the start of the day where it counts days ## or weeks, so a weekly grid begins on the day of the first row time rather ## than on the Sunday before it. Measured on MATLAB R2024a. %!test %! TT = timetable ((1:5)', 'RowTimes', ... %! datetime (2024, 6, 1, 5, 0, 0) + hours (0:4)'); %! R = retime (TT, 'regular', 'previous', 'TimeStep', caldays (1)); %! assert_equal (height (R), 2); %! assert_equal (R.Properties.RowTimes, ... %! [datetime(2024, 6, 1); datetime(2024, 6, 2)]); %! assert_equal (R.Properties.TimeStep, caldays (1)); %!test %! TT = timetable ((1:5)', 'RowTimes', ... %! datetime (2024, 6, 1, 5, 0, 0) + hours (0:4)'); %! R = retime (TT, 'regular', 'previous', 'TimeStep', caldays (2)); %! assert_equal (R.Properties.RowTimes, ... %! [datetime(2024, 6, 1); datetime(2024, 6, 3)]); ## Test a month step starts on the first of the month the first row is in %!test %! TT = timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 3, 20, 7, 0, 0) + hours (0:2)'); %! R = retime (TT, 'regular', 'previous', 'TimeStep', calmonths (1)); %! assert_equal (R.Properties.RowTimes, ... %! [datetime(2024, 3, 1); datetime(2024, 4, 1)]); ## Test a year and a quarter step count months, so they start there too %!test %! TT = timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 3, 20, 7, 0, 0) + hours (0:2)'); %! R = retime (TT, 'regular', 'previous', 'TimeStep', calyears (1)); %! assert_equal (R.Properties.RowTimes, ... %! [datetime(2024, 3, 1); datetime(2025, 3, 1)]); %!test %! TT = timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 3, 20, 7, 0, 0) + hours (0:2)'); %! R = retime (TT, 'regular', 'previous', 'TimeStep', calquarters (1)); %! assert_equal (R.Properties.RowTimes, ... %! [datetime(2024, 3, 1); datetime(2024, 6, 1)]); ## Test a week step counts days, so it starts on the day and not the Sunday %!test %! TT = timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 6, 5, 7, 0, 0) + hours (0:2)'); %! R = retime (TT, 'regular', 'previous', 'TimeStep', calweeks (1)); %! assert_equal (R.Properties.RowTimes, ... %! [datetime(2024, 6, 5); datetime(2024, 6, 12)]); %!test %! TT = timetable ((1:3)', 'RowTimes', ... %! datetime (2024, 3, 20, 7, 0, 0) + hours (0:2)'); %! R = retime (TT, 'regular', 'previous', 'TimeStep', calweeks (2)); %! assert_equal (R.Properties.RowTimes, ... %! [datetime(2024, 3, 20); datetime(2024, 4, 3)]); ## Test a calendar step obeys the same rule about where the grid ends %!test %! TT = timetable ((1:5)', 'RowTimes', ... %! datetime (2024, 6, 1, 5, 0, 0) + hours (0:4)'); %! assert_equal (height (retime (TT, 'regular', 'sum', ... %! 'TimeStep', caldays (1))), 1); %! assert_equal (height (retime (TT, 'regular', @sum, ... %! 'TimeStep', caldays (1))), 1); ## Test 'synchronize' takes a calendar step as well %!test %! d0 = datetime (2024, 6, 1, 5, 0, 0); %! A = timetable ((1:5)', 'RowTimes', d0 + hours (0:4)'); %! B = timetable ((6:10)', 'RowTimes', d0 + minutes (30) + hours (0:4)', ... %! 'VariableNames', {'W'}); %! assert_equal (height (synchronize (A, B, 'regular', 'previous', ... %! 'TimeStep', caldays (1))), 2); %! assert_equal (height (synchronize (A, B, 'regular', 'sum', ... %! 'TimeStep', caldays (1))), 1); ## A method needs the row times ordered only where it names a row by where ## it comes: the filling methods and the two aggregations that take the ## first or the last of a bin. The rest gather what falls in each bin and ## take no interest in the order, so a missing row time simply falls in no ## bin. Measured on MATLAB R2024a. %!test %! d0 = datetime (2024, 6, 1); %! N = timetable ([d0; d0 + hours(1); NaT; d0 + hours(3)], (1:4)'); %! assert_equal (retime (N, 'hourly', 'sum').Var1', [1, 2, 0, 4]); %! assert_equal (retime (N, 'hourly', 'count').Var1', [1, 1, 0, 1]); %! assert_equal (retime (N, 'hourly', 'prod').Var1', [1, 2, 1, 4]); %!test %! d0 = datetime (2024, 6, 1); %! N = timetable ([d0; d0 + hours(1); NaT; d0 + hours(3)], (1:4)'); %! assert_equal (retime (N, 'hourly', 'mean').Var1', [1, 2, NaN, 4]); %! assert_equal (retime (N, 'hourly', 'median').Var1', [1, 2, NaN, 4]); %! assert_equal (retime (N, 'hourly', 'mode').Var1', [1, 2, NaN, 4]); %! assert_equal (retime (N, 'hourly', 'min').Var1', [1, 2, NaN, 4]); %! assert_equal (retime (N, 'hourly', 'max').Var1', [1, 2, NaN, 4]); %!test %! d0 = datetime (2024, 6, 1); %! N = timetable ([d0; d0 + hours(1); NaT; d0 + hours(3)], (1:4)'); %! assert_equal (retime (N, 'hourly', @sum).Var1', [1, 2, 0, 4]); ## Test the two filling methods that ask nothing of the order take it too %!test %! d0 = datetime (2024, 6, 1); %! N = timetable ([d0; d0 + hours(1); NaT; d0 + hours(3)], (1:4)'); %! assert_equal (retime (N, 'hourly', 'fillwithmissing').Var1', ... %! [1, 2, NaN, 4]); %! assert_equal (retime (N, 'hourly', 'fillwithconstant', ... %! 'Constant', 9).Var1', [1, 2, 9, 4]); ## Test the row times of the result span the times that name an instant %!test %! d0 = datetime (2024, 6, 1); %! N = timetable ([d0; d0 + hours(1); NaT; d0 + hours(3)], (1:4)'); %! R = retime (N, 'hourly', 'sum'); %! assert_equal (height (R), 4); %! assert_equal (R.Properties.RowTimes, d0 + hours (0:3)'); ## Test a method that reads the neighbours refuses a missing row time %!error ... %! d0 = datetime (2024, 6, 1); %! retime (timetable ([d0; d0 + hours(1); NaT; d0 + hours(3)], (1:4)'), ... %! 'hourly', 'previous'); %!error ... %! d0 = datetime (2024, 6, 1); %! retime (timetable ([d0; d0 + hours(1); NaT; d0 + hours(3)], (1:4)'), ... %! 'hourly', 'linear'); ## Test 'firstvalue' and 'lastvalue' name a row by where it comes, so they ## ask for the order as the filling methods do %!error ... %! d0 = datetime (2024, 6, 1); %! retime (timetable (d0 + hours ([0; 2; 1; 3]), (1:4)'), ... %! 'hourly', 'firstvalue'); %!error ... %! d0 = datetime (2024, 6, 1); %! retime (timetable (d0 + hours ([0; 2; 1; 3]), (1:4)'), ... %! 'hourly', 'lastvalue'); ## Test the gathering methods take row times in any order %!test %! d0 = datetime (2024, 6, 1); %! S = timetable (d0 + hours ([0; 2; 1; 3]), (1:4)'); %! assert_equal (retime (S, 'hourly', 'sum').Var1', [1, 3, 2, 4]); %! assert_equal (retime (S, 'hourly', 'mean').Var1', [1, 3, 2, 4]); %! assert_equal (retime (S, 'hourly', 'fillwithmissing').Var1', [1, 3, 2, 4]); ## Test descending row times are sorted, so every method takes them %!test %! d0 = datetime (2024, 6, 1); %! D = timetable (d0 + hours ([3; 2; 1; 0]), (1:4)'); %! assert_equal (retime (D, 'hourly', 'sum').Var1', [4, 3, 2, 1]); %! assert_equal (retime (D, 'hourly', 'previous').Var1', [4, 3, 2, 1]); %! assert_equal (retime (D, 'hourly', 'firstvalue').Var1', [4, 3, 2, 1]); ## Test row times that are all missing leave the grid nothing to span %!error ... %! retime (timetable ([NaT; NaT], [1; 2]), 'hourly', 'sum'); ## Test a 'regular' grid runs on to the step at or after the last row %!test %! TT = timetable ((1:5)', 'RowTimes', datetime (2024, 6, 1) + hours (0:4)'); %! R = retime (TT, 'regular', 'previous', 'TimeStep', minutes (45)); %! assert_equal (height (R), 7); %! assert_equal (R.Properties.RowTimes(7), datetime (2024, 6, 1, 4, 30, 0)); %! assert_equal (R.Var1, [1; 1; 2; 3; 4; 4; 5]); ## Test an hourly grid stops on the last row time when it lands there %!test %! tn = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tn, [1; 2; 3; 4], 'VariableNames', {'v'}); %! assert_equal (retime (TT, 'hourly', 'previous').Properties.RowTimes, tn); ## Test a weekly grid begins on the Sunday at or before the first row time %!test %! t = datetime (2024, 1, 3) + days (0:20)'; %! TT = timetable (t, (1:21)', 'VariableNames', {'v'}); %! rt = retime (TT, 'weekly', 'previous').Properties.RowTimes; %! assert_equal (rt(1), datetime (2023, 12, 31)); ## Test a weekly grid steps by one week %!test %! t = datetime (2024, 1, 3) + days (0:20)'; %! TT = timetable (t, (1:21)', 'VariableNames', {'v'}); %! rt = retime (TT, 'weekly', 'previous').Properties.RowTimes; %! assert_equal (numel (rt), 5); ## Test a monthly grid anchors on the first of the month %!test %! tn = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tn, [1; 2; 3; 4], 'VariableNames', {'v'}); %! rt = retime (TT, 'monthly', 'previous').Properties.RowTimes; %! assert_equal (rt, [datetime(2024,1,1); datetime(2024,2,1)]); ## Test a quarterly grid anchors on the first of the quarter %!test %! tn = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tn, [1; 2; 3; 4], 'VariableNames', {'v'}); %! rt = retime (TT, 'quarterly', 'previous').Properties.RowTimes; %! assert_equal (rt, [datetime(2024,1,1); datetime(2024,4,1)]); ## Test a yearly grid anchors on the first of the year %!test %! tn = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tn, [1; 2; 3; 4], 'VariableNames', {'v'}); %! rt = retime (TT, 'yearly', 'previous').Properties.RowTimes; %! assert_equal (rt, [datetime(2024,1,1); datetime(2025,1,1)]); ## Test 'regular' builds a grid from a time step %!test %! tn = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tn, [1; 2; 3; 4], 'VariableNames', {'v'}); %! rt = retime (TT, 'regular', 'previous', 'TimeStep', minutes (30)); %! assert_equal (numel (rt.Properties.RowTimes), 7); ## Test 'SampleRate' is read in hertz %!test %! ts = datetime (2024, 1, 1) + seconds (0:4)'; %! TT = timetable (ts, (1:5)', 'VariableNames', {'v'}); %! rt = retime (TT, 'regular', 'previous', 'SampleRate', 2); %! assert_equal (numel (rt.Properties.RowTimes), 9); ## Test an empty target vector gives a timetable with no rows %!test %! tn = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tn, [1; 2; 3; 4], 'VariableNames', {'v'}); %! assert_equal (size (retime (TT, datetime.empty (0, 1), 'previous')), [0, 1]); ## Test 'retime' resamples duration row times %!test %! U = timetable (seconds ([0; 1; 3]), [10; 20; 40], 'VariableNames', {'v'}); %! assert_equal (retime (U, seconds ((0:3)'), 'linear').v, [10; 20; 30; 40]); ## Test duration row times take a time unit grid %!test %! U = timetable (seconds ([0; 1; 3]), [10; 20; 40], 'VariableNames', {'v'}); %! rt = retime (U, 'secondly', 'previous').Properties.RowTimes; %! assert_equal (rt, seconds ((0:3)')); ## Test the row times of the result keep their own type %!test %! U = timetable (seconds ([0; 1; 3]), [10; 20; 40], 'VariableNames', {'v'}); %! rt = retime (U, seconds ((0:3)'), 'linear').Properties.RowTimes; %! assert_equal (class (rt), 'duration'); ## Test 'retime' keeps the variable units %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, [10; 20; 40], [1; 2; 3], 'VariableNames', {'a', 'b'}); %! TT.Properties.VariableUnits = {'m', 'kg'}; %! th = datetime (2024, 1, 1) + hours (0:3)'; %! u = retime (TT, th, 'previous').Properties.VariableUnits; %! assert_equal (u, {'m', 'kg'}); ## Test 'retime' keeps the variable continuity %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, [10; 20; 40], [1; 2; 3], 'VariableNames', {'a', 'b'}); %! TT.Properties.VariableContinuity = {'continuous', 'step'}; %! th = datetime (2024, 1, 1) + hours (0:3)'; %! c = retime (TT, th, 'previous').Properties.VariableContinuity; %! assert_equal (c, {'continuous', 'step'}); ## Test the result of 'retime' is regular and carries its time step %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, [10; 20; 40], 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (isregular (retime (TT, th, 'previous')), true); ## Test 'retime' returns a timetable %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, [10; 20; 40], 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (class (retime (TT, th, 'previous')), 'timetable'); ## Test 'VariableContinuity' drives the method when none is given %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, [10; 20; 40], [10; 20; 40], [10; 20; 40], ... %! 'VariableNames', {'unset', 'cont', 'stp'}); %! TT.Properties.VariableContinuity = {'unset', 'continuous', 'step'}; %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (retime (TT, th, 'default').unset, [10; 20; NaN; 40]); ## Test a continuous variable interpolates under 'default' %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, [10; 20; 40], 'VariableNames', {'cont'}); %! TT.Properties.VariableContinuity = {'continuous'}; %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (retime (TT, th, 'default').cont, [10; 20; 30; 40]); ## Test a step variable holds its previous value under 'default' %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, [10; 20; 40], 'VariableNames', {'stp'}); %! TT.Properties.VariableContinuity = {'step'}; %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (retime (TT, th, 'default').stp, [10; 20; 20; 40]); ## Test an event variable is left missing under 'default' %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, [10; 20; 40], 'VariableNames', {'evt'}); %! TT.Properties.VariableContinuity = {'event'}; %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (retime (TT, th, 'default').evt, [10; 20; NaN; 40]); ## Test an explicit method overrides the continuity %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, [10; 20; 40], 'VariableNames', {'stp'}); %! TT.Properties.VariableContinuity = {'step'}; %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (retime (TT, th, 'linear').stp, [10; 20; 30; 40]); ## Test input validation for 'timetable.retime' %!error ... %! retime (timetable (datetime (2024, 1, 1), 1)); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! seconds ((0:2)')); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! datetime (2024, 1, 1) + hours ([1 0 2]')); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! datetime (2024, 1, 1) + hours ([0 0 2]')); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! 'regular', 'previous'); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! 'regular', 'previous', 'TimeStep', hours (1), 'SampleRate', 2); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! 'regular', 'previous', 'SampleRate', -1); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! 'regular', 'previous', 'TimeStep', 5); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! 'regular', 'previous', 'TimeStep', calmonths (1) + caldays (3)); %!error ... %! retime (timetable (hours ((0:2)'), (1:3)'), 'regular', 'previous', ... %! 'TimeStep', caldays (1)); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'linear', 'EndValues', 'none'); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'linear', 'EndValues', [1 2]); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'bogus'); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'linear', 'Bogus', 1); %!error ... %! retime (timetable (hours ((0:2)'), (1:3)'), 'hourly', 'Bogus', 1); %!error ... %! retime (timetable (hours ((0:2)'), (1:3)'), 'hourly', 'linear', ... %! 'TimeStep'); %!error ... %! retime (timetable (hours ((0:2)'), (1:3)'), 'hourly', 5, 6); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), ... %! categorical ({'a'; 'b'; 'c'}), 'VariableNames', {'v'}), datetime (2024, ... %! 1, 1) + hours ((0:2)'), 'linear'); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), ... %! logical ([1; 0; 1]), 'VariableNames', {'v'}), datetime (2024, 1, ... %! 1) + hours ((0:2)'), 'fillwithmissing'); %!error ... %! retime (timetable (seconds ((0:2)'), (1:3)'), 'monthly', 'previous'); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), 'bogus'); %!error ... %! retime (timetable (hours ((0:2)'), (1:3)'), 'Bogus', 1); ## Test 'fillmissing' interpolates a datetime variable against the row times %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! d = [datetime(2024, 1, 1); NaT; datetime(2024, 1, 3)]; %! TT = timetable (t, d, 'VariableNames', {'d'}); %! assert_equal (fillmissing (TT, 'linear').d(2), datetime (2024, 1, 2)); ## Test 'retime' interpolates with a modified Akima spline %!test %! ta = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (ta, [0; 1; 8; 27], 'VariableNames', {'v'}); %! tq = datetime (2024, 1, 1) + minutes (0:30:180)'; %! v = [0; 0.0056818181818182; 1; 3.6126692456480; 8; 15.812392179413; 27]; %! assert (retime (TT, tq, 'makima').v, v, 1e-12); ## Test 'retime' interpolates over unevenly spaced row times %!test %! ta = datetime (2024, 6, 1) + hours ([0; 1; 3]); %! TT = timetable (ta, [0; 1; 8], 'VariableNames', {'v'}); %! tq = datetime (2024, 6, 1) + hours ([0; 0.5; 1; 2; 3; 4]); %! v = [0; 0.5; 1; 4.5; 8; 11.5]; %! assert_equal (retime (TT, tq, 'linear').v, v); %!test %! ta = datetime (2024, 6, 1) + hours ([0; 1; 3]); %! TT = timetable (ta, [0; 1; 8], 'VariableNames', {'v'}); %! tq = datetime (2024, 6, 1) + hours ([0; 0.5; 1; 2; 3; 4]); %! v = [0; 0.29166666666666663; 1; 3.666666666666667; 8; 14]; %! assert (retime (TT, tq, 'spline').v, v, 1e-12); ## Test the shape-preserving interpolant over the same rows %!test %! ta = datetime (2024, 6, 1) + hours ([0; 1; 3]); %! TT = timetable (ta, [0; 1; 8], 'VariableNames', {'v'}); %! tq = datetime (2024, 6, 1) + hours ([0; 0.5; 1; 2; 3; 4]); %! v = [0; 0.33769379844961239; 1; 3.5746124031007751; 8; 13.723837209302324]; %! assert (retime (TT, tq, 'pchip').v, v, 1e-12); %!test %! ta = datetime (2024, 6, 1) + hours ([0; 1; 3]); %! TT = timetable (ta, [0; 1; 8], 'VariableNames', {'v'}); %! tq = datetime (2024, 6, 1) + hours ([0; 0.5; 1; 2; 3; 4]); %! v = [0; 0.265625; 1; 3.8421336206896552; 8; 11.983297413793101]; %! assert (retime (TT, tq, 'makima').v, v, 1e-12); ## Test the neighbour methods over the same rows %!test %! ta = datetime (2024, 6, 1) + hours ([0; 1; 3]); %! TT = timetable (ta, [0; 1; 8], 'VariableNames', {'v'}); %! tq = datetime (2024, 6, 1) + hours ([0; 0.5; 1; 2; 3; 4]); %! assert_equal (retime (TT, tq, 'previous').v, [0; 0; 1; 1; 8; 8]); %! assert_equal (retime (TT, tq, 'next').v, [0; 1; 1; 8; 8; NaN]); %! assert_equal (retime (TT, tq, 'nearest').v, [0; 1; 1; 8; 8; 8]); ## Test 'EndValues' asked for by name gives what the method gives anyway %!test %! ta = datetime (2024, 6, 1) + hours ([0; 1; 3]); %! TT = timetable (ta, [0; 1; 8], 'VariableNames', {'v'}); %! tq = datetime (2024, 6, 1) + hours ([0; 0.5; 1; 2; 3; 4]); %! v = retime (TT, tq, 'linear', 'EndValues', 'extrap').v; %! assert_equal (v, [0; 0.5; 1; 4.5; 8; 11.5]); %!test %! ta = datetime (2024, 6, 1) + hours ([0; 1; 3]); %! TT = timetable (ta, [0; 1; 8], 'VariableNames', {'v'}); %! tq = datetime (2024, 6, 1) + hours ([0; 0.5; 1; 2; 3; 4]); %! v = retime (TT, tq, 'spline', 'EndValues', 'extrap').v; %! assert (v, [0; 0.29166666666666663; 1; 3.666666666666667; 8; 14], 1e-12); ## Test a constant 'EndValues' replaces only the row past the data %!test %! ta = datetime (2024, 6, 1) + hours ([0; 1; 3]); %! TT = timetable (ta, [0; 1; 8], 'VariableNames', {'v'}); %! tq = datetime (2024, 6, 1) + hours ([0; 0.5; 1; 2; 3; 4]); %! v = retime (TT, tq, 'linear', 'EndValues', 99).v; %! assert_equal (v, [0; 0.5; 1; 4.5; 8; 99]); ## Test a 'Constant' fills every row the source has nothing for %!test %! ta = datetime (2024, 6, 1) + hours ([0; 1; 3]); %! TT = timetable (ta, [0; 1; 8], 'VariableNames', {'v'}); %! tq = datetime (2024, 6, 1) + hours ([0; 0.5; 1; 2; 3; 4]); %! v = retime (TT, tq, 'fillwithconstant', 'Constant', 9).v; %! assert_equal (v, [0; 9; 1; 9; 8; 9]); ## Test a 'Constant' of missing fills them with the missing value %!test %! ta = datetime (2024, 6, 1) + hours ([0; 1; 3]); %! TT = timetable (ta, [0; 1; 8], 'VariableNames', {'v'}); %! tq = datetime (2024, 6, 1) + hours ([0; 0.5; 1; 2; 3; 4]); %! v = retime (TT, tq, 'fillwithconstant', 'Constant', missing).v; %! assert_equal (v, [0; NaN; 1; NaN; 8; NaN]); ## A character 'Constant' fills a cell array of strings with that string. ## MATLAB refuses it, reporting that a char cannot be converted to a cell; ## we take it, since it is the only reading a char constant can have there. %!test %! ta = datetime (2024, 6, 1) + hours ([0; 1; 3]); %! TT = timetable (ta, {'p'; 'q'; 'r'}, 'VariableNames', {'s'}); %! tq = datetime (2024, 6, 1) + hours ([0; 0.5; 1; 2; 3; 4]); %! v = retime (TT, tq, 'fillwithconstant', 'Constant', 'z').s; %! assert_equal (v, {'p'; 'z'; 'q'; 'z'; 'r'; 'z'}); ## Test 'retime' sums the rows falling in each bin, an empty bin giving zero %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, [1; 2; 3; 4], 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! assert_equal (retime (TT, tg, 'sum').v, [3; 0; 7; 0]); ## Test an empty bin makes a product of one %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, [1; 2; 3; 4], 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! assert_equal (retime (TT, tg, 'prod').v, [2; 1; 12; 1]); ## Test an empty bin has no mean %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, [1; 2; 3; 4], 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! assert_equal (retime (TT, tg, 'mean').v, [1.5; NaN; 3.5; NaN]); ## Test an empty bin has no smallest value %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, [1; 2; 3; 4], 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! assert_equal (retime (TT, tg, 'min').v, [1; NaN; 3; NaN]); ## Test an empty bin has no largest value %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, [1; 2; 3; 4], 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! assert_equal (retime (TT, tg, 'max').v, [2; NaN; 4; NaN]); ## Test an empty bin counts no rows %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, [1; 2; 3; 4], 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! assert_equal (retime (TT, tg, 'count').v, [2; 0; 2; 0]); ## Test an empty bin has no first value %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, [1; 2; 3; 4], 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! assert_equal (retime (TT, tg, 'firstvalue').v, [1; NaN; 3; NaN]); ## Test the last value of each bin %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, [1; 2; 3; 4], 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! assert_equal (retime (TT, tg, 'lastvalue').v, [2; NaN; 4; NaN]); ## Test the median of each bin %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, [1; 2; 3; 4], 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! assert_equal (retime (TT, tg, 'median').v, [1.5; NaN; 3.5; NaN]); ## Test the mode of each bin %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, [1; 2; 3; 4], 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! assert_equal (retime (TT, tg, 'mode').v, [1; NaN; 3; NaN]); ## Test a function handle is applied to the rows of each bin %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, [1; 2; 3; 4], 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! assert_equal (retime (TT, tg, @(x) numel (x)).v, [2; 0; 2; 0]); ## Test a function handle is given an empty slice for an empty bin %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, [1; 2; 3; 4], 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! assert_equal (retime (TT, tg, @(x) double (isempty (x))).v, [0; 1; 0; 1]); ## Test the slice a function handle is given keeps the variable's type %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, [1; 2; 3; 4], 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! f = @(x) double (isa (x, 'double')); %! assert_equal (retime (TT, tg, f).v, [1; 1; 1; 1]); ## Test the bins are closed on the left by default %!test %! ti = datetime (2024, 1, 1) + hours (0:4)'; %! TT = timetable (ti, [1; 2; 3; 4; 5], 'VariableNames', {'v'}); %! te = datetime (2024, 1, 1) + hours (0:2:4)'; %! assert_equal (retime (TT, te, 'sum').v, [3; 7; 5]); ## Test 'IncludedEdge' closes the bins on the right %!test %! ti = datetime (2024, 1, 1) + hours (0:4)'; %! TT = timetable (ti, [1; 2; 3; 4; 5], 'VariableNames', {'v'}); %! te = datetime (2024, 1, 1) + hours (0:2:4)'; %! v = retime (TT, te, 'sum', 'IncludedEdge', 'right').v; %! assert_equal (v, [1; 5; 9]); ## Test aggregating a duration variable keeps it a duration %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, seconds ([1; 2; 3; 4]), 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! assert_equal (class (retime (TT, tg, 'sum').v), 'duration'); ## Test the mean of a logical variable is a double %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, logical ([1; 0; 1]), 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (class (retime (TT, th, 'mean').v), 'double'); ## Test aggregating a datetime variable keeps it a datetime %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, ta, 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (class (retime (TT, th, 'mean').v), 'datetime'); ## Test counting the rows of a categorical variable %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, categorical ({'x'; 'y'; 'x'}), 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (retime (TT, th, 'count').v, [1; 1; 0; 1]); ## Test an aggregation accepts duplicated row times %!test %! td = datetime (2024, 1, 1) + hours ([0 1 1 3])'; %! TT = timetable (td, [1; 2; 3; 4], 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (retime (TT, th, 'sum').v, [1; 5; 0; 4]); ## Test an aggregation accepts unsorted row times %!test %! tu = datetime (2024, 1, 1) + hours ([2 0 1 3])'; %! TT = timetable (tu, [1; 2; 3; 4], 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (retime (TT, th, 'sum').v, [2; 3; 1; 4]); ## Test aggregating duration row times %!test %! U = timetable (seconds ([0; 1; 3]), [10; 20; 40], 'VariableNames', {'v'}); %! assert_equal (retime (U, seconds ((0:2:4)'), 'sum').v, [30; 40; 0]); ## Test input validation for 'timetable.retime' aggregation %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'std'); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'sum', 'IncludedEdge', ... %! 'middle'); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), ... %! categorical ({'a'; 'b'; 'c'}), 'VariableNames', {'v'}), datetime (2024, ... %! 1, 1) + hours ((0:2)'), 'mean'); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'VariableNames', {'v'}), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'sum'); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ([0 1 4 5]'), ... %! [1; 2; 3; 4], 'VariableNames', {'v'}), datetime (2024, 1, ... %! 1) + hours ((0:2:6)'), @(x) max (x) - min (x)); ## Test 'fillwithconstant' gives an added row the constant %!test %! tb = datetime (2024, 1, 1) + hours (1:3)'; %! TT = timetable (tb, [10; 20; 40], 'VariableNames', {'v'}); %! tx = datetime (2024, 1, 1) + hours (0:4)'; %! v = retime (TT, tx, 'fillwithconstant', 'Constant', 7).v; %! assert_equal (v, [7; 10; 20; 40; 7]); ## Test a row the target times keep is not given the constant %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, [10; 20; 40], 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! v = retime (TT, th, 'fillwithconstant', 'Constant', 7).v; %! assert_equal (v, [10; 20; 7; 40]); ## Test a duration constant fills a duration variable %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, seconds ([1; 2; 3]), 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! v = retime (TT, th, 'fillwithconstant', 'Constant', seconds (9)).v; %! assert_equal (v, seconds ([1; 2; 9; 3])); ## Test a character constant names a category and adds it %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, categorical ({'x'; 'y'; 'x'}), 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! v = retime (TT, th, 'fillwithconstant', 'Constant', 'z').v; %! assert_equal (v, categorical ({'x'; 'y'; 'z'; 'x'})); ## Test 'fillwithconstant' serves a logical variable, which has no missing %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, logical ([1; 0; 1]), 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! v = retime (TT, th, 'fillwithconstant', 'Constant', true).v; %! assert_equal (v, logical ([1; 0; 1; 1])); ## Test an option the method cannot use is warned about and ignored %!warning ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'linear', 'Constant', 7); %!warning ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'sum', 'EndValues', 99); %!warning ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'linear', 'IncludedEdge', ... %! 'right'); ## Test an ignored option leaves the result exactly as it was %!test %! st = warning ('off', 'all'); %! unwind_protect %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, [10; 20; 40], 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! v = retime (TT, th, 'linear', 'Constant', 7).v; %! unwind_protect_cleanup %! warning (st); %! end_unwind_protect %! assert_equal (v, [10; 20; 30; 40]); ## Test input validation for 'timetable.retime' with a constant %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'fillwithconstant'); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'fillwithconstant', 'Constant', ... %! [1 2]); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), ... %! seconds ((1:3)'), 'VariableNames', {'v'}), datetime (2024, 1, ... %! 1) + hours ((0:2)'), 'fillwithconstant', 'Constant', 9); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), ... %! string ({'a'; 'b'; 'c'}), 'VariableNames', {'v'}), datetime (2024, 1, ... %! 1) + hours ((0:2)'), 'fillwithconstant', 'Constant', 7); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'VariableNames', {'v'}), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'fillwithconstant', 'Constant', ... %! 0); ## Test a constant naming a category may be more than one character %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, categorical ({'x'; 'y'; 'x'}), 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! v = retime (TT, th, 'fillwithconstant', 'Constant', 'newcat').v; %! assert_equal (v, categorical ({'x'; 'y'; 'newcat'; 'x'})); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! datetime (2024, 1, 1) + hours ((0:2)'), 'fillwithconstant', 'Constant', ... %! 'zz'); ## Test 'synchronize' puts both timetables on every time either carries %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! B = timetable (t2, [1; 2; 4], 'VariableNames', {'b'}); %! rt = synchronize (A, B, 'union').Properties.RowTimes; %! assert_equal (rt, datetime (2024, 1, 1) + hours ((0:4)')); ## Test 'union' is the base when none is named %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! B = timetable (t2, [1; 2; 4], 'VariableNames', {'b'}); %! assert_equal (synchronize (A, B).Properties.RowTimes, ... %! synchronize (A, B, 'union').Properties.RowTimes); ## Test 'intersection' keeps only the times both carry %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! B = timetable (t2, [1; 2; 4], 'VariableNames', {'b'}); %! rt = synchronize (A, B, 'intersection').Properties.RowTimes; %! assert_equal (rt, datetime (2024, 1, 1) + hours (1)); ## Test 'commonrange' keeps the times within the span both cover %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! B = timetable (t2, [1; 2; 4], 'VariableNames', {'b'}); %! rt = synchronize (A, B, 'commonrange').Properties.RowTimes; %! assert_equal (rt, datetime (2024, 1, 1) + hours ((1:3)')); ## Test 'first' takes the row times of the first timetable %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! B = timetable (t2, [1; 2; 4], 'VariableNames', {'b'}); %! assert_equal (synchronize (A, B, 'first').Properties.RowTimes, t1); ## Test 'last' takes the row times of the last timetable %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! B = timetable (t2, [1; 2; 4], 'VariableNames', {'b'}); %! assert_equal (synchronize (A, B, 'last').Properties.RowTimes, t2); ## Test 'regular' builds the base from a time step %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! B = timetable (t2, [1; 2; 4], 'VariableNames', {'b'}); %! R = synchronize (A, B, 'regular', 'TimeStep', hours (1)); %! assert_equal (R.Properties.RowTimes, datetime (2024, 1, 1) + hours ((0:4)')); ## Test a named time step is the unit the result reports in %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! B = timetable (t2, [1; 2; 4], 'VariableNames', {'b'}); %! R = synchronize (A, B, 'regular', 'TimeStep', hours (1)); %! assert_equal (char (R.Properties.TimeStep), '1 hr'); ## Test each timetable keeps its own values on the shared base %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! B = timetable (t2, [1; 2; 4], 'VariableNames', {'b'}); %! assert_equal (synchronize (A, B, 'union').a, [10; 20; NaN; 40; NaN]); ## Test the second timetable is filled on the shared base too %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! B = timetable (t2, [1; 2; 4], 'VariableNames', {'b'}); %! assert_equal (synchronize (A, B, 'union').b, [NaN; 1; 2; NaN; 4]); ## Test a method applies to every timetable given %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! B = timetable (t2, [1; 2; 4], 'VariableNames', {'b'}); %! assert_equal (synchronize (A, B, 'union', 'linear').a, [10; 20; 30; 40; 50]); ## Test an aggregation applies to every timetable given %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! B = timetable (t2, [1; 2; 4], 'VariableNames', {'b'}); %! assert_equal (synchronize (A, B, 'union', 'sum').a, [10; 20; 0; 40; 0]); ## Test three timetables are set side by side %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! t3 = datetime (2024, 1, 1) + hours ([0 2])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! B = timetable (t2, [1; 2; 4], 'VariableNames', {'b'}); %! C = timetable (t3, [100; 200], 'VariableNames', {'c'}); %! R = synchronize (A, B, C, 'union'); %! assert_equal (R.Properties.VariableNames, {'a', 'b', 'c'}); ## Test a variable name carried by two timetables is renamed for each %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! D = timetable (t2, [7; 8; 9], 'VariableNames', {'a'}); %! R = synchronize (A, D, 'union'); %! assert_equal (R.Properties.VariableNames, {'a_A', 'a_D'}); ## Test a timetable with no rows takes no part in choosing the base %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! E = timetable (datetime.empty (0, 1), zeros (0, 1), 'VariableNames', {'z'}); %! assert_equal (size (synchronize (A, E, 'union')), [3, 2]); ## Test timetables that share no span have no common range %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t4 = datetime (2024, 1, 10) + hours ([0 1])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! E = timetable (t4, [5; 6], 'VariableNames', {'e'}); %! assert_equal (size (synchronize (A, E, 'commonrange')), [0, 2]); ## Test one timetable is put on its own base %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! assert_equal (size (synchronize (A, 'union')), [3, 1]); ## Test 'synchronize' keeps the variable units of each timetable %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! A.Properties.VariableUnits = {'m'}; %! B = timetable (t2, [1; 2; 4], 'VariableNames', {'b'}); %! u = synchronize (A, B, 'union').Properties.VariableUnits; %! assert_equal (u{1}, 'm'); ## Test 'synchronize' returns a timetable %!test %! t1 = datetime (2024, 1, 1) + hours ([0 1 3])'; %! t2 = datetime (2024, 1, 1) + hours ([1 2 4])'; %! A = timetable (t1, [10; 20; 40], 'VariableNames', {'a'}); %! B = timetable (t2, [1; 2; 4], 'VariableNames', {'b'}); %! assert_equal (class (synchronize (A, B, 'union')), 'timetable'); ## Test 'synchronize' puts duration row times on one base too %!test %! G = timetable (seconds ([0; 1; 3]), [10; 20; 40], 'VariableNames', {'g'}); %! H = timetable (seconds ([1; 2; 4]), [1; 2; 4], 'VariableNames', {'h'}); %! rt = synchronize (G, H, 'union').Properties.RowTimes; %! assert_equal (rt, seconds ((0:4)')); ## Test input validation for 'timetable.synchronize' %!test # a char variable's gaps are filled with blanks %! A = timetable (hours ([1; 2]), ['a'; 'b'], 'VariableNames', {'C'}); %! B = timetable (hours ([1.5; 2]), [3; 4], 'VariableNames', {'X'}); %! S = synchronize (A, B); %! assert_equal (S.C, ['a'; ' '; 'b']); %!test # one timetable synchronized with itself numbers the later names %! TT = timetable (hours ([1; 2]), [1; 2], 'VariableNames', {'A'}); %! assert_equal (synchronize (TT, TT).Properties.VariableNames, ... %! {'A_TT', 'A_TT_1'}); %! assert_equal (synchronize (TT, TT, TT).Properties.VariableNames, ... %! {'A_TT', 'A_TT_1', 'A_TT_2'}); %!error ... %! synchronize (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! timetable (seconds ((0:2)'), (1:3)'), 'union'); %!error ... %! synchronize (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)', ... %! 'VariableNames', {'b'}), 'bogus'); %!error ... %! synchronize (timetable (hours ((0:2)'), (1:3)'), ... %! timetable (hours ((0:2)'), (1:3)', 'VariableNames', {'b'}), 'Bogus', 1); %!error ... %! synchronize (timetable (hours ((0:2)'), (1:3)'), ... %! timetable (hours ((0:2)'), (1:3)', 'VariableNames', {'b'}), 'union', ... %! 'Bogus', 1); %!error ... %! synchronize (timetable (hours ((0:2)'), (1:3)'), ... %! timetable (hours ((0:2)'), (1:3)', 'VariableNames', {'b'}), 'union', ... %! 'linear', 'Constant'); %!error ... %! synchronize (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)', ... %! 'VariableNames', {'b'}), 'regular'); %!error ... %! synchronize (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)', ... %! 'VariableNames', {'b'}), 'union', {'linear', 'previous'}); ## Test 'lag' moves every value one row later by default %!test %! t = datetime (2024, 1, 1) + hours ((0:4)'); %! TT = timetable (t, (1:5)', 'VariableNames', {'v'}); %! assert_equal (lag (TT).v, [NaN; 1; 2; 3; 4]); ## Test 'lag' moves the values as many rows as it is asked %!test %! t = datetime (2024, 1, 1) + hours ((0:4)'); %! TT = timetable (t, (1:5)', 'VariableNames', {'v'}); %! assert_equal (lag (TT, 2).v, [NaN; NaN; 1; 2; 3]); ## Test a negative lag moves the values earlier %!test %! t = datetime (2024, 1, 1) + hours ((0:4)'); %! TT = timetable (t, (1:5)', 'VariableNames', {'v'}); %! assert_equal (lag (TT, -1).v, [2; 3; 4; 5; NaN]); ## A row count moves the data by rows, so a timetable regular in a calendar ## unit is lagged although no two of its steps are the same length and it is ## not regular with respect to absolute time (measured on MATLAB R2024a). %!test %! CM = timetable ((1:6)', 'TimeStep', calmonths (1), ... %! 'StartTime', datetime (2024, 6, 1)); %! assert_equal (isregular (CM), false); %! assert_equal (isregular (CM, 'months'), true); %!test %! CM = timetable ((1:6)', 'TimeStep', calmonths (1), ... %! 'StartTime', datetime (2024, 6, 1)); %! L = lag (CM, 1); %! assert_equal (L.Var1, [NaN; 1; 2; 3; 4; 5]); %! assert_equal (L.Properties.RowTimes, CM.Properties.RowTimes); %! assert_equal (L.Properties.TimeStep, calmonths (1)); %! assert_equal (isregular (L), false); %!test %! CM = timetable ((1:6)', 'TimeStep', calmonths (1), ... %! 'StartTime', datetime (2024, 6, 1)); %! assert_equal (lag (CM, 2).Var1, [NaN; NaN; 1; 2; 3; 4]); %!test %! CM = timetable ((1:6)', 'TimeStep', calmonths (1), ... %! 'StartTime', datetime (2024, 6, 1)); %! assert_equal (lag (CM, -1).Var1, [2; 3; 4; 5; 6; NaN]); ## Test the same lag named as a calendarDuration agrees with the row count %!test %! CM = timetable ((1:6)', 'TimeStep', calmonths (1), ... %! 'StartTime', datetime (2024, 6, 1)); %! assert_equal (lag (CM, calmonths (1)).Var1, [NaN; 1; 2; 3; 4; 5]); ## A lag is measured in time and not in row position, so a timetable whose ## row times descend moves its data the other way for the same count. ## Measured on MATLAB R2024a. %!test %! D = timetable (datetime (2024, 6, 1) + hours ([5; 4; 3; 2; 1; 0]), (1:6)'); %! assert_equal (seconds (D.Properties.TimeStep), -3600); %! assert_equal (isregular (D), true); %! assert_equal (lag (D, 1).Var1', [2, 3, 4, 5, 6, NaN]); %! assert_equal (lag (D, 2).Var1', [3, 4, 5, 6, NaN, NaN]); %! assert_equal (lag (D, -1).Var1', [NaN, 1, 2, 3, 4, 5]); ## Test a lag named as a duration was already signed by the step %!test %! D = timetable (datetime (2024, 6, 1) + hours ([5; 4; 3; 2; 1; 0]), (1:6)'); %! assert_equal (lag (D, hours (1)).Var1', [2, 3, 4, 5, 6, NaN]); ## Test duration row times that descend do the same %!test %! DD = timetable (hours ([5; 4; 3; 2; 1; 0]), (1:6)'); %! assert_equal (lag (DD, 1).Var1', [2, 3, 4, 5, 6, NaN]); ## Test an ascending timetable is unchanged by any of this %!test %! A = timetable (datetime (2024, 6, 1) + hours (0:5)', (1:6)'); %! assert_equal (lag (A, 1).Var1', [NaN, 1, 2, 3, 4, 5]); %! assert_equal (lag (A, -1).Var1', [2, 3, 4, 5, 6, NaN]); ## Test a timetable regular in days is lagged by a row count as well %!test %! DY = timetable ((1:5)', 'TimeStep', caldays (1), ... %! 'StartTime', datetime (2024, 6, 1)); %! assert_equal (lag (DY, 1).Var1, [NaN; 1; 2; 3; 4]); ## Test a lag of no rows leaves the values where they were %!test %! t = datetime (2024, 1, 1) + hours ((0:4)'); %! TT = timetable (t, (1:5)', 'VariableNames', {'v'}); %! assert_equal (lag (TT, 0).v, (1:5)'); ## Test a lag past the height leaves every row missing %!test %! t = datetime (2024, 1, 1) + hours ((0:4)'); %! TT = timetable (t, (1:5)', 'VariableNames', {'v'}); %! assert_equal (lag (TT, 99).v, NaN (5, 1)); ## Test the row times do not move %!test %! t = datetime (2024, 1, 1) + hours ((0:4)'); %! TT = timetable (t, (1:5)', 'VariableNames', {'v'}); %! assert_equal (lag (TT, 2).Properties.RowTimes, t); ## Test a duration lag counts by the time step %!test %! t = datetime (2024, 1, 1) + hours ((0:4)'); %! TT = timetable (t, (1:5)', 'VariableNames', {'v'}); %! assert_equal (lag (TT, hours (2)).v, [NaN; NaN; 1; 2; 3]); ## Test a negative duration lag moves the values earlier %!test %! t = datetime (2024, 1, 1) + hours ((0:4)'); %! TT = timetable (t, (1:5)', 'VariableNames', {'v'}); %! assert_equal (lag (TT, hours (-1)).v, [2; 3; 4; 5; NaN]); ## Test a calendarDuration lag counts by a calendar step %!test %! t = datetime (2024, 1, 1) + caldays ((0:4)'); %! TT = timetable (t, (1:5)', 'VariableNames', {'v'}); %! assert_equal (lag (TT, caldays (2)).v, [NaN; NaN; 1; 2; 3]); ## Test duration row times take a lag %!test %! TT = timetable (seconds ((0:3)'), (1:4)', 'VariableNames', {'v'}); %! assert_equal (lag (TT, 1).v, [NaN; 1; 2; 3]); ## Test a logical variable is vacated with false, having no missing value %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, logical ([1; 0; 1]), 'VariableNames', {'v'}); %! assert_equal (lag (TT).v, logical ([0; 1; 0])); ## Test a character variable is vacated with a space %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, ['a'; 'b'; 'c'], 'VariableNames', {'v'}); %! assert_equal (lag (TT).v, [' '; 'a'; 'b']); ## Test a string variable is vacated with a missing string %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, string ({'a'; 'b'; 'c'}), 'VariableNames', {'v'}); %! assert_equal (ismissing (lag (TT).v), [true; false; false]); ## Test a categorical variable is vacated with a missing category %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, categorical ({'x'; 'y'; 'x'}), 'VariableNames', {'v'}); %! assert_equal (ismissing (lag (TT).v), [true; false; false]); ## Test a cell variable is vacated with an empty entry %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, {'a'; 'b'; 'c'}, 'VariableNames', {'v'}); %! assert_equal (lag (TT).v, {''; 'a'; 'b'}); ## Test 'lag' keeps the variable units %!test %! t = datetime (2024, 1, 1) + hours ((0:4)'); %! TT = timetable (t, (1:5)', (11:15)', 'VariableNames', {'a', 'b'}); %! TT.Properties.VariableUnits = {'m', 'kg'}; %! assert_equal (lag (TT).Properties.VariableUnits, {'m', 'kg'}); ## Test 'lag' keeps the variable continuity %!test %! t = datetime (2024, 1, 1) + hours ((0:4)'); %! TT = timetable (t, (1:5)', (11:15)', 'VariableNames', {'a', 'b'}); %! TT.Properties.VariableContinuity = {'continuous', 'step'}; %! c = lag (TT).Properties.VariableContinuity; %! assert_equal (c, {'continuous', 'step'}); ## Test 'lag' keeps the time step %!test %! t = datetime (2024, 1, 1) + hours ((0:4)'); %! TT = timetable (t, (1:5)', 'VariableNames', {'v'}); %! assert_equal (lag (TT).Properties.TimeStep, TT.Properties.TimeStep); ## Test 'lag' shifts every variable %!test %! t = datetime (2024, 1, 1) + hours ((0:4)'); %! TT = timetable (t, (1:5)', (11:15)', 'VariableNames', {'a', 'b'}); %! assert_equal (lag (TT).b, [NaN; 11; 12; 13; 14]); ## Test 'lag' returns a timetable %!test %! t = datetime (2024, 1, 1) + hours ((0:4)'); %! TT = timetable (t, (1:5)', 'VariableNames', {'v'}); %! assert_equal (class (lag (TT)), 'timetable'); ## Test input validation for 'timetable.lag' %!error ... %! lag (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), 1.5); %!error ... %! lag (timetable (datetime (2024, 1, 1) + hours ([0 1 3]'), (1:3)')); %!error ... %! lag (timetable (datetime (2024, 1, 1), 1)); %!error ... %! lag (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! minutes (90)); %!error ... %! lag (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! caldays (1)); %!error ... %! lag (timetable (datetime (2024, 1, 1) + hours ((0:2)'), (1:3)'), ... %! calmonths (1)); ## Test a named sample rate is the unit the result reports its step in %!test %! ts = datetime (2024, 1, 1) + seconds ((0:4)'); %! TT = timetable (ts, (1:5)', 'VariableNames', {'v'}); %! R = retime (TT, 'regular', 'previous', 'SampleRate', 2); %! assert_equal (char (R.Properties.TimeStep), '0.5 sec'); ## Test a regular grid starts on a step boundary, not on the first row time %!test %! to = datetime (2024, 1, 1, 0, 17, 0) + hours (0:2)'; %! TT = timetable (to, (1:3)', 'VariableNames', {'v'}); %! rt = retime (TT, 'regular', 'previous', 'TimeStep', hours (1)); %! assert_equal (rt.Properties.RowTimes, datetime (2024, 1, ... %! 1) + hours ((0:3)')); ## Test a step that does not divide the span still covers it %!test %! tn = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tn, (1:4)', 'VariableNames', {'v'}); %! rt = retime (TT, 'regular', 'previous', 'TimeStep', minutes (50)); %! assert_equal (numel (rt.Properties.RowTimes), 5); ## Test 'prod' has no meaning for a duration variable %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), ... %! seconds ((1:3)'), 'VariableNames', {'v'}), datetime (2024, 1, ... %! 1) + hours ((0:2)'), 'prod'); ## Test an empty bin answers false for a logical variable %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, logical ([1; 0; 1; 0]), 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! v = retime (TT, tg, 'firstvalue').v; %! assert_equal (v, logical ([1; 0; 1; 0])); ## Test an empty bin answers with an empty entry for a cell variable %!test %! tc = datetime (2024, 1, 1) + hours ([0 1 4 5])'; %! TT = timetable (tc, {1; 2; 3; 4}, 'VariableNames', {'v'}); %! tg = datetime (2024, 1, 1) + hours (0:2:6)'; %! assert_equal (retime (TT, tg, 'firstvalue').v, {1; ''; 3; ''}); ## Test a constant of 'missing' fills with the variable's own missing value %!test %! tv = datetime (2024, 1, 1) + hours ([0 1 3])'; %! th = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tv, [10; 20; 40], 'VariableNames', {'v'}); %! v = retime (TT, th, 'fillwithconstant', 'Constant', missing).v; %! assert_equal (v, [10; 20; NaN; 40]); ## Test a constant of 'missing' serves a string variable %!test %! tv = datetime (2024, 1, 1) + hours ([0 1 3])'; %! th = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tv, string ({'a'; 'b'; 'c'}), 'VariableNames', {'v'}); %! v = retime (TT, th, 'fillwithconstant', 'Constant', missing).v; %! assert_equal (ismissing (v), [false; false; true; false]); %!error ... %! retime (timetable (datetime (2024, 1, 1) + hours ((0:2)'), ... %! logical ([1; 0; 1]), 'VariableNames', {'v'}), datetime (2024, 1, ... %! 1) + hours ((0:2)'), 'fillwithconstant', 'Constant', missing); ## Test a duration lag is counted against a calendar step %!test %! td = datetime (2024, 1, 1) + caldays (0:4)'; %! TT = timetable (td, (1:5)', 'VariableNames', {'v'}); %! assert_equal (lag (TT, days (1)).v, [NaN; 1; 2; 3; 4]); ## Test an empty bin has no mean for a logical variable, which means in double %!test %! ta = datetime (2024, 1, 1) + hours ([0 1 3])'; %! TT = timetable (ta, logical ([1; 0; 1]), 'VariableNames', {'v'}); %! th = datetime (2024, 1, 1) + hours (0:3)'; %! assert_equal (retime (TT, th, 'mean').v, [1; 0; NaN; 1]); ## Test 'addprop' adds a custom property describing the whole timetable %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, (1:3)', (11:13)', 'VariableNames', {'a', 'b'}); %! TT = addprop (TT, {'p'}, {'table'}); %! assert_equal (fieldnames (TT.Properties.CustomProperties), {'p'}); ## Test a table-typed custom property takes the value it is given %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, (1:3)', 'VariableNames', {'a'}); %! TT = addprop (TT, {'p'}, {'table'}); %! TT.Properties.CustomProperties.p = 7; %! assert_equal (TT.Properties.CustomProperties.p, 7); ## Test a variable-typed custom property takes one entry per variable %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, (1:3)', (11:13)', 'VariableNames', {'a', 'b'}); %! TT = addprop (TT, {'q'}, {'variable'}); %! TT.Properties.CustomProperties.q = {'m', 'kg'}; %! assert_equal (TT.Properties.CustomProperties.q, {'m', 'kg'}); ## Test 'rmprop' removes a custom property %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, (1:3)', 'VariableNames', {'a'}); %! TT = addprop (TT, {'p'}, {'table'}); %! TT = rmprop (TT, {'p'}); %! assert_equal (isempty (fieldnames (TT.Properties.CustomProperties)), true); ## Test 'rmprop' ignores a name matching no custom property %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, (1:3)', 'VariableNames', {'a'}); %! assert_equal (class (rmprop (TT, {'nope'})), 'timetable'); ## Test a variable-typed property gains an entry with a new variable %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, (1:3)', (11:13)', 'VariableNames', {'a', 'b'}); %! TT = addprop (TT, {'q'}, {'variable'}); %! TT.Properties.CustomProperties.q = {'m', 'kg'}; %! V = addvars (TT, (21:23)', 'NewVariableNames', {'c'}); %! assert_equal (numel (V.Properties.CustomProperties.q), 3); ## Test a variable-typed property loses the entry of a removed variable %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, (1:3)', (11:13)', 'VariableNames', {'a', 'b'}); %! TT = addprop (TT, {'q'}, {'variable'}); %! TT.Properties.CustomProperties.q = {'m', 'kg'}; %! V = removevars (TT, 'a'); %! assert_equal (V.Properties.CustomProperties.q, {'kg'}); ## Test a variable-typed property follows a moved variable %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, (1:3)', (11:13)', 'VariableNames', {'a', 'b'}); %! TT = addprop (TT, {'q'}, {'variable'}); %! TT.Properties.CustomProperties.q = {'m', 'kg'}; %! V = movevars (TT, 'b', 'Before', 'a'); %! assert_equal (V.Properties.CustomProperties.q, {'kg', 'm'}); ## Test a variable-typed property survives a resampling %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, (1:3)', (11:13)', 'VariableNames', {'a', 'b'}); %! TT = addprop (TT, {'q'}, {'variable'}); %! TT.Properties.CustomProperties.q = {'m', 'kg'}; %! V = retime (TT, 'hourly', 'previous'); %! assert_equal (V.Properties.CustomProperties.q, {'m', 'kg'}); ## Test a variable-typed property survives a shift %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, (1:3)', (11:13)', 'VariableNames', {'a', 'b'}); %! TT = addprop (TT, {'q'}, {'variable'}); %! TT.Properties.CustomProperties.q = {'m', 'kg'}; %! assert_equal (lag (TT).Properties.CustomProperties.q, {'m', 'kg'}); ## Test input validation for 'timetable.addprop' and 'timetable.rmprop' %!error ... %! addprop (timetable (datetime (2024, 1, 1), 1), {'p'}); %!error ... %! addprop (timetable (datetime (2024, 1, 1), 1), {'p'}, {'bogus'}); %!error ... %! addprop (addprop (timetable (datetime (2024, 1, 1), 1), {'p'}, ... %! {'table'}), {'p'}, {'table'}); %!error ... %! rmprop (timetable (datetime (2024, 1, 1), 1)); %!error ... %! rmprop (timetable (datetime (2024, 1, 1), 1), 7); ## Test a merged variable has no entry in a variable-typed custom property %!test %! t = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (t, (1:3)', (11:13)', 'VariableNames', {'a', 'b'}); %! TT = addprop (TT, {'q'}, {'variable'}); %! TT.Properties.CustomProperties.q = {'m', 'kg'}; %! q = mergevars (TT, {'a', 'b'}).Properties.CustomProperties.q; %! assert_equal (q, {[]}); ## Test a shared message names the timetable rather than a table %!error ... %! fillmissing (timetable (datetime (2024, 1, 1) + hours ((0:2)'), ... %! [1; NaN; 3], 'VariableNames', {'v'}), 'constant', {1, 2, 3}); %!error ... %! standardizeMissing (timetable (datetime (2024, 1, 1) + hours ((0:2)'), ... %! [1; NaN; 3], 'VariableNames', {'v'}), {@sin}); %!error ... %! pivot (timetable (datetime (2024, 1, 1) + hours ((0:1)'), ... %! categorical ({'a'; 'b'}), 'VariableNames', {'g'}), 'Rows', 'g', ... %! 'IncludeTotals', {1}); ## Test a constant fills a gap that was there before the call %!test %! tm = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tm, [10; NaN; 30; 40], 'VariableNames', {'v'}); %! v = retime (TT, tm, 'fillwithconstant', 'Constant', 7).v; %! assert_equal (v, [10; 7; 30; 40]); ## Test a constant fills an added row and an old gap alike %!test %! tm = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tm, [10; NaN; 30; 40], 'VariableNames', {'v'}); %! tq = datetime (2024, 1, 1) + minutes (0:30:180)'; %! v = retime (TT, tq, 'fillwithconstant', 'Constant', 7).v; %! assert_equal (v, [10; 7; 7; 7; 30; 7; 40]); ## Test a constant fills a missing string %!test %! tm = datetime (2024, 1, 1) + hours (0:3)'; %! TT = timetable (tm, string ({'a'; missing; 'c'; 'd'}), ... %! 'VariableNames', {'v'}); %! v = retime (TT, tm, 'fillwithconstant', 'Constant', 'z').v; %! assert_equal (v, string ({'a'; 'z'; 'c'; 'd'})); ## Test a regular grid floors its start to a step boundary from midnight %!test %! to = datetime (2024, 1, 1, 0, 17, 0) + hours (0:2)'; %! TT = timetable (to, (1:3)', 'VariableNames', {'v'}); %! rt = retime (TT, 'regular', 'previous', 'TimeStep', minutes (7)); %! assert_equal (rt.Properties.RowTimes(1), datetime (2024, 1, 1, 0, 14, 0)); ## Test the origin is the day rather than the week or the epoch %!test %! tw = datetime (2024, 3, 6, 5, 43, 0) + hours (0:2)'; %! TT = timetable (tw, (1:3)', 'VariableNames', {'v'}); %! rt = retime (TT, 'regular', 'previous', 'TimeStep', minutes (50)); %! assert_equal (rt.Properties.RowTimes(1), datetime (2024, 3, 6, 5, 0, 0)); ## Test duration row times floor from zero %!test %! TT = timetable (seconds ([17; 77; 137]), (1:3)', 'VariableNames', {'v'}); %! rt = retime (TT, 'regular', 'previous', 'TimeStep', seconds (50)); %! assert_equal (rt.Properties.RowTimes, seconds ((0:50:150)')); ## Test a numeric variable-scoped property gains a missing entry, not a cell %!test %! tm = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (tm, (1:3)', (11:13)', 'VariableNames', {'a', 'b'}); %! TT = addprop (TT, {'q'}, {'variable'}); %! TT.Properties.CustomProperties.q = [10 20]; %! q = timetable2table (TT).Properties.CustomProperties.q; %! assert_equal (q, [NaN 10 20]); ## Test a merged variable blanks a numeric entry with a missing value %!test %! tm = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (tm, (1:3)', (11:13)', 'VariableNames', {'a', 'b'}); %! TT = addprop (TT, {'q'}, {'variable'}); %! TT.Properties.CustomProperties.q = [10 20]; %! q = mergevars (TT, {'a', 'b'}).Properties.CustomProperties.q; %! assert_equal (q, NaN); ################################################################################ ## ** 'timetable2csv' ** ## ################################################################################ ## Test the row-time column is tagged with its type, zone and format %!test %! t = datetime (2024, 1, (1:2)', 'TimeZone', 'UTC'); %! t.Format = 'uuuu-MM-dd'; %! TT = timetable (t, (1:2)', 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! timetable2csv (TT, fname); %! txt = fileread (fname); %! assert_equal (! isempty (strfind (txt, ... %! '"RowTimes|datetime UTC|uuuu-MM-dd"')), true); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the row times are written in ISO form, not in their display format %!test %! t = datetime (2024, 1, 2); %! t.Format = 'dd/MM/uuuu'; %! TT = timetable (t, 1, 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! timetable2csv (TT, fname); %! txt = fileread (fname); %! assert_equal (! isempty (strfind (txt, '2024-01-02T00:00:00')), true); %! assert_equal (isempty (strfind (txt, '02/01/2024')), true); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the row dimension name is written in the variable-name row %!test %! t = datetime (2024, 1, (1:2)'); %! TT = timetable (t, (1:2)', 'VariableNames', {'v'}); %! TT.Properties.DimensionNames{1} = 'Stamp'; %! fname = [tempname(), '.csv']; %! unwind_protect %! timetable2csv (TT, fname); %! lines = strsplit (fileread (fname), "\n"); %! assert_equal (strncmp (lines{3}, '"Stamp","v"', 11), true); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test too few input arguments %!error ... %! timetable2csv (timetable ((1:3)', 'TimeStep', hours (1))) ## Test an invalid FILE %!error ... %! timetable2csv (timetable ((1:3)', 'TimeStep', hours (1)), 42) ## Test an unknown option, and one without a value %!error ... %! timetable2csv (timetable ((1:3)', 'TimeStep', hours (1)), 'f.csv', ... %! 'NoSuchOption', 1) %!error ... %! timetable2csv (timetable ((1:3)', 'TimeStep', hours (1)), 'f.csv', ... %! 'NoSuchOption') ## Test an invalid 'WriteVariableNames' %!error ... %! timetable2csv (timetable ((1:3)', 'TimeStep', hours (1)), 'f.csv', ... %! 'WriteVariableNames', 'yes') ################################################################################ ## ** 'timetable2ods' ** ## ################################################################################ ## Test the row-time column is tagged with its type, zone and format %!test %! t = datetime (2024, 1, (1:2)', 'TimeZone', 'UTC'); %! t.Format = 'uuuu-MM-dd'; %! TT = timetable (t, (1:2)', 'VariableNames', {'v'}); %! fname = [tempname(), '.fods']; %! unwind_protect %! timetable2ods (TT, fname); %! txt = fileread (fname); %! assert_equal (! isempty (strfind (txt, ... %! 'RowTimes|datetime UTC|uuuu-MM-dd')), true); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the row dimension name heads the data sheet %!test %! t = datetime (2024, 1, (1:2)'); %! TT = timetable (t, (1:2)', 'VariableNames', {'v'}); %! TT.Properties.DimensionNames{1} = 'Stamp'; %! fname = [tempname(), '.ods']; %! unwind_protect %! timetable2ods (TT, fname); %! assert_equal (ods2table (fname).Properties.VariableNames, {'Stamp', 'v'}); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test 'append' adds the rows to an existing sheet %!test %! t = datetime (2024, 1, (1:2)'); %! TT = timetable (t, (1:2)', 'VariableNames', {'v'}); %! fname = [tempname(), '.ods']; %! unwind_protect %! timetable2ods (TT, fname, 'Sheet', 'S'); %! TT2 = timetable (datetime (2024, 1, (3:4)'), (3:4)', ... %! 'VariableNames', {'v'}); %! timetable2ods (TT2, fname, 'Sheet', 'S', 'WriteMode', 'append'); %! assert_equal (height (ods2timetable (fname, 'Sheet', 'S')), 4); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test 'replacefile' discards the other sheets %!test %! t = datetime (2024, 1, (1:2)'); %! TT = timetable (t, (1:2)', 'VariableNames', {'v'}); %! fname = [tempname(), '.ods']; %! unwind_protect %! table2ods (table ((1:2)', 'VariableNames', {'a'}), fname, 'Sheet', 'One'); %! timetable2ods (TT, fname, 'Sheet', 'Two', 'WriteMode', 'replacefile'); %! assert_equal (fieldnames (ods2struct (fname)), {'Two'}); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test too few input arguments %!error ... %! timetable2ods (timetable ((1:3)', 'TimeStep', hours (1))) ## Test an invalid FILE %!error ... %! timetable2ods (timetable ((1:3)', 'TimeStep', hours (1)), 42) ## Test an invalid file extension %!error ... %! timetable2ods (timetable ((1:3)', 'TimeStep', hours (1)), 'f.txt') ## Test an unknown option, and one without a value %!error ... %! timetable2ods (timetable ((1:3)', 'TimeStep', hours (1)), 'f.ods', ... %! 'NoSuchOption', 1) %!error ... %! timetable2ods (timetable ((1:3)', 'TimeStep', hours (1)), 'f.ods', ... %! 'NoSuchOption') ## Test an invalid 'WriteVariableNames' %!error ... %! timetable2ods (timetable ((1:3)', 'TimeStep', hours (1)), 'f.ods', ... %! 'WriteVariableNames', 'yes') ## Test an invalid 'Sheet' %!error ... %! timetable2ods (timetable ((1:3)', 'TimeStep', hours (1)), 'f.ods', ... %! 'Sheet', 42) ## Test an invalid 'WriteMode' %!error ... %! timetable2ods (timetable ((1:3)', 'TimeStep', hours (1)), 'f.ods', ... %! 'WriteMode', 'nosuchmode') ## Test a variable of a type the writers cannot write is refused %!error ... %! timetable2ods (timetable (inputParser (), 'TimeStep', hours (1)), ... %! [tempname() '.ods']) %!error ... %! timetable2csv (timetable (inputParser (), 'TimeStep', hours (1)), ... %! [tempname() '.csv']) ################################################################################ ## ** 'writetimetable' ** ## ################################################################################ ## Test the row dimension name heads the file, not 'Row' %!test %! TT = timetable (datetime (2024, 1, (1:2)'), (1:2)', 'VariableNames', {'v'}); %! TT.Properties.DimensionNames{1} = 'Stamp'; %! fname = [tempname(), '.csv']; %! unwind_protect %! writetimetable (TT, fname); %! lines = strsplit (fileread (fname), "\n"); %! assert_equal (lines{1}, 'Stamp,v'); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test a duration row time is written as a clock, not as its display format %!test %! TT = timetable (hours ([1; 2]), (1:2)', 'VariableNames', {'v'}); %! fname = [tempname(), '.csv']; %! unwind_protect %! writetimetable (TT, fname); %! txt = fileread (fname); %! assert_equal (! isempty (strfind (txt, '01:00:00')), true); %! assert_equal (isempty (strfind (txt, '1 hr')), true); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test the row times reach a spreadsheet as native cells %!test %! TT = timetable (datetime (2024, 1, (1:2)'), (1:2)', 'VariableNames', {'v'}); %! fname = [tempname(), '.fods']; %! unwind_protect %! writetimetable (TT, fname); %! txt = fileread (fname); %! hit = strfind (txt, 'office:date-value="2024-01-01'); %! assert_equal (! isempty (hit), true); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test too few input arguments %!error ... %! writetimetable (timetable ((1:3)', 'TimeStep', hours (1))) ## Test an invalid FILENAME %!error ... %! writetimetable (timetable ((1:3)', 'TimeStep', hours (1)), 42) ## Test an unknown option, and one without a value %!error ... %! writetimetable (timetable ((1:3)', 'TimeStep', hours (1)), 'f.csv', ... %! 'NoSuchOption', 1) %!error ... %! writetimetable (timetable ((1:3)', 'TimeStep', hours (1)), 'f.csv', ... %! 'NoSuchOption') ## Test an invalid 'WriteVariableNames' %!error ... %! writetimetable (timetable ((1:3)', 'TimeStep', hours (1)), 'f.csv', ... %! 'WriteVariableNames', 'yes') ## Test an unsupported extension %!error ... %! writetimetable (timetable ((1:3)', 'TimeStep', hours (1)), 'f.bogus') ## Test a legacy Excel extension %!error ... %! writetimetable (timetable ((1:3)', 'TimeStep', hours (1)), 'f.xls') ## Test 'Sheet' is refused for a text file %!error ... %! writetimetable (timetable ((1:3)', 'TimeStep', hours (1)), 'f.csv', ... %! 'Sheet', 'S') ## Test an invalid text 'WriteMode' %!error ... %! writetimetable (timetable ((1:3)', 'TimeStep', hours (1)), 'f.csv', ... %! 'WriteMode', 'nosuchmode') ## Test an unknown 'FileType' is refused whatever the extension says %!error ... %! writetimetable (timetable ((1:3)', 'TimeStep', hours (1)), 'f.csv', ... %! 'FileType', 'binary') ## Test a spreadsheet 'Sheet' must be a name %!error ... %! writetimetable (timetable ((1:3)', 'TimeStep', hours (1)), 'f.ods', ... %! 'Sheet', 3) ## Test a 'Range' cannot be appended to %!error ... %! writetimetable (timetable ((1:3)', 'TimeStep', hours (1)), 'f.ods', ... %! 'Range', 'A1', 'WriteMode', 'append') ## Test a 'Range' cannot be written into a file that is already there. The ## target is a file of the package itself, so the refusal is proved without ## a file being created that the failing block could not delete again. %!error ... %! writetimetable (timetable ((1:3)', 'TimeStep', hours (1)), ... %! file_in_loadpath ('timetable.m'), ... %! 'FileType', 'spreadsheet', 'Range', 'A1') ## Test a variable of a type the writer cannot write is refused %!error ... %! writetimetable (timetable (inputParser (), 'TimeStep', hours (1)), ... %! [tempname() '.csv']) ################################################################################ ## ** Conversion to arrays ** ## ################################################################################ ## Test 'table2array' joins the variables and leaves the row times out %!test %! TT = timetable (hours ([1; 2]), [1.5; 2], [3; 4]); %! assert_equal (table2array (TT), [1.5, 3; 2, 4]); %!assert_equal (table2array (timetable (hours ([1; 2]), [1; 2], ... %! string ({'x'; 'y'}))), ... %! string ({'1', 'x'; '2', 'y'})) %!assert_equal (table2array (timetable (hours ([1; 2]), [1; 2], ... %! {'a'; 'b'})), {1, 'a'; 2, 'b'}) %!test %! TT = timetable (hours ([1; 2]), [1; 2], [3; 4]); %! assert_equal (table2array (TT(1:0,:)), zeros (0, 2)); ## Test 'table2cell' holds one row of one variable in each cell %!test %! TT = timetable (hours ([1; 2]), [1.5; 2], {'a'; 'b'}); %! assert_equal (table2cell (TT), {1.5, 'a'; 2, 'b'}); %!assert_equal (table2cell (timetable (hours ([1; 2]), [1, 2; 3, 4])), ... %! {[1, 2]; [3, 4]}) %!assert_equal (table2cell (eventtable (hours ([1; 2]), 'EventLabels', ... %! {'a'; 'b'})), {'a'; 'b'}) %!error ... %! table2cell (timetable (containers.Map ({'a'}, {1}), 'TimeStep', hours (1))) ## Test 'table2struct' leaves the row times out %!test %! TT = timetable (hours ([1; 2]), [1.5; 2], [3; 4]); %! S = table2struct (TT); %! assert_equal (size (S), [2, 1]); %! assert_equal (fieldnames (S), {'Var1'; 'Var2'}); %! assert_equal ([S.Var1], [1.5, 2]); %! assert_equal ([S.Var2], [3, 4]); %!test %! TT = timetable (hours ([1; 2]), [1.5; 2], [3; 4]); %! S = table2struct (TT, 'ToScalar', true); %! assert_equal (S, struct ('Var1', [1.5; 2], 'Var2', [3; 4])); %!test %! TT = timetable (hours ([1; 2]), [1.5; 2], [3; 4]); %! assert_equal (size (table2struct (TT, 'ToScalar', false)), [2, 1]); %! assert_equal (size (table2struct (TT(1:0,:))), [0, 1]); %!error ... %! table2struct (timetable (hours (1), 1), 'ToScalar') %!error ... %! table2struct (timetable (hours (1), 1), 'Scalar', true) %!error ... %! table2struct (timetable (containers.Map ({'a'}, {1}), 'TimeStep', hours (1))) ################################################################################ ## ** 'timetable2table' ** ## ################################################################################ %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! V = timetable (tv, (1:3)', (4:6)', 'VariableNames', {'A', 'B'}); %! T = timetable2table (V); %! assert_equal (class (T), 'table'); %! assert_equal (size (T), [3, 3]); %! assert_equal (T.Properties.VariableNames, {'tv', 'A', 'B'}); %! assert_equal (T.tv, tv); %! assert_equal (T.Properties.DimensionNames, {'Row', 'Variables'}); ## Test the result carries no row names %!test %! V = timetable (hours (0:2)', (1:3)', 'VariableNames', {'A'}); %! assert_equal (timetable2table (V).Properties.RowNames, {}); ## Test 'ConvertRowTimes' false discards the times %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! V = timetable (tv, (1:3)', (4:6)', 'VariableNames', {'A', 'B'}); %! T = timetable2table (V, 'ConvertRowTimes', false); %! assert_equal (size (T), [3, 2]); %! assert_equal (T.Properties.VariableNames, {'A', 'B'}); %! assert_equal (T.Properties.DimensionNames, {'Row', 'Variables'}); ## Test duration row times become a duration variable %!test %! V = timetable (hours (0:2)', (1:3)', 'VariableNames', {'A'}); %! T = timetable2table (V); %! assert_equal (class (T.Time), 'duration'); ## Test the variable metadata shifts along with the variables %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! V = timetable (tv, (1:3)', (4:6)', 'VariableNames', {'A', 'B'}); %! V.Properties.VariableUnits = {'m', 's'}; %! V.Properties.Description = 'a description'; %! T = timetable2table (V); %! assert_equal (T.Properties.VariableUnits, {'', 'm', 's'}); %! assert_equal (T.Properties.Description, 'a description'); ## Test the metadata is not shifted when the times are discarded %!test %! tv = datetime (2024, 1, 1) + hours (0:2)'; %! V = timetable (tv, (1:3)', (4:6)', 'VariableNames', {'A', 'B'}); %! V.Properties.VariableUnits = {'m', 's'}; %! T = timetable2table (V, 'ConvertRowTimes', false); %! assert_equal (T.Properties.VariableUnits, {'m', 's'}); %!error ... %! timetable2table (timetable (hours (0:2)', (1:3)'), ... %! 'ConvertRowTimes', 'yes'); %!error ... %! timetable2table (timetable (hours (0:2)', (1:3)'), 'Bogus', 1); %!error ... %! timetable2table (timetable (hours (0:2)', (1:3)'), 'Bogus'); ## Test a custom property describing the timetable survives the conversion %!test %! tv = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (tv, (1:3)', 'VariableNames', {'A'}); %! TT = addprop (TT, {'p'}, {'table'}); %! TT.Properties.CustomProperties.p = 7; %! assert_equal (timetable2table (TT).Properties.CustomProperties.p, 7); ## Test the row times become a variable with no entry of their own %!test %! tv = datetime (2024, 1, 1) + hours ((0:2)'); %! TT = timetable (tv, (1:3)', (11:13)', 'VariableNames', {'A', 'B'}); %! TT = addprop (TT, {'q'}, {'variable'}); %! TT.Properties.CustomProperties.q = {'m', 'kg'}; %! q = timetable2table (TT).Properties.CustomProperties.q; %! assert_equal (numel (q), 3); %! assert_equal (q(2:3), {'m', 'kg'}); pr0m1th3as-datatypes-9450b29/inst/tests/tscollection.m-tst000066400000000000000000001671511527370255000235200ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## Expected values measured on MATLAB R2024a. ################################################################################ ## ** Create and describe 'tscollection' ** ## ################################################################################ ## Available Methods ## ## ## ## 'tscollection' 'get' 'set' 'fieldnames' ## ## 'properties' 'size' 'length' 'isempty' ## ## 'isequal' ## ## ## ################################################################################ %!shared A, B, C, TC, DA %! A = timeseries ([1; 2; 3; 4], [0; 1; 2; 3], [1; 2; 3; 4], 'Name', 'a'); %! A.DataInfo.Units = 'm'; %! A.Events = tsdata.event ('ev', 1.5); %! B = timeseries ([10, 11; 20, 21; 30, 31; 40, 41], [0; 1; 2; 3], 'Name', 'b'); %! C = timeseries ([5; 6; 7; 8], [0; 1; 2; 3], 'Name', 'c'); %! TC = tscollection ({A, B}); %! DA = timeseries ([1; 2; 3], {'01-Jan-2024', '02-Jan-2024', ... %! '03-Jan-2024'}, 'Name', 'da'); ## Test an empty collection %!test %! t = tscollection (); %! assert_equal (size (t), [0, 0]); %! assert_equal (t.Length, 0); %! assert_equal (t.Time, zeros (0, 1)); %! assert_equal (t.Name, 'unnamed'); %! assert_equal (gettimeseriesnames (t), {}); ## Test one series, on its own time vector %!test %! t = tscollection (A); %! assert_equal (gettimeseriesnames (t), {'a'}); %! assert_equal (t.Name, 'unnamed'); %! assert_equal (t.Time, [0; 1; 2; 3]); %! assert_equal (t.Length, 4); ## Test a cell array of series, row or column %!assert_equal (gettimeseriesnames (tscollection ({A, B})), {'a', 'b'}) %!assert_equal (gettimeseriesnames (tscollection ({A; B})), {'a', 'b'}) %!assert_equal (size (tscollection ({A, B})), [4, 2]) %!assert_equal (size (tscollection ({})), [0, 0]) ## Test the name option, any case, the last one winning %!assert_equal (tscollection ({A, B}, 'Name', 'mycol').Name, 'mycol') %!assert_equal (tscollection ({A, B}, 'name', 'x', 'NAME', 'y').Name, 'y') %!assert_equal (tscollection (A, 'Name', "str").Name, 'str') %!assert_equal (tscollection ([0; 1], 'Name', 'tv').Name, 'tv') ## Test a time vector gives no members, sorted, a row stored as a column %!test %! t = tscollection ([3; 1; 2]); %! assert_equal (t.Time, [1; 2; 3]); %! assert_equal (size (t), [3, 0]); %! assert_equal (gettimeseriesnames (t), {}); %!assert_equal (tscollection ([0, 1, 2]).Time, [0; 1; 2]) %!assert_equal (tscollection ([0; 1; 1; 2]).Time, [0; 1; 1; 2]) %!assert_equal (tscollection (5).Time, 5) %!assert_equal (size (tscollection (5)), [1, 0]) %!assert_equal (size (tscollection ([])), [0, 0]) ## Test an integer time vector is stored as double; MATLAB keeps the class %!assert_equal (class (tscollection (int32 ([0; 1; 2])).Time), 'double') %!assert_equal (class (tscollection (single ([0; 1; 2])).Time), 'double') ## Test member names follow 'matlab.lang.makeValidName' %!assert_equal (gettimeseriesnames (tscollection (timeseries ([1; 2]))), ... %! {'unnamed'}) %!assert_equal (gettimeseriesnames (tscollection (timeseries ([1; 2], ... %! [0; 1], 'Name', 'my data'))), {'myData'}) %!assert_equal (gettimeseriesnames (tscollection (timeseries ([1; 2], ... %! [0; 1], 'Name', '1abc'))), {'x1abc'}) %!assert_equal (gettimeseriesnames (tscollection (timeseries ([1; 2], ... %! [0; 1], 'Name', ''))), {'x'}) %!assert_equal (gettimeseriesnames (tscollection ({timeseries([1; 2], ... %! [0; 1], 'Name', 'x y'), timeseries([1; 2], [0; 1], ... %! 'Name', 'x_y')})), {'xY', 'x_y'}) ## Test a member takes its name as its series' name %!test %! t = tscollection (timeseries ([1; 2], [0; 1], 'Name', 'my data')); %! x = t.myData; %! assert_equal (x.Name, 'myData'); ## Test a member keeps its data, quality codes, units and events %!test %! t = tscollection (A); %! x = t.a; %! assert_equal (x.Data, [1; 2; 3; 4]); %! assert_equal (x.Quality, [1; 2; 3; 4]); %! assert_equal (x.DataInfo.Units, 'm'); %! assert_equal (x.Events(1).Name, 'ev'); %! assert_equal (x.Events(1).Time, 1.5); ## Test a dated series gives a dated collection %!test %! t = tscollection (DA); %! assert_equal (t.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %! assert_equal (t.TimeInfo.Units, 'days'); %! assert_equal (t.Time, [0; 1; 2]); %! assert_equal (getabstime (t), {'01-Jan-2024 00:00:00'; ... %! '02-Jan-2024 00:00:00'; ... %! '03-Jan-2024 00:00:00'}); ## Test a member with time along the last dimension %!test %! t = tscollection (timeseries (reshape (1:8, 1, 2, 4), [0; 1; 2; 3], ... %! 'Name', 'd3')); %! assert_equal (size (t), [4, 1]); %! x = t.d3; %! assert_equal (size (x.Data), [1, 2, 4]); %! assert_equal (x.IsTimeFirst, false); ## Test a series with no samples gives an empty collection, as in MATLAB %!test %! t = tscollection (timeseries ('nm')); %! assert_equal (size (t), [0, 0]); %! assert_equal (gettimeseriesnames (t), {}); %!assert_equal (size (tscollection (timeseries ())), [0, 0]) %!assert_equal (size (tscollection (timeseries (zeros (0, 1), ... %! zeros (0, 1)))), [0, 0]) ## Test a name differing from a property in case is a member's %!test %! t = tscollection (timeseries ([1; 2], [0; 1], 'Name', 'name')); %! assert_equal (gettimeseriesnames (t), {'name'}); %! assert_equal (class (t.name), 'timeseries'); %! assert_equal (t.Name, 'unnamed'); ## Test repeated times are accepted %!test %! t = tscollection (timeseries ([1; 2; 3; 4], [0; 1; 1; 2], 'Name', 'd')); %! assert_equal (t.Time, [0; 1; 1; 2]); %! assert_equal (hasduplicatetimes (t), true); ## Test a dated series joining an undated collection loses its start date %!test %! X = timeseries ([5; 6; 7; 8], [0; 1; 2; 3], 'Name', 'x'); %! X.TimeInfo.StartDate = '01-Jan-2024'; %! warning ('off', 'all', 'local'); %! t = tscollection ({A, X}); %! x = t.x; %! assert_equal (x.TimeInfo.StartDate, ''); %! assert_equal (t.TimeInfo.StartDate, ''); %!warning ... %! X = timeseries ([5; 6; 7; 8], [0; 1; 2; 3], 'Name', 'x'); ... %! X.TimeInfo.StartDate = '01-Jan-2024'; ... %! tscollection ({A, X}); %!error tscollection (A, B) %!error tscollection ([0; 1; 2; 3], C) %!error ... %! tscollection (A, 'abc') %!error ... %! tscollection ([0; 1], 'Name') %!error ... %! tscollection (A, 'Foo', 1) %!error ... %! tscollection (A, 'Time', [5; 6; 7; 8]) %!error ... %! tscollection (A, 'Name', 5) %!error ... %! tscollection ('abc') %!error ... %! tscollection (TC) %!error ... %! tscollection (ones (2, 2)) %!error ... %! tscollection ({A, 3}) %!error ... %! tscollection ([A, A]) %!error tscollection ([0; 1; NaN]) %!error tscollection ([0; 1; Inf]) %!error ... %! tscollection ({A, timeseries([1; 2; 3], [0; 1; 2], 'Name', 'z')}) %!error ... %! tscollection ({A, timeseries([1; 2; 3; 4], [0; 1; 2; 4], 'Name', 'z')}) %!error ... %! AM = timeseries ([1; 2; 3; 4], [0; 1; 2; 3], 'Name', 'am'); ... %! AM.TimeInfo.Units = 'minutes'; ... %! tscollection ({B, AM}) %!error ... %! DB = timeseries ([4; 5; 6], {'05-Jan-2024', '06-Jan-2024', ... %! '07-Jan-2024'}, 'Name', 'db'); ... %! tscollection ({DA, DB}) ## Test a dated member's events stay placed when its start date is dropped %!test %! d = timeseries ([4; 5; 6], [0; 1; 2], 'Name', 'd'); %! d.TimeInfo.StartDate = '01-Jan-2024'; %! d = addevent (d, 'ev', 1); %! warning ('off', 'all', 'local'); %! t = tscollection ({timeseries([1; 2; 3], [0; 1; 2], 'Name', 'u'), d}); %! x = t.d; %! assert_equal (x.Events(1).StartDate, ''); %! assert_equal (gettsatevent (x, 'ev').Data, 5); %! assert_equal (class (timeseries2timetable (t).Properties.Events), ... %! 'eventtable'); %!error ... %! X = timeseries ([5; 6; 7; 8], [0; 1; 2; 3], 'Name', 'x'); ... %! X.TimeInfo.StartDate = '01-Jan-2024'; ... %! tscollection ({X, A}) %!error ... %! tscollection ({A, A}) %!error ... %! tscollection ({A, timeseries([1; 2; 3; 4], [0; 1; 2; 3], 'Name', 'A')}) %!error ... %! tscollection ({timeseries([1; 2]), timeseries([3; 4])}) ## Test a member cannot take a property's name; MATLAB lets 'Time' hide it %!error ... %! tscollection (timeseries ([1; 2], [0; 1], 'Name', 'Time')) %!error ... %! tscollection (timeseries ([1; 2], [0; 1], 'Name', 'Length')) %!error ... %! tscollection ({timeseries(), timeseries()}) %!error ... %! tscollection ({A, timeseries('e')}) %!test %! S = get (TC); %! assert_equal (fieldnames (S), {'Name'; 'Time'; 'TimeInfo'; 'Length'; ... %! 'a'; 'b'}); %! assert_equal (S.Name, 'unnamed'); %! assert_equal (S.Time, [0; 1; 2; 3]); %! assert_equal (S.Length, 4); %! assert_equal (class (S.TimeInfo), 'tsdata.timemetadata'); %! assert_equal (class (S.a), 'timeseries'); %!assert_equal (get (TC, 'Name'), 'unnamed') %!assert_equal (get (TC, 'name'), 'unnamed') %!assert_equal (get (TC, "Length"), 4) %!assert_equal (get (TC, 'b').Data(1,:), [10, 11]) %!test %! v = get (TC, {'Name', 'Length'}); %! assert_equal (v, {'unnamed', 4}); ## Test an exact name comes first, so a member differing from a property ## in case is reached %!test %! t = addts (tscollection ([0; 1; 2; 3]), [1; 2; 3; 4], 'time'); %! assert_equal (class (get (t, 'time')), 'timeseries'); %! assert_equal (class (get (t, 'Time')), 'double'); %! t = set (t, 'time', timeseries ([9; 9; 9; 9], [0; 1; 2; 3])); %! assert_equal (t.time.Data, [9; 9; 9; 9]); %!error get (TC, 'Name', 1) %!error ... %! get (TC, 5) %!error get (TC, 'zz') %!test %! t = set (TC, 'Name', 'viaset'); %! assert_equal (t.Name, 'viaset'); %! assert_equal (TC.Name, 'unnamed'); %!test %! t = TC; %! set (t, 'name', 'z', 'Time', [5; 6; 7; 8]); %! assert_equal (t.Name, 'z'); %! assert_equal (t.Time, [5; 6; 7; 8]); %!test %! t = set (TC, 'a', timeseries ([9; 8; 7; 6], [0; 1; 2; 3])); %! x = t.a; %! assert_equal (x.Data, [9; 8; 7; 6]); %!assert_equal (fieldnames (set (TC)), fieldnames (get (TC))) %!error ... %! set (TC, 'Name') %!error set (TC, 5, 1) %!error set (TC, 'zz', 1) %!error ... %! t = set (TC, 'Name', 5); %!error ... %! set (TC(1:2), 'Name', 'x') %!assert_equal (fieldnames (TC), {'Name'; 'Time'; 'TimeInfo'; 'Length'; ... %! 'a'; 'b'}) %!assert_equal (fieldnames (tscollection ()), {'Name'; 'Time'; 'TimeInfo'; ... %! 'Length'}) %!assert_equal (properties (TC), {'Name'; 'Time'; 'TimeInfo'; 'Length'; ... %! 'a'; 'b'}) %!assert_equal (size (TC), [4, 2]) %!assert_equal (size (TC, 1), 4) %!assert_equal (size (TC, 2), 2) %!assert_equal (size (TC, 3), 1) %!assert_equal (size (TC, [1, 2]), [4, 2]) %!assert_equal (size (tscollection ([0; 1])), [2, 0]) %!assert_equal (size (tscollection (C)), [4, 1]) ## Test several outputs; MATLAB refuses them %!test %! [r, c] = size (TC); %! assert_equal ([r, c], [4, 2]); %! [r, c, p] = size (TC); %! assert_equal ([r, c, p], [4, 2, 1]); %!error size (TC, 0) %!error size (TC, 1.5) %!assert_equal (length (TC), 4) %!assert_equal (length (tscollection ()), 0) %!assert_equal (length (tscollection ([0; 1; 2])), 3) %!assert_equal (isempty (TC), false) %!assert_equal (isempty (tscollection ()), true) %!assert_equal (isempty (tscollection ([0; 1])), false) %!assert_equal (isempty (tscollection (timeseries ())), true) %!assert_equal (isempty (tscollection (timeseries ([1; 2]))), false) %!assert_equal (isequal (TC, TC), true) %!assert_equal (isequal (TC, tscollection ({A, B})), true) %!assert_equal (isequal (TC, TC, TC), true) %!assert_equal (isequal (TC, tscollection ({B, A})), false) %!assert_equal (isequal (TC, tscollection ({A, B}, 'Name', 'x')), false) %!assert_equal (isequal (TC, tscollection (A)), false) %!assert_equal (isequal (TC, 5), false) %!test %! x = tscollection ([0; 1]); %! y = x; %! y.TimeInfo.UserData = 5; %! assert_equal (isequal (x, y), false); %!error ... %! isequal (TC) ################################################################################ ## ** Members ** ## ################################################################################ ## Available Methods ## ## ## ## 'addts' 'removets' 'gettimeseriesnames' ## ## 'settimeseriesnames' 'setTimeseriesName' ## ## ## ################################################################################ %!assert_equal (gettimeseriesnames (addts (TC, C)), {'a', 'b', 'c'}) %!test %! t = addts (TC, [5; 6; 7; 8], 'd'); %! assert_equal (gettimeseriesnames (t), {'a', 'b', 'd'}); %! x = t.d; %! assert_equal (class (x), 'timeseries'); %! assert_equal (x.Name, 'd'); %! assert_equal (x.Data, [5; 6; 7; 8]); %! assert_equal (x.Time, [0; 1; 2; 3]); %!test %! E = timeseries ([1; 1; 1; 1], [0; 1; 2; 3], 'Name', 'e'); %! t = addts (TC, {C, E}); %! assert_equal (gettimeseriesnames (t), {'a', 'b', 'c', 'e'}); %!assert_equal (gettimeseriesnames (addts (TC, [5; 6; 7; 8], "sd")), ... %! {'a', 'b', 'sd'}) %!assert_equal (gettimeseriesnames (addts (TC, [5; 6; 7; 8], 'my d')), ... %! {'a', 'b', 'myD'}) %!assert_equal (gettimeseriesnames (addts (TC, timeseries ([1; 2; 3; 4], ... %! [0; 1; 2; 3]))), {'a', 'b', 'unnamed'}) %!test %! t = addts (TC, [5, 6; 7, 8; 9, 10; 11, 12], 'm'); %! x = t.m; %! assert_equal (size (x.Data), [4, 2]); %!test %! t = addts (TC, [5, 6, 7, 8], 'r'); %! x = t.r; %! assert_equal (size (x.Data), [1, 1, 4]); %!test %! t = addts (TC, int8 ([1; 2; 3; 4]), 'i'); %! x = t.i; %! assert_equal (class (x.Data), 'int8'); %!test %! t = addts (TC, timeseries (reshape (1:8, 1, 2, 4), [0; 1; 2; 3], ... %! 'Name', 'd3')); %! x = t.d3; %! assert_equal (size (x.Data), [1, 2, 4]); ## Test an empty collection takes a series with no samples, as in MATLAB %!test %! t = addts (tscollection (), timeseries ()); %! assert_equal (gettimeseriesnames (t), {'x'}); %! assert_equal (size (t), [0, 1]); ## Test an empty collection takes the times of the first series %!test %! t = addts (tscollection (), A); %! assert_equal (t.Time, [0; 1; 2; 3]); %! assert_equal (size (t), [4, 1]); %!assert_equal (gettimeseriesnames (addts (tscollection ([0; 1; 2; 3]), A)), ... %! {'a'}) %!test %! DC = timeseries ([7; 8; 9], {'01-Jan-2024', '02-Jan-2024', ... %! '03-Jan-2024'}, 'Name', 'dc'); %! t = addts (tscollection (DA), DC); %! assert_equal (gettimeseriesnames (t), {'da', 'dc'}); ## Test the member takes the collection's time metadata %!test %! t = TC; %! t.TimeInfo.Format = 'yyyy'; %! t = addts (t, C); %! x = t.c; %! assert_equal (x.TimeInfo.Format, 'yyyy'); %!error ... %! addts (TC, A) %!error ... %! addts (TC, timeseries ([1; 2; 3; 4], [0; 1; 2; 3], 'Name', 'A')) %!error ... %! addts (TC, {C, C}) %!error ... %! addts (TC, [5; 6; 7; 8], 'Time') %!error ... %! addts (TC, timeseries ([1; 2; 3], [0; 1; 2], 'Name', 'e')) %!error ... %! addts (TC, timeseries ([1; 2; 3; 4], [0; 1; 2; 4], 'Name', 'e')) %!error ... %! addts (tscollection (DA), timeseries ([7; 8; 9], [0; 1; 2], 'Name', 'dc')) %!error ... %! addts (TC, [1; 2; 3], 'd') %!error ... %! addts (TC, [5; 6; 7; 8]) %!error addts (TC, 5) %!error ... %! addts (TC, 'abcd', 'd') %!error ... %! addts (TC, {1; 2; 3; 4}, 'd') %!error ... %! addts (TC, [5; 6; 7; 8], 5) %!error ... %! addts (TC, {C, 5}) %!error ... %! addts (TC, 'abc') %!error ... %! addts (TC, [5; 6; 7; 8], 'd', 'extra') %!error addts (TC) %!error ... %! addts (tscollection (), [1; 2], 'd') %!test %! t = removets (TC, 'a'); %! assert_equal (gettimeseriesnames (t), {'b'}); %! assert_equal (size (t), [4, 1]); ## Test the last member out leaves the time vector %!test %! t = removets (TC, {'a', 'b'}); %! assert_equal (gettimeseriesnames (t), {}); %! assert_equal (size (t), [4, 0]); %! assert_equal (t.Time, [0; 1; 2; 3]); %!assert_equal (gettimeseriesnames (removets (TC, "b")), {'a'}) %!assert_equal (gettimeseriesnames (removets (TC, string ({'a', 'b'}))), {}) %!assert_equal (gettimeseriesnames (removets (TC, {'b'; 'a'})), {}) ## Test an unknown name is refused; MATLAB ignores it %!error ... %! removets (TC, 'zz') %!error ... %! removets (TC, 'A') %!error ... %! removets (TC, {'a', 'zz'}) %!error ... %! removets (tscollection (), 'a') %!error ... %! removets (TC, 5) %!error removets (TC) %!test %! n = gettimeseriesnames (TC); %! assert_equal (class (n), 'cell'); %! assert_equal (size (n), [1, 2]); %!assert_equal (gettimeseriesnames (addts (tscollection ({B, A}), C)), ... %! {'b', 'a', 'c'}) %!assert_equal (size (gettimeseriesnames (tscollection ([0; 1]))), [0, 0]) ## Test the renamed member moves to the end, as in MATLAB %!test %! t = settimeseriesnames (TC, 'a', 'z'); %! assert_equal (gettimeseriesnames (t), {'b', 'z'}); %! x = t.z; %! assert_equal (x.Name, 'z'); %! assert_equal (x.Data, [1; 2; 3; 4]); %!assert_equal (gettimeseriesnames (settimeseriesnames (TC, 'a', "sz")), ... %! {'b', 'sz'}) %!assert_equal (gettimeseriesnames (settimeseriesnames (TC, "b", 'q')), ... %! {'a', 'q'}) %!assert_equal (gettimeseriesnames (settimeseriesnames (TC, 'a', 'a')), ... %! {'a', 'b'}) %!assert_equal (gettimeseriesnames (settimeseriesnames (TC, 'a', 'A')), ... %! {'b', 'A'}) ## Test a name taken by another member is refused; MATLAB drops that member %!error ... %! settimeseriesnames (TC, 'a', 'b') %!error ... %! settimeseriesnames (TC, 'b', 'a') ## Test a name differing only in case is refused; MATLAB then reads 'b' %!error ... %! settimeseriesnames (TC, 'a', 'B') %!error ... %! settimeseriesnames (TC, 'a', 'Length') %!error ... %! settimeseriesnames (TC, 'zz', 'y') %!error ... %! settimeseriesnames (TC, 'A', 'z') %!error ... %! settimeseriesnames (TC, 'a', 'my x') %!error ... %! settimeseriesnames (TC, 'a', '') %!error ... %! settimeseriesnames (TC, 'a', 5) %!error ... %! settimeseriesnames (TC, {'a', 'b'}, {'p', 'q'}) %!error ... %! settimeseriesnames (TC, 'a') %!assert_equal (gettimeseriesnames (setTimeseriesName (TC, 'a', 'z')), ... %! {'b', 'z'}) %!error ... %! setTimeseriesName (TC, 'zz', 'y') %!error ... %! setTimeseriesName (TC, 'a', 'b') ################################################################################ ## ** Samples and time ** ## ################################################################################ ## Available Methods ## ## ## ## 'addsampletocollection' 'delsamplefromcollection' ## ## 'getsampleusingtime' 'hasduplicatetimes' ## ## 'getabstime' 'setabstime' 'resample' ## ## ## ################################################################################ %!shared A, B, TC, DA %! A = timeseries ([1; 2; 3; 4], [0; 1; 2; 3], [1; 2; 3; 4], 'Name', 'a'); %! A.DataInfo.Units = 'm'; %! A.Events = tsdata.event ('ev', 1.5); %! B = timeseries ([10, 11; 20, 21; 30, 31; 40, 41], [0; 1; 2; 3], 'Name', 'b'); %! TC = tscollection ({A, B}); %! DA = timeseries ([1; 2; 3], {'01-Jan-2024', '02-Jan-2024', ... %! '03-Jan-2024'}, 'Name', 'da'); %!test %! t = addsampletocollection (TC, 'Time', 1.5, 'a', 99, 'b', [7, 8]); %! assert_equal (t.Time, [0; 1; 1.5; 2; 3]); %! x = t.a; %! assert_equal (x.Data, [1; 2; 99; 3; 4]); %! assert_equal (x.Quality, [1; 2; 3; 3; 4]); %! y = t.b; %! assert_equal (y.Data, [10, 11; 20, 21; 7, 8; 30, 31; 40, 41]); ## Test a member not named takes NaN %!test %! t = addsampletocollection (TC, 'Time', 1.5, 'a', 99); %! y = t.b; %! assert_equal (y.Data, [10, 11; 20, 21; NaN, NaN; 30, 31; 40, 41]); %!test %! t = addsampletocollection (TC, 'Time', 1.5); %! assert_equal (t.Time, [0; 1; 1.5; 2; 3]); %! x = t.a; %! assert_equal (x.Data, [1; 2; NaN; 3; 4]); ## Test a new code is the nearest sample's, a tie going to the later %!assert_equal (addsampletocollection (TC, 'Time', 1.2, 'a', 9).a.Quality, ... %! [1; 2; 2; 3; 4]) %!assert_equal (addsampletocollection (TC, 'Time', 1.8, 'a', 9).a.Quality, ... %! [1; 2; 3; 3; 4]) %!assert_equal (addsampletocollection (TC, 'Time', -1, 'a', 9).a.Quality, ... %! [1; 1; 2; 3; 4]) %!assert_equal (addsampletocollection (TC, 'Time', 5, 'a', 9).a.Quality, ... %! [1; 2; 3; 4; 4]) ## Test the samples go in time order, after an equal time %!assert_equal (addsampletocollection (TC, 'Time', 5, 'a', 99).Time, ... %! [0; 1; 2; 3; 5]) %!assert_equal (addsampletocollection (TC, 'Time', -1, 'a', 99).a.Data, ... %! [99; 1; 2; 3; 4]) %!test %! t = addsampletocollection (TC, 'Time', 1, 'a', 99); %! assert_equal (t.Time, [0; 1; 1; 2; 3]); %! x = t.a; %! assert_equal (x.Data, [1; 2; 99; 3; 4]); %!test %! t = addsampletocollection (TC, 'Time', [4; 5], 'a', [98; 99]); %! assert_equal (t.Time, [0; 1; 2; 3; 4; 5]); %! x = t.a; %! assert_equal (x.Data, [1; 2; 3; 4; 98; 99]); ## Test the fill of integer and logical members %!test %! I = timeseries (int8 ([1; 2; 3; 4]), [0; 1; 2; 3], 'Name', 'i'); %! t = addsampletocollection (tscollection ({A, I}), 'Time', 1.5, 'a', 9); %! x = t.i; %! assert_equal (x.Data, int8 ([1; 2; 0; 3; 4])); %!test %! L = timeseries (logical ([1; 0; 1; 0]), [0; 1; 2; 3], 'Name', 'l'); %! t = addsampletocollection (tscollection ({A, L}), 'Time', 1.5, 'a', 9); %! x = t.l; %! assert_equal (x.Data, logical ([1; 0; 0; 1; 0])); ## Test the keyword in any case %!assert_equal (addsampletocollection (TC, 'time', 1.5, 'a', 99).Time, ... %! [0; 1; 1.5; 2; 3]) %!assert_equal (addsampletocollection (TC, 'TIME', 1.5, 'a', 99).Time, ... %! [0; 1; 1.5; 2; 3]) %!assert_equal (addsampletocollection (tscollection ([0; 1; 2]), 'Time', ... %! 1.5).Time, [0; 1; 1.5; 2]) ## Test a dated collection takes dates and numbers %!test %! t = addsampletocollection (tscollection (DA), 'Time', '04-Jan-2024', ... %! 'da', 4); %! assert_equal (t.Time, [0; 1; 2; 3]); %! assert_equal (getabstime (t){end}, '04-Jan-2024 00:00:00'); %!assert_equal (addsampletocollection (tscollection (DA), 'Time', 3, ... %! 'da', 4).Time, [0; 1; 2; 3]) ## Test events and units are kept %!test %! t = addsampletocollection (TC, 'Time', 1.5, 'a', 99); %! x = t.a; %! assert_equal (numel (x.Events), 1); %! assert_equal (x.DataInfo.Units, 'm'); %!error ... %! addsampletocollection (TC, 'a', 99) %!error ... %! addsampletocollection (TC, 'Time', NaN, 'a', 1) %!error ... %! addsampletocollection (TC, 'Time', '01-Jan-2024', 'a', 1) %!error ... %! addsampletocollection (TC, 'Time', 1.5, 'a', [1, 2, 3]) %!error ... %! addsampletocollection (TC, 'Time', 1.5, 'b', [7; 8]) %!error ... %! addsampletocollection (TC, 'Time', [4; 5], 'a', [1; 2; 3]) %!error ... %! addsampletocollection (TC, 'Time', 1.5, 'a', {1}) %!error ... %! addsampletocollection (TC, 'Time', 1.5, 'zz', 1) %!error ... %! addsampletocollection (TC, 'Time', 1.5, 'A', 1) %!error ... %! addsampletocollection (TC, 'Time', 1.5, 'a', 99, 'a', 98) %!error ... %! addsampletocollection (TC, 'Time') %!error ... %! addsampletocollection (TC, 5, 1) %!test %! t = delsamplefromcollection (TC, 'Index', 2); %! assert_equal (t.Time, [0; 2; 3]); %! x = t.a; %! assert_equal (x.Data, [1; 3; 4]); %! assert_equal (x.Quality, [1; 3; 4]); %! y = t.b; %! assert_equal (y.Data, [10, 11; 30, 31; 40, 41]); %!assert_equal (delsamplefromcollection (TC, 'Value', 1).Time, [0; 2; 3]) %!assert_equal (delsamplefromcollection (TC, 'Value', 1.5).Time, [0; 1; 2; 3]) %!assert_equal (delsamplefromcollection (TC, 'index', 2).Time, [0; 2; 3]) %!assert_equal (delsamplefromcollection (TC, 'Index', [1, 3]).Time, [1; 3]) %!assert_equal (delsamplefromcollection (TC, 'Index', [2, 2]).Time, [0; 2; 3]) %!assert_equal (delsamplefromcollection (TC, 'Value', [0, 3]).Time, [1; 2]) %!assert_equal (delsamplefromcollection (tscollection ([0; 1; 1; 2]), ... %! 'Value', 1).Time, [0; 2]) %!assert_equal (delsamplefromcollection (tscollection (DA), 'Value', ... %! '02-Jan-2024').Time, [0; 2]) ## Test removing every sample keeps the members %!test %! t = delsamplefromcollection (TC, 'Index', 1:4); %! assert_equal (size (t.Time), [0, 1]); %! assert_equal (gettimeseriesnames (t), {'a', 'b'}); %! x = t.a; %! assert_equal (size (x.Data), [0, 1]); %!assert_equal (numel (delsamplefromcollection (TC, 'Index', 1).a.Events), 1) %!error ... %! delsamplefromcollection (TC, 'Index', 9) %!error ... %! delsamplefromcollection (TC, 'Index', 0) %!error ... %! delsamplefromcollection (TC, 'Index', 2.5) %!error ... %! delsamplefromcollection (TC, 'Index', logical ([1, 0, 1, 0])) %!error ... %! delsamplefromcollection (TC, 'Foo', 1) %!error ... %! delsamplefromcollection (TC, 'Index') %!test %! t = getsampleusingtime (TC, 1); %! assert_equal (t.Time, 1); %! x = t.a; %! assert_equal (x.Data, 2); %! assert_equal (x.Quality, 2); ## Test the name and the members' metadata are kept; MATLAB names the ## result 'unnamed' %!test %! t = getsampleusingtime (tscollection ({A, B}, 'Name', 'nm'), 1, 2); %! assert_equal (t.Time, [1; 2]); %! assert_equal (t.Name, 'nm'); %! assert_equal (gettimeseriesnames (t), {'a', 'b'}); %! x = t.a; %! assert_equal (x.Data, [2; 3]); %! assert_equal (x.Quality, [2; 3]); %! assert_equal (numel (x.Events), 1); %!assert_equal (getsampleusingtime (TC, 0.5, 2.5).Time, [1; 2]) %!assert_equal (getsampleusingtime (TC, 1, 1).Time, 1) ## Test no sample in range keeps every member; MATLAB drops them %!test %! t = getsampleusingtime (tscollection ({A, B}, 'Name', 'nm'), 5); %! assert_equal (size (t), [0, 2]); %! assert_equal (gettimeseriesnames (t), {'a', 'b'}); %! assert_equal (t.Name, 'nm'); %!assert_equal (size (getsampleusingtime (TC, 1.5)), [0, 2]) %!assert_equal (size (getsampleusingtime (TC, 2, 1)), [0, 2]) %!assert_equal (getsampleusingtime (tscollection (DA), '02-Jan-2024').Time, 1) %!assert_equal (getsampleusingtime (tscollection ([0; 1; 1; 2]), 1, ... %! 'AllowDuplicateTimes', true).Time, [1; 1]) %!error ... %! getsampleusingtime (tscollection ([0; 1; 1; 2]), 1) %!error ... %! getsampleusingtime (TC, [1, 2]) %!error ... %! getsampleusingtime (TC) %!assert_equal (hasduplicatetimes (TC), false) %!assert_equal (hasduplicatetimes (tscollection ([0; 1; 1])), true) %!assert_equal (hasduplicatetimes (tscollection ()), false) %!assert_equal (getabstime (tscollection (DA)), {'01-Jan-2024 00:00:00'; ... %! '02-Jan-2024 00:00:00'; ... %! '03-Jan-2024 00:00:00'}) %!test %! t = TC; %! t.TimeInfo.StartDate = '01-Jan-2024'; %! assert_equal (getabstime (t), {'01-Jan-2024 00:00:00'; ... %! '01-Jan-2024 00:00:01'; ... %! '01-Jan-2024 00:00:02'; ... %! '01-Jan-2024 00:00:03'}); %! x = t.a; %! assert_equal (x.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); ## Test the format is used; MATLAB writes the default whatever it says %!test %! t = TC; %! t.TimeInfo.StartDate = '01-Jan-2024'; %! t.TimeInfo.Units = 'days'; %! t.TimeInfo.Format = 'yyyy-mm-dd'; %! assert_equal (getabstime (t), {'2024-01-01'; '2024-01-02'; ... %! '2024-01-03'; '2024-01-04'}); %!error ... %! getabstime (TC) %!error ... %! getabstime (tscollection ()) %!test %! t = setabstime (TC, {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024', ... %! '04-Jan-2024'}); %! assert_equal (t.Time, [0; 86400; 172800; 259200]); %! assert_equal (t.TimeInfo.Units, 'seconds'); %! assert_equal (t.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %! x = t.a; %! assert_equal (x.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %! assert_equal (x.Time, [0; 86400; 172800; 259200]); ## Test unsorted dates carry every member's samples; MATLAB leaves them %!test %! t = setabstime (TC, {'03-Jan-2024', '01-Jan-2024', '02-Jan-2024', ... %! '04-Jan-2024'}); %! assert_equal (getabstime (t), {'01-Jan-2024 00:00:00'; ... %! '02-Jan-2024 00:00:00'; ... %! '03-Jan-2024 00:00:00'; ... %! '04-Jan-2024 00:00:00'}); %! x = t.a; %! assert_equal (x.Data, [2; 3; 1; 4]); %! assert_equal (x.Quality, [2; 3; 1; 4]); %! y = t.b; %! assert_equal (y.Data, [20, 21; 30, 31; 10, 11; 40, 41]); %!test %! t = TC; %! t.TimeInfo.Units = 'hours'; %! t = setabstime (t, {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024', ... %! '04-Jan-2024'}); %! assert_equal (t.Time, [0; 24; 48; 72]); %!test %! t = setabstime (TC, {'2024-01-01', '2024-01-02', '2024-01-03', ... %! '2024-01-04'}, 'yyyy-mm-dd'); %! assert_equal (t.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %! assert_equal (t.Time, [0; 86400; 172800; 259200]); %!test %! t = setabstime (TC, {'01-Jan-2024 06:00:00', '01-Jan-2024 12:00:00', ... %! '02-Jan-2024 00:00:00', '02-Jan-2024 06:00:00'}); %! assert_equal (t.Time, [0; 21600; 64800; 86400]); %!assert_equal (setabstime (TC, {'01-Jan-2024'; '01-Jan-2024'; ... %! '02-Jan-2024'; '03-Jan-2024'}).Time, [0; 0; 86400; 172800]) ## Test each date is read by its own format; MATLAB reads all by the first's %!assert_equal (getabstime (setabstime (TC, {'01-Jan-2024', '2024-01-02', ... %! '03-Jan-2024', '04-Jan-2024'})){2}, '02-Jan-2024 00:00:00') ## Test a string array, a character matrix and a datetime array %!assert_equal (setabstime (TC, string ({'01-Jan-2024', '02-Jan-2024', ... %! '03-Jan-2024', '04-Jan-2024'})).Time, ... %! [0; 86400; 172800; 259200]) %!assert_equal (setabstime (TC, char ({'01-Jan-2024', '02-Jan-2024', ... %! '03-Jan-2024', '04-Jan-2024'})).Time, ... %! [0; 86400; 172800; 259200]) %!test %! t = setabstime (TC, datetime (2024, 1, 1:4)); %! assert_equal (t.Time, [0; 86400; 172800; 259200]); %! assert_equal (t.TimeInfo.StartDate, '01-Jan-2024 00:00:00'); %!assert_equal (getabstime (setabstime (tscollection ([0; 1; 2]), ... %! {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024'})), ... %! {'01-Jan-2024 00:00:00'; '02-Jan-2024 00:00:00'; ... %! '03-Jan-2024 00:00:00'}) ## Test events keep their times %!assert_equal (setabstime (TC, {'03-Jan-2024', '01-Jan-2024', ... %! '02-Jan-2024', '04-Jan-2024'}).a.Events(1).Time, 1.5) ## Test numbers are refused; MATLAB takes them as relative times %!error ... %! setabstime (TC, datenum (2024, 1, 1:4)) %!error ... %! setabstime (TC, [0, 1, 2, 3]) %!error ... %! setabstime (TC, {'01-Jan-2024', '02-Jan-2024'}) %!error ... %! setabstime (TC, {'foo', 'bar', 'baz', 'qux'}) ## Test a date not written in the format is refused; MATLAB forces it %!error ... %! setabstime (TC, {'01-Jan-2024', '02-Jan-2024', '03-Jan-2024', ... %! '04-Jan-2024'}, 'yyyy-mm-dd') %!error ... %! setabstime (TC) %!test %! t = resample (TC, [0, 0.5, 1]); %! assert_equal (t.Time, [0; 0.5; 1]); %! x = t.a; %! assert_equal (x.Data, [1; 1.5; 2]); %! assert_equal (x.Quality, [1; 2; 2]); %! y = t.b; %! assert_equal (y.Data, [10, 11; 15, 16; 20, 21]); %!test %! t = resample (TC, [0, 0.5, 1], 'zoh'); %! x = t.a; %! assert_equal (x.Data, [1; 1; 2]); %! y = t.b; %! assert_equal (y.Data, [10, 11; 10, 11; 20, 21]); ## Test each member's own method %!test %! t = resample (tscollection ({setinterpmethod(A, 'zoh'), B}), [0, 0.5, 1]); %! x = t.a; %! assert_equal (x.Data, [1; 1; 2]); %! y = t.b; %! assert_equal (y.Data, [10, 11; 15, 16; 20, 21]); ## Test outside the range a member with codes gives NaN; MATLAB raises %!test %! t = resample (TC, [2.5, 3.5, 4]); %! x = t.a; %! assert_equal (x.Data, [3.5; NaN; NaN]); %! assert_equal (x.Quality, [4; 4; 4]); %! assert_equal (resample (TC, [-1, 0, 1]).a.Data, [NaN; 1; 2]); %!test %! Q = A; %! Q.QualityInfo.Code = [1, 2, 3, 4, 7]; %! Q.QualityInfo.Description = {'q1', 'q2', 'q3', 'q4', 'q7'}; %! t = resample (tscollection ({Q, B}), [0, 0.5, 1, 1.5], 'linear', 7); %! x = t.a; %! assert_equal (x.Quality, [1; 7; 2; 7]); %! y = t.b; %! assert_equal (y.Quality, []); %!test %! I = timeseries (int8 ([1; 2; 3; 4]), [0; 1; 2; 3], 'Name', 'i'); %! t = resample (tscollection ({A, I}), [0, 0.5, 1]); %! x = t.i; %! assert_equal (x.Data, int8 ([1; 2; 2])); %!test %! L = timeseries (logical ([1; 0; 1; 0]), [0; 1; 2; 3], 'Name', 'l'); %! t = resample (tscollection ({A, L}), [0, 0.5, 1]); %! x = t.l; %! assert_equal (x.Data, [true; true; false]); %!assert_equal (resample (TC, [1, 0.5, 0]).Time, [0; 0.5; 1]) %!assert_equal (size (resample (TC, [0; 0.5; 1]).Time), [3, 1]) %!test %! t = resample (tscollection (DA), [0, 0.5, 1]); %! assert_equal (t.Time, [0; 0.5; 1]); %! assert_equal (getabstime (t), {'01-Jan-2024 00:00:00'; ... %! '01-Jan-2024 12:00:00'; ... %! '02-Jan-2024 00:00:00'}); %!test %! t = resample (tscollection (DA), {'01-Jan-2024 12:00:00', ... %! '02-Jan-2024 00:00:00'}); %! assert_equal (t.Time, [0.5; 1]); %! x = t.da; %! assert_equal (x.Data, [1.5; 2]); %!test %! t = resample (tscollection ({A, B}, 'Name', 'nm'), [0, 0.5, 1]); %! assert_equal (t.Name, 'nm'); %! x = t.a; %! assert_equal (numel (x.Events), 1); %! assert_equal (x.DataInfo.Units, 'm'); %!assert_equal (size (resample (tscollection (), [0, 1])), [0, 0]) %!error ... %! resample (TC, [0, NaN, 1]) %!error ... %! resample (TC, [0, 0.5, 1], 'foo') %!error ... %! resample (TC, [0, 0.5, 1], 'zoh', 7) %!error ... %! resample (tscollection (B), [0, 0.5, 1], 'linear', 7) %!error ... %! resample (tscollection (timeseries (1, 0, 'Name', 'o')), [0, 1]) %!error ... %! resample (TC) ################################################################################ ## ** Indexing and concatenation ** ## ################################################################################ ## Available Methods ## ## ## ## 'subsref' 'subsasgn' 'end' ## ## 'horzcat' 'vertcat' ## ## ## ################################################################################ %!shared A, B, C, TC %! A = timeseries ([1; 2; 3; 4], [0; 1; 2; 3], [1; 2; 3; 4], 'Name', 'a'); %! A.DataInfo.Units = 'm'; %! A.Events = tsdata.event ('ev', 1.5); %! B = timeseries ([10, 11; 20, 21; 30, 31; 40, 41], [0; 1; 2; 3], 'Name', 'b'); %! C = timeseries ([5; 6; 7; 8], [0; 1; 2; 3], 'Name', 'c'); %! TC = tscollection ({A, B}); %!assert_equal (TC.Name, 'unnamed') %!assert_equal (TC.Time, [0; 1; 2; 3]) %!assert_equal (class (TC.TimeInfo), 'tsdata.timemetadata') %!assert_equal (TC.TimeInfo.Units, 'seconds') %!assert_equal (TC.Length, 4) %!assert_equal (class (TC.a), 'timeseries') %!assert_equal (TC.a.Data, [1; 2; 3; 4]) %!assert_equal (TC.("b").Data(2,:), [20, 21]) %!test %! t = TC(2); %! assert_equal (class (t), 'tscollection'); %! assert_equal (t.Time, 1); %! assert_equal (size (t), [1, 2]); %!test %! t = TC(2:3); %! assert_equal (t.Time, [1; 2]); %! x = t.a; %! assert_equal (x.Data, [2; 3]); %! assert_equal (x.Quality, [2; 3]); %! assert_equal (numel (x.Events), 1); %!assert_equal (TC(end).Time, 3) %!assert_equal (TC(end-1:end).Time, [2; 3]) %!assert_equal (gettimeseriesnames (TC(2, 'a')), {'a'}) %!assert_equal (TC(2, 'a').Time, 1) %!assert_equal (gettimeseriesnames (TC(:, 1)), {'a'}) %!assert_equal (TC(:, 1).Time, [0; 1; 2; 3]) %!assert_equal (gettimeseriesnames (TC(1, 2)), {'b'}) %!assert_equal (gettimeseriesnames (TC(1:2, {'b'})), {'b'}) %!assert_equal (gettimeseriesnames (TC(:, string ({'b', 'a'}))), {'b', 'a'}) %!assert_equal (gettimeseriesnames (TC(:, end)), {'b'}) %!assert_equal (gettimeseriesnames (TC(:, logical ([0, 1]))), {'b'}) %!assert_equal (TC(2).a.Data, 2) ## Test a logical mask selects its samples; MATLAB gives sample 2 %!test %! t = TC(logical ([1, 0, 1, 0])); %! assert_equal (t.Time, [0; 2]); %! x = t.a; %! assert_equal (x.Data, [1; 3]); %!assert_equal (TC(logical ([0, 1, 1])).Time, [1; 2]) ## Test indices are taken as a set, as in MATLAB %!assert_equal (TC([3, 1]).Time, [0; 2]) %!assert_equal (TC([3, 1]).a.Data, [1; 3]) %!assert_equal (TC([1, 1]).Time, 0) ## Test an empty selection keeps the members; MATLAB drops them %!assert_equal (size (TC([])), [0, 2]) %!test %! t = tscollection ({A, B}, 'Name', 'nm'); %! t = t(2); %! assert_equal (t.Name, 'nm'); %!test %! t = TC; %! t.TimeInfo.StartDate = '01-Jan-2024'; %! t = t(2:3); %! assert_equal (getabstime (t), {'01-Jan-2024 00:00:01'; ... %! '01-Jan-2024 00:00:02'}); %!error ... %! TC(5) %!error ... %! TC(0) %!error ... %! TC(1.5) %!error ... %! TC(true (1, 5)) %!error ... %! TC({1}) %!error ... %! TC(2, 3) %!error ... %! TC(2, true (1, 3)) %!error TC(:, 'zz') %!error ... %! TC(:, {1}) %!error ... %! TC(1, 2, 3) %!error TC{1} %!error ... %! TC.nonexist ## Test member names are read exactly; MATLAB reads TC.A as TC.a %!error TC.A %!test %! t = TC; %! t.Name = 'q'; %! assert_equal (t.Name, 'q'); %! t.Name = "sn"; %! assert_equal (t.Name, 'sn'); %! t.Name = ''; %! assert_equal (t.Name, ''); %!test %! t = TC; %! t.Time = [10, 11, 12, 13]; %! assert_equal (t.Time, [10; 11; 12; 13]); %! x = t.b; %! assert_equal (x.Time, [10; 11; 12; 13]); %! assert_equal (t.TimeInfo.Start, 10); %!test %! t = TC; %! t.Time = [0; 1; 1; 3]; %! x = t.a; %! assert_equal (x.Time, [0; 1; 1; 3]); %!test %! t = TC; %! t.Time(4) = 5; %! assert_equal (t.Time, [0; 1; 2; 5]); ## Test an integer time vector is stored as double; MATLAB keeps the class %!test %! t = TC; %! t.Time = int8 ([0; 1; 2; 3]); %! assert_equal (class (t.Time), 'double'); %! x = t.a; %! assert_equal (class (x.Time), 'double'); ## Test units relabel the times without converting them %!test %! t = TC; %! t.TimeInfo.Units = 'minutes'; %! assert_equal (t.Time, [0; 1; 2; 3]); %! x = t.a; %! assert_equal (x.TimeInfo.Units, 'minutes'); %! assert_equal (x.Time, [0; 1; 2; 3]); %!test %! t = TC; %! t.TimeInfo.Format = 'yyyy'; %! assert_equal (t.TimeInfo.Format, 'yyyy'); %! x = t.b; %! assert_equal (x.TimeInfo.Format, 'yyyy'); %!test %! t = TC; %! ti = tsdata.timemetadata (); %! ti.Units = 'hours'; %! t.TimeInfo = ti; %! assert_equal (t.TimeInfo.Units, 'hours'); %! assert_equal (t.TimeInfo.Length, 4); %! assert_equal (t.a.TimeInfo.Units, 'hours'); ## Test a member replaced by dot keeps its key and takes it as its name %!test %! t = TC; %! t.a = timeseries ([9; 8; 7; 6], [0; 1; 2; 3], 'Name', 'zz'); %! assert_equal (gettimeseriesnames (t), {'a', 'b'}); %! x = t.a; %! assert_equal (x.Name, 'a'); %! assert_equal (x.Data, [9; 8; 7; 6]); %!test %! t = TC; %! t.a.Data(2) = 99; %! assert_equal (t.a.Data, [1; 99; 3; 4]); %!test %! t = TC; %! t.a.DataInfo.Units = 'km'; %! assert_equal (t.a.DataInfo.Units, 'km'); %!test %! t = TC; %! t.c = C; %! assert_equal (gettimeseriesnames (t), {'a', 'b', 'c'}); %!test %! t = TC; %! t.C = C; %! assert_equal (gettimeseriesnames (t), {'a', 'b', 'C'}); %!error ... %! t = TC; t.Time = [0; 2; 1; 3]; %!error ... %! t = TC; t.Time = [0; 1; 2]; %!error ... %! t = tscollection ([0; 1; 2]); t.Time = [0; 1; 2; 3; 4]; %!error ... %! t = TC; t.Time = [0; 1; NaN; 3]; %!error ... %! t = TC; t.Time = 'abcd'; ## Test a name that is not text is refused; MATLAB takes it %!error ... %! t = TC; t.Name = 5; %!error ... %! t = TC; t.Name = {'x'}; ## Test the time units are validated; MATLAB takes any text %!error ... %! t = TC; t.TimeInfo.Units = 'foo'; ## Test the derived time properties are read-only; MATLAB warns and ignores %!error ... %! t = TC; t.TimeInfo.Start = 5; %!error ... %! t = TC; t.TimeInfo = 5; %!error ... %! t = TC; t.Length = 5; ## Test a member of other times is refused; MATLAB moves its samples %!error ... %! t = TC; t.a = timeseries ([1; 2; 3], [0; 1; 2], 'Name', 'a'); %!error ... %! t = TC; t.a = timeseries ([9; 8; 7; 6], [0; 1; 2; 4]); %!error ... %! t = TC; t.a.Time = [0; 1; 2; 4]; %!error ... %! t = TC; t.a.TimeInfo.Units = 'minutes'; %!error ... %! t = TC; t.a.Name = 'renamed'; %!error ... %! t = TC; t.a.Data = [1; 2; 3]; %!error ... %! t = TC; t.('my x') = timeseries ([1; 2; 3; 4], [0; 1; 2; 3]); %!error ... %! t = TC; t.a = []; %!error ... %! t = TC; t.c = 5; %!error ... %! t = TC; t.zz.Data = 1; %!error ... %! t = TC; t.A = C; %!error ... %! t = TC; t.c = timeseries ([1; 2; 3], [0; 1; 2]); %!error ... %! t = TC; t(2) = TC(3); %!error ... %! t = TC; t{1} = 1; %!test %! P1 = tscollection ({B, C}, 'Name', 'first'); %! P2 = tscollection ({timeseries([50, 51; 60, 61], [4; 5], 'Name', 'b'), ... %! timeseries([9; 9], [4; 5], 'Name', 'c')}, ... %! 'Name', 'second'); %! t = [P1; P2]; %! assert_equal (t.Time, [0; 1; 2; 3; 4; 5]); %! assert_equal (t.Name, 'first'); %! y = t.c; %! assert_equal (y.Data, [5; 6; 7; 8; 9; 9]); %! x = t.b; %! assert_equal (x.Data(5:6,:), [50, 51; 60, 61]); %!test %! Q = timeseries ([5; 6], [4; 5], [7; 8], 'Name', 'a'); %! t = [tscollection(A); tscollection(Q)]; %! x = t.a; %! assert_equal (x.Quality, [1; 2; 3; 4; 7; 8]); %! assert_equal (x.Data, [1; 2; 3; 4; 5; 6]); %! assert_equal (numel (x.Events), 1); %! assert_equal (x.DataInfo.Units, ''); ## Test collections may touch at a time %!test %! P2 = tscollection ({timeseries([5; 6], [3; 4], 'Name', 'a'), ... %! timeseries([50, 51; 60, 61], [3; 4], 'Name', 'b')}); %! A0 = timeseries ([1; 2; 3; 4], [0; 1; 2; 3], 'Name', 'a'); %! t = [tscollection({A0, B}); P2]; %! assert_equal (t.Time, [0; 1; 2; 3; 3; 4]); %!test %! P1 = tscollection ({B, C}); %! P2 = tscollection ({timeseries([9; 9], [4; 5], 'Name', 'c'), ... %! timeseries([50, 51; 60, 61], [4; 5], 'Name', 'b')}); %! warning ('off', 'all', 'local'); %! t = [P1; P2]; %! assert_equal (gettimeseriesnames (t), {'b', 'c'}); %! x = t.b; %! assert_equal (x.Data(5:6,:), [50, 51; 60, 61]); %!warning ... %! P1 = tscollection ({B, C}); ... %! P2 = tscollection ({timeseries([9; 9], [4; 5], 'Name', 'c'), ... %! timeseries([50, 51; 60, 61], [4; 5], 'Name', 'b')}); ... %! [P1; P2]; %!test %! DA = timeseries ([1; 2; 3], {'01-Jan-2024', '02-Jan-2024', ... %! '03-Jan-2024'}, 'Name', 'da'); %! DB = timeseries ([7; 8], {'04-Jan-2024', '05-Jan-2024'}, 'Name', 'da'); %! t = [tscollection(DA); tscollection(DB)]; %! assert_equal (t.Time, [0; 1; 2; 3; 4]); %! assert_equal (getabstime (t){5}, '05-Jan-2024 00:00:00'); %!assert_equal (isequal (vertcat (TC), TC), true) ## Test collections whose members have no samples join %!test %! t0 = delsamplefromcollection (tscollection (timeseries ((1:3)', ... %! [0; 1; 2], 'Name', 'a')), 'Index', 1:3); %! assert_equal (vertcat (t0, t0).Length, 0); %!error ... %! P2 = tscollection ({timeseries([5; 6], [2.5; 4], 'Name', 'a'), ... %! timeseries([50, 51; 60, 61], [2.5; 4], ... %! 'Name', 'b')}); ... %! vertcat (TC, P2) %!error ... %! vertcat (TC, TC) %!error ... %! vertcat (TC, tscollection (timeseries ([5; 6], [4; 5], 'Name', 'a'))) %!error ... %! vertcat (TC, tscollection ({timeseries([5; 6], [4; 5], 'Name', 'a'), ... %! timeseries([5; 6], [4; 5], 'Name', 'c')})) %!error ... %! vertcat (TC, tscollection ()) %!error ... %! vertcat (TC, 5) %!error ... %! vertcat (TC, tscollection ({timeseries([5, 6; 7, 8], [4; 5], ... %! 'Name', 'a'), ... %! timeseries([50, 51; 60, 61], [4; 5], ... %! 'Name', 'b')})) ## Test quality codes on only some members are refused; MATLAB gives a ## member it cannot read, or drops them %!error ... %! vertcat (tscollection (A), tscollection (timeseries ([5; 6], [4; 5], ... %! 'Name', 'a'))) %!error ... %! vertcat (tscollection (timeseries ([1; 2], [-2; -1], 'Name', 'a')), ... %! tscollection (A)) %!error ... %! vertcat (tscollection (timeseries (int8 ([5; 6]), [0; 1], 'Name', 'c')), ... %! tscollection (timeseries ([1.4; 300], [2; 3], 'Name', 'c'))) %!error ... %! DA = timeseries ([1; 2; 3], {'01-Jan-2024', '02-Jan-2024', ... %! '03-Jan-2024'}, 'Name', 'c'); ... %! vertcat (tscollection (C), tscollection (DA)) %!assert_equal (gettimeseriesnames ([TC, tscollection(C)]), {'a', 'b', 'c'}) ## Test the members are sorted by name, as in MATLAB %!assert_equal (gettimeseriesnames ([tscollection(C), TC]), {'a', 'b', 'c'}) %!assert_equal (gettimeseriesnames ([tscollection({timeseries([1; 2], ... %! [0; 1], 'Name', 'z'), timeseries([1; 2], [0; 1], ... %! 'Name', 'm')}), tscollection(timeseries ([3; 4], [0; 1], ... %! 'Name', 'a'))]), {'a', 'm', 'z'}) %!assert_equal (gettimeseriesnames ([TC, tscollection([0; 1; 2; 3])]), ... %! {'a', 'b'}) %!assert_equal (gettimeseriesnames (horzcat (TC, tscollection (C), ... %! tscollection (timeseries ([1; 2; 3; 4], [0; 1; 2; 3], ... %! 'Name', 'e')))), {'a', 'b', 'c', 'e'}) ## Test an equal shared member is taken once, as in MATLAB %!assert_equal (gettimeseriesnames ([TC, TC]), {'a', 'b'}) %!test %! N = timeseries ([1; NaN; 3; 4], [0; 1; 2; 3], 'Name', 'n'); %! assert_equal (gettimeseriesnames ([tscollection(N), tscollection(N)]), ... %! {'n'}); ## Test the name of the first; MATLAB names the result 'unnamed' %!assert_equal ([tscollection({A, B}, 'Name', 'first'), ... %! tscollection(C, 'Name', 'second')].Name, 'first') ## Test the members joined take the first collection's time metadata %!test %! t1 = tscollection (timeseries ([1; 2], [0; 1], 'Name', 'a')); %! t1.TimeInfo.StartDate = '01-Jan-2024'; %! z = tscollection (timeseries ([1; 2], [0; 1], 'Name', 'z')); %! z.TimeInfo.StartDate = '01-Jan-2024'; %! z.TimeInfo.Format = 'yyyy-mm-dd'; %! h = [t1, z]; %! assert_equal (getabstime (h.z), getabstime (h)); %!error ... %! horzcat (tscollection (C), tscollection (timeseries ([0; 0; 0; 0], ... %! [0; 1; 2; 3], ... %! 'Name', 'c'))) %!error ... %! horzcat (TC, tscollection (timeseries ([0; 0; 0; 0], [0; 1; 2; 3], ... %! 'Name', 'A'))) %!error ... %! horzcat (TC, tscollection (timeseries ([1; 2; 3], [0; 1; 2], 'Name', 'c'))) %!error ... %! horzcat (TC, tscollection (timeseries ([1; 2; 3; 4], [0; 1; 2; 4], ... %! 'Name', 'c'))) %!error ... %! CM = C; CM.TimeInfo.Units = 'minutes'; ... %! horzcat (TC, tscollection (CM)) %!error ... %! horzcat (TC, C) pr0m1th3as-datatypes-9450b29/inst/timerange.m000066400000000000000000000445011527370255000210100ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef timerange ## -*- texinfo -*- ## @deftp {datatypes} timerange ## ## Subscript into a timetable by a range of times. ## ## A utility class that selects the rows of a timetable whose times fall in ## an interval. The interval is half-open by default, including the time it ## starts at and excluding the one it ends at, so that ranges laid end to ## end select each row exactly once. ## ## @seealso{withtol, timetable} ## @end deftp properties (SetAccess = private, Hidden) ## Lower bound, or -Inf for none first ## Upper bound, or Inf for none last ## One of 'open', 'closed', 'openleft', 'openright' intervalType endproperties methods (Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif disp (this); endfunction ## Custom display function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif fprintf (" timerange subscript: %s to %s, %s\n\n", ... boundstr (this.first), boundstr (this.last), ... this.intervalType); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timerange} {@var{tr} =} timerange (@var{startTime}, @var{endTime}) ## @deftypefnx {timerange} {@var{tr} =} timerange (@var{startTime}, @var{endTime}, @var{intervalType}) ## @deftypefnx {timerange} {@var{tr} =} timerange (@var{t}, @var{unit}) ## @deftypefnx {timerange} {@var{tr} =} timerange (@var{startTime}, @var{endTime}, @var{unit}) ## ## Create a time range subscript. ## ## @code{@var{tr} = timerange (@var{startTime}, @var{endTime})} creates a ## subscript selecting the rows of a timetable whose times are at or after ## @var{startTime} and strictly before @var{endTime}. The bounds may be ## @code{datetime} or @code{duration} scalars, text that reads as one, or ## @code{-Inf} and @code{Inf} for no bound at that end; they may not both ## be infinite, there being nothing to say what kind of time is meant. ## ## @code{@var{tr} = timerange (@var{startTime}, @var{endTime}, ## @var{intervalType})} says which bounds belong to the interval: ## @qcode{'openright'} (the default) includes the start alone, ## @qcode{'openleft'} the end alone, @qcode{'closed'} both and ## @qcode{'open'} neither. ## ## @code{@var{tr} = timerange (@var{t}, @var{unit})} covers the whole ## calendar period containing @var{t}, one of @qcode{'seconds'}, ## @qcode{'minutes'}, @qcode{'hours'}, @qcode{'days'}, @qcode{'weeks'}, ## @qcode{'months'}, @qcode{'quarters'} or @qcode{'years'}, in the ## singular or the plural. @var{t} must be a @code{datetime}: a ## @code{duration} is elapsed time and sits on no calendar. ## ## @code{@var{tr} = timerange (@var{startTime}, @var{endTime}, ## @var{unit})} widens the range to whole periods of @var{unit}: it runs ## from the start of the period holding @var{startTime} to the start of ## the period after the one holding @var{endTime}, the far end left out ## as usual. Both bounds must be @code{datetime} scalars. A unit and an ## interval type cannot both be given, the unit fixing the ends itself. ## ## @seealso{withtol, timetable} ## @end deftypefn function this = timerange (varargin) if (nargin < 2 || nargin > 3) print_usage (); endif first = varargin{1}; second = varargin{2}; ## Two event filters name the bounds by what was happening rather than ## by when. They cannot be resolved here, the events living on the ## timetable the range will subscript, so they are kept as they are and ## resolved when it arrives. if (isa (first, 'eventfilter') || isa (second, 'eventfilter')) if (! (isa (first, 'eventfilter') && isa (second, 'eventfilter'))) error (strcat ("timerange: an event filter bound needs an event", ... " filter at the other end too; a range runs from", ... " one event to another.")); endif if (nargin > 2) error (strcat ("timerange: an interval type cannot be given with", ... " event filter bounds.")); endif this.first = first; this.last = second; this.intervalType = 'openright'; return endif ## The two-argument form is a unit whenever the second argument names ## one; anything else there is the far bound of the interval. if (nargin == 2 && isUnitName (second)) if (! isdatetime (first)) error (strcat ("timerange: T must be a datetime when a unit of", ... " time is given; a duration sits on no calendar.")); endif [this.first, this.last] = unitPeriod (first, second); this.intervalType = 'openright'; return endif ## A unit in the third place widens the range to whole units rather ## than saying which ends belong to it: it runs from the start of the ## unit holding the first bound to the start of the unit after the one ## holding the second, and the far end is left out as usual. if (nargin == 3 && isUnitName (varargin{3})) if (! (isdatetime (first) && isdatetime (second))) error (strcat ("timerange: START and END must both be datetimes", ... " when a unit of time is given; a duration sits", ... " on no calendar.")); endif [this.first, ~] = unitPeriod (first, varargin{3}); [~, this.last] = unitPeriod (second, varargin{3}); this.intervalType = 'openright'; return endif this.first = checkBound (first, 'START'); this.last = checkBound (second, 'END'); if (isnumeric (this.first) && isnumeric (this.last)) error (strcat ("timerange: the bounds cannot both be infinite;", ... " at least one must say what kind of time is", ... " meant.")); endif if (nargin < 3) this.intervalType = 'openright'; else this.intervalType = checkIntervalType (varargin{3}); endif endfunction ## -*- texinfo -*- ## @deftypefn {timerange} {@var{tf} =} hasEventBounds (@var{tr}) ## ## Whether the range's bounds are event filters awaiting a timetable. ## ## @end deftypefn function tf = hasEventBounds (this) tf = isa (this.first, 'eventfilter'); endfunction ## -*- texinfo -*- ## @deftypefn {timerange} {@var{tr2} =} resolveEventBounds (@var{tr}, @var{tt}) ## ## Return the range with its event filter bounds read against @var{tt}. ## ## Each bound becomes the time of the first event of @var{tt} its filter ## matches, so the range runs from the start of one event to the start of ## the other and stops short of it. ## ## @end deftypefn function this = resolveEventBounds (this, tt) this.first = firstEventTime (this.first, tt, 'START'); this.last = firstEventTime (this.last, tt, 'END'); endfunction ## -*- texinfo -*- ## @deftypefn {timerange} {[@var{lo}, @var{hi}, @var{cl}, @var{cr}] =} interval (@var{tr}, @var{rowTimes}) ## ## Return the interval the range describes, in the row times' own type. ## ## @var{lo} and @var{hi} come back as the class of @var{rowTimes}, or as ## @code{-Inf} or @code{Inf} where the range is unbounded on that side. ## @var{cl} and @var{cr} say whether the interval includes the bound at ## its lower and its upper end. ## ## @end deftypefn function [lo, hi, cl, cr] = interval (this, rowTimes) lo = matchBound (this.first, rowTimes); hi = matchBound (this.last, rowTimes); cl = any (strcmp (this.intervalType, {'closed', 'openright'})); cr = any (strcmp (this.intervalType, {'closed', 'openleft'})); endfunction ## -*- texinfo -*- ## @deftypefn {timerange} {@var{ix} =} rowIndices (@var{tr}, @var{rowTimes}) ## ## Return the positions in @var{rowTimes} the range selects. ## ## The positions come back in ascending order, being the rows of the ## timetable in the order it holds them. ## ## @end deftypefn function ix = rowIndices (this, rowTimes) lo = matchBound (this.first, rowTimes); hi = matchBound (this.last, rowTimes); closedLeft = any (strcmp (this.intervalType, {'closed', 'openright'})); closedRight = any (strcmp (this.intervalType, {'closed', 'openleft'})); keep = true (numel (rowTimes), 1); if (! isnumeric (lo)) if (closedLeft) keep = keep & rowTimes >= lo; else keep = keep & rowTimes > lo; endif endif if (! isnumeric (hi)) if (closedRight) keep = keep & rowTimes <= hi; else keep = keep & rowTimes < hi; endif endif ix = find (keep); endfunction endmethods endclassdef ## Whether NAME is one of the calendar units the one-unit form takes. function tf = isUnitName (name) if (isa (name, 'string') && isscalar (name)) name = char (name); endif tf = ischar (name) && isrow (name) && any (strcmpi (name, ... {'second', 'seconds', 'minute', 'minutes', 'hour', 'hours', ... 'day', 'days', 'week', 'weeks', 'month', 'months', ... 'quarter', 'quarters', 'year', 'years'})); endfunction ## The period of one UNIT containing T, as its first instant and the first ## instant of the period after it. function [lo, hi] = unitPeriod (t, unit) if (isa (unit, 'string')) unit = char (unit); endif unit = lower (unit); if (unit(end) == 's') unit = unit(1:end-1); endif lo = dateshift (t, 'start', unit); switch (unit) case 'second' hi = lo + seconds (1); case 'minute' hi = lo + minutes (1); case 'hour' hi = lo + hours (1); case 'day' hi = lo + caldays (1); case 'week' hi = lo + calweeks (1); case 'month' hi = lo + calmonths (1); case 'quarter' hi = lo + calquarters (1); case 'year' hi = lo + calyears (1); endswitch endfunction ## Validate one bound: a datetime or duration scalar, text that reads as one, ## or an infinity standing for no bound at all. Text is kept as text and ## read once the row times say which kind of time it must be. function b = checkBound (b, which) if (isnumeric (b) && isscalar (b) && isinf (b)) return endif if (isdatetime (b) || isduration (b)) if (! isscalar (b)) error ("timerange: %s must be a scalar.", which); endif return endif if (isa (b, 'string') && isscalar (b)) b = char (b); endif if (ischar (b) && isrow (b)) return endif error (strcat ("timerange: %s must be a datetime or duration scalar,", ... " text naming one, or -Inf or Inf."), which); endfunction ## Validate the interval type. function t = checkIntervalType (t) if (isa (t, 'string') && isscalar (t)) t = char (t); endif types = {'open', 'closed', 'openleft', 'openright'}; if (! (ischar (t) && isrow (t) && any (strcmpi (t, types)))) error (strcat ("timerange: INTERVALTYPE must be 'openright',", ... " 'openleft', 'open' or 'closed'.")); endif t = lower (t); endfunction ## One bound as a value comparable with ROWTIMES: an infinity stays numeric ## and marks the bound absent, text is read as the row times' own type, and ## a time of the other kind cannot be compared at all. function b = matchBound (b, rowTimes) if (isnumeric (b)) return endif if (ischar (b)) if (isdatetime (rowTimes)) b = datetime (b); b.TimeZone = rowTimes.TimeZone; else b = duration (b); endif return endif if (! strcmp (class (b), class (rowTimes))) error (strcat ("timerange: a timetable with %s row times cannot be", ... " subscripted with %s bounds."), ... class (rowTimes), class (b)); endif endfunction ## The bound as text, for the display. function s = boundstr (b) if (isnumeric (b)) s = sprintf ("%g", b); elseif (ischar (b)) s = b; else s = cellstr (b){1}; endif endfunction ## Test the default interval includes its start and excludes its end %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! assert_equal (rowIndices (timerange (tv(2), tv(4)), tv), [2; 3]); ## Test each interval type %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! assert_equal (rowIndices (timerange (tv(2), tv(4), 'openright'), tv), [2;3]); %! assert_equal (rowIndices (timerange (tv(2), tv(4), 'openleft'), tv), [3;4]); %! assert_equal (rowIndices (timerange (tv(2), tv(4), 'closed'), tv), [2;3;4]); %! assert_equal (rowIndices (timerange (tv(2), tv(4), 'open'), tv), 3); ## Test an infinite bound leaves that end open %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! assert_equal (rowIndices (timerange (-Inf, tv(3)), tv), [1; 2]); %! assert_equal (rowIndices (timerange (tv(3), Inf), tv), [3; 4; 5; 6]); ## Test text bounds are read as the row times' own type %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! tr = timerange ('01-Jan-2024 01:00:00', '01-Jan-2024 03:00:00'); %! assert_equal (rowIndices (tr, tv), [2; 3]); ## Test a range on duration row times %!test %! dv = hours (0:5)'; %! assert_equal (rowIndices (timerange (dv(2), dv(4)), dv), [2; 3]); ## Test the unit form covers the period containing the time %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! assert_equal (rowIndices (timerange (tv(3), 'hour'), tv), 3); %! assert_equal (rowIndices (timerange (tv(1), 'day'), tv), (1:6)'); %! assert_equal (rowIndices (timerange (tv(1), 'year'), tv), (1:6)'); %! assert_equal (rowIndices (timerange (tv(1), 'hours'), tv), 1); ## The three-argument unit form widens the range to whole periods spanning ## both bounds, rather than saying which ends belong to it. Measured on ## MATLAB R2024a. ## Test a unit spanning both bounds runs from period start to period end %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! tr = timerange (tv(2), tv(5), 'hours'); %! assert_equal (rowIndices (tr, tv), (2:5)'); ## Test a coarser unit reaches past both bounds %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! assert_equal (rowIndices (timerange (tv(2), tv(5), 'days'), tv), (1:6)'); %! assert_equal (rowIndices (timerange (tv(2), tv(5), 'months'), tv), (1:6)'); %! assert_equal (rowIndices (timerange (tv(2), tv(5), 'quarters'), tv), ... %! (1:6)'); %! assert_equal (rowIndices (timerange (tv(2), tv(5), 'years'), tv), (1:6)'); ## Test a finer unit adds only the part of the period past the end bound %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! assert_equal (rowIndices (timerange (tv(2), tv(5), 'seconds'), tv), ... %! (2:5)'); %! assert_equal (rowIndices (timerange (tv(2), tv(5), 'minutes'), tv), ... %! (2:5)'); ## Test the singular names are taken as the plural ones are %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! assert_equal (rowIndices (timerange (tv(2), tv(5), 'hour'), tv), (2:5)'); %! assert_equal (rowIndices (timerange (tv(2), tv(5), 'day'), tv), (1:6)'); ## Test the widened range keeps its far end out %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! [lo, hi] = interval (timerange (tv(2), tv(4), 'hours'), tv); %! assert_equal (lo, tv(2)); %! assert_equal (hi, tv(5)); ## Test the bounds of the unit form are read off a partial time %!test %! tv = datetime (2024, 1, 1) + minutes (0:30:150)'; %! assert_equal (rowIndices (timerange (tv(2), tv(4), 'hours'), tv), (1:4)'); ## Test input validation for the three-argument unit form %!error ... %! timerange (hours (1), hours (3), 'hours'); %!error ... %! timerange (datetime (2024, 1, 1), hours (3), 'hours'); ## Test a reversed or distant range selects nothing %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! assert_equal (rowIndices (timerange (tv(4), tv(2)), tv), zeros (0, 1)); %! assert_equal (rowIndices (timerange (tv(1) - hours (9), ... %! tv(1) - hours (5)), tv), zeros (0, 1)); %!error ... %! timerange (datetime (2024, 1, 1), datetime (2024, 1, 2), 'halfopen'); %!error ... %! timerange (-Inf, Inf); %!error ... %! timerange ({1}, datetime (2024, 1, 1)); %!error ... %! timerange (datetime (2024, 1, 1), datetime (2024, 1, 1) + hours (0:2)'); %!error ... %! timerange (hours (1), 'day'); %!error ... %! rowIndices (timerange (datetime (2024, 1, 1), datetime (2024, 1, 2)), ... %! hours (0:2)'); ## The time of the first event TT carries that EF matches, which is what one ## end of a range given as event filters comes to. function t = firstEventTime (ef, tt, which) times = eventTimes (ef, tt); if (isempty (times)) error (strcat ("timerange: the %s bound's event filter matches no", ... " event of this timetable."), which); endif t = times(1); endfunction pr0m1th3as-datatypes-9450b29/inst/timeseries.m000066400000000000000000005171651527370255000212210ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef timeseries ## -*- texinfo -*- ## @deftp {datatypes} timeseries ## ## Array of samples indexed by time. ## ## A @code{timeseries} object holds a sequence of samples, each taken at a ## time on a common time vector, together with the metadata that describes ## them: the units of the data and of the time, an optional absolute start ## date, the method of evaluating the series between samples, a quality code ## per sample, and named events. ## ## Each sample may be a scalar, a vector, or an array. The time vector runs ## either down the first dimension of the data, each row being a sample, or ## along its last dimension, each slice being one; @qcode{IsTimeFirst} ## reports which, as the constructor describes. ## ## @code{timeseries} is a value class: every method returns a modified copy ## and leaves its input unchanged. An array of @code{timeseries} objects is ## built by concatenation, as @code{[@var{ts1}, @var{ts2}]}. ## ## MATLAB recommends @code{timetable} for new code; @code{timeseries} is ## provided for code that exchanges it. ## ## Property names are case-sensitive in dot indexing, as for every other ## class; MATLAB also reads @code{@var{ts}.name} for ## @code{@var{ts}.Name}. @code{get} and @code{set} accept any case. ## ## @seealso{tscollection, timeseries2timetable, timetable, ## tsdata.timemetadata, tsdata.datametadata, tsdata.qualmetadata, ## tsdata.event, tsdata.interpolation} ## @end deftp properties (Dependent) ## -*- texinfo -*- ## @deftp {timeseries} {property} Events ## ## The events of the series. ## ## A vector of @code{tsdata.event} objects, or @code{[]} when there are ## none, by default. ## ## @end deftp Events ## -*- texinfo -*- ## @deftp {timeseries} {property} Name ## ## The name of the series. ## ## A character vector, @qcode{'unnamed'} by default, and @qcode{''} for a ## series created with no argument. A string scalar is converted; any ## other value is refused, where MATLAB accepts it. ## ## @end deftp Name ## -*- texinfo -*- ## @deftp {timeseries} {property} UserData ## ## Any data the user attaches, @code{[]} by default. ## ## @end deftp UserData ## -*- texinfo -*- ## @deftp {timeseries} {property} Data ## ## The samples. ## ## A numeric or logical array of any class, holding one sample per time ## and read against the time vector as the constructor describes: rows, ## or slices along the last dimension, with a matrix of as many columns ## as there are times stored as an @code{Rx1xN} array. Any other number ## of samples than there are times is refused, except while one of the ## two is empty, which lets a series be built by assigning @qcode{Time} ## and @qcode{Data} in turn. ## ## @end deftp Data ## -*- texinfo -*- ## @deftp {timeseries} {property} DataInfo ## ## The metadata of the data, a @code{tsdata.datametadata} object. ## ## @end deftp DataInfo ## -*- texinfo -*- ## @deftp {timeseries} {property} Time ## ## The time vector. ## ## A column vector of finite, non-decreasing times, in ## @qcode{@var{ts}.TimeInfo.Units}, counted from ## @qcode{@var{ts}.TimeInfo.StartDate} when that is set. Repeated times ## are allowed. An unsorted time vector is refused on assignment, since ## sorting it would move samples it was not given with. It is stored as ## @code{double}: MATLAB keeps an integer class, and then rounds new times ## to it. ## ## @end deftp Time ## -*- texinfo -*- ## @deftp {timeseries} {property} TimeInfo ## ## The metadata of the time vector, a @code{tsdata.timemetadata} object. ## Its @qcode{Length}, @qcode{Start}, @qcode{End} and @qcode{Increment} ## follow @qcode{Time} and cannot be assigned. ## ## @end deftp TimeInfo ## -*- texinfo -*- ## @deftp {timeseries} {property} Quality ## ## A quality code per sample. ## ## Integers from -128 to 127, stored as @code{double}, @code{[]} by ## default. Either one code per sample, as a vector, or one per data ## element, as an array the size of @qcode{Data}. Their meaning is listed ## in @qcode{QualityInfo}. ## ## @end deftp Quality ## -*- texinfo -*- ## @deftp {timeseries} {property} QualityInfo ## ## The description of the quality codes, a @code{tsdata.qualmetadata} ## object. ## ## @end deftp QualityInfo ## -*- texinfo -*- ## @deftp {timeseries} {property} IsTimeFirst ## ## Whether the time vector runs along the first dimension of the data. ## ## @code{true} when each row of the data is a sample, @code{false} when ## the time vector runs along the last dimension of the data, as the ## constructor describes. It is kept when samples are selected, so a ## three-dimensional series cut to one sample holds a matrix with ## @qcode{IsTimeFirst} still @code{false}. Assigning the value it already ## has is accepted, and assigning the other is refused, since it would ## misalign the data with the time vector. ## ## @end deftp IsTimeFirst ## -*- texinfo -*- ## @deftp {timeseries} {property} TreatNaNasMissing ## ## Whether the statistics methods treat @code{NaN} as missing data. ## ## A logical scalar, @code{true} by default; 0 and 1 are converted. ## ## @end deftp TreatNaNasMissing ## -*- texinfo -*- ## @deftp {timeseries} {property} Length ## ## The number of samples, which is the length of the time vector. ## Read-only. ## ## @end deftp Length endproperties properties (Access = private) events_ = [] name_ = 'unnamed' userData_ = [] data_ = [] dataInfo_ = tsdata.datametadata () time_ = zeros (0, 1) timeInfo_ = tsdata.timemetadata () quality_ = [] qualityInfo_ = tsdata.qualmetadata () treatNaN_ = true ## The dimension of the data the time vector runs along: 1, or the last ## dimension, which a matrix holding one sample takes as 3 timeDim_ = 1 endproperties methods (Hidden) function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif if (! isscalar (this)) disp (this); return; endif fprintf ("\n timeseries\n\n Common Properties:\n"); names = {'Name', 'Time', 'TimeInfo', 'Data', 'DataInfo'}; for i = 1:numel (names) fprintf ("%16s: %s\n", names{i}, ... timeseries.dispValue (this.(names{i}))); endfor fprintf ("\n"); endfunction function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif if (! isscalar (this)) fprintf (" %s timeseries array\n\n", sizestr (this)); return; endif fprintf ("\n timeseries with properties:\n\n"); names = properties (this); for i = 1:numel (names) fprintf ("%21s: %s\n", names{i}, ... timeseries.dispValue (this.(names{i}))); endfor fprintf ("\n"); endfunction endmethods methods (Static, Hidden) ## The body of 'get' for 'timeseries' and the 'tsdata' classes. OBJ is ## the object or array, SCOPE the name errors are raised under, and the ## remaining argument, if any, the property name or names. function out = propertyGet (obj, scope, varargin) if (numel (varargin) > 1) error ("%s: too many input arguments.", scope); endif allNames = properties (class (obj)); if (isempty (varargin)) if (isscalar (obj)) out = struct (); for i = 1:numel (allNames) out.(allNames{i}) = obj.(allNames{i}); endfor else out = cell (numel (obj), numel (allNames)); for k = 1:numel (obj) for i = 1:numel (allNames) out{k,i} = obj(k).(allNames{i}); endfor endfor endif return; endif names = varargin{1}; isList = iscell (names) || (isstring (names) && ! isscalar (names)); if (isstring (names)) names = cellstr (names); endif if (ischar (names) && isrow (names)) names = {names}; endif if (! iscellstr (names)) error (strcat ("%s: NAME must be a character vector or a cell", ... " array of character vectors."), scope); endif for i = 1:numel (names) [names{i}, errmsg] = propertyName (names{i}, allNames); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif endfor out = cell (numel (obj), numel (names)); for k = 1:numel (obj) for i = 1:numel (names) out{k,i} = obj(k).(names{i}); endfor endfor if (! isList) if (isscalar (obj)) out = out{1}; else out = reshape (out, size (obj)); endif endif endfunction ## The body of 'set' for 'timeseries' and the 'tsdata' classes: returns ## OBJ with the property name and value pairs in VARARGIN set on every ## element. Writing the result back to the caller stays with each ## class's 'set', since 'inputname' names the arguments of its caller. function obj = propertySet (obj, scope, varargin) if (mod (numel (varargin), 2) != 0) error ("%s: name-value arguments must be in pairs.", scope); endif allNames = properties (class (obj)); for i = 1:2:numel (varargin) name = varargin{i}; if (! isText (name)) error ("%s: NAME must be a character vector.", scope); endif [name, errmsg] = propertyName (char (name), allNames); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif for k = 1:numel (obj) obj(k).(name) = varargin{i+1}; endfor endfor endfunction ## A property value as the property listings of 'timeseries' and the ## 'tsdata' classes show it: text quoted, a scalar written out, anything ## else summarized by its size and class. function str = dispValue (val) if (ischar (val) && (isrow (val) || isempty (val))) str = sprintf ("'%s'", val); elseif (isempty (val) && (isnumeric (val) || islogical (val))) if (isequal (size (val), [0, 0])) str = '[]'; else str = sprintf ("[%s %s]", sizestr (val), class (val)); endif elseif ((isnumeric (val) || islogical (val)) && isscalar (val)) str = num2str (val); elseif (isa (val, 'function_handle')) str = func2str (val); if (str(1) != '@') str = ['@', str]; endif elseif (iscell (val)) str = sprintf ("{%s cell}", sizestr (val)); else str = sprintf ("[%s %s]", sizestr (val), class (val)); endif endfunction endmethods ################################################################################ ## ** Create and describe 'timeseries' ** ## ################################################################################ ## Available Methods ## ## ## ## 'timeseries' 'get' 'set' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} timeseries () ## @deftypefnx {timeseries} {@var{ts} =} timeseries (@var{name}) ## @deftypefnx {timeseries} {@var{ts} =} timeseries (@var{data}) ## @deftypefnx {timeseries} {@var{ts} =} timeseries (@var{data}, @var{time}) ## @deftypefnx {timeseries} {@var{ts} =} timeseries (@var{data}, @var{time}, @var{quality}) ## @deftypefnx {timeseries} {@var{ts} =} timeseries (@dots{}, @qcode{'Name'}, @var{name}) ## ## Create a @code{timeseries} object. ## ## @code{@var{ts} = timeseries ()} returns an empty series with no name. ## ## @code{@var{ts} = timeseries (@var{name})} returns an empty series named ## @var{name}, a character vector or a string scalar. MATLAB takes a ## string scalar here as the data of a one-sample series. ## ## @code{@var{ts} = timeseries (@var{data})} returns a series of the ## samples in @var{data}, a numeric or logical array, at times 0, 1, 2, ## @dots{} seconds. For data of two dimensions or fewer each row is a ## sample; for data of three dimensions or more each slice along the last ## dimension is one. A row vector of more than one element is taken as ## that many scalar samples and stored as a @code{1x1xN} array. ## ## @code{@var{ts} = timeseries (@var{data}, @var{time})} takes the samples ## at @var{time}, which is one of: ## ## @itemize ## @item a numeric vector of one time per sample, in seconds; ## @item a cell array of character vectors, a string array or a ## @code{datetime} array of dates, one per sample, which gives @qcode{Time} ## in days from the earliest of them, with that date as ## @qcode{TimeInfo.StartDate}; each date is read by its own format, a time ## zone is dropped and the clock time kept, and @code{NaT} is refused; ## @item a @code{duration} array, relative times in the units its display ## format names: @qcode{'s'}, @qcode{'m'}, @qcode{'h'} and @qcode{'d'} give ## seconds, minutes, hours and days, any other format seconds; ## @item a cell array of numeric scalars, read as a numeric vector; ## @item @code{[]}, for the default times. ## @end itemize ## ## With @var{N} times, data of three dimensions or more must have @var{N} ## slices along its last dimension. A matrix with @var{N} rows has a ## sample per row; otherwise one with @var{N} columns is stored as an ## @code{Rx1xN} array, a sample per column, so a row vector of @var{N} ## elements gives @var{N} scalar samples; and with a single time the whole ## matrix is one sample. The times need not be sorted: the samples are ## sorted with them, keeping the order of samples at equal times. They ## are stored as @code{double} whatever their class. MATLAB accepts ## neither @code{datetime} nor @code{duration} arrays here. ## ## @code{@var{ts} = timeseries (@var{data}, @var{time}, @var{quality})} ## also sets a quality code per sample: integers from -128 to 127, as a ## vector of one per sample or an array the size of @var{data} as stored, ## or @code{[]} for none. A matrix stored a sample per column, as an ## @code{Rx1xN} array, takes codes per element in that shape too; MATLAB ## refuses them in the matrix's own @code{RxN} shape, as here. ## ## @code{@var{ts} = timeseries (@dots{}, @qcode{'Name'}, @var{name})} names ## the series; the option name is matched in any case, and a later ## occurrence overrides an earlier one. A @var{name} that is not text is ## refused, where MATLAB ignores it. ## ## @code{@var{ts} = timeseries (@var{ts0})} returns the series @var{ts0} ## itself. ## ## @seealso{timetable, istimeseries, tsdata.timemetadata} ## @end deftypefn function this = timeseries (varargin) if (nargin == 0) this.name_ = ''; return; endif ## A series or a name alone arg1 = varargin{1}; if (nargin == 1 && isa (arg1, 'timeseries')) this = arg1; return; endif if (isText (arg1)) if (nargin > 1) error ("timeseries: DATA must be a numeric or logical array."); endif this.name_ = char (arg1); return; endif if (! (isnumeric (arg1) || islogical (arg1))) error ("timeseries: DATA must be a numeric or logical array."); endif data = arg1; ## Positional arguments end at the first option name args = varargin(2:end); nPos = numel (args); for i = 1:numel (args) if (isText (args{i})) nPos = i - 1; break; endif endfor if (nPos > 2) error ("timeseries: too many input arguments."); endif opts = args(nPos+1:end); if (mod (numel (opts), 2) != 0) error ("timeseries: name-value arguments must be in pairs."); endif for i = 1:2:numel (opts) if (! strcmpi (opts{i}, 'Name')) error ("timeseries: invalid optional paired argument."); endif if (! isText (opts{i+1})) error ("timeseries: 'Name' must be a character vector."); endif this.name_ = char (opts{i+1}); endfor ## Time vector time = []; startDate = ''; units = ''; if (nPos >= 1) [time, startDate, units, errmsg] = parseTime (args{1}); if (! isempty (errmsg)) error ("timeseries: %s", errmsg); endif endif ## Which dimension of the data the time vector runs along if (nPos < 1 || isempty (time)) [data, td] = orientData (data, []); time = (0:size (data, td) - 1)'; else [data, td, errmsg] = orientData (data, numel (time)); if (! isempty (errmsg)) error (strcat ("timeseries: DATA and TIME must have the same", ... " number of samples.")); endif endif nSamples = numel (time); ## Quality quality = []; if (nPos == 2) [quality, errmsg] = qualityValue (args{2}, data, nSamples, td); if (! isempty (errmsg)) error ("timeseries: QUALITY %s", errmsg); endif endif ## Sort the whole record by time, stably if (any (diff (time) < 0)) [time, idx] = sort (time); data = takeSamples (data, idx, td); if (! isempty (quality)) quality = takeSamples (quality, idx, td); endif endif this.timeDim_ = td; this.data_ = data; this.time_ = time; this.quality_ = quality; this.timeInfo_.TimeVector = time; if (! isempty (units)) this.timeInfo_.Units = units; endif if (! isempty (startDate)) this.timeInfo_.StartDate = startDate; endif endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{S} =} get (@var{ts}) ## @deftypefnx {timeseries} {@var{value} =} get (@var{ts}, @var{name}) ## @deftypefnx {timeseries} {@var{values} =} get (@var{ts}, @var{names}) ## ## Return property values. ## ## @code{@var{S} = get (@var{ts})} returns a structure of every property ## of the series @var{ts}. ## ## @code{@var{value} = get (@var{ts}, @var{name})} returns the property ## @var{name}, a character vector or a string scalar matched in any case. ## ## @code{@var{values} = get (@var{ts}, @var{names})} returns a row cell ## array of the properties named in the cell array @var{names}. ## ## For an array of series, @code{get} with one @var{name} returns a cell ## array the size of the array, with a cell array of @var{names} a cell ## array with a row per series and a column per property, and with no ## name a cell array with a row per series and a column per property of ## every property. ## ## @seealso{timeseries.set, timeseries} ## @end deftypefn function out = get (this, varargin) out = timeseries.propertyGet (this, 'timeseries.get', varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {} set (@var{ts}, @var{name}, @var{value}) ## @deftypefnx {timeseries} {} set (@var{ts}, @var{name1}, @var{value1}, @dots{}) ## @deftypefnx {timeseries} {@var{ts2} =} set (@var{ts}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {timeseries} {@var{S} =} set (@var{ts}) ## ## Set property values. ## ## @code{set (@var{ts}, @var{name}, @var{value}, @dots{})} sets each ## property @var{name}, matched in any case, to its @var{value}, in the ## order given, in the variable @var{ts} of the caller. @var{ts} must ## therefore be a variable, not an expression such as an indexed element ## or @code{@var{tc}.Series}; set those with dot assignment. For an array ## of series every element is set. ## ## @code{@var{ts2} = set (@var{ts}, @var{name}, @var{value}, @dots{})} ## returns the modified series and leaves @var{ts} as it was, so ## @var{ts} may then be any expression. ## ## @code{@var{S} = set (@var{ts})} returns what @code{get (@var{ts})} ## does. A name with no value is refused; MATLAB returns the current ## value quoted as text. ## ## @seealso{timeseries.get, timeseries} ## @end deftypefn function varargout = set (this, varargin) if (nargin == 1) varargout{1} = get (this); return; endif this = timeseries.propertySet (this, 'timeseries.set', varargin{:}); if (nargout > 0) varargout{1} = this; return; endif ## With no output the caller's own variable is set, as in MATLAB name = inputname (1); if (isempty (name)) error (strcat ("timeseries.set: with no output argument, TS must", ... " be a variable; set anything else with dot", ... " assignment.")); endif assignin ('caller', name, this); endfunction endmethods ################################################################################ ## ** Data and sample operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'getdatasamples' 'getdatasamplesize' 'getsamples' 'getsampleusingtime' ## ## 'addsample' 'delsample' 'append' 'hasduplicatetimes' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{data} =} getdatasamples (@var{ts}, @var{ind}) ## ## Return the data of selected samples. ## ## @code{@var{data} = getdatasamples (@var{ts}, @var{ind})} returns the ## data of the samples @var{ind} of the series @var{ts}, in the order ## given, repeats included: the rows @var{ind} of @qcode{Data} for a series ## whose time runs down its first dimension, the slices @var{ind} along the ## last dimension otherwise. @var{ind} is a vector of positive integers ## not exceeding @qcode{Length}, or a logical mask no longer than ## @qcode{Length}; @code{[]} returns @code{[]}. ## ## @seealso{timeseries.getsamples, timeseries.getdatasamplesize} ## @end deftypefn function data = getdatasamples (this, varargin) if (numel (varargin) != 1) error ("timeseries.getdatasamples: invalid number of input arguments."); endif mustBeScalar (this, 'getdatasamples'); [idx, errmsg] = sampleIndex (varargin{1}, numel (this.time_), true); if (! isempty (errmsg)) error ("timeseries.getdatasamples: IND %s", errmsg); endif if (isempty (idx)) data = []; else data = takeSamples (this.data_, idx, this.timeDim_); endif endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{sz} =} getdatasamplesize (@var{ts}) ## ## Return the size of one sample. ## ## @code{@var{sz} = getdatasamplesize (@var{ts})} returns the size of each ## sample of the series @var{ts}: @code{[1 @var{C}]} for data of @var{C} ## columns with a sample per row, and the size of a slice for data whose ## time runs along the last dimension, so @code{[2 3]} for a ## @code{2x3x4} array. It is @code{[]} for a series with no samples. ## ## @seealso{timeseries.getdatasamples} ## @end deftypefn function sz = getdatasamplesize (this) mustBeScalar (this, 'getdatasamplesize'); if (isempty (this.time_) || isempty (this.data_)) sz = []; else sz = sampleSize (this.data_, this.timeDim_); endif endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts2} =} getsamples (@var{ts}, @var{ind}) ## ## Return a series of selected samples. ## ## @code{@var{ts2} = getsamples (@var{ts}, @var{ind})} returns the series ## @var{ts} holding only the samples @var{ind}, which are taken in time ## order whatever order they are given in, repeats included. @var{ind} ## is as for @code{getdatasamples}. Their times and quality codes go with ## them, and every other property is kept. ## ## MATLAB names the result @qcode{'unnamed'}, and for an empty @var{ind} ## returns @code{timeseries ()} with every property reset; here the name ## and properties are kept in both cases. ## ## @seealso{timeseries.getdatasamples, timeseries.getsampleusingtime, ## timeseries.delsample} ## @end deftypefn function this = getsamples (this, varargin) if (numel (varargin) != 1) error ("timeseries.getsamples: invalid number of input arguments."); endif mustBeScalar (this, 'getsamples'); [idx, errmsg] = sampleIndex (varargin{1}, numel (this.time_), true); if (! isempty (errmsg)) error ("timeseries.getsamples: IND %s", errmsg); endif this = subset (this, sort (idx)); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts2} =} getsampleusingtime (@var{ts}, @var{t}) ## @deftypefnx {timeseries} {@var{ts2} =} getsampleusingtime (@var{ts}, @var{t}, @qcode{'AllowDuplicateTimes'}, @var{tf}) ## @deftypefnx {timeseries} {@var{ts2} =} getsampleusingtime (@var{ts}, @var{t1}, @var{t2}) ## ## Return a series of the samples at a time or between two times. ## ## @code{@var{ts2} = getsampleusingtime (@var{ts}, @var{t})} returns the ## series @var{ts} holding only the sample at time @var{t}. When several ## samples share that time the call is refused, unless ## @qcode{'AllowDuplicateTimes'} is @code{true}, which returns them all. ## MATLAB returns them all for @code{false} too. ## ## @code{@var{ts2} = getsampleusingtime (@var{ts}, @var{t1}, @var{t2})} ## returns the samples from @var{t1} to @var{t2}, both included. ## ## For a series with no @qcode{TimeInfo.StartDate} the times are numbers ## in its units. For a series with one they are dates: text, a ## @code{datenum}, or a @code{datetime}, which MATLAB does not accept. ## No sample in range, @code{NaN}, or @var{t1} after @var{t2} give a ## series with no samples. Every property but the samples is kept; ## MATLAB names the result @qcode{'unnamed'} and, when it is empty, ## resets every property. ## ## @seealso{timeseries.getsamples, timeseries.delsample} ## @end deftypefn function this = getsampleusingtime (this, varargin) mustBeScalar (this, 'getsampleusingtime'); scope = 'timeseries.getsampleusingtime'; nargs = numel (varargin); if (nargs < 1 || nargs > 3) error ("%s: invalid number of input arguments.", scope); endif allowDup = false; isRange = nargs == 2; if (nargs == 3) if (! (isText (varargin{2}) && strcmpi (varargin{2}, 'AllowDuplicateTimes'))) error ("%s: invalid optional paired argument.", scope); endif allowDup = varargin{3}; if (! ((islogical (allowDup) || isnumeric (allowDup)) && isscalar (allowDup) && any (allowDup == [0, 1]))) error ("%s: 'AllowDuplicateTimes' must be a logical scalar.", scope); endif endif [t, tol, errmsg] = relativeTime (this, varargin{1}, true); if (isempty (errmsg) && ! isscalar (t)) errmsg = "must be a single time."; endif if (! isempty (errmsg)) error ("%s: %s %s", scope, ifelse (isRange, 'T1', 'T'), errmsg); endif if (isRange) [t2, tol2, errmsg] = relativeTime (this, varargin{2}, true); if (isempty (errmsg) && ! isscalar (t2)) errmsg = "must be a single time."; endif if (! isempty (errmsg)) error ("%s: T2 %s", scope, errmsg); endif idx = find (this.time_ >= t - tol & this.time_ <= t2 + tol2); else idx = find (abs (this.time_ - t) <= tol); if (numel (idx) > 1 && ! allowDup) error (strcat ("%s: several samples share time T; set", ... " 'AllowDuplicateTimes' to true to return them", ... " all."), scope); endif endif this = subset (this, idx); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} addsample (@var{ts}, @qcode{'Data'}, @var{data}, @qcode{'Time'}, @var{time}) ## @deftypefnx {timeseries} {@var{ts} =} addsample (@dots{}, @qcode{'Quality'}, @var{quality}) ## @deftypefnx {timeseries} {@var{ts} =} addsample (@dots{}, @qcode{'OverwriteFlag'}, @var{tf}) ## @deftypefnx {timeseries} {@var{ts} =} addsample (@var{ts}, @var{S}) ## ## Add samples to a series. ## ## @code{@var{ts} = addsample (@var{ts}, @qcode{'Data'}, @var{data}, ## @qcode{'Time'}, @var{time})} inserts the samples @var{data} at the ## times @var{time}, one sample per time, keeping the series in time ## order; a sample at a time already present goes after the samples ## there. @var{data} is read against @var{time} as the constructor reads ## data against times, and each sample must have the size of the series' ## samples; a series with no samples takes any. For a series with no ## @qcode{TimeInfo.StartDate} @var{time} is numbers in its units; for one ## with a start date it may also be dates as text, or a @code{datetime}. ## The class of the data follows concatenation, so an @code{int8} sample ## makes a @code{double} series @code{int8}. ## ## The option names are matched in any case, and a later occurrence ## overrides an earlier one. They may also be given as the fields of a ## structure @var{S}. ## ## @table @asis ## @item @qcode{'Quality'} ## The quality codes of the new samples, one per sample or one per data ## element, as the series has them. Refused for a series without quality ## codes. When a series with codes is given none, each new sample takes ## the code of the sample nearest in time, the later one on a tie. ## ## @item @qcode{'OverwriteFlag'} ## When @code{true}, a new sample at a time already present replaces the ## data of every sample there, and their quality codes if ## @qcode{'Quality'} is given, instead of being inserted. @code{false} by ## default. MATLAB refuses 0 and 1 here. ## @end table ## ## @seealso{timeseries.delsample, timeseries.append} ## @end deftypefn function this = addsample (this, varargin) mustBeScalar (this, 'addsample'); scope = 'timeseries.addsample'; args = varargin; if (numel (args) == 1 && isstruct (args{1}) && isscalar (args{1})) args = [fieldnames(args{1}), struct2cell(args{1})]'; args = args(:)'; endif if (mod (numel (args), 2) != 0) error ("%s: name-value arguments must be in pairs.", scope); endif optNames = {'Data', 'Time', 'Quality', 'OverwriteFlag'}; opts = {[], [], [], false}; for i = 1:2:numel (args) k = []; if (isText (args{i})) k = find (strcmpi (args{i}, optNames)); endif if (isempty (k)) error ("%s: invalid optional paired argument.", scope); endif opts{k} = args{i+1}; endfor [data, time, quality, overwrite] = opts{:}; if (iscell (data) || ! (isnumeric (data) || islogical (data))) error ("%s: 'Data' must be a numeric or logical array.", scope); endif if (isempty (data)) error ("%s: 'Data' must not be empty.", scope); endif if (isempty (time)) error ("%s: 'Time' must not be empty.", scope); endif [time, tol, errmsg] = relativeTime (this, time, false); if (! isempty (errmsg)) error ("%s: 'Time' %s", scope, errmsg); endif if (! all (isfinite (time))) error ("%s: 'Time' must be finite.", scope); endif if (! ((islogical (overwrite) || isnumeric (overwrite)) && isscalar (overwrite) && any (overwrite == [0, 1]))) error ("%s: 'OverwriteFlag' must be a logical scalar.", scope); endif k = numel (time); tdS = this.timeDim_; if (k == 1 && tdS > 1 && ! isempty (this.data_) && isequal (size (data), size (takeSamples (this.data_, 1, tdS)))) ## One sample shaped as the series' own, whatever its dimensions td = tdS; else [data, td, errmsg] = orientData (data, k); if (! isempty (errmsg)) error ("%s: 'Data' must have one sample per time.", scope); endif endif ## A series with no samples takes them as the constructor would if (isempty (this.time_) && isempty (this.data_)) if (! isempty (quality)) error (strcat ("%s: 'Quality' cannot be added to a series", ... " without quality codes."), scope); endif [time, idx] = sort (time); this.data_ = takeSamples (data, idx, td); this.time_ = time; this.timeDim_ = td; this.timeInfo_.TimeVector = time; if (isempty (this.name_)) this.name_ = 'unnamed'; endif return; endif if (isempty (this.time_) || isempty (this.data_)) error ("%s: the 'Data' and 'Time' of TS do not agree.", scope); endif ## New samples laid out as the series' own tdS = this.timeDim_; ss = sampleSize (this.data_, tdS); if (! isequal (sampleSize (data, td), ss)) error ("%s: 'Data' must hold samples of size %s.", scope, ... strjoin (arrayfun (@num2str, ss, 'UniformOutput', false), 'x')); endif data = layOut (data, td, tdS, ss, k); ## Quality codes of the new samples hasQ = ! isempty (this.quality_); perElement = hasQ && numel (this.quality_) != numel (this.time_); if (! isempty (quality)) if (! hasQ) error (strcat ("%s: 'Quality' cannot be added to a series", ... " without quality codes."), scope); endif if (! ((isnumeric (quality) || islogical (quality)) && isreal (quality) && all (quality(:) == fix (quality(:))) && all (quality(:) >= -128 & quality(:) <= 127))) error ("%s: 'Quality' must hold integers from -128 to 127.", scope); endif quality = double (quality); if (perElement && numel (quality) == numel (data)) quality = reshape (quality, size (data)); elseif (! perElement && numel (quality) == k) if (tdS > 1) quality = reshape (quality, [ones(1, tdS - 1), k]); else quality = quality(:); endif else error (strcat ("%s: 'Quality' must have one code per new sample", ... " or one per new data element."), scope); endif elseif (hasQ) ## The code of the nearest sample in time, the later on a tie near = zeros (k, 1); for i = 1:k d = abs (this.time_ - time(i)); near(i) = find (d == min (d), 1, 'last'); endfor quality = takeSamples (this.quality_, near, tdS); endif ## Replace the samples at times already present keep = true (k, 1); if (overwrite) for i = 1:k at = find (abs (this.time_ - time(i)) <= tol); if (isempty (at)) continue; endif keep(i) = false; for j = at' this.data_ = putSample (this.data_, j, ... takeSamples (data, i, tdS), tdS); if (! isempty (quality) && ! isempty (opts{3})) this.quality_ = putSample (this.quality_, j, ... takeSamples (quality, i, tdS), tdS); endif endfor endfor endif ## Insert the rest, keeping the series in time order if (any (keep)) cdim = max (tdS, 1); newData = cat (cdim, this.data_, takeSamples (data, find (keep), tdS)); newTime = [this.time_; time(keep)]; if (hasQ) newQ = cat (cdim, this.quality_, ... takeSamples (quality, find (keep), tdS)); endif [newTime, idx] = sort (newTime); this.data_ = takeSamples (newData, idx, tdS); this.time_ = newTime; if (hasQ) this.quality_ = takeSamples (newQ, idx, tdS); endif this.timeInfo_.TimeVector = newTime; endif endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} delsample (@var{ts}, @qcode{'Index'}, @var{ind}) ## @deftypefnx {timeseries} {@var{ts} =} delsample (@var{ts}, @qcode{'Value'}, @var{time}) ## ## Remove samples from a series. ## ## @code{@var{ts} = delsample (@var{ts}, @qcode{'Index'}, @var{ind})} ## removes the samples @var{ind}, a vector of positive integers not ## exceeding @qcode{Length}; repeats are allowed and @code{[]} removes ## nothing. A logical mask is refused, as in MATLAB. ## ## @code{@var{ts} = delsample (@var{ts}, @qcode{'Value'}, @var{time})} ## removes every sample at each of the times @var{time}; a time no sample ## has removes nothing. For a series with no @qcode{TimeInfo.StartDate} ## the times are numbers in its units; for one with a start date they may ## also be dates as text, or a @code{datetime}. ## ## The option names are matched in any case. Every property but the ## samples is kept. MATLAB turns quality codes held per data element of ## three-dimensional data into a vector here; they lose the slice of each ## removed sample, as the data does. ## ## @seealso{timeseries.addsample, timeseries.getsamples} ## @end deftypefn function this = delsample (this, varargin) mustBeScalar (this, 'delsample'); scope = 'timeseries.delsample'; if (numel (varargin) != 2) error ("%s: invalid number of input arguments.", scope); endif [opt, val] = varargin{:}; n = numel (this.time_); if (isText (opt) && strcmpi (opt, 'Index')) [idx, errmsg] = sampleIndex (val, n, false); if (! isempty (errmsg)) error ("%s: 'Index' %s", scope, errmsg); endif elseif (isText (opt) && strcmpi (opt, 'Value')) [t, tol, errmsg] = relativeTime (this, val, false); if (! isempty (errmsg)) error ("%s: 'Value' %s", scope, errmsg); endif idx = []; for i = 1:numel (t) idx = [idx; find(abs (this.time_ - t(i)) <= tol)]; endfor else error ("%s: the option must be 'Index' or 'Value'.", scope); endif keep = true (n, 1); keep(idx) = false; this = subset (this, find (keep)); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} append (@var{ts1}, @var{ts2}, @dots{}) ## ## Join series end to end in time. ## ## @code{@var{ts} = append (@var{ts1}, @var{ts2}, @dots{})} returns one ## series holding the samples of every argument in turn. An argument may ## be an array of series, taken element by element, and series with no ## samples are skipped. Each series must start at or after the end of the ## one before, so equal times where two meet give a repeated time; a gap ## is kept. Their samples must be of one size; the class of the data ## follows concatenation. ## ## Series with a @qcode{TimeInfo.StartDate} and series without one cannot ## be mixed. Times are converted to the coarsest units among them, and ## dates are counted from the first series' start date. ## ## Quality codes are joined when every series has them and refused when ## only some do; MATLAB then keeps too few codes or drops them. The ## result takes its name, @qcode{DataInfo.Units} and @qcode{QualityInfo} ## from the series when they all agree, and otherwise @qcode{'unnamed'}, ## @qcode{''} and none; MATLAB always names it @qcode{'unnamed'} and drops ## @qcode{QualityInfo}. Events are gathered from every series, and every ## other property is the first series'. ## ## @seealso{timeseries.addsample} ## @end deftypefn function out = append (varargin) scope = 'timeseries.append'; list = {}; for i = 1:numel (varargin) if (! isa (varargin{i}, 'timeseries')) error ("%s: every argument must be a timeseries.", scope); endif for k = 1:numel (varargin{i}) list{end+1} = varargin{i}(k); endfor endfor if (isempty (list)) out = varargin{1}; return; endif full = list(cellfun (@(x) x.Length > 0, list)); if (isempty (full)) out = list{1}; return; elseif (numel (full) == 1) out = full{1}; return; endif first = full{1}; absolute = cellfun (@(x) ! isempty (x.timeInfo_.StartDate), full); if (any (absolute) && ! all (absolute)) error (strcat ("%s: series with and without a start date cannot", ... " be appended."), scope); endif ss = sampleSize (first.data_, first.timeDim_); if (! all (cellfun (@(x) isequal (sampleSize (x.data_, x.timeDim_), ... ss), full))) error ("%s: every series must have samples of the same size.", scope); endif hasQ = cellfun (@(x) ! isempty (x.quality_), full); if (any (hasQ) && ! all (hasQ)) error (strcat ("%s: either every series or none must have quality", ... " codes."), scope); endif ## Times in the coarsest units, from the first start date units = cellfun (@(x) x.timeInfo_.Units, full, 'UniformOutput', false); [~, coarsest] = max (cellfun (@unitRank, units)); unit = units{coarsest}; td = first.timeDim_; times = cell (1, numel (full)); datas = times; quals = times; for i = 1:numel (full) x = full{i}; ns = x.time_ * nsPerUnit (x.timeInfo_.Units); if (all (absolute)) dvX = tsdata.timemetadata.dateVector (x.timeInfo_.StartDate); dv1 = tsdata.timemetadata.dateVector (first.timeInfo_.StartDate); ns = ns + tsdata.timemetadata.dateOffset (dvX, dv1); endif times{i} = ns / nsPerUnit (unit); n = x.Length; datas{i} = layOut (x.data_, x.timeDim_, td, ss, n); if (all (hasQ)) q = x.quality_; if (numel (q) == n) if (td == 1) q = q(:); else q = reshape (q, [ones(1, td - 1), n]); endif else q = layOut (q, x.timeDim_, td, ss, n); endif quals{i} = q; endif if (i > 1 && times{i}(1) < times{i-1}(end) ... - 1e-12 * max (1, abs (times{i-1}(end)))) error (strcat ("%s: each series must start at or after the end", ... " of the one before."), scope); endif endfor out = first; out.data_ = cat (td, datas{:}); out.time_ = vertcat (times{:}); out.timeDim_ = td; out.timeInfo_.Units = unit; out.timeInfo_.TimeVector = out.time_; if (all (hasQ)) try out.quality_ = cat (td, quals{:}); catch error (strcat ("%s: quality codes must be given the same way in", ... " every series."), scope); end_try_catch endif names = cellfun (@(x) x.name_, full, 'UniformOutput', false); if (! all (strcmp (names, names{1}))) out.name_ = 'unnamed'; endif dunits = cellfun (@(x) x.dataInfo_.Units, full, 'UniformOutput', false); if (! all (strcmp (dunits, dunits{1}))) out.dataInfo_.Units = ''; endif qi = first.qualityInfo_; for i = 2:numel (full) qj = full{i}.qualityInfo_; if (! (isequal (qj.Code, qi.Code) && isequal (qj.Description, qi.Description))) out.qualityInfo_ = tsdata.qualmetadata (); break; endif endfor events = {}; for i = 1:numel (full) if (! isempty (full{i}.events_)) events{end+1} = full{i}.events_(:)'; endif endfor if (isempty (events)) out.events_ = []; else out.events_ = [events{:}]; endif endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{tf} =} hasduplicatetimes (@var{ts}) ## ## True where a series repeats a time. ## ## @code{@var{tf} = hasduplicatetimes (@var{ts})} returns @code{true} when ## two samples of the series @var{ts} share a time. For an array of ## series it returns a logical array of its size, one answer per series; ## MATLAB returns a single answer that can miss a series with repeated ## times. ## ## @seealso{timeseries.getsampleusingtime} ## @end deftypefn function tf = hasduplicatetimes (this) tf = false (size (this)); for k = 1:numel (this) tf(k) = any (diff (this(k).time_) == 0); endfor endfunction endmethods ################################################################################ ## ** Time representation ** ## ################################################################################ ## Available Methods ## ## ## ## 'getabstime' 'setabstime' 'setuniformtime' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{dates} =} getabstime (@var{ts}) ## ## Return the dates of the samples as text. ## ## @code{@var{dates} = getabstime (@var{ts})} returns a column cell array ## of character vectors, the date of each sample of the series @var{ts}: ## its time, in @qcode{TimeInfo.Units}, counted from ## @qcode{TimeInfo.StartDate}. They are written in the @code{datestr} ## format @qcode{TimeInfo.Format}, or @qcode{'dd-mmm-yyyy HH:MM:SS'} when ## that is empty; seconds are rounded to the millisecond and the rest cut ## off, as @code{datestr} does. A series with no samples gives @code{@{@}}. ## A series with no @qcode{TimeInfo.StartDate} is refused. ## ## MATLAB uses @qcode{TimeInfo.Format} only when it is one of the formats ## its documentation lists, such as @qcode{'dd-mmm-yyyy'} or ## @qcode{'mm/dd/yyyy'}, and writes the default for any other, ## @qcode{'yyyy-mm-dd'} included; here every format is used. ## ## @seealso{timeseries.setabstime, tsdata.timemetadata} ## @end deftypefn function dates = getabstime (this) mustBeScalar (this, 'getabstime'); startDate = this.timeInfo_.StartDate; if (isempty (startDate)) error ("timeseries.getabstime: TS has no 'TimeInfo.StartDate'."); endif if (isempty (this.time_)) dates = {}; return; endif fmt = this.timeInfo_.Format; if (isempty (fmt)) fmt = 'dd-mmm-yyyy HH:MM:SS'; endif dv0 = tsdata.timemetadata.dateVector (startDate); dn = datenum (dv0(1), dv0(2), dv0(3)) ... + ((dv0(4) * 3600 + dv0(5) * 60 + dv0(6)) * 1e9 ... + this.time_ * nsPerUnit (this.timeInfo_.Units)) / 864e11; dates = cellstr (datestr (dn, fmt)); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} setabstime (@var{ts}, @var{dates}) ## @deftypefnx {timeseries} {@var{ts} =} setabstime (@var{ts}, @var{dates}, @var{format}) ## ## Set the time vector of a series from dates. ## ## @code{@var{ts} = setabstime (@var{ts}, @var{dates})} gives the samples ## of the series @var{ts} the dates @var{dates}, one per sample, in order: ## a cell array of character vectors, a string array, a character matrix ## of one date per row, a character vector for a single sample, or a ## @code{datetime} array. The earliest date becomes ## @qcode{TimeInfo.StartDate}, stored as @qcode{'dd-mmm-yyyy HH:MM:SS'} ## with milliseconds as @qcode{'.FFF'} when the seconds are not whole, and ## @qcode{Time} holds each date's offset from it in the series' own ## @qcode{TimeInfo.Units}, which are kept: a series in seconds given one ## date a day gets times @code{[0 86400 172800 @dots{}]}. Each date is read ## by its own format. Dates given as times of day alone, such as ## @qcode{'13:00:00'}, give times since midnight and no start date, as in ## MATLAB. ## ## @code{@var{ts} = setabstime (@var{ts}, @var{dates}, @var{format})} reads ## every date strictly in the @code{datestr} format @var{format}. A date ## not written in @var{format} is refused. The start date is stored as ## above whatever the format, where MATLAB writes it in @var{format}, from ## which every later reading has to guess it again: @qcode{'dd/mm/yyyy'} ## then comes back as a date of another month. ## ## The dates need not be sorted: the samples, with their quality codes, ## are sorted with them, keeping the order of samples at equal dates, as ## the constructor sorts them. ## ## MATLAB differs in four ways. It does not move the samples with unsorted ## dates, so each is given another sample's date. It reads every date by ## the format of the first, so @code{@{'01-Jan-2024', '2024-01-02'@}} gives ## a date in year 7. It forces text into @var{format} when it does not ## fit, so @qcode{'yyyy-mm-dd'} on @qcode{'01-Jan-2024'} gives a date in ## year 6. And it takes @code{datenum} values, @code{datetime} values and ## plain numbers as relative seconds, dropping the dates; here numbers are ## refused, since a @code{datenum} cannot be told from a plain number, and ## a @code{datetime} is read as its dates. Convert a @code{datenum} ## @var{x} with @code{datetime (@var{x}, 'ConvertFrom', 'datenum')}. ## ## @seealso{timeseries.getabstime, timeseries.setuniformtime} ## @end deftypefn function this = setabstime (this, dates, format) mustBeScalar (this, 'setabstime'); scope = 'timeseries.setabstime'; if (nargin < 2) error ("%s: invalid number of input arguments.", scope); endif if (nargin < 3) format = ''; elseif (isa (format, 'string') && isscalar (format)) format = char (format); elseif (! (ischar (format) && isrow (format))) error ("%s: FORMAT must be a character vector.", scope); endif if (isnumeric (dates) || islogical (dates)) error (strcat ("%s: DATES must be dates, as text or datetime", ... " values; convert a datenum X with datetime (X,", ... " 'ConvertFrom', 'datenum')."), scope); endif [dv, errmsg] = dateVectors (dates, format); if (! isempty (errmsg)) error ("%s: DATES %s", scope, errmsg); endif if (any (dv(:,1) < 0 | dv(:,1) > 9999)) error ("%s: DATES must fall in the years 0 to 9999.", scope); endif n = numel (this.time_); if (rows (dv) != n) error ("%s: DATES must hold one date per sample.", scope); endif if (n == 0) return; endif ## Times of day alone give times since midnight and no start date isTimeOnly = false; if (! isa (dates, 'datetime')) txt = dates; if (ischar (txt) || isa (txt, 'string')) txt = cellstr (txt); endif timeOfDay = ['^\s*\d{1,2}:\d{2}(:\d{2}(\.\d+)?)?', ... '\s*([AaPp][Mm])?\s*$']; isTimeOnly = all (cellfun (@(x) ! isempty (regexp (x, timeOfDay, ... 'once')), txt(:))); endif if (isTimeOnly) ns = (dv(:,4) * 3600 + dv(:,5) * 60 + dv(:,6)) * 1e9; startDate = ''; else [~, dv0] = dateOffsets (dv); startDate = tsdata.timemetadata.dateText (dv0); ## Counted from the start date as stored, to the millisecond ns = tsdata.timemetadata.dateOffset ( ... dv, tsdata.timemetadata.dateVector (startDate)); endif time = ns / nsPerUnit (this.timeInfo_.Units); ## Sort the whole record, as the constructor does (MATLAB sorts the ## times alone and leaves each sample with another's date) [time, idx] = sort (time); this.data_ = takeSamples (this.data_, idx, this.timeDim_); if (! isempty (this.quality_)) this.quality_ = takeSamples (this.quality_, idx, this.timeDim_); endif this.time_ = time; this.timeInfo_.TimeVector = time; this.timeInfo_.StartDate = startDate; endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} setuniformtime (@var{ts}, @qcode{'StartTime'}, @var{start}) ## @deftypefnx {timeseries} {@var{ts} =} setuniformtime (@var{ts}, @qcode{'Interval'}, @var{step}) ## @deftypefnx {timeseries} {@var{ts} =} setuniformtime (@var{ts}, @qcode{'EndTime'}, @var{end}) ## @deftypefnx {timeseries} {@var{ts} =} setuniformtime (@var{ts}, @var{name1}, @var{value1}, @var{name2}, @var{value2}, @dots{}) ## ## Give a series a uniform time vector. ## ## @code{@var{ts} = setuniformtime (@var{ts}, @dots{})} replaces the time ## vector of the series @var{ts} by one of evenly spaced times, one per ## sample, in its own units; the samples are not moved. The times are ## fixed by any one or two of these options, or all three when they ## agree, the names matched in any case and a later occurrence overriding ## an earlier one: ## ## @multitable @columnfractions 0.4 0.6 ## @headitem Given @tab Times ## @item @qcode{'StartTime'} @tab from @var{start}, a step of 1 ## @item @qcode{'Interval'} @tab from 0, a step of @var{step} ## @item @qcode{'EndTime'} @tab from 0 to @var{end} ## @item @qcode{'StartTime'}, @qcode{'Interval'} @tab from @var{start}, a ## step of @var{step} ## @item @qcode{'StartTime'}, @qcode{'EndTime'} @tab from @var{start} to ## @var{end}, both exact ## @item @qcode{'Interval'}, @qcode{'EndTime'} @tab to @var{end}, a step of ## @var{step} ## @end multitable ## ## Each value is a finite real scalar in the series' units, relative to ## @qcode{TimeInfo.StartDate} when the series has one. A step of zero gives ## equal times. A negative step, a start after the end, or an end below ## zero given alone are refused, since the times would run backwards; ## MATLAB then empties the time vector and keeps the data. MATLAB also ## ignores @code{NaN}, which is refused here, and a series of one sample ## given equal start and end times, which MATLAB refuses, gets that time. ## ## @seealso{timeseries.setabstime, tsdata.timemetadata} ## @end deftypefn function this = setuniformtime (this, varargin) mustBeScalar (this, 'setuniformtime'); scope = 'timeseries.setuniformtime'; if (isempty (varargin)) error (strcat ("%s: at least one of 'StartTime', 'Interval' or", ... " 'EndTime' is required."), scope); endif if (mod (numel (varargin), 2) != 0) error ("%s: name-value arguments must be in pairs.", scope); endif optNames = {'StartTime', 'Interval', 'EndTime'}; vals = {[], [], []}; for i = 1:2:numel (varargin) k = []; if (isText (varargin{i})) k = find (strcmpi (varargin{i}, optNames)); endif if (isempty (k)) error ("%s: invalid optional paired argument.", scope); endif v = varargin{i+1}; if (! (isnumeric (v) && isreal (v) && isscalar (v))) error ("%s: '%s' must be a real scalar.", scope, optNames{k}); endif if (! isfinite (v)) error ("%s: '%s' must be finite.", scope, optNames{k}); endif vals{k} = double (v); endfor n = numel (this.time_); if (n == 0) error ("%s: TS has no samples.", scope); endif [t0, dt, t1] = vals{:}; given = ! cellfun (@isempty, vals); if (given(2) && dt < 0) error ("%s: 'Interval' must not be negative.", scope); endif if (given(1) && given(3) && t0 > t1) error ("%s: 'StartTime' must not come after 'EndTime'.", scope); endif if (isequal (given, [false, false, true]) && t1 < 0) error (strcat ("%s: 'EndTime' given alone must not be negative,", ... " since the times start at 0."), scope); endif mismatch = strcat ("%s: 'StartTime', 'Interval' and 'EndTime' do", ... " not fit the number of samples."); k = (0:n-1)'; switch (bin2dec (char (given + '0'))) case 4 # StartTime time = t0 + k; case 2 # Interval time = k * dt; case 1 # EndTime if (n == 1) error (mismatch, scope); endif time = linspace (0, t1, n)'; case 6 # StartTime and Interval time = t0 + k * dt; case 5 # StartTime and EndTime if (n == 1) if (t0 != t1) error (mismatch, scope); endif time = t0; else time = linspace (t0, t1, n)'; endif case 3 # Interval and EndTime time = t1 - flipud (k) * dt; otherwise # all three tol = 1e-12 * max ([1, abs(t0), abs(t1)]); if (abs (t0 + (n - 1) * dt - t1) > tol) error (mismatch, scope); endif if (n == 1) time = t0; else time = linspace (t0, t1, n)'; endif endswitch this.time_ = time; this.timeInfo_.TimeVector = time; endfunction endmethods ################################################################################ ## ** Events ** ## ################################################################################ ## Available Methods ## ## ## ## 'addevent' 'delevent' 'gettsbeforeevent' ## ## 'gettsbeforeatevent' 'gettsatevent' ## ## 'gettsafteratevent' 'gettsafterevent' ## ## 'gettsbetweenevents' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} addevent (@var{ts}, @var{event}) ## @deftypefnx {timeseries} {@var{ts} =} addevent (@var{ts}, @var{name}, @var{time}) ## ## Add events to a series. ## ## @code{@var{ts} = addevent (@var{ts}, @var{event})} appends the ## @code{tsdata.event} object or array @var{event} to the @qcode{Events} of ## the series @var{ts}, in the order given. ## ## @code{@var{ts} = addevent (@var{ts}, @var{name}, @var{time})} appends an ## event named @var{name} at @var{time}. @var{name} is a character vector ## or a string scalar, and @var{time} a real number in the series' units, ## counted from its @qcode{TimeInfo.StartDate} when it has one; for a ## series with a start date @var{time} may also be a date, as text or a ## @code{datetime}. For several events, @var{name} is a cell array of ## character vectors or a string array, and @var{time} a cell array, a ## numeric vector or a string array of as many times. ## ## Events are kept in the order they are added, not sorted; an event ## whose name and time match one already held is not added again, while ## the same name at another time is. ## ## A date on a series with no start date is refused, since the series has ## no calendar to place it on; MATLAB accepts it and reads the event as ## time 0. A @code{NaN} time is refused too. ## ## @seealso{timeseries.delevent, tsdata.event, timeseries.gettsatevent} ## @end deftypefn function this = addevent (this, varargin) mustBeScalar (this, 'addevent'); scope = 'timeseries.addevent'; if (numel (varargin) == 1) events = varargin{1}; if (! isa (events, 'tsdata.event')) error ("%s: EVENT must be a tsdata.event object.", scope); endif events = events(:)'; elseif (numel (varargin) == 2) [names, times] = varargin{:}; if (isText (names)) names = {char(names)}; times = {times}; elseif (iscellstr (names) || isa (names, 'string')) names = cellstr (names); if (isnumeric (times) || isa (times, 'string')) times = num2cell (times); if (isa (varargin{2}, 'string')) times = cellfun (@char, times, 'UniformOutput', false); endif elseif (! iscell (times)) times = {times}; endif if (numel (times) != numel (names)) error ("%s: NAME and TIME must hold as many events.", scope); endif else error (strcat ("%s: NAME must be a character vector, a string or", ... " a cell array of character vectors."), scope); endif evs = cell (1, numel (names)); startDate = this.timeInfo_.StartDate; for i = 1:numel (names) t = times{i}; isDate = ischar (t) || isa (t, 'string') || isa (t, 'datetime'); if (isDate) if (isempty (startDate)) error (strcat ("%s: TS has no start date to place a dated", ... " event on; set 'TimeInfo.StartDate' or use", ... " setabstime first."), scope); endif try e = tsdata.event (names{i}, t); catch err error ("%s: TIME is not a date.", scope); end_try_catch else if (! (isnumeric (t) && isreal (t) && isscalar (t) && ! isnan (t))) error ("%s: TIME must be a real number or a date.", scope); endif e = tsdata.event (names{i}, double (t)); e.Units = this.timeInfo_.Units; e.StartDate = startDate; endif evs{i} = e; endfor events = [evs{:}]; else error ("%s: invalid number of input arguments.", scope); endif ## 'arrayfun' would call its function once with the whole array if (isempty (this.timeInfo_.StartDate)) for i = 1:numel (events) if (! isempty (events(i).StartDate)) error (strcat ("%s: TS has no start date to place a dated", ... " event on; set 'TimeInfo.StartDate' or use", ... " setabstime first."), scope); endif endfor endif held = this.events_; for i = 1:numel (events) isHeld = false; for j = 1:numel (held) isHeld = isHeld || sameEvent (held(j), events(i)); endfor if (isHeld) continue; endif if (isempty (held)) held = events(i); else ## Appended along the list as it lies, a row or a column held(end+1) = events(i); endif endfor this.events_ = held; endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} delevent (@var{ts}, @var{name}) ## @deftypefnx {timeseries} {@var{ts} =} delevent (@var{ts}, @var{name}, @var{n}) ## ## Remove events from a series. ## ## @code{@var{ts} = delevent (@var{ts}, @var{name})} removes the first event ## named @var{name}, a character vector or a string scalar matched in case, ## from the @qcode{Events} of the series @var{ts}; for a cell array of ## names, the first event of each. ## ## @code{@var{ts} = delevent (@var{ts}, @var{name}, @var{n})} removes the ## @var{n}th event of each name instead, counting only the events of that ## name in the order they are held. ## ## A name no event has, or an @var{n} beyond the events of that name, is ## refused; MATLAB then removes nothing, silently. ## ## @seealso{timeseries.addevent} ## @end deftypefn function this = delevent (this, varargin) mustBeScalar (this, 'delevent'); scope = 'timeseries.delevent'; if (numel (varargin) < 1 || numel (varargin) > 2) error ("%s: invalid number of input arguments.", scope); endif names = varargin{1}; if (isText (names)) names = {char(names)}; elseif (! (iscellstr (names) || isa (names, 'string'))) error (strcat ("%s: NAME must be a character vector, a string or a", ... " cell array of character vectors."), scope); endif names = cellstr (names); n = 1; if (numel (varargin) == 2) n = varargin{2}; endif drop = zeros (1, numel (names)); for i = 1:numel (names) [drop(i), errmsg] = findEvent (this.events_, names{i}, n); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif endfor keep = true (1, numel (this.events_)); keep(drop) = false; if (any (keep)) this.events_ = this.events_(keep); else this.events_ = []; endif endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts2} =} gettsbeforeevent (@var{ts}, @var{event}) ## @deftypefnx {timeseries} {@var{ts2} =} gettsbeforeevent (@var{ts}, @var{event}, @var{n}) ## ## Return a series of the samples before the event. ## ## @code{@var{ts2} = gettsbeforeevent (@var{ts}, @var{event})} returns the ## series @var{ts} holding only the samples whose time is strictly before ## the time of @var{event}, compared exactly. @var{event} is the name of an ## event of @var{ts}, a character vector or a string scalar matched in case, ## which selects the first event of that name; or a @code{tsdata.event} ## object, which need not belong to @var{ts}. ## ## @code{@var{ts2} = gettsbeforeevent (@var{ts}, @var{event}, @var{n})} uses ## the @var{n}th event named @var{event}, counting only the events of that ## name in the order they are held. For a @code{tsdata.event} object ## @var{n} can only be 1. A name no event has, or an @var{n} beyond the ## events of that name, is refused; MATLAB then returns an empty series. So ## is a dated event on a series with no start date, which MATLAB reads as ## time 0. ## ## @seealso{timeseries.gettsbeforeatevent, timeseries.gettsatevent, ## timeseries.gettsafteratevent, timeseries.gettsafterevent, ## timeseries.gettsbetweenevents, timeseries.addevent} ## @end deftypefn function this = gettsbeforeevent (this, varargin) mustBeScalar (this, 'gettsbeforeevent'); t = eventQueryTime (this, 'gettsbeforeevent', varargin{:}); this = subset (this, find (this.time_ < t)); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts2} =} gettsbeforeatevent (@var{ts}, @var{event}) ## @deftypefnx {timeseries} {@var{ts2} =} gettsbeforeatevent (@var{ts}, @var{event}, @var{n}) ## ## Return a series of the samples before and at the event. ## ## @code{@var{ts2} = gettsbeforeatevent (@var{ts}, @var{event})} returns the ## series @var{ts} holding only the samples whose time is before or at the ## time of @var{event}, compared exactly. @var{event} is the name of an ## event of @var{ts}, a character vector or a string scalar matched in case, ## which selects the first event of that name; or a @code{tsdata.event} ## object, which need not belong to @var{ts}. ## ## @code{@var{ts2} = gettsbeforeatevent (@var{ts}, @var{event}, @var{n})} ## uses the @var{n}th event named @var{event}, counting only the events of ## that name in the order they are held. For a @code{tsdata.event} object ## @var{n} can only be 1. A name no event has, or an @var{n} beyond the ## events of that name, is refused; MATLAB then returns an empty series. So ## is a dated event on a series with no start date, which MATLAB reads as ## time 0. ## ## @seealso{timeseries.gettsbeforeevent, timeseries.gettsatevent, ## timeseries.gettsafteratevent, timeseries.gettsafterevent, ## timeseries.gettsbetweenevents, timeseries.addevent} ## @end deftypefn function this = gettsbeforeatevent (this, varargin) mustBeScalar (this, 'gettsbeforeatevent'); t = eventQueryTime (this, 'gettsbeforeatevent', varargin{:}); this = subset (this, find (this.time_ <= t)); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts2} =} gettsatevent (@var{ts}, @var{event}) ## @deftypefnx {timeseries} {@var{ts2} =} gettsatevent (@var{ts}, @var{event}, @var{n}) ## ## Return a series of the samples at the event. ## ## @code{@var{ts2} = gettsatevent (@var{ts}, @var{event})} returns the ## series @var{ts} holding only the samples whose time is at the time of ## @var{event}, compared exactly. @var{event} is the name of an event of ## @var{ts}, a character vector or a string scalar matched in case, which ## selects the first event of that name; or a @code{tsdata.event} object, ## which need not belong to @var{ts}. ## ## @code{@var{ts2} = gettsatevent (@var{ts}, @var{event}, @var{n})} uses the ## @var{n}th event named @var{event}, counting only the events of that name ## in the order they are held. For a @code{tsdata.event} object @var{n} can ## only be 1. A name no event has, or an @var{n} beyond the events of that ## name, is refused; MATLAB then returns an empty series. So is a dated ## event on a series with no start date, which MATLAB reads as time 0. ## ## @seealso{timeseries.gettsbeforeevent, timeseries.gettsbeforeatevent, ## timeseries.gettsafteratevent, timeseries.gettsafterevent, ## timeseries.gettsbetweenevents, timeseries.addevent} ## @end deftypefn function this = gettsatevent (this, varargin) mustBeScalar (this, 'gettsatevent'); t = eventQueryTime (this, 'gettsatevent', varargin{:}); this = subset (this, find (this.time_ == t)); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts2} =} gettsafteratevent (@var{ts}, @var{event}) ## @deftypefnx {timeseries} {@var{ts2} =} gettsafteratevent (@var{ts}, @var{event}, @var{n}) ## ## Return a series of the samples at and after the event. ## ## @code{@var{ts2} = gettsafteratevent (@var{ts}, @var{event})} returns the ## series @var{ts} holding only the samples whose time is at or after the ## time of @var{event}, compared exactly. @var{event} is the name of an ## event of @var{ts}, a character vector or a string scalar matched in case, ## which selects the first event of that name; or a @code{tsdata.event} ## object, which need not belong to @var{ts}. ## ## @code{@var{ts2} = gettsafteratevent (@var{ts}, @var{event}, @var{n})} ## uses the @var{n}th event named @var{event}, counting only the events of ## that name in the order they are held. For a @code{tsdata.event} object ## @var{n} can only be 1. A name no event has, or an @var{n} beyond the ## events of that name, is refused; MATLAB then returns an empty series. So ## is a dated event on a series with no start date, which MATLAB reads as ## time 0. ## ## @seealso{timeseries.gettsbeforeevent, timeseries.gettsbeforeatevent, ## timeseries.gettsatevent, timeseries.gettsafterevent, ## timeseries.gettsbetweenevents, timeseries.addevent} ## @end deftypefn function this = gettsafteratevent (this, varargin) mustBeScalar (this, 'gettsafteratevent'); t = eventQueryTime (this, 'gettsafteratevent', varargin{:}); this = subset (this, find (this.time_ >= t)); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts2} =} gettsafterevent (@var{ts}, @var{event}) ## @deftypefnx {timeseries} {@var{ts2} =} gettsafterevent (@var{ts}, @var{event}, @var{n}) ## ## Return a series of the samples after the event. ## ## @code{@var{ts2} = gettsafterevent (@var{ts}, @var{event})} returns the ## series @var{ts} holding only the samples whose time is strictly after the ## time of @var{event}, compared exactly. @var{event} is the name of an ## event of @var{ts}, a character vector or a string scalar matched in case, ## which selects the first event of that name; or a @code{tsdata.event} ## object, which need not belong to @var{ts}. ## ## @code{@var{ts2} = gettsafterevent (@var{ts}, @var{event}, @var{n})} uses ## the @var{n}th event named @var{event}, counting only the events of that ## name in the order they are held. For a @code{tsdata.event} object ## @var{n} can only be 1. A name no event has, or an @var{n} beyond the ## events of that name, is refused; MATLAB then returns an empty series. So ## is a dated event on a series with no start date, which MATLAB reads as ## time 0. ## ## @seealso{timeseries.gettsbeforeevent, timeseries.gettsbeforeatevent, ## timeseries.gettsatevent, timeseries.gettsafteratevent, ## timeseries.gettsbetweenevents, timeseries.addevent} ## @end deftypefn function this = gettsafterevent (this, varargin) mustBeScalar (this, 'gettsafterevent'); t = eventQueryTime (this, 'gettsafterevent', varargin{:}); this = subset (this, find (this.time_ > t)); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts2} =} gettsbetweenevents (@var{ts}, @var{event1}, @var{event2}) ## @deftypefnx {timeseries} {@var{ts2} =} gettsbetweenevents (@var{ts}, @var{event1}, @var{event2}, @var{n1}, @var{n2}) ## ## Return a series of the samples between two events. ## ## @code{@var{ts2} = gettsbetweenevents (@var{ts}, @var{event1}, ## @var{event2})} returns the series @var{ts} holding only the samples from ## the time of @var{event1} to the time of @var{event2}, both included; none ## when @var{event2} comes before @var{event1}. Each event is a name or a ## @code{tsdata.event} object, as for @code{gettsafterevent}. ## ## @code{@var{ts2} = gettsbetweenevents (@var{ts}, @var{event1}, ## @var{event2}, @var{n1}, @var{n2})} uses the @var{n1}th event named ## @var{event1} and the @var{n2}th named @var{event2}; for an event given ## as a @code{tsdata.event} object its number can only be 1. Names and ## numbers no event has are refused, as for @code{gettsafterevent}. ## ## @seealso{timeseries.gettsafterevent, timeseries.gettsbeforeevent, ## timeseries.addevent} ## @end deftypefn function this = gettsbetweenevents (this, varargin) mustBeScalar (this, 'gettsbetweenevents'); scope = 'gettsbetweenevents'; if (numel (varargin) == 2) t1 = eventQueryTime (this, scope, varargin{1}); t2 = eventQueryTime (this, scope, varargin{2}); elseif (numel (varargin) == 4) t1 = eventQueryTime (this, scope, varargin{1}, varargin{3}); t2 = eventQueryTime (this, scope, varargin{2}, varargin{4}); else error ("timeseries.%s: invalid number of input arguments.", scope); endif this = subset (this, find (this.time_ >= t1 & this.time_ <= t2)); endfunction endmethods ################################################################################ ## ** Interpolation, resampling, synchronising ** ## ################################################################################ ## Available Methods ## ## ## ## 'getinterpmethod' 'setinterpmethod' 'resample' 'synchronize' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{name} =} getinterpmethod (@var{ts}) ## ## Return the name of the interpolation method. ## ## @code{@var{name} = getinterpmethod (@var{ts})} returns ## @qcode{'linear'}, @qcode{'zoh'} or @qcode{'myFuncHandle'}, the ## @qcode{Name} of @qcode{@var{ts}.DataInfo.Interpolation}. ## ## @seealso{timeseries.setinterpmethod, tsdata.interpolation} ## @end deftypefn function name = getinterpmethod (this) mustBeScalar (this, 'getinterpmethod'); name = this.dataInfo_.Interpolation.Name; endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} setinterpmethod (@var{ts}, @var{name}) ## @deftypefnx {timeseries} {@var{ts} =} setinterpmethod (@var{ts}, @var{fh}) ## @deftypefnx {timeseries} {@var{ts} =} setinterpmethod (@var{ts}, @var{ip}) ## ## Set the interpolation method. ## ## @code{@var{ts} = setinterpmethod (@var{ts}, @var{name})} sets the method ## @code{resample} and @code{synchronize} use to @var{name}, ## @qcode{'linear'} or @qcode{'zoh'} (zero-order hold, keeping each value ## until the next sample), matched in any case and stored in lower case. ## MATLAB stores any name, @qcode{'cubic'} included, and fails only on ## resampling; here another name is refused. ## ## @code{@var{ts} = setinterpmethod (@var{ts}, @var{fh})} sets a method ## evaluated by the function handle @var{fh}, called as MATLAB calls it: ## @code{@var{newData} = @var{fh} (@var{newTime}, @var{oldTime}, ## @var{oldData})}, with the times as columns and the data with time along ## its first dimension, so three-dimensional data is passed permuted. ## ## @code{@var{ts} = setinterpmethod (@var{ts}, @var{ip})} sets the method ## held by the @code{tsdata.interpolation} object @var{ip}. ## ## @seealso{timeseries.getinterpmethod, timeseries.resample, ## tsdata.interpolation} ## @end deftypefn function this = setinterpmethod (this, method) mustBeScalar (this, 'setinterpmethod'); scope = 'timeseries.setinterpmethod'; if (nargin < 2) error ("%s: invalid number of input arguments.", scope); endif if (isText (method)) method = lower (char (method)); if (! any (strcmp (method, {'linear', 'zoh'}))) error ("%s: METHOD must be 'linear' or 'zoh': '%s'", scope, method); endif method = tsdata.interpolation (method); elseif (isa (method, 'function_handle') && isscalar (method)) method = tsdata.interpolation (method); elseif (! (isa (method, 'tsdata.interpolation') && isscalar (method))) error (strcat ("%s: METHOD must be 'linear', 'zoh', a function", ... " handle or a tsdata.interpolation object."), scope); endif this.dataInfo_.Interpolation = method; endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} resample (@var{ts}, @var{time}) ## @deftypefnx {timeseries} {@var{ts} =} resample (@var{ts}, @var{time}, @var{method}) ## @deftypefnx {timeseries} {@var{ts} =} resample (@var{ts}, @var{time}, @var{method}, @var{code}) ## ## Evaluate a series at new times. ## ## @code{@var{ts} = resample (@var{ts}, @var{time})} returns the series ## @var{ts} with its samples replaced by its values at @var{time}, found ## with its own interpolation method. @var{time} is numeric, in the ## series' units; for a series with a @qcode{TimeInfo.StartDate} it may ## also be dates, as text or a @code{datetime}, and numbers count from the ## start date. The new times are sorted, repeats kept. Samples that are ## @code{NaN} are left out before interpolating, as in MATLAB; at a time ## outside the series the data is @code{NaN}. The class of the data is ## kept. Every property but the samples is kept. ## ## @code{@var{ts} = resample (@var{ts}, @var{time}, @var{method})} uses ## @var{method}, @qcode{'linear'} or @qcode{'zoh'} in any case, instead; ## @code{[]} keeps the series' own. ## ## @code{@var{ts} = resample (@var{ts}, @var{time}, @var{method}, ## @var{code})} gives the quality code @var{code}, an integer listed in ## @qcode{QualityInfo.Code}, to every new time the series did not already ## hold. @code{[]} gives none. ## ## A series with quality codes gives each other new time the code of the ## nearest sample, a tie going to the later, as MATLAB does; under ## @qcode{'zoh'} a value takes the code of the sample it is held from, ## where MATLAB takes the nearest one's, which may be the next sample's. ## Outside the series a code is the nearest sample's too, where MATLAB ## raises for a series with codes. Custom methods are called on the data ## only. ## ## @seealso{timeseries.synchronize, timeseries.setinterpmethod} ## @end deftypefn function this = resample (this, varargin) mustBeScalar (this, 'resample'); scope = 'timeseries.resample'; if (numel (varargin) < 1 || numel (varargin) > 3) error ("%s: invalid number of input arguments.", scope); endif method = ''; code = []; if (numel (varargin) >= 2) method = varargin{2}; endif if (numel (varargin) == 3) code = varargin{3}; endif if (isempty (this.time_) && isempty (this.data_)) return; endif [t, ~, errmsg] = relativeTime (this, varargin{1}, false); if (! isempty (errmsg)) error ("%s: TIME %s", scope, errmsg); endif if (! all (isfinite (t))) error ("%s: TIME must be finite.", scope); endif [method, errmsg] = interpName (method); if (! isempty (errmsg)) error ("%s: METHOD %s", scope, errmsg); endif [this, errmsg] = resampleAt (this, sort (t), method, code); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {[@var{ts1}, @var{ts2}] =} synchronize (@var{ts1}, @var{ts2}, @var{method}) ## @deftypefnx {timeseries} {[@var{ts1}, @var{ts2}] =} synchronize (@dots{}, @var{name}, @var{value}) ## ## Resample two series onto common times. ## ## @code{[@var{ts1}, @var{ts2}] = synchronize (@var{ts1}, @var{ts2}, ## @var{method})} resamples both series onto one set of times within the ## span both cover, chosen by @var{method}, matched in any case: ## ## @table @asis ## @item @qcode{'union'} ## every time of either series in that span; ## @item @qcode{'intersection'} ## the times both series hold, within the tolerance; ## @item @qcode{'uniform'} ## times from the start of the span, a step of @qcode{'Interval'} apart. ## @end table ## ## Series that do not overlap become series with no samples. Each keeps its ## own units, the times converted exactly; MATLAB converts them with errors ## near the tenth digit. Dated series are placed by their start dates, and ## both results count from the earlier of the two unless ## @qcode{'KeepOriginalTimes'} is @code{true}. A dated series with an ## undated one is refused, since the undated one has no calendar; MATLAB ## then reads both as undated. ## ## The options, matched in any case: ## ## @table @asis ## @item @qcode{'Interval'} ## the step of @qcode{'uniform'}, a positive number in the first series' ## units, 1 by default; MATLAB accepts zero and negative steps and gives one ## sample or none. ## @item @qcode{'InterpMethod'} ## @qcode{'linear'} or @qcode{'zoh'}, for both series; by default each uses ## its own. ## @item @qcode{'QualityCode'} ## a quality code given to the times each series did not already hold, as ## for @code{resample}. ## @item @qcode{'KeepOriginalTimes'} ## @code{true} to keep each dated series on its own start date. ## @item @qcode{'tolerance'} ## how close two times must be to count as one, in the first series' ## units, @code{1e-10} by default; the second series' time is kept. This ## is the documented meaning; MATLAB ignores any tolerance above about ## @code{1e-10}, so there @code{0.1} matches no times @code{0.05} apart, ## while here it matches them. ## @end table ## ## Quality codes follow the rules of @code{resample}. ## ## @seealso{timeseries.resample, timeseries.append} ## @end deftypefn function [ts1, ts2] = synchronize (ts1, ts2, varargin) scope = 'timeseries.synchronize'; if (nargin < 3) error ("%s: invalid number of input arguments.", scope); endif if (! isa (ts1, 'timeseries')) error ("%s: TS1 must be a timeseries.", scope); endif if (! isa (ts2, 'timeseries')) error ("%s: TS2 must be a timeseries.", scope); endif mustBeScalar (ts1, 'synchronize'); mustBeScalar (ts2, 'synchronize'); how = varargin{1}; methods = {'union', 'intersection', 'uniform'}; if (! (isText (how) && any (strcmpi (how, methods)))) error (strcat ("%s: METHOD must be 'union', 'intersection' or", ... " 'uniform'."), scope); endif how = lower (char (how)); opts = varargin(2:end); if (mod (numel (opts), 2) != 0) error ("%s: name-value arguments must be in pairs.", scope); endif optNames = {'Interval', 'InterpMethod', 'QualityCode', ... 'KeepOriginalTimes', 'tolerance'}; vals = {1, '', [], false, 1e-10}; for i = 1:2:numel (opts) k = []; if (isText (opts{i})) k = find (strcmpi (opts{i}, optNames)); endif if (isempty (k)) error ("%s: invalid optional paired argument.", scope); endif vals{k} = opts{i+1}; endfor [interval, method, code, keep, tol] = vals{:}; if (! (isnumeric (interval) && isreal (interval) && isscalar (interval) && isfinite (interval) && interval > 0)) error ("%s: 'Interval' must be a positive number.", scope); endif [method, errmsg] = interpName (method); if (! isempty (errmsg)) error ("%s: 'InterpMethod' %s", scope, errmsg); endif if (! ((islogical (keep) || isnumeric (keep)) && isscalar (keep) && any (keep == [0, 1]))) error ("%s: 'KeepOriginalTimes' must be a logical scalar.", scope); endif if (! (isnumeric (tol) && isreal (tol) && isscalar (tol) && tol >= 0)) error ("%s: 'tolerance' must be a non-negative number.", scope); endif if (isempty (ts1.time_) || isempty (ts2.time_)) error ("%s: TS1 and TS2 must hold samples.", scope); endif sd1 = ts1.timeInfo_.StartDate; sd2 = ts2.timeInfo_.StartDate; if (isempty (sd1) != isempty (sd2)) error (strcat ("%s: one series has a start date and the other has", ... " none; give the other one with setabstime or", ... " 'TimeInfo.StartDate'."), scope); endif ## Both on one scale: nanoseconds from the earlier start date ns1 = nsPerUnit (ts1.timeInfo_.Units); ns2 = nsPerUnit (ts2.timeInfo_.Units); off1 = 0; off2 = 0; refDate = ''; if (! isempty (sd1)) [off, ~] = dateOffsets ([tsdata.timemetadata.dateVector(sd1); tsdata.timemetadata.dateVector(sd2)]); off1 = off(1); off2 = off(2); if (off1 == 0) refDate = sd1; else refDate = sd2; endif endif t1 = ts1.time_ * ns1 + off1; t2 = ts2.time_ * ns2 + off2; tolNs = tol * ns1; lo = max (t1(1), t2(1)); hi = min (t1(end), t2(end)); if (lo > hi + tolNs) grid = zeros (0, 1); else in1 = t1(t1 >= lo - tolNs & t1 <= hi + tolNs); in2 = t2(t2 >= lo - tolNs & t2 <= hi + tolNs); switch (how) case 'union' grid = in2; for i = 1:numel (in1) if (! any (abs (in2 - in1(i)) <= tolNs)) grid(end+1,1) = in1(i); endif endfor case 'intersection' grid = zeros (0, 1); for i = 1:numel (in1) k = find (abs (in2 - in1(i)) <= tolNs, 1); if (! isempty (k)) grid(end+1,1) = in2(k); endif endfor otherwise step = interval * ns1; grid = lo + (0:floor ((hi - lo) / step + 1e-9))' * step; endswitch grid = unique (grid); endif ## Series that do not overlap become series with no samples if (isempty (grid)) ts1 = subset (ts1, []); ts2 = subset (ts2, []); if (! isempty (refDate) && ! keep) ts1.timeInfo_.StartDate = refDate; ts2.timeInfo_.StartDate = refDate; endif return; endif ## Each series evaluated in its own frame: at its own time where the ## grid holds that time, or matches it within the tolerance. The ## results are labelled with the grid times, counted from the earlier ## start date unless each keeps its own. label1 = off1; label2 = off2; if (! isempty (refDate) && ! keep) label1 = 0; label2 = 0; endif [at1, lab1] = frameTimes (grid, t1, ts1.time_, off1, ns1, tolNs, label1); [at2, lab2] = frameTimes (grid, t2, ts2.time_, off2, ns2, tolNs, label2); [ts1, errmsg] = resampleAt (ts1, at1, method, code, true); if (! isempty (errmsg)) error ("%s: %s", scope, regexprep (errmsg, '^TS ', 'TS1 ')); endif [ts2, errmsg] = resampleAt (ts2, at2, method, code, true); if (! isempty (errmsg)) error ("%s: %s", scope, regexprep (errmsg, '^TS ', 'TS2 ')); endif ts1.time_ = lab1; ts2.time_ = lab2; ts1.timeInfo_.TimeVector = lab1; ts2.timeInfo_.TimeVector = lab2; if (! isempty (refDate) && ! keep) ts1.timeInfo_.StartDate = refDate; ts2.timeInfo_.StartDate = refDate; endif endfunction endmethods ################################################################################ ## ** Statistics ** ## ################################################################################ ## Available Methods ## ## ## ## 'mean' 'median' 'mode' 'std' ## ## 'var' 'sum' 'min' 'max' ## ## 'iqr' 'getqualitydesc' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{v} =} mean (@var{ts}) ## @deftypefnx {timeseries} {@var{v} =} mean (@var{ts}, @var{name}, @var{value}, @dots{}) ## ## Return the mean of a series over time. ## ## @code{@var{v} = mean (@var{ts})} returns the mean of each data element ## over time, so @var{v} has the size of one sample; with @qcode{'time'} ## weighting it is the time-weighted mean. @var{v} is @code{double} ## whatever the class of the data, and @code{[]} for a series with no ## samples. ## ## @code{@var{v} = mean (@var{ts}, @var{name}, @var{value}, @dots{})} takes ## these options, which all nine statistics share, the names matched in any ## case: ## ## @table @asis ## @item @qcode{'Quality'} ## the quality codes of samples to leave out, as missing; a value left ## without samples is @code{NaN}, a sum 0. A code no sample has leaves ## nothing out, with a warning. ## @item @qcode{'MissingData'} ## @qcode{'remove'}, the default, leaves missing values out; ## @qcode{'interpolate'} fills those between samples linearly in time ## first. ## @item @qcode{'Weighting'} ## @qcode{'none'}, the default, or @qcode{'time'}, which weights each sample ## by the time it covers: an interior sample by the mean of its two ## intervals, an end sample by its one interval, the weights scaled to a ## mean of 1, so that uniform time changes nothing. ## @end table ## ## @code{NaN} is missing while @qcode{TreatNaNasMissing} is @code{true}; ## when it is @code{false}, @code{mean}, @code{median}, @code{std}, ## @code{var} and @code{sum} give @code{NaN} where the data holds one, ## while @code{min}, @code{max}, @code{mode} and @code{iqr} still skip it, ## as in MATLAB. ## ## MATLAB applies @qcode{'time'} weighting by multiplying the data by the ## weights and taking the ordinary statistic of the product, which is ## right for @code{mean} and @code{sum} only: there the minimum of ## @code{[1 2 4 8 16]} at times @code{[0 1 3 7 8]} is 0.5556. Here each ## statistic is the time-weighted one: 1 for that minimum. MATLAB also ## fails on logical data, and in @code{std}, @code{var} and @code{iqr} on ## integer data, which here are computed in @code{double}. ## ## @seealso{timeseries.median, timeseries.std, timeseries.sum} ## @end deftypefn function v = mean (this, varargin) v = statistic (this, 'mean', varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{v} =} median (@var{ts}) ## @deftypefnx {timeseries} {@var{v} =} median (@var{ts}, @var{name}, @var{value}, @dots{}) ## ## Return the median of a series over time. ## ## @code{@var{v} = median (@var{ts})} returns the median of each data ## element over time, so @var{v} has the size of one sample; with ## @qcode{'time'} weighting it is the time-weighted median. @var{v} keeps ## the class of the data, and is @code{[]} for a series with no samples. ## Where a value is left without samples it is @code{NaN}, and @var{v} is ## then @code{double}, as integer and logical classes hold no @code{NaN}. ## ## The options, and where MATLAB differs, are given under ## @code{timeseries.mean}. ## ## @seealso{timeseries.mean, timeseries.mode, timeseries.iqr} ## @end deftypefn function v = median (this, varargin) v = statistic (this, 'median', varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{v} =} mode (@var{ts}) ## @deftypefnx {timeseries} {@var{v} =} mode (@var{ts}, @var{name}, @var{value}, @dots{}) ## ## Return the mode of a series over time. ## ## @code{@var{v} = mode (@var{ts})} returns the most frequent value of each ## data element over time, the smallest on a tie, so @var{v} has the size ## of one sample; with @qcode{'time'} weighting it is the value of most ## total weight. @var{v} keeps the class of the data, and is @code{[]} for ## a series with no samples. Where a value is left without samples it is ## @code{NaN}, and @var{v} is then @code{double}, as integer and logical ## classes hold no @code{NaN}. ## ## The options, and where MATLAB differs, are given under ## @code{timeseries.mean}. ## ## @seealso{timeseries.mean, timeseries.median} ## @end deftypefn function v = mode (this, varargin) v = statistic (this, 'mode', varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{v} =} std (@var{ts}) ## @deftypefnx {timeseries} {@var{v} =} std (@var{ts}, @var{name}, @var{value}, @dots{}) ## ## Return the standard deviation of a series over time. ## ## @code{@var{v} = std (@var{ts})} returns the standard deviation of each ## data element over time, normalised by @math{N-1}, so @var{v} has the ## size of one sample; with @qcode{'time'} weighting it is taken about the ## time-weighted mean. @var{v} is @code{double} whatever the class of the ## data, and @code{[]} for a series with no samples. ## ## The options, and where MATLAB differs, are given under ## @code{timeseries.mean}. ## ## @seealso{timeseries.mean, timeseries.var} ## @end deftypefn function v = std (this, varargin) v = statistic (this, 'std', varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{v} =} var (@var{ts}) ## @deftypefnx {timeseries} {@var{v} =} var (@var{ts}, @var{name}, @var{value}, @dots{}) ## ## Return the variance of a series over time. ## ## @code{@var{v} = var (@var{ts})} returns the variance of each data ## element over time, normalised by @math{N-1}, so @var{v} has the size of ## one sample; with @qcode{'time'} weighting it is taken about the ## time-weighted mean. @var{v} is @code{double} whatever the class of the ## data, and @code{[]} for a series with no samples. ## ## The options, and where MATLAB differs, are given under ## @code{timeseries.mean}. ## ## @seealso{timeseries.mean, timeseries.std} ## @end deftypefn function v = var (this, varargin) v = statistic (this, 'var', varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{v} =} sum (@var{ts}) ## @deftypefnx {timeseries} {@var{v} =} sum (@var{ts}, @var{name}, @var{value}, @dots{}) ## ## Return the sum of a series over time. ## ## @code{@var{v} = sum (@var{ts})} returns the sum of each data element ## over time, so @var{v} has the size of one sample; with @qcode{'time'} ## weighting it is the sum of the weighted samples. @var{v} is ## @code{double} whatever the class of the data, and @code{[]} for a series ## with no samples. ## ## The options, and where MATLAB differs, are given under ## @code{timeseries.mean}. ## ## @seealso{timeseries.mean} ## @end deftypefn function v = sum (this, varargin) v = statistic (this, 'sum', varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{v} =} min (@var{ts}) ## @deftypefnx {timeseries} {@var{v} =} min (@var{ts}, @var{name}, @var{value}, @dots{}) ## ## Return the minimum of a series over time. ## ## @code{@var{v} = min (@var{ts})} returns the smallest value of each data ## element over time, so @var{v} has the size of one sample; weighting ## leaves it unchanged. @var{v} keeps the class of the data, and is ## @code{[]} for a series with no samples. Where a value is left without ## samples it is @code{NaN}, and @var{v} is then @code{double}, as integer ## and logical classes hold no @code{NaN}. ## ## The options, and where MATLAB differs, are given under ## @code{timeseries.mean}. ## ## @seealso{timeseries.max, timeseries.mean} ## @end deftypefn function v = min (this, varargin) v = statistic (this, 'min', varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{v} =} max (@var{ts}) ## @deftypefnx {timeseries} {@var{v} =} max (@var{ts}, @var{name}, @var{value}, @dots{}) ## ## Return the maximum of a series over time. ## ## @code{@var{v} = max (@var{ts})} returns the largest value of each data ## element over time, so @var{v} has the size of one sample; weighting ## leaves it unchanged. @var{v} keeps the class of the data, and is ## @code{[]} for a series with no samples. Where a value is left without ## samples it is @code{NaN}, and @var{v} is then @code{double}, as integer ## and logical classes hold no @code{NaN}. ## ## The options, and where MATLAB differs, are given under ## @code{timeseries.mean}. ## ## @seealso{timeseries.min, timeseries.mean} ## @end deftypefn function v = max (this, varargin) v = statistic (this, 'max', varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{v} =} iqr (@var{ts}) ## @deftypefnx {timeseries} {@var{v} =} iqr (@var{ts}, @var{name}, @var{value}, @dots{}) ## ## Return the interquartile range of a series over time. ## ## @code{@var{v} = iqr (@var{ts})} returns the interquartile range of each ## data element over time, the quartiles placed by the @code{(k - 0.5)/n} ## rule, so @var{v} has the size of one sample; with @qcode{'time'} ## weighting the cumulative weight takes the place of @var{k}. @var{v} is ## @code{double} whatever the class of the data, and @code{[]} for a series ## with no samples. ## ## The options, and where MATLAB differs, are given under ## @code{timeseries.mean}. ## ## @seealso{timeseries.median, timeseries.mean} ## @end deftypefn function v = iqr (this, varargin) v = statistic (this, 'iqr', varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{desc} =} getqualitydesc (@var{ts}) ## ## Return the description of each quality code. ## ## @code{@var{desc} = getqualitydesc (@var{ts})} returns a cell array the ## size of @qcode{@var{ts}.Quality} holding, for each code, its description ## in @qcode{@var{ts}.QualityInfo}: the element of @qcode{Description} at ## the position of the code in @qcode{Code}. A series without quality ## codes gives @code{@{@}}. A code with no description is refused. ## ## @seealso{tsdata.qualmetadata} ## @end deftypefn function desc = getqualitydesc (this) mustBeScalar (this, 'getqualitydesc'); q = this.quality_; if (isempty (q)) desc = {}; return; endif codes = this.qualityInfo_.Code; text = this.qualityInfo_.Description; desc = cell (size (q)); for i = 1:numel (q) k = find (codes == q(i), 1); if (isempty (k) || ! iscell (text) || k > numel (text)) error (strcat ("timeseries.getqualitydesc: quality code %d", ... " has no description in 'QualityInfo'."), q(i)); endif desc{i} = text{k}; endfor endfunction endmethods ################################################################################ ## ** Arithmetic and comparison ** ## ################################################################################ ## Available Methods ## ## ## ## 'plus' 'minus' 'times' 'mtimes' ## ## 'rdivide' 'ldivide' 'mrdivide' 'mldivide' ## ## 'eq' 'isequal' 'isequalwithequalnans' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} plus (@var{a}, @var{b}) ## ## The sum of series, @code{@var{a} + @var{b}}. ## ## @code{@var{ts} = plus (@var{a}, @var{b})} returns the sum of @var{a} and ## @var{b}, sample by sample. One of them may be a number: a scalar, or an ## array the size of one sample. ## ## Two series must hold as many samples, of sizes the operator accepts, ## at times in the same units. Two dated series must be at the same ## instants; two undated ones may also differ by a constant offset, and ## the result then takes the times of @var{a}, with a warning. A dated ## series and an undated one are refused. The result is named ## @qcode{'unnamed'} and takes its other properties from @var{a}, with the ## events of both, no quality codes and linear interpolation; ## @qcode{DataInfo.Units} is kept where both series have the same. With a ## number, every property of the series is kept. The data is ## @code{double} whatever its class. ## ## @seealso{timeseries.minus, timeseries.times, timeseries.mtimes} ## @end deftypefn function ts = plus (a, b) ts = arithmetic (a, b, 'plus'); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} minus (@var{a}, @var{b}) ## ## The difference of series, @code{@var{a} - @var{b}}. ## ## @code{@var{ts} = minus (@var{a}, @var{b})} returns the difference of ## @var{a} and @var{b}, sample by sample. One of them may be a number: a ## scalar, or an array the size of one sample. ## ## The rules for two series are given under @code{timeseries.plus}. ## ## @seealso{timeseries.plus} ## @end deftypefn function ts = minus (a, b) ts = arithmetic (a, b, 'minus'); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} times (@var{a}, @var{b}) ## ## The element-wise product of series, @code{@var{a} .* @var{b}}. ## ## @code{@var{ts} = times (@var{a}, @var{b})} returns the element-wise ## product of @var{a} and @var{b}, sample by sample. One of them may be a ## number: a scalar, or an array the size of one sample. ## ## The rules for two series are given under @code{timeseries.plus}, ## save that @qcode{DataInfo.Units} is dropped, since a product or ## quotient of units is the unit of neither; MATLAB keeps it where both ## series have the same. ## ## @seealso{timeseries.mtimes, timeseries.plus} ## @end deftypefn function ts = times (a, b) ts = arithmetic (a, b, 'times'); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} mtimes (@var{a}, @var{b}) ## ## The matrix product of series, @code{@var{a} * @var{b}}. ## ## @code{@var{ts} = mtimes (@var{a}, @var{b})} returns the matrix product ## of @var{a} and @var{b}, sample by sample. One of them may be a number ## whose size fits the product with one sample. ## ## The rules for two series are given under @code{timeseries.plus}, ## save that @qcode{DataInfo.Units} is dropped, since a product or ## quotient of units is the unit of neither; MATLAB keeps it where both ## series have the same. ## ## @seealso{timeseries.times, timeseries.plus} ## @end deftypefn function ts = mtimes (a, b) ts = arithmetic (a, b, 'mtimes'); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} rdivide (@var{a}, @var{b}) ## ## The element-wise quotient of series, @code{@var{a} ./ @var{b}}. ## ## @code{@var{ts} = rdivide (@var{a}, @var{b})} returns the element-wise ## quotient of @var{a} and @var{b}, sample by sample. One of them may be a ## number: a scalar, or an array the size of one sample. ## ## The rules for two series are given under @code{timeseries.plus}, ## save that @qcode{DataInfo.Units} is dropped, since a product or ## quotient of units is the unit of neither; MATLAB keeps it where both ## series have the same. ## ## @seealso{timeseries.ldivide, timeseries.mrdivide} ## @end deftypefn function ts = rdivide (a, b) ts = arithmetic (a, b, 'rdivide'); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} ldivide (@var{a}, @var{b}) ## ## The element-wise left quotient, @code{@var{a} .\ @var{b}}. ## ## @code{@var{ts} = ldivide (@var{a}, @var{b})} returns the element-wise ## quotient of @var{b} by @var{a}, sample by sample. One of them may be ## a number: a scalar, or an array the size of one sample. ## ## The rules for two series are given under @code{timeseries.plus}, ## save that @qcode{DataInfo.Units} is dropped, since a product or ## quotient of units is the unit of neither; MATLAB keeps it where both ## series have the same. ## ## @seealso{timeseries.rdivide, timeseries.mldivide} ## @end deftypefn function ts = ldivide (a, b) ts = arithmetic (a, b, 'ldivide'); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} mrdivide (@var{a}, @var{b}) ## ## The matrix right division of series, @code{@var{a} / @var{b}}. ## ## @code{@var{ts} = mrdivide (@var{a}, @var{b})} returns the matrix right ## division of @var{a} by @var{b}, sample by sample; each result keeps the ## size of one sample. One of them may be a number whose size fits the ## division with one sample. ## ## The rules for two series are given under @code{timeseries.plus}, ## save that @qcode{DataInfo.Units} is dropped, since a product or ## quotient of units is the unit of neither; MATLAB keeps it where both ## series have the same. ## ## A number divided by a series, @code{2 / @var{ts}}, divides each sample; ## MATLAB divides by the whole data column as one matrix. ## ## @seealso{timeseries.mldivide, timeseries.rdivide} ## @end deftypefn function ts = mrdivide (a, b) ts = arithmetic (a, b, 'mrdivide'); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} mldivide (@var{a}, @var{b}) ## ## The matrix left division of series, @code{@var{a} \ @var{b}}. ## ## @code{@var{ts} = mldivide (@var{a}, @var{b})} returns the matrix left ## division of @var{b} by @var{a}, sample by sample; each result keeps the ## size of one sample. One of them may be a number whose size fits the ## division with one sample. ## ## The rules for two series are given under @code{timeseries.plus}, ## save that @qcode{DataInfo.Units} is dropped, since a product or ## quotient of units is the unit of neither; MATLAB keeps it where both ## series have the same. ## ## @seealso{timeseries.mrdivide, timeseries.ldivide} ## @end deftypefn function ts = mldivide (a, b) ts = arithmetic (a, b, 'mldivide'); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{tf} =} eq (@var{a}, @var{b}) ## ## True when two series are equal, @code{@var{a} == @var{b}}. ## ## @code{@var{tf} = eq (@var{a}, @var{b})} returns a single logical for the ## whole series, as in MATLAB, not one per sample: @code{true} when ## @var{a} and @var{b} are series equal in every property but ## @qcode{UserData}, the class of the data aside, and @code{false} when ## either is not a series. @code{NaN} is unequal to itself. ## ## @seealso{timeseries.isequal, timeseries.isequalwithequalnans} ## @end deftypefn function tf = eq (a, b) tf = sameSeries (a, b, false); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{tf} =} isequal (@var{a}, @var{b}) ## ## True when two series are equal. ## ## @code{@var{tf} = isequal (@var{a}, @var{b})} is @code{@var{a} == ## @var{b}}: every property but @qcode{UserData} compared, the class of the ## data aside, @code{NaN} unequal to itself. ## ## @seealso{timeseries.eq, timeseries.isequalwithequalnans} ## @end deftypefn function tf = isequal (a, b) tf = sameSeries (a, b, false); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{tf} =} isequalwithequalnans (@var{a}, @var{b}) ## ## True when two series are equal, @code{NaN} equal to itself. ## ## @code{@var{tf} = isequalwithequalnans (@var{a}, @var{b})} is ## @code{isequal (@var{a}, @var{b})} with @code{NaN} counted as equal to ## @code{NaN}. ## ## @seealso{timeseries.isequal, timeseries.eq} ## @end deftypefn function tf = isequalwithequalnans (a, b) tf = sameSeries (a, b, true); endfunction endmethods ################################################################################ ## ** Filtering and detrending ** ## ################################################################################ ## Available Methods ## ## ## ## 'detrend' 'filter' 'idealfilter' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} detrend (@var{ts}, @var{type}) ## @deftypefnx {timeseries} {@var{ts} =} detrend (@var{ts}, @var{type}, @var{ind}) ## ## Remove a polynomial trend from a series. ## ## @code{@var{ts} = detrend (@var{ts}, @var{type})} subtracts from each ## column of the data the polynomial fitted to it by least squares against ## @qcode{Time}: its mean for @qcode{'constant'} or 0, a straight line for ## @qcode{'linear'} or 1, and a polynomial of that degree for any larger ## integer. @code{NaN} values are left out of the fit and stay @code{NaN}. ## Every other property is kept. ## ## @code{@var{ts} = detrend (@var{ts}, @var{type}, @var{ind})} detrends ## only the columns @var{ind} of the data, or its rows where ## @qcode{IsTimeFirst} is @code{false}. ## ## The data must be @code{double} or @code{single}, of two dimensions. ## MATLAB fits against the positions of the samples, closing up the gaps ## left by @code{NaN}, so its line is wrong where the time is not uniform ## or a value is missing; it also returns a series of fewer than three ## samples unchanged, where here @qcode{'constant'} of @code{[1 3]} is ## @code{[-1 1]}. ## ## @seealso{timeseries.filter, timeseries.idealfilter} ## @end deftypefn function this = detrend (this, type, ind) mustBeScalar (this, 'detrend'); if (nargin < 2) error ("timeseries.detrend: invalid number of input arguments."); endif if (isText (type) && any (strcmpi (type, {'constant', 'linear'}))) p = double (strcmpi (type, 'linear')); elseif (isnumeric (type) && isreal (type) && isscalar (type) && type >= 0 && type == fix (type)) p = double (type); else error (strcat ("timeseries.detrend: TYPE must be 'constant',", ... " 'linear' or a polynomial degree.")); endif if (nargin < 3) [X, cols] = signalData (this, 'detrend', true); else [X, cols] = signalData (this, 'detrend', true, ind); endif t = this.time_; for j = cols(:)' ok = ! isnan (X(:,j)); if (! any (ok)) continue; endif x = X(ok,j) - mean (X(ok,j)); if (p > 0) mu = mean (t(ok)); sc = max (abs (t(ok) - mu)); if (sc == 0) sc = 1; endif V = ((t(ok) - mu) / sc) .^ (0:p); x -= V * (V \ x); endif X(ok,j) = x; endfor this.data_ = signalBack (this, X); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} filter (@var{ts}, @var{b}, @var{a}) ## @deftypefnx {timeseries} {@var{ts} =} filter (@var{ts}, @var{b}, @var{a}, @var{ind}) ## ## Filter a series. ## ## @code{@var{ts} = filter (@var{ts}, @var{b}, @var{a})} runs each column ## of the data through the filter of numerator coefficients @var{b} and ## denominator coefficients @var{a}, as @code{filter (@var{b}, @var{a}, ## @var{x})} does, along time from a zero initial state. @code{NaN} ## values are first filled by the interpolation method of the series, so ## the data must not begin or end with one. Every other property is ## kept. ## ## @code{@var{ts} = filter (@var{ts}, @var{b}, @var{a}, @var{ind})} ## filters only the columns @var{ind} of the data, or its rows where ## @qcode{IsTimeFirst} is @code{false}. ## ## The data must be @code{double} or @code{single}, and the times ## uniformly spaced. MATLAB filters data of three dimensions across its ## rows when @var{ind} is not given, and filters a series with irregular ## times as if they were uniform, though its documentation requires them ## to be. ## ## @seealso{timeseries.idealfilter, timeseries.detrend} ## @end deftypefn function this = filter (this, b, a, ind) mustBeScalar (this, 'filter'); if (nargin < 3) error ("timeseries.filter: invalid number of input arguments."); endif if (! (isnumeric (b) && isvector (b) && isnumeric (a) && isvector (a))) error ("timeseries.filter: B and A must be numeric vectors."); endif if (a(1) == 0) error ("timeseries.filter: the first element of A must not be zero."); endif if (nargin < 4) [X, cols] = signalData (this, 'filter', false); else [X, cols] = signalData (this, 'filter', false, ind); endif if (! isUniform (this.time_)) error ("timeseries.filter: the times of TS must be uniformly spaced."); endif [X, errmsg] = fillGaps (this, X, cols); if (! isempty (errmsg)) error ("timeseries.filter: %s", errmsg); endif if (! isempty (X) && ! isempty (cols)) X(:,cols) = filter (b, a, X(:,cols), [], 1); endif this.data_ = signalBack (this, X); endfunction ## -*- texinfo -*- ## @deftypefn {timeseries} {@var{ts} =} idealfilter (@var{ts}, @var{intervals}, @var{type}) ## @deftypefnx {timeseries} {@var{ts} =} idealfilter (@var{ts}, @var{intervals}, @var{type}, @var{ind}) ## ## Filter a series with an ideal filter. ## ## @code{@var{ts} = idealfilter (@var{ts}, @var{intervals}, @var{type})} ## removes the mean of each column of the data, then keeps the frequencies ## in @var{intervals} (@var{type} @qcode{'pass'}) or removes them ## (@qcode{'notch'}) through the discrete Fourier transform. The mean is ## not added back. @var{intervals} holds one frequency interval per row, ## @code{[@var{lo}, @var{hi}]}, in cycles per unit of ## @qcode{TimeInfo.Units}; a frequency @var{f} is in it when ## @code{@var{lo} < @var{f} <= @var{hi}}. @code{NaN} values are first ## filled by the interpolation method of the series, so the data must not ## begin or end with one. A series with irregular times is resampled ## first, by its interpolation method, onto as many uniformly spaced times ## from its first time to its last, which it keeps. Every other property ## is kept. ## ## @code{@var{ts} = idealfilter (@var{ts}, @var{intervals}, @var{type}, ## @var{ind})} filters only the columns @var{ind} of the data, or its rows ## where @qcode{IsTimeFirst} is @code{false}. ## ## The data must be @code{double} or @code{single}, of two dimensions. ## MATLAB returns complex @code{NaN} throughout for data that begins or ## ends with @code{NaN}, keeps the mean of a series of fewer than three ## samples, and reads only the first and last columns of ## @var{intervals}. ## ## @seealso{timeseries.filter, timeseries.detrend} ## @end deftypefn function this = idealfilter (this, intervals, type, ind) mustBeScalar (this, 'idealfilter'); if (nargin < 3) error ("timeseries.idealfilter: invalid number of input arguments."); endif if (! (isnumeric (intervals) && isreal (intervals) && ismatrix (intervals) && columns (intervals) == 2 && rows (intervals) >= 1 && ! any (isnan (intervals(:))))) error (strcat ("timeseries.idealfilter: INTERVALS must be a", ... " numeric matrix of two columns, one interval", ... " per row.")); endif if (! (isText (type) && any (strcmp (type, {'pass', 'notch'})))) error ("timeseries.idealfilter: TYPE must be 'pass' or 'notch'."); endif if (nargin < 4) [X, cols] = signalData (this, 'idealfilter', true); else [X, cols] = signalData (this, 'idealfilter', true, ind); endif [X, errmsg] = fillGaps (this, X, cols); if (! isempty (errmsg)) error ("timeseries.idealfilter: %s", errmsg); endif n = numel (this.time_); if (n == 0 || isempty (cols)) return; endif if (! isUniform (this.time_)) this.data_ = signalBack (this, X); t = linspace (this.time_(1), this.time_(end), n)'; this = resampleAt (this, t, [], []); X = signalData (this, 'idealfilter', true); endif t = this.time_; dt = 1; if (n > 1) dt = (t(end) - t(1)) / (n - 1); endif k = (0:n-1)'; f = min (k, n - k) / (n * dt); lo = double (min (intervals, [], 2))'; hi = double (max (intervals, [], 2))'; keep = any (lo < f & f <= hi, 2); if (strcmp (type, 'notch')) keep = ! keep; endif Y = X(:,cols) - mean (X(:,cols), 1); X(:,cols) = real (ifft (fft (Y, [], 1) .* keep, [], 1)); this.data_ = signalBack (this, X); endfunction endmethods methods (Access = private) ## The series holding only the samples IDX, in that order, with every ## other property kept. function this = subset (this, idx) idx = idx(:); if (! isempty (this.data_)) this.data_ = takeSamples (this.data_, idx, this.timeDim_); endif this.time_ = this.time_(idx); if (! isempty (this.quality_)) this.quality_ = takeSamples (this.quality_, idx, this.timeDim_); endif this.timeInfo_.TimeVector = this.time_; endfunction ## The time, in this series' units, of the event a query names: EVENT is ## a name, with the occurrence N among the events of that name, or a ## tsdata.event object. Errors are raised under 'timeseries.METHOD'. function t = eventQueryTime (this, method, varargin) scope = ['timeseries.', method]; if (numel (varargin) < 1 || numel (varargin) > 2) error ("%s: invalid number of input arguments.", scope); endif event = varargin{1}; if (isa (event, 'tsdata.event') && isscalar (event)) ## An object is one event, so its only occurrence is the first if (numel (varargin) == 2 && ! isequal (varargin{2}, 1)) error ("%s: N must be 1 for a tsdata.event object.", scope); endif e = event; elseif (isText (event)) n = 1; if (numel (varargin) == 2) n = varargin{2}; endif [k, errmsg] = findEvent (this.events_, char (event), n); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif e = this.events_(k); else error (strcat ("%s: EVENT must be an event name or a tsdata.event", ... " object."), scope); endif startDate = this.timeInfo_.StartDate; eUnits = e.Units; if (isempty (eUnits)) eUnits = this.timeInfo_.Units; endif ## The event's own time where it counts as the series does, since a ## round trip through nanoseconds need not return the same number if (strcmp (eUnits, this.timeInfo_.Units) && (isempty (e.StartDate) || strcmp (e.StartDate, startDate))) t = e.Time; return; endif ns = e.Time * nsPerUnit (eUnits); if (! isempty (e.StartDate)) if (isempty (startDate)) error (strcat ("%s: TS has no start date to place the dated", ... " event '%s' on."), scope, e.Name); endif [off, ~] = dateOffsets ([tsdata.timemetadata.dateVector(startDate); tsdata.timemetadata.dateVector(e.StartDate)]); ## 'dateOffsets' counts from the earlier of the two if (off(1) == 0) ns += off(2); else ns -= off(1); endif endif t = ns / nsPerUnit (this.timeInfo_.Units); ## A time converted from other units or another date lands on a sample ## it names only up to rounding; it is that sample's time d = abs (this.time_ - t); k = find (d == min (d), 1); if (! isempty (k) && d(k) <= 1e-12 * max (1, abs (t))) t = this.time_(k); endif endfunction ## The series evaluated at the sorted times T, in its own units, by the ## method METHOD ('' for its own), the times it did not hold given the ## quality code CODE. A series of one sample is refused unless ALLOWONE. ## Returns an empty ERRMSG, or the body of the message the caller raises. function [this, errmsg] = resampleAt (this, t, method, code, allowOne) errmsg = ''; t = t(:); n = numel (this.time_); ## 'synchronize' evaluates a single sample at its own time, as MATLAB ## does; 'resample' needs two if (n < 1 || (n < 2 && ! (nargin > 4 && allowOne))) errmsg = "TS must hold at least two samples."; return; endif ip = this.dataInfo_.Interpolation; if (! isempty (method)) ip = tsdata.interpolation (method); elseif (isempty (ip.Fhandle)) ip = tsdata.interpolation ('linear'); endif hasQ = ! isempty (this.quality_); if (! isempty (code)) if (! hasQ) errmsg = "CODE is given for a series without quality codes."; return; endif if (! (isnumeric (code) && isreal (code) && isscalar (code) && code == fix (code))) errmsg = "CODE must be an integer."; return; endif if (! any (this.qualityInfo_.Code == code)) errmsg = sprintf ("CODE %d is not listed in 'QualityInfo.Code'.", ... code); return; endif endif ## The data with time first, as the interpolant takes and returns it td = this.timeDim_; ss = sampleSize (this.data_, td); od = this.data_; if (td > 1) od = permute (od, [td, 1:td-1]); endif nd = ip.Fhandle (t, this.time_, double (od)); if (! (isnumeric (nd) || islogical (nd)) || size (nd, 1) != numel (t) || numel (nd) != numel (t) * prod (ss)) errmsg = "the interpolation function must return one sample per time."; return; endif if (td == 1) nd = reshape (nd, [numel(t), ss(2:end)]); else nd = permute (reshape (nd, [numel(t), ss]), [2:td, 1]); endif if (islogical (this.data_)) nd = nd != 0; else nd = cast (nd, class (this.data_)); endif ## Quality: the sample a 'zoh' value is held from, otherwise the nearest, ## a tie going to the later; CODE at the times not already held if (hasQ) src = zeros (numel (t), 1); isZoh = strcmp (ip.Name, 'zoh'); ## A sample that is NaN throughout holds no value held = ! all (isnan (reshape (double (od), numel (this.time_), [])), 2); for i = 1:numel (t) k = []; if (isZoh) k = find (this.time_ <= t(i) & held, 1, 'last'); endif if (isempty (k)) d = abs (this.time_ - t(i)); k = find (d == min (d), 1, 'last'); endif src(i) = k; endfor q = takeSamples (this.quality_, src, td); if (! isempty (code)) isNew = ! ismember (t, this.time_); q = putSample (q, find (isNew), double (code), td); endif this.quality_ = q; endif this.data_ = nd; this.time_ = t; this.timeInfo_.TimeVector = t; endfunction ## The statistic FN of the series over time, with the options in ## VARARGIN: one value per data element, shaped as one sample. function v = statistic (this, fn, varargin) scope = ['timeseries.', fn]; mustBeScalar (this, fn); if (mod (numel (varargin), 2) != 0) error ("%s: name-value arguments must be in pairs.", scope); endif codes = []; fill = false; weighted = false; for i = 1:2:numel (varargin) [name, val] = varargin{i:i+1}; if (! isText (name)) error ("%s: invalid optional paired argument.", scope); endif switch (lower (char (name))) case 'quality' if (! ((isnumeric (val) || islogical (val)) && isreal (val) && all (val(:) == fix (val(:))))) error ("%s: 'Quality' must hold integer codes.", scope); endif codes = double (val(:)); case 'missingdata' if (! (isText (val) && any (strcmpi (val, {'remove', 'interpolate'})))) error ("%s: 'MissingData' must be 'remove' or 'interpolate'.", ... scope); endif fill = strcmpi (val, 'interpolate'); case 'weighting' if (! (isText (val) && any (strcmpi (val, {'none', 'time'})))) error ("%s: 'Weighting' must be 'none' or 'time'.", scope); endif weighted = strcmpi (val, 'time'); otherwise error ("%s: invalid optional paired argument.", scope); endswitch endfor n = numel (this.time_); if (n == 0 || isempty (this.data_)) v = []; return; endif td = this.timeDim_; ss = sampleSize (this.data_, td); cls = class (this.data_); ## Time first, one column per data element X = this.data_; if (td > 1) X = permute (X, [td, 1:td-1]); endif X = double (reshape (X, n, [])); m = columns (X); ## Missing: NaN while it counts as missing, and the excluded codes miss = false (n, m); if (this.treatNaN_) miss = isnan (X); endif if (! isempty (codes)) q = this.quality_; if (isempty (q) || ! any (ismember (codes, q(:)))) warning ("%s: no sample has the quality codes given.", scope); else absent = unique (codes(! ismember (codes, q(:)))); if (numel (absent) == 1) warning ("%s: no sample has the quality code %d.", scope, absent); elseif (! isempty (absent)) warning ("%s: no sample has the quality codes %s.", scope, ... strjoin (arrayfun (@num2str, absent', ... 'UniformOutput', false), ', ')); endif if (td > 1) q = permute (q, [td, 1:td-1]); endif q = reshape (q, n, []); if (columns (q) == 1) q = repmat (q, 1, m); endif miss |= ismember (q, codes); endif endif ## Missing values between samples filled linearly in time t = this.time_; if (fill) for j = 1:m ok = ! miss(:,j); if (sum (ok) >= 2) inner = miss(:,j) & t > min (t(ok)) & t < max (t(ok)); X(inner,j) = interp1 (t(ok), X(ok,j), t(inner), 'linear'); miss(inner,j) = false; endif endfor endif ## The time each sample covers, scaled to a mean of 1 w = ones (n, 1); if (weighted && n > 1) d = diff (t); w = [d(1); (d(1:end-1) + d(2:end)) / 2; d(end)]; if (all (w == 0)) w = ones (n, 1); endif w = w / mean (w); endif v = NaN (1, m); skipsNaN = any (strcmp (fn, {'min', 'max', 'mode', 'iqr'})); for j = 1:m keep = ! miss(:,j); if (skipsNaN) keep &= ! isnan (X(:,j)); endif v(j) = columnStatistic (fn, X(keep,j), w(keep)); endfor v = reshape (v, [ss, 1]); if (numel (ss) == 2 && ss(1) == 1) v = reshape (v, ss); endif ## A value left without samples is NaN, so the class of integer or ## logical data is kept only when every value has samples if (any (strcmp (fn, {'median', 'mode', 'min', 'max'})) && ! ((isinteger (this.data_) || islogical (this.data_)) && any (isnan (v(:))))) if (strcmp (cls, 'logical')) v = v != 0 & ! isnan (v); else v = cast (v, cls); endif endif endfunction ## The arithmetic OP of A and B, either or both a series. function ts = arithmetic (a, b, op) scope = ['timeseries.', op]; f = str2func (op); aIs = isa (a, 'timeseries'); bIs = isa (b, 'timeseries'); if (aIs) mustBeScalar (a, op); endif if (bIs) mustBeScalar (b, op); endif if (aIs && bIs) n = numel (a.time_); if (numel (b.time_) != n) error ("%s: A and B must hold as many samples.", scope); endif if (! strcmp (a.timeInfo_.Units, b.timeInfo_.Units)) error ("%s: A and B must have their times in the same units.", ... scope); endif sdA = a.timeInfo_.StartDate; sdB = b.timeInfo_.StartDate; if (isempty (sdA) != isempty (sdB)) error (strcat ("%s: A and B must both have a start date or", ... " neither."), scope); endif if (! isempty (sdA)) [off, ~] = dateOffsets ([tsdata.timemetadata.dateVector(sdA); tsdata.timemetadata.dateVector(sdB)]); nsu = nsPerUnit (a.timeInfo_.Units); ## Instants a microsecond apart, rounding aside, are the same if (any (abs ((a.time_ * nsu + off(1)) - (b.time_ * nsu + off(2))) > 1e3)) error ("%s: A and B must be dated at the same instants.", scope); endif elseif (n > 0) d = b.time_ - a.time_; tol = 1e-12 * max ([1; abs(a.time_)]); if (any (abs (d - d(1)) > tol)) error (strcat ("%s: the times of A and B must be equal or", ... " differ by a constant offset."), scope); endif if (d(1) != 0) warning (strcat ("%s: the times of A and B differ by a", ... " constant offset; the result takes", ... " the times of A."), scope); endif endif if (a.timeDim_ != b.timeDim_) error ("%s: A and B must hold samples of the same shape.", scope); endif [data, td, errmsg] = sampleOp (f, double (a.data_), ... double (b.data_), a.timeDim_, n, ... op, true); ts = a; ts.name_ = 'unnamed'; units = ''; if (any (strcmp (op, {'plus', 'minus'})) && strcmp (a.dataInfo_.Units, b.dataInfo_.Units)) units = a.dataInfo_.Units; endif ts.dataInfo_.Units = units; ts.dataInfo_.Interpolation = tsdata.interpolation ('linear'); ts.quality_ = []; ts.qualityInfo_ = tsdata.qualmetadata (); if (isempty (a.events_)) ts.events_ = b.events_; elseif (! isempty (b.events_)) ts.events_ = [a.events_(:)', b.events_(:)']; endif else if (aIs) ts = a; num = b; left = true; else ts = b; num = a; left = false; endif if (! ((isnumeric (num) || islogical (num)) && ! isempty (num))) error ("%s: the other operand must be a series or a number.", scope); endif n = numel (ts.time_); num = double (num); if (left) [data, td, errmsg] = sampleOp (f, double (ts.data_), num, ... ts.timeDim_, n, op, false); else [data, td, errmsg] = sampleOp (@(x, y) f (y, x), ... double (ts.data_), num, ... ts.timeDim_, n, op, false); endif endif if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif q = ts.quality_; if (! isempty (q) && numel (q) != n && ! isequal (size (data), size (q))) error (strcat ("%s: the result changes the size of the samples, so", ... " the quality codes held per data element cannot", ... " follow it."), scope); endif ## Codes held per sample lie along the time dimension of the result if (! isempty (q) && numel (q) == n) if (td == 1) ts.quality_ = q(:); else ts.quality_ = reshape (q, [ones(1, td - 1), n]); endif endif ts.data_ = data; ts.timeDim_ = td; endfunction ## The data of the series as a double matrix, time first and one column ## per data element, and the columns the optional IND selects, all when ## it is not given. Errors are raised under 'timeseries.METHOD'; with ## TWODIMS, data of more than two dimensions is refused. function [X, cols] = signalData (this, method, twoDims, varargin) scope = ['timeseries.', method]; data = this.data_; if (! isfloat (data) || iscomplex (data)) error ("%s: the data must be real, of class double or single.", ... scope); endif if (twoDims && ndims (data) > 2) error ("%s: the data must have two dimensions.", scope); endif n = numel (this.time_); td = this.timeDim_; ss = sampleSize (data, td); if (td > 1) data = permute (data, [td, 1:td-1]); endif X = double (reshape (data, n, [])); if (td == 1) nc = size (this.data_, 2); what = 'columns'; else nc = ss(1); what = 'rows'; endif if (isempty (varargin)) cols = 1:columns (X); return; endif ind = varargin{1}; if (! (isnumeric (ind) && isreal (ind) && (isvector (ind) || isempty (ind)) && all (ind == fix (ind) & ind >= 1 & ind <= nc))) error (strcat ("%s: IND must be positive integers not exceeding", ... " the number of %s of the data."), scope, what); endif if (td == 1) cols = unique (double (ind(:)))'; else mask = false ([ss, 1]); mask(ind,:) = true; cols = find (mask(:))'; endif endfunction ## The data of the series from X, a matrix laid out as 'signalData' ## returns it, in the class and layout of the data. function data = signalBack (this, X) td = this.timeDim_; n = numel (this.time_); if (td == 1) data = X; else ss = sampleSize (this.data_, td); data = permute (reshape (X, [n, ss]), [2:td, 1]); endif data = cast (data, class (this.data_)); endfunction ## The columns COLS of X, laid out as 'signalData' returns it, with each ## NaN filled by the interpolation method of the series. A column that ## begins or ends with NaN gives the rest of an error message in ERRMSG. function [X, errmsg] = fillGaps (this, X, cols) errmsg = ''; if (isempty (X)) return; endif ip = this.dataInfo_.Interpolation; if (isempty (ip.Fhandle)) ip = tsdata.interpolation ('linear'); endif t = this.time_; for j = cols(:)' gap = isnan (X(:,j)); if (! any (gap)) continue; endif if (gap(1) || gap(end)) errmsg = "the data must not begin or end with NaN."; return; endif v = ip.Fhandle (t(gap), t(! gap), X(! gap,j)); if (! (isnumeric (v) || islogical (v)) || numel (v) != sum (gap)) errmsg = strcat ("the interpolation function must return one", ... " sample per time."); return; endif X(gap,j) = v(:); endfor endfunction ## Convert user times X to times of this series. Numbers are relative ## times in its units, or, for a series with a start date when ## NUMISDATENUM is true, datenums; text and datetime values are dates. ## Returns them as a double column, the tolerance a match allows, and an ## empty ERRMSG, or the rest of the message the caller raises. function [t, tol, errmsg] = relativeTime (this, x, numIsDatenum) tol = 0; errmsg = ''; startDate = this.timeInfo_.StartDate; isDate = ischar (x) || iscellstr (x) || isa (x, 'string') ... || isa (x, 'datetime'); if (isempty (startDate)) if (isDate || ! (isnumeric (x) && isreal (x))) t = []; errmsg = "must be numeric for a series without a start date."; return; endif t = double (x(:)); elseif (isnumeric (x) && isreal (x) && ! numIsDatenum) t = double (x(:)); else dv0 = tsdata.timemetadata.dateVector (startDate); nsu = nsPerUnit (this.timeInfo_.Units); if (isnumeric (x) && isreal (x)) ## A datenum reaches a series only to its own precision t = (double (x(:)) - datenum (dv0)) * 864e11 / nsu; tol = 1e-9 * 864e11 / nsu; else [dv, dverr] = dateVectors (x, ''); if (! isempty (dverr)) t = []; errmsg = "must be dates, as text, datenum or datetime values."; return; endif ## Dates are exact to the microsecond, rounding apart t = tsdata.timemetadata.dateOffset (dv, dv0) / nsu; tol = 1e3 / nsu; endif endif if (any (isnan (t)) && numel (t) > 1) errmsg = "must not be NaN."; endif endfunction endmethods ## Property access methods function val = get.Events (this) val = this.events_; endfunction function this = set.Events (this, val) if (isnumeric (val) && isempty (val)) this.events_ = []; elseif (isa (val, 'tsdata.event') && (isvector (val) || isempty (val))) this.events_ = val; else error (strcat ("timeseries: 'Events' must be a vector of", ... " tsdata.event objects.")); endif endfunction function val = get.Name (this) val = this.name_; endfunction function this = set.Name (this, val) if (! isText (val)) error ("timeseries: 'Name' must be a character vector."); endif val = char (val); if (isempty (val)) val = ''; endif this.name_ = val; endfunction function val = get.UserData (this) val = this.userData_; endfunction function this = set.UserData (this, val) this.userData_ = val; endfunction function val = get.Data (this) val = this.data_; endfunction function this = set.Data (this, val) if (iscell (val)) error ("timeseries: 'Data' must not be a cell array."); endif if (! (isnumeric (val) || islogical (val))) error ("timeseries: 'Data' must be a numeric or logical array."); endif n = numel (this.time_); if (n > 0 && isequal (size (val), size (this.data_))) td = this.timeDim_; elseif (n > 0 && ! isempty (val)) [val, td, errmsg] = orientData (val, n); if (! isempty (errmsg)) error ("timeseries: 'Data' must have one sample per time."); endif elseif (ndims (val) <= 2) td = 1; else td = ndims (val); endif q = this.quality_; if (! isempty (q) && numel (q) != n && ! isequal (size (val), size (q))) error (strcat ("timeseries: 'Data' must keep the size of the", ... " quality codes held per data element; clear", ... " 'Quality' first.")); endif this.data_ = val; this.timeDim_ = td; endfunction function val = get.DataInfo (this) val = this.dataInfo_; endfunction function this = set.DataInfo (this, val) if (! (isa (val, 'tsdata.datametadata') && isscalar (val))) error (strcat ("timeseries: 'DataInfo' must be a", ... " tsdata.datametadata object.")); endif this.dataInfo_ = val; endfunction function val = get.Time (this) val = this.time_; endfunction function this = set.Time (this, val) if (! (isnumeric (val) && isreal (val) && (isvector (val) || isempty (val)))) error ("timeseries: 'Time' must be a numeric vector."); endif val = double (val(:)); if (! all (isfinite (val))) error ("timeseries: 'Time' must be finite."); endif if (any (diff (val) < 0)) error ("timeseries: 'Time' must be non-decreasing."); endif if (! isempty (this.quality_) && numel (val) != numel (this.time_)) error ("timeseries: 'Time' must have one time per quality code."); endif if (! isempty (this.data_) && ! isempty (val) && numel (val) != numel (this.time_)) [data, td, errmsg] = orientData (this.data_, numel (val)); if (! isempty (errmsg)) error ("timeseries: 'Time' must have one time per sample."); endif this.data_ = data; this.timeDim_ = td; endif this.time_ = val; this.timeInfo_.TimeVector = val; endfunction function val = get.TimeInfo (this) val = this.timeInfo_; endfunction function this = set.TimeInfo (this, val) if (! (isa (val, 'tsdata.timemetadata') && isscalar (val))) error (strcat ("timeseries: 'TimeInfo' must be a", ... " tsdata.timemetadata object.")); endif ## The derived properties always follow this series' own time vector val.TimeVector = this.time_; ## Events dated on their own count from the start date being removed, ## since a series without one cannot place a date old = this.timeInfo_.StartDate; if (! isempty (old) && isempty (val.StartDate)) dv0 = tsdata.timemetadata.dateVector (old); for k = 1:numel (this.events_) e = this.events_(k); if (isempty (e.StartDate)) continue; endif eUnits = e.Units; if (isempty (eUnits)) eUnits = val.Units; endif ns = e.Time * nsPerUnit (eUnits) + tsdata.timemetadata.dateOffset ... (tsdata.timemetadata.dateVector (e.StartDate), dv0); e.StartDate = ''; e.Time = ns / nsPerUnit (eUnits); this.events_(k) = e; endfor endif this.timeInfo_ = val; endfunction function val = get.Quality (this) val = this.quality_; endfunction function this = set.Quality (this, val) [val, errmsg] = qualityValue (val, this.data_, numel (this.time_), ... this.timeDim_); if (! isempty (errmsg)) error ("timeseries: 'Quality' %s", errmsg); endif this.quality_ = val; endfunction function val = get.QualityInfo (this) val = this.qualityInfo_; endfunction function this = set.QualityInfo (this, val) if (! (isa (val, 'tsdata.qualmetadata') && isscalar (val))) error (strcat ("timeseries: 'QualityInfo' must be a", ... " tsdata.qualmetadata object.")); endif this.qualityInfo_ = val; endfunction function val = get.IsTimeFirst (this) val = this.timeDim_ == 1; endfunction function this = set.IsTimeFirst (this, val) if (! ((islogical (val) || isnumeric (val)) && isscalar (val) && any (val == [0, 1]))) error ("timeseries: 'IsTimeFirst' must be a logical scalar."); endif if (logical (val) != (this.timeDim_ == 1)) error (strcat ("timeseries: 'IsTimeFirst' cannot be %s: the", ... " time vector runs along the %s dimension of", ... " the data."), mat2str (logical (val)), ... ifelse (logical (val), 'last', 'first')); endif endfunction function val = get.TreatNaNasMissing (this) val = this.treatNaN_; endfunction function this = set.TreatNaNasMissing (this, val) if (! ((islogical (val) || isnumeric (val)) && isscalar (val) && any (val == [0, 1]))) error ("timeseries: 'TreatNaNasMissing' must be a logical scalar."); endif this.treatNaN_ = logical (val); endfunction function val = get.Length (this) val = numel (this.time_); endfunction function this = set.Length (this, val) error ("timeseries: 'Length' is read-only."); endfunction endmethods endclassdef ## Resolve NAME, in any case, to one of the property names ALLNAMES. Returns ## the property's own spelling and an empty ERRMSG, or the body of the ## message the caller raises. function [name, errmsg] = propertyName (name, allNames) errmsg = ''; idx = find (strcmpi (name, allNames), 1); if (isempty (idx)) errmsg = sprintf ("unknown property: '%s'", name); else name = allNames{idx}; endif endfunction ## The size of X as MATLAB writes it in a summary, as '3x1'. function str = sizestr (x) str = strjoin (arrayfun (@num2str, size (x), 'UniformOutput', false), 'x'); endfunction ## True for a character vector (or '') and for a string scalar. function tf = isText (x) tf = (ischar (x) && (isrow (x) || isempty (x))) ... || (isa (x, 'string') && isscalar (x)); endfunction ## Find the Nth event named NAME in EVENTS, counting only those of that name. ## Returns its index and an empty ERRMSG, or the body of the message the ## caller raises. function [k, errmsg] = findEvent (events, name, n) k = 0; errmsg = ''; if (isempty (events)) errmsg = "TS has no events."; return; endif if (! (isnumeric (n) && isreal (n) && isscalar (n) && n == fix (n) && n >= 1)) errmsg = "N must be a positive integer."; return; endif idx = find (strcmp ({events.Name}, name)); if (isempty (idx)) errmsg = sprintf ("TS has no event named '%s'.", name); elseif (n > numel (idx)) errmsg = sprintf ("TS has no event number %d named '%s'.", n, name); else k = idx(n); endif endfunction ## True when events A and B have the same name and the same time. function tf = sameEvent (a, b) tf = strcmp (a.Name, b.Name) && a.Time == b.Time ... && strcmp (a.Units, b.Units) && strcmp (a.StartDate, b.StartDate); endfunction ## Read an interpolation method name for 'resample' or 'synchronize': '' or ## [] for the series' own. Returns it in lower case and an empty ERRMSG, or ## the rest of the message the caller raises after naming it. function [method, errmsg] = interpName (method) errmsg = ''; if (isempty (method) && (isnumeric (method) || ischar (method))) method = ''; return; endif if (! isText (method) || ! any (strcmpi (char (method), {'linear', 'zoh'}))) errmsg = "must be 'linear' or 'zoh'."; return; endif method = lower (char (method)); endfunction ## The statistic FN of the column X with the weights W, W all ones when ## unweighted. Unweighted they are MATLAB's; weighted, the time-weighted ## statistic, which with equal weights is the same. function v = columnStatistic (fn, x, w) n = numel (x); if (n == 0) if (strcmp (fn, 'sum')) v = 0; else v = NaN; endif return; endif switch (fn) case 'mean' v = sum (w .* x) / sum (w); case 'sum' v = sum (w .* x); case {'var', 'std'} if (n == 1) v = 0; else w = w / mean (w); mu = sum (w .* x) / sum (w); v = sum (w .* (x - mu) .^ 2) / (n - 1); endif if (strcmp (fn, 'std')) v = sqrt (v); endif case 'median' v = weightedQuantile (x, w, 0.5); case 'iqr' v = weightedQuantile (x, w, 0.75) - weightedQuantile (x, w, 0.25); case 'min' v = min (x); case 'max' v = max (x); case 'mode' [u, ~, k] = unique (x); total = accumarray (k(:), w(:)); [~, i] = max (total); v = u(i); endswitch endfunction ## The quantile P of X under the weights W: the (k - 0.5)/n rule, with the ## cumulative weight for k, linear between and constant beyond the ends. ## NaN in X gives NaN. function v = weightedQuantile (x, w, p) if (any (isnan (x))) v = NaN; return; endif [x, i] = sort (x(:)); w = w(i); if (numel (x) == 1) v = x; return; endif pos = (cumsum (w) - w / 2) / sum (w); if (p <= pos(1)) v = x(1); elseif (p >= pos(end)) v = x(end); else k = find (pos <= p, 1, 'last'); if (pos(k) == p) v = x(k); else v = x(k) + (p - pos(k)) / (pos(k+1) - pos(k)) * (x(k+1) - x(k)); endif endif endfunction ## Apply F to the data X, N samples along dimension TD, and Y: another ## series' data when BOTHSERIES, else a number, a scalar or one sample's ## size. Element-wise operators broadcast; matrix operators run per sample, ## named by OP. Returns the data, the dimension its time runs along, and an ## empty ERRMSG, or the body of the message the caller raises. function [data, td, errmsg] = sampleOp (f, x, y, td, n, op, bothSeries) data = []; errmsg = ''; matrixOp = any (strcmp (op, {'mtimes', 'mrdivide', 'mldivide'})); ss = sampleSize (x, td); if (! matrixOp) if (bothSeries) if (! isequal (size (x), size (y))) errmsg = "A and B must hold samples of the same size."; return; endif elseif (! (isscalar (y) || isequal (size (y), ss) || (td == 1 && isequal (size (y), [1, ss(2)])))) errmsg = "a number must be a scalar or the size of one sample."; return; endif data = f (x, y); return; endif if (n == 0) data = x; return; endif ## A scalar sample is multiplied only by a number, as MATLAB applies matrix ## rules there; a division keeps the size of the sample isDivision = ! strcmp (op, 'mtimes'); if (isequal (ss, [1, 1]) && ! bothSeries && ! isscalar (y) && strcmp (op, 'mtimes')) errmsg = "the sizes of the samples do not fit the operator."; return; endif parts = cell (1, n); for k = 1:n xk = reshape (takeSamples (x, k, td), ss); if (bothSeries) yk = reshape (takeSamples (y, k, td), ss); else yk = y; endif try r = f (xk, yk); catch errmsg = "the sizes of the samples do not fit the operator."; return; end_try_catch if ((k > 1 && ! isequal (size (r), size (parts{1}))) || (isDivision && ! isequal (size (r), ss))) errmsg = "the sizes of the samples do not fit the operator."; return; endif parts{k} = r; endfor ## The results laid as the series lays its samples: rows where time runs ## first and a sample is a row, slices along a new last dimension otherwise rs = size (parts{1}); if (isequal (rs, [1, 1])) data = vertcat (parts{:}); td = 1; elseif (td == 1 && rs(1) == 1) data = vertcat (parts{:}); else data = cat (numel (rs) + 1, parts{:}); td = numel (rs) + 1; endif endfunction ## True when the series A and B agree in every property but UserData, the ## class of the data aside; NaN equal to itself when EQUALNANS. function tf = sameSeries (a, b, equalNans) tf = false; if (! (isa (a, 'timeseries') && isa (b, 'timeseries')) || ! isequal (size (a), size (b))) return; endif if (equalNans) same = @(x, y) isequaln (x, y); else same = @(x, y) isequal (x, y); endif for k = 1:numel (a) x = a(k); y = b(k); ok = strcmp (x.Name, y.Name) && same (double (x.Data), double (y.Data)) ... && isequal (x.Time, y.Time) && x.IsTimeFirst == y.IsTimeFirst ... && isequal (x.Quality, y.Quality) ... && x.TreatNaNasMissing == y.TreatNaNasMissing; ti = {x.TimeInfo, y.TimeInfo}; ok = ok && strcmp (ti{1}.Units, ti{2}.Units) ... && strcmp (ti{1}.StartDate, ti{2}.StartDate) ... && strcmp (ti{1}.Format, ti{2}.Format) ... && isequal (ti{1}.UserData, ti{2}.UserData); di = {x.DataInfo, y.DataInfo}; ok = ok && strcmp (di{1}.Units, di{2}.Units) ... && strcmp (di{1}.Interpolation.Name, di{2}.Interpolation.Name) ... && isequal (di{1}.UserData, di{2}.UserData) ... && isequal (di{1}.InterpretSingleRowDataAs3D, ... di{2}.InterpretSingleRowDataAs3D); if (ok && strcmp (di{1}.Interpolation.Name, 'myFuncHandle')) ok = strcmp (func2str (di{1}.Interpolation.Fhandle), ... func2str (di{2}.Interpolation.Fhandle)); endif qi = {x.QualityInfo, y.QualityInfo}; ok = ok && isequal (qi{1}.Code, qi{2}.Code) ... && isequal (qi{1}.Description, qi{2}.Description) ... && isequal (qi{1}.UserData, qi{2}.UserData); ev = {x.Events, y.Events}; ok = ok && numel (ev{1}) == numel (ev{2}); for j = 1:numel (ev{1}) if (! ok) break; endif e1 = ev{1}(j); e2 = ev{2}(j); ok = strcmp (e1.Name, e2.Name) && e1.Time == e2.Time ... && strcmp (e1.Units, e2.Units) ... && strcmp (e1.StartDate, e2.StartDate) ... && isequal (e1.EventData, e2.EventData); endfor if (! ok) return; endif endfor tf = true; endfunction ## The times at which 'synchronize' evaluates a series, and the times it ## labels the result with, for the GRID of times in nanoseconds on a common ## scale. TNS are the series' own times on that scale, OWN the same times in ## its units, OFF its offset on the scale and NS its nanoseconds per unit. A ## grid time the series holds is its own time exactly, and one within TOLNS ## of a time it holds is evaluated there. Labels count from OFFLABEL. function [evalT, labelT] = frameTimes (grid, tNs, own, off, ns, tolNs, offLabel) [isOwn, k] = ismember (grid, tNs); evalT = (grid - off) / ns; evalT(isOwn) = own(k(isOwn)); for i = find (! isOwn)' [d, j] = min (abs (tNs - grid(i))); if (d <= tolNs) evalT(i) = own(j); endif endfor labelT = (grid - offLabel) / ns; if (offLabel == off) labelT(isOwn) = own(k(isOwn)); endif endfunction ## True when the times T are uniformly spaced: a single step, positive, up to ## rounding. No time or one time counts as uniform. function tf = isUniform (t) d = diff (t(:)); tf = isempty (d) || (all (d > 0) && max (d) - min (d) <= 1e-10 * max (d)); endfunction ## Raise unless OBJ is a single series. function mustBeScalar (obj, method) if (! isscalar (obj)) error ("timeseries.%s: TS must be a single series.", method); endif endfunction ## Validate sample indices IND for a series of N samples. A logical mask no ## longer than N is allowed where ALLOWLOGICAL is true. Returns the indices ## and an empty ERRMSG, or the rest of the message the caller raises. function [idx, errmsg] = sampleIndex (ind, n, allowLogical) errmsg = ''; idx = []; if (islogical (ind) && allowLogical) if (! (isvector (ind) || isempty (ind)) || numel (ind) > n) errmsg = "must not be a logical mask longer than the series."; return; endif idx = find (ind(:)); elseif (isnumeric (ind) && isreal (ind) && (isvector (ind) || isempty (ind))) idx = double (ind(:)); if (! all (idx == fix (idx) & idx >= 1 & idx <= n)) errmsg = "must be positive integers not exceeding the number of samples."; idx = []; endif else errmsg = "must be positive integers not exceeding the number of samples."; endif endfunction ## Lay out DATA, N samples of size SS along dimension TDFROM, along dimension ## TDTO instead. function data = layOut (data, tdFrom, tdTo, ss, n) if (tdFrom == tdTo) return; endif if (tdTo == 1) data = permute (data, [tdFrom, 1:tdFrom-1]); data = reshape (data, [n, ss(2:end)]); else data = permute (data, [2:ndims(data), 1]); data = reshape (data, [ss, n]); endif endfunction ## Put SAMPLE, one sample laid out along dimension TD, at position J of DATA. function data = putSample (data, j, sample, td) subs = repmat ({':'}, 1, max (ndims (data), td)); subs{td} = j; data(subs{:}) = sample; endfunction ## Nanoseconds in one time unit, each exact in double, so converting as ## T * NSFROM / NSTO is exact wherever the result is representable. function ns = nsPerUnit (unit) switch (unit) case 'weeks' ns = 6048e11; case 'days' ns = 864e11; case 'hours' ns = 36e11; case 'minutes' ns = 6e10; case 'seconds' ns = 1e9; case 'milliseconds' ns = 1e6; case 'microseconds' ns = 1e3; otherwise ns = 1; endswitch endfunction function r = unitRank (unit) r = find (strcmp (unit, {'nanoseconds', 'microseconds', 'milliseconds', ... 'seconds', 'minutes', 'hours', 'days', 'weeks'})); endfunction ## Find the dimension of DATA the time vector runs along, given N times, or ## the default reading when N is empty. Returns DATA, reshaped when a matrix ## holds a sample per column, the dimension TD, and an empty ERRMSG, or the ## body of the message the caller raises. The rule is MATLAB's: three ## dimensions or more run time along the last; a matrix of N rows along the ## first; one of N columns along a third, as an Rx1xN array; and with a ## single time a matrix is one sample, whatever its size. function [data, td, errmsg] = orientData (data, n) errmsg = ''; nd = ndims (data); if (isempty (n)) if (nd >= 3) td = nd; elseif (isrow (data) && numel (data) > 1) data = reshape (data, 1, 1, numel (data)); td = 3; else td = 1; endif elseif (nd >= 3) td = nd; if (size (data, nd) != n) errmsg = "must have one sample per time."; endif elseif (size (data, 1) == n) td = 1; elseif (size (data, 2) == n) data = reshape (data, size (data, 1), 1, n); td = 3; elseif (n == 1) td = 3; else td = 1; errmsg = "must have one sample per time."; endif endfunction ## Select the samples IDX of DATA, whose time runs along dimension TD. function data = takeSamples (data, idx, td) subs = repmat ({':'}, 1, max (ndims (data), td)); subs{td} = idx; data = data(subs{:}); endfunction ## The size of one sample of DATA, whose time runs along dimension TD. function sz = sampleSize (data, td) if (td == 1) sz = [1, size(data, 2)]; else sz = size (data); sz(end+1:td) = 1; sz = sz(1:td-1); endif endfunction ## Read a constructor TIME argument. Returns a double column, the start date ## of a time vector given as dates ('' otherwise), the units the argument ## implies ('' for plain numbers), and an empty ERRMSG, or the body of the ## message the caller raises. function [time, startDate, units, errmsg] = parseTime (time) startDate = ''; units = ''; errmsg = ''; if (isa (time, 'duration')) if (! all (isfinite (seconds (time(:))))) errmsg = "TIME must be finite."; return; endif ## MATLAB's own pairing of units and formats in 'timeseries2timetable', ## read backwards switch (time.Format) case 'm' units = 'minutes'; time = minutes (time(:)); case 'h' units = 'hours'; time = hours (time(:)); case 'd' units = 'days'; time = days (time(:)); otherwise units = 'seconds'; time = seconds (time(:)); endswitch time = double (time); return; endif if (iscell (time) && ! isempty (time) && all (cellfun (@(x) isnumeric (x) && isscalar (x), time(:)))) time = cell2mat (time(:)); endif if ((iscellstr (time) || isa (time, 'string') || isa (time, 'datetime')) && isempty (time)) time = []; return; endif if (iscellstr (time) || isa (time, 'string') || isa (time, 'datetime')) [dv, errmsg] = dateVectors (time, ''); if (! isempty (errmsg)) errmsg = ["TIME ", errmsg]; return; endif if (any (dv(:,1) < 0 | dv(:,1) > 9999)) errmsg = "TIME must fall in the years 0 to 9999."; return; endif [~, dv0] = dateOffsets (dv); startDate = tsdata.timemetadata.dateText (dv0); ## Counted from the start date as stored, to the millisecond ns = tsdata.timemetadata.dateOffset ( ... dv, tsdata.timemetadata.dateVector (startDate)); time = ns / 864e11; units = 'days'; return; endif if (! (isnumeric (time) && isreal (time) && (isvector (time) || isempty (time)))) errmsg = "TIME must be a numeric vector, dates or durations."; return; endif time = double (time(:)); if (! all (isfinite (time))) errmsg = "TIME must be finite."; endif endfunction ## Read dates given as text (a cellstr, a string array, a character matrix ## of one date per row, a character vector) or as a datetime array, into a ## date vector per row. With FMT each text date is read strictly in that ## format; without, each is read by its own. Returns an empty ERRMSG, or ## the end of the message the caller raises after naming the argument. function [dv, errmsg] = dateVectors (x, fmt) dv = zeros (0, 6); errmsg = ''; if (isa (x, 'datetime')) if (any (isnat (x(:)))) errmsg = "must not hold NaT."; return; endif dv = datevec (x(:)); return; endif if (ischar (x)) x = cellstr (x); elseif (isa (x, 'string')) x = cellstr (x); endif if (! iscellstr (x)) errmsg = "must be dates, as text or datetime values."; return; endif x = x(:); dv = zeros (numel (x), 6); for i = 1:numel (x) try [dvi, ok] = tsdata.timemetadata.dateVector (x{i}); if (isempty (fmt) && ok) dv(i,:) = dvi; elseif (isempty (fmt)) dv(i,:) = datevec (x{i}); else dv(i,:) = datevec (x{i}, fmt); endif catch if (isempty (fmt)) errmsg = sprintf ("holds text that is not a date: '%s'", x{i}); else errmsg = sprintf ("holds a date not written as '%s': '%s'", ... fmt, x{i}); endif dv = zeros (0, 6); return; end_try_catch endfor endfunction ## The offsets of the dates DV, one date vector per row, from the earliest, ## in nanoseconds, and the earliest as a date vector. Whole days and the ## seconds of the day are taken apart, so a whole number of seconds is exact. function [ns, dv0] = dateOffsets (dv) ns = zeros (rows (dv), 1); dv0 = []; if (isempty (dv)) return; endif dayNum = datenum (dv(:,1), dv(:,2), dv(:,3)); secs = dv(:,4) * 3600 + dv(:,5) * 60 + dv(:,6); [~, i0] = min (dayNum * 86400 + secs); ns = (dayNum - dayNum(i0)) * 864e11 + (secs - secs(i0)) * 1e9; dv0 = dv(i0,:); endfunction ## Validate quality codes for data DATA of N samples along dimension TD. ## Returns them as double, a vector laid along the time dimension, and an ## empty ERRMSG, or the rest of the message the caller raises after naming ## the codes. function [q, errmsg] = qualityValue (q, data, n, td) errmsg = ''; if (isempty (q) && (isnumeric (q) || islogical (q))) q = []; return; endif if (! ((isnumeric (q) || islogical (q)) && isreal (q))) errmsg = "must be an integer array."; return; endif if (! (all (q(:) == fix (q(:))) && all (q(:) >= -128 & q(:) <= 127))) errmsg = "must hold integers from -128 to 127."; return; endif q = double (q); if (isequal (size (q), size (data)) && n > 0) return; endif if (isvector (q) && numel (q) == n) if (td == 1) q = q(:); else q = reshape (q, [ones(1, td - 1), n]); endif return; endif errmsg = "must have one code per sample or the size of the data."; endfunction ## 'pkg test' reads the BISTs from the file it is testing, so a class ## whose suite lives in inst/tests/ reads as carrying none. This block ## answers for it and checks nothing itself. %!test %! ## The suite for this class is inst/tests/timeseries.m-tst. pr0m1th3as-datatypes-9450b29/inst/timeseries2timetable.m000066400000000000000000000635241527370255000231650ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{TT} =} timeseries2timetable (@var{ts}) ## @deftypefnx {datatypes} {@var{TT} =} timeseries2timetable (@var{ts1}, @dots{}, @var{tsN}) ## @deftypefnx {datatypes} {@var{TT} =} timeseries2timetable (@var{tc}) ## ## Convert time series to a timetable. ## ## @code{@var{TT} = timeseries2timetable (@var{ts})} returns a timetable with ## a variable for each series in the @code{timeseries} array @var{ts}, and ## @code{@var{TT} = timeseries2timetable (@var{ts1}, @dots{}, @var{tsN})} one ## for each of the series @var{ts1} to @var{tsN}, which must then each be a ## single series. A @code{tscollection} @var{tc} may stand for any of them ## and gives a variable for each of its members, in order; MATLAB accepts ## @code{timeseries} alone. ## ## Every series must have samples, and all the same times, in the same ## units, with the same @qcode{TimeInfo.StartDate}; convert series on other ## times one by one and combine the timetables with @code{synchronize}. ## ## The row times are the time vector: a @code{duration} array for series ## without a start date, displayed in the units of the series (seconds and ## finer units in seconds, weeks in days), and a @code{datetime} array from ## the start date otherwise, in the format @qcode{TimeInfo.Format} gives, ## or @qcode{'dd-MMM-uuuu HH:mm:ss'} when it is empty. MATLAB copies ## @qcode{TimeInfo.Format}, a @code{datestr} format, as it is and fails; ## here it is translated to the @code{datetime} format that writes the same ## text, but for an hour on a 12-hour clock, which @code{datestr} pads with ## a space and a @code{datetime} writes without padding. ## ## Each variable is named after its series, @qcode{Data} for a series with ## no name, made unique with the suffixes @qcode{_1}, @qcode{_2}, @dots{} in ## order. A name reserved in a timetable, @qcode{Properties}, @qcode{Time} ## or @qcode{Variables}, takes a suffix too, with a warning; MATLAB raises ## for the last two. The data are laid with time along the first ## dimension, so data whose time runs along its last dimension is permuted; ## a series whose samples then need more than two dimensions is refused, ## since a timetable here holds two-dimensional variables only, where ## MATLAB gives a variable of more. ## ## @qcode{DataInfo.Units} becomes @qcode{VariableUnits}, and the ## interpolation method @qcode{VariableContinuity}: @qcode{'linear'} is ## @qcode{'continuous'} and @qcode{'zoh'} is @qcode{'step'}; a series with an ## interpolation function is refused. @qcode{Properties.UserData} is the ## @qcode{UserData} of the first series that has any. ## ## The events of the series become the @qcode{Properties.Events} of the ## timetable: an @code{eventtable} of their times, with their names as ## @qcode{EventLabels} and, when any event has some, their @qcode{EventData} ## as a variable; an event held by several series appears once. MATLAB drops ## the events, with a warning. Quality codes are dropped, with a warning, as ## in MATLAB. ## ## @seealso{timeseries, tscollection, timetable, eventtable} ## @end deftypefn function TT = timeseries2timetable (varargin) if (nargin < 1) error ("timeseries2timetable: invalid number of input arguments."); endif ## The series, one per variable list = {}; for i = 1:nargin x = varargin{i}; if (isa (x, 'timeseries')) if (nargin > 1 && ! isscalar (x)) error (strcat ("timeseries2timetable: with several inputs, each", ... " timeseries must be a single series.")); endif for k = 1:numel (x) list{end+1} = x(k); endfor elseif (isa (x, 'tscollection')) members = gettimeseriesnames (x); for k = 1:numel (members) list{end+1} = subsref (x, struct ('type', '.', 'subs', members{k})); endfor else error (strcat ("timeseries2timetable: every input must be a", ... " timeseries or a tscollection.")); endif endfor if (isempty (list)) error ("timeseries2timetable: there is no series to convert."); endif ## One time vector for all first = list{1}; if (any (cellfun (@(ts) ts.Length == 0, list))) error ("timeseries2timetable: every series must have samples."); endif if (! all (cellfun (@(ts) ts.Length == first.Length, list))) error (strcat ("timeseries2timetable: every series must have the same", ... " number of samples; convert each to a timetable and", ... " combine them with synchronize.")); endif ti = first.TimeInfo; for i = 2:numel (list) tj = list{i}.TimeInfo; if (! (isequal (list{i}.Time, first.Time) && strcmp (tj.Units, ti.Units) && strcmp (tj.StartDate, ti.StartDate))) error (strcat ("timeseries2timetable: every series must have the", ... " same times, time units and start date; convert", ... " each to a timetable and combine them with", ... " synchronize.")); endif endfor ## The variables nv = numel (list); names = cell (1, nv); values = cell (1, nv); units = cell (1, nv); continuity = cell (1, nv); userData = []; hasQuality = false; for i = 1:nv ts = list{i}; names{i} = ts.Name; if (isempty (names{i}) || strcmp (names{i}, 'unnamed')) names{i} = 'Data'; endif data = ts.Data; if (! ts.IsTimeFirst) data = permute (data, [ndims(data), 1:ndims(data)-1]); endif if (ndims (data) > 2) error (strcat ("timeseries2timetable: the samples of '%s' are not", ... " rows, and a timetable holds two-dimensional", ... " variables only."), names{i}); endif values{i} = data; units{i} = ts.DataInfo.Units; switch (getinterpmethod (ts)) case 'linear' continuity{i} = 'continuous'; case 'zoh' continuity{i} = 'step'; otherwise error (strcat ("timeseries2timetable: the interpolation function", ... " of '%s' has no timetable continuity; set its", ... " method to 'linear' or 'zoh'."), names{i}); endswitch if (isempty (userData)) userData = ts.UserData; endif hasQuality = hasQuality || ! isempty (ts.Quality); endfor reserved = {'Properties', 'Time', 'Variables'}; unique = matlab.lang.makeUniqueStrings (names, reserved); for i = find (ismember (names, reserved)) warning (strcat ("timeseries2timetable: a series named '%s' is", ... " renamed '%s', since the name is reserved in a", ... " timetable."), names{i}, unique{i}); endfor if (hasQuality) warning (strcat ("timeseries2timetable: quality codes cannot be held", ... " by a timetable and are dropped.")); endif rowTimes = timesOf (first.Time, ti); TT = timetable (values{:}, 'RowTimes', rowTimes, 'VariableNames', unique); if (! all (cellfun (@isempty, units))) TT.Properties.VariableUnits = units; endif TT.Properties.VariableContinuity = continuity; TT.Properties.UserData = userData; ## The events of every series, each once labels = {}; eventData = {}; eventTimes = []; for i = 1:nv ts = list{i}; for k = 1:numel (ts.Events) e = ts.Events(k); eUnits = e.Units; if (isempty (eUnits)) eUnits = ti.Units; endif start = e.StartDate; if (isempty (start)) start = ti.StartDate; elseif (isempty (ti.StartDate)) error (strcat ("timeseries2timetable: the event '%s' is dated,", ... " but its series has no start date to place it", ... " on."), e.Name); endif eti = ti; eti.Units = eUnits; eti.StartDate = start; t = timesOf (e.Time, eti); seen = false; for j = 1:numel (labels) if (strcmp (labels{j}, e.Name) && eventTimes(j) == t && isequal (eventData{j}, e.EventData)) seen = true; break; endif endfor if (! seen) labels{end+1,1} = e.Name; eventData{end+1,1} = e.EventData; if (isempty (eventTimes)) eventTimes = t; else eventTimes = [eventTimes; t]; endif endif endfor endfor if (! isempty (labels)) eventTimes.Format = rowTimes.Format; ET = eventtable (eventTimes, 'EventLabels', labels); if (! all (cellfun (@isempty, eventData))) ET.EventData = eventData; endif TT.Properties.Events = ET; endif endfunction ## The times T, in the units of the time metadata TI, as row times: a ## duration shown in those units, or a datetime from TI.StartDate. function rt = timesOf (t, ti) switch (ti.Units) case 'weeks' rt = days (7 * t); fmt = 'd'; case 'days' rt = days (t); fmt = 'd'; case 'hours' rt = hours (t); fmt = 'h'; case 'minutes' rt = minutes (t); fmt = 'm'; case 'seconds' rt = seconds (t); fmt = 's'; case 'milliseconds' rt = seconds (t / 1e3); fmt = 's'; case 'microseconds' rt = seconds (t / 1e6); fmt = 's'; otherwise rt = seconds (t / 1e9); fmt = 's'; endswitch if (isempty (ti.StartDate)) rt.Format = fmt; return; endif rt = datetime (tsdata.timemetadata.dateVector (ti.StartDate)) + rt; if (isempty (ti.Format)) rt.Format = 'dd-MMM-uuuu HH:mm:ss'; else rt.Format = datetimeFormat (ti.Format); endif endfunction ## The datetime format that writes what the datestr format FMT writes. function out = datetimeFormat (fmt) hasAmPm = ! isempty (regexp (fmt, 'AM|PM', 'once')); tokens = {'yyyy', 'uuuu'; 'yy', 'uu'; 'QQ', 'QQQ'; ... 'mmmm', 'MMMM'; 'mmm', 'MMM'; ... 'mm', 'MM'; 'm', 'MMMMM'; 'dddd', 'eeee'; 'ddd', 'eee'; ... 'dd', 'dd'; 'd', 'eeeee'; 'HH', 'HH'; 'MM', 'mm'; ... 'SS', 'ss'; 'FFF', 'SSS'; 'AM', 'a'; 'PM', 'a'}; ## datestr pads a 12-hour clock with a space, which datetime cannot if (hasAmPm) tokens{strcmp (tokens(:,1), 'HH'), 2} = 'h'; endif out = ''; i = 1; while (i <= numel (fmt)) done = false; for k = 1:rows (tokens) tok = tokens{k,1}; if (strncmp (fmt(i:end), tok, numel (tok))) out = [out, tokens{k,2}]; i += numel (tok); done = true; break; endif endfor if (done) continue; endif ## Any other letter is literal text to a datetime format if (isletter (fmt(i))) out = [out, "'", fmt(i), "'"]; else out = [out, fmt(i)]; endif i += 1; endwhile endfunction %!demo %! ## Each series becomes a variable of a timetable. Without a start date %! ## the row times are durations, in the units of the series. %! %! temp = timeseries ([12.1; 14.5; 13.2], [0; 30; 60], 'Name', 'temp'); %! temp.TimeInfo.Units = 'minutes'; %! temp.DataInfo.Units = 'degC'; %! wind = timeseries ([3; 5; 4], [0; 30; 60], 'Name', 'wind'); %! wind.TimeInfo.Units = 'minutes'; %! TT = timeseries2timetable (temp, wind) %! TT.Properties.VariableUnits %!demo %! ## A dated series gives `datetime` row times, and its events become the %! ## timetable's event table; MATLAB drops the events. %! %! ts = timeseries ([3; 5; 4], {'01-Mar-2024', '02-Mar-2024', ... %! '03-Mar-2024'}, 'Name', 'level'); %! ts = addevent (ts, 'inspection', 1); %! TT = timeseries2timetable (ts); %! TT.Properties.RowTimes %! TT.Properties.Events %!demo %! ## A collection gives a variable per member. %! %! tc = tscollection ({timeseries([1; 2], [0; 1], 'Name', 'a'), ... %! timeseries([3; 4], [0; 1], 'Name', 'b')}); %! timeseries2timetable (tc) %!shared A, B, U, DA %! A = timeseries ([1; 2; 3; 4], [0; 1; 2; 3], [1; 2; 3; 4], 'Name', 'alpha'); %! A.DataInfo.Units = 'm'; %! A.UserData = 7; %! B = setinterpmethod (timeseries ([10, 11; 20, 21; 30, 31; 40, 41], ... %! [0; 1; 2; 3], 'Name', 'beta'), 'zoh'); %! U = timeseries ([5; 6; 7; 8], [0; 1; 2; 3]); %! DA = timeseries ([1; 2; 3], {'01-Jan-2024 06:00:00', ... %! '02-Jan-2024 06:00:00', '03-Jan-2024 06:00:00'}, ... %! 'Name', 'da'); ## Test one series %!test %! warning ('off', 'all', 'local'); %! tt = timeseries2timetable (A); %! assert_equal (class (tt), 'timetable'); %! assert_equal (size (tt), [4, 1]); %! assert_equal (tt.Properties.VariableNames, {'alpha'}); %! assert_equal (class (tt.Properties.RowTimes), 'duration'); %! assert_equal (tt.Properties.RowTimes.Format, 's'); %! assert_equal (seconds (tt.Properties.RowTimes), [0; 1; 2; 3]); %! assert_equal (tt.alpha, [1; 2; 3; 4]); %! assert_equal (tt.Properties.VariableUnits, {'m'}); %! assert_equal (tt.Properties.VariableContinuity, {'continuous'}); %! assert_equal (tt.Properties.DimensionNames, {'Time', 'Variables'}); %! assert_equal (tt.Properties.UserData, 7); %! assert_equal (tt.Properties.Description, ''); %!test %! tt = timeseries2timetable (B); %! assert_equal (size (tt.beta), [4, 2]); %! assert_equal (tt.beta, [10, 11; 20, 21; 30, 31; 40, 41]); %! assert_equal (tt.Properties.VariableContinuity, {'step'}); %!assert_equal (timeseries2timetable (U).Properties.VariableNames, {'Data'}) %!assert_equal (timeseries2timetable (timeseries ([1; 2], [0; 1], ... %! 'Name', '')).Properties.VariableNames, {'Data'}) ## Test several series, in order %!test %! warning ('off', 'all', 'local'); %! tt = timeseries2timetable (A, B); %! assert_equal (size (tt), [4, 2]); %! assert_equal (tt.Properties.VariableNames, {'alpha', 'beta'}); %! assert_equal (tt.Properties.VariableUnits, {'m', ''}); %! assert_equal (tt.Properties.VariableContinuity, {'continuous', 'step'}); %!test %! warning ('off', 'all', 'local'); %! tt = timeseries2timetable ([A, B]); %! assert_equal (tt.Properties.VariableNames, {'alpha', 'beta'}); ## Test names made unique in order, case-sensitive, kept verbatim %!assert_equal (timeseries2timetable (U, U).Properties.VariableNames, ... %! {'Data', 'Data_1'}) %!test %! warning ('off', 'all', 'local'); %! tt = timeseries2timetable (A, A, A); %! assert_equal (tt.Properties.VariableNames, {'alpha', 'alpha_1', 'alpha_2'}); %!test %! warning ('off', 'all', 'local'); %! assert_equal (timeseries2timetable (A, U).Properties.VariableNames, ... %! {'alpha', 'Data'}); %!assert_equal (timeseries2timetable (timeseries ([1; 2], [0; 1], 'Name', ... %! 'a'), timeseries ([3; 4], [0; 1], 'Name', 'a_1'), ... %! timeseries ([5; 6], [0; 1], 'Name', 'a')) ... %! .Properties.VariableNames, {'a', 'a_1', 'a_2'}) %!assert_equal (timeseries2timetable (timeseries ([1; 2], [0; 1], 'Name', ... %! 'alpha'), timeseries ([3; 4], [0; 1], 'Name', 'Alpha')) ... %! .Properties.VariableNames, {'alpha', 'Alpha'}) %!assert_equal (timeseries2timetable (timeseries ([1; 2], [0; 1], 'Name', ... %! 'a b-c')).Properties.VariableNames, {'a b-c'}) %!assert_equal (timeseries2timetable (timeseries ([1; 2], [0; 1], 'Name', ... %! 'Data'), timeseries ([3; 4], [0; 1])) ... %! .Properties.VariableNames, {'Data', 'Data_1'}) ## Test reserved names take a suffix; MATLAB raises for 'Time', 'Variables' %!test %! warning ('off', 'all', 'local'); %! tt = timeseries2timetable (timeseries ([1; 2], [0; 1], 'Name', 'Time')); %! assert_equal (tt.Properties.VariableNames, {'Time_1'}); %! tt = timeseries2timetable (timeseries ([1; 2], [0; 1], 'Name', ... %! 'Variables')); %! assert_equal (tt.Properties.VariableNames, {'Variables_1'}); %! tt = timeseries2timetable (timeseries ([1; 2], [0; 1], 'Name', ... %! 'Properties'), ... %! timeseries ([1; 2], [0; 1], 'Name', ... %! 'Properties_1')); %! assert_equal (tt.Properties.VariableNames, {'Properties_2', 'Properties_1'}); %!warning ... %! timeseries2timetable (timeseries ([1; 2], [0; 1], 'Name', 'Time')); ## Test the row times in the units of the series %!test %! m = timeseries ([1; 2; 3], [0; 1.5; 3]); %! m.TimeInfo.Units = 'minutes'; %! tt = timeseries2timetable (m); %! assert_equal (tt.Properties.RowTimes.Format, 'm'); %! assert_equal (minutes (tt.Properties.RowTimes), [0; 1.5; 3]); %!test %! m = timeseries ([1; 2; 3], [0; 1; 2]); %! m.TimeInfo.Units = 'hours'; %! tt = timeseries2timetable (m); %! assert_equal (tt.Properties.RowTimes.Format, 'h'); %! assert_equal (hours (tt.Properties.RowTimes), [0; 1; 2]); %!test %! m = timeseries ([1; 2; 3], [0; 1; 2]); %! m.TimeInfo.Units = 'days'; %! tt = timeseries2timetable (m); %! assert_equal (tt.Properties.RowTimes.Format, 'd'); %! assert_equal (days (tt.Properties.RowTimes), [0; 1; 2]); %!test %! m = timeseries ([1; 2; 3], [0; 1; 2]); %! m.TimeInfo.Units = 'weeks'; %! tt = timeseries2timetable (m); %! assert_equal (tt.Properties.RowTimes.Format, 'd'); %! assert_equal (days (tt.Properties.RowTimes), [0; 7; 14]); %!test %! m = timeseries ([1; 2; 3], [0; 1; 2]); %! m.TimeInfo.Units = 'milliseconds'; %! tt = timeseries2timetable (m); %! assert_equal (tt.Properties.RowTimes.Format, 's'); %! assert_equal (milliseconds (tt.Properties.RowTimes), [0; 1; 2]); %!test %! m = timeseries ([1; 2], [0; 1]); %! m.TimeInfo.Units = 'microseconds'; %! tt = timeseries2timetable (m); %! assert_equal (tt.Properties.RowTimes.Format, 's'); %! assert_equal (seconds (tt.Properties.RowTimes), [0; 1e-6]); %!assert_equal (seconds (timeseries2timetable (timeseries ([1; 2; 3], ... %! [0; 1; 1])).Properties.RowTimes), [0; 1; 1]) ## Test dated row times %!test %! tt = timeseries2timetable (DA); %! assert_equal (class (tt.Properties.RowTimes), 'datetime'); %! assert_equal (tt.Properties.RowTimes.Format, 'dd-MMM-uuuu HH:mm:ss'); %! assert_equal (cellstr (char (tt.Properties.RowTimes)), ... %! {'01-Jan-2024 06:00:00'; '02-Jan-2024 06:00:00'; ... %! '03-Jan-2024 06:00:00'}); %! assert_equal (tt.Properties.RowTimes.TimeZone, ''); %!test %! m = DA; %! m.TimeInfo.Units = 'hours'; %! tt = timeseries2timetable (m); %! assert_equal (cellstr (char (tt.Properties.RowTimes)), ... %! {'01-Jan-2024 06:00:00'; '01-Jan-2024 07:00:00'; ... %! '01-Jan-2024 08:00:00'}); %!test %! p = timeseries ([1; 2; 3], [0; 0.25; 0.5], 'Name', 'p'); %! p.TimeInfo.Units = 'days'; %! p.TimeInfo.StartDate = '15-Mar-2024 10:30:00'; %! tt = timeseries2timetable (p); %! assert_equal (cellstr (datestr (tt.Properties.RowTimes, ... %! 'yyyy-mm-dd HH:MM:SS')), ... %! {'2024-03-15 10:30:00'; '2024-03-15 16:30:00'; ... %! '2024-03-15 22:30:00'}); ## Test the time format is translated; MATLAB fails on it %!test %! m = DA; %! m.TimeInfo.Format = 'yyyy-mm-dd'; %! tt = timeseries2timetable (m); %! assert_equal (tt.Properties.RowTimes.Format, 'uuuu-MM-dd'); %! assert_equal (char (tt.Properties.RowTimes(1)), '2024-01-01'); %!test %! m = DA; %! m.TimeInfo.Format = 'dd/mm/yyyy HH:MM PM'; %! tt = timeseries2timetable (m); %! assert_equal (tt.Properties.RowTimes.Format, 'dd/MM/uuuu h:mm a'); %! assert_equal (char (tt.Properties.RowTimes(1)), '01/01/2024 6:00 AM'); %!test %! m = DA; %! m.TimeInfo.Format = 'QQ-yyyy'; %! tt = timeseries2timetable (m); %! assert_equal (tt.Properties.RowTimes.Format, 'QQQ-uuuu'); %! assert_equal (char (tt.Properties.RowTimes(1)), 'Q1-2024'); %!test %! m = DA; %! m.TimeInfo.Format = 'mmm dd, yyyy'; %! tt = timeseries2timetable (m); %! assert_equal (tt.Properties.RowTimes.Format, 'MMM dd, uuuu'); %! assert_equal (char (tt.Properties.RowTimes(2)), 'Jan 02, 2024'); ## Test data laid time first %!test %! tt = timeseries2timetable (timeseries (reshape (1:8, 2, 1, 4), ... %! [0; 1; 2; 3], 'Name', 'r3')); %! assert_equal (tt.r3, [1, 2; 3, 4; 5, 6; 7, 8]); %!test %! tt = timeseries2timetable (timeseries ([1, 2, 3, 4], [0, 1, 2, 3], ... %! 'Name', 'rv')); %! assert_equal (tt.rv, [1; 2; 3; 4]); %!assert_equal (timeseries2timetable (timeseries ([1, 2, 3], 5, 'Name', ... %! 'row')).row, [1, 2, 3]) %!assert_equal (size (timeseries2timetable (timeseries (7, 2))), [1, 1]) ## Test the class, complex values and NaN are kept %!test %! tt = timeseries2timetable (timeseries (int8 ([1; 2; 3]), [0; 1; 2], ... %! 'Name', 'i'), ... %! timeseries (logical ([1; 0; 1]), [0; 1; 2], ... %! 'Name', 'l')); %! assert_equal (class (tt.i), 'int8'); %! assert_equal (class (tt.l), 'logical'); %!assert_equal (timeseries2timetable (timeseries ([1; NaN; 3], [0; 1; 2], ... %! 'Name', 'n')).n, [1; NaN; 3]) %!assert_equal (timeseries2timetable (timeseries (complex ([1; 2], [3; 4]), ... %! [0; 1], 'Name', 'c')).c, complex ([1; 2], [3; 4])) ## Test UserData is the first series' that has any %!test %! p = timeseries ([1; 2], [0; 1], 'Name', 'p'); %! q = timeseries ([3; 4], [0; 1], 'Name', 'q'); %! q.UserData = 8; %! assert_equal (timeseries2timetable (p, q).Properties.UserData, 8); %! p.UserData = 7; %! assert_equal (timeseries2timetable (p, q).Properties.UserData, 7); %!assert_equal (timeseries2timetable (U).Properties.VariableUnits, {}) ## Test a collection gives a variable per member %!test %! warning ('off', 'all', 'local'); %! tt = timeseries2timetable (tscollection ({A, B})); %! assert_equal (tt.Properties.VariableNames, {'alpha', 'beta'}); %! assert_equal (tt.beta, [10, 11; 20, 21; 30, 31; 40, 41]); %!test %! warning ('off', 'all', 'local'); %! tt = timeseries2timetable (tscollection ({A, B}), U); %! assert_equal (tt.Properties.VariableNames, {'alpha', 'beta', 'Data'}); ## Test events become the timetable's events; MATLAB drops them %!test %! warning ('off', 'all', 'local'); %! a = addevent (A, 'ev', 1.5); %! tt = timeseries2timetable (a); %! ET = tt.Properties.Events; %! assert_equal (class (ET), 'eventtable'); %! assert_equal (seconds (ET.Properties.RowTimes), 1.5); %! assert_equal (ET.EventLabels, {'ev'}); %!test %! e = tsdata.event ('dev', 1); %! e.EventData = 'note'; %! m = addevent (DA, e); %! tt = timeseries2timetable (m); %! ET = tt.Properties.Events; %! assert_equal (char (ET.Properties.RowTimes), '01-Jan-2024 06:00:01'); %! assert_equal (ET.EventData, {'note'}); ## Test an event several series hold appears once %!test %! a = addevent (U, 'ev', 1.5); %! a.Name = 'a'; %! b = addevent (U, 'ev', 2.5); %! b.Name = 'b'; %! tt = timeseries2timetable (tscollection ({a, b})); %! assert_equal (tt.Properties.Events.EventLabels, {'ev'; 'ev'}); %! tt = timeseries2timetable (a, a); %! assert_equal (numel (tt.Properties.Events.EventLabels), 1); %!assert_equal (timeseries2timetable (U).Properties.Events, []) ## Test quality codes are dropped with a warning, as in MATLAB %!warning ... %! timeseries2timetable (A); %!error ... %! timeseries2timetable () %!error ... %! timeseries2timetable (5) %!error ... %! timeseries2timetable (U, 'Foo', 1) %!error ... %! timeseries2timetable ([U, U], U) %!error ... %! timeseries2timetable (tscollection ([0; 1])) %!error ... %! timeseries2timetable (timeseries ()) %!error ... %! timeseries2timetable (U, timeseries ([1; 2; 3], [0; 1; 2])) %!error ... %! timeseries2timetable (U, timeseries ([1; 2; 3; 4], [0; 1; 2; 4])) %!error ... %! m = U; m.TimeInfo.Units = 'minutes'; ... %! timeseries2timetable (U, m) %!error ... %! timeseries2timetable (DA, timeseries ([7; 8; 9], [0; 1; 2])) %!error ... %! timeseries2timetable (timeseries (reshape (1:24, 2, 3, 4), ... %! [0; 1; 2; 3], 'Name', 'd3')) %!error ... %! x = timeseries ([1; 2; 3], [0; 1; 2], 'Name', 'x'); ... %! x.Events = tsdata.event ('dd', '02-Jan-2024'); ... %! timeseries2timetable (x) %!error ... %! m = setinterpmethod (U, @(nt, ot, od) interp1 (ot, od, nt, 'nearest')); ... %! m.Name = 'fh'; ... %! timeseries2timetable (m) pr0m1th3as-datatypes-9450b29/inst/timetable.m000066400000000000000000011147401527370255000210070ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef timetable < tabular ## -*- texinfo -*- ## @deftp {datatypes} timetable ## ## Array of tabular data whose rows are labelled by time. ## ## A timetable collects heterogeneous data into columnar variables exactly ## as a @code{table} does, but labels each of its rows with a time instead ## of a name. The row times are a @code{datetime} or a @code{duration} ## vector, one element per row, and they are what the type exists for: rows ## can be selected, aligned and resampled by when they happened. ## ## Unlike the row names of a @code{table}, the row times need not be unique, ## sorted, or present; duplicates, a descending order and missing times are ## all accepted and merely make the timetable irregular. ## ## A timetable is @emph{regular} when its rows are evenly spaced. The ## spacing is reported by the @qcode{TimeStep} property and its reciprocal ## @qcode{SampleRate}, and a timetable that is not evenly spaced reports a ## @qcode{NaN} time step. A timetable told its step at construction, by ## @qcode{'TimeStep'} or @qcode{'SampleRate'}, remembers it even where a ## subset is too short to imply one, so a single row taken out of it is ## still hourly; one that read its step off the row times it was given has ## nothing to fall back on and reports @qcode{NaN} there. ## ## A timetable is not a @code{table} and neither is a subclass of the ## other; both derive from the same abstract class, so @code{istabular} is ## true for either while @code{istable} and @code{istimetable} each single ## one of them out. ## ## Assigning an empty matrix to a subscripted timetable deletes rows or ## variables exactly as it does for a @code{table}, and the row times go ## with the rows they label. The row times themselves cannot be deleted. ## ## Assigning to a subscripted timetable with parentheses or curly ## brackets, or to its variable dimension name, writes values and ## creates variables and rows as it does for a @code{table}; a row added ## this way has a missing row time, or the next one along a step given ## when the timetable was created. A timetable assigned with parentheses ## must carry the row times of the rows it is assigned to, and is refused ## otherwise, where MATLAB pastes its rows by position and ignores their ## times; @code{synchronize} aligns two timetables first. ## ## Besides the @code{timetable} constructor, you can also use ## @code{table2timetable} and @code{array2timetable} to create timetables ## from the respective data types. ## ## @code{eventtable} derives from this class and inherits every method ## documented here; where one of them behaves differently for an event ## table, its own documentation says so. ## ## @seealso{table, eventtable, istimetable, istabular, timetable.isregular, ## datetime, duration} ## @end deftp properties ## -*- texinfo -*- ## @deftp {timetable} {property} RowTimes ## ## Row times ## ## Row times, specified as a @code{datetime} or @code{duration} vector ## with one element for each row of the timetable. They are stored as a ## column vector whatever shape they are given in. Unlike the row names ## of a table they need not be unique, sorted or present: duplicate ## times, a descending order and missing times are all accepted, and each ## of them merely makes the timetable irregular. You can access them ## with @qcode{@var{tt}.Properties.RowTimes} and assign them the same ## way, which recomputes @qcode{TimeStep}. ## ## @end deftp RowTimes = [] ## -*- texinfo -*- ## @deftp {timetable} {property} StartTime ## ## Time of the first row ## ## Time of the first row, of the same type as @qcode{RowTimes}. It ## follows the first row: deleting the first row leaves @qcode{StartTime} ## naming the row that is now first. A timetable left with no rows keeps ## the value it had. Assigning it shifts every row time by the same ## amount and preserves @qcode{TimeStep}. ## ## @end deftp StartTime = [] ## -*- texinfo -*- ## @deftp {timetable} {property} SampleRate ## ## Sampling rate in hertz ## ## Sampling rate, specified as a positive numeric scalar, being the ## reciprocal of @qcode{TimeStep} measured in seconds. It is ## @qcode{NaN} whenever the time step is a @code{calendarDuration}, ## a calendar step having no fixed length in seconds, and whenever the ## timetable is irregular. Assigning it regenerates the row times from ## @qcode{StartTime}, as assigning @qcode{TimeStep} does. ## ## @end deftp SampleRate = [] ## -*- texinfo -*- ## @deftp {timetable} {property} TimeStep ## ## Spacing between row times ## ## Spacing between consecutive row times, specified as a @code{duration} ## or @code{calendarDuration} scalar, or a @qcode{NaN} duration when the ## timetable is irregular. Two rows or more always imply a step of their ## own and it is read off them afresh, so a subset with a gap in it steps ## by nothing and a reversed one steps backwards. Fewer than two rows ## imply nothing, and there it matters how the step was arrived at: one ## given by @qcode{'TimeStep'} or @qcode{'SampleRate'} is remembered, ## while one read off the row times is not and becomes @qcode{NaN}, its ## class resetting to @code{duration} with it. A freshly built one-row ## timetable has no step either way. A negative step is as regular as a ## positive one. Assigning it regenerates the row times from ## @qcode{StartTime}, even when the timetable was irregular. ## ## @end deftp TimeStep = [] ## -*- texinfo -*- ## @deftp {timetable} {property} Events ## ## Event table attached to this timetable ## ## Event table attached to this timetable, specified as an ## @code{eventtable}, or empty when no events are attached. A plain ## @code{timetable} may be assigned and is converted, though no variable ## of it is guessed to hold the event labels; assigning @code{[]} detaches ## whatever was there. The event table's row times must be of the same ## type as this timetable's, so a duration-keyed timetable takes a ## duration-keyed event table and a datetime-keyed one takes a ## datetime-keyed event table. Nothing requires the events to lie within ## the span of the row times, and events entirely outside it are accepted. ## ## An operation that preserves the rows carries the event table through ## unchanged: it is never subsetted, filtered, re-timed or reordered along ## with them. ## ## An @code{eventtable} inherits this property but cannot read or assign ## it, an event table not being something that can itself carry events. ## ## @end deftp Events = [] endproperties properties (Access = protected) ## Whether the time step was declared rather than inferred. It is set ## when a step is given, by 'TimeStep' or by 'SampleRate', and cleared ## when the row times are given instead and the step read off them. It ## decides one thing only: what becomes of the step when a result is left ## with fewer than two rows and there is no spacing left to read. A ## declared step is remembered there, an inferred one is not. StepDeclared = false endproperties ################################################################################ ## ** Subclass hooks ** ## ################################################################################ ## ## ## The eleven hooks 'tabular' declares, implemented for a timetable, whose ## ## rows are labelled by 'RowTimes' and always carry labels. ## ## ## ################################################################################ methods (Access = protected) ## Always true. A timetable's row times are not optional the way a ## table's row names are: every row has one, even if it is missing. function tf = hasRowLabels (this) tf = true; endfunction ## The 'RowTimes' vector exactly as stored, a datetime or duration ## column with one element per row. function out = getRowLabels (this) out = this.RowTimes; endfunction ## The name of the row dimension, which is what the row times are known ## by. Unlike a table, whose labels answer to the fixed 'RowNames', a ## timetable's answer to whatever 'DimensionNames{1}' says, and that is ## taken from the name of the variable they were built from. function out = rowLabelName (this) out = this.DimensionNames{1}; endfunction ## The row times rendered for display, as a column cellstr. Row times ## are not text, so unlike a table this hook has real work to do, and it ## renders them in whatever format they carry. function out = rowLabelStrings (this) if (isempty (this.RowTimes)) out = {}; else out = cellstr (this.RowTimes); out = out(:); endif endfunction ## The row dimension name, which heads the column of row times as a ## variable's name heads its own. Unlike a table, whose row names are ## printed bare, a timetable's row times are a named dimension and are ## shown as one. function out = rowLabelHeader (this) out = this.DimensionNames{1}; endfunction ## A timetable orders by its row times, which are named by the row ## dimension and by nothing else. function out = rowLabelKeyNames (this) out = this.DimensionNames(1); endfunction ## Row times given outright, the step read off them afresh. function this = setRowLabels (this, labels) this = applyRowTimes (this, labels, true); endfunction ## The variables alone, as a table: the row times label rows and a table ## has no rows to label. function out = plainTable (this) vals = this.VariableValues; if (isempty (vals)) out = table.empty (height (this), 0); else out = table (vals{:}, 'VariableNames', this.VariableNames); out.Properties.VariableDescriptions = this.VariableDescriptions; out.Properties.VariableUnits = this.VariableUnits; endif endfunction ## The row times are an ordinary grouping key, named by the row ## dimension. function tf = groupsByLabels (this) tf = true; endfunction ## A bare 'sortrows (tt)' orders by the row times. function tf = sortsByLabelsByDefault (this) tf = true; endfunction ## Row times are part of what makes a row distinct. function tf = uniqueIncludesLabels (this) tf = true; endfunction ## A missing row time disqualifies its row: there is no placing such a ## row in time, whatever its variables hold. function tf = usableRowLabels (this) tf = ! ismissing (this.RowTimes); tf = tf(:); endfunction ## The row times are summarised beside the variables, under the row ## dimension's name. They carry no description, units or continuity, and ## report instead where they start, how fast they run and how far apart ## they step. function [name, entry] = summaryLabelEntry (this) name = this.DimensionNames{1}; rt = this.RowTimes; e = struct (); e.Size = size (rt); e.Type = class (rt); if (isdatetime (rt)) e.TimeZone = rt.TimeZone; endif e.SampleRate = this.SampleRate; e.StartTime = this.StartTime; entry = tabular.summaryStats (e, rt); entry.TimeStep = this.TimeStep; endfunction ## A timetable interpolates against its row times, measured in seconds ## from the first of them. A row time that is missing places nothing, so ## the whole object is refused rather than filled around the gap. function [x, ownPoints, errmsg] = fillSamplePoints (this) x = []; ownPoints = true; errmsg = ''; rt = this.RowTimes; if (any (ismissing (rt))) errmsg = "row times must not be missing."; return endif if (isempty (rt)) x = zeros (0, 1); elseif (isdatetime (rt)) x = seconds (rt - rt(1)); else x = seconds (rt); endif x = x(:); endfunction ## The four properties a timetable publishes about its row times, keyed ## by the names they are published under. 'RowTimes' is the stored ## truth; the other three describe it and are maintained with it. function out = rowLabelProperties (this) out = struct (); out.RowTimes = this.RowTimes; out.StartTime = this.StartTime; out.SampleRate = this.SampleRate; out.TimeStep = this.TimeStep; out.Events = eventsOf (this); endfunction ## The attached event table, read through an accessor rather than off the ## property. An 'eventtable' inherits the property but must never hold ## anything in it, so nothing inherited from here names it directly. function out = eventsOf (this) out = this.Events; endfunction function this = detachEvents (this) this.Events = []; endfunction ## The attached event table assigned, through the same seam. VAL is ## whatever the user gave and is validated here. function this = setEventsOf (this, val) this.Events = checkEvents (this, val); endfunction ## A variable named by one of the three '...Variable' options of ## 'extractevents': its name, its values at the extracted rows, and its ## index, which is what the second output loses. function [name, col, ix] = takenVariable (this, ref, ixRows, opt) ix = resolveVarRef (this, ref); if (numel (ix) != 1) error (strcat ("timetable.extractevents: '%s' must name exactly", ... " one variable; %d were named."), opt, numel (ix)); endif name = this.VariableNames{ix}; col = getvar (this, ix); col = col(ixRows,:); endfunction ## The event tables of OPS carried onto TBL, which is the result of a ## binary operation over them. One operand carrying events hands them ## over unchanged; two or more are merged; none leaves the result with ## none. Returns an errmsg body for the caller to raise under its own ## name. function [tbl, errmsg] = carryEvents (tbl, ops) errmsg = ''; ev = []; for i = 1:numel (ops) if (! isa (ops{i}, 'tabular')) continue; endif evi = eventsOf (ops{i}); ## An attached event table with no events is still attached, so the ## test is on the class and not on emptiness. if (! isa (evi, 'eventtable')) continue; endif if (! isa (ev, 'eventtable')) ev = evi; else [ev, errmsg] = mergeEventTables (ev, evi); if (! isempty (errmsg)) return; endif endif endfor tbl = setEventsOf (tbl, ev); endfunction ## Two event tables merged, which is an outer join keyed on the row times ## together with every variable the two share. A key found on both sides ## yields one row, a key found on one yields a row whose variables from ## the other side are missing, and the result is ordered by those same ## keys, so operand order does not reach the answer. Variables the two ## do not share are no obstacle; the three event properties are, and must ## agree. function [ev, errmsg] = mergeEventTables (evA, evB) ev = []; ## 'Properties' is synthesised by 'subsref' and a method's dot access ## does not go through it, so the metadata is read as a struct here. PA = getProperties (evA); PB = getProperties (evB); errmsg = mergeableEvents (PA, PB); if (! isempty (errmsg)) return; endif namesA = PA.VariableNames; namesB = PB.VariableNames; common = namesA(ismember (namesA, namesB)); ## The key columns of each side, the row times leading. lCols = {getRowLabels(evA)}; rCols = {getRowLabels(evB)}; for k = 1:numel (common) lCols{end+1} = getvar (evA, common{k}); rCols{end+1} = getvar (evB, common{k}); endfor [pL, pR, errmsg] = tabular.joinProxies (lCols, rCols); if (! isempty (errmsg)) return; endif ## One output row per distinct key, taking the left row where there is ## one. A key repeated within an operand contributes once: an event ## table holding a row twice describes one event twice. nA = height (evA); [~, ~, ic] = unique ([pL; pR], 'rows'); icL = ic(1:nA); icR = ic(nA+1:end); ng = max (ic); ixA = zeros (ng, 1); ixB = zeros (ng, 1); for g = 1:ng a = find (icL == g, 1); b = find (icR == g, 1); if (! isempty (a)) ixA(g) = a; endif if (! isempty (b)) ixB(g) = b; endif endfor [ev, errmsg] = joinBuildSide (evA, ixA); if (! isempty (errmsg)) return; endif ## A row that came from the right alone has no row time and no key ## values from the left, and both are known: they are what matched ## nothing, so the right side carries them. rtA = getRowLabels (evA); rtB = getRowLabels (evB); rt = rtA(max (ixA, 1)); fromB = (ixA == 0); if (any (fromB)) rt(fromB) = rtB(ixB(fromB)); for k = 1:numel (common) col = getvar (ev, common{k}); src = getvar (evB, common{k}); col(fromB,:) = src(ixB(fromB),:); ev = setvar (ev, common{k}, col); endfor endif ev = setRowLabels (ev, rt); ## The variables only the right side has, filled where the left had no ## row of its own. bOnly = namesB(! ismember (namesB, namesA)); if (! isempty (bOnly)) ixOnly = resolveVarRef (evB, bOnly); [Bout, errmsg] = joinBuildSide (subsetvars (evB, ixOnly), ixB); if (! isempty (errmsg)) return; endif for k = 1:numel (bOnly) ev = setvar (ev, bOnly{k}, getvar (Bout, k)); endfor endif ev = sortrows (ev, [{PA.DimensionNames{1}}, common]); endfunction ## One of those four assigned. Each of them writes through to the ## others, since all four describe the same vector: new row times imply ## a new step, and a new step, rate or start regenerates the row times. function [this, handled] = setRowLabelProperty (this, name, val, chain_s) handled = true; switch (name) case 'RowTimes' val = checkRowTimes (val, height (this)); this.StepDeclared = false; this = applyRowTimes (this, val, true); case 'StartTime' val = checkStartTime (val); this = applyRowTimes (this, reanchored (this, val), false); case 'TimeStep' val = checkTimeStep (val, this.StartTime); rt = steppedTimes (this.StartTime, val, height (this)); this.StepDeclared = true; this = applyRowTimes (this, rt, true, val); case 'SampleRate' val = seconds (1 / checkSampleRate (val)); rt = steppedTimes (this.StartTime, val, height (this)); this.StepDeclared = true; this = applyRowTimes (this, rt, true, val); case 'Events' this = setEventsOf (this, val); otherwise handled = false; endswitch endfunction ## Keeps the row times picked out by IXROWS, in the order given, and ## brings the step back into agreement with them. A result of two rows ## or more implies a step of its own, so it is read off afresh: a ## reversed subset of an hourly timetable steps by minus one hour and a ## subset with a gap in it steps by nothing. A shorter result implies ## nothing, and there the step is kept only if it was declared: a single ## row taken out of a timetable built with 'TimeStep' stays hourly, one ## taken out of a timetable built from its row times does not. function this = subsetRowLabels (this, ixRows) rt = this.RowTimes(ixRows); this = applyRowTimes (this, rt, numel (rt) > 1); endfunction ## Drops the row times, leaving an empty vector of the type they had and ## no step. A timetable cannot really be without them, so this is only ## reached where a result carries no rows to label. function this = clearRowLabels (this) this.RowTimes = this.RowTimes([]); this.TimeStep = seconds (NaN); this.SampleRate = NaN; endfunction ## Matches ROWREF against the row times and returns the rows it picks ## out. A reference may be a range of times or a tolerant match, each of ## which knows how to select from a vector of times; or a datetime or ## duration vector, or text that converts to one, which matches exactly. ## Row times need not be unique, so one reference may pick out several ## rows, and they come back in reference order. Raises naming every ## reference that matches none. function [ixRows, newLabels] = resolveRowRef (this, rowRef) newLabels = rowRef([]); ## A range whose bounds are event filters reads them against this ## timetable, which is what carries the events they name. if (isa (rowRef, 'timerange') && hasEventBounds (rowRef)) rowRef = resolveEventBounds (rowRef, this); endif if (isa (rowRef, 'timerange') || isa (rowRef, 'withtol')) ixRows = rowIndices (rowRef, this.RowTimes); return endif ## A row time is matched against the times there are, not read as a ## position, so a time the timetable does not carry simply matches ## nothing. Reading it gives no rows; assigning to it asks for the ## row, and the caller takes the second output to add it. A NaN ## marks where an unmatched time stood, so the caller can put the row ## it adds in the place the reference named. ref = rowRefTimes (rowRef, this.RowTimes); ixRows = zeros (0, 1); keep = true (numel (ref), 1); for i = 1:numel (ref) hit = find (this.RowTimes == ref(i)); if (isempty (hit)) keep(i) = false; ixRows = [ixRows; NaN]; else ixRows = [ixRows; hit(:)]; endif endfor if (nargout > 1) newLabels = ref(! keep); else ixRows = ixRows(! isnan (ixRows)); endif endfunction ## Row times need not be unique, so a repeated row simply repeats its ## time; the step is recomputed from the result, which a repetition ## generally makes irregular. function this = growRowLabels (this, n) rt = this.RowTimes; nrow = numel (rt); if (n <= nrow) return; endif step = this.TimeStep; if (nrow > 0 && this.StepDeclared && ! any (ismissing (step))) ## A declared step says what the row after the last one is, so the ## grid goes on rather than breaking. A step only read off the rows ## says nothing about a row that was never there. add = repmat (rt(nrow), [n - nrow, 1]); t = rt(nrow); for k = 1:(n - nrow) t = t + step; add(k) = t; endfor this = applyRowTimes (this, [rt(:); add(:)], true, step); else add = missingTimes (rt, n - nrow); this = applyRowTimes (this, [rt(:); add(:)], true); endif endfunction function this = repeatRowLabels (this, n, elementwise) nrow = numel (this.RowTimes); if (elementwise) ix = repelem ((1:nrow)', n, 1); else ix = repmat ((1:nrow)', n, 1); endif this = applyRowTimes (this, this.RowTimes(ix), true); endfunction ## A timetable built from an apply method's output. Each output row takes ## the row time of the input row ROWIX names it came from; with no index ## to go on the rows take the row times from the top of the input. ## ROWLABELS means nothing here, the row times following the index. function out = assembleApply (this, vars, names, rowLabels, rowIx, ... caller) if (isempty (vars)) nrows = 0; else nrows = size (vars{1}, 1); endif ## Every row of a timetable is labelled by a row time, and there are ## only as many of those as the timetable has rows. A result longer ## than that, or one carrying rows a group had nothing to match, is ## refused rather than answered with a time repeated to fill the gap. if (nrows > height (this) || any (isnan (rowIx(:)))) error (strcat ("%s: the result has rows the timetable has no row", ... " time for; ask for it as a table with", ... " 'OutputFormat', 'table'."), caller); endif if (isempty (rowIx)) ## With no index to go on the result takes the first row times, one ## per row it has: a reduction to a single row takes the first, and a ## function returning several takes as many from the top. n = min (nrows, height (this)); rowIx = (1:n)'; if (n < nrows && n > 0) rowIx = [rowIx; repmat(n, nrows - n, 1)]; endif endif rt = this.RowTimes(rowIx(:)); out = timetable (vars{:}, 'RowTimes', rt, 'VariableNames', names); if (this.StepDeclared && numel (rt) < 2) ## Too few rows to imply a spacing of their own, and the object this ## was built from had been told one: it keeps that step, exactly as a ## subset too short to imply one does. With two rows or more the ## result's own times say what the step is, so a grouped apply reports ## the spacing of the groups rather than the one it started from. out.StepDeclared = true; out.TimeStep = this.TimeStep; out.SampleRate = this.SampleRate; endif endfunction ## Wraps the metadata struct that 'getProperties' assembles in a ## 'datatypes.tabular.TimetableProperties', the class that adds the four ## row time properties to the shared ones and fixes the order the whole ## set displays in. function out = makeProperties (this) out = datatypes.tabular.TimetableProperties (getProperties (this), ... this.CustomPropTypes); endfunction endmethods ################################################################################ ## ** The row time engine ** ## ################################################################################ ## ## ## 'RowTimes' is the stored truth and 'StartTime', 'SampleRate' and ## ## 'TimeStep' describe it. 'applyRowTimes' is the only writer of all four, ## ## so the four cannot drift apart, and every method that touches a row goes ## ## through it. ## ## ## ################################################################################ methods (Access = protected) ## The row time state set in one operation: RT becomes the row times and ## the other three properties are brought into agreement with it. ## INFERSTEP asks for the step to be read off RT; when it is false the ## stored step is kept, which is what carries a step through a subset ## too short to imply one and through a shift of the start time. ## 'StartTime' follows the first row and, when there is no first row, ## stays as it was. ## ## GIVENSTEP is the step the caller generated RT from, when there was ## one. The step is still read off RT, because a calendar step need not ## reproduce the times it generated and an unreproducible one leaves the ## timetable irregular. But where RT implies any step at all, the ## caller's own object is the one kept: a step given as 'hours (1)' goes ## on reading as one hour rather than as the 01:00:00 a difference of ## datetimes comes out as, and one given as 'hours (24)' stays a ## duration rather than becoming the calendar day it also describes. function this = applyRowTimes (this, rt, inferStep, givenStep) oldStep = this.TimeStep; this.RowTimes = rt(:); if (inferStep) [this.TimeStep, this.SampleRate] = stepOf (this.RowTimes); if (nargin > 3 && (numel (this.RowTimes) < 2 || ! any (ismissing (this.TimeStep)))) this.TimeStep = givenStep; this.SampleRate = stepRate (givenStep); elseif (this.StepDeclared && ! any (ismissing (this.TimeStep))) ## A declared step also names the unit the class goes on reporting ## in, so a step read off a subset reads in that unit too: an ## hourly timetable subset to every other row reports '2 hr', not ## the '02:00:00' a difference of datetimes comes out as. this.TimeStep = carryStepFormat (oldStep, this.TimeStep); endif elseif (numel (this.RowTimes) < 2 && ! this.StepDeclared) ## Fewer than two rows imply no spacing of their own, and an object ## that was never told one has nothing to keep. The class resets ## with the value, so an inferred calendar step becomes a duration. this.TimeStep = seconds (NaN); this.SampleRate = NaN; endif if (! isempty (this.RowTimes)) this.StartTime = this.RowTimes(1); elseif (! this.StepDeclared) ## The start was only ever the first row time and there is none ## left, so it goes missing rather than empty. this.StartTime = missingTimes (this.RowTimes, 1); endif endfunction ## The metadata a stacked operand contributes: whatever the result does ## not carry already, the first input to have set a thing keeping it. function tbl = adoptMetadata (tbl, in) props = getProperties (in); fields = {'VariableDescriptions', 'VariableUnits', ... 'VariableContinuity', 'Description', 'UserData'}; for i = 1:numel (fields) if (isempty (tbl.(fields{i}))) tbl.(fields{i}) = props.(fields{i}); endif endfor if (isempty (tbl.CustomProperties) && ! isempty (props.CustomProperties)) tbl.CustomProperties = props.CustomProperties; tbl.CustomPropTypes = customPropTypes (in); endif endfunction ## The row times re-anchored so that the first of them is ST, which is ## what assigning 'StartTime' does. Every row keeps its offset from the ## first, so an irregular timetable stays exactly as irregular as it was, ## and a calendar-stepped one keeps its calendar offsets: moving a ## monthly timetable to the 15th of a month gives the 15th of every ## following month, where shifting by a fixed number of days would not. ## ## ST need not be of the type the row times already have. Giving a ## datetime to a timetable keyed by elapsed time is how the recording is ## said to have begun on a date, and giving a duration to a dated one ## reduces it back to elapsed time. function rt = reanchored (this, st) rt = this.RowTimes; if (isempty (rt)) rt = st([]); elseif (iscalendarduration (this.TimeStep)) rt = steppedTimes (st, this.TimeStep, numel (rt)); else rt = st + (rt - rt(1)); endif endfunction ## The body behind 'lag'. N is the shift as the caller gave it, which ## may be a count of rows, a duration or a calendarDuration. Returns an ## errmsg body for the caller to raise under its own name. function [tt, errmsg] = lagResult (this, n) tt = this; [k, errmsg] = lagCount (this, n); if (! isempty (errmsg)) return endif ## Where each row takes its value from, 0 for a row that has nothing ## to take. nr = height (this); si = (1:nr)' - k; si(si < 1 | si > nr) = 0; for j = 1:width (this) src = this.VariableValues{j}; if (ischar (src)) ## A character matrix has no missing value, and MATLAB blanks the ## row rather than refusing the variable. col = src(max (si, 1), :); col(si == 0, :) = " "; else [B, errmsg] = joinBuildSide (subsetvars (this, j), si); if (! isempty (errmsg)) return endif col = B.VariableValues{1}; endif tt.VariableValues{j} = col; endfor endfunction ## How many rows a lag moves the data by. A count says so directly; a ## duration says it in time, which the step turns into rows; and a ## calendarDuration says it in a calendar unit, which only a timetable ## regular in that unit can answer at all. function [k, errmsg] = lagCount (this, n) k = 0; errmsg = ''; if (isa (n, 'calendarDuration')) if (! isscalar (n)) errmsg = "the lag must be a scalar."; return endif [unit, per, errmsg] = lagCalendarUnit (n); if (! isempty (errmsg)) return endif if (! isregular (this, unit)) errmsg = sprintf (strcat ("the timetable must be regular with", ... " respect to %s."), unit); return endif step = calendarStepOf (this.RowTimes); [~, sper, errmsg] = lagCalendarUnit (step); if (! isempty (errmsg)) return endif if (sper == 0 || mod (per, sper) != 0) errmsg = strcat ("the lag must be a multiple of the time step", ... " of the timetable."); return endif k = per / sper; return endif if (isduration (n)) if (! (isscalar (n) && ! ismissing (n))) errmsg = "the lag must be a scalar."; return endif if (! isregular (this)) errmsg = strcat ("the timetable must be regular with respect", ... " to time."); return endif step = this.TimeStep; if (iscalendarduration (step)) ## A calendar step has no fixed length, so a lag given as a ## duration can only be counted where the step is a whole number ## of days and the lag is too. [unit, sper, errmsg] = lagCalendarUnit (step); if (! isempty (errmsg)) return endif if (! strcmp (unit, 'days')) errmsg = strcat ("a lag given as a duration cannot be", ... " counted against a step in months."); return endif r = days (n) / sper; else r = seconds (n) / seconds (step); endif if (abs (r - round (r)) > 4 * eps (abs (r) + 1)) errmsg = strcat ("the lag must be a multiple of the time step", ... " of the timetable."); return endif k = round (r); return endif if (! (isnumeric (n) && isscalar (n) && isreal (n) && isfinite (n) && n == fix (n))) errmsg = strcat ("the lag must be a whole number of rows, a", ... " duration, or a calendarDuration."); return endif if (! isregular (this)) ## A row count moves the data by rows whatever the step measures, so ## a timetable regular in a calendar unit answers here although it is ## not regular with respect to absolute time: a month is a step even ## where no two of them are the same length. cstep = calendarStepOf (this.RowTimes); cunit = ''; if (! isempty (cstep)) cunit = lagCalendarUnit (cstep); endif if (isempty (cunit) || ! isregular (this, cunit)) errmsg = strcat ("the timetable must be regular with respect to", ... " time."); return endif endif k = double (n); ## A lag is measured in time and not in row position, so where the row ## times descend a step later in time is a row earlier in the table and ## the count moves the other way. A lag given as a duration or a ## calendarDuration is already signed, being divided by the step. if (lagDescending (this)) k = -k; endif endfunction ## Whether the row times run backwards, which the step says where it has ## a sign to give and the ends say otherwise. function tf = lagDescending (this) tf = false; ts = this.TimeStep; if (isduration (ts) && ! any (ismissing (ts))) tf = seconds (ts) < 0; return endif rt = this.RowTimes; if (numel (rt) > 1) tf = rt(end) < rt(1); endif endfunction ## The body behind 'synchronize'. ARGS is the whole argument list and ## NAMES the caller's names for them, which are what a clash between two ## variables of the same name is settled by. Returns an errmsg body and ## any warnings for the caller to emit under its own name. function [tt, errmsg, warns] = synchronizeResult (this, args, names) tt = this; errmsg = ''; warns = {}; ## The operands are the timetables the call opens with; everything ## after the first argument that is not one describes what to do. nOp = 0; while (nOp < numel (args) && isa (args{nOp+1}, 'timetable')) nOp += 1; endwhile ops = args(1:nOp); rest = args(nOp+1:end); names = names(1:nOp); o = ops{1}; rt1 = o.RowTimes; for k = 2:nOp o = ops{k}; if (isdatetime (o.RowTimes) != isdatetime (rt1)) errmsg = strcat ("every timetable must have row times of the", ... " same type."); return endif endfor ## The time base, the method and the options, in that order and each ## of them optional. optNames = {'TimeStep', 'SampleRate', 'EndValues', 'IncludedEdge', ... 'Constant'}; spec = 'union'; if (! isempty (rest)) first = rest{1}; if (isa (first, 'string') && isscalar (first)) first = char (first); endif if (isdatetime (first) || isduration (first) || (ischar (first) && isrow (first) && ! any (strcmpi (first, optNames)))) spec = first; rest = rest(2:end); endif endif errmsg = timeBaseError (spec, true); if (! isempty (errmsg)) return endif method = 'default'; if (retimeIsMethod (rest)) method = rest{1}; if (! is_function_handle (method)) method = lower (char (method)); endif rest = rest(2:end); elseif (isempty (rest) && iscell (spec)) errmsg = strcat ("the method must be one name or one function", ... " handle for every timetable."); return endif if (! isempty (rest) && iscell (rest{1})) errmsg = strcat ("the method must be one name or one function", ... " handle for every timetable."); return endif errmsg = retimeOptions (rest, optNames); if (! isempty (errmsg)) return endif dfValues = {missing, missing, 'extrap', missing, missing}; [tstep, srate, endVals, incEdge, konst] = ... parsePairedArguments (optNames, dfValues, rest(:)); [endVals, incEdge, edgeGiven, errmsg] = retimeCheckOptions (endVals, ... incEdge); if (! isempty (errmsg)) return endif [errmsg, warns] = retimeOptionUse (method, ... namedOption (rest, 'Constant'), endVals, edgeGiven); if (! isempty (errmsg)) return endif ## Where the rows of the answer sit. [nt, errmsg] = synchronizeBase (ops, spec, tstep, srate, ... retimeIsAggregation (method), incEdge); if (! isempty (errmsg)) return endif ## Every operand put on that base, then set side by side. Two ## variables of the same name are told apart by the names the caller ## gave the timetables they came from. out = {}; for k = 1:nOp [o, errmsg] = retimeOnto (ops{k}, nt, method, endVals, incEdge, ... konst); if (! isempty (errmsg)) return endif out{k} = o; endfor out = synchronizeRename (out, names); ## The pieces are combined with 'horzcat', which would merge their ## event tables and raise under its own name for a call the user made ## to 'synchronize'. The public method carries the events from the ## operands themselves, so the pieces go in carrying none. for k = 1:nOp out{k} = detachEvents (out{k}); endfor tt = out{1}; for k = 2:nOp tt = horzcat (tt, out{k}); endfor given = retimeGivenStep (tstep, srate); if (! isempty (given)) tt = applyRowTimes (tt, nt, true, given); endif endfunction ## The argument surface of 'retime': the target the caller named, the ## method if one was given, and the options. The method is positional ## and optional at once, so a name in its place is read as the method ## only when it names one or nothing follows it, as in MATLAB, which is ## how a call can leave the method out entirely. ## Returns an errmsg body for the caller to raise under its own name. function [tt, errmsg, warns] = retimeResult (this, args) tt = this; errmsg = ''; warns = {}; if (isempty (args)) errmsg = "too few input arguments."; return endif spec = args{1}; rest = args(2:end); errmsg = timeBaseError (spec, false); if (! isempty (errmsg)) return endif optNames = {'TimeStep', 'SampleRate', 'EndValues', 'IncludedEdge', ... 'Constant'}; method = 'default'; if (retimeIsMethod (rest)) method = rest{1}; if (! is_function_handle (method)) method = lower (char (method)); endif rest = rest(2:end); endif errmsg = retimeOptions (rest, optNames); if (! isempty (errmsg)) return endif dfValues = {missing, missing, 'extrap', missing, missing}; [tstep, srate, endVals, incEdge, konst] = ... parsePairedArguments (optNames, dfValues, rest(:)); [endVals, incEdge, edgeGiven, errmsg] = retimeCheckOptions (endVals, ... incEdge); if (! isempty (errmsg)) return endif [errmsg, warns] = retimeOptionUse (method, ... namedOption (rest, 'Constant'), endVals, edgeGiven); if (! isempty (errmsg)) return endif [nt, errmsg] = retimeTimes (this.RowTimes, spec, tstep, srate, ... retimeIsAggregation (method), incEdge); if (! isempty (errmsg)) return endif [tt, errmsg] = retimeOnto (this, nt, method, endVals, incEdge, konst); if (isempty (errmsg)) ## A step the caller named is the unit the result reports in, so an ## hourly grid asked for as 'TimeStep' reads '1 hr' and not ## '01:00:00', and a rate of 2 Hz reads '0.5 sec'. given = retimeGivenStep (tstep, srate); if (! isempty (given)) tt = applyRowTimes (tt, nt, true, given); endif endif endfunction ## The body behind 'retime', and the per-operand half of 'synchronize'. ## NT is the target row times already resolved, METHOD a fill or an ## interpolation method, and ENDVALS the 'EndValues' option. Returns an ## errmsg body for the caller to raise under its own name. ## ## Each family of methods reaches its values a different way, which is ## why the variables are rebuilt one at a time rather than the whole ## object filled at once. A neighbouring value is found by looking the ## target time up among the row times, and needs no missing value on the ## way, so it serves a cell or a char variable that has none. ## Interpolation goes through the fill engine, which does need one. function [tt, errmsg] = retimeOnto (this, nt, method, endVals, ... incEdge, konst) tt = this; errmsg = ''; rt = this.RowTimes; nt = nt(:); nOld = numel (rt); nNew = numel (nt); [mv, errmsg] = retimeMethods (this, method); if (! isempty (errmsg)) return endif interps = {'linear', 'spline', 'pchip', 'makima'}; neighbours = {'previous', 'next', 'nearest'}; ## Two of the aggregations name a row by where it comes rather than by ## what it holds, so they need the row times ordered as the filling ## methods do, although they gather rather than fill. ordered = {'firstvalue', 'lastvalue'}; aggs = timetable.retimeAggregations (); nv = width (this); isInterp = false (1, nv); isAgg = false (1, nv); isNeigh = false (1, nv); isOrder = false (1, nv); isFilled = false (1, nv); for j = 1:nv if (is_function_handle (mv{j})) isAgg(j) = true; else isInterp(j) = any (strcmp (mv{j}, interps)); isNeigh(j) = any (strcmp (mv{j}, neighbours)); isAgg(j) = any (strcmp (mv{j}, aggs)); isOrder(j) = any (strcmp (mv{j}, ordered)); endif ## A value that was missing before the call is a gap like any other ## and is filled with them, so a method that fills goes through the ## fill engine, which knows how to skip one. A type with no missing ## value can have no such gap and takes the shorter route, which is ## also the only one open to it. isFilled(j) = (isInterp(j) || isNeigh(j)) ... && retimeHasMissing (this.VariableValues{j}); endfor ## What the row times must say depends on what is asked of them. ## Interpolation reads them as sample points and needs each row placed ## once; a neighbouring value needs only an order to look along; and an ## aggregation puts rows into bins, which repeats and disorder do not ## disturb. if (nOld > 1) if (any (isInterp) && numel (unique (rt)) != nOld) errmsg = sprintf (strcat ("the row times must be unique when", ... " retiming with '%s'."), ... retimeMethodName (method)); return endif if ((any (isInterp) || any (isNeigh) || any (isOrder)) && ! (issorted (rt) || issorted (flipud (rt)))) errmsg = sprintf (strcat ("the row times must be sorted when", ... " retiming with '%s'."), ... retimeMethodName (method)); return endif endif ## A variable an interpolating method cannot take is refused here ## rather than by the fill engine, so that the complaint names the ## method the caller asked for and the variable that does not suit it. for j = find (isInterp) v = this.VariableValues{j}; if (! (isnumeric (v) || isdatetime (v) || isduration (v))) errmsg = sprintf (strcat ("the '%s' method takes numeric,", ... " datetime and duration variables only, and", ... " '%s' is of class '%s'."), mv{j}, ... this.VariableNames{j}, class (v)); return endif endfor F = []; if (any (isFilled) && nOld > 0 && nNew > 0) [F, errmsg] = retimeFilled (this, find (isFilled), mv, nt, endVals); if (! isempty (errmsg)) return endif endif ## The result is shaped from a row of the source and then overwritten ## variable by variable, no row of it being a row of the source. if (nOld == 0) tt = subsetrows (this, zeros (0, 1)); else tt = subsetrows (this, ones (nNew, 1)); endif ## Which target bin each row falls into, worked out once for every ## aggregating variable since they all share the grid. bins = []; if (any (isAgg) && nOld > 0 && nNew > 0) [bins, errmsg] = retimeBins (rt, nt, incEdge); if (! isempty (errmsg)) return endif endif ivars = find (isFilled); for j = 1:width (this) src = this.VariableValues{j}; if (isFilled(j) && nOld > 0 && nNew > 0) tt.VariableValues{j} = F.VariableValues{find (ivars == j)}; continue endif if (isAgg(j) && nOld > 0 && nNew > 0) [col, errmsg] = retimeAggregated (src, bins, nNew, mv{j}, ... this.VariableNames{j}); if (! isempty (errmsg)) return endif tt.VariableValues{j} = col; continue endif ## A row the target keeps takes the value it had; a row the target ## adds takes the constant, which needs no missing value of its own ## and so serves a variable that has none. if (strcmp (mv{j}, 'fillwithconstant') && nOld > 0 && nNew > 0) errmsg = retimeConstantFits (konst, src, this.VariableNames{j}); if (! isempty (errmsg)) return endif si = retimeNeighbor (rt, nt, 'exact'); col = src(max (si, 1), :); fill = konst; if (isa (konst, 'missing')) ## The missing value itself has to be turned into the variable's ## own before it can be put in one. [fill, errmsg] = retimeMissingRow (src, 1); if (! isempty (errmsg)) return endif endif ## A value that was already missing is a gap like any other and ## takes the constant with the rows the target adds. gap = si == 0; if (retimeHasMissing (src)) gap = gap | any (__varmissing__ (col), 2); endif col(gap, :) = fill; tt.VariableValues{j} = col; continue endif ## Rows taken from the source where there is one and built missing ## where there is not, which is the same question an outer join asks ## of its unmatched rows and is answered in the same place. if (nOld == 0 || nNew == 0 || any (strcmp (mv{j}, {'fillwithmissing', 'fillwithconstant'}))) if (nOld > 0 && nNew > 0 && (islogical (src) || (iscell (src) && ! iscellstr (src)))) errmsg = sprintf (strcat ("the 'fillwithmissing' method needs", ... " a variable with a missing value, and", ... " '%s' is of class '%s'."), ... this.VariableNames{j}, class (src)); return endif si = zeros (nNew, 1); if (nOld > 0 && nNew > 0) si = retimeNeighbor (rt, nt, 'exact'); endif else si = retimeNeighbor (rt, nt, mv{j}); endif [B, errmsg] = joinBuildSide (subsetvars (this, j), si); if (! isempty (errmsg)) return endif col = B.VariableValues{1}; ## A constant 'EndValues' answers for every target outside the span ## of the row times, whether or not the method found it a value ## there; only 'extrap' leaves the method's own answer standing. if (nOld > 0 && nNew > 0 && ! (ischar (endVals) && strcmp (endVals, 'extrap'))) out = nt < min (rt) | nt > max (rt); if (any (out)) try col(out,:) = endVals; catch errmsg = sprintf (strcat ("the 'EndValues' constant cannot", ... " be assigned to variable '%s' of class", ... " '%s'."), this.VariableNames{j}, ... class (col)); return end_try_catch endif endif tt.VariableValues{j} = col; endfor tt = applyRowTimes (tt, nt, true); endfunction ## The variables of THIS that a fill method serves, filled onto the target ## times. The added rows are placed among the existing ones and left ## missing, so that a row the target adds and a value that was missing ## before the call are the same kind of gap and are filled alike. function [F, errmsg] = retimeFilled (this, ivars, mv, nt, endVals) F = []; errmsg = ''; rt = this.RowTimes; nOld = numel (rt); sub = subsetvars (this, ivars); [ut, ~, back] = unique ([rt(:); nt(:)]); idx = zeros (numel (ut), 1); idx(back(1:nOld)) = (1:nOld)'; [U, errmsg] = joinBuildSide (sub, idx); if (! isempty (errmsg)) return endif U = applyRowTimes (U, ut, true); ## One pass per distinct method, the variables of each named together. done = false (1, numel (ivars)); for k = 1:numel (ivars) if (done(k)) continue endif m = mv{ivars(k)}; same = find (strcmp (mv(ivars), m)); done(same) = true; [U, TF, errmsg] = fillmissingResult (U, m, 'DataVariables', same, ... 'EndValues', endVals); if (! isempty (errmsg)) return endif endfor F = subsetrows (U, back(nOld+1:end)); endfunction ## The method each variable takes. Every method but 'default' applies to ## all of them; 'default' reads 'VariableContinuity', where 'continuous' ## interpolates, 'step' holds the previous value, and 'unset' and 'event' ## are left missing, which is what an unset property leaves every one. function [mv, errmsg] = retimeMethods (this, method) mv = {}; errmsg = ''; if (is_function_handle (method)) mv = repmat ({method}, 1, width (this)); return endif if (any (strcmp (method, retimeKnownMethods ()))) mv = repmat ({method}, 1, width (this)); return endif if (! strcmp (method, 'default')) errmsg = sprintf ("unknown method '%s'.", method); return endif mv = repmat ({'fillwithmissing'}, 1, width (this)); cont = this.VariableContinuity; if (isempty (cont)) return endif mv(strcmp (cont, 'continuous')) = {'linear'}; mv(strcmp (cont, 'step')) = {'previous'}; endfunction endmethods methods (Static, Access = protected) ## The variable names of every operand of a concatenation, and the row ## dimension name the result takes. Also the place both concatenations ## refuse an input that is not tabular at all. A table put before the ## timetable needs no check here: concatenation dispatches on the first ## operand, so such a call reaches 'table' and is refused there. ## ## Metadata is read through 'getProperties' rather than from the ## properties themselves, because a public property read from outside ## its own class goes through 'subsref' and is taken for a variable ## name; a concatenation that mixes the two classes reads a table's. function [names, dimName] = catOperands (args, caller) if (! all (cellfun (@istabular, args))) error (strcat ("timetable.%s: all inputs must be tables or", ... " timetables."), caller); endif names = cell (1, numel (args)); dimName = 'Time'; seen = false; for i = 1:numel (args) props = getProperties (args{i}); names{i} = props.VariableNames; if (! seen && istimetable (args{i}) && ! strcmp (props.DimensionNames{1}, 'Time')) ## An explicit name outranks the default whichever side it is on, ## and the first explicit one wins. dimName = props.DimensionNames{1}; seen = true; endif endfor endfunction endmethods ################################################################################ ## ** Create timetable ** ## ################################################################################ ## Available Methods ## ## ## ## 'timetable' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tt} =} timetable (@var{rowTimes}, @var{var1}, @dots{}, @var{varN}) ## @deftypefnx {timetable} {@var{tt} =} timetable (@var{var1}, @dots{}, @var{varN}, @qcode{'RowTimes'}, @var{rowTimes}) ## @deftypefnx {timetable} {@var{tt} =} timetable (@var{var1}, @dots{}, @var{varN}, @qcode{'TimeStep'}, @var{dt}) ## @deftypefnx {timetable} {@var{tt} =} timetable (@var{var1}, @dots{}, @var{varN}, @qcode{'SampleRate'}, @var{fs}) ## @deftypefnx {timetable} {@var{tt} =} timetable (@dots{}, @qcode{'StartTime'}, @var{t0}) ## @deftypefnx {timetable} {@var{tt} =} timetable (@qcode{'Size'}, @var{sz}, @qcode{'VariableTypes'}, @var{varTypes}, @dots{}) ## @deftypefnx {timetable} {@var{tt} =} timetable (@dots{}, @qcode{'VariableNames'}, @var{varNames}) ## @deftypefnx {timetable} {@var{tt} =} timetable (@dots{}, @qcode{'DimensionNames'}, @var{dimNames}) ## ## Create a new timetable. ## ## @code{@var{tt} = timetable (@var{rowTimes}, @var{var1}, @dots{}, ## @var{varN})} creates a timetable whose rows are labelled by ## @var{rowTimes}, a @code{datetime} or @code{duration} vector with one ## element for each row, and whose variables are the remaining input ## arguments. Variable names are taken from the names of the input ## variables, and the row dimension is named after @var{rowTimes} itself. ## ## @code{@var{tt} = timetable (@var{var1}, @dots{}, @var{varN}, ## @qcode{'RowTimes'}, @var{rowTimes})} does the same with the row times ## given as a Name-Value pair, in which case the row dimension is named ## @qcode{'Time'}. ## ## @code{@var{tt} = timetable (@var{var1}, @dots{}, @var{varN}, ## @qcode{'TimeStep'}, @var{dt})} generates the row times instead of ## taking them, starting at @var{t0} and stepping by @var{dt}, a ## @code{duration} or @code{calendarDuration} scalar. A calendar step ## requires a @code{datetime} start time. ## ## @code{@var{tt} = timetable (@var{var1}, @dots{}, @var{varN}, ## @qcode{'SampleRate'}, @var{fs})} generates them at @var{fs} rows per ## second, which is the same as a @qcode{TimeStep} of ## @code{seconds (1 / @var{fs})}. ## ## @code{@var{tt} = timetable (@dots{}, @qcode{'StartTime'}, @var{t0})} ## sets the time of the first row for either of the two generating ## forms. It defaults to a zero @code{duration} in the units of the ## step, so that a timetable generated without one is keyed by elapsed ## time rather than by date. ## ## @code{@var{tt} = timetable (@qcode{'Size'}, @var{sz}, ## @qcode{'VariableTypes'}, @var{varTypes}, @dots{})} creates a ## preallocated timetable of the given size, filled with the default ## value of each type. @var{sz} must be a two-element numeric array ## giving the number of rows and of variables. The row times must still ## be supplied, by any one of @qcode{'RowTimes'}, @qcode{'TimeStep'} or ## @qcode{'SampleRate'}. ## ## @code{@var{tt} = timetable (@dots{}, @qcode{'VariableNames'}, ## @var{varNames})} specifies the variable names to use, as a cell array ## of character vectors or a string array with one nonempty and unique ## element per variable. ## ## @code{@var{tt} = timetable (@dots{}, @qcode{'DimensionNames'}, ## @var{dimNames})} specifies the two dimension names to use, naming the ## rows and the variables. ## ## @code{@var{tt} = timetable ()} returns an empty timetable with 0 rows ## and 0 variables. ## ## @seealso{table.table2timetable, array2timetable, timetable.isregular} ## @end deftypefn function this = timetable (varargin) ## Return an empty timetable object if (nargin == 0) this.DimensionNames = {'Time', 'Variables'}; this.RowTimes = NaT (0, 1); this.StartTime = NaT; this.TimeStep = seconds (NaN); this.SampleRate = NaN; return endif ## Parse optional Name-Value paired arguments optNames = {'VariableNames', 'DimensionNames', 'RowTimes', ... 'TimeStep', 'SampleRate', 'StartTime'}; dfValues = {{}, {}, missing, missing, missing, missing}; [VariableNames, DimensionNames, RowTimes, TimeStep, SampleRate, ... StartTime, args] = parsePairedArguments (optNames, dfValues, ... varargin(:)); ## The row times are said exactly once, either as the leading ## argument or by one of the three Name-Value pairs. given = [wasGiven(RowTimes), wasGiven(TimeStep), wasGiven(SampleRate)]; leading = ! isempty (args) && ... (isdatetime (args{1}) || isduration (args{1})); if (leading && any (given)) error (strcat ("timetable: row times were given both as the first", ... " argument and as a Name-Value pair.")); endif if (sum (given) > 1) error (strcat ("timetable: only one of 'RowTimes', 'TimeStep' and", ... " 'SampleRate' may be given.")); endif if (! leading && ! any (given)) error (strcat ("timetable: row times are required; give them as", ... " the first argument or with 'RowTimes',", ... " 'TimeStep' or 'SampleRate'.")); endif ## The row dimension is named after the vector the row times came from, ## which only the leading form has a name for; the other forms leave it ## at the default. The name is read before ARGS is shortened. rowDimName = 'Time'; if (leading) inName = inputname (1); if (! isempty (inName)) rowDimName = inName; endif RowTimes = args{1}; args(1) = []; endif ## Check optional Name-Value paired arguments if (! isempty (VariableNames)) if (! (iscellstr (VariableNames) || isa (VariableNames, 'string'))) error (strcat ("timetable: 'VariableNames' must be either a", ... " cell array of character vectors or a string", ... " array.")); endif VariableNames = cellstr (VariableNames); if (any (cellfun (@isempty, VariableNames))) error ("timetable: 'VariableNames' must contain nonempty names."); endif endif if (isempty (DimensionNames)) DimensionNames = {rowDimName, 'Variables'}; endif if (! (iscellstr (DimensionNames) || isa (DimensionNames, 'string')) || numel (DimensionNames) != 2) error (strcat ("timetable: 'DimensionNames' must be either a", ... " two-element cell array of character vectors or", ... " a two-element string array.")); endif this.DimensionNames = cellstr (DimensionNames); ## Dimension names cannot match reserved timetable identifiers reserved = {'Properties', 'RowTimes', 'VariableNames', ':'}; idr = ismember (this.DimensionNames, reserved); if (any (idr)) error (strcat ("timetable: 'DimensionNames' cannot include the", ... " reserved name: '%s'"), this.DimensionNames{idr}); endif ## Check for conflict between VariableNames and DimensionNames idx = ismember (this.DimensionNames, VariableNames); if (any (idx)) error ("timetable: duplicate dimension and variable name: '%s'", ... this.DimensionNames{idx}); endif ## Construct a preallocated timetable with default values preallocated = (numel (args) == 4 && strcmpi (args{1}, 'Size') && strcmpi (args{3}, 'VariableTypes')); if (preallocated) ## Validate the size specifier if (! isnumeric (args{2}) || numel (args{2}) != 2) error ("timetable: 'Size' must be a two-element numeric vector."); endif ## Get number of rows and variables nr = args{2}(1); nv = args{2}(2); ## Get variable types varTypes = args{4}; if (! iscellstr (varTypes) || numel (varTypes) != nv) error (strcat ("timetable: 'VariableTypes' must be a", ... " cellstring array of the same number of", ... " elements as defined in SZ(2).")); endif ## Check optional arguments if (! isempty (VariableNames) && numel (VariableNames) != nv) error (strcat ("timetable: inconsistent number of", ... " 'VariableNames' and 'VariableTypes'.")); elseif (isempty (VariableNames)) VariableNames = cell (1, nv); for i = 1:nv VariableNames{i} = sprintf ("Var%d", i); endfor endif ## Populate variables with defaults VariableValues = cell (1, nv); for i = 1:nv VariableValues{i} = defaultColumn (varTypes{i}, nr); endfor nrows = nr; ## Construct a timetable with data from input arguments else ## Get variable names from input arguments if (isempty (VariableNames)) VariableNames = cell (size (args)); for i = 1:numel (args) VariableNames{i} = inputname (i + leading); if (isempty (VariableNames{i})) VariableNames{i} = sprintf ("Var%d", i); endif endfor endif ## Check for unique names in input arguments [uqNames, ix] = __unique__ (VariableNames); if (numel (uqNames) < numel (VariableNames)) ixBad = 1:numel (VariableNames); ixBad(ix) = []; error ("timetable: duplicate variable names: %s", ... strjoin (VariableNames(ixBad), ", ")); endif ## Check number of variable names and input arguments if (numel (VariableNames) != numel (args)) error (strcat ("timetable: inconsistent number of variable", ... " names (%d) and variable values (%d)."), ... numel (VariableNames), numel (args)); endif ## Only a cell variable can hold a function handle, a dictionary or ## a time series for i = 1:numel (args) errmsg = variableRefusal (this, args{i}); if (! isempty (errmsg)) error ("timetable: %s", errmsg); endif endfor ## Check size of input variables nrows = []; if (! isempty (args)) nrows = size (args{1}, 1); if (ndims (args{1}) > 2) error (strcat ("timetable: variable values must not have more", ... " than 2 dimensions: input 1 '%s' has %d."), ... VariableNames{1}, ndims (args{1})); endif for i = 2:numel (args) if (ndims (args{i}) > 2) error (strcat ("timetable: variable values must not have", ... " more than 2 dimensions: input %d '%s'", ... " has %d."), ... i, VariableNames{i}, ndims (args{i})); endif nrows2 = size (args{i}, 1); if (nrows != nrows2) error (strcat ("timetable: inconsistent sizes between", ... " variables: var '%s' has %d rows; var '%s'", ... " has %d rows."), ... VariableNames{1}, nrows, VariableNames{i}, nrows2); endif endfor endif VariableValues = args(:)'; endif ## Resolve the row times, which either were given or are generated from ## a step. A generated vector needs a row count, and with no variables ## to count there is none, so it comes out empty. if (leading || given(1)) RowTimes = checkRowTimes (RowTimes, nrows); StartTime = startOf (RowTimes); else if (given(3)) TimeStep = seconds (1 / checkSampleRate (SampleRate)); endif if (! wasGiven (StartTime)) StartTime = defaultStart (TimeStep); else StartTime = checkStartTime (StartTime); endif TimeStep = checkTimeStep (TimeStep, StartTime); if (isempty (nrows)) nrows = 0; endif RowTimes = steppedTimes (StartTime, TimeStep, nrows); endif ## Construction ## Descriptions and units stay unset until they are given: an empty ## property says 'none', which is not the same as one blank per ## variable and is what an empty assignment returns them to. this.VariableDescriptions = {}; this.VariableUnits = {}; this.VariableNames = VariableNames(:)'; this.VariableValues = VariableValues; this.VariableTypes = cellfun ('class', VariableValues, ... 'UniformOutput', false); this.StartTime = StartTime; this.StepDeclared = ! (leading || given(1)); if (leading || given(1)) this = applyRowTimes (this, RowTimes, true); else this = applyRowTimes (this, RowTimes, true, TimeStep); endif endfunction endmethods ################################################################################ ## ** Concatenation Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'vertcat' 'horzcat' 'cat' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tt} =} vertcat (@var{tt1}, @var{tt2}, @dots{}) ## ## Concatenate timetables vertically. ## ## @code{@var{tt} = vertcat (@var{tt1}, @var{tt2}, @dots{})} stacks the ## rows of its inputs, which must all have the same variable names. It ## is the operation @code{[@var{tt1}; @var{tt2}]} performs. ## ## The row times are stacked in the order the inputs are given and are ## never sorted, so a later block that starts before an earlier one ends ## simply makes the result irregular, as duplicate times do. The time ## step is read afresh from the stacked times, so two blocks that meet ## exactly keep the step they share and any other pair loses it. ## ## A @code{table} may be stacked onto a timetable, but only after it: ## its rows carry no times and are labelled with missing ones. The row ## dimension is named after the first input that does not use the ## default name. ## ## Where an operand is an @code{eventtable}, or carries one on its ## @qcode{Events} property, the result is an event table too and the ## operands' event tables are merged: an outer join keyed on the event ## times and every variable the two share, ordered by those keys. The ## three properties saying which variables describe the events must ## agree, and the operation is refused where they do not. ## ## @seealso{horzcat, cat} ## @end deftypefn function tbl = vertcat (varargin) varargin = tabular.drop_null_operands (varargin); if (isempty (varargin)) tbl = timetable (); return; elseif (numel (varargin) == 1 && istimetable (varargin{1})) tbl = varargin{1}; return; endif [names, dimName] = timetable.catOperands (varargin, 'vertcat'); ## Every input names the same variables, in whatever order. sorted = cellfun (@sort, names, 'UniformOutput', false); if (numel (sorted) > 1 && ! isequal (sorted{:})) error (strcat ("timetable.vertcat: all inputs must have identical", ... " variable names.")); endif numCols = numel (names{1}); tbl = varargin{1}; tbl.DimensionNames{1} = dimName; rt = tbl.RowTimes; for i = 2:numel (varargin) in = varargin{i}; ixVars = cellfun (@(x) find (ismember (names{1}, x)), names{i}); in = subsetvars (in, ixVars); inVals = varValues (in); for v = 1:numCols tbl.VariableValues{v} = [tbl.VariableValues{v}; inVals{v}]; endfor ## A table brings rows but no times, and they are labelled missing. if (istimetable (in)) rt = [rt; getRowLabels(in)]; else rt = [rt; missingTimes(rt, height (in))]; endif tbl = adoptMetadata (tbl, in); endfor tbl.VariableTypes = cellfun ('class', tbl.VariableValues, ... 'UniformOutput', false); ## Concatenation reads its step off the times it ends up with. tbl.StepDeclared = false; tbl = applyRowTimes (tbl, rt, true); for i = 1:numel (varargin) if (isa (varargin{i}, 'tabular')) tbl = promoteResult (varargin{i}, tbl); endif endfor [tbl, errmsg] = carryEvents (tbl, varargin); if (! isempty (errmsg)) error ("timetable.vertcat: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tt} =} horzcat (@var{tt1}, @var{tt2}, @dots{}) ## ## Concatenate timetables horizontally. ## ## @code{@var{tt} = horzcat (@var{tt1}, @var{tt2}, @dots{})} appends the ## variables of its inputs, which must all have distinct variable names ## and, where they are timetables, identical row times. It is the ## operation @code{[@var{tt1}, @var{tt2}]} performs. ## ## A @code{table} may be appended to a timetable, but only after it, and ## it must have as many rows as the timetable has. The row times and the ## time step are those of the timetable, and the row dimension is named ## after the first input that does not use the default name. ## ## Where an operand is an @code{eventtable}, or carries one on its ## @qcode{Events} property, the result is an event table too and the ## operands' event tables are merged: an outer join keyed on the event ## times and every variable the two share, ordered by those keys. The ## three properties saying which variables describe the events must ## agree, and the operation is refused where they do not. ## ## @seealso{vertcat, cat} ## @end deftypefn function tbl = horzcat (varargin) varargin = tabular.drop_null_operands (varargin); if (isempty (varargin)) tbl = timetable (); return; elseif (numel (varargin) == 1 && istimetable (varargin{1})) tbl = varargin{1}; return; endif [names, dimName] = timetable.catOperands (varargin, 'horzcat'); ## Variable names are distinct across the whole result. This is ## checked before the row times, as in MATLAB, so two blocks that ## share a name are refused for the name whatever their times. allNames = [names{:}]; if (numel (allNames) != numel (__unique__ (allNames))) error (strcat ("timetable.horzcat: all inputs must have unique", ... " variable names.")); endif if (numel (__unique__ (cellfun (@height, varargin))) != 1) error (strcat ("timetable.horzcat: all inputs must have the same", ... " number of rows.")); endif for i = 2:numel (varargin) if (istimetable (varargin{i}) && ! isequal (getRowLabels (varargin{i}), getRowLabels (varargin{1}))) error (strcat ("timetable.horzcat: all timetables being", ... " concatenated must have the same row times.")); endif endfor tbl = varargin{1}; tbl.DimensionNames{1} = dimName; tbl.VariableNames = allNames; for i = 2:numel (varargin) in = varargin{i}; props = getProperties (in); inVals = varValues (in); nA = numel (tbl.VariableValues); nB = numel (inVals); tbl.VariableContinuity = tabular.merge_continuity ( ... tbl.VariableContinuity, nA, ... props.VariableContinuity, nB); tbl.VariableDescriptions = tabular.merge_meta ( ... tbl.VariableDescriptions, nA, ... props.VariableDescriptions, nB); tbl.VariableUnits = tabular.merge_meta (tbl.VariableUnits, nA, ... props.VariableUnits, nB); tbl.VariableValues = [tbl.VariableValues, inVals]; if (isempty (tbl.Description)) tbl.Description = props.Description; endif if (isempty (tbl.UserData)) tbl.UserData = props.UserData; endif endfor tbl.VariableTypes = cellfun ('class', tbl.VariableValues, ... 'UniformOutput', false); [cp, cpTypes] = merge_hcat_props (tbl, varargin); tbl.CustomProperties = cp; tbl.CustomPropTypes = cpTypes; for i = 1:numel (varargin) if (isa (varargin{i}, 'tabular')) tbl = promoteResult (varargin{i}, tbl); endif endfor [tbl, errmsg] = carryEvents (tbl, varargin); if (! isempty (errmsg)) error ("timetable.horzcat: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tt} =} cat (@var{dim}, @var{tt1}, @var{tt2}, @dots{}) ## ## Concatenate timetables along a dimension. ## ## @code{@var{tt} = cat (@var{dim}, @var{tt1}, @var{tt2}, @dots{})} ## concatenates along @var{dim}, which must be 1 or 2: a timetable has ## two dimensions and there is nothing to stack along a third. ## @code{cat (1, @dots{})} is @code{vertcat} and @code{cat (2, @dots{})} ## is @code{horzcat}. ## ## @seealso{vertcat, horzcat} ## @end deftypefn function tbl = cat (dim, varargin) if (nargin < 1) error ("timetable.cat: too few input arguments."); endif if (! (isnumeric (dim) && isscalar (dim) && any (dim == [1, 2]))) error ("timetable.cat: DIM must be 1 or 2 for a 2-D timetable."); endif if (dim == 1) tbl = vertcat (varargin{:}); else tbl = horzcat (varargin{:}); endif endfunction endmethods ################################################################################ ## ** Size and Shape Operations ** ## ################################################################################ ## Available Methods ## ## ## ## 'height' 'width' 'size' 'numel' ## ## 'ndims' 'isempty' 'repelem' 'repmat' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timetable} {@var{H} =} height (@var{tt}) ## ## Number of rows in timetable. ## ## @code{@var{H} = height (@var{tt})} returns the number of rows in the ## timetable @var{tt} as a scalar. It is the equivalent of ## @qcode{size (@var{tt}, 1)}. ## ## The count comes from the row times rather than from the variables, so ## a timetable whose variables have all been removed still reports the ## rows it labels. ## ## @seealso{width, size} ## @end deftypefn function out = height (this) out = numel (this.RowTimes); endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{W} =} width (@var{tt}) ## ## Number of variables in timetable. ## ## @code{@var{W} = width (@var{tt})} returns the number of variables in ## the timetable @var{tt} as a scalar. It is the equivalent of ## @qcode{size (@var{tt}, 2)}. The row times are not a variable and are ## not counted. ## ## @seealso{height, size} ## @end deftypefn function out = width (this) out = numel (this.VariableNames); endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{sz} =} size (@var{tt}) ## @deftypefnx {timetable} {@var{dim_sz} =} size (@var{tt}, @var{dim}) ## @deftypefnx {timetable} {[@var{rows}, @var{columns}] =} size (@var{tt}) ## ## Size of a timetable. ## ## @code{@var{sz} = size (@var{tt})} returns a two-element row vector ## with the number of rows and the number of variables in @var{tt}. ## ## @code{@var{dim_sz} = size (@var{tt}, @var{dim})} returns the size ## along the dimension @var{dim}. A timetable has two dimensions, so any ## dimension above the second has size 1. ## ## @seealso{height, width, ndims, numel} ## @end deftypefn function varargout = size (this, dim) sz = [height(this), width(this)]; if (nargin == 2) ## Sizes along the requested dimension(s); dimensions above 2 are 1. dim_sz = ones (1, numel (dim)); valid = dim <= 2; dim_sz(valid) = sz(dim(valid)); if (nargout > 1) varargout = num2cell (dim_sz); else varargout{1} = dim_sz; endif elseif (nargout <= 1) varargout{1} = sz; else varargout{1} = sz(1); varargout{2} = sz(2); [varargout{3:nargout}] = deal (1); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{N} =} numel (@var{tt}) ## ## Number of elements in timetable. ## ## @code{@var{N} = numel (@var{tt})} returns the number of rows times the ## number of variables in @var{tt}. ## ## @seealso{size, height, width} ## @end deftypefn function out = numel (this, varargin) if (nargin < 2) out = prod (size (this)); else ## Given subscripts, report how many elements a reference with them ## would produce, which is what the classdef machinery asks in order ## to size a chained reference. A '()' reference into a timetable ## yields one timetable whatever the subscripts select, an empty one ## included, so the answer is always one. out = 1; endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{N} =} ndims (@var{tt}) ## ## Number of dimensions in timetable. ## ## @code{@var{N} = ndims (@var{tt})} always returns 2. A timetable is ## always a two-dimensional container of rows and variables, whatever the ## shape of the values held in those variables. ## ## @seealso{size} ## @end deftypefn function out = ndims (this) out = 2; endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{TF} =} isempty (@var{tt}) ## ## True for an empty timetable. ## ## @code{@var{TF} = isempty (@var{tt})} returns true when @var{tt} has no ## rows or no variables, and false otherwise. ## ## @seealso{size, height, width} ## @end deftypefn function TF = isempty (this) TF = prod (size (this)) == 0; endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tt2} =} repelem (@var{tt}, @var{rows}, @var{cols}) ## ## Repeat each row and variable of a timetable in place. ## ## @code{@var{tt2} = repelem (@var{tt}, @var{rows}, @var{cols})} repeats ## each row of @var{tt} @var{rows} times and each variable @var{cols} ## times, keeping the repeats of a row together. Either count may be a ## vector with one count per row, or per variable, and a count of zero ## drops that row or variable. Both counts must be given, a timetable ## having exactly two dimensions. Each repeated row ## carries the row time of the row it came from, so the result has ## repeated row times and is generally irregular; each repeated variable ## takes a numbered name, @qcode{A} becoming @qcode{A}, @qcode{A_1}. ## ## @seealso{repmat, timetable} ## @end deftypefn function tt2 = repelem (this, varargin) [tt2, errmsg] = repeatResult (this, varargin, true); if (! isempty (errmsg)) error ("timetable.repelem: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tt2} =} repmat (@var{tt}, @var{sz}) ## @deftypefnx {timetable} {@var{tt2} =} repmat (@var{tt}, @var{rows}, @var{cols}) ## ## Repeat a timetable as a block. ## ## @code{@var{tt2} = repmat (@var{tt}, @var{rows}, @var{cols})} repeats ## the whole timetable @var{rows} times downwards and @var{cols} times ## across. Given a single argument both counts take it. Each copy carries ## the row times it came from, so the result has repeated row times and is ## generally irregular; each repeated variable takes a numbered name, ## @qcode{A} becoming @qcode{A}, @qcode{A_1}. ## ## @seealso{repelem, timetable} ## @end deftypefn function tt2 = repmat (this, varargin) [tt2, errmsg] = repeatResult (this, varargin, false); if (! isempty (errmsg)) error ("timetable.repmat: %s", errmsg); endif endfunction endmethods ################################################################################ ## ** Regularity and Type ** ## ################################################################################ ## Available Methods ## ## ## ## 'isregular' 'istimetable' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timetable} {@var{TF} =} isregular (@var{tt}) ## @deftypefnx {timetable} {@var{TF} =} isregular (@var{tt}, @var{unit}) ## ## True when the row times of a timetable are evenly spaced. ## ## @code{@var{TF} = isregular (@var{tt})} returns true when @var{tt} is ## regular with respect to absolute time, that is when consecutive row ## times are separated by the same fixed length of time. A calendar ## month has no fixed length, so a monthly timetable answers false here ## and true for @qcode{'months'} below. ## ## @code{@var{TF} = isregular (@var{tt}, @var{unit})} returns true when ## the row times are evenly spaced by a whole number of @var{unit}, one ## of @qcode{'time'}, @qcode{'days'}, @qcode{'weeks'}, ## @qcode{'months'}, @qcode{'quarters'} or @qcode{'years'}. ## @qcode{'time'} means absolute time and is what the one-argument form ## asks. There is no unit smaller than a day: an hourly timetable is ## regular in time and in nothing else. ## ## The calendar units are measured on the calendar rather than in ## elapsed time, which is what separates them. Across a daylight saving ## change a run of calendar days is regular in @qcode{'days'} and regular ## in nothing else, one of those days being an hour shorter than the ## rest, and such a timetable reports no time step at all while still ## answering true here. ## ## A timetable with fewer than two rows has no spacing to measure and ## answers from the time step it remembers, so a single row taken out of ## a timetable told it was hourly is still regular in time, while one ## taken out of a timetable that read its step off its row times is ## not. ## ## @strong{The answer is always computed from the row times and ## @var{unit} is always checked}, which MATLAB does neither of when the ## step was declared with @qcode{'TimeStep'} or @qcode{'SampleRate'} and ## is a @code{duration}: there it answers from the stored step alone. Two ## consequences follow there and not here. A misspelt unit, and the ## non-units @qcode{'seconds'}, @qcode{'minutes'} and @qcode{'hours'}, ## come back @code{false} rather than raising. And two timetables with ## @strong{identical row times} disagree: times built as ## @code{d0 + hours (0:24:96)} answer true for @qcode{'days'} while the ## same times declared as @qcode{'TimeStep'}, @code{hours (24)} answer ## false. Both answer true here. ## ## @seealso{timetable} ## @end deftypefn function TF = isregular (this, unit) if (nargin < 2) unit = 'time'; endif if (isstring (unit) && isscalar (unit)) unit = char (unit); endif units = {'time', 'days', 'weeks', 'months', 'quarters', 'years'}; if (! (ischar (unit) && isrow (unit)) || ! any (strcmp (unit, units))) error (strcat ("timetable.isregular: UNIT must be one of 'time',", ... " 'days', 'weeks', 'months', 'quarters' or", ... " 'years'.")); endif rt = this.RowTimes; if (strcmp (unit, 'time')) if (numel (rt) > 1) ds = seconds (diff (rt)); TF = ! any (ismissing (rt)) && ds(1) != 0 && all (ds == ds(1)); else TF = isduration (this.TimeStep) && ! any (ismissing (this.TimeStep)); endif return endif ## Every other unit is measured on the row times rather than on the ## stored step, so that a run of calendar days survives a daylight ## saving change even though its step does not. if (numel (rt) > 1) cd = calendarStepOf (rt); elseif (iscalendarduration (this.TimeStep)) cd = this.TimeStep; else cd = []; endif TF = unitDivides (unit, cd); endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{TF} =} istimetable (@var{tt}) ## ## True if input is a @code{timetable}, false otherwise. ## ## @code{@var{TF} = istimetable (@var{tt})} always returns true for a ## timetable, irrespective of its size. ## ## @seealso{istable, istabular} ## @end deftypefn function TF = istimetable (this) TF = true; endfunction endmethods ################################################################################ ## ** Comparison ** ## ################################################################################ ## Available Methods ## ## ## ## 'isequal' 'isequaln' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timetable} {@var{TF} =} isequal (@var{A}, @var{B}) ## @deftypefnx {timetable} {@var{TF} =} isequal (@var{A}, @var{B}, @dots{}) ## ## Test timetables for equality. ## ## @code{@var{TF} = isequal (@var{A}, @var{B})} returns a logical scalar ## @var{TF}, which is @qcode{true} when the timetables @var{A} and @var{B} ## are the same size, carry the same row times, variable names and ## metadata, and each pair of corresponding variables holds equal values, ## and @qcode{false} otherwise. ## ## The row times take part in the comparison and the properties that ## merely describe them do not, so @qcode{TimeStep}, @qcode{SampleRate} ## and @qcode{StartTime} are excluded. A timetable told its time step at ## construction therefore equals one that read the same step off the row ## times it was given, although the two report a different ## @qcode{TimeStep} once subset to a single row. ## ## Variables are compared by value and not by class, exactly as ## @code{isequal} compares arrays elsewhere, so a timetable holding ## @code{int8 ([1; 2])} equals one holding @code{[1; 2]}. The ## @qcode{VariableTypes} property, which only restates those classes, ## takes no part in the comparison. Every other property does: two ## timetables differing only in @qcode{Description}, @qcode{UserData}, ## @qcode{VariableUnits}, @qcode{VariableDescriptions}, ## @qcode{VariableContinuity} or a custom property are not equal. ## ## As with @qcode{NaN}, missing values are never equal, so a missing row ## time or element anywhere in either timetable makes the result ## @qcode{false}; use @code{isequaln} to treat missing values as equal. ## ## Further timetables may be supplied, as in @code{isequal (@var{A}, ## @var{B}, @var{C}, @dots{})}, in which case @var{TF} is @qcode{true} ## only when all of them are equal to one another. Any argument that is ## not a timetable, a table included, makes the result @qcode{false} ## rather than raising an error. ## ## @end deftypefn function TF = isequal (varargin) if (nargin < 2) error ("timetable.isequal: too few input arguments."); endif TF = false; if (all (cellfun (@(x) isa (x, 'timetable'), varargin))) TF = isequalResult (varargin{1}, varargin(2:end), false); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{TF} =} isequaln (@var{A}, @var{B}) ## @deftypefnx {timetable} {@var{TF} =} isequaln (@var{A}, @var{B}, @dots{}) ## ## Test timetables for equality, treating missing values as equal. ## ## @code{@var{TF} = isequaln (@var{A}, @var{B})} is identical to ## @code{isequal (@var{A}, @var{B})} except that missing values are ## treated as equal to one another, in the same way that @code{isequaln} ## treats @qcode{NaN}. Two timetables whose row times are @qcode{NaT} in ## the same places are therefore equal, where @code{isequal} calls them ## unequal. ## ## Further timetables may be supplied, as in @code{isequaln (@var{A}, ## @var{B}, @var{C}, @dots{})}, in which case @var{TF} is @qcode{true} ## only when all of them are equal to one another. Any argument that is ## not a timetable, a table included, makes the result @qcode{false} ## rather than raising an error. ## ## @end deftypefn function TF = isequaln (varargin) if (nargin < 2) error ("timetable.isequaln: too few input arguments."); endif TF = false; if (all (cellfun (@(x) isa (x, 'timetable'), varargin))) TF = isequalResult (varargin{1}, varargin(2:end), true); endif endfunction endmethods ################################################################################ ## ** Missing Data ** ## ################################################################################ ## Available Methods ## ## ## ## 'ismissing' 'anymissing' 'rmmissing' ## ## 'standardizeMissing' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timetable} {@var{TF} =} ismissing (@var{tt}) ## @deftypefnx {timetable} {@var{TF} =} ismissing (@var{tt}, @var{indicator}) ## ## Find missing values in the variables of a timetable. ## ## @code{@var{TF} = ismissing (@var{tt})} returns a logical array with one ## row per row of @var{tt} and one column per variable, true where the ## variable is missing there. What counts as missing depends on the type: ## @qcode{NaN} for numeric data, @qcode{NaT} for @code{datetime}, ## @qcode{} for @code{categorical}, and an empty character ## vector or string. ## ## The row times are not read. They label the rows rather than being one ## of them, so a missing row time is not reported here and does not make ## @code{anymissing} true. @code{rmmissing} does drop such a row, being ## about what can be kept rather than about what is missing. ## ## @code{@var{TF} = ismissing (@var{tt}, @var{indicator})} treats the ## values in @var{indicator} as missing instead. When specifying an ## @var{indicator}, all default missing values are ignored. If you want to ## keep them, you need to define them in @var{indicator}, which ## @code{missing} does for every variable. @var{indicator} follows the ## rules of @code{table.ismissing}. ## ## @seealso{timetable.anymissing, timetable.rmmissing, ## timetable.standardizeMissing, timetable} ## @end deftypefn function TF = ismissing (this, varargin) [TF, errmsg] = ismissingResult (this, varargin{:}); if (! isempty (errmsg)) error ("timetable.ismissing: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{TF} =} anymissing (@var{tt}) ## ## True when any variable of a timetable has a missing value. ## ## @code{@var{TF} = anymissing (@var{tt})} is @code{any (ismissing ## (@var{tt})(:))} and reads the same thing: the variables, and not the ## row times. A timetable whose only missing value is a row time ## answers false here and still loses that row to @code{rmmissing}. ## ## @seealso{timetable.ismissing, timetable.rmmissing, timetable} ## @end deftypefn function TF = anymissing (this) TF = any (any (ismissing (this))); endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttB} =} rmmissing (@var{ttA}) ## @deftypefnx {timetable} {@var{ttB} =} rmmissing (@var{ttA}, @var{dim}) ## @deftypefnx {timetable} {@var{ttB} =} rmmissing (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {timetable} {[@var{ttB}, @var{TF}] =} rmmissing (@dots{}) ## ## Remove the incomplete rows or variables of a timetable. ## ## @code{@var{ttB} = rmmissing (@var{ttA})} removes every row holding a ## missing value. @code{rmmissing (@var{ttA}, 2)} removes every variable ## holding one instead, and keeps every row and its row time. ## ## @strong{A row whose row time is missing is removed whatever its ## variables hold}, and whatever @qcode{'DataVariables'} or ## @qcode{'MinNumMissing'} say: a row that cannot be placed in time is ## not a row a timetable can keep. That is a precondition rather than a ## report of missingness, which is why @code{ismissing} does not mark ## such a row and @code{anymissing} does not count it. ## ## @qcode{'DataVariables'} names the variables to judge a row by, and ## @qcode{'MinNumMissing'} how many missing values a row must hold before ## it goes, or with @var{dim} equal to 2 how many a variable must. ## @qcode{'MissingLocations'} marks the missing values in place of ## @code{ismissing}, as a logical matrix or a table, exactly as for ## @code{table.rmmissing}. None of them reaches the row times. ## ## @code{[@var{ttB}, @var{TF}] = rmmissing (@dots{})} also returns a ## logical column marking the rows that were removed, the ones dropped ## for their row times included, or with @var{dim} equal to 2 a logical ## row marking the removed variables. ## ## The time step is read afresh from the rows that survive. ## ## @seealso{timetable.ismissing, timetable.standardizeMissing, timetable} ## @end deftypefn function [tbl, TF] = rmmissing (this, varargin) [tbl, TF, errmsg] = rmmissingResult (this, varargin{:}); if (! isempty (errmsg)) error ("timetable.rmmissing: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttB} =} standardizeMissing (@var{ttA}, @var{indicator}) ## @deftypefnx {timetable} {@var{ttB} =} standardizeMissing (@dots{}, @qcode{'DataVariables'}, @var{vars}) ## ## Make given values missing in a timetable. ## ## @code{@var{ttB} = standardizeMissing (@var{ttA}, @var{indicator})} ## replaces every value matching @var{indicator} with the missing value ## of its own type, so that @code{ismissing} and @code{rmmissing} will ## afterwards treat it as missing. Each indicator applies only to the ## variables of its own kind, as @code{table.standardizeMissing} ## describes: numbers to floating point variables, text to text and ## @code{categorical} variables, and @code{duration} and @code{datetime} ## indicators to variables of their own type. ## ## @qcode{'DataVariables'} restricts it to the variables named. The row ## times are not a data variable and are never rewritten, so the time ## step is unchanged. ## ## @seealso{timetable.ismissing, timetable.rmmissing, timetable} ## @end deftypefn function tbl = standardizeMissing (this, varargin) [tbl, errmsg] = standardizeMissingResult (this, varargin{:}); if (! isempty (errmsg)) error ("timetable.standardizeMissing: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttB} =} fillmissing (@var{ttA}, @var{method}) ## @deftypefnx {timetable} {@var{ttB} =} fillmissing (@var{ttA}, @qcode{'constant'}, @var{v}) ## @deftypefnx {timetable} {@var{ttB} =} fillmissing (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {timetable} {[@var{ttB}, @var{TF}] =} fillmissing (@dots{}) ## ## Fill the missing values of a timetable. ## ## @code{@var{ttB} = fillmissing (@var{ttA}, @var{method})} replaces each ## missing value by one worked out from the values around it. ## @qcode{'previous'}, @qcode{'next'} and @qcode{'nearest'} copy a ## neighbouring value; @qcode{'linear'}, @qcode{'spline'}, ## @qcode{'pchip'} and @qcode{'makima'} interpolate; and ## @qcode{'constant'} takes the value given after it. ## ## @strong{The row times are what the filling runs against}, not the ## order of the rows. A gap an hour after its left neighbour and two ## hours before its right one is filled a third of the way between them ## by @qcode{'linear'}, and takes the left value under ## @qcode{'nearest'}, where counting rows would put it midway and call ## the two neighbours equally close. The row times are already the ## sample points, so @qcode{'SamplePoints'} is not accepted; a timetable ## whose row times are not all known is refused outright rather than ## filled around the gap. ## ## @qcode{'DataVariables'} names the variables to fill and ## @qcode{'EndValues'} says what to do with a gap that has no neighbour ## on one side, taking @qcode{'extrap'}, another method name, or a ## constant. ## ## A variable with nothing @code{ismissing} counts as missing, such as a ## cell array that is not a cell array of character vectors, a structure ## or a nested table, is left as it is. MATLAB refuses the whole call ## when such a variable is among the data variables. ## ## @code{[@var{ttB}, @var{TF}] = fillmissing (@dots{})} also returns a ## logical array marking what was filled. The row times themselves are ## never filled and the time step is unchanged. ## ## @seealso{timetable.ismissing, timetable.rmmissing, ## timetable.standardizeMissing, timetable} ## @end deftypefn function [tbl, TF] = fillmissing (this, varargin) [tbl, TF, errmsg] = fillmissingResult (this, varargin{:}); if (! isempty (errmsg)) error ("timetable.fillmissing: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {} summary (@var{tt}) ## @deftypefnx {timetable} {@var{s} =} summary (@var{tt}) ## ## Summarise a timetable. ## ## @code{summary (@var{tt})} prints what the timetable holds: its size, ## its description where it has one, the row times, the variables with ## their types and any units and descriptions, and a table of statistics ## for everything that has any. ## ## @code{@var{s} = summary (@var{tt})} returns that as a structure ## instead, one field per variable and one for the row times, filed under ## the row dimension's name and coming first. ## ## The row times report where they start, how fast they run and how far ## apart they step, in @qcode{StartTime}, @qcode{SampleRate} and ## @qcode{TimeStep}, alongside the statistics a @code{datetime} or ## @code{duration} variable would report. They carry no description, ## units or continuity, having none. ## ## Which statistics a variable reports follows its type. Numeric, ## @code{datetime} and @code{duration} variables report the count of ## missing values, the smallest, the median, the largest, the mean and ## the standard deviation; an integer reports no deviation and its median ## rounds to its own type; an ordinal @code{categorical} is ordered but ## has no mean; a plain one reports its categories and their counts; a ## @code{logical} reports how many are true and false and appears in no ## statistics; and everything else reports the count of missing values ## alone. A variable of several columns reports one row per column. ## ## @seealso{timetable.ismissing, timetable} ## @end deftypefn function [varargout] = summary (this, varargin) if (! isempty (varargin)) error ("timetable.summary: too many input arguments."); endif s = summaryOf (this); if (nargout == 0) summaryPrint (this, s, inputname (1)); elseif (nargout == 1) varargout{1} = s; else error ("timetable.summary: invalid number of output arguments."); endif endfunction endmethods ################################################################################ ## ** Variable Management ** ## ################################################################################ ## Available Methods ## ## ## ## 'addvars' 'removevars' 'movevars' 'renamevars' ## ## 'convertvars' 'mergevars' 'splitvars' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttB} =} addvars (@var{ttA}, @var{var1}, @dots{}, @var{varN}) ## @deftypefnx {timetable} {@var{ttB} =} addvars (@dots{}, @qcode{'Before'}, @var{location}) ## @deftypefnx {timetable} {@var{ttB} =} addvars (@dots{}, @qcode{'After'}, @var{location}) ## @deftypefnx {timetable} {@var{ttB} =} addvars (@dots{}, @qcode{'NewVariableNames'}, @var{newNames}) ## ## Add variables to a timetable. ## ## @code{@var{ttB} = addvars (@var{ttA}, @var{var1}, @dots{})} appends ## each array as a new variable at the right-hand end. Every one must ## have as many rows as the timetable has. ## ## Unnamed variables take the name of the workspace variable they came ## from, or @qcode{Var@var{N}} where there is none. A name that would ## collide with the row dimension is not allowed and one that arrives by ## the workspace route is given a suffix instead. ## ## @qcode{'Before'} and @qcode{'After'} place the new variables beside an ## existing one, named or numbered. The row times are not a variable and ## cannot be used as the location, nor can position zero. ## ## The row times are untouched, so the time step is unchanged. ## ## @seealso{timetable.removevars, timetable.movevars, timetable} ## @end deftypefn function tbl = addvars (this, varargin) ## Only the public method can read the caller's names. argNames = cell (1, numel (varargin)); for i = 1:numel (varargin) argNames{i} = inputname (i + 1); endfor [tbl, errmsg] = addvarsResult (this, argNames, varargin{:}); if (! isempty (errmsg)) error ("timetable.addvars: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttB} =} removevars (@var{ttA}, @var{vars}) ## ## Remove variables from a timetable. ## ## @code{@var{ttB} = removevars (@var{ttA}, @var{vars})} deletes the ## variables named, numbered, selected by a logical vector or picked out ## by a @code{vartype}. ## ## The row times are not a variable and cannot be removed: a timetable ## keeps them whatever else goes. Removing every variable leaves a ## timetable with none, still carrying its row times and its time step, ## rather than an empty one. ## ## Removing a variable of an @code{eventtable} that one of its three ## event properties names clears that designation, the name having ## nothing left to resolve to. Deleting the variable by assigning an ## empty matrix does the same. ## ## @seealso{timetable.addvars, timetable.movevars, timetable} ## @end deftypefn function tbl = removevars (this, varargin) [tbl, errmsg] = removevarsResult (this, varargin{:}); if (! isempty (errmsg)) error ("timetable.removevars: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttB} =} movevars (@var{ttA}, @var{vars}, @qcode{'Before'}, @var{location}) ## @deftypefnx {timetable} {@var{ttB} =} movevars (@var{ttA}, @var{vars}, @qcode{'After'}, @var{location}) ## ## Move variables within a timetable. ## ## @code{@var{ttB} = movevars (@var{ttA}, @var{vars}, @dots{})} puts the ## variables named in @var{vars} before or after @var{location}, which ## names or numbers another variable. Each variable takes its units, ## description and continuity with it. ## ## The row times are not a variable: they can be neither moved nor used ## as the location, and they stay where they are whatever else is ## reordered. ## ## @seealso{timetable.addvars, timetable.removevars, timetable} ## @end deftypefn function tbl = movevars (this, varargin) [tbl, errmsg] = movevarsResult (this, varargin{:}); if (! isempty (errmsg)) error ("timetable.movevars: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttB} =} renamevars (@var{ttA}, @var{vars}, @var{newNames}) ## ## Rename variables in a timetable. ## ## @code{@var{ttB} = renamevars (@var{ttA}, @var{vars}, @var{newNames})} ## gives each variable in @var{vars} the matching name in ## @var{newNames}. Everything else about the variable is kept, its units ## and its description included. ## ## Dimension names are not renamed here: assign to ## @qcode{@var{tt}.Properties.DimensionNames} for that. A variable ## cannot be given the row dimension's name either, the two sharing one ## namespace. ## ## Renaming a variable of an @code{eventtable} that one of its three ## event properties names carries that designation to the new name. ## MATLAB clears it instead and does not restore it when the variable is ## renamed back; see deviation D6. ## ## @seealso{timetable.movevars, timetable} ## @end deftypefn function tbl = renamevars (this, varargin) [tbl, errmsg] = renamevarsResult (this, varargin{:}); if (! isempty (errmsg)) error ("timetable.renamevars: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttB} =} convertvars (@var{ttA}, @var{vars}, @var{dataType}) ## ## Convert variables of a timetable to a given type. ## ## @code{@var{ttB} = convertvars (@var{ttA}, @var{vars}, @var{dataType})} ## converts each variable in @var{vars}, which may be named, numbered, ## selected by a logical vector or picked out by a @code{vartype}. ## @var{dataType} is a type name or a function handle that performs the ## conversion. ## ## The row times are not a data variable and cannot be converted; assign ## to @qcode{@var{tt}.Properties.RowTimes} to change them. ## ## @seealso{timetable.renamevars, timetable} ## @end deftypefn function tbl = convertvars (this, varargin) [tbl, errmsg] = convertvarsResult (this, varargin{:}); if (! isempty (errmsg)) error ("timetable.convertvars: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttB} =} mergevars (@var{ttA}, @var{vars}) ## @deftypefnx {timetable} {@var{ttB} =} mergevars (@dots{}, @qcode{'NewVariableName'}, @var{name}) ## @deftypefnx {timetable} {@var{ttB} =} mergevars (@dots{}, @qcode{'MergeAsTable'}, @var{tf}) ## ## Combine several variables of a timetable into one. ## ## @code{@var{ttB} = mergevars (@var{ttA}, @var{vars})} replaces the ## variables in @var{vars} with a single multi-column one, put where the ## first of them was. It is named @qcode{Var@var{N}} unless ## @qcode{'NewVariableName'} says otherwise, and the merged variables are ## no longer reachable by their old names. ## ## Nothing the merged variables carried about themselves survives: one ## variable carries one unit, one description, one continuity and one ## entry of a custom property describing the variables, and there is no ## saying which of them the merged variable should take, so it takes ## none: where the continuity is set at all, the merged variable's ## reads @qcode{'unset'}, and a property that was never set stays ## unset. The remaining variables keep theirs. ## ## The row times are not a variable and cannot be merged. ## ## Merging a variable of an @code{eventtable} that one of its three event ## properties names clears that designation, the merged variable being a ## different variable under a different name. MATLAB refuses the call ## instead, on the ground that the merged variable is no longer a column. ## ## @seealso{timetable.splitvars, timetable} ## @end deftypefn function tbl = mergevars (this, varargin) [tbl, errmsg] = mergevarsResult (this, varargin{:}); if (! isempty (errmsg)) error ("timetable.mergevars: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttB} =} splitvars (@var{ttA}) ## @deftypefnx {timetable} {@var{ttB} =} splitvars (@var{ttA}, @var{vars}) ## @deftypefnx {timetable} {@var{ttB} =} splitvars (@dots{}, @qcode{'NewVariableNames'}, @var{newNames}) ## ## Split multi-column variables of a timetable into one each. ## ## @code{@var{ttB} = splitvars (@var{ttA})} splits every multi-column ## variable, and every nested table, into one variable per column. ## @code{@var{ttB} = splitvars (@var{ttA}, @var{vars})} splits only those ## named. ## ## The new variables are named after the one they came from with a column ## number appended, unless @qcode{'NewVariableNames'} gives them names. ## Splitting a variable that was merged does not bring back the units the ## merge discarded. ## ## The row times are not a variable and cannot be split. ## ## @seealso{timetable.mergevars, timetable} ## @end deftypefn function tbl = splitvars (this, varargin) [tbl, errmsg] = splitvarsResult (this, varargin{:}); if (! isempty (errmsg)) error ("timetable.splitvars: %s", errmsg); endif endfunction endmethods ################################################################################ ## ** First and Last Rows ** ## ################################################################################ ## Available Methods ## ## ## ## 'head' 'tail' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timetable} {} head (@var{tt}) ## @deftypefnx {timetable} {} head (@var{tt}, @var{k}) ## @deftypefnx {timetable} {@var{out} =} head (@var{tt}, @var{k}) ## ## Display or return the first @var{k} rows of a timetable. ## ## @code{head (@var{tt})} displays the first eight rows of @var{tt}, or ## all of them if it has fewer. @code{head (@var{tt}, @var{k})} displays ## the first @var{k} instead. @var{k} must be a real, nonnegative, ## integer scalar value; a @var{k} of zero displays no rows at all rather ## than raising. ## ## @code{@var{out} = head (@var{tt}, @var{k})} returns those rows in a ## new timetable instead of displaying them. If @var{k} is omitted or ## empty it defaults to eight. ## ## The rows come back in the order the timetable holds them. @code{head} ## takes the first rows and not the earliest ones, so on a timetable ## whose times are out of order it returns whatever happens to be stored ## first. Sort it beforehand to ask the other question. ## ## The time step is read afresh from the row times that are kept, so the ## head of an hourly timetable is hourly and the head of a timetable ## whose spacing changes partway may not be. ## ## @seealso{timetable.tail, sortrows, timetable} ## @end deftypefn function [varargout] = head (this, k) if (nargin < 2) k = []; endif [ixRows, errmsg] = headTailRows (this, k, false); if (! isempty (errmsg)) error ("timetable.head: %s", errmsg); endif out = subsetrows (this, ixRows); if (nargout == 0) print_table (out); elseif (nargout == 1) varargout{1} = out; else error ("timetable.head: invalid number of output arguments."); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {} tail (@var{tt}) ## @deftypefnx {timetable} {} tail (@var{tt}, @var{k}) ## @deftypefnx {timetable} {@var{out} =} tail (@var{tt}, @var{k}) ## ## Display or return the last @var{k} rows of a timetable. ## ## @code{tail (@var{tt})} displays the last eight rows of @var{tt}, or ## all of them if it has fewer. @code{tail (@var{tt}, @var{k})} displays ## the last @var{k} instead. @var{k} must be a real, nonnegative, ## integer scalar value; a @var{k} of zero displays no rows at all rather ## than raising. ## ## @code{@var{out} = tail (@var{tt}, @var{k})} returns those rows in a ## new timetable instead of displaying them. If @var{k} is omitted or ## empty it defaults to eight. ## ## The rows come back in the order the timetable holds them, and they are ## the last rows rather than the latest ones. The time step is read ## afresh from the row times that are kept. ## ## @seealso{timetable.head, sortrows, timetable} ## @end deftypefn function [varargout] = tail (this, k) if (nargin < 2) k = []; endif [ixRows, errmsg] = headTailRows (this, k, true); if (! isempty (errmsg)) error ("timetable.tail: %s", errmsg); endif out = subsetrows (this, ixRows); if (nargout == 0) print_table (out); elseif (nargout == 1) varargout{1} = out; else error ("timetable.tail: invalid number of output arguments."); endif endfunction endmethods ################################################################################ ## ** Row Ordering and Sets ** ## ################################################################################ ## Available Methods ## ## ## ## 'sortrows' 'unique' 'topkrows' ## ## 'issorted' 'issortedrows' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttB} =} sortrows (@var{ttA}) ## @deftypefnx {timetable} {@var{ttB} =} sortrows (@var{ttA}, @var{rowDimName}) ## @deftypefnx {timetable} {@var{ttB} =} sortrows (@var{ttA}, @var{vars}) ## @deftypefnx {timetable} {@var{ttB} =} sortrows (@var{ttA}, @dots{}, @var{direction}) ## @deftypefnx {timetable} {@var{ttB} =} sortrows (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {timetable} {[@var{ttB}, @var{index}] =} sortrows (@dots{}) ## ## Sort the rows of a timetable. ## ## @code{@var{ttB} = sortrows (@var{ttA})} orders the rows by their row ## times, earliest first. Rows sharing a time keep the order they were ## in: the row times alone decide, and the variables are never consulted ## to break a tie. ## ## @code{@var{ttB} = sortrows (@var{ttA}, @var{rowDimName})} does the ## same, naming the row times through the first of ## @code{@var{ttA}.Properties.DimensionNames}. That name is the only way ## to reach them: a numeric index counts the variables, so ## @code{sortrows (@var{ttA}, 1)} orders by the first variable and not by ## the times, and there is no index that means the row times. Renaming ## the row dimension renames the key with it. ## ## @code{@var{ttB} = sortrows (@var{ttA}, @var{vars})} orders by one or ## more variables, named, indexed by number, selected by a logical vector ## or picked out by a @code{vartype}. A negative index sorts that ## variable in descending order. The row dimension name may appear among ## @var{vars}, in which case the row times take their turn as a key like ## any other. ## ## @code{@var{ttB} = sortrows (@dots{}, @var{direction})} sorts as ## @qcode{'ascend'} or @qcode{'descend'}, either one direction for every ## key or one per key. A direction must follow the keys it applies to, ## so @code{sortrows (@var{ttA}, 'descend')} is an error rather than a ## reversed sort: with nothing else given, the first argument is read as ## the variables to sort by. ## ## The name-value pair @qcode{'MissingPlacement'} takes ## @qcode{'auto'} (the default), @qcode{'first'} or @qcode{'last'} and ## says where missing keys go. @qcode{'auto'} puts them last when ## sorting up and first when sorting down. @qcode{'ComparisonMethod'} ## takes @qcode{'auto'}, @qcode{'real'} or @qcode{'abs'} and applies to ## numeric variables. Both must follow the keys as well. ## ## @code{[@var{ttB}, @var{index}] = sortrows (@dots{})} also returns the ## permutation, so that @code{@var{ttA}(@var{index},:)} is @var{ttB}. ## ## The time step is read afresh from the sorted row times rather than ## carried over, so sorting an out-of-order hourly timetable makes it ## regular again, and sorting one that ran backwards turns its step from ## minus one hour to plus one. ## ## @seealso{timetable.issortedrows, timetable} ## @end deftypefn function [tbl, index] = sortrows (this, varargin) [index, errmsg] = sortrowsIndex (this, varargin); if (! isempty (errmsg)) error ("timetable.sortrows: %s", errmsg); endif tbl = subsetrows (this, index); endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttB} =} unique (@var{ttA}) ## @deftypefnx {timetable} {@var{ttB} =} unique (@var{ttA}, @var{setOrder}) ## @deftypefnx {timetable} {[@var{ttB}, @var{ia}, @var{ic}] =} unique (@dots{}) ## ## Unique rows of a timetable. ## ## @code{@var{ttB} = unique (@var{ttA})} returns the distinct rows of ## @var{ttA}, ordered by row time. A row is its time together with its ## values, so two rows count as one only when they agree on both. Rows ## sharing a time but differing in a variable are all kept, as are rows ## sharing their values at different times; the row times are a column of ## the comparison and not a key that overrides it. ## ## @code{@var{ttB} = unique (@var{ttA}, @var{setOrder})} chooses the ## order of the result. @qcode{'sorted'} is the default and ## @qcode{'stable'} keeps the rows in the order they were met. ## @qcode{'first'} and @qcode{'last'} say which of a set of equal rows is ## the one reported in @var{ia}. @qcode{'rows'} is accepted and changes ## nothing, rows being the only thing a timetable compares. ## ## @code{[@var{ttB}, @var{ia}, @var{ic}] = unique (@dots{})} also returns ## index vectors, such that @var{ttB} is @code{@var{ttA}(@var{ia},:)} and ## @var{ttA} is @code{@var{ttB}(@var{ic},:)}. ## ## A timetable with no variables is compared on its row times alone, so ## repeated times reduce to one. The time step is read afresh from the ## row times that survive. ## ## Variables of @code{cell}, other than a cell array of character ## vectors, and of @code{struct} have no order to compare and are ## refused. ## ## @seealso{sortrows, timetable} ## @end deftypefn function [tbl, ia, ic] = unique (this, varargin) [ia, ic, errmsg] = uniqueIndex (this, varargin); if (! isempty (errmsg)) error ("timetable.unique: %s", errmsg); endif tbl = subsetrows (this, ia); endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttB} =} topkrows (@var{ttA}, @var{k}) ## @deftypefnx {timetable} {@var{ttB} =} topkrows (@var{ttA}, @var{k}, @var{vars}) ## @deftypefnx {timetable} {@var{ttB} =} topkrows (@var{ttA}, @var{k}, @var{vars}, @var{direction}) ## @deftypefnx {timetable} {@var{ttB} =} topkrows (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {timetable} {[@var{ttB}, @var{index}] =} topkrows (@dots{}) ## ## The top @var{k} rows of a timetable, in sorted order. ## ## @code{@var{ttB} = topkrows (@var{ttA}, @var{k})} returns the @var{k} ## rows with the latest row times, latest first. This is where it parts ## company with @code{head}, which takes the first rows as they are ## stored: @code{topkrows} ranks them. ## ## @code{@var{ttB} = topkrows (@var{ttA}, @var{k}, @var{vars})} ranks by ## one or more variables instead, named, indexed by number, selected by a ## logical vector or picked out by a @code{vartype}. The row dimension ## name may be used to rank by the row times explicitly. A numeric index ## counts the variables, and its sign is read the other way round from ## @code{sortrows}: a positive index ranks downwards. ## ## @code{@var{ttB} = topkrows (@dots{}, @var{direction})} sorts as ## @qcode{'ascend'} or @qcode{'descend'}, the latter being the default ## and what makes these the top rows rather than the bottom ones. ## ## Missing keys rank last however the sort runs, which is the one place ## @code{topkrows} differs from @code{sortrows} beyond its direction. ## @qcode{'MissingPlacement'} overrides that, and ## @qcode{'ComparisonMethod'} applies to numeric variables as usual. ## ## Asking for more rows than there are returns all of them, still ## ranked, and a @var{k} of zero returns none. ## ## @code{[@var{ttB}, @var{index}] = topkrows (@dots{})} also returns the ## rows chosen, so that @code{@var{ttA}(@var{index},:)} is @var{ttB}. ## ## The time step is read afresh from the row times that are kept, so the ## default ranking of a regular timetable steps backwards. ## ## @seealso{timetable.head, sortrows, timetable} ## @end deftypefn function [tbl, ix] = topkrows (this, k, varargin) [ix, errmsg] = topkrowsIndex (this, k, varargin); if (! isempty (errmsg)) error ("timetable.topkrows: %s", errmsg); endif tbl = subsetrows (this, ix); endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{TF} =} issorted (@var{tt}) ## ## True when the row times of a timetable are in ascending order. ## ## @code{@var{TF} = issorted (@var{tt})} returns true when each row time ## is at or after the one before it. The ordering is not strict, so ## repeated times are sorted; a timetable with any missing row time is ## not, and one with fewer than two rows has nothing out of order and so ## is. Only the row times are read: the variables take no part. ## ## This is the whole of the question @code{issorted} answers here. It ## takes no dimension, no direction and no options; use ## @code{issortedrows} to ask about a direction, about a variable, or ## about where missing values should fall. ## ## A @code{table} has no @code{issorted}, its row names being labels ## rather than an ordering. ## ## @seealso{timetable.issortedrows, sortrows, timetable} ## @end deftypefn function TF = issorted (this, varargin) if (nargin > 1) error (strcat ("timetable.issorted: no options are accepted;", ... " use 'issortedrows' instead.")); endif TF = issorted (this.RowTimes); endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{TF} =} issortedrows (@var{tt}) ## @deftypefnx {timetable} {@var{TF} =} issortedrows (@var{tt}, @var{rowDimName}) ## @deftypefnx {timetable} {@var{TF} =} issortedrows (@var{tt}, @var{vars}) ## @deftypefnx {timetable} {@var{TF} =} issortedrows (@var{tt}, @dots{}, @var{direction}) ## @deftypefnx {timetable} {@var{TF} =} issortedrows (@dots{}, @var{Name}, @var{Value}) ## ## True when the rows of a timetable are already in a given order. ## ## @code{@var{TF} = issortedrows (@var{tt})} returns true when the rows ## are in ascending order of their row times, which is the order ## @code{sortrows} would put them in with nothing else asked. ## ## Every form @code{sortrows} accepts is accepted here and asks the same ## question of it: the row dimension name, one or more variables named, ## indexed or selected, a direction, and the @qcode{'MissingPlacement'} ## and @qcode{'ComparisonMethod'} pairs. A direction must follow the ## keys it applies to, as it must there. ## ## The sort is stable, so the answer is exactly whether sorting would ## leave every row where it already is. ## ## @seealso{issorted, sortrows, timetable} ## @end deftypefn function TF = issortedrows (this, varargin) [TF, errmsg] = issortedrowsCheck (this, varargin); if (! isempty (errmsg)) error ("timetable.issortedrows: %s", errmsg); endif endfunction endmethods ################################################################################ ## ** Range Predicates ** ## ################################################################################ ## Available Methods ## ## ## ## 'containsrange' 'overlapsrange' 'withinrange' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timetable} {@var{TF} =} containsrange (@var{tt}, @var{ref}) ## @deftypefnx {timetable} {[@var{TF}, @var{whichRows}] =} containsrange (@var{tt}, @var{ref}) ## ## True when a timetable spans the whole of a range of times. ## ## @code{@var{TF} = containsrange (@var{tt}, @var{ref})} returns true when ## every instant of @var{ref} lies inside the range of @var{tt}, that is ## between its earliest and its latest row time, both included. The row ## times need be neither sorted nor unique, since only the two ends of the ## range are read, and missing row times take no part in it. ## ## @var{ref} says what range is meant, in one of three ways. A ## @code{timerange} gives its own bounds, and says at each end whether the ## bound itself belongs to the range. A @code{timetable} gives the range ## between its earliest and its latest row time, both ends included. A ## @code{datetime} or @code{duration} scalar gives a single instant. A ## reference whose times are of the other kind is refused: elapsed time ## and a calendar cannot be compared. ## ## @code{[@var{TF}, @var{whichRows}] = containsrange (@dots{})} also ## returns a column of logicals, one per row of @var{tt}, saying which of ## its rows fall in @var{ref}. That answer is the same for all three ## range predicates and is independent of @var{TF}: rows may fall in a ## range the timetable does not contain. ## ## A timetable whose row times are all missing has no range and answers ## false, and so does a reference naming no instant, such as a @code{NaT}. ## ## @seealso{timetable.overlapsrange, timetable.withinrange, timerange, ## timetable} ## @end deftypefn function [TF, whichRows] = containsrange (this, ref) if (nargin < 2) error ("timetable.containsrange: not enough input arguments."); endif [R, errmsg] = rangeRef (ref, this.RowTimes); if (! isempty (errmsg)) error ("timetable.containsrange: %s", errmsg); endif whichRows = R.whichRows; [tlo, thi, spanOk] = timeSpan (this.RowTimes); TF = spanOk && R.valid && R.hasLo && R.hasHi ... && R.lo >= tlo && R.hi <= thi; endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{TF} =} overlapsrange (@var{tt}, @var{ref}) ## @deftypefnx {timetable} {[@var{TF}, @var{whichRows}] =} overlapsrange (@var{tt}, @var{ref}) ## ## True when a timetable and a range of times have any instant in common. ## ## @code{@var{TF} = overlapsrange (@var{tt}, @var{ref})} returns true when ## the range of @var{tt}, from its earliest to its latest row time, shares ## at least one instant with @var{ref}. Meeting at a single instant is ## enough: a range that starts exactly at the last row time overlaps. ## ## Whether the ends themselves count is @var{ref}'s to say. A ## @code{timerange} that excludes its lower bound does not overlap a ## timetable that reaches only as far as that bound. ## ## @var{ref} takes the same three forms as in @code{containsrange}: a ## @code{timerange}, a @code{timetable} whose earliest and latest row ## times give the range, or a @code{datetime} or @code{duration} scalar ## naming one instant. ## ## @code{[@var{TF}, @var{whichRows}] = overlapsrange (@dots{})} also ## returns a column of logicals saying which rows of @var{tt} fall in ## @var{ref}. A timetable can overlap a range without any of its rows ## falling in it, the overlap being of the ranges and not of the rows. ## ## @seealso{timetable.containsrange, timetable.withinrange, timerange, ## timetable} ## @end deftypefn function [TF, whichRows] = overlapsrange (this, ref) if (nargin < 2) error ("timetable.overlapsrange: not enough input arguments."); endif [R, errmsg] = rangeRef (ref, this.RowTimes); if (! isempty (errmsg)) error ("timetable.overlapsrange: %s", errmsg); endif whichRows = R.whichRows; [tlo, thi, spanOk] = timeSpan (this.RowTimes); if (! (spanOk && R.valid)) TF = false; return endif above = ! R.hasLo || boundMet (thi, R.lo, R.closedLeft, 'above'); below = ! R.hasHi || boundMet (tlo, R.hi, R.closedRight, 'below'); TF = above && below; endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{TF} =} withinrange (@var{tt}, @var{ref}) ## @deftypefnx {timetable} {[@var{TF}, @var{whichRows}] =} withinrange (@var{tt}, @var{ref}) ## ## True when the whole range of a timetable lies inside a range of times. ## ## @code{@var{TF} = withinrange (@var{tt}, @var{ref})} returns true when ## every row time of @var{tt} lies inside @var{ref}, which is to say that ## its earliest and its latest row time both do. ## ## Whether the ends themselves count is @var{ref}'s to say, and this is ## where it tells. A @code{timerange} is half open by default, excluding ## the time it ends at, so a timetable whose last row falls exactly on ## that time is @emph{not} within it; the same range built closed ## contains that instant, and the answer turns true. ## ## @var{ref} takes the same three forms as in @code{containsrange}: a ## @code{timerange}, a @code{timetable} whose earliest and latest row ## times give the range, or a @code{datetime} or @code{duration} scalar ## naming one instant. A timetable is within a single instant only when ## every row time it has is that instant. ## ## @code{[@var{TF}, @var{whichRows}] = withinrange (@dots{})} also returns ## a column of logicals saying which rows of @var{tt} fall in @var{ref}. ## When @var{TF} is true they are all true, this being the one predicate ## for which the two answers agree. ## ## @seealso{timetable.containsrange, timetable.overlapsrange, timerange, ## timetable} ## @end deftypefn function [TF, whichRows] = withinrange (this, ref) if (nargin < 2) error ("timetable.withinrange: not enough input arguments."); endif [R, errmsg] = rangeRef (ref, this.RowTimes); if (! isempty (errmsg)) error ("timetable.withinrange: %s", errmsg); endif whichRows = R.whichRows; [tlo, thi, spanOk] = timeSpan (this.RowTimes); if (! (spanOk && R.valid)) TF = false; return endif above = ! R.hasLo || boundMet (tlo, R.lo, R.closedLeft, 'above'); below = ! R.hasHi || boundMet (thi, R.hi, R.closedRight, 'below'); TF = above && below; endfunction endmethods ################################################################################ ## ** Apply Functions to Timetable Contents ** ## ################################################################################ ## Available Methods ## ## ## ## 'varfun' 'rowfun' 'grouptransform' 'groupcounts' ## ## 'groupsummary' 'groupfilter' 'stack' 'rows2vars' ## ## 'join' 'innerjoin' 'outerjoin' 'inner2outer' ## ## 'findgroups' 'splitapply' 'unstack' 'pivot' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timetable} {@var{B} =} varfun (@var{func}, @var{A}) ## @deftypefnx {timetable} {@var{B} =} varfun (@var{func}, @var{A}, @var{Name}, @var{Value}, @dots{}) ## ## Apply a function to each variable of a timetable. ## ## @code{@var{B} = varfun (@var{func}, @var{A})} applies the function ## handle @var{func} separately to each variable of the timetable @var{A} ## and returns the results in the timetable @var{B}. Each variable is ## passed whole, so a function that reduces a column to a scalar gives a ## one-row result. The output variables are named for the function and ## the variable they came from, as in @qcode{mean_Speed}, and every row of ## the result carries the row time of the first row it was computed from. ## ## @code{@var{B} = varfun (@var{func}, @var{A}, @var{Name}, @var{Value}, ## @dots{})} modifies the operation through the following ## @var{Name}/@var{Value} pairs: ## ## @table @asis ## @item @qcode{'InputVariables'} ## The variables of @var{A} that @var{func} is applied to, given as ## variable names, indices, a logical vector, or a function handle. By ## default every variable of @var{A} that is not a grouping variable is ## used. ## ## @item @qcode{'GroupingVariables'} ## One or more variables of @var{A} that define groups of rows, or the row ## dimension name, which groups by the row times themselves. @var{func} ## is then applied once to each group and @var{B} has one row per group, ## carrying the row time of the first row of the group. Grouping ## variables appear in @var{B} alongside a @qcode{GroupCount} variable; ## the row times do not, being the row times of the result. Rows with a ## missing value in any grouping variable are omitted. ## ## @item @qcode{'OutputFormat'} ## One of @qcode{'table'} (the default, and also selected by ## @qcode{'auto'}), which returns the results in a timetable; ## @qcode{'uniform'}, which requires @var{func} to return a scalar and ## returns them in an array; or @qcode{'cell'}, which returns them in a ## cell array. ## ## @item @qcode{'ErrorHandler'} ## A function handle called whenever @var{func} raises an error, receiving ## a structure with fields @qcode{identifier}, @qcode{message} and ## @qcode{index}, followed by the arguments @var{func} was called with. ## Its outputs are used in place of the ones @var{func} did not return. ## @end table ## ## @end deftypefn function B = varfun (func, A, varargin) if (nargin < 2) error ("timetable.varfun: too few input arguments."); endif [B, errmsg] = varfunResult (A, func, varargin); if (! isempty (errmsg)) error ("timetable.varfun: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{B} =} rowfun (@var{func}, @var{A}) ## @deftypefnx {timetable} {@var{B} =} rowfun (@var{func}, @var{A}, @var{Name}, @var{Value}, @dots{}) ## ## Apply a function to each row of a timetable. ## ## @code{@var{B} = rowfun (@var{func}, @var{A})} applies the function ## handle @var{func} to each row of the timetable @var{A} and returns the ## results in the timetable @var{B}, which has one row for each row of ## @var{A} and carries its row times. By default the value of each ## variable in the row is passed to @var{func} as a separate input ## argument, and the output variables of @var{B} are named @qcode{Var1}, ## @qcode{Var2}, and so on. ## ## @code{@var{B} = rowfun (@var{func}, @var{A}, @var{Name}, @var{Value}, ## @dots{})} modifies the operation through the following ## @var{Name}/@var{Value} pairs: ## ## @table @asis ## @item @qcode{'InputVariables'} ## The variables of @var{A} that are passed to @var{func}, given as ## variable names, indices, a logical vector, or a function handle. By ## default every variable of @var{A} that is not a grouping variable is ## used. ## ## @item @qcode{'GroupingVariables'} ## One or more variables of @var{A} that define groups of rows, or the row ## dimension name, which groups by the row times themselves. @var{func} ## is then applied once to each group, receiving the values of each input ## variable across the rows of the group, and @var{B} has one row per ## group, carrying the row time of the first row of the group. Grouping ## variables appear in @var{B} alongside a @qcode{GroupCount} variable; ## the row times do not, being the row times of the result. Rows with a ## missing value in any grouping variable are omitted. ## ## @item @qcode{'OutputVariableNames'} ## The names of the output variables of @var{B}, one per output of ## @var{func}. ## ## @item @qcode{'NumOutputs'} ## The number of output arguments to request from @var{func}. It defaults ## to the number of @qcode{'OutputVariableNames'} if those are given, ## otherwise to @code{1}. ## ## @item @qcode{'SeparateInputs'} ## A logical scalar. When @code{true} (the default), the value of each ## input variable is passed to @var{func} as a separate argument. When ## @code{false}, the values of the row are joined into one array, as ## @code{table2array} joins the variables, and passed as a single ## argument. Variables that cannot form one array are refused. ## ## @item @qcode{'ExtractCellContents'} ## A logical scalar. When @code{true}, the contents of cell-valued ## variables are extracted before being passed to @var{func}. It defaults ## to @code{false}. ## ## @item @qcode{'OutputFormat'} ## One of @qcode{'table'} (the default, and also selected by ## @qcode{'auto'}), which returns the results in a timetable; ## @qcode{'uniform'}, which requires @var{func} to return a scalar and ## returns them in an array; or @qcode{'cell'}, which returns them in a ## cell array. ## ## @item @qcode{'ErrorHandler'} ## A function handle called whenever @var{func} raises an error, receiving ## a structure with fields @qcode{identifier}, @qcode{message} and ## @qcode{index}, followed by the arguments @var{func} was called with. ## Its outputs are used in place of the ones @var{func} did not return. ## @end table ## ## @end deftypefn function B = rowfun (func, A, varargin) if (nargin < 2) error ("timetable.rowfun: too few input arguments."); endif [B, errmsg] = rowfunResult (A, func, varargin); if (! isempty (errmsg)) error ("timetable.rowfun: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{G} =} grouptransform (@var{tt}, @var{groupvars}, @var{method}) ## @deftypefnx {timetable} {@var{G} =} grouptransform (@var{tt}, @var{groupvars}, @var{method}, @var{datavars}) ## @deftypefnx {timetable} {@var{G} =} grouptransform (@var{tt}, @var{groupvars}, @var{groupbins}, @var{method}, @dots{}) ## @deftypefnx {timetable} {@var{G} =} grouptransform (@dots{}, @var{Name}, @var{Value}) ## ## Transform the variables of a timetable group by group. ## ## @code{@var{G} = grouptransform (@var{tt}, @var{groupvars}, ## @var{method})} groups the rows of @var{tt} by the variables named in ## @var{groupvars} and applies @var{method} to each data variable within ## each group. The result has the same rows as @var{tt}, in the same ## order and carrying the same row times, so a transform never moves a row ## in time. ## ## @var{groupvars} names one or more variables, or the row dimension name, ## which groups by the row times themselves. @var{method} is one of ## @qcode{'zscore'}, @qcode{'norm'}, @qcode{'meancenter'}, ## @qcode{'rescale'}, @qcode{'meanfill'} and @qcode{'linearfill'}, or a ## function handle applied to each group. ## ## @code{@var{G} = grouptransform (@var{tt}, @var{groupvars}, ## @var{method}, @var{datavars})} transforms only the variables named in ## @var{datavars}. By default every variable that is not a grouping ## variable is transformed. ## ## @code{@var{G} = grouptransform (@var{tt}, @var{groupvars}, ## @var{groupbins}, @var{method}, @dots{})} bins the grouping variables ## before grouping, @var{groupbins} being a bin count, a vector of edges, ## or a time unit for a datetime or duration variable. ## ## A duration is binned by the units that have a fixed length of their ## own, @qcode{'second'}, @qcode{'minute'}, @qcode{'hour'}, ## @qcode{'day'} and @qcode{'year'}; a week, a month and a quarter have ## none and are refused. A narrow bin over a wide span asks for more ## bins than a @code{categorical} can name, and the call is refused ## rather than answered; MATLAB stops at 65536 bins there and returns a ## grid that does not reach the end of the span. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @table @asis ## @item @qcode{'ReplaceValues'} ## A logical scalar. When @code{true} (the default) each transformed ## variable replaces the one it came from; when @code{false} the results ## are appended as new variables named for the method and the variable ## they came from, as in @qcode{zscore_Speed}. ## ## @item @qcode{'IncludedEdge'} ## Which edge of a bin is included, @qcode{'left'} (the default) or ## @qcode{'right'}. It applies only where @var{groupbins} was given. ## @end table ## ## @end deftypefn function G = grouptransform (tt, groupvars, varargin) if (nargin < 3) error ("timetable.grouptransform: too few input arguments."); endif [G, errmsg] = grouptransformResult (tt, groupvars, varargin); if (! isempty (errmsg)) error ("timetable.grouptransform: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{G} =} groupcounts (@var{tt}, @var{groupvars}) ## @deftypefnx {timetable} {@var{G} =} groupcounts (@var{tt}, @var{groupvars}, @var{groupbins}) ## @deftypefnx {timetable} {@var{G} =} groupcounts (@dots{}, @var{Name}, @var{Value}) ## ## Count the rows of a timetable in each group. ## ## @code{@var{G} = groupcounts (@var{tt}, @var{groupvars})} groups the ## rows of @var{tt} by the variables named in @var{groupvars} and returns ## a @code{table} with one row per group, carrying the grouping variables, ## a @qcode{GroupCount} variable and a @qcode{Percent} variable. The ## result is a table and not a timetable: its rows describe groups rather ## than instants, so there is nothing left for row times to label. ## ## @var{groupvars} names one or more variables, or the row dimension name, ## which groups by the row times themselves. ## ## @code{@var{G} = groupcounts (@var{tt}, @var{groupvars}, ## @var{groupbins})} bins the grouping variables before grouping, ## @var{groupbins} being a bin count, a vector of edges, or a time unit ## for a datetime or duration variable. ## ## A duration is binned by the units that have a fixed length of their ## own, @qcode{'second'}, @qcode{'minute'}, @qcode{'hour'}, ## @qcode{'day'} and @qcode{'year'}; a week, a month and a quarter have ## none and are refused. A narrow bin over a wide span asks for more ## bins than a @code{categorical} can name, and the call is refused ## rather than answered; MATLAB stops at 65536 bins there and returns a ## grid that does not reach the end of the span. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @table @asis ## @item @qcode{'IncludeMissingGroups'} ## A logical scalar. When @code{true} (the default) rows whose grouping ## value is missing form a group of their own, sorted last. ## ## @item @qcode{'IncludeEmptyGroups'} ## A logical scalar. When @code{true} the unused categories of a ## categorical or binned grouping variable are reported as groups with a ## count of zero. It defaults to @code{false}. ## ## @item @qcode{'IncludedEdge'} ## Which edge of a bin is included, @qcode{'left'} (the default) or ## @qcode{'right'}. It applies only where @var{groupbins} was given. ## @end table ## ## @end deftypefn function G = groupcounts (tt, groupvars, varargin) if (nargin < 2) error ("timetable.groupcounts: too few input arguments."); endif [G, errmsg] = groupcountsResult (tt, groupvars, varargin); if (! isempty (errmsg)) error ("timetable.groupcounts: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{G} =} groupsummary (@var{tt}, @var{groupvars}) ## @deftypefnx {timetable} {@var{G} =} groupsummary (@var{tt}, @var{groupvars}, @var{method}) ## @deftypefnx {timetable} {@var{G} =} groupsummary (@var{tt}, @var{groupvars}, @var{method}, @var{datavars}) ## @deftypefnx {timetable} {@var{G} =} groupsummary (@var{tt}, @var{groupvars}, @var{groupbins}, @dots{}) ## @deftypefnx {timetable} {@var{G} =} groupsummary (@dots{}, @var{Name}, @var{Value}) ## ## Summarise the variables of a timetable group by group. ## ## @code{@var{G} = groupsummary (@var{tt}, @var{groupvars})} groups the ## rows of @var{tt} by the variables named in @var{groupvars} and returns ## a @code{table} with one row per group, carrying the grouping variables ## and a @qcode{GroupCount} variable. The result is a table and not a ## timetable: its rows describe groups rather than instants, so there is ## nothing left for row times to label. ## ## @var{groupvars} names one or more variables, or the row dimension name, ## which groups by the row times themselves. ## ## @code{@var{G} = groupsummary (@var{tt}, @var{groupvars}, @var{method})} ## also applies @var{method} to each data variable of each group, adding ## one variable per method and data variable named for both, as in ## @qcode{mean_Speed}. @var{method} is one of @qcode{'mean'}, ## @qcode{'sum'}, @qcode{'min'}, @qcode{'max'}, @qcode{'range'}, ## @qcode{'median'}, @qcode{'mode'}, @qcode{'var'}, @qcode{'std'}, ## @qcode{'nummissing'}, @qcode{'nnz'} and @qcode{'numunique'}, or ## @qcode{'all'} for every one of them in that order, or a function handle, ## or a cell array of any of these. A name may be abbreviated to any ## unambiguous prefix. @qcode{'std'} and @qcode{'var'} are not defined on ## an integer variable and refuse one. ## ## @code{@var{G} = groupsummary (@var{tt}, @var{groupvars}, @var{method}, ## @var{datavars})} summarises only the variables named in @var{datavars}. ## By default every variable that is not a grouping variable is used. ## ## @code{@var{G} = groupsummary (@var{tt}, @var{groupvars}, ## @var{groupbins}, @dots{})} bins the grouping variables before grouping, ## @var{groupbins} being a bin count, a vector of edges, or a time unit for ## a datetime or duration variable. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @table @asis ## @item @qcode{'IncludeMissingGroups'} ## A logical scalar. When @code{true} (the default) rows whose grouping ## value is missing form a group of their own, sorted last. ## ## @item @qcode{'IncludeEmptyGroups'} ## A logical scalar. When @code{true} the unused categories of a ## categorical or binned grouping variable are reported as groups with a ## count of zero. It defaults to @code{false}. ## ## @item @qcode{'IncludedEdge'} ## Which edge of a bin is included, @qcode{'left'} (the default) or ## @qcode{'right'}. It applies only where @var{groupbins} was given. ## @end table ## ## @end deftypefn function G = groupsummary (tt, groupvars, varargin) if (nargin < 2) error ("timetable.groupsummary: too few input arguments."); endif [G, errmsg] = groupsummaryResult (tt, groupvars, varargin); if (! isempty (errmsg)) error ("timetable.groupsummary: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{G} =} groupfilter (@var{tt}, @var{groupvars}, @var{method}) ## @deftypefnx {timetable} {@var{G} =} groupfilter (@var{tt}, @var{groupvars}, @var{method}, @var{datavars}) ## @deftypefnx {timetable} {@var{G} =} groupfilter (@var{tt}, @var{groupvars}, @var{groupbins}, @var{method}, @dots{}) ## @deftypefnx {timetable} {@var{G} =} groupfilter (@dots{}, @qcode{'IncludedEdge'}, @var{edge}) ## ## Keep the rows of a timetable whose group passes a test. ## ## @code{@var{G} = groupfilter (@var{tt}, @var{groupvars}, @var{method})} ## groups the rows of @var{tt} by the variables named in @var{groupvars}, ## applies the function handle @var{method} to each data variable of each ## group, and returns the rows of the groups it accepted. @var{method} ## receives the values of one variable across the rows of one group and ## answers with a logical scalar, keeping or dropping the whole group, or ## with one logical per row of the group, keeping those rows. The rows ## that survive keep their order and their row times. ## ## @var{groupvars} names one or more variables, or the row dimension name, ## which groups by the row times themselves. ## ## @code{@var{G} = groupfilter (@var{tt}, @var{groupvars}, @var{method}, ## @var{datavars})} applies @var{method} only to the variables named in ## @var{datavars}. By default every variable that is not a grouping ## variable is used, and a row is kept only where every one of them ## accepts it. ## ## @code{@var{G} = groupfilter (@var{tt}, @var{groupvars}, ## @var{groupbins}, @var{method}, @dots{})} bins the grouping variables ## before grouping, @var{groupbins} being a bin count, a vector of edges, ## or a time unit for a datetime or duration variable. The ## @qcode{'IncludedEdge'} option then says which edge of a bin is ## included, @qcode{'left'} (the default) or @qcode{'right'}. ## ## A duration is binned by the units that have a fixed length of their ## own, @qcode{'second'}, @qcode{'minute'}, @qcode{'hour'}, ## @qcode{'day'} and @qcode{'year'}; a week, a month and a quarter have ## none and are refused. A narrow bin over a wide span asks for more ## bins than a @code{categorical} can name, and the call is refused ## rather than answered; MATLAB stops at 65536 bins there and returns a ## grid that does not reach the end of the span. ## ## @end deftypefn function G = groupfilter (tt, groupvars, varargin) if (nargin < 3) error ("timetable.groupfilter: too few input arguments."); endif [G, errmsg] = groupfilterResult (tt, groupvars, varargin); if (! isempty (errmsg)) error ("timetable.groupfilter: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tt2} =} stack (@var{tt}, @var{vars}) ## @deftypefnx {timetable} {@var{tt2} =} stack (@var{tt}, @var{vars}, @var{Name}, @var{Value}) ## @deftypefnx {timetable} {[@var{tt2}, @var{index}] =} stack (@dots{}) ## ## Stack several variables of a timetable into one. ## ## @code{@var{tt2} = stack (@var{tt}, @var{vars})} replaces the variables ## named in @var{vars} with a single variable holding their values one ## under the other, and an indicator variable naming which of them each ## value came from. Every other variable is carried along, repeated once ## per stacked variable, and so is the row time of the row it came from: ## a timetable of two rows stacking two variables has four rows and two ## pairs of equal row times, which generally makes it irregular. ## ## @var{vars} names the variables to stack. A cell array of variable ## references stacks several groups at once, one new variable per group, ## each group holding the same number of variables. The stacked variables ## are joined as @code{table2array} joins them, and must each have one ## column; variables that cannot join are refused. ## ## @code{[@var{tt2}, @var{index}] = stack (@dots{})} also returns ## @var{index}, naming the row of @var{tt} each row of @var{tt2} came ## from. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @table @asis ## @item @qcode{'ConstantVariables'} ## The variables carried along unstacked. By default every variable that ## is not being stacked is carried. ## ## @item @qcode{'NewDataVariableName'} ## The name of the stacked variable, one per group. By default the names ## of the stacked variables are joined with underscores. ## ## @item @qcode{'IndexVariableName'} ## The name of the indicator variable. By default it is the name of the ## stacked variable followed by @qcode{_Indicator}. ## @end table ## ## @seealso{timetable} ## @end deftypefn function [tt2, index] = stack (tt, vars, varargin) if (nargin < 2) vars = []; endif [tt2, index, errmsg] = stackResult (tt, vars, varargin); if (! isempty (errmsg)) error ("timetable.stack: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tbl} =} rows2vars (@var{tt}) ## @deftypefnx {timetable} {@var{tbl} =} rows2vars (@var{tt}, @var{Name}, @var{Value}) ## ## Turn the rows of a timetable into variables. ## ## @code{@var{tbl} = rows2vars (@var{tt})} returns a @code{table} whose ## rows are the variables of @var{tt} and whose variables are its rows. ## The first variable, @qcode{OriginalVariableNames}, names the variables ## of @var{tt}; the rest are named for the row times they came from, ## rendered as the timetable displays them and made into valid names, and ## the second dimension of the result takes the name of the row dimension. ## ## The result is a table and not a timetable: its rows are variables and ## there is no longer a time to label them by. ## ## Where the variables are not all of one type the result holds cell ## arrays, every value being wrapped so that one variable can carry them ## all. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @table @asis ## @item @qcode{'DataVariables'} ## The variables of @var{tt} that become rows. By default all of them do. ## ## @item @qcode{'VariableNamesSource'} ## A variable of @var{tt} whose values name the new variables instead of ## the row times. A repeated name is numbered rather than refused, and ## the second dimension of the result takes the name of that variable. ## ## @item @qcode{'VariableNamingRule'} ## @qcode{'modify'} (the default) makes each new name a valid identifier; ## @qcode{'preserve'} keeps it as it is. ## @end table ## ## @end deftypefn function tbl = rows2vars (tt, varargin) [tbl, errmsg] = rows2varsResult (tt, varargin); if (! isempty (errmsg)) error ("timetable.rows2vars: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttC} =} join (@var{ttL}, @var{tblR}) ## @deftypefnx {timetable} {@var{ttC} =} join (@var{ttL}, @var{tblR}, @var{Name}, @var{Value}) ## @deftypefnx {timetable} {[@var{ttC}, @var{index}] =} join (@dots{}) ## ## Join a timetable with another tabular object. ## ## @code{@var{ttC} = join (@var{ttL}, @var{tblR})} returns a timetable ## with every row of @var{ttL}, its row times kept, and beside it the row ## of @var{tblR} whose key matches. The right operand contributes ## variables only, so the result is a timetable exactly as long as ## @var{ttL}. ## ## The key must name a row of @var{tblR} for every row of @var{ttL}, and ## the keys of @var{tblR} must be unique. With no key named, two ## timetables join on their row times; a timetable and a table have no ## key in common unless one is named, since the row times are not a ## variable. ## ## @code{[@var{ttC}, @var{index}] = join (@dots{})} also returns ## @var{index}, naming the row of @var{tblR} each row took. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @table @asis ## @item @qcode{'Keys'} ## The variables to match on, named on both sides. The row dimension name ## names the row times. ## ## @item @qcode{'LeftKeys'}, @qcode{'RightKeys'} ## The variables to match on, named separately for each side and given ## together. They must name the same number of keys. ## ## @item @qcode{'LeftVariables'}, @qcode{'RightVariables'} ## The variables each side contributes. By default the left contributes ## all of its own and the right all but its keys. ## ## @item @qcode{'KeepOneCopy'} ## Variables that both sides carry and only the left contributes. ## @end table ## ## A variable name carried by both sides and contributed by both is ## suffixed with the caller's own name for each operand, falling back to ## @qcode{_left} and @qcode{_right}. ## ## Where an operand is an @code{eventtable}, or carries one on its ## @qcode{Events} property, the result is an event table too and the ## operands' event tables are merged: an outer join keyed on the event ## times and every variable the two share, ordered by those keys. The ## three properties saying which variables describe the events must ## agree, and the operation is refused where they do not. ## ## @end deftypefn function [ttC, index] = join (ttL, tblR, varargin) if (nargin < 2) error ("timetable.join: too few input arguments."); endif ## The caller's own names for the operands are read here, before the ## shared body, which cannot see them. [ttC, index, errmsg] = joinResult (ttL, tblR, varargin, ... inputname (1), inputname (2)); if (! isempty (errmsg)) error ("timetable.join: %s", errmsg); endif [ttC, errmsg] = carryEvents (ttC, {ttL, tblR}); if (! isempty (errmsg)) error ("timetable.join: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttC} =} innerjoin (@var{ttL}, @var{tblR}) ## @deftypefnx {timetable} {@var{ttC} =} innerjoin (@var{ttL}, @var{tblR}, @var{Name}, @var{Value}) ## @deftypefnx {timetable} {[@var{ttC}, @var{iL}, @var{iR}] =} innerjoin (@dots{}) ## ## Join a timetable with another tabular object, keeping matched rows. ## ## @code{@var{ttC} = innerjoin (@var{ttL}, @var{tblR})} returns a ## timetable holding one row for every pair of rows of @var{ttL} and ## @var{tblR} whose keys match, in key order. Each row carries the row ## time of the row of @var{ttL} it came from, so a key matched more than ## once repeats its row time and the result is generally irregular. ## ## With no key named, two timetables join on their row times; a timetable ## and a table have no key in common unless one is named, since the row ## times are not a variable. ## ## @code{[@var{ttC}, @var{iL}, @var{iR}] = innerjoin (@dots{})} also ## returns the rows of @var{ttL} and of @var{tblR} each row came from. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @table @asis ## @item @qcode{'Keys'} ## The variables to match on, named on both sides. The row dimension name ## names the row times. ## ## @item @qcode{'LeftKeys'}, @qcode{'RightKeys'} ## The variables to match on, named separately for each side and given ## together. ## ## @item @qcode{'LeftVariables'}, @qcode{'RightVariables'} ## The variables each side contributes. By default the left contributes ## all of its own and the right all but its keys. ## @end table ## ## Where an operand is an @code{eventtable}, or carries one on its ## @qcode{Events} property, the result is an event table too and the ## operands' event tables are merged: an outer join keyed on the event ## times and every variable the two share, ordered by those keys. The ## three properties saying which variables describe the events must ## agree, and the operation is refused where they do not. ## ## @end deftypefn function [ttC, iL, iR] = innerjoin (ttL, tblR, varargin) if (nargin < 2) error ("timetable.innerjoin: too few input arguments."); endif [ttC, iL, iR, errmsg] = innerjoinResult (ttL, tblR, varargin, ... inputname (1), inputname (2)); if (! isempty (errmsg)) error ("timetable.innerjoin: %s", errmsg); endif [ttC, errmsg] = carryEvents (ttC, {ttL, tblR}); if (! isempty (errmsg)) error ("timetable.innerjoin: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttC} =} outerjoin (@var{ttL}, @var{tblR}) ## @deftypefnx {timetable} {@var{ttC} =} outerjoin (@var{ttL}, @var{tblR}, @var{Name}, @var{Value}) ## @deftypefnx {timetable} {[@var{ttC}, @var{iL}, @var{iR}] =} outerjoin (@dots{}) ## ## Join a timetable with another tabular object, keeping unmatched rows. ## ## @code{@var{ttC} = outerjoin (@var{ttL}, @var{tblR})} returns a ## timetable holding one row for every pair of rows of @var{ttL} and ## @var{tblR} whose keys match, and one for every row of either that ## matched nothing, its variables from the other side filled with missing ## values. A row that came from a row of @var{ttL} carries its row time; ## a row that matched nothing on the left has none of its own. ## ## With no key named, two timetables join on their row times; a timetable ## and a table have no key in common unless one is named. ## ## @code{[@var{ttC}, @var{iL}, @var{iR}] = outerjoin (@dots{})} also ## returns the rows of @var{ttL} and of @var{tblR} each row came from, ## @code{0} where it came from neither. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @table @asis ## @item @qcode{'Keys'}, @qcode{'LeftKeys'}, @qcode{'RightKeys'} ## The variables to match on. The row dimension name names the row times. ## ## @item @qcode{'LeftVariables'}, @qcode{'RightVariables'} ## The variables each side contributes. By default each contributes all ## of its own, keys included. ## ## @item @qcode{'Type'} ## @qcode{'full'} (the default) keeps the unmatched rows of both sides, ## @qcode{'left'} and @qcode{'right'} only those of the side named. ## ## @item @qcode{'MergeKeys'} ## A logical scalar. When @code{true} each pair of keys becomes a single ## variable in the left one's position, taking its value from whichever ## side had a row. ## @end table ## ## Where an operand is an @code{eventtable}, or carries one on its ## @qcode{Events} property, the result is an event table too and the ## operands' event tables are merged: an outer join keyed on the event ## times and every variable the two share, ordered by those keys. The ## three properties saying which variables describe the events must ## agree, and the operation is refused where they do not. ## ## @end deftypefn function [ttC, iL, iR] = outerjoin (ttL, tblR, varargin) if (nargin < 2) error ("timetable.outerjoin: too few input arguments."); endif [ttC, iL, iR, errmsg] = outerjoinResult (ttL, tblR, varargin, ... inputname (1), inputname (2)); if (! isempty (errmsg)) error ("timetable.outerjoin: %s", errmsg); endif [ttC, errmsg] = carryEvents (ttC, {ttL, tblR}); if (! isempty (errmsg)) error ("timetable.outerjoin: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tt2} =} inner2outer (@var{tt}) ## ## Turn the nesting of a timetable inside out. ## ## @code{@var{tt2} = inner2outer (@var{tt})} returns a timetable whose ## variables are named for the variables of the tables nested inside ## @var{tt}, each holding a nested table named for the variables of ## @var{tt} that held them. The rows and their row times are untouched: ## only the nesting is turned inside out. ## ## An inner variable name held by one nested table alone becomes a plain ## variable rather than a nested one. A timetable holding nothing nested ## is returned as it is. ## ## @seealso{timetable} ## @end deftypefn function tt2 = inner2outer (tt) [tt2, errmsg] = inner2outerResult (tt); if (! isempty (errmsg)) error ("timetable.inner2outer: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{G} =} findgroups (@var{tt}) ## @deftypefnx {timetable} {[@var{G}, @var{TID}] =} findgroups (@var{tt}) ## ## Number the groups the rows of a timetable fall into. ## ## @code{@var{G} = findgroups (@var{tt})} returns a column of group ## numbers, one per row of @var{tt}, numbering the distinct combinations ## of its variables in sorted order. A row holding a missing value in any ## variable belongs to no group and is numbered @code{NaN}. ## ## The row times take no part: a timetable groups by its variables, as a ## table does. ## ## @code{[@var{G}, @var{TID}] = findgroups (@var{tt})} also returns a ## @code{table} with one row per group, holding the combination of values ## that defines it. It is a table and not a timetable, its rows being ## groups rather than instants. ## ## @seealso{splitapply, timetable} ## @end deftypefn function [G, TID] = findgroups (tt) if (nargin != 1) error ("timetable.findgroups: too few input arguments."); endif [G, TID, errmsg] = findgroupsResult (tt); if (! isempty (errmsg)) error ("timetable.findgroups: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{Y} =} splitapply (@var{func}, @var{tt}, @var{G}) ## @deftypefnx {timetable} {[@var{Y1}, @dots{}] =} splitapply (@var{func}, @var{tt}, @var{G}) ## ## Apply a function to each group of rows of a timetable. ## ## @code{@var{Y} = splitapply (@var{func}, @var{tt}, @var{G})} splits the ## rows of @var{tt} into the groups the numbers in @var{G} name, calls ## @var{func} once per group with one argument per variable holding that ## group's rows, and stacks the results. @var{G} holds one number per row ## of @var{tt}, as @code{findgroups} returns; a row numbered @code{NaN} ## belongs to no group and is left out. ## ## The row times take no part: @var{func} receives the variables alone. ## ## @code{[@var{Y1}, @dots{}] = splitapply (@dots{})} asks @var{func} for as ## many outputs as are requested and stacks each of them. ## ## @seealso{findgroups, timetable} ## @end deftypefn function varargout = splitapply (func, tt, G) if (nargin != 3) error ("timetable.splitapply: too few input arguments."); endif nout = max (nargout, 1); [results, N, errmsg] = splitapplyResult (tt, func, G, nout); if (! isempty (errmsg)) error ("timetable.splitapply: %s", errmsg); endif varargout = cell (1, nout); for k = 1:nout varargout{k} = vertcat (results{:,k}); endfor endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tt2} =} unstack (@var{tt}, @var{vars}, @var{ivar}) ## @deftypefnx {timetable} {@var{tt2} =} unstack (@var{tt}, @var{vars}, @var{ivar}, @var{Name}, @var{Value}) ## @deftypefnx {timetable} {[@var{tt2}, @var{index}] =} unstack (@dots{}) ## ## Spread one variable of a timetable across several. ## ## @code{@var{tt2} = unstack (@var{tt}, @var{vars}, @var{ivar})} replaces ## the variable named in @var{vars} with one variable per distinct value ## of the indicator variable @var{ivar}, each holding the values that ## carried that indicator. Rows sharing a group become one row. ## ## The row times group: with no @qcode{'GroupingVariables'} given, one row ## comes back per distinct row time, so @var{tt2} is a timetable carrying ## those times. A group that has no value for an indicator is filled with ## a missing value. ## ## @code{[@var{tt2}, @var{index}] = unstack (@dots{})} also returns ## @var{index}, naming a row of @var{tt} in each group. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @table @asis ## @item @qcode{'GroupingVariables'} ## The variables whose combinations define the rows of the result, ## alongside the row times. ## ## @item @qcode{'ConstantVariables'} ## The variables carried along unchanged, taken from one row of each group. ## ## @item @qcode{'NewDataVariableNames'} ## The names of the new variables, one per distinct indicator value. ## ## @item @qcode{'AggregationFunction'} ## The function applied where a group holds several values for one ## indicator. By default numeric data are summed and everything else must ## be unique within its group. ## ## @item @qcode{'VariableNamingRule'} ## @qcode{'modify'} (the default) makes each new name a valid identifier; ## @qcode{'preserve'} keeps it as it is. ## @end table ## ## @seealso{timetable.stack, timetable} ## @end deftypefn function [tt2, index] = unstack (tt, vars, ivar, varargin) if (nargin < 3) vars = []; ivar = []; endif [tt2, index, errmsg] = unstackResult (tt, vars, ivar, varargin); if (! isempty (errmsg)) error ("timetable.unstack: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{P} =} pivot (@var{tt}, @var{Name}, @var{Value}, @dots{}) ## ## Summarise a timetable across two grouping dimensions. ## ## @code{@var{P} = pivot (@var{tt}, @qcode{'Rows'}, @var{rowvars}, ## @qcode{'Columns'}, @var{colvars})} groups the rows of @var{tt} by ## @var{rowvars} down the page and by @var{colvars} across it, and returns ## a @code{table} holding one row per row group and one variable per ## column group. At least one of the two must be given. ## ## Either may name the row dimension, which groups by the row times: as ## @qcode{'Rows'} they become a variable of the result, and as ## @qcode{'Columns'} they name its variables. The result is a table and ## not a timetable, its rows and columns being groups rather than ## instants. ## ## With no @qcode{'DataVariable'} the cells count the rows of each group; ## with one they summarise its values, by @qcode{'Method'}, which defaults ## to summing them. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @table @asis ## @item @qcode{'Rows'}, @qcode{'Columns'} ## The variables grouping down the page and across it, or the row ## dimension name. ## ## @item @qcode{'DataVariable'} ## The variable summarised in each cell. ## ## @item @qcode{'Method'} ## The summary applied to each cell, a method name or a function handle. ## ## @item @qcode{'RowsBinMethod'}, @qcode{'ColumnsBinMethod'} ## Binning applied to the grouping variables before grouping, with ## @qcode{'IncludedEdge'} saying which edge of a bin is included. ## ## @item @qcode{'IncludeMissingGroups'}, @qcode{'IncludeEmptyGroups'} ## Whether a group of missing values, and whether an unused category, are ## reported. ## ## @item @qcode{'IncludeTotals'} ## Whether a marginal row and column are added, labelled ## @qcode{Overall_}. ## ## @item @qcode{'RowLabelPlacement'} ## Whether the row groups become a variable of the result or its row ## names. ## ## @item @qcode{'OutputFormat'} ## @qcode{'flat'} (the default) or @qcode{'nested'}, which groups the data ## variables into nested tables by the column hierarchy. ## @end table ## ## @seealso{groupsummary, timetable} ## @end deftypefn function P = pivot (tt, varargin) [P, errmsg] = pivotResult (tt, varargin); if (! isempty (errmsg)) error ("timetable.pivot: %s", errmsg); endif endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttB} =} retime (@var{ttA}, @var{newTimes}) ## @deftypefnx {timetable} {@var{ttB} =} retime (@var{ttA}, @var{newTimeStep}) ## @deftypefnx {timetable} {@var{ttB} =} retime (@dots{}, @var{method}) ## @deftypefnx {timetable} {@var{ttB} =} retime (@dots{}, @var{Name}, @var{Value}) ## ## Resample a timetable onto new row times. ## ## @code{@var{ttB} = retime (@var{ttA}, @var{newTimes})} returns a ## timetable whose rows sit at @var{newTimes}, a @code{datetime} or ## @code{duration} vector of the same kind as the row times of ## @var{ttA}, sorted and without repeats. ## ## @code{@var{ttB} = retime (@var{ttA}, @var{newTimeStep})} builds those ## times instead from a time unit, one of @qcode{'secondly'}, ## @qcode{'minutely'}, @qcode{'hourly'}, @qcode{'daily'}, ## @qcode{'weekly'}, @qcode{'monthly'}, @qcode{'quarterly'} and ## @qcode{'yearly'}. The grid starts at the unit's own boundary at or ## before the first row time. A filling method reads the times as sample ## points and runs the grid to the first boundary at or after the last row ## time, so three hours of data retimed @qcode{'daily'} spans two days; an ## aggregating method reads them as bins and stops at the bin holding the ## last row time, so the same three hours give one day. Weeks begin on ## Sunday. A @code{duration} row time carries no calendar, so a unit ## measures it only where a @code{duration} counts in that unit: ## @qcode{'secondly'}, @qcode{'minutely'}, @qcode{'hourly'}, ## @qcode{'daily'} and @qcode{'yearly'} are taken and @qcode{'weekly'}, ## @qcode{'monthly'} and @qcode{'quarterly'} are refused. ## ## @code{@var{ttB} = retime (@var{ttA}, @qcode{'regular'}, @dots{})} ## builds a grid from the first row time in steps of the ## @qcode{'TimeStep'} or @qcode{'SampleRate'} given after it, exactly one ## of which is required. @qcode{'SampleRate'} is in hertz. It is bounded ## by the same rule. ## ## @var{method} says how a row that the old times do not carry takes its ## value. @qcode{'fillwithmissing'} leaves it missing, a character ## variable blank, where MATLAB puts @code{char (0)}, and a logical or ## cell variable, which has no missing value, is refused; ## @qcode{'previous'}, @qcode{'next'} and @qcode{'nearest'} copy a ## neighbouring value; and @qcode{'linear'}, @qcode{'spline'}, ## @qcode{'pchip'} and @qcode{'makima'} interpolate, which restricts the ## timetable to numeric, @code{datetime} and @code{duration} variables. ## @qcode{'default'}, which is also what no method at all means, reads ## @qcode{VariableContinuity} and treats a @qcode{'continuous'} variable ## as @qcode{'linear'}, a @qcode{'step'} one as @qcode{'previous'}, and ## an @qcode{'unset'} or @qcode{'event'} one as ## @qcode{'fillwithmissing'}. ## ## @var{method} may instead aggregate the rows the target times gather ## into each bin, as @qcode{'sum'}, @qcode{'prod'}, @qcode{'mean'}, ## @qcode{'median'}, @qcode{'mode'}, @qcode{'min'}, @qcode{'max'}, ## @qcode{'count'}, @qcode{'firstvalue'} or @qcode{'lastvalue'}, or as a ## function handle taking the rows of one bin and returning a single row. ## A bin holding no row answers 0 for @qcode{'sum'}, 1 for ## @qcode{'prod'}, 0 for @qcode{'count'} and missing for the rest, and a ## function handle is called on it too, with an empty slice of the ## variable's own type. Every method but @qcode{'count'}, ## @qcode{'firstvalue'} and @qcode{'lastvalue'} needs a numeric, ## @code{logical}, @code{datetime} or @code{duration} variable. ## ## @qcode{'IncludedEdge'} says which edge of a bin belongs to it, ## @qcode{'left'} by default, under which the last bin runs on past the ## last target time; @qcode{'right'} instead lets the first bin run back ## before the first. A grid built from a unit or a step moves with it: ## closed on the right it starts at the first bin that can hold a row and ## ends one unit past the last row time. It is read only by an ## aggregating method. ## ## An option the chosen method cannot use is @strong{warned about and ## ignored} rather than dropped in silence, once per call. MATLAB says ## nothing in that case. ## ## An aggregation reads the row times as bins rather than as sample ## points, so it accepts row times that repeat and row times out of ## order, both of which the other methods refuse. ## ## @qcode{'fillwithconstant'} gives every added row the value of ## @qcode{'Constant'}, which is required with it and must be a value of ## the variable's own kind: a number for a numeric or @code{logical} ## variable, a @code{duration} for a @code{duration} one, and text for a ## @code{string} or a @code{categorical} one, where a name the variable ## does not yet carry is added to its categories. A number standing for ## a @code{duration} or for text is refused rather than read as days or ## as digits. Needing no missing value of its own, it serves a variable ## that has none. ## ## @qcode{'EndValues'} says what a target time outside the span of the ## old ones takes, either @qcode{'extrap'} or a constant. It is ## @qcode{'extrap'} by default, under which @qcode{'linear'} and the ## interpolating methods extrapolate and the neighbouring methods carry ## the nearest known value outward. ## ## @strong{A value that was missing before the call is a gap like any ## other} and is filled with them: a row the new times keep is resampled ## along with the rest, so @qcode{'previous'} gives it the last value ## that was not missing rather than the missing value it held. Only ## @qcode{'fillwithmissing'} leaves such a value alone. ## ## Every property of the timetable survives, @qcode{VariableUnits}, ## @qcode{VariableDescriptions} and @qcode{VariableContinuity} included, ## and the result carries the time step its new row times imply. ## ## @seealso{timetable, timetable.isregular, timetable.fillmissing, ## timetable.synchronize} ## @end deftypefn function tt = retime (this, varargin) [tt, errmsg, warns] = retimeResult (this, varargin); for k = 1:numel (warns) warning ("timetable.retime: %s", warns{k}); endfor if (! isempty (errmsg)) error ("timetable.retime: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{ttB} =} lag (@var{ttA}) ## @deftypefnx {timetable} {@var{ttB} =} lag (@var{ttA}, @var{n}) ## ## Shift the data of a timetable along its own row times. ## ## @code{@var{ttB} = lag (@var{ttA})} moves every value one row later, ## leaving the first row missing and dropping what fell off the end. ## The row times themselves do not move, so the values arrive against ## later times than the ones they were recorded at. ## ## @var{n} says how far, as a whole number of rows, as a ## @code{duration} that must be a multiple of the time step, or as a ## @code{calendarDuration} whose unit the timetable must be regular in. ## A negative @var{n} moves the values earlier instead, and a lag past ## the height of the timetable leaves every row missing. ## ## The timetable must be @strong{regular}: a shift by rows only means ## anything when the rows are evenly spaced, and a timetable of one row ## or of none is not regular. ## ## A row left with no value takes the missing value of its variable's ## own type, which for a @code{logical} variable is @code{false} and for ## a character one a space, neither type having a missing value of its ## own. Every property of the timetable survives. ## ## @seealso{timetable.retime, timetable.synchronize, timetable.isregular, ## timetable} ## @end deftypefn function tt = lag (this, n) if (nargin < 2) n = 1; endif [tt, errmsg] = lagResult (this, n); if (! isempty (errmsg)) error ("timetable.lag: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tt} =} synchronize (@var{tt1}, @var{tt2}) ## @deftypefnx {timetable} {@var{tt} =} synchronize (@var{tt1}, @dots{}, @var{ttN}) ## @deftypefnx {timetable} {@var{tt} =} synchronize (@dots{}, @var{newTimes}) ## @deftypefnx {timetable} {@var{tt} =} synchronize (@dots{}, @var{method}) ## @deftypefnx {timetable} {@var{tt} =} synchronize (@dots{}, @var{Name}, @var{Value}) ## ## Put several timetables on one set of row times and set them side by ## side. ## ## @code{@var{tt} = synchronize (@var{tt1}, @var{tt2})} returns a ## timetable holding the variables of both, at every row time either of ## them carries. Any number of timetables may be given, and they must ## all have row times of the same type. ## ## @var{newTimes} says where the rows of the answer sit. It is a ## @code{datetime} or @code{duration} vector, one of the time units ## @code{retime} takes, @qcode{'regular'} with a @qcode{'TimeStep'} or a ## @qcode{'SampleRate'}, or one of: ## ## @table @asis ## @item @qcode{'union'} ## Every time any of them carries. This is the default. ## ## @item @qcode{'intersection'} ## Only the times every one of them carries. ## ## @item @qcode{'commonrange'} ## Every time any of them carries that lies within the span they all ## cover. ## ## @item @qcode{'first'}, @qcode{'last'} ## The row times of the first or of the last timetable given. ## @end table ## ## A timetable with no rows says nothing about where the answer should ## sit and takes no part in choosing. ## ## @var{method} and the options are @code{retime}'s, and one method ## serves every timetable: a method for each is not accepted. ## ## Two timetables carrying a variable of the same name have both of them ## renamed for the timetable they came from, so @var{a} from @var{tt1} ## and @var{tt2} become @code{a_tt1} and @code{a_tt2}. An operand that ## is an expression rather than a variable is known by its place in the ## call. Operands with the same name, as a timetable synchronized with ## itself, number the later names (@code{a_tt}, @code{a_tt_1}), as in ## MATLAB. ## ## Where an operand is an @code{eventtable}, or carries one on its ## @qcode{Events} property, the result is an event table too and the ## operands' event tables are merged: an outer join keyed on the event ## times and every variable the two share, ordered by those keys. The ## three properties saying which variables describe the events must ## agree, and the operation is refused where they do not. ## ## @seealso{timetable.retime, timetable, horzcat, timetable.isregular} ## @end deftypefn function tt = synchronize (varargin) names = cell (1, numel (varargin)); for k = 1:numel (varargin) names{k} = inputname (k); endfor [tt, errmsg, warns] = synchronizeResult (varargin{1}, varargin, names); for k = 1:numel (warns) warning ("timetable.synchronize: %s", warns{k}); endfor if (! isempty (errmsg)) error ("timetable.synchronize: %s", errmsg); endif [tt, errmsg] = carryEvents (tt, varargin); if (! isempty (errmsg)) error ("timetable.synchronize: %s", errmsg); endif endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timetable} {@var{et} =} extractevents (@var{tt}, @var{rows}) ## @deftypefnx {timetable} {@var{et} =} extractevents (@var{tt}, @var{labels}) ## @deftypefnx {timetable} {@var{et} =} extractevents (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {timetable} {[@var{et}, @var{tt2}] =} extractevents (@dots{}) ## ## Build an event table out of the rows of a timetable. ## ## @code{@var{et} = extractevents (@var{tt}, @var{rows})} returns an ## @code{eventtable} whose events happen at the times of the rows ## @var{rows} picks out. @var{rows} is any row subscript a timetable ## accepts: row numbers, a logical vector, row times, a @code{timerange}, ## a @code{withtol} or a colon. The events come out in the order the ## subscript names them and a row named twice gives two events; with no ## option saying otherwise the event table carries no variables at all ## and designates nothing. ## ## @code{@var{et} = extractevents (@var{tt}, @var{labels})} takes a ## @code{categorical} vector with one element per row of @var{tt} and ## makes an event of every row whose label is not missing, labelled by ## it. Consecutive rows sharing a label are @emph{not} gathered into one ## interval: each row is its own instantaneous event. ## ## @code{[@var{et}, @var{tt2}] = extractevents (@dots{})} also returns ## @var{tt2}, a copy of @var{tt} without the variables the event table ## took from it. Only variables named by @qcode{'EventDataVariables'} or ## by one of the three @qcode{'@dots{}Variable'} options are taken, so a ## call that names none leaves @var{tt2} exactly as @var{tt} was. ## ## The following @var{Name}-@var{Value} options are supported: ## ## @multitable @columnfractions 0.30 0.70 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'EventLabels'} @tab A scalar, labelling every event ## alike, or one value per event. Added as a variable named ## @qcode{EventLabels}. ## ## @item @qcode{'EventLabelsVariable'} @tab The name of a variable of ## @var{tt} holding the labels. It keeps its own name in the event ## table and leaves @var{tt2}. ## ## @item @qcode{'EventLengths'} @tab A @code{duration} or ## @code{calendarDuration}, scalar or one per event, added as a variable ## named @qcode{EventLengths}. ## ## @item @qcode{'EventLengthsVariable'} @tab The name of a variable of ## @var{tt} holding the lengths, which must be a @code{duration} or ## @code{calendarDuration} column. ## ## @item @qcode{'EventEnds'} @tab Scalar or one per event, of the same ## type as the row times, added as a variable named @qcode{EventEnds}. ## ## @item @qcode{'EventEndsVariable'} @tab The name of a variable of ## @var{tt} holding the end times, which must be a @code{datetime} or ## @code{duration} column. ## ## @item @qcode{'EventDataVariables'} @tab Variables of @var{tt} to carry ## into the event table as event data. Each keeps its own name. ## ## @item @qcode{'PreserveEventVariables'} @tab A logical scalar. When ## @qcode{true} the variables the event table took stay in @var{tt2} as ## well. It says what to do with variables that were named, so it ## requires at least one option that names some. ## ## @end multitable ## ## Lengths and ends are mutually exclusive, however they are given, and ## so are labels given as values and labels named as a variable. Neither ## labels option may be combined with the @var{labels} form, which ## carries its own. ## ## @seealso{eventtable, timetable.syncevents, timetable} ## @end deftypefn function [ET, TT2] = extractevents (this, arg, varargin) if (nargin < 2) error ("timetable.extractevents: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error (strcat ("timetable.extractevents: name-value arguments", ... " must be in pairs.")); endif ## Parse optional paired arguments optNames = {'EventLabels', 'EventLabelsVariable', 'EventLengths', ... 'EventLengthsVariable', 'EventEnds', 'EventEndsVariable', ... 'EventDataVariables', 'PreserveEventVariables'}; dfValues = repmat ({missing}, 1, numel (optNames)); [Labels, LabelsVar, Lengths, LengthsVar, Ends, EndsVar, DataVars, ... Preserve, args] = parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error (strcat ("timetable.extractevents: invalid optional paired", ... " argument.")); endif ## A length and an end say the same thing two ways, and so do labels ## given as values and labels named as a variable. if ((wasGiven (Lengths) || wasGiven (LengthsVar)) && (wasGiven (Ends) || wasGiven (EndsVar))) error (strcat ("timetable.extractevents: specify either the event", ... " lengths or the event ends, but not both.")); endif if (wasGiven (Labels) && wasGiven (LabelsVar)) error (strcat ("timetable.extractevents: specify either the event", ... " labels or an event labels variable, but not both.")); endif ## The two forms of the second argument. A categorical says which ## rows are events and what each is called at once, so it settles the ## labels and nothing else may. byLabels = iscategorical (arg); if (byLabels) if (wasGiven (Labels) || wasGiven (LabelsVar)) error (strcat ("timetable.extractevents: event labels cannot be", ... " given as well when the second input is a", ... " categorical, which carries its own.")); endif if (numel (arg) != height (this)) error (strcat ("timetable.extractevents: the labels must have", ... " one element per row of the timetable; got %d", ... " for %d rows."), numel (arg), height (this)); endif keep = ! ismissing (arg(:)); ixRows = find (keep); else ixRows = resolveRowVarRefs (this, arg, ':'); ixRows = validateRowIndex (this, ixRows); ixRows = ixRows(:); endif nev = numel (ixRows); ## The event table's variables, and which of the timetable's went into ## them. Order follows the kinds: what the events are called, how far ## they run, then whatever data rides along. varNames = {}; varValues = {}; taken = []; lblVar = []; lenVar = []; endVar = []; if (byLabels) lab = arg(:); varNames{end+1} = 'EventLabels'; varValues{end+1} = lab(ixRows); lblVar = 'EventLabels'; elseif (wasGiven (Labels)) varNames{end+1} = 'EventLabels'; varValues{end+1} = eventOptionValue (Labels, nev, 'EventLabels'); lblVar = 'EventLabels'; elseif (wasGiven (LabelsVar)) [lblVar, col, ix] = takenVariable (this, LabelsVar, ixRows, ... 'EventLabelsVariable'); varNames{end+1} = lblVar; varValues{end+1} = col; taken = [taken, ix]; endif if (wasGiven (Lengths)) varNames{end+1} = 'EventLengths'; varValues{end+1} = eventOptionValue (Lengths, nev, 'EventLengths'); lenVar = 'EventLengths'; elseif (wasGiven (LengthsVar)) [lenVar, col, ix] = takenVariable (this, LengthsVar, ixRows, ... 'EventLengthsVariable'); if (! (isduration (col) || iscalendarduration (col))) error (strcat ("timetable.extractevents: the variable named by", ... " 'EventLengthsVariable' must be a duration or a", ... " calendarDuration; '%s' is a %s."), lenVar, ... class (col)); endif varNames{end+1} = lenVar; varValues{end+1} = col; taken = [taken, ix]; endif if (wasGiven (Ends)) varNames{end+1} = 'EventEnds'; varValues{end+1} = eventOptionValue (Ends, nev, 'EventEnds'); endVar = 'EventEnds'; elseif (wasGiven (EndsVar)) [endVar, col, ix] = takenVariable (this, EndsVar, ixRows, ... 'EventEndsVariable'); if (! (isdatetime (col) || isduration (col))) error (strcat ("timetable.extractevents: the variable named by", ... " 'EventEndsVariable' must be a datetime or a", ... " duration; '%s' is a %s."), endVar, class (col)); endif varNames{end+1} = endVar; varValues{end+1} = col; taken = [taken, ix]; endif if (wasGiven (DataVars)) ixData = resolveVarRef (this, DataVars); for k = 1:numel (ixData) col = getvar (this, ixData(k)); varNames{end+1} = this.VariableNames{ixData(k)}; varValues{end+1} = col(ixRows,:); endfor taken = [taken, ixData(:)']; endif ## 'PreserveEventVariables' says what becomes of the variables the ## event table took, so it needs a call that takes some. if (wasGiven (Preserve)) if (! (islogical (Preserve) && isscalar (Preserve))) error (strcat ("timetable.extractevents:", ... " 'PreserveEventVariables' must be a logical", ... " scalar.")); endif if (isempty (taken)) error (strcat ("timetable.extractevents:", ... " 'PreserveEventVariables' says what becomes of", ... " the variables taken from the timetable, so at", ... " least one of 'EventDataVariables',", ... " 'EventLabelsVariable', 'EventLengthsVariable'", ... " and 'EventEndsVariable' must be given.")); endif else Preserve = false; endif rt = getRowLabels (this); ET = eventtable (timetable (rt(ixRows), varValues{:}, ... 'VariableNames', varNames)); ET.EventLabelsVariable = lblVar; ET.EventLengthsVariable = lenVar; ET.EventEndsVariable = endVar; if (nargout > 1) TT2 = this; if (! Preserve && ! isempty (taken)) TT2 = subsetvars (this, setdiff (1:width (this), unique (taken))); endif endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tt2} =} syncevents (@var{tt}) ## @deftypefnx {timetable} {@var{tt2} =} syncevents (@var{tt}, @var{defaultLabel}) ## @deftypefnx {timetable} {@var{tt2} =} syncevents (@dots{}, @qcode{'EventDataVariables'}, @var{vars}) ## ## Copy the attached event table's data onto the rows it covers. ## ## @code{@var{tt2} = syncevents (@var{tt})} adds the variables of the ## event table attached to @var{tt} as variables of @var{tt}, each row ## taking the values of the event covering its time. A row no event ## covers takes missing values. @var{tt} must have an event table ## attached. ## ## An event covers the times from its own onwards and stops short of its ## end, so an event beginning at 02:00 and lasting two hours covers 02:00 ## and 03:00 but not 04:00, and an event of zero length covers nothing. ## An event with neither a length nor an end is an instant and covers ## only a row at exactly its own time. ## ## A row covered by more than one event is @emph{repeated}, once per ## event, which generally makes the result irregular. ## ## By default every variable of the event table is copied except the one ## holding the event lengths or ends, which describes the events rather ## than what happened. @qcode{'EventDataVariables'} names the variables ## to copy instead, and replaces that default rather than adding to it, ## so it can ask for the lengths and can leave the labels out. ## ## @code{@var{tt2} = syncevents (@var{tt}, @var{defaultLabel})} gives ## @var{defaultLabel} to the rows no event covers, in place of a missing ## label. It reaches the labels variable alone; every other copied ## variable stays missing there. ## ## A copied variable whose name @var{tt} already uses has both of them ## renamed, the timetable's with a @qcode{_tt} suffix and the event ## table's with @qcode{_et}, so neither is lost to the other. ## ## The result keeps the event table it was synchronised from. ## ## @seealso{timetable.extractevents, eventtable, timetable} ## @end deftypefn function TT2 = syncevents (this, varargin) [DataVars, args] = parsePairedArguments ({'EventDataVariables'}, ... {missing}, varargin(:)); if (numel (args) > 1) error (strcat ("timetable.syncevents: too many input arguments;", ... " only a default label may be given positionally.")); endif hasDefault = (numel (args) == 1); ev = eventsOf (this); ## An attached event table with no events is still attached; it covers ## no row, so every row is answered as one no event reached. if (! isa (ev, 'eventtable')) error (strcat ("timetable.syncevents: the timetable has no event", ... " table attached; assign one to", ... " 'Properties.Events' first.")); endif Pev = getProperties (ev); ## Which of the event table's variables are copied. The default is ## everything that says what happened, which is everything but the ## variable saying how far the event ran. if (wasGiven (DataVars)) ixCopy = resolveVarRef (ev, DataVars); ixCopy = ixCopy(:)'; else ## At most one of the two is set, an event stating its extent once, ## so the name is taken from whichever it is rather than by joining ## a name to an empty matrix. extent = Pev.EventLengthsVariable; if (isempty (extent)) extent = Pev.EventEndsVariable; endif ixCopy = 1:width (ev); if (! isempty (extent)) ixCopy = ixCopy(! strcmp (Pev.VariableNames, extent)); endif endif ## The span of each event. A length or an end makes it an interval, ## closed on the left and open on the right; with neither it is an ## instant. evTimes = getRowLabels (ev); if (! isempty (Pev.EventLengthsVariable)) evEnds = evTimes + getvar (ev, Pev.EventLengthsVariable); elseif (! isempty (Pev.EventEndsVariable)) evEnds = getvar (ev, Pev.EventEndsVariable); else evEnds = []; endif ## Every row paired with each event covering it, and once with no ## event where none does. The pairs are built row by row so that the ## result is in row order, and within a row in event order. rt = getRowLabels (this); ixRows = []; ixEv = []; for r = 1:numel (rt) if (isempty (evEnds)) hit = find (evTimes == rt(r)); else hit = find (evTimes <= rt(r) & rt(r) < evEnds); endif if (isempty (hit)) ixRows = [ixRows; r]; ixEv = [ixEv; 0]; else ixRows = [ixRows; repmat(r, numel (hit), 1)]; ixEv = [ixEv; hit(:)]; endif endfor TT2 = subsetrows (this, ixRows); ## The copied columns, missing wherever the row met no event. [evOut, errmsg] = joinBuildSide (subsetvars (ev, ixCopy), ixEv); if (! isempty (errmsg)) error ("timetable.syncevents: %s", errmsg); endif copyNames = Pev.VariableNames(ixCopy); ## A default label answers for the rows no event covered, and reaches ## the labels variable alone. if (hasDefault) lblVar = Pev.EventLabelsVariable; if (isempty (lblVar)) error (strcat ("timetable.syncevents: a default label needs the", ... " event table to say which of its variables holds", ... " the labels; 'EventLabelsVariable' is unset.")); endif k = find (strcmp (copyNames, lblVar), 1); if (! isempty (k)) col = evOut.VariableValues{k}; val = args{1}; ## A label of another type is brought to the labels variable's own ## rather than assigned across it, which would leave the conversion ## to the core and warn about it on the way. if (isa (col, 'string') && ! isa (col, class (val))) val = string (val); elseif (iscell (col) && ! iscell (val)) if (! ischar (val)) val = char (string (val)); endif val = {val}; endif col(ixEv == 0,:) = val; evOut.VariableValues{k} = col; endif endif ## A name used on both sides names neither cleanly, so both move. clash = ismember (copyNames, TT2.VariableNames); if (any (clash)) newSelf = TT2.VariableNames; for k = find (clash) j = find (strcmp (newSelf, copyNames{k}), 1); newSelf{j} = [newSelf{j}, '_tt']; copyNames{k} = [copyNames{k}, '_et']; endfor TT2.VariableNames = newSelf; endif for k = 1:numel (copyNames) TT2 = setvar (TT2, copyNames{k}, evOut.VariableValues{k}); endfor endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tt} =} addprop (@var{tt}, @var{propertyNames}, @var{propertyTypes}) ## ## Add custom properties to a timetable. ## ## @code{@var{tt} = addprop (@var{tt}, @var{propertyNames}, ## @var{propertyTypes})} adds properties that carry custom metadata to the ## timetable @var{tt}. @var{propertyNames} names them, as a character ## vector, a cell array of character vectors, or a string array, and ## @var{propertyTypes} says what each one describes, one per name: ## @qcode{'table'} for a property describing the timetable as a whole and ## @qcode{'variable'} for one carrying an entry per variable, which is ## kept in step as variables are added, removed, moved and renamed. ## ## A property is added empty; assign it through ## @qcode{@var{tt}.Properties.CustomProperties.@var{PropertyName}}. The ## row times are not a variable and carry no entry of their own. ## ## @seealso{timetable.rmprop, timetable} ## @end deftypefn function tt = addprop (this, Names, Types) if (nargin < 3) error ("timetable.addprop: too few input arguments."); endif [tt, errmsg] = addpropResult (this, Names, Types); if (! isempty (errmsg)) error ("timetable.addprop: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tt} =} rmprop (@var{tt}, @var{propertyNames}) ## ## Remove custom properties from a timetable. ## ## @code{@var{tt} = rmprop (@var{tt}, @var{propertyNames})} removes the ## named properties carrying custom metadata from the timetable ## @var{tt}. @var{propertyNames} is a character vector, a cell array of ## character vectors, or a string array. A name matching no custom ## property is ignored rather than refused. ## ## @seealso{timetable.addprop, timetable} ## @end deftypefn function tt = rmprop (this, Names) if (nargin < 2) error ("timetable.rmprop: too few input arguments."); endif [tt, errmsg] = rmpropResult (this, Names); if (! isempty (errmsg)) error ("timetable.rmprop: %s", errmsg); endif endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timetable} {@var{A} =} table2array (@var{tt}) ## ## Converts a timetable to an array. ## ## @code{@var{A} = table2array (@var{tt})} places the variables of ## @var{tt} side by side and joins them into one array @var{A}, as ## @code{table.table2array} joins the variables of a table. The row ## times and every other property of @var{tt} are left out. ## @code{@var{tt}@{:,:@}} and @code{@var{tt}.Variables} return the same ## array. ## ## @seealso{timetable.table2cell, timetable.table2struct, ## timetable.timetable2table, table.table2array} ## @end deftypefn function A = table2array (this) A = varsAsArray (this, 'table2array'); endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{C} =} table2cell (@var{tt}) ## ## Converts a timetable to a cell array. ## ## @code{@var{C} = table2cell (@var{tt})} returns a cell array @var{C} of ## the same size as @var{tt}, in which each cell holds one row of one ## variable, in the variable's own type, as @code{table.table2cell} does ## for a table. The row times and every other property of @var{tt} are ## left out. ## ## @seealso{timetable.table2array, timetable.table2struct, ## timetable.timetable2table, table.table2cell} ## @end deftypefn function C = table2cell (this) C = varsAsCell (this, 'table2cell'); endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{S} =} table2struct (@var{tt}) ## @deftypefnx {timetable} {@var{S} =} table2struct (@var{tt}, @qcode{'ToScalar'}, @qcode{true}) ## ## Converts a timetable to a scalar structure or structure array. ## ## @code{@var{S} = table2struct (@var{tt})} returns a structure array with ## the same fields as the variables in @var{tt}. The length of @var{S} is ## the same as the height of @var{tt}. ## ## @code{@var{S} = table2struct (@var{tt}, @qcode{'ToScalar'}, ## @qcode{true})} returns a scalar structure with the same fields as the ## variables in @var{tt}. Each field has the same rows as @var{tt}. ## ## The row times and every other property of @var{tt} are left out. ## ## @seealso{timetable.table2array, timetable.table2cell, ## timetable.timetable2table, table.table2struct} ## @end deftypefn function S = table2struct (this, varargin) [S, errmsg] = table2structResult (this, varargin); if (! isempty (errmsg)) error ("timetable.table2struct: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tbl} =} timetable2table (@var{tt}) ## @deftypefnx {timetable} {@var{tbl} =} timetable2table (@var{tt}, @qcode{'ConvertRowTimes'}, @var{tf}) ## ## Convert a timetable to a table. ## ## @code{@var{tbl} = timetable2table (@var{tt})} converts the timetable ## @var{tt} to a table whose @strong{first} variable is the row times, ## named after the row dimension they came from. The dimension names of ## the result are the defaults, @qcode{'Row'} and @qcode{'Variables'}, a ## table labelling its rows by name rather than by time. ## ## @code{@var{tbl} = timetable2table (@var{tt}, @qcode{'ConvertRowTimes'}, ## @var{tf})} keeps the row times as that first variable when @var{tf} is ## true, which is the default, and @strong{discards them} when it is false: ## the result has only the variables, and the times are gone rather than ## kept elsewhere. ## ## An event table attached to @var{tt} is not carried across, a table having ## nowhere to keep one, and converting the result back with ## @code{table2timetable} does not bring it back. An @code{eventtable} ## given here becomes a plain table like any other timetable. ## ## @seealso{table.table2timetable, array2timetable, timetable, table, ## eventtable} ## @end deftypefn function tbl = timetable2table (this, varargin) if (mod (numel (varargin), 2) != 0) error (strcat ("timetable.timetable2table: name-value arguments", ... " must be in pairs.")); endif ## Parse optional paired arguments [ConvertRowTimes, args] = parsePairedArguments ({'ConvertRowTimes'}, ... {true}, varargin(:)); if (! isempty (args)) error ("timetable.timetable2table: invalid optional paired argument."); endif if (! (isscalar (ConvertRowTimes) && (islogical (ConvertRowTimes) || isnumeric (ConvertRowTimes)))) error (strcat ("timetable.timetable2table: 'ConvertRowTimes' must", ... " be a logical scalar.")); endif ## 'Properties' is synthesised by 'subsref' and a method's dot access to ## its own class does not go through it, so the metadata is read off the ## properties themselves and the values through 'getvar'. varNames = this.VariableNames; vals = cell (1, numel (varNames)); for i = 1:numel (varNames) vals{i} = getvar (this, varNames{i}); endfor if (ConvertRowTimes) vals = [{getRowLabels(this)}, vals]; varNames = [{this.DimensionNames{1}}, varNames]; endif tbl = table (vals{:}, 'VariableNames', varNames); ## The rows stop being labelled by time and are labelled by nothing, so ## the first dimension takes a table's own default name; the second ## goes on naming what it named, being a name for the variables and not ## for the rows. tbl.Properties.DimensionNames = {'Row', this.DimensionNames{2}}; if (! isempty (this.VariableDescriptions)) tbl.Properties.VariableDescriptions = shiftMeta ( ... this.VariableDescriptions, ConvertRowTimes, ''); endif if (! isempty (this.VariableUnits)) tbl.Properties.VariableUnits = shiftMeta (this.VariableUnits, ... ConvertRowTimes, ''); endif if (! isempty (this.VariableContinuity)) tbl.Properties.VariableContinuity = shiftMeta ( ... this.VariableContinuity, ConvertRowTimes, 'unset'); endif if (! isempty (this.Description)) tbl.Properties.Description = this.Description; endif if (! isempty (this.UserData)) tbl.Properties.UserData = this.UserData; endif ## The row times become the first variable when they are converted, and a ## custom property describing the variables has no entry for them. ixVars = 1:numel (varNames); if (ConvertRowTimes) ixVars = [0, 1:(numel (varNames) - 1)]; endif tbl = tabular.carryCustomProps (tbl, this, ixVars); endfunction endmethods ################################################################################ ## ** File I/O ** ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {timetable} {} timetable2csv (@var{tt}, @var{file}) ## @deftypefnx {timetable} {} timetable2csv (@var{tt}, @var{file}, @var{Name}, @var{Value}) ## ## Write a timetable to a comma-separated-value (CSV) file. ## ## @code{timetable2csv (@var{tt}, @var{file})} writes the timetable ## @var{tt} to @var{file}, which may be a character vector, a cellstr, or a ## string scalar. The resulting file can be read back with ## @code{csv2timetable}. ## ## The file begins with a comment line reporting how many consecutive rows ## hold the variable types, names, descriptions, and units, in that order. ## Those header rows are followed by one row of data per timetable row. ## The variables are serialized exactly as @code{table2csv} serializes ## them. ## ## The row times lead the file as a column of their own, written in ISO ## 8601 form so that they are exact whatever their display format. The ## column is tagged @qcode{RowTimes} in the variable-type row, followed by ## the row times' own type, their @code{TimeZone} where they have one, and ## their @code{Format}; the row dimension name travels in the ## variable-name row beside it. A zone-aware @code{datetime} and a ## @code{duration} of any resolution therefore both come back exactly as ## they went out, which @code{writetimetable} does not manage for either. ## ## The row times are not optional: a timetable without them is not one, so ## there is no switch to leave them out. ## ## The following @var{Name}-@var{Value} options are supported: ## ## @multitable @columnfractions 0.30 0.70 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'WriteVariableNames'} @tab A logical scalar specifying ## whether the variable names are written (default @qcode{true}). When ## @qcode{false} the file carries none, so @code{csv2timetable} numbers the ## variables on read and can no longer group the columns: a multicolumn ## variable comes back as separate variables and a nested table as flat ## columns. The row dimension name is a name too and goes with them, so ## the row times come back under the default @qcode{Time}. ## @end multitable ## ## A CSV file holds one table and has nowhere to put a second, so an ## attached event table is @strong{not} written and nothing warns. Write ## the timetable to an OpenDocument spreadsheet with @code{timetable2ods} ## to keep its events. @code{TimeStep}, @code{SampleRate} and ## @code{VariableContinuity} are not written either; the first two are ## worked out again from the row times on read. ## ## Note the following round-trip limitation when reading the file back ## with @code{csv2timetable}: @code{calendarDuration} and ## @code{categorical} variables are returned as cell arrays of character ## vectors and their values are not reconstructed. ## ## @seealso{csv2timetable, timetable.timetable2ods, ## timetable.writetimetable, table.table2csv} ## @end deftypefn function timetable2csv (this, file, varargin) if (nargin < 2) error ("timetable.timetable2csv: too few input arguments."); endif if (! ((ischar (file) && isvector (file)) || iscellstr (file) ... || isa (file, 'string'))) error (strcat ("timetable.timetable2csv: FILE must be a character", ... " vector, cellstr, or string.")); endif file = char (cellstr (file)); if (mod (numel (varargin), 2) != 0) error (strcat ("timetable.timetable2csv: name-value arguments must", ... " be in pairs.")); endif ## Parse optional paired arguments optNames = {'WriteVariableNames'}; dfValues = {true}; [writeVarNames, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error ("timetable.timetable2csv: invalid optional paired argument."); endif if (! (islogical (writeVarNames) && isscalar (writeVarNames))) error (strcat ("timetable.timetable2csv: 'WriteVariableNames' must", ... " be a logical scalar.")); endif [csv, errmsg] = __csv_parts__ (this, writeVarNames, true); if (! isempty (errmsg)) error ("timetable.timetable2csv: %s", errmsg); endif msg = __table2csv__ (file, csv); if (msg) error ("timetable.timetable2csv: %s", msg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {} timetable2ods (@var{tt}, @var{file}) ## @deftypefnx {timetable} {} timetable2ods (@var{tt}, @var{file}, @var{Name}, @var{Value}) ## ## Write a timetable to an OpenDocument spreadsheet file. ## ## @code{timetable2ods (@var{tt}, @var{file})} writes the timetable ## @var{tt} to @var{file}, which may be a character vector, a cellstr, or a ## string scalar. When @var{file} ends in @qcode{.ods} a compressed ## (ZIP-packaged) OpenDocument spreadsheet is written; when it ends in ## @qcode{.fods} a flat (single-XML) one is written instead. The resulting ## file can be read back with @code{ods2timetable}. ## ## The data sheet is headed by the row dimension name and the variable ## names and then carries one natively typed cell per value, and a hidden ## @qcode{__datatypes_meta__} sheet carries the variable types, ## descriptions and units, exactly as @code{table2ods} writes them. ## ## The row times lead the sheet as a column of their own, written as ## native @code{date} or @code{time} cells as a @code{datetime} or ## @code{duration} variable is, so a spreadsheet application shows them as ## times rather than as text. The column is tagged @qcode{RowTimes} in the ## hidden sheet, followed by the row times' own type, their ## @code{TimeZone} where they have one, and their @code{Format}, so that ## all three come back exactly. ## ## The row times are not optional: a timetable without them is not one, so ## there is no switch to leave them out. ## ## The following @var{Name}-@var{Value} options are supported: ## ## @multitable @columnfractions 0.30 0.70 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'Sheet'} @tab The name of the sheet to write (default ## @qcode{'Sheet1'}). When @var{file} already exists the named sheet is ## added or replaced while every other sheet is preserved, so a workbook ## can be built up one object at a time. ## @item @qcode{'WriteVariableNames'} @tab A logical scalar specifying ## whether the variable names are written (default @qcode{true}). When ## @qcode{false} the file carries none at all, the hidden metadata sheet ## included, so @code{ods2timetable} numbers the variables on read and can ## no longer group the columns. The row dimension name is a name too and ## goes with them. ## @item @qcode{'WriteMode'} @tab @qcode{'overwritesheet'} or ## @qcode{'inplace'} replace the sheet (the default when the sheet ## exists), @qcode{'append'} appends the timetable's rows to it, and ## @qcode{'replacefile'} discards any existing file. ## @end multitable ## ## An attached event table is written too, on a sheet of its own named ## @qcode{_Events}, with a @qcode{## Events crossref:} line in the ## hidden metadata sheet tying the two together and carrying the event ## table's three variable designations. @code{ods2timetable} reads that ## sheet back and attaches it, and skips it when choosing which sheet to ## read. This is the only file format of the package that carries events: ## @code{timetable2csv} and @code{writetimetable} drop them. ## ## Note the following round-trip limitation when reading the file back ## with @code{ods2timetable}: @code{calendarDuration} and ## @code{categorical} variables are returned as cell arrays of character ## vectors and their values are not reconstructed. ## ## @seealso{ods2timetable, timetable.timetable2csv, struct2ods, ## table.table2ods} ## @end deftypefn function timetable2ods (this, file, varargin) if (nargin < 2) error ("timetable.timetable2ods: too few input arguments."); endif if (! ((ischar (file) && isvector (file)) || iscellstr (file) ... || isa (file, 'string'))) error (strcat ("timetable.timetable2ods: FILE must be a character", ... " vector, cellstr, or string.")); endif file = char (cellstr (file)); ## A '.fods' file is written as flat XML, a '.ods' file as a ZIP package. [~, ~, ext] = fileparts (file); if (strcmpi (ext, '.fods')) is_flat = true; elseif (strcmpi (ext, '.ods')) is_flat = false; else error (strcat ("timetable.timetable2ods: FILE must have a '.ods'", ... " or '.fods' extension.")); endif if (mod (numel (varargin), 2) != 0) error (strcat ("timetable.timetable2ods: name-value arguments must", ... " be in pairs.")); endif ## Parse optional paired arguments optNames = {'Sheet', 'WriteMode', 'WriteVariableNames'}; dfValues = {'Sheet1', '', true}; [sheet, writeMode, writeVarNames, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error ("timetable.timetable2ods: invalid optional paired argument."); endif if (! (islogical (writeVarNames) && isscalar (writeVarNames))) error (strcat ("timetable.timetable2ods: 'WriteVariableNames' must", ... " be a logical scalar.")); endif if (isa (sheet, 'string')) sheet = char (sheet); endif if (! (ischar (sheet) && isrow (sheet))) error ("timetable.timetable2ods: 'Sheet' must be a sheet name."); endif writeMode = lower (char (writeMode)); switch (writeMode) case {'', 'replacefile', 'overwritesheet', 'inplace', 'append'} ## supported write modes otherwise error ("timetable.timetable2ods: 'WriteMode' '%s' is not valid.", ... writeMode); endswitch ## Merge into an existing workbook (preserving other sheets) by reading ## it back, modifying the struct of objects, and rewriting the file. if (exist (file, 'file') && ! strcmp (writeMode, 'replacefile')) s = ods2struct (file); s = __mergesheet__ (s, this, sheet, writeMode); struct2ods (file, s); return; endif ## An attached event table needs a sheet of its own and a line tying ## the two together, which is the workbook writer's work, so the file ## is written as a workbook of one timetable rather than as one sheet. if (isa (eventsOf (this), 'eventtable')) struct2ods (file, __mergesheet__ (struct (), this, sheet, '')); return; endif ## Fresh single-sheet write. [V, vtype, meta, hdr] = __ods_parts__ (this, ... 'timetable.timetable2ods', ... writeVarNames, true); msg = __table2ods__ (file, V, vtype, meta, is_flat, ... struct ('sheetname', sheet, 'header', {hdr})); if (! isequal (msg, 0)) error ("timetable.timetable2ods: %s", msg); endif endfunction ## -*- texinfo -*- ## @deftypefn {timetable} {} writetimetable (@var{tt}, @var{filename}) ## @deftypefnx {timetable} {} writetimetable (@var{tt}, @var{filename}, @var{Name}, @var{Value}) ## ## Write a timetable to a file in the MATLAB-compatible form. ## ## @code{writetimetable (@var{tt}, @var{filename})} writes the timetable ## @var{tt} to @var{filename} with no hidden metadata of any kind: one ## header row of names and then one row per row of the timetable, which is ## what @code{readtimetable} and MATLAB both expect. The file type follows ## the extension, @qcode{.txt}, @qcode{.csv} and @qcode{.dat} being text ## and @qcode{.ods}, @qcode{.fods}, @qcode{.xlsx} and @qcode{.xlsm} ## spreadsheets, and @qcode{'FileType'} overrides it. ## ## The row times lead the file under the row dimension name, as MATLAB ## writes them. They are not optional: a timetable without them is not ## one, so there is no switch to leave them out, and @qcode{'WriteRowNames'} ## is refused because a timetable has no row names. ## ## @qcode{.ods} is the OpenDocument spreadsheet format, an open standard ## that LibreOffice and other applications read and write on every ## platform. @strong{MATLAB is the exception and cannot be given one.} ## Its @code{writetimetable} refuses the extension, and its readers open an ## OpenDocument file only by handing it to Excel, so a MATLAB user without ## Excel cannot read one at all. A file written for a MATLAB user should ## therefore be @qcode{.csv} or @qcode{.xlsx}. Everywhere else the format ## is a good choice, and @code{timetable2ods} writes it losslessly and is ## the only format of the package that carries an event table. ## ## @strong{A zone-aware @code{datetime} is written in the RFC 9557 form}, ## @qcode{2024-03-09T22:00:00-05:00[America/New_York]}, rather than as the ## display string MATLAB writes. This is deviation @strong{D7}; see the ## note below for why it is better. An unzoned @code{datetime} and a ## @code{duration} are written as MATLAB writes them, both of those ## round-tripping through MATLAB exactly. ## ## The following @var{Name}-@var{Value} options are supported: ## ## @multitable @columnfractions 0.30 0.70 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'FileType'} @tab @qcode{'text'} or @qcode{'spreadsheet'}, ## overriding what the extension says. ## @item @qcode{'WriteVariableNames'} @tab A logical scalar specifying ## whether the header row of names is written (default @qcode{true}). ## @item @qcode{'Delimiter'} @tab The field delimiter of a text file, named ## (@qcode{'comma'}, @qcode{'space'}, @qcode{'tab'}, @qcode{'semi'}, ## @qcode{'bar'}) or given as the character itself (default @qcode{','}). ## @item @qcode{'QuoteStrings'} @tab @qcode{'minimal'}, @qcode{'all'} or ## @qcode{'none'}, saying which text fields are quoted (default ## @qcode{'minimal'}). ## @item @qcode{'Sheet'} @tab The sheet of a spreadsheet to write. ## @item @qcode{'Range'} @tab The top-left cell a fresh spreadsheet write ## is anchored at, in A1 notation. ## @item @qcode{'WriteMode'} @tab @qcode{'overwrite'} or @qcode{'append'} ## for a text file; @qcode{'overwritesheet'}, @qcode{'inplace'}, ## @qcode{'append'} or @qcode{'replacefile'} for a spreadsheet. ## @end multitable ## ## An attached event table is @strong{not} written and nothing warns, as ## nothing warns in MATLAB. Use @code{timetable2ods}, the only format of ## the package that carries events. ## ## @strong{Deviation D7, and why the RFC 9557 form is the better answer.} ## MATLAB writes a zoned @code{datetime} as its display string, so the zone ## is written only when the @qcode{Format} happens to ask for it, and ## @code{readtimetable} then reads the file back unzoned and at the wrong ## instant without complaining. Measured against R2026a, @strong{no text ## form of a zoned row time survives MATLAB's own reader}: given a format ## carrying the offset, MATLAB writes ## @qcode{2024-03-09T22:00:00-05:00} itself and then fails to read that ## very file, so there is no encoding that keeps MATLAB working. The ## choice is between a loud failure there and a silent one, and this ## package takes the loud one. RFC 9557 is a published standard, not an ## invention of ours, and it carries both the offset and the zone name: the ## offset alone cannot name a zone, and the name alone cannot say which ## side of a repeated hour an instant falls on, so a timestamp inside a ## daylight-saving fold round-trips here and would not otherwise. ## ## @seealso{readtimetable, timetable.timetable2ods, ## timetable.timetable2csv, table.writetable} ## @end deftypefn function writetimetable (this, filename, varargin) if (nargin < 2) error ("timetable.writetimetable: too few input arguments."); endif if (! ((ischar (filename) && isvector (filename)) ... || (isa (filename, 'string') && isscalar (filename)))) error (strcat ("timetable.writetimetable: FILENAME must be a", ... " character vector or string scalar.")); endif file = char (filename); if (mod (numel (varargin), 2) != 0) error (strcat ("timetable.writetimetable: name-value arguments", ... " must be in pairs.")); endif if (any (strcmpi (varargin(1:2:end), 'WriteRowNames'))) error (strcat ("timetable.writetimetable: 'WriteRowNames' is not", ... " supported; a timetable labels its rows by time.", ... " Write a table with row names using 'writetable'.")); endif ## Parse optional paired arguments optNames = {'FileType', 'WriteVariableNames', 'Delimiter', ... 'QuoteStrings', 'Sheet', 'Range', 'WriteMode'}; dfValues = {'', true, ',', 'minimal', '', '', ''}; [fileType, writeVarNames, delim, quoteStrings, sheet, range, ... writeMode, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error (strcat ("timetable.writetimetable: invalid optional paired", ... " argument.")); endif if (! (islogical (writeVarNames) && isscalar (writeVarNames))) error (strcat ("timetable.writetimetable: 'WriteVariableNames'", ... " must be a logical scalar.")); endif if (isa (sheet, 'string')) sheet = char (sheet); endif if (isa (range, 'string')) range = char (range); endif writeMode = lower (char (writeMode)); [~, ~, ext] = fileparts (file); if (isempty (fileType)) switch (lower (ext)) case {'.txt', '.csv', '.dat'} fileType = 'text'; case {'.ods', '.fods', '.xlsx', '.xlsm'} fileType = 'spreadsheet'; case {'.xls', '.xlsb'} error (strcat ("timetable.writetimetable: '%s' Excel files are", ... " not supported; use '.xlsx', '.ods', or a text", ... " format."), ext); otherwise error (strcat ("timetable.writetimetable: cannot infer the", ... " file type from '%s'; specify 'FileType'."), ext); endswitch endif switch (lower (fileType)) case 'text' fmt = 'display'; case 'spreadsheet' fmt = 'iso'; otherwise error (strcat ("timetable.writetimetable: 'FileType' must be", ... " 'text' or 'spreadsheet'.")); endswitch isXlsx = any (strcmpi (ext, {'.xlsx', '.xlsm'})); appendMode = false; if (strcmp (fmt, 'display')) if (! isempty (sheet) || ! isempty (range)) error (strcat ("timetable.writetimetable: 'Sheet' and 'Range'", ... " are not supported for text files.")); endif switch (writeMode) case {'', 'overwrite'} appendMode = false; case 'append' appendMode = true; otherwise error (strcat ("timetable.writetimetable: 'WriteMode' '%s' is", ... " not valid for text files; use 'overwrite' or", ... " 'append'."), writeMode); endswitch else if (! isempty (sheet) && ! (ischar (sheet) && isrow (sheet))) error ("timetable.writetimetable: 'Sheet' must be a sheet name."); endif switch (writeMode) case {'', 'replacefile', 'overwritesheet', 'inplace', 'append'} ## supported spreadsheet write modes otherwise error (strcat ("timetable.writetimetable: 'WriteMode' '%s' is", ... " not valid for spreadsheet files."), writeMode); endswitch if (! isempty (range)) if (strcmp (writeMode, 'append')) error (strcat ("timetable.writetimetable: 'Range' is not", ... " supported with 'WriteMode' 'append'.")); endif if (exist (file, 'file') && ! strcmp (writeMode, 'replacefile')) error (strcat ("timetable.writetimetable: 'Range' is not", ... " supported when writing into an existing file.")); endif endif endif __interop_write__ (this, 'timetable.writetimetable', file, ... struct ('ext', ext, 'isXlsx', isXlsx, 'fmt', fmt, ... 'appendMode', appendMode, ... 'writeVarNames', writeVarNames, ... 'writeRowLabels', true, ... 'rowLabelHeader', rowLabelName (this), ... 'delim', delim, ... 'quoteStrings', quoteStrings, ... 'sheet', sheet, 'range', range, ... 'writeMode', writeMode, ... 'fname', 'writetimetable')); endfunction endmethods methods (Static, Hidden) ## The named methods that aggregate the rows falling in one target bin. ## Kept in one place because both the method resolver and the family ## test read it. function out = retimeAggregations () out = {'sum', 'prod', 'mean', 'median', 'mode', 'min', 'max', ... 'count', 'firstvalue', 'lastvalue'}; endfunction endmethods methods (Static) ## -*- texinfo -*- ## @deftypefn {timetable} {@var{tt} =} timetable.empty () ## @deftypefnx {timetable} {@var{tt} =} timetable.empty (@var{n}) ## @deftypefnx {timetable} {@var{tt} =} timetable.empty (@var{r}, @var{v}) ## @deftypefnx {timetable} {@var{tt} =} timetable.empty (@var{sz}) ## ## Create an empty timetable. ## ## @code{@var{tt} = timetable.empty ()} returns a 0-by-0 timetable. ## ## @code{@var{tt} = timetable.empty (@var{r}, @var{v})} returns a ## timetable with @var{r} rows and @var{v} variables, at least one of ## which must be zero. The row times are @qcode{NaT}, one per row, since ## a row of a timetable is labelled whether or not it holds anything. A ## timetable with variables but no rows names them @qcode{Var1} to ## @qcode{VarN} and gives each of them a @qcode{double} value. ## ## @code{@var{tt} = timetable.empty (@var{sz})} takes the two dimensions ## from the two-element vector @var{sz}, and ## @code{timetable.empty (@var{n})} is the same as ## @code{timetable.empty (@var{n}, @var{n})}. ## ## @seealso{timetable, table, isempty, height, width} ## @end deftypefn function tt = empty (varargin) [sz, errmsg] = tabular.emptySize ('timetable', varargin); if (! isempty (errmsg)) error ('timetable.empty: %s', errmsg); endif tt = timetable ('Size', sz, 'VariableTypes', ... repmat ({'double'}, 1, sz(2)), ... 'RowTimes', NaT (sz(1), 1)); endfunction endmethods endclassdef ## Whether an optional argument was given at all. The parser reports an ## absent one as its default, and for the row times an empty value is a ## legitimate thing to give, a timetable with no rows being built from an ## empty vector. A 'missing' is the sentinel because none of the arguments ## it stands in for can ever be one. ## Resolve the second argument of a range predicate into the interval it ## describes, and into the rows of the timetable that fall in it. All three ## predicates need the same values and differ only in how they compare that ## interval with the span of the row times, so the reading is done once here. ## 'valid' is false where the reference names no instant at all, which makes ## every predicate false rather than vacuously true. function [R, errmsg] = rangeRef (ref, rowTimes) R = struct ('valid', false, 'lo', [], 'hi', [], 'hasLo', false, ... 'hasHi', false, 'closedLeft', true, 'closedRight', true, ... 'whichRows', false (numel (rowTimes), 1)); errmsg = ''; ## A range knows its own bounds and its own closure, and it is the range ## that refuses a bound of the wrong kind, exactly as it does when it is ## used as a subscript. if (isa (ref, 'timerange')) [lo, hi, cl, cr] = interval (ref, rowTimes); R.valid = true; R.lo = lo; R.hi = hi; R.hasLo = ! isnumeric (lo); R.hasHi = ! isnumeric (hi); R.closedLeft = cl; R.closedRight = cr; R.whichRows(rowIndices (ref, rowTimes)) = true; return endif if (isa (ref, 'timetable')) other = ref.Properties.RowTimes; if (! strcmp (class (other), class (rowTimes))) errmsg = sprintf (strcat ("a timetable with %s row times cannot be", ... " compared against one with %s row times."), ... class (rowTimes), class (other)); return endif [lo, hi, ok] = timeSpan (other); if (! ok) return endif elseif ((isdatetime (ref) || isduration (ref)) && isscalar (ref)) if (! strcmp (class (ref), class (rowTimes))) errmsg = sprintf (strcat ("a timetable with %s row times cannot be", ... " compared against a %s."), ... class (rowTimes), class (ref)); return endif if (ismissing (ref)) return endif lo = ref; hi = ref; else errmsg = strcat ("REF must be a timetable, a timerange, or a datetime", ... " or duration scalar."); return endif ## Both remaining forms name a closed interval, so the defaults stand. R.valid = true; R.lo = lo; R.hi = hi; R.hasLo = true; R.hasHi = true; R.whichRows = rowTimes >= lo & rowTimes <= hi; endfunction ## The closed interval the row times themselves span. Missing times are not ## part of it, and a timetable left with none has no span to compare. function [lo, hi, ok] = timeSpan (rowTimes) lo = []; hi = []; keep = ! ismissing (rowTimes); ok = any (keep); if (! ok) return endif rt = rowTimes(keep); lo = min (rt); hi = max (rt); endfunction ## Compare one end of a span against one bound, letting the bound say whether ## standing on it counts. 'above' asks whether T reaches the lower bound B, ## 'below' whether it stays under the upper one. function tf = boundMet (t, b, closed, side) if (strcmp (side, 'above')) if (closed) tf = t >= b; else tf = t > b; endif else if (closed) tf = t <= b; else tf = t < b; endif endif endfunction ## The value assigned to 'Events', validated and, where it is a plain ## timetable, converted. An empty matrix detaches whatever was there. The ## event times must be of the same type as the row times they annotate, ## since an event that cannot be placed against a row says nothing about it. function val = checkEvents (this, val) clstype = class (this); if (isnumeric (val) && isempty (val)) val = []; return endif if (! isa (val, 'timetable')) error (strcat ("%s.subsasgn: 'Events' must be an eventtable, a", ... " timetable to convert into one, or [] to detach the", ... " one attached."), clstype); endif if (! isa (val, 'eventtable')) val = eventtable (val); endif evClass = class (val.Properties.RowTimes); ttClass = class (this.Properties.RowTimes); if (! strcmp (evClass, ttClass)) error (strcat ("%s.subsasgn: the attached event table's row times must", ... " be of the same type as this %s's row times: got %s", ... " against %s."), clstype, clstype, evClass, ttClass); endif endfunction ## One value given as an option to 'extractevents', sized to the events: one ## value stands for every event alike, otherwise there is one apiece. function val = eventOptionValue (val, nev, opt) ## Text becomes a cellstr, as it does in the 'eventtable' constructor: a ## character matrix is one value per row, and a character row vector is a ## single value. if (ischar (val)) val = cellstr (val); endif if (isscalar (val)) val = repmat (val(:), nev, 1); elseif (numel (val) == nev) val = val(:); else error (strcat ("timetable.extractevents: '%s' must be a scalar or", ... " have one element per event; got %d for %d events."), ... opt, numel (val), nev); endif endfunction ## Whether two event tables may be merged at all. Their variables need not ## agree, an outer join filling what one side lacks, but the three properties ## saying which variables describe the events must: a merge that had to choose ## between two answers to that would be inventing one. The message names both ## values, since the usual way into a mismatch is renaming a variable in one ## of the two and it has to point back there. function errmsg = mergeableEvents (PA, PB) errmsg = ''; props = {'EventLabelsVariable', 'EventLengthsVariable', ... 'EventEndsVariable'}; for k = 1:numel (props) a = PA.(props{k}); b = PB.(props{k}); if (! isequal (a, b)) errmsg = sprintf (strcat ("the event tables attached to the inputs", ... " cannot be merged: their '%s' differ", ... " (%s and %s)."), ... props{k}, nameOrUnset (a), nameOrUnset (b)); return; endif endfor endfunction ## One event property's value, rendered for a message. function txt = nameOrUnset (val) if (isempty (val)) txt = 'unset'; else txt = sprintf ("'%s'", val); endif endfunction function tf = wasGiven (x) tf = ! isa (x, 'missing'); endfunction ## The default value of a column of NR rows of the named type, used by the ## preallocating constructor form. Kept beside the constructor rather than ## shared with 'table', whose own list differs: a timetable cannot hold a ## timetable variable and says so with its own scope. function col = defaultColumn (vartype, nr) switch (vartype) case {'double', 'single', 'int8', 'uint8', 'int16', 'uint16', ... 'int32', 'uint32', 'int64', 'uint64'} col = zeros (nr, 1, vartype); case {'doublenan', 'doubleNaN'} col = NaN (nr, 1, 'double'); case {'singlenan', 'singleNaN'} col = NaN (nr, 1, 'single'); case 'logical' col = logical (zeros (nr, 1)); case 'categorical' col = categorical (NaN (nr, 1)); case 'datetime' col = NaT (nr, 1); case 'duration' col = seconds (zeros (nr, 1)); case 'calendarDuration' col = calendarDuration (zeros (nr, 3)); case 'string' col = string (NaN (nr, 1)); case {'cellstr', 'char'} col = repmat (cellstr (""), nr, 1); case 'cell' col = cell (nr, 1); case 'struct' col = repmat (struct, nr, 1); case 'table' col = table ([]); case 'timetable' error ("timetable: 'timetable' variable type not supported yet."); otherwise error ("timetable: unsupported variable type: '%s'", vartype); endswitch endfunction ## Validate a row time vector of N rows and return it as a column. N may be ## empty, which asks only that the vector be of a usable type. function rt = checkRowTimes (rt, n) if (! (isdatetime (rt) || isduration (rt))) error (strcat ("timetable: 'RowTimes' must be a datetime or a", ... " duration vector.")); endif if (! isvector (rt) && ! isempty (rt)) error ("timetable: 'RowTimes' must be a vector."); endif if (! isempty (n) && numel (rt) != n) error (strcat ("timetable: the number of 'RowTimes' (%d) must equal", ... " the number of rows (%d)."), numel (rt), n); endif rt = rt(:); endfunction ## Validate a start time, a datetime or duration scalar. It need not match ## the type the row times already have: assigning one of the other kind ## re-types them, which is how an elapsed-time timetable is given a date to ## have begun on. function st = checkStartTime (st) if (! (isdatetime (st) || isduration (st))) error (strcat ("timetable: 'StartTime' must be a datetime or a", ... " duration scalar.")); endif if (! isscalar (st)) error ("timetable: 'StartTime' must be a scalar."); endif endfunction ## Validate a time step against the start time it will be counted from. A ## calendar step has no fixed length, so it can only be counted on a ## calendar and needs a datetime to count from. function ts = checkTimeStep (ts, st) if (! (isduration (ts) || iscalendarduration (ts))) error (strcat ("timetable: 'TimeStep' must be a duration or a", ... " calendarDuration scalar.")); endif if (! isscalar (ts)) error ("timetable: 'TimeStep' must be a scalar."); endif if (iscalendarduration (ts) && ! isdatetime (st)) error (strcat ("timetable: 'StartTime' must be a datetime when", ... " 'TimeStep' is a calendarDuration.")); endif if (iscalendarduration (ts)) dv = datevec (ts); if (any (dv(4:6)) || ((dv(1) != 0 || dv(2) != 0) && dv(3) != 0)) error (strcat ("timetable: a calendarDuration 'TimeStep' must name", ... " a single calendar unit; use 'caldays', 'calweeks',", ... " 'calmonths', 'calquarters' or 'calyears'.")); endif endif endfunction ## Validate a sample rate, a real numeric scalar in hertz. Neither the sign ## nor the magnitude is constrained: a negative rate steps backwards, which ## is as regular as stepping forwards, and the degenerate rates simply carry ## their arithmetic into the row times, a rate of zero giving an infinite ## step and an infinite rate giving no step at all. function fs = checkSampleRate (fs) if (! (isnumeric (fs) && isscalar (fs) && isreal (fs))) error ("timetable: 'SampleRate' must be a numeric scalar."); endif endfunction ## The start time a generated timetable takes when none was given: a zero of ## the step's own kind, so that the row times read as elapsed time in the ## units the step was written in. A calendar step has no such zero and is ## refused before this is reached. function st = defaultStart (ts) st = seconds (0); if (isduration (ts)) ## A zero of the step's own units, taken by copying its format rather ## than by subtracting it from itself, which would be NaN for a step ## that is not finite. st.Format = ts.Format; endif endfunction ## N row times counted from ST in steps of TS. function rt = steppedTimes (st, ts, n) if (n == 0) rt = st([]); else rt = st + ts * (0:n-1)'; ## The first row time is the start time itself. Reaching it as ## 'start + step * 0' costs nothing until the step is not finite, where ## 'Inf * 0' is NaN and would poison a row that has no step in it. rt(1) = st; endif endfunction ## N missing row times of the same type as RT, which is what the rows of a ## plain table are labelled with when one is stacked onto a timetable. function m = missingTimes (rt, n) m = repmat (startOf (rt([])), [n, 1]); endfunction ## The first of a vector of row times, or a missing one of the same type ## when the vector has none to take. function st = startOf (rt) if (isempty (rt)) if (isdatetime (rt)) st = NaT; st.TimeZone = rt.TimeZone; else st = seconds (NaN); endif else st = rt(1); endif endfunction ## The time step implied by a vector of row times, and the sample rate that ## goes with it. Fewer than two rows imply nothing, and neither does a ## vector carrying a missing time, a repeated time or an uneven gap: all of ## them give a NaN duration, which is what marks a timetable as irregular. ## ## A calendar step is preferred wherever there is one, and the two halves of ## the calendar are inferred on different terms. Months, quarters and years ## have no fixed length in seconds, so an even calendar spacing is all they ## can be asked for. Days and weeks are meant to be absolute, so they must ## be even in absolute time as well: a run of calendar days interrupted by a ## clock change contains a day of 23 hours and is not regular at all. ## ## Failing that, a vector evenly spaced in absolute time gives a duration ## step, keeping the format the difference came out in. function [ts, fs] = stepOf (rt) ts = seconds (NaN); if (numel (rt) >= 2 && ! any (ismissing (rt))) d = diff (rt); ds = seconds (d); absReg = (ds(1) != 0 && all (ds == ds(1))); cd = calendarStepOf (rt); if (! isempty (cd) && (calendarMonths (cd) != 0 || absReg)) ts = stepUnit (cd); elseif (absReg) ts = d(1); endif endif fs = stepRate (ts); endfunction ## The constant calendar difference of a vector of row times, or empty when ## it has none. Duration row times carry no calendar and never have one. ## ## A calendar step names one calendar unit and no time of day. An hourly ## run, a run of days-plus-two-hours and a run of months-plus-a-day are each ## evenly spaced on the calendar in a sense, and none of them is a calendar ## step; the last is refused for an explicit 'TimeStep' too. function cd = calendarStepOf (rt) cd = []; if (! isdatetime (rt) || numel (rt) < 2 || any (ismissing (rt))) return endif c = caldiff (rt); dv = datevec (c); if (all (all (dv == dv(1,:))) && any (dv(1,1:3)) && ! any (dv(1,4:6)) && ! ((dv(1,1) != 0 || dv(1,2) != 0) && dv(1,3) != 0)) cd = c(1); endif endfunction ## A calendar step written in the coarsest whole unit it fills. 'caldiff' ## reports in years, months and days, so a weekly run comes back as '7d' and ## a quarterly one as '3mo'; a step names its own unit instead. The value is ## the same either way and only the unit it is written in changes. function cd = stepUnit (cd) dv = datevec (cd); months = dv(1) * 12 + dv(2); if (months == 0 && dv(3) != 0 && mod (dv(3), 7) == 0) cd = calweeks (dv(3) / 7); elseif (dv(3) == 0 && months != 0 && mod (months, 3) == 0 && mod (months, 12) != 0) cd = calquarters (months / 3); endif endfunction ## A calendar step counted in whole months, years and quarters included. function m = calendarMonths (cd) dv = datevec (cd); m = dv(1) * 12 + dv(2); endfunction ## The sample rate of a time step: rows per second, and NaN for a calendar ## step, which has no fixed length in seconds to take a reciprocal of. A ## NaN step gives a NaN rate by the same arithmetic. ## The format of a declared step carried onto a step recomputed from the row ## times. Only a duration has one to carry, and a calendar step that gave way ## to a duration, or the reverse, keeps whatever the new value came with. function step = carryStepFormat (oldStep, step) if (isa (oldStep, 'duration') && isa (step, 'duration')) step.Format = oldStep.Format; endif endfunction function fs = stepRate (ts) if (iscalendarduration (ts)) fs = NaN; else fs = 1 / seconds (ts); endif endfunction ## Whether a calendar step is a whole, nonzero number of UNIT. The two ## families do not mix: a step in days is a whole number of days and of ## weeks but of no months, and a step in months is a whole number of months, ## quarters or years but of no days. function tf = unitDivides (unit, cd) tf = false; if (isempty (cd)) return endif dv = datevec (cd); months = dv(1) * 12 + dv(2); days = dv(3); switch (unit) case 'days' tf = months == 0 && days != 0; case 'weeks' tf = months == 0 && days != 0 && mod (days, 7) == 0; case 'months' tf = days == 0 && months != 0; case 'quarters' tf = days == 0 && months != 0 && mod (months, 3) == 0; case 'years' tf = days == 0 && months != 0 && mod (months, 12) == 0; endswitch endfunction ## A row time reference converted to the type of the row times it will be ## matched against, so that a date may be given as text. function ref = rowRefTimes (rowRef, rt) if (isdatetime (rowRef) || isduration (rowRef)) ref = rowRef(:); if (! strcmp (class (ref), class (rt))) error (strcat ("timetable: a row time subscript must be a %s, to", ... " match the row times."), class (rt)); endif return endif txt = cellstr (rowRef); try if (isdatetime (rt)) try ref = datetime (txt); ref.TimeZone = rt.TimeZone; catch ## Text carrying an offset names an instant, which can only be read ## into the zone of the row times. ref = datetime (txt, 'TimeZone', rt.TimeZone); end_try_catch else ref = duration (txt); endif catch error (strcat ("timetable: a row subscript could not be read as a", ... " %s: '%s'"), class (rt), strjoin (txt, ", ")); end_try_catch ref = ref(:); endfunction ## The target row times a 'retime' call asks for. SPEC is a time vector, one ## of the eight time units, or 'regular'; TSTEP and SRATE carry the ## 'TimeStep' and 'SampleRate' options, of which 'regular' takes exactly one. ## Returns an errmsg body for the caller to raise. function [nt, errmsg] = retimeTimes (rt, spec, tstep, srate, isAgg, incEdge) nt = []; errmsg = ''; if (nargin < 5) isAgg = false; endif if (nargin < 6) incEdge = 'left'; endif ## A bin closed on the right is labelled by its upper bound, so the grid ## runs from the boundary at or after the first row time to the one at or ## after the last, where a bin closed on the left runs from the boundary at ## or before the first to the one at or before the last. The edge says ## nothing about a grid of points to compute a value for. binRight = isAgg && strcmpi (incEdge, 'right'); binLeft = isAgg && ! binRight; ## A time vector is taken as it is, once it agrees with the row times about ## what kind of time they are. if (isdatetime (spec) || isduration (spec)) if (isdatetime (rt) != isdatetime (spec)) errmsg = strcat ("the target times must be of the same type as", ... " the row times."); return endif nt = spec(:); if (any (ismissing (nt))) errmsg = "the target times must not be missing."; return endif if (numel (nt) > 1 && ! (issorted (nt) || issorted (flipud (nt)))) errmsg = "the target times must be sorted."; return endif if (numel (unique (nt)) != numel (nt)) errmsg = "the target times must be unique."; return endif return endif if (isa (spec, 'string') && isscalar (spec)) spec = char (spec); endif if (! (ischar (spec) && isrow (spec))) errmsg = strcat ("the target times must be a time vector, a time", ... " unit, or 'regular'."); return endif spec = lower (spec); ## Nothing to place a grid against, and nowhere for it to start. if (isempty (rt)) nt = rt; return endif ## A missing row time places no row, but it does not stop the others being ## placed: the grid is built from the times that name an instant and the ## row falls in no bin. Only a timetable with nothing but missing times ## leaves the grid nothing to span. if (all (ismissing (rt))) errmsg = "the row times must not all be missing."; return endif if (strcmp (spec, 'regular')) if (wasGiven (tstep) && wasGiven (srate)) errmsg = strcat ("'regular' takes a 'TimeStep' or a", ... " 'SampleRate', not both."); return elseif (wasGiven (tstep) && iscalendarduration (tstep)) ## A calendar step follows the wall clock, so the grid is stepped one ## unit at a time from a boundary of that unit rather than computed ## from a fixed length. Row times that carry no calendar have no such ## boundary to start from. if (! isdatetime (rt)) errmsg = strcat ("a 'TimeStep' given as a calendarDuration needs", ... " datetime row times, which carry the calendar it", ... " counts in."); return endif [cunit, cper, errmsg] = retimeCalendarStep (tstep); if (! isempty (errmsg)) return endif [lo, hi] = gridSpan (rt); clo = floorToCalendar (lo, cunit); if (binRight && clo < lo) clo = advanceCalendar (clo, cunit, cper); endif nt = calendarGrid (clo, hi, cunit, cper, binLeft); return elseif (wasGiven (tstep)) if (! (isduration (tstep) && isscalar (tstep) && ! ismissing (tstep) && seconds (tstep) > 0)) errmsg = strcat ("'TimeStep' must be a positive duration or", ... " calendarDuration scalar."); return endif step = tstep; elseif (wasGiven (srate)) if (! (isnumeric (srate) && isscalar (srate) && isreal (srate) && isfinite (srate) && srate > 0)) errmsg = "'SampleRate' must be a positive real scalar."; return endif step = seconds (1 / srate); else errmsg = strcat ("'regular' requires a 'TimeStep' or a", ... " 'SampleRate'."); return endif [lo, hi] = gridSpan (rt); lo = floorToStep (lo, step); if (binRight && lo < min (rt)) lo = lo + step; endif nt = unitSteps (lo, hi, step, binLeft); return endif errmsg = timeBaseError (spec, false); if (! isempty (errmsg)) return endif [nt, errmsg] = unitGrid (rt, spec, binLeft, binRight); endfunction ## The span a target grid must cover. A descending timetable is taken by its ## ends rather than by its order, the grid itself always running forward. function [lo, hi] = gridSpan (rt) lo = min (rt); hi = max (rt); endfunction ## A grid from LO in steps of STEP whose last point lands at or past HI. The ## count is worked out rather than stepped towards, a duration dividing a ## span exactly. function nt = steppedGrid (lo, hi, step) n = 0; if (hi > lo) n = ceil (seconds (hi - lo) / seconds (step)); endif nt = lo + step * (0:n)'; endfunction ## The grid a time unit asks for, which ends at a different place depending ## on what is done with it: an aggregation gathers the rows falling in each ## bin, so a bin past the last row time would come back empty and the grid ## stops at the step at or before it; a fill computes a value for every ## target time whether a row reaches it or not, so its grid runs on to the ## step at or after, as 'regular' does. The count is checked against HI ## rather than trusted from the division, so a last row time landing exactly ## on a step is never lost to a rounding error in it. function nt = unitSteps (lo, hi, step, binLeft) if (! binLeft) nt = steppedGrid (lo, hi, step); return endif n = 0; if (hi > lo) n = floor (seconds (hi - lo) / seconds (step)); if (lo + step * (n + 1) <= hi) n += 1; endif endif nt = lo + step * (0:n)'; endfunction ## The same for a calendar step, which has no fixed length to divide a span ## by and so is stepped one unit at a time. function nt = calendarGrid (lo, hi, unit, per, binLeft) if (nargin < 4) per = 1; endif if (nargin < 5) binLeft = false; endif nt = lo; k = 0; t = lo; while (t < hi) k += 1; switch (unit) case {'day', 'days'} t = lo + caldays (per * k); case 'week' t = lo + calweeks (k); case {'month', 'months'} t = lo + calmonths (per * k); case 'quarter' t = lo + calquarters (k); case 'year' t = lo + calyears (k); endswitch if (binLeft && t > hi) break endif nt(k+1,1) = t; endwhile endfunction ## One unit on from a boundary, which is where a grid of bins closed on the ## right starts when the first row time is not on a boundary itself. function t = advanceUnit (t, unit) switch (unit) case 'second' t = t + seconds (1); case 'minute' t = t + minutes (1); case 'hour' t = t + hours (1); case 'day' t = t + caldays (1); case 'week' t = t + calweeks (1); case 'month' t = t + calmonths (1); case 'quarter' t = t + calquarters (1); case 'year' t = t + calyears (1); endswitch endfunction function t = advanceCalendar (t, unit, per) if (strcmp (unit, 'months')) t = t + calmonths (per); else t = t + caldays (per); endif endfunction ## The boundary a calendar grid starts from: the start of the month where the ## step counts months, and the start of the day where it counts days. A step ## in quarters or years counts months and a step in weeks counts days, so ## neither starts on a boundary of its own name -- a weekly grid begins on the ## day of the first row time, not on the Sunday before it. function lo = floorToCalendar (lo, unit) if (strcmp (unit, 'months')) lo = dateshift (lo, 'start', 'month'); else lo = dateshift (lo, 'start', 'day'); endif endfunction ## The single calendar unit a 'TimeStep' names, and how many of it. Quarters ## and years are counted in months and weeks in days, which is how a ## calendarDuration stores them; a step mixing the two, or carrying a time of ## day, names no one unit and is refused, as it is in MATLAB. function [unit, per, errmsg] = retimeCalendarStep (ts) unit = ''; per = 0; errmsg = ''; if (! (isscalar (ts) && ! ismissing (ts))) errmsg = "'TimeStep' must be a positive calendarDuration scalar."; return endif dv = datevec (ts); months = dv(1) * 12 + dv(2); dys = dv(3); if (any (dv(4:6)) || (months != 0 && dys != 0)) errmsg = strcat ("a 'TimeStep' given as a calendarDuration must name", ... " one calendar unit; use 'caldays', 'calweeks',", ... " 'calmonths', 'calquarters' or 'calyears'."); return endif if (months > 0) unit = 'months'; per = months; elseif (dys > 0) unit = 'days'; per = dys; else errmsg = "'TimeStep' must be a positive calendarDuration scalar."; endif endfunction ## The grid a time unit asks for: from the unit's own boundary at or before ## the first row time, to the last boundary the method has any use for. function [nt, errmsg] = unitGrid (rt, spec, binLeft, binRight) nt = []; errmsg = ''; if (nargin < 3) binLeft = false; endif if (nargin < 4) binRight = false; endif switch (spec) case 'secondly' unit = 'second'; case 'minutely' unit = 'minute'; case 'hourly' unit = 'hour'; case 'daily' unit = 'day'; case 'weekly' unit = 'week'; case 'monthly' unit = 'month'; case 'quarterly' unit = 'quarter'; case 'yearly' unit = 'year'; endswitch [lo, hi] = gridSpan (rt); lo0 = lo; if (isdatetime (rt)) lo = dateshift (lo, 'start', unit); if (binRight && lo < lo0) lo = advanceUnit (lo, unit); endif switch (unit) case 'second' nt = unitSteps (lo, hi, seconds (1), binLeft); case 'minute' nt = unitSteps (lo, hi, minutes (1), binLeft); case 'hour' nt = unitSteps (lo, hi, hours (1), binLeft); otherwise ## A calendar step follows the wall clock, so a day across a clock ## change is still one day and a month is still one month. nt = calendarGrid (lo, hi, unit, 1, binLeft); endswitch return endif ## A duration carries no calendar, so a unit measures it only where the ## unit is one a duration counts in -- which is to say where there is a ## duration constructor for it. A week, a month and a quarter have none; ## a year is the fixed 365.2425 days that 'years' builds. The same rule ## decides which units bin a duration grouping variable. switch (unit) case 'second' step = seconds (1); case 'minute' step = minutes (1); case 'hour' step = hours (1); case 'day' step = days (1); case 'year' step = years (1); otherwise errmsg = sprintf (strcat ("'%s' is not a unit a duration counts in;", ... " use 'secondly', 'minutely', 'hourly',", ... " 'daily' or 'yearly'."), spec); return endswitch lo = step * floor (seconds (lo) / seconds (step)); if (binRight && lo < lo0) lo = lo + step; endif nt = unitSteps (lo, hi, step, binLeft); ## The grid is generated from the unit's own duration and so comes out in ## the unit's display format. The row times keep theirs: asking for the ## rows by the minute says how to gather them, not how to print them. nt.Format = rt.Format; endfunction ## Where each target time takes a neighbouring value from: the index of the ## row of RT that METHOD picks for it, or 0 where there is none on that side. ## The times are compared as seconds from the first row time, which is what ## makes an irregular timetable pick by distance rather than by row count. A ## target falling exactly on a row time takes that row under every method, ## and a tie between two neighbours goes to the later, as it does in ## 'fillmissing'. function si = retimeNeighbor (rt, nt, method) ## A missing row time names no instant, so it is no neighbour of any ## target and takes no part in the search. It is dropped before the sort ## rather than left to sort last, where it would sit at the end of the ## table 'lookup' reads and answer for the target beyond the last real ## row. keep = find (! ismissing (rt(:))); [rs, o] = sort (rt(keep)); ord = keep(o); if (isdatetime (rs)) x = seconds (rs - rs(1)); q = seconds (nt(:) - rs(1)); else x = seconds (rs); q = seconds (nt(:)); endif ip = lookup (x, q); # last x at or before q, 0 when none hit = false (size (q)); nz = ip > 0; hit(nz) = x(ip(nz)) == q(nz); in = ip + 1; in(hit) = ip(hit); in(in > numel (x)) = 0; switch (method) case 'exact' ## Only a target that lands on a row time takes anything at all, which ## is what keeps a row the target keeps and leaves the rest missing. j = zeros (size (q)); j(hit) = ip(hit); case 'previous' j = ip; case 'next' j = in; case 'nearest' j = ip; j(ip == 0) = in(ip == 0); both = find (ip > 0 & in > 0); later = (x(in(both)) - q(both)) <= (q(both) - x(ip(both))); j(both(later)) = in(both(later)); endswitch si = zeros (size (j)); ok = j > 0; si(ok) = ord(j(ok)); endfunction ## The name a complaint about the row times should use, a function handle ## having none of its own. ## Whether the method the caller named gathers the rows falling in each ## target bin, which is what decides where the grid of a time unit ends. A ## function handle aggregates; naming no method at all fills. ## The complaint about a time base given as text that names none, or empty ## where it does. 'synchronize' has time bases of its own besides the time ## units and 'regular' that 'retime' knows, and says so when it complains. function errmsg = timeBaseError (spec, forSync) errmsg = ''; if (isa (spec, 'string') && isscalar (spec)) spec = char (spec); endif if (! (ischar (spec) && isrow (spec))) return endif units = {'secondly', 'minutely', 'hourly', 'daily', 'weekly', ... 'monthly', 'quarterly', 'yearly', 'regular'}; bases = {'union', 'intersection', 'commonrange', 'first', 'last'}; if (forSync && ! any (strcmpi (spec, [units, bases]))) errmsg = sprintf (strcat ("'%s' is not a time base; use", ... " 'union', 'intersection', 'commonrange',", ... " 'first', 'last', 'regular', a time unit,", ... " or a time vector."), spec); elseif (! forSync && ! any (strcmpi (spec, units))) errmsg = sprintf (strcat ("'%s' is not a time unit; use one of", ... " 'secondly', 'minutely', 'hourly',", ... " 'daily', 'weekly', 'monthly',", ... " 'quarterly' and 'yearly', a time vector,", ... " or 'regular'."), spec); endif endfunction ## The complaint about the Name-Value arguments of 'retime' and ## 'synchronize', or empty where there is none. function errmsg = retimeOptions (rest, optNames) errmsg = ''; if (mod (numel (rest), 2) != 0) errmsg = "name-value arguments must be in pairs."; return endif for k = 1:2:numel (rest) name = rest{k}; if (isa (name, 'string') && isscalar (name)) name = char (name); endif if (! (ischar (name) && isrow (name) && any (strcmpi (name, optNames)))) errmsg = "invalid optional paired argument."; return endif endfor endfunction ## Whether the argument after the time base is the method. A name is read ## as the method when it names one or when nothing follows it, as in MATLAB; ## anywhere else it starts the Name-Value arguments. function out = retimeIsMethod (rest) out = false; if (isempty (rest)) return endif first = rest{1}; if (is_function_handle (first)) out = true; return endif if (isa (first, 'string') && isscalar (first)) first = char (first); endif if (ischar (first) && isrow (first)) out = any (strcmpi (first, [{'default'}, retimeKnownMethods()])) ... || numel (rest) == 1; endif endfunction ## The methods 'retime' and 'synchronize' know by name, 'default' aside. function out = retimeKnownMethods () out = [{'fillwithmissing', 'fillwithconstant', 'previous', 'next', ... 'nearest', 'linear', 'spline', 'pchip', 'makima'}, ... timetable.retimeAggregations()]; endfunction function out = retimeIsAggregation (method) out = is_function_handle (method) ... || (ischar (method) && isrow (method) && any (strcmp (method, timetable.retimeAggregations ()))); endfunction function out = retimeMethodName (method) if (is_function_handle (method)) out = func2str (method); else out = method; endif endfunction ## Which target bin each row time falls into, or NaN for a row in none. The ## bins are the intervals the target times cut the line into: closed on the ## left by default, so the last one runs on to the end of time, and closed on ## the right instead when asked, so the first one runs back from the start. function [bins, errmsg] = retimeBins (rt, nt, incEdge) bins = []; errmsg = ''; ns = sort (nt); if (isdatetime (rt)) x = seconds (rt(:) - ns(1)); e = seconds (ns(:)' - ns(1)); else x = seconds (rt(:)); e = seconds (ns(:)'); endif if (strcmp (incEdge, 'left')) bins = __binassign__ (x, [e, Inf], true); else bins = __binassign__ (x, [-Inf, e], false); endif ## The bins are numbered along the sorted target times; a caller that asked ## for them in the other order is answered in the order it asked. if (numel (nt) > 1 && ! issorted (nt)) [~, back] = ismember (nt(:), ns(:)); remap = zeros (numel (ns), 1); remap(back) = (1:numel (nt))'; ok = ! isnan (bins); bins(ok) = remap(bins(ok)); endif endfunction ## One variable aggregated onto NNEW target bins. BINS says which bin each ## row belongs to. Returns an errmsg body for the caller to raise. function [col, errmsg] = retimeAggregated (src, bins, nNew, m, vname) col = []; errmsg = ''; counting = {'count', 'firstvalue', 'lastvalue'}; ## The computing methods work on a number, so a variable that is not one ## and cannot be read as one is refused; picking a row out or counting the ## rows asks nothing of the type at all. if (! (is_function_handle (m) || any (strcmp (m, counting)))) if (! (isnumeric (src) || islogical (src) || isdatetime (src) || isduration (src))) errmsg = sprintf (strcat ("the '%s' method takes numeric, logical,", ... " datetime and duration variables only, and '%s'", ... " is of class '%s'."), m, vname, class (src)); return endif if (isdatetime (src) && any (strcmp (m, {'sum', 'prod'}))) errmsg = sprintf (strcat ("the '%s' method has no meaning for the", ... " datetime variable '%s'."), m, vname); return endif if (isduration (src) && strcmp (m, 'prod')) errmsg = sprintf (strcat ("the 'prod' method has no meaning for the", ... " duration variable '%s'."), vname); return endif endif for j = 1:nNew rows = find (bins == j); [v, errmsg] = retimeApply (m, src(rows,:), src); if (! isempty (errmsg)) if (is_function_handle (m)) errmsg = sprintf (strcat ("the function handle method %s returned", ... " a value that is not a single row for variable", ... " '%s'."), func2str (m), vname); endif return endif if (j == 1) col = repmat (v, nNew, 1); else col(j,:) = v; endif endfor endfunction ## One method applied to the rows of one bin, returning a single row. PROTO ## is the whole variable, which carries the type an empty bin still has to ## answer in. function [v, errmsg] = retimeApply (m, x, proto) v = []; errmsg = ''; if (is_function_handle (m)) v = m (x); if (size (v, 1) != 1) errmsg = "not a single row."; endif return endif nc = max (size (proto, 2), 1); if (strcmp (m, 'count')) v = repmat (size (x, 1), 1, nc); return endif if (any (strcmp (m, {'firstvalue', 'lastvalue'}))) if (isempty (x)) [v, errmsg] = retimeMissingRow (proto, nc); elseif (strcmp (m, 'firstvalue')) v = x(1,:); else v = x(end,:); endif return endif ## An empty bin takes the method's own empty value, which is 0 for a sum ## and 1 for a product because that is what each of them makes of nothing, ## and missing for every method that has to look at a value to answer. if (isempty (x)) if (strcmp (m, 'sum')) v = retimeZeroRow (proto, nc, 0); elseif (strcmp (m, 'prod')) v = retimeZeroRow (proto, nc, 1); elseif (islogical (proto) || isinteger (proto)) ## These methods answer in double for such a variable, so an empty bin ## answers with the missing value of a double and not of the variable. v = NaN (1, nc); else [v, errmsg] = retimeMissingRow (proto, nc); endif return endif ## A datetime is measured from its own first entry and a duration in ## seconds, so that each comes back as what it was. if (isdatetime (x)) origin = x(1); y = seconds (x - origin); elseif (isduration (x)) y = seconds (x); else y = double (x); endif nan = isnan (y); cnt = sum (! nan, 1); z = y; z(nan) = 0; switch (m) case 'sum' w = sum (z, 1); case 'prod' z(nan) = 1; w = prod (z, 1); case 'mean' w = sum (z, 1) ./ cnt; case 'min' w = min (y, [], 1); case 'max' w = max (y, [], 1); case {'median', 'mode'} w = NaN (1, columns (y)); for c = 1:columns (y) col = y(! nan(:,c), c); if (! isempty (col)) w(c) = feval (m, col); endif endfor endswitch if (isdatetime (x)) v = origin + seconds (w); elseif (isduration (x)) v = seconds (w); elseif (islogical (x)) v = w; else v = cast (w, class (x)); endif endfunction ## One row of missing values matching the type of PROTO, and one row of a ## constant matching it, for the bins that hold nothing. function [v, errmsg] = retimeMissingRow (proto, nc) v = []; errmsg = ''; if (isdatetime (proto)) v = repmat (NaT, 1, nc); elseif (isduration (proto)) v = seconds (NaN (1, nc)); elseif (isa (proto, 'categorical')) v = repmat (categorical (missing), 1, nc); elseif (isa (proto, 'string')) v = repmat (string (missing), 1, nc); elseif (iscellstr (proto)) v = repmat ({''}, 1, nc); elseif (islogical (proto)) ## Neither a logical nor a character has a missing value, and an empty ## bin still has to answer in the variable's own type. v = false (1, nc); elseif (ischar (proto)) v = repmat (" ", 1, nc); elseif (iscell (proto)) v = repmat ({''}, 1, nc); elseif (isinteger (proto)) v = NaN (1, nc); elseif (isnumeric (proto)) v = NaN (1, nc, class (proto)); else errmsg = sprintf (strcat ("a bin holding no row has no value for a", ... " variable of class '%s'."), class (proto)); endif endfunction function v = retimeZeroRow (proto, nc, k) if (isduration (proto)) v = seconds (k * ones (1, nc)); elseif (islogical (proto)) v = k * ones (1, nc); else v = cast (k * ones (1, nc), class (proto)); endif endfunction ## Whether a 'Constant' can stand in for a value of variable SRC, and the ## complaint when it cannot. The constant's type must belong to the ## variable's own family: that MATLAB can coerce one into the other is not ## enough, a bare number meaning nine days in a duration variable and the ## text "7" in a string one. Numbers and logicals are one family, as they ## are everywhere else, and text names a category. function errmsg = retimeConstantFits (konst, src, vname) errmsg = ''; ## The missing value itself stands for a value of any type that has one. if (isa (konst, 'missing') && isscalar (konst)) if (! retimeHasMissing (src)) errmsg = sprintf (strcat ("variable '%s' of class '%s' has no", ... " missing value to fill with."), vname, ... class (src)); endif return endif txt = ischar (konst) || isa (konst, 'string') || iscellstr (konst); ## One value, and for a variable that holds text one word of it: a category ## name is a word rather than a letter, so a character vector counts as a ## single value there and nowhere else. txtVar = isa (src, 'categorical') || isa (src, 'string') ... || iscellstr (src); if (! (isscalar (konst) || (txtVar && ischar (konst) && isrow (konst)))) errmsg = "'Constant' must be a scalar."; return endif if (isnumeric (src) || islogical (src)) ok = isnumeric (konst) || islogical (konst); elseif (isdatetime (src)) ok = isdatetime (konst); elseif (isduration (src)) ok = isduration (konst); elseif (isa (src, 'calendarDuration')) ok = isa (konst, 'calendarDuration'); elseif (isa (src, 'categorical')) ok = isa (konst, 'categorical') || txt; elseif (isa (src, 'string')) ok = isa (konst, 'string') || ischar (konst); elseif (iscellstr (src)) ok = txt; elseif (iscell (src)) ok = iscell (konst); else ok = strcmp (class (konst), class (src)); endif if (! ok) errmsg = sprintf (strcat ("the 'Constant' is of class '%s', which is", ... " not a value of variable '%s' of class '%s'."), ... class (konst), vname, class (src)); endif endfunction ## The two options both 'retime' and 'synchronize' read the same way. ## 'EndValues' is narrower here than in 'fillmissing': a target time outside ## the old span either extrapolates or takes a constant, the method names ## 'fillmissing' also accepts having no meaning for a grid that reaches past ## the data. function [endVals, incEdge, edgeGiven, errmsg] = ... retimeCheckOptions (endVals, incEdge) errmsg = ''; edgeGiven = wasGiven (incEdge); if (! edgeGiven) incEdge = 'left'; endif if (isa (endVals, 'string') && isscalar (endVals)) endVals = char (endVals); endif if (ischar (endVals)) if (! strcmpi (endVals, 'extrap')) errmsg = strcat ("'EndValues' must be 'extrap' or a constant", ... " scalar."); return endif endVals = 'extrap'; elseif (! isscalar (endVals)) errmsg = "'EndValues' constant must be a scalar."; return endif if (isa (incEdge, 'string') && isscalar (incEdge)) incEdge = char (incEdge); endif if (! (ischar (incEdge) && isrow (incEdge) && any (strcmpi (incEdge, {'left', 'right'})))) errmsg = "'IncludedEdge' must be 'left' or 'right'."; return endif incEdge = lower (incEdge); endfunction ## Whether each option the caller gave is one the chosen method can use. ## 'Constant' is what 'fillwithconstant' fills with and there is no sensible ## value to invent for it, so it is required rather than defaulted; the ## others are warned about and ignored, once per call. function [errmsg, warns] = retimeOptionUse (method, kGiven, endVals, ... edgeGiven) errmsg = ''; warns = {}; aggs = timetable.retimeAggregations (); isAggCall = is_function_handle (method) || any (strcmp (method, aggs)); if (strcmp (method, 'fillwithconstant') && ! kGiven) errmsg = strcat ("the 'fillwithconstant' method requires a", ... " 'Constant'."); return endif if (kGiven && ! strcmp (method, 'fillwithconstant')) warns{end+1} = sprintf (strcat ("'Constant' has no effect with the", ... " '%s' method."), retimeMethodName (method)); endif if (! any (strcmp (endVals, 'extrap')) && isAggCall) warns{end+1} = sprintf (strcat ("'EndValues' has no effect with the", ... " '%s' method."), retimeMethodName (method)); endif if (edgeGiven && ! isAggCall) warns{end+1} = sprintf (strcat ("'IncludedEdge' has no effect with", ... " the '%s' method."), ... retimeMethodName (method)); endif endfunction ## The row times every operand of a 'synchronize' is put onto. A timetable ## with no rows says nothing about where the answer should sit, so it takes ## no part in choosing; without that an empty operand would empty an ## intersection. function [nt, errmsg] = synchronizeBase (ops, spec, tstep, srate, isAgg, ... incEdge) nt = []; errmsg = ''; if (nargin < 5) isAgg = false; endif if (nargin < 6) incEdge = 'left'; endif have = {}; for k = 1:numel (ops) o = ops{k}; rt = o.Properties.RowTimes; if (! isempty (rt)) have{end+1} = rt(:); endif endfor if (isempty (have)) o = ops{1}; nt = o.Properties.RowTimes; return endif if (isdatetime (spec) || isduration (spec)) [nt, errmsg] = retimeTimes (have{1}, spec, tstep, srate, isAgg, ... incEdge); return endif if (isa (spec, 'string') && isscalar (spec)) spec = char (spec); endif if (! (ischar (spec) && isrow (spec))) errmsg = strcat ("the time base must be a time vector, a time unit,", ... " 'union', 'intersection', 'commonrange', 'first',", ... " 'last', or 'regular'."); return endif all = have{1}; for k = 2:numel (have) all = [all; have{k}]; endfor all = unique (all); switch (lower (spec)) case 'union' nt = all; case 'intersection' nt = have{1}; for k = 2:numel (have) nt = intersect (nt, have{k}); endfor nt = nt(:); case 'commonrange' lo = min (have{1}); hi = max (have{1}); for k = 2:numel (have) lo = max (lo, min (have{k})); hi = min (hi, max (have{k})); endfor nt = all(all >= lo & all <= hi); case 'first' o = ops{1}; nt = o.Properties.RowTimes(:); case 'last' o = ops{end}; nt = o.Properties.RowTimes(:); otherwise ## Anything left is a time unit or 'regular', and a complaint about it ## has to offer the time bases as well, which 'retime' knows nothing ## about. errmsg = timeBaseError (spec, true); if (isempty (errmsg)) [nt, errmsg] = retimeTimes (all, spec, tstep, srate, isAgg, ... incEdge); endif endswitch endfunction ## Two operands carrying a variable of the same name have both of them ## renamed for the timetable they came from, which is what the caller called ## it. An operand the caller did not name, an expression rather than a ## variable, is known by its place in the call. function ops = synchronizeRename (ops, names) all = {}; for k = 1:numel (ops) o = ops{k}; all = [all, o.Properties.VariableNames]; endfor [~, first] = unique (all, 'first'); dup = all; dup(first) = []; if (isempty (dup)) return endif for k = 1:numel (ops) o = ops{k}; vn = o.Properties.VariableNames; tag = names{k}; if (isempty (tag)) tag = sprintf ("%d", k); endif for j = 1:numel (vn) if (any (strcmp (vn{j}, dup))) vn{j} = sprintf ("%s_%s", vn{j}, tag); endif endfor o.Properties.VariableNames = vn; ops{k} = o; endfor ## Operands under one name still clash; the later ones are numbered, as in ## MATLAB. final = cellfun (@(o) o.Properties.VariableNames, ops, ... "UniformOutput", false); taken = [final{:}]; seen = {}; for k = 1:numel (ops) vn = final{k}; for j = 1:numel (vn) if (any (strcmp (vn{j}, seen))) m = 1; while (any (strcmp (sprintf ("%s_%d", vn{j}, m), [taken, seen]))) m++; endwhile vn{j} = sprintf ("%s_%d", vn{j}, m); endif seen{end+1} = vn{j}; endfor if (! isequal (vn, final{k})) o = ops{k}; o.Properties.VariableNames = vn; ops{k} = o; endif endfor endfunction ## The calendar unit a calendarDuration lag is counted in, and how many of ## that unit it holds. A step mixing months with days names no single unit ## and so cannot count a lag at all. function [unit, per, errmsg] = lagCalendarUnit (cd) unit = ''; per = 0; errmsg = ''; if (isempty (cd)) errmsg = "the timetable has no calendar time step."; return endif dv = datevec (cd); months = dv(1) * 12 + dv(2); dys = dv(3); if (any (dv(4:6))) errmsg = strcat ("a lag in calendar units cannot carry a time of", ... " day."); return endif if (months != 0 && dys != 0) errmsg = strcat ("a lag in calendar units cannot mix months with", ... " days."); return endif if (months != 0) unit = 'months'; per = months; elseif (dys != 0) unit = 'days'; per = dys; else errmsg = "the lag must not be zero calendar units."; endif endfunction ## The time step the caller named, if it named one at all. A sample rate ## names it just as a time step does, in seconds. function step = retimeGivenStep (tstep, srate) step = []; if (wasGiven (tstep)) step = tstep; elseif (wasGiven (srate)) step = seconds (1 / srate); endif endfunction ## Whether a variable can hold a missing value at all. A logical, a ## character matrix and a cell of anything but text have none, so they can ## carry no gap and nothing can fill one for them. function tf = retimeHasMissing (v) tf = ! (islogical (v) || ischar (v) || (iscell (v) && ! iscellstr (v))); endfunction ## The step boundary at or before T. A grid asked for as a step starts on ## one of those, as a grid asked for as a time unit starts on the unit's own ## boundary; for a datetime the boundaries are counted from midnight of its ## own day, there being no other origin to count them from. function t = floorToStep (t, step) s = seconds (step); if (isdatetime (t)) origin = dateshift (t, 'start', 'day'); t = origin + step * floor (seconds (t - origin) / s); else t = step * floor (seconds (t) / s); endif endfunction ## Whether the caller named an option, read off the argument list rather than ## off the value it parsed to. The value cannot answer it: the sentinel that ## marks an option as absent is 'missing', which is also a value a caller may ## legitimately ask for. function tf = namedOption (args, name) tf = false; for k = 1:2:numel (args) v = args{k}; if (isa (v, 'string') && isscalar (v)) v = char (v); endif if (ischar (v) && isrow (v) && strcmpi (v, name)) tf = true; return endif endfor endfunction ## Per-variable metadata with an entry prepended for the row times when they ## have become a variable of their own, since they carried none as times. function meta = shiftMeta (meta, prepend, blank) if (prepend) meta = [{blank}, meta]; endif endfunction ## 'pkg test' reads the BISTs from the file it is testing, so a class ## whose suite lives in inst/tests/ reads as carrying none. This block ## answers for it and checks nothing itself. %!test %! ## The suite for this class is inst/tests/timetable.m-tst. pr0m1th3as-datatypes-9450b29/inst/tscollection.m000066400000000000000000002003701527370255000215350ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef tscollection ## -*- texinfo -*- ## @deftp {datatypes} tscollection ## ## Collection of time series on one time vector. ## ## A @code{tscollection} object holds several @code{timeseries} objects, ## its members, which share one time vector: a sample of the collection is ## the sample of every member at one time. Each member is reached by its ## name, as @code{@var{tc}.@var{name}}, and keeps its own data, quality ## codes, events and metadata; only the time vector and its ## @qcode{TimeInfo} are the collection's. ## ## @code{tscollection} is a value class: every method returns a modified ## copy and leaves its input unchanged. ## ## Its size is the number of samples by the number of members, and ## @code{@var{tc}(@var{i}, @var{j})} selects samples @var{i} of members ## @var{j}, the members given by position or by name. ## ## Member names are unique ignoring case, and read with the exact name; ## MATLAB reads @code{@var{tc}.A} for a member @qcode{a}. A member cannot ## take the name of one of the four properties. ## ## @seealso{timeseries, istscollection, timeseries2timetable, ## tsdata.timemetadata} ## @end deftp properties (Dependent) ## -*- texinfo -*- ## @deftp {tscollection} {property} Name ## ## The name of the collection. ## ## A character vector, @qcode{'unnamed'} by default. A string scalar is ## converted; any other value is refused, where MATLAB accepts it. ## ## @end deftp Name ## -*- texinfo -*- ## @deftp {tscollection} {property} Time ## ## The time vector every member shares. ## ## A column vector of finite, non-decreasing times, in ## @qcode{@var{tc}.TimeInfo.Units}, counted from ## @qcode{@var{tc}.TimeInfo.StartDate} when that is set, and stored as ## @code{double}. Repeated times are allowed. Assigning it gives every ## member the new times, without moving any sample; it must hold as many ## times as there are samples, and an unsorted one is refused. ## ## @end deftp Time ## -*- texinfo -*- ## @deftp {tscollection} {property} TimeInfo ## ## The metadata of the time vector, a @code{tsdata.timemetadata} object, ## which every member shares. Assigning it, or one of its properties, ## assigns it to every member. Its @qcode{Length}, @qcode{Start}, ## @qcode{End} and @qcode{Increment} follow @qcode{Time} and cannot be ## assigned. ## ## @end deftp TimeInfo ## -*- texinfo -*- ## @deftp {tscollection} {property} Length ## ## The number of samples, which is the length of the time vector. ## Read-only. ## ## @end deftp Length endproperties properties (Access = private) name_ = 'unnamed' time_ = zeros (0, 1) timeInfo_ = tsdata.timemetadata () ## The members, in order, and the name each is reached by members_ = {} names_ = {} endproperties methods (Hidden) function display (this) disp (this, true); endfunction function str = disp (this, summary) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) if (nargin > 1) str = evalc ('disp (this, summary)'); else str = evalc ('disp (this)'); endif return; endif if (nargin > 1 && summary) fprintf ("\nTime Series Collection Object: %s\n\n", this.name_); if (isempty (this.time_)) fprintf (" Empty\n\n"); return; endif fprintf ("Time vector characteristics\n\n"); if (isempty (this.timeInfo_.StartDate)) units = this.timeInfo_.Units; fprintf (" %-22s%s %s\n", 'Start time', ... num2str (this.time_(1)), units); fprintf (" %-22s%s %s\n", 'End time', ... num2str (this.time_(end)), units); else dates = getabstime (this); fprintf (" %-22s%s\n", 'Start date', dates{1}); fprintf (" %-22s%s\n", 'End date', dates{end}); endif fprintf ("\nMember Time Series Objects:\n\n"); fprintf (" %s\n", this.names_{:}); fprintf ("\n\n"); return; endif sz = size (this); fprintf (" %dx%d tscollection array with properties:\n\n", sz); names = {'Name', 'Time', 'TimeInfo', 'Length'}; for i = 1:numel (names) fprintf ("%12s: %s\n", names{i}, ... timeseries.dispValue (subsref (this, ... struct ('type', '.', ... 'subs', names{i})))); endfor endfunction endmethods ################################################################################ ## ** Create and describe 'tscollection' ** ## ################################################################################ ## Available Methods ## ## ## ## 'tscollection' 'get' 'set' 'fieldnames' ## ## 'properties' 'size' 'length' 'isempty' ## ## 'isequal' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{tc} =} tscollection () ## @deftypefnx {tscollection} {@var{tc} =} tscollection (@var{ts}) ## @deftypefnx {tscollection} {@var{tc} =} tscollection (@{@var{ts1}, @var{ts2}, @dots{}@}) ## @deftypefnx {tscollection} {@var{tc} =} tscollection (@var{time}) ## @deftypefnx {tscollection} {@var{tc} =} tscollection (@dots{}, @qcode{'Name'}, @var{name}) ## ## Create a @code{tscollection} object. ## ## @code{@var{tc} = tscollection ()} returns an empty collection, with no ## samples and no members. ## ## @code{@var{tc} = tscollection (@var{ts})} returns a collection of the ## single @code{timeseries} @var{ts}, on its time vector and its ## @qcode{TimeInfo}. A series with no samples gives an empty collection, ## as in MATLAB. ## ## @code{@var{tc} = tscollection (@{@var{ts1}, @var{ts2}, @dots{}@})} ## returns a collection of the series in the cell array, a row or a column, ## on the time vector of the first; every other series must have the same ## times, in the same units, and the same @qcode{TimeInfo.StartDate}. An ## empty cell array gives an empty collection, and a series with no ## samples is refused, as in MATLAB. ## ## @code{@var{tc} = tscollection (@var{time})} returns a collection with no ## members on the numeric vector @var{time}, which must be finite and is ## sorted, in seconds. ## ## Each member is named by @code{matlab.lang.makeValidName} of the name of ## its series, which it then takes: a series named @qcode{'my data'} ## becomes the member @qcode{myData}, and one named @qcode{''} the member ## @qcode{x}. The names must be unique ignoring case, and none may be the ## name of a property: @qcode{Name}, @qcode{Time}, @qcode{TimeInfo} or ## @qcode{Length}. MATLAB refuses only @qcode{Length}, and a member named ## @qcode{Time} then hides the time vector. ## ## @code{@var{tc} = tscollection (@dots{}, @qcode{'Name'}, @var{name})} ## names the collection, @qcode{'unnamed'} by default; the option name is ## matched in any case, and a later occurrence overrides an earlier one. ## ## MATLAB ignores a second positional argument, so that ## @code{tscollection (@var{ts1}, @var{ts2})} holds @var{ts1} only; here it ## is refused. ## ## @seealso{timeseries, tscollection.addts, istscollection} ## @end deftypefn function this = tscollection (varargin) if (nargin == 0) return; endif arg1 = varargin{1}; opts = varargin(2:end); if (! isempty (opts) && ! isText (opts{1})) error ("tscollection: too many input arguments."); endif if (mod (numel (opts), 2) != 0) error ("tscollection: name-value arguments must be in pairs."); endif for i = 1:2:numel (opts) if (! (isText (opts{i}) && strcmpi (opts{i}, 'Name'))) error ("tscollection: invalid optional paired argument."); endif if (! isText (opts{i+1})) error ("tscollection: 'Name' must be a character vector."); endif this.name_ = char (opts{i+1}); endfor if (isa (arg1, 'timeseries')) if (! isscalar (arg1)) error (strcat ("tscollection: TS must be a single timeseries;", ... " give several in a cell array.")); endif ## A series with no samples gives an empty collection, as in MATLAB if (arg1.Length == 0) return; endif list = {arg1}; elseif (iscell (arg1)) if (! all (cellfun (@(x) isa (x, 'timeseries') && isscalar (x), ... arg1(:)))) error (strcat ("tscollection: a cell array must hold one", ... " timeseries in each cell.")); endif if (any (cellfun (@(x) x.Length == 0, arg1(:)))) error (strcat ("tscollection: a timeseries with no samples", ... " cannot join a collection in a cell array.")); endif list = arg1(:)'; elseif (isnumeric (arg1) && isreal (arg1) && (isvector (arg1) || isempty (arg1))) time = double (arg1(:)); if (! all (isfinite (time))) error ("tscollection: TIME must be finite."); endif this.time_ = sort (time); this.timeInfo_.TimeVector = this.time_; return; else error (strcat ("tscollection: the first argument must be a time", ... " vector, a timeseries or a cell array of timeseries.")); endif if (! isempty (list)) this.time_ = list{1}.Time; ti = list{1}.TimeInfo; ti.TimeVector = this.time_; this.timeInfo_ = ti; endif [this, errmsg] = addMembers (this, list); if (! isempty (errmsg)) error ("tscollection: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{S} =} get (@var{tc}) ## @deftypefnx {tscollection} {@var{value} =} get (@var{tc}, @var{name}) ## @deftypefnx {tscollection} {@var{values} =} get (@var{tc}, @var{names}) ## ## Return property values and members. ## ## @code{@var{S} = get (@var{tc})} returns a structure of the four ## properties of the collection @var{tc} followed by its members. ## ## @code{@var{value} = get (@var{tc}, @var{name})} returns the property or ## member @var{name}, a character vector or a string scalar matched in any ## case. ## ## @code{@var{values} = get (@var{tc}, @var{names})} returns a row cell ## array of the properties and members named in the cell array ## @var{names}. ## ## @seealso{tscollection.set, tscollection.fieldnames} ## @end deftypefn function out = get (this, varargin) scope = 'tscollection.get'; if (numel (varargin) > 1) error ("%s: too many input arguments.", scope); endif allNames = fieldnames (this); if (isempty (varargin)) out = struct (); for i = 1:numel (allNames) out.(allNames{i}) = fieldValue (this, allNames{i}); endfor return; endif names = varargin{1}; isList = iscell (names) || (isstring (names) && ! isscalar (names)); if (isstring (names)) names = cellstr (names); endif if (ischar (names) && isrow (names)) names = {names}; endif if (! iscellstr (names)) error (strcat ("%s: NAME must be a character vector or a cell", ... " array of character vectors."), scope); endif out = cell (1, numel (names)); for i = 1:numel (names) k = find (strcmp (names{i}, allNames), 1); if (isempty (k)) k = find (strcmpi (names{i}, allNames), 1); endif if (isempty (k)) error ("%s: unknown property: '%s'", scope, names{i}); endif out{i} = fieldValue (this, allNames{k}); endfor if (! isList) out = out{1}; endif endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {} set (@var{tc}, @var{name}, @var{value}) ## @deftypefnx {tscollection} {} set (@var{tc}, @var{name1}, @var{value1}, @dots{}) ## @deftypefnx {tscollection} {@var{tc2} =} set (@var{tc}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {tscollection} {@var{S} =} set (@var{tc}) ## ## Set property values and members. ## ## @code{set (@var{tc}, @var{name}, @var{value}, @dots{})} sets each ## property or member @var{name}, matched in any case, to its @var{value}, ## in the order given, in the variable @var{tc} of the caller, as dot ## assignment does. @var{tc} must therefore be a variable. ## ## @code{@var{tc2} = set (@var{tc}, @var{name}, @var{value}, @dots{})} ## returns the modified collection and leaves @var{tc} as it was. ## ## @code{@var{S} = set (@var{tc})} returns what @code{get (@var{tc})} ## does. ## ## @seealso{tscollection.get} ## @end deftypefn function varargout = set (this, varargin) scope = 'tscollection.set'; if (nargin == 1) varargout{1} = get (this); return; endif if (mod (numel (varargin), 2) != 0) error ("%s: name-value arguments must be in pairs.", scope); endif allNames = fieldnames (this); for i = 1:2:numel (varargin) name = varargin{i}; if (! isText (name)) error ("%s: NAME must be a character vector.", scope); endif k = find (strcmp (char (name), allNames), 1); if (isempty (k)) k = find (strcmpi (char (name), allNames), 1); endif if (isempty (k)) error ("%s: unknown property: '%s'", scope, char (name)); endif this = subsasgn (this, struct ('type', '.', 'subs', allNames{k}), ... varargin{i+1}); endfor if (nargout > 0) varargout{1} = this; return; endif ## With no output the caller's own variable is set, as in MATLAB name = inputname (1); if (isempty (name)) error (strcat ("%s: with no output argument, TC must be a", ... " variable."), scope); endif assignin ('caller', name, this); endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{names} =} fieldnames (@var{tc}) ## ## Return the names of the properties and members. ## ## @code{@var{names} = fieldnames (@var{tc})} returns a column cell array ## of @qcode{Name}, @qcode{Time}, @qcode{TimeInfo} and @qcode{Length}, ## followed by the names of the members of @var{tc} in order. ## ## @seealso{tscollection.properties, tscollection.gettimeseriesnames} ## @end deftypefn function names = fieldnames (this) names = [{'Name'; 'Time'; 'TimeInfo'; 'Length'}; this.names_(:)]; endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{names} =} properties (@var{tc}) ## ## Return the names of the properties and members. ## ## @code{@var{names} = properties (@var{tc})} returns what ## @code{fieldnames (@var{tc})} does: the members are reached as properties ## are. ## ## @seealso{tscollection.fieldnames} ## @end deftypefn function names = properties (this) names = fieldnames (this); endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{sz} =} size (@var{tc}) ## @deftypefnx {tscollection} {@var{dim_sz} =} size (@var{tc}, @var{dim}) ## @deftypefnx {tscollection} {[@var{nsamples}, @var{nmembers}] =} size (@var{tc}) ## ## Return the number of samples and the number of members. ## ## @code{@var{sz} = size (@var{tc})} returns @code{[@var{nsamples}, ## @var{nmembers}]}. @code{size (@var{tc}, @var{dim})} returns the size ## along dimension @var{dim}, a positive integer or a vector of them; ## dimensions above 2 have size 1. With several outputs each takes one ## dimension, where MATLAB refuses more than one output. ## ## @seealso{tscollection.length, tscollection.isempty} ## @end deftypefn function varargout = size (this, dim) sz = [numel(this.time_), numel(this.names_)]; if (nargin == 2) if (! (isnumeric (dim) && isreal (dim) && ! isempty (dim) && all (dim(:) == fix (dim(:)) & dim(:) >= 1))) error ("tscollection.size: DIM must be positive integers."); endif dim_sz = ones (1, numel (dim)); valid = dim <= 2; dim_sz(valid) = sz(dim(valid)); if (nargout > 1) varargout = num2cell (dim_sz); else varargout{1} = dim_sz; endif elseif (nargout <= 1) varargout{1} = sz; else varargout{1} = sz(1); varargout{2} = sz(2); [varargout{3:nargout}] = deal (1); endif endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{n} =} length (@var{tc}) ## ## Return the number of samples. ## ## @code{@var{n} = length (@var{tc})} returns @qcode{@var{tc}.Length}, the ## number of times, whatever the number of members. ## ## @seealso{tscollection.size} ## @end deftypefn function n = length (this) n = numel (this.time_); endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{tf} =} isempty (@var{tc}) ## ## True for a collection with no samples. ## ## @code{@var{tf} = isempty (@var{tc})} returns @code{true} when the ## collection @var{tc} has no times, whether it has members or not, and ## @code{false} for a collection with times and no members. ## ## @seealso{tscollection.size} ## @end deftypefn function tf = isempty (this) tf = isempty (this.time_); endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{tf} =} isequal (@var{tc1}, @var{tc2}, @dots{}) ## ## True when collections are equal. ## ## @code{@var{tf} = isequal (@var{tc1}, @var{tc2}, @dots{})} returns ## @code{true} when every argument is a @code{tscollection} with the same ## name, time vector and @qcode{TimeInfo}, and the same members in the same ## order, each equal as @code{isequal} compares series, @code{NaN} unequal ## to itself. ## ## @seealso{timeseries.isequal} ## @end deftypefn function tf = isequal (varargin) if (nargin < 2) error ("tscollection.isequal: invalid number of input arguments."); endif tf = false; a = varargin{1}; for i = 2:nargin b = varargin{i}; if (! (isa (a, 'tscollection') && isa (b, 'tscollection'))) return; endif if (! (strcmp (a.name_, b.name_) && isequal (a.time_, b.time_) && sameTimeInfo (a.timeInfo_, b.timeInfo_, true) && isequal (a.names_, b.names_))) return; endif for k = 1:numel (a.members_) if (! isequal (a.members_{k}, b.members_{k})) return; endif endfor endfor tf = true; endfunction endmethods ################################################################################ ## ** Members ** ## ################################################################################ ## Available Methods ## ## ## ## 'addts' 'removets' 'gettimeseriesnames' ## ## 'settimeseriesnames' 'setTimeseriesName' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{tc} =} addts (@var{tc}, @var{ts}) ## @deftypefnx {tscollection} {@var{tc} =} addts (@var{tc}, @{@var{ts1}, @var{ts2}, @dots{}@}) ## @deftypefnx {tscollection} {@var{tc} =} addts (@var{tc}, @var{data}, @var{name}) ## ## Add members to a collection. ## ## @code{@var{tc} = addts (@var{tc}, @var{ts})} adds the @code{timeseries} ## @var{ts} as the last member of @var{tc}, and ## @code{@var{tc} = addts (@var{tc}, @{@var{ts1}, @var{ts2}, @dots{}@})} ## adds each series in the cell array in turn. Each must have the times of ## the collection, in the same units; a series with a ## @qcode{TimeInfo.StartDate} added to a collection without one loses its ## start date, with a warning, as in MATLAB, and the reverse is refused. A ## collection with no samples and no members takes the times of the first ## series. Each member is named as the constructor names it. ## ## @code{@var{tc} = addts (@var{tc}, @var{data}, @var{name})} adds a member ## @var{name} holding the samples @var{data}, a numeric or logical array of ## one sample per time of the collection, read against the time vector as ## the @code{timeseries} constructor reads it. ## ## Names must be unique ignoring case: MATLAB refuses @qcode{A} beside ## @qcode{a} here, though @code{settimeseriesnames} allows it there. ## MATLAB also ignores a fourth argument, which is refused here. ## ## @seealso{tscollection.removets, tscollection.settimeseriesnames} ## @end deftypefn function this = addts (this, varargin) scope = 'tscollection.addts'; if (numel (varargin) < 1) error ("%s: invalid number of input arguments.", scope); elseif (numel (varargin) > 2) error ("%s: too many input arguments.", scope); endif arg = varargin{1}; if (numel (varargin) == 2) name = varargin{2}; if (! isText (name)) error ("%s: NAME must be a character vector.", scope); endif if (iscell (arg) || ! (isnumeric (arg) || islogical (arg))) error ("%s: DATA must be a numeric or logical array.", scope); endif if (isempty (this.time_)) error ("%s: TC has no times to add DATA on.", scope); endif try ts = timeseries (arg, this.time_, 'Name', char (name)); catch error ("%s: DATA must have one sample per time of TC.", scope); end_try_catch ts.TimeInfo = this.timeInfo_; list = {ts}; elseif (isa (arg, 'timeseries') && isscalar (arg)) list = {arg}; elseif (iscell (arg) && all (cellfun (@(x) isa (x, 'timeseries') ... && isscalar (x), arg(:)))) list = arg(:)'; elseif (isnumeric (arg) || islogical (arg)) error ("%s: DATA must be given with a NAME.", scope); else error (strcat ("%s: TS must be a timeseries or a cell array of", ... " timeseries."), scope); endif if (isempty (this.time_) && isempty (this.names_) && ! isempty (list)) this.time_ = list{1}.Time; ti = list{1}.TimeInfo; ti.TimeVector = this.time_; this.timeInfo_ = ti; endif [this, errmsg] = addMembers (this, list); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{tc} =} removets (@var{tc}, @var{names}) ## ## Remove members from a collection. ## ## @code{@var{tc} = removets (@var{tc}, @var{names})} removes the members ## @var{names}, a character vector, a cell array of character vectors or a ## string array, each the exact name of a member. The time vector stays, ## so removing every member leaves a collection with times and no members. ## A name that is no member is refused, where MATLAB ignores it. ## ## @seealso{tscollection.addts, tscollection.gettimeseriesnames} ## @end deftypefn function this = removets (this, names) scope = 'tscollection.removets'; if (nargin < 2) error ("%s: invalid number of input arguments.", scope); endif [names, errmsg] = nameList (names); if (! isempty (errmsg)) error ("%s: NAMES %s", scope, errmsg); endif drop = false (size (this.names_)); for i = 1:numel (names) k = find (strcmp (names{i}, this.names_)); if (isempty (k)) error ("%s: TC has no member named '%s'.", scope, names{i}); endif drop(k) = true; endfor this.members_(drop) = []; this.names_(drop) = []; endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{names} =} gettimeseriesnames (@var{tc}) ## ## Return the names of the members. ## ## @code{@var{names} = gettimeseriesnames (@var{tc})} returns a row cell ## array of the names of the members of @var{tc}, in order, or @code{@{@}} ## when it has none. ## ## @seealso{tscollection.settimeseriesnames, tscollection.fieldnames} ## @end deftypefn function names = gettimeseriesnames (this) if (isempty (this.names_)) names = {}; else names = this.names_; endif endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{tc} =} settimeseriesnames (@var{tc}, @var{old}, @var{new}) ## ## Rename a member. ## ## @code{@var{tc} = settimeseriesnames (@var{tc}, @var{old}, @var{new})} ## renames the member @var{old}, its exact name, to @var{new}, a valid ## variable name, both character vectors or string scalars; the series ## takes the new name too, and the member becomes the last, as in ## MATLAB. @var{new} must not be the name of another member, ignoring ## case, nor of one of the four properties. ## ## MATLAB accepts the name of another member and drops that member, and ## accepts a name differing from another only in case, after which that ## member cannot be reached; both are refused here. ## ## @seealso{tscollection.gettimeseriesnames, tscollection.addts} ## @end deftypefn function this = settimeseriesnames (this, old, new) scope = 'tscollection.settimeseriesnames'; if (nargin < 3) error ("%s: invalid number of input arguments.", scope); endif if (! isText (old)) error ("%s: OLD must be a character vector.", scope); endif old = char (old); k = find (strcmp (old, this.names_)); if (isempty (k)) error ("%s: TC has no member named '%s'.", scope, old); endif if (! isText (new) || ! isvarname (char (new))) error ("%s: NEW must be a valid variable name.", scope); endif new = char (new); if (strcmp (new, old)) return; endif others = this.names_; others(k) = []; errmsg = nameClash (new, others); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif ## The renamed member moves to the end, as in MATLAB m = this.members_{k}; m.Name = new; this.names_ = [others, {new}]; this.members_(k) = []; this.members_{end+1} = m; endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{tc} =} setTimeseriesName (@var{tc}, @var{old}, @var{new}) ## ## Rename a member. ## ## @code{@var{tc} = setTimeseriesName (@var{tc}, @var{old}, @var{new})} is ## @code{settimeseriesnames}, kept for compatibility; MATLAB marks it ## obsolete. ## ## @seealso{tscollection.settimeseriesnames} ## @end deftypefn function this = setTimeseriesName (this, varargin) try this = settimeseriesnames (this, varargin{:}); catch err error ("tscollection.setTimeseriesName: %s", ... regexprep (err.message, '^[\w.]+: ', '')); end_try_catch endfunction endmethods ################################################################################ ## ** Samples and time ** ## ################################################################################ ## Available Methods ## ## ## ## 'addsampletocollection' 'delsamplefromcollection' ## ## 'getsampleusingtime' 'hasduplicatetimes' ## ## 'getabstime' 'setabstime' 'resample' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{tc} =} addsampletocollection (@var{tc}, @qcode{'Time'}, @var{time}, @var{name1}, @var{data1}, @dots{}) ## ## Add samples to every member. ## ## @code{@var{tc} = addsampletocollection (@var{tc}, @qcode{'Time'}, ## @var{time}, @var{name1}, @var{data1}, @dots{})} inserts samples at the ## times @var{time}, keeping the collection in time order; a sample at a ## time already present goes after the samples there. The member ## @var{name1}, its exact name, takes the samples @var{data1}, one per ## time, read against @var{time} as the @code{timeseries} constructor reads ## data against times and each of the size of the member's samples. A ## member not named takes @code{NaN}, or 0 for integer data and ## @code{false} for logical data. @qcode{'Time'} is matched in any case. ## A row of data for a column of times is accepted, as the ## @code{timeseries} constructor accepts it; MATLAB refuses it for a ## collection. ## ## For a collection with no @qcode{TimeInfo.StartDate} @var{time} is ## numbers in its units; for one with a start date it may also be dates as ## text, or a @code{datetime}. A member with quality codes gives each new ## sample the code of the sample nearest in time, the later one on a tie, ## as MATLAB does. ## ## @seealso{tscollection.delsamplefromcollection, timeseries.addsample} ## @end deftypefn function this = addsampletocollection (this, varargin) scope = 'tscollection.addsampletocollection'; if (mod (numel (varargin), 2) != 0) error ("%s: name-value arguments must be in pairs.", scope); endif time = []; data = cell (size (this.names_)); given = false (size (this.names_)); for i = 1:2:numel (varargin) if (! isText (varargin{i})) error ("%s: invalid optional paired argument.", scope); endif key = char (varargin{i}); if (strcmpi (key, 'Time')) time = varargin{i+1}; continue; endif k = find (strcmp (key, this.names_)); if (isempty (k)) error ("%s: TC has no member named '%s'.", scope, key); endif if (given(k)) error ("%s: member '%s' is given more than once.", scope, key); endif given(k) = true; data{k} = varargin{i+1}; endfor if (isempty (time)) error ("%s: 'Time' must not be empty.", scope); endif if (isnumeric (time) && ! all (isfinite (time(:)))) error ("%s: 'Time' must be finite.", scope); endif if (ischar (time)) nNew = rows (time); else nNew = numel (time); endif ## The new time vector, from a series of sample positions try p = addsample (proxy (this), 'Data', zeros (nNew, 1), 'Time', time); catch err error ("%s: %s", scope, proxyMessage (err)); end_try_catch for k = 1:numel (this.members_) m = this.members_{k}; ss = getdatasamplesize (m); if (given(k)) d = data{k}; if (iscell (d) || ! (isnumeric (d) || islogical (d))) error (strcat ("%s: the data of '%s' must be a numeric or", ... " logical array."), scope, this.names_{k}); endif try s = timeseries (d, (1:nNew)'); catch error ("%s: the data of '%s' must have one sample per time.", ... scope, this.names_{k}); end_try_catch if (! isempty (ss) && ! isequal (getdatasamplesize (s), ss)) error ("%s: the data of '%s' must hold samples of size %s.", ... scope, this.names_{k}, ... strjoin (arrayfun (@num2str, ss, 'UniformOutput', false), ... 'x')); endif else d = fillSamples (m, ss, nNew); endif this.members_{k} = addsample (m, 'Data', d, 'Time', time); endfor this.time_ = p.Time; this.timeInfo_.TimeVector = this.time_; endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{tc} =} delsamplefromcollection (@var{tc}, @qcode{'Index'}, @var{ind}) ## @deftypefnx {tscollection} {@var{tc} =} delsamplefromcollection (@var{tc}, @qcode{'Value'}, @var{time}) ## ## Remove samples from every member. ## ## @code{@var{tc} = delsamplefromcollection (@var{tc}, @qcode{'Index'}, ## @var{ind})} removes the samples @var{ind}, a vector of positive integers ## not exceeding @qcode{Length}; repeats are allowed and @code{[]} removes ## nothing. A logical mask is refused, as in MATLAB. ## ## @code{@var{tc} = delsamplefromcollection (@var{tc}, @qcode{'Value'}, ## @var{time})} removes every sample at each of the times @var{time}; a ## time no sample has removes nothing. For a collection with no ## @qcode{TimeInfo.StartDate} the times are numbers in its units; for one ## with a start date they may also be dates as text, or a @code{datetime}. ## ## The option names are matched in any case. Removing every sample leaves ## the members, with no samples. ## ## @seealso{tscollection.addsampletocollection, timeseries.delsample} ## @end deftypefn function this = delsamplefromcollection (this, varargin) scope = 'tscollection.delsamplefromcollection'; if (numel (varargin) != 2) error ("%s: invalid number of input arguments.", scope); endif try p = delsample (proxy (this), varargin{:}); catch err error ("%s: %s", scope, proxyMessage (err)); end_try_catch this = subsetSamples (this, p.Data); endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{tc2} =} getsampleusingtime (@var{tc}, @var{t}) ## @deftypefnx {tscollection} {@var{tc2} =} getsampleusingtime (@var{tc}, @var{t}, @qcode{'AllowDuplicateTimes'}, @var{tf}) ## @deftypefnx {tscollection} {@var{tc2} =} getsampleusingtime (@var{tc}, @var{t1}, @var{t2}) ## ## Return a collection of the samples at a time or between two times. ## ## @code{@var{tc2} = getsampleusingtime (@var{tc}, @var{t})} returns the ## collection @var{tc} holding only the sample at time @var{t}. When ## several samples share that time the call is refused, unless ## @qcode{'AllowDuplicateTimes'} is @code{true}, which returns them all. ## ## @code{@var{tc2} = getsampleusingtime (@var{tc}, @var{t1}, @var{t2})} ## returns the samples from @var{t1} to @var{t2}, both included. ## ## The times are read as @code{timeseries.getsampleusingtime} reads them. ## No sample in range gives a collection with no samples and every member. ## The name and every member's properties are kept; MATLAB names the ## result @qcode{'unnamed'} and, when it is empty, drops every member. ## ## @seealso{timeseries.getsampleusingtime} ## @end deftypefn function this = getsampleusingtime (this, varargin) scope = 'tscollection.getsampleusingtime'; try p = getsampleusingtime (proxy (this), varargin{:}); catch err error ("%s: %s", scope, proxyMessage (err)); end_try_catch this = subsetSamples (this, p.Data); endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{tf} =} hasduplicatetimes (@var{tc}) ## ## True where a collection repeats a time. ## ## @code{@var{tf} = hasduplicatetimes (@var{tc})} returns @code{true} when ## two samples of the collection @var{tc} share a time. ## ## @seealso{tscollection.getsampleusingtime} ## @end deftypefn function tf = hasduplicatetimes (this) tf = any (diff (this.time_) == 0); endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{dates} =} getabstime (@var{tc}) ## ## Return the dates of the samples as text. ## ## @code{@var{dates} = getabstime (@var{tc})} returns a column cell array ## of character vectors, the date of each sample of the collection ## @var{tc}, written as @code{timeseries.getabstime} writes them: in the ## format @qcode{TimeInfo.Format}, or @qcode{'dd-mmm-yyyy HH:MM:SS'} when ## that is empty. MATLAB writes the default whatever ## @qcode{TimeInfo.Format} says. A collection with no ## @qcode{TimeInfo.StartDate} is refused. ## ## @seealso{tscollection.setabstime, timeseries.getabstime} ## @end deftypefn function dates = getabstime (this) if (isempty (this.timeInfo_.StartDate)) error ("tscollection.getabstime: TC has no 'TimeInfo.StartDate'."); endif dates = getabstime (proxy (this)); endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{tc} =} setabstime (@var{tc}, @var{dates}) ## @deftypefnx {tscollection} {@var{tc} =} setabstime (@var{tc}, @var{dates}, @var{format}) ## ## Set the time vector of a collection from dates. ## ## @code{@var{tc} = setabstime (@var{tc}, @var{dates})} gives the samples ## of the collection @var{tc} the dates @var{dates}, one per sample, in ## order, exactly as @code{timeseries.setabstime} gives them to a series, ## in the collection's own units; every member takes them. With ## @var{format} every date is read strictly in it. ## ## The dates need not be sorted: every member's samples, with their quality ## codes, are sorted with them. MATLAB sorts the times alone, so each ## sample is given another's date. It also reads every date by the format ## of the first, forces text into @var{format}, and takes numbers and ## @code{datetime} values as relative times, dropping the dates; see ## @code{timeseries.setabstime} for how each is read here. ## ## @seealso{tscollection.getabstime, timeseries.setabstime} ## @end deftypefn function this = setabstime (this, varargin) scope = 'tscollection.setabstime'; if (numel (varargin) < 1 || numel (varargin) > 2) error ("%s: invalid number of input arguments.", scope); endif try p = setabstime (proxy (this), varargin{:}); catch err error ("%s: %s", scope, proxyMessage (err)); end_try_catch for k = 1:numel (this.members_) this.members_{k} = setabstime (this.members_{k}, varargin{:}); endfor this.time_ = p.Time; this.timeInfo_ = p.TimeInfo; endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{tc} =} resample (@var{tc}, @var{time}) ## @deftypefnx {tscollection} {@var{tc} =} resample (@var{tc}, @var{time}, @var{method}) ## @deftypefnx {tscollection} {@var{tc} =} resample (@var{tc}, @var{time}, @var{method}, @var{code}) ## ## Evaluate every member at new times. ## ## @code{@var{tc} = resample (@var{tc}, @var{time})} returns the collection ## @var{tc} with every member evaluated at @var{time} by its own ## interpolation method, as @code{timeseries.resample} evaluates a series: ## at a time outside the collection the data is @code{NaN}, and a member ## with quality codes gives each new time the code of the nearest sample. ## MATLAB raises there for a member with quality codes. @var{time} is ## taken in any order and sorted, as @code{timeseries.resample} takes it; ## MATLAB refuses descending times for a collection. ## ## @code{@var{tc} = resample (@var{tc}, @var{time}, @var{method})} uses ## @var{method}, @qcode{'linear'} or @qcode{'zoh'}, for every member; ## @code{[]} keeps each member's own. ## ## @code{@var{tc} = resample (@var{tc}, @var{time}, @var{method}, ## @var{code})} gives the quality code @var{code} to every new time the ## collection did not already hold, in each member with quality codes; it ## must be listed in the @qcode{QualityInfo.Code} of each of them. Members ## without quality codes are resampled without it. ## ## @seealso{timeseries.resample} ## @end deftypefn function this = resample (this, varargin) scope = 'tscollection.resample'; if (numel (varargin) < 1 || numel (varargin) > 3) error ("%s: invalid number of input arguments.", scope); endif method = []; code = []; if (numel (varargin) >= 2) method = varargin{2}; endif if (numel (varargin) == 3) code = varargin{3}; endif if (isempty (this.time_)) return; endif if (numel (this.time_) < 2) error ("%s: TC must hold at least two samples.", scope); endif try p = resample (proxy (this), varargin{1}, method); catch err error ("%s: %s", scope, proxyMessage (err)); end_try_catch hasQ = cellfun (@(m) ! isempty (m.Quality), this.members_); if (! isempty (code) && ! any (hasQ)) error ("%s: CODE is given, but no member has quality codes.", scope); endif for k = 1:numel (this.members_) try if (hasQ(k)) this.members_{k} = resample (this.members_{k}, varargin{1}, ... method, code); else this.members_{k} = resample (this.members_{k}, varargin{1}, ... method); endif catch err error ("%s: member '%s': %s", scope, this.names_{k}, ... bareMessage (err)); end_try_catch endfor this.time_ = p.Time; this.timeInfo_.TimeVector = this.time_; endfunction endmethods ################################################################################ ## ** Indexing and concatenation ** ## ################################################################################ ## Available Methods ## ## ## ## 'subsref' 'subsasgn' 'end' ## ## 'horzcat' 'vertcat' ## ## ## ################################################################################ methods (Access = public) ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{tc} =} vertcat (@var{tc1}, @var{tc2}, @dots{}) ## ## Join collections end to end in time. ## ## @code{@var{tc} = [@var{tc1}; @var{tc2}; @dots{}]} returns one collection ## holding the samples of every argument in turn. The collections must ## have the same members, by name, taken in the order of the first, with a ## warning when another orders them differently, as in MATLAB. Each must ## start at or after the end of the one before, so equal times where two ## meet give a repeated time. Each member is joined as ## @code{timeseries.append} joins series: times in the coarsest units among ## them, dates counted from the first start date, and samples of one size ## and one class, as in MATLAB. ## ## A member's quality codes are joined when it has them in every ## collection, and refused when only some do; MATLAB then gives a member ## that cannot be read, or drops them. The result keeps the name of the ## first collection, where MATLAB names it @qcode{'unnamed'}. ## ## @seealso{tscollection.horzcat, timeseries.append} ## @end deftypefn function out = vertcat (varargin) scope = 'tscollection.vertcat'; if (! all (cellfun (@(x) isa (x, 'tscollection'), varargin))) error ("%s: every argument must be a tscollection.", scope); endif out = varargin{1}; if (nargin == 1) return; endif names = out.names_; for i = 2:nargin other = varargin{i}.names_; if (numel (other) != numel (names) || ! isempty (setdiff (other, names))) error ("%s: every collection must have the same members.", scope); endif if (! isequal (other, names)) warning (strcat ("%s: the members are taken in the order of the", ... " first collection."), scope); endif endfor ## The new time vector, from series of sample positions proxies = cellfun (@proxy, varargin, 'UniformOutput', false); try p = append (proxies{:}); catch err error ("%s: %s", scope, collectionMessage (err)); end_try_catch for k = 1:numel (names) list = cell (1, nargin); for i = 1:nargin c = varargin{i}; list{i} = c.members_{strcmp (names{k}, c.names_)}; endfor full = list(cellfun (@(m) m.Length > 0, list)); sizes = cellfun (@getdatasamplesize, full, 'UniformOutput', false); if (! all (cellfun (@(s) isequal (s, sizes{1}), sizes))) error ("%s: the samples of '%s' must be of one size throughout.", ... scope, names{k}); endif classes = cellfun (@(m) class (m.Data), full, 'UniformOutput', false); if (! isempty (classes) && ! all (strcmp (classes, classes{1}))) error ("%s: the data of '%s' must be of one class throughout.", ... scope, names{k}); endif hasQ = cellfun (@(m) ! isempty (m.Quality), full); if (any (hasQ) && ! all (hasQ)) error (strcat ("%s: '%s' must have quality codes in every", ... " collection or in none."), scope, names{k}); endif m = append (list{:}); m.Name = names{k}; out.members_{k} = m; endfor out.time_ = p.Time; out.timeInfo_ = p.TimeInfo; endfunction ## -*- texinfo -*- ## @deftypefn {tscollection} {@var{tc} =} horzcat (@var{tc1}, @var{tc2}, @dots{}) ## ## Join the members of collections. ## ## @code{@var{tc} = [@var{tc1}, @var{tc2}, @dots{}]} returns one collection ## holding the members of every argument, sorted by name, as in MATLAB, ## capitals before lower case letters. The collections must have the ## same time vector, in the same units, and the same ## @qcode{TimeInfo.StartDate}. A member name present in more than one is ## taken once when the members are equal in every property, @code{NaN} ## equal to itself, and refused otherwise; MATLAB compares their data ## alone. Names differing only in case are refused. The result keeps ## the name of the first collection, where MATLAB names it ## @qcode{'unnamed'}. ## ## @seealso{tscollection.vertcat, tscollection.addts} ## @end deftypefn function out = horzcat (varargin) scope = 'tscollection.horzcat'; if (! all (cellfun (@(x) isa (x, 'tscollection'), varargin))) error ("%s: every argument must be a tscollection.", scope); endif out = varargin{1}; for i = 2:nargin c = varargin{i}; if (! (isequal (c.time_, out.time_) && sameTimeInfo (c.timeInfo_, out.timeInfo_, false))) error ("%s: every collection must have the same time vector.", scope); endif for k = 1:numel (c.names_) name = c.names_{k}; j = find (strcmp (name, out.names_)); if (! isempty (j)) if (! isequalwithequalnans (out.members_{j}, c.members_{k})) error (strcat ("%s: the member '%s' shared by the", ... " collections must be the same in each."), ... scope, name); endif continue; endif errmsg = nameClash (name, out.names_); if (! isempty (errmsg)) error ("%s: %s", scope, errmsg); endif m = c.members_{k}; m.TimeInfo = out.timeInfo_; out.names_{end+1} = name; out.members_{end+1} = m; endfor endfor ## MATLAB sorts the members of the result by name if (nargin > 1) [out.names_, idx] = sort (out.names_); out.members_ = out.members_(idx); endif endfunction endmethods methods (Hidden) function last_index = end (this, end_dim, ndim_obj) if (end_dim == 2) last_index = numel (this.names_); elseif (ndim_obj > 2 && end_dim > 2) last_index = 1; else last_index = numel (this.time_); endif endfunction function varargout = subsref (this, s) scope = 'tscollection.subsref'; switch (s(1).type) case '()' out = indexed (this, s(1).subs, scope); case '{}' error ("%s: '{}' indexing is not supported.", scope); case '.' name = fieldName (s(1).subs, scope); out = fieldValue (this, name); if (isempty (out) && ! any (strcmp (name, fieldnames (this)))) error ("%s: TC has no property or member named '%s'.", ... scope, name); endif endswitch if (numel (s) > 1) out = subsref (out, s(2:end)); endif varargout{1} = out; endfunction function this = subsasgn (this, s, val) scope = 'tscollection.subsasgn'; if (! strcmp (s(1).type, '.')) error (strcat ("%s: only '.' assignment is supported; select", ... " samples and members with '()' and assign the", ... " result."), scope); endif name = fieldName (s(1).subs, scope); chain = s(2:end); switch (name) case 'Name' if (! isempty (chain)) val = subsasgn (this.name_, chain, val); endif if (! isText (val)) error ("%s: 'Name' must be a character vector.", scope); endif this.name_ = char (val); case 'Time' if (! isempty (chain)) val = subsasgn (this.time_, chain, val); endif this = setTime (this, val, scope); case 'TimeInfo' if (! isempty (chain)) val = subsasgn (this.timeInfo_, chain, val); endif if (! (isa (val, 'tsdata.timemetadata') && isscalar (val))) error (strcat ("%s: 'TimeInfo' must be a tsdata.timemetadata", ... " object."), scope); endif val.TimeVector = this.time_; this.timeInfo_ = val; for k = 1:numel (this.members_) this.members_{k}.TimeInfo = val; endfor case 'Length' error ("%s: 'Length' is read-only.", scope); otherwise k = find (strcmp (name, this.names_)); if (isempty (k)) if (! isempty (chain)) error ("%s: TC has no member named '%s'.", scope, name); endif if (! (isa (val, 'timeseries') && isscalar (val))) error (strcat ("%s: a member must be a timeseries; remove", ... " one with removets."), scope); endif if (! isvarname (name)) error (strcat ("%s: a member name must be a valid variable", ... " name: '%s'"), scope, name); endif val.Name = name; try this = addts (this, val); catch err error ("%s: %s", scope, bareMessage (err)); end_try_catch return; endif m = this.members_{k}; if (! isempty (chain)) if (strcmp (chain(1).type, '.') && ischar (chain(1).subs)) switch (chain(1).subs) case {'Time', 'TimeInfo'} error (strcat ("%s: set the time of every member with", ... " TC.%s."), scope, chain(1).subs); case 'Name' error ("%s: rename a member with settimeseriesnames.", ... scope); endswitch endif try val = subsasgn (m, chain, val); catch err error ("%s: member '%s': %s", scope, name, bareMessage (err)); end_try_catch endif if (! (isa (val, 'timeseries') && isscalar (val))) error (strcat ("%s: a member must be a timeseries; remove one", ... " with removets."), scope); endif if (! (isequal (val.Time, this.time_) && sameTimeInfo (val.TimeInfo, this.timeInfo_, false))) error (strcat ("%s: the time vector of '%s' must match that of", ... " TC."), scope, name); endif val.Name = name; val.TimeInfo = this.timeInfo_; this.members_{k} = val; endswitch endfunction endmethods methods (Access = private) ## Add the series in LIST, a cell array, as members of this collection, ## whose time vector they must share. Returns an empty ERRMSG, or the ## body of the message the caller raises. function [this, errmsg] = addMembers (this, list) errmsg = ''; for i = 1:numel (list) ts = list{i}; key = matlab.lang.makeValidName (ts.Name); ti = ts.TimeInfo; if (! isempty (ti.StartDate) && isempty (this.timeInfo_.StartDate)) warning (strcat ("tscollection: the start date of '%s' is", ... " dropped, since the collection has none."), key); ti.StartDate = ''; endif if (! (isequal (ts.Time, this.time_) && sameTimeInfo (ti, this.timeInfo_, false))) errmsg = sprintf (strcat ("the time vector of '%s' must match", ... " that of the collection."), key); return; endif errmsg = nameClash (key, this.names_); if (! isempty (errmsg)) return; endif ts.Name = key; ts.TimeInfo = this.timeInfo_; this.names_{end+1} = key; this.members_{end+1} = ts; endfor endfunction ## The value of the property or member NAME, or [] for neither. function val = fieldValue (this, name) switch (name) case 'Name' val = this.name_; case 'Time' val = this.time_; case 'TimeInfo' val = this.timeInfo_; case 'Length' val = numel (this.time_); otherwise k = find (strcmp (name, this.names_)); if (isempty (k)) val = []; else val = this.members_{k}; endif endswitch endfunction ## A series of the positions 1 to N of the samples, on this collection's ## time vector and TimeInfo, through which the 'timeseries' methods read ## times and select samples for every member alike. function p = proxy (this) n = numel (this.time_); p = timeseries ((1:n)', this.time_); p.TimeInfo = this.timeInfo_; endfunction ## The collection holding only the samples IDX, in time order. function this = subsetSamples (this, idx) idx = idx(:); this.time_ = this.time_(idx); this.timeInfo_.TimeVector = this.time_; for k = 1:numel (this.members_) this.members_{k} = getsamples (this.members_{k}, idx); endfor endfunction ## Assign the time vector TIME to the collection and every member. function this = setTime (this, time, scope) if (! (isnumeric (time) && isreal (time) && (isvector (time) || isempty (time)))) error ("%s: 'Time' must be a numeric vector.", scope); endif time = double (time(:)); if (! all (isfinite (time))) error ("%s: 'Time' must be finite.", scope); endif if (any (diff (time) < 0)) error ("%s: 'Time' must be non-decreasing.", scope); endif if (numel (time) != numel (this.time_)) error ("%s: 'Time' must have one time per sample.", scope); endif this.time_ = time; this.timeInfo_.TimeVector = time; for k = 1:numel (this.members_) this.members_{k}.Time = time; endfor endfunction ## The collection '()' indexing selects with the subscripts SUBS: ## samples, then members by position or name. function this = indexed (this, subs, scope) if (numel (subs) < 1 || numel (subs) > 2) error ("%s: '()' indexing takes one or two subscripts.", scope); endif n = numel (this.time_); ind = subs{1}; if (ischar (ind) && strcmp (ind, ':')) idx = (1:n)'; elseif (islogical (ind)) if (! (isvector (ind) || isempty (ind)) || numel (ind) > n) error (strcat ("%s: a logical sample index must not be longer", ... " than TC."), scope); endif idx = find (ind(:)); elseif (isnumeric (ind) && isreal (ind)) idx = double (ind(:)); if (! all (idx == fix (idx) & idx >= 1 & idx <= n)) error (strcat ("%s: sample indices must be positive integers not", ... " exceeding the number of samples."), scope); endif idx = unique (idx); else error (strcat ("%s: sample indices must be positive integers, a", ... " logical mask or ':'."), scope); endif this = subsetSamples (this, idx); if (numel (subs) < 2) return; endif ind = subs{2}; nm = numel (this.names_); if (ischar (ind) && strcmp (ind, ':')) return; elseif (isnumeric (ind) && isreal (ind)) k = double (ind(:))'; if (! all (k == fix (k) & k >= 1 & k <= nm)) error (strcat ("%s: member indices must be positive integers not", ... " exceeding the number of members."), scope); endif elseif (islogical (ind)) if (! (isvector (ind) || isempty (ind)) || numel (ind) > nm) error (strcat ("%s: a logical member index must not be longer", ... " than the number of members."), scope); endif k = find (ind(:))'; else [names, errmsg] = nameList (ind); if (! isempty (errmsg)) error (strcat ("%s: members must be given by position, by name", ... " or as ':'."), scope); endif k = zeros (1, numel (names)); for i = 1:numel (names) j = find (strcmp (names{i}, this.names_)); if (isempty (j)) error ("%s: TC has no member named '%s'.", scope, names{i}); endif k(i) = j; endfor endif k = unique (k, 'stable'); this.names_ = this.names_(k); this.members_ = this.members_(k); endfunction endmethods ## Property access, for the texinfo and 'properties' of the class; dot ## access from outside goes through 'subsref' methods function val = get.Name (this) val = this.name_; endfunction function val = get.Time (this) val = this.time_; endfunction function val = get.TimeInfo (this) val = this.timeInfo_; endfunction function val = get.Length (this) val = numel (this.time_); endfunction endmethods endclassdef ## True for a character vector (or '') and for a string scalar. function tf = isText (x) tf = (ischar (x) && (isrow (x) || isempty (x))) ... || (isa (x, 'string') && isscalar (x)); endfunction ## Read a '.' subscript: a character vector or a string scalar. function name = fieldName (subs, scope) if (isstring (subs) && isscalar (subs)) subs = char (subs); endif if (! (ischar (subs) && isrow (subs))) error (strcat ("%s: '.' index argument must be a character vector or a", ... " string scalar."), scope); endif name = subs; endfunction ## Read member names given as a character vector, a cell array of character ## vectors or a string array. Returns a cellstr and an empty ERRMSG, or the ## rest of the message the caller raises after naming them. function [names, errmsg] = nameList (names) errmsg = ''; if (isa (names, 'string')) names = cellstr (names); elseif (ischar (names) && (isrow (names) || isempty (names))) names = {names}; endif if (! iscellstr (names)) names = {}; errmsg = strcat ("must be a character vector, a cell array of", ... " character vectors or a string array."); return; endif names = names(:)'; endfunction ## The message a new member name NAME raises beside the names OTHERS, or '' ## when it may join them. function errmsg = nameClash (name, others) errmsg = ''; if (any (strcmp (name, {'Name', 'Time', 'TimeInfo', 'Length'}))) errmsg = sprintf ("a member cannot be named '%s', a property of TC.", name); elseif (any (strcmpi (name, others))) errmsg = sprintf (strcat ("the member name '%s' conflicts with the", ... " name of an existing member, ignoring", ... " case."), name); endif endfunction ## True when the time metadata A and B agree in units and start date, and, ## where FORMAT is true, in format and user data. function tf = sameTimeInfo (a, b, format) tf = strcmp (a.Units, b.Units) && strcmp (a.StartDate, b.StartDate); if (format) tf = tf && strcmp (a.Format, b.Format) && isequal (a.UserData, b.UserData); endif endfunction ## The message of ERR without the name of the function that raised it. function msg = bareMessage (err) msg = regexprep (err.message, '^[\w.]+: ', ''); endfunction ## The message a 'timeseries' method raised on the proxy of a collection, as ## it applies to the collection. function msg = proxyMessage (err) msg = bareMessage (err); msg = regexprep (msg, '\', 'TC'); msg = regexprep (msg, '\', 'a collection'); endfunction ## The message 'append' raised over the time vectors of the collections, as ## it applies to them. function msg = collectionMessage (err) msg = bareMessage (err); msg = strrep (msg, 'series with and without a start date cannot be', ... 'collections with and without a start date cannot be'); msg = strrep (msg, 'appended', 'concatenated'); msg = strrep (msg, 'each series', 'each collection'); endfunction ## N samples of member M, whose samples are of size SS, filled with NaN, or ## with 0 for integer data and false for logical data. function d = fillSamples (m, ss, n) data = m.Data; if (isempty (ss)) ## A member with no samples keeps the shape of its empty data sz = size (data); if (isequal (sz, [0, 0])) ss = [1, 1]; elseif (m.IsTimeFirst) ss = [1, sz(2:end)]; else ss = sz(1:end-1); endif endif if (m.IsTimeFirst) sz = [n, ss(2:end)]; else sz = [ss, n]; endif if (islogical (data)) d = false (sz); elseif (isinteger (data)) d = zeros (sz, class (data)); elseif (isa (data, 'single')) d = NaN (sz, 'single'); else d = NaN (sz); endif endfunction ## 'pkg test' reads the BISTs from the file it is testing, so a class ## whose suite lives in inst/tests/ reads as carrying none. This block ## answers for it and checks nothing itself. %!test %! ## The suite for this class is inst/tests/tscollection.m-tst. pr0m1th3as-datatypes-9450b29/inst/tzdata/000077500000000000000000000000001527370255000201425ustar00rootroot00000000000000pr0m1th3as-datatypes-9450b29/inst/tzdata/LICENSE000066400000000000000000000003741527370255000211530ustar00rootroot00000000000000Unless specified below, all files in the tz code and data (including this LICENSE file) are in the public domain. If the files date.c, newstrftime.3, and strftime.c are present, they contain material derived from BSD and use the BSD 3-clause license. pr0m1th3as-datatypes-9450b29/inst/tzdata/africa000066400000000000000000001616411527370255000213230ustar00rootroot00000000000000# tzdb data for Africa and environs # This file is in the public domain, so clarified as of # 2009-05-17 by Arthur David Olson. # This file is by no means authoritative; if you think you know better, # go ahead and edit the file, and please send any changes to # the public mailing list tz@iana.org for general use in the future. # For more, please see the file CONTRIBUTING in the tz distribution. # From Paul Eggert (2018-05-27): # # Unless otherwise specified, the source for data through 1990 is: # Thomas G. Shanks and Rique Pottenger, The International Atlas (6th edition), # San Diego: ACS Publications, Inc. (2003). # Unfortunately this book contains many errors and cites no sources. # # Many years ago Gwillim Law wrote that a good source # for time zone data was the International Air Transport # Association's Standard Schedules Information Manual (IATA SSIM), # published semiannually. Law sent in several helpful summaries # of the IATA's data after 1990. Except where otherwise noted, # IATA SSIM is the source for entries after 1990. # # Another source occasionally used is Edward W. Whitman, World Time Differences, # Whitman Publishing Co, 2 Niagara Av, Ealing, London (undated), which # I found in the UCLA library. # # For data circa 1899, a common source is: # Milne J. Civil time. Geogr J. 1899 Feb;13(2):173-94. # https://www.jstor.org/stable/1774359 # # For the 1911/1912 establishment of standard time in French possessions, see: # Société Française de Physique, Recueil de constantes physiques (1913), # page 752, 18b. # # European-style abbreviations are commonly used along the Mediterranean. # For sub-Saharan Africa abbreviations were less standardized. # Previous editions of this database used WAT, CAT, SAT, and EAT # for UT +00 through +03, respectively, # but in 1997 Mark R V Murray reported that # 'SAST' is the official abbreviation for +02 in the country of South Africa, # 'CAT' is commonly used for +02 in countries north of South Africa, and # 'WAT' is probably the best name for +01, as the common phrase for # the area that includes Nigeria is "West Africa". # # To summarize, the following abbreviations seemed to have some currency: # +00 GMT Greenwich Mean Time # +02 CAT Central Africa Time # +02 SAST South Africa Standard Time # and Murray suggested the following abbreviation: # +01 WAT West Africa Time # Murray's suggestion seems to have caught on in news reports and the like. # I vaguely recall 'WAT' also being used for -01 in the past but # cannot now come up with solid citations. # # I invented the following abbreviations in the 1990s: # +02 WAST West Africa Summer Time # +03 CAST Central Africa Summer Time # +03 SAST South Africa Summer Time # +03 EAT East Africa Time # 'EAT' seems to have caught on and is in current timestamps, and though # the other abbreviations are rarer and are only in past timestamps, # they are paired with better-attested non-DST abbreviations. # Corrections are welcome. # Algeria # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Algeria 1916 only - Jun 14 23:00s 1:00 S Rule Algeria 1916 1919 - Oct Sun>=1 23:00s 0 - Rule Algeria 1917 only - Mar 24 23:00s 1:00 S Rule Algeria 1918 only - Mar 9 23:00s 1:00 S Rule Algeria 1919 only - Mar 1 23:00s 1:00 S Rule Algeria 1920 only - Feb 14 23:00s 1:00 S Rule Algeria 1920 only - Oct 23 23:00s 0 - Rule Algeria 1921 only - Mar 14 23:00s 1:00 S Rule Algeria 1921 only - Jun 21 23:00s 0 - Rule Algeria 1939 only - Sep 11 23:00s 1:00 S Rule Algeria 1939 only - Nov 19 1:00 0 - Rule Algeria 1944 1945 - Apr Mon>=1 2:00 1:00 S Rule Algeria 1944 only - Oct 8 2:00 0 - Rule Algeria 1945 only - Sep 16 1:00 0 - Rule Algeria 1971 only - Apr 25 23:00s 1:00 S Rule Algeria 1971 only - Sep 26 23:00s 0 - Rule Algeria 1977 only - May 6 0:00 1:00 S Rule Algeria 1977 only - Oct 21 0:00 0 - Rule Algeria 1978 only - Mar 24 1:00 1:00 S Rule Algeria 1978 only - Sep 22 3:00 0 - Rule Algeria 1980 only - Apr 25 0:00 1:00 S Rule Algeria 1980 only - Oct 31 2:00 0 - # See Europe/Paris for PMT-related transitions. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Africa/Algiers 0:12:12 - LMT 1891 Mar 16 0:09:21 - PMT 1911 Mar 11 # Paris Mean Time 0:00 Algeria WE%sT 1940 Feb 25 2:00 1:00 Algeria CE%sT 1946 Oct 7 0:00 - WET 1956 Jan 29 1:00 - CET 1963 Apr 14 0:00 Algeria WE%sT 1977 Oct 21 1:00 Algeria CE%sT 1979 Oct 26 0:00 Algeria WE%sT 1981 May 1:00 - CET # Cape Verde / Cabo Verde # # From Tim Parenti (2024-07-01), per Paul Eggert (2018-02-16): # For timestamps before independence, see commentary for Europe/Lisbon. # Shanks gives 1907 instead for the transition to -02. # # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Atlantic/Cape_Verde -1:34:04 - LMT 1912 Jan 01 2:00u # Praia -2:00 - %z 1942 Sep -2:00 1:00 %z 1945 Oct 15 -2:00 - %z 1975 Nov 25 2:00 -1:00 - %z # Chad # Fort-Lamy was renamed to N’Djamena on 1973-04-06. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Africa/Ndjamena 1:00:12 - LMT 1912 Jan 1 # Fort-Lamy 1:00 - WAT 1979 Oct 14 1:00 1:00 WAST 1980 Mar 8 1:00 - WAT # Burkina Faso # Côte d'Ivoire (Ivory Coast) # The Gambia # Ghana # Guinea # Iceland # Mali # Mauritania # St Helena # Senegal # Sierra Leone # Togo # The other parts of the St Helena territory are similar: # Tristan da Cunha: on GMT, say Whitman and the CIA # Ascension: on GMT, say the USNO (1995-12-21) and the CIA # Gough (scientific station since 1955; sealers wintered previously): # on GMT, says the CIA # Inaccessible, Nightingale: uninhabited # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Africa/Abidjan -0:16:08 - LMT 1912 Jan 1 0:00 - GMT ############################################################################### # Egypt # Milne says Cairo used 2:05:08.9, the local mean time of the Abbasizeh # observatory. Milne also says that the official time for # Egypt was mean noon at the Great Pyramid, 2:04:30.5, but apparently this # did not apply to Cairo, Alexandria, or Port Said. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Egypt 1940 only - Jul 15 0:00 1:00 S Rule Egypt 1940 only - Oct 1 0:00 0 - Rule Egypt 1941 only - Apr 15 0:00 1:00 S Rule Egypt 1941 only - Sep 16 0:00 0 - Rule Egypt 1942 1944 - Apr 1 0:00 1:00 S Rule Egypt 1942 only - Oct 27 0:00 0 - Rule Egypt 1943 1945 - Nov 1 0:00 0 - Rule Egypt 1945 only - Apr 16 0:00 1:00 S Rule Egypt 1957 only - May 10 0:00 1:00 S Rule Egypt 1957 1958 - Oct 1 0:00 0 - Rule Egypt 1958 only - May 1 0:00 1:00 S Rule Egypt 1959 1981 - May 1 1:00 1:00 S Rule Egypt 1959 1965 - Sep 30 3:00 0 - Rule Egypt 1966 1994 - Oct 1 3:00 0 - Rule Egypt 1982 only - Jul 25 1:00 1:00 S Rule Egypt 1983 only - Jul 12 1:00 1:00 S Rule Egypt 1984 1988 - May 1 1:00 1:00 S Rule Egypt 1989 only - May 6 1:00 1:00 S Rule Egypt 1990 1994 - May 1 1:00 1:00 S # IATA (after 1990) says transitions are at 0:00. # Go with IATA starting in 1995, except correct 1995 entry from 09-30 to 09-29. # From Alexander Krivenyshev (2011-04-20): # "...Egypt's interim cabinet decided on Wednesday to cancel daylight # saving time after a poll posted on its website showed the majority of # Egyptians would approve the cancellation." # # Egypt to cancel daylight saving time # http://www.almasryalyoum.com/en/node/407168 # or # http://www.worldtimezone.com/dst_news/dst_news_egypt04.html Rule Egypt 1995 2010 - Apr lastFri 0:00s 1:00 S Rule Egypt 1995 2005 - Sep lastThu 24:00 0 - # From Steffen Thorsen (2006-09-19): # The Egyptian Gazette, issue 41,090 (2006-09-18), page 1, reports: # Egypt will turn back clocks by one hour at the midnight of Thursday # after observing the daylight saving time since May. # http://news.gom.com.eg/gazette/pdf/2006/09/18/01.pdf Rule Egypt 2006 only - Sep 21 24:00 0 - # From Dirk Losch (2007-08-14): # I received a mail from an airline which says that the daylight # saving time in Egypt will end in the night of 2007-09-06 to 2007-09-07. # From Jesper Nørgaard Welen (2007-08-15): [The following agree:] # http://www.nentjes.info/Bill/bill5.htm # https://www.timeanddate.com/worldclock/city.html?n=53 # From Steffen Thorsen (2007-09-04): The official information...: # http://www.sis.gov.eg/En/EgyptOnline/Miscellaneous/000002/0207000000000000001580.htm Rule Egypt 2007 only - Sep Thu>=1 24:00 0 - # From Abdelrahman Hassan (2007-09-06): # Due to the Hijri (lunar Islamic calendar) year being 11 days shorter # than the year of the Gregorian calendar, Ramadan shifts earlier each # year. This year it will be observed September 13 (September is quite # hot in Egypt), and the idea is to make fasting easier for workers by # shifting business hours one hour out of daytime heat. Consequently, # unless discontinued, next DST may end Thursday 28 August 2008. # From Paul Eggert (2007-08-17): # For lack of better info, assume the new rule is last Thursday in August. # From Petr Machata (2009-04-06): # The following appeared in Red Hat bugzilla[1] (edited): # # > $ zdump -v /usr/share/zoneinfo/Africa/Cairo | grep 2009 # > /usr/share/zoneinfo/Africa/Cairo Thu Apr 23 21:59:59 2009 UTC = Thu = # Apr 23 # > 23:59:59 2009 EET isdst=0 gmtoff=7200 # > /usr/share/zoneinfo/Africa/Cairo Thu Apr 23 22:00:00 2009 UTC = Fri = # Apr 24 # > 01:00:00 2009 EEST isdst=1 gmtoff=10800 # > /usr/share/zoneinfo/Africa/Cairo Thu Aug 27 20:59:59 2009 UTC = Thu = # Aug 27 # > 23:59:59 2009 EEST isdst=1 gmtoff=10800 # > /usr/share/zoneinfo/Africa/Cairo Thu Aug 27 21:00:00 2009 UTC = Thu = # Aug 27 # > 23:00:00 2009 EET isdst=0 gmtoff=7200 # # > end date should be Thu Sep 24 2009 (Last Thursday in September at 23:59= # :59) # > http://support.microsoft.com/kb/958729/ # # timeanddate[2] and another site I've found[3] also support that. # # [1] https://bugzilla.redhat.com/show_bug.cgi?id=492263 # [2] https://www.timeanddate.com/worldclock/clockchange.html?n=53 # [3] https://wwp.greenwichmeantime.com/time-zone/africa/egypt/ # From Arthur David Olson (2009-04-20): # In 2009 (and for the next several years), Ramadan ends before the fourth # Thursday in September; Egypt is expected to revert to the last Thursday # in September. # From Steffen Thorsen (2009-08-11): # We have been able to confirm the August change with the Egyptian Cabinet # Information and Decision Support Center: # https://www.timeanddate.com/news/time/egypt-dst-ends-2009.html # # The Middle East News Agency # https://www.mena.org.eg/index.aspx # also reports "Egypt starts winter time on August 21" # today in article numbered "71, 11/08/2009 12:25 GMT." # Only the title above is available without a subscription to their service, # and can be found by searching for "winter" in their search engine # (at least today). # From Alexander Krivenyshev (2010-07-20): # According to News from Egypt - Al-Masry Al-Youm Egypt's cabinet has # decided that Daylight Saving Time will not be used in Egypt during # Ramadan. # # Arabic translation: # "Clocks to go back during Ramadan - and then forward again" # http://www.almasryalyoum.com/en/news/clocks-go-back-during-ramadan-and-then-forward-again # http://www.worldtimezone.com/dst_news/dst_news_egypt02.html # From Ahmad El-Dardiry (2014-05-07): # Egypt is to change back to Daylight system on May 15 # http://english.ahram.org.eg/NewsContent/1/64/100735/Egypt/Politics-/Egypts-government-to-reapply-daylight-saving-time-.aspx # From Gunther Vermier (2014-05-13): # our Egypt office confirms that the change will be at 15 May "midnight" (24:00) # From Imed Chihi (2014-06-04): # We have finally "located" a precise official reference about the DST changes # in Egypt. The Ministers Cabinet decision is explained at # http://www.cabinet.gov.eg/Media/CabinetMeetingsDetails.aspx?id=347 ... # [T]his (Arabic) site is not accessible outside Egypt, but the page ... # translates into: "With regard to daylight saving time, it is scheduled to # take effect at exactly twelve o'clock this evening, Thursday, 15 MAY 2014, # to be suspended by twelve o'clock on the evening of Thursday, 26 JUN 2014, # and re-established again at the end of the month of Ramadan, at twelve # o'clock on the evening of Thursday, 31 JUL 2014." This statement has been # reproduced by other (more accessible) sites[, e.g.,]... # http://elgornal.net/news/news.aspx?id=4699258 # From Steffen Thorsen (2015-04-08): # Egypt will start DST on midnight after Thursday, April 30, 2015. # This is based on a law (no 35) from May 15, 2014 saying it starts the last # Thursday of April.... Clocks will still be turned back for Ramadan, but # dates not yet announced.... # http://almogaz.com/news/weird-news/2015/04/05/1947105 ... # https://www.timeanddate.com/news/time/egypt-starts-dst-2015.html # From Ahmed Nazmy (2015-04-20): # Egypt's ministers cabinet just announced ... that it will cancel DST at # least for 2015. # # From Tim Parenti (2015-04-20): # http://english.ahram.org.eg/WriterArticles/NewsContentP/1/128195/Egypt/No-daylight-saving-this-summer-Egypts-prime-minist.aspx # "Egypt's cabinet agreed on Monday not to switch clocks for daylight saving # time this summer, and carry out studies on the possibility of canceling the # practice altogether in future years." # # From Paul Eggert (2015-04-24): # Yesterday the office of Egyptian President El-Sisi announced his # decision to abandon DST permanently. See Ahram Online 2015-04-24. # http://english.ahram.org.eg/NewsContent/1/64/128509/Egypt/Politics-/Sisi-cancels-daylight-saving-time-in-Egypt.aspx # From Steffen Thorsen (2016-04-29): # Egypt will have DST from July 7 until the end of October.... # http://english.ahram.org.eg/NewsContentP/1/204655/Egypt/Daylight-savings-time-returning-to-Egypt-on--July.aspx # From Mina Samuel (2016-07-04): # Egyptian government took the decision to cancel the DST, # From Ahmad ElDardiry (2023-03-01): # Egypt officially announced today that daylight savings will be # applied from last Friday of April to last Thursday of October. # From Paul Eggert (2023-03-01): # Assume transitions are at 00:00 and 24:00 respectively. # From Amir Adib (2023-03-07): # https://www.facebook.com/EgyptianCabinet/posts/638829614954129/ Rule Egypt 2008 only - Aug lastThu 24:00 0 - Rule Egypt 2009 only - Aug 20 24:00 0 - Rule Egypt 2010 only - Aug 10 24:00 0 - Rule Egypt 2010 only - Sep 9 24:00 1:00 S Rule Egypt 2010 only - Sep lastThu 24:00 0 - Rule Egypt 2014 only - May 15 24:00 1:00 S Rule Egypt 2014 only - Jun 26 24:00 0 - Rule Egypt 2014 only - Jul 31 24:00 1:00 S Rule Egypt 2014 only - Sep lastThu 24:00 0 - Rule Egypt 2023 max - Apr lastFri 0:00 1:00 S Rule Egypt 2023 max - Oct lastThu 24:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] #STDOFF 2:05:08.9 Zone Africa/Cairo 2:05:09 - LMT 1900 Oct 2:00 Egypt EE%sT # Guinea-Bissau # # From Tim Parenti (2024-07-01), per Paul Eggert (2018-02-16): # For timestamps before independence, see commentary for Europe/Lisbon. # # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Africa/Bissau -1:02:20 - LMT 1912 Jan 1 1:00u -1:00 - %z 1975 0:00 - GMT # Comoros # Djibouti # Eritrea # Ethiopia # Kenya # Madagascar # Mayotte # Somalia # Tanzania # Uganda # From P Chan (2020-10-24): # # The standard time of GMT+2:30 was adopted in the East Africa Protectorate.... # [The Official Gazette, 1908-05-01, p 274] # https://books.google.com/books?id=e-cAC-sjPSEC&pg=PA274 # # At midnight on 30 June 1928 the clocks throughout Kenya was put forward # half an hour by the Alteration of Time Ordinance, 1928. # https://gazettes.africa/archive/ke/1928/ke-government-gazette-dated-1928-05-11-no-28.pdf # [Ordinance No. 11 of 1928, The Official Gazette, 1928-06-26, p 813] # https://books.google.com/books?id=2S0S6os32ZUC&pg=PA813 # # The 1928 ordinance was repealed by the Alteration of Time (repeal) Ordinance, # 1929 and the time was restored to GMT+2:30 at midnight on 4 January 1930. # [Ordinance No. 97 of 1929, The Official Gazette, 1929-12-31, p 2701] # https://books.google.com/books?id=_g18jIZQlwwC&pg=PA2701 # # The Alteration of Time Ordinance, 1936 changed the time to GMT+2:45 # and repealed the previous ordinance at midnight on 31 December 1936. # [The Official Gazette, 1936-07-21, p 705] # https://books.google.com/books?id=K7j41z0aC5wC&pg=PA705 # # The Defence (Amendment of Laws No. 120) Regulations changed the time # to GMT+3 at midnight on 31 July 1942. # [Kenya Official Gazette Supplement No. 32, 1942-07-21, p 331] # https://books.google.com/books?hl=zh-TW&id=c_E-AQAAIAAJ&pg=PA331 # The provision of the 1936 ordinance was not repealed and was later # incorporated in the Interpretation and General Clauses Ordinance in 1948. # Although it was overridden by the 1942 regulations. # [The Laws of Kenya in force on 1948-09-21, Title I, Chapter 1, 31] # https://dds.crl.edu/item/217517 (p.101) # In 1950 the Interpretation and General Clauses Ordinance was amended to adopt # GMT+3 permanently as the 1942 regulations were due to expire on 10 December. # https://books.google.com/books?id=jvR8mUDAwR0C&pg=PA787 # [Ordinance No. 44 of 1950, Kenya Ordinances 1950, Vol. XXIX, p 294] # https://books.google.com/books?id=-_dQAQAAMAAJ&pg=PA294 # From Paul Eggert (2020-10-24): # The 1908-05-01 announcement does not give an effective date, # so just say "1908 May". # From Paul Eggert (2018-09-11): # Unfortunately tzdb records only Western clock time in use in Ethiopia, # as the tzdb format is not up to properly recording a common Ethiopian # timekeeping practice that is based on solar time. See: # Mortada D. If you have a meeting in Ethiopia, you'd better double # check the time. PRI's The World. 2015-01-30 15:15 -05. # https://www.pri.org/stories/2015-01-30/if-you-have-meeting-ethiopia-you-better-double-check-time # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Africa/Nairobi 2:27:16 - LMT 1908 May 2:30 - %z 1928 Jun 30 24:00 3:00 - EAT 1930 Jan 4 24:00 2:30 - %z 1936 Dec 31 24:00 2:45 - %z 1942 Jul 31 24:00 3:00 - EAT # Liberia # # From Paul Eggert (2017-03-02): # # The Nautical Almanac for the Year 1970, p 264, is the source for -0:44:30. # # In 1972 Liberia was the last country to switch from a UT offset # that was not a multiple of 15 or 20 minutes. The 1972 change was on # 1972-01-07, according to an entry dated 1972-01-04 on p 330 of: # Presidential Papers: First year of the administration of # President William R. Tolbert, Jr., July 23, 1971-July 31, 1972. # Monrovia: Executive Mansion. # # Use the abbreviation "MMT" before 1972, as the more accurate numeric # abbreviation "-004430" would be one byte over the POSIX limit. # # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Africa/Monrovia -0:43:08 - LMT 1882 -0:43:08 - MMT 1919 Mar # Monrovia Mean Time -0:44:30 - MMT 1972 Jan 7 # approximately MMT 0:00 - GMT ############################################################################### # Libya # From Even Scharning (2012-11-10): # Libya set their time one hour back at 02:00 on Saturday November 10. # https://www.libyaherald.com/2012/11/04/clocks-to-go-back-an-hour-on-saturday/ # Here is an official source [in Arabic]: http://ls.ly/fb6Yc # # Steffen Thorsen forwarded a translation (2012-11-10) in # https://mm.icann.org/pipermail/tz/2012-November/018451.html # # From Tim Parenti (2012-11-11): # Treat the 2012-11-10 change as a zone change from UTC+2 to UTC+1. # The DST rules planned for 2013 and onward roughly mirror those of Europe # (either two days before them or five days after them, so as to fall on # lastFri instead of lastSun). # From Even Scharning (2013-10-25): # The scheduled end of DST in Libya on Friday, October 25, 2013 was # cancelled yesterday.... # https://www.libyaherald.com/2013/10/24/correction-no-time-change-tomorrow/ # # From Paul Eggert (2013-10-25): # For now, assume they're reverting to the pre-2012 rules of permanent UT +02. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Libya 1951 only - Oct 14 2:00 1:00 S Rule Libya 1952 only - Jan 1 0:00 0 - Rule Libya 1953 only - Oct 9 2:00 1:00 S Rule Libya 1954 only - Jan 1 0:00 0 - Rule Libya 1955 only - Sep 30 0:00 1:00 S Rule Libya 1956 only - Jan 1 0:00 0 - Rule Libya 1982 1984 - Apr 1 0:00 1:00 S Rule Libya 1982 1985 - Oct 1 0:00 0 - Rule Libya 1985 only - Apr 6 0:00 1:00 S Rule Libya 1986 only - Apr 4 0:00 1:00 S Rule Libya 1986 only - Oct 3 0:00 0 - Rule Libya 1987 1989 - Apr 1 0:00 1:00 S Rule Libya 1987 1989 - Oct 1 0:00 0 - Rule Libya 1997 only - Apr 4 0:00 1:00 S Rule Libya 1997 only - Oct 4 0:00 0 - Rule Libya 2013 only - Mar lastFri 1:00 1:00 S Rule Libya 2013 only - Oct lastFri 2:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Africa/Tripoli 0:52:44 - LMT 1920 1:00 Libya CE%sT 1959 2:00 - EET 1982 1:00 Libya CE%sT 1990 May 4 # The 1996 and 1997 entries are from Shanks & Pottenger; # the IATA SSIM data entries contain some obvious errors. 2:00 - EET 1996 Sep 30 1:00 Libya CE%sT 1997 Oct 4 2:00 - EET 2012 Nov 10 2:00 1:00 Libya CE%sT 2013 Oct 25 2:00 2:00 - EET # Mauritius # From Steffen Thorsen (2008-06-25): # Mauritius plans to observe DST from 2008-11-01 to 2009-03-31 on a trial # basis.... # It seems that Mauritius observed daylight saving time from 1982-10-10 to # 1983-03-20 as well, but that was not successful.... # https://www.timeanddate.com/news/time/mauritius-daylight-saving-time.html # From Alex Krivenyshev (2008-06-25): # http://economicdevelopment.gov.mu/portal/site/Mainhomepage/menuitem.a42b24128104d9845dabddd154508a0c/?content_id=0a7cee8b5d69a110VgnVCM1000000a04a8c0RCRD # From Arthur David Olson (2008-06-30): # The www.timeanddate.com article cited by Steffen Thorsen notes that "A # final decision has yet to be made on the times that daylight saving # would begin and end on these dates." As a place holder, use midnight. # From Paul Eggert (2008-06-30): # Follow Thorsen on DST in 1982/1983, instead of Shanks & Pottenger. # From Steffen Thorsen (2008-07-10): # According to # http://www.lexpress.mu/display_article.php?news_id=111216 # (in French), Mauritius will start and end their DST a few days earlier # than previously announced (2008-11-01 to 2009-03-31). The new start # date is 2008-10-26 at 02:00 and the new end date is 2009-03-27 (no time # given, but it is probably at either 2 or 3 wall clock time). # # A little strange though, since the article says that they moved the date # to align itself with Europe and USA which also change time on that date, # but that means they have not paid attention to what happened in # USA/Canada last year (DST ends first Sunday in November). I also wonder # why that they end on a Friday, instead of aligning with Europe which # changes two days later. # From Alex Krivenyshev (2008-07-11): # Seems that English language article "The revival of daylight saving # time: Energy conservation?"- No. 16578 (07/11/2008) was originally # published on Monday, June 30, 2008... # # I guess that article in French "Le gouvernement avance l'introduction # de l'heure d'été" stating that DST in Mauritius starting on October 26 # and ending on March 27, 2009 is the most recent one.... # http://www.worldtimezone.com/dst_news/dst_news_mauritius02.html # From Riad M. Hossen Ally (2008-08-03): # The Government of Mauritius weblink # http://www.gov.mu/portal/site/pmosite/menuitem.4ca0efdee47462e7440a600248a521ca/?content_id=4728ca68b2a5b110VgnVCM1000000a04a8c0RCRD # Cabinet Decision of July 18th, 2008 states as follows: # # 4. ...Cabinet has agreed to the introduction into the National Assembly # of the Time Bill which provides for the introduction of summer time in # Mauritius. The summer time period which will be of one hour ahead of # the standard time, will be aligned with that in Europe and the United # States of America. It will start at two o'clock in the morning on the # last Sunday of October and will end at two o'clock in the morning on # the last Sunday of March the following year. The summer time for the # year 2008-2009 will, therefore, be effective as from 26 October 2008 # and end on 29 March 2009. # From Ed Maste (2008-10-07): # THE TIME BILL (No. XXVII of 2008) Explanatory Memorandum states the # beginning / ending of summer time is 2 o'clock standard time in the # morning of the last Sunday of October / last Sunday of March. # http://www.gov.mu/portal/goc/assemblysite/file/bill2708.pdf # From Steffen Thorsen (2009-06-05): # According to several sources, Mauritius will not continue to observe # DST the coming summer... # # Some sources, in French: # http://www.defimedia.info/news/946/Rashid-Beebeejaun-:-«-L%E2%80%99heure-d%E2%80%99été-ne-sera-pas-appliquée-cette-année-» # http://lexpress.mu/Story/3398~Beebeejaun---Les-objectifs-d-économie-d-énergie-de-l-heure-d-été-ont-été-atteints- # # Our wrap-up: # https://www.timeanddate.com/news/time/mauritius-dst-will-not-repeat.html # From Arthur David Olson (2009-07-11): # The "mauritius-dst-will-not-repeat" wrapup includes this: # "The trial ended on March 29, 2009, when the clocks moved back by one hour # at 2am (or 02:00) local time..." # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Mauritius 1982 only - Oct 10 0:00 1:00 - Rule Mauritius 1983 only - Mar 21 0:00 0 - Rule Mauritius 2008 only - Oct lastSun 2:00 1:00 - Rule Mauritius 2009 only - Mar lastSun 2:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Indian/Mauritius 3:50:00 - LMT 1907 # Port Louis 4:00 Mauritius %z # Agalega Is, Rodriguez # no information; probably like Indian/Mauritius # Morocco # See Africa/Ceuta for Spanish Morocco. # From Paul Eggert (2026-06-26): # In “Morocco’s GMT+1, a century of shifting time and a debate far from over� # https://en.yabiladi.com/articles/details/191310/morocco-s-gmt1-century-shifting-time # (2026-03-30), Yabiladi’s Latifa Babas reports the following: # * A 1913-10-26 royal dahir established GMT as legal time in Morocco. # * A 1918-05-10 dahir instituted DST on 1918-05-16 at 00:00. # * A September 1939 dahir restarted DST on 1939-09-12. # * A February 1940 dahir restarted DST as early as 1940-02-25. # * Standard time resumed in September 1946. # * A June 1950 decree restarted DST, which ran until as late as October. # * Royal decree 455-67 (1967-06-02) restarted DST on June 3 at noon. # * After 1967 Morocco used DST “during several summers, particularly # throughout the 1970s and 1980s.� # Babas consulted official records that disagree with and are surely # more correct than our pre-2008 timestamp data, which came from the # unreliable Shanks & Pottenger. Unfortunately, Babas did not provide # enough detail to correct our data. # From Alex Krivenyshev (2008-05-09): # Here is an article that Morocco plan to introduce Daylight Saving Time between # 1 June, 2008 and 27 September, 2008. # # "... Morocco is to save energy by adjusting its clock during summer so it will # be one hour ahead of GMT between 1 June and 27 September, according to # Communication Minister and Government Spokesman, Khalid Naciri...." # # http://www.worldtimezone.com/dst_news/dst_news_morocco01.html # http://en.afrik.com/news11892.html # From Alex Krivenyshev (2008-05-09): # The Morocco time change can be confirmed on Morocco web site Maghreb Arabe # Presse: # http://www.map.ma/eng/sections/box3/morocco_shifts_to_da/view # # Morocco shifts to daylight time on June 1st through September 27, Govt. # spokesman. # From Patrice Scattolin (2008-05-09): # According to this article: # https://www.avmaroc.com/actualite/heure-dete-comment-a127896.html # (and republished here: ) # the changes occur at midnight: # # Saturday night May 31st at midnight (which in French is to be # interpreted as the night between Saturday and Sunday) # Sunday night the 28th at midnight # # Seeing that the 28th is Monday, I am guessing that she intends to say # the midnight of the 28th which is the midnight between Sunday and # Monday, which jives with other sources that say that it's inclusive # June 1st to Sept 27th. # # The decision was taken by decree *2-08-224 *but I can't find the decree # published on the web. # # It's also confirmed here: # http://www.maroc.ma/NR/exeres/FACF141F-D910-44B0-B7FA-6E03733425D1.htm # on a government portal as being between June 1st and Sept 27th (not yet # posted in English). # # The following Google query will generate many relevant hits: # https://www.google.com/search?hl=en&q=Conseil+de+gouvernement+maroc+heure+avance&btnG=Search # From Steffen Thorsen (2008-08-27): # Morocco will change the clocks back on the midnight between August 31 # and September 1. They originally planned to observe DST to near the end # of September: # # One article about it (in French): # http://www.menara.ma/fr/Actualites/Maroc/Societe/ci.retour_a_l_heure_gmt_a_partir_du_dimanche_31_aout_a_minuit_officiel_.default # # We have some further details posted here: # https://www.timeanddate.com/news/time/morocco-ends-dst-early-2008.html # From Steffen Thorsen (2009-03-17): # Morocco will observe DST from 2009-06-01 00:00 to 2009-08-21 00:00 according # to many sources, such as # http://news.marweb.com/morocco/entertainment/morocco-daylight-saving.html # http://www.medi1sat.ma/fr/depeche.aspx?idp=2312 # (French) # # Our summary: # https://www.timeanddate.com/news/time/morocco-starts-dst-2009.html # From Alexander Krivenyshev (2009-03-17): # Here is a link to official document from Royaume du Maroc Premier Ministre, # Ministère de la Modernisation des Secteurs Publics # # Under Article 1 of Royal Decree No. 455-67 of Act 23 safar 1387 (2 June 1967) # concerning the amendment of the legal time, the Ministry of Modernization of # Public Sectors announced that the official time in the Kingdom will be # advanced 60 minutes from Sunday 31 May 2009 at midnight. # # http://www.mmsp.gov.ma/francais/Actualites_fr/PDF_Actualites_Fr/HeureEte_FR.pdf # http://www.worldtimezone.com/dst_news/dst_news_morocco03.html # From Steffen Thorsen (2010-04-13): # Several news media in Morocco report that the Ministry of Modernization # of Public Sectors has announced that Morocco will have DST from # 2010-05-02 to 2010-08-08. # # Example: # http://www.lavieeco.com/actualites/4099-le-maroc-passera-a-l-heure-d-ete-gmt1-le-2-mai.html # (French) # Our page: # https://www.timeanddate.com/news/time/morocco-starts-dst-2010.html # From Dan Abitol (2011-03-30): # ...Rules for Africa/Casablanca are the following (24h format) # The 3rd April 2011 at 00:00:00, [it] will be 3rd April 01:00:00 # The 31st July 2011 at 00:59:59, [it] will be 31st July 00:00:00 # ...Official links of change in morocco # The change was broadcast on the FM Radio # I ve called ANRT (telecom regulations in Morocco) at # +212.537.71.84.00 # http://www.anrt.net.ma/fr/ # They said that # http://www.map.ma/fr/sections/accueil/l_heure_legale_au_ma/view # is the official publication to look at. # They said that the decision was already taken. # # More articles in the press # https://www.yabiladi.com/articles/details/5058/secret-l-heure-d-ete-maroc-leve.html # http://www.lematin.ma/Actualite/Express/Article.asp?id=148923 # http://www.lavieeco.com/actualite/Le-Maroc-passe-sur-GMT+1-a-partir-de-dim # From Petr Machata (2011-03-30): # They have it written in English here: # http://www.map.ma/eng/sections/home/morocco_to_spring_fo/view # # It says there that "Morocco will resume its standard time on July 31, # 2011 at midnight." Now they don't say whether they mean midnight of # wall clock time (i.e. 11pm UTC), but that's what I would assume. It has # also been like that in the past. # From Alexander Krivenyshev (2012-03-09): # According to Infomédiaire web site from Morocco (infomediaire.ma), # on March 9, 2012, (in French) Heure légale: # Le Maroc adopte officiellement l'heure d'été # http://www.infomediaire.ma/news/maroc/heure-légale-le-maroc-adopte-officiellement-lheure-dété # Governing Council adopted draft decree, that Morocco DST starts on # the last Sunday of March (March 25, 2012) and ends on # last Sunday of September (September 30, 2012) # except the month of Ramadan. # or (brief) # http://www.worldtimezone.com/dst_news/dst_news_morocco06.html # From Arthur David Olson (2012-03-10): # The infomediaire.ma source indicates that the system is to be in # effect every year. It gives 03H00 as the "fall back" time of day; # it lacks a "spring forward" time of day; assume 2:00 XXX. # Wait on specifying the Ramadan exception for details about # start date, start time of day, end date, and end time of day XXX. # From Christophe Tropamer (2012-03-16): # Seen Morocco change again: # http://www.le2uminutes.com/actualite.php # "...à partir du dernier dimanche d'avril et non fins mars, # comme annoncé précédemment." # From Milamber Space Network (2012-07-17): # The official return to GMT is announced by the Moroccan government: # http://www.mmsp.gov.ma/fr/actualites.aspx?id=288 [in French] # # Google translation, lightly edited: # Back to the standard time of the Kingdom (GMT) # Pursuant to Decree No. 2-12-126 issued on 26 Jumada (I) 1433 (April 18, # 2012) and in accordance with the order of Mr. President of the # Government No. 3-47-12 issued on 24 Sha'ban (11 July 2012), the Ministry # of Public Service and Administration Modernization announces the return # of the legal time of the Kingdom (GMT) from Friday, July 20, 2012 until # Monday, August 20, 2012. So the time will be delayed by 60 minutes from # 3:00 am Friday, July 20, 2012 and will again be advanced by 60 minutes # August 20, 2012 from 2:00 am. # From Paul Eggert (2013-03-06): # Morocco's daylight-saving transitions due to Ramadan seem to be # announced a bit in advance. On 2012-07-11 the Moroccan government # announced that year's Ramadan daylight-saving transitions would be # 2012-07-20 and 2012-08-20; see # http://www.mmsp.gov.ma/fr/actualites.aspx?id=288 # From Andrew Paprocki (2013-07-02): # Morocco announced that the year's Ramadan daylight-savings # transitions would be 2013-07-07 and 2013-08-10; see: # http://www.maroc.ma/en/news/morocco-suspends-daylight-saving-time-july-7-aug10 # From Steffen Thorsen (2013-09-28): # Morocco extends DST by one month, on very short notice, just 1 day # before it was going to end. There is a new decree (2.13.781) for # this, where DST from now on goes from last Sunday of March at 02:00 # to last Sunday of October at 03:00, similar to EU rules. Official # source (French): # http://www.maroc.gov.ma/fr/actualites/lhoraire-dete-gmt1-maintenu-jusquau-27-octobre-2013 # Another source (specifying the time for start and end in the decree): # http://www.lemag.ma/Heure-d-ete-au-Maroc-jusqu-au-27-octobre_a75620.html # From Sebastien Willemijns (2014-03-18): # http://www.afriquinfos.com/articles/2014/3/18/maroc-heure-dete-avancez-tous-horloges-247891.asp # From Milamber Space Network (2014-06-05): # The Moroccan government has recently announced that the country will return # to standard time at 03:00 on Saturday, June 28, 2014 local time.... DST # will resume again at 02:00 on Saturday, August 2, 2014.... # http://www.mmsp.gov.ma/fr/actualites.aspx?id=586 # From Milamber (2015-06-08): # (Google Translation) The hour will thus be delayed 60 minutes # Sunday, June 14 at 3:00, the ministry said in a statement, adding # that the time will be advanced again 60 minutes Sunday, July 19, # 2015 at 2:00. The move comes under 2.12.126 Decree of 26 Jumada I # 1433 (18 April 2012) and the decision of the Head of Government of # 16 N. 3-29-15 Chaaban 1435 (4 June 2015). # Source (french): # https://lnt.ma/le-maroc-reculera-dune-heure-le-dimanche-14-juin/ # # From Milamber (2015-06-09): # http://www.mmsp.gov.ma/fr/actualites.aspx?id=863 # # From Michael Deckers (2015-06-09): # [The gov.ma announcement] would (probably) make the switch on 2015-07-19 go # from 03:00 to 04:00 rather than from 02:00 to 03:00, as in the patch.... # I think the patch is correct and the quoted text is wrong; the text in # agrees # with the patch. # From Mohamed Essedik Najd (2018-10-26): # Today, a Moroccan government council approved the perpetual addition # of 60 minutes to the regular Moroccan timezone. # From Matt Johnson (2018-10-28): # http://www.sgg.gov.ma/Portals/1/BO/2018/BO_6720-bis_Ar.pdf # # From Maamar Abdelkader (2018-11-01): # We usually move clocks back the previous week end and come back to the +1 # the week end after.... The government does not announce yet the decision # about this temporary change. But it s 99% sure that it will be the case, # as in previous years. An unofficial survey was done these days, showing # that 64% of asked people are ok for moving from +1 to +0 during Ramadan. # https://leconomiste.com/article/1035870-enquete-l-economiste-sunergia-64-des-marocains-plebiscitent-le-gmt-pendant-ramadan # From Naoufal Semlali (2019-04-16): # Morocco will be on GMT starting from Sunday, May 5th 2019 at 3am. # The switch to GMT+1 will occur on Sunday, June 9th 2019 at 2am.... # http://fr.le360.ma/societe/voici-la-date-du-retour-a-lheure-legale-au-maroc-188222 # From Semlali Naoufal (2020-04-14): # Following the announcement by the Moroccan government, the switch to # GMT time will take place on Sunday, April 19, 2020 from 3 a.m. and # the return to GMT+1 time will take place on Sunday, May 31, 2020 at 2 a.m.... # https://maroc-diplomatique.net/maroc-le-retour-a-lheure-gmt-est-prevu-dimanche-prochain/ # http://aujourdhui.ma/actualite/gmt1-retour-a-lheure-normale-dimanche-prochain-1 # # From Milamber (2020-05-31) # In Morocco (where I live), the end of Ramadan (Arabic month) is followed by # the Eid al-Fitr, and concretely it's 1 or 2 day offs for the people (with # traditional visiting of family, big lunches/dinners, etc.). So for this # year the astronomical calculations don't include the following 2 days off in # the calc. These 2 days fall in a Sunday/Monday, so it's not acceptable by # people to have a time shift during these 2 days off. Perhaps you can modify # the (predicted) rules for next years: if the end of Ramadan is a (probable) # Friday or Saturday (and so the 2 days off are on a weekend), the next time # shift will be the next weekend. # # From Milamber (2021-03-31, 2022-03-10): # https://www.mmsp.gov.ma/fr/actualites.aspx?id=2076 # https://www.ecoactu.ma/horaires-administration-ramadan-gmtheure-gmt-a-partir-de-dimanche-27-mars/ # # From Milamber (2023-03-14, 2023-03-15): # The return to legal GMT time will take place this Sunday, March 19 at 3 a.m. # ... the return to GMT+1 will be made on Sunday April 23, 2023 at 2 a.m. # https://www.mmsp.gov.ma/fr/actualites/passage-à-l%E2%80%99heure-gmt-à-partir-du-dimanche-19-mars-2023 # From Paul Eggert (2026-06-25): # https://www.moroccoworldnews.com/2026/06/325034/confirmed-morocco-to-restore-gmt-on-september-20-ending-eight-year-gmt1-saga/ # Today the Moroccan government adopted Decree No. 2.26.530, which abrogates # the 2018 decree that put it at +01 with daylight saving during Ramadan. # The plan is to go back to +00 without DST on 2026-09-20 at 02:00. # From Anass Taghjichte (2026-07-03): # https://www.sgg.gov.ma/BO/AR/3111/2026/BO_7521_Ar.pdf # From Afaf EL MAAYATI (2026-07-06): # https://www.mapexpress.ma/actualite/activite-gouvernementale/conseil-gouvernement-approuve-projet-decret-relatif-au-retour-lheure-legale/ # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Morocco 1939 only - Sep 12 0:00 1:00 - Rule Morocco 1939 only - Nov 19 0:00 0 - Rule Morocco 1940 only - Feb 25 0:00 1:00 - Rule Morocco 1945 only - Nov 18 0:00 0 - Rule Morocco 1950 only - Jun 11 0:00 1:00 - Rule Morocco 1950 only - Oct 29 0:00 0 - Rule Morocco 1967 only - Jun 3 12:00 1:00 - Rule Morocco 1967 only - Oct 1 0:00 0 - Rule Morocco 1974 only - Jun 24 0:00 1:00 - Rule Morocco 1974 only - Sep 1 0:00 0 - Rule Morocco 1976 1977 - May 1 0:00 1:00 - Rule Morocco 1976 only - Aug 1 0:00 0 - Rule Morocco 1977 only - Sep 28 0:00 0 - Rule Morocco 1978 only - Jun 1 0:00 1:00 - Rule Morocco 1978 only - Aug 4 0:00 0 - Rule Morocco 2008 only - Jun 1 0:00 1:00 - Rule Morocco 2008 only - Sep 1 0:00 0 - Rule Morocco 2009 only - Jun 1 0:00 1:00 - Rule Morocco 2009 only - Aug 21 0:00 0 - Rule Morocco 2010 only - May 2 0:00 1:00 - Rule Morocco 2010 only - Aug 8 0:00 0 - Rule Morocco 2011 only - Apr 3 0:00 1:00 - Rule Morocco 2011 only - Jul 31 0:00 0 - Rule Morocco 2012 2013 - Apr lastSun 2:00 1:00 - Rule Morocco 2012 only - Jul 20 3:00 0 - Rule Morocco 2012 only - Aug 20 2:00 1:00 - Rule Morocco 2012 only - Sep 30 3:00 0 - Rule Morocco 2013 only - Jul 7 3:00 0 - Rule Morocco 2013 only - Aug 10 2:00 1:00 - Rule Morocco 2013 2018 - Oct lastSun 3:00 0 - Rule Morocco 2014 2018 - Mar lastSun 2:00 1:00 - Rule Morocco 2014 only - Jun 28 3:00 0 - Rule Morocco 2014 only - Aug 2 2:00 1:00 - Rule Morocco 2015 only - Jun 14 3:00 0 - Rule Morocco 2015 only - Jul 19 2:00 1:00 - Rule Morocco 2016 only - Jun 5 3:00 0 - Rule Morocco 2016 only - Jul 10 2:00 1:00 - Rule Morocco 2017 only - May 21 3:00 0 - Rule Morocco 2017 only - Jul 2 2:00 1:00 - Rule Morocco 2018 only - May 13 3:00 0 - Rule Morocco 2018 only - Jun 17 2:00 1:00 - Rule Morocco 2019 only - May 5 3:00 -1:00 - Rule Morocco 2019 only - Jun 9 2:00 0 - Rule Morocco 2020 only - Apr 19 3:00 -1:00 - Rule Morocco 2020 only - May 31 2:00 0 - Rule Morocco 2021 only - Apr 11 3:00 -1:00 - Rule Morocco 2021 only - May 16 2:00 0 - Rule Morocco 2022 only - Mar 27 3:00 -1:00 - Rule Morocco 2022 only - May 8 2:00 0 - Rule Morocco 2023 only - Mar 19 3:00 -1:00 - Rule Morocco 2023 only - Apr 23 2:00 0 - Rule Morocco 2024 only - Mar 10 3:00 -1:00 - Rule Morocco 2024 only - Apr 14 2:00 0 - Rule Morocco 2025 only - Feb 23 3:00 -1:00 - Rule Morocco 2025 only - Apr 6 2:00 0 - Rule Morocco 2026 only - Feb 15 3:00 -1:00 - Rule Morocco 2026 only - Mar 22 2:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Africa/Casablanca -0:30:20 - LMT 1913 Oct 26 0:00 Morocco %z 1984 Mar 16 1:00 - %z 1986 0:00 Morocco %z 2018 Oct 28 3:00 1:00 Morocco %z 2026 Sep 20 2:00 0:00 - %z # Western Sahara # # From Gwillim Law (2013-10-22): # A correspondent who is usually well informed about time zone matters # ... says that Western Sahara observes daylight saving time, just as # Morocco does. # # From Paul Eggert (2013-10-23): # Assume that this has been true since Western Sahara switched to GMT, # since most of it was then controlled by Morocco. Zone Africa/El_Aaiun -0:52:48 - LMT 1934 Jan # El Aaiún -1:00 - %z 1976 Apr 14 0:00 Morocco %z 2018 Oct 28 3:00 1:00 Morocco %z 2026 Sep 20 2:00 0:00 - %z # Botswana # Burundi # Democratic Republic of the Congo (eastern) # Malawi # Mozambique # Rwanda # Zambia # Zimbabwe # # From Tim Parenti (2024-07-01): # For timestamps before Mozambique's independence, see commentary for # Europe/Lisbon. # # From Paul Eggert (2024-05-24): # The London Gazette, 1903-04-03, page 2245, says that # as of 1903-03-03 a time ball at the port of Lourenço Marques # (as Maputo was then called) was dropped daily at 13:00:00 LMT, # corresponding to 22:49:41.7 GMT, so local time was +02:10:18.3. # Conversely, the newspaper South Africa, 1909-02-09, page 321, # says the port had just installed an apparatus that communicated # "from the controlling clock in the new Observatory at Reuben Point ... # exact mean South African time, i.e., 30 deg., or 2 hours East of Greenwich". # Although Shanks gives 1903-03-01 for the transition to CAT, # evidently the port transitioned to CAT after 1903-03-03 but before # the Portuguese legal transition of 1912-01-01 (see Europe/Lisbon commentary). # For lack of better info, list 1909 as the transition date. # # Zone NAME STDOFF RULES FORMAT [UNTIL] #STDOFF 2:10:18.3 Zone Africa/Maputo 2:10:18 - LMT 1909 2:00 - CAT # Namibia # From Arthur David Olson (2017-08-09): # The text of the "Namibia Time Act, 1994" is available online at # www.lac.org.na/laws/1994/811.pdf # and includes this nugget: # Notwithstanding the provisions of subsection (2) of section 1, the # first winter period after the commencement of this Act shall # commence at OOhOO on Monday 21 March 1994 and shall end at 02h00 on # Sunday 4 September 1994. # From Michael Deckers (2017-04-06): # ... both summer and winter time are called "standard" # (which differs from the use in Ireland) ... # From Petronella Sibeene (2007-03-30): # http://allafrica.com/stories/200703300178.html # While the entire country changes its time, Katima Mulilo and other # settlements in Caprivi unofficially will not because the sun there # rises and sets earlier compared to other regions. Chief of # Forecasting Riaan van Zyl explained that the far eastern parts of # the country are close to 40 minutes earlier in sunrise than the rest # of the country. # # From Paul Eggert (2017-02-22): # Although the Zambezi Region (formerly known as Caprivi) informally # observes Botswana time, we have no details about historical practice. # In the meantime people there can use Africa/Gaborone. # See: Immanuel S. The Namibian. 2017-02-23. # https://www.namibian.com.na/51480/read/Time-change-divides-lawmakers # From Steffen Thorsen (2017-08-09): # Namibia is going to change their time zone to what is now their DST: # https://www.newera.com.na/2017/02/23/namibias-winter-time-might-be-repealed/ # This video is from the government decision: # https://www.nbc.na/news/na-passes-namibia-time-bill-repealing-1994-namibia-time-act.8665 # We have made the assumption so far that they will change their time zone at # the same time they would normally start DST, the first Sunday in September: # https://www.timeanddate.com/news/time/namibia-new-time-zone.html # From Paul Eggert (2017-04-09): # Before the change, summer and winter time were both standard time legally. # However in common parlance, winter time was considered to be DST. See, e.g.: # http://www.nbc.na/news/namibias-winter-time-could-be-scrapped.2706 # https://zone.my.na/news/times-are-changing-in-namibia # https://www.newera.com.na/2017/02/23/namibias-winter-time-might-be-repealed/ # Use plain "WAT" and "CAT" for the time zone abbreviations, to be compatible # with Namibia's neighbors. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S # Vanguard section, for zic and other parsers that support negative DST. Rule Namibia 1994 only - Mar 21 0:00 -1:00 WAT Rule Namibia 1994 2017 - Sep Sun>=1 2:00 0 CAT Rule Namibia 1995 2017 - Apr Sun>=1 2:00 -1:00 WAT # Rearguard section, for parsers lacking negative DST; see ziguard.awk. #Rule Namibia 1994 only - Mar 21 0:00 0 WAT #Rule Namibia 1994 2017 - Sep Sun>=1 2:00 1:00 CAT #Rule Namibia 1995 2017 - Apr Sun>=1 2:00 0 WAT # End of rearguard section. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Africa/Windhoek 1:08:24 - LMT 1892 Feb 8 1:30 - %z 1903 Mar 2:00 - SAST 1942 Sep 20 2:00 2:00 1:00 SAST 1943 Mar 21 2:00 2:00 - SAST 1990 Mar 21 # independence # Vanguard section, for zic and other parsers that support negative DST. 2:00 Namibia %s # Rearguard section, for parsers lacking negative DST; see ziguard.awk. # 2:00 - CAT 1994 Mar 21 0:00 # From Paul Eggert (2017-04-07): # The official date of the 2017 rule change was 2017-10-24. See: # http://www.lac.org.na/laws/annoSTAT/Namibian%20Time%20Act%209%20of%202017.pdf # 1:00 Namibia %s 2017 Oct 24 # 2:00 - CAT # End of rearguard section. # Angola # Benin # Cameroon # Central African Republic # Democratic Republic of the Congo (western) # Republic of the Congo # Equatorial Guinea # Gabon # Niger # Nigeria # From P Chan (2020-12-03): # GMT was adopted as the standard time of Lagos on 1905-07-01. # Lagos Weekly Record, 1905-06-24, p 3 # http://ddsnext.crl.edu/titles/31558#?c=0&m=668&s=0&cv=2&r=0&xywh=1446,5221,1931,1235 # says "It is officially notified that on and after the 1st of July 1905 # Greenwich Mean Solar Time will be adopted throughout the Colony and # Protectorate, and that it will be necessary to put all clocks 13 minutes and # 35 seconds back, recording local mean time." # # It seemed that Lagos returned to LMT on 1908-07-01. # [The Lagos Standard], 1908-07-01, p 5 # http://ddsnext.crl.edu/titles/31556#?c=0&m=78&s=0&cv=4&r=0&xywh=-92,3590,3944,2523 # says "Scarcely have the people become accustomed to this new time, when # another official notice has now appeared announcing that from and after the # 1st July next, return will be made to local mean time." # # From P Chan (2020-11-27): # On 1914-01-01, standard time of GMT+0:30 was adopted for the unified Nigeria. # Colonial Reports - Annual. No. 878. Nigeria. Report for 1914. (April 1916), # p 27 # https://libsysdigi.library.illinois.edu/ilharvest/Africana/Books2011-05/3064634/3064634_1914/3064634_1914_opt.pdf#page=27 # "On January 1st [1914], a universal standard time for Nigeria was adopted, # viz., half an hour fast on Greenwich mean time, corresponding to the meridian # 7° 30' E. long." # Lloyd's Register of Shipping (1915) says "Hitherto the time observed in Lagos # was the local mean time. On 1st January, 1914, standard time for the whole of # Nigeria was introduced ... Lagos time has been advanced about 16 minutes # accordingly." # # In 1919, standard time was changed to GMT+1. # Interpretation Ordinance (Cap 2) # The Laws of Nigeria, Containing the Ordinances of Nigeria, in Force on the # 1st Day of January, 1923, Vol.I [p 16] # https://books.google.com/books?id=BOMrAQAAMAAJ&pg=PA16 # "The expression 'Standard time' means standard time as used in Nigeria: # namely, 60 minutes in advance of Greenwich mean time. (As amended by 18 of # 1919, s. 2.)" # From Tim Parenti (2020-12-10): # The Lagos Weekly Record, 1919-09-20, p 3 details discussion on the first # reading of this Bill by the Legislative Council of the Colony of Nigeria on # Thursday 1919-08-28: # http://ddsnext.crl.edu/titles/31558?terms&item_id=303484#?m=1118&c=1&s=0&cv=2&r=0&xywh=1261,3408,2994,1915 # "The proposal is that the Globe should be divided into twelve zones East and # West of Greenwich, of one hour each, Nigeria falling into the zone with a # standard of one hour fast on Greenwich Mean Time. Nigeria standard time is # now 30 minutes in advance of Greenwich Mean Time ... according to the new # proposal, standard time will be advanced another 30 minutes". It was further # proposed that the firing of the time guns likewise be adjusted by 30 minutes # to compensate. # From Tim Parenti (2020-12-10), per P Chan (2020-12-11): # The text of Ordinance 18 of 1919, published in Nigeria Gazette, Vol 6, No 52, # shows that the change was assented to the following day and took effect "on # the 1st day of September, 1919." # Nigeria Gazette and Supplements 1919 Jan-Dec, Reference: 73266B-40, # img 245-246 # https://microform.digital/boa/collections/77/volumes/539/nigeria-lagos-1887-1919 # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Africa/Lagos 0:13:35 - LMT 1905 Jul 1 0:00 - GMT 1908 Jul 1 0:13:35 - LMT 1914 Jan 1 0:30 - %z 1919 Sep 1 1:00 - WAT # São Tomé and Príncipe # See Europe/Lisbon for info about the 1912 transition. # From Steffen Thorsen (2018-01-08): # Multiple sources tell that São Tomé changed from UTC to UTC+1 as # they entered the year 2018. # From Michael Deckers (2018-01-08): # the switch is from 01:00 to 02:00 ... [Decree No. 25/2017] # http://www.mnec.gov.st/index.php/publicacoes/documentos/file/90-decreto-lei-n-25-2017 # From Vadim Nasardinov (2018-12-29): # São Tomé and Príncipe is about to do the following on Jan 1, 2019: # https://www.stp-press.st/2018/12/05/governo-jesus-ja-decidiu-repor-hora-legal-sao-tomense/ # # From Michael Deckers (2018-12-30): # https://www.legis-palop.org/download.jsp?idFile=102818 # ... [The legal time of the country, which coincides with universal # coordinated time, will be reinstituted at 2 o'clock on day 1 of January, 2019.] Zone Africa/Sao_Tome 0:26:56 - LMT 1884 #STDOFF -0:36:44.68 -0:36:45 - LMT 1912 Jan 1 00:00u # Lisbon MT 0:00 - GMT 2018 Jan 1 01:00 1:00 - WAT 2019 Jan 1 02:00 0:00 - GMT # Eswatini (Swaziland) # Lesotho # South Africa # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule SA 1942 1943 - Sep Sun>=15 2:00 1:00 - Rule SA 1943 1944 - Mar Sun>=15 2:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Africa/Johannesburg 1:52:00 - LMT 1892 Feb 8 1:30 - SAST 1903 Mar 2:00 SA SAST # # Marion and Prince Edward Is # scientific station since 1947 # no information # Sudan # From # Sudan News Agency (2000-01-13), # also reported by Michaël De Beukelaer-Dossche via Steffen Thorsen: # Clocks will be moved ahead for 60 minutes all over the Sudan as of noon # Saturday.... This was announced Thursday by Caretaker State Minister for # Manpower Abdul-Rahman Nur-Eddin. # From Ahmed Atyya, National Telecommunications Corp. (NTC), Sudan (2017-10-17): # ... the Republic of Sudan is going to change the time zone from (GMT+3:00) # to (GMT+ 2:00) starting from Wednesday 1 November 2017. # # From Paul Eggert (2017-10-18): # A scanned copy (in Arabic) of Cabinet Resolution No. 352 for the # year 2017 can be found as an attachment in email today from Yahia # Abdalla of NTC, archived at: # https://mm.icann.org/pipermail/tz/2017-October/025333.html # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Sudan 1970 only - May 1 0:00 1:00 S Rule Sudan 1970 1985 - Oct 15 0:00 0 - Rule Sudan 1971 only - Apr 30 0:00 1:00 S Rule Sudan 1972 1985 - Apr lastSun 0:00 1:00 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Africa/Khartoum 2:10:08 - LMT 1931 2:00 Sudan CA%sT 2000 Jan 15 12:00 3:00 - EAT 2017 Nov 1 2:00 - CAT # South Sudan # From Steffen Thorsen (2021-01-18): # "South Sudan will change its time zone by setting the clock back 1 # hour on February 1, 2021...." # from https://eyeradio.org/south-sudan-adopts-new-time-zone-makuei/ # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Africa/Juba 2:06:28 - LMT 1931 2:00 Sudan CA%sT 2000 Jan 15 12:00 3:00 - EAT 2021 Feb 1 00:00 2:00 - CAT # Tunisia # From Gwillim Law (2005-04-30): # My correspondent, Risto Nykänen, has alerted me to another adoption of DST, # this time in Tunisia. According to Yahoo France News # , in a story attributed to AP # and dated 2005-04-26, "Tunisia has decided to advance its official time by # one hour, starting on Sunday, May 1. Henceforth, Tunisian time will be # UTC+2 instead of UTC+1. The change will take place at 23:00 UTC next # Saturday." (My translation) # # From Oscar van Vlijmen (2005-05-02): # La Presse, the first national daily newspaper ... # http://www.lapresse.tn/archives/archives280405/actualites/lheure.html # ... DST for 2005: on: Sun May 1 0h standard time, off: Fri Sept. 30, # 1h standard time. # # From Atef Loukil (2006-03-28): # The daylight saving time will be the same each year: # Beginning : the last Sunday of March at 02:00 # Ending : the last Sunday of October at 03:00 ... # http://www.tap.info.tn/en/index.php?option=com_content&task=view&id=1188&Itemid=50 # From Steffen Thorsen (2009-03-16): # According to several news sources, Tunisia will not observe DST this year. # (Arabic) # http://www.elbashayer.com/?page=viewn&nid=42546 # https://www.babnet.net/kiwidetail-15295.asp # # We have also confirmed this with the US embassy in Tunisia. # We have a wrap-up about this on the following page: # https://www.timeanddate.com/news/time/tunisia-cancels-dst-2009.html # From Alexander Krivenyshev (2009-03-17): # Here is a link to Tunis Afrique Presse News Agency # # Standard time to be kept the whole year long (tap.info.tn): # # (in English) # http://www.tap.info.tn/en/index.php?option=com_content&task=view&id=26813&Itemid=157 # # (in Arabic) # http://www.tap.info.tn/ar/index.php?option=com_content&task=view&id=61240&Itemid=1 # From Arthur David Olson (2009-03-18): # The Tunis Afrique Presse News Agency notice contains this: "This measure is # due to the fact that the fasting month of Ramadan coincides with the period # concerned by summer time. Therefore, the standard time will be kept # unchanged the whole year long." So foregoing DST seems to be an exception # (albeit one that may be repeated in the future). # From Alexander Krivenyshev (2010-03-27): # According to some news reports Tunis confirmed not to use DST in 2010 # # (translation): # "The Tunisian government has decided to abandon DST, which was scheduled on # Sunday... # Tunisian authorities had suspended the DST for the first time last year also # coincided with the month of Ramadan..." # # (in Arabic) # http://www.moheet.com/show_news.aspx?nid=358861&pg=1 # http://www.almadenahnews.com/newss/news.php?c=118&id=38036 # http://www.worldtimezone.com/dst_news/dst_news_tunis02.html # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Tunisia 1939 only - Apr 15 23:00s 1:00 S Rule Tunisia 1939 only - Nov 18 23:00s 0 - Rule Tunisia 1940 only - Feb 25 23:00s 1:00 S Rule Tunisia 1941 only - Oct 6 0:00 0 - Rule Tunisia 1942 only - Mar 9 0:00 1:00 S Rule Tunisia 1942 only - Nov 2 3:00 0 - Rule Tunisia 1943 only - Mar 29 2:00 1:00 S Rule Tunisia 1943 only - Apr 17 2:00 0 - Rule Tunisia 1943 only - Apr 25 2:00 1:00 S Rule Tunisia 1943 only - Oct 4 2:00 0 - Rule Tunisia 1944 1945 - Apr Mon>=1 2:00 1:00 S Rule Tunisia 1944 only - Oct 8 0:00 0 - Rule Tunisia 1945 only - Sep 16 0:00 0 - Rule Tunisia 1977 only - Apr 30 0:00s 1:00 S Rule Tunisia 1977 only - Sep 24 0:00s 0 - Rule Tunisia 1978 only - May 1 0:00s 1:00 S Rule Tunisia 1978 only - Oct 1 0:00s 0 - Rule Tunisia 1988 only - Jun 1 0:00s 1:00 S Rule Tunisia 1988 1990 - Sep lastSun 0:00s 0 - Rule Tunisia 1989 only - Mar 26 0:00s 1:00 S Rule Tunisia 1990 only - May 1 0:00s 1:00 S Rule Tunisia 2005 only - May 1 0:00s 1:00 S Rule Tunisia 2005 only - Sep 30 1:00s 0 - Rule Tunisia 2006 2008 - Mar lastSun 2:00s 1:00 S Rule Tunisia 2006 2008 - Oct lastSun 2:00s 0 - # See Europe/Paris commentary for PMT-related transitions. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Africa/Tunis 0:40:44 - LMT 1881 May 12 0:09:21 - PMT 1911 Mar 11 # Paris Mean Time 1:00 Tunisia CE%sT pr0m1th3as-datatypes-9450b29/inst/tzdata/antarctica000066400000000000000000000334001527370255000221760ustar00rootroot00000000000000# tzdb data for Antarctica and environs # This file is in the public domain, so clarified as of # 2009-05-17 by Arthur David Olson. # From Paul Eggert (2025-08-16): # To keep things manageable, list only locations occupied year-round; see # Antarctic Facilities Information # https://www.comnap.aq/antarctic-facilities-information # for information. # Unless otherwise specified, we have no time zone information. # FORMAT is '-00' and STDOFF is 0 for locations while uninhabited. # Argentina - year-round bases # Belgrano II, Confin Coast, -770227-0343737, since 1972-02-05 # Carlini, Potter Cove, King George Island, -6414-0602320, since 1982-01 # Esperanza, Hope Bay, -6323-05659, since 1952-12-17 # Marambio, -6414-05637, since 1969-10-29 # Orcadas, Laurie I, -6016-04444, since 1904-02-22 # San Martín, Barry I, -6808-06706, since 1951-03-21 # (except 1960-03 / 1976-03-21) # Australia - territories # Heard Island, McDonald Islands (uninhabited) # previously sealers and scientific personnel wintered # Margaret Turner reports # https://web.archive.org/web/20021204222245/http://www.dstc.qut.edu.au/DST/marg/daylight.html # (1999-09-30) that they're UT +05, with no DST; # presumably this is when they have visitors. # # year-round bases # Casey, Bailey Peninsula, -6617+11032, since 1969 # Davis, Vestfold Hills, -6835+07759, since 1957-01-13 # (except 1964-11 - 1969-02) # Mawson, Holme Bay, -6736+06253, since 1954-02-13 # From Steffen Thorsen (2009-03-11): # Three Australian stations in Antarctica have changed their time zone: # Casey moved from UTC+8 to UTC+11 # Davis moved from UTC+7 to UTC+5 # Mawson moved from UTC+6 to UTC+5 # The changes occurred on 2009-10-18 at 02:00 (local times). # # Government source: (Australian Antarctic Division) # http://www.aad.gov.au/default.asp?casid=37079 # # We have more background information here: # https://www.timeanddate.com/news/time/antarctica-new-times.html # From Steffen Thorsen (2010-03-10): # We got these changes from the Australian Antarctic Division: ... # # - Casey station reverted to its normal time of UTC+8 on 5 March 2010. # The change to UTC+11 is being considered as a regular summer thing but # has not been decided yet. # # - Davis station will revert to its normal time of UTC+7 at 10 March 2010 # 20:00 UTC. # # - Mawson station stays on UTC+5. # # Background: # https://www.timeanddate.com/news/time/antartica-time-changes-2010.html # From Steffen Thorsen (2016-10-28): # Australian Antarctica Division informed us that Casey changed time # zone to UTC+11 in "the morning of 22nd October 2016". # From Steffen Thorsen (2020-10-02, as corrected): # Based on information we have received from the Australian Antarctic # Division, Casey station and Macquarie Island station will move to Tasmanian # daylight savings time on Sunday 4 October. This will take effect from 0001 # hrs on Sunday 4 October 2020 and will mean Casey and Macquarie Island will # be on the same time zone as Hobart. Some past dates too for this 3 hour # time change back and forth between UTC+8 and UTC+11 for Casey: # - 2018 Oct 7 4:00 - 2019 Mar 17 3:00 - 2019 Oct 4 3:00 - 2020 Mar 8 3:00 # and now - 2020 Oct 4 0:01 # From Paul Eggert (2023-12-20): # Transitions from 2021 on are taken from: # https://www.timeanddate.com/time/zone/antarctica/casey # retrieved at various dates. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Antarctica/Casey 0 - -00 1969 8:00 - %z 2009 Oct 18 2:00 11:00 - %z 2010 Mar 5 2:00 8:00 - %z 2011 Oct 28 2:00 11:00 - %z 2012 Feb 21 17:00u 8:00 - %z 2016 Oct 22 11:00 - %z 2018 Mar 11 4:00 8:00 - %z 2018 Oct 7 4:00 11:00 - %z 2019 Mar 17 3:00 8:00 - %z 2019 Oct 4 3:00 11:00 - %z 2020 Mar 8 3:00 8:00 - %z 2020 Oct 4 0:01 11:00 - %z 2021 Mar 14 0:00 8:00 - %z 2021 Oct 3 0:01 11:00 - %z 2022 Mar 13 0:00 8:00 - %z 2022 Oct 2 0:01 11:00 - %z 2023 Mar 9 3:00 8:00 - %z Zone Antarctica/Davis 0 - -00 1957 Jan 13 7:00 - %z 1964 Nov 0 - -00 1969 Feb 7:00 - %z 2009 Oct 18 2:00 5:00 - %z 2010 Mar 10 20:00u 7:00 - %z 2011 Oct 28 2:00 5:00 - %z 2012 Feb 21 20:00u 7:00 - %z Zone Antarctica/Mawson 0 - -00 1954 Feb 13 6:00 - %z 2009 Oct 18 2:00 5:00 - %z # References: # Casey Weather (1998-02-26) # http://www.antdiv.gov.au/aad/exop/sfo/casey/casey_aws.html # Davis Station, Antarctica (1998-02-26) # http://www.antdiv.gov.au/aad/exop/sfo/davis/video.html # Mawson Station, Antarctica (1998-02-25) # http://www.antdiv.gov.au/aad/exop/sfo/mawson/video.html # Belgium - year-round base # Princess Elisabeth, Queen Maud Land, -713412+0231200, since 2007 # Brazil - year-round base # Ferraz, King George Island, -6205+05824, since 1983/4 # Bulgaria - year-round base # St. Kliment Ohridski, Livingston Island, -623829-0602153, since 1988 # Chile - year-round bases and towns # Escudero, South Shetland Is, -621157-0585735, since 1994 # Frei Montalva, King George Island, -6214-05848, since 1969-03-07 # O'Higgins, Antarctic Peninsula, -6319-05704, since 1948-02 # Prat, -6230-05941 # Villa Las Estrellas (a town), around the Frei base, since 1984-04-09 # These locations employ Region of Magallanes time; use # TZ='America/Punta_Arenas'. # China - year-round bases # Great Wall, King George Island, -6213-05858, since 1985-02-20 # Zhongshan, Larsemann Hills, Prydz Bay, -6922+07623, since 1989-02-26 # Qinling, Inexpressible I, Terra Nova Bay, -7456+16343, since 2024-02-07 # France - year-round bases (also see "France & Italy") # # From Antoine Leca (1997-01-20): # Time data entries are from Nicole Pailleau at the IFRTP # (French Institute for Polar Research and Technology). # She confirms that French Southern Territories and Terre Adélie bases # don't observe daylight saving time, even if Terre Adélie supplies came # from Tasmania. # # French Southern Territories with year-round inhabitants # # Alfred Faure, Possession Island, Crozet Islands, -462551+0515152, since 1964; # sealing & whaling stations operated variously 1802/1911+; # see Asia/Dubai. # # Martin-de-Viviès, Amsterdam Island, -374105+0773155, since 1950 # Port-aux-Français, Kerguelen Islands, -492110+0701303, since 1951; # whaling & sealing station operated 1908/1914, 1920/1929, and 1951/1956 # # St Paul Island - near Amsterdam, uninhabited # fishing stations operated variously 1819/1931 # # Kerguelen - see Indian/Maldives. # # year-round base in the main continent # Dumont d'Urville - see Pacific/Port_Moresby. # France & Italy - year-round base # Concordia, -750600+1232000, since 2005 # https://en.wikipedia.org/wiki/Concordia_Station # Can use Asia/Singapore, which it has agreed with since inception. # Germany - year-round base # Neumayer III, -704080-0081602, since 2009 # India - year-round bases # Bharati, -692428+0761114, since 2012 # Maitri, -704558+0114356, since 1989 # Italy - year-round base (also see "France & Italy") # Zuchelli, Terra Nova Bay, -744140+1640647, since 1986 # Japan - year-round bases # See Asia/Riyadh. # S Korea - year-round base # Jang Bogo, Terra Nova Bay, -743700+1641205 since 2014 # King Sejong, King George Island, -6213-05847, since 1988 # New Zealand - claims # Balleny Islands (never inhabited) # Scott Island (never inhabited) # # year-round base # Scott Base, Ross Island, since 1957-01. # See Pacific/Auckland. # Norway - territories # Bouvet (never inhabited) # # claims # Peter I Island (never inhabited) # # year-round base # Troll, Queen Maud Land, -720041+0023206, since 2005-02-12 # # From Paul-Inge Flakstad (2014-03-10): # I recently had a long dialog about this with the developer of timegenie.com. # In the absence of specific dates, he decided to choose some likely ones: # GMT +1 - From March 1 to the last Sunday in March # GMT +2 - From the last Sunday in March until the last Sunday in October # GMT +1 - From the last Sunday in October until November 7 # GMT +0 - From November 7 until March 1 # The dates for switching to and from UTC+0 will probably not be absolutely # correct, but they should be quite close to the actual dates. # # From Paul Eggert (2014-03-21): # The CET-switching Troll rules require zic from tz 2014b or later, so as # suggested by Bengt-Inge Larsson comment them out for now, and approximate # with only UTC and CEST. Uncomment them when 2014b is more prevalent. # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S #Rule Troll 2005 max - Mar 1 1:00u 1:00 +01 Rule Troll 2005 max - Mar lastSun 1:00u 2:00 +02 #Rule Troll 2005 max - Oct lastSun 1:00u 1:00 +01 #Rule Troll 2004 max - Nov 7 1:00u 0:00 +00 # Remove the following line when uncommenting the above '#Rule' lines. Rule Troll 2004 max - Oct lastSun 1:00u 0:00 +00 # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Antarctica/Troll 0 - -00 2005 Feb 12 0:00 Troll %s # Poland - year-round base # Arctowski, King George Island, -620945-0582745, since 1977 # Romania - year-bound base # Law-Racoviță, Larsemann Hills, -692319+0762251, since 1986 # Russia - year-round bases # Bellingshausen, King George Island, -621159-0585337, since 1968-02-22 # Mirny, Davis coast, -6633+09301, since 1956-02 # Molodezhnaya, Alasheyev Bay, -6740+04551, # year-round from 1962-02 to 1999-07-01 # Novolazarevskaya, Queen Maud Land, -7046+01150, # year-round from 1960/61 to 1992 # Vostok, since 1957-12-16, temporarily closed 1994-02/1994-11 # From Craig Mundell (1994-12-15): # http://quest.arc.nasa.gov/antarctica/QA/computers/Directions,Time,ZIP # Vostok, which is one of the Russian stations, is set on the same # time as Moscow, Russia. # # From Lee Hotz (2001-03-08): # I queried the folks at Columbia who spent the summer at Vostok and this is # what they had to say about time there: # "in the US Camp (East Camp) we have been on New Zealand (McMurdo) # time, which is 12 hours ahead of GMT. The Russian Station Vostok was # 6 hours behind that (although only 2 miles away, i.e. 6 hours ahead # of GMT). This is a time zone I think two hours east of Moscow. The # natural time zone is in between the two: 8 hours ahead of GMT." # # From Paul Eggert (2001-05-04): # This seems to be hopelessly confusing, so I asked Lee Hotz about it # in person. He said that some Antarctic locations set their local # time so that noon is the warmest part of the day, and that this # changes during the year and does not necessarily correspond to mean # solar noon. So the Vostok time might have been whatever the clocks # happened to be during their visit. So we still don't really know what time # it is at Vostok. # # From Zakhary V. Akulov (2023-12-17 22:00:48 +0700): # ... from December, 18, 2023 00:00 by my decision the local time of # the Antarctic research base Vostok will correspond to UTC+5. # (2023-12-19): We constantly interact with Progress base, with company who # builds new wintering station, with sledge convoys, with aviation - they all # use UTC+5. Besides, difference between Moscow time is just 2 hours now, not 4. # (2023-12-19, in response to the question "Has local time at Vostok # been UTC+6 ever since 1957, or has it changed before?"): No. At least # since my antarctic career start, 10 years ago, Vostok base has UTC+7. # (In response to a 2023-12-18 question "from 02:00 to 00:00 today"): This. # # From Paul Eggert (2023-12-18): # For lack of better info, guess Vostok was at +07 from founding through today, # except when closed. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Antarctica/Vostok 0 - -00 1957 Dec 16 7:00 - %z 1994 Feb 0 - -00 1994 Nov 7:00 - %z 2023 Dec 18 2:00 5:00 - %z # S Africa - year-round bases # Marion Island, -4653+03752 # SANAE IV, Vesleskarvet, Queen Maud Land, -714022-0025026, since 1997 # Ukraine - year-round base # Vernadsky (formerly Faraday), Galindez Island, -651445-0641526, since 1954 # United Kingdom # # British Antarctic Territories (BAT) claims # South Orkney Islands # scientific station from 1903 # whaling station at Signy I 1920/1926 # South Shetland Islands # # year-round bases # Bird Island, South Georgia, -5400-03803, since 1983 # Deception Island, -6259-06034, whaling station 1912/1931, # scientific station 1943/1967, # previously sealers and a scientific expedition wintered by accident, # and a garrison was deployed briefly # Halley, Coates Land, -7535-02604, since 1956-01-06 # Halley is on a moving ice shelf and is periodically relocated # so that it is never more than 10km from its nominal location. # Rothera, Adelaide Island, -6734-6808, since 1976-12-01 # # From Paul Eggert (2002-10-22) # says Rothera is -03 all year. # # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Antarctica/Rothera 0 - -00 1976 Dec 1 -3:00 - %z # Uruguay - year round base # Artigas, King George Island, -621104-0585107 # USA - year-round bases # # Palmer, Anvers Island, since 1965 (moved 2 miles in 1968) # See 'southamerica' for Antarctica/Palmer, since it uses South American DST. # # McMurdo Station, Ross Island, since 1955-12 # Amundsen-Scott South Pole Station, continuously occupied since 1956-11-20 # # From Chris Carrier (1996-06-27): # Siple, the first commander of the South Pole station, # stated that he would have liked to have kept GMT at the station, # but that he found it more convenient to keep GMT+12 # as supplies for the station were coming from McMurdo Sound, # which was on GMT+12 because New Zealand was on GMT+12 all year # at that time (1957). (Source: Siple's book 90° South.) # # From Susan Smith # http://www.cybertours.com/whs/pole10.html # (1995-11-13 16:24:56 +1300, no longer available): # We use the same time as McMurdo does. # And they use the same time as Christchurch, NZ does.... # One last quirk about South Pole time. # All the electric clocks are usually wrong. # Something about the generators running at 60.1hertz or something # makes all of the clocks run fast. So every couple of days, # we have to go around and set them back 5 minutes or so. # Maybe if we let them run fast all of the time, we'd get to leave here sooner!! # # See Pacific/Auckland. pr0m1th3as-datatypes-9450b29/inst/tzdata/asia000066400000000000000000005717651527370255000210270ustar00rootroot00000000000000# tzdb data for Asia and environs # This file is in the public domain, so clarified as of # 2009-05-17 by Arthur David Olson. # This file is by no means authoritative; if you think you know better, # go ahead and edit the file, and please send any changes to # the public mailing list tz@iana.org for general use in the future. # For more, please see the file CONTRIBUTING in the tz distribution. # From Paul Eggert (2019-07-11): # # Unless otherwise specified, the source for data through 1990 is: # Thomas G. Shanks and Rique Pottenger, The International Atlas (6th edition), # San Diego: ACS Publications, Inc. (2003). # Unfortunately this book contains many errors and cites no sources. # # Many years ago Gwillim Law wrote that a good source # for time zone data was the International Air Transport # Association's Standard Schedules Information Manual (IATA SSIM), # published semiannually. Law sent in several helpful summaries # of the IATA's data after 1990. Except where otherwise noted, # IATA SSIM is the source for entries after 1990. # # Another source occasionally used is Edward W. Whitman, World Time Differences, # Whitman Publishing Co, 2 Niagara Av, Ealing, London (undated), which # I found in the UCLA library. # # For data circa 1899, a common source is: # Milne J. Civil time. Geogr J. 1899 Feb;13(2):173-94. # https://www.jstor.org/stable/1774359 # # For Russian data circa 1919, a source is: # Byalokoz EL. New Counting of Time in Russia since July 1, 1919. # (See the 'europe' file for a fuller citation.) # # The following alphabetic abbreviations appear in these tables # (corrections are welcome): # std dst # LMT Local Mean Time # 2:00 EET EEST Eastern European Time # 2:00 IST IDT Israel # 5:30 IST India # 7:00 WIB west Indonesia (Waktu Indonesia Barat) # 8:00 WITA central Indonesia (Waktu Indonesia Tengah) # 8:00 CST China # 8:00 HKT HKST Hong Kong (HKWT* for Winter Time in late 1941) # 8:00 PST PDT* Philippines # 8:30 KST KDT Korea when at +0830 # 9:00 WIT east Indonesia (Waktu Indonesia Timur) # 9:00 JST JDT Japan # 9:00 KST KDT Korea when at +09 # *I invented the abbreviations HKWT and PDT; see below. # Otherwise, these tables typically use numeric abbreviations like +03 # and +0330 for integer hour and minute UT offsets. Although earlier # editions invented alphabetic time zone abbreviations for every # offset, this did not reflect common practice. # # See the 'europe' file for Russia and Turkey in Asia. # From Guy Harris: # Incorporates data for Singapore from Robert Elz' asia 1.1, as well as # additional information from Tom Yap, Sun Microsystems Intercontinental # Technical Support (including a page from the Official Airline Guide - # Worldwide Edition). ############################################################################### # These rules are stolen from the 'europe' file. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule EUAsia 1981 max - Mar lastSun 1:00u 1:00 S Rule EUAsia 1979 1995 - Sep lastSun 1:00u 0 - Rule EUAsia 1996 max - Oct lastSun 1:00u 0 - Rule E-EurAsia 1981 max - Mar lastSun 0:00 1:00 - Rule E-EurAsia 1979 1995 - Sep lastSun 0:00 0 - Rule E-EurAsia 1996 max - Oct lastSun 0:00 0 - Rule RussiaAsia 1981 1984 - Apr 1 0:00 1:00 - Rule RussiaAsia 1981 1983 - Oct 1 0:00 0 - Rule RussiaAsia 1984 1995 - Sep lastSun 2:00s 0 - Rule RussiaAsia 1985 2010 - Mar lastSun 2:00s 1:00 - Rule RussiaAsia 1996 2010 - Oct lastSun 2:00s 0 - # Afghanistan # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Kabul 4:36:48 - LMT 1890 4:00 - %z 1945 4:30 - %z # Armenia # From Paul Eggert (2006-03-22): # Shanks & Pottenger have Yerevan switching to 3:00 (with Russian DST) # in spring 1991, then to 4:00 with no DST in fall 1995, then # readopting Russian DST in 1997. Go with Shanks & Pottenger, even # when they disagree with others. Edgar Der-Danieliantz # reported (1996-05-04) that Yerevan probably wouldn't use DST # in 1996, though it did use DST in 1995. IATA SSIM (1991/1998) reports that # Armenia switched from 3:00 to 4:00 in 1998 and observed DST after 1991, # but started switching at 3:00s in 1998. # From Arthur David Olson (2011-06-15): # While Russia abandoned DST in 2011, Armenia may choose to # follow Russia's "old" rules. # From Alexander Krivenyshev (2012-02-10): # According to News Armenia, on Feb 9, 2012, # http://newsarmenia.ru/society/20120209/42609695.html # # The Armenia National Assembly adopted final reading of Amendments to the # Law "On procedure of calculation time on the territory of the Republic of # Armenia" according to which Armenia [is] abolishing Daylight Saving Time. # or # (brief) # http://www.worldtimezone.com/dst_news/dst_news_armenia03.html # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Armenia 2011 only - Mar lastSun 2:00s 1:00 - Rule Armenia 2011 only - Oct lastSun 2:00s 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Yerevan 2:58:00 - LMT 1924 May 2 3:00 - %z 1957 Mar 4:00 RussiaAsia %z 1991 Mar 31 2:00s 3:00 RussiaAsia %z 1995 Sep 24 2:00s 4:00 - %z 1997 4:00 RussiaAsia %z 2011 4:00 Armenia %z # Azerbaijan # From Rustam Aliyev of the Azerbaijan Internet Forum (2005-10-23): # According to the resolution of Cabinet of Ministers, 1997 # From Paul Eggert (2015-09-17): It was Resolution No. 21 (1997-03-17). # http://code.az/files/daylight_res.pdf # From Steffen Thorsen (2016-03-17): # ... the Azerbaijani Cabinet of Ministers has cancelled switching to # daylight saving time.... # https://www.azernews.az/azerbaijan/94137.html # http://vestnikkavkaza.net/news/Azerbaijani-Cabinet-of-Ministers-cancels-daylight-saving-time.html # http://en.apa.az/xeber_azerbaijan_abolishes_daylight_savings_ti_240862.html # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Azer 1997 2015 - Mar lastSun 4:00 1:00 - Rule Azer 1997 2015 - Oct lastSun 5:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Baku 3:19:24 - LMT 1924 May 2 3:00 - %z 1957 Mar 4:00 RussiaAsia %z 1991 Mar 31 2:00s 3:00 RussiaAsia %z 1992 Sep lastSun 2:00s 4:00 - %z 1996 4:00 EUAsia %z 1997 4:00 Azer %z # Bangladesh # From Alexander Krivenyshev (2009-05-13): # According to newspaper Asian Tribune (May 6, 2009) Bangladesh may introduce # Daylight Saving Time from June 16 to Sept 30 # # Bangladesh to introduce daylight saving time likely from June 16 # http://www.asiantribune.com/?q=node/17288 # http://www.worldtimezone.com/dst_news/dst_news_bangladesh02.html # # "... Bangladesh government has decided to switch daylight saving time from # June # 16 till September 30 in a bid to ensure maximum use of daylight to cope with # crippling power crisis. " # # The switch will remain in effect from June 16 to Sept 30 (2009) but if # implemented the next year, it will come in force from April 1, 2010 # From Steffen Thorsen (2009-06-02): # They have finally decided now, but changed the start date to midnight between # the 19th and 20th, and they have not set the end date yet. # # Some sources: # https://in.reuters.com/article/southAsiaNews/idINIndia-40017620090601 # http://bdnews24.com/details.php?id=85889&cid=2 # # Our wrap-up: # https://www.timeanddate.com/news/time/bangladesh-daylight-saving-2009.html # From A. N. M. Kamrus Saadat (2009-06-15): # Finally we've got the official mail regarding DST start time where DST start # time is mentioned as Jun 19 2009, 23:00 from BTRC (Bangladesh # Telecommunication Regulatory Commission). # # No DST end date has been announced yet. # From Alexander Krivenyshev (2009-09-25): # Bangladesh won't go back to Standard Time from October 1, 2009, # instead it will continue DST measure till the cabinet makes a fresh decision. # # Following report by same newspaper-"The Daily Star Friday": # "DST change awaits cabinet decision-Clock won't go back by 1-hr from Oct 1" # http://www.thedailystar.net/newDesign/news-details.php?nid=107021 # http://www.worldtimezone.com/dst_news/dst_news_bangladesh04.html # From Steffen Thorsen (2009-10-13): # IANS (Indo-Asian News Service) now reports: # Bangladesh has decided that the clock advanced by an hour to make # maximum use of daylight hours as an energy saving measure would # "continue for an indefinite period." # # One of many places where it is published: # http://www.thaindian.com/newsportal/business/bangladesh-to-continue-indefinitely-with-advanced-time_100259987.html # From Alexander Krivenyshev (2009-12-24): # According to Bangladesh newspaper "The Daily Star," # Bangladesh will change its clock back to Standard Time on Dec 31, 2009. # # Clock goes back 1-hr on Dec 31 night. # http://www.thedailystar.net/newDesign/news-details.php?nid=119228 # http://www.worldtimezone.com/dst_news/dst_news_bangladesh05.html # # "...The government yesterday decided to put the clock back by one hour # on December 31 midnight and the new time will continue until March 31, # 2010 midnight. The decision came at a cabinet meeting at the Prime # Minister's Office last night..." # From Alexander Krivenyshev (2010-03-22): # According to Bangladesh newspaper "The Daily Star," # Cabinet cancels Daylight Saving Time # http://www.thedailystar.net/newDesign/latest_news.php?nid=22817 # http://www.worldtimezone.com/dst_news/dst_news_bangladesh06.html # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Dhaka 2009 only - Jun 19 23:00 1:00 - Rule Dhaka 2009 only - Dec 31 24:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Dhaka 6:01:40 - LMT 1890 5:53:20 - HMT 1941 Oct # Howrah Mean Time? 6:30 - %z 1942 May 15 5:30 - %z 1942 Sep 6:30 - %z 1951 Sep 30 6:00 - %z 2009 6:00 Dhaka %z # Bhutan # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Thimphu 5:58:36 - LMT 1947 Aug 15 # or Thimbu 5:30 - %z 1987 Oct 6:00 - %z # British Indian Ocean Territory # Whitman and the 1995 CIA time zone map say 5:00, but the # 1997 and later maps say 6:00. Assume the switch occurred in 1996. # We have no information as to when standard time was introduced; # assume it occurred in 1907, the same year as Mauritius (which # then contained the Chagos Archipelago). # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Indian/Chagos 4:49:40 - LMT 1907 5:00 - %z 1996 6:00 - %z # Cocos (Keeling) Islands # Myanmar (Burma) # Milne says 6:24:40 was the meridian of the time ball observatory at Rangoon. # From Paul Eggert (2017-04-20): # Page 27 of Reed & Low (cited for Asia/Kolkata) says "Rangoon local time is # used upon the railways and telegraphs of Burma, and is 6h. 24m. 47s. ahead # of Greenwich." This refers to the period before Burma's transition to +0630, # a transition for which Shanks is the only source. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Yangon 6:24:47 - LMT 1880 # or Rangoon 6:24:47 - RMT 1920 # Rangoon local time 6:30 - %z 1942 May 9:00 - %z 1945 May 3 6:30 - %z # China # From Phake Nick (2020-04-15): # According to this news report: # http://news.sina.com.cn/c/2004-09-01/19524201403.shtml # on April 11, 1919, newspaper in Shanghai said clocks in Shanghai will spring # forward for an hour starting from midnight of that Saturday. The report did # not mention what happened in Shanghai thereafter, but it mentioned that a # similar trial in Tianjin which ended at October 1st as citizens are told to # recede the clock on September 30 from 12:00pm to 11:00pm. The trial at # Tianjin got terminated in 1920. # # From Paul Eggert (2020-04-15): # The Returns of Trade and Trade Reports, page 711, says "Daylight saving was # given a trial during the year, and from the 12th April to the 1st October # the clocks were all set one hour ahead of sun time. Though the scheme was # generally esteemed a success, it was announced early in 1920 that it would # not be repeated." # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Shang 1919 only - Apr 12 24:00 1:00 D Rule Shang 1919 only - Sep 30 24:00 0 S # From Paul Eggert (2018-10-02): # The following comes from Table 1 of: # Li Yu. Research on the daylight saving movement in 1940s Shanghai. # Nanjing Journal of Social Sciences. 2014;(2):144-50. # http://oversea.cnki.net/kns55/detail.aspx?dbname=CJFD2014&filename=NJSH201402020 # The table lists dates only; I am guessing 00:00 and 24:00 transition times. # Also, the table lists the planned end of DST in 1949, but the corresponding # zone line cuts this off on May 28, when the Communists took power. # From Phake Nick (2020-04-15): # # For the history of time in Shanghai between 1940-1942, the situation is # actually slightly more complex than the table [below].... At the time, # there were three different authorities in Shanghai, including Shanghai # International Settlement, a settlement established by western countries with # its own westernized form of government, Shanghai French Concession, similar # to the international settlement but is controlled by French, and then the # rest of the city of Shanghai, which have already been controlled by Japanese # force through a puppet local government (Wang Jingwei regime). It was # additionally complicated by the circumstances that, according to the 1940s # Shanghai summer time essay cited in the database, some # departments/businesses/people in the Shanghai city itself during that time # period, refused to change their clock and instead only changed their opening # hours. # # For example, as quoted in the article, in 1940, other than the authority # itself, power, tram, bus companies, cinema, department stores, and other # public service organizations have all decided to follow the summer time and # spring forward the clock. On the other hand, the custom office refused to # spring forward the clock because of worry on mechanical wear to the physical # clock, postal office refused to spring forward because of disruption to # business and log-keeping, although they did changed their office hour to # match rest of the city. So is travel agents, and also weather # observatory. It is said both time standards had their own supporters in the # city at the time, those who prefer new time standard would have moved their # clock while those who prefer the old time standard would keep their clock # unchange, and there were different clocks that use different time standard # in the city at the time for people who use different time standard to adjust # their clock to their preferred time. # # a. For the 1940 May 31 spring forward, the essay [says] ... "Hong # Kong government implemented the spring forward in the same time on # the same date as Shanghai". # # b. For the 1940 fall back, it was said that they initially intended to do # so on September 30 00:59 at night, however they postponed it to October 12 # after discussion with relevant parties. However schools restored to the # original schedule ten days earlier. # # c. For the 1941 spring forward, it is said to start from March 15 # "following the previous year's method", and in addition to that the essay # cited an announcement in 1941 from the Wang regime which said the Special # City of Shanghai under Wang regime control will follow the DST rule set by # the Settlements, irrespective of the original DST plan announced by the Wang # regime for other area under its control(April 1 to September 30). (no idea # to situation before that announcement) # # d. For the 1941 fall back, it was said that the fall back would occurs at # the end of September (A newspaper headline cited by the essay, published on # October 1, 1941, have the headlines which said "French Concession would # rewind to the old clock this morning), but it ultimately didn't happen due # to disagreement between the international settlement authority and the # French concession authority, and the fall back ultimately occurred on # November 1. # # e. In 1941 December, Japan have officially started war with the United # States and the United Kingdom, and in Shanghai they have marched into the # international settlement, taken over its control # # f. For the 1942 spring forward, the essay said that the spring forward # started on January 31. It said this time the custom office and postal # department will also change their clocks, unlike before. # # g. The essay itself didn't cover any specific changes thereafter until the # end of the war, it quoted a November 1942 command from the government of the # Wang regime, which claim the daylight saving time applies year round during # the war. However, the essay ambiguously said the period is "February 1 to # September 30", which I don't really understand what is the meaning of such # period in the context of year round implementation here.. More researches # might be needed to show exactly what happened during that period of time. # From Phake Nick (2020-04-15): # According to a Japanese tour bus pamphlet in Nanjing area believed to be # from around year 1941: http://www.tt-museum.jp/tairiku_0280_nan1941.html , # the schedule listed was in the format of Japanese time. Which indicate some # use of the Japanese time (instead of syncing by DST) might have occurred in # the Yangtze river delta area during that period of time although the scope # of such use will need to be investigated to determine. # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Shang 1940 only - Jun 1 0:00 1:00 D Rule Shang 1940 only - Oct 12 24:00 0 S Rule Shang 1941 only - Mar 15 0:00 1:00 D Rule Shang 1941 only - Nov 1 24:00 0 S Rule Shang 1942 only - Jan 31 0:00 1:00 D Rule Shang 1945 only - Sep 1 24:00 0 S Rule Shang 1946 only - May 15 0:00 1:00 D Rule Shang 1946 only - Sep 30 24:00 0 S Rule Shang 1947 only - Apr 15 0:00 1:00 D Rule Shang 1947 only - Oct 31 24:00 0 S Rule Shang 1948 1949 - May 1 0:00 1:00 D Rule Shang 1948 1949 - Sep 30 24:00 0 S #plan # From Guy Harris: # People's Republic of China. Yes, they really have only one time zone. # From Bob Devine (1988-01-28): # No they don't. See TIME mag, 1986-02-17 p.52. Even though # China is across 4 physical time zones, before Feb 1, 1986 only the # Peking (Beijing) time zone was recognized. Since that date, China # has two of 'em - Peking's and Ürümqi (named after the capital of # the Xinjiang Uyghur Autonomous Region). I don't know about DST for it. # # . . .I just deleted the DST table and this editor makes it too # painful to suck in another copy. So, here is what I have for # DST start/end dates for Peking's time zone (info from AP): # # 1986 May 4 - Sept 14 # 1987 mid-April - ?? # From U. S. Naval Observatory (1989-01-19): # CHINA 8 H AHEAD OF UTC ALL OF CHINA, INCL TAIWAN # CHINA 9 H AHEAD OF UTC APR 17 - SEP 10 # From Paul Eggert (2008-02-11): # Jim Mann, "A clumsy embrace for another western custom: China on daylight # time - sort of", Los Angeles Times, 1986-05-05 ... [says] that China began # observing daylight saving time in 1986. # From P Chan (2018-05-07): # The start and end time of DST in China [from 1986 on] should be 2:00 # (i.e. 2:00 to 3:00 at the start and 2:00 to 1:00 at the end).... # Government notices about summer time: # # 1986-04-12 http://www.zj.gov.cn/attach/zfgb/198608.pdf p.21-22 # (To establish summer time from 1986. On 4 May, set the clocks ahead one hour # at 2 am. On 14 September, set the clocks backward one hour at 2 am.) # # 1987-02-15 http://www.gov.cn/gongbao/shuju/1987/gwyb198703.pdf p.114 # (Summer time in 1987 to start from 12 April until 13 September) # # 1987-09-09 http://www.gov.cn/gongbao/shuju/1987/gwyb198721.pdf p.709 # (From 1988, summer time to start from 2 am of the first Sunday of mid-April # until 2 am of the first Sunday of mid-September) # # 1992-03-03 http://www.gov.cn/gongbao/shuju/1992/gwyb199205.pdf p.152 # (To suspend summer time from 1992) # # The first page of People's Daily on 12 April 1988 stating that summer time # to begin on 17 April. # http://data.people.com.cn/pic/101p/1988/04/1988041201.jpg # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule PRC 1986 only - May 4 2:00 1:00 D Rule PRC 1986 1991 - Sep Sun>=11 2:00 0 S Rule PRC 1987 1991 - Apr Sun>=11 2:00 1:00 D # From Anthony Fok (2001-12-20): # BTW, I did some research on-line and found some info regarding these five # historic timezones from some Taiwan websites. And yes, there are official # Chinese names for these locales (before 1949). # # From Jesper Nørgaard Welen (2006-07-14): # I have investigated the timezones around 1970 on the # https://www.astro.com/atlas site [with provinces and county # boundaries summarized below].... A few other exceptions were two # counties on the Sichuan side of the Xizang-Sichuan border, # counties Dege and Baiyu which lies on the Sichuan side and are # therefore supposed to be GMT+7, Xizang region being GMT+6, but Dege # county is GMT+8 according to astro.com while Baiyu county is GMT+6 # (could be true), for the moment I am assuming that those two # counties are mistakes in the astro.com data. # From Paul Eggert (2017-01-05): # Alois Treindl kindly sent me translations of the following two sources: # # (1) # Guo Qing-sheng (National Time-Service Center, CAS, Xi'an 710600, China) # Beijing Time at the Beginning of the PRC # China Historical Materials of Science and Technology # (Zhongguo ke ji shi liao, 中国科技�料). 2003;24(1):5-9. # http://oversea.cnki.net/kcms/detail/detail.aspx?filename=ZGKS200301000&dbname=CJFD2003 # It gives evidence that at the beginning of the PRC, Beijing time was # officially apparent solar time! However, Guo also says that the # evidence is dubious, as the relevant institute of astronomy had not # been taken over by the PRC yet. It's plausible that apparent solar # time was announced but never implemented, and that people continued # to use UT+8. As the Shanghai radio station (and I presume the # observatory) was still under control of French missionaries, it # could well have ignored any such mandate. # # (2) # Guo Qing-sheng (Shaanxi Astronomical Observatory, CAS, Xi'an 710600, China) # A Study on the Standard Time Changes for the Past 100 Years in China # [undated and unknown publication location] # It says several things: # * The Qing dynasty used local apparent solar time throughout China. # * The Republic of China instituted Beijing mean solar time effective # the official calendar book of 1914. # * The French Concession in Shanghai set up signal stations in # French docks in the 1890s, controlled by Xujiahui (Zikawei) # Observatory and set to local mean time. # * "From the end of the 19th century" it changed to UT+8. # * Chinese Customs (by then reduced to a tool of foreign powers) # eventually standardized on this time for all ports, and it # became used by railways as well. # * In 1918 the Central Observatory proposed dividing China into # five time zones (see below for details). This caught on # at first only in coastal areas observing UT+8. # * During WWII all of China was in theory was at UT+7. In practice # this was ignored in the west, and I presume was ignored in # Japanese-occupied territory. # * Japanese-occupied Manchuria was at UT+9, i.e., Japan time. # * The five-zone plan was resurrected after WWII and officially put into # place (with some modifications) in March 1948. It's not clear # how well it was observed in areas under Nationalist control. # * The People's Liberation Army used UT+8 during the civil war. # # An AP article "Shanghai Internat'l Area Little Changed" in the # Lewiston (ME) Daily Sun (1939-05-29), p 17, said "Even the time is # different - the occupied districts going by Tokyo time, an hour # ahead of that prevailing in the rest of Shanghai." Guess that the # Xujiahui Observatory was under French control and stuck with UT +08. # # In earlier versions of this file, China had many separate Zone entries, but # this was based on what were apparently incorrect data in Shanks & Pottenger. # This has now been simplified to the two entries Asia/Shanghai and # Asia/Urumqi, with the others being links for backward compatibility. # Proposed in 1918 and theoretically in effect until 1949 (although in practice # mainly observed in coastal areas), the five zones were: # # Changbai Time ("Long-white Time", Long-white = Heilongjiang area) UT +08:30 # Now part of Asia/Shanghai; its pre-1970 times are not recorded here. # Heilongjiang (except Mohe county), Jilin # # Zhongyuan Time ("Central plain Time") UT +08 # Now part of Asia/Shanghai. # most of China # Milne gives 8:05:43.2 for Xujiahui Observatory time.... # Guo says Shanghai switched to UT +08 "from the end of the 19th century". # # Long-shu Time (probably as Long and Shu were two names of the area) UT +07 # Now part of Asia/Shanghai; its pre-1970 times are not recorded here. # Guangxi, Guizhou, Hainan, Ningxia, Sichuan, Shaanxi, and Yunnan; # most of Gansu; west Inner Mongolia; east Qinghai; and the Guangdong # counties Deqing, Enping, Kaiping, Luoding, Taishan, Xinxing, # Yangchun, Yangjiang, Yu'nan, and Yunfu. # # Xin-zang Time ("Xinjiang-Tibet Time") UT +06 # This region is now part of either Asia/Urumqi or Asia/Shanghai with # current boundaries uncertain; times before 1970 for areas that # disagree with Ürümqi or Shanghai are not recorded here. # The Gansu counties Aksay, Anxi, Dunhuang, Subei; west Qinghai; # the Guangdong counties Xuwen, Haikang, Suixi, Lianjiang, # Zhanjiang, Wuchuan, Huazhou, Gaozhou, Maoming, Dianbai, and Xinyi; # east Tibet, including Lhasa, Chamdo, Shigaise, Jimsar, Shawan and Hutubi; # east Xinjiang, including Ürümqi, Turpan, Karamay, Korla, Minfeng, Jinghe, # Wusu, Qiemo, Xinyan, Wulanwusu, Jinghe, Yumin, Tacheng, Tuoli, Emin, # Shihezi, Changji, Yanqi, Heshuo, Tuokexun, Tulufan, Shanshan, Hami, # Fukang, Kuitun, Kumukuli, Miquan, Qitai, and Turfan. # # Kunlun Time UT +05:30 # This region is now in the same status as Xin-zang Time (see above). # West Tibet, including Pulan, Aheqi, Shufu, Shule; # West Xinjiang, including Aksu, Atushi, Yining, Hetian, Cele, Luopu, Nileke, # Zhaosu, Tekesi, Gongliu, Chabuchaer, Huocheng, Bole, Pishan, Suiding, # and Yarkand. # From Luther Ma (2009-10-17): # Almost all (>99.9%) ethnic Chinese (properly ethnic Han) living in # Xinjiang use Chinese Standard Time. Some are aware of Xinjiang time, # but have no need of it. All planes, trains, and schools function on # what is called "Beijing time." When Han make an appointment in Chinese # they implicitly use Beijing time. # # On the other hand, ethnic Uyghurs, who make up about half the # population of Xinjiang, typically use "Xinjiang time" which is two # hours behind Beijing time, or UT +06. The government of the Xinjiang # Uyghur Autonomous Region, (XAUR, or just Xinjiang for short) as well as # local governments such as the Ürümqi city government use both times in # publications, referring to what is popularly called Xinjiang time as # "Ürümqi time." When Uyghurs make an appointment in the Uyghur language # they almost invariably use Xinjiang time. # # (Their ethnic Han compatriots would typically have no clue of its # widespread use, however, because so extremely few of them are fluent in # Uyghur, comparable to the number of Anglo-Americans fluent in Navajo.) # # (...As with the rest of China there was a brief interval ending in 1990 # or 1991 when summer time was in use. The confusion was severe, with # the province not having dual times but four times in use at the same # time. Some areas remained on standard Xinjiang time or Beijing time and # others moving their clocks ahead.) # From Luther Ma (2009-11-19): # With the risk of being redundant to previous answers these are the most common # English "transliterations" (w/o using non-English symbols): # # 1. Wulumuqi... # 2. Kashi... # 3. Urumqi... # 4. Kashgar... # ... # 5. It seems that Uyghurs in Ürümqi has been using Xinjiang since at least the # 1960's. I know of one Han, now over 50, who grew up in the surrounding # countryside and used Xinjiang time as a child. # # 6. Likewise for Kashgar and the rest of south Xinjiang I don't know of any # start date for Xinjiang time. # # Without having access to local historical records, nor the ability to legally # publish them, I would go with October 1, 1949, when Xinjiang became the Uyghur # Autonomous Region under the PRC. (Before that Uyghurs, of course, would also # not be using Beijing time, but some local time.) # From David Cochrane (2014-03-26): # Just a confirmation that Ürümqi time was implemented in Ürümqi on 1 Feb 1986: # https://content.time.com/time/magazine/article/0,9171,960684,00.html # From Luther Ma (2014-04-22): # I have interviewed numerous people of various nationalities and from # different localities in Xinjiang and can confirm the information in Guo's # report regarding Xinjiang, as well as the Time article reference by David # Cochrane. Whether officially recognized or not (and both are officially # recognized), two separate times have been in use in Xinjiang since at least # the Cultural Revolution: Xinjiang Time (XJT), aka Ürümqi Time or local time; # and Beijing Time. There is no confusion in Xinjiang as to which name refers # to which time. Both are widely used in the province, although in some # population groups might be use one to the exclusion of the other. The only # problem is that computers and smart phones list Ürümqi (or Kashgar) as # having the same time as Beijing. # From Paul Eggert (2014-06-30): # In the early days of the PRC, Tibet was given its own time zone (UT +06) # but this was withdrawn in 1959 and never reinstated; see Tubten Khétsun, # Memories of life in Lhasa under Chinese Rule, Columbia U Press, ISBN # 978-0231142861 (2008), translator's introduction by Matthew Akester, p x. # As this is before our 1970 cutoff, Tibet doesn't need a separate zone. # # Xinjiang Time is well-documented as being officially recognized. E.g., see # "The Working-Calendar for The Xinjiang Uygur Autonomous Region Government" # (2014-04-22). # Unfortunately, we have no good records of time in Xinjiang before 1986. # During the 20th century parts of Xinjiang were ruled by the Qing dynasty, # the Republic of China, various warlords, the First and Second East Turkestan # Republics, the Soviet Union, the Kuomintang, and the People's Republic of # China, and tracking down all these organizations' timekeeping rules would be # quite a trick. Approximate this lost history by a transition from LMT to # UT +06 at the start of 1928, the year of accession of the warlord Jin Shuren, # which happens to be the date given by Shanks & Pottenger (no doubt as a # guess) as the transition from LMT. Ignore the usage of +08 before # 1986-02-01 under the theory that the transition date to +08 is unknown and # that the sort of users who prefer Asia/Urumqi now typically ignored the # +08 mandate back then. # Zone NAME STDOFF RULES FORMAT [UNTIL] # Beijing time, used throughout China; represented by Shanghai. #STDOFF 8:05:43.2 Zone Asia/Shanghai 8:05:43 - LMT 1901 8:00 Shang C%sT 1949 May 28 8:00 PRC C%sT # Xinjiang time, used by many in western China; represented by Ürümqi / Ürümchi # / Wulumuqi. (Please use Asia/Shanghai if you prefer Beijing time.) Zone Asia/Urumqi 5:50:20 - LMT 1928 6:00 - %z # Hong Kong # Milne gives 7:36:41.7. # From Lee Yiu Chung (2009-10-24): # I found there are some mistakes for the...DST rule for Hong # Kong. [According] to the DST record from Hong Kong Observatory (actually, # it is not [an] observatory, but the official meteorological agency of HK, # and also serves as the official timing agency), there are some missing # and incorrect rules. Although the exact switch over time is missing, I # think 3:30 is correct. # From Phake Nick (2018-10-27): # According to Singaporean newspaper # http://eresources.nlb.gov.sg/newspapers/Digitised/Article/singfreepresswk19041102-1.2.37 # the day that Hong Kong start using GMT+8 should be Oct 30, 1904. # # From Paul Eggert (2018-11-17): # Hong Kong had a time ball near the Marine Police Station, Tsim Sha Tsui. # "The ball was raised manually each day and dropped at exactly 1pm # (except on Sundays and Government holidays)." # Dyson AD. From Time Ball to Atomic Clock. Hong Kong Government. 1983. # # "From 1904 October 30 the time-ball at Hong Kong has been dropped by order # of the Governor of the Colony at 17h 0m 0s G.M.T., which is 23m 18s.14 in # advance of 1h 0m 0s of Hong Kong mean time." # Hollis HP. Universal Time, Longitudes, and Geodesy. Mon Not R Astron Soc. # 1905-02-10;65(4):405-6. https://doi.org/10.1093/mnras/65.4.382 # # From Joseph Myers (2018-11-18): # An astronomer before 1925 referring to GMT would have been using the old # astronomical convention where the day started at noon, not midnight. # # From Steve Allen (2018-11-17): # Meteorological Observations made at the Hongkong Observatory in the year 1904 # page 4 # ... the log of drop times in Table II shows that on Sunday 1904-10-30 the # ball was dropped. So that looks like a special case drop for the sake # of broadcasting the new local time. # # From Phake Nick (2018-11-18): # According to The Hong Kong Weekly Press, 1904-10-29, p.324, the # governor of Hong Kong at the time stated that "We are further desired to # make it known that the change will be effected by firing the gun and by the # dropping of the Ball at 23min. 18sec. before one." # From Paul Eggert (2018-11-18): # See for this; unfortunately Flash is required. # From Phake Nick (2018-10-26): # I went to check microfilm records stored at Hong Kong Public Library.... # on September 30 1941, according to Ta Kung Pao (Hong Kong edition), it was # stated that fallback would occur on the next day (the 1st)'s "03:00 am (Hong # Kong Time 04:00 am)" and the clock will fall back for a half hour. (03:00 # probably refer to the time commonly used in mainland China at the time given # the paper's background) ... the sunrise/sunset time given by South China # Morning Post for October 1st was indeed moved by half an hour compares to # before. After that, in December, the battle to capture Hong Kong started and # the library doesn't seems to have any record stored about press during that # period of time. Some media resumed publication soon after that within the # same month, but there were not much information about time there. Later they # started including a radio program guide when they restored radio service, # explicitly mentioning it use Tokyo standard time, and later added a note # saying it's half an hour ahead of the old Hong Kong standard time, and it # also seems to indicate that Hong Kong was not using GMT+8 when it was # captured by Japan. # # Image of related sections on newspaper: # * 1941-09-30, Ta Kung Pao (Hong Kong), "Winter Time start tomorrow". # https://i.imgur.com/6waY51Z.jpg (Chinese) # * 1941-09-29, South China Morning Post, Information on sunrise/sunset # time and other things for September 30 and October 1. # https://i.imgur.com/kCiUR78.jpg # * 1942-02-05. The Hong Kong News, Radio Program Guide. # https://i.imgur.com/eVvDMzS.jpg # * 1941-06-14. Hong Kong Daily Press, Daylight Saving from 3am Tomorrow. # https://i.imgur.com/05KkvtC.png # * 1941-09-30, Hong Kong Daily Press, Winter Time Warning. # https://i.imgur.com/dge4kFJ.png # From Paul Eggert (2019-07-11): # "Hong Kong winter time" is considered to be daylight saving. # "Hong Kong had adopted daylight saving on June 15 as a wartime measure, # clocks moving forward one hour until October 1, when they would be put back # by just half an hour for 'Hong Kong Winter time', so that daylight saving # operated year round." -- Low Z. The longest day: when wartime Hong Kong # introduced daylight saving. South China Morning Post. 2019-06-28. # https://www.scmp.com/magazines/post-magazine/short-reads/article/3016281/longest-day-when-wartime-hong-kong-introduced # From P Chan (2018-12-31): # * According to the Hong Kong Daylight-Saving Regulations, 1941, the # 1941 spring-forward transition was at 03:00. # http://sunzi.lib.hku.hk/hkgro/view/g1941/304271.pdf # http://sunzi.lib.hku.hk/hkgro/view/g1941/305516.pdf # * According to some articles from South China Morning Post, +08 was # resumed on 1945-11-18 at 02:00. # https://i.imgur.com/M2IsZ3c.png # https://i.imgur.com/iOPqrVo.png # https://i.imgur.com/fffcGDs.png # * Some newspapers ... said the 1946 spring-forward transition was on # 04-21 at 00:00. The Kung Sheung Evening News 1946-04-20 (Chinese) # https://i.imgur.com/ZSzent0.png # https://mmis.hkpl.gov.hk///c/portal/cover?c=QF757YsWv5%2FH7zGe%2FKF%2BFLYsuqGhRBfe p.4 # The Kung Sheung Daily News 1946-04-21 (Chinese) # https://i.imgur.com/7ecmRlcm.png # https://mmis.hkpl.gov.hk///c/portal/cover?c=QF757YsWv5%2BQBGt1%2BwUj5qG2GqtwR3Wh p.4 # * According to the Summer Time Ordinance (1946), the fallback # transitions between 1946 and 1952 were at 03:30 Standard Time (+08) # http://oelawhk.lib.hku.hk/archive/files/bb74b06a74d5294620a15de560ab33c6.pdf # * Some other laws and regulations related to DST from 1953 to 1979 # Summer Time Ordinance 1953 # https://i.imgur.com/IOlJMav.jpg # Summer Time (Amendment) Ordinance 1965 # https://i.imgur.com/8rofeLa.jpg # Interpretation and General Clauses Ordinance (1966) # https://i.imgur.com/joy3msj.jpg # Emergency (Summer Time) Regulation 1973 # Interpretation and General Clauses (Amendment) Ordinance 1977 # https://i.imgur.com/RaNqnc4.jpg # Resolution of the Legislative Council passed on 9 May 1979 # https://www.legco.gov.hk/yr78-79/english/lc_sitg/hansard/h790509.pdf#page=39 # From Paul Eggert (2020-04-15): # Here are the dates given at # https://www.hko.gov.hk/en/gts/time/Summertime.htm # as of 2020-02-10: # Year Period # 1941 15 Jun to 30 Sep # 1942 Whole year # 1943 Whole year # 1944 Whole year # 1945 Whole year # 1946 20 Apr to 1 Dec # 1947 13 Apr to 30 Nov # 1948 2 May to 31 Oct # 1949 3 Apr to 30 Oct # 1950 2 Apr to 29 Oct # 1951 1 Apr to 28 Oct # 1952 6 Apr to 2 Nov # 1953 5 Apr to 1 Nov # 1954 21 Mar to 31 Oct # 1955 20 Mar to 6 Nov # 1956 18 Mar to 4 Nov # 1957 24 Mar to 3 Nov # 1958 23 Mar to 2 Nov # 1959 22 Mar to 1 Nov # 1960 20 Mar to 6 Nov # 1961 19 Mar to 5 Nov # 1962 18 Mar to 4 Nov # 1963 24 Mar to 3 Nov # 1964 22 Mar to 1 Nov # 1965 18 Apr to 17 Oct # 1966 17 Apr to 16 Oct # 1967 16 Apr to 22 Oct # 1968 21 Apr to 20 Oct # 1969 20 Apr to 19 Oct # 1970 19 Apr to 18 Oct # 1971 18 Apr to 17 Oct # 1972 16 Apr to 22 Oct # 1973 22 Apr to 21 Oct # 1973/74 30 Dec 73 to 20 Oct 74 # 1975 20 Apr to 19 Oct # 1976 18 Apr to 17 Oct # 1977 Nil # 1978 Nil # 1979 13 May to 21 Oct # 1980 to Now Nil # The page does not give times of day for transitions, # or dates for the 1942 and 1945 transitions. # The Japanese occupation of Hong Kong began 1941-12-25. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule HK 1946 only - Apr 21 0:00 1:00 S Rule HK 1946 only - Dec 1 3:30s 0 - Rule HK 1947 only - Apr 13 3:30s 1:00 S Rule HK 1947 only - Nov 30 3:30s 0 - Rule HK 1948 only - May 2 3:30s 1:00 S Rule HK 1948 1952 - Oct Sun>=28 3:30s 0 - Rule HK 1949 1953 - Apr Sun>=1 3:30 1:00 S Rule HK 1953 1964 - Oct Sun>=31 3:30 0 - Rule HK 1954 1964 - Mar Sun>=18 3:30 1:00 S Rule HK 1965 1976 - Apr Sun>=16 3:30 1:00 S Rule HK 1965 1976 - Oct Sun>=16 3:30 0 - Rule HK 1973 only - Dec 30 3:30 1:00 S Rule HK 1979 only - May 13 3:30 1:00 S Rule HK 1979 only - Oct 21 3:30 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] #STDOFF 7:36:41.7 Zone Asia/Hong_Kong 7:36:42 - LMT 1904 Oct 29 17:00u 8:00 - HKT 1941 Jun 15 3:00 8:00 1:00 HKST 1941 Oct 1 4:00 8:00 0:30 HKWT 1941 Dec 25 9:00 - JST 1945 Nov 18 2:00 8:00 HK HK%sT ############################################################################### # Taiwan # From smallufo (2010-04-03): # According to Taiwan's CWB [Central Weather Bureau], # http://www.cwb.gov.tw/V6/astronomy/cdata/summert.htm # Taipei has DST in 1979 between July 1st and Sep 30. # From Yu-Cheng Chuang (2013-07-12): # On Dec 28, 1895, the Meiji Emperor announced Ordinance No. 167 of # Meiji Year 28 "The clause about standard time", mentioned that # Taiwan and Penghu Islands, as well as Yaeyama and Miyako Islands # (both in Okinawa) adopt the Western Standard Time which is based on # 120E. The adoption began from Jan 1, 1896. The original text can be # found on Wikisource: # https://ja.wikisource.org/wiki/標準時ニ關スル件_(公布時) # ... This could be the first adoption of time zone in Taiwan, because # during the Qing Dynasty, it seems that there was no time zone # declared officially. # # Later, in the beginning of World War II, on Sep 25, 1937, the Showa # Emperor announced Ordinance No. 529 of Showa Year 12 "The clause of # revision in the ordinance No. 167 of Meiji year 28 about standard # time", in which abolished the adoption of Western Standard Time in # western islands (listed above), which means the whole Japan # territory, including later occupations, adopt Japan Central Time # (UT+9). The adoption began on Oct 1, 1937. The original text can # be found on Wikisource: # https://ja.wikisource.org/wiki/明治二�八年勅令第百六�七號標準時ニ關スル件中改正ノ件 # # That is, the time zone of Taipei switched to UT+9 on Oct 1, 1937. # From Yu-Cheng Chuang (2014-07-02): # I've found more evidence about when the time zone was switched from UT+9 # back to UT+8 after WW2. I believe it was on Sep 21, 1945. In a document # during Japanese era [1] in which the officer told the staff to change time # zone back to Western Standard Time (UT+8) on Sep 21. And in another # history page of National Cheng Kung University [2], on Sep 21 there is a # note "from today, switch back to Western Standard Time". From these two # materials, I believe that the time zone change happened on Sep 21. And # today I have found another monthly journal called "The Astronomical Herald" # from The Astronomical Society of Japan [3] in which it mentioned the fact # that: # # 1. Standard Time of the Country (Japan) was adopted on Jan 1, 1888, using # the time at 135E (GMT+9) # # 2. Standard Time of the Country was renamed to Central Standard Time, on Jan # 1, 1898, and on the same day, the new territories Taiwan and Penghu islands, # as well as Yaeyama and Miyako islands, adopted a new time zone called # Western Standard Time, which is in GMT+8. # # 3. Western Standard Time was deprecated on Sep 30, 1937. From then all the # territories of Japan adopted the same time zone, which is Central Standard # Time. # # [1] Academica Historica, Taiwan: # http://163.29.208.22:8080/govsaleShowImage/connect_img.php?s=00101738900090036&e=00101738900090037 # [2] Nat'l Cheng Kung University 70th Anniversary Special Site: # http://www.ncku.edu.tw/~ncku70/menu/001/01_01.htm # [3] Yukio Niimi, The Standard Time in Japan (1997), p.475: # http://www.asj.or.jp/geppou/archive_open/1997/pdf/19971001c.pdf # Yu-Cheng Chuang (2014-07-03): # I finally have found the real official gazette about changing back to # Western Standard Time on Sep 21 in Taiwan. It's Taiwan Governor-General # Bulletin No. 386 in Showa 20 years (1945), published on Sep 19, 1945. [1] ... # [It] abolishes Bulletin No. 207 in Showa 12 years (1937), which is a local # bulletin in Taiwan for that Ordinance No. 529. It also mentioned that 1am on # Sep 21, 1945 will be 12am on Sep 21. I think this bulletin is much more # official than the one I mentioned in my first mail, because it's from the # top-level government in Taiwan. If you're going to quote any resource, this # would be a good one. # [1] Taiwan Governor-General Gazette, No. 1018, Sep 19, 1945: # http://db2.th.gov.tw/db2/view/viewImg.php?imgcode=0072031018a&num=19&bgn=019&end=019&otherImg=&type=gener # From Yu-Cheng Chuang (2014-07-02): # In 1946, DST in Taiwan was from May 15 and ended on Sep 30. The info from # Central Weather Bureau website was not correct. # # Original Bulletin: # http://subtpg.tpg.gov.tw/og/image2.asp?f=03502F0AKM1AF # http://subtpg.tpg.gov.tw/og/image2.asp?f=0350300AKM1B0 (cont.) # # In 1947, DST in Taiwan was expanded to Oct 31. There is a backup of that # telegram announcement from Taiwan Province Government: # # http://subtpg.tpg.gov.tw/og/image2.asp?f=0360310AKZ431 # # Here is a brief translation: # # The Summer Time this year is adopted from midnight Apr 15 until Sep 20 # midnight. To save (energy?) consumption, we're expanding Summer Time # adoption till Oct 31 midnight. # # The Central Weather Bureau website didn't mention that, however it can # be found from historical government announcement database. # From Paul Eggert (2014-07-03): # As per Yu-Cheng Chuang, say that Taiwan was at UT +09 from 1937-10-01 # until 1945-09-21 at 01:00, overriding Shanks & Pottenger. # Likewise, use Yu-Cheng Chuang's data for DST in Taiwan. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Taiwan 1946 only - May 15 0:00 1:00 D Rule Taiwan 1946 only - Oct 1 0:00 0 S Rule Taiwan 1947 only - Apr 15 0:00 1:00 D Rule Taiwan 1947 only - Nov 1 0:00 0 S Rule Taiwan 1948 1951 - May 1 0:00 1:00 D Rule Taiwan 1948 1951 - Oct 1 0:00 0 S Rule Taiwan 1952 only - Mar 1 0:00 1:00 D Rule Taiwan 1952 1954 - Nov 1 0:00 0 S Rule Taiwan 1953 1959 - Apr 1 0:00 1:00 D Rule Taiwan 1955 1961 - Oct 1 0:00 0 S Rule Taiwan 1960 1961 - Jun 1 0:00 1:00 D Rule Taiwan 1974 1975 - Apr 1 0:00 1:00 D Rule Taiwan 1974 1975 - Oct 1 0:00 0 S Rule Taiwan 1979 only - Jul 1 0:00 1:00 D Rule Taiwan 1979 only - Oct 1 0:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] # Taipei or Taibei or T'ai-pei Zone Asia/Taipei 8:06:00 - LMT 1896 Jan 1 8:00 - CST 1937 Oct 1 9:00 - JST 1945 Sep 21 1:00 8:00 Taiwan C%sT # Macau (Macao, Aomen) # # From P Chan (2018-05-10): # * LegisMac # http://legismac.safp.gov.mo/legismac/descqry/Descqry.jsf?lang=pt # A database for searching titles of legal documents of Macau in # Chinese and Portuguese. The term "HOR�RIO DE VERÃO" can be used for # searching decrees about summer time. # * Archives of Macao # http://www.archives.gov.mo/en/bo/ # It contains images of old official gazettes. # * The Macao Meteorological and Geophysical Bureau have a page listing the # summer time history. But it is not complete and has some mistakes. # http://www.smg.gov.mo/smg/geophysics/e_t_Summer%20Time.htm # Macau adopted GMT+8 on 30 Oct 1904 to follow Hong Kong. Clocks were # advanced by 25 minutes and 50 seconds. Which means the LMT used was # +7:34:10. As stated in the "Portaria No. 204" dated 21 October 1904 # and published in the Official Gazette on 29 October 1904. # http://igallery.icm.gov.mo/Images/Archives/BO/MO_AH_PUB_BO_1904_10/MO_AH_PUB_BO_1904_10_00025_Grey.JPG # # Therefore the 1911 decree of Portugal did not change time in Macau. # # From LegisMac, here is a list of decrees that changed the time ... # [Decree Gazette-no. date; titles omitted in this quotation] # DIL 732 BOCM 51 1941.12.20 # DIL 764 BOCM 9S 1942.04.30 # DIL 781 BOCM 21 1942.10.10 # PT 3434 BOCM 8S 1943.04.17 # PT 3504 BOCM 20 1943.09.25 # PT 3843 BOCM 39 1945.09.29 # PT 3961 BOCM 17 1946.04.27 # PT 4026 BOCM 39 1946.09.28 # PT 4153 BOCM 16 1947.04.10 # PT 4271 BOCM 48 1947.11.29 # PT 4374 BOCM 18 1948.05.01 # PT 4465 BOCM 44 1948.10.30 # PT 4590 BOCM 14 1949.04.02 # PT 4666 BOCM 44 1949.10.29 # PT 4771 BOCM 12 1950.03.25 # PT 4838 BOCM 43 1950.10.28 # PT 4946 BOCM 12 1951.03.24 # PT 5025 BO 43 1951.10.27 # PT 5149 BO 14 1952.04.05 # PT 5251 BO 43 1952.10.25 # PT 5366 BO 13 1953.03.28 # PT 5444 BO 44 1953.10.31 # PT 5540 BO 12 1954.03.20 # PT 5589 BO 44 1954.10.30 # PT 5676 BO 12 1955.03.19 # PT 5739 BO 45 1955.11.05 # PT 5823 BO 11 1956.03.17 # PT 5891 BO 44 1956.11.03 # PT 5981 BO 12 1957.03.23 # PT 6064 BO 43 1957.10.26 # PT 6172 BO 12 1958.03.22 # PT 6243 BO 43 1958.10.25 # PT 6341 BO 12 1959.03.21 # PT 6411 BO 43 1959.10.24 # PT 6514 BO 11 1960.03.12 # PT 6584 BO 44 1960.10.29 # PT 6721 BO 10 1961.03.11 # PT 6815 BO 43 1961.10.28 # PT 6947 BO 10 1962.03.10 # PT 7080 BO 43 1962.10.27 # PT 7218 BO 12 1963.03.23 # PT 7340 BO 43 1963.10.26 # PT 7491 BO 11 1964.03.14 # PT 7664 BO 43 1964.10.24 # PT 7846 BO 15 1965.04.10 # PT 7979 BO 42 1965.10.16 # PT 8146 BO 15 1966.04.09 # PT 8252 BO 41 1966.10.08 # PT 8429 BO 15 1967.04.15 # PT 8540 BO 41 1967.10.14 # PT 8735 BO 15 1968.04.13 # PT 8860 BO 41 1968.10.12 # PT 9035 BO 16 1969.04.19 # PT 9156 BO 42 1969.10.18 # PT 9328 BO 15 1970.04.11 # PT 9418 BO 41 1970.10.10 # PT 9587 BO 14 1971.04.03 # PT 9702 BO 41 1971.10.09 # PT 38-A/72 BO 14 1972.04.01 # PT 126-A/72 BO 41 1972.10.07 # PT 61/73 BO 14 1973.04.07 # PT 182/73 BO 40 1973.10.06 # PT 282/73 BO 51 1973.12.22 # PT 177/74 BO 41 1974.10.12 # PT 51/75 BO 15 1975.04.12 # PT 173/75 BO 41 1975.10.11 # PT 67/76/M BO 14 1976.04.03 # PT 169/76/M BO 41 1976.10.09 # PT 78/79/M BO 19 1979.05.12 # PT 166/79/M BO 42 1979.10.20 # Note that DIL 732 does not belong to "HOR�RIO DE VERÃO" according to # LegisMac.... Note that between 1942 and 1945, the time switched # between GMT+9 and GMT+10. Also in 1965 and 1965 the DST ended at 2:30am. # From Paul Eggert (2018-05-10): # The 1904 decree says that Macau changed from the meridian of # Fortaleza do Monte, presumably the basis for the 7:34:10 for LMT. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Macau 1942 1943 - Apr 30 23:00 1:00 - Rule Macau 1942 only - Nov 17 23:00 0 - Rule Macau 1943 only - Sep 30 23:00 0 S Rule Macau 1946 only - Apr 30 23:00s 1:00 D Rule Macau 1946 only - Sep 30 23:00s 0 S Rule Macau 1947 only - Apr 19 23:00s 1:00 D Rule Macau 1947 only - Nov 30 23:00s 0 S Rule Macau 1948 only - May 2 23:00s 1:00 D Rule Macau 1948 only - Oct 31 23:00s 0 S Rule Macau 1949 1950 - Apr Sat>=1 23:00s 1:00 D Rule Macau 1949 1950 - Oct lastSat 23:00s 0 S Rule Macau 1951 only - Mar 31 23:00s 1:00 D Rule Macau 1951 only - Oct 28 23:00s 0 S Rule Macau 1952 1953 - Apr Sat>=1 23:00s 1:00 D Rule Macau 1952 only - Nov 1 23:00s 0 S Rule Macau 1953 1954 - Oct lastSat 23:00s 0 S Rule Macau 1954 1956 - Mar Sat>=17 23:00s 1:00 D Rule Macau 1955 only - Nov 5 23:00s 0 S Rule Macau 1956 1964 - Nov Sun>=1 03:30 0 S Rule Macau 1957 1964 - Mar Sun>=18 03:30 1:00 D Rule Macau 1965 1973 - Apr Sun>=16 03:30 1:00 D Rule Macau 1965 1966 - Oct Sun>=16 02:30 0 S Rule Macau 1967 1976 - Oct Sun>=16 03:30 0 S Rule Macau 1973 only - Dec 30 03:30 1:00 D Rule Macau 1975 1976 - Apr Sun>=16 03:30 1:00 D Rule Macau 1979 only - May 13 03:30 1:00 D Rule Macau 1979 only - Oct Sun>=16 03:30 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Macau 7:34:10 - LMT 1904 Oct 30 8:00 - CST 1941 Dec 21 23:00 9:00 Macau %z 1945 Sep 30 24:00 8:00 Macau C%sT ############################################################################### # Cyprus # Milne says the Eastern Telegraph Company used 2:14:00. Stick with LMT. # IATA SSIM (1998-09) has Cyprus using EU rules for the first time. # From Paul Eggert (2016-09-09): # Yesterday's Cyprus Mail reports that Northern Cyprus followed Turkey's # lead and switched from +02/+03 to +03 year-round. # http://cyprus-mail.com/2016/09/08/two-time-zones-cyprus-turkey-will-not-turn-clocks-back-next-month/ # # From Even Scharning (2016-10-31): # Looks like the time zone split in Cyprus went through last night. # http://cyprus-mail.com/2016/10/30/cyprus-new-division-two-time-zones-now-reality/ # From Paul Eggert (2017-10-18): # Northern Cyprus will reinstate winter time on October 29, thus # staying in sync with the rest of Cyprus. See: Anastasiou A. # Cyprus to remain united in time. Cyprus Mail 2017-10-17. # https://cyprus-mail.com/2017/10/17/cyprus-remain-united-time/ # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Cyprus 1975 only - Apr 13 0:00 1:00 S Rule Cyprus 1975 only - Oct 12 0:00 0 - Rule Cyprus 1976 only - May 15 0:00 1:00 S Rule Cyprus 1976 only - Oct 11 0:00 0 - Rule Cyprus 1977 1980 - Apr Sun>=1 0:00 1:00 S Rule Cyprus 1977 only - Sep 25 0:00 0 - Rule Cyprus 1978 only - Oct 2 0:00 0 - Rule Cyprus 1979 1997 - Sep lastSun 0:00 0 - Rule Cyprus 1981 1998 - Mar lastSun 0:00 1:00 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Nicosia 2:13:28 - LMT 1921 Nov 14 2:00 Cyprus EE%sT 1998 Sep 2:00 EUAsia EE%sT Zone Asia/Famagusta 2:15:48 - LMT 1921 Nov 14 2:00 Cyprus EE%sT 1998 Sep 2:00 EUAsia EE%sT 2016 Sep 8 3:00 - %z 2017 Oct 29 1:00u 2:00 EUAsia EE%sT # Georgia # From Paul Eggert (1994-11-19): # Today's _Economist_ (p 60) reports that Georgia moved its clocks forward # an hour recently, due to a law proposed by Zurab Murvanidze, # an MP who went on a hunger strike for 11 days to force discussion about it! # We have no details, but we'll guess they didn't move the clocks back in fall. # # From Mathew Englander, quoting AP (1996-10-23 13:05-04): # Instead of putting back clocks at the end of October, Georgia # will stay on daylight savings time this winter to save energy, # President Eduard Shevardnadze decreed Wednesday. # # From the BBC via Joseph S. Myers (2004-06-27): # # Georgia moved closer to Western Europe on Sunday... The former Soviet # republic has changed its time zone back to that of Moscow. As a result it # is now just four hours ahead of Greenwich Mean Time, rather than five hours # ahead. The switch was decreed by the pro-Western president of Georgia, # Mikheil Saakashvili, who said the change was partly prompted by the process # of integration into Europe. # From Teimuraz Abashidze (2005-11-07): # Government of Georgia ... decided to NOT CHANGE daylight savings time on # [Oct.] 30, as it was done before during last more than 10 years. # Currently, we are in fact GMT +4:00, as before 30 October it was GMT # +3:00.... The problem is, there is NO FORMAL LAW or governmental document # about it. As far as I can find, I was told, that there is no document, # because we just DIDN'T ISSUE document about switching to winter time.... # I don't know what can be done, especially knowing that some years ago our # DST rules where changed THREE TIMES during one month. # Milne 1899 says Tbilisi (Tiflis) time was 2:59:05.7. # Byalokoz 1919 says Georgia was 2:59:11. # Go with Byalokoz. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Tbilisi 2:59:11 - LMT 1880 2:59:11 - TBMT 1924 May 2 # Tbilisi Mean Time 3:00 - %z 1957 Mar 4:00 RussiaAsia %z 1991 Mar 31 2:00s 3:00 RussiaAsia %z 1992 3:00 E-EurAsia %z 1994 Sep lastSun 4:00 E-EurAsia %z 1996 Oct lastSun 4:00 1:00 %z 1997 Mar lastSun 4:00 E-EurAsia %z 2004 Jun 27 3:00 RussiaAsia %z 2005 Mar lastSun 2:00 4:00 - %z # East Timor # From Tim Parenti (2024-07-01): # The 1912-01-01 transition occurred at 00:00 new time, per the 1911-05-24 # Portuguese decree (see Europe/Lisbon). A provision in article 5(c) of the # decree prescribed that Timor "will keep counting time in harmony with # neighboring foreign colonies, [for] as long as they do not adopt the time # that belongs to them in [the Washington Convention] system." # See Indonesia for the 1945 transition. # From João Carrascalão, brother of the former governor of East Timor, in # East Timor may be late for its millennium # (1999-12-26/31): # Portugal tried to change the time forward in 1974 because the sun # rises too early but the suggestion raised a lot of problems with the # Timorese and I still don't think it would work today because it # conflicts with their way of life. # From Paul Eggert (2000-12-04): # We don't have any record of the above attempt. # Most likely our records are incomplete, but we have no better data. # From Manoel de Almeida e Silva, Deputy Spokesman for the UN Secretary-General # http://www.hri.org/news/world/undh/2000/00-08-16.undh.html # (2000-08-16): # The Cabinet of the East Timor Transition Administration decided # today to advance East Timor's time by one hour. The time change, # which will be permanent, with no seasonal adjustment, will happen at # midnight on Saturday, September 16. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Dili 8:22:20 - LMT 1911 Dec 31 16:00u 8:00 - %z 1942 Feb 21 23:00 9:00 - %z 1976 May 3 8:00 - %z 2000 Sep 17 0:00 9:00 - %z # India # British astronomer Henry Park Hollis disliked India Standard Time's offset: # "A new time system has been proposed for India, Further India, and Burmah. # The scheme suggested is that the times of the meridians 5½ and 6½ hours # east of Greenwich should be adopted in these territories. No reason is # given why hourly meridians five hours and six hours east should not be # chosen; a plan which would bring the time of India into harmony with # that of almost the whole of the civilised world." # Hollis HP. Universal Time, Longitudes, and Geodesy. Mon Not R Astron Soc. # 1905-02-10;65(4):405-6. https://doi.org/10.1093/mnras/65.4.382 # From Ian P. Beacock, in "A brief history of (modern) time", The Atlantic # https://www.theatlantic.com/technology/archive/2015/12/the-creation-of-modern-time/421419/ # (2015-12-22): # In January 1906, several thousand cotton-mill workers rioted on the # outskirts of Bombay.... They were protesting the proposed abolition of # local time in favor of Indian Standard Time.... Journalists called this # dispute the "Battle of the Clocks." It lasted nearly half a century. # From Paul Eggert (2017-04-20): # Good luck trying to nail down old timekeeping records in India. # "... in the nineteenth century ... Madras Observatory took its magnetic # measurements on Göttingen time, its meteorological measurements on Madras # (local) time, dropped its time ball on Greenwich (ocean navigator's) time, # and distributed civil (local time)." -- Bartky IR. Selling the true time: # 19th-century timekeeping in america. Stanford U Press (2000), 247 note 19. # "A more potent cause of resistance to the general adoption of the present # standard time lies in the fact that it is Madras time. The citizen of # Bombay, proud of being 'primus in Indis' and of Calcutta, equally proud of # his city being the Capital of India, and - for a part of the year - the Seat # of the Supreme Government, alike look down on Madras, and refuse to change # the time they are using, for that of what they regard as a benighted # Presidency; while Madras, having for long given the standard time to the # rest of India, would resist the adoption of any other Indian standard in its # place." -- Oldham RD. On Time in India: a suggestion for its improvement. # Proceedings of the Asiatic Society of Bengal (April 1899), 49-55. # # "In 1870 ... Madras time - 'now used by the telegraph and regulated from the # only government observatory' - was suggested as a standard railway time, # first to be adopted on the Great Indian Peninsular Railway (GIPR).... # Calcutta, Bombay, and Karachi, were to be allowed to continue with their # local time for civil purposes." - Prasad R. Tracks of Change: Railways and # Everyday Life in Colonial India. Cambridge University Press (2016), 145. # # Reed S, Low F. The Indian Year Book 1936-37. Bennett, Coleman, pp 27-8. # https://archive.org/details/in.ernet.dli.2015.282212 # This lists +052110 as Madras local time used in railways, and says that on # 1906-01-01 railways and telegraphs in India switched to +0530. Some # municipalities retained their former time, and the time in Calcutta # continued to depend on whether you were at the railway station or at # government offices. Government time was at +055320 (according to Shanks) or # at +0554 (according to the Indian Year Book). Railway time is more # appropriate for our purposes, as it was better documented, it is what we do # elsewhere (e.g., Europe/London before 1880), and after 1906 it was # consistent in the region now identified by Asia/Kolkata. So, use railway # time for 1870-1941. Shanks is our only (and dubious) source for the # 1941-1945 data. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Kolkata 5:53:28 - LMT 1854 Jun 28 # Kolkata 5:53:20 - HMT 1870 # Howrah Mean Time? 5:21:10 - MMT 1906 Jan 1 # Madras local time 5:30 - IST 1941 Oct 5:30 1:00 %z 1942 May 15 5:30 - IST 1942 Sep 5:30 1:00 %z 1945 Oct 15 5:30 - IST # Since 1970 the following are like Asia/Kolkata: # Andaman Is # Lakshadweep (Laccadive, Minicoy and Amindivi Is) # Nicobar Is # Indonesia # # From Paul Eggert (2014-09-06): # The 1876 Report of the Secretary of the [US] Navy, p 306 says that Batavia # civil time was 7:07:12.5. # # From Gwillim Law (2001-05-28), overriding Shanks & Pottenger: # http://www.sumatera-inc.com/go_to_invest/about_indonesia.asp#standtime # says that Indonesia's time zones changed on 1988-01-01. Looking at some # time zone maps, I think that must refer to Western Borneo (Kalimantan Barat # and Kalimantan Tengah) switching from UTC+8 to UTC+7. # # From Paul Eggert (2007-03-10): # Here is another correction to Shanks & Pottenger. # JohnTWB writes that Japanese forces did not surrender control in # Indonesia until 1945-09-01 00:00 at the earliest (in Jakarta) and # other formal surrender ceremonies were September 9, 11, and 13, plus # September 12 for the regional surrender to Mountbatten in Singapore. # These would be the earliest possible times for a change. # Régimes horaires pour le monde entier, by Henri Le Corre, (Éditions # Traditionnelles, 1987, Paris) says that Java and Madura switched # from UT +09 to +07:30 on 1945-09-23, and gives 1944-09-01 for Jayapura # (Hollandia). For now, assume all Indonesian locations other than Jayapura # switched on 1945-09-23. # # From Paul Eggert (2013-08-11): # Normally the tz database uses English-language abbreviations, but in # Indonesia it's typical to use Indonesian-language abbreviations even # when writing in English. For example, see the English-language # summary published by the Time and Frequency Laboratory of the # Research Center for Calibration, Instrumentation and Metrology, # Indonesia, (2006-09-29). # The time zone abbreviations and UT offsets are: # # WIB - +07 - Waktu Indonesia Barat (Indonesia western time) # WITA - +08 - Waktu Indonesia Tengah (Indonesia central time) # WIT - +09 - Waktu Indonesia Timur (Indonesia eastern time) # # Zone NAME STDOFF RULES FORMAT [UNTIL] # Java, Sumatra #STDOFF 7:07:12.5 Zone Asia/Jakarta 7:07:12 - LMT 1867 Aug 10 # Shanks & Pottenger say the next transition was at 1924 Jan 1 0:13, # but this must be a typo. 7:07:12 - BMT 1923 Dec 31 16:40u # Batavia 7:20 - %z 1932 Nov 7:30 - %z 1942 Mar 23 9:00 - %z 1945 Sep 23 7:30 - %z 1948 May 8:00 - %z 1950 May 7:30 - %z 1964 7:00 - WIB # west and central Borneo Zone Asia/Pontianak 7:17:20 - LMT 1908 May 7:17:20 - PMT 1932 Nov # Pontianak MT 7:30 - %z 1942 Jan 29 9:00 - %z 1945 Sep 23 7:30 - %z 1948 May 8:00 - %z 1950 May 7:30 - %z 1964 8:00 - WITA 1988 Jan 1 7:00 - WIB # Sulawesi, Lesser Sundas, east and south Borneo Zone Asia/Makassar 7:57:36 - LMT 1920 7:57:36 - MMT 1932 Nov # Macassar MT 8:00 - %z 1942 Feb 9 9:00 - %z 1945 Sep 23 8:00 - WITA # Maluku Islands, West Papua, Papua Zone Asia/Jayapura 9:22:48 - LMT 1932 Nov 9:00 - %z 1944 Sep 1 9:30 - %z 1964 9:00 - WIT # Iran # From Roozbeh Pournader (2022-05-30): # Here's an order from the Cabinet to the rest of the government to switch to # Tehran time, which is mentioned to be already at +03:30: # https://qavanin.ir/Law/TreeText/180138 # Just in case that goes away, I also saved a copy at archive.org: # https://web.archive.org/web/20220530111940/https://qavanin.ir/Law/TreeText/180138 # Here's my translation: # # "Circular on Matching the Hours of Governmental and Official Circles # in Provinces # Approved 1314/03/22 [=1935-06-13] # According to the ruling of the Honorable Cabinet, it is ordered that from # now on in all internal provinces of the country, governmental and official # circles set their time to match Tehran time (three hours and half before # Greenwich).... # # I still haven't found out when Tehran itself switched to +03:30.... # # From Paul Eggert (2022-06-05): # Although the above says Tehran was at +03:30 before 1935-06-13, we don't # know when it switched to +03:30. For now, use 1935-06-13 as the switch date. # Although most likely wrong, we have no better info. # From Roozbeh Pournader (2022-06-01): # This is from Kayhan newspaper, one of the major Iranian newspapers, from # March 20, 1978, page 2: # # "Pull the clocks 60 minutes forward # As we informed before, from the fourth day of the month Farvardin of the # new year [=1978-03-24], clocks will be pulled forward, and people's daily # work and life program will start one hour earlier than the current program. # On the 1st day of the month Farvardin of this year [=1977-03-21], they had # pulled the clocks forward by one hour, but in the month of Mehr # [=1977-09-23], the clocks were pulled back by 30 minutes. # In this way, from the 4th day of the month Farvardin, clocks will be ahead # of the previous years by one hour and a half. # According to the new program, during the night of 4th of Farvardin, when # the midnight, meaning 24 o'clock is announced, the hands of the clock must # be pulled forward by one hour and thus consider midnight 1 o'clock in the # forenoon." # # This implies that in September 1977, when the daylight savings time was # done with, Iran didn't go back to +03:30, but immediately to +04:00. # # # This is from the major Iranian newspaper Ettela'at, dated [1978-08-03]..., # page 32. It looks like they decided to get the clocks back to +4:00 # just in time for Ramadan that year: # # "Tomorrow Night, Pull the Clocks Back by One Hour # At 1 o'clock in the forenoon of Saturday 14 Mordad [=1978-08-05], the # clocks will be pulled one hour back and instead of 1 o'clock in the # forenoon, Radio Iran will announce 24 o'clock. # This decision was made in the Cabinet of Ministers meeting of 25 Tir # [=1978-07-16], [...] # At the beginning of the year 2537 [=March 1978: Iran was using a different # year number for a few years then, based on the Coronation of Cyrus the # Great], the country's official time was pulled forward by one hour and now # the official time is one hour and a half ahead compared to last year, # because in Farvardin of last year [=March 1977], the official time was # pulled forward one hour and this continued until the second half of last # year [=September 1977] until in the second half of last year the official # time was pulled back half an hour and that half hour still remains." # # This matches the time of the true noon published in the newspapers, as they # clearly go from +05:00 to +04:00 after that date (which happened during a # long weekend in Iran). # From Roozbeh Pournader (2022-05-31): # [Movahedi S. Cultural preconceptions of time: Can we use operational time # to meddle in God's Time? Comp Stud Soc Hist. 1985;27(3):385-400] # https://www.jstor.org/stable/178704 # Here's the quotes from the paper: # 1. '"Iran's official time keeper moved the clock one hour forward as from # March 22, 1977 (Farvardin 2, 2536) to make maximum use of daylight and save # in energy consumption. Thus Iran joined such other countries as Britain in # observing what is known as 'daylight saving.' The proposal was originally # put forward by the Ministry of Energy, in no way having any influence on # observing religious ceremonies. Moving time one hour forward in summer # means that at 11:00 o'clock on March 21, the official time was set as # midnight March 22. Then September 24 will actually begin one hour later # than the end of September 23 [...]." Iran's time base thus continued to be # Greenwich Mean Time plus three and one-half hours (plus four and one-half # hours in summer).' # # The article sources this from Iran Almanac and Book of Facts, 1977, Tehran: # Echo of Iran, which is on Google Books at # https://www.google.com/books/edition/Iran_Almanac_and_Book_of_Facts/9ybVAAAAMAAJ. # (I confirmed it by searching for snippets.) # # 2. "After the fall of the shah, the revolutionary government returned to # daylight-saving time (DST) on 26 May 1979." # # This seems to have been announced just one day in advance, on 25 May 1979. # # The change in 1977 clearly seems to be the first daylight savings effort in # Iran. But the article doesn't mention what happened in 1978 (which was # still during the shah's government), or how things continued in 1979 # onwards (which was during the Islamic Republic). # From Francis Santoni (2022-06-01): # for Iran and 1977 the effective change is only 20 October # (UIT No. 143 17.XI.1977) and not 23 September (UIT No. 141 13.IX.1977). # UIT is the Operational Bulletin of International Telecommunication Union. # From Roozbeh Pournader (2025-03-18): # ... the exact time of Iran's transition from +0400 to +0330 ... was Friday # 1357/8/19 AP=1978-11-10. Here's a newspaper clip from the Ettela'at # newspaper, dated 1357/8/14 AP=1978-11-05, translated from Persian # (at https://w.wiki/DUEY): # Following the government's decision about returning the official time # to the previous status, the spokesperson for the Ministry of Energy # announced today: At the hour 24 of Friday 19th of Aban (=1978-11-10), # the country's time will be pulled back half an hour. # # From Roozbeh Pournader (2003-03-15): # This is an English translation of what I just found (originally in Persian). # The Gregorian dates in brackets are mine: # # Official Newspaper No. 13548-1370/6/25 [1991-09-16] # No. 16760/T233 H 1370/6/10 [1991-09-01] # # The Rule About Change of the Official Time of the Country # # The Board of Ministers, in the meeting dated 1370/5/23 [1991-08-14], # based on the suggestion number 2221/D dated 1370/4/22 [1991-07-13] # of the Country's Organization for Official and Employment Affairs, # and referring to the law for equating the working hours of workers # and officers in the whole country dated 1359/4/23 [1980-07-14], and # for synchronizing the official times of the country, agreed that: # # The official time of the country will should move forward one hour # at the 24[:00] hours of the first day of Farvardin and should return # to its previous state at the 24[:00] hours of the 30th day of # Shahrivar. # # First Deputy to the President - Hassan Habibi # # From personal experience, that agrees with what has been followed # for at least the last 5 years. Before that, for a few years, the # date used was the first Thursday night of Farvardin and the last # Thursday night of Shahrivar, but I can't give exact dates.... # # From Roozbeh Pournader (2005-04-05): # The text of the Iranian law, in effect since 1925, clearly mentions # that the true solar year is the measure, and there is no arithmetic # leap year calculation involved. There has never been any serious # plan to change that law.... # # From N.F. Hase (2026-07-26): # Ettela'at (Tehran daily) printed a daily panel headed "taqvim-e # Ettela'at / sa'at-ha-ye shar'i", attributed to Dr. Riyazi Kermani. # Each panel gives today's maghrib followed by fajr, sunrise, solar noon # and maghrib for the next day, together with the Iranian, Hijri and # Gregorian dates.... 26 May 1979 (5 Khord�d 1358) # https://commons.wikimedia.org/wiki/File:Ettelaat13580305.pdf # ... That also matches Movahedi's wording, that the government # returned to DST on 26 May, and the note in the 2022 thread that the # change was announced one day in advance on 25 May. # # From Paul Eggert (2026-07-27): # Go with Pournader for 1935 through spring 1979 (with the last # transition clarified by Hase), and for timestamps after August 1991; # go with Shanks & Pottenger for other timestamps. # Go with Santoni's citation of the UIT for fall 1977, as 20 October 1977 # is 28 Mehr 1356, consistent with the "Mehr" in Pournader's source. # Assume that the UIT's "1930" is UTC, i.e., 24:00 local time. # # From Oscar van Vlijmen (2005-03-30), writing about future # discrepancies between cal-persia and the Iranian calendar: # For 2091 solar-longitude-after yields 2091-03-20 08:40:07.7 UT for # the vernal equinox and that gets so close to 12:00 some local # Iranian time that the definition of the correct location needs to be # known exactly, amongst other factors. 2157 is even closer: # 2157-03-20 08:37:15.5 UT. But the Gregorian year 2025 should give # no interpretation problem whatsoever. By the way, another instant # in the near future where there will be a discrepancy between # arithmetical and astronomical Iranian calendars will be in 2058: # vernal equinox on 2058-03-20 09:03:05.9 UT. The Java version of # Reingold's/Dershowitz' calculator gives correctly the Gregorian date # 2058-03-21 for 1 Farvardin 1437 (astronomical). # # From Steffen Thorsen (2006-03-22): # Several of my users have reported that Iran will not observe DST anymore: # http://www.irna.ir/en/news/view/line-17/0603193812164948.htm # # From Reuters (2007-09-16), with a heads-up from Jesper Nørgaard Welen: # ... the Guardian Council ... approved a law on Sunday to re-introduce # daylight saving time ... # https://uk.reuters.com/article/oilRpt/idUKBLA65048420070916 # # From Roozbeh Pournader (2007-11-05): # This is quoted from Official Gazette of the Islamic Republic of # Iran, Volume 63, No. 18242, dated Tuesday 1386/6/24 # [2007-10-16]. I am doing the best translation I can:... # The official time of the country will be moved forward for one hour # on the 24 hours of the first day of the month of Farvardin and will # be changed back to its previous state on the 24 hours of the # thirtieth day of Shahrivar. # # From Ali Mirjamali (2022-05-10): # Official IR News Agency announcement: irna.ir/xjJ3TT # ... # Highlights: DST will be cancelled for the next Iranian year 1402 # (i.e 2023-March-21) and forthcoming years. # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S # Work around a bug in zic 2022a and earlier. Rule Iran 1910 only - Jan 1 0:00 0 - # Rule Iran 1977 only - Mar 21 23:00 1:00 - Rule Iran 1977 only - Oct 20 24:00 0 - Rule Iran 1978 only - Mar 24 24:00 1:00 - Rule Iran 1978 only - Aug 5 1:00 0 - Rule Iran 1979 only - May 26 0:00 1:00 - Rule Iran 1979 only - Sep 18 24:00 0 - Rule Iran 1980 only - Mar 20 24:00 1:00 - Rule Iran 1980 only - Sep 22 24:00 0 - Rule Iran 1991 only - May 2 24:00 1:00 - Rule Iran 1992 1995 - Mar 21 24:00 1:00 - Rule Iran 1991 1995 - Sep 21 24:00 0 - Rule Iran 1996 only - Mar 20 24:00 1:00 - Rule Iran 1996 only - Sep 20 24:00 0 - Rule Iran 1997 1999 - Mar 21 24:00 1:00 - Rule Iran 1997 1999 - Sep 21 24:00 0 - Rule Iran 2000 only - Mar 20 24:00 1:00 - Rule Iran 2000 only - Sep 20 24:00 0 - Rule Iran 2001 2003 - Mar 21 24:00 1:00 - Rule Iran 2001 2003 - Sep 21 24:00 0 - Rule Iran 2004 only - Mar 20 24:00 1:00 - Rule Iran 2004 only - Sep 20 24:00 0 - Rule Iran 2005 only - Mar 21 24:00 1:00 - Rule Iran 2005 only - Sep 21 24:00 0 - Rule Iran 2008 only - Mar 20 24:00 1:00 - Rule Iran 2008 only - Sep 20 24:00 0 - Rule Iran 2009 2011 - Mar 21 24:00 1:00 - Rule Iran 2009 2011 - Sep 21 24:00 0 - Rule Iran 2012 only - Mar 20 24:00 1:00 - Rule Iran 2012 only - Sep 20 24:00 0 - Rule Iran 2013 2015 - Mar 21 24:00 1:00 - Rule Iran 2013 2015 - Sep 21 24:00 0 - Rule Iran 2016 only - Mar 20 24:00 1:00 - Rule Iran 2016 only - Sep 20 24:00 0 - Rule Iran 2017 2019 - Mar 21 24:00 1:00 - Rule Iran 2017 2019 - Sep 21 24:00 0 - Rule Iran 2020 only - Mar 20 24:00 1:00 - Rule Iran 2020 only - Sep 20 24:00 0 - Rule Iran 2021 2022 - Mar 21 24:00 1:00 - Rule Iran 2021 2022 - Sep 21 24:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Tehran 3:25:44 - LMT 1916 3:25:44 - TMT 1935 Jun 13 # Tehran Mean Time 3:30 Iran %z 1977 Oct 20 24:00 4:00 Iran %z 1978 Nov 10 24:00 3:30 Iran %z # Iraq # # From Jonathan Lennox (2000-06-12): # An article in this week's Economist ("Inside the Saddam-free zone", p. 50 in # the U.S. edition) on the Iraqi Kurds contains a paragraph: # "The three northern provinces ... switched their clocks this spring and # are an hour ahead of Baghdad." # # But Rives McDow (2000-06-18) quotes a contact in Iraqi-Kurdistan as follows: # In the past, some Kurdish nationalists, as a protest to the Iraqi # Government, did not adhere to daylight saving time. They referred # to daylight saving as Saddam time. But, as of today, the time zone # in Iraqi-Kurdistan is on standard time with Baghdad, Iraq. # # So we'll ignore the Economist's claim. # From Steffen Thorsen (2008-03-10): # The cabinet in Iraq abolished DST last week, according to the following # news sources (in Arabic): # http://www.aljeeran.net/wesima_articles/news-20080305-98602.html # http://www.aswataliraq.info/look/article.tpl?id=2047&IdLanguage=17&IdPublication=4&NrArticle=71743&NrIssue=1&NrSection=10 # # We have published a short article in English about the change: # https://www.timeanddate.com/news/time/iraq-dumps-daylight-saving.html # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Iraq 1982 only - May 1 0:00 1:00 - Rule Iraq 1982 1984 - Oct 1 0:00 0 - Rule Iraq 1983 only - Mar 31 0:00 1:00 - Rule Iraq 1984 1985 - Apr 1 0:00 1:00 - Rule Iraq 1985 1990 - Sep lastSun 1:00s 0 - Rule Iraq 1986 1990 - Mar lastSun 1:00s 1:00 - # IATA SSIM (1991/1996) says Apr 1 12:01am UTC; guess the ':01' is a typo. # Shanks & Pottenger say Iraq did not observe DST 1992/1997; ignore this. # Rule Iraq 1991 2007 - Apr 1 3:00s 1:00 - Rule Iraq 1991 2007 - Oct 1 3:00s 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Baghdad 2:57:40 - LMT 1890 2:57:36 - BMT 1918 # Baghdad Mean Time? 3:00 - %z 1982 May 3:00 Iraq %z ############################################################################### # Israel # For more info about the motivation for DST in Israel, see: # Barak Y. Israel's Daylight Saving Time controversy. Israel Affairs. # 2020-08-11. https://doi.org/10.1080/13537121.2020.1806564 # From Ephraim Silverberg (2001-01-11): # # I coined "IST/IDT" circa 1988. Until then there were three # different abbreviations in use: # # JST Jerusalem Standard Time [Danny Braniss, Hebrew University] # IZT Israel Zonal (sic) Time [Prof. Haim Papo, Technion] # EEST Eastern Europe Standard Time [used by almost everyone else] # # Since timezones should be called by country and not capital cities, # I ruled out JST. As Israel is in Asia Minor and not Eastern Europe, # EEST was equally unacceptable. Since "zonal" was not compatible with # any other timezone abbreviation, I felt that 'IST' was the way to go # and, indeed, it has received almost universal acceptance in timezone # settings in Israeli computers. # # In any case, I am happy to share timezone abbreviations with India, # high on my favorite-country list (and not only because my wife's # family is from India). # From P Chan (2020-10-27), with corrections: # # 1940-1946 Supplement No. 2 to the Palestine Gazette # # issue page Order No. dated start end note # 1 1010 729 67 of 1940 1940-05-22 1940-05-31* 1940-09-30* revoked by #2 # 2 1013 758 73 of 1940 1940-05-31 1940-05-31 1940-09-30 # 3 1055 1574 196 of 1940 1940-11-06 1940-11-16 1940-12-31 # 4 1066 1811 208 of 1940 1940-12-17 1940-12-31 1941-12-31 # 5 1156 1967 116 of 1941 1941-12-16 1941-12-31 1942-12-31* amended by #6 # 6 1228 1608 86 of 1942 1942-10-14 1941-12-31 1942-10-31 # 7 1256 279 21 of 1943 1943-03-18 1943-03-31 1943-10-31 # 8 1323 249 19 of 1944 1944-03-13 1944-03-31 1944-10-31 # 9 1402 328 20 of 1945 1945-04-05 1945-04-15 1945-10-31 #10 1487 596 14 of 1946 1946-04-04 1946-04-15 1946-10-31 # # 1948 Iton Rishmi (Official Gazette of the Provisional Government) # # issue page dated start end #11 2 7 1948-05-20 1948-05-22 1948-10-31* # ^This moved timezone to +04, replaced by #12 from 1948-08-31 24:00 GMT. #12 17 (Annex B) 84 1948-08-22 1948-08-31 1948-10-31 # # 1949-2000 Kovetz HaTakanot (Collection of Regulations) # # issue page dated start end note #13 6 133 1949-03-23 1949-04-30 1949-10-31 #14 80 755 1950-03-17 1950-04-15 1950-09-14 #15 164 782 1951-03-22 1951-03-31 1951-09-29* amended by #16 #16 206 1940 1951-09-23 ---------- 1951-10-22* amended by #17 #17 212 78 1951-10-19 ---------- 1951-11-10 #18 254 652 1952-03-03 1952-04-19 1952-09-27* amended by #19 #19 300 11 1952-09-15 ---------- 1952-10-18 #20 348 817 1953-03-03 1953-04-11 1953-09-12 #21 420 385 1954-02-17 1954-06-12 1954-09-11 #22 497 548 1955-01-14 1955-06-11 1955-09-10 #23 591 608 1956-03-12 1956-06-02 1956-09-29 #24 680 957 1957-02-08 1957-04-27 1957-09-21 #25 3192 1418 1974-06-28 1974-07-06 1974-10-12 #26 3322 1389 1975-04-03 1975-04-19 1975-08-30 #27 4146 2089 1980-07-15 1980-08-02 1980-09-13 #28 4604 1081 1984-02-22 1984-05-05* 1984-08-25* revoked by #29 #29 4619 1312 1984-04-06 1984-05-05 1984-08-25 #30 4744 475 1984-12-23 1985-04-13 1985-09-14* amended by #31 #31 4851 1848 1985-08-18 ---------- 1985-08-31 #32 4932 899 1986-04-22 1986-05-17 1986-09-06 #33 5013 580 1987-02-15 1987-04-18* 1987-08-22* revoked by #34 #34 5021 744 1987-03-30 1987-04-14 1987-09-12 #35 5096 659 1988-02-14 1988-04-09 1988-09-03 #36 5167 514 1989-02-03 1989-04-29 1989-09-02 #37 5248 375 1990-01-23 1990-03-24 1990-08-25 #38 5335 612 1991-02-10 1991-03-09* 1991-08-31 amended by #39 # 1992-03-28 1992-09-05 #39 5339 709 1991-03-04 1991-03-23 ---------- #40 5506 503 1993-02-18 1993-04-02 1993-09-05 # 1994-04-01 1994-08-28 # 1995-03-31 1995-09-03 #41 5731 438 1996-01-01 1996-03-14 1996-09-15 # 1997-03-13* 1997-09-18* overridden by 1997 Temp Prov # 1998-03-19* 1998-09-17* revoked by #42 #42 5853 1243 1997-09-18 1998-03-19 1998-09-05 #43 5937 77 1998-10-18 1999-04-02 1999-09-03 # 2000-04-14* 2000-09-15* revoked by #44 # 2001-04-13* 2001-09-14* revoked by #44 #44 6024 39 2000-03-14 2000-04-14 2000-10-22* overridden by 2000 Temp Prov # 2001-04-06* 2001-10-10* overridden by 2000 Temp Prov # 2002-03-29* 2002-10-29* overridden by 2000 Temp Prov # # These are laws enacted by the Knesset since the Minister could only alter the # transition dates at least six months in advanced under the 1992 Law. # dated start end # 1997 Temporary Provisions 1997-03-06 1997-03-20 1997-09-13 # 2000 Temporary Provisions 2000-07-28 ---------- 2000-10-06 # 2001-04-09 2001-09-24 # 2002-03-29 2002-10-07 # 2003-03-28 2003-10-03 # 2004-04-07 2004-09-22 # Note: # Transition times in 1940-1957 (#1-#24) were midnight GMT, # in 1974-1998 (#25-#42 and the 1997 Temporary Provisions) were midnight, # in 1999-April 2000 (#43,#44) were 02:00, # in the 2000 Temporary Provisions were 01:00. # # ----------------------------------------------------------------------------- # Links: # 1 https://findit.library.yale.edu/images_layout/view?parentoid=15537490&increment=687 # 2 https://findit.library.yale.edu/images_layout/view?parentoid=15537490&increment=716 # 3 https://findit.library.yale.edu/images_layout/view?parentoid=15537491&increment=721 # 4 https://findit.library.yale.edu/images_layout/view?parentoid=15537491&increment=958 # 5 https://findit.library.yale.edu/images_layout/view?parentoid=15537502&increment=558 # 6 https://findit.library.yale.edu/images_layout/view?parentoid=15537511&increment=105 # 7 https://findit.library.yale.edu/images_layout/view?parentoid=15537516&increment=278 # 8 https://findit.library.yale.edu/images_layout/view?parentoid=15537522&increment=248 # 9 https://findit.library.yale.edu/images_layout/view?parentoid=15537530&increment=329 #10 https://findit.library.yale.edu/images_layout/view?parentoid=15537537&increment=601 #11 https://www.nevo.co.il/law_word/law12/er-002.pdf#page=3 #12 https://www.nevo.co.il/law_word/law12/er-017-t2.pdf#page=4 #13 https://www.nevo.co.il/law_word/law06/tak-0006.pdf#page=3 #14 https://www.nevo.co.il/law_word/law06/tak-0080.pdf#page=7 #15 https://www.nevo.co.il/law_word/law06/tak-0164.pdf#page=10 #16 https://www.nevo.co.il/law_word/law06/tak-0206.pdf#page=4 #17 https://www.nevo.co.il/law_word/law06/tak-0212.pdf#page=2 #18 https://www.nevo.co.il/law_word/law06/tak-0254.pdf#page=4 #19 https://www.nevo.co.il/law_word/law06/tak-0300.pdf#page=5 #20 https://www.nevo.co.il/law_word/law06/tak-0348.pdf#page=3 #21 https://www.nevo.co.il/law_word/law06/tak-0420.pdf#page=5 #22 https://www.nevo.co.il/law_word/law06/tak-0497.pdf#page=10 #23 https://www.nevo.co.il/law_word/law06/tak-0591.pdf#page=6 #24 https://www.nevo.co.il/law_word/law06/tak-0680.pdf#page=3 #25 https://www.nevo.co.il/law_word/law06/tak-3192.pdf#page=2 #26 https://www.nevo.co.il/law_word/law06/tak-3322.pdf#page=5 #27 https://www.nevo.co.il/law_word/law06/tak-4146.pdf#page=2 #28 https://www.nevo.co.il/law_word/law06/tak-4604.pdf#page=7 #29 https://www.nevo.co.il/law_word/law06/tak-4619.pdf#page=2 #30 https://www.nevo.co.il/law_word/law06/tak-4744.pdf#page=11 #31 https://www.nevo.co.il/law_word/law06/tak-4851.pdf#page=2 #32 https://www.nevo.co.il/law_word/law06/tak-4932.pdf#page=19 #33 https://www.nevo.co.il/law_word/law06/tak-5013.pdf#page=8 #34 https://www.nevo.co.il/law_word/law06/tak-5021.pdf#page=8 #35 https://www.nevo.co.il/law_word/law06/tak-5096.pdf#page=3 #36 https://www.nevo.co.il/law_word/law06/tak-5167.pdf#page=2 #37 https://www.nevo.co.il/law_word/law06/tak-5248.pdf#page=7 #38 https://www.nevo.co.il/law_word/law06/tak-5335.pdf#page=6 #39 https://www.nevo.co.il/law_word/law06/tak-5339.pdf#page=7 #40 https://www.nevo.co.il/law_word/law06/tak-5506.pdf#page=19 #41 https://www.nevo.co.il/law_word/law06/tak-5731.pdf#page=2 #42 https://www.nevo.co.il/law_word/law06/tak-5853.pdf#page=3 #43 https://www.nevo.co.il/law_word/law06/tak-5937.pdf#page=9 #44 https://www.nevo.co.il/law_word/law06/tak-6024.pdf#page=4 # # Time Determination (Temporary Provisions) Law, 1997 # https://www.nevo.co.il/law_html/law19/p201_003.htm # # Time Determination (Temporary Provisions) Law, 2000 # https://www.nevo.co.il/law_html/law19/p201_004.htm # # Time Determination Law, 1992 and amendments # https://www.nevo.co.il/law_html/law01/p201_002.htm # https://main.knesset.gov.il/Activity/Legislation/Laws/Pages/LawPrimary.aspx?lawitemid=2001174 # From Paul Eggert (2020-10-27): # Several of the midnight transitions mentioned above are ambiguous; # are they 00:00, 00:00s, 24:00, or 24:00s? When resolving these ambiguities, # try to minimize changes from previous tzdb versions, for lack of better info. # Commentary from previous versions is included below, to help explain this. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Zion 1940 only - May 31 24:00u 1:00 D Rule Zion 1940 only - Sep 30 24:00u 0 S Rule Zion 1940 only - Nov 16 24:00u 1:00 D Rule Zion 1942 1946 - Oct 31 24:00u 0 S Rule Zion 1943 1944 - Mar 31 24:00u 1:00 D Rule Zion 1945 1946 - Apr 15 24:00u 1:00 D Rule Zion 1948 only - May 22 24:00u 2:00 DD Rule Zion 1948 only - Aug 31 24:00u 1:00 D Rule Zion 1948 1949 - Oct 31 24:00u 0 S Rule Zion 1949 only - Apr 30 24:00u 1:00 D Rule Zion 1950 only - Apr 15 24:00u 1:00 D Rule Zion 1950 only - Sep 14 24:00u 0 S Rule Zion 1951 only - Mar 31 24:00u 1:00 D Rule Zion 1951 only - Nov 10 24:00u 0 S Rule Zion 1952 only - Apr 19 24:00u 1:00 D Rule Zion 1952 only - Oct 18 24:00u 0 S Rule Zion 1953 only - Apr 11 24:00u 1:00 D Rule Zion 1953 only - Sep 12 24:00u 0 S Rule Zion 1954 only - Jun 12 24:00u 1:00 D Rule Zion 1954 only - Sep 11 24:00u 0 S Rule Zion 1955 only - Jun 11 24:00u 1:00 D Rule Zion 1955 only - Sep 10 24:00u 0 S Rule Zion 1956 only - Jun 2 24:00u 1:00 D Rule Zion 1956 only - Sep 29 24:00u 0 S Rule Zion 1957 only - Apr 27 24:00u 1:00 D Rule Zion 1957 only - Sep 21 24:00u 0 S Rule Zion 1974 only - Jul 6 24:00 1:00 D Rule Zion 1974 only - Oct 12 24:00 0 S Rule Zion 1975 only - Apr 19 24:00 1:00 D Rule Zion 1975 only - Aug 30 24:00 0 S # From Alois Treindl (2019-03-06): # http://www.moin.gov.il/Documents/שעון%20קיץ/clock-50-years-7-2014.pdf # From Isaac Starkman (2019-03-06): # Summer time was in that period in 1980 and 1984, see # https://www.ynet.co.il/articles/0,7340,L-3951073,00.html # You can of course read it in translation. # I checked the local newspapers for that years. # It started on midnight and end at 01.00 am. # From Paul Eggert (2019-03-06): # Also see this thread about the moin.gov.il URL: # https://mm.icann.org/pipermail/tz/2018-November/027194.html Rule Zion 1980 only - Aug 2 24:00s 1:00 D Rule Zion 1980 only - Sep 13 24:00s 0 S Rule Zion 1984 only - May 5 24:00s 1:00 D Rule Zion 1984 only - Aug 25 24:00s 0 S Rule Zion 1985 only - Apr 13 24:00 1:00 D Rule Zion 1985 only - Aug 31 24:00 0 S Rule Zion 1986 only - May 17 24:00 1:00 D Rule Zion 1986 only - Sep 6 24:00 0 S Rule Zion 1987 only - Apr 14 24:00 1:00 D Rule Zion 1987 only - Sep 12 24:00 0 S # From Avigdor Finkelstein (2014-03-05): # I check the Parliament (Knesset) records and there it's stated that the # [1988] transition should take place on Saturday night, when the Sabbath # ends and changes to Sunday. Rule Zion 1988 only - Apr 9 24:00 1:00 D Rule Zion 1988 only - Sep 3 24:00 0 S # From Ephraim Silverberg # (1997-03-04, 1998-03-16, 1998-12-28, 2000-01-17, 2000-07-25, 2004-12-22, # and 2005-02-17): # According to the Office of the Secretary General of the Ministry of # Interior, there is NO set rule for Daylight-Savings/Standard time changes. # One thing is entrenched in law, however: that there must be at least 150 # days of daylight savings time annually. From 1993-1998, the change to # daylight savings time was on a Friday morning from midnight IST to # 1 a.m IDT; up until 1998, the change back to standard time was on a # Saturday night from midnight daylight savings time to 11 p.m. standard # time. 1996 is an exception to this rule where the change back to standard # time took place on Sunday night instead of Saturday night to avoid # conflicts with the Jewish New Year. In 1999, the change to # daylight savings time was still on a Friday morning but from # 2 a.m. IST to 3 a.m. IDT; furthermore, the change back to standard time # was also on a Friday morning from 2 a.m. IDT to 1 a.m. IST for # 1999 only. In the year 2000, the change to daylight savings time was # similar to 1999, but although the change back will be on a Friday, it # will take place from 1 a.m. IDT to midnight IST. Starting in 2001, all # changes to/from will take place at 1 a.m. old time, but now there is no # rule as to what day of the week it will take place in as the start date # (except in 2003) is the night after the Passover Seder (i.e. the eve # of the 16th of Nisan in the lunar Hebrew calendar) and the end date # (except in 2002) is three nights before Yom Kippur [Day of Atonement] # (the eve of the 7th of Tishrei in the lunar Hebrew calendar). # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Zion 1989 only - Apr 29 24:00 1:00 D Rule Zion 1989 only - Sep 2 24:00 0 S Rule Zion 1990 only - Mar 24 24:00 1:00 D Rule Zion 1990 only - Aug 25 24:00 0 S Rule Zion 1991 only - Mar 23 24:00 1:00 D Rule Zion 1991 only - Aug 31 24:00 0 S Rule Zion 1992 only - Mar 28 24:00 1:00 D Rule Zion 1992 only - Sep 5 24:00 0 S Rule Zion 1993 only - Apr 2 0:00 1:00 D Rule Zion 1993 only - Sep 5 0:00 0 S # The dates for 1994-1995 were obtained from Office of the Spokeswoman for the # Ministry of Interior, Jerusalem, Israel. The spokeswoman can be reached by # calling the office directly at 972-2-6701447 or 972-2-6701448. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Zion 1994 only - Apr 1 0:00 1:00 D Rule Zion 1994 only - Aug 28 0:00 0 S Rule Zion 1995 only - Mar 31 0:00 1:00 D Rule Zion 1995 only - Sep 3 0:00 0 S # The dates for 1996 were determined by the Minister of Interior of the # time, Haim Ramon. The official announcement regarding 1996-1998 # (with the dates for 1997-1998 no longer being relevant) can be viewed at: # # ftp://ftp.cs.huji.ac.il/pub/tz/announcements/1996-1998.ramon.ps.gz # # The dates for 1997-1998 were altered by his successor, Rabbi Eli Suissa. # # The official announcements for the years 1997-1999 can be viewed at: # # ftp://ftp.cs.huji.ac.il/pub/tz/announcements/YYYY.ps.gz # # where YYYY is the relevant year. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Zion 1996 only - Mar 14 24:00 1:00 D Rule Zion 1996 only - Sep 15 24:00 0 S Rule Zion 1997 only - Mar 20 24:00 1:00 D Rule Zion 1997 only - Sep 13 24:00 0 S Rule Zion 1998 only - Mar 20 0:00 1:00 D Rule Zion 1998 only - Sep 6 0:00 0 S Rule Zion 1999 only - Apr 2 2:00 1:00 D Rule Zion 1999 only - Sep 3 2:00 0 S # The Knesset Interior Committee has changed the dates for 2000 for # the third time in just over a year and have set new dates for the # years 2001-2004 as well. # # The official announcement for the start date of 2000 can be viewed at: # # ftp://ftp.cs.huji.ac.il/pub/tz/announcements/2000-start.ps.gz # # The official announcement for the end date of 2000 and the dates # for the years 2001-2004 can be viewed at: # # ftp://ftp.cs.huji.ac.il/pub/tz/announcements/2000-2004.ps.gz # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Zion 2000 only - Apr 14 2:00 1:00 D Rule Zion 2000 only - Oct 6 1:00 0 S Rule Zion 2001 only - Apr 9 1:00 1:00 D Rule Zion 2001 only - Sep 24 1:00 0 S Rule Zion 2002 only - Mar 29 1:00 1:00 D Rule Zion 2002 only - Oct 7 1:00 0 S Rule Zion 2003 only - Mar 28 1:00 1:00 D Rule Zion 2003 only - Oct 3 1:00 0 S Rule Zion 2004 only - Apr 7 1:00 1:00 D Rule Zion 2004 only - Sep 22 1:00 0 S # The proposed law agreed upon by the Knesset Interior Committee on # 2005-02-14 is that, for 2005 and beyond, DST starts at 02:00 the # last Friday before April 2nd (i.e. the last Friday in March or April # 1st itself if it falls on a Friday) and ends at 02:00 on the Saturday # night _before_ the fast of Yom Kippur. # # Those who can read Hebrew can view the announcement at: # # ftp://ftp.cs.huji.ac.il/pub/tz/announcements/2005+beyond.ps # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Zion 2005 2012 - Apr Fri<=1 2:00 1:00 D Rule Zion 2005 only - Oct 9 2:00 0 S Rule Zion 2006 only - Oct 1 2:00 0 S Rule Zion 2007 only - Sep 16 2:00 0 S Rule Zion 2008 only - Oct 5 2:00 0 S Rule Zion 2009 only - Sep 27 2:00 0 S Rule Zion 2010 only - Sep 12 2:00 0 S Rule Zion 2011 only - Oct 2 2:00 0 S Rule Zion 2012 only - Sep 23 2:00 0 S # From Ephraim Silverberg (2020-10-26): # The current time law (2013) from the State of Israel can be viewed # (in Hebrew) at: # ftp://ftp.cs.huji.ac.il/pub/tz/israel/announcements/2013+law.pdf # It translates to: # Every year, in the period from the Friday before the last Sunday in # the month of March at 02:00 a.m. until the last Sunday of the month # of October at 02:00 a.m., Israel Time will be advanced an additional # hour such that it will be UTC+3. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Zion 2013 max - Mar Fri>=23 2:00 1:00 D Rule Zion 2013 max - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Jerusalem 2:20:54 - LMT 1880 2:20:40 - JMT 1918 # Jerusalem Mean Time? 2:00 Zion I%sT ############################################################################### # Japan # '9:00' and 'JST' is from Guy Harris. # From Paul Eggert (2020-01-19): # Starting in the 7th century, Japan generally followed an ancient Chinese # timekeeping system that divided night and day into six hours each, # with hour length depending on season. In 1873 the government # started requiring the use of a Western style 24-hour clock. See: # Yulia Frumer, "Making Time: Astronomical Time Measurement in Tokugawa Japan" # . As the tzdb code and # data support only 24-hour clocks, its tables model timestamps before # 1873 using Western-style local mean time. # From Hideyuki Suzuki (1998-11-09): # 'Tokyo' usually stands for the former location of Tokyo Astronomical # Observatory: 139° 44' 40.90" E (9h 18m 58.727s), 35° 39' 16.0" N. # This data is from 'Rika Nenpyou (Chronological Scientific Tables) 1996' # edited by National Astronomical Observatory of Japan.... # JST (Japan Standard Time) has been used since 1888-01-01 00:00 (JST). # The law is enacted on 1886-07-07. # From Hideyuki Suzuki (1998-11-16): # The ordinance No. 51 (1886) established "standard time" in Japan, # which stands for the time on 135° E. # In the ordinance No. 167 (1895), "standard time" was renamed to "central # standard time". And the same ordinance also established "western standard # time", which stands for the time on 120° E.... But "western standard # time" was abolished in the ordinance No. 529 (1937). In the ordinance No. # 167, there is no mention regarding for what place western standard time is # standard.... # # I wrote "ordinance" above, but I don't know how to translate. # In Japanese it's "chokurei", which means ordinance from emperor. # From Yu-Cheng Chuang (2013-07-12): # ...the Meiji Emperor announced Ordinance No. 167 of Meiji Year 28 "The clause # about standard time" ... The adoption began from Jan 1, 1896. # https://ja.wikisource.org/wiki/標準時ニ關スル件_(公布時) # # ...the Showa Emperor announced Ordinance No. 529 of Showa Year 12 ... which # means the whole Japan territory, including later occupations, adopt Japan # Central Time (UT+9). The adoption began on Oct 1, 1937. # https://ja.wikisource.org/wiki/明治二�八年勅令第百六�七號標準時ニ關スル件中改正ノ件 # From Paul Eggert (1995-03-06): # Today's _Asahi Evening News_ (page 4) reports that Japan had # daylight saving between 1948 and 1951, but "the system was discontinued # because the public believed it would lead to longer working hours." # From Mayumi Negishi in the 2005-08-10 Japan Times: # http://www.japantimes.co.jp/cgi-bin/getarticle.pl5?nn20050810f2.htm # Occupation authorities imposed daylight-saving time on Japan on # [1948-05-01].... But lack of prior debate and the execution of # daylight-saving time just three days after the bill was passed generated # deep hatred of the concept.... The Diet unceremoniously passed a bill to # dump the unpopular system in October 1951, less than a month after the San # Francisco Peace Treaty was signed. (A government poll in 1951 showed 53% # of the Japanese wanted to scrap daylight-saving time, as opposed to 30% who # wanted to keep it.) # From Takayuki Nikai (2018-01-19): # The source of information is Japanese law. # http://www.shugiin.go.jp/internet/itdb_housei.nsf/html/houritsu/00219480428029.htm # http://www.shugiin.go.jp/internet/itdb_housei.nsf/html/houritsu/00719500331039.htm # ... In summary, it is written as follows. From 24:00 on the first Saturday # in May, until 0:00 on the day after the second Saturday in September. # From Phake Nick (2018-09-27): # [T]he webpage authored by National Astronomical Observatory of Japan # https://eco.mtk.nao.ac.jp/koyomi/wiki/BBFEB9EF2FB2C6BBFEB9EF.html # ... mentioned that using Showa 23 (year 1948) as example, 13pm of September # 11 in summer time will equal to 0am of September 12 in standard time. # It cited a document issued by the Liaison Office which briefly existed # during the postwar period of Japan, where the detail on implementation # of the summer time is described in the document. # https://eco.mtk.nao.ac.jp/koyomi/wiki/BBFEB9EF2FB2C6BBFEB9EFB2C6BBFEB9EFA4CEBCC2BBDCA4CBA4C4A4A4A4C6.pdf # The text in the document do instruct a fall back to occur at # September 11, 13pm in summer time, while ordinary citizens can # change the clock before they sleep. # # From Paul Eggert (2018-09-27): # This instruction is equivalent to "Sat>=8 25:00", so use that. zic treats # it like "Sun>=9 01:00", which is not quite the same but is the best we can # do in any POSIX or C platform. The "25:00" assumes zic from 2007 or later, # which should be safe now. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Japan 1948 only - May Sat>=1 24:00 1:00 D Rule Japan 1948 1951 - Sep Sat>=8 25:00 0 S Rule Japan 1949 only - Apr Sat>=1 24:00 1:00 D Rule Japan 1950 1951 - May Sat>=1 24:00 1:00 D # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Tokyo 9:18:59 - LMT 1887 Dec 31 15:00u 9:00 Japan J%sT # Since 1938, all Japanese possessions have been like Asia/Tokyo, # except that Truk (Chuuk), Ponape (Pohnpei), and Jaluit (Kosrae) did not # switch from +10 to +09 until 1941-04-01; see the 'australasia' file. # Jordan # # From # Jordan Week (1999-07-01) via Steffen Thorsen (1999-09-09): # Clocks in Jordan were forwarded one hour on Wednesday at midnight, # in accordance with the government's decision to implement summer time # all year round. # # From # Jordan Week (1999-09-30) via Steffen Thorsen (1999-11-09): # Winter time starts today Thursday, 30 September. Clocks will be turned back # by one hour. This is the latest government decision and it's final! # The decision was taken because of the increase in working hours in # government's departments from six to seven hours. # # From Paul Eggert (2005-11-22): # Starting 2003 transitions are from Steffen Thorsen's web site timeanddate.com. # # From Steffen Thorsen (2005-11-23): # For Jordan I have received multiple independent user reports every year # about DST end dates, as the end-rule is different every year. # # From Steffen Thorsen (2006-10-01), after a heads-up from Hilal Malawi: # http://www.petranews.gov.jo/nepras/2006/Sep/05/4000.htm # "Jordan will switch to winter time on Friday, October 27". # # From Steffen Thorsen (2009-04-02): # This single one might be good enough, (2009-03-24, Arabic): # http://petra.gov.jo/Artical.aspx?Lng=2&Section=8&Artical=95279 # # Google's translation: # # > The Council of Ministers decided in 2002 to adopt the principle of timely # > submission of the summer at 60 minutes as of midnight on the last Thursday # > of the month of March of each year. # # So - this means the midnight between Thursday and Friday since 2002. # From Arthur David Olson (2009-04-06): # We still have Jordan switching to DST on Thursdays in 2000 and 2001. # From Steffen Thorsen (2012-10-25): # Yesterday the government in Jordan announced that they will not # switch back to standard time this winter, so the will stay on DST # until about the same time next year (at least). # http://www.petra.gov.jo/Public_News/Nws_NewsDetails.aspx?NewsID=88950 # From Steffen Thorsen (2013-12-11): # Jordan Times and other sources say that Jordan is going back to # UTC+2 on 2013-12-19 at midnight: # http://jordantimes.com/govt-decides-to-switch-back-to-wintertime # Official, in Arabic: # http://www.petra.gov.jo/public_news/Nws_NewsDetails.aspx?Menu_ID=&Site_Id=2&lang=1&NewsID=133230&CatID=14 # ... Our background/permalink about it # https://www.timeanddate.com/news/time/jordan-reverses-dst-decision.html # ... # http://www.petra.gov.jo/Public_News/Nws_NewsDetails.aspx?lang=2&site_id=1&NewsID=133313&Type=P # ... says midnight for the coming one and 1:00 for the ones in the future # (and they will use DST again next year, using the normal schedule). # From Paul Eggert (2013-12-11): # As Steffen suggested, consider the past 21-month experiment to be DST. # From Steffen Thorsen (2021-09-24): # The Jordanian Government announced yesterday that they will start DST # in February instead of March: # https://petra.gov.jo/Include/InnerPage.jsp?ID=37683&lang=en&name=en_news (English) # https://petra.gov.jo/Include/InnerPage.jsp?ID=189969&lang=ar&name=news (Arabic) # From the Arabic version, it seems to say it would be at midnight # (assume 24:00) on the last Thursday in February, starting from 2022. # From Issam Al-Zuwairi (2022-10-05): # The Council of Ministers in Jordan decided Wednesday 5th October 2022, # that daylight saving time (DST) will be throughout the year.... # # From Brian Inglis (2022-10-06): # https://petra.gov.jo/Include/InnerPage.jsp?ID=45567&lang=en&name=en_news # # From Paul Eggert (2022-10-05): # Like Syria, model this as a transition from EEST +03 (DST) to plain +03 # (non-DST) at the point where DST would otherwise have ended. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Jordan 1973 only - Jun 6 0:00 1:00 S Rule Jordan 1973 1975 - Oct 1 0:00 0 - Rule Jordan 1974 1977 - May 1 0:00 1:00 S Rule Jordan 1976 only - Nov 1 0:00 0 - Rule Jordan 1977 only - Oct 1 0:00 0 - Rule Jordan 1978 only - Apr 30 0:00 1:00 S Rule Jordan 1978 only - Sep 30 0:00 0 - Rule Jordan 1985 only - Apr 1 0:00 1:00 S Rule Jordan 1985 only - Oct 1 0:00 0 - Rule Jordan 1986 1988 - Apr Fri>=1 0:00 1:00 S Rule Jordan 1986 1990 - Oct Fri>=1 0:00 0 - Rule Jordan 1989 only - May 8 0:00 1:00 S Rule Jordan 1990 only - Apr 27 0:00 1:00 S Rule Jordan 1991 only - Apr 17 0:00 1:00 S Rule Jordan 1991 only - Sep 27 0:00 0 - Rule Jordan 1992 only - Apr 10 0:00 1:00 S Rule Jordan 1992 1993 - Oct Fri>=1 0:00 0 - Rule Jordan 1993 1998 - Apr Fri>=1 0:00 1:00 S Rule Jordan 1994 only - Sep Fri>=15 0:00 0 - Rule Jordan 1995 1998 - Sep Fri>=15 0:00s 0 - Rule Jordan 1999 only - Jul 1 0:00s 1:00 S Rule Jordan 1999 2002 - Sep lastFri 0:00s 0 - Rule Jordan 2000 2001 - Mar lastThu 0:00s 1:00 S Rule Jordan 2002 2012 - Mar lastThu 24:00 1:00 S Rule Jordan 2003 only - Oct 24 0:00s 0 - Rule Jordan 2004 only - Oct 15 0:00s 0 - Rule Jordan 2005 only - Sep lastFri 0:00s 0 - Rule Jordan 2006 2011 - Oct lastFri 0:00s 0 - Rule Jordan 2013 only - Dec 20 0:00 0 - Rule Jordan 2014 2021 - Mar lastThu 24:00 1:00 S Rule Jordan 2014 2022 - Oct lastFri 0:00s 0 - Rule Jordan 2022 only - Feb lastThu 24:00 1:00 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Amman 2:23:44 - LMT 1931 2:00 Jordan EE%sT 2022 Oct 28 0:00s 3:00 - %z # Kazakhstan # From Kazakhstan Embassy's News Bulletin No. 11 # (2005-03-21): # The Government of Kazakhstan passed a resolution March 15 abolishing # daylight saving time citing lack of economic benefits and health # complications coupled with a decrease in productivity. # # From Branislav Kojic (in Astana) via Gwillim Law (2005-06-28): # ... what happened was that the former Kazakhstan Eastern time zone # was "blended" with the Central zone. Therefore, Kazakhstan now has # two time zones, and difference between them is one hour. The zone # closer to UTC is the former Western zone (probably still called the # same), encompassing four provinces in the west: Aqtöbe, Atyraū, # Mangghystaū, and West Kazakhstan. The other zone encompasses # everything else.... I guess that would make Kazakhstan time zones # de jure UTC+5 and UTC+6 respectively. # From Stepan Golosunov (2016-03-27): # Review of the linked documents from http://adilet.zan.kz/ # produced the following data for post-1991 Kazakhstan: # # 0. Act of the Cabinet of Ministers of the USSR # from 1991-02-04 No. 20 # http://pravo.gov.ru/proxy/ips/?docbody=&nd=102010545 # removed the extra hour ("decree time") on the territory of the USSR # starting with the last Sunday of March 1991. # It also allowed (but not mandated) Kazakh SSR, Kirghiz SSR, Tajik SSR, # Turkmen SSR and Uzbek SSR to not have "summer" time. # # The 1992-01-13 act also refers to the act of the Cabinet of Ministers # of the Kazakh SSR from 1991-03-20 No. 170 "About the act of the Cabinet # of Ministers of the USSR from 1991-02-04 No. 20" but I didn't found its # text. # # According to Izvestia newspaper No. 68 (23334) from 1991-03-20 # -- page 6; available at http://libinfo.org/newsr/newsr2574.djvu via # http://libinfo.org/index.php?id=58564 -- on 1991-03-31 at 2:00 during # transition to "summer" time: # Republic of Georgia, Latvian SSR, Lithuanian SSR, SSR Moldova, # Estonian SSR; Komi ASSR; Kaliningrad oblast; Nenets autonomous okrug # were to move clocks 1 hour forward. # Kazakh SSR (excluding Uralsk oblast); Republic of Kyrgyzstan, Tajik # SSR; Andijan, Jizzakh, Namangan, Sirdarya, Tashkent, Fergana oblasts # of the Uzbek SSR were to move clocks 1 hour backwards. # Other territories were to not move clocks. # When the "summer" time would end on 1991-09-29, clocks were to be # moved 1 hour backwards on the territory of the USSR excluding # Kazakhstan, Kirghizia, Uzbekistan, Turkmenia, Tajikistan. # # Apparently there were last minute changes. Apparently Kazakh act No. 170 # was one of such changes. # # https://ru.wikipedia.org/wiki/Декретное_врем� # claims that Sovetskaya Rossiya newspaper on 1991-03-29 published that # Nenets autonomous okrug, Komi and Kazakhstan (excluding Uralsk oblast) # were to not move clocks and Uralsk oblast was to move clocks # forward; on 1991-09-29 Kazakhstan was to move clocks backwards. # (Probably there were changes even after that publication. There is an # article claiming that Kaliningrad oblast decided on 1991-03-29 to not # move clocks.) # # This implies that on 1991-03-31 Asia/Oral remained on +04/+05 while # the rest of Kazakhstan switched from +06/+07 to +05/06 or from +05/06 # to +04/+05. It's unclear how Qyzylorda oblast moved into the fifth # time belt. (By switching from +04/+05 to +05/+06 on 1991-09-29?) ... # # 1. Act of the Cabinet of Ministers of the Republic of Kazakhstan # from 1992-01-13 No. 28 # http://adilet.zan.kz/rus/docs/P920000028_ # (text includes modification from the 1996 act) # introduced new rules for calculation of time, mirroring Russian # 1992-01-08 act. It specified that time would be calculated # according to time belts plus extra hour ("decree time"), moved clocks # on the whole territory of Kazakhstan 1 hour forward on 1992-01-19 at # 2:00, specified DST rules. It acknowledged that Kazakhstan was # located in the fourth and the fifth time belts and specified the # border between them to be located east of Qostanay and Aktyubinsk # oblasts (notably including Turgai and Qyzylorda oblasts into the fifth # time belt). # # This means switch on 1992-01-19 at 2:00 from +04/+05 to +05/+06 for # Asia/Aqtau, Asia/Aqtobe, Asia/Oral, Atyraū and Qostanay oblasts; from # +05/+06 to +06/+07 for Asia/Almaty and Asia/Qyzylorda (and Arkalyk).... # # 2. Act of the Cabinet of Ministers of the Republic of Kazakhstan # from 1992-03-27 No. 284 # http://adilet.zan.kz/rus/docs/P920000284_ # cancels extra hour ("decree time") for Uralsk and Qyzylorda oblasts # since the last Sunday of March 1992, while keeping them in the fourth # and the fifth time belts respectively. # # 3. Order of the Prime Minister of the Republic of Kazakhstan # from 1994-09-23 No. 384 # http://adilet.zan.kz/rus/docs/R940000384_ # cancels the extra hour ("decree time") on the territory of Mangghystaū # oblast since the last Sunday of September 1994 (saying that time on # the territory would correspond to the third time belt as a # result).... # # 4. Act of the Government of the Republic of Kazakhstan # from 1996-05-08 No. 575 # http://adilet.zan.kz/rus/docs/P960000575_ # amends the 1992-01-13 act to end summer time in October instead # of September, mirroring identical Russian change from 1996-04-23 act. # # 5. Act of the Government of the Republic of Kazakhstan # from 1999-03-26 No. 305 # http://adilet.zan.kz/rus/docs/P990000305_ # cancels the extra hour ("decree time") for Atyraū oblast since the # last Sunday of March 1999 while retaining the oblast in the fourth # time belt. # # This means change from +05/+06 to +04/+05.... # # 6. Act of the Government of the Republic of Kazakhstan # from 2000-11-23 No. 1749 # http://adilet.zan.kz/rus/archive/docs/P000001749_/23.11.2000 # replaces the previous five documents. # # The only changes I noticed are in definition of the border between the # fourth and the fifth time belts. They account for changes in spelling # and administrative division (splitting of Turgai oblast in 1997 # probably changed time in territories incorporated into Qostanay oblast # (including Arkalyk) from +06/+07 to +05/+06) and move Qyzylorda oblast # from being in the fifth time belt and not using decree time into the # fourth time belt (no change in practice). # # 7. Act of the Government of the Republic of Kazakhstan # from 2003-12-29 No. 1342 # http://adilet.zan.kz/rus/docs/P030001342_ # modified the 2000-11-23 act. No relevant changes, apparently. # # 8. Act of the Government of the Republic of Kazakhstan # from 2004-07-20 No. 775 # http://adilet.zan.kz/rus/archive/docs/P040000775_/20.07.2004 # modified the 2000-11-23 act to move Qostanay and Qyzylorda oblasts into # the fifth time belt and add Aktobe oblast to the list of regions not # using extra hour ("decree time"), leaving Kazakhstan with only 2 time # zones (+04/+05 and +06/+07). The changes were to be implemented # during DST transitions in 2004 and 2005 but the acts got radically # amended before implementation happened. # # 9. Act of the Government of the Republic of Kazakhstan # from 2004-09-15 No. 1059 # http://adilet.zan.kz/rus/docs/P040001059_ # modified the 2000-11-23 act to remove exceptions from the "decree time" # (leaving Kazakhstan in +05/+06 and +06/+07 zones), amended the # 2004-07-20 act to implement changes for Atyraū, West Kazakhstan, # Qostanay, Qyzylorda and Mangghystaū oblasts by not moving clocks # during the 2004 transition to "winter" time. # # This means transition from +04/+05 to +05/+06 for Atyraū oblast (no # zone currently), Asia/Oral, Asia/Aqtau and transition from +05/+06 to # +06/+07 for Qostanay oblast (Qostanay and Arkalyk, no zones currently) # and Asia/Qyzylorda on 2004-10-31 at 3:00.... # # 10. Act of the Government of the Republic of Kazakhstan # from 2005-03-15 No. 231 # http://adilet.zan.kz/rus/docs/P050000231_ # removes DST provisions from the 2000-11-23 act, removes most of the # (already implemented) provisions from the 2004-07-20 and 2004-09-15 # acts, comes into effect 10 days after official publication. # The only practical effect seems to be the abolition of the summer # time. # # Unamended version of the act of the Government of the Russian Federation # No. 23 from 1992-01-08 [See 'europe' file for details]. # Kazakh 1992-01-13 act appears to provide the same rules and 1992-03-27 # act was to be enacted on the last Sunday of March 1992. # From Stepan Golosunov (2016-11-08): # Turgai reorganization should affect only southern part of Qostanay # oblast. Which should probably be separated into Asia/Arkalyk zone. # (There were also 1970, 1988 and 1990 Turgai oblast reorganizations # according to wikipedia.) # # [For Qostanay] http://www.ng.kz/gazeta/195/hranit/ # suggests that clocks were to be moved 40 minutes backwards on # 1920-01-01 to the fourth time belt. But I do not understand # how that could happen.... # # [For Atyrau and Oral] 1919 decree # (http://www.worldtimezone.com/dst_news/dst_news_russia-1919-02-08.html # and in Byalokoz) lists Ural river (plus 10 versts on its left bank) in # the third time belt (before 1930 this means +03). # From Alexander Konzurovski (2018-12-20): # (Asia/Qyzylorda) is changing its time zone from UTC+6 to UTC+5 # effective December 21st, 2018.... # http://adilet.zan.kz/rus/docs/P1800000817 (russian language). # From Zhanbolat Raimbekov (2024-01-19): # Kazakhstan (all parts) switching to UTC+5 on March 1, 2024 # https://www.gov.kz/memleket/entities/mti/press/news/details/688998?lang=ru # [in Russian] # (2024-01-20): https://primeminister.kz/ru/decisions/19012024-20 # # From Alexander Krivenyshev (2024-01-19): # According to a different news and the official web site for the Ministry of # Trade and Integration of the Republic of Kazakhstan: # https://en.inform.kz/news/kazakhstan-to-switch-to-single-hour-zone-mar-1-54ad0b/ # Zone NAME STDOFF RULES FORMAT [UNTIL] # # Almaty (formerly Alma-Ata), representing most locations in Kazakhstan # This includes Abai/Abay (ISO 3166-2 code KZ-10), Aqmola/Akmola (KZ-11), # Almaty (KZ-19), Almaty city (KZ-75), Astana city (KZ-71), # East Kazakhstan (KZ-63), Jambyl/Zhambyl (KZ-31), Jetisu/Zhetysu (KZ-33), # Karaganda (KZ-35), North Kazakhstan (KZ-59), Pavlodar (KZ-55), # Shymkent city (KZ-79), Turkistan (KZ-61), and Ulytau (KZ-62). Zone Asia/Almaty 5:07:48 - LMT 1924 May 2 # or Alma-Ata 5:00 - %z 1930 Jun 21 6:00 RussiaAsia %z 1991 Mar 31 2:00s 5:00 RussiaAsia %z 1992 Jan 19 2:00s 6:00 RussiaAsia %z 2004 Oct 31 2:00s 6:00 - %z 2024 Mar 1 0:00 5:00 - %z # Qyzylorda (aka Kyzylorda, Kizilorda, Kzyl-Orda, etc.) (KZ-43) Zone Asia/Qyzylorda 4:21:52 - LMT 1924 May 2 4:00 - %z 1930 Jun 21 5:00 - %z 1981 Apr 1 5:00 1:00 %z 1981 Oct 1 6:00 - %z 1982 Apr 1 5:00 RussiaAsia %z 1991 Mar 31 2:00s 4:00 RussiaAsia %z 1991 Sep 29 2:00s 5:00 RussiaAsia %z 1992 Jan 19 2:00s 6:00 RussiaAsia %z 1992 Mar 29 2:00s 5:00 RussiaAsia %z 2004 Oct 31 2:00s 6:00 - %z 2018 Dec 21 0:00 5:00 - %z # Qostanay (aka Kostanay, Kustanay) (KZ-39) # The 1991/2 rules are unclear partly because of the 1997 Turgai # reorganization. Zone Asia/Qostanay 4:14:28 - LMT 1924 May 2 4:00 - %z 1930 Jun 21 5:00 - %z 1981 Apr 1 5:00 1:00 %z 1981 Oct 1 6:00 - %z 1982 Apr 1 5:00 RussiaAsia %z 1991 Mar 31 2:00s 4:00 RussiaAsia %z 1992 Jan 19 2:00s 5:00 RussiaAsia %z 2004 Oct 31 2:00s 6:00 - %z 2024 Mar 1 0:00 5:00 - %z # Aqtöbe (aka Aktobe, formerly Aktyubinsk) (KZ-15) Zone Asia/Aqtobe 3:48:40 - LMT 1924 May 2 4:00 - %z 1930 Jun 21 5:00 - %z 1981 Apr 1 5:00 1:00 %z 1981 Oct 1 6:00 - %z 1982 Apr 1 5:00 RussiaAsia %z 1991 Mar 31 2:00s 4:00 RussiaAsia %z 1992 Jan 19 2:00s 5:00 RussiaAsia %z 2004 Oct 31 2:00s 5:00 - %z # Mangghystaū (KZ-47) # Aqtau was not founded until 1963, but it represents an inhabited region, # so include timestamps before 1963. Zone Asia/Aqtau 3:21:04 - LMT 1924 May 2 4:00 - %z 1930 Jun 21 5:00 - %z 1981 Oct 1 6:00 - %z 1982 Apr 1 5:00 RussiaAsia %z 1991 Mar 31 2:00s 4:00 RussiaAsia %z 1992 Jan 19 2:00s 5:00 RussiaAsia %z 1994 Sep 25 2:00s 4:00 RussiaAsia %z 2004 Oct 31 2:00s 5:00 - %z # Atyraū (KZ-23) is like Mangghystaū except it switched from # +04/+05 to +05/+06 in spring 1999, not fall 1994. Zone Asia/Atyrau 3:27:44 - LMT 1924 May 2 3:00 - %z 1930 Jun 21 5:00 - %z 1981 Oct 1 6:00 - %z 1982 Apr 1 5:00 RussiaAsia %z 1991 Mar 31 2:00s 4:00 RussiaAsia %z 1992 Jan 19 2:00s 5:00 RussiaAsia %z 1999 Mar 28 2:00s 4:00 RussiaAsia %z 2004 Oct 31 2:00s 5:00 - %z # West Kazakhstan (KZ-27) # From Paul Eggert (2016-03-18): # The 1989 transition is from USSR act No. 227 (1989-03-14). Zone Asia/Oral 3:25:24 - LMT 1924 May 2 # or Ural'sk 3:00 - %z 1930 Jun 21 5:00 - %z 1981 Apr 1 5:00 1:00 %z 1981 Oct 1 6:00 - %z 1982 Apr 1 5:00 RussiaAsia %z 1989 Mar 26 2:00s 4:00 RussiaAsia %z 1992 Jan 19 2:00s 5:00 RussiaAsia %z 1992 Mar 29 2:00s 4:00 RussiaAsia %z 2004 Oct 31 2:00s 5:00 - %z # Kyrgyzstan (Kirgizstan) # Transitions through 1991 are from Shanks & Pottenger. # From Paul Eggert (2005-08-15): # According to an article dated today in the Kyrgyzstan Development Gateway # http://eng.gateway.kg/cgi-bin/page.pl?id=1&story_name=doc9979.shtml # Kyrgyzstan is canceling the daylight saving time system. I take the article # to mean that they will leave their clocks at 6 hours ahead of UTC. # From Malik Abdugaliev (2005-09-21): # Our government cancels daylight saving time 6th of August 2005. # From 2005-08-12 our GMT-offset is +6, w/o any daylight saving. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Kyrgyz 1992 1996 - Apr Sun>=7 0:00s 1:00 - Rule Kyrgyz 1992 1996 - Sep lastSun 0:00 0 - Rule Kyrgyz 1997 2005 - Mar lastSun 2:30 1:00 - Rule Kyrgyz 1997 2004 - Oct lastSun 2:30 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Bishkek 4:58:24 - LMT 1924 May 2 5:00 - %z 1930 Jun 21 6:00 RussiaAsia %z 1991 Mar 31 2:00s 5:00 RussiaAsia %z 1991 Aug 31 2:00 5:00 Kyrgyz %z 2005 Aug 12 6:00 - %z ############################################################################### # Korea (North and South) # From Annie I. Bang (2006-07-10): # http://www.koreaherald.com/view.php?ud=200607100012 # Korea ran a daylight saving program from 1949-61 but stopped it # during the 1950-53 Korean War. The system was temporarily enforced # between 1987 and 1988 ... # From Sanghyuk Jung (2014-10-29): # https://mm.icann.org/pipermail/tz/2014-October/021830.html # According to the Korean Wikipedia # https://ko.wikipedia.org/wiki/한국_표준시 # [oldid=12896437 2014-09-04 08:03 UTC] # DST in Republic of Korea was as follows.... And I checked old # newspapers in Korean, all articles correspond with data in Wikipedia. # For example, the article in 1948 (Korean Language) proved that DST # started at June 1 in that year. For another example, the article in # 1988 said that DST started at 2:00 AM in that year. # From Phake Nick (2018-10-27): # 1. According to official announcement from Korean government, the DST end # date in South Korea should be # 1955-09-08 without specifying time # http://theme.archives.go.kr/next/common/viewEbook.do?singleData=N&archiveEventId=0027977557 # 1956-09-29 without specifying time # http://theme.archives.go.kr/next/common/viewEbook.do?singleData=N&archiveEventId=0027978341 # 1957-09-21 24 o'clock # http://theme.archives.go.kr/next/common/viewEbook.do?singleData=N&archiveEventId=0027979690#3 # 1958-09-20 24 o'clock # http://theme.archives.go.kr/next/common/viewEbook.do?singleData=N&archiveEventId=0027981189 # 1959-09-19 24 o'clock # http://theme.archives.go.kr/next/common/viewEbook.do?singleData=N&archiveEventId=0027982974#2 # 1960-09-17 24 o'clock # http://theme.archives.go.kr/next/common/viewEbook.do?singleData=N&archiveEventId=0028044104 # ... # 2.... https://namu.wiki/w/대한민국%20표준시 ... [says] # when Korea was using GMT+8:30 as standard time, the international # aviation/marine/meteorological industry in the country refused to # follow and continued to use GMT+9:00 for interoperability. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule ROK 1948 only - Jun 1 0:00 1:00 D Rule ROK 1948 only - Sep 12 24:00 0 S Rule ROK 1949 only - Apr 3 0:00 1:00 D Rule ROK 1949 1951 - Sep Sat>=7 24:00 0 S Rule ROK 1950 only - Apr 1 0:00 1:00 D Rule ROK 1951 only - May 6 0:00 1:00 D Rule ROK 1955 only - May 5 0:00 1:00 D Rule ROK 1955 only - Sep 8 24:00 0 S Rule ROK 1956 only - May 20 0:00 1:00 D Rule ROK 1956 only - Sep 29 24:00 0 S Rule ROK 1957 1960 - May Sun>=1 0:00 1:00 D Rule ROK 1957 1960 - Sep Sat>=17 24:00 0 S Rule ROK 1987 1988 - May Sun>=8 2:00 1:00 D Rule ROK 1987 1988 - Oct Sun>=8 3:00 0 S # From Paul Eggert (2016-08-23): # The Korean Wikipedia entry gives the following sources for UT offsets: # # 1908: Official Journal Article No. 3994 (decree No. 5) # 1912: Governor-General of Korea Official Gazette Issue No. 367 # (Announcement No. 338) # 1954: Presidential Decree No. 876 (1954-03-17) # 1961: Law No. 676 (1961-08-07) # # (Another source "1987: Law No. 3919 (1986-12-31)" was in the 2014-10-30 # edition of the Korean Wikipedia entry.) # # I guessed that time zone abbreviations through 1945 followed the same # rules as discussed under Taiwan, with nominal switches from JST to KST # when the respective cities were taken over by the Allies after WWII. # # For Pyongyang, guess no changes from World War II until 2015, as we # have no information otherwise. # From Steffen Thorsen (2015-08-07): # According to many news sources, North Korea is going to change to # the 8:30 time zone on August 15, one example: # http://www.bbc.com/news/world-asia-33815049 # # From Paul Eggert (2015-08-15): # Bells rang out midnight (00:00) Friday as part of the celebrations. See: # Talmadge E. North Korea celebrates new time zone, 'Pyongyang Time' # http://news.yahoo.com/north-korea-celebrates-time-zone-pyongyang-time-164038128.html # There is no common English-language abbreviation for this time zone. # Use KST, as that's what we already use for 1954-1961 in ROK. # From Kang Seonghoon (2018-04-29): # North Korea will revert its time zone from UTC+8:30 (PYT; Pyongyang # Time) back to UTC+9 (KST; Korea Standard Time). # # From Seo Sanghyeon (2018-04-30): # Rodong Sinmun 2018-04-30 announced Pyongyang Time transition plan. # https://www.nknews.org/kcna/wp-content/uploads/sites/5/2018/04/rodong-2018-04-30.pdf # ... the transition date is 2018-05-05 ... Citation should be Decree # No. 2232 of April 30, 2018, of the Presidium of the Supreme People's # Assembly, as published in Rodong Sinmun. # From Tim Parenti (2018-04-29): # It appears to be the front page story at the top in the right-most column. # # From Paul Eggert (2018-05-04): # The BBC reported that the transition was from 23:30 to 24:00 today. # https://www.bbc.com/news/world-asia-44010705 # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Seoul 8:27:52 - LMT 1908 Apr 1 8:30 - KST 1912 Jan 1 9:00 - JST 1945 Sep 8 9:00 ROK K%sT 1954 Mar 21 8:30 ROK K%sT 1961 Aug 10 9:00 ROK K%sT Zone Asia/Pyongyang 8:23:00 - LMT 1908 Apr 1 8:30 - KST 1912 Jan 1 9:00 - JST 1945 Aug 24 9:00 - KST 2015 Aug 15 00:00 8:30 - KST 2018 May 4 23:30 9:00 - KST # Lebanon # # From Saadallah Itani (2023-03-23): # Lebanon ... announced today delay of Spring forward from March 25 to April 20. # # From Paul Eggert (2023-03-27): # This announcement was by the Lebanese caretaker prime minister Najib Mikati. # https://www.mtv.com.lb/en/News/Local/1352516/lebanon-postpones-daylight-saving-time-adoption # A video was later leaked to the media of parliament speaker Nabih Berri # asking Mikati to postpone DST to aid observance of Ramadan, Mikati objecting # that this would cause problems such as scheduling airline flights, to which # Berri interjected, "What flights?" # # The change was controversial and led to a partly-sectarian divide. # Many Lebanese institutions, including the education ministry, the Maronite # church, and two news channels LCBI and MTV, ignored the announcement and # went ahead with the long-scheduled spring-forward on March 25/26, some # arguing that the prime minister had not followed the law because the change # had not been approved by the cabinet. Google went with the announcement; # Apple ignored it. At least one bank followed the announcement for its doors, # but ignored the announcement in internal computer systems. # Beirut international airport listed two times for each departure. # Dan Azzi wrote "My view is that this whole thing is a Dumb and Dumber movie." # Eventually the prime minister backed down, said the cabinet had decided to # stick with its 1998 decision, and that DST would begin midnight March 29/30. # https://www.nna-leb.gov.lb/en/miscellaneous/604093/lebanon-has-two-times-of-day-amid-daylight-savings # https://www.cnbc.com/2023/03/27/lebanon-in-two-different-time-zones-as-government-disagrees-on-daylight-savings.html # # Although we could model the chaos with two Zones, that would likely cause # more trouble than it would cure. Since so many manual clocks and # computer-based timestamps ignored the announcement, stick with official # cabinet resolutions in the data while recording the prime minister's # announcement as a comment. This is how we treated a similar situation in # Rio de Janeiro in spring 1993. # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Lebanon 1920 only - Mar 28 0:00 1:00 S Rule Lebanon 1920 only - Oct 25 0:00 0 - Rule Lebanon 1921 only - Apr 3 0:00 1:00 S Rule Lebanon 1921 only - Oct 3 0:00 0 - Rule Lebanon 1922 only - Mar 26 0:00 1:00 S Rule Lebanon 1922 only - Oct 8 0:00 0 - Rule Lebanon 1923 only - Apr 22 0:00 1:00 S Rule Lebanon 1923 only - Sep 16 0:00 0 - Rule Lebanon 1957 1961 - May 1 0:00 1:00 S Rule Lebanon 1957 1961 - Oct 1 0:00 0 - Rule Lebanon 1972 only - Jun 22 0:00 1:00 S Rule Lebanon 1972 1977 - Oct 1 0:00 0 - Rule Lebanon 1973 1977 - May 1 0:00 1:00 S Rule Lebanon 1978 only - Apr 30 0:00 1:00 S Rule Lebanon 1978 only - Sep 30 0:00 0 - Rule Lebanon 1984 1987 - May 1 0:00 1:00 S Rule Lebanon 1984 1991 - Oct 16 0:00 0 - Rule Lebanon 1988 only - Jun 1 0:00 1:00 S Rule Lebanon 1989 only - May 10 0:00 1:00 S Rule Lebanon 1990 1992 - May 1 0:00 1:00 S Rule Lebanon 1992 only - Oct 4 0:00 0 - Rule Lebanon 1993 max - Mar lastSun 0:00 1:00 S Rule Lebanon 1993 1998 - Sep lastSun 0:00 0 - Rule Lebanon 1999 max - Oct lastSun 0:00 0 - # This one-time rule, announced by the prime minister first for April 21 # then for March 30, is commented out for reasons described above. #Rule Lebanon 2023 only - Mar 30 0:00 1:00 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Beirut 2:22:00 - LMT 1880 2:00 Lebanon EE%sT # Brunei # Malaysia (eastern) # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule NBorneo 1935 1941 - Sep 14 0:00 0:20 - Rule NBorneo 1935 1941 - Dec 14 0:00 0 - # # For peninsular Malaysia see Asia/Singapore. # # Sabah & Sarawak # From Paul Eggert (2014-08-12): # The data entries here are mostly from Shanks & Pottenger, but the 1942, 1945 # and 1982 transition dates are from Mok Ly Yng. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Kuching 7:21:20 - LMT 1926 Mar 7:30 - %z 1933 8:00 NBorneo %z 1942 Feb 16 9:00 - %z 1945 Sep 12 8:00 - %z # Maldives # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Indian/Maldives 4:54:00 - LMT 1880 # Malé 4:54:00 - MMT 1960 # Malé Mean Time 5:00 - %z # Mongolia # Shanks & Pottenger say that Mongolia has three time zones, but # The USNO (1995-12-21) and the CIA map Standard Time Zones of the World # (2005-03) both say that it has just one. # From Oscar van Vlijmen (1999-12-11): # General Information Mongolia # (1999-09) # "Time: Mongolia has two time zones. Three westernmost provinces of # Bayan-Ölgii, Uvs, and Hovd are one hour earlier than the capital city, and # the rest of the country follows the Ulaanbaatar time, which is UTC/GMT plus # eight hours." # From Rives McDow (1999-12-13): # Mongolia discontinued the use of daylight savings time in 1999; 1998 # being the last year it was implemented. The dates of implementation I am # unsure of, but most probably it was similar to Russia, except for the time # of implementation may have been different.... # Some maps in the past have indicated that there was an additional time # zone in the eastern part of Mongolia, including the provinces of Dornod, # Sükhbaatar, and possibly Khentii. # From Paul Eggert (1999-12-15): # Naming and spelling is tricky in Mongolia. # We'll use Hovd (also spelled Chovd and Khovd) to represent the west zone; # the capital of the Hovd province is sometimes called Hovd, sometimes Dund-Us, # and sometimes Jirgalanta (with variant spellings), but the name Hovd # is good enough for our purposes. # From Rives McDow (2001-05-13): # In addition to Mongolia starting daylight savings as reported earlier # (adopted DST on 2001-04-27 02:00 local time, ending 2001-09-28), # there are three time zones. # # Provinces [at 7:00]: Bayan-Ölgii, Uvs, Khovd, Zavkhan, Govi-Altai # Provinces [at 8:00]: Khövsgöl, Bulgan, Arkhangai, Khentii, Töv, # Bayankhongor, Övörkhangai, Dundgovi, Dornogovi, Ömnögovi # Provinces [at 9:00]: Dornod, Sükhbaatar # # [The province of Selenge is omitted from the above lists.] # From Ganbold Ts., Ulaanbaatar (2004-04-17): # Daylight saving occurs at 02:00 local time last Saturday of March. # It will change back to normal at 02:00 local time last Saturday of # September.... As I remember this rule was changed in 2001. # # From Paul Eggert (2004-04-17): # For now, assume Rives McDow's informant got confused about Friday vs # Saturday, and that his 2001 dates should have 1 added to them. # From Paul Eggert (2005-07-26): # We have wildly conflicting information about Mongolia's time zones. # Bill Bonnet (2005-05-19) reports that the US Embassy in Ulaanbaatar says # there is only one time zone and that DST is observed, citing Microsoft # Windows XP as the source. Risto Nykänen (2005-05-16) reports that # travelmongolia.org says there are two time zones (UT +07, +08) with no DST. # Oscar van Vlijmen (2005-05-20) reports that the Mongolian Embassy in # Washington, DC says there are two time zones, with DST observed. # He also found # http://ubpost.mongolnews.mn/index.php?subaction=showcomments&id=1111634894&archive=&start_from=&ucat=1& # which also says that there is DST, and which has a comment by "Toddius" # (2005-03-31 06:05 +0700) saying "Mongolia actually has 3.5 time zones. # The West (OLGII) is +7 GMT, most of the country is ULAT is +8 GMT # and some Eastern provinces are +9 GMT but Sükhbaatar Aimag is SUHK +8.5 GMT. # The SUKH timezone is new this year, it is one of the few things the # parliament passed during the tumultuous winter session." # For now, let's ignore this information, until we have more confirmation. # From Ganbold Ts. (2007-02-26): # Parliament of Mongolia has just changed the daylight-saving rule in February. # They decided not to adopt daylight-saving time.... # http://www.mongolnews.mn/index.php?module=unuudur&sec=view&id=15742 # From Deborah Goldsmith (2008-03-30): # We received a bug report claiming that the tz database UTC offset for # Asia/Choibalsan (GMT+09:00) is incorrect, and that it should be GMT # +08:00 instead. Different sources appear to disagree with the tz # database on this, e.g.: # # https://www.timeanddate.com/worldclock/city.html?n=1026 # http://www.worldtimeserver.com/current_time_in_MN.aspx # # both say GMT+08:00. # From Steffen Thorsen (2008-03-31): # eznis airways, which operates several domestic flights, has a flight # schedule here: # http://www.eznis.com/Container.jsp?id=112 # (click the English flag for English) # # There it appears that flights between Choibalsan and Ulaanbaatar arrive # about 1:35 - 1:50 hours later in local clock time, no matter the # direction, while Ulaanbaatar-Khovd takes 2 hours in the Eastern # direction and 3:35 back, which indicates that Ulaanbaatar and Khovd are # in different time zones (like we know about), while Choibalsan and # Ulaanbaatar are in the same time zone (correction needed). # From Arthur David Olson (2008-05-19): # Assume that Choibalsan is indeed offset by 8:00. # From Heitor David Pinto (2024-06-23): # Sources about time zones in Mongolia seem to list one of two conflicting # configurations. The first configuration, mentioned in a comment to the TZ # database in 1999, citing a Mongolian government website, lists the provinces # of Bayan-Ölgii, Khovd and Uvs in UTC+7, and the rest of the country in # UTC+8. The second configuration, mentioned in a comment to the database in # 2001, lists Bayan-Ölgii, Khovd, Uvs, Govi-Altai and Zavkhan in UTC+7, Dornod # and Sükhbaatar in UTC+9, and the rest of the country in UTC+8. # # The first configuration is still mentioned by several Mongolian travel # agencies: # https://www.adventurerider.mn/en/page/about_mongolia # http://www.naturetours.mn/nt/mongolia.php # https://www.newjuulchin.mn/web/content/7506?unique=fa24a0f6e96e022a3578ee5195ac879638c734ce # # It also matches these flight schedules in 2013: # http://web.archive.org/web/20130722023600/https://www.hunnuair.com/en/timetabled # The flight times imply that the airports of Uliastai (Zavkhan), Choibalsan # (Dornod) and Altai (Govi-Altai) are in the same time zone as Ulaanbaatar, # and Khovd is one hour behind.... # # The second configuration was mentioned by an official of the Mongolian # standards agency in an interview in 2014: https://ikon.mn/n/9v6 # And it's still listed by the Mongolian aviation agency: # https://ais.mn/files/aip/eAIP/2023-12-25/html/eSUP/ZM-eSUP-23-04-en-MN.html # # ... I believe that the first configuration is what is actually observed in # Mongolia and has been so all along, at least since 1999. The second # configuration closely matches the ideal time zone boundaries at 97.5° E and # 112.5° E but it doesn't seem to be used in practice. # From Ganbold Tsagaankhuu (2015-03-10): # It seems like yesterday Mongolian Government meeting has concluded to use # daylight saving time in Mongolia.... Starting at 2:00AM of last Saturday of # March 2015, daylight saving time starts. And 00:00AM of last Saturday of # September daylight saving time ends. Source: # http://zasag.mn/news/view/8969 # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Mongol 1983 1984 - Apr 1 0:00 1:00 - Rule Mongol 1983 only - Oct 1 0:00 0 - # Shanks & Pottenger and IATA SSIM say 1990s switches occurred at 00:00, # but McDow says the 2001 switches occurred at 02:00. Also, IATA SSIM # (1996-09) says 1996-10-25. Go with Shanks & Pottenger through 1998. # # Shanks & Pottenger say that the Sept. 1984 through Sept. 1990 switches # in Choibalsan (more precisely, in Dornod and Sükhbaatar) took place # at 02:00 standard time, not at 00:00 local time as in the rest of # the country. That would be odd, and possibly is a result of their # correction of 02:00 (in the previous edition) not being done correctly # in the latest edition; so ignore it for now. # From Ganbold Tsagaankhuu (2017-02-09): # Mongolian Government meeting has concluded today to cancel daylight # saving time adoption in Mongolia. Source: http://zasag.mn/news/view/16192 Rule Mongol 1985 1998 - Mar lastSun 0:00 1:00 - Rule Mongol 1984 1998 - Sep lastSun 0:00 0 - # IATA SSIM (1999-09) says Mongolia no longer observes DST. Rule Mongol 2001 only - Apr lastSat 2:00 1:00 - Rule Mongol 2001 2006 - Sep lastSat 2:00 0 - Rule Mongol 2002 2006 - Mar lastSat 2:00 1:00 - Rule Mongol 2015 2016 - Mar lastSat 2:00 1:00 - Rule Mongol 2015 2016 - Sep lastSat 0:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] # Hovd, a.k.a. Chovd, Dund-Us, Dzhargalant, Khovd, Jirgalanta Zone Asia/Hovd 6:06:36 - LMT 1905 Aug 6:00 - %z 1978 7:00 Mongol %z # Ulaanbaatar, a.k.a. Ulan Bataar, Ulan Bator, Urga Zone Asia/Ulaanbaatar 7:07:32 - LMT 1905 Aug 7:00 - %z 1978 8:00 Mongol %z # Nepal # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Kathmandu 5:41:16 - LMT 1920 5:30 - %z 1986 5:45 - %z # Pakistan # From Rives McDow (2002-03-13): # I have been advised that Pakistan has decided to adopt dst on a # TRIAL basis for one year, starting 00:01 local time on April 7, 2002 # and ending at 00:01 local time October 6, 2002. This is what I was # told, but I believe that the actual time of change may be 00:00; the # 00:01 was to make it clear which day it was on. # From Paul Eggert (2002-03-15): # Jesper Nørgaard found this URL: # http://www.pak.gov.pk/public/news/app/app06_dec.htm # (dated 2001-12-06) which says that the Cabinet adopted a scheme "to # advance the clocks by one hour on the night between the first # Saturday and Sunday of April and revert to the original position on # 15th October each year". This agrees with McDow's 04-07 at 00:00, # but disagrees about the October transition, and makes it sound like # it's not on a trial basis. Also, the "between the first Saturday # and Sunday of April" phrase, if taken literally, means that the # transition takes place at 00:00 on the first Sunday on or after 04-02. # From Paul Eggert (2003-02-09): # DAWN reported on 2002-10-05 # that 2002 DST ended that day at midnight. Go with McDow for now. # From Steffen Thorsen (2003-03-14): # According to http://www.dawn.com/2003/03/07/top15.htm # there will be no DST in Pakistan this year: # # ISLAMABAD, March 6: Information and Media Development Minister Sheikh # Rashid Ahmed on Thursday said the cabinet had reversed a previous # decision to advance clocks by one hour in summer and put them back by # one hour in winter with the aim of saving light hours and energy. # # The minister told a news conference that the experiment had rather # shown 8 per cent higher consumption of electricity. # From Alex Krivenyshev (2008-05-15): # # Here is an article that Pakistan plan to introduce Daylight Saving Time # on June 1, 2008 for 3 months. # # "... The federal cabinet on Wednesday announced a new conservation plan to # help reduce load shedding by approving the closure of commercial centres at # 9pm and moving clocks forward by one hour for the next three months. ...." # # http://www.worldtimezone.com/dst_news/dst_news_pakistan01.html # http://www.dailytimes.com.pk/default.asp?page=2008\05\15\story_15-5-2008_pg1_4 # From Arthur David Olson (2008-05-19): # XXX--midnight transitions is a guess; 2008 only is a guess. # From Alexander Krivenyshev (2008-08-28): # Pakistan government has decided to keep the watches one-hour advanced # for another 2 months - plan to return to Standard Time on October 31 # instead of August 31. # # http://www.worldtimezone.com/dst_news/dst_news_pakistan02.html # http://dailymailnews.com/200808/28/news/dmbrn03.html # From Alexander Krivenyshev (2009-04-08): # Based on previous media reports that "... proposed plan to # advance clocks by one hour from May 1 will cause disturbance # to the working schedules rather than bringing discipline in # official working." # http://www.thenews.com.pk/daily_detail.asp?id=171280 # # recent news that instead of May 2009 - Pakistan plan to # introduce DST from April 15, 2009 # # FYI: Associated Press Of Pakistan # April 08, 2009 # Cabinet okays proposal to advance clocks by one hour from April 15 # http://www.app.com.pk/en_/index.php?option=com_content&task=view&id=73043&Itemid=1 # http://www.worldtimezone.com/dst_news/dst_news_pakistan05.html # # .... # The Federal Cabinet on Wednesday approved the proposal to # advance clocks in the country by one hour from April 15 to # conserve energy" # From Steffen Thorsen (2009-09-17): # "The News International," Pakistan reports that: "The Federal # Government has decided to restore the previous time by moving the # clocks backward by one hour from October 1. A formal announcement to # this effect will be made after the Prime Minister grants approval in # this regard." # http://www.thenews.com.pk/updates.asp?id=87168 # From Alexander Krivenyshev (2009-09-28): # According to Associated Press Of Pakistan, it is confirmed that # Pakistan clocks across the country would be turned back by an hour from # October 1, 2009. # # "Clocks to go back one hour from 1 Oct" # http://www.app.com.pk/en_/index.php?option=com_content&task=view&id=86715&Itemid=2 # http://www.worldtimezone.com/dst_news/dst_news_pakistan07.htm # # From Steffen Thorsen (2009-09-29): # Now they seem to have changed their mind, November 1 is the new date: # http://www.thenews.com.pk/top_story_detail.asp?Id=24742 # "The country's clocks will be reversed by one hour on November 1. # Officials of Federal Ministry for Interior told this to Geo News on # Monday." # # And more importantly, it seems that these dates will be kept every year: # "It has now been decided that clocks will be wound forward by one hour # on April 15 and reversed by an hour on November 1 every year without # obtaining prior approval, the officials added." # # We have confirmed this year's end date with both with the Ministry of # Water and Power and the Pakistan Electric Power Company: # https://www.timeanddate.com/news/time/pakistan-ends-dst09.html # From Christoph Göhre (2009-10-01): # [T]he German Consulate General in Karachi reported me today that Pakistan # will go back to standard time on 1st of November. # From Steffen Thorsen (2010-03-26): # Steffen Thorsen wrote: # > On Thursday (2010-03-25) it was announced that DST would start in # > Pakistan on 2010-04-01. # > # > Then today, the president said that they might have to revert the # > decision if it is not supported by the parliament. So at the time # > being, it seems unclear if DST will be actually observed or not - but # > April 1 could be a more likely date than April 15. # Now, it seems that the decision to not observe DST in final: # # "Govt Withdraws Plan To Advance Clocks" # http://www.apakistannews.com/govt-withdraws-plan-to-advance-clocks-172041 # # "People laud PM's announcement to end DST" # http://www.app.com.pk/en_/index.php?option=com_content&task=view&id=99374&Itemid=2 # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Pakistan 2002 only - Apr Sun>=2 0:00 1:00 S Rule Pakistan 2002 only - Oct Sun>=2 0:00 0 - Rule Pakistan 2008 only - Jun 1 0:00 1:00 S Rule Pakistan 2008 2009 - Nov 1 0:00 0 - Rule Pakistan 2009 only - Apr 15 0:00 1:00 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Karachi 4:28:12 - LMT 1907 5:30 - %z 1942 Sep 5:30 1:00 %z 1945 Oct 15 5:30 - %z 1951 Sep 30 5:00 - %z 1971 Mar 26 5:00 Pakistan PK%sT # Pakistan Time # Palestine # From Amos Shapir (1998-02-15): # # From 1917 until 1948-05-15, all of Palestine, including the parts now # known as the Gaza Strip and the West Bank, was under British rule. # Therefore the rules given for Israel for that period, apply there too... # # The Gaza Strip was under Egyptian rule between 1948-05-15 until 1967-06-05 # (except a short occupation by Israel from 1956-11 till 1957-03, but no # time zone was affected then). It was never formally annexed to Egypt, # though. # # The rest of Palestine was under Jordanian rule at that time, formally # annexed in 1950 as the West Bank (and the word "Trans" was dropped from # the country's previous name of "the Hashemite Kingdom of the # Trans-Jordan"). So the rules for Jordan for that time apply. Major # towns in that area are Nablus (Shchem), El-Halil (Hebron), Ramallah, and # East Jerusalem. # # Both areas were occupied by Israel in June 1967, but not annexed (except # for East Jerusalem). They were on Israel time since then; there might # have been a Military Governor's order about time zones, but I'm not aware # of any (such orders may have been issued semi-annually whenever summer # time was in effect, but maybe the legal aspect of time was just neglected). # # The Palestinian Authority was established in 1993, and got hold of most # towns in the West Bank and Gaza by 1995. I know that in order to # demonstrate...independence, they have been switching to # summer time and back on a different schedule than Israel's, but I don't # know when this was started, or what algorithm is used (most likely the # Jordanian one). # # To summarize, the table should probably look something like that: # # Area \ when | 1918-1947 | 1948-1967 | 1967-1995 | 1996- # ------------+-----------+-----------+-----------+----------- # Israel | Zion | Zion | Zion | Zion # West bank | Zion | Jordan | Zion | Jordan # Gaza | Zion | Egypt | Zion | Jordan # # I guess more info may be available from the PA's web page (if/when they # have one). # From Paul Eggert (2006-03-22): # Shanks & Pottenger write that Gaza did not observe DST until 1957, but go # with Shapir and assume that it observed DST from 1940 through 1947, # and that it used Jordanian rules starting in 1996. # We don't yet need a separate entry for the West Bank, since # the only differences between it and Gaza that we know about # occurred before our cutoff date of 1970. # However, as we get more information, we may need to add entries # for parts of the West Bank as they transitioned from Israel's rules # to Palestine's rules. # From IINS News Service - Israel - 1998-03-23 10:38:07 Israel time, # forwarded by Ephraim Silverberg: # # Despite the fact that Israel changed over to daylight savings time # last week, the PLO Authority (PA) has decided not to turn its clocks # one-hour forward at this time. As a sign of independence from Israeli rule, # the PA has decided to implement DST in April. # From Paul Eggert (1999-09-20): # Daoud Kuttab writes in Holiday havoc # http://www.jpost.com/com/Archive/22.Apr.1999/Opinion/Article-2.html # (Jerusalem Post, 1999-04-22) that # the Palestinian National Authority changed to DST on 1999-04-15. # I vaguely recall that they switch back in October (sorry, forgot the source). # For now, let's assume that the spring switch was at 24:00, # and that they switch at 0:00 on the 3rd Fridays of April and October. # From Paul Eggert (2005-11-22): # Starting 2004 transitions are from Steffen Thorsen's web site timeanddate.com. # From Steffen Thorsen (2005-11-23): # A user from Gaza reported that Gaza made the change early because of # the Ramadan. Next year Ramadan will be even earlier, so I think # there is a good chance next year's end date will be around two weeks # earlier - the same goes for Jordan. # From Steffen Thorsen (2006-08-17): # I was informed by a user in Bethlehem that in Bethlehem it started the # same day as Israel, and after checking with other users in the area, I # was informed that they started DST one day after Israel. I was not # able to find any authoritative sources at the time, nor details if # Gaza changed as well, but presumed Gaza to follow the same rules as # the West Bank. # From Steffen Thorsen (2006-09-26): # according to the Palestine News Network (2006-09-19): # http://english.pnn.ps/index.php?option=com_content&task=view&id=596&Itemid=5 # > The Council of Ministers announced that this year its winter schedule # > will begin early, as of midnight Thursday. It is also time to turn # > back the clocks for winter. Friday will begin an hour late this week. # I guess it is likely that next year's date will be moved as well, # because of the Ramadan. # From Jesper Nørgaard Welen (2007-09-18): # According to Steffen Thorsen's web site the Gaza Strip and the rest of the # Palestinian territories left DST early on 13.th. of September at 2:00. # From Paul Eggert (2007-09-20): # My understanding is that Gaza and the West Bank disagree even over when # the weekend is (Thursday+Friday versus Friday+Saturday), so I'd be a bit # surprised if they agreed about DST. But for now, assume they agree. # For lack of better information, predict that future changes will be # the 2nd Thursday of September at 02:00. # From Alexander Krivenyshev (2008-08-28): # Here is an article, that Mideast running on different clocks at Ramadan. # # Gaza Strip (as Egypt) ended DST at midnight Thursday (Aug 28, 2008), while # the West Bank will end Daylight Saving Time at midnight Sunday (Aug 31, 2008). # # http://www.guardian.co.uk/world/feedarticle/7759001 # http://www.abcnews.go.com/International/wireStory?id=5676087 # http://www.worldtimezone.com/dst_news/dst_news_gazastrip01.html # From Alexander Krivenyshev (2009-03-26): # According to the Palestine News Network (arabic.pnn.ps), Palestinian # government decided to start Daylight Time on Thursday night March # 26 and continue until the night of 27 September 2009. # # (in Arabic) # http://arabic.pnn.ps/index.php?option=com_content&task=view&id=50850 # # (English translation) # http://www.worldtimezone.com/dst_news/dst_news_westbank01.html # From Steffen Thorsen (2009-08-31): # Palestine's Council of Ministers announced that they will revert back to # winter time on Friday, 2009-09-04. # # One news source: # http://www.safa.ps/ara/?action=showdetail&seid=4158 # (Palestinian press agency, Arabic), # Google translate: "Decided that the Palestinian government in Ramallah # headed by Salam Fayyad, the start of work in time for the winter of # 2009, starting on Friday approved the fourth delay Sept. clock sixty # minutes per hour as of Friday morning." # # We are not sure if Gaza will do the same, last year they had a different # end date, we will keep this page updated: # https://www.timeanddate.com/news/time/westbank-gaza-dst-2009.html # From Alexander Krivenyshev (2009-09-02): # Seems that Gaza Strip will go back to Winter Time same date as West Bank. # # According to Palestinian Ministry Of Interior, West Bank and Gaza Strip plan # to change time back to Standard time on September 4, 2009. # # "Winter time unite the West Bank and Gaza" # (from Palestinian National Authority): # http://www.moi.gov.ps/en/?page=633167343250594025&nid=11505 # http://www.worldtimezone.com/dst_news/dst_news_gazastrip02.html # From Alexander Krivenyshev (2010-03-19): # According to Voice of Palestine DST will last for 191 days, from March # 26, 2010 till "the last Sunday before the tenth day of Tishri # (October), each year" (October 03, 2010?) # # http://palvoice.org/forums/showthread.php?t=245697 # (in Arabic) # http://www.worldtimezone.com/dst_news/dst_news_westbank03.html # From Steffen Thorsen (2010-03-24): # ...Ma'an News Agency reports that Hamas cabinet has decided it will # start one day later, at 12:01am. Not sure if they really mean 12:01am or # noon though: # # http://www.maannews.net/eng/ViewDetails.aspx?ID=271178 # (Ma'an News Agency) # "At 12:01am Friday, clocks in Israel and the West Bank will change to # 1:01am, while Gaza clocks will change at 12:01am Saturday morning." # From Steffen Thorsen (2010-08-11): # According to several sources, including # http://www.maannews.net/eng/ViewDetails.aspx?ID=306795 # the clocks were set back one hour at 2010-08-11 00:00:00 local time in # Gaza and the West Bank. # Some more background info: # https://www.timeanddate.com/news/time/westbank-gaza-end-dst-2010.html # From Steffen Thorsen (2011-08-26): # Gaza and the West Bank did go back to standard time in the beginning of # August, and will now enter daylight saving time again on 2011-08-30 # 00:00 (so two periods of DST in 2011). The pause was because of # Ramadan. # # http://www.maannews.net/eng/ViewDetails.aspx?ID=416217 # Additional info: # https://www.timeanddate.com/news/time/palestine-dst-2011.html # From Alexander Krivenyshev (2011-08-27): # According to the article in The Jerusalem Post: # "...Earlier this month, the Palestinian government in the West Bank decided to # move to standard time for 30 days, during Ramadan. The Palestinians in the # Gaza Strip accepted the change and also moved their clocks one hour back. # The Hamas government said on Saturday that it won't observe summertime after # the Muslim feast of Id al-Fitr, which begins on Tuesday..." # ... # https://www.jpost.com/MiddleEast/Article.aspx?id=235650 # http://www.worldtimezone.com/dst_news/dst_news_gazastrip05.html # The rules for Egypt are stolen from the 'africa' file. # From Steffen Thorsen (2011-09-30): # West Bank did end Daylight Saving Time this morning/midnight (2011-09-30 # 00:00). # So West Bank and Gaza now have the same time again. # # Many sources, including: # http://www.maannews.net/eng/ViewDetails.aspx?ID=424808 # From Steffen Thorsen (2012-03-26): # Palestinian news sources tell that both Gaza and West Bank will start DST # on Friday (Thursday midnight, 2012-03-29 24:00). # Some of many sources in Arabic: # http://www.samanews.com/index.php?act=Show&id=122638 # # http://safa.ps/details/news/74352/بدء-التوقيت-الصي�ي-بالض�ة-وغزة-ليلة-الجمعة.html # # Our brief summary: # https://www.timeanddate.com/news/time/gaza-west-bank-dst-2012.html # From Steffen Thorsen (2013-03-26): # The following news sources tells that Palestine will "start daylight saving # time from midnight on Friday, March 29, 2013" (translated). # [These are in Arabic and are for Gaza and for Ramallah, respectively.] # http://www.samanews.com/index.php?act=Show&id=154120 # http://safa.ps/details/news/99844/رام-الله-بدء-التوقيت-الصي�ي-29-الجاري.html # From Steffen Thorsen (2013-09-24): # The Gaza and West Bank are ending DST Thursday at midnight # (2013-09-27 00:00:00) (one hour earlier than last year...). # This source in English, says "that winter time will go into effect # at midnight on Thursday in the West Bank and Gaza Strip": # http://english.wafa.ps/index.php?action=detail&id=23246 # official source...: # http://www.palestinecabinet.gov.ps/ar/Views/ViewDetails.aspx?pid=1252 # From Steffen Thorsen (2015-03-03): # Sources such as http://www.alquds.com/news/article/view/id/548257 # and https://www.raya.ps/ar/news/890705.html say Palestine areas will # start DST on 2015-03-28 00:00 which is one day later than expected. # # From Paul Eggert (2015-03-03): # https://www.timeanddate.com/time/change/west-bank/ramallah?year=2014 # says that the fall 2014 transition was Oct 23 at 24:00. # From Hannah Kreitem (2016-03-09): # http://www.palestinecabinet.gov.ps/WebSite/ar/ViewDetails?ID=31728 # [Google translation]: "The Council also decided to start daylight # saving in Palestine as of one o'clock on Saturday morning, # 2016-03-26, to provide the clock 60 minutes ahead." # From Sharef Mustafa (2016-10-19): # [T]he Palestinian cabinet decision (Mar 8th 2016) published on # http://www.palestinecabinet.gov.ps/WebSite/Upload/Decree/GOV_17/16032016134830.pdf # states that summer time will end on Oct 29th at 01:00. # From Sharef Mustafa (2018-03-16): # Palestine summer time will start on Mar 24th 2018 ... # http://www.palestinecabinet.gov.ps/Website/AR/NDecrees/ViewFile.ashx?ID=e7a42ab7-ee23-435a-b9c8-a4f7e81f3817 # From Even Scharning (2019-03-23): # http://pnn.ps/news/401130 # http://palweather.ps/ar/node/50136.html # # From Sharif Mustafa (2019-03-26): # The Palestinian cabinet announced today that the switch to DST will # be on Fri Mar 29th 2019 by advancing the clock by 60 minutes. # http://palestinecabinet.gov.ps/Website/AR/NDecrees/ViewFile.ashx?ID=e54e9ea1-50ee-4137-84df-0d6c78da259b # # From Even Scharning (2019-04-10): # Our source in Palestine said it happened Friday 29 at 00:00 local time.... # From Sharef Mustafa (2019-10-18): # Palestine summer time will end on midnight Oct 26th 2019 ... # # From Steffen Thorsen (2020-10-20): # Some sources such as these say, and display on clocks, that DST ended at # midnight last year... # https://www.amad.ps/ar/post/320006 # # From Tim Parenti (2020-10-20): # The report of the Palestinian Cabinet meeting of 2019-10-14 confirms # a decision on (translated): "The start of the winter time in Palestine, by # delaying the clock by sixty minutes, starting from midnight on Friday / # Saturday corresponding to 26/10/2019." # http://www.palestinecabinet.gov.ps/portal/meeting/details/43948 # From Sharef Mustafa (2020-10-20): # As per the palestinian cabinet announcement yesterday , the day light saving # shall [end] on Oct 24th 2020 at 01:00AM by delaying the clock by 60 minutes. # http://www.palestinecabinet.gov.ps/portal/Meeting/Details/51584 # From Pierre Cashon (2020-10-20): # The summer time this year started on March 28 at 00:00. # https://wafa.ps/ar_page.aspx?id=GveQNZa872839351758aGveQNZ # http://www.palestinecabinet.gov.ps/portal/meeting/details/50284 # The winter time in 2015 started on October 23 at 01:00. # https://wafa.ps/ar_page.aspx?id=CgpCdYa670694628582aCgpCdY # http://www.palestinecabinet.gov.ps/portal/meeting/details/27583 # From P Chan (2021-10-18): # http://wafa.ps/Pages/Details/34701 # Palestine winter time will start from midnight 2021-10-29 (Thursday-Friday). # # From Heba Hemad, Palestine Ministry of Telecom & IT (2021-10-20): # ... winter time will begin in Palestine from Friday 10-29, 01:00 AM # by 60 minutes backwards. # # From Tim Parenti (2021-10-25), per Paul Eggert (2021-10-24): # Guess future fall transitions at 01:00 on the Friday preceding October's # last Sunday (i.e., Fri>=23), as this is more consistent with recent practice. # From Heba Hamad (2022-03-10): # summer time will begin in Palestine from Sunday 03-27-2022, 00:00 AM. # From Heba Hamad (2022-08-30): # winter time will begin in Palestine from Saturday 10-29, 02:00 AM by # 60 minutes backwards. Also the state of Palestine adopted the summer # and winter time for the years: 2023,2024,2025,2026 ... # https://mm.icann.org/pipermail/tz/attachments/20220830/9f024566/Time-0001.pdf # (2022-08-31): ... the Saturday before the last Sunday in March and October # at 2:00 AM ,for the years from 2023 to 2026. # (2022-09-05): https://mtit.pna.ps/Site/New/1453 # From Heba Hamad (2023-03-22): # ... summer time will begin in Palestine from Saturday 04-29-2023, # 02:00 AM by 60 minutes forward. # From Heba Hemad (2023-10-09): # ... winter time will begin in Palestine from Saturday 10-28-2023, # 02:00 AM by 60 minutes back. # # From Heba Hamad (2024-01-25): # the summer time for the years 2024,2025 will begin in Palestine # from Saturday at 02:00 AM by 60 minutes forward as shown below: # year date # 2024 2024-04-20 # 2025 2025-04-12 # # From Paul Eggert (2024-01-25): # For now, guess that spring and fall transitions will normally # continue to use 2022's rules, that during DST Palestine will switch # to standard time at 02:00 the last Saturday before Ramadan and back # to DST at 02:00 the second Saturday after Ramadan, and that # if the normal spring-forward or fall-back transition occurs during # Ramadan the former is delayed and the latter advanced. # To implement this, I predicted Ramadan-oriented transition dates for # 2026 through 2086 by running the following program under GNU Emacs 29.2, # with the results integrated by hand into the table below. # Predictions after 2086 are approximated without Ramadan. # # (let ((islamic-year 1447)) # (require 'cal-islam) # (while (< islamic-year 1510) # (let ((a (calendar-islamic-to-absolute (list 9 1 islamic-year))) # (b (+ 1 (calendar-islamic-to-absolute (list 10 1 islamic-year)))) # (saturday 6)) # (while (/= saturday (mod (setq a (1- a)) 7))) # (while (/= saturday (mod b 7)) # (setq b (1+ b))) # (setq b (+ 7 b)) # (setq a (calendar-gregorian-from-absolute a)) # (setq b (calendar-gregorian-from-absolute b)) # (insert # (format # (concat "Rule Palestine\t%d\tonly\t-\t%s\t%2d\t2:00\t0\t-\n" # "Rule Palestine\t%d\tonly\t-\t%s\t%2d\t2:00\t1:00\tS\n") # (car (cdr (cdr a))) (calendar-month-name (car a) t) (car (cdr a)) # (car (cdr (cdr b))) (calendar-month-name (car b) t) (car (cdr b))))) # (setq islamic-year (+ 1 islamic-year)))) # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule EgyptAsia 1957 only - May 10 0:00 1:00 S Rule EgyptAsia 1957 1958 - Oct 1 0:00 0 - Rule EgyptAsia 1958 only - May 1 0:00 1:00 S Rule EgyptAsia 1959 1967 - May 1 1:00 1:00 S Rule EgyptAsia 1959 1965 - Sep 30 3:00 0 - Rule EgyptAsia 1966 only - Oct 1 3:00 0 - Rule Palestine 1999 2005 - Apr Fri>=15 0:00 1:00 S Rule Palestine 1999 2003 - Oct Fri>=15 0:00 0 - Rule Palestine 2004 only - Oct 1 1:00 0 - Rule Palestine 2005 only - Oct 4 2:00 0 - Rule Palestine 2006 2007 - Apr 1 0:00 1:00 S Rule Palestine 2006 only - Sep 22 0:00 0 - Rule Palestine 2007 only - Sep 13 2:00 0 - Rule Palestine 2008 2009 - Mar lastFri 0:00 1:00 S Rule Palestine 2008 only - Sep 1 0:00 0 - Rule Palestine 2009 only - Sep 4 1:00 0 - Rule Palestine 2010 only - Mar 26 0:00 1:00 S Rule Palestine 2010 only - Aug 11 0:00 0 - Rule Palestine 2011 only - Apr 1 0:01 1:00 S Rule Palestine 2011 only - Aug 1 0:00 0 - Rule Palestine 2011 only - Aug 30 0:00 1:00 S Rule Palestine 2011 only - Sep 30 0:00 0 - Rule Palestine 2012 2014 - Mar lastThu 24:00 1:00 S Rule Palestine 2012 only - Sep 21 1:00 0 - Rule Palestine 2013 only - Sep 27 0:00 0 - Rule Palestine 2014 only - Oct 24 0:00 0 - Rule Palestine 2015 only - Mar 28 0:00 1:00 S Rule Palestine 2015 only - Oct 23 1:00 0 - Rule Palestine 2016 2018 - Mar Sat<=30 1:00 1:00 S Rule Palestine 2016 2018 - Oct Sat<=30 1:00 0 - Rule Palestine 2019 only - Mar 29 0:00 1:00 S Rule Palestine 2019 only - Oct Sat<=30 0:00 0 - Rule Palestine 2020 2021 - Mar Sat<=30 0:00 1:00 S Rule Palestine 2020 only - Oct 24 1:00 0 - Rule Palestine 2021 only - Oct 29 1:00 0 - Rule Palestine 2022 only - Mar 27 0:00 1:00 S Rule Palestine 2022 2035 - Oct Sat<=30 2:00 0 - Rule Palestine 2023 only - Apr 29 2:00 1:00 S Rule Palestine 2024 only - Apr 20 2:00 1:00 S Rule Palestine 2025 only - Apr 12 2:00 1:00 S Rule Palestine 2026 2054 - Mar Sat<=30 2:00 1:00 S Rule Palestine 2036 only - Oct 18 2:00 0 - Rule Palestine 2037 only - Oct 10 2:00 0 - Rule Palestine 2038 only - Sep 25 2:00 0 - Rule Palestine 2039 only - Sep 17 2:00 0 - Rule Palestine 2040 only - Sep 1 2:00 0 - Rule Palestine 2040 only - Oct 20 2:00 1:00 S Rule Palestine 2040 2067 - Oct Sat<=30 2:00 0 - Rule Palestine 2041 only - Aug 24 2:00 0 - Rule Palestine 2041 only - Oct 5 2:00 1:00 S Rule Palestine 2042 only - Aug 16 2:00 0 - Rule Palestine 2042 only - Sep 27 2:00 1:00 S Rule Palestine 2043 only - Aug 1 2:00 0 - Rule Palestine 2043 only - Sep 19 2:00 1:00 S Rule Palestine 2044 only - Jul 23 2:00 0 - Rule Palestine 2044 only - Sep 3 2:00 1:00 S Rule Palestine 2045 only - Jul 15 2:00 0 - Rule Palestine 2045 only - Aug 26 2:00 1:00 S Rule Palestine 2046 only - Jun 30 2:00 0 - Rule Palestine 2046 only - Aug 18 2:00 1:00 S Rule Palestine 2047 only - Jun 22 2:00 0 - Rule Palestine 2047 only - Aug 3 2:00 1:00 S Rule Palestine 2048 only - Jun 6 2:00 0 - Rule Palestine 2048 only - Jul 25 2:00 1:00 S Rule Palestine 2049 only - May 29 2:00 0 - Rule Palestine 2049 only - Jul 10 2:00 1:00 S Rule Palestine 2050 only - May 21 2:00 0 - Rule Palestine 2050 only - Jul 2 2:00 1:00 S Rule Palestine 2051 only - May 6 2:00 0 - Rule Palestine 2051 only - Jun 24 2:00 1:00 S Rule Palestine 2052 only - Apr 27 2:00 0 - Rule Palestine 2052 only - Jun 8 2:00 1:00 S Rule Palestine 2053 only - Apr 12 2:00 0 - Rule Palestine 2053 only - May 31 2:00 1:00 S Rule Palestine 2054 only - Apr 4 2:00 0 - Rule Palestine 2054 only - May 23 2:00 1:00 S Rule Palestine 2055 only - May 8 2:00 1:00 S Rule Palestine 2056 only - Apr 29 2:00 1:00 S Rule Palestine 2057 only - Apr 14 2:00 1:00 S Rule Palestine 2058 only - Apr 6 2:00 1:00 S Rule Palestine 2059 max - Mar Sat<=30 2:00 1:00 S Rule Palestine 2068 only - Oct 20 2:00 0 - Rule Palestine 2069 only - Oct 12 2:00 0 - Rule Palestine 2070 only - Oct 4 2:00 0 - Rule Palestine 2071 only - Sep 19 2:00 0 - Rule Palestine 2072 only - Sep 10 2:00 0 - Rule Palestine 2072 only - Oct 22 2:00 1:00 S Rule Palestine 2072 max - Oct Sat<=30 2:00 0 - Rule Palestine 2073 only - Sep 2 2:00 0 - Rule Palestine 2073 only - Oct 14 2:00 1:00 S Rule Palestine 2074 only - Aug 18 2:00 0 - Rule Palestine 2074 only - Oct 6 2:00 1:00 S Rule Palestine 2075 only - Aug 10 2:00 0 - Rule Palestine 2075 only - Sep 21 2:00 1:00 S Rule Palestine 2076 only - Jul 25 2:00 0 - Rule Palestine 2076 only - Sep 12 2:00 1:00 S Rule Palestine 2077 only - Jul 17 2:00 0 - Rule Palestine 2077 only - Sep 4 2:00 1:00 S Rule Palestine 2078 only - Jul 9 2:00 0 - Rule Palestine 2078 only - Aug 20 2:00 1:00 S Rule Palestine 2079 only - Jun 24 2:00 0 - Rule Palestine 2079 only - Aug 12 2:00 1:00 S Rule Palestine 2080 only - Jun 15 2:00 0 - Rule Palestine 2080 only - Jul 27 2:00 1:00 S Rule Palestine 2081 only - Jun 7 2:00 0 - Rule Palestine 2081 only - Jul 19 2:00 1:00 S Rule Palestine 2082 only - May 23 2:00 0 - Rule Palestine 2082 only - Jul 11 2:00 1:00 S Rule Palestine 2083 only - May 15 2:00 0 - Rule Palestine 2083 only - Jun 26 2:00 1:00 S Rule Palestine 2084 only - Apr 29 2:00 0 - Rule Palestine 2084 only - Jun 17 2:00 1:00 S Rule Palestine 2085 only - Apr 21 2:00 0 - Rule Palestine 2085 only - Jun 9 2:00 1:00 S Rule Palestine 2086 only - Apr 13 2:00 0 - Rule Palestine 2086 only - May 25 2:00 1:00 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Gaza 2:17:52 - LMT 1900 Oct 2:00 Zion EET/EEST 1948 May 15 2:00 EgyptAsia EE%sT 1967 Jun 5 2:00 Zion I%sT 1996 2:00 Jordan EE%sT 1999 2:00 Palestine EE%sT 2008 Aug 29 0:00 2:00 - EET 2008 Sep 2:00 Palestine EE%sT 2010 2:00 - EET 2010 Mar 27 0:01 2:00 Palestine EE%sT 2011 Aug 1 2:00 - EET 2012 2:00 Palestine EE%sT Zone Asia/Hebron 2:20:23 - LMT 1900 Oct 2:00 Zion EET/EEST 1948 May 15 2:00 EgyptAsia EE%sT 1967 Jun 5 2:00 Zion I%sT 1996 2:00 Jordan EE%sT 1999 2:00 Palestine EE%sT # Paracel Is # no information # Philippines # From Paul Eggert (2024-01-21): # The Spanish initially used American (west-of-Greenwich) time. # It is unknown what time Manila kept when the British occupied it from # 1762-10-06 through 1764-04; for now assume it kept American time. # On 1844-08-16, Narciso Clavería, governor-general of the # Philippines, issued a proclamation announcing that 1844-12-30 was to # be immediately followed by 1845-01-01; see R.H. van Gent's # History of the International Date Line # https://webspace.science.uu.nl/~gent0113/idl/idl_philippines.htm # From P Chan (2021-05-10): # Here's a fairly comprehensive article in Japanese: # https://wiki.suikawiki.org/n/Philippine%20Time # (2021-05-16): # According to the references listed in the article, # the periods that the Philippines (Manila) observed DST or used +9 are: # # 1936-10-31 24:00 to 1937-01-15 24:00 # (Proclamation No. 104, Proclamation No. 126) # 1941-12-15 24:00 to 1945-11-30 24:00 # (Proclamation No. 789, Proclamation No. 20) # 1954-04-11 24:00 to 1954-06-04 24:00 # (Proclamation No. 13, Proclamation No. 33) # 1977-03-27 24:00 to 1977-09-21 24:00 # (Proclamation No. 1629, Proclamation No. 1641) # 1990-05-21 00:00 to 1990-07-28 24:00 # (National Emergency Memorandum Order No. 17, Executive Order No. 415) # # Proclamation No. 104 ... October 30, 1936 # https://www.officialgazette.gov.ph/1936/10/30/proclamation-no-104-s-1936/ # Proclamation No. 126 ... January 15, 1937 # https://www.officialgazette.gov.ph/1937/01/15/proclamation-no-126-s-1937/ # Proclamation No. 789 ... December 13, 1941 # https://www.officialgazette.gov.ph/1941/12/13/proclamation-no-789-s-1941/ # Proclamation No. 20 ... November 11, 1945 # https://www.officialgazette.gov.ph/1945/11/11/proclamation-no-20-s-1945/ # Proclamation No. 13 ... April 6, 1954 # https://www.officialgazette.gov.ph/1954/04/06/proclamation-no-13-s-1954/ # Proclamation No. 33 ... June 3, 1954 # https://www.officialgazette.gov.ph/1954/06/03/proclamation-no-33-s-1954/ # Proclamation No. 1629 ... March 25, 1977 # https://www.officialgazette.gov.ph/1977/03/25/proclamation-no-1629-s-1977/ # Proclamation No. 1641 ...May 26, 1977 # https://www.officialgazette.gov.ph/1977/05/26/proclamation-no-1641-s-1977/ # National Emergency Memorandum Order No. 17 ... May 2, 1990 # https://www.officialgazette.gov.ph/1990/05/02/national-emergency-memorandum-order-no-17-s-1990/ # Executive Order No. 415 ... July 20, 1990 # https://www.officialgazette.gov.ph/1990/07/20/executive-order-no-415-s-1990/ # # During WWII, Proclamation No. 789 fixed two periods of DST. The first period # was set to continue only until January 31, 1942. But Manila was occupied by # the Japanese earlier in the month.... # # For the date of the adoption of standard time, Shank[s] gives 1899-05-11. # The article is not able to state the basis of that. I guess it was based on # a US War Department Circular issued on that date. # https://books.google.com/books?id=JZ1PAAAAYAAJ&pg=RA3-PA8 # # However, according to other sources, standard time was adopted on # 1899-09-06. Also, the LMT was GMT+8:03:52 # https://books.google.com/books?id=MOYIAQAAIAAJ&pg=PA521 # https://books.google.com/books?id=lSnqqatpYikC&pg=PA21 # # From Paul Eggert (2024-09-05): # The penultimate URL in P Chan's email refers to page 521 of # Selga M, The Time Service in the Philippines. # Proc Pan-Pacific Science Congress. Vol. 1 (1923), 519-532. # It says, "The change from the meridian 120° 58' 04" to the 120th implied a # change of 3 min. 52s.26 in time; consequently on 6th September, 1899, # Manila Observatory gave the noon signal 3 min. 52s.26 later than before". # # Wikipedia says the US declared Manila liberated on March 4, 1945; # this doesn't affect clocks, just our time zone abbreviation and DST flag. # From Paul Goyette (2018-06-15) with URLs updated by Guy Harris (2024-02-15): # In the Philippines, there is a national law, Republic Act No. 10535 # which declares the official time here as "Philippine Standard Time". # The act [1] even specifies use of PST as the abbreviation, although # the FAQ provided by PAGASA [2] uses the "acronym PhST to distinguish # it from the Pacific Standard Time (PST)." # [1] https://www.officialgazette.gov.ph/2013/05/15/republic-act-no-10535/ # [2] https://prsd.pagasa.dost.gov.ph/index.php/28-astronomy/302-philippine-standard-time # # From Paul Eggert (2018-06-19): # I surveyed recent news reports, and my impression is that "PST" is # more popular among reliable English-language news sources. This is # not just a measure of Google hit counts: it's also the sizes and # influence of the sources. There is no current abbreviation for DST, # so use "PDT", the usual American style. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Phil 1936 only - Oct 31 24:00 1:00 D Rule Phil 1937 only - Jan 15 24:00 0 S Rule Phil 1941 only - Dec 15 24:00 1:00 D # The following three rules were canceled by Japan: #Rule Phil 1942 only - Jan 31 24:00 0 S #Rule Phil 1942 only - Mar 1 0:00 1:00 D #Rule Phil 1942 only - Jun 30 24:00 0 S Rule Phil 1945 only - Nov 30 24:00 0 S Rule Phil 1954 only - Apr 11 24:00 1:00 D Rule Phil 1954 only - Jun 4 24:00 0 S Rule Phil 1977 only - Mar 27 24:00 1:00 D Rule Phil 1977 only - Sep 21 24:00 0 S Rule Phil 1990 only - May 21 0:00 1:00 D Rule Phil 1990 only - Jul 28 24:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Manila -15:56:08 - LMT 1844 Dec 31 8:03:52 - LMT 1899 Sep 6 4:00u 8:00 Phil P%sT 1942 Feb 11 24:00 9:00 - JST 1945 Mar 4 8:00 Phil P%sT # Bahrain # Qatar # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Qatar 3:26:08 - LMT 1920 # Al Dawhah / Doha 4:00 - %z 1972 Jun 3:00 - %z # Kuwait # Saudi Arabia # Yemen # # Japan's year-round bases in Antarctica match this since 1970. # # From Paul Eggert (2018-08-29): # Time in Saudi Arabia and other countries in the Arabian peninsula was not # standardized until 1968 or so; we don't know exactly when, and possibly it # has never been made official. Richard P Hunt, in "Islam city yielding to # modern times", New York Times (1961-04-09), p 20, wrote that only airlines # observed standard time, and that people in Jeddah mostly observed quasi-solar # time, doing so by setting their watches at sunrise to 6 o'clock (or to 12 # o'clock for "Arab" time). # # Timekeeping differed depending on who you were and which part of Saudi # Arabia you were in. In 1969, Elias Antar wrote that although a common # practice had been to set one's watch to 12:00 (i.e., midnight) at sunset - # which meant that the time on one side of a mountain could differ greatly from # the time on the other side - many foreigners set their watches to 6pm # instead, while airlines instead used UTC +03 (except in Dhahran, where they # used UTC +04), Aramco used UTC +03 with DST, and the Trans-Arabian Pipe Line # Company used Aramco time in eastern Saudi Arabia and airline time in western. # (The American Military Aid Advisory Group used plain UTC.) Antar writes, # "A man named Higgins, so the story goes, used to run a local power # station. One day, the whole thing became too much for Higgins and he # assembled his staff and laid down the law. 'I've had enough of this,' he # shrieked. 'It is now 12 o'clock Higgins Time, and from now on this station is # going to run on Higgins Time.' And so, until last year, it did." See: # Antar E. Dinner at When? Saudi Aramco World, 1969 March/April. 2-3. # http://archive.aramcoworld.com/issue/196902/dinner.at.when.htm # Also see: Antar EN. Arabian flying is confusing. # Port Angeles (WA) Evening News. 1965-03-10. page 3. # # The TZ database cannot represent quasi-solar time; airline time is the best # we can do. The 1946 foreign air news digest of the U.S. Civil Aeronautics # Board (OCLC 42299995) reported that the "... Arabian Government, inaugurated # a weekly Dhahran-Cairo service, via the Saudi Arabian cities of Riyadh and # Jidda, on March 14, 1947". Shanks & Pottenger guessed 1950; go with the # earlier date. # # Shanks & Pottenger also state that until 1968-05-01 Saudi Arabia had two # time zones; the other zone, at UT +04, was in the far eastern part of # the country. Presumably this is documenting airline time. Ignore this, # as it's before our 1970 cutoff. # # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Riyadh 3:06:52 - LMT 1947 Mar 14 3:00 - %z # Singapore # taken from Mok Ly Yng (2003-10-30) # https://web.archive.org/web/20190822231045/http://www.math.nus.edu.sg/~mathelmr/teaching/timezone.html # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Singapore 6:55:25 - LMT 1901 Jan 1 6:55:25 - SMT 1905 Jun 1 # Singapore M.T. 7:00 - %z 1933 Jan 1 7:00 0:20 %z 1936 Jan 1 7:20 - %z 1941 Sep 1 7:30 - %z 1942 Feb 16 9:00 - %z 1945 Sep 12 7:30 - %z 1981 Dec 31 16:00u 8:00 - %z # Spratly Is # no information # Sri Lanka # From Paul Eggert (2013-02-21): # Milne says "Madras mean time use from May 1, 1898. Prior to this Colombo # mean time, 5h. 4m. 21.9s. F., was used." But 5:04:21.9 differs considerably # from Colombo's meridian 5:19:24, so for now ignore Milne and stick with # Shanks and Pottenger. # From Paul Eggert (1996-09-03): # "Sri Lanka advances clock by an hour to avoid blackout" # (, 1996-05-24, # no longer available as of 1999-08-17) # reported "the country's standard time will be put forward by one hour at # midnight Friday (1830 GMT) 'in the light of the present power crisis'." # # From Dharmasiri Senanayake, Sri Lanka Media Minister (1996-10-24), as quoted # by Shamindra in Daily News - Hot News Section # (1996-10-26): # With effect from 12.30 a.m. on 26th October 1996 # Sri Lanka will be six (06) hours ahead of GMT. # From Jesper Nørgaard Welen (2006-04-14), quoting Sri Lanka News Online # (2006-04-13): # 0030 hrs on April 15, 2006 (midnight of April 14, 2006 +30 minutes) # at present, become 2400 hours of April 14, 2006 (midnight of April 14, 2006). # From Peter Apps and Ranga Sirila of Reuters (2006-04-12) in: # http://today.reuters.co.uk/news/newsArticle.aspx?type=scienceNews&storyID=2006-04-12T172228Z_01_COL295762_RTRIDST_0_SCIENCE-SRILANKA-TIME-DC.XML # [The Tamil Tigers] never accepted the original 1996 time change and simply # kept their clocks set five and a half hours ahead of Greenwich Mean # Time (GMT), in line with neighbor India. # From Paul Eggert (2006-04-18): # People who live in regions under Tamil control can use [TZ='Asia/Kolkata'], # as that zone has agreed with the Tamil areas since our cutoff date of 1970. # From Sadika Sumanapala (2016-10-19): # According to http://www.sltime.org (maintained by Measurement Units, # Standards & Services Department, Sri Lanka) abbreviation for Sri Lanka # standard time is SLST. # # From Paul Eggert (2016-10-18): # "SLST" seems to be reasonably recent and rarely used outside time # zone nerd sources. I searched Google News and found three uses of # it in the International Business Times of India in February and # March of this year when discussing cricket match times, but nothing # since then (though there has been a lot of cricket) and nothing in # other English-language news sources. Our old abbreviation "LKT" is # even worse. For now, let's use a numeric abbreviation; we can # switch to "SLST" if it catches on. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Colombo 5:19:24 - LMT 1880 5:19:32 - MMT 1906 # Moratuwa Mean Time 5:30 - %z 1942 Jan 5 5:30 0:30 %z 1942 Sep 5:30 1:00 %z 1945 Oct 16 2:00 5:30 - %z 1996 May 25 0:00 6:30 - %z 1996 Oct 26 0:30 6:00 - %z 2006 Apr 15 0:30 5:30 - %z # Syria # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Syria 1920 1923 - Apr Sun>=15 2:00 1:00 S Rule Syria 1920 1923 - Oct Sun>=1 2:00 0 - Rule Syria 1962 only - Apr 29 2:00 1:00 S Rule Syria 1962 only - Oct 1 2:00 0 - Rule Syria 1963 1965 - May 1 2:00 1:00 S Rule Syria 1963 only - Sep 30 2:00 0 - Rule Syria 1964 only - Oct 1 2:00 0 - Rule Syria 1965 only - Sep 30 2:00 0 - Rule Syria 1966 only - Apr 24 2:00 1:00 S Rule Syria 1966 1976 - Oct 1 2:00 0 - Rule Syria 1967 1978 - May 1 2:00 1:00 S Rule Syria 1977 1978 - Sep 1 2:00 0 - Rule Syria 1983 1984 - Apr 9 2:00 1:00 S Rule Syria 1983 1984 - Oct 1 2:00 0 - Rule Syria 1986 only - Feb 16 2:00 1:00 S Rule Syria 1986 only - Oct 9 2:00 0 - Rule Syria 1987 only - Mar 1 2:00 1:00 S Rule Syria 1987 1988 - Oct 31 2:00 0 - Rule Syria 1988 only - Mar 15 2:00 1:00 S Rule Syria 1989 only - Mar 31 2:00 1:00 S Rule Syria 1989 only - Oct 1 2:00 0 - Rule Syria 1990 only - Apr 1 2:00 1:00 S Rule Syria 1990 only - Sep 30 2:00 0 - Rule Syria 1991 only - Apr 1 0:00 1:00 S Rule Syria 1991 1992 - Oct 1 0:00 0 - Rule Syria 1992 only - Apr 8 0:00 1:00 S Rule Syria 1993 only - Mar 26 0:00 1:00 S Rule Syria 1993 only - Sep 25 0:00 0 - # IATA SSIM (1998-02) says 1998-04-02; # (1998-09) says 1999-03-29 and 1999-09-29; (1999-02) says 1999-04-02, # 2000-04-02, and 2001-04-02; (1999-09) says 2000-03-31 and 2001-03-31; # (2006) says 2006-03-31 and 2006-09-22; # for now ignore all these claims and go with Shanks & Pottenger, # except for the 2006-09-22 claim (which seems right for Ramadan). Rule Syria 1994 1996 - Apr 1 0:00 1:00 S Rule Syria 1994 2005 - Oct 1 0:00 0 - Rule Syria 1997 1998 - Mar lastMon 0:00 1:00 S Rule Syria 1999 2006 - Apr 1 0:00 1:00 S # From Stephen Colebourne (2006-09-18): # According to IATA data, Syria will change DST on 21st September [21:00 UTC] # this year [only].... This is probably related to Ramadan, like Egypt. Rule Syria 2006 only - Sep 22 0:00 0 - # From Paul Eggert (2007-03-29): # Today the AP reported "Syria will switch to summertime at midnight Thursday." # http://www.iht.com/articles/ap/2007/03/29/africa/ME-GEN-Syria-Time-Change.php Rule Syria 2007 only - Mar lastFri 0:00 1:00 S # From Jesper Nørgaard (2007-10-27): # The sister center ICARDA of my work CIMMYT is confirming that Syria DST will # not take place 1st November at 0:00 o'clock but 1st November at 24:00 or # rather Midnight between Thursday and Friday. This does make more sense than # having it between Wednesday and Thursday (two workdays in Syria) since the # weekend in Syria is not Saturday and Sunday, but Friday and Saturday. So now # it is implemented at midnight of the last workday before weekend... # # From Steffen Thorsen (2007-10-27): # Jesper Nørgaard Welen wrote: # # > "Winter local time in Syria will be observed at midnight of Thursday 1 # > November 2007, and the clock will be put back 1 hour." # # I found confirmation on this in this gov.sy-article (Arabic): # http://wehda.alwehda.gov.sy/_print_veiw.asp?FileName=12521710520070926111247 # # which using Google's translate tools says: # Council of Ministers also approved the commencement of work on # identifying the winter time as of Friday, 2/11/2007 where the 60th # minute delay at midnight Thursday 1/11/2007. Rule Syria 2007 only - Nov Fri>=1 0:00 0 - # From Stephen Colebourne (2008-03-17): # For everyone's info, I saw an IATA time zone change for [Syria] for # this month (March 2008) in the last day or so.... # Country Time Standard --- DST Start --- --- DST End --- DST # Name Zone Variation Time Date Time Date # Variation # Syrian Arab # Republic SY +0200 2200 03APR08 2100 30SEP08 +0300 # 2200 02APR09 2100 30SEP09 +0300 # 2200 01APR10 2100 30SEP10 +0300 # From Arthur David Olson (2008-03-17): # Here's a link to English-language coverage by the Syrian Arab News # Agency (SANA)... # http://www.sana.sy/eng/21/2008/03/11/165173.htm # ...which reads (in part) "The Cabinet approved the suggestion of the # Ministry of Electricity to begin daylight savings time on Friday April # 4th, advancing clocks one hour ahead on midnight of Thursday April 3rd." # Since Syria is two hours east of UTC, the 2200 and 2100 transition times # shown above match up with midnight in Syria. # From Arthur David Olson (2008-03-18): # My best guess at a Syrian rule is "the Friday nearest April 1"; # coding that involves either using a "Mar Fri>=29" construct that old time zone # compilers can't handle or having multiple Rules (a la Israel). # For now, use "Apr Fri>=1", and go with IATA on a uniform Sep 30 end. # From Steffen Thorsen (2008-10-07): # Syria has now officially decided to end DST on 2008-11-01 this year, # according to the following article in the Syrian Arab News Agency (SANA). # # The article is in Arabic, and seems to tell that they will go back to # winter time on 2008-11-01 at 00:00 local daylight time (delaying/setting # clocks back 60 minutes). # # http://sana.sy/ara/2/2008/10/07/195459.htm # From Steffen Thorsen (2009-03-19): # Syria will start DST on 2009-03-27 00:00 this year according to many sources, # two examples: # # http://www.sana.sy/eng/21/2009/03/17/217563.htm # (English, Syrian Arab News # Agency) # http://thawra.alwehda.gov.sy/_View_news2.asp?FileName=94459258720090318012209 # (Arabic, gov-site) # # We have not found any sources saying anything about when DST ends this year. # # Our summary # https://www.timeanddate.com/news/time/syria-dst-starts-march-27-2009.html # From Steffen Thorsen (2009-10-27): # The Syrian Arab News Network on 2009-09-29 reported that Syria will # revert back to winter (standard) time on midnight between Thursday # 2009-10-29 and Friday 2009-10-30: # http://www.sana.sy/ara/2/2009/09/29/247012.htm (Arabic) # From Arthur David Olson (2009-10-28): # We'll see if future DST switching times turn out to be end of the last # Thursday of the month or the start of the last Friday of the month or # something else. For now, use the start of the last Friday. # From Steffen Thorsen (2010-03-17): # The "Syrian News Station" reported on 2010-03-16 that the Council of # Ministers has decided that Syria will start DST on midnight Thursday # 2010-04-01: (midnight between Thursday and Friday): # http://sns.sy/sns/?path=news/read/11421 (Arabic) # From Steffen Thorsen (2012-03-26): # Today, Syria's government announced that they will start DST early on Friday # (00:00). This is a bit earlier than the past two years. # # From Syrian Arab News Agency, in Arabic: # http://www.sana.sy/ara/2/2012/03/26/408215.htm # # Our brief summary: # https://www.timeanddate.com/news/time/syria-dst-2012.html # From Steffen Thorsen (2022-10-05): # Syria is adopting year-round DST, starting this autumn.... # From https://www.enabbaladi.net/archives/607812 # "This [the decision] came after the weekly government meeting today, # Tuesday 4 October ..." # # From Paul Eggert (2022-10-05): # Like Jordan, model this as a transition from EEST +03 (DST) to plain +03 # (non-DST) at the point where DST would otherwise have ended. Rule Syria 2008 only - Apr Fri>=1 0:00 1:00 S Rule Syria 2008 only - Nov 1 0:00 0 - Rule Syria 2009 only - Mar lastFri 0:00 1:00 S Rule Syria 2010 2011 - Apr Fri>=1 0:00 1:00 S Rule Syria 2012 2022 - Mar lastFri 0:00 1:00 S Rule Syria 2009 2022 - Oct lastFri 0:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Damascus 2:25:12 - LMT 1920 # Dimashq 2:00 Syria EE%sT 2022 Oct 28 0:00 3:00 - %z # Tajikistan # From Shanks & Pottenger. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Dushanbe 4:35:12 - LMT 1924 May 2 5:00 - %z 1930 Jun 21 6:00 RussiaAsia %z 1991 Mar 31 2:00s 5:00 1:00 %z 1991 Sep 9 2:00s 5:00 - %z # Cambodia # Christmas I # Laos # Thailand # Vietnam (northern) # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Bangkok 6:42:04 - LMT 1880 6:42:04 - BMT 1920 Apr # Bangkok Mean Time 7:00 - %z # Turkmenistan # From Shanks & Pottenger. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Ashgabat 3:53:32 - LMT 1924 May 2 # or Ashkhabad 4:00 - %z 1930 Jun 21 5:00 RussiaAsia %z 1991 Mar 31 2:00 4:00 RussiaAsia %z 1992 Jan 19 2:00 5:00 - %z # Oman # Réunion # Seychelles # United Arab Emirates # # The Crozet Is also observe Réunion time; see the 'antarctica' file. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Dubai 3:41:12 - LMT 1920 4:00 - %z # Uzbekistan # Byalokoz 1919 says Uzbekistan was 4:27:53. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Asia/Samarkand 4:27:53 - LMT 1924 May 2 4:00 - %z 1930 Jun 21 5:00 - %z 1981 Apr 1 5:00 1:00 %z 1981 Oct 1 6:00 - %z 1982 Apr 1 5:00 RussiaAsia %z 1992 5:00 - %z # Milne says Tashkent was 4:37:10.8. #STDOFF 4:37:10.8 Zone Asia/Tashkent 4:37:11 - LMT 1924 May 2 5:00 - %z 1930 Jun 21 6:00 RussiaAsia %z 1991 Mar 31 2:00 5:00 RussiaAsia %z 1992 5:00 - %z # Vietnam (southern) # From Paul Eggert (2014-10-04): # Milne gives 7:16:56 for the meridian of Saigon in 1899, as being # used in Lower Laos, Cambodia, and Annam. But this is quite a ways # from Saigon's location. For now, ignore this and stick with Shanks # and Pottenger for LMT before 1906. # From Arthur David Olson (2008-03-18): # The English-language name of Vietnam's most populous city is "Ho Chi Minh # City"; use Ho_Chi_Minh below to avoid a name of more than 14 characters. # From Paul Eggert (2024-01-14) after a 2014 heads-up from Trần Ng�c Quân # and a 2024-01-14 heads-up from �oàn Trần Công Danh: # Trần Tiến Bình's authoritative book "Lịch Việt Nam: thế kỷ XX-XXI (1901-2100)" # (Nhà xuất bản Văn Hoá - Thông Tin, Hanoi, 2005), pp 49-50, # is quoted verbatim in: # http://www.thoigian.com.vn/?mPage=P80D01 # is translated by Brian Inglis in: # https://mm.icann.org/pipermail/tz/2014-October/021654.html # and is the basis for the information below. # # The 1906 transition was effective July 1 and standardized Indochina to # Phù Liễn Observatory, legally 104° 17' 17" east of Paris. # It's unclear whether this meant legal Paris Mean Time (00:09:21) or # the Paris Meridian; for now guess the former and round the exact # 07:06:30.1333... to 07:06:30.13 as the legal spec used 66 2/3 ms precision. # which is used below even though the modern-day Phù Liễn Observatory # is closer to 07:06:31. Abbreviate Phù Liễn Mean Time as PLMT. # # The following transitions occurred in Indochina in general (before 1954) # and in South Vietnam in particular (after 1954): # To 07:00 on 1911-05-01. # To 08:00 on 1942-12-31 at 23:00. # To 09:00 on 1945-03-14 at 23:00. # To 07:00 on 1945-09-02 in Vietnam. # To 08:00 on 1947-04-01 in French-controlled Indochina. # To 07:00 on 1955-07-01 in South Vietnam. # To 08:00 on 1959-12-31 at 23:00 in South Vietnam. # To 07:00 on 1975-06-13 in South Vietnam. # # Trần cites the following sources; it's unclear which supplied the info above. # # Hoàng Xuân Hãn: "Lịch và lịch Việt Nam". Tập san Khoa h�c Xã hội, # No. 9, Paris, February 1982. # # Lê Thành Lân: "Lịch và niên biểu lịch sử hai mươi thế kỷ (0001-2010)", # NXB Thống kê, Hanoi, 2000. # # Lê Thành Lân: "Lịch hai thế kỷ (1802-2010) và các lịch vĩnh cửu", # NXB Thuận Hoá, Huế, 1995. # # Here is the decision for the September 1945 transition: # Võ Nguyên Giáp, Việt Nam Dân Quốc Công Báo, No. 1 (1945-09-29), page 13 # http://baochi.nlv.gov.vn/baochi/cgi-bin/baochi?a=d&d=JwvzO19450929.2.5&dliv=none # It says that on 1945-09-01 at 24:00, Vietnam moved back two hours, to +07. # It also mentions a 1945-03-29 decree (by a Japanese Governor-General) # to set the time zone to +09, but does not say whether that decree # merely legalized an earlier change to +09. # # July 1955 transition: # Ngô �ình Diệm, Công Báo Việt Nam, No. 92 (1955-07-02), page 1780-1781 # Ordinance (Dụ) No. 46 (1955-06-25) # http://ddsnext.crl.edu/titles/32341#?c=0&m=29&s=0&cv=4&r=0&xywh=-89%2C342%2C1724%2C1216 # It says that on 1955-07-01 at 01:00, South Vietnam moved back 1 hour (to +07). # # December 1959 transition: # Ngô �ình Diệm, Công Báo Việt Nam Cộng Hòa, 1960 part 1 (1960-01-02), page 62 # Decree (Sắc lệnh) No. 362-TTP (1959-12-30) # http://ddsnext.crl.edu/titles/32341#?c=0&m=138&s=0&cv=793&r=0&xywh=-54%2C1504%2C1705%2C1202 # It says that on 1959-12-31 at 23:00, South Vietnam moved forward 1 hour (to +08). # Zone NAME STDOFF RULES FORMAT [UNTIL] #STDOFF 7:06:30.13 Zone Asia/Ho_Chi_Minh 7:06:30 - LMT 1906 Jul 1 7:06:30 - PLMT 1911 May 1 # Phù Liễn MT 7:00 - %z 1942 Dec 31 23:00 8:00 - %z 1945 Mar 14 23:00 9:00 - %z 1945 Sep 1 24:00 7:00 - %z 1947 Apr 1 8:00 - %z 1955 Jul 1 01:00 7:00 - %z 1959 Dec 31 23:00 8:00 - %z 1975 Jun 13 7:00 - %z # From Paul Eggert (2019-02-19): # # The Ho Chi Minh entry suffices for most purposes as it agrees with all of # Vietnam since 1975-06-13. Presumably clocks often changed in south Vietnam # in the early 1970s as locations changed hands during the war; however the # details are unknown and would likely be too voluminous for this database. # # For timestamps in north Vietnam back to 1970 (the tzdb cutoff), # use Asia/Bangkok; see the VN entries in the file zone1970.tab. # For timestamps before 1970, see Asia/Hanoi in the file 'backzone'. pr0m1th3as-datatypes-9450b29/inst/tzdata/australasia000066400000000000000000003004431527370255000224020ustar00rootroot00000000000000# tzdb data for Australasia and environs, and for much of the Pacific # This file is in the public domain, so clarified as of # 2009-05-17 by Arthur David Olson. # This file also includes Pacific islands. # Notes are at the end of this file ############################################################################### # Australia # Please see the notes below for the controversy about "EST" versus "AEST" etc. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Aus 1917 only - Jan 1 2:00s 1:00 D Rule Aus 1917 only - Mar lastSun 2:00s 0 S Rule Aus 1942 only - Jan 1 2:00s 1:00 D Rule Aus 1942 only - Mar lastSun 2:00s 0 S Rule Aus 1942 only - Sep 27 2:00s 1:00 D Rule Aus 1943 1944 - Mar lastSun 2:00s 0 S Rule Aus 1943 only - Oct 3 2:00s 1:00 D # Zone NAME STDOFF RULES FORMAT [UNTIL] # Northern Territory Zone Australia/Darwin 8:43:20 - LMT 1895 Feb 9:00 - ACST 1899 May 9:30 Aus AC%sT # Western Australia # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule AW 1974 only - Oct lastSun 2:00s 1:00 D Rule AW 1975 only - Mar Sun>=1 2:00s 0 S Rule AW 1983 only - Oct lastSun 2:00s 1:00 D Rule AW 1984 only - Mar Sun>=1 2:00s 0 S Rule AW 1991 only - Nov 17 2:00s 1:00 D Rule AW 1992 only - Mar Sun>=1 2:00s 0 S Rule AW 2006 only - Dec 3 2:00s 1:00 D Rule AW 2007 2009 - Mar lastSun 2:00s 0 S Rule AW 2007 2008 - Oct lastSun 2:00s 1:00 D Zone Australia/Perth 7:43:24 - LMT 1895 Dec 8:00 Aus AW%sT 1943 Jul 8:00 AW AW%sT Zone Australia/Eucla 8:35:28 - LMT 1895 Dec 8:45 Aus %z 1943 Jul 8:45 AW %z # Queensland # # From Alex Livingston (1996-11-01): # I have heard or read more than once that some resort islands off the coast # of Queensland chose to keep observing daylight-saving time even after # Queensland ceased to. # # From Paul Eggert (1996-11-22): # IATA SSIM (1993-02/1994-09) say that the Holiday Islands (Hayman, Lindeman, # Hamilton) observed DST for two years after the rest of Queensland stopped. # Hamilton is the largest, but there is also a Hamilton in Victoria, # so use Lindeman. # # From J William Piggott (2016-02-20): # There is no location named Holiday Islands in Queensland Australia; holiday # islands is a colloquial term used globally. Hayman and Lindeman are at the # north and south extremes of the Whitsunday Islands archipelago, and # Hamilton is in between; it is reasonable to believe that this time zone # applies to all of the Whitsundays. # http://www.australia.gov.au/about-australia/australian-story/austn-islands # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule AQ 1971 only - Oct lastSun 2:00s 1:00 D Rule AQ 1972 only - Feb lastSun 2:00s 0 S Rule AQ 1989 1991 - Oct lastSun 2:00s 1:00 D Rule AQ 1990 1992 - Mar Sun>=1 2:00s 0 S Rule Holiday 1992 1993 - Oct lastSun 2:00s 1:00 D Rule Holiday 1993 1994 - Mar Sun>=1 2:00s 0 S Zone Australia/Brisbane 10:12:08 - LMT 1895 10:00 Aus AE%sT 1971 10:00 AQ AE%sT Zone Australia/Lindeman 9:55:56 - LMT 1895 10:00 Aus AE%sT 1971 10:00 AQ AE%sT 1992 Jul 10:00 Holiday AE%sT # South Australia # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule AS 1971 1985 - Oct lastSun 2:00s 1:00 D Rule AS 1986 only - Oct 19 2:00s 1:00 D Rule AS 1987 2007 - Oct lastSun 2:00s 1:00 D Rule AS 1972 only - Feb 27 2:00s 0 S Rule AS 1973 1985 - Mar Sun>=1 2:00s 0 S Rule AS 1986 1990 - Mar Sun>=15 2:00s 0 S Rule AS 1991 only - Mar 3 2:00s 0 S Rule AS 1992 only - Mar 22 2:00s 0 S Rule AS 1993 only - Mar 7 2:00s 0 S Rule AS 1994 only - Mar 20 2:00s 0 S Rule AS 1995 2005 - Mar lastSun 2:00s 0 S Rule AS 2006 only - Apr 2 2:00s 0 S Rule AS 2007 only - Mar lastSun 2:00s 0 S Rule AS 2008 max - Apr Sun>=1 2:00s 0 S Rule AS 2008 max - Oct Sun>=1 2:00s 1:00 D # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Australia/Adelaide 9:14:20 - LMT 1895 Feb 9:00 - ACST 1899 May 9:30 Aus AC%sT 1971 9:30 AS AC%sT # Tasmania # # From Paul Eggert (2005-08-16): # http://www.bom.gov.au/climate/averages/tables/dst_times.shtml # says King Island didn't observe DST from WWII until late 1971. # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule AT 1916 only - Oct Sun>=1 2:00s 1:00 D Rule AT 1917 only - Mar lastSun 2:00s 0 S Rule AT 1917 1918 - Oct Sun>=22 2:00s 1:00 D Rule AT 1918 1919 - Mar Sun>=1 2:00s 0 S Rule AT 1967 only - Oct Sun>=1 2:00s 1:00 D Rule AT 1968 only - Mar Sun>=29 2:00s 0 S Rule AT 1968 1985 - Oct lastSun 2:00s 1:00 D Rule AT 1969 1971 - Mar Sun>=8 2:00s 0 S Rule AT 1972 only - Feb lastSun 2:00s 0 S Rule AT 1973 1981 - Mar Sun>=1 2:00s 0 S Rule AT 1982 1983 - Mar lastSun 2:00s 0 S Rule AT 1984 1986 - Mar Sun>=1 2:00s 0 S Rule AT 1986 only - Oct Sun>=15 2:00s 1:00 D Rule AT 1987 1990 - Mar Sun>=15 2:00s 0 S Rule AT 1987 only - Oct Sun>=22 2:00s 1:00 D Rule AT 1988 1990 - Oct lastSun 2:00s 1:00 D Rule AT 1991 1999 - Oct Sun>=1 2:00s 1:00 D Rule AT 1991 2005 - Mar lastSun 2:00s 0 S Rule AT 2000 only - Aug lastSun 2:00s 1:00 D Rule AT 2001 max - Oct Sun>=1 2:00s 1:00 D Rule AT 2006 only - Apr Sun>=1 2:00s 0 S Rule AT 2007 only - Mar lastSun 2:00s 0 S Rule AT 2008 max - Apr Sun>=1 2:00s 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Australia/Hobart 9:49:16 - LMT 1895 Sep 10:00 AT AE%sT 1919 Oct 24 10:00 Aus AE%sT 1967 10:00 AT AE%sT # Victoria # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule AV 1971 1985 - Oct lastSun 2:00s 1:00 D Rule AV 1972 only - Feb lastSun 2:00s 0 S Rule AV 1973 1985 - Mar Sun>=1 2:00s 0 S Rule AV 1986 1990 - Mar Sun>=15 2:00s 0 S Rule AV 1986 1987 - Oct Sun>=15 2:00s 1:00 D Rule AV 1988 1999 - Oct lastSun 2:00s 1:00 D Rule AV 1991 1994 - Mar Sun>=1 2:00s 0 S Rule AV 1995 2005 - Mar lastSun 2:00s 0 S Rule AV 2000 only - Aug lastSun 2:00s 1:00 D Rule AV 2001 2007 - Oct lastSun 2:00s 1:00 D Rule AV 2006 only - Apr Sun>=1 2:00s 0 S Rule AV 2007 only - Mar lastSun 2:00s 0 S Rule AV 2008 max - Apr Sun>=1 2:00s 0 S Rule AV 2008 max - Oct Sun>=1 2:00s 1:00 D # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Australia/Melbourne 9:39:52 - LMT 1895 Feb 10:00 Aus AE%sT 1971 10:00 AV AE%sT # New South Wales # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule AN 1971 1985 - Oct lastSun 2:00s 1:00 D Rule AN 1972 only - Feb 27 2:00s 0 S Rule AN 1973 1981 - Mar Sun>=1 2:00s 0 S Rule AN 1982 only - Apr Sun>=1 2:00s 0 S Rule AN 1983 1985 - Mar Sun>=1 2:00s 0 S Rule AN 1986 1989 - Mar Sun>=15 2:00s 0 S Rule AN 1986 only - Oct 19 2:00s 1:00 D Rule AN 1987 1999 - Oct lastSun 2:00s 1:00 D Rule AN 1990 1995 - Mar Sun>=1 2:00s 0 S Rule AN 1996 2005 - Mar lastSun 2:00s 0 S Rule AN 2000 only - Aug lastSun 2:00s 1:00 D Rule AN 2001 2007 - Oct lastSun 2:00s 1:00 D Rule AN 2006 only - Apr Sun>=1 2:00s 0 S Rule AN 2007 only - Mar lastSun 2:00s 0 S Rule AN 2008 max - Apr Sun>=1 2:00s 0 S Rule AN 2008 max - Oct Sun>=1 2:00s 1:00 D # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Australia/Sydney 10:04:52 - LMT 1895 Feb 10:00 Aus AE%sT 1971 10:00 AN AE%sT Zone Australia/Broken_Hill 9:25:48 - LMT 1895 Feb 10:00 - AEST 1896 Aug 23 9:00 - ACST 1899 May 9:30 Aus AC%sT 1971 9:30 AN AC%sT 2000 9:30 AS AC%sT # Lord Howe Island # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule LH 1981 1984 - Oct lastSun 2:00 1:00 - Rule LH 1982 1985 - Mar Sun>=1 2:00 0 - Rule LH 1985 only - Oct lastSun 2:00 0:30 - Rule LH 1986 1989 - Mar Sun>=15 2:00 0 - Rule LH 1986 only - Oct 19 2:00 0:30 - Rule LH 1987 1999 - Oct lastSun 2:00 0:30 - Rule LH 1990 1995 - Mar Sun>=1 2:00 0 - Rule LH 1996 2005 - Mar lastSun 2:00 0 - Rule LH 2000 only - Aug lastSun 2:00 0:30 - Rule LH 2001 2007 - Oct lastSun 2:00 0:30 - Rule LH 2006 only - Apr Sun>=1 2:00 0 - Rule LH 2007 only - Mar lastSun 2:00 0 - Rule LH 2008 max - Apr Sun>=1 2:00 0 - Rule LH 2008 max - Oct Sun>=1 2:00 0:30 - Zone Australia/Lord_Howe 10:36:20 - LMT 1895 Feb 10:00 - AEST 1981 Mar 10:30 LH %z 1985 Jul 10:30 LH %z # Australian miscellany # # Ashmore Is, Cartier # no indigenous inhabitants; only seasonal caretakers # no times are set # # Coral Sea Is # no indigenous inhabitants; only meteorologists # no times are set # # Macquarie # Permanent occupation (scientific station) 1911-1915 and since 25 March 1948; # sealing and penguin oil station operated Nov 1899 to Apr 1919. See the # Tasmania Parks & Wildlife Service history of sealing at Macquarie Island # http://www.parks.tas.gov.au/index.aspx?base=1828 # http://www.parks.tas.gov.au/index.aspx?base=1831 # Guess that it was like Australia/Hobart while inhabited before 2010. # # From Steffen Thorsen (2010-03-10): # We got these changes from the Australian Antarctic Division: # - Macquarie Island will stay on UTC+11 for winter and therefore not # switch back from daylight savings time when other parts of Australia do # on 4 April. # # From Arthur David Olson (2013-05-23): # The 1919 transition is overspecified below so pre-2013 zics # will produce a binary file with an [A]EST-type as the first 32-bit type; # this is required for correct handling of times before 1916 by # pre-2013 versions of localtime. Zone Antarctica/Macquarie 0 - -00 1899 Nov 10:00 - AEST 1916 Oct 1 2:00 10:00 1:00 AEDT 1917 Feb 10:00 Aus AE%sT 1919 Apr 1 0:00s 0 - -00 1948 Mar 25 10:00 Aus AE%sT 1967 10:00 AT AE%sT 2010 10:00 1:00 AEDT 2011 10:00 AT AE%sT # Fiji # Milne gives 11:55:44 for Suva. # From Alexander Krivenyshev (2009-11-10): # According to Fiji Broadcasting Corporation, Fiji plans to re-introduce DST # from November 29th 2009 to April 25th 2010. # # "Daylight savings to commence this month" # http://www.radiofiji.com.fj/fullstory.php?id=23719 # http://www.worldtimezone.com/dst_news/dst_news_fiji01.html # From Steffen Thorsen (2009-11-10): # The Fiji Government has posted some more details about the approved # amendments: # http://www.fiji.gov.fj/publish/page_16198.shtml # From Steffen Thorsen (2010-03-03): # The Cabinet in Fiji has decided to end DST about a month early, on # 2010-03-28 at 03:00. # The plan is to observe DST again, from 2010-10-24 to sometime in March # 2011 (last Sunday a good guess?). # # Official source: # http://www.fiji.gov.fj/index.php?option=com_content&view=article&id=1096:3310-cabinet-approves-change-in-daylight-savings-dates&catid=49:cabinet-releases&Itemid=166 # # A bit more background info here: # https://www.timeanddate.com/news/time/fiji-dst-ends-march-2010.html # From Alexander Krivenyshev (2010-10-24): # According to Radio Fiji and Fiji Times online, Fiji will end DST 3 # weeks earlier than expected - on March 6, 2011, not March 27, 2011... # Here is confirmation from Government of the Republic of the Fiji Islands, # Ministry of Information (fiji.gov.fj) web site: # http://www.fiji.gov.fj/index.php?option=com_content&view=article&id=2608:daylight-savings&catid=71:press-releases&Itemid=155 # http://www.worldtimezone.com/dst_news/dst_news_fiji04.html # From Steffen Thorsen (2011-10-03): # Now the dates have been confirmed, and at least our start date # assumption was correct (end date was one week wrong). # # http://www.fiji.gov.fj/index.php?option=com_content&view=article&id=4966:daylight-saving-starts-in-fiji&catid=71:press-releases&Itemid=155 # which says # Members of the public are reminded to change their time to one hour in # advance at 2am to 3am on October 23, 2011 and one hour back at 3am to # 2am on February 26 next year. # From Ken Rylander (2011-10-24) # Another change to the Fiji DST end date. In the TZ database the end date for # Fiji DST 2012, is currently Feb 26. This has been changed to Jan 22. # # http://www.fiji.gov.fj/index.php?option=com_content&view=article&id=5017:amendments-to-daylight-savings&catid=71:press-releases&Itemid=155 # states: # # The end of daylight saving scheduled initially for the 26th of February 2012 # has been brought forward to the 22nd of January 2012. # The commencement of daylight saving will remain unchanged and start # on the 23rd of October, 2011. # From the Fiji Government Online Portal (2012-08-21) via Steffen Thorsen: # The Minister for Labour, Industrial Relations and Employment Mr Jone Usamate # today confirmed that Fiji will start daylight savings at 2 am on Sunday 21st # October 2012 and end at 3 am on Sunday 20th January 2013. # http://www.fiji.gov.fj/index.php?option=com_content&view=article&id=6702&catid=71&Itemid=155 # From the Fijian Government Media Center (2013-08-30) via David Wheeler: # Fiji will start daylight savings on Sunday 27th October, 2013 ... # move clocks forward by one hour from 2am # http://www.fiji.gov.fj/Media-Center/Press-Releases/DAYLIGHT-SAVING-STARTS-ON-SUNDAY,-27th-OCTOBER-201.aspx # From Steffen Thorsen (2013-01-10): # Fiji will end DST on 2014-01-19 02:00: # http://www.fiji.gov.fj/Media-Center/Press-Releases/DAYLIGHT-SAVINGS-TO-END-THIS-MONTH-(1).aspx # From Ken Rylander (2014-10-20): # DST will start Nov. 2 this year. # http://www.fiji.gov.fj/Media-Center/Press-Releases/DAYLIGHT-SAVING-STARTS-ON-SUNDAY,-NOVEMBER-2ND.aspx # From a government order dated 2015-08-26 and published as Legal Notice No. 77 # in the Government of Fiji Gazette Supplement No. 24 (2015-08-28), # via Ken Rylander (2015-09-02): # the daylight saving period is 1 hour in advance of the standard time # commencing at 2.00 am on Sunday 1st November, 2015 and ending at # 3.00 am on Sunday 17th January, 2016. # From Raymond Kumar (2016-10-04): # http://www.fiji.gov.fj/Media-Center/Press-Releases/DAYLIGHT-SAVING-STARTS-ON-6th-NOVEMBER,-2016.aspx # "Fiji's daylight savings will begin on Sunday, 6 November 2016, when # clocks go forward an hour at 2am to 3am.... Daylight Saving will # end at 3.00am on Sunday 15th January 2017." # From Paul Eggert (2017-08-21): # Dominic Fok writes (2017-08-20) that DST ends 2018-01-14, citing # Extraordinary Government of Fiji Gazette Supplement No. 21 (2017-08-27), # [Legal Notice No. 41] of an order of the previous day by J Usamate. # From Raymond Kumar (2018-07-13): # http://www.fijitimes.com/government-approves-2018-daylight-saving/ # ... The daylight saving period will end at 3am on Sunday January 13, 2019. # From Paul Eggert (2019-08-06): # Today Raymond Kumar reported the Government of Fiji Gazette Supplement No. 27 # (2019-08-02) said that Fiji observes DST "commencing at 2.00 am on # Sunday, 10 November 2019 and ending at 3.00 am on Sunday, 12 January 2020." # For now, guess DST from 02:00 the second Sunday in November to 03:00 # the first Sunday on or after January 12. January transitions reportedly # depend on when school terms start. Although the guess is ad hoc, it matches # transitions planned this year and seems more likely to match future practice # than guessing no DST. # From Michael Deckers (2019-08-06): # https://www.laws.gov.fj/LawsAsMade/downloadfile/848 # From Raymond Kumar (2020-10-08): # [DST in Fiji] is from December 20th 2020, till 17th January 2021. # From Alan Mintz (2020-10-08): # https://www.laws.gov.fj/LawsAsMade/GetFile/1071 # From Tim Parenti (2020-10-08): # https://www.fijivillage.com/news/Daylight-saving-from-Dec-20th-this-year-to-Jan-17th-2021-8rf4x5/ # "Minister for Employment, Parveen Bala says they had never thought of # stopping daylight saving. He says it was just to decide on when it should # start and end. Bala says it is a short period..." # # From Tim Parenti (2021-10-11), per Jashneel Kumar (2021-10-11) and P Chan # (2021-10-12): # https://www.fiji.gov.fj/Media-Centre/Speeches/English/PM-BAINIMARAMA-S-COVID-19-ANNOUNCEMENT-10-10-21 # https://www.fbcnews.com.fj/news/covid-19/curfew-moved-back-to-11pm/ # In a 2021-10-10 speech concerning updated Covid-19 mitigation measures in # Fiji, prime minister Josaia Voreqe "Frank" Bainimarama announced the # suspension of DST for the 2021/2022 season: "Given that we are in the process # of readjusting in the midst of so many changes, we will also put Daylight # Savings Time on hold for this year. It will also make the reopening of # scheduled commercial air service much smoother if we don't have to be # concerned shifting arrival and departure times, which may look like a simple # thing but requires some significant logistical adjustments domestically and # internationally." # From Shalvin Narayan (2022-10-27): # Please note that there will not be any daylight savings time change # in Fiji for 2022-2023.... # https://www.facebook.com/FijianGovernment/posts/pfbid0mmWVTYmTibn66ybpFda75pDcf34SSpoSaskJW5gXwaKo5Sgc7273Q4fXWc6kQV6Hl # From Almaz Mingaleev (2023-10-06): # Cabinet approved the suspension of Daylight Saving and appropriate # legislative changes will be considered including the repeal of the # Daylight Saving Act 1998 # https://www.fiji.gov.fj/Media-Centre/Speeches/English/CABINET-DECISIONS-3-OCTOBER-2023 # # From Paul Eggert (2023-10-06): # For now, assume DST is suspended indefinitely. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Fiji 1998 1999 - Nov Sun>=1 2:00 1:00 - Rule Fiji 1999 2000 - Feb lastSun 3:00 0 - Rule Fiji 2009 only - Nov 29 2:00 1:00 - Rule Fiji 2010 only - Mar lastSun 3:00 0 - Rule Fiji 2010 2013 - Oct Sun>=21 2:00 1:00 - Rule Fiji 2011 only - Mar Sun>=1 3:00 0 - Rule Fiji 2012 2013 - Jan Sun>=18 3:00 0 - Rule Fiji 2014 only - Jan Sun>=18 2:00 0 - Rule Fiji 2014 2018 - Nov Sun>=1 2:00 1:00 - Rule Fiji 2015 2021 - Jan Sun>=12 3:00 0 - Rule Fiji 2019 only - Nov Sun>=8 2:00 1:00 - Rule Fiji 2020 only - Dec 20 2:00 1:00 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Fiji 11:55:44 - LMT 1915 Oct 26 # Suva 12:00 Fiji %z # French Polynesia # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Gambier -8:59:48 - LMT 1912 Oct 1 # Rikitea -9:00 - %z Zone Pacific/Marquesas -9:18:00 - LMT 1912 Oct 1 -9:30 - %z Zone Pacific/Tahiti -9:58:16 - LMT 1912 Oct 1 # Papeete -10:00 - %z # Clipperton (near North America) is administered from French Polynesia; # it is uninhabited. # Guam # N Mariana Is # Rule NAME FROM TO - IN ON AT SAVE LETTER/S # http://guamlegislature.com/Public_Laws_5th/PL05-025.pdf # http://documents.guam.gov/wp-content/uploads/E.O.-59-7-Guam-Daylight-Savings-Time-May-6-1959.pdf Rule Guam 1959 only - Jun 27 2:00 1:00 D # http://documents.guam.gov/wp-content/uploads/E.O.-61-5-Revocation-of-Daylight-Saving-Time-and-Restoratio.pdf Rule Guam 1961 only - Jan 29 2:00 0 S # http://documents.guam.gov/wp-content/uploads/E.O.-67-13-Guam-Daylight-Savings-Time.pdf Rule Guam 1967 only - Sep 1 2:00 1:00 D # http://documents.guam.gov/wp-content/uploads/E.O.-69-2-Repeal-of-Guam-Daylight-Saving-Time.pdf Rule Guam 1969 only - Jan 26 0:01 0 S # http://documents.guam.gov/wp-content/uploads/E.O.-69-10-Guam-Daylight-Saving-Time.pdf Rule Guam 1969 only - Jun 22 2:00 1:00 D Rule Guam 1969 only - Aug 31 2:00 0 S # http://documents.guam.gov/wp-content/uploads/E.O.-70-10-Guam-Daylight-Saving-Time.pdf # http://documents.guam.gov/wp-content/uploads/E.O.-70-30-End-of-Guam-Daylight-Saving-Time.pdf # http://documents.guam.gov/wp-content/uploads/E.O.-71-5-Guam-Daylight-Savings-Time.pdf Rule Guam 1970 1971 - Apr lastSun 2:00 1:00 D Rule Guam 1970 1971 - Sep Sun>=1 2:00 0 S # http://documents.guam.gov/wp-content/uploads/E.O.-73-28.-Guam-Day-light-Saving-Time.pdf Rule Guam 1973 only - Dec 16 2:00 1:00 D # http://documents.guam.gov/wp-content/uploads/E.O.-74-7-Guam-Daylight-Savings-Time-Rescinded.pdf Rule Guam 1974 only - Feb 24 2:00 0 S # http://documents.guam.gov/wp-content/uploads/E.O.-76-13-Daylight-Savings-Time.pdf Rule Guam 1976 only - May 26 2:00 1:00 D # http://documents.guam.gov/wp-content/uploads/E.O.-76-25-Revocation-of-E.O.-76-13.pdf Rule Guam 1976 only - Aug 22 2:01 0 S # http://documents.guam.gov/wp-content/uploads/E.O.-77-4-Daylight-Savings-Time.pdf Rule Guam 1977 only - Apr 24 2:00 1:00 D # http://documents.guam.gov/wp-content/uploads/E.O.-77-18-Guam-Standard-Time.pdf Rule Guam 1977 only - Aug 28 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Guam -14:21:00 - LMT 1844 Dec 31 9:39:00 - LMT 1901 # Agana 10:00 - GST 1941 Dec 10 # Guam 9:00 - %z 1944 Jul 31 10:00 Guam G%sT 2000 Dec 23 10:00 - ChST # Chamorro Standard Time # Kiribati (Gilbert Is) # Marshall Is # Tuvalu # Wake # Wallis & Futuna # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Tarawa 11:32:04 - LMT 1901 # Bairiki 12:00 - %z # Kiribati (except Gilbert Is) # See Pacific/Tarawa for the Gilbert Is. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Kanton 0 - -00 1937 Aug 31 -12:00 - %z 1979 Oct -11:00 - %z 1994 Dec 31 13:00 - %z Zone Pacific/Kiritimati -10:29:20 - LMT 1901 -10:40 - %z 1979 Oct -10:00 - %z 1994 Dec 31 14:00 - %z # Marshall Is # See Pacific/Tarawa for most locations. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Kwajalein 11:09:20 - LMT 1901 11:00 - %z 1937 10:00 - %z 1941 Apr 1 9:00 - %z 1944 Feb 6 11:00 - %z 1969 Oct -12:00 - %z 1993 Aug 20 24:00 12:00 - %z # Micronesia # For Chuuk and Yap see Pacific/Port_Moresby. # For Pohnpei see Pacific/Guadalcanal. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Kosrae -13:08:04 - LMT 1844 Dec 31 10:51:56 - LMT 1901 11:00 - %z 1914 Oct 9:00 - %z 1919 Feb 1 11:00 - %z 1937 10:00 - %z 1941 Apr 1 9:00 - %z 1945 Aug 11:00 - %z 1969 Oct 12:00 - %z 1999 11:00 - %z # Nauru # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Nauru 11:07:40 - LMT 1921 Jan 15 # Uaobe 11:30 - %z 1942 Aug 29 9:00 - %z 1945 Sep 8 11:30 - %z 1979 Feb 10 2:00 12:00 - %z # New Caledonia # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule NC 1977 1978 - Dec Sun>=1 0:00 1:00 - Rule NC 1978 1979 - Feb 27 0:00 0 - Rule NC 1996 only - Dec 1 2:00s 1:00 - # Shanks & Pottenger say the following was at 2:00; go with IATA. Rule NC 1997 only - Mar 2 2:00s 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Noumea 11:05:48 - LMT 1912 Jan 13 # Nouméa 11:00 NC %z ############################################################################### # New Zealand # McMurdo Station and Scott Base in Antarctica use Auckland time. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule NZ 1927 only - Nov 6 2:00 1:00 S Rule NZ 1928 only - Mar 4 2:00 0 M Rule NZ 1928 1933 - Oct Sun>=8 2:00 0:30 S Rule NZ 1929 1933 - Mar Sun>=15 2:00 0 M Rule NZ 1934 1940 - Apr lastSun 2:00 0 M Rule NZ 1934 1940 - Sep lastSun 2:00 0:30 S Rule NZ 1946 only - Jan 1 0:00 0 S # Since 1957 Chatham has been 45 minutes ahead of NZ, but until 2018a # there was no documented single notation for the date and time of this # transition. Duplicate the Rule lines for now, to give the 2018a change # time to percolate out. Rule NZ 1974 only - Nov Sun>=1 2:00s 1:00 D Rule Chatham 1974 only - Nov Sun>=1 2:45s 1:00 - Rule NZ 1975 only - Feb lastSun 2:00s 0 S Rule Chatham 1975 only - Feb lastSun 2:45s 0 - Rule NZ 1975 1988 - Oct lastSun 2:00s 1:00 D Rule Chatham 1975 1988 - Oct lastSun 2:45s 1:00 - Rule NZ 1976 1989 - Mar Sun>=1 2:00s 0 S Rule Chatham 1976 1989 - Mar Sun>=1 2:45s 0 - Rule NZ 1989 only - Oct Sun>=8 2:00s 1:00 D Rule Chatham 1989 only - Oct Sun>=8 2:45s 1:00 - Rule NZ 1990 2006 - Oct Sun>=1 2:00s 1:00 D Rule Chatham 1990 2006 - Oct Sun>=1 2:45s 1:00 - Rule NZ 1990 2007 - Mar Sun>=15 2:00s 0 S Rule Chatham 1990 2007 - Mar Sun>=15 2:45s 0 - Rule NZ 2007 max - Sep lastSun 2:00s 1:00 D Rule Chatham 2007 max - Sep lastSun 2:45s 1:00 - Rule NZ 2008 max - Apr Sun>=1 2:00s 0 S Rule Chatham 2008 max - Apr Sun>=1 2:45s 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Auckland 11:39:04 - LMT 1868 Nov 2 11:30 NZ NZ%sT 1946 Jan 1 12:00 NZ NZ%sT Zone Pacific/Chatham 12:13:48 - LMT 1868 Nov 2 12:15 - %z 1946 Jan 1 12:45 Chatham %z # Auckland Is # uninhabited; M�ori and Moriori, colonial settlers, pastoralists, sealers, # and scientific personnel have wintered # Campbell I # minor whaling stations operated 1909/1914 # scientific station operated 1941/1995; # previously whalers, sealers, pastoralists, and scientific personnel wintered # was probably like Pacific/Auckland # Cook Is # # From Alexander Krivenyshev (2021-03-24): # In 1899 the Cook Islands celebrated Christmas twice to correct the calendar. # According to the old books, missionaries were unaware of # the International Date line, when they came from Sydney. # Thus the Cook Islands were one day ahead.... # http://nzetc.victoria.ac.nz/tm/scholarly/tei-KloDisc-t1-body-d18.html # ... Appendix to the Journals of the House of Representatives, 1900 # https://atojs.natlib.govt.nz/cgi-bin/atojs?a=d&d=AJHR1900-I.2.1.2.3 # (page 20) # # From Michael Deckers (2021-03-24): # ... in the Cook Island Act of 1915-10-11, online at # http://www.paclii.org/ck/legis/ck-nz_act/cia1915132/ # "651. The hour of the day shall in each of the islands included in the # Cook Islands be determined in accordance with the meridian of that island." # so that local (mean?) time was still used in Rarotonga (and Niue) in 1915. # This was changed in the Cook Island Amendment Act of 1952-10-16 ... # http://www.paclii.org/ck/legis/ck-nz_act/ciaa1952212/ # "651 (1) The hour of the day in each of the islands included in the Cook # Islands, other than Niue, shall be determined as if each island were # situated on the meridian one hundred and fifty-seven degrees thirty minutes # West of Greenwich. (2) The hour of the day in the Island of Niue shall be # determined as if that island were situated on the meridian one hundred and # seventy degrees West of Greenwich." # This act does not state when it takes effect, so one has to assume it # applies since 1952-10-16. But there is the possibility that the act just # legalized prior existing practice, as we had seen with the Guernsey law of # 1913-06-18 for the switch in 1909-04-19. # # From Paul Eggert (2021-03-24): # Transitions after 1952 are from Shanks & Pottenger. # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Cook 1978 only - Nov 12 0:00 0:30 - Rule Cook 1979 1991 - Mar Sun>=1 0:00 0 - Rule Cook 1979 1990 - Oct lastSun 0:00 0:30 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Rarotonga 13:20:56 - LMT 1899 Dec 26 # Avarua -10:39:04 - LMT 1952 Oct 16 -10:30 - %z 1978 Nov 12 -10:00 Cook %z ############################################################################### # Niue # See Pacific/Rarotonga comments for 1952 transition. # # From Tim Parenti (2021-09-13): # Consecutive contemporaneous editions of The Air Almanac listed -11:20 for # Niue as of Apr 1964 but -11 as of Aug 1964: # Apr 1964: https://books.google.com/books?id=_1So677Y5vUC&pg=SL1-PA23 # Aug 1964: https://books.google.com/books?id=MbJloqd-zyUC&pg=SL1-PA23 # Without greater specificity, guess 1964-07-01 for this transition. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Niue -11:19:40 - LMT 1952 Oct 16 # Alofi -11:20 - %z 1964 Jul -11:00 - %z # Norfolk # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Norfolk 11:11:52 - LMT 1901 # Kingston 11:12 - %z 1951 11:30 - %z 1974 Oct 27 02:00s 11:30 1:00 %z 1975 Mar 2 02:00s 11:30 - %z 2015 Oct 4 02:00s 11:00 - %z 2019 Jul 11:00 AN %z # Palau (Belau) # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Palau -15:02:04 - LMT 1844 Dec 31 # Koror 8:57:56 - LMT 1901 9:00 - %z # Papua New Guinea # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Port_Moresby 9:48:40 - LMT 1880 9:48:32 - PMMT 1895 # Port Moresby Mean Time 10:00 - %z # # From Paul Eggert (2014-10-13): # Base the Bougainville entry on the Arawa-Kieta region, which appears to have # the most people even though it was devastated in the Bougainville Civil War. # # Although Shanks gives 1942-03-15 / 1943-11-01 for UT +09, these dates # are apparently rough guesswork from the starts of military campaigns. # The World War II entries below are instead based on Arawa-Kieta. # The Japanese occupied Kieta in July 1942, # according to the Pacific War Online Encyclopedia # https://pwencycl.kgbudge.com/B/o/Bougainville.htm # and seem to have controlled it until their 1945-08-21 surrender. # # The Autonomous Region of Bougainville switched from UT +10 to +11 # on 2014-12-28 at 02:00. They call +11 "Bougainville Standard Time". # See: # http://www.bougainville24.com/bougainville-issues/bougainville-gets-own-timezone/ # Zone Pacific/Bougainville 10:22:16 - LMT 1880 9:48:32 - PMMT 1895 10:00 - %z 1942 Jul 9:00 - %z 1945 Aug 21 10:00 - %z 2014 Dec 28 2:00 11:00 - %z # Pitcairn # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Pitcairn -8:40:20 - LMT 1901 # Adamstown -8:30 - %z 1998 Apr 27 0:00 -8:00 - %z # American Samoa # Midway Zone Pacific/Pago_Pago 12:37:12 - LMT 1892 Jul 5 -11:22:48 - LMT 1911 -11:00 - SST # S=Samoa # Samoa (formerly and also known as Western Samoa) # From Steffen Thorsen (2009-10-16): # We have been in contact with the government of Samoa again, and received # the following info: # # "Cabinet has now approved Daylight Saving to be effected next year # commencing from the last Sunday of September 2010 and conclude first # Sunday of April 2011." # # Background info: # https://www.timeanddate.com/news/time/samoa-dst-plan-2009.html # # Samoa's Daylight Saving Time Act 2009 is available here, but does not # contain any dates: # http://www.parliament.gov.ws/documents/acts/Daylight%20Saving%20Act%20%202009%20(English)%20-%20Final%207-7-091.pdf # From Laupue Raymond Hughes (2010-10-07): # Please see # http://www.mcil.gov.ws # the Ministry of Commerce, Industry and Labour (sideframe) "Last Sunday # September 2010 (26/09/10) - adjust clocks forward from 12:00 midnight # to 01:00am and First Sunday April 2011 (03/04/11) - adjust clocks # backwards from 1:00am to 12:00am" # From Laupue Raymond Hughes (2011-03-07): # [http://www.mcil.gov.ws/ftcd/daylight_saving_2011.pdf] # # ... when the standard time strikes the hour of four o'clock (4.00am # or 0400 Hours) on the 2nd April 2011, then all instruments used to # measure standard time are to be adjusted/changed to three o'clock # (3:00am or 0300Hrs). # From David Zülke (2011-05-09): # Subject: Samoa to move timezone from east to west of international date line # # http://www.morningstar.co.uk/uk/markets/newsfeeditem.aspx?id=138501958347963 # From Paul Eggert (2014-06-27): # The International Date Line Act 2011 # http://www.parliament.gov.ws/images/ACTS/International_Date_Line_Act__2011_-_Eng.pdf # changed Samoa from UT -11 to +13, effective "12 o'clock midnight, on # Thursday 29th December 2011". The International Date Line was adjusted # accordingly. # From Laupue Raymond Hughes (2011-09-02): # http://www.mcil.gov.ws/mcil_publications.html # # here is the official website publication for Samoa DST and dateline change # # DST # Year End Time Start Time # 2011 - - - - - - 24 September 3:00am to 4:00am # 2012 01 April 4:00am to 3:00am - - - - - - # # Dateline Change skip Friday 30th Dec 2011 # Thursday 29th December 2011 23:59:59 Hours # Saturday 31st December 2011 00:00:00 Hours # # From Nicholas Pereira (2012-09-10): # Daylight Saving Time commences on Sunday 30th September 2012 and # ends on Sunday 7th of April 2013.... # http://www.mcil.gov.ws/mcil_publications.html # # From Paul Eggert (2014-07-08): # That web page currently lists transitions for 2012/3 and 2013/4. # Assume the pattern instituted in 2012 will continue indefinitely. # # From Geoffrey D. Bennett (2021-09-20): # https://www.mcil.gov.ws/storage/2021/09/MCIL-Scan_20210920_120553.pdf # DST has been cancelled for this year. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule WS 2010 only - Sep lastSun 0:00 1 - Rule WS 2011 only - Apr Sat>=1 4:00 0 - Rule WS 2011 only - Sep lastSat 3:00 1 - Rule WS 2012 2021 - Apr Sun>=1 4:00 0 - Rule WS 2012 2020 - Sep lastSun 3:00 1 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Apia 12:33:04 - LMT 1892 Jul 5 -11:26:56 - LMT 1911 -11:30 - %z 1950 -11:00 WS %z 2011 Dec 29 24:00 13:00 WS %z # Solomon Is # excludes Bougainville, for which see Papua New Guinea # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Guadalcanal 10:39:48 - LMT 1912 Oct 1 # Honiara 11:00 - %z # Tokelau # # From Gwillim Law (2011-12-29) # A correspondent informed me that Tokelau, like Samoa, will be skipping # December 31 this year ... # # From Steffen Thorsen (2012-07-25) # ... we double checked by calling hotels and offices based in Tokelau asking # about the time there, and they all told a time that agrees with UTC+13.... # Shanks says UT-10 from 1901 [but] ... there is a good chance the change # actually was to UT-11 back then. # # From Paul Eggert (2012-07-25) # A Google Books snippet of Appendix to the Journals of the House of # Representatives of New Zealand, Session 1948, # , page 65, says Tokelau # was "11 hours slow on G.M.T." Go with Thorsen and assume Shanks & Pottenger # are off by an hour starting in 1901. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Fakaofo -11:24:56 - LMT 1901 -11:00 - %z 2011 Dec 30 13:00 - %z # Tonga # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Tonga 1999 only - Oct 7 2:00s 1:00 - Rule Tonga 2000 only - Mar 19 2:00s 0 - Rule Tonga 2000 2001 - Nov Sun>=1 2:00 1:00 - Rule Tonga 2001 2002 - Jan lastSun 2:00 0 - Rule Tonga 2016 only - Nov Sun>=1 2:00 1:00 - Rule Tonga 2017 only - Jan Sun>=15 3:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Tongatapu 12:19:12 - LMT 1945 Sep 10 12:20 - %z 1961 13:00 - %z 1999 13:00 Tonga %z # US minor outlying islands # Howland, Baker # Howland was mined for guano by American companies 1857-1878 and British # 1886-1891; Baker was similar but exact dates are not known. # Inhabited by civilians 1935-1942; U.S. military bases 1943-1944; # uninhabited thereafter. # Howland observed Hawaii Standard Time (UT -10:30) in 1937; # see page 206 of Elgen M. Long and Marie K. Long, # Amelia Earhart: the Mystery Solved, Simon & Schuster (2000). # So most likely Howland and Baker observed Hawaii Time from 1935 # until they were abandoned after the war. # Jarvis # Mined for guano by American companies 1857-1879 and British 1883?-1891?. # Inhabited by civilians 1935-1942; IGY scientific base 1957-1958; # uninhabited thereafter. # no information; was probably like Pacific/Kiritimati # Johnston # # From Paul Eggert (2017-02-10): # Sometimes Johnston kept Hawaii time, and sometimes it was an hour behind. # Details are uncertain. We have no data for Johnston after 1970, so # treat it like Hawaii for now. Since Johnston is now uninhabited, # its link to Pacific/Honolulu is in the 'backward' file. # # In his memoirs of June 6th to October 4, 1945 # (2005), Herbert C. Bach writes, # "We started our letdown to Kwajalein Atoll and landed there at 5:00 AM # Johnston time, 1:30 AM Kwajalein time." This was in June 1945, and # confirms that Johnston kept the same time as Honolulu in summer 1945. # # From Lyle McElhaney (2014-03-11): # [W]hen JI was being used for that [atomic bomb] testing, the time being used # was not Hawaiian time but rather the same time being used on the ships, # which had a GMT offset of -11 hours. This apparently applied to at least the # time from Operation Newsreel (Hardtack I/Teak shot, 1958-08-01) to the last # Operation Fishbowl shot (Tightrope, 1962-11-04).... [See] Herman Hoerlin, # "The United States High-Altitude Test Experience: A Review Emphasizing the # Impact on the Environment", Los Alamos LA-6405, Oct 1976. # https://www.fas.org/sgp/othergov/doe/lanl/docs1/00322994.pdf # See the table on page 4 where he lists GMT and local times for the tests; a # footnote for the JI tests reads that local time is "JI time = Hawaii Time # Minus One Hour". # Kingman # uninhabited # Palmyra # uninhabited since World War II; was probably like Pacific/Kiritimati # Vanuatu # From P Chan (2020-11-27): # Joint Daylight Saving Regulation No 59 of 1973 # New Hebrides Condominium Gazette No 336. December 1973 # http://www.paclii.org/vu/other/VUNHGovGaz//1973/11.pdf#page=15 # # Joint Daylight Saving (Repeal) Regulation No 10 of 1974 # New Hebrides Condominium Gazette No 336. March 1974 # http://www.paclii.org/vu/other/VUNHGovGaz//1974/3.pdf#page=11 # # Summer Time Act No. 35 of 1982 [commenced 1983-09-01] # http://www.paclii.org/vu/other/VUGovGaz/1982/32.pdf#page=48 # # Summer Time Act (Cap 157) # Laws of the Republic of Vanuatu Revised Edition 1988 # http://www.paclii.org/cgi-bin/sinodisp/vu/legis/consol_act1988/sta147/sta147.html # # Summer Time (Amendment) Act No. 6 of 1991 [commenced 1991-11-11] # http://www.paclii.org/vu/legis/num_act/sta1991227/ # # Summer Time (Repeal) Act No. 4 of 1993 [commenced 1993-05-03] # http://www.paclii.org/vu/other/VUGovGaz/1993/15.pdf#page=59 # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Vanuatu 1973 only - Dec 22 12:00u 1:00 - Rule Vanuatu 1974 only - Mar 30 12:00u 0 - Rule Vanuatu 1983 1991 - Sep Sat>=22 24:00 1:00 - Rule Vanuatu 1984 1991 - Mar Sat>=22 24:00 0 - Rule Vanuatu 1992 1993 - Jan Sat>=22 24:00 0 - Rule Vanuatu 1992 only - Oct Sat>=22 24:00 1:00 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Efate 11:13:16 - LMT 1912 Jan 13 # Vila 11:00 Vanuatu %z ############################################################################### # NOTES # This file is by no means authoritative; if you think you know better, # go ahead and edit the file, and please send any changes to # the public mailing list tz@iana.org for general use in the future. # For more, please see the file CONTRIBUTING in the tz distribution. # From Paul Eggert (2018-11-18): # # Unless otherwise specified, the source for data through 1990 is: # Thomas G. Shanks and Rique Pottenger, The International Atlas (6th edition), # San Diego: ACS Publications, Inc. (2003). # Unfortunately this book contains many errors and cites no sources. # # Many years ago Gwillim Law wrote that a good source # for time zone data was the International Air Transport # Association's Standard Schedules Information Manual (IATA SSIM), # published semiannually. Law sent in several helpful summaries # of the IATA's data after 1990. Except where otherwise noted, # IATA SSIM is the source for entries after 1990. # # Another source occasionally used is Edward W. Whitman, World Time Differences, # Whitman Publishing Co, 2 Niagara Av, Ealing, London (undated), which # I found in the UCLA library. # # For data circa 1899, a common source is: # Milne J. Civil time. Geogr J. 1899 Feb;13(2):173-94. # https://www.jstor.org/stable/1774359 # # For the 1911/1912 establishment of standard time in French possessions, see: # Société Française de Physique, Recueil de constantes physiques (1913), # page 752, 18b. # # A reliable and entertaining source about time zones is # Derek Howse, Greenwich time and longitude, Philip Wilson Publishers (1997). # # I invented the abbreviation marked "*". # The following abbreviations are from other sources. # Corrections are welcome! # std dst # LMT Local Mean Time # 8:00 AWST AWDT Western Australia # 9:30 ACST ACDT Central Australia # 10:00 AEST AEDT Eastern Australia # 10:00 GST GDT* Guam through 2000 # 10:00 ChST Chamorro # 11:30 NZMT NZST New Zealand through 1945 # 12:00 NZST NZDT New Zealand 1946-present # -11:00 SST Samoa # -10:00 HST Hawaii # # See the 'northamerica' file for Hawaii. # See the 'southamerica' file for Easter I and the Galápagos Is. ############################################################################### # Australia # From Paul Eggert (2014-06-30): # Daylight saving time has long been controversial in Australia, pitting # region against region, rural against urban, and local against global. # For example, in her review of Graeme Davison's _The Unforgiving # Minute: how Australians learned to tell the time_ (1993), Perth native # Phillipa J Martyr wrote, "The section entitled 'Saving Daylight' was # very informative, but was (as can, sadly, only be expected from a # Melbourne-based study) replete with the usual chuckleheaded # Queenslanders and straw-chewing yokels from the West prattling fables # about fading curtains and crazed farm animals." # Electronic Journal of Australian and New Zealand History (1997-03-03) # http://www.jcu.edu.au/aff/history/reviews/davison.htm # From P Chan (2020-11-20): # Daylight Saving Act 1916 (No. 40 of 1916) [1916-12-21, commenced 1917-01-01] # http://classic.austlii.edu.au/au/legis/cth/num_act/dsa1916401916192/ # # Daylight Saving Repeal Act 1917 (No. 35 of 1917) [1917-09-25] # http://classic.austlii.edu.au/au/legis/cth/num_act/dsra1917351917243/ # # Statutory Rules 1941, No. 323 [1941-12-24] # https://www.legislation.gov.au/Details/C1941L00323 # # Statutory Rules 1942, No. 392 [1942-09-10] # https://www.legislation.gov.au/Details/C1942L00392 # # Statutory Rules 1943, No. 241 [1943-09-29] # https://www.legislation.gov.au/Details/C1943L00241 # # All transition times should be 02:00 standard time. # From Paul Eggert (2005-12-08): # Implementation Dates of Daylight Saving Time within Australia # http://www.bom.gov.au/climate/averages/tables/dst_times.shtml # summarizes daylight saving issues in Australia. # From Arthur David Olson (2005-12-12): # Lawlink NSW:Daylight Saving in New South Wales # http://www.lawlink.nsw.gov.au/lawlink/Corporate/ll_agdinfo.nsf/pages/community_relations_daylight_saving # covers New South Wales in particular. # From John Mackin (1991-03-06): # We in Australia have _never_ referred to DST as 'daylight' time. # It is called 'summer' time. Now by a happy coincidence, 'summer' # and 'standard' happen to start with the same letter; hence, the # abbreviation does _not_ change... # The legislation does not actually define abbreviations, at least # in this State, but the abbreviation is just commonly taken to be the # initials of the phrase, and the legislation here uniformly uses # the phrase 'summer time' and does not use the phrase 'daylight # time'. # Announcers on the Commonwealth radio network, the ABC (for Australian # Broadcasting Commission), use the phrases 'Eastern Standard Time' # or 'Eastern Summer Time'. (Note, though, that as I say in the # current australasia file, there is really no such thing.) Announcers # on its overseas service, Radio Australia, use the same phrases # prefixed by the word 'Australian' when referring to local times; # time announcements on that service, naturally enough, are made in UTC. # From Paul Eggert (2014-06-30): # # Inspired by Mackin's remarks quoted above, earlier versions of this # file used "EST" for both Eastern Standard Time and Eastern Summer # Time in Australia, and similarly for "CST", "CWST", and "WST". # However, these abbreviations were confusing and were not common # practice among Australians, and there were justifiable complaints # about them, so I attempted to survey current Australian usage. # For the tz database, the full English phrase is not that important; # what matters is the abbreviation. It's difficult to survey the web # directly for abbreviation usage, as there are so many false hits for # strings like "EST" and "EDT", so I looked for pages that defined an # abbreviation for eastern or central DST in Australia, and got the # following numbers of unique hits for the listed Google queries: # # 10 "Eastern Daylight Time AEST" site:au [some are false hits] # 10 "Eastern Summer Time AEST" site:au # 10 "Summer Time AEDT" site:au # 13 "EDST Eastern Daylight Saving Time" site:au # 18 "Summer Time ESST" site:au # 28 "Eastern Daylight Saving Time EDST" site:au # 39 "EDT Eastern Daylight Time" site:au [some are false hits] # 53 "Eastern Daylight Time EDT" site:au [some are false hits] # 54 "AEDT Australian Eastern Daylight Time" site:au # 182 "Eastern Daylight Time AEDT" site:au # # 17 "Central Daylight Time CDT" site:au [some are false hits] # 46 "Central Daylight Time ACDT" site:au # # I tried several other variants (e.g., "Eastern Summer Time EST") but # they all returned fewer than 10 unique hits. I also looked for pages # mentioning both "western standard time" and an abbreviation, since # there is no WST in the US to generate false hits, and found: # # 156 "western standard time" AWST site:au # 226 "western standard time" WST site:au # # I then surveyed the top ten newspapers in Australia by circulation as # listed in Wikipedia, using Google queries like "AEDT site:heraldsun.com.au" # and obtaining estimated counts from the initial page of search results. # All ten papers greatly preferred "AEDT" to "EDT". The papers # surveyed were the Herald Sun, The Daily Telegraph, The Courier-Mail, # The Sydney Morning Herald, The West Australian, The Age, The Advertiser, # The Australian, The Financial Review, and The Herald (Newcastle). # # I also searched for historical usage, to see whether abbreviations # like "AEDT" are new. A Trove search # found only one newspaper (The Canberra Times) with a house style # dating back to the 1970s, I expect because other newspapers weren't # fully indexed. The Canberra Times strongly preferred abbreviations # like "AEDT". The first occurrence of "AEDT" was a World Weather # column (1971-11-17, page 24), and of "ACDT" was a Scoreboard column # (1993-01-24, p 16). The style was the typical usage but was not # strictly enforced; for example, "Welcome to the twilight zones ..." # (1994-10-29, p 1) uses the abbreviations AEST/AEDT, CST/CDT, and # WST, and goes on to say, "The confusion and frustration some feel # about the lack of uniformity among Australia's six states and two # territories has prompted one group to form its very own political # party -- the Sydney-based Daylight Saving Extension Party." # # I also surveyed federal government sources. They did not agree: # # The Australian Government (2014-03-26) # http://australia.gov.au/about-australia/our-country/time # (This document was produced by the Department of Finance.) # AEST ACST AWST AEDT ACDT # # Bureau of Meteorology (2012-11-08) # http://www.bom.gov.au/climate/averages/tables/daysavtm.shtml # EST CST WST EDT CDT # # Civil Aviation Safety Authority (undated) # http://services.casa.gov.au/outnback/inc/pages/episode3/episode-3_time_zones.shtml # EST CST WST (no abbreviations given for DST) # # Geoscience Australia (2011-11-24) # http://www.ga.gov.au/geodesy/astro/sunrise.jsp # AEST ACST AWST AEDT ACDT # # Parliamentary Library (2008-11-10) # https://www.aph.gov.au/binaries/library/pubs/rp/2008-09/09rp14.pdf # EST CST WST preferred for standard time; AEST AEDT ACST ACDT also used # # The Transport Safety Bureau has an extensive series of accident reports, # and investigators seem to use whatever abbreviation they like. # Googling site:atsb.gov.au found the following number of unique hits: # 311 "ESuT", 195 "EDT", 26 "AEDT", 83 "CSuT", 46 "CDT". # "_SuT" tended to appear in older reports, and "A_DT" tended to # appear in reports of events with international implications. # # From the above it appears that there is a working consensus in # Australia to use trailing "DT" for daylight saving time; although # some sources use trailing "SST" or "ST" or "SuT" they are by far in # the minority. The case for leading "A" is weaker, but since it # seems to be preferred in the overall web and is preferred in all # the leading newspaper websites and in many government departments, # it has a stronger case than omitting the leading "A". The current # version of the database therefore uses abbreviations like "AEST" and # "AEDT" for Australian time zones. # From Paul Eggert (1995-12-19): # Shanks & Pottenger report 2:00 for all autumn changes in Australia and NZ. # Mark Prior writes that his newspaper # reports that NSW's fall 1995 change will occur at 2:00, # but Robert Elz says it's been 3:00 in Victoria since 1970 # and perhaps the newspaper's '2:00' is referring to standard time. # For now we'll continue to assume 2:00s for changes since 1960. # From Eric Ulevik (1998-01-05): # # Here are some URLs to Australian time legislation. These URLs are stable, # and should probably be included in the data file. There are probably more # relevant entries in this database. # # NSW (including LHI and Broken Hill): # Standard Time Act 1987 (updated 1995-04-04) # https://www.austlii.edu.au/au/legis/nsw/consol_act/sta1987137/index.html # ACT # Standard Time and Summer Time Act 1972 # https://www.austlii.edu.au/au/legis/act/consol_act/stasta1972279/index.html # SA # Standard Time Act, 1898 # https://www.austlii.edu.au/au/legis/sa/consol_act/sta1898137/index.html # From David Grosz (2005-06-13): # It was announced last week that Daylight Saving would be extended by # one week next year to allow for the 2006 Commonwealth Games. # Daylight Saving is now to end for next year only on the first Sunday # in April instead of the last Sunday in March. # # From Gwillim Law (2005-06-14): # I did some Googling and found that all of those states (and territory) plan # to extend DST together in 2006. # ACT: http://www.cmd.act.gov.au/mediareleases/fileread.cfm?file=86.txt # New South Wales: http://www.thecouriermail.news.com.au/common/story_page/0,5936,15538869%255E1702,00.html # South Australia: http://www.news.com.au/story/0,10117,15555031-1246,00.html # Tasmania: http://www.media.tas.gov.au/release.php?id=14772 # Victoria: I wasn't able to find anything separate, but the other articles # allude to it. # But not Queensland # http://www.news.com.au/story/0,10117,15564030-1248,00.html # Northern Territory # From George Shepherd via Simon Woodhead via Robert Elz (1991-03-06): # # The NORTHERN TERRITORY.. [ Courtesy N.T. Dept of the Chief Minister ] # # [ Nov 1990 ] # # N.T. have never utilised any DST due to sub-tropical/tropical location. # ... # Zone Australia/North 9:30 - CST # From Bradley White (1991-03-04): # A recent excerpt from an Australian newspaper... # the Northern Territory do[es] not have daylight saving. # Western Australia # From George Shepherd via Simon Woodhead via Robert Elz (1991-03-06): # # The state of WESTERN AUSTRALIA.. [ Courtesy W.A. dept Premier+Cabinet ] # # [ Nov 1990 ] # # W.A. suffers from a great deal of public and political opposition to # # DST in principle. A bill is brought before parliament in most years, but # # usually defeated either in the upper house, or in party caucus # # before reaching parliament. # ... # Zone Australia/West 8:00 AW %sST # ... # Rule AW 1974 only - Oct lastSun 2:00 1:00 D # Rule AW 1975 only - Mar Sun>=1 3:00 0 W # Rule AW 1983 only - Oct lastSun 2:00 1:00 D # Rule AW 1984 only - Mar Sun>=1 3:00 0 W # From Bradley White (1991-03-04): # A recent excerpt from an Australian newspaper... # Western Australia...do[es] not have daylight saving. # From John D. Newman via Bradley White (1991-11-02): # Western Australia is still on "winter time". Some DH in Sydney # rang me at home a few days ago at 6.00am. (He had just arrived at # work at 9.00am.) # W.A. is switching to Summer Time on Nov 17th just to confuse # everybody again. # From Arthur David Olson (1992-03-08): # The 1992 ending date used in the rules is a best guess; # it matches what was used in the past. # From Christopher Hunt (2006-11-21), after an advance warning # from Jesper Nørgaard Welen (2006-11-01): # WA are trialing DST for three years. # http://www.parliament.wa.gov.au/parliament/bills.nsf/9A1B183144403DA54825721200088DF1/$File/Bill175-1B.pdf # From Paul Eggert (2018-04-01): # The Guardian Express of Perth, Australia reported today that the # government decided to advance the clocks permanently on January 1, # 2019, from UT +08 to UT +09. The article noted that an exemption # would be made for people aged 61 and over, who "can apply in writing # to have the extra hour of sunshine removed from their area." See: # Daylight saving coming to WA in 2019. Guardian Express. 2018-04-01. # https://www.communitynews.com.au/guardian-express/news/exclusive-daylight-savings-coming-wa-summer-2018/ # [The article ends with "Today's date is April 1."] # The Australian Bureau of Meteorology FAQ # http://www.bom.gov.au/faq/faqgen.htm # (1999-09-27) writes that Giles Meteorological Station uses # South Australian time even though it's located in Western Australia. # From Rives McDow (2002-04-09): # The most interesting region I have found consists of three towns on the # southern coast.... South Australia observes daylight saving time; Western # Australia does not. The two states are one and a half hours apart. The # residents decided to forget about this nonsense of changing the clock so # much and set the local time 20 hours and 45 minutes from the # international date line, or right in the middle of the time of South # Australia and Western Australia.... # # From Paul Eggert (2002-04-09): # This is confirmed by the section entitled # "What's the deal with time zones???" in # http://www.earthsci.unimelb.edu.au/~awatkins/null.html # # From Alex Livingston (2006-12-07): # ... it was just on four years ago that I drove along the Eyre Highway, # which passes through eastern Western Australia close to the southern # coast of the continent. # # I paid particular attention to the time kept there. There can be no # dispute that UTC+08:45 was considered "the time" from the border # village just inside the border with South Australia to as far west # as just east of Caiguna. There can also be no dispute that Eucla is # the largest population centre in this zone.... # # Now that Western Australia is observing daylight saving, the # question arose whether this part of the state would follow suit. I # just called the border village and confirmed that indeed they have, # meaning that they are now observing UTC+09:45. # # (2006-12-09): # I personally doubt that either experimentation with daylight saving # in WA or its introduction in SA had anything to do with the genesis # of this time zone. My hunch is that it's been around since well # before 1975. I remember seeing it noted on road maps decades ago. # # From Gilmore Davidson (2019-04-08): # https://www.abc.net.au/news/2019-04-08/this-remote-stretch-of-desert-has-its-own-custom-time-zone/10981000 # ... include[s] a rough description of the geographical boundaries... # "The time zone exists for about 340 kilometres and takes in the tiny # roadhouse communities of Cocklebiddy, Madura, Eucla and Border Village." # ... and an indication that the zone has definitely been in existence # since before the 1970 cut-off of the database ... # From Paul Eggert (2019-05-17): # That ABC Esperance story by Christien de Garis also says: # Although the Central Western Time Zone is not officially recognised (your # phones won't automatically change), there is a sign instructing you which # way to wind your clocks 45 minutes and scrawled underneath one of them in # Texta is the word: 'Why'? # "Good question," Mr Pike said. # "I don't even know that, and it's been going for over 50 years." # From Paul Eggert (2006-12-15): # For lack of better info, assume the tradition dates back to the # introduction of standard time in 1895. # From Stuart Bishop (2024-11-12): # An article discussing the in-use but technically unofficial timezones # in the Western Australian portion of the Nullarbor Plain. # https://www.abc.net.au/news/2024-11-22/outback-wa-properties-strange-time-zones/104542494 # From Paul Eggert (2024-11-12): # As the article says, the Eyre Bird Observatory and nearby sheep stations # can use Tokyo time. Other possibilities include Asia/Chita, Asia/Seoul, # and Asia/Jayapura. # Queensland # From Paul Eggert (2018-02-26): # I lack access to the following source for Queensland DST: # Pearce C. History of daylight saving time in Queensland. # Queensland Hist J. 2017 Aug;23(6):389-403 # https://search.informit.com.au/documentSummary;dn=994682348436426;res=IELHSS # From George Shepherd via Simon Woodhead via Robert Elz (1991-03-06): # # The state of QUEENSLAND.. [ Courtesy Qld. Dept Premier Econ&Trade Devel ] # # [ Dec 1990 ] # ... # Zone Australia/Queensland 10:00 AQ %sST # ... # Rule AQ 1971 only - Oct lastSun 2:00 1:00 D # Rule AQ 1972 only - Feb lastSun 3:00 0 E # Rule AQ 1989 max - Oct lastSun 2:00 1:00 D # Rule AQ 1990 max - Mar Sun>=1 3:00 0 E # From Bradley White (1989-12-24): # "Australia/Queensland" now observes daylight time (i.e. from # October 1989). # From Bradley White (1991-03-04): # A recent excerpt from an Australian newspaper... # ...Queensland...[has] agreed to end daylight saving # at 3am tomorrow (March 3)... # From John Mackin (1991-03-06): # I can certainly confirm for my part that Daylight Saving in NSW did in fact # end on Sunday, 3 March. I don't know at what hour, though. (It surprised # me.) # From Bradley White (1992-03-08): # ...there was recently a referendum in Queensland which resulted # in the experimental daylight saving system being abandoned. So, ... # ... # Rule QLD 1989 1991 - Oct lastSun 2:00 1:00 D # Rule QLD 1990 1992 - Mar Sun>=1 3:00 0 S # ... # From Arthur David Olson (1992-03-08): # The chosen rules the union of the 1971/1972 change and the 1989-1992 changes. # southeast Australia # # From Paul Eggert (2007-07-23): # Starting autumn 2008 Victoria, NSW, South Australia, Tasmania and the ACT # end DST the first Sunday in April and start DST the first Sunday in October. # http://www.theage.com.au/news/national/daylight-savings-to-span-six-months/2007/06/27/1182623966703.html # South Australia # From Bradley White (1991-03-04): # A recent excerpt from an Australian newspaper... # ...South Australia...[has] agreed to end daylight saving # at 3am tomorrow (March 3)... # From George Shepherd via Simon Woodhead via Robert Elz (1991-03-06): # # The state of SOUTH AUSTRALIA....[ Courtesy of S.A. Dept of Labour ] # # [ Nov 1990 ] # ... # Zone Australia/South 9:30 AS %sST # ... # Rule AS 1971 max - Oct lastSun 2:00 1:00 D # Rule AS 1972 1985 - Mar Sun>=1 3:00 0 C # Rule AS 1986 1990 - Mar Sun>=15 3:00 0 C # Rule AS 1991 max - Mar Sun>=1 3:00 0 C # From Bradley White (1992-03-11): # Recent correspondence with a friend in Adelaide # contained the following exchange: "Due to the Adelaide Festival, # South Australia delays setting back our clocks for a few weeks." # From Robert Elz (1992-03-13): # I heard that apparently (or at least, it appears that) # South Aus will have an extra 3 weeks daylight saving every even # numbered year (from 1990). That's when the Adelaide Festival # is on... # From Robert Elz (1992-03-16, 00:57:07 +1000): # DST didn't end in Adelaide today (yesterday).... # But whether it's "4th Sunday" or "2nd last Sunday" I have no idea whatever... # (it's just as likely to be "the Sunday we pick for this year"...). # From Bradley White (1994-04-11): # If Sun, 15 March, 1992 was at +1030 as kre asserts, but yet Sun, 20 March, # 1994 was at +0930 as John Connolly's customer seems to assert, then I can # only conclude that the actual rule is more complicated.... # From John Warburton (1994-10-07): # The new Daylight Savings dates for South Australia ... # was gazetted in the Government Hansard on Sep 26 1994.... # start on last Sunday in October and end in last sunday in March. # From Paul Eggert (2007-07-23): # See "southeast Australia" above for 2008 and later. # Tasmania # From P Chan (2020-11-20): # Tasmania observed DST in 1916-1919. # # Daylight Saving Act, 1916 (7 Geo V, No 2) [1916-09-22] # http://classic.austlii.edu.au/au/legis/tas/num_act/tdsa19167gvn2267/ # # Daylight Saving Amendment Act, 1917 (8 Geo V, No 5) [1917-10-01] # http://classic.austlii.edu.au/au/legis/tas/num_act/tdsaa19178gvn5347/ # # Daylight Saving Act Repeal Act, 1919 (10 Geo V, No 9) [1919-10-24] # http://classic.austlii.edu.au/au/legis/tas/num_act/tdsara191910gvn9339/ # # King Island is mentioned in the 1967 Act but not the 1968 Act. # Therefore it possibly observed DST from 1968/69. # # Daylight Saving Act 1967 (No. 33 of 1967) [1967-09-22] # http://classic.austlii.edu.au/au/legis/tas/num_act/dsa196733o1967211/ # # Daylight Saving Act 1968 (No. 42 of 1968) [1968-10-15] # http://classic.austlii.edu.au/au/legis/tas/num_act/dsa196842o1968211/ # The rules for 1967 through 1991 were reported by George Shepherd # via Simon Woodhead via Robert Elz (1991-03-06): # # The state of TASMANIA.. [Courtesy Tasmanian Dept of Premier + Cabinet ] # # [ Nov 1990 ] # From Bill Hart via Guy Harris (1991-10-10): # Oh yes, the new daylight savings rules are uniquely tasmanian, we have # 6 weeks a year now when we are out of sync with the rest of Australia # (but nothing new about that). # From Alex Livingston (1999-10-04): # I heard on the ABC (Australian Broadcasting Corporation) radio news on the # (long) weekend that Tasmania, which usually goes its own way in this regard, # has decided to join with most of NSW, the ACT, and most of Victoria # (Australia) and start daylight saving on the last Sunday in August in 2000 # instead of the first Sunday in October. # Sim Alam (2000-07-03) reported a legal citation for the 2000/2001 rules: # http://www.thelaw.tas.gov.au/fragview/42++1968+GS3A@EN+2000070300 # From Paul Eggert (2007-07-23): # See "southeast Australia" above for 2008 and later. # Victoria # The rules for 1971 through 1991 were reported by George Shepherd # via Simon Woodhead via Robert Elz (1991-03-06): # # The state of VICTORIA.. [ Courtesy of Vic. Dept of Premier + Cabinet ] # # [ Nov 1990 ] # From Scott Harrington (2001-08-29): # On KQED's "City Arts and Lectures" program last night I heard an # interesting story about daylight savings time. Dr. John Heilbron was # discussing his book "The Sun in the Church: Cathedrals as Solar # Observatories"[1], and in particular the Shrine of Remembrance[2] located # in Melbourne, Australia. # # Apparently the shrine's main purpose is a beam of sunlight which # illuminates a special spot on the floor at the 11th hour of the 11th day # of the 11th month (Remembrance Day) every year in memory of Australia's # fallen WWI soldiers. And if you go there on Nov. 11, at 11am local time, # you will indeed see the sunbeam illuminate the special spot at the # expected time. # # However, that is only because of some special mirror contraption that had # to be employed, since due to daylight savings time, the true solar time of # the remembrance moment occurs one hour later (or earlier?). Perhaps # someone with more information on this jury-rig can tell us more. # # [1] http://www.hup.harvard.edu/catalog/HEISUN.html # [2] http://www.shrine.org.au # From Paul Eggert (2007-07-23): # See "southeast Australia" above for 2008 and later. # New South Wales # From Arthur David Olson: # New South Wales and subjurisdictions have their own ideas of a fun time. # Based on law library research by John Mackin, # who notes: # In Australia, time is not legislated federally, but rather by the # individual states. Thus, while such terms as "Eastern Standard Time" # [I mean, of course, Australian EST, not any other kind] are in common # use, _they have NO REAL MEANING_, as they are not defined in the # legislation. This is very important to understand. # I have researched New South Wales time only... # From Eric Ulevik (1999-05-26): # DST will start in NSW on the last Sunday of August, rather than the usual # October in 2000. See: Matthew Moore, # Two months more daylight saving, Sydney Morning Herald (1999-05-26). # http://www.smh.com.au/news/9905/26/pageone/pageone4.html # From Paul Eggert (1999-09-27): # See the following official NSW source: # Daylight Saving in New South Wales. # http://dir.gis.nsw.gov.au/cgi-bin/genobject/document/other/daylightsaving/tigGmZ # # Narrabri Shire (NSW) council has announced it will ignore the extension of # daylight saving next year. See: # Narrabri Council to ignore daylight saving # http://abc.net.au/news/regionals/neweng/monthly/regeng-22jul1999-1.htm # (1999-07-22). For now, we'll wait to see if this really happens. # # Victoria will follow NSW. See: # Vic to extend daylight saving (1999-07-28) # http://abc.net.au/local/news/olympics/1999/07/item19990728112314_1.htm # # However, South Australia rejected the DST request. See: # South Australia rejects Olympics daylight savings request (1999-07-19) # http://abc.net.au/news/olympics/1999/07/item19990719151754_1.htm # # Queensland also will not observe DST for the Olympics. See: # Qld says no to daylight savings for Olympics # http://abc.net.au/news/olympics/1999/06/item19990601114608_1.htm # (1999-06-01), which quotes Queensland Premier Peter Beattie as saying # "Look you've got to remember in my family when this came up last time # I voted for it, my wife voted against it and she said to me it's all very # well for you, you don't have to worry about getting the children out of # bed, getting them to school, getting them to sleep at night. # I've been through all this argument domestically...my wife rules." # # Broken Hill will stick with South Australian time in 2000. See: # Broken Hill to be behind the times (1999-07-21) # http://abc.net.au/news/regionals/brokenh/monthly/regbrok-21jul1999-6.htm # IATA SSIM (1998-09) says that the spring 2000 change for Australian # Capital Territory, New South Wales except Lord Howe Island and Broken # Hill, and Victoria will be August 27, presumably due to the Sydney Olympics. # From Eric Ulevik, referring to Sydney's Sun Herald (2000-08-13), page 29: # The Queensland Premier Peter Beattie is encouraging northern NSW # towns to use Queensland time. # From Paul Eggert (2007-07-23): # See "southeast Australia" above for 2008 and later. # Yancowinna # From John Mackin (1989-01-04): # 'Broken Hill' means the County of Yancowinna. # From George Shepherd via Simon Woodhead via Robert Elz (1991-03-06): # # YANCOWINNA.. [ Confirmation courtesy of Broken Hill Postmaster ] # # [ Dec 1990 ] # ... # # Yancowinna uses Central Standard Time, despite [its] location on the # # New South Wales side of the S.A. border. Most business and social dealings # # are with CST zones, therefore CST is legislated by local government # # although the switch to Summer Time occurs in line with N.S.W. There have # # been years when this did not apply, but the historical data is not # # presently available. # Zone Australia/Yancowinna 9:30 AY %sST # ... # Rule AY 1971 1985 - Oct lastSun 2:00 1:00 D # Rule AY 1972 only - Feb lastSun 3:00 0 C # [followed by other Rules] # Lord Howe Island # From George Shepherd via Simon Woodhead via Robert Elz (1991-03-06): # LHI... [ Courtesy of Pauline Van Winsen ] # [ Dec 1990 ] # Lord Howe Island is located off the New South Wales coast, and is half an # hour ahead of NSW time. # From James Lonergan, Secretary, Lord Howe Island Board (2000-01-27): # Lord Howe Island summer time in 2000/2001 will commence on the same # date as the rest of NSW (i.e. 2000-08-27). For your information the # Lord Howe Island Board (controlling authority for the Island) is # seeking the community's views on various options for summer time # arrangements on the Island, e.g. advance clocks by 1 full hour # instead of only 30 minutes. [Dependent] on the wishes of residents # the Board may approach the NSW government to change the existing # arrangements. The starting date for summer time on the Island will # however always coincide with the rest of NSW. # From James Lonergan, Secretary, Lord Howe Island Board (2000-10-25): # Lord Howe Island advances clocks by 30 minutes during DST in NSW and retards # clocks by 30 minutes when DST finishes. Since DST was most recently # introduced in NSW, the "changeover" time on the Island has been 02:00 as # shown on clocks on LHI. I guess this means that for 30 minutes at the start # of DST, LHI is actually 1 hour ahead of the rest of NSW. # From Paul Eggert (2006-03-22): # For Lord Howe dates we use Shanks & Pottenger through 1989, and # Lonergan thereafter. For times we use Lonergan. # From Paul Eggert (2007-07-23): # See "southeast Australia" above for 2008 and later. # From Steffen Thorsen (2009-04-28): # According to the official press release, South Australia's extended daylight # saving period will continue with the same rules as used during the 2008-2009 # summer (southern hemisphere). # # From # http://www.safework.sa.gov.au/uploaded_files/DaylightDatesSet.pdf # The extended daylight saving period that South Australia has been trialling # for over the last year is now set to be ongoing. # Daylight saving will continue to start on the first Sunday in October each # year and finish on the first Sunday in April the following year. # Industrial Relations Minister, Paul Caica, says this provides South Australia # with a consistent half hour time difference with NSW, Victoria, Tasmania and # the ACT for all 52 weeks of the year... # # We have a wrap-up here: # https://www.timeanddate.com/news/time/south-australia-extends-dst.html ############################################################################### # New Zealand # From Mark Davies (1990-10-03): # the 1989/90 year was a trial of an extended "daylight saving" period. # This trial was deemed successful and the extended period adopted for # subsequent years (with the addition of a further week at the start). # source - phone call to Ministry of Internal Affairs Head Office. # From George Shepherd via Simon Woodhead via Robert Elz (1991-03-06): # # The Country of New Zealand (Australia's east island -) Gee they hate that! # # or is Australia the west island of N.Z. # # [ courtesy of Geoff Tribble.. Auckland N.Z. ] # # [ Nov 1990 ] # ... # Rule NZ 1974 1988 - Oct lastSun 2:00 1:00 D # Rule NZ 1989 max - Oct Sun>=1 2:00 1:00 D # Rule NZ 1975 1989 - Mar Sun>=1 3:00 0 S # Rule NZ 1990 max - Mar lastSun 3:00 0 S # ... # Zone NZ 12:00 NZ NZ%sT # New Zealand # Zone NZ-CHAT 12:45 - NZ-CHAT # Chatham Island # From Arthur David Olson (1992-03-08): # The chosen rules use the Davies October 8 values for the start of DST in 1989 # rather than the October 1 value. # From Paul Eggert (1995-12-19); # Shank & Pottenger report 2:00 for all autumn changes in Australia and NZ. # Robert Uzgalis writes that the New Zealand Daylight # Savings Time Order in Council dated 1990-06-18 specifies 2:00 standard # time on both the first Sunday in October and the third Sunday in March. # As with Australia, we'll assume the tradition is 2:00s, not 2:00. # # From Paul Eggert (2006-03-22): # The Department of Internal Affairs (DIA) maintains a brief history, # as does Carol Squires; see tz-link.html for the full references. # Use these sources in preference to Shanks & Pottenger. # # For Chatham, IATA SSIM (1991/1999) gives the NZ rules but with # transitions at 2:45 local standard time; this confirms that Chatham # is always exactly 45 minutes ahead of Auckland. # From Colin Sharples (2007-04-30): # DST will now start on the last Sunday in September, and end on the # first Sunday in April. The changes take effect this year, meaning # that DST will begin on 2007-09-30 2008-04-06. # http://www.dia.govt.nz/diawebsite.nsf/wpg_URL/Services-Daylight-Saving-Daylight-saving-to-be-extended # From Paul Eggert (2014-07-14): # Chatham Island time was formally standardized on 1957-01-01 by # New Zealand's Standard Time Amendment Act 1956 (1956-10-26). # https://www.austlii.edu.au/nz/legis/hist_act/staa19561956n100244.pdf # According to Google Books snippet view, a speaker in the New Zealand # parliamentary debates in 1956 said "Clause 78 makes provision for standard # time in the Chatham Islands. The time there is 45 minutes in advance of New # Zealand time. I understand that is the time they keep locally, anyhow." # For now, assume this practice goes back to the introduction of standard time # in New Zealand, as this would make Chatham Islands time almost exactly match # LMT back when New Zealand was at UT +11:30; also, assume Chatham Islands did # not observe New Zealand's prewar DST. ############################################################################### # Bonin (Ogasawara) Islands and Marcus Island (Minami-Tori-shima) # From Wakaba (2019-01-28) via Phake Nick: # National Diet Library of Japan has several reports by Japanese Government # officers that describe the time used in islands when they visited there. # According to them (and other sources such as newspapers), standard time UTC # + 10 (JST + 1) and DST UTC + 11 (JST + 2) was used until its return to Japan # at 1968-06-26 00:00 JST. The exact periods of DST are still unknown. # I guessed Guam, Mariana, and Bonin and Marcus districts might have # synchronized their DST periods, but reports imply they had their own # decisions, i.e. there were three or more different time zones.... # # https://wiki.suikawiki.org/n/�笠原諸島�標準時 # From Phake Nick (2019-02-12): # Because their last time change to return to Japanese time when they returned # to Japanese rule was right before 1970, ... per the current tz database # rule, the information doesn't warrant creation of a new timezone for Bonin # Islands itself and is thus as an anecdotal note for interest purpose only. # ... [The link mentioned above] described some special timekeeping phenomenon # regarding Marcus island, another remote island currently owned by Japanese # in the same administrative unit as Bonin Islands. Many reports claim that # the American coastal guard on the American quarter of the island use its own # coastal guard time, and most sources describe the time as UTC+11, being two # hours faster than JST used by some Japanese personnel on the island. Some # sites describe it as same as Wake Island/Guam time although it would be # incorrect to be same as Guam. And then in a few Japanese governmental # report from 1980s (from National Institute of Information and Communications # Technology) regarding the construction of VLBI facility on the Marcus # Island, it claimed that there are three time standards being used on the # island at the time which include not just JST (UTC+9) or [US]CG time # (UTC+11) but also a JMSDF time (UTC+10) (Japan Maritime Self-Defense # Force). Unfortunately there are no other sources that mentioned such time # and there are also no information on things like how the time was used. # Fiji # Howse writes (p 153) that in 1879 the British governor of Fiji # enacted an ordinance standardizing the islands on Antipodean Time # instead of the American system (which was one day behind). # From Rives McDow (1998-10-08): # Fiji will introduce DST effective 0200 local time, 1998-11-01 # until 0300 local time 1999-02-28. Each year the DST period will # be from the first Sunday in November until the last Sunday in February. # From Paul Eggert (2000-01-08): # IATA SSIM (1999-09) says DST ends 0100 local time. Go with McDow. # From the BBC World Service in # http://news.bbc.co.uk/2/hi/asia-pacific/205226.stm (1998-10-31 16:03 UTC): # The Fijian government says the main reasons for the time change is to # improve productivity and reduce road accidents.... [T]he move is also # intended to boost Fiji's ability to attract tourists to witness the dawning # of the new millennium. # http://www.fiji.gov.fj/press/2000_09/2000_09_13-05.shtml (2000-09-13) # reports that Fiji has discontinued DST. # Kiribati # From Paul Eggert (1996-01-22): # Today's _Wall Street Journal_ (page 1) reports that Kiribati # "declared it the same day [throughout] the country as of Jan. 1, 1995" # as part of the competition to be first into the 21st century. # From Kerry Shetline (2018-02-03): # December 31 was the day that was skipped, so that the transition # would be from Friday December 30, 1994 to Sunday January 1, 1995. # From Paul Eggert (2018-02-04): # One source for this is page 202 of: Bartky IR. One Time Fits All: # The Campaigns for Global Uniformity (2007). # Kanton # From Paul Eggert (2021-05-27): # Kiribati's +13 timezone is represented by Kanton, its only populated # island. (It was formerly spelled "Canton", but Gilbertese lacks "C".) # Kanton was settled on 1937-08-31 by two British radio operators # ; # Americans came the next year and built an airfield, partly to # establish airline service and perhaps partly anticipating the # next war. Aside from the war, the airfield was used by commercial # airlines until long-range jets became standard; although currently # for emergency use only, China says it is considering rebuilding the # airfield for high-end niche tourism. Kanton has about two dozen # people, caretakers who rotate in from the rest of Kiribati in 2-5 # year shifts, and who use some of the leftover structures # . # Kwajalein # From an AP article (1993-08-22): # "The nearly 3,000 Americans living on this remote Pacific atoll have a good # excuse for not remembering Saturday night: there wasn't one. Residents were # going to bed Friday night and waking up Sunday morning because at midnight # -- 8 A.M. Eastern daylight time on Saturday -- Kwajalein was jumping from # one side of the international date line to the other." # "In Marshall Islands, Friday is followed by Sunday", NY Times. 1993-08-22. # https://www.nytimes.com/1993/08/22/world/in-marshall-islands-friday-is-followed-by-sunday.html # From Paul Eggert (2022-03-31): # Phake Nick (2018-10-27) noted 's # citation of a 1993 AP article published in the New York Times saying # Kwajalein synchronized its day with the US mainland about 40 years earlier. # However the AP article is vague and possibly wrong about this. The article # says the earlier switch was "about 40 years ago when the United States # Army established a missile test range here". However, the Kwajalein Test # Center was established on 1960-10-01 and was run by the US Navy. It was # transferred to the US Army on 1964-07-01. See "Seize the High Ground" # . # Given that Shanks was right on the money about the 1993 change, I'm inclined # to take Shanks's word for the 1969 change unless we find better evidence. # N Mariana Is, Guam # From Phake Nick (2018-10-27): # Guam Island was briefly annexed by Japan during ... year 1941-1944 ... # however there are no detailed information about what time it use during that # period. It would probably be reasonable to assume Guam use GMT+9 during # that period of time like the surrounding area. # From Paul Eggert (2023-01-23): # Howse writes (p 153) "The Spaniards, on the other hand, reached the # Philippines and the Ladrones from America," and implies that the Ladrones # (now called the Marianas) kept American date for quite some time. # For now, we assume the Ladrones switched at the same time as the Philippines; # see Asia/Manila. # # Use 1941-12-10 and 1944-07-31 for Guam WWII transitions, as the rough start # and end of Japanese control of Agana. We don't know whether the Northern # Marianas followed Guam's DST rules from 1959 through 1977; for now, assume # they did as that avoids the need for a separate zone due to our 1970 cutoff. # # US Public Law 106-564 (2000-12-23) made UT +10 the official standard time, # under the name "Chamorro standard time". There is no official abbreviation, # but Congressman Robert A. Underwood, author of the bill that became law, # wrote in a press release (2000-12-27) that he will seek the use of "ChST". # See also the commentary for Micronesia. # Marshall Is # See the commentary for Micronesia. # Micronesia (and nearby) # From Paul Eggert (2018-11-18): # Like the Ladrones (see Guam commentary), assume the Spanish East Indies # kept American time until the Philippines switched at the end of 1844. # From Paul Eggert (1999-10-29): # The Federated States of Micronesia Visitors Board writes in # The Federated States of Micronesia - Visitor Information (1999-01-26) # http://www.fsmgov.org/info/clocks.html # that Truk and Yap are UT +10, and Ponape and Kosrae are +11. # We don't know when Kosrae switched from +12; assume January 1 for now. # From Phake Nick (2018-10-27): # # From a Japanese wiki site https://wiki.suikawiki.org/n/�洋群島�標準時 # ... # For "Southern Islands" (modern region of Mariana + Palau + Federation of # Micronesia + Marshall Islands): # # A 1906 Japanese magazine shown the Caroline Islands and Mariana Islands # who was occupied by Germany at the time as GMT+10, together with the like # of German New Guinea. However there is a marking saying it have not been # implemented (yet). No further information after that were found. # # Japan invaded those islands in 1914, and records shows that they were # instructed to use JST at the time. # # 1915 January telecommunication record on the Jaluit Atoll shows they use # the meridian of 170E as standard time (GMT+11:20), which is similar to the # longitude of the atoll. # 1915 February record say the 170E standard time is to be used until # February 9 noon, and after February 9 noon they are to use JST. # However these are time used within the Japanese Military at the time and # probably does not reflect the time used by local resident at the time (that # is if they keep their own time back then) # # In January 1919 the occupying force issued a command that split the area # into three different timezone with meridian of 135E, 150E, 165E (JST+0, +1, # +2), and the command was to become effective from February 1 of the same # year. Despite the target of the command is still only for the occupying # force itself, further publication have described the time as the standard # time for the occupied area and thus it can probably be seen as such. # * Area that use meridian of 135E: Palau and Yap civil administration area # (Southern Islands Western Standard Time) # * Area that use meridian of 150E: Truk (Chuuk) and Saipan civil # administration area (Southern Islands Central Standard Time) # * Area that use meridian of 165E: Ponape (Pohnpei) and Jaluit civil # administration area (Southern Islands Eastern Standard Time). # * In the next few years Japanese occupation of those islands have been # formalized via League of Nation Mandate (South Pacific Mandate) and formal # governance structure have been established, these district [become # subprefectures] and timezone classification have been inherited as standard # time of the area. # * Saipan subprefecture include Mariana islands (exclude Guam which was # occupied by America at the time), Palau and Yap subprefecture rule the # Western Caroline Islands with 137E longitude as border, Truk and Ponape # subprefecture rule the Eastern Caroline Islands with 154E as border, Ponape # subprefecture also rule part of Marshall Islands to the west of 164E # starting from (1918?) and Jaluit subprefecture rule the rest of the # Marshall Islands. # # And then in year 1937, an announcement was made to change the time in the # area into 2 timezones: # * Area that use meridian of 135E: area administered by Palau, Yap and # Saipan subprefecture (Southern Islands Western Standard Time) # * Area that use meridian of 150E: area administered by Truk (Chuuk), # Ponape (Pohnpei) and Jaluit subprefecture (Southern Islands Eastern # Standard Time) # # Another announcement issued in 1941 say that on April 1 that year, # standard time of the Southern Islands would be changed to use the meridian # of 135E (GMT+9), and thus abolishing timezone different within the area. # # Then Pacific theater of WWII started and Japan slowly lose control on the # island. The webpage I linked above contain no information during this # period of time.... # # After the end of WWII, in 1946 February, a document written by the # (former?) Japanese military personnel describe there are 3 hours time # different between Caroline islands time/Wake island time and the Chungking # time, which would mean the time being used there at the time was GMT+10. # # After that, the area become Trust Territories of the Pacific Islands # under American administration from year 1947. The site listed some # American/International books/maps/publications about time used in those # area during this period of time but they doesn't seems to be reliable # information so it would be the best if someone know where can more reliable # information can be found. # # # From Paul Eggert (2018-11-18): # # For the above, use vague dates like "1914" and "1945" for transitions that # plausibly exist but for which the details are not known. The information # for Wake is too sketchy to act on. # # The 1906 GMT+10 info about German-controlled islands might not have been # done, so omit it from the data for now. # # The Jaluit info governs Kwajalein. # Midway # From Charles T O'Connor, KMTH DJ (1956), # quoted in the KTMH section of the Radio Heritage Collection # (2002-12-31): # For the past two months we've been on what is known as Daylight # Saving Time. This time has put us on air at 5am in the morning, # your time down there in New Zealand. Starting September 2, 1956 # we'll again go back to Standard Time. This'll mean that we'll go to # air at 6am your time. # # From Paul Eggert (2003-03-23): # We don't know the date of that quote, but we'll guess they # started DST on June 3. Possibly DST was observed other years # in Midway, but we have no record of it. # Nauru # From Phake Nick (2018-10-31): # Currently, the tz database say Nauru use LMT until 1921, and then # switched to GMT+11:30 for the next two decades. # However, a number of timezone map published in America/Japan back then # showed its timezone as GMT+11 per https://wiki.suikawiki.org/n/ナウル�標準時 # And it would also be nice if the 1921 transition date could be sourced. # ... # The "Nauru Standard Time Act 1978 Time Change" # http://ronlaw.gov.nr/nauru_lpms/files/gazettes/4b23a17d2030150404db7a5fa5872f52.pdf#page=3 # based on "Nauru Standard Time Act 1978 Time Change" # http://www.paclii.org/nr/legis/num_act/nsta1978207/ defined that "Nauru # Alternative Time" (GMT+12) should be in effect from 1979 Feb. # # From Paul Eggert (2018-11-19): # The 1921-01-15 introduction of standard time is in Shanks; it is also in # "Standard Time Throughout the World", US National Bureau of Standards (1935), # https://nvlpubs.nist.gov/nistpubs/Legacy/circ/nbscircular406.pdf # page 3, which does not give the UT offset. In response to a comment by # Phake Nick I set the Nauru time of occupation by Japan to # 1942-08-29/1945-09-08 by using dates from: # https://en.wikipedia.org/wiki/Japanese_occupation_of_Nauru # Norfolk # From Alexander Krivenyshev (2015-09-23): # Norfolk Island will change ... from +1130 to +1100: # https://www.comlaw.gov.au/Details/F2015L01483/Explanatory%20Statement/Text # ... at 12.30 am (by legal time in New South Wales) on 4 October 2015. # http://www.norfolkisland.gov.nf/nia/MediaRelease/Media%20Release%20Norfolk%20Island%20Standard%20Time%20Change.pdf # From Paul Eggert (2019-08-28): # Transitions before 2015 are from timeanddate.com, which consulted # the Norfolk Island Museum and the Australian Bureau of Meteorology's # Norfolk Island station, and found no record of Norfolk observing DST # other than in 1974/5. See: # https://www.timeanddate.com/time/australia/norfolk-island.html # However, disagree with timeanddate about the 1975-03-02 transition; # timeanddate has 02:00 but 02:00s corresponds to what the NSW law said # (thanks to Michael Deckers). # Norfolk started observing Australian DST in spring 2019. # From Kyle Czech (2019-08-13): # https://www.legislation.gov.au/Details/F2018L01702 # From Michael Deckers (2019-08-14): # https://www.legislation.gov.au/Details/F2019C00010 # Palau # See commentary for Micronesia. # Pitcairn # From Rives McDow (1999-11-08): # A Proclamation was signed by the Governor of Pitcairn on the 27th March 1998 # with regard to Pitcairn Standard Time. The Proclamation is as follows. # # The local time for general purposes in the Islands shall be # Co-ordinated Universal time minus 8 hours and shall be known # as Pitcairn Standard Time. # # ... I have also seen Pitcairn listed as UTC minus 9 hours in several # references, and can only assume that this was an error in interpretation # somehow in light of this proclamation. # From Rives McDow (1999-11-09): # The Proclamation regarding Pitcairn time came into effect on 27 April 1998 # ... at midnight. # From Howie Phelps (1999-11-10), who talked to a Pitcairner via shortwave: # Betty Christian told me yesterday that their local time is the same as # Pacific Standard Time. They used to be ½ hour different from us here in # Sacramento but it was changed a couple of years ago. # (Western) Samoa and American Samoa # Howse writes (p 153) that after the 1879 standardization on Antipodean # time by the British governor of Fiji, the King of Samoa decided to change # "the date in his kingdom from the Antipodean to the American system, # ordaining - by a masterpiece of diplomatic flattery - that # the Fourth of July should be celebrated twice in that year." # This happened in 1892, according to the Evening News (Sydney) of 1892-07-20. # https://webspace.science.uu.nl/~gent0113/idl/idl_alaska_samoa.htm # Although Shanks & Pottenger says they both switched to UT -11:30 # in 1911, and to -11 in 1950, many earlier sources give -11 # for American Samoa, e.g., the US National Bureau of Standards # circular "Standard Time Throughout the World", 1932. # https://nvlpubs.nist.gov/nistpubs/Legacy/circ/nbscircular399.pdf # Assume American Samoa switched to -11 in 1911, not 1950, # and that after 1950 they agreed until (western) Samoa skipped a # day in 2011. Assume also that the Samoas follow the US and New # Zealand's "ST"/"DT" style of daylight-saving abbreviations. # Tonga # From Paul Eggert (2021-03-04): # In 1943 "The standard time kept is 12 hrs. 19 min. 12 sec. fast # on Greenwich mean time." according to the Admiralty's Hydrographic # Dept., Pacific Islands Pilot, Vol. II, 7th ed., 1943, p 360. # From Michael Deckers (2021-03-03): # [Ian R Bartky: "One Time Fits All: The Campaigns for Global Uniformity". # Stanford University Press. 2007. p. 255]: # On 10 September 1945 Tonga adopted a standard time 12 hours, # 20 minutes in advance of Greenwich. # From Paul Eggert (1996-01-22): # Today's _Wall Street Journal_ (p 1) reports that "Tonga has been plotting # to sneak ahead of [New Zealanders] by introducing daylight-saving time." # Since Kiribati has moved the Date Line it's not clear what Tonga will do. # Don Mundell writes in the 1997-02-20 Tonga Chronicle # How Tonga became 'The Land where Time Begins': # http://www.tongatapu.net.to/tonga/homeland/timebegins.htm # # Until 1941 Tonga maintained a standard time 50 minutes ahead of NZST # 12 hours and 20 minutes ahead of GMT. When New Zealand adjusted its # standard time in 1940s, Tonga had the choice of subtracting from its # local time to come on the same standard time as New Zealand or of # advancing its time to maintain the differential of 13° # (approximately 50 minutes ahead of New Zealand time). # # Because His Majesty King T�ufaʻ�hau Tupou IV, then Crown Prince # Tungī, preferred to ensure Tonga's title as the land where time # begins, the Legislative Assembly approved the latter change. # # But some of the older, more conservative members from the outer # islands objected. "If at midnight on Dec. 31, we move ahead 40 # minutes, as your Royal Highness wishes, what becomes of the 40 # minutes we have lost?" # # The Crown Prince, presented an unanswerable argument: "Remember that # on the World Day of Prayer, you would be the first people on Earth # to say your prayers in the morning." # # From Tim Parenti (2021-09-13), per Paul Eggert (2006-03-22) and Michael # Deckers (2021-03-03): # Mundell places the transition from +12:20 to +13 in 1941, while Shanks & # Pottenger say the transition was on 1968-10-01. # # The Air Almanac published contemporaneous tables of standard times, # which listed +12:20 as of Nov 1960 and +13 as of Mar 1961: # Nov 1960: https://books.google.com/books?id=bVgtWM6kPZUC&pg=SL1-PA19 # Mar 1961: https://books.google.com/books?id=W2nItAul4g0C&pg=SL1-PA19 # (Thanks to P Chan for pointing us toward these sources.) # This agrees with Bartky, who writes that "since 1961 [Tonga's] official time # has been thirteen hours in advance of Greenwich time" (p. 202) and further # writes in an endnote that this was because "the legislation was amended" on # 1960-10-19. (p. 255) # # Without greater specificity, presume that Bartky and the Air Almanac point to # a 1961-01-01 transition, as T�ufaʻ�hau Tupou IV was still Crown Prince in # 1961 and this still jives with the gist of Mundell's telling, and go with # this over Shanks & Pottenger. # From Eric Ulevik (1999-05-03): # Tonga's director of tourism, who is also secretary of the National Millennium # Committee, has a plan to get Tonga back in front. # He has proposed a one-off move to tropical daylight saving for Tonga from # October to March, which has won approval in principle from the Tongan # Government. # From Steffen Thorsen (1999-09-09): # * Tonga will introduce DST in November # # I was given this link by John Letts: # http://news.bbc.co.uk/hi/english/world/asia-pacific/newsid_424000/424764.stm # # I have not been able to find exact dates for the transition in November # yet. By reading this article it seems like Fiji will be 14 hours ahead # of UTC as well, but as far as I know Fiji will only be 13 hours ahead # (12 + 1 hour DST). # From Arthur David Olson (1999-09-20): # According to : # "Daylight Savings Time will take effect on Oct. 2 through April 15, 2000 # and annually thereafter from the first Saturday in October through the # third Saturday of April. Under the system approved by Privy Council on # Sept. 10, clocks must be turned ahead one hour on the opening day and # set back an hour on the closing date." # Alas, no indication of the time of day. # From Rives McDow (1999-10-06): # Tonga started its Daylight Saving on Saturday morning October 2nd at 0200am. # Daylight Saving ends on April 16 at 0300am which is Sunday morning. # From Steffen Thorsen (2000-10-31): # Back in March I found a notice on the website http://www.tongaonline.com # that Tonga changed back to standard time one month early, on March 19 # instead of the original reported date April 16. Unfortunately, the article # is no longer available on the site, and I did not make a copy of the # text, and I have forgotten to report it here. # (Original URL was ) # From Rives McDow (2000-12-01): # Tonga is observing DST as of 2000-11-04 and will stop on 2001-01-27. # From Sione Moala-Mafi (2001-09-20) via Rives McDow: # At 2:00am on the first Sunday of November, the standard time in the Kingdom # shall be moved forward by one hour to 3:00am. At 2:00am on the last Sunday # of January the standard time in the Kingdom shall be moved backward by one # hour to 1:00am. # From Pulu ʻAnau (2002-11-05): # The law was for 3 years, supposedly to get renewed. It wasn't. # From Pulu ʻAnau (2016-10-27): # http://mic.gov.to/news-today/press-releases/6375-daylight-saving-set-to-run-from-6-november-2016-to-15-january-2017 # Cannot find anyone who knows the rules, has seen the duration or has seen # the cabinet decision, but it appears we are following Fiji's rule set. # # From Tim Parenti (2016-10-26): # Assume Tonga will observe DST from the first Sunday in November at 02:00 # through the third Sunday in January at 03:00, like Fiji, for now. # From David Wade (2017-10-18): # In August government was dissolved by the King. The current prime minister # continued in office in care taker mode. It is easy to see that few # decisions will be made until elections 16th November. # # From Paul Eggert (2017-10-18): # For now, guess that DST is discontinued. That's what the IATA is guessing. ############################################################################### # The International Date Line # From Gwillim Law (2000-01-03): # # The International Date Line is not defined by any international standard, # convention, or treaty. Mapmakers are free to draw it as they please. # Reputable mapmakers will simply ensure that every point of land appears on # the correct side of the IDL, according to the date legally observed there. # # When Kiribati adopted a uniform date in 1995, thereby moving the Phoenix and # Line Islands to the west side of the IDL (or, if you prefer, moving the IDL # to the east side of the Phoenix and Line Islands), I suppose that most # mapmakers redrew the IDL following the boundary of Kiribati. Even that line # has a rather arbitrary nature. The straight-line boundaries between Pacific # island nations that are shown on many maps are based on an international # convention, but are not legally binding national borders.... The date is # governed by the IDL; therefore, even on the high seas, there may be some # places as late as fourteen hours later than UTC. And, since the IDL is not # an international standard, there are some places on the high seas where the # correct date is ambiguous. # From Wikipedia (2023-01-23): # The nautical time zone system is analogous to the terrestrial time zone # system for use on high seas. Under the system time changes are required for # changes of longitude in one-hour steps. The one-hour step corresponds to a # time zone width of 15° longitude. The 15° gore that is offset from GMT or # UT1 (not UTC) by twelve hours is bisected by the nautical date line into two # 7°30' gores that differ from GMT by ±12 hours. A nautical date line is # implied but not explicitly drawn on time zone maps. It follows the 180th # meridian except where it is interrupted by territorial waters adjacent to # land, forming gaps: it is a pole-to-pole dashed line. # From Paul Eggert (2023-01-23): # The American Practical Navigator , # 2019 edition, merely says that the International Date Line # "coincides with the 180th meridian over most of its length." pr0m1th3as-datatypes-9450b29/inst/tzdata/backward000066400000000000000000000333431527370255000216510ustar00rootroot00000000000000# Links and zones for backward compatibility # This file is in the public domain, so clarified as of # 2009-05-17 by Arthur David Olson. # This file provides links from old or merged timezone names to current ones. # It also provides a few zone entries for old naming conventions. # Many names changed in 1993 and in 1995, and many merged names moved here # in the period from 2013 through 2022. Several of these names are # also present in the file 'backzone', which has data important only # for pre-1970 timestamps and so is out of scope for tzdb proper. # Although this file is optional and tzdb will work if you omit it by # building with 'make BACKWARD=', in practice downstream users # typically use this file for backward compatibility. # This file is divided into sections, one for each major reason for a # backward compatibility link or zone. Each section of links is # sorted by name. # A "#= TARGET1" comment labels each link inserted only because some # .zi parsers (including tzcode through 2022e) mishandle links to links. # The comment says what the target would be if these parsers were fixed # so that data could contain links to links. For example, the line # "Link Australia/Sydney Australia/ACT #= Australia/Canberra" would be # "Link Australia/Canberra Australia/ACT" were it not that data lines # refrain from linking to links like Australia/Canberra, which means # the Australia/ACT line links instead to Australia/Sydney, # Australia/Canberra's target. # Pre-1993 naming conventions # Link TARGET LINK-NAME #= TARGET1 Link Australia/Sydney Australia/ACT #= Australia/Canberra Link Australia/Lord_Howe Australia/LHI Link Australia/Sydney Australia/NSW Link Australia/Darwin Australia/North Link Australia/Brisbane Australia/Queensland Link Australia/Adelaide Australia/South Link Australia/Hobart Australia/Tasmania Link Australia/Melbourne Australia/Victoria Link Australia/Perth Australia/West Link Australia/Broken_Hill Australia/Yancowinna Link America/Rio_Branco Brazil/Acre #= America/Porto_Acre Link America/Noronha Brazil/DeNoronha Link America/Sao_Paulo Brazil/East Link America/Manaus Brazil/West Link Europe/Brussels CET Link America/Halifax Canada/Atlantic Link America/Winnipeg Canada/Central # This line is commented out, as the name exceeded the 14-character limit # and was an unused misnomer. #Link America/Regina Canada/East-Saskatchewan Link America/Toronto Canada/Eastern Link America/Edmonton Canada/Mountain Link America/St_Johns Canada/Newfoundland Link America/Vancouver Canada/Pacific Link America/Regina Canada/Saskatchewan Link America/Whitehorse Canada/Yukon Link America/Santiago Chile/Continental Link Pacific/Easter Chile/EasterIsland Link America/Havana Cuba Link Europe/Athens EET Link America/Panama EST Link Africa/Cairo Egypt Link Europe/Dublin Eire # Vanguard section, for most .zi parsers. #Link GMT Etc/GMT #Link GMT Etc/GMT+0 #Link GMT Etc/GMT-0 #Link GMT Etc/GMT0 #Link GMT Etc/Greenwich # Rearguard section, for TZUpdater 2.3.2 and earlier. Link Etc/GMT Etc/GMT+0 Link Etc/GMT Etc/GMT-0 Link Etc/GMT Etc/GMT0 Link Etc/GMT Etc/Greenwich # End of rearguard section. Link Etc/UTC Etc/UCT Link Etc/UTC Etc/Universal Link Etc/UTC Etc/Zulu Link Europe/London GB Link Europe/London GB-Eire # Vanguard section, for most .zi parsers. #Link GMT GMT+0 #Link GMT GMT-0 #Link GMT GMT0 #Link GMT Greenwich # Rearguard section, for TZUpdater 2.3.2 and earlier. Link Etc/GMT GMT+0 Link Etc/GMT GMT-0 Link Etc/GMT GMT0 Link Etc/GMT Greenwich # End of rearguard section. Link Asia/Hong_Kong Hongkong Link Africa/Abidjan Iceland #= Atlantic/Reykjavik Link Asia/Tehran Iran Link Asia/Jerusalem Israel Link America/Jamaica Jamaica Link Asia/Tokyo Japan Link Pacific/Kwajalein Kwajalein Link Africa/Tripoli Libya Link Europe/Brussels MET Link America/Phoenix MST Link America/Tijuana Mexico/BajaNorte Link America/Mazatlan Mexico/BajaSur Link America/Mexico_City Mexico/General Link Pacific/Auckland NZ Link Pacific/Chatham NZ-CHAT Link America/Denver Navajo #= America/Shiprock Link Asia/Shanghai PRC Link Europe/Warsaw Poland Link Europe/Lisbon Portugal Link Asia/Taipei ROC Link Asia/Seoul ROK Link Asia/Singapore Singapore Link Europe/Istanbul Turkey Link Etc/UTC UCT Link America/Anchorage US/Alaska Link America/Adak US/Aleutian Link America/Phoenix US/Arizona Link America/Chicago US/Central Link America/Indiana/Indianapolis US/East-Indiana Link America/New_York US/Eastern Link Pacific/Honolulu US/Hawaii Link America/Indiana/Knox US/Indiana-Starke Link America/Detroit US/Michigan Link America/Denver US/Mountain Link America/Los_Angeles US/Pacific Link Pacific/Pago_Pago US/Samoa Link Etc/UTC UTC Link Etc/UTC Universal Link Europe/Moscow W-SU Link Etc/UTC Zulu # Two-part names that were renamed mostly to three-part names in 1995 # Link TARGET LINK-NAME #= TARGET1 Link America/Argentina/Buenos_Aires America/Buenos_Aires Link America/Argentina/Catamarca America/Catamarca Link America/Argentina/Cordoba America/Cordoba Link America/Indiana/Indianapolis America/Indianapolis Link America/Argentina/Jujuy America/Jujuy Link America/Indiana/Knox America/Knox_IN Link America/Kentucky/Louisville America/Louisville Link America/Argentina/Mendoza America/Mendoza Link America/Puerto_Rico America/Virgin #= America/St_Thomas Link Pacific/Pago_Pago Pacific/Samoa # Pre-2013 practice, which typically had a Zone per zone.tab line # Link TARGET LINK-NAME Link Africa/Abidjan Africa/Accra Link Africa/Nairobi Africa/Addis_Ababa Link Africa/Nairobi Africa/Asmara Link Africa/Abidjan Africa/Bamako Link Africa/Lagos Africa/Bangui Link Africa/Abidjan Africa/Banjul Link Africa/Maputo Africa/Blantyre Link Africa/Lagos Africa/Brazzaville Link Africa/Maputo Africa/Bujumbura Link Africa/Abidjan Africa/Conakry Link Africa/Abidjan Africa/Dakar Link Africa/Nairobi Africa/Dar_es_Salaam Link Africa/Nairobi Africa/Djibouti Link Africa/Lagos Africa/Douala Link Africa/Abidjan Africa/Freetown Link Africa/Maputo Africa/Gaborone Link Africa/Maputo Africa/Harare Link Africa/Nairobi Africa/Kampala Link Africa/Maputo Africa/Kigali Link Africa/Lagos Africa/Kinshasa Link Africa/Lagos Africa/Libreville Link Africa/Abidjan Africa/Lome Link Africa/Lagos Africa/Luanda Link Africa/Maputo Africa/Lubumbashi Link Africa/Maputo Africa/Lusaka Link Africa/Lagos Africa/Malabo Link Africa/Johannesburg Africa/Maseru Link Africa/Johannesburg Africa/Mbabane Link Africa/Nairobi Africa/Mogadishu Link Africa/Lagos Africa/Niamey Link Africa/Abidjan Africa/Nouakchott Link Africa/Abidjan Africa/Ouagadougou Link Africa/Lagos Africa/Porto-Novo Link America/Puerto_Rico America/Anguilla Link America/Puerto_Rico America/Antigua Link America/Puerto_Rico America/Aruba Link America/Panama America/Atikokan Link America/Puerto_Rico America/Blanc-Sablon Link America/Panama America/Cayman Link America/Phoenix America/Creston Link America/Puerto_Rico America/Curacao Link America/Puerto_Rico America/Dominica Link America/Puerto_Rico America/Grenada Link America/Puerto_Rico America/Guadeloupe Link America/Puerto_Rico America/Kralendijk Link America/Puerto_Rico America/Lower_Princes Link America/Puerto_Rico America/Marigot Link America/Puerto_Rico America/Montserrat Link America/Toronto America/Nassau Link America/Puerto_Rico America/Port_of_Spain Link America/Puerto_Rico America/St_Barthelemy Link America/Puerto_Rico America/St_Kitts Link America/Puerto_Rico America/St_Lucia Link America/Puerto_Rico America/St_Thomas Link America/Puerto_Rico America/St_Vincent Link America/Puerto_Rico America/Tortola Link Pacific/Port_Moresby Antarctica/DumontDUrville Link Pacific/Auckland Antarctica/McMurdo Link Asia/Riyadh Antarctica/Syowa Link Europe/Berlin Arctic/Longyearbyen Link Asia/Riyadh Asia/Aden Link Asia/Qatar Asia/Bahrain Link Asia/Kuching Asia/Brunei Link Asia/Singapore Asia/Kuala_Lumpur Link Asia/Riyadh Asia/Kuwait Link Asia/Dubai Asia/Muscat Link Asia/Bangkok Asia/Phnom_Penh Link Asia/Bangkok Asia/Vientiane Link Africa/Abidjan Atlantic/Reykjavik Link Africa/Abidjan Atlantic/St_Helena Link Europe/Brussels Europe/Amsterdam Link Europe/Prague Europe/Bratislava Link Europe/Zurich Europe/Busingen Link Europe/Berlin Europe/Copenhagen Link Europe/London Europe/Guernsey Link Europe/London Europe/Isle_of_Man Link Europe/London Europe/Jersey Link Europe/Belgrade Europe/Ljubljana Link Europe/Brussels Europe/Luxembourg Link Europe/Helsinki Europe/Mariehamn Link Europe/Paris Europe/Monaco Link Europe/Berlin Europe/Oslo Link Europe/Belgrade Europe/Podgorica Link Europe/Rome Europe/San_Marino Link Europe/Belgrade Europe/Sarajevo Link Europe/Belgrade Europe/Skopje Link Europe/Berlin Europe/Stockholm Link Europe/Zurich Europe/Vaduz Link Europe/Rome Europe/Vatican Link Europe/Belgrade Europe/Zagreb Link Africa/Nairobi Indian/Antananarivo Link Asia/Bangkok Indian/Christmas Link Asia/Yangon Indian/Cocos Link Africa/Nairobi Indian/Comoro Link Indian/Maldives Indian/Kerguelen Link Asia/Dubai Indian/Mahe Link Africa/Nairobi Indian/Mayotte Link Asia/Dubai Indian/Reunion Link Pacific/Port_Moresby Pacific/Chuuk Link Pacific/Tarawa Pacific/Funafuti Link Pacific/Tarawa Pacific/Majuro Link Pacific/Pago_Pago Pacific/Midway Link Pacific/Guadalcanal Pacific/Pohnpei Link Pacific/Guam Pacific/Saipan Link Pacific/Tarawa Pacific/Wake Link Pacific/Tarawa Pacific/Wallis # Non-zone.tab locations with timestamps since 1970 that duplicate # those of an existing location # Link TARGET LINK-NAME Link Africa/Abidjan Africa/Timbuktu Link America/Argentina/Catamarca America/Argentina/ComodRivadavia Link America/Adak America/Atka Link America/Panama America/Coral_Harbour Link America/Tijuana America/Ensenada Link America/Indiana/Indianapolis America/Fort_Wayne Link America/Toronto America/Montreal Link America/Toronto America/Nipigon Link America/Iqaluit America/Pangnirtung Link America/Rio_Branco America/Porto_Acre Link America/Winnipeg America/Rainy_River Link America/Argentina/Cordoba America/Rosario Link America/Tijuana America/Santa_Isabel Link America/Denver America/Shiprock Link America/Toronto America/Thunder_Bay Link America/Edmonton America/Yellowknife Link Pacific/Auckland Antarctica/South_Pole Link Asia/Ulaanbaatar Asia/Choibalsan Link Asia/Shanghai Asia/Chongqing Link Asia/Shanghai Asia/Harbin Link Asia/Urumqi Asia/Kashgar Link Asia/Jerusalem Asia/Tel_Aviv Link Europe/Berlin Atlantic/Jan_Mayen Link Australia/Sydney Australia/Canberra Link Australia/Hobart Australia/Currie Link Europe/London Europe/Belfast Link Europe/Chisinau Europe/Tiraspol Link Europe/Kyiv Europe/Uzhgorod Link Europe/Kyiv Europe/Zaporozhye Link Pacific/Kanton Pacific/Enderbury Link Pacific/Honolulu Pacific/Johnston Link Pacific/Port_Moresby Pacific/Yap Link Europe/Lisbon WET # Alternate names for the same location # Link TARGET LINK-NAME #= TARGET1 Link Africa/Nairobi Africa/Asmera #= Africa/Asmara Link America/Nuuk America/Godthab Link Asia/Ashgabat Asia/Ashkhabad Link Asia/Kolkata Asia/Calcutta Link Asia/Shanghai Asia/Chungking #= Asia/Chongqing Link Asia/Dhaka Asia/Dacca # Istanbul is in both continents. Link Europe/Istanbul Asia/Istanbul Link Asia/Kathmandu Asia/Katmandu Link Asia/Macau Asia/Macao Link Asia/Yangon Asia/Rangoon Link Asia/Ho_Chi_Minh Asia/Saigon Link Asia/Thimphu Asia/Thimbu Link Asia/Makassar Asia/Ujung_Pandang Link Asia/Ulaanbaatar Asia/Ulan_Bator Link Atlantic/Faroe Atlantic/Faeroe Link Europe/Kyiv Europe/Kiev # Classically, Cyprus is in Asia; e.g. see Herodotus, Histories, I.72. # However, for various reasons many users expect to find it under Europe. Link Asia/Nicosia Europe/Nicosia Link Pacific/Honolulu HST Link Pacific/Guadalcanal Pacific/Ponape #= Pacific/Pohnpei Link Pacific/Port_Moresby Pacific/Truk #= Pacific/Chuuk # The backward-compatibility zones PST8PDT, MST7MDT, CST6CDT, and EST5EDT # Near-copy of US rules in the northamerica file, # so that this file can be compiled separately. # To conform to POSIX, this copy uses D instead of W for War and P for Peace. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule USback 1918 1919 - Mar lastSun 2:00 1:00 D Rule USback 1918 1919 - Oct lastSun 2:00 0 S Rule USback 1942 only - Feb 9 2:00 1:00 D # War Rule USback 1945 only - Aug 14 23:00u 1:00 D # Peace Rule USback 1945 only - Sep 30 2:00 0 S Rule USback 1967 2006 - Oct lastSun 2:00 0 S Rule USback 1967 1973 - Apr lastSun 2:00 1:00 D Rule USback 1974 only - Jan 6 2:00 1:00 D Rule USback 1975 only - Feb lastSun 2:00 1:00 D Rule USback 1976 1986 - Apr lastSun 2:00 1:00 D Rule USback 1987 2006 - Apr Sun>=1 2:00 1:00 D Rule USback 2007 max - Mar Sun>=8 2:00 1:00 D Rule USback 2007 max - Nov Sun>=1 2:00 0 S # From Arthur David Olson (2005-12-19): # We generate the files specified below to guard against old files with # obsolete information being left in the time zone binary directory. # We limit the list to names that have appeared in previous versions of # this time zone package. # We do these as separate Zones rather than as Links to avoid problems if # a particular place changes whether it observes DST. # # From Paul Eggert (2026-07-24): # These zones are for backward compatibility with Unix System V (1983). # They are ahistorical, in that they pretend standard time extends # backward indefinitely into the past. # They use USback (not US) rules so that they conform to POSIX instead # of using abbreviations like EWT for Eastern War Time during World War II. # The zones are here, not in the northamerica file, to lessen # confusion downstream by making it clear that the zones are present # only for backward compatibility. # New software should use settings like TZ='America/New_York' instead # of these backwards-compatibility zones. # # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone PST8PDT -8:00 USback P%sT Zone MST7MDT -7:00 USback M%sT Zone CST6CDT -6:00 USback C%sT Zone EST5EDT -5:00 USback E%sT pr0m1th3as-datatypes-9450b29/inst/tzdata/backzone000066400000000000000000002101601527370255000216610ustar00rootroot00000000000000# Zones that go back beyond the scope of the tz database # This file is in the public domain. # This file is by no means authoritative; if you think you know better, # go ahead and edit the file, and please send any changes to # the public mailing list tz@iana.org for general use in the future. # For more, please see the file CONTRIBUTING in the tz distribution. # When proposing changes to this file, please use 'git format-patch' # format, either by attaching the resulting .patch file to your email, # or by using 'git send-email'. This will help maintainers save time. # From Paul Eggert (2014-10-31): # This file contains data outside the normal scope of the tz database, # in that its zones do not differ from normal tz zones after 1970. # Links in this file point to zones in this file, superseding links in # the file 'backward'. # Although zones in this file may be of some use for analyzing # pre-1970 timestamps, they are less reliable, cover only a tiny # sliver of the pre-1970 era, and cannot feasibly be improved to cover # most of the era. Because the zones are out of normal scope for the # database, less effort is put into maintaining this file. Many of # the zones were formerly in other source files, but were removed or # replaced by links as their data entries were questionable and/or they # differed from other zones only in pre-1970 timestamps. # Unless otherwise specified, the source for data through 1990 is: # Thomas G. Shanks and Rique Pottenger, The International Atlas (6th edition), # San Diego: ACS Publications, Inc. (2003). # Unfortunately this book contains many errors and cites no sources. # This file is not intended to be compiled standalone, as it # assumes rules from other files. In the tz distribution, use # 'make PACKRATDATA=backzone zones' to compile and install this file. # From Paul Eggert (2020-04-15): # The following remarks should be incorporated into this table sometime. # Patches in 'git format-patch' format would be welcome. # # From Phake Nick (2020-04-15): # ... the historical timezone data for those China zones seems to be # incorrect. The transition to GMT+8 date given there for these zones # were 1980 which also contradict the file description that they do # not disagree with normal zone after 1970. According to sources that # have also been cited in the asia file, except Xinjiang and Tibet, # they should have adopted the Beijing Time from around 1949/1950 # depends on exactly when each of those cities were taken over by the # communist army. And they should also follow the DST setting of # Asia/Shanghai after that point of time. In addition, # http://gaz.ncl.edu.tw/detail.jsp?sysid=E1091792 the document from # Chongqing Nationalist government say in year 1945 all of China # should adopt summer time due to the war (not sure whether it # continued after WWII ends)(Probably only enforced in area under # their rule at the time?) The Asia/Harbin's 1932 and 1940 entry # should also be incorrect. As per sources recorded at # https://wiki.suikawiki.org/n/%E6%BA%80%E5%B7%9E%E5%9B%BD%E3%81%AE%E6%A8%99%E6%BA%96%E6%99%82 # , in 1932 Harbin should have adopted UTC+8:00 instead of data # currently listed in the tz database according to official # announcement from Manchuko. And they should have adopted GMT+9 in # 1937 January 1st according to official announcement at the time # being cited on the webpage. # Zones are sorted by zone name. Each zone is preceded by the # name of the country that the zone is in, along with any other # commentary and rules associated with the entry. # If the zone overrides links in the main data, it # is followed by the corresponding Link lines. # If the zone overrides main-data links only when building with # PACKRATLIST=zone.tab, it is followed by a commented-out Link line # that starts with "#PACKRATLIST zone.tab". # # As explained in the zic man page, the zone columns are: # Zone NAME STDOFF RULES FORMAT [UNTIL] # and the rule columns are: # Rule NAME FROM TO - IN ON AT SAVE LETTER/S # Ghana # From P Chan (2020-11-20): # Interpretation Amendment Ordinance, 1915 (No.24 of 1915) [1915-11-02] # Ordinances of the Gold Coast, Ashanti, Northern Territories 1915, p 69-71 # https://books.google.com/books?id=ErA-AQAAIAAJ&pg=PA70 # This Ordinance added "'Time' shall mean Greenwich Mean Time" to the # Interpretation Ordinance, 1876. # # Determination of the Time Ordinance, 1919 (No. 18 of 1919) [1919-11-24] # Ordinances of the Gold Coast, Ashanti, Northern Territories 1919, p 75-76 # https://books.google.com/books?id=MbA-AQAAIAAJ&pg=PA75 # This Ordinance removed the previous definition of time and introduced DST. # # Time Determination Ordinance (Cap. 214) # The Laws of the Gold Coast (including Togoland Under British Mandate) # Vol. II (1937), p 2328 # https://books.google.com/books?id=Z7M-AQAAIAAJ&pg=PA2328 # Revised edition of the 1919 Ordinance. # # Time Determination (Amendment) Ordinance, 1940 (No. 9 of 1940) [1940-04-06] # Annual Volume of the Laws of the Gold Coast: # Containing All Legislation Enacted During Year 1940, p 22 # https://books.google.com/books?id=1ao-AQAAIAAJ&pg=PA22 # This Ordinance changed the forward transition from September to May. # # Defence (Time Determination Ordinance Amendment) Regulations, 1942 # (Regulations No. 6 of 1942) [1942-01-31, commenced on 1942-02-08] # Annual Volume of the Laws of the Gold Coast: # Containing All Legislation Enacted During Year 1942, p 48 # https://books.google.com/books?id=Das-AQAAIAAJ&pg=PA48 # These regulations advanced the [standard] time by thirty minutes. # # Defence (Time Determination Ordinance Amendment (No.2)) Regulations, # 1942 (Regulations No. 28 of 1942) [1942-04-25] # Annual Volume of the Laws of the Gold Coast: # Containing All Legislation Enacted During Year 1942, p 87 # https://books.google.com/books?id=Das-AQAAIAAJ&pg=PA87 # These regulations abolished DST and changed the time to GMT+0:30. # # Defence (Revocation) (No.4) Regulations, 1945 (Regulations No. 45 of # 1945) [1945-10-24, commenced on 1946-01-06] # Annual Volume of the Laws of the Gold Coast: # Containing All Legislation Enacted During Year 1945, p 256 # https://books.google.com/books?id=9as-AQAAIAAJ&pg=PA256 # These regulations revoked the previous two sets of Regulations. # # Time Determination (Amendment) Ordinance, 1945 (No. 18 of 1945) [1946-01-06] # Annual Volume of the Laws of the Gold Coast: # Containing All Legislation Enacted During Year 1945, p 69 # https://books.google.com/books?id=9as-AQAAIAAJ&pg=PA69 # This Ordinance abolished DST. # # Time Determination (Amendment) Ordinance, 1950 (No. 26 of 1950) [1950-07-22] # Annual Volume of the Laws of the Gold Coast: # Containing All Legislation Enacted During Year 1950, p 35 # https://books.google.com/books?id=e60-AQAAIAAJ&pg=PA35 # This Ordinance restored DST but with thirty minutes offset. # # Time Determination Ordinance (Cap. 264) # The Laws of the Gold Coast, Vol. V (1954), p 380 # https://books.google.com/books?id=Mqc-AQAAIAAJ&pg=PA380 # Revised edition of the Time Determination Ordinance. # # Time Determination (Amendment) Ordinance, 1956 (No. 21 of 1956) [1956-08-29] # Annual Volume of the Ordinances of the Gold Coast Enacted During the # Year 1956, p 83 # https://books.google.com/books?id=VLE-AQAAIAAJ&pg=PA83 # This Ordinance abolished DST. Rule Ghana 1919 only - Nov 24 0:00 0:20 +0020 Rule Ghana 1920 1942 - Jan 1 2:00 0 GMT Rule Ghana 1920 1939 - Sep 1 2:00 0:20 +0020 Rule Ghana 1940 1941 - May 1 2:00 0:20 +0020 Rule Ghana 1950 1955 - Sep 1 2:00 0:30 +0030 Rule Ghana 1951 1956 - Jan 1 2:00 0 GMT Zone Africa/Accra -0:00:52 - LMT 1915 Nov 2 0:00 Ghana %s 1942 Feb 8 0:30 - +0030 1946 Jan 6 0:00 Ghana %s # Ethiopia # From Paul Eggert (2014-07-31): # Like the Swahili of Kenya and Tanzania, many Ethiopians keep a # 12-hour clock starting at our 06:00, so their "8 o'clock" is our # 02:00 or 14:00. Keep this in mind when you ask the time in Amharic. # # Shanks & Pottenger write that Ethiopia had six narrowly spaced time # zones between 1870 and 1890, that they merged to 38E50 (2:35:20) in # 1890, and that they switched to 3:00 on 1936-05-05. Perhaps 38E50 # was for Adis Dera. Quite likely the Shanks data entries are wrong # anyway. Zone Africa/Addis_Ababa 2:34:48 - LMT 1870 2:35:20 - ADMT 1936 May 5 # Adis Dera MT 3:00 - EAT # Eritrea Zone Africa/Asmara 2:35:32 - LMT 1870 2:35:32 - AMT 1890 # Asmara Mean Time 2:35:20 - ADMT 1936 May 5 # Adis Dera MT 3:00 - EAT Link Africa/Asmara Africa/Asmera # Mali (southern) Zone Africa/Bamako -0:32:00 - LMT 1912 0:00 - GMT 1934 Feb 26 -1:00 - -01 1960 Jun 20 0:00 - GMT #PACKRATLIST zone.tab Link Africa/Bamako Africa/Timbuktu # Central African Republic Zone Africa/Bangui 1:14:20 - LMT 1912 1:00 - WAT # The Gambia # From P Chan (2020-12-09): # Standard time of GMT-1 was adopted on 1933-04-01. On 1942-02-01, GMT was # adopted as a war time measure. This was made permanent in 1946. # # Interpretation Ordinance, 1914 (No. 12 of 1914) [1914-09-29] # Interpretation Ordinance, 1933 (No. 10 of 1933) [1933-03-31] # Notice No. 5 of 1942, Colony of the Gambia Government Gazette, Vol. LIX, # No.2, 1942-01-15, p 2 # Interpretation (Amendment) Ordinance, 1946 (No. 3 of 1946) [1946-07-15] Zone Africa/Banjul -1:06:36 - LMT 1912 -1:06:36 - BMT 1933 Apr 1 # Banjul Mean Time -1:00 - -01 1942 Feb 1 0:00 0:00 - GMT # Malawi # From P Chan (2020-12-09): # In 1911, Zomba mean time was adopted as the legal time of Nyasaland. In # 1914, Zomba mean time switched from GMT+2:21:10 to GMT+2:21. On 1925-07-01, # GMT+2 was adopted. # # Interpretation and General Clauses Ordinance, 1911 (No. 12 of 1911) # [1911-07-24] # Notice No. 124 of 1914, 1914-06-30, The Nyasaland Government Gazette, Vol. # XXI, No. 8, 1914-06-30, p 122 # Interpretation and General Clauses (Amendment) Ordinance, 1925 (No. 3 of # 1925) [1925-04-02] Zone Africa/Blantyre 2:20:00 - LMT 1911 Jul 24 2:21:10 - ZMT 1914 Jun 30 # Zomba Mean Time 2:21 - ZMT 1925 Jul 1 2:00 - CAT # Republic of the Congo Zone Africa/Brazzaville 1:01:08 - LMT 1912 1:00 - WAT # Burundi Zone Africa/Bujumbura 1:57:28 - LMT 1890 2:00 - CAT # Guinea Zone Africa/Conakry -0:54:52 - LMT 1912 0:00 - GMT 1934 Feb 26 -1:00 - -01 1960 0:00 - GMT # Senegal Zone Africa/Dakar -1:09:44 - LMT 1912 -1:00 - -01 1941 Jun 0:00 - GMT # Tanzania Zone Africa/Dar_es_Salaam 2:37:08 - LMT 1931 3:00 - EAT 1948 2:45 - +0245 1961 3:00 - EAT # Djibouti Zone Africa/Djibouti 2:52:36 - LMT 1911 Jul 3:00 - EAT # Cameroon # Whitman says they switched to 1:00 in 1920; go with Shanks & Pottenger. Zone Africa/Douala 0:38:48 - LMT 1912 1:00 - WAT # Sierra Leone # From P Chan (2020-12-09): # Standard time of GMT-1 was adopted on 1913-07-01. Twenty minutes of DST was # introduce[d] in 1932 and was suspended in 1939. In 1941, GMT was adopted by # Defence Regulations. This was made permanent in 1946. # # Government Notice No. 121 of 1913, 1913-06-06, Sierra Leone Royal Gazette, # Vol. XLIV, No. 1384, 1913-06-14, p 347 # Alteration of Time Ordinance, 1932 (No. 34 of 1932) [1932-12-01] # Alteration of Time (Amendment) Ordinance, 1938 (No. 25 of 1938) [1938-11-24] # Defence Regulations (No. 9), 1939 (Regulations No. 9 of 1939), 1939-09-05 # Defence Regulations (No. 11), 1939 (Regulations No. 11 of 1939), 1939-09-27 # Defence (Amendment) (No. 17) Regulations, 1941 (Public Notice No. 157 of # 1941), 1914-12-04 # Alteration of Time (Amendment) Ordinance, 1946 (No. 2 of 1946) [1946-02-07] # From Tim Parenti (2021-03-02), per P Chan (2021-02-25): # For Sierra Leone in 1957-1962, the standard time was defined in the # Alteration of Time Ordinance, 1932 (as amended in 1946, renamed to Local Time # Ordinance in 1960 and Local Time Act in 1961). It was unamended throughout # that period. See references to "Time" in the Alphabetical Index of the # Legislation in force on the 31st day of December, # 1957: https://books.google.com/books?id=lvQ-AQAAIAAJ&pg=RA2-PA49 # 1958: https://books.google.com/books?id=4fQ-AQAAIAAJ&pg=RA2-PA50 # 1959: https://books.google.com/books?id=p_U-AQAAIAAJ&pg=RA2-PA55 # 1960: https://books.google.com/books?id=JPY-AQAAIAAJ&pg=RA3-PA37 # 1961: https://books.google.com/books?id=7vY-AQAAIAAJ&pg=RA3-PA41 # 1962: https://books.google.com/books?id=W_c-AQAAIAAJ&pg=RA3-PA44 # 1963: https://books.google.com/books?id=9vk-AQAAIAAJ&pg=RA1-PA47 # # Although Shanks & Pottenger had DST from Jun 1 00:00 to Sep 1 00:00 in this # period, many contemporaneous almanacs agree that it wasn't used: # https://mm.icann.org/pipermail/tz/2021-February/029866.html # Go with the above. Rule SL 1932 only - Dec 1 0:00 0:20 -0040 Rule SL 1933 1938 - Mar 31 24:00 0 -01 Rule SL 1933 1939 - Aug 31 24:00 0:20 -0040 Rule SL 1939 only - May 31 24:00 0 -01 Zone Africa/Freetown -0:53:00 - LMT 1882 -0:53:00 - FMT 1913 Jul 1 # Freetown MT -1:00 SL %s 1939 Sep 5 -1:00 - -01 1941 Dec 6 24:00 0:00 - GMT # Botswana # From Paul Eggert (2013-02-21): # Milne says they were regulated by the Cape Town Signal in 1899; # assume they switched to 2:00 when Cape Town did. Zone Africa/Gaborone 1:43:40 - LMT 1885 1:30 - SAST 1903 Mar 2:00 - CAT 1943 Sep 19 2:00 2:00 1:00 CAST 1944 Mar 19 2:00 2:00 - CAT # Zimbabwe Zone Africa/Harare 2:04:12 - LMT 1903 Mar 2:00 - CAT # Uganda Zone Africa/Kampala 2:09:40 - LMT 1928 Jul 3:00 - EAT 1930 2:30 - +0230 1948 2:45 - +0245 1957 3:00 - EAT # Rwanda Zone Africa/Kigali 2:00:16 - LMT 1935 Jun 2:00 - CAT # Democratic Republic of the Congo (west) Zone Africa/Kinshasa 1:01:12 - LMT 1897 Nov 9 1:00 - WAT # Gabon Zone Africa/Libreville 0:37:48 - LMT 1912 1:00 - WAT # Togo Zone Africa/Lome 0:04:52 - LMT 1893 0:00 - GMT # Angola # # From Tim Parenti (2024-07-01), per Paul Eggert (2018-02-16): # For timestamps before independence, see commentary for Europe/Lisbon. # Zone Africa/Luanda 0:52:56 - LMT 1892 0:52:04 - LMT 1911 Dec 31 23:00u # Luanda MT? 1:00 - WAT # Democratic Republic of the Congo (east) # # From Alois Treindl (2022-02-28): # My main source for its time zone history is # Henri le Corre, Régimes horaires pour l'Europe et l'Afrique. # Shanks follows le Corre. As does Françoise Schneider-Gauquelin in her book # Problèmes de l'heure résolus pour le monde entier. # Zone Africa/Lubumbashi 1:49:52 - LMT 1897 Nov 9 1:00 - WAT 1920 Apr 25 2:00 - CAT # Zambia Zone Africa/Lusaka 1:53:08 - LMT 1903 Mar 2:00 - CAT # Equatorial Guinea # # Although Shanks says that Malabo switched from UT +00 to +01 on 1963-12-15, # a Google Books search says that London Calling, Issues 432-465 (1948), p 19, # says that Spanish Guinea was at +01 back then. The Shanks data entries # are most likely wrong, but we have nothing better; use them here for now. # Zone Africa/Malabo 0:35:08 - LMT 1912 0:00 - GMT 1963 Dec 15 1:00 - WAT # Lesotho Zone Africa/Maseru 1:50:00 - LMT 1903 Mar 2:00 - SAST 1943 Sep 19 2:00 2:00 1:00 SAST 1944 Mar 19 2:00 2:00 - SAST # Eswatini (formerly Swaziland) Zone Africa/Mbabane 2:04:24 - LMT 1903 Mar 2:00 - SAST # Somalia Zone Africa/Mogadishu 3:01:28 - LMT 1893 Nov 3:00 - EAT 1931 2:30 - +0230 1957 3:00 - EAT # Niger Zone Africa/Niamey 0:08:28 - LMT 1912 -1:00 - -01 1934 Feb 26 0:00 - GMT 1960 1:00 - WAT # Mauritania Zone Africa/Nouakchott -1:03:48 - LMT 1912 0:00 - GMT 1934 Feb 26 -1:00 - -01 1960 Nov 28 0:00 - GMT # Burkina Faso Zone Africa/Ouagadougou -0:06:04 - LMT 1912 0:00 - GMT # Benin # Whitman says they switched to 1:00 in 1946, not 1934; # go with Shanks & Pottenger. Zone Africa/Porto-Novo 0:10:28 - LMT 1912 Jan 1 0:00 - GMT 1934 Feb 26 1:00 - WAT # Mali (northern) Zone Africa/Timbuktu -0:12:04 - LMT 1912 0:00 - GMT # Anguilla Zone America/Anguilla -4:12:16 - LMT 1912 Mar 2 -4:00 - AST # Antigua and Barbuda Zone America/Antigua -4:07:12 - LMT 1912 Mar 2 -5:00 - EST 1951 -4:00 - AST # Chubut, Argentina # The name "Comodoro Rivadavia" exceeds the 14-byte POSIX limit. Zone America/Argentina/ComodRivadavia -4:30:00 - LMT 1894 Oct 31 -4:16:48 - CMT 1920 May -4:00 - -04 1930 Dec -4:00 Arg -04/-03 1969 Oct 5 -3:00 Arg -03/-02 1991 Mar 3 -4:00 - -04 1991 Oct 20 -3:00 Arg -03/-02 1999 Oct 3 -4:00 Arg -04/-03 2000 Mar 3 -3:00 - -03 2004 Jun 1 -4:00 - -04 2004 Jun 20 -3:00 - -03 # Aruba Zone America/Aruba -4:40:24 - LMT 1912 Feb 12 # Oranjestad -4:30 - -0430 1965 -4:00 - AST # Atikokan, Ontario # From Paul Eggert (1997-10-17): # Mark Brader writes that an article in the 1997-10-14 Toronto Star # says that Atikokan, Ontario currently does not observe DST, # but will vote on 11-10 whether to use EST/EDT. # He also writes that the Ontario Time Act (1990, Chapter T.9) # http://www.gov.on.ca/MBS/english/publications/statregs/conttext.html # says that Ontario east of 90W uses EST/EDT, and west of 90W uses CST/CDT. # Officially Atikokan is therefore on CST/CDT, and most likely this report # concerns a non-official time observed as a matter of local practice. # # From Paul Eggert (2000-10-02): # Matthews and Vincent (1998) write that Atikokan, Pickle Lake, and # New Osnaburgh observe [EST] all year, that Big Trout Lake observes # CST/CDT, and that Upsala and Shebandowan observe EST/EDT, all in # violation of the official Ontario rules. # # From Paul Eggert (2006-07-09): # Chris Walton (2006-07-06) mentioned an article by Stephanie MacLellan in the # 2005-07-21 Chronicle-Journal, which said: # # The clocks in Atikokan stay set on standard time year-round. # This means they spend about half the time on central time and # the other half on eastern time. # # For the most part, the system works, Mayor Dennis Brown said. # # "The majority of businesses in Atikokan deal more with Eastern # Canada, but there are some that deal with Western Canada," he # said. "I don't see any changes happening here." # # Walton also writes "Supposedly Pickle Lake and Mishkeegogamang # [New Osnaburgh] follow the same practice." # From Garry McKinnon (2006-07-14) via Chris Walton: # I chatted with a member of my board who has an outstanding memory # and a long history in Atikokan (and in the telecom industry) and he # can say for certain that Atikokan has been practicing the current # time keeping since 1952, at least. # From Paul Eggert (2006-07-17): # Shanks & Pottenger say that Atikokan has agreed with Rainy River # ever since standard time was introduced, but the information from # McKinnon sounds more authoritative. For now, assume that Atikokan # switched to EST immediately after WWII era daylight saving time # ended. This matches the old (less populous) America/Coral_Harbour # entry since our cutoff date of 1970, so we can move # America/Coral_Harbour to the 'backward' file. Zone America/Atikokan -6:06:28 - LMT 1895 -6:00 Canada C%sT 1940 Sep 29 -6:00 1:00 CDT 1942 Feb 9 2:00s -6:00 Canada C%sT 1945 Sep 30 2:00 -5:00 - EST #PACKRATLIST zone.tab Link America/Atikokan America/Coral_Harbour # Quebec east of Natashquan # From Paul Eggert (2021-05-09): # H. David Matthews and Mary Vincent's map # "It's about TIME", _Canadian Geographic_ (September-October 1998) # http://www.canadiangeographic.ca/Magazine/SO98/alacarte.asp # says that Quebec east of the -63 meridian is supposed to observe # AST, but residents as far east as Natashquan use EST/EDT, and # residents east of Natashquan use AST. # The Quebec department of justice writes in # "The situation in Minganie and Basse-Côte-Nord" # https://www.justice.gouv.qc.ca/en/department/ministre/functions-and-responsabilities/legal-time-in-quebec/the-situation-in-minganie-and-basse-cote-nord/ # that the coastal strip from just east of Natashquan to Blanc-Sablon # observes Atlantic standard time all year round. # This common practice was codified into law as of 2007; see Legal Time Act, # CQLR c T-5.1 . # For lack of better info, guess this practice began around 1970, contra to # Shanks & Pottenger who have this region observing AST/ADT. Zone America/Blanc-Sablon -3:48:28 - LMT 1884 -4:00 Canada A%sT 1970 -4:00 - AST # Cayman Is Zone America/Cayman -5:25:32 - LMT 1890 # Georgetown -5:07:10 - KMT 1912 Feb # Kingston Mean Time -5:00 - EST # United States # # From Paul Eggert (2018-03-18): # America/Chillicothe would be tricky, as it was a city of two-timers: # "To prevent a constant mixup at Chillicothe, caused by the courthouse # clock running on central time and the city running on 'daylight saving' # time, a third hand was added to the dial of the courthouse clock." # -- Ohio news in brief. The Cedarville Herald. 1920-05-21;43(21):1 (col. 5) # https://digitalcommons.cedarville.edu/cedarville_herald/794 # Canada Zone America/Coral_Harbour -5:32:40 - LMT 1884 -5:00 NT_YK E%sT 1946 -5:00 - EST # From Chris Walton (2011-12-01): # There are two areas within the Canadian province of British Columbia # that do not currently observe daylight saving: # a) The Creston Valley (includes the town of Creston and surrounding area) # b) The eastern half of the Peace River Regional District # (includes the cities of Dawson Creek and Fort St. John) # Earlier this year I stumbled across a detailed article about the time # keeping history of Creston; it was written by Tammy Hardwick who is the # manager of the Creston & District Museum. The article was written in May 2009. # http://www.ilovecreston.com/?p=articles&t=spec&ar=260 # According to the article, Creston has not changed its clocks since June 1918. # i.e. Creston has been stuck on UT-7 for 93 years. # Dawson Creek, on the other hand, changed its clocks as recently as April 1972. # Unfortunately the exact date for the time change in June 1918 remains # unknown and will be difficult to ascertain. I e-mailed Tammy a few months # ago to ask if Sunday June 2 was a reasonable guess. She said it was just # as plausible as any other date (in June). She also said that after writing # the article she had discovered another time change in 1916; this is the # subject of another article which she wrote in October 2010. # http://www.creston.museum.bc.ca/index.php?module=comments&uop=view_comment&cm+id=56 # Here is a summary of the three clock change events in Creston's history: # 1. 1884 or 1885: adoption of Mountain Standard Time (GMT-7) # Exact date unknown # 2. Oct 1916: switch to Pacific Standard Time (GMT-8) # Exact date in October unknown; Sunday October 1 is a reasonable guess. # 3. June 1918: switch to Pacific Daylight Time (GMT-7) # Exact date in June unknown; Sunday June 2 is a reasonable guess. # note 1: # On Oct 27/1918 when daylight saving ended in the rest of Canada, # Creston did not change its clocks. # note 2: # During WWII when the Federal Government legislated a mandatory clock change, # Creston did not oblige. # note 3: # There is no guarantee that Creston will remain on Mountain Standard Time # (UTC-7) forever. # The subject was debated at least once this year by the town Council. # http://www.bclocalnews.com/kootenay_rockies/crestonvalleyadvance/news/116760809.html # During a period WWII, summer time (Daylight saying) was mandatory in Canada. # In Creston, that was handled by shifting the area to PST (-8:00) then applying # summer time to cause the offset to be -7:00, the same as it had been before # the change. It can be argued that the timezone abbreviation during this # period should be PDT rather than MST, but that doesn't seem important enough # (to anyone) to further complicate the rules. # The transition dates (and times) are guesses. Zone America/Creston -7:46:04 - LMT 1884 -7:00 - MST 1916 Oct 1 -8:00 - PST 1918 Jun 2 -7:00 - MST # Curaçao # Milne gives 4:35:46.9 for Curaçao mean time; round to nearest. # # From Paul Eggert (2006-03-22): # Shanks & Pottenger say that The Bottom and Philipsburg have been at # -4:00 since standard time was introduced on 1912-03-02; and that # Kralendijk and Rincon used Kralendijk Mean Time (-4:33:08) from # 1912-02-02 to 1965-01-01. The former is dubious, since S&P also say # Saba Island has been like Curaçao. # This all predates our 1970 cutoff, though. # # By July 2007 Curaçao and St Maarten are planned to become # associated states within the Netherlands, much like Aruba; # Bonaire, Saba and St Eustatius would become directly part of the # Netherlands as Kingdom Islands. This won't affect their time zones # though, as far as we know. # Zone America/Curacao -4:35:47 - LMT 1912 Feb 12 # Willemstad -4:30 - -0430 1965 -4:00 - AST Link America/Curacao America/Kralendijk Link America/Curacao America/Lower_Princes # Dominica Zone America/Dominica -4:05:36 - LMT 1911 Jul 1 0:01 # Roseau -4:00 - AST # Baja California # See 'northamerica' for why this entry is here rather than there. Zone America/Ensenada -7:46:28 - LMT 1922 Jan 1 0:13:32 -8:00 - PST 1927 Jun 10 23:00 -7:00 - MST 1930 Nov 16 -8:00 - PST 1942 Apr -7:00 - MST 1949 Jan 14 -8:00 - PST 1996 -8:00 Mexico P%sT # Grenada Zone America/Grenada -4:07:00 - LMT 1911 Jul # St George's -4:00 - AST # Guadeloupe Zone America/Guadeloupe -4:06:08 - LMT 1911 Jun 8 # Pointe-à-Pitre -4:00 - AST # Canada # # From Paul Eggert (2015-03-24): # Since 1970 most of Quebec has been like Toronto; see # America/Toronto. However, earlier versions of the tz database # mistakenly relied on data from Shanks & Pottenger saying that Quebec # differed from Ontario after 1970, and the following rules and zone # were created for most of Quebec from the incorrect Shanks & # Pottenger data. The post-1970 entries have been corrected, but the # pre-1970 entries are unchecked and probably have errors. # Rule Mont 1917 only - Mar 25 2:00 1:00 D Rule Mont 1917 only - Apr 24 0:00 0 S Rule Mont 1919 only - Mar 31 2:30 1:00 D Rule Mont 1919 only - Oct 25 2:30 0 S Rule Mont 1920 only - May 2 2:30 1:00 D Rule Mont 1920 1922 - Oct Sun>=1 2:30 0 S Rule Mont 1921 only - May 1 2:00 1:00 D Rule Mont 1922 only - Apr 30 2:00 1:00 D Rule Mont 1924 only - May 17 2:00 1:00 D Rule Mont 1924 1926 - Sep lastSun 2:30 0 S Rule Mont 1925 1926 - May Sun>=1 2:00 1:00 D Rule Mont 1927 1937 - Apr lastSat 24:00 1:00 D Rule Mont 1927 1937 - Sep lastSat 24:00 0 S Rule Mont 1938 1940 - Apr lastSun 0:00 1:00 D Rule Mont 1938 1939 - Sep lastSun 0:00 0 S Rule Mont 1946 1973 - Apr lastSun 2:00 1:00 D Rule Mont 1945 1948 - Sep lastSun 2:00 0 S Rule Mont 1949 1950 - Oct lastSun 2:00 0 S Rule Mont 1951 1956 - Sep lastSun 2:00 0 S Rule Mont 1957 1973 - Oct lastSun 2:00 0 S Zone America/Montreal -4:54:16 - LMT 1884 -5:00 Mont E%sT 1918 -5:00 Canada E%sT 1919 -5:00 Mont E%sT 1942 Feb 9 2:00s -5:00 Canada E%sT 1946 -5:00 Mont E%sT 1974 -5:00 Canada E%sT # Montserrat # From Paul Eggert (2006-03-22): # In 1995 volcanic eruptions forced evacuation of Plymouth, the capital. # world.gazetteer.com says Cork Hill is the most populous location now. Zone America/Montserrat -4:08:52 - LMT 1911 Jul 1 0:01 # Cork Hill -4:00 - AST # The Bahamas # # For 1899 Milne gives -5:09:29.5; round that. # # From P Chan (2020-11-27, corrected on 2020-12-02): # There were two periods of DST observed in 1942-1945: 1942-05-01 # midnight to 1944-12-31 midnight and 1945-02-01 to 1945-10-17 midnight. # "midnight" should mean 24:00 from the context. # # War Time Order 1942 [1942-05-01] and War Time (No. 2) Order 1942 [1942-09-29] # Appendix to the Statutes of 7 George VI. and the Year 1942. p 34, 43 # https://books.google.com/books?id=5rlNAQAAIAAJ&pg=RA3-PA34 # https://books.google.com/books?id=5rlNAQAAIAAJ&pg=RA3-PA43 # # War Time Order 1943 [1943-03-31] and War Time Order 1944 [1943-12-29] # Appendix to the Statutes of 8 George VI. and the Year 1943. p 9-10, 28-29 # https://books.google.com/books?id=5rlNAQAAIAAJ&pg=RA4-PA9 # https://books.google.com/books?id=5rlNAQAAIAAJ&pg=RA4-PA28 # # War Time Order 1945 [1945-01-31] and the Order which revoke War Time Order # 1945 [1945-10-16] Appendix to the Statutes of 9 George VI. and the Year # 1945. p 160, 247-248 # https://books.google.com/books?id=5rlNAQAAIAAJ&pg=RA6-PA160 # https://books.google.com/books?id=5rlNAQAAIAAJ&pg=RA6-PA247 # # From Sue Williams (2006-12-07): # The Bahamas announced about a month ago that they plan to change their DST # rules to sync with the U.S. starting in 2007.... # http://www.jonesbahamas.com/?c=45&a=10412 Rule Bahamas 1942 only - May 1 24:00 1:00 W Rule Bahamas 1944 only - Dec 31 24:00 0 S Rule Bahamas 1945 only - Feb 1 0:00 1:00 W Rule Bahamas 1945 only - Aug 14 23:00u 1:00 P # Peace Rule Bahamas 1945 only - Oct 17 24:00 0 S Rule Bahamas 1964 1975 - Oct lastSun 2:00 0 S Rule Bahamas 1964 1975 - Apr lastSun 2:00 1:00 D Zone America/Nassau -5:09:30 - LMT 1912 Mar 2 -5:00 Bahamas E%sT 1976 -5:00 US E%sT # Canada # From Chris Walton (2022-10-15): # I would also like to see America/Nipigon and America/Rainy_River converted # into link entries because I have zero faith in the current Shanks based data. # From Paul Eggert (2022-10-15): # These are now links in the primary data. Also see America/Thunder_Bay. Zone America/Nipigon -5:53:04 - LMT 1895 -5:00 Canada E%sT 1940 Sep 29 -5:00 1:00 EDT 1942 Feb 9 2:00s -5:00 Canada E%sT # From Rives McDow (1999-11-08): # On October 31, when the rest of Nunavut went to Central time, # Pangnirtung wobbled. Here is the result of their wobble: # # The following businesses and organizations in Pangnirtung use Central Time: # # First Air, Power Corp, Nunavut Construction, Health Center, RCMP, # Eastern Arctic National Parks, A & D Specialist # # The following businesses and organizations in Pangnirtung use Eastern Time: # # Hamlet office, All other businesses, Both schools, Airport operator # # This has made for an interesting situation there, which warranted the news. # No one there that I spoke with seems concerned, or has plans to # change the local methods of keeping time, as it evidently does not # really interfere with any activities or make things difficult locally. # They plan to celebrate New Year's turn-over twice, one hour apart, # so it appears that the situation will last at least that long. # The Nunavut Intergovernmental Affairs hopes that they will "come to # their senses", but the locals evidently don't see any problem with # the current state of affairs. # From Michaela Rodrigue, writing in the # Nunatsiaq News (1999-11-19): # http://www.nunatsiaqonline.ca/archives/nunavut991130/nvt91119_17.html # Clyde River, Pangnirtung and Sanikiluaq now operate with two time zones, # central - or Nunavut time - for government offices, and eastern time # for municipal offices and schools.... Igloolik [was similar but then] # made the switch to central time on Saturday, Nov. 6. # From Chris Walton (2022-11-06): # The implication is that Pangnirtung and Qikiqtarjuaq have observed Eastern # Time as far back as 1984 (and likely even further back than that). # It is possible that these communities never actually observed Atlantic # Time, but that would be difficult to prove. # From Paul Eggert (2022-11-06): # This is in 'backzone' as we have no good evidence that Pangnirtung differs # from America/Iqaluit since 1970. A Google Books snippet view of # volume 2, page 186 of "Pilot of Arctic Canada", published 1959 by # the Canadian Hydrographic Service, suggests (though does not state) # that Pangnirtung observed EST then. # # aka Panniqtuuq Zone America/Pangnirtung 0 - -00 1921 # trading post est. -5:00 NT_YK E%sT 1999 Oct 31 2:00 -6:00 Canada C%sT 2000 Oct 29 2:00 -5:00 Canada E%sT # United States # # From Paul Eggert (2018-03-18): # America/Palm_Springs would be tricky, as it kept two sets of clocks # in 1946/7. See the following notes. # # From Steve Allen (2018-01-19): # The shadow of Mt. San Jacinto brings darkness very early in the winter # months. In 1946 the chamber of commerce decided to put the clocks of Palm # Springs forward by an hour in the winter. # https://www.desertsun.com/story/life/2017/12/27/palm-springs-struggle-daylight-savings-time-and-idea-sun-time/984416001/ # Desert Sun, Number 18, 1 November 1946 # https://cdnc.ucr.edu/cgi-bin/cdnc?a=d&d=DS19461101 # has proposal for meeting on front page and page 21. # Desert Sun, Number 19, 5 November 1946 # https://cdnc.ucr.edu/cgi-bin/cdnc?a=d&d=DS19461105 # reports that Sun Time won at the meeting on front page and page 5. # Desert Sun, Number 37, 7 January 1947 # https://cdnc.ucr.edu/cgi-bin/cdnc?a=d&d=DS19470107.2.12 # front page reports request to abandon Sun Time and page 7 notes a "class war". # Desert Sun, Number 38, 10 January 1947 # https://cdnc.ucr.edu/cgi-bin/cdnc?a=d&d=DS19470110 # front page reports on end. # Trinidad and Tobago Zone America/Port_of_Spain -4:06:04 - LMT 1912 Mar 2 -4:00 - AST Link America/Port_of_Spain America/Marigot Link America/Port_of_Spain America/St_Barthelemy # Canada # From Chris Walton (2022-10-15): # I would also like to see America/Nipigon and America/Rainy_River converted # into link entries because I have zero faith in the current Shanks based data. # From Paul Eggert (2022-10-15): # These are now links in the primary data. Also see America/Thunder_Bay. Zone America/Rainy_River -6:18:16 - LMT 1895 -6:00 Canada C%sT 1940 Sep 29 -6:00 1:00 CDT 1942 Feb 9 2:00s -6:00 Canada C%sT # Argentina # This entry was intended for the following areas, but has been superseded by # more detailed zones. # Santa Fe (SF), Entre Ríos (ER), Corrientes (CN), Misiones (MN), Chaco (CC), # Formosa (FM), La Pampa (LP), Chubut (CH) Zone America/Rosario -4:02:40 - LMT 1894 Nov -4:16:44 - CMT 1920 May -4:00 - -04 1930 Dec -4:00 Arg -04/-03 1969 Oct 5 -3:00 Arg -03/-02 1991 Jul -3:00 - -03 1999 Oct 3 0:00 -4:00 Arg -04/-03 2000 Mar 3 0:00 -3:00 - -03 # St Kitts-Nevis Zone America/St_Kitts -4:10:52 - LMT 1912 Mar 2 # Basseterre -4:00 - AST # St Lucia Zone America/St_Lucia -4:04:00 - LMT 1890 # Castries -4:04:00 - CMT 1912 # Castries Mean Time -4:00 - AST # US Virgin Is Zone America/St_Thomas -4:19:44 - LMT 1911 Jul # Charlotte Amalie -4:00 - AST Link America/St_Thomas America/Virgin # St Vincent and the Grenadines Zone America/St_Vincent -4:04:56 - LMT 1890 # Kingstown -4:04:56 - KMT 1912 # Kingstown Mean Time -4:00 - AST # Canada # # From Paul Eggert (2003-07-27): # Willett (1914-03) writes (p. 17) "In the Cities of Fort William, and # Port Arthur, Ontario, the principle of the Bill has been in # operation for the past three years, and in the City of Moose Jaw, # Saskatchewan, for one year." # # From David Bryan via Tory Tronrud, Director/Curator, # Thunder Bay Museum (2003-11-12): # There is some suggestion, however, that, by-law or not, daylight # savings time was being practiced in Fort William and Port Arthur # before 1909.... [I]n 1910, the line between the Eastern and Central # Time Zones was permanently moved about two hundred miles west to # include the Thunder Bay area.... When Canada adopted daylight # savings time in 1916, Fort William and Port Arthur, having done so # already, did not change their clocks.... During the Second World # War,... [t]he cities agreed to implement DST during the summer # months for the remainder of the war years. # # From Jeffery Nichols (2020-02-06): # According to the [Shanks] atlas, those western Ontario zones are huge, # covering most of Ontario northwest of Sault Ste Marie and Timmins. # The zones seem to include towns bigger than the ones they're named after, # like Dryden in America/Rainy_River and Wawa (and maybe Attawapiskat) in # America/Nipigon. I assume it's too much trouble to change the name of the # zone (like when you found out that America/Glace_Bay includes Sydney, Nova # Scotia).... # # From Chris Walton (2022-10-15): # The TZ database currently shows that Thunder Bay has observed daylight # saving every year from 1970 onwards with the exception of 1973. # Back in July I raised some doubts on this mailing list about the 1973 data. # I now have more proof that it is wrong. # [attached Chronicle-Journal front pages, 1973-04-28 and 1973-10-27] # # From Paul Eggert (2022-10-15): # This is now a link in the primary data. The following entry is # from Shanks & Pottenger, with corrections as noted above. # Zone America/Thunder_Bay -5:57:00 - LMT 1895 -6:00 - CST 1910 -5:00 - EST 1942 -5:00 Canada E%sT 1970 -5:00 Toronto E%sT 1974 -5:00 Canada E%sT # British Virgin Is Zone America/Tortola -4:18:28 - LMT 1911 Jul # Road Town -4:00 - AST # Yellowknife, Northwest Territories # Commented out as too wrong to be useful. #Zone America/Yellowknife 0 - -00 1935 # Yellowknife founded? # -7:00 NT_YK M%sT 1980 # -7:00 Canada M%sT # Dumont d'Urville, Île des Pétrels, -6640+14001, since 1956-11 # (2005-12-05) # # Another base at Port-Martin, 50km east, began operation in 1947. # It was destroyed by fire on 1952-01-14. # Zone Antarctica/DumontDUrville 0 - -00 1947 10:00 - +10 1952 Jan 14 0 - -00 1956 Nov 10:00 - +10 # McMurdo, Ross Island, since 1955-12 Zone Antarctica/McMurdo 0 - -00 1956 12:00 NZ NZ%sT Link Antarctica/McMurdo Antarctica/South_Pole # Syowa, Antarctica # # From Hideyuki Suzuki (1999-02-06): # In all Japanese stations, +0300 is used as the standard time. # # Syowa station, which is the first antarctic station of Japan, # was established on 1957-01-29. Since Syowa station is still the main # station of Japan, it's appropriate for the principal location. # See: # NIPR Antarctic Research Activities (1999-08-17) # http://www.nipr.ac.jp/english/ara01.html Zone Antarctica/Syowa 0 - -00 1957 Jan 29 3:00 - +03 # Yemen # Milne says 2:59:54 was the meridian of the saluting battery at Aden, # and that Yemen was at 1:55:56, the meridian of the Hagia Sophia. Zone Asia/Aden 2:59:54 - LMT 1950 3:00 - +03 # Bahrain # # From Paul Eggert (2020-07-23): # Most of this data comes from: # Stewart A. Why Gulf Standard Time is far from standard: the fascinating story # behind the time zone's invention. The National (Abu Dhabi). 2020-07-22. # https://www.thenational.ae/arts-culture/why-gulf-standard-time-is-far-from-standard-the-fascinating-story-behind-the-time-zone-s-invention-1.1052589 # Stewart writes that before 1941 some companies in Bahrain were at +0330 and # others at +0323. Reginald George Alban, a British political agent based in # Manama, worked to standardize this, and from 1941-07-20 Bahrain was at # +0330. However, BOAC asked that clocks be moved to gain more light at day's # end, so Bahrain switched to +04 on 1944-01-01. # # Re the 1941 transition, Stewart privately sent me this citation: # "File 16/53 Enquiries Re: Calculation of Local Time", British Library: India # Office Records and Private Papers, IOR/R/15/2/1564, in Qatar Digital Library # https://www.qdl.qa/archive/81055/vdc_100000000282.0x00012b # It says there was no real standard in Bahrain before 1941-07-20. # +0330 was used by steamers of the British India Co, by Petroleum Concessions # and by Cable & Wireless; +0323 was used by the Eastern Bank Ltd, BOAC, and # Bahrein Petroleum (Bapco), and California Arabian Standard Oil Co (Casoc) # adopted DST effective 1941-05-24. Alban suggested adopting DST, R.B. Coomb # of C&W countersuggested +0330, and although C.A. Rodstrom of Casoc (formerly # of Bapco) stated that Bahrain had formerly used +0330 before Bapco arrived # but Bapco switched to +0323 because of "constant confusion", the consensus # was +0330. The government adopted +0330 in 1941-07-20 and companies seem to # have switched by 08-01. No time of day was given for the 1940s transitions. Zone Asia/Bahrain 3:22:20 - LMT 1941 Jul 20 # Manamah 3:30 - +0330 1944 Jan 1 4:00 - +04 1972 Jun 3:00 - +03 # Brunei Zone Asia/Brunei 7:39:40 - LMT 1926 Mar # Bandar Seri Begawan 7:30 - +0730 1933 8:00 - +08 # India # # From Paul Eggert (2014-09-06): # The 1876 Report of the Secretary of the [US] Navy, p 305 says that Madras # civil time was 5:20:57.3. # # From Paul Eggert (2014-08-21): # In tomorrow's The Hindu, Nitya Menon reports that India had two civil time # zones starting in 1884, one in Bombay and one in Calcutta, and that railways # used a third time zone based on Madras time (80° 18' 30" E). Also, # in 1881 Bombay briefly switched to Madras time, but switched back. See: # http://www.thehindu.com/news/cities/chennai/madras-375-when-madras-clocked-the-time/article6339393.ece #Zone Asia/Chennai [not enough info to complete] # China # Long-shu Time (probably due to Long and Shu being two names of that area) # Guangxi, Guizhou, Hainan, Ningxia, Sichuan, Shaanxi, and Yunnan; # most of Gansu; west Inner Mongolia; west Qinghai; and the Guangdong # counties Deqing, Enping, Kaiping, Luoding, Taishan, Xinxing, # Yangchun, Yangjiang, Yu'nan, and Yunfu. Zone Asia/Chongqing 7:06:20 - LMT 1928 # or Chungking 7:00 - +07 1980 May 8:00 PRC C%sT Link Asia/Chongqing Asia/Chungking # Vietnam # From Paul Eggert (2014-10-13): # See Asia/Ho_Chi_Minh for the source for this data. # Trần's book says the 1954-55 transition to 07:00 in Hanoi was in # October 1954, with exact date and time unspecified. Zone Asia/Hanoi 7:03:24 - LMT 1906 Jul 1 7:06:30 - PLMT 1911 May 1 7:00 - +07 1942 Dec 31 23:00 8:00 - +08 1945 Mar 14 23:00 9:00 - +09 1945 Sep 2 7:00 - +07 1947 Apr 1 8:00 - +08 1954 Oct 7:00 - +07 # China # Changbai Time ("Long-white Time", Long-white = Heilongjiang area) # Heilongjiang (except Mohe county), Jilin Zone Asia/Harbin 8:26:44 - LMT 1928 # or Haerbin 8:30 - +0830 1932 Mar 8:00 - CST 1940 9:00 - +09 1966 May 8:30 - +0830 1980 May 8:00 PRC C%sT # far west China Zone Asia/Kashgar 5:03:56 - LMT 1928 # or Kashi or Kaxgar 5:30 - +0530 1940 5:00 - +05 1980 May 8:00 PRC C%sT # peninsular Malaysia # taken from Mok Ly Yng (2003-10-30) # https://web.archive.org/web/20190822231045/http://www.math.nus.edu.sg/~mathelmr/teaching/timezone.html # This agrees with Singapore since 1905-06-01. Zone Asia/Kuala_Lumpur 6:46:46 - LMT 1901 Jan 1 6:55:25 - SMT 1905 Jun 1 # Singapore M.T. 7:00 - +07 1933 Jan 1 7:00 0:20 +0720 1936 Jan 1 7:20 - +0720 1941 Sep 1 7:30 - +0730 1942 Feb 16 9:00 - +09 1945 Sep 12 7:30 - +0730 1981 Dec 31 16:00u 8:00 - +08 # Kuwait Zone Asia/Kuwait 3:11:56 - LMT 1950 3:00 - +03 # Oman # Milne says 3:54:24 was the meridian of the Muscat Tidal Observatory. Zone Asia/Muscat 3:54:24 - LMT 1920 4:00 - +04 # India # From Tim Parenti (2024-07-01), per Paul Eggert (2014-08-11), after a # heads-up from Stephen Colebourne: # According to a Portuguese decree (1911-05-24), Portuguese India switched to # UT +05 on 1912-01-01 (see Europe/Lisbon). #Zone Asia/Panaji [not enough info to complete] # Cambodia # From an adoptive daughter of the late Cambodian ruler Prince Sihanouk, # via Alois Treindl (2019-08-08): # # King Sihanouk said that, during the Japanese occupation, starting with # what historians refer to as "le coup de force du 9 mars 1945", Cambodia, # like the entire French Indochina, used Tokyo time zone. After Japan # surrendered, 2 September 1945, Cambodia fell under French rule again and # adopted Hanoi time zone again. # # However, on 7 January 1946, Sihanouk and Tioulong managed to obtain a # status of "internal autonomy" from the government of Charles de Gaulle. # Although many fields remained under the administration of the French # (customs, taxes, justice, defence, foreign affairs, etc.), the Cambodian # administration was responsible for religious matters and traditional # celebrations, which included our calendar and time. The time zone was GMT # + 7 and _no_ DST was applied. # # After Sihanouk and Tioulong achieved full independence, on 9 November 1953, # GMT + 7 was maintained. # From Paul Eggert (2019-08-26): # See Asia/Ho_Chi_Minh for the source for most of rest of this data. Zone Asia/Phnom_Penh 6:59:40 - LMT 1906 Jul 1 7:06:30 - PLMT 1911 May 1 7:00 - +07 1942 Dec 31 23:00 8:00 - +08 1945 Mar 14 23:00 9:00 - +09 1945 Sep 2 7:00 - +07 # Israel Zone Asia/Tel_Aviv 2:19:04 - LMT 1880 2:21 - JMT 1918 2:00 Zion I%sT # Laos # From Paul Eggert (2014-10-11): # See Asia/Ho_Chi_Minh for the source for most of this data. # Trần's book says that Laos reverted to UT +07 on 1955-04-15. # Also, guess that Laos reverted to +07 on 1945-09-02, when Vietnam did; # this is probably wrong but it's better than guessing no transition. Zone Asia/Vientiane 6:50:24 - LMT 1906 Jul 1 7:06:30 - PLMT 1911 May 1 7:00 - +07 1942 Dec 31 23:00 8:00 - +08 1945 Mar 14 23:00 9:00 - +09 1945 Sep 2 7:00 - +07 1947 Apr 1 8:00 - +08 1955 Apr 15 7:00 - +07 # Jan Mayen # From Whitman: Zone Atlantic/Jan_Mayen -1:00 - -01 # Iceland # # From Adam David (1993-11-06): # The name of the timezone in Iceland for system / mail / news purposes is GMT. # # (1993-12-05): # This material is paraphrased from the 1988 edition of the University of # Iceland Almanak. # # From January 1st, 1908 the whole of Iceland was standardised at 1 hour # behind GMT. Previously, local mean solar time was used in different parts # of Iceland, the almanak had been based on Reykjavík mean solar time which # was 1 hour and 28 minutes behind GMT. # # "first day of winter" referred to [below] means the first day of the 26 weeks # of winter, according to the old icelandic calendar that dates back to the # time the norsemen first settled Iceland. The first day of winter is always # Saturday, but is not dependent on the Julian or Gregorian calendars. # # (1993-12-10): # I have a reference from the Oxford Icelandic-English dictionary for the # beginning of winter, which ties it to the ecclesiastical calendar (and thus # to the julian/gregorian calendar) over the period in question. # the winter begins on the Saturday next before St. Luke's day # (old style), or on St. Luke's day, if a Saturday. # St. Luke's day ought to be traceable from ecclesiastical sources. "old style" # might be a reference to the Julian calendar as opposed to Gregorian, or it # might mean something else (???). # # From Paul Eggert (2014-11-22): # The information below is taken from the 1988 Almanak; see # http://www.almanak.hi.is/klukkan.html # Rule Iceland 1917 1919 - Feb 19 23:00 1:00 - Rule Iceland 1917 only - Oct 21 1:00 0 - Rule Iceland 1918 1919 - Nov 16 1:00 0 - Rule Iceland 1921 only - Mar 19 23:00 1:00 - Rule Iceland 1921 only - Jun 23 1:00 0 - Rule Iceland 1939 only - Apr 29 23:00 1:00 - Rule Iceland 1939 only - Oct 29 2:00 0 - Rule Iceland 1940 only - Feb 25 2:00 1:00 - Rule Iceland 1940 1941 - Nov Sun>=2 1:00s 0 - Rule Iceland 1941 1942 - Mar Sun>=2 1:00s 1:00 - # 1943-1946 - first Sunday in March until first Sunday in winter Rule Iceland 1943 1946 - Mar Sun>=1 1:00s 1:00 - Rule Iceland 1942 1948 - Oct Sun>=22 1:00s 0 - # 1947-1967 - first Sunday in April until first Sunday in winter Rule Iceland 1947 1967 - Apr Sun>=1 1:00s 1:00 - # 1949 and 1967 Oct transitions delayed by 1 week Rule Iceland 1949 only - Oct 30 1:00s 0 - Rule Iceland 1950 1966 - Oct Sun>=22 1:00s 0 - Rule Iceland 1967 only - Oct 29 1:00s 0 - Zone Atlantic/Reykjavik -1:28 - LMT 1908 -1:00 Iceland -01/+00 1968 Apr 7 1:00s 0:00 - GMT Link Atlantic/Reykjavik Iceland # St Helena Zone Atlantic/St_Helena -0:22:48 - LMT 1890 # Jamestown -0:22:48 - JMT 1951 # Jamestown Mean Time 0:00 - GMT # King Island Zone Australia/Currie 9:35:28 - LMT 1895 Sep 10:00 AT AE%sT 1919 Oct 24 10:00 Aus AE%sT 1968 Oct 15 10:00 AT AE%sT # Netherlands # Howse writes that the Netherlands' railways used GMT between 1892 and 1940, # but for other purposes the Netherlands used Amsterdam mean time. # However, Robert H. van Gent writes (2001-04-01): # Howse's statement is only correct up to 1909. From 1909-05-01 (00:00:00 # Amsterdam mean time) onwards, the whole of the Netherlands (including # the Dutch railways) was required by law to observe Amsterdam mean time # (19 minutes 32.13 seconds ahead of GMT). This had already been the # common practice (except for the railways) for many decades but it was # not until 1909 when the Dutch government finally defined this by law. # On 1937-07-01 this was changed to 20 minutes (exactly) ahead of GMT and # was generally known as Dutch Time ("Nederlandse Tijd"). # # (2001-04-08): # 1892-05-01 was the date when the Dutch railways were by law required to # observe GMT while the remainder of the Netherlands adhered to the common # practice of following Amsterdam mean time. # # (2001-04-09): # In 1835 the authorities of the province of North Holland requested the # municipal authorities of the towns and cities in the province to observe # Amsterdam mean time but I do not know in how many cases this request was # actually followed. # # From 1852 onwards the Dutch telegraph offices were by law required to # observe Amsterdam mean time. As the time signals from the observatory of # Leiden were also distributed by the telegraph system, I assume that most # places linked up with the telegraph (and railway) system automatically # adopted Amsterdam mean time. # # Although the early Dutch railway companies initially observed a variety # of times, most of them had adopted Amsterdam mean time by 1858 but it # was not until 1866 when they were all required by law to observe # Amsterdam mean time. # The data entries before 1945 are taken from # https://www.staff.science.uu.nl/~gent0113/wettijd/wettijd.htm # From Paul Eggert (2021-05-09): # I invented the abbreviations AMT for Amsterdam Mean Time and NST for # Netherlands Summer Time, used in the Netherlands from 1835 to 1937. Rule Neth 1916 only - May 1 0:00 1:00 NST # Netherlands Summer Time Rule Neth 1916 only - Oct 1 0:00 0 AMT # Amsterdam Mean Time Rule Neth 1917 only - Apr 16 2:00s 1:00 NST Rule Neth 1917 only - Sep 17 2:00s 0 AMT Rule Neth 1918 1921 - Apr Mon>=1 2:00s 1:00 NST Rule Neth 1918 1921 - Sep lastMon 2:00s 0 AMT Rule Neth 1922 only - Mar lastSun 2:00s 1:00 NST Rule Neth 1922 1936 - Oct Sun>=2 2:00s 0 AMT Rule Neth 1923 only - Jun Fri>=1 2:00s 1:00 NST Rule Neth 1924 only - Mar lastSun 2:00s 1:00 NST Rule Neth 1925 only - Jun Fri>=1 2:00s 1:00 NST # From 1926 through 1939 DST began 05-15, except that it was delayed by a week # in years when 05-15 fell in the Pentecost weekend. Rule Neth 1926 1931 - May 15 2:00s 1:00 NST Rule Neth 1932 only - May 22 2:00s 1:00 NST Rule Neth 1933 1936 - May 15 2:00s 1:00 NST Rule Neth 1937 only - May 22 2:00s 1:00 NST Rule Neth 1937 only - Jul 1 0:00 1:00 S Rule Neth 1937 1939 - Oct Sun>=2 2:00s 0 - Rule Neth 1938 1939 - May 15 2:00s 1:00 S Rule Neth 1945 only - Apr 2 2:00s 1:00 S Rule Neth 1945 only - Sep 16 2:00s 0 - #STDOFF 0:19:32.13 Zone Europe/Amsterdam 0:19:32 - LMT 1835 0:19:32 Neth %s 1937 Jul 1 0:20 Neth +0020/+0120 1940 May 16 0:00 1:00 C-Eur CE%sT 1945 Apr 2 2:00 1:00 Neth CE%sT 1977 1:00 EU CE%sT # Northern Ireland Zone Europe/Belfast -0:23:40 - LMT 1880 Aug 2 -0:25:21 - DMT 1916 May 21 2:00 # DMT = Dublin/Dunsink MT -0:25:21 1:00 IST 1916 Oct 1 2:00s # IST = Irish Summer Time 0:00 GB-Eire %s 1968 Oct 27 1:00 - BST 1971 Oct 31 2:00u 0:00 GB-Eire %s 1996 0:00 EU GMT/BST # Denmark # From Jesper Nørgaard Welen (2005-04-26): # the law [introducing standard time] was in effect from 1894-01-01.... # The page https://www.retsinformation.dk/eli/lta/1893/83 # confirms this, and states that the law was put forth 1893-03-29. # # The EU [actually, EEC and Euratom] treaty with effect from 1973: # https://www.retsinformation.dk/eli/lta/1972/21100 # # This provoked a new law from 1974 to make possible summer time changes # in subsequent decrees with the law # https://www.retsinformation.dk/eli/lta/1974/223 # # It seems however that no decree was set forward until 1980. I have # not found any decree, but in another related law, the effecting DST # changes are stated explicitly to be from 1980-04-06 at 02:00 to # 1980-09-28 at 02:00. If this is true, this differs slightly from # the EU rule in that DST runs to 02:00, not 03:00. We don't know # when Denmark began using the EU rule correctly, but we have only # confirmation of the 1980-time, so I presume it was correct in 1981: # The law is about the management of the extra hour, concerning # working hours reported and effect on obligatory-rest rules (which # was suspended on that night): # https://web.archive.org/web/20140104053304/https://www.retsinformation.dk/Forms/R0710.aspx?id=60267 # From Jesper Nørgaard Welen (2005-06-11): # The Herning Folkeblad (1980-09-26) reported that the night between # Saturday and Sunday the clock is set back from three to two. # From Paul Eggert (2005-06-11): # Hence the "02:00" of the 1980 law refers to standard time, not # wall-clock time, and so the EU rules were in effect in 1980. Rule Denmark 1916 only - May 14 23:00 1:00 S Rule Denmark 1916 only - Sep 30 23:00 0 - Rule Denmark 1940 only - May 15 0:00 1:00 S Rule Denmark 1945 only - Apr 2 2:00s 1:00 S Rule Denmark 1945 only - Aug 15 2:00s 0 - Rule Denmark 1946 only - May 1 2:00s 1:00 S Rule Denmark 1946 only - Sep 1 2:00s 0 - Rule Denmark 1947 only - May 4 2:00s 1:00 S Rule Denmark 1947 only - Aug 10 2:00s 0 - Rule Denmark 1948 only - May 9 2:00s 1:00 S Rule Denmark 1948 only - Aug 8 2:00s 0 - # Zone Europe/Copenhagen 0:50:20 - LMT 1890 0:50:20 - CMT 1894 Jan 1 # Copenhagen MT 1:00 Denmark CE%sT 1942 Nov 2 2:00s 1:00 C-Eur CE%sT 1945 Apr 2 2:00 1:00 Denmark CE%sT 1980 1:00 EU CE%sT # Guernsey # Data from Joseph S. Myers # https://mm.icann.org/pipermail/tz/2013-September/019883.html # References to be added # LMT is for Town Church, St. Peter Port, 49° 27' 17" N, 2° 32' 10" W. Zone Europe/Guernsey -0:10:09 - LMT 1913 Jun 18 0:00 GB-Eire %s 1940 Jul 2 1:00 C-Eur CE%sT 1945 May 8 0:00 GB-Eire %s 1968 Oct 27 1:00 - BST 1971 Oct 31 2:00u 0:00 GB-Eire %s 1996 0:00 EU GMT/BST # Isle of Man # # From Lester Caine (2013-09-04): # The Isle of Man legislation is now on-line at # , starting with the original Statutory # Time Act in 1883 and including additional confirmation of some of # the dates of the 'Summer Time' orders originating at # Westminster. There is a little uncertainty as to the starting date # of the first summer time in 1916 which may have been announced a # couple of days late. There is still a substantial number of # documents to work through, but it is thought that every GB change # was also implemented on the island. # # AT4 of 1883 - The Statutory Time et cetera Act 1883 - # LMT Location - 54.1508N -4.4814E - Tynwald Hill ( Manx parliament ) Zone Europe/Isle_of_Man -0:17:55 - LMT 1883 Mar 30 0:00s 0:00 GB-Eire %s 1968 Oct 27 1:00 - BST 1971 Oct 31 2:00u 0:00 GB-Eire %s 1996 0:00 EU GMT/BST # Jersey # Data from Joseph S. Myers # https://mm.icann.org/pipermail/tz/2013-September/019883.html # References to be added # LMT is for Parish Church, St. Helier, 49° 11' 0.57" N, 2° 6' 24.33" W. Zone Europe/Jersey -0:08:26 - LMT 1898 Jun 11 16:00u 0:00 GB-Eire %s 1940 Jul 2 1:00 C-Eur CE%sT 1945 May 8 0:00 GB-Eire %s 1968 Oct 27 1:00 - BST 1971 Oct 31 2:00u 0:00 GB-Eire %s 1996 0:00 EU GMT/BST # Slovenia Zone Europe/Ljubljana 0:58:04 - LMT 1884 1:00 - CET 1941 Apr 18 23:00 1:00 C-Eur CE%sT 1945 May 8 2:00s 1:00 1:00 CEST 1945 Sep 16 2:00s 1:00 - CET 1982 Nov 27 1:00 EU CE%sT # Luxembourg # Whitman disagrees with most of these dates in minor ways; # go with Shanks & Pottenger. Rule Lux 1916 only - May 14 23:00 1:00 S Rule Lux 1916 only - Oct 1 1:00 0 - Rule Lux 1917 only - Apr 28 23:00 1:00 S Rule Lux 1917 only - Sep 17 1:00 0 - Rule Lux 1918 only - Apr Mon>=15 2:00s 1:00 S Rule Lux 1918 only - Sep Mon>=15 2:00s 0 - Rule Lux 1919 only - Mar 1 23:00 1:00 S Rule Lux 1919 only - Oct 5 3:00 0 - Rule Lux 1920 only - Feb 14 23:00 1:00 S Rule Lux 1920 only - Oct 24 2:00 0 - Rule Lux 1921 only - Mar 14 23:00 1:00 S Rule Lux 1921 only - Oct 26 2:00 0 - Rule Lux 1922 only - Mar 25 23:00 1:00 S Rule Lux 1922 only - Oct Sun>=2 1:00 0 - Rule Lux 1923 only - Apr 21 23:00 1:00 S Rule Lux 1923 only - Oct Sun>=2 2:00 0 - Rule Lux 1924 only - Mar 29 23:00 1:00 S Rule Lux 1924 1928 - Oct Sun>=2 1:00 0 - Rule Lux 1925 only - Apr 5 23:00 1:00 S Rule Lux 1926 only - Apr 17 23:00 1:00 S Rule Lux 1927 only - Apr 9 23:00 1:00 S Rule Lux 1928 only - Apr 14 23:00 1:00 S Rule Lux 1929 only - Apr 20 23:00 1:00 S Zone Europe/Luxembourg 0:24:36 - LMT 1904 Jun 1:00 Lux CE%sT 1918 Nov 25 0:00 Lux WE%sT 1929 Oct 6 2:00s 0:00 Belgium WE%sT 1940 May 14 3:00 1:00 C-Eur WE%sT 1944 Sep 18 3:00 1:00 Belgium CE%sT 1977 1:00 EU CE%sT # Monaco # # From Michael Deckers (2020-06-12): # In the "Journal de Monaco" of 1892-05-24, online at # https://journaldemonaco.gouv.mc/var/jdm/storage/original/application/b1c67c12c5af11b41ea888fb048e4fe8.pdf # we read: ... # [In virtue of a Sovereign Ordinance of the May 13 of the current [year], # legal time in the Principality will be set to, from the date of June 1, # 1892 onwards, to the meridian of Paris, as in France.] # In the "Journal de Monaco" of 1911-03-28, online at # https://journaldemonaco.gouv.mc/var/jdm/storage/original/application/de74ffb7db53d4f599059fe8f0ed482a.pdf # we read an ordinance of 1911-03-16: ... # [Legal time in the Principality will be set, from the date of promulgation # of the present ordinance, to legal time in France.... Consequently, legal # time will be retarded by 9 minutes and 21 seconds.] # Zone Europe/Monaco 0:29:32 - LMT 1892 Jun 1 0:09:21 - PMT 1911 Mar 29 # Paris Mean Time 0:00 France WE%sT 1945 Sep 16 3:00 1:00 France CE%sT 1977 1:00 EU CE%sT # Norway # http://met.no/met/met_lex/q_u/sommertid.html (2004-01) agrees with Shanks & # Pottenger. Rule Norway 1916 only - May 22 1:00 1:00 S Rule Norway 1916 only - Sep 30 0:00 0 - Rule Norway 1945 only - Apr 2 2:00s 1:00 S Rule Norway 1945 only - Oct 1 2:00s 0 - Rule Norway 1959 1964 - Mar Sun>=15 2:00s 1:00 S Rule Norway 1959 1965 - Sep Sun>=15 2:00s 0 - Rule Norway 1965 only - Apr 25 2:00s 1:00 S Zone Europe/Oslo 0:43:00 - LMT 1895 Jan 1 1:00 Norway CE%sT 1940 Aug 10 23:00 1:00 C-Eur CE%sT 1945 Apr 2 2:00 1:00 Norway CE%sT 1980 1:00 EU CE%sT Link Europe/Oslo Arctic/Longyearbyen #PACKRATLIST zone.tab Link Europe/Oslo Atlantic/Jan_Mayen # Bosnia and Herzegovina Zone Europe/Sarajevo 1:13:40 - LMT 1884 1:00 - CET 1941 Apr 18 23:00 1:00 C-Eur CE%sT 1945 May 8 2:00s 1:00 1:00 CEST 1945 Sep 16 2:00s 1:00 - CET 1982 Nov 27 1:00 EU CE%sT # North Macedonia Zone Europe/Skopje 1:25:44 - LMT 1884 1:00 - CET 1941 Apr 18 23:00 1:00 C-Eur CE%sT 1945 May 8 2:00s 1:00 1:00 CEST 1945 Sep 16 2:00s 1:00 - CET 1982 Nov 27 1:00 EU CE%sT # Sweden # From Ivan Nilsson (2001-04-13), superseding Shanks & Pottenger: # # The law "Svensk författningssamling 1878, no 14" about standard time in 1879: # From the beginning of 1879 (that is 01-01 00:00) the time for all # places in the country is "the mean solar time for the meridian at # three degrees, or twelve minutes of time, to the west of the # meridian of the Observatory of Stockholm". The law is dated 1878-05-31. # # The observatory at that time had the meridian 18° 03' 30" # eastern longitude = 01:12:14 in time. Less 12 minutes gives the # national standard time as 01:00:14 ahead of GMT.... # # About the beginning of CET in Sweden. The lawtext ("Svensk # författningssamling 1899, no 44") states, that "from the beginning # of 1900... ... the same as the mean solar time for the meridian at # the distance of one hour of time from the meridian of the English # observatory at Greenwich, or at 12 minutes 14 seconds to the west # from the meridian of the Observatory of Stockholm". The law is dated # 1899-06-16. In short: At 1900-01-01 00:00:00 the new standard time # in Sweden is 01:00:00 ahead of GMT. # # 1916: The lawtext ("Svensk författningssamling 1916, no 124") states # that "1916-05-15 is considered to begin one hour earlier". It is # pretty obvious that at 05-14 23:00 the clocks are set to 05-15 00:00.... # Further the law says, that "1916-09-30 is considered to end one hour later". # # The laws regulating [DST] are available on the site of the Swedish # Parliament beginning with 1985 - the laws regulating 1980/1984 are # not available on the site (to my knowledge they are only available # in Swedish): (type # "sommartid" without the quotes in the field "Fritext" and then click # the Sök-button). # # (2001-05-13): # # I have now found a newspaper stating that at 1916-10-01 01:00 # summertime the church-clocks etc were set back one hour to show # 1916-10-01 00:00 standard time. The article also reports that some # people thought the switch to standard time would take place already # at 1916-10-01 00:00 summer time, but they had to wait for another # hour before the event took place. # # Source: The newspaper "Dagens Nyheter", 1916-10-01, page 7 upper left. # An extra-special abbreviation style is SET for Swedish Time (svensk # normaltid) 1879-1899, 3° west of the Stockholm Observatory. Zone Europe/Stockholm 1:12:12 - LMT 1879 Jan 1 1:00:14 - SET 1900 Jan 1 # Swedish Time 1:00 - CET 1916 May 14 23:00 1:00 1:00 CEST 1916 Oct 1 1:00 1:00 - CET 1980 1:00 EU CE%sT # Moldova / Transnistria Zone Europe/Tiraspol 1:58:32 - LMT 1880 1:55 - CMT 1918 Feb 15 # Chisinau MT 1:44:24 - BMT 1931 Jul 24 # Bucharest MT 2:00 Romania EE%sT 1940 Aug 15 2:00 1:00 EEST 1941 Jul 17 1:00 C-Eur CE%sT 1944 Aug 24 3:00 Russia MSK/MSD 1991 Mar 31 2:00 2:00 Russia EE%sT 1992 Jan 19 2:00 3:00 Russia MSK/MSD # Liechtenstein # From Paul Eggert (2022-07-21): # Shanks & Pottenger say Vaduz is like Zurich starting June 1894. # From Alois Treindl (2019-07-04): # I was able to access the online archive of the Vaduz paper Vaterland ... # I could confirm from the paper that Liechtenstein did in fact follow # the same DST in 1941 and 1942 as Switzerland did. Zone Europe/Vaduz 0:38:04 - LMT 1894 Jun 1:00 Swiss CE%sT 1981 1:00 EU CE%sT # Croatia Zone Europe/Zagreb 1:03:52 - LMT 1884 1:00 - CET 1941 Apr 18 23:00 1:00 C-Eur CE%sT 1945 May 8 2:00s 1:00 1:00 CEST 1945 Sep 16 2:00s 1:00 - CET 1982 Nov 27 1:00 EU CE%sT # Madagascar Zone Indian/Antananarivo 3:10:04 - LMT 1911 Jul 3:00 - EAT 1954 Feb 27 23:00s 3:00 1:00 EAST 1954 May 29 23:00s 3:00 - EAT # Christmas Zone Indian/Christmas 7:02:52 - LMT 1895 Feb 7:00 - +07 # Cocos (Keeling) Is # These islands were ruled by the Ross family from about 1830 to 1978. # We don't know when standard time was introduced; for now, we guess 1900. Zone Indian/Cocos 6:27:40 - LMT 1900 6:30 - +0630 # Comoros Zone Indian/Comoro 2:53:04 - LMT 1911 Jul # Moroni, Gran Comoro 3:00 - EAT # Kerguelen Zone Indian/Kerguelen 0 - -00 1950 # Port-aux-Français 5:00 - +05 # Seychelles # # From P Chan (2020-11-27): # Standard Time was adopted on 1907-01-01. # # Standard Time Ordinance (Chapter 237) # The Laws of Seychelles in Force on the 31st December, 1971, Vol. 6, p 571 # https://books.google.com/books?id=efE-AQAAIAAJ&pg=PA571 # # From Tim Parenti (2020-12-05): # A footnote on https://books.google.com/books?id=DYdDAQAAMAAJ&pg=PA1689 # confirms that Ordinance No. 9 of 1906 "was brought into force on the 1st # January, 1907." Zone Indian/Mahe 3:41:48 - LMT 1907 Jan 1 # Victoria 4:00 - +04 # From Paul Eggert (2001-05-30): # Aldabra, Farquhar, and Desroches, originally dependencies of the # Seychelles, were transferred to the British Indian Ocean Territory # in 1965 and returned to Seychelles control in 1976. We don't know # whether this affected their time zone, so omit this for now. # Possibly the islands were uninhabited. # Mayotte Zone Indian/Mayotte 3:00:56 - LMT 1911 Jul # Mamoutzou 3:00 - EAT # Réunion Zone Indian/Reunion 3:41:52 - LMT 1911 Jun # Saint-Denis 4:00 - +04 # # Scattered Islands (Îles Éparses) administered from Réunion are as follows. # The following information about them is taken from # Îles Éparses (, 1997-07-22, # in French; no longer available as of 1999-08-17). # We have no info about their time zone histories. # # Bassas da India - uninhabited # Europa Island - inhabited from 1905 to 1910 by two families # Glorioso Is - inhabited until at least 1958 # Juan de Nova - uninhabited # Tromelin - inhabited until at least 1958 # Micronesia # Also see Pacific/Pohnpei and commentary for Micronesia in 'australasia'. # # From Paul Eggert (2018-11-18): # Alan Eugene Davis writes (1996-03-16), # "I am certain, having lived there for the past decade, that 'Truk' # (now properly known as Chuuk) ... is in the time zone GMT+10." # Shanks & Pottenger write that Truk switched from UT +10 to +11 # on 1978-10-01; ignore this for now. Zone Pacific/Chuuk -13:52:52 - LMT 1844 Dec 31 10:07:08 - LMT 1901 10:00 - +10 1914 Oct 9:00 - +09 1919 Feb 1 10:00 - +10 1941 Apr 1 9:00 - +09 1945 Aug 10:00 - +10 Link Pacific/Chuuk Pacific/Truk Link Pacific/Chuuk Pacific/Yap # Phoenix Islands, Kiribati # From Paul Eggert (2021-05-27): # Enderbury was inhabited 1860/1880s to mine guano, and 1938-03-06/1942-02-09 # for aviation (ostensibly commercial, but military uses foreseen). # The 19th-century dates are approximate. See Pacific/Kanton for # the currently inhabited representative for this timezone. Zone Pacific/Enderbury 0 - -00 1860 -11:24:20 - LMT 1885 0 - -00 1938 Mar 6 -12:00 - -12 1942 Feb 9 0 - -00 # Tuvalu Zone Pacific/Funafuti 11:56:52 - LMT 1901 12:00 - +12 # Johnston Zone Pacific/Johnston -10:00 - HST # Marshall Is Zone Pacific/Majuro 11:24:48 - LMT 1901 11:00 - +11 1914 Oct 9:00 - +09 1919 Feb 1 11:00 - +11 1937 10:00 - +10 1941 Apr 1 9:00 - +09 1944 Jan 30 11:00 - +11 1969 Oct 12:00 - +12 # Midway # # From Mark Brader (2005-01-23): # [] # reproduced a Pan American Airways timetable from 1936, for their weekly # "Orient Express" flights between San Francisco and Manila, and connecting # flights to Chicago and the US East Coast. As it uses some time zone # designations that I've never seen before:.... # Fri. 6:30A Lv. HONOLULU (Pearl Harbor), H.I. . H.L.T. Ar. 5:30P Sun. # " 3:00P Ar. MIDWAY ISLAND . . . . . . . . . M.L.T. Lv. 6:00A " # Zone Pacific/Midway -11:49:28 - LMT 1901 -11:00 - -11 1956 Jun 3 -11:00 1:00 -10 1956 Sep 2 -11:00 - SST # S=Samoa # Micronesia # Also see Pacific/Chuuk and commentary for Micronesia in 'australasia'. Zone Pacific/Pohnpei -13:27:08 - LMT 1844 Dec 31 # Kolonia 10:32:52 - LMT 1901 11:00 - +11 1914 Oct 9:00 - +09 1919 Feb 1 11:00 - +11 1937 10:00 - +10 1941 Apr 1 9:00 - +09 1945 Aug 11:00 - +11 Link Pacific/Pohnpei Pacific/Ponape # N Mariana Is # # From Paul Eggert (2022-08-16): # Although Shanks & Pottenger say Saipan used +09 and then switched # to Guam time in October 1969, this is surely wrong. # Saipan used Guam time in the late 1950s; see page 4 of the minutes on the # conference of the 12th Saipan Legislature and the Select Committee on # Saipan Mission, 5th Guam Legislature (1959-09-11): # http://www.nmhcouncil.org/nmhc_archives/U.S.%20Navy%20Civil%20Affairs%20Files%201944-1962/1959/1959%2009%2017%20letter,%20minutes%20of%20conference,%20Borja.pdf # For now, assume Saipan switched to Guam time after the Battle of Saipan. # Zone Pacific/Saipan -14:17:00 - LMT 1844 Dec 31 9:43:00 - LMT 1901 9:00 - +09 1944 Jul 9 10:00 Guam G%sT 2000 Dec 23 10:00 - ChST # Chamorro Standard Time # Wake # From Vernice Anderson, Personal Secretary to Philip Jessup, # US Ambassador At Large (oral history interview, 1971-02-02): # # Saturday, the 14th [of October, 1950] - ... The time was all the # more confusing at that point, because we had crossed the # International Date Line, thus getting two Sundays. Furthermore, we # discovered that Wake Island had two hours of daylight saving time # making calculation of time in Washington difficult if not almost # impossible. # # https://www.trumanlibrary.org/oralhist/andrsonv.htm # From Paul Eggert (2003-03-23): # We have no other report of DST in Wake Island, so omit this info for now. # Also see commentary for Micronesia in 'australasia'. Zone Pacific/Wake 11:06:28 - LMT 1901 12:00 - +12 # Wallis and Futuna Zone Pacific/Wallis 12:15:20 - LMT 1901 12:00 - +12 # From Paul Eggert (2024-05-22): # The following zones pretend that standard time extends backward # indefinitely into the past, and so are ahistorical. # In current TZDB these entries are links to geographical locations # that agree with the ahistorical zones since 1970. # These are in numeric rather than alphabetic order. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone HST -10:00 - HST Zone MST -7:00 - MST Zone EST -5:00 - EST Zone WET 0:00 EU WE%sT Zone CET 1:00 C-Eur CE%sT Zone MET 1:00 C-Eur ME%sT Zone EET 2:00 EU EE%sT # Local Variables: # coding: utf-8 # End: pr0m1th3as-datatypes-9450b29/inst/tzdata/calendars000066400000000000000000000112341527370255000220220ustar00rootroot00000000000000----- Calendrical issues ----- As mentioned in Theory.html, although calendrical issues are out of scope for tzdb, they indicate the sort of problems that we would run into if we extended tzdb further into the past. The following information and sources go beyond Theory.html's brief discussion. They sometimes disagree. France Gregorian calendar adopted 1582-12-20. French Revolutionary calendar used 1793-11-24 through 1805-12-31, and (in Paris only) 1871-05-06 through 1871-05-23. Russia Soviet Russia adopted the Gregorian calendar on 1918-02-14. It also used 5- and 6-day work weeks at times, in parallel with the Gregorian calendar; see . Sweden (and Finland) From: Mark Brader Subject: Re: Gregorian reform - a part of locale? Date: 1996-07-06 In 1700, Denmark made the transition from Julian to Gregorian. Sweden decided to *start* a transition in 1700 as well, but rather than have one of those unsightly calendar gaps :-), they simply decreed that the next leap year after 1696 would be in 1744 - putting the whole country on a calendar different from both Julian and Gregorian for a period of 40 years. However, in 1704 something went wrong and the plan was not carried through; they did, after all, have a leap year that year. And one in 1708. In 1712 they gave it up and went back to Julian, putting 30 days in February that year!... Then in 1753, Sweden made the transition to Gregorian in the usual manner, getting there only 13 years behind the original schedule. (A previous posting of this story was challenged, and Swedish readers produced the following references to support it: "Tideräkning och historia" by Natanael Beckman (1924) and "Tid, en bok om tideräkning och kalenderväsen" by Lars-Olof Lodén (1968). Grotefend's data From: "Michael Palmer" [with two obvious typos fixed] Subject: Re: Gregorian Calendar (was Re: Another FHC related question Newsgroups: soc.genealogy.german Date: Tue, 9 Feb 1999 02:32:48 -800 ... The following is a(n incomplete) listing, arranged chronologically, of European states, with the date they converted from the Julian to the Gregorian calendar: 04/15 Oct 1582 - Italy (with exceptions), Spain, Portugal, Poland (Roman Catholics and Danzig only) 09/20 Dec 1582 - France, Lorraine 21 Dec 1582/ 01 Jan 1583 - Holland, Brabant, Flanders, Hennegau 10/21 Feb 1583 - bishopric of Liege (Lüttich) 13/24 Feb 1583 - bishopric of Augsburg 04/15 Oct 1583 - electorate of Trier 05/16 Oct 1583 - Bavaria, bishoprics of Freising, Eichstedt, Regensburg, Salzburg, Brixen 13/24 Oct 1583 - Austrian Oberelsaß and Breisgau 20/31 Oct 1583 - bishopric of Basel 02/13 Nov 1583 - duchy of Jülich-Berg 02/13 Nov 1583 - electorate and city of Köln 04/15 Nov 1583 - bishopric of Würzburg 11/22 Nov 1583 - electorate of Mainz 16/27 Nov 1583 - bishopric of Strassburg and the margraviate of Baden 17/28 Nov 1583 - bishopric of Münster and duchy of Cleve 14/25 Dec 1583 - Steiermark 06/17 Jan 1584 - Austria and Bohemia 11/22 Jan 1584 - Lucerne, Uri, Schwyz, Zug, Freiburg, Solothurn 12/23 Jan 1584 - Silesia and the Lausitz 22 Jan/ 02 Feb 1584 - Hungary (legally on 21 Oct 1587) Jun 1584 - Unterwalden 01/12 Jul 1584 - duchy of Westfalen 16/27 Jun 1585 - bishopric of Paderborn 14/25 Dec 1590 - Transylvania 22 Aug/ 02 Sep 1612 - duchy of Prussia 13/24 Dec 1614 - Pfalz-Neuburg 1617 - duchy of Kurland (reverted to the Julian calendar in 1796) 1624 - bishopric of Osnabrück 1630 - bishopric of Minden 15/26 Mar 1631 - bishopric of Hildesheim 1655 - Kanton Wallis 05/16 Feb 1682 - city of Strassburg 18 Feb/ 01 Mar 1700 - Protestant Germany (including Swedish possessions in Germany), Denmark, Norway 30 Jun/ 12 Jul 1700 - Gelderland, Zutphen 10 Nov/ 12 Dec 1700 - Utrecht, Overijssel 31 Dec 1700/ 12 Jan 1701 - Friesland, Groningen, Zürich, Bern, Basel, Geneva, Thurgau, and Schaffhausen 1724 - Glarus, Appenzell, and the city of St. Gallen 01 Jan 1750 - Pisa and Florence 02/14 Sep 1752 - Great Britain 17 Feb/ 01 Mar 1753 - Sweden 1760-1812 - Graubünden The Russian empire (including Finland and the Baltic states) did not convert to the Gregorian calendar until the Soviet revolution of 1917. Source: H. Grotefend, _Taschenbuch der Zeitrechnung des deutschen Mittelalters und der Neuzeit_, herausgegeben von Dr. O. Grotefend (Hannover: Hahnsche Buchhandlung, 1941), pp. 26-28. ----- This file is in the public domain, so clarified as of 2009-05-17 by Arthur David Olson. ----- Local Variables: coding: utf-8 End: pr0m1th3as-datatypes-9450b29/inst/tzdata/etcetera000066400000000000000000000060641527370255000216670ustar00rootroot00000000000000# tzdb data for ships at sea and other miscellany # This file is in the public domain, so clarified as of # 2009-05-17 by Arthur David Olson. # These entries are for uses not otherwise covered by the tz database. # Their main practical use is for platforms like Android that lack # support for POSIX proleptic TZ strings. On such platforms these entries # can be useful if the timezone database is wrong or if a ship or # aircraft at sea is not in a timezone. # Starting with POSIX 1003.1-2001, the entries below are all # unnecessary as settings for the TZ environment variable. E.g., # instead of TZ='Etc/GMT+4' one can use the POSIX setting TZ='<-04>+4'. # # Do not use a POSIX TZ setting like TZ='GMT+4', which is four hours # behind GMT but uses the completely misleading abbreviation "GMT". # The following zone is used by tzcode functions like gmtime, # which load the "UTC" file to handle seconds properly. Zone Etc/UTC 0 - UTC # If leap second support is enabled, functions like gmtime # load the "GMT" file to handle leap seconds properly. # Vanguard section, which works with most .zi parsers. #Zone GMT 0 - GMT # Rearguard section, for TZUpdater 2.3.2 and earlier. Zone Etc/GMT 0 - GMT # The following link uses older naming conventions, # but it belongs here, not in the file 'backward', # as it is needed for tzcode releases through 2022a, # where functions like gmtime load "GMT" instead of the "Etc/UTC". # We want this to work even on installations that omit 'backward'. Link Etc/GMT GMT # End of rearguard section. # Be consistent with POSIX TZ settings in the Zone names, # even though this is the opposite of what many people expect. # POSIX has positive signs west of Greenwich, but many people expect # positive signs east of Greenwich. For example, TZ='Etc/GMT+4' uses # the abbreviation "-04" and corresponds to 4 hours behind UT # (i.e. west of Greenwich) even though many people would expect it to # mean 4 hours ahead of UT (i.e. east of Greenwich). # Earlier incarnations of this package were not POSIX-compliant, # and had lines such as # Zone GMT-12 -12 - GMT-1200 # We did not want things to change quietly if someone accustomed to the old # way does a # zic -l GMT-12 # so we moved the names into the Etc subdirectory. # Also, the time zone abbreviations are now compatible with %z. # There is no "Etc/Unknown" entry, as CLDR says that "Etc/Unknown" # corresponds to an unknown or invalid time zone, and things would get # confusing if Etc/Unknown were made valid here. Zone Etc/GMT-14 14 - %z Zone Etc/GMT-13 13 - %z Zone Etc/GMT-12 12 - %z Zone Etc/GMT-11 11 - %z Zone Etc/GMT-10 10 - %z Zone Etc/GMT-9 9 - %z Zone Etc/GMT-8 8 - %z Zone Etc/GMT-7 7 - %z Zone Etc/GMT-6 6 - %z Zone Etc/GMT-5 5 - %z Zone Etc/GMT-4 4 - %z Zone Etc/GMT-3 3 - %z Zone Etc/GMT-2 2 - %z Zone Etc/GMT-1 1 - %z Zone Etc/GMT+1 -1 - %z Zone Etc/GMT+2 -2 - %z Zone Etc/GMT+3 -3 - %z Zone Etc/GMT+4 -4 - %z Zone Etc/GMT+5 -5 - %z Zone Etc/GMT+6 -6 - %z Zone Etc/GMT+7 -7 - %z Zone Etc/GMT+8 -8 - %z Zone Etc/GMT+9 -9 - %z Zone Etc/GMT+10 -10 - %z Zone Etc/GMT+11 -11 - %z Zone Etc/GMT+12 -12 - %z pr0m1th3as-datatypes-9450b29/inst/tzdata/europe000066400000000000000000005600701527370255000213740ustar00rootroot00000000000000# tzdb data for Europe and environs # This file is in the public domain, so clarified as of # 2009-05-17 by Arthur David Olson. # This file is by no means authoritative; if you think you know better, # go ahead and edit the file, and please send any changes to # the public mailing list tz@iana.org for general use in the future. # For more, please see the file CONTRIBUTING in the tz distribution. # From Paul Eggert (2017-02-10): # # Unless otherwise specified, the source for data through 1990 is: # Thomas G. Shanks and Rique Pottenger, The International Atlas (6th edition), # San Diego: ACS Publications, Inc. (2003). # Unfortunately this book contains many errors and cites no sources. # # Many years ago Gwillim Law wrote that a good source # for time zone data was the International Air Transport # Association's Standard Schedules Information Manual (IATA SSIM), # published semiannually. Law sent in several helpful summaries # of the IATA's data after 1990. Except where otherwise noted, # IATA SSIM is the source for entries after 1990. # # A reliable and entertaining source about time zones is # Derek Howse, Greenwich time and longitude, Philip Wilson Publishers (1997). # # Except where otherwise noted, Shanks & Pottenger is the source for # entries through 1991, and IATA SSIM is the source for entries afterwards. # # Other sources occasionally used include: # # Edward W. Whitman, World Time Differences, # Whitman Publishing Co, 2 Niagara Av, Ealing, London (undated), # which I found in the UCLA library. # # William Willett, The Waste of Daylight, 19th edition # # [PDF] (1914-03) # # Milne J. Civil time. Geogr J. 1899 Feb;13(2):173-94 # . He writes: # "It is requested that corrections and additions to these tables # may be sent to Mr. John Milne, Royal Geographical Society, # Savile Row, London." Nowadays please see the file CONTRIBUTING. # # Byalokoz EL. New Counting of Time in Russia since July 1, 1919. # This Russian-language source was consulted by Vladimir Karpinsky; see # https://mm.icann.org/pipermail/tz/2014-August/021320.html # The full Russian citation is: # Б�локоз, Евгений Людвигович. �овый �чет времени в течении �уток # введенный декретом Совета народных коми��аров дл� в�ей Ро��ии � 1-го # июл� 1919 г. / Изд. 2-е Междуведом�твенной коми��ии. - Петроград: # Де��та� го�. тип., 1919. # http://resolver.gpntb.ru/purl?docushare/dsweb/Get/Resource-2011/Byalokoz__E.L.__Novyy__schet__vremeni__v__techenie__sutok__izd__2(1).pdf # # Brazil's Divisão de Serviços da Hora (DISHO) # History of Summer Time # # (1998-09-21, in Portuguese) # # I invented the abbreviations marked '*' in the following table; # the rest are variants of the "xMT" pattern for a city's mean time, # or are from other sources. Corrections are welcome! # std dst 2dst # LMT Local Mean Time # -4:00 AST ADT Atlantic # 0:00 GMT BST BDST Greenwich, British Summer # 0:00 GMT IST Greenwich, Irish Summer # 0:00 WET WEST WEMT Western Europe # 1:00 BST British Standard (1968-1971) # 1:00 IST GMT Irish Standard (1968-) with winter DST # 1:00 CET CEST CEMT Central Europe # 1:00:14 SET Swedish (1879-1899) # 1:36:34 RMT* LST* Riga, Latvian Summer (1880-1926)* # 2:00 EET EEST Eastern Europe # 3:00 MSK MSD MDST* Moscow # From Peter Ilieve (1994-12-04), re EEC/EC/EU members: # The original six: Belgium, France, (West) Germany, Italy, # Luxembourg, the Netherlands. # Plus, from 1 Jan 73: Denmark, Ireland, United Kingdom. # Plus, from 1 Jan 81: Greece. # Plus, from 1 Jan 86: Spain, Portugal. # Plus, from 1 Jan 95: Austria, Finland, Sweden. (Norway negotiated terms for # entry but in a referendum on 28 Nov 94 the people voted No by 52.2% to 47.8% # on a turnout of 88.6%. This was almost the same result as Norway's previous # referendum in 1972, they are the only country to have said No twice. # Referendums in the other three countries voted Yes.) # ... # Estonia ... uses EU dates but not at 01:00 GMT, they use midnight GMT. # I don't think they know yet what they will do from 1996 onwards. # ... # There shouldn't be any [current members who are not using EU rules]. # A Directive has the force of law, member states are obliged to enact # national law to implement it. The only contentious issue was the # different end date for the UK and Ireland, and this was always allowed # in the Directive. ############################################################################### # Britain (United Kingdom) and Ireland (Eire) # From Peter Ilieve (1994-07-06): # # On 17 Jan 1994 the Independent, a UK quality newspaper, had a piece about # historical vistas along the Thames in west London. There was a photo # and a sketch map showing some of the sightlines involved. One paragraph # of the text said: # # 'An old stone obelisk marking a forgotten terrestrial meridian stands # beside the river at Kew. In the 18th century, before time and longitude # was standardised by the Royal Observatory in Greenwich, scholars observed # this stone and the movement of stars from Kew Observatory nearby. They # made their calculations and set the time for the Horse Guards and Parliament, # but now the stone is obscured by scrubwood and can only be seen by walking # along the towpath within a few yards of it.' # # I have a one inch to one mile map of London and my estimate of the stone's # position is 51° 28' 30" N, 0° 18' 45" W. The longitude should # be within about ±2". The Ordnance Survey grid reference is TQ172761. # # [This yields STDOFF = -0:01:15 for London LMT in the 18th century.] # From Paul Eggert (1993-11-18): # # Howse writes that Britain was the first country to use standard time. # The railways cared most about the inconsistencies of local mean time, # and it was they who forced a uniform time on the country. # The original idea was credited to Dr. William Hyde Wollaston (1766-1828) # and was popularized by Abraham Follett Osler (1808-1903). # The first railway to adopt London time was the Great Western Railway # in November 1840; other railways followed suit, and by 1847 most # (though not all) railways used London time. On 1847-09-22 the # Railway Clearing House, an industry standards body, recommended that GMT be # adopted at all stations as soon as the General Post Office permitted it. # The transition occurred on 12-01 for the L&NW, the Caledonian, # and presumably other railways; the January 1848 Bradshaw's lists many # railways as using GMT. By 1855 the vast majority of public # clocks in Britain were set to GMT (though some, like the great clock # on Tom Tower at Christ Church, Oxford, were fitted with two minute hands, # one for local time and one for GMT). The last major holdout was the legal # system, which stubbornly stuck to local time for many years, leading # to oddities like polls opening at 08:13 and closing at 16:13. # The legal system finally switched to GMT when the Statutes (Definition # of Time) Act took effect; it received the Royal Assent on 1880-08-02. # # In the tables below, we condense this complicated story into a single # transition date for London, namely 1847-12-01. We don't know as much # about Dublin, so we use 1880-08-02, the legal transition time. # From Paul Eggert (2014-07-19): # The ancients had no need for daylight saving, as they kept time # informally or via hours whose length depended on the time of year. # Daylight saving time in its modern sense was invented by the # New Zealand entomologist George Vernon Hudson (1867-1946), # whose day job as a postal clerk led him to value # after-hours daylight in which to pursue his research. # In 1895 he presented a paper to the Wellington Philosophical Society # that proposed a two-hour daylight-saving shift. See: # Hudson GV. On seasonal time-adjustment in countries south of lat. 30°. # Transactions and Proceedings of the New Zealand Institute. 1895;28:734 # http://rsnz.natlib.govt.nz/volume/rsnz_28/rsnz_28_00_006110.html # Although some interest was expressed in New Zealand, his proposal # did not find its way into law and eventually it was almost forgotten. # # In England, DST was independently reinvented by William Willett (1857-1915), # a London builder and member of the Royal Astronomical Society # who circulated a pamphlet "The Waste of Daylight" (1907) # that proposed advancing clocks 20 minutes on each of four Sundays in April, # and retarding them by the same amount on four Sundays in September. # A bill was drafted in 1909 and introduced in Parliament several times, # but it met with ridicule and opposition, especially from farming interests. # Later editions of the pamphlet proposed one-hour summer time, and # it was eventually adopted as a wartime measure in 1916. # See: Summer Time Arrives Early, The Times (2000-05-18). # A monument to Willett was unveiled on 1927-05-21, in an open space in # a 45-acre wood near Chislehurst, Kent that was purchased by popular # subscription and open to the public. On the south face of the monolith, # designed by G. W. Miller, is the William Willett Memorial Sundial, # which is permanently set to Summer Time. # From Winston Churchill (1934-04-28): # It is one of the paradoxes of history that we should owe the boon of # summer time, which gives every year to the people of this country # between 160 and 170 hours more daylight leisure, to a war which # plunged Europe into darkness for four years, and shook the # foundations of civilization throughout the world. # -- "A Silent Toast to William Willett", Pictorial Weekly; # republished in Finest Hour (Spring 2002) 1(114):26 # https://www.winstonchurchill.org/publications/finest-hour/finest-hour-114/a-silent-toast-to-william-willett-by-winston-s-churchill # From Paul Eggert (2015-08-08): # The OED Supplement says that the English originally said "Daylight Saving" # when they were debating the adoption of DST in 1908; but by 1916 this # term appears only in quotes taken from DST's opponents, whereas the # proponents (who eventually won the argument) are quoted as using "Summer". # The term "Summer Time" was introduced by Herbert Samuel, Home Secretary; see: # Viscount Samuel. Leisure in a Democracy. Cambridge University Press # ISBN 978-1-107-49471-8 (1949, reissued 2015), p 8. # From Arthur David Olson (1989-01-19): # A source at the British Information Office in New York avers that it's # known as "British" Summer Time in all parts of the United Kingdom. # Date: 4 Jan 89 08:57:25 GMT (Wed) # From: Jonathan Leffler # [British Summer Time] is fixed annually by Act of Parliament. # If you can predict what Parliament will do, you should be in # politics making a fortune, not computing. # From Chris Carrier (1996-06-14): # I remember reading in various wartime issues of the London Times the # acronym BDST for British Double Summer Time. Look for the published # time of sunrise and sunset in The Times, when BDST was in effect, and # if you find a zone reference it will say, "All times B.D.S.T." # From Joseph S. Myers (1999-09-02): # ... some military cables (WO 219/4100 - this is a copy from the # main SHAEF archives held in the US National Archives, SHAEF/5252/8/516) # agree that the usage is BDST (this appears in a message dated 17 Feb 1945). # From Joseph S. Myers (2000-10-03): # On 18th April 1941, Sir Stephen Tallents of the BBC wrote to Sir # Alexander Maxwell of the Home Office asking whether there was any # official designation; the reply of the 21st was that there wasn't # but he couldn't think of anything better than the "Double British # Summer Time" that the BBC had been using informally. # https://www.polyomino.org.uk/british-time/bbc-19410418.png # https://www.polyomino.org.uk/british-time/ho-19410421.png # From Sir Alexander Maxwell (1941-04-21) in the letter mentioned above: # [N]o official designation has as far as I know been adopted for the time # which is to be introduced in May.... # I cannot think of anything better than "Double British Summer Time" # which could not be said to run counter to any official description. # From Paul Eggert (2000-10-02): # Howse writes (p 157) 'DBST' too, but 'BDST' seems to have been common # and follows the more usual convention of putting the location name first, # so we use 'BDST'. # Peter Ilieve (1998-04-19) described at length # the history of summer time legislation in the United Kingdom. # Since 1998 Joseph S. Myers has been updating # and extending this list, which can be found in # https://www.polyomino.org.uk/british-time/ # From Joseph S. Myers (1998-01-06): # # The legal time in the UK outside of summer time is definitely GMT, not UTC; # see Lord Tanlaw's speech # https://www.publications.parliament.uk/pa/ld199798/ldhansrd/vo970611/text/70611-10.htm#70611-10_head0 # (Lords Hansard 11 June 1997 columns 964 to 976). # From Paul Eggert (2006-03-22): # # For lack of other data, follow Shanks & Pottenger for Eire in 1940-1948. # # Given Ilieve and Myers's data, the following claims by Shanks & Pottenger # are incorrect: # * Wales did not switch from GMT to daylight saving time until # 1921 Apr 3, when they began to conform with the rest of Great Britain. # Actually, Wales was identical after 1880. # * Eire had two transitions on 1916 Oct 1. # It actually just had one transition. # * Northern Ireland used single daylight saving time throughout WW II. # Actually, it conformed to Britain. # * GB-Eire changed standard time to 1 hour ahead of GMT on 1968-02-18. # Actually, that date saw the usual switch to summer time. # Standard time was not changed until 1968-10-27 (the clocks didn't change). # # Here is another incorrect claim by Shanks & Pottenger: # * Jersey, Guernsey, and the Isle of Man did not switch from GMT # to daylight saving time until 1921 Apr 3, when they began to # conform with Great Britain. # S.R.&O. 1916, No. 382 and HO 45/10811/312364 (quoted above) say otherwise. # # The following claim by Shanks & Pottenger is possible though doubtful; # we'll ignore it for now. # * Dublin's 1971-10-31 switch was at 02:00, even though London's was 03:00. # From Paul Eggert (2017-12-04): # # Dunsink Observatory (8 km NW of Dublin's center) was to Dublin as # Greenwich was to London. For example: # # "Timeball on the ballast office is down. Dunsink time." # -- James Joyce, Ulysses # # The abbreviation DMT stood for "Dublin Mean Time" or "Dunsink Mean Time"; # this being Ireland, opinions differed. # # Whitman says Dublin/Dunsink Mean Time was UT-00:25:21, which agrees # with measurements of recent visitors to the Meridian Room of Dunsink # Observatory; see Malone D. Dunsink and timekeeping. 2016-01-24. # . Malone # writes that the Nautical Almanac listed UT-00:25:22 until 1896, when # it moved to UT-00:25:21.1 (I confirmed that the 1893 edition used # the former and the 1896 edition used the latter). Evidently the # news of this change propagated slowly, as Milne 1899 still lists # UT-00:25:22 and cites the International Telegraph Bureau. As it is # not clear that there was any practical significance to the change # from UT-00:25:22 to UT-00:25:21.1 in civil timekeeping, omit this # transition for now and just use the latter value. # "Countess Markievicz ... claimed that the [1916] abolition of Dublin Mean Time # was among various actions undertaken by the 'English' government that # would 'put the whole country into the SF (Sinn Féin) camp'. She claimed # Irish 'public feeling (was) outraged by forcing of English time on us'." # -- Parsons M. Dublin lost its time zone - and 25 minutes - after 1916 Rising. # Irish Times 2014-10-27. # https://www.irishtimes.com/news/politics/dublin-lost-its-time-zone-and-25-minutes-after-1916-rising-1.1977411 # From Joseph S. Myers (2005-01-26): # Irish laws are available online at . # These include various relating to legal time, for example: # # ZZA13Y1923.html ZZA12Y1924.html ZZA8Y1925.html ZZSIV20PG1267.html # # ZZSI71Y1947.html ZZSI128Y1948.html ZZSI23Y1949.html ZZSI41Y1950.html # ZZSI27Y1951.html ZZSI73Y1952.html # # ZZSI11Y1961.html ZZSI232Y1961.html ZZSI182Y1962.html # ZZSI167Y1963.html ZZSI257Y1964.html ZZSI198Y1967.html # ZZA23Y1968.html ZZA17Y1971.html # # ZZSI67Y1981.html ZZSI212Y1982.html ZZSI45Y1986.html # ZZSI264Y1988.html ZZSI52Y1990.html ZZSI371Y1992.html # ZZSI395Y1994.html ZZSI484Y1997.html ZZSI506Y2001.html # # [These are all relative to the root, e.g., the first is # .] # # (These are those I found, but there could be more. In any case these # should allow various updates to the comments in the europe file to cover # the laws applicable in Ireland.) # # (Note that the time in the Republic of Ireland since 1968 has been defined # in terms of standard time being GMT+1 with a period of winter time when it # is GMT, rather than standard time being GMT with a period of summer time # being GMT+1.) # From Paul Eggert (1999-03-28): # Clive Feather (, 1997-03-31) # reports that Folkestone (Cheriton) Shuttle Terminal uses Concession Time # (CT), equivalent to French civil time. # Julian Hill (, 1998-09-30) reports that # trains between Dollands Moor (the freight facility next door) # and Frethun run in CT. # My admittedly uninformed guess is that the terminal has two authorities, # the French concession operators and the British civil authorities, # and that the time depends on who you're talking to. # If, say, the British police were called to the station for some reason, # I would expect the official police report to use GMT/BST and not CET/CEST. # This is a borderline case, but for now let's stick to GMT/BST. # From an anonymous contributor (1996-06-02): # The law governing time in Ireland is under Statutory Instrument SI 395/94, # which gives force to European Union 7th Council Directive No. 94/21/EC. # Under this directive, the Minister for Justice in Ireland makes appropriate # regulations. I spoke this morning with the Secretary of the Department of # Justice (tel +353 1 678 9711) who confirmed to me that the correct name is # "Irish Summer Time", abbreviated to "IST". # # From Paul Eggert (2017-12-07): # The 1996 anonymous contributor's goal was to determine the correct # abbreviation for summer time in Dublin and so the contributor # focused on the "IST", not on the "Irish Summer Time". Though the # "IST" was correct, the "Irish Summer Time" appears to have been an # error, as Ireland's Standard Time (Amendment) Act, 1971 states that # standard time in Ireland remains at UT +01 and is observed in # summer, and that Greenwich mean time is observed in winter. (Thanks # to Derick Rethans for pointing out the error.) That is, when # Ireland amended the 1968 act that established UT +01 as Irish # Standard Time, it left standard time unchanged and established GMT # as a negative daylight saving time in winter. So, in this database # IST stands for Irish Summer Time for timestamps before 1968, and for # Irish Standard Time after that. See: # https://www.irishstatutebook.ie/eli/1971/act/17/enacted/en/print # Michael Deckers (2017-06-01) gave the following URLs for Ireland's # Summer Time Act, 1925 and Summer Time Orders, 1926 and 1947: # https://www.irishstatutebook.ie/eli/1925/act/8/enacted/en/print # https://www.irishstatutebook.ie/eli/1926/sro/919/made/en/print # https://www.irishstatutebook.ie/eli/1947/sro/71/made/en/print # From Stan Ulbrych (2026-09-12): # Ireland's Summer Time Act, 1925 (No. 8 of 1925, see above) # ended summer time at 02:00 GMT on the day after the third Saturday in # September, that is, 1925-09-20. The UK's Summer Time Act, 1925, however, # moved the UK's end date to the day after the first Saturday in October with # immediate effect, but it did not apply to the Irish Free State. # Ireland matched the October date only from 1926, via the Summer Time Order, # 1926, whose recital replaces the period fixed by section 2 of the 1925 Act # and mentions no earlier order. Moving approval of that order in the # Dáil on 1926-03-24, Minister Kevin O'Higgins said: # # Deputies will remember that last year there was a discrepancy of a # fortnight between the termination of Summer Time in the Saorstát and # in Great Britain and Northern Ireland. As a result of that, serious # inconvenience was caused to railway companies, and there was a certain # amount of dislocation of postal services in the country. # # (See: https://www.oireachtas.ie/en/debates/debate/dail/1926-03-24/18/) # So Dublin fell back on 1925-09-20, two weeks before London. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S # Summer Time Act, 1916 Rule GB-Eire 1916 only - May 21 2:00s 1:00 BST Rule GB-Eire 1916 only - Oct 1 2:00s 0 GMT # S.R.&O. 1917, No. 358 Rule GB-Eire 1917 only - Apr 8 2:00s 1:00 BST Rule GB-Eire 1917 only - Sep 17 2:00s 0 GMT # S.R.&O. 1918, No. 274 Rule GB-Eire 1918 only - Mar 24 2:00s 1:00 BST Rule GB-Eire 1918 only - Sep 30 2:00s 0 GMT # S.R.&O. 1919, No. 297 Rule GB-Eire 1919 only - Mar 30 2:00s 1:00 BST Rule GB-Eire 1919 only - Sep 29 2:00s 0 GMT # S.R.&O. 1920, No. 458 Rule GB-Eire 1920 only - Mar 28 2:00s 1:00 BST # S.R.&O. 1920, No. 1844 Rule GB-Eire 1920 only - Oct 25 2:00s 0 GMT # S.R.&O. 1921, No. 363 Rule GB-Eire 1921 only - Apr 3 2:00s 1:00 BST Rule GB-Eire 1921 only - Oct 3 2:00s 0 GMT # S.R.&O. 1922, No. 264 Rule GB-Eire 1922 only - Mar 26 2:00s 1:00 BST Rule GB-Eire 1922 only - Oct 8 2:00s 0 GMT # The Summer Time Act, 1922 Rule GB-Eire 1923 only - Apr Sun>=16 2:00s 1:00 BST Rule GB-Eire 1923 1924 - Sep Sun>=16 2:00s 0 GMT Rule GB-Eire 1924 only - Apr Sun>=9 2:00s 1:00 BST Rule GB-Eire 1925 1926 - Apr Sun>=16 2:00s 1:00 BST # The Summer Time Act, 1925 Rule GB-Eire 1925 1938 - Oct Sun>=2 2:00s 0 GMT Rule GB-Eire 1927 only - Apr Sun>=9 2:00s 1:00 BST Rule GB-Eire 1928 1929 - Apr Sun>=16 2:00s 1:00 BST Rule GB-Eire 1930 only - Apr Sun>=9 2:00s 1:00 BST Rule GB-Eire 1931 1932 - Apr Sun>=16 2:00s 1:00 BST Rule GB-Eire 1933 only - Apr Sun>=9 2:00s 1:00 BST Rule GB-Eire 1934 only - Apr Sun>=16 2:00s 1:00 BST Rule GB-Eire 1935 only - Apr Sun>=9 2:00s 1:00 BST Rule GB-Eire 1936 1937 - Apr Sun>=16 2:00s 1:00 BST Rule GB-Eire 1938 only - Apr Sun>=9 2:00s 1:00 BST Rule GB-Eire 1939 only - Apr Sun>=16 2:00s 1:00 BST # S.R.&O. 1939, No. 1379 Rule GB-Eire 1939 only - Nov Sun>=16 2:00s 0 GMT # S.R.&O. 1940, No. 172 and No. 1883 Rule GB-Eire 1940 only - Feb Sun>=23 2:00s 1:00 BST # S.R.&O. 1941, No. 476 Rule GB-Eire 1941 only - May Sun>=2 1:00s 2:00 BDST Rule GB-Eire 1941 1943 - Aug Sun>=9 1:00s 1:00 BST # S.R.&O. 1942, No. 506 Rule GB-Eire 1942 1944 - Apr Sun>=2 1:00s 2:00 BDST # S.R.&O. 1944, No. 932 Rule GB-Eire 1944 only - Sep Sun>=16 1:00s 1:00 BST # S.R.&O. 1945, No. 312 Rule GB-Eire 1945 only - Apr Mon>=2 1:00s 2:00 BDST Rule GB-Eire 1945 only - Jul Sun>=9 1:00s 1:00 BST # S.R.&O. 1945, No. 1208 Rule GB-Eire 1945 1946 - Oct Sun>=2 2:00s 0 GMT Rule GB-Eire 1946 only - Apr Sun>=9 2:00s 1:00 BST # The Summer Time Act, 1947 Rule GB-Eire 1947 only - Mar 16 2:00s 1:00 BST Rule GB-Eire 1947 only - Apr 13 1:00s 2:00 BDST Rule GB-Eire 1947 only - Aug 10 1:00s 1:00 BST Rule GB-Eire 1947 only - Nov 2 2:00s 0 GMT # Summer Time Order, 1948 (S.I. 1948/495) Rule GB-Eire 1948 only - Mar 14 2:00s 1:00 BST Rule GB-Eire 1948 only - Oct 31 2:00s 0 GMT # Summer Time Order, 1949 (S.I. 1949/373) Rule GB-Eire 1949 only - Apr 3 2:00s 1:00 BST Rule GB-Eire 1949 only - Oct 30 2:00s 0 GMT # Summer Time Order, 1950 (S.I. 1950/518) # Summer Time Order, 1951 (S.I. 1951/430) # Summer Time Order, 1952 (S.I. 1952/451) Rule GB-Eire 1950 1952 - Apr Sun>=14 2:00s 1:00 BST Rule GB-Eire 1950 1952 - Oct Sun>=21 2:00s 0 GMT # revert to the rules of the Summer Time Act, 1925 Rule GB-Eire 1953 only - Apr Sun>=16 2:00s 1:00 BST Rule GB-Eire 1953 1960 - Oct Sun>=2 2:00s 0 GMT Rule GB-Eire 1954 only - Apr Sun>=9 2:00s 1:00 BST Rule GB-Eire 1955 1956 - Apr Sun>=16 2:00s 1:00 BST Rule GB-Eire 1957 only - Apr Sun>=9 2:00s 1:00 BST Rule GB-Eire 1958 1959 - Apr Sun>=16 2:00s 1:00 BST Rule GB-Eire 1960 only - Apr Sun>=9 2:00s 1:00 BST # Summer Time Order, 1961 (S.I. 1961/71) # Summer Time (1962) Order, 1961 (S.I. 1961/2465) # Summer Time Order, 1963 (S.I. 1963/81) Rule GB-Eire 1961 1963 - Mar lastSun 2:00s 1:00 BST Rule GB-Eire 1961 1968 - Oct Sun>=23 2:00s 0 GMT # Summer Time (1964) Order, 1963 (S.I. 1963/2101) # Summer Time Order, 1964 (S.I. 1964/1201) # Summer Time Order, 1967 (S.I. 1967/1148) Rule GB-Eire 1964 1967 - Mar Sun>=19 2:00s 1:00 BST # Summer Time Order, 1968 (S.I. 1968/117) Rule GB-Eire 1968 only - Feb 18 2:00s 1:00 BST # The British Standard Time Act, 1968 # (no summer time) # The Summer Time Act, 1972 Rule GB-Eire 1972 1980 - Mar Sun>=16 2:00s 1:00 BST Rule GB-Eire 1972 1980 - Oct Sun>=23 2:00s 0 GMT # Summer Time Order, 1980 (S.I. 1980/1089) # Summer Time Order, 1982 (S.I. 1982/1673) # Summer Time Order, 1986 (S.I. 1986/223) # Summer Time Order, 1988 (S.I. 1988/931) Rule GB-Eire 1981 1995 - Mar lastSun 1:00u 1:00 BST Rule GB-Eire 1981 1989 - Oct Sun>=23 1:00u 0 GMT # Summer Time Order, 1989 (S.I. 1989/985) # Summer Time Order, 1992 (S.I. 1992/1729) # Summer Time Order 1994 (S.I. 1994/2798) Rule GB-Eire 1990 1995 - Oct Sun>=22 1:00u 0 GMT # Summer Time Order 1997 (S.I. 1997/2982) # See EU for rules starting in 1996. # # Use Europe/London for Jersey, Guernsey, and the Isle of Man. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/London -0:01:15 - LMT 1847 Dec 1 0:00 GB-Eire %s 1968 Oct 27 1:00 - BST 1971 Oct 31 2:00u 0:00 GB-Eire %s 1996 0:00 EU GMT/BST # From Paul Eggert (2018-02-15): # In January 2018 we discovered that the negative SAVE values in the # Eire rules cause problems with tests for ICU: # https://mm.icann.org/pipermail/tz/2018-January/025825.html # and with tests for OpenJDK: # https://mm.icann.org/pipermail/tz/2018-January/025822.html # # To work around this problem, the build procedure can translate the # following data into two forms, one with negative SAVE values and the # other form with a traditional approximation for Irish timestamps # after 1971-10-31 02:00 UTC; although this approximation has tm_isdst # flags that are reversed, its UTC offsets are correct and this often # suffices.... # # The following is like GB-Eire and EU, except with standard time in # summer and negative daylight saving time in winter. It is for when # negative SAVE values are used. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Eire 1971 only - Oct 31 2:00u -1:00 - Rule Eire 1972 1980 - Mar Sun>=16 2:00u 0 - Rule Eire 1972 1980 - Oct Sun>=23 2:00u -1:00 - Rule Eire 1981 max - Mar lastSun 1:00u 0 - Rule Eire 1981 1989 - Oct Sun>=23 1:00u -1:00 - Rule Eire 1990 1995 - Oct Sun>=22 1:00u -1:00 - Rule Eire 1996 max - Oct lastSun 1:00u -1:00 - # Zone NAME STDOFF RULES FORMAT [UNTIL] #STDOFF -0:25:21.1 Zone Europe/Dublin -0:25:21 - LMT 1880 Aug 2 -0:25:21 - DMT 1916 May 21 2:00s -0:25:21 1:00 IST 1916 Oct 1 2:00s 0:00 GB-Eire %s 1921 Dec 6 # independence 0:00 GB-Eire GMT/IST 1925 Sep Sun>=16 2:00s 0:00 - GMT 1926 0:00 GB-Eire GMT/IST 1940 Feb 25 2:00s 0:00 1:00 IST 1946 Oct 6 2:00s 0:00 - GMT 1947 Mar 16 2:00s 0:00 1:00 IST 1947 Nov 2 2:00s 0:00 - GMT 1948 Apr 18 2:00s 0:00 GB-Eire GMT/IST 1968 Oct 27 # Vanguard section, for zic and other parsers that support negative DST. 1:00 Eire IST/GMT # Rearguard section, for parsers lacking negative DST; see ziguard.awk. # 1:00 - IST 1971 Oct 31 2:00u # 0:00 GB-Eire GMT/IST 1996 # 0:00 EU GMT/IST # End of rearguard section. ############################################################################### # Europe # The following rules are for the European Union and for its # predecessor organization, the European Communities. # For brevity they are called "EU rules" elsewhere in this file. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule EU 1977 1980 - Apr Sun>=1 1:00u 1:00 S Rule EU 1977 only - Sep lastSun 1:00u 0 - Rule EU 1978 only - Oct 1 1:00u 0 - Rule EU 1979 1995 - Sep lastSun 1:00u 0 - Rule EU 1981 max - Mar lastSun 1:00u 1:00 S Rule EU 1996 max - Oct lastSun 1:00u 0 - # The most recent directive covers the years starting in 2002. See: # Directive 2000/84/EC of the European Parliament and of the Council # of 19 January 2001 on summer-time arrangements. # http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=CELEX:32000L0084:EN:NOT # W-Eur differs from EU only in that W-Eur uses standard time. Rule W-Eur 1977 1980 - Apr Sun>=1 1:00s 1:00 S Rule W-Eur 1977 only - Sep lastSun 1:00s 0 - Rule W-Eur 1978 only - Oct 1 1:00s 0 - Rule W-Eur 1979 1995 - Sep lastSun 1:00s 0 - Rule W-Eur 1981 max - Mar lastSun 1:00s 1:00 S Rule W-Eur 1996 max - Oct lastSun 1:00s 0 - # Older C-Eur rules are for convenience in the tables. # From 1977 on, C-Eur differs from EU only in that C-Eur uses standard time. Rule C-Eur 1916 only - Apr 30 23:00 1:00 S Rule C-Eur 1916 only - Oct 1 1:00 0 - Rule C-Eur 1917 1918 - Apr Mon>=15 2:00s 1:00 S Rule C-Eur 1917 1918 - Sep Mon>=15 2:00s 0 - Rule C-Eur 1940 only - Apr 1 2:00s 1:00 S Rule C-Eur 1942 only - Nov 2 2:00s 0 - Rule C-Eur 1943 only - Mar 29 2:00s 1:00 S Rule C-Eur 1943 only - Oct 4 2:00s 0 - Rule C-Eur 1944 1945 - Apr Mon>=1 2:00s 1:00 S # Whitman gives 1944 Oct 7; go with Shanks & Pottenger. Rule C-Eur 1944 only - Oct 2 2:00s 0 - # From Jesper Nørgaard Welen (2008-07-13): # # I found what is probably a typo of 2:00 which should perhaps be 2:00s # in the C-Eur rule from tz database version 2008d (this part was # corrected in version 2008d). The circumstantial evidence is simply the # tz database itself, as seen below: # # Zone Europe/Paris ... # 0:00 France WE%sT 1945 Sep 16 3:00 # # Zone Europe/Monaco ... # 0:00 France WE%sT 1945 Sep 16 3:00 # # Zone Europe/Belgrade ... # 1:00 1:00 CEST 1945 Sep 16 2:00s # # Rule France 1945 only - Sep 16 3:00 0 - # Rule Belgium 1945 only - Sep 16 2:00s 0 - # Rule Neth 1945 only - Sep 16 2:00s 0 - # # The rule line to be changed is: # # Rule C-Eur 1945 only - Sep 16 2:00 0 - # # It seems that Paris, Monaco, Rule France, Rule Belgium all agree on # 2:00 standard time, e.g. 3:00 local time. However there are no # countries that use C-Eur rules in September 1945, so the only items # affected are apparently these fictitious zones that translate acronyms # CET and MET: # # Zone CET 1:00 C-Eur CE%sT # Zone MET 1:00 C-Eur ME%sT # # It this is right then the corrected version would look like: # # Rule C-Eur 1945 only - Sep 16 2:00s 0 - # # A small step for mankind though 8-) Rule C-Eur 1945 only - Sep 16 2:00s 0 - Rule C-Eur 1977 1980 - Apr Sun>=1 2:00s 1:00 S Rule C-Eur 1977 only - Sep lastSun 2:00s 0 - Rule C-Eur 1978 only - Oct 1 2:00s 0 - Rule C-Eur 1979 1995 - Sep lastSun 2:00s 0 - Rule C-Eur 1981 max - Mar lastSun 2:00s 1:00 S Rule C-Eur 1996 max - Oct lastSun 2:00s 0 - # E-Eur differs from EU only in that E-Eur switches at midnight local time. Rule E-Eur 1977 1980 - Apr Sun>=1 0:00 1:00 S Rule E-Eur 1977 only - Sep lastSun 0:00 0 - Rule E-Eur 1978 only - Oct 1 0:00 0 - Rule E-Eur 1979 1995 - Sep lastSun 0:00 0 - Rule E-Eur 1981 max - Mar lastSun 0:00 1:00 S Rule E-Eur 1996 max - Oct lastSun 0:00 0 - # Daylight saving time for Russia and the Soviet Union # # The 1917-1921 decree URLs are from Alexander Belopolsky (2016-08-23). # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Russia 1917 only - Jul 1 23:00 1:00 MST # Moscow Summer Time # # Decree No. 142 (1917-12-22) http://istmat.info/node/28137 Rule Russia 1917 only - Dec 28 0:00 0 MMT # Moscow Mean Time # # Decree No. 497 (1918-05-30) http://istmat.info/node/30001 Rule Russia 1918 only - May 31 22:00 2:00 MDST # Moscow Double Summer Time Rule Russia 1918 only - Sep 16 1:00 1:00 MST # # Decree No. 258 (1919-05-29) http://istmat.info/node/37949 Rule Russia 1919 only - May 31 23:00 2:00 MDST # Rule Russia 1919 only - Jul 1 0:00u 1:00 MSD Rule Russia 1919 only - Aug 16 0:00 0 MSK # # Decree No. 63 (1921-02-03) http://istmat.info/node/45840 Rule Russia 1921 only - Feb 14 23:00 1:00 MSD # # Decree No. 121 (1921-03-07) http://istmat.info/node/45949 Rule Russia 1921 only - Mar 20 23:00 2:00 +05 # Rule Russia 1921 only - Sep 1 0:00 1:00 MSD Rule Russia 1921 only - Oct 1 0:00 0 - # Act No. 925 of the Council of Ministers of the USSR (1980-10-24): Rule Russia 1981 1984 - Apr 1 0:00 1:00 S Rule Russia 1981 1983 - Oct 1 0:00 0 - # Act No. 967 of the Council of Ministers of the USSR (1984-09-13), repeated in # Act No. 227 of the Council of Ministers of the USSR (1989-03-14): Rule Russia 1984 1995 - Sep lastSun 2:00s 0 - Rule Russia 1985 2010 - Mar lastSun 2:00s 1:00 S # Rule Russia 1996 2010 - Oct lastSun 2:00s 0 - # As described below, Russia's 2014 change affects Zone data, not Rule data. # From Stepan Golosunov (2016-03-07): # Wikipedia and other sources refer to the Act of the Council of # Ministers of the USSR from 1988-01-04 No. 5 and the Act of the # Council of Ministers of the USSR from 1989-03-14 No. 227. # # I did not find full texts of these acts. For the 1989 one we have # title at https://base.garant.ru/70754136/ : # "About change in calculation of time on the territories of # Lithuanian SSR, Latvian SSR and Estonian SSR, Astrakhan, # Kaliningrad, Kirov, Kuybyshev, Ulyanovsk and Uralsk oblasts". # And http://astrozet.net/files/Zones/DOC/RU/1980-925.txt appears to # contain quotes from both acts: Since last Sunday of March 1988 rules # of the second time belt are installed in Volgograd and Saratov # oblasts. Since last Sunday of March 1989: # a) Lithuanian SSR, Latvian SSR, Estonian SSR, Kaliningrad oblast: # second time belt rules without extra hour (Moscow-1); # b) Astrakhan, Kirov, Kuybyshev, Ulyanovsk oblasts: second time belt # rules (Moscow time) # c) Uralsk oblast: third time belt rules (Moscow+1). # From Stepan Golosunov (2016-03-27): # Unamended version of the act of the # Government of the Russian Federation No. 23 from 08.01.1992 # http://pravo.gov.ru/proxy/ips/?docbody=&nd=102014034&rdk=0 # says that every year clocks were to be moved forward on last Sunday # of March at 2 hours and moved backwards on last Sunday of September # at 3 hours. It was amended in 1996 to replace September with October. # From Alexander Krivenyshev (2011-06-14): # According to Kremlin press service, Russian President Dmitry Medvedev # signed a federal law "On calculation of time" on June 9, 2011. # According to the law Russia is abolishing daylight saving time. # # Medvedev signed a law "On the Calculation of Time" (in russian): # http://bmockbe.ru/events/?ID=7583 # # Medvedev signed a law on the calculation of the time (in russian): # https://www.regnum.ru/news/polit/1413906.html # From Arthur David Olson (2011-06-15): # Take "abolishing daylight saving time" to mean that time is now considered # to be standard. # Previous editions of this database used abbreviations like MET DST # for Central European Summer Time, but this didn't agree with common usage. # From Markus Kuhn (1996-07-12): # The official German names ... are # # Mitteleuropäische Zeit (MEZ) = UTC+01:00 # Mitteleuropäische Sommerzeit (MESZ) = UTC+02:00 # # as defined in the German Time Act (Gesetz über die Zeitbestimmung (ZeitG), # 1978-07-25, Bundesgesetzblatt, Jahrgang 1978, Teil I, S. 1110-1111).... # I wrote ... to the German Federal Physical-Technical Institution # # Physikalisch-Technische Bundesanstalt (PTB) # Laboratorium 4.41 "Zeiteinheit" # Postfach 3345 # D-38023 Braunschweig # phone: +49 531 592-0 # # ... I received today an answer letter from Dr. Peter Hetzel, head of the PTB # department for time and frequency transmission. He explained that the # PTB translates MEZ and MESZ into English as # # Central European Time (CET) = UTC+01:00 # Central European Summer Time (CEST) = UTC+02:00 # Albania # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Albania 1940 only - Jun 16 0:00 1:00 S Rule Albania 1942 only - Nov 2 3:00 0 - Rule Albania 1943 only - Mar 29 2:00 1:00 S Rule Albania 1943 only - Apr 10 3:00 0 - Rule Albania 1974 only - May 4 0:00 1:00 S Rule Albania 1974 only - Oct 2 0:00 0 - Rule Albania 1975 only - May 1 0:00 1:00 S Rule Albania 1975 only - Oct 2 0:00 0 - Rule Albania 1976 only - May 2 0:00 1:00 S Rule Albania 1976 only - Oct 3 0:00 0 - Rule Albania 1977 only - May 8 0:00 1:00 S Rule Albania 1977 only - Oct 2 0:00 0 - Rule Albania 1978 only - May 6 0:00 1:00 S Rule Albania 1978 only - Oct 1 0:00 0 - Rule Albania 1979 only - May 5 0:00 1:00 S Rule Albania 1979 only - Sep 30 0:00 0 - Rule Albania 1980 only - May 3 0:00 1:00 S Rule Albania 1980 only - Oct 4 0:00 0 - Rule Albania 1981 only - Apr 26 0:00 1:00 S Rule Albania 1981 only - Sep 27 0:00 0 - Rule Albania 1982 only - May 2 0:00 1:00 S Rule Albania 1982 only - Oct 3 0:00 0 - Rule Albania 1983 only - Apr 18 0:00 1:00 S Rule Albania 1983 only - Oct 1 0:00 0 - Rule Albania 1984 only - Apr 1 0:00 1:00 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Tirane 1:19:20 - LMT 1914 1:00 - CET 1940 Jun 16 1:00 Albania CE%sT 1984 Jul 1:00 EU CE%sT # Andorra # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Andorra 0:06:04 - LMT 1901 0:00 - WET 1946 Sep 30 1:00 - CET 1985 Mar 31 2:00 1:00 EU CE%sT # Austria # Milne says Vienna time was 1:05:21. # From Paul Eggert (2006-03-22): Shanks & Pottenger give 1918-06-16 and # 1945-11-18, but the Austrian Federal Office of Metrology and # Surveying (BEV) gives 1918-09-16 and for Vienna gives the "alleged" # date of 1945-04-12 with no time. For the 1980-04-06 transition # Shanks & Pottenger give 02:00, the BEV 00:00. Go with the BEV, # and guess 02:00 for 1945-04-12. # From Alois Treindl (2019-07-22): # In 1946 the end of DST was on Monday, 7 October 1946, at 3:00 am. # Shanks had this right. Source: Die Weltpresse, 5. Oktober 1946, page 5. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Austria 1920 only - Apr 5 2:00s 1:00 S Rule Austria 1920 only - Sep 13 2:00s 0 - Rule Austria 1946 only - Apr 14 2:00s 1:00 S Rule Austria 1946 only - Oct 7 2:00s 0 - Rule Austria 1947 1948 - Oct Sun>=1 2:00s 0 - Rule Austria 1947 only - Apr 6 2:00s 1:00 S Rule Austria 1948 only - Apr 18 2:00s 1:00 S Rule Austria 1980 only - Apr 6 0:00 1:00 S Rule Austria 1980 only - Sep 28 0:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Vienna 1:05:21 - LMT 1893 Apr 1:00 C-Eur CE%sT 1920 1:00 Austria CE%sT 1940 Apr 1 2:00s 1:00 C-Eur CE%sT 1945 Apr 2 2:00s 1:00 1:00 CEST 1945 Apr 12 2:00s 1:00 - CET 1946 1:00 Austria CE%sT 1981 1:00 EU CE%sT # Belarus # # From Stepan Golosunov (2016-07-02): # http://www.lawbelarus.com/repub/sub30/texf9611.htm # (Act of the Cabinet of Ministers of the Republic of Belarus from # 1992-03-25 No. 157) ... says clocks were to be moved forward at 2:00 # on last Sunday of March and backward at 3:00 on last Sunday of September # (the same as previous USSR and contemporary Russian regulations). # # From Yauhen Kharuzhy (2011-09-16): # By latest Belarus government act Europe/Minsk timezone was changed to # GMT+3 without DST (was GMT+2 with DST). # # Sources (Russian language): # http://www.belta.by/ru/all_news/society/V-Belarusi-otmenjaetsja-perexod-na-sezonnoe-vremja_i_572952.html # http://naviny.by/rubrics/society/2011/09/16/ic_articles_116_175144/ # https://news.tut.by/society/250578.html # # From Alexander Bokovoy (2014-10-09): # Belarussian government decided against changing to winter time.... # http://eng.belta.by/all_news/society/Belarus-decides-against-adjusting-time-in-Russias-wake_i_76335.html # # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Minsk 1:50:16 - LMT 1880 1:50 - MMT 1924 May 2 # Minsk Mean Time 2:00 - EET 1930 Jun 21 3:00 - MSK 1941 Jun 28 1:00 C-Eur CE%sT 1944 Jul 3 3:00 Russia MSK/MSD 1990 3:00 - MSK 1991 Mar 31 2:00s 2:00 Russia EE%sT 2011 Mar 27 2:00s 3:00 - %z # Belgium # Luxembourg # Netherlands # # From Michael Deckers (2019-08-25): # The exposition in the web page # https://www.bestor.be/wiki/index.php/Voyager_dans_le_temps._L%E2%80%99introduction_de_la_norme_de_Greenwich_en_Belgique # gives several contemporary sources from which one can conclude that # the switch in Europe/Brussels on 1892-05-01 was from 00:17:30 to 00:00:00. # # From Paul Eggert (2019-08-28): # This quote helps explain the late-1914 situation: # In early November 1914, the Germans imposed the time zone used in central # Europe and forced the inhabitants to set their watches and public clocks # sixty minutes ahead. Many were reluctant to accept "German time" and # continued to use "Belgian time" among themselves. Reflecting the spirit of # resistance that arose in the population, a song made fun of this change.... # The song ended: # Putting your clock forward # Will but hasten the happy hour # When we kick out the Boches! # See: Pluvinage G. Brussels on German time. Cahiers Bruxellois - # Brusselse Cahiers. 2014;XLVI(1E):15-38. # https://www.cairn.info/revue-cahiers-bruxellois-2014-1E-page-15.htm # # Entries from 1914 through 1917 are taken from "De tijd in België" # . # Entries from 1918 through 1991 are taken from: # Annuaire de L'Observatoire Royal de Belgique, # Avenue Circulaire, 3, B-1180 BRUXELLES, CLVIIe année, 1991 # (Imprimerie HAYEZ, s.p.r.l., Rue Fin, 4, 1080 BRUXELLES, MCMXC), # pp 8-9. # Thanks to Pascal Delmoitie for the 1918/1991 references. # The 1918 rules are listed for completeness; they apply to unoccupied Belgium. # Assume Brussels switched to WET in 1918 when the armistice took effect. # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Belgium 1918 only - Mar 9 0:00s 1:00 S Rule Belgium 1918 1919 - Oct Sat>=1 23:00s 0 - Rule Belgium 1919 only - Mar 1 23:00s 1:00 S Rule Belgium 1920 only - Feb 14 23:00s 1:00 S Rule Belgium 1920 only - Oct 23 23:00s 0 - Rule Belgium 1921 only - Mar 14 23:00s 1:00 S Rule Belgium 1921 only - Oct 25 23:00s 0 - Rule Belgium 1922 only - Mar 25 23:00s 1:00 S Rule Belgium 1922 1927 - Oct Sat>=1 23:00s 0 - Rule Belgium 1923 only - Apr 21 23:00s 1:00 S Rule Belgium 1924 only - Mar 29 23:00s 1:00 S Rule Belgium 1925 only - Apr 4 23:00s 1:00 S # DISHO writes that a royal decree of 1926-02-22 specified the Sun following 3rd # Sat in Apr (except if it's Easter, in which case it's one Sunday earlier), # to Sun following 1st Sat in Oct, and that a royal decree of 1928-09-15 # changed the transition times to 02:00 GMT. Rule Belgium 1926 only - Apr 17 23:00s 1:00 S Rule Belgium 1927 only - Apr 9 23:00s 1:00 S Rule Belgium 1928 only - Apr 14 23:00s 1:00 S Rule Belgium 1928 1938 - Oct Sun>=2 2:00s 0 - Rule Belgium 1929 only - Apr 21 2:00s 1:00 S Rule Belgium 1930 only - Apr 13 2:00s 1:00 S Rule Belgium 1931 only - Apr 19 2:00s 1:00 S Rule Belgium 1932 only - Apr 3 2:00s 1:00 S Rule Belgium 1933 only - Mar 26 2:00s 1:00 S Rule Belgium 1934 only - Apr 8 2:00s 1:00 S Rule Belgium 1935 only - Mar 31 2:00s 1:00 S Rule Belgium 1936 only - Apr 19 2:00s 1:00 S Rule Belgium 1937 only - Apr 4 2:00s 1:00 S Rule Belgium 1938 only - Mar 27 2:00s 1:00 S Rule Belgium 1939 only - Apr 16 2:00s 1:00 S Rule Belgium 1939 only - Nov 19 2:00s 0 - Rule Belgium 1940 only - Feb 25 2:00s 1:00 S Rule Belgium 1944 only - Sep 17 2:00s 0 - Rule Belgium 1945 only - Apr 2 2:00s 1:00 S Rule Belgium 1945 only - Sep 16 2:00s 0 - Rule Belgium 1946 only - May 19 2:00s 1:00 S Rule Belgium 1946 only - Oct 7 2:00s 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Brussels 0:17:30 - LMT 1880 0:17:30 - BMT 1892 May 1 00:17:30 0:00 - WET 1914 Nov 8 1:00 - CET 1916 May 1 0:00 1:00 C-Eur CE%sT 1918 Nov 11 11:00u 0:00 Belgium WE%sT 1940 May 20 2:00s 1:00 C-Eur CE%sT 1944 Sep 3 1:00 Belgium CE%sT 1977 1:00 EU CE%sT # Bulgaria # # From Plamen Simenov via Steffen Thorsen (1999-09-09): # A document of Government of Bulgaria (No. 94/1997) says: # EET -> EETDST is in 03:00 Local time in last Sunday of March ... # EETDST -> EET is in 04:00 Local time in last Sunday of October # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Bulg 1979 only - Mar 31 23:00 1:00 S Rule Bulg 1979 only - Oct 1 1:00 0 - Rule Bulg 1980 1982 - Apr Sat>=1 23:00 1:00 S Rule Bulg 1980 only - Sep 29 1:00 0 - Rule Bulg 1981 only - Sep 27 2:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Sofia 1:33:16 - LMT 1880 1:56:56 - IMT 1894 Nov 30 # Istanbul MT? 2:00 - EET 1942 Nov 2 3:00 1:00 C-Eur CE%sT 1945 1:00 - CET 1945 Apr 2 3:00 2:00 - EET 1979 Mar 31 23:00 2:00 Bulg EE%sT 1982 Sep 26 3:00 2:00 C-Eur EE%sT 1991 2:00 E-Eur EE%sT 1997 2:00 EU EE%sT # Cyprus # Please see the 'asia' file for Asia/Nicosia. # Czech Republic (Czechia) # Slovakia # # From Ivan Benovic (2024-01-30): # https://www.slov-lex.sk/pravne-predpisy/SK/ZZ/1946/54/ # (This is an official link to the Czechoslovak Summer Time Act of # March 8, 1946 that authorizes the Czechoslovak government to set the # exact dates of change to summer time and back to Central European Time. # The act also implicitly confirms Central European Time as the # official time zone of Czechoslovakia and currently remains in force # in both the Czech Republic and Slovakia.) # https://www.psp.cz/eknih/1945pns/tisky/t0216_00.htm # (This is a link to the original legislative proposal dating back to # February 22, 1946. The accompanying memorandum to the proposal says # that an advisory committee on European railroad transportation that # met in Brussels in October 1945 decided that the change of time # should be carried out in all participating countries in a strictly # coordinated manner....) # # From Paul Eggert (2024-01-30): # The source for Czech data is: Kdy za�íná a kon�í letní �as. # https://kalendar.beda.cz/kdy-zacina-a-konci-letni-cas # Its main text disagrees with its quoted sources only in 1918, # where the main text says spring and autumn transitions # occurred at 02:00 and 03:00 respectively (as usual), # whereas the 1918 source "Oznámení o zavedení letního �asu v roce 1918" # says transitions were at 01:00 and 02:00 respectively. # As the 1918 source appears to be a humorous piece, and it is # unlikely that Prague would have disagreed with its neighbors by an hour, # go with the main text for now. # # We know of no English-language name for historical Czech winter time; # abbreviate it as "GMT", as it happened to be GMT. # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Czech 1945 only - Apr Mon>=1 2:00s 1:00 S Rule Czech 1945 only - Oct 1 2:00s 0 - Rule Czech 1946 only - May 6 2:00s 1:00 S Rule Czech 1946 1949 - Oct Sun>=1 2:00s 0 - Rule Czech 1947 1948 - Apr Sun>=15 2:00s 1:00 S Rule Czech 1949 only - Apr 9 2:00s 1:00 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Prague 0:57:44 - LMT 1850 0:57:44 - PMT 1891 Oct # Prague Mean Time 1:00 C-Eur CE%sT 1945 May 9 1:00 Czech CE%sT 1946 Dec 1 3:00 # Vanguard section, for zic and other parsers that support negative DST. 1:00 -1:00 GMT 1947 Feb 23 2:00 # Rearguard section, for parsers lacking negative DST; see ziguard.awk. # 0:00 - GMT 1947 Feb 23 2:00 # End of rearguard section. 1:00 Czech CE%sT 1979 1:00 EU CE%sT # Faroe Is # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Atlantic/Faroe -0:27:04 - LMT 1908 Jan 11 # Tórshavn 0:00 - WET 1981 0:00 EU WE%sT # Greenland # # From Paul Eggert (2026-01-22): # During World War II, Greenland was effectively independent of Denmark and # observed daylight saving time. TIME, volume 37, page 23 (1941-04-21) # says, # "Penfield and West made their way to the U.S.'s most northerly consulate. # They were astonished to find that Greenlanders, with almost 24 hours of # sunlight a day during the summer, have daylight saving time." # As the details are unknown they are omitted from the data for now. # # During World War II, Germany maintained secret manned weather stations in # East Greenland and Franz Josef Land, but we don't know their time zones. # Also, they're likely out of scope for the database # as we lack resources to track every bit of military activity. # My source for this is Wilhelm Dege's book mentioned under Svalbard. # # From Paul Eggert (2017-12-10): # Greenland joined the European Communities as part of Denmark, # obtained home rule on 1979-05-01, and left the European Communities # on 1985-02-01. It therefore should have been using EU # rules at least through 1984. Shanks & Pottenger say Scoresbysund and Godthåb # used C-Eur rules after 1980, but IATA SSIM (1991/1996) says they use EU # rules since at least 1991. Assume EU rules since 1980. # From Gwillim Law (2001-06-06), citing # (2001-03-15), # and with translations corrected by Steffen Thorsen: # # Greenland has four local times, and the relation to UTC # is according to the following time line: # # The military zone near Thule UTC-4 # Standard Greenland time UTC-3 # Scoresbysund UTC-1 # Danmarkshavn UTC # # In the military area near Thule and in Danmarkshavn DST will not be # introduced. # From Rives McDow (2001-11-01): # # I correspond regularly with the Dansk Polarcenter, and wrote them at # the time to clarify the situation in Thule. Unfortunately, I have # not heard back from them regarding my recent letter. [But I have # info from earlier correspondence.] # # According to the center, a very small local time zone around Thule # Air Base keeps the time according to UTC-4, implementing daylight # savings using North America rules, changing the time at 02:00 local time.... # # The east coast of Greenland north of the community of Scoresbysund # uses UTC in the same way as in Iceland, year round, with no dst. # There are just a few stations on this coast, including the # Danmarkshavn ICAO weather station mentioned in your September 29th # email. The other stations are two sledge patrol stations in # Mestersvig and Daneborg, the air force base at Station Nord, and the # DPC research station at Zackenberg. # # Scoresbysund and two small villages nearby keep time UTC-1 and use # the same daylight savings time period as in West Greenland (Godthåb). # # The rest of Greenland, including Godthåb (this area, although it # includes central Greenland, is known as west Greenland), keeps time # UTC-3, with daylight savings methods according to European rules. # # It is common procedure to use UTC 0 in the wilderness of East and # North Greenland, because it is mainly Icelandic aircraft operators # maintaining traffic in these areas. However, the official status of # this area is that it sticks with Godthåb time. This area might be # considered a dual time zone in some respects because of this. # From Rives McDow (2001-11-19): # I heard back from someone stationed at Thule; the time change took place # there at 2:00 AM. # From Paul Eggert (2006-03-22): # From 1997 on the CIA map shows Danmarkshavn on GMT; # the 1995 map as like Godthåb. # For lack of better info, assume they were like Godthåb before 1996. # startkart.no says Thule does not observe DST, but this is clearly an error, # so go with Shanks & Pottenger for Thule transitions until this year. # For 2007 on assume Thule will stay in sync with US DST rules. # From J William Piggott (2016-02-20): # "Greenland north of the community of Scoresbysund" is officially named # "National Park" by Executive Order: # http://naalakkersuisut.gl/~/media/Nanoq/Files/Attached%20Files/Engelske-tekster/Legislation/Executive%20Order%20National%20Park.rtf # It is their only National Park. # From Jonas Nyrup (2022-11-24): # On last Saturday in October 2023 when DST ends America/Nuuk will switch # from -03/-02 to -02/-01 # https://sermitsiaq.ag/forslagtidsforskel-danmark-mindskes-sommertid-beholdes # ... # https://sermitsiaq.ag/groenland-skifte-tidszone-trods-bekymringer # # From Jürgen Appel (2022-11-25): # https://ina.gl/samlinger/oversigt-over-samlinger/samling/dagsordener/dagsorden.aspx?lang=da&day=24-11-2022 # # From Thomas M. Steenholdt (2022-12-02): # - The bill to move America/Nuuk from UTC-03 to UTC-02 passed. # - The bill to stop observing DST did not (Greenland will stop observing DST # when EU does). # Details on the implementation are here (section 6): # https://ina.gl/dvd/EM%202022/pdf/media/2553529/pkt17_em2022_tidens_bestemmelse_bem_da.pdf # This is how the change will be implemented: # 1. The shift *to* DST in 2023 happens as normal. # 2. The shift *from* DST in 2023 happens as normal, but coincides with the # shift to UTC-02 normaltime (people will not change their clocks here). # 3. After this, DST is still observed, but as -02/-01 instead of -03/-02. # # From Múte Bourup Egede via Jógvan Svabo Samuelsen (2023-03-15): # Greenland will not switch to Daylight Saving Time this year, 2023, # because the standard time for Greenland will change from UTC -3 to UTC -2. # However, Greenland will change to Daylight Saving Time again in 2024 # and onwards. # From Jule Dabars (2023-10-29): # https://www.dr.dk/nyheder/seneste/i-nat-skal-uret-stilles-en-time-tilbage-men-foerste-gang-sker-det-ikke-i-groenland # with a link to that page: # https://naalakkersuisut.gl/Nyheder/2023/10/2710_sommertid # ... Ittoqqortoormiit joins the time of Nuuk at March 2024. # What would mean that America/Scoresbysund would either be in -01 year round # or in -02/-01 like America/Nuuk, but no longer in -01/+00. # # From Paul Eggert (2023-10-29): # For now, assume it will be like America/Nuuk. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Thule 1991 1992 - Mar lastSun 2:00 1:00 D Rule Thule 1991 1992 - Sep lastSun 2:00 0 S Rule Thule 1993 2006 - Apr Sun>=1 2:00 1:00 D Rule Thule 1993 2006 - Oct lastSun 2:00 0 S Rule Thule 2007 max - Mar Sun>=8 2:00 1:00 D Rule Thule 2007 max - Nov Sun>=1 2:00 0 S # # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Danmarkshavn -1:14:40 - LMT 1916 Jul 28 -3:00 - %z 1980 Apr 6 2:00 -3:00 EU %z 1996 0:00 - GMT # # Use the old name Scoresbysund, as the current name Ittoqqortoormiit # exceeds tzdb's 14-letter limit and has no common English abbreviation. Zone America/Scoresbysund -1:27:52 - LMT 1916 Jul 28 # Ittoqqortoormiit -2:00 - %z 1980 Apr 6 2:00 -2:00 C-Eur %z 1981 Mar 29 -1:00 EU %z 2024 Mar 31 -2:00 EU %z Zone America/Nuuk -3:26:56 - LMT 1916 Jul 28 # Godthåb -3:00 - %z 1980 Apr 6 2:00 -3:00 EU %z 2023 Mar 26 1:00u -2:00 - %z 2023 Oct 29 1:00u -2:00 EU %z Zone America/Thule -4:35:08 - LMT 1916 Jul 28 # Pituffik -4:00 Thule A%sT # Estonia # # From Paul Eggert (2016-03-18): # The 1989 transition is from USSR act No. 227 (1989-03-14). # # From Peter Ilieve (1994-10-15): # A relative in Tallinn confirms the accuracy of the data for 1989 onwards # [through 1994] and gives the legal authority for it, # a regulation of the Government of Estonia, No. 111 of 1989.... # # From Peter Ilieve (1996-10-28): # [IATA SSIM (1992/1996) claims that the Baltic republics switch at 01:00s, # but a relative confirms that Estonia still switches at 02:00s, writing:] # "I do not [know] exactly but there are some little different # (confusing) rules for International Air and Railway Transport Schedules # conversion in Sunday connected with end of summer time in Estonia.... # A discussion is running about the summer time efficiency and effect on # human physiology. It seems that Estonia maybe will not change to # summer time next spring." # From Peter Ilieve (1998-11-04), heavily edited: # The 1998-09-22 Estonian time law # http://trip.rk.ee/cgi-bin/thw?${BASE}=akt&${OOHTML}=rtd&TA=1998&TO=1&AN=1390 # refers to the Eighth Directive and cites the association agreement between # the EU and Estonia, ratified by the Estonian law (RT II 1995, 22-27, 120). # # I also asked [my relative] whether they use any standard abbreviation # for their standard and summer times. He says no, they use "suveaeg" # (summer time) and "talveaeg" (winter time). # From The Baltic Times (1999-09-09) # via Steffen Thorsen: # This year will mark the last time Estonia shifts to summer time, # a council of the ruling coalition announced Sept. 6.... # But what this could mean for Estonia's chances of joining the European # Union are still unclear. In 1994, the EU declared summer time compulsory # for all member states until 2001. Brussels has yet to decide what to do # after that. # From Mart Oruaas (2000-01-29): # Regulation No. 301 (1999-10-12) obsoletes previous regulation # No. 206 (1998-09-22) and thus sticks Estonia to +02:00 GMT for all # the year round. The regulation is effective 1999-11-01. # From Toomas Soome (2002-02-21): # The Estonian government has changed once again timezone politics. # Now we are using again EU rules. # # From Urmet Jänes (2002-03-28): # The legislative reference is Government decree No. 84 on 2002-02-21. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Tallinn 1:39:00 - LMT 1880 1:39:00 - TMT 1918 Feb # Tallinn Mean Time 1:00 C-Eur CE%sT 1919 Jul 1:39:00 - TMT 1921 May 2:00 - EET 1940 Aug 6 3:00 - MSK 1941 Sep 15 1:00 C-Eur CE%sT 1944 Sep 22 3:00 Russia MSK/MSD 1989 Mar 26 2:00s 2:00 1:00 EEST 1989 Sep 24 2:00s 2:00 C-Eur EE%sT 1998 Sep 22 2:00 EU EE%sT 1999 Oct 31 4:00 2:00 - EET 2002 Feb 21 2:00 EU EE%sT # Finland # From Hannu Strang (1994-09-25 06:03:37 UTC): # Well, here in Helsinki we're just changing from summer time to regular one, # and it's supposed to change at 4am... # From Janne Snabb (2010-07-15): # # I noticed that the Finland data is not accurate for years 1981 and 1982. # During these two first trial years the DST adjustment was made one hour # earlier than in forthcoming years. Starting 1983 the adjustment was made # according to the central European standards. # # This is documented in Heikki Oja: Aikakirja 2007, published by The Almanac # Office of University of Helsinki, ISBN 952-10-3221-9, available online (in # Finnish) at # https://almanakka.helsinki.fi/aikakirja/Aikakirja2007kokonaan.pdf # # Page 105 (56 in PDF version) has a handy table of all past daylight savings # transitions. It is easy enough to interpret without Finnish skills. # # This is also confirmed by Finnish Broadcasting Company's archive at: # http://www.yle.fi/elavaarkisto/?s=s&g=1&ag=5&t=&a=3401 # # The news clip from 1981 says that "the time between 2 and 3 o'clock does not # exist tonight." # From Konstantin Hyppönen (2014-06-13): # [Heikki Oja's book Aikakirja 2013] # https://almanakka.helsinki.fi/images/aikakirja/Aikakirja2013kokonaan.pdf # pages 104-105, including a scan from a newspaper published on Apr 2 1942 # say that ... [o]n Apr 2 1942, 24 o'clock (which means Apr 3 1942, # 00:00), clocks were moved one hour forward. The newspaper # mentions "on the night from Thursday to Friday".... # On Oct 4 1942, clocks were moved at 1:00 one hour backwards. # # From Paul Eggert (2014-06-14): # Go with Oja over Shanks. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Finland 1942 only - Apr 2 24:00 1:00 S Rule Finland 1942 only - Oct 4 1:00 0 - Rule Finland 1981 1982 - Mar lastSun 2:00 1:00 S Rule Finland 1981 1982 - Sep lastSun 3:00 0 - # Milne says Helsinki (Helsingfors) time was 1:39:49.2 (official document). # Zone NAME STDOFF RULES FORMAT [UNTIL] #STDOFF 1:39:49.2 Zone Europe/Helsinki 1:39:49 - LMT 1878 May 31 1:39:49 - HMT 1921 May # Helsinki Mean Time 2:00 Finland EE%sT 1983 2:00 EU EE%sT # France # Monaco # From Robert H. van Gent (2025-07-21): # The most recent issue of the Annuaire [par le Bureau des Longitudes] # on Gallica (2021) ... lists information for France # https://gallica.bnf.fr/ark:/12148/bpt6k9127672b/f52.item # From Paul Eggert (2025-07-21): # Go with the 2020 Annuaire (published 2021) except as noted below. # From Ciro Discepolo (2000-12-20): # # Henri Le Corre, Régimes horaires pour le monde entier, Éditions # Traditionnelles - Paris 2 books, 1993 # # Gabriel, Traité de l'heure dans le monde, Guy Trédaniel, # Paris, 1991 # # Françoise Gauquelin, Problèmes de l'heure résolus en astrologie, # Guy Trédaniel, Paris 1987 # From Michael Deckers (2020-06-11): # the law of 1891 # was published on 1891-03-15, so it could only take force on 1891-03-16. # From Michael Deckers (2020-06-10): # Le Gaulois, 1911-03-11, page 1/6, online at # https://www.retronews.fr/societe/echo-de-presse/2018/01/29/1911-change-lheure-de-paris # ... [ Instantly, all pressure driven clock dials halted... Nine minutes and # twenty-one seconds later the hands resumed their circular motion. ] # There are also precise reports about how the change was prepared in train # stations: all the publicly visible clocks stopped at midnight railway time # (or were covered), only the chief of service had a watch, labeled # "Heure ancienne", that he kept running until it reached 00:04:21, when # he announced "Heure nouvelle". See the "Le Petit Journal 1911-03-11". # https://gallica.bnf.fr/ark:/12148/bpt6k6192911/f1.item.zoom # # From Michael Deckers (2020-06-12): # That "all French clocks stopped" for 00:09:21 is a misreading of French # newspapers; this sort of adjustment applies only to certain # remote-controlled clocks ("pendules pneumatiques", of which there existed # perhaps a dozen in Paris, and which simply could not be set back remotely), # but not to all the clocks in all French towns and villages. For instance, # the following story in the "Courrier de Saône-et-Loire" 1911-03-11, page 2: # only works if legal time was stepped back (was not monotone): ... # [One can observe that children who had been born at midnight less 5 # minutes and who had died at midnight of the old time, would turn out to # be dead before being born, time having been set back and having # suppressed 9 minutes and 25 seconds of their existence, that is, more # than they could spend.] # # From Paul Eggert (2020-06-12): # French time in railway stations was legally five minutes behind civil time, # which explains why railway "old time" ran to 00:04:21 instead of to 00:09:21. # The law's text (which Michael Deckers noted is at # ) says only that # at 1911-03-11 00:00 legal time was that of Paris mean time delayed by # nine minutes and twenty-one seconds, and does not say how the # transition from Paris mean time was to occur. # # tzdb has no way to represent stopped clocks. As the railway practice # was to keep a watch running on "old time" to decide when to restart # the other clocks, this could be modeled as a transition for "old time" at # 00:09:21. However, since the law was ambiguous and clocks outside railway # stations were probably done haphazardly with the popular impression being # that the transition was done at 00:00 "old time", simply leave the time # blank; this causes zic to default to 00:00 "old time" which is good enough. # Do something similar for the 1891-03-16 transition. There are similar # problems in Algiers, Monaco and Tunis. # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule France 1916 only - Jun 14 23:00s 1:00 S Rule France 1916 1919 - Oct Sun>=1 23:00s 0 - Rule France 1917 only - Mar 24 23:00s 1:00 S Rule France 1918 only - Mar 9 23:00s 1:00 S Rule France 1919 only - Mar 1 23:00s 1:00 S Rule France 1920 only - Feb 14 23:00s 1:00 S Rule France 1920 only - Oct 23 23:00s 0 - Rule France 1921 only - Mar 14 23:00s 1:00 S Rule France 1921 only - Oct 25 23:00s 0 - Rule France 1922 only - Mar 25 23:00s 1:00 S # From Robert H. van Gent (2025-07-22): # There is a curious history behind the erroneous date for the start of # daylight saving in France in 1923 as listed in the current issues of # the Annuaire du Bureau des Longitudes. [See:] # https://lists.iana.org/hyperkitty/list/tz@iana.org/message/MYQEJMSXO2AIEZ3UIXZKMTTAIPY7KNT2/ # From Brian Inglis (2025-07-23): # Légifrance JORF No. 0073 du 15 mars 1922 # https://www.legifrance.gouv.fr/jorf/jo/id/JORFCONT000000008324 # Légifrance JORF No. 0139 du 25 mai 1923 # https://www.legifrance.gouv.fr/jorf/jo/id/JORFCONT000000008416 # From Paul Eggert (2025-07-23): # The latter specifies March's last Saturday at 23:00 to October's first # Saturday at 24:00, except that if neighboring allies agree the dates # can be moved to April's third Saturday and September's third Saturday. # Apparently spring 1923 was tricky. DISHO writes that in 1930, # because of Easter, the transitions were Apr 12 and Oct 5. # Use the 2020 Annuaire dates, except for spring 1923 where # Shanks & Pottenger's May 26 matches the dates given in the 1924 and # 1961-2001 issues of the Annuaire. Rule France 1922 1938 - Oct Sat>=1 23:00s 0 - Rule France 1923 only - May 26 23:00s 1:00 S Rule France 1924 only - Mar 29 23:00s 1:00 S Rule France 1925 only - Apr 4 23:00s 1:00 S Rule France 1926 only - Apr 17 23:00s 1:00 S Rule France 1927 only - Apr 9 23:00s 1:00 S Rule France 1928 only - Apr 14 23:00s 1:00 S Rule France 1929 only - Apr 20 23:00s 1:00 S Rule France 1930 only - Apr 12 23:00s 1:00 S Rule France 1931 only - Apr 18 23:00s 1:00 S Rule France 1932 only - Apr 2 23:00s 1:00 S Rule France 1933 only - Mar 25 23:00s 1:00 S Rule France 1934 only - Apr 7 23:00s 1:00 S Rule France 1935 only - Mar 30 23:00s 1:00 S Rule France 1936 only - Apr 18 23:00s 1:00 S Rule France 1937 only - Apr 3 23:00s 1:00 S Rule France 1938 only - Mar 26 23:00s 1:00 S Rule France 1939 only - Apr 15 23:00s 1:00 S Rule France 1939 only - Nov 18 23:00s 0 - Rule France 1940 only - Feb 25 2:00 1:00 S # The French rules for 1941-1944 were not used in Paris, but Shanks & Pottenger # write that they were used in Monaco and in many French locations. # Le Corre writes that the upper limit of the free zone was Arnéguy, Orthez, # Mont-de-Marsan, Bazas, Langon, Lamothe-Montravel, Marœuil, La # Rochefoucauld, Champagne-Mouton, La Roche-Posay, La Haye-Descartes, # Loches, Montrichard, Vierzon, Bourges, Moulins, Digoin, # Paray-le-Monial, Montceau-les-Mines, Chalon-sur-Saône, Arbois, # Dole, Morez, St-Claude, and Collonges (Haute-Savoie). Rule France 1941 only - May 5 0:00 2:00 M # Midsummer # Shanks & Pottenger say this transition occurred at Oct 6 1:00, # but go with Denis Excoffier (1997-12-12), # who quotes the Ephémérides astronomiques for 1998 from Bureau des Longitudes # as saying 5/10/41 22hUT. Rule France 1941 only - Oct 6 0:00 1:00 S Rule France 1942 only - Mar 9 0:00 2:00 M Rule France 1942 only - Nov 2 3:00 1:00 S Rule France 1943 only - Mar 29 2:00 2:00 M Rule France 1943 only - Oct 4 3:00 1:00 S Rule France 1944 only - Apr 3 2:00 2:00 M Rule France 1944 only - Oct 8 1:00 1:00 S Rule France 1945 only - Apr 2 2:00 2:00 M Rule France 1945 only - Sep 16 3:00 0 - # Shanks & Pottenger give Mar 28 2:00 and Sep 26 3:00; # go with Excoffier's 28/3/76 0hUT and 25/9/76 23hUT. Rule France 1976 only - Mar 28 1:00 1:00 S Rule France 1976 only - Sep 26 1:00 0 - # Howse writes that the time in France was officially based # on PMT-0:09:21 until 1978-08-09, when the time base finally switched to UTC. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Paris 0:09:21 - LMT 1891 Mar 16 0:09:21 - PMT 1911 Mar 11 # Paris Mean Time # Shanks & Pottenger give 1940 Jun 14 0:00; go with Excoffier and Le Corre. 0:00 France WE%sT 1940 Jun 14 23:00 # Le Corre says Paris stuck with occupied-France time after the liberation; # go with Shanks & Pottenger. 1:00 C-Eur CE%sT 1944 Aug 25 0:00 France WE%sT 1945 Sep 16 3:00 1:00 France CE%sT 1977 1:00 EU CE%sT # Denmark # Germany # Norway # Sweden # From Markus Kuhn (1998-09-29): # The German time zone web site by the Physikalisch-Technische # Bundesanstalt contains DST information back to 1916. # [See tz-link.html for the URL.] # From Jörg Schilling (2002-10-23): # In 1945, Berlin was switched to Moscow Summer time (GMT+4) by # https://www.dhm.de/lemo/html/biografien/BersarinNikolai/ # General [Nikolai] Bersarin. # From Paul Eggert (2003-03-08): # http://www.parlament-berlin.de/pds-fraktion.nsf/727459127c8b66ee8525662300459099/defc77cb784f180ac1256c2b0030274b/$FILE/bersarint.pdf # says that Bersarin issued an order to use Moscow time on May 20. # However, Moscow did not observe daylight saving in 1945, so # this was equivalent to UT +03, not +04. # Svalbard & Jan Mayen # From Steffen Thorsen (2001-05-01): # Although I could not find it explicitly, it seems that Jan Mayen and # Svalbard have been using the same time as Norway at least since the # time they were declared as parts of Norway. Svalbard was declared # as a part of Norway by law of 1925-07-17 no 11, section 4 and Jan # Mayen by law of 1930-02-27 no 2, section 2. (From # and # ). The law/regulation # for normal/standard time in Norway is from 1894-06-29 no 1 (came # into operation on 1895-01-01) and Svalbard/Jan Mayen seem to be a # part of this law since 1925/1930. (From # ) I have not been # able to find if Jan Mayen used a different time zone (e.g. -0100) # before 1930. Jan Mayen has only been "inhabited" since 1921 by # Norwegian meteorologists and maybe used the same time as Norway ever # since 1921. Svalbard (Arctic/Longyearbyen) has been inhabited since # before 1895, and therefore probably changed the local time somewhere # between 1895 and 1925 (inclusive). # From Paul Eggert (2013-09-04): # # Actually, Jan Mayen was never occupied by Germany during World War II, # so it must have diverged from Oslo time during the war, as Oslo was # keeping Berlin time. # # says that the meteorologists # burned down their station in 1940 and left the island, but returned in # 1941 with a small Norwegian garrison and continued operations despite # frequent air attacks from Germans. In 1943 the Americans established a # radiolocating station on the island, called "Atlantic City". Possibly # the UT offset changed during the war, but I think it unlikely that # Jan Mayen used German daylight-saving rules. # # Svalbard is more complicated, as it was raided in August 1941 by an # Allied party that evacuated the civilian population to England (says # ). The Svalbard FAQ # says that the Germans were # expelled on 1942-05-14. However, small parties of Germans did return, # and according to Wilhelm Dege's book "War North of 80" (1954) # http://www.ucalgary.ca/UofC/departments/UP/1-55238/1-55238-110-2.html # the German armed forces at the Svalbard weather station code-named # Haudegen did not surrender to the Allies until September 1945. # # All these events predate our cutoff date of 1970, so use Europe/Berlin # for these regions. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Germany 1946 only - Apr 14 2:00s 1:00 S Rule Germany 1946 only - Oct 7 2:00s 0 - Rule Germany 1947 1949 - Oct Sun>=1 2:00s 0 - # https://www.ptb.de/cms/en/ptb/fachabteilungen/abt4/fb-44/ag-441/realisation-of-legal-time-in-germany/dst-and-midsummer-dst-in-germany-until-1979.html # says the following transition occurred at 3:00 MEZ, not the 2:00 MEZ # given in Shanks & Pottenger. Go with the PTB. Rule Germany 1947 only - Apr 6 3:00s 1:00 S Rule Germany 1947 only - May 11 2:00s 2:00 M Rule Germany 1947 only - Jun 29 3:00 1:00 S Rule Germany 1948 only - Apr 18 2:00s 1:00 S Rule Germany 1949 only - Apr 10 2:00s 1:00 S Rule SovietZone 1945 only - May 24 2:00 2:00 M # Midsummer Rule SovietZone 1945 only - Sep 24 3:00 1:00 S Rule SovietZone 1945 only - Nov 18 2:00s 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Berlin 0:53:28 - LMT 1893 Apr 1:00 C-Eur CE%sT 1945 May 24 2:00 1:00 SovietZone CE%sT 1946 1:00 Germany CE%sT 1980 1:00 EU CE%sT # Georgia # Please see the "asia" file for Asia/Tbilisi. # Herodotus (Histories, IV.45) says Georgia north of the Phasis (now Rioni) # is in Europe. Our reference location Tbilisi is in the Asian part. # Gibraltar # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Gibraltar -0:21:24 - LMT 1880 Aug 2 0:00 GB-Eire %s 1957 Apr 14 2:00 1:00 - CET 1982 1:00 EU CE%sT # Greece # Rule NAME FROM TO - IN ON AT SAVE LETTER/S # Whitman gives 1932 Jul 5 - Nov 1; go with Shanks & Pottenger. Rule Greece 1932 only - Jul 7 0:00 1:00 S Rule Greece 1932 only - Sep 1 0:00 0 - # Whitman gives 1941 Apr 25 - ?; go with Shanks & Pottenger. Rule Greece 1941 only - Apr 7 0:00 1:00 S # Whitman gives 1942 Feb 2 - ?; go with Shanks & Pottenger. Rule Greece 1942 only - Nov 2 3:00 0 - Rule Greece 1943 only - Mar 30 0:00 1:00 S Rule Greece 1943 only - Oct 4 0:00 0 - # Whitman gives 1944 Oct 3 - Oct 31; go with Shanks & Pottenger. Rule Greece 1952 only - Jul 1 0:00 1:00 S Rule Greece 1952 only - Nov 2 0:00 0 - Rule Greece 1975 only - Apr 12 0:00s 1:00 S Rule Greece 1975 only - Nov 26 0:00s 0 - Rule Greece 1976 only - Apr 11 2:00s 1:00 S Rule Greece 1976 only - Oct 10 2:00s 0 - Rule Greece 1977 1978 - Apr Sun>=1 2:00s 1:00 S Rule Greece 1977 only - Sep 26 2:00s 0 - Rule Greece 1978 only - Sep 24 4:00 0 - Rule Greece 1979 only - Apr 1 9:00 1:00 S Rule Greece 1979 only - Sep 29 2:00 0 - Rule Greece 1980 only - Apr 1 0:00 1:00 S Rule Greece 1980 only - Sep 28 0:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Athens 1:34:52 - LMT 1895 Sep 14 1:34:52 - AMT 1916 Jul 28 0:01 # Athens MT 2:00 Greece EE%sT 1941 Apr 30 1:00 Greece CE%sT 1944 Apr 4 2:00 Greece EE%sT 1981 # Shanks & Pottenger say it switched to C-Eur in 1981; # go with EU rules instead, since Greece joined Jan 1. 2:00 EU EE%sT # Hungary # From Michael Deckers (2020-06-09): # an Austrian encyclopedia of railroads of 1913, online at # http://www.zeno.org/Roell-1912/A/Eisenbahnzeit # says that the switch [to CET] happened on 1890-11-01. # From Géza Nyáry (2020-06-07): # Data for 1918-1983 are based on the archive database of Library Hungaricana. # The dates are collected from original, scanned governmental orders, # bulletins, instructions and public press. # [See URLs below.] # Rule NAME FROM TO - IN ON AT SAVE LETTER/S # https://library.hungaricana.hu/hu/view/OGYK_RT_1918/?pg=238 # https://library.hungaricana.hu/hu/view/OGYK_RT_1919/?pg=808 # https://library.hungaricana.hu/hu/view/OGYK_RT_1920/?pg=201 Rule Hungary 1918 1919 - Apr 15 2:00 1:00 S Rule Hungary 1918 1920 - Sep Mon>=15 3:00 0 - Rule Hungary 1920 only - Apr 5 2:00 1:00 S # https://library.hungaricana.hu/hu/view/OGYK_RT_1945/?pg=882 Rule Hungary 1945 only - May 1 23:00 1:00 S Rule Hungary 1945 only - Nov 1 1:00 0 - # https://library.hungaricana.hu/hu/view/Delmagyarorszag_1946_03/?pg=49 Rule Hungary 1946 only - Mar 31 2:00s 1:00 S # https://library.hungaricana.hu/hu/view/Delmagyarorszag_1946_09/?pg=54 Rule Hungary 1946 only - Oct 7 2:00 0 - # https://library.hungaricana.hu/hu/view/KulfBelfHirek_1947_04_1__001-123/?pg=90 # https://library.hungaricana.hu/hu/view/DunantuliNaplo_1947_09/?pg=128 # https://library.hungaricana.hu/hu/view/KulfBelfHirek_1948_03_3__001-123/?pg=304 # https://library.hungaricana.hu/hu/view/Zala_1948_09/?pg=64 # https://library.hungaricana.hu/hu/view/SatoraljaujhelyiLeveltar_ZempleniNepujsag_1948/?pg=53 # https://library.hungaricana.hu/hu/view/SatoraljaujhelyiLeveltar_ZempleniNepujsag_1948/?pg=160 # https://library.hungaricana.hu/hu/view/UjSzo_1949_01-04/?pg=102 # https://library.hungaricana.hu/hu/view/KeletMagyarorszag_1949_03/?pg=96 # https://library.hungaricana.hu/hu/view/Delmagyarorszag_1949_09/?pg=94 Rule Hungary 1947 1949 - Apr Sun>=4 2:00s 1:00 S Rule Hungary 1947 1949 - Oct Sun>=1 2:00s 0 - # https://library.hungaricana.hu/hu/view/DTT_KOZL_TanacsokKozlonye_1954/?pg=513 Rule Hungary 1954 only - May 23 0:00 1:00 S Rule Hungary 1954 only - Oct 3 0:00 0 - # https://library.hungaricana.hu/hu/view/DTT_KOZL_TanacsokKozlonye_1955/?pg=398 Rule Hungary 1955 only - May 22 2:00 1:00 S Rule Hungary 1955 only - Oct 2 3:00 0 - # https://library.hungaricana.hu/hu/view/HevesMegyeiNepujsag_1956_06/?pg=0 # https://library.hungaricana.hu/hu/view/EszakMagyarorszag_1956_06/?pg=6 # https://library.hungaricana.hu/hu/view/SzolnokMegyeiNeplap_1957_04/?pg=120 # https://library.hungaricana.hu/hu/view/PestMegyeiHirlap_1957_09/?pg=143 Rule Hungary 1956 1957 - Jun Sun>=1 2:00 1:00 S Rule Hungary 1956 1957 - Sep lastSun 3:00 0 - # https://library.hungaricana.hu/hu/view/DTT_KOZL_TanacsokKozlonye_1980/?pg=189 Rule Hungary 1980 only - Apr 6 0:00 1:00 S Rule Hungary 1980 only - Sep 28 1:00 0 - # https://library.hungaricana.hu/hu/view/DTT_KOZL_TanacsokKozlonye_1980/?pg=1227 # https://library.hungaricana.hu/hu/view/Delmagyarorszag_1981_01/?pg=79 # https://library.hungaricana.hu/hu/view/DTT_KOZL_TanacsokKozlonye_1982/?pg=115 # https://library.hungaricana.hu/hu/view/DTT_KOZL_TanacsokKozlonye_1983/?pg=85 Rule Hungary 1981 1983 - Mar lastSun 0:00 1:00 S Rule Hungary 1981 1983 - Sep lastSun 1:00 0 - # # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Budapest 1:16:20 - LMT 1890 Nov 1 1:00 C-Eur CE%sT 1918 # https://library.hungaricana.hu/hu/view/OGYK_RT_1941/?pg=1204 # https://library.hungaricana.hu/hu/view/OGYK_RT_1942/?pg=3955 1:00 Hungary CE%sT 1941 Apr 7 23:00 1:00 C-Eur CE%sT 1945 1:00 Hungary CE%sT 1984 1:00 EU CE%sT # Italy # San Marino # Vatican City # # From Paul Eggert (2001-03-06): # Sicily and Sardinia each had their own time zones from 1866 to 1893, # called Palermo Time (+00:53:28) and Cagliari Time (+00:36:32). # During World War II, German-controlled Italy used German time. # But these events all occurred before the 1970 cutoff, # so record only the time in Rome. # # From Stephen Trainor (2019-05-06): # http://www.ac-ilsestante.it/MERIDIANE/ora_legale/ORA_LEGALE_ESTIVA_IN_ITALIA.htm # ... the [1866] law went into effect on 12 December 1866, rather than # the date of the decree (22 Sep 1866) # https://web.archive.org/web/20070824155341/http://www.iav.it/planetario/didastro/didastro/english.htm # ... "In Italy in 1866 there were 6 railway times (Torino, Verona, Firenze, # Roma, Napoli, Palermo). On that year it was decided to unify them, adopting # the average time of Rome (even if this city was not yet part of the # kingdom). On the 12th December 1866, on the starting of the winter time # table, it took effect in the railways, the post office and the telegraph, # not only for the internal service but also for the public.... Milano set # the public watches on the Rome time on the same day (12th December 1866), # Torino and Bologna on the 1st January 1867, Venezia the 1st May 1880 and the # last city was Cagliari in 1886." # # From Luigi Rosa (2019-05-07): # this is the scan of the decree: # http://www.radiomarconi.com/marconi/filopanti/1866c.jpg # # From Michael Deckers (2016-10-24): # http://www.ac-ilsestante.it/MERIDIANE/ora_legale quotes a law of 1893-08-10 # ... [translated as] "The preceding dispositions will enter into # force at the instant at which, according to the time specified in # the 1st article, the 1st of November 1893 will begin...." # # From Pierpaolo Bernardi (2016-10-20): # The authoritative source for time in Italy is the national metrological # institute, which has a summary page of historical DST data at # http://www.inrim.it/res/tf/ora_legale_i.shtml # [now at http://oldsite.inrim.it/res/tf/ora_legale_i.shtml as of 2017] # (2016-10-24): # http://www.renzobaldini.it/le-ore-legali-in-italia/ # has still different data for 1944. It divides Italy in two, as # there were effectively two governments at the time, north of Gothic # Line German controlled territory, official government RSI, and south # of the Gothic Line, controlled by allied armies. # # From Brian Inglis (2016-10-23): # Viceregal LEGISLATIVE DECREE. 14 September 1944, no. 219. # Restoration of Standard Time. (044U0219) (OJ 62 of 30.9.1944) ... # Given the R. law decreed on 1944-03-29, no. 92, by which standard time is # advanced to sixty minutes later starting at hour two on 1944-04-02; ... # Starting at hour three on the date 1944-09-17 standard time will be resumed. # # From Alois Treindl (2019-07-02): # I spent 6 Euros to buy two archive copies of Il Messaggero, a Roman paper, # for 1 and 2 April 1944. The edition of 2 April has this note: "Tonight at 2 # am, put forward the clock by one hour. Remember that in the night between # today and Monday the 'ora legale' will come in force again." That makes it # clear that in Rome the change was on Monday, 3 April 1944 at 2 am. # # From Paul Eggert (2021-10-05): # Go with INRiM for DST rules, except as corrected by Inglis for 1944 # for the Kingdom of Italy. This is consistent with Renzo Baldini. # Model Rome's occupation by using C-Eur rules from 1943-09-10 # to 1944-06-04; although Rome was an open city during this period, it # was effectively controlled by Germany. Using C-Eur is consistent # with Treindl's comment about Rome in April 1944, as the "Rule Italy" # lines during German occupation do not affect Europe/Rome # (though they do affect Europe/Malta). # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Italy 1916 only - Jun 3 24:00 1:00 S Rule Italy 1916 1917 - Sep 30 24:00 0 - Rule Italy 1917 only - Mar 31 24:00 1:00 S Rule Italy 1918 only - Mar 9 24:00 1:00 S Rule Italy 1918 only - Oct 6 24:00 0 - Rule Italy 1919 only - Mar 1 24:00 1:00 S Rule Italy 1919 only - Oct 4 24:00 0 - Rule Italy 1920 only - Mar 20 24:00 1:00 S Rule Italy 1920 only - Sep 18 24:00 0 - Rule Italy 1940 only - Jun 14 24:00 1:00 S Rule Italy 1942 only - Nov 2 2:00s 0 - Rule Italy 1943 only - Mar 29 2:00s 1:00 S Rule Italy 1943 only - Oct 4 2:00s 0 - Rule Italy 1944 only - Apr 2 2:00s 1:00 S Rule Italy 1944 only - Sep 17 2:00s 0 - Rule Italy 1945 only - Apr 2 2:00 1:00 S Rule Italy 1945 only - Sep 15 1:00 0 - Rule Italy 1946 only - Mar 17 2:00s 1:00 S Rule Italy 1946 only - Oct 6 2:00s 0 - Rule Italy 1947 only - Mar 16 0:00s 1:00 S Rule Italy 1947 only - Oct 5 0:00s 0 - Rule Italy 1948 only - Feb 29 2:00s 1:00 S Rule Italy 1948 only - Oct 3 2:00s 0 - Rule Italy 1966 1968 - May Sun>=22 0:00s 1:00 S Rule Italy 1966 only - Sep 24 24:00 0 - Rule Italy 1967 1969 - Sep Sun>=22 0:00s 0 - Rule Italy 1969 only - Jun 1 0:00s 1:00 S Rule Italy 1970 only - May 31 0:00s 1:00 S Rule Italy 1970 only - Sep lastSun 0:00s 0 - Rule Italy 1971 1972 - May Sun>=22 0:00s 1:00 S Rule Italy 1971 only - Sep lastSun 0:00s 0 - Rule Italy 1972 only - Oct 1 0:00s 0 - Rule Italy 1973 only - Jun 3 0:00s 1:00 S Rule Italy 1973 1974 - Sep lastSun 0:00s 0 - Rule Italy 1974 only - May 26 0:00s 1:00 S Rule Italy 1975 only - Jun 1 0:00s 1:00 S Rule Italy 1975 1977 - Sep lastSun 0:00s 0 - Rule Italy 1976 only - May 30 0:00s 1:00 S Rule Italy 1977 1979 - May Sun>=22 0:00s 1:00 S Rule Italy 1978 only - Oct 1 0:00s 0 - Rule Italy 1979 only - Sep 30 0:00s 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Rome 0:49:56 - LMT 1866 Dec 12 0:49:56 - RMT 1893 Oct 31 23:00u # Rome Mean 1:00 Italy CE%sT 1943 Sep 10 1:00 C-Eur CE%sT 1944 Jun 4 1:00 Italy CE%sT 1980 1:00 EU CE%sT # Latvia # From Liene Kanepe (1998-09-17): # I asked about this matter Scientific Secretary of the Institute of Astronomy # of The University of Latvia Dr. paed Mr. Ilgonis Vilks. I also searched the # correct data in juridical acts and I found some juridical documents about # changes in the counting of time in Latvia from 1981.... # # Act No. 35 of the Council of Ministers of Latvian SSR of 1981-01-22 ... # according to the Act No. 925 of the Council of Ministers of USSR of 1980-10-24 # ...: all year round the time of 2nd time zone + 1 hour, in addition turning # the hands of the clock 1 hour forward on 1 April at 00:00 (GMT 31 March 21:00) # and 1 hour backward on the 1 October at 00:00 (GMT 30 September 20:00). # # Act No. 592 of the Council of Ministers of Latvian SSR of 1984-09-24 ... # according to the Act No. 967 of the Council of Ministers of USSR of 1984-09-13 # ...: all year round the time of 2nd time zone + 1 hour, in addition turning # the hands of the clock 1 hour forward on the last Sunday of March at 02:00 # (GMT 23:00 on the previous day) and 1 hour backward on the last Sunday of # September at 03:00 (GMT 23:00 on the previous day). # # Act No. 81 of the Council of Ministers of Latvian SSR of 1989-03-22 ... # according to the Act No. 227 of the Council of Ministers of USSR of 1989-03-14 # ...: since the last Sunday of March 1989 in Lithuanian SSR, Latvian SSR, # Estonian SSR and Kaliningrad region of Russian Federation all year round the # time of 2nd time zone (Moscow time minus one hour). On the territory of Latvia # transition to summer time is performed on the last Sunday of March at 02:00 # (GMT 00:00), turning the hands of the clock 1 hour forward. The end of # daylight saving time is performed on the last Sunday of September at 03:00 # (GMT 00:00), turning the hands of the clock 1 hour backward. Exception is # 1989-03-26, when we must not turn the hands of the clock.... # # The Regulations of the Cabinet of Ministers of the Republic of Latvia of # 1997-01-21 on transition to Summer time ... established the same order of # daylight savings time settings as in the States of the European Union. # From Andrei Ivanov (2000-03-06): # This year Latvia will not switch to Daylight Savings Time (as specified in # The Regulations of the Cabinet of Ministers of the Rep. of Latvia of # 29-Feb-2000 (No. 79) , # in Latvian for subscribers only). # From RFE/RL Newsline # http://www.rferl.org/newsline/2001/01/3-CEE/cee-030101.html # (2001-01-03), noted after a heads-up by Rives McDow: # The Latvian government on 2 January decided that the country will # institute daylight-saving time this spring, LETA reported. # Last February the three Baltic states decided not to turn back their # clocks one hour in the spring.... # Minister of Economy Aigars Kalvītis noted that Latvia had too few # daylight hours and thus decided to comply with a draft European # Commission directive that provides for instituting daylight-saving # time in EU countries between 2002 and 2006. The Latvian government # urged Lithuania and Estonia to adopt a similar time policy, but it # appears that they will not do so.... # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Latvia 1989 1996 - Mar lastSun 2:00s 1:00 S Rule Latvia 1989 1996 - Sep lastSun 2:00s 0 - # Milne 1899 says Riga was 1:36:28 (Polytechnique House time). # Byalokoz 1919 says Latvia was 1:36:34. # Go with Byalokoz. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Riga 1:36:34 - LMT 1880 1:36:34 - RMT 1918 Apr 15 2:00 # Riga MT 1:36:34 1:00 LST 1918 Sep 16 3:00 # Latvian ST 1:36:34 - RMT 1919 Apr 1 2:00 1:36:34 1:00 LST 1919 May 22 3:00 1:36:34 - RMT 1926 May 11 2:00 - EET 1940 Aug 5 3:00 - MSK 1941 Jul 1:00 C-Eur CE%sT 1944 Oct 13 3:00 Russia MSK/MSD 1989 Mar lastSun 2:00s 2:00 1:00 EEST 1989 Sep lastSun 2:00s 2:00 Latvia EE%sT 1997 Jan 21 2:00 EU EE%sT 2000 Feb 29 2:00 - EET 2001 Jan 2 2:00 EU EE%sT # Lithuania # From Paul Eggert (2016-03-18): # The 1989 transition is from USSR act No. 227 (1989-03-14). # From Paul Eggert (1996-11-22): # IATA SSIM (1992/1996) says Lithuania uses W-Eur rules, but since it is # known to be wrong about Estonia and Latvia, assume it's wrong here too. # From Marius Gedminas (1998-08-07): # I would like to inform that in this year Lithuanian time zone # (Europe/Vilnius) was changed. # From ELTA No. 972 (2582) (1999-09-29) , # via Steffen Thorsen: # Lithuania has shifted back to the second time zone (GMT plus two hours) # to be valid here starting from October 31, # as decided by the national government on Wednesday.... # The Lithuanian government also announced plans to consider a # motion to give up shifting to summer time in spring, as it was # already done by Estonia. # From the Fact File, Lithuanian State Department of Tourism # (2000-03-27): # Local time is GMT+2 hours ..., no daylight saving. # From a user via Klaus Marten (2003-02-07): # As a candidate for membership of the European Union, Lithuania will # observe Summer Time in 2003, changing its clocks at the times laid # down in EU Directive 2000/84 of 19.I.01 (i.e. at the same times as its # neighbour Latvia). The text of the Lithuanian government Order of # 7.XI.02 to this effect can be found at # http://www.lrvk.lt/nut/11/n1749.htm # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Vilnius 1:41:16 - LMT 1880 1:24:00 - WMT 1917 # Warsaw Mean Time 1:35:36 - KMT 1919 Oct 10 # Kaunas Mean Time 1:00 - CET 1920 Jul 12 2:00 - EET 1920 Oct 9 1:00 - CET 1940 Aug 3 3:00 - MSK 1941 Jun 24 1:00 C-Eur CE%sT 1944 Aug 3:00 Russia MSK/MSD 1989 Mar 26 2:00s 2:00 Russia EE%sT 1991 Sep 29 2:00s 2:00 C-Eur EE%sT 1998 2:00 - EET 1998 Mar 29 1:00u 1:00 EU CE%sT 1999 Oct 31 1:00u 2:00 - EET 2003 Jan 1 2:00 EU EE%sT # Malta # # From Paul Eggert (2016-10-21): # Assume 1900-1972 was like Rome, overriding Shanks. # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Malta 1973 only - Mar 31 0:00s 1:00 S Rule Malta 1973 only - Sep 29 0:00s 0 - Rule Malta 1974 only - Apr 21 0:00s 1:00 S Rule Malta 1974 only - Sep 16 0:00s 0 - Rule Malta 1975 1979 - Apr Sun>=15 2:00 1:00 S Rule Malta 1975 1980 - Sep Sun>=15 2:00 0 - Rule Malta 1980 only - Mar 31 2:00 1:00 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Malta 0:58:04 - LMT 1893 Nov 2 # Valletta 1:00 Italy CE%sT 1973 Mar 31 1:00 Malta CE%sT 1981 1:00 EU CE%sT # Moldova # From Stepan Golosunov (2016-03-07): # the act of the government of the Republic of Moldova Nr. 132 from 1990-05-04 # ... says that since 1990-05-06 on the territory of the Moldavian SSR # time would be calculated as the standard time of the second time belt # plus one hour of the "summer" time. To implement that clocks would be # adjusted one hour backwards at 1990-05-06 2:00. After that "summer" # time would be cancelled last Sunday of September at 3:00 and # reintroduced last Sunday of March at 2:00. # From Paul Eggert (2006-03-22): # A previous version of this database followed Shanks & Pottenger, who write # that Tiraspol switched to Moscow time on 1992-01-19 at 02:00. # However, this is most likely an error, as Moldova declared independence # on 1991-08-27 (the 1992-01-19 date is that of a Russian decree). # In early 1992 there was large-scale interethnic violence in the area # and it's possible that some Russophones continued to observe Moscow time. # But [two people] separately reported via # Jesper Nørgaard that as of 2001-01-24 Tiraspol was like Chisinau. # The Tiraspol entry has therefore been removed for now. # # From Alexander Krivenyshev (2011-10-17): # Pridnestrovian Moldavian Republic (PMR, also known as # "Pridnestrovie") has abolished seasonal clock change (no transition # to the Winter Time). # # News (in Russian): # http://www.kyivpost.ua/russia/news/pridnestrove-otkazalos-ot-perehoda-na-zimnee-vremya-30954.html # http://www.allmoldova.com/moldova-news/1249064116.html # # The substance of this change (reinstatement of the Tiraspol entry) # is from a patch from Petr Machata (2011-10-17) # # From Tim Parenti (2011-10-19) # In addition, being situated at +4651+2938 would give Tiraspol # a pre-1880 LMT offset of 1:58:32. # # (which agrees with the earlier entry that had been removed) # # From Alexander Krivenyshev (2011-10-26) # NO need to divide Moldova into two timezones at this point. # As of today, Transnistria (Pridnestrovie)- Tiraspol reversed its own # decision to abolish DST this winter. # Following Moldova and neighboring Ukraine- Transnistria (Pridnestrovie)- # Tiraspol will go back to winter time on October 30, 2011. # News from Moldova (in russian): # https://ru.publika.md/link_317061.html # From Roman Tudos (2015-07-02): # http://lex.justice.md/index.php?action=view&view=doc&lang=1&id=355077 # From Paul Eggert (2015-07-01): # The above-mentioned official link to IGO1445-868/2014 states that # 2014-10-26's fallback transition occurred at 03:00 local time. Also, # https://www.trm.md/en/social/la-30-martie-vom-trece-la-ora-de-vara # says the 2014-03-30 spring-forward transition was at 02:00 local time. # Guess that since 1997 Moldova has switched one hour before the EU. # From Heitor David Pinto (2026-02-22): # Soviet Moldovan resolution 132 of 1990 defined the summer time period from # the last Sunday in March at 2:00 to the last Sunday in September at 3:00, # matching the dates used in most of Europe at the time: # https://web.archive.org/web/20211107050832/http://lex.justice.md/viewdoc.php?action=view&view=doc&id=298782&lang=1 # # It seems that in 1996 Moldova changed the end date to October like most of # Europe, but kept the transitions at 2:00 and 3:00 rather than 1:00 UTC, # which would have been locally 3:00 and 4:00.... # # The notices in the Moldovan government website and broadcaster showed the # transitions at 2:00 and 3:00 until 2021: # 2015 https://old.gov.md/en/node/7304 # 2016 https://old.gov.md/en/node/12587 # 2017 https://old.gov.md/en/node/20654 # 2017 https://old.gov.md/en/content/moldova-upholds-winter-time-night-28-29-october # 2018 https://old.gov.md/en/content/moldova-switch-summer-time # 2018 https://old.gov.md/en/content/cabinet-ministers-informs-about-switch-winter-time-28-october # 2019 https://old.gov.md/en/content/moldova-switch-summer-time-31-march # 2019 https://old.gov.md/en/node/31122 # 2020 https://old.gov.md/en/node/32771 # 2020 https://old.gov.md/en/node/34497 # 2021 https://trm.md/ro/social/moldova-trece-in-aceasta-noapte-la-ora-de-vara # 2021 https://trm.md/en/social/republica-moldova-trece-la-ora-de-iarna1 # # However, since 2022, the notices showed the transitions at 3:00 and 4:00, # matching the EU rule at 1:00 UTC: # 2022 https://trm.md/en/social/in-acest-weekend-republica-moldova-trece-la-ora-de-vara # 2022 https://old.gov.md/en/content/moldova-switch-winter-time # 2023 https://moldova1.md/p/6587/ora-de-vara-2023-cum-schimbam-acele-ceasornicelor-si-cand-trecem-la-ora-de-vara # 2023 https://old.gov.md/en/node/46662 # 2024 https://moldova1.md/p/26535/republica-moldova-trece-la-ora-de-vara-in-acest-weekend # 2024 https://moldova1.md/p/37768/republica-moldova-trece-in-aceasta-noapte-la-ora-de-iarna # 2025 https://moldova1.md/p/46349/republica-moldova-trece-la-ora-de-vara-pe-30-martie-cum-ne-afecteaza-si-ce-recomanda-medicii # 2025 https://moldova1.md/p/60469/republica-moldova-trece-la-ora-de-iarna-ceasurile-se-dau-inapoi-cu-o-ora # # It seems that the changes to the end date and transition times were just # done in practice without formally changing the resolution. In late 2025, the # government said that the Soviet resolution was still in force, and proposed # a new resolution to replace it and formally establish the EU rule: # ... based on the notices, it seems that in practice Moldova already # uses the EU rule since 2022. This was also the year when Moldova applied to # join the EU. # # From Robert Bastian (2026-02-26): # This has been approved and published in the government gazette: # https://monitorul.gov.md/ro/monitorul/view/pdf/3234/part/2#page=27 # # From Paul Eggert (2026-02-24): # Also see Svetlana Rudenko, "Moldova abandons the 'Soviet era'", Logos Press, # 2026-02-21 . # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Moldova 1997 2021 - Mar lastSun 2:00 1:00 S Rule Moldova 1997 2021 - Oct lastSun 3:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Chisinau 1:55:20 - LMT 1880 1:55 - CMT 1918 Feb 15 # Chisinau MT 1:44:24 - BMT 1931 Jul 24 # Bucharest MT 2:00 Romania EE%sT 1940 Aug 15 2:00 1:00 EEST 1941 Jul 17 1:00 C-Eur CE%sT 1944 Aug 24 3:00 Russia MSK/MSD 1990 May 6 2:00 2:00 Russia EE%sT 1992 2:00 E-Eur EE%sT 1997 # See Romania commentary for the guessed 1997 transition to EU rules. 2:00 Moldova EE%sT 2022 2:00 EU EE%sT # Poland # The 1919 dates and times can be found in Tygodnik Urzędowy nr 1 (1919-03-20), # pp 1-2. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Poland 1918 1919 - Sep 16 2:00s 0 - Rule Poland 1919 only - Apr 15 2:00s 1:00 S Rule Poland 1944 only - Apr 3 2:00s 1:00 S # Whitman gives 1944 Nov 30; go with Shanks & Pottenger. Rule Poland 1944 only - Oct 4 2:00 0 - # For 1944-1948 Whitman gives the previous day; go with Shanks & Pottenger. Rule Poland 1945 only - Apr 29 0:00 1:00 S Rule Poland 1945 only - Nov 1 0:00 0 - # For 1946 on the source is Kazimierz Borkowski, # Toruń Center for Astronomy, Dept. of Radio Astronomy, Nicolaus Copernicus U., # https://www.astro.uni.torun.pl/~kb/Artykuly/U-PA/Czas2.htm#tth_tAb1 # Thanks to Przemysław Augustyniak (2005-05-28) for this reference. # He also gives these further references: # Mon Pol nr 13, poz 162 (1995) # Druk nr 2180 (2003) Rule Poland 1946 only - Apr 14 0:00s 1:00 S Rule Poland 1946 only - Oct 7 2:00s 0 - Rule Poland 1947 only - May 4 2:00s 1:00 S Rule Poland 1947 1949 - Oct Sun>=1 2:00s 0 - Rule Poland 1948 only - Apr 18 2:00s 1:00 S Rule Poland 1949 only - Apr 10 2:00s 1:00 S Rule Poland 1957 only - Jun 2 1:00s 1:00 S Rule Poland 1957 1958 - Sep lastSun 1:00s 0 - Rule Poland 1958 only - Mar 30 1:00s 1:00 S Rule Poland 1959 only - May 31 1:00s 1:00 S Rule Poland 1959 1961 - Oct Sun>=1 1:00s 0 - Rule Poland 1960 only - Apr 3 1:00s 1:00 S Rule Poland 1961 1964 - May lastSun 1:00s 1:00 S Rule Poland 1962 1964 - Sep lastSun 1:00s 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Warsaw 1:24:00 - LMT 1880 1:24:00 - WMT 1915 Aug 5 # Warsaw Mean Time 1:00 C-Eur CE%sT 1918 Sep 16 3:00 2:00 Poland EE%sT 1922 Jun 1:00 Poland CE%sT 1940 Jun 23 2:00 1:00 C-Eur CE%sT 1944 Oct 1:00 Poland CE%sT 1977 1:00 W-Eur CE%sT 1988 1:00 EU CE%sT # Portugal # From Tim Parenti (2024-07-01), per Alois Treindl (2021-02-07) and Michael # Deckers (2021-02-10): # http://oal.ul.pt/documentos/2018/01/hl1911a2018.pdf/ # The Astronomical Observatory of Lisbon has published a list detailing the # historical transitions in legal time within continental Portugal. It # directly references many decrees and ordinances which are, in turn, # referenced below. They can be viewed in the public archives of the Diário da # República (until 1976-04-09 known as the Diário do Govêrno) at # https://dre.pt/ (in Portuguese). # # Most of the Rules below have been updated simply to match the Observatory's # listing for continental (mainland) Portugal. Although there are over 50 # referenced decrees and ordinances, only the handful with comments below have # been verified against the text, typically to provide additional confidence # wherever dates provided by Whitman and Shanks & Pottenger had disagreed. # See further below for the Azores and Madeira. # From Tim Parenti (2024-07-01), per Paul Eggert (2014-08-11), after a # heads-up from Stephen Colebourne: # According to a 1911-05-24 Portuguese decree, Lisbon was at -0:36:44.68, but # switched to GMT on 1912-01-01 at 00:00. # https://dre.pt/dr/detalhe/decreto/593090 # https://dre.pt/application/conteudo/593090 # The decree made legal time throughout Portugal and her possessions # "subordinate to the Greenwich meridian, according to the principle adopted at # the Washington Convention in 1884" and eliminated the "difference of five # minutes between the internal and external clocks of railway stations". # # The decree was gazetted in the 1911-05-30 issue of Diário do Govêrno, and is # considered to be dated 1911-05-24 by that issue's summary; however, the text # of the decree itself is dated 1911-05-26. The Diário da República website # notes the discrepancy, but later laws and the Observatory all seem to refer # to this decree by the 1911-05-24 date. # # From Michael Deckers (2018-02-15): # article 5 [of the 1911 decree; Deckers's translation] ...: # These dispositions shall enter into force at the instant at which, # according to the 2nd article, the civil day January 1, 1912 begins, # all clocks therefore having to be advanced or set back correspondingly ... # Rule NAME FROM TO - IN ON AT SAVE LETTER/S # From Tim Parenti (2024-07-01): # Decreto 1469 of 1915-03-30 ... had nothing to do with DST; # rather it regulated the sending of time signals. # But we do see linkage to Spain in the 1920s below. # https://dre.pt/dr/detalhe/decreto/1469-1915-285721 # https://dre.pt/application/conteudo/285721 # # According to the Observatory, standard time was first advanced by Decreto # 2433 of 1916-06-09 and restored by Decreto 2712 of 1916-10-28. While Whitman # gives 1916-10-31 for the latter transition, Shanks & Pottenger agrees more # closely with the decree, which stated that its provision "will start sixty # minutes after the end of 31 October, according to the current time," i.e., # 01:00 on 1 November. # https://dre.pt/dr/detalhe/decreto/2433-1916-267192 # https://dre.pt/application/conteudo/267192 # https://dre.pt/dr/detalhe/decreto/2712-1916-590937 # https://dre.pt/application/conteudo/590937 Rule Port 1916 only - Jun 17 23:00 1:00 S Rule Port 1916 only - Nov 1 1:00 0 - # From Tim Parenti (2024-07-01): # Article 7 of Decreto 2922 of 1916-12-30 stated that "the legal time will be # advanced by sixty minutes from 1 March to 31 October." Per Article 15, this # came into force from 1917-01-01. Just before the first fall back, Decreto # 3446 of 1917-10-11 changed the annual end date to 14 October. # https://dre.pt/dr/detalhe/decreto/2922-1916-261894 # https://dre.pt/application/conteudo/261894 # https://dre.pt/dr/detalhe/decreto/3446-1917-495161 # https://dre.pt/application/conteudo/495161 # This annual change was revoked by Decreto 8038 of 1922-02-18. # https://dre.pt/dr/detalhe/decreto/8038-1922-569751 # https://dre.pt/application/conteudo/569751 Rule Port 1917 1921 - Mar 1 0:00 1:00 S Rule Port 1917 1921 - Oct 14 24:00 0 - # From Tim Parenti (2024-07-01): # Decreto 9592 of 1924-04-14 noted that "France maintains the advance of legal # time in the summer and Spain has now adopted it for the first time" and # considered "that the absence of similar measures would cause serious # difficulties for international rail connections with consequent repercussions # on domestic service hours..." along with "inconvenient analogues...for postal # and telegraph services." Summer time would be in effect from 17 April to 4 # October, with the spring change explicitly specified by bringing clocks # forward from 16 April 23:00. # https://dre.pt/dr/detalhe/decreto/9592-1924-652133 # https://dre.pt/application/conteudo/652133 # # Decreto 10700, issued 1925-04-16, noted that Spain had not continued summer # time, declared that "the current legal hour prior to 17 April remains # unchanged from that day forward", and revoked legislation to the contrary, # just a day before summer time would have otherwise resumed. # https://dre.pt/dr/detalhe/decreto/10700-1925-437826 # https://dre.pt/application/conteudo/437826 Rule Port 1924 only - Apr 16 23:00s 1:00 S Rule Port 1924 only - Oct 4 23:00s 0 - Rule Port 1926 only - Apr 17 23:00s 1:00 S Rule Port 1926 1929 - Oct Sat>=1 23:00s 0 - Rule Port 1927 only - Apr 9 23:00s 1:00 S Rule Port 1928 only - Apr 14 23:00s 1:00 S Rule Port 1929 only - Apr 20 23:00s 1:00 S Rule Port 1931 only - Apr 18 23:00s 1:00 S # Whitman gives 1931 Oct 8; go with Shanks & Pottenger. Rule Port 1931 1932 - Oct Sat>=1 23:00s 0 - Rule Port 1932 only - Apr 2 23:00s 1:00 S Rule Port 1934 only - Apr 7 23:00s 1:00 S # Whitman gives 1934 Oct 5; go with Shanks & Pottenger. # Note: The 1935 law specified 10-06 00:00, not 10-05 24:00, but the following # is equivalent and more succinct. Rule Port 1934 1938 - Oct Sat>=1 23:00s 0 - # Shanks & Pottenger give 1935 Apr 30; go with Whitman. Rule Port 1935 only - Mar 30 23:00s 1:00 S Rule Port 1936 only - Apr 18 23:00s 1:00 S # Whitman gives 1937 Apr 2; go with Shanks & Pottenger. Rule Port 1937 only - Apr 3 23:00s 1:00 S Rule Port 1938 only - Mar 26 23:00s 1:00 S Rule Port 1939 only - Apr 15 23:00s 1:00 S # Whitman gives 1939 Oct 7; go with Shanks & Pottenger. Rule Port 1939 only - Nov 18 23:00s 0 - # From Tim Parenti (2024-07-01): # Portaria 9465 of 1940-02-17 advanced clocks from Saturday 1940-02-24 23:00. # The clocks were restored by Portaria 9658, issued Monday 1940-10-07, # effective from 24:00 that very night, which agrees with Shanks & Pottenger; # Whitman gives Saturday 1940-10-05 instead. # https://dre.pt/dr/detalhe/portaria/9465-1940-189096 # https://dre.pt/application/conteudo/189096 # https://dre.pt/dr/detalhe/portaria/9658-1940-196729 # https://dre.pt/application/conteudo/196729 Rule Port 1940 only - Feb 24 23:00s 1:00 S Rule Port 1940 only - Oct 7 23:00s 0 - Rule Port 1941 only - Apr 5 23:00s 1:00 S Rule Port 1941 only - Oct 5 23:00s 0 - Rule Port 1942 1945 - Mar Sat>=8 23:00s 1:00 S Rule Port 1942 only - Apr 25 22:00s 2:00 M # Midsummer Rule Port 1942 only - Aug 15 22:00s 1:00 S Rule Port 1942 1945 - Oct Sat>=24 23:00s 0 - Rule Port 1943 only - Apr 17 22:00s 2:00 M Rule Port 1943 1945 - Aug Sat>=25 22:00s 1:00 S Rule Port 1944 1945 - Apr Sat>=21 22:00s 2:00 M Rule Port 1946 only - Apr Sat>=1 23:00s 1:00 S Rule Port 1946 only - Oct Sat>=1 23:00s 0 - # From Tim Parenti (2024-07-01), per Alois Treindl (2021-02-07): # The Astronomical Observatory of Lisbon cites Portaria 11767 of 1947-03-28 for # 1947 and Portaria 12286 of 1948-02-19 for 1948. # https://dre.pt/dr/detalhe/portaria/11767-1947-414787 # https://dre.pt/application/conteudo/414787 # https://dre.pt/dr/detalhe/portaria/12286-1948-152953 # https://dre.pt/application/conteudo/152953 # # Although the latter ordinance explicitly had the 1948-10-03 transition # scheduled for 02:00 rather than 03:00 as had been used in 1947, Decreto-Lei # 37048 of 1948-09-07 recognized "that it is advisable to definitely set...the # 'summer time' regime", and fixed the fall transition at 03:00 moving forward. # https://dre.pt/dr/detalhe/decreto-lei/37048-1948-373810 # https://dre.pt/application/conteudo/373810 # While the Observatory only cites this act for 1949-1965 and not for 1948, it # does not appear to have had any provision delaying its effect, so assume that # it overrode the prior ordinance for 1948-10-03. # # Whitman says DST was not observed in 1950 and gives Oct lastSun for 1952 on. # The Observatory, however, agrees with Shanks & Pottenger that 1950 was not an # exception and that Oct Sun>=1 was maintained through 1965. Rule Port 1947 1966 - Apr Sun>=1 2:00s 1:00 S Rule Port 1947 1965 - Oct Sun>=1 2:00s 0 - # From Tim Parenti (2024-07-01): # Decreto-Lei 47233 of 1966-10-01 considered that the "duality" in time was # "the cause of serious disturbances" and noted that "the countries with which # we have the most frequent contacts...have already adopted" a solution # coinciding with the extant "summer time". It established that the former # "summer time" would apply year-round on the mainland and adjacent islands # with immediate effect, as the fall back would have otherwise occurred later # that evening. # https://dre.pt/dr/detalhe/decreto-lei/47233-1966-293729 # Model this by changing zones without changing clocks at the # previously-appointed fall back time. # # Decreto-Lei 309/76 of 1976-04-27 acknowledged that those international # contacts had returned to adopting seasonal times, and considered that the # year-round advancement "entails considerable sacrifices for the vast majority # of the working population during the winter months", including morning # visibility concerns for schoolchildren. It specified, beginning 1976-09-26 # 01:00, an annual return to UT+00 on the mainland from 00:00 UT on Sep lastSun # to 00:00 UT on Mar lastSun (unless the latter date fell on Easter, in which # case it was to be brought forward to the preceding Sunday). It also assigned # the Permanent Time Commission to study and propose revisions for the Azores # and Madeira, neither of which resumed DST until 1982 (as described further # below). # https://dre.pt/dr/detalhe/decreto-lei/309-1976-502063 Rule Port 1976 only - Sep lastSun 1:00 0 - Rule Port 1977 only - Mar lastSun 0:00s 1:00 S Rule Port 1977 only - Sep lastSun 0:00s 0 - # From Tim Parenti (2024-07-01): # Beginning in 1978, rather than triggering the Easter rule of the 1976 decree # (Easter fell on 1978-03-26), Article 5 was used instead, which allowed DST # dates to be changed by order of the Minister of Education and Scientific # Research, upon consultation with the Permanent Time Commission, "whenever # considered convenient." As such, a series of one-off ordinances were # promulgated for the mainland in 1978 through 1980, after which the 1976 # decree naturally came back into force from 1981. Rule Port 1978 1980 - Apr Sun>=1 1:00s 1:00 S Rule Port 1978 only - Oct 1 1:00s 0 - Rule Port 1979 1980 - Sep lastSun 1:00s 0 - Rule Port 1981 1986 - Mar lastSun 0:00s 1:00 S Rule Port 1981 1985 - Sep lastSun 0:00s 0 - # From Tim Parenti (2024-07-01): # Decreto-Lei 44-B/86 of 1986-03-07 switched mainland Portugal's transition # times from 0:00s to 1:00u to harmonize with the EEC from 1986-03-30. # https://dre.pt/dr/detalhe/decreto-lei/44-b-1986-628280 # (Transitions of 1:00s as previously reported and used by the W-Eur rules, # though equivalent, appear to have been fiction here.) Madeira continued to # use 0:00s for spring 1986 before joining with the mainland using 1:00u in the # fall; meanwhile, in the Azores the two were equivalent, so the law specifying # 0:00s wasn't touched until 1992. (See below for more on the islands.) # # From Rui Pedro Salgueiro (1992-11-12): # Portugal has recently (September, 27) changed timezone # (from WET to MET or CET) to harmonize with EEC. # # Martin Bruckmann (1996-02-29) reports via Peter Ilieve # that Portugal is reverting to 0:00 by not moving its clocks this spring. # The new Prime Minister was fed up with getting up in the dark in the winter. # # From Paul Eggert (1996-11-12): # IATA SSIM (1991-09) reports several 1991-09 and 1992-09 transitions # at 02:00u, not 01:00u. Assume that these are typos. # # Zone NAME STDOFF RULES FORMAT [UNTIL] #STDOFF -0:36:44.68 Zone Europe/Lisbon -0:36:45 - LMT 1884 -0:36:45 - LMT 1912 Jan 1 0:00u # Lisbon MT 0:00 Port WE%sT 1966 Oct 2 2:00s 1:00 - CET 1976 Sep 26 1:00 0:00 Port WE%sT 1986 0:00 EU WE%sT 1992 Sep 27 1:00u 1:00 EU CE%sT 1996 Mar 31 1:00u 0:00 EU WE%sT # From Tim Parenti (2024-07-01): # For the Azores and Madeira, legislation was followed from the laws currently # in force as listed at: # https://oal.ul.pt/hora-legal/legislacao/ # working backward through references of revocation and abrogation to # Decreto-Lei 47233 of 1966-10-01, the last time DST was abolished across the # mainland and its adjacent islands. # # From Tim Parenti (2026-05-26): # Observance of DST on the Azores and Madeira was explicitly covered by # mainland legislation in: # - Portaria 11767 of 1947-03-28 for 1947, # - Portaria 12286 of 1948-02-19 for 1948, and # - Decreto-Lei 37048 of 1948-09-07 through its revocation in 1966. # (See mainland commentary, above.) However, most legislation prior to 1947 # didn't explicitly call out these "adjacent islands", so we assume that DST # rules on the islands prior to 1947 were also like that of the mainland. Zone Atlantic/Azores -1:42:40 - LMT 1884 # Ponta Delgada -1:54:32 - HMT 1912 Jan 1 2:00u # Horta MT # Vanguard section, for zic and other parsers that support %z. -2:00 Port %z 1966 Oct 2 2:00s # From Tim Parenti (2024-07-01): # While Decreto-Lei 309/76 of 1976-04-27 reintroduced DST on the mainland by # falling back on 1976-09-26, it assigned the Permanent Time Commission to # study and propose revisions for the Azores and Madeira. Decreto Regional # 9/77/A of 1977-05-17 affirmed that "the legal time remained unchanged in the # Azores" at UT-1, and would remain there year-round. # https://dre.pt/dr/detalhe/decreto-regional/9-1977-252066 # # Decreto Regional 2/82/A, published 1982-03-02, adopted DST in the same # fashion as the mainland used at the time. # https://dre.pt/dr/detalhe/decreto-regional/2-1982-599965 # Though transitions in the Azores officially remained at 0:00s through 1992, # this was equivalent to the EU-style 1:00u adopted by the mainland in 1986, so # model it as such. -1:00 - %z 1982 Mar 28 0:00s -1:00 Port %z 1986 # Rearguard section, for parsers lacking %z; see ziguard.awk. # -2:00 Port -02/-01 1942 Apr 25 22:00s # -2:00 Port +00 1942 Aug 15 22:00s # -2:00 Port -02/-01 1943 Apr 17 22:00s # -2:00 Port +00 1943 Aug 28 22:00s # -2:00 Port -02/-01 1944 Apr 22 22:00s # -2:00 Port +00 1944 Aug 26 22:00s # -2:00 Port -02/-01 1945 Apr 21 22:00s # -2:00 Port +00 1945 Aug 25 22:00s # -2:00 Port -02/-01 1966 Oct 2 2:00s # -1:00 - -01 1982 Mar 28 0:00s # -1:00 Port -01/+00 1986 # End of rearguard section. # # From Paul Eggert (1996-11-12): # IATA SSIM (1991/1992) reports that the Azores were at -1:00. # IATA SSIM (1993-02) says +0:00; later issues (through 1996-09) say -1:00. # # From Tim Parenti (2024-07-01): # After mainland Portugal had shifted forward an hour from 1992-09-27, Decreto # Legislativo Regional 29/92/A of 1992-12-23 sought to "reduce the time # difference" by shifting the Azores forward as well from 1992-12-27. Just six # months later, this was revoked by Decreto Legislativo Regional 9/93/A, citing # "major changes in work habits and way of life." Though the revocation didn't # give a transition time, it was signed Wednesday 1993-06-16; assume it took # effect later that evening, and that an EU-style spring forward (to +01) was # still observed in the interim on 1993-03-28. # https://dre.pt/dr/detalhe/decreto-legislativo-regional/29-1992-621553 # https://dre.pt/dr/detalhe/decreto-legislativo-regional/9-1993-389633 -1:00 EU %z 1992 Dec 27 1:00s 0:00 EU WE%sT 1993 Jun 17 1:00u -1:00 EU %z Zone Atlantic/Madeira -1:07:36 - LMT 1884 # Funchal -1:07:36 - FMT 1912 Jan 1 1:00u # Funchal MT # Vanguard section, for zic and other parsers that support %z. -1:00 Port %z 1966 Oct 2 2:00s # Rearguard section, for parsers lacking %z; see ziguard.awk. # -1:00 Port -01/+00 1942 Apr 25 22:00s # -1:00 Port +01 1942 Aug 15 22:00s # -1:00 Port -01/+00 1943 Apr 17 22:00s # -1:00 Port +01 1943 Aug 28 22:00s # -1:00 Port -01/+00 1944 Apr 22 22:00s # -1:00 Port +01 1944 Aug 26 22:00s # -1:00 Port -01/+00 1945 Apr 21 22:00s # -1:00 Port +01 1945 Aug 25 22:00s # -1:00 Port -01/+00 1966 Oct 2 2:00s # End of rearguard section. # # From Tim Parenti (2024-07-01): # Decreto Regional 5/82/M, published 1982-04-03, established DST transitions at # 0:00u, which for Madeira is equivalent to the mainland's rules (0:00s) at the # time. It came into effect the day following its publication, Sunday # 1982-04-04, thus resuming Madeira's DST practice about a week later than the # mainland and the Azores. # https://dre.pt/dr/detalhe/decreto-regional/5-1982-608273 # # Decreto Legislativo Regional 18/86/M, published 1986-10-01, adopted EU-style # rules (1:00u) and entered into immediate force after being signed on # 1986-07-31. # https://dre.pt/dr/detalhe/decreto-legislativo-regional/18-1986-221705 0:00 - WET 1982 Apr 4 0:00 Port WE%sT 1986 Jul 31 0:00 EU WE%sT # Romania # # From Paul Eggert (1999-10-07): # Nine O'clock # (1998-10-23) reports that the switch occurred at # 04:00 local time in fall 1998. For lack of better info, # assume that Romania switched to EU rules in 1997, # the same year as Bulgaria. # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Romania 1932 only - May 21 0:00s 1:00 S Rule Romania 1932 1939 - Oct Sun>=1 0:00s 0 - Rule Romania 1933 1939 - Apr Sun>=2 0:00s 1:00 S Rule Romania 1979 only - May 27 0:00 1:00 S Rule Romania 1979 only - Sep lastSun 0:00 0 - Rule Romania 1980 only - Apr 5 23:00 1:00 S Rule Romania 1980 only - Sep lastSun 1:00 0 - Rule Romania 1991 1993 - Mar lastSun 0:00s 1:00 S Rule Romania 1991 1993 - Sep lastSun 0:00s 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Bucharest 1:44:24 - LMT 1891 Oct 1:44:24 - BMT 1931 Jul 24 # Bucharest MT 2:00 Romania EE%sT 1981 Mar 29 2:00s 2:00 C-Eur EE%sT 1991 2:00 Romania EE%sT 1994 2:00 E-Eur EE%sT 1997 2:00 EU EE%sT # Russia # From Alexander Krivenyshev (2011-09-15): # Based on last Russian Government Decree No. 725 on August 31, 2011 # (Government document # http://www.government.ru/gov/results/16355/print/ # in Russian) # there are few corrections have to be made for some Russian time zones... # All updated Russian Time Zones were placed in table and translated to English # by WorldTimeZone.com at the link below: # http://www.worldtimezone.com/dst_news/dst_news_russia36.htm # From Sanjeev Gupta (2011-09-27): # Scans of [Decree No. 23 of January 8, 1992] are available at: # http://government.consultant.ru/page.aspx?1223966 # They are in Cyrillic letters (presumably Russian). # From Arthur David Olson (2012-05-09): # Regarding the instant when clocks in time-zone-shifting parts of Russia # changed in September 2011: # # One source is # http://government.ru/gov/results/16355/ # which, according to translate.google.com, begins "Decree of August 31, # 2011 No. 725" and contains no other dates or "effective date" information. # # Another source is # https://rg.ru/2011/09/06/chas-zona-dok.html # which, according to translate.google.com, begins "Resolution of the # Government of the Russian Federation on August 31, 2011 N 725" and also # contains "Date first official publication: September 6, 2011 Posted on: # in the 'RG' - Federal Issue No. 5573 September 6, 2011" but which # does not contain any "effective date" information. # # Another source is # https://en.wikipedia.org/wiki/Oymyakonsky_District#cite_note-RuTime-7 # which, in note 8, contains "Resolution No. 725 of August 31, 2011... # Effective as of after 7 days following the day of the official publication" # but which does not contain any reference to September 6, 2011. # # The Wikipedia article refers to # http://base.consultant.ru/cons/cgi/online.cgi?req=doc;base=LAW;n=118896 # which seems to copy the text of the government.ru page. # # Tobias Conradi combines Wikipedia's # "as of after 7 days following the day of the official publication" # with www.rg.ru's "Date of first official publication: September 6, 2011" to # get September 13, 2011 as the cutover date (unusually, a Tuesday, as Tobias # Conradi notes). # # None of the sources indicates a time of day for changing clocks. # # Go with 2011-09-13 0:00s. # From Alexander Krivenyshev (2014-07-01): # According to the Russian news (ITAR-TASS News Agency) # http://en.itar-tass.com/russia/738562 # the State Duma has approved ... the draft bill on returning to # winter time standard and return Russia 11 time zones. The new # regulations will come into effect on October 26, 2014 at 02:00 ... # http://asozd2.duma.gov.ru/main.nsf/(Spravka)?OpenAgent&RN=431985-6&02 # Here is a link where we put together table (based on approved Bill N # 431985-6) with proposed 11 Russian time zones and corresponding # areas/cities/administrative centers in the Russian Federation (in English): # http://www.worldtimezone.com/dst_news/dst_news_russia65.html # # From Alexander Krivenyshev (2014-07-22): # Putin signed the Federal Law 431985-6 ... (in Russian) # http://itar-tass.com/obschestvo/1333711 # http://www.pravo.gov.ru:8080/page.aspx?111660 # http://www.kremlin.ru/acts/46279 # From October 26, 2014 the new Russian time zone map will look like this: # http://www.worldtimezone.com/dst_news/dst_news_russia-map-2014-07.html # From Paul Eggert (2006-03-22): # Moscow time zone abbreviations after 1919-07-01, and Moscow rules after 1991, # are from Andrey A. Chernov. The rest is from Shanks & Pottenger, # except we follow Chernov's report that 1992 DST transitions were Sat # 23:00, not Sun 02:00s. # # From Stanislaw A. Kuzikowski (1994-06-29): # But now it is some months since Novosibirsk is 3 hours ahead of Moscow! # I do not know why they have decided to make this change; # as far as I remember it was done exactly during winter->summer switching # so we (Novosibirsk) simply did not switch. # # From Andrey A. Chernov (1996-10-04): # 'MSK' and 'MSD' were born and used initially on Moscow computers with # UNIX-like OSes by several developer groups (e.g. Demos group, Kiae group).... # The next step was the UUCP network, the Relcom predecessor # (used mainly for mail), and MSK/MSD was actively used there. # # From Chris Carrier (1996-10-30): # According to a friend of mine who rode the Trans-Siberian Railroad from # Moscow to Irkutsk in 1995, public air and rail transport in Russia ... # still follows Moscow time, no matter where in Russia it is located. # # For Grozny, Chechnya, we have the following story from # John Daniszewski, "Scavengers in the Rubble", Los Angeles Times (2001-02-07): # News - often false - is spread by word of mouth. A rumor that it was # time to move the clocks back put this whole city out of sync with # the rest of Russia for two weeks - even soldiers stationed here began # enforcing curfew at the wrong time. # # From Gwillim Law (2001-06-05): # There's considerable evidence that Sakhalin Island used to be in # UTC+11, and has changed to UTC+10, in this decade. I start with the # SSIM, which listed Yuzhno-Sakhalinsk in zone RU10 along with Magadan # until February 1997, and then in RU9 with Khabarovsk and Vladivostok # since September 1997.... Although the Kuril Islands are # administratively part of Sakhalin oblast', they appear to have # remained on UTC+11 along with Magadan. # From Marat Nigametzianov (2018-07-16): # this is link to order from 1956 about timezone in USSR # http://astro.uni-altai.ru/~orion/blog/2011/11/novyie-granitsyi-chasovyih-poyasov-v-sssr/ # # From Paul Eggert (2018-07-16): # Perhaps someone could translate the link mentioned above, and use it # to correct our data for the ex-Soviet Union. It cites the following: # «По��ное врем� и новые границы ча�овых по��ов» / �о�т. П.�. Долгов, # отв. ред. Г.Д. Бурдун - М: Комитет �тандартов, мер и измерительных # приборов при Совете Мини�тров СССР, Междуведом�твенна� коми��и� # единой �лужбы времени, 1956 г. # This book looks like it would be a helpful resource for the Soviet # Union through 1956. Although a copy was in the Scientific Library # of Tomsk State University, I have not been able to track down a copy nearby. # # From Stepan Golosunov (2018-07-21): # http://astro.uni-altai.ru/~orion/blog/2015/05/center-reforma-ischisleniya-vremeni-br-na-territorii-sssr-v-1957-godu-center/ # says that the 1956 decision to change time belts' borders was not # implemented as planned in 1956 and the change happened in 1957. # There is also the problem that actual time zones were different from # the official time belts (and from many time belts' maps) as there were # numerous exceptions to application of time belt rules. For example, # https://ru.wikipedia.org/wiki/Мо�ков�кое_врем�#Перемещение_границы_применени�_мо�ков�кого_времени_на_во�ток # says that by 1962 there were many regions in the 3rd time belt that # were on Moscow time, referring to a 1962 map. By 1989 number of such # exceptions grew considerably. # From Tim Parenti (2014-07-06): # The comments detailing the coverage of each Russian zone are meant to assist # with maintenance only and represent our best guesses as to which regions # are covered by each zone. They are not meant to be taken as an authoritative # listing. The region codes listed come from # https://en.wikipedia.org/w/?title=Federal_subjects_of_Russia&oldid=611810498 # and are used for convenience only; no guarantees are made regarding their # future stability. ISO 3166-2:RU codes are also listed for first-level # divisions where available. # From Tim Parenti (2014-07-03): # Europe/Kaliningrad covers... # 39 RU-KGD Kaliningrad Oblast # From Paul Eggert (2019-07-25): # Although Shanks lists 1945-01-01 as the date for transition from # +01/+02 to +02/+03, more likely this is a placeholder. Guess that # the transition occurred at 1945-04-10 00:00, which is about when # Königsberg surrendered to Soviet troops. (Thanks to Alois Treindl.) # From Paul Eggert (2016-03-18): # The 1989 transition is from USSR act No. 227 (1989-03-14). # From Stepan Golosunov (2016-03-07): # http://www.rgo.ru/ru/kaliningradskoe-oblastnoe-otdelenie/ob-otdelenii/publikacii/kak-nam-zhilos-bez-letnego-vremeni # confirms that the 1989 change to Moscow-1 was implemented. # (The article, though, is misattributed to 1990 while saying that # summer->winter transition would be done on the 24 of September. But # 1990-09-24 was Monday, while 1989-09-24 was Sunday as expected.) # ... # http://www.kaliningradka.ru/site_pc/cherez/index.php?ELEMENT_ID=40091 # says that Kaliningrad switched to Moscow-1 on 1989-03-26, avoided # at the last moment switch to Moscow-1 on 1991-03-31, switched to # Moscow on 1991-11-03, switched to Moscow-1 on 1992-01-19. Zone Europe/Kaliningrad 1:22:00 - LMT 1893 Apr 1:00 C-Eur CE%sT 1945 Apr 10 2:00 Poland EE%sT 1946 Apr 7 3:00 Russia MSK/MSD 1989 Mar 26 2:00s 2:00 Russia EE%sT 2011 Mar 27 2:00s 3:00 - %z 2014 Oct 26 2:00s 2:00 - EET # From Paul Eggert (2016-02-21), per Tim Parenti (2014-07-03) and # Oscar van Vlijmen (2001-08-25): # Europe/Moscow covers... # 01 RU-AD Adygea, Republic of # 05 RU-DA Dagestan, Republic of # 06 RU-IN Ingushetia, Republic of # 07 RU-KB Kabardino-Balkar Republic # 08 RU-KL Kalmykia, Republic of # 09 RU-KC Karachay-Cherkess Republic # 10 RU-KR Karelia, Republic of # 11 RU-KO Komi Republic # 12 RU-ME Mari El Republic # 13 RU-MO Mordovia, Republic of # 15 RU-SE North Ossetia-Alania, Republic of # 16 RU-TA Tatarstan, Republic of # 20 RU-CE Chechen Republic # 21 RU-CU Chuvash Republic # 23 RU-KDA Krasnodar Krai # 26 RU-STA Stavropol Krai # 29 RU-ARK Arkhangelsk Oblast # 31 RU-BEL Belgorod Oblast # 32 RU-BRY Bryansk Oblast # 33 RU-VLA Vladimir Oblast # 35 RU-VLG Vologda Oblast # 36 RU-VOR Voronezh Oblast # 37 RU-IVA Ivanovo Oblast # 40 RU-KLU Kaluga Oblast # 44 RU-KOS Kostroma Oblast # 46 RU-KRS Kursk Oblast # 47 RU-LEN Leningrad Oblast # 48 RU-LIP Lipetsk Oblast # 50 RU-MOS Moscow Oblast # 51 RU-MUR Murmansk Oblast # 52 RU-NIZ Nizhny Novgorod Oblast # 53 RU-NGR Novgorod Oblast # 57 RU-ORL Oryol Oblast # 58 RU-PNZ Penza Oblast # 60 RU-PSK Pskov Oblast # 61 RU-ROS Rostov Oblast # 62 RU-RYA Ryazan Oblast # 67 RU-SMO Smolensk Oblast # 68 RU-TAM Tambov Oblast # 69 RU-TVE Tver Oblast # 71 RU-TUL Tula Oblast # 76 RU-YAR Yaroslavl Oblast # 77 RU-MOW Moscow # 78 RU-SPE Saint Petersburg # 83 RU-NEN Nenets Autonomous Okrug # From Paul Eggert (2016-08-23): # The Soviets switched to UT-based time in 1919. Decree No. 59 # (1919-02-08) http://istmat.info/node/35567 established UT-based time # zones, and Decree No. 147 (1919-03-29) http://istmat.info/node/35854 # specified a transition date of 1919-07-01, apparently at 00:00 UT. # No doubt only the Soviet-controlled regions switched on that date; # later transitions to UT-based time in other parts of Russia are # taken from what appear to be guesses by Shanks. # (Thanks to Alexander Belopolsky for pointers to the decrees.) # From Stepan Golosunov (2016-03-07): # 11. Regions-violators, 1981-1982. # Wikipedia refers to # http://maps.monetonos.ru/maps/raznoe/Old_Maps/Old_Maps/Articles/022/3_1981.html # http://besp.narod.ru/nauka_1981_3.htm # # The second link provides two articles scanned from the Nauka i Zhizn # magazine No. 3, 1981 and a scan of the short article attributed to # the Trud newspaper from February 1982. The first link provides the # same Nauka i Zhizn articles converted to the text form (but misses # time belt changes map). # # The second Nauka i Zhizn article says that in addition to # introduction of summer time on 1981-04-01 there are some time belt # border changes on 1981-10-01, mostly affecting Nenets Autonomous # Okrug, Krasnoyarsk Krai, Yakutia, Magadan Oblast and Chukotka # according to the provided map (colored one). In addition to that # "time violators" (regions which were not using rules of the time # belts in which they were located) would not be moving off the DST on # 1981-10-01 to restore the decree time usage. (Komi ASSR was # supposed to repeat that move in October 1982 to account for the 2 # hour difference.) Map depicting "time violators" before 1981-10-01 # is also provided. # # The article from Trud says that 1981-10-01 changes caused problems # and some territories would be moved to pre-1981-10-01 time by not # moving to summer time on 1982-04-01. Namely: Dagestan, # Kabardino-Balkar, Kalmyk, Komi, Mari, Mordovian, North Ossetian, # Tatar, Chechen-Ingush and Chuvash ASSR, Krasnodar and Stavropol # krais, Arkhangelsk, Vladimir, Vologda, Voronezh, Gorky, Ivanovo, # Kostroma, Lipetsk, Penza, Rostov, Ryazan, Tambov, Tyumen and # Yaroslavl oblasts, Nenets and Evenk autonomous okrugs, Khatangsky # district of Taymyr Autonomous Okrug. As a result Evenk Autonomous # Okrug and Khatangsky district of Taymyr Autonomous Okrug would end # up on Moscow+4, Tyumen Oblast on Moscow+2 and the rest on Moscow # time. # # http://astrozet.net/files/Zones/DOC/RU/1980-925.txt # attributes the 1982 changes to the Act of the Council of Ministers # of the USSR No. 126 from 18.02.1982. 1980-925.txt also adds # Udmurtia to the list of affected territories and lists Khatangsky # district separately from Taymyr Autonomous Okrug. Probably erroneously. # # The affected territories are currently listed under Europe/Moscow, # Asia/Yekaterinburg and Asia/Krasnoyarsk. # # 12. Udmurtia # The fact that Udmurtia is depicted as a violator in the Nauka i # Zhizn article hints at Izhevsk being on different time from # Kuybyshev before 1981-10-01. Udmurtia is not mentioned in the 1989 act. # http://astrozet.net/files/Zones/DOC/RU/1980-925.txt # implies Udmurtia was on Moscow time after 1982-04-01. # Wikipedia implies Udmurtia being on Moscow+1 until 1991. # # ... # # All Russian zones are supposed to have by default a -1 change at # 1991-03-31 2:00 (cancellation of the decree time in the USSR) and a +1 # change at 1992-01-19 2:00 (restoration of the decree time in Russia). # # There were some exceptions, though. # Wikipedia says newspapers listed Astrakhan, Saratov, Kirov, Volgograd, # Izhevsk, Grozny, Kazan and Samara as such exceptions for the 1992 # change. (Different newspapers providing different lists. And some # lists found in the internet are quite wild.) # # And apparently some exceptions were reverted in the last moment. # http://www.kaliningradka.ru/site_pc/cherez/index.php?ELEMENT_ID=40091 # says that Kaliningrad decided not to be an exception 2 days before the # 1991-03-31 switch and one person at # https://izhevsk.ru/forum_light_message/50/682597-m8369040.html # says he remembers that Samara opted out of the 1992-01-19 exception # 2 days before the switch. # # From Alois Treindl (2022-02-15): # the Russian wikipedia page # https://ru.wikipedia.org/wiki/Мо�ков�кое_врем�#Перемещение_границы_применени�_мо�ков�кого_времени_на_во�ток # contains the sentence (in Google translation) "In the autumn of # 1981, Arkhangelsk, Vologda, Yaroslavl, Ivanovo, Vladimir, Ryazan, # Lipetsk, Voronezh, Rostov-on-Don, Krasnodar and regions to the east # of those named (about 30 in total) parted ways with Moscow time. # However, the convenience of common time with Moscow turned out to be # decisive - in 1982, these regions again switched to Moscow time." # Shanks International atlas has similar information, and also the # Russian book Zaitsev A., Kutalev D. A new astrologer's reference # book. Coordinates of cities and time corrections, - The World of # Urania, 2012 (Russian: Зайцев �., Куталёв Д., �овый �правочник # а�тролога. Координаты городов и временные поправки). # To me it seems that an extra zone is needed, which starts with LMT # util 1919, later follows Moscow since 1930, but deviates from it # between 1 October 1981 until 1 April 1982. # # # From Paul Eggert (2022-02-15): # Given the above, we appear to be missing some Zone entries for the # chaotic early 1980s in Russia. It's not clear what these entries # should be. For now, sweep this under the rug and just document the # time in Moscow. # From Vladimir Karpinsky (2014-07-08): # LMT in Moscow (before Jul 3, 1916) is 2:30:17, that was defined by Moscow # Observatory (coordinates: 55° 45' 29.70", 37° 34' 05.30").... # LMT in Moscow since Jul 3, 1916 is 2:31:01 as a result of new standard. # (The info is from the book by Byalokoz ... p. 18.) # The time in St. Petersburg as capital of Russia was defined by # Pulkov observatory, near St. Petersburg. In 1916 LMT Moscow # was synchronized with LMT St. Petersburg (+30 minutes), (Pulkov observatory # coordinates: 59° 46' 18.70", 30° 19' 40.70") so 30° 19' 40.70" > # 2h01m18.7s = 2:01:19. LMT Moscow = LMT St.Petersburg + 30m 2:01:19 + 0:30 = # 2:31:19 ... # # From Paul Eggert (2014-07-08): # Milne does not list Moscow, but suggests that its time might be listed in # Résumés mensuels et annuels des observations météorologiques (1895). # Presumably this is OCLC 85825704, a journal published with parallel text in # Russian and French. This source has not been located; go with Karpinsky. Zone Europe/Moscow 2:30:17 - LMT 1880 2:30:17 - MMT 1916 Jul 3 # Moscow Mean Time 2:31:19 Russia %s 1919 Jul 1 0:00u 3:00 Russia %s 1921 Oct 3:00 Russia MSK/MSD 1922 Oct 2:00 - EET 1930 Jun 21 3:00 Russia MSK/MSD 1991 Mar 31 2:00s 2:00 Russia EE%sT 1992 Jan 19 2:00s 3:00 Russia MSK/MSD 2011 Mar 27 2:00s 4:00 - MSK 2014 Oct 26 2:00s 3:00 - MSK # From Paul Eggert (2016-12-06): # Europe/Simferopol covers Crimea. Zone Europe/Simferopol 2:16:24 - LMT 1880 2:16 - SMT 1924 May 2 # Simferopol Mean T 2:00 - EET 1930 Jun 21 3:00 - MSK 1941 Nov 1:00 C-Eur CE%sT 1944 Apr 13 3:00 Russia MSK/MSD 1990 3:00 - MSK 1990 Jul 1 2:00 2:00 - EET 1992 Mar 20 # Central Crimea used Moscow time 1994/1997. # # From Paul Eggert (2022-07-21): # The _Economist_ (1994-05-28, p 45) reported that central Crimea switched # from Kyiv to Moscow time sometime after the January 1994 elections. # Shanks (1999) says "date of change uncertain", but implies that it happened # sometime between the 1994 DST switches. Shanks & Pottenger simply say # 1994-09-25 03:00, but that can't be right. For now, guess it # changed in May. This change evidently didn't last long; see below. 2:00 C-Eur EE%sT 1994 May # From IATA SSIM (1994/1997), which also said that Kerch is still like Kyiv. 3:00 C-Eur MSK/MSD 1996 Mar 31 0:00s 3:00 1:00 MSD 1996 Oct 27 3:00s # IATA SSIM (1997-09) said Crimea switched to EET/EEST. # Assume it happened in March by not changing the clocks. 3:00 - MSK 1997 Mar lastSun 1:00u # From Alexander Krivenyshev (2014-03-17): # time change at 2:00 (2am) on March 30, 2014 # https://vz.ru/news/2014/3/17/677464.html # From Tim Parenti (2022-07-01), per Paul Eggert (2014-03-30): # The clocks at the railway station in Simferopol were put forward from 22:00 # to 24:00 the previous day in a "symbolic ceremony"; however, per # contemporaneous news reports, "ordinary Crimeans [made] the daylight savings # time switch at 2am" on Sunday. # https://www.business-standard.com/article/pti-stories/crimea-to-set-clocks-to-russia-time-114033000014_1.html # https://www.reuters.com/article/us-ukraine-crisis-crimea-time/crimea-switches-to-moscow-time-amid-incorporation-frenzy-idUKBREA2S0LT20140329 # https://www.bbc.com/news/av/world-europe-26806583 2:00 EU EE%sT 2014 Mar 30 2:00 4:00 - MSK 2014 Oct 26 2:00s 3:00 - MSK # From Paul Eggert (2016-03-18): # Europe/Astrakhan covers: # 30 RU-AST Astrakhan Oblast # # The 1989 transition is from USSR act No. 227 (1989-03-14). # From Alexander Krivenyshev (2016-01-12): # On February 10, 2016 Astrakhan Oblast got approval by the Federation # Council to change its time zone to UTC+4 (from current UTC+3 Moscow time).... # This Federal Law shall enter into force on 27 March 2016 at 02:00. # From Matt Johnson (2016-03-09): # http://publication.pravo.gov.ru/Document/View/0001201602150056 Zone Europe/Astrakhan 3:12:12 - LMT 1924 May 3:00 - %z 1930 Jun 21 4:00 Russia %z 1989 Mar 26 2:00s 3:00 Russia %z 1991 Mar 31 2:00s 4:00 - %z 1992 Mar 29 2:00s 3:00 Russia %z 2011 Mar 27 2:00s 4:00 - %z 2014 Oct 26 2:00s 3:00 - %z 2016 Mar 27 2:00s 4:00 - %z # From Paul Eggert (2016-11-11): # Europe/Volgograd covers: # 34 RU-VGG Volgograd Oblast # The 1988 transition is from USSR act No. 5 (1988-01-04). # From Alexander Fetisov (2018-09-20): # Volgograd region in southern Russia (Europe/Volgograd) change # timezone from UTC+3 to UTC+4 from 28oct2018. # http://sozd.parliament.gov.ru/bill/452878-7 # # From Stepan Golosunov (2018-10-11): # The law has been published today on # http://publication.pravo.gov.ru/Document/View/0001201810110037 # From Alexander Krivenyshev (2020-11-27): # The State Duma approved (Nov 24, 2020) the transition of the Volgograd # region to the Moscow time zone.... # https://sozd.duma.gov.ru/bill/1012130-7 # # From Stepan Golosunov (2020-12-05): # Currently proposed text for the second reading (expected on December 8) ... # changes the date to December 27. https://v1.ru/text/gorod/2020/12/04/69601031/ # # From Stepan Golosunov (2020-12-22): # The law was published today on # http://publication.pravo.gov.ru/Document/View/0001202012220002 Zone Europe/Volgograd 2:57:40 - LMT 1920 Jan 3 3:00 - %z 1930 Jun 21 4:00 - %z 1961 Nov 11 4:00 Russia %z 1988 Mar 27 2:00s 3:00 Russia MSK/MSD 1991 Mar 31 2:00s 4:00 - %z 1992 Mar 29 2:00s 3:00 Russia MSK/MSD 2011 Mar 27 2:00s 4:00 - MSK 2014 Oct 26 2:00s 3:00 - MSK 2018 Oct 28 2:00s 4:00 - %z 2020 Dec 27 2:00s 3:00 - MSK # From Paul Eggert (2016-11-11): # Europe/Saratov covers: # 64 RU-SAR Saratov Oblast # From Yuri Konotopov (2016-11-11): # Dec 4, 2016 02:00 UTC+3.... Saratov Region's local time will be ... UTC+4. # From Stepan Golosunov (2016-11-11): # ... Byalokoz listed Saratov on 03:04:18. # From Stepan Golosunov (2016-11-22): # http://publication.pravo.gov.ru/Document/View/0001201611220031 Zone Europe/Saratov 3:04:18 - LMT 1919 Jul 1 0:00u 3:00 - %z 1930 Jun 21 4:00 Russia %z 1988 Mar 27 2:00s 3:00 Russia %z 1991 Mar 31 2:00s 4:00 - %z 1992 Mar 29 2:00s 3:00 Russia %z 2011 Mar 27 2:00s 4:00 - %z 2014 Oct 26 2:00s 3:00 - %z 2016 Dec 4 2:00s 4:00 - %z # From Paul Eggert (2016-03-18): # Europe/Kirov covers: # 43 RU-KIR Kirov Oblast # The 1989 transition is from USSR act No. 227 (1989-03-14). # Zone Europe/Kirov 3:18:48 - LMT 1919 Jul 1 0:00u 3:00 - %z 1930 Jun 21 4:00 Russia %z 1989 Mar 26 2:00s 3:00 Russia MSK/MSD 1991 Mar 31 2:00s 4:00 - %z 1992 Mar 29 2:00s 3:00 Russia MSK/MSD 2011 Mar 27 2:00s 4:00 - MSK 2014 Oct 26 2:00s 3:00 - MSK # From Tim Parenti (2014-07-03), per Oscar van Vlijmen (2001-08-25): # Europe/Samara covers... # 18 RU-UD Udmurt Republic # 63 RU-SAM Samara Oblast # From Paul Eggert (2016-03-18): # Byalokoz 1919 says Samara was 3:20:20. # The 1989 transition is from USSR act No. 227 (1989-03-14). Zone Europe/Samara 3:20:20 - LMT 1919 Jul 1 0:00u 3:00 - %z 1930 Jun 21 4:00 - %z 1935 Jan 27 4:00 Russia %z 1989 Mar 26 2:00s 3:00 Russia %z 1991 Mar 31 2:00s 2:00 Russia %z 1991 Sep 29 2:00s 3:00 - %z 1991 Oct 20 3:00 4:00 Russia %z 2010 Mar 28 2:00s 3:00 Russia %z 2011 Mar 27 2:00s 4:00 - %z # From Paul Eggert (2016-03-18): # Europe/Ulyanovsk covers: # 73 RU-ULY Ulyanovsk Oblast # The 1989 transition is from USSR act No. 227 (1989-03-14). # From Alexander Krivenyshev (2016-02-17): # Ulyanovsk ... on their way to change time zones by March 27, 2016 at 2am. # Ulyanovsk Oblast ... from MSK to MSK+1 (UTC+3 to UTC+4) ... # 920582-6 ... 02/17/2016 The State Duma passed the bill in the first reading. # From Matt Johnson (2016-03-09): # http://publication.pravo.gov.ru/Document/View/0001201603090051 Zone Europe/Ulyanovsk 3:13:36 - LMT 1919 Jul 1 0:00u 3:00 - %z 1930 Jun 21 4:00 Russia %z 1989 Mar 26 2:00s 3:00 Russia %z 1991 Mar 31 2:00s 2:00 Russia %z 1992 Jan 19 2:00s 3:00 Russia %z 2011 Mar 27 2:00s 4:00 - %z 2014 Oct 26 2:00s 3:00 - %z 2016 Mar 27 2:00s 4:00 - %z # From Tim Parenti (2014-07-03), per Oscar van Vlijmen (2001-08-25): # Asia/Yekaterinburg covers... # 02 RU-BA Bashkortostan, Republic of # 90 RU-PER Perm Krai # 45 RU-KGN Kurgan Oblast # 56 RU-ORE Orenburg Oblast # 66 RU-SVE Sverdlovsk Oblast # 72 RU-TYU Tyumen Oblast # 74 RU-CHE Chelyabinsk Oblast # 86 RU-KHM Khanty-Mansi Autonomous Okrug - Yugra # 89 RU-YAN Yamalo-Nenets Autonomous Okrug # # Note: Effective 2005-12-01, (59) Perm Oblast and (81) Komi-Permyak # Autonomous Okrug merged to form (90, RU-PER) Perm Krai. # Milne says Yekaterinburg was 4:02:32.9. # Byalokoz 1919 says its provincial time was based on Perm, at 3:45:05. # Assume it switched on 1916-07-03, the time of the new standard. # The 1919 and 1930 transitions are from Shanks. #STDOFF 4:02:32.9 Zone Asia/Yekaterinburg 4:02:33 - LMT 1916 Jul 3 3:45:05 - PMT 1919 Jul 15 4:00 4:00 - %z 1930 Jun 21 5:00 Russia %z 1991 Mar 31 2:00s 4:00 Russia %z 1992 Jan 19 2:00s 5:00 Russia %z 2011 Mar 27 2:00s 6:00 - %z 2014 Oct 26 2:00s 5:00 - %z # From Tim Parenti (2014-07-03), per Oscar van Vlijmen (2001-08-25): # Asia/Omsk covers... # 55 RU-OMS Omsk Oblast # Byalokoz 1919 says Omsk was 4:53:30. Zone Asia/Omsk 4:53:30 - LMT 1919 Nov 14 5:00 - %z 1930 Jun 21 6:00 Russia %z 1991 Mar 31 2:00s 5:00 Russia %z 1992 Jan 19 2:00s 6:00 Russia %z 2011 Mar 27 2:00s 7:00 - %z 2014 Oct 26 2:00s 6:00 - %z # From Paul Eggert (2016-02-22): # Asia/Barnaul covers: # 04 RU-AL Altai Republic # 22 RU-ALT Altai Krai # Data before 1991 are from Shanks & Pottenger. # From Stepan Golosunov (2016-03-07): # Letter of Bank of Russia from 1995-05-25 # http://www.bestpravo.ru/rossijskoje/lj-akty/y3a.htm # suggests that Altai Republic transitioned to Moscow+3 on # 1995-05-28. # # https://regnum.ru/news/society/1957270.html # has some historical data for Altai Krai: # before 1957: west part on UT+6, east on UT+7 # after 1957: UT+7 # since 1995: UT+6 # http://barnaul.rusplt.ru/index/pochemu_altajskij_kraj_okazalsja_v_neprivychnom_chasovom_pojase-17648.html # confirms that and provides more details including 1995-05-28 transition date. # From Alexander Krivenyshev (2016-02-17): # Altai Krai and Altai Republic on their way to change time zones # by March 27, 2016 at 2am.... # Altai Republic / Gorno-Altaysk MSK+3 to MSK+4 (UTC+6 to UTC+7) ... # Altai Krai / Barnaul MSK+3 to MSK+4 (UTC+6 to UTC+7) # From Matt Johnson (2016-03-09): # http://publication.pravo.gov.ru/Document/View/0001201603090043 # http://publication.pravo.gov.ru/Document/View/0001201603090038 Zone Asia/Barnaul 5:35:00 - LMT 1919 Dec 10 6:00 - %z 1930 Jun 21 7:00 Russia %z 1991 Mar 31 2:00s 6:00 Russia %z 1992 Jan 19 2:00s 7:00 Russia %z 1995 May 28 6:00 Russia %z 2011 Mar 27 2:00s 7:00 - %z 2014 Oct 26 2:00s 6:00 - %z 2016 Mar 27 2:00s 7:00 - %z # From Paul Eggert (2016-03-18): # Asia/Novosibirsk covers: # 54 RU-NVS Novosibirsk Oblast # From Stepan Golosunov (2016-05-30): # http://asozd2.duma.gov.ru/main.nsf/(Spravka)?OpenAgent&RN=1085784-6 # moves Novosibirsk oblast from UTC+6 to UTC+7. # From Stepan Golosunov (2016-07-04): # The law was signed yesterday and published today on # http://publication.pravo.gov.ru/Document/View/0001201607040064 Zone Asia/Novosibirsk 5:31:40 - LMT 1919 Dec 14 6:00 6:00 - %z 1930 Jun 21 7:00 Russia %z 1991 Mar 31 2:00s 6:00 Russia %z 1992 Jan 19 2:00s 7:00 Russia %z 1993 May 23 # say Shanks & P. 6:00 Russia %z 2011 Mar 27 2:00s 7:00 - %z 2014 Oct 26 2:00s 6:00 - %z 2016 Jul 24 2:00s 7:00 - %z # From Paul Eggert (2016-03-18): # Asia/Tomsk covers: # 70 RU-TOM Tomsk Oblast # From Stepan Golosunov (2016-03-24): # Byalokoz listed Tomsk at 5:39:51. # From Stanislaw A. Kuzikowski (1994-06-29): # Tomsk is still 4 hours ahead of Moscow. # From Stepan Golosunov (2016-03-19): # http://pravo.gov.ru/proxy/ips/?docbody=&nd=102075743 # (fifth time belt being UTC+5+1(decree time) # / UTC+5+1(decree time)+1(summer time)) ... # Note that time belts (numbered from 2 (Moscow) to 12 according to their # GMT/UTC offset and having too many exceptions like regions formally # belonging to one belt but using time from another) were replaced # with time zones in 2011 with different numbering (there was a # 2-hour gap between second and third zones in 2011-2014). # From Stepan Golosunov (2016-04-12): # http://asozd2.duma.gov.ru/main.nsf/(SpravkaNew)?OpenAgent&RN=1006865-6 # This bill was approved in the first reading today. It moves Tomsk oblast # from UTC+6 to UTC+7 and is supposed to come into effect on 2016-05-29 at # 2:00. The bill needs to be approved in the second and the third readings by # the State Duma, approved by the Federation Council, signed by the President # and published to become a law. Minor changes in the text are to be expected # before the second reading (references need to be updated to account for the # recent changes). # # Judging by the ultra-short one-day amendments period, recent similar laws, # the State Duma schedule and the Federation Council schedule # http://www.duma.gov.ru/legislative/planning/day-shedule/por_vesna_2016/ # http://council.gov.ru/activity/meetings/schedule/63303 # I speculate that the final text of the bill will be proposed tomorrow, the # bill will be approved in the second and the third readings on Friday, # approved by the Federation Council on 2016-04-20, signed by the President and # published as a law around 2016-04-26. # From Matt Johnson (2016-04-26): # http://publication.pravo.gov.ru/Document/View/0001201604260048 Zone Asia/Tomsk 5:39:51 - LMT 1919 Dec 22 6:00 - %z 1930 Jun 21 7:00 Russia %z 1991 Mar 31 2:00s 6:00 Russia %z 1992 Jan 19 2:00s 7:00 Russia %z 2002 May 1 3:00 6:00 Russia %z 2011 Mar 27 2:00s 7:00 - %z 2014 Oct 26 2:00s 6:00 - %z 2016 May 29 2:00s 7:00 - %z # From Tim Parenti (2014-07-03): # Asia/Novokuznetsk covers... # 42 RU-KEM Kemerovo Oblast # From Alexander Krivenyshev (2009-10-13): # Kemerovo oblast' (Kemerovo region) in Russia will change current time zone on # March 28, 2010: # from current Russia Zone 6 - Krasnoyarsk Time Zone (KRA) UTC +0700 # to Russia Zone 5 - Novosibirsk Time Zone (NOV) UTC +0600 # # This is according to Government of Russia decree No. 740, on September # 14, 2009 "Application in the territory of the Kemerovo region the Fifth # time zone." ("Russia Zone 5" or old "USSR Zone 5" is GMT +0600) # # Russian Government web site (Russian language) # http://www.government.ru/content/governmentactivity/rfgovernmentdecisions/archive/2009/09/14/991633.htm # or Russian-English translation by WorldTimeZone.com with reference # map to local region and new Russia Time Zone map after March 28, 2010 # http://www.worldtimezone.com/dst_news/dst_news_russia03.html # # Thus, when Russia will switch to DST on the night of March 28, 2010 # Kemerovo region (Kemerovo oblast') will not change the clock. # From Tim Parenti (2014-07-02), per Alexander Krivenyshev (2014-07-02): # The Kemerovo region will remain at UTC+7 through the 2014-10-26 change, thus # realigning itself with KRAT. Zone Asia/Novokuznetsk 5:48:48 - LMT 1924 May 1 6:00 - %z 1930 Jun 21 7:00 Russia %z 1991 Mar 31 2:00s 6:00 Russia %z 1992 Jan 19 2:00s 7:00 Russia %z 2010 Mar 28 2:00s 6:00 Russia %z 2011 Mar 27 2:00s 7:00 - %z # From Tim Parenti (2014-07-03), per Oscar van Vlijmen (2001-08-25): # Asia/Krasnoyarsk covers... # 17 RU-TY Tuva Republic # 19 RU-KK Khakassia, Republic of # 24 RU-KYA Krasnoyarsk Krai # # Note: Effective 2007-01-01, (88) Evenk Autonomous Okrug and (84) Taymyr # Autonomous Okrug were merged into (24, RU-KYA) Krasnoyarsk Krai. # Byalokoz 1919 says Krasnoyarsk was 6:11:26. Zone Asia/Krasnoyarsk 6:11:26 - LMT 1920 Jan 6 6:00 - %z 1930 Jun 21 7:00 Russia %z 1991 Mar 31 2:00s 6:00 Russia %z 1992 Jan 19 2:00s 7:00 Russia %z 2011 Mar 27 2:00s 8:00 - %z 2014 Oct 26 2:00s 7:00 - %z # From Tim Parenti (2014-07-03), per Oscar van Vlijmen (2001-08-25): # Asia/Irkutsk covers... # 03 RU-BU Buryatia, Republic of # 38 RU-IRK Irkutsk Oblast # # Note: Effective 2008-01-01, (85) Ust-Orda Buryat Autonomous Okrug was # merged into (38, RU-IRK) Irkutsk Oblast. # Milne 1899 says Irkutsk was 6:57:15. # Byalokoz 1919 says Irkutsk was 6:57:05. # Go with Byalokoz. Zone Asia/Irkutsk 6:57:05 - LMT 1880 6:57:05 - IMT 1920 Jan 25 # Irkutsk Mean Time 7:00 - %z 1930 Jun 21 8:00 Russia %z 1991 Mar 31 2:00s 7:00 Russia %z 1992 Jan 19 2:00s 8:00 Russia %z 2011 Mar 27 2:00s 9:00 - %z 2014 Oct 26 2:00s 8:00 - %z # From Tim Parenti (2014-07-06): # Asia/Chita covers... # 92 RU-ZAB Zabaykalsky Krai # # Note: Effective 2008-03-01, (75) Chita Oblast and (80) Agin-Buryat # Autonomous Okrug merged to form (92, RU-ZAB) Zabaykalsky Krai. # From Alexander Krivenyshev (2016-01-02): # [The] time zone in the Trans-Baikal Territory (Zabaykalsky Krai) - # Asia/Chita [is changing] from UTC+8 to UTC+9. Effective date will # be March 27, 2016 at 2:00am.... # http://publication.pravo.gov.ru/Document/View/0001201512300107 Zone Asia/Chita 7:33:52 - LMT 1919 Dec 15 8:00 - %z 1930 Jun 21 9:00 Russia %z 1991 Mar 31 2:00s 8:00 Russia %z 1992 Jan 19 2:00s 9:00 Russia %z 2011 Mar 27 2:00s 10:00 - %z 2014 Oct 26 2:00s 8:00 - %z 2016 Mar 27 2:00 9:00 - %z # From Tim Parenti (2014-07-03), per Oscar van Vlijmen (2009-11-29): # Asia/Yakutsk covers... # 28 RU-AMU Amur Oblast # # ...and parts of (14, RU-SA) Sakha (Yakutia) Republic: # 14-02 **** Aldansky District # 14-04 **** Amginsky District # 14-05 **** Anabarsky District # 14-06 **** Bulunsky District # 14-07 **** Verkhnevilyuysky District # 14-10 **** Vilyuysky District # 14-11 **** Gorny District # 14-12 **** Zhigansky District # 14-13 **** Kobyaysky District # 14-14 **** Lensky District # 14-15 **** Megino-Kangalassky District # 14-16 **** Mirninsky District # 14-18 **** Namsky District # 14-19 **** Neryungrinsky District # 14-21 **** Nyurbinsky District # 14-23 **** Olenyoksky District # 14-24 **** Olyokminsky District # 14-26 **** Suntarsky District # 14-27 **** Tattinsky District # 14-29 **** Ust-Aldansky District # 14-32 **** Khangalassky District # 14-33 **** Churapchinsky District # 14-34 **** Eveno-Bytantaysky National District # From Tim Parenti (2014-07-03): # Our commentary seems to have lost mention of (14-19) Neryungrinsky District. # Since the surrounding districts of Sakha are all YAKT, assume this is, too. # Also assume its history has been the same as the rest of Asia/Yakutsk. # Byalokoz 1919 says Yakutsk was 8:38:58. Zone Asia/Yakutsk 8:38:58 - LMT 1919 Dec 15 8:00 - %z 1930 Jun 21 9:00 Russia %z 1991 Mar 31 2:00s 8:00 Russia %z 1992 Jan 19 2:00s 9:00 Russia %z 2011 Mar 27 2:00s 10:00 - %z 2014 Oct 26 2:00s 9:00 - %z # From Tim Parenti (2014-07-03), per Oscar van Vlijmen (2009-11-29): # Asia/Vladivostok covers... # 25 RU-PRI Primorsky Krai # 27 RU-KHA Khabarovsk Krai # 79 RU-YEV Jewish Autonomous Oblast # # ...and parts of (14, RU-SA) Sakha (Yakutia) Republic: # 14-09 **** Verkhoyansky District # 14-31 **** Ust-Yansky District # Milne 1899 says Vladivostok was 8:47:33.5. # Byalokoz 1919 says Vladivostok was 8:47:31. # Go with Byalokoz. Zone Asia/Vladivostok 8:47:31 - LMT 1922 Nov 15 9:00 - %z 1930 Jun 21 10:00 Russia %z 1991 Mar 31 2:00s 9:00 Russia %z 1992 Jan 19 2:00s 10:00 Russia %z 2011 Mar 27 2:00s 11:00 - %z 2014 Oct 26 2:00s 10:00 - %z # From Tim Parenti (2014-07-03): # Asia/Khandyga covers parts of (14, RU-SA) Sakha (Yakutia) Republic: # 14-28 **** Tomponsky District # 14-30 **** Ust-Maysky District # From Arthur David Olson (2022-03-21): # Tomponsky and Ust-Maysky switched from Vladivostok time to Yakutsk time # in 2011. # From Paul Eggert (2012-11-25): # Shanks and Pottenger (2003) has Khandyga on Yakutsk time. # Make a wild guess that it switched to Vladivostok time in 2004. # This transition is no doubt wrong, but we have no better info. Zone Asia/Khandyga 9:02:13 - LMT 1919 Dec 15 8:00 - %z 1930 Jun 21 9:00 Russia %z 1991 Mar 31 2:00s 8:00 Russia %z 1992 Jan 19 2:00s 9:00 Russia %z 2004 10:00 Russia %z 2011 Mar 27 2:00s 11:00 - %z 2011 Sep 13 0:00s # Decree 725? 10:00 - %z 2014 Oct 26 2:00s 9:00 - %z # From Tim Parenti (2014-07-03): # Asia/Sakhalin covers... # 65 RU-SAK Sakhalin Oblast # ...with the exception of: # 65-11 **** Severo-Kurilsky District (North Kuril Islands) # From Matt Johnson (2016-02-22): # Asia/Sakhalin is moving (in entirety) from UTC+10 to UTC+11 ... # (2016-03-09): # http://publication.pravo.gov.ru/Document/View/0001201603090044 # The Zone name should be Asia/Yuzhno-Sakhalinsk, but that's too long. Zone Asia/Sakhalin 9:30:48 - LMT 1905 Aug 23 9:00 - %z 1945 Aug 25 11:00 Russia %z 1991 Mar 31 2:00s # Sakhalin T 10:00 Russia %z 1992 Jan 19 2:00s 11:00 Russia %z 1997 Mar lastSun 2:00s 10:00 Russia %z 2011 Mar 27 2:00s 11:00 - %z 2014 Oct 26 2:00s 10:00 - %z 2016 Mar 27 2:00s 11:00 - %z # From Tim Parenti (2014-07-03), per Oscar van Vlijmen (2009-11-29): # Asia/Magadan covers... # 49 RU-MAG Magadan Oblast # From Tim Parenti (2014-07-06), per Alexander Krivenyshev (2014-07-02): # Magadan Oblast is moving from UTC+12 to UTC+10 on 2014-10-26; however, # several districts of Sakha Republic as well as Severo-Kurilsky District of # the Sakhalin Oblast (also known as the North Kuril Islands), represented # until now by Asia/Magadan, will instead move to UTC+11. These regions will # need their own zone. # From Alexander Krivenyshev (2016-03-27): # ... draft bill 948300-6 to change its time zone from UTC+10 to UTC+11 ... # will take ... effect ... on April 24, 2016 at 2 o'clock # # From Matt Johnson (2016-04-05): # ... signed by the President today ... # http://publication.pravo.gov.ru/Document/View/0001201604050038 Zone Asia/Magadan 10:03:12 - LMT 1924 May 2 10:00 - %z 1930 Jun 21 # Magadan Time 11:00 Russia %z 1991 Mar 31 2:00s 10:00 Russia %z 1992 Jan 19 2:00s 11:00 Russia %z 2011 Mar 27 2:00s 12:00 - %z 2014 Oct 26 2:00s 10:00 - %z 2016 Apr 24 2:00s 11:00 - %z # From Tim Parenti (2014-07-06): # Asia/Srednekolymsk covers parts of (14, RU-SA) Sakha (Yakutia) Republic: # 14-01 **** Abyysky District # 14-03 **** Allaikhovsky District # 14-08 **** Verkhnekolymsky District # 14-17 **** Momsky District # 14-20 **** Nizhnekolymsky District # 14-25 **** Srednekolymsky District # # ...and parts of (65, RU-SAK) Sakhalin Oblast: # 65-11 **** Severo-Kurilsky District (North Kuril Islands) # From Tim Parenti (2014-07-02): # Oymyakonsky District of Sakha Republic (represented by Ust-Nera), along with # most of Sakhalin Oblast (represented by Sakhalin) will be moving to UTC+10 on # 2014-10-26 to stay aligned with VLAT/SAKT; however, Severo-Kurilsky District # of the Sakhalin Oblast (also known as the North Kuril Islands, represented by # Severo-Kurilsk) will remain on UTC+11. # From Tim Parenti (2014-07-06): # Assume North Kuril Islands have history like Magadan before 2011-03-27. # There is a decent chance this is wrong, in which case a new zone # Asia/Severo-Kurilsk would become necessary. # # Srednekolymsk and Zyryanka are the most populous places amongst these # districts, but have very similar populations. In fact, Wikipedia currently # lists them both as having 3528 people, exactly 1668 males and 1860 females # each! (Yikes!) # https://en.wikipedia.org/w/?title=Srednekolymsky_District&oldid=603435276 # https://en.wikipedia.org/w/?title=Verkhnekolymsky_District&oldid=594378493 # Assume this is a mistake, albeit an amusing one. # # Looking at censuses, the populations of the two municipalities seem to have # fluctuated recently. Zyryanka was more populous than Srednekolymsk in the # 1989 and 2002 censuses, but Srednekolymsk was more populous in the most # recent (2010) census, 3525 to 3170. (See pages 195 and 197 of # http://www.gks.ru/free_doc/new_site/perepis2010/croc/Documents/Vol1/pub-01-05.pdf # in Russian.) In addition, Srednekolymsk appears to be a much older # settlement and the population of Zyryanka seems to be declining. # Go with Srednekolymsk. Zone Asia/Srednekolymsk 10:14:52 - LMT 1924 May 2 10:00 - %z 1930 Jun 21 11:00 Russia %z 1991 Mar 31 2:00s 10:00 Russia %z 1992 Jan 19 2:00s 11:00 Russia %z 2011 Mar 27 2:00s 12:00 - %z 2014 Oct 26 2:00s 11:00 - %z # From Tim Parenti (2014-07-03): # Asia/Ust-Nera covers parts of (14, RU-SA) Sakha (Yakutia) Republic: # 14-22 **** Oymyakonsky District # From Arthur David Olson (2022-03-21): # Oymyakonsky and the Kuril Islands switched from # Magadan time to Vladivostok time in 2011. # # From Tim Parenti (2014-07-06), per Alexander Krivenyshev (2014-07-02): # It's unlikely that any of the Kuril Islands were involved in such a switch, # as the South and Middle Kurils have been on UTC+11 (SAKT) with the rest of # Sakhalin Oblast since at least 2011-09, and the North Kurils have been on # UTC+12 since at least then, too. Zone Asia/Ust-Nera 9:32:54 - LMT 1919 Dec 15 8:00 - %z 1930 Jun 21 9:00 Russia %z 1981 Apr 1 11:00 Russia %z 1991 Mar 31 2:00s 10:00 Russia %z 1992 Jan 19 2:00s 11:00 Russia %z 2011 Mar 27 2:00s 12:00 - %z 2011 Sep 13 0:00s # Decree 725? 11:00 - %z 2014 Oct 26 2:00s 10:00 - %z # From Tim Parenti (2014-07-03), per Oscar van Vlijmen (2001-08-25): # Asia/Kamchatka covers... # 91 RU-KAM Kamchatka Krai # # Note: Effective 2007-07-01, (41) Kamchatka Oblast and (82) Koryak # Autonomous Okrug merged to form (91, RU-KAM) Kamchatka Krai. # The Zone name should be Asia/Petropavlovsk-Kamchatski or perhaps # Asia/Petropavlovsk-Kamchatsky, but these are too long. Zone Asia/Kamchatka 10:34:36 - LMT 1922 Nov 10 11:00 - %z 1930 Jun 21 12:00 Russia %z 1991 Mar 31 2:00s 11:00 Russia %z 1992 Jan 19 2:00s 12:00 Russia %z 2010 Mar 28 2:00s 11:00 Russia %z 2011 Mar 27 2:00s 12:00 - %z # From Tim Parenti (2014-07-03): # Asia/Anadyr covers... # 87 RU-CHU Chukotka Autonomous Okrug Zone Asia/Anadyr 11:49:56 - LMT 1924 May 2 12:00 - %z 1930 Jun 21 13:00 Russia %z 1982 Apr 1 0:00s 12:00 Russia %z 1991 Mar 31 2:00s 11:00 Russia %z 1992 Jan 19 2:00s 12:00 Russia %z 2010 Mar 28 2:00s 11:00 Russia %z 2011 Mar 27 2:00s 12:00 - %z # Bosnia & Herzegovina # Croatia # Kosovo # Montenegro # North Macedonia # Serbia # Slovenia # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Belgrade 1:22:00 - LMT 1884 1:00 - CET 1941 Apr 18 23:00 1:00 C-Eur CE%sT 1945 1:00 - CET 1945 May 8 2:00s 1:00 1:00 CEST 1945 Sep 16 2:00s # Metod Koželj reports that the legal date of # transition to EU rules was 1982-11-27, for all of Yugoslavia at the time. # Shanks & Pottenger don't give as much detail, so go with Koželj. 1:00 - CET 1982 Nov 27 1:00 EU CE%sT # Spain # # From Paul Eggert (2016-12-14): # # The source for Europe/Madrid before 2013 is: # Planesas P. La hora oficial en España y sus cambios. # Anuario del Observatorio Astronómico de Madrid (2013, in Spanish). # http://astronomia.ign.es/rknowsys-theme/images/webAstro/paginas/documentos/Anuario/lahoraoficialenespana.pdf # As this source says that historical time in the Canaries is obscure, # and it does not discuss Ceuta, stick with Shanks for now for that data. # # In the 1918 and 1919 fallback transitions in Spain, the clock for # the hour-longer day officially kept going after midnight, so that # the repeated instances of that day's 00:00 hour were 24 hours apart, # with a fallback transition from the second occurrence of 00:59... to # the next day's 00:00. Our data format cannot represent this # directly, and instead repeats the first hour of the next day, with a # fallback transition from the next day's 00:59... to 00:00. # From Michael Deckers (2016-12-15): # The Royal Decree of 1900-07-26 quoted by Planesas, online at # https://www.boe.es/datos/pdfs/BOE//1900/209/A00383-00384.pdf # says in its article 5 (my translation): # These dispositions will enter into force beginning with the # instant at which, according to the time indicated in article 1, # the 1st day of January of 1901 will begin. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Spain 1918 only - Apr 15 23:00 1:00 S Rule Spain 1918 1919 - Oct 6 24:00s 0 - Rule Spain 1919 only - Apr 6 23:00 1:00 S Rule Spain 1924 only - Apr 16 23:00 1:00 S Rule Spain 1924 only - Oct 4 24:00s 0 - Rule Spain 1926 only - Apr 17 23:00 1:00 S Rule Spain 1926 1929 - Oct Sat>=1 24:00s 0 - Rule Spain 1927 only - Apr 9 23:00 1:00 S Rule Spain 1928 only - Apr 15 0:00 1:00 S Rule Spain 1929 only - Apr 20 23:00 1:00 S # Republican Spain during the civil war; it controlled Madrid until 1939-03-28. Rule Spain 1937 only - Jun 16 23:00 1:00 S Rule Spain 1937 only - Oct 2 24:00s 0 - Rule Spain 1938 only - Apr 2 23:00 1:00 S Rule Spain 1938 only - Apr 30 23:00 2:00 M Rule Spain 1938 only - Oct 2 24:00 1:00 S # The following rules are for unified Spain again. # # Planesas does not say what happened in Madrid between its fall on # 1939-03-28 and the Nationalist spring-forward transition on # 1939-04-15. For lack of better info, assume Madrid's clocks did not # change during that period. # # The first rule is commented out, as it is redundant for Republican Spain. #Rule Spain 1939 only - Apr 15 23:00 1:00 S Rule Spain 1939 only - Oct 7 24:00s 0 - Rule Spain 1942 only - May 2 23:00 1:00 S Rule Spain 1942 only - Sep 1 1:00 0 - Rule Spain 1943 1946 - Apr Sat>=13 23:00 1:00 S Rule Spain 1943 1944 - Oct Sun>=1 1:00 0 - Rule Spain 1945 1946 - Sep lastSun 1:00 0 - Rule Spain 1949 only - Apr 30 23:00 1:00 S Rule Spain 1949 only - Oct 2 1:00 0 - Rule Spain 1974 1975 - Apr Sat>=12 23:00 1:00 S Rule Spain 1974 1975 - Oct Sun>=1 1:00 0 - Rule Spain 1976 only - Mar 27 23:00 1:00 S Rule Spain 1976 1977 - Sep lastSun 1:00 0 - Rule Spain 1977 only - Apr 2 23:00 1:00 S Rule Spain 1978 only - Apr 2 2:00s 1:00 S Rule Spain 1978 only - Oct 1 2:00s 0 - # Nationalist Spain during the civil war #Rule NatSpain 1937 only - May 22 23:00 1:00 S #Rule NatSpain 1937 1938 - Oct Sat>=1 24:00s 0 - #Rule NatSpain 1938 only - Mar 26 23:00 1:00 S # The following rules are copied from Morocco from 1967 through 1978, # except with "S" letters. Rule SpainAfrica 1967 only - Jun 3 12:00 1:00 S Rule SpainAfrica 1967 only - Oct 1 0:00 0 - Rule SpainAfrica 1974 only - Jun 24 0:00 1:00 S Rule SpainAfrica 1974 only - Sep 1 0:00 0 - Rule SpainAfrica 1976 1977 - May 1 0:00 1:00 S Rule SpainAfrica 1976 only - Aug 1 0:00 0 - Rule SpainAfrica 1977 only - Sep 28 0:00 0 - Rule SpainAfrica 1978 only - Jun 1 0:00 1:00 S Rule SpainAfrica 1978 only - Aug 4 0:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Madrid -0:14:44 - LMT 1901 Jan 1 0:00u 0:00 Spain WE%sT 1940 Mar 16 23:00 1:00 Spain CE%sT 1979 1:00 EU CE%sT Zone Africa/Ceuta -0:21:16 - LMT 1901 Jan 1 0:00u 0:00 - WET 1918 May 6 23:00 0:00 1:00 WEST 1918 Oct 7 23:00 0:00 - WET 1924 0:00 Spain WE%sT 1929 0:00 - WET 1967 # Help zishrink.awk. 0:00 SpainAfrica WE%sT 1984 Mar 16 1:00 - CET 1986 1:00 EU CE%sT Zone Atlantic/Canary -1:01:36 - LMT 1922 Mar # Las Palmas de Gran C. -1:00 - %z 1946 Sep 30 1:00 0:00 - WET 1980 Apr 6 0:00s 0:00 1:00 WEST 1980 Sep 28 1:00u 0:00 EU WE%sT # IATA SSIM (1996-09) says the Canaries switch at 2:00u, not 1:00u. # Ignore this for now, as the Canaries are part of the EU. # Germany (Busingen enclave) # Liechtenstein # Switzerland # # From Howse: # By the end of the 18th century clocks and watches became commonplace # and their performance improved enormously. Communities began to keep # mean time in preference to apparent time - Geneva from 1780 .... # Rule NAME FROM TO - IN ON AT SAVE LETTER/S # From Whitman (who writes "Midnight?"): # Rule Swiss 1940 only - Nov 2 0:00 1:00 S # Rule Swiss 1940 only - Dec 31 0:00 0 - # From Shanks & Pottenger: # Rule Swiss 1941 1942 - May Sun>=1 2:00 1:00 S # Rule Swiss 1941 1942 - Oct Sun>=1 0:00 0 - # From Alois Treindl (2008-12-17): # I have researched the DST usage in Switzerland during the 1940ies. # # As I wrote in an earlier message, I suspected the current tzdata values # to be wrong. This is now verified. # # I have found copies of the original ruling by the Swiss Federal # government, in 'Eidgenössische Gesetzessammlung 1941 and 1942' (Swiss # federal law collection)... # # DST began on Monday 5 May 1941, 1:00 am by shifting the clocks to 2:00 am # DST ended on Monday 6 Oct 1941, 2:00 am by shifting the clocks to 1:00 am. # # DST began on Monday, 4 May 1942 at 01:00 am # DST ended on Monday, 5 Oct 1942 at 02:00 am # # There was no DST in 1940, I have checked the law collection carefully. # It is also indicated by the fact that the 1942 entry in the law # collection points back to 1941 as a reference, but no reference to any # other years are made. # # Newspaper articles I have read in the archives on 6 May 1941 reported # about the introduction of DST (Sommerzeit in German) during the previous # night as an absolute novelty, because this was the first time that such # a thing had happened in Switzerland. # # I have also checked 1916, because one book source (Gabriel, Traité de # l'heure dans le monde) claims that Switzerland had DST in 1916. This is # false, no official document could be found. Probably Gabriel got misled # by references to Germany, which introduced DST in 1916 for the first time. # # The tzdata rules for Switzerland must be changed to: # Rule Swiss 1941 1942 - May Mon>=1 1:00 1:00 S # Rule Swiss 1941 1942 - Oct Mon>=1 2:00 0 - # # The 1940 rules must be deleted. # # One further detail for Switzerland, which is probably out of scope for # most users of tzdata: The [Europe/Zurich zone] ... # describes all of Switzerland correctly, with the exception of # the Canton de Genève (Geneva, Genf). Between 1848 and 1894 Geneva did not # follow Bern Mean Time but kept its own local mean time. # To represent this, an extra zone would be needed. # # From Alois Treindl (2013-09-11): # The Federal regulations say # https://www.admin.ch/opc/de/classified-compilation/20071096/index.html # ... the meridian for Bern mean time ... is 7° 26' 22.50". # Expressed in time, it is 0h29m45.5s. # From Pierre-Yves Berger (2013-09-11): # the "Circulaire du conseil fédéral" (December 11 1893) # http://www.amtsdruckschriften.bar.admin.ch/viewOrigDoc.do?id=10071353 # clearly states that the [1894-06-01] change should be done at midnight # but if no one is present after 11 at night, could be postponed until one # hour before the beginning of service. # From Paul Eggert (2024-05-24): # Express BMT as 0:29:45.500, approximately the same precision 7° 26' 22.50". # # We can find no reliable source for Shanks's assertion that all of Switzerland # except Geneva switched to Bern Mean Time at 00:00 on 1848-09-12. This book: # # Jakob Messerli. Gleichmässig, pünktlich, schnell. Zeiteinteilung und # Zeitgebrauch in der Schweiz im 19. Jahrhundert. Chronos, Zurich 1995, # ISBN 3-905311-68-2, OCLC 717570797. # # suggests that the transition was more gradual, and that the Swiss did not # agree about civil time during the transition. The timekeeping it gives the # most detail for is postal and telegraph time: here, federal legislation (the # "Bundesgesetz über die Erstellung von elektrischen Telegraphen") passed on # 1851-11-23, and an official implementation notice was published 1853-07-16 # (Bundesblatt 1853, Bd. II, S. 859). On p 72 Messerli writes that in # practice since July 1853 Bernese time was used in "all postal and telegraph # offices in Switzerland from Geneva to St. Gallen and Basel to Chiasso" # (Google translation). For now, model this transition as occurring on # 1853-07-16, though it probably occurred at some other date in Zurich, and # legal civil time probably changed at still some other transition date. # From Tobias Conradi (2011-09-12): # Büsingen , surrounded by the Swiss canton # Schaffhausen, did not start observing DST in 1980 as the rest of DE # (West Germany at that time) and DD (East Germany at that time) did. # DD merged into DE, the area is currently covered by code DE in ISO 3166-1, # which in turn is covered by the zone Europe/Berlin. # # Source for the time in Büsingen 1980: # http://www.srf.ch/player/video?id=c012c029-03b7-4c2b-9164-aa5902cd58d3 # # From Arthur David Olson (2012-03-03): # Büsingen and Zurich have shared clocks since 1970. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Swiss 1941 1942 - May Mon>=1 1:00 1:00 S Rule Swiss 1941 1942 - Oct Mon>=1 2:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Zurich 0:34:08 - LMT 1853 Jul 16 # See above comment. #STDOFF 0:29:45.500 0:29:46 - BMT 1894 Jun # Bern Mean Time 1:00 Swiss CE%sT 1981 1:00 EU CE%sT # Turkey # From Alois Treindl (2019-08-12): # http://www.astrolojidergisi.com/yazsaati.htm has researched the time zone # history of Turkey, based on newspaper archives and official documents. # From Paul Eggert (2019-08-28): # That source (Oya Vulaş, "Türkiye'de Yaz Saati Uygulamaları") # is used for 1940/1972, where it seems more reliable than our other # sources. # From Kıvanç Yazan (2019-08-12): # http://www.resmigazete.gov.tr/arsiv/14539.pdf#page=24 # 1973-06-03 01:00 -> 02:00, 1973-11-04 02:00 -> 01:00 # # http://www.resmigazete.gov.tr/arsiv/14829.pdf#page=1 # 1974-03-31 02:00 -> 03:00, 1974-11-03 02:00 -> 01:00 # # http://www.resmigazete.gov.tr/arsiv/15161.pdf#page=1 # 1975-03-22 02:00 -> 03:00, 1975-11-02 02:00 -> 01:00 # # http://www.resmigazete.gov.tr/arsiv/15535_1.pdf#page=1 # 1976-03-21 02:00 -> 03:00, 1976-10-31 02:00 -> 01:00 # # http://www.resmigazete.gov.tr/arsiv/15778.pdf#page=5 # 1977-04-03 02:00 -> 03:00, 1977-10-16 02:00 -> 01:00, # 1978-04-02 02:00 -> 03:00 (not applied, see below) # 1978-10-15 02:00 -> 01:00 (not applied, see below) # 1979-04-01 02:00 -> 03:00 (not applied, see below) # 1979-10-14 02:00 -> 01:00 (not applied, see below) # # http://www.resmigazete.gov.tr/arsiv/16245.pdf#page=17 # This cancels the previous decision, and repeats it only for 1978. # 1978-04-02 02:00 -> 03:00, 1978-10-15 02:00 -> 01:00 # (not applied due to standard TZ change below) # # http://www.resmigazete.gov.tr/arsiv/16331.pdf#page=3 # This decision changes the default longitude for Turkish time zone from 30 # degrees East to 45 degrees East. This means a standard TZ change, from +2 # to +3. This is published & applied on 1978-06-29. At that time, Turkey was # already on summer time (already on 45E). Hence, this new law just meant an # "continuous summer time". Note that this was reversed in a few years. # # http://www.resmigazete.gov.tr/arsiv/18119_1.pdf#page=1 # 1983-07-31 02:00 -> 03:00 (note that this jumps TZ to +4) # 1983-10-02 02:00 -> 01:00 (back to +3) # # http://www.resmigazete.gov.tr/arsiv/18561.pdf (page 1 and 34) # At this time, Turkey is still on +3 with no spring-forward on early # 1984. This decision is published on 10/31/1984. Page 1 declares # the decision of reverting the "default longitude change". So the # standard time should go back to +3 (30E). And page 34 explains when # that will happen: 1984-11-01 02:00 -> 01:00. You can think of this # as "end of continuous summer time, change of standard time zone". # # http://www.resmigazete.gov.tr/arsiv/18713.pdf#page=1 # 1985-04-20 01:00 -> 02:00, 1985-09-28 02:00 -> 01:00 # From Kıvanç Yazan (2016-09-25): # 1) For 1986-2006, DST started at 01:00 local and ended at 02:00 local, with # no exceptions. # 2) 1994's lastSun was overridden with Mar 20 ... # Here are official papers: # http://www.resmigazete.gov.tr/arsiv/19032.pdf#page=2 for 1986 # http://www.resmigazete.gov.tr/arsiv/19400.pdf#page=4 for 1987 # http://www.resmigazete.gov.tr/arsiv/19752.pdf#page=15 for 1988 # http://www.resmigazete.gov.tr/arsiv/20102.pdf#page=6 for 1989 # http://www.resmigazete.gov.tr/arsiv/20464.pdf#page=1 for 1990 - 1992 # http://www.resmigazete.gov.tr/arsiv/21531.pdf#page=15 for 1993 - 1995 # http://www.resmigazete.gov.tr/arsiv/21879.pdf#page=1 for overriding 1994 # http://www.resmigazete.gov.tr/arsiv/22588.pdf#page=1 for 1996, 1997 # http://www.resmigazete.gov.tr/arsiv/23286.pdf#page=10 for 1998 - 2000 # http://www.resmigazete.gov.tr/eskiler/2001/03/20010324.htm#2 - for 2001 # http://www.resmigazete.gov.tr/eskiler/2002/03/20020316.htm#2 - for 2002-2006 # From Paul Eggert (2016-09-25): # Prefer the above sources to Shanks & Pottenger for timestamps after 1985. # From Steffen Thorsen (2007-03-09): # Starting 2007 though, it seems that they are adopting EU's 1:00 UTC # start/end time, according to the following page (2007-03-07): # http://www.ntvmsnbc.com/news/402029.asp # The official document is located here - it is in Turkish...: # http://rega.basbakanlik.gov.tr/eskiler/2007/03/20070307-7.htm # I was able to locate the following seemingly official document # (on a non-government server though) describing dates between 2002 and 2006: # http://www.alomaliye.com/bkk_2002_3769.htm # From Gökdeniz Karadağ (2011-03-10): # According to the articles linked below, Turkey will change into summer # time zone (GMT+3) on March 28, 2011 at 3:00 a.m. instead of March 27. # This change is due to a nationwide exam on 27th. # https://www.worldbulletin.net/?aType=haber&ArticleID=70872 # Turkish: # https://www.hurriyet.com.tr/yaz-saati-uygulamasi-bir-gun-ileri-alindi-17230464 # From Faruk Pasin (2014-02-14): # The DST for Turkey has been changed for this year because of the # Turkish Local election.... # http://www.sabah.com.tr/Ekonomi/2014/02/12/yaz-saatinde-onemli-degisiklik # ... so Turkey will move clocks forward one hour on March 31 at 3:00 a.m. # From Randal L. Schwartz (2014-04-15): # Having landed on a flight from the states to Istanbul (via AMS) on March 31, # I can tell you that NOBODY (even the airlines) respected this timezone DST # change delay. Maybe the word just didn't get out in time. # From Paul Eggert (2014-06-15): # The press reported massive confusion, as election officials obeyed the rule # change but cell phones (and airline baggage systems) did not. See: # Kostidis M. Eventful elections in Turkey. Balkan News Agency # http://www.balkaneu.com/eventful-elections-turkey/ 2014-03-30. # I guess the best we can do is document the official time. # From Fatih (2015-09-29): # It's officially announced now by the Ministry of Energy. # Turkey delays winter time to 8th of November 04:00 # http://www.aa.com.tr/tr/turkiye/yaz-saati-uygulamasi-8-kasimda-sona-erecek/362217 # # From BBC News (2015-10-25): # Confused Turks are asking "what's the time?" after automatic clocks defied a # government decision ... "For the next two weeks #Turkey is on EEST... Erdogan # Engineered Standard Time," said Twitter user @aysekarahasan. # http://www.bbc.com/news/world-europe-34631326 # From Burak AYDIN (2016-09-08): # Turkey will stay in Daylight Saving Time even in winter.... # http://www.resmigazete.gov.tr/eskiler/2016/09/20160908-2.pdf # # From Paul Eggert (2016-09-07): # The change is permanent, so this is the new standard time in Turkey. # It takes effect today, which is not much notice. # From Kıvanç Yazan (2017-10-28): # Turkey will go back to Daylight Saving Time starting 2018-10. # http://www.resmigazete.gov.tr/eskiler/2017/10/20171028-5.pdf # # From Even Scharning (2017-11-08): # ... today it was announced that the DST will become "continuous": # http://www.hurriyet.com.tr/son-dakika-yaz-saati-uygulamasi-surekli-hale-geldi-40637482 # From Paul Eggert (2017-11-08): # Although Google Translate misfires on that source, it looks like # Turkey reversed last month's decision, and so will stay at +03. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Turkey 1916 only - May 1 0:00 1:00 S Rule Turkey 1916 only - Oct 1 0:00 0 - Rule Turkey 1920 only - Mar 28 0:00 1:00 S Rule Turkey 1920 only - Oct 25 0:00 0 - Rule Turkey 1921 only - Apr 3 0:00 1:00 S Rule Turkey 1921 only - Oct 3 0:00 0 - Rule Turkey 1922 only - Mar 26 0:00 1:00 S Rule Turkey 1922 only - Oct 8 0:00 0 - # Whitman gives 1923 Apr 28 - Sep 16 and no DST in 1924-1925; # go with Shanks & Pottenger. Rule Turkey 1924 only - May 13 0:00 1:00 S Rule Turkey 1924 1925 - Oct 1 0:00 0 - Rule Turkey 1925 only - May 1 0:00 1:00 S Rule Turkey 1940 only - Jul 1 0:00 1:00 S Rule Turkey 1940 only - Oct 6 0:00 0 - Rule Turkey 1940 only - Dec 1 0:00 1:00 S Rule Turkey 1941 only - Sep 21 0:00 0 - Rule Turkey 1942 only - Apr 1 0:00 1:00 S Rule Turkey 1945 only - Oct 8 0:00 0 - Rule Turkey 1946 only - Jun 1 0:00 1:00 S Rule Turkey 1946 only - Oct 1 0:00 0 - Rule Turkey 1947 1948 - Apr Sun>=16 0:00 1:00 S Rule Turkey 1947 1951 - Oct Sun>=2 0:00 0 - Rule Turkey 1949 only - Apr 10 0:00 1:00 S Rule Turkey 1950 only - Apr 16 0:00 1:00 S Rule Turkey 1951 only - Apr 22 0:00 1:00 S # DST for 15 months; unusual but we'll let it pass. Rule Turkey 1962 only - Jul 15 0:00 1:00 S Rule Turkey 1963 only - Oct 30 0:00 0 - Rule Turkey 1964 only - May 15 0:00 1:00 S Rule Turkey 1964 only - Oct 1 0:00 0 - Rule Turkey 1973 only - Jun 3 1:00 1:00 S Rule Turkey 1973 1976 - Oct Sun>=31 2:00 0 - Rule Turkey 1974 only - Mar 31 2:00 1:00 S Rule Turkey 1975 only - Mar 22 2:00 1:00 S Rule Turkey 1976 only - Mar 21 2:00 1:00 S Rule Turkey 1977 1978 - Apr Sun>=1 2:00 1:00 S Rule Turkey 1977 1978 - Oct Sun>=15 2:00 0 - Rule Turkey 1978 only - Jun 29 0:00 0 - Rule Turkey 1983 only - Jul 31 2:00 1:00 S Rule Turkey 1983 only - Oct 2 2:00 0 - Rule Turkey 1985 only - Apr 20 1:00s 1:00 S Rule Turkey 1985 only - Sep 28 1:00s 0 - Rule Turkey 1986 1993 - Mar lastSun 1:00s 1:00 S Rule Turkey 1986 1995 - Sep lastSun 1:00s 0 - Rule Turkey 1994 only - Mar 20 1:00s 1:00 S Rule Turkey 1995 2006 - Mar lastSun 1:00s 1:00 S Rule Turkey 1996 2006 - Oct lastSun 1:00s 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Istanbul 1:55:52 - LMT 1880 1:56:56 - IMT 1910 Oct # Istanbul Mean Time? 2:00 Turkey EE%sT 1978 Jun 29 3:00 Turkey %z 1984 Nov 1 2:00 2:00 Turkey EE%sT 2007 2:00 EU EE%sT 2011 Mar 27 1:00u 2:00 - EET 2011 Mar 28 1:00u 2:00 EU EE%sT 2014 Mar 30 1:00u 2:00 - EET 2014 Mar 31 1:00u 2:00 EU EE%sT 2015 Oct 25 1:00u 2:00 1:00 EEST 2015 Nov 8 1:00u 2:00 EU EE%sT 2016 Sep 7 3:00 - %z # Ukraine # # From Alois Treindl (2014-03-01): # REGULATION A N O V A on March 20, 1992 N 139 ... means that from # 1992 on, Ukraine had DST with begin time at 02:00 am, on last Sunday # in March, and end time 03:00 am, last Sunday in September.... # CABINET OF MINISTERS OF UKRAINE RESOLUTION on May 13, 1996 N 509 # "On the order of computation time on the territory of Ukraine" .... # As this cabinet decision is from May 1996, it seems likely that the # transition in March 1996, which predates it, was still at 2:00 am # and not at 3:00 as would have been under EU rules. # This is why I have set the change to EU rules into May 1996, # so that the change in March is stil covered by the Ukraine rule. # The next change in October 1996 happened under EU rules. # # From Paul Eggert (2022-08-27): # For now, assume that Ukraine's zones all followed the same rules, # except that Crimea switched to Moscow time in 1994 as described elsewhere. # From Igor Karpov, who works for the Ukrainian Ministry of Justice, # via Garrett Wollman (2003-01-27): # BTW, I've found the official document on this matter. It's government # regulations No. 509, May 13, 1996. In my poor translation it says: # "Time in Ukraine is set to second timezone (Kiev time). Each last Sunday # of March at 3am the time is changing to 4am and each last Sunday of # October the time at 4am is changing to 3am" # From Alexander Krivenyshev (2011-09-20): # On September 20, 2011 the deputies of the Verkhovna Rada agreed to # abolish the transfer clock to winter time. # # Bill No. 8330 of MP from the Party of Regions Oleg Nadoshi got # approval from 266 deputies. # # Ukraine abolishes transfer back to the winter time (in Russian) # http://news.mail.ru/politics/6861560/ # # The Ukrainians will no longer change the clock (in Russian) # http://www.segodnya.ua/news/14290482.html # # Deputies cancelled the winter time (in Russian) # https://www.pravda.com.ua/rus/news/2011/09/20/6600616/ # # From Philip Pizzey (2011-10-18): # Today my Ukrainian colleagues have informed me that the # Ukrainian parliament have decided that they will go to winter # time this year after all. # # From Udo Schwedt (2011-10-18): # As far as I understand, the recent change to the Ukrainian time zone # (Europe/Kiev) to introduce permanent daylight saving time (similar # to Russia) was reverted today: # http://portal.rada.gov.ua/rada/control/en/publish/article/info_left?art_id=287324&cat_id=105995 # # Also reported by Alexander Bokovoy (2011-10-18) who also noted: # The law documents themselves are at # http://w1.c1.rada.gov.ua/pls/zweb_n/webproc4_1?id=&pf3511=41484 # From Vladimir in Moscow via Alois Treindl re Kyiv time 1991/2 (2014-02-28): # First in Ukraine they changed Time zone from UTC+3 to UTC+2 with DST: # 03 25 1990 02:00 -03.00 1 Time Zone 3 with DST # 07 01 1990 02:00 -02.00 1 Time Zone 2 with DST # * Ukrainian Government's Resolution of 18.06.1990, No. 134. # http://search.ligazakon.ua/l_doc2.nsf/link1/T001500.html # # They did not end DST in September, 1990 (according to the law, # "summer time" was still in action): # 09 30 1990 03:00 -02.00 1 Time Zone 2 with DST # * Ukrainian Government's Resolution of 21.09.1990, No. 272. # http://search.ligazakon.ua/l_doc2.nsf/link1/KP900272.html # # Again no change in March, 1991 ("summer time" in action): # 03 31 1991 02:00 -02.00 1 Time Zone 2 with DST # # DST ended in September 1991 ("summer time" ended): # 09 29 1991 03:00 -02.00 0 Time Zone 2, no DST # * Ukrainian Government's Resolution of 25.09.1991, No. 225. # http://www.uazakon.com/documents/date_21/pg_iwgdoc.htm # This is an answer. # # Since 1992 they had normal DST procedure: # 03 29 1992 02:00 -02.00 1 DST started # 09 27 1992 03:00 -02.00 0 DST ended # * Ukrainian Government's Resolution of 20.03.1992, No. 139. # http://www.uazakon.com/documents/date_8u/pg_grcasa.htm # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Europe/Kyiv 2:02:04 - LMT 1880 2:02:04 - KMT 1924 May 2 # Kyiv Mean Time 2:00 - EET 1930 Jun 21 3:00 - MSK 1941 Sep 20 1:00 C-Eur CE%sT 1943 Nov 6 3:00 Russia MSK/MSD 1990 Jul 1 2:00 2:00 1:00 EEST 1991 Sep 29 3:00 2:00 C-Eur EE%sT 1996 May 13 2:00 EU EE%sT ############################################################################### # One source shows that Bulgaria, Cyprus, Finland, and Greece observe DST from # the last Sunday in March to the last Sunday in September in 1986. # The source shows Romania changing a day later than everybody else. # # According to Bernard Sieloff's source, Poland is in the MET time zone but # uses the WE DST rules. The Western USSR uses EET+1 and ME DST rules. # Bernard Sieloff's source claims Romania switches on the same day, but at # 00:00 standard time (i.e., 01:00 DST). It also claims that Turkey # switches on the same day, but switches on at 01:00 standard time # and off at 00:00 standard time (i.e., 01:00 DST) # ... # Date: Wed, 28 Jan 87 16:56:27 -0100 # From: Tom Hofmann # ... # # ...the European time rules are...standardized since 1981, when # most European countries started DST. Before that year, only # a few countries (UK, France, Italy) had DST, each according # to own national rules. In 1981, however, DST started on # 'Apr firstSun', and not on 'Mar lastSun' as in the following # years... # But also since 1981 there are some more national exceptions # than listed in 'europe': Switzerland, for example, joined DST # one year later, Denmark ended DST on 'Oct 1' instead of 'Sep # lastSun' in 1981 - I don't know how they handle now. # # Finally, DST ist always from 'Apr 1' to 'Oct 1' in the # Soviet Union (as far as I know). # # Tom Hofmann, Scientific Computer Center, CIBA-GEIGY AG, # 4002 Basle, Switzerland # ... # ... # Date: Wed, 4 Feb 87 22:35:22 +0100 # From: Dik T. Winter # ... # # The information from Tom Hofmann is (as far as I know) not entirely correct. # After a request from chongo at amdahl I tried to retrieve all information # about DST in Europe. I was able to find all from about 1969. # # ...standardization on DST in Europe started in about 1977 with switches on # first Sunday in April and last Sunday in September... # In 1981 UK joined Europe insofar that # the starting day for both shifted to last Sunday in March. And from 1982 # the whole of Europe used DST, with switch dates April 1 and October 1 in # the Sov[i]et Union. In 1985 the SU reverted to standard Europe[a]n switch # dates... # # It should also be remembered that time-zones are not constants; e.g. # Portugal switched in 1976 from MET (or CET) to WET with DST... # Note also that though there were rules for switch dates not # all countries abided to these dates, and many individual deviations # occurred, though not since 1982 I believe. Another note: it is always # assumed that DST is 1 hour ahead of normal time, this need not be the # case; at least in the Netherlands there have been times when DST was 2 hours # in advance of normal time. # # ... # dik t. winter, cwi, amsterdam, nederland # ... # From Bob Devine (1988-01-28): # ... # Greece: Last Sunday in April to last Sunday in September (iffy on dates). # Since 1978. Change at midnight. # ... # Monaco: has same DST as France. # ... pr0m1th3as-datatypes-9450b29/inst/tzdata/factory000066400000000000000000000017351527370255000215420ustar00rootroot00000000000000# tzdb data for noncommittal factory settings # This file is in the public domain, so clarified as of # 2009-05-17 by Arthur David Olson. # For distributors who don't want to specify a timezone in their # installation procedures. Users who run 'date' will get the # time zone abbreviation "-00", indicating that the actual time zone # is unknown. # TZ="Factory" was added to TZDB in 1989, and in 2016 its abbreviation # was changed to "-00" from a longish English-language error message. # Around 2010, CLDR added "Etc/Unknown" for use with TZDB, to stand # for an unknown or invalid time zone. These two notions differ: # TZ="Factory" is a valid timezone, so tzalloc("Factory") succeeds, whereas # TZ="Etc/Unknown" is invalid and tzalloc("Etc/Unknown") fails. # Also, a downstream distributor could modify Factory to be a # default timezone suitable for the devices it manufactures, # whereas that cannot happen for Etc/Unknown. # Zone NAME STDOFF RULES FORMAT Zone Factory 0 - -00 pr0m1th3as-datatypes-9450b29/inst/tzdata/iso3166.tab000066400000000000000000000113511527370255000217450ustar00rootroot00000000000000# ISO 3166 alpha-2 country codes # # This file is in the public domain, so clarified as of # 2009-05-17 by Arthur David Olson. # # From Paul Eggert (2025-07-01): # This file contains a table of two-letter country codes. Columns are # separated by a single tab. Lines beginning with ‘#’ are comments. # All text uses UTF-8 encoding. The columns of the table are as follows: # # 1. ISO 3166-1 alpha-2 country code, current as of # ISO/TC 46 N1127 (2024-02-29). See: ISO/TC 46 Documents # https://www.iso.org/committee/48750.html?view=documents # 2. The usual English name for the coded region. This sometimes # departs from ISO-listed names, sometimes so that sorted subsets # of names are useful (e.g., “Samoa (American)� and “Samoa # (western)� rather than “American Samoa� and “Samoa�), # sometimes to avoid confusion among non-experts (e.g., # “Czech Republic� and “Turkey� rather than “Czechia� and “Türkiye�), # and sometimes to omit needless detail or churn (e.g., “Netherlands� # rather than “Netherlands (the)� or “Netherlands (Kingdom of the)�). # # The table is sorted by country code. # # This table is intended as an aid for users, to help them select time # zone data appropriate for their practical needs. It is not intended # to take or endorse any position on legal or territorial claims. # #country- #code name of country, territory, area, or subdivision AD Andorra AE United Arab Emirates AF Afghanistan AG Antigua & Barbuda AI Anguilla AL Albania AM Armenia AO Angola AQ Antarctica AR Argentina AS Samoa (American) AT Austria AU Australia AW Aruba AX Åland Islands AZ Azerbaijan BA Bosnia & Herzegovina BB Barbados BD Bangladesh BE Belgium BF Burkina Faso BG Bulgaria BH Bahrain BI Burundi BJ Benin BL St Barthelemy BM Bermuda BN Brunei BO Bolivia BQ Caribbean NL BR Brazil BS Bahamas BT Bhutan BV Bouvet Island BW Botswana BY Belarus BZ Belize CA Canada CC Cocos (Keeling) Islands CD Congo (Dem. Rep.) CF Central African Rep. CG Congo (Rep.) CH Switzerland CI Côte d’Ivoire CK Cook Islands CL Chile CM Cameroon CN China CO Colombia CR Costa Rica CU Cuba CV Cape Verde CW Curaçao CX Christmas Island CY Cyprus CZ Czech Republic DE Germany DJ Djibouti DK Denmark DM Dominica DO Dominican Republic DZ Algeria EC Ecuador EE Estonia EG Egypt EH Western Sahara ER Eritrea ES Spain ET Ethiopia FI Finland FJ Fiji FK Falkland Islands FM Micronesia FO Faroe Islands FR France GA Gabon GB Britain (UK) GD Grenada GE Georgia GF French Guiana GG Guernsey GH Ghana GI Gibraltar GL Greenland GM Gambia GN Guinea GP Guadeloupe GQ Equatorial Guinea GR Greece GS South Georgia & the South Sandwich Islands GT Guatemala GU Guam GW Guinea-Bissau GY Guyana HK Hong Kong HM Heard Island & McDonald Islands HN Honduras HR Croatia HT Haiti HU Hungary ID Indonesia IE Ireland IL Israel IM Isle of Man IN India IO British Indian Ocean Territory IQ Iraq IR Iran IS Iceland IT Italy JE Jersey JM Jamaica JO Jordan JP Japan KE Kenya KG Kyrgyzstan KH Cambodia KI Kiribati KM Comoros KN St Kitts & Nevis KP Korea (North) KR Korea (South) KW Kuwait KY Cayman Islands KZ Kazakhstan LA Laos LB Lebanon LC St Lucia LI Liechtenstein LK Sri Lanka LR Liberia LS Lesotho LT Lithuania LU Luxembourg LV Latvia LY Libya MA Morocco MC Monaco MD Moldova ME Montenegro MF St Martin (French) MG Madagascar MH Marshall Islands MK North Macedonia ML Mali MM Myanmar (Burma) MN Mongolia MO Macau MP Northern Mariana Islands MQ Martinique MR Mauritania MS Montserrat MT Malta MU Mauritius MV Maldives MW Malawi MX Mexico MY Malaysia MZ Mozambique NA Namibia NC New Caledonia NE Niger NF Norfolk Island NG Nigeria NI Nicaragua NL Netherlands NO Norway NP Nepal NR Nauru NU Niue NZ New Zealand OM Oman PA Panama PE Peru PF French Polynesia PG Papua New Guinea PH Philippines PK Pakistan PL Poland PM St Pierre & Miquelon PN Pitcairn PR Puerto Rico PS Palestine PT Portugal PW Palau PY Paraguay QA Qatar RE Réunion RO Romania RS Serbia RU Russia RW Rwanda SA Saudi Arabia SB Solomon Islands SC Seychelles SD Sudan SE Sweden SG Singapore SH St Helena SI Slovenia SJ Svalbard & Jan Mayen SK Slovakia SL Sierra Leone SM San Marino SN Senegal SO Somalia SR Suriname SS South Sudan ST Sao Tome & Principe SV El Salvador SX St Maarten (Dutch) SY Syria SZ Eswatini (Swaziland) TC Turks & Caicos Is TD Chad TF French S. Terr. TG Togo TH Thailand TJ Tajikistan TK Tokelau TL East Timor TM Turkmenistan TN Tunisia TO Tonga TR Turkey TT Trinidad & Tobago TV Tuvalu TW Taiwan TZ Tanzania UA Ukraine UG Uganda UM US minor outlying islands US United States UY Uruguay UZ Uzbekistan VA Vatican City VC St Vincent VE Venezuela VG Virgin Islands (UK) VI Virgin Islands (US) VN Vietnam VU Vanuatu WF Wallis & Futuna WS Samoa (western) YE Yemen YT Mayotte ZA South Africa ZM Zambia ZW Zimbabwe pr0m1th3as-datatypes-9450b29/inst/tzdata/leap-seconds.list000066400000000000000000000117111527370255000234150ustar00rootroot00000000000000# ATOMIC TIME # Coordinated Universal Time (UTC) is the reference time scale derived # from The "Temps Atomique International" (TAI) calculated by the Bureau # International des Poids et Mesures (BIPM) using a worldwide network of atomic # clocks. UTC differs from TAI by an integer number of seconds; it is the basis # of all activities in the world. # # # ASTRONOMICAL TIME (UT1) is the time scale based on the rate of rotation of the earth. # It is now mainly derived from Very Long Baseline Interferometry (VLBI). The various # irregular fluctuations progressively detected in the rotation rate of the Earth led # in 1972 to the replacement of UT1 by UTC as the reference time scale. # # # LEAP SECOND # Atomic clocks are more stable than the rate of the earth's rotation since the latter # undergoes a full range of geophysical perturbations at various time scales: lunisolar # and core-mantle torques, atmospheric and oceanic effects, etc. # Leap seconds are needed to keep the two time scales in agreement, i.e. UT1-UTC smaller # than 0.9 seconds. Therefore, when necessary a "leap second" is applied to UTC. # Since the adoption of this system in 1972 it has been necessary to add a number of seconds to UTC, # firstly due to the initial choice of the value of the second (1/86400 mean solar day of # the year 1820) and secondly to the general slowing down of the Earth's rotation. It is # theoretically possible to have a negative leap second (a second removed from UTC), but so far, # all leap seconds have been positive (a second has been added to UTC). Based on what we know about # the earth's rotation, it is unlikely that we will ever have a negative leap second. # # # HISTORY # The first leap second was added on June 30, 1972. Until the year 2000, it was necessary in average to add a # leap second at a rate of 1 to 2 years. Since the year 2000 leap seconds are introduced with an # average interval of 3 to 4 years due to the acceleration of the Earth's rotation speed. # # # RESPONSIBILITY OF THE DECISION TO INTRODUCE A LEAP SECOND IN UTC # The decision to introduce a leap second in UTC is the responsibility of the Earth Orientation Center of # the International Earth Rotation and reference System Service (IERS). This center is located at Paris # Observatory. According to international agreements, leap seconds should be scheduled only for certain dates: # first preference is given to the end of December and June, and second preference at the end of March # and September. Since the introduction of leap seconds in 1972, only dates in June and December were used. # # Questions or comments to: # Christian Bizouard: christian.bizouard@obspm.fr # Earth orientation Center of the IERS # Paris Observatory, France # # # # COPYRIGHT STATUS OF THIS FILE # This file is in the public domain. # # # VALIDITY OF THE FILE # It is important to express the validity of the file. These next two dates are # given in units of seconds since 1900.0. # # 1) Last update of the file. # # Updated through IERS Bulletin C (https://hpiers.obspm.fr/iers/bul/bulc/bulletinc.dat) # # The following line shows the last update of this file in NTP timestamp: # #$ 3992312697 # # 2) Expiration date of the file given on a semi-annual basis: last June or last December # # File expires on 28 June 2027 # # Expire date in NTP timestamp: # #@ 4023129600 # # # LIST OF LEAP SECONDS # NTP timestamp (X parameter) is the number of seconds since 1900.0 # # MJD: The Modified Julian Day number. MJD = X/86400 + 15020 # # DTAI: The difference DTAI= TAI-UTC in units of seconds # It is the quantity to add to UTC to get the time in TAI # # Day Month Year : epoch in clear # #NTP Time DTAI Day Month Year # 2272060800 10 # 1 Jan 1972 2287785600 11 # 1 Jul 1972 2303683200 12 # 1 Jan 1973 2335219200 13 # 1 Jan 1974 2366755200 14 # 1 Jan 1975 2398291200 15 # 1 Jan 1976 2429913600 16 # 1 Jan 1977 2461449600 17 # 1 Jan 1978 2492985600 18 # 1 Jan 1979 2524521600 19 # 1 Jan 1980 2571782400 20 # 1 Jul 1981 2603318400 21 # 1 Jul 1982 2634854400 22 # 1 Jul 1983 2698012800 23 # 1 Jul 1985 2776982400 24 # 1 Jan 1988 2840140800 25 # 1 Jan 1990 2871676800 26 # 1 Jan 1991 2918937600 27 # 1 Jul 1992 2950473600 28 # 1 Jul 1993 2982009600 29 # 1 Jul 1994 3029443200 30 # 1 Jan 1996 3076704000 31 # 1 Jul 1997 3124137600 32 # 1 Jan 1999 3345062400 33 # 1 Jan 2006 3439756800 34 # 1 Jan 2009 3550089600 35 # 1 Jul 2012 3644697600 36 # 1 Jul 2015 3692217600 37 # 1 Jan 2017 # # A hash code has been generated to be able to verify the integrity # of this file. For more information about using this hash code, # please see the readme file in the 'source' directory : # https://hpiers.obspm.fr/iers/bul/bulc/ntp/sources/README # #h a9bad145 84c31c70 758402aa b37bfd54 5923836a pr0m1th3as-datatypes-9450b29/inst/tzdata/leapseconds000066400000000000000000000070341527370255000223710ustar00rootroot00000000000000# Leap seconds (1972-2016) optionally added to TZif files. # This file is in the public domain. # This file is generated automatically from the data in the public-domain # NIST/IERS format leap-seconds.list file, which can be copied from # # or via a less-secure protocol and with different comments and # less volatile timestamp metadata, from # . # For more about leap-seconds.list, please see # The NTP Timescale and Leap Seconds # . # The rules for leap seconds are specified in Annex 1 (Time scales) of: # Standard-frequency and time-signal emissions. # International Telecommunication Union - Radiocommunication Sector # (ITU-R) Recommendation TF.460-6 (02/2002) # . # The International Earth Rotation and Reference Systems Service (IERS) # periodically used leap seconds to keep UTC to within 0.9 s of UT1 # (a proxy for Earth's angle in space as measured by astronomers). # See: Levine J. Coordinated Universal Time and the leap second. # URSI Radio Sci Bull. 2016;89(4):30-6. doi:10.23919/URSIRSB.2016.7909995 # . # There were no leap seconds before 1972, as no official mechanism # accounted for the discrepancy between atomic time (TAI) and the earth's # rotation. The first ("1 Jan 1972") data line in leap-seconds.list # does not denote a leap second; it denotes the start of the current definition # of UTC. # All leap-seconds are Stationary (S) at the given UTC time. # The correction (+ or -) is made at the given time, so if a negative # leap second had ever occurred, a line would have looked like this: # Leap YEAR MON DAY 23:59:59 - S # Typical lines look like this: # Leap YEAR MON DAY 23:59:60 + S Leap 1972 Jun 30 23:59:60 + S Leap 1972 Dec 31 23:59:60 + S Leap 1973 Dec 31 23:59:60 + S Leap 1974 Dec 31 23:59:60 + S Leap 1975 Dec 31 23:59:60 + S Leap 1976 Dec 31 23:59:60 + S Leap 1977 Dec 31 23:59:60 + S Leap 1978 Dec 31 23:59:60 + S Leap 1979 Dec 31 23:59:60 + S Leap 1981 Jun 30 23:59:60 + S Leap 1982 Jun 30 23:59:60 + S Leap 1983 Jun 30 23:59:60 + S Leap 1985 Jun 30 23:59:60 + S Leap 1987 Dec 31 23:59:60 + S Leap 1989 Dec 31 23:59:60 + S Leap 1990 Dec 31 23:59:60 + S Leap 1992 Jun 30 23:59:60 + S Leap 1993 Jun 30 23:59:60 + S Leap 1994 Jun 30 23:59:60 + S Leap 1995 Dec 31 23:59:60 + S Leap 1997 Jun 30 23:59:60 + S Leap 1998 Dec 31 23:59:60 + S Leap 2005 Dec 31 23:59:60 + S Leap 2008 Dec 31 23:59:60 + S Leap 2012 Jun 30 23:59:60 + S Leap 2015 Jun 30 23:59:60 + S Leap 2016 Dec 31 23:59:60 + S # UTC timestamp when this leap second list expires. # The idea was that any additional leap seconds would come after this. # This timestamp is obsolete as there will be no more leap seconds. # This Expires line is commented out, to avoid confusion, and # so that pre-2020a zic implementations do not reject this file. #Expires 2027 Jun 28 00:00:00 # Here are POSIX timestamps for the data in this file. # "#updated" gives the last time the leap seconds data changed # or, if this file was derived from the IERS leap-seconds.list, # the last time that file changed in any way. #updated 1783323897 (2026-07-06 07:44:57 UTC) # "#expires" gives the first time this file might be wrong. # This timestamp is obsolete as there will be no more leap seconds. #expires 1814140800 (2027-06-28 00:00:00 UTC) # Updated through IERS Bulletin C (https://hpiers.obspm.fr/iers/bul/bulc/bulletinc.dat) # File expires on 28 June 2027 pr0m1th3as-datatypes-9450b29/inst/tzdata/northamerica000066400000000000000000005525761527370255000225650ustar00rootroot00000000000000# tzdb data for North and Central America and environs # This file is in the public domain, so clarified as of # 2009-05-17 by Arthur David Olson. # also includes Central America and the Caribbean # This file is by no means authoritative; if you think you know better, # go ahead and edit the file, and please send any changes to # the public mailing list tz@iana.org for general use in the future. # For more, please see the file CONTRIBUTING in the tz distribution. # From Paul Eggert (1999-03-22): # A reliable and entertaining source about time zones is # Derek Howse, Greenwich time and longitude, Philip Wilson Publishers (1997). ############################################################################### # United States # From Paul Eggert (1999-03-31): # Howse writes (pp 121-125) that time zones were invented by # Professor Charles Ferdinand Dowd (1825-1904), # Principal of Temple Grove Ladies' Seminary (Saratoga Springs, NY). # His pamphlet "A System of National Time for Railroads" (1870) # was the result of his proposals at the Convention of Railroad Trunk Lines # in New York City (1869-10). His 1870 proposal was based on Washington, DC, # but in 1872-05 he moved the proposed origin to Greenwich. # From Paul Eggert (2024-11-18): # Dowd's proposal left many details unresolved, such as where to draw # lines between time zones. Sandford Fleming of the Canadian Pacific Railway # argued for Dowd's proposal in 1876, and Cleveland Abbe of the American # Meteorology Society published a report in 1879 recommending four US time # zones based on GMT. However, the key individual who made time zones # work in the US was William Frederick Allen - railway engineer, # managing editor of the Travelers' Guide, and secretary of the # General Time Convention, a railway standardization group. Allen # spent months in dialogs with scientific and railway leaders, # developed a workable plan to institute time zones, and presented it # to the General Time Convention on 1883-04-11, saying that his plan # meant "local time would be practically abolished" - a plus for # railway scheduling. By the next convention on 1883-10-11 nearly all # railroads had agreed and it took effect on 1883-11-18. That Sunday # was called the "day of two noons", as some locations observed noon # twice. Allen witnessed the transition in New York City, writing: # # I heard the bells of St. Paul's strike on the old time. Four # minutes later, obedient to the electrical signal from the Naval # Observatory ... the time-ball made its rapid descent, the chimes # of old Trinity rang twelve measured strokes, and local time was # abandoned, probably forever. # # Most of the US soon followed suit. See: # Bartky IR. The adoption of standard time. Technol Cult 1989 Jan;30(1):25-56. # https://dx.doi.org/10.2307/3105430 # From Paul Eggert (2005-04-16): # That 1883 transition occurred at 12:00 new time, not at 12:00 old time. # See p 46 of David Prerau, Seize the daylight, Thunder's Mouth Press (2005). # From Paul Eggert (2006-03-22): # A good source for time zone historical data in the US is # Thomas G. Shanks, The American Atlas (5th edition), # San Diego: ACS Publications, Inc. (1991). # Make sure you have the errata sheet; the book is somewhat useless without it. # It is the source for most of the pre-1991 US entries below. # From Paul Eggert (2001-03-06): # Daylight Saving Time was first suggested as a joke by Benjamin Franklin # in his whimsical essay "An Economical Project for Diminishing the Cost # of Light" published in the Journal de Paris (1784-04-26). # Not everyone is happy with the results: # # I don't really care how time is reckoned so long as there is some # agreement about it, but I object to being told that I am saving # daylight when my reason tells me that I am doing nothing of the kind. # I even object to the implication that I am wasting something # valuable if I stay in bed after the sun has risen. As an admirer # of moonlight I resent the bossy insistence of those who want to # reduce my time for enjoying it. At the back of the Daylight Saving # scheme I detect the bony, blue-fingered hand of Puritanism, eager # to push people into bed earlier, and get them up earlier, to make # them healthy, wealthy and wise in spite of themselves. # # -- Robertson Davies, The diary of Samuel Marchbanks, # Clarke, Irwin (1947), XIX, Sunday # # For more about the first ten years of DST in the United States, see # Robert Garland, Ten years of daylight saving from the Pittsburgh standpoint # (Carnegie Library of Pittsburgh, 1927). # https://web.archive.org/web/20160517155308/http://www.clpgh.org/exhibit/dst.html # # Shanks says that DST was called "War Time" in the US in 1918 and 1919. # However, DST was imposed by the Standard Time Act of 1918, which # was the first nationwide legal time standard, and apparently # time was just called "Standard Time" or "Daylight Saving Time". # From Paul Eggert (2019-06-04) and Tim Parenti (2026-07-21): # Here is the legal basis for the US federal rules. # * Public Law 65-106 (1918-03-19) implemented standard and daylight saving # time for the first time across the US, springing forward on March's last # Sunday and falling back on October's last Sunday. # https://www.loc.gov/law/help/statutes-at-large/65th-congress/session-2/c65s2ch24.pdf # * Public Law 66-40 (1919-08-20) repealed DST on October 1919's last Sunday. # https://www.loc.gov/law/help/statutes-at-large/66th-congress/session-1/c66s1ch51.pdf # * Public Law 77-403 (1942-01-20) started wartime DST on 1942-02-09. # https://www.loc.gov/law/help/statutes-at-large/77th-congress/session-2/c77s2ch7.pdf # * Public Law 79-187 (1945-09-25) ended wartime DST on 1945-09-30. # https://www.loc.gov/law/help/statutes-at-large/79th-congress/session-1/c79s1ch388.pdf # * Public Law 89-387 (1966-04-13) reinstituted a national standard for DST, # from April's last Sunday to October's last Sunday, effective 1967. # https://www.govinfo.gov/content/pkg/STATUTE-80/pdf/STATUTE-80-Pg107.pdf # * Public Law 93-182 (1973-12-15) applied DST year-round as an emergency # measure. It took effect on the fourth Sunday after its enactment, # effectively moving the 1974 spring-forward to 01-06. # https://www.govinfo.gov/content/pkg/STATUTE-87/pdf/STATUTE-87-Pg707.pdf # It was meant to last for 16 months or two winters (through 1975-04-27); # however... # * Public Law 93-434 (1974-10-05) created an exemption for much of the second # winter, restoring standard time from the usual date of 1974-10-27 while # moving the 1975 spring-forward up to February's last Sunday. # https://www.govinfo.gov/content/pkg/STATUTE-88/pdf/STATUTE-88-Pg1209.pdf # * Public Law 99-359 (1986-07-08) moved the spring-forward to April's first # Sunday. # https://www.govinfo.gov/content/pkg/STATUTE-100/pdf/STATUTE-100-Pg764.pdf # * Public Law 109-58 (2005-08-08), effective 2007, moved the spring-forward # to March's second Sunday and the fall-back to November's first Sunday. # https://www.govinfo.gov/content/pkg/PLAW-109publ58/pdf/PLAW-109publ58.pdf # All transitions are at 02:00 local time. # From Arthur David Olson: # Before the Uniform Time Act of 1966 took effect in 1967, observance of # Daylight Saving Time in the US was by local option, except during wartime. # From Arthur David Olson (2000-09-25): # Last night I heard part of a rebroadcast of a 1945 Arch Oboler radio drama. # In the introduction, Oboler spoke of "Eastern Peace Time." # An AltaVista search turned up: # https://web.archive.org/web/20000926032210/http://rowayton.org/rhs/hstaug45.html # "When the time is announced over the radio now, it is 'Eastern Peace # Time' instead of the old familiar 'Eastern War Time.' Peace is wonderful." # (August 1945) by way of confirmation. # # From Paul Eggert (2017-09-23): # This was the V-J Day issue of the Clamdigger, a Rowayton, CT newsletter. # From Joseph Gallant citing # George H. Douglas, _The Early Days of Radio Broadcasting_ (1987): # At 7 P.M. (Eastern War Time) [on 1945-08-14], the networks were set # to switch to London for Attlee's address, but the American people # never got to hear his speech live. According to one press account, # CBS' Bob Trout was first to announce the word of Japan's surrender, # but a few seconds later, NBC, ABC and Mutual also flashed the word # of surrender, all of whom interrupting the bells of Big Ben in # London which were to precede Mr. Attlee's speech. # From Paul Eggert (2003-02-09): It was Robert St John, not Bob Trout. From # Myrna Oliver's obituary of St John on page B16 of today's Los Angeles Times: # # ... a war-weary U.S. clung to radios, awaiting word of Japan's surrender. # Any announcement from Asia would reach St. John's New York newsroom on a # wire service teletype machine, which had prescribed signals for major news. # Associated Press, for example, would ring five bells before spewing out # typed copy of an important story, and 10 bells for news "of transcendental # importance." # # On Aug. 14, stalling while talking steadily into the NBC networks' open # microphone, St. John heard five bells and waited only to hear a sixth bell, # before announcing confidently: "Ladies and gentlemen, World War II is over. # The Japanese have agreed to our surrender terms." # # He had scored a 20-second scoop on other broadcasters. # From Arthur David Olson (2005-08-22): # Paul has been careful to use the "US" rules only in those locations # that are part of the United States; this reflects the real scope of # U.S. government action. So even though the "US" rules have changed # in the latest release, other countries won't be affected. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule US 1918 1919 - Mar lastSun 2:00 1:00 D Rule US 1918 1919 - Oct lastSun 2:00 0 S Rule US 1942 only - Feb 9 2:00 1:00 W # War Rule US 1945 only - Aug 14 23:00u 1:00 P # Peace Rule US 1945 only - Sep 30 2:00 0 S Rule US 1967 2006 - Oct lastSun 2:00 0 S Rule US 1967 1973 - Apr lastSun 2:00 1:00 D Rule US 1974 only - Jan 6 2:00 1:00 D Rule US 1975 only - Feb lastSun 2:00 1:00 D Rule US 1976 1986 - Apr lastSun 2:00 1:00 D Rule US 1987 2006 - Apr Sun>=1 2:00 1:00 D Rule US 2007 max - Mar Sun>=8 2:00 1:00 D Rule US 2007 max - Nov Sun>=1 2:00 0 S # From U. S. Naval Observatory (1989-01-19): # USA EASTERN 5 H BEHIND UTC NEW YORK, WASHINGTON # USA EASTERN 4 H BEHIND UTC APR 3 - OCT 30 # USA CENTRAL 6 H BEHIND UTC CHICAGO, HOUSTON # USA CENTRAL 5 H BEHIND UTC APR 3 - OCT 30 # USA MOUNTAIN 7 H BEHIND UTC DENVER # USA MOUNTAIN 6 H BEHIND UTC APR 3 - OCT 30 # USA PACIFIC 8 H BEHIND UTC L.A., SAN FRANCISCO # USA PACIFIC 7 H BEHIND UTC APR 3 - OCT 30 # USA ALASKA STD 9 H BEHIND UTC MOST OF ALASKA (AKST) # USA ALASKA STD 8 H BEHIND UTC APR 3 - OCT 30 (AKDT) # USA ALEUTIAN 10 H BEHIND UTC ISLANDS WEST OF 170W # USA " 9 H BEHIND UTC APR 3 - OCT 30 # USA HAWAII 10 H BEHIND UTC # USA BERING 11 H BEHIND UTC SAMOA, MIDWAY # From Arthur David Olson (1989-01-21): # The above dates are for 1988. # Note the "AKST" and "AKDT" abbreviations, the claim that there's # no DST in Samoa, and the claim that there is DST in Alaska and the # Aleutians. # From Arthur David Olson (1988-02-13): # Legal standard time zone names, from United States Code (1982 Edition and # Supplement III), Title 15, Chapter 6, Section 260 and forward. First, names # up to 1967-04-01 (when most provisions of the Uniform Time Act of 1966 # took effect), as explained in sections 263 and 261: # (none) # United States standard eastern time # United States standard mountain time # United States standard central time # United States standard Pacific time # (none) # United States standard Alaska time # (none) # Next, names from 1967-04-01 until 1983-11-30 (the date for # public law 98-181): # Atlantic standard time # eastern standard time # central standard time # mountain standard time # Pacific standard time # Yukon standard time # Alaska-Hawaii standard time # Bering standard time # And after 1983-11-30: # Atlantic standard time # eastern standard time # central standard time # mountain standard time # Pacific standard time # Alaska standard time # Hawaii-Aleutian standard time # Samoa standard time # The law doesn't give abbreviations. # # From Paul Eggert (2016-12-19): # Here are URLs for the 1918 and 1966 legislation: # http://uscode.house.gov/statviewer.htm?volume=40&page=451 # http://uscode.house.gov/statviewer.htm?volume=80&page=108 # Although the 1918 names were officially "United States Standard # Eastern Time" and similarly for "Central", "Mountain", "Pacific", # and "Alaska", in practice "Standard" was placed just before "Time", # as codified in 1966. In practice, Alaska time was abbreviated "AST" # before 1968. Summarizing the 1967 name changes: # 1918 names 1967 names # -08 Standard Pacific Time (PST) Pacific standard time (PST) # -09 (unofficial) Yukon (YST) Yukon standard time (YST) # -10 Standard Alaska Time (AST) Alaska-Hawaii standard time (AHST) # -11 (unofficial) Nome (NST) Bering standard time (BST) # # From Paul Eggert (2023-01-23), from a 2001-01-08 heads-up from Rives McDow: # Public law 106-564 (2000-12-23) introduced "Chamorro standard time" # for time in Guam and the Northern Marianas. See the file "australasia". # Also see 15 U.S.C. §263 . # # From Paul Eggert (2015-04-17): # HST and HDT are standardized abbreviations for Hawaii-Aleutian # standard and daylight times. See section 9.47 (p 234) of the # U.S. Government Printing Office Style Manual (2008) # https://www.gpo.gov/fdsys/pkg/GPO-STYLEMANUAL-2008/pdf/GPO-STYLEMANUAL-2008.pdf # From Arthur David Olson, 2005-08-09 # The following was signed into law on 2005-08-08. # # H.R. 6, Energy Policy Act of 2005, SEC. 110. DAYLIGHT SAVINGS. # (a) Amendment.--Section 3(a) of the Uniform Time Act of 1966 (15 # U.S.C. 260a(a)) is amended-- # (1) by striking "first Sunday of April" and inserting "second # Sunday of March"; and # (2) by striking "last Sunday of October" and inserting "first # Sunday of November'. # (b) Effective Date.--Subsection (a) shall take effect 1 year after the # date of enactment of this Act or March 1, 2007, whichever is later. # (c) Report to Congress.--Not later than 9 months after the effective # date stated in subsection (b), the Secretary shall report to Congress # on the impact of this section on energy consumption in the United # States. # (d) Right to Revert.--Congress retains the right to revert the # Daylight Saving Time back to the 2005 time schedules once the # Department study is complete. # US eastern time, represented by New York # Connecticut, Delaware, District of Columbia, most of Florida, # Georgia, southeast Indiana (Dearborn and Ohio counties), eastern Kentucky # (except America/Kentucky/Louisville below), Maine, Maryland, Massachusetts, # New Hampshire, New Jersey, New York, North Carolina, Ohio, # Pennsylvania, Rhode Island, South Carolina, eastern Tennessee, # Vermont, Virginia, West Virginia # From Dave Cantor (2004-11-02): # Early this summer I had the occasion to visit the Mount Washington # Observatory weather station atop (of course!) Mount Washington [, NH].... # One of the staff members said that the station was on Eastern Standard Time # and didn't change their clocks for Daylight Saving ... so that their # reports will always have times which are 5 hours behind UTC. # From Paul Eggert (2005-08-26): # According to today's Huntsville Times # http://www.al.com/news/huntsvilletimes/index.ssf?/base/news/1125047783228320.xml&coll=1 # a few towns on Alabama's "eastern border with Georgia, such as Phenix City # in Russell County, Lanett in Chambers County and some towns in Lee County, # set their watches and clocks on Eastern time." It quotes H.H. "Bubba" # Roberts, city administrator in Phenix City. as saying "We are in the Central # time zone, but we do go by the Eastern time zone because so many people work # in Columbus." # # From Paul Eggert (2017-02-22): # Four cities are involved. The two not mentioned above are Smiths Station # and Valley. Barbara Brooks, Valley's assistant treasurer, heard it started # because West Point Pepperell textile mills were in Alabama while the # corporate office was in Georgia, and residents voted to keep Eastern # time even after the mills closed. See: Kazek K. Did you know which # Alabama towns are in a different time zone? al.com 2017-02-06. # http://www.al.com/living/index.ssf/2017/02/do_you_know_which_alabama_town.html # From Paul Eggert (2014-09-06): # Monthly Notices of the Royal Astronomical Society 44, 4 (1884-02-08), 208 # says that New York City Hall time was 3 minutes 58.4 seconds fast of # Eastern time (i.e., -4:56:01.6) just before the 1883 switch. # Rule NAME FROM TO - IN ON AT SAVE LETTER Rule NYC 1920 only - Mar lastSun 2:00 1:00 D Rule NYC 1920 only - Oct lastSun 2:00 0 S Rule NYC 1921 1966 - Apr lastSun 2:00 1:00 D Rule NYC 1921 1954 - Sep lastSun 2:00 0 S Rule NYC 1955 1966 - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] #STDOFF -4:56:01.6 Zone America/New_York -4:56:02 - LMT 1883 Nov 18 17:00u -5:00 US E%sT 1920 -5:00 NYC E%sT 1942 -5:00 US E%sT 1946 -5:00 NYC E%sT 1967 -5:00 US E%sT # US central time, represented by Chicago # Alabama, Arkansas, Florida panhandle (Bay, Calhoun, Escambia, # Gulf, Holmes, Jackson, Okaloosa, Santa Rosa, Walton, and # Washington counties), Illinois, western Indiana # (Gibson, Jasper, Lake, LaPorte, Newton, Porter, Posey, Spencer, # Vanderburgh, and Warrick counties), Iowa, most of Kansas, western # Kentucky, Louisiana, Minnesota, Mississippi, Missouri, eastern # Nebraska, eastern North Dakota, Oklahoma, eastern South Dakota, # western Tennessee, most of Texas, Wisconsin # From Paul Eggert (2018-01-07): # In 1869 the Chicago Astronomical Society contracted with the city to keep # time. Though delayed by the Great Fire, by 1880 a wire ran from the # Dearborn Observatory (on the University of Chicago campus) to City Hall, # which then sent signals to police and fire stations. However, railroads got # their time signals from the Allegheny Observatory, the Madison Observatory, # the Ann Arbor Observatory, etc., so their clocks did not agree with each # other or with the city's official time. The confusion took some years to # clear up. See: # Moser M. How Chicago gave America its time zones. Chicago. 2018-01-04. # http://www.chicagomag.com/city-life/January-2018/How-Chicago-Gave-America-Its-Time-Zones/ # From Larry M. Smith (2006-04-26) re Wisconsin: # https://docs.legis.wisconsin.gov/statutes/statutes/175.pdf # is currently enforced at the 01:00 time of change. Because the local # "bar time" in the state corresponds to 02:00, a number of citations # are issued for the "sale of class 'B' alcohol after prohibited # hours" within the deviated hour of this change every year.... # # From Douglas R. Bomberg (2007-03-12): # Wisconsin has enacted (nearly eleventh-hour) legislation to get WI # Statue 175 closer in synch with the US Congress' intent.... # https://docs.legis.wisconsin.gov/2007/related/acts/3 # From an email administrator of the City of Fort Pierre, SD (2015-12-21): # Fort Pierre is technically located in the Mountain time zone as is # the rest of Stanley County. Most of Stanley County and Fort Pierre # uses the Central time zone due to doing most of their business in # Pierre so it simplifies schedules. I have lived in Stanley County # all my life and it has been that way since I can remember. (43 years!) # # From Paul Eggert (2015-12-25): # Assume this practice predates 1970, so Fort Pierre can use America/Chicago. # From Paul Eggert (2015-04-06): # In 1950s Nashville a public clock had dueling faces, one for conservatives # and the other for liberals; the two sides didn't agree about the time of day. # I haven't found a photo of this clock, nor have I tracked down the TIME # magazine report cited below, but here's the story as told by the late # American journalist John Seigenthaler, who was there: # # "The two [newspaper] owners held strongly contrasting political and # ideological views. Evans was a New South liberal, Stahlman an Old South # conservative, and their two papers frequently clashed editorially, often on # the same day.... In the 1950s as the state legislature was grappling with # the question of whether to approve daylight saving time for the entire state, # TIME magazine reported: # # "'The Nashville Banner and The Nashville Tennessean rarely agree on anything # but the time of day - and last week they couldn't agree on that.' # # "It was all too true. The clock on the front of the building had two faces - # The Tennessean side of the building facing west, the other, east. When it # was high noon Banner time, it was 11 a.m. Tennessean time." # # Seigenthaler J. For 100 years, Tennessean had it covered. # The Tennessean 2007-05-11, republished 2015-04-06. # https://www.tennessean.com/story/insider/extras/2015/04/06/archives-seigenthaler-for-100-years-the-tennessean-had-it-covered/25348545/ # Rule NAME FROM TO - IN ON AT SAVE LETTER Rule Chicago 1920 only - Jun 13 2:00 1:00 D Rule Chicago 1920 1921 - Oct lastSun 2:00 0 S Rule Chicago 1921 only - Mar lastSun 2:00 1:00 D Rule Chicago 1922 1966 - Apr lastSun 2:00 1:00 D Rule Chicago 1922 1954 - Sep lastSun 2:00 0 S Rule Chicago 1955 1966 - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Chicago -5:50:36 - LMT 1883 Nov 18 18:00u -6:00 US C%sT 1920 -6:00 Chicago C%sT 1936 Mar 1 2:00 -5:00 - EST 1936 Nov 15 2:00 -6:00 Chicago C%sT 1942 -6:00 US C%sT 1946 -6:00 Chicago C%sT 1967 -6:00 US C%sT # Oliver County, ND switched from mountain to central time on 1992-10-25. Zone America/North_Dakota/Center -6:45:12 - LMT 1883 Nov 18 19:00u -7:00 US M%sT 1992 Oct 25 2:00 -6:00 US C%sT # Morton County, ND, switched from mountain to central time on # 2003-10-26, except for the area around Mandan which was already central time. # See . # Officially this switch also included part of Sioux County, and # Jones, Mellette, and Todd Counties in South Dakota; # but in practice these other counties were already observing central time. # See . Zone America/North_Dakota/New_Salem -6:45:39 - LMT 1883 Nov 18 19:00u -7:00 US M%sT 2003 Oct 26 2:00 -6:00 US C%sT # From Josh Findley (2011-01-21): # ...it appears that Mercer County, North Dakota, changed from the # mountain time zone to the central time zone at the last transition from # daylight-saving to standard time (on Nov. 7, 2010): # https://www.gpo.gov/fdsys/pkg/FR-2010-09-29/html/2010-24376.htm # http://www.bismarcktribune.com/news/local/article_1eb1b588-c758-11df-b472-001cc4c03286.html # From Andy Lipscomb (2011-01-24): # ...according to the Census Bureau, the largest city is Beulah (although # it's commonly referred to as Beulah-Hazen, with Hazen being the next # largest city in Mercer County). Google Maps places Beulah's city hall # at 47° 15' 51" N, 101° 46' 40" W, which yields an offset of 6h47'07". Zone America/North_Dakota/Beulah -6:47:07 - LMT 1883 Nov 18 19:00u -7:00 US M%sT 2010 Nov 7 2:00 -6:00 US C%sT # US mountain time, represented by Denver # # Colorado, far western Kansas, Montana, western # Nebraska, Nevada border (Jackpot, Owyhee, and Mountain City), # New Mexico, southwestern North Dakota, # western South Dakota, far western Texas (El Paso County, Hudspeth County, # and Pine Springs and Nickel Creek in Culberson County), Utah, Wyoming # # From Paul Eggert (2018-10-25): # On 1921-03-04 federal law placed all of Texas into the central time zone. # However, El Paso ignored the law for decades and continued to observe # mountain time, on the grounds that that's what they had always done # and they weren't about to let the federal government tell them what to do. # Eventually the federal government gave in and changed the law on # 1970-04-10 to match what El Paso was actually doing. Although # that's slightly after our 1970 cutoff, there is no need to create a # separate zone for El Paso since they were ignoring the law anyway. See: # Long T. El Pasoans were time rebels, fought to stay in Mountain zone. # El Paso Times. 2018-10-24 06:40 -06. # https://www.elpasotimes.com/story/news/local/el-paso/2018/10/24/el-pasoans-were-time-rebels-fought-stay-mountain-zone/1744509002/ # # Rule NAME FROM TO - IN ON AT SAVE LETTER Rule Denver 1920 1921 - Mar lastSun 2:00 1:00 D Rule Denver 1920 only - Oct lastSun 2:00 0 S Rule Denver 1921 only - May 22 2:00 0 S Rule Denver 1965 1966 - Apr lastSun 2:00 1:00 D Rule Denver 1965 1966 - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Denver -6:59:56 - LMT 1883 Nov 18 19:00u -7:00 US M%sT 1920 -7:00 Denver M%sT 1942 -7:00 US M%sT 1946 -7:00 Denver M%sT 1967 -7:00 US M%sT # US Pacific time, represented by Los Angeles # # California, northern Idaho (Benewah, Bonner, Boundary, Clearwater, # Kootenai, Latah, Lewis, Nez Perce, and Shoshone counties, Idaho county # north of the Salmon River, and the towns of Burgdorf and Warren), # Nevada (except West Wendover), Oregon (except the northern ¾ of # Malheur county), and Washington # From Paul Eggert (2016-08-20): # In early February 1948, in response to California's electricity shortage, # PG&E changed power frequency from 60 to 59.5 Hz during daylight hours, # causing electric clocks to lose six minutes per day. (This did not change # legal time, and is not part of the data here.) See: # Ross SA. An energy crisis from the past: Northern California in 1948. # Working Paper No. 8, Institute of Governmental Studies, UC Berkeley, # 1973-11. https://escholarship.org/uc/item/8x22k30c # # In another measure to save electricity, DST was instituted from 1948-03-14 # at 02:01 to 1949-01-16 at 02:00, with the governor having the option to move # the fallback transition earlier. See pages 3-4 of: # http://clerk.assembly.ca.gov/sites/clerk.assembly.ca.gov/files/archive/Statutes/1948/48Vol1_Chapters.pdf # # In response: # # Governor Warren received a torrent of objecting mail, and it is not too much # to speculate that the objections to Daylight Saving Time were one important # factor in the defeat of the Dewey-Warren Presidential ticket in California. # -- Ross, p 25 # # On December 8 the governor exercised the option, setting the date to January 1 # (LA Times 1948-12-09). The transition time was 02:00 (LA Times 1949-01-01). # # Despite the controversy, in 1949 California voters approved Proposition 12, # which established DST from April's last Sunday at 01:00 until September's # last Sunday at 02:00. This was amended by 1962's Proposition 6, which changed # the fall-back date to October's last Sunday. See: # https://repository.uchastings.edu/cgi/viewcontent.cgi?article=1501&context=ca_ballot_props # https://repository.uchastings.edu/cgi/viewcontent.cgi?article=1636&context=ca_ballot_props # # Rule NAME FROM TO - IN ON AT SAVE LETTER Rule CA 1948 only - Mar 14 2:01 1:00 D Rule CA 1949 only - Jan 1 2:00 0 S Rule CA 1950 1966 - Apr lastSun 1:00 1:00 D Rule CA 1950 1961 - Sep lastSun 2:00 0 S Rule CA 1962 1966 - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Los_Angeles -7:52:58 - LMT 1883 Nov 18 20:00u -8:00 US P%sT 1946 -8:00 CA P%sT 1967 -8:00 US P%sT # Alaska # AK%sT is the modern abbreviation for -09 per USNO. # # From Paul Eggert (2017-06-15): # Howse writes that Alaska switched from the Julian to the Gregorian calendar, # and from east-of-GMT to west-of-GMT days, when the US bought it from Russia. # On Friday, 1867-10-18 (Gregorian), at precisely 15:30 local time, the # Russian forts and fleet at Sitka fired salutes to mark the ceremony of # formal transfer. See the Sacramento Daily Union (1867-11-14), p 3, col 2. # https://cdnc.ucr.edu/cgi-bin/cdnc?a=d&d=SDU18671114.2.12.1 # Sitka workers did not change their calendars until Sunday, 1867-10-20, # and so celebrated two Sundays that week. See: Ahllund T (tr Hallamaa P). # From the memoirs of a Finnish workman. Alaska History. 2006 Fall;21(2):1-25. # http://alaskahistoricalsociety.org/wp-content/uploads/2016/12/Ahllund-2006-Memoirs-of-a-Finnish-Workman.pdf # Include only the time zone part of this transition, ignoring the switch # from Julian to Gregorian, since we can't represent the Julian calendar. # # As far as we know, of the locations mentioned below only Sitka was # permanently inhabited in 1867 by anyone using either calendar. # (Yakutat was colonized by the Russians in 1799, but the settlement was # destroyed in 1805 by a Yakutat-kon war party.) Many of Alaska's inhabitants # were unaware of the US acquisition of Alaska, much less of any calendar or # time change. However, the Russian-influenced part of Alaska did observe # Russian time, and it is more accurate to model this than to ignore it. # The database format requires an exact transition time; use the Russian # salute as a somewhat-arbitrary time for the formal transfer of control for # all of Alaska. # # From Tim Parenti (2026-08-26): # Sitka's UT offset changed from +14:58:47 to -9:01:13, so effectuate this # change at 15:30:00 +14:58:47, or 1867-10-19 00:31:13 UT in the Alaskan # locations below. # From Paul Eggert (2014-07-18): # One opinion of the early 1980s turmoil in Alaska over time zones and # daylight saving time appeared as graffiti on a Juneau airport wall: # "Welcome to Juneau. Please turn your watch back to the 19th century." # See: Turner W. Alaska's four time zones now two. NY Times 1983-11-01. # http://www.nytimes.com/1983/11/01/us/alaska-s-four-time-zones-now-two.html # # Steve Ferguson (2011-01-31) referred to the following source: # Norris F. Keeping time in Alaska: national directives, local response. # Alaska History 2001;16(1-2). # http://alaskahistoricalsociety.org/discover-alaska/glimpses-of-the-past/keeping-time-in-alaska/ # From Arthur David Olson (2011-02-01): # Here's database-relevant material from the 2001 "Alaska History" article: # # On September 20 [1979]...DOT...officials decreed that on April 27, # 1980, Juneau and other nearby communities would move to Yukon Time. # Sitka, Petersburg, Wrangell, and Ketchikan, however, would remain on # Pacific Time. # # ...on September 22, 1980, DOT Secretary Neil E. Goldschmidt rescinded the # Department's September 1979 decision. Juneau and other communities in # northern Southeast reverted to Pacific Time on October 26. # # On October 28 [1983]...the Metlakatla Indian Community Council voted # unanimously to keep the reservation on Pacific Time. # # According to DOT official Joanne Petrie, Indian reservations are not # bound to follow time zones imposed by neighboring jurisdictions. # # (The last is consistent with how the database now handles the Navajo # Nation.) # From Arthur David Olson (2011-02-09): # I just spoke by phone with a staff member at the Metlakatla Indian # Community office (using contact information available at # http://www.commerce.state.ak.us/dca/commdb/CIS.cfm?Comm_Boro_name=Metlakatla # It's shortly after 1:00 here on the east coast of the United States; # the staffer said it was shortly after 10:00 there. When I asked whether # that meant they were on Pacific time, they said no - they were on their # own time. I asked about daylight saving; they said it wasn't used. I # did not inquire about practices in the past. # From Arthur David Olson (2011-08-17): # For lack of better information, assume that Metlakatla's # abandonment of use of daylight saving resulted from the 1983 vote. # From Steffen Thorsen (2015-11-09): # It seems Metlakatla did go off PST on Sunday, November 1, changing # their time to AKST and are going to follow Alaska's DST, switching # between AKST and AKDT from now on.... # https://www.krbd.org/2015/10/30/annette-island-times-they-are-a-changing/ # From Ryan Stanley (2018-11-06): # The Metlakatla community in Alaska has decided not to change its # clock back an hour starting on November 4th, 2018 (day before yesterday). # They will be gmtoff=-28800 year-round. # https://www.facebook.com/141055983004923/photos/pb.141055983004923.-2207520000.1541465673./569081370202380/ # From Paul Eggert (2018-12-16): # In a 2018-12-11 special election, Metlakatla voted to go back to # Alaska time (including daylight saving time) starting next year. # https://www.krbd.org/2018/12/12/metlakatla-to-follow-alaska-standard-time-allow-liquor-sales/ # # From Ryan Stanley (2019-01-11): # The community will be changing back on the 20th of this month... # From Tim Parenti (2019-01-11): # Per an announcement on the Metlakatla community's official Facebook page, the # "fall back" will be on Sunday 2019-01-20 at 02:00: # https://www.facebook.com/141055983004923/photos/607150969728753/ # So they won't be waiting for Alaska to join them on 2019-03-10, but will # rather change their clocks twice in seven weeks. # From Paul Eggert (2023-01-23): # America/Adak is for the Aleutian Islands that are part of Alaska # and are west of 169.5° W. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Juneau 15:02:19 - LMT 1867 Oct 19 0:31:13u -8:57:41 - LMT 1900 Aug 20 12:00 -8:00 - PST 1942 -8:00 US P%sT 1946 -8:00 - PST 1969 -8:00 US P%sT 1980 Apr 27 2:00 -9:00 US Y%sT 1980 Oct 26 2:00 -8:00 US P%sT 1983 Oct 30 2:00 -9:00 US Y%sT 1983 Nov 30 -9:00 US AK%sT Zone America/Sitka 14:58:47 - LMT 1867 Oct 19 0:31:13u -9:01:13 - LMT 1900 Aug 20 12:00 -8:00 - PST 1942 -8:00 US P%sT 1946 -8:00 - PST 1969 -8:00 US P%sT 1983 Oct 30 2:00 -9:00 US Y%sT 1983 Nov 30 -9:00 US AK%sT Zone America/Metlakatla 15:13:42 - LMT 1867 Oct 19 0:31:13u -8:46:18 - LMT 1900 Aug 20 12:00 -8:00 - PST 1942 -8:00 US P%sT 1946 -8:00 - PST 1969 -8:00 US P%sT 1983 Oct 30 2:00 -8:00 - PST 2015 Nov 1 2:00 -9:00 US AK%sT 2018 Nov 4 2:00 -8:00 - PST 2019 Jan 20 2:00 -9:00 US AK%sT Zone America/Yakutat 14:41:05 - LMT 1867 Oct 19 0:31:13u -9:18:55 - LMT 1900 Aug 20 12:00 -9:00 - YST 1942 -9:00 US Y%sT 1946 -9:00 - YST 1969 -9:00 US Y%sT 1983 Nov 30 -9:00 US AK%sT Zone America/Anchorage 14:00:24 - LMT 1867 Oct 19 0:31:13u -9:59:36 - LMT 1900 Aug 20 12:00 -10:00 - AST 1942 -10:00 US A%sT 1967 Apr -10:00 - AHST 1969 -10:00 US AH%sT 1983 Oct 30 2:00 -9:00 US Y%sT 1983 Nov 30 -9:00 US AK%sT Zone America/Nome 12:58:22 - LMT 1867 Oct 19 0:31:13u -11:01:38 - LMT 1900 Aug 20 12:00 -11:00 - NST 1942 -11:00 US N%sT 1946 -11:00 - NST 1967 Apr -11:00 - BST 1969 -11:00 US B%sT 1983 Oct 30 2:00 -9:00 US Y%sT 1983 Nov 30 -9:00 US AK%sT Zone America/Adak 12:13:22 - LMT 1867 Oct 19 0:31:13u -11:46:38 - LMT 1900 Aug 20 12:00 -11:00 - NST 1942 -11:00 US N%sT 1946 -11:00 - NST 1967 Apr -11:00 - BST 1969 -11:00 US B%sT 1983 Oct 30 2:00 -10:00 US AH%sT 1983 Nov 30 -10:00 US H%sT # The following switches don't make our 1970 cutoff. # # Kiska observed Tokyo date and time during Japanese occupation from # 1942-06-06 to 1943-07-29, and similarly for Attu from 1942-06-07 to # 1943-05-29 (all dates American). Both islands are now uninhabited. # # Shanks writes that part of southwest Alaska (e.g. Aniak) # switched from -11:00 to -10:00 on 1968-09-22 at 02:00, # and another part (e.g. Akiak) made the same switch five weeks later. # # From David Flater (2004-11-09): # In e-mail, 2004-11-02, Ray Hudson, historian/liaison to the Unalaska # Historic Preservation Commission, provided this information, which # suggests that Unalaska deviated from statutory time from early 1967 # possibly until 1983: # # Minutes of the Unalaska City Council Meeting, January 10, 1967: # "Except for St. Paul and Akutan, Unalaska is the only important # location not on Alaska Standard Time. The following resolution was # made by William Robinson and seconded by Henry Swanson: Be it # resolved that the City of Unalaska hereby goes to Alaska Standard # Time as of midnight Friday, January 13, 1967 (1 A.M. Saturday, # January 14, Alaska Standard Time.) This resolution was passed with # three votes for and one against." # Hawaii # From Arthur David Olson (2010-12-09): # "Hawaiian Time" by Robert C. Schmitt and Doak C. Cox appears on pages 207-225 # of volume 26 of The Hawaiian Journal of History (1992). As of 2010-12-09, # the article is available at # https://evols.library.manoa.hawaii.edu/bitstream/10524/239/2/JL26215.pdf # and indicates that standard time was adopted effective noon, January # 13, 1896 (page 218), that in "1933, the Legislature decreed daylight # saving for the period between the last Sunday of each April and the # last Sunday of each September, but less than a month later repealed the # act," (page 220), that year-round daylight saving time was in effect # from 1942-02-09 to 1945-09-30 (page 221, with no time of day given for # when clocks changed) and that clocks were changed by 30 minutes # effective the second Sunday of June, 1947 (page 219, with no time of # day given for when clocks changed). A footnote for the 1933 changes # cites Session Laws of Hawaii 1933, "Act. 90 (approved 26 Apr. 1933) # and Act 163 (approved 21 May 1933)." # From Arthur David Olson (2011-01-19): # The following is from "Laws of the Territory of Hawaii Passed by the # Seventeenth Legislature: Regular Session 1933," available (as of # 2011-01-19) at American University's Pence Law Library. Page 85: "Act # 90...At 2 o'clock ante meridian of the last Sunday in April of each # year, the standard time of this Territory shall be advanced one # hour...This Act shall take effect upon its approval. Approved this 26th # day of April, A. D. 1933. LAWRENCE M JUDD, Governor of the Territory of # Hawaii." Page 172: "Act 163...Act 90 of the Session Laws of 1933 is # hereby repealed...This Act shall take effect upon its approval, upon # which date the standard time of this Territory shall be restored to # that existing immediately prior to the taking effect of said Act 90. # Approved this 21st day of May, A. D. 1933. LAWRENCE M. JUDD, Governor # of the Territory of Hawaii." # # Note that 1933-05-21 was a Sunday. # We're left to guess the time of day when Act 163 was approved; guess noon. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Pacific/Honolulu -10:31:26 - LMT 1896 Jan 13 12:00 -10:30 - HST 1933 Apr 30 2:00 -10:30 1:00 HDT 1933 May 21 12:00 -10:30 US H%sT 1947 Jun 8 2:00 -10:00 - HST # Now we turn to US areas that have diverged from the consensus since 1970. # Arizona mostly uses MST. # From Paul Eggert (2002-10-20): # # The information in the rest of this paragraph is derived from the # Daylight Saving Time web page # (2002-01-23) # maintained by the Arizona State Library, Archives and Public Records. # Between 1944-01-01 and 1944-04-01 the State of Arizona used standard # time, but by federal law railroads, airlines, bus lines, military # personnel, and some engaged in interstate commerce continued to # observe war (i.e., daylight saving) time. The 1944-03-17 Phoenix # Gazette says that was the date the law changed, and that 04-01 was # the date the state's clocks would change. In 1945 the State of # Arizona used standard time all year, again with exceptions only as # mandated by federal law. Arizona observed DST in 1967, but Arizona # Laws 1968, ch. 183 (effective 1968-03-21) repealed DST. # # Shanks says the 1944 experiment came to an end on 1944-03-17. # Go with the Arizona State Library instead. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Phoenix -7:28:18 - LMT 1883 Nov 18 19:00u -7:00 US M%sT 1944 Jan 1 0:01 -7:00 - MST 1944 Apr 1 0:01 -7:00 US M%sT 1944 Oct 1 0:01 -7:00 - MST 1967 -7:00 US M%sT 1968 Mar 21 -7:00 - MST # From Arthur David Olson (1988-02-13): # A writer from the Inter Tribal Council of Arizona, Inc., # notes in private correspondence dated 1987-12-28 that "Presently, only the # Navajo Nation participates in the Daylight Saving Time policy, due to its # large size and location in three states." (The "only" means that other # tribal nations don't use DST.) # # From Paul Eggert (2013-08-26): # See America/Denver for a zone appropriate for the Navajo Nation. # Southern Idaho (Ada, Adams, Bannock, Bear Lake, Bingham, Blaine, # Boise, Bonneville, Butte, Camas, Canyon, Caribou, Cassia, Clark, # Custer, Elmore, Franklin, Fremont, Gem, Gooding, Jefferson, Jerome, # Lemhi, Lincoln, Madison, Minidoka, Oneida, Owyhee, Payette, Power, # Teton, Twin Falls, Valley, Washington counties, and the southern # quarter of Idaho county) and eastern Oregon (most of Malheur County) # switched four weeks late in 1974. # # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Boise -7:44:49 - LMT 1883 Nov 18 20:00u -8:00 US P%sT 1923 May 13 2:00 -7:00 US M%sT 1974 -7:00 - MST 1974 Feb 3 2:00 -7:00 US M%sT # Indiana # # For a map of Indiana's time zone regions, see: # https://en.wikipedia.org/wiki/Time_in_Indiana # # From Paul Eggert (2018-11-30): # A brief but entertaining history of time in Indiana describes a 1949 debate # in the Indiana House where city legislators (who favored "fast time") # tussled with farm legislators (who didn't) over a bill to outlaw DST: # "Lacking enough votes, the city faction tries to filibuster until time runs # out on the session at midnight, but rural champion Rep. Herbert Copeland, # R-Madison, leans over the gallery railing and forces the official clock # back to 9 p.m., breaking it in the process. The clock sticks on 9 as the # debate rages on into the night. The filibuster finally dies out and the # bill passes, while outside the chamber, clocks read 3:30 a.m. In the end, # it doesn't matter which side won. The law has no enforcement powers and # is simply ignored by fast-time communities." # How Indiana went from 'God's time' to split zones and daylight-saving. # Indianapolis Star. 2018-11-27 14:58 -05. # https://www.indystar.com/story/news/politics/2018/11/27/indianapolis-indiana-time-zone-history-central-eastern-daylight-savings-time/2126300002/ # # From Paul Eggert (2007-08-17): # Since 1970, most of Indiana has been like America/Indiana/Indianapolis, # with the following exceptions: # # - Gibson, Jasper, Lake, LaPorte, Newton, Porter, Posey, Spencer, # Vanderburgh, and Warrick counties have been like America/Chicago. # # - Dearborn and Ohio counties have been like America/New_York. # # - Clark, Floyd, and Harrison counties have been like # America/Kentucky/Louisville. # # - Crawford, Daviess, Dubois, Knox, Martin, Perry, Pike, Pulaski, Starke, # and Switzerland counties have their own time zone histories as noted below. # # Shanks partitioned Indiana into 345 regions, each with its own time history, # and wrote "Even newspaper reports present contradictory information." # Those Hoosiers! Such a flighty and changeable people! # Fortunately, most of the complexity occurred before our cutoff date of 1970. # # Other than Indianapolis, the Indiana place names are so nondescript # that they would be ambiguous if we left them at the 'America' level. # So we reluctantly put them all in a subdirectory 'America/Indiana'. # From Paul Eggert (2014-06-26): # https://www.federalregister.gov/articles/2006/01/20/06-563/standard-time-zone-boundary-in-the-state-of-indiana # says "DOT is relocating the time zone boundary in Indiana to move Starke, # Pulaski, Knox, Daviess, Martin, Pike, Dubois, and Perry Counties from the # Eastern Time Zone to the Central Time Zone.... The effective date of # this rule is 2 a.m. EST Sunday, April 2, 2006, which is the # changeover date from standard time to Daylight Saving Time." # Strictly speaking, this meant the affected counties changed their # clocks twice that night, but this obviously was in error. The intent # was that 01:59:59 EST be followed by 02:00:00 CDT. # From Gwillim Law (2007-02-10): # The Associated Press has been reporting that Pulaski County, Indiana is # going to switch from Central to Eastern Time on March 11, 2007.... # http://www.indystar.com/apps/pbcs.dll/article?AID=/20070207/LOCAL190108/702070524/0/LOCAL # Rule NAME FROM TO - IN ON AT SAVE LETTER Rule Indianapolis 1941 only - Jun 22 2:00 1:00 D Rule Indianapolis 1941 1954 - Sep lastSun 2:00 0 S Rule Indianapolis 1946 1954 - Apr lastSun 2:00 1:00 D # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Indiana/Indianapolis -5:44:38 - LMT 1883 Nov 18 18:00u -6:00 US C%sT 1920 -6:00 Indianapolis C%sT 1942 -6:00 US C%sT 1946 -6:00 Indianapolis C%sT 1955 Apr 24 2:00 -5:00 - EST 1957 Sep 29 2:00 -6:00 - CST 1958 Apr 27 2:00 -5:00 - EST 1969 -5:00 US E%sT 1971 -5:00 - EST 2006 -5:00 US E%sT # # Eastern Crawford County, Indiana, left its clocks alone in 1974, # as well as from 1976 through 2005. # Rule NAME FROM TO - IN ON AT SAVE LETTER Rule Marengo 1951 only - Apr lastSun 2:00 1:00 D Rule Marengo 1951 only - Sep lastSun 2:00 0 S Rule Marengo 1954 1960 - Apr lastSun 2:00 1:00 D Rule Marengo 1954 1960 - Sep lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Indiana/Marengo -5:45:23 - LMT 1883 Nov 18 18:00u -6:00 US C%sT 1951 -6:00 Marengo C%sT 1961 Apr 30 2:00 -5:00 - EST 1969 -5:00 US E%sT 1974 Jan 6 2:00 -6:00 1:00 CDT 1974 Oct 27 2:00 -5:00 US E%sT 1976 -5:00 - EST 2006 -5:00 US E%sT # # Daviess, Dubois, Knox, and Martin Counties, Indiana, # switched from eastern to central time in April 2006, then switched back # in November 2007. # Rule NAME FROM TO - IN ON AT SAVE LETTER Rule Vincennes 1946 only - Apr lastSun 2:00 1:00 D Rule Vincennes 1946 only - Sep lastSun 2:00 0 S Rule Vincennes 1953 1954 - Apr lastSun 2:00 1:00 D Rule Vincennes 1953 1959 - Sep lastSun 2:00 0 S Rule Vincennes 1955 only - May 1 0:00 1:00 D Rule Vincennes 1956 1963 - Apr lastSun 2:00 1:00 D Rule Vincennes 1960 only - Oct lastSun 2:00 0 S Rule Vincennes 1961 only - Sep lastSun 2:00 0 S Rule Vincennes 1962 1963 - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Indiana/Vincennes -5:50:07 - LMT 1883 Nov 18 18:00u -6:00 US C%sT 1946 -6:00 Vincennes C%sT 1964 Apr 26 2:00 -5:00 - EST 1969 -5:00 US E%sT 1971 -5:00 - EST 2006 Apr 2 2:00 -6:00 US C%sT 2007 Nov 4 2:00 -5:00 US E%sT # # Perry County, Indiana, switched from eastern to central time in April 2006. # From Alois Treindl (2019-07-09): # The Indianapolis News, Friday 27 October 1967 states that Perry County # returned to CST. It went again to EST on 27 April 1969, as documented by the # Indianapolis star of Saturday 26 April. # Rule NAME FROM TO - IN ON AT SAVE LETTER Rule Perry 1955 only - May 1 0:00 1:00 D Rule Perry 1955 1960 - Sep lastSun 2:00 0 S Rule Perry 1956 1963 - Apr lastSun 2:00 1:00 D Rule Perry 1961 1963 - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Indiana/Tell_City -5:47:03 - LMT 1883 Nov 18 18:00u -6:00 US C%sT 1946 -6:00 Perry C%sT 1964 Apr 26 2:00 -5:00 - EST 1967 Oct 29 2:00 -6:00 US C%sT 1969 Apr 27 2:00 -5:00 US E%sT 1971 -5:00 - EST 2006 Apr 2 2:00 -6:00 US C%sT # # Pike County, Indiana moved from central to eastern time in 1977, # then switched back in 2006, then switched back again in 2007. # Rule NAME FROM TO - IN ON AT SAVE LETTER Rule Pike 1955 only - May 1 0:00 1:00 D Rule Pike 1955 1960 - Sep lastSun 2:00 0 S Rule Pike 1956 1964 - Apr lastSun 2:00 1:00 D Rule Pike 1961 1964 - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Indiana/Petersburg -5:49:07 - LMT 1883 Nov 18 18:00u -6:00 US C%sT 1955 -6:00 Pike C%sT 1965 Apr 25 2:00 -5:00 - EST 1966 Oct 30 2:00 -6:00 US C%sT 1977 Oct 30 2:00 -5:00 - EST 2006 Apr 2 2:00 -6:00 US C%sT 2007 Nov 4 2:00 -5:00 US E%sT # # Starke County, Indiana moved from central to eastern time in 1991, # then switched back in 2006. # From Arthur David Olson (1991-10-28): # An article on page A3 of the Sunday, 1991-10-27 Washington Post # notes that Starke County switched from Central time to Eastern time as of # 1991-10-27. # Rule NAME FROM TO - IN ON AT SAVE LETTER Rule Starke 1947 1961 - Apr lastSun 2:00 1:00 D Rule Starke 1947 1954 - Sep lastSun 2:00 0 S Rule Starke 1955 1956 - Oct lastSun 2:00 0 S Rule Starke 1957 1958 - Sep lastSun 2:00 0 S Rule Starke 1959 1961 - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Indiana/Knox -5:46:30 - LMT 1883 Nov 18 18:00u -6:00 US C%sT 1947 -6:00 Starke C%sT 1962 Apr 29 2:00 -5:00 - EST 1963 Oct 27 2:00 -6:00 US C%sT 1991 Oct 27 2:00 -5:00 - EST 2006 Apr 2 2:00 -6:00 US C%sT # # Pulaski County, Indiana, switched from eastern to central time in # April 2006 and then switched back in March 2007. # Rule NAME FROM TO - IN ON AT SAVE LETTER Rule Pulaski 1946 1960 - Apr lastSun 2:00 1:00 D Rule Pulaski 1946 1954 - Sep lastSun 2:00 0 S Rule Pulaski 1955 1956 - Oct lastSun 2:00 0 S Rule Pulaski 1957 1960 - Sep lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Indiana/Winamac -5:46:25 - LMT 1883 Nov 18 18:00u -6:00 US C%sT 1946 -6:00 Pulaski C%sT 1961 Apr 30 2:00 -5:00 - EST 1969 -5:00 US E%sT 1971 -5:00 - EST 2006 Apr 2 2:00 -6:00 US C%sT 2007 Mar 11 2:00 -5:00 US E%sT # # Switzerland County, Indiana, did not observe DST from 1973 through 2005. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Indiana/Vevay -5:40:16 - LMT 1883 Nov 18 18:00u -6:00 US C%sT 1954 Apr 25 2:00 -5:00 - EST 1969 -5:00 US E%sT 1973 -5:00 - EST 2006 -5:00 US E%sT # From Paul Eggert (2018-03-20): # The Louisville & Nashville Railroad's 1883-11-18 change occurred at # 10:00 old local time; train were supposed to come to a standstill # for precisely 18 minutes. See Bartky Fig. 1 (page 50). It is not # clear how this matched civil time in Louisville, so for now continue # to assume Louisville switched at noon new local time, like New York. # # From Michael Deckers (2019-08-06): # From the contemporary source given by Alois Treindl, # the switch in Louisville on 1946-04-28 was on 00:01 # From Paul Eggert (2019-08-26): # That source was the Louisville Courier-Journal, 1946-04-27, p 4. # Shanks gives 02:00 for all 20th-century transition times in Louisville. # Evidently this is wrong for spring 1946. Although also likely wrong # for other dates, we have no data. # # Part of Kentucky left its clocks alone in 1974. # This also includes Clark, Floyd, and Harrison counties in Indiana. # Rule NAME FROM TO - IN ON AT SAVE LETTER Rule Louisville 1921 only - May 1 2:00 1:00 D Rule Louisville 1921 only - Sep 1 2:00 0 S Rule Louisville 1941 only - Apr lastSun 2:00 1:00 D Rule Louisville 1941 only - Sep lastSun 2:00 0 S Rule Louisville 1946 only - Apr lastSun 0:01 1:00 D Rule Louisville 1946 only - Jun 2 2:00 0 S Rule Louisville 1950 1961 - Apr lastSun 2:00 1:00 D Rule Louisville 1950 1955 - Sep lastSun 2:00 0 S Rule Louisville 1956 1961 - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Kentucky/Louisville -5:43:02 - LMT 1883 Nov 18 18:00u -6:00 US C%sT 1921 -6:00 Louisville C%sT 1942 -6:00 US C%sT 1946 -6:00 Louisville C%sT 1961 Jul 23 2:00 -5:00 - EST 1968 -5:00 US E%sT 1974 Jan 6 2:00 -6:00 1:00 CDT 1974 Oct 27 2:00 -5:00 US E%sT # # Wayne County, Kentucky # # From Lake Cumberland LIFE # http://www.lake-cumberland.com/life/archive/news990129time.shtml # (1999-01-29) via WKYM-101.7: # Clinton County has joined Wayne County in asking the DoT to change from # the Central to the Eastern time zone.... The Wayne County government made # the same request in December. And while Russell County officials have not # taken action, the majority of respondents to a poll conducted there in # August indicated they would like to change to "fast time" also. # The three Lake Cumberland counties are the farthest east of any U.S. # location in the Central time zone. # # From Rich Wales (2000-08-29): # After prolonged debate, and despite continuing deep differences of opinion, # Wayne County (central Kentucky) is switching from Central (-0600) to Eastern # (-0500) time. They won't "fall back" this year. See Sara Shipley, # The difference an hour makes, Nando Times (2000-08-29 15:33 -0400). # # From Paul Eggert (2001-07-16): # The final rule was published in the # Federal Register 65, 160 (2000-08-17), pp 50154-50158. # https://www.gpo.gov/fdsys/pkg/FR-2000-08-17/html/00-20854.htm # Zone America/Kentucky/Monticello -5:39:24 - LMT 1883 Nov 18 18:00u -6:00 US C%sT 1946 -6:00 - CST 1968 -6:00 US C%sT 2000 Oct 29 2:00 -5:00 US E%sT # From Rives McDow (2000-08-30): # Here ... are all the changes in the US since 1985. # Kearny County, KS (put all of county on central; # previously split between MST and CST) ... 1990-10 # Starke County, IN (from CST to EST) ... 1991-10 # Oliver County, ND (from MST to CST) ... 1992-10 # West Wendover, NV (from PST TO MST) ... 1999-10 # Wayne County, KY (from CST to EST) ... 2000-10 # # From Paul Eggert (2001-07-17): # We don't know where the line used to be within Kearny County, KS, # so omit that change for now. # See America/Indiana/Knox for the Starke County, IN change. # See America/North_Dakota/Center for the Oliver County, ND change. # West Wendover, NV officially switched from Pacific to mountain time on # 1999-10-31. See the # Federal Register 64, 203 (1999-10-21), pp 56705-56707. # https://www.gpo.gov/fdsys/pkg/FR-1999-10-21/html/99-27240.htm # However, the Federal Register says that West Wendover already operated # on mountain time, and the rule merely made this official; # hence a separate tz entry is not needed. # Michigan # # From Bob Devine (1988-01-28): # Michigan didn't observe DST from 1968 to 1973. # # From Paul Eggert (1999-03-31): # Shanks writes that Michigan started using standard time on 1885-09-18, # but Howse writes (pp 124-125, referring to Popular Astronomy, 1901-01) # that Detroit kept # # local time until 1900 when the City Council decreed that clocks should # be put back twenty-eight minutes to Central Standard Time. Half the # city obeyed, half refused. After considerable debate, the decision # was rescinded and the city reverted to Sun time. A derisive offer to # erect a sundial in front of the city hall was referred to the # Committee on Sewers. Then, in 1905, Central time was adopted # by city vote. # # This story is too entertaining to be false, so go with Howse over Shanks. # # From Paul Eggert (2001-03-06): # Garland (1927) writes "Cleveland and Detroit advanced their clocks # one hour in 1914." This change is not in Shanks. We have no more # info, so omit this for now. # # From Paul Eggert (2019-07-06): # Due to a complicated set of legal maneuvers, in 1967 Michigan did # not start daylight saving time when the rest of the US did. # Instead, it began DST on Jun 14 at 00:01. This was big news: # the Detroit Free Press reported it at the top of Page 1 on # 1967-06-14, in an article "State Adjusting to Switch to Fast Time" # by Gary Blonston, above an article about Thurgood Marshall's # confirmation to the US Supreme Court. Although Shanks says Detroit # observed DST until 1967-10-29 00:01, that time of day seems to be # incorrect, as the Free Press later said DST ended in Michigan at the # same time as the rest of the US. Also, although Shanks reports no DST in # Detroit in 1968, it did observe DST that year; in the November 1968 # election Michigan voters narrowly repealed DST, effective 1969. # # Most of Michigan observed DST from 1973 on, but was a bit late in 1975. # Rule NAME FROM TO - IN ON AT SAVE LETTER Rule Detroit 1948 only - Apr lastSun 2:00 1:00 D Rule Detroit 1948 only - Sep lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Detroit -5:32:11 - LMT 1905 -6:00 - CST 1915 May 15 2:00 -5:00 - EST 1942 -5:00 US E%sT 1946 -5:00 Detroit E%sT 1967 Jun 14 0:01 -5:00 US E%sT 1969 -5:00 - EST 1973 -5:00 US E%sT 1975 -5:00 - EST 1975 Apr 27 2:00 -5:00 US E%sT # # Dickinson, Gogebic, Iron, and Menominee Counties, Michigan, # switched from EST to CST/CDT in 1973. # Rule NAME FROM TO - IN ON AT SAVE LETTER Rule Menominee 1946 only - Apr lastSun 2:00 1:00 D Rule Menominee 1946 only - Sep lastSun 2:00 0 S Rule Menominee 1966 only - Apr lastSun 2:00 1:00 D Rule Menominee 1966 only - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Menominee -5:50:27 - LMT 1885 Sep 18 12:00 -6:00 US C%sT 1946 -6:00 Menominee C%sT 1969 Apr 27 2:00 -5:00 - EST 1973 Apr 29 2:00 -6:00 US C%sT # Navassa # administered by the US Fish and Wildlife Service # claimed by US under the provisions of the 1856 Guano Islands Act # also claimed by Haiti # occupied 1857/1900 by the Navassa Phosphate Co # US lighthouse 1917/1996-09 # currently uninhabited # see Mark Fineman, "An Isle Rich in Guano and Discord", # _Los Angeles Times_ (1998-11-10), A1, A10; it cites # Jimmy Skaggs, _The Great Guano Rush_ (1994). # From Rob van Gent (2025-07-23): # Another useful source for historical time zone information appears to be # a series of circulars with the title "Standard Time Throughout the World" # issued between 1925 and 1950 by the U.S. Bureau of Standards. # I found the following issues online: # https://nvlpubs.nist.gov/nistpubs/Legacy/circ/nbscircular280.pdf (1925) # https://nvlpubs.nist.gov/nistpubs/Legacy/circ/nbscircular399.pdf (1932) # https://nvlpubs.nist.gov/nistpubs/Legacy/circ/nbscircular406.pdf (1935) # https://nvlpubs.nist.gov/nistpubs/Legacy/circ/nbscircular496.pdf (1950) ################################################################################ # From Paul Eggert (2017-02-10): # # Unless otherwise specified, the source for data through 1990 is: # Thomas G. Shanks and Rique Pottenger, The International Atlas (6th edition), # San Diego: ACS Publications, Inc. (2003). # Unfortunately this book contains many errors and cites no sources. # # Many years ago Gwillim Law wrote that a good source # for time zone data was the International Air Transport # Association's Standard Schedules Information Manual (IATA SSIM), # published semiannually. Law sent in several helpful summaries # of the IATA's data after 1990. Except where otherwise noted, # IATA SSIM is the source for entries after 1990. # # Other sources occasionally used include: # # Milne J. Civil time. Geogr J. 1899 Feb;13(2):173-94 # . # # Pearce C. The Great Daylight Saving Time Controversy. # Australian Ebook Publisher. 2017. ISBN 978-1-925516-96-8. # # Edward W. Whitman, World Time Differences, # Whitman Publishing Co, 2 Niagara Av, Ealing, London (undated), # which I found in the UCLA library. # # William Willett, The Waste of Daylight, 19th edition # # [PDF] (1914-03) # # For the 1911/1912 establishment of standard time in French possessions, see: # Société Française de Physique, Recueil de constantes physiques (1913), # page 752, 18b. # # See the 'europe' file for Greenland. # Canada # From Alain LaBonté (1994-11-14): # I post here the time zone abbreviations standardized in Canada # for both English and French in the CAN/CSA-Z234.4-89 standard.... # # UTC Standard time Daylight saving time # offset French English French English # -2:30 - - HAT NDT # -3 - - HAA ADT # -3:30 HNT NST - - # -4 HNA AST HAE EDT # -5 HNE EST HAC CDT # -6 HNC CST HAR MDT # -7 HNR MST HAP PDT # -8 HNP PST HAY YDT # -9 HNY YST - - # # HN: Heure Normale ST: Standard Time # HA: Heure Avancée DT: Daylight saving Time # # A: de l'Atlantique Atlantic # C: du Centre Central # E: de l'Est Eastern # M: Mountain # N: Newfoundland # P: du Pacifique Pacific # R: des Rocheuses # T: de Terre-Neuve # Y: du Yukon Yukon # # From Paul Eggert (1994-11-22): # Alas, this sort of thing must be handled by localization software. # Unless otherwise specified, the data entries for Canada are all from Shanks # & Pottenger. # From Chris Walton (2006-04-01, 2006-04-25, 2006-06-26, 2007-01-31, # 2007-03-01): # The British Columbia government announced yesterday that it will # adjust daylight savings next year to align with changes in the # U.S. and the rest of Canada.... # https://archive.news.gov.bc.ca/releases/news_releases_2005-2009/2006AG0014-000330.htm # ... # Nova Scotia # Daylight saving time will be extended by four weeks starting in 2007.... # https://www.novascotia.ca/just/regulations/rg2/2006/ma1206.pdf # # [For New Brunswick] the new legislation dictates that the time change is to # be done at 02:00 instead of 00:01. # https://www.gnb.ca/0062/acts/BBA-2006/Chap-19.pdf # ... # Manitoba has traditionally changed the clock every fall at 03:00. # As of 2006, the transition is to take place one hour earlier at 02:00. # https://web2.gov.mb.ca/laws/statutes/ccsm/o030e.php # ... # [Alberta, Ontario, Quebec] will follow US rules. # http://www.qp.gov.ab.ca/documents/spring/CH03_06.CFM # http://www.e-laws.gov.on.ca/DBLaws/Source/Regs/English/2006/R06111_e.htm # http://www2.publicationsduquebec.gouv.qc.ca/dynamicSearch/telecharge.php?type=5&file=2006C39A.PDF # ... # P.E.I. will follow US rules.... # http://www.assembly.pe.ca/bills/pdf_chapter/62/3/chapter-41.pdf # ... # Province of Newfoundland and Labrador.... # http://www.hoa.gov.nl.ca/hoa/bills/Bill0634.htm # ... # Yukon # https://www.gov.yk.ca/legislation/regs/oic2006_127.pdf # ... # N.W.T. will follow US rules. Whoever maintains the government web site # does not seem to believe in bookmarks. To see the news release, click the # following link and search for "Daylight Savings Time Change". Press the # "Daylight Savings Time Change" link; it will fire off a popup using # JavaScript. # http://www.exec.gov.nt.ca/currentnews/currentPR.asp?mode=archive # ... # Nunavut # An amendment to the Interpretation Act was registered on February 19/2007.... # http://action.attavik.ca/home/justice-gn/attach/2007/gaz02part2.pdf # From Paul Eggert (2014-10-18): # H. David Matthews and Mary Vincent's map # "It's about TIME", _Canadian Geographic_ (September-October 1998) # https://web.archive.org/web/19990827055050/https://canadiangeographic.ca/SO98/geomap.htm # contains detailed boundaries for regions observing nonstandard # time and daylight saving time arrangements in Canada circa 1998. # # National Research Council Canada maintains info about time zones and DST. # https://www.nrc-cnrc.gc.ca/eng/services/time/time_zones.html # https://www.nrc-cnrc.gc.ca/eng/services/time/faq/index.html#Q5 # Its unofficial information is often taken from Matthews and Vincent. # From Paul Eggert (2006-06-27): # For now, assume all of DST-observing Canada will fall into line with the # new US DST rules, # From Chris Walton (2011-12-01) # In the first of Tammy Hardwick's articles # http://www.ilovecreston.com/?p=articles&t=spec&ar=260 # she quotes the Friday November 1/1918 edition of the Creston Review. # The quote includes these two statements: # 'Sunday the CPR went back to the old system of time...' # '... The daylight saving scheme was dropped all over Canada at the same time,' # These statements refer to a transition from daylight time to standard time # that occurred nationally on Sunday October 27/1918. This transition was # also documented in the Saturday October 26/1918 edition of the Toronto Star. # In light of that evidence, we alter the date from the earlier believed # Oct 31, to Oct 27, 1918 (and Sunday is a more likely transition day # than Thursday) in all Canadian rulesets. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Canada 1918 only - Apr 14 2:00 1:00 D Rule Canada 1918 only - Oct 27 2:00 0 S Rule Canada 1942 only - Feb 9 2:00 1:00 W # War Rule Canada 1945 only - Aug 14 23:00u 1:00 P # Peace Rule Canada 1945 only - Sep 30 2:00 0 S Rule Canada 1974 1986 - Apr lastSun 2:00 1:00 D Rule Canada 1974 2006 - Oct lastSun 2:00 0 S Rule Canada 1987 2006 - Apr Sun>=1 2:00 1:00 D Rule Canada 2007 max - Mar Sun>=8 2:00 1:00 D Rule Canada 2007 max - Nov Sun>=1 2:00 0 S # Newfoundland and Labrador # From Paul Eggert (2017-10-14): # Legally Labrador should observe Newfoundland time; see: # McLeod J. Labrador time - legal or not? St. John's Telegram, 2017-10-07 # http://www.thetelegram.com/news/local/labrador-time--legal-or-not-154860/ # Matthews and Vincent (1998) write that the only part of Labrador # that follows the rules is the southeast corner, including Port Hope # Simpson and Mary's Harbour, but excluding, say, Black Tickle. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule StJohns 1917 only - Apr 8 2:00 1:00 D Rule StJohns 1917 only - Sep 17 2:00 0 S # Whitman gives 1919 Apr 5 and 1920 Apr 5; go with Shanks & Pottenger. Rule StJohns 1919 only - May 5 23:00 1:00 D Rule StJohns 1919 only - Aug 12 23:00 0 S # For 1931-1935 Whitman gives Apr same date; go with Shanks & Pottenger. Rule StJohns 1920 1935 - May Sun>=1 23:00 1:00 D Rule StJohns 1920 1935 - Oct lastSun 23:00 0 S # For 1936-1941 Whitman gives May Sun>=8 and Oct Sun>=1; go with Shanks & # Pottenger. Rule StJohns 1936 1941 - May Mon>=9 0:00 1:00 D Rule StJohns 1936 1941 - Oct Mon>=2 0:00 0 S # Whitman gives the following transitions: # 1942 03-01/12-31, 1943 05-30/09-05, 1944 07-10/09-02, 1945 01-01/10-07 # but go with Shanks & Pottenger and assume they used Canadian rules. # For 1946-9 Whitman gives May 5,4,9,1 - Oct 1,5,3,2, and for 1950 he gives # Apr 30 - Sep 24; go with Shanks & Pottenger. Rule StJohns 1946 1950 - May Sun>=8 2:00 1:00 D Rule StJohns 1946 1950 - Oct Sun>=2 2:00 0 S Rule StJohns 1951 1986 - Apr lastSun 2:00 1:00 D Rule StJohns 1951 1959 - Sep lastSun 2:00 0 S Rule StJohns 1960 1986 - Oct lastSun 2:00 0 S # From Paul Eggert (2000-10-02): # INMS (2000-09-12) says that, since 1988 at least, Newfoundland switches # at 00:01 local time. For now, assume it started in 1987. # From Michael Pelley (2011-09-12): # We received today, Monday, September 12, 2011, notification that the # changes to the Newfoundland Standard Time Act have been proclaimed. # The change in the Act stipulates that the change from Daylight Savings # Time to Standard Time and from Standard Time to Daylight Savings Time # now occurs at 2:00AM. # ... # http://www.assembly.nl.ca/legislation/sr/annualstatutes/2011/1106.chp.htm # ... # MICHAEL PELLEY | Manager of Enterprise Architecture - Solution Delivery # Office of the Chief Information Officer # Executive Council # Government of Newfoundland & Labrador Rule StJohns 1987 only - Apr Sun>=1 0:01 1:00 D Rule StJohns 1987 2006 - Oct lastSun 0:01 0 S Rule StJohns 1988 only - Apr Sun>=1 0:01 2:00 DD Rule StJohns 1989 2006 - Apr Sun>=1 0:01 1:00 D Rule StJohns 2007 2011 - Mar Sun>=8 0:01 1:00 D Rule StJohns 2007 2010 - Nov Sun>=1 0:01 0 S # # St John's has an apostrophe, but POSIX file names can't have apostrophes. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/St_Johns -3:30:52 - LMT 1884 -3:30:52 StJohns N%sT 1918 -3:30:52 Canada N%sT 1919 -3:30:52 StJohns N%sT 1935 Mar 30 -3:30 StJohns N%sT 1942 May 11 -3:30 Canada N%sT 1946 -3:30 StJohns N%sT 2011 Nov -3:30 Canada N%sT # most of east Labrador # The name 'Happy Valley-Goose Bay' is too long; use 'Goose Bay'. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Goose_Bay -4:01:40 - LMT 1884 # Happy Valley-Goose Bay -3:30:52 - NST 1918 -3:30:52 Canada N%sT 1919 -3:30:52 - NST 1935 Mar 30 -3:30 - NST 1936 -3:30 StJohns N%sT 1942 May 11 -3:30 Canada N%sT 1946 -3:30 StJohns N%sT 1966 Mar 15 2:00 -4:00 StJohns A%sT 2011 Nov -4:00 Canada A%sT # west Labrador, Nova Scotia, Prince Edward I, # Îles-de-la-Madeleine, Listuguj reserve # From Brian Inglis (2015-07-20): # From the historical weather station records available at: # https://weatherspark.com/history/28351/1971/Sydney-Nova-Scotia-Canada # Sydney shares the same time history as Glace Bay, so was # likely to be the same across the island.... # Sydney, as the capital and most populous location, or Cape Breton, would # have been better names for the zone had we known this in 1996. # From Paul Eggert (2015-07-20): # Shanks & Pottenger write that since 1970 most of this region has been like # Halifax. Many locales did not observe peacetime DST until 1972; # the Cape Breton area, represented by Glace Bay, is the largest we know of # (Glace Bay was perhaps not the best name choice but no point changing now). # Shanks & Pottenger also write that Liverpool, NS was the only town # in Canada to observe DST in 1971 but not 1970; for now we'll assume # this is a typo. # From Jeffery Nichols (2020-01-09): # America/Halifax ... also applies to Îles-de-la-Madeleine and the Listuguj # reserve in Quebec. Officially, this came into effect on January 1, 2007 # (Legal Time Act, CQLR c T-5.1), but the legislative debates surrounding that # bill say that it is "accommodating the customs and practices" of those # regions, which suggests that they have always been in-line with Halifax. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Halifax 1916 only - Apr 1 0:00 1:00 D Rule Halifax 1916 only - Oct 1 0:00 0 S Rule Halifax 1920 only - May 9 0:00 1:00 D Rule Halifax 1920 only - Aug 29 0:00 0 S Rule Halifax 1921 only - May 6 0:00 1:00 D Rule Halifax 1921 1922 - Sep 5 0:00 0 S Rule Halifax 1922 only - Apr 30 0:00 1:00 D Rule Halifax 1923 1925 - May Sun>=1 0:00 1:00 D Rule Halifax 1923 only - Sep 4 0:00 0 S Rule Halifax 1924 only - Sep 15 0:00 0 S Rule Halifax 1925 only - Sep 28 0:00 0 S Rule Halifax 1926 only - May 16 0:00 1:00 D Rule Halifax 1926 only - Sep 13 0:00 0 S Rule Halifax 1927 only - May 1 0:00 1:00 D Rule Halifax 1927 only - Sep 26 0:00 0 S Rule Halifax 1928 1931 - May Sun>=8 0:00 1:00 D Rule Halifax 1928 only - Sep 9 0:00 0 S Rule Halifax 1929 only - Sep 3 0:00 0 S Rule Halifax 1930 only - Sep 15 0:00 0 S Rule Halifax 1931 1932 - Sep Mon>=24 0:00 0 S Rule Halifax 1932 only - May 1 0:00 1:00 D Rule Halifax 1933 only - Apr 30 0:00 1:00 D Rule Halifax 1933 only - Oct 2 0:00 0 S Rule Halifax 1934 only - May 20 0:00 1:00 D Rule Halifax 1934 only - Sep 16 0:00 0 S Rule Halifax 1935 only - Jun 2 0:00 1:00 D Rule Halifax 1935 only - Sep 30 0:00 0 S Rule Halifax 1936 only - Jun 1 0:00 1:00 D Rule Halifax 1936 only - Sep 14 0:00 0 S Rule Halifax 1937 1938 - May Sun>=1 0:00 1:00 D Rule Halifax 1937 1941 - Sep Mon>=24 0:00 0 S Rule Halifax 1939 only - May 28 0:00 1:00 D Rule Halifax 1940 1941 - May Sun>=1 0:00 1:00 D Rule Halifax 1946 1949 - Apr lastSun 2:00 1:00 D Rule Halifax 1946 1949 - Sep lastSun 2:00 0 S Rule Halifax 1951 1954 - Apr lastSun 2:00 1:00 D Rule Halifax 1951 1954 - Sep lastSun 2:00 0 S Rule Halifax 1956 1959 - Apr lastSun 2:00 1:00 D Rule Halifax 1956 1959 - Sep lastSun 2:00 0 S Rule Halifax 1962 1973 - Apr lastSun 2:00 1:00 D Rule Halifax 1962 1973 - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Halifax -4:14:24 - LMT 1902 Jun 15 -4:00 Halifax A%sT 1918 -4:00 Canada A%sT 1919 -4:00 Halifax A%sT 1942 Feb 9 2:00s -4:00 Canada A%sT 1946 -4:00 Halifax A%sT 1974 -4:00 Canada A%sT Zone America/Glace_Bay -3:59:48 - LMT 1902 Jun 15 -4:00 Canada A%sT 1953 -4:00 Halifax A%sT 1954 -4:00 - AST 1972 -4:00 Halifax A%sT 1974 -4:00 Canada A%sT # New Brunswick # From Paul Eggert (2007-01-31): # The Time Definition Act # says they changed at 00:01 through 2006, and # makes it # clear that this was the case since at least 1993. # For now, assume it started in 1993. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Moncton 1933 1935 - Jun Sun>=8 1:00 1:00 D Rule Moncton 1933 1935 - Sep Sun>=8 1:00 0 S Rule Moncton 1936 1938 - Jun Sun>=1 1:00 1:00 D Rule Moncton 1936 1938 - Sep Sun>=1 1:00 0 S Rule Moncton 1939 only - May 27 1:00 1:00 D Rule Moncton 1939 1941 - Sep Sat>=21 1:00 0 S Rule Moncton 1940 only - May 19 1:00 1:00 D Rule Moncton 1941 only - May 4 1:00 1:00 D Rule Moncton 1946 1972 - Apr lastSun 2:00 1:00 D Rule Moncton 1946 1956 - Sep lastSun 2:00 0 S Rule Moncton 1957 1972 - Oct lastSun 2:00 0 S Rule Moncton 1993 2006 - Apr Sun>=1 0:01 1:00 D Rule Moncton 1993 2006 - Oct lastSun 0:01 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Moncton -4:19:08 - LMT 1883 Dec 9 -5:00 - EST 1902 Jun 15 -4:00 Canada A%sT 1933 -4:00 Moncton A%sT 1942 -4:00 Canada A%sT 1946 -4:00 Moncton A%sT 1973 -4:00 Canada A%sT 1993 -4:00 Moncton A%sT 2007 -4:00 Canada A%sT # Quebec # From Paul Eggert (2020-01-10): # See America/Toronto for most of Quebec, including Montreal. # See America/Halifax for the Îles de la Madeleine and the Listuguj reserve. # See America/Puerto_Rico for east of Natashquan. # Ontario # From Mark Brader (2003-07-26): # [According to the Toronto Star] Orillia, Ontario, adopted DST # effective Saturday, 1912-06-22, 22:00; the article mentions that # Port Arthur (now part of Thunder Bay, Ontario) as well as Moose Jaw # have already done so. In Orillia DST was to run until Saturday, # 1912-08-31 (no time mentioned), but it was met with considerable # hostility from certain segments of the public, and was revoked after # only two weeks - I copied it as Saturday, 1912-07-07, 22:00, but # presumably that should be -07-06. (1912-06-19, -07-12; also letters # earlier in June). # # Kenora, Ontario, was to abandon DST on 1914-06-01 (-05-21). # # From Paul Eggert (2017-07-08): # For more on Orillia, see: Daubs K. Bold attempt at daylight saving # time became a comic failure in Orillia. Toronto Star 2017-07-08. # https://www.thestar.com/news/insight/2017/07/08/bold-attempt-at-daylight-saving-time-became-a-comic-failure-in-orillia.html # From Paul Eggert (2025-03-20): # Also see the 1912-06-17 front page of The Evening Sunbeam, # reproduced in: Richardson M. "Daylight saving was a confusing # time in Orillia" in the 2025-03-15 Orillia Matters. Richardson writes, # "The first Sunday after the switch was made, [DST proponent and # Orillia mayor William Sword] Frost walked into church an hour late. # This became a symbol of the downfall of daylight saving in Orillia." # The mayor became known as "Daylight Bill". # https://www.orilliamatters.com/local-news/column-daylight-saving-was-a-confusing-time-in-orillia-10377529 # From Mark Brader (2010-03-06): # # In the (Toronto) Globe and Mail for Saturday, 1955-09-24, in the bottom # right corner of page 1, it says that Toronto will return to standard # time at 2 am Sunday morning (which agrees with the database), and that: # # The one-hour setback will go into effect throughout most of Ontario, # except in areas like Windsor which remains on standard time all year. # # ... I don't know if Windsor began observing DST when Detroit did, # or in 1974, or on some other date. # # By the way, the article continues by noting that: # # Some cities in the United States have pushed the deadline back # three weeks and will change over from daylight saving in October. # From Chris Walton (2024-01-09): # The [Toronto] changes in 1947, 1948, and 1949 took place at 2:00 a.m. local # time instead of midnight.... Toronto Daily Star - ... # April 2, 1947 - Page 39 ... April 7, 1948 - Page 13 ... # April 2, 1949 - Page 1 ... April 7, 1949 - Page 24 ... # November 25, 1949 - Page 52 ... April 21, 1950 - Page 14 ... # September 19, 1950 - Page 46 ... September 20, 1950 - Page 3 ... # November 24, 1950 - Page 21 # From Arthur David Olson (2010-07-17): # # "Standard Time and Time Zones in Canada" appeared in # The Journal of The Royal Astronomical Society of Canada, # volume 26, number 2 (February 1932) and, as of 2010-07-17, # was available at # http://adsabs.harvard.edu/full/1932JRASC..26...49S # # It includes the text below (starting on page 57): # # A list of the places in Canada using daylight saving time would # require yearly revision. From information kindly furnished by # the provincial governments and by the postmasters in many cities # and towns, it is found that the following places used daylight sav- # ing in 1930. The information for the province of Quebec is definite, # for the other provinces only approximate: # # Province Daylight saving time used # Prince Edward Island Not used. # Nova Scotia In Halifax only. # New Brunswick In St. John only. # Quebec In the following places: # Montreal Lachine # Quebec Mont-Royal # Lévis Iberville # St. Lambert Cap de la Madelèine # Verdun Loretteville # Westmount Richmond # Outremont St. Jérôme # Longueuil Greenfield Park # Arvida Waterloo # Chambly-Canton Beaulieu # Melbourne La Tuque # St. Théophile Buckingham # Ontario Used generally in the cities and towns along # the southerly part of the province. Not # used in the northwesterly part. # Manitoba Not used. # Saskatchewan In Regina only. # Alberta Not used. # British Columbia Not used. # # With some exceptions, the use of daylight saving may be said to be limited # to those cities and towns lying between Quebec city and Windsor, Ont. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Toronto 1919 only - Mar 30 23:30 1:00 D Rule Toronto 1919 only - Oct 26 0:00 0 S Rule Toronto 1920 only - May 2 2:00 1:00 D Rule Toronto 1920 only - Sep 26 0:00 0 S Rule Toronto 1921 only - May 15 2:00 1:00 D Rule Toronto 1921 only - Sep 15 2:00 0 S Rule Toronto 1922 1923 - May Sun>=8 2:00 1:00 D # Shanks & Pottenger say 1923-09-19; assume it's a typo and that "-16" # was meant. Rule Toronto 1922 1926 - Sep Sun>=15 2:00 0 S Rule Toronto 1924 1927 - May Sun>=1 2:00 1:00 D Rule Toronto 1927 1937 - Sep Sun>=25 2:00 0 S Rule Toronto 1928 1937 - Apr Sun>=25 2:00 1:00 D Rule Toronto 1938 1940 - Apr lastSun 2:00 1:00 D Rule Toronto 1938 1939 - Sep lastSun 2:00 0 S Rule Toronto 1945 1948 - Sep lastSun 2:00 0 S Rule Toronto 1946 1973 - Apr lastSun 2:00 1:00 D Rule Toronto 1949 1950 - Nov lastSun 2:00 0 S Rule Toronto 1951 1956 - Sep lastSun 2:00 0 S # Shanks & Pottenger say Toronto ended DST a week early in 1971, # namely on 1971-10-24, but Mark Brader wrote (2003-05-31) that this # is wrong, and that he had confirmed it by checking the 1971-10-30 # Toronto Star, which said that DST was ending 1971-10-31 as usual. Rule Toronto 1957 1973 - Oct lastSun 2:00 0 S # The Bahamas match Toronto since 1970. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Toronto -5:17:32 - LMT 1895 -5:00 Canada E%sT 1919 -5:00 Toronto E%sT 1942 Feb 9 2:00s -5:00 Canada E%sT 1946 -5:00 Toronto E%sT 1974 -5:00 Canada E%sT # From Tim Parenti (2026-09-21), per Paul Eggert (2000-10-02, 2006-07-17): # In the northwest of the province, most portions of Kenora, Rainy River, and # Thunder Bay Districts lying broadly west of 90°W longitude have historically # been aligned with neighboring Manitoba and use America/Winnipeg, in # accordance with section 2(2) of Ontario's Time Act, RSO 1990 chapter T.9: # https://www.ontario.ca/laws/statute/90t09 # # In practice, there are several exceptions to the 90°W dividing line set by # the province, all of which technically contravene the official Ontario rules: # - An area around Big Trout Lake in Kenora District aligns with # America/Winnipeg despite extending east of 90°W. # - An area around Upsala in the southwest of Thunder Bay District observes # Eastern Time (-05/-04) like America/Toronto despite extending west of # 90°W. # - Atikokan in the east of Rainy River District has observed year-round -05 # (EST) since at least 1952; this matches America/Panama since our 1970 # cutoff. # - Pickle Lake and the Mishkeegogamang First Nation (formerly New Osnaburgh) # in Kenora District have also observed year-round -05 (EST) since at least # 1998, though it is less clear how far back that arrangement goes; assume # the area is covered in the same way as Atikokan. # See the 'backzone' file for further commentary. # # From Tim Parenti (2026-10-13): # On 2026-09-17, after neighboring Manitoba announced a shift to year-round -05 # (see Manitoba, below), local leaders throughout northwestern Ontario # coordinated regionally alongside their local members of provincial parliament # to swiftly conduct several local surveys on the matter ahead of a potential # divergence on 2026-11-01. All of the surveys returned more than 90% support # for remaining in alignment with the “Manitoba model�. # https://www.nwonewswatch.com/local-journalism-initiative-lji/vast-majority-want-to-end-time-changes-mayor-12838025 # https://www.kenoraminerandnews.com/news/local-news/kenora-asks-province-to-end-clock-changes/article_5118b819-9f67-42ce-938a-1478b8ab541e.html # From Aoun Khan and Chris Walton (2026-10-13): # Ontario is set to move parts of the province’s northwest region to permanent # daylight time in order to align with Manitoba. # https://www.cp24.com/news/canada/2026/10/13/ontario-to-move-northwest-regions-to-permanent-daylight-time/ # The provincial government has issued a written news release: # https://news.ontario.ca/en/release/1008130/ontario-moving-to-permanent-daylight-saving-time-in-parts-of-northwestern-ontario # From Heitor David Pinto (2026-10-22): # [On 2026-10-19] the government of Ontario issued [Regulation 333/26] setting # the time in the part of the province west of 90°W as UTC-5 all year. It # comes into force on 1 November 2026. # https://www.ontario.ca/laws/regulation/r26333 # Manitoba # From Paul Eggert (2026-05-08): # For 1916 timestamps America/Winnipeg covers only a small region. See: # Cassidy C. Winnipeg’s 110-year history with daylight time. # Winnipeg Free Press. 2026-05-06. # https://www.winnipegfreepress.com/our-communities/correspondents/2026/05/06/winnipegs-110-year-history-with-daylight-time # Of the 1916 experiment, Cassidy writes: “As rural areas and nearby # urban centres such as Selkirk and Brandon did not adopt DST, the # City of Winnipeg essentially had its own time zone.� Cassidy also # writes that province-wide DST came into effect on 1963-05-12. # # Shanks & Pottenger write that Winnipeg did not observe DST in 1964 and 1965. # Although dubious in the light of Cassidy’s article, we lack a better source. # Perhaps S&P’s data are for the train stations, not for the city? # Also, S&P say Manitoba switched at 02:00 (not 02:00s) starting in 1966. # Since 02:00s is clearly correct for 1967 on, assume 02:00s in 1966 too. # From Rob Douglas (2006-04-06): # the old Manitoba Time Act - as amended by Bill 2, assented to # March 27, 1987 ... said ... # "between two o'clock Central Standard Time in the morning of # the first Sunday of April of each year and two o'clock Central # Standard Time in the morning of the last Sunday of October next # following, one hour in advance of Central Standard Time."... # I believe that the English legislation [of the old time act] had # been assented to (March 22, 1967).... # Also, as far as I can tell, there was no order-in-council varying # the time of Daylight Saving Time for 2005 and so the provisions of # the 1987 version would apply - the changeover was at 2:00 Central # Standard Time (i.e. not until 3:00 Central Daylight Time). # From Tim Parenti (2026-09-29): # As much of western Canada began announcing year-round DST in early 2026, # Manitoba Premier Wab Kinew publicly mused on forming a "Prairie time zone" by # joining Saskatchewan and Alberta on year-round -06, while committing to # conducting a public engagement survey on the matter. The results, however, # showed that while "[a] total of 92 per cent of Manitobans surveyed wanted to # see an end to seasonal time changes", "[a] majority of Manitobans (58 per # cent) preferred to stay on permanent DST, while 34 per cent voted to # eliminate the time change but stay on standard time." # # The survey results were published on Thursday 2026-09-17 alongside a policy # announcement adopting permanent DST, or year-round -05, just over 6 weeks # before clocks would otherwise fall back on 2026-11-01. # https://news.gov.mb.ca/news/?item=75397 # https://www.cbc.ca/news/canada/manitoba/daylight-time-manitoba-9.7347844 # # While other potential options would have required further legislation, the # Official Time Amendment Act (Bill 2 of the 42nd Legislature's 5th Session), # assented to 2023-05-30, provides for year-round -05 upon proclamation. # https://web2.gov.mb.ca/laws/statutes/2023/pdf/c00423.pdf # Order in Council 221/2026, issued 2026-09-23, directed the proclamation # bringing the Act into force on 2026-10-31, 26 hours before clocks would # otherwise fall back. # https://oic.gov.mb.ca/OICDocs/2026/09/Municipal%20and%20Northern%20Relations.260923.%20Official%20Time%20Amendment%20Act.2212026.pdf # https://web2.gov.mb.ca/laws/statutes/proclamations/2023c4(2026-10-31).pdf # # The Act further allows for the Lieutenant Governor in Council to, on # recommendation of the relevant minister, "make regulations varying the # reckoning" of official time in Manitoba, provided the minister has "due # regard for the applicable time zone in neighbouring jurisdictions." # # Use a temporary hack like Alberta’s to work around potential CLDR # limitations. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Winn 1916 only - Apr 23 0:00 1:00 D Rule Winn 1916 only - Sep 17 0:00 0 S Rule Winn 1918 only - Apr 14 2:00 1:00 D Rule Winn 1918 only - Oct 27 2:00 0 S Rule Winn 1937 only - May 16 2:00 1:00 D Rule Winn 1937 only - Sep 26 2:00 0 S Rule Winn 1942 only - Feb 9 2:00 1:00 W # War Rule Winn 1945 only - Aug 14 23:00u 1:00 P # Peace Rule Winn 1945 only - Sep lastSun 2:00 0 S Rule Winn 1946 only - May 12 2:00 1:00 D Rule Winn 1946 only - Oct 13 2:00 0 S Rule Winn 1947 1949 - Apr lastSun 2:00 1:00 D Rule Winn 1947 1949 - Sep lastSun 2:00 0 S Rule Winn 1950 only - May 1 2:00 1:00 D Rule Winn 1950 only - Sep 30 2:00 0 S Rule Winn 1951 1960 - Apr lastSun 2:00 1:00 D Rule Winn 1951 1958 - Sep lastSun 2:00 0 S Rule Winn 1959 only - Oct lastSun 2:00 0 S Rule Winn 1960 only - Sep lastSun 2:00 0 S Rule Winn 1963 only - Apr lastSun 2:00 1:00 D Rule Winn 1963 only - Sep 22 2:00 0 S Rule Winn 1966 1986 - Apr lastSun 2:00s 1:00 D Rule Winn 1966 2005 - Oct lastSun 2:00s 0 S Rule Winn 1987 2005 - Apr Sun>=1 2:00s 1:00 D # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Winnipeg -6:28:36 - LMT 1887 Jul 16 -6:00 Winn C%sT 2006 -6:00 Canada C%sT 2026 Oct 31 # Temporary hack; see above. -6:00 Canada CDT 2026 Nov 1 2:00 # End of temporary hack. -5:00 - EST # Saskatchewan # From Mark Brader (2003-07-26): # The first actual adoption of DST in Canada was at the municipal # level. As the [Toronto] Star put it (1912-06-07), "While people # elsewhere have long been talking of legislation to save daylight, # the city of Moose Jaw [Saskatchewan] has acted on its own hook." # DST in Moose Jaw began on Saturday, 1912-06-01 (no time mentioned: # presumably late evening, as below), and would run until "the end of # the summer". The discrepancy between municipal time and railroad # time was noted. # From Paul Eggert (2003-07-27): # Willett (1914-03) notes that DST "has been in operation ... in the # City of Moose Jaw, Saskatchewan, for one year." # From Paul Eggert (2019-07-25): # Pearce's book says Regina observed DST in 1914-1917. No dates and times, # unfortunately. It also says that in 1914 Saskatoon observed DST # from 1 June to 6 July, and that DST was also tried out in Davidson, # Melfort, and Prince Albert. # From Paul Eggert (2006-03-22): # Shanks & Pottenger say that since 1970 this region has mostly been as Regina. # Some western towns (e.g. Swift Current) switched from MST/MDT to CST in 1972. # Other western towns (e.g. Lloydminster) are like Edmonton. # Matthews and Vincent (1998) write that Denare Beach and Creighton # are like Winnipeg, in violation of Saskatchewan law. # From W. Jones (1992-11-06): # The. . .below is based on information I got from our law library, the # provincial archives, and the provincial Community Services department. # A precise history would require digging through newspaper archives, and # since you didn't say what you wanted, I didn't bother. # # Saskatchewan is split by a time zone meridian (105W) and over the years # the boundary became pretty ragged as communities near it reevaluated # their affiliations in one direction or the other. In 1965 a provincial # referendum favoured legislating common time practices. # # On 15 April 1966 the Time Act (c. T-14, Revised Statutes of # Saskatchewan 1978) was proclaimed, and established that the eastern # part of Saskatchewan would use CST year round, that districts in # northwest Saskatchewan would by default follow CST but could opt to # follow Mountain Time rules (thus 1 hour difference in the winter and # zero in the summer), and that districts in southwest Saskatchewan would # by default follow MT but could opt to follow CST. # # It took a few years for the dust to settle (I know one story of a town # on one time zone having its school in another, such that a mom had to # serve her family lunch in two shifts), but presently it seems that only # a few towns on the border with Alberta (e.g. Lloydminster) follow MT # rules any more; all other districts appear to have used CST year round # since sometime in the 1960s. # From Chris Walton (2006-06-26): # The Saskatchewan time act which was last updated in 1996 is about 30 pages # long and rather painful to read. # http://www.qp.gov.sk.ca/documents/English/Statutes/Statutes/T14.pdf # From Heitor David Pinto (2026-05-14): # In Saskatchewan, a bill was passed to replace the Time Act. It sets UTC-6 all # year in the whole province, including Lloydminster, but allows the government # to issue regulations specifying a different time in localities that request # so: # https://docs.legassembly.sk.ca/legdocs/Bills/30L2S/Bill30-58.pdf # The bill ... received royal assent today. # From Tim Parenti (2026-05-14): # In light of Alberta joining Saskatchewan on year-round -06, this simplifies # the prior Act's framework for the many local exceptions to year-round -06 in # border areas, by extending province-wide the notion of "time option areas" # which can be prescribed by regulation "if it is in the provincial interest" # for those areas to observe either "UTC-5, UTC-6 or UTC-7 for all or part of # the year" on at least 30 days' notice. # From Tim Parenti (2026-10-27), per Heitor David Pinto (2026-10-27): # Order in Council 438/2026 brings the new Act into force from 2026-10-30. # https://publications.saskatchewan.ca/#/products/130175 # This should allow the area around Creighton to more straightforwardly remain # aligned with Manitoba on year-round -05, since its previous de facto # arrangement technically violated provincial law without a special charter # like Lloydminster. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Regina 1918 only - Apr 14 2:00 1:00 D Rule Regina 1918 only - Oct 27 2:00 0 S Rule Regina 1930 1934 - May Sun>=1 0:00 1:00 D Rule Regina 1930 1934 - Oct Sun>=1 0:00 0 S Rule Regina 1937 1941 - Apr Sun>=8 0:00 1:00 D Rule Regina 1937 only - Oct Sun>=8 0:00 0 S Rule Regina 1938 only - Oct Sun>=1 0:00 0 S Rule Regina 1939 1941 - Oct Sun>=8 0:00 0 S Rule Regina 1942 only - Feb 9 2:00 1:00 W # War Rule Regina 1945 only - Aug 14 23:00u 1:00 P # Peace Rule Regina 1945 only - Sep lastSun 2:00 0 S Rule Regina 1946 only - Apr Sun>=8 2:00 1:00 D Rule Regina 1946 only - Oct Sun>=8 2:00 0 S Rule Regina 1947 1957 - Apr lastSun 2:00 1:00 D Rule Regina 1947 1957 - Sep lastSun 2:00 0 S Rule Regina 1959 only - Apr lastSun 2:00 1:00 D Rule Regina 1959 only - Oct lastSun 2:00 0 S # Rule Swift 1957 only - Apr lastSun 2:00 1:00 D Rule Swift 1957 only - Oct lastSun 2:00 0 S Rule Swift 1959 1961 - Apr lastSun 2:00 1:00 D Rule Swift 1959 only - Oct lastSun 2:00 0 S Rule Swift 1960 1961 - Sep lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Regina -6:58:36 - LMT 1905 Sep -7:00 Regina M%sT 1960 Apr lastSun 2:00 -6:00 - CST Zone America/Swift_Current -7:11:20 - LMT 1905 Sep -7:00 Canada M%sT 1946 Apr lastSun 2:00 -7:00 Regina M%sT 1950 -7:00 Swift M%sT 1972 Apr lastSun 2:00 -6:00 - CST # Alberta # From Alois Treindl (2019-07-19): # There was no DST in Alberta in 1967... Calgary Herald, 29 April 1967. # 1969, no DST, from Edmonton Journal 18 April 1969 # # From Paul Eggert (2019-07-25): # Pearce's book says that Alberta's 1948 Daylight Saving Act required # Mountain Standard Time without DST, and that "anyone who broke that law # could be fined up to $25 and costs". There seems to be no record of # anybody paying the fine. The law was not changed until an August 1971 # plebiscite reinstituted DST in 1972. This story is also mentioned in: # Boyer JP. Forcing Choice: The Risky Reward of Referendums. Dundum. 2017. # ISBN 978-1459739123. # From Roozbeh Pournader (2026-04-20): # https://calgaryherald.com/opinion/columnists/bell-alberta-daylight-time-year-round-premier-danielle-smith # # From Tim Parenti (2026-04-23): # Section 3 of Bill 31, the Red Tape Reduction Statutes Amendment Act, 2026 # https://docs.assembly.ab.ca/LADDAR_files/docs/bills/bill/legislature_31/session_2/20251023_bill-031.pdf # would repeal the Daylight Saving Time Act in the Revised Statutes of Alberta # 2000 Chapter D-5: # https://web.archive.org/web/20240128134751/https://kings-printer.alberta.ca/documents/Acts/D05.pdf # ...and substitutes a new chapter with language that closely parallels the # original. The new title is the Official Time Act and will be numbered # Chapter O-5.7. The Act establishes a standard time of UTC−6 without # replacing the language previously used to effectuate DST. # # From Tim Parenti (2026-06-19): # After receiving Royal Assent on 2026-05-14, Order in Council 204/2026 was # issued on 2026-06-18 proclaiming the relevant section of the bill in force on # the same date. Order in Council 206/2026, issued the same day, uses the # regulatory authority within the Act to prescribe the official term "Alberta # Time"; we use the traditional abbreviation CST for consistency. # https://kings-printer.alberta.ca/Documents/Orders/Orders_in_Council/2026/2026_204.pdf # https://kings-printer.alberta.ca/Documents/Orders/Orders_in_Council/2026/2026_206.pdf # # Since wall clock times do not diverge from past practice until 2026-11-01, # use that transition date for now to work around potential CLDR limitations in # the meantime; see British Columbia, below. # # From Paul Eggert (2026-07-02): # The temporary hack for Alberta is needed for CLDR 48.2 (2026-03-17) # and earlier, not the CLDR 48.1-and-earlier which drives BC’s temporary hack. # Only a few platforms track minor CLDR releases, though, so the two # temporary hacks have roughly the same effect in practice. # # The term “Alberta Time� is legally prescribed from yesterday until # 2031-06-30, when the regulation in OiC 206/2026 expires to ensure that the # term is reviewed by then for relevancy and need. This plan for possible # obsolescence affects neither timekeeping nor TZDB’s data, which do # not contain the string “Alberta Time�. # # From Tim Parenti (2026-07-21): # The new Official Time Act is now in the Revised Statutes as Chapter O-5.7: # https://kings-printer.alberta.ca/documents/Acts/O05p7.pdf # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Edm 1918 1919 - Apr Sun>=8 2:00 1:00 D Rule Edm 1918 only - Oct 27 2:00 0 S Rule Edm 1919 only - May 27 2:00 0 S Rule Edm 1920 1923 - Apr lastSun 2:00 1:00 D Rule Edm 1920 only - Oct lastSun 2:00 0 S Rule Edm 1921 1923 - Sep lastSun 2:00 0 S Rule Edm 1942 only - Feb 9 2:00 1:00 W # War Rule Edm 1945 only - Aug 14 23:00u 1:00 P # Peace Rule Edm 1945 only - Sep lastSun 2:00 0 S Rule Edm 1947 only - Apr lastSun 2:00 1:00 D Rule Edm 1947 only - Sep lastSun 2:00 0 S Rule Edm 1972 1986 - Apr lastSun 2:00 1:00 D Rule Edm 1972 2006 - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Edmonton -7:33:52 - LMT 1906 Sep -7:00 Edm M%sT 1987 -7:00 Canada M%sT 2026 Jun 18 # Temporary hack; see above. -7:00 Canada MDT 2026 Nov 1 2:00 # End of temporary hack. -6:00 - CST # British Columbia # From Paul Eggert (2006-03-22): # Shanks & Pottenger write that since 1970 most of this region has # been like Vancouver. # Dawson Creek uses MST. Much of east BC is like Edmonton. # From Matt Johnson (2015-09-21): # Fort Nelson, BC, Canada will cancel DST this year. So while previously they # were aligned with America/Vancouver, they're now aligned with # America/Dawson_Creek. # http://www.northernrockies.ca/EN/meta/news/archives/2015/northern-rockies-time-change.html # # From Tim Parenti (2015-09-23): # This requires a new zone for the Northern Rockies Regional Municipality, # America/Fort_Nelson. The resolution of 2014-12-08 was reached following a # 2014-11-15 poll with nearly 75% support. Effectively, the municipality has # been on MST (-0700) like Dawson Creek since it advanced its clocks on # 2015-03-08. # # From Paul Eggert (2019-07-25): # Shanks says Fort Nelson did not observe DST in 1946, unlike Vancouver. # Alois Treindl confirmed this on 07-22, citing the 1946-04-27 Vancouver Daily # Province. He also cited the 1946-09-28 Victoria Daily Times, which said # that Vancouver, Victoria, etc. "change at midnight Saturday"; for now, # guess they meant 02:00 Sunday since 02:00 was common practice in Vancouver. # # Early Vancouver, Volume Four, by Major J.S. Matthews, V.D., 2011 edition # says that a 1922 plebiscite adopted DST, but a 1923 plebiscite rejected it. # http://former.vancouver.ca/ctyclerk/archives/digitized/EarlyVan/SearchEarlyVan/Vol4pdf/MatthewsEarlyVancouverVol4_DaylightSavings.pdf # A catalog entry for a newspaper clipping seems to indicate that Vancouver # observed DST in 1941 from 07-07 through 09-27; see # https://searcharchives.vancouver.ca/daylight-saving-1918-starts-again-july-7-1941-start-d-s-sept-27-end-of-d-s-1941 # We have no further details, so omit them for now. # From Arthur David Olson (2026-03-02): # B. C. Gov News: “Adopting permanent daylight saving time: ‘Spring forward’ # on March 8 will be the last time change, ending twice-yearly clock changes.� # https://news.gov.bc.ca/releases/2026AG0013-000209 # # From Paul Eggert (2026-09-23): # The law says that 21 hours after the usual 2026-03-08 02:00 switch from # PST to PDT, the next day inaugurated the new standard time Pacific Time, # i.e., just one clock change but two name changes separated by 21 hours. # PT, the obvious abbreviation for Pacific Time, is one letter too short # to conform to TZDB’s (and POSIX’s) [-+[:alnum:]]{3,6} requirements. # For now, do this: # 1. As a temporary hack, pretend that the BC law takes effect # not on 2026-03-09 at 00:00, but on 2026-11-01 at 02:00. # This pretense works around a limitation in CLDR 48.1 (2026-01-08), # which would otherwise say the interval uses “Pacific Standard Time�. # (Below, this temporary hack is marked “Temporary hack; see above.�) # Strictly speaking this hack is incorrect since the interval uses # standard time, but it does have the right UT offset and it # works around the CLDR limitation. We should be able to remove # the temporary hack by November when there would be little point # to keeping it anyway. # 2. After the BC law takes effect, model the time as MST sans DST. # We can change this later if another conforming non-numeric abbreviation # for Pacific Time becomes more popular. Possibilities include: # MST - the most compatible with existing software and practice, # and already used in parts of BC and in Yukon # PDT - almost as software-friendly, but confusing because it implies # it is DST and is paired with PST, whereas PT is standard time # -07 - accurate and clear in itself, but makes BC look odd vs neighbors # PCT - as I understand it, inspired by the phrase “Pacific Coast Time� # though legally it’s “Pacific Time�; suggested by the BC government # # From Tim Parenti (2026-08-21): # In the southeast of the province, the Regional District of East Kootenay # (RDEK), Columbia Shuswap Regional District’s Electoral Area A (Rural Golden), # and the Town of Golden have all historically been aligned with neighboring # Alberta and use America/Edmonton. # # Shortly after the BC provincial government announced its adoption of # year-round -07, the RDEK Board of Directors on 2026-03-13 initially voted # narrowly, 8–7, that it would fall back one last time to rejoin the rest of BC # on -07 from November 2026. However, citing the rushed decision and lack of # feedback from residents, Mayor Steve Fairbairn of Elkford used his power to # ask the question be called a second time, remarking, “Pardon the pun, but # this is not a time-sensitive issue,� and the initial decision was rescinded # by the RDEK Board at its 2026-04-10 meeting. # https://www.cbc.ca/news/canada/british-columbia/what-time-is-it-in-the-east-kootenay-debate-9.7132624 # In the ensuing months, after Alberta announced and implemented its similar # shift to “permanent DST� (see above), RDEK coordinated a public survey across # all of the relevant BC communities. The survey found 89% of respondents in # favor of ending seasonal time changes, with 66% of that group preferring to # remain aligned with Alberta time on year-round -06 instead of the rest of BC. # In line with those results, the RDEK Board adopted year-round -06 at its # 2026-08-14 meeting. # https://www.cbc.ca/news/canada/british-columbia/east-kootenay-stops-changing-clocks-9.7308201 # # From Tim Parenti (2026-10-15): # Echoing RDEK’s earlier false step, the Town of Golden initially opted for # year-round -07 by a 4–2 Council vote on 2026-09-01. This was met with # significant pushback from residents preferring to maintain the existing # alignment with RDEK and Alberta, with a formal petition swiftly gathering # 2,395 signatures in two weeks from the town of 3,986 and some smaller # neighboring communities. In response, Council unanimously rescinded the # resolution at the following meeting on 2026-09-22, and unanimously replaced # it with year-round -06 to keep the regional alignment. # Acknowledging that “its initial move was driven in part by concerns about # dark winter mornings and pedestrian safety for children travelling to # school�, the new resolution directs town staff to partner on safety supports # and commits to reviewing the decision and its impacts in October 2027. # https://www.ctvnews.ca/calgary/article/golden-bc-council-reverses-course-on-time-zone-opts-for-mountain-daylight-time/ # Following a 2026-09-10 deferral “to gather and assess further information� in # light of the ongoing petition, the surrounding CSRD Area A unanimously # formalized its own change on 2026-10-15 in order to maintain the alignment. # https://www.csrd.bc.ca/m/newsflash/home/detail/2486 # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Vanc 1918 only - Apr 14 2:00 1:00 D Rule Vanc 1918 only - Oct 27 2:00 0 S Rule Vanc 1942 only - Feb 9 2:00 1:00 W # War Rule Vanc 1945 only - Aug 14 23:00u 1:00 P # Peace Rule Vanc 1945 only - Sep 30 2:00 0 S Rule Vanc 1946 1986 - Apr lastSun 2:00 1:00 D Rule Vanc 1946 only - Sep 29 2:00 0 S Rule Vanc 1947 1961 - Sep lastSun 2:00 0 S Rule Vanc 1962 2006 - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Vancouver -8:12:28 - LMT 1884 -8:00 Vanc P%sT 1987 -8:00 Canada P%sT 2026 Mar 9 # Temporary hack; see above. -8:00 Canada PDT 2026 Nov 1 2:00 # End of temporary hack. -7:00 - MST Zone America/Dawson_Creek -8:00:56 - LMT 1884 -8:00 Canada P%sT 1947 -8:00 Vanc P%sT 1972 Aug 30 2:00 -7:00 - MST Zone America/Fort_Nelson -8:10:47 - LMT 1884 -8:00 Vanc P%sT 1946 -8:00 - PST 1947 -8:00 Vanc P%sT 1987 -8:00 Canada P%sT 2015 Mar 8 2:00 -7:00 - MST # For Creston see America/Phoenix. # Northwest Territories, Nunavut, Yukon # From Chris Walton (2022-11-06): # Whitehorse Star - Thursday April 22, 1965 - page 1 # title: DST Starts Monday ... # https://www.newspapers.com/image/578587481/ # The title of this first article is wrong and/or misleading. # Also, the start time shown in the article is vague; it simply says "after # midnight" when it probably should have stated 2:00a.m.... # # Whitehorse Star - Monday October 25, 1965 - page 15 ... # https://www.newspapers.com/image/578589147/ # The 1965 Yukon Council minutes can be found here: # http://assets.yukonarchives.ca/PER_YG_06_1965_C20_S02_v1.pdf # ... I do not currently believe that NWT touched any of its clocks in 1965.... # # Whitehorse Star - Thursday Feb 24,1966 - page 2 # title: It's Time for YDT ... # https://www.newspapers.com/image/578575979/ ... # America/Whitehorse as a permanent change from UTC-9(YST) to # UTC-8(PST) at 00:00 on Sunday February 27, 1966.... # # Whitehorse Star - Friday April 28,1972 - page 6 # title: Daylight Saving Time for N.W.T.... # https://www.newspapers.com/image/578701610/ ... # Nunavut and NWT zones ... DST starting in 1972.... Start and End ... # should be the same as the rest of Canada # # # From Paul Eggert (2022-11-06): # For now, assume Yukon's 1965-04-22 spring forward was 00:00 -> 02:00, as this # seems likely than 02:00 -> 04:00 and matches "after midnight". # From Paul Eggert (2006-03-22): # Dawson switched to PST in 1973. Inuvik switched to MST in 1979. # Mathew Englander (1996-10-07) gives the following refs: # * 1967. Paragraph 28(34)(g) of the Interpretation Act, S.C. 1967-68, # c. 7 defines Yukon standard time as UTC-9.... # see Interpretation Act, R.S.C. 1985, c. I-21, s. 35(1). # [https://www.canlii.org/en/ca/laws/stat/rsc-1985-c-i-21/latest/rsc-1985-c-i-21.html] # * C.O. 1973/214 switched Yukon to PST on 1973-10-28 00:00. # * O.I.C. 1980/02 established DST. # * O.I.C. 1987/056 changed DST to Apr firstSun 2:00 to Oct lastSun 2:00. # From Brian Inglis (2015-04-14): # # I tried to trace the history of Yukon time and found the following # regulations, giving the reference title and URL if found, regulation name, # and relevant quote if available. Each regulation specifically revokes its # predecessor. The final reference is to the current Interpretation Act # authorizing and resulting from these regulatory changes. # # Only recent regulations were retrievable via Yukon government site search or # index, and only some via Canadian legal sources. Other sources used include # articles titled "Standard Time and Time Zones in Canada" from JRASC via ADS # Abstracts, cited by ADO for 1932 ..., and updated versions from 1958 and # 1970 quoted below; each article includes current extracts from provincial # and territorial ST and DST regulations at the end, summaries and details of # standard times and daylight saving time at many locations across Canada, # with time zone maps, tables and calculations for Canadian Sunrise, Sunset, # and LMST; they also cover many countries and global locations, with a chart # and table showing current Universal Time offsets, and may be useful as # another source of information for 1970 and earlier. # # * Standard Time and Time Zones in Canada; Smith, C.C.; JRASC, Vol. 26, # pp.49-77; February 1932; SAO/NASA Astrophysics Data System (ADS) # http://adsabs.harvard.edu/abs/1932JRASC..26...49S from p.75: # Yukon Interpretation Ordinance # Yukon standard time is the local mean time at the one hundred and # thirty-fifth meridian. # # * Standard Time and Time Zones in Canada; Smith, C.C.; Thomson, Malcolm M.; # JRASC, Vol. 52, pp.193-223; October 1958; SAO/NASA Astrophysics Data System # (ADS) http://adsabs.harvard.edu/abs/1958JRASC..52..193S from pp.220-1: # Yukon Interpretation Ordinance, 1955, Chap. 16. # # (1) Subject to this section, standard time shall be reckoned as nine # hours behind Greenwich Time and called Yukon Standard Time. # # (2) Notwithstanding subsection (1), the Commissioner may make regulations # varying the manner of reckoning standard time. # # * Yukon Territory Commissioner's Order 1966-20 Interpretation Ordinance # [no online source found] # # * Standard Time and Time Zones in Canada; Thomson, Malcolm M.; JRASC, # Vol. 64, pp.129-162; June 1970; SAO/NASA Astrophysics Data System (ADS) # http://adsabs.harvard.edu/abs/1970JRASC..64..129T from p.156: Yukon # Territory Commissioner's Order 1967-59 Interpretation Ordinance ... # # 1. Commissioner's Order 1966-20 dated at Whitehorse in the Yukon # Territory on 27th January, 1966, is hereby revoked. # # 2. Yukon (East) Standard Time as defined by section 36 of the # Interpretation Ordinance from and after mid-night on the 28th day of May, # 1967 shall be reckoned in the same manner as Pacific Standard Time, that # is to say, eight hours behind Greenwich Time in the area of the Yukon # Territory lying east of the 138th degree longitude west. # # 3. In the remainder of the Territory, lying west of the 138th degree # longitude west, Yukon (West) Standard Time shall be reckoned as nine # hours behind Greenwich Time. # # * Yukon Standard Time defined as Pacific Standard Time, YCO 1973/214 # https://www.canlii.org/en/yk/laws/regu/yco-1973-214/latest/yco-1973-214.html # C.O. 1973/214 INTERPRETATION ACT ... # # 1. Effective October 28, 1973 Commissioner's Order 1967/59 is hereby # revoked. # # 2. Yukon Standard Time as defined by section 36 of the Interpretation # Act from and after midnight on the twenty-eighth day of October, 1973 # shall be reckoned in the same manner as Pacific Standard Time, that is # to say eight hours behind Greenwich Time. # # * O.I.C. 1980/02 INTERPRETATION ACT # https://mm.icann.org/pipermail/tz/attachments/20201125/d5adc93b/CAYTOIC1980-02DST1980-01-04-0001.pdf # # * Yukon Daylight Saving Time, YOIC 1987/56 # https://www.canlii.org/en/yk/laws/regu/yoic-1987-56/latest/yoic-1987-56.html # O.I.C. 1987/056 INTERPRETATION ACT ... # # In every year between # (a) two o'clock in the morning in the first Sunday in April, and # (b) two o'clock in the morning in the last Sunday in October, # Standard Time shall be reckoned as seven hours behind Greenwich Time and # called Yukon Daylight Saving Time. # ... # Dated ... 9th day of March, A.D., 1987. # # * Yukon Daylight Saving Time 2006, YOIC 2006/127 # https://www.canlii.org/en/yk/laws/regu/yoic-2006-127/latest/yoic-2006-127.html # O.I.C. 2006/127 INTERPRETATION ACT ... # # 1. In Yukon each year the time for general purposes shall be 7 hours # behind Greenwich mean time during the period commencing at two o'clock # in the forenoon on the second Sunday of March and ending at two o'clock # in the forenoon on the first Sunday of November and shall be called # Yukon Daylight Saving Time. # # 2. Order-in-Council 1987/56 is revoked. # # 3. This order comes into force January 1, 2007. # # * Interpretation Act, RSY 2002, c 125 # https://www.canlii.org/en/yk/laws/stat/rsy-2002-c-125/latest/rsy-2002-c-125.html # From Chris Walton (2022-11-06): # The 5th edition of the Atlas of Canada contains a time zone map that # shows both legislated and observed time zone boundaries. # All communities on Baffin Island are shown to be observing Eastern time. # The date on the map is 1984. # https://ftp.maps.canada.ca/pub/nrcan_rncan/raster/atlas_5_ed/eng/other/referencemaps/mcr4056.pdf # From Rives McDow (1999-09-04): # Nunavut ... moved ... to incorporate the whole territory into one time zone. # Nunavut moves to single time zone Oct. 31 # http://www.nunatsiaq.com/nunavut/nvt90903_13.html # From Paul Eggert (1999-09-20): # Basic Facts: The New Territory # http://www.nunavut.com/basicfacts/english/basicfacts_1territory.html # (1999) reports that ... Coral Harbour does not observe DST. # From Paul Eggert (2000-10-02): # Matthews and Vincent (1998) say the following, but we lack histories # for these potential new Zones. # # The Canadian Forces station at Alert uses Eastern Time while the # handful of residents at the Eureka weather station [in the Central # zone] skip daylight savings. Baffin Island, which is crossed by the # Central, Eastern and Atlantic Time zones only uses Eastern Time. # Gjoa Haven, Taloyoak and Pelly Bay all use Mountain instead of # Central Time and Southampton Island [in the Central zone] is not # required to use daylight savings. # From # Nunavut now has two time zones (2000-11-10): # The Nunavut government would allow its employees in Kugluktuk and # Cambridge Bay to operate on central time year-round, putting them # one hour behind the rest of Nunavut for six months during the winter. # At the end of October the two communities had rebelled against # Nunavut's unified time zone, refusing to shift to eastern time with # the rest of the territory for the winter. Cambridge Bay remained on # central time, while Kugluktuk, even farther west, reverted to # mountain time, which they had used before the advent of Nunavut's # unified time zone in 1999. # # From Rives McDow (2001-01-20), quoting the Nunavut government: # The preceding decision came into effect at midnight, Saturday Nov 4, 2000. # From Paul Eggert (2000-12-04): # Let's just keep track of the official times for now. # From Rives McDow (2001-03-07): # The premier of Nunavut has issued a ministerial statement advising # that effective 2001-04-01, the territory of Nunavut will revert # back to three time zones (mountain, central, and eastern). Of the # cities in Nunavut, Coral Harbor is the only one that I know of that # has said it will not observe dst, staying on EST year round. I'm # checking for more info, and will get back to you if I come up with # more. # [Also see (2001-03-09).] # From Gwillim Law (2005-05-21): # According to ... # http://www.canadiangeographic.ca/Magazine/SO98/geomap.asp # (from a 1998 Canadian Geographic article), the de facto and de jure time # for Southampton Island (at the north end of Hudson Bay) is UTC-5 all year # round. Using Google, it's easy to find other websites that confirm this. # I wasn't able to find how far back this time regimen goes, but since it # predates the creation of Nunavut, it probably goes back many years.... # The Inuktitut name of Coral Harbour is Sallit, but it's rarely used. # # From Paul Eggert (2014-10-17): # For lack of better information, assume that Southampton Island observed # daylight saving only during wartime. Gwillim Law's email also # mentioned maps now maintained by National Research Council Canada; # see above for an up-to-date link. # From Chris Walton (2007-03-01): # ... the community of Resolute (located on Cornwallis Island in # Nunavut) moved from Central Time to Eastern Time last November. # Basically the community did not change its clocks at the end of # daylight saving.... # http://www.nnsl.com/frames/newspapers/2006-11/nov13_06none.html # From Chris Walton (2011-03-21): # Back in 2007 I initiated the creation of a new "zone file" for Resolute # Bay. Resolute Bay is a small community located about 900km north of # the Arctic Circle. The zone file was required because Resolute Bay had # decided to use UTC-5 instead of UTC-6 for the winter of 2006-2007. # # According to new information which I received last week, Resolute Bay # went back to using UTC-6 in the winter of 2007-2008... # # On March 11/2007 most of Canada went onto daylight saving. On March # 14/2007 I phoned the Resolute Bay hamlet office to do a "time check." I # talked to somebody that was both knowledgeable and helpful. I was able # to confirm that Resolute Bay was still operating on UTC-5. It was # explained to me that Resolute Bay had been on the Eastern Time zone # (EST) in the winter, and was now back on the Central Time zone (CDT). # i.e. the time zone had changed twice in the last year but the clocks # had not moved. The residents had to know which time zone they were in # so they could follow the correct TV schedule... # # On Nov 02/2008 most of Canada went onto standard time. On Nov 03/2008 I # phoned the Resolute Bay hamlet office...[D]ue to the challenging nature # of the phone call, I decided to seek out an alternate source of # information. I found an e-mail address for somebody by the name of # Stephanie Adams whose job was listed as "Inns North Support Officer for # Arctic Co-operatives." I was under the impression that Stephanie lived # and worked in Resolute Bay... # # On March 14/2011 I phoned the hamlet office again. I was told that # Resolute Bay had been using Central Standard Time over the winter of # 2010-2011 and that the clocks had therefore been moved one hour ahead # on March 13/2011. The person I talked to was aware that Resolute Bay # had previously experimented with Eastern Standard Time but he could not # tell me when the practice had stopped. # # On March 17/2011 I searched the Web to find an e-mail address of # somebody that might be able to tell me exactly when Resolute Bay went # off Eastern Standard Time. I stumbled on the name "Aziz Kheraj." Aziz # used to be the mayor of Resolute Bay and he apparently owns half the # businesses including "South Camp Inn." This website has some info on # Aziz: # http://www.uphere.ca/node/493 # # I sent Aziz an e-mail asking when Resolute Bay had stopped using # Eastern Standard Time. # # Aziz responded quickly with this: "hi, The time was not changed for the # 1 year only, the following year, the community went back to the old way # of "spring ahead-fall behind" currently we are zulu plus 5 hrs and in # the winter Zulu plus 6 hrs" # # This of course conflicted with everything I had ascertained in November 2008. # # I sent Aziz a copy of my 2008 e-mail exchange with Stephanie. Aziz # responded with this: "Hi, Stephanie lives in Winnipeg. I live here, You # may want to check with the weather office in Resolute Bay or do a # search on the weather through Env. Canada. web site" # # If I had realized the Stephanie did not live in Resolute Bay I would # never have contacted her. I now believe that all the information I # obtained in November 2008 should be ignored... # I apologize for reporting incorrect information in 2008. # From Tim Parenti (2020-03-05): # The government of Yukon announced [yesterday] the cessation of seasonal time # changes. "After clocks are pushed ahead one hour on March 8, the territory # will remain on [UTC-07]. ... [The government] found 93 per cent of # respondents wanted to end seasonal time changes and, of that group, 70 per # cent wanted 'permanent Pacific Daylight Saving Time.'" # https://www.cbc.ca/news/canada/north/yukon-end-daylight-saving-time-1.5486358 # # Although the government press release prefers PDT, we prefer MST for # consistency with nearby Dawson Creek, Creston, and Fort Nelson. # https://yukon.ca/en/news/yukon-end-seasonal-time-change # From Andrew G. Smith (2020-09-24): # Yukon has completed its regulatory change to be on UTC -7 year-round.... # http://www.gov.yk.ca/legislation/regs/oic2020_125.pdf # What we have done is re-defined Yukon Standard Time, as we are # authorized to do under section 33 of our Interpretation Act: # http://www.gov.yk.ca/legislation/acts/interpretation_c.pdf # # From Paul Eggert (2020-09-24): # tzdb uses the obsolete YST abbreviation for standard time in Yukon through # about 1970, and uses PST for standard time in Yukon since then. Consistent # with that, use MST for -07, the new standard time in Yukon effective Nov. 1. # From Tim Parenti (2026-04-21): # "[Northwest Territories] Premier R.J. Simpson announced Monday that the # territory will move to end seasonal time changes and will adopt a year-round # time standard instead. ... Simpson has previously said the territory wouldn't # end seasonal time changes until Alberta does." # https://www.cbc.ca/news/canada/north/nwt-ends-daylight-saving-9.7170964 # # From Tim Parenti (2026-07-24): # Now that Alberta has adopted year-round -06, NWT plans to follow through with # the same. Bill 13 of the 19th Assembly's 2nd Session, assented to # 2021-03-31, provides the NWT Commissioner in Executive Council with authority # to fix a year-round standard time: # https://www.ntlegislativeassembly.ca/sites/default/files/legacy/bill_13_-_reprint_-_interpretation_a.pdf # # From Paul Eggert (2026-08-21): # The regulations were registered 2026-08-18 and came into force today: # https://www.justice.gov.nt.ca/en/files/legislation/interpretation/interpretation.r2.pdf # Use a temporary hack like Alberta’s. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule NT_YK 1918 only - Apr 14 2:00 1:00 D Rule NT_YK 1918 only - Oct 27 2:00 0 S Rule NT_YK 1919 only - May 25 2:00 1:00 D Rule NT_YK 1919 only - Nov 1 0:00 0 S Rule NT_YK 1942 only - Feb 9 2:00 1:00 W # War Rule NT_YK 1945 only - Aug 14 23:00u 1:00 P # Peace Rule NT_YK 1945 only - Sep 30 2:00 0 S Rule NT_YK 1972 1986 - Apr lastSun 2:00 1:00 D Rule NT_YK 1972 2006 - Oct lastSun 2:00 0 S Rule NT_YK 1987 2006 - Apr Sun>=1 2:00 1:00 D Rule Yukon 1965 only - Apr lastSun 0:00 2:00 DD Rule Yukon 1965 only - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] # formerly Frobisher Bay Zone America/Iqaluit 0 - -00 1942 Aug # Frobisher Bay est. -5:00 NT_YK E%sT 1999 Oct 31 2:00 -6:00 Canada C%sT 2000 Oct 29 2:00 -5:00 Canada E%sT # aka Qausuittuq Zone America/Resolute 0 - -00 1947 Aug 31 # Resolute founded -6:00 NT_YK C%sT 2000 Oct 29 2:00 -5:00 - EST 2001 Apr 1 3:00 -6:00 Canada C%sT 2006 Oct 29 2:00 -5:00 - EST 2007 Mar 11 3:00 -6:00 Canada C%sT # aka Kangiqiniq Zone America/Rankin_Inlet 0 - -00 1957 # Rankin Inlet founded -6:00 NT_YK C%sT 2000 Oct 29 2:00 -5:00 - EST 2001 Apr 1 3:00 -6:00 Canada C%sT # aka Iqaluktuuttiaq Zone America/Cambridge_Bay 0 - -00 1920 # trading post est.? -7:00 NT_YK M%sT 1999 Oct 31 2:00 -6:00 Canada C%sT 2000 Oct 29 2:00 -5:00 - EST 2000 Nov 5 0:00 -6:00 - CST 2001 Apr 1 3:00 -7:00 Canada M%sT Zone America/Inuvik 0 - -00 1953 # Inuvik founded -8:00 NT_YK P%sT 1979 Apr lastSun 2:00 -7:00 NT_YK M%sT 1980 -7:00 Canada M%sT 2026 Aug 21 # Temporary hack; see above. -7:00 Canada MDT 2026 Nov 1 2:00 # End of temporary hack. -6:00 - CST Zone America/Whitehorse -9:00:12 - LMT 1900 Aug 20 -9:00 NT_YK Y%sT 1965 -9:00 Yukon Y%sT 1966 Feb 27 0:00 -8:00 - PST 1980 -8:00 Canada P%sT 2020 Nov 1 -7:00 - MST Zone America/Dawson -9:17:40 - LMT 1900 Aug 20 -9:00 NT_YK Y%sT 1965 -9:00 Yukon Y%sT 1973 Oct 28 0:00 -8:00 - PST 1980 -8:00 Canada P%sT 2020 Nov 1 -7:00 - MST ############################################################################### # Mexico # From Paul Eggert (2014-12-07): # The Investigation and Analysis Service of the # Mexican Library of Congress (MLoC) has published a # history of Mexican local time (in Spanish) # http://www.diputados.gob.mx/bibliot/publica/inveyana/polisoc/horver/index.htm # # Here are the discrepancies between Shanks & Pottenger (S&P) and the MLoC. # (In all cases we go with the MLoC.) # S&P report that Baja was at -8:00 in 1922/1923. # S&P say the 1930 transition in Baja was 1930-11-16. # S&P report no DST during summer 1931. # S&P report a transition at 1932-03-30 23:00, not 1932-04-01. # From Gwillim Law (2001-02-20): # There are some other discrepancies between the Decrees page and the # tz database. I think they can best be explained by supposing that # the researchers who prepared the Decrees page failed to find some of # the relevant documents. # From Heitor David Pinto (2024-08-04): # In 1931, the decree implementing DST specified that it would take # effect on 30 April.... # https://www.dof.gob.mx/nota_to_imagen_fs.php?cod_diario=192270&pagina=2&seccion=1 # # In 1981, the decree changing Campeche, Yucatán and Quintana Roo to UTC-5 # specified that it would enter into force on 26 December 1981 at 2:00.... # https://www.dof.gob.mx/nota_to_imagen_fs.php?codnota=4705667&fecha=23/12/1981&cod_diario=202796 # # In 1982, the decree returning Campeche and Yucatán to UTC-6 specified that # it would enter into force on 2 November 1982 at 2:00.... # https://www.dof.gob.mx/nota_to_imagen_fs.php?cod_diario=205689&pagina=3&seccion=0 # # Quintana Roo changed to UTC-6 on 4 January 1983 at 0:00, and again # to UTC-5 on 26 October 1997 at 2:00.... # https://www.dof.gob.mx/nota_to_imagen_fs.php?codnota=4787355&fecha=28/12/1982&cod_diario=206112 # https://www.dof.gob.mx/nota_to_imagen_fs.php?cod_diario=209559&pagina=15&seccion=0 # # Durango, Coahuila, Nuevo León and Tamaulipas were set to UTC-7 on 1 January # 1922, and changed to UTC-6 on 10 June 1927. Then Durango, Coahuila and # Nuevo León (but not Tamaulipas) returned to UTC-7 on 15 November 1930, # observed DST in 1931, and changed again to UTC-6 on 1 April 1932.... # https://www.dof.gob.mx/nota_to_imagen_fs.php?codnota=4441846&fecha=29/12/1921&cod_diario=187468 # https://www.dof.gob.mx/nota_to_imagen_fs.php?codnota=4541520&fecha=09/06/1927&cod_diario=193920 # https://www.dof.gob.mx/nota_to_imagen_fs.php?codnota=4491963&fecha=15/11/1930&cod_diario=190835 # https://www.dof.gob.mx/nota_to_imagen_fs.php?codnota=4418437&fecha=21/01/1932&cod_diario=185588 # # ... the ... 10 June 1927 ... decree only said 10 June 1927, without # specifying a time, so I suppose that it should be considered at 0:00. # https://www.dof.gob.mx/nota_to_imagen_fs.php?codnota=4541520&fecha=09/06/1927&cod_diario=193920 # # In 1942, the decree changing Baja California, Baja California Sur, Sonora, # Sinaloa and Nayarit to UTC-7 was published on 24 April, but it said that it # would apply from 1 April, so it's unclear when the change actually # occurred. The database currently shows 24 April 1942. # https://www.dof.gob.mx/nota_to_imagen_fs.php?cod_diario=192203&pagina=2&seccion=1 # # Baja California Sur, Sonora, Sinaloa and Nayarit never used UTC-8. The ... # 14 January 1949 ... change [to UTC-8] only occurred in Baja California. # https://www.dof.gob.mx/nota_to_imagen_fs.php?codnota=4515613&fecha=13/01/1949&cod_diario=192309 # # In 1945, the decree changing Baja California to UTC-8 specified that it # would take effect on the third day from its publication. # It was published on 12 November, so it would take effect on 15 November.... # https://www.dof.gob.mx/nota_to_imagen_fs.php?codnota=4555049&fecha=12/11/1945&cod_diario=194763 # # In 1948, the decree changing Baja California to UTC-7 specified that it # would take effect on "this date". The decree was made on 13 March, # but published on 5 April, so it's unclear when the change actually occurred. # The database currently shows 5 April 1948. # https://www.dof.gob.mx/nota_to_imagen_fs.php?cod_diario=188624&pagina=2&seccion=0 # # In 1949, the decree changing Baja California to UTC-8 was published on 13 # January, but it said that it would apply from 1 January, so it's unclear when # the change actually occurred. The database currently shows 14 January 1949. # https://www.dof.gob.mx/nota_to_imagen_fs.php?codnota=4515613&fecha=13/01/1949&cod_diario=192309 # # Baja California also observed UTC-7 from 1 May to 24 September 1950, # from 29 April to 30 September 1951 at 2:00, # and from 27 April to 28 September 1952 at 2:00.... # https://www.dof.gob.mx/nota_to_imagen_fs.php?codnota=4600403&fecha=29/04/1950&cod_diario=197505 # https://www.dof.gob.mx/nota_to_imagen_fs.php?codnota=4623553&fecha=23/09/1950&cod_diario=198805 # https://www.dof.gob.mx/nota_to_imagen_fs.php?codnota=4469444&fecha=27/04/1951&cod_diario=189317 # https://www.dof.gob.mx/nota_to_imagen_fs.php?codnota=4533868&fecha=10/03/1952&cod_diario=193465 # # All changes in Baja California from 1948 to 1952 match those in California, # on the same dates or with a difference of one day. # So it may be easier to implement these changes as DST with rule CA # during this whole period. # From Alois Treindl (2025-07-29): # I did a quick newspaper archive research on https://hndm.iib.unam.mx/ # and found that Periódico Oficial del Estado de Baja California Norte # (1973-04-20) states clearly that DST was observed from last Sunday # in April to last Sunday in October.... I have a few more data from the # official bulletin for DST begin or end in Baja California 1964 1967 1969 # 1972 1973 (already sent) 1974 1975 1976 I do not know whether it is safe to # assume that it also applied in the years where I did not yet find proof. # The 1974 end of DST contains a reference to an Acuerdo of 1973-dec-20 which # I could not find.... One might assume that Baja California, which followed # US-CA in all these other yours, did the same. # # From Paul Eggert (2025-08-04): # Assume that Tijuana agreed with San Diego from 1953 through 1996, # as this agrees with Alois Treindl's data and with Shanks. # For now, keep the slightly-different 1948/1952 history for Baja California, # as we have no information on whether 1948/1952 clocks in Tijuana followed # the decrees or followed San Diego. # From Mark Schapiro, writing in The Nation (2002-10-28): # https://www.thenation.com/article/archive/sowing-disaster/ # When Mexican clocks were turned back for daylight saving time in the spring, # the Zapotecs refused to make the adjustment, insisting that they live in # "God's time," not in what they derisively call "Fox time," referring to # President Vicente Fox in far-off Mexico City. # From Paul Eggert (2025-08-04): # Unfortunately we have no data to track this informal practice. # From Alan Perry (1996-02-15): # A guy from our Mexico subsidiary finally found the Presidential Decree # outlining the timezone changes in Mexico. # # ------------- Begin Forwarded Message ------------- # # I finally got my hands on the Official Presidential Decree that sets up the # rules for the DST changes. The rules are: # # 1. The country is divided in 3 timezones: # - Baja California Norte (the Mexico/BajaNorte TZ) # - Baja California Sur, Nayarit, Sinaloa and Sonora (the Mexico/BajaSur TZ) # - The rest of the country (the Mexico/General TZ) # # 2. From the first Sunday in April at 2:00 AM to the last Sunday in October # at 2:00 AM, the times in each zone are as follows: # BajaNorte: GMT+7 # BajaSur: GMT+6 # General: GMT+5 # # 3. The rest of the year, the times are as follows: # BajaNorte: GMT+8 # BajaSur: GMT+7 # General: GMT+6 # # The Decree was published in Mexico's Official Newspaper on January 4th. # # -------------- End Forwarded Message -------------- # From Paul Eggert (1996-06-12): # For an English translation of the decree, see # "Diario Oficial: Time Zone Changeover" (1996-01-04). # http://mexico-travel.com/extra/timezone_eng.html # From Rives McDow (1998-10-08): # The State of Quintana Roo has reverted back to central STD and DST times # (i.e. UTC -0600 and -0500 as of 1998-08-02). # From Rives McDow (2000-01-10): # Effective April 4, 1999 at 2:00 AM local time, Sonora changed to the time # zone 5 hours from the International Date Line, and will not observe daylight # savings time so as to stay on the same time zone as the southern part of # Arizona year round. # From Jesper Nørgaard, translating # (2001-01-17): # In Oaxaca, the 55.000 teachers from the Section 22 of the National # Syndicate of Education Workers, refuse to apply daylight saving each # year, so that the more than 10,000 schools work at normal hour the # whole year. # From Gwillim Law (2001-01-19): # ... says # (translated):... # January 17, 2000 - The Energy Secretary, Ernesto Martens, announced # that Summer Time will be reduced from seven to five months, starting # this year.... # http://www.publico.com.mx/scripts/texto3.asp?action=pagina&pag=21&pos=p&secc=naci&date=01/17/2001 # [translated], says "summer time will ... take effect on the first Sunday # in May, and end on the last Sunday of September. # From Arthur David Olson (2001-01-25): # The 2001-01-24 traditional Washington Post contained the page one # story "Timely Issue Divides Mexicans."... # http://www.washingtonpost.com/wp-dyn/articles/A37383-2001Jan23.html # ... Mexico City Mayor López Obrador "...is threatening to keep # Mexico City and its 20 million residents on a different time than # the rest of the country..." In particular, López Obrador would abolish # observation of Daylight Saving Time. # Official statute published by the Energy Department # http://www.conae.gob.mx/ahorro/decretohorver2001.html#decre # (2001-02-01) shows Baja and Chihauhua as still using US DST rules, # and Sonora with no DST. This was reported by Jesper Nørgaard (2001-02-03). # From Paul Eggert (2001-03-03): # # https://www.latimes.com/archives/la-xpm-2001-mar-03-mn-32561-story.html # James F. Smith writes in today's LA Times # * Sonora will continue to observe standard time. # * Last week Mexico City's mayor Andrés Manuel López Obrador decreed that # the Federal District will not adopt DST. # * 4 of 16 district leaders announced they'll ignore the decree. # * The decree does not affect federal-controlled facilities including # the airport, banks, hospitals, and schools. # # For now we'll assume that the Federal District will bow to federal rules. # From Jesper Nørgaard (2001-04-01): # I found some references to the Mexican application of daylight # saving, which modifies what I had already sent you, stating earlier # that a number of northern Mexican states would go on daylight # saving. The modification reverts this to only cover Baja California # (Norte), while all other states (except Sonora, who has no daylight # saving all year) will follow the original decree of president # Vicente Fox, starting daylight saving May 6, 2001 and ending # September 30, 2001. # References: "Diario de Monterrey" # Palabra (2001-03-31) # From Reuters (2001-09-04): # Mexico's Supreme Court on Tuesday declared that daylight savings was # unconstitutional in Mexico City, creating the possibility the # capital will be in a different time zone from the rest of the nation # next year.... The Supreme Court's ruling takes effect at 2:00 # a.m. (0800 GMT) on Sept. 30, when Mexico is scheduled to revert to # standard time. "This is so residents of the Federal District are not # subject to unexpected time changes," a statement from the court said. # From Jesper Nørgaard Welen (2002-03-12): # ... consulting my local grocery store(!) and my coworkers, they all insisted # that a new decision had been made to reinstate US style DST in Mexico.... # http://www.conae.gob.mx/ahorro/horaver2001_m1_2002.html (2002-02-20) # confirms this. Sonora as usual is the only state where DST is not applied. # From Steffen Thorsen (2009-12-28): # # Steffen Thorsen wrote: # > Mexico's House of Representatives has approved a proposal for northern # > Mexico's border cities to share the same daylight saving schedule as # > the United States. # Now this has passed both the Congress and the Senate, so starting from # 2010, some border regions will be the same: # http://www.signonsandiego.com/news/2009/dec/28/clocks-will-match-both-sides-border/ # http://www.elmananarey.com/diario/noticia/nacional/noticias/empatan_horario_de_frontera_con_eu/621939 # (Spanish) # # Could not find the new law text, but the proposed law text changes are here: # http://gaceta.diputados.gob.mx/Gaceta/61/2009/dic/20091210-V.pdf # (Gaceta Parlamentaria) # # There is also a list of the votes here: # http://gaceta.diputados.gob.mx/Gaceta/61/2009/dic/V2-101209.html # # Our page: # https://www.timeanddate.com/news/time/north-mexico-dst-change.html # From Arthur David Olson (2010-01-20): # The page # http://dof.gob.mx/nota_detalle.php?codigo=5127480&fecha=06/01/2010 # includes this text: # En los municipios fronterizos de Tijuana y Mexicali en Baja California; # Juárez y Ojinaga en Chihuahua; Acuña y Piedras Negras en Coahuila; # Anáhuac en Nuevo León; y Nuevo Laredo, Reynosa y Matamoros en # Tamaulipas, la aplicación de este horario estacional surtirá efecto # desde las dos horas del segundo domingo de marzo y concluirá a las dos # horas del primer domingo de noviembre. # En los municipios fronterizos que se encuentren ubicados en la franja # fronteriza norte en el territorio comprendido entre la línea # internacional y la línea paralela ubicada a una distancia de veinte # kilómetros, así como la Ciudad de Ensenada, Baja California, hacia el # interior del país, la aplicación de este horario estacional surtirá # efecto desde las dos horas del segundo domingo de marzo y concluirá a # las dos horas del primer domingo de noviembre. # From Steffen Thorsen (2014-12-08), translated by Gwillim Law: # The Mexican state of Quintana Roo will likely change to EST in 2015. # # http://www.unioncancun.mx/articulo/2014/12/04/medio-ambiente/congreso-aprueba-una-hora-mas-de-sol-en-qroo # "With this change, the time conflict that has existed between the municipios # of Quintana Roo and the municipio of Felipe Carrillo Puerto may come to an # end. The latter declared itself in rebellion 15 years ago when a time change # was initiated in Mexico, and since then it has refused to change its time # zone along with the rest of the country." # # From Steffen Thorsen (2015-01-14), translated by Gwillim Law: # http://sipse.com/novedades/confirman-aplicacion-de-nueva-zona-horaria-para-quintana-roo-132331.html # "...the new time zone will come into effect at two o'clock on the first Sunday # of February, when we will have to advance the clock one hour from its current # time..." # Also, the new zone will not use DST. # # From Carlos Raúl Perasso (2015-02-02): # The decree that modifies the Mexican Hour System Law has finally # been published at the Diario Oficial de la Federación # http://www.dof.gob.mx/nota_detalle.php?codigo=5380123&fecha=31/01/2015 # It establishes 5 zones for Mexico: # 1- Zona Centro (Central Zone): Corresponds to longitude 90 W, # includes most of Mexico, excluding what's mentioned below. # 2- Zona Pacífico (Pacific Zone): Longitude 105 W, includes the # states of Baja California Sur; Chihuahua; Nayarit (excluding Bahía # de Banderas which lies in Central Zone); Sinaloa and Sonora. # 3- Zona Noroeste (Northwest Zone): Longitude 120 W, includes the # state of Baja California. # 4- Zona Sureste (Southeast Zone): Longitude 75 W, includes the state # of Quintana Roo. # 5- The islands, reefs and keys shall take their timezone from the # longitude they are located at. # From Paul Eggert (2022-10-28): # The new Mexican law was published today: # https://www.dof.gob.mx/nota_detalle.php?codigo=5670045&fecha=28/10/2022 # This abolishes DST except where US DST rules are observed, # and in addition changes all of Chihuahua to -06 with no DST. # From Heitor David Pinto (2022-11-28): # Now the northern [municipios] want to have the same time zone as the # respective neighboring cities in the US, for example Juárez in UTC-7 with # DST, matching El Paso, and Ojinaga in UTC-6 with DST, matching Presidio.... # the president authorized the publication of the decree for November 29, # so the time change would occur on November 30 at 0:00. # http://puentelibre.mx/noticia/ciudad_juarez_cambio_horario_noviembre_2022/ # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Mexico 1931 only - Apr 30 0:00 1:00 D Rule Mexico 1931 only - Oct 1 0:00 0 S Rule Mexico 1939 only - Feb 5 0:00 1:00 D Rule Mexico 1939 only - Jun 25 0:00 0 S Rule Mexico 1940 only - Dec 9 0:00 1:00 D Rule Mexico 1941 only - Apr 1 0:00 0 S Rule Mexico 1943 only - Dec 16 0:00 1:00 W # War Rule Mexico 1944 only - May 1 0:00 0 S Rule Mexico 1950 only - Feb 12 0:00 1:00 D Rule Mexico 1950 only - Jul 30 0:00 0 S Rule Mexico 1996 2000 - Apr Sun>=1 2:00 1:00 D Rule Mexico 1996 2000 - Oct lastSun 2:00 0 S Rule Mexico 2001 only - May Sun>=1 2:00 1:00 D Rule Mexico 2001 only - Sep lastSun 2:00 0 S Rule Mexico 2002 2022 - Apr Sun>=1 2:00 1:00 D Rule Mexico 2002 2022 - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] # Quintana Roo; represented by Cancún Zone America/Cancun -5:47:04 - LMT 1922 Jan 1 6:00u -6:00 - CST 1981 Dec 26 2:00 -5:00 - EST 1983 Jan 4 0:00 -6:00 Mexico C%sT 1997 Oct 26 2:00 -5:00 Mexico E%sT 1998 Aug 2 2:00 -6:00 Mexico C%sT 2015 Feb 1 2:00 -5:00 - EST # Campeche, Yucatán; represented by Mérida Zone America/Merida -5:58:28 - LMT 1922 Jan 1 6:00u -6:00 - CST 1981 Dec 26 2:00 -5:00 - EST 1982 Nov 2 2:00 -6:00 Mexico C%sT # Coahuila, Nuevo León, Tamaulipas (near US border) # This includes the following municipios: # in Coahuila: Acuña, Allende, Guerrero, Hidalgo, Jiménez, Morelos, Nava, # Ocampo, Piedras Negras, Villa Unión, Zaragoza # in Nuevo León: Anáhuac # in Tamaulipas: Nuevo Laredo, Guerrero, Mier, Miguel Alemán, Camargo, # Gustavo Díaz Ordaz, Reynosa, Río Bravo, Valle Hermoso, Matamoros. # https://www.dof.gob.mx/nota_detalle.php?codigo=5670045&fecha=28/10/2022 Zone America/Matamoros -6:30:00 - LMT 1922 Jan 1 6:00u -6:00 - CST 1988 -6:00 US C%sT 1989 -6:00 Mexico C%sT 2010 -6:00 US C%sT # Durango; Coahuila, Nuevo León, Tamaulipas (away from US border) Zone America/Monterrey -6:41:16 - LMT 1922 Jan 1 6:00u -7:00 - MST 1927 Jun 10 -6:00 - CST 1930 Nov 15 -7:00 Mexico M%sT 1932 Apr 1 -6:00 - CST 1988 -6:00 US C%sT 1989 -6:00 Mexico C%sT # Central Mexico Zone America/Mexico_City -6:36:36 - LMT 1922 Jan 1 7:00u -7:00 - MST 1927 Jun 10 -6:00 - CST 1930 Nov 15 -7:00 Mexico M%sT 1932 Apr 1 -6:00 Mexico C%sT 2001 Sep 30 2:00 -6:00 - CST 2002 Feb 20 -6:00 Mexico C%sT # Chihuahua (near US border - western side) # This includes the municipios of Janos, Ascensión, Juárez, Guadalupe, and # Práxedis G Guerrero. # https://gaceta.diputados.gob.mx/PDF/65/2022/nov/20221124-VII.pdf Zone America/Ciudad_Juarez -7:05:56 - LMT 1922 Jan 1 7:00u -7:00 - MST 1927 Jun 10 -6:00 - CST 1930 Nov 15 -7:00 Mexico M%sT 1932 Apr 1 -6:00 - CST 1996 -6:00 Mexico C%sT 1998 -6:00 - CST 1998 Apr Sun>=1 3:00 -7:00 Mexico M%sT 2010 -7:00 US M%sT 2022 Oct 30 2:00 -6:00 - CST 2022 Nov 30 0:00 -7:00 US M%sT # Chihuahua (near US border - eastern side) # This includes the municipios of Coyame del Sotol, Ojinaga, and Manuel # Benavides. # https://gaceta.diputados.gob.mx/PDF/65/2022/nov/20221124-VII.pdf Zone America/Ojinaga -6:57:40 - LMT 1922 Jan 1 7:00u -7:00 - MST 1927 Jun 10 -6:00 - CST 1930 Nov 15 -7:00 Mexico M%sT 1932 Apr 1 -6:00 - CST 1996 -6:00 Mexico C%sT 1998 -6:00 - CST 1998 Apr Sun>=1 3:00 -7:00 Mexico M%sT 2010 -7:00 US M%sT 2022 Oct 30 2:00 -6:00 - CST 2022 Nov 30 0:00 -6:00 US C%sT # Chihuahua (away from US border) Zone America/Chihuahua -7:04:20 - LMT 1922 Jan 1 7:00u -7:00 - MST 1927 Jun 10 -6:00 - CST 1930 Nov 15 -7:00 Mexico M%sT 1932 Apr 1 -6:00 - CST 1996 -6:00 Mexico C%sT 1998 -6:00 - CST 1998 Apr Sun>=1 3:00 -7:00 Mexico M%sT 2022 Oct 30 2:00 -6:00 - CST # Sonora Zone America/Hermosillo -7:23:52 - LMT 1922 Jan 1 7:00u -7:00 - MST 1927 Jun 10 -6:00 - CST 1930 Nov 15 -7:00 Mexico M%sT 1932 Apr 1 -6:00 - CST 1942 Apr 24 -7:00 - MST 1996 -7:00 Mexico M%sT 1999 -7:00 - MST # Baja California Sur, Nayarit (except Bahía de Banderas), Sinaloa Zone America/Mazatlan -7:05:40 - LMT 1922 Jan 1 7:00u -7:00 - MST 1927 Jun 10 -6:00 - CST 1930 Nov 15 -7:00 Mexico M%sT 1932 Apr 1 -6:00 - CST 1942 Apr 24 -7:00 - MST 1970 -7:00 Mexico M%sT # Bahía de Banderas # From Alexander Krivenyshev (2010-04-21): # According to news, Bahía de Banderas (Mexican state of Nayarit) # changed time zone UTC-7 to new time zone UTC-6 on April 4, 2010 (to # share the same time zone as nearby city Puerto Vallarta, Jalisco). # # (Spanish) # Bahía de Banderas homologa su horario al del centro del # país, a partir de este domingo # http://www.nayarit.gob.mx/notes.asp?id=20748 # # Bahía de Banderas homologa su horario con el del Centro del # País # http://www.bahiadebanderas.gob.mx/principal/index.php?option=com_content&view=article&id=261:bahia-de-banderas-homologa-su-horario-con-el-del-centro-del-pais&catid=42:comunicacion-social&Itemid=50 # # (English) # Puerto Vallarta and Bahía de Banderas: One Time Zone # http://virtualvallarta.com/puertovallarta/puertovallarta/localnews/2009-12-03-Puerto-Vallarta-and-Bahia-de-Banderas-One-Time-Zone.shtml # http://www.worldtimezone.com/dst_news/dst_news_mexico08.html # # "Mexico's Senate approved the amendments to the Mexican Schedule System that # will allow Bahía de Banderas and Puerto Vallarta to share the same time # zone ..." # Baja California Sur, Nayarit, Sinaloa # From Arthur David Olson (2010-05-01): # Use "Bahia_Banderas" to keep the name to fourteen characters. Zone America/Bahia_Banderas -7:01:00 - LMT 1922 Jan 1 7:00u -7:00 - MST 1927 Jun 10 -6:00 - CST 1930 Nov 15 -7:00 Mexico M%sT 1932 Apr 1 -6:00 - CST 1942 Apr 24 -7:00 - MST 1970 -7:00 Mexico M%sT 2010 Apr 4 2:00 -6:00 Mexico C%sT # Baja California Zone America/Tijuana -7:48:04 - LMT 1922 Jan 1 7:00u -7:00 - MST 1924 -8:00 - PST 1927 Jun 10 -7:00 - MST 1930 Nov 15 -8:00 - PST 1931 Apr 1 -8:00 1:00 PDT 1931 Sep 30 -8:00 - PST 1942 Apr 24 -8:00 1:00 PWT 1945 Aug 14 23:00u -8:00 1:00 PPT 1945 Nov 15 # Peace -8:00 - PST 1948 Apr 5 -8:00 1:00 PDT 1949 Jan 14 -8:00 - PST 1950 May 1 -8:00 1:00 PDT 1950 Sep 24 -8:00 - PST 1951 Apr 29 2:00 -8:00 1:00 PDT 1951 Sep 30 2:00 -8:00 - PST 1952 Apr 27 2:00 -8:00 1:00 PDT 1952 Sep 28 2:00 -8:00 CA P%sT 1967 -8:00 US P%sT 1996 -8:00 Mexico P%sT 2001 -8:00 US P%sT 2002 Feb 20 -8:00 Mexico P%sT 2010 -8:00 US P%sT # From Paul Eggert (2006-03-22): # Formerly there was an America/Ensenada zone, which differed from # America/Tijuana only in that it did not observe DST from 1976 # through 1995. This was as per Shanks (1999). But Shanks & Pottenger say # Ensenada did not observe DST from 1948 through 1975. Guy Harris reports # that the 1987 OAG says "Only Ensenada, Mexicali, San Felipe and # Tijuana observe DST," which agrees with Shanks & Pottenger but implies that # DST-observance was a town-by-town matter back then. This concerns # data after 1970 so most likely there should be at least one Zone # other than America/Tijuana for Baja, but it's not clear yet what its # name or contents should be. # # From Paul Eggert (2015-10-08): # Formerly there was an America/Santa_Isabel zone, but this appears to # have come from a misreading of # http://dof.gob.mx/nota_detalle.php?codigo=5127480&fecha=06/01/2010 # It has been moved to the 'backward' file. # # From Paul Eggert (2022-10-28): # Today's new law states that the entire state of Baja California # follows US DST rules, which agrees with simplifications noted above. # # # Revillagigedo Is # no information ############################################################################### # Barbados # For 1899 Milne gives -3:58:29.2. # From P Chan (2020-12-09 and 2020-12-11): # Standard time of GMT-4 was adopted in 1911. # Definition of Time Act, 1911 (1911-7) [1911-08-28] # 1912, Laws of Barbados (5 v.), OCLC Number: 919801291, Vol. 4, Image No. 522 # 1944, Laws of Barbados (5 v.), OCLC Number: 84548697, Vol. 4, Image No. 122 # http://llmc.com/browse.aspx?type=2&coll=85&div=297 # # DST was observed in 1942-44. # Defence (Daylight Saving) Regulations, 1942, 1942-04-13 # Defence (Daylight Saving) (Repeal) Regulations, 1942, 1942-08-22 # Defence (Daylight Saving) Regulations, 1943, 1943-04-16 # Defence (Daylight Saving) (Repeal) Regulations, 1943, 1943-09-01 # Defence (Daylight Saving) Regulations, 1944, 1944-03-21 # [Defence (Daylight Saving) (Amendment) Regulations 1944, 1944-03-28] # Defence (Daylight Saving) (Repeal) Regulations, 1944, 1944-08-30 # # 1914-, Subsidiary Legis., Annual Vols. OCLC Number: 226290591 # 1942: Image Nos. 527-528, 555-556 # 1943: Image Nos. 178-179, 198 # 1944: Image Nos. 113-115, 129 # http://llmc.com/titledescfull.aspx?type=2&coll=85&div=297&set=98437 # # From Tim Parenti (2021-02-20): # The transitions below are derived from P Chan's sources, except that the 1977 # through 1980 transitions are from Shanks & Pottenger since we have no better # data there. Of particular note, the 1944 DST regulation only advanced the # time to "exactly three and a half hours later than Greenwich mean time", as # opposed to "three hours" in the 1942 and 1943 regulations. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Barb 1942 only - Apr 19 5:00u 1:00 D Rule Barb 1942 only - Aug 31 6:00u 0 S Rule Barb 1943 only - May 2 5:00u 1:00 D Rule Barb 1943 only - Sep 5 6:00u 0 S Rule Barb 1944 only - Apr 10 5:00u 0:30 - Rule Barb 1944 only - Sep 10 6:00u 0 S Rule Barb 1977 only - Jun 12 2:00 1:00 D Rule Barb 1977 1978 - Oct Sun>=1 2:00 0 S Rule Barb 1978 1980 - Apr Sun>=15 2:00 1:00 D Rule Barb 1979 only - Sep 30 2:00 0 S Rule Barb 1980 only - Sep 25 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] #STDOFF -3:58:29.2 Zone America/Barbados -3:58:29 - LMT 1911 Aug 28 # Bridgetown -4:00 Barb A%sT 1944 -4:00 Barb AST/-0330 1945 -4:00 Barb A%sT # Belize # From P Chan (2020-11-03): # Below are some laws related to the time in British Honduras/Belize: # # Definition of Time Ordinance, 1927 (No.4 of 1927) [1927-04-01] # Ordinances of British Honduras Passed in the Year 1927, p 19-20 # https://books.google.com/books?id=LqEpAQAAMAAJ&pg=RA3-PA19 # # Definition of Time (Amendment) Ordinance, 1942 (No. 5 of 1942) [1942-06-27] # Ordinances of British Honduras Passed in the Year 1942, p 31-32 # https://books.google.com/books?id=h6MpAQAAMAAJ&pg=RA6-PA95-IA44 # # Definition of Time Ordinance, 1945 (No. 19 of 1945) [1945-12-15] # Ordinances of British Honduras Passed in the Year 1945, p 49-50 # https://books.google.com/books?id=xaMpAQAAMAAJ&pg=RA2-PP1 # # Definition of Time Ordinance, 1947 (No. 1 of 1947) [1947-03-11] # Ordinances of British Honduras Passed in the Year 1947, p 1-2 # https://books.google.com/books?id=xaMpAQAAMAAJ&pg=RA3-PA1 # # Time (Definition of) Ordinance (Chapter 180) # The Laws of British Honduras in Force on the 15th Day of September, 1958 , Volume IV, p 2580 # https://books.google.com/books?id=v5QpAQAAMAAJ&pg=PA2580 # # Time (Definition of) (Amendment) Ordinance, 1968 (No. 13 of 1968) [1968-08-03] # https://books.google.com/books?id=xij7KEB_58wC&pg=RA1-PA428-IA9 # # Definition of Time Act (Chapter 339) # Law of Belize, Revised Edition 2000 # http://www.belizelaw.org/web/lawadmin/PDF%20files/cap339.pdf # From Paul Eggert (2020-11-03): # The transitions below are derived from P Chan's sources, except that the # 1973 through 1983 transitions are from Shanks & Pottenger since we have # no better data there. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Belize 1918 1941 - Oct Sat>=1 24:00 0:30 -0530 Rule Belize 1919 1942 - Feb Sat>=8 24:00 0 CST Rule Belize 1942 only - Jun 27 24:00 1:00 CWT Rule Belize 1945 only - Aug 14 23:00u 1:00 CPT Rule Belize 1945 only - Dec 15 24:00 0 CST Rule Belize 1947 1967 - Oct Sat>=1 24:00 0:30 -0530 Rule Belize 1948 1968 - Feb Sat>=8 24:00 0 CST Rule Belize 1973 only - Dec 5 0:00 1:00 CDT Rule Belize 1974 only - Feb 9 0:00 0 CST Rule Belize 1982 only - Dec 18 0:00 1:00 CDT Rule Belize 1983 only - Feb 12 0:00 0 CST # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Belize -5:52:48 - LMT 1912 Apr 1 -6:00 Belize %s # Bermuda # From Paul Eggert (2022-07-27): # For 1899 Milne gives -4:19:18.3 as the meridian of the clock tower, # Bermuda dockyard, Ireland I. This agrees with standard offset given in the # Daylight Saving Act, 1917 cited below. # It is not known when this time became standard for Bermuda; guess 1890. # The transition to -04 was specified by: # 1930: The Time Zone Act, 1929 (1929: No. 39) [1929-11-08] # https://books.google.com/books?id=7tdMAQAAIAAJ&pg=RA54-PP1 # From P Chan (2020-11-20): # Most of the information can be found online from the Bermuda National # Library - Digital Collection which includes The Royal Gazette (RG) until 1957 # https://bnl.contentdm.oclc.org/digital/ # I will cite the ID. For example, [10000] means # https://bnl.contentdm.oclc.org/digital/collection/BermudaNP02/id/10000 # # 1917: Apr 5 midnight to Sep 30 midnight # Daylight Saving Act, 1917 (1917 No. 13) [1917-04-02] # Bermuda Acts and Resolves 1917, p 37-38 # https://books.google.com/books?id=M-lCAQAAMAAJ&pg=PA36-IA2 # RG, 1917-04-04, p 6 [42340] gives the spring forward date. # # 1918: Apr 13 midnight to Sep 15 midnight # Daylight Saving Act, 1918 (1918 No. 9) [1918-04-06] # Bermuda Acts and Resolves 1917, p 13 # https://books.google.com/books?id=K-lCAQAAMAAJ&pg=RA1-PA7 # # Note that local mean time was still used before 1930. # # During WWII, DST was introduced by Defence Regulations # 1942: Jan 11 02:00 to Oct 18 02:00 [113646], [115726] # 1943: Mar 21 02:00 to Oct 31 02:00 [116704], [118193] # 1944: Mar 12 02:00 to Nov 5 02:00 [119225], [121593] # 1945: Mar 11 02:00 to Nov 4 02:00 [122369], [124461] # RG, 1942-01-08, p 2, 1942-10-12, p 2 , 1943-03-06, p 2, 1943-09-03, p 1, # 1944-02-29, p 6, 1944-09-20, p 2, 1945-02-13, p 2, 1945-11-03, p 1 # # In 1946, the House of Assembly rejected DST twice. [128686], [128076] # RG, 1946-03-16 p 1,1946-04-13 p 1 # # 1947: third Sunday in May 02:00 to second Sunday in September 02:00 # DST in 1947 was defined in the Daylight Saving Act, 1947 (1947: No. 12) # which expired at the end of the year. [125784] ,[132405], [144454], [138226] # RG, 1947-02-27, p 1, 1947-05-15, p 1, 1947-09-13, p 1, 1947-12-30, p 1 # # 1948-1952: fourth Sunday in May 02:00 to first Sunday in September 02:00 # DST in 1948 was defined in the Daylight Saving Act, 1948 (1948 : No. 12) # which was set to expired at the end of the year but it was extended until # the end of 1952 and was not further extended. # [129802], [139403], [146008], [135240], [144330], [139049], [143309], # [148271], [149773], [153589], [153802], [155924] # RG, 1948-04-13, p 1, 1948-05-22, p 1, 1948-09-04, p 1, 1949-05-21, p1, # 1949-09-03, p 1, 1950-05-27 p 1, 1950-09-02, p 1, 1951-05-27, p 1, # 1951-09-01, p 1, 1952-05-23, p 1, 1952-09-26, p 1, 1952-12-21, p 8 # # In 1953-1955, the House of Assembly rejected DST each year. [158996], # [162620], [166720] RG, 1953-05-02, p 1, 1954-04-01 p 1, 1955-03-12, p 1 # # 1956: fourth Sunday in May 02:00 to last Sunday in October 02:00 # Time Zone (Seasonal Variation) Act, 1956 (1956: No.44) [1956-05-25] # Bermuda Public Acts 1956, p 331-332 # https://books.google.com/books?id=Xs1AlmD_cEwC&pg=PA63 # # The extension of the Act was rejected by the House of Assembly. [176218] # RG, 1956-12-13, p 1 # # From the Chronological Table of Public and Private Acts up to 1985, it seems # that there does not exist other Acts related to DST before 1973. # https://books.google.com/books?id=r9hMAQAAIAAJ&pg=RA23-PA1 # Public Acts of the Legislature of the Islands of Bermuda, Together with # Statutory Instruments in Force Thereunder, Vol VII # From Dan Jones, reporting in The Royal Gazette (2006-06-26): # Next year, however, clocks in the US will go forward on the second Sunday # in March, until the first Sunday in November. And, after the Time Zone # (Seasonal Variation) Bill 2006 was passed in the House of Assembly on # Friday, the same thing will happen in Bermuda. # http://www.theroyalgazette.com/apps/pbcs.dll/article?AID=/20060529/NEWS/105290135 # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Bermuda 1917 only - Apr 5 24:00 1:00 - Rule Bermuda 1917 only - Sep 30 24:00 0 - Rule Bermuda 1918 only - Apr 13 24:00 1:00 - Rule Bermuda 1918 only - Sep 15 24:00 0 S Rule Bermuda 1942 only - Jan 11 2:00 1:00 D Rule Bermuda 1942 only - Oct 18 2:00 0 S Rule Bermuda 1943 only - Mar 21 2:00 1:00 D Rule Bermuda 1943 only - Oct 31 2:00 0 S Rule Bermuda 1944 1945 - Mar Sun>=8 2:00 1:00 D Rule Bermuda 1944 1945 - Nov Sun>=1 2:00 0 S Rule Bermuda 1947 only - May Sun>=15 2:00 1:00 D Rule Bermuda 1947 only - Sep Sun>=8 2:00 0 S Rule Bermuda 1948 1952 - May Sun>=22 2:00 1:00 D Rule Bermuda 1948 1952 - Sep Sun>=1 2:00 0 S Rule Bermuda 1956 only - May Sun>=22 2:00 1:00 D Rule Bermuda 1956 only - Oct lastSun 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] #STDOFF -4:19:18.3 Zone Atlantic/Bermuda -4:19:18 - LMT 1890 # Hamilton -4:19:18 Bermuda BMT/BST 1930 Jan 1 2:00 -4:00 Bermuda A%sT 1974 Apr 28 2:00 -4:00 Canada A%sT 1976 -4:00 US A%sT # Costa Rica # Milne gives -5:36:13.3 as San José mean time. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule CR 1979 1980 - Feb lastSun 0:00 1:00 D Rule CR 1979 1980 - Jun Sun>=1 0:00 0 S Rule CR 1991 1992 - Jan Sat>=15 0:00 1:00 D # IATA SSIM (1991-09) says the following was at 1:00; # go with Shanks & Pottenger. Rule CR 1991 only - Jul 1 0:00 0 S Rule CR 1992 only - Mar 15 0:00 0 S # There are too many San Josés elsewhere, so we'll use 'Costa Rica'. # Zone NAME STDOFF RULES FORMAT [UNTIL] #STDOFF -5:36:13.3 Zone America/Costa_Rica -5:36:13 - LMT 1890 # San José -5:36:13 - SJMT 1921 Jan 15 # San José Mean Time -6:00 CR C%sT # Coco # no information; probably like America/Costa_Rica # Cuba # From Paul Eggert (2013-02-21): # Milne gives -5:28:50.45 for the observatory at Havana, -5:29:23.57 # for the port, and -5:30 for meteorological observations. # For now, stick with Shanks & Pottenger. # From Arthur David Olson (1999-03-29): # The 1999-03-28 exhibition baseball game held in Havana, Cuba, between # the Cuban National Team and the Baltimore Orioles was carried live on # the Orioles Radio Network, including affiliate WTOP in Washington, DC. # During the game, play-by-play announcer Jim Hunter noted that # "We'll be losing two hours of sleep...Cuba switched to Daylight Saving # Time today." (The "two hour" remark referred to losing one hour of # sleep on 1999-03-28 - when the announcers were in Cuba as it switched # to DST - and one more hour on 1999-04-04 - when the announcers will have # returned to Baltimore, which switches on that date.) # From Steffen Thorsen (2013-11-11): # DST start in Cuba in 2004 ... does not follow the same rules as the # years before. The correct date should be Sunday 2004-03-28 00:00 ... # https://web.archive.org/web/20040402060750/http://www.granma.cu/espanol/2004/marzo/sab27/reloj.html # From Evert van der Veer via Steffen Thorsen (2004-10-28): # Cuba is not going back to standard time this year. # From Paul Eggert (2006-03-22): # http://www.granma.cu/ingles/2004/septiembre/juev30/41medid-i.html # says that it's due to a problem at the Antonio Guiteras # thermoelectric plant, and says "This October there will be no return # to normal hours (after daylight saving time)". # For now, let's assume that it's a temporary measure. # From Carlos A. Carnero Delgado (2005-11-12): # This year (just like in 2004-2005) there's no change in time zone # adjustment in Cuba. We will stay in daylight saving time: # http://www.granma.cu/espanol/2005/noviembre/mier9/horario.html # From Jesper Nørgaard Welen (2006-10-21): # An article in GRANMA INTERNACIONAL claims that Cuba will end # the 3 years of permanent DST next weekend, see # http://www.granma.cu/ingles/2006/octubre/lun16/43horario.html # "On Saturday night, October 28 going into Sunday, October 29, at 01:00, # watches should be set back one hour - going back to 00:00 hours - returning # to the normal schedule.... # From Paul Eggert (2007-03-02): # , dated yesterday, # says Cuban clocks will advance at midnight on March 10. # For lack of better information, assume Cuba will use US rules, # except that it switches at midnight standard time as usual. # # From Steffen Thorsen (2007-10-25): # Carlos Alberto Fonseca Arauz informed me that Cuba will end DST one week # earlier - on the last Sunday of October, just like in 2006. # # He supplied these references: # # http://www.prensalatina.com.mx/article.asp?ID={4CC32C1B-A9F7-42FB-8A07-8631AFC923AF}&language=ES # http://actualidad.terra.es/sociedad/articulo/cuba_llama_ahorrar_energia_cambio_1957044.htm # # From Alex Krivenyshev (2007-10-25): # Here is also article from Granma (Cuba): # # Regirá el Horario Normal desde el próximo domingo 28 de octubre # http://www.granma.cubaweb.cu/2007/10/24/nacional/artic07.html # # http://www.worldtimezone.com/dst_news/dst_news_cuba03.html # From Arthur David Olson (2008-03-09): # I'm in Maryland which is now observing United States Eastern Daylight # Time. At 9:44 local time I used RealPlayer to listen to # http://media.enet.cu/radioreloj # a Cuban information station, and heard # the time announced as "ocho cuarenta y cuatro" ("eight forty-four"), # indicating that Cuba is still on standard time. # From Steffen Thorsen (2008-03-12): # It seems that Cuba will start DST on Sunday, 2007-03-16... # It was announced yesterday, according to this source (in Spanish): # http://www.nnc.cubaweb.cu/marzo-2008/cien-1-11-3-08.htm # # Some more background information is posted here: # https://www.timeanddate.com/news/time/cuba-starts-dst-march-16.html # # The article also says that Cuba has been observing DST since 1963, # while Shanks (and tzdata) has 1965 as the first date (except in the # 1940's). Many other web pages in Cuba also claim that it has been # observed since 1963, but with the exception of 1970 - an exception # which is not present in tzdata/Shanks. So there is a chance we need to # change some historic records as well. # # One example: # http://www.radiohc.cu/espanol/noticias/mar07/11mar/hor.htm # From Jesper Nørgaard Welen (2008-03-13): # The Cuban time change has just been confirmed on the most authoritative # web site, the Granma. Please check out # http://www.granma.cubaweb.cu/2008/03/13/nacional/artic10.html # # Basically as expected after Steffen Thorsen's information, the change # will take place midnight between Saturday and Sunday. # From Arthur David Olson (2008-03-12): # Assume Sun>=15 (third Sunday) going forward. # From Alexander Krivenyshev (2009-03-04) # According to the Radio Reloj - Cuba will start Daylight Saving Time on # midnight between Saturday, March 07, 2009 and Sunday, March 08, 2009- # not on midnight March 14 / March 15 as previously thought. # # http://www.worldtimezone.com/dst_news/dst_news_cuba05.html # (in Spanish) # From Arthur David Olson (2009-03-09) # I listened over the Internet to # http://media.enet.cu/readioreloj # this morning; when it was 10:05 a. m. here in Bethesda, Maryland the # the time was announced as "diez cinco" - the same time as here, indicating # that has indeed switched to DST. Assume second Sunday from 2009 forward. # From Steffen Thorsen (2011-03-08): # Granma announced that Cuba is going to start DST on 2011-03-20 00:00:00 # this year. Nothing about the end date known so far (if that has # changed at all). # # Source: # http://granma.co.cu/2011/03/08/nacional/artic01.html # # Our info: # https://www.timeanddate.com/news/time/cuba-starts-dst-2011.html # # From Steffen Thorsen (2011-10-30) # Cuba will end DST two weeks later this year. Instead of going back # tonight, it has been delayed to 2011-11-13 at 01:00. # # One source (Spanish) # http://www.radioangulo.cu/noticias/cuba/17105-cuba-restablecera-el-horario-del-meridiano-de-greenwich.html # # Our page: # https://www.timeanddate.com/news/time/cuba-time-changes-2011.html # # From Steffen Thorsen (2012-03-01) # According to Radio Reloj, Cuba will start DST on Midnight between March # 31 and April 1. # # Radio Reloj has the following info (Spanish): # http://www.radioreloj.cu/index.php/noticias-radio-reloj/71-miscelaneas/7529-cuba-aplicara-el-horario-de-verano-desde-el-1-de-abril # # Our info on it: # https://www.timeanddate.com/news/time/cuba-starts-dst-2012.html # From Steffen Thorsen (2012-11-03): # Radio Reloj and many other sources report that Cuba is changing back # to standard time on 2012-11-04: # http://www.radioreloj.cu/index.php/noticias-radio-reloj/36-nacionales/9961-regira-horario-normal-en-cuba-desde-el-domingo-cuatro-de-noviembre # From Paul Eggert (2012-11-03): # For now, assume the future rule is first Sunday in November. # From an anonymous contributor (2026-10-24): # On October 24, 2026, the Cuban authorities announced that daylight saving # time would remain in effect until November 7, 2027, suspending the usual # return to standard time scheduled for November 1, 2026. # From Tim Parenti (2026-10-26): # The measure delays the start times for schools and workplaces by 30 minutes # during the period in which standard time would normally be in effect "to # avoid disrupting social activities during the early part of the day". # http://www.cuba.cu/otras/2026-10-25/cuba-mantendra-horario-de-verano-hasta-noviembre-de-2027/69459 # https://en.cibercuba.com/noticias/2026-10-24-u1-e74-s27061-nid343708-cuba-mantendra-horario-verano-noviembre-2027-retrasara # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Cuba 1928 only - Jun 10 0:00 1:00 D Rule Cuba 1928 only - Oct 10 0:00 0 S Rule Cuba 1940 1942 - Jun Sun>=1 0:00 1:00 D Rule Cuba 1940 1942 - Sep Sun>=1 0:00 0 S Rule Cuba 1945 1946 - Jun Sun>=1 0:00 1:00 D Rule Cuba 1945 1946 - Sep Sun>=1 0:00 0 S Rule Cuba 1965 only - Jun 1 0:00 1:00 D Rule Cuba 1965 only - Sep 30 0:00 0 S Rule Cuba 1966 only - May 29 0:00 1:00 D Rule Cuba 1966 only - Oct 2 0:00 0 S Rule Cuba 1967 only - Apr 8 0:00 1:00 D Rule Cuba 1967 1968 - Sep Sun>=8 0:00 0 S Rule Cuba 1968 only - Apr 14 0:00 1:00 D Rule Cuba 1969 1977 - Apr lastSun 0:00 1:00 D Rule Cuba 1969 1971 - Oct lastSun 0:00 0 S Rule Cuba 1972 1974 - Oct 8 0:00 0 S Rule Cuba 1975 1977 - Oct lastSun 0:00 0 S Rule Cuba 1978 only - May 7 0:00 1:00 D Rule Cuba 1978 1990 - Oct Sun>=8 0:00 0 S Rule Cuba 1979 1980 - Mar Sun>=15 0:00 1:00 D Rule Cuba 1981 1985 - May Sun>=5 0:00 1:00 D Rule Cuba 1986 1989 - Mar Sun>=14 0:00 1:00 D Rule Cuba 1990 1997 - Apr Sun>=1 0:00 1:00 D Rule Cuba 1991 1995 - Oct Sun>=8 0:00s 0 S Rule Cuba 1996 only - Oct 6 0:00s 0 S Rule Cuba 1997 only - Oct 12 0:00s 0 S Rule Cuba 1998 1999 - Mar lastSun 0:00s 1:00 D Rule Cuba 1998 2003 - Oct lastSun 0:00s 0 S Rule Cuba 2000 2003 - Apr Sun>=1 0:00s 1:00 D Rule Cuba 2004 only - Mar lastSun 0:00s 1:00 D Rule Cuba 2006 2010 - Oct lastSun 0:00s 0 S Rule Cuba 2007 only - Mar Sun>=8 0:00s 1:00 D Rule Cuba 2008 only - Mar Sun>=15 0:00s 1:00 D Rule Cuba 2009 2010 - Mar Sun>=8 0:00s 1:00 D Rule Cuba 2011 only - Mar Sun>=15 0:00s 1:00 D Rule Cuba 2011 only - Nov 13 0:00s 0 S Rule Cuba 2012 only - Apr 1 0:00s 1:00 D Rule Cuba 2012 2025 - Nov Sun>=1 0:00s 0 S Rule Cuba 2013 2026 - Mar Sun>=8 0:00s 1:00 D Rule Cuba 2027 max - Nov Sun>=1 0:00s 0 S Rule Cuba 2028 max - Mar Sun>=8 0:00s 1:00 D # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Havana -5:29:28 - LMT 1890 -5:29:36 - HMT 1925 Jul 19 12:00 # Havana MT -5:00 Cuba C%sT # Dominican Republic # From Steffen Thorsen (2000-10-30): # Enrique Morales reported to me that the Dominican Republic has changed the # time zone to Eastern Standard Time as of Sunday 29 at 2 am.... # http://www.listin.com.do/antes/261000/republica/princi.html # From Paul Eggert (2000-12-04): # That URL (2000-10-26, in Spanish) says they planned to use US-style DST. # From Rives McDow (2000-12-01): # Dominican Republic changed its mind and presidential decree on Tuesday, # November 28, 2000, with a new decree. On Sunday, December 3 at 1:00 AM the # Dominican Republic will be reverting to 8 hours from the International Date # Line, and will not be using DST in the foreseeable future. The reason they # decided to use DST was to be in synch with Puerto Rico, who was also going # to implement DST. When Puerto Rico didn't implement DST, the president # decided to revert. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule DR 1966 only - Oct 30 0:00 1:00 EDT Rule DR 1967 only - Feb 28 0:00 0 EST Rule DR 1969 1973 - Oct lastSun 0:00 0:30 -0430 Rule DR 1970 only - Feb 21 0:00 0 EST Rule DR 1971 only - Jan 20 0:00 0 EST Rule DR 1972 1974 - Jan 21 0:00 0 EST # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Santo_Domingo -4:39:36 - LMT 1890 -4:40 - SDMT 1933 Apr 1 12:00 # S. Dom. MT -5:00 DR %s 1974 Oct 27 -4:00 - AST 2000 Oct 29 2:00 -5:00 US E%sT 2000 Dec 3 1:00 -4:00 - AST # El Salvador # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Salv 1987 1988 - May Sun>=1 0:00 1:00 D Rule Salv 1987 1988 - Sep lastSun 0:00 0 S # There are too many San Salvadors elsewhere, so use America/El_Salvador # instead of America/San_Salvador. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/El_Salvador -5:56:48 - LMT 1921 # San Salvador -6:00 Salv C%sT # Guatemala # # From Gwillim Law (2006-04-22), after a heads-up from Oscar van Vlijmen: # Diario Co Latino, at # , # says in an article dated 2006-04-19 that the Guatemalan government had # decided on that date to advance official time by 60 minutes, to lessen the # impact of the elevated cost of oil.... Daylight saving time will last from # 2006-04-29 24:00 (Guatemalan standard time) to 2006-09-30 (time unspecified). # From Paul Eggert (2006-06-22): # The Ministry of Energy and Mines, press release CP-15/2006 # (2006-04-19), says DST ends at 24:00. See # http://www.sieca.org.gt/Sitio_publico/Energeticos/Doc/Medidas/Cambio_Horario_Nac_190406.pdf # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Guat 1973 only - Nov 25 0:00 1:00 D Rule Guat 1974 only - Feb 24 0:00 0 S Rule Guat 1983 only - May 21 0:00 1:00 D Rule Guat 1983 only - Sep 22 0:00 0 S Rule Guat 1991 only - Mar 23 0:00 1:00 D Rule Guat 1991 only - Sep 7 0:00 0 S Rule Guat 2006 only - Apr 30 0:00 1:00 D Rule Guat 2006 only - Oct 1 0:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Guatemala -6:02:04 - LMT 1918 Oct 5 -6:00 Guat C%sT # Haiti # From Gwillim Law (2005-04-15): # Risto O. Nykänen wrote me that Haiti is now on DST. # I searched for confirmation, and I found a press release # on the Web page of the Haitian Consulate in Chicago (2005-03-31), # . Translated from French, it says: # # "The Prime Minister's Communication Office notifies the public in general # and the press in particular that, following a decision of the Interior # Ministry and the Territorial Collectivities [I suppose that means the # provinces], Haiti will move to Eastern Daylight Time in the night from next # Saturday the 2nd to Sunday the 3rd. # # "Consequently, the Prime Minister's Communication Office wishes to inform # the population that the country's clocks will be set forward one hour # starting at midnight. This provision will hold until the last Saturday in # October 2005. # # "Port-au-Prince, March 31, 2005" # # From Steffen Thorsen (2006-04-04): # I have been informed by users that Haiti observes DST this year like # last year, so the current "only" rule for 2005 might be changed to a # "max" rule or to last until 2006. (Who knows if they will observe DST # next year or if they will extend their DST like US/Canada next year). # # I have found this article about it (in French): # http://www.haitipressnetwork.com/news.cfm?articleID=7612 # # The reason seems to be an energy crisis. # From Stephen Colebourne (2007-02-22): # Some IATA info: Haiti won't be having DST in 2007. # From Steffen Thorsen (2012-03-11): # According to several news sources, Haiti will observe DST this year, # apparently using the same start and end date as USA/Canada. # So this means they have already changed their time. # # http://www.alterpresse.org/spip.php?article12510 # http://radiovision2000haiti.net/home/?p=13253 # # From Arthur David Olson (2012-03-11): # The alterpresse.org source seems to show a US-style leap from 2:00 a.m. to # 3:00 a.m. rather than the traditional Haitian jump at midnight. # Assume a US-style fall back as well. # From Steffen Thorsen (2013-03-10): # It appears that Haiti is observing DST this year as well, same rules # as US/Canada. They did it last year as well, and it looks like they # are going to observe DST every year now... # # http://radiovision2000haiti.net/public/haiti-avis-changement-dheure-dimanche/ # http://www.canalplushaiti.net/?p=6714 # From Steffen Thorsen (2016-03-12): # Jean Antoine, editor of www.haiti-reference.com informed us that Haiti # are not going on DST this year. Several other resources confirm this: ... # https://www.radiotelevisioncaraibes.com/presse/heure_d_t_pas_de_changement_d_heure_pr_vu_pour_cet_ann_e.html # https://www.vantbefinfo.com/changement-dheure-pas-pour-haiti/ # http://news.anmwe.com/haiti-lheure-nationale-ne-sera-ni-avancee-ni-reculee-cette-annee/ # From Steffen Thorsen (2017-03-12): # We have received 4 mails from different people telling that Haiti # has started DST again today, and this source seems to confirm that, # I have not been able to find a more authoritative source: # https://www.haitilibre.com/en/news-20319-haiti-notices-time-change-in-haiti.html # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Haiti 1983 only - May 8 0:00 1:00 D Rule Haiti 1984 1987 - Apr lastSun 0:00 1:00 D Rule Haiti 1983 1987 - Oct lastSun 0:00 0 S # Shanks & Pottenger say AT is 2:00, but IATA SSIM (1991/1997) says 1:00s. # Go with IATA. Rule Haiti 1988 1997 - Apr Sun>=1 1:00s 1:00 D Rule Haiti 1988 1997 - Oct lastSun 1:00s 0 S Rule Haiti 2005 2006 - Apr Sun>=1 0:00 1:00 D Rule Haiti 2005 2006 - Oct lastSun 0:00 0 S Rule Haiti 2012 2015 - Mar Sun>=8 2:00 1:00 D Rule Haiti 2012 2015 - Nov Sun>=1 2:00 0 S Rule Haiti 2017 max - Mar Sun>=8 2:00 1:00 D Rule Haiti 2017 max - Nov Sun>=1 2:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Port-au-Prince -4:49:20 - LMT 1890 -4:49 - PPMT 1917 Jan 24 12:00 # P-a-P MT -5:00 Haiti E%sT # Honduras # Shanks & Pottenger say 1921 Jan 1; go with Whitman's more precise Apr 1. # From Paul Eggert (2006-05-05): # worldtimezone.com reports a 2006-05-02 Spanish-language AP article # saying Honduras will start using DST midnight Saturday, effective 4 # months until September. La Tribuna reported today # that Manuel Zelaya, the president # of Honduras, refused to back down on this. # From Jesper Nørgaard Welen (2006-08-08): # It seems that Honduras has returned from DST to standard time this Monday at # 00:00 hours (prolonging Sunday to 25 hours duration). # http://www.worldtimezone.com/dst_news/dst_news_honduras04.html # From Paul Eggert (2006-08-08): # Also see Diario El Heraldo, The country returns to standard time (2006-08-08). # http://www.elheraldo.hn/nota.php?nid=54941&sec=12 # It mentions executive decree 18-2006. # From Steffen Thorsen (2006-08-17): # Honduras will observe DST from 2007 to 2009, exact dates are not # published, I have located this authoritative source: # http://www.presidencia.gob.hn/noticia.aspx?nId=47 # From Steffen Thorsen (2007-03-30): # http://www.laprensahn.com/pais_nota.php?id04962=7386 # So it seems that Honduras will not enter DST this year.... # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Hond 1987 1988 - May Sun>=1 0:00 1:00 D Rule Hond 1987 1988 - Sep lastSun 0:00 0 S Rule Hond 2006 only - May Sun>=1 0:00 1:00 D Rule Hond 2006 only - Aug Mon>=1 0:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Tegucigalpa -5:48:52 - LMT 1921 Apr -6:00 Hond C%sT # # Great Swan I ceded by US to Honduras in 1972 # Jamaica # Shanks & Pottenger give -5:07:12, but Milne records -5:07:10.41 from an # unspecified official document, and says "This time is used throughout the # island". Go with Milne. # # Shanks & Pottenger give April 28 for the 1974 spring-forward transition, but # Lance Neita writes that Prime Minister Michael Manley decreed it January 5. # Assume Neita meant Jan 6 02:00, the same as the US. Neita also writes that # Manley's supporters associated this act with Manley's nickname "Joshua" # (recall that in the Bible the sun stood still at Joshua's request), # and with the Rod of Correction which Manley said he had received from # Haile Selassie, Emperor of Ethiopia. See: # Neita L. The politician in all of us. Jamaica Observer 2014-09-20 # http://www.jamaicaobserver.com/columns/The-politician-in-all-of-us_17573647 # # Zone NAME STDOFF RULES FORMAT [UNTIL] #STDOFF -5:07:10.41 Zone America/Jamaica -5:07:10 - LMT 1890 # Kingston -5:07:10 - KMT 1912 Feb # Kingston Mean Time -5:00 - EST 1974 -5:00 US E%sT 1984 -5:00 - EST # Martinique # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Martinique -4:04:20 - LMT 1890 # Fort-de-France -4:04:20 - FFMT 1911 May 1 # Fort-de-France MT -4:00 - AST 1980 Apr 6 -4:00 1:00 ADT 1980 Sep 28 -4:00 - AST # Nicaragua # # This uses Shanks & Pottenger for times before 2005. # # From Steffen Thorsen (2005-04-12): # I've got reports from 8 different people that Nicaragua just started # DST on Sunday 2005-04-10, in order to save energy because of # expensive petroleum. The exact end date for DST is not yet # announced, only "September" but some sites also say "mid-September". # Some background information is available on the President's official site: # http://www.presidencia.gob.ni/Presidencia/Files_index/Secretaria/Notas%20de%20Prensa/Presidente/2005/ABRIL/Gobierno-de-nicaragua-adelanta-hora-oficial-06abril.htm # The Decree, no 23-2005 is available here: # http://www.presidencia.gob.ni/buscador_gaceta/BD/DECRETOS/2005/Decreto%2023-2005%20Se%20adelanta%20en%20una%20hora%20en%20todo%20el%20territorio%20nacional%20apartir%20de%20las%2024horas%20del%2009%20de%20Abril.pdf # # From Paul Eggert (2005-05-01): # The decree doesn't say anything about daylight saving, but for now let's # assume that it is daylight saving.... # # From Gwillim Law (2005-04-21): # The Associated Press story on the time change, which can be found at # http://www.lapalmainteractivo.com/guias/content/gen/ap/America_Latina/AMC_GEN_NICARAGUA_HORA.html # and elsewhere, says (fifth paragraph, translated from Spanish): "The last # time that a change of clocks was applied to save energy was in the year 2000 # during the Arnoldo Alemán administration."... # The northamerica file says that Nicaragua has been on UTC-6 continuously # since December 1998. I wasn't able to find any details of Nicaraguan time # changes in 2000. Perhaps a note could be added to the northamerica file, to # the effect that we have indirect evidence that DST was observed in 2000. # # From Jesper Nørgaard Welen (2005-11-02): # Nicaragua left DST the 2005-10-02 at 00:00 (local time). # http://www.presidencia.gob.ni/presidencia/files_index/secretaria/comunicados/2005/septiembre/26septiembre-cambio-hora.htm # (2005-09-26) # # From Jesper Nørgaard Welen (2006-05-05): # http://www.elnuevodiario.com.ni/2006/05/01/nacionales/18410 # (my informal translation) # By order of the president of the republic, Enrique Bolaños, Nicaragua # advanced by sixty minutes their official time, yesterday at 2 in the # morning, and will stay that way until 30th of September. # # From Jesper Nørgaard Welen (2006-09-30): # http://www.presidencia.gob.ni/buscador_gaceta/BD/DECRETOS/2006/D-063-2006P-PRN-Cambio-Hora.pdf # My informal translation runs: # The natural sun time is restored in all the national territory, in that the # time is returned one hour at 01:00 am of October 1 of 2006. # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Nic 1979 1980 - Mar Sun>=16 0:00 1:00 D Rule Nic 1979 1980 - Jun Mon>=23 0:00 0 S Rule Nic 2005 only - Apr 10 0:00 1:00 D Rule Nic 2005 only - Oct Sun>=1 0:00 0 S Rule Nic 2006 only - Apr 30 2:00 1:00 D Rule Nic 2006 only - Oct Sun>=1 1:00 0 S # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Managua -5:45:08 - LMT 1890 -5:45:12 - MMT 1934 Jun 23 # Managua Mean Time? -6:00 - CST 1973 May -5:00 - EST 1975 Feb 16 -6:00 Nic C%sT 1992 Jan 1 4:00 -5:00 - EST 1992 Sep 24 -6:00 - CST 1993 -5:00 - EST 1997 -6:00 Nic C%sT # Cayman Is # Panama # # Atikokan and Coral Harbour, Canada, match Panama since 1970. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Panama -5:18:08 - LMT 1890 -5:19:36 - CMT 1908 Apr 22 # Colón Mean Time -5:00 - EST # Anguilla # Antigua & Barbuda # Aruba # Caribbean Netherlands # Curaçao # Dominica # Grenada # Guadeloupe # Montserrat # Puerto Rico # St Barthélemy # St Kitts-Nevis # Sint Maarten / St Martin # St Lucia # St Vincent & the Grenadines # Trinidad & Tobago # Virgin Is (UK & US) # # There are too many San Juans elsewhere, so we'll use 'Puerto_Rico'. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Puerto_Rico -4:24:25 - LMT 1899 Mar 28 12:00 # San Juan -4:00 - AST 1942 May 3 -4:00 US A%sT 1946 -4:00 - AST # St Pierre and Miquelon # There are too many St Pierres elsewhere, so we'll use 'Miquelon'. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Miquelon -3:44:40 - LMT 1911 Jun 15 # St Pierre -4:00 - AST 1980 May -3:00 - %z 1987 -3:00 Canada %z # Turks and Caicos # # From Chris Dunn in # https://bugs.debian.org/415007 # (2007-03-15): In the Turks & Caicos Islands (America/Grand_Turk) the # daylight saving dates for time changes have been adjusted to match # the recent U.S. change of dates. # # From Brian Inglis (2007-04-28): # http://www.turksandcaicos.tc/calendar/index.htm [2007-04-26] # there is an entry for Nov 4 "Daylight Savings Time Ends 2007" and three # rows before that there is an out of date entry for Oct: # "Eastern Standard Times Begins 2007 # Clocks are set back one hour at 2:00 a.m. local Daylight Saving Time" # indicating that the normal ET rules are followed. # From Paul Eggert (2014-08-19): # The 2014-08-13 Cabinet meeting decided to stay on UT -04 year-round. See: # http://tcweeklynews.com/daylight-savings-time-to-be-maintained-p5353-127.htm # Model this as a switch from EST/EDT to AST ... # From Chris Walton (2014-11-04): # ... the TCI government appears to have delayed the switch to # "permanent daylight saving time" by one year.... # http://tcweeklynews.com/time-change-to-go-ahead-this-november-p5437-127.htm # # From the Turks & Caicos Cabinet (2017-07-20), heads-up from Steffen Thorsen: # ... agreed to the reintroduction in TCI of Daylight Saving Time (DST) # during the summer months and Standard Time, also known as Local # Time, during the winter months with effect from April 2018 ... # https://www.gov.uk/government/news/turks-and-caicos-post-cabinet-meeting-statement--3 # From Paul Eggert (2017-08-26): # The date of effect of the spring 2018 change appears to be March 11, # which makes more sense. See: Hamilton D. Time change back # by March 2018 for TCI. Magnetic Media. 2017-08-25. # http://magneticmediatv.com/2017/08/time-change-back-by-march-2018-for-tci/ # # From P Chan (2020-11-27): # Standard Time Declaration Order 2015 (L.N. 15/2015) # http://online.fliphtml5.com/fizd/czin/#p=2 # # Standard Time Declaration Order 2017 (L.N. 31/2017) # http://online.fliphtml5.com/fizd/dmcu/#p=2 # # From Tim Parenti (2020-12-05): # Although L.N. 31/2017 reads that it "shall come into operation at 2:00 a.m. # on 11th March 2018", a precise interpretation here poses some problems. The # order states that "the standard time to be observed throughout the Turks and # Caicos Islands shall be the same time zone as the Eastern United States of # America" and further clarifies "[f]or the avoidance of doubt" that it # "applies to the Eastern Standard Time as well as any changes thereto for # Daylight Saving Time." However, as clocks in Turks and Caicos approached # 02:00 -04, and thus the declared implementation time, it was still 01:00 EST # (-05), as DST in the Eastern US would not start until an hour later. # # Since it is unlikely that those on the islands switched their clocks twice in # the span of an hour, we assume instead that the adoption of EDT actually took # effect once clocks in the Eastern US had sprung forward, from 03:00 -04. # This discrepancy only affects the time zone abbreviation and DST flag for the # intervening hour, not wall clock times, as -04 was maintained throughout. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Grand_Turk -4:44:32 - LMT 1890 #STDOFF -5:07:10.41 -5:07:10 - KMT 1912 Feb # Kingston Mean Time -5:00 - EST 1979 -5:00 US E%sT 2015 Mar 8 2:00 -4:00 - AST 2018 Mar 11 3:00 -5:00 US E%sT # Local Variables: # coding: utf-8 # End: pr0m1th3as-datatypes-9450b29/inst/tzdata/southamerica000066400000000000000000002741471527370255000225700ustar00rootroot00000000000000# tzdb data for South America and environs # This file is in the public domain, so clarified as of # 2009-05-17 by Arthur David Olson. # This file is by no means authoritative; if you think you know better, # go ahead and edit the file, and please send any changes to # the public mailing list tz@iana.org for general use in the future. # For more, please see the file CONTRIBUTING in the tz distribution. # From Paul Eggert (2016-12-05): # # Unless otherwise specified, the source for data through 1990 is: # Thomas G. Shanks and Rique Pottenger, The International Atlas (6th edition), # San Diego: ACS Publications, Inc. (2003). # Unfortunately this book contains many errors and cites no sources. # # Many years ago Gwillim Law wrote that a good source # for time zone data was the International Air Transport # Association's Standard Schedules Information Manual (IATA SSIM), # published semiannually. Law sent in several helpful summaries # of the IATA's data after 1990. Except where otherwise noted, # IATA SSIM is the source for entries after 1990. # # For data circa 1899, a common source is: # Milne J. Civil time. Geogr J. 1899 Feb;13(2):173-94. # https://www.jstor.org/stable/1774359 # # These tables use numeric abbreviations like -03 and -0330 for # integer hour and minute UT offsets. Although earlier editions used # alphabetic time zone abbreviations, these abbreviations were # invented and did not reflect common practice. ############################################################################### ############################################################################### # Argentina # From Bob Devine (1988-01-28): # Argentina: first Sunday in October to first Sunday in April since 1976. # Double Summer time from 1969 to 1974. Switches at midnight. # From U. S. Naval Observatory (1988-01-19): # ARGENTINA 3 H BEHIND UTC # From Hernan G. Otero (1995-06-26): # I am sending modifications to the Argentine time zone table... # AR was chosen because they are the ISO letters that represent Argentina. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Arg 1930 only - Dec 1 0:00 1:00 - Rule Arg 1931 only - Apr 1 0:00 0 - Rule Arg 1931 only - Oct 15 0:00 1:00 - Rule Arg 1932 1940 - Mar 1 0:00 0 - Rule Arg 1932 1939 - Nov 1 0:00 1:00 - Rule Arg 1940 only - Jul 1 0:00 1:00 - Rule Arg 1941 only - Jun 15 0:00 0 - Rule Arg 1941 only - Oct 15 0:00 1:00 - Rule Arg 1943 only - Aug 1 0:00 0 - Rule Arg 1943 only - Oct 15 0:00 1:00 - Rule Arg 1946 only - Mar 1 0:00 0 - Rule Arg 1946 only - Oct 1 0:00 1:00 - Rule Arg 1963 only - Oct 1 0:00 0 - Rule Arg 1963 only - Dec 15 0:00 1:00 - Rule Arg 1964 1966 - Mar 1 0:00 0 - Rule Arg 1964 1966 - Oct 15 0:00 1:00 - Rule Arg 1967 only - Apr 2 0:00 0 - Rule Arg 1967 1968 - Oct Sun>=1 0:00 1:00 - Rule Arg 1968 1969 - Apr Sun>=1 0:00 0 - Rule Arg 1974 only - Jan 23 0:00 1:00 - Rule Arg 1974 only - May 1 0:00 0 - Rule Arg 1988 only - Dec 1 0:00 1:00 - # # From Hernan G. Otero (1995-06-26): # These corrections were contributed by InterSoft Argentina S.A., # obtaining the data from the: # Talleres de Hidrografía Naval Argentina # (Argentine Naval Hydrography Institute) Rule Arg 1989 1993 - Mar Sun>=1 0:00 0 - Rule Arg 1989 1992 - Oct Sun>=15 0:00 1:00 - # # From Hernan G. Otero (1995-06-26): # From this moment on, the law that mandated the daylight saving # time corrections was derogated and no more modifications # to the time zones (for daylight saving) are now made. # # From Rives McDow (2000-01-10): # On October 3, 1999, 0:00 local, Argentina implemented daylight savings time, # which did not result in the switch of a time zone, as they stayed 9 hours # from the International Date Line. Rule Arg 1999 only - Oct Sun>=1 0:00 1:00 - # From Paul Eggert (2007-12-28): # DST was set to expire on March 5, not March 3, but since it was converted # to standard time on March 3 it's more convenient for us to pretend that # it ended on March 3. Rule Arg 2000 only - Mar 3 0:00 0 - # # From Peter Gradelski via Steffen Thorsen (2000-03-01): # We just checked with our São Paulo office and they say the government of # Argentina decided not to become one of the countries that go on or off DST. # So Buenos Aires should be -3 hours from GMT at all times. # # From Fabián L. Arce Jofré (2000-04-04): # The law that claimed DST for Argentina was derogated by President Fernando # de la Rúa on March 2, 2000, because it would make people spend more energy # in the winter time, rather than less. The change took effect on March 3. # # From Mariano Absatz (2001-06-06): # one of the major newspapers here in Argentina said that the 1999 # Timezone Law (which never was effectively applied) will (would?) be # in effect.... The article is at # http://ar.clarin.com/diario/2001-06-06/e-01701.htm # ... The Law itself is "Ley No. 25155", sanctioned on 1999-08-25, enacted # 1999-09-17, and published 1999-09-21. The official publication is at: # http://www.boletin.jus.gov.ar/BON/Primera/1999/09-Septiembre/21/PDF/BO21-09-99LEG.PDF # Regretfully, you have to subscribe (and pay) for the on-line version.... # # (2001-06-12): # the timezone for Argentina will not change next Sunday. # Apparently it will do so on Sunday 24th.... # http://ar.clarin.com/diario/2001-06-12/s-03501.htm # # (2001-06-25): # Last Friday (yes, the last working day before the date of the change), the # Senate annulled the 1999 law that introduced the changes later postponed. # http://www.clarin.com.ar/diario/2001-06-22/s-03601.htm # It remains the vote of the Deputies..., but it will be the same.... # This kind of things had always been done this way in Argentina. # We are still -03:00 all year round in all of the country. # # From Steffen Thorsen (2007-12-21): # A user (Leonardo Chaim) reported that Argentina will adopt DST.... # all of the country (all Zone-entries) are affected. News reports like # http://www.lanacion.com.ar/opinion/nota.asp?nota_id=973037 indicate # that Argentina will use DST next year as well, from October to # March, although exact rules are not given. # # From Jesper Nørgaard Welen (2007-12-26) # The last hurdle of Argentina DST is over, the proposal was approved in # the lower chamber too (Diputados) with a vote 192 for and 2 against. # By the way thanks to Mariano Absatz and Daniel Mario Vega for the link to # the original scanned proposal, where the dates and the zero hours are # clear and unambiguous...This is the article about final approval: # http://www.lanacion.com.ar/politica/nota.asp?nota_id=973996 # # From Paul Eggert (2007-12-22): # For dates after mid-2008, the following rules are my guesses and # are quite possibly wrong, but are more likely than no DST at all. # From Alexander Krivenyshev (2008-09-05): # As per message from Carlos Alberto Fonseca Arauz (Nicaragua), # Argentina will start DST on Sunday October 19, 2008. # # http://www.worldtimezone.com/dst_news/dst_news_argentina03.html # http://www.impulsobaires.com.ar/nota.php?id=57832 (in spanish) # From Juan Manuel Docile in https://bugs.gentoo.org/240339 (2008-10-07) # via Rodrigo Severo: # Argentinian law No. 25.155 is no longer valid. # http://www.infoleg.gov.ar/infolegInternet/anexos/60000-64999/60036/norma.htm # The new one is law No. 26.350 # http://www.infoleg.gov.ar/infolegInternet/anexos/135000-139999/136191/norma.htm # So there is no summer time in Argentina for now. # From Mariano Absatz (2008-10-20): # Decree 1693/2008 applies Law 26.350 for the summer 2008/2009 establishing DST # in Argentina from 2008-10-19 until 2009-03-15. # http://www.boletinoficial.gov.ar/Bora.Portal/CustomControls/PdfContent.aspx?fp=16102008&pi=3&pf=4&s=0&sec=01 # # Decree 1705/2008 excepting 12 Provinces from applying DST in the summer # 2008/2009: Catamarca, La Rioja, Mendoza, Salta, San Juan, San Luis, La # Pampa, Neuquén, Rio Negro, Chubut, Santa Cruz and Tierra del Fuego # http://www.boletinoficial.gov.ar/Bora.Portal/CustomControls/PdfContent.aspx?fp=17102008&pi=1&pf=1&s=0&sec=01 # # Press release 235 dated Saturday October 18th, from the Government of the # Province of Jujuy saying it will not apply DST either (even when it was not # included in Decree 1705/2008). # http://www.jujuy.gov.ar/index2/partes_prensa/18_10_08/235-181008.doc # From fullinet (2009-10-18): # As announced in # http://www.argentina.gob.ar/argentina/portal/paginas.dhtml?pagina=356 # (an official .gob.ar) under title: "Sin Cambio de Hora" # (English: "No hour change"). # # "Por el momento, el Gobierno Nacional resolvió no modificar la hora # oficial, decisión que estaba en estudio para su implementación el # domingo 18 de octubre. Desde el Ministerio de Planificación se anunció # que la Argentina hoy, en estas condiciones meteorológicas, no necesita # la modificación del huso horario, ya que 2009 nos encuentra con # crecimiento en la producción y distribución energética." Rule Arg 2007 only - Dec 30 0:00 1:00 - Rule Arg 2008 2009 - Mar Sun>=15 0:00 0 - Rule Arg 2008 only - Oct Sun>=15 0:00 1:00 - # From Mariano Absatz (2004-05-21): # Today it was officially published that the Province of Mendoza is changing # its timezone this winter... starting tomorrow night.... # http://www.gobernac.mendoza.gov.ar/boletin/pdf/20040521-27158-normas.pdf # From Paul Eggert (2004-05-24): # It's Law No. 7,210. This change is due to a public power emergency, so for # now we'll assume it's for this year only. # # From Paul Eggert (2018-01-31): # Hora de verano para la República Argentina # http://buenasiembra.com.ar/esoterismo/astrologia/hora-de-verano-de-la-republica-argentina-27.html # says that standard time in Argentina from 1894-10-31 # to 1920-05-01 was -4:16:48.25. Go with this more precise value # over Shanks & Pottenger. It is upward compatible with Milne, who # says Córdoba time was -4:16:48.2. # # From Mariano Absatz (2004-06-05): # These media articles from a major newspaper mostly cover the current state: # http://www.lanacion.com.ar/04/05/27/de_604825.asp # http://www.lanacion.com.ar/04/05/28/de_605203.asp # # The following eight (8) provinces pulled clocks back to UTC-04:00 at # midnight Monday May 31st. (that is, the night between 05/31 and 06/01). # Apparently, all nine provinces would go back to UTC-03:00 at the same # time in October 17th. # # Catamarca, Chubut, La Rioja, San Juan, San Luis, Santa Cruz, # Tierra del Fuego, Tucumán. # # From Mariano Absatz (2004-06-14): # ... this weekend, the Province of Tucumán decided it'd go back to UTC-03:00 # yesterday midnight (that is, at 24:00 Saturday 12th), since the people's # annoyance with the change is much higher than the power savings obtained.... # # From Gwillim Law (2004-06-14): # http://www.lanacion.com.ar/04/06/10/de_609078.asp ... # "The time change in Tierra del Fuego was a conflicted decision from # the start. The government had decreed that the measure would take # effect on June 1, but a normative error forced the new time to begin # three days earlier, from a Saturday to a Sunday.... # Our understanding was that the change was originally scheduled to take place # on June 1 at 00:00 in Chubut, Santa Cruz, Tierra del Fuego (and some other # provinces). Sunday was May 30, only two days earlier. So the article # contains a contradiction. I would give more credence to the Saturday/Sunday # date than the "three days earlier" phrase, and conclude that Tierra del # Fuego set its clocks back at 2004-05-30 00:00. # # From Steffen Thorsen (2004-10-05): # The previous law 7210 which changed the province of Mendoza's time zone # back in May have been modified slightly in a new law 7277, which set the # new end date to 2004-09-26 (original date was 2004-10-17). # http://www.gobernac.mendoza.gov.ar/boletin/pdf/20040924-27244-normas.pdf # # From Mariano Absatz (2004-10-05): # San Juan changed from UTC-03:00 to UTC-04:00 at midnight between # Sunday, May 30th and Monday, May 31st. It changed back to UTC-03:00 # at midnight between Saturday, July 24th and Sunday, July 25th.... # http://www.sanjuan.gov.ar/prensa/archivo/000329.html # http://www.sanjuan.gov.ar/prensa/archivo/000426.html # http://www.sanjuan.gov.ar/prensa/archivo/000441.html # From Alex Krivenyshev (2008-01-17): # Here are articles that Argentina Province San Luis is planning to end DST # as earlier as upcoming Monday January 21, 2008 or February 2008: # # Provincia argentina retrasa reloj y marca diferencia con resto del país # (Argentine Province delayed clock and mark difference with the rest of the # country) # http://cl.invertia.com/noticias/noticia.aspx?idNoticia=200801171849_EFE_ET4373&idtel # # Es inminente que en San Luis atrasen una hora los relojes # (It is imminent in San Luis clocks one hour delay) # https://www.lagaceta.com.ar/nota/253414/Economia/Es-inminente-que-en-San-Luis-atrasen-una-hora-los-relojes.html # http://www.worldtimezone.com/dst_news/dst_news_argentina02.html # From Jesper Nørgaard Welen (2008-01-18): # The page of the San Luis provincial government # http://www.sanluis.gov.ar/notas.asp?idCanal=0&id=22812 # confirms what Alex Krivenyshev has earlier sent to the tz # emailing list about that San Luis plans to return to standard # time much earlier than the rest of the country. It also # confirms that upon request the provinces San Juan and Mendoza # refused to follow San Luis in this change. # # The change is supposed to take place Monday the 21st at 0:00 # hours. As far as I understand it if this goes ahead, we need # a new timezone for San Luis (although there are also documented # independent changes in the southamerica file of San Luis in # 1990 and 1991 which has not been confirmed). # From Jesper Nørgaard Welen (2008-01-25): # Unfortunately the below page has become defunct, about the San Luis # time change. Perhaps because it now is part of a group of pages "Most # important pages of 2008." # # You can use # http://www.sanluis.gov.ar/notas.asp?idCanal=8141&id=22834 # instead it seems. Or use "Buscador" from the main page of the San Luis # government, and fill in "huso" and click OK, and you will get 3 pages # from which the first one is identical to the above. # From Mariano Absatz (2008-01-28): # I can confirm that the Province of San Luis (and so far only that # province) decided to go back to UTC-3 effective midnight Jan 20th 2008 # (that is, Monday 21st at 0:00 is the time the clocks were delayed back # 1 hour), and they intend to keep UTC-3 as their timezone all year round # (that is, unless they change their mind any minute now). # # So we'll have to add yet another city to 'southamerica' (I think San # Luis city is the mos populated city in the Province, so it'd be # America/Argentina/San_Luis... of course I can't remember if San Luis's # history of particular changes goes along with Mendoza or San Juan :-( # (I only remember not being able to collect hard facts about San Luis # back in 2004, when these provinces changed to UTC-4 for a few days, I # mailed them personally and never got an answer). # From Paul Eggert (2014-08-12): # Unless otherwise specified, data entries are from Shanks & Pottenger through # 1992, from the IATA otherwise. As noted below, Shanks & Pottenger say that # America/Cordoba split into 6 subregions during 1991/1992, one of which # was America/San_Luis, but we haven't verified this yet so for now we'll # keep America/Cordoba a single region rather than splitting it into the # other 5 subregions. # From Mariano Absatz (2009-03-13): # Yesterday (with our usual 2-day notice) the Province of San Luis # decided that next Sunday instead of "staying" @utc-03:00 they will go # to utc-04:00 until the second Saturday in October... # # The press release is at # http://www.sanluis.gov.ar/SL/Paginas/NoticiaDetalle.asp?TemaId=1&InfoPrensaId=3102 # (I couldn't find the decree, but www.sanluis.gov.ar # is the official page for the Province Government.) # # There's also a note in only one of the major national papers ... # http://www.lanacion.com.ar/nota.asp?nota_id=1107912 # # The press release says [quick and dirty translation]: # ... announced that next Sunday, at 00:00, Puntanos (the San Luis # inhabitants) will have to turn back one hour their clocks # # Since then, San Luis will establish its own Province timezone. Thus, # during 2009, this timezone change will run from 00:00 the third Sunday # in March until 24:00 of the second Saturday in October. # From Mariano Absatz (2009-10-16): # ...the Province of San Luis is a case in itself. # # The Law at # http://www.diputadossanluis.gov.ar/diputadosasp/paginas/verNorma.asp?NormaID=276 # is ambiguous because establishes a calendar from the 2nd Sunday in # October at 0:00 thru the 2nd Saturday in March at 24:00 and the # complement of that starting on the 2nd Sunday of March at 0:00 and # ending on the 2nd Saturday of March at 24:00. # # This clearly breaks every time the 1st of March or October is a Sunday. # # IMHO, the "spirit of the Law" is to make the changes at 0:00 on the 2nd # Sunday of October and March. # # The problem is that the changes in the rest of the Provinces that did # change in 2007/2008, were made according to the Federal Law and Decrees # that did so on the 3rd Sunday of October and March. # # In fact, San Luis actually switched from UTC-4 to UTC-3 last Sunday # (October 11th) at 0:00. # # So I guess a new set of rules, besides "Arg", must be made and the last # America/Argentina/San_Luis entries should change to use these... # ... # From Alexander Krivenyshev (2010-04-09): # According to news reports from El Diario de la República Province San # Luis, Argentina (standard time UTC-04) will keep Daylight Saving Time # after April 11, 2010 - will continue to have same time as rest of # Argentina (UTC-3) (no DST). # # Confirmaron la prórroga del huso horario de verano (Spanish) # http://www.eldiariodelarepublica.com/index.php?option=com_content&task=view&id=29383&Itemid=9 # or (some English translation): # http://www.worldtimezone.com/dst_news/dst_news_argentina08.html # From Mariano Absatz (2010-04-12): # yes...I can confirm this...and given that San Luis keeps calling # UTC-03:00 "summer time", we should't just let San Luis go back to "Arg" # rules...San Luis is still using "Western ARgentina Time" and it got # stuck on Summer daylight savings time even though the summer is over. # From Paul Eggert (2018-01-23): # Perhaps San Luis operates on the legal fiction that it is at -04 # with perpetual daylight saving time, but ordinary usage typically seems to # just say it's at -03; see, for example, # https://es.wikipedia.org/wiki/Hora_oficial_argentina # We've documented similar situations as being plain changes to # standard time, so let's do that here too. This does not change UTC # offsets, only tm_isdst and the time zone abbreviations. One minor # plus is that this silences a zic complaint that there's no POSIX TZ # setting for timestamps past 2038. # Zone NAME STDOFF RULES FORMAT [UNTIL] # # Buenos Aires (BA), Capital Federal (CF), Zone America/Argentina/Buenos_Aires -3:53:48 - LMT 1894 Oct 31 #STDOFF -4:16:48.25 -4:16:48 - CMT 1920 May # Córdoba Mean Time -4:00 - %z 1930 Dec -4:00 Arg %z 1969 Oct 5 -3:00 Arg %z 1999 Oct 3 -4:00 Arg %z 2000 Mar 3 -3:00 Arg %z # # Córdoba (CB), Santa Fe (SF), Entre Ríos (ER), Corrientes (CN), Misiones (MN), # Chaco (CC), Formosa (FM), Santiago del Estero (SE) # # Shanks & Pottenger also make the following claims, which we haven't verified: # - Formosa switched to -3:00 on 1991-01-07. # - Misiones switched to -3:00 on 1990-12-29. # - Chaco switched to -3:00 on 1991-01-04. # - Santiago del Estero switched to -4:00 on 1991-04-01, # then to -3:00 on 1991-04-26. # #STDOFF -4:16:48.25 Zone America/Argentina/Cordoba -4:16:48 - LMT 1894 Oct 31 -4:16:48 - CMT 1920 May -4:00 - %z 1930 Dec -4:00 Arg %z 1969 Oct 5 -3:00 Arg %z 1991 Mar 3 -4:00 - %z 1991 Oct 20 -3:00 Arg %z 1999 Oct 3 -4:00 Arg %z 2000 Mar 3 -3:00 Arg %z # # Salta (SA), La Pampa (LP), Neuquén (NQ), Rio Negro (RN) Zone America/Argentina/Salta -4:21:40 - LMT 1894 Oct 31 #STDOFF -4:16:48.25 -4:16:48 - CMT 1920 May -4:00 - %z 1930 Dec -4:00 Arg %z 1969 Oct 5 -3:00 Arg %z 1991 Mar 3 -4:00 - %z 1991 Oct 20 -3:00 Arg %z 1999 Oct 3 -4:00 Arg %z 2000 Mar 3 -3:00 Arg %z 2008 Oct 18 -3:00 - %z # # Tucumán (TM) Zone America/Argentina/Tucuman -4:20:52 - LMT 1894 Oct 31 #STDOFF -4:16:48.25 -4:16:48 - CMT 1920 May -4:00 - %z 1930 Dec -4:00 Arg %z 1969 Oct 5 -3:00 Arg %z 1991 Mar 3 -4:00 - %z 1991 Oct 20 -3:00 Arg %z 1999 Oct 3 -4:00 Arg %z 2000 Mar 3 -3:00 - %z 2004 Jun 1 -4:00 - %z 2004 Jun 13 -3:00 Arg %z # # La Rioja (LR) Zone America/Argentina/La_Rioja -4:27:24 - LMT 1894 Oct 31 #STDOFF -4:16:48.25 -4:16:48 - CMT 1920 May -4:00 - %z 1930 Dec -4:00 Arg %z 1969 Oct 5 -3:00 Arg %z 1991 Mar 1 -4:00 - %z 1991 May 7 -3:00 Arg %z 1999 Oct 3 -4:00 Arg %z 2000 Mar 3 -3:00 - %z 2004 Jun 1 -4:00 - %z 2004 Jun 20 -3:00 Arg %z 2008 Oct 18 -3:00 - %z # # San Juan (SJ) Zone America/Argentina/San_Juan -4:34:04 - LMT 1894 Oct 31 #STDOFF -4:16:48.25 -4:16:48 - CMT 1920 May -4:00 - %z 1930 Dec -4:00 Arg %z 1969 Oct 5 -3:00 Arg %z 1991 Mar 1 -4:00 - %z 1991 May 7 -3:00 Arg %z 1999 Oct 3 -4:00 Arg %z 2000 Mar 3 -3:00 - %z 2004 May 31 -4:00 - %z 2004 Jul 25 -3:00 Arg %z 2008 Oct 18 -3:00 - %z # # Jujuy (JY) Zone America/Argentina/Jujuy -4:21:12 - LMT 1894 Oct 31 #STDOFF -4:16:48.25 -4:16:48 - CMT 1920 May -4:00 - %z 1930 Dec -4:00 Arg %z 1969 Oct 5 -3:00 Arg %z 1990 Mar 4 -4:00 - %z 1990 Oct 28 -4:00 1:00 %z 1991 Mar 17 -4:00 - %z 1991 Oct 6 -3:00 1:00 %z 1992 -3:00 Arg %z 1999 Oct 3 -4:00 Arg %z 2000 Mar 3 -3:00 Arg %z 2008 Oct 18 -3:00 - %z # # Catamarca (CT), Chubut (CH) Zone America/Argentina/Catamarca -4:23:08 - LMT 1894 Oct 31 #STDOFF -4:16:48.25 -4:16:48 - CMT 1920 May -4:00 - %z 1930 Dec -4:00 Arg %z 1969 Oct 5 -3:00 Arg %z 1991 Mar 3 -4:00 - %z 1991 Oct 20 -3:00 Arg %z 1999 Oct 3 -4:00 Arg %z 2000 Mar 3 -3:00 - %z 2004 Jun 1 -4:00 - %z 2004 Jun 20 -3:00 Arg %z 2008 Oct 18 -3:00 - %z # # Mendoza (MZ) Zone America/Argentina/Mendoza -4:35:16 - LMT 1894 Oct 31 #STDOFF -4:16:48.25 -4:16:48 - CMT 1920 May -4:00 - %z 1930 Dec -4:00 Arg %z 1969 Oct 5 -3:00 Arg %z 1990 Mar 4 -4:00 - %z 1990 Oct 15 -4:00 1:00 %z 1991 Mar 1 -4:00 - %z 1991 Oct 15 -4:00 1:00 %z 1992 Mar 1 -4:00 - %z 1992 Oct 18 -3:00 Arg %z 1999 Oct 3 -4:00 Arg %z 2000 Mar 3 -3:00 - %z 2004 May 23 -4:00 - %z 2004 Sep 26 -3:00 Arg %z 2008 Oct 18 -3:00 - %z # # San Luis (SL) Rule SanLuis 2008 2009 - Mar Sun>=8 0:00 0 - Rule SanLuis 2007 2008 - Oct Sun>=8 0:00 1:00 - Zone America/Argentina/San_Luis -4:25:24 - LMT 1894 Oct 31 #STDOFF -4:16:48.25 -4:16:48 - CMT 1920 May -4:00 - %z 1930 Dec -4:00 Arg %z 1969 Oct 5 -3:00 Arg %z 1990 -3:00 1:00 %z 1990 Mar 14 -4:00 - %z 1990 Oct 15 -4:00 1:00 %z 1991 Mar 1 -4:00 - %z 1991 Jun 1 -3:00 - %z 1999 Oct 3 -4:00 1:00 %z 2000 Mar 3 -3:00 - %z 2004 May 31 -4:00 - %z 2004 Jul 25 -3:00 Arg %z 2008 Jan 21 -4:00 SanLuis %z 2009 Oct 11 -3:00 - %z # # Santa Cruz (SC) Zone America/Argentina/Rio_Gallegos -4:36:52 - LMT 1894 Oct 31 #STDOFF -4:16:48.25 -4:16:48 - CMT 1920 May -4:00 - %z 1930 Dec -4:00 Arg %z 1969 Oct 5 -3:00 Arg %z 1999 Oct 3 -4:00 Arg %z 2000 Mar 3 -3:00 - %z 2004 Jun 1 -4:00 - %z 2004 Jun 20 -3:00 Arg %z 2008 Oct 18 -3:00 - %z # # Tierra del Fuego, Antártida e Islas del Atlántico Sur (TF) Zone America/Argentina/Ushuaia -4:33:12 - LMT 1894 Oct 31 #STDOFF -4:16:48.25 -4:16:48 - CMT 1920 May -4:00 - %z 1930 Dec -4:00 Arg %z 1969 Oct 5 -3:00 Arg %z 1999 Oct 3 -4:00 Arg %z 2000 Mar 3 -3:00 - %z 2004 May 30 -4:00 - %z 2004 Jun 20 -3:00 Arg %z 2008 Oct 18 -3:00 - %z # Bolivia # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/La_Paz -4:32:36 - LMT 1890 -4:32:36 - CMT 1931 Oct 15 # Calamarca MT -4:32:36 1:00 BST 1932 Mar 21 # Bolivia ST -4:00 - %z # Brazil # From Paul Eggert (1993-11-18): # The mayor of Rio recently attempted to change the time zone rules # just in his city, in order to leave more summer time for the tourist trade. # The rule change lasted only part of the day; # the federal government refused to follow the city's rules, and business # was in a chaos, so the mayor backed down that afternoon. # From IATA SSIM (1996-02): # _Only_ the following states in BR1 observe DST: Rio Grande do Sul (RS), # Santa Catarina (SC), Paraná (PR), São Paulo (SP), Rio de Janeiro (RJ), # Espírito Santo (ES), Minas Gerais (MG), Bahia (BA), Goiás (GO), # Distrito Federal (DF), Tocantins (TO), Sergipe [SE] and Alagoas [AL]. # [The last three states are new to this issue of the IATA SSIM.] # From Gwillim Law (1996-10-07): # Geography, history (Tocantins was part of Goiás until 1989), and other # sources of time zone information lead me to believe that AL, SE, and TO were # always in BR1, and so the only change was whether or not they observed DST.... # The earliest issue of the SSIM I have is 2/91. Each issue from then until # 9/95 says that DST is observed only in the ten states I quoted from 9/95, # along with Mato Grosso (MT) and Mato Grosso do Sul (MS), which are in BR2 # (UTC-4).... The other two time zones given for Brazil are BR3, which is # UTC-5, no DST, and applies only in the state of Acre (AC); and BR4, which is # UTC-2, and applies to Fernando de Noronha (formerly FN, but I believe it's # become part of the state of Pernambuco). The boundary between BR1 and BR2 # has never been clearly stated. They've simply been called East and West. # However, some conclusions can be drawn from another IATA manual: the Airline # Coding Directory, which lists close to 400 airports in Brazil. For each # airport it gives a time zone which is coded to the SSIM. From that # information, I'm led to conclude that the states of Amapá (AP), Ceará (CE), # Maranhão (MA), Paraíba (PR), Pernambuco (PE), Piauí (PI), and Rio Grande do # Norte (RN), and the eastern part of Pará (PA) are all in BR1 without DST. # From Marcos Tadeu (1998-09-27): # Brazilian official page # From Jesper Nørgaard (2000-11-03): # [For an official list of which regions in Brazil use which time zones, see:] # http://pcdsh01.on.br/Fusbr.htm # http://pcdsh01.on.br/Fusbrhv.htm # From Celso Doria via David Madeo (2002-10-09): # The reason for the delay this year has to do with elections in Brazil. # # Unlike in the United States, elections in Brazil are 100% computerized and # the results are known almost immediately. Yesterday, it was the first # round of the elections when 115 million Brazilians voted for President, # Governor, Senators, Federal Deputies, and State Deputies. Nobody is # counting (or re-counting) votes anymore and we know there will be a second # round for the Presidency and also for some Governors. The 2nd round will # take place on October 27th. # # The reason why the DST will only begin November 3rd is that the thousands # of electoral machines used cannot have their time changed, and since the # Constitution says the elections must begin at 8:00 AM and end at 5:00 PM, # the Government decided to postpone DST, instead of changing the Constitution # (maybe, for the next elections, it will be possible to change the clock)... # From Rodrigo Severo (2004-10-04): # It's just the biannual change made necessary by the much hyped, supposedly # modern Brazilian ... voting machines which, apparently, can't deal # with a time change between the first and the second rounds of the elections. # From Steffen Thorsen (2007-09-20): # Brazil will start DST on 2007-10-14 00:00 and end on 2008-02-17 00:00: # http://www.mme.gov.br/site/news/detail.do;jsessionid=BBA06811AFCAAC28F0285210913513DA?newsId=13975 # From Paul Schulze (2008-06-24): # ...by law number 11.662 of April 24, 2008 (published in the "Diario # Oficial da União"...) in Brazil there are changes in the timezones, # effective today (00:00am at June 24, 2008) as follows: # # a) The timezone UTC+5 is extinguished, with all the Acre state and the # part of the Amazonas state that had this timezone now being put to the # timezone UTC+4 # b) The whole Pará state now is put at timezone UTC+3, instead of just # part of it, as was before. # # This change follows a proposal of senator Tiao Viana of Acre state, that # proposed it due to concerns about open television channels displaying # programs inappropriate to youths in the states that had the timezone # UTC+5 too early in the night. In the occasion, some more corrections # were proposed, trying to unify the timezones of any given state. This # change modifies timezone rules defined in decree 2.784 of 18 June, # 1913. # From Rodrigo Severo (2008-06-24): # Just correcting the URL: # https://www.in.gov.br/imprensa/visualiza/index.jsp?jornal=do&secao=1&pagina=1&data=25/04/2008 # # As a result of the above Decree I believe the America/Rio_Branco # timezone shall be modified from UTC-5 to UTC-4 and a new timezone shall # be created to represent the...west side of the Pará State. I # suggest this new timezone be called Santarem as the most # important/populated city in the affected area. # # This new timezone would be the same as the Rio_Branco timezone up to # the 2008/06/24 change which would be to UTC-3 instead of UTC-4. # From Alex Krivenyshev (2008-06-24): # This is a quick reference page for New and Old Brazil Time Zones map. # http://www.worldtimezone.com/brazil-time-new-old.php # # - 4 time zones replaced by 3 time zones - eliminating time zone UTC-05 # (state Acre and the part of the Amazonas will be UTC/GMT-04) - western # part of Par state is moving to one timezone UTC-03 (from UTC-04). # From Paul Eggert (2002-10-10): # The official decrees referenced below are mostly taken from # Decretos sobre o Horário de Verão no Brasil. # http://pcdsh01.on.br/DecHV.html # From Steffen Thorsen (2008-08-29): # As announced by the government and many newspapers in Brazil late # yesterday, Brazil will start DST on 2008-10-19 (need to change rule) and # it will end on 2009-02-15 (current rule for Brazil is fine). Based on # past years experience with the elections, there was a good chance that # the start was postponed to November, but it did not happen this year. # # It has not yet been posted to http://pcdsh01.on.br/DecHV.html # # An official page about it: # http://www.mme.gov.br/site/news/detail.do?newsId=16722 # Note that this link does not always work directly, but must be accessed # by going to # http://www.mme.gov.br/first # # One example link that works directly: # http://jornale.com.br/index.php?option=com_content&task=view&id=13530&Itemid=54 # (Portuguese) # # We have a written a short article about it as well: # https://www.timeanddate.com/news/time/brazil-dst-2008-2009.html # # From Alexander Krivenyshev (2011-10-04): # State Bahia will return to Daylight savings time this year after 8 years off. # The announcement was made by Governor Jaques Wagner in an interview to a # television station in Salvador. # In Portuguese: # http://g1.globo.com/bahia/noticia/2011/10/governador-jaques-wagner-confirma-horario-de-verao-na-bahia.html # https://noticias.terra.com.br/brasil/noticias/0,,OI5390887-EI8139,00-Bahia+volta+a+ter+horario+de+verao+apos+oito+anos.html # From Guilherme Bernardes Rodrigues (2011-10-07): # There is news in the media, however there is still no decree about it. # I just send a e-mail to Zulmira Brandao at http://pcdsh01.on.br/ the # official agency about time in Brazil, and she confirmed that the old rule is # still in force. # From Guilherme Bernardes Rodrigues (2011-10-14) # It's official, the President signed a decree that includes Bahia in summer # time. # [ and in a second message (same day): ] # I found the decree. # # DECRETO No. 7.584, DE 13 DE OUTUBRO DE 2011 # Link : # http://www.in.gov.br/visualiza/index.jsp?data=13/10/2011&jornal=1000&pagina=6&totalArquivos=6 # From Kelley Cook (2012-10-16): # The governor of state of Bahia in Brazil announced on Thursday that # due to public pressure, he is reversing the DST policy they implemented # last year and will not be going to Summer Time on October 21st.... # http://www.correio24horas.com.br/r/artigo/apos-pressoes-wagner-suspende-horario-de-verao-na-bahia # From Rodrigo Severo (2012-10-16): # Tocantins state will have DST. # https://noticias.terra.com.br/brasil/noticias/0,,OI6232536-EI306.html # From Steffen Thorsen (2013-09-20): # Tocantins in Brazil is very likely not to observe DST from October.... # http://conexaoto.com.br/2013/09/18/ministerio-confirma-que-tocantins-esta-fora-do-horario-de-verao-em-2013-mas-falta-publicacao-de-decreto # We will keep this article updated when this is confirmed: # https://www.timeanddate.com/news/time/brazil-starts-dst-2013.html # From Steffen Thorsen (2013-10-17): # https://www.timeanddate.com/news/time/acre-amazonas-change-time-zone.html # Senator Jorge Viana announced that Acre will change time zone on November 10. # He did not specify the time of the change, nor if western parts of Amazonas # will change as well. # # From Paul Eggert (2013-10-17): # For now, assume western Amazonas will change as well. # From Daniel Dias Rodrigues (2026-10-20): # Under Brazilian law, a period stated as "until day X" includes day X. # From Tim Parenti (2026-10-27): # With the decrees explicitly naming the hour of 01:00 for only a few fall back # transitions, there remains some ambiguity in whether other implied midnight # fall back transitions may have therefore been intended as 24:00s (i.e., # 25:00 -> 24:00) rather than as 24:00 (24:00 -> 23:00), particularly prior to # 1966. Since contemporaneous reporting was somewhat inconsistent on this, # minimize changes from earlier tzdb versions where there is ambiguity. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S # Decree 20,466 (1931-10-01) # Decree 21,896 (1932-01-10) Rule Brazil 1931 only - Oct 3 11:00 1:00 - Rule Brazil 1932 1933 - Mar 31 24:00 0 - Rule Brazil 1932 only - Oct 3 0:00 1:00 - # Decree 23,195 (1933-10-10) # revoked DST. # Decree 27,496 (1949-11-24) # Decree 27,998 (1950-04-13) Rule Brazil 1949 1952 - Dec 1 0:00 1:00 - Rule Brazil 1950 only - Apr 16 1:00 0 - Rule Brazil 1951 1952 - Mar 31 24:00 0 - # Decree 32,308 (1953-02-24) Rule Brazil 1953 only - Feb 28 24:00 0 - # Decree 34,724 (1953-11-30) # revoked DST. # Decree 52,700 (1963-10-18) # established DST as an "Hora Especial" from 1963-10-23 00:00 in SP, RJ, GB, # MG, ES, due to the prolongation of the drought. It originally specified an # end date of 1964-02-29 24:00. # Decree 53,071 (1963-12-03) # extended DST as a usual "hora de verão" to all of the national territory from # 1963-12-09 and would have extended its end date to 1964-03-31 24:00. Rule Brazil 1963 only - Dec 9 0:00 1:00 - # Decree 53,604 (1964-02-25) # restored the original end date of 1964-03-01 00:00 due to the start of # school. Rule Brazil 1964 only - Mar 1 0:00 0 - # Decree 55,639 (1965-01-27) Rule Brazil 1965 only - Jan 31 0:00 1:00 - Rule Brazil 1965 only - Mar 31 24:00 0 - # Decree 57,303 (1965-11-22) Rule Brazil 1965 only - Nov 30 24:00 1:00 - # Decree 57,843 (1966-02-18) Rule Brazil 1966 only - Mar 1 1:00 0 - Rule Brazil 1966 1967 - Nov 1 0:00 1:00 - Rule Brazil 1967 1968 - Mar 1 0:00 0 - # Decree 63,429 (1968-10-15) # revoked DST. # Decree 91,698 (1985-09-27) Rule Brazil 1985 only - Nov 2 0:00 1:00 - # Decree 92,310 (1986-01-21) # Decree 92,463 (1986-03-13) Rule Brazil 1986 only - Mar 15 0:00 0 - # Decree 93,316 (1986-10-01) Rule Brazil 1986 only - Oct 25 0:00 1:00 - Rule Brazil 1987 only - Feb 14 0:00 0 - # Decree 94,922 (1987-09-22) Rule Brazil 1987 only - Oct 25 0:00 1:00 - Rule Brazil 1988 only - Feb 7 0:00 0 - # Decree 96,676 (1988-09-12) # except for the states of AC, AM, PA, RR, RO, and AP (then a territory) Rule Brazil 1988 only - Oct 16 0:00 1:00 - Rule Brazil 1989 only - Jan 29 0:00 0 - # Decree 98,077 (1989-08-21) # with the same exceptions Rule Brazil 1989 only - Oct 15 0:00 1:00 - Rule Brazil 1990 only - Feb 11 0:00 0 - # Decree 99,530 (1990-09-17) # adopted by RS, SC, PR, SP, RJ, ES, MG, GO, MS, DF. # Decree 99,629 (1990-10-19) adds BA, MT. Rule Brazil 1990 only - Oct 21 0:00 1:00 - Rule Brazil 1991 only - Feb 17 0:00 0 - # Unnumbered decree (1991-09-25) # adopted by RS, SC, PR, SP, RJ, ES, MG, BA, GO, MT, MS, DF. Rule Brazil 1991 only - Oct 20 0:00 1:00 - Rule Brazil 1992 only - Feb 9 0:00 0 - # Unnumbered decree (1992-10-16) # adopted by same states. Rule Brazil 1992 only - Oct 25 0:00 1:00 - Rule Brazil 1993 only - Jan 31 0:00 0 - # Decree 942 (1993-09-28) # adopted by same states, plus AM. # Decree 1,252 (1994-09-22; # web page corrected 2004-01-07) adopted by same states, minus AM. # Decree 1,636 (1995-09-14) # adopted by same states, plus MT and TO. # Decree 1,674 (1995-10-13) # adds AL, SE. Rule Brazil 1993 1995 - Oct Sun>=11 0:00 1:00 - Rule Brazil 1994 1995 - Feb Sun>=15 0:00 0 - Rule Brazil 1996 only - Feb 11 0:00 0 - # Decree 2,000 (1996-09-04) # adopted by same states, minus AL, SE. Rule Brazil 1996 only - Oct 6 0:00 1:00 - Rule Brazil 1997 only - Feb 16 0:00 0 - # From Daniel C. Sobral (1998-02-12): # In 1997, the DS began on October 6. The stated reason was that # because international television networks ignored Brazil's policy on DS, # they bought the wrong times on satellite for coverage of Pope's visit. # This year, the ending date of DS was postponed to March 1 # to help dealing with the shortages of electric power. # # Decree 2,317 (1997-09-04), adopted by same states. Rule Brazil 1997 only - Oct 6 0:00 1:00 - # Decree 2,495 # (1998-02-10) Rule Brazil 1998 only - Mar 1 0:00 0 - # Decree 2,780 (1998-09-11) # adopted by the same states as before. Rule Brazil 1998 only - Oct 11 0:00 1:00 - Rule Brazil 1999 only - Feb 21 0:00 0 - # Decree 3,150 # (1999-08-23) adopted by same states. # Decree 3,188 (1999-09-30) # adds SE, AL, PB, PE, RN, CE, PI, MA and RR. Rule Brazil 1999 only - Oct 3 0:00 1:00 - Rule Brazil 2000 only - Feb 27 0:00 0 - # Decree 3,592 (2000-09-06) # adopted by the same states as before. # Decree 3,630 (2000-10-13) # repeals DST in PE and RR, effective 2000-10-15 00:00. # Decree 3,632 (2000-10-17) # repeals DST in SE, AL, PB, RN, CE, PI and MA, effective 2000-10-22 00:00. # Decree 3,916 # (2001-09-13) reestablishes DST in AL, CE, MA, PB, PE, PI, RN, SE. Rule Brazil 2000 2001 - Oct Sun>=8 0:00 1:00 - Rule Brazil 2001 2006 - Feb Sun>=15 0:00 0 - # Decree 4,399 (2002-10-01) repeals DST in AL, CE, MA, PB, PE, PI, RN, SE. # 4,399 Rule Brazil 2002 only - Nov 3 0:00 1:00 - # Decree 4,844 (2003-09-24; corrected 2003-09-26) repeals DST in BA, MT, TO. # 4,844 Rule Brazil 2003 only - Oct 19 0:00 1:00 - # Decree 5,223 (2004-10-01) reestablishes DST in MT. # 5,223 Rule Brazil 2004 only - Nov 2 0:00 1:00 - # Decree 5,539 (2005-09-19), # adopted by the same states as before. Rule Brazil 2005 only - Oct 16 0:00 1:00 - # Decree 5,920 (2006-10-03), # adopted by the same states as before. Rule Brazil 2006 only - Nov 5 0:00 1:00 - Rule Brazil 2007 only - Feb 25 0:00 0 - # Decree 6,212 (2007-09-26), # adopted by the same states as before. Rule Brazil 2007 only - Oct Sun>=8 0:00 1:00 - # From Frederico A. C. Neves (2008-09-10): # According to this decree # http://www.planalto.gov.br/ccivil_03/_Ato2007-2010/2008/Decreto/D6558.htm # [t]he DST period in Brazil now on will be from the 3rd Oct Sunday to the # 3rd Feb Sunday. There is an exception on the return date when this is # the Carnival Sunday then the return date will be the next Sunday... Rule Brazil 2008 2017 - Oct Sun>=15 0:00 1:00 - Rule Brazil 2008 2011 - Feb Sun>=15 0:00 0 - # Decree 7,584 (2011-10-13) # added Bahia. Rule Brazil 2012 only - Feb Sun>=22 0:00 0 - # Decree 7,826 (2012-10-15) # removed Bahia and added Tocantins. # Decree 8,112 (2013-09-30) # removed Tocantins. Rule Brazil 2013 2014 - Feb Sun>=15 0:00 0 - Rule Brazil 2015 only - Feb Sun>=22 0:00 0 - Rule Brazil 2016 2019 - Feb Sun>=15 0:00 0 - # From Steffen Thorsen (2017-12-18): # According to many media sources, next year's DST start in Brazil will move to # the first Sunday of November # ... https://www.timeanddate.com/news/time/brazil-delays-dst-2018.html # From Steffen Thorsen (2017-12-20): # http://www.planalto.gov.br/ccivil_03/_ato2015-2018/2017/decreto/D9242.htm # From Fábio Gomes (2018-10-04): # The Brazilian president just announced a new change on this year DST. # It was scheduled to start on November 4th and it was changed to November 18th. # From Rodrigo Brüning Wessler (2018-10-15): # The Brazilian government just announced that the change in DST was # canceled.... Maybe the president Michel Temer also woke up one hour # earlier today. :) Rule Brazil 2018 only - Nov Sun>=1 0:00 1:00 - # The last ruleset listed above says that the following states observed DST: # DF, ES, GO, MG, MS, MT, PR, RJ, RS, SC, SP. # # From Steffen Thorsen (2019-04-05): # According to multiple sources the Brazilian president wants to get rid of DST. # https://gmconline.com.br/noticias/politica/bolsonaro-horario-de-verao-deve-acabar-este-ano # https://g1.globo.com/economia/noticia/2019/04/05/governo-anuncia-fim-do-horario-de-verao.ghtml # From Marcus Diniz (2019-04-25): # Brazil no longer has DST changes - decree signed today # https://g1.globo.com/politica/noticia/2019/04/25/bolsonaro-assina-decreto-que-acaba-com-o-horario-de-verao.ghtml # From Daniel Soares de Oliveira (2019-04-26): # http://www.planalto.gov.br/ccivil_03/_Ato2019-2022/2019/Decreto/D9772.htm # Zone NAME STDOFF RULES FORMAT [UNTIL] # # Fernando de Noronha (administratively part of PE) Zone America/Noronha -2:09:40 - LMT 1914 -2:00 Brazil %z 1990 Sep 17 -2:00 - %z 1999 Sep 30 -2:00 Brazil %z 2000 Oct 15 -2:00 - %z 2001 Sep 13 -2:00 Brazil %z 2002 Oct 1 -2:00 - %z # Other Atlantic islands have no permanent settlement. # These include Trindade and Martim Vaz (administratively part of ES), # Rocas Atoll (RN), and the St Peter and St Paul Archipelago (PE). # Fernando de Noronha was a separate territory from 1942-09-02 to 1989-01-01; # it also included the Penedos. # # Amapá (AP), east Pará (PA) # East Pará includes Belém, Marabá, Serra Norte, and São Félix do Xingu. # The division between east and west Pará is the river Xingu. # In the north a very small part from the river Javary (now Jari I guess, # the border with Amapá) to the Amazon, then to the Xingu. Zone America/Belem -3:13:56 - LMT 1914 -3:00 Brazil %z 1988 Sep 12 -3:00 - %z # # west Pará (PA) # West Pará includes Altamira, Óbidos, Prainha, Oriximiná, and Santarém. Zone America/Santarem -3:38:48 - LMT 1914 -4:00 Brazil %z 1988 Sep 12 -4:00 - %z 2008 Jun 24 0:00 -3:00 - %z # # Maranhão (MA), Piauí (PI), Ceará (CE), Rio Grande do Norte (RN), # Paraíba (PB) Zone America/Fortaleza -2:34:00 - LMT 1914 -3:00 Brazil %z 1990 Sep 17 -3:00 - %z 1999 Sep 30 -3:00 Brazil %z 2000 Oct 22 -3:00 - %z 2001 Sep 13 -3:00 Brazil %z 2002 Oct 1 -3:00 - %z # # Pernambuco (PE) (except Atlantic islands) Zone America/Recife -2:19:36 - LMT 1914 -3:00 Brazil %z 1990 Sep 17 -3:00 - %z 1999 Sep 30 -3:00 Brazil %z 2000 Oct 15 -3:00 - %z 2001 Sep 13 -3:00 Brazil %z 2002 Oct 1 -3:00 - %z # # Tocantins (TO) Zone America/Araguaina -3:12:48 - LMT 1914 -3:00 Brazil %z 1990 Sep 17 -3:00 - %z 1995 Sep 14 -3:00 Brazil %z 2003 Sep 24 -3:00 - %z 2012 Oct 21 -3:00 Brazil %z 2013 Sep -3:00 - %z # # Alagoas (AL), Sergipe (SE) Zone America/Maceio -2:22:52 - LMT 1914 -3:00 Brazil %z 1990 Sep 17 -3:00 - %z 1995 Oct 13 -3:00 Brazil %z 1996 Sep 4 -3:00 - %z 1999 Sep 30 -3:00 Brazil %z 2000 Oct 22 -3:00 - %z 2001 Sep 13 -3:00 Brazil %z 2002 Oct 1 -3:00 - %z # # Bahia (BA) # There are too many Salvadors elsewhere, so use America/Bahia instead # of America/Salvador. Zone America/Bahia -2:34:04 - LMT 1914 -3:00 Brazil %z 2003 Sep 24 -3:00 - %z 2011 Oct 16 -3:00 Brazil %z 2012 Oct 21 -3:00 - %z # # Goiás (GO), Distrito Federal (DF), Minas Gerais (MG), # Espírito Santo (ES), Rio de Janeiro (RJ), São Paulo (SP), Paraná (PR), # Santa Catarina (SC), Rio Grande do Sul (RS) # The 1963 date is for MG, ES, RJ, SP; others started DST on 1963-12-09. Zone America/Sao_Paulo -3:06:28 - LMT 1914 -3:00 Brazil %z 1963 Oct 23 0:00 -3:00 1:00 %z 1964 -3:00 Brazil %z # # Mato Grosso do Sul (MS) Zone America/Campo_Grande -3:38:28 - LMT 1914 -4:00 Brazil %z # # Mato Grosso (MT) Zone America/Cuiaba -3:44:20 - LMT 1914 -4:00 Brazil %z 2003 Sep 24 -4:00 - %z 2004 Oct 1 -4:00 Brazil %z # # Rondônia (RO) Zone America/Porto_Velho -4:15:36 - LMT 1914 -4:00 Brazil %z 1988 Sep 12 -4:00 - %z # # Roraima (RR) Zone America/Boa_Vista -4:02:40 - LMT 1914 -4:00 Brazil %z 1988 Sep 12 -4:00 - %z 1999 Sep 30 -4:00 Brazil %z 2000 Oct 15 -4:00 - %z # # east Amazonas (AM): Boca do Acre, Jutaí, Manaus, Floriano Peixoto # The great circle line from Tabatinga to Porto Acre divides # east from west Amazonas. Zone America/Manaus -4:00:04 - LMT 1914 -4:00 Brazil %z 1988 Sep 12 -4:00 - %z 1993 Sep 28 -4:00 Brazil %z 1994 Sep 22 -4:00 - %z # # west Amazonas (AM): Atalaia do Norte, Boca do Maoco, Benjamin Constant, # Eirunepé, Envira, Ipixuna Zone America/Eirunepe -4:39:28 - LMT 1914 -5:00 Brazil %z 1988 Sep 12 -5:00 - %z 1993 Sep 28 -5:00 Brazil %z 1994 Sep 22 -5:00 - %z 2008 Jun 24 0:00 -4:00 - %z 2013 Nov 10 -5:00 - %z # # Acre (AC) Zone America/Rio_Branco -4:31:12 - LMT 1914 -5:00 Brazil %z 1988 Sep 12 -5:00 - %z 2008 Jun 24 0:00 -4:00 - %z 2013 Nov 10 -5:00 - %z # Chile # From Paul Eggert (2022-03-15): # Shanks & Pottenger says America/Santiago introduced standard time in # 1890 and rounds its UT offset to 70W40; guess that in practice this # was the same offset as in 1916-1919. It also says Pacific/Easter # standardized on 109W22 in 1890; assume this didn't change the clocks. # # Dates for America/Santiago from 1910 to 2004 are primarily from # the following source, cited by Oscar van Vlijmen (2006-10-08): # [1] Chile Law # http://www.webexhibits.org/daylightsaving/chile.html # This contains a copy of this official table: # Cambios en la hora oficial de Chile desde 1900 (retrieved 2008-03-30) # https://web.archive.org/web/20080330200901/http://www.horaoficial.cl/cambio.htm # [1] needs several corrections, though. # # The first set of corrections is from: # [2] History of the Official Time of Chile # http://www.horaoficial.cl/ing/horaof_ing.html (retrieved 2012-03-06). See: # https://web.archive.org/web/20120306042032/http://www.horaoficial.cl/ing/horaof_ing.html # This is an English translation of: # Historia de la hora oficial de Chile (retrieved 2012-10-24). See: # https://web.archive.org/web/20121024234627/http://www.horaoficial.cl/horaof.htm # A fancier Spanish version (requiring mouse-clicking) is at: # http://www.horaoficial.cl/historia_hora.php # Conflicts between [1] and [2] were resolved as follows: # # - [1] says the 1910 transition was Jan 1, [2] says Jan 10 and cites # Boletín No. 1, Aviso No. 1 (1910). Go with [2]. # # - [1] says SMT was -4:42:45, [2] says Chile's official time from # 1916 to 1919 was -4:42:46.3, the meridian of Chile's National # Astronomical Observatory (OAN), then located in what is now # Quinta Normal in Santiago. Go with [1], as this matches the meridian # referred to by the relevant Chilean laws to this day. # # - [1] says the 1918 transition was Sep 1, [2] says Sep 10 and cites # Boletín No. 22, Aviso No. 129/1918 (1918-08-23). Go with [2]. # # - [1] does not give times for transitions; assume they occur # at midnight mainland time, the current common practice. However, # go with [2]'s specification of 23:00 for the 1947-05-21 transition. # # Another correction to [1] is from Jesper Nørgaard Welen, who # wrote (2006-10-08), "I think that there are some obvious mistakes in # the suggested link from Oscar van Vlijmen,... for instance entry 66 # says that GMT-4 ended 1990-09-12 while entry 67 only begins GMT-3 at # 1990-09-15 (they should have been 1990-09-15 and 1990-09-16 # respectively), but anyhow it clears up some doubts too." # # Data for Pacific/Easter from 1910 through 1967 come from Shanks & # Pottenger. After that, for lack of better info assume # Pacific/Easter is always two hours behind America/Santiago; # this is known to work for DST transitions starting in 2008 and # may well be true for earlier transitions. # From Tim Parenti (2022-07-06): # For a brief period of roughly six weeks in 1946, DST was only observed on an # emergency basis in specific regions of central Chile; namely, "the national # territory between the provinces of Coquimbo and Concepción, inclusive". # This was enacted by Decree 3,891, dated 1946-07-13, and took effect # 1946-07-14 24:00, advancing these central regions to -03. # https://www.diariooficial.interior.gob.cl/versiones-anteriores/do-h/19460715/#page/1 # The decree contemplated "[t]hat this advancement of the Official Time, even # though it has been proposed for the cities of Santiago and Valparaíso only, # must be agreed with that of other cities, due to the connection of various # activities that require it, such as, for example, the operation of rail # services". It was originally set to expire after 30 days but was extended # through 1946-08-31 by Decree 4,506, dated 1946-08-13. # https://www.diariooficial.interior.gob.cl/versiones-anteriores/do-h/19460814/#page/1 # # Law Number 8,522, promulgated 1946-08-27, reunified Chilean clocks at their # new "Summer Time" of -04, reckoned as that of "the meridian of the # Astronomical Observatory of Lo Espejo, advanced by 42 minutes and 45 # seconds". Although this law specified the new Summer Time to start on 1 # September each year, a special "transitional article" started it a few days # early, as soon as the law took effect. As the law was to take force "from # the date of its publication in the 'Diario Oficial', which happened the # following day, presume the change took place in Santiago and its environs # from 24:00 -03 to 23:00 -04 on Wednesday 1946-08-28. Although this was a # no-op for wall clocks in the north and south of the country, put their formal # start to DST an hour later when they reached 24:00 -04. # https://www.diariooficial.interior.gob.cl/versiones-anteriores/do-h/19460828/#page/1 # After a brief "Winter Time" stint at -05 beginning 1947-04-01, Law Number # 8,777, promulgated 1947-05-17, established year-round -04 "from 23:00 on the # second day after it is published in the 'Diario Oficial'." It was published # on Monday 1947-05-19 and so took effect from Wednesday 1947-05-21 23:00. # https://www.diariooficial.interior.gob.cl/versiones-anteriores/do-h/19470519/#page/1 # From Eduardo Krell (1995-10-19): # The law says to switch to DST at midnight [24:00] on the second SATURDAY # of October.... The law is the same for March and October. # (1998-09-29): # Because of the drought this year, the government decided to go into # DST earlier (saturday 9/26 at 24:00). This is a one-time change only ... # (unless there's another dry season next year, I guess). # From Julio I. Pacheco Troncoso (1999-03-18): # Because of the same drought, the government decided to end DST later, # on April 3, (one-time change). # From Germán Poo-Caamaño (2008-03-03): # Due to drought, Chile extends Daylight Time in three weeks. This # is one-time change (Saturday 3/29 at 24:00 for America/Santiago # and Saturday 3/29 at 22:00 for Pacific/Easter) # The Supreme Decree is located at # http://www.shoa.cl/servicios/supremo316.pdf # # From José Miguel Garrido (2008-03-05): # http://www.shoa.cl/noticias/2008/04hora/hora.htm # From Angel Chiang (2010-03-04): # Subject: DST in Chile exceptionally extended to 3 April due to earthquake # http://www.gobiernodechile.cl/viewNoticia.aspx?idArticulo=30098 # # From Arthur David Olson (2010-03-06): # Angel Chiang's message confirmed by Julio Pacheco; Julio provided a patch. # From Glenn Eychaner (2011-03-28): # http://diario.elmercurio.com/2011/03/28/_portada/_portada/noticias/7565897A-CA86-49E6-9E03-660B21A4883E.htm?id=3D{7565897A-CA86-49E6-9E03-660B21A4883E} # In English: # Chile's clocks will go back an hour this year on the 7th of May instead # of this Saturday. They will go forward again the 3rd Saturday in # August, not in October as they have since 1968. # From Mauricio Parada (2012-02-22), translated by Glenn Eychaner (2012-02-23): # As stated in the website of the Chilean Energy Ministry # http://www.minenergia.cl/ministerio/noticias/generales/gobierno-anuncia-fechas-de-cambio-de.html # The Chilean Government has decided to postpone the entrance into winter time # (to leave DST) from March 11 2012 to April 28th 2012.... # Quote from the website communication: # # 6. For the year 2012, the dates of entry into winter time will be as follows: # a. Saturday April 28, 2012, clocks should go back 60 minutes; that is, at # 23:59:59, instead of passing to 0:00, the time should be adjusted to be 23:00 # of the same day. # b. Saturday, September 1, 2012, clocks should go forward 60 minutes; that is, # at 23:59:59, instead of passing to 0:00, the time should be adjusted to be # 01:00 on September 2. # From Steffen Thorsen (2013-02-15): # According to several news sources, Chile has extended DST this year, # they will end DST later and start DST earlier than planned. They # hope to save energy. The new end date is 2013-04-28 00:00 and new # start date is 2013-09-08 00:00.... # http://www.gob.cl/informa/2013/02/15/gobierno-anuncia-fechas-de-cambio-de-hora-para-el-ano-2013.htm # From José Miguel Garrido (2014-02-19): # Today appeared in the Diario Oficial a decree amending the time change # dates to 2014. # DST End: last Saturday of April 2014 (Sun 27 Apr 2014 03:00 UTC) # DST Start: first Saturday of September 2014 (Sun 07 Sep 2014 04:00 UTC) # From Tim Parenti (2025-03-22): # Decreto 307 of 2014 of the Ministry of the Interior and Public Security, # promulgated 2014-01-30 and published 2014-02-19: # https://www.diariooficial.interior.gob.cl/media/2014/02/19/do-20140219.pdf#page=1 # https://www.bcn.cl/leychile/navegar?idNorma=1059557 # From Eduardo Romero Urra (2015-03-03): # Today has been published officially that Chile will use the DST time # permanently until March 25 of 2017 # From Tim Parenti (2025-03-22): # Decreto 106 of 2015 of the Ministry of the Interior and Public Security, # promulgated 2015-01-27 and published 2015-03-03: # https://www.diariooficial.interior.gob.cl/media/2015/03/03/do-20150303.pdf#page=1 # https://www.bcn.cl/leychile/navegar?idNorma=1075157 # From Juan Correa (2016-03-18): # The decree regarding DST has been published in today's Official Gazette... # It does consider the second Saturday of May and August as the dates # for the transition; and it lists DST dates until 2019, but I think # this scheme will stick. # From Paul Eggert (2016-03-18): # The decree says transitions occur at 24:00; in practice this appears # to mean 24:00 mainland time, not 24:00 local time, so that Easter # Island is always two hours behind the mainland. # From Tim Parenti (2025-03-22): # Decreto 253 of 2016 of the Ministry of the Interior and Public Security, # promulgated 2016-03-16 and published 2016-03-18. # https://www.diariooficial.interior.gob.cl/media/2016/03/18/do-20160318.pdf#page=1 # https://www.bcn.cl/leychile/navegar?idNorma=1088502 # From Juan Correa (2016-12-04): # Magallanes region ... will keep DST (UTC -3) all year round.... # http://www.soychile.cl/Santiago/Sociedad/2016/12/04/433428/Bachelet-firmo-el-decreto-para-establecer-un-horario-unico-para-la-Region-de-Magallanes.aspx # From Tim Parenti (2025-03-22), via Deborah Goldsmith (2017-01-19): # Decreto 1820 of 2016 of the Ministry of the Interior and Public Security, # promulgated 2016-12-02 and published 2017-01-17: # https://www.diariooficial.interior.gob.cl/publicaciones/2017/01/17/41660/01/1169626.pdf # https://www.bcn.cl/leychile/Navegar?idNorma=1099217 # Model this as a change to standard offset effective 2016-12-04. # From Juan Correa (2018-08-13): # As of moments ago, the Ministry of Energy in Chile has announced the new # schema for DST. ... Announcement in video (in Spanish): # https://twitter.com/MinEnergia/status/1029000399129374720 # From Yonathan Dossow (2018-08-13): # The video says "first Saturday of September", we all know it means Sunday at # midnight. # From Tim Parenti (2018-08-13): # Translating the captions on the video at 0:44-0:55, "We want to announce as # Government that from 2019, Winter Time will be increased to 5 months, between # the first Saturday of April and the first Saturday of September." # At 2:08-2:20, "The Magallanes region will maintain its current time, as # decided by the citizens during 2017, but our Government will promote a # regional dialogue table to gather their opinion on this matter." # https://twitter.com/MinEnergia/status/1029009354001973248 # "We will keep the new time policy unchanged for at least the next 4 years." # So we extend the new rules on Saturdays at 24:00 mainland time indefinitely. # From Tim Parenti (2025-03-22), via Juan Correa (2019-02-04): # Decreto 1286 of 2018 of the Ministry of the Interior and Public Security, # promulgated 2018-09-21 and published 2018-11-23: # https://www.diariooficial.interior.gob.cl/publicaciones/2018/11/23/42212/01/1498738.pdf # https://www.bcn.cl/leychile/Navegar?idNorma=1125760 # From Juan Correa (2022-04-02): # I found there was a decree published last Thursday that will keep # Magallanes region to UTC -3 "indefinitely". # From Tim Parenti (2025-03-22): # Decreto 143 of 2022 of the Ministry of the Interior and Public Security, # promulgated 2022-03-29 and published 2022-03-31: # https://www.diariooficial.interior.gob.cl/publicaciones/2022/03/31/43217-B/01/2108910.pdf # https://www.bcn.cl/leychile/Navegar?idNorma=1174342 # From Juan Correa (2022-08-09): # the Internal Affairs Ministry (Ministerio del Interior) informed DST # for America/Santiago will start on midnight of September 11th; # and will end on April 1st, 2023. Magallanes region (America/Punta_Arenas) # will keep UTC -3 "indefinitely"... This is because on September 4th # we will have a voting whether to approve a new Constitution. # # From Tim Parenti (2025-03-22), via Eduardo Romero Urra (2022-08-17): # Decreto 224 of 2022 of the Ministry of the Interior and Public Security, # promulgated 2022-07-14 and published 2022-08-13: # https://www.diariooficial.interior.gob.cl/publicaciones/2022/08/13/43327/01/2172567.pdf # https://www.bcn.cl/leychile/navegar?idNorma=1179983 # From Yonathan Dossow (2025-03-20): # [Aysén] region will keep UTC-3 all year... # https://www.cnnchile.com/pais/region-de-aysen-mantendra-horario-de-verano-todo-el-ano_20250320/ # https://www.latercera.com/nacional/noticia/tras-consulta-ciudadana-region-de-aysen-mantendra-el-horario-de-verano-durante-todo-el-ano/ # https://x.com/min_interior/status/1902692504270672098 # # From Tim Parenti (2025-03-22), via Eduardo Romero Urra (2025-03-20): # Decreto 93 of 2025 of the Ministry of the Interior and Public Security, # promulgated 2025-03-11 and published 2025-03-20: # https://www.diariooficial.interior.gob.cl/publicaciones/2025/03/20/44104/01/2624263.pdf # https://www.bcn.cl/leychile/Navegar?idNorma=1211955 # Model this as a change to standard offset effective 2025-03-20. # From Paul Eggert (2026-08-11): # Chile’s timekeeping regime is renewed by Decreto 98 of 2026 of the # Ministry of the Interior, Undersecretariat of the Interior, # promulgated 2026-06-30 and published 2026-07-02: # https://www.bcn.cl/leychile/navegar?idNorma=1225724 # Although this decree stops at fall 2029, assume that # similar DST rules will continue thereafter. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Chile 1927 1931 - Sep 1 0:00 1:00 - Rule Chile 1928 1932 - Apr 1 0:00 0 - Rule Chile 1968 only - Nov 3 4:00u 1:00 - Rule Chile 1969 only - Mar 30 3:00u 0 - Rule Chile 1969 only - Nov 23 4:00u 1:00 - Rule Chile 1970 only - Mar 29 3:00u 0 - Rule Chile 1971 only - Mar 14 3:00u 0 - Rule Chile 1970 1972 - Oct Sun>=9 4:00u 1:00 - Rule Chile 1972 1986 - Mar Sun>=9 3:00u 0 - Rule Chile 1973 only - Sep 30 4:00u 1:00 - Rule Chile 1974 1987 - Oct Sun>=9 4:00u 1:00 - Rule Chile 1987 only - Apr 12 3:00u 0 - Rule Chile 1988 1990 - Mar Sun>=9 3:00u 0 - Rule Chile 1988 1989 - Oct Sun>=9 4:00u 1:00 - Rule Chile 1990 only - Sep 16 4:00u 1:00 - Rule Chile 1991 1996 - Mar Sun>=9 3:00u 0 - Rule Chile 1991 1997 - Oct Sun>=9 4:00u 1:00 - Rule Chile 1997 only - Mar 30 3:00u 0 - Rule Chile 1998 only - Mar Sun>=9 3:00u 0 - Rule Chile 1998 only - Sep 27 4:00u 1:00 - Rule Chile 1999 only - Apr 4 3:00u 0 - Rule Chile 1999 2010 - Oct Sun>=9 4:00u 1:00 - Rule Chile 2000 2007 - Mar Sun>=9 3:00u 0 - # N.B.: the end of March 29 in Chile is March 30 in Universal time, # which is used below in specifying the transition. Rule Chile 2008 only - Mar 30 3:00u 0 - Rule Chile 2009 only - Mar Sun>=9 3:00u 0 - Rule Chile 2010 only - Apr Sun>=1 3:00u 0 - Rule Chile 2011 only - May Sun>=2 3:00u 0 - Rule Chile 2011 only - Aug Sun>=16 4:00u 1:00 - Rule Chile 2012 2014 - Apr Sun>=23 3:00u 0 - Rule Chile 2012 2014 - Sep Sun>=2 4:00u 1:00 - Rule Chile 2016 2018 - May Sun>=9 3:00u 0 - Rule Chile 2016 2018 - Aug Sun>=9 4:00u 1:00 - Rule Chile 2019 max - Apr Sun>=2 3:00u 0 - Rule Chile 2019 2021 - Sep Sun>=2 4:00u 1:00 - Rule Chile 2022 only - Sep Sun>=9 4:00u 1:00 - Rule Chile 2023 max - Sep Sun>=2 4:00u 1:00 - # IATA SSIM anomalies: (1992-02) says 1992-03-14; # (1996-09) says 1998-03-08. Ignore these. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Santiago -4:42:45 - LMT 1890 -4:42:45 - SMT 1910 Jan 10 # Santiago Mean Time -5:00 - %z 1916 Jul 1 -4:42:45 - SMT 1918 Sep 10 -4:00 - %z 1919 Jul 1 -4:42:45 - SMT 1927 Sep 1 -5:00 Chile %z 1932 Sep 1 -4:00 - %z 1942 Jun 1 -5:00 - %z 1942 Aug 1 -4:00 - %z 1946 Jul 14 24:00 -4:00 1:00 %z 1946 Aug 28 24:00 # central CL -5:00 1:00 %z 1947 Mar 31 24:00 -5:00 - %z 1947 May 21 23:00 -4:00 Chile %z Zone America/Coyhaique -4:48:16 - LMT 1890 -4:42:45 - SMT 1910 Jan 10 -5:00 - %z 1916 Jul 1 -4:42:45 - SMT 1918 Sep 10 -4:00 - %z 1919 Jul 1 -4:42:45 - SMT 1927 Sep 1 -5:00 Chile %z 1932 Sep 1 -4:00 - %z 1942 Jun 1 -5:00 - %z 1942 Aug 1 -4:00 - %z 1946 Aug 28 24:00 -5:00 1:00 %z 1947 Mar 31 24:00 -5:00 - %z 1947 May 21 23:00 -4:00 Chile %z 2025 Mar 20 -3:00 - %z Zone America/Punta_Arenas -4:43:40 - LMT 1890 -4:42:45 - SMT 1910 Jan 10 -5:00 - %z 1916 Jul 1 -4:42:45 - SMT 1918 Sep 10 -4:00 - %z 1919 Jul 1 -4:42:45 - SMT 1927 Sep 1 -5:00 Chile %z 1932 Sep 1 -4:00 - %z 1942 Jun 1 -5:00 - %z 1942 Aug 1 -4:00 - %z 1946 Aug 28 24:00 -5:00 1:00 %z 1947 Mar 31 24:00 -5:00 - %z 1947 May 21 23:00 -4:00 Chile %z 2016 Dec 4 -3:00 - %z Zone Pacific/Easter -7:17:28 - LMT 1890 -7:17:28 - EMT 1932 Sep # Easter Mean Time -7:00 Chile %z 1982 Mar 14 3:00u # Easter Time -6:00 Chile %z # # Salas y Gómez Island is uninhabited. # Other Chilean locations, including Juan Fernández Is, Desventuradas Is, # and Antarctic bases, are like America/Santiago. # Antarctic base using South American rules # (See the file 'antarctica' for more.) # # Palmer, Anvers Island, since 1965 (moved 2 miles in 1968) # # From Ethan Dicks (1996-10-06): # It keeps the same time as Punta Arenas, Chile, because, just like us # and the South Pole, that's the other end of their supply line.... # I verified with someone who was there that since 1980, # Palmer has followed Chile. Prior to that, before the Falklands War, # Palmer used to be supplied from Argentina. # # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Antarctica/Palmer 0 - -00 1965 -4:00 Arg %z 1969 Oct 5 -3:00 Arg %z 1982 May -4:00 Chile %z 2016 Dec 4 -3:00 - %z # Colombia # Milne gives 4:56:16.4 for Bogotá time in 1899. He writes, # "A variation of fifteen minutes in the public clocks of Bogota is not rare." # From Paul Eggert (2026-07-17): # The 1992 spring forward was May 2 at 00:00. See: # Jiménez FA. La noche en que Colombia perdió una hora para encarar el apagón, # ¿se repetirá la historia en 2026? El Colombiano. 2026-06-21. # https://www.elcolombiano.com/negocios/colombia-perdio-una-hora-para-evitar-apagon-horario-gaviria-LF37983109 # From Alois Treindl (2022-11-10): # End of time change in Colombia 1993 ... should be 6 February 24h ... # DECRETO 267 DE 1993 # https://www.suin-juriscol.gov.co/viewDocument.asp?ruta=Decretos/1061335 # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule CO 1992 only - May 2 0:00 1:00 - Rule CO 1993 only - Feb 6 24:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] #STDOFF -4:56:16.4 Zone America/Bogota -4:56:16 - LMT 1884 Mar 13 -4:56:16 - BMT 1914 Nov 23 # Bogotá Mean Time -5:00 CO %z # Malpelo, Providencia, San Andres # no information; probably like America/Bogota # Ecuador # # Milne says the Central and South American Telegraph Company used -5:24:15. # # From Alois Treindl (2016-12-15): # https://www.elcomercio.com/actualidad/hora-sixto-1993.html # ... Whether the law applied also to Galápagos, I do not know. # From Paul Eggert (2016-12-15): # https://www.elcomercio.com/afull/modificacion-husohorario-ecuador-presidentes-decreto.html # This says President Sixto Durán Ballén signed decree No. 285, which # established DST from 1992-11-28 to 1993-02-05; it does not give transition # times. The people called it "hora de Sixto" ("Sixto hour"). The change did # not go over well; a popular song "Qué hora es" by Jaime Guevara had lyrics # that included "Amanecía en mitad de la noche, los guaguas iban a clase sin # sol" ("It was dawning in the middle of the night, the buses went to class # without sun"). Although Ballén's campaign slogan was "Ni un paso atrás" # (Not one step back), the clocks went back in 1993 and the experiment was not # repeated. For now, assume transitions were at 00:00 local time country-wide. # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Ecuador 1992 only - Nov 28 0:00 1:00 - Rule Ecuador 1993 only - Feb 5 0:00 0 - # # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Guayaquil -5:19:20 - LMT 1890 -5:14:00 - QMT 1931 # Quito Mean Time -5:00 Ecuador %z Zone Pacific/Galapagos -5:58:24 - LMT 1931 # Puerto Baquerizo Moreno -5:00 - %z 1986 -6:00 Ecuador %z # Falklands # From Paul Eggert (2006-03-22): # Between 1990 and 2000 inclusive, Shanks & Pottenger and the IATA agree except # the IATA gives 1996-09-08. Go with Shanks & Pottenger. # From Falkland Islands Government Office, London (2001-01-22) # via Jesper Nørgaard: # ... the clocks revert back to Local Mean Time at 2 am on Sunday 15 # April 2001 and advance one hour to summer time at 2 am on Sunday 2 # September. It is anticipated that the clocks will revert back at 2 # am on Sunday 21 April 2002 and advance to summer time at 2 am on # Sunday 1 September. # From Rives McDow (2001-02-13): # # I have communicated several times with people there, and the last # time I had communications that was helpful was in 1998. Here is # what was said then: # # "The general rule was that Stanley used daylight saving and the Camp # did not. However for various reasons many people in the Camp have # started to use daylight saving (known locally as 'Stanley Time') # There is no rule as to who uses daylight saving - it is a matter of # personal choice and so it is impossible to draw a map showing who # uses it and who does not. Any list would be out of date as soon as # it was produced. This year daylight saving ended on April 18/19th # and started again on September 12/13th. I do not know what the rule # is, but can find out if you like. We do not change at the same time # as UK or Chile." # # I did have in my notes that the rule was "Second Saturday in Sep at # 0:00 until third Saturday in Apr at 0:00". I think that this does # not agree in some cases with Shanks; is this true? # # Also, there is no mention in the list that some areas in the # Falklands do not use DST. I have found in my communications there # that these areas are on the western half of East Falkland and all of # West Falkland. Stanley is the only place that consistently observes # DST. Again, as in other places in the world, the farmers don't like # it. West Falkland is almost entirely sheep farmers. # # I know one lady there that keeps a list of which farm keeps DST and # which doesn't each year. She runs a shop in Stanley, and says that # the list changes each year. She uses it to communicate to her # customers, catching them when they are home for lunch or dinner. # From Paul Eggert (2001-03-05): # For now, we'll just record the time in Stanley, since we have no # better info. # From Steffen Thorsen (2011-04-01): # The Falkland Islands will not turn back clocks this winter, but stay on # daylight saving time. # # One source: # http://www.falklandnews.com/public/story.cfm?get=5914&source=3 # # We have gotten this confirmed by a clerk of the legislative assembly: # Normally the clocks revert to Local Mean Time (UTC/GMT -4 hours) on the # third Sunday of April at 0200hrs and advance to Summer Time (UTC/GMT -3 # hours) on the first Sunday of September at 0200hrs. # # IMPORTANT NOTE: During 2011, on a trial basis, the Falkland Islands # will not revert to local mean time, but clocks will remain on Summer # time (UTC/GMT - 3 hours) throughout the whole of 2011. Any long term # change to local time following the trial period will be notified. # # From Andrew Newman (2012-02-24) # A letter from Justin McPhee, Chief Executive, # Cable & Wireless Falkland Islands (dated 2012-02-22) # states... # The current Atlantic/Stanley entry under South America expects the # clocks to go back to standard Falklands Time (FKT) on the 15th April. # The database entry states that in 2011 Stanley was staying on fixed # summer time on a trial basis only. FIG need to contact IANA and/or # the maintainers of the database to inform them we're adopting # the same policy this year and suggest recommendations for future years. # # For now we will assume permanent -03 for the Falklands # until advised differently (to apply for 2012 and beyond, after the 2011 # experiment was apparently successful.) # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Falk 1937 1938 - Sep lastSun 0:00 1:00 - Rule Falk 1938 1942 - Mar Sun>=19 0:00 0 - Rule Falk 1939 only - Oct 1 0:00 1:00 - Rule Falk 1940 1942 - Sep lastSun 0:00 1:00 - Rule Falk 1943 only - Jan 1 0:00 0 - Rule Falk 1983 only - Sep lastSun 0:00 1:00 - Rule Falk 1984 1985 - Apr lastSun 0:00 0 - Rule Falk 1984 only - Sep 16 0:00 1:00 - Rule Falk 1985 2000 - Sep Sun>=9 0:00 1:00 - Rule Falk 1986 2000 - Apr Sun>=16 0:00 0 - Rule Falk 2001 2010 - Apr Sun>=15 2:00 0 - Rule Falk 2001 2010 - Sep Sun>=1 2:00 1:00 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Atlantic/Stanley -3:51:24 - LMT 1890 -3:51:24 - SMT 1912 Mar 12 # Stanley Mean Time -4:00 Falk %z 1983 May -3:00 Falk %z 1985 Sep 15 -4:00 Falk %z 2010 Sep 5 2:00 -3:00 - %z # French Guiana # For the 1911/1912 establishment of standard time in French possessions, see: # Société Française de Physique, Recueil de constantes physiques (1913), # page 752, 18b. # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Cayenne -3:29:20 - LMT 1911 Jul 1 -4:00 - %z 1967 Oct -3:00 - %z # Guyana # From P Chan (2020-11-27): # https://books.google.com/books?id=5-5CAQAAMAAJ&pg=SA1-PA547 # The Official Gazette of British Guiana. (New Series.) Vol. XL. July to # December, 1915, p 1547, lists as several notes: # "Local Mean Time 3 hours 52 mins. 39 secs. slow of Greenwich Mean Time # (Georgetown.) From 1st August, 1911, British Guiana Standard Mean Time 4 # hours slow of Greenwich Mean Time, by notice in Official Gazette on 1st July, # 1911. From 1st March, 1915, British Guiana Standard Mean Time 3 hours 45 # mins. 0 secs. slow of Greenwich Mean Time, by notice in Official Gazette on # 23rd January, 1915." # # https://parliament.gov.gy/documents/acts/10923-act_no._27_of_1975_-_interpretation_and_general_clauses_(amendment)_act_1975.pdf # Interpretation and general clauses (Amendment) Act 1975 (Act No. 27 of 1975) # [dated 1975-07-31] # "This Act...shall come into operation on 1st August, 1975." # "...where any expression of time occurs...the time referred to shall signify # the standard time of Guyana which shall be three hours behind Greenwich Mean # Time." # # Circular No. 10/1992 dated 1992-03-20 # https://dps.gov.gy/wp-content/uploads/2018/12/1992-03-20-Circular-010.pdf # "...cabinet has decided that with effect from Sunday 29th March, 1992, Guyana # Standard Time would be re-established at 01:00 hours by adjusting the hands # of the clock back to 24:00 hours." # Legislated in the Interpretation and general clauses (Amendment) Act 1992 # (Act No. 6 of 1992) [passed 1992-03-27, published 1992-04-18] # https://parliament.gov.gy/documents/acts/5885-6_of_1992_interpretation_and_general_clauses_(amendment)_act_1992.pdf # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Guyana -3:52:39 - LMT 1911 Aug 1 # Georgetown -4:00 - %z 1915 Mar 1 -3:45 - %z 1975 Aug 1 -3:00 - %z 1992 Mar 29 1:00 -4:00 - %z # Paraguay # # From Paul Eggert (2006-03-22): # Shanks & Pottenger say that spring transitions are 01:00 -> 02:00, # and autumn transitions are 00:00 -> 23:00. Go with pre-1999 # editions of Shanks, and with the IATA, who say transitions occur at 00:00. # # From Waldemar Villamayor-Venialbo (2013-09-20): # No time of the day is established for the adjustment, so people normally # adjust their clocks at 0 hour of the given dates. # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Para 1975 1988 - Oct 1 0:00 1:00 - Rule Para 1975 1978 - Mar 1 0:00 0 - Rule Para 1979 1991 - Apr 1 0:00 0 - Rule Para 1989 only - Oct 22 0:00 1:00 - Rule Para 1990 only - Oct 1 0:00 1:00 - Rule Para 1991 only - Oct 6 0:00 1:00 - Rule Para 1992 only - Mar 1 0:00 0 - Rule Para 1992 only - Oct 5 0:00 1:00 - Rule Para 1993 only - Mar 31 0:00 0 - Rule Para 1993 1995 - Oct 1 0:00 1:00 - Rule Para 1994 1995 - Feb lastSun 0:00 0 - Rule Para 1996 only - Mar 1 0:00 0 - # IATA SSIM (2000-02) says 1999-10-10; ignore this for now. # From Steffen Thorsen (2000-10-02): # I have three independent reports that Paraguay changed to DST this Sunday # (10-01). # # Translated by Gwillim Law (2001-02-27) from # Noticias, a daily paper in Asunción, Paraguay (2000-10-01): # http://www.diarionoticias.com.py/011000/nacional/naciona1.htm # Starting at 0:00 today, the clock will be set forward 60 minutes, in # fulfillment of Decree No. 7,273 of the Executive Power.... The time change # system has been operating for several years. Formerly there was a separate # decree each year; the new law has the same effect, but permanently. Every # year, the time will change on the first Sunday of October; likewise, the # clock will be set back on the first Sunday of March. # Rule Para 1996 2001 - Oct Sun>=1 0:00 1:00 - # IATA SSIM (1997-09) says Mar 1; go with Shanks & Pottenger. Rule Para 1997 only - Feb lastSun 0:00 0 - # Shanks & Pottenger say 1999-02-28; IATA SSIM (1999-02) says 1999-02-27, but # (1999-09) reports no date; go with above sources and Gerd Knops (2001-02-27). Rule Para 1998 2001 - Mar Sun>=1 0:00 0 - # From Rives McDow (2002-02-28): # A decree was issued in Paraguay (No. 16350) on 2002-02-26 that changed the # dst method to be from the first Sunday in September to the first Sunday in # April. Rule Para 2002 2004 - Apr Sun>=1 0:00 0 - Rule Para 2002 2003 - Sep Sun>=1 0:00 1:00 - # # From Jesper Nørgaard Welen (2005-01-02): # There are several sources that claim that Paraguay made # a timezone rule change in autumn 2004. # From Steffen Thorsen (2005-01-05): # Decree 1,867 (2004-03-05) # From Carlos Raúl Perasso via Jesper Nørgaard Welen (2006-10-13) # http://www.presidencia.gov.py/decretos/D1867.pdf Rule Para 2004 2009 - Oct Sun>=15 0:00 1:00 - Rule Para 2005 2009 - Mar Sun>=8 0:00 0 - # From Carlos Raúl Perasso (2010-02-18): # By decree number 3958 issued yesterday # http://www.presidencia.gov.py/v1/wp-content/uploads/2010/02/decreto3958.pdf # Paraguay changes its DST schedule, postponing the March rule to April and # modifying the October date. The decree reads: # ... # Art. 1. It is hereby established that from the second Sunday of the month of # April of this year (2010), the official time is to be set back 60 minutes, # and that on the first Sunday of the month of October, it is to be set # forward 60 minutes, in all the territory of the Paraguayan Republic. # ... Rule Para 2010 2024 - Oct Sun>=1 0:00 1:00 - Rule Para 2010 2012 - Apr Sun>=8 0:00 0 - # # From Steffen Thorsen (2013-03-07): # Paraguay will end DST on 2013-03-24 00:00.... # http://www.ande.gov.py/interna.php?id=1075 # # From Carlos Raúl Perasso (2013-03-15): # The change in Paraguay is now final. Decree number 10780 # http://www.presidencia.gov.py/uploads/pdf/presidencia-3b86ff4b691c79d4f5927ca964922ec74772ce857c02ca054a52a37b49afc7fb.pdf # From Carlos Raúl Perasso (2014-02-28): # Decree 1264 can be found at: # http://www.presidencia.gov.py/archivos/documentos/DECRETO1264_ey9r8zai.pdf # # From Paul Eggert (2023-07-26): # Transition dates are now set by Law No. 7115, not by presidential decree. # https://www.abc.com.py/politica/2023/07/12/promulgacion-el-cambio-de-hora-sera-por-ley/ # From Carlos Raúl Perasso (2023-07-27): # http://silpy.congreso.gov.py/descarga/ley-144138 Rule Para 2013 2024 - Mar Sun>=22 0:00 0 - # # From Heitor David Pinto (2024-09-24): # Today the Congress of Paraguay passed a bill to observe UTC-3 permanently.... # The text of the bill says that it would enter into force on the first # Sunday in October 2024, the same date currently scheduled to start DST.... # https://silpy.congreso.gov.py/web/expediente/132531 # (2024-10-14): # The president approved the law on 11 October 2024, # and it was officially published on 14 October 2024. # https://www.gacetaoficial.gov.py/index/detalle_publicacion/89723 # The text of the law says that it enters into force on the first # Sunday in October 2024 (6 October 2024). But the constitution # prohibits retroactive effect, and the civil code says that laws # enter into force on the day after their publication or on the day # that they specify, and it also says that they don't have retroactive # effect. So I think that the time change on 6 October 2024 should # still be considered as DST according to the previous law, and # permanently UTC-3 from 15 October 2024 according to the new law.... # https://www.constituteproject.org/constitution/Paraguay_2011 # https://www.oas.org/dil/esp/codigo_civil_paraguay.pdf # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Asuncion -3:50:40 - LMT 1890 -3:50:40 - AMT 1931 Oct 10 # Asunción Mean Time -4:00 - %z 1972 Oct -3:00 - %z 1974 Apr -4:00 Para %z 2024 Oct 15 -3:00 - %z # Peru # # From Evelyn C. Leeper via Mark Brader (2003-10-26) # : # When we were in Peru in 1985-1986, they apparently switched over # sometime between December 29 and January 3 while we were on the Amazon. # # From Paul Eggert (2006-03-22): # Shanks & Pottenger don't have this transition. Assume 1986 was like 1987. # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Peru 1938 only - Jan 1 0:00 1:00 - Rule Peru 1938 only - Apr 1 0:00 0 - Rule Peru 1938 1939 - Sep lastSun 0:00 1:00 - Rule Peru 1939 1940 - Mar Sun>=24 0:00 0 - Rule Peru 1986 1987 - Jan 1 0:00 1:00 - Rule Peru 1986 1987 - Apr 1 0:00 0 - Rule Peru 1990 only - Jan 1 0:00 1:00 - Rule Peru 1990 only - Apr 1 0:00 0 - # IATA is ambiguous for 1993/1995; go with Shanks & Pottenger. Rule Peru 1994 only - Jan 1 0:00 1:00 - Rule Peru 1994 only - Apr 1 0:00 0 - # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Lima -5:08:12 - LMT 1890 -5:08:36 - LMT 1908 Jul 28 # Lima Mean Time? -5:00 Peru %z # South Georgia # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone Atlantic/South_Georgia -2:26:08 - LMT 1890 # Grytviken -2:00 - %z # South Sandwich Is # uninhabited; scientific personnel have wintered # Suriname # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Paramaribo -3:40:40 - LMT 1911 -3:40:52 - PMT 1935 # Paramaribo Mean Time -3:40:36 - PMT 1945 Oct # The capital moved? -3:30 - %z 1984 Oct -3:00 - %z # Uruguay # From Paul Eggert (1993-11-18): # Uruguay wins the prize for the strangest peacetime manipulation of the rules. # # From Tim Parenti (2018-02-20), per Jeremie Bonjour (2018-01-31) and Michael # Deckers (2018-02-20): # ... At least they kept good records... # # http://www.armada.mil.uy/ContenidosPDFs/sohma/web/almanaque/almanaque_2018.pdf#page=36 # Page 36 of Almanaque 2018, published by the Oceanography, Hydrography, and # Meteorology Service of the Uruguayan Navy, seems to give many transitions # with greater clarity than we've had before. It directly references many laws # and decrees which are, in turn, referenced below. They can be viewed in the # public archives of the Diario Oficial (in Spanish) at # http://www.impo.com.uy/diariooficial/ # # Ley No. 3920 of 1908-06-10 placed the determination of legal time under the # auspices of the National Institute for the Prediction of Time. It is unclear # exactly what offset was used during this period, though Ley No. 7200 of # 1920-04-23 used the Observatory of the National Meteorological Institute in # Montevideo (34° 54' 33" S, 56° 12' 45" W) as its reference meridian, # retarding legal time by 15 minutes 9 seconds from 1920-04-30 24:00, # resulting in UT-04. Assume the corresponding LMT of UT-03:44:51 (given on # page 725 of the Proceedings of the Second Pan-American Scientific Congress, # 1915-1916) was in use, and merely became official from 1908-06-10. # https://www.impo.com.uy/diariooficial/1908/06/18/12 # https://www.impo.com.uy/diariooficial/1920/04/27/9 # # Ley No. 7594 of 1923-06-28 specified legal time as Observatory time advanced # by 44 minutes 51 seconds (UT-03) "from 30 September to 31 March", and by 14 # minutes 51 seconds (UT-03:30) "the rest of the year"; a message from the # National Council of Administration the same day, published directly below the # law in the Diario Oficial, specified the first transition to be 1923-09-30 # 24:00. This effectively established standard time at UT-03:30 with 30 # minutes DST. Assume transitions at 24:00 on the specified days until Ley No. # 7919 of 1926-03-05 ended this arrangement, repealing all "laws and other # provisions which oppose" it, resulting in year-round UT-03:30; a Resolución # of 1926-03-11 puts the final transition at 1926-03-31 24:00, the same as it # would have been under the previous law. # https://www.impo.com.uy/diariooficial/1923/07/02/2 # https://www.impo.com.uy/diariooficial/1926/03/10/2 # https://www.impo.com.uy/diariooficial/1926/03/18/2 # # Rule NAME FROM TO - IN ON AT SAVE LETTER/S Rule Uruguay 1923 1925 - Oct 1 0:00 0:30 - Rule Uruguay 1924 1926 - Apr 1 0:00 0 - # From Tim Parenti (2018-02-15): # http://www.impo.com.uy/diariooficial/1933/10/27/6 # # It appears Ley No. 9122 of 1933 was never published as such in the Diario # Oficial, but instead appeared as Document 26 in the Diario on Friday # 1933-10-27 as a decree made Monday 1933-10-23 and filed under the Ministry of # National Defense. It reinstituted a DST of 30 minutes (to UT-03) "from the # last Sunday of October...until the last Saturday of March." In accordance # with this provision, the first transition was explicitly specified in Article # 2 of the decree as Saturday 1933-10-28 at 24:00; that is, Sunday 1933-10-29 # at 00:00. Assume transitions at 00:00 Sunday throughout. # # Departing from the matter-of-fact nature of previous timekeeping laws, the # 1933 decree "consider[s] the advantages of...the advance of legal time": # # "Whereas: The measure adopted by almost all nations at the time of the last # World War still persists in North America and Europe, precisely because of # the economic, hygienic, and social advantages derived from such an # emergency measure... # # Whereas: The advance of the legal time during the summer seasons, by # displacing social activity near sunrise, favors the citizen populations # and especially the society that creates and works..." # # It further specified that "necessary measures" be taken to ensure that # "public spectacles finish, in general, before [01:00]." Rule Uruguay 1933 1938 - Oct lastSun 0:00 0:30 - Rule Uruguay 1934 1941 - Mar lastSat 24:00 0 - # From Tim Parenti (2018-02-15): # Most of the Rules below, and their contemporaneous Zone lines, have been # updated simply to match the Almanaque 2018. Although the document does not # list exact transition times, midnight transitions were already present in our # data here for all transitions through 2004-09, and this is both consistent # with prior transitions and verified in several decrees marked below between # 1939-09 and 2004-09, wherein the relevant text was typically of the form: # # "From 0 hours on [date], the legal time of the entire Republic will be... # # In accordance with [the preceding], on [previous date] at 24 hours, all # clocks throughout the Republic will be [advanced/retarded] by..." # # It is possible that there is greater specificity to be found for the Rules # below, but it is buried in no fewer than 40 different decrees individually # referenced by the Almanaque for the period from 1939-09 to 2014-09. # Four-fifths of these were promulgated less than two weeks before taking # effect; more than half within a week and none more than 5 weeks. Only the # handful with comments below have been checked with any thoroughness. Rule Uruguay 1939 only - Oct 1 0:00 0:30 - Rule Uruguay 1940 only - Oct 27 0:00 0:30 - # From Tim Parenti (2018-02-15): # Decreto 1145 of the Ministry of National Defense, dated 1941-07-26, specified # UT-03 from Friday 1941-08-01 00:00, citing an "urgent...need to save fuel". # http://www.impo.com.uy/diariooficial/1941/08/04/1 Rule Uruguay 1941 only - Aug 1 0:00 0:30 - # From Tim Parenti (2018-02-15): # Decreto 1866 of the Ministry of National Defense, dated 1942-12-09, specified # further advancement (to UT-02:30) from Sunday 1942-12-13 24:00. Since clocks # never went back to UT-03:30 thereafter, this is modeled as advancing standard # time by 30 minutes to UT-03, while retaining 30 minutes of DST. # http://www.impo.com.uy/diariooficial/1942/12/16/3 Rule Uruguay 1942 only - Dec 14 0:00 0:30 - Rule Uruguay 1943 only - Mar 14 0:00 0 - Rule Uruguay 1959 only - May 24 0:00 0:30 - Rule Uruguay 1959 only - Nov 15 0:00 0 - Rule Uruguay 1960 only - Jan 17 0:00 1:00 - Rule Uruguay 1960 only - Mar 6 0:00 0 - Rule Uruguay 1965 only - Apr 4 0:00 1:00 - Rule Uruguay 1965 only - Sep 26 0:00 0 - # From Tim Parenti (2018-02-15): # Decreto 321/968 of 1968-05-25, citing emergency drought measures decreed the # day before, brought clocks forward 30 minutes from Monday 1968-05-27 00:00. # http://www.impo.com.uy/diariooficial/1968/05/30/5 Rule Uruguay 1968 only - May 27 0:00 0:30 - Rule Uruguay 1968 only - Dec 1 0:00 0 - # From Tim Parenti (2018-02-15): # Decreto 188/970 of 1970-04-23 instituted restrictions on electricity # consumption "as a consequence of the current rainfall regime in the country". # Articles 13 and 14 advanced clocks by an hour from Saturday 1970-04-25 00:00. # http://www.impo.com.uy/diariooficial/1970/04/29/4 Rule Uruguay 1970 only - Apr 25 0:00 1:00 - Rule Uruguay 1970 only - Jun 14 0:00 0 - Rule Uruguay 1972 only - Apr 23 0:00 1:00 - Rule Uruguay 1972 only - Jul 16 0:00 0 - # From Tim Parenti (2018-02-15): # Decreto 29/974 of 1974-01-11, citing "the international rise in the price of # oil", advanced clocks by 90 minutes (to UT-01:30). Decreto 163/974 of # 1974-03-04 returned 60 of those minutes (to UT-02:30), and the remaining 30 # minutes followed in Decreto 679/974 of 1974-08-29. # http://www.impo.com.uy/diariooficial/1974/01/22/11 # http://www.impo.com.uy/diariooficial/1974/03/14/3 # http://www.impo.com.uy/diariooficial/1974/09/04/6 Rule Uruguay 1974 only - Jan 13 0:00 1:30 - Rule Uruguay 1974 only - Mar 10 0:00 0:30 - Rule Uruguay 1974 only - Sep 1 0:00 0 - Rule Uruguay 1974 only - Dec 22 0:00 1:00 - Rule Uruguay 1975 only - Mar 30 0:00 0 - Rule Uruguay 1976 only - Dec 19 0:00 1:00 - Rule Uruguay 1977 only - Mar 6 0:00 0 - Rule Uruguay 1977 only - Dec 4 0:00 1:00 - Rule Uruguay 1978 1979 - Mar Sun>=1 0:00 0 - Rule Uruguay 1978 only - Dec 17 0:00 1:00 - Rule Uruguay 1979 only - Apr 29 0:00 1:00 - Rule Uruguay 1980 only - Mar 16 0:00 0 - # From Tim Parenti (2018-02-15): # Decreto 725/987 of 1987-12-04 cited "better use of national tourist # attractions" to advance clocks one hour from Monday 1987-12-14 00:00. # http://www.impo.com.uy/diariooficial/1988/01/25/1 Rule Uruguay 1987 only - Dec 14 0:00 1:00 - Rule Uruguay 1988 only - Feb 28 0:00 0 - Rule Uruguay 1988 only - Dec 11 0:00 1:00 - Rule Uruguay 1989 only - Mar 5 0:00 0 - Rule Uruguay 1989 only - Oct 29 0:00 1:00 - Rule Uruguay 1990 only - Feb 25 0:00 0 - # From Tim Parenti (2018-02-15), per Paul Eggert (1999-11-04): # IATA agrees as below for 1990-10 through 1993-02. Per Almanaque 2018, the # 1992/1993 season appears to be the first in over half a century where DST # both began and ended pursuant to the same decree. Rule Uruguay 1990 1991 - Oct Sun>=21 0:00 1:00 - Rule Uruguay 1991 1992 - Mar Sun>=1 0:00 0 - Rule Uruguay 1992 only - Oct 18 0:00 1:00 - Rule Uruguay 1993 only - Feb 28 0:00 0 - # From Eduardo Cota (2004-09-20): # The Uruguayan government has decreed a change in the local time.... # From Tim Parenti (2018-02-15): # Decreto 328/004 of 2004-09-15. # http://www.impo.com.uy/diariooficial/2004/09/23/documentos.pdf#page=1 Rule Uruguay 2004 only - Sep 19 0:00 1:00 - # From Steffen Thorsen (2005-03-11): # Uruguay's DST was scheduled to end on Sunday, 2005-03-13, but in order to # save energy ... it was postponed two weeks.... # From Tim Parenti (2018-02-15): # This 2005 postponement is not in Almanaque 2018. Go with the contemporaneous # reporting, which is confirmed by Decreto 107/005 of 2005-03-10 amending # Decreto 328/004: # http://www.impo.com.uy/diariooficial/2005/03/15/documentos.pdf#page=1 # The original decree specified a transition of 2005-03-12 24:00, but the new # one specified 2005-03-27 02:00. Rule Uruguay 2005 only - Mar 27 2:00 0 - # From Eduardo Cota (2005-09-27): # ...from 2005-10-09 at 02:00 local time, until 2006-03-12 at 02:00 local time, # official time in Uruguay will be at GMT -2. # From Tim Parenti (2018-02-15): # Decreto 318/005 of 2005-09-19. # http://www.impo.com.uy/diariooficial/2005/09/23/documentos.pdf#page=1 Rule Uruguay 2005 only - Oct 9 2:00 1:00 - Rule Uruguay 2006 2015 - Mar Sun>=8 2:00 0 - # From Tim Parenti (2018-02-15), per Jesper Nørgaard Welen (2006-09-06): # Decreto 311/006 of 2006-09-04 established regular DST from the first Sunday # of October at 02:00 through the second Sunday of March at 02:00. Almanaque # 2018 appears to have a few typoed dates through this period; ignore them. # http://www.impo.com.uy/diariooficial/2006/09/08/documentos.pdf#page=1 Rule Uruguay 2006 2014 - Oct Sun>=1 2:00 1:00 - # From Steffen Thorsen (2015-06-30): # ... it looks like they will not be using DST the coming summer: # http://www.elobservador.com.uy/gobierno-resolvio-que-no-habra-cambio-horario-verano-n656787 # http://www.republica.com.uy/este-ano-no-se-modificara-el-huso-horario-en-uruguay/523760/ # From Paul Eggert (2015-06-30): # Apparently restaurateurs complained that DST caused people to go to the beach # instead of out to dinner. # From Pablo Camargo (2015-07-13): # http://archivo.presidencia.gub.uy/sci/decretos/2015/06/cons_min_201.pdf # From Tim Parenti (2018-02-15): # Decreto 178/015 of 2015-06-29; repeals Decreto 311/006. # This Zone can be simplified once we assume zic %z. Zone America/Montevideo -3:44:51 - LMT 1908 Jun 10 -3:44:51 - MMT 1920 May 1 # Montevideo MT -4:00 - %z 1923 Oct 1 -3:30 Uruguay %z 1942 Dec 14 -3:00 Uruguay %z 1960 -3:00 Uruguay %z 1968 -3:00 Uruguay %z 1970 -3:00 Uruguay %z 1974 -3:00 Uruguay %z 1974 Mar 10 -3:00 Uruguay %z 1974 Dec 22 -3:00 Uruguay %z # Venezuela # # From Paul Eggert (2015-07-28): # For the 1965 transition see Gaceta Oficial No. 27.619 (1964-12-15), p 205.533 # http://www.pgr.gob.ve/dmdocuments/1964/27619.pdf # # From John Stainforth (2007-11-28): # ... the change for Venezuela originally expected for 2007-12-31 has # been brought forward to 2007-12-09. The official announcement was # published today in the "Gaceta Oficial de la República Bolivariana # de Venezuela, número 38.819" (official document for all laws or # resolution publication) # http://www.globovision.com/news.php?nid=72208 # From Alexander Krivenyshev (2016-04-15): # https://actualidad.rt.com/actualidad/204758-venezuela-modificar-huso-horario-sequia-elnino # # From Paul Eggert (2016-04-15): # Clocks advance 30 minutes on 2016-05-01 at 02:30.... # "'Venezuela's new time-zone: hours without light, hours without water, # hours of presidential broadcasts, hours of lines,' quipped comedian # Jean Mary Curró ...". See: Cawthorne A, Kai D. Venezuela scraps # half-hour time difference set by Chavez. Reuters 2016-04-15 14:50 -0400 # https://www.reuters.com/article/us-venezuela-timezone-idUSKCN0XC2BE # # From Matt Johnson (2016-04-20): # ... published in the official Gazette [2016-04-18], here: # http://historico.tsj.gob.ve/gaceta_ext/abril/1842016/E-1842016-4551.pdf # Zone NAME STDOFF RULES FORMAT [UNTIL] Zone America/Caracas -4:27:44 - LMT 1890 -4:27:40 - CMT 1912 Feb 12 # Caracas Mean Time? -4:30 - %z 1965 Jan 1 0:00 -4:00 - %z 2007 Dec 9 3:00 -4:30 - %z 2016 May 1 2:30 -4:00 - %z pr0m1th3as-datatypes-9450b29/inst/tzdata/version000066400000000000000000000000061527370255000215460ustar00rootroot000000000000002026f pr0m1th3as-datatypes-9450b29/inst/tzdata/windowsZones.xml000066400000000000000000001535671527370255000234160ustar00rootroot00000000000000 pr0m1th3as-datatypes-9450b29/inst/tzdata/zone.tab000066400000000000000000000445711527370255000216200ustar00rootroot00000000000000# tzdb timezone descriptions (deprecated version) # # This file is in the public domain, so clarified as of # 2009-05-17 by Arthur David Olson. # # From Paul Eggert (2021-09-20): # This file is intended as a backward-compatibility aid for older programs. # New programs should use zone1970.tab. This file is like zone1970.tab (see # zone1970.tab's comments), but with the following additional restrictions: # # 1. This file contains only ASCII characters. # 2. The first data column contains exactly one country code. # # Because of (2), each row stands for an area that is the intersection # of a region identified by a country code and of a timezone where civil # clocks have agreed since 1970; this is a narrower definition than # that of zone1970.tab. # # Unlike zone1970.tab, a row's third column can be a Link from # 'backward' instead of a Zone. # # This table is intended as an aid for users, to help them select timezones # appropriate for their practical needs. It is not intended to take or # endorse any position on legal or territorial claims. # #country- #code coordinates TZ comments AD +4230+00131 Europe/Andorra AE +2518+05518 Asia/Dubai AF +3431+06912 Asia/Kabul AG +1703-06148 America/Antigua AI +1812-06304 America/Anguilla AL +4120+01950 Europe/Tirane AM +4011+04430 Asia/Yerevan AO -0848+01314 Africa/Luanda AQ -7750+16636 Antarctica/McMurdo New Zealand time - McMurdo, South Pole AQ -6617+11031 Antarctica/Casey Casey AQ -6835+07758 Antarctica/Davis Davis AQ -6640+14001 Antarctica/DumontDUrville Dumont-d'Urville AQ -6736+06253 Antarctica/Mawson Mawson AQ -6448-06406 Antarctica/Palmer Palmer AQ -6734-06808 Antarctica/Rothera Rothera AQ -690022+0393524 Antarctica/Syowa Syowa AQ -720041+0023206 Antarctica/Troll Troll AQ -7824+10654 Antarctica/Vostok Vostok AR -3436-05827 America/Argentina/Buenos_Aires Buenos Aires (BA, CF) AR -3124-06411 America/Argentina/Cordoba Argentina (most areas: CB, CC, CN, ER, FM, MN, SE, SF) AR -2447-06525 America/Argentina/Salta Salta (SA, LP, NQ, RN) AR -2411-06518 America/Argentina/Jujuy Jujuy (JY) AR -2649-06513 America/Argentina/Tucuman Tucuman (TM) AR -2828-06547 America/Argentina/Catamarca Catamarca (CT), Chubut (CH) AR -2926-06651 America/Argentina/La_Rioja La Rioja (LR) AR -3132-06831 America/Argentina/San_Juan San Juan (SJ) AR -3253-06849 America/Argentina/Mendoza Mendoza (MZ) AR -3319-06621 America/Argentina/San_Luis San Luis (SL) AR -5138-06913 America/Argentina/Rio_Gallegos Santa Cruz (SC) AR -5448-06818 America/Argentina/Ushuaia Tierra del Fuego (TF) AS -1416-17042 Pacific/Pago_Pago AT +4813+01620 Europe/Vienna AU -3133+15905 Australia/Lord_Howe Lord Howe Island AU -5430+15857 Antarctica/Macquarie Macquarie Island AU -4253+14719 Australia/Hobart Tasmania AU -3749+14458 Australia/Melbourne Victoria AU -3352+15113 Australia/Sydney New South Wales (most areas) AU -3157+14127 Australia/Broken_Hill New South Wales (Yancowinna) AU -2728+15302 Australia/Brisbane Queensland (most areas) AU -2016+14900 Australia/Lindeman Queensland (Whitsunday Islands) AU -3455+13835 Australia/Adelaide South Australia AU -1228+13050 Australia/Darwin Northern Territory AU -3157+11551 Australia/Perth Western Australia (most areas) AU -3143+12852 Australia/Eucla Western Australia (Eucla) AW +1230-06958 America/Aruba AX +6006+01957 Europe/Mariehamn AZ +4023+04951 Asia/Baku BA +4352+01825 Europe/Sarajevo BB +1306-05937 America/Barbados BD +2343+09025 Asia/Dhaka BE +5050+00420 Europe/Brussels BF +1222-00131 Africa/Ouagadougou BG +4241+02319 Europe/Sofia BH +2623+05035 Asia/Bahrain BI -0323+02922 Africa/Bujumbura BJ +0629+00237 Africa/Porto-Novo BL +1753-06251 America/St_Barthelemy BM +3217-06446 Atlantic/Bermuda BN +0456+11455 Asia/Brunei BO -1630-06809 America/La_Paz BQ +120903-0681636 America/Kralendijk BR -0351-03225 America/Noronha Atlantic islands BR -0127-04829 America/Belem Para (east), Amapa BR -0343-03830 America/Fortaleza Brazil (northeast: MA, PI, CE, RN, PB) BR -0803-03454 America/Recife Pernambuco BR -0712-04812 America/Araguaina Tocantins BR -0940-03543 America/Maceio Alagoas, Sergipe BR -1259-03831 America/Bahia Bahia BR -2332-04637 America/Sao_Paulo Brazil (southeast: GO, DF, MG, ES, RJ, SP, PR, SC, RS) BR -2027-05437 America/Campo_Grande Mato Grosso do Sul BR -1535-05605 America/Cuiaba Mato Grosso BR -0226-05452 America/Santarem Para (west) BR -0846-06354 America/Porto_Velho Rondonia BR +0249-06040 America/Boa_Vista Roraima BR -0308-06001 America/Manaus Amazonas (east) BR -0640-06952 America/Eirunepe Amazonas (west) BR -0958-06748 America/Rio_Branco Acre BS +2505-07721 America/Nassau BT +2728+08939 Asia/Thimphu BW -2439+02555 Africa/Gaborone BY +5354+02734 Europe/Minsk BZ +1730-08812 America/Belize CA +4734-05243 America/St_Johns Newfoundland, Labrador (SE) CA +4439-06336 America/Halifax Atlantic - NS (most areas), PE CA +4612-05957 America/Glace_Bay Atlantic - NS (Cape Breton) CA +4606-06447 America/Moncton Atlantic - New Brunswick CA +5320-06025 America/Goose_Bay Atlantic - Labrador (most areas) CA +5125-05707 America/Blanc-Sablon AST - QC (Lower North Shore) CA +4339-07923 America/Toronto Eastern - ON & QC (most areas) CA +6344-06828 America/Iqaluit Eastern - NU (most areas) CA +484531-0913718 America/Atikokan EST - ON (Atikokan), NU (Coral H) CA +4953-09709 America/Winnipeg EST - Manitoba, ON (northwest) CA +744144-0944945 America/Resolute Central - NU (Resolute) CA +624900-0920459 America/Rankin_Inlet Central - NU (central) CA +5024-10439 America/Regina CST - SK (most areas) CA +5017-10750 America/Swift_Current CST - SK (midwest) CA +5333-11328 America/Edmonton CST - AB, BC(E), NT(E), SK(W) CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west) CA +682059-1334300 America/Inuvik CST - NT (west) CA +4916-12307 America/Vancouver MST - BC (most areas) CA +4906-11631 America/Creston MST - BC (Creston) CA +5546-12014 America/Dawson_Creek MST - BC (Dawson Cr, Ft St John) CA +5848-12242 America/Fort_Nelson MST - BC (Ft Nelson) CA +6043-13503 America/Whitehorse MST - Yukon (east) CA +6404-13925 America/Dawson MST - Yukon (west) CC -1210+09655 Indian/Cocos CD -0418+01518 Africa/Kinshasa Dem. Rep. of Congo (west) CD -1140+02728 Africa/Lubumbashi Dem. Rep. of Congo (east) CF +0422+01835 Africa/Bangui CG -0416+01517 Africa/Brazzaville CH +4723+00832 Europe/Zurich CI +0519-00402 Africa/Abidjan CK -2114-15946 Pacific/Rarotonga CL -3327-07040 America/Santiago most of Chile CL -4534-07204 America/Coyhaique Aysen Region CL -5309-07055 America/Punta_Arenas Magallanes Region CL -2709-10926 Pacific/Easter Easter Island CM +0403+00942 Africa/Douala CN +3114+12128 Asia/Shanghai Beijing Time CN +4348+08735 Asia/Urumqi Xinjiang Time CO +0436-07405 America/Bogota CR +0956-08405 America/Costa_Rica CU +2308-08222 America/Havana CV +1455-02331 Atlantic/Cape_Verde CW +1211-06900 America/Curacao CX -1025+10543 Indian/Christmas CY +3510+03322 Asia/Nicosia most of Cyprus CY +3507+03357 Asia/Famagusta Northern Cyprus CZ +5005+01426 Europe/Prague DE +5230+01322 Europe/Berlin most of Germany DE +4742+00841 Europe/Busingen Busingen DJ +1136+04309 Africa/Djibouti DK +5540+01235 Europe/Copenhagen DM +1518-06124 America/Dominica DO +1828-06954 America/Santo_Domingo DZ +3647+00303 Africa/Algiers EC -0210-07950 America/Guayaquil Ecuador (mainland) EC -0054-08936 Pacific/Galapagos Galapagos Islands EE +5925+02445 Europe/Tallinn EG +3003+03115 Africa/Cairo EH +2709-01312 Africa/El_Aaiun ER +1520+03853 Africa/Asmara ES +4024-00341 Europe/Madrid Spain (mainland) ES +3553-00519 Africa/Ceuta Ceuta, Melilla ES +2806-01524 Atlantic/Canary Canary Islands ET +0902+03842 Africa/Addis_Ababa FI +6010+02458 Europe/Helsinki FJ -1808+17825 Pacific/Fiji FK -5142-05751 Atlantic/Stanley FM +0725+15147 Pacific/Chuuk Chuuk/Truk, Yap FM +0658+15813 Pacific/Pohnpei Pohnpei/Ponape FM +0519+16259 Pacific/Kosrae Kosrae FO +6201-00646 Atlantic/Faroe FR +4852+00220 Europe/Paris GA +0023+00927 Africa/Libreville GB +513030-0000731 Europe/London GD +1203-06145 America/Grenada GE +4143+04449 Asia/Tbilisi GF +0456-05220 America/Cayenne GG +492717-0023210 Europe/Guernsey GH +0533-00013 Africa/Accra GI +3608-00521 Europe/Gibraltar GL +6411-05144 America/Nuuk most of Greenland GL +7646-01840 America/Danmarkshavn National Park (east coast) GL +7029-02158 America/Scoresbysund Scoresbysund/Ittoqqortoormiit GL +7634-06847 America/Thule Thule/Pituffik GM +1328-01639 Africa/Banjul GN +0931-01343 Africa/Conakry GP +1614-06132 America/Guadeloupe GQ +0345+00847 Africa/Malabo GR +3758+02343 Europe/Athens GS -5416-03632 Atlantic/South_Georgia GT +1438-09031 America/Guatemala GU +1328+14445 Pacific/Guam GW +1151-01535 Africa/Bissau GY +0648-05810 America/Guyana HK +2217+11409 Asia/Hong_Kong HN +1406-08713 America/Tegucigalpa HR +4548+01558 Europe/Zagreb HT +1832-07220 America/Port-au-Prince HU +4730+01905 Europe/Budapest ID -0610+10648 Asia/Jakarta Java, Sumatra ID -0002+10920 Asia/Pontianak Borneo (west, central) ID -0507+11924 Asia/Makassar Borneo (east, south), Sulawesi/Celebes, Bali, Nusa Tengarra, Timor (west) ID -0232+14042 Asia/Jayapura New Guinea (West Papua / Irian Jaya), Malukus/Moluccas IE +5320-00615 Europe/Dublin IL +314650+0351326 Asia/Jerusalem IM +5409-00428 Europe/Isle_of_Man IN +2232+08822 Asia/Kolkata IO -0720+07225 Indian/Chagos IQ +3321+04425 Asia/Baghdad IR +3540+05126 Asia/Tehran IS +6409-02151 Atlantic/Reykjavik IT +4154+01229 Europe/Rome JE +491101-0020624 Europe/Jersey JM +175805-0764736 America/Jamaica JO +3157+03556 Asia/Amman JP +353916+1394441 Asia/Tokyo KE -0117+03649 Africa/Nairobi KG +4254+07436 Asia/Bishkek KH +1133+10455 Asia/Phnom_Penh KI +0125+17300 Pacific/Tarawa Gilbert Islands KI -0247-17143 Pacific/Kanton Phoenix Islands KI +0152-15720 Pacific/Kiritimati Line Islands KM -1141+04316 Indian/Comoro KN +1718-06243 America/St_Kitts KP +3901+12545 Asia/Pyongyang KR +3733+12658 Asia/Seoul KW +2920+04759 Asia/Kuwait KY +1918-08123 America/Cayman KZ +4315+07657 Asia/Almaty most of Kazakhstan KZ +4448+06528 Asia/Qyzylorda Qyzylorda/Kyzylorda/Kzyl-Orda KZ +5312+06337 Asia/Qostanay Qostanay/Kostanay/Kustanay KZ +5017+05710 Asia/Aqtobe Aqtobe/Aktobe KZ +4431+05016 Asia/Aqtau Mangghystau/Mankistau KZ +4707+05156 Asia/Atyrau Atyrau/Atirau/Gur'yev KZ +5113+05121 Asia/Oral West Kazakhstan LA +1758+10236 Asia/Vientiane LB +3353+03530 Asia/Beirut LC +1401-06100 America/St_Lucia LI +4709+00931 Europe/Vaduz LK +0656+07951 Asia/Colombo LR +0618-01047 Africa/Monrovia LS -2928+02730 Africa/Maseru LT +5441+02519 Europe/Vilnius LU +4936+00609 Europe/Luxembourg LV +5657+02406 Europe/Riga LY +3254+01311 Africa/Tripoli MA +3339-00735 Africa/Casablanca MC +4342+00723 Europe/Monaco MD +4700+02850 Europe/Chisinau ME +4226+01916 Europe/Podgorica MF +1804-06305 America/Marigot MG -1855+04731 Indian/Antananarivo MH +0709+17112 Pacific/Majuro most of Marshall Islands MH +0905+16720 Pacific/Kwajalein Kwajalein MK +4159+02126 Europe/Skopje ML +1239-00800 Africa/Bamako MM +1647+09610 Asia/Yangon MN +4755+10653 Asia/Ulaanbaatar most of Mongolia MN +4801+09139 Asia/Hovd Bayan-Olgii, Hovd, Uvs MO +221150+1133230 Asia/Macau MP +1512+14545 Pacific/Saipan MQ +1436-06105 America/Martinique MR +1806-01557 Africa/Nouakchott MS +1643-06213 America/Montserrat MT +3554+01431 Europe/Malta MU -2010+05730 Indian/Mauritius MV +0410+07330 Indian/Maldives MW -1547+03500 Africa/Blantyre MX +1924-09909 America/Mexico_City Central Mexico MX +2105-08646 America/Cancun Quintana Roo MX +2058-08937 America/Merida Campeche, Yucatan MX +2540-10019 America/Monterrey Durango; Coahuila, Nuevo Leon, Tamaulipas (most areas) MX +2550-09730 America/Matamoros Coahuila, Nuevo Leon, Tamaulipas (US border) MX +2838-10605 America/Chihuahua Chihuahua (most areas) MX +3144-10629 America/Ciudad_Juarez Chihuahua (US border - west) MX +2934-10425 America/Ojinaga Chihuahua (US border - east) MX +2313-10625 America/Mazatlan Baja California Sur, Nayarit (most areas), Sinaloa MX +2048-10515 America/Bahia_Banderas Bahia de Banderas MX +2904-11058 America/Hermosillo Sonora MX +3232-11701 America/Tijuana Baja California MY +0310+10142 Asia/Kuala_Lumpur Malaysia (peninsula) MY +0133+11020 Asia/Kuching Sabah, Sarawak MZ -2558+03235 Africa/Maputo NA -2234+01706 Africa/Windhoek NC -2216+16627 Pacific/Noumea NE +1331+00207 Africa/Niamey NF -2903+16758 Pacific/Norfolk NG +0627+00324 Africa/Lagos NI +1209-08617 America/Managua NL +5222+00454 Europe/Amsterdam NO +5955+01045 Europe/Oslo NP +2743+08519 Asia/Kathmandu NR -0031+16655 Pacific/Nauru NU -1901-16955 Pacific/Niue NZ -3652+17446 Pacific/Auckland most of New Zealand NZ -4357-17633 Pacific/Chatham Chatham Islands OM +2336+05835 Asia/Muscat PA +0858-07932 America/Panama PE -1203-07703 America/Lima PF -1732-14934 Pacific/Tahiti Society Islands PF -0900-13930 Pacific/Marquesas Marquesas Islands PF -2308-13457 Pacific/Gambier Gambier Islands PG -0930+14710 Pacific/Port_Moresby most of Papua New Guinea PG -0613+15534 Pacific/Bougainville Bougainville PH +143512+1205804 Asia/Manila PK +2452+06703 Asia/Karachi PL +5215+02100 Europe/Warsaw PM +4703-05620 America/Miquelon PN -2504-13005 Pacific/Pitcairn PR +182806-0660622 America/Puerto_Rico PS +3130+03428 Asia/Gaza Gaza Strip PS +313200+0350542 Asia/Hebron West Bank PT +3843-00908 Europe/Lisbon Portugal (mainland) PT +3238-01654 Atlantic/Madeira Madeira Islands PT +3744-02540 Atlantic/Azores Azores PW +0720+13429 Pacific/Palau PY -2516-05740 America/Asuncion QA +2517+05132 Asia/Qatar RE -2052+05528 Indian/Reunion RO +4426+02606 Europe/Bucharest RS +4450+02030 Europe/Belgrade RU +5443+02030 Europe/Kaliningrad MSK-01 - Kaliningrad RU +554521+0373704 Europe/Moscow MSK+00 - Moscow area # The obsolescent zone.tab format cannot represent Europe/Simferopol well. # Put it in RU section and list as UA. See "territorial claims" above. # Programs should use zone1970.tab instead; see above. UA +4457+03406 Europe/Simferopol Crimea RU +5836+04939 Europe/Kirov MSK+00 - Kirov RU +4844+04425 Europe/Volgograd MSK+00 - Volgograd RU +4621+04803 Europe/Astrakhan MSK+01 - Astrakhan RU +5134+04602 Europe/Saratov MSK+01 - Saratov RU +5420+04824 Europe/Ulyanovsk MSK+01 - Ulyanovsk RU +5312+05009 Europe/Samara MSK+01 - Samara, Udmurtia RU +5651+06036 Asia/Yekaterinburg MSK+02 - Urals RU +5500+07324 Asia/Omsk MSK+03 - Omsk RU +5502+08255 Asia/Novosibirsk MSK+04 - Novosibirsk RU +5322+08345 Asia/Barnaul MSK+04 - Altai RU +5630+08458 Asia/Tomsk MSK+04 - Tomsk RU +5345+08707 Asia/Novokuznetsk MSK+04 - Kemerovo RU +5601+09250 Asia/Krasnoyarsk MSK+04 - Krasnoyarsk area RU +5216+10420 Asia/Irkutsk MSK+05 - Irkutsk, Buryatia RU +5203+11328 Asia/Chita MSK+06 - Zabaykalsky RU +6200+12940 Asia/Yakutsk MSK+06 - Lena River RU +623923+1353314 Asia/Khandyga MSK+06 - Tomponsky, Ust-Maysky RU +4310+13156 Asia/Vladivostok MSK+07 - Amur River RU +643337+1431336 Asia/Ust-Nera MSK+07 - Oymyakonsky RU +5934+15048 Asia/Magadan MSK+08 - Magadan RU +4658+14242 Asia/Sakhalin MSK+08 - Sakhalin Island RU +6728+15343 Asia/Srednekolymsk MSK+08 - Sakha (E), N Kuril Is RU +5301+15839 Asia/Kamchatka MSK+09 - Kamchatka RU +6445+17729 Asia/Anadyr MSK+09 - Bering Sea RW -0157+03004 Africa/Kigali SA +2438+04643 Asia/Riyadh SB -0932+16012 Pacific/Guadalcanal SC -0440+05528 Indian/Mahe SD +1536+03232 Africa/Khartoum SE +5920+01803 Europe/Stockholm SG +0117+10351 Asia/Singapore SH -1555-00542 Atlantic/St_Helena SI +4603+01431 Europe/Ljubljana SJ +7800+01600 Arctic/Longyearbyen SK +4809+01707 Europe/Bratislava SL +0830-01315 Africa/Freetown SM +4355+01228 Europe/San_Marino SN +1440-01726 Africa/Dakar SO +0204+04522 Africa/Mogadishu SR +0550-05510 America/Paramaribo SS +0451+03137 Africa/Juba ST +0020+00644 Africa/Sao_Tome SV +1342-08912 America/El_Salvador SX +180305-0630250 America/Lower_Princes SY +3330+03618 Asia/Damascus SZ -2618+03106 Africa/Mbabane TC +2128-07108 America/Grand_Turk TD +1207+01503 Africa/Ndjamena TF -492110+0701303 Indian/Kerguelen TG +0608+00113 Africa/Lome TH +1345+10031 Asia/Bangkok TJ +3835+06848 Asia/Dushanbe TK -0922-17114 Pacific/Fakaofo TL -0833+12535 Asia/Dili TM +3757+05823 Asia/Ashgabat TN +3648+01011 Africa/Tunis TO -210800-1751200 Pacific/Tongatapu TR +4101+02858 Europe/Istanbul TT +1039-06131 America/Port_of_Spain TV -0831+17913 Pacific/Funafuti TW +2503+12130 Asia/Taipei TZ -0648+03917 Africa/Dar_es_Salaam UA +5026+03031 Europe/Kyiv most of Ukraine UG +0019+03225 Africa/Kampala UM +2813-17722 Pacific/Midway Midway Islands UM +1917+16637 Pacific/Wake Wake Island US +404251-0740023 America/New_York Eastern (most areas) US +421953-0830245 America/Detroit Eastern - MI (most areas) US +381515-0854534 America/Kentucky/Louisville Eastern - KY (Louisville area) US +364947-0845057 America/Kentucky/Monticello Eastern - KY (Wayne) US +394606-0860929 America/Indiana/Indianapolis Eastern - IN (most areas) US +384038-0873143 America/Indiana/Vincennes Eastern - IN (Da, Du, K, Mn) US +410305-0863611 America/Indiana/Winamac Eastern - IN (Pulaski) US +382232-0862041 America/Indiana/Marengo Eastern - IN (Crawford) US +382931-0871643 America/Indiana/Petersburg Eastern - IN (Pike) US +384452-0850402 America/Indiana/Vevay Eastern - IN (Switzerland) US +415100-0873900 America/Chicago Central (most areas) US +375711-0864541 America/Indiana/Tell_City Central - IN (Perry) US +411745-0863730 America/Indiana/Knox Central - IN (Starke) US +450628-0873651 America/Menominee Central - MI (Wisconsin border) US +470659-1011757 America/North_Dakota/Center Central - ND (Oliver) US +465042-1012439 America/North_Dakota/New_Salem Central - ND (Morton rural) US +471551-1014640 America/North_Dakota/Beulah Central - ND (Mercer) US +394421-1045903 America/Denver Mountain (most areas) US +433649-1161209 America/Boise Mountain - ID (south), OR (east) US +332654-1120424 America/Phoenix MST - AZ (except Navajo) US +340308-1181434 America/Los_Angeles Pacific US +611305-1495401 America/Anchorage Alaska (most areas) US +581807-1342511 America/Juneau Alaska - Juneau area US +571035-1351807 America/Sitka Alaska - Sitka area US +550737-1313435 America/Metlakatla Alaska - Annette Island US +593249-1394338 America/Yakutat Alaska - Yakutat US +643004-1652423 America/Nome Alaska (west) US +515248-1763929 America/Adak Alaska - western Aleutians US +211825-1575130 Pacific/Honolulu Hawaii UY -345433-0561245 America/Montevideo UZ +3940+06648 Asia/Samarkand Uzbekistan (west) UZ +4120+06918 Asia/Tashkent Uzbekistan (east) VA +415408+0122711 Europe/Vatican VC +1309-06114 America/St_Vincent VE +1030-06656 America/Caracas VG +1827-06437 America/Tortola VI +1821-06456 America/St_Thomas VN +1045+10640 Asia/Ho_Chi_Minh VU -1740+16825 Pacific/Efate WF -1318-17610 Pacific/Wallis WS -1350-17144 Pacific/Apia YE +1245+04512 Asia/Aden YT -1247+04514 Indian/Mayotte ZA -2615+02800 Africa/Johannesburg ZM -1525+02817 Africa/Lusaka ZW -1750+03103 Africa/Harare pr0m1th3as-datatypes-9450b29/inst/tzdata/zone1970.tab000066400000000000000000000422701527370255000221330ustar00rootroot00000000000000# tzdb timezone descriptions # # This file is in the public domain. # # From Paul Eggert (2025-05-15): # This file contains a table where each row stands for a timezone where # civil timestamps have agreed since 1970. Columns are separated by # a single tab. Lines beginning with ‘#’ are comments. All text uses # UTF-8 encoding. The columns of the table are as follows: # # 1. The countries that overlap the timezone, as a comma-separated list # of ISO 3166 2-character country codes. # 2. Latitude and longitude of the timezone’s principal location # in ISO 6709 sign-degrees-minutes-seconds format, # either ±DDMM±DDDMM or ±DDMMSS±DDDMMSS, # first latitude (+ is north), then longitude (+ is east). # 3. Timezone name used in value of TZ environment variable. # Please see the theory.html file for how these names are chosen. # If multiple timezones overlap a country, each has a row in the # table, with each column 1 containing the country code. # 4. Comments; present if and only if countries have multiple timezones, # and useful only for those countries. For example, the comments # for the row with countries CH,DE,LI and name Europe/Zurich # are useful only for DE, since CH and LI have no other timezones. # # If a timezone covers multiple countries, the most-populous city is used, # and that country is listed first in column 1; any other countries # are listed alphabetically by country code. The table is sorted # first by country code, then (if possible) by an order within the # country that (1) makes some geographical sense, and (2) puts the # most populous timezones first, where that does not contradict (1). # # This table is intended as an aid for users, to help them select timezones # appropriate for their practical needs. It is not intended to take or # endorse any position on legal or territorial claims. # #country- #codes coordinates TZ comments AD +4230+00131 Europe/Andorra AE,OM,RE,SC,TF +2518+05518 Asia/Dubai Crozet AF +3431+06912 Asia/Kabul AL +4120+01950 Europe/Tirane AM +4011+04430 Asia/Yerevan AQ -6617+11031 Antarctica/Casey Casey AQ -6835+07758 Antarctica/Davis Davis AQ -6736+06253 Antarctica/Mawson Mawson AQ -6448-06406 Antarctica/Palmer Palmer AQ -6734-06808 Antarctica/Rothera Rothera AQ -720041+0023206 Antarctica/Troll Troll AQ -7824+10654 Antarctica/Vostok Vostok AR -3436-05827 America/Argentina/Buenos_Aires Buenos Aires (BA, CF) AR -3124-06411 America/Argentina/Cordoba most areas: CB, CC, CN, ER, FM, MN, SE, SF AR -2447-06525 America/Argentina/Salta Salta (SA, LP, NQ, RN) AR -2411-06518 America/Argentina/Jujuy Jujuy (JY) AR -2649-06513 America/Argentina/Tucuman Tucumán (TM) AR -2828-06547 America/Argentina/Catamarca Catamarca (CT), Chubut (CH) AR -2926-06651 America/Argentina/La_Rioja La Rioja (LR) AR -3132-06831 America/Argentina/San_Juan San Juan (SJ) AR -3253-06849 America/Argentina/Mendoza Mendoza (MZ) AR -3319-06621 America/Argentina/San_Luis San Luis (SL) AR -5138-06913 America/Argentina/Rio_Gallegos Santa Cruz (SC) AR -5448-06818 America/Argentina/Ushuaia Tierra del Fuego (TF) AS,UM -1416-17042 Pacific/Pago_Pago Midway AT +4813+01620 Europe/Vienna AU -3133+15905 Australia/Lord_Howe Lord Howe Island AU -5430+15857 Antarctica/Macquarie Macquarie Island AU -4253+14719 Australia/Hobart Tasmania AU -3749+14458 Australia/Melbourne Victoria AU -3352+15113 Australia/Sydney New South Wales (most areas) AU -3157+14127 Australia/Broken_Hill New South Wales (Yancowinna) AU -2728+15302 Australia/Brisbane Queensland (most areas) AU -2016+14900 Australia/Lindeman Queensland (Whitsunday Islands) AU -3455+13835 Australia/Adelaide South Australia AU -1228+13050 Australia/Darwin Northern Territory AU -3157+11551 Australia/Perth Western Australia (most areas) AU -3143+12852 Australia/Eucla Western Australia (Eucla) AZ +4023+04951 Asia/Baku BB +1306-05937 America/Barbados BD +2343+09025 Asia/Dhaka BE,LU,NL +5050+00420 Europe/Brussels BG +4241+02319 Europe/Sofia BM +3217-06446 Atlantic/Bermuda BO -1630-06809 America/La_Paz BR -0351-03225 America/Noronha Atlantic islands BR -0127-04829 America/Belem Pará (east), Amapá BR -0343-03830 America/Fortaleza Brazil (northeast: MA, PI, CE, RN, PB) BR -0803-03454 America/Recife Pernambuco BR -0712-04812 America/Araguaina Tocantins BR -0940-03543 America/Maceio Alagoas, Sergipe BR -1259-03831 America/Bahia Bahia BR -2332-04637 America/Sao_Paulo Brazil (southeast: GO, DF, MG, ES, RJ, SP, PR, SC, RS) BR -2027-05437 America/Campo_Grande Mato Grosso do Sul BR -1535-05605 America/Cuiaba Mato Grosso BR -0226-05452 America/Santarem Pará (west) BR -0846-06354 America/Porto_Velho Rondônia BR +0249-06040 America/Boa_Vista Roraima BR -0308-06001 America/Manaus Amazonas (east) BR -0640-06952 America/Eirunepe Amazonas (west) BR -0958-06748 America/Rio_Branco Acre BT +2728+08939 Asia/Thimphu BY +5354+02734 Europe/Minsk BZ +1730-08812 America/Belize CA +4734-05243 America/St_Johns Newfoundland, Labrador (SE) CA +4439-06336 America/Halifax Atlantic - NS (most areas), PE CA +4612-05957 America/Glace_Bay Atlantic - NS (Cape Breton) CA +4606-06447 America/Moncton Atlantic - New Brunswick CA +5320-06025 America/Goose_Bay Atlantic - Labrador (most areas) CA,BS +4339-07923 America/Toronto Eastern - ON & QC (most areas) CA +6344-06828 America/Iqaluit Eastern - NU (most areas) CA +4953-09709 America/Winnipeg EST - Manitoba, ON (northwest) CA +744144-0944945 America/Resolute Central - NU (Resolute) CA +624900-0920459 America/Rankin_Inlet Central - NU (central) CA +5024-10439 America/Regina CST - SK (most areas) CA +5017-10750 America/Swift_Current CST - SK (midwest) CA +5333-11328 America/Edmonton CST - AB, BC(E), NT(E), SK(W) CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west) CA +682059-1334300 America/Inuvik CST - NT (west) CA +4916-12307 America/Vancouver MST - BC (most areas) CA +5546-12014 America/Dawson_Creek MST - BC (Dawson Cr, Ft St John) CA +5848-12242 America/Fort_Nelson MST - BC (Ft Nelson) CA +6043-13503 America/Whitehorse MST - Yukon (east) CA +6404-13925 America/Dawson MST - Yukon (west) CH,DE,LI +4723+00832 Europe/Zurich Büsingen CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG +0519-00402 Africa/Abidjan CK -2114-15946 Pacific/Rarotonga CL -3327-07040 America/Santiago most of Chile CL -4534-07204 America/Coyhaique Aysén Region CL -5309-07055 America/Punta_Arenas Magallanes Region CL -2709-10926 Pacific/Easter Easter Island CN +3114+12128 Asia/Shanghai Beijing Time CN +4348+08735 Asia/Urumqi Xinjiang Time CO +0436-07405 America/Bogota CR +0956-08405 America/Costa_Rica CU +2308-08222 America/Havana CV +1455-02331 Atlantic/Cape_Verde CY +3510+03322 Asia/Nicosia most of Cyprus CY +3507+03357 Asia/Famagusta Northern Cyprus CZ,SK +5005+01426 Europe/Prague DE,DK,NO,SE,SJ +5230+01322 Europe/Berlin most of Germany DO +1828-06954 America/Santo_Domingo DZ +3647+00303 Africa/Algiers EC -0210-07950 America/Guayaquil Ecuador (mainland) EC -0054-08936 Pacific/Galapagos Galápagos Islands EE +5925+02445 Europe/Tallinn EG +3003+03115 Africa/Cairo EH +2709-01312 Africa/El_Aaiun ES +4024-00341 Europe/Madrid Spain (mainland) ES +3553-00519 Africa/Ceuta Ceuta, Melilla ES +2806-01524 Atlantic/Canary Canary Islands FI,AX +6010+02458 Europe/Helsinki FJ -1808+17825 Pacific/Fiji FK -5142-05751 Atlantic/Stanley FM +0519+16259 Pacific/Kosrae Kosrae FO +6201-00646 Atlantic/Faroe FR,MC +4852+00220 Europe/Paris GB,GG,IM,JE +513030-0000731 Europe/London GE +4143+04449 Asia/Tbilisi GF +0456-05220 America/Cayenne GI +3608-00521 Europe/Gibraltar GL +6411-05144 America/Nuuk most of Greenland GL +7646-01840 America/Danmarkshavn National Park (east coast) GL +7029-02158 America/Scoresbysund Scoresbysund/Ittoqqortoormiit GL +7634-06847 America/Thule Thule/Pituffik GR +3758+02343 Europe/Athens GS -5416-03632 Atlantic/South_Georgia GT +1438-09031 America/Guatemala GU,MP +1328+14445 Pacific/Guam GW +1151-01535 Africa/Bissau GY +0648-05810 America/Guyana HK +2217+11409 Asia/Hong_Kong HN +1406-08713 America/Tegucigalpa HT +1832-07220 America/Port-au-Prince HU +4730+01905 Europe/Budapest ID -0610+10648 Asia/Jakarta Java, Sumatra ID -0002+10920 Asia/Pontianak Borneo (west, central) ID -0507+11924 Asia/Makassar Borneo (east, south), Sulawesi/Celebes, Bali, Nusa Tengarra, Timor (west) ID -0232+14042 Asia/Jayapura New Guinea (West Papua / Irian Jaya), Malukus/Moluccas IE +5320-00615 Europe/Dublin IL +314650+0351326 Asia/Jerusalem IN +2232+08822 Asia/Kolkata IO -0720+07225 Indian/Chagos IQ +3321+04425 Asia/Baghdad IR +3540+05126 Asia/Tehran IT,SM,VA +4154+01229 Europe/Rome JM +175805-0764736 America/Jamaica JO +3157+03556 Asia/Amman JP,AU +353916+1394441 Asia/Tokyo Eyre Bird Observatory KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT -0117+03649 Africa/Nairobi KG +4254+07436 Asia/Bishkek KI,MH,TV,UM,WF +0125+17300 Pacific/Tarawa Gilberts, Marshalls, Wake KI -0247-17143 Pacific/Kanton Phoenix Islands KI +0152-15720 Pacific/Kiritimati Line Islands KP +3901+12545 Asia/Pyongyang KR +3733+12658 Asia/Seoul KZ +4315+07657 Asia/Almaty most of Kazakhstan KZ +4448+06528 Asia/Qyzylorda Qyzylorda/Kyzylorda/Kzyl-Orda KZ +5312+06337 Asia/Qostanay Qostanay/Kostanay/Kustanay KZ +5017+05710 Asia/Aqtobe Aqtöbe/Aktobe KZ +4431+05016 Asia/Aqtau Mangghystaū/Mankistau KZ +4707+05156 Asia/Atyrau Atyraū/Atirau/Gur’yev KZ +5113+05121 Asia/Oral West Kazakhstan LB +3353+03530 Asia/Beirut LK +0656+07951 Asia/Colombo LR +0618-01047 Africa/Monrovia LT +5441+02519 Europe/Vilnius LV +5657+02406 Europe/Riga LY +3254+01311 Africa/Tripoli MA +3339-00735 Africa/Casablanca MD +4700+02850 Europe/Chisinau MH +0905+16720 Pacific/Kwajalein Kwajalein MM,CC +1647+09610 Asia/Yangon MN +4755+10653 Asia/Ulaanbaatar most of Mongolia MN +4801+09139 Asia/Hovd Bayan-Ölgii, Hovd, Uvs MO +221150+1133230 Asia/Macau MQ +1436-06105 America/Martinique MT +3554+01431 Europe/Malta MU -2010+05730 Indian/Mauritius MV,TF +0410+07330 Indian/Maldives Kerguelen, St Paul I, Amsterdam I MX +1924-09909 America/Mexico_City Central Mexico MX +2105-08646 America/Cancun Quintana Roo MX +2058-08937 America/Merida Campeche, Yucatán MX +2540-10019 America/Monterrey Durango; Coahuila, Nuevo León, Tamaulipas (most areas) MX +2550-09730 America/Matamoros Coahuila, Nuevo León, Tamaulipas (US border) MX +2838-10605 America/Chihuahua Chihuahua (most areas) MX +3144-10629 America/Ciudad_Juarez Chihuahua (US border - west) MX +2934-10425 America/Ojinaga Chihuahua (US border - east) MX +2313-10625 America/Mazatlan Baja California Sur, Nayarit (most areas), Sinaloa MX +2048-10515 America/Bahia_Banderas Bahía de Banderas MX +2904-11058 America/Hermosillo Sonora MX +3232-11701 America/Tijuana Baja California MY,BN +0133+11020 Asia/Kuching Sabah, Sarawak MZ,BI,BW,CD,MW,RW,ZM,ZW -2558+03235 Africa/Maputo Central Africa Time NA -2234+01706 Africa/Windhoek NC -2216+16627 Pacific/Noumea NF -2903+16758 Pacific/Norfolk NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE +0627+00324 Africa/Lagos West Africa Time NI +1209-08617 America/Managua NP +2743+08519 Asia/Kathmandu NR -0031+16655 Pacific/Nauru NU -1901-16955 Pacific/Niue NZ,AQ -3652+17446 Pacific/Auckland New Zealand time NZ -4357-17633 Pacific/Chatham Chatham Islands PA,CA,KY +0858-07932 America/Panama EST - ON (Atikokan), NU (Coral H) PE -1203-07703 America/Lima PF -1732-14934 Pacific/Tahiti Society Islands PF -0900-13930 Pacific/Marquesas Marquesas Islands PF -2308-13457 Pacific/Gambier Gambier Islands PG,AQ,FM -0930+14710 Pacific/Port_Moresby Papua New Guinea (most areas), Chuuk, Yap, Dumont d’Urville PG -0613+15534 Pacific/Bougainville Bougainville PH +143512+1205804 Asia/Manila PK +2452+06703 Asia/Karachi PL +5215+02100 Europe/Warsaw PM +4703-05620 America/Miquelon PN -2504-13005 Pacific/Pitcairn PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI +182806-0660622 America/Puerto_Rico AST - QC (Lower North Shore) PS +3130+03428 Asia/Gaza Gaza Strip PS +313200+0350542 Asia/Hebron West Bank PT +3843-00908 Europe/Lisbon Portugal (mainland) PT +3238-01654 Atlantic/Madeira Madeira Islands PT +3744-02540 Atlantic/Azores Azores PW +0720+13429 Pacific/Palau PY -2516-05740 America/Asuncion QA,BH +2517+05132 Asia/Qatar RO +4426+02606 Europe/Bucharest RS,BA,HR,ME,MK,SI +4450+02030 Europe/Belgrade RU +5443+02030 Europe/Kaliningrad MSK-01 - Kaliningrad RU +554521+0373704 Europe/Moscow MSK+00 - Moscow area # Mention RU and UA alphabetically. See “territorial claims� above. RU,UA +4457+03406 Europe/Simferopol Crimea RU +5836+04939 Europe/Kirov MSK+00 - Kirov RU +4844+04425 Europe/Volgograd MSK+00 - Volgograd RU +4621+04803 Europe/Astrakhan MSK+01 - Astrakhan RU +5134+04602 Europe/Saratov MSK+01 - Saratov RU +5420+04824 Europe/Ulyanovsk MSK+01 - Ulyanovsk RU +5312+05009 Europe/Samara MSK+01 - Samara, Udmurtia RU +5651+06036 Asia/Yekaterinburg MSK+02 - Urals RU +5500+07324 Asia/Omsk MSK+03 - Omsk RU +5502+08255 Asia/Novosibirsk MSK+04 - Novosibirsk RU +5322+08345 Asia/Barnaul MSK+04 - Altai RU +5630+08458 Asia/Tomsk MSK+04 - Tomsk RU +5345+08707 Asia/Novokuznetsk MSK+04 - Kemerovo RU +5601+09250 Asia/Krasnoyarsk MSK+04 - Krasnoyarsk area RU +5216+10420 Asia/Irkutsk MSK+05 - Irkutsk, Buryatia RU +5203+11328 Asia/Chita MSK+06 - Zabaykalsky RU +6200+12940 Asia/Yakutsk MSK+06 - Lena River RU +623923+1353314 Asia/Khandyga MSK+06 - Tomponsky, Ust-Maysky RU +4310+13156 Asia/Vladivostok MSK+07 - Amur River RU +643337+1431336 Asia/Ust-Nera MSK+07 - Oymyakonsky RU +5934+15048 Asia/Magadan MSK+08 - Magadan RU +4658+14242 Asia/Sakhalin MSK+08 - Sakhalin Island RU +6728+15343 Asia/Srednekolymsk MSK+08 - Sakha (E), N Kuril Is RU +5301+15839 Asia/Kamchatka MSK+09 - Kamchatka RU +6445+17729 Asia/Anadyr MSK+09 - Bering Sea SA,AQ,KW,YE +2438+04643 Asia/Riyadh Syowa SB,FM -0932+16012 Pacific/Guadalcanal Pohnpei SD +1536+03232 Africa/Khartoum SG,AQ,MY +0117+10351 Asia/Singapore peninsular Malaysia, Concordia SR +0550-05510 America/Paramaribo SS +0451+03137 Africa/Juba ST +0020+00644 Africa/Sao_Tome SV +1342-08912 America/El_Salvador SY +3330+03618 Asia/Damascus TC +2128-07108 America/Grand_Turk TD +1207+01503 Africa/Ndjamena TH,CX,KH,LA,VN +1345+10031 Asia/Bangkok north Vietnam TJ +3835+06848 Asia/Dushanbe TK -0922-17114 Pacific/Fakaofo TL -0833+12535 Asia/Dili TM +3757+05823 Asia/Ashgabat TN +3648+01011 Africa/Tunis TO -210800-1751200 Pacific/Tongatapu TR +4101+02858 Europe/Istanbul TW +2503+12130 Asia/Taipei UA +5026+03031 Europe/Kyiv most of Ukraine US +404251-0740023 America/New_York Eastern (most areas) US +421953-0830245 America/Detroit Eastern - MI (most areas) US +381515-0854534 America/Kentucky/Louisville Eastern - KY (Louisville area) US +364947-0845057 America/Kentucky/Monticello Eastern - KY (Wayne) US +394606-0860929 America/Indiana/Indianapolis Eastern - IN (most areas) US +384038-0873143 America/Indiana/Vincennes Eastern - IN (Da, Du, K, Mn) US +410305-0863611 America/Indiana/Winamac Eastern - IN (Pulaski) US +382232-0862041 America/Indiana/Marengo Eastern - IN (Crawford) US +382931-0871643 America/Indiana/Petersburg Eastern - IN (Pike) US +384452-0850402 America/Indiana/Vevay Eastern - IN (Switzerland) US +415100-0873900 America/Chicago Central (most areas) US +375711-0864541 America/Indiana/Tell_City Central - IN (Perry) US +411745-0863730 America/Indiana/Knox Central - IN (Starke) US +450628-0873651 America/Menominee Central - MI (Wisconsin border) US +470659-1011757 America/North_Dakota/Center Central - ND (Oliver) US +465042-1012439 America/North_Dakota/New_Salem Central - ND (Morton rural) US +471551-1014640 America/North_Dakota/Beulah Central - ND (Mercer) US +394421-1045903 America/Denver Mountain (most areas) US +433649-1161209 America/Boise Mountain - ID (south), OR (east) US,CA +332654-1120424 America/Phoenix MST - AZ (most areas), Creston BC US +340308-1181434 America/Los_Angeles Pacific US +611305-1495401 America/Anchorage Alaska (most areas) US +581807-1342511 America/Juneau Alaska - Juneau area US +571035-1351807 America/Sitka Alaska - Sitka area US +550737-1313435 America/Metlakatla Alaska - Annette Island US +593249-1394338 America/Yakutat Alaska - Yakutat US +643004-1652423 America/Nome Alaska (west) US +515248-1763929 America/Adak Alaska - western Aleutians US +211825-1575130 Pacific/Honolulu Hawaii UY -345433-0561245 America/Montevideo UZ +3940+06648 Asia/Samarkand Uzbekistan (west) UZ +4120+06918 Asia/Tashkent Uzbekistan (east) VE +1030-06656 America/Caracas VN +1045+10640 Asia/Ho_Chi_Minh south Vietnam VU -1740+16825 Pacific/Efate WS -1350-17144 Pacific/Apia ZA,LS,SZ -2615+02800 Africa/Johannesburg # # The next section contains experimental tab-separated comments for # use by user agents like tzselect that identify continents and oceans. # # For example, the comment ‘#@AQAntarctica/’ means the country code # AQ is in the continent Antarctica regardless of the Zone name, # so Pacific/Auckland should be listed under Antarctica as well as # under the Pacific because its line’s country codes include AQ. # # If more than one country code is affected each is listed separated # by commas, e.g., ‘#@IS,SHAtlantic/’. If a country code is in # more than one continent or ocean, each is listed separated by # commas, e.g., the second column of ‘#@CY,TRAsia/,Europe/’. # # These experimental comments are present only for country codes where # the continent or ocean is not already obvious from the Zone name. # For example, there is no such comment for RU since it already # corresponds to Zone names starting with both ‘Europe/’ and ‘Asia/’. # #@AQ Antarctica/ #@IS,SH Atlantic/ #@CY,TR Asia/,Europe/ #@SJ Arctic/ #@CC,CX,KM,MG,YT Indian/ pr0m1th3as-datatypes-9450b29/inst/tzdata/zonenow.tab000066400000000000000000000203511527370255000223320ustar00rootroot00000000000000# tzdb timezone descriptions, for users who do not care about old timestamps # # This file is in the public domain. # # From Paul Eggert (2023-12-18): # This file contains a table where each row stands for a timezone # where civil timestamps are predicted to agree from now on. # This file is like zone1970.tab (see zone1970.tab’s comments), # but with the following changes: # # 1. Each timezone corresponds to a set of clocks that are planned # to agree from now on. This is a larger set of clocks than in # zone1970.tab, where each timezone’s clocks must agree from 1970 on. # 2. The first column is irrelevant and ignored. # 3. The table is sorted in a different way: # first by standard time UTC offset; # then, if DST is used, by daylight saving UTC offset; # then by time zone abbreviation. # 4. Every timezone has a nonempty comments column, with wording # distinguishing the timezone only from other timezones with the # same UTC offset at some point during the year. # # The format of this table is experimental, and may change in future versions. # # This table is intended as an aid for users, to help them select timezones # appropriate for their practical needs. It is not intended to take or # endorse any position on legal or territorial claims. # #XX coordinates TZ comments # # -11 - SST XX -1416-17042 Pacific/Pago_Pago Midway; Samoa (SST) # # -11 XX -1901-16955 Pacific/Niue Niue # # -10 - HST XX +211825-1575130 Pacific/Honolulu Hawaii (HST) # # -10 XX -1732-14934 Pacific/Tahiti Tahiti; Cook Islands # # -10/-09 - HST / HDT (North America DST) XX +515248-1763929 America/Adak western Aleutians in Alaska (HST/HDT) # # -09:30 XX -0900-13930 Pacific/Marquesas Marquesas # # -09 XX -2308-13457 Pacific/Gambier Gambier # # -09/-08 - AKST/AKDT (North America DST) XX +611305-1495401 America/Anchorage most of Alaska (AKST/AKDT) # # -08 XX -2504-13005 Pacific/Pitcairn Pitcairn # # -08/-07 - PST/PDT (North America DST) XX +340308-1181434 America/Los_Angeles Pacific (PST/PDT) - US; Mexico near US border # # -08/-07 - PST/PDT (North America DST) until 2026-11-01 02:00; then MST XX +4916-12307 America/Vancouver Mountain Standard (MST) - British Columbia (most areas) # # -07 - MST XX +332654-1120424 America/Phoenix Mountain Standard (MST) - Arizona; western Mexico; Yukon # # -07/-06 - MST/MDT (North America DST) XX +394421-1045903 America/Denver Mountain (MST/MDT) - US; Mexico near US border; western Nunavut # # -07/-06 - MST/MDT (North America DST) until 2026-11-01 02:00; then CST XX +5333-11328 America/Edmonton Central Standard (CST) - Alberta, NWT, and some neighbors # # -06 XX -0054-08936 Pacific/Galapagos Galápagos # # -06 - CST XX +1924-09909 America/Mexico_City Central Standard (CST) - Saskatchewan; central Mexico; Central America # # -06/-05 (Chile DST) XX -2709-10926 Pacific/Easter Easter Island # # -06/-05 - CST/CDT (North America DST) XX +415100-0873900 America/Chicago Central (CST/CDT) - US; Mexico near US border; central Nunavut # # -06/-05 - CST/CDT (North America DST) until 2026-11-01 02:00; then EST XX +4953-09709 America/Winnipeg Eastern Standard (EST) - Manitoba, northwestern Ontario # # -05 XX -1203-07703 America/Lima eastern South America # # -05 - EST XX +175805-0764736 America/Jamaica Eastern Standard (EST) - Caymans; Jamaica; eastern Mexico; Panama # # -05/-04 - CST/CDT (Cuba DST) XX +2308-08222 America/Havana Cuba # # -05/-04 - EST/EDT (North America DST) XX +404251-0740023 America/New_York Eastern (EST/EDT) - US & Canada # # -04 XX +1030-06656 America/Caracas western South America # # -04 - AST XX +1828-06954 America/Santo_Domingo Atlantic Standard (AST) - eastern Caribbean # # -04/-03 (Chile DST) XX -3327-07040 America/Santiago most of Chile # # -04/-03 - AST/ADT (North America DST) XX +4439-06336 America/Halifax Atlantic (AST/ADT) - Canada; Bermuda # # -03:30/-02:30 - NST/NDT (North America DST) XX +4734-05243 America/St_Johns Newfoundland (NST/NDT) # # -03 XX -2332-04637 America/Sao_Paulo eastern and southern South America # # -03/-02 (North America DST) XX +4703-05620 America/Miquelon St Pierre & Miquelon # # -02 XX -0351-03225 America/Noronha Fernando de Noronha; South Georgia # # -02/-01 (EU DST) XX +6411-05144 America/Nuuk most of Greenland # # -01 XX +1455-02331 Atlantic/Cape_Verde Cape Verde # # -01/+00 (EU DST) XX +3744-02540 Atlantic/Azores Azores # # +00 - GMT XX +0519-00402 Africa/Abidjan far western Africa; Iceland (GMT) # # +00/+01 - GMT/BST (EU DST) XX +513030-0000731 Europe/London United Kingdom (GMT/BST) # # +00/+01 - WET/WEST (EU DST) XX +3843-00908 Europe/Lisbon western Europe (WET/WEST) # # +00/+02 - Troll DST XX -720041+0023206 Antarctica/Troll Troll Station in Antarctica # # +01 - CET XX +3647+00303 Africa/Algiers Algeria, Tunisia (CET) # # +01 - WAT XX +0627+00324 Africa/Lagos western Africa (WAT) # # +01/+00 - IST/GMT (EU DST in reverse) XX +5320-00615 Europe/Dublin Ireland (IST/GMT) # # +01/+00 - (Morocco DST) XX +3339-00735 Africa/Casablanca Morocco # # +01/+02 - CET/CEST (EU DST) XX +4852+00220 Europe/Paris central Europe (CET/CEST) # # +02 - CAT XX -2558+03235 Africa/Maputo central Africa (CAT) # # +02 - EET XX +3254+01311 Africa/Tripoli Libya; Kaliningrad (EET) # # +02 - SAST XX -2615+02800 Africa/Johannesburg southern Africa (SAST) # # +02/+03 - EET/EEST (EU DST) XX +3758+02343 Europe/Athens eastern Europe (EET/EEST) # # +02/+03 - EET/EEST (Egypt DST) XX +3003+03115 Africa/Cairo Egypt # # +02/+03 - EET/EEST (Lebanon DST) XX +3353+03530 Asia/Beirut Lebanon # # +02/+03 - EET/EEST (Palestine DST) XX +3130+03428 Asia/Gaza Palestine # # +02/+03 - IST/IDT (Israel DST) XX +314650+0351326 Asia/Jerusalem Israel # # +03 XX +4101+02858 Europe/Istanbul Near East; Belarus # # +03 - EAT XX -0117+03649 Africa/Nairobi eastern Africa (EAT) # # +03 - MSK XX +554521+0373704 Europe/Moscow Moscow (MSK) # # +03:30 XX +3540+05126 Asia/Tehran Iran # # +04 XX +2518+05518 Asia/Dubai Russia; Caucasus; Persian Gulf; Seychelles; Réunion # # +04:30 XX +3431+06912 Asia/Kabul Afghanistan # # +05 XX +4120+06918 Asia/Tashkent Russia; Kazakhstan; Tajikistan; Turkmenistan; Uzbekistan; Maldives # # +05 - PKT XX +2452+06703 Asia/Karachi Pakistan (PKT) # # +05:30 XX +0656+07951 Asia/Colombo Sri Lanka # # +05:30 - IST XX +2232+08822 Asia/Kolkata India (IST) # # +05:45 XX +2743+08519 Asia/Kathmandu Nepal # # +06 XX +2343+09025 Asia/Dhaka Russia; Kyrgyzstan; Bhutan; Bangladesh; Chagos # # +06:30 XX +1647+09610 Asia/Yangon Myanmar; Cocos # # +07 XX +1345+10031 Asia/Bangkok Russia; Indochina; Christmas Island # # +07 - WIB XX -0610+10648 Asia/Jakarta Indonesia (WIB) # # +08 XX +0117+10351 Asia/Singapore Russia; Brunei; Malaysia; Singapore; Concordia # # +08 - AWST XX -3157+11551 Australia/Perth Western Australia (AWST) # # +08 - CST XX +3114+12128 Asia/Shanghai China (CST) # # +08 - HKT XX +2217+11409 Asia/Hong_Kong Hong Kong (HKT) # # +08 - PHT XX +143512+1205804 Asia/Manila Philippines (PHT) # # +08 - WITA XX -0507+11924 Asia/Makassar Indonesia (WITA) # # +08:45 XX -3143+12852 Australia/Eucla Eucla # # +09 XX +5203+11328 Asia/Chita Russia; Palau; East Timor # # +09 - JST XX +353916+1394441 Asia/Tokyo Japan (JST); Eyre Bird Observatory # # +09 - KST XX +3733+12658 Asia/Seoul Korea (KST) # # +09 - WIT XX -0232+14042 Asia/Jayapura Indonesia (WIT) # # +09:30 - ACST XX -1228+13050 Australia/Darwin Northern Territory (ACST) # # +09:30/+10:30 - ACST/ACDT (Australia DST) XX -3455+13835 Australia/Adelaide South Australia (ACST/ACDT) # # +10 XX +4310+13156 Asia/Vladivostok Russia; Yap; Chuuk; Papua New Guinea; Dumont d’Urville # # +10 - AEST XX -2728+15302 Australia/Brisbane Queensland (AEST) # # +10 - ChST XX +1328+14445 Pacific/Guam Mariana Islands (ChST) # # +10/+11 - AEST/AEDT (Australia DST) XX -3352+15113 Australia/Sydney southeast Australia (AEST/AEDT) # # +10:30/+11 XX -3133+15905 Australia/Lord_Howe Lord Howe Island # # +11 XX -0613+15534 Pacific/Bougainville Russia; Kosrae; Bougainville; Solomons # # +11/+12 (Australia DST) XX -2903+16758 Pacific/Norfolk Norfolk Island # # +12 XX +5301+15839 Asia/Kamchatka Russia; Tuvalu; Fiji; etc. # # +12/+13 (New Zealand DST) XX -3652+17446 Pacific/Auckland New Zealand (NZST/NZDT) # # +12:45/+13:45 (Chatham DST) XX -4357-17633 Pacific/Chatham Chatham Islands # # +13 XX -210800-1751200 Pacific/Tongatapu Kanton; Tokelau; Samoa (western); Tonga # # +14 XX +0152-15720 Pacific/Kiritimati Kiritimati pr0m1th3as-datatypes-9450b29/inst/vartype.m000066400000000000000000000112011527370255000205160ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef vartype ## -*- texinfo -*- ## @deftp {datatypes} vartype ## ## Subscript into a table by variable type. ## ## A utility class that facilitates subscripting table variables according to ## their data type. ## ## @seealso{table} ## @end deftp properties (SetAccess = private, Hidden) ## Type of data type endproperties methods (Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif disp (this); endfunction ## Custom display function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif fprintf ("\n table vartype subscript:\n\n"); fprintf (" Select table variables matching the type '%s'\n\n", ... this.type); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {vartype} {@var{S} =} vartype (@var{type}) ## ## Create a subscript into table by variable type. ## ## @code{@var{S} = vartype (@var{type})} creates a subscript to select table ## variables of a specified type. The input argument, @var{type}, must be a ## character vector or a string scalar that specifies any type that is ## accepted by the @code{isa} function, such as @code{numeric}, ## @code{logical}, @code{integer}, @code{string}, @code{categorical}, etc. ## It can also be @code{cellstr} to select variables that contain cell ## arrays of character vectors or @code{numeric} to select variables with ## numeric values. ## ## @end deftypefn function this = vartype (type) if (! ((isvector (type) && ischar (type)) || isa (type, "string"))) error (strcat ("vartype: TYPE must be either a character", ... " vector or a string scalar.")); endif this.type = char (cellstr (type)); endfunction ## -*- texinfo -*- ## @deftypefn {vartype} {@var{TF} =} varMatch (@var{obj}, @var{value}) ## ## Match a @qcode{vartype} object to a variable value. ## ## @code{@var{TF} = varMatch (@var{obj}, @var{value})} compares the class ## type of the variable in @var{value} with the predefined class type in the ## @qcode{vartype} object. If they are equal, @var{TF} is @qcode{true}, ## otherwise @var{TF} is @qcode{false}. ## ## @end deftypefn function TF = varMatch (this, varVal) if (isequal (this.type, 'cellstr')) TF = iscellstr (varVal); elseif (isequal (this.type, 'numeric')) TF = isnumeric (varVal); else TF = isa (varVal, this.type); endif endfunction endmethods endclassdef ## Test output %!test %! S = vartype ('cellstr'); %! assert_equal (isa (S, "vartype"), true); %!test %! S = vartype ('cellstr'); %! assert_equal (S.varMatch ({2343}), false); %! assert_equal (S.varMatch ({'as'}), true); %!test %! S = vartype ('string'); %! assert_equal (S.varMatch (string ('as')), true); %! assert_equal (S.varMatch ({'as'}), false); %!test %! S = vartype ('single'); %! assert_equal (S.varMatch (34.5), false); %! assert_equal (S.varMatch (single (34.5)), true); %!test %! S = vartype ('numeric'); %! assert_equal (S.varMatch (int8 (34)), true); %! assert_equal (S.varMatch (single (34.5)), true); %!test %! S = vartype ('duration'); %! assert_equal (S.varMatch (int8 (34)), false); %! assert_equal (S.varMatch (calweeks (3)), false); %! assert_equal (S.varMatch (hours (12)), true); %!test %! S = vartype ('calendarDuration'); %! assert_equal (S.varMatch ('char'), false); %! assert_equal (S.varMatch (calweeks (3)), true); %! assert_equal (S.varMatch (hours (12)), false); ## Test input validation %!error ... %! vartype (3) pr0m1th3as-datatypes-9450b29/inst/withtol.m000066400000000000000000000166561527370255000205410ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . classdef withtol ## -*- texinfo -*- ## @deftp {datatypes} withtol ## ## Subscript into a timetable by times matched within a tolerance. ## ## A utility class that selects the rows of a timetable whose times fall ## within a tolerance of the times asked for. It exists because a row time ## is rarely known to the last microsecond, so asking for one exactly would ## usually select nothing. ## ## @seealso{timerange, timetable} ## @end deftp properties (SetAccess = private, Hidden) ## The times to match subscriptTimes ## Half-width of the window around each of them tolerance endproperties methods (Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif disp (this); endfunction ## Custom display function str = disp (this) ## With an output, the text it would print, as Octave's disp gives if (nargout > 0) str = evalc ('disp (this)'); return; endif fprintf (" withtol subscript: %d time(s), tolerance %s\n\n", ... numel (this.subscriptTimes), cellstr (this.tolerance){1}); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {withtol} {@var{wt} =} withtol (@var{subscriptTimes}, @var{tol}) ## ## Create a tolerant time subscript. ## ## @code{@var{wt} = withtol (@var{subscriptTimes}, @var{tol})} creates a ## subscript selecting the rows of a timetable whose times are within ## @var{tol} of one of @var{subscriptTimes}. The window is closed: a row ## exactly @var{tol} away is selected. ## ## @var{subscriptTimes} may be a @code{datetime} or @code{duration} ## vector, or text that reads as one. @var{tol} must be a nonnegative ## @code{duration} scalar; a bare number is not accepted, there being ## nothing to say what unit it is in. ## ## The tolerance must be less than half the smallest gap between the ## times asked for, so that their windows cannot overlap and no row can ## be selected twice. ## ## @seealso{timerange, timetable} ## @end deftypefn function this = withtol (subscriptTimes, tol) if (nargin != 2) print_usage (); endif if (! (isduration (tol) && isscalar (tol))) error (strcat ("withtol: TOL must be a duration scalar; a number", ... " says nothing about the unit it is in.")); endif if (! (seconds (tol) >= 0)) error ("withtol: TOL must be nonnegative."); endif if (isa (subscriptTimes, 'string') || ischar (subscriptTimes) || iscellstr (subscriptTimes)) subscriptTimes = cellstr (subscriptTimes); elseif (! (isdatetime (subscriptTimes) || isduration (subscriptTimes))) error (strcat ("withtol: SUBSCRIPTTIMES must be a datetime or", ... " duration vector, or text naming times.")); endif ## Windows that overlap could select a row twice, so the tolerance is ## bounded by half the closest the asked-for times come to each other. if (! iscellstr (subscriptTimes) && numel (subscriptTimes) > 1) gaps = abs (diff (sort (subscriptTimes(:)))); least = min (seconds (gaps)); if (! (seconds (tol) < least / 2)) error (strcat ("withtol: TOL must be less than half the", ... " smallest gap between the subscript times, %s,", ... " or a row could be selected twice."), ... cellstr (gaps(1) - gaps(1) + seconds (least / 2)){1}); endif endif this.subscriptTimes = subscriptTimes; this.tolerance = tol; endfunction ## -*- texinfo -*- ## @deftypefn {withtol} {@var{ix} =} rowIndices (@var{wt}, @var{rowTimes}) ## ## Return the positions in @var{rowTimes} the subscript selects. ## ## The positions come back grouped by the time that matched them, in the ## order those times were asked for, rather than in the order the rows ## are held. ## ## @end deftypefn function ix = rowIndices (this, rowTimes) subs = this.subscriptTimes; if (iscellstr (subs)) if (isdatetime (rowTimes)) subs = datetime (subs); subs.TimeZone = rowTimes.TimeZone; else subs = duration (subs); endif elseif (! strcmp (class (subs), class (rowTimes))) error (strcat ("withtol: a timetable with %s row times cannot be", ... " subscripted with %s times."), ... class (rowTimes), class (subs)); endif ix = []; for i = 1:numel (subs) gap = abs (seconds (rowTimes - subs(i))); ix = [ix; find(gap <= seconds (this.tolerance))]; endfor endfunction endmethods endclassdef ## Test the tolerance window is closed at both ends %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! assert_equal (rowIndices (withtol (tv(3), hours (1)), tv), [2; 3; 4]); %! assert_equal (rowIndices (withtol (tv(3), hours (1) - seconds (1)), tv), 3); %! assert_equal (rowIndices (withtol (tv(3), seconds (0)), tv), 3); ## Test the rows come back in the order the times were asked for %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! assert_equal (rowIndices (withtol (tv([2 5]), minutes (1)), tv), [2; 5]); %! assert_equal (rowIndices (withtol (tv([5 2]), minutes (1)), tv), [5; 2]); ## Test a time that matches nothing selects nothing %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! assert_equal (rowIndices (withtol (tv(1) - hours (9), minutes (1)), tv), ... %! zeros (0, 1)); ## Test text times and duration row times %!test %! tv = datetime (2024, 1, 1) + hours (0:5)'; %! assert_equal (rowIndices (withtol ('01-Jan-2024 02:00:00', ... %! minutes (1)), tv), 3); %! dv = hours (0:5)'; %! assert_equal (rowIndices (withtol (dv(3), minutes (1)), dv), 3); %!error ... %! withtol (datetime (2024, 1, 1), 1); %!error ... %! withtol (datetime (2024, 1, 1), hours (-1)); %!error ... %! tv = datetime (2024, 1, 1) + hours (0:5)'; withtol (tv([3 4]), hours (1)); %!error ... %! withtol (1, hours (1)); %!error ... %! rowIndices (withtol (datetime (2024, 1, 1), hours (1)), hours (0:2)'); pr0m1th3as-datatypes-9450b29/inst/xlsx2struct.m000066400000000000000000000111511527370255000213550ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{s} =} xlsx2struct (@var{filename}) ## ## Read every sheet of an Excel workbook into a scalar structure. ## ## @code{@var{s} = xlsx2struct (@var{filename})} reads each sheet of the Office ## Open XML workbook named by @var{filename} (@qcode{.xlsx} or @qcode{.xlsm}) ## into a @code{table} and returns a scalar structure with one field per sheet, ## in sheet order. Each sheet is read as by @code{readtable} (variable names ## from the first row, types detected automatically); it is the inverse of ## @code{struct2xlsx}. ## ## A sheet name that is not a valid structure field name is canonicalised with ## @code{matlab.lang.makeValidName} (and made unique if two names collide); when ## the field name differs from the sheet name the original is stored on that ## field's table as the @qcode{'ActualSheetName'} custom property, so a ## subsequent @code{struct2xlsx} restores the exact sheet name. ## ## @seealso{struct2xlsx, readtable, table.writetable, ods2struct} ## @end deftypefn function s = xlsx2struct (filename) if (nargin != 1) print_usage (); endif if (! (ischar (filename) || iscellstr (filename) || isa (filename, 'string'))) error (strcat ("xlsx2struct: FILENAME must be a character vector,", ... " cellstr, or string.")); endif file = char (cellstr (filename)); ## Enumerate the sheet names (the first output doubles as an error probe). [data, ~, ~, names] = __xlsx2table__ (file); if (ischar (data)) error ("xlsx2struct: %s", data); endif s = struct (); usedFields = {}; for k = 1:numel (names) sn = names{k}; R = readtable (file, 'Sheet', sn); fn = matlab.lang.makeValidName (sn); base = fn; j = 1; while (any (strcmp (fn, usedFields))) fn = sprintf ('%s_%d', base, j); j += 1; endwhile usedFields{end+1} = fn; if (! strcmp (fn, sn)) R = addprop (R, 'ActualSheetName', 'table'); R.Properties.CustomProperties.ActualSheetName = sn; endif s.(fn) = R; endfor endfunction %!demo %! ## `xlsx2struct` is the inverse of `struct2xlsx`: it reads every worksheet of %! ## an Excel workbook into a scalar struct, one field per sheet, in sheet %! ## order. %! %! wb.Patients = table ({'Li'; 'Diaz'}, [38; 40], 'VariableNames', {'Name', ... %! 'Age'}); %! wb.Visits = table ([1; 2; 3], 'VariableNames', {'Visit'}); %! filename = fullfile (tempdir (), 'clinic.xlsx'); %! struct2xlsx (filename, wb); %! %! s = xlsx2struct (filename); %! s.Patients %! %! delete (filename); %!test # round-trip a multi-sheet workbook written by struct2xlsx %! s.alpha = table ([1; 2; 3], {'a'; 'b'; 'c'}, 'VariableNames', {'x', 'y'}); %! s.beta = table ([10.5; 20.5], 'VariableNames', {'v'}); %! fn = [tempname() '.xlsx']; %! unwind_protect %! struct2xlsx (fn, s); %! r = xlsx2struct (fn); %! assert_equal (fieldnames (r), {'alpha'; 'beta'}); %! assert_equal (r.alpha.x, [1; 2; 3]); %! assert_equal (r.beta.v, [10.5; 20.5]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!test # an odd sheet name canonicalises and stashes ActualSheetName %! T = table ([7; 8], 'VariableNames', {'v'}); %! T = addprop (T, 'ActualSheetName', 'table'); %! T.Properties.CustomProperties.ActualSheetName = 'My Sheet'; %! s.only = T; %! fn = [tempname() '.xlsx']; %! unwind_protect %! struct2xlsx (fn, s); %! r = xlsx2struct (fn); %! f = fieldnames (r); %! assert_equal (numel (f), 1); %! assert_equal (isvarname (f{1}), true); %! assert_equal (r.(f{1}).Properties.CustomProperties.ActualSheetName, ... %! 'My Sheet'); %! assert_equal (r.(f{1}).v, [7; 8]); %! unwind_protect_cleanup %! delete (fn); %! end_unwind_protect %!error ... %! xlsx2struct (42) %!error ... %! xlsx2struct ([tempname() '.xlsx']) pr0m1th3as-datatypes-9450b29/inst/years.m000066400000000000000000000041241527370255000201550ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the datatypes package for GNU Octave. ## ## 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 . ## -*- texinfo -*- ## @deftypefn {datatypes} {@var{D} =} years (@var{X}) ## ## Fixed-time duration in years. ## ## @code{@var{D} = years (@var{X})} returns a @qcode{duration} array ## representing fixed-time duration years equivalent to the values in @var{X}, ## which must be a numeric array. A fixed-length year is equal to 365.2425 ## days. ## ## @code{years} is also available as a method for @qcode{duration} arrays, in ## which case it performs the opposite conversion. ## ## @seealso{duration, days, hours, minutes, seconds, milliseconds, ## duration.years} ## @end deftypefn function out = years (x) if (nargin == 0) x = 1; elseif (! isnumeric (x)) error ("years: input array must be numeric."); elseif (! isreal (x)) error ("years: input array must be real."); endif out = duration (0, 0, 0, double (x) * 31556952000, 'Format', 'y'); endfunction %!demo %! ## `years` builds a *fixed-length* year of 365.2425 days: a constant span, %! ## not a calendar year. %! %! years (1) %! years (0.5) %!test %! X = magic (3); %! D = years (X); %! assert_equal (size (D), size (X)); %!test %! D = years ([1, 2, 3]); %! assert_equal (years (D), [1, 2, 3]); %!test %! D = years (); %! assert_equal (years (D), 1); %!error years ("asd"); %!error years (1+i); pr0m1th3as-datatypes-9450b29/src/000077500000000000000000000000001527370255000164655ustar00rootroot00000000000000pr0m1th3as-datatypes-9450b29/src/Makefile000066400000000000000000000032341527370255000201270ustar00rootroot00000000000000# Makefile for compiling required oct files TARGET_OS ?= $(OS) ifeq ($(TARGET_OS),Windows_NT) LIBS = -lole32 else LIBS = endif MKOCTFILE ?= mkoctfile LDFLAGS += $(LIBS) TZFLAGS = -DHAS_REMOTE_API=0 # Make glibc define __USE_LARGEFILE64 so miniz selects its fopen64/ftello64/ # fseeko64 branch instead of the plain fopen/ftello path. This silences miniz's # "may not support large files" #pragma message and enables 64-bit file offsets. MINIZFLAGS = -D_LARGEFILE64_SOURCE OCTFILES = __datetime__.oct \ __ldml__.oct \ __table2csv__.oct \ __csv2table__.oct \ __table2ods__.oct \ __ods2table__.oct \ __table2xlsx__.oct \ __xlsx2table__.oct \ __ckeyHash__.oct \ __nkeyHash__.oct \ __json2dictionary__.oct \ __dictionary2json__.oct .PHONY: all all: $(OCTFILES) __datetime__.oct: __datetime__.cc tz.cpp civil.h $(MKOCTFILE) '-I ./date' __datetime__.cc tz.cpp $(TZFLAGS) $(LDFLAGS) __table2ods__.oct: __table2ods__.cc ods/pugixml.cpp ods/miniz.c $(MKOCTFILE) -I./ods $(MINIZFLAGS) __table2ods__.cc ods/pugixml.cpp ods/miniz.c $(LDFLAGS) __ods2table__.oct: __ods2table__.cc ods/pugixml.cpp ods/miniz.c $(MKOCTFILE) -I./ods $(MINIZFLAGS) __ods2table__.cc ods/pugixml.cpp ods/miniz.c $(LDFLAGS) __table2xlsx__.oct: __table2xlsx__.cc ods/miniz.c $(MKOCTFILE) -I./ods $(MINIZFLAGS) __table2xlsx__.cc ods/miniz.c $(LDFLAGS) __xlsx2table__.oct: __xlsx2table__.cc ods/pugixml.cpp ods/miniz.c $(MKOCTFILE) -I./ods $(MINIZFLAGS) __xlsx2table__.cc ods/pugixml.cpp ods/miniz.c $(LDFLAGS) %.oct: %.cc $(MKOCTFILE) $< $(LDFLAGS) .PHONY: clean clean: rm -f $(OCTFILES) pr0m1th3as-datatypes-9450b29/src/__ckeyHash__.cc000066400000000000000000000127451527370255000213400ustar00rootroot00000000000000/* Copyright (C) 2025-2026 Andreas Bertsatos This file is part of the datatypes package for GNU Octave. 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 . */ #define FNV1A64_PRIME 0x00000100000001b3 #include #include using namespace std; static inline constexpr uint64_t fnv1a64 (const char* buf, size_t len, uint64_t out) { for (size_t i = 0; i < len; i++) { // default char signedness depends on architecture, for signed for // backwards compatibility const unsigned char b = buf[i]; out = (out ^ b) * FNV1A64_PRIME; } return out; } namespace /* tests */ { constexpr std::array testData{0, 1, char(0x80), 0}; static_assert(0 == fnv1a64(testData.data(), 0, 0)); static_assert(0x123 == fnv1a64(testData.data(), 0, 0x123)); static_assert(0 == fnv1a64(testData.data(), 1, 0)); static_assert(FNV1A64_PRIME == fnv1a64(testData.data(), 1, 1)); static_assert(FNV1A64_PRIME == fnv1a64(testData.data(), 2, 0)); } // Hash a 64-bit count, least significant byte first so that the result does // not depend on the architecture. static inline uint64_t fnv1a64_count (uint64_t n, uint64_t out) { for (size_t k = 0; k < 8; k++) { const unsigned char b = static_cast ((n >> (8 * k)) & 0xff); out = (out ^ b) * FNV1A64_PRIME; } return out; } DEFUN_DLD (__ckeyHash__, args, nargout, "-*- texinfo -*-\n\ @deftypefn {} {@var{uint64} =} __ckeyHash__ (@var{str})\n\ @deftypefnx {} {@var{uint64} =} __ckeyHash__ (@var{cstr})\n\ @deftypefnx {} {@var{uint64} =} __ckeyHash__ (@dots{}, @var{FNV1A64_BASE})\n\ \n\ \n\ Fowler–Noll–Vo hash key for a character vector or a cell array of them. \n\ \n\ Given a cell array, the elements are hashed with their lengths so that the \ boundaries between them are part of the key and no two different splittings \ of the same characters can collide. \n\ \n\ This is a helper function for @qcode{keyHash} methods of `datatypes`' classes. \ Do NOT use this function directly. \n\ \n\ @end deftypefn") { octave_uint64 base; // Validate input if (args.length () < 1) { error ("__ckeyHash__: too few input arguments."); } if (! (args(0).is_string () || args(0).iscell ())) { error ("__ckeyHash__: STR must be a character vector or a cell array of " "character vectors."); } // Get or assign a base value if (args.length() > 1) { base = args(1).uint64_scalar_value (); } else { base = 0xcbf29ce484222325; // default FNV1A64_BASE } octave_uint64 out; if (args(0).iscell ()) { // A composite is hashed as its element count followed by each element's // length and bytes. Flattening the elements into one string instead -- // which is what this did before -- loses the boundaries between them, so // {'ab', 'c'} and {'a', 'bc'} were one key. Framing every element the // same way also makes later key types purely additive: a struct is a // field count followed by framed name/value pairs, one level deeper in // the same scheme, so no value fixed here has to change again. // // The count is hashed even when it is zero, which is what separates {} // from {''} and stops an empty composite from erroring. const Cell c = args(0).cell_value (); const octave_idx_type n = c.numel (); uint64_t key = fnv1a64_count (static_cast (n), base); for (octave_idx_type i = 0; i < n; i++) { if (! c(i).is_string ()) { error ("__ckeyHash__: cell elements must be character vectors."); } // Column-major order, matching what x(:) would give in m-code. A char // matrix element is framed by its total length only, so it can collide // with a character vector holding the same characters; the two are // never equal keys, and the contract permits unequal keys to collide. const charNDArray el = c(i).char_array_value (); const octave_idx_type len = el.numel (); key = fnv1a64_count (static_cast (len), key); key = fnv1a64 (el.data (), static_cast (len), key); } out = key; } else { // A character vector is a single sequence, not a composite, so it is // hashed unframed and its hash code is unchanged by the framing above. // // char_array_value (), not string_value (). The latter converts a // multi-row char matrix by taking its FIRST ROW and discarding the rest, // with only a warning -- and keyHash passed x(:), an N-by-1 matrix, so // every character vector was hashed as its first character alone. In // 1.3.0 that made 'hello' and 'hxxxx' one key, and keyMatch compared hash // codes, so it called them equal. char_array_value () reads the whole // array in column-major order, which is what x(:) meant to say. const charNDArray str = args(0).char_array_value (); out = fnv1a64 (str.data (), static_cast (str.numel ()), base); } octave_value_list retval (nargout); retval(0) = out; return retval; } pr0m1th3as-datatypes-9450b29/src/__csv2table__.cc000066400000000000000000000154441527370255000214650ustar00rootroot00000000000000/* Copyright (C) 2025-2026 Andreas Bertsatos This file is part of the datatypes package for GNU Octave. 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 . */ #include #include #include #include #include #include #include #include #include #include #include using namespace std; DEFUN_DLD (__csv2table__, args, nargout, "-*- texinfo -*-\n \ @deftypefn {datatypes} {@var{C} =} __csv2table__ (@var{file})\n\ \n\ \n\ Barebone function for loading a CSV file to a cell array.\n\ \n\ This is a helper IO function for the @qcode{csv2table} function. Do NOT \ call it directly. \n\ \n\ @end deftypefn") { octave_value_list retval(nargout); // Check input arguments if (args.length() != 1 && args.length() != 2) { error ("__csv2table__: one or two input arguments are required."); } // Get input arguments string file = args(0).string_value(); // Optional field delimiter (a single character); defaults to a comma. string _sep = ","; if (args.length() == 2) { string d = args(1).string_value(); if (d.length() != 1) { error ("__csv2table__: DELIMITER must be a single character."); } _sep = d; } // Open CSV file ifstream fd(file.c_str()); if (! fd.is_open()) { retval(0) = "cannot open file '" + file + "' for reading."; return retval; } // Initialize necessary variable; char sep = _sep[0]; string _prot = "\""; char prot = _prot[0]; string line, word; bool inside = false; int cols = 0; int rows = 1; // Get number of columns from parsing the first line getline (fd, line); for (int c = 0, len = line.length(); c <= len; c++) { if (c == len || ((line[c] == sep || line[c] == 10) && ! inside)) { cols++; } else if ((inside) && line[c] == prot && (c + 1 < len && line[c + 1] == prot)) { ++c; } else if (line[c] == prot) { inside = ! inside; } } // Get number of row from parsing the remaining file while (getline(fd, line)) { rows++; } fd.clear(); // Rewind fd.seekg (0, ios::beg); if (! fd.good()) { retval(0) = "cannot read '" + file + "'."; return retval; } //Initialize cell array Cell C(rows, cols); for (int c = 0; c < cols; c++) { for (int r = 0; r < rows; r++) { C(r,c) = ""; } } // Parse (again) each line of the file bool line_too_long = false; for (int row = 0; row < rows; row++) { getline (fd, line); word = ""; inside = false; int col = 0; bool oinside = false; for (int k = 0, len = line.length(); k <= len; k++) { if ((k == len || line[k] == sep) && (! inside)) { // Check number of columns if (! line_too_long && col == cols) { line_too_long = true; warning ("__csv2table__: line(s) found with more fields " "than in headerline"); break; } // Check for last char to be 13 (CR) and remove if found if (word.length () && word[word.length () - 1] == char(13)) { word.resize (word.size () - 1); } // Check if scalar const char *word_str = word.c_str (); char *err; double val = strtod (word_str, &err); // The "NA" token (Octave's missing value, as written by 'table2csv') // is not recognized by strtod; map it to NA explicitly. bool is_na = (! oinside) && (word == "NA"); // A fully consumed, unquoted token is numeric. bool is_num = (word != "") && (! oinside) && (err == word_str + word.length ()); // A field that is empty and was never quoted carries no value at all, // where an empty quoted field ("") carries an empty string. The two // are kept apart, as [] and '', so that a reader told the column's // type can tell a missing entry from an empty one. bool is_blank = (word == "") && (! oinside); // Store into the cell; check if it is in address argument range if (col < cols) { if (is_blank) { C(row, col) = octave_value (Matrix (0, 0)); } else if (is_na) { C(row, col) = octave_value (octave::numeric_limits::NA ()); } else if (! is_num) { C(row, col) = octave_value (word); } else { // A plain integer of 16 or more digits cannot be held exactly by // a double, so parse it without loss into a 64-bit integer. size_t start = (word[0] == '+' || word[0] == '-') ? 1 : 0; bool is_int = (word.length () > start); size_t ndig = 0; for (size_t i = start; i < word.length (); i++) { if (word[i] < '0' || word[i] > '9') { is_int = false; break; } ndig++; } if (is_int && ndig >= 16) { errno = 0; if (word[0] == '-') { long long llv = strtoll (word_str, &err, 10); C(row, col) = (errno == 0) ? octave_value (octave_int64 (llv)) : octave_value (val); } else { unsigned long long ullv = strtoull (word_str, &err, 10); C(row, col) = (errno == 0) ? octave_value (octave_uint64 (ullv)) : octave_value (val); } } else { C(row, col) = octave_value (val); } } } col++; word = ""; oinside = false; } else if ((inside) && line[k] == prot && (k + 1 < len && line[k+1] == prot)) { // Inside a string word += prot; ++k; } else if (line[k] == prot) { // Switch in/out of string oinside = inside; inside = ! inside; } else { word += line[k]; } } } // Close file fd.close(); retval(0) = C; return retval; } pr0m1th3as-datatypes-9450b29/src/__datetime__.cc000066400000000000000000002075061527370255000213760ustar00rootroot00000000000000/* Copyright (C) 2024-2026 Andreas Bertsatos This file is part of the datatypes package for GNU Octave. 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 . */ #include #include #include #include #include #include #include #include "date/tz.h" #include "civil.h" using namespace std; using namespace date; auto double2milli (double time_sec) { using ds = chrono::duration; local_time time{ds{time_sec}}; auto tp = round(time); return tp; } auto double2micro (double time_sec) { using ds = chrono::duration; local_time time{ds{time_sec}}; auto tp = round(time); return tp; } auto double2nano (double time_sec) { using ds = chrono::duration; local_time time{ds{time_sec}}; auto tp = round(time); return tp; } // A zone the conversions below can be asked about: a zone of the database, or // a fixed offset from UTC, which the database does not hold and does not need. // A fixed offset has one regime for all time and no daylight saving, so FIXED // answers for it and TZ is null. struct zone_ref { const time_zone *tz = nullptr; sys_info fixed; }; // '+HH:MM' or '+HH:MM:SS', either sign: the only spellings the class stores for // a fixed-offset zone. Anything else is a name for the database. bool parse_fixed_offset (const string& name, chrono::seconds& off) { const size_t L = name.size (); if ((L != 6 && L != 9) || (name[0] != '+' && name[0] != '-') || name[3] != ':' || (L == 9 && name[6] != ':')) { return false; } long f[3] = {0, 0, 0}; for (size_t k = 1, i = 0; k < L; k += 3, i++) { if (! isdigit ((unsigned char) name[k]) || ! isdigit ((unsigned char) name[k+1])) { return false; } f[i] = (name[k] - '0') * 10 + (name[k+1] - '0'); } if (f[0] > 23 || f[1] > 59 || f[2] > 59) { return false; } long v = f[0] * 3600 + f[1] * 60 + f[2]; off = chrono::seconds {name[0] == '-' ? -v : v}; return true; } // The abbreviation tzdata itself writes for a numeric zone, such as '+04' or // '+0530', and 'GMT' for a zero offset, which is what MATLAB's 'z' shows there. string fixed_abbrev (chrono::seconds off) { long a = labs (off.count ()); if (a == 0) { return "GMT"; } char sgn = (off.count () < 0 ? '-' : '+'); // The caller admits at most '+23:59:59', so two digits per field always fit. long hh = (a / 3600) % 100; char buf[16]; if (a % 60) { snprintf (buf, sizeof (buf), "%c%02ld%02ld%02ld", sgn, hh, (a % 3600) / 60, a % 60); } else if (a % 3600) { snprintf (buf, sizeof (buf), "%c%02ld%02ld", sgn, hh, (a % 3600) / 60); } else { snprintf (buf, sizeof (buf), "%c%02ld", sgn, hh); } return string (buf); } // The zone a name stands for. Throws, as 'locate_zone' does, for a name the // database does not know. zone_ref find_zone (const string& name) { zone_ref z; chrono::seconds off; if (parse_fixed_offset (name, off)) { z.fixed.begin = sys_seconds::min (); z.fixed.end = sys_seconds::max (); z.fixed.offset = off; z.fixed.save = chrono::minutes {0}; z.fixed.abbrev = fixed_abbrev (off); } else { z.tz = locate_zone (name); } return z; } // A one-entry cache of the last regime found in a zone. Looking a moment up // in the tz database is by far the most expensive thing here -- an order of // magnitude more than the component arithmetic around it, which is why a zoned // conversion costs so much more than an unzoned one -- and the elements of a // datetime array are nearly always close enough together to share a regime, so // answering from the cache is what makes a long array cheap. Each is a local // variable of the loop that uses it, so nothing is shared between calls. struct sys_cache { const time_zone *tz = nullptr; sys_info si; bool valid = false; }; // A moment names one regime, so the cached range is exactly the one the // database reports and the cache is exact. A fixed offset is its own answer. const sys_info& cached_sys_info (const zone_ref& z, sys_seconds sys, sys_cache& zc) { if (! z.tz) { return z.fixed; } if (! zc.valid || zc.tz != z.tz || sys < zc.si.begin || sys >= zc.si.end) { zc.si = z.tz->get_info (sys); zc.tz = z.tz; zc.valid = true; } return zc.si; } struct local_cache { const time_zone *tz = nullptr; local_info li; chrono::seconds lo{0}, hi{0}; bool valid = false; // The daylight saving part last worked out, and the regime it belongs to. const time_zone *dst_tz = nullptr; sys_seconds dst_begin; double dst = 0; bool dst_valid = false; }; // A wall clock may name one moment, two, or none, and which of those it is can // only be settled near a transition. The cached answer is therefore kept only // for clocks that name one moment, and only over the regime's range pulled in // by a wide margin at each end, so anything within two days of a transition // goes back to the database. No transition has ever moved a clock by remotely // that much, so the margin cannot hide an ambiguous or skipped clock. const local_info& cached_local_info (const zone_ref& z, local_time lt, local_cache& lc) { // A fixed offset names exactly one moment for every clock. if (! z.tz) { lc.li.result = local_info::unique; lc.li.first = z.fixed; lc.tz = nullptr; lc.valid = false; return lc.li; } auto l = chrono::duration_cast (lt.time_since_epoch ()); if (lc.valid && lc.tz == z.tz && l >= lc.lo && l < lc.hi) { return lc.li; } lc.li = z.tz->get_info (lt); lc.tz = z.tz; lc.valid = false; if (lc.li.result == local_info::unique) { auto margin = chrono::hours {48}; auto lo = lc.li.first.begin.time_since_epoch () + lc.li.first.offset + margin; auto hi = lc.li.first.end.time_since_epoch () + lc.li.first.offset - margin; if (lo < hi) { lc.lo = lo; lc.hi = hi; lc.valid = true; } } return lc.li; } // Resolve a wall-clock (local) time in 'timezone' to a zoned_time, applying // MATLAB's rules for the two local times that do not name a unique instant. // Where the clock goes back, the repeated hour is ambiguous and MATLAB takes // the LATER offset, i.e. standard time. Where the clock goes forward, the // skipped interval does not exist and MATLAB shifts the wall clock forward by // the length of the gap: 02:30 inside a one-hour gap becomes 03:30, and a // half-hour gap (Australia/Lord_Howe) shifts by half an hour. Shifting by the // gap is the same as reading the wall clock at the pre-transition offset, and // leaves a local time that is unique, so the make_zoned below cannot throw. // Without this, date.h throws ambiguous_local_time or nonexistent_local_time, // and the uncaught exception aborts the whole Octave session. template auto resolve_local (const string& timezone, local_time lt) { const time_zone *tz = locate_zone (timezone); auto info = tz->get_info (lt); if (info.result == local_info::nonexistent) { return make_zoned (tz, lt + (info.second.offset - info.first.offset)); } else if (info.result == local_info::ambiguous) { return make_zoned (tz, lt, choose::latest); } return make_zoned (tz, lt); } auto from_to_tz_milli (double time_sec, string from_tzone, string to_tzone) { auto tp = double2milli (time_sec); auto from = resolve_local (from_tzone, tp); auto to = make_zoned (to_tzone, from.get_sys_time ()); return to; } auto from_to_tz_micro (double time_sec, string from_tzone, string to_tzone) { auto tp = double2micro (time_sec); auto from = resolve_local (from_tzone, tp); auto to = make_zoned (to_tzone, from.get_sys_time ()); return to; } auto from_to_tz_nano (double time_sec, string from_tzone, string to_tzone) { auto tp = double2nano (time_sec); auto from = resolve_local (from_tzone, tp); auto to = make_zoned (to_tzone, from.get_sys_time ()); return to; } // Components of a count of seconds from 1970-01-01, which is what every // 'ConvertFrom' serial becomes before it is read. The date is taken in 64 bits // and only the time WITHIN the day is handed to chrono, so no year is // truncated; a whole day of microseconds is a value chrono is always safe with, // however distant the day itself. The two corrections guard the seam: past // 2^53 seconds a double's spacing exceeds a second, so the split can land a // hair either side of midnight. // The decimal unit a day-count serial can deliver: the power of ten NEAREST its // own resolution, and never finer than the microsecond this class stores. A // datenum at 2024 resolves about ten microseconds, so the value built from // 10:30:45 lies 2.68 us short of it; reporting 10:30:44.999997 would claim // precision the number never carried, and would break the round trip a serial // exists for. The unit has to follow the epoch: an Excel serial counts from // 1899-12-30 and so resolves sixteen times finer than a datenum, which is why // it recovers a microsecond figure a datenum cannot. double serial_unit (double serial_days) { double a = fabs (serial_days); double q = (nextafter (a, INFINITY) - a) * 86400.0; if (! (q > 0) || ! isfinite (q)) { return 1e-6; } double u = pow (10.0, round (log10 (q))); return (u < 1e-6 ? 1e-6 : u); } RowVector seconds2vector (double time_sec, string precision, double unit = 0) { RowVector OUT(6); double dayf = floor (time_sec / 86400.0); double sec_of_day = time_sec - dayf * 86400.0; if (unit > 0) { sec_of_day = round (sec_of_day / unit) * unit; } if (sec_of_day >= 86400.0) { sec_of_day -= 86400.0; dayf += 1; } else if (sec_of_day < 0) { sec_of_day += 86400.0; dayf -= 1; } int64_t y, mo, d; civil::civil_from_days ((int64_t) dayf, y, mo, d); auto tp = double2micro (sec_of_day); auto day_tp = chrono::floor(tp); hh_mm_ss time_tp{tp - day_tp}; OUT(0) = (double) y; OUT(1) = (double) mo; OUT(2) = (double) d; OUT(3) = time_tp.hours().count(); OUT(4) = time_tp.minutes().count(); OUT(5) = (double)time_tp.seconds().count() + (double)time_tp.subseconds().count() / 1000000; if (precision == "milliseconds") { OUT(5) = round (OUT(5) * 1000) / 1000; } return OUT; } // Components of a bare wall clock. Split out of 'tz2vector' so that a caller // which already holds the local time -- having added a known offset to a moment // rather than built a zoned_time -- does not have to look the zone up again to // get it back. template RowVector localtime2vector (const LocalType& t_local, string precision) { RowVector OUT(6); auto today_local = chrono::floor(t_local); hh_mm_ss time_local{t_local - today_local}; year_month_day date_local{today_local}; OUT(0) = (int)date_local.year(); OUT(1) = (unsigned int)date_local.month(); OUT(2) = (unsigned int)date_local.day(); OUT(3) = time_local.hours().count(); OUT(4) = time_local.minutes().count(); if (precision == "milliseconds") { OUT(5) = (double)time_local.seconds().count() + (double)time_local.subseconds().count() / 1000; } else { OUT(5) = (double)time_local.seconds().count() + (double)time_local.subseconds().count() / 1000000; } return OUT; } template RowVector tz2vector (const ZonedType& to, string precision) { return localtime2vector (to.get_local_time (), precision); } // Aggregate a single set of (possibly non-canonical) date/time components into // a local_time, mirroring the rollover math of the component-normalisation path // below so the two stay in lockstep. No time zone is involved: the result is a // bare wall clock, which is what every mode here starts from -- some going // on to resolve it against a zone, others already knowing the offset and // needing only the arithmetic. Callers must screen NaN/Inf beforehand. // Whole days carried by the time components. The division must FLOOR, and in // 64 bits: truncating toward zero and then taking one more day off whenever the // time is negative takes a day too many every time that division came out // exact, so an hour component of -24 landed two days back rather than one. The // seconds left behind are read off by 'seconds2vector', which answers modulo a // day whatever the sign it is given, so only this count has to be right. int64_t time_extra_days (double time_sec) { return (int64_t) floor (time_sec / 86400.0); } // Normalize a component set without consulting 'date.h' or the timezone // database: DAYS is the day count from 1970-01-01 and HMS(3..5) the time of // day. This is the whole answer for an unzoned wall clock, and being 64-bit // throughout it is what lets a year outside [-32767, 32767] survive. The time // of day still goes through 'seconds2vector', which is safe at any year because // what it is handed is one day's worth of seconds. void components2civil (double Yv, double Mv, double Dv, double hv, double mv, double sv, double xv, string precision, int64_t& days, RowVector& hms) { double time_sec = hv * 3600 + mv * 60 + sv + xv / 1000; int64_t extra_days = time_extra_days (time_sec); hms = seconds2vector (remainder (time_sec, 86400), precision); days = civil::components2days (Yv, Mv, Dv, extra_days); } // Write the date a day count names, and a time of day, into the six output // arrays at index I. Templated because the matrix branches collect their // output in 'ColumnVector' and the rest in 'NDArray'. template void set_civil (int64_t days, const RowVector& hms, int i, Vec& Y, Vec& M, Vec& D, Vec& h, Vec& m, Vec& s) { int64_t yo, mo, dd; civil::civil_from_days (days, yo, mo, dd); Y(i) = (double) yo; M(i) = (double) mo; D(i) = (double) dd; h(i) = hms(3); m(i) = hms(4); s(i) = hms(5); } // Everything the daylight-saving questions need about one wall clock, from a // SINGLE tz-database lookup. The database answers by local time, and a local // time names one moment, two, or none; 'get_info' reports which, along with the // regime on each side, and every question below is then a matter of choosing // between them rather than of asking again. // // CHOSEN is the offset 'resolve_local' would settle on, which is the answer for // an array built from a wall clock. KEPT is SRCOFF where that still names this // clock and CHOSEN otherwise, which is how calendar arithmetic and 'dateshift' // keep an element on the pass it was already on; passing HAVESRC false asks for // CHOSEN alone. ISDST and ABBREV belong to whichever of the two candidates // KEPT names, so they describe the element's own pass rather than the later // one -- 'EDT' for the hour that is still on daylight saving and 'EST' for the // hour that repeats it. Reading the flag off the regime is also why this is // right where reasoning from "the earlier pass is the daylight one" is not: // Ireland's database entries count winter as the saving period. // // Callers must screen NaN/Inf. struct local_fold { double chosen; double kept; double dst; bool isdst; string abbrev; }; // Whole years to move a date by before a timezone may be asked about it. // 'date.h' holds a year in a 'short', and there is no zone DATA out beyond its // range in any case -- only the rule the database carries for all future time. // That rule is written as an nth weekday of a month, so it repeats exactly on // the Gregorian calendar's 400-year period, over which weekday and leap year // both return to where they started. A future date is therefore moved into // [2038, 2438), where that rule governs, and a past one into [1400, 1800), // which is before any zone had a transition and so answers with local mean // time. R2026a extrapolates the same way and agrees period for period: its // March transition falls on the same day at 40000 as at 40400, and at 2040 as // at 2440. double zone_year_shift (double Yv) { if (Yv > 32767) { return 400.0 * floor ((Yv - 2038.0) / 400.0); } if (Yv < -32767) { return 400.0 * floor ((Yv - 1400.0) / 400.0); } return 0.0; } // The wall clock a component set names, moved by SHIFT years so that 'date.h' // can hold it. The components are folded in 64 bits first, and the shift is // taken from the year they land in rather than from the year given, since the // months, days or time can carry a date across that range on their own. A // caller wanting the date back adds SHIFT to the year it reads. local_time components2localtime (double Yv, double Mv, double Dv, double hv, double mv, double sv, double xv, string precision, double& shift) { int64_t dnum; RowVector hms; components2civil (Yv, Mv, Dv, hv, mv, sv, xv, precision, dnum, hms); int64_t y, mo, d; civil::civil_from_days (dnum, y, mo, d); shift = zone_year_shift ((double) y); dnum -= (int64_t) (shift / 400.0) * 146097; double tod = hms(3) * 3600 + hms(4) * 60 + hms(5); return local_days {days {(int) dnum}} + chrono::microseconds {(int64_t) round (tod * 1e6)}; } // The daylight saving part of a regime's offset in seconds, as MATLAB counts // it. The database writes the winter of some zones as a negative saving // (Ireland, and Morocco during Ramadan), where MATLAB counts the other half of // the year as the daylight time instead: a negative saving is standard time, // and a zero saving between two negative ones is daylight time by that much. // A zero saving beside only one negative saving is standard time, as Ireland's // permanent +01:00 of 1968 to 1971 is. Measured against R2024a for // Europe/Dublin and Africa/Casablanca. Only a zero saving needs the // neighbouring regimes, two more lookups, so the answer for the last such // regime is kept in LC. double daylight_saving (const zone_ref& z, const sys_info& own, local_cache& lc) { if (! z.tz || own.save < chrono::minutes {0}) { return 0; } if (own.save > chrono::minutes {0}) { return (double) chrono::duration_cast (own.save).count (); } if (lc.dst_valid && lc.dst_tz == z.tz && lc.dst_begin == own.begin) { return lc.dst; } double d = 0; const sys_seconds lo {sys_days {year::min () / January / 1}}; const sys_seconds hi {sys_days {year::max () / January / 1}}; if (own.begin > lo && own.end < hi) { const sys_info prev = z.tz->get_info (own.begin - chrono::seconds {1}); const sys_info next = z.tz->get_info (own.end); if (prev.save < chrono::minutes {0} && next.save < chrono::minutes {0}) { auto neg = chrono::duration_cast (prev.save); d = - (double) neg.count (); } } lc.dst_tz = z.tz; lc.dst_begin = own.begin; lc.dst = d; lc.dst_valid = true; return d; } local_fold components2fold (double Yv, double Mv, double Dv, double hv, double mv, double sv, double xv, const zone_ref& tz, string precision, double srcOff, bool haveSrc, local_cache& lc) { double shift; auto lt = components2localtime (Yv, Mv, Dv, hv, mv, sv, xv, precision, shift); const local_info& info = cached_local_info (tz, lt, lc); // 'resolve_local' shifts a clock inside a gap forward past it, which lands // it in the regime after the transition, and takes the later of a repeated // pair; a clock that names one moment has only 'first' filled in. const sys_info *pick = (info.result == local_info::unique) ? &info.first : &info.second; local_fold out; out.chosen = (double) pick->offset.count (); out.kept = out.chosen; if (haveSrc && info.result != local_info::nonexistent) { double so = round (srcOff); if (so == (double) info.first.offset.count () || (info.result == local_info::ambiguous && so == (double) info.second.offset.count ())) { out.kept = so; } } const sys_info *own = pick; if (info.result == local_info::ambiguous && out.kept == (double) info.first.offset.count ()) { own = &info.first; } const sys_info mine = *own; out.abbrev = mine.abbrev; out.dst = daylight_saving (tz, mine, lc); out.isdst = (out.dst != 0); return out; } // The instant a wall clock names, under the same rules as 'resolve_local': the // later of a repeated pair, and a clock inside a gap shifted past it -- which // is the same instant as reading it at the offset in force before the // transition, whence the use of 'first' there. Built from one cached lookup // and a subtraction rather than from a zoned_time, which would look the clock // up again to be asked for its instant. sys_time local2sys (const zone_ref& tz, local_time lt, local_cache& lc) { const local_info& info = cached_local_info (tz, lt, lc); auto off = (info.result == local_info::ambiguous) ? info.second.offset : info.first.offset; return sys_time {lt.time_since_epoch ()} - off; } // Components and offset of an absolute instant, read in 'timezone'. A moment // names one wall clock, so nothing here has to be resolved, and the offset that // the single lookup returns is added to reach the local time rather than a // zoned_time being built -- which would look the same moment up again on every // question asked of it. void sys2components (sys_time sys, const zone_ref& tz, RowVector& OUT, double& off, string precision, sys_cache& zc) { const sys_info& si = cached_sys_info (tz, chrono::floor (sys), zc); local_time lt {(sys + si.offset).time_since_epoch ()}; OUT = localtime2vector (lt, precision); off = (double) si.offset.count (); } void sys2components (double posix_sec, const zone_ref& tz, RowVector& OUT, double& off, sys_cache& zc) { using ds = chrono::duration; auto sys = round (sys_time {ds {posix_sec}}); sys2components (sys, tz, OUT, off, "microseconds", zc); } auto timezone_precision (double time_sec, string timezone, string precision) { auto tz = make_zoned (current_zone (), chrono::system_clock::now ()); if (precision == "milliseconds") { auto tp = double2milli (time_sec); tz = resolve_local (timezone, tp); } else if (precision == "microseconds") { auto tp = double2micro (time_sec); tz = resolve_local (timezone, tp); } else { auto tp = double2nano (time_sec); // microseconds using duration_type = std::chrono::duration>; tz = resolve_local (timezone, std::chrono::time_point_cast (tp)); } return tz; } template RowVector timezone2vector (const LocalType& t_local) { RowVector OUT(6); auto day_tp = chrono::floor(t_local); hh_mm_ss time_tp{t_local - day_tp}; year_month_day date_tp{day_tp}; OUT(0) = (int)date_tp.year(); OUT(1) = (unsigned int)date_tp.month(); OUT(2) = (unsigned int)date_tp.day(); OUT(3) = time_tp.hours().count(); OUT(4) = time_tp.minutes().count(); OUT(5) = (double)time_tp.seconds().count() + (double)time_tp.subseconds().count() / 1000000000; return OUT; } NDArray expand_input (dim_vector sz, octave_value args) { NDArray OUT(sz, 0); if (args.is_scalar_type ()) { for (int i = 0; i < sz.numel (); i++) { OUT(i) = args.scalar_value (); } } else { NDArray tmp = args.array_value (); for (int i = 0; i < sz.numel (); i++) { OUT(i) = tmp(i); } } return OUT; } double check_nan_inf (RowVector IN) { int n = IN.numel (); double OUT = 0; bool is_nan = false; bool isPinf = false; bool isNinf = false; for (int i = 0; i < n; i++) { if (isnan (IN(i))) { is_nan = true; } else if (! is_nan && ! isNinf && isinf (IN(i)) && IN(i) > 0) { isPinf = true; } else if (! is_nan && ! isPinf && isinf (IN(i)) && IN(i) < 0) { isNinf = true; } else if (! is_nan && isNinf && isinf (IN(i)) && IN(i) > 0) { is_nan = true; } else if (! is_nan && isPinf && isinf (IN(i)) && IN(i) < 0) { is_nan = true; } } if (is_nan) { OUT = NAN; } else if (isPinf) { OUT = INFINITY; } else if (isNinf) { OUT = -INFINITY; } return OUT; } DEFUN_DLD(__datetime__, args, nargout, "-*- texinfo -*-\n\ @deftypefn {datatypes} {[@var{Y}, @var{MO}, @var{D}, @var{H}, @var{MI}, @var{S}]} __datetime__ (@dots{})\n\ @deftypefnx {datatypes} {[@var{Y}, @var{MO}, @var{D}, @var{H}, @var{MI}, @var{S}, @var{errmsg}]} __datetime__ (@dots{})\n\ @deftypefnx {datatypes} {[@dots{}] =} __datetime__ (@qcode{'now'})\n\ @deftypefnx {datatypes} {[@dots{}] =} __datetime__ (@qcode{'today'})\n\ @deftypefnx {datatypes} {[@dots{}] =} __datetime__ (@qcode{'tomorrow'})\n\ @deftypefnx {datatypes} {[@dots{}] =} __datetime__ (@qcode{'yesterday'})\n\ @deftypefnx {datatypes} {[@dots{}] =} __datetime__ (@var{Y}, @var{MO}, @var{D})\n\ @deftypefnx {datatypes} {[@dots{}] =} __datetime__ (@var{Y}, @var{MO}, @var{D}, @var{H}, @var{MI}, @var{S})\n\ @deftypefnx {datatypes} {[@dots{}] =} __datetime__ (@var{Y}, @var{MO}, @var{D}, @var{H}, @var{MI}, @var{S}, @var{MS})\n\ @deftypefnx {datatypes} {[@dots{}] =} __datetime__ (@var{X}, @qcode{'ConvertFrom'}, @var{dateType})\n\ @deftypefnx {datatypes} {[@dots{}] =} __datetime__ (@dots{}, @qcode{'Precision'}, @var{precision})\n\ @deftypefnx {datatypes} {[@dots{}] =} __datetime__ (@dots{}, @qcode{'TimeZone'}, @var{tzone}, @qcode{'toTimeZone'}, @var{totzone})\n\ @deftypefnx {datatypes} {[@dots{}] =} __datetime__ (@dots{}, @qcode{'Offset'}, @var{off})\n\ \n\ \n\ Base function for datetime class. \n\ \n\n\ The @qcode{'Offset'} parameter gives the stored UTC offset of each element \n\ in seconds, which is what tells apart the two instants a repeated wall \n\ clock names on the day a zone puts its clock back. It is accepted by the \n\ @qcode{'ConvertTo'} modes listed below; those that also read it answer for \n\ the wall clock alone when it is not given. \n\ \n\n\ @table @asis \n\ @item @qcode{'instant'} \n\ POSIX seconds of each element, taken from its offset by subtraction, with no \n\ time-zone lookup at all. \n\ \n\ @item @qcode{'fromposix'} \n\ The six components of each POSIX instant read in @var{tzone}, plus the \n\ offset in force there as a seventh output. \n\ \n\ @item @qcode{'rezone'} \n\ The six components of each element expressed in @var{totzone} while keeping \n\ its instant, plus the offset there as a seventh output. \n\ \n\ @item @qcode{'zoneoffset'} \n\ The offset the wall clock resolves to. \n\ \n\ @item @qcode{'keepfold'} \n\ The given offset where it still names this wall clock, and the resolved \n\ offset otherwise. \n\ \n\ @item @qcode{'isdst'}, @qcode{'zoneabbrev'} \n\ Daylight-saving flag and zone abbreviation, for the element's own pass over \n\ a repeated clock when an offset is given. \n\ @end table \n\ \n\n\ @end deftypefn") { // The 'leapseconds' mode reports the leap-second table of the shipped tz // database as the POSIX time of each insertion, that is, of the first instant // after the inserted second. It takes no data and returns a single output, // so it is answered ahead of the output-count guard below. if (args.length () == 1 && args(0).is_string () && args(0).string_value () == "leapseconds") { octave_value_list ls_out (1); try { const auto& leaps = get_tzdb ().leap_seconds; ColumnVector OUT (leaps.size ()); for (size_t i = 0; i < leaps.size (); i++) { OUT(i) = (double) leaps[i].date ().time_since_epoch ().count (); } ls_out(0) = OUT; } catch (const exception& e) { error ("__datetime__: TZDB error: %s", e.what ()); } return ls_out; } // The 'systemtimezone' mode returns the IANA name of the system time zone, // or 'UTC' when it cannot be determined, as for the default zone below. if (args.length () == 1 && args(0).is_string () && args(0).string_value () == "systemtimezone") { string name = "UTC"; try { name = current_zone () -> name (); } catch (const exception&) { } return octave_value_list (1, octave_value (name)); } // The 'ConvertTo' serial mode returns a single output; every other mode // requires either 6 or 7 output arguments. Detect the mode up front so the // output-count guard below can exempt it. bool toSerial = false; for (int i = 0; i + 1 < args.length (); i++) { if (args(i).is_string () && args(i).string_value () == "ConvertTo") { toSerial = true; } } // Either 6 or 7 output arguments are required if (nargout > 7) { error ("__datetime__: too many output arguments."); } if (nargout < 6 && ! toSerial) { error ("__datetime__: too few output arguments."); } // Prepare input output arguments int nargin = args.length (); octave_value_list retval(nargout); for (int i = 0; i < nargout; i++) { retval(i) = 0; } // Add defaults string timezone; try { timezone = current_zone () -> name (); } catch (const exception& e) { octave_stdout << "__datetime__: TZDB error: " << e.what() << "\n"; octave_stdout << "Falling back to UTC.\n"; timezone = "UTC"; } string to_tzone = timezone; // Whether a zone was NAMED, which is not the same question as which zone is // in force: the defaults above are the machine's, and an unzoned datetime // must not be resolved against them. bool haveZone = false; string precision = "milliseconds"; bool doLeapSec = false; bool doConvert = false; string convertFrom = ""; string convertTo = ""; // The stored UTC offset of each element, which is what distinguishes the two // moments a repeated wall clock names. Optional: without it the modes below // answer for the clock alone, as they did before it existed. octave_value offsetArg; bool haveOffset = false; // Parse paired arguments here while (nargin > 2 && args(nargin - 2).is_string ()) { if (args(nargin - 2).string_value () == "ConvertFrom") { if (args(nargin - 1).is_string ()) { convertFrom = args(nargin - 1).string_value (); doConvert = true; } else { if (nargout == 7) { retval(6) = "invalid type for 'ConvertFrom'."; return retval; } else { error ("__datetime__: invalid type for 'ConvertFrom'."); } } } else if (args(nargin - 2).string_value () == "ConvertTo") { if (args(nargin - 1).is_string ()) { convertTo = args(nargin - 1).string_value (); } else { if (nargout == 7) { retval(6) = "invalid type for 'ConvertTo'."; return retval; } else { error ("__datetime__: invalid type for 'ConvertTo'."); } } } else if (args(nargin - 2).string_value () == "Offset") { if (args(nargin - 1).isnumeric ()) { offsetArg = args(nargin - 1); haveOffset = true; } else { if (nargout == 7) { retval(6) = "invalid type for 'Offset'."; return retval; } else { error ("__datetime__: invalid type for 'Offset'."); } } } else if (args(nargin - 2).string_value () == "Precision") { if (args(nargin - 1).is_string ()) { precision = args(nargin - 1).string_value (); } else { if (nargout == 7) { retval(6) = "invalid type for 'Precision'."; return retval; } else { error ("__datetime__: invalid type for 'Precision'."); } } } else if (args(nargin - 2).string_value () == "TimeZone") { if (args(nargin - 1).is_string ()) { haveZone = true; if (args(nargin - 1).string_value () == "UTCLeapSeconds") { timezone = "UTC"; doLeapSec = true; } else { timezone = args(nargin - 1).string_value (); } } else { if (nargout == 7) { retval(6) = "invalid type for 'TimeZone'."; return retval; } else { error ("__datetime__: invalid type for 'TimeZone'."); } } } else if (args(nargin - 2).string_value () == "toTimeZone") { if (args(nargin - 1).is_string ()) { haveZone = true; if (args(nargin - 1).string_value () == "UTCLeapSeconds") { to_tzone = "UTC"; doLeapSec = true; } else { to_tzone = args(nargin - 1).string_value (); } } else { if (nargout == 7) { retval(6) = "invalid type for 'toTimeZone'."; return retval; } else { error ("__datetime__: invalid type for 'toTimeZone'."); } } } //else if (args(nargin - 2).string_value () == "Format") {} //else if (args(nargin - 2).string_value () == "InputFormat") {} //else if (args(nargin - 2).string_value () == "Locale") {} //else if (args(nargin - 2).string_value () == "PivotYear") {} else { if (nargout == 7) { retval(6) = "unrecognized optional paired argument."; return retval; } else { error ("__datetime__: unrecognized optional paired argument."); } } nargin = nargin - 2; } // Check for valid timezone input arguments try { find_zone (timezone); } catch (const exception&) { if (nargout == 7) { retval(6) = "unrecognized timezone: '" + timezone + "'"; return retval; } else { error ("__datetime__: invalid string value for 'TimeZone'."); } } try { find_zone (to_tzone); } catch (const exception&) { if (nargout == 7) { retval(6) = "unrecognized timezone: '" + to_tzone + "'"; return retval; } else { error ("__datetime__: invalid string value for 'toTimeZone'."); } } // Handle relativeDay (only one argument left) if (args(0).is_string ()) { Matrix Y(1,1); Matrix M(1,1); Matrix D(1,1); Matrix h(1,1); Matrix m(1,1); Matrix s(1,1); auto today = chrono::system_clock::now (); // The wall clock an instant shows in the named zone, a fixed offset // included. const zone_ref zr = find_zone (timezone); sys_cache zc0; auto local_of = [&] (auto tp) { auto lt = tp + cached_sys_info (zr, chrono::floor (tp), zc0).offset; return local_time {lt.time_since_epoch ()}; }; if (args(0).string_value () == "now") { RowVector OUT = timezone2vector (local_of (today)); Y(0) = OUT(0); M(0) = OUT(1); D(0) = OUT(2); h(0) = OUT(3); m(0) = OUT(4); if (precision == "milliseconds") { s(0) = round (OUT(5) * 1000) / 1000; } else if (precision == "microseconds") { s(0) = round (OUT(5) * 1000000) / 1000000; } else { s(0) = OUT(5); } } else if (args(0).string_value () == "today") { RowVector OUT = timezone2vector (local_of (floor(today))); Y(0) = OUT(0); M(0) = OUT(1); D(0) = OUT(2); h(0) = 0; m(0) = 0; s(0) = 0; } else if (args(0).string_value () == "yesterday") { RowVector OUT = timezone2vector (local_of (floor(today) - days{1})); Y(0) = OUT(0); M(0) = OUT(1); D(0) = OUT(2); h(0) = 0; m(0) = 0; s(0) = 0; } else if (args(0).string_value () == "tomorrow") { RowVector OUT = timezone2vector (local_of (floor(today) + days{1})); Y(0) = OUT(0); M(0) = OUT(1); D(0) = OUT(2); h(0) = 0; m(0) = 0; s(0) = 0; } retval(0) = Y; retval(1) = M; retval(2) = D; retval(3) = h; retval(4) = m; retval(5) = s; return retval; } // Handle convertFrom (only one argument left) if (doConvert) { if (nargin > 1) { string errmsg = "only a single numeric array is allowed"; errmsg += " when using the 'ConvertFrom' parameter."; if (nargout == 7) { retval(6) = errmsg; return retval; } else { errmsg = "__datetime__: " + errmsg; error ("%s", errmsg.c_str ()); } } // Initialize output argument double time_sec; int n = args(0).numel (); Matrix Y(n,1); // Years (numeric) Matrix M(n,1); // Months (numeric) Matrix D(n,1); // Days (numeric) Matrix h(n,1); // Hours (numeric) Matrix m(n,1); // Minutes (numeric) Matrix s(n,1); // Seconds (numeric) // Fix datetimes from input argument to seconds // according to the requested date/time representation // Precision is limited to microseconds allowing for maximum range // between [-32768-01-01, 32767-12-31]. Set to 'milliseconds' just // rounds to nearest millisecond, while 'nanoseconds' is ignored. ColumnVector Dnum = args(0).column_vector_value (); if (convertFrom == "datenum") { for (int i = 0; i < n; i++) { if (isnan (Dnum(i))) { Y(i) = NAN; M(i) = NAN; D(i) = NAN; h(i) = NAN; m(i) = NAN; s(i) = NAN; } else if (isinf (Dnum(i)) && Dnum(i) > 0) { Y(i) = INFINITY; M(i) = INFINITY; D(i) = INFINITY; h(i) = INFINITY; m(i) = INFINITY; s(i) = INFINITY; } else if (isinf (Dnum(i)) && Dnum(i) < 0) { Y(i) = -INFINITY; M(i) = -INFINITY; D(i) = -INFINITY; h(i) = -INFINITY; m(i) = -INFINITY; s(i) = -INFINITY; } else { time_sec = (Dnum(i) - 719529) * 86400; // to seconds RowVector OUT = seconds2vector (time_sec, precision, serial_unit (Dnum(i))); Y(i) = OUT(0); M(i) = OUT(1); D(i) = OUT(2); h(i) = OUT(3); m(i) = OUT(4); s(i) = OUT(5); } } } else if (convertFrom == "excel") { for (int i = 0; i < n; i++) { if (isnan (Dnum(i))) { Y(i) = NAN; M(i) = NAN; D(i) = NAN; h(i) = NAN; m(i) = NAN; s(i) = NAN; } else if (isinf (Dnum(i)) && Dnum(i) > 0) { Y(i) = INFINITY; M(i) = INFINITY; D(i) = INFINITY; h(i) = INFINITY; m(i) = INFINITY; s(i) = INFINITY; } else if (isinf (Dnum(i)) && Dnum(i) < 0) { Y(i) = -INFINITY; M(i) = -INFINITY; D(i) = -INFINITY; h(i) = -INFINITY; m(i) = -INFINITY; s(i) = -INFINITY; } else { if (Dnum(i) <= 60) { time_sec = (Dnum(i) - 25568) * 86400; // to seconds } else { time_sec = (Dnum(i) - 25569) * 86400; // to seconds } RowVector OUT = seconds2vector (time_sec, precision, serial_unit (Dnum(i))); Y(i) = OUT(0); M(i) = OUT(1); D(i) = OUT(2); h(i) = OUT(3); m(i) = OUT(4); s(i) = OUT(5); } } } else if (convertFrom == "posixtime") { for (int i = 0; i < n; i++) { if (isnan (Dnum(i))) { Y(i) = NAN; M(i) = NAN; D(i) = NAN; h(i) = NAN; m(i) = NAN; s(i) = NAN; } else if (isinf (Dnum(i)) && Dnum(i) > 0) { Y(i) = INFINITY; M(i) = INFINITY; D(i) = INFINITY; h(i) = INFINITY; m(i) = INFINITY; s(i) = INFINITY; } else if (isinf (Dnum(i)) && Dnum(i) < 0) { Y(i) = -INFINITY; M(i) = -INFINITY; D(i) = -INFINITY; h(i) = -INFINITY; m(i) = -INFINITY; s(i) = -INFINITY; } else { time_sec = Dnum(i); // already in seconds RowVector OUT = seconds2vector (time_sec, precision); Y(i) = OUT(0); M(i) = OUT(1); D(i) = OUT(2); h(i) = OUT(3); m(i) = OUT(4); s(i) = OUT(5); } } } else if (convertFrom == "epochtime") { if (doLeapSec) { for (int i = 0; i < n; i++) { if (isnan (Dnum(i))) { Y(i) = NAN; M(i) = NAN; D(i) = NAN; h(i) = NAN; m(i) = NAN; s(i) = NAN; } else if (isinf (Dnum(i)) && Dnum(i) > 0) { Y(i) = INFINITY; M(i) = INFINITY; D(i) = INFINITY; h(i) = INFINITY; m(i) = INFINITY; s(i) = INFINITY; } else if (isinf (Dnum(i)) && Dnum(i) < 0) { Y(i) = -INFINITY; M(i) = -INFINITY; D(i) = -INFINITY; h(i) = -INFINITY; m(i) = -INFINITY; s(i) = -INFINITY; } else { time_sec = Dnum(i); // already in seconds // This is a workaround, since I don't know how to properly account // for leap seconds in a fashion that these are added to the given // representation instead of being substructed auto dt = chrono::duration_cast (chrono::duration{time_sec}); auto tp0 = double2micro(0); auto from0 = make_zoned (timezone, tp0); auto from_utc = clock_cast(from0.get_sys_time()); auto to = make_zoned (timezone, clock_cast (from_utc + dt)); auto tp = double2micro(time_sec); auto ti = make_zoned (timezone, tp); auto out = make_zoned (timezone, ti.get_sys_time () + (ti.get_sys_time () - to.get_sys_time ())); RowVector OUT = tz2vector (out, precision); Y(i) = OUT(0); M(i) = OUT(1); D(i) = OUT(2); h(i) = OUT(3); m(i) = OUT(4); s(i) = OUT(5); } } } else { for (int i = 0; i < n; i++) { if (isnan (Dnum(i))) { Y(i) = NAN; M(i) = NAN; D(i) = NAN; h(i) = NAN; m(i) = NAN; s(i) = NAN; } else if (isinf (Dnum(i)) && Dnum(i) > 0) { Y(i) = INFINITY; M(i) = INFINITY; D(i) = INFINITY; h(i) = INFINITY; m(i) = INFINITY; s(i) = INFINITY; } else if (isinf (Dnum(i)) && Dnum(i) < 0) { Y(i) = -INFINITY; M(i) = -INFINITY; D(i) = -INFINITY; h(i) = -INFINITY; m(i) = -INFINITY; s(i) = -INFINITY; } else { time_sec = Dnum(i); // already in seconds RowVector OUT = seconds2vector (time_sec, precision); Y(i) = OUT(0); M(i) = OUT(1); D(i) = OUT(2); h(i) = OUT(3); m(i) = OUT(4); s(i) = OUT(5); } } } } else { string errmsg = "unsupported option for the 'ConvertFrom' parameter."; if (nargout == 7) { retval(6) = errmsg; return retval; } else { errmsg = "__datetime__: " + errmsg; error ("%s", errmsg.c_str ()); } } // Reshape output arguments retval(0) = Y.reshape (args(0).dims ()); retval(1) = M.reshape (args(0).dims ()); retval(2) = D.reshape (args(0).dims ()); retval(3) = h.reshape (args(0).dims ()); retval(4) = m.reshape (args(0).dims ()); retval(5) = s.reshape (args(0).dims ()); return retval; } // Handle single numeric matrix with either 3 or 6 columns // 'ConvertTo','fromposix' reads absolute instants, given as POSIX seconds, // into the wall clock of 'timezone' and reports the UTC offset in force at // each of them as a seventh output. A moment names one clock, so no // resolution is involved and the offset comes back from the same lookup that // produced the components -- which is the whole reason this exists as one // mode rather than as a conversion followed by a separate offset query. if (convertTo == "fromposix") { dim_vector sz = args(0).dims (); NDArray P = args(0).array_value (); NDArray Y(sz, 0), M(sz, 0), D(sz, 0), h(sz, 0), m(sz, 0), s(sz, 0); NDArray OFF(sz, 0); const zone_ref tzp = find_zone (timezone); sys_cache zc; for (int i = 0; i < sz.numel (); i++) { if (isnan (P(i))) { Y(i) = NAN; M(i) = NAN; D(i) = NAN; h(i) = NAN; m(i) = NAN; s(i) = NAN; OFF(i) = NAN; } else if (isinf (P(i))) { Y(i) = P(i); M(i) = P(i); D(i) = P(i); h(i) = P(i); m(i) = P(i); s(i) = P(i); OFF(i) = 0; } else if (timezone == "UTC" || ! tzp.tz) { // UTC and a fixed offset have no transitions, so the instant is read // straight off the calendar and any year survives. double off = (tzp.tz ? 0 : (double) tzp.fixed.offset.count ()); RowVector C = seconds2vector (P(i) + off, precision); Y(i) = C(0); M(i) = C(1); D(i) = C(2); h(i) = C(3); m(i) = C(4); s(i) = C(5); OFF(i) = off; } else { // Read the instant on the calendar first, in 64 bits, so that the year // is known before the zone is asked about it; a year outside what // 'date.h' holds is then moved by a whole multiple of 400 -- 146097 // days exactly, so the wall clock and its transitions are untouched -- // and put back on the answer. RowVector U = seconds2vector (P(i), precision); double sh = zone_year_shift (U(0)); RowVector C(6); double off; sys2components (P(i) - (sh / 400.0) * 146097.0 * 86400.0, tzp, C, off, zc); Y(i) = C(0) + sh; M(i) = C(1); D(i) = C(2); h(i) = C(3); m(i) = C(4); s(i) = C(5); OFF(i) = off; } } retval(0) = Y; retval(1) = M; retval(2) = D; retval(3) = h; retval(4) = m; retval(5) = s; if (nargout == 7) { retval(6) = OFF; } return retval; } if (nargin == 1) { int n = args(0).rows (); ColumnVector Y(n); // Years (numeric) ColumnVector M(n); // Months (numeric) ColumnVector D(n); // Days (numeric) ColumnVector h(n); // Hours (numeric) ColumnVector m(n); // Minutes (numeric) ColumnVector s(n); // Seconds (numeric) if (args(0).ndims () != 2) { string errmsg = "single numeric data input must be a matrix"; errmsg += " unless the 'ConvertFrom' parameter is used."; if (nargout == 7) { retval(6) = errmsg; return retval; } else { errmsg = "__datetime__: " + errmsg; error ("%s", errmsg.c_str ()); } } if (args(0).columns () != 3 && args(0).columns () != 6) { string errmsg = "single numeric matrix must have three or six"; errmsg += " columns unless the 'ConvertFrom' parameter is used."; if (nargout == 7) { retval(6) = errmsg; return retval; } else { errmsg = "__datetime__: " + errmsg; error ("%s", errmsg.c_str ()); } } if (args(0).columns () == 3) { Matrix YMD = args(0).matrix_value (); for (int i = 0; i < n; i++) { if (round (YMD(i,0)) != YMD(i,0) || round (YMD(i,1)) != YMD(i,1) || round (YMD(i,2)) != YMD(i,2)) { string errmsg = "Year, Month, and Day components"; errmsg += " must be integer values."; if (nargout == 7) { retval(6) = errmsg; return retval; } else { errmsg = "__datetime__: " + errmsg; error ("%s", errmsg.c_str ()); } } } for (int i = 0; i < n; i++) { RowVector tmp(3); tmp(0) = YMD(i,0); tmp(1) = YMD(i,1); tmp(2) = YMD(i,2); double out = check_nan_inf (tmp); if (isnan (out)) { Y(i) = NAN; M(i) = NAN; D(i) = NAN; h(i) = NAN; m(i) = NAN; s(i) = NAN; } else if (isinf (out)) { Y(i) = out; M(i) = out; D(i) = out; h(i) = out; m(i) = out; s(i) = out; } else { RowVector hms(6, 0); set_civil (civil::components2days (YMD(i,0), YMD(i,1), YMD(i,2), 0), hms, i, Y, M, D, h, m, s); } } } else if (args(0).columns () == 6) { Matrix YMDhms = args(0).matrix_value (); for (int i = 0; i < n; i++) { if (round (YMDhms(i,0)) != YMDhms(i,0) || round (YMDhms(i,1)) != YMDhms(i,1) || round (YMDhms(i,2)) != YMDhms(i,2) || round (YMDhms(i,3)) != YMDhms(i,3) || round (YMDhms(i,4)) != YMDhms(i,4)) { string errmsg = "Year, Month, Day, Hour, and Minute"; errmsg += " components must be integer values."; if (nargout == 7) { retval(6) = errmsg; return retval; } else { errmsg = "__datetime__: " + errmsg; error ("%s", errmsg.c_str ()); } } } for (int i = 0; i < n; i++) { RowVector tmp(6); tmp(0) = YMDhms(i,0); tmp(1) = YMDhms(i,1); tmp(2) = YMDhms(i,2); tmp(3) = YMDhms(i,3); tmp(4) = YMDhms(i,4); tmp(5) = YMDhms(i,5); double out = check_nan_inf (tmp); if (isnan (out)) { Y(i) = NAN; M(i) = NAN; D(i) = NAN; h(i) = NAN; m(i) = NAN; s(i) = NAN; } else if (isinf (out)) { Y(i) = out; M(i) = out; D(i) = out; h(i) = out; m(i) = out; s(i) = out; } else { // Aggregate hours, minutes, and seconds into seconds, calculate extra // days for later and retrieve remaining hours, minutes, and seconds int64_t dnum; RowVector hms; components2civil (YMDhms(i,0), YMDhms(i,1), YMDhms(i,2), YMDhms(i,3), YMDhms(i,4), YMDhms(i,5), 0, precision, dnum, hms); set_civil (dnum, hms, i, Y, M, D, h, m, s); } } } retval(0) = Y; retval(1) = M; retval(2) = D; retval(3) = h; retval(4) = m; retval(5) = s; return retval; } // Handle 3, 6, or 7 input data arguments if (nargin == 3 || nargin == 6 || nargin == 7) { // Check all input data arguments are numeric for (int i = 1; i < nargin; i++) { if (! args(i).isnumeric ()) { string errmsg = "input data arguments must be numeric."; if (nargout == 7) { retval(6) = errmsg; return retval; } else { errmsg = "__datetime__: " + errmsg; error ("%s", errmsg.c_str ()); } } } // Determine the common size: scalar arguments broadcast to the size of // the non-scalar arguments, which must all share a common size. dim_vector sz (1, 1); bool sized = false; for (int i = 0; i < nargin; i++) { if (args(i).is_scalar_type ()) { continue; } dim_vector sz1 = args(i).dims (); if (! sized) { sz = sz1; sized = true; } else if (sz1 != sz) { string errmsg = "numeric data input arguments"; errmsg += " must be of common size or scalars."; if (nargout == 7) { retval(6) = errmsg; return retval; } else { errmsg = "__datetime__: " + errmsg; error ("%s", errmsg.c_str ()); } } } // Initialize output vectors with input data NDArray Y = expand_input (sz, args(0)); // Years (numeric) NDArray M = expand_input (sz, args(1)); // Months (numeric) NDArray D = expand_input (sz, args(2)); // Days (numeric) NDArray h(sz, 0); // Hours (numeric) NDArray m(sz, 0); // Minutes (numeric) NDArray s(sz, 0); // Seconds (numeric) NDArray x(sz, 0); // Milliseconds (internal use) if (nargin > 3) { h = expand_input (sz, args(3)); m = expand_input (sz, args(4)); s = expand_input (sz, args(5)); } if (nargin > 6) { x = expand_input (sz, args(6)); } NDArray OF(sz, 0); if (haveOffset) { OF = expand_input (sz, offsetArg); } // Beyond this point, all input data have common size // 'ConvertTo','instant' returns the absolute instant of each element as // POSIX seconds, taken from the stored offset rather than by resolving the // wall clock. The offset already says which moment is meant, so this asks // the tz database nothing at all -- it is subtraction -- which is what // makes it the cheapest path in the class and the one every comparison, // ordering and set operation is built on. if (convertTo == "instant") { NDArray S(sz, 0); for (int i = 0; i < sz.numel (); i++) { RowVector tmp(7); tmp(0) = Y(i); tmp(1) = M(i); tmp(2) = D(i); tmp(3) = h(i); tmp(4) = m(i); tmp(5) = s(i); tmp(6) = x(i); double chk = check_nan_inf (tmp); if (isnan (chk)) { S(i) = NAN; } else if (isinf (chk)) { S(i) = chk; } else { // Counted in 64-bit days and seconds rather than built as a chrono // time point: microseconds in an 'int64' run out at about 292277 // years from the epoch, well inside the range the calendar now // carries, and they would wrap silently rather than refuse. int64_t dnum; RowVector hms; components2civil (Y(i), M(i), D(i), h(i), m(i), s(i), x(i), precision, dnum, hms); S(i) = (double) dnum * 86400.0 + hms(3) * 3600 + hms(4) * 60 + hms(5) - OF(i); } } retval(0) = S; return retval; } // 'ConvertTo','zoneoffset' returns the UTC offset in seconds that the wall // clock resolves to, and 'keepfold' the offset an element should carry when // it arrives on this clock from another: the one it came with, where that // still names this clock, and the resolved one otherwise. Both come from // one lookup. Not-A-Time and infinite elements take a zero offset, the // same value an unzoned array carries. if (convertTo == "zoneoffset" || convertTo == "keepfold") { bool useSrc = (convertTo == "keepfold") && haveOffset; NDArray A(sz, 0); const zone_ref tzp = find_zone (timezone); local_cache lc; for (int i = 0; i < sz.numel (); i++) { RowVector tmp(7); tmp(0) = Y(i); tmp(1) = M(i); tmp(2) = D(i); tmp(3) = h(i); tmp(4) = m(i); tmp(5) = s(i); tmp(6) = x(i); double chk = check_nan_inf (tmp); if (isnan (chk) || isinf (chk)) { A(i) = isnan (chk) ? NAN : 0; } else { local_fold lf = components2fold (Y(i), M(i), D(i), h(i), m(i), s(i), x(i), tzp, precision, OF(i), useSrc, lc); A(i) = useSrc ? lf.kept : lf.chosen; } } retval(0) = A; return retval; } // 'ConvertTo','rezone' expresses each element in 'toTimeZone' while keeping // its instant, returning the new components and, as a seventh output, the // offset in force there. It takes the instant from the stored offset, so // the source zone is not needed and -- more to the point -- nothing is // resolved on the way out: converting the clock instead would make the // database resolve it in the source zone first, which is the question with // two answers, and an element on the earlier pass would come out an hour // wrong. if (convertTo == "rezone") { NDArray rY(sz, 0), rM(sz, 0), rD(sz, 0); NDArray rh(sz, 0), rm(sz, 0), rs(sz, 0), rOFF(sz, 0); const zone_ref tzp = find_zone (to_tzone); sys_cache zc; for (int i = 0; i < sz.numel (); i++) { RowVector tmp(7); tmp(0) = Y(i); tmp(1) = M(i); tmp(2) = D(i); tmp(3) = h(i); tmp(4) = m(i); tmp(5) = s(i); tmp(6) = x(i); double chk = check_nan_inf (tmp); if (isnan (chk)) { rY(i) = NAN; rM(i) = NAN; rD(i) = NAN; rh(i) = NAN; rm(i) = NAN; rs(i) = NAN; rOFF(i) = NAN; } else if (isinf (chk)) { rY(i) = chk; rM(i) = chk; rD(i) = chk; rh(i) = chk; rm(i) = chk; rs(i) = chk; rOFF(i) = 0; } else { // Counted on the calendar rather than as a chrono time point, and // with a year outside 'date.h''s range moved by a whole multiple of // 400 before the target zone is asked about it -- 146097 days // exactly, so the instant keeps its place in the year. int64_t dnum; RowVector hms; components2civil (Y(i), M(i), D(i), h(i), m(i), s(i), x(i), precision, dnum, hms); double p = (double) dnum * 86400.0 + hms(3) * 3600 + hms(4) * 60 + hms(5) - OF(i); int64_t yf, mf, df; civil::civil_from_days (dnum, yf, mf, df); double sh = zone_year_shift ((double) yf); RowVector C(6); double off; sys2components (p - (sh / 400.0) * 146097.0 * 86400.0, tzp, C, off, zc); rY(i) = C(0) + sh; rM(i) = C(1); rD(i) = C(2); rh(i) = C(3); rm(i) = C(4); rs(i) = C(5); rOFF(i) = off; } } retval(0) = rY; retval(1) = rM; retval(2) = rD; retval(3) = rh; retval(4) = rm; retval(5) = rs; if (nargout == 7) { retval(6) = rOFF; } return retval; } // 'ConvertTo','posixtime' returns POSIX seconds (double) instead of the six // canonical components. The wall-clock components are interpreted in // 'timezone' (pass 'TimeZone','UTC' for unzoned datetimes so the serial is // free of any system-zone DST offset), Not-A-Time maps to NaN, and infinite // datetimes preserve their sign. if (convertTo == "posixtime") { const zone_ref tzp = find_zone (timezone); local_cache lc; NDArray S(sz, 0); for (int i = 0; i < sz.numel (); i++) { RowVector tmp(7); tmp(0) = Y(i); tmp(1) = M(i); tmp(2) = D(i); tmp(3) = h(i); tmp(4) = m(i); tmp(5) = s(i); tmp(6) = x(i); double chk = check_nan_inf (tmp); if (isnan (chk)) { S(i) = NAN; } else if (isinf (chk)) { S(i) = chk; } else if (timezone == "UTC" || ! tzp.tz) { // No offset to resolve, so the calendar answers on its own and any // year survives. int64_t dnum; RowVector hms; components2civil (Y(i), M(i), D(i), h(i), m(i), s(i), x(i), precision, dnum, hms); S(i) = (double) dnum * 86400.0 + hms(3) * 3600 + hms(4) * 60 + hms(5) - (tzp.tz ? 0 : (double) tzp.fixed.offset.count ()); } else { double sh; auto lt = components2localtime (Y(i), M(i), D(i), h(i), m(i), s(i), x(i), precision, sh); auto sys = local2sys (tzp, lt, lc); S(i) = (double) sys.time_since_epoch ().count () / 1000000.0 + (sh / 400.0) * 146097.0 * 86400.0; } } retval(0) = S; return retval; } // 'ConvertTo','zoneabbrev' returns the zone abbreviation (e.g. "EDT", // "EST", "UTC") active at each element, as a cell array of character // vectors. The wall-clock components are interpreted in 'timezone'; // Not-A-Time and infinite datetimes map to an empty string. if (convertTo == "zoneabbrev") { Cell A(sz); const zone_ref tzp = find_zone (timezone); local_cache lc; for (int i = 0; i < sz.numel (); i++) { RowVector tmp(7); tmp(0) = Y(i); tmp(1) = M(i); tmp(2) = D(i); tmp(3) = h(i); tmp(4) = m(i); tmp(5) = s(i); tmp(6) = x(i); double chk = check_nan_inf (tmp); if (isnan (chk) || isinf (chk)) { A(i) = ""; } else { A(i) = components2fold (Y(i), M(i), D(i), h(i), m(i), s(i), x(i), tzp, precision, OF(i), haveOffset, lc).abbrev; } } retval(0) = A; return retval; } // 'ConvertTo','dstoffset' returns the daylight saving part of the offset of // each element in seconds, as 'daylight_saving' counts it, for the pass // the element is on when an offset is given. Not-A-Time and infinite // datetimes map to NaN. if (convertTo == "dstoffset") { NDArray A(sz, 0); const zone_ref tzp = find_zone (timezone); local_cache lc; for (int i = 0; i < sz.numel (); i++) { RowVector tmp(7); tmp(0) = Y(i); tmp(1) = M(i); tmp(2) = D(i); tmp(3) = h(i); tmp(4) = m(i); tmp(5) = s(i); tmp(6) = x(i); double chk = check_nan_inf (tmp); if (isnan (chk) || isinf (chk)) { A(i) = NAN; } else { A(i) = components2fold (Y(i), M(i), D(i), h(i), m(i), s(i), x(i), tzp, precision, OF(i), haveOffset, lc).dst; } } retval(0) = A; return retval; } // 'ConvertTo','isdst' returns a 1/0 array (converted to logical by the // caller), true where daylight saving time is in effect at each element. // The wall-clock components are interpreted in 'timezone'; Not-A-Time and // infinite datetimes map to false. if (convertTo == "isdst") { NDArray A(sz, 0); const zone_ref tzp = find_zone (timezone); local_cache lc; for (int i = 0; i < sz.numel (); i++) { RowVector tmp(7); tmp(0) = Y(i); tmp(1) = M(i); tmp(2) = D(i); tmp(3) = h(i); tmp(4) = m(i); tmp(5) = s(i); tmp(6) = x(i); double chk = check_nan_inf (tmp); if (isnan (chk) || isinf (chk)) { A(i) = 0; } else { A(i) = components2fold (Y(i), M(i), D(i), h(i), m(i), s(i), x(i), tzp, precision, OF(i), haveOffset, lc).isdst ? 1 : 0; } } retval(0) = A; return retval; } // No zone was named, so there is no zone to resolve the wall clock // against: normalize the components and hand them straight back. The path // below instead read the clock in the machine's own zone and wrote it back // out in the same one, which is the identity everywhere EXCEPT where // 'resolve_local' moves a clock -- so an unzoned wall clock falling in the // machine's spring-forward gap was silently shifted past it, and the answer // an unzoned datetime gave depended on where the code ran. The matrix // forms of this constructor never consulted a zone, so the two spellings // also disagreed with each other. // UTC joins the no-zone case: it has no transitions, so resolving a wall // clock against it is the identity and the calendar answers alone. That is // what lets a UTC datetime hold a year the timezone database cannot, as // R2026a's does. if (! haveZone || (timezone == "UTC" && to_tzone == "UTC")) { for (int i = 0; i < sz.numel (); i++) { RowVector tmp(7); tmp(0) = Y(i); tmp(1) = M(i); tmp(2) = D(i); tmp(3) = h(i); tmp(4) = m(i); tmp(5) = s(i); tmp(6) = x(i); double out = check_nan_inf (tmp); if (isnan (out)) { Y(i) = NAN; M(i) = NAN; D(i) = NAN; h(i) = NAN; m(i) = NAN; s(i) = NAN; } else if (isinf (out)) { Y(i) = out; M(i) = out; D(i) = out; h(i) = out; m(i) = out; s(i) = out; } else { int64_t dnum; RowVector hms; components2civil (Y(i), M(i), D(i), h(i), m(i), s(i), x(i), precision, dnum, hms); set_civil (dnum, hms, i, Y, M, D, h, m, s); } } retval(0) = Y; retval(1) = M; retval(2) = D; retval(3) = h; retval(4) = m; retval(5) = s; return retval; } const zone_ref tzFrom = find_zone (timezone); const zone_ref tzTo = find_zone (to_tzone); local_cache lcn; sys_cache zcn; for (int i = 0; i < sz.numel (); i++) { RowVector tmp(7); tmp(0) = Y(i); tmp(1) = M(i); tmp(2) = D(i); tmp(3) = h(i); tmp(4) = m(i); tmp(5) = s(i); tmp(6) = x(i); double out = check_nan_inf (tmp); if (isnan (out)) { Y(i) = NAN; M(i) = NAN; D(i) = NAN; h(i) = NAN; m(i) = NAN; s(i) = NAN; x(i) = NAN; } else if (isinf (out)) { Y(i) = out; M(i) = out; D(i) = out; h(i) = out; m(i) = out; s(i) = out; x(i) = out; } else { // Fold the components and resolve the wall clock in its own zone; // the shift that brought it into range goes back on the year. double sh; auto datetime = components2localtime (Y(i), M(i), D(i), h(i), m(i), s(i), x(i), precision, sh); // Make timezone conversion, resolving the wall clock and reading the // instant back in the target zone through the cached lookups. auto sysp = local2sys (tzFrom, datetime, lcn); RowVector OUT(6); double dropOff; sys2components (sysp, tzTo, OUT, dropOff, precision, zcn); Y(i) = OUT(0) + sh; M(i) = OUT(1); D(i) = OUT(2); h(i) = OUT(3); m(i) = OUT(4); s(i) = OUT(5); } } // Return output arguments retval(0) = Y; retval(1) = M; retval(2) = D; retval(3) = h; retval(4) = m; retval(5) = s; return retval; } else { string errmsg = "numeric input data arguments must be"; errmsg += " three, six, or seven separate arrays."; if (nargout == 7) { retval(6) = errmsg; return retval; } else { errmsg = "__datetime__: " + errmsg; error ("%s", errmsg.c_str ()); } } // Should never reach this point! Exit safely, just in case. return retval; } /* ## Fixed-offset zone abbreviations: the widest output is seven characters, ## because 'parse_fixed_offset' admits at most '+23:59:59'. %!assert_equal (__datetime__ (2026, 3, 9, 14, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', '+23:59:59', ... %! 'Precision', 'microseconds'), {'+235959'}) %!assert_equal (__datetime__ (2026, 3, 9, 14, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', '-23:59:59', ... %! 'Precision', 'microseconds'), {'-235959'}) %!assert_equal (__datetime__ (2026, 3, 9, 14, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', '+05:30', ... %! 'Precision', 'microseconds'), {'+0530'}) %!assert_equal (__datetime__ (2026, 3, 9, 14, 0, 0, 'ConvertTo', ... %! 'zoneabbrev', 'TimeZone', '+00:00', ... %! 'Precision', 'microseconds'), {'GMT'}) %!error <__datetime__: invalid string value for 'TimeZone'.> ... %! __datetime__ (2026, 3, 9, 14, 0, 0, 'ConvertTo', 'zoneabbrev', ... %! 'TimeZone', '+24:00', 'Precision', 'microseconds') */ pr0m1th3as-datatypes-9450b29/src/__dictionary2json__.cc000066400000000000000000000207461527370255000227220ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the datatypes package for GNU Octave. 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 . */ #include #include #include #include #include #include #include #include #include using namespace std; class json_writer { public: json_writer (bool pretty, bool preserve) : out (), pretty (pretty), preserve (preserve) { } string out; void value (const octave_value& v, int level) { if (v.isstruct ()) { octave_map m = v.map_value (); if (m.nfields () == 0) out += "null"; else object (m.contents ("keys")(0).cell_value (), m.contents ("values")(0).cell_value (), level); } else if (v.iscell ()) array (v.cell_value (), level); else if (v.is_string ()) text (v.string_value ()); else if (v.numel () != 1) numbers (v, level); else scalar (v); } private: bool pretty; bool preserve; void newline (int level) { if (pretty) { out += '\n'; out.append (4 * level, ' '); } } void object (const Cell& keys, const Cell& vals, int level) { if (keys.numel () == 0) { out += "{}"; return; } out += '{'; for (octave_idx_type k = 0; k < keys.numel (); k++) { if (k > 0) out += ','; newline (level + 1); key (keys(k)); out += pretty ? ": " : ":"; value (vals(k), level + 1); } newline (level); out += '}'; } // A key given as text, or as a number or logical value written as text void key (const octave_value& k) { if (k.is_string ()) text (k.string_value ()); else if (k.islogical ()) text (k.bool_value () ? "true" : "false"); else if (k.isinteger ()) text (integer (k)); else text (shortest (k.double_value (), false)); } void array (const Cell& items, int level) { if (items.numel () == 0) { out += "[]"; return; } out += '['; for (octave_idx_type k = 0; k < items.numel (); k++) { if (k > 0) out += ','; newline (level + 1); value (items(k), level + 1); } newline (level); out += ']'; } // A numeric or logical vector, as an array of its elements void numbers (const octave_value& v, int level) { octave_idx_type n = v.numel (); if (n == 0) { out += "[]"; return; } // Indexing needs a value it may change octave_value w = v; out += '['; for (octave_idx_type k = 0; k < n; k++) { if (k > 0) out += ','; newline (level + 1); scalar (w.index_op (octave_value_list (octave_value (k + 1)))); } newline (level); out += ']'; } void scalar (const octave_value& v) { if (v.islogical ()) out += v.bool_value () ? "true" : "false"; else if (v.isinteger ()) out += integer (v); else { double d = v.double_value (); if (std::isnan (d)) out += preserve ? "NaN" : "null"; else if (std::isinf (d)) out += preserve ? (d > 0 ? "Infinity" : "-Infinity") : "null"; else out += shortest (d, true); } } // An integer of any class, exactly static string integer (const octave_value& v) { char buf[32]; if (v.is_uint64_type ()) snprintf (buf, sizeof (buf), "%llu", static_cast (v.uint64_scalar_value ().value ())); else if (v.is_int64_type ()) snprintf (buf, sizeof (buf), "%lld", static_cast (v.int64_scalar_value ().value ())); else if (v.is_uint32_type () || v.is_uint16_type () || v.is_uint8_type ()) snprintf (buf, sizeof (buf), "%llu", static_cast (v.uint32_scalar_value ().value ())); else snprintf (buf, sizeof (buf), "%lld", static_cast (v.int32_scalar_value ().value ())); return string (buf); } // The double D in the fewest digits that read back as D, laid out as // RapidJSON lays them out: in full up to 1e21, with '.0' when integral // and POINT is true, as 0.000ddd down to 1e-6, else with an exponent static string shortest (double d, bool point) { if (std::isnan (d)) return "NaN"; if (std::isinf (d)) return d > 0 ? "Inf" : "-Inf"; string sign = std::signbit (d) ? "-" : ""; double a = fabs (d); if (a == 0) return sign + (point ? "0.0" : "0"); char buf[40]; int p = 1; for (; p <= 17; p++) { snprintf (buf, sizeof (buf), "%.*e", p - 1, a); if (strtod (buf, nullptr) == a) break; } // The digits and the decimal exponent of the first one string mant (buf, strchr (buf, 'e') - buf); int e10 = atoi (strchr (buf, 'e') + 1); string digits; for (char c : mant) if (c != '.') digits += c; while (digits.size () > 1 && digits.back () == '0') digits.pop_back (); int n = digits.size (); int kk = e10 + 1; string r; if (kk >= n && kk <= 21) { r = digits + string (kk - n, '0'); if (point) r += ".0"; } else if (kk > 0 && kk <= 21) r = digits.substr (0, kk) + "." + digits.substr (kk); else if (kk > -6 && kk <= 0) r = "0." + string (-kk, '0') + digits; else if (n == 1) r = digits + "e" + to_string (kk - 1); else r = digits.substr (0, 1) + "." + digits.substr (1) + "e" + to_string (kk - 1); return sign + r; } void text (const string& s) { out += '"'; for (unsigned char c : s) { switch (c) { case '"': out += "\\\""; break; case '\\': out += "\\\\"; break; case '\n': out += "\\n"; break; case '\r': out += "\\r"; break; case '\t': out += "\\t"; break; case '\b': out += "\\b"; break; case '\f': out += "\\f"; break; default: if (c < 0x20) { char buf[8]; snprintf (buf, sizeof (buf), "\\u%04x", c); out += buf; } else out += static_cast (c); } } out += '"'; } }; DEFUN_DLD (__dictionary2json__, args, nargout, "-*- texinfo -*-\n \ @deftypefn {datatypes} {@var{errmsg} =} __dictionary2json__ (@var{tree}, @var{file}, @var{pretty}, @var{preserve})\n\ \n\ \n\ Write a tree of JSON values to a file.\n\ \n\ @var{tree} has the form @qcode{__json2dictionary__} returns: an object is a \ scalar structure with the fields @qcode{keys} and @qcode{values}, the keys \ text, numbers or logical values; an array a cell array; text a character \ row; a number or logical value a scalar, a vector of them an array; and \ @qcode{null} a 0-by-0 structure array with no fields. @var{pretty} \ indents by four spaces, @var{preserve} writes @code{NaN} and @code{Inf} \ rather than @qcode{null}. @var{errmsg} is empty, or names the file that \ could not be written.\n\ \n\ This is a helper IO function for the @qcode{writedictionary} method. Do \ NOT call it directly.\n\ \n\ @end deftypefn") { if (args.length () != 4) error ("__dictionary2json__: four input arguments are required."); json_writer w (args(2).bool_value (), args(3).bool_value ()); w.value (args(0), 0); string file = args(1).string_value (); string errmsg; ofstream fd (file.c_str (), ios::out | ios::binary | ios::trunc); if (! fd.is_open ()) errmsg = "unable to open the file '" + file + "' for writing."; else { fd << w.out; fd.close (); if (fd.fail ()) errmsg = "unable to write the file '" + file + "'."; } octave_value_list retval (nargout > 0 ? 1 : 0); if (nargout > 0) retval(0) = errmsg; return retval; } pr0m1th3as-datatypes-9450b29/src/__json2dictionary__.cc000066400000000000000000000404661527370255000227230ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the datatypes package for GNU Octave. 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 . */ #include #include #include #include #include #include #include #include #include #include using namespace std; // Nesting deeper than this is refused: readdictionary follows the tree // recursively, a few calls a level, within Octave's recursion limit static const int max_depth = 64; class json_parser { public: json_parser (const string& text, bool comments, bool commas, bool infnan) : s (text), pos (0), allow_comments (comments), allow_commas (commas), allow_infnan (infnan), err () { } // The value of the whole text, or an error message in ERR octave_value parse () { // A UTF-8 byte order mark is not part of the text if (s.compare (0, 3, "\xEF\xBB\xBF") == 0) pos = 3; octave_value v; if (! skip_space ()) return v; if (pos >= s.size ()) { fail ("the text holds no value"); return v; } v = value (0); if (! err.empty ()) return octave_value (); if (! skip_space ()) return octave_value (); if (pos < s.size ()) { fail ("unexpected text after the value"); return octave_value (); } return v; } const string& error_message () const { return err; } private: const string& s; size_t pos; bool allow_comments; bool allow_commas; bool allow_infnan; string err; // Record the first error, with the line and column of POS void fail (const string& what) { if (! err.empty ()) return; size_t line = 1; size_t col = 1; for (size_t k = 0; k < pos && k < s.size (); k++) { if (s[k] == '\n') { line++; col = 1; } // A column counts characters, not the bytes that continue one else if ((static_cast (s[k]) & 0xC0) != 0x80) col++; } err = what + " at line " + to_string (line) + ", column " + to_string (col) + "."; } // Skip white space and, when allowed, comments; false on an error bool skip_space () { while (pos < s.size ()) { char c = s[pos]; if (c == ' ' || c == '\t' || c == '\n' || c == '\r') pos++; else if (c == '/' && pos + 1 < s.size () && (s[pos+1] == '/' || s[pos+1] == '*')) { if (! allow_comments) { fail ("comments are not allowed"); return false; } if (s[pos+1] == '/') { while (pos < s.size () && s[pos] != '\n') pos++; } else { size_t end = s.find ("*/", pos + 2); if (end == string::npos) { fail ("unterminated comment"); return false; } pos = end + 2; } } else break; } return true; } bool starts_with (const char *word) const { return s.compare (pos, strlen (word), word) == 0; } octave_value value (int depth) { if (depth > max_depth) { fail ("nesting too deep"); return octave_value (); } if (pos >= s.size ()) { fail ("unexpected end of the text"); return octave_value (); } char c = s[pos]; if (c == '{') return object (depth); if (c == '[') return array (depth); if (c == '"') { string str; if (! text (str)) return octave_value (); return octave_value (str); } if (starts_with ("true")) { pos += 4; return octave_value (true); } if (starts_with ("false")) { pos += 5; return octave_value (false); } if (starts_with ("null")) { pos += 4; // A struct array of no elements and no fields marks a null return octave_value (octave_map (dim_vector (0, 0))); } if (c == '-' || (c >= '0' && c <= '9') || c == 'N' || c == 'I') return number (); fail ("invalid value"); return octave_value (); } octave_value object (int depth) { pos++; // Gathered first and made cells once: growing a cell copies it vector keys; vector vals; if (! skip_space ()) return octave_value (); if (pos < s.size () && s[pos] == '}') { pos++; return make_object (keys, vals); } while (true) { if (pos >= s.size () || s[pos] != '"') { if (pos < s.size () && s[pos] == '}' && ! keys.empty () && ! allow_commas) fail ("trailing commas are not allowed"); else fail ("missing object key"); return octave_value (); } string key; if (! text (key)) return octave_value (); if (! skip_space ()) return octave_value (); if (pos >= s.size () || s[pos] != ':') { fail ("missing ':' after an object key"); return octave_value (); } pos++; if (! skip_space ()) return octave_value (); octave_value v = value (depth + 1); if (! err.empty ()) return octave_value (); keys.push_back (octave_value (key)); vals.push_back (v); if (! skip_space ()) return octave_value (); if (pos < s.size () && s[pos] == ',') { pos++; if (! skip_space ()) return octave_value (); if (pos < s.size () && s[pos] == '}' && allow_commas) { pos++; break; } continue; } if (pos < s.size () && s[pos] == '}') { pos++; break; } fail ("missing ',' or '}' in an object"); return octave_value (); } return make_object (keys, vals); } // A column cell of ITEMS static Cell column (const vector& items) { Cell c (dim_vector (items.size (), 1)); for (size_t k = 0; k < items.size (); k++) c(k) = items[k]; return c; } static octave_value make_object (const vector& keys, const vector& vals) { octave_scalar_map m; m.assign ("keys", octave_value (column (keys))); m.assign ("values", octave_value (column (vals))); return octave_value (m); } octave_value array (int depth) { pos++; vector items; if (! skip_space ()) return octave_value (); if (pos < s.size () && s[pos] == ']') { pos++; return octave_value (column (items)); } while (true) { if (pos < s.size () && s[pos] == ']') { fail (allow_commas ? "invalid value" : "trailing commas are not allowed"); return octave_value (); } octave_value v = value (depth + 1); if (! err.empty ()) return octave_value (); items.push_back (v); if (! skip_space ()) return octave_value (); if (pos < s.size () && s[pos] == ',') { pos++; if (! skip_space ()) return octave_value (); if (pos < s.size () && s[pos] == ']' && allow_commas) { pos++; break; } continue; } if (pos < s.size () && s[pos] == ']') { pos++; break; } fail ("missing ',' or ']' in an array"); return octave_value (); } return octave_value (column (items)); } static void put_utf8 (string& out, unsigned long cp) { if (cp < 0x80) out += static_cast (cp); else if (cp < 0x800) { out += static_cast (0xC0 | (cp >> 6)); out += static_cast (0x80 | (cp & 0x3F)); } else if (cp < 0x10000) { out += static_cast (0xE0 | (cp >> 12)); out += static_cast (0x80 | ((cp >> 6) & 0x3F)); out += static_cast (0x80 | (cp & 0x3F)); } else { out += static_cast (0xF0 | (cp >> 18)); out += static_cast (0x80 | ((cp >> 12) & 0x3F)); out += static_cast (0x80 | ((cp >> 6) & 0x3F)); out += static_cast (0x80 | (cp & 0x3F)); } } // Four hexadecimal digits at POS, or -1 long hex4 () { if (pos + 4 > s.size ()) return -1; long v = 0; for (int k = 0; k < 4; k++) { char c = s[pos+k]; v <<= 4; if (c >= '0' && c <= '9') v |= c - '0'; else if (c >= 'a' && c <= 'f') v |= c - 'a' + 10; else if (c >= 'A' && c <= 'F') v |= c - 'A' + 10; else return -1; } pos += 4; return v; } // The text at POS, which starts with its opening quote, into OUT bool text (string& out) { pos++; while (true) { if (pos >= s.size ()) { fail ("unterminated text"); return false; } unsigned char c = s[pos]; if (c == '"') { pos++; return true; } if (c < 0x20) { fail ("a control character in text must be escaped"); return false; } if (c != '\\') { out += static_cast (c); pos++; continue; } pos++; if (pos >= s.size ()) { fail ("unterminated text"); return false; } char e = s[pos++]; switch (e) { case '"': out += '"'; break; case '\\': out += '\\'; break; case '/': out += '/'; break; case 'b': out += '\b'; break; case 'f': out += '\f'; break; case 'n': out += '\n'; break; case 'r': out += '\r'; break; case 't': out += '\t'; break; case 'u': { long cp = hex4 (); if (cp < 0) { fail ("invalid \\u escape"); return false; } // A high surrogate must be followed by a low one if (cp >= 0xD800 && cp <= 0xDBFF) { long lo = -1; if (pos + 1 < s.size () && s[pos] == '\\' && s[pos+1] == 'u') { pos += 2; lo = hex4 (); } if (lo < 0xDC00 || lo > 0xDFFF) { fail ("invalid \\u escape"); return false; } cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); } else if (cp >= 0xDC00 && cp <= 0xDFFF) { fail ("invalid \\u escape"); return false; } put_utf8 (out, cp); break; } default: pos--; fail ("invalid escape in text"); return false; } } } octave_value number () { size_t start = pos; bool neg = false; if (s[pos] == '-') { neg = true; pos++; } // The lenient forms of the non-finite numbers if (allow_infnan) { if (! neg && starts_with ("NaN")) { pos += 3; return octave_value (octave_NaN); } if (starts_with ("Infinity") || starts_with ("Inf")) { pos += starts_with ("Infinity") ? 8 : 3; return octave_value (neg ? -octave_Inf : octave_Inf); } } if (pos >= s.size () || s[pos] < '0' || s[pos] > '9') { pos = start; fail ("invalid value"); return octave_value (); } // No leading zeros, as JSON writes numbers if (s[pos] == '0' && pos + 1 < s.size () && s[pos+1] >= '0' && s[pos+1] <= '9') { pos = start; fail ("invalid number"); return octave_value (); } while (pos < s.size () && s[pos] >= '0' && s[pos] <= '9') pos++; bool integral = true; if (pos < s.size () && s[pos] == '.') { integral = false; pos++; if (pos >= s.size () || s[pos] < '0' || s[pos] > '9') { fail ("invalid number"); return octave_value (); } while (pos < s.size () && s[pos] >= '0' && s[pos] <= '9') pos++; } if (pos < s.size () && (s[pos] == 'e' || s[pos] == 'E')) { integral = false; pos++; if (pos < s.size () && (s[pos] == '+' || s[pos] == '-')) pos++; if (pos >= s.size () || s[pos] < '0' || s[pos] > '9') { fail ("invalid number"); return octave_value (); } while (pos < s.size () && s[pos] >= '0' && s[pos] <= '9') pos++; } string lit = s.substr (start, pos - start); double d = strtod (lit.c_str (), nullptr); // An integer beyond 2^53 is kept exactly when a 64-bit type holds it. // The double rounds 2^53 + 1 down to 2^53, so the test is on the exact // integer. const long long flint = 9007199254740992LL; if (integral && fabs (d) >= 9007199254740992.0) { errno = 0; if (neg) { long long v = strtoll (lit.c_str (), nullptr, 10); if (errno == 0 && v < -flint) return octave_value (octave_int64 (static_cast (v))); } else { unsigned long long v = strtoull (lit.c_str (), nullptr, 10); if (errno == 0 && v > static_cast (flint)) return octave_value (octave_uint64 (static_cast (v))); } } return octave_value (d); } }; DEFUN_DLD (__json2dictionary__, args, nargout, "-*- texinfo -*-\n \ @deftypefn {datatypes} {[@var{tree}, @var{errmsg}] =} __json2dictionary__ (@var{text}, @var{comments}, @var{commas}, @var{infnan})\n\ \n\ \n\ Parse JSON text into a tree that keeps the kinds of JSON values.\n\ \n\ An object is a scalar structure with the fields @qcode{keys} and \ @qcode{values}, two column cell arrays in file order, duplicate keys \ kept. An array is a column cell array, text a character row, a number a \ @code{double}, or an @code{int64} or @code{uint64} for an integer beyond \ 2^53 that one holds exactly, @qcode{true} and @qcode{false} logical \ scalars, and @qcode{null} a 0-by-0 structure array with no fields. \ @var{comments}, @var{commas} and @var{infnan} allow comments, trailing \ commas and the non-finite numbers. On an error @var{tree} is empty and \ @var{errmsg} names it with its line and column.\n\ \n\ This is a helper IO function for the @qcode{readdictionary} function. Do \ NOT call it directly.\n\ \n\ @end deftypefn") { if (args.length () != 4) error ("__json2dictionary__: four input arguments are required."); if (! args(0).is_string ()) error ("__json2dictionary__: TEXT must be a character vector."); string text = args(0).string_value (); json_parser p (text, args(1).bool_value (), args(2).bool_value (), args(3).bool_value ()); octave_value tree = p.parse (); octave_value_list retval (nargout > 1 ? 2 : 1); retval(0) = p.error_message ().empty () ? tree : octave_value (Matrix ()); if (nargout > 1) retval(1) = p.error_message (); return retval; } pr0m1th3as-datatypes-9450b29/src/__ldml__.cc000066400000000000000000001356731527370255000205370ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the datatypes package for GNU Octave. 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 . */ #include #include #include #include #include #include #include using namespace std; // Locale name tables for the LDML 'InputFormat' parser. These mirror the // tables returned by dtLocaleNames in datetime.m: full and abbreviated month // names, full and abbreviated weekday names (Sunday first), and the two day // period markers. All entries are lower case; matching folds case and Greek // accents, so only the plain forms are needed here. Spanish day periods carry // a no-break space (U+00A0), matching MATLAB. struct LocaleNames { const char *lang; const char *mFull[12]; const char *mAbbr[12]; const char *wFull[7]; const char *wAbbr[7]; const char *dpMark[2]; }; static const LocaleNames LOCALES[] = { {"en", {"january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"}, {"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"}, {"sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"}, {"sun", "mon", "tue", "wed", "thu", "fri", "sat"}, {"am", "pm"}}, {"fr", {"janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"}, {"janv", "févr", "mars", "avr", "mai", "juin", "juil", "août", "sept", "oct", "nov", "déc"}, {"dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"}, {"dim", "lun", "mar", "mer", "jeu", "ven", "sam"}, {"am", "pm"}}, {"de", {"januar", "februar", "märz", "april", "mai", "juni", "juli", "august", "september", "oktober", "november", "dezember"}, {"jan", "feb", "märz", "apr", "mai", "juni", "juli", "aug", "sept", "okt", "nov", "dez"}, {"sonntag", "montag", "dienstag", "mittwoch", "donnerstag", "freitag", "samstag"}, {"so", "mo", "di", "mi", "do", "fr", "sa"}, {"am", "pm"}}, {"es", {"enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"}, {"ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sept", "oct", "nov", "dic"}, {"domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado"}, {"dom", "lun", "mar", "mié", "jue", "vie", "sáb"}, // Spelled with an explicit no-break space (U+00A0), as MATLAB has it; a // literal one here is too easily flattened to a plain space by an editor. {"a.\u00A0m.", "p.\u00A0m."}}, {"it", {"gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno", "luglio", "agosto", "settembre", "ottobre", "novembre", "dicembre"}, {"gen", "feb", "mar", "apr", "mag", "giu", "lug", "ago", "set", "ott", "nov", "dic"}, {"domenica", "lunedì", "martedì", "mercoledì", "giovedì", "venerdì", "sabato"}, {"dom", "lun", "mar", "mer", "gio", "ven", "sab"}, {"am", "pm"}}, {"pt", {"janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"}, {"jan", "fev", "mar", "abr", "mai", "jun", "jul", "ago", "set", "out", "nov", "dez"}, {"domingo", "segunda-feira", "terça-feira", "quarta-feira", "quinta-feira", "sexta-feira", "sábado"}, {"dom", "seg", "ter", "qua", "qui", "sex", "sáb"}, {"am", "pm"}}, {"el", {"Ιανουα�ίου", "Φεβ�ουα�ίου", "Μα�τίου", "Απ�ιλίου", "Μα�ου", "Ιουνίου", "Ιουλίου", "Αυγο�στου", "Σεπτεμβ�ίου", "Οκτωβ�ίου", "�οεμβ�ίου", "Δεκεμβ�ίου"}, {"Ιαν", "Φεβ", "Μα�", "Απ�", "Μα�", "Ιουν", "Ιουλ", "Αυγ", "Σεπ", "Οκτ", "�οε", "Δεκ"}, {"Κυ�ιακή", "Δευτέ�α", "Τ�ίτη", "Τετά�τη", "Πέμπτη", "Πα�ασκευή", "Σάββατο"}, {"Κυ�", "Δευ", "Τ�ί", "Τετ", "Πέμ", "Πα�", "Σάβ"}, {"π.μ.", "μ.μ."}} }; static const int NLOCALES = sizeof (LOCALES) / sizeof (LOCALES[0]); // Decode a UTF-8 byte string into code points. Malformed bytes are passed // through as their own code point, which cannot match any table entry and so // simply fails to parse, as it does in the m-code path. static void utf8_decode (const string& s, vector& cp) { cp.clear (); size_t i = 0, n = s.size (); while (i < n) { unsigned char c = static_cast (s[i]); uint32_t u; int extra; if (c < 0x80) { u = c; extra = 0; } else if ((c & 0xE0) == 0xC0) { u = c & 0x1F; extra = 1; } else if ((c & 0xF0) == 0xE0) { u = c & 0x0F; extra = 2; } else if ((c & 0xF8) == 0xF0) { u = c & 0x07; extra = 3; } else { cp.push_back (c); i++; continue; } if (i + extra >= n) { cp.push_back (c); i++; continue; } bool ok = true; for (int k = 1; k <= extra; k++) { unsigned char cc = static_cast (s[i+k]); if ((cc & 0xC0) != 0x80) { ok = false; break; } u = (u << 6) | (cc & 0x3F); } if (! ok) { cp.push_back (c); i++; continue; } cp.push_back (u); i += extra + 1; } } // Case- and accent-fold a UTF-8 string to a code point vector. ASCII // capitals, the Latin-1 capitals and the contiguous Greek capitals go to // lower case by +32; the accented Greek capitals, the accented lower case // vowels, the dialytika forms and the final sigma map to their plain lower // case base letters. Latin accents are kept, so 'MÄRZ' matches 'märz' but // 'marz' does not, as in MATLAB. static void case_fold (const string& s, vector& cp) { static const uint32_t from[] = {902, 904, 905, 906, 908, 910, 911, 938, 939, 940, 941, 942, 943, 972, 973, 974, 970, 971, 912, 944, 962}; static const uint32_t to[] = {945, 949, 951, 953, 959, 965, 969, 953, 965, 945, 949, 951, 953, 959, 965, 969, 953, 965, 953, 965, 963}; static const int nmap = sizeof (from) / sizeof (from[0]); utf8_decode (s, cp); for (size_t i = 0; i < cp.size (); i++) { uint32_t u = cp[i]; if (u >= 65 && u <= 90) { u += 32; } else if (u >= 0xC0 && u <= 0xDE && u != 0xD7) { // The Latin-1 capitals, but for the multiplication sign u += 32; } else if (u >= 913 && u <= 937) { u += 32; } for (int k = 0; k < nmap; k++) { if (u == from[k]) { u = to[k]; break; } } cp[i] = u; } } // Folded name tables for one locale, built once and reused. struct FoldedLocale { bool built; vector mFull[12]; vector mAbbr[12]; vector wFull[7]; vector wAbbr[7]; vector dpMark[2]; }; static FoldedLocale FOLDED[NLOCALES]; // Resolve a 'Locale' string to an index into LOCALES, mirroring the // normalization in dtLocaleNames: empty or 'system' means English, and a // regional suffix ('fr_FR' or 'fr-FR') is ignored. Returns -1 for an unknown // language. static int locale_index (const string& locale) { string lang; for (size_t i = 0; i < locale.size (); i++) { char c = locale[i]; if (c == '_' || c == '-') { break; } lang += static_cast (tolower (static_cast (c))); } if (lang.empty () || lang == "system") { lang = "en"; } for (int k = 0; k < NLOCALES; k++) { if (lang == LOCALES[k].lang) { return k; } } return -1; } static const FoldedLocale& folded_locale (int idx) { FoldedLocale& f = FOLDED[idx]; if (! f.built) { const LocaleNames& L = LOCALES[idx]; for (int k = 0; k < 12; k++) { case_fold (L.mFull[k], f.mFull[k]); case_fold (L.mAbbr[k], f.mAbbr[k]); } for (int k = 0; k < 7; k++) { case_fold (L.wFull[k], f.wFull[k]); case_fold (L.wAbbr[k], f.wAbbr[k]); } for (int k = 0; k < 2; k++) { case_fold (L.dpMark[k], f.dpMark[k]); } f.built = true; } return f; } // The month a regional name stands for, or 0: Austrian German writes January // as 'Jänner' ('Jän'), which MATLAB reads under 'de_AT' alone. static int regional_month (const string& word, const string& locale) { string tag; for (size_t i = 0; i < locale.size (); i++) tag += (locale[i] == '-') ? '_' : static_cast (tolower (static_cast (locale[i]))); if (tag != "de_at") return 0; vector fw, full, abbr; case_fold (word, fw); case_fold ("jänner", full); case_fold ("jän", abbr); return (fw == full || fw == abbr) ? 1 : 0; } // Return the 1-based index of the first entry of TBL whose folded form matches // WORD, or 0 if none does. static int fold_find (const string& word, const vector *tbl, int n) { vector fw; case_fold (word, fw); for (int k = 0; k < n; k++) { if (fw == tbl[k]) { return k + 1; } } return 0; } // One LDML token: either a field run (sym set, n = run length) or a literal // (sym = '\0', lit = the literal text). struct Token { char sym; int n; string lit; }; // Split an LDML pattern into tokens. Single quotes delimit literal text and // a doubled '' is a literal apostrophe. static void tokenize (const string& fmt, vector& toks) { toks.clear (); size_t i = 0, L = fmt.size (); while (i < L) { char c = fmt[i]; if (c == '\'') { if (i + 1 < L && fmt[i+1] == '\'') { Token t; t.sym = '\0'; t.n = 0; t.lit = "'"; toks.push_back (t); i += 2; } else { size_t j = i + 1; string buf; while (j < L && fmt[j] != '\'') { buf += fmt[j]; j++; } Token t; t.sym = '\0'; t.n = 0; t.lit = buf; toks.push_back (t); i = j + 1; } } else if (isalpha (static_cast (c))) { size_t j = i; while (j < L && fmt[j] == c) { j++; } Token t; t.sym = c; t.n = static_cast (j - i); toks.push_back (t); i = j; } else { Token t; t.sym = '\0'; t.n = 0; t.lit = string (1, c); toks.push_back (t); i++; } } } // A name character is an ASCII letter or any byte above 127, which is a lead // or continuation byte of an accented UTF-8 letter. static inline bool is_name_char (char c) { unsigned char u = static_cast (c); return (u > 127 || isalpha (u)); } // Days from the civil epoch 1970-01-01 (Howard Hinnant's algorithm), used for // the day-of-year field so that an out-of-range value rolls exactly as // datenum/datevec do in the m-code path. Counts are 'int64_t', never 'long', // which is only 32 bits on Windows. static int64_t days_from_civil (int64_t y, unsigned m, unsigned d) { y -= m <= 2; const int64_t era = (y >= 0 ? y : y - 399) / 400; const unsigned yoe = static_cast (y - era * 400); const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; return era * 146097 + static_cast (doe) - 719468; } static void civil_from_days (int64_t z, int64_t& y, unsigned& m, unsigned& d) { z += 719468; const int64_t era = (z >= 0 ? z : z - 146096) / 146097; const unsigned doe = static_cast (z - era * 146097); const unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; y = static_cast (yoe) + era * 400; const unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100); const unsigned mp = (5 * doy + 2) / 153; d = doy - (153 * mp + 2) / 5 + 1; m = mp + (mp < 10 ? 3 : -9); y += (m <= 2); } // Length of a month, for validating a parsed date. Returns 0 for a month // outside 1..12, which fails the day test for any day. static int days_in_month (double Y, double M) { static const int dpm[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; int m = static_cast (M); if (m < 1 || m > 12) { return 0; } if (m == 2) { int64_t y = static_cast (Y); bool leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0); return (leap ? 29 : 28); } return dpm[m-1]; } // Time zone abbreviations the 'z' field reads, with their offsets from UTC in // seconds: every name MATLAB R2024a reads in any of the thirteen locales // probed, at the one offset it has wherever it is read, and MSK, TRT, AZT and // GET, which it reads in none. A name with two meanings is never here but in // ZONE_ABBR_LOCALE. struct ZoneAbbr { const char *name; int off; }; static const ZoneAbbr ZONE_ABBR[] = { {"ACDT", 37800}, {"ADT", -10800}, {"AEDT", 39600}, {"AEST", 36000}, {"AFT", 16200}, {"AKDT", -28800}, {"AKST", -32400}, {"AMST", -10800}, {"AST", -14400}, {"AWDT", 32400}, {"AWST", 28800}, {"AZOST", 0}, {"AZOT", -3600}, {"AZT", 14400}, {"BRST", -7200}, {"BRT", -10800}, {"CDT", -18000}, {"CEST", 7200}, {"CET", 3600}, {"CST", -21600}, {"CXT", 25200}, {"EAST", -21600}, {"ECT", -18000}, {"EDT", -14400}, {"EEST", 10800}, {"EET", 7200}, {"EST", -18000}, {"FKST", -7200}, {"GALT", -21600}, {"GET", 14400}, {"GMT", 0}, {"GST", 14400}, {"HDT", -32400}, {"HST", -36000}, {"ICT", 25200}, {"IRDT", 16200}, {"IRST", 12600}, {"MDT", -21600}, {"MESZ", 7200}, {"MEZ", 3600}, {"MSK", 10800}, {"MST", -25200}, {"MVT", 18000}, {"MYT", 28800}, {"NDT", -9000}, {"NPT", 20700}, {"NST", -12600}, {"NZDT", 46800}, {"NZST", 43200}, {"OESZ", 10800}, {"OEZ", 7200}, {"PDT", -25200}, {"PKT", 18000}, {"PMDT", -7200}, {"PMST", -10800}, {"PST", -28800}, {"TRT", 10800}, {"UT", 0}, {"UTC", 0}, {"UYT", -10800}, {"VET", -14400}, {"WESZ", 3600}, {"WEST", 3600}, {"WET", 0}, {"WEZ", 0}, {"WIB", 25200}, {"WIT", 32400}, {"WITA", 28800}, {"Z", 0} }; static const int NZONE_ABBR = sizeof (ZONE_ABBR) / sizeof (ZONE_ABBR[0]); // The abbreviations with two meanings, read only under a locale that settles // them, at the offset MATLAB reads there, and refused under every other. struct ZoneAbbrLocale { const char *name; const char *locale; int off; }; static const ZoneAbbrLocale ZONE_ABBR_LOCALE[] = { {"ACST", "en_au", 34200}, {"ACST", "pt_br", -14400}, {"AMT", "pt_br", -14400}, {"BST", "en_ca", 21600}, {"BST", "en_gb", 3600}, {"IST", "en_ca", 19800}, {"IST", "en_ie", 3600}, {"IST", "en_in", 19800} }; static const int NZONE_ABBR_LOCALE = sizeof (ZONE_ABBR_LOCALE) / sizeof (ZONE_ABBR_LOCALE[0]); static inline bool is_zone_sym (char c) { return (c == 'z' || c == 'Z' || c == 'X' || c == 'x'); } // Two digits at POS, moving POS past them. static bool read_2digits (const string& s, size_t& pos, int& val) { if (pos + 1 < s.size () && isdigit (static_cast (s[pos])) && isdigit (static_cast (s[pos+1]))) { val = (s[pos] - '0') * 10 + (s[pos+1] - '0'); pos += 2; return true; } return false; } // A signed offset at POS, as every offset field reads one: an hour of one or // two digits, then optionally minutes and then seconds of two digits each, // with or without a colon. The hour runs to 23. POS moves past what was read // only on success. static bool read_signed_offset (const string& s, size_t& pos, double& off) { const size_t L = s.size (); size_t j = pos; if (j >= L || (s[j] != '+' && s[j] != '-')) { return false; } const double sgn = (s[j] == '-' ? -1 : 1); j++; if (j >= L || ! isdigit (static_cast (s[j]))) { return false; } int hh = s[j++] - '0'; if (j < L && isdigit (static_cast (s[j]))) { hh = hh * 10 + (s[j++] - '0'); } int mm = 0, ss = 0; size_t k = (j < L && s[j] == ':') ? j + 1 : j; if (read_2digits (s, k, mm)) { j = k; k = (j < L && s[j] == ':') ? j + 1 : j; if (read_2digits (s, k, ss)) { j = k; } } if (hh > 23 || mm > 59 || ss > 59) { return false; } off = sgn * (hh * 3600.0 + mm * 60.0 + ss); pos = j; return true; } // The field of an offset symbol at POS. Every width of 'Z', 'X' and 'x' reads // the same forms, as MATLAB does: 'Z' for zero in either letter case, a signed // offset, and a signed offset after 'GMT' or 'UTC' in any letter case, which // alone is zero too. 'z' reads the same and the abbreviations in ZONE_ABBR, // and those in ZONE_ABBR_LOCALE under their own LOCALE, whose separator may be // '_' or '-'. static bool read_zone_field (const string& s, size_t& pos, char sym, const string& locale, double& off) { const size_t L = s.size (); size_t j = pos; string word; while (j < L && isalpha (static_cast (s[j]))) { word += static_cast (toupper (static_cast (s[j]))); j++; } if (word.empty ()) { return read_signed_offset (s, pos, off); } if (word == "GMT" || word == "UTC") { if (! read_signed_offset (s, j, off)) { off = 0; } pos = j; return true; } if (sym != 'z') { if (word != "Z") { return false; } off = 0; pos = j; return true; } string loc; for (size_t i = 0; i < locale.size (); i++) { char c = static_cast (tolower (static_cast (locale[i]))); loc += (c == '-' ? '_' : c); } for (int k = 0; k < NZONE_ABBR_LOCALE; k++) { if (word == ZONE_ABBR_LOCALE[k].name && loc == ZONE_ABBR_LOCALE[k].locale) { off = ZONE_ABBR_LOCALE[k].off; pos = j; return true; } } for (int k = 0; k < NZONE_ABBR; k++) { if (word == ZONE_ABBR[k].name) { off = ZONE_ABBR[k].off; pos = j; return true; } } return false; } // Parse a cell array of date/time strings under an LDML 'InputFormat', // returning an N-by-6 date-vector matrix, and in OFF the UTC offset in seconds // that each string carries in an offset field, NaN where it carries none or // cannot be read. This is a direct port of dtParseInput in datetime.m and must // stay behaviourally identical to it. LEAPOK admits a 60th second, which only // exists in the 'UTCLeapSeconds' zone; whether the second named was actually // inserted is settled in m-code, which owns the leap-second table. LOCALE is // the full tag, which the 'z' field needs where the language alone is not // enough. static Matrix ldml_parse (const Cell& strs, const string& fmt, double pivot, int lidx, bool leapok, bool lone, const string& locale, ColumnVector& OFF) { vector toks; tokenize (fmt, toks); const int nt = static_cast (toks.size ()); const FoldedLocale& F = folded_locale (lidx); // Whether the format names any date field. MATLAB defaults a wholly absent // date to today, but when some date field is present the missing parts fall // back to month 1 / day 1 (and the current year). bool hasDate = false; for (int t = 0; t < nt; t++) { char c = toks[t].sym; if (c && (c == 'y' || c == 'u' || c == 'M' || c == 'L' || c == 'd' || c == 'D')) { hasDate = true; break; } } // Today's local date, for the fields the format leaves out (the m-code // reads these from clock, which is local time as well). // Plain localtime, not localtime_r: the latter is POSIX and the MinGW // headers Octave builds with on Windows do not declare it, exposing only // the Microsoft localtime_s, whose argument order differs between the // Microsoft and C11 Annex K spellings. localtime is C89 and available on // every toolchain; its static buffer is copied out at once and no other // localtime-family call can intervene. time_t tt = time (nullptr); const struct tm *lp = localtime (&tt); if (! lp) { error ("datetime: cannot read the system clock."); } const struct tm lt = *lp; const double nowY = lt.tm_year + 1900; const double nowM = lt.tm_mon + 1; const double nowD = lt.tm_mday; // The two-digit year window, as in the m-code: base is the century of the // pivot, and a year below the pivot belongs to the next century. double pmod = fmod (pivot, 100.0); if (pmod < 0) { pmod += 100.0; } const double base = pivot - pmod; const octave_idx_type n = strs.numel (); Matrix DV (n, 6); OFF = ColumnVector (n, numeric_limits::quiet_NaN ()); for (octave_idx_type r = 0; r < n; r++) { const string s = strs(r).string_value (); const size_t L = s.size (); size_t pos = 0; double Yv = nowY; double Mv = hasDate ? 1 : nowM; double Dv = hasDate ? 1 : nowD; double Hv = 0, MIv = 0, Sv = 0; int ampm = 0; double offv = numeric_limits::quiet_NaN (); bool ok = true; for (int t = 0; t < nt && ok; t++) { const Token& tk = toks[t]; if (tk.sym == '\0') { // Literal text must appear verbatim. size_t m = tk.lit.size (); if (pos + m <= L && s.compare (pos, m, tk.lit) == 0) { pos += m; } else { ok = false; } } else if (is_zone_sym (tk.sym)) { if (! read_zone_field (s, pos, tk.sym, locale, offv)) { ok = false; } } else if ((tk.sym == 'e' && tk.n >= 3) || tk.sym == 'a') { // A weekday name carries no value (the year/month/day already fix the // date) but MATLAB rejects a token that is not a real weekday name, so // validate it. A day period sets the AM/PM flag. Both grab up to the // next literal so that compound names such as the Portuguese // "segunda-feira" and the Spanish "a. m." are captured whole. string word; if (t < nt - 1 && toks[t+1].sym == '\0') { size_t p = s.find (toks[t+1].lit, pos); if (p == string::npos) { ok = false; break; } word = s.substr (pos, p - pos); pos = p; } else if (t == nt - 1) { word = s.substr (pos); // last token: the name runs to the end pos = L; } else { size_t j = pos; while (j < L && is_name_char (s[j])) { j++; } word = s.substr (pos, j - pos); pos = j; } if (tk.sym == 'a') { // A day period marker keeps any trailing period: the Spanish // markers are "a. m." and "p. m." and the Greek ones are // "π.μ."/"μ.μ.", so the period is part of the name. int idxA = fold_find (word, F.dpMark, 2); if (idxA == 1) { ampm = 1; } else if (idxA == 2) { ampm = 2; } else { ok = false; } } else { // A weekday name drops an abbreviation period before matching. if (! word.empty () && word[word.size()-1] == '.') { word.erase (word.size () - 1); } if (word.empty () || (fold_find (word, F.wFull, 7) == 0 && fold_find (word, F.wAbbr, 7) == 0)) { ok = false; } } } else if (tk.sym == 'M' && tk.n >= 3) { // Month name: grab a run of name characters. size_t j = pos; while (j < L && is_name_char (s[j])) { j++; } string word = s.substr (pos, j - pos); pos = j; if (word.empty ()) { ok = false; break; } int idx = fold_find (word, F.mFull, 12); if (idx == 0) { idx = fold_find (word, F.mAbbr, 12); if (idx == 0) { idx = regional_month (word, locale); } if (idx == 0) { ok = false; break; } // Consume the trailing period of an abbreviation (fr/de/pt). if (pos < L && s[pos] == '.') { pos++; } } Mv = idx; } else { // Numeric field. Butted against another numeric field, take exactly // the run length; otherwise take up to the field's natural width. A // year standing free is the exception: it takes every digit the text // offers, and what bounds it is its VALUE, checked below. A width // would be the wrong instrument, since leading zeros carry no value and // the text may hold any number of them -- '00002024' is the year 2024, // and is what this renderer emits for it under 'uuuuuuuu'. bool nextNum = (t < nt - 1) && toks[t+1].sym != '\0' && ! (((toks[t+1].sym == 'M' || toks[t+1].sym == 'e') && toks[t+1].n >= 3) || toks[t+1].sym == 'a' || is_zone_sym (toks[t+1].sym)); bool isYear = (tk.sym == 'y' || tk.sym == 'u'); int w; if (nextNum) { w = tk.n; } else { switch (tk.sym) { case 'y': case 'u': w = (tk.n == 2 ? 2 : -1); break; case 'D': w = 3; break; case 'S': w = 9; break; default: w = 2; break; } } size_t j = pos; bool neg = false; // 'u' is the proleptic year and carries a sign; 'y' is the year within // an era and does not. MATLAB draws the line in the same place, so // 'uuuu-MM-dd' reads '-1000-03-15' and 'yyyy-MM-dd' refuses it. if (tk.sym == 'u' && tk.n != 2 && j < L && s[j] == '-') { neg = true; j++; } size_t start = j; double val = 0; int ndig = 0; while (j < L && (w < 0 || (j - start) < static_cast (w)) && s[j] >= '0' && s[j] <= '9') { val = val * 10 + (s[j] - '0'); j++; ndig++; } pos = j; if (ndig == 0) { ok = false; break; } if (isYear) { // Bounded only by what a year can be held in exactly. MATLAB stops // at six significant digits here, which we followed until the // calendar was widened past it -- at which point the cap made our own // files unreadable to us, 'writetable' emitting a year that // 'readtable' then refused. Reading wider text than MATLAB is an // additive divergence: every string it accepts is read the same way. if (val > 9007199254740992.0) { ok = false; break; } if (neg) { val = -val; } } switch (tk.sym) { case 'y': case 'u': if (tk.n == 2) { Yv = base + val; if (Yv < pivot) { Yv += 100; } } else { Yv = val; } break; case 'M': Mv = val; break; case 'd': Dv = val; break; case 'D': { // Day of year, resolved through the civil calendar. Only the // month and day are taken, exactly as the m-code does, so a value // beyond the end of the year keeps the parsed year. int64_t yy; unsigned mm, dd; civil_from_days (days_from_civil (static_cast (Yv), 1, 1) + static_cast (val) - 1, yy, mm, dd); Mv = mm; Dv = dd; break; } case 'H': case 'h': case 'k': case 'K': Hv = val; break; case 'm': MIv = val; break; case 's': Sv = Sv + val; break; case 'S': Sv = Sv + val / pow (10.0, ndig); break; default: // 'Q'/'G'/'W'/'e' numeric forms carry no component here. break; } } } if (ok && pos < L) { ok = false; // trailing text not covered by the format } if (ok) { if (ampm == 2 && Hv < 12) { Hv += 12; } else if (ampm == 1 && Hv == 12) { Hv = 0; } // The components must name a date that exists. MATLAB rejects // 2024-04-31 and 2023-02-29 just as it rejects unparseable text, and // likewise an hour past 23, a minute past 59, or a second reaching 60. ok = (Mv >= 1 && Mv <= 12 && Dv >= 1 && Dv <= days_in_month (Yv, Mv) && Hv >= 0 && Hv <= 23 && MIv >= 0 && MIv <= 59 && Sv >= 0 && Sv < (leapok ? 61 : 60)); } if (! ok) { // A lone string that cannot be converted is an error, but within an // array only the offending element is lost, becoming NaT, as MATLAB // does -- one bad row must not cost the whole column. LONE comes from // the caller, which has already removed blanks and so knows whether the // input was a single string where N does not. if (lone) { error ("datetime: could not parse the date/time string '%s' with " "'InputFormat' '%s'.", s.c_str (), fmt.c_str ()); } for (int c = 0; c < 6; c++) { DV(r,c) = numeric_limits::quiet_NaN (); } continue; } OFF(r) = offv; DV(r,0) = Yv; DV(r,1) = Mv; DV(r,2) = Dv; DV(r,3) = Hv; DV(r,4) = MIv; DV(r,5) = Sv; } return DV; } // Display name tables for the renderer. These are deliberately English-only // and capitalized: MATLAB's 'Format' rendering is not locale aware, only // 'InputFormat' parsing is. static const char *R_MABBR[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; static const char *R_MFULL[12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; static const char *R_WABBR[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; static const char *R_WFULL[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; static const char *R_QORD[4] = {"1st quarter", "2nd quarter", "3rd quarter", "4th quarter"}; // sprintf ('%0*d', width, value). static string zeropad (int width, long long value) { char buf[64]; if (value < 0) { // The sign sits OUTSIDE the padded field, so 'uuuuuuuu' renders year // -32767 as '-00032767' and not '-0032767'. Counting the sign against the // width would also cost the field a digit, which is what stopped the // rendered text from being readable back. snprintf (buf, sizeof (buf), "-%0*lld", width, -value); } else { snprintf (buf, sizeof (buf), "%0*lld", width, value); } return string (buf); } // Numeric short UTC-offset form ('UTC', 'UTC+3', 'UTC+5:30') used by the // MATLAB-compatible 'z' style for zones without a named abbreviation. static string zone_utc (double offSec) { long a = static_cast (round (fabs (offSec))); if (a == 0) { return "UTC"; } long hh = a / 3600; long mm = (a % 3600) / 60; long ss = a % 60; char sgn = (offSec < 0 ? '-' : '+'); char buf[64]; if (ss != 0) { snprintf (buf, sizeof (buf), "UTC%c%ld:%02ld:%02ld", sgn, hh, mm, ss); } else if (mm == 0) { snprintf (buf, sizeof (buf), "UTC%c%ld", sgn, hh); } else { snprintf (buf, sizeof (buf), "UTC%c%ld:%02ld", sgn, hh, mm); } return string (buf); } // MATLAB-compatible 'z' rendering: the IANA letter abbreviation only for the // North American zones (plus GMT/UTC) that MATLAB names, keyed on the // (abbreviation, offset) pair so collisions resolve correctly. static string zone_matlab (const string& ab, double offSec) { static const char *names[] = {"EST", "EDT", "CST", "CDT", "MST", "MDT", "PST", "PDT", "AKST", "AKDT", "HST", "HAST", "HADT", "AST", "ADT", "GMT", "UTC"}; static const int hours[] = {-5, -4, -6, -5, -7, -6, -8, -7, -9, -8, -10, -10, -9, -4, -3, 0, 0}; static const int nz = sizeof (hours) / sizeof (hours[0]); for (int k = 0; k < nz; k++) { if (ab == names[k] && fabs (offSec - hours[k] * 3600.0) < 1) { return ab; } } return zone_utc (offSec); } // Render one numeric time-zone offset field, the ISO-8601 families Z/X/x. static string zone_field (char c, int nn, double offSec) { long a = static_cast (round (fabs (offSec))); long hh = a / 3600; long mm = (a % 3600) / 60; long ss = a % 60; char sgn = (offSec < 0 ? '-' : '+'); char buf[64]; // The extended forms write the seconds of an offset that has them, as // MATLAB does for a zone such as '+03:00:30': 'ZZZZ' and width 5 of each // family. The shorter forms have no place for them. bool withSec = (ss != 0 && (nn >= 5 || (c == 'Z' && nn == 4))); if (c == 'Z') { if (nn >= 1 && nn <= 3) { snprintf (buf, sizeof (buf), "%c%02ld%02ld", sgn, hh, mm); } else if (nn == 4) { // Localized GMT format; a zero offset is just 'UTC'. if (offSec == 0) { return "UTC"; } snprintf (buf, sizeof (buf), "UTC%c%02ld:%02ld", sgn, hh, mm); } else { // ISO-8601 extended (ZZZZZ); a zero offset renders as 'Z'. if (offSec == 0) { return "Z"; } snprintf (buf, sizeof (buf), "%c%02ld:%02ld", sgn, hh, mm); } } else if (c == 'X') { if (offSec == 0) { return "Z"; } if (nn == 1) { if (mm == 0) { snprintf (buf, sizeof (buf), "%c%02ld", sgn, hh); } else { snprintf (buf, sizeof (buf), "%c%02ld%02ld", sgn, hh, mm); } } else if (nn == 2 || nn == 4) { snprintf (buf, sizeof (buf), "%c%02ld%02ld", sgn, hh, mm); } else { snprintf (buf, sizeof (buf), "%c%02ld:%02ld", sgn, hh, mm); } } else { if (nn == 1) { if (mm == 0) { snprintf (buf, sizeof (buf), "%c%02ld", sgn, hh); } else { snprintf (buf, sizeof (buf), "%c%02ld%02ld", sgn, hh, mm); } } else if (nn == 2 || nn == 4) { snprintf (buf, sizeof (buf), "%c%02ld%02ld", sgn, hh, mm); } else { snprintf (buf, sizeof (buf), "%c%02ld:%02ld", sgn, hh, mm); } } string out (buf); if (withSec) { snprintf (buf, sizeof (buf), ":%02ld", ss); out += buf; } return out; } // Octave's mod for a positive divisor: the result carries the divisor's sign. static double omod (double x, double y) { double r = fmod (x, y); if (r != 0 && ((r < 0) != (y < 0))) { r += y; } return r; } // Render each element of a datetime array under a concrete LDML pattern, a // port of dtFormatStrings in datetime.m. The zone-dependent quantities OFF // (offset in seconds) and ABBR (zone abbreviation) are supplied by the caller, // which reads them from the compiled tz database; everything else is derived // here. NaT elements render as 'NaT' and infinite years as '-Inf'/'Inf'. static Cell ldml_format (const NDArray& Y, const NDArray& M, const NDArray& D, const NDArray& H, const NDArray& Mi, const NDArray& S, const string& fmt, const string& zoneStyle, bool hasTZ, const NDArray& off, const Cell& abbr) { vector toks; tokenize (fmt, toks); const int nt = static_cast (toks.size ()); const octave_idx_type n = Y.numel (); Cell cstr (Y.dims ()); for (octave_idx_type k = 0; k < n; k++) { if (octave::math::isnan (Y(k))) { cstr(k) = "NaT"; continue; } else if (octave::math::isinf (Y(k))) { cstr(k) = (Y(k) > 0 ? "Inf" : "-Inf"); continue; } const int64_t yy = static_cast (Y(k)); const int mo = static_cast (M(k)); const int dd = static_cast (D(k)); const int hh = static_cast (H(k)); const int mi = static_cast (Mi(k)); string str; for (int t = 0; t < nt; t++) { const Token& tk = toks[t]; if (tk.sym == '\0') { str += tk.lit; continue; } const char c = tk.sym; const int nn = tk.n; string piece; char buf[64]; switch (c) { case 'y': case 'u': if (nn == 2) { piece = zeropad (2, static_cast (omod (Y(k), 100.0))); } else { piece = zeropad (nn, yy); } break; case 'M': if (mo < 1 || mo > 12) { error ("datetime: month out of range while rendering."); } switch (nn) { case 1: snprintf (buf, sizeof (buf), "%d", mo); piece = buf; break; case 2: piece = zeropad (2, mo); break; case 3: piece = R_MABBR[mo-1]; break; case 4: piece = R_MFULL[mo-1]; break; default: piece = string (1, R_MFULL[mo-1][0]); break; } break; case 'd': if (nn == 1) { snprintf (buf, sizeof (buf), "%d", dd); piece = buf; } else { piece = zeropad (nn, dd); } break; case 'D': { int64_t doy = days_from_civil (yy, mo, dd) - days_from_civil (yy, 1, 1) + 1; piece = zeropad (nn, doy); break; } case 'e': { // weekday, Sunday = 1 (1970-01-01 was a Thursday). int64_t z = days_from_civil (yy, mo, dd); int wd = static_cast (omod (z + 4, 7.0)) + 1; switch (nn) { case 1: snprintf (buf, sizeof (buf), "%d", wd); piece = buf; break; case 2: piece = zeropad (2, wd); break; case 3: piece = R_WABBR[wd-1]; break; case 4: piece = R_WFULL[wd-1]; break; default: piece = string (1, R_WFULL[wd-1][0]); break; } break; } case 'H': if (nn == 1) { snprintf (buf, sizeof (buf), "%d", hh); piece = buf; } else { piece = zeropad (2, hh); } break; case 'h': { int h12 = static_cast (omod (hh - 1, 12.0)) + 1; if (nn == 1) { snprintf (buf, sizeof (buf), "%d", h12); piece = buf; } else { piece = zeropad (2, h12); } break; } case 'm': if (nn == 1) { snprintf (buf, sizeof (buf), "%d", mi); piece = buf; } else { piece = zeropad (2, mi); } break; case 's': { long sec = static_cast (floor (S(k))); if (nn == 1) { snprintf (buf, sizeof (buf), "%ld", sec); piece = buf; } else { piece = zeropad (2, sec); } break; } case 'S': { // Fractional seconds, truncated to nn digits. Round to whole // microseconds first (the stored precision) so an exact value such // as .678 -- held as 0.67799999... -- yields the intended digits. long micros = static_cast (floor ((S(k) - floor (S(k))) * 1e6 + 0.5)); if (nn <= 6) { long p = 1; for (int q = 0; q < 6 - nn; q++) { p *= 10; } piece = zeropad (nn, micros / p); } else { long p = 1; for (int q = 0; q < nn - 6; q++) { p *= 10; } piece = zeropad (nn, micros * p); } break; } case 'a': piece = (hh >= 12 ? "PM" : "AM"); break; case 'Q': { int q = (mo - 1) / 3 + 1; switch (nn) { case 1: snprintf (buf, sizeof (buf), "%d", q); piece = buf; break; case 2: piece = zeropad (2, q); break; case 3: snprintf (buf, sizeof (buf), "Q%d", q); piece = buf; break; case 4: piece = R_QORD[q-1]; break; default: snprintf (buf, sizeof (buf), "%d", q); piece = buf; break; } break; } case 'G': piece = "CE"; break; case 'W': { // Week of the month, Sunday based, first week = week 1. int64_t z1 = days_from_civil (yy, mo, 1); int firstDow = static_cast (omod (z1 + 4, 7.0)) + 1; int w = (dd - 1 + (firstDow - 1)) / 7 + 1; snprintf (buf, sizeof (buf), "%d", w); piece = buf; break; } case 'z': case 'Z': case 'X': case 'x': if (! hasTZ) { piece = string (nn, '*'); } else if (c == 'z') { // 'z'/'zz'/'zzz' follow the session style; 'zzzz' and 'zzzzz' are // Octave-specific per-format overrides. string zst; if (nn == 4) { zst = "iana"; } else if (nn >= 5) { zst = "matlab"; } else { zst = zoneStyle; } string ab = abbr(k).string_value (); if (zst == "iana") { piece = ab; } else { piece = zone_matlab (ab, off(k)); } } else { piece = zone_field (c, nn, off(k)); } break; default: error ("datetime: unsupported format symbol '%c'.", c); } str += piece; } cstr(k) = str; } return cstr; } DEFUN_DLD (__ldml__, args, , "-*- texinfo -*-\n\ @deftypefn {} {[@var{DV}, @var{OFF}] =} __ldml__ (\"parse\", @var{strs}, @var{fmt}, \ @var{pivot}, @var{locale})\n\ \n\ \n\ Parse date/time strings under an LDML @qcode{'InputFormat'} pattern. \n\ \n\ @var{strs} is a cell array of character vectors, @var{fmt} an LDML pattern, \ @var{pivot} the two-digit year pivot, and @var{locale} a language tag such as \ @qcode{'fr_FR'} (empty or @qcode{'system'} for English). The return value is \ an N-by-6 date-vector matrix, and the second output @var{OFF} the UTC offset \ in seconds each string carries in an offset field, NaN where it carries \ none. \n\ \n\ @code{@var{syms} = __ldml__ (\"symbols\", @var{fmt})} returns the field \ symbols of @var{fmt}, in order and one character per field run, with literal \ text omitted. \n\ \n\ This is a helper function for the @qcode{datetime} class of the `datatypes` \ package. Do NOT use this function directly. \n\ \n\ @end deftypefn") { if (args.length () < 1 || ! args(0).is_string ()) { error ("__ldml__: first argument must be an action string."); } string action = args(0).string_value (); if (action == "parse") { if (args.length () < 5 || args.length () > 7) { error ("__ldml__: 'parse' takes STRS, FMT, PIVOT, LOCALE, and optional " "LEAPOK and LONE flags."); } if (! args(1).iscellstr ()) { error ("__ldml__: STRS must be a cell array of character vectors."); } if (! args(2).is_string ()) { error ("__ldml__: FMT must be a character vector."); } Cell strs = args(1).cell_value (); string fmt = args(2).string_value (); double pivot = args(3).double_value (); string locale = (args(4).is_string () ? args(4).string_value () : string ()); int lidx = locale_index (locale); if (lidx < 0) { error ("__ldml__: unsupported locale '%s'.", locale.c_str ()); } bool leapok = (args.length () >= 6 && args(5).bool_value ()); bool lone = (args.length () == 7 ? args(6).bool_value () : strs.numel () == 1); ColumnVector OFF; Matrix DV = ldml_parse (strs, fmt, pivot, lidx, leapok, lone, locale, OFF); return ovl (DV, OFF); } if (action == "symbols") { if (args.length () != 2) { error ("__ldml__: 'symbols' takes FMT."); } if (! args(1).is_string ()) { error ("__ldml__: FMT must be a character vector."); } vector toks; tokenize (args(1).string_value (), toks); string syms; for (size_t k = 0; k < toks.size (); k++) { if (toks[k].sym != '\0') { syms += toks[k].sym; } } return ovl (syms); } if (action == "format") { if (args.length () != 12) { error ("__ldml__: 'format' takes Y, M, D, H, MI, S, FMT, ZONESTYLE, " "HASTZ, OFF, and ABBR."); } NDArray Y = args(1).array_value (); NDArray M = args(2).array_value (); NDArray D = args(3).array_value (); NDArray H = args(4).array_value (); NDArray Mi = args(5).array_value (); NDArray S = args(6).array_value (); string fmt = args(7).string_value (); string zoneStyle = args(8).string_value (); bool hasTZ = args(9).bool_value (); NDArray off = args(10).array_value (); Cell abbr = (args(11).iscell () ? args(11).cell_value () : Cell ()); return ovl (ldml_format (Y, M, D, H, Mi, S, fmt, zoneStyle, hasTZ, off, abbr)); } error ("__ldml__: unknown action '%s'.", action.c_str ()); } pr0m1th3as-datatypes-9450b29/src/__nkeyHash__.cc000066400000000000000000000212011527370255000213360ustar00rootroot00000000000000/* Copyright (C) 2025-2026 Andreas Bertsatos This file is part of the datatypes package for GNU Octave. 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 . */ #define FNV1A64_PRIME 0x00000100000001b3 #include #include #include using namespace std; inline bool isLittleEndian() { unsigned int value = 0x01020304; unsigned char bytes[4] = {}; std::memcpy (bytes, &value, sizeof (value)); return bytes[0] == 0x04; } // The canonical quiet NaN bit pattern. Floating point has // two families of values whose in-memory representation is not unique but // which 'isequaln' -- and hence 'keyMatch' -- calls equal: the two signed // zeros, and every NaN. Hashing raw bytes therefore gave two equal keys // different hash codes, which breaks the keyHash/keyMatch contract: a // hash-bucketed container never reaches a candidate whose hash differs. The // sign bit is not academic here. On x86-64 the literal NaN is positive but // '0/0' and 'Inf - Inf' set it, so 'seconds (NaN)' and 'seconds (0/0)' were // different duration keys. // // These are written as fixed bytes rather than taken from std::numeric_limits // so that a NaN hashes to the same code on every platform, which is the // cross-session, cross-worker determinism keyHash documents. They are also // the patterns the literal NaN already had, so canonicalising moves the stray // representations onto the existing hash codes rather than changing them. #define CANON_NAN64 0x7ff8000000000000ULL #define CANON_NAN32 0x7fc00000U // Hash a bit pattern, least significant byte first, which is the order both // integer paths below produce. Emitting by shifting rather than by walking // the value's bytes in memory makes the result depend on the value alone, so // a big-endian machine takes this same path and there is no second, untested // code path to get wrong -- which is exactly where GitHub issues 38, 40 and // 43 came from. The static_asserts below check it against the byte walker. template static inline constexpr uint64_t fnv1a64_bits (U bits, uint64_t out) { for (size_t k = 0; k < sizeof (U); k++) { // signedness as in the byte paths below, for backwards compatibility const signed char sb = static_cast ((bits >> (8 * k)) & 0xff); out = (out ^ sb) * FNV1A64_PRIME; } return out; } // Hash a floating point buffer, canonicalising -0.0 to +0.0 and every NaN to // CANON_NAN*. A value that is neither a signed zero nor a NaN hashes exactly // as it did before this canonicalisation existed. template static inline uint64_t fnv1a64_float (const char *buf, size_t nelem, U canon_nan, uint64_t out) { for (size_t i = 0; i < nelem; i++) { T v; std::memcpy (&v, buf + i * sizeof (T), sizeof (T)); U bits; if (v != v) { // NaN, whatever its sign and payload bits = canon_nan; } else { // true for both +0.0 and -0.0, and assigning collapses them if (v == T (0)) { v = T (0); } // Copying a float into an unsigned integer of the same width gives the // IEEE bit pattern on either byte order, both types laying their bytes // out the same way on any one machine. std::memcpy (&bits, &v, sizeof (T)); } out = fnv1a64_bits (bits, out); } return out; } static inline constexpr uint64_t fnv1a64 (const char* buf, size_t len, uint64_t out) { for (size_t i = 0; i < len; i++) { // default char signedness depends on architecture, for signed for // backwards compatibility const signed char b = buf[i]; out = (out ^ b) * FNV1A64_PRIME; } return out; } static inline constexpr uint64_t fnv1a64 (const char* buf, size_t len, size_t nbytes, uint64_t out) { for (size_t i = 0; i < len; i++) { // reverse bytes for big endian systems size_t dv = i / nbytes; size_t md = i % nbytes; size_t ii = dv * nbytes + nbytes - md - 1; const signed char b = buf[ii]; out = (out ^ b) * FNV1A64_PRIME; } return out; } namespace /* tests */ { constexpr std::array testData{0, 1, char(0x80), 0}; static_assert(0 == fnv1a64(testData.data(), 0, 0)); static_assert(0x123 == fnv1a64(testData.data(), 0, 0x123)); static_assert(0 == fnv1a64(testData.data(), 1, 0)); static_assert(FNV1A64_PRIME == fnv1a64(testData.data(), 1, 1)); static_assert(FNV1A64_PRIME == fnv1a64(testData.data(), 2, 0)); // The shift emission must agree with walking the same pattern's bytes in // little-endian order, which is what the pre-canonicalisation code did on a // little-endian machine. Checked at compile time, so it is verified on every // build including the big-endian ones this file can no longer branch on. constexpr std::array nan64LE{0, 0, 0, 0, 0, 0, char(0xf8), 0x7f}; constexpr std::array nan32LE{0, 0, char(0xc0), 0x7f}; constexpr std::array one64LE{0, 0, 0, 0, 0, 0, char(0xf0), 0x3f}; static_assert(fnv1a64(nan64LE.data(), 8, 0xcbf29ce484222325) == fnv1a64_bits(CANON_NAN64, 0xcbf29ce484222325)); static_assert(fnv1a64(nan32LE.data(), 4, 0xcbf29ce484222325) == fnv1a64_bits(CANON_NAN32, 0xcbf29ce484222325)); // 1.0, whose high byte 0x3f is positive and whose 0xf0 is not, so both // signedness cases are covered static_assert(fnv1a64(one64LE.data(), 8, 0xcbf29ce484222325) == fnv1a64_bits(0x3ff0000000000000ULL, 0xcbf29ce484222325)); } DEFUN_DLD (__nkeyHash__, args, nargout, "-*- texinfo -*-\n\ @deftypefn {} {@var{uint64} =} __nkeyHash__ (@var{x})\n\ @deftypefnx {} {@var{uint64} =} __nkeyHash__ (@var{x}, @var{FNV1A64_BASE})\n\ \n\ \n\ Fowler–Noll–Vo hash key for a numeric vector. \n\ \n\ This is a helper function for @qcode{keyHash} methods of `datatypes`' classes. \ Do NOT use this function directly. \n\ \n\ @end deftypefn") { octave_uint64 base; // Validate input if (args.length () < 1) { error ("__nkeyHash__: too few input arguments."); } if (! (args(0).isnumeric () || args(0).islogical ())) { error ("__nkeyHash__: X must be either numeric or logical."); } if (args(0).is_range ()) { error ("__nkeyHash__: X cannot be a range."); } // Get or assign a base value if (args.length () > 1) { base = args(1).uint64_scalar_value (); } else { base = 0xcbf29ce484222325; // default FNV1A64_BASE } // Cast numeric input to const char. size_t, not uint32_t: byte_size () // exceeds 4 GiB for a large array and truncating it would silently hash a // prefix of the data. size_t len = args(0).byte_size (); const void *in = args(0).mex_get_data (); const char *buf = static_cast(in); // Generate the hash key octave_value_list retval (nargout); // Floating point first, so that -0.0 and NaN are canonicalised. Complex // arrays are stored as interleaved real/imaginary components of the same // type and so are covered by the same pass; integer and logical types have // neither a signed zero nor a NaN and are hashed untouched. if (args(0).is_double_type ()) { octave_uint64 out = fnv1a64_float (buf, len / sizeof (double), CANON_NAN64, base); retval(0) = out; } else if (args(0).is_single_type ()) { octave_uint64 out = fnv1a64_float (buf, len / sizeof (float), CANON_NAN32, base); retval(0) = out; } else if (isLittleEndian ()) { octave_uint64 out = fnv1a64 (buf, len, base); retval(0) = out; } else { // Integer and logical types only; the floating point branches above // already emitted little-endian bytes on either architecture. uint32_t nbytes = 1; if (args(0).is_int16_type () || args(0).is_uint16_type ()) { nbytes = 2; } else if (args(0).is_int32_type () || args(0).is_uint32_type ()) { nbytes = 4; } else if (args(0).is_int64_type () || args(0).is_uint64_type ()) { nbytes = 8; } octave_uint64 out = fnv1a64 (buf, len, nbytes, base); retval(0) = out; } return retval; } pr0m1th3as-datatypes-9450b29/src/__ods2table__.cc000066400000000000000000000370271527370255000214600ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the datatypes package for GNU Octave. 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 . */ #include #include #include #include #include #include #include #include #include #include #include "pugixml.hpp" #include "miniz.h" using namespace std; // A compressed '.ods' begins with the ZIP local-file-header magic "PK\x03\x04". static bool is_zip_file (const string &file) { ifstream f (file.c_str (), ios::binary); char m[4] = {0}; f.read (m, 4); return (m[0] == 'P' && m[1] == 'K' && m[2] == 3 && m[3] == 4); } // Parse a numeric string the same way __csv2table__ does: a plain integer of // 16 or more digits is stored losslessly as a 64-bit integer, everything else // becomes a double. static octave_value parse_float (const string &word) { const char *word_str = word.c_str (); char *err; double val = strtod (word_str, &err); size_t start = (! word.empty () && (word[0] == '+' || word[0] == '-')) ? 1 : 0; bool is_int = (word.length () > start); size_t ndig = 0; for (size_t i = start; i < word.length (); i++) { if (word[i] < '0' || word[i] > '9') { is_int = false; break; } ndig++; } if (is_int && ndig >= 16) { errno = 0; if (word[0] == '-') { long long llv = strtoll (word_str, &err, 10); return (errno == 0) ? octave_value (octave_int64 (llv)) : octave_value (val); } else { unsigned long long ullv = strtoull (word_str, &err, 10); return (errno == 0) ? octave_value (octave_uint64 (ullv)) : octave_value (val); } } return octave_value (val); } // Concatenated text of a cell's children. static string cell_text (const pugi::xml_node &cell) { string out; for (pugi::xml_node p = cell.child ("text:p"); p; p = p.next_sibling ("text:p")) { if (! out.empty ()) out += "\n"; out += p.text ().as_string (); } return out; } // Parse one into a value grid and a parallel per-cell value-type // grid. 'typed' selects data-sheet decoding (float/boolean/date/time/string, // with missing cells left empty); when false every cell is returned as text // (used for the metadata sheet). Row/column repeat counts are honoured so the // grid stays rectangular. static void read_sheet (const pugi::xml_node &table, Cell &data, Cell &vtype, bool typed, bool trim) { // First pass: collect rows as vectors of (value-type, raw-string) and find // the maximum column count. vector>> grid; size_t maxcols = 0; // When 'trim' is set (foreign spreadsheets with no metadata sheet), trailing // empty cells -- blank cells padded out with large 'number-columns-repeated' // counts -- and trailing empty rows are dropped so the grid spans only the // sheet's used bounding box; leading and interior empties are preserved as // blank cells, keeping absolute A1 coordinates intact for the caller's // 'Range' handling. When 'trim' is clear (our house files, whose metadata // sheet makes the grid authoritative), every cell is materialised verbatim -- // a trailing row of all-missing values is real data and must be kept. // empty rows not yet flushed (may be trailing) long pending_rows = 0; for (pugi::xml_node row = table.child ("table:table-row"); row; row = row.next_sibling ("table:table-row")) { long rrep = row.attribute ("table:number-rows-repeated").as_int (1); if (rrep < 1) rrep = 1; vector> cols; // empty cells not yet flushed (may be trailing) long pending_cols = 0; for (pugi::xml_node cell = row.child ("table:table-cell"); cell; cell = cell.next_sibling ("table:table-cell")) { long crep = cell.attribute ("table:number-columns-repeated").as_int (1); if (crep < 1) crep = 1; string vt = cell.attribute ("office:value-type").as_string (); string raw; if (vt == "float" || vt == "percentage" || vt == "currency") raw = cell.attribute ("office:value").as_string (); else if (vt == "boolean") raw = cell.attribute ("office:boolean-value").as_string (); else if (vt == "date") raw = cell.attribute ("office:date-value").as_string (); else if (vt == "time") raw = cell.attribute ("office:time-value").as_string (); else // string / empty raw = cell_text (cell); // An error cell, which Calc writes as a string marked // calcext:value-type="error", is a missing value, as MATLAB reads one if (string (cell.attribute ("calcext:value-type").as_string ()) == "error") { vt.clear (); raw.clear (); } // blank: defer (may be trailing) if (trim && vt.empty () && raw.empty ()) { pending_cols += crep; } // content cell: flush deferred blanks first else { for (long k = 0; k < pending_cols; k++) cols.push_back (make_pair (string (), string ())); pending_cols = 0; for (long k = 0; k < crep; k++) cols.push_back (make_pair (vt, raw)); } } // A row with no content cells is empty; defer it in case it is trailing. if (trim && cols.empty ()) { pending_rows += rrep; } else { for (long k = 0; k < pending_rows; k++) grid.push_back (vector> ()); pending_rows = 0; if (cols.size () > maxcols) maxcols = cols.size (); for (long k = 0; k < rrep; k++) grid.push_back (cols); } } size_t nrows = grid.size (); data = Cell (nrows, maxcols); vtype = Cell (nrows, maxcols); for (size_t r = 0; r < nrows; r++) { for (size_t c = 0; c < maxcols; c++) { string vt = (c < grid[r].size ()) ? grid[r][c].first : string (); string raw = (c < grid[r].size ()) ? grid[r][c].second : string (); vtype(r, c) = vt; if (! typed) // metadata sheet: text only { // A cell with no value type at all carries no text, which is not the // same as a cell holding an empty string: a metadata block written // for one nesting level says with it that another level has none. if (vt.empty ()) data(r, c) = Matrix (0, 0); else data(r, c) = raw; } else if (vt == "float" || vt == "percentage" || vt == "currency") { data(r, c) = parse_float (raw); } else if (vt == "boolean") { data(r, c) = octave_value (raw == "true"); } else if (vt.empty ()) // missing / blank cell { data(r, c) = Matrix (0, 0); } else // date / time / string -> raw text { data(r, c) = raw; } } } } // If the metadata grid is sectioned (a multi-sheet workbook written by // 'struct2ods' precedes each table's metadata block with a "## Sheet: " // marker row), return only the section whose marker matches 'sel'. An // unsectioned grid (single-sheet house file, no markers) describes the first // data sheet, 'first', alone: it is returned whole for that sheet and as an // empty grid for any other, which a later writer may have added without // metadata. A sectioned grid with no matching marker yields an empty grid. static Cell meta_section (const Cell &meta, const string &sel, const string &first) { octave_idx_type nr = meta.rows (); octave_idx_type nc = meta.columns (); const string pfx = "## Sheet: "; vector marks; vector mnames; for (octave_idx_type r = 0; r < nr; r++) { if (meta(r, 0).is_string ()) { string s = meta(r, 0).string_value (); if (s.size () >= pfx.size () && s.compare (0, pfx.size (), pfx) == 0) { marks.push_back (r); mnames.push_back (s.substr (pfx.size ())); } } } if (marks.empty ()) // unsectioned: single-sheet house file return (sel == first) ? meta : Cell (); for (size_t i = 0; i < marks.size (); i++) { if (mnames[i] == sel) { octave_idx_type r0 = marks[i] + 1; octave_idx_type r1 = (i + 1 < marks.size ()) ? marks[i + 1] : nr; // The shared metadata grid is padded to a common width; drop this // section's trailing all-empty columns so its width matches the sheet's // own variable count (otherwise the padding reads as extra variables). octave_idx_type used = 0; for (octave_idx_type c = 0; c < nc; c++) { bool any = false; for (octave_idx_type r = r0; r < r1 && ! any; r++) if (! meta(r, c).isempty ()) any = true; if (any) used = c + 1; } Cell out (r1 - r0, used); for (octave_idx_type r = r0; r < r1; r++) for (octave_idx_type c = 0; c < used; c++) out(r - r0, c) = meta(r, c); return out; } } // sectioned but no section for this sheet return Cell (); } // The rows of the metadata grid above the first "## Sheet: " marker. They // belong to no sheet and carry facts about the workbook as a whole, such as // which sheet holds the events of which other sheet. An unsectioned grid, or // one whose first row is already a marker, has no preamble. static Cell meta_preamble (const Cell &meta) { octave_idx_type nr = meta.rows (); octave_idx_type nc = meta.columns (); const string pfx = "## Sheet: "; octave_idx_type first = -1; for (octave_idx_type r = 0; r < nr && first < 0; r++) { if (meta(r, 0).is_string ()) { string s = meta(r, 0).string_value (); if (s.size () >= pfx.size () && s.compare (0, pfx.size (), pfx) == 0) first = r; } } if (first <= 0) return Cell (); Cell out (first, nc); for (octave_idx_type r = 0; r < first; r++) for (octave_idx_type c = 0; c < nc; c++) out(r, c) = meta(r, c); return out; } DEFUN_DLD (__ods2table__, args, , "-*- texinfo -*-\n \ @deftypefn {datatypes} {[@var{data}, @var{vtype}, @var{meta}] =} \ __ods2table__ (@var{file})\n\ @deftypefnx {datatypes} {[@var{data}, @var{vtype}, @var{meta}] =} \ __ods2table__ (@var{file}, @var{sheet})\n\ @deftypefnx {datatypes} {[@var{data}, @var{vtype}, @var{meta}, @var{names}, \ @var{preamble}] =} __ods2table__ (@dots{})\n\ \n\ \n\ Barebone function for reading a flat ODS (@qcode{.fods}) file.\n\ \n\ This is a helper IO function for the @qcode{ods2table} function. Do NOT call \ it directly. \n\ \n\ @end deftypefn") { octave_value_list retval (5); // Keep the value-type, metadata, sheet-name, and preamble outputs defined // even on the error paths, so a caller's multi-output call never sees an // undefined return element. retval(1) = Cell (); retval(2) = Cell (); retval(3) = Cell (); retval(4) = Cell (); if (args.length () < 1 || args.length () > 2) error ("__ods2table__: one or two input arguments are required."); string file = args(0).string_value (); // Optional sheet selector: a name (character vector) or a 1-based index over // the data sheets (the hidden '__datatypes_meta__' sheet is never counted). string want_name; long want_index = 0; bool by_name = false, by_index = false; if (args.length () == 2 && ! args(1).isempty ()) { if (args(1).is_string ()) { want_name = args(1).string_value (); by_name = true; } else { want_index = static_cast (args(1).scalar_value ()); by_index = true; } } // A compressed '.ods' is a ZIP whose content.xml holds the spreadsheet; a // flat '.fods' is the XML document itself. pugi::xml_document doc; pugi::xml_parse_result res; if (is_zip_file (file)) { mz_zip_archive zip; memset (&zip, 0, sizeof (zip)); if (! mz_zip_reader_init_file (&zip, file.c_str (), 0)) { retval(0) = string ("cannot read '") + file + "' as a ZIP archive."; return retval; } size_t sz = 0; void *p = mz_zip_reader_extract_file_to_heap (&zip, "content.xml", &sz, 0); if (! p) { mz_zip_reader_end (&zip); retval(0) = string ("'") + file + "' has no 'content.xml' entry."; return retval; } res = doc.load_buffer (p, sz); mz_free (p); mz_zip_reader_end (&zip); } else { res = doc.load_file (file.c_str ()); } if (! res) { retval(0) = string ("cannot read '") + file + "': " + res.description () + "."; return retval; } // The spreadsheet lives under (flat) or // (packaged). pugi::xml_node root = doc.child ("office:document"); if (! root) root = doc.child ("office:document-content"); pugi::xml_node spreadsheet = root.child ("office:body").child ("office:spreadsheet"); if (! spreadsheet) { retval(0) = string ("'") + file + "' is not an OpenDocument spreadsheet."; return retval; } pugi::xml_node data_tbl, meta_tbl; long data_seen = 0; vector sheet_names; // all data sheet names, document order for (pugi::xml_node t = spreadsheet.child ("table:table"); t; t = t.next_sibling ("table:table")) { string name = t.attribute ("table:name").as_string (); if (name == "__datatypes_meta__") { meta_tbl = t; continue; } sheet_names.push_back (name); data_seen++; if (by_name) { if (name == want_name && ! data_tbl) data_tbl = t; } else if (by_index) { if (data_seen == want_index && ! data_tbl) data_tbl = t; } else if (! data_tbl) data_tbl = t; } // A requested sheet that does not exist is an error the caller reports. if ((by_name || by_index) && ! data_tbl) { if (by_name) retval(0) = string ("sheet '") + want_name + "' not found in '" + file + "'."; else retval(0) = string ("sheet index ") + std::to_string (want_index) + " out of range in '" + file + "'."; return retval; } Cell data, vtype, meta, meta_vt; // Trim the data grid to the used block only for foreign spreadsheets (no // metadata sheet); our own house files are authoritative and kept verbatim. if (data_tbl) read_sheet (data_tbl, data, vtype, true, ! meta_tbl); if (meta_tbl) read_sheet (meta_tbl, meta, meta_vt, false, false); // The preamble is read off the whole grid, before the section belonging to // one sheet replaces it. Cell preamble = meta_preamble (meta); // In a multi-sheet house workbook the metadata sheet is sectioned; keep only // the section belonging to the selected data sheet. if (meta.numel () > 0 && data_tbl) { string sel_name = data_tbl.attribute ("table:name").as_string (); meta = meta_section (meta, sel_name, sheet_names.front ()); } Cell names_out (1, sheet_names.size ()); for (size_t i = 0; i < sheet_names.size (); i++) names_out(i) = sheet_names[i]; retval(0) = data; retval(1) = vtype; retval(2) = meta; retval(3) = names_out; retval(4) = preamble; return retval; } pr0m1th3as-datatypes-9450b29/src/__table2csv__.cc000066400000000000000000000141021527370255000214530ustar00rootroot00000000000000/* Copyright (C) 2025-2026 Andreas Bertsatos This file is part of the datatypes package for GNU Octave. 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 . */ #include #include #include #include #include #include #include #include using namespace std; // Write the shortest decimal string that reads back as exactly the same // double. 15 significant digits suffice for most values and 17 always do, so // a value is written with the fewest digits that survive the read. static int shortest_double (char *buf, size_t n, double v) { int cx = snprintf (buf, n, "%.15g", v); if (strtod (buf, nullptr) != v) { cx = snprintf (buf, n, "%.16g", v); if (strtod (buf, nullptr) != v) cx = snprintf (buf, n, "%.17g", v); } return cx; } DEFUN_DLD (__table2csv__, args, nargout, "-*- texinfo -*-\n \ @deftypefn {datatypes} {} __table2csv__ (@var{file}, @var{C})\n\ \n\ \n\ Barebone function for saving a cell array to a CSV file.\n\ \n\ This is a helper IO function for the @qcode{table2csv} method of the \ @qcode{table} class. Do NOT call it directly. \n\ \n\ @end deftypefn") { octave_value_list retval(nargout); // Check input arguments if (args.length() < 2 || args.length() > 5) { error ("__table2csv__: two to five input arguments are required."); } // Get input arguments string file = args(0).string_value (); Cell C = args(1).cell_value (); // Optional field delimiter (a single character); defaults to a comma. string sep = ","; if (args.length() >= 3) { string d = args(2).string_value (); if (d.length () != 1) { error ("__table2csv__: DELIMITER must be a single character."); } sep = d; } // Optional string-quoting mode: "all" (default), "minimal", or "none". string quote_mode = "all"; if (args.length() >= 4) { quote_mode = args(3).string_value (); } // Optional append flag: when true, rows are appended to an existing file // instead of truncating it (used by writetable's 'WriteMode', 'append'). bool append = false; if (args.length() >= 5) { append = args(4).bool_value (); } // Open CSV file in binary mode, so that rows end in '\n' on every platform // rather than '\r\n' on Windows ofstream fd(file.c_str (), append ? (ios::out | ios::app | ios::binary) : (ios::out | ios::binary)); if (! fd.is_open ()) { retval(0) = "cannot open file '" + file + "' for writing."; return retval; } // Initialize necessary variable; string word; string prot = "\""; int rows = C.rows (); int cols = C.columns (); // Process each row for (int row = 0; row < rows; row++) { // Process each element in row for (int col = 0; col < cols; col++) { word = ""; // Add separator if (col != 0) { word += sep; } // Integer scalar values are written exactly: a double cannot hold the // full 64-bit integer range, so bypass the double conversion below. if (C(row, col).isinteger () && C(row, col).numel () == 1) { ostringstream oss; if (C(row, col).is_uint8_type () || C(row, col).is_uint16_type () || C(row, col).is_uint32_type () || C(row, col).is_uint64_type ()) { oss << C(row, col).uint64_scalar_value ().value (); } else { oss << C(row, col).int64_scalar_value ().value (); } word += oss.str (); } // Real numeric values else if (C(row, col).is_real_scalar ()) { double value = C(row, col).double_value (); // Handle NaN first if (octave::math::isna (value)) { word += "NA"; } else if (isnan (value)) { word += "NaN"; } // Infinities as MATLAB writes them; strtod reads any capitals back else if (isinf (value)) { word += (value > 0) ? "Inf" : "-Inf"; } else { char tmp[32]; shortest_double (tmp, 32, value); word += tmp; } } // String values else if (C(row, col).is_string ()) { string str = C(row, col).string_value (); // Decide whether to quote: always ("all"), never ("none"), or only // when the field contains the delimiter, a quote, or a line break // ("minimal"). bool quote; if (quote_mode == "none") { quote = false; } else if (quote_mode == "minimal") { quote = (str.find (sep) != string::npos || str.find (prot) != string::npos || str.find ('\n') != string::npos || str.find ('\r') != string::npos); } else { quote = true; } if (quote) { // Escape embedded quotes by doubling them (RFC 4180) so that // __csv2table__ can restore them when reading the field back. size_t pos = 0; while ((pos = str.find (prot, pos)) != string::npos) { str.insert (pos, prot); pos += 2; } str = prot + str + prot; } word += str; } // Everything else is forced to NaN else if (!C(row, col).isempty ()) { word += "NaN"; } fd << word; } // Add end of line fd << endl; } // Close file fd.close(); retval(0) = 0; return retval; } pr0m1th3as-datatypes-9450b29/src/__table2ods__.cc000066400000000000000000001112321527370255000214470ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the datatypes package for GNU Octave. 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 . */ #include #include #include #include #include #include #include #include #include #include "pugixml.hpp" #include "miniz.h" using namespace std; // Write the shortest decimal string that reads back as exactly the same // double. 15 significant digits suffice for most values and 17 always do, so // a value is written with the fewest digits that survive the read. static int shortest_double (char *buf, size_t n, double v) { int cx = snprintf (buf, n, "%.15g", v); if (strtod (buf, nullptr) != v) { cx = snprintf (buf, n, "%.16g", v); if (strtod (buf, nullptr) != v) cx = snprintf (buf, n, "%.17g", v); } return cx; } static const char *ODS_MIMETYPE = "application/vnd.oasis.opendocument.spreadsheet"; // Write a numeric : integer scalars keep their exact digits // (a double cannot hold the full 64-bit range; the reader restores the exact // class from the metadata sheet), everything else prints the shortest // decimal that reads back as the same double. NaN and // empty produce a bare, value-less cell (i.e. a missing value), and an // infinity the text Inf or -Inf. static void write_float (pugi::xml_node &cell, const octave_value &ov) { if (ov.isinteger () && ov.numel () == 1) { ostringstream oss; if (ov.is_uint8_type () || ov.is_uint16_type () || ov.is_uint32_type () || ov.is_uint64_type ()) oss << ov.uint64_scalar_value ().value (); else oss << ov.int64_scalar_value ().value (); string s = oss.str (); cell.append_attribute ("office:value-type") = "float"; cell.append_attribute ("office:value") = s.c_str (); cell.append_child ("text:p").text ().set (s.c_str ()); } else if (ov.is_real_scalar ()) { double value = ov.double_value (); if (isnan (value)) return; // NaN -> missing (bare cell) if (isinf (value)) { // A spreadsheet application loads a non-finite number as 0, so an // infinity is written as the text Inf or -Inf, which reads back as the // number cell.append_attribute ("office:value-type") = "string"; cell.append_child ("text:p").text ().set (value > 0 ? "Inf" : "-Inf"); return; } char tmp[32]; shortest_double (tmp, 32, value); cell.append_attribute ("office:value-type") = "float"; cell.append_attribute ("office:value") = tmp; cell.append_child ("text:p").text ().set (tmp); } } // Emit one under 'row' for the value 'ov', encoded per the // ODS value-type 'vt' ("float" | "boolean" | "date" | "time" | "string"). // datetime/duration arrive from the .m layer already formatted as ISO 8601 // strings (office:date-value / office:time-value); an empty string is a // missing value. The "string" branch inspects the value so that generic // 'cell' columns still map numbers to float and text to string. static void write_cell (pugi::xml_node &row, const octave_value &ov, const string &vt) { pugi::xml_node cell = row.append_child ("table:table-cell"); // An empty value is a missing cell, EXCEPT an empty char, which is a real // empty string and must be preserved (as an empty string cell) below. if (ov.isempty () && ! ov.is_string ()) return; if (vt == "float") { write_float (cell, ov); } else if (vt == "boolean") { if (ov.is_real_scalar ()) { double value = ov.double_value (); if (isnan (value)) return; bool b = (value != 0); cell.append_attribute ("office:value-type") = "boolean"; cell.append_attribute ("office:boolean-value") = b ? "true" : "false"; cell.append_child ("text:p").text ().set (b ? "TRUE" : "FALSE"); } } else if (vt == "date") { if (ov.is_string ()) { string str = ov.string_value (); if (str.empty ()) return; // NaT -> missing (bare cell) // Cells at exactly midnight display as a plain date, the rest as a // date-and-time value. size_t tpos = str.find ('T'); bool date_only = (tpos != string::npos && str.substr (tpos + 1) == "00:00:00"); cell.append_attribute ("table:style-name") = date_only ? "ce_date" : "ce_datetime"; cell.append_attribute ("office:value-type") = "date"; cell.append_attribute ("office:date-value") = str.c_str (); cell.append_child ("text:p").text ().set (str.c_str ()); } } else if (vt == "time") { if (ov.is_string ()) { string str = ov.string_value (); if (str.empty ()) return; // missing -> bare cell cell.append_attribute ("table:style-name") = "ce_time"; cell.append_attribute ("office:value-type") = "time"; cell.append_attribute ("office:time-value") = str.c_str (); cell.append_child ("text:p").text ().set (str.c_str ()); } } else // "string" / "cell" (generic) { if (ov.is_string ()) { string str = ov.string_value (); cell.append_attribute ("office:value-type") = "string"; cell.append_child ("text:p").text ().set (str.c_str ()); } else // numbers in a generic cell column { write_float (cell, ov); } } } // Estimate the number of characters a cell displays as, used to size columns. // A missing cell is 0; date/time cells use their displayed (formatted) width // rather than the longer ISO string. static size_t display_len (const octave_value &ov, const string &vt) { if (ov.isempty () && ! ov.is_string ()) return 0; if (vt == "boolean") return 5; // "FALSE" if (vt == "float") { if (ov.isinteger () && ov.numel () == 1) { ostringstream oss; if (ov.is_uint8_type () || ov.is_uint16_type () || ov.is_uint32_type () || ov.is_uint64_type ()) oss << ov.uint64_scalar_value ().value (); else oss << ov.int64_scalar_value ().value (); return oss.str ().size (); } if (ov.is_real_scalar ()) { double v = ov.double_value (); if (isnan (v)) return 0; if (isinf (v)) return (v > 0) ? 3 : 4; // "Inf", "-Inf" char tmp[32]; return shortest_double (tmp, 32, v); } return 0; } if (vt == "date" && ov.is_string ()) { string s = ov.string_value (); size_t tp = s.find ('T'); if (tp != string::npos && s.substr (tp + 1) == "00:00:00") return 10; // date only: YYYY-MM-DD return 19; // date and time: YYYY-MM-DD HH:MM:SS } if (ov.is_string ()) // time (ISO) / string return ov.string_value ().size (); return 0; } // Estimate a display width (in cm) for each data column from its widest cell. // The metrics are approximate (we lack the viewer's font), erring wide so that // dates and numbers are not clipped to '###'. static vector compute_col_widths (const Cell &C, const Cell &vtype, const Cell &header) { octave_idx_type rows = C.rows (); octave_idx_type cols = C.columns (); bool have_vt = (vtype.numel () == cols); bool have_hd = (header.columns () == cols && header.rows () > 0); vector widths (cols, 0.0); for (octave_idx_type c = 0; c < cols; c++) { size_t maxlen = 0; if (have_hd) for (octave_idx_type h = 0; h < header.rows (); h++) if (header(h, c).is_string ()) { size_t l = header(h, c).string_value ().size (); if (l > maxlen) maxlen = l; } for (octave_idx_type r = 0; r < rows; r++) { string vt = have_vt ? vtype(c).string_value () : string ("string"); size_t l = display_len (C(r, c), vt); if (l > maxlen) maxlen = l; } double cm = maxlen * 0.21 + 0.35; // ~0.21 cm/char plus padding if (cm < 0.75) cm = 0.75; if (cm > 12.0) cm = 12.0; widths[c] = cm; } return widths; } // Emit a leading empty cell spanning 'coff' columns (a 'Range' column offset). static void pad_cols (pugi::xml_node &row, octave_idx_type coff) { if (coff <= 0) return; pugi::xml_node pad = row.append_child ("table:table-cell"); if (coff > 1) pad.append_attribute ("table:number-columns-repeated") = (int) coff; } // Append the data rows of grid 'C' (per-column value-types 'vtype') to an // existing table node. Used both for a fresh sheet and when appending rows to // an existing sheet ('WriteMode', 'append'). An empty 'vtype' treats every // cell as a "string" (the text-only metadata sheet). static void append_data_rows (pugi::xml_node &table, const Cell &C, const Cell &vtype, octave_idx_type coff = 0) { octave_idx_type rows = C.rows (); octave_idx_type cols = C.columns (); bool have_vt = (vtype.numel () == cols); for (octave_idx_type r = 0; r < rows; r++) { pugi::xml_node row = table.append_child ("table:table-row"); pad_cols (row, coff); for (octave_idx_type c = 0; c < cols; c++) { string vt = have_vt ? vtype(c).string_value () : string ("string"); write_cell (row, C(r, c), vt); } } } // Populate an (already named) table node: optional "coN" column-width styles, // a 'Range' row/column offset, an optional visible header row of variable // names, then the data rows. static void write_sheet_body (pugi::xml_node &table, const Cell &C, const Cell &vtype, bool emit_cols, const Cell &header, octave_idx_type roff, octave_idx_type coff) { octave_idx_type cols = C.columns (); if (emit_cols) { // Leading empty column definition(s) for a 'Range' column offset. if (coff > 0) { pugi::xml_node col = table.append_child ("table:table-column"); if (coff > 1) col.append_attribute ("table:number-columns-repeated") = (int) coff; } for (octave_idx_type c = 0; c < cols; c++) { char nm[16]; snprintf (nm, 16, "co%d", (int) (c + 1)); table.append_child ("table:table-column") .append_attribute ("table:style-name") = nm; } } // Leading empty rows for a 'Range' row offset. if (roff > 0) { pugi::xml_node row = table.append_child ("table:table-row"); if (roff > 1) row.append_attribute ("table:number-rows-repeated") = (int) roff; } // Optional visible header rows of variable names (text cells). A nested // table needs one row per nesting level, so the block may be several rows // deep; the metadata records how many, and the reader takes that many off. if (header.columns () == cols && cols > 0) { for (octave_idx_type h = 0; h < header.rows (); h++) { pugi::xml_node row = table.append_child ("table:table-row"); pad_cols (row, coff); for (octave_idx_type c = 0; c < cols; c++) { pugi::xml_node cell = row.append_child ("table:table-cell"); string t = header(h, c).is_string () ? header(h, c).string_value () : string (); cell.append_attribute ("office:value-type") = "string"; cell.append_child ("text:p").text ().set (t.c_str ()); } } } append_data_rows (table, C, vtype, coff); } // Write a full element named 'name' into 'spreadsheet' from the // cell grid 'C'. See 'write_sheet_body' for the emit_cols/header/roff/coff // arguments. static void write_sheet (pugi::xml_node &spreadsheet, const string &name, const Cell &C, const Cell &vtype, const char *style = 0, bool emit_cols = false, const Cell &header = Cell (), octave_idx_type roff = 0, octave_idx_type coff = 0) { pugi::xml_node table = spreadsheet.append_child ("table:table"); table.append_attribute ("table:name") = name.c_str (); if (style) table.append_attribute ("table:style-name") = style; write_sheet_body (table, C, vtype, emit_cols, header, roff, coff); } // Declare the OpenDocument namespaces and version common to the flat document // and the packaged content.xml. static void add_office_namespaces (pugi::xml_node &root) { root.append_attribute ("xmlns:office") = "urn:oasis:names:tc:opendocument:xmlns:office:1.0"; root.append_attribute ("xmlns:table") = "urn:oasis:names:tc:opendocument:xmlns:table:1.0"; root.append_attribute ("xmlns:text") = "urn:oasis:names:tc:opendocument:xmlns:text:1.0"; root.append_attribute ("xmlns:style") = "urn:oasis:names:tc:opendocument:xmlns:style:1.0"; root.append_attribute ("xmlns:number") = "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"; root.append_attribute ("office:version") = "1.3"; } // Helpers for building the number-format elements of a data style. static void number_part (pugi::xml_node &st, const char *elem, const char *style_val) { pugi::xml_node n = st.append_child (elem); if (style_val) n.append_attribute ("number:style") = style_val; } static void number_text (pugi::xml_node &st, const char *txt) { st.append_child ("number:text").text ().set (txt); } // Declare the automatic styles: a table style that hides the // '__datatypes_meta__' sheet, plus number formats bound to cell styles so that // date and time cells display as formatted dates/times rather than as the // underlying serial numbers. static void fill_automatic_styles (pugi::xml_node &styles) { pugi::xml_node hid = styles.append_child ("style:style"); hid.append_attribute ("style:name") = "hidden_tbl"; hid.append_attribute ("style:family") = "table"; hid.append_child ("style:table-properties") .append_attribute ("table:display") = "false"; // Date format: YYYY-MM-DD pugi::xml_node nd = styles.append_child ("number:date-style"); nd.append_attribute ("style:name") = "N_date"; number_part (nd, "number:year", "long"); number_text (nd, "-"); number_part (nd, "number:month", "long"); number_text (nd, "-"); number_part (nd, "number:day", "long"); // Date-time format: YYYY-MM-DD HH:MM:SS pugi::xml_node ndt = styles.append_child ("number:date-style"); ndt.append_attribute ("style:name") = "N_datetime"; number_part (ndt, "number:year", "long"); number_text (ndt, "-"); number_part (ndt, "number:month", "long"); number_text (ndt, "-"); number_part (ndt, "number:day", "long"); number_text (ndt, " "); number_part (ndt, "number:hours", "long"); number_text (ndt, ":"); number_part (ndt, "number:minutes", "long"); number_text (ndt, ":"); number_part (ndt, "number:seconds", "long"); // Duration format: [HH]:MM:SS, hours not wrapped at 24 pugi::xml_node nt = styles.append_child ("number:time-style"); nt.append_attribute ("style:name") = "N_time"; nt.append_attribute ("number:truncate-on-overflow") = "false"; number_part (nt, "number:hours", "long"); number_text (nt, ":"); number_part (nt, "number:minutes", "long"); number_text (nt, ":"); number_part (nt, "number:seconds", "long"); // Cell styles binding each value type to its number format const char *ce[][2] = { {"ce_date", "N_date"}, {"ce_datetime", "N_datetime"}, {"ce_time", "N_time"} }; for (int i = 0; i < 3; i++) { pugi::xml_node st = styles.append_child ("style:style"); st.append_attribute ("style:name") = ce[i][0]; st.append_attribute ("style:family") = "table-cell"; st.append_attribute ("style:data-style-name") = ce[i][1]; } } // Create and populate the node under 'root'. static void add_automatic_styles (pugi::xml_node &root) { pugi::xml_node styles = root.append_child ("office:automatic-styles"); fill_automatic_styles (styles); } // Append a "coN" table-column style carrying an explicit width for each data // column, so viewers show columns wide enough for their content (the optimal- // width flag alone is not honoured on load). static void add_column_styles (pugi::xml_node &root, const vector &widths) { pugi::xml_node styles = root.child ("office:automatic-styles"); for (size_t i = 0; i < widths.size (); i++) { pugi::xml_node st = styles.append_child ("style:style"); char nm[16]; snprintf (nm, 16, "co%d", (int) (i + 1)); st.append_attribute ("style:name") = nm; st.append_attribute ("style:family") = "table-column"; pugi::xml_node cp = st.append_child ("style:table-column-properties"); char w[32]; snprintf (w, 32, "%.3fcm", widths[i]); cp.append_attribute ("style:column-width") = w; cp.append_attribute ("style:use-optimal-column-width") = "true"; } } // Append office:body -> office:spreadsheet with the data and metadata sheets. // The data sheet emits per-column width styles; the metadata sheet is tagged // with the hidden table style. static void build_spreadsheet (pugi::xml_node &root, const Cell &data, const Cell &vtype, const Cell &meta, const Cell &header, const string &sheetname, octave_idx_type roff, octave_idx_type coff) { pugi::xml_node body = root.append_child ("office:body"); pugi::xml_node spreadsheet = body.append_child ("office:spreadsheet"); write_sheet (spreadsheet, sheetname, data, vtype, 0, true, header, roff, coff); // The hidden metadata sheet is written only for the house format (non-empty // 'meta'); the MATLAB-compatible 'writetable' path passes an empty 'meta'. if (meta.numel () > 0) write_sheet (spreadsheet, "__datatypes_meta__", meta, Cell (), "hidden_tbl"); } // Append office:body -> office:spreadsheet with several data sheets (from the // struct array 'sheets', fields 'name'/'data'/'vtype') followed by one hidden, // sectioned metadata sheet. Used by 'struct2ods' for multi-sheet workbooks. // Each data sheet carries its own visible header rows of variable names, from // the 'header' field when the struct array has one, and no per-column widths. static void build_multi_spreadsheet (pugi::xml_node &root, const octave_map &sheets, const Cell &meta) { pugi::xml_node body = root.append_child ("office:body"); pugi::xml_node spreadsheet = body.append_child ("office:spreadsheet"); Cell names = sheets.contents ("name"); Cell datas = sheets.contents ("data"); Cell vtypes = sheets.contents ("vtype"); bool have_hd = sheets.isfield ("header"); Cell headers = have_hd ? sheets.contents ("header") : Cell (); octave_idx_type K = names.numel (); for (octave_idx_type k = 0; k < K; k++) write_sheet (spreadsheet, names(k).string_value (), datas(k).cell_value (), vtypes(k).cell_value (), 0, false, have_hd ? headers(k).cell_value () : Cell (), 0, 0); if (meta.numel () > 0) write_sheet (spreadsheet, "__datatypes_meta__", meta, Cell (), "hidden_tbl"); } // Populate a document root with automatic styles and the spreadsheet body, // dispatching to the single-sheet or multi-sheet builder. Shared by the flat // and packaged output paths. static void populate_root (pugi::xml_node &root, bool multi, const octave_map &sheets, const Cell &data, const Cell &vtype, const Cell &meta, const Cell &header, const string &sheetname, octave_idx_type roff, octave_idx_type coff) { add_automatic_styles (root); if (multi) { build_multi_spreadsheet (root, sheets, meta); } else { add_column_styles (root, compute_col_widths (data, vtype, header)); build_spreadsheet (root, data, vtype, meta, header, sheetname, roff, coff); } } // Append a little-endian integer to a byte buffer. static void put16 (string &b, mz_uint16 v) { b.push_back ((char) (v & 0xff)); b.push_back ((char) ((v >> 8) & 0xff)); } static void put32 (string &b, mz_uint32 v) { for (int i = 0; i < 4; i++) b.push_back ((char) ((v >> (8 * i)) & 0xff)); } // One entry of the archive: name, raw payload, and whether to deflate it. struct ods_entry { string name; string data; bool compress; }; // Write a list of entries as a compliant '.ods' ZIP. miniz's own zip writer // sets the data-descriptor flag on every entry, which makes the required stored // 'mimetype' entry illegal (a stored entry has no discoverable end without its // sizes in the local header) and strict readers reject the package. We instead // emit the archive by hand -- mimetype first, stored, no data descriptor and no // extra field -- using miniz only for CRC-32 and raw deflate. The caller is // responsible for putting a stored 'mimetype' entry first. static string write_zip_entries (const string &file, const vector &entries) { int flags = tdefl_create_comp_flags_from_zip_params (MZ_DEFAULT_LEVEL, -15, MZ_DEFAULT_STRATEGY); string local, central; mz_uint16 count = 0; for (size_t i = 0; i < entries.size (); i++) { const ods_entry &e = entries[i]; mz_uint32 usize = (mz_uint32) e.data.size (); mz_uint32 crc = (mz_uint32) mz_crc32 (MZ_CRC32_INIT, (const unsigned char *) e.data.data (), e.data.size ()); string payload; mz_uint16 method = 0; if (e.compress && usize > 0) { size_t outlen = 0; void *cd = tdefl_compress_mem_to_heap (e.data.data (), e.data.size (), &outlen, flags); if (cd && outlen < e.data.size ()) { payload.assign ((const char *) cd, outlen); method = 8; } else payload = e.data; // incompressible: store it if (cd) mz_free (cd); } else payload = e.data; mz_uint32 csize = (mz_uint32) payload.size (); mz_uint32 offset = (mz_uint32) local.size (); mz_uint16 namelen = (mz_uint16) e.name.size (); // Local file header (no general-purpose flags, no extra field) put32 (local, 0x04034b50); put16 (local, 20); put16 (local, 0); put16 (local, method); put16 (local, 0); put16 (local, 0); put32 (local, crc); put32 (local, csize); put32 (local, usize); put16 (local, namelen); put16 (local, 0); local += e.name; local += payload; // Central directory header put32 (central, 0x02014b50); put16 (central, 20); put16 (central, 20); put16 (central, 0); put16 (central, method); put16 (central, 0); put16 (central, 0); put32 (central, crc); put32 (central, csize); put32 (central, usize); put16 (central, namelen); put16 (central, 0); put16 (central, 0); put16 (central, 0); put16 (central, 0); put32 (central, 0); put32 (central, offset); central += e.name; count++; } mz_uint32 cd_offset = (mz_uint32) local.size (); mz_uint32 cd_size = (mz_uint32) central.size (); string eocd; put32 (eocd, 0x06054b50); put16 (eocd, 0); put16 (eocd, 0); put16 (eocd, count); put16 (eocd, count); put32 (eocd, cd_size); put32 (eocd, cd_offset); put16 (eocd, 0); ofstream f (file.c_str (), ios::binary); if (! f.is_open ()) return "cannot open file '" + file + "' for writing."; f.write (local.data (), local.size ()); f.write (central.data (), central.size ()); f.write (eocd.data (), eocd.size ()); f.close (); if (! f) return "failed to write ODS archive '" + file + "'."; return ""; } // Package a freshly built content.xml as a minimal, compliant '.ods'. static string write_ods_zip (const string &file, const string &content) { string manifest = "\n" "\n" " \n" " \n" "\n"; vector entries; entries.push_back (ods_entry {"mimetype", string (ODS_MIMETYPE), false}); entries.push_back (ods_entry {"META-INF/manifest.xml", manifest, true}); entries.push_back (ods_entry {"content.xml", content, true}); return write_zip_entries (file, entries); } // The text of the cell at 0-based column 'col' of a metadata row, as the // reader sees it: repeated cells are counted and several paragraphs join with // newlines. static string meta_cell (const pugi::xml_node &row, long col) { long c = 0; for (pugi::xml_node cell = row.child ("table:table-cell"); cell; cell = cell.next_sibling ("table:table-cell")) { long rep = cell.attribute ("table:number-columns-repeated").as_int (1); if (rep < 1) rep = 1; if (col < c + rep) { string out; for (pugi::xml_node p = cell.child ("text:p"); p; p = p.next_sibling ("text:p")) { if (! out.empty ()) out += "\n"; out += p.text ().as_string (); } return out; } c += rep; } return string (); } // Keep the metadata sheet true to the data sheets once 'sheetname' has been // written without metadata. A replaced sheet loses its section and every // events crossref line naming it; in an unsectioned grid, which describes the // first data sheet 'first' alone, replacing that sheet removes the grid. A // sheet added beside an unsectioned grid first gives the grid the marker of // the sheet it describes, so that it is not taken for the new sheet's. static void update_meta (pugi::xml_node &spreadsheet, pugi::xml_node &meta, const string &sheetname, const string &first, bool replaced) { const string mark = "## Sheet: "; const string xref = "## Events crossref:"; bool sectioned = false; for (pugi::xml_node row = meta.child ("table:table-row"); row && ! sectioned; row = row.next_sibling ("table:table-row")) sectioned = (meta_cell (row, 0).compare (0, mark.size (), mark) == 0); if (! sectioned) { if (replaced && sheetname == first) spreadsheet.remove_child (meta); else if (! replaced) { pugi::xml_node top = meta.child ("table:table-row"); pugi::xml_node row = meta.insert_child_before ("table:table-row", top); pugi::xml_node cell = row.append_child ("table:table-cell"); cell.append_attribute ("office:value-type") = "string"; cell.append_child ("text:p").text ().set ((mark + first).c_str ()); } return; } if (! replaced) return; bool inside = false; pugi::xml_node row = meta.child ("table:table-row"); while (row) { pugi::xml_node next = row.next_sibling ("table:table-row"); string c0 = meta_cell (row, 0); bool drop = false; if (c0.compare (0, mark.size (), mark) == 0) { inside = (c0.substr (mark.size ()) == sheetname); drop = inside; } else if (inside) drop = true; else if (c0.compare (0, xref.size (), xref) == 0) drop = (meta_cell (row, 1) == sheetname || meta_cell (row, 2) == sheetname); if (drop) meta.remove_child (row); row = next; } } // Read-modify-write an existing spreadsheet: add or replace the sheet named // 'sheetname' (or, for 'append', append the data rows to it) while preserving // every other sheet and every other package part. 'writemode' is "append", // "overwritesheet", "inplace", or "" (default); all but "append" replace the // sheet's contents. Returns "" on success or an error message. static string merge_ods (const string &file, bool flat, const Cell &data, const Cell &vtype, const Cell &header, const string &sheetname, const string &writemode) { // --- Read the existing package --- vector entries; // packaged '.ods': every archive member size_t content_idx = 0; bool have_content = false; string content_xml; if (flat) { ifstream f (file.c_str (), ios::binary); if (! f.is_open ()) return "cannot read '" + file + "'."; ostringstream ss; ss << f.rdbuf (); content_xml = ss.str (); } else { mz_zip_archive zip; memset (&zip, 0, sizeof (zip)); if (! mz_zip_reader_init_file (&zip, file.c_str (), 0)) return "cannot read '" + file + "' as a ZIP archive."; mz_uint n = mz_zip_reader_get_num_files (&zip); for (mz_uint i = 0; i < n; i++) { mz_zip_archive_file_stat st; if (! mz_zip_reader_file_stat (&zip, i, &st)) continue; size_t sz = 0; void *p = mz_zip_reader_extract_to_heap (&zip, i, &sz, 0); string bytes = (p ? string ((const char *) p, sz) : string ()); if (p) mz_free (p); string nm = st.m_filename; if (nm == "content.xml") { content_idx = entries.size (); have_content = true; content_xml = bytes; } entries.push_back (ods_entry {nm, bytes, nm != "mimetype"}); } mz_zip_reader_end (&zip); if (! have_content) return "'" + file + "' has no 'content.xml' entry."; } // --- Parse content.xml and locate the spreadsheet --- pugi::xml_document doc; if (! doc.load_buffer (content_xml.data (), content_xml.size ())) return "cannot parse the spreadsheet in '" + file + "'."; pugi::xml_node root = doc.child ("office:document"); if (! root) root = doc.child ("office:document-content"); if (! root) return "'" + file + "' is not an OpenDocument spreadsheet."; pugi::xml_node spreadsheet = root.child ("office:body").child ("office:spreadsheet"); if (! spreadsheet) return "'" + file + "' is not an OpenDocument spreadsheet."; // --- Ensure our date/time cell styles are present (foreign files lack them; // without them date/time cells keep their value but lose formatting) --- pugi::xml_node styles = root.child ("office:automatic-styles"); bool have_styles = false; if (styles) for (pugi::xml_node s = styles.child ("style:style"); s; s = s.next_sibling ("style:style")) if (string (s.attribute ("style:name").as_string ()) == "ce_date") { have_styles = true; break; } if (! have_styles) { if (! styles) styles = root.insert_child_before ("office:automatic-styles", root.child ("office:body")); fill_automatic_styles (styles); } // --- Add, replace, or append to the target sheet --- pugi::xml_node sheet, meta; string first; for (pugi::xml_node t = spreadsheet.child ("table:table"); t; t = t.next_sibling ("table:table")) { string name = t.attribute ("table:name").as_string (); if (name == "__datatypes_meta__") meta = t; else if (first.empty ()) first = name; if (name == sheetname && ! sheet) sheet = t; } const bool existed = sheet; if (writemode == "append" && sheet) { // Append the data rows below the sheet's existing content, no header. append_data_rows (sheet, data, vtype, 0); } else if (sheet) { // Replace the sheet's contents in place (default / overwritesheet / // inplace), keeping its position among the other sheets. while (sheet.first_child ()) sheet.remove_child (sheet.first_child ()); write_sheet_body (sheet, data, vtype, false, header, 0, 0); } else { // The sheet does not exist yet: add it after the existing sheets. write_sheet (spreadsheet, sheetname, data, vtype, 0, false, header, 0, 0); } // --- Keep the metadata sheet true to the data sheets; rows appended to an // existing sheet change nothing it records --- if (meta && ! first.empty () && ! (writemode == "append" && existed)) update_meta (spreadsheet, meta, sheetname, first, existed); // --- Serialize and write back --- ostringstream oss; doc.save (oss, " "); string newxml = oss.str (); if (flat) { ofstream f (file.c_str (), ios::binary); if (! f.is_open ()) return "cannot open file '" + file + "' for writing."; f.write (newxml.data (), newxml.size ()); f.close (); if (! f) return "failed to write '" + file + "'."; return ""; } entries[content_idx].data = newxml; // The stored 'mimetype' entry must come first. for (size_t i = 0; i < entries.size (); i++) if (entries[i].name == "mimetype") { entries[i].compress = false; if (i != 0) { ods_entry tmp = entries[0]; entries[0] = entries[i]; entries[i] = tmp; } break; } return write_zip_entries (file, entries); } DEFUN_DLD (__table2ods__, args, nargout, "-*- texinfo -*-\n \ @deftypefn {datatypes} {} __table2ods__ (@var{file}, @var{data}, \ @var{vtype}, @var{meta}, @var{flat})\n\ @deftypefnx {datatypes} {} __table2ods__ (@var{file}, @var{data}, \ @var{vtype}, @var{meta}, @var{flat}, @var{opts})\n\ \n\ \n\ Barebone function for saving a table to a flat (@qcode{.fods}) or compressed \ (@qcode{.ods}) OpenDocument spreadsheet file.\n\ \n\ This is a helper IO function for the @qcode{table2ods} method of the \ @qcode{table} class. Do NOT call it directly. \n\ \n\ @end deftypefn") { octave_value_list retval (nargout); if (args.length () != 5 && args.length () != 6) error ("__table2ods__: five or six input arguments are required."); string file = args(0).string_value (); Cell data = args(1).cell_value (); Cell vtype = args(2).cell_value (); Cell meta = args(3).cell_value (); bool flat = args(4).bool_value (); // Optional options struct (6th arg) with any of the fields: // 'header' -- visible header row of variable names (cellstr); when // present, an empty 'meta' suppresses the metadata sheet. // 'sheetname' -- name of the data sheet (default "Sheet1"). // 'roff'/'coff' -- leading empty row/column offsets for a 'Range' anchor. Cell header; string sheetname = "Sheet1"; octave_idx_type roff = 0, coff = 0; bool multi = false; octave_map sheets; bool merge = false; string writemode; if (args.length () == 6) { octave_scalar_map opts = args(5).scalar_map_value (); if (opts.isfield ("header")) header = opts.contents ("header").cell_value (); if (opts.isfield ("sheetname")) sheetname = opts.contents ("sheetname").string_value (); if (opts.isfield ("roff")) roff = (octave_idx_type) opts.contents ("roff").double_value (); if (opts.isfield ("coff")) coff = (octave_idx_type) opts.contents ("coff").double_value (); // Multi-sheet mode: 'sheets' is a struct array (fields name/data/vtype) and // 'meta' is the single sectioned metadata grid. Overrides the scalar args. if (opts.isfield ("sheets")) { multi = true; sheets = opts.contents ("sheets").map_value (); meta = opts.isfield ("meta") ? opts.contents ("meta").cell_value () : Cell (); } // Merge mode: add/replace/append to a sheet of an existing file in place. if (opts.isfield ("merge")) merge = opts.contents ("merge").bool_value (); if (opts.isfield ("writemode")) writemode = opts.contents ("writemode").string_value (); } // Read-modify-write an existing package rather than writing a fresh one. if (merge) { string msg = merge_ods (file, flat, data, vtype, header, sheetname, writemode); retval(0) = msg.empty () ? octave_value (0.0) : octave_value (msg); return retval; } pugi::xml_document doc; pugi::xml_node decl = doc.append_child (pugi::node_declaration); decl.append_attribute ("version") = "1.0"; decl.append_attribute ("encoding") = "UTF-8"; if (flat) { // A flat '.fods' is a single carrying the mimetype pugi::xml_node root = doc.append_child ("office:document"); add_office_namespaces (root); root.append_attribute ("office:mimetype") = ODS_MIMETYPE; populate_root (root, multi, sheets, data, vtype, meta, header, sheetname, roff, coff); if (! doc.save_file (file.c_str (), " ")) { retval(0) = "cannot open file '" + file + "' for writing."; return retval; } } else { // A compressed '.ods' packages a content.xml () pugi::xml_node root = doc.append_child ("office:document-content"); add_office_namespaces (root); populate_root (root, multi, sheets, data, vtype, meta, header, sheetname, roff, coff); ostringstream oss; doc.save (oss, " "); string msg = write_ods_zip (file, oss.str ()); if (! msg.empty ()) { retval(0) = msg; return retval; } } retval(0) = 0; return retval; } pr0m1th3as-datatypes-9450b29/src/__table2xlsx__.cc000066400000000000000000000411671527370255000216710ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the datatypes package for GNU Octave. 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 . */ #include #include #include #include #include #include #include #include #include "miniz.h" using namespace std; // Days from 1970-01-01 to civil date y-m-d (Howard Hinnant's algorithm). static long days_from_civil (long y, unsigned m, unsigned d) { y -= m <= 2; long era = (y >= 0 ? y : y - 399) / 400; unsigned yoe = (unsigned) (y - era * 400); unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; return era * 146097 + (long) doe - 719468; } // Excel serial number (1900 date system) for an ISO datetime "Y-M-DThh:mm:ss". // 25569 is the serial of 1970-01-01. The 1900 system wrongly treats 1900 as a // leap year, so serial 61 (1900-03-01) counts the nonexistent 1900-02-29 and // every earlier date is one less than the plain day count: 1900-02-28 is 59, // 1900-01-01 is 1 and 1899-12-31 is 0. 'date_only' reports whether the time // part is exactly midnight, so the caller can pick a date vs date-and-time // format. static bool iso_datetime_to_serial (const string &s, double &serial, bool &date_only) { int Y, Mo, D, h, mi; double sec; if (sscanf (s.c_str (), "%d-%d-%dT%d:%d:%lf", &Y, &Mo, &D, &h, &mi, &sec) != 6) return false; double day = (double) days_from_civil (Y, (unsigned) Mo, (unsigned) D) + 25569.0; if (day < 61.0) day -= 1.0; serial = day + (h * 3600.0 + mi * 60.0 + sec) / 86400.0; date_only = (h == 0 && mi == 0 && sec == 0.0); return true; } // Excel time serial (fraction of a day) for an ISO 8601 duration "PTnHnMnS", // hours not wrapped at 24; a leading '-' negates it. static bool pt_duration_to_serial (const string &in, double &serial) { string s = in; bool neg = (! s.empty () && s[0] == '-'); if (neg) s.erase (0, 1); double H, M, S; if (sscanf (s.c_str (), "PT%lfH%lfM%lfS", &H, &M, &S) != 3) return false; serial = (H * 3600.0 + M * 60.0 + S) / 86400.0; if (neg) serial = -serial; return true; } // Spreadsheet column letters for a 1-based column index (1->A, 27->AA). static string col_letter (long n) { string s; while (n > 0) { long r = (n - 1) % 26; s = string (1, (char) ('A' + r)) + s; n = (n - 1) / 26; } return s; } // XML-escape text content / attribute values. static string xml_escape (const string &in) { string out; out.reserve (in.size ()); for (size_t i = 0; i < in.size (); i++) { char c = in[i]; switch (c) { case '&': out += "&"; break; case '<': out += "<"; break; case '>': out += ">"; break; case '"': out += """; break; default: out += c; } } return out; } // Emit one worksheet cell at (row, col) for value 'ov' of value-type 'vt'. // A missing/empty value produces no cell (a blank). Style indices match the // cellXfs written in styles.xml: 1 = date, 2 = date-and-time, 3 = duration. static void emit_cell (ostringstream &oss, long row, long col, const octave_value &ov, const string &vt) { if (ov.isempty () && ! ov.is_string ()) return; string ref = col_letter (col) + std::to_string (row); if (vt == "float") { if (ov.isinteger () && ov.numel () == 1) { ostringstream v; if (ov.is_uint8_type () || ov.is_uint16_type () || ov.is_uint32_type () || ov.is_uint64_type ()) v << ov.uint64_scalar_value ().value (); else v << ov.int64_scalar_value ().value (); oss << "" << v.str () << ""; } else if (ov.is_real_scalar ()) { double val = ov.double_value (); if (isnan (val)) return; if (isinf (val)) { // Excel has no infinite number; the text reads back as the number oss << "" << (val > 0 ? "Inf" : "-Inf") << ""; return; } char tmp[32]; snprintf (tmp, 32, "%.15g", val); oss << "" << tmp << ""; } } else if (vt == "boolean") { if (ov.is_real_scalar ()) { double val = ov.double_value (); if (isnan (val)) return; oss << "" << (val != 0 ? 1 : 0) << ""; } } else if (vt == "date") { if (ov.is_string ()) { double serial; bool date_only; string str = ov.string_value (); if (str.empty () || ! iso_datetime_to_serial (str, serial, date_only)) return; // Excel's 1900 date system has no serial below 0 (1899-12-31), so an // earlier date cannot be stored as a date at all and goes out as text, as // MATLAB does. MATLAB writes it in the datetime's display format; the // ISO form used here is what this layer receives and is unambiguous. if (serial < 0.0) { string txt = str; size_t tpos = txt.find ('T'); if (tpos != string::npos) { if (date_only) txt.erase (tpos); else txt[tpos] = ' '; } oss << "" << xml_escape (txt) << ""; return; } char tmp[32]; snprintf (tmp, 32, "%.11g", serial); oss << "" << tmp << ""; } } else if (vt == "time") { if (ov.is_string ()) { double serial; string str = ov.string_value (); if (str.empty () || ! pt_duration_to_serial (str, serial)) return; char tmp[32]; snprintf (tmp, 32, "%.11g", serial); oss << "" << tmp << ""; } } else // string / generic { if (ov.is_string ()) { oss << "" << xml_escape (ov.string_value ()) << ""; } else if (ov.is_real_scalar ()) { double val = ov.double_value (); if (isnan (val)) return; if (isinf (val)) { oss << "" << (val > 0 ? "Inf" : "-Inf") << ""; return; } char tmp[32]; snprintf (tmp, 32, "%.15g", val); oss << "" << tmp << ""; } } } // Build the worksheet part from the data grid, per-column value types, an // optional header row of variable names, and a 'Range' row/column offset. static string build_worksheet (const Cell &data, const Cell &vtype, const Cell &header, long roff, long coff) { octave_idx_type rows = data.rows (); octave_idx_type cols = data.columns (); bool have_vt = (vtype.numel () == cols); bool have_hd = (header.numel () == cols && cols > 0); ostringstream oss; oss << "" << ""; // 1-based row number of the next row - 1 long row = roff; if (have_hd) { row++; oss << ""; for (octave_idx_type c = 0; c < cols; c++) emit_cell (oss, row, coff + c + 1, header(c), "string"); oss << ""; } for (octave_idx_type r = 0; r < rows; r++) { row++; oss << ""; for (octave_idx_type c = 0; c < cols; c++) { string vt = have_vt ? vtype(c).string_value () : string ("string"); emit_cell (oss, row, coff + c + 1, data(r, c), vt); } oss << ""; } oss << ""; return oss.str (); } // [Content_Types].xml with one worksheet override per sheet. 'macro' selects // the macro-enabled ('.xlsm') workbook content type. static string content_types_xml (bool macro, size_t nsheets) { string wb = macro ? "application/vnd.ms-excel.sheet.macroEnabled.main+xml" : "application/vnd.openxmlformats-officedocument.spreadsheetml" ".sheet.main+xml"; ostringstream o; o << "" "" "" "" ""; for (size_t k = 1; k <= nsheets; k++) o << ""; o << ""; return o.str (); } static const char *ROOT_RELS = "" ""; // workbook.xml.rels: one worksheet relationship per sheet plus the styles part. static string workbook_rels_xml (size_t nsheets) { ostringstream o; o << "" ""; for (size_t k = 1; k <= nsheets; k++) o << ""; o << ""; return o.str (); } // Styles: three custom number formats (date, date-and-time, duration) bound to // cell formats s=1..3; s=0 is the default General format. static const char *STYLES_XML = "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""; // workbook.xml listing each sheet (sheetId 1..N, relationship rId1..rIdN). static string workbook_xml (const vector &names) { ostringstream o; o << "" ""; for (size_t k = 0; k < names.size (); k++) o << ""; o << ""; return o.str (); } DEFUN_DLD (__table2xlsx__, args, nargout, "-*- texinfo -*-\n \ @deftypefn {datatypes} {} __table2xlsx__ (@var{file}, @var{data}, \ @var{vtype}, @var{opts})\n\ \n\ \n\ Barebone function for writing an Office Open XML (@qcode{.xlsx} / \ @qcode{.xlsm}) spreadsheet. A single sheet is written from @var{data} / \ @var{vtype}; when @var{opts} carries a @qcode{sheets} struct array (fields \ @qcode{name}, @qcode{data}, @qcode{vtype}, @qcode{header}) a multi-sheet \ workbook is written instead.\n\ \n\ This is a helper IO function for @qcode{writetable} and @qcode{struct2xlsx}. \ Do NOT call it directly. \n\ \n\ @end deftypefn") { octave_value_list retval (nargout); if (args.length () != 3 && args.length () != 4) error ("__table2xlsx__: three or four input arguments are required."); string file = args(0).string_value (); Cell data = args(1).cell_value (); Cell vtype = args(2).cell_value (); Cell header; string sheetname = "Sheet1"; long roff = 0, coff = 0; bool macro = false; bool multi = false; octave_map sheets; if (args.length () == 4) { octave_scalar_map opts = args(3).scalar_map_value (); if (opts.isfield ("header")) header = opts.contents ("header").cell_value (); if (opts.isfield ("sheetname")) sheetname = opts.contents ("sheetname").string_value (); if (opts.isfield ("roff")) roff = (long) opts.contents ("roff").double_value (); if (opts.isfield ("coff")) coff = (long) opts.contents ("coff").double_value (); if (opts.isfield ("macro")) macro = opts.contents ("macro").bool_value (); if (opts.isfield ("sheets")) { multi = true; sheets = opts.contents ("sheets").map_value (); } } // Build the worksheet XML for each sheet. vector names, sheetxml; if (multi) { Cell nm = sheets.contents ("name"); Cell da = sheets.contents ("data"); Cell vt = sheets.contents ("vtype"); bool have_hd = sheets.isfield ("header"); Cell hd; if (have_hd) hd = sheets.contents ("header"); for (octave_idx_type k = 0; k < nm.numel (); k++) { names.push_back (nm(k).string_value ()); sheetxml.push_back (build_worksheet (da(k).cell_value (), vt(k).cell_value (), have_hd ? hd(k).cell_value () : Cell (), 0, 0)); } } else { names.push_back (sheetname); sheetxml.push_back (build_worksheet (data, vtype, header, roff, coff)); } string ctypes = content_types_xml (macro, names.size ()); string wb = workbook_xml (names); string wbrels = workbook_rels_xml (names.size ()); string root_rels = ROOT_RELS, styles = STYLES_XML; // Package the parts. XLSX has no stored-first-entry rule, so miniz's own ZIP // writer is fine here. mz_zip_archive zip; memset (&zip, 0, sizeof (zip)); remove (file.c_str ()); if (! mz_zip_writer_init_file (&zip, file.c_str (), 0)) { retval(0) = "cannot open file '" + file + "' for writing."; return retval; } bool ok = true; ok = ok && mz_zip_writer_add_mem (&zip, "[Content_Types].xml", ctypes.data (), ctypes.size (), MZ_DEFAULT_COMPRESSION); ok = ok && mz_zip_writer_add_mem (&zip, "_rels/.rels", root_rels.data (), root_rels.size (), MZ_DEFAULT_COMPRESSION); ok = ok && mz_zip_writer_add_mem (&zip, "xl/workbook.xml", wb.data (), wb.size (), MZ_DEFAULT_COMPRESSION); ok = ok && mz_zip_writer_add_mem (&zip, "xl/_rels/workbook.xml.rels", wbrels.data (), wbrels.size (), MZ_DEFAULT_COMPRESSION); ok = ok && mz_zip_writer_add_mem (&zip, "xl/styles.xml", styles.data (), styles.size (), MZ_DEFAULT_COMPRESSION); for (size_t k = 0; k < sheetxml.size () && ok; k++) { string pn = "xl/worksheets/sheet" + std::to_string (k + 1) + ".xml"; ok = ok && mz_zip_writer_add_mem (&zip, pn.c_str (), sheetxml[k].data (), sheetxml[k].size (), MZ_DEFAULT_COMPRESSION); } if (! ok || ! mz_zip_writer_finalize_archive (&zip)) { mz_zip_writer_end (&zip); remove (file.c_str ()); retval(0) = "failed to write Excel archive '" + file + "'."; return retval; } mz_zip_writer_end (&zip); retval(0) = 0; return retval; } pr0m1th3as-datatypes-9450b29/src/__xlsx2table__.cc000066400000000000000000000324251527370255000216660ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the datatypes package for GNU Octave. 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 . */ #include #include #include #include #include #include #include #include #include #include #include #include "pugixml.hpp" #include "miniz.h" using namespace std; // Civil date from a day count since 1970-01-01 (inverse of days_from_civil). static void civil_from_days (long z, long &y, unsigned &m, unsigned &d) { z += 719468; long era = (z >= 0 ? z : z - 146096) / 146097; unsigned doe = (unsigned) (z - era * 146097); unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; long yy = (long) yoe + era * 400; unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100); unsigned mp = (5 * doy + 2) / 153; d = doy - (153 * mp + 2) / 5 + 1; m = mp < 10 ? mp + 3 : mp - 9; y = yy + (m <= 2); } // Excel serial -> ISO datetime "Y-M-DThh:mm:ss" (whole-second resolution), the // form the .m layer's datetime parser expects. 25569 is the serial of // 1970-01-01 in Excel's 1900 date system, which wrongly treats 1900 as a leap // year: serials above 60 span the nonexistent 1900-02-29, so every serial up to // and including 60 -- serial 60 being that phantom date itself -- counts from // the day after the 1899-12-30 epoch. Serials 60 and 61 therefore both give // 1900-03-01, and a fractional 60.5 lands a day earlier than 60; this matches // both Excel and MATLAB, and '__datetime__.cc' applies the same rule to // datetime (..., 'ConvertFrom', 'excel'). static string serial_to_iso_datetime (double serial) { double days = serial - (serial <= 60.0 ? 25568.0 : 25569.0); long intdays = (long) floor (days); long secs = (long) llround ((days - intdays) * 86400.0); if (secs >= 86400) { secs -= 86400; intdays++; } long y; unsigned mo, d; civil_from_days (intdays, y, mo, d); unsigned h = (unsigned) (secs / 3600) % 24, mi = (unsigned) (secs / 60) % 60; unsigned s = (unsigned) secs % 60; char buf[48]; snprintf (buf, sizeof (buf), "%04ld-%02u-%02uT%02u:%02u:%02u", y, mo, d, h, mi, s); return buf; } // Excel time serial -> ISO 8601 duration "PTnHnMnS" (hours not wrapped). static string serial_to_pt_duration (double serial) { bool neg = serial < 0; long isec = (long) llround (fabs (serial) * 86400.0); long H = isec / 3600, M = (isec % 3600) / 60, S = isec % 60; char buf[64]; snprintf (buf, 64, "%sPT%ldH%ldM%ldS", neg ? "-" : "", H, M, S); return buf; } // Parse an A1 cell reference into 1-based (row, col). static bool ref_to_rowcol (const string &ref, long &row, long &col) { size_t i = 0; col = 0; while (i < ref.size () && isalpha ((unsigned char) ref[i])) { col = col * 26 + (toupper ((unsigned char) ref[i]) - 'A' + 1); i++; } if (i == 0 || i >= ref.size ()) return false; row = atol (ref.c_str () + i); return (col > 0 && row > 0); } // Extract a named archive member as a string; empty if absent. static string read_member (mz_zip_archive &zip, const char *name) { size_t sz = 0; void *p = mz_zip_reader_extract_file_to_heap (&zip, name, &sz, 0); if (! p) return string (); string out ((const char *) p, sz); mz_free (p); return out; } // Concatenate the text of all descendants of a node (shared-string item). static string all_text (const pugi::xml_node &node) { string out; for (pugi::xml_node t = node.child ("t"); t; t = t.next_sibling ("t")) out += t.text ().as_string (); for (pugi::xml_node r = node.child ("r"); r; r = r.next_sibling ("r")) out += r.child ("t").text ().as_string (); return out; } // Classify a cell's style index into 'date', 'time', or "" (plain number), // using the number-format bound to that cell format. static string style_kind (long s, const vector &xf_fmt, const map &custom_fmt) { if (s < 0 || (size_t) s >= xf_fmt.size ()) return string (); int id = xf_fmt[s]; // Built-in date/time format ids. if ((id >= 14 && id <= 22)) return "date"; if (id == 45 || id == 46 || id == 47) return "time"; if (id >= 164) { map::const_iterator it = custom_fmt.find (id); if (it != custom_fmt.end ()) { const string &f = it->second; if (f.find ('[') != string::npos) // elapsed [h]/[hh]/[mm] -> time return "time"; if (f.find ('y') != string::npos || f.find ('Y') != string::npos || f.find ('d') != string::npos || f.find ('D') != string::npos) return "date"; if (f.find ('h') != string::npos || f.find ('H') != string::npos || f.find ('s') != string::npos || f.find ('S') != string::npos) return "date"; // time-of-day -> datetime } } return string (); } DEFUN_DLD (__xlsx2table__, args, , "-*- texinfo -*-\n \ @deftypefn {datatypes} {[@var{data}, @var{vtype}, @var{meta}, @var{names}] =} \ __xlsx2table__ (@var{file})\n\ @deftypefnx {datatypes} {[@var{data}, @var{vtype}, @var{meta}, @var{names}] =} \ __xlsx2table__ (@var{file}, @var{sheet})\n\ \n\ \n\ Barebone function for reading a sheet of an Office Open XML (@qcode{.xlsx} / \ @qcode{.xlsm}) spreadsheet.\n\ \n\ This is a helper IO function for the @qcode{readtable} function. Do NOT call \ it directly. \n\ \n\ @end deftypefn") { octave_value_list retval (4); retval(1) = Cell (); // no metadata sheet in the interop format retval(2) = Cell (); retval(3) = Cell (); if (args.length () < 1 || args.length () > 2) error ("__xlsx2table__: one or two input arguments are required."); string file = args(0).string_value (); string want_name; long want_index = 0; bool by_name = false, by_index = false; if (args.length () == 2 && ! args(1).isempty ()) { if (args(1).is_string ()) { want_name = args(1).string_value (); by_name = true; } else { want_index = (long) args(1).scalar_value (); by_index = true; } } mz_zip_archive zip; memset (&zip, 0, sizeof (zip)); if (! mz_zip_reader_init_file (&zip, file.c_str (), 0)) { retval(0) = string ("cannot read '") + file + "' as an Excel archive."; return retval; } // --- workbook.xml: sheet names in order, plus each sheet's // relationship id --- pugi::xml_document wbdoc; string wbxml = read_member (zip, "xl/workbook.xml"); if (wbxml.empty () || ! wbdoc.load_buffer (wbxml.data (), wbxml.size ())) { mz_zip_reader_end (&zip); retval(0) = string ("'") + file + "' is not a readable Excel workbook."; return retval; } vector sheet_names, sheet_rid; for (pugi::xml_node sh = wbdoc.child ("workbook").child ("sheets").child ("sheet"); sh; sh = sh.next_sibling ("sheet")) { sheet_names.push_back (sh.attribute ("name").as_string ()); sheet_rid.push_back (sh.attribute ("r:id").as_string ()); } // Select the sheet. long sel = -1; if (by_name) { for (size_t i = 0; i < sheet_names.size (); i++) if (sheet_names[i] == want_name) { sel = (long) i; break; } if (sel < 0) { mz_zip_reader_end (&zip); retval(0) = string ("sheet '") + want_name + "' not found in '" + file + "'."; return retval; } } else if (by_index) { if (want_index < 1 || (size_t) want_index > sheet_names.size ()) { mz_zip_reader_end (&zip); retval(0) = string ("sheet index ") + std::to_string (want_index) + " out of range in '" + file + "'."; return retval; } sel = want_index - 1; } else sel = (sheet_names.empty () ? -1 : 0); Cell names_out (1, sheet_names.size ()); for (size_t i = 0; i < sheet_names.size (); i++) names_out(i) = sheet_names[i]; retval(3) = names_out; if (sel < 0) { mz_zip_reader_end (&zip); retval(0) = Cell (); retval(1) = Cell (); return retval; } // --- resolve the worksheet part via workbook relationships --- string target = "worksheets/sheet1.xml"; pugi::xml_document rdoc; string rxml = read_member (zip, "xl/_rels/workbook.xml.rels"); if (! rxml.empty () && rdoc.load_buffer (rxml.data (), rxml.size ())) { for (pugi::xml_node rel = rdoc.child ("Relationships").child ("Relationship"); rel; rel = rel.next_sibling ("Relationship")) if (sheet_rid[sel] == rel.attribute ("Id").as_string ()) { target = rel.attribute ("Target").as_string (); break; } } if (! target.empty () && target[0] == '/') target = target.substr (1); else target = "xl/" + target; // --- shared strings (optional) --- vector shared; string sxml = read_member (zip, "xl/sharedStrings.xml"); if (! sxml.empty ()) { pugi::xml_document sdoc; if (sdoc.load_buffer (sxml.data (), sxml.size ())) for (pugi::xml_node si = sdoc.child ("sst").child ("si"); si; si = si.next_sibling ("si")) shared.push_back (all_text (si)); } // --- styles: map each cell format (s index) to its number-format id --- vector xf_fmt; map custom_fmt; string stxml = read_member (zip, "xl/styles.xml"); if (! stxml.empty ()) { pugi::xml_document stdoc; if (stdoc.load_buffer (stxml.data (), stxml.size ())) { pugi::xml_node ss = stdoc.child ("styleSheet"); for (pugi::xml_node nf = ss.child ("numFmts").child ("numFmt"); nf; nf = nf.next_sibling ("numFmt")) custom_fmt[nf.attribute ("numFmtId").as_int ()] = nf.attribute ("formatCode").as_string (); for (pugi::xml_node xf = ss.child ("cellXfs").child ("xf"); xf; xf = xf.next_sibling ("xf")) xf_fmt.push_back (xf.attribute ("numFmtId").as_int (0)); } } // --- worksheet cells --- pugi::xml_document wsdoc; string wsxml = read_member (zip, target.c_str ()); mz_zip_reader_end (&zip); if (wsxml.empty () || ! wsdoc.load_buffer (wsxml.data (), wsxml.size ())) { retval(0) = string ("cannot read sheet in '") + file + "'."; return retval; } struct xcell { long row, col; octave_value val; string vt; }; vector cells; long maxrow = 0, maxcol = 0; pugi::xml_node sheetData = wsdoc.child ("worksheet").child ("sheetData"); for (pugi::xml_node row = sheetData.child ("row"); row; row = row.next_sibling ("row")) { for (pugi::xml_node c = row.child ("c"); c; c = c.next_sibling ("c")) { long r, col; if (! ref_to_rowcol (c.attribute ("r").as_string (), r, col)) continue; string t = c.attribute ("t").as_string (); long s = c.attribute ("s").as_int (-1); // An error cell (#N/A, #NUM! and the rest) is a missing value, as // MATLAB reads one if (t == "e") continue; octave_value val; string vt; if (t == "inlineStr") { val = octave_value (all_text (c.child ("is"))); vt = "string"; } else if (t == "s") // shared string index { long idx = c.child ("v").text ().as_llong (-1); val = octave_value ((idx >= 0 && (size_t) idx < shared.size ()) ? shared[idx] : string ()); vt = "string"; } else if (t == "str") // formula string result { val = octave_value (c.child ("v").text ().as_string ()); vt = "string"; } else if (t == "b") // boolean { val = octave_value (c.child ("v").text ().as_int (0) != 0); vt = "boolean"; } else // number (possibly a date/time) { pugi::xml_node v = c.child ("v"); if (! v) continue; // empty cell double num = atof (v.text ().as_string ()); string kind = style_kind (s, xf_fmt, custom_fmt); if (kind == "date") { val = octave_value (serial_to_iso_datetime (num)); vt = "date"; } else if (kind == "time") { val = octave_value (serial_to_pt_duration (num)); vt = "time"; } else { val = octave_value (num); vt = "float"; } } xcell xc; xc.row = r; xc.col = col; xc.val = val; xc.vt = vt; cells.push_back (xc); if (r > maxrow) maxrow = r; if (col > maxcol) maxcol = col; } } Cell data (maxrow, maxcol), vtype (maxrow, maxcol); for (long r = 0; r < maxrow; r++) for (long col = 0; col < maxcol; col++) { data(r, col) = Matrix (0, 0); // blank vtype(r, col) = string (); } for (size_t i = 0; i < cells.size (); i++) { data(cells[i].row - 1, cells[i].col - 1) = cells[i].val; vtype(cells[i].row - 1, cells[i].col - 1) = cells[i].vt; } retval(0) = data; retval(1) = vtype; return retval; } pr0m1th3as-datatypes-9450b29/src/civil.h000066400000000000000000000067371527370255000177610ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the datatypes package for GNU Octave. 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 . */ // Proleptic Gregorian civil arithmetic in 64 bits (Howard Hinnant's algorithms, // the same ones 'date.h' uses). It exists because 'date.h' stores a year in a // 'short': every calendar question that does not also need timezone data is // answered here instead, so that a year outside [-32767, 32767] is carried // rather than truncated. Timezone questions still go through 'date.h', which // is the only place its 16 bits are a real limit -- there is no zone data out // there to look up. Every count is 'int64_t', never 'long', which is only 32 // bits on Windows. #ifndef DATATYPES_CIVIL_H #define DATATYPES_CIVIL_H #include namespace civil { using std::int64_t; // Days from 1970-01-01 to the civil date Y-M-D. M must lie in [1, 12]; D may // be any integer, and one outside its month simply names a day further along // (D = 0 is the last day of the previous month, which is the anchor the // component normalization below is built on). inline int64_t days_from_civil (int64_t y, int64_t m, int64_t d) { y -= (m <= 2); const int64_t era = (y >= 0 ? y : y - 399) / 400; const int64_t yoe = y - era * 400; const int64_t doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; const int64_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; return era * 146097 + doe - 719468; } // The civil date a day count names. Exact inverse of 'days_from_civil' for // every canonical date. inline void civil_from_days (int64_t z, int64_t& y, int64_t& m, int64_t& d) { z += 719468; const int64_t era = (z >= 0 ? z : z - 146096) / 146097; const int64_t doe = z - era * 146097; const int64_t yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; const int64_t doy = doe - (365 * yoe + yoe / 4 - yoe / 100); const int64_t mp = (5 * doy + 2) / 153; d = doy - (153 * mp + 2) / 5 + 1; m = mp + (mp < 10 ? 3 : -9); y = yoe + era * 400 + (m <= 2); } // The day a component triple names, EXTRA_DAYS being the whole days its time // components carry. The folding is the one the class has always applied and // which R2026a agrees with row for row: whole twelvemonths fold into the // year, the remainder is applied as months to day 0 of month 0 -- an anchor // lying in the PREVIOUS month -- and the days are added to that. Month and // day may be anything; only the result has to be a real date. inline int64_t components2days (double Yv, double Mv, double Dv, int64_t extra_days) { const int64_t Y = (int64_t) Yv; const int64_t M = (int64_t) Mv; const int64_t yy = Y + M / 12; const int64_t dmi = M % 12 - 1; const int64_t dy = (dmi - (dmi < 0 ? 11 : 0)) / 12; return days_from_civil (yy + dy, dmi - dy * 12 + 1, 0) + (int64_t) Dv + extra_days; } } #endif pr0m1th3as-datatypes-9450b29/src/date/000077500000000000000000000000001527370255000174025ustar00rootroot00000000000000pr0m1th3as-datatypes-9450b29/src/date/chrono_io.h000066400000000000000000000026731527370255000215420ustar00rootroot00000000000000#ifndef CHRONO_IO_H #define CHRONO_IO_H // The MIT License (MIT) // // Copyright (c) 2016, 2017 Howard Hinnant // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // Our apologies. When the previous paragraph was written, lowercase had not yet // been invented (that would involve another several millennia of evolution). // We did not mean to shout. // This functionality has moved to "date.h" #include "date.h" #endif // CHRONO_IO_H pr0m1th3as-datatypes-9450b29/src/date/date.h000066400000000000000000007303131527370255000204770ustar00rootroot00000000000000#ifndef DATE_H #define DATE_H // The MIT License (MIT) // // Copyright (c) 2015, 2016, 2017 Howard Hinnant // Copyright (c) 2016 Adrian Colomitchi // Copyright (c) 2017 Florian Dang // Copyright (c) 2017 Paul Thompson // Copyright (c) 2018, 2019 Tomasz KamiÅ„ski // Copyright (c) 2019 Jiangang Zhuang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // Our apologies. When the previous paragraph was written, lowercase had not yet // been invented (that would involve another several millennia of evolution). // We did not mean to shout. #ifndef HAS_STRING_VIEW # if __cplusplus >= 201703 || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) # define HAS_STRING_VIEW 1 # else # define HAS_STRING_VIEW 0 # endif #endif // HAS_STRING_VIEW #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAS_STRING_VIEW # include #endif #include #include #ifdef __GNUC__ # pragma GCC diagnostic push # if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ > 7) # pragma GCC diagnostic ignored "-Wpedantic" # endif # if __GNUC__ < 5 // GCC 4.9 Bug 61489 Wrong warning with -Wmissing-field-initializers # pragma GCC diagnostic ignored "-Wmissing-field-initializers" # endif #endif #ifdef _MSC_VER # pragma warning(push) // warning C4127: conditional expression is constant # pragma warning(disable : 4127) #endif namespace date { //---------------+ // Configuration | //---------------+ #ifndef ONLY_C_LOCALE # define ONLY_C_LOCALE 0 #endif #if defined(_MSC_VER) && (!defined(__clang__) || (_MSC_VER < 1910)) // MSVC # ifndef _SILENCE_CXX17_UNCAUGHT_EXCEPTION_DEPRECATION_WARNING # define _SILENCE_CXX17_UNCAUGHT_EXCEPTION_DEPRECATION_WARNING # endif # if _MSC_VER < 1910 // before VS2017 # define CONSTDATA const # define CONSTCD11 # define CONSTCD14 # define NOEXCEPT _NOEXCEPT # else // VS2017 and later # define CONSTDATA constexpr const # define CONSTCD11 constexpr # define CONSTCD14 constexpr # define NOEXCEPT noexcept # endif #elif defined(__SUNPRO_CC) && __SUNPRO_CC <= 0x5150 // Oracle Developer Studio 12.6 and earlier # define CONSTDATA constexpr const # define CONSTCD11 constexpr # define CONSTCD14 # define NOEXCEPT noexcept #elif __cplusplus >= 201402 // C++14 # define CONSTDATA constexpr const # define CONSTCD11 constexpr # define CONSTCD14 constexpr # define NOEXCEPT noexcept #else // C++11 # define CONSTDATA constexpr const # define CONSTCD11 constexpr # define CONSTCD14 # define NOEXCEPT noexcept #endif #ifndef HAS_UNCAUGHT_EXCEPTIONS # if __cplusplus >= 201703 || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) # define HAS_UNCAUGHT_EXCEPTIONS 1 # else # define HAS_UNCAUGHT_EXCEPTIONS 0 # endif #endif // HAS_UNCAUGHT_EXCEPTIONS #ifndef HAS_VOID_T # if __cplusplus >= 201703 || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) # define HAS_VOID_T 1 # else # define HAS_VOID_T 0 # endif #endif // HAS_VOID_T // Protect from Oracle sun macro #ifdef sun # undef sun #endif // Work around for a NVCC compiler bug which causes it to fail // to compile std::ratio_{multiply,divide} when used directly // in the std::chrono::duration template instantiations below namespace detail { template using ratio_multiply = decltype(std::ratio_multiply{}); template using ratio_divide = decltype(std::ratio_divide{}); } // namespace detail //-----------+ // Interface | //-----------+ // durations using days = std::chrono::duration , std::chrono::hours::period>>; using weeks = std::chrono::duration , days::period>>; using years = std::chrono::duration , days::period>>; using months = std::chrono::duration >>; // time_point template using sys_time = std::chrono::time_point; using sys_days = sys_time; using sys_seconds = sys_time; struct local_t {}; template using local_time = std::chrono::time_point; using local_seconds = local_time; using local_days = local_time; // types struct last_spec { explicit last_spec() = default; }; class day; class month; class year; class weekday; class weekday_indexed; class weekday_last; class month_day; class month_day_last; class month_weekday; class month_weekday_last; class year_month; class year_month_day; class year_month_day_last; class year_month_weekday; class year_month_weekday_last; // date composition operators CONSTCD11 year_month operator/(const year& y, const month& m) NOEXCEPT; CONSTCD11 year_month operator/(const year& y, int m) NOEXCEPT; CONSTCD11 month_day operator/(const day& d, const month& m) NOEXCEPT; CONSTCD11 month_day operator/(const day& d, int m) NOEXCEPT; CONSTCD11 month_day operator/(const month& m, const day& d) NOEXCEPT; CONSTCD11 month_day operator/(const month& m, int d) NOEXCEPT; CONSTCD11 month_day operator/(int m, const day& d) NOEXCEPT; CONSTCD11 month_day_last operator/(const month& m, last_spec) NOEXCEPT; CONSTCD11 month_day_last operator/(int m, last_spec) NOEXCEPT; CONSTCD11 month_day_last operator/(last_spec, const month& m) NOEXCEPT; CONSTCD11 month_day_last operator/(last_spec, int m) NOEXCEPT; CONSTCD11 month_weekday operator/(const month& m, const weekday_indexed& wdi) NOEXCEPT; CONSTCD11 month_weekday operator/(int m, const weekday_indexed& wdi) NOEXCEPT; CONSTCD11 month_weekday operator/(const weekday_indexed& wdi, const month& m) NOEXCEPT; CONSTCD11 month_weekday operator/(const weekday_indexed& wdi, int m) NOEXCEPT; CONSTCD11 month_weekday_last operator/(const month& m, const weekday_last& wdl) NOEXCEPT; CONSTCD11 month_weekday_last operator/(int m, const weekday_last& wdl) NOEXCEPT; CONSTCD11 month_weekday_last operator/(const weekday_last& wdl, const month& m) NOEXCEPT; CONSTCD11 month_weekday_last operator/(const weekday_last& wdl, int m) NOEXCEPT; CONSTCD11 year_month_day operator/(const year_month& ym, const day& d) NOEXCEPT; CONSTCD11 year_month_day operator/(const year_month& ym, int d) NOEXCEPT; CONSTCD11 year_month_day operator/(const year& y, const month_day& md) NOEXCEPT; CONSTCD11 year_month_day operator/(int y, const month_day& md) NOEXCEPT; CONSTCD11 year_month_day operator/(const month_day& md, const year& y) NOEXCEPT; CONSTCD11 year_month_day operator/(const month_day& md, int y) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const year_month& ym, last_spec) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const year& y, const month_day_last& mdl) NOEXCEPT; CONSTCD11 year_month_day_last operator/(int y, const month_day_last& mdl) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const month_day_last& mdl, const year& y) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const month_day_last& mdl, int y) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const year_month& ym, const weekday_indexed& wdi) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const year& y, const month_weekday& mwd) NOEXCEPT; CONSTCD11 year_month_weekday operator/(int y, const month_weekday& mwd) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const month_weekday& mwd, const year& y) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const month_weekday& mwd, int y) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const year_month& ym, const weekday_last& wdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const year& y, const month_weekday_last& mwdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(int y, const month_weekday_last& mwdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const month_weekday_last& mwdl, const year& y) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const month_weekday_last& mwdl, int y) NOEXCEPT; // Detailed interface // day class day { unsigned char d_; public: day() = default; explicit CONSTCD11 day(unsigned d) NOEXCEPT; CONSTCD14 day& operator++() NOEXCEPT; CONSTCD14 day operator++(int) NOEXCEPT; CONSTCD14 day& operator--() NOEXCEPT; CONSTCD14 day operator--(int) NOEXCEPT; CONSTCD14 day& operator+=(const days& d) NOEXCEPT; CONSTCD14 day& operator-=(const days& d) NOEXCEPT; CONSTCD11 explicit operator unsigned() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator!=(const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator< (const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator> (const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator<=(const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator>=(const day& x, const day& y) NOEXCEPT; CONSTCD11 day operator+(const day& x, const days& y) NOEXCEPT; CONSTCD11 day operator+(const days& x, const day& y) NOEXCEPT; CONSTCD11 day operator-(const day& x, const days& y) NOEXCEPT; CONSTCD11 days operator-(const day& x, const day& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const day& d); // month class month { unsigned char m_; public: month() = default; explicit CONSTCD11 month(unsigned m) NOEXCEPT; CONSTCD14 month& operator++() NOEXCEPT; CONSTCD14 month operator++(int) NOEXCEPT; CONSTCD14 month& operator--() NOEXCEPT; CONSTCD14 month operator--(int) NOEXCEPT; CONSTCD14 month& operator+=(const months& m) NOEXCEPT; CONSTCD14 month& operator-=(const months& m) NOEXCEPT; CONSTCD11 explicit operator unsigned() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator!=(const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator< (const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator> (const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator<=(const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator>=(const month& x, const month& y) NOEXCEPT; CONSTCD14 month operator+(const month& x, const months& y) NOEXCEPT; CONSTCD14 month operator+(const months& x, const month& y) NOEXCEPT; CONSTCD14 month operator-(const month& x, const months& y) NOEXCEPT; CONSTCD14 months operator-(const month& x, const month& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month& m); // year class year { short y_; public: year() = default; explicit CONSTCD11 year(int y) NOEXCEPT; CONSTCD14 year& operator++() NOEXCEPT; CONSTCD14 year operator++(int) NOEXCEPT; CONSTCD14 year& operator--() NOEXCEPT; CONSTCD14 year operator--(int) NOEXCEPT; CONSTCD14 year& operator+=(const years& y) NOEXCEPT; CONSTCD14 year& operator-=(const years& y) NOEXCEPT; CONSTCD11 year operator-() const NOEXCEPT; CONSTCD11 year operator+() const NOEXCEPT; CONSTCD11 bool is_leap() const NOEXCEPT; CONSTCD11 explicit operator int() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; static CONSTCD11 year min() NOEXCEPT { return year{-32767}; } static CONSTCD11 year max() NOEXCEPT { return year{32767}; } }; CONSTCD11 bool operator==(const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator!=(const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator< (const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator> (const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator<=(const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator>=(const year& x, const year& y) NOEXCEPT; CONSTCD11 year operator+(const year& x, const years& y) NOEXCEPT; CONSTCD11 year operator+(const years& x, const year& y) NOEXCEPT; CONSTCD11 year operator-(const year& x, const years& y) NOEXCEPT; CONSTCD11 years operator-(const year& x, const year& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year& y); // weekday class weekday { unsigned char wd_; public: weekday() = default; explicit CONSTCD11 weekday(unsigned wd) NOEXCEPT; CONSTCD14 weekday(const sys_days& dp) NOEXCEPT; CONSTCD14 explicit weekday(const local_days& dp) NOEXCEPT; CONSTCD14 weekday& operator++() NOEXCEPT; CONSTCD14 weekday operator++(int) NOEXCEPT; CONSTCD14 weekday& operator--() NOEXCEPT; CONSTCD14 weekday operator--(int) NOEXCEPT; CONSTCD14 weekday& operator+=(const days& d) NOEXCEPT; CONSTCD14 weekday& operator-=(const days& d) NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; CONSTCD11 unsigned c_encoding() const NOEXCEPT; CONSTCD11 unsigned iso_encoding() const NOEXCEPT; CONSTCD11 weekday_indexed operator[](unsigned index) const NOEXCEPT; CONSTCD11 weekday_last operator[](last_spec) const NOEXCEPT; private: static CONSTCD14 unsigned char weekday_from_days(int z) NOEXCEPT; friend CONSTCD11 bool operator==(const weekday& x, const weekday& y) NOEXCEPT; friend CONSTCD14 days operator-(const weekday& x, const weekday& y) NOEXCEPT; friend CONSTCD14 weekday operator+(const weekday& x, const days& y) NOEXCEPT; template friend std::basic_ostream& operator<<(std::basic_ostream& os, const weekday& wd); friend class weekday_indexed; }; CONSTCD11 bool operator==(const weekday& x, const weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const weekday& x, const weekday& y) NOEXCEPT; CONSTCD14 weekday operator+(const weekday& x, const days& y) NOEXCEPT; CONSTCD14 weekday operator+(const days& x, const weekday& y) NOEXCEPT; CONSTCD14 weekday operator-(const weekday& x, const days& y) NOEXCEPT; CONSTCD14 days operator-(const weekday& x, const weekday& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const weekday& wd); // weekday_indexed class weekday_indexed { unsigned char wd_ : 4; unsigned char index_ : 4; public: weekday_indexed() = default; CONSTCD11 weekday_indexed(const date::weekday& wd, unsigned index) NOEXCEPT; CONSTCD11 date::weekday weekday() const NOEXCEPT; CONSTCD11 unsigned index() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT; CONSTCD11 bool operator!=(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_indexed& wdi); // weekday_last class weekday_last { date::weekday wd_; public: explicit CONSTCD11 weekday_last(const date::weekday& wd) NOEXCEPT; CONSTCD11 date::weekday weekday() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const weekday_last& x, const weekday_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const weekday_last& x, const weekday_last& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_last& wdl); namespace detail { struct unspecified_month_disambiguator {}; } // namespace detail // year_month class year_month { date::year y_; date::month m_; public: year_month() = default; CONSTCD11 year_month(const date::year& y, const date::month& m) NOEXCEPT; CONSTCD11 date::year year() const NOEXCEPT; CONSTCD11 date::month month() const NOEXCEPT; template CONSTCD14 year_month& operator+=(const months& dm) NOEXCEPT; template CONSTCD14 year_month& operator-=(const months& dm) NOEXCEPT; CONSTCD14 year_month& operator+=(const years& dy) NOEXCEPT; CONSTCD14 year_month& operator-=(const years& dy) NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator< (const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator> (const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_month& x, const year_month& y) NOEXCEPT; template CONSTCD14 year_month operator+(const year_month& ym, const months& dm) NOEXCEPT; template CONSTCD14 year_month operator+(const months& dm, const year_month& ym) NOEXCEPT; template CONSTCD14 year_month operator-(const year_month& ym, const months& dm) NOEXCEPT; CONSTCD11 months operator-(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 year_month operator+(const year_month& ym, const years& dy) NOEXCEPT; CONSTCD11 year_month operator+(const years& dy, const year_month& ym) NOEXCEPT; CONSTCD11 year_month operator-(const year_month& ym, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month& ym); // month_day class month_day { date::month m_; date::day d_; public: month_day() = default; CONSTCD11 month_day(const date::month& m, const date::day& d) NOEXCEPT; CONSTCD11 date::month month() const NOEXCEPT; CONSTCD11 date::day day() const NOEXCEPT; CONSTCD14 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator< (const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator> (const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator<=(const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator>=(const month_day& x, const month_day& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_day& md); // month_day_last class month_day_last { date::month m_; public: CONSTCD11 explicit month_day_last(const date::month& m) NOEXCEPT; CONSTCD11 date::month month() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator< (const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator> (const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator<=(const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator>=(const month_day_last& x, const month_day_last& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_day_last& mdl); // month_weekday class month_weekday { date::month m_; date::weekday_indexed wdi_; public: CONSTCD11 month_weekday(const date::month& m, const date::weekday_indexed& wdi) NOEXCEPT; CONSTCD11 date::month month() const NOEXCEPT; CONSTCD11 date::weekday_indexed weekday_indexed() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_weekday& x, const month_weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_weekday& x, const month_weekday& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday& mwd); // month_weekday_last class month_weekday_last { date::month m_; date::weekday_last wdl_; public: CONSTCD11 month_weekday_last(const date::month& m, const date::weekday_last& wd) NOEXCEPT; CONSTCD11 date::month month() const NOEXCEPT; CONSTCD11 date::weekday_last weekday_last() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday_last& mwdl); // class year_month_day class year_month_day { date::year y_; date::month m_; date::day d_; public: year_month_day() = default; CONSTCD11 year_month_day(const date::year& y, const date::month& m, const date::day& d) NOEXCEPT; CONSTCD14 year_month_day(const year_month_day_last& ymdl) NOEXCEPT; CONSTCD14 year_month_day(sys_days dp) NOEXCEPT; CONSTCD14 explicit year_month_day(local_days dp) NOEXCEPT; template CONSTCD14 year_month_day& operator+=(const months& m) NOEXCEPT; template CONSTCD14 year_month_day& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_day& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_day& operator-=(const years& y) NOEXCEPT; CONSTCD11 date::year year() const NOEXCEPT; CONSTCD11 date::month month() const NOEXCEPT; CONSTCD11 date::day day() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD14 bool ok() const NOEXCEPT; private: static CONSTCD14 year_month_day from_days(days dp) NOEXCEPT; CONSTCD14 days to_days() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator< (const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator> (const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_month_day& x, const year_month_day& y) NOEXCEPT; template CONSTCD14 year_month_day operator+(const year_month_day& ymd, const months& dm) NOEXCEPT; template CONSTCD14 year_month_day operator+(const months& dm, const year_month_day& ymd) NOEXCEPT; template CONSTCD14 year_month_day operator-(const year_month_day& ymd, const months& dm) NOEXCEPT; CONSTCD11 year_month_day operator+(const year_month_day& ymd, const years& dy) NOEXCEPT; CONSTCD11 year_month_day operator+(const years& dy, const year_month_day& ymd) NOEXCEPT; CONSTCD11 year_month_day operator-(const year_month_day& ymd, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day& ymd); // year_month_day_last class year_month_day_last { date::year y_; date::month_day_last mdl_; public: CONSTCD11 year_month_day_last(const date::year& y, const date::month_day_last& mdl) NOEXCEPT; template CONSTCD14 year_month_day_last& operator+=(const months& m) NOEXCEPT; template CONSTCD14 year_month_day_last& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_day_last& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_day_last& operator-=(const years& y) NOEXCEPT; CONSTCD11 date::year year() const NOEXCEPT; CONSTCD11 date::month month() const NOEXCEPT; CONSTCD11 date::month_day_last month_day_last() const NOEXCEPT; CONSTCD14 date::day day() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator< (const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator> (const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; template CONSTCD14 year_month_day_last operator+(const year_month_day_last& ymdl, const months& dm) NOEXCEPT; template CONSTCD14 year_month_day_last operator+(const months& dm, const year_month_day_last& ymdl) NOEXCEPT; CONSTCD11 year_month_day_last operator+(const year_month_day_last& ymdl, const years& dy) NOEXCEPT; CONSTCD11 year_month_day_last operator+(const years& dy, const year_month_day_last& ymdl) NOEXCEPT; template CONSTCD14 year_month_day_last operator-(const year_month_day_last& ymdl, const months& dm) NOEXCEPT; CONSTCD11 year_month_day_last operator-(const year_month_day_last& ymdl, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day_last& ymdl); // year_month_weekday class year_month_weekday { date::year y_; date::month m_; date::weekday_indexed wdi_; public: year_month_weekday() = default; CONSTCD11 year_month_weekday(const date::year& y, const date::month& m, const date::weekday_indexed& wdi) NOEXCEPT; CONSTCD14 year_month_weekday(const sys_days& dp) NOEXCEPT; CONSTCD14 explicit year_month_weekday(const local_days& dp) NOEXCEPT; template CONSTCD14 year_month_weekday& operator+=(const months& m) NOEXCEPT; template CONSTCD14 year_month_weekday& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_weekday& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_weekday& operator-=(const years& y) NOEXCEPT; CONSTCD11 date::year year() const NOEXCEPT; CONSTCD11 date::month month() const NOEXCEPT; CONSTCD11 date::weekday weekday() const NOEXCEPT; CONSTCD11 unsigned index() const NOEXCEPT; CONSTCD11 date::weekday_indexed weekday_indexed() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD14 bool ok() const NOEXCEPT; private: static CONSTCD14 year_month_weekday from_days(days dp) NOEXCEPT; CONSTCD14 days to_days() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT; template CONSTCD14 year_month_weekday operator+(const year_month_weekday& ymwd, const months& dm) NOEXCEPT; template CONSTCD14 year_month_weekday operator+(const months& dm, const year_month_weekday& ymwd) NOEXCEPT; CONSTCD11 year_month_weekday operator+(const year_month_weekday& ymwd, const years& dy) NOEXCEPT; CONSTCD11 year_month_weekday operator+(const years& dy, const year_month_weekday& ymwd) NOEXCEPT; template CONSTCD14 year_month_weekday operator-(const year_month_weekday& ymwd, const months& dm) NOEXCEPT; CONSTCD11 year_month_weekday operator-(const year_month_weekday& ymwd, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday& ymwdi); // year_month_weekday_last class year_month_weekday_last { date::year y_; date::month m_; date::weekday_last wdl_; public: CONSTCD11 year_month_weekday_last(const date::year& y, const date::month& m, const date::weekday_last& wdl) NOEXCEPT; template CONSTCD14 year_month_weekday_last& operator+=(const months& m) NOEXCEPT; template CONSTCD14 year_month_weekday_last& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_weekday_last& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_weekday_last& operator-=(const years& y) NOEXCEPT; CONSTCD11 date::year year() const NOEXCEPT; CONSTCD11 date::month month() const NOEXCEPT; CONSTCD11 date::weekday weekday() const NOEXCEPT; CONSTCD11 date::weekday_last weekday_last() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; private: CONSTCD14 days to_days() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT; template CONSTCD14 year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT; template CONSTCD14 year_month_weekday_last operator+(const months& dm, const year_month_weekday_last& ymwdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT; CONSTCD11 year_month_weekday_last operator+(const years& dy, const year_month_weekday_last& ymwdl) NOEXCEPT; template CONSTCD14 year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT; CONSTCD11 year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday_last& ymwdl); #if !defined(_MSC_VER) || (_MSC_VER >= 1900) inline namespace literals { CONSTCD11 date::day operator ""_d(unsigned long long d) NOEXCEPT; CONSTCD11 date::year operator ""_y(unsigned long long y) NOEXCEPT; } // inline namespace literals #endif // !defined(_MSC_VER) || (_MSC_VER >= 1900) // CONSTDATA date::month January{1}; // CONSTDATA date::month February{2}; // CONSTDATA date::month March{3}; // CONSTDATA date::month April{4}; // CONSTDATA date::month May{5}; // CONSTDATA date::month June{6}; // CONSTDATA date::month July{7}; // CONSTDATA date::month August{8}; // CONSTDATA date::month September{9}; // CONSTDATA date::month October{10}; // CONSTDATA date::month November{11}; // CONSTDATA date::month December{12}; // // CONSTDATA date::weekday Sunday{0u}; // CONSTDATA date::weekday Monday{1u}; // CONSTDATA date::weekday Tuesday{2u}; // CONSTDATA date::weekday Wednesday{3u}; // CONSTDATA date::weekday Thursday{4u}; // CONSTDATA date::weekday Friday{5u}; // CONSTDATA date::weekday Saturday{6u}; #if HAS_VOID_T template > struct is_clock : std::false_type {}; template struct is_clock> : std::true_type {}; template inline constexpr bool is_clock_v = is_clock::value; #endif // HAS_VOID_T //----------------+ // Implementation | //----------------+ // utilities namespace detail { template> class save_istream { protected: std::basic_ios& is_; CharT fill_; std::ios::fmtflags flags_; std::streamsize precision_; std::streamsize width_; std::basic_ostream* tie_; std::locale loc_; public: ~save_istream() { is_.fill(fill_); is_.flags(flags_); is_.precision(precision_); is_.width(width_); is_.imbue(loc_); is_.tie(tie_); } save_istream(const save_istream&) = delete; save_istream& operator=(const save_istream&) = delete; explicit save_istream(std::basic_ios& is) : is_(is) , fill_(is.fill()) , flags_(is.flags()) , precision_(is.precision()) , width_(is.width(0)) , tie_(is.tie(nullptr)) , loc_(is.getloc()) { if (tie_ != nullptr) tie_->flush(); } }; template> class save_ostream : private save_istream { public: ~save_ostream() { if ((this->flags_ & std::ios::unitbuf) && #if HAS_UNCAUGHT_EXCEPTIONS std::uncaught_exceptions() == 0 && #else !std::uncaught_exception() && #endif this->is_.good()) this->is_.rdbuf()->pubsync(); } save_ostream(const save_ostream&) = delete; save_ostream& operator=(const save_ostream&) = delete; explicit save_ostream(std::basic_ios& os) : save_istream(os) { } }; template struct choose_trunc_type { static const int digits = std::numeric_limits::digits; using type = typename std::conditional < digits < 32, std::int32_t, typename std::conditional < digits < 64, std::int64_t, #ifdef __SIZEOF_INT128__ __int128 #else std::int64_t #endif >::type >::type; }; template CONSTCD11 inline typename std::enable_if < !std::chrono::treat_as_floating_point::value, T >::type trunc(T t) NOEXCEPT { return t; } template CONSTCD14 inline typename std::enable_if < std::chrono::treat_as_floating_point::value, T >::type trunc(T t) NOEXCEPT { using std::numeric_limits; using I = typename choose_trunc_type::type; CONSTDATA auto digits = numeric_limits::digits; static_assert(digits < numeric_limits::digits, ""); CONSTDATA auto max = I{1} << (digits-1); CONSTDATA auto min = -max; const auto negative = t < T{0}; if (min <= t && t <= max && t != 0 && t == t) { t = static_cast(static_cast(t)); if (t == 0 && negative) t = -t; } return t; } template struct static_gcd { static const std::intmax_t value = static_gcd::value; }; template struct static_gcd { static const std::intmax_t value = Xp; }; template <> struct static_gcd<0, 0> { static const std::intmax_t value = 1; }; template struct no_overflow { private: static const std::intmax_t gcd_n1_n2 = static_gcd::value; static const std::intmax_t gcd_d1_d2 = static_gcd::value; static const std::intmax_t n1 = R1::num / gcd_n1_n2; static const std::intmax_t d1 = R1::den / gcd_d1_d2; static const std::intmax_t n2 = R2::num / gcd_n1_n2; static const std::intmax_t d2 = R2::den / gcd_d1_d2; #ifdef __cpp_constexpr static const std::intmax_t max = std::numeric_limits::max(); #else static const std::intmax_t max = LLONG_MAX; #endif template struct mul // overflow == false { static const std::intmax_t value = Xp * Yp; }; template struct mul { static const std::intmax_t value = 1; }; public: static const bool value = (n1 <= max / d2) && (n2 <= max / d1); typedef std::ratio::value, mul::value> type; }; } // detail // trunc towards zero template CONSTCD11 inline typename std::enable_if < detail::no_overflow::value, To >::type trunc(const std::chrono::duration& d) { return To{detail::trunc(std::chrono::duration_cast(d).count())}; } template CONSTCD11 inline typename std::enable_if < !detail::no_overflow::value, To >::type trunc(const std::chrono::duration& d) { using std::chrono::duration_cast; using std::chrono::duration; using rep = typename std::common_type::type; return To{detail::trunc(duration_cast(duration_cast>(d)).count())}; } #ifndef HAS_CHRONO_ROUNDING # if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 190023918 || (_MSC_FULL_VER >= 190000000 && defined (__clang__))) # define HAS_CHRONO_ROUNDING 1 # elif defined(__cpp_lib_chrono) && __cplusplus > 201402 && __cpp_lib_chrono >= 201510 # define HAS_CHRONO_ROUNDING 1 # elif defined(_LIBCPP_VERSION) && __cplusplus > 201402 && _LIBCPP_VERSION >= 3800 # define HAS_CHRONO_ROUNDING 1 # else # define HAS_CHRONO_ROUNDING 0 # endif #endif // HAS_CHRONO_ROUNDING #if HAS_CHRONO_ROUNDING == 0 // round down template CONSTCD14 inline typename std::enable_if < detail::no_overflow::value, To >::type floor(const std::chrono::duration& d) { auto t = trunc(d); if (t > d) return t - To{1}; return t; } template CONSTCD14 inline typename std::enable_if < !detail::no_overflow::value, To >::type floor(const std::chrono::duration& d) { using rep = typename std::common_type::type; return floor(floor>(d)); } // round to nearest, to even on tie template CONSTCD14 inline To round(const std::chrono::duration& d) { auto t0 = floor(d); auto t1 = t0 + To{1}; if (t1 == To{0} && t0 < To{0}) t1 = -t1; auto diff0 = d - t0; auto diff1 = t1 - d; if (diff0 == diff1) { if (t0 - trunc(t0/2)*2 == To{0}) return t0; return t1; } if (diff0 < diff1) return t0; return t1; } // round up template CONSTCD14 inline To ceil(const std::chrono::duration& d) { auto t = trunc(d); if (t < d) return t + To{1}; return t; } template ::is_signed >::type> CONSTCD11 std::chrono::duration abs(std::chrono::duration d) { return d >= d.zero() ? d : static_cast(-d); } // round down template CONSTCD11 inline std::chrono::time_point floor(const std::chrono::time_point& tp) { using std::chrono::time_point; return time_point{date::floor(tp.time_since_epoch())}; } // round to nearest, to even on tie template CONSTCD11 inline std::chrono::time_point round(const std::chrono::time_point& tp) { using std::chrono::time_point; return time_point{round(tp.time_since_epoch())}; } // round up template CONSTCD11 inline std::chrono::time_point ceil(const std::chrono::time_point& tp) { using std::chrono::time_point; return time_point{ceil(tp.time_since_epoch())}; } #else // HAS_CHRONO_ROUNDING == 1 using std::chrono::floor; using std::chrono::ceil; using std::chrono::round; using std::chrono::abs; #endif // HAS_CHRONO_ROUNDING namespace detail { template CONSTCD14 inline typename std::enable_if < !std::chrono::treat_as_floating_point::value, To >::type round_i(const std::chrono::duration& d) { return round(d); } template CONSTCD14 inline typename std::enable_if < std::chrono::treat_as_floating_point::value, To >::type round_i(const std::chrono::duration& d) { return d; } template CONSTCD11 inline std::chrono::time_point round_i(const std::chrono::time_point& tp) { using std::chrono::time_point; return time_point{round_i(tp.time_since_epoch())}; } } // detail // trunc towards zero template CONSTCD11 inline std::chrono::time_point trunc(const std::chrono::time_point& tp) { using std::chrono::time_point; return time_point{trunc(tp.time_since_epoch())}; } // day CONSTCD11 inline day::day(unsigned d) NOEXCEPT : d_(static_cast(d)) {} CONSTCD14 inline day& day::operator++() NOEXCEPT {++d_; return *this;} CONSTCD14 inline day day::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline day& day::operator--() NOEXCEPT {--d_; return *this;} CONSTCD14 inline day day::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline day& day::operator+=(const days& d) NOEXCEPT {*this = *this + d; return *this;} CONSTCD14 inline day& day::operator-=(const days& d) NOEXCEPT {*this = *this - d; return *this;} CONSTCD11 inline day::operator unsigned() const NOEXCEPT {return d_;} CONSTCD11 inline bool day::ok() const NOEXCEPT {return 1 <= d_ && d_ <= 31;} CONSTCD11 inline bool operator==(const day& x, const day& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const day& x, const day& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const day& x, const day& y) NOEXCEPT { return static_cast(x) < static_cast(y); } CONSTCD11 inline bool operator>(const day& x, const day& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const day& x, const day& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const day& x, const day& y) NOEXCEPT { return !(x < y); } CONSTCD11 inline days operator-(const day& x, const day& y) NOEXCEPT { return days{static_cast(static_cast(x) - static_cast(y))}; } CONSTCD11 inline day operator+(const day& x, const days& y) NOEXCEPT { return day{static_cast(x) + static_cast(y.count())}; } CONSTCD11 inline day operator+(const days& x, const day& y) NOEXCEPT { return y + x; } CONSTCD11 inline day operator-(const day& x, const days& y) NOEXCEPT { return x + -y; } namespace detail { template std::basic_ostream& low_level_fmt(std::basic_ostream& os, const day& d) { detail::save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::right); os.width(2); os << static_cast(d); return os; } } // namespace detail template inline std::basic_ostream& operator<<(std::basic_ostream& os, const day& d) { detail::low_level_fmt(os, d); if (!d.ok()) os << " is not a valid day"; return os; } // month CONSTCD11 inline month::month(unsigned m) NOEXCEPT : m_(static_cast(m)) {} CONSTCD14 inline month& month::operator++() NOEXCEPT {*this += months{1}; return *this;} CONSTCD14 inline month month::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline month& month::operator--() NOEXCEPT {*this -= months{1}; return *this;} CONSTCD14 inline month month::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline month& month::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline month& month::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD11 inline month::operator unsigned() const NOEXCEPT {return m_;} CONSTCD11 inline bool month::ok() const NOEXCEPT {return 1 <= m_ && m_ <= 12;} CONSTCD11 inline bool operator==(const month& x, const month& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const month& x, const month& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const month& x, const month& y) NOEXCEPT { return static_cast(x) < static_cast(y); } CONSTCD11 inline bool operator>(const month& x, const month& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const month& x, const month& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const month& x, const month& y) NOEXCEPT { return !(x < y); } CONSTCD14 inline months operator-(const month& x, const month& y) NOEXCEPT { auto const d = static_cast(x) - static_cast(y); return months(d <= 11 ? d : d + 12); } CONSTCD14 inline month operator+(const month& x, const months& y) NOEXCEPT { auto const mu = static_cast(static_cast(x)) + y.count() - 1; auto const yr = (mu >= 0 ? mu : mu-11) / 12; return month{static_cast(mu - yr * 12 + 1)}; } CONSTCD14 inline month operator+(const months& x, const month& y) NOEXCEPT { return y + x; } CONSTCD14 inline month operator-(const month& x, const months& y) NOEXCEPT { return x + -y; } namespace detail { template std::basic_ostream& low_level_fmt(std::basic_ostream& os, const month& m) { if (m.ok()) { CharT fmt[] = {'%', 'b', 0}; os << format(os.getloc(), fmt, m); } else os << static_cast(m); return os; } } // namespace detail template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month& m) { detail::low_level_fmt(os, m); if (!m.ok()) os << " is not a valid month"; return os; } // year CONSTCD11 inline year::year(int y) NOEXCEPT : y_(static_cast(y)) {} CONSTCD14 inline year& year::operator++() NOEXCEPT {++y_; return *this;} CONSTCD14 inline year year::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline year& year::operator--() NOEXCEPT {--y_; return *this;} CONSTCD14 inline year year::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline year& year::operator+=(const years& y) NOEXCEPT {*this = *this + y; return *this;} CONSTCD14 inline year& year::operator-=(const years& y) NOEXCEPT {*this = *this - y; return *this;} CONSTCD11 inline year year::operator-() const NOEXCEPT {return year{-y_};} CONSTCD11 inline year year::operator+() const NOEXCEPT {return *this;} CONSTCD11 inline bool year::is_leap() const NOEXCEPT { return y_ % 4 == 0 && (y_ % 100 != 0 || y_ % 400 == 0); } CONSTCD11 inline year::operator int() const NOEXCEPT {return y_;} CONSTCD11 inline bool year::ok() const NOEXCEPT { return y_ != std::numeric_limits::min(); } CONSTCD11 inline bool operator==(const year& x, const year& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const year& x, const year& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year& x, const year& y) NOEXCEPT { return static_cast(x) < static_cast(y); } CONSTCD11 inline bool operator>(const year& x, const year& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year& x, const year& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year& x, const year& y) NOEXCEPT { return !(x < y); } CONSTCD11 inline years operator-(const year& x, const year& y) NOEXCEPT { return years{static_cast(x) - static_cast(y)}; } CONSTCD11 inline year operator+(const year& x, const years& y) NOEXCEPT { return year{static_cast(x) + y.count()}; } CONSTCD11 inline year operator+(const years& x, const year& y) NOEXCEPT { return y + x; } CONSTCD11 inline year operator-(const year& x, const years& y) NOEXCEPT { return year{static_cast(x) - y.count()}; } namespace detail { template std::basic_ostream& low_level_fmt(std::basic_ostream& os, const year& y) { detail::save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::internal); os.width(4 + (y < year{0})); os.imbue(std::locale::classic()); os << static_cast(y); return os; } } // namespace detail template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year& y) { detail::low_level_fmt(os, y); if (!y.ok()) os << " is not a valid year"; return os; } // weekday CONSTCD14 inline unsigned char weekday::weekday_from_days(int z) NOEXCEPT { auto u = static_cast(z); return static_cast(z >= -4 ? (u+4) % 7 : u % 7); } CONSTCD11 inline weekday::weekday(unsigned wd) NOEXCEPT : wd_(static_cast(wd != 7 ? wd : 0)) {} CONSTCD14 inline weekday::weekday(const sys_days& dp) NOEXCEPT : wd_(weekday_from_days(dp.time_since_epoch().count())) {} CONSTCD14 inline weekday::weekday(const local_days& dp) NOEXCEPT : wd_(weekday_from_days(dp.time_since_epoch().count())) {} CONSTCD14 inline weekday& weekday::operator++() NOEXCEPT {*this += days{1}; return *this;} CONSTCD14 inline weekday weekday::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline weekday& weekday::operator--() NOEXCEPT {*this -= days{1}; return *this;} CONSTCD14 inline weekday weekday::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline weekday& weekday::operator+=(const days& d) NOEXCEPT { *this = *this + d; return *this; } CONSTCD14 inline weekday& weekday::operator-=(const days& d) NOEXCEPT { *this = *this - d; return *this; } CONSTCD11 inline bool weekday::ok() const NOEXCEPT {return wd_ <= 6;} CONSTCD11 inline unsigned weekday::c_encoding() const NOEXCEPT { return unsigned{wd_}; } CONSTCD11 inline unsigned weekday::iso_encoding() const NOEXCEPT { return unsigned{((wd_ == 0u) ? 7u : wd_)}; } CONSTCD11 inline bool operator==(const weekday& x, const weekday& y) NOEXCEPT { return x.wd_ == y.wd_; } CONSTCD11 inline bool operator!=(const weekday& x, const weekday& y) NOEXCEPT { return !(x == y); } CONSTCD14 inline days operator-(const weekday& x, const weekday& y) NOEXCEPT { auto const wdu = x.wd_ - y.wd_; auto const wk = (wdu >= 0 ? wdu : wdu-6) / 7; return days{wdu - wk * 7}; } CONSTCD14 inline weekday operator+(const weekday& x, const days& y) NOEXCEPT { auto const wdu = static_cast(static_cast(x.wd_)) + y.count(); auto const wk = (wdu >= 0 ? wdu : wdu-6) / 7; return weekday{static_cast(wdu - wk * 7)}; } CONSTCD14 inline weekday operator+(const days& x, const weekday& y) NOEXCEPT { return y + x; } CONSTCD14 inline weekday operator-(const weekday& x, const days& y) NOEXCEPT { return x + -y; } namespace detail { template std::basic_ostream& low_level_fmt(std::basic_ostream& os, const weekday& wd) { if (wd.ok()) { CharT fmt[] = {'%', 'a', 0}; os << format(fmt, wd); } else os << wd.c_encoding(); return os; } } // namespace detail template inline std::basic_ostream& operator<<(std::basic_ostream& os, const weekday& wd) { detail::low_level_fmt(os, wd); if (!wd.ok()) os << " is not a valid weekday"; return os; } #if !defined(_MSC_VER) || (_MSC_VER >= 1900) inline namespace literals { CONSTCD11 inline date::day operator ""_d(unsigned long long d) NOEXCEPT { return date::day{static_cast(d)}; } CONSTCD11 inline date::year operator ""_y(unsigned long long y) NOEXCEPT { return date::year(static_cast(y)); } #endif // !defined(_MSC_VER) || (_MSC_VER >= 1900) CONSTDATA date::last_spec last{}; CONSTDATA date::month jan{1}; CONSTDATA date::month feb{2}; CONSTDATA date::month mar{3}; CONSTDATA date::month apr{4}; CONSTDATA date::month may{5}; CONSTDATA date::month jun{6}; CONSTDATA date::month jul{7}; CONSTDATA date::month aug{8}; CONSTDATA date::month sep{9}; CONSTDATA date::month oct{10}; CONSTDATA date::month nov{11}; CONSTDATA date::month dec{12}; CONSTDATA date::weekday sun{0u}; CONSTDATA date::weekday mon{1u}; CONSTDATA date::weekday tue{2u}; CONSTDATA date::weekday wed{3u}; CONSTDATA date::weekday thu{4u}; CONSTDATA date::weekday fri{5u}; CONSTDATA date::weekday sat{6u}; #if !defined(_MSC_VER) || (_MSC_VER >= 1900) } // inline namespace literals #endif CONSTDATA date::month January{1}; CONSTDATA date::month February{2}; CONSTDATA date::month March{3}; CONSTDATA date::month April{4}; CONSTDATA date::month May{5}; CONSTDATA date::month June{6}; CONSTDATA date::month July{7}; CONSTDATA date::month August{8}; CONSTDATA date::month September{9}; CONSTDATA date::month October{10}; CONSTDATA date::month November{11}; CONSTDATA date::month December{12}; CONSTDATA date::weekday Monday{1}; CONSTDATA date::weekday Tuesday{2}; CONSTDATA date::weekday Wednesday{3}; CONSTDATA date::weekday Thursday{4}; CONSTDATA date::weekday Friday{5}; CONSTDATA date::weekday Saturday{6}; CONSTDATA date::weekday Sunday{7}; // weekday_indexed CONSTCD11 inline weekday weekday_indexed::weekday() const NOEXCEPT { return date::weekday{static_cast(wd_)}; } CONSTCD11 inline unsigned weekday_indexed::index() const NOEXCEPT {return index_;} CONSTCD11 inline bool weekday_indexed::ok() const NOEXCEPT { return weekday().ok() && 1 <= index_ && index_ <= 5; } #ifdef __GNUC__ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wconversion" #endif // __GNUC__ CONSTCD11 inline weekday_indexed::weekday_indexed(const date::weekday& wd, unsigned index) NOEXCEPT : wd_(static_cast(static_cast(wd.wd_))) , index_(static_cast(index)) {} #ifdef __GNUC__ # pragma GCC diagnostic pop #endif // __GNUC__ namespace detail { template std::basic_ostream& low_level_fmt(std::basic_ostream& os, const weekday_indexed& wdi) { return low_level_fmt(os, wdi.weekday()) << '[' << wdi.index() << ']'; } } // namespace detail template inline std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_indexed& wdi) { detail::low_level_fmt(os, wdi); if (!wdi.ok()) os << " is not a valid weekday_indexed"; return os; } CONSTCD11 inline weekday_indexed weekday::operator[](unsigned index) const NOEXCEPT { return {*this, index}; } CONSTCD11 inline bool operator==(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT { return x.weekday() == y.weekday() && x.index() == y.index(); } CONSTCD11 inline bool operator!=(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT { return !(x == y); } // weekday_last CONSTCD11 inline date::weekday weekday_last::weekday() const NOEXCEPT {return wd_;} CONSTCD11 inline bool weekday_last::ok() const NOEXCEPT {return wd_.ok();} CONSTCD11 inline weekday_last::weekday_last(const date::weekday& wd) NOEXCEPT : wd_(wd) {} CONSTCD11 inline bool operator==(const weekday_last& x, const weekday_last& y) NOEXCEPT { return x.weekday() == y.weekday(); } CONSTCD11 inline bool operator!=(const weekday_last& x, const weekday_last& y) NOEXCEPT { return !(x == y); } namespace detail { template std::basic_ostream& low_level_fmt(std::basic_ostream& os, const weekday_last& wdl) { return low_level_fmt(os, wdl.weekday()) << "[last]"; } } // namespace detail template inline std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_last& wdl) { detail::low_level_fmt(os, wdl); if (!wdl.ok()) os << " is not a valid weekday_last"; return os; } CONSTCD11 inline weekday_last weekday::operator[](last_spec) const NOEXCEPT { return weekday_last{*this}; } // year_month CONSTCD11 inline year_month::year_month(const date::year& y, const date::month& m) NOEXCEPT : y_(y) , m_(m) {} CONSTCD11 inline year year_month::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month::month() const NOEXCEPT {return m_;} CONSTCD11 inline bool year_month::ok() const NOEXCEPT {return y_.ok() && m_.ok();} template CONSTCD14 inline year_month& year_month::operator+=(const months& dm) NOEXCEPT { *this = *this + dm; return *this; } template CONSTCD14 inline year_month& year_month::operator-=(const months& dm) NOEXCEPT { *this = *this - dm; return *this; } CONSTCD14 inline year_month& year_month::operator+=(const years& dy) NOEXCEPT { *this = *this + dy; return *this; } CONSTCD14 inline year_month& year_month::operator-=(const years& dy) NOEXCEPT { *this = *this - dy; return *this; } CONSTCD11 inline bool operator==(const year_month& x, const year_month& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month(); } CONSTCD11 inline bool operator!=(const year_month& x, const year_month& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_month& x, const year_month& y) NOEXCEPT { return x.year() < y.year() ? true : (x.year() > y.year() ? false : (x.month() < y.month())); } CONSTCD11 inline bool operator>(const year_month& x, const year_month& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_month& x, const year_month& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_month& x, const year_month& y) NOEXCEPT { return !(x < y); } template CONSTCD14 inline year_month operator+(const year_month& ym, const months& dm) NOEXCEPT { auto dmi = static_cast(static_cast(ym.month())) - 1 + dm.count(); auto dy = (dmi >= 0 ? dmi : dmi-11) / 12; dmi = dmi - dy * 12 + 1; return (ym.year() + years(dy)) / month(static_cast(dmi)); } template CONSTCD14 inline year_month operator+(const months& dm, const year_month& ym) NOEXCEPT { return ym + dm; } template CONSTCD14 inline year_month operator-(const year_month& ym, const months& dm) NOEXCEPT { return ym + -dm; } CONSTCD11 inline months operator-(const year_month& x, const year_month& y) NOEXCEPT { return (x.year() - y.year()) + months(static_cast(x.month()) - static_cast(y.month())); } CONSTCD11 inline year_month operator+(const year_month& ym, const years& dy) NOEXCEPT { return (ym.year() + dy) / ym.month(); } CONSTCD11 inline year_month operator+(const years& dy, const year_month& ym) NOEXCEPT { return ym + dy; } CONSTCD11 inline year_month operator-(const year_month& ym, const years& dy) NOEXCEPT { return ym + -dy; } namespace detail { template std::basic_ostream& low_level_fmt(std::basic_ostream& os, const year_month& ym) { low_level_fmt(os, ym.year()) << '/'; return low_level_fmt(os, ym.month()); } } // namespace detail template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month& ym) { detail::low_level_fmt(os, ym); if (!ym.ok()) os << " is not a valid year_month"; return os; } // month_day CONSTCD11 inline month_day::month_day(const date::month& m, const date::day& d) NOEXCEPT : m_(m) , d_(d) {} CONSTCD11 inline date::month month_day::month() const NOEXCEPT {return m_;} CONSTCD11 inline date::day month_day::day() const NOEXCEPT {return d_;} CONSTCD14 inline bool month_day::ok() const NOEXCEPT { CONSTDATA date::day d[] = { date::day(31), date::day(29), date::day(31), date::day(30), date::day(31), date::day(30), date::day(31), date::day(31), date::day(30), date::day(31), date::day(30), date::day(31) }; return m_.ok() && date::day{1} <= d_ && d_ <= d[static_cast(m_)-1]; } CONSTCD11 inline bool operator==(const month_day& x, const month_day& y) NOEXCEPT { return x.month() == y.month() && x.day() == y.day(); } CONSTCD11 inline bool operator!=(const month_day& x, const month_day& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const month_day& x, const month_day& y) NOEXCEPT { return x.month() < y.month() ? true : (x.month() > y.month() ? false : (x.day() < y.day())); } CONSTCD11 inline bool operator>(const month_day& x, const month_day& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const month_day& x, const month_day& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const month_day& x, const month_day& y) NOEXCEPT { return !(x < y); } namespace detail { template std::basic_ostream& low_level_fmt(std::basic_ostream& os, const month_day& md) { low_level_fmt(os, md.month()) << '/'; return low_level_fmt(os, md.day()); } } // namespace detail template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_day& md) { detail::low_level_fmt(os, md); if (!md.ok()) os << " is not a valid month_day"; return os; } // month_day_last CONSTCD11 inline month month_day_last::month() const NOEXCEPT {return m_;} CONSTCD11 inline bool month_day_last::ok() const NOEXCEPT {return m_.ok();} CONSTCD11 inline month_day_last::month_day_last(const date::month& m) NOEXCEPT : m_(m) {} CONSTCD11 inline bool operator==(const month_day_last& x, const month_day_last& y) NOEXCEPT { return x.month() == y.month(); } CONSTCD11 inline bool operator!=(const month_day_last& x, const month_day_last& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const month_day_last& x, const month_day_last& y) NOEXCEPT { return x.month() < y.month(); } CONSTCD11 inline bool operator>(const month_day_last& x, const month_day_last& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const month_day_last& x, const month_day_last& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const month_day_last& x, const month_day_last& y) NOEXCEPT { return !(x < y); } namespace detail { template std::basic_ostream& low_level_fmt(std::basic_ostream& os, const month_day_last& mdl) { return low_level_fmt(os, mdl.month()) << "/last"; } } // namespace detail template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_day_last& mdl) { detail::low_level_fmt(os, mdl); if (!mdl.ok()) os << " is not a valid month_day_last"; return os; } // month_weekday CONSTCD11 inline month_weekday::month_weekday(const date::month& m, const date::weekday_indexed& wdi) NOEXCEPT : m_(m) , wdi_(wdi) {} CONSTCD11 inline month month_weekday::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday_indexed month_weekday::weekday_indexed() const NOEXCEPT { return wdi_; } CONSTCD11 inline bool month_weekday::ok() const NOEXCEPT { return m_.ok() && wdi_.ok(); } CONSTCD11 inline bool operator==(const month_weekday& x, const month_weekday& y) NOEXCEPT { return x.month() == y.month() && x.weekday_indexed() == y.weekday_indexed(); } CONSTCD11 inline bool operator!=(const month_weekday& x, const month_weekday& y) NOEXCEPT { return !(x == y); } namespace detail { template std::basic_ostream& low_level_fmt(std::basic_ostream& os, const month_weekday& mwd) { low_level_fmt(os, mwd.month()) << '/'; return low_level_fmt(os, mwd.weekday_indexed()); } } // namespace detail template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday& mwd) { detail::low_level_fmt(os, mwd); if (!mwd.ok()) os << " is not a valid month_weekday"; return os; } // month_weekday_last CONSTCD11 inline month_weekday_last::month_weekday_last(const date::month& m, const date::weekday_last& wdl) NOEXCEPT : m_(m) , wdl_(wdl) {} CONSTCD11 inline month month_weekday_last::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday_last month_weekday_last::weekday_last() const NOEXCEPT { return wdl_; } CONSTCD11 inline bool month_weekday_last::ok() const NOEXCEPT { return m_.ok() && wdl_.ok(); } CONSTCD11 inline bool operator==(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT { return x.month() == y.month() && x.weekday_last() == y.weekday_last(); } CONSTCD11 inline bool operator!=(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT { return !(x == y); } namespace detail { template std::basic_ostream& low_level_fmt(std::basic_ostream& os, const month_weekday_last& mwdl) { low_level_fmt(os, mwdl.month()) << '/'; return low_level_fmt(os, mwdl.weekday_last()); } } // namespace detail template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday_last& mwdl) { detail::low_level_fmt(os, mwdl); if (!mwdl.ok()) os << " is not a valid month_weekday_last"; return os; } // year_month_day_last CONSTCD11 inline year_month_day_last::year_month_day_last(const date::year& y, const date::month_day_last& mdl) NOEXCEPT : y_(y) , mdl_(mdl) {} template CONSTCD14 inline year_month_day_last& year_month_day_last::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } template CONSTCD14 inline year_month_day_last& year_month_day_last::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_day_last& year_month_day_last::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_day_last& year_month_day_last::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD11 inline year year_month_day_last::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_day_last::month() const NOEXCEPT {return mdl_.month();} CONSTCD11 inline month_day_last year_month_day_last::month_day_last() const NOEXCEPT { return mdl_; } CONSTCD14 inline day year_month_day_last::day() const NOEXCEPT { CONSTDATA date::day d[] = { date::day(31), date::day(28), date::day(31), date::day(30), date::day(31), date::day(30), date::day(31), date::day(31), date::day(30), date::day(31), date::day(30), date::day(31) }; return (month() != February || !y_.is_leap()) && mdl_.ok() ? d[static_cast(month()) - 1] : date::day{29}; } CONSTCD14 inline year_month_day_last::operator sys_days() const NOEXCEPT { return sys_days(year()/month()/day()); } CONSTCD14 inline year_month_day_last::operator local_days() const NOEXCEPT { return local_days(year()/month()/day()); } CONSTCD11 inline bool year_month_day_last::ok() const NOEXCEPT { return y_.ok() && mdl_.ok(); } CONSTCD11 inline bool operator==(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return x.year() == y.year() && x.month_day_last() == y.month_day_last(); } CONSTCD11 inline bool operator!=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return x.year() < y.year() ? true : (x.year() > y.year() ? false : (x.month_day_last() < y.month_day_last())); } CONSTCD11 inline bool operator>(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return !(x < y); } namespace detail { template std::basic_ostream& low_level_fmt(std::basic_ostream& os, const year_month_day_last& ymdl) { low_level_fmt(os, ymdl.year()) << '/'; return low_level_fmt(os, ymdl.month_day_last()); } } // namespace detail template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day_last& ymdl) { detail::low_level_fmt(os, ymdl); if (!ymdl.ok()) os << " is not a valid year_month_day_last"; return os; } template CONSTCD14 inline year_month_day_last operator+(const year_month_day_last& ymdl, const months& dm) NOEXCEPT { return (ymdl.year() / ymdl.month() + dm) / last; } template CONSTCD14 inline year_month_day_last operator+(const months& dm, const year_month_day_last& ymdl) NOEXCEPT { return ymdl + dm; } template CONSTCD14 inline year_month_day_last operator-(const year_month_day_last& ymdl, const months& dm) NOEXCEPT { return ymdl + (-dm); } CONSTCD11 inline year_month_day_last operator+(const year_month_day_last& ymdl, const years& dy) NOEXCEPT { return {ymdl.year()+dy, ymdl.month_day_last()}; } CONSTCD11 inline year_month_day_last operator+(const years& dy, const year_month_day_last& ymdl) NOEXCEPT { return ymdl + dy; } CONSTCD11 inline year_month_day_last operator-(const year_month_day_last& ymdl, const years& dy) NOEXCEPT { return ymdl + (-dy); } // year_month_day CONSTCD11 inline year_month_day::year_month_day(const date::year& y, const date::month& m, const date::day& d) NOEXCEPT : y_(y) , m_(m) , d_(d) {} CONSTCD14 inline year_month_day::year_month_day(const year_month_day_last& ymdl) NOEXCEPT : y_(ymdl.year()) , m_(ymdl.month()) , d_(ymdl.day()) {} CONSTCD14 inline year_month_day::year_month_day(sys_days dp) NOEXCEPT : year_month_day(from_days(dp.time_since_epoch())) {} CONSTCD14 inline year_month_day::year_month_day(local_days dp) NOEXCEPT : year_month_day(from_days(dp.time_since_epoch())) {} CONSTCD11 inline year year_month_day::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_day::month() const NOEXCEPT {return m_;} CONSTCD11 inline day year_month_day::day() const NOEXCEPT {return d_;} template CONSTCD14 inline year_month_day& year_month_day::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } template CONSTCD14 inline year_month_day& year_month_day::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_day& year_month_day::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_day& year_month_day::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD14 inline days year_month_day::to_days() const NOEXCEPT { static_assert(std::numeric_limits::digits >= 18, "This algorithm has not been ported to a 16 bit unsigned integer"); static_assert(std::numeric_limits::digits >= 20, "This algorithm has not been ported to a 16 bit signed integer"); auto const y = static_cast(y_) - (m_ <= February); auto const m = static_cast(m_); auto const d = static_cast(d_); auto const era = (y >= 0 ? y : y-399) / 400; auto const yoe = static_cast(y - era * 400); // [0, 399] auto const doy = (153*(m > 2 ? m-3 : m+9) + 2)/5 + d-1; // [0, 365] auto const doe = yoe * 365 + yoe/4 - yoe/100 + doy; // [0, 146096] return days{era * 146097 + static_cast(doe) - 719468}; } CONSTCD14 inline year_month_day::operator sys_days() const NOEXCEPT { return sys_days{to_days()}; } CONSTCD14 inline year_month_day::operator local_days() const NOEXCEPT { return local_days{to_days()}; } CONSTCD14 inline bool year_month_day::ok() const NOEXCEPT { if (!(y_.ok() && m_.ok())) return false; return date::day{1} <= d_ && d_ <= (y_ / m_ / last).day(); } CONSTCD11 inline bool operator==(const year_month_day& x, const year_month_day& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month() && x.day() == y.day(); } CONSTCD11 inline bool operator!=(const year_month_day& x, const year_month_day& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_month_day& x, const year_month_day& y) NOEXCEPT { return x.year() < y.year() ? true : (x.year() > y.year() ? false : (x.month() < y.month() ? true : (x.month() > y.month() ? false : (x.day() < y.day())))); } CONSTCD11 inline bool operator>(const year_month_day& x, const year_month_day& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_month_day& x, const year_month_day& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_month_day& x, const year_month_day& y) NOEXCEPT { return !(x < y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day& ymd) { detail::save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::right); os.imbue(std::locale::classic()); os << static_cast(ymd.year()) << '-'; os.width(2); os << static_cast(ymd.month()) << '-'; os.width(2); os << static_cast(ymd.day()); if (!ymd.ok()) os << " is not a valid year_month_day"; return os; } CONSTCD14 inline year_month_day year_month_day::from_days(days dp) NOEXCEPT { static_assert(std::numeric_limits::digits >= 18, "This algorithm has not been ported to a 16 bit unsigned integer"); static_assert(std::numeric_limits::digits >= 20, "This algorithm has not been ported to a 16 bit signed integer"); auto const z = dp.count() + 719468; auto const era = (z >= 0 ? z : z - 146096) / 146097; auto const doe = static_cast(z - era * 146097); // [0, 146096] auto const yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365; // [0, 399] auto const y = static_cast(yoe) + era * 400; auto const doy = doe - (365*yoe + yoe/4 - yoe/100); // [0, 365] auto const mp = (5*doy + 2)/153; // [0, 11] auto const d = doy - (153*mp+2)/5 + 1; // [1, 31] auto const m = mp < 10 ? mp+3 : mp-9; // [1, 12] return year_month_day{date::year{y + (m <= 2)}, date::month(m), date::day(d)}; } template CONSTCD14 inline year_month_day operator+(const year_month_day& ymd, const months& dm) NOEXCEPT { return (ymd.year() / ymd.month() + dm) / ymd.day(); } template CONSTCD14 inline year_month_day operator+(const months& dm, const year_month_day& ymd) NOEXCEPT { return ymd + dm; } template CONSTCD14 inline year_month_day operator-(const year_month_day& ymd, const months& dm) NOEXCEPT { return ymd + (-dm); } CONSTCD11 inline year_month_day operator+(const year_month_day& ymd, const years& dy) NOEXCEPT { return (ymd.year() + dy) / ymd.month() / ymd.day(); } CONSTCD11 inline year_month_day operator+(const years& dy, const year_month_day& ymd) NOEXCEPT { return ymd + dy; } CONSTCD11 inline year_month_day operator-(const year_month_day& ymd, const years& dy) NOEXCEPT { return ymd + (-dy); } // year_month_weekday CONSTCD11 inline year_month_weekday::year_month_weekday(const date::year& y, const date::month& m, const date::weekday_indexed& wdi) NOEXCEPT : y_(y) , m_(m) , wdi_(wdi) {} CONSTCD14 inline year_month_weekday::year_month_weekday(const sys_days& dp) NOEXCEPT : year_month_weekday(from_days(dp.time_since_epoch())) {} CONSTCD14 inline year_month_weekday::year_month_weekday(const local_days& dp) NOEXCEPT : year_month_weekday(from_days(dp.time_since_epoch())) {} template CONSTCD14 inline year_month_weekday& year_month_weekday::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } template CONSTCD14 inline year_month_weekday& year_month_weekday::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_weekday& year_month_weekday::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_weekday& year_month_weekday::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD11 inline year year_month_weekday::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_weekday::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday year_month_weekday::weekday() const NOEXCEPT { return wdi_.weekday(); } CONSTCD11 inline unsigned year_month_weekday::index() const NOEXCEPT { return wdi_.index(); } CONSTCD11 inline weekday_indexed year_month_weekday::weekday_indexed() const NOEXCEPT { return wdi_; } CONSTCD14 inline year_month_weekday::operator sys_days() const NOEXCEPT { return sys_days{to_days()}; } CONSTCD14 inline year_month_weekday::operator local_days() const NOEXCEPT { return local_days{to_days()}; } CONSTCD14 inline bool year_month_weekday::ok() const NOEXCEPT { if (!y_.ok() || !m_.ok() || !wdi_.weekday().ok() || wdi_.index() < 1) return false; if (wdi_.index() <= 4) return true; auto d2 = wdi_.weekday() - date::weekday(static_cast(y_/m_/1)) + days((wdi_.index()-1)*7 + 1); return static_cast(d2.count()) <= static_cast((y_/m_/last).day()); } CONSTCD14 inline year_month_weekday year_month_weekday::from_days(days d) NOEXCEPT { sys_days dp{d}; auto const wd = date::weekday(dp); auto const ymd = year_month_day(dp); return {ymd.year(), ymd.month(), wd[(static_cast(ymd.day())-1)/7+1]}; } CONSTCD14 inline days year_month_weekday::to_days() const NOEXCEPT { auto d = sys_days(y_/m_/1); return (d + (wdi_.weekday() - date::weekday(d) + days{(wdi_.index()-1)*7}) ).time_since_epoch(); } CONSTCD11 inline bool operator==(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month() && x.weekday_indexed() == y.weekday_indexed(); } CONSTCD11 inline bool operator!=(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday& ymwdi) { detail::low_level_fmt(os, ymwdi.year()) << '/'; detail::low_level_fmt(os, ymwdi.month()) << '/'; detail::low_level_fmt(os, ymwdi.weekday_indexed()); if (!ymwdi.ok()) os << " is not a valid year_month_weekday"; return os; } template CONSTCD14 inline year_month_weekday operator+(const year_month_weekday& ymwd, const months& dm) NOEXCEPT { return (ymwd.year() / ymwd.month() + dm) / ymwd.weekday_indexed(); } template CONSTCD14 inline year_month_weekday operator+(const months& dm, const year_month_weekday& ymwd) NOEXCEPT { return ymwd + dm; } template CONSTCD14 inline year_month_weekday operator-(const year_month_weekday& ymwd, const months& dm) NOEXCEPT { return ymwd + (-dm); } CONSTCD11 inline year_month_weekday operator+(const year_month_weekday& ymwd, const years& dy) NOEXCEPT { return {ymwd.year()+dy, ymwd.month(), ymwd.weekday_indexed()}; } CONSTCD11 inline year_month_weekday operator+(const years& dy, const year_month_weekday& ymwd) NOEXCEPT { return ymwd + dy; } CONSTCD11 inline year_month_weekday operator-(const year_month_weekday& ymwd, const years& dy) NOEXCEPT { return ymwd + (-dy); } // year_month_weekday_last CONSTCD11 inline year_month_weekday_last::year_month_weekday_last(const date::year& y, const date::month& m, const date::weekday_last& wdl) NOEXCEPT : y_(y) , m_(m) , wdl_(wdl) {} template CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } template CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD11 inline year year_month_weekday_last::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_weekday_last::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday year_month_weekday_last::weekday() const NOEXCEPT { return wdl_.weekday(); } CONSTCD11 inline weekday_last year_month_weekday_last::weekday_last() const NOEXCEPT { return wdl_; } CONSTCD14 inline year_month_weekday_last::operator sys_days() const NOEXCEPT { return sys_days{to_days()}; } CONSTCD14 inline year_month_weekday_last::operator local_days() const NOEXCEPT { return local_days{to_days()}; } CONSTCD11 inline bool year_month_weekday_last::ok() const NOEXCEPT { return y_.ok() && m_.ok() && wdl_.ok(); } CONSTCD14 inline days year_month_weekday_last::to_days() const NOEXCEPT { auto const d = sys_days(y_/m_/last); return (d - (date::weekday{d} - wdl_.weekday())).time_since_epoch(); } CONSTCD11 inline bool operator==(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month() && x.weekday_last() == y.weekday_last(); } CONSTCD11 inline bool operator!=(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday_last& ymwdl) { detail::low_level_fmt(os, ymwdl.year()) << '/'; detail::low_level_fmt(os, ymwdl.month()) << '/'; detail::low_level_fmt(os, ymwdl.weekday_last()); if (!ymwdl.ok()) os << " is not a valid year_month_weekday_last"; return os; } template CONSTCD14 inline year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT { return (ymwdl.year() / ymwdl.month() + dm) / ymwdl.weekday_last(); } template CONSTCD14 inline year_month_weekday_last operator+(const months& dm, const year_month_weekday_last& ymwdl) NOEXCEPT { return ymwdl + dm; } template CONSTCD14 inline year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT { return ymwdl + (-dm); } CONSTCD11 inline year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT { return {ymwdl.year()+dy, ymwdl.month(), ymwdl.weekday_last()}; } CONSTCD11 inline year_month_weekday_last operator+(const years& dy, const year_month_weekday_last& ymwdl) NOEXCEPT { return ymwdl + dy; } CONSTCD11 inline year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT { return ymwdl + (-dy); } // year_month from operator/() CONSTCD11 inline year_month operator/(const year& y, const month& m) NOEXCEPT { return {y, m}; } CONSTCD11 inline year_month operator/(const year& y, int m) NOEXCEPT { return y / month(static_cast(m)); } // month_day from operator/() CONSTCD11 inline month_day operator/(const month& m, const day& d) NOEXCEPT { return {m, d}; } CONSTCD11 inline month_day operator/(const day& d, const month& m) NOEXCEPT { return m / d; } CONSTCD11 inline month_day operator/(const month& m, int d) NOEXCEPT { return m / day(static_cast(d)); } CONSTCD11 inline month_day operator/(int m, const day& d) NOEXCEPT { return month(static_cast(m)) / d; } CONSTCD11 inline month_day operator/(const day& d, int m) NOEXCEPT {return m / d;} // month_day_last from operator/() CONSTCD11 inline month_day_last operator/(const month& m, last_spec) NOEXCEPT { return month_day_last{m}; } CONSTCD11 inline month_day_last operator/(last_spec, const month& m) NOEXCEPT { return m/last; } CONSTCD11 inline month_day_last operator/(int m, last_spec) NOEXCEPT { return month(static_cast(m))/last; } CONSTCD11 inline month_day_last operator/(last_spec, int m) NOEXCEPT { return m/last; } // month_weekday from operator/() CONSTCD11 inline month_weekday operator/(const month& m, const weekday_indexed& wdi) NOEXCEPT { return {m, wdi}; } CONSTCD11 inline month_weekday operator/(const weekday_indexed& wdi, const month& m) NOEXCEPT { return m / wdi; } CONSTCD11 inline month_weekday operator/(int m, const weekday_indexed& wdi) NOEXCEPT { return month(static_cast(m)) / wdi; } CONSTCD11 inline month_weekday operator/(const weekday_indexed& wdi, int m) NOEXCEPT { return m / wdi; } // month_weekday_last from operator/() CONSTCD11 inline month_weekday_last operator/(const month& m, const weekday_last& wdl) NOEXCEPT { return {m, wdl}; } CONSTCD11 inline month_weekday_last operator/(const weekday_last& wdl, const month& m) NOEXCEPT { return m / wdl; } CONSTCD11 inline month_weekday_last operator/(int m, const weekday_last& wdl) NOEXCEPT { return month(static_cast(m)) / wdl; } CONSTCD11 inline month_weekday_last operator/(const weekday_last& wdl, int m) NOEXCEPT { return m / wdl; } // year_month_day from operator/() CONSTCD11 inline year_month_day operator/(const year_month& ym, const day& d) NOEXCEPT { return {ym.year(), ym.month(), d}; } CONSTCD11 inline year_month_day operator/(const year_month& ym, int d) NOEXCEPT { return ym / day(static_cast(d)); } CONSTCD11 inline year_month_day operator/(const year& y, const month_day& md) NOEXCEPT { return y / md.month() / md.day(); } CONSTCD11 inline year_month_day operator/(int y, const month_day& md) NOEXCEPT { return year(y) / md; } CONSTCD11 inline year_month_day operator/(const month_day& md, const year& y) NOEXCEPT { return y / md; } CONSTCD11 inline year_month_day operator/(const month_day& md, int y) NOEXCEPT { return year(y) / md; } // year_month_day_last from operator/() CONSTCD11 inline year_month_day_last operator/(const year_month& ym, last_spec) NOEXCEPT { return {ym.year(), month_day_last{ym.month()}}; } CONSTCD11 inline year_month_day_last operator/(const year& y, const month_day_last& mdl) NOEXCEPT { return {y, mdl}; } CONSTCD11 inline year_month_day_last operator/(int y, const month_day_last& mdl) NOEXCEPT { return year(y) / mdl; } CONSTCD11 inline year_month_day_last operator/(const month_day_last& mdl, const year& y) NOEXCEPT { return y / mdl; } CONSTCD11 inline year_month_day_last operator/(const month_day_last& mdl, int y) NOEXCEPT { return year(y) / mdl; } // year_month_weekday from operator/() CONSTCD11 inline year_month_weekday operator/(const year_month& ym, const weekday_indexed& wdi) NOEXCEPT { return {ym.year(), ym.month(), wdi}; } CONSTCD11 inline year_month_weekday operator/(const year& y, const month_weekday& mwd) NOEXCEPT { return {y, mwd.month(), mwd.weekday_indexed()}; } CONSTCD11 inline year_month_weekday operator/(int y, const month_weekday& mwd) NOEXCEPT { return year(y) / mwd; } CONSTCD11 inline year_month_weekday operator/(const month_weekday& mwd, const year& y) NOEXCEPT { return y / mwd; } CONSTCD11 inline year_month_weekday operator/(const month_weekday& mwd, int y) NOEXCEPT { return year(y) / mwd; } // year_month_weekday_last from operator/() CONSTCD11 inline year_month_weekday_last operator/(const year_month& ym, const weekday_last& wdl) NOEXCEPT { return {ym.year(), ym.month(), wdl}; } CONSTCD11 inline year_month_weekday_last operator/(const year& y, const month_weekday_last& mwdl) NOEXCEPT { return {y, mwdl.month(), mwdl.weekday_last()}; } CONSTCD11 inline year_month_weekday_last operator/(int y, const month_weekday_last& mwdl) NOEXCEPT { return year(y) / mwdl; } CONSTCD11 inline year_month_weekday_last operator/(const month_weekday_last& mwdl, const year& y) NOEXCEPT { return y / mwdl; } CONSTCD11 inline year_month_weekday_last operator/(const month_weekday_last& mwdl, int y) NOEXCEPT { return year(y) / mwdl; } template struct fields; template std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const fields& fds, const std::string* abbrev = nullptr, const std::chrono::seconds* offset_sec = nullptr); template std::basic_istream& from_stream(std::basic_istream& is, const CharT* fmt, fields& fds, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr); // hh_mm_ss namespace detail { struct undocumented {explicit undocumented() = default;}; // width::value is the number of fractional decimal digits in 1/n // width<0>::value and width<1>::value are defined to be 0 // If 1/n takes more than 18 fractional decimal digits, // the result is truncated to 19. // Example: width<2>::value == 1 // Example: width<3>::value == 19 // Example: width<4>::value == 2 // Example: width<10>::value == 1 // Example: width<1000>::value == 3 template struct width { static_assert(d > 0, "width called with zero denominator"); static CONSTDATA unsigned value = 1 + width::value; }; template struct width { static CONSTDATA unsigned value = 0; }; template struct static_pow10 { private: static CONSTDATA std::uint64_t h = static_pow10::value; public: static CONSTDATA std::uint64_t value = h * h * (exp % 2 ? 10 : 1); }; template <> struct static_pow10<0> { static CONSTDATA std::uint64_t value = 1; }; template class decimal_format_seconds { using CT = typename std::common_type::type; using rep = typename CT::rep; static unsigned CONSTDATA trial_width = detail::width::value; public: static unsigned CONSTDATA width = trial_width < 19 ? trial_width : 6u; using precision = std::chrono::duration::value>>; private: std::chrono::seconds s_; precision sub_s_; public: CONSTCD11 decimal_format_seconds() : s_() , sub_s_() {} CONSTCD11 explicit decimal_format_seconds(const Duration& d) NOEXCEPT : s_(std::chrono::duration_cast(d)) , sub_s_(std::chrono::duration_cast(d - s_)) {} CONSTCD14 std::chrono::seconds& seconds() NOEXCEPT {return s_;} CONSTCD11 std::chrono::seconds seconds() const NOEXCEPT {return s_;} CONSTCD11 precision subseconds() const NOEXCEPT {return sub_s_;} CONSTCD14 precision to_duration() const NOEXCEPT { return s_ + sub_s_; } CONSTCD11 bool in_conventional_range() const NOEXCEPT { return sub_s_ < std::chrono::seconds{1} && s_ < std::chrono::minutes{1}; } template friend std::basic_ostream& operator<<(std::basic_ostream& os, const decimal_format_seconds& x) { return x.print(os, std::chrono::treat_as_floating_point{}); } template std::basic_ostream& print(std::basic_ostream& os, std::true_type) const { date::detail::save_ostream _(os); std::chrono::duration d = s_ + sub_s_; if (d < std::chrono::seconds{10}) os << '0'; os.precision(width+6); os << std::fixed << d.count(); return os; } template std::basic_ostream& print(std::basic_ostream& os, std::false_type) const { date::detail::save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::right); os.width(2); os << s_.count(); if (width > 0) { #if !ONLY_C_LOCALE os << std::use_facet>(os.getloc()).decimal_point(); #else os << '.'; #endif date::detail::save_ostream _s(os); os.imbue(std::locale::classic()); os.width(width); os << sub_s_.count(); } return os; } }; template inline CONSTCD11 typename std::enable_if < std::numeric_limits::is_signed, std::chrono::duration >::type abs(std::chrono::duration d) { return d >= d.zero() ? +d : -d; } template inline CONSTCD11 typename std::enable_if < !std::numeric_limits::is_signed, std::chrono::duration >::type abs(std::chrono::duration d) { return d; } } // namespace detail template class hh_mm_ss { using dfs = detail::decimal_format_seconds::type>; std::chrono::hours h_; std::chrono::minutes m_; dfs s_; bool neg_; public: static unsigned CONSTDATA fractional_width = dfs::width; using precision = typename dfs::precision; CONSTCD11 hh_mm_ss() NOEXCEPT : hh_mm_ss(Duration::zero()) {} CONSTCD11 explicit hh_mm_ss(Duration d) NOEXCEPT : h_(std::chrono::duration_cast(detail::abs(d))) , m_(std::chrono::duration_cast(detail::abs(d)) - h_) , s_(detail::abs(d) - h_ - m_) , neg_(d < Duration::zero()) {} CONSTCD11 std::chrono::hours hours() const NOEXCEPT {return h_;} CONSTCD11 std::chrono::minutes minutes() const NOEXCEPT {return m_;} CONSTCD11 std::chrono::seconds seconds() const NOEXCEPT {return s_.seconds();} CONSTCD14 std::chrono::seconds& seconds(detail::undocumented) NOEXCEPT {return s_.seconds();} CONSTCD11 precision subseconds() const NOEXCEPT {return s_.subseconds();} CONSTCD11 bool is_negative() const NOEXCEPT {return neg_;} CONSTCD11 explicit operator precision() const NOEXCEPT {return to_duration();} CONSTCD11 precision to_duration() const NOEXCEPT {return (s_.to_duration() + m_ + h_) * (1-2*neg_);} CONSTCD11 bool in_conventional_range() const NOEXCEPT { return !neg_ && h_ < days{1} && m_ < std::chrono::hours{1} && s_.in_conventional_range(); } private: template friend std::basic_ostream& operator<<(std::basic_ostream& os, hh_mm_ss const& tod) { if (tod.is_negative()) os << '-'; if (tod.h_ < std::chrono::hours{10}) os << '0'; os << tod.h_.count() << ':'; if (tod.m_ < std::chrono::minutes{10}) os << '0'; os << tod.m_.count() << ':' << tod.s_; return os; } template friend std::basic_ostream& date::to_stream(std::basic_ostream& os, const CharT* fmt, const fields& fds, const std::string* abbrev, const std::chrono::seconds* offset_sec); template friend std::basic_istream& date::from_stream(std::basic_istream& is, const CharT* fmt, fields& fds, std::basic_string* abbrev, std::chrono::minutes* offset); }; inline CONSTCD14 bool is_am(std::chrono::hours const& h) NOEXCEPT { using std::chrono::hours; return hours{0} <= h && h < hours{12}; } inline CONSTCD14 bool is_pm(std::chrono::hours const& h) NOEXCEPT { using std::chrono::hours; return hours{12} <= h && h < hours{24}; } inline CONSTCD14 std::chrono::hours make12(std::chrono::hours h) NOEXCEPT { using std::chrono::hours; if (h < hours{12}) { if (h == hours{0}) h = hours{12}; } else { if (h != hours{12}) h = h - hours{12}; } return h; } inline CONSTCD14 std::chrono::hours make24(std::chrono::hours h, bool is_pm) NOEXCEPT { using std::chrono::hours; if (is_pm) { if (h != hours{12}) h = h + hours{12}; } else if (h == hours{12}) h = hours{0}; return h; } template using time_of_day = hh_mm_ss; template CONSTCD11 inline hh_mm_ss> make_time(const std::chrono::duration& d) { return hh_mm_ss>(d); } template inline typename std::enable_if < !std::is_convertible::value, std::basic_ostream& >::type operator<<(std::basic_ostream& os, const sys_time& tp) { auto const dp = date::floor(tp); return os << year_month_day(dp) << ' ' << make_time(tp-dp); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const sys_days& dp) { return os << year_month_day(dp); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const local_time& ut) { return (date::operator<<(os, sys_time{ut.time_since_epoch()})); } namespace detail { template class string_literal; template inline CONSTCD14 string_literal::type, N1 + N2 - 1> operator+(const string_literal& x, const string_literal& y) NOEXCEPT; template class string_literal { CharT p_[N]; CONSTCD11 string_literal() NOEXCEPT : p_{} {} public: using const_iterator = const CharT*; string_literal(string_literal const&) = default; string_literal& operator=(string_literal const&) = delete; template ::type> CONSTCD11 string_literal(CharT c) NOEXCEPT : p_{c} { } template ::type> CONSTCD11 string_literal(CharT c1, CharT c2) NOEXCEPT : p_{c1, c2} { } template ::type> CONSTCD11 string_literal(CharT c1, CharT c2, CharT c3) NOEXCEPT : p_{c1, c2, c3} { } CONSTCD14 string_literal(const CharT(&a)[N]) NOEXCEPT : p_{} { for (std::size_t i = 0; i < N; ++i) p_[i] = a[i]; } template ::type> CONSTCD14 string_literal(const char(&a)[N]) NOEXCEPT : p_{} { for (std::size_t i = 0; i < N; ++i) p_[i] = a[i]; } template ::value>::type> CONSTCD14 string_literal(string_literal const& a) NOEXCEPT : p_{} { for (std::size_t i = 0; i < N; ++i) p_[i] = a[i]; } CONSTCD11 const CharT* data() const NOEXCEPT {return p_;} CONSTCD11 std::size_t size() const NOEXCEPT {return N-1;} CONSTCD11 const_iterator begin() const NOEXCEPT {return p_;} CONSTCD11 const_iterator end() const NOEXCEPT {return p_ + N-1;} CONSTCD11 CharT const& operator[](std::size_t n) const NOEXCEPT { return p_[n]; } template friend std::basic_ostream& operator<<(std::basic_ostream& os, const string_literal& s) { return os << s.p_; } template friend CONSTCD14 string_literal::type, N1 + N2 - 1> operator+(const string_literal& x, const string_literal& y) NOEXCEPT; }; template CONSTCD11 inline string_literal operator+(const string_literal& x, const string_literal& y) NOEXCEPT { return string_literal(x[0], y[0]); } template CONSTCD11 inline string_literal operator+(const string_literal& x, const string_literal& y) NOEXCEPT { return string_literal(x[0], x[1], y[0]); } template CONSTCD14 inline string_literal::type, N1 + N2 - 1> operator+(const string_literal& x, const string_literal& y) NOEXCEPT { using CT = typename std::conditional::type; string_literal r; std::size_t i = 0; for (; i < N1-1; ++i) r.p_[i] = CT(x.p_[i]); for (std::size_t j = 0; j < N2; ++j, ++i) r.p_[i] = CT(y.p_[j]); return r; } template inline std::basic_string operator+(std::basic_string x, const string_literal& y) { x.append(y.data(), y.size()); return x; } #if __cplusplus >= 201402 && (!defined(__EDG_VERSION__) || __EDG_VERSION__ > 411) \ && (!defined(__SUNPRO_CC) || __SUNPRO_CC > 0x5150) template ::value || std::is_same::value || std::is_same::value || std::is_same::value>> CONSTCD14 inline string_literal msl(CharT c) NOEXCEPT { return string_literal{c}; } CONSTCD14 inline std::size_t to_string_len(std::intmax_t i) { std::size_t r = 0; do { i /= 10; ++r; } while (i > 0); return r; } template CONSTCD14 inline std::enable_if_t < N < 10, string_literal > msl() NOEXCEPT { return msl(char(N % 10 + '0')); } template CONSTCD14 inline std::enable_if_t < 10 <= N, string_literal > msl() NOEXCEPT { return msl() + msl(char(N % 10 + '0')); } template CONSTCD14 inline std::enable_if_t < std::ratio::type::den != 1, string_literal::type::num) + to_string_len(std::ratio::type::den) + 4> > msl(std::ratio) NOEXCEPT { using R = typename std::ratio::type; return msl(CharT{'['}) + msl() + msl(CharT{'/'}) + msl() + msl(CharT{']'}); } template CONSTCD14 inline std::enable_if_t < std::ratio::type::den == 1, string_literal::type::num) + 3> > msl(std::ratio) NOEXCEPT { using R = typename std::ratio::type; return msl(CharT{'['}) + msl() + msl(CharT{']'}); } #else // __cplusplus < 201402 || (defined(__EDG_VERSION__) && __EDG_VERSION__ <= 411) inline std::string to_string(std::uint64_t x) { return std::to_string(x); } template inline std::basic_string to_string(std::uint64_t x) { auto y = std::to_string(x); return std::basic_string(y.begin(), y.end()); } template inline typename std::enable_if < std::ratio::type::den != 1, std::basic_string >::type msl(std::ratio) { using R = typename std::ratio::type; return std::basic_string(1, '[') + to_string(R::num) + CharT{'/'} + to_string(R::den) + CharT{']'}; } template inline typename std::enable_if < std::ratio::type::den == 1, std::basic_string >::type msl(std::ratio) { using R = typename std::ratio::type; return std::basic_string(1, '[') + to_string(R::num) + CharT{']'}; } #endif // __cplusplus < 201402 || (defined(__EDG_VERSION__) && __EDG_VERSION__ <= 411) template CONSTCD11 inline string_literal msl(std::atto) NOEXCEPT { return string_literal{'a'}; } template CONSTCD11 inline string_literal msl(std::femto) NOEXCEPT { return string_literal{'f'}; } template CONSTCD11 inline string_literal msl(std::pico) NOEXCEPT { return string_literal{'p'}; } template CONSTCD11 inline string_literal msl(std::nano) NOEXCEPT { return string_literal{'n'}; } template CONSTCD11 inline typename std::enable_if < std::is_same::value, string_literal >::type msl(std::micro) NOEXCEPT { return string_literal{'\xC2', '\xB5'}; } template CONSTCD11 inline typename std::enable_if < !std::is_same::value, string_literal >::type msl(std::micro) NOEXCEPT { return string_literal{CharT{static_cast('\xB5')}}; } template CONSTCD11 inline string_literal msl(std::milli) NOEXCEPT { return string_literal{'m'}; } template CONSTCD11 inline string_literal msl(std::centi) NOEXCEPT { return string_literal{'c'}; } template CONSTCD11 inline string_literal msl(std::deca) NOEXCEPT { return string_literal{'d', 'a'}; } template CONSTCD11 inline string_literal msl(std::deci) NOEXCEPT { return string_literal{'d'}; } template CONSTCD11 inline string_literal msl(std::hecto) NOEXCEPT { return string_literal{'h'}; } template CONSTCD11 inline string_literal msl(std::kilo) NOEXCEPT { return string_literal{'k'}; } template CONSTCD11 inline string_literal msl(std::mega) NOEXCEPT { return string_literal{'M'}; } template CONSTCD11 inline string_literal msl(std::giga) NOEXCEPT { return string_literal{'G'}; } template CONSTCD11 inline string_literal msl(std::tera) NOEXCEPT { return string_literal{'T'}; } template CONSTCD11 inline string_literal msl(std::peta) NOEXCEPT { return string_literal{'P'}; } template CONSTCD11 inline string_literal msl(std::exa) NOEXCEPT { return string_literal{'E'}; } template CONSTCD11 inline auto get_units(Period p) -> decltype(msl(p) + string_literal{'s'}) { return msl(p) + string_literal{'s'}; } template CONSTCD11 inline string_literal get_units(std::ratio<1>) { return string_literal{'s'}; } template CONSTCD11 inline string_literal get_units(std::ratio<3600>) { return string_literal{'h'}; } template CONSTCD11 inline string_literal get_units(std::ratio<60>) { return string_literal{'m', 'i', 'n'}; } template CONSTCD11 inline string_literal get_units(std::ratio<86400>) { return string_literal{'d'}; } template > struct make_string; template <> struct make_string { template static std::string from(Rep n) { return std::to_string(n); } }; template struct make_string { template static std::basic_string from(Rep n) { auto s = std::to_string(n); return std::basic_string(s.begin(), s.end()); } }; template <> struct make_string { template static std::wstring from(Rep n) { return std::to_wstring(n); } }; template struct make_string { template static std::basic_string from(Rep n) { auto s = std::to_wstring(n); return std::basic_string(s.begin(), s.end()); } }; } // namespace detail // to_stream CONSTDATA year nanyear{-32768}; template struct fields { year_month_day ymd{nanyear/0/0}; weekday wd{8u}; hh_mm_ss tod{}; bool has_tod = false; #if !defined(__clang__) && defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__ <= 409) fields() : ymd{nanyear/0/0}, wd{8u}, tod{}, has_tod{false} {} #else fields() = default; #endif fields(year_month_day ymd_) : ymd(ymd_) {} fields(weekday wd_) : wd(wd_) {} fields(hh_mm_ss tod_) : tod(tod_), has_tod(true) {} fields(year_month_day ymd_, weekday wd_) : ymd(ymd_), wd(wd_) {} fields(year_month_day ymd_, hh_mm_ss tod_) : ymd(ymd_), tod(tod_), has_tod(true) {} fields(weekday wd_, hh_mm_ss tod_) : wd(wd_), tod(tod_), has_tod(true) {} fields(year_month_day ymd_, weekday wd_, hh_mm_ss tod_) : ymd(ymd_) , wd(wd_) , tod(tod_) , has_tod(true) {} }; namespace detail { template unsigned extract_weekday(std::basic_ostream& os, const fields& fds) { if (!fds.ymd.ok() && !fds.wd.ok()) { // fds does not contain a valid weekday os.setstate(std::ios::failbit); return 8; } weekday wd; if (fds.ymd.ok()) { wd = weekday{sys_days(fds.ymd)}; if (fds.wd.ok() && wd != fds.wd) { // fds.ymd and fds.wd are inconsistent os.setstate(std::ios::failbit); return 8; } } else wd = fds.wd; return static_cast((wd - Sunday).count()); } template unsigned extract_month(std::basic_ostream& os, const fields& fds) { if (!fds.ymd.month().ok()) { // fds does not contain a valid month os.setstate(std::ios::failbit); return 0; } return static_cast(fds.ymd.month()); } } // namespace detail #if ONLY_C_LOCALE namespace detail { inline std::pair weekday_names() { static const std::string nm[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; return std::make_pair(nm, nm+sizeof(nm)/sizeof(nm[0])); } inline std::pair month_names() { static const std::string nm[] = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; return std::make_pair(nm, nm+sizeof(nm)/sizeof(nm[0])); } inline std::pair ampm_names() { static const std::string nm[] = { "AM", "PM" }; return std::make_pair(nm, nm+sizeof(nm)/sizeof(nm[0])); } template FwdIter scan_keyword(std::basic_istream& is, FwdIter kb, FwdIter ke) { size_t nkw = static_cast(std::distance(kb, ke)); const unsigned char doesnt_match = '\0'; const unsigned char might_match = '\1'; const unsigned char does_match = '\2'; unsigned char statbuf[100]; unsigned char* status = statbuf; std::unique_ptr stat_hold(0, free); if (nkw > sizeof(statbuf)) { status = (unsigned char*)std::malloc(nkw); if (status == nullptr) throw std::bad_alloc(); stat_hold.reset(status); } size_t n_might_match = nkw; // At this point, any keyword might match size_t n_does_match = 0; // but none of them definitely do // Initialize all statuses to might_match, except for "" keywords are does_match unsigned char* st = status; for (auto ky = kb; ky != ke; ++ky, ++st) { if (!ky->empty()) *st = might_match; else { *st = does_match; --n_might_match; ++n_does_match; } } // While there might be a match, test keywords against the next CharT for (size_t indx = 0; is && n_might_match > 0; ++indx) { // Peek at the next CharT but don't consume it auto ic = is.peek(); if (ic == EOF) { is.setstate(std::ios::eofbit); break; } auto c = static_cast(toupper(static_cast(ic))); bool consume = false; // For each keyword which might match, see if the indx character is c // If a match if found, consume c // If a match is found, and that is the last character in the keyword, // then that keyword matches. // If the keyword doesn't match this character, then change the keyword // to doesn't match st = status; for (auto ky = kb; ky != ke; ++ky, ++st) { if (*st == might_match) { if (c == static_cast(toupper(static_cast((*ky)[indx])))) { consume = true; if (ky->size() == indx+1) { *st = does_match; --n_might_match; ++n_does_match; } } else { *st = doesnt_match; --n_might_match; } } } // consume if we matched a character if (consume) { (void)is.get(); // If we consumed a character and there might be a matched keyword that // was marked matched on a previous iteration, then such keywords // are now marked as not matching. if (n_might_match + n_does_match > 1) { st = status; for (auto ky = kb; ky != ke; ++ky, ++st) { if (*st == does_match && ky->size() != indx+1) { *st = doesnt_match; --n_does_match; } } } } } // We've exited the loop because we hit eof and/or we have no more "might matches". // Return the first matching result for (st = status; kb != ke; ++kb, ++st) if (*st == does_match) break; if (kb == ke) is.setstate(std::ios::failbit); return kb; } } // namespace detail #endif // ONLY_C_LOCALE template std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const fields& fds, const std::string* abbrev, const std::chrono::seconds* offset_sec) { #if ONLY_C_LOCALE using detail::weekday_names; using detail::month_names; using detail::ampm_names; #endif using detail::save_ostream; using detail::get_units; using detail::extract_weekday; using detail::extract_month; using std::ios; using std::chrono::duration_cast; using std::chrono::seconds; using std::chrono::minutes; using std::chrono::hours; date::detail::save_ostream ss(os); os.fill(' '); os.flags(std::ios::skipws | std::ios::dec); os.width(0); tm tm{}; bool insert_negative = fds.has_tod && fds.tod.to_duration() < Duration::zero(); #if !ONLY_C_LOCALE auto& facet = std::use_facet>(os.getloc()); #endif const CharT* command = nullptr; CharT modified = CharT{}; for (; *fmt; ++fmt) { switch (*fmt) { case 'a': case 'A': if (command) { if (modified == CharT{}) { tm.tm_wday = static_cast(extract_weekday(os, fds)); if (os.fail()) return os; #if !ONLY_C_LOCALE const CharT f[] = {'%', *fmt}; facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); #else // ONLY_C_LOCALE os << weekday_names().first[tm.tm_wday+7*(*fmt == 'a')]; #endif // ONLY_C_LOCALE } else { os << CharT{'%'} << modified << *fmt; modified = CharT{}; } command = nullptr; } else os << *fmt; break; case 'b': case 'B': case 'h': if (command) { if (modified == CharT{}) { tm.tm_mon = static_cast(extract_month(os, fds)) - 1; #if !ONLY_C_LOCALE const CharT f[] = {'%', *fmt}; facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); #else // ONLY_C_LOCALE os << month_names().first[tm.tm_mon+12*(*fmt != 'B')]; #endif // ONLY_C_LOCALE } else { os << CharT{'%'} << modified << *fmt; modified = CharT{}; } command = nullptr; } else os << *fmt; break; case 'c': case 'x': if (command) { if (modified == CharT{'O'}) os << CharT{'%'} << modified << *fmt; else { if (!fds.ymd.ok()) os.setstate(std::ios::failbit); if (*fmt == 'c' && !fds.has_tod) os.setstate(std::ios::failbit); #if !ONLY_C_LOCALE tm = std::tm{}; auto const& ymd = fds.ymd; auto ld = local_days(ymd); if (*fmt == 'c') { tm.tm_sec = static_cast(fds.tod.seconds().count()); tm.tm_min = static_cast(fds.tod.minutes().count()); tm.tm_hour = static_cast(fds.tod.hours().count()); } tm.tm_mday = static_cast(static_cast(ymd.day())); tm.tm_mon = static_cast(extract_month(os, fds) - 1); tm.tm_year = static_cast(ymd.year()) - 1900; tm.tm_wday = static_cast(extract_weekday(os, fds)); if (os.fail()) return os; tm.tm_yday = static_cast((ld - local_days(ymd.year()/1/1)).count()); CharT f[3] = {'%'}; auto fe = std::begin(f) + 1; if (modified == CharT{'E'}) *fe++ = modified; *fe++ = *fmt; facet.put(os, os, os.fill(), &tm, std::begin(f), fe); #else // ONLY_C_LOCALE if (*fmt == 'c') { auto wd = static_cast(extract_weekday(os, fds)); os << weekday_names().first[static_cast(wd)+7] << ' '; os << month_names().first[extract_month(os, fds)-1+12] << ' '; auto d = static_cast(static_cast(fds.ymd.day())); if (d < 10) os << ' '; os << d << ' ' << make_time(duration_cast(fds.tod.to_duration())) << ' ' << fds.ymd.year(); } else // *fmt == 'x' { auto const& ymd = fds.ymd; save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::right); os.width(2); os << static_cast(ymd.month()) << CharT{'/'}; os.width(2); os << static_cast(ymd.day()) << CharT{'/'}; os.width(2); os << static_cast(ymd.year()) % 100; } #endif // ONLY_C_LOCALE } command = nullptr; modified = CharT{}; } else os << *fmt; break; case 'C': if (command) { if (modified == CharT{'O'}) os << CharT{'%'} << modified << *fmt; else { if (!fds.ymd.year().ok()) os.setstate(std::ios::failbit); auto y = static_cast(fds.ymd.year()); #if !ONLY_C_LOCALE if (modified == CharT{}) #endif { save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::right); if (y >= 0) { os.width(2); os << y/100; } else { os << CharT{'-'}; os.width(2); os << -(y-99)/100; } } #if !ONLY_C_LOCALE else if (modified == CharT{'E'}) { tm.tm_year = y - 1900; CharT f[3] = {'%', 'E', 'C'}; facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); } #endif } command = nullptr; modified = CharT{}; } else os << *fmt; break; case 'd': case 'e': if (command) { if (modified == CharT{'E'}) os << CharT{'%'} << modified << *fmt; else { if (!fds.ymd.day().ok()) os.setstate(std::ios::failbit); auto d = static_cast(static_cast(fds.ymd.day())); #if !ONLY_C_LOCALE if (modified == CharT{}) #endif { save_ostream _(os); if (*fmt == CharT{'d'}) os.fill('0'); else os.fill(' '); os.flags(std::ios::dec | std::ios::right); os.width(2); os << d; } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { tm.tm_mday = d; CharT f[3] = {'%', 'O', *fmt}; facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); } #endif } command = nullptr; modified = CharT{}; } else os << *fmt; break; case 'D': if (command) { if (modified == CharT{}) { if (!fds.ymd.ok()) os.setstate(std::ios::failbit); auto const& ymd = fds.ymd; save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::right); os.width(2); os << static_cast(ymd.month()) << CharT{'/'}; os.width(2); os << static_cast(ymd.day()) << CharT{'/'}; os.width(2); os << static_cast(ymd.year()) % 100; } else { os << CharT{'%'} << modified << *fmt; modified = CharT{}; } command = nullptr; } else os << *fmt; break; case 'F': if (command) { if (modified == CharT{}) { if (!fds.ymd.ok()) os.setstate(std::ios::failbit); auto const& ymd = fds.ymd; save_ostream _(os); os.imbue(std::locale::classic()); os.fill('0'); os.flags(std::ios::dec | std::ios::right); os.width(4); os << static_cast(ymd.year()) << CharT{'-'}; os.width(2); os << static_cast(ymd.month()) << CharT{'-'}; os.width(2); os << static_cast(ymd.day()); } else { os << CharT{'%'} << modified << *fmt; modified = CharT{}; } command = nullptr; } else os << *fmt; break; case 'g': case 'G': if (command) { if (modified == CharT{}) { if (!fds.ymd.ok()) os.setstate(std::ios::failbit); auto ld = local_days(fds.ymd); auto y = year_month_day{ld + days{3}}.year(); auto start = local_days((y-years{1})/December/Thursday[last]) + (Monday-Thursday); if (ld < start) --y; if (*fmt == CharT{'G'}) os << y; else { save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::right); os.width(2); os << std::abs(static_cast(y)) % 100; } } else { os << CharT{'%'} << modified << *fmt; modified = CharT{}; } command = nullptr; } else os << *fmt; break; case 'H': case 'I': if (command) { if (modified == CharT{'E'}) os << CharT{'%'} << modified << *fmt; else { if (!fds.has_tod) os.setstate(std::ios::failbit); if (insert_negative) { os << '-'; insert_negative = false; } auto hms = fds.tod; #if !ONLY_C_LOCALE if (modified == CharT{}) #endif { auto h = *fmt == CharT{'I'} ? date::make12(hms.hours()) : hms.hours(); if (h < hours{10}) os << CharT{'0'}; os << h.count(); } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { const CharT f[] = {'%', modified, *fmt}; tm.tm_hour = static_cast(hms.hours().count()); facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); } #endif } modified = CharT{}; command = nullptr; } else os << *fmt; break; case 'j': if (command) { if (modified == CharT{}) { if (fds.ymd.ok() || fds.has_tod) { days doy; if (fds.ymd.ok()) { auto ld = local_days(fds.ymd); auto y = fds.ymd.year(); doy = ld - local_days(y/January/1) + days{1}; } else { doy = duration_cast(fds.tod.to_duration()); } save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::right); os.width(3); os << doy.count(); } else { os.setstate(std::ios::failbit); } } else { os << CharT{'%'} << modified << *fmt; modified = CharT{}; } command = nullptr; } else os << *fmt; break; case 'm': if (command) { if (modified == CharT{'E'}) os << CharT{'%'} << modified << *fmt; else { if (!fds.ymd.month().ok()) os.setstate(std::ios::failbit); auto m = static_cast(fds.ymd.month()); #if !ONLY_C_LOCALE if (modified == CharT{}) #endif { if (m < 10) os << CharT{'0'}; os << m; } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { const CharT f[] = {'%', modified, *fmt}; tm.tm_mon = static_cast(m-1); facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); } #endif } modified = CharT{}; command = nullptr; } else os << *fmt; break; case 'M': if (command) { if (modified == CharT{'E'}) os << CharT{'%'} << modified << *fmt; else { if (!fds.has_tod) os.setstate(std::ios::failbit); if (insert_negative) { os << '-'; insert_negative = false; } #if !ONLY_C_LOCALE if (modified == CharT{}) #endif { if (fds.tod.minutes() < minutes{10}) os << CharT{'0'}; os << fds.tod.minutes().count(); } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { const CharT f[] = {'%', modified, *fmt}; tm.tm_min = static_cast(fds.tod.minutes().count()); facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); } #endif } modified = CharT{}; command = nullptr; } else os << *fmt; break; case 'n': if (command) { if (modified == CharT{}) os << CharT{'\n'}; else { os << CharT{'%'} << modified << *fmt; modified = CharT{}; } command = nullptr; } else os << *fmt; break; case 'p': if (command) { if (modified == CharT{}) { if (!fds.has_tod) os.setstate(std::ios::failbit); #if !ONLY_C_LOCALE const CharT f[] = {'%', *fmt}; tm.tm_hour = static_cast(fds.tod.hours().count()); facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); #else if (date::is_am(fds.tod.hours())) os << ampm_names().first[0]; else os << ampm_names().first[1]; #endif } else { os << CharT{'%'} << modified << *fmt; } modified = CharT{}; command = nullptr; } else os << *fmt; break; case 'Q': case 'q': if (command) { if (modified == CharT{}) { if (!fds.has_tod) os.setstate(std::ios::failbit); auto d = fds.tod.to_duration(); if (*fmt == 'q') os << get_units(typename decltype(d)::period::type{}); else os << d.count(); } else { os << CharT{'%'} << modified << *fmt; } modified = CharT{}; command = nullptr; } else os << *fmt; break; case 'r': if (command) { if (modified == CharT{}) { if (!fds.has_tod) os.setstate(std::ios::failbit); #if !ONLY_C_LOCALE const CharT f[] = {'%', *fmt}; tm.tm_hour = static_cast(fds.tod.hours().count()); tm.tm_min = static_cast(fds.tod.minutes().count()); tm.tm_sec = static_cast(fds.tod.seconds().count()); facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); #else hh_mm_ss tod(duration_cast(fds.tod.to_duration())); save_ostream _(os); os.fill('0'); os.width(2); os << date::make12(tod.hours()).count() << CharT{':'}; os.width(2); os << tod.minutes().count() << CharT{':'}; os.width(2); os << tod.seconds().count() << CharT{' '}; if (date::is_am(tod.hours())) os << ampm_names().first[0]; else os << ampm_names().first[1]; #endif } else { os << CharT{'%'} << modified << *fmt; } modified = CharT{}; command = nullptr; } else os << *fmt; break; case 'R': if (command) { if (modified == CharT{}) { if (!fds.has_tod) os.setstate(std::ios::failbit); if (fds.tod.hours() < hours{10}) os << CharT{'0'}; os << fds.tod.hours().count() << CharT{':'}; if (fds.tod.minutes() < minutes{10}) os << CharT{'0'}; os << fds.tod.minutes().count(); } else { os << CharT{'%'} << modified << *fmt; modified = CharT{}; } command = nullptr; } else os << *fmt; break; case 'S': if (command) { if (modified == CharT{'E'}) os << CharT{'%'} << modified << *fmt; else { if (!fds.has_tod) os.setstate(std::ios::failbit); if (insert_negative) { os << '-'; insert_negative = false; } #if !ONLY_C_LOCALE if (modified == CharT{}) #endif { os << fds.tod.s_; } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { const CharT f[] = {'%', modified, *fmt}; tm.tm_sec = static_cast(fds.tod.s_.seconds().count()); facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); } #endif } modified = CharT{}; command = nullptr; } else os << *fmt; break; case 't': if (command) { if (modified == CharT{}) os << CharT{'\t'}; else { os << CharT{'%'} << modified << *fmt; modified = CharT{}; } command = nullptr; } else os << *fmt; break; case 'T': if (command) { if (modified == CharT{}) { if (!fds.has_tod) os.setstate(std::ios::failbit); os << fds.tod; } else { os << CharT{'%'} << modified << *fmt; modified = CharT{}; } command = nullptr; } else os << *fmt; break; case 'u': if (command) { if (modified == CharT{'E'}) os << CharT{'%'} << modified << *fmt; else { auto wd = extract_weekday(os, fds); #if !ONLY_C_LOCALE if (modified == CharT{}) #endif { os << (wd != 0 ? wd : 7u); } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { const CharT f[] = {'%', modified, *fmt}; tm.tm_wday = static_cast(wd); facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); } #endif } modified = CharT{}; command = nullptr; } else os << *fmt; break; case 'U': if (command) { if (modified == CharT{'E'}) os << CharT{'%'} << modified << *fmt; else { auto const& ymd = fds.ymd; if (!ymd.ok()) os.setstate(std::ios::failbit); auto ld = local_days(ymd); #if !ONLY_C_LOCALE if (modified == CharT{}) #endif { auto st = local_days(Sunday[1]/January/ymd.year()); if (ld < st) os << CharT{'0'} << CharT{'0'}; else { auto wn = duration_cast(ld - st).count() + 1; if (wn < 10) os << CharT{'0'}; os << wn; } } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { const CharT f[] = {'%', modified, *fmt}; tm.tm_year = static_cast(ymd.year()) - 1900; tm.tm_wday = static_cast(extract_weekday(os, fds)); if (os.fail()) return os; tm.tm_yday = static_cast((ld - local_days(ymd.year()/1/1)).count()); facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); } #endif } modified = CharT{}; command = nullptr; } else os << *fmt; break; case 'V': if (command) { if (modified == CharT{'E'}) os << CharT{'%'} << modified << *fmt; else { if (!fds.ymd.ok()) os.setstate(std::ios::failbit); auto ld = local_days(fds.ymd); #if !ONLY_C_LOCALE if (modified == CharT{}) #endif { auto y = year_month_day{ld + days{3}}.year(); auto st = local_days((y-years{1})/12/Thursday[last]) + (Monday-Thursday); if (ld < st) { --y; st = local_days((y - years{1})/12/Thursday[last]) + (Monday-Thursday); } auto wn = duration_cast(ld - st).count() + 1; if (wn < 10) os << CharT{'0'}; os << wn; } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { const CharT f[] = {'%', modified, *fmt}; auto const& ymd = fds.ymd; tm.tm_year = static_cast(ymd.year()) - 1900; tm.tm_wday = static_cast(extract_weekday(os, fds)); if (os.fail()) return os; tm.tm_yday = static_cast((ld - local_days(ymd.year()/1/1)).count()); facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); } #endif } modified = CharT{}; command = nullptr; } else os << *fmt; break; case 'w': if (command) { auto wd = extract_weekday(os, fds); if (os.fail()) return os; #if !ONLY_C_LOCALE if (modified == CharT{}) #else if (modified != CharT{'E'}) #endif { os << wd; } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { const CharT f[] = {'%', modified, *fmt}; tm.tm_wday = static_cast(wd); facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); } #endif else { os << CharT{'%'} << modified << *fmt; } modified = CharT{}; command = nullptr; } else os << *fmt; break; case 'W': if (command) { if (modified == CharT{'E'}) os << CharT{'%'} << modified << *fmt; else { auto const& ymd = fds.ymd; if (!ymd.ok()) os.setstate(std::ios::failbit); auto ld = local_days(ymd); #if !ONLY_C_LOCALE if (modified == CharT{}) #endif { auto st = local_days(Monday[1]/January/ymd.year()); if (ld < st) os << CharT{'0'} << CharT{'0'}; else { auto wn = duration_cast(ld - st).count() + 1; if (wn < 10) os << CharT{'0'}; os << wn; } } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { const CharT f[] = {'%', modified, *fmt}; tm.tm_year = static_cast(ymd.year()) - 1900; tm.tm_wday = static_cast(extract_weekday(os, fds)); if (os.fail()) return os; tm.tm_yday = static_cast((ld - local_days(ymd.year()/1/1)).count()); facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); } #endif } modified = CharT{}; command = nullptr; } else os << *fmt; break; case 'X': if (command) { if (modified == CharT{'O'}) os << CharT{'%'} << modified << *fmt; else { if (!fds.has_tod) os.setstate(std::ios::failbit); #if !ONLY_C_LOCALE tm = std::tm{}; tm.tm_sec = static_cast(fds.tod.seconds().count()); tm.tm_min = static_cast(fds.tod.minutes().count()); tm.tm_hour = static_cast(fds.tod.hours().count()); CharT f[3] = {'%'}; auto fe = std::begin(f) + 1; if (modified == CharT{'E'}) *fe++ = modified; *fe++ = *fmt; facet.put(os, os, os.fill(), &tm, std::begin(f), fe); #else os << fds.tod; #endif } command = nullptr; modified = CharT{}; } else os << *fmt; break; case 'y': if (command) { if (!fds.ymd.year().ok()) os.setstate(std::ios::failbit); auto y = static_cast(fds.ymd.year()); #if !ONLY_C_LOCALE if (modified == CharT{}) { #endif y = std::abs(y) % 100; if (y < 10) os << CharT{'0'}; os << y; #if !ONLY_C_LOCALE } else { const CharT f[] = {'%', modified, *fmt}; tm.tm_year = y - 1900; facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); } #endif modified = CharT{}; command = nullptr; } else os << *fmt; break; case 'Y': if (command) { if (modified == CharT{'O'}) os << CharT{'%'} << modified << *fmt; else { if (!fds.ymd.year().ok()) os.setstate(std::ios::failbit); auto y = fds.ymd.year(); #if !ONLY_C_LOCALE if (modified == CharT{}) #endif { save_ostream _(os); os.imbue(std::locale::classic()); os << y; } #if !ONLY_C_LOCALE else if (modified == CharT{'E'}) { const CharT f[] = {'%', modified, *fmt}; tm.tm_year = static_cast(y) - 1900; facet.put(os, os, os.fill(), &tm, std::begin(f), std::end(f)); } #endif } modified = CharT{}; command = nullptr; } else os << *fmt; break; case 'z': if (command) { if (offset_sec == nullptr) { // Can not format %z with unknown offset os.setstate(ios::failbit); return os; } auto m = duration_cast(*offset_sec); auto neg = m < minutes{0}; m = date::abs(m); auto h = duration_cast(m); m -= h; if (neg) os << CharT{'-'}; else os << CharT{'+'}; if (h < hours{10}) os << CharT{'0'}; os << h.count(); if (modified != CharT{}) os << CharT{':'}; if (m < minutes{10}) os << CharT{'0'}; os << m.count(); command = nullptr; modified = CharT{}; } else os << *fmt; break; case 'Z': if (command) { if (modified == CharT{}) { if (abbrev == nullptr) { // Can not format %Z with unknown time_zone os.setstate(ios::failbit); return os; } for (auto c : *abbrev) os << CharT(c); } else { os << CharT{'%'} << modified << *fmt; modified = CharT{}; } command = nullptr; } else os << *fmt; break; case 'E': case 'O': if (command) { if (modified == CharT{}) { modified = *fmt; } else { os << CharT{'%'} << modified << *fmt; command = nullptr; modified = CharT{}; } } else os << *fmt; break; case '%': if (command) { if (modified == CharT{}) { os << CharT{'%'}; command = nullptr; } else { os << CharT{'%'} << modified << CharT{'%'}; command = nullptr; modified = CharT{}; } } else command = fmt; break; default: if (command) { os << CharT{'%'}; command = nullptr; } if (modified != CharT{}) { os << modified; modified = CharT{}; } os << *fmt; break; } } if (command) os << CharT{'%'}; if (modified != CharT{}) os << modified; return os; } template inline std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const year& y) { using CT = std::chrono::seconds; fields fds{y/0/0}; return to_stream(os, fmt, fds); } template inline std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const month& m) { using CT = std::chrono::seconds; fields fds{m/0/nanyear}; return to_stream(os, fmt, fds); } template inline std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const day& d) { using CT = std::chrono::seconds; fields fds{d/0/nanyear}; return to_stream(os, fmt, fds); } template inline std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const weekday& wd) { using CT = std::chrono::seconds; fields fds{wd}; return to_stream(os, fmt, fds); } template inline std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const year_month& ym) { using CT = std::chrono::seconds; fields fds{ym/0}; return to_stream(os, fmt, fds); } template inline std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const month_day& md) { using CT = std::chrono::seconds; fields fds{md/nanyear}; return to_stream(os, fmt, fds); } template inline std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const year_month_day& ymd) { using CT = std::chrono::seconds; fields fds{ymd}; return to_stream(os, fmt, fds); } template inline std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const std::chrono::duration& d) { using Duration = std::chrono::duration; using CT = typename std::common_type::type; fields fds{hh_mm_ss{d}}; return to_stream(os, fmt, fds); } template std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const local_time& tp, const std::string* abbrev = nullptr, const std::chrono::seconds* offset_sec = nullptr) { using CT = typename std::common_type::type; auto ld = std::chrono::time_point_cast(tp); fields fds; if (ld <= tp) fds = fields{year_month_day{ld}, hh_mm_ss{tp-local_seconds{ld}}}; else fds = fields{year_month_day{ld - days{1}}, hh_mm_ss{days{1} - (local_seconds{ld} - tp)}}; return to_stream(os, fmt, fds, abbrev, offset_sec); } template std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const sys_time& tp) { using std::chrono::seconds; using CT = typename std::common_type::type; const std::string abbrev("UTC"); CONSTDATA seconds offset{0}; auto sd = std::chrono::time_point_cast(tp); fields fds; if (sd <= tp) fds = fields{year_month_day{sd}, hh_mm_ss{tp-sys_seconds{sd}}}; else fds = fields{year_month_day{sd - days{1}}, hh_mm_ss{days{1} - (sys_seconds{sd} - tp)}}; return to_stream(os, fmt, fds, &abbrev, &offset); } // format template auto format(const std::locale& loc, const CharT* fmt, const Streamable& tp) -> decltype(to_stream(std::declval&>(), fmt, tp), std::basic_string{}) { std::basic_ostringstream os; os.exceptions(std::ios::failbit | std::ios::badbit); os.imbue(loc); to_stream(os, fmt, tp); return os.str(); } template auto format(const CharT* fmt, const Streamable& tp) -> decltype(to_stream(std::declval&>(), fmt, tp), std::basic_string{}) { std::basic_ostringstream os; os.exceptions(std::ios::failbit | std::ios::badbit); to_stream(os, fmt, tp); return os.str(); } template auto format(const std::locale& loc, const std::basic_string& fmt, const Streamable& tp) -> decltype(to_stream(std::declval&>(), fmt.c_str(), tp), std::basic_string{}) { std::basic_ostringstream os; os.exceptions(std::ios::failbit | std::ios::badbit); os.imbue(loc); to_stream(os, fmt.c_str(), tp); return os.str(); } template auto format(const std::basic_string& fmt, const Streamable& tp) -> decltype(to_stream(std::declval&>(), fmt.c_str(), tp), std::basic_string{}) { std::basic_ostringstream os; os.exceptions(std::ios::failbit | std::ios::badbit); to_stream(os, fmt.c_str(), tp); return os.str(); } // parse namespace detail { template bool read_char(std::basic_istream& is, CharT fmt, std::ios::iostate& err) { auto ic = is.get(); if (Traits::eq_int_type(ic, Traits::eof()) || !Traits::eq(Traits::to_char_type(ic), fmt)) { err |= std::ios::failbit; is.setstate(std::ios::failbit); return false; } return true; } template unsigned read_unsigned(std::basic_istream& is, unsigned m = 1, unsigned M = 10) { unsigned x = 0; unsigned count = 0; while (true) { auto ic = is.peek(); if (Traits::eq_int_type(ic, Traits::eof())) break; auto c = static_cast(Traits::to_char_type(ic)); if (!('0' <= c && c <= '9')) break; (void)is.get(); ++count; x = 10*x + static_cast(c - '0'); if (count == M) break; } if (count < m) is.setstate(std::ios::failbit); return x; } template int read_signed(std::basic_istream& is, unsigned m = 1, unsigned M = 10) { auto ic = is.peek(); if (!Traits::eq_int_type(ic, Traits::eof())) { auto c = static_cast(Traits::to_char_type(ic)); if (('0' <= c && c <= '9') || c == '-' || c == '+') { if (c == '-' || c == '+') { (void)is.get(); --M; } auto x = static_cast(read_unsigned(is, std::max(m, 1u), M)); if (!is.fail()) { if (c == '-') x = -x; return x; } } } if (m > 0) is.setstate(std::ios::failbit); return 0; } template long double read_long_double(std::basic_istream& is, unsigned m = 1, unsigned M = 10) { unsigned count = 0; unsigned fcount = 0; unsigned long long i = 0; unsigned long long f = 0; bool parsing_fraction = false; #if ONLY_C_LOCALE typename Traits::int_type decimal_point = '.'; #else auto decimal_point = Traits::to_int_type( std::use_facet>(is.getloc()).decimal_point()); #endif while (true) { auto ic = is.peek(); if (Traits::eq_int_type(ic, Traits::eof())) break; if (Traits::eq_int_type(ic, decimal_point)) { decimal_point = Traits::eof(); parsing_fraction = true; } else { auto c = static_cast(Traits::to_char_type(ic)); if (!('0' <= c && c <= '9')) break; if (!parsing_fraction) { i = 10*i + static_cast(c - '0'); } else { f = 10*f + static_cast(c - '0'); ++fcount; } } (void)is.get(); if (++count == M) break; } if (count < m) { is.setstate(std::ios::failbit); return 0; } return static_cast(i) + static_cast(f)/std::pow(10.L, fcount); } struct rs { int& i; unsigned m; unsigned M; }; struct ru { int& i; unsigned m; unsigned M; }; struct rld { long double& i; unsigned m; unsigned M; }; template void read(std::basic_istream&) { } template void read(std::basic_istream& is, CharT a0, Args&& ...args); template void read(std::basic_istream& is, rs a0, Args&& ...args); template void read(std::basic_istream& is, ru a0, Args&& ...args); template void read(std::basic_istream& is, int a0, Args&& ...args); template void read(std::basic_istream& is, rld a0, Args&& ...args); template void read(std::basic_istream& is, CharT a0, Args&& ...args) { // No-op if a0 == CharT{} if (a0 != CharT{}) { auto ic = is.peek(); if (Traits::eq_int_type(ic, Traits::eof())) { is.setstate(std::ios::failbit | std::ios::eofbit); return; } if (!Traits::eq(Traits::to_char_type(ic), a0)) { is.setstate(std::ios::failbit); return; } (void)is.get(); } read(is, std::forward(args)...); } template void read(std::basic_istream& is, rs a0, Args&& ...args) { auto x = read_signed(is, a0.m, a0.M); if (is.fail()) return; a0.i = x; read(is, std::forward(args)...); } template void read(std::basic_istream& is, ru a0, Args&& ...args) { auto x = read_unsigned(is, a0.m, a0.M); if (is.fail()) return; a0.i = static_cast(x); read(is, std::forward(args)...); } template void read(std::basic_istream& is, int a0, Args&& ...args) { if (a0 != -1) { auto u = static_cast(a0); CharT buf[std::numeric_limits::digits10+2u] = {}; auto e = buf; do { *e++ = static_cast(CharT(u % 10) + CharT{'0'}); u /= 10; } while (u > 0); #if defined(__GNUC__) && __GNUC__ >= 11 #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wstringop-overflow" #endif std::reverse(buf, e); #if defined(__GNUC__) && __GNUC__ >= 11 #pragma GCC diagnostic pop #endif for (auto p = buf; p != e && is.rdstate() == std::ios::goodbit; ++p) read(is, *p); } if (is.rdstate() == std::ios::goodbit) read(is, std::forward(args)...); } template void read(std::basic_istream& is, rld a0, Args&& ...args) { auto x = read_long_double(is, a0.m, a0.M); if (is.fail()) return; a0.i = x; read(is, std::forward(args)...); } template inline void checked_set(T& value, T from, T not_a_value, std::basic_ios& is) { if (!is.fail()) { if (value == not_a_value) value = std::move(from); else if (value != from) is.setstate(std::ios::failbit); } } } // namespace detail; template > std::basic_istream& from_stream(std::basic_istream& is, const CharT* fmt, fields& fds, std::basic_string* abbrev, std::chrono::minutes* offset) { using std::numeric_limits; using std::ios; using std::chrono::duration; using std::chrono::duration_cast; using std::chrono::seconds; using std::chrono::minutes; using std::chrono::hours; using detail::round_i; typename std::basic_istream::sentry ok{is, true}; if (ok) { date::detail::save_istream ss(is); is.fill(' '); is.flags(std::ios::skipws | std::ios::dec); is.width(0); #if !ONLY_C_LOCALE auto& f = std::use_facet>(is.getloc()); std::tm tm{}; #endif const CharT* command = nullptr; auto modified = CharT{}; auto width = -1; CONSTDATA int not_a_year = numeric_limits::min(); CONSTDATA int not_a_2digit_year = 100; CONSTDATA int not_a_century = numeric_limits::min(); CONSTDATA int not_a_month = 0; CONSTDATA int not_a_day = 0; CONSTDATA int not_a_hour = numeric_limits::min(); CONSTDATA int not_a_hour_12_value = 0; CONSTDATA int not_a_minute = not_a_hour; CONSTDATA Duration not_a_second = Duration::min(); CONSTDATA int not_a_doy = -1; CONSTDATA int not_a_weekday = 8; CONSTDATA int not_a_week_num = 100; CONSTDATA int not_a_ampm = -1; CONSTDATA minutes not_a_offset = minutes::min(); int Y = not_a_year; // c, F, Y * int y = not_a_2digit_year; // D, x, y * int g = not_a_2digit_year; // g * int G = not_a_year; // G * int C = not_a_century; // C * int m = not_a_month; // b, B, h, m, c, D, F, x * int d = not_a_day; // c, d, D, e, F, x * int j = not_a_doy; // j * int wd = not_a_weekday; // a, A, u, w * int H = not_a_hour; // c, H, R, T, X * int I = not_a_hour_12_value; // I, r * int p = not_a_ampm; // p, r * int M = not_a_minute; // c, M, r, R, T, X * Duration s = not_a_second; // c, r, S, T, X * int U = not_a_week_num; // U * int V = not_a_week_num; // V * int W = not_a_week_num; // W * std::basic_string temp_abbrev; // Z * minutes temp_offset = not_a_offset; // z * using detail::read; using detail::rs; using detail::ru; using detail::rld; using detail::checked_set; for (; *fmt != CharT{} && !is.fail(); ++fmt) { switch (*fmt) { case 'a': case 'A': case 'u': case 'w': // wd: a, A, u, w if (command) { int trial_wd = not_a_weekday; if (*fmt == 'a' || *fmt == 'A') { if (modified == CharT{}) { #if !ONLY_C_LOCALE ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); is.setstate(err); if (!is.fail()) trial_wd = tm.tm_wday; #else auto nm = detail::weekday_names(); auto i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; if (!is.fail()) trial_wd = i % 7; #endif } else read(is, CharT{'%'}, width, modified, *fmt); } else // *fmt == 'u' || *fmt == 'w' { #if !ONLY_C_LOCALE if (modified == CharT{}) #else if (modified != CharT{'E'}) #endif { read(is, ru{trial_wd, 1, width == -1 ? 1u : static_cast(width)}); if (!is.fail()) { if (*fmt == 'u') { if (!(1 <= trial_wd && trial_wd <= 7)) { trial_wd = not_a_weekday; is.setstate(ios::failbit); } else if (trial_wd == 7) trial_wd = 0; } else // *fmt == 'w' { if (!(0 <= trial_wd && trial_wd <= 6)) { trial_wd = not_a_weekday; is.setstate(ios::failbit); } } } } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); is.setstate(err); if (!is.fail()) trial_wd = tm.tm_wday; } #endif else read(is, CharT{'%'}, width, modified, *fmt); } if (trial_wd != not_a_weekday) checked_set(wd, trial_wd, not_a_weekday, is); } else // !command read(is, *fmt); command = nullptr; width = -1; modified = CharT{}; break; case 'b': case 'B': case 'h': if (command) { if (modified == CharT{}) { int ttm = not_a_month; #if !ONLY_C_LOCALE ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); if ((err & ios::failbit) == 0) ttm = tm.tm_mon + 1; is.setstate(err); #else auto nm = detail::month_names(); auto i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; if (!is.fail()) ttm = i % 12 + 1; #endif checked_set(m, ttm, not_a_month, is); } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'c': if (command) { if (modified != CharT{'O'}) { #if !ONLY_C_LOCALE ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); if ((err & ios::failbit) == 0) { checked_set(Y, tm.tm_year + 1900, not_a_year, is); checked_set(m, tm.tm_mon + 1, not_a_month, is); checked_set(d, tm.tm_mday, not_a_day, is); checked_set(H, tm.tm_hour, not_a_hour, is); checked_set(M, tm.tm_min, not_a_minute, is); checked_set(s, duration_cast(seconds{tm.tm_sec}), not_a_second, is); } is.setstate(err); #else // "%a %b %e %T %Y" auto nm = detail::weekday_names(); auto i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; checked_set(wd, static_cast(i % 7), not_a_weekday, is); ws(is); nm = detail::month_names(); i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; checked_set(m, static_cast(i % 12 + 1), not_a_month, is); ws(is); int td = not_a_day; read(is, ru{td, 1, 2}); checked_set(d, td, not_a_day, is); ws(is); using dfs = detail::decimal_format_seconds; CONSTDATA auto w = Duration::period::den == 1 ? 2 : 3 + dfs::width; int tH; int tM; long double S{}; read(is, ru{tH, 1, 2}, CharT{':'}, ru{tM, 1, 2}, CharT{':'}, rld{S, 1, w}); checked_set(H, tH, not_a_hour, is); checked_set(M, tM, not_a_minute, is); checked_set(s, round_i(duration{S}), not_a_second, is); ws(is); int tY = not_a_year; // No need for `rs` here, negative years can't parse // with "%c" since `width` is hardcoded to 4 read(is, ru{tY, 1, 4u}); checked_set(Y, tY, not_a_year, is); #endif } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'x': if (command) { if (modified != CharT{'O'}) { #if !ONLY_C_LOCALE ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); if ((err & ios::failbit) == 0) { checked_set(Y, tm.tm_year + 1900, not_a_year, is); checked_set(m, tm.tm_mon + 1, not_a_month, is); checked_set(d, tm.tm_mday, not_a_day, is); } is.setstate(err); #else // "%m/%d/%y" int ty = not_a_2digit_year; int tm = not_a_month; int td = not_a_day; read(is, ru{tm, 1, 2}, CharT{'/'}, ru{td, 1, 2}, CharT{'/'}, ru{ty, 1, 2}); checked_set(y, ty, not_a_2digit_year, is); checked_set(m, tm, not_a_month, is); checked_set(d, td, not_a_day, is); #endif } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'X': if (command) { if (modified != CharT{'O'}) { #if !ONLY_C_LOCALE ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); if ((err & ios::failbit) == 0) { checked_set(H, tm.tm_hour, not_a_hour, is); checked_set(M, tm.tm_min, not_a_minute, is); checked_set(s, duration_cast(seconds{tm.tm_sec}), not_a_second, is); } is.setstate(err); #else // "%T" using dfs = detail::decimal_format_seconds; CONSTDATA auto w = Duration::period::den == 1 ? 2 : 3 + dfs::width; int tH = not_a_hour; int tM = not_a_minute; long double S{}; read(is, ru{tH, 1, 2}, CharT{':'}, ru{tM, 1, 2}, CharT{':'}, rld{S, 1, w}); checked_set(H, tH, not_a_hour, is); checked_set(M, tM, not_a_minute, is); checked_set(s, round_i(duration{S}), not_a_second, is); #endif } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'C': if (command) { int tC = not_a_century; #if !ONLY_C_LOCALE if (modified == CharT{}) { #endif read(is, rs{tC, 1, width == -1 ? 2u : static_cast(width)}); #if !ONLY_C_LOCALE } else { ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); if ((err & ios::failbit) == 0) { auto tY = tm.tm_year + 1900; tC = (tY >= 0 ? tY : tY-99) / 100; } is.setstate(err); } #endif checked_set(C, tC, not_a_century, is); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'D': if (command) { if (modified == CharT{}) { int tn = not_a_month; int td = not_a_day; int ty = not_a_2digit_year; read(is, ru{tn, 1, 2}, CharT{'\0'}, CharT{'/'}, CharT{'\0'}, ru{td, 1, 2}, CharT{'\0'}, CharT{'/'}, CharT{'\0'}, ru{ty, 1, 2}); checked_set(y, ty, not_a_2digit_year, is); checked_set(m, tn, not_a_month, is); checked_set(d, td, not_a_day, is); } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'F': if (command) { if (modified == CharT{}) { int tY = not_a_year; int tn = not_a_month; int td = not_a_day; read(is, rs{tY, 1, width == -1 ? 4u : static_cast(width)}, CharT{'-'}, ru{tn, 1, 2}, CharT{'-'}, ru{td, 1, 2}); checked_set(Y, tY, not_a_year, is); checked_set(m, tn, not_a_month, is); checked_set(d, td, not_a_day, is); } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'd': case 'e': if (command) { #if !ONLY_C_LOCALE if (modified == CharT{}) #else if (modified != CharT{'E'}) #endif { int td = not_a_day; read(is, ru{td, 1, width == -1 ? 2u : static_cast(width)}); checked_set(d, td, not_a_day, is); } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); command = nullptr; width = -1; modified = CharT{}; if ((err & ios::failbit) == 0) checked_set(d, tm.tm_mday, not_a_day, is); is.setstate(err); } #endif else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'H': if (command) { #if !ONLY_C_LOCALE if (modified == CharT{}) #else if (modified != CharT{'E'}) #endif { int tH = not_a_hour; read(is, ru{tH, 1, width == -1 ? 2u : static_cast(width)}); checked_set(H, tH, not_a_hour, is); } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); if ((err & ios::failbit) == 0) checked_set(H, tm.tm_hour, not_a_hour, is); is.setstate(err); } #endif else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'I': if (command) { if (modified == CharT{}) { int tI = not_a_hour_12_value; // reads in an hour into I, but most be in [1, 12] read(is, ru{tI, 1, width == -1 ? 2u : static_cast(width)}); if (!(1 <= tI && tI <= 12)) is.setstate(ios::failbit); checked_set(I, tI, not_a_hour_12_value, is); } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'j': if (command) { if (modified == CharT{}) { int tj = not_a_doy; read(is, ru{tj, 1, width == -1 ? 3u : static_cast(width)}); checked_set(j, tj, not_a_doy, is); } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'M': if (command) { #if !ONLY_C_LOCALE if (modified == CharT{}) #else if (modified != CharT{'E'}) #endif { int tM = not_a_minute; read(is, ru{tM, 1, width == -1 ? 2u : static_cast(width)}); checked_set(M, tM, not_a_minute, is); } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); if ((err & ios::failbit) == 0) checked_set(M, tm.tm_min, not_a_minute, is); is.setstate(err); } #endif else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'm': if (command) { #if !ONLY_C_LOCALE if (modified == CharT{}) #else if (modified != CharT{'E'}) #endif { int tn = not_a_month; read(is, ru{tn, 1, width == -1 ? 2u : static_cast(width)}); checked_set(m, tn, not_a_month, is); } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); if ((err & ios::failbit) == 0) checked_set(m, tm.tm_mon + 1, not_a_month, is); is.setstate(err); } #endif else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'n': case 't': if (command) { if (modified == CharT{}) { // %n matches a single white space character // %t matches 0 or 1 white space characters auto ic = is.peek(); if (Traits::eq_int_type(ic, Traits::eof())) { ios::iostate err = ios::eofbit; if (*fmt == 'n') err |= ios::failbit; is.setstate(err); break; } if (isspace(ic)) { (void)is.get(); } else if (*fmt == 'n') is.setstate(ios::failbit); } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'p': if (command) { if (modified == CharT{}) { int tp = not_a_ampm; #if !ONLY_C_LOCALE tm = std::tm{}; tm.tm_hour = 1; ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); is.setstate(err); if (tm.tm_hour == 1) tp = 0; else if (tm.tm_hour == 13) tp = 1; else is.setstate(err); #else auto nm = detail::ampm_names(); auto i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; tp = static_cast(i); #endif checked_set(p, tp, not_a_ampm, is); } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'r': if (command) { if (modified == CharT{}) { #if !ONLY_C_LOCALE ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); if ((err & ios::failbit) == 0) { checked_set(H, tm.tm_hour, not_a_hour, is); checked_set(M, tm.tm_min, not_a_hour, is); checked_set(s, duration_cast(seconds{tm.tm_sec}), not_a_second, is); } is.setstate(err); #else // "%I:%M:%S %p" using dfs = detail::decimal_format_seconds; CONSTDATA auto w = Duration::period::den == 1 ? 2 : 3 + dfs::width; long double S{}; int tI = not_a_hour_12_value; int tM = not_a_minute; read(is, ru{tI, 1, 2}, CharT{':'}, ru{tM, 1, 2}, CharT{':'}, rld{S, 1, w}); checked_set(I, tI, not_a_hour_12_value, is); checked_set(M, tM, not_a_minute, is); checked_set(s, round_i(duration{S}), not_a_second, is); ws(is); auto nm = detail::ampm_names(); auto i = detail::scan_keyword(is, nm.first, nm.second) - nm.first; checked_set(p, static_cast(i), not_a_ampm, is); #endif } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'R': if (command) { if (modified == CharT{}) { int tH = not_a_hour; int tM = not_a_minute; read(is, ru{tH, 1, 2}, CharT{'\0'}, CharT{':'}, CharT{'\0'}, ru{tM, 1, 2}, CharT{'\0'}); checked_set(H, tH, not_a_hour, is); checked_set(M, tM, not_a_minute, is); } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'S': if (command) { #if !ONLY_C_LOCALE if (modified == CharT{}) #else if (modified != CharT{'E'}) #endif { using dfs = detail::decimal_format_seconds; CONSTDATA auto w = Duration::period::den == 1 ? 2 : 3 + dfs::width; long double S{}; read(is, rld{S, 1, width == -1 ? w : static_cast(width)}); checked_set(s, round_i(duration{S}), not_a_second, is); } #if !ONLY_C_LOCALE else if (modified == CharT{'O'}) { ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); if ((err & ios::failbit) == 0) checked_set(s, duration_cast(seconds{tm.tm_sec}), not_a_second, is); is.setstate(err); } #endif else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'T': if (command) { if (modified == CharT{}) { using dfs = detail::decimal_format_seconds; CONSTDATA auto w = Duration::period::den == 1 ? 2 : 3 + dfs::width; int tH = not_a_hour; int tM = not_a_minute; long double S{}; read(is, ru{tH, 1, 2}, CharT{':'}, ru{tM, 1, 2}, CharT{':'}, rld{S, 1, w}); checked_set(H, tH, not_a_hour, is); checked_set(M, tM, not_a_minute, is); checked_set(s, round_i(duration{S}), not_a_second, is); } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'Y': if (command) { #if !ONLY_C_LOCALE if (modified == CharT{}) #else if (modified != CharT{'O'}) #endif { int tY = not_a_year; read(is, rs{tY, 1, width == -1 ? 4u : static_cast(width)}); checked_set(Y, tY, not_a_year, is); } #if !ONLY_C_LOCALE else if (modified == CharT{'E'}) { ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); if ((err & ios::failbit) == 0) checked_set(Y, tm.tm_year + 1900, not_a_year, is); is.setstate(err); } #endif else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'y': if (command) { #if !ONLY_C_LOCALE if (modified == CharT{}) #endif { int ty = not_a_2digit_year; read(is, ru{ty, 1, width == -1 ? 2u : static_cast(width)}); checked_set(y, ty, not_a_2digit_year, is); } #if !ONLY_C_LOCALE else { ios::iostate err = ios::goodbit; f.get(is, nullptr, is, err, &tm, command, fmt+1); if ((err & ios::failbit) == 0) checked_set(Y, tm.tm_year + 1900, not_a_year, is); is.setstate(err); } #endif command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'g': if (command) { if (modified == CharT{}) { int tg = not_a_2digit_year; read(is, ru{tg, 1, width == -1 ? 2u : static_cast(width)}); checked_set(g, tg, not_a_2digit_year, is); } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'G': if (command) { if (modified == CharT{}) { int tG = not_a_year; read(is, rs{tG, 1, width == -1 ? 4u : static_cast(width)}); checked_set(G, tG, not_a_year, is); } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'U': if (command) { if (modified == CharT{}) { int tU = not_a_week_num; read(is, ru{tU, 1, width == -1 ? 2u : static_cast(width)}); checked_set(U, tU, not_a_week_num, is); } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'V': if (command) { if (modified == CharT{}) { int tV = not_a_week_num; read(is, ru{tV, 1, width == -1 ? 2u : static_cast(width)}); checked_set(V, tV, not_a_week_num, is); } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'W': if (command) { if (modified == CharT{}) { int tW = not_a_week_num; read(is, ru{tW, 1, width == -1 ? 2u : static_cast(width)}); checked_set(W, tW, not_a_week_num, is); } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'E': case 'O': if (command) { if (modified == CharT{}) { modified = *fmt; } else { read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } } else read(is, *fmt); break; case '%': if (command) { if (modified == CharT{}) read(is, *fmt); else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else command = fmt; break; case 'z': if (command) { int tH, tM; minutes toff = not_a_offset; bool neg = false; auto ic = is.peek(); if (!Traits::eq_int_type(ic, Traits::eof())) { auto c = static_cast(Traits::to_char_type(ic)); if (c == '-') { neg = true; (void)is.get(); } else if (c == '+') (void)is.get(); } if (modified == CharT{}) { read(is, ru{tH, 2, 2}); if (!is.fail()) toff = hours{std::abs(tH)}; if (is.good()) { ic = is.peek(); if (!Traits::eq_int_type(ic, Traits::eof())) { auto c = static_cast(Traits::to_char_type(ic)); if ('0' <= c && c <= '9') { read(is, ru{tM, 2, 2}); if (!is.fail()) toff += minutes{tM}; } } } } else { read(is, ru{tH, 1, 2}); if (!is.fail()) toff = hours{std::abs(tH)}; if (is.good()) { ic = is.peek(); if (!Traits::eq_int_type(ic, Traits::eof())) { auto c = static_cast(Traits::to_char_type(ic)); if (c == ':') { (void)is.get(); read(is, ru{tM, 2, 2}); if (!is.fail()) toff += minutes{tM}; } } } } if (neg && !is.fail()) toff = -toff; checked_set(temp_offset, toff, not_a_offset, is); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; case 'Z': if (command) { if (modified == CharT{}) { std::basic_string buf; while (is.rdstate() == std::ios::goodbit) { auto i = is.rdbuf()->sgetc(); if (Traits::eq_int_type(i, Traits::eof())) { is.setstate(ios::eofbit); break; } auto wc = Traits::to_char_type(i); auto c = static_cast(wc); // is c a valid time zone name or abbreviation character? if (!(CharT{1} < wc && wc < CharT{127}) || !(isalnum(c) || c == '_' || c == '/' || c == '-' || c == '+')) break; buf.push_back(c); is.rdbuf()->sbumpc(); } if (buf.empty()) is.setstate(ios::failbit); checked_set(temp_abbrev, buf, {}, is); } else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } else read(is, *fmt); break; default: if (command) { if (width == -1 && modified == CharT{} && '0' <= *fmt && *fmt <= '9') { width = static_cast(*fmt) - '0'; while ('0' <= fmt[1] && fmt[1] <= '9') width = 10*width + static_cast(*++fmt) - '0'; } else { if (modified == CharT{}) read(is, CharT{'%'}, width, *fmt); else read(is, CharT{'%'}, width, modified, *fmt); command = nullptr; width = -1; modified = CharT{}; } } else // !command { if (isspace(static_cast(*fmt))) { // space matches 0 or more white space characters if (is.good()) ws(is); } else read(is, *fmt); } break; } } // is.fail() || *fmt == CharT{} if (is.rdstate() == ios::goodbit && command) { if (modified == CharT{}) read(is, CharT{'%'}, width); else read(is, CharT{'%'}, width, modified); } if (!is.fail()) { if (y != not_a_2digit_year) { // Convert y and an optional C to Y if (!(0 <= y && y <= 99)) goto broken; if (C == not_a_century) { if (Y == not_a_year) { if (y >= 69) C = 19; else C = 20; } else { C = (Y >= 0 ? Y : Y-100) / 100; } } int tY; if (C >= 0) tY = 100*C + y; else tY = 100*(C+1) - (y == 0 ? 100 : y); if (Y != not_a_year && Y != tY) goto broken; Y = tY; } if (g != not_a_2digit_year) { // Convert g and an optional C to G if (!(0 <= g && g <= 99)) goto broken; if (C == not_a_century) { if (G == not_a_year) { if (g >= 69) C = 19; else C = 20; } else { C = (G >= 0 ? G : G-100) / 100; } } int tG; if (C >= 0) tG = 100*C + g; else tG = 100*(C+1) - (g == 0 ? 100 : g); if (G != not_a_year && G != tG) goto broken; G = tG; } if (Y < static_cast(year::min()) || Y > static_cast(year::max())) Y = not_a_year; bool computed = false; if (G != not_a_year && V != not_a_week_num && wd != not_a_weekday) { year_month_day ymd_trial = sys_days(year{G-1}/December/Thursday[last]) + (Monday-Thursday) + weeks{V-1} + (weekday{static_cast(wd)}-Monday); if (Y == not_a_year) Y = static_cast(ymd_trial.year()); else if (year{Y} != ymd_trial.year()) goto broken; if (m == not_a_month) m = static_cast(static_cast(ymd_trial.month())); else if (month(static_cast(m)) != ymd_trial.month()) goto broken; if (d == not_a_day) d = static_cast(static_cast(ymd_trial.day())); else if (day(static_cast(d)) != ymd_trial.day()) goto broken; computed = true; } if (Y != not_a_year && U != not_a_week_num && wd != not_a_weekday) { year_month_day ymd_trial = sys_days(year{Y}/January/Sunday[1]) + weeks{U-1} + (weekday{static_cast(wd)} - Sunday); if (year{Y} != ymd_trial.year()) goto broken; if (m == not_a_month) m = static_cast(static_cast(ymd_trial.month())); else if (month(static_cast(m)) != ymd_trial.month()) goto broken; if (d == not_a_day) d = static_cast(static_cast(ymd_trial.day())); else if (day(static_cast(d)) != ymd_trial.day()) goto broken; computed = true; } if (Y != not_a_year && W != not_a_week_num && wd != not_a_weekday) { year_month_day ymd_trial = sys_days(year{Y}/January/Monday[1]) + weeks{W-1} + (weekday{static_cast(wd)} - Monday); if (year{Y} != ymd_trial.year()) goto broken; if (m == not_a_month) m = static_cast(static_cast(ymd_trial.month())); else if (month(static_cast(m)) != ymd_trial.month()) goto broken; if (d == not_a_day) d = static_cast(static_cast(ymd_trial.day())); else if (day(static_cast(d)) != ymd_trial.day()) goto broken; computed = true; } if (j != not_a_doy && Y != not_a_year) { auto ymd_trial = year_month_day{local_days(year{Y}/1/1) + days{j-1}}; if (m == not_a_month) m = static_cast(static_cast(ymd_trial.month())); else if (month(static_cast(m)) != ymd_trial.month()) goto broken; if (d == not_a_day) d = static_cast(static_cast(ymd_trial.day())); else if (day(static_cast(d)) != ymd_trial.day()) goto broken; j = not_a_doy; } auto ymd = year{Y}/m/d; if (ymd.ok()) { if (wd == not_a_weekday) wd = static_cast((weekday(sys_days(ymd)) - Sunday).count()); else if (wd != static_cast((weekday(sys_days(ymd)) - Sunday).count())) goto broken; if (!computed) { if (G != not_a_year || V != not_a_week_num) { sys_days sd = ymd; auto G_trial = year_month_day{sd + days{3}}.year(); auto start = sys_days((G_trial - years{1})/December/Thursday[last]) + (Monday - Thursday); if (sd < start) { --G_trial; if (V != not_a_week_num) start = sys_days((G_trial - years{1})/December/Thursday[last]) + (Monday - Thursday); } if (G != not_a_year && G != static_cast(G_trial)) goto broken; if (V != not_a_week_num) { auto V_trial = duration_cast(sd - start).count() + 1; if (V != V_trial) goto broken; } } if (U != not_a_week_num) { auto start = sys_days(Sunday[1]/January/ymd.year()); auto U_trial = floor(sys_days(ymd) - start).count() + 1; if (U != U_trial) goto broken; } if (W != not_a_week_num) { auto start = sys_days(Monday[1]/January/ymd.year()); auto W_trial = floor(sys_days(ymd) - start).count() + 1; if (W != W_trial) goto broken; } } } fds.ymd = ymd; if (I != not_a_hour_12_value) { if (!(1 <= I && I <= 12)) goto broken; if (p != not_a_ampm) { // p is in [0, 1] == [AM, PM] // Store trial H in I if (I == 12) --p; I += p*12; // Either set H from I or make sure H and I are consistent if (H == not_a_hour) H = I; else if (I != H) goto broken; } else // p == not_a_ampm { // if H, make sure H and I could be consistent if (H != not_a_hour) { if (I == 12) { if (H != 0 && H != 12) goto broken; } else if (!(I == H || I == H+12)) { goto broken; } } else // I is ambiguous, AM or PM? goto broken; } } if (H != not_a_hour) { fds.has_tod = true; fds.tod = hh_mm_ss{hours{H}}; } if (M != not_a_minute) { fds.has_tod = true; fds.tod.m_ = minutes{M}; } if (s != not_a_second) { fds.has_tod = true; fds.tod.s_ = detail::decimal_format_seconds{s}; } if (j != not_a_doy) { fds.has_tod = true; fds.tod.h_ += hours{days{j}}; } if (wd != not_a_weekday) fds.wd = weekday{static_cast(wd)}; if (abbrev != nullptr) *abbrev = std::move(temp_abbrev); if (offset != nullptr && temp_offset != not_a_offset) *offset = temp_offset; } return is; } broken: is.setstate(ios::failbit); return is; } template > std::basic_istream& from_stream(std::basic_istream& is, const CharT* fmt, year& y, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) { using CT = std::chrono::seconds; fields fds{}; date::from_stream(is, fmt, fds, abbrev, offset); if (!fds.ymd.year().ok()) is.setstate(std::ios::failbit); if (!is.fail()) y = fds.ymd.year(); return is; } template > std::basic_istream& from_stream(std::basic_istream& is, const CharT* fmt, month& m, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) { using CT = std::chrono::seconds; fields fds{}; date::from_stream(is, fmt, fds, abbrev, offset); if (!fds.ymd.month().ok()) is.setstate(std::ios::failbit); if (!is.fail()) m = fds.ymd.month(); return is; } template > std::basic_istream& from_stream(std::basic_istream& is, const CharT* fmt, day& d, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) { using CT = std::chrono::seconds; fields fds{}; date::from_stream(is, fmt, fds, abbrev, offset); if (!fds.ymd.day().ok()) is.setstate(std::ios::failbit); if (!is.fail()) d = fds.ymd.day(); return is; } template > std::basic_istream& from_stream(std::basic_istream& is, const CharT* fmt, weekday& wd, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) { using CT = std::chrono::seconds; fields fds{}; date::from_stream(is, fmt, fds, abbrev, offset); if (!fds.wd.ok()) is.setstate(std::ios::failbit); if (!is.fail()) wd = fds.wd; return is; } template > std::basic_istream& from_stream(std::basic_istream& is, const CharT* fmt, year_month& ym, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) { using CT = std::chrono::seconds; fields fds{}; date::from_stream(is, fmt, fds, abbrev, offset); if (!fds.ymd.month().ok()) is.setstate(std::ios::failbit); if (!is.fail()) ym = fds.ymd.year()/fds.ymd.month(); return is; } template > std::basic_istream& from_stream(std::basic_istream& is, const CharT* fmt, month_day& md, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) { using CT = std::chrono::seconds; fields fds{}; date::from_stream(is, fmt, fds, abbrev, offset); if (!fds.ymd.month().ok() || !fds.ymd.day().ok()) is.setstate(std::ios::failbit); if (!is.fail()) md = fds.ymd.month()/fds.ymd.day(); return is; } template > std::basic_istream& from_stream(std::basic_istream& is, const CharT* fmt, year_month_day& ymd, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) { using CT = std::chrono::seconds; fields fds{}; date::from_stream(is, fmt, fds, abbrev, offset); if (!fds.ymd.ok()) is.setstate(std::ios::failbit); if (!is.fail()) ymd = fds.ymd; return is; } template > std::basic_istream& from_stream(std::basic_istream& is, const CharT* fmt, sys_time& tp, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) { using CT = typename std::common_type::type; using detail::round_i; std::chrono::minutes offset_local{}; auto offptr = offset ? offset : &offset_local; fields fds{}; fds.has_tod = true; date::from_stream(is, fmt, fds, abbrev, offptr); if (!fds.ymd.ok() || !fds.tod.in_conventional_range()) is.setstate(std::ios::failbit); if (!is.fail()) tp = round_i(sys_days(fds.ymd) - *offptr + fds.tod.to_duration()); return is; } template > std::basic_istream& from_stream(std::basic_istream& is, const CharT* fmt, local_time& tp, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) { using CT = typename std::common_type::type; using detail::round_i; fields fds{}; fds.has_tod = true; date::from_stream(is, fmt, fds, abbrev, offset); if (!fds.ymd.ok() || !fds.tod.in_conventional_range()) is.setstate(std::ios::failbit); if (!is.fail()) tp = round_i(local_seconds{local_days(fds.ymd)} + fds.tod.to_duration()); return is; } template > std::basic_istream& from_stream(std::basic_istream& is, const CharT* fmt, std::chrono::duration& d, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) { using Duration = std::chrono::duration; using CT = typename std::common_type::type; using detail::round_i; fields fds{}; date::from_stream(is, fmt, fds, abbrev, offset); if (!fds.has_tod) is.setstate(std::ios::failbit); if (!is.fail()) d = round_i(fds.tod.to_duration()); return is; } template , class Alloc = std::allocator> struct parse_manip { const std::basic_string format_; Parsable& tp_; std::basic_string* abbrev_; std::chrono::minutes* offset_; public: parse_manip(std::basic_string format, Parsable& tp, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) : format_(std::move(format)) , tp_(tp) , abbrev_(abbrev) , offset_(offset) {} #if HAS_STRING_VIEW parse_manip(const CharT* format, Parsable& tp, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) : format_(format) , tp_(tp) , abbrev_(abbrev) , offset_(offset) {} parse_manip(std::basic_string_view format, Parsable& tp, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) : format_(format) , tp_(tp) , abbrev_(abbrev) , offset_(offset) {} #endif // HAS_STRING_VIEW }; #ifdef _MSC_VER template std::basic_istream& operator>>(std::basic_istream& is, const parse_manip& x) { return date::from_stream(is, x.format_.c_str(), x.tp_, x.abbrev_, x.offset_); } template inline auto parse(const std::basic_string& format, Parsable& tp) -> decltype(date::from_stream(std::declval&>(), format.c_str(), tp), parse_manip{format, tp}) { return {format, tp}; } template inline auto parse(const std::basic_string& format, Parsable& tp, std::basic_string& abbrev) -> decltype(date::from_stream(std::declval&>(), format.c_str(), tp, &abbrev), parse_manip{format, tp, &abbrev}) { return {format, tp, &abbrev}; } template inline auto parse(const std::basic_string& format, Parsable& tp, std::chrono::minutes& offset) -> decltype(date::from_stream(std::declval&>(), format.c_str(), tp, std::declval*>(), &offset), parse_manip{format, tp, nullptr, &offset}) { return {format, tp, nullptr, &offset}; } template inline auto parse(const std::basic_string& format, Parsable& tp, std::basic_string& abbrev, std::chrono::minutes& offset) -> decltype(date::from_stream(std::declval&>(), format.c_str(), tp, &abbrev, &offset), parse_manip{format, tp, &abbrev, &offset}) { return {format, tp, &abbrev, &offset}; } // const CharT* formats template inline auto parse(const CharT* format, Parsable& tp) -> decltype(date::from_stream(std::declval&>(), format, tp), parse_manip{format, tp}) { return {format, tp}; } template inline auto parse(const CharT* format, Parsable& tp, std::basic_string& abbrev) -> decltype(date::from_stream(std::declval&>(), format, tp, &abbrev), parse_manip{format, tp, &abbrev}) { return {format, tp, &abbrev}; } template inline auto parse(const CharT* format, Parsable& tp, std::chrono::minutes& offset) -> decltype(date::from_stream(std::declval&>(), format, tp, std::declval*>(), &offset), parse_manip{format, tp, nullptr, &offset}) { return {format, tp, nullptr, &offset}; } template inline auto parse(const CharT* format, Parsable& tp, std::basic_string& abbrev, std::chrono::minutes& offset) -> decltype(date::from_stream(std::declval&>(), format, tp, &abbrev, &offset), parse_manip{format, tp, &abbrev, &offset}) { return {format, tp, &abbrev, &offset}; } #else // !defined _MSC_VER template std::basic_istream& operator>>(std::basic_istream& is, const parse_manip& x) { return from_stream(is, x.format_.c_str(), x.tp_, x.abbrev_, x.offset_); } template inline auto parse(const std::basic_string& format, Parsable& tp) -> decltype(from_stream(std::declval&>(), format.c_str(), tp), parse_manip{format, tp}) { return {format, tp}; } template inline auto parse(const std::basic_string& format, Parsable& tp, std::basic_string& abbrev) -> decltype(from_stream(std::declval&>(), format.c_str(), tp, &abbrev), parse_manip{format, tp, &abbrev}) { return {format, tp, &abbrev}; } template inline auto parse(const std::basic_string& format, Parsable& tp, std::chrono::minutes& offset) -> decltype(from_stream(std::declval&>(), format.c_str(), tp, std::declval*>(), &offset), parse_manip{format, tp, nullptr, &offset}) { return {format, tp, nullptr, &offset}; } template inline auto parse(const std::basic_string& format, Parsable& tp, std::basic_string& abbrev, std::chrono::minutes& offset) -> decltype(from_stream(std::declval&>(), format.c_str(), tp, &abbrev, &offset), parse_manip{format, tp, &abbrev, &offset}) { return {format, tp, &abbrev, &offset}; } // const CharT* formats template inline auto parse(const CharT* format, Parsable& tp) -> decltype(from_stream(std::declval&>(), format, tp), parse_manip{format, tp}) { return {format, tp}; } template inline auto parse(const CharT* format, Parsable& tp, std::basic_string& abbrev) -> decltype(from_stream(std::declval&>(), format, tp, &abbrev), parse_manip{format, tp, &abbrev}) { return {format, tp, &abbrev}; } template inline auto parse(const CharT* format, Parsable& tp, std::chrono::minutes& offset) -> decltype(from_stream(std::declval&>(), format, tp, std::declval*>(), &offset), parse_manip{format, tp, nullptr, &offset}) { return {format, tp, nullptr, &offset}; } template inline auto parse(const CharT* format, Parsable& tp, std::basic_string& abbrev, std::chrono::minutes& offset) -> decltype(from_stream(std::declval&>(), format, tp, &abbrev, &offset), parse_manip{format, tp, &abbrev, &offset}) { return {format, tp, &abbrev, &offset}; } #endif // !defined _MSC_VER // duration streaming template inline std::basic_ostream& operator<<(std::basic_ostream& os, const std::chrono::duration& d) { return os << detail::make_string::from(d.count()) + detail::get_units(typename Period::type{}); } } // namespace date #ifdef _MSC_VER # pragma warning(pop) #endif #ifdef __GNUC__ # pragma GCC diagnostic pop #endif #endif // DATE_H pr0m1th3as-datatypes-9450b29/src/date/ios.h000066400000000000000000000031111527370255000203410ustar00rootroot00000000000000// // ios.h // DateTimeLib // // The MIT License (MIT) // // Copyright (c) 2016 Alexander Kormanovsky // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #ifndef ios_hpp #define ios_hpp #if __APPLE__ # include # if TARGET_OS_IPHONE # include namespace date { namespace iOSUtils { std::string get_tzdata_path(); std::string get_current_timezone(); } // namespace iOSUtils } // namespace date # endif // TARGET_OS_IPHONE #else // !__APPLE__ # define TARGET_OS_IPHONE 0 #endif // !__APPLE__ #endif // ios_hpp pr0m1th3as-datatypes-9450b29/src/date/islamic.h000066400000000000000000002127131527370255000212020ustar00rootroot00000000000000#ifndef ISLAMIC_H #define ISLAMIC_H // The MIT License (MIT) // // Copyright (c) 2016 Howard Hinnant // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // Our apologies. When the previous paragraph was written, lowercase had not yet // been invented (that would involve another several millennia of evolution). // We did not mean to shout. #include "date.h" namespace islamic { // durations using days = date::days; using weeks = date::weeks; using years = std::chrono::duration , days::period>>; using months = std::chrono::duration >>; // time_point using sys_days = date::sys_days; using local_days = date::local_days; // types struct last_spec { explicit last_spec() = default; }; class day; class month; class year; class weekday; class weekday_indexed; class weekday_last; class month_day; class month_day_last; class month_weekday; class month_weekday_last; class year_month; class year_month_day; class year_month_day_last; class year_month_weekday; class year_month_weekday_last; // date composition operators CONSTCD11 year_month operator/(const year& y, const month& m) NOEXCEPT; CONSTCD11 year_month operator/(const year& y, int m) NOEXCEPT; CONSTCD11 month_day operator/(const day& d, const month& m) NOEXCEPT; CONSTCD11 month_day operator/(const day& d, int m) NOEXCEPT; CONSTCD11 month_day operator/(const month& m, const day& d) NOEXCEPT; CONSTCD11 month_day operator/(const month& m, int d) NOEXCEPT; CONSTCD11 month_day operator/(int m, const day& d) NOEXCEPT; CONSTCD11 month_day_last operator/(const month& m, last_spec) NOEXCEPT; CONSTCD11 month_day_last operator/(int m, last_spec) NOEXCEPT; CONSTCD11 month_day_last operator/(last_spec, const month& m) NOEXCEPT; CONSTCD11 month_day_last operator/(last_spec, int m) NOEXCEPT; CONSTCD11 month_weekday operator/(const month& m, const weekday_indexed& wdi) NOEXCEPT; CONSTCD11 month_weekday operator/(int m, const weekday_indexed& wdi) NOEXCEPT; CONSTCD11 month_weekday operator/(const weekday_indexed& wdi, const month& m) NOEXCEPT; CONSTCD11 month_weekday operator/(const weekday_indexed& wdi, int m) NOEXCEPT; CONSTCD11 month_weekday_last operator/(const month& m, const weekday_last& wdl) NOEXCEPT; CONSTCD11 month_weekday_last operator/(int m, const weekday_last& wdl) NOEXCEPT; CONSTCD11 month_weekday_last operator/(const weekday_last& wdl, const month& m) NOEXCEPT; CONSTCD11 month_weekday_last operator/(const weekday_last& wdl, int m) NOEXCEPT; CONSTCD11 year_month_day operator/(const year_month& ym, const day& d) NOEXCEPT; CONSTCD11 year_month_day operator/(const year_month& ym, int d) NOEXCEPT; CONSTCD11 year_month_day operator/(const year& y, const month_day& md) NOEXCEPT; CONSTCD11 year_month_day operator/(int y, const month_day& md) NOEXCEPT; CONSTCD11 year_month_day operator/(const month_day& md, const year& y) NOEXCEPT; CONSTCD11 year_month_day operator/(const month_day& md, int y) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const year_month& ym, last_spec) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const year& y, const month_day_last& mdl) NOEXCEPT; CONSTCD11 year_month_day_last operator/(int y, const month_day_last& mdl) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const month_day_last& mdl, const year& y) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const month_day_last& mdl, int y) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const year_month& ym, const weekday_indexed& wdi) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const year& y, const month_weekday& mwd) NOEXCEPT; CONSTCD11 year_month_weekday operator/(int y, const month_weekday& mwd) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const month_weekday& mwd, const year& y) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const month_weekday& mwd, int y) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const year_month& ym, const weekday_last& wdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const year& y, const month_weekday_last& mwdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(int y, const month_weekday_last& mwdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const month_weekday_last& mwdl, const year& y) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const month_weekday_last& mwdl, int y) NOEXCEPT; // Detailed interface // day class day { unsigned char d_; public: explicit CONSTCD11 day(unsigned d) NOEXCEPT; CONSTCD14 day& operator++() NOEXCEPT; CONSTCD14 day operator++(int) NOEXCEPT; CONSTCD14 day& operator--() NOEXCEPT; CONSTCD14 day operator--(int) NOEXCEPT; CONSTCD14 day& operator+=(const days& d) NOEXCEPT; CONSTCD14 day& operator-=(const days& d) NOEXCEPT; CONSTCD11 explicit operator unsigned() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator!=(const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator< (const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator> (const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator<=(const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator>=(const day& x, const day& y) NOEXCEPT; CONSTCD11 day operator+(const day& x, const days& y) NOEXCEPT; CONSTCD11 day operator+(const days& x, const day& y) NOEXCEPT; CONSTCD11 day operator-(const day& x, const days& y) NOEXCEPT; CONSTCD11 days operator-(const day& x, const day& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const day& d); // month class month { unsigned char m_; public: explicit CONSTCD11 month(unsigned m) NOEXCEPT; CONSTCD14 month& operator++() NOEXCEPT; CONSTCD14 month operator++(int) NOEXCEPT; CONSTCD14 month& operator--() NOEXCEPT; CONSTCD14 month operator--(int) NOEXCEPT; CONSTCD14 month& operator+=(const months& m) NOEXCEPT; CONSTCD14 month& operator-=(const months& m) NOEXCEPT; CONSTCD11 explicit operator unsigned() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator!=(const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator< (const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator> (const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator<=(const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator>=(const month& x, const month& y) NOEXCEPT; CONSTCD14 month operator+(const month& x, const months& y) NOEXCEPT; CONSTCD14 month operator+(const months& x, const month& y) NOEXCEPT; CONSTCD14 month operator-(const month& x, const months& y) NOEXCEPT; CONSTCD14 months operator-(const month& x, const month& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month& m); // year class year { short y_; public: explicit CONSTCD11 year(int y) NOEXCEPT; CONSTCD14 year& operator++() NOEXCEPT; CONSTCD14 year operator++(int) NOEXCEPT; CONSTCD14 year& operator--() NOEXCEPT; CONSTCD14 year operator--(int) NOEXCEPT; CONSTCD14 year& operator+=(const years& y) NOEXCEPT; CONSTCD14 year& operator-=(const years& y) NOEXCEPT; CONSTCD14 bool is_leap() const NOEXCEPT; CONSTCD11 explicit operator int() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; static CONSTCD11 year min() NOEXCEPT; static CONSTCD11 year max() NOEXCEPT; }; CONSTCD11 bool operator==(const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator!=(const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator< (const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator> (const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator<=(const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator>=(const year& x, const year& y) NOEXCEPT; CONSTCD11 year operator+(const year& x, const years& y) NOEXCEPT; CONSTCD11 year operator+(const years& x, const year& y) NOEXCEPT; CONSTCD11 year operator-(const year& x, const years& y) NOEXCEPT; CONSTCD11 years operator-(const year& x, const year& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year& y); // weekday class weekday { unsigned char wd_; public: explicit CONSTCD11 weekday(unsigned wd) NOEXCEPT; explicit weekday(int) = delete; CONSTCD11 weekday(const sys_days& dp) NOEXCEPT; CONSTCD11 explicit weekday(const local_days& dp) NOEXCEPT; CONSTCD14 weekday& operator++() NOEXCEPT; CONSTCD14 weekday operator++(int) NOEXCEPT; CONSTCD14 weekday& operator--() NOEXCEPT; CONSTCD14 weekday operator--(int) NOEXCEPT; CONSTCD14 weekday& operator+=(const days& d) NOEXCEPT; CONSTCD14 weekday& operator-=(const days& d) NOEXCEPT; CONSTCD11 explicit operator unsigned() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; CONSTCD11 weekday_indexed operator[](unsigned index) const NOEXCEPT; CONSTCD11 weekday_last operator[](last_spec) const NOEXCEPT; private: static CONSTCD11 unsigned char weekday_from_days(int z) NOEXCEPT; }; CONSTCD11 bool operator==(const weekday& x, const weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const weekday& x, const weekday& y) NOEXCEPT; CONSTCD14 weekday operator+(const weekday& x, const days& y) NOEXCEPT; CONSTCD14 weekday operator+(const days& x, const weekday& y) NOEXCEPT; CONSTCD14 weekday operator-(const weekday& x, const days& y) NOEXCEPT; CONSTCD14 days operator-(const weekday& x, const weekday& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const weekday& wd); // weekday_indexed class weekday_indexed { unsigned char wd_ : 4; unsigned char index_ : 4; public: CONSTCD11 weekday_indexed(const islamic::weekday& wd, unsigned index) NOEXCEPT; CONSTCD11 islamic::weekday weekday() const NOEXCEPT; CONSTCD11 unsigned index() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT; CONSTCD11 bool operator!=(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_indexed& wdi); // weekday_last class weekday_last { islamic::weekday wd_; public: explicit CONSTCD11 weekday_last(const islamic::weekday& wd) NOEXCEPT; CONSTCD11 islamic::weekday weekday() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const weekday_last& x, const weekday_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const weekday_last& x, const weekday_last& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_last& wdl); // year_month class year_month { islamic::year y_; islamic::month m_; public: CONSTCD11 year_month(const islamic::year& y, const islamic::month& m) NOEXCEPT; CONSTCD11 islamic::year year() const NOEXCEPT; CONSTCD11 islamic::month month() const NOEXCEPT; CONSTCD14 year_month& operator+=(const months& dm) NOEXCEPT; CONSTCD14 year_month& operator-=(const months& dm) NOEXCEPT; CONSTCD14 year_month& operator+=(const years& dy) NOEXCEPT; CONSTCD14 year_month& operator-=(const years& dy) NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator< (const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator> (const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD14 year_month operator+(const year_month& ym, const months& dm) NOEXCEPT; CONSTCD14 year_month operator+(const months& dm, const year_month& ym) NOEXCEPT; CONSTCD14 year_month operator-(const year_month& ym, const months& dm) NOEXCEPT; CONSTCD11 months operator-(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 year_month operator+(const year_month& ym, const years& dy) NOEXCEPT; CONSTCD11 year_month operator+(const years& dy, const year_month& ym) NOEXCEPT; CONSTCD11 year_month operator-(const year_month& ym, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month& ym); // month_day class month_day { islamic::month m_; islamic::day d_; public: CONSTCD11 month_day(const islamic::month& m, const islamic::day& d) NOEXCEPT; CONSTCD11 islamic::month month() const NOEXCEPT; CONSTCD11 islamic::day day() const NOEXCEPT; CONSTCD14 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator< (const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator> (const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator<=(const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator>=(const month_day& x, const month_day& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_day& md); // month_day_last class month_day_last { islamic::month m_; public: CONSTCD11 explicit month_day_last(const islamic::month& m) NOEXCEPT; CONSTCD11 islamic::month month() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator< (const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator> (const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator<=(const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator>=(const month_day_last& x, const month_day_last& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_day_last& mdl); // month_weekday class month_weekday { islamic::month m_; islamic::weekday_indexed wdi_; public: CONSTCD11 month_weekday(const islamic::month& m, const islamic::weekday_indexed& wdi) NOEXCEPT; CONSTCD11 islamic::month month() const NOEXCEPT; CONSTCD11 islamic::weekday_indexed weekday_indexed() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_weekday& x, const month_weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_weekday& x, const month_weekday& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday& mwd); // month_weekday_last class month_weekday_last { islamic::month m_; islamic::weekday_last wdl_; public: CONSTCD11 month_weekday_last(const islamic::month& m, const islamic::weekday_last& wd) NOEXCEPT; CONSTCD11 islamic::month month() const NOEXCEPT; CONSTCD11 islamic::weekday_last weekday_last() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday_last& mwdl); // class year_month_day class year_month_day { islamic::year y_; islamic::month m_; islamic::day d_; public: CONSTCD11 year_month_day(const islamic::year& y, const islamic::month& m, const islamic::day& d) NOEXCEPT; CONSTCD14 year_month_day(const year_month_day_last& ymdl) NOEXCEPT; CONSTCD14 year_month_day(sys_days dp) NOEXCEPT; CONSTCD14 explicit year_month_day(local_days dp) NOEXCEPT; CONSTCD14 year_month_day& operator+=(const months& m) NOEXCEPT; CONSTCD14 year_month_day& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_day& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_day& operator-=(const years& y) NOEXCEPT; CONSTCD11 islamic::year year() const NOEXCEPT; CONSTCD11 islamic::month month() const NOEXCEPT; CONSTCD11 islamic::day day() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD14 bool ok() const NOEXCEPT; private: static CONSTCD14 year_month_day from_days(days dp) NOEXCEPT; CONSTCD14 days to_days() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator< (const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator> (const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD14 year_month_day operator+(const year_month_day& ymd, const months& dm) NOEXCEPT; CONSTCD14 year_month_day operator+(const months& dm, const year_month_day& ymd) NOEXCEPT; CONSTCD14 year_month_day operator-(const year_month_day& ymd, const months& dm) NOEXCEPT; CONSTCD11 year_month_day operator+(const year_month_day& ymd, const years& dy) NOEXCEPT; CONSTCD11 year_month_day operator+(const years& dy, const year_month_day& ymd) NOEXCEPT; CONSTCD11 year_month_day operator-(const year_month_day& ymd, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day& ymd); // year_month_day_last class year_month_day_last { islamic::year y_; islamic::month_day_last mdl_; public: CONSTCD11 year_month_day_last(const islamic::year& y, const islamic::month_day_last& mdl) NOEXCEPT; CONSTCD14 year_month_day_last& operator+=(const months& m) NOEXCEPT; CONSTCD14 year_month_day_last& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_day_last& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_day_last& operator-=(const years& y) NOEXCEPT; CONSTCD11 islamic::year year() const NOEXCEPT; CONSTCD11 islamic::month month() const NOEXCEPT; CONSTCD11 islamic::month_day_last month_day_last() const NOEXCEPT; CONSTCD14 islamic::day day() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator< (const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator> (const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD14 year_month_day_last operator+(const year_month_day_last& ymdl, const months& dm) NOEXCEPT; CONSTCD14 year_month_day_last operator+(const months& dm, const year_month_day_last& ymdl) NOEXCEPT; CONSTCD11 year_month_day_last operator+(const year_month_day_last& ymdl, const years& dy) NOEXCEPT; CONSTCD11 year_month_day_last operator+(const years& dy, const year_month_day_last& ymdl) NOEXCEPT; CONSTCD14 year_month_day_last operator-(const year_month_day_last& ymdl, const months& dm) NOEXCEPT; CONSTCD11 year_month_day_last operator-(const year_month_day_last& ymdl, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day_last& ymdl); // year_month_weekday class year_month_weekday { islamic::year y_; islamic::month m_; islamic::weekday_indexed wdi_; public: CONSTCD11 year_month_weekday(const islamic::year& y, const islamic::month& m, const islamic::weekday_indexed& wdi) NOEXCEPT; CONSTCD14 year_month_weekday(const sys_days& dp) NOEXCEPT; CONSTCD14 explicit year_month_weekday(const local_days& dp) NOEXCEPT; CONSTCD14 year_month_weekday& operator+=(const months& m) NOEXCEPT; CONSTCD14 year_month_weekday& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_weekday& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_weekday& operator-=(const years& y) NOEXCEPT; CONSTCD11 islamic::year year() const NOEXCEPT; CONSTCD11 islamic::month month() const NOEXCEPT; CONSTCD11 islamic::weekday weekday() const NOEXCEPT; CONSTCD11 unsigned index() const NOEXCEPT; CONSTCD11 islamic::weekday_indexed weekday_indexed() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD14 bool ok() const NOEXCEPT; private: static CONSTCD14 year_month_weekday from_days(days dp) NOEXCEPT; CONSTCD14 days to_days() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT; CONSTCD14 year_month_weekday operator+(const year_month_weekday& ymwd, const months& dm) NOEXCEPT; CONSTCD14 year_month_weekday operator+(const months& dm, const year_month_weekday& ymwd) NOEXCEPT; CONSTCD11 year_month_weekday operator+(const year_month_weekday& ymwd, const years& dy) NOEXCEPT; CONSTCD11 year_month_weekday operator+(const years& dy, const year_month_weekday& ymwd) NOEXCEPT; CONSTCD14 year_month_weekday operator-(const year_month_weekday& ymwd, const months& dm) NOEXCEPT; CONSTCD11 year_month_weekday operator-(const year_month_weekday& ymwd, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday& ymwdi); // year_month_weekday_last class year_month_weekday_last { islamic::year y_; islamic::month m_; islamic::weekday_last wdl_; public: CONSTCD11 year_month_weekday_last(const islamic::year& y, const islamic::month& m, const islamic::weekday_last& wdl) NOEXCEPT; CONSTCD14 year_month_weekday_last& operator+=(const months& m) NOEXCEPT; CONSTCD14 year_month_weekday_last& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_weekday_last& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_weekday_last& operator-=(const years& y) NOEXCEPT; CONSTCD11 islamic::year year() const NOEXCEPT; CONSTCD11 islamic::month month() const NOEXCEPT; CONSTCD11 islamic::weekday weekday() const NOEXCEPT; CONSTCD11 islamic::weekday_last weekday_last() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; private: CONSTCD14 days to_days() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT; CONSTCD14 year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT; CONSTCD14 year_month_weekday_last operator+(const months& dm, const year_month_weekday_last& ymwdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT; CONSTCD11 year_month_weekday_last operator+(const years& dy, const year_month_weekday_last& ymwdl) NOEXCEPT; CONSTCD14 year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT; CONSTCD11 year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday_last& ymwdl); #if !defined(_MSC_VER) || (_MSC_VER >= 1900) inline namespace literals { CONSTCD11 islamic::day operator ""_d(unsigned long long d) NOEXCEPT; CONSTCD11 islamic::year operator ""_y(unsigned long long y) NOEXCEPT; } // inline namespace literals #endif // !defined(_MSC_VER) || (_MSC_VER >= 1900) //----------------+ // Implementation | //----------------+ // day CONSTCD11 inline day::day(unsigned d) NOEXCEPT : d_(static_cast(d)) {} CONSTCD14 inline day& day::operator++() NOEXCEPT {++d_; return *this;} CONSTCD14 inline day day::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline day& day::operator--() NOEXCEPT {--d_; return *this;} CONSTCD14 inline day day::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline day& day::operator+=(const days& d) NOEXCEPT {*this = *this + d; return *this;} CONSTCD14 inline day& day::operator-=(const days& d) NOEXCEPT {*this = *this - d; return *this;} CONSTCD11 inline day::operator unsigned() const NOEXCEPT {return d_;} CONSTCD11 inline bool day::ok() const NOEXCEPT {return 1 <= d_ && d_ <= 30;} CONSTCD11 inline bool operator==(const day& x, const day& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const day& x, const day& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const day& x, const day& y) NOEXCEPT { return static_cast(x) < static_cast(y); } CONSTCD11 inline bool operator>(const day& x, const day& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const day& x, const day& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const day& x, const day& y) NOEXCEPT { return !(x < y); } CONSTCD11 inline days operator-(const day& x, const day& y) NOEXCEPT { return days{static_cast(static_cast(x) - static_cast(y))}; } CONSTCD11 inline day operator+(const day& x, const days& y) NOEXCEPT { return day{static_cast(x) + static_cast(y.count())}; } CONSTCD11 inline day operator+(const days& x, const day& y) NOEXCEPT { return y + x; } CONSTCD11 inline day operator-(const day& x, const days& y) NOEXCEPT { return x + -y; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const day& d) { date::detail::save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::right); os.width(2); os << static_cast(d); return os; } // month CONSTCD11 inline month::month(unsigned m) NOEXCEPT : m_(static_cast(m)) {} CONSTCD14 inline month& month::operator++() NOEXCEPT {if (++m_ == 13) m_ = 1; return *this;} CONSTCD14 inline month month::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline month& month::operator--() NOEXCEPT {if (--m_ == 0) m_ = 12; return *this;} CONSTCD14 inline month month::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline month& month::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline month& month::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD11 inline month::operator unsigned() const NOEXCEPT {return m_;} CONSTCD11 inline bool month::ok() const NOEXCEPT {return 1 <= m_ && m_ <= 12;} CONSTCD11 inline bool operator==(const month& x, const month& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const month& x, const month& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const month& x, const month& y) NOEXCEPT { return static_cast(x) < static_cast(y); } CONSTCD11 inline bool operator>(const month& x, const month& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const month& x, const month& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const month& x, const month& y) NOEXCEPT { return !(x < y); } CONSTCD14 inline months operator-(const month& x, const month& y) NOEXCEPT { auto const d = static_cast(x) - static_cast(y); return months(d <= 11 ? d : d + 12); } CONSTCD14 inline month operator+(const month& x, const months& y) NOEXCEPT { auto const mu = static_cast(static_cast(x)) - 1 + y.count(); auto const yr = (mu >= 0 ? mu : mu-11) / 12; return month{static_cast(mu - yr * 12 + 1)}; } CONSTCD14 inline month operator+(const months& x, const month& y) NOEXCEPT { return y + x; } CONSTCD14 inline month operator-(const month& x, const months& y) NOEXCEPT { return x + -y; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month& m) { switch (static_cast(m)) { case 1: os << "Muharram"; break; case 2: os << "Safar"; break; case 3: os << "Rabi' al-awwal"; break; case 4: os << "Rabi' al-thani"; break; case 5: os << "Jumada al-awwal"; break; case 6: os << "Jumada al-Thani"; break; case 7: os << "Rajab"; break; case 8: os << "Sha'ban"; break; case 9: os << "Ramadan"; break; case 10: os << "Shawwal"; break; case 11: os << "Dhu al-Qi'dah"; break; case 12: os << "Dhu al-Hijjah"; break; default: os << static_cast(m) << " is not a valid month"; break; } return os; } // year CONSTCD11 inline year::year(int y) NOEXCEPT : y_(static_cast(y)) {} CONSTCD14 inline year& year::operator++() NOEXCEPT {++y_; return *this;} CONSTCD14 inline year year::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline year& year::operator--() NOEXCEPT {--y_; return *this;} CONSTCD14 inline year year::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline year& year::operator+=(const years& y) NOEXCEPT {*this = *this + y; return *this;} CONSTCD14 inline year& year::operator-=(const years& y) NOEXCEPT {*this = *this - y; return *this;} CONSTCD14 inline bool year::is_leap() const NOEXCEPT { int y = y_ - 1; const int era = (y >= 0 ? y : y-29) / 30; const unsigned yoe = static_cast(y - era * 30); switch (yoe) { case 1: case 4: case 6: case 9: case 12: case 15: case 17: case 20: case 23: case 25: case 28: return true; default: return false; } } CONSTCD11 inline year::operator int() const NOEXCEPT {return y_;} CONSTCD11 inline bool year::ok() const NOEXCEPT {return true;} CONSTCD11 inline year year::min() NOEXCEPT { return year{std::numeric_limits::min()}; } CONSTCD11 inline year year::max() NOEXCEPT { return year{std::numeric_limits::max()}; } CONSTCD11 inline bool operator==(const year& x, const year& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const year& x, const year& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year& x, const year& y) NOEXCEPT { return static_cast(x) < static_cast(y); } CONSTCD11 inline bool operator>(const year& x, const year& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year& x, const year& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year& x, const year& y) NOEXCEPT { return !(x < y); } CONSTCD11 inline years operator-(const year& x, const year& y) NOEXCEPT { return years{static_cast(x) - static_cast(y)}; } CONSTCD11 inline year operator+(const year& x, const years& y) NOEXCEPT { return year{static_cast(x) + y.count()}; } CONSTCD11 inline year operator+(const years& x, const year& y) NOEXCEPT { return y + x; } CONSTCD11 inline year operator-(const year& x, const years& y) NOEXCEPT { return year{static_cast(x) - y.count()}; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year& y) { date::detail::save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::internal); os.width(4 + (y < year{0})); os << static_cast(y); return os; } // weekday CONSTCD11 inline unsigned char weekday::weekday_from_days(int z) NOEXCEPT { return static_cast(static_cast( z >= -4 ? (z+4) % 7 : (z+5) % 7 + 6)); } CONSTCD11 inline weekday::weekday(unsigned wd) NOEXCEPT : wd_(static_cast(wd)) {} CONSTCD11 inline weekday::weekday(const sys_days& dp) NOEXCEPT : wd_(weekday_from_days(dp.time_since_epoch().count())) {} CONSTCD11 inline weekday::weekday(const local_days& dp) NOEXCEPT : wd_(weekday_from_days(dp.time_since_epoch().count())) {} CONSTCD14 inline weekday& weekday::operator++() NOEXCEPT {if (++wd_ == 7) wd_ = 0; return *this;} CONSTCD14 inline weekday weekday::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline weekday& weekday::operator--() NOEXCEPT {if (wd_-- == 0) wd_ = 6; return *this;} CONSTCD14 inline weekday weekday::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline weekday& weekday::operator+=(const days& d) NOEXCEPT { *this = *this + d; return *this; } CONSTCD14 inline weekday& weekday::operator-=(const days& d) NOEXCEPT { *this = *this - d; return *this; } CONSTCD11 inline weekday::operator unsigned() const NOEXCEPT { return static_cast(wd_); } CONSTCD11 inline bool weekday::ok() const NOEXCEPT {return wd_ <= 6;} CONSTCD11 inline bool operator==(const weekday& x, const weekday& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const weekday& x, const weekday& y) NOEXCEPT { return !(x == y); } CONSTCD14 inline days operator-(const weekday& x, const weekday& y) NOEXCEPT { auto const diff = static_cast(x) - static_cast(y); return days{diff <= 6 ? diff : diff + 7}; } CONSTCD14 inline weekday operator+(const weekday& x, const days& y) NOEXCEPT { auto const wdu = static_cast(static_cast(x)) + y.count(); auto const wk = (wdu >= 0 ? wdu : wdu-6) / 7; return weekday{static_cast(wdu - wk * 7)}; } CONSTCD14 inline weekday operator+(const days& x, const weekday& y) NOEXCEPT { return y + x; } CONSTCD14 inline weekday operator-(const weekday& x, const days& y) NOEXCEPT { return x + -y; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const weekday& wd) { switch (static_cast(wd)) { case 0: os << "al-Aḥad"; break; case 1: os << "al-Ithnayn"; break; case 2: os << "ath-ThulÄ�thÄ�’"; break; case 3: os << "al-Arba‘Ä�’"; break; case 4: os << "al-KhamÄ«s"; break; case 5: os << "al-Jum‘ah"; break; case 6: os << "as-Sabt"; break; default: os << static_cast(wd) << " is not a valid weekday"; break; } return os; } #if !defined(_MSC_VER) || (_MSC_VER >= 1900) inline namespace literals { CONSTCD11 inline islamic::day operator ""_d(unsigned long long d) NOEXCEPT { return islamic::day{static_cast(d)}; } CONSTCD11 inline islamic::year operator ""_y(unsigned long long y) NOEXCEPT { return islamic::year(static_cast(y)); } #endif // !defined(_MSC_VER) || (_MSC_VER >= 1900) CONSTDATA islamic::last_spec last{}; #if !defined(_MSC_VER) || (_MSC_VER >= 1900) } // inline namespace literals #endif // weekday_indexed CONSTCD11 inline weekday weekday_indexed::weekday() const NOEXCEPT { return islamic::weekday{static_cast(wd_)}; } CONSTCD11 inline unsigned weekday_indexed::index() const NOEXCEPT {return index_;} CONSTCD11 inline bool weekday_indexed::ok() const NOEXCEPT { return weekday().ok() && 1 <= index_ && index_ <= 5; } CONSTCD11 inline weekday_indexed::weekday_indexed(const islamic::weekday& wd, unsigned index) NOEXCEPT : wd_(static_cast(static_cast(wd))) , index_(static_cast(index)) {} template inline std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_indexed& wdi) { return os << wdi.weekday() << '[' << wdi.index() << ']'; } CONSTCD11 inline weekday_indexed weekday::operator[](unsigned index) const NOEXCEPT { return {*this, index}; } CONSTCD11 inline bool operator==(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT { return x.weekday() == y.weekday() && x.index() == y.index(); } CONSTCD11 inline bool operator!=(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT { return !(x == y); } // weekday_last CONSTCD11 inline islamic::weekday weekday_last::weekday() const NOEXCEPT {return wd_;} CONSTCD11 inline bool weekday_last::ok() const NOEXCEPT {return wd_.ok();} CONSTCD11 inline weekday_last::weekday_last(const islamic::weekday& wd) NOEXCEPT : wd_(wd) {} CONSTCD11 inline bool operator==(const weekday_last& x, const weekday_last& y) NOEXCEPT { return x.weekday() == y.weekday(); } CONSTCD11 inline bool operator!=(const weekday_last& x, const weekday_last& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_last& wdl) { return os << wdl.weekday() << "[last]"; } CONSTCD11 inline weekday_last weekday::operator[](last_spec) const NOEXCEPT { return weekday_last{*this}; } // year_month CONSTCD11 inline year_month::year_month(const islamic::year& y, const islamic::month& m) NOEXCEPT : y_(y) , m_(m) {} CONSTCD11 inline year year_month::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month::month() const NOEXCEPT {return m_;} CONSTCD11 inline bool year_month::ok() const NOEXCEPT {return y_.ok() && m_.ok();} CONSTCD14 inline year_month& year_month::operator+=(const months& dm) NOEXCEPT { *this = *this + dm; return *this; } CONSTCD14 inline year_month& year_month::operator-=(const months& dm) NOEXCEPT { *this = *this - dm; return *this; } CONSTCD14 inline year_month& year_month::operator+=(const years& dy) NOEXCEPT { *this = *this + dy; return *this; } CONSTCD14 inline year_month& year_month::operator-=(const years& dy) NOEXCEPT { *this = *this - dy; return *this; } CONSTCD11 inline bool operator==(const year_month& x, const year_month& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month(); } CONSTCD11 inline bool operator!=(const year_month& x, const year_month& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_month& x, const year_month& y) NOEXCEPT { return x.year() < y.year() ? true : (x.year() > y.year() ? false : (x.month() < y.month())); } CONSTCD11 inline bool operator>(const year_month& x, const year_month& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_month& x, const year_month& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_month& x, const year_month& y) NOEXCEPT { return !(x < y); } CONSTCD14 inline year_month operator+(const year_month& ym, const months& dm) NOEXCEPT { auto dmi = static_cast(static_cast(ym.month())) - 1 + dm.count(); auto dy = (dmi >= 0 ? dmi : dmi-11) / 12; dmi = dmi - dy * 12 + 1; return (ym.year() + years(dy)) / month(static_cast(dmi)); } CONSTCD14 inline year_month operator+(const months& dm, const year_month& ym) NOEXCEPT { return ym + dm; } CONSTCD14 inline year_month operator-(const year_month& ym, const months& dm) NOEXCEPT { return ym + -dm; } CONSTCD11 inline months operator-(const year_month& x, const year_month& y) NOEXCEPT { return (x.year() - y.year()) + months(static_cast(x.month()) - static_cast(y.month())); } CONSTCD11 inline year_month operator+(const year_month& ym, const years& dy) NOEXCEPT { return (ym.year() + dy) / ym.month(); } CONSTCD11 inline year_month operator+(const years& dy, const year_month& ym) NOEXCEPT { return ym + dy; } CONSTCD11 inline year_month operator-(const year_month& ym, const years& dy) NOEXCEPT { return ym + -dy; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month& ym) { return os << ym.year() << '/' << ym.month(); } // month_day CONSTCD11 inline month_day::month_day(const islamic::month& m, const islamic::day& d) NOEXCEPT : m_(m) , d_(d) {} CONSTCD11 inline islamic::month month_day::month() const NOEXCEPT {return m_;} CONSTCD11 inline islamic::day month_day::day() const NOEXCEPT {return d_;} CONSTCD14 inline bool month_day::ok() const NOEXCEPT { CONSTDATA islamic::day d[] = {30_d, 29_d, 30_d, 29_d, 30_d, 29_d, 30_d, 29_d, 30_d, 29_d, 30_d, 30_d}; return m_.ok() && 1_d <= d_ && d_ <= d[static_cast(m_)-1]; } CONSTCD11 inline bool operator==(const month_day& x, const month_day& y) NOEXCEPT { return x.month() == y.month() && x.day() == y.day(); } CONSTCD11 inline bool operator!=(const month_day& x, const month_day& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const month_day& x, const month_day& y) NOEXCEPT { return x.month() < y.month() ? true : (x.month() > y.month() ? false : (x.day() < y.day())); } CONSTCD11 inline bool operator>(const month_day& x, const month_day& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const month_day& x, const month_day& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const month_day& x, const month_day& y) NOEXCEPT { return !(x < y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_day& md) { return os << md.month() << '/' << md.day(); } // month_day_last CONSTCD11 inline month month_day_last::month() const NOEXCEPT {return m_;} CONSTCD11 inline bool month_day_last::ok() const NOEXCEPT {return m_.ok();} CONSTCD11 inline month_day_last::month_day_last(const islamic::month& m) NOEXCEPT : m_(m) {} CONSTCD11 inline bool operator==(const month_day_last& x, const month_day_last& y) NOEXCEPT { return x.month() == y.month(); } CONSTCD11 inline bool operator!=(const month_day_last& x, const month_day_last& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const month_day_last& x, const month_day_last& y) NOEXCEPT { return x.month() < y.month(); } CONSTCD11 inline bool operator>(const month_day_last& x, const month_day_last& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const month_day_last& x, const month_day_last& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const month_day_last& x, const month_day_last& y) NOEXCEPT { return !(x < y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_day_last& mdl) { return os << mdl.month() << "/last"; } // month_weekday CONSTCD11 inline month_weekday::month_weekday(const islamic::month& m, const islamic::weekday_indexed& wdi) NOEXCEPT : m_(m) , wdi_(wdi) {} CONSTCD11 inline month month_weekday::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday_indexed month_weekday::weekday_indexed() const NOEXCEPT { return wdi_; } CONSTCD11 inline bool month_weekday::ok() const NOEXCEPT { return m_.ok() && wdi_.ok(); } CONSTCD11 inline bool operator==(const month_weekday& x, const month_weekday& y) NOEXCEPT { return x.month() == y.month() && x.weekday_indexed() == y.weekday_indexed(); } CONSTCD11 inline bool operator!=(const month_weekday& x, const month_weekday& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday& mwd) { return os << mwd.month() << '/' << mwd.weekday_indexed(); } // month_weekday_last CONSTCD11 inline month_weekday_last::month_weekday_last(const islamic::month& m, const islamic::weekday_last& wdl) NOEXCEPT : m_(m) , wdl_(wdl) {} CONSTCD11 inline month month_weekday_last::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday_last month_weekday_last::weekday_last() const NOEXCEPT { return wdl_; } CONSTCD11 inline bool month_weekday_last::ok() const NOEXCEPT { return m_.ok() && wdl_.ok(); } CONSTCD11 inline bool operator==(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT { return x.month() == y.month() && x.weekday_last() == y.weekday_last(); } CONSTCD11 inline bool operator!=(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday_last& mwdl) { return os << mwdl.month() << '/' << mwdl.weekday_last(); } // year_month_day_last CONSTCD11 inline year_month_day_last::year_month_day_last(const islamic::year& y, const islamic::month_day_last& mdl) NOEXCEPT : y_(y) , mdl_(mdl) {} CONSTCD14 inline year_month_day_last& year_month_day_last::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline year_month_day_last& year_month_day_last::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_day_last& year_month_day_last::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_day_last& year_month_day_last::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD11 inline year year_month_day_last::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_day_last::month() const NOEXCEPT {return mdl_.month();} CONSTCD11 inline month_day_last year_month_day_last::month_day_last() const NOEXCEPT { return mdl_; } CONSTCD14 inline day year_month_day_last::day() const NOEXCEPT { CONSTDATA islamic::day d[] = {30_d, 29_d, 30_d, 29_d, 30_d, 29_d, 30_d, 29_d, 30_d, 29_d, 30_d, 29_d}; return month() != islamic::month(12) || !y_.is_leap() ? d[static_cast(month())-1] : 30_d; } CONSTCD14 inline year_month_day_last::operator sys_days() const NOEXCEPT { return sys_days(year()/month()/day()); } CONSTCD14 inline year_month_day_last::operator local_days() const NOEXCEPT { return local_days(year()/month()/day()); } CONSTCD11 inline bool year_month_day_last::ok() const NOEXCEPT { return y_.ok() && mdl_.ok(); } CONSTCD11 inline bool operator==(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return x.year() == y.year() && x.month_day_last() == y.month_day_last(); } CONSTCD11 inline bool operator!=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return x.year() < y.year() ? true : (x.year() > y.year() ? false : (x.month_day_last() < y.month_day_last())); } CONSTCD11 inline bool operator>(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return !(x < y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day_last& ymdl) { return os << ymdl.year() << '/' << ymdl.month_day_last(); } CONSTCD14 inline year_month_day_last operator+(const year_month_day_last& ymdl, const months& dm) NOEXCEPT { return (ymdl.year() / ymdl.month() + dm) / last; } CONSTCD14 inline year_month_day_last operator+(const months& dm, const year_month_day_last& ymdl) NOEXCEPT { return ymdl + dm; } CONSTCD14 inline year_month_day_last operator-(const year_month_day_last& ymdl, const months& dm) NOEXCEPT { return ymdl + (-dm); } CONSTCD11 inline year_month_day_last operator+(const year_month_day_last& ymdl, const years& dy) NOEXCEPT { return {ymdl.year()+dy, ymdl.month_day_last()}; } CONSTCD11 inline year_month_day_last operator+(const years& dy, const year_month_day_last& ymdl) NOEXCEPT { return ymdl + dy; } CONSTCD11 inline year_month_day_last operator-(const year_month_day_last& ymdl, const years& dy) NOEXCEPT { return ymdl + (-dy); } // year_month_day CONSTCD11 inline year_month_day::year_month_day(const islamic::year& y, const islamic::month& m, const islamic::day& d) NOEXCEPT : y_(y) , m_(m) , d_(d) {} CONSTCD14 inline year_month_day::year_month_day(const year_month_day_last& ymdl) NOEXCEPT : y_(ymdl.year()) , m_(ymdl.month()) , d_(ymdl.day()) {} CONSTCD14 inline year_month_day::year_month_day(sys_days dp) NOEXCEPT : year_month_day(from_days(dp.time_since_epoch())) {} CONSTCD14 inline year_month_day::year_month_day(local_days dp) NOEXCEPT : year_month_day(from_days(dp.time_since_epoch())) {} CONSTCD11 inline year year_month_day::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_day::month() const NOEXCEPT {return m_;} CONSTCD11 inline day year_month_day::day() const NOEXCEPT {return d_;} CONSTCD14 inline year_month_day& year_month_day::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline year_month_day& year_month_day::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_day& year_month_day::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_day& year_month_day::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD14 inline days year_month_day::to_days() const NOEXCEPT { static_assert(std::numeric_limits::digits >= 18, "This algorithm has not been ported to a 16 bit unsigned integer"); static_assert(std::numeric_limits::digits >= 20, "This algorithm has not been ported to a 16 bit signed integer"); auto const y = static_cast(y_) - 1; auto const m = static_cast(m_); auto const d = static_cast(d_); auto const era = (y >= 0 ? y : y-29) / 30; auto const yoe = static_cast(y - era * 30); // [0, 29] auto const doy = 29*(m-1) + m/2 + d-1; // [0, 354] auto const doe = yoe * 354 + (11*(yoe+1)+3)/30 + doy; // [0, 10630] return days{era * 10631 + static_cast(doe) - 492148}; } CONSTCD14 inline year_month_day::operator sys_days() const NOEXCEPT { return sys_days{to_days()}; } CONSTCD14 inline year_month_day::operator local_days() const NOEXCEPT { return local_days{to_days()}; } CONSTCD14 inline bool year_month_day::ok() const NOEXCEPT { if (!(y_.ok() && m_.ok())) return false; return 1_d <= d_ && d_ <= (y_/m_/last).day(); } CONSTCD11 inline bool operator==(const year_month_day& x, const year_month_day& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month() && x.day() == y.day(); } CONSTCD11 inline bool operator!=(const year_month_day& x, const year_month_day& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_month_day& x, const year_month_day& y) NOEXCEPT { return x.year() < y.year() ? true : (x.year() > y.year() ? false : (x.month() < y.month() ? true : (x.month() > y.month() ? false : (x.day() < y.day())))); } CONSTCD11 inline bool operator>(const year_month_day& x, const year_month_day& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_month_day& x, const year_month_day& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_month_day& x, const year_month_day& y) NOEXCEPT { return !(x < y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day& ymd) { date::detail::save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::right); os << ymd.year() << '-'; os.width(2); os << static_cast(ymd.month()) << '-'; os << ymd.day(); return os; } CONSTCD14 inline year_month_day year_month_day::from_days(days dp) NOEXCEPT { static_assert(std::numeric_limits::digits >= 18, "This algorithm has not been ported to a 16 bit unsigned integer"); static_assert(std::numeric_limits::digits >= 20, "This algorithm has not been ported to a 16 bit signed integer"); auto const z = dp.count() + 492148; auto const era = (z >= 0 ? z : z - 10630) / 10631; auto const doe = static_cast(z - era * 10631); // [0, 10630] auto const yoe = (30*doe + 10646)/10631 - 1; // [0, 29] auto const y = static_cast(yoe) + era * 30 + 1; auto const doy = doe - (yoe * 354 + (11*(yoe+1)+3)/30); // [0, 354] auto const m = (11*doy + 330) / 325; // [1, 12] auto const d = doy - (29*(m-1) + m/2) + 1; // [1, 30] return year_month_day{islamic::year{y}, islamic::month(m), islamic::day(d)}; } CONSTCD14 inline year_month_day operator+(const year_month_day& ymd, const months& dm) NOEXCEPT { return (ymd.year() / ymd.month() + dm) / ymd.day(); } CONSTCD14 inline year_month_day operator+(const months& dm, const year_month_day& ymd) NOEXCEPT { return ymd + dm; } CONSTCD14 inline year_month_day operator-(const year_month_day& ymd, const months& dm) NOEXCEPT { return ymd + (-dm); } CONSTCD11 inline year_month_day operator+(const year_month_day& ymd, const years& dy) NOEXCEPT { return (ymd.year() + dy) / ymd.month() / ymd.day(); } CONSTCD11 inline year_month_day operator+(const years& dy, const year_month_day& ymd) NOEXCEPT { return ymd + dy; } CONSTCD11 inline year_month_day operator-(const year_month_day& ymd, const years& dy) NOEXCEPT { return ymd + (-dy); } // year_month_weekday CONSTCD11 inline year_month_weekday::year_month_weekday(const islamic::year& y, const islamic::month& m, const islamic::weekday_indexed& wdi) NOEXCEPT : y_(y) , m_(m) , wdi_(wdi) {} CONSTCD14 inline year_month_weekday::year_month_weekday(const sys_days& dp) NOEXCEPT : year_month_weekday(from_days(dp.time_since_epoch())) {} CONSTCD14 inline year_month_weekday::year_month_weekday(const local_days& dp) NOEXCEPT : year_month_weekday(from_days(dp.time_since_epoch())) {} CONSTCD14 inline year_month_weekday& year_month_weekday::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline year_month_weekday& year_month_weekday::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_weekday& year_month_weekday::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_weekday& year_month_weekday::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD11 inline year year_month_weekday::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_weekday::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday year_month_weekday::weekday() const NOEXCEPT { return wdi_.weekday(); } CONSTCD11 inline unsigned year_month_weekday::index() const NOEXCEPT { return wdi_.index(); } CONSTCD11 inline weekday_indexed year_month_weekday::weekday_indexed() const NOEXCEPT { return wdi_; } CONSTCD14 inline year_month_weekday::operator sys_days() const NOEXCEPT { return sys_days{to_days()}; } CONSTCD14 inline year_month_weekday::operator local_days() const NOEXCEPT { return local_days{to_days()}; } CONSTCD14 inline bool year_month_weekday::ok() const NOEXCEPT { if (!y_.ok() || !m_.ok() || !wdi_.weekday().ok() || wdi_.index() < 1) return false; if (wdi_.index() <= 4) return true; auto d2 = wdi_.weekday() - islamic::weekday(y_/m_/1) + days((wdi_.index()-1)*7 + 1); return static_cast(d2.count()) <= static_cast((y_/m_/last).day()); } CONSTCD14 inline year_month_weekday year_month_weekday::from_days(days d) NOEXCEPT { sys_days dp{d}; auto const wd = islamic::weekday(dp); auto const ymd = year_month_day(dp); return {ymd.year(), ymd.month(), wd[(static_cast(ymd.day())-1)/7+1]}; } CONSTCD14 inline days year_month_weekday::to_days() const NOEXCEPT { auto d = sys_days(y_/m_/1); return (d + (wdi_.weekday() - islamic::weekday(d) + days{(wdi_.index()-1)*7}) ).time_since_epoch(); } CONSTCD11 inline bool operator==(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month() && x.weekday_indexed() == y.weekday_indexed(); } CONSTCD11 inline bool operator!=(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday& ymwdi) { return os << ymwdi.year() << '/' << ymwdi.month() << '/' << ymwdi.weekday_indexed(); } CONSTCD14 inline year_month_weekday operator+(const year_month_weekday& ymwd, const months& dm) NOEXCEPT { return (ymwd.year() / ymwd.month() + dm) / ymwd.weekday_indexed(); } CONSTCD14 inline year_month_weekday operator+(const months& dm, const year_month_weekday& ymwd) NOEXCEPT { return ymwd + dm; } CONSTCD14 inline year_month_weekday operator-(const year_month_weekday& ymwd, const months& dm) NOEXCEPT { return ymwd + (-dm); } CONSTCD11 inline year_month_weekday operator+(const year_month_weekday& ymwd, const years& dy) NOEXCEPT { return {ymwd.year()+dy, ymwd.month(), ymwd.weekday_indexed()}; } CONSTCD11 inline year_month_weekday operator+(const years& dy, const year_month_weekday& ymwd) NOEXCEPT { return ymwd + dy; } CONSTCD11 inline year_month_weekday operator-(const year_month_weekday& ymwd, const years& dy) NOEXCEPT { return ymwd + (-dy); } // year_month_weekday_last CONSTCD11 inline year_month_weekday_last::year_month_weekday_last(const islamic::year& y, const islamic::month& m, const islamic::weekday_last& wdl) NOEXCEPT : y_(y) , m_(m) , wdl_(wdl) {} CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD11 inline year year_month_weekday_last::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_weekday_last::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday year_month_weekday_last::weekday() const NOEXCEPT { return wdl_.weekday(); } CONSTCD11 inline weekday_last year_month_weekday_last::weekday_last() const NOEXCEPT { return wdl_; } CONSTCD14 inline year_month_weekday_last::operator sys_days() const NOEXCEPT { return sys_days{to_days()}; } CONSTCD14 inline year_month_weekday_last::operator local_days() const NOEXCEPT { return local_days{to_days()}; } CONSTCD11 inline bool year_month_weekday_last::ok() const NOEXCEPT { return y_.ok() && m_.ok() && wdl_.ok(); } CONSTCD14 inline days year_month_weekday_last::to_days() const NOEXCEPT { auto const d = sys_days(y_/m_/last); return (d - (islamic::weekday{d} - wdl_.weekday())).time_since_epoch(); } CONSTCD11 inline bool operator==(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month() && x.weekday_last() == y.weekday_last(); } CONSTCD11 inline bool operator!=(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday_last& ymwdl) { return os << ymwdl.year() << '/' << ymwdl.month() << '/' << ymwdl.weekday_last(); } CONSTCD14 inline year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT { return (ymwdl.year() / ymwdl.month() + dm) / ymwdl.weekday_last(); } CONSTCD14 inline year_month_weekday_last operator+(const months& dm, const year_month_weekday_last& ymwdl) NOEXCEPT { return ymwdl + dm; } CONSTCD14 inline year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT { return ymwdl + (-dm); } CONSTCD11 inline year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT { return {ymwdl.year()+dy, ymwdl.month(), ymwdl.weekday_last()}; } CONSTCD11 inline year_month_weekday_last operator+(const years& dy, const year_month_weekday_last& ymwdl) NOEXCEPT { return ymwdl + dy; } CONSTCD11 inline year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT { return ymwdl + (-dy); } // year_month from operator/() CONSTCD11 inline year_month operator/(const year& y, const month& m) NOEXCEPT { return {y, m}; } CONSTCD11 inline year_month operator/(const year& y, int m) NOEXCEPT { return y / month(static_cast(m)); } // month_day from operator/() CONSTCD11 inline month_day operator/(const month& m, const day& d) NOEXCEPT { return {m, d}; } CONSTCD11 inline month_day operator/(const day& d, const month& m) NOEXCEPT { return m / d; } CONSTCD11 inline month_day operator/(const month& m, int d) NOEXCEPT { return m / day(static_cast(d)); } CONSTCD11 inline month_day operator/(int m, const day& d) NOEXCEPT { return month(static_cast(m)) / d; } CONSTCD11 inline month_day operator/(const day& d, int m) NOEXCEPT {return m / d;} // month_day_last from operator/() CONSTCD11 inline month_day_last operator/(const month& m, last_spec) NOEXCEPT { return month_day_last{m}; } CONSTCD11 inline month_day_last operator/(last_spec, const month& m) NOEXCEPT { return m/last; } CONSTCD11 inline month_day_last operator/(int m, last_spec) NOEXCEPT { return month(static_cast(m))/last; } CONSTCD11 inline month_day_last operator/(last_spec, int m) NOEXCEPT { return m/last; } // month_weekday from operator/() CONSTCD11 inline month_weekday operator/(const month& m, const weekday_indexed& wdi) NOEXCEPT { return {m, wdi}; } CONSTCD11 inline month_weekday operator/(const weekday_indexed& wdi, const month& m) NOEXCEPT { return m / wdi; } CONSTCD11 inline month_weekday operator/(int m, const weekday_indexed& wdi) NOEXCEPT { return month(static_cast(m)) / wdi; } CONSTCD11 inline month_weekday operator/(const weekday_indexed& wdi, int m) NOEXCEPT { return m / wdi; } // month_weekday_last from operator/() CONSTCD11 inline month_weekday_last operator/(const month& m, const weekday_last& wdl) NOEXCEPT { return {m, wdl}; } CONSTCD11 inline month_weekday_last operator/(const weekday_last& wdl, const month& m) NOEXCEPT { return m / wdl; } CONSTCD11 inline month_weekday_last operator/(int m, const weekday_last& wdl) NOEXCEPT { return month(static_cast(m)) / wdl; } CONSTCD11 inline month_weekday_last operator/(const weekday_last& wdl, int m) NOEXCEPT { return m / wdl; } // year_month_day from operator/() CONSTCD11 inline year_month_day operator/(const year_month& ym, const day& d) NOEXCEPT { return {ym.year(), ym.month(), d}; } CONSTCD11 inline year_month_day operator/(const year_month& ym, int d) NOEXCEPT { return ym / day(static_cast(d)); } CONSTCD11 inline year_month_day operator/(const year& y, const month_day& md) NOEXCEPT { return y / md.month() / md.day(); } CONSTCD11 inline year_month_day operator/(int y, const month_day& md) NOEXCEPT { return year(y) / md; } CONSTCD11 inline year_month_day operator/(const month_day& md, const year& y) NOEXCEPT { return y / md; } CONSTCD11 inline year_month_day operator/(const month_day& md, int y) NOEXCEPT { return year(y) / md; } // year_month_day_last from operator/() CONSTCD11 inline year_month_day_last operator/(const year_month& ym, last_spec) NOEXCEPT { return {ym.year(), month_day_last{ym.month()}}; } CONSTCD11 inline year_month_day_last operator/(const year& y, const month_day_last& mdl) NOEXCEPT { return {y, mdl}; } CONSTCD11 inline year_month_day_last operator/(int y, const month_day_last& mdl) NOEXCEPT { return year(y) / mdl; } CONSTCD11 inline year_month_day_last operator/(const month_day_last& mdl, const year& y) NOEXCEPT { return y / mdl; } CONSTCD11 inline year_month_day_last operator/(const month_day_last& mdl, int y) NOEXCEPT { return year(y) / mdl; } // year_month_weekday from operator/() CONSTCD11 inline year_month_weekday operator/(const year_month& ym, const weekday_indexed& wdi) NOEXCEPT { return {ym.year(), ym.month(), wdi}; } CONSTCD11 inline year_month_weekday operator/(const year& y, const month_weekday& mwd) NOEXCEPT { return {y, mwd.month(), mwd.weekday_indexed()}; } CONSTCD11 inline year_month_weekday operator/(int y, const month_weekday& mwd) NOEXCEPT { return year(y) / mwd; } CONSTCD11 inline year_month_weekday operator/(const month_weekday& mwd, const year& y) NOEXCEPT { return y / mwd; } CONSTCD11 inline year_month_weekday operator/(const month_weekday& mwd, int y) NOEXCEPT { return year(y) / mwd; } // year_month_weekday_last from operator/() CONSTCD11 inline year_month_weekday_last operator/(const year_month& ym, const weekday_last& wdl) NOEXCEPT { return {ym.year(), ym.month(), wdl}; } CONSTCD11 inline year_month_weekday_last operator/(const year& y, const month_weekday_last& mwdl) NOEXCEPT { return {y, mwdl.month(), mwdl.weekday_last()}; } CONSTCD11 inline year_month_weekday_last operator/(int y, const month_weekday_last& mwdl) NOEXCEPT { return year(y) / mwdl; } CONSTCD11 inline year_month_weekday_last operator/(const month_weekday_last& mwdl, const year& y) NOEXCEPT { return y / mwdl; } CONSTCD11 inline year_month_weekday_last operator/(const month_weekday_last& mwdl, int y) NOEXCEPT { return year(y) / mwdl; } } // namespace islamic #endif // ISLAMIC_H pr0m1th3as-datatypes-9450b29/src/date/iso_week.h000066400000000000000000001242001527370255000213570ustar00rootroot00000000000000#ifndef ISO_WEEK_H #define ISO_WEEK_H // The MIT License (MIT) // // Copyright (c) 2015, 2016, 2017 Howard Hinnant // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #include "date.h" #include namespace iso_week { // y/wn/wd // wn/wd/y // wd/wn/y using days = date::days; using weeks = date::weeks; using years = date::years; // time_point using sys_days = date::sys_days; using local_days = date::local_days; // types struct last_week { explicit last_week() = default; }; class weekday; class weeknum; class year; class year_weeknum; class year_lastweek; class weeknum_weekday; class lastweek_weekday; class year_weeknum_weekday; class year_lastweek_weekday; // date composition operators CONSTCD11 year_weeknum operator/(const year& y, const weeknum& wn) NOEXCEPT; CONSTCD11 year_weeknum operator/(const year& y, int wn) NOEXCEPT; CONSTCD11 year_lastweek operator/(const year& y, last_week wn) NOEXCEPT; CONSTCD11 weeknum_weekday operator/(const weeknum& wn, const weekday& wd) NOEXCEPT; CONSTCD11 weeknum_weekday operator/(const weeknum& wn, int wd) NOEXCEPT; CONSTCD11 weeknum_weekday operator/(const weekday& wd, const weeknum& wn) NOEXCEPT; CONSTCD11 weeknum_weekday operator/(const weekday& wd, int wn) NOEXCEPT; CONSTCD11 lastweek_weekday operator/(const last_week& wn, const weekday& wd) NOEXCEPT; CONSTCD11 lastweek_weekday operator/(const last_week& wn, int wd) NOEXCEPT; CONSTCD11 lastweek_weekday operator/(const weekday& wd, const last_week& wn) NOEXCEPT; CONSTCD11 year_weeknum_weekday operator/(const year_weeknum& ywn, const weekday& wd) NOEXCEPT; CONSTCD11 year_weeknum_weekday operator/(const year_weeknum& ywn, int wd) NOEXCEPT; CONSTCD11 year_weeknum_weekday operator/(const weeknum_weekday& wnwd, const year& y) NOEXCEPT; CONSTCD11 year_weeknum_weekday operator/(const weeknum_weekday& wnwd, int y) NOEXCEPT; CONSTCD11 year_lastweek_weekday operator/(const year_lastweek& ylw, const weekday& wd) NOEXCEPT; CONSTCD11 year_lastweek_weekday operator/(const year_lastweek& ylw, int wd) NOEXCEPT; CONSTCD11 year_lastweek_weekday operator/(const lastweek_weekday& lwwd, const year& y) NOEXCEPT; CONSTCD11 year_lastweek_weekday operator/(const lastweek_weekday& lwwd, int y) NOEXCEPT; // weekday class weekday { unsigned char wd_; public: explicit CONSTCD11 weekday(unsigned wd) NOEXCEPT; CONSTCD11 weekday(date::weekday wd) NOEXCEPT; explicit weekday(int) = delete; CONSTCD11 weekday(const sys_days& dp) NOEXCEPT; CONSTCD11 explicit weekday(const local_days& dp) NOEXCEPT; weekday& operator++() NOEXCEPT; weekday operator++(int) NOEXCEPT; weekday& operator--() NOEXCEPT; weekday operator--(int) NOEXCEPT; weekday& operator+=(const days& d) NOEXCEPT; weekday& operator-=(const days& d) NOEXCEPT; CONSTCD11 explicit operator unsigned() const NOEXCEPT; CONSTCD11 operator date::weekday() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; private: static CONSTCD11 unsigned char weekday_from_days(int z) NOEXCEPT; static CONSTCD11 unsigned char to_iso_encoding(unsigned char) NOEXCEPT; static CONSTCD11 unsigned from_iso_encoding(unsigned) NOEXCEPT; }; CONSTCD11 bool operator==(const weekday& x, const weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const weekday& x, const weekday& y) NOEXCEPT; CONSTCD14 weekday operator+(const weekday& x, const days& y) NOEXCEPT; CONSTCD14 weekday operator+(const days& x, const weekday& y) NOEXCEPT; CONSTCD14 weekday operator-(const weekday& x, const days& y) NOEXCEPT; CONSTCD14 days operator-(const weekday& x, const weekday& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const weekday& wd); // year class year { short y_; public: explicit CONSTCD11 year(int y) NOEXCEPT; year& operator++() NOEXCEPT; year operator++(int) NOEXCEPT; year& operator--() NOEXCEPT; year operator--(int) NOEXCEPT; year& operator+=(const years& y) NOEXCEPT; year& operator-=(const years& y) NOEXCEPT; CONSTCD14 bool is_leap() const NOEXCEPT; CONSTCD11 explicit operator int() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; static CONSTCD11 year min() NOEXCEPT; static CONSTCD11 year max() NOEXCEPT; }; CONSTCD11 bool operator==(const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator!=(const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator< (const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator> (const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator<=(const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator>=(const year& x, const year& y) NOEXCEPT; CONSTCD11 year operator+(const year& x, const years& y) NOEXCEPT; CONSTCD11 year operator+(const years& x, const year& y) NOEXCEPT; CONSTCD11 year operator-(const year& x, const years& y) NOEXCEPT; CONSTCD11 years operator-(const year& x, const year& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year& y); // weeknum class weeknum { unsigned char wn_; public: explicit CONSTCD11 weeknum(unsigned wn) NOEXCEPT; weeknum& operator++() NOEXCEPT; weeknum operator++(int) NOEXCEPT; weeknum& operator--() NOEXCEPT; weeknum operator--(int) NOEXCEPT; weeknum& operator+=(const weeks& y) NOEXCEPT; weeknum& operator-=(const weeks& y) NOEXCEPT; CONSTCD11 explicit operator unsigned() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const weeknum& x, const weeknum& y) NOEXCEPT; CONSTCD11 bool operator!=(const weeknum& x, const weeknum& y) NOEXCEPT; CONSTCD11 bool operator< (const weeknum& x, const weeknum& y) NOEXCEPT; CONSTCD11 bool operator> (const weeknum& x, const weeknum& y) NOEXCEPT; CONSTCD11 bool operator<=(const weeknum& x, const weeknum& y) NOEXCEPT; CONSTCD11 bool operator>=(const weeknum& x, const weeknum& y) NOEXCEPT; CONSTCD11 weeknum operator+(const weeknum& x, const weeks& y) NOEXCEPT; CONSTCD11 weeknum operator+(const weeks& x, const weeknum& y) NOEXCEPT; CONSTCD11 weeknum operator-(const weeknum& x, const weeks& y) NOEXCEPT; CONSTCD11 weeks operator-(const weeknum& x, const weeknum& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const weeknum& wn); // year_weeknum class year_weeknum { iso_week::year y_; iso_week::weeknum wn_; public: CONSTCD11 year_weeknum(const iso_week::year& y, const iso_week::weeknum& wn) NOEXCEPT; CONSTCD11 iso_week::year year() const NOEXCEPT; CONSTCD11 iso_week::weeknum weeknum() const NOEXCEPT; year_weeknum& operator+=(const years& dy) NOEXCEPT; year_weeknum& operator-=(const years& dy) NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_weeknum& x, const year_weeknum& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_weeknum& x, const year_weeknum& y) NOEXCEPT; CONSTCD11 bool operator< (const year_weeknum& x, const year_weeknum& y) NOEXCEPT; CONSTCD11 bool operator> (const year_weeknum& x, const year_weeknum& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_weeknum& x, const year_weeknum& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_weeknum& x, const year_weeknum& y) NOEXCEPT; CONSTCD11 year_weeknum operator+(const year_weeknum& ym, const years& dy) NOEXCEPT; CONSTCD11 year_weeknum operator+(const years& dy, const year_weeknum& ym) NOEXCEPT; CONSTCD11 year_weeknum operator-(const year_weeknum& ym, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_weeknum& ym); // year_lastweek class year_lastweek { iso_week::year y_; public: CONSTCD11 explicit year_lastweek(const iso_week::year& y) NOEXCEPT; CONSTCD11 iso_week::year year() const NOEXCEPT; CONSTCD14 iso_week::weeknum weeknum() const NOEXCEPT; year_lastweek& operator+=(const years& dy) NOEXCEPT; year_lastweek& operator-=(const years& dy) NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_lastweek& x, const year_lastweek& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_lastweek& x, const year_lastweek& y) NOEXCEPT; CONSTCD11 bool operator< (const year_lastweek& x, const year_lastweek& y) NOEXCEPT; CONSTCD11 bool operator> (const year_lastweek& x, const year_lastweek& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_lastweek& x, const year_lastweek& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_lastweek& x, const year_lastweek& y) NOEXCEPT; CONSTCD11 year_lastweek operator+(const year_lastweek& ym, const years& dy) NOEXCEPT; CONSTCD11 year_lastweek operator+(const years& dy, const year_lastweek& ym) NOEXCEPT; CONSTCD11 year_lastweek operator-(const year_lastweek& ym, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_lastweek& ym); // weeknum_weekday class weeknum_weekday { iso_week::weeknum wn_; iso_week::weekday wd_; public: CONSTCD11 weeknum_weekday(const iso_week::weeknum& wn, const iso_week::weekday& wd) NOEXCEPT; CONSTCD11 iso_week::weeknum weeknum() const NOEXCEPT; CONSTCD11 iso_week::weekday weekday() const NOEXCEPT; CONSTCD14 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const weeknum_weekday& x, const weeknum_weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const weeknum_weekday& x, const weeknum_weekday& y) NOEXCEPT; CONSTCD11 bool operator< (const weeknum_weekday& x, const weeknum_weekday& y) NOEXCEPT; CONSTCD11 bool operator> (const weeknum_weekday& x, const weeknum_weekday& y) NOEXCEPT; CONSTCD11 bool operator<=(const weeknum_weekday& x, const weeknum_weekday& y) NOEXCEPT; CONSTCD11 bool operator>=(const weeknum_weekday& x, const weeknum_weekday& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const weeknum_weekday& md); // lastweek_weekday class lastweek_weekday { iso_week::weekday wd_; public: CONSTCD11 explicit lastweek_weekday(const iso_week::weekday& wd) NOEXCEPT; CONSTCD11 iso_week::weekday weekday() const NOEXCEPT; CONSTCD14 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const lastweek_weekday& x, const lastweek_weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const lastweek_weekday& x, const lastweek_weekday& y) NOEXCEPT; CONSTCD11 bool operator< (const lastweek_weekday& x, const lastweek_weekday& y) NOEXCEPT; CONSTCD11 bool operator> (const lastweek_weekday& x, const lastweek_weekday& y) NOEXCEPT; CONSTCD11 bool operator<=(const lastweek_weekday& x, const lastweek_weekday& y) NOEXCEPT; CONSTCD11 bool operator>=(const lastweek_weekday& x, const lastweek_weekday& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const lastweek_weekday& md); // year_lastweek_weekday class year_lastweek_weekday { iso_week::year y_; iso_week::weekday wd_; public: CONSTCD11 year_lastweek_weekday(const iso_week::year& y, const iso_week::weekday& wd) NOEXCEPT; year_lastweek_weekday& operator+=(const years& y) NOEXCEPT; year_lastweek_weekday& operator-=(const years& y) NOEXCEPT; CONSTCD11 iso_week::year year() const NOEXCEPT; CONSTCD14 iso_week::weeknum weeknum() const NOEXCEPT; CONSTCD11 iso_week::weekday weekday() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_lastweek_weekday& x, const year_lastweek_weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_lastweek_weekday& x, const year_lastweek_weekday& y) NOEXCEPT; CONSTCD11 bool operator< (const year_lastweek_weekday& x, const year_lastweek_weekday& y) NOEXCEPT; CONSTCD11 bool operator> (const year_lastweek_weekday& x, const year_lastweek_weekday& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_lastweek_weekday& x, const year_lastweek_weekday& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_lastweek_weekday& x, const year_lastweek_weekday& y) NOEXCEPT; CONSTCD11 year_lastweek_weekday operator+(const year_lastweek_weekday& ywnwd, const years& y) NOEXCEPT; CONSTCD11 year_lastweek_weekday operator+(const years& y, const year_lastweek_weekday& ywnwd) NOEXCEPT; CONSTCD11 year_lastweek_weekday operator-(const year_lastweek_weekday& ywnwd, const years& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_lastweek_weekday& ywnwd); // class year_weeknum_weekday class year_weeknum_weekday { iso_week::year y_; iso_week::weeknum wn_; iso_week::weekday wd_; public: CONSTCD11 year_weeknum_weekday(const iso_week::year& y, const iso_week::weeknum& wn, const iso_week::weekday& wd) NOEXCEPT; CONSTCD14 year_weeknum_weekday(const year_lastweek_weekday& ylwwd) NOEXCEPT; CONSTCD14 year_weeknum_weekday(const sys_days& dp) NOEXCEPT; CONSTCD14 explicit year_weeknum_weekday(const local_days& dp) NOEXCEPT; year_weeknum_weekday& operator+=(const years& y) NOEXCEPT; year_weeknum_weekday& operator-=(const years& y) NOEXCEPT; CONSTCD11 iso_week::year year() const NOEXCEPT; CONSTCD11 iso_week::weeknum weeknum() const NOEXCEPT; CONSTCD11 iso_week::weekday weekday() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD14 bool ok() const NOEXCEPT; private: static CONSTCD14 year_weeknum_weekday from_days(days dp) NOEXCEPT; }; CONSTCD11 bool operator==(const year_weeknum_weekday& x, const year_weeknum_weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_weeknum_weekday& x, const year_weeknum_weekday& y) NOEXCEPT; CONSTCD11 bool operator< (const year_weeknum_weekday& x, const year_weeknum_weekday& y) NOEXCEPT; CONSTCD11 bool operator> (const year_weeknum_weekday& x, const year_weeknum_weekday& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_weeknum_weekday& x, const year_weeknum_weekday& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_weeknum_weekday& x, const year_weeknum_weekday& y) NOEXCEPT; CONSTCD11 year_weeknum_weekday operator+(const year_weeknum_weekday& ywnwd, const years& y) NOEXCEPT; CONSTCD11 year_weeknum_weekday operator+(const years& y, const year_weeknum_weekday& ywnwd) NOEXCEPT; CONSTCD11 year_weeknum_weekday operator-(const year_weeknum_weekday& ywnwd, const years& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_weeknum_weekday& ywnwd); //----------------+ // Implementation | //----------------+ // weekday CONSTCD11 inline unsigned char weekday::to_iso_encoding(unsigned char z) NOEXCEPT { return z != 0 ? z : (unsigned char)7; } CONSTCD11 inline unsigned weekday::from_iso_encoding(unsigned z) NOEXCEPT { return z != 7 ? z : 0u; } CONSTCD11 inline unsigned char weekday::weekday_from_days(int z) NOEXCEPT { return to_iso_encoding(static_cast(static_cast( z >= -4 ? (z+4) % 7 : (z+5) % 7 + 6))); } CONSTCD11 inline weekday::weekday(unsigned wd) NOEXCEPT : wd_(static_cast(wd)) {} CONSTCD11 inline weekday::weekday(date::weekday wd) NOEXCEPT : wd_(wd.iso_encoding()) {} CONSTCD11 inline weekday::weekday(const sys_days& dp) NOEXCEPT : wd_(weekday_from_days(dp.time_since_epoch().count())) {} CONSTCD11 inline weekday::weekday(const local_days& dp) NOEXCEPT : wd_(weekday_from_days(dp.time_since_epoch().count())) {} inline weekday& weekday::operator++() NOEXCEPT {if (++wd_ == 8) wd_ = 1; return *this;} inline weekday weekday::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} inline weekday& weekday::operator--() NOEXCEPT {if (wd_-- == 1) wd_ = 7; return *this;} inline weekday weekday::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} inline weekday& weekday::operator+=(const days& d) NOEXCEPT { *this = *this + d; return *this; } inline weekday& weekday::operator-=(const days& d) NOEXCEPT { *this = *this - d; return *this; } CONSTCD11 inline weekday::operator unsigned() const NOEXCEPT { return wd_; } CONSTCD11 inline weekday::operator date::weekday() const NOEXCEPT { return date::weekday{from_iso_encoding(unsigned{wd_})}; } CONSTCD11 inline bool weekday::ok() const NOEXCEPT {return 1 <= wd_ && wd_ <= 7;} CONSTCD11 inline bool operator==(const weekday& x, const weekday& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const weekday& x, const weekday& y) NOEXCEPT { return !(x == y); } CONSTCD14 inline days operator-(const weekday& x, const weekday& y) NOEXCEPT { auto const diff = static_cast(x) - static_cast(y); return days{diff <= 6 ? diff : diff + 7}; } CONSTCD14 inline weekday operator+(const weekday& x, const days& y) NOEXCEPT { auto const wdu = static_cast(static_cast(x) - 1u) + y.count(); auto const wk = (wdu >= 0 ? wdu : wdu-6) / 7; return weekday{static_cast(wdu - wk * 7) + 1u}; } CONSTCD14 inline weekday operator+(const days& x, const weekday& y) NOEXCEPT { return y + x; } CONSTCD14 inline weekday operator-(const weekday& x, const days& y) NOEXCEPT { return x + -y; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const weekday& wd) { switch (static_cast(wd)) { case 7: os << "Sun"; break; case 1: os << "Mon"; break; case 2: os << "Tue"; break; case 3: os << "Wed"; break; case 4: os << "Thu"; break; case 5: os << "Fri"; break; case 6: os << "Sat"; break; default: os << static_cast(wd) << " is not a valid weekday"; break; } return os; } // year CONSTCD11 inline year::year(int y) NOEXCEPT : y_(static_cast(y)) {} inline year& year::operator++() NOEXCEPT {++y_; return *this;} inline year year::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} inline year& year::operator--() NOEXCEPT {--y_; return *this;} inline year year::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} inline year& year::operator+=(const years& y) NOEXCEPT {*this = *this + y; return *this;} inline year& year::operator-=(const years& y) NOEXCEPT {*this = *this - y; return *this;} CONSTCD14 inline bool year::is_leap() const NOEXCEPT { const auto y = date::year{static_cast(y_)}; const auto s0 = sys_days((y-years{1})/12/date::thu[date::last]); const auto s1 = sys_days(y/12/date::thu[date::last]); return s1-s0 != days{7*52}; } CONSTCD11 inline year::operator int() const NOEXCEPT {return y_;} CONSTCD11 inline bool year::ok() const NOEXCEPT {return min() <= *this && *this <= max();} CONSTCD11 inline year year::min() NOEXCEPT { using std::chrono::seconds; using std::chrono::minutes; using std::chrono::hours; using std::chrono::duration_cast; static_assert(sizeof(seconds)*CHAR_BIT >= 41, "seconds may overflow"); static_assert(sizeof(hours)*CHAR_BIT >= 30, "hours may overflow"); return sizeof(minutes)*CHAR_BIT < 34 ? year{1970} + duration_cast(minutes::min()) : year{std::numeric_limits::min()}; } CONSTCD11 inline year year::max() NOEXCEPT { using std::chrono::seconds; using std::chrono::minutes; using std::chrono::hours; using std::chrono::duration_cast; static_assert(sizeof(seconds)*CHAR_BIT >= 41, "seconds may overflow"); static_assert(sizeof(hours)*CHAR_BIT >= 30, "hours may overflow"); return sizeof(minutes)*CHAR_BIT < 34 ? year{1969} + duration_cast(minutes::max()) : year{std::numeric_limits::max()}; } CONSTCD11 inline bool operator==(const year& x, const year& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const year& x, const year& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year& x, const year& y) NOEXCEPT { return static_cast(x) < static_cast(y); } CONSTCD11 inline bool operator>(const year& x, const year& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year& x, const year& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year& x, const year& y) NOEXCEPT { return !(x < y); } CONSTCD11 inline years operator-(const year& x, const year& y) NOEXCEPT { return years{static_cast(x) - static_cast(y)}; } CONSTCD11 inline year operator+(const year& x, const years& y) NOEXCEPT { return year{static_cast(x) + y.count()}; } CONSTCD11 inline year operator+(const years& x, const year& y) NOEXCEPT { return y + x; } CONSTCD11 inline year operator-(const year& x, const years& y) NOEXCEPT { return year{static_cast(x) - y.count()}; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year& y) { date::detail::save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::internal); os.width(4 + (y < year{0})); os << static_cast(y); return os; } #if !defined(_MSC_VER) || (_MSC_VER >= 1900) inline namespace literals { CONSTCD11 inline iso_week::year operator ""_y(unsigned long long y) NOEXCEPT { return iso_week::year(static_cast(y)); } CONSTCD11 inline iso_week::weeknum operator ""_w(unsigned long long wn) NOEXCEPT { return iso_week::weeknum(static_cast(wn)); } #endif // !defined(_MSC_VER) || (_MSC_VER >= 1900) CONSTDATA iso_week::last_week last{}; CONSTDATA iso_week::weekday sun{7u}; CONSTDATA iso_week::weekday mon{1u}; CONSTDATA iso_week::weekday tue{2u}; CONSTDATA iso_week::weekday wed{3u}; CONSTDATA iso_week::weekday thu{4u}; CONSTDATA iso_week::weekday fri{5u}; CONSTDATA iso_week::weekday sat{6u}; #if !defined(_MSC_VER) || (_MSC_VER >= 1900) } // inline namespace literals #endif // weeknum CONSTCD11 inline weeknum::weeknum(unsigned wn) NOEXCEPT : wn_(static_cast(wn)) {} inline weeknum& weeknum::operator++() NOEXCEPT {++wn_; return *this;} inline weeknum weeknum::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} inline weeknum& weeknum::operator--() NOEXCEPT {--wn_; return *this;} inline weeknum weeknum::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} inline weeknum& weeknum::operator+=(const weeks& y) NOEXCEPT { *this = *this + y; return *this; } inline weeknum& weeknum::operator-=(const weeks& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD11 inline weeknum::operator unsigned() const NOEXCEPT {return wn_;} CONSTCD11 inline bool weeknum::ok() const NOEXCEPT {return 1 <= wn_ && wn_ <= 53;} CONSTCD11 inline bool operator==(const weeknum& x, const weeknum& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const weeknum& x, const weeknum& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const weeknum& x, const weeknum& y) NOEXCEPT { return static_cast(x) < static_cast(y); } CONSTCD11 inline bool operator>(const weeknum& x, const weeknum& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const weeknum& x, const weeknum& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const weeknum& x, const weeknum& y) NOEXCEPT { return !(x < y); } CONSTCD11 inline weeks operator-(const weeknum& x, const weeknum& y) NOEXCEPT { return weeks{static_cast(static_cast(x)) - static_cast(static_cast(y))}; } CONSTCD11 inline weeknum operator+(const weeknum& x, const weeks& y) NOEXCEPT { return weeknum{static_cast(x) + static_cast(y.count())}; } CONSTCD11 inline weeknum operator+(const weeks& x, const weeknum& y) NOEXCEPT { return y + x; } CONSTCD11 inline weeknum operator-(const weeknum& x, const weeks& y) NOEXCEPT { return x + -y; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const weeknum& wn) { date::detail::save_ostream _(os); os << 'W'; os.fill('0'); os.flags(std::ios::dec | std::ios::right); os.width(2); os << static_cast(wn); return os; } // year_weeknum CONSTCD11 inline year_weeknum::year_weeknum(const iso_week::year& y, const iso_week::weeknum& wn) NOEXCEPT : y_(y) , wn_(wn) {} CONSTCD11 inline year year_weeknum::year() const NOEXCEPT {return y_;} CONSTCD11 inline weeknum year_weeknum::weeknum() const NOEXCEPT {return wn_;} CONSTCD11 inline bool year_weeknum::ok() const NOEXCEPT { return y_.ok() && 1u <= static_cast(wn_) && wn_ <= (y_/last).weeknum(); } inline year_weeknum& year_weeknum::operator+=(const years& dy) NOEXCEPT { *this = *this + dy; return *this; } inline year_weeknum& year_weeknum::operator-=(const years& dy) NOEXCEPT { *this = *this - dy; return *this; } CONSTCD11 inline bool operator==(const year_weeknum& x, const year_weeknum& y) NOEXCEPT { return x.year() == y.year() && x.weeknum() == y.weeknum(); } CONSTCD11 inline bool operator!=(const year_weeknum& x, const year_weeknum& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_weeknum& x, const year_weeknum& y) NOEXCEPT { return x.year() < y.year() ? true : (x.year() > y.year() ? false : (x.weeknum() < y.weeknum())); } CONSTCD11 inline bool operator>(const year_weeknum& x, const year_weeknum& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_weeknum& x, const year_weeknum& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_weeknum& x, const year_weeknum& y) NOEXCEPT { return !(x < y); } CONSTCD11 inline year_weeknum operator+(const year_weeknum& ym, const years& dy) NOEXCEPT { return (ym.year() + dy) / ym.weeknum(); } CONSTCD11 inline year_weeknum operator+(const years& dy, const year_weeknum& ym) NOEXCEPT { return ym + dy; } CONSTCD11 inline year_weeknum operator-(const year_weeknum& ym, const years& dy) NOEXCEPT { return ym + -dy; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_weeknum& ywn) { return os << ywn.year() << '-' << ywn.weeknum(); } // year_lastweek CONSTCD11 inline year_lastweek::year_lastweek(const iso_week::year& y) NOEXCEPT : y_(y) {} CONSTCD11 inline year year_lastweek::year() const NOEXCEPT {return y_;} CONSTCD14 inline weeknum year_lastweek::weeknum() const NOEXCEPT { return iso_week::weeknum(y_.is_leap() ? 53u : 52u); } CONSTCD11 inline bool year_lastweek::ok() const NOEXCEPT {return y_.ok();} inline year_lastweek& year_lastweek::operator+=(const years& dy) NOEXCEPT { *this = *this + dy; return *this; } inline year_lastweek& year_lastweek::operator-=(const years& dy) NOEXCEPT { *this = *this - dy; return *this; } CONSTCD11 inline bool operator==(const year_lastweek& x, const year_lastweek& y) NOEXCEPT { return x.year() == y.year(); } CONSTCD11 inline bool operator!=(const year_lastweek& x, const year_lastweek& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_lastweek& x, const year_lastweek& y) NOEXCEPT { return x.year() < y.year(); } CONSTCD11 inline bool operator>(const year_lastweek& x, const year_lastweek& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_lastweek& x, const year_lastweek& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_lastweek& x, const year_lastweek& y) NOEXCEPT { return !(x < y); } CONSTCD11 inline year_lastweek operator+(const year_lastweek& ym, const years& dy) NOEXCEPT { return year_lastweek{ym.year() + dy}; } CONSTCD11 inline year_lastweek operator+(const years& dy, const year_lastweek& ym) NOEXCEPT { return ym + dy; } CONSTCD11 inline year_lastweek operator-(const year_lastweek& ym, const years& dy) NOEXCEPT { return ym + -dy; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_lastweek& ywn) { return os << ywn.year() << "-W last"; } // weeknum_weekday CONSTCD11 inline weeknum_weekday::weeknum_weekday(const iso_week::weeknum& wn, const iso_week::weekday& wd) NOEXCEPT : wn_(wn) , wd_(wd) {} CONSTCD11 inline weeknum weeknum_weekday::weeknum() const NOEXCEPT {return wn_;} CONSTCD11 inline weekday weeknum_weekday::weekday() const NOEXCEPT {return wd_;} CONSTCD14 inline bool weeknum_weekday::ok() const NOEXCEPT { return wn_.ok() && wd_.ok(); } CONSTCD11 inline bool operator==(const weeknum_weekday& x, const weeknum_weekday& y) NOEXCEPT { return x.weeknum() == y.weeknum() && x.weekday() == y.weekday(); } CONSTCD11 inline bool operator!=(const weeknum_weekday& x, const weeknum_weekday& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const weeknum_weekday& x, const weeknum_weekday& y) NOEXCEPT { return x.weeknum() < y.weeknum() ? true : (x.weeknum() > y.weeknum() ? false : (static_cast(x.weekday()) < static_cast(y.weekday()))); } CONSTCD11 inline bool operator>(const weeknum_weekday& x, const weeknum_weekday& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const weeknum_weekday& x, const weeknum_weekday& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const weeknum_weekday& x, const weeknum_weekday& y) NOEXCEPT { return !(x < y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const weeknum_weekday& md) { return os << md.weeknum() << '-' << md.weekday(); } // lastweek_weekday CONSTCD11 inline lastweek_weekday::lastweek_weekday(const iso_week::weekday& wd) NOEXCEPT : wd_(wd) {} CONSTCD11 inline weekday lastweek_weekday::weekday() const NOEXCEPT {return wd_;} CONSTCD14 inline bool lastweek_weekday::ok() const NOEXCEPT { return wd_.ok(); } CONSTCD11 inline bool operator==(const lastweek_weekday& x, const lastweek_weekday& y) NOEXCEPT { return x.weekday() == y.weekday(); } CONSTCD11 inline bool operator!=(const lastweek_weekday& x, const lastweek_weekday& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const lastweek_weekday& x, const lastweek_weekday& y) NOEXCEPT { return static_cast(x.weekday()) < static_cast(y.weekday()); } CONSTCD11 inline bool operator>(const lastweek_weekday& x, const lastweek_weekday& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const lastweek_weekday& x, const lastweek_weekday& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const lastweek_weekday& x, const lastweek_weekday& y) NOEXCEPT { return !(x < y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const lastweek_weekday& md) { return os << "W last-" << md.weekday(); } // year_lastweek_weekday CONSTCD11 inline year_lastweek_weekday::year_lastweek_weekday(const iso_week::year& y, const iso_week::weekday& wd) NOEXCEPT : y_(y) , wd_(wd) {} inline year_lastweek_weekday& year_lastweek_weekday::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } inline year_lastweek_weekday& year_lastweek_weekday::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD11 inline year year_lastweek_weekday::year() const NOEXCEPT {return y_;} CONSTCD14 inline weeknum year_lastweek_weekday::weeknum() const NOEXCEPT { return (y_ / last).weeknum(); } CONSTCD11 inline weekday year_lastweek_weekday::weekday() const NOEXCEPT {return wd_;} CONSTCD14 inline year_lastweek_weekday::operator sys_days() const NOEXCEPT { return sys_days(date::year{static_cast(y_)}/date::dec/date::thu[date::last]) + (sun - thu) - (sun - wd_); } CONSTCD14 inline year_lastweek_weekday::operator local_days() const NOEXCEPT { return local_days(date::year{static_cast(y_)}/date::dec/date::thu[date::last]) + (sun - thu) - (sun - wd_); } CONSTCD11 inline bool year_lastweek_weekday::ok() const NOEXCEPT { return y_.ok() && wd_.ok(); } CONSTCD11 inline bool operator==(const year_lastweek_weekday& x, const year_lastweek_weekday& y) NOEXCEPT { return x.year() == y.year() && x.weekday() == y.weekday(); } CONSTCD11 inline bool operator!=(const year_lastweek_weekday& x, const year_lastweek_weekday& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_lastweek_weekday& x, const year_lastweek_weekday& y) NOEXCEPT { return x.year() < y.year() ? true : (x.year() > y.year() ? false : (static_cast(x.weekday()) < static_cast(y.weekday()))); } CONSTCD11 inline bool operator>(const year_lastweek_weekday& x, const year_lastweek_weekday& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_lastweek_weekday& x, const year_lastweek_weekday& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_lastweek_weekday& x, const year_lastweek_weekday& y) NOEXCEPT { return !(x < y); } CONSTCD11 inline year_lastweek_weekday operator+(const year_lastweek_weekday& ywnwd, const years& y) NOEXCEPT { return (ywnwd.year() + y) / last / ywnwd.weekday(); } CONSTCD11 inline year_lastweek_weekday operator+(const years& y, const year_lastweek_weekday& ywnwd) NOEXCEPT { return ywnwd + y; } CONSTCD11 inline year_lastweek_weekday operator-(const year_lastweek_weekday& ywnwd, const years& y) NOEXCEPT { return ywnwd + -y; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_lastweek_weekday& ywnwd) { return os << ywnwd.year() << "-W last-" << ywnwd.weekday(); } // year_weeknum_weekday CONSTCD11 inline year_weeknum_weekday::year_weeknum_weekday(const iso_week::year& y, const iso_week::weeknum& wn, const iso_week::weekday& wd) NOEXCEPT : y_(y) , wn_(wn) , wd_(wd) {} CONSTCD14 inline year_weeknum_weekday::year_weeknum_weekday(const year_lastweek_weekday& ylwwd) NOEXCEPT : y_(ylwwd.year()) , wn_(ylwwd.weeknum()) , wd_(ylwwd.weekday()) {} CONSTCD14 inline year_weeknum_weekday::year_weeknum_weekday(const sys_days& dp) NOEXCEPT : year_weeknum_weekday(from_days(dp.time_since_epoch())) {} CONSTCD14 inline year_weeknum_weekday::year_weeknum_weekday(const local_days& dp) NOEXCEPT : year_weeknum_weekday(from_days(dp.time_since_epoch())) {} inline year_weeknum_weekday& year_weeknum_weekday::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } inline year_weeknum_weekday& year_weeknum_weekday::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD11 inline year year_weeknum_weekday::year() const NOEXCEPT {return y_;} CONSTCD11 inline weeknum year_weeknum_weekday::weeknum() const NOEXCEPT {return wn_;} CONSTCD11 inline weekday year_weeknum_weekday::weekday() const NOEXCEPT {return wd_;} CONSTCD14 inline year_weeknum_weekday::operator sys_days() const NOEXCEPT { return sys_days(date::year{static_cast(y_)-1}/date::dec/date::thu[date::last]) + (date::mon - date::thu) + weeks{static_cast(wn_)-1} + (wd_ - mon); } CONSTCD14 inline year_weeknum_weekday::operator local_days() const NOEXCEPT { return local_days(date::year{static_cast(y_)-1}/date::dec/date::thu[date::last]) + (date::mon - date::thu) + weeks{static_cast(wn_)-1} + (wd_ - mon); } CONSTCD14 inline bool year_weeknum_weekday::ok() const NOEXCEPT { return y_.ok() && wd_.ok() && iso_week::weeknum{1u} <= wn_ && wn_ <= year_lastweek{y_}.weeknum(); } CONSTCD14 inline year_weeknum_weekday year_weeknum_weekday::from_days(days d) NOEXCEPT { const auto dp = sys_days{d}; const auto wd = iso_week::weekday{dp}; auto y = date::year_month_day{dp + days{3}}.year(); auto start = sys_days((y - date::years{1})/date::dec/date::thu[date::last]) + (mon-thu); if (dp < start) { --y; start = sys_days((y - date::years{1})/date::dec/date::thu[date::last]) + (mon-thu); } const auto wn = iso_week::weeknum( static_cast(date::trunc(dp - start).count() + 1)); return {iso_week::year(static_cast(y)), wn, wd}; } CONSTCD11 inline bool operator==(const year_weeknum_weekday& x, const year_weeknum_weekday& y) NOEXCEPT { return x.year() == y.year() && x.weeknum() == y.weeknum() && x.weekday() == y.weekday(); } CONSTCD11 inline bool operator!=(const year_weeknum_weekday& x, const year_weeknum_weekday& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_weeknum_weekday& x, const year_weeknum_weekday& y) NOEXCEPT { return x.year() < y.year() ? true : (x.year() > y.year() ? false : (x.weeknum() < y.weeknum() ? true : (x.weeknum() > y.weeknum() ? false : (static_cast(x.weekday()) < static_cast(y.weekday()))))); } CONSTCD11 inline bool operator>(const year_weeknum_weekday& x, const year_weeknum_weekday& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_weeknum_weekday& x, const year_weeknum_weekday& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_weeknum_weekday& x, const year_weeknum_weekday& y) NOEXCEPT { return !(x < y); } CONSTCD11 inline year_weeknum_weekday operator+(const year_weeknum_weekday& ywnwd, const years& y) NOEXCEPT { return (ywnwd.year() + y) / ywnwd.weeknum() / ywnwd.weekday(); } CONSTCD11 inline year_weeknum_weekday operator+(const years& y, const year_weeknum_weekday& ywnwd) NOEXCEPT { return ywnwd + y; } CONSTCD11 inline year_weeknum_weekday operator-(const year_weeknum_weekday& ywnwd, const years& y) NOEXCEPT { return ywnwd + -y; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_weeknum_weekday& ywnwd) { return os << ywnwd.year() << '-' << ywnwd.weeknum() << '-' << ywnwd.weekday(); } // date composition operators CONSTCD11 inline year_weeknum operator/(const year& y, const weeknum& wn) NOEXCEPT { return {y, wn}; } CONSTCD11 inline year_weeknum operator/(const year& y, int wn) NOEXCEPT { return y/weeknum(static_cast(wn)); } CONSTCD11 inline year_lastweek operator/(const year& y, last_week) NOEXCEPT { return year_lastweek{y}; } CONSTCD11 inline weeknum_weekday operator/(const weeknum& wn, const weekday& wd) NOEXCEPT { return {wn, wd}; } CONSTCD11 inline weeknum_weekday operator/(const weeknum& wn, int wd) NOEXCEPT { return wn/weekday{static_cast(wd)}; } CONSTCD11 inline weeknum_weekday operator/(const weekday& wd, const weeknum& wn) NOEXCEPT { return wn/wd; } CONSTCD11 inline weeknum_weekday operator/(const weekday& wd, int wn) NOEXCEPT { return weeknum{static_cast(wn)}/wd; } CONSTCD11 inline lastweek_weekday operator/(const last_week&, const weekday& wd) NOEXCEPT { return lastweek_weekday{wd}; } CONSTCD11 inline lastweek_weekday operator/(const last_week& wn, int wd) NOEXCEPT { return wn / weekday{static_cast(wd)}; } CONSTCD11 inline lastweek_weekday operator/(const weekday& wd, const last_week& wn) NOEXCEPT { return wn / wd; } CONSTCD11 inline year_weeknum_weekday operator/(const year_weeknum& ywn, const weekday& wd) NOEXCEPT { return {ywn.year(), ywn.weeknum(), wd}; } CONSTCD11 inline year_weeknum_weekday operator/(const year_weeknum& ywn, int wd) NOEXCEPT { return ywn / weekday(static_cast(wd)); } CONSTCD11 inline year_weeknum_weekday operator/(const weeknum_weekday& wnwd, const year& y) NOEXCEPT { return {y, wnwd.weeknum(), wnwd.weekday()}; } CONSTCD11 inline year_weeknum_weekday operator/(const weeknum_weekday& wnwd, int y) NOEXCEPT { return wnwd / year{y}; } CONSTCD11 inline year_lastweek_weekday operator/(const year_lastweek& ylw, const weekday& wd) NOEXCEPT { return {ylw.year(), wd}; } CONSTCD11 inline year_lastweek_weekday operator/(const year_lastweek& ylw, int wd) NOEXCEPT { return ylw / weekday(static_cast(wd)); } CONSTCD11 inline year_lastweek_weekday operator/(const lastweek_weekday& lwwd, const year& y) NOEXCEPT { return {y, lwwd.weekday()}; } CONSTCD11 inline year_lastweek_weekday operator/(const lastweek_weekday& lwwd, int y) NOEXCEPT { return lwwd / year{y}; } } // namespace iso_week #endif // ISO_WEEK_H pr0m1th3as-datatypes-9450b29/src/date/julian.h000066400000000000000000002144541527370255000210470ustar00rootroot00000000000000#ifndef JULIAN_H #define JULIAN_H // The MIT License (MIT) // // Copyright (c) 2016 Howard Hinnant // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // Our apologies. When the previous paragraph was written, lowercase had not yet // been invented (that would involve another several millennia of evolution). // We did not mean to shout. #include "date.h" namespace julian { // durations using days = date::days; using weeks = date::weeks; using years = std::chrono::duration , days::period>>; using months = std::chrono::duration >>; // time_point using sys_days = date::sys_days; using local_days = date::local_days; // types struct last_spec { explicit last_spec() = default; }; class day; class month; class year; class weekday; class weekday_indexed; class weekday_last; class month_day; class month_day_last; class month_weekday; class month_weekday_last; class year_month; class year_month_day; class year_month_day_last; class year_month_weekday; class year_month_weekday_last; // date composition operators CONSTCD11 year_month operator/(const year& y, const month& m) NOEXCEPT; CONSTCD11 year_month operator/(const year& y, int m) NOEXCEPT; CONSTCD11 month_day operator/(const day& d, const month& m) NOEXCEPT; CONSTCD11 month_day operator/(const day& d, int m) NOEXCEPT; CONSTCD11 month_day operator/(const month& m, const day& d) NOEXCEPT; CONSTCD11 month_day operator/(const month& m, int d) NOEXCEPT; CONSTCD11 month_day operator/(int m, const day& d) NOEXCEPT; CONSTCD11 month_day_last operator/(const month& m, last_spec) NOEXCEPT; CONSTCD11 month_day_last operator/(int m, last_spec) NOEXCEPT; CONSTCD11 month_day_last operator/(last_spec, const month& m) NOEXCEPT; CONSTCD11 month_day_last operator/(last_spec, int m) NOEXCEPT; CONSTCD11 month_weekday operator/(const month& m, const weekday_indexed& wdi) NOEXCEPT; CONSTCD11 month_weekday operator/(int m, const weekday_indexed& wdi) NOEXCEPT; CONSTCD11 month_weekday operator/(const weekday_indexed& wdi, const month& m) NOEXCEPT; CONSTCD11 month_weekday operator/(const weekday_indexed& wdi, int m) NOEXCEPT; CONSTCD11 month_weekday_last operator/(const month& m, const weekday_last& wdl) NOEXCEPT; CONSTCD11 month_weekday_last operator/(int m, const weekday_last& wdl) NOEXCEPT; CONSTCD11 month_weekday_last operator/(const weekday_last& wdl, const month& m) NOEXCEPT; CONSTCD11 month_weekday_last operator/(const weekday_last& wdl, int m) NOEXCEPT; CONSTCD11 year_month_day operator/(const year_month& ym, const day& d) NOEXCEPT; CONSTCD11 year_month_day operator/(const year_month& ym, int d) NOEXCEPT; CONSTCD11 year_month_day operator/(const year& y, const month_day& md) NOEXCEPT; CONSTCD11 year_month_day operator/(int y, const month_day& md) NOEXCEPT; CONSTCD11 year_month_day operator/(const month_day& md, const year& y) NOEXCEPT; CONSTCD11 year_month_day operator/(const month_day& md, int y) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const year_month& ym, last_spec) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const year& y, const month_day_last& mdl) NOEXCEPT; CONSTCD11 year_month_day_last operator/(int y, const month_day_last& mdl) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const month_day_last& mdl, const year& y) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const month_day_last& mdl, int y) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const year_month& ym, const weekday_indexed& wdi) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const year& y, const month_weekday& mwd) NOEXCEPT; CONSTCD11 year_month_weekday operator/(int y, const month_weekday& mwd) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const month_weekday& mwd, const year& y) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const month_weekday& mwd, int y) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const year_month& ym, const weekday_last& wdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const year& y, const month_weekday_last& mwdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(int y, const month_weekday_last& mwdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const month_weekday_last& mwdl, const year& y) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const month_weekday_last& mwdl, int y) NOEXCEPT; // Detailed interface // day class day { unsigned char d_; public: explicit CONSTCD11 day(unsigned d) NOEXCEPT; CONSTCD14 day& operator++() NOEXCEPT; CONSTCD14 day operator++(int) NOEXCEPT; CONSTCD14 day& operator--() NOEXCEPT; CONSTCD14 day operator--(int) NOEXCEPT; CONSTCD14 day& operator+=(const days& d) NOEXCEPT; CONSTCD14 day& operator-=(const days& d) NOEXCEPT; CONSTCD11 explicit operator unsigned() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator!=(const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator< (const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator> (const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator<=(const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator>=(const day& x, const day& y) NOEXCEPT; CONSTCD11 day operator+(const day& x, const days& y) NOEXCEPT; CONSTCD11 day operator+(const days& x, const day& y) NOEXCEPT; CONSTCD11 day operator-(const day& x, const days& y) NOEXCEPT; CONSTCD11 days operator-(const day& x, const day& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const day& d); // month class month { unsigned char m_; public: explicit CONSTCD11 month(unsigned m) NOEXCEPT; CONSTCD14 month& operator++() NOEXCEPT; CONSTCD14 month operator++(int) NOEXCEPT; CONSTCD14 month& operator--() NOEXCEPT; CONSTCD14 month operator--(int) NOEXCEPT; CONSTCD14 month& operator+=(const months& m) NOEXCEPT; CONSTCD14 month& operator-=(const months& m) NOEXCEPT; CONSTCD11 explicit operator unsigned() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator!=(const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator< (const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator> (const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator<=(const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator>=(const month& x, const month& y) NOEXCEPT; CONSTCD14 month operator+(const month& x, const months& y) NOEXCEPT; CONSTCD14 month operator+(const months& x, const month& y) NOEXCEPT; CONSTCD14 month operator-(const month& x, const months& y) NOEXCEPT; CONSTCD14 months operator-(const month& x, const month& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month& m); // year class year { short y_; public: explicit CONSTCD11 year(int y) NOEXCEPT; CONSTCD14 year& operator++() NOEXCEPT; CONSTCD14 year operator++(int) NOEXCEPT; CONSTCD14 year& operator--() NOEXCEPT; CONSTCD14 year operator--(int) NOEXCEPT; CONSTCD14 year& operator+=(const years& y) NOEXCEPT; CONSTCD14 year& operator-=(const years& y) NOEXCEPT; CONSTCD11 bool is_leap() const NOEXCEPT; CONSTCD11 explicit operator int() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; static CONSTCD11 year min() NOEXCEPT; static CONSTCD11 year max() NOEXCEPT; }; CONSTCD11 bool operator==(const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator!=(const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator< (const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator> (const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator<=(const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator>=(const year& x, const year& y) NOEXCEPT; CONSTCD11 year operator+(const year& x, const years& y) NOEXCEPT; CONSTCD11 year operator+(const years& x, const year& y) NOEXCEPT; CONSTCD11 year operator-(const year& x, const years& y) NOEXCEPT; CONSTCD11 years operator-(const year& x, const year& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year& y); // weekday class weekday { unsigned char wd_; public: explicit CONSTCD11 weekday(unsigned wd) NOEXCEPT; explicit weekday(int) = delete; CONSTCD11 weekday(const sys_days& dp) NOEXCEPT; CONSTCD11 explicit weekday(const local_days& dp) NOEXCEPT; CONSTCD14 weekday& operator++() NOEXCEPT; CONSTCD14 weekday operator++(int) NOEXCEPT; CONSTCD14 weekday& operator--() NOEXCEPT; CONSTCD14 weekday operator--(int) NOEXCEPT; CONSTCD14 weekday& operator+=(const days& d) NOEXCEPT; CONSTCD14 weekday& operator-=(const days& d) NOEXCEPT; CONSTCD11 explicit operator unsigned() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; CONSTCD11 weekday_indexed operator[](unsigned index) const NOEXCEPT; CONSTCD11 weekday_last operator[](last_spec) const NOEXCEPT; private: static CONSTCD11 unsigned char weekday_from_days(int z) NOEXCEPT; }; CONSTCD11 bool operator==(const weekday& x, const weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const weekday& x, const weekday& y) NOEXCEPT; CONSTCD14 weekday operator+(const weekday& x, const days& y) NOEXCEPT; CONSTCD14 weekday operator+(const days& x, const weekday& y) NOEXCEPT; CONSTCD14 weekday operator-(const weekday& x, const days& y) NOEXCEPT; CONSTCD14 days operator-(const weekday& x, const weekday& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const weekday& wd); // weekday_indexed class weekday_indexed { unsigned char wd_ : 4; unsigned char index_ : 4; public: CONSTCD11 weekday_indexed(const julian::weekday& wd, unsigned index) NOEXCEPT; CONSTCD11 julian::weekday weekday() const NOEXCEPT; CONSTCD11 unsigned index() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT; CONSTCD11 bool operator!=(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_indexed& wdi); // weekday_last class weekday_last { julian::weekday wd_; public: explicit CONSTCD11 weekday_last(const julian::weekday& wd) NOEXCEPT; CONSTCD11 julian::weekday weekday() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const weekday_last& x, const weekday_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const weekday_last& x, const weekday_last& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_last& wdl); // year_month class year_month { julian::year y_; julian::month m_; public: CONSTCD11 year_month(const julian::year& y, const julian::month& m) NOEXCEPT; CONSTCD11 julian::year year() const NOEXCEPT; CONSTCD11 julian::month month() const NOEXCEPT; CONSTCD14 year_month& operator+=(const months& dm) NOEXCEPT; CONSTCD14 year_month& operator-=(const months& dm) NOEXCEPT; CONSTCD14 year_month& operator+=(const years& dy) NOEXCEPT; CONSTCD14 year_month& operator-=(const years& dy) NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator< (const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator> (const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD14 year_month operator+(const year_month& ym, const months& dm) NOEXCEPT; CONSTCD14 year_month operator+(const months& dm, const year_month& ym) NOEXCEPT; CONSTCD14 year_month operator-(const year_month& ym, const months& dm) NOEXCEPT; CONSTCD11 months operator-(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 year_month operator+(const year_month& ym, const years& dy) NOEXCEPT; CONSTCD11 year_month operator+(const years& dy, const year_month& ym) NOEXCEPT; CONSTCD11 year_month operator-(const year_month& ym, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month& ym); // month_day class month_day { julian::month m_; julian::day d_; public: CONSTCD11 month_day(const julian::month& m, const julian::day& d) NOEXCEPT; CONSTCD11 julian::month month() const NOEXCEPT; CONSTCD11 julian::day day() const NOEXCEPT; CONSTCD14 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator< (const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator> (const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator<=(const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator>=(const month_day& x, const month_day& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_day& md); // month_day_last class month_day_last { julian::month m_; public: CONSTCD11 explicit month_day_last(const julian::month& m) NOEXCEPT; CONSTCD11 julian::month month() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator< (const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator> (const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator<=(const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator>=(const month_day_last& x, const month_day_last& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_day_last& mdl); // month_weekday class month_weekday { julian::month m_; julian::weekday_indexed wdi_; public: CONSTCD11 month_weekday(const julian::month& m, const julian::weekday_indexed& wdi) NOEXCEPT; CONSTCD11 julian::month month() const NOEXCEPT; CONSTCD11 julian::weekday_indexed weekday_indexed() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_weekday& x, const month_weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_weekday& x, const month_weekday& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday& mwd); // month_weekday_last class month_weekday_last { julian::month m_; julian::weekday_last wdl_; public: CONSTCD11 month_weekday_last(const julian::month& m, const julian::weekday_last& wd) NOEXCEPT; CONSTCD11 julian::month month() const NOEXCEPT; CONSTCD11 julian::weekday_last weekday_last() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday_last& mwdl); // class year_month_day class year_month_day { julian::year y_; julian::month m_; julian::day d_; public: CONSTCD11 year_month_day(const julian::year& y, const julian::month& m, const julian::day& d) NOEXCEPT; CONSTCD14 year_month_day(const year_month_day_last& ymdl) NOEXCEPT; CONSTCD14 year_month_day(sys_days dp) NOEXCEPT; CONSTCD14 explicit year_month_day(local_days dp) NOEXCEPT; CONSTCD14 year_month_day& operator+=(const months& m) NOEXCEPT; CONSTCD14 year_month_day& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_day& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_day& operator-=(const years& y) NOEXCEPT; CONSTCD11 julian::year year() const NOEXCEPT; CONSTCD11 julian::month month() const NOEXCEPT; CONSTCD11 julian::day day() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD14 bool ok() const NOEXCEPT; private: static CONSTCD14 year_month_day from_days(days dp) NOEXCEPT; CONSTCD14 days to_days() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator< (const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator> (const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD14 year_month_day operator+(const year_month_day& ymd, const months& dm) NOEXCEPT; CONSTCD14 year_month_day operator+(const months& dm, const year_month_day& ymd) NOEXCEPT; CONSTCD14 year_month_day operator-(const year_month_day& ymd, const months& dm) NOEXCEPT; CONSTCD11 year_month_day operator+(const year_month_day& ymd, const years& dy) NOEXCEPT; CONSTCD11 year_month_day operator+(const years& dy, const year_month_day& ymd) NOEXCEPT; CONSTCD11 year_month_day operator-(const year_month_day& ymd, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day& ymd); // year_month_day_last class year_month_day_last { julian::year y_; julian::month_day_last mdl_; public: CONSTCD11 year_month_day_last(const julian::year& y, const julian::month_day_last& mdl) NOEXCEPT; CONSTCD14 year_month_day_last& operator+=(const months& m) NOEXCEPT; CONSTCD14 year_month_day_last& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_day_last& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_day_last& operator-=(const years& y) NOEXCEPT; CONSTCD11 julian::year year() const NOEXCEPT; CONSTCD11 julian::month month() const NOEXCEPT; CONSTCD11 julian::month_day_last month_day_last() const NOEXCEPT; CONSTCD14 julian::day day() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator< (const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator> (const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD14 year_month_day_last operator+(const year_month_day_last& ymdl, const months& dm) NOEXCEPT; CONSTCD14 year_month_day_last operator+(const months& dm, const year_month_day_last& ymdl) NOEXCEPT; CONSTCD11 year_month_day_last operator+(const year_month_day_last& ymdl, const years& dy) NOEXCEPT; CONSTCD11 year_month_day_last operator+(const years& dy, const year_month_day_last& ymdl) NOEXCEPT; CONSTCD14 year_month_day_last operator-(const year_month_day_last& ymdl, const months& dm) NOEXCEPT; CONSTCD11 year_month_day_last operator-(const year_month_day_last& ymdl, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day_last& ymdl); // year_month_weekday class year_month_weekday { julian::year y_; julian::month m_; julian::weekday_indexed wdi_; public: CONSTCD11 year_month_weekday(const julian::year& y, const julian::month& m, const julian::weekday_indexed& wdi) NOEXCEPT; CONSTCD14 year_month_weekday(const sys_days& dp) NOEXCEPT; CONSTCD14 explicit year_month_weekday(const local_days& dp) NOEXCEPT; CONSTCD14 year_month_weekday& operator+=(const months& m) NOEXCEPT; CONSTCD14 year_month_weekday& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_weekday& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_weekday& operator-=(const years& y) NOEXCEPT; CONSTCD11 julian::year year() const NOEXCEPT; CONSTCD11 julian::month month() const NOEXCEPT; CONSTCD11 julian::weekday weekday() const NOEXCEPT; CONSTCD11 unsigned index() const NOEXCEPT; CONSTCD11 julian::weekday_indexed weekday_indexed() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD14 bool ok() const NOEXCEPT; private: static CONSTCD14 year_month_weekday from_days(days dp) NOEXCEPT; CONSTCD14 days to_days() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT; CONSTCD14 year_month_weekday operator+(const year_month_weekday& ymwd, const months& dm) NOEXCEPT; CONSTCD14 year_month_weekday operator+(const months& dm, const year_month_weekday& ymwd) NOEXCEPT; CONSTCD11 year_month_weekday operator+(const year_month_weekday& ymwd, const years& dy) NOEXCEPT; CONSTCD11 year_month_weekday operator+(const years& dy, const year_month_weekday& ymwd) NOEXCEPT; CONSTCD14 year_month_weekday operator-(const year_month_weekday& ymwd, const months& dm) NOEXCEPT; CONSTCD11 year_month_weekday operator-(const year_month_weekday& ymwd, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday& ymwdi); // year_month_weekday_last class year_month_weekday_last { julian::year y_; julian::month m_; julian::weekday_last wdl_; public: CONSTCD11 year_month_weekday_last(const julian::year& y, const julian::month& m, const julian::weekday_last& wdl) NOEXCEPT; CONSTCD14 year_month_weekday_last& operator+=(const months& m) NOEXCEPT; CONSTCD14 year_month_weekday_last& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_weekday_last& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_weekday_last& operator-=(const years& y) NOEXCEPT; CONSTCD11 julian::year year() const NOEXCEPT; CONSTCD11 julian::month month() const NOEXCEPT; CONSTCD11 julian::weekday weekday() const NOEXCEPT; CONSTCD11 julian::weekday_last weekday_last() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; private: CONSTCD14 days to_days() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT; CONSTCD14 year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT; CONSTCD14 year_month_weekday_last operator+(const months& dm, const year_month_weekday_last& ymwdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT; CONSTCD11 year_month_weekday_last operator+(const years& dy, const year_month_weekday_last& ymwdl) NOEXCEPT; CONSTCD14 year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT; CONSTCD11 year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday_last& ymwdl); #if !defined(_MSC_VER) || (_MSC_VER >= 1900) inline namespace literals { CONSTCD11 julian::day operator ""_d(unsigned long long d) NOEXCEPT; CONSTCD11 julian::year operator ""_y(unsigned long long y) NOEXCEPT; // CONSTDATA julian::month jan{1}; // CONSTDATA julian::month feb{2}; // CONSTDATA julian::month mar{3}; // CONSTDATA julian::month apr{4}; // CONSTDATA julian::month may{5}; // CONSTDATA julian::month jun{6}; // CONSTDATA julian::month jul{7}; // CONSTDATA julian::month aug{8}; // CONSTDATA julian::month sep{9}; // CONSTDATA julian::month oct{10}; // CONSTDATA julian::month nov{11}; // CONSTDATA julian::month dec{12}; } // inline namespace literals #endif // !defined(_MSC_VER) || (_MSC_VER >= 1900) //----------------+ // Implementation | //----------------+ // day CONSTCD11 inline day::day(unsigned d) NOEXCEPT : d_(static_cast(d)) {} CONSTCD14 inline day& day::operator++() NOEXCEPT {++d_; return *this;} CONSTCD14 inline day day::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline day& day::operator--() NOEXCEPT {--d_; return *this;} CONSTCD14 inline day day::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline day& day::operator+=(const days& d) NOEXCEPT {*this = *this + d; return *this;} CONSTCD14 inline day& day::operator-=(const days& d) NOEXCEPT {*this = *this - d; return *this;} CONSTCD11 inline day::operator unsigned() const NOEXCEPT {return d_;} CONSTCD11 inline bool day::ok() const NOEXCEPT {return 1 <= d_ && d_ <= 31;} CONSTCD11 inline bool operator==(const day& x, const day& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const day& x, const day& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const day& x, const day& y) NOEXCEPT { return static_cast(x) < static_cast(y); } CONSTCD11 inline bool operator>(const day& x, const day& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const day& x, const day& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const day& x, const day& y) NOEXCEPT { return !(x < y); } CONSTCD11 inline days operator-(const day& x, const day& y) NOEXCEPT { return days{static_cast(static_cast(x) - static_cast(y))}; } CONSTCD11 inline day operator+(const day& x, const days& y) NOEXCEPT { return day{static_cast(x) + static_cast(y.count())}; } CONSTCD11 inline day operator+(const days& x, const day& y) NOEXCEPT { return y + x; } CONSTCD11 inline day operator-(const day& x, const days& y) NOEXCEPT { return x + -y; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const day& d) { date::detail::save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::right); os.width(2); os << static_cast(d); return os; } // month CONSTCD11 inline month::month(unsigned m) NOEXCEPT : m_(static_cast(m)) {} CONSTCD14 inline month& month::operator++() NOEXCEPT {if (++m_ == 13) m_ = 1; return *this;} CONSTCD14 inline month month::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline month& month::operator--() NOEXCEPT {if (--m_ == 0) m_ = 12; return *this;} CONSTCD14 inline month month::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline month& month::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline month& month::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD11 inline month::operator unsigned() const NOEXCEPT {return m_;} CONSTCD11 inline bool month::ok() const NOEXCEPT {return 1 <= m_ && m_ <= 12;} CONSTCD11 inline bool operator==(const month& x, const month& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const month& x, const month& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const month& x, const month& y) NOEXCEPT { return static_cast(x) < static_cast(y); } CONSTCD11 inline bool operator>(const month& x, const month& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const month& x, const month& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const month& x, const month& y) NOEXCEPT { return !(x < y); } CONSTCD14 inline months operator-(const month& x, const month& y) NOEXCEPT { auto const d = static_cast(x) - static_cast(y); return months(d <= 11 ? d : d + 12); } CONSTCD14 inline month operator+(const month& x, const months& y) NOEXCEPT { auto const mu = static_cast(static_cast(x)) - 1 + y.count(); auto const yr = (mu >= 0 ? mu : mu-11) / 12; return month{static_cast(mu - yr * 12 + 1)}; } CONSTCD14 inline month operator+(const months& x, const month& y) NOEXCEPT { return y + x; } CONSTCD14 inline month operator-(const month& x, const months& y) NOEXCEPT { return x + -y; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month& m) { switch (static_cast(m)) { case 1: os << "Jan"; break; case 2: os << "Feb"; break; case 3: os << "Mar"; break; case 4: os << "Apr"; break; case 5: os << "May"; break; case 6: os << "Jun"; break; case 7: os << "Jul"; break; case 8: os << "Aug"; break; case 9: os << "Sep"; break; case 10: os << "Oct"; break; case 11: os << "Nov"; break; case 12: os << "Dec"; break; default: os << static_cast(m) << " is not a valid month"; break; } return os; } // year CONSTCD11 inline year::year(int y) NOEXCEPT : y_(static_cast(y)) {} CONSTCD14 inline year& year::operator++() NOEXCEPT {++y_; return *this;} CONSTCD14 inline year year::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline year& year::operator--() NOEXCEPT {--y_; return *this;} CONSTCD14 inline year year::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline year& year::operator+=(const years& y) NOEXCEPT {*this = *this + y; return *this;} CONSTCD14 inline year& year::operator-=(const years& y) NOEXCEPT {*this = *this - y; return *this;} CONSTCD11 inline bool year::is_leap() const NOEXCEPT { return y_ % 4 == 0; } CONSTCD11 inline year::operator int() const NOEXCEPT {return y_;} CONSTCD11 inline bool year::ok() const NOEXCEPT {return true;} CONSTCD11 inline year year::min() NOEXCEPT { return year{std::numeric_limits::min()}; } CONSTCD11 inline year year::max() NOEXCEPT { return year{std::numeric_limits::max()}; } CONSTCD11 inline bool operator==(const year& x, const year& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const year& x, const year& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year& x, const year& y) NOEXCEPT { return static_cast(x) < static_cast(y); } CONSTCD11 inline bool operator>(const year& x, const year& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year& x, const year& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year& x, const year& y) NOEXCEPT { return !(x < y); } CONSTCD11 inline years operator-(const year& x, const year& y) NOEXCEPT { return years{static_cast(x) - static_cast(y)}; } CONSTCD11 inline year operator+(const year& x, const years& y) NOEXCEPT { return year{static_cast(x) + y.count()}; } CONSTCD11 inline year operator+(const years& x, const year& y) NOEXCEPT { return y + x; } CONSTCD11 inline year operator-(const year& x, const years& y) NOEXCEPT { return year{static_cast(x) - y.count()}; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year& y) { date::detail::save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::internal); os.width(4 + (y < year{0})); os << static_cast(y); return os; } // weekday CONSTCD11 inline unsigned char weekday::weekday_from_days(int z) NOEXCEPT { return static_cast(static_cast( z >= -4 ? (z+4) % 7 : (z+5) % 7 + 6)); } CONSTCD11 inline weekday::weekday(unsigned wd) NOEXCEPT : wd_(static_cast(wd)) {} CONSTCD11 inline weekday::weekday(const sys_days& dp) NOEXCEPT : wd_(weekday_from_days(dp.time_since_epoch().count())) {} CONSTCD11 inline weekday::weekday(const local_days& dp) NOEXCEPT : wd_(weekday_from_days(dp.time_since_epoch().count())) {} CONSTCD14 inline weekday& weekday::operator++() NOEXCEPT {if (++wd_ == 7) wd_ = 0; return *this;} CONSTCD14 inline weekday weekday::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline weekday& weekday::operator--() NOEXCEPT {if (wd_-- == 0) wd_ = 6; return *this;} CONSTCD14 inline weekday weekday::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline weekday& weekday::operator+=(const days& d) NOEXCEPT { *this = *this + d; return *this; } CONSTCD14 inline weekday& weekday::operator-=(const days& d) NOEXCEPT { *this = *this - d; return *this; } CONSTCD11 inline weekday::operator unsigned() const NOEXCEPT { return static_cast(wd_); } CONSTCD11 inline bool weekday::ok() const NOEXCEPT {return wd_ <= 6;} CONSTCD11 inline bool operator==(const weekday& x, const weekday& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const weekday& x, const weekday& y) NOEXCEPT { return !(x == y); } CONSTCD14 inline days operator-(const weekday& x, const weekday& y) NOEXCEPT { auto const diff = static_cast(x) - static_cast(y); return days{diff <= 6 ? diff : diff + 7}; } CONSTCD14 inline weekday operator+(const weekday& x, const days& y) NOEXCEPT { auto const wdu = static_cast(static_cast(x)) + y.count(); auto const wk = (wdu >= 0 ? wdu : wdu-6) / 7; return weekday{static_cast(wdu - wk * 7)}; } CONSTCD14 inline weekday operator+(const days& x, const weekday& y) NOEXCEPT { return y + x; } CONSTCD14 inline weekday operator-(const weekday& x, const days& y) NOEXCEPT { return x + -y; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const weekday& wd) { switch (static_cast(wd)) { case 0: os << "Sun"; break; case 1: os << "Mon"; break; case 2: os << "Tue"; break; case 3: os << "Wed"; break; case 4: os << "Thu"; break; case 5: os << "Fri"; break; case 6: os << "Sat"; break; default: os << static_cast(wd) << " is not a valid weekday"; break; } return os; } #if !defined(_MSC_VER) || (_MSC_VER >= 1900) inline namespace literals { CONSTCD11 inline julian::day operator ""_d(unsigned long long d) NOEXCEPT { return julian::day{static_cast(d)}; } CONSTCD11 inline julian::year operator ""_y(unsigned long long y) NOEXCEPT { return julian::year(static_cast(y)); } #endif // !defined(_MSC_VER) || (_MSC_VER >= 1900) CONSTDATA julian::last_spec last{}; CONSTDATA julian::month jan{1}; CONSTDATA julian::month feb{2}; CONSTDATA julian::month mar{3}; CONSTDATA julian::month apr{4}; CONSTDATA julian::month may{5}; CONSTDATA julian::month jun{6}; CONSTDATA julian::month jul{7}; CONSTDATA julian::month aug{8}; CONSTDATA julian::month sep{9}; CONSTDATA julian::month oct{10}; CONSTDATA julian::month nov{11}; CONSTDATA julian::month dec{12}; CONSTDATA julian::weekday sun{0u}; CONSTDATA julian::weekday mon{1u}; CONSTDATA julian::weekday tue{2u}; CONSTDATA julian::weekday wed{3u}; CONSTDATA julian::weekday thu{4u}; CONSTDATA julian::weekday fri{5u}; CONSTDATA julian::weekday sat{6u}; #if !defined(_MSC_VER) || (_MSC_VER >= 1900) } // inline namespace literals #endif // weekday_indexed CONSTCD11 inline weekday weekday_indexed::weekday() const NOEXCEPT { return julian::weekday{static_cast(wd_)}; } CONSTCD11 inline unsigned weekday_indexed::index() const NOEXCEPT {return index_;} CONSTCD11 inline bool weekday_indexed::ok() const NOEXCEPT { return weekday().ok() && 1 <= index_ && index_ <= 5; } CONSTCD11 inline weekday_indexed::weekday_indexed(const julian::weekday& wd, unsigned index) NOEXCEPT : wd_(static_cast(static_cast(wd))) , index_(static_cast(index)) {} template inline std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_indexed& wdi) { return os << wdi.weekday() << '[' << wdi.index() << ']'; } CONSTCD11 inline weekday_indexed weekday::operator[](unsigned index) const NOEXCEPT { return {*this, index}; } CONSTCD11 inline bool operator==(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT { return x.weekday() == y.weekday() && x.index() == y.index(); } CONSTCD11 inline bool operator!=(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT { return !(x == y); } // weekday_last CONSTCD11 inline julian::weekday weekday_last::weekday() const NOEXCEPT {return wd_;} CONSTCD11 inline bool weekday_last::ok() const NOEXCEPT {return wd_.ok();} CONSTCD11 inline weekday_last::weekday_last(const julian::weekday& wd) NOEXCEPT : wd_(wd) {} CONSTCD11 inline bool operator==(const weekday_last& x, const weekday_last& y) NOEXCEPT { return x.weekday() == y.weekday(); } CONSTCD11 inline bool operator!=(const weekday_last& x, const weekday_last& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_last& wdl) { return os << wdl.weekday() << "[last]"; } CONSTCD11 inline weekday_last weekday::operator[](last_spec) const NOEXCEPT { return weekday_last{*this}; } // year_month CONSTCD11 inline year_month::year_month(const julian::year& y, const julian::month& m) NOEXCEPT : y_(y) , m_(m) {} CONSTCD11 inline year year_month::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month::month() const NOEXCEPT {return m_;} CONSTCD11 inline bool year_month::ok() const NOEXCEPT {return y_.ok() && m_.ok();} CONSTCD14 inline year_month& year_month::operator+=(const months& dm) NOEXCEPT { *this = *this + dm; return *this; } CONSTCD14 inline year_month& year_month::operator-=(const months& dm) NOEXCEPT { *this = *this - dm; return *this; } CONSTCD14 inline year_month& year_month::operator+=(const years& dy) NOEXCEPT { *this = *this + dy; return *this; } CONSTCD14 inline year_month& year_month::operator-=(const years& dy) NOEXCEPT { *this = *this - dy; return *this; } CONSTCD11 inline bool operator==(const year_month& x, const year_month& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month(); } CONSTCD11 inline bool operator!=(const year_month& x, const year_month& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_month& x, const year_month& y) NOEXCEPT { return x.year() < y.year() ? true : (x.year() > y.year() ? false : (x.month() < y.month())); } CONSTCD11 inline bool operator>(const year_month& x, const year_month& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_month& x, const year_month& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_month& x, const year_month& y) NOEXCEPT { return !(x < y); } CONSTCD14 inline year_month operator+(const year_month& ym, const months& dm) NOEXCEPT { auto dmi = static_cast(static_cast(ym.month())) - 1 + dm.count(); auto dy = (dmi >= 0 ? dmi : dmi-11) / 12; dmi = dmi - dy * 12 + 1; return (ym.year() + years(dy)) / month(static_cast(dmi)); } CONSTCD14 inline year_month operator+(const months& dm, const year_month& ym) NOEXCEPT { return ym + dm; } CONSTCD14 inline year_month operator-(const year_month& ym, const months& dm) NOEXCEPT { return ym + -dm; } CONSTCD11 inline months operator-(const year_month& x, const year_month& y) NOEXCEPT { return (x.year() - y.year()) + months(static_cast(x.month()) - static_cast(y.month())); } CONSTCD11 inline year_month operator+(const year_month& ym, const years& dy) NOEXCEPT { return (ym.year() + dy) / ym.month(); } CONSTCD11 inline year_month operator+(const years& dy, const year_month& ym) NOEXCEPT { return ym + dy; } CONSTCD11 inline year_month operator-(const year_month& ym, const years& dy) NOEXCEPT { return ym + -dy; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month& ym) { return os << ym.year() << '/' << ym.month(); } // month_day CONSTCD11 inline month_day::month_day(const julian::month& m, const julian::day& d) NOEXCEPT : m_(m) , d_(d) {} CONSTCD11 inline julian::month month_day::month() const NOEXCEPT {return m_;} CONSTCD11 inline julian::day month_day::day() const NOEXCEPT {return d_;} CONSTCD14 inline bool month_day::ok() const NOEXCEPT { CONSTDATA julian::day d[] = { julian::day(31), julian::day(29), julian::day(31), julian::day(30), julian::day(31), julian::day(30), julian::day(31), julian::day(31), julian::day(30), julian::day(31), julian::day(30), julian::day(31) }; return m_.ok() && julian::day(1) <= d_ && d_ <= d[static_cast(m_)-1]; } CONSTCD11 inline bool operator==(const month_day& x, const month_day& y) NOEXCEPT { return x.month() == y.month() && x.day() == y.day(); } CONSTCD11 inline bool operator!=(const month_day& x, const month_day& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const month_day& x, const month_day& y) NOEXCEPT { return x.month() < y.month() ? true : (x.month() > y.month() ? false : (x.day() < y.day())); } CONSTCD11 inline bool operator>(const month_day& x, const month_day& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const month_day& x, const month_day& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const month_day& x, const month_day& y) NOEXCEPT { return !(x < y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_day& md) { return os << md.month() << '/' << md.day(); } // month_day_last CONSTCD11 inline month month_day_last::month() const NOEXCEPT {return m_;} CONSTCD11 inline bool month_day_last::ok() const NOEXCEPT {return m_.ok();} CONSTCD11 inline month_day_last::month_day_last(const julian::month& m) NOEXCEPT : m_(m) {} CONSTCD11 inline bool operator==(const month_day_last& x, const month_day_last& y) NOEXCEPT { return x.month() == y.month(); } CONSTCD11 inline bool operator!=(const month_day_last& x, const month_day_last& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const month_day_last& x, const month_day_last& y) NOEXCEPT { return x.month() < y.month(); } CONSTCD11 inline bool operator>(const month_day_last& x, const month_day_last& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const month_day_last& x, const month_day_last& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const month_day_last& x, const month_day_last& y) NOEXCEPT { return !(x < y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_day_last& mdl) { return os << mdl.month() << "/last"; } // month_weekday CONSTCD11 inline month_weekday::month_weekday(const julian::month& m, const julian::weekday_indexed& wdi) NOEXCEPT : m_(m) , wdi_(wdi) {} CONSTCD11 inline month month_weekday::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday_indexed month_weekday::weekday_indexed() const NOEXCEPT { return wdi_; } CONSTCD11 inline bool month_weekday::ok() const NOEXCEPT { return m_.ok() && wdi_.ok(); } CONSTCD11 inline bool operator==(const month_weekday& x, const month_weekday& y) NOEXCEPT { return x.month() == y.month() && x.weekday_indexed() == y.weekday_indexed(); } CONSTCD11 inline bool operator!=(const month_weekday& x, const month_weekday& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday& mwd) { return os << mwd.month() << '/' << mwd.weekday_indexed(); } // month_weekday_last CONSTCD11 inline month_weekday_last::month_weekday_last(const julian::month& m, const julian::weekday_last& wdl) NOEXCEPT : m_(m) , wdl_(wdl) {} CONSTCD11 inline month month_weekday_last::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday_last month_weekday_last::weekday_last() const NOEXCEPT { return wdl_; } CONSTCD11 inline bool month_weekday_last::ok() const NOEXCEPT { return m_.ok() && wdl_.ok(); } CONSTCD11 inline bool operator==(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT { return x.month() == y.month() && x.weekday_last() == y.weekday_last(); } CONSTCD11 inline bool operator!=(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday_last& mwdl) { return os << mwdl.month() << '/' << mwdl.weekday_last(); } // year_month_day_last CONSTCD11 inline year_month_day_last::year_month_day_last(const julian::year& y, const julian::month_day_last& mdl) NOEXCEPT : y_(y) , mdl_(mdl) {} CONSTCD14 inline year_month_day_last& year_month_day_last::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline year_month_day_last& year_month_day_last::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_day_last& year_month_day_last::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_day_last& year_month_day_last::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD11 inline year year_month_day_last::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_day_last::month() const NOEXCEPT {return mdl_.month();} CONSTCD11 inline month_day_last year_month_day_last::month_day_last() const NOEXCEPT { return mdl_; } CONSTCD14 inline day year_month_day_last::day() const NOEXCEPT { CONSTDATA julian::day d[] = { julian::day(31), julian::day(28), julian::day(31), julian::day(30), julian::day(31), julian::day(30), julian::day(31), julian::day(31), julian::day(30), julian::day(31), julian::day(30), julian::day(31) }; return month() != feb || !y_.is_leap() ? d[static_cast(month())-1] : julian::day(29); } CONSTCD14 inline year_month_day_last::operator sys_days() const NOEXCEPT { return sys_days(year()/month()/day()); } CONSTCD14 inline year_month_day_last::operator local_days() const NOEXCEPT { return local_days(year()/month()/day()); } CONSTCD11 inline bool year_month_day_last::ok() const NOEXCEPT { return y_.ok() && mdl_.ok(); } CONSTCD11 inline bool operator==(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return x.year() == y.year() && x.month_day_last() == y.month_day_last(); } CONSTCD11 inline bool operator!=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return x.year() < y.year() ? true : (x.year() > y.year() ? false : (x.month_day_last() < y.month_day_last())); } CONSTCD11 inline bool operator>(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return !(x < y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day_last& ymdl) { return os << ymdl.year() << '/' << ymdl.month_day_last(); } CONSTCD14 inline year_month_day_last operator+(const year_month_day_last& ymdl, const months& dm) NOEXCEPT { return (ymdl.year() / ymdl.month() + dm) / last; } CONSTCD14 inline year_month_day_last operator+(const months& dm, const year_month_day_last& ymdl) NOEXCEPT { return ymdl + dm; } CONSTCD14 inline year_month_day_last operator-(const year_month_day_last& ymdl, const months& dm) NOEXCEPT { return ymdl + (-dm); } CONSTCD11 inline year_month_day_last operator+(const year_month_day_last& ymdl, const years& dy) NOEXCEPT { return {ymdl.year()+dy, ymdl.month_day_last()}; } CONSTCD11 inline year_month_day_last operator+(const years& dy, const year_month_day_last& ymdl) NOEXCEPT { return ymdl + dy; } CONSTCD11 inline year_month_day_last operator-(const year_month_day_last& ymdl, const years& dy) NOEXCEPT { return ymdl + (-dy); } // year_month_day CONSTCD11 inline year_month_day::year_month_day(const julian::year& y, const julian::month& m, const julian::day& d) NOEXCEPT : y_(y) , m_(m) , d_(d) {} CONSTCD14 inline year_month_day::year_month_day(const year_month_day_last& ymdl) NOEXCEPT : y_(ymdl.year()) , m_(ymdl.month()) , d_(ymdl.day()) {} CONSTCD14 inline year_month_day::year_month_day(sys_days dp) NOEXCEPT : year_month_day(from_days(dp.time_since_epoch())) {} CONSTCD14 inline year_month_day::year_month_day(local_days dp) NOEXCEPT : year_month_day(from_days(dp.time_since_epoch())) {} CONSTCD11 inline year year_month_day::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_day::month() const NOEXCEPT {return m_;} CONSTCD11 inline day year_month_day::day() const NOEXCEPT {return d_;} CONSTCD14 inline year_month_day& year_month_day::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline year_month_day& year_month_day::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_day& year_month_day::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_day& year_month_day::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD14 inline days year_month_day::to_days() const NOEXCEPT { static_assert(std::numeric_limits::digits >= 18, "This algorithm has not been ported to a 16 bit unsigned integer"); static_assert(std::numeric_limits::digits >= 20, "This algorithm has not been ported to a 16 bit signed integer"); auto const y = static_cast(y_) - (m_ <= feb); auto const m = static_cast(m_); auto const d = static_cast(d_); auto const era = (y >= 0 ? y : y-3) / 4; auto const yoe = static_cast(y - era * 4); // [0, 3] auto const doy = (153*(m > 2 ? m-3 : m+9) + 2)/5 + d-1; // [0, 365] auto const doe = yoe * 365 + doy; // [0, 1460] return days{era * 1461 + static_cast(doe) - 719470}; } CONSTCD14 inline year_month_day::operator sys_days() const NOEXCEPT { return sys_days{to_days()}; } CONSTCD14 inline year_month_day::operator local_days() const NOEXCEPT { return local_days{to_days()}; } CONSTCD14 inline bool year_month_day::ok() const NOEXCEPT { if (!(y_.ok() && m_.ok())) return false; return julian::day(1) <= d_ && d_ <= (y_/m_/last).day(); } CONSTCD11 inline bool operator==(const year_month_day& x, const year_month_day& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month() && x.day() == y.day(); } CONSTCD11 inline bool operator!=(const year_month_day& x, const year_month_day& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_month_day& x, const year_month_day& y) NOEXCEPT { return x.year() < y.year() ? true : (x.year() > y.year() ? false : (x.month() < y.month() ? true : (x.month() > y.month() ? false : (x.day() < y.day())))); } CONSTCD11 inline bool operator>(const year_month_day& x, const year_month_day& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_month_day& x, const year_month_day& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_month_day& x, const year_month_day& y) NOEXCEPT { return !(x < y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day& ymd) { date::detail::save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::right); os << ymd.year() << '-'; os.width(2); os << static_cast(ymd.month()) << '-'; os << ymd.day(); return os; } CONSTCD14 inline year_month_day year_month_day::from_days(days dp) NOEXCEPT { static_assert(std::numeric_limits::digits >= 18, "This algorithm has not been ported to a 16 bit unsigned integer"); static_assert(std::numeric_limits::digits >= 20, "This algorithm has not been ported to a 16 bit signed integer"); auto const z = dp.count() + 719470; auto const era = (z >= 0 ? z : z - 1460) / 1461; auto const doe = static_cast(z - era * 1461); // [0, 1460] auto const yoe = (doe - doe/1460) / 365; // [0, 3] auto const y = static_cast(yoe) + era * 4; auto const doy = doe - 365*yoe; // [0, 365] auto const mp = (5*doy + 2)/153; // [0, 11] auto const d = doy - (153*mp+2)/5 + 1; // [1, 31] auto const m = mp < 10 ? mp+3 : mp-9; // [1, 12] return year_month_day{julian::year{y + (m <= 2)}, julian::month(m), julian::day(d)}; } CONSTCD14 inline year_month_day operator+(const year_month_day& ymd, const months& dm) NOEXCEPT { return (ymd.year() / ymd.month() + dm) / ymd.day(); } CONSTCD14 inline year_month_day operator+(const months& dm, const year_month_day& ymd) NOEXCEPT { return ymd + dm; } CONSTCD14 inline year_month_day operator-(const year_month_day& ymd, const months& dm) NOEXCEPT { return ymd + (-dm); } CONSTCD11 inline year_month_day operator+(const year_month_day& ymd, const years& dy) NOEXCEPT { return (ymd.year() + dy) / ymd.month() / ymd.day(); } CONSTCD11 inline year_month_day operator+(const years& dy, const year_month_day& ymd) NOEXCEPT { return ymd + dy; } CONSTCD11 inline year_month_day operator-(const year_month_day& ymd, const years& dy) NOEXCEPT { return ymd + (-dy); } // year_month_weekday CONSTCD11 inline year_month_weekday::year_month_weekday(const julian::year& y, const julian::month& m, const julian::weekday_indexed& wdi) NOEXCEPT : y_(y) , m_(m) , wdi_(wdi) {} CONSTCD14 inline year_month_weekday::year_month_weekday(const sys_days& dp) NOEXCEPT : year_month_weekday(from_days(dp.time_since_epoch())) {} CONSTCD14 inline year_month_weekday::year_month_weekday(const local_days& dp) NOEXCEPT : year_month_weekday(from_days(dp.time_since_epoch())) {} CONSTCD14 inline year_month_weekday& year_month_weekday::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline year_month_weekday& year_month_weekday::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_weekday& year_month_weekday::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_weekday& year_month_weekday::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD11 inline year year_month_weekday::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_weekday::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday year_month_weekday::weekday() const NOEXCEPT { return wdi_.weekday(); } CONSTCD11 inline unsigned year_month_weekday::index() const NOEXCEPT { return wdi_.index(); } CONSTCD11 inline weekday_indexed year_month_weekday::weekday_indexed() const NOEXCEPT { return wdi_; } CONSTCD14 inline year_month_weekday::operator sys_days() const NOEXCEPT { return sys_days{to_days()}; } CONSTCD14 inline year_month_weekday::operator local_days() const NOEXCEPT { return local_days{to_days()}; } CONSTCD14 inline bool year_month_weekday::ok() const NOEXCEPT { if (!y_.ok() || !m_.ok() || !wdi_.weekday().ok() || wdi_.index() < 1) return false; if (wdi_.index() <= 4) return true; auto d2 = wdi_.weekday() - julian::weekday(y_/m_/1) + days((wdi_.index()-1)*7 + 1); return static_cast(d2.count()) <= static_cast((y_/m_/last).day()); } CONSTCD14 inline year_month_weekday year_month_weekday::from_days(days d) NOEXCEPT { sys_days dp{d}; auto const wd = julian::weekday(dp); auto const ymd = year_month_day(dp); return {ymd.year(), ymd.month(), wd[(static_cast(ymd.day())-1)/7+1]}; } CONSTCD14 inline days year_month_weekday::to_days() const NOEXCEPT { auto d = sys_days(y_/m_/1); return (d + (wdi_.weekday() - julian::weekday(d) + days{(wdi_.index()-1)*7}) ).time_since_epoch(); } CONSTCD11 inline bool operator==(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month() && x.weekday_indexed() == y.weekday_indexed(); } CONSTCD11 inline bool operator!=(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday& ymwdi) { return os << ymwdi.year() << '/' << ymwdi.month() << '/' << ymwdi.weekday_indexed(); } CONSTCD14 inline year_month_weekday operator+(const year_month_weekday& ymwd, const months& dm) NOEXCEPT { return (ymwd.year() / ymwd.month() + dm) / ymwd.weekday_indexed(); } CONSTCD14 inline year_month_weekday operator+(const months& dm, const year_month_weekday& ymwd) NOEXCEPT { return ymwd + dm; } CONSTCD14 inline year_month_weekday operator-(const year_month_weekday& ymwd, const months& dm) NOEXCEPT { return ymwd + (-dm); } CONSTCD11 inline year_month_weekday operator+(const year_month_weekday& ymwd, const years& dy) NOEXCEPT { return {ymwd.year()+dy, ymwd.month(), ymwd.weekday_indexed()}; } CONSTCD11 inline year_month_weekday operator+(const years& dy, const year_month_weekday& ymwd) NOEXCEPT { return ymwd + dy; } CONSTCD11 inline year_month_weekday operator-(const year_month_weekday& ymwd, const years& dy) NOEXCEPT { return ymwd + (-dy); } // year_month_weekday_last CONSTCD11 inline year_month_weekday_last::year_month_weekday_last(const julian::year& y, const julian::month& m, const julian::weekday_last& wdl) NOEXCEPT : y_(y) , m_(m) , wdl_(wdl) {} CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD11 inline year year_month_weekday_last::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_weekday_last::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday year_month_weekday_last::weekday() const NOEXCEPT { return wdl_.weekday(); } CONSTCD11 inline weekday_last year_month_weekday_last::weekday_last() const NOEXCEPT { return wdl_; } CONSTCD14 inline year_month_weekday_last::operator sys_days() const NOEXCEPT { return sys_days{to_days()}; } CONSTCD14 inline year_month_weekday_last::operator local_days() const NOEXCEPT { return local_days{to_days()}; } CONSTCD11 inline bool year_month_weekday_last::ok() const NOEXCEPT { return y_.ok() && m_.ok() && wdl_.ok(); } CONSTCD14 inline days year_month_weekday_last::to_days() const NOEXCEPT { auto const d = sys_days(y_/m_/last); return (d - (julian::weekday{d} - wdl_.weekday())).time_since_epoch(); } CONSTCD11 inline bool operator==(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month() && x.weekday_last() == y.weekday_last(); } CONSTCD11 inline bool operator!=(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday_last& ymwdl) { return os << ymwdl.year() << '/' << ymwdl.month() << '/' << ymwdl.weekday_last(); } CONSTCD14 inline year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT { return (ymwdl.year() / ymwdl.month() + dm) / ymwdl.weekday_last(); } CONSTCD14 inline year_month_weekday_last operator+(const months& dm, const year_month_weekday_last& ymwdl) NOEXCEPT { return ymwdl + dm; } CONSTCD14 inline year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT { return ymwdl + (-dm); } CONSTCD11 inline year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT { return {ymwdl.year()+dy, ymwdl.month(), ymwdl.weekday_last()}; } CONSTCD11 inline year_month_weekday_last operator+(const years& dy, const year_month_weekday_last& ymwdl) NOEXCEPT { return ymwdl + dy; } CONSTCD11 inline year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT { return ymwdl + (-dy); } // year_month from operator/() CONSTCD11 inline year_month operator/(const year& y, const month& m) NOEXCEPT { return {y, m}; } CONSTCD11 inline year_month operator/(const year& y, int m) NOEXCEPT { return y / month(static_cast(m)); } // month_day from operator/() CONSTCD11 inline month_day operator/(const month& m, const day& d) NOEXCEPT { return {m, d}; } CONSTCD11 inline month_day operator/(const day& d, const month& m) NOEXCEPT { return m / d; } CONSTCD11 inline month_day operator/(const month& m, int d) NOEXCEPT { return m / day(static_cast(d)); } CONSTCD11 inline month_day operator/(int m, const day& d) NOEXCEPT { return month(static_cast(m)) / d; } CONSTCD11 inline month_day operator/(const day& d, int m) NOEXCEPT {return m / d;} // month_day_last from operator/() CONSTCD11 inline month_day_last operator/(const month& m, last_spec) NOEXCEPT { return month_day_last{m}; } CONSTCD11 inline month_day_last operator/(last_spec, const month& m) NOEXCEPT { return m/last; } CONSTCD11 inline month_day_last operator/(int m, last_spec) NOEXCEPT { return month(static_cast(m))/last; } CONSTCD11 inline month_day_last operator/(last_spec, int m) NOEXCEPT { return m/last; } // month_weekday from operator/() CONSTCD11 inline month_weekday operator/(const month& m, const weekday_indexed& wdi) NOEXCEPT { return {m, wdi}; } CONSTCD11 inline month_weekday operator/(const weekday_indexed& wdi, const month& m) NOEXCEPT { return m / wdi; } CONSTCD11 inline month_weekday operator/(int m, const weekday_indexed& wdi) NOEXCEPT { return month(static_cast(m)) / wdi; } CONSTCD11 inline month_weekday operator/(const weekday_indexed& wdi, int m) NOEXCEPT { return m / wdi; } // month_weekday_last from operator/() CONSTCD11 inline month_weekday_last operator/(const month& m, const weekday_last& wdl) NOEXCEPT { return {m, wdl}; } CONSTCD11 inline month_weekday_last operator/(const weekday_last& wdl, const month& m) NOEXCEPT { return m / wdl; } CONSTCD11 inline month_weekday_last operator/(int m, const weekday_last& wdl) NOEXCEPT { return month(static_cast(m)) / wdl; } CONSTCD11 inline month_weekday_last operator/(const weekday_last& wdl, int m) NOEXCEPT { return m / wdl; } // year_month_day from operator/() CONSTCD11 inline year_month_day operator/(const year_month& ym, const day& d) NOEXCEPT { return {ym.year(), ym.month(), d}; } CONSTCD11 inline year_month_day operator/(const year_month& ym, int d) NOEXCEPT { return ym / day(static_cast(d)); } CONSTCD11 inline year_month_day operator/(const year& y, const month_day& md) NOEXCEPT { return y / md.month() / md.day(); } CONSTCD11 inline year_month_day operator/(int y, const month_day& md) NOEXCEPT { return year(y) / md; } CONSTCD11 inline year_month_day operator/(const month_day& md, const year& y) NOEXCEPT { return y / md; } CONSTCD11 inline year_month_day operator/(const month_day& md, int y) NOEXCEPT { return year(y) / md; } // year_month_day_last from operator/() CONSTCD11 inline year_month_day_last operator/(const year_month& ym, last_spec) NOEXCEPT { return {ym.year(), month_day_last{ym.month()}}; } CONSTCD11 inline year_month_day_last operator/(const year& y, const month_day_last& mdl) NOEXCEPT { return {y, mdl}; } CONSTCD11 inline year_month_day_last operator/(int y, const month_day_last& mdl) NOEXCEPT { return year(y) / mdl; } CONSTCD11 inline year_month_day_last operator/(const month_day_last& mdl, const year& y) NOEXCEPT { return y / mdl; } CONSTCD11 inline year_month_day_last operator/(const month_day_last& mdl, int y) NOEXCEPT { return year(y) / mdl; } // year_month_weekday from operator/() CONSTCD11 inline year_month_weekday operator/(const year_month& ym, const weekday_indexed& wdi) NOEXCEPT { return {ym.year(), ym.month(), wdi}; } CONSTCD11 inline year_month_weekday operator/(const year& y, const month_weekday& mwd) NOEXCEPT { return {y, mwd.month(), mwd.weekday_indexed()}; } CONSTCD11 inline year_month_weekday operator/(int y, const month_weekday& mwd) NOEXCEPT { return year(y) / mwd; } CONSTCD11 inline year_month_weekday operator/(const month_weekday& mwd, const year& y) NOEXCEPT { return y / mwd; } CONSTCD11 inline year_month_weekday operator/(const month_weekday& mwd, int y) NOEXCEPT { return year(y) / mwd; } // year_month_weekday_last from operator/() CONSTCD11 inline year_month_weekday_last operator/(const year_month& ym, const weekday_last& wdl) NOEXCEPT { return {ym.year(), ym.month(), wdl}; } CONSTCD11 inline year_month_weekday_last operator/(const year& y, const month_weekday_last& mwdl) NOEXCEPT { return {y, mwdl.month(), mwdl.weekday_last()}; } CONSTCD11 inline year_month_weekday_last operator/(int y, const month_weekday_last& mwdl) NOEXCEPT { return year(y) / mwdl; } CONSTCD11 inline year_month_weekday_last operator/(const month_weekday_last& mwdl, const year& y) NOEXCEPT { return y / mwdl; } CONSTCD11 inline year_month_weekday_last operator/(const month_weekday_last& mwdl, int y) NOEXCEPT { return year(y) / mwdl; } } // namespace julian #endif // JULIAN_H pr0m1th3as-datatypes-9450b29/src/date/ptz.h000066400000000000000000000653341527370255000204030ustar00rootroot00000000000000#ifndef PTZ_H #define PTZ_H // The MIT License (MIT) // // Copyright (c) 2017 Howard Hinnant // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // This header allows Posix-style time zones as specified for TZ here: // http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_03 // // Posix::time_zone can be constructed with a posix-style string and then used in // a zoned_time like so: // // zoned_time zt{"EST5EDT,M3.2.0,M11.1.0", // system_clock::now()}; // or: // // Posix::time_zone tz{"EST5EDT,M3.2.0,M11.1.0"}; // zoned_time zt{tz, system_clock::now()}; // // In C++17 CTAD simplifies this to: // // Posix::time_zone tz{"EST5EDT,M3.2.0,M11.1.0"}; // zoned_time zt{tz, system_clock::now()}; // // Extension to the Posix rules to allow a constant daylight saving offset: // // If the rule set is missing (everything starting with ','), then // there must be exactly one abbreviation (std or daylight) with // length 3 or greater, and that will be used as the constant offset. If // there are two, the std abbreviation is silently set to "", and the // result is constant daylight saving. If there are zero abbreviations // with no rule set, an exception is thrown. // // Example: // "EST5" yields a constant offset of -5h with 0h save and "EST abbreviation. // "5EDT" yields a constant offset of -4h with 1h save and "EDT" abbreviation. // "EST5EDT" and "5EDT4" are both equal to "5EDT". // // Note, Posix-style time zones are not recommended for all of the reasons described here: // https://stackoverflow.com/tags/timezone/info // // They are provided here as a non-trivial custom time zone example, and if you really // have to have Posix time zones, you're welcome to use this one. #include "date/tz.h" #include #include #include #include namespace Posix { namespace detail { #if HAS_STRING_VIEW using string_t = std::string_view; #else // !HAS_STRING_VIEW using string_t = std::string; #endif // !HAS_STRING_VIEW class rule; void throw_invalid(const string_t& s, unsigned i, const string_t& message); unsigned read_date(const string_t& s, unsigned i, rule& r); unsigned read_name(const string_t& s, unsigned i, std::string& name); unsigned read_signed_time(const string_t& s, unsigned i, std::chrono::seconds& t); unsigned read_unsigned_time(const string_t& s, unsigned i, std::chrono::seconds& t); unsigned read_unsigned(const string_t& s, unsigned i, unsigned limit, unsigned& u, const string_t& message = string_t{}); class rule { enum {off, J, M, N}; date::month m_; date::weekday wd_; unsigned short n_ : 14; unsigned short mode_ : 2; std::chrono::duration time_ = std::chrono::hours{2}; public: rule() : mode_(off) {} bool ok() const {return mode_ != off;} date::local_seconds operator()(date::year y) const; std::string to_string() const; friend std::ostream& operator<<(std::ostream& os, const rule& r); friend unsigned read_date(const string_t& s, unsigned i, rule& r); friend bool operator==(const rule& x, const rule& y); }; inline bool operator==(const rule& x, const rule& y) { if (x.mode_ != y.mode_) return false; switch (x.mode_) { case rule::J: case rule::N: return x.n_ == y.n_; case rule::M: return x.m_ == y.m_ && x.n_ == y.n_ && x.wd_ == y.wd_; default: return true; } } inline bool operator!=(const rule& x, const rule& y) { return !(x == y); } inline date::local_seconds rule::operator()(date::year y) const { using date::local_days; using date::January; using date::days; using date::last; using sec = std::chrono::seconds; date::local_seconds t; switch (mode_) { case J: t = local_days{y/January/0} + days{n_ + (y.is_leap() && n_ > 59)} + sec{time_}; break; case M: t = (n_ == 5 ? local_days{y/m_/wd_[last]} : local_days{y/m_/wd_[n_]}) + sec{time_}; break; case N: t = local_days{y/January/1} + days{n_} + sec{time_}; break; default: assert(!"rule called with bad mode"); } return t; } inline std::string rule::to_string() const { using namespace std::chrono; auto print_offset = [](seconds off) { std::string nm; if (off != hours{2}) { date::hh_mm_ss offset{off}; nm = '/'; nm += std::to_string(offset.hours().count()); if (offset.minutes() != minutes{0} || offset.seconds() != seconds{0}) { nm += ':'; if (offset.minutes() < minutes{10}) nm += '0'; nm += std::to_string(offset.minutes().count()); if (offset.seconds() != seconds{0}) { nm += ':'; if (offset.seconds() < seconds{10}) nm += '0'; nm += std::to_string(offset.seconds().count()); } } } return nm; }; std::string nm; switch (mode_) { case rule::J: nm = 'J'; nm += std::to_string(n_); break; case rule::M: nm = 'M'; nm += std::to_string(static_cast(m_)); nm += '.'; nm += std::to_string(n_); nm += '.'; nm += std::to_string(wd_.c_encoding()); break; case rule::N: nm = std::to_string(n_); break; default: break; } nm += print_offset(time_); return nm; } inline std::ostream& operator<<(std::ostream& os, const rule& r) { switch (r.mode_) { case rule::J: os << 'J' << r.n_ << date::format(" %T", r.time_); break; case rule::M: if (r.n_ == 5) os << r.m_/r.wd_[date::last]; else os << r.m_/r.wd_[r.n_]; os << date::format(" %T", r.time_); break; case rule::N: os << r.n_ << date::format(" %T", r.time_); break; default: break; } return os; } } // namespace detail class time_zone { std::string std_abbrev_; std::string dst_abbrev_ = {}; std::chrono::seconds offset_; std::chrono::seconds save_ = std::chrono::hours{1}; detail::rule start_rule_; detail::rule end_rule_; public: explicit time_zone(const detail::string_t& name); template date::sys_info get_info(date::sys_time st) const; template date::local_info get_info(date::local_time tp) const; template date::sys_time::type> to_sys(date::local_time tp) const; template date::sys_time::type> to_sys(date::local_time tp, date::choose z) const; template date::local_time::type> to_local(date::sys_time tp) const; friend std::ostream& operator<<(std::ostream& os, const time_zone& z); const time_zone* operator->() const {return this;} std::string name() const; friend bool operator==(const time_zone& x, const time_zone& y); private: date::sys_seconds get_start(date::year y) const; date::sys_seconds get_prev_start(date::year y) const; date::sys_seconds get_next_start(date::year y) const; date::sys_seconds get_end(date::year y) const; date::sys_seconds get_prev_end(date::year y) const; date::sys_seconds get_next_end(date::year y) const; date::sys_info contant_offset() const; }; inline date::sys_seconds time_zone::get_start(date::year y) const { return date::sys_seconds{(start_rule_(y) - offset_).time_since_epoch()}; } inline date::sys_seconds time_zone::get_prev_start(date::year y) const { return date::sys_seconds{(start_rule_(--y) - offset_).time_since_epoch()}; } inline date::sys_seconds time_zone::get_next_start(date::year y) const { return date::sys_seconds{(start_rule_(++y) - offset_).time_since_epoch()}; } inline date::sys_seconds time_zone::get_end(date::year y) const { return date::sys_seconds{(end_rule_(y) - (offset_ + save_)).time_since_epoch()}; } inline date::sys_seconds time_zone::get_prev_end(date::year y) const { return date::sys_seconds{(end_rule_(--y) - (offset_ + save_)).time_since_epoch()}; } inline date::sys_seconds time_zone::get_next_end(date::year y) const { return date::sys_seconds{(end_rule_(++y) - (offset_ + save_)).time_since_epoch()}; } inline date::sys_info time_zone::contant_offset() const { using date::year; using date::sys_info; using date::sys_days; using date::January; using date::December; using date::last; using date::days; using std::chrono::minutes; sys_info r; r.begin = sys_days{year::min()/January/1}; r.end = sys_days{year::max()/December/last} + days{1} - std::chrono::seconds{1}; if (std_abbrev_.size() > 0) { r.abbrev = std_abbrev_; r.offset = offset_; r.save = {}; } else { r.abbrev = dst_abbrev_; r.offset = offset_ + save_; r.save = date::ceil(save_); } return r; } inline time_zone::time_zone(const detail::string_t& s) { using detail::read_name; using detail::read_signed_time; using detail::throw_invalid; auto i = read_name(s, 0, std_abbrev_); auto std_name_i = i; auto abbrev_name_i = i; i = read_signed_time(s, i, offset_); offset_ = -offset_; if (i != s.size()) { i = read_name(s, i, dst_abbrev_); abbrev_name_i = i; if (i != s.size()) { if (s[i] != ',') { i = read_signed_time(s, i, save_); save_ = -save_ - offset_; } if (i != s.size()) { if (s[i] != ',') throw_invalid(s, i, "Expecting end of string or ',' to start rule"); ++i; i = read_date(s, i, start_rule_); if (i == s.size() || s[i] != ',') throw_invalid(s, i, "Expecting ',' and then the ending rule"); ++i; i = read_date(s, i, end_rule_); if (i != s.size()) throw_invalid(s, i, "Found unexpected trailing characters"); } } } if (start_rule_.ok()) { if (std_abbrev_.size() < 3) throw_invalid(s, std_name_i, "Zone with rules must have a std" " abbreviation of length 3 or greater"); if (dst_abbrev_.size() < 3) throw_invalid(s, abbrev_name_i, "Zone with rules must have a daylight" " abbreviation of length 3 or greater"); } else { if (dst_abbrev_.size() >= 3) { std_abbrev_.clear(); } else if (std_abbrev_.size() < 3) { throw_invalid(s, std_name_i, "Zone must have at least one abbreviation" " of length 3 or greater"); } else { dst_abbrev_.clear(); save_ = {}; } } } template date::sys_info time_zone::get_info(date::sys_time st) const { using date::sys_info; using date::year_month_day; using date::sys_days; using date::floor; using date::ceil; using date::days; using date::year; using date::January; using date::December; using date::last; using std::chrono::minutes; sys_info r{}; r.offset = offset_; if (start_rule_.ok()) { auto y = year_month_day{floor(st)}.year(); if (st >= get_next_start(y)) ++y; else if (st < get_prev_end(y)) --y; auto start = get_start(y); auto end = get_end(y); if (start <= end) // (northern hemisphere) { if (start <= st && st < end) { r.begin = start; r.end = end; r.offset += save_; r.save = ceil(save_); r.abbrev = dst_abbrev_; } else if (st < start) { r.begin = get_prev_end(y); r.end = start; r.abbrev = std_abbrev_; } else // st >= end { r.begin = end; r.end = get_next_start(y); r.abbrev = std_abbrev_; } } else // end < start (southern hemisphere) { if (end <= st && st < start) { r.begin = end; r.end = start; r.abbrev = std_abbrev_; } else if (st < end) { r.begin = get_prev_start(y); r.end = end; r.offset += save_; r.save = ceil(save_); r.abbrev = dst_abbrev_; } else // st >= start { r.begin = start; r.end = get_next_end(y); r.offset += save_; r.save = ceil(save_); r.abbrev = dst_abbrev_; } } } else r = contant_offset(); using seconds = std::chrono::seconds; assert(r.begin <= floor(st) && floor(st) <= r.end); return r; } template date::local_info time_zone::get_info(date::local_time tp) const { using date::local_info; using date::year_month_day; using date::days; using date::sys_days; using date::sys_seconds; using date::year; using date::ceil; using date::January; using date::December; using date::last; using std::chrono::seconds; using std::chrono::minutes; local_info r{}; using date::floor; if (start_rule_.ok()) { auto y = year_month_day{floor(tp)}.year(); auto start = get_start(y); auto end = get_end(y); auto utcs = sys_seconds{floor(tp - offset_).time_since_epoch()}; auto utcd = sys_seconds{floor(tp - (offset_ + save_)).time_since_epoch()}; auto northern = start <= end; if ((utcs < start) != (utcd < start)) { if (northern) r.first.begin = get_prev_end(y); else r.first.begin = end; r.first.end = start; r.first.offset = offset_; r.first.abbrev = std_abbrev_; r.second.begin = start; if (northern) r.second.end = end; else r.second.end = get_next_end(y); r.second.abbrev = dst_abbrev_; r.second.offset = offset_ + save_; r.second.save = ceil(save_); r.result = save_ > seconds{0} ? local_info::nonexistent : local_info::ambiguous; } else if ((utcs < end) != (utcd < end)) { if (northern) r.first.begin = start; else r.first.begin = get_prev_start(y); r.first.end = end; r.first.offset = offset_ + save_; r.first.save = ceil(save_); r.first.abbrev = dst_abbrev_; r.second.begin = end; if (northern) r.second.end = get_next_start(y); else r.second.end = start; r.second.abbrev = std_abbrev_; r.second.offset = offset_; r.result = save_ > seconds{0} ? local_info::ambiguous : local_info::nonexistent; } else r.first = get_info(utcs); } else r.first = contant_offset(); return r; } template date::sys_time::type> time_zone::to_sys(date::local_time tp) const { using date::local_info; using date::sys_time; using date::ambiguous_local_time; using date::nonexistent_local_time; auto i = get_info(tp); if (i.result == local_info::nonexistent) throw nonexistent_local_time(tp, i); else if (i.result == local_info::ambiguous) throw ambiguous_local_time(tp, i); return sys_time{tp.time_since_epoch()} - i.first.offset; } template date::sys_time::type> time_zone::to_sys(date::local_time tp, date::choose z) const { using date::local_info; using date::sys_time; using date::choose; auto i = get_info(tp); if (i.result == local_info::nonexistent) { return i.first.end; } else if (i.result == local_info::ambiguous) { if (z == choose::latest) return sys_time{tp.time_since_epoch()} - i.second.offset; } return sys_time{tp.time_since_epoch()} - i.first.offset; } template date::local_time::type> time_zone::to_local(date::sys_time tp) const { using date::local_time; using std::chrono::seconds; using LT = local_time::type>; auto i = get_info(tp); return LT{(tp + i.offset).time_since_epoch()}; } inline std::ostream& operator<<(std::ostream& os, const time_zone& z) { using date::operator<<; os << '{'; os << z.std_abbrev_ << ", " << z.dst_abbrev_ << date::format(", %T, ", z.offset_) << date::format("%T, [", z.save_) << z.start_rule_ << ", " << z.end_rule_ << ")}"; return os; } inline std::string time_zone::name() const { using namespace date; using namespace std::chrono; auto print_abbrev = [](std::string const& nm) { if (std::any_of(nm.begin(), nm.end(), [](char c) { return !std::isalpha(c); })) { return '<' + nm + '>'; } return nm; }; auto print_offset = [](seconds off) { std::string nm; date::hh_mm_ss offset{-off}; if (offset.is_negative()) nm += '-'; nm += std::to_string(offset.hours().count()); if (offset.minutes() != minutes{0} || offset.seconds() != seconds{0}) { nm += ':'; if (offset.minutes() < minutes{10}) nm += '0'; nm += std::to_string(offset.minutes().count()); if (offset.seconds() != seconds{0}) { nm += ':'; if (offset.seconds() < seconds{10}) nm += '0'; nm += std::to_string(offset.seconds().count()); } } return nm; }; auto nm = print_abbrev(std_abbrev_); nm += print_offset(offset_); if (!dst_abbrev_.empty()) { nm += print_abbrev(dst_abbrev_); if (save_ != hours{1}) nm += print_offset(offset_+save_); if (start_rule_.ok()) { nm += ','; nm += start_rule_.to_string(); nm += ','; nm += end_rule_.to_string(); } } return nm; } inline bool operator==(const time_zone& x, const time_zone& y) { return x.std_abbrev_ == y.std_abbrev_ && x.dst_abbrev_ == y. dst_abbrev_ && x.offset_ == y.offset_ && x.save_ == y.save_ && x.start_rule_ == y.start_rule_ && x.end_rule_ == y.end_rule_; } inline bool operator!=(const time_zone& x, const time_zone& y) { return !(x == y); } namespace detail { inline void throw_invalid(const string_t& s, unsigned i, const string_t& message) { throw std::runtime_error(std::string("Invalid time_zone initializer.\n") + std::string(message) + ":\n" + std::string(s) + '\n' + "\x1b[1;32m" + std::string(i, '~') + '^' + std::string(i < s.size() ? s.size()-i-1 : 0, '~') + "\x1b[0m"); } inline unsigned read_date(const string_t& s, unsigned i, rule& r) { using date::month; using date::weekday; if (i == s.size()) throw_invalid(s, i, "Expected rule but found end of string"); if (s[i] == 'J') { ++i; unsigned n; i = read_unsigned(s, i, 3, n, "Expected to find the Julian day [1, 365]"); if (!(1 <= n && n <= 365)) throw_invalid(s, i-1, "Expected Julian day to be in the range [1, 365]"); r.mode_ = rule::J; r.n_ = n; } else if (s[i] == 'M') { ++i; unsigned m; i = read_unsigned(s, i, 2, m, "Expected to find month [1, 12]"); if (!(1 <= m && m <= 12)) throw_invalid(s, i-1, "Expected month to be in the range [1, 12]"); if (i == s.size() || s[i] != '.') throw_invalid(s, i, "Expected '.' after month"); ++i; unsigned n; i = read_unsigned(s, i, 1, n, "Expected to find week number [1, 5]"); if (!(1 <= n && n <= 5)) throw_invalid(s, i-1, "Expected week number to be in the range [1, 5]"); if (i == s.size() || s[i] != '.') throw_invalid(s, i, "Expected '.' after weekday index"); ++i; unsigned wd; i = read_unsigned(s, i, 1, wd, "Expected to find day of week [0, 6]"); if (wd > 6) throw_invalid(s, i-1, "Expected day of week to be in the range [0, 6]"); r.mode_ = rule::M; r.m_ = month{m}; r.wd_ = weekday{wd}; r.n_ = n; } else if (std::isdigit(s[i])) { unsigned n; i = read_unsigned(s, i, 3, n); if (n > 365) throw_invalid(s, i-1, "Expected Julian day to be in the range [0, 365]"); r.mode_ = rule::N; r.n_ = n; } else throw_invalid(s, i, "Expected 'J', 'M', or a digit to start rule"); if (i != s.size() && s[i] == '/') { ++i; std::chrono::seconds t; i = read_unsigned_time(s, i, t); r.time_ = t; } return i; } inline unsigned read_name(const string_t& s, unsigned i, std::string& name) { if (i == s.size()) throw_invalid(s, i, "Expected a name but found end of string"); if (s[i] == '<') { ++i; while (true) { if (i == s.size()) throw_invalid(s, i, "Expected to find closing '>', but found end of string"); if (s[i] == '>') break; name.push_back(s[i]); ++i; } ++i; } else { while (i != s.size() && std::isalpha(s[i])) { name.push_back(s[i]); ++i; } } return i; } inline unsigned read_signed_time(const string_t& s, unsigned i, std::chrono::seconds& t) { if (i == s.size()) throw_invalid(s, i, "Expected to read signed time, but found end of string"); bool negative = false; if (s[i] == '-') { negative = true; ++i; } else if (s[i] == '+') ++i; i = read_unsigned_time(s, i, t); if (negative) t = -t; return i; } inline unsigned read_unsigned_time(const string_t& s, unsigned i, std::chrono::seconds& t) { using std::chrono::seconds; using std::chrono::minutes; using std::chrono::hours; if (i == s.size()) throw_invalid(s, i, "Expected to read unsigned time, but found end of string"); unsigned x; i = read_unsigned(s, i, 2, x, "Expected to find hours [0, 24]"); if (x > 24) throw_invalid(s, i-1, "Expected hours to be in the range [0, 24]"); t = hours{x}; if (i != s.size() && s[i] == ':') { ++i; i = read_unsigned(s, i, 2, x, "Expected to find minutes [0, 59]"); if (x > 59) throw_invalid(s, i-1, "Expected minutes to be in the range [0, 59]"); t += minutes{x}; if (i != s.size() && s[i] == ':') { ++i; i = read_unsigned(s, i, 2, x, "Expected to find seconds [0, 59]"); if (x > 59) throw_invalid(s, i-1, "Expected seconds to be in the range [0, 59]"); t += seconds{x}; } } return i; } inline unsigned read_unsigned(const string_t& s, unsigned i, unsigned limit, unsigned& u, const string_t& message) { if (i == s.size() || !std::isdigit(s[i])) throw_invalid(s, i, message); u = static_cast(s[i] - '0'); unsigned count = 1; for (++i; count < limit && i != s.size() && std::isdigit(s[i]); ++i, ++count) u = u * 10 + static_cast(s[i] - '0'); return i; } } // namespace detail } // namespace Posix namespace date { template <> struct zoned_traits { #if HAS_STRING_VIEW static Posix::time_zone locate_zone(std::string_view name) { return Posix::time_zone{name}; } #else // !HAS_STRING_VIEW static Posix::time_zone locate_zone(const std::string& name) { return Posix::time_zone{name}; } static Posix::time_zone locate_zone(const char* name) { return Posix::time_zone{name}; } #endif // !HAS_STRING_VIEW }; } // namespace date #endif // PTZ_H pr0m1th3as-datatypes-9450b29/src/date/solar_hijri.h000066400000000000000000002316041527370255000220660ustar00rootroot00000000000000#ifndef SOLAR_HIJRI_H #define SOLAR_HIJRI_H // The MIT License (MIT) // // Copyright (c) 2016 Howard Hinnant // Copyright (c) 2019 Asad. Gharighi // // Calculations are based on: // https://www.timeanddate.com/calendar/persian-calendar.html // and follow style // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // Our apologies. When the previous paragraph was written, lowercase had not yet // been invented (that would involve another several millennia of evolution). // We did not mean to shout. #include "date.h" namespace solar_hijri { namespace internal { static const auto epoch = static_cast(2121446); static const auto days_in_era = static_cast(1029983); static const auto years_in_era = static_cast(2820); static const auto unix_time_shift = static_cast(2440588); auto const years_in_first_cycle = static_cast(29); auto const years_in_other_cycles = static_cast(33); auto const years_in_period = static_cast(128); // 29 + 3*33 auto const days_in_first_cycle = static_cast(10592); // 28/4 + 29*365 auto const days_in_other_cycles = static_cast(12053); // 32/4 + 33*365 auto const days_in_period = static_cast(46751); // days_in_first_cycle + 3*days_in_other_cycles; } // durations using days = date::days; using weeks = date::weeks; using years = std::chrono::duration , days::period>>; using months = std::chrono::duration >>; // time_point using sys_days = date::sys_days; using local_days = date::local_days; // types struct last_spec { explicit last_spec() = default; }; class day; class month; class year; class weekday; class weekday_indexed; class weekday_last; class month_day; class month_day_last; class month_weekday; class month_weekday_last; class year_month; class year_month_day; class year_month_day_last; class year_month_weekday; class year_month_weekday_last; // date composition operators CONSTCD11 year_month operator/(const year& y, const month& m) NOEXCEPT; CONSTCD11 year_month operator/(const year& y, int m) NOEXCEPT; CONSTCD11 month_day operator/(const day& d, const month& m) NOEXCEPT; CONSTCD11 month_day operator/(const day& d, int m) NOEXCEPT; CONSTCD11 month_day operator/(const month& m, const day& d) NOEXCEPT; CONSTCD11 month_day operator/(const month& m, int d) NOEXCEPT; CONSTCD11 month_day operator/(int m, const day& d) NOEXCEPT; CONSTCD11 month_day_last operator/(const month& m, last_spec) NOEXCEPT; CONSTCD11 month_day_last operator/(int m, last_spec) NOEXCEPT; CONSTCD11 month_day_last operator/(last_spec, const month& m) NOEXCEPT; CONSTCD11 month_day_last operator/(last_spec, int m) NOEXCEPT; CONSTCD11 month_weekday operator/(const month& m, const weekday_indexed& wdi) NOEXCEPT; CONSTCD11 month_weekday operator/(int m, const weekday_indexed& wdi) NOEXCEPT; CONSTCD11 month_weekday operator/(const weekday_indexed& wdi, const month& m) NOEXCEPT; CONSTCD11 month_weekday operator/(const weekday_indexed& wdi, int m) NOEXCEPT; CONSTCD11 month_weekday_last operator/(const month& m, const weekday_last& wdl) NOEXCEPT; CONSTCD11 month_weekday_last operator/(int m, const weekday_last& wdl) NOEXCEPT; CONSTCD11 month_weekday_last operator/(const weekday_last& wdl, const month& m) NOEXCEPT; CONSTCD11 month_weekday_last operator/(const weekday_last& wdl, int m) NOEXCEPT; CONSTCD11 year_month_day operator/(const year_month& ym, const day& d) NOEXCEPT; CONSTCD11 year_month_day operator/(const year_month& ym, int d) NOEXCEPT; CONSTCD11 year_month_day operator/(const year& y, const month_day& md) NOEXCEPT; CONSTCD11 year_month_day operator/(int y, const month_day& md) NOEXCEPT; CONSTCD11 year_month_day operator/(const month_day& md, const year& y) NOEXCEPT; CONSTCD11 year_month_day operator/(const month_day& md, int y) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const year_month& ym, last_spec) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const year& y, const month_day_last& mdl) NOEXCEPT; CONSTCD11 year_month_day_last operator/(int y, const month_day_last& mdl) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const month_day_last& mdl, const year& y) NOEXCEPT; CONSTCD11 year_month_day_last operator/(const month_day_last& mdl, int y) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const year_month& ym, const weekday_indexed& wdi) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const year& y, const month_weekday& mwd) NOEXCEPT; CONSTCD11 year_month_weekday operator/(int y, const month_weekday& mwd) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const month_weekday& mwd, const year& y) NOEXCEPT; CONSTCD11 year_month_weekday operator/(const month_weekday& mwd, int y) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const year_month& ym, const weekday_last& wdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const year& y, const month_weekday_last& mwdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(int y, const month_weekday_last& mwdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const month_weekday_last& mwdl, const year& y) NOEXCEPT; CONSTCD11 year_month_weekday_last operator/(const month_weekday_last& mwdl, int y) NOEXCEPT; // Detailed interface // day class day { unsigned char d_; public: day() = default; explicit CONSTCD11 day(unsigned d) NOEXCEPT; CONSTCD14 day& operator++() NOEXCEPT; CONSTCD14 day operator++(int) NOEXCEPT; CONSTCD14 day& operator--() NOEXCEPT; CONSTCD14 day operator--(int) NOEXCEPT; CONSTCD14 day& operator+=(const days& d) NOEXCEPT; CONSTCD14 day& operator-=(const days& d) NOEXCEPT; CONSTCD11 explicit operator unsigned() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator!=(const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator< (const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator> (const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator<=(const day& x, const day& y) NOEXCEPT; CONSTCD11 bool operator>=(const day& x, const day& y) NOEXCEPT; CONSTCD11 day operator+(const day& x, const days& y) NOEXCEPT; CONSTCD11 day operator+(const days& x, const day& y) NOEXCEPT; CONSTCD11 day operator-(const day& x, const days& y) NOEXCEPT; CONSTCD11 days operator-(const day& x, const day& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const day& d); // month class month { unsigned char m_; public: month() = default; explicit CONSTCD11 month(unsigned m) NOEXCEPT; CONSTCD14 month& operator++() NOEXCEPT; CONSTCD14 month operator++(int) NOEXCEPT; CONSTCD14 month& operator--() NOEXCEPT; CONSTCD14 month operator--(int) NOEXCEPT; CONSTCD14 month& operator+=(const months& m) NOEXCEPT; CONSTCD14 month& operator-=(const months& m) NOEXCEPT; CONSTCD11 explicit operator unsigned() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator!=(const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator< (const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator> (const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator<=(const month& x, const month& y) NOEXCEPT; CONSTCD11 bool operator>=(const month& x, const month& y) NOEXCEPT; CONSTCD14 month operator+(const month& x, const months& y) NOEXCEPT; CONSTCD14 month operator+(const months& x, const month& y) NOEXCEPT; CONSTCD14 month operator-(const month& x, const months& y) NOEXCEPT; CONSTCD14 months operator-(const month& x, const month& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month& m); // year class year { short y_; public: year() = default; explicit CONSTCD11 year(int y) NOEXCEPT; CONSTCD14 year& operator++() NOEXCEPT; CONSTCD14 year operator++(int) NOEXCEPT; CONSTCD14 year& operator--() NOEXCEPT; CONSTCD14 year operator--(int) NOEXCEPT; CONSTCD14 year& operator+=(const years& y) NOEXCEPT; CONSTCD14 year& operator-=(const years& y) NOEXCEPT; CONSTCD14 bool is_leap() const NOEXCEPT; CONSTCD11 explicit operator int() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; static CONSTCD11 year min() NOEXCEPT; static CONSTCD11 year max() NOEXCEPT; }; CONSTCD11 bool operator==(const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator!=(const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator< (const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator> (const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator<=(const year& x, const year& y) NOEXCEPT; CONSTCD11 bool operator>=(const year& x, const year& y) NOEXCEPT; CONSTCD11 year operator+(const year& x, const years& y) NOEXCEPT; CONSTCD11 year operator+(const years& x, const year& y) NOEXCEPT; CONSTCD11 year operator-(const year& x, const years& y) NOEXCEPT; CONSTCD11 years operator-(const year& x, const year& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year& y); // weekday class weekday { unsigned char wd_; public: weekday() = default; explicit CONSTCD11 weekday(unsigned wd) NOEXCEPT; explicit weekday(int) = delete; CONSTCD11 weekday(const sys_days& dp) NOEXCEPT; CONSTCD11 explicit weekday(const local_days& dp) NOEXCEPT; CONSTCD14 weekday& operator++() NOEXCEPT; CONSTCD14 weekday operator++(int) NOEXCEPT; CONSTCD14 weekday& operator--() NOEXCEPT; CONSTCD14 weekday operator--(int) NOEXCEPT; CONSTCD14 weekday& operator+=(const days& d) NOEXCEPT; CONSTCD14 weekday& operator-=(const days& d) NOEXCEPT; CONSTCD11 explicit operator unsigned() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; CONSTCD11 weekday_indexed operator[](unsigned index) const NOEXCEPT; CONSTCD11 weekday_last operator[](last_spec) const NOEXCEPT; private: static CONSTCD11 unsigned char weekday_from_days(int z) NOEXCEPT; }; CONSTCD11 bool operator==(const weekday& x, const weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const weekday& x, const weekday& y) NOEXCEPT; CONSTCD14 weekday operator+(const weekday& x, const days& y) NOEXCEPT; CONSTCD14 weekday operator+(const days& x, const weekday& y) NOEXCEPT; CONSTCD14 weekday operator-(const weekday& x, const days& y) NOEXCEPT; CONSTCD14 days operator-(const weekday& x, const weekday& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const weekday& wd); // weekday_indexed class weekday_indexed { unsigned char wd_ : 4; unsigned char index_ : 4; public: weekday_indexed() = default; CONSTCD11 weekday_indexed(const solar_hijri::weekday& wd, unsigned index) NOEXCEPT; CONSTCD11 solar_hijri::weekday weekday() const NOEXCEPT; CONSTCD11 unsigned index() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT; CONSTCD11 bool operator!=(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_indexed& wdi); // weekday_last class weekday_last { solar_hijri::weekday wd_; public: weekday_last() = default; explicit CONSTCD11 weekday_last(const solar_hijri::weekday& wd) NOEXCEPT; CONSTCD11 solar_hijri::weekday weekday() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const weekday_last& x, const weekday_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const weekday_last& x, const weekday_last& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_last& wdl); // year_month class year_month { solar_hijri::year y_; solar_hijri::month m_; public: year_month() = default; CONSTCD11 year_month(const solar_hijri::year& y, const solar_hijri::month& m) NOEXCEPT; CONSTCD11 solar_hijri::year year() const NOEXCEPT; CONSTCD11 solar_hijri::month month() const NOEXCEPT; CONSTCD14 year_month& operator+=(const months& dm) NOEXCEPT; CONSTCD14 year_month& operator-=(const months& dm) NOEXCEPT; CONSTCD14 year_month& operator+=(const years& dy) NOEXCEPT; CONSTCD14 year_month& operator-=(const years& dy) NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator< (const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator> (const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD14 year_month operator+(const year_month& ym, const months& dm) NOEXCEPT; CONSTCD14 year_month operator+(const months& dm, const year_month& ym) NOEXCEPT; CONSTCD14 year_month operator-(const year_month& ym, const months& dm) NOEXCEPT; CONSTCD11 months operator-(const year_month& x, const year_month& y) NOEXCEPT; CONSTCD11 year_month operator+(const year_month& ym, const years& dy) NOEXCEPT; CONSTCD11 year_month operator+(const years& dy, const year_month& ym) NOEXCEPT; CONSTCD11 year_month operator-(const year_month& ym, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month& ym); // month_day class month_day { solar_hijri::month m_; solar_hijri::day d_; public: month_day() = default; CONSTCD11 month_day(const solar_hijri::month& m, const solar_hijri::day& d) NOEXCEPT; CONSTCD11 solar_hijri::month month() const NOEXCEPT; CONSTCD11 solar_hijri::day day() const NOEXCEPT; CONSTCD14 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator< (const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator> (const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator<=(const month_day& x, const month_day& y) NOEXCEPT; CONSTCD11 bool operator>=(const month_day& x, const month_day& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_day& md); // month_day_last class month_day_last { solar_hijri::month m_; public: month_day_last() = default; CONSTCD11 explicit month_day_last(const solar_hijri::month& m) NOEXCEPT; CONSTCD11 solar_hijri::month month() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator< (const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator> (const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator<=(const month_day_last& x, const month_day_last& y) NOEXCEPT; CONSTCD11 bool operator>=(const month_day_last& x, const month_day_last& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_day_last& mdl); // month_weekday class month_weekday { solar_hijri::month m_; solar_hijri::weekday_indexed wdi_; public: month_weekday() = default; CONSTCD11 month_weekday(const solar_hijri::month& m, const solar_hijri::weekday_indexed& wdi) NOEXCEPT; CONSTCD11 solar_hijri::month month() const NOEXCEPT; CONSTCD11 solar_hijri::weekday_indexed weekday_indexed() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_weekday& x, const month_weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_weekday& x, const month_weekday& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday& mwd); // month_weekday_last class month_weekday_last { solar_hijri::month m_; solar_hijri::weekday_last wdl_; public: month_weekday_last() = default; CONSTCD11 month_weekday_last(const solar_hijri::month& m, const solar_hijri::weekday_last& wd) NOEXCEPT; CONSTCD11 solar_hijri::month month() const NOEXCEPT; CONSTCD11 solar_hijri::weekday_last weekday_last() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday_last& mwdl); // class year_month_day class year_month_day { solar_hijri::year y_; solar_hijri::month m_; solar_hijri::day d_; public: year_month_day() = default; CONSTCD11 year_month_day(const solar_hijri::year& y, const solar_hijri::month& m, const solar_hijri::day& d) NOEXCEPT; CONSTCD14 year_month_day(const year_month_day_last& ymdl) NOEXCEPT; CONSTCD14 year_month_day(sys_days dp) NOEXCEPT; CONSTCD14 explicit year_month_day(local_days dp) NOEXCEPT; CONSTCD14 year_month_day& operator+=(const months& m) NOEXCEPT; CONSTCD14 year_month_day& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_day& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_day& operator-=(const years& y) NOEXCEPT; CONSTCD11 solar_hijri::year year() const NOEXCEPT; CONSTCD11 solar_hijri::month month() const NOEXCEPT; CONSTCD11 solar_hijri::day day() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD14 bool ok() const NOEXCEPT; private: static CONSTCD14 year_month_day from_days(days dp) NOEXCEPT; CONSTCD14 days to_days() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator< (const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator> (const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_month_day& x, const year_month_day& y) NOEXCEPT; CONSTCD14 year_month_day operator+(const year_month_day& ymd, const months& dm) NOEXCEPT; CONSTCD14 year_month_day operator+(const months& dm, const year_month_day& ymd) NOEXCEPT; CONSTCD14 year_month_day operator-(const year_month_day& ymd, const months& dm) NOEXCEPT; CONSTCD11 year_month_day operator+(const year_month_day& ymd, const years& dy) NOEXCEPT; CONSTCD11 year_month_day operator+(const years& dy, const year_month_day& ymd) NOEXCEPT; CONSTCD11 year_month_day operator-(const year_month_day& ymd, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day& ymd); // year_month_day_last class year_month_day_last { solar_hijri::year y_; solar_hijri::month_day_last mdl_; public: year_month_day_last() = default; CONSTCD11 year_month_day_last(const solar_hijri::year& y, const solar_hijri::month_day_last& mdl) NOEXCEPT; CONSTCD14 year_month_day_last& operator+=(const months& m) NOEXCEPT; CONSTCD14 year_month_day_last& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_day_last& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_day_last& operator-=(const years& y) NOEXCEPT; CONSTCD11 solar_hijri::year year() const NOEXCEPT; CONSTCD11 solar_hijri::month month() const NOEXCEPT; CONSTCD11 solar_hijri::month_day_last month_day_last() const NOEXCEPT; CONSTCD14 solar_hijri::day day() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator< (const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator> (const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator<=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD11 bool operator>=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT; CONSTCD14 year_month_day_last operator+(const year_month_day_last& ymdl, const months& dm) NOEXCEPT; CONSTCD14 year_month_day_last operator+(const months& dm, const year_month_day_last& ymdl) NOEXCEPT; CONSTCD11 year_month_day_last operator+(const year_month_day_last& ymdl, const years& dy) NOEXCEPT; CONSTCD11 year_month_day_last operator+(const years& dy, const year_month_day_last& ymdl) NOEXCEPT; CONSTCD14 year_month_day_last operator-(const year_month_day_last& ymdl, const months& dm) NOEXCEPT; CONSTCD11 year_month_day_last operator-(const year_month_day_last& ymdl, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day_last& ymdl); // year_month_weekday class year_month_weekday { solar_hijri::year y_; solar_hijri::month m_; solar_hijri::weekday_indexed wdi_; public: year_month_weekday() = default; CONSTCD11 year_month_weekday(const solar_hijri::year& y, const solar_hijri::month& m, const solar_hijri::weekday_indexed& wdi) NOEXCEPT; CONSTCD14 year_month_weekday(const sys_days& dp) NOEXCEPT; CONSTCD14 explicit year_month_weekday(const local_days& dp) NOEXCEPT; CONSTCD14 year_month_weekday& operator+=(const months& m) NOEXCEPT; CONSTCD14 year_month_weekday& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_weekday& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_weekday& operator-=(const years& y) NOEXCEPT; CONSTCD11 solar_hijri::year year() const NOEXCEPT; CONSTCD11 solar_hijri::month month() const NOEXCEPT; CONSTCD11 solar_hijri::weekday weekday() const NOEXCEPT; CONSTCD11 unsigned index() const NOEXCEPT; CONSTCD11 solar_hijri::weekday_indexed weekday_indexed() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD14 bool ok() const NOEXCEPT; private: static CONSTCD14 year_month_weekday from_days(days dp) NOEXCEPT; CONSTCD14 days to_days() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT; CONSTCD14 year_month_weekday operator+(const year_month_weekday& ymwd, const months& dm) NOEXCEPT; CONSTCD14 year_month_weekday operator+(const months& dm, const year_month_weekday& ymwd) NOEXCEPT; CONSTCD11 year_month_weekday operator+(const year_month_weekday& ymwd, const years& dy) NOEXCEPT; CONSTCD11 year_month_weekday operator+(const years& dy, const year_month_weekday& ymwd) NOEXCEPT; CONSTCD14 year_month_weekday operator-(const year_month_weekday& ymwd, const months& dm) NOEXCEPT; CONSTCD11 year_month_weekday operator-(const year_month_weekday& ymwd, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday& ymwdi); // year_month_weekday_last class year_month_weekday_last { solar_hijri::year y_; solar_hijri::month m_; solar_hijri::weekday_last wdl_; public: year_month_weekday_last() = default; CONSTCD11 year_month_weekday_last(const solar_hijri::year& y, const solar_hijri::month& m, const solar_hijri::weekday_last& wdl) NOEXCEPT; CONSTCD14 year_month_weekday_last& operator+=(const months& m) NOEXCEPT; CONSTCD14 year_month_weekday_last& operator-=(const months& m) NOEXCEPT; CONSTCD14 year_month_weekday_last& operator+=(const years& y) NOEXCEPT; CONSTCD14 year_month_weekday_last& operator-=(const years& y) NOEXCEPT; CONSTCD11 solar_hijri::year year() const NOEXCEPT; CONSTCD11 solar_hijri::month month() const NOEXCEPT; CONSTCD11 solar_hijri::weekday weekday() const NOEXCEPT; CONSTCD11 solar_hijri::weekday_last weekday_last() const NOEXCEPT; CONSTCD14 operator sys_days() const NOEXCEPT; CONSTCD14 explicit operator local_days() const NOEXCEPT; CONSTCD11 bool ok() const NOEXCEPT; private: CONSTCD14 days to_days() const NOEXCEPT; }; CONSTCD11 bool operator==(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT; CONSTCD11 bool operator!=(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT; CONSTCD14 year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT; CONSTCD14 year_month_weekday_last operator+(const months& dm, const year_month_weekday_last& ymwdl) NOEXCEPT; CONSTCD11 year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT; CONSTCD11 year_month_weekday_last operator+(const years& dy, const year_month_weekday_last& ymwdl) NOEXCEPT; CONSTCD14 year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT; CONSTCD11 year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT; template std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday_last& ymwdl); #if !defined(_MSC_VER) || (_MSC_VER >= 1900) inline namespace literals { CONSTCD11 solar_hijri::day operator ""_d(unsigned long long d) NOEXCEPT; CONSTCD11 solar_hijri::year operator ""_y(unsigned long long y) NOEXCEPT; } // inline namespace literals #endif // !defined(_MSC_VER) || (_MSC_VER >= 1900) //----------------+ // Implementation | //----------------+ // day CONSTCD11 inline day::day(unsigned d) NOEXCEPT : d_(static_cast(d)) {} CONSTCD14 inline day& day::operator++() NOEXCEPT {++d_; return *this;} CONSTCD14 inline day day::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline day& day::operator--() NOEXCEPT {--d_; return *this;} CONSTCD14 inline day day::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline day& day::operator+=(const days& d) NOEXCEPT {*this = *this + d; return *this;} CONSTCD14 inline day& day::operator-=(const days& d) NOEXCEPT {*this = *this - d; return *this;} CONSTCD11 inline day::operator unsigned() const NOEXCEPT {return d_;} CONSTCD11 inline bool day::ok() const NOEXCEPT {return 1 <= d_ && d_ <= 30;} CONSTCD11 inline bool operator==(const day& x, const day& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const day& x, const day& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const day& x, const day& y) NOEXCEPT { return static_cast(x) < static_cast(y); } CONSTCD11 inline bool operator>(const day& x, const day& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const day& x, const day& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const day& x, const day& y) NOEXCEPT { return !(x < y); } CONSTCD11 inline days operator-(const day& x, const day& y) NOEXCEPT { return days{static_cast(static_cast(x) - static_cast(y))}; } CONSTCD11 inline day operator+(const day& x, const days& y) NOEXCEPT { return day{static_cast(x) + static_cast(y.count())}; } CONSTCD11 inline day operator+(const days& x, const day& y) NOEXCEPT { return y + x; } CONSTCD11 inline day operator-(const day& x, const days& y) NOEXCEPT { return x + -y; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const day& d) { date::detail::save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::right); os.width(2); os << static_cast(d); return os; } // month CONSTCD11 inline month::month(unsigned m) NOEXCEPT : m_(static_cast(m)) {} CONSTCD14 inline month& month::operator++() NOEXCEPT {if (++m_ == 13) m_ = 1; return *this;} CONSTCD14 inline month month::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline month& month::operator--() NOEXCEPT {if (--m_ == 0) m_ = 12; return *this;} CONSTCD14 inline month month::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline month& month::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline month& month::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD11 inline month::operator unsigned() const NOEXCEPT {return m_;} CONSTCD11 inline bool month::ok() const NOEXCEPT {return 1 <= m_ && m_ <= 12;} CONSTCD11 inline bool operator==(const month& x, const month& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const month& x, const month& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const month& x, const month& y) NOEXCEPT { return static_cast(x) < static_cast(y); } CONSTCD11 inline bool operator>(const month& x, const month& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const month& x, const month& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const month& x, const month& y) NOEXCEPT { return !(x < y); } CONSTCD14 inline months operator-(const month& x, const month& y) NOEXCEPT { auto const d = static_cast(x) - static_cast(y); return months(d <= 11 ? d : d + 12); } CONSTCD14 inline month operator+(const month& x, const months& y) NOEXCEPT { auto const mu = static_cast(static_cast(x)) - 1 + y.count(); auto const yr = (mu >= 0 ? mu : mu-11) / 12; return month{static_cast(mu - yr * 12 + 1)}; } CONSTCD14 inline month operator+(const months& x, const month& y) NOEXCEPT { return y + x; } CONSTCD14 inline month operator-(const month& x, const months& y) NOEXCEPT { return x + -y; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month& m) { switch (static_cast(m)) { case 1: os << "Farvardin"; break; case 2: os << "Ordibehesht"; break; case 3: os << "Khordad"; break; case 4: os << "Tir"; break; case 5: os << "Mordad"; break; case 6: os << "Shahrivar"; break; case 7: os << "Mehr"; break; case 8: os << "Aban"; break; case 9: os << "Azar"; break; case 10: os << "Dey"; break; case 11: os << "Bahman"; break; case 12: os << "Esfand"; break; default: os << static_cast(m) << " is not a valid month"; break; } return os; } // year CONSTCD11 inline year::year(int y) NOEXCEPT : y_(static_cast(y)) {} CONSTCD14 inline year& year::operator++() NOEXCEPT {++y_; return *this;} CONSTCD14 inline year year::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline year& year::operator--() NOEXCEPT {--y_; return *this;} CONSTCD14 inline year year::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline year& year::operator+=(const years& y) NOEXCEPT {*this = *this + y; return *this;} CONSTCD14 inline year& year::operator-=(const years& y) NOEXCEPT {*this = *this - y; return *this;} CONSTCD14 inline bool year::is_leap() const NOEXCEPT { using namespace internal; auto const y = static_cast(y_)-475; auto const era_d = static_cast(y >= 0 ? y : y-years_in_era+1) / static_cast(years_in_era); auto const era = static_cast(era_d); auto const yoe = static_cast(y - era * years_in_era); // Reference: https://www.timeanddate.com/date/iran-leap-year.html // 29 + 33 + 33 + 33 = 128 // 22 * 128 + 4 auto const yoc = (yoe < (22 * 128)) ? ((yoe%128) < 29 ? yoe%128 : (yoe%128 - 29)%33) : yoe - (22 * 128) + 33; return (yoc != 0 && (yoc%4)==0); } CONSTCD11 inline year::operator int() const NOEXCEPT {return y_;} CONSTCD11 inline bool year::ok() const NOEXCEPT {return true;} CONSTCD11 inline year year::min() NOEXCEPT { return year{std::numeric_limits::min()}; } CONSTCD11 inline year year::max() NOEXCEPT { return year{std::numeric_limits::max()}; } CONSTCD11 inline bool operator==(const year& x, const year& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const year& x, const year& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year& x, const year& y) NOEXCEPT { return static_cast(x) < static_cast(y); } CONSTCD11 inline bool operator>(const year& x, const year& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year& x, const year& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year& x, const year& y) NOEXCEPT { return !(x < y); } CONSTCD11 inline years operator-(const year& x, const year& y) NOEXCEPT { return years{static_cast(x) - static_cast(y)}; } CONSTCD11 inline year operator+(const year& x, const years& y) NOEXCEPT { return year{static_cast(x) + y.count()}; } CONSTCD11 inline year operator+(const years& x, const year& y) NOEXCEPT { return y + x; } CONSTCD11 inline year operator-(const year& x, const years& y) NOEXCEPT { return year{static_cast(x) - y.count()}; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year& y) { date::detail::save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::internal); os.width(4 + (y < year{0})); os << static_cast(y); return os; } // weekday CONSTCD11 inline unsigned char weekday::weekday_from_days(int z) NOEXCEPT { auto u = static_cast(z); return static_cast(z >= -4 ? (u+4) % 7 : u % 7); } CONSTCD11 inline weekday::weekday(unsigned wd) NOEXCEPT : wd_(static_cast(wd != 7 ? wd : 0)) {} CONSTCD11 inline weekday::weekday(const sys_days& dp) NOEXCEPT : wd_(weekday_from_days(dp.time_since_epoch().count())) {} CONSTCD11 inline weekday::weekday(const local_days& dp) NOEXCEPT : wd_(weekday_from_days(dp.time_since_epoch().count())) {} CONSTCD14 inline weekday& weekday::operator++() NOEXCEPT {if (++wd_ == 7) wd_ = 0; return *this;} CONSTCD14 inline weekday weekday::operator++(int) NOEXCEPT {auto tmp(*this); ++(*this); return tmp;} CONSTCD14 inline weekday& weekday::operator--() NOEXCEPT {if (wd_-- == 0) wd_ = 6; return *this;} CONSTCD14 inline weekday weekday::operator--(int) NOEXCEPT {auto tmp(*this); --(*this); return tmp;} CONSTCD14 inline weekday& weekday::operator+=(const days& d) NOEXCEPT { *this = *this + d; return *this; } CONSTCD14 inline weekday& weekday::operator-=(const days& d) NOEXCEPT { *this = *this - d; return *this; } CONSTCD11 inline weekday::operator unsigned() const NOEXCEPT { return static_cast(wd_); } CONSTCD11 inline bool weekday::ok() const NOEXCEPT {return wd_ <= 6;} CONSTCD11 inline bool operator==(const weekday& x, const weekday& y) NOEXCEPT { return static_cast(x) == static_cast(y); } CONSTCD11 inline bool operator!=(const weekday& x, const weekday& y) NOEXCEPT { return !(x == y); } CONSTCD14 inline days operator-(const weekday& x, const weekday& y) NOEXCEPT { auto const diff = static_cast(x) - static_cast(y); return days{diff <= 6 ? diff : diff + 7}; } CONSTCD14 inline weekday operator+(const weekday& x, const days& y) NOEXCEPT { auto const wdu = static_cast(static_cast(x)) + y.count(); auto const wk = (wdu >= 0 ? wdu : wdu-6) / 7; return weekday{static_cast(wdu - wk * 7)}; } CONSTCD14 inline weekday operator+(const days& x, const weekday& y) NOEXCEPT { return y + x; } CONSTCD14 inline weekday operator-(const weekday& x, const days& y) NOEXCEPT { return x + -y; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const weekday& wd) { switch (static_cast(wd)) { case 0: os << "Yekshanbe"; break; case 1: os << "Doshanbe"; break; case 2: os << "Seshanbe"; break; case 3: os << "Chaharshanbe"; break; case 4: os << "Panjshanbe"; break; case 5: os << "Adine"; break; case 6: os << "Shanbe"; break; default: os << static_cast(wd) << " is not a valid weekday"; break; } return os; } #if !defined(_MSC_VER) || (_MSC_VER >= 1900) inline namespace literals { CONSTCD11 inline solar_hijri::day operator ""_d(unsigned long long d) NOEXCEPT { return solar_hijri::day{static_cast(d)}; } CONSTCD11 inline solar_hijri::year operator ""_y(unsigned long long y) NOEXCEPT { return solar_hijri::year(static_cast(y)); } #endif // !defined(_MSC_VER) || (_MSC_VER >= 1900) CONSTDATA solar_hijri::last_spec last{}; CONSTDATA solar_hijri::month far {1}; CONSTDATA solar_hijri::month ord {2}; CONSTDATA solar_hijri::month kho {3}; CONSTDATA solar_hijri::month tir {4}; CONSTDATA solar_hijri::month mor {5}; CONSTDATA solar_hijri::month sha {6}; CONSTDATA solar_hijri::month meh {7}; CONSTDATA solar_hijri::month aba {8}; CONSTDATA solar_hijri::month aza {9}; CONSTDATA solar_hijri::month dey {10}; CONSTDATA solar_hijri::month bah {11}; CONSTDATA solar_hijri::month esf {12}; CONSTDATA solar_hijri::month Farvardin {1}; CONSTDATA solar_hijri::month Ordibehesht {2}; CONSTDATA solar_hijri::month Khordad {3}; CONSTDATA solar_hijri::month Tir {4}; CONSTDATA solar_hijri::month Mordad {5}; CONSTDATA solar_hijri::month Shahrivar {6}; CONSTDATA solar_hijri::month Mehr {7}; CONSTDATA solar_hijri::month Aban {8}; CONSTDATA solar_hijri::month Azar {9}; CONSTDATA solar_hijri::month Dey {10}; CONSTDATA solar_hijri::month Bahman {11}; CONSTDATA solar_hijri::month Esfand {12}; CONSTDATA solar_hijri::weekday yek {0u}; CONSTDATA solar_hijri::weekday dos {1u}; CONSTDATA solar_hijri::weekday ses {2u}; CONSTDATA solar_hijri::weekday cha {3u}; CONSTDATA solar_hijri::weekday pan {4u}; CONSTDATA solar_hijri::weekday adi {5u}; CONSTDATA solar_hijri::weekday shn {6u}; CONSTDATA solar_hijri::weekday Yekshanbe {0u}; CONSTDATA solar_hijri::weekday Doshanbe {1u}; CONSTDATA solar_hijri::weekday Seshanbe {2u}; CONSTDATA solar_hijri::weekday Chaharshanbe {3u}; CONSTDATA solar_hijri::weekday Panjshanbe {4u}; CONSTDATA solar_hijri::weekday Adine {5u}; CONSTDATA solar_hijri::weekday Shanbe {6u}; #if !defined(_MSC_VER) || (_MSC_VER >= 1900) } // inline namespace literals #endif // weekday_indexed CONSTCD11 inline weekday weekday_indexed::weekday() const NOEXCEPT { return solar_hijri::weekday{static_cast(wd_)}; } CONSTCD11 inline unsigned weekday_indexed::index() const NOEXCEPT {return index_;} CONSTCD11 inline bool weekday_indexed::ok() const NOEXCEPT { return weekday().ok() && 1 <= index_ && index_ <= 5; } CONSTCD11 inline weekday_indexed::weekday_indexed(const solar_hijri::weekday& wd, unsigned index) NOEXCEPT : wd_(static_cast(static_cast(wd))) , index_(static_cast(index)) {} template inline std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_indexed& wdi) { return os << wdi.weekday() << '[' << wdi.index() << ']'; } CONSTCD11 inline weekday_indexed weekday::operator[](unsigned index) const NOEXCEPT { return {*this, index}; } CONSTCD11 inline bool operator==(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT { return x.weekday() == y.weekday() && x.index() == y.index(); } CONSTCD11 inline bool operator!=(const weekday_indexed& x, const weekday_indexed& y) NOEXCEPT { return !(x == y); } // weekday_last CONSTCD11 inline solar_hijri::weekday weekday_last::weekday() const NOEXCEPT {return wd_;} CONSTCD11 inline bool weekday_last::ok() const NOEXCEPT {return wd_.ok();} CONSTCD11 inline weekday_last::weekday_last(const solar_hijri::weekday& wd) NOEXCEPT : wd_(wd) {} CONSTCD11 inline bool operator==(const weekday_last& x, const weekday_last& y) NOEXCEPT { return x.weekday() == y.weekday(); } CONSTCD11 inline bool operator!=(const weekday_last& x, const weekday_last& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const weekday_last& wdl) { return os << wdl.weekday() << "[last]"; } CONSTCD11 inline weekday_last weekday::operator[](last_spec) const NOEXCEPT { return weekday_last{*this}; } // year_month CONSTCD11 inline year_month::year_month(const solar_hijri::year& y, const solar_hijri::month& m) NOEXCEPT : y_(y) , m_(m) {} CONSTCD11 inline year year_month::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month::month() const NOEXCEPT {return m_;} CONSTCD11 inline bool year_month::ok() const NOEXCEPT {return y_.ok() && m_.ok();} CONSTCD14 inline year_month& year_month::operator+=(const months& dm) NOEXCEPT { *this = *this + dm; return *this; } CONSTCD14 inline year_month& year_month::operator-=(const months& dm) NOEXCEPT { *this = *this - dm; return *this; } CONSTCD14 inline year_month& year_month::operator+=(const years& dy) NOEXCEPT { *this = *this + dy; return *this; } CONSTCD14 inline year_month& year_month::operator-=(const years& dy) NOEXCEPT { *this = *this - dy; return *this; } CONSTCD11 inline bool operator==(const year_month& x, const year_month& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month(); } CONSTCD11 inline bool operator!=(const year_month& x, const year_month& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_month& x, const year_month& y) NOEXCEPT { return x.year() < y.year() ? true : (x.year() > y.year() ? false : (x.month() < y.month())); } CONSTCD11 inline bool operator>(const year_month& x, const year_month& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_month& x, const year_month& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_month& x, const year_month& y) NOEXCEPT { return !(x < y); } CONSTCD14 inline year_month operator+(const year_month& ym, const months& dm) NOEXCEPT { auto dmi = static_cast(static_cast(ym.month())) - 1 + dm.count(); auto dy = (dmi >= 0 ? dmi : dmi-11) / 12; dmi = dmi - dy * 12 + 1; return (ym.year() + years(dy)) / month(static_cast(dmi)); } CONSTCD14 inline year_month operator+(const months& dm, const year_month& ym) NOEXCEPT { return ym + dm; } CONSTCD14 inline year_month operator-(const year_month& ym, const months& dm) NOEXCEPT { return ym + -dm; } CONSTCD11 inline months operator-(const year_month& x, const year_month& y) NOEXCEPT { return (x.year() - y.year()) + months(static_cast(x.month()) - static_cast(y.month())); } CONSTCD11 inline year_month operator+(const year_month& ym, const years& dy) NOEXCEPT { return (ym.year() + dy) / ym.month(); } CONSTCD11 inline year_month operator+(const years& dy, const year_month& ym) NOEXCEPT { return ym + dy; } CONSTCD11 inline year_month operator-(const year_month& ym, const years& dy) NOEXCEPT { return ym + -dy; } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month& ym) { return os << ym.year() << '/' << ym.month(); } // month_day CONSTCD11 inline month_day::month_day(const solar_hijri::month& m, const solar_hijri::day& d) NOEXCEPT : m_(m) , d_(d) {} CONSTCD11 inline solar_hijri::month month_day::month() const NOEXCEPT {return m_;} CONSTCD11 inline solar_hijri::day month_day::day() const NOEXCEPT {return d_;} CONSTCD14 inline bool month_day::ok() const NOEXCEPT { CONSTDATA solar_hijri::day d[] = { solar_hijri::day(31), solar_hijri::day(31), solar_hijri::day(31), solar_hijri::day(31), solar_hijri::day(31), solar_hijri::day(31), solar_hijri::day(30), solar_hijri::day(30), solar_hijri::day(30), solar_hijri::day(30), solar_hijri::day(30), solar_hijri::day(30) }; return m_.ok() && solar_hijri::day(1) <= d_ && d_ <= d[static_cast(m_)-1]; } CONSTCD11 inline bool operator==(const month_day& x, const month_day& y) NOEXCEPT { return x.month() == y.month() && x.day() == y.day(); } CONSTCD11 inline bool operator!=(const month_day& x, const month_day& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const month_day& x, const month_day& y) NOEXCEPT { return x.month() < y.month() ? true : (x.month() > y.month() ? false : (x.day() < y.day())); } CONSTCD11 inline bool operator>(const month_day& x, const month_day& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const month_day& x, const month_day& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const month_day& x, const month_day& y) NOEXCEPT { return !(x < y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_day& md) { return os << md.month() << '/' << md.day(); } // month_day_last CONSTCD11 inline month month_day_last::month() const NOEXCEPT {return m_;} CONSTCD11 inline bool month_day_last::ok() const NOEXCEPT {return m_.ok();} CONSTCD11 inline month_day_last::month_day_last(const solar_hijri::month& m) NOEXCEPT : m_(m) {} CONSTCD11 inline bool operator==(const month_day_last& x, const month_day_last& y) NOEXCEPT { return x.month() == y.month(); } CONSTCD11 inline bool operator!=(const month_day_last& x, const month_day_last& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const month_day_last& x, const month_day_last& y) NOEXCEPT { return x.month() < y.month(); } CONSTCD11 inline bool operator>(const month_day_last& x, const month_day_last& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const month_day_last& x, const month_day_last& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const month_day_last& x, const month_day_last& y) NOEXCEPT { return !(x < y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_day_last& mdl) { return os << mdl.month() << "/last"; } // month_weekday CONSTCD11 inline month_weekday::month_weekday(const solar_hijri::month& m, const solar_hijri::weekday_indexed& wdi) NOEXCEPT : m_(m) , wdi_(wdi) {} CONSTCD11 inline month month_weekday::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday_indexed month_weekday::weekday_indexed() const NOEXCEPT { return wdi_; } CONSTCD11 inline bool month_weekday::ok() const NOEXCEPT { return m_.ok() && wdi_.ok(); } CONSTCD11 inline bool operator==(const month_weekday& x, const month_weekday& y) NOEXCEPT { return x.month() == y.month() && x.weekday_indexed() == y.weekday_indexed(); } CONSTCD11 inline bool operator!=(const month_weekday& x, const month_weekday& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday& mwd) { return os << mwd.month() << '/' << mwd.weekday_indexed(); } // month_weekday_last CONSTCD11 inline month_weekday_last::month_weekday_last(const solar_hijri::month& m, const solar_hijri::weekday_last& wdl) NOEXCEPT : m_(m) , wdl_(wdl) {} CONSTCD11 inline month month_weekday_last::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday_last month_weekday_last::weekday_last() const NOEXCEPT { return wdl_; } CONSTCD11 inline bool month_weekday_last::ok() const NOEXCEPT { return m_.ok() && wdl_.ok(); } CONSTCD11 inline bool operator==(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT { return x.month() == y.month() && x.weekday_last() == y.weekday_last(); } CONSTCD11 inline bool operator!=(const month_weekday_last& x, const month_weekday_last& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const month_weekday_last& mwdl) { return os << mwdl.month() << '/' << mwdl.weekday_last(); } // year_month_day_last CONSTCD11 inline year_month_day_last::year_month_day_last(const solar_hijri::year& y, const solar_hijri::month_day_last& mdl) NOEXCEPT : y_(y) , mdl_(mdl) {} CONSTCD14 inline year_month_day_last& year_month_day_last::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline year_month_day_last& year_month_day_last::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_day_last& year_month_day_last::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_day_last& year_month_day_last::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD11 inline year year_month_day_last::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_day_last::month() const NOEXCEPT {return mdl_.month();} CONSTCD11 inline month_day_last year_month_day_last::month_day_last() const NOEXCEPT { return mdl_; } CONSTCD14 inline day year_month_day_last::day() const NOEXCEPT { CONSTDATA solar_hijri::day d[] = { solar_hijri::day(31), solar_hijri::day(31), solar_hijri::day(31), solar_hijri::day(31), solar_hijri::day(31), solar_hijri::day(31), solar_hijri::day(30), solar_hijri::day(30), solar_hijri::day(30), solar_hijri::day(30), solar_hijri::day(30), solar_hijri::day(29) }; return month() != esf || !y_.is_leap() ? d[static_cast(month()) - 1] : solar_hijri::day(30); } CONSTCD14 inline year_month_day_last::operator sys_days() const NOEXCEPT { return sys_days(year()/month()/day()); } CONSTCD14 inline year_month_day_last::operator local_days() const NOEXCEPT { return local_days(year()/month()/day()); } CONSTCD11 inline bool year_month_day_last::ok() const NOEXCEPT { return y_.ok() && mdl_.ok(); } CONSTCD11 inline bool operator==(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return x.year() == y.year() && x.month_day_last() == y.month_day_last(); } CONSTCD11 inline bool operator!=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return x.year() < y.year() ? true : (x.year() > y.year() ? false : (x.month_day_last() < y.month_day_last())); } CONSTCD11 inline bool operator>(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_month_day_last& x, const year_month_day_last& y) NOEXCEPT { return !(x < y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day_last& ymdl) { return os << ymdl.year() << '/' << ymdl.month_day_last(); } CONSTCD14 inline year_month_day_last operator+(const year_month_day_last& ymdl, const months& dm) NOEXCEPT { return (ymdl.year() / ymdl.month() + dm) / last; } CONSTCD14 inline year_month_day_last operator+(const months& dm, const year_month_day_last& ymdl) NOEXCEPT { return ymdl + dm; } CONSTCD14 inline year_month_day_last operator-(const year_month_day_last& ymdl, const months& dm) NOEXCEPT { return ymdl + (-dm); } CONSTCD11 inline year_month_day_last operator+(const year_month_day_last& ymdl, const years& dy) NOEXCEPT { return {ymdl.year()+dy, ymdl.month_day_last()}; } CONSTCD11 inline year_month_day_last operator+(const years& dy, const year_month_day_last& ymdl) NOEXCEPT { return ymdl + dy; } CONSTCD11 inline year_month_day_last operator-(const year_month_day_last& ymdl, const years& dy) NOEXCEPT { return ymdl + (-dy); } // year_month_day CONSTCD11 inline year_month_day::year_month_day(const solar_hijri::year& y, const solar_hijri::month& m, const solar_hijri::day& d) NOEXCEPT : y_(y) , m_(m) , d_(d) {} CONSTCD14 inline year_month_day::year_month_day(const year_month_day_last& ymdl) NOEXCEPT : y_(ymdl.year()) , m_(ymdl.month()) , d_(ymdl.day()) {} CONSTCD14 inline year_month_day::year_month_day(sys_days dp) NOEXCEPT : year_month_day(from_days(dp.time_since_epoch())) {} CONSTCD14 inline year_month_day::year_month_day(local_days dp) NOEXCEPT : year_month_day(from_days(dp.time_since_epoch())) {} CONSTCD11 inline year year_month_day::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_day::month() const NOEXCEPT {return m_;} CONSTCD11 inline day year_month_day::day() const NOEXCEPT {return d_;} CONSTCD14 inline year_month_day& year_month_day::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline year_month_day& year_month_day::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_day& year_month_day::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_day& year_month_day::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD14 inline days year_month_day::to_days() const NOEXCEPT { static_assert(std::numeric_limits::digits >= 18, "This algorithm has not been ported to a 16 bit unsigned integer"); static_assert(std::numeric_limits::digits >= 20, "This algorithm has not been ported to a 16 bit signed integer"); using namespace internal; auto const y = static_cast(y_) - 475; auto const m = static_cast(m_); auto const d = static_cast(d_); auto const era_d = static_cast(y >= 0 ? y : y-years_in_era+1) / static_cast(years_in_era); auto const era = static_cast(era_d); auto const fdoe = static_cast(epoch + era * days_in_era); auto const yoe = static_cast(y - era * years_in_era); auto const period_d = static_cast(yoe/years_in_period); auto const period = static_cast(period_d); auto const yop = yoe%years_in_period; auto const fdop = period*days_in_period; auto const cycle = yop < 29 ? 0 : static_cast((yop-29)/years_in_other_cycles + 1); auto const yoc = yop < 29 ? yop : (yop-29)%years_in_other_cycles; auto const fdoc = cycle > 0 ? days_in_first_cycle + (cycle-1)*days_in_other_cycles : 0; auto const group = yoc < 1 ? 0 : static_cast((yoc-1) / 4); auto const yog = static_cast(yoc < 1 ? -1 : (yoc-1) % 4); auto const fdoyog = group*1461 + (yog+1)*365; auto const fdoyoe = fdop + fdoc + fdoyog; auto const doy = 30*(m-1) + ((m > 6) ? 6 : m-1) + d-1; // [0, 365] auto const doe = fdoe + fdoyoe + doy; return days{doe - unix_time_shift}; } CONSTCD14 inline year_month_day::operator sys_days() const NOEXCEPT { return sys_days{to_days()}; } CONSTCD14 inline year_month_day::operator local_days() const NOEXCEPT { return local_days{to_days()}; } CONSTCD14 inline bool year_month_day::ok() const NOEXCEPT { if (!(y_.ok() && m_.ok())) return false; return solar_hijri::day(1) <= d_ && d_ <= (y_/m_/last).day(); } CONSTCD11 inline bool operator==(const year_month_day& x, const year_month_day& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month() && x.day() == y.day(); } CONSTCD11 inline bool operator!=(const year_month_day& x, const year_month_day& y) NOEXCEPT { return !(x == y); } CONSTCD11 inline bool operator<(const year_month_day& x, const year_month_day& y) NOEXCEPT { return x.year() < y.year() ? true : (x.year() > y.year() ? false : (x.month() < y.month() ? true : (x.month() > y.month() ? false : (x.day() < y.day())))); } CONSTCD11 inline bool operator>(const year_month_day& x, const year_month_day& y) NOEXCEPT { return y < x; } CONSTCD11 inline bool operator<=(const year_month_day& x, const year_month_day& y) NOEXCEPT { return !(y < x); } CONSTCD11 inline bool operator>=(const year_month_day& x, const year_month_day& y) NOEXCEPT { return !(x < y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_day& ymd) { date::detail::save_ostream _(os); os.fill('0'); os.flags(std::ios::dec | std::ios::right); os << ymd.year() << '-'; os.width(2); os << static_cast(ymd.month()) << '-'; os << ymd.day(); return os; } CONSTCD14 inline year_month_day year_month_day::from_days(days dp) NOEXCEPT { static_assert(std::numeric_limits::digits >= 18, "This algorithm has not been ported to a 16 bit unsigned integer"); static_assert(std::numeric_limits::digits >= 20, "This algorithm has not been ported to a 16 bit signed integer"); using namespace internal; auto const z = dp.count() + unix_time_shift; auto const delta = static_cast(z - epoch); auto const era = static_cast(delta >= 0 ? delta : delta-days_in_era+1) / static_cast(days_in_era); auto const era_i = static_cast(era); auto const fdoe = static_cast(epoch + static_cast(era_i * days_in_era)); auto const doe_fdoe = z - fdoe; auto const period = static_cast(doe_fdoe < 22*days_in_period ? doe_fdoe / days_in_period : 22); auto const dop = doe_fdoe % days_in_period; auto const cycle = dop < days_in_first_cycle ? 0 : (dop-days_in_first_cycle) / days_in_other_cycles + 1; auto const doc = dop < days_in_first_cycle ? dop : (dop-days_in_first_cycle) % days_in_other_cycles; auto const group = doc < 365 && period != 22 ? -1 : static_cast(((doc < 365 ? 365 : doc)-365)/1461); auto const yog = doc < 365 && period != 22 ? -1 : static_cast( (period != 22 ? ((doc-365 )%1461) : doc)/365); auto const yoc = group == -1 ? 0 : (period != 22 ? 1 : 0) + group*4 + (yog == 4 ? 3 : yog); auto const doy = group == -1 ? doc : (period != 22 ? ((yoc-1)%4 == 0 ? (group >= 0 ? (doe_fdoe - (period*days_in_period) - (cycle > 0 ? days_in_first_cycle + (cycle-1)*days_in_other_cycles : 0) - (group*1461 + ((yog == 4 ? 3 : yog)+1)*365)) : 365) : doe_fdoe - (period*days_in_period) - (cycle > 0 ? days_in_first_cycle + (cycle-1)*days_in_other_cycles : 0) - (group*1461 + ((yog == 4 ? 3 : yog)+1)*365)) : (yog == 4 ? 365 : doe_fdoe - (period*days_in_period) - yog*365)); auto const yoe = period != 22 ? period*years_in_period + (cycle > 0 ? years_in_first_cycle + (cycle-1)*years_in_other_cycles : 0) + yoc : 22*years_in_period + ((yog == 4) ? 3 : yog); auto const y = static_cast(static_cast(yoe) + 475 + era_i * years_in_era); auto const m = doy < 186 ? doy/31 + 1 : (doy-186)/30 + 7; // [1, 12] auto const d = doy - (30*(m-1) + ((m > 6) ? 6 : m-1) - 1); // [1, 31] return year_month_day{solar_hijri::year(y), solar_hijri::month(m), solar_hijri::day(d)}; } CONSTCD14 inline year_month_day operator+(const year_month_day& ymd, const months& dm) NOEXCEPT { return (ymd.year() / ymd.month() + dm) / ymd.day(); } CONSTCD14 inline year_month_day operator+(const months& dm, const year_month_day& ymd) NOEXCEPT { return ymd + dm; } CONSTCD14 inline year_month_day operator-(const year_month_day& ymd, const months& dm) NOEXCEPT { return ymd + (-dm); } CONSTCD11 inline year_month_day operator+(const year_month_day& ymd, const years& dy) NOEXCEPT { return (ymd.year() + dy) / ymd.month() / ymd.day(); } CONSTCD11 inline year_month_day operator+(const years& dy, const year_month_day& ymd) NOEXCEPT { return ymd + dy; } CONSTCD11 inline year_month_day operator-(const year_month_day& ymd, const years& dy) NOEXCEPT { return ymd + (-dy); } // year_month_weekday CONSTCD11 inline year_month_weekday::year_month_weekday(const solar_hijri::year& y, const solar_hijri::month& m, const solar_hijri::weekday_indexed& wdi) NOEXCEPT : y_(y) , m_(m) , wdi_(wdi) {} CONSTCD14 inline year_month_weekday::year_month_weekday(const sys_days& dp) NOEXCEPT : year_month_weekday(from_days(dp.time_since_epoch())) {} CONSTCD14 inline year_month_weekday::year_month_weekday(const local_days& dp) NOEXCEPT : year_month_weekday(from_days(dp.time_since_epoch())) {} CONSTCD14 inline year_month_weekday& year_month_weekday::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline year_month_weekday& year_month_weekday::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_weekday& year_month_weekday::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_weekday& year_month_weekday::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD11 inline year year_month_weekday::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_weekday::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday year_month_weekday::weekday() const NOEXCEPT { return wdi_.weekday(); } CONSTCD11 inline unsigned year_month_weekday::index() const NOEXCEPT { return wdi_.index(); } CONSTCD11 inline weekday_indexed year_month_weekday::weekday_indexed() const NOEXCEPT { return wdi_; } CONSTCD14 inline year_month_weekday::operator sys_days() const NOEXCEPT { return sys_days{to_days()}; } CONSTCD14 inline year_month_weekday::operator local_days() const NOEXCEPT { return local_days{to_days()}; } CONSTCD14 inline bool year_month_weekday::ok() const NOEXCEPT { if (!y_.ok() || !m_.ok() || !wdi_.weekday().ok() || wdi_.index() < 1) return false; if (wdi_.index() <= 4) return true; auto d2 = wdi_.weekday() - solar_hijri::weekday(y_/m_/1) + days((wdi_.index()-1)*7 + 1); return static_cast(d2.count()) <= static_cast((y_/m_/last).day()); } CONSTCD14 inline year_month_weekday year_month_weekday::from_days(days d) NOEXCEPT { sys_days dp{d}; auto const wd = solar_hijri::weekday(dp); auto const ymd = year_month_day(dp); return {ymd.year(), ymd.month(), wd[(static_cast(ymd.day())-1)/7+1]}; } CONSTCD14 inline days year_month_weekday::to_days() const NOEXCEPT { auto d = sys_days(y_/m_/1); return (d + (wdi_.weekday() - solar_hijri::weekday(d) + days{(wdi_.index()-1)*7}) ).time_since_epoch(); } CONSTCD11 inline bool operator==(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month() && x.weekday_indexed() == y.weekday_indexed(); } CONSTCD11 inline bool operator!=(const year_month_weekday& x, const year_month_weekday& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday& ymwdi) { return os << ymwdi.year() << '/' << ymwdi.month() << '/' << ymwdi.weekday_indexed(); } CONSTCD14 inline year_month_weekday operator+(const year_month_weekday& ymwd, const months& dm) NOEXCEPT { return (ymwd.year() / ymwd.month() + dm) / ymwd.weekday_indexed(); } CONSTCD14 inline year_month_weekday operator+(const months& dm, const year_month_weekday& ymwd) NOEXCEPT { return ymwd + dm; } CONSTCD14 inline year_month_weekday operator-(const year_month_weekday& ymwd, const months& dm) NOEXCEPT { return ymwd + (-dm); } CONSTCD11 inline year_month_weekday operator+(const year_month_weekday& ymwd, const years& dy) NOEXCEPT { return {ymwd.year()+dy, ymwd.month(), ymwd.weekday_indexed()}; } CONSTCD11 inline year_month_weekday operator+(const years& dy, const year_month_weekday& ymwd) NOEXCEPT { return ymwd + dy; } CONSTCD11 inline year_month_weekday operator-(const year_month_weekday& ymwd, const years& dy) NOEXCEPT { return ymwd + (-dy); } // year_month_weekday_last CONSTCD11 inline year_month_weekday_last::year_month_weekday_last(const solar_hijri::year& y, const solar_hijri::month& m, const solar_hijri::weekday_last& wdl) NOEXCEPT : y_(y) , m_(m) , wdl_(wdl) {} CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator+=(const months& m) NOEXCEPT { *this = *this + m; return *this; } CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator-=(const months& m) NOEXCEPT { *this = *this - m; return *this; } CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator+=(const years& y) NOEXCEPT { *this = *this + y; return *this; } CONSTCD14 inline year_month_weekday_last& year_month_weekday_last::operator-=(const years& y) NOEXCEPT { *this = *this - y; return *this; } CONSTCD11 inline year year_month_weekday_last::year() const NOEXCEPT {return y_;} CONSTCD11 inline month year_month_weekday_last::month() const NOEXCEPT {return m_;} CONSTCD11 inline weekday year_month_weekday_last::weekday() const NOEXCEPT { return wdl_.weekday(); } CONSTCD11 inline weekday_last year_month_weekday_last::weekday_last() const NOEXCEPT { return wdl_; } CONSTCD14 inline year_month_weekday_last::operator sys_days() const NOEXCEPT { return sys_days{to_days()}; } CONSTCD14 inline year_month_weekday_last::operator local_days() const NOEXCEPT { return local_days{to_days()}; } CONSTCD11 inline bool year_month_weekday_last::ok() const NOEXCEPT { return y_.ok() && m_.ok() && wdl_.ok(); } CONSTCD14 inline days year_month_weekday_last::to_days() const NOEXCEPT { auto const d = sys_days(y_/m_/last); return (d - (solar_hijri::weekday{d} - wdl_.weekday())).time_since_epoch(); } CONSTCD11 inline bool operator==(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT { return x.year() == y.year() && x.month() == y.month() && x.weekday_last() == y.weekday_last(); } CONSTCD11 inline bool operator!=(const year_month_weekday_last& x, const year_month_weekday_last& y) NOEXCEPT { return !(x == y); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const year_month_weekday_last& ymwdl) { return os << ymwdl.year() << '/' << ymwdl.month() << '/' << ymwdl.weekday_last(); } CONSTCD14 inline year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT { return (ymwdl.year() / ymwdl.month() + dm) / ymwdl.weekday_last(); } CONSTCD14 inline year_month_weekday_last operator+(const months& dm, const year_month_weekday_last& ymwdl) NOEXCEPT { return ymwdl + dm; } CONSTCD14 inline year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const months& dm) NOEXCEPT { return ymwdl + (-dm); } CONSTCD11 inline year_month_weekday_last operator+(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT { return {ymwdl.year()+dy, ymwdl.month(), ymwdl.weekday_last()}; } CONSTCD11 inline year_month_weekday_last operator+(const years& dy, const year_month_weekday_last& ymwdl) NOEXCEPT { return ymwdl + dy; } CONSTCD11 inline year_month_weekday_last operator-(const year_month_weekday_last& ymwdl, const years& dy) NOEXCEPT { return ymwdl + (-dy); } // year_month from operator/() CONSTCD11 inline year_month operator/(const year& y, const month& m) NOEXCEPT { return {y, m}; } CONSTCD11 inline year_month operator/(const year& y, int m) NOEXCEPT { return y / month(static_cast(m)); } // month_day from operator/() CONSTCD11 inline month_day operator/(const month& m, const day& d) NOEXCEPT { return {m, d}; } CONSTCD11 inline month_day operator/(const day& d, const month& m) NOEXCEPT { return m / d; } CONSTCD11 inline month_day operator/(const month& m, int d) NOEXCEPT { return m / day(static_cast(d)); } CONSTCD11 inline month_day operator/(int m, const day& d) NOEXCEPT { return month(static_cast(m)) / d; } CONSTCD11 inline month_day operator/(const day& d, int m) NOEXCEPT {return m / d;} // month_day_last from operator/() CONSTCD11 inline month_day_last operator/(const month& m, last_spec) NOEXCEPT { return month_day_last{m}; } CONSTCD11 inline month_day_last operator/(last_spec, const month& m) NOEXCEPT { return m/last; } CONSTCD11 inline month_day_last operator/(int m, last_spec) NOEXCEPT { return month(static_cast(m))/last; } CONSTCD11 inline month_day_last operator/(last_spec, int m) NOEXCEPT { return m/last; } // month_weekday from operator/() CONSTCD11 inline month_weekday operator/(const month& m, const weekday_indexed& wdi) NOEXCEPT { return {m, wdi}; } CONSTCD11 inline month_weekday operator/(const weekday_indexed& wdi, const month& m) NOEXCEPT { return m / wdi; } CONSTCD11 inline month_weekday operator/(int m, const weekday_indexed& wdi) NOEXCEPT { return month(static_cast(m)) / wdi; } CONSTCD11 inline month_weekday operator/(const weekday_indexed& wdi, int m) NOEXCEPT { return m / wdi; } // month_weekday_last from operator/() CONSTCD11 inline month_weekday_last operator/(const month& m, const weekday_last& wdl) NOEXCEPT { return {m, wdl}; } CONSTCD11 inline month_weekday_last operator/(const weekday_last& wdl, const month& m) NOEXCEPT { return m / wdl; } CONSTCD11 inline month_weekday_last operator/(int m, const weekday_last& wdl) NOEXCEPT { return month(static_cast(m)) / wdl; } CONSTCD11 inline month_weekday_last operator/(const weekday_last& wdl, int m) NOEXCEPT { return m / wdl; } // year_month_day from operator/() CONSTCD11 inline year_month_day operator/(const year_month& ym, const day& d) NOEXCEPT { return {ym.year(), ym.month(), d}; } CONSTCD11 inline year_month_day operator/(const year_month& ym, int d) NOEXCEPT { return ym / day(static_cast(d)); } CONSTCD11 inline year_month_day operator/(const year& y, const month_day& md) NOEXCEPT { return y / md.month() / md.day(); } CONSTCD11 inline year_month_day operator/(int y, const month_day& md) NOEXCEPT { return year(y) / md; } CONSTCD11 inline year_month_day operator/(const month_day& md, const year& y) NOEXCEPT { return y / md; } CONSTCD11 inline year_month_day operator/(const month_day& md, int y) NOEXCEPT { return year(y) / md; } // year_month_day_last from operator/() CONSTCD11 inline year_month_day_last operator/(const year_month& ym, last_spec) NOEXCEPT { return {ym.year(), month_day_last{ym.month()}}; } CONSTCD11 inline year_month_day_last operator/(const year& y, const month_day_last& mdl) NOEXCEPT { return {y, mdl}; } CONSTCD11 inline year_month_day_last operator/(int y, const month_day_last& mdl) NOEXCEPT { return year(y) / mdl; } CONSTCD11 inline year_month_day_last operator/(const month_day_last& mdl, const year& y) NOEXCEPT { return y / mdl; } CONSTCD11 inline year_month_day_last operator/(const month_day_last& mdl, int y) NOEXCEPT { return year(y) / mdl; } // year_month_weekday from operator/() CONSTCD11 inline year_month_weekday operator/(const year_month& ym, const weekday_indexed& wdi) NOEXCEPT { return {ym.year(), ym.month(), wdi}; } CONSTCD11 inline year_month_weekday operator/(const year& y, const month_weekday& mwd) NOEXCEPT { return {y, mwd.month(), mwd.weekday_indexed()}; } CONSTCD11 inline year_month_weekday operator/(int y, const month_weekday& mwd) NOEXCEPT { return year(y) / mwd; } CONSTCD11 inline year_month_weekday operator/(const month_weekday& mwd, const year& y) NOEXCEPT { return y / mwd; } CONSTCD11 inline year_month_weekday operator/(const month_weekday& mwd, int y) NOEXCEPT { return year(y) / mwd; } // year_month_weekday_last from operator/() CONSTCD11 inline year_month_weekday_last operator/(const year_month& ym, const weekday_last& wdl) NOEXCEPT { return {ym.year(), ym.month(), wdl}; } CONSTCD11 inline year_month_weekday_last operator/(const year& y, const month_weekday_last& mwdl) NOEXCEPT { return {y, mwdl.month(), mwdl.weekday_last()}; } CONSTCD11 inline year_month_weekday_last operator/(int y, const month_weekday_last& mwdl) NOEXCEPT { return year(y) / mwdl; } CONSTCD11 inline year_month_weekday_last operator/(const month_weekday_last& mwdl, const year& y) NOEXCEPT { return y / mwdl; } CONSTCD11 inline year_month_weekday_last operator/(const month_weekday_last& mwdl, int y) NOEXCEPT { return year(y) / mwdl; } } // namespace solar_hijri #endif // SOLAR_HIJRI_H pr0m1th3as-datatypes-9450b29/src/date/tz.h000066400000000000000000002464611527370255000202250ustar00rootroot00000000000000#ifndef TZ_H #define TZ_H // The MIT License (MIT) // // Copyright (c) 2015, 2016, 2017 Howard Hinnant // Copyright (c) 2017 Jiangang Zhuang // Copyright (c) 2017 Aaron Bishop // Copyright (c) 2017 Tomasz KamiÅ„ski // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // Our apologies. When the previous paragraph was written, lowercase had not yet // been invented (that would involve another several millennia of evolution). // We did not mean to shout. // Get more recent database at http://www.iana.org/time-zones // The notion of "current timezone" is something the operating system is expected to "just // know". How it knows this is system specific. It's often a value set by the user at OS // installation time and recorded by the OS somewhere. On Linux and Mac systems the current // timezone name is obtained by looking at the name or contents of a particular file on // disk. On Windows the current timezone name comes from the registry. In either method, // there is no guarantee that the "native" current timezone name obtained will match any // of the "Standard" names in this library's "database". On Linux, the names usually do // seem to match so mapping functions to map from native to "Standard" are typically not // required. On Windows, the names are never "Standard" so mapping is always required. // Technically any OS may use the mapping process but currently only Windows does use it. #ifndef USE_OS_TZDB # define USE_OS_TZDB 0 #endif #ifndef HAS_REMOTE_API # if USE_OS_TZDB == 0 # if defined __ANDROID__ # define HAS_REMOTE_API 0 # else # define HAS_REMOTE_API 1 # endif # else // HAS_REMOTE_API makes no sense when using the OS timezone database # define HAS_REMOTE_API 0 # endif #endif #ifdef __clang__ # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wconstant-logical-operand" #endif static_assert(!(USE_OS_TZDB && HAS_REMOTE_API), "USE_OS_TZDB and HAS_REMOTE_API can not be used together"); #ifdef __clang__ # pragma clang diagnostic pop #endif #ifndef AUTO_DOWNLOAD # define AUTO_DOWNLOAD HAS_REMOTE_API #endif static_assert(HAS_REMOTE_API == 0 ? AUTO_DOWNLOAD == 0 : true, "AUTO_DOWNLOAD can not be turned on without HAS_REMOTE_API"); #ifndef USE_SHELL_API # define USE_SHELL_API 1 #endif #if USE_OS_TZDB # ifdef _WIN32 # error "USE_OS_TZDB can not be used on Windows" # endif #endif #ifndef HAS_DEDUCTION_GUIDES # if __cplusplus >= 201703 # define HAS_DEDUCTION_GUIDES 1 # else # define HAS_DEDUCTION_GUIDES 0 # endif #endif // HAS_DEDUCTION_GUIDES #include "date.h" #if defined(_MSC_VER) && (_MSC_VER < 1900) #include "tz_private.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 # ifdef DATE_BUILD_DLL # define DATE_API __declspec(dllexport) # elif defined(DATE_USE_DLL) # define DATE_API __declspec(dllimport) # else # define DATE_API # endif #else # ifdef DATE_BUILD_DLL # define DATE_API __attribute__ ((visibility ("default"))) # else # define DATE_API # endif #endif namespace date { enum class choose {earliest, latest}; #if defined(BUILD_TZ_LIB) # if defined(ANDROID) || defined(__ANDROID__) struct tzdb; static std::unique_ptr init_tzdb(); # endif // defined(ANDROID) || defined(__ANDROID__) #endif // defined(BUILD_TZ_LIB) namespace detail { struct undocumented; template struct nodeduct { using type = T; }; template using nodeduct_t = typename nodeduct::type; } struct sys_info { sys_seconds begin; sys_seconds end; std::chrono::seconds offset; std::chrono::minutes save; std::string abbrev; }; template std::basic_ostream& operator<<(std::basic_ostream& os, const sys_info& r) { os << r.begin << '\n'; os << r.end << '\n'; os << make_time(r.offset) << "\n"; os << make_time(r.save) << "\n"; os << r.abbrev << '\n'; return os; } struct local_info { enum {unique, nonexistent, ambiguous} result; sys_info first; sys_info second; }; template std::basic_ostream& operator<<(std::basic_ostream& os, const local_info& r) { if (r.result == local_info::nonexistent) os << "nonexistent between\n"; else if (r.result == local_info::ambiguous) os << "ambiguous between\n"; os << r.first; if (r.result != local_info::unique) { os << "and\n"; os << r.second; } return os; } class nonexistent_local_time : public std::runtime_error { public: template nonexistent_local_time(local_time tp, const local_info& i); private: template static std::string make_msg(local_time tp, const local_info& i); }; template inline nonexistent_local_time::nonexistent_local_time(local_time tp, const local_info& i) : std::runtime_error(make_msg(tp, i)) { } template std::string nonexistent_local_time::make_msg(local_time tp, const local_info& i) { assert(i.result == local_info::nonexistent); std::ostringstream os; os << tp << " is in a gap between\n" << local_seconds{i.first.end.time_since_epoch()} + i.first.offset << ' ' << i.first.abbrev << " and\n" << local_seconds{i.second.begin.time_since_epoch()} + i.second.offset << ' ' << i.second.abbrev << " which are both equivalent to\n"; date::operator<<(os, i.first.end) << " UTC"; return os.str(); } class ambiguous_local_time : public std::runtime_error { public: template ambiguous_local_time(local_time tp, const local_info& i); private: template static std::string make_msg(local_time tp, const local_info& i); }; template inline ambiguous_local_time::ambiguous_local_time(local_time tp, const local_info& i) : std::runtime_error(make_msg(tp, i)) { } template std::string ambiguous_local_time::make_msg(local_time tp, const local_info& i) { assert(i.result == local_info::ambiguous); std::ostringstream os; os << tp << " is ambiguous. It could be\n" << tp << ' ' << i.first.abbrev << " == " << tp - i.first.offset << " UTC or\n" << tp << ' ' << i.second.abbrev << " == " << tp - i.second.offset << " UTC"; return os.str(); } class time_zone; #if HAS_STRING_VIEW DATE_API const time_zone* locate_zone(std::string_view tz_name); #else DATE_API const time_zone* locate_zone(const std::string& tz_name); #endif DATE_API const time_zone* current_zone(); template struct zoned_traits { }; template <> struct zoned_traits { static const time_zone* default_zone() { return date::locate_zone("Etc/UTC"); } #if HAS_STRING_VIEW static const time_zone* locate_zone(std::string_view name) { return date::locate_zone(name); } #else // !HAS_STRING_VIEW static const time_zone* locate_zone(const std::string& name) { return date::locate_zone(name); } static const time_zone* locate_zone(const char* name) { return date::locate_zone(name); } #endif // !HAS_STRING_VIEW }; template class zoned_time; template bool operator==(const zoned_time& x, const zoned_time& y); template class zoned_time { public: using duration = typename std::common_type::type; private: TimeZonePtr zone_; sys_time tp_; public: #if !defined(_MSC_VER) || (_MSC_VER > 1916) template ::default_zone())> #endif zoned_time(); #if !defined(_MSC_VER) || (_MSC_VER > 1916) template ::default_zone())> #endif zoned_time(const sys_time& st); explicit zoned_time(TimeZonePtr z); #if HAS_STRING_VIEW template ::locate_zone(std::string_view())) >::value >::type> explicit zoned_time(std::string_view name); #else # if !defined(_MSC_VER) || (_MSC_VER > 1916) template ::locate_zone(std::string())) >::value >::type> # endif explicit zoned_time(const std::string& name); #endif template , sys_time>::value >::type> zoned_time(const zoned_time& zt) NOEXCEPT; zoned_time(TimeZonePtr z, const sys_time& st); #if !defined(_MSC_VER) || (_MSC_VER > 1916) template ()->to_sys(local_time{})), sys_time >::value >::type> #endif zoned_time(TimeZonePtr z, const local_time& tp); #if !defined(_MSC_VER) || (_MSC_VER > 1916) template ()->to_sys(local_time{}, choose::earliest)), sys_time >::value >::type> #endif zoned_time(TimeZonePtr z, const local_time& tp, choose c); template , sys_time>::value >::type> zoned_time(TimeZonePtr z, const zoned_time& zt); template , sys_time>::value >::type> zoned_time(TimeZonePtr z, const zoned_time& zt, choose); #if HAS_STRING_VIEW template ::locate_zone(std::string_view())), sys_time >::value >::type> zoned_time(std::string_view name, detail::nodeduct_t&> st); template ::locate_zone(std::string_view())), local_time >::value >::type> zoned_time(std::string_view name, detail::nodeduct_t&> tp); template ::locate_zone(std::string_view())), local_time, choose >::value >::type> zoned_time(std::string_view name, detail::nodeduct_t&> tp, choose c); template , sys_time>::value && std::is_constructible < zoned_time, decltype(zoned_traits::locate_zone(std::string_view())), zoned_time >::value >::type> zoned_time(std::string_view name, const zoned_time& zt); template , sys_time>::value && std::is_constructible < zoned_time, decltype(zoned_traits::locate_zone(std::string_view())), zoned_time, choose >::value >::type> zoned_time(std::string_view name, const zoned_time& zt, choose); #else // !HAS_STRING_VIEW #if !defined(_MSC_VER) || (_MSC_VER > 1916) template ::locate_zone(std::string())), sys_time >::value >::type> #endif zoned_time(const std::string& name, const sys_time& st); #if !defined(_MSC_VER) || (_MSC_VER > 1916) template ::locate_zone(std::string())), sys_time >::value >::type> #endif zoned_time(const char* name, const sys_time& st); #if !defined(_MSC_VER) || (_MSC_VER > 1916) template ::locate_zone(std::string())), local_time >::value >::type> #endif zoned_time(const std::string& name, const local_time& tp); #if !defined(_MSC_VER) || (_MSC_VER > 1916) template ::locate_zone(std::string())), local_time >::value >::type> #endif zoned_time(const char* name, const local_time& tp); #if !defined(_MSC_VER) || (_MSC_VER > 1916) template ::locate_zone(std::string())), local_time, choose >::value >::type> #endif zoned_time(const std::string& name, const local_time& tp, choose c); #if !defined(_MSC_VER) || (_MSC_VER > 1916) template ::locate_zone(std::string())), local_time, choose >::value >::type> #endif zoned_time(const char* name, const local_time& tp, choose c); #if !defined(_MSC_VER) || (_MSC_VER > 1916) template , sys_time>::value && std::is_constructible < zoned_time, decltype(zoned_traits::locate_zone(std::string())), zoned_time >::value >::type> #else template #endif zoned_time(const std::string& name, const zoned_time& zt); #if !defined(_MSC_VER) || (_MSC_VER > 1916) template , sys_time>::value && std::is_constructible < zoned_time, decltype(zoned_traits::locate_zone(std::string())), zoned_time >::value >::type> #else template #endif zoned_time(const char* name, const zoned_time& zt); #if !defined(_MSC_VER) || (_MSC_VER > 1916) template , sys_time>::value && std::is_constructible < zoned_time, decltype(zoned_traits::locate_zone(std::string())), zoned_time, choose >::value >::type> #else template #endif zoned_time(const std::string& name, const zoned_time& zt, choose); #if !defined(_MSC_VER) || (_MSC_VER > 1916) template , sys_time>::value && std::is_constructible < zoned_time, decltype(zoned_traits::locate_zone(std::string())), zoned_time, choose >::value >::type> #else template #endif zoned_time(const char* name, const zoned_time& zt, choose); #endif // !HAS_STRING_VIEW zoned_time& operator=(const sys_time& st); zoned_time& operator=(const local_time& ut); explicit operator sys_time() const; explicit operator local_time() const; TimeZonePtr get_time_zone() const; local_time get_local_time() const; sys_time get_sys_time() const; sys_info get_info() const; template friend bool operator==(const zoned_time& x, const zoned_time& y); template friend std::basic_ostream& operator<<(std::basic_ostream& os, const zoned_time& t); private: template friend class zoned_time; template static TimeZonePtr2&& check(TimeZonePtr2&& p); }; using zoned_seconds = zoned_time; #if HAS_DEDUCTION_GUIDES namespace detail { template using time_zone_representation = std::conditional_t < std::is_convertible::value, time_zone const*, std::remove_cv_t> >; } zoned_time() -> zoned_time; template zoned_time(sys_time) -> zoned_time>; template zoned_time(TimeZonePtrOrName&&) -> zoned_time>; template zoned_time(TimeZonePtrOrName&&, sys_time) -> zoned_time, detail::time_zone_representation>; template zoned_time(TimeZonePtrOrName&&, local_time, choose = choose::earliest) -> zoned_time, detail::time_zone_representation>; template zoned_time(TimeZonePtrOrName&&, zoned_time, choose = choose::earliest) -> zoned_time, detail::time_zone_representation>; #endif // HAS_DEDUCTION_GUIDES template inline bool operator==(const zoned_time& x, const zoned_time& y) { return x.zone_ == y.zone_ && x.tp_ == y.tp_; } template inline bool operator!=(const zoned_time& x, const zoned_time& y) { return !(x == y); } #if !defined(_MSC_VER) || (_MSC_VER >= 1900) namespace detail { # if USE_OS_TZDB struct transition; struct expanded_ttinfo; # else // !USE_OS_TZDB struct zonelet; class Rule; # endif // !USE_OS_TZDB } #endif // !defined(_MSC_VER) || (_MSC_VER >= 1900) class time_zone { private: std::string name_; #if USE_OS_TZDB std::vector transitions_; std::vector ttinfos_; #else // !USE_OS_TZDB std::vector zonelets_; #endif // !USE_OS_TZDB std::unique_ptr adjusted_; public: #if !defined(_MSC_VER) || (_MSC_VER >= 1900) time_zone(time_zone&&) = default; time_zone& operator=(time_zone&&) = default; #else // defined(_MSC_VER) && (_MSC_VER < 1900) time_zone(time_zone&& src); time_zone& operator=(time_zone&& src); #endif // defined(_MSC_VER) && (_MSC_VER < 1900) DATE_API explicit time_zone(const std::string& s, detail::undocumented); const std::string& name() const NOEXCEPT; template sys_info get_info(sys_time st) const; template local_info get_info(local_time tp) const; template sys_time::type> to_sys(local_time tp) const; template sys_time::type> to_sys(local_time tp, choose z) const; template local_time::type> to_local(sys_time tp) const; friend bool operator==(const time_zone& x, const time_zone& y) NOEXCEPT; friend bool operator< (const time_zone& x, const time_zone& y) NOEXCEPT; friend DATE_API std::ostream& operator<<(std::ostream& os, const time_zone& z); #if !USE_OS_TZDB DATE_API void add(const std::string& s); #else # if defined(BUILD_TZ_LIB) # if defined(ANDROID) || defined(__ANDROID__) friend std::unique_ptr init_tzdb(); # endif // defined(ANDROID) || defined(__ANDROID__) # endif // defined(BUILD_TZ_LIB) #endif // !USE_OS_TZDB private: DATE_API sys_info get_info_impl(sys_seconds tp) const; DATE_API local_info get_info_impl(local_seconds tp) const; template sys_time::type> to_sys_impl(local_time tp, choose z, std::false_type) const; template sys_time::type> to_sys_impl(local_time tp, choose, std::true_type) const; #if USE_OS_TZDB DATE_API void init() const; DATE_API void init_impl(); DATE_API sys_info load_sys_info(std::vector::const_iterator i) const; template DATE_API void load_data(std::istream& inf, std::int32_t tzh_leapcnt, std::int32_t tzh_timecnt, std::int32_t tzh_typecnt, std::int32_t tzh_charcnt); # if defined(ANDROID) || defined(__ANDROID__) void parse_from_android_tzdata(std::ifstream& inf, const std::size_t off); # endif // defined(ANDROID) || defined(__ANDROID__) #else // !USE_OS_TZDB DATE_API sys_info get_info_impl(sys_seconds tp, int tz_int) const; DATE_API void adjust_infos(const std::vector& rules); DATE_API void parse_info(std::istream& in); #endif // !USE_OS_TZDB }; #if defined(_MSC_VER) && (_MSC_VER < 1900) inline time_zone::time_zone(time_zone&& src) : name_(std::move(src.name_)) , zonelets_(std::move(src.zonelets_)) , adjusted_(std::move(src.adjusted_)) {} inline time_zone& time_zone::operator=(time_zone&& src) { name_ = std::move(src.name_); zonelets_ = std::move(src.zonelets_); adjusted_ = std::move(src.adjusted_); return *this; } #endif // defined(_MSC_VER) && (_MSC_VER < 1900) inline const std::string& time_zone::name() const NOEXCEPT { return name_; } template inline sys_info time_zone::get_info(sys_time st) const { return get_info_impl(date::floor(st)); } template inline local_info time_zone::get_info(local_time tp) const { return get_info_impl(date::floor(tp)); } template inline sys_time::type> time_zone::to_sys(local_time tp) const { return to_sys_impl(tp, choose{}, std::true_type{}); } template inline sys_time::type> time_zone::to_sys(local_time tp, choose z) const { return to_sys_impl(tp, z, std::false_type{}); } template inline local_time::type> time_zone::to_local(sys_time tp) const { using LT = local_time::type>; auto i = get_info(tp); return LT{(tp + i.offset).time_since_epoch()}; } inline bool operator==(const time_zone& x, const time_zone& y) NOEXCEPT {return x.name_ == y.name_;} inline bool operator< (const time_zone& x, const time_zone& y) NOEXCEPT {return x.name_ < y.name_;} inline bool operator!=(const time_zone& x, const time_zone& y) NOEXCEPT {return !(x == y);} inline bool operator> (const time_zone& x, const time_zone& y) NOEXCEPT {return y < x;} inline bool operator<=(const time_zone& x, const time_zone& y) NOEXCEPT {return !(y < x);} inline bool operator>=(const time_zone& x, const time_zone& y) NOEXCEPT {return !(x < y);} template sys_time::type> time_zone::to_sys_impl(local_time tp, choose z, std::false_type) const { auto i = get_info(tp); if (i.result == local_info::nonexistent) { return i.first.end; } else if (i.result == local_info::ambiguous) { if (z == choose::latest) return sys_time{tp.time_since_epoch()} - i.second.offset; } return sys_time{tp.time_since_epoch()} - i.first.offset; } template sys_time::type> time_zone::to_sys_impl(local_time tp, choose, std::true_type) const { auto i = get_info(tp); if (i.result == local_info::nonexistent) throw nonexistent_local_time(tp, i); else if (i.result == local_info::ambiguous) throw ambiguous_local_time(tp, i); return sys_time{tp.time_since_epoch()} - i.first.offset; } #if !USE_OS_TZDB class time_zone_link { private: std::string name_; std::string target_; public: DATE_API explicit time_zone_link(const std::string& s); const std::string& name() const {return name_;} const std::string& target() const {return target_;} friend bool operator==(const time_zone_link& x, const time_zone_link& y) {return x.name_ == y.name_;} friend bool operator< (const time_zone_link& x, const time_zone_link& y) {return x.name_ < y.name_;} friend DATE_API std::ostream& operator<<(std::ostream& os, const time_zone_link& x); }; using link = time_zone_link; inline bool operator!=(const time_zone_link& x, const time_zone_link& y) {return !(x == y);} inline bool operator> (const time_zone_link& x, const time_zone_link& y) {return y < x;} inline bool operator<=(const time_zone_link& x, const time_zone_link& y) {return !(y < x);} inline bool operator>=(const time_zone_link& x, const time_zone_link& y) {return !(x < y);} #endif // !USE_OS_TZDB class leap_second { private: sys_seconds date_; public: #if USE_OS_TZDB DATE_API explicit leap_second(const sys_seconds& s, detail::undocumented); #else DATE_API explicit leap_second(const std::string& s, detail::undocumented); #endif sys_seconds date() const {return date_;} friend bool operator==(const leap_second& x, const leap_second& y) {return x.date_ == y.date_;} friend bool operator< (const leap_second& x, const leap_second& y) {return x.date_ < y.date_;} template friend bool operator==(const leap_second& x, const sys_time& y) { return x.date_ == y; } template friend bool operator< (const leap_second& x, const sys_time& y) { return x.date_ < y; } template friend bool operator< (const sys_time& x, const leap_second& y) { return x < y.date_; } friend DATE_API std::ostream& operator<<(std::ostream& os, const leap_second& x); }; inline bool operator!=(const leap_second& x, const leap_second& y) {return !(x == y);} inline bool operator> (const leap_second& x, const leap_second& y) {return y < x;} inline bool operator<=(const leap_second& x, const leap_second& y) {return !(y < x);} inline bool operator>=(const leap_second& x, const leap_second& y) {return !(x < y);} template inline bool operator==(const sys_time& x, const leap_second& y) { return y == x; } template inline bool operator!=(const leap_second& x, const sys_time& y) { return !(x == y); } template inline bool operator!=(const sys_time& x, const leap_second& y) { return !(x == y); } template inline bool operator> (const leap_second& x, const sys_time& y) { return y < x; } template inline bool operator> (const sys_time& x, const leap_second& y) { return y < x; } template inline bool operator<=(const leap_second& x, const sys_time& y) { return !(y < x); } template inline bool operator<=(const sys_time& x, const leap_second& y) { return !(y < x); } template inline bool operator>=(const leap_second& x, const sys_time& y) { return !(x < y); } template inline bool operator>=(const sys_time& x, const leap_second& y) { return !(x < y); } using leap = leap_second; #ifdef _WIN32 namespace detail { // The time zone mapping is modelled after this data file: // http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml // and the field names match the element names from the mapZone element // of windowsZones.xml. // The website displays this file here: // http://www.unicode.org/cldr/charts/latest/supplemental/zone_tzid.html // The html view is sorted before being displayed but is otherwise the same // There is a mapping between the os centric view (in this case windows) // the html displays uses and the generic view the xml file. // That mapping is this: // display column "windows" -> xml field "other". // display column "region" -> xml field "territory". // display column "tzid" -> xml field "type". // This structure uses the generic terminology because it could be // used to to support other os/native name conversions, not just windows, // and using the same generic names helps retain the connection to the // origin of the data that we are using. struct timezone_mapping { timezone_mapping(const char* other, const char* territory, const char* type) : other(other), territory(territory), type(type) { } timezone_mapping() = default; std::string other; std::string territory; std::string type; }; } // detail #endif // _WIN32 struct tzdb { std::string version = "unknown"; std::vector zones; #if !USE_OS_TZDB std::vector links; #endif std::vector leap_seconds; #if !USE_OS_TZDB std::vector rules; #endif #ifdef _WIN32 std::vector mappings; #endif tzdb* next = nullptr; tzdb() = default; #if !defined(_MSC_VER) || (_MSC_VER >= 1900) tzdb(tzdb&&) = default; tzdb& operator=(tzdb&&) = default; #else // defined(_MSC_VER) && (_MSC_VER < 1900) tzdb(tzdb&& src) : version(std::move(src.version)) , zones(std::move(src.zones)) , links(std::move(src.links)) , leap_seconds(std::move(src.leap_seconds)) , rules(std::move(src.rules)) , mappings(std::move(src.mappings)) {} tzdb& operator=(tzdb&& src) { version = std::move(src.version); zones = std::move(src.zones); links = std::move(src.links); leap_seconds = std::move(src.leap_seconds); rules = std::move(src.rules); mappings = std::move(src.mappings); return *this; } #endif // defined(_MSC_VER) && (_MSC_VER < 1900) #if HAS_STRING_VIEW DATE_API const time_zone* locate_zone(std::string_view tz_name) const; #else DATE_API const time_zone* locate_zone(const std::string& tz_name) const; #endif DATE_API const time_zone* current_zone() const; }; using TZ_DB = tzdb; DATE_API std::ostream& operator<<(std::ostream& os, const tzdb& db); DATE_API const tzdb& get_tzdb(); class tzdb_list { std::atomic head_{nullptr}; public: DATE_API ~tzdb_list(); tzdb_list() = default; DATE_API tzdb_list(tzdb_list&& x) NOEXCEPT; const tzdb& front() const NOEXCEPT {return *head_;} tzdb& front() NOEXCEPT {return *head_;} class const_iterator; const_iterator begin() const NOEXCEPT; const_iterator end() const NOEXCEPT; const_iterator cbegin() const NOEXCEPT; const_iterator cend() const NOEXCEPT; DATE_API const_iterator erase_after(const_iterator p) NOEXCEPT; struct undocumented_helper; private: void push_front(tzdb* tzdb) NOEXCEPT; }; class tzdb_list::const_iterator { tzdb* p_ = nullptr; explicit const_iterator(tzdb* p) NOEXCEPT : p_{p} {} public: const_iterator() = default; using iterator_category = std::forward_iterator_tag; using value_type = tzdb; using reference = const value_type&; using pointer = const value_type*; using difference_type = std::ptrdiff_t; reference operator*() const NOEXCEPT {return *p_;} pointer operator->() const NOEXCEPT {return p_;} const_iterator& operator++() NOEXCEPT {p_ = p_->next; return *this;} const_iterator operator++(int) NOEXCEPT {auto t = *this; ++(*this); return t;} friend bool operator==(const const_iterator& x, const const_iterator& y) NOEXCEPT {return x.p_ == y.p_;} friend bool operator!=(const const_iterator& x, const const_iterator& y) NOEXCEPT {return !(x == y);} friend class tzdb_list; }; inline tzdb_list::const_iterator tzdb_list::begin() const NOEXCEPT { return const_iterator{head_}; } inline tzdb_list::const_iterator tzdb_list::end() const NOEXCEPT { return const_iterator{nullptr}; } inline tzdb_list::const_iterator tzdb_list::cbegin() const NOEXCEPT { return begin(); } inline tzdb_list::const_iterator tzdb_list::cend() const NOEXCEPT { return end(); } DATE_API tzdb_list& get_tzdb_list(); #if !USE_OS_TZDB DATE_API const tzdb& reload_tzdb(); DATE_API void set_install(const std::string& install); #endif // !USE_OS_TZDB #if HAS_REMOTE_API DATE_API std::string remote_version(); // if provided error_buffer size should be at least CURL_ERROR_SIZE DATE_API bool remote_download(const std::string& version, char* error_buffer = nullptr); DATE_API bool remote_install(const std::string& version); #endif // zoned_time namespace detail { template inline T* to_raw_pointer(T* p) NOEXCEPT { return p; } template inline auto to_raw_pointer(Pointer p) NOEXCEPT -> decltype(detail::to_raw_pointer(p.operator->())) { return detail::to_raw_pointer(p.operator->()); } } // namespace detail template template inline TimeZonePtr2&& zoned_time::check(TimeZonePtr2&& p) { if (detail::to_raw_pointer(p) == nullptr) throw std::runtime_error( "zoned_time constructed with a time zone pointer == nullptr"); return std::forward(p); } template #if !defined(_MSC_VER) || (_MSC_VER > 1916) template #endif inline zoned_time::zoned_time() : zone_(check(zoned_traits::default_zone())) {} template #if !defined(_MSC_VER) || (_MSC_VER > 1916) template #endif inline zoned_time::zoned_time(const sys_time& st) : zone_(check(zoned_traits::default_zone())) , tp_(st) {} template inline zoned_time::zoned_time(TimeZonePtr z) : zone_(check(std::move(z))) {} #if HAS_STRING_VIEW template template inline zoned_time::zoned_time(std::string_view name) : zoned_time(zoned_traits::locate_zone(name)) {} #else // !HAS_STRING_VIEW template #if !defined(_MSC_VER) || (_MSC_VER > 1916) template #endif inline zoned_time::zoned_time(const std::string& name) : zoned_time(zoned_traits::locate_zone(name)) {} #endif // !HAS_STRING_VIEW template template inline zoned_time::zoned_time(const zoned_time& zt) NOEXCEPT : zone_(zt.zone_) , tp_(zt.tp_) {} template inline zoned_time::zoned_time(TimeZonePtr z, const sys_time& st) : zone_(check(std::move(z))) , tp_(st) {} template #if !defined(_MSC_VER) || (_MSC_VER > 1916) template #endif inline zoned_time::zoned_time(TimeZonePtr z, const local_time& t) : zone_(check(std::move(z))) , tp_(zone_->to_sys(t)) {} template #if !defined(_MSC_VER) || (_MSC_VER > 1916) template #endif inline zoned_time::zoned_time(TimeZonePtr z, const local_time& t, choose c) : zone_(check(std::move(z))) , tp_(zone_->to_sys(t, c)) {} template template inline zoned_time::zoned_time(TimeZonePtr z, const zoned_time& zt) : zone_(check(std::move(z))) , tp_(zt.tp_) {} template template inline zoned_time::zoned_time(TimeZonePtr z, const zoned_time& zt, choose) : zoned_time(std::move(z), zt) {} #if HAS_STRING_VIEW template template inline zoned_time::zoned_time(std::string_view name, detail::nodeduct_t&> st) : zoned_time(zoned_traits::locate_zone(name), st) {} template template inline zoned_time::zoned_time(std::string_view name, detail::nodeduct_t&> t) : zoned_time(zoned_traits::locate_zone(name), t) {} template template inline zoned_time::zoned_time(std::string_view name, detail::nodeduct_t&> t, choose c) : zoned_time(zoned_traits::locate_zone(name), t, c) {} template template inline zoned_time::zoned_time(std::string_view name, const zoned_time& zt) : zoned_time(zoned_traits::locate_zone(name), zt) {} template template inline zoned_time::zoned_time(std::string_view name, const zoned_time& zt, choose c) : zoned_time(zoned_traits::locate_zone(name), zt, c) {} #else // !HAS_STRING_VIEW template #if !defined(_MSC_VER) || (_MSC_VER > 1916) template #endif inline zoned_time::zoned_time(const std::string& name, const sys_time& st) : zoned_time(zoned_traits::locate_zone(name), st) {} template #if !defined(_MSC_VER) || (_MSC_VER > 1916) template #endif inline zoned_time::zoned_time(const char* name, const sys_time& st) : zoned_time(zoned_traits::locate_zone(name), st) {} template #if !defined(_MSC_VER) || (_MSC_VER > 1916) template #endif inline zoned_time::zoned_time(const std::string& name, const local_time& t) : zoned_time(zoned_traits::locate_zone(name), t) {} template #if !defined(_MSC_VER) || (_MSC_VER > 1916) template #endif inline zoned_time::zoned_time(const char* name, const local_time& t) : zoned_time(zoned_traits::locate_zone(name), t) {} template #if !defined(_MSC_VER) || (_MSC_VER > 1916) template #endif inline zoned_time::zoned_time(const std::string& name, const local_time& t, choose c) : zoned_time(zoned_traits::locate_zone(name), t, c) {} template #if !defined(_MSC_VER) || (_MSC_VER > 1916) template #endif inline zoned_time::zoned_time(const char* name, const local_time& t, choose c) : zoned_time(zoned_traits::locate_zone(name), t, c) {} template #if !defined(_MSC_VER) || (_MSC_VER > 1916) template #else template #endif inline zoned_time::zoned_time(const std::string& name, const zoned_time& zt) : zoned_time(zoned_traits::locate_zone(name), zt) {} template #if !defined(_MSC_VER) || (_MSC_VER > 1916) template #else template #endif inline zoned_time::zoned_time(const char* name, const zoned_time& zt) : zoned_time(zoned_traits::locate_zone(name), zt) {} template #if !defined(_MSC_VER) || (_MSC_VER > 1916) template #else template #endif inline zoned_time::zoned_time(const std::string& name, const zoned_time& zt, choose c) : zoned_time(zoned_traits::locate_zone(name), zt, c) {} template #if !defined(_MSC_VER) || (_MSC_VER > 1916) template #else template #endif inline zoned_time::zoned_time(const char* name, const zoned_time& zt, choose c) : zoned_time(zoned_traits::locate_zone(name), zt, c) {} #endif // HAS_STRING_VIEW template inline zoned_time& zoned_time::operator=(const sys_time& st) { tp_ = st; return *this; } template inline zoned_time& zoned_time::operator=(const local_time& ut) { tp_ = zone_->to_sys(ut); return *this; } template inline zoned_time::operator local_time::duration>() const { return get_local_time(); } template inline zoned_time::operator sys_time::duration>() const { return get_sys_time(); } template inline TimeZonePtr zoned_time::get_time_zone() const { return zone_; } template inline local_time::duration> zoned_time::get_local_time() const { return zone_->to_local(tp_); } template inline sys_time::duration> zoned_time::get_sys_time() const { return tp_; } template inline sys_info zoned_time::get_info() const { return zone_->get_info(tp_); } // make_zoned_time inline zoned_time make_zoned() { return zoned_time(); } template inline zoned_time::type> make_zoned(const sys_time& tp) { return zoned_time::type>(tp); } template 1916) #if !defined(__INTEL_COMPILER) || (__INTEL_COMPILER > 1600) , class = typename std::enable_if < std::is_class < typename std::decay < decltype(*detail::to_raw_pointer(std::declval())) >::type >{} >::type #endif #endif > inline zoned_time make_zoned(TimeZonePtr z) { return zoned_time(std::move(z)); } inline zoned_seconds make_zoned(const std::string& name) { return zoned_seconds(name); } template 1916) #if !defined(__INTEL_COMPILER) || (__INTEL_COMPILER > 1600) , class = typename std::enable_if < std::is_class())>::type>{} >::type #endif #endif > inline zoned_time::type, TimeZonePtr> make_zoned(TimeZonePtr zone, const local_time& tp) { return zoned_time::type, TimeZonePtr>(std::move(zone), tp); } template 1916) #if !defined(__INTEL_COMPILER) || (__INTEL_COMPILER > 1600) , class = typename std::enable_if < std::is_class())>::type>{} >::type #endif #endif > inline zoned_time::type, TimeZonePtr> make_zoned(TimeZonePtr zone, const local_time& tp, choose c) { return zoned_time::type, TimeZonePtr>(std::move(zone), tp, c); } template inline zoned_time::type> make_zoned(const std::string& name, const local_time& tp) { return zoned_time::type>(name, tp); } template inline zoned_time::type> make_zoned(const std::string& name, const local_time& tp, choose c) { return zoned_time::type>(name, tp, c); } template inline zoned_time make_zoned(TimeZonePtr zone, const zoned_time& zt) { return zoned_time(std::move(zone), zt); } template inline zoned_time make_zoned(const std::string& name, const zoned_time& zt) { return zoned_time(name, zt); } template inline zoned_time make_zoned(TimeZonePtr zone, const zoned_time& zt, choose c) { return zoned_time(std::move(zone), zt, c); } template inline zoned_time make_zoned(const std::string& name, const zoned_time& zt, choose c) { return zoned_time(name, zt, c); } template 1916) #if !defined(__INTEL_COMPILER) || (__INTEL_COMPILER > 1600) , class = typename std::enable_if < std::is_class())>::type>{} >::type #endif #endif > inline zoned_time::type, TimeZonePtr> make_zoned(TimeZonePtr zone, const sys_time& st) { return zoned_time::type, TimeZonePtr>(std::move(zone), st); } template inline zoned_time::type> make_zoned(const std::string& name, const sys_time& st) { return zoned_time::type>(name, st); } template std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const zoned_time& tp) { using duration = typename zoned_time::duration; using LT = local_time; auto const st = tp.get_sys_time(); auto const info = tp.get_time_zone()->get_info(st); return to_stream(os, fmt, LT{(st+info.offset).time_since_epoch()}, &info.abbrev, &info.offset); } template inline std::basic_ostream& operator<<(std::basic_ostream& os, const zoned_time& t) { const CharT fmt[] = {'%', 'F', ' ', '%', 'T', ' ', '%', 'Z', CharT{}}; return to_stream(os, fmt, t); } class utc_clock { public: using duration = std::chrono::system_clock::duration; using rep = duration::rep; using period = duration::period; using time_point = std::chrono::time_point; static CONSTDATA bool is_steady = false; static time_point now(); template static std::chrono::time_point::type> to_sys(const std::chrono::time_point&); template static std::chrono::time_point::type> from_sys(const std::chrono::time_point&); template static std::chrono::time_point::type> to_local(const std::chrono::time_point&); template static std::chrono::time_point::type> from_local(const std::chrono::time_point&); }; template using utc_time = std::chrono::time_point; using utc_seconds = utc_time; template utc_time::type> utc_clock::from_sys(const sys_time& st) { using std::chrono::seconds; using CD = typename std::common_type::type; auto const& leaps = get_tzdb().leap_seconds; auto const lt = std::upper_bound(leaps.begin(), leaps.end(), st); return utc_time{st.time_since_epoch() + seconds{lt-leaps.begin()}}; } // Return pair // first is true if ut is during a leap second insertion, otherwise false. // If ut is during a leap second insertion, that leap second is included in the count template std::pair is_leap_second(date::utc_time const& ut) { using std::chrono::seconds; using duration = typename std::common_type::type; auto const& leaps = get_tzdb().leap_seconds; auto tp = sys_time{ut.time_since_epoch()}; auto const lt = std::upper_bound(leaps.begin(), leaps.end(), tp); auto ds = seconds{lt-leaps.begin()}; tp -= ds; auto ls = false; if (lt > leaps.begin()) { if (tp < lt[-1]) { if (tp >= lt[-1].date() - seconds{1}) ls = true; else --ds; } } return {ls, ds}; } struct leap_second_info { bool is_leap_second; std::chrono::seconds elapsed; }; template leap_second_info get_leap_second_info(date::utc_time const& ut) { auto p = is_leap_second(ut); return {p.first, p.second}; } template sys_time::type> utc_clock::to_sys(const utc_time& ut) { using std::chrono::seconds; using CD = typename std::common_type::type; auto ls = is_leap_second(ut); auto tp = sys_time{ut.time_since_epoch() - ls.second}; if (ls.first) tp = floor(tp) + seconds{1} - CD{1}; return tp; } inline utc_clock::time_point utc_clock::now() { return from_sys(std::chrono::system_clock::now()); } template utc_time::type> utc_clock::from_local(const local_time& st) { return from_sys(sys_time{st.time_since_epoch()}); } template local_time::type> utc_clock::to_local(const utc_time& ut) { using CD = typename std::common_type::type; return local_time{to_sys(ut).time_since_epoch()}; } template std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const utc_time& t) { using std::chrono::seconds; using CT = typename std::common_type::type; const std::string abbrev("UTC"); CONSTDATA seconds offset{0}; auto ls = is_leap_second(t); auto tp = sys_time{t.time_since_epoch() - ls.second}; auto const sd = floor(tp); year_month_day ymd = sd; auto time = make_time(tp - sys_seconds{sd}); time.seconds(detail::undocumented{}) += seconds{ls.first}; fields fds{ymd, time}; return to_stream(os, fmt, fds, &abbrev, &offset); } template std::basic_ostream& operator<<(std::basic_ostream& os, const utc_time& t) { const CharT fmt[] = {'%', 'F', ' ', '%', 'T', CharT{}}; return to_stream(os, fmt, t); } template > std::basic_istream& from_stream(std::basic_istream& is, const CharT* fmt, utc_time& tp, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) { using std::chrono::seconds; using std::chrono::minutes; using CT = typename std::common_type::type; minutes offset_local{}; auto offptr = offset ? offset : &offset_local; fields fds{}; fds.has_tod = true; from_stream(is, fmt, fds, abbrev, offptr); if (!fds.ymd.ok()) is.setstate(std::ios::failbit); if (!is.fail()) { bool is_60_sec = fds.tod.seconds() == seconds{60}; if (is_60_sec) fds.tod.seconds(detail::undocumented{}) -= seconds{1}; auto tmp = utc_clock::from_sys(sys_days(fds.ymd) - *offptr + fds.tod.to_duration()); if (is_60_sec) tmp += seconds{1}; if (is_60_sec != is_leap_second(tmp).first || !fds.tod.in_conventional_range()) { is.setstate(std::ios::failbit); return is; } tp = std::chrono::time_point_cast(tmp); } return is; } // tai_clock class tai_clock { public: using duration = std::chrono::system_clock::duration; using rep = duration::rep; using period = duration::period; using time_point = std::chrono::time_point; static const bool is_steady = false; static time_point now(); template static std::chrono::time_point::type> to_utc(const std::chrono::time_point&) NOEXCEPT; template static std::chrono::time_point::type> from_utc(const std::chrono::time_point&) NOEXCEPT; template static std::chrono::time_point::type> to_local(const std::chrono::time_point&) NOEXCEPT; template static std::chrono::time_point::type> from_local(const std::chrono::time_point&) NOEXCEPT; }; template using tai_time = std::chrono::time_point; using tai_seconds = tai_time; template inline utc_time::type> tai_clock::to_utc(const tai_time& t) NOEXCEPT { using std::chrono::seconds; using CD = typename std::common_type::type; return utc_time{t.time_since_epoch()} - (sys_days(year{1970}/January/1) - sys_days(year{1958}/January/1) + seconds{10}); } template inline tai_time::type> tai_clock::from_utc(const utc_time& t) NOEXCEPT { using std::chrono::seconds; using CD = typename std::common_type::type; return tai_time{t.time_since_epoch()} + (sys_days(year{1970}/January/1) - sys_days(year{1958}/January/1) + seconds{10}); } inline tai_clock::time_point tai_clock::now() { return from_utc(utc_clock::now()); } template inline local_time::type> tai_clock::to_local(const tai_time& t) NOEXCEPT { using CD = typename std::common_type::type; return local_time{t.time_since_epoch()} - (local_days(year{1970}/January/1) - local_days(year{1958}/January/1)); } template inline tai_time::type> tai_clock::from_local(const local_time& t) NOEXCEPT { using CD = typename std::common_type::type; return tai_time{t.time_since_epoch()} + (local_days(year{1970}/January/1) - local_days(year{1958}/January/1)); } template std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const tai_time& t) { const std::string abbrev("TAI"); CONSTDATA std::chrono::seconds offset{0}; return to_stream(os, fmt, tai_clock::to_local(t), &abbrev, &offset); } template std::basic_ostream& operator<<(std::basic_ostream& os, const tai_time& t) { const CharT fmt[] = {'%', 'F', ' ', '%', 'T', CharT{}}; return to_stream(os, fmt, t); } template > std::basic_istream& from_stream(std::basic_istream& is, const CharT* fmt, tai_time& tp, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) { local_time lp; from_stream(is, fmt, lp, abbrev, offset); if (!is.fail()) tp = tai_clock::from_local(lp); return is; } // gps_clock class gps_clock { public: using duration = std::chrono::system_clock::duration; using rep = duration::rep; using period = duration::period; using time_point = std::chrono::time_point; static const bool is_steady = false; static time_point now(); template static std::chrono::time_point::type> to_utc(const std::chrono::time_point&) NOEXCEPT; template static std::chrono::time_point::type> from_utc(const std::chrono::time_point&) NOEXCEPT; template static std::chrono::time_point::type> to_local(const std::chrono::time_point&) NOEXCEPT; template static std::chrono::time_point::type> from_local(const std::chrono::time_point&) NOEXCEPT; }; template using gps_time = std::chrono::time_point; using gps_seconds = gps_time; template inline utc_time::type> gps_clock::to_utc(const gps_time& t) NOEXCEPT { using std::chrono::seconds; using CD = typename std::common_type::type; return utc_time{t.time_since_epoch()} + (sys_days(year{1980}/January/Sunday[1]) - sys_days(year{1970}/January/1) + seconds{9}); } template inline gps_time::type> gps_clock::from_utc(const utc_time& t) NOEXCEPT { using std::chrono::seconds; using CD = typename std::common_type::type; return gps_time{t.time_since_epoch()} - (sys_days(year{1980}/January/Sunday[1]) - sys_days(year{1970}/January/1) + seconds{9}); } inline gps_clock::time_point gps_clock::now() { return from_utc(utc_clock::now()); } template inline local_time::type> gps_clock::to_local(const gps_time& t) NOEXCEPT { using CD = typename std::common_type::type; return local_time{t.time_since_epoch()} + (local_days(year{1980}/January/Sunday[1]) - local_days(year{1970}/January/1)); } template inline gps_time::type> gps_clock::from_local(const local_time& t) NOEXCEPT { using CD = typename std::common_type::type; return gps_time{t.time_since_epoch()} - (local_days(year{1980}/January/Sunday[1]) - local_days(year{1970}/January/1)); } template std::basic_ostream& to_stream(std::basic_ostream& os, const CharT* fmt, const gps_time& t) { const std::string abbrev("GPS"); CONSTDATA std::chrono::seconds offset{0}; return to_stream(os, fmt, gps_clock::to_local(t), &abbrev, &offset); } template std::basic_ostream& operator<<(std::basic_ostream& os, const gps_time& t) { const CharT fmt[] = {'%', 'F', ' ', '%', 'T', CharT{}}; return to_stream(os, fmt, t); } template > std::basic_istream& from_stream(std::basic_istream& is, const CharT* fmt, gps_time& tp, std::basic_string* abbrev = nullptr, std::chrono::minutes* offset = nullptr) { local_time lp; from_stream(is, fmt, lp, abbrev, offset); if (!is.fail()) tp = gps_clock::from_local(lp); return is; } // clock_time_conversion template struct clock_time_conversion {}; template <> struct clock_time_conversion { template CONSTCD14 sys_time operator()(const sys_time& st) const { return st; } }; template <> struct clock_time_conversion { template CONSTCD14 utc_time operator()(const utc_time& ut) const { return ut; } }; template<> struct clock_time_conversion { template CONSTCD14 local_time operator()(const local_time& lt) const { return lt; } }; template <> struct clock_time_conversion { template utc_time::type> operator()(const sys_time& st) const { return utc_clock::from_sys(st); } }; template <> struct clock_time_conversion { template sys_time::type> operator()(const utc_time& ut) const { return utc_clock::to_sys(ut); } }; template<> struct clock_time_conversion { template CONSTCD14 local_time operator()(const sys_time& st) const { return local_time{st.time_since_epoch()}; } }; template<> struct clock_time_conversion { template CONSTCD14 sys_time operator()(const local_time& lt) const { return sys_time{lt.time_since_epoch()}; } }; template<> struct clock_time_conversion { template utc_time::type> operator()(const local_time& lt) const { return utc_clock::from_local(lt); } }; template<> struct clock_time_conversion { template local_time::type> operator()(const utc_time& ut) const { return utc_clock::to_local(ut); } }; template struct clock_time_conversion { template CONSTCD14 std::chrono::time_point operator()(const std::chrono::time_point& tp) const { return tp; } }; namespace ctc_detail { template using time_point = std::chrono::time_point; using std::declval; using std::chrono::system_clock; //Check if TimePoint is time for given clock, //if not emits hard error template struct return_clock_time { using clock_time_point = time_point; using type = TimePoint; static_assert(std::is_same::value, "time point with appropariate clock shall be returned"); }; // Check if Clock has to_sys method accepting TimePoint with given duration const& and // returning sys_time. If so has nested type member equal to return type to_sys. template struct return_to_sys {}; template struct return_to_sys < Clock, Duration, decltype(Clock::to_sys(declval const&>()), void()) > : return_clock_time < system_clock, decltype(Clock::to_sys(declval const&>())) > {}; // Similiar to above template struct return_from_sys {}; template struct return_from_sys < Clock, Duration, decltype(Clock::from_sys(declval const&>()), void()) > : return_clock_time < Clock, decltype(Clock::from_sys(declval const&>())) > {}; // Similiar to above template struct return_to_utc {}; template struct return_to_utc < Clock, Duration, decltype(Clock::to_utc(declval const&>()), void()) > : return_clock_time < utc_clock, decltype(Clock::to_utc(declval const&>()))> {}; // Similiar to above template struct return_from_utc {}; template struct return_from_utc < Clock, Duration, decltype(Clock::from_utc(declval const&>()), void()) > : return_clock_time < Clock, decltype(Clock::from_utc(declval const&>())) > {}; // Similiar to above template struct return_to_local {}; template struct return_to_local < Clock, Duration, decltype(Clock::to_local(declval const&>()), void()) > : return_clock_time < local_t, decltype(Clock::to_local(declval const&>())) > {}; // Similiar to above template struct return_from_local {}; template struct return_from_local < Clock, Duration, decltype(Clock::from_local(declval const&>()), void()) > : return_clock_time < Clock, decltype(Clock::from_local(declval const&>())) > {}; } // namespace ctc_detail template struct clock_time_conversion { template CONSTCD14 typename ctc_detail::return_to_sys::type operator()(const std::chrono::time_point& tp) const { return SrcClock::to_sys(tp); } }; template struct clock_time_conversion { template CONSTCD14 typename ctc_detail::return_from_sys::type operator()(const sys_time& st) const { return DstClock::from_sys(st); } }; template struct clock_time_conversion { template CONSTCD14 typename ctc_detail::return_to_utc::type operator()(const std::chrono::time_point& tp) const { return SrcClock::to_utc(tp); } }; template struct clock_time_conversion { template CONSTCD14 typename ctc_detail::return_from_utc::type operator()(const utc_time& ut) const { return DstClock::from_utc(ut); } }; template struct clock_time_conversion { template CONSTCD14 typename ctc_detail::return_to_local::type operator()(const std::chrono::time_point& tp) const { return SrcClock::to_local(tp); } }; template struct clock_time_conversion { template CONSTCD14 typename ctc_detail::return_from_local::type operator()(const local_time& lt) const { return DstClock::from_local(lt); } }; namespace clock_cast_detail { template using time_point = std::chrono::time_point; using std::chrono::system_clock; template CONSTCD14 auto conv_clock(const time_point& t) -> decltype(std::declval>()(t)) { return clock_time_conversion{}(t); } //direct trait conversion, 1st candidate template CONSTCD14 auto cc_impl(const time_point& t, const time_point*) -> decltype(conv_clock(t)) { return conv_clock(t); } //conversion through sys, 2nd candidate template CONSTCD14 auto cc_impl(const time_point& t, const void*) -> decltype(conv_clock(conv_clock(t))) { return conv_clock(conv_clock(t)); } //conversion through utc, 2nd candidate template CONSTCD14 auto cc_impl(const time_point& t, const void*) -> decltype(0, // MSVC_WORKAROUND conv_clock(conv_clock(t))) { return conv_clock(conv_clock(t)); } //conversion through sys and utc, 3rd candidate template CONSTCD14 auto cc_impl(const time_point& t, ...) -> decltype(conv_clock(conv_clock(conv_clock(t)))) { return conv_clock(conv_clock(conv_clock(t))); } //conversion through utc and sys, 3rd candidate template CONSTCD14 auto cc_impl(const time_point& t, ...) -> decltype(0, // MSVC_WORKAROUND conv_clock(conv_clock(conv_clock(t)))) { return conv_clock(conv_clock(conv_clock(t))); } } // namespace clock_cast_detail template CONSTCD14 auto clock_cast(const std::chrono::time_point& tp) -> decltype(clock_cast_detail::cc_impl(tp, &tp)) { return clock_cast_detail::cc_impl(tp, &tp); } // Deprecated API template inline sys_time::type> to_sys_time(const utc_time& t) { return utc_clock::to_sys(t); } template inline sys_time::type> to_sys_time(const tai_time& t) { return utc_clock::to_sys(tai_clock::to_utc(t)); } template inline sys_time::type> to_sys_time(const gps_time& t) { return utc_clock::to_sys(gps_clock::to_utc(t)); } template inline utc_time::type> to_utc_time(const sys_time& t) { return utc_clock::from_sys(t); } template inline utc_time::type> to_utc_time(const tai_time& t) { return tai_clock::to_utc(t); } template inline utc_time::type> to_utc_time(const gps_time& t) { return gps_clock::to_utc(t); } template inline tai_time::type> to_tai_time(const sys_time& t) { return tai_clock::from_utc(utc_clock::from_sys(t)); } template inline tai_time::type> to_tai_time(const utc_time& t) { return tai_clock::from_utc(t); } template inline tai_time::type> to_tai_time(const gps_time& t) { return tai_clock::from_utc(gps_clock::to_utc(t)); } template inline gps_time::type> to_gps_time(const sys_time& t) { return gps_clock::from_utc(utc_clock::from_sys(t)); } template inline gps_time::type> to_gps_time(const utc_time& t) { return gps_clock::from_utc(t); } template inline gps_time::type> to_gps_time(const tai_time& t) { return gps_clock::from_utc(tai_clock::to_utc(t)); } } // namespace date #endif // TZ_H pr0m1th3as-datatypes-9450b29/src/date/tz_private.h000066400000000000000000000246621527370255000217540ustar00rootroot00000000000000#ifndef TZ_PRIVATE_H #define TZ_PRIVATE_H // The MIT License (MIT) // // Copyright (c) 2015, 2016 Howard Hinnant // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // Our apologies. When the previous paragraph was written, lowercase had not yet // been invented (that would involve another several millennia of evolution). // We did not mean to shout. #if !defined(_MSC_VER) || (_MSC_VER >= 1900) #include "tz.h" #else #include "date.h" #include #endif namespace date { namespace detail { #if !USE_OS_TZDB enum class tz {utc, local, standard}; //forward declare to avoid warnings in gcc 6.2 class MonthDayTime; std::istream& operator>>(std::istream& is, MonthDayTime& x); std::ostream& operator<<(std::ostream& os, const MonthDayTime& x); class MonthDayTime { private: struct pair { #if defined(_MSC_VER) && (_MSC_VER < 1900) pair() : month_day_(date::jan / 1), weekday_(0U) {} pair(const date::month_day& month_day, const date::weekday& weekday) : month_day_(month_day), weekday_(weekday) {} #endif date::month_day month_day_; date::weekday weekday_; }; enum Type {month_day, month_last_dow, lteq, gteq}; Type type_{month_day}; #if !defined(_MSC_VER) || (_MSC_VER >= 1900) union U #else struct U #endif { date::month_day month_day_; date::month_weekday_last month_weekday_last_; pair month_day_weekday_; #if !defined(_MSC_VER) || (_MSC_VER >= 1900) U() : month_day_{date::jan/1} {} #else U() : month_day_(date::jan/1), month_weekday_last_(date::month(0U), date::weekday_last(date::weekday(0U))) {} #endif // !defined(_MSC_VER) || (_MSC_VER >= 1900) U& operator=(const date::month_day& x); U& operator=(const date::month_weekday_last& x); U& operator=(const pair& x); } u; std::chrono::hours h_{0}; std::chrono::minutes m_{0}; std::chrono::seconds s_{0}; tz zone_{tz::local}; public: MonthDayTime() = default; MonthDayTime(local_seconds tp, tz timezone); MonthDayTime(const date::month_day& md, tz timezone); date::day day() const; date::month month() const; tz zone() const {return zone_;} void canonicalize(date::year y); sys_seconds to_sys(date::year y, std::chrono::seconds offset, std::chrono::seconds save) const; sys_days to_sys_days(date::year y) const; sys_seconds to_time_point(date::year y) const; int compare(date::year y, const MonthDayTime& x, date::year yx, std::chrono::seconds offset, std::chrono::minutes prev_save) const; friend std::istream& operator>>(std::istream& is, MonthDayTime& x); friend std::ostream& operator<<(std::ostream& os, const MonthDayTime& x); }; // A Rule specifies one or more set of datetimes without using an offset. // Multiple dates are specified with multiple years. The years in effect // go from starting_year_ to ending_year_, inclusive. starting_year_ <= // ending_year_. save_ is in effect for times from the specified time // onward, including the specified time. When the specified time is // local, it uses the save_ from the chronologically previous Rule, or if // there is none, 0. //forward declare to avoid warnings in gcc 6.2 class Rule; bool operator==(const Rule& x, const Rule& y); bool operator<(const Rule& x, const Rule& y); bool operator==(const Rule& x, const date::year& y); bool operator<(const Rule& x, const date::year& y); bool operator==(const date::year& x, const Rule& y); bool operator<(const date::year& x, const Rule& y); bool operator==(const Rule& x, const std::string& y); bool operator<(const Rule& x, const std::string& y); bool operator==(const std::string& x, const Rule& y); bool operator<(const std::string& x, const Rule& y); std::ostream& operator<<(std::ostream& os, const Rule& r); class Rule { private: std::string name_; date::year starting_year_{0}; date::year ending_year_{0}; MonthDayTime starting_at_; std::chrono::minutes save_{0}; std::string abbrev_; public: Rule() = default; explicit Rule(const std::string& s); Rule(const Rule& r, date::year starting_year, date::year ending_year); const std::string& name() const {return name_;} const std::string& abbrev() const {return abbrev_;} const MonthDayTime& mdt() const {return starting_at_;} const date::year& starting_year() const {return starting_year_;} const date::year& ending_year() const {return ending_year_;} const std::chrono::minutes& save() const {return save_;} static void split_overlaps(std::vector& rules); friend bool operator==(const Rule& x, const Rule& y); friend bool operator<(const Rule& x, const Rule& y); friend bool operator==(const Rule& x, const date::year& y); friend bool operator<(const Rule& x, const date::year& y); friend bool operator==(const date::year& x, const Rule& y); friend bool operator<(const date::year& x, const Rule& y); friend bool operator==(const Rule& x, const std::string& y); friend bool operator<(const Rule& x, const std::string& y); friend bool operator==(const std::string& x, const Rule& y); friend bool operator<(const std::string& x, const Rule& y); friend std::ostream& operator<<(std::ostream& os, const Rule& r); private: date::day day() const; date::month month() const; static void split_overlaps(std::vector& rules, std::size_t i, std::size_t& e); static bool overlaps(const Rule& x, const Rule& y); static void split(std::vector& rules, std::size_t i, std::size_t k, std::size_t& e); }; inline bool operator!=(const Rule& x, const Rule& y) {return !(x == y);} inline bool operator> (const Rule& x, const Rule& y) {return y < x;} inline bool operator<=(const Rule& x, const Rule& y) {return !(y < x);} inline bool operator>=(const Rule& x, const Rule& y) {return !(x < y);} inline bool operator!=(const Rule& x, const date::year& y) {return !(x == y);} inline bool operator> (const Rule& x, const date::year& y) {return y < x;} inline bool operator<=(const Rule& x, const date::year& y) {return !(y < x);} inline bool operator>=(const Rule& x, const date::year& y) {return !(x < y);} inline bool operator!=(const date::year& x, const Rule& y) {return !(x == y);} inline bool operator> (const date::year& x, const Rule& y) {return y < x;} inline bool operator<=(const date::year& x, const Rule& y) {return !(y < x);} inline bool operator>=(const date::year& x, const Rule& y) {return !(x < y);} inline bool operator!=(const Rule& x, const std::string& y) {return !(x == y);} inline bool operator> (const Rule& x, const std::string& y) {return y < x;} inline bool operator<=(const Rule& x, const std::string& y) {return !(y < x);} inline bool operator>=(const Rule& x, const std::string& y) {return !(x < y);} inline bool operator!=(const std::string& x, const Rule& y) {return !(x == y);} inline bool operator> (const std::string& x, const Rule& y) {return y < x;} inline bool operator<=(const std::string& x, const Rule& y) {return !(y < x);} inline bool operator>=(const std::string& x, const Rule& y) {return !(x < y);} struct zonelet { enum tag {has_rule, has_save, is_empty}; std::chrono::seconds gmtoff_; tag tag_ = has_rule; #if !defined(_MSC_VER) || (_MSC_VER >= 1900) union U #else struct U #endif { std::string rule_; std::chrono::minutes save_; ~U() {} U() {} U(const U&) {} U& operator=(const U&) = delete; } u; std::string format_; date::year until_year_{0}; MonthDayTime until_date_; sys_seconds until_utc_; local_seconds until_std_; local_seconds until_loc_; std::chrono::minutes initial_save_{0}; std::string initial_abbrev_; std::pair first_rule_{nullptr, date::year::min()}; std::pair last_rule_{nullptr, date::year::max()}; ~zonelet(); zonelet(); zonelet(const zonelet& i); zonelet& operator=(const zonelet&) = delete; }; #else // USE_OS_TZDB struct ttinfo { std::int32_t tt_gmtoff; unsigned char tt_isdst; unsigned char tt_abbrind; unsigned char pad[2]; }; static_assert(sizeof(ttinfo) == 8, ""); struct expanded_ttinfo { std::chrono::seconds offset; std::string abbrev; bool is_dst; }; struct transition { sys_seconds timepoint; const expanded_ttinfo* info; transition(sys_seconds tp, const expanded_ttinfo* i = nullptr) : timepoint(tp) , info(i) {} friend std::ostream& operator<<(std::ostream& os, const transition& t) { date::operator<<(os, t.timepoint) << "Z "; if (t.info->offset >= std::chrono::seconds{0}) os << '+'; os << make_time(t.info->offset); if (t.info->is_dst > 0) os << " daylight "; else os << " standard "; os << t.info->abbrev; return os; } }; #endif // USE_OS_TZDB } // namespace detail } // namespace date #if defined(_MSC_VER) && (_MSC_VER < 1900) #include "tz.h" #endif #endif // TZ_PRIVATE_H pr0m1th3as-datatypes-9450b29/src/doc-cache000066400000000000000000000231101527370255000202130ustar00rootroot00000000000000# doc-cache created by Octave 11.3.0 # name: cache # type: cell # rows: 3 # columns: 12 # name: # type: sq_string # elements: 1 # length: 12 __ckeyHash__ # name: # type: sq_string # elements: 1 # length: 475 : uint64 = __ckeyHash__ (str) : uint64 = __ckeyHash__ (cstr) : uint64 = __ckeyHash__ (..., FNV1A64_BASE) Fowler–Noll–Vo hash key for a character vector or a cell array of them. Given a cell array, the elements are hashed with their lengths so that the boundaries between them are part of the key and no two different splittings of the same characters can collide. This is a helper function for keyHash methods of `datatypes`' classes. Do NOT use this function directly. # name: # type: sq_string # elements: 1 # length: 75 Fowler–Noll–Vo hash key for a character vector or a cell array of them. # name: # type: sq_string # elements: 1 # length: 13 __csv2table__ # name: # type: sq_string # elements: 1 # length: 176 datatypes: C = __csv2table__ (file) Barebone function for loading a CSV file to a cell array. This is a helper IO function for the csv2table function. Do NOT call it directly. # name: # type: sq_string # elements: 1 # length: 57 Barebone function for loading a CSV file to a cell array. # name: # type: sq_string # elements: 1 # length: 12 __datetime__ # name: # type: sq_string # elements: 1 # length: 1708 datatypes: [Y, MO, D, H, MI, S] __datetime__ (...) datatypes: [Y, MO, D, H, MI, S, errmsg] __datetime__ (...) datatypes: [...] = __datetime__ ('now') datatypes: [...] = __datetime__ ('today') datatypes: [...] = __datetime__ ('tomorrow') datatypes: [...] = __datetime__ ('yesterday') datatypes: [...] = __datetime__ (Y, MO, D) datatypes: [...] = __datetime__ (Y, MO, D, H, MI, S) datatypes: [...] = __datetime__ (Y, MO, D, H, MI, S, MS) datatypes: [...] = __datetime__ (X, 'ConvertFrom', dateType) datatypes: [...] = __datetime__ (..., 'Precision', precision) datatypes: [...] = __datetime__ (..., 'TimeZone', tzone, 'toTimeZone', totzone) datatypes: [...] = __datetime__ (..., 'Offset', off) Base function for datetime class. The 'Offset' parameter gives the stored UTC offset of each element in seconds, which is what tells apart the two instants a repeated wall clock names on the day a zone puts its clock back. It is accepted by the 'ConvertTo' modes listed below; those that also read it answer for the wall clock alone when it is not given. 'instant' POSIX seconds of each element, taken from its offset by subtraction, with no time-zone lookup at all. 'fromposix' The six components of each POSIX instant read in tzone, plus the offset in force there as a seventh output. 'rezone' The six components of each element expressed in totzone while keeping its instant, plus the offset there as a seventh output. 'zoneoffset' The offset the wall clock resolves to. 'keepfold' The given offset where it still names this wall clock, and the resolved offset otherwise. 'isdst', 'zoneabbrev' Daylight-saving flag and zone abbreviation, for the element's own pass over a repeated clock when an offset is given. # name: # type: sq_string # elements: 1 # length: 33 Base function for datetime class. # name: # type: sq_string # elements: 1 # length: 19 __dictionary2json__ # name: # type: sq_string # elements: 1 # length: 648 datatypes: errmsg = __dictionary2json__ (tree, file, pretty, preserve) Write a tree of JSON values to a file. tree has the form __json2dictionary__ returns: an object is a scalar structure with the fields keys and values, the keys text, numbers or logical values; an array a cell array; text a character row; a number or logical value a scalar, a vector of them an array; and null a 0-by-0 structure array with no fields. pretty indents by four spaces, preserve writes NaN and Inf rather than null. errmsg is empty, or names the file that could not be written. This is a helper IO function for the writedictionary method. Do NOT call it directly. # name: # type: sq_string # elements: 1 # length: 38 Write a tree of JSON values to a file. # name: # type: sq_string # elements: 1 # length: 19 __json2dictionary__ # name: # type: sq_string # elements: 1 # length: 747 datatypes: [tree, errmsg] = __json2dictionary__ (text, comments, commas, infnan) Parse JSON text into a tree that keeps the kinds of JSON values. An object is a scalar structure with the fields keys and values, two column cell arrays in file order, duplicate keys kept. An array is a column cell array, text a character row, a number a double, or an int64 or uint64 for an integer beyond 2^53 that one holds exactly, true and false logical scalars, and null a 0-by-0 structure array with no fields. comments, commas and infnan allow comments, trailing commas and the non-finite numbers. On an error tree is empty and errmsg names it with its line and column. This is a helper IO function for the readdictionary function. Do NOT call it directly. # name: # type: sq_string # elements: 1 # length: 64 Parse JSON text into a tree that keeps the kinds of JSON values. # name: # type: sq_string # elements: 1 # length: 8 __ldml__ # name: # type: sq_string # elements: 1 # length: 710 : [DV, OFF] = __ldml__ ("parse", strs, fmt, pivot, locale) Parse date/time strings under an LDML 'InputFormat' pattern. strs is a cell array of character vectors, fmt an LDML pattern, pivot the two-digit year pivot, and locale a language tag such as 'fr_FR' (empty or 'system' for English). The return value is an N-by-6 date-vector matrix, and the second output OFF the UTC offset in seconds each string carries in an offset field, NaN where it carries none. syms = __ldml__ ("symbols", fmt) returns the field symbols of fmt, in order and one character per field run, with literal text omitted. This is a helper function for the datetime class of the `datatypes` package. Do NOT use this function directly. # name: # type: sq_string # elements: 1 # length: 60 Parse date/time strings under an LDML 'InputFormat' pattern. # name: # type: sq_string # elements: 1 # length: 12 __nkeyHash__ # name: # type: sq_string # elements: 1 # length: 226 : uint64 = __nkeyHash__ (x) : uint64 = __nkeyHash__ (x, FNV1A64_BASE) Fowler–Noll–Vo hash key for a numeric vector. This is a helper function for keyHash methods of `datatypes`' classes. Do NOT use this function directly. # name: # type: sq_string # elements: 1 # length: 49 Fowler–Noll–Vo hash key for a numeric vector. # name: # type: sq_string # elements: 1 # length: 13 __ods2table__ # name: # type: sq_string # elements: 1 # length: 322 datatypes: [data, vtype, meta] = __ods2table__ (file) datatypes: [data, vtype, meta] = __ods2table__ (file, sheet) datatypes: [data, vtype, meta, names, preamble] = __ods2table__ (...) Barebone function for reading a flat ODS (.fods) file. This is a helper IO function for the ods2table function. Do NOT call it directly. # name: # type: sq_string # elements: 1 # length: 54 Barebone function for reading a flat ODS (.fods) file. # name: # type: sq_string # elements: 1 # length: 13 __table2csv__ # name: # type: sq_string # elements: 1 # length: 191 datatypes: __table2csv__ (file, C) Barebone function for saving a cell array to a CSV file. This is a helper IO function for the table2csv method of the table class. Do NOT call it directly. # name: # type: sq_string # elements: 1 # length: 56 Barebone function for saving a cell array to a CSV file. # name: # type: sq_string # elements: 1 # length: 13 __table2ods__ # name: # type: sq_string # elements: 1 # length: 326 datatypes: __table2ods__ (file, data, vtype, meta, flat) datatypes: __table2ods__ (file, data, vtype, meta, flat, opts) Barebone function for saving a table to a flat (.fods) or compressed (.ods) OpenDocument spreadsheet file. This is a helper IO function for the table2ods method of the table class. Do NOT call it directly. # name: # type: sq_string # elements: 1 # length: 106 Barebone function for saving a table to a flat (.fods) or compressed (.ods) OpenDocument spreadsheet file. # name: # type: sq_string # elements: 1 # length: 14 __table2xlsx__ # name: # type: sq_string # elements: 1 # length: 379 datatypes: __table2xlsx__ (file, data, vtype, opts) Barebone function for writing an Office Open XML (.xlsx / .xlsm) spreadsheet. A single sheet is written from data / vtype; when opts carries a sheets struct array (fields name, data, vtype, header) a multi-sheet workbook is written instead. This is a helper IO function for writetable and struct2xlsx. Do NOT call it directly. # name: # type: sq_string # elements: 1 # length: 77 Barebone function for writing an Office Open XML (.xlsx / .xlsm) spreadsheet. # name: # type: sq_string # elements: 1 # length: 14 __xlsx2table__ # name: # type: sq_string # elements: 1 # length: 302 datatypes: [data, vtype, meta, names] = __xlsx2table__ (file) datatypes: [data, vtype, meta, names] = __xlsx2table__ (file, sheet) Barebone function for reading a sheet of an Office Open XML (.xlsx / .xlsm) spreadsheet. This is a helper IO function for the readtable function. Do NOT call it directly. # name: # type: sq_string # elements: 1 # length: 88 Barebone function for reading a sheet of an Office Open XML (.xlsx / .xlsm) spreadsheet. pr0m1th3as-datatypes-9450b29/src/ods/000077500000000000000000000000001527370255000172525ustar00rootroot00000000000000pr0m1th3as-datatypes-9450b29/src/ods/LICENSE.md000066400000000000000000000020671527370255000206630ustar00rootroot00000000000000MIT License Copyright (c) 2006-2023 Arseny Kapoulkine Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pr0m1th3as-datatypes-9450b29/src/ods/miniz.c000066400000000000000000011657651527370255000205710ustar00rootroot00000000000000#include "miniz.h" /************************************************************************** * * Copyright 2013-2014 RAD Game Tools and Valve Software * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC * All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * **************************************************************************/ typedef unsigned char mz_validate_uint16[sizeof(mz_uint16) == 2 ? 1 : -1]; typedef unsigned char mz_validate_uint32[sizeof(mz_uint32) == 4 ? 1 : -1]; typedef unsigned char mz_validate_uint64[sizeof(mz_uint64) == 8 ? 1 : -1]; #ifdef __cplusplus extern "C" { #endif /* ------------------- zlib-style API's */ mz_ulong mz_adler32(mz_ulong adler, const unsigned char *ptr, size_t buf_len) { mz_uint32 i, s1 = (mz_uint32)(adler & 0xffff), s2 = (mz_uint32)(adler >> 16); size_t block_len = buf_len % 5552; if (!ptr) return MZ_ADLER32_INIT; while (buf_len) { for (i = 0; i + 7 < block_len; i += 8, ptr += 8) { s1 += ptr[0], s2 += s1; s1 += ptr[1], s2 += s1; s1 += ptr[2], s2 += s1; s1 += ptr[3], s2 += s1; s1 += ptr[4], s2 += s1; s1 += ptr[5], s2 += s1; s1 += ptr[6], s2 += s1; s1 += ptr[7], s2 += s1; } for (; i < block_len; ++i) s1 += *ptr++, s2 += s1; s1 %= 65521U, s2 %= 65521U; buf_len -= block_len; block_len = 5552; } return (s2 << 16) + s1; } /* Karl Malbrain's compact CRC-32. See "A compact CCITT crc16 and crc32 C implementation that balances processor cache usage against speed": http://www.geocities.com/malbrain/ */ #if 0 mz_ulong mz_crc32(mz_ulong crc, const mz_uint8 *ptr, size_t buf_len) { static const mz_uint32 s_crc32[16] = { 0, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c }; mz_uint32 crcu32 = (mz_uint32)crc; if (!ptr) return MZ_CRC32_INIT; crcu32 = ~crcu32; while (buf_len--) { mz_uint8 b = *ptr++; crcu32 = (crcu32 >> 4) ^ s_crc32[(crcu32 & 0xF) ^ (b & 0xF)]; crcu32 = (crcu32 >> 4) ^ s_crc32[(crcu32 & 0xF) ^ (b >> 4)]; } return ~crcu32; } #elif defined(USE_EXTERNAL_MZCRC) /* If USE_EXTERNAL_CRC is defined, an external module will export the * mz_crc32() symbol for us to use, e.g. an SSE-accelerated version. * Depending on the impl, it may be necessary to ~ the input/output crc values. */ mz_ulong mz_crc32(mz_ulong crc, const mz_uint8 *ptr, size_t buf_len); #else /* Faster, but larger CPU cache footprint. */ mz_ulong mz_crc32(mz_ulong crc, const mz_uint8 *ptr, size_t buf_len) { static const mz_uint32 s_crc_table[256] = { 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D }; mz_uint32 crc32 = (mz_uint32)crc ^ 0xFFFFFFFF; const mz_uint8 *pByte_buf = (const mz_uint8 *)ptr; while (buf_len >= 4) { crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[0]) & 0xFF]; crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[1]) & 0xFF]; crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[2]) & 0xFF]; crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[3]) & 0xFF]; pByte_buf += 4; buf_len -= 4; } while (buf_len) { crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[0]) & 0xFF]; ++pByte_buf; --buf_len; } return ~crc32; } #endif void mz_free(void *p) { MZ_FREE(p); } MINIZ_EXPORT void *miniz_def_alloc_func(void *opaque, size_t items, size_t size) { (void)opaque, (void)items, (void)size; return MZ_MALLOC(items * size); } MINIZ_EXPORT void miniz_def_free_func(void *opaque, void *address) { (void)opaque, (void)address; MZ_FREE(address); } MINIZ_EXPORT void *miniz_def_realloc_func(void *opaque, void *address, size_t items, size_t size) { (void)opaque, (void)address, (void)items, (void)size; return MZ_REALLOC(address, items * size); } const char *mz_version(void) { return MZ_VERSION; } #ifndef MINIZ_NO_ZLIB_APIS #ifndef MINIZ_NO_DEFLATE_APIS int mz_deflateInit(mz_streamp pStream, int level) { return mz_deflateInit2(pStream, level, MZ_DEFLATED, MZ_DEFAULT_WINDOW_BITS, 9, MZ_DEFAULT_STRATEGY); } int mz_deflateInit2(mz_streamp pStream, int level, int method, int window_bits, int mem_level, int strategy) { tdefl_compressor *pComp; mz_uint comp_flags = TDEFL_COMPUTE_ADLER32 | tdefl_create_comp_flags_from_zip_params(level, window_bits, strategy); if (!pStream) return MZ_STREAM_ERROR; if ((method != MZ_DEFLATED) || ((mem_level < 1) || (mem_level > 9)) || ((window_bits != MZ_DEFAULT_WINDOW_BITS) && (-window_bits != MZ_DEFAULT_WINDOW_BITS))) return MZ_PARAM_ERROR; pStream->data_type = 0; pStream->adler = MZ_ADLER32_INIT; pStream->msg = NULL; pStream->reserved = 0; pStream->total_in = 0; pStream->total_out = 0; if (!pStream->zalloc) pStream->zalloc = miniz_def_alloc_func; if (!pStream->zfree) pStream->zfree = miniz_def_free_func; pComp = (tdefl_compressor *)pStream->zalloc(pStream->opaque, 1, sizeof(tdefl_compressor)); if (!pComp) return MZ_MEM_ERROR; pStream->state = (struct mz_internal_state *)pComp; if (tdefl_init(pComp, NULL, NULL, comp_flags) != TDEFL_STATUS_OKAY) { mz_deflateEnd(pStream); return MZ_PARAM_ERROR; } return MZ_OK; } int mz_deflateReset(mz_streamp pStream) { if ((!pStream) || (!pStream->state) || (!pStream->zalloc) || (!pStream->zfree)) return MZ_STREAM_ERROR; pStream->total_in = pStream->total_out = 0; tdefl_init((tdefl_compressor *)pStream->state, NULL, NULL, ((tdefl_compressor *)pStream->state)->m_flags); return MZ_OK; } int mz_deflate(mz_streamp pStream, int flush) { size_t in_bytes, out_bytes; mz_ulong orig_total_in, orig_total_out; int mz_status = MZ_OK; if ((!pStream) || (!pStream->state) || (flush < 0) || (flush > MZ_FINISH) || (!pStream->next_out)) return MZ_STREAM_ERROR; if (!pStream->avail_out) return MZ_BUF_ERROR; if (flush == MZ_PARTIAL_FLUSH) flush = MZ_SYNC_FLUSH; if (((tdefl_compressor *)pStream->state)->m_prev_return_status == TDEFL_STATUS_DONE) return (flush == MZ_FINISH) ? MZ_STREAM_END : MZ_BUF_ERROR; orig_total_in = pStream->total_in; orig_total_out = pStream->total_out; for (;;) { tdefl_status defl_status; in_bytes = pStream->avail_in; out_bytes = pStream->avail_out; defl_status = tdefl_compress((tdefl_compressor *)pStream->state, pStream->next_in, &in_bytes, pStream->next_out, &out_bytes, (tdefl_flush)flush); pStream->next_in += (mz_uint)in_bytes; pStream->avail_in -= (mz_uint)in_bytes; pStream->total_in += (mz_uint)in_bytes; pStream->adler = tdefl_get_adler32((tdefl_compressor *)pStream->state); pStream->next_out += (mz_uint)out_bytes; pStream->avail_out -= (mz_uint)out_bytes; pStream->total_out += (mz_uint)out_bytes; if (defl_status < 0) { mz_status = MZ_STREAM_ERROR; break; } else if (defl_status == TDEFL_STATUS_DONE) { mz_status = MZ_STREAM_END; break; } else if (!pStream->avail_out) break; else if ((!pStream->avail_in) && (flush != MZ_FINISH)) { if ((flush) || (pStream->total_in != orig_total_in) || (pStream->total_out != orig_total_out)) break; return MZ_BUF_ERROR; /* Can't make forward progress without some input. */ } } return mz_status; } int mz_deflateEnd(mz_streamp pStream) { if (!pStream) return MZ_STREAM_ERROR; if (pStream->state) { pStream->zfree(pStream->opaque, pStream->state); pStream->state = NULL; } return MZ_OK; } mz_ulong mz_deflateBound(mz_streamp pStream, mz_ulong source_len) { (void)pStream; /* This is really over conservative. (And lame, but it's actually pretty tricky to compute a true upper bound given the way tdefl's blocking works.) */ return MZ_MAX(128 + (source_len * 110) / 100, 128 + source_len + ((source_len / (31 * 1024)) + 1) * 5); } int mz_compress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len, int level) { int status; mz_stream stream; memset(&stream, 0, sizeof(stream)); /* In case mz_ulong is 64-bits (argh I hate longs). */ if ((mz_uint64)(source_len | *pDest_len) > 0xFFFFFFFFU) return MZ_PARAM_ERROR; stream.next_in = pSource; stream.avail_in = (mz_uint32)source_len; stream.next_out = pDest; stream.avail_out = (mz_uint32)*pDest_len; status = mz_deflateInit(&stream, level); if (status != MZ_OK) return status; status = mz_deflate(&stream, MZ_FINISH); if (status != MZ_STREAM_END) { mz_deflateEnd(&stream); return (status == MZ_OK) ? MZ_BUF_ERROR : status; } *pDest_len = stream.total_out; return mz_deflateEnd(&stream); } int mz_compress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len) { return mz_compress2(pDest, pDest_len, pSource, source_len, MZ_DEFAULT_COMPRESSION); } mz_ulong mz_compressBound(mz_ulong source_len) { return mz_deflateBound(NULL, source_len); } #endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/ #ifndef MINIZ_NO_INFLATE_APIS typedef struct { tinfl_decompressor m_decomp; mz_uint m_dict_ofs, m_dict_avail, m_first_call, m_has_flushed; int m_window_bits; mz_uint8 m_dict[TINFL_LZ_DICT_SIZE]; tinfl_status m_last_status; } inflate_state; int mz_inflateInit2(mz_streamp pStream, int window_bits) { inflate_state *pDecomp; if (!pStream) return MZ_STREAM_ERROR; if ((window_bits != MZ_DEFAULT_WINDOW_BITS) && (-window_bits != MZ_DEFAULT_WINDOW_BITS)) return MZ_PARAM_ERROR; pStream->data_type = 0; pStream->adler = 0; pStream->msg = NULL; pStream->total_in = 0; pStream->total_out = 0; pStream->reserved = 0; if (!pStream->zalloc) pStream->zalloc = miniz_def_alloc_func; if (!pStream->zfree) pStream->zfree = miniz_def_free_func; pDecomp = (inflate_state *)pStream->zalloc(pStream->opaque, 1, sizeof(inflate_state)); if (!pDecomp) return MZ_MEM_ERROR; pStream->state = (struct mz_internal_state *)pDecomp; tinfl_init(&pDecomp->m_decomp); pDecomp->m_dict_ofs = 0; pDecomp->m_dict_avail = 0; pDecomp->m_last_status = TINFL_STATUS_NEEDS_MORE_INPUT; pDecomp->m_first_call = 1; pDecomp->m_has_flushed = 0; pDecomp->m_window_bits = window_bits; return MZ_OK; } int mz_inflateInit(mz_streamp pStream) { return mz_inflateInit2(pStream, MZ_DEFAULT_WINDOW_BITS); } int mz_inflateReset(mz_streamp pStream) { inflate_state *pDecomp; if (!pStream) return MZ_STREAM_ERROR; pStream->data_type = 0; pStream->adler = 0; pStream->msg = NULL; pStream->total_in = 0; pStream->total_out = 0; pStream->reserved = 0; pDecomp = (inflate_state *)pStream->state; tinfl_init(&pDecomp->m_decomp); pDecomp->m_dict_ofs = 0; pDecomp->m_dict_avail = 0; pDecomp->m_last_status = TINFL_STATUS_NEEDS_MORE_INPUT; pDecomp->m_first_call = 1; pDecomp->m_has_flushed = 0; /* pDecomp->m_window_bits = window_bits */; return MZ_OK; } int mz_inflate(mz_streamp pStream, int flush) { inflate_state *pState; mz_uint n, first_call, decomp_flags = TINFL_FLAG_COMPUTE_ADLER32; size_t in_bytes, out_bytes, orig_avail_in; tinfl_status status; if ((!pStream) || (!pStream->state)) return MZ_STREAM_ERROR; if (flush == MZ_PARTIAL_FLUSH) flush = MZ_SYNC_FLUSH; if ((flush) && (flush != MZ_SYNC_FLUSH) && (flush != MZ_FINISH)) return MZ_STREAM_ERROR; pState = (inflate_state *)pStream->state; if (pState->m_window_bits > 0) decomp_flags |= TINFL_FLAG_PARSE_ZLIB_HEADER; orig_avail_in = pStream->avail_in; first_call = pState->m_first_call; pState->m_first_call = 0; if (pState->m_last_status < 0) return MZ_DATA_ERROR; if (pState->m_has_flushed && (flush != MZ_FINISH)) return MZ_STREAM_ERROR; pState->m_has_flushed |= (flush == MZ_FINISH); if ((flush == MZ_FINISH) && (first_call)) { /* MZ_FINISH on the first call implies that the input and output buffers are large enough to hold the entire compressed/decompressed file. */ decomp_flags |= TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF; in_bytes = pStream->avail_in; out_bytes = pStream->avail_out; status = tinfl_decompress(&pState->m_decomp, pStream->next_in, &in_bytes, pStream->next_out, pStream->next_out, &out_bytes, decomp_flags); pState->m_last_status = status; pStream->next_in += (mz_uint)in_bytes; pStream->avail_in -= (mz_uint)in_bytes; pStream->total_in += (mz_uint)in_bytes; pStream->adler = tinfl_get_adler32(&pState->m_decomp); pStream->next_out += (mz_uint)out_bytes; pStream->avail_out -= (mz_uint)out_bytes; pStream->total_out += (mz_uint)out_bytes; if (status < 0) return MZ_DATA_ERROR; else if (status != TINFL_STATUS_DONE) { pState->m_last_status = TINFL_STATUS_FAILED; return MZ_BUF_ERROR; } return MZ_STREAM_END; } /* flush != MZ_FINISH then we must assume there's more input. */ if (flush != MZ_FINISH) decomp_flags |= TINFL_FLAG_HAS_MORE_INPUT; if (pState->m_dict_avail) { n = MZ_MIN(pState->m_dict_avail, pStream->avail_out); memcpy(pStream->next_out, pState->m_dict + pState->m_dict_ofs, n); pStream->next_out += n; pStream->avail_out -= n; pStream->total_out += n; pState->m_dict_avail -= n; pState->m_dict_ofs = (pState->m_dict_ofs + n) & (TINFL_LZ_DICT_SIZE - 1); return ((pState->m_last_status == TINFL_STATUS_DONE) && (!pState->m_dict_avail)) ? MZ_STREAM_END : MZ_OK; } for (;;) { in_bytes = pStream->avail_in; out_bytes = TINFL_LZ_DICT_SIZE - pState->m_dict_ofs; status = tinfl_decompress(&pState->m_decomp, pStream->next_in, &in_bytes, pState->m_dict, pState->m_dict + pState->m_dict_ofs, &out_bytes, decomp_flags); pState->m_last_status = status; pStream->next_in += (mz_uint)in_bytes; pStream->avail_in -= (mz_uint)in_bytes; pStream->total_in += (mz_uint)in_bytes; pStream->adler = tinfl_get_adler32(&pState->m_decomp); pState->m_dict_avail = (mz_uint)out_bytes; n = MZ_MIN(pState->m_dict_avail, pStream->avail_out); memcpy(pStream->next_out, pState->m_dict + pState->m_dict_ofs, n); pStream->next_out += n; pStream->avail_out -= n; pStream->total_out += n; pState->m_dict_avail -= n; pState->m_dict_ofs = (pState->m_dict_ofs + n) & (TINFL_LZ_DICT_SIZE - 1); if (status < 0) return MZ_DATA_ERROR; /* Stream is corrupted (there could be some uncompressed data left in the output dictionary - oh well). */ else if ((status == TINFL_STATUS_NEEDS_MORE_INPUT) && (!orig_avail_in)) return MZ_BUF_ERROR; /* Signal caller that we can't make forward progress without supplying more input or by setting flush to MZ_FINISH. */ else if (flush == MZ_FINISH) { /* The output buffer MUST be large to hold the remaining uncompressed data when flush==MZ_FINISH. */ if (status == TINFL_STATUS_DONE) return pState->m_dict_avail ? MZ_BUF_ERROR : MZ_STREAM_END; /* status here must be TINFL_STATUS_HAS_MORE_OUTPUT, which means there's at least 1 more byte on the way. If there's no more room left in the output buffer then something is wrong. */ else if (!pStream->avail_out) return MZ_BUF_ERROR; } else if ((status == TINFL_STATUS_DONE) || (!pStream->avail_in) || (!pStream->avail_out) || (pState->m_dict_avail)) break; } return ((status == TINFL_STATUS_DONE) && (!pState->m_dict_avail)) ? MZ_STREAM_END : MZ_OK; } int mz_inflateEnd(mz_streamp pStream) { if (!pStream) return MZ_STREAM_ERROR; if (pStream->state) { pStream->zfree(pStream->opaque, pStream->state); pStream->state = NULL; } return MZ_OK; } int mz_uncompress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong *pSource_len) { mz_stream stream; int status; memset(&stream, 0, sizeof(stream)); /* In case mz_ulong is 64-bits (argh I hate longs). */ if ((mz_uint64)(*pSource_len | *pDest_len) > 0xFFFFFFFFU) return MZ_PARAM_ERROR; stream.next_in = pSource; stream.avail_in = (mz_uint32)*pSource_len; stream.next_out = pDest; stream.avail_out = (mz_uint32)*pDest_len; status = mz_inflateInit(&stream); if (status != MZ_OK) return status; status = mz_inflate(&stream, MZ_FINISH); *pSource_len = *pSource_len - stream.avail_in; if (status != MZ_STREAM_END) { mz_inflateEnd(&stream); return ((status == MZ_BUF_ERROR) && (!stream.avail_in)) ? MZ_DATA_ERROR : status; } *pDest_len = stream.total_out; return mz_inflateEnd(&stream); } int mz_uncompress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len) { return mz_uncompress2(pDest, pDest_len, pSource, &source_len); } #endif /*#ifndef MINIZ_NO_INFLATE_APIS*/ const char *mz_error(int err) { static struct { int m_err; const char *m_pDesc; } s_error_descs[] = { { MZ_OK, "" }, { MZ_STREAM_END, "stream end" }, { MZ_NEED_DICT, "need dictionary" }, { MZ_ERRNO, "file error" }, { MZ_STREAM_ERROR, "stream error" }, { MZ_DATA_ERROR, "data error" }, { MZ_MEM_ERROR, "out of memory" }, { MZ_BUF_ERROR, "buf error" }, { MZ_VERSION_ERROR, "version error" }, { MZ_PARAM_ERROR, "parameter error" } }; mz_uint i; for (i = 0; i < sizeof(s_error_descs) / sizeof(s_error_descs[0]); ++i) if (s_error_descs[i].m_err == err) return s_error_descs[i].m_pDesc; return NULL; } #endif /*MINIZ_NO_ZLIB_APIS */ #ifdef __cplusplus } #endif /* This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to */ /************************************************************************** * * Copyright 2013-2014 RAD Game Tools and Valve Software * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC * All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * **************************************************************************/ #ifndef MINIZ_NO_DEFLATE_APIS #ifdef __cplusplus extern "C" { #endif /* ------------------- Low-level Compression (independent from all decompression API's) */ /* Purposely making these tables static for faster init and thread safety. */ static const mz_uint16 s_tdefl_len_sym[256] = { 257, 258, 259, 260, 261, 262, 263, 264, 265, 265, 266, 266, 267, 267, 268, 268, 269, 269, 269, 269, 270, 270, 270, 270, 271, 271, 271, 271, 272, 272, 272, 272, 273, 273, 273, 273, 273, 273, 273, 273, 274, 274, 274, 274, 274, 274, 274, 274, 275, 275, 275, 275, 275, 275, 275, 275, 276, 276, 276, 276, 276, 276, 276, 276, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 285 }; static const mz_uint8 s_tdefl_len_extra[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0 }; static const mz_uint8 s_tdefl_small_dist_sym[512] = { 0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17 }; static const mz_uint8 s_tdefl_small_dist_extra[512] = { 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 }; static const mz_uint8 s_tdefl_large_dist_sym[128] = { 0, 0, 18, 19, 20, 20, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29 }; static const mz_uint8 s_tdefl_large_dist_extra[128] = { 0, 0, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13 }; /* Radix sorts tdefl_sym_freq[] array by 16-bit key m_key. Returns ptr to sorted values. */ typedef struct { mz_uint16 m_key, m_sym_index; } tdefl_sym_freq; static tdefl_sym_freq *tdefl_radix_sort_syms(mz_uint num_syms, tdefl_sym_freq *pSyms0, tdefl_sym_freq *pSyms1) { mz_uint32 total_passes = 2, pass_shift, pass, i, hist[256 * 2]; tdefl_sym_freq *pCur_syms = pSyms0, *pNew_syms = pSyms1; MZ_CLEAR_ARR(hist); for (i = 0; i < num_syms; i++) { mz_uint freq = pSyms0[i].m_key; hist[freq & 0xFF]++; hist[256 + ((freq >> 8) & 0xFF)]++; } while ((total_passes > 1) && (num_syms == hist[(total_passes - 1) * 256])) total_passes--; for (pass_shift = 0, pass = 0; pass < total_passes; pass++, pass_shift += 8) { const mz_uint32 *pHist = &hist[pass << 8]; mz_uint offsets[256], cur_ofs = 0; for (i = 0; i < 256; i++) { offsets[i] = cur_ofs; cur_ofs += pHist[i]; } for (i = 0; i < num_syms; i++) pNew_syms[offsets[(pCur_syms[i].m_key >> pass_shift) & 0xFF]++] = pCur_syms[i]; { tdefl_sym_freq *t = pCur_syms; pCur_syms = pNew_syms; pNew_syms = t; } } return pCur_syms; } /* tdefl_calculate_minimum_redundancy() originally written by: Alistair Moffat, alistair@cs.mu.oz.au, Jyrki Katajainen, jyrki@diku.dk, November 1996. */ static void tdefl_calculate_minimum_redundancy(tdefl_sym_freq *A, int n) { int root, leaf, next, avbl, used, dpth; if (n == 0) return; else if (n == 1) { A[0].m_key = 1; return; } A[0].m_key += A[1].m_key; root = 0; leaf = 2; for (next = 1; next < n - 1; next++) { if (leaf >= n || A[root].m_key < A[leaf].m_key) { A[next].m_key = A[root].m_key; A[root++].m_key = (mz_uint16)next; } else A[next].m_key = A[leaf++].m_key; if (leaf >= n || (root < next && A[root].m_key < A[leaf].m_key)) { A[next].m_key = (mz_uint16)(A[next].m_key + A[root].m_key); A[root++].m_key = (mz_uint16)next; } else A[next].m_key = (mz_uint16)(A[next].m_key + A[leaf++].m_key); } A[n - 2].m_key = 0; for (next = n - 3; next >= 0; next--) A[next].m_key = A[A[next].m_key].m_key + 1; avbl = 1; used = dpth = 0; root = n - 2; next = n - 1; while (avbl > 0) { while (root >= 0 && (int)A[root].m_key == dpth) { used++; root--; } while (avbl > used) { A[next--].m_key = (mz_uint16)(dpth); avbl--; } avbl = 2 * used; dpth++; used = 0; } } /* Limits canonical Huffman code table's max code size. */ enum { TDEFL_MAX_SUPPORTED_HUFF_CODESIZE = 32 }; static void tdefl_huffman_enforce_max_code_size(int *pNum_codes, int code_list_len, int max_code_size) { int i; mz_uint32 total = 0; if (code_list_len <= 1) return; for (i = max_code_size + 1; i <= TDEFL_MAX_SUPPORTED_HUFF_CODESIZE; i++) pNum_codes[max_code_size] += pNum_codes[i]; for (i = max_code_size; i > 0; i--) total += (((mz_uint32)pNum_codes[i]) << (max_code_size - i)); while (total != (1UL << max_code_size)) { pNum_codes[max_code_size]--; for (i = max_code_size - 1; i > 0; i--) if (pNum_codes[i]) { pNum_codes[i]--; pNum_codes[i + 1] += 2; break; } total--; } } static void tdefl_optimize_huffman_table(tdefl_compressor *d, int table_num, int table_len, int code_size_limit, int static_table) { int i, j, l, num_codes[1 + TDEFL_MAX_SUPPORTED_HUFF_CODESIZE]; mz_uint next_code[TDEFL_MAX_SUPPORTED_HUFF_CODESIZE + 1]; MZ_CLEAR_ARR(num_codes); if (static_table) { for (i = 0; i < table_len; i++) num_codes[d->m_huff_code_sizes[table_num][i]]++; } else { tdefl_sym_freq syms0[TDEFL_MAX_HUFF_SYMBOLS], syms1[TDEFL_MAX_HUFF_SYMBOLS], *pSyms; int num_used_syms = 0; const mz_uint16 *pSym_count = &d->m_huff_count[table_num][0]; for (i = 0; i < table_len; i++) if (pSym_count[i]) { syms0[num_used_syms].m_key = (mz_uint16)pSym_count[i]; syms0[num_used_syms++].m_sym_index = (mz_uint16)i; } pSyms = tdefl_radix_sort_syms(num_used_syms, syms0, syms1); tdefl_calculate_minimum_redundancy(pSyms, num_used_syms); for (i = 0; i < num_used_syms; i++) num_codes[pSyms[i].m_key]++; tdefl_huffman_enforce_max_code_size(num_codes, num_used_syms, code_size_limit); MZ_CLEAR_ARR(d->m_huff_code_sizes[table_num]); MZ_CLEAR_ARR(d->m_huff_codes[table_num]); for (i = 1, j = num_used_syms; i <= code_size_limit; i++) for (l = num_codes[i]; l > 0; l--) d->m_huff_code_sizes[table_num][pSyms[--j].m_sym_index] = (mz_uint8)(i); } next_code[1] = 0; for (j = 0, i = 2; i <= code_size_limit; i++) next_code[i] = j = ((j + num_codes[i - 1]) << 1); for (i = 0; i < table_len; i++) { mz_uint rev_code = 0, code, code_size; if ((code_size = d->m_huff_code_sizes[table_num][i]) == 0) continue; code = next_code[code_size]++; for (l = code_size; l > 0; l--, code >>= 1) rev_code = (rev_code << 1) | (code & 1); d->m_huff_codes[table_num][i] = (mz_uint16)rev_code; } } #define TDEFL_PUT_BITS(b, l) \ do \ { \ mz_uint bits = b; \ mz_uint len = l; \ MZ_ASSERT(bits <= ((1U << len) - 1U)); \ d->m_bit_buffer |= (bits << d->m_bits_in); \ d->m_bits_in += len; \ while (d->m_bits_in >= 8) \ { \ if (d->m_pOutput_buf < d->m_pOutput_buf_end) \ *d->m_pOutput_buf++ = (mz_uint8)(d->m_bit_buffer); \ d->m_bit_buffer >>= 8; \ d->m_bits_in -= 8; \ } \ } \ MZ_MACRO_END #define TDEFL_RLE_PREV_CODE_SIZE() \ { \ if (rle_repeat_count) \ { \ if (rle_repeat_count < 3) \ { \ d->m_huff_count[2][prev_code_size] = (mz_uint16)(d->m_huff_count[2][prev_code_size] + rle_repeat_count); \ while (rle_repeat_count--) \ packed_code_sizes[num_packed_code_sizes++] = prev_code_size; \ } \ else \ { \ d->m_huff_count[2][16] = (mz_uint16)(d->m_huff_count[2][16] + 1); \ packed_code_sizes[num_packed_code_sizes++] = 16; \ packed_code_sizes[num_packed_code_sizes++] = (mz_uint8)(rle_repeat_count - 3); \ } \ rle_repeat_count = 0; \ } \ } #define TDEFL_RLE_ZERO_CODE_SIZE() \ { \ if (rle_z_count) \ { \ if (rle_z_count < 3) \ { \ d->m_huff_count[2][0] = (mz_uint16)(d->m_huff_count[2][0] + rle_z_count); \ while (rle_z_count--) \ packed_code_sizes[num_packed_code_sizes++] = 0; \ } \ else if (rle_z_count <= 10) \ { \ d->m_huff_count[2][17] = (mz_uint16)(d->m_huff_count[2][17] + 1); \ packed_code_sizes[num_packed_code_sizes++] = 17; \ packed_code_sizes[num_packed_code_sizes++] = (mz_uint8)(rle_z_count - 3); \ } \ else \ { \ d->m_huff_count[2][18] = (mz_uint16)(d->m_huff_count[2][18] + 1); \ packed_code_sizes[num_packed_code_sizes++] = 18; \ packed_code_sizes[num_packed_code_sizes++] = (mz_uint8)(rle_z_count - 11); \ } \ rle_z_count = 0; \ } \ } static const mz_uint8 s_tdefl_packed_code_size_syms_swizzle[] = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; static void tdefl_start_dynamic_block(tdefl_compressor *d) { int num_lit_codes, num_dist_codes, num_bit_lengths; mz_uint i, total_code_sizes_to_pack, num_packed_code_sizes, rle_z_count, rle_repeat_count, packed_code_sizes_index; mz_uint8 code_sizes_to_pack[TDEFL_MAX_HUFF_SYMBOLS_0 + TDEFL_MAX_HUFF_SYMBOLS_1], packed_code_sizes[TDEFL_MAX_HUFF_SYMBOLS_0 + TDEFL_MAX_HUFF_SYMBOLS_1], prev_code_size = 0xFF; d->m_huff_count[0][256] = 1; tdefl_optimize_huffman_table(d, 0, TDEFL_MAX_HUFF_SYMBOLS_0, 15, MZ_FALSE); tdefl_optimize_huffman_table(d, 1, TDEFL_MAX_HUFF_SYMBOLS_1, 15, MZ_FALSE); for (num_lit_codes = 286; num_lit_codes > 257; num_lit_codes--) if (d->m_huff_code_sizes[0][num_lit_codes - 1]) break; for (num_dist_codes = 30; num_dist_codes > 1; num_dist_codes--) if (d->m_huff_code_sizes[1][num_dist_codes - 1]) break; memcpy(code_sizes_to_pack, &d->m_huff_code_sizes[0][0], num_lit_codes); memcpy(code_sizes_to_pack + num_lit_codes, &d->m_huff_code_sizes[1][0], num_dist_codes); total_code_sizes_to_pack = num_lit_codes + num_dist_codes; num_packed_code_sizes = 0; rle_z_count = 0; rle_repeat_count = 0; memset(&d->m_huff_count[2][0], 0, sizeof(d->m_huff_count[2][0]) * TDEFL_MAX_HUFF_SYMBOLS_2); for (i = 0; i < total_code_sizes_to_pack; i++) { mz_uint8 code_size = code_sizes_to_pack[i]; if (!code_size) { TDEFL_RLE_PREV_CODE_SIZE(); if (++rle_z_count == 138) { TDEFL_RLE_ZERO_CODE_SIZE(); } } else { TDEFL_RLE_ZERO_CODE_SIZE(); if (code_size != prev_code_size) { TDEFL_RLE_PREV_CODE_SIZE(); d->m_huff_count[2][code_size] = (mz_uint16)(d->m_huff_count[2][code_size] + 1); packed_code_sizes[num_packed_code_sizes++] = code_size; } else if (++rle_repeat_count == 6) { TDEFL_RLE_PREV_CODE_SIZE(); } } prev_code_size = code_size; } if (rle_repeat_count) { TDEFL_RLE_PREV_CODE_SIZE(); } else { TDEFL_RLE_ZERO_CODE_SIZE(); } tdefl_optimize_huffman_table(d, 2, TDEFL_MAX_HUFF_SYMBOLS_2, 7, MZ_FALSE); TDEFL_PUT_BITS(2, 2); TDEFL_PUT_BITS(num_lit_codes - 257, 5); TDEFL_PUT_BITS(num_dist_codes - 1, 5); for (num_bit_lengths = 18; num_bit_lengths >= 0; num_bit_lengths--) if (d->m_huff_code_sizes[2][s_tdefl_packed_code_size_syms_swizzle[num_bit_lengths]]) break; num_bit_lengths = MZ_MAX(4, (num_bit_lengths + 1)); TDEFL_PUT_BITS(num_bit_lengths - 4, 4); for (i = 0; (int)i < num_bit_lengths; i++) TDEFL_PUT_BITS(d->m_huff_code_sizes[2][s_tdefl_packed_code_size_syms_swizzle[i]], 3); for (packed_code_sizes_index = 0; packed_code_sizes_index < num_packed_code_sizes;) { mz_uint code = packed_code_sizes[packed_code_sizes_index++]; MZ_ASSERT(code < TDEFL_MAX_HUFF_SYMBOLS_2); TDEFL_PUT_BITS(d->m_huff_codes[2][code], d->m_huff_code_sizes[2][code]); if (code >= 16) TDEFL_PUT_BITS(packed_code_sizes[packed_code_sizes_index++], "\02\03\07"[code - 16]); } } static void tdefl_start_static_block(tdefl_compressor *d) { mz_uint i; mz_uint8 *p = &d->m_huff_code_sizes[0][0]; for (i = 0; i <= 143; ++i) *p++ = 8; for (; i <= 255; ++i) *p++ = 9; for (; i <= 279; ++i) *p++ = 7; for (; i <= 287; ++i) *p++ = 8; memset(d->m_huff_code_sizes[1], 5, 32); tdefl_optimize_huffman_table(d, 0, 288, 15, MZ_TRUE); tdefl_optimize_huffman_table(d, 1, 32, 15, MZ_TRUE); TDEFL_PUT_BITS(1, 2); } static const mz_uint mz_bitmasks[17] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF }; #if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN && MINIZ_HAS_64BIT_REGISTERS static mz_bool tdefl_compress_lz_codes(tdefl_compressor *d) { mz_uint flags; mz_uint8 *pLZ_codes; mz_uint8 *pOutput_buf = d->m_pOutput_buf; mz_uint8 *pLZ_code_buf_end = d->m_pLZ_code_buf; mz_uint64 bit_buffer = d->m_bit_buffer; mz_uint bits_in = d->m_bits_in; #define TDEFL_PUT_BITS_FAST(b, l) \ { \ bit_buffer |= (((mz_uint64)(b)) << bits_in); \ bits_in += (l); \ } flags = 1; for (pLZ_codes = d->m_lz_code_buf; pLZ_codes < pLZ_code_buf_end; flags >>= 1) { if (flags == 1) flags = *pLZ_codes++ | 0x100; if (flags & 1) { mz_uint s0, s1, n0, n1, sym, num_extra_bits; mz_uint match_len = pLZ_codes[0]; mz_uint match_dist = (pLZ_codes[1] | (pLZ_codes[2] << 8)); pLZ_codes += 3; MZ_ASSERT(d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]); TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][s_tdefl_len_sym[match_len]], d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]); TDEFL_PUT_BITS_FAST(match_len & mz_bitmasks[s_tdefl_len_extra[match_len]], s_tdefl_len_extra[match_len]); /* This sequence coaxes MSVC into using cmov's vs. jmp's. */ s0 = s_tdefl_small_dist_sym[match_dist & 511]; n0 = s_tdefl_small_dist_extra[match_dist & 511]; s1 = s_tdefl_large_dist_sym[match_dist >> 8]; n1 = s_tdefl_large_dist_extra[match_dist >> 8]; sym = (match_dist < 512) ? s0 : s1; num_extra_bits = (match_dist < 512) ? n0 : n1; MZ_ASSERT(d->m_huff_code_sizes[1][sym]); TDEFL_PUT_BITS_FAST(d->m_huff_codes[1][sym], d->m_huff_code_sizes[1][sym]); TDEFL_PUT_BITS_FAST(match_dist & mz_bitmasks[num_extra_bits], num_extra_bits); } else { mz_uint lit = *pLZ_codes++; MZ_ASSERT(d->m_huff_code_sizes[0][lit]); TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]); if (((flags & 2) == 0) && (pLZ_codes < pLZ_code_buf_end)) { flags >>= 1; lit = *pLZ_codes++; MZ_ASSERT(d->m_huff_code_sizes[0][lit]); TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]); if (((flags & 2) == 0) && (pLZ_codes < pLZ_code_buf_end)) { flags >>= 1; lit = *pLZ_codes++; MZ_ASSERT(d->m_huff_code_sizes[0][lit]); TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]); } } } if (pOutput_buf >= d->m_pOutput_buf_end) return MZ_FALSE; memcpy(pOutput_buf, &bit_buffer, sizeof(mz_uint64)); pOutput_buf += (bits_in >> 3); bit_buffer >>= (bits_in & ~7); bits_in &= 7; } #undef TDEFL_PUT_BITS_FAST d->m_pOutput_buf = pOutput_buf; d->m_bits_in = 0; d->m_bit_buffer = 0; while (bits_in) { mz_uint32 n = MZ_MIN(bits_in, 16); TDEFL_PUT_BITS((mz_uint)bit_buffer & mz_bitmasks[n], n); bit_buffer >>= n; bits_in -= n; } TDEFL_PUT_BITS(d->m_huff_codes[0][256], d->m_huff_code_sizes[0][256]); return (d->m_pOutput_buf < d->m_pOutput_buf_end); } #else static mz_bool tdefl_compress_lz_codes(tdefl_compressor *d) { mz_uint flags; mz_uint8 *pLZ_codes; flags = 1; for (pLZ_codes = d->m_lz_code_buf; pLZ_codes < d->m_pLZ_code_buf; flags >>= 1) { if (flags == 1) flags = *pLZ_codes++ | 0x100; if (flags & 1) { mz_uint sym, num_extra_bits; mz_uint match_len = pLZ_codes[0], match_dist = (pLZ_codes[1] | (pLZ_codes[2] << 8)); pLZ_codes += 3; MZ_ASSERT(d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]); TDEFL_PUT_BITS(d->m_huff_codes[0][s_tdefl_len_sym[match_len]], d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]); TDEFL_PUT_BITS(match_len & mz_bitmasks[s_tdefl_len_extra[match_len]], s_tdefl_len_extra[match_len]); if (match_dist < 512) { sym = s_tdefl_small_dist_sym[match_dist]; num_extra_bits = s_tdefl_small_dist_extra[match_dist]; } else { sym = s_tdefl_large_dist_sym[match_dist >> 8]; num_extra_bits = s_tdefl_large_dist_extra[match_dist >> 8]; } MZ_ASSERT(d->m_huff_code_sizes[1][sym]); TDEFL_PUT_BITS(d->m_huff_codes[1][sym], d->m_huff_code_sizes[1][sym]); TDEFL_PUT_BITS(match_dist & mz_bitmasks[num_extra_bits], num_extra_bits); } else { mz_uint lit = *pLZ_codes++; MZ_ASSERT(d->m_huff_code_sizes[0][lit]); TDEFL_PUT_BITS(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]); } } TDEFL_PUT_BITS(d->m_huff_codes[0][256], d->m_huff_code_sizes[0][256]); return (d->m_pOutput_buf < d->m_pOutput_buf_end); } #endif /* MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN && MINIZ_HAS_64BIT_REGISTERS */ static mz_bool tdefl_compress_block(tdefl_compressor *d, mz_bool static_block) { if (static_block) tdefl_start_static_block(d); else tdefl_start_dynamic_block(d); return tdefl_compress_lz_codes(d); } static const mz_uint s_tdefl_num_probes[11]; static int tdefl_flush_block(tdefl_compressor *d, int flush) { mz_uint saved_bit_buf, saved_bits_in; mz_uint8 *pSaved_output_buf; mz_bool comp_block_succeeded = MZ_FALSE; int n, use_raw_block = ((d->m_flags & TDEFL_FORCE_ALL_RAW_BLOCKS) != 0) && (d->m_lookahead_pos - d->m_lz_code_buf_dict_pos) <= d->m_dict_size; mz_uint8 *pOutput_buf_start = ((d->m_pPut_buf_func == NULL) && ((*d->m_pOut_buf_size - d->m_out_buf_ofs) >= TDEFL_OUT_BUF_SIZE)) ? ((mz_uint8 *)d->m_pOut_buf + d->m_out_buf_ofs) : d->m_output_buf; d->m_pOutput_buf = pOutput_buf_start; d->m_pOutput_buf_end = d->m_pOutput_buf + TDEFL_OUT_BUF_SIZE - 16; MZ_ASSERT(!d->m_output_flush_remaining); d->m_output_flush_ofs = 0; d->m_output_flush_remaining = 0; *d->m_pLZ_flags = (mz_uint8)(*d->m_pLZ_flags >> d->m_num_flags_left); d->m_pLZ_code_buf -= (d->m_num_flags_left == 8); if ((d->m_flags & TDEFL_WRITE_ZLIB_HEADER) && (!d->m_block_index)) { const mz_uint8 cmf = 0x78; mz_uint8 flg, flevel = 3; mz_uint header, i, mz_un = sizeof(s_tdefl_num_probes) / sizeof(mz_uint); /* Determine compression level by reversing the process in tdefl_create_comp_flags_from_zip_params() */ for (i = 0; i < mz_un; i++) if (s_tdefl_num_probes[i] == (d->m_flags & 0xFFF)) break; if (i < 2) flevel = 0; else if (i < 6) flevel = 1; else if (i == 6) flevel = 2; header = cmf << 8 | (flevel << 6); header += 31 - (header % 31); flg = header & 0xFF; TDEFL_PUT_BITS(cmf, 8); TDEFL_PUT_BITS(flg, 8); } TDEFL_PUT_BITS(flush == TDEFL_FINISH, 1); pSaved_output_buf = d->m_pOutput_buf; saved_bit_buf = d->m_bit_buffer; saved_bits_in = d->m_bits_in; if (!use_raw_block) comp_block_succeeded = tdefl_compress_block(d, (d->m_flags & TDEFL_FORCE_ALL_STATIC_BLOCKS) || (d->m_total_lz_bytes < 48)); /* If the block gets expanded, forget the current contents of the output buffer and send a raw block instead. */ if (((use_raw_block) || ((d->m_total_lz_bytes) && ((d->m_pOutput_buf - pSaved_output_buf + 1U) >= d->m_total_lz_bytes))) && ((d->m_lookahead_pos - d->m_lz_code_buf_dict_pos) <= d->m_dict_size)) { mz_uint i; d->m_pOutput_buf = pSaved_output_buf; d->m_bit_buffer = saved_bit_buf, d->m_bits_in = saved_bits_in; TDEFL_PUT_BITS(0, 2); if (d->m_bits_in) { TDEFL_PUT_BITS(0, 8 - d->m_bits_in); } for (i = 2; i; --i, d->m_total_lz_bytes ^= 0xFFFF) { TDEFL_PUT_BITS(d->m_total_lz_bytes & 0xFFFF, 16); } for (i = 0; i < d->m_total_lz_bytes; ++i) { TDEFL_PUT_BITS(d->m_dict[(d->m_lz_code_buf_dict_pos + i) & TDEFL_LZ_DICT_SIZE_MASK], 8); } } /* Check for the extremely unlikely (if not impossible) case of the compressed block not fitting into the output buffer when using dynamic codes. */ else if (!comp_block_succeeded) { d->m_pOutput_buf = pSaved_output_buf; d->m_bit_buffer = saved_bit_buf, d->m_bits_in = saved_bits_in; tdefl_compress_block(d, MZ_TRUE); } if (flush) { if (flush == TDEFL_FINISH) { if (d->m_bits_in) { TDEFL_PUT_BITS(0, 8 - d->m_bits_in); } if (d->m_flags & TDEFL_WRITE_ZLIB_HEADER) { mz_uint i, a = d->m_adler32; for (i = 0; i < 4; i++) { TDEFL_PUT_BITS((a >> 24) & 0xFF, 8); a <<= 8; } } } else { mz_uint i, z = 0; TDEFL_PUT_BITS(0, 3); if (d->m_bits_in) { TDEFL_PUT_BITS(0, 8 - d->m_bits_in); } for (i = 2; i; --i, z ^= 0xFFFF) { TDEFL_PUT_BITS(z & 0xFFFF, 16); } } } MZ_ASSERT(d->m_pOutput_buf < d->m_pOutput_buf_end); memset(&d->m_huff_count[0][0], 0, sizeof(d->m_huff_count[0][0]) * TDEFL_MAX_HUFF_SYMBOLS_0); memset(&d->m_huff_count[1][0], 0, sizeof(d->m_huff_count[1][0]) * TDEFL_MAX_HUFF_SYMBOLS_1); d->m_pLZ_code_buf = d->m_lz_code_buf + 1; d->m_pLZ_flags = d->m_lz_code_buf; d->m_num_flags_left = 8; d->m_lz_code_buf_dict_pos += d->m_total_lz_bytes; d->m_total_lz_bytes = 0; d->m_block_index++; if ((n = (int)(d->m_pOutput_buf - pOutput_buf_start)) != 0) { if (d->m_pPut_buf_func) { *d->m_pIn_buf_size = d->m_pSrc - (const mz_uint8 *)d->m_pIn_buf; if (!(*d->m_pPut_buf_func)(d->m_output_buf, n, d->m_pPut_buf_user)) return (d->m_prev_return_status = TDEFL_STATUS_PUT_BUF_FAILED); } else if (pOutput_buf_start == d->m_output_buf) { int bytes_to_copy = (int)MZ_MIN((size_t)n, (size_t)(*d->m_pOut_buf_size - d->m_out_buf_ofs)); memcpy((mz_uint8 *)d->m_pOut_buf + d->m_out_buf_ofs, d->m_output_buf, bytes_to_copy); d->m_out_buf_ofs += bytes_to_copy; if ((n -= bytes_to_copy) != 0) { d->m_output_flush_ofs = bytes_to_copy; d->m_output_flush_remaining = n; } } else { d->m_out_buf_ofs += n; } } return d->m_output_flush_remaining; } #if MINIZ_USE_UNALIGNED_LOADS_AND_STORES #ifdef MINIZ_UNALIGNED_USE_MEMCPY static mz_uint16 TDEFL_READ_UNALIGNED_WORD(const mz_uint8* p) { mz_uint16 ret; memcpy(&ret, p, sizeof(mz_uint16)); return ret; } static mz_uint16 TDEFL_READ_UNALIGNED_WORD2(const mz_uint16* p) { mz_uint16 ret; memcpy(&ret, p, sizeof(mz_uint16)); return ret; } #else #define TDEFL_READ_UNALIGNED_WORD(p) *(const mz_uint16 *)(p) #define TDEFL_READ_UNALIGNED_WORD2(p) *(const mz_uint16 *)(p) #endif static MZ_FORCEINLINE void tdefl_find_match(tdefl_compressor *d, mz_uint lookahead_pos, mz_uint max_dist, mz_uint max_match_len, mz_uint *pMatch_dist, mz_uint *pMatch_len) { mz_uint dist, pos = lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK, match_len = *pMatch_len, probe_pos = pos, next_probe_pos, probe_len; mz_uint num_probes_left = d->m_max_probes[match_len >= 32]; const mz_uint16 *s = (const mz_uint16 *)(d->m_dict + pos), *p, *q; mz_uint16 c01 = TDEFL_READ_UNALIGNED_WORD(&d->m_dict[pos + match_len - 1]), s01 = TDEFL_READ_UNALIGNED_WORD2(s); MZ_ASSERT(max_match_len <= TDEFL_MAX_MATCH_LEN); if (max_match_len <= match_len) return; for (;;) { for (;;) { if (--num_probes_left == 0) return; #define TDEFL_PROBE \ next_probe_pos = d->m_next[probe_pos]; \ if ((!next_probe_pos) || ((dist = (mz_uint16)(lookahead_pos - next_probe_pos)) > max_dist)) \ return; \ probe_pos = next_probe_pos & TDEFL_LZ_DICT_SIZE_MASK; \ if (TDEFL_READ_UNALIGNED_WORD(&d->m_dict[probe_pos + match_len - 1]) == c01) \ break; TDEFL_PROBE; TDEFL_PROBE; TDEFL_PROBE; } if (!dist) break; q = (const mz_uint16 *)(d->m_dict + probe_pos); if (TDEFL_READ_UNALIGNED_WORD2(q) != s01) continue; p = s; probe_len = 32; do { } while ((TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (--probe_len > 0)); if (!probe_len) { *pMatch_dist = dist; *pMatch_len = MZ_MIN(max_match_len, (mz_uint)TDEFL_MAX_MATCH_LEN); break; } else if ((probe_len = ((mz_uint)(p - s) * 2) + (mz_uint)(*(const mz_uint8 *)p == *(const mz_uint8 *)q)) > match_len) { *pMatch_dist = dist; if ((*pMatch_len = match_len = MZ_MIN(max_match_len, probe_len)) == max_match_len) break; c01 = TDEFL_READ_UNALIGNED_WORD(&d->m_dict[pos + match_len - 1]); } } } #else static MZ_FORCEINLINE void tdefl_find_match(tdefl_compressor *d, mz_uint lookahead_pos, mz_uint max_dist, mz_uint max_match_len, mz_uint *pMatch_dist, mz_uint *pMatch_len) { mz_uint dist, pos = lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK, match_len = *pMatch_len, probe_pos = pos, next_probe_pos, probe_len; mz_uint num_probes_left = d->m_max_probes[match_len >= 32]; const mz_uint8 *s = d->m_dict + pos, *p, *q; mz_uint8 c0 = d->m_dict[pos + match_len], c1 = d->m_dict[pos + match_len - 1]; MZ_ASSERT(max_match_len <= TDEFL_MAX_MATCH_LEN); if (max_match_len <= match_len) return; for (;;) { for (;;) { if (--num_probes_left == 0) return; #define TDEFL_PROBE \ next_probe_pos = d->m_next[probe_pos]; \ if ((!next_probe_pos) || ((dist = (mz_uint16)(lookahead_pos - next_probe_pos)) > max_dist)) \ return; \ probe_pos = next_probe_pos & TDEFL_LZ_DICT_SIZE_MASK; \ if ((d->m_dict[probe_pos + match_len] == c0) && (d->m_dict[probe_pos + match_len - 1] == c1)) \ break; TDEFL_PROBE; TDEFL_PROBE; TDEFL_PROBE; } if (!dist) break; p = s; q = d->m_dict + probe_pos; for (probe_len = 0; probe_len < max_match_len; probe_len++) if (*p++ != *q++) break; if (probe_len > match_len) { *pMatch_dist = dist; if ((*pMatch_len = match_len = probe_len) == max_match_len) return; c0 = d->m_dict[pos + match_len]; c1 = d->m_dict[pos + match_len - 1]; } } } #endif /* #if MINIZ_USE_UNALIGNED_LOADS_AND_STORES */ #if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN #ifdef MINIZ_UNALIGNED_USE_MEMCPY static mz_uint32 TDEFL_READ_UNALIGNED_WORD32(const mz_uint8* p) { mz_uint32 ret; memcpy(&ret, p, sizeof(mz_uint32)); return ret; } #else #define TDEFL_READ_UNALIGNED_WORD32(p) *(const mz_uint32 *)(p) #endif static mz_bool tdefl_compress_fast(tdefl_compressor *d) { /* Faster, minimally featured LZRW1-style match+parse loop with better register utilization. Intended for applications where raw throughput is valued more highly than ratio. */ mz_uint lookahead_pos = d->m_lookahead_pos, lookahead_size = d->m_lookahead_size, dict_size = d->m_dict_size, total_lz_bytes = d->m_total_lz_bytes, num_flags_left = d->m_num_flags_left; mz_uint8 *pLZ_code_buf = d->m_pLZ_code_buf, *pLZ_flags = d->m_pLZ_flags; mz_uint cur_pos = lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK; while ((d->m_src_buf_left) || ((d->m_flush) && (lookahead_size))) { const mz_uint TDEFL_COMP_FAST_LOOKAHEAD_SIZE = 4096; mz_uint dst_pos = (lookahead_pos + lookahead_size) & TDEFL_LZ_DICT_SIZE_MASK; mz_uint num_bytes_to_process = (mz_uint)MZ_MIN(d->m_src_buf_left, TDEFL_COMP_FAST_LOOKAHEAD_SIZE - lookahead_size); d->m_src_buf_left -= num_bytes_to_process; lookahead_size += num_bytes_to_process; while (num_bytes_to_process) { mz_uint32 n = MZ_MIN(TDEFL_LZ_DICT_SIZE - dst_pos, num_bytes_to_process); memcpy(d->m_dict + dst_pos, d->m_pSrc, n); if (dst_pos < (TDEFL_MAX_MATCH_LEN - 1)) memcpy(d->m_dict + TDEFL_LZ_DICT_SIZE + dst_pos, d->m_pSrc, MZ_MIN(n, (TDEFL_MAX_MATCH_LEN - 1) - dst_pos)); d->m_pSrc += n; dst_pos = (dst_pos + n) & TDEFL_LZ_DICT_SIZE_MASK; num_bytes_to_process -= n; } dict_size = MZ_MIN(TDEFL_LZ_DICT_SIZE - lookahead_size, dict_size); if ((!d->m_flush) && (lookahead_size < TDEFL_COMP_FAST_LOOKAHEAD_SIZE)) break; while (lookahead_size >= 4) { mz_uint cur_match_dist, cur_match_len = 1; mz_uint8 *pCur_dict = d->m_dict + cur_pos; mz_uint first_trigram = TDEFL_READ_UNALIGNED_WORD32(pCur_dict) & 0xFFFFFF; mz_uint hash = (first_trigram ^ (first_trigram >> (24 - (TDEFL_LZ_HASH_BITS - 8)))) & TDEFL_LEVEL1_HASH_SIZE_MASK; mz_uint probe_pos = d->m_hash[hash]; d->m_hash[hash] = (mz_uint16)lookahead_pos; if (((cur_match_dist = (mz_uint16)(lookahead_pos - probe_pos)) <= dict_size) && ((TDEFL_READ_UNALIGNED_WORD32(d->m_dict + (probe_pos &= TDEFL_LZ_DICT_SIZE_MASK)) & 0xFFFFFF) == first_trigram)) { const mz_uint16 *p = (const mz_uint16 *)pCur_dict; const mz_uint16 *q = (const mz_uint16 *)(d->m_dict + probe_pos); mz_uint32 probe_len = 32; do { } while ((TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (--probe_len > 0)); cur_match_len = ((mz_uint)(p - (const mz_uint16 *)pCur_dict) * 2) + (mz_uint)(*(const mz_uint8 *)p == *(const mz_uint8 *)q); if (!probe_len) cur_match_len = cur_match_dist ? TDEFL_MAX_MATCH_LEN : 0; if ((cur_match_len < TDEFL_MIN_MATCH_LEN) || ((cur_match_len == TDEFL_MIN_MATCH_LEN) && (cur_match_dist >= 8U * 1024U))) { cur_match_len = 1; *pLZ_code_buf++ = (mz_uint8)first_trigram; *pLZ_flags = (mz_uint8)(*pLZ_flags >> 1); d->m_huff_count[0][(mz_uint8)first_trigram]++; } else { mz_uint32 s0, s1; cur_match_len = MZ_MIN(cur_match_len, lookahead_size); MZ_ASSERT((cur_match_len >= TDEFL_MIN_MATCH_LEN) && (cur_match_dist >= 1) && (cur_match_dist <= TDEFL_LZ_DICT_SIZE)); cur_match_dist--; pLZ_code_buf[0] = (mz_uint8)(cur_match_len - TDEFL_MIN_MATCH_LEN); #ifdef MINIZ_UNALIGNED_USE_MEMCPY memcpy(&pLZ_code_buf[1], &cur_match_dist, sizeof(cur_match_dist)); #else *(mz_uint16 *)(&pLZ_code_buf[1]) = (mz_uint16)cur_match_dist; #endif pLZ_code_buf += 3; *pLZ_flags = (mz_uint8)((*pLZ_flags >> 1) | 0x80); s0 = s_tdefl_small_dist_sym[cur_match_dist & 511]; s1 = s_tdefl_large_dist_sym[cur_match_dist >> 8]; d->m_huff_count[1][(cur_match_dist < 512) ? s0 : s1]++; d->m_huff_count[0][s_tdefl_len_sym[cur_match_len - TDEFL_MIN_MATCH_LEN]]++; } } else { *pLZ_code_buf++ = (mz_uint8)first_trigram; *pLZ_flags = (mz_uint8)(*pLZ_flags >> 1); d->m_huff_count[0][(mz_uint8)first_trigram]++; } if (--num_flags_left == 0) { num_flags_left = 8; pLZ_flags = pLZ_code_buf++; } total_lz_bytes += cur_match_len; lookahead_pos += cur_match_len; dict_size = MZ_MIN(dict_size + cur_match_len, (mz_uint)TDEFL_LZ_DICT_SIZE); cur_pos = (cur_pos + cur_match_len) & TDEFL_LZ_DICT_SIZE_MASK; MZ_ASSERT(lookahead_size >= cur_match_len); lookahead_size -= cur_match_len; if (pLZ_code_buf > &d->m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE - 8]) { int n; d->m_lookahead_pos = lookahead_pos; d->m_lookahead_size = lookahead_size; d->m_dict_size = dict_size; d->m_total_lz_bytes = total_lz_bytes; d->m_pLZ_code_buf = pLZ_code_buf; d->m_pLZ_flags = pLZ_flags; d->m_num_flags_left = num_flags_left; if ((n = tdefl_flush_block(d, 0)) != 0) return (n < 0) ? MZ_FALSE : MZ_TRUE; total_lz_bytes = d->m_total_lz_bytes; pLZ_code_buf = d->m_pLZ_code_buf; pLZ_flags = d->m_pLZ_flags; num_flags_left = d->m_num_flags_left; } } while (lookahead_size) { mz_uint8 lit = d->m_dict[cur_pos]; total_lz_bytes++; *pLZ_code_buf++ = lit; *pLZ_flags = (mz_uint8)(*pLZ_flags >> 1); if (--num_flags_left == 0) { num_flags_left = 8; pLZ_flags = pLZ_code_buf++; } d->m_huff_count[0][lit]++; lookahead_pos++; dict_size = MZ_MIN(dict_size + 1, (mz_uint)TDEFL_LZ_DICT_SIZE); cur_pos = (cur_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK; lookahead_size--; if (pLZ_code_buf > &d->m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE - 8]) { int n; d->m_lookahead_pos = lookahead_pos; d->m_lookahead_size = lookahead_size; d->m_dict_size = dict_size; d->m_total_lz_bytes = total_lz_bytes; d->m_pLZ_code_buf = pLZ_code_buf; d->m_pLZ_flags = pLZ_flags; d->m_num_flags_left = num_flags_left; if ((n = tdefl_flush_block(d, 0)) != 0) return (n < 0) ? MZ_FALSE : MZ_TRUE; total_lz_bytes = d->m_total_lz_bytes; pLZ_code_buf = d->m_pLZ_code_buf; pLZ_flags = d->m_pLZ_flags; num_flags_left = d->m_num_flags_left; } } } d->m_lookahead_pos = lookahead_pos; d->m_lookahead_size = lookahead_size; d->m_dict_size = dict_size; d->m_total_lz_bytes = total_lz_bytes; d->m_pLZ_code_buf = pLZ_code_buf; d->m_pLZ_flags = pLZ_flags; d->m_num_flags_left = num_flags_left; return MZ_TRUE; } #endif /* MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN */ static MZ_FORCEINLINE void tdefl_record_literal(tdefl_compressor *d, mz_uint8 lit) { d->m_total_lz_bytes++; *d->m_pLZ_code_buf++ = lit; *d->m_pLZ_flags = (mz_uint8)(*d->m_pLZ_flags >> 1); if (--d->m_num_flags_left == 0) { d->m_num_flags_left = 8; d->m_pLZ_flags = d->m_pLZ_code_buf++; } d->m_huff_count[0][lit]++; } static MZ_FORCEINLINE void tdefl_record_match(tdefl_compressor *d, mz_uint match_len, mz_uint match_dist) { mz_uint32 s0, s1; MZ_ASSERT((match_len >= TDEFL_MIN_MATCH_LEN) && (match_dist >= 1) && (match_dist <= TDEFL_LZ_DICT_SIZE)); d->m_total_lz_bytes += match_len; d->m_pLZ_code_buf[0] = (mz_uint8)(match_len - TDEFL_MIN_MATCH_LEN); match_dist -= 1; d->m_pLZ_code_buf[1] = (mz_uint8)(match_dist & 0xFF); d->m_pLZ_code_buf[2] = (mz_uint8)(match_dist >> 8); d->m_pLZ_code_buf += 3; *d->m_pLZ_flags = (mz_uint8)((*d->m_pLZ_flags >> 1) | 0x80); if (--d->m_num_flags_left == 0) { d->m_num_flags_left = 8; d->m_pLZ_flags = d->m_pLZ_code_buf++; } s0 = s_tdefl_small_dist_sym[match_dist & 511]; s1 = s_tdefl_large_dist_sym[(match_dist >> 8) & 127]; d->m_huff_count[1][(match_dist < 512) ? s0 : s1]++; d->m_huff_count[0][s_tdefl_len_sym[match_len - TDEFL_MIN_MATCH_LEN]]++; } static mz_bool tdefl_compress_normal(tdefl_compressor *d) { const mz_uint8 *pSrc = d->m_pSrc; size_t src_buf_left = d->m_src_buf_left; tdefl_flush flush = d->m_flush; while ((src_buf_left) || ((flush) && (d->m_lookahead_size))) { mz_uint len_to_move, cur_match_dist, cur_match_len, cur_pos; /* Update dictionary and hash chains. Keeps the lookahead size equal to TDEFL_MAX_MATCH_LEN. */ if ((d->m_lookahead_size + d->m_dict_size) >= (TDEFL_MIN_MATCH_LEN - 1)) { mz_uint dst_pos = (d->m_lookahead_pos + d->m_lookahead_size) & TDEFL_LZ_DICT_SIZE_MASK, ins_pos = d->m_lookahead_pos + d->m_lookahead_size - 2; mz_uint hash = (d->m_dict[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] << TDEFL_LZ_HASH_SHIFT) ^ d->m_dict[(ins_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK]; mz_uint num_bytes_to_process = (mz_uint)MZ_MIN(src_buf_left, TDEFL_MAX_MATCH_LEN - d->m_lookahead_size); const mz_uint8 *pSrc_end = pSrc ? pSrc + num_bytes_to_process : NULL; src_buf_left -= num_bytes_to_process; d->m_lookahead_size += num_bytes_to_process; while (pSrc != pSrc_end) { mz_uint8 c = *pSrc++; d->m_dict[dst_pos] = c; if (dst_pos < (TDEFL_MAX_MATCH_LEN - 1)) d->m_dict[TDEFL_LZ_DICT_SIZE + dst_pos] = c; hash = ((hash << TDEFL_LZ_HASH_SHIFT) ^ c) & (TDEFL_LZ_HASH_SIZE - 1); d->m_next[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] = d->m_hash[hash]; d->m_hash[hash] = (mz_uint16)(ins_pos); dst_pos = (dst_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK; ins_pos++; } } else { while ((src_buf_left) && (d->m_lookahead_size < TDEFL_MAX_MATCH_LEN)) { mz_uint8 c = *pSrc++; mz_uint dst_pos = (d->m_lookahead_pos + d->m_lookahead_size) & TDEFL_LZ_DICT_SIZE_MASK; src_buf_left--; d->m_dict[dst_pos] = c; if (dst_pos < (TDEFL_MAX_MATCH_LEN - 1)) d->m_dict[TDEFL_LZ_DICT_SIZE + dst_pos] = c; if ((++d->m_lookahead_size + d->m_dict_size) >= TDEFL_MIN_MATCH_LEN) { mz_uint ins_pos = d->m_lookahead_pos + (d->m_lookahead_size - 1) - 2; mz_uint hash = ((d->m_dict[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] << (TDEFL_LZ_HASH_SHIFT * 2)) ^ (d->m_dict[(ins_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK] << TDEFL_LZ_HASH_SHIFT) ^ c) & (TDEFL_LZ_HASH_SIZE - 1); d->m_next[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] = d->m_hash[hash]; d->m_hash[hash] = (mz_uint16)(ins_pos); } } } d->m_dict_size = MZ_MIN(TDEFL_LZ_DICT_SIZE - d->m_lookahead_size, d->m_dict_size); if ((!flush) && (d->m_lookahead_size < TDEFL_MAX_MATCH_LEN)) break; /* Simple lazy/greedy parsing state machine. */ len_to_move = 1; cur_match_dist = 0; cur_match_len = d->m_saved_match_len ? d->m_saved_match_len : (TDEFL_MIN_MATCH_LEN - 1); cur_pos = d->m_lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK; if (d->m_flags & (TDEFL_RLE_MATCHES | TDEFL_FORCE_ALL_RAW_BLOCKS)) { if ((d->m_dict_size) && (!(d->m_flags & TDEFL_FORCE_ALL_RAW_BLOCKS))) { mz_uint8 c = d->m_dict[(cur_pos - 1) & TDEFL_LZ_DICT_SIZE_MASK]; cur_match_len = 0; while (cur_match_len < d->m_lookahead_size) { if (d->m_dict[cur_pos + cur_match_len] != c) break; cur_match_len++; } if (cur_match_len < TDEFL_MIN_MATCH_LEN) cur_match_len = 0; else cur_match_dist = 1; } } else { tdefl_find_match(d, d->m_lookahead_pos, d->m_dict_size, d->m_lookahead_size, &cur_match_dist, &cur_match_len); } if (((cur_match_len == TDEFL_MIN_MATCH_LEN) && (cur_match_dist >= 8U * 1024U)) || (cur_pos == cur_match_dist) || ((d->m_flags & TDEFL_FILTER_MATCHES) && (cur_match_len <= 5))) { cur_match_dist = cur_match_len = 0; } if (d->m_saved_match_len) { if (cur_match_len > d->m_saved_match_len) { tdefl_record_literal(d, (mz_uint8)d->m_saved_lit); if (cur_match_len >= 128) { tdefl_record_match(d, cur_match_len, cur_match_dist); d->m_saved_match_len = 0; len_to_move = cur_match_len; } else { d->m_saved_lit = d->m_dict[cur_pos]; d->m_saved_match_dist = cur_match_dist; d->m_saved_match_len = cur_match_len; } } else { tdefl_record_match(d, d->m_saved_match_len, d->m_saved_match_dist); len_to_move = d->m_saved_match_len - 1; d->m_saved_match_len = 0; } } else if (!cur_match_dist) tdefl_record_literal(d, d->m_dict[MZ_MIN(cur_pos, sizeof(d->m_dict) - 1)]); else if ((d->m_greedy_parsing) || (d->m_flags & TDEFL_RLE_MATCHES) || (cur_match_len >= 128)) { tdefl_record_match(d, cur_match_len, cur_match_dist); len_to_move = cur_match_len; } else { d->m_saved_lit = d->m_dict[MZ_MIN(cur_pos, sizeof(d->m_dict) - 1)]; d->m_saved_match_dist = cur_match_dist; d->m_saved_match_len = cur_match_len; } /* Move the lookahead forward by len_to_move bytes. */ d->m_lookahead_pos += len_to_move; MZ_ASSERT(d->m_lookahead_size >= len_to_move); d->m_lookahead_size -= len_to_move; d->m_dict_size = MZ_MIN(d->m_dict_size + len_to_move, (mz_uint)TDEFL_LZ_DICT_SIZE); /* Check if it's time to flush the current LZ codes to the internal output buffer. */ if ((d->m_pLZ_code_buf > &d->m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE - 8]) || ((d->m_total_lz_bytes > 31 * 1024) && (((((mz_uint)(d->m_pLZ_code_buf - d->m_lz_code_buf) * 115) >> 7) >= d->m_total_lz_bytes) || (d->m_flags & TDEFL_FORCE_ALL_RAW_BLOCKS)))) { int n; d->m_pSrc = pSrc; d->m_src_buf_left = src_buf_left; if ((n = tdefl_flush_block(d, 0)) != 0) return (n < 0) ? MZ_FALSE : MZ_TRUE; } } d->m_pSrc = pSrc; d->m_src_buf_left = src_buf_left; return MZ_TRUE; } static tdefl_status tdefl_flush_output_buffer(tdefl_compressor *d) { if (d->m_pIn_buf_size) { *d->m_pIn_buf_size = d->m_pSrc - (const mz_uint8 *)d->m_pIn_buf; } if (d->m_pOut_buf_size) { size_t n = MZ_MIN(*d->m_pOut_buf_size - d->m_out_buf_ofs, d->m_output_flush_remaining); memcpy((mz_uint8 *)d->m_pOut_buf + d->m_out_buf_ofs, d->m_output_buf + d->m_output_flush_ofs, n); d->m_output_flush_ofs += (mz_uint)n; d->m_output_flush_remaining -= (mz_uint)n; d->m_out_buf_ofs += n; *d->m_pOut_buf_size = d->m_out_buf_ofs; } return (d->m_finished && !d->m_output_flush_remaining) ? TDEFL_STATUS_DONE : TDEFL_STATUS_OKAY; } tdefl_status tdefl_compress(tdefl_compressor *d, const void *pIn_buf, size_t *pIn_buf_size, void *pOut_buf, size_t *pOut_buf_size, tdefl_flush flush) { if (!d) { if (pIn_buf_size) *pIn_buf_size = 0; if (pOut_buf_size) *pOut_buf_size = 0; return TDEFL_STATUS_BAD_PARAM; } d->m_pIn_buf = pIn_buf; d->m_pIn_buf_size = pIn_buf_size; d->m_pOut_buf = pOut_buf; d->m_pOut_buf_size = pOut_buf_size; d->m_pSrc = (const mz_uint8 *)(pIn_buf); d->m_src_buf_left = pIn_buf_size ? *pIn_buf_size : 0; d->m_out_buf_ofs = 0; d->m_flush = flush; if (((d->m_pPut_buf_func != NULL) == ((pOut_buf != NULL) || (pOut_buf_size != NULL))) || (d->m_prev_return_status != TDEFL_STATUS_OKAY) || (d->m_wants_to_finish && (flush != TDEFL_FINISH)) || (pIn_buf_size && *pIn_buf_size && !pIn_buf) || (pOut_buf_size && *pOut_buf_size && !pOut_buf)) { if (pIn_buf_size) *pIn_buf_size = 0; if (pOut_buf_size) *pOut_buf_size = 0; return (d->m_prev_return_status = TDEFL_STATUS_BAD_PARAM); } d->m_wants_to_finish |= (flush == TDEFL_FINISH); if ((d->m_output_flush_remaining) || (d->m_finished)) return (d->m_prev_return_status = tdefl_flush_output_buffer(d)); #if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN if (((d->m_flags & TDEFL_MAX_PROBES_MASK) == 1) && ((d->m_flags & TDEFL_GREEDY_PARSING_FLAG) != 0) && ((d->m_flags & (TDEFL_FILTER_MATCHES | TDEFL_FORCE_ALL_RAW_BLOCKS | TDEFL_RLE_MATCHES)) == 0)) { if (!tdefl_compress_fast(d)) return d->m_prev_return_status; } else #endif /* #if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN */ { if (!tdefl_compress_normal(d)) return d->m_prev_return_status; } if ((d->m_flags & (TDEFL_WRITE_ZLIB_HEADER | TDEFL_COMPUTE_ADLER32)) && (pIn_buf)) d->m_adler32 = (mz_uint32)mz_adler32(d->m_adler32, (const mz_uint8 *)pIn_buf, d->m_pSrc - (const mz_uint8 *)pIn_buf); if ((flush) && (!d->m_lookahead_size) && (!d->m_src_buf_left) && (!d->m_output_flush_remaining)) { if (tdefl_flush_block(d, flush) < 0) return d->m_prev_return_status; d->m_finished = (flush == TDEFL_FINISH); if (flush == TDEFL_FULL_FLUSH) { MZ_CLEAR_ARR(d->m_hash); MZ_CLEAR_ARR(d->m_next); d->m_dict_size = 0; } } return (d->m_prev_return_status = tdefl_flush_output_buffer(d)); } tdefl_status tdefl_compress_buffer(tdefl_compressor *d, const void *pIn_buf, size_t in_buf_size, tdefl_flush flush) { MZ_ASSERT(d->m_pPut_buf_func); return tdefl_compress(d, pIn_buf, &in_buf_size, NULL, NULL, flush); } tdefl_status tdefl_init(tdefl_compressor *d, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags) { d->m_pPut_buf_func = pPut_buf_func; d->m_pPut_buf_user = pPut_buf_user; d->m_flags = (mz_uint)(flags); d->m_max_probes[0] = 1 + ((flags & 0xFFF) + 2) / 3; d->m_greedy_parsing = (flags & TDEFL_GREEDY_PARSING_FLAG) != 0; d->m_max_probes[1] = 1 + (((flags & 0xFFF) >> 2) + 2) / 3; if (!(flags & TDEFL_NONDETERMINISTIC_PARSING_FLAG)) MZ_CLEAR_ARR(d->m_hash); d->m_lookahead_pos = d->m_lookahead_size = d->m_dict_size = d->m_total_lz_bytes = d->m_lz_code_buf_dict_pos = d->m_bits_in = 0; d->m_output_flush_ofs = d->m_output_flush_remaining = d->m_finished = d->m_block_index = d->m_bit_buffer = d->m_wants_to_finish = 0; d->m_pLZ_code_buf = d->m_lz_code_buf + 1; d->m_pLZ_flags = d->m_lz_code_buf; *d->m_pLZ_flags = 0; d->m_num_flags_left = 8; d->m_pOutput_buf = d->m_output_buf; d->m_pOutput_buf_end = d->m_output_buf; d->m_prev_return_status = TDEFL_STATUS_OKAY; d->m_saved_match_dist = d->m_saved_match_len = d->m_saved_lit = 0; d->m_adler32 = 1; d->m_pIn_buf = NULL; d->m_pOut_buf = NULL; d->m_pIn_buf_size = NULL; d->m_pOut_buf_size = NULL; d->m_flush = TDEFL_NO_FLUSH; d->m_pSrc = NULL; d->m_src_buf_left = 0; d->m_out_buf_ofs = 0; if (!(flags & TDEFL_NONDETERMINISTIC_PARSING_FLAG)) MZ_CLEAR_ARR(d->m_dict); memset(&d->m_huff_count[0][0], 0, sizeof(d->m_huff_count[0][0]) * TDEFL_MAX_HUFF_SYMBOLS_0); memset(&d->m_huff_count[1][0], 0, sizeof(d->m_huff_count[1][0]) * TDEFL_MAX_HUFF_SYMBOLS_1); return TDEFL_STATUS_OKAY; } tdefl_status tdefl_get_prev_return_status(tdefl_compressor *d) { return d->m_prev_return_status; } mz_uint32 tdefl_get_adler32(tdefl_compressor *d) { return d->m_adler32; } mz_bool tdefl_compress_mem_to_output(const void *pBuf, size_t buf_len, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags) { tdefl_compressor *pComp; mz_bool succeeded; if (((buf_len) && (!pBuf)) || (!pPut_buf_func)) return MZ_FALSE; pComp = (tdefl_compressor *)MZ_MALLOC(sizeof(tdefl_compressor)); if (!pComp) return MZ_FALSE; succeeded = (tdefl_init(pComp, pPut_buf_func, pPut_buf_user, flags) == TDEFL_STATUS_OKAY); succeeded = succeeded && (tdefl_compress_buffer(pComp, pBuf, buf_len, TDEFL_FINISH) == TDEFL_STATUS_DONE); MZ_FREE(pComp); return succeeded; } typedef struct { size_t m_size, m_capacity; mz_uint8 *m_pBuf; mz_bool m_expandable; } tdefl_output_buffer; static mz_bool tdefl_output_buffer_putter(const void *pBuf, int len, void *pUser) { tdefl_output_buffer *p = (tdefl_output_buffer *)pUser; size_t new_size = p->m_size + len; if (new_size > p->m_capacity) { size_t new_capacity = p->m_capacity; mz_uint8 *pNew_buf; if (!p->m_expandable) return MZ_FALSE; do { new_capacity = MZ_MAX(128U, new_capacity << 1U); } while (new_size > new_capacity); pNew_buf = (mz_uint8 *)MZ_REALLOC(p->m_pBuf, new_capacity); if (!pNew_buf) return MZ_FALSE; p->m_pBuf = pNew_buf; p->m_capacity = new_capacity; } memcpy((mz_uint8 *)p->m_pBuf + p->m_size, pBuf, len); p->m_size = new_size; return MZ_TRUE; } void *tdefl_compress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags) { tdefl_output_buffer out_buf; MZ_CLEAR_OBJ(out_buf); if (!pOut_len) return MZ_FALSE; else *pOut_len = 0; out_buf.m_expandable = MZ_TRUE; if (!tdefl_compress_mem_to_output(pSrc_buf, src_buf_len, tdefl_output_buffer_putter, &out_buf, flags)) return NULL; *pOut_len = out_buf.m_size; return out_buf.m_pBuf; } size_t tdefl_compress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags) { tdefl_output_buffer out_buf; MZ_CLEAR_OBJ(out_buf); if (!pOut_buf) return 0; out_buf.m_pBuf = (mz_uint8 *)pOut_buf; out_buf.m_capacity = out_buf_len; if (!tdefl_compress_mem_to_output(pSrc_buf, src_buf_len, tdefl_output_buffer_putter, &out_buf, flags)) return 0; return out_buf.m_size; } static const mz_uint s_tdefl_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 }; /* level may actually range from [0,10] (10 is a "hidden" max level, where we want a bit more compression and it's fine if throughput to fall off a cliff on some files). */ mz_uint tdefl_create_comp_flags_from_zip_params(int level, int window_bits, int strategy) { mz_uint comp_flags = s_tdefl_num_probes[(level >= 0) ? MZ_MIN(10, level) : MZ_DEFAULT_LEVEL] | ((level <= 3) ? TDEFL_GREEDY_PARSING_FLAG : 0); if (window_bits > 0) comp_flags |= TDEFL_WRITE_ZLIB_HEADER; if (!level) comp_flags |= TDEFL_FORCE_ALL_RAW_BLOCKS; else if (strategy == MZ_FILTERED) comp_flags |= TDEFL_FILTER_MATCHES; else if (strategy == MZ_HUFFMAN_ONLY) comp_flags &= ~TDEFL_MAX_PROBES_MASK; else if (strategy == MZ_FIXED) comp_flags |= TDEFL_FORCE_ALL_STATIC_BLOCKS; else if (strategy == MZ_RLE) comp_flags |= TDEFL_RLE_MATCHES; return comp_flags; } #ifdef _MSC_VER #pragma warning(push) #pragma warning(disable : 4204) /* nonstandard extension used : non-constant aggregate initializer (also supported by GNU C and C99, so no big deal) */ #endif /* Simple PNG writer function by Alex Evans, 2011. Released into the public domain: https://gist.github.com/908299, more context at http://altdevblogaday.org/2011/04/06/a-smaller-jpg-encoder/. This is actually a modification of Alex's original code so PNG files generated by this function pass pngcheck. */ void *tdefl_write_image_to_png_file_in_memory_ex(const void *pImage, int w, int h, int num_chans, size_t *pLen_out, mz_uint level, mz_bool flip) { /* Using a local copy of this array here in case MINIZ_NO_ZLIB_APIS was defined. */ static const mz_uint s_tdefl_png_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 }; tdefl_compressor *pComp = (tdefl_compressor *)MZ_MALLOC(sizeof(tdefl_compressor)); tdefl_output_buffer out_buf; int i, bpl = w * num_chans, y, z; mz_uint32 c; *pLen_out = 0; if (!pComp) return NULL; MZ_CLEAR_OBJ(out_buf); out_buf.m_expandable = MZ_TRUE; out_buf.m_capacity = 57 + MZ_MAX(64, (1 + bpl) * h); if (NULL == (out_buf.m_pBuf = (mz_uint8 *)MZ_MALLOC(out_buf.m_capacity))) { MZ_FREE(pComp); return NULL; } /* write dummy header */ for (z = 41; z; --z) tdefl_output_buffer_putter(&z, 1, &out_buf); /* compress image data */ tdefl_init(pComp, tdefl_output_buffer_putter, &out_buf, s_tdefl_png_num_probes[MZ_MIN(10, level)] | TDEFL_WRITE_ZLIB_HEADER); for (y = 0; y < h; ++y) { tdefl_compress_buffer(pComp, &z, 1, TDEFL_NO_FLUSH); tdefl_compress_buffer(pComp, (mz_uint8 *)pImage + (flip ? (h - 1 - y) : y) * bpl, bpl, TDEFL_NO_FLUSH); } if (tdefl_compress_buffer(pComp, NULL, 0, TDEFL_FINISH) != TDEFL_STATUS_DONE) { MZ_FREE(pComp); MZ_FREE(out_buf.m_pBuf); return NULL; } /* write real header */ *pLen_out = out_buf.m_size - 41; { static const mz_uint8 chans[] = { 0x00, 0x00, 0x04, 0x02, 0x06 }; mz_uint8 pnghdr[41] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x44, 0x41, 0x54 }; pnghdr[18] = (mz_uint8)(w >> 8); pnghdr[19] = (mz_uint8)w; pnghdr[22] = (mz_uint8)(h >> 8); pnghdr[23] = (mz_uint8)h; pnghdr[25] = chans[num_chans]; pnghdr[33] = (mz_uint8)(*pLen_out >> 24); pnghdr[34] = (mz_uint8)(*pLen_out >> 16); pnghdr[35] = (mz_uint8)(*pLen_out >> 8); pnghdr[36] = (mz_uint8)*pLen_out; c = (mz_uint32)mz_crc32(MZ_CRC32_INIT, pnghdr + 12, 17); for (i = 0; i < 4; ++i, c <<= 8) ((mz_uint8 *)(pnghdr + 29))[i] = (mz_uint8)(c >> 24); memcpy(out_buf.m_pBuf, pnghdr, 41); } /* write footer (IDAT CRC-32, followed by IEND chunk) */ if (!tdefl_output_buffer_putter("\0\0\0\0\0\0\0\0\x49\x45\x4e\x44\xae\x42\x60\x82", 16, &out_buf)) { *pLen_out = 0; MZ_FREE(pComp); MZ_FREE(out_buf.m_pBuf); return NULL; } c = (mz_uint32)mz_crc32(MZ_CRC32_INIT, out_buf.m_pBuf + 41 - 4, *pLen_out + 4); for (i = 0; i < 4; ++i, c <<= 8) (out_buf.m_pBuf + out_buf.m_size - 16)[i] = (mz_uint8)(c >> 24); /* compute final size of file, grab compressed data buffer and return */ *pLen_out += 57; MZ_FREE(pComp); return out_buf.m_pBuf; } void *tdefl_write_image_to_png_file_in_memory(const void *pImage, int w, int h, int num_chans, size_t *pLen_out) { /* Level 6 corresponds to TDEFL_DEFAULT_MAX_PROBES or MZ_DEFAULT_LEVEL (but we can't depend on MZ_DEFAULT_LEVEL being available in case the zlib API's where #defined out) */ return tdefl_write_image_to_png_file_in_memory_ex(pImage, w, h, num_chans, pLen_out, 6, MZ_FALSE); } #ifndef MINIZ_NO_MALLOC /* Allocate the tdefl_compressor and tinfl_decompressor structures in C so that */ /* non-C language bindings to tdefL_ and tinfl_ API don't need to worry about */ /* structure size and allocation mechanism. */ tdefl_compressor *tdefl_compressor_alloc(void) { return (tdefl_compressor *)MZ_MALLOC(sizeof(tdefl_compressor)); } void tdefl_compressor_free(tdefl_compressor *pComp) { MZ_FREE(pComp); } #endif #ifdef _MSC_VER #pragma warning(pop) #endif #ifdef __cplusplus } #endif #endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/ /************************************************************************** * * Copyright 2013-2014 RAD Game Tools and Valve Software * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC * All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * **************************************************************************/ #ifndef MINIZ_NO_INFLATE_APIS #ifdef __cplusplus extern "C" { #endif /* ------------------- Low-level Decompression (completely independent from all compression API's) */ #define TINFL_MEMCPY(d, s, l) memcpy(d, s, l) #define TINFL_MEMSET(p, c, l) memset(p, c, l) #define TINFL_CR_BEGIN \ switch (r->m_state) \ { \ case 0: #define TINFL_CR_RETURN(state_index, result) \ do \ { \ status = result; \ r->m_state = state_index; \ goto common_exit; \ case state_index:; \ } \ MZ_MACRO_END #define TINFL_CR_RETURN_FOREVER(state_index, result) \ do \ { \ for (;;) \ { \ TINFL_CR_RETURN(state_index, result); \ } \ } \ MZ_MACRO_END #define TINFL_CR_FINISH } #define TINFL_GET_BYTE(state_index, c) \ do \ { \ while (pIn_buf_cur >= pIn_buf_end) \ { \ TINFL_CR_RETURN(state_index, (decomp_flags & TINFL_FLAG_HAS_MORE_INPUT) ? TINFL_STATUS_NEEDS_MORE_INPUT : TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS); \ } \ c = *pIn_buf_cur++; \ } \ MZ_MACRO_END #define TINFL_NEED_BITS(state_index, n) \ do \ { \ mz_uint c; \ TINFL_GET_BYTE(state_index, c); \ bit_buf |= (((tinfl_bit_buf_t)c) << num_bits); \ num_bits += 8; \ } while (num_bits < (mz_uint)(n)) #define TINFL_SKIP_BITS(state_index, n) \ do \ { \ if (num_bits < (mz_uint)(n)) \ { \ TINFL_NEED_BITS(state_index, n); \ } \ bit_buf >>= (n); \ num_bits -= (n); \ } \ MZ_MACRO_END #define TINFL_GET_BITS(state_index, b, n) \ do \ { \ if (num_bits < (mz_uint)(n)) \ { \ TINFL_NEED_BITS(state_index, n); \ } \ b = bit_buf & ((1 << (n)) - 1); \ bit_buf >>= (n); \ num_bits -= (n); \ } \ MZ_MACRO_END /* TINFL_HUFF_BITBUF_FILL() is only used rarely, when the number of bytes remaining in the input buffer falls below 2. */ /* It reads just enough bytes from the input stream that are needed to decode the next Huffman code (and absolutely no more). It works by trying to fully decode a */ /* Huffman code by using whatever bits are currently present in the bit buffer. If this fails, it reads another byte, and tries again until it succeeds or until the */ /* bit buffer contains >=15 bits (deflate's max. Huffman code size). */ #define TINFL_HUFF_BITBUF_FILL(state_index, pLookUp, pTree) \ do \ { \ temp = pLookUp[bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]; \ if (temp >= 0) \ { \ code_len = temp >> 9; \ if ((code_len) && (num_bits >= code_len)) \ break; \ } \ else if (num_bits > TINFL_FAST_LOOKUP_BITS) \ { \ code_len = TINFL_FAST_LOOKUP_BITS; \ do \ { \ temp = pTree[~temp + ((bit_buf >> code_len++) & 1)]; \ } while ((temp < 0) && (num_bits >= (code_len + 1))); \ if (temp >= 0) \ break; \ } \ TINFL_GET_BYTE(state_index, c); \ bit_buf |= (((tinfl_bit_buf_t)c) << num_bits); \ num_bits += 8; \ } while (num_bits < 15); /* TINFL_HUFF_DECODE() decodes the next Huffman coded symbol. It's more complex than you would initially expect because the zlib API expects the decompressor to never read */ /* beyond the final byte of the deflate stream. (In other words, when this macro wants to read another byte from the input, it REALLY needs another byte in order to fully */ /* decode the next Huffman code.) Handling this properly is particularly important on raw deflate (non-zlib) streams, which aren't followed by a byte aligned adler-32. */ /* The slow path is only executed at the very end of the input buffer. */ /* v1.16: The original macro handled the case at the very end of the passed-in input buffer, but we also need to handle the case where the user passes in 1+zillion bytes */ /* following the deflate data and our non-conservative read-ahead path won't kick in here on this code. This is much trickier. */ #define TINFL_HUFF_DECODE(state_index, sym, pLookUp, pTree) \ do \ { \ int temp; \ mz_uint code_len, c; \ if (num_bits < 15) \ { \ if ((pIn_buf_end - pIn_buf_cur) < 2) \ { \ TINFL_HUFF_BITBUF_FILL(state_index, pLookUp, pTree); \ } \ else \ { \ bit_buf |= (((tinfl_bit_buf_t)pIn_buf_cur[0]) << num_bits) | (((tinfl_bit_buf_t)pIn_buf_cur[1]) << (num_bits + 8)); \ pIn_buf_cur += 2; \ num_bits += 16; \ } \ } \ if ((temp = pLookUp[bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0) \ code_len = temp >> 9, temp &= 511; \ else \ { \ code_len = TINFL_FAST_LOOKUP_BITS; \ do \ { \ temp = pTree[~temp + ((bit_buf >> code_len++) & 1)]; \ } while (temp < 0); \ } \ sym = temp; \ bit_buf >>= code_len; \ num_bits -= code_len; \ } \ MZ_MACRO_END static void tinfl_clear_tree(tinfl_decompressor *r) { if (r->m_type == 0) MZ_CLEAR_ARR(r->m_tree_0); else if (r->m_type == 1) MZ_CLEAR_ARR(r->m_tree_1); else MZ_CLEAR_ARR(r->m_tree_2); } tinfl_status tinfl_decompress(tinfl_decompressor *r, const mz_uint8 *pIn_buf_next, size_t *pIn_buf_size, mz_uint8 *pOut_buf_start, mz_uint8 *pOut_buf_next, size_t *pOut_buf_size, const mz_uint32 decomp_flags) { static const mz_uint16 s_length_base[31] = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 }; static const mz_uint8 s_length_extra[31] = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0 }; static const mz_uint16 s_dist_base[32] = { 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577, 0, 0 }; static const mz_uint8 s_dist_extra[32] = { 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 }; static const mz_uint8 s_length_dezigzag[19] = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; static const mz_uint16 s_min_table_sizes[3] = { 257, 1, 4 }; mz_int16 *pTrees[3]; mz_uint8 *pCode_sizes[3]; tinfl_status status = TINFL_STATUS_FAILED; mz_uint32 num_bits, dist, counter, num_extra; tinfl_bit_buf_t bit_buf; const mz_uint8 *pIn_buf_cur = pIn_buf_next, *const pIn_buf_end = pIn_buf_next + *pIn_buf_size; mz_uint8 *pOut_buf_cur = pOut_buf_next, *const pOut_buf_end = pOut_buf_next ? pOut_buf_next + *pOut_buf_size : NULL; size_t out_buf_size_mask = (decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF) ? (size_t)-1 : ((pOut_buf_next - pOut_buf_start) + *pOut_buf_size) - 1, dist_from_out_buf_start; /* Ensure the output buffer's size is a power of 2, unless the output buffer is large enough to hold the entire output file (in which case it doesn't matter). */ if (((out_buf_size_mask + 1) & out_buf_size_mask) || (pOut_buf_next < pOut_buf_start)) { *pIn_buf_size = *pOut_buf_size = 0; return TINFL_STATUS_BAD_PARAM; } pTrees[0] = r->m_tree_0; pTrees[1] = r->m_tree_1; pTrees[2] = r->m_tree_2; pCode_sizes[0] = r->m_code_size_0; pCode_sizes[1] = r->m_code_size_1; pCode_sizes[2] = r->m_code_size_2; num_bits = r->m_num_bits; bit_buf = r->m_bit_buf; dist = r->m_dist; counter = r->m_counter; num_extra = r->m_num_extra; dist_from_out_buf_start = r->m_dist_from_out_buf_start; TINFL_CR_BEGIN bit_buf = num_bits = dist = counter = num_extra = r->m_zhdr0 = r->m_zhdr1 = 0; r->m_z_adler32 = r->m_check_adler32 = 1; if (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER) { TINFL_GET_BYTE(1, r->m_zhdr0); TINFL_GET_BYTE(2, r->m_zhdr1); counter = (((r->m_zhdr0 * 256 + r->m_zhdr1) % 31 != 0) || (r->m_zhdr1 & 32) || ((r->m_zhdr0 & 15) != 8)); if (!(decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF)) counter |= (((1U << (8U + (r->m_zhdr0 >> 4))) > 32768U) || ((out_buf_size_mask + 1) < (size_t)((size_t)1 << (8U + (r->m_zhdr0 >> 4))))); if (counter) { TINFL_CR_RETURN_FOREVER(36, TINFL_STATUS_FAILED); } } do { TINFL_GET_BITS(3, r->m_final, 3); r->m_type = r->m_final >> 1; if (r->m_type == 0) { TINFL_SKIP_BITS(5, num_bits & 7); for (counter = 0; counter < 4; ++counter) { if (num_bits) TINFL_GET_BITS(6, r->m_raw_header[counter], 8); else TINFL_GET_BYTE(7, r->m_raw_header[counter]); } if ((counter = (r->m_raw_header[0] | (r->m_raw_header[1] << 8))) != (mz_uint)(0xFFFF ^ (r->m_raw_header[2] | (r->m_raw_header[3] << 8)))) { TINFL_CR_RETURN_FOREVER(39, TINFL_STATUS_FAILED); } while ((counter) && (num_bits)) { TINFL_GET_BITS(51, dist, 8); while (pOut_buf_cur >= pOut_buf_end) { TINFL_CR_RETURN(52, TINFL_STATUS_HAS_MORE_OUTPUT); } *pOut_buf_cur++ = (mz_uint8)dist; counter--; } while (counter) { size_t n; while (pOut_buf_cur >= pOut_buf_end) { TINFL_CR_RETURN(9, TINFL_STATUS_HAS_MORE_OUTPUT); } while (pIn_buf_cur >= pIn_buf_end) { TINFL_CR_RETURN(38, (decomp_flags & TINFL_FLAG_HAS_MORE_INPUT) ? TINFL_STATUS_NEEDS_MORE_INPUT : TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS); } n = MZ_MIN(MZ_MIN((size_t)(pOut_buf_end - pOut_buf_cur), (size_t)(pIn_buf_end - pIn_buf_cur)), counter); TINFL_MEMCPY(pOut_buf_cur, pIn_buf_cur, n); pIn_buf_cur += n; pOut_buf_cur += n; counter -= (mz_uint)n; } } else if (r->m_type == 3) { TINFL_CR_RETURN_FOREVER(10, TINFL_STATUS_FAILED); } else { if (r->m_type == 1) { mz_uint8 *p = r->m_code_size_0; mz_uint i; r->m_table_sizes[0] = 288; r->m_table_sizes[1] = 32; TINFL_MEMSET(r->m_code_size_1, 5, 32); for (i = 0; i <= 143; ++i) *p++ = 8; for (; i <= 255; ++i) *p++ = 9; for (; i <= 279; ++i) *p++ = 7; for (; i <= 287; ++i) *p++ = 8; } else { for (counter = 0; counter < 3; counter++) { TINFL_GET_BITS(11, r->m_table_sizes[counter], "\05\05\04"[counter]); r->m_table_sizes[counter] += s_min_table_sizes[counter]; } MZ_CLEAR_ARR(r->m_code_size_2); for (counter = 0; counter < r->m_table_sizes[2]; counter++) { mz_uint s; TINFL_GET_BITS(14, s, 3); r->m_code_size_2[s_length_dezigzag[counter]] = (mz_uint8)s; } r->m_table_sizes[2] = 19; } for (; (int)r->m_type >= 0; r->m_type--) { int tree_next, tree_cur; mz_int16 *pLookUp; mz_int16 *pTree; mz_uint8 *pCode_size; mz_uint i, j, used_syms, total, sym_index, next_code[17], total_syms[16]; pLookUp = r->m_look_up[r->m_type]; pTree = pTrees[r->m_type]; pCode_size = pCode_sizes[r->m_type]; MZ_CLEAR_ARR(total_syms); TINFL_MEMSET(pLookUp, 0, sizeof(r->m_look_up[0])); tinfl_clear_tree(r); for (i = 0; i < r->m_table_sizes[r->m_type]; ++i) total_syms[pCode_size[i]]++; used_syms = 0, total = 0; next_code[0] = next_code[1] = 0; for (i = 1; i <= 15; ++i) { used_syms += total_syms[i]; next_code[i + 1] = (total = ((total + total_syms[i]) << 1)); } if ((65536 != total) && (used_syms > 1)) { TINFL_CR_RETURN_FOREVER(35, TINFL_STATUS_FAILED); } for (tree_next = -1, sym_index = 0; sym_index < r->m_table_sizes[r->m_type]; ++sym_index) { mz_uint rev_code = 0, l, cur_code, code_size = pCode_size[sym_index]; if (!code_size) continue; cur_code = next_code[code_size]++; for (l = code_size; l > 0; l--, cur_code >>= 1) rev_code = (rev_code << 1) | (cur_code & 1); if (code_size <= TINFL_FAST_LOOKUP_BITS) { mz_int16 k = (mz_int16)((code_size << 9) | sym_index); while (rev_code < TINFL_FAST_LOOKUP_SIZE) { pLookUp[rev_code] = k; rev_code += (1 << code_size); } continue; } if (0 == (tree_cur = pLookUp[rev_code & (TINFL_FAST_LOOKUP_SIZE - 1)])) { pLookUp[rev_code & (TINFL_FAST_LOOKUP_SIZE - 1)] = (mz_int16)tree_next; tree_cur = tree_next; tree_next -= 2; } rev_code >>= (TINFL_FAST_LOOKUP_BITS - 1); for (j = code_size; j > (TINFL_FAST_LOOKUP_BITS + 1); j--) { tree_cur -= ((rev_code >>= 1) & 1); if (!pTree[-tree_cur - 1]) { pTree[-tree_cur - 1] = (mz_int16)tree_next; tree_cur = tree_next; tree_next -= 2; } else tree_cur = pTree[-tree_cur - 1]; } tree_cur -= ((rev_code >>= 1) & 1); pTree[-tree_cur - 1] = (mz_int16)sym_index; } if (r->m_type == 2) { for (counter = 0; counter < (r->m_table_sizes[0] + r->m_table_sizes[1]);) { mz_uint s; TINFL_HUFF_DECODE(16, dist, r->m_look_up[2], r->m_tree_2); if (dist < 16) { r->m_len_codes[counter++] = (mz_uint8)dist; continue; } if ((dist == 16) && (!counter)) { TINFL_CR_RETURN_FOREVER(17, TINFL_STATUS_FAILED); } num_extra = "\02\03\07"[dist - 16]; TINFL_GET_BITS(18, s, num_extra); s += "\03\03\013"[dist - 16]; TINFL_MEMSET(r->m_len_codes + counter, (dist == 16) ? r->m_len_codes[counter - 1] : 0, s); counter += s; } if ((r->m_table_sizes[0] + r->m_table_sizes[1]) != counter) { TINFL_CR_RETURN_FOREVER(21, TINFL_STATUS_FAILED); } TINFL_MEMCPY(r->m_code_size_0, r->m_len_codes, r->m_table_sizes[0]); TINFL_MEMCPY(r->m_code_size_1, r->m_len_codes + r->m_table_sizes[0], r->m_table_sizes[1]); } } for (;;) { mz_uint8 *pSrc; for (;;) { if (((pIn_buf_end - pIn_buf_cur) < 4) || ((pOut_buf_end - pOut_buf_cur) < 2)) { TINFL_HUFF_DECODE(23, counter, r->m_look_up[0], r->m_tree_0); if (counter >= 256) break; while (pOut_buf_cur >= pOut_buf_end) { TINFL_CR_RETURN(24, TINFL_STATUS_HAS_MORE_OUTPUT); } *pOut_buf_cur++ = (mz_uint8)counter; } else { int sym2; mz_uint code_len; #if TINFL_USE_64BIT_BITBUF if (num_bits < 30) { bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE32(pIn_buf_cur)) << num_bits); pIn_buf_cur += 4; num_bits += 32; } #else if (num_bits < 15) { bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE16(pIn_buf_cur)) << num_bits); pIn_buf_cur += 2; num_bits += 16; } #endif if ((sym2 = r->m_look_up[0][bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0) code_len = sym2 >> 9; else { code_len = TINFL_FAST_LOOKUP_BITS; do { sym2 = r->m_tree_0[~sym2 + ((bit_buf >> code_len++) & 1)]; } while (sym2 < 0); } counter = sym2; bit_buf >>= code_len; num_bits -= code_len; if (counter & 256) break; #if !TINFL_USE_64BIT_BITBUF if (num_bits < 15) { bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE16(pIn_buf_cur)) << num_bits); pIn_buf_cur += 2; num_bits += 16; } #endif if ((sym2 = r->m_look_up[0][bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0) code_len = sym2 >> 9; else { code_len = TINFL_FAST_LOOKUP_BITS; do { sym2 = r->m_tree_0[~sym2 + ((bit_buf >> code_len++) & 1)]; } while (sym2 < 0); } bit_buf >>= code_len; num_bits -= code_len; pOut_buf_cur[0] = (mz_uint8)counter; if (sym2 & 256) { pOut_buf_cur++; counter = sym2; break; } pOut_buf_cur[1] = (mz_uint8)sym2; pOut_buf_cur += 2; } } if ((counter &= 511) == 256) break; num_extra = s_length_extra[counter - 257]; counter = s_length_base[counter - 257]; if (num_extra) { mz_uint extra_bits; TINFL_GET_BITS(25, extra_bits, num_extra); counter += extra_bits; } TINFL_HUFF_DECODE(26, dist, r->m_look_up[1], r->m_tree_1); num_extra = s_dist_extra[dist]; dist = s_dist_base[dist]; if (num_extra) { mz_uint extra_bits; TINFL_GET_BITS(27, extra_bits, num_extra); dist += extra_bits; } dist_from_out_buf_start = pOut_buf_cur - pOut_buf_start; if ((dist == 0 || dist > dist_from_out_buf_start || dist_from_out_buf_start == 0) && (decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF)) { TINFL_CR_RETURN_FOREVER(37, TINFL_STATUS_FAILED); } pSrc = pOut_buf_start + ((dist_from_out_buf_start - dist) & out_buf_size_mask); if ((MZ_MAX(pOut_buf_cur, pSrc) + counter) > pOut_buf_end) { while (counter--) { while (pOut_buf_cur >= pOut_buf_end) { TINFL_CR_RETURN(53, TINFL_STATUS_HAS_MORE_OUTPUT); } *pOut_buf_cur++ = pOut_buf_start[(dist_from_out_buf_start++ - dist) & out_buf_size_mask]; } continue; } #if MINIZ_USE_UNALIGNED_LOADS_AND_STORES else if ((counter >= 9) && (counter <= dist)) { const mz_uint8 *pSrc_end = pSrc + (counter & ~7); do { #ifdef MINIZ_UNALIGNED_USE_MEMCPY memcpy(pOut_buf_cur, pSrc, sizeof(mz_uint32)*2); #else ((mz_uint32 *)pOut_buf_cur)[0] = ((const mz_uint32 *)pSrc)[0]; ((mz_uint32 *)pOut_buf_cur)[1] = ((const mz_uint32 *)pSrc)[1]; #endif pOut_buf_cur += 8; } while ((pSrc += 8) < pSrc_end); if ((counter &= 7) < 3) { if (counter) { pOut_buf_cur[0] = pSrc[0]; if (counter > 1) pOut_buf_cur[1] = pSrc[1]; pOut_buf_cur += counter; } continue; } } #endif while(counter>2) { pOut_buf_cur[0] = pSrc[0]; pOut_buf_cur[1] = pSrc[1]; pOut_buf_cur[2] = pSrc[2]; pOut_buf_cur += 3; pSrc += 3; counter -= 3; } if (counter > 0) { pOut_buf_cur[0] = pSrc[0]; if (counter > 1) pOut_buf_cur[1] = pSrc[1]; pOut_buf_cur += counter; } } } } while (!(r->m_final & 1)); /* Ensure byte alignment and put back any bytes from the bitbuf if we've looked ahead too far on gzip, or other Deflate streams followed by arbitrary data. */ /* I'm being super conservative here. A number of simplifications can be made to the byte alignment part, and the Adler32 check shouldn't ever need to worry about reading from the bitbuf now. */ TINFL_SKIP_BITS(32, num_bits & 7); while ((pIn_buf_cur > pIn_buf_next) && (num_bits >= 8)) { --pIn_buf_cur; num_bits -= 8; } bit_buf &= ~(~(tinfl_bit_buf_t)0 << num_bits); MZ_ASSERT(!num_bits); /* if this assert fires then we've read beyond the end of non-deflate/zlib streams with following data (such as gzip streams). */ if (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER) { for (counter = 0; counter < 4; ++counter) { mz_uint s; if (num_bits) TINFL_GET_BITS(41, s, 8); else TINFL_GET_BYTE(42, s); r->m_z_adler32 = (r->m_z_adler32 << 8) | s; } } TINFL_CR_RETURN_FOREVER(34, TINFL_STATUS_DONE); TINFL_CR_FINISH common_exit: /* As long as we aren't telling the caller that we NEED more input to make forward progress: */ /* Put back any bytes from the bitbuf in case we've looked ahead too far on gzip, or other Deflate streams followed by arbitrary data. */ /* We need to be very careful here to NOT push back any bytes we definitely know we need to make forward progress, though, or we'll lock the caller up into an inf loop. */ if ((status != TINFL_STATUS_NEEDS_MORE_INPUT) && (status != TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS)) { while ((pIn_buf_cur > pIn_buf_next) && (num_bits >= 8)) { --pIn_buf_cur; num_bits -= 8; } } r->m_num_bits = num_bits; r->m_bit_buf = bit_buf & ~(~(tinfl_bit_buf_t)0 << num_bits); r->m_dist = dist; r->m_counter = counter; r->m_num_extra = num_extra; r->m_dist_from_out_buf_start = dist_from_out_buf_start; *pIn_buf_size = pIn_buf_cur - pIn_buf_next; *pOut_buf_size = pOut_buf_cur - pOut_buf_next; if ((decomp_flags & (TINFL_FLAG_PARSE_ZLIB_HEADER | TINFL_FLAG_COMPUTE_ADLER32)) && (status >= 0)) { const mz_uint8 *ptr = pOut_buf_next; size_t buf_len = *pOut_buf_size; mz_uint32 i, s1 = r->m_check_adler32 & 0xffff, s2 = r->m_check_adler32 >> 16; size_t block_len = buf_len % 5552; while (buf_len) { for (i = 0; i + 7 < block_len; i += 8, ptr += 8) { s1 += ptr[0], s2 += s1; s1 += ptr[1], s2 += s1; s1 += ptr[2], s2 += s1; s1 += ptr[3], s2 += s1; s1 += ptr[4], s2 += s1; s1 += ptr[5], s2 += s1; s1 += ptr[6], s2 += s1; s1 += ptr[7], s2 += s1; } for (; i < block_len; ++i) s1 += *ptr++, s2 += s1; s1 %= 65521U, s2 %= 65521U; buf_len -= block_len; block_len = 5552; } r->m_check_adler32 = (s2 << 16) + s1; if ((status == TINFL_STATUS_DONE) && (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER) && (r->m_check_adler32 != r->m_z_adler32)) status = TINFL_STATUS_ADLER32_MISMATCH; } return status; } /* Higher level helper functions. */ void *tinfl_decompress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags) { tinfl_decompressor decomp; void *pBuf = NULL, *pNew_buf; size_t src_buf_ofs = 0, out_buf_capacity = 0; *pOut_len = 0; tinfl_init(&decomp); for (;;) { size_t src_buf_size = src_buf_len - src_buf_ofs, dst_buf_size = out_buf_capacity - *pOut_len, new_out_buf_capacity; tinfl_status status = tinfl_decompress(&decomp, (const mz_uint8 *)pSrc_buf + src_buf_ofs, &src_buf_size, (mz_uint8 *)pBuf, pBuf ? (mz_uint8 *)pBuf + *pOut_len : NULL, &dst_buf_size, (flags & ~TINFL_FLAG_HAS_MORE_INPUT) | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF); if ((status < 0) || (status == TINFL_STATUS_NEEDS_MORE_INPUT)) { MZ_FREE(pBuf); *pOut_len = 0; return NULL; } src_buf_ofs += src_buf_size; *pOut_len += dst_buf_size; if (status == TINFL_STATUS_DONE) break; new_out_buf_capacity = out_buf_capacity * 2; if (new_out_buf_capacity < 128) new_out_buf_capacity = 128; pNew_buf = MZ_REALLOC(pBuf, new_out_buf_capacity); if (!pNew_buf) { MZ_FREE(pBuf); *pOut_len = 0; return NULL; } pBuf = pNew_buf; out_buf_capacity = new_out_buf_capacity; } return pBuf; } size_t tinfl_decompress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags) { tinfl_decompressor decomp; tinfl_status status; tinfl_init(&decomp); status = tinfl_decompress(&decomp, (const mz_uint8 *)pSrc_buf, &src_buf_len, (mz_uint8 *)pOut_buf, (mz_uint8 *)pOut_buf, &out_buf_len, (flags & ~TINFL_FLAG_HAS_MORE_INPUT) | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF); return (status != TINFL_STATUS_DONE) ? TINFL_DECOMPRESS_MEM_TO_MEM_FAILED : out_buf_len; } int tinfl_decompress_mem_to_callback(const void *pIn_buf, size_t *pIn_buf_size, tinfl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags) { int result = 0; tinfl_decompressor decomp; mz_uint8 *pDict = (mz_uint8 *)MZ_MALLOC(TINFL_LZ_DICT_SIZE); size_t in_buf_ofs = 0, dict_ofs = 0; if (!pDict) return TINFL_STATUS_FAILED; memset(pDict,0,TINFL_LZ_DICT_SIZE); tinfl_init(&decomp); for (;;) { size_t in_buf_size = *pIn_buf_size - in_buf_ofs, dst_buf_size = TINFL_LZ_DICT_SIZE - dict_ofs; tinfl_status status = tinfl_decompress(&decomp, (const mz_uint8 *)pIn_buf + in_buf_ofs, &in_buf_size, pDict, pDict + dict_ofs, &dst_buf_size, (flags & ~(TINFL_FLAG_HAS_MORE_INPUT | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF))); in_buf_ofs += in_buf_size; if ((dst_buf_size) && (!(*pPut_buf_func)(pDict + dict_ofs, (int)dst_buf_size, pPut_buf_user))) break; if (status != TINFL_STATUS_HAS_MORE_OUTPUT) { result = (status == TINFL_STATUS_DONE); break; } dict_ofs = (dict_ofs + dst_buf_size) & (TINFL_LZ_DICT_SIZE - 1); } MZ_FREE(pDict); *pIn_buf_size = in_buf_ofs; return result; } #ifndef MINIZ_NO_MALLOC tinfl_decompressor *tinfl_decompressor_alloc(void) { tinfl_decompressor *pDecomp = (tinfl_decompressor *)MZ_MALLOC(sizeof(tinfl_decompressor)); if (pDecomp) tinfl_init(pDecomp); return pDecomp; } void tinfl_decompressor_free(tinfl_decompressor *pDecomp) { MZ_FREE(pDecomp); } #endif #ifdef __cplusplus } #endif #endif /*#ifndef MINIZ_NO_INFLATE_APIS*/ /************************************************************************** * * Copyright 2013-2014 RAD Game Tools and Valve Software * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC * Copyright 2016 Martin Raiber * All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * **************************************************************************/ #ifndef MINIZ_NO_ARCHIVE_APIS #ifdef __cplusplus extern "C" { #endif /* ------------------- .ZIP archive reading */ #ifdef MINIZ_NO_STDIO #define MZ_FILE void * #else #include #if defined(_MSC_VER) || defined(__MINGW64__) #define WIN32_LEAN_AND_MEAN #include static WCHAR* mz_utf8z_to_widechar(const char* str) { int reqChars = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); WCHAR* wStr = (WCHAR*)malloc(reqChars * sizeof(WCHAR)); MultiByteToWideChar(CP_UTF8, 0, str, -1, wStr, reqChars); return wStr; } static FILE *mz_fopen(const char *pFilename, const char *pMode) { WCHAR* wFilename = mz_utf8z_to_widechar(pFilename); WCHAR* wMode = mz_utf8z_to_widechar(pMode); FILE* pFile = NULL; errno_t err = _wfopen_s(&pFile, wFilename, wMode); free(wFilename); free(wMode); return err ? NULL : pFile; } static FILE *mz_freopen(const char *pPath, const char *pMode, FILE *pStream) { WCHAR* wPath = mz_utf8z_to_widechar(pPath); WCHAR* wMode = mz_utf8z_to_widechar(pMode); FILE* pFile = NULL; errno_t err = _wfreopen_s(&pFile, wPath, wMode, pStream); free(wPath); free(wMode); return err ? NULL : pFile; } static int mz_stat64(const char *path, struct __stat64 *buffer) { WCHAR* wPath = mz_utf8z_to_widechar(path); int res = _wstat64(wPath, buffer); free(wPath); return res; } #ifndef MINIZ_NO_TIME #include #endif #define MZ_FOPEN mz_fopen #define MZ_FCLOSE fclose #define MZ_FREAD fread #define MZ_FWRITE fwrite #define MZ_FTELL64 _ftelli64 #define MZ_FSEEK64 _fseeki64 #define MZ_FILE_STAT_STRUCT _stat64 #define MZ_FILE_STAT mz_stat64 #define MZ_FFLUSH fflush #define MZ_FREOPEN mz_freopen #define MZ_DELETE_FILE remove #elif defined(__MINGW32__) || defined(__WATCOMC__) #ifndef MINIZ_NO_TIME #include #endif #define MZ_FOPEN(f, m) fopen(f, m) #define MZ_FCLOSE fclose #define MZ_FREAD fread #define MZ_FWRITE fwrite #define MZ_FTELL64 _ftelli64 #define MZ_FSEEK64 _fseeki64 #define MZ_FILE_STAT_STRUCT stat #define MZ_FILE_STAT stat #define MZ_FFLUSH fflush #define MZ_FREOPEN(f, m, s) freopen(f, m, s) #define MZ_DELETE_FILE remove #elif defined(__TINYC__) #ifndef MINIZ_NO_TIME #include #endif #define MZ_FOPEN(f, m) fopen(f, m) #define MZ_FCLOSE fclose #define MZ_FREAD fread #define MZ_FWRITE fwrite #define MZ_FTELL64 ftell #define MZ_FSEEK64 fseek #define MZ_FILE_STAT_STRUCT stat #define MZ_FILE_STAT stat #define MZ_FFLUSH fflush #define MZ_FREOPEN(f, m, s) freopen(f, m, s) #define MZ_DELETE_FILE remove #elif defined(__USE_LARGEFILE64) /* gcc, clang */ #ifndef MINIZ_NO_TIME #include #endif #define MZ_FOPEN(f, m) fopen64(f, m) #define MZ_FCLOSE fclose #define MZ_FREAD fread #define MZ_FWRITE fwrite #define MZ_FTELL64 ftello64 #define MZ_FSEEK64 fseeko64 #define MZ_FILE_STAT_STRUCT stat64 #define MZ_FILE_STAT stat64 #define MZ_FFLUSH fflush #define MZ_FREOPEN(p, m, s) freopen64(p, m, s) #define MZ_DELETE_FILE remove #elif defined(__APPLE__) || defined(__FreeBSD__) #ifndef MINIZ_NO_TIME #include #endif #define MZ_FOPEN(f, m) fopen(f, m) #define MZ_FCLOSE fclose #define MZ_FREAD fread #define MZ_FWRITE fwrite #define MZ_FTELL64 ftello #define MZ_FSEEK64 fseeko #define MZ_FILE_STAT_STRUCT stat #define MZ_FILE_STAT stat #define MZ_FFLUSH fflush #define MZ_FREOPEN(p, m, s) freopen(p, m, s) #define MZ_DELETE_FILE remove #else #pragma message("Using fopen, ftello, fseeko, stat() etc. path for file I/O - this path may not support large files.") #ifndef MINIZ_NO_TIME #include #endif #define MZ_FOPEN(f, m) fopen(f, m) #define MZ_FCLOSE fclose #define MZ_FREAD fread #define MZ_FWRITE fwrite #ifdef __STRICT_ANSI__ #define MZ_FTELL64 ftell #define MZ_FSEEK64 fseek #else #define MZ_FTELL64 ftello #define MZ_FSEEK64 fseeko #endif #define MZ_FILE_STAT_STRUCT stat #define MZ_FILE_STAT stat #define MZ_FFLUSH fflush #define MZ_FREOPEN(f, m, s) freopen(f, m, s) #define MZ_DELETE_FILE remove #endif /* #ifdef _MSC_VER */ #endif /* #ifdef MINIZ_NO_STDIO */ #define MZ_TOLOWER(c) ((((c) >= 'A') && ((c) <= 'Z')) ? ((c) - 'A' + 'a') : (c)) /* Various ZIP archive enums. To completely avoid cross platform compiler alignment and platform endian issues, miniz.c doesn't use structs for any of this stuff. */ enum { /* ZIP archive identifiers and record sizes */ MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG = 0x06054b50, MZ_ZIP_CENTRAL_DIR_HEADER_SIG = 0x02014b50, MZ_ZIP_LOCAL_DIR_HEADER_SIG = 0x04034b50, MZ_ZIP_LOCAL_DIR_HEADER_SIZE = 30, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE = 46, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE = 22, /* ZIP64 archive identifier and record sizes */ MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIG = 0x06064b50, MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG = 0x07064b50, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE = 56, MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE = 20, MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID = 0x0001, MZ_ZIP_DATA_DESCRIPTOR_ID = 0x08074b50, MZ_ZIP_DATA_DESCRIPTER_SIZE64 = 24, MZ_ZIP_DATA_DESCRIPTER_SIZE32 = 16, /* Central directory header record offsets */ MZ_ZIP_CDH_SIG_OFS = 0, MZ_ZIP_CDH_VERSION_MADE_BY_OFS = 4, MZ_ZIP_CDH_VERSION_NEEDED_OFS = 6, MZ_ZIP_CDH_BIT_FLAG_OFS = 8, MZ_ZIP_CDH_METHOD_OFS = 10, MZ_ZIP_CDH_FILE_TIME_OFS = 12, MZ_ZIP_CDH_FILE_DATE_OFS = 14, MZ_ZIP_CDH_CRC32_OFS = 16, MZ_ZIP_CDH_COMPRESSED_SIZE_OFS = 20, MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS = 24, MZ_ZIP_CDH_FILENAME_LEN_OFS = 28, MZ_ZIP_CDH_EXTRA_LEN_OFS = 30, MZ_ZIP_CDH_COMMENT_LEN_OFS = 32, MZ_ZIP_CDH_DISK_START_OFS = 34, MZ_ZIP_CDH_INTERNAL_ATTR_OFS = 36, MZ_ZIP_CDH_EXTERNAL_ATTR_OFS = 38, MZ_ZIP_CDH_LOCAL_HEADER_OFS = 42, /* Local directory header offsets */ MZ_ZIP_LDH_SIG_OFS = 0, MZ_ZIP_LDH_VERSION_NEEDED_OFS = 4, MZ_ZIP_LDH_BIT_FLAG_OFS = 6, MZ_ZIP_LDH_METHOD_OFS = 8, MZ_ZIP_LDH_FILE_TIME_OFS = 10, MZ_ZIP_LDH_FILE_DATE_OFS = 12, MZ_ZIP_LDH_CRC32_OFS = 14, MZ_ZIP_LDH_COMPRESSED_SIZE_OFS = 18, MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS = 22, MZ_ZIP_LDH_FILENAME_LEN_OFS = 26, MZ_ZIP_LDH_EXTRA_LEN_OFS = 28, MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR = 1 << 3, /* End of central directory offsets */ MZ_ZIP_ECDH_SIG_OFS = 0, MZ_ZIP_ECDH_NUM_THIS_DISK_OFS = 4, MZ_ZIP_ECDH_NUM_DISK_CDIR_OFS = 6, MZ_ZIP_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS = 8, MZ_ZIP_ECDH_CDIR_TOTAL_ENTRIES_OFS = 10, MZ_ZIP_ECDH_CDIR_SIZE_OFS = 12, MZ_ZIP_ECDH_CDIR_OFS_OFS = 16, MZ_ZIP_ECDH_COMMENT_SIZE_OFS = 20, /* ZIP64 End of central directory locator offsets */ MZ_ZIP64_ECDL_SIG_OFS = 0, /* 4 bytes */ MZ_ZIP64_ECDL_NUM_DISK_CDIR_OFS = 4, /* 4 bytes */ MZ_ZIP64_ECDL_REL_OFS_TO_ZIP64_ECDR_OFS = 8, /* 8 bytes */ MZ_ZIP64_ECDL_TOTAL_NUMBER_OF_DISKS_OFS = 16, /* 4 bytes */ /* ZIP64 End of central directory header offsets */ MZ_ZIP64_ECDH_SIG_OFS = 0, /* 4 bytes */ MZ_ZIP64_ECDH_SIZE_OF_RECORD_OFS = 4, /* 8 bytes */ MZ_ZIP64_ECDH_VERSION_MADE_BY_OFS = 12, /* 2 bytes */ MZ_ZIP64_ECDH_VERSION_NEEDED_OFS = 14, /* 2 bytes */ MZ_ZIP64_ECDH_NUM_THIS_DISK_OFS = 16, /* 4 bytes */ MZ_ZIP64_ECDH_NUM_DISK_CDIR_OFS = 20, /* 4 bytes */ MZ_ZIP64_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS = 24, /* 8 bytes */ MZ_ZIP64_ECDH_CDIR_TOTAL_ENTRIES_OFS = 32, /* 8 bytes */ MZ_ZIP64_ECDH_CDIR_SIZE_OFS = 40, /* 8 bytes */ MZ_ZIP64_ECDH_CDIR_OFS_OFS = 48, /* 8 bytes */ MZ_ZIP_VERSION_MADE_BY_DOS_FILESYSTEM_ID = 0, MZ_ZIP_DOS_DIR_ATTRIBUTE_BITFLAG = 0x10, MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED = 1, MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG = 32, MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION = 64, MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_LOCAL_DIR_IS_MASKED = 8192, MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_UTF8 = 1 << 11 }; typedef struct { void *m_p; size_t m_size, m_capacity; mz_uint m_element_size; } mz_zip_array; struct mz_zip_internal_state_tag { mz_zip_array m_central_dir; mz_zip_array m_central_dir_offsets; mz_zip_array m_sorted_central_dir_offsets; /* The flags passed in when the archive is initially opened. */ mz_uint32 m_init_flags; /* MZ_TRUE if the archive has a zip64 end of central directory headers, etc. */ mz_bool m_zip64; /* MZ_TRUE if we found zip64 extended info in the central directory (m_zip64 will also be slammed to true too, even if we didn't find a zip64 end of central dir header, etc.) */ mz_bool m_zip64_has_extended_info_fields; /* These fields are used by the file, FILE, memory, and memory/heap read/write helpers. */ MZ_FILE *m_pFile; mz_uint64 m_file_archive_start_ofs; void *m_pMem; size_t m_mem_size; size_t m_mem_capacity; }; #define MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(array_ptr, element_size) (array_ptr)->m_element_size = element_size #if defined(DEBUG) || defined(_DEBUG) static MZ_FORCEINLINE mz_uint mz_zip_array_range_check(const mz_zip_array *pArray, mz_uint index) { MZ_ASSERT(index < pArray->m_size); return index; } #define MZ_ZIP_ARRAY_ELEMENT(array_ptr, element_type, index) ((element_type *)((array_ptr)->m_p))[mz_zip_array_range_check(array_ptr, index)] #else #define MZ_ZIP_ARRAY_ELEMENT(array_ptr, element_type, index) ((element_type *)((array_ptr)->m_p))[index] #endif static MZ_FORCEINLINE void mz_zip_array_init(mz_zip_array *pArray, mz_uint32 element_size) { memset(pArray, 0, sizeof(mz_zip_array)); pArray->m_element_size = element_size; } static MZ_FORCEINLINE void mz_zip_array_clear(mz_zip_archive *pZip, mz_zip_array *pArray) { pZip->m_pFree(pZip->m_pAlloc_opaque, pArray->m_p); memset(pArray, 0, sizeof(mz_zip_array)); } static mz_bool mz_zip_array_ensure_capacity(mz_zip_archive *pZip, mz_zip_array *pArray, size_t min_new_capacity, mz_uint growing) { void *pNew_p; size_t new_capacity = min_new_capacity; MZ_ASSERT(pArray->m_element_size); if (pArray->m_capacity >= min_new_capacity) return MZ_TRUE; if (growing) { new_capacity = MZ_MAX(1, pArray->m_capacity); while (new_capacity < min_new_capacity) new_capacity *= 2; } if (NULL == (pNew_p = pZip->m_pRealloc(pZip->m_pAlloc_opaque, pArray->m_p, pArray->m_element_size, new_capacity))) return MZ_FALSE; pArray->m_p = pNew_p; pArray->m_capacity = new_capacity; return MZ_TRUE; } static MZ_FORCEINLINE mz_bool mz_zip_array_reserve(mz_zip_archive *pZip, mz_zip_array *pArray, size_t new_capacity, mz_uint growing) { if (new_capacity > pArray->m_capacity) { if (!mz_zip_array_ensure_capacity(pZip, pArray, new_capacity, growing)) return MZ_FALSE; } return MZ_TRUE; } static MZ_FORCEINLINE mz_bool mz_zip_array_resize(mz_zip_archive *pZip, mz_zip_array *pArray, size_t new_size, mz_uint growing) { if (new_size > pArray->m_capacity) { if (!mz_zip_array_ensure_capacity(pZip, pArray, new_size, growing)) return MZ_FALSE; } pArray->m_size = new_size; return MZ_TRUE; } static MZ_FORCEINLINE mz_bool mz_zip_array_ensure_room(mz_zip_archive *pZip, mz_zip_array *pArray, size_t n) { return mz_zip_array_reserve(pZip, pArray, pArray->m_size + n, MZ_TRUE); } static MZ_FORCEINLINE mz_bool mz_zip_array_push_back(mz_zip_archive *pZip, mz_zip_array *pArray, const void *pElements, size_t n) { size_t orig_size = pArray->m_size; if (!mz_zip_array_resize(pZip, pArray, orig_size + n, MZ_TRUE)) return MZ_FALSE; if (n > 0) memcpy((mz_uint8 *)pArray->m_p + orig_size * pArray->m_element_size, pElements, n * pArray->m_element_size); return MZ_TRUE; } #ifndef MINIZ_NO_TIME static MZ_TIME_T mz_zip_dos_to_time_t(int dos_time, int dos_date) { struct tm tm; memset(&tm, 0, sizeof(tm)); tm.tm_isdst = -1; tm.tm_year = ((dos_date >> 9) & 127) + 1980 - 1900; tm.tm_mon = ((dos_date >> 5) & 15) - 1; tm.tm_mday = dos_date & 31; tm.tm_hour = (dos_time >> 11) & 31; tm.tm_min = (dos_time >> 5) & 63; tm.tm_sec = (dos_time << 1) & 62; return mktime(&tm); } #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS static void mz_zip_time_t_to_dos_time(MZ_TIME_T time, mz_uint16 *pDOS_time, mz_uint16 *pDOS_date) { #ifdef _MSC_VER struct tm tm_struct; struct tm *tm = &tm_struct; errno_t err = localtime_s(tm, &time); if (err) { *pDOS_date = 0; *pDOS_time = 0; return; } #else struct tm *tm = localtime(&time); #endif /* #ifdef _MSC_VER */ *pDOS_time = (mz_uint16)(((tm->tm_hour) << 11) + ((tm->tm_min) << 5) + ((tm->tm_sec) >> 1)); *pDOS_date = (mz_uint16)(((tm->tm_year + 1900 - 1980) << 9) + ((tm->tm_mon + 1) << 5) + tm->tm_mday); } #endif /* MINIZ_NO_ARCHIVE_WRITING_APIS */ #ifndef MINIZ_NO_STDIO #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS static mz_bool mz_zip_get_file_modified_time(const char *pFilename, MZ_TIME_T *pTime) { struct MZ_FILE_STAT_STRUCT file_stat; /* On Linux with x86 glibc, this call will fail on large files (I think >= 0x80000000 bytes) unless you compiled with _LARGEFILE64_SOURCE. Argh. */ if (MZ_FILE_STAT(pFilename, &file_stat) != 0) return MZ_FALSE; *pTime = file_stat.st_mtime; return MZ_TRUE; } #endif /* #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS*/ static mz_bool mz_zip_set_file_times(const char *pFilename, MZ_TIME_T access_time, MZ_TIME_T modified_time) { struct utimbuf t; memset(&t, 0, sizeof(t)); t.actime = access_time; t.modtime = modified_time; return !utime(pFilename, &t); } #endif /* #ifndef MINIZ_NO_STDIO */ #endif /* #ifndef MINIZ_NO_TIME */ static MZ_FORCEINLINE mz_bool mz_zip_set_error(mz_zip_archive *pZip, mz_zip_error err_num) { if (pZip) pZip->m_last_error = err_num; return MZ_FALSE; } static mz_bool mz_zip_reader_init_internal(mz_zip_archive *pZip, mz_uint flags) { (void)flags; if ((!pZip) || (pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_INVALID)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); if (!pZip->m_pAlloc) pZip->m_pAlloc = miniz_def_alloc_func; if (!pZip->m_pFree) pZip->m_pFree = miniz_def_free_func; if (!pZip->m_pRealloc) pZip->m_pRealloc = miniz_def_realloc_func; pZip->m_archive_size = 0; pZip->m_central_directory_file_ofs = 0; pZip->m_total_files = 0; pZip->m_last_error = MZ_ZIP_NO_ERROR; if (NULL == (pZip->m_pState = (mz_zip_internal_state *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(mz_zip_internal_state)))) return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); memset(pZip->m_pState, 0, sizeof(mz_zip_internal_state)); MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir, sizeof(mz_uint8)); MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir_offsets, sizeof(mz_uint32)); MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_sorted_central_dir_offsets, sizeof(mz_uint32)); pZip->m_pState->m_init_flags = flags; pZip->m_pState->m_zip64 = MZ_FALSE; pZip->m_pState->m_zip64_has_extended_info_fields = MZ_FALSE; pZip->m_zip_mode = MZ_ZIP_MODE_READING; return MZ_TRUE; } static MZ_FORCEINLINE mz_bool mz_zip_reader_filename_less(const mz_zip_array *pCentral_dir_array, const mz_zip_array *pCentral_dir_offsets, mz_uint l_index, mz_uint r_index) { const mz_uint8 *pL = &MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_array, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_offsets, mz_uint32, l_index)), *pE; const mz_uint8 *pR = &MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_array, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_offsets, mz_uint32, r_index)); mz_uint l_len = MZ_READ_LE16(pL + MZ_ZIP_CDH_FILENAME_LEN_OFS), r_len = MZ_READ_LE16(pR + MZ_ZIP_CDH_FILENAME_LEN_OFS); mz_uint8 l = 0, r = 0; pL += MZ_ZIP_CENTRAL_DIR_HEADER_SIZE; pR += MZ_ZIP_CENTRAL_DIR_HEADER_SIZE; pE = pL + MZ_MIN(l_len, r_len); while (pL < pE) { if ((l = MZ_TOLOWER(*pL)) != (r = MZ_TOLOWER(*pR))) break; pL++; pR++; } return (pL == pE) ? (l_len < r_len) : (l < r); } #define MZ_SWAP_UINT32(a, b) \ do \ { \ mz_uint32 t = a; \ a = b; \ b = t; \ } \ MZ_MACRO_END /* Heap sort of lowercased filenames, used to help accelerate plain central directory searches by mz_zip_reader_locate_file(). (Could also use qsort(), but it could allocate memory.) */ static void mz_zip_reader_sort_central_dir_offsets_by_filename(mz_zip_archive *pZip) { mz_zip_internal_state *pState = pZip->m_pState; const mz_zip_array *pCentral_dir_offsets = &pState->m_central_dir_offsets; const mz_zip_array *pCentral_dir = &pState->m_central_dir; mz_uint32 *pIndices; mz_uint32 start, end; const mz_uint32 size = pZip->m_total_files; if (size <= 1U) return; pIndices = &MZ_ZIP_ARRAY_ELEMENT(&pState->m_sorted_central_dir_offsets, mz_uint32, 0); start = (size - 2U) >> 1U; for (;;) { mz_uint64 child, root = start; for (;;) { if ((child = (root << 1U) + 1U) >= size) break; child += (((child + 1U) < size) && (mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[child], pIndices[child + 1U]))); if (!mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[root], pIndices[child])) break; MZ_SWAP_UINT32(pIndices[root], pIndices[child]); root = child; } if (!start) break; start--; } end = size - 1; while (end > 0) { mz_uint64 child, root = 0; MZ_SWAP_UINT32(pIndices[end], pIndices[0]); for (;;) { if ((child = (root << 1U) + 1U) >= end) break; child += (((child + 1U) < end) && mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[child], pIndices[child + 1U])); if (!mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[root], pIndices[child])) break; MZ_SWAP_UINT32(pIndices[root], pIndices[child]); root = child; } end--; } } static mz_bool mz_zip_reader_locate_header_sig(mz_zip_archive *pZip, mz_uint32 record_sig, mz_uint32 record_size, mz_int64 *pOfs) { mz_int64 cur_file_ofs; mz_uint32 buf_u32[4096 / sizeof(mz_uint32)]; mz_uint8 *pBuf = (mz_uint8 *)buf_u32; /* Basic sanity checks - reject files which are too small */ if (pZip->m_archive_size < record_size) return MZ_FALSE; /* Find the record by scanning the file from the end towards the beginning. */ cur_file_ofs = MZ_MAX((mz_int64)pZip->m_archive_size - (mz_int64)sizeof(buf_u32), 0); for (;;) { int i, n = (int)MZ_MIN(sizeof(buf_u32), pZip->m_archive_size - cur_file_ofs); if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pBuf, n) != (mz_uint)n) return MZ_FALSE; for (i = n - 4; i >= 0; --i) { mz_uint s = MZ_READ_LE32(pBuf + i); if (s == record_sig) { if ((pZip->m_archive_size - (cur_file_ofs + i)) >= record_size) break; } } if (i >= 0) { cur_file_ofs += i; break; } /* Give up if we've searched the entire file, or we've gone back "too far" (~64kb) */ if ((!cur_file_ofs) || ((pZip->m_archive_size - cur_file_ofs) >= (MZ_UINT16_MAX + record_size))) return MZ_FALSE; cur_file_ofs = MZ_MAX(cur_file_ofs - (sizeof(buf_u32) - 3), 0); } *pOfs = cur_file_ofs; return MZ_TRUE; } static mz_bool mz_zip_reader_read_central_dir(mz_zip_archive *pZip, mz_uint flags) { mz_uint cdir_size = 0, cdir_entries_on_this_disk = 0, num_this_disk = 0, cdir_disk_index = 0; mz_uint64 cdir_ofs = 0; mz_int64 cur_file_ofs = 0; const mz_uint8 *p; mz_uint32 buf_u32[4096 / sizeof(mz_uint32)]; mz_uint8 *pBuf = (mz_uint8 *)buf_u32; mz_bool sort_central_dir = ((flags & MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY) == 0); mz_uint32 zip64_end_of_central_dir_locator_u32[(MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; mz_uint8 *pZip64_locator = (mz_uint8 *)zip64_end_of_central_dir_locator_u32; mz_uint32 zip64_end_of_central_dir_header_u32[(MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; mz_uint8 *pZip64_end_of_central_dir = (mz_uint8 *)zip64_end_of_central_dir_header_u32; mz_uint64 zip64_end_of_central_dir_ofs = 0; /* Basic sanity checks - reject files which are too small, and check the first 4 bytes of the file to make sure a local header is there. */ if (pZip->m_archive_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); if (!mz_zip_reader_locate_header_sig(pZip, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE, &cur_file_ofs)) return mz_zip_set_error(pZip, MZ_ZIP_FAILED_FINDING_CENTRAL_DIR); /* Read and verify the end of central directory record. */ if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pBuf, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) != MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); if (MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_SIG_OFS) != MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG) return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); if (cur_file_ofs >= (MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE)) { if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs - MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE, pZip64_locator, MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) == MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) { if (MZ_READ_LE32(pZip64_locator + MZ_ZIP64_ECDL_SIG_OFS) == MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG) { zip64_end_of_central_dir_ofs = MZ_READ_LE64(pZip64_locator + MZ_ZIP64_ECDL_REL_OFS_TO_ZIP64_ECDR_OFS); if (zip64_end_of_central_dir_ofs > (pZip->m_archive_size - MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE)) return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); if (pZip->m_pRead(pZip->m_pIO_opaque, zip64_end_of_central_dir_ofs, pZip64_end_of_central_dir, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) == MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) { if (MZ_READ_LE32(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_SIG_OFS) == MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIG) { pZip->m_pState->m_zip64 = MZ_TRUE; } } } } } pZip->m_total_files = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_CDIR_TOTAL_ENTRIES_OFS); cdir_entries_on_this_disk = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS); num_this_disk = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_NUM_THIS_DISK_OFS); cdir_disk_index = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_NUM_DISK_CDIR_OFS); cdir_size = MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_CDIR_SIZE_OFS); cdir_ofs = MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_CDIR_OFS_OFS); if (pZip->m_pState->m_zip64) { mz_uint32 zip64_total_num_of_disks = MZ_READ_LE32(pZip64_locator + MZ_ZIP64_ECDL_TOTAL_NUMBER_OF_DISKS_OFS); mz_uint64 zip64_cdir_total_entries = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_TOTAL_ENTRIES_OFS); mz_uint64 zip64_cdir_total_entries_on_this_disk = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS); mz_uint64 zip64_size_of_end_of_central_dir_record = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_SIZE_OF_RECORD_OFS); mz_uint64 zip64_size_of_central_directory = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_SIZE_OFS); if (zip64_size_of_end_of_central_dir_record < (MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE - 12)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); if (zip64_total_num_of_disks != 1U) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); /* Check for miniz's practical limits */ if (zip64_cdir_total_entries > MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); pZip->m_total_files = (mz_uint32)zip64_cdir_total_entries; if (zip64_cdir_total_entries_on_this_disk > MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); cdir_entries_on_this_disk = (mz_uint32)zip64_cdir_total_entries_on_this_disk; /* Check for miniz's current practical limits (sorry, this should be enough for millions of files) */ if (zip64_size_of_central_directory > MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); cdir_size = (mz_uint32)zip64_size_of_central_directory; num_this_disk = MZ_READ_LE32(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_NUM_THIS_DISK_OFS); cdir_disk_index = MZ_READ_LE32(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_NUM_DISK_CDIR_OFS); cdir_ofs = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_OFS_OFS); } if (pZip->m_total_files != cdir_entries_on_this_disk) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); if (((num_this_disk | cdir_disk_index) != 0) && ((num_this_disk != 1) || (cdir_disk_index != 1))) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); if (cdir_size < (mz_uint64)pZip->m_total_files * MZ_ZIP_CENTRAL_DIR_HEADER_SIZE) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); if ((cdir_ofs + (mz_uint64)cdir_size) > pZip->m_archive_size) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); pZip->m_central_directory_file_ofs = cdir_ofs; if (pZip->m_total_files) { mz_uint i, n; /* Read the entire central directory into a heap block, and allocate another heap block to hold the unsorted central dir file record offsets, and possibly another to hold the sorted indices. */ if ((!mz_zip_array_resize(pZip, &pZip->m_pState->m_central_dir, cdir_size, MZ_FALSE)) || (!mz_zip_array_resize(pZip, &pZip->m_pState->m_central_dir_offsets, pZip->m_total_files, MZ_FALSE))) return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); if (sort_central_dir) { if (!mz_zip_array_resize(pZip, &pZip->m_pState->m_sorted_central_dir_offsets, pZip->m_total_files, MZ_FALSE)) return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } if (pZip->m_pRead(pZip->m_pIO_opaque, cdir_ofs, pZip->m_pState->m_central_dir.m_p, cdir_size) != cdir_size) return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); /* Now create an index into the central directory file records, do some basic sanity checking on each record */ p = (const mz_uint8 *)pZip->m_pState->m_central_dir.m_p; for (n = cdir_size, i = 0; i < pZip->m_total_files; ++i) { mz_uint total_header_size, disk_index, bit_flags, filename_size, ext_data_size; mz_uint64 comp_size, decomp_size, local_header_ofs; if ((n < MZ_ZIP_CENTRAL_DIR_HEADER_SIZE) || (MZ_READ_LE32(p) != MZ_ZIP_CENTRAL_DIR_HEADER_SIG)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, i) = (mz_uint32)(p - (const mz_uint8 *)pZip->m_pState->m_central_dir.m_p); if (sort_central_dir) MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_sorted_central_dir_offsets, mz_uint32, i) = i; comp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS); decomp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS); local_header_ofs = MZ_READ_LE32(p + MZ_ZIP_CDH_LOCAL_HEADER_OFS); filename_size = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); ext_data_size = MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS); if ((!pZip->m_pState->m_zip64_has_extended_info_fields) && (ext_data_size) && (MZ_MAX(MZ_MAX(comp_size, decomp_size), local_header_ofs) == MZ_UINT32_MAX)) { /* Attempt to find zip64 extended information field in the entry's extra data */ mz_uint32 extra_size_remaining = ext_data_size; if (extra_size_remaining) { const mz_uint8 *pExtra_data; void* buf = NULL; if (MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size + ext_data_size > n) { buf = MZ_MALLOC(ext_data_size); if(buf==NULL) return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); if (pZip->m_pRead(pZip->m_pIO_opaque, cdir_ofs + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size, buf, ext_data_size) != ext_data_size) { MZ_FREE(buf); return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); } pExtra_data = (mz_uint8*)buf; } else { pExtra_data = p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size; } do { mz_uint32 field_id; mz_uint32 field_data_size; if (extra_size_remaining < (sizeof(mz_uint16) * 2)) { MZ_FREE(buf); return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); } field_id = MZ_READ_LE16(pExtra_data); field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); if ((field_data_size + sizeof(mz_uint16) * 2) > extra_size_remaining) { MZ_FREE(buf); return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); } if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) { /* Ok, the archive didn't have any zip64 headers but it uses a zip64 extended information field so mark it as zip64 anyway (this can occur with infozip's zip util when it reads compresses files from stdin). */ pZip->m_pState->m_zip64 = MZ_TRUE; pZip->m_pState->m_zip64_has_extended_info_fields = MZ_TRUE; break; } pExtra_data += sizeof(mz_uint16) * 2 + field_data_size; extra_size_remaining = extra_size_remaining - sizeof(mz_uint16) * 2 - field_data_size; } while (extra_size_remaining); MZ_FREE(buf); } } /* I've seen archives that aren't marked as zip64 that uses zip64 ext data, argh */ if ((comp_size != MZ_UINT32_MAX) && (decomp_size != MZ_UINT32_MAX)) { if (((!MZ_READ_LE32(p + MZ_ZIP_CDH_METHOD_OFS)) && (decomp_size != comp_size)) || (decomp_size && !comp_size)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); } disk_index = MZ_READ_LE16(p + MZ_ZIP_CDH_DISK_START_OFS); if ((disk_index == MZ_UINT16_MAX) || ((disk_index != num_this_disk) && (disk_index != 1))) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK); if (comp_size != MZ_UINT32_MAX) { if (((mz_uint64)MZ_READ_LE32(p + MZ_ZIP_CDH_LOCAL_HEADER_OFS) + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + comp_size) > pZip->m_archive_size) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); } bit_flags = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS); if (bit_flags & MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_LOCAL_DIR_IS_MASKED) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); if ((total_header_size = MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS) + MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS) + MZ_READ_LE16(p + MZ_ZIP_CDH_COMMENT_LEN_OFS)) > n) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); n -= total_header_size; p += total_header_size; } } if (sort_central_dir) mz_zip_reader_sort_central_dir_offsets_by_filename(pZip); return MZ_TRUE; } void mz_zip_zero_struct(mz_zip_archive *pZip) { if (pZip) MZ_CLEAR_PTR(pZip); } static mz_bool mz_zip_reader_end_internal(mz_zip_archive *pZip, mz_bool set_last_error) { mz_bool status = MZ_TRUE; if (!pZip) return MZ_FALSE; if ((!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || (pZip->m_zip_mode != MZ_ZIP_MODE_READING)) { if (set_last_error) pZip->m_last_error = MZ_ZIP_INVALID_PARAMETER; return MZ_FALSE; } if (pZip->m_pState) { mz_zip_internal_state *pState = pZip->m_pState; pZip->m_pState = NULL; mz_zip_array_clear(pZip, &pState->m_central_dir); mz_zip_array_clear(pZip, &pState->m_central_dir_offsets); mz_zip_array_clear(pZip, &pState->m_sorted_central_dir_offsets); #ifndef MINIZ_NO_STDIO if (pState->m_pFile) { if (pZip->m_zip_type == MZ_ZIP_TYPE_FILE) { if (MZ_FCLOSE(pState->m_pFile) == EOF) { if (set_last_error) pZip->m_last_error = MZ_ZIP_FILE_CLOSE_FAILED; status = MZ_FALSE; } } pState->m_pFile = NULL; } #endif /* #ifndef MINIZ_NO_STDIO */ pZip->m_pFree(pZip->m_pAlloc_opaque, pState); } pZip->m_zip_mode = MZ_ZIP_MODE_INVALID; return status; } mz_bool mz_zip_reader_end(mz_zip_archive *pZip) { return mz_zip_reader_end_internal(pZip, MZ_TRUE); } mz_bool mz_zip_reader_init(mz_zip_archive *pZip, mz_uint64 size, mz_uint flags) { if ((!pZip) || (!pZip->m_pRead)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); if (!mz_zip_reader_init_internal(pZip, flags)) return MZ_FALSE; pZip->m_zip_type = MZ_ZIP_TYPE_USER; pZip->m_archive_size = size; if (!mz_zip_reader_read_central_dir(pZip, flags)) { mz_zip_reader_end_internal(pZip, MZ_FALSE); return MZ_FALSE; } return MZ_TRUE; } static size_t mz_zip_mem_read_func(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n) { mz_zip_archive *pZip = (mz_zip_archive *)pOpaque; size_t s = (file_ofs >= pZip->m_archive_size) ? 0 : (size_t)MZ_MIN(pZip->m_archive_size - file_ofs, n); memcpy(pBuf, (const mz_uint8 *)pZip->m_pState->m_pMem + file_ofs, s); return s; } mz_bool mz_zip_reader_init_mem(mz_zip_archive *pZip, const void *pMem, size_t size, mz_uint flags) { if (!pMem) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); if (size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); if (!mz_zip_reader_init_internal(pZip, flags)) return MZ_FALSE; pZip->m_zip_type = MZ_ZIP_TYPE_MEMORY; pZip->m_archive_size = size; pZip->m_pRead = mz_zip_mem_read_func; pZip->m_pIO_opaque = pZip; pZip->m_pNeeds_keepalive = NULL; #ifdef __cplusplus pZip->m_pState->m_pMem = const_cast(pMem); #else pZip->m_pState->m_pMem = (void *)pMem; #endif pZip->m_pState->m_mem_size = size; if (!mz_zip_reader_read_central_dir(pZip, flags)) { mz_zip_reader_end_internal(pZip, MZ_FALSE); return MZ_FALSE; } return MZ_TRUE; } #ifndef MINIZ_NO_STDIO static size_t mz_zip_file_read_func(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n) { mz_zip_archive *pZip = (mz_zip_archive *)pOpaque; mz_int64 cur_ofs = MZ_FTELL64(pZip->m_pState->m_pFile); file_ofs += pZip->m_pState->m_file_archive_start_ofs; if (((mz_int64)file_ofs < 0) || (((cur_ofs != (mz_int64)file_ofs)) && (MZ_FSEEK64(pZip->m_pState->m_pFile, (mz_int64)file_ofs, SEEK_SET)))) return 0; return MZ_FREAD(pBuf, 1, n, pZip->m_pState->m_pFile); } mz_bool mz_zip_reader_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint32 flags) { return mz_zip_reader_init_file_v2(pZip, pFilename, flags, 0, 0); } mz_bool mz_zip_reader_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags, mz_uint64 file_start_ofs, mz_uint64 archive_size) { mz_uint64 file_size; MZ_FILE *pFile; if ((!pZip) || (!pFilename) || ((archive_size) && (archive_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE))) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); pFile = MZ_FOPEN(pFilename, "rb"); if (!pFile) return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); file_size = archive_size; if (!file_size) { if (MZ_FSEEK64(pFile, 0, SEEK_END)) { MZ_FCLOSE(pFile); return mz_zip_set_error(pZip, MZ_ZIP_FILE_SEEK_FAILED); } file_size = MZ_FTELL64(pFile); } /* TODO: Better sanity check archive_size and the # of actual remaining bytes */ if (file_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) { MZ_FCLOSE(pFile); return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); } if (!mz_zip_reader_init_internal(pZip, flags)) { MZ_FCLOSE(pFile); return MZ_FALSE; } pZip->m_zip_type = MZ_ZIP_TYPE_FILE; pZip->m_pRead = mz_zip_file_read_func; pZip->m_pIO_opaque = pZip; pZip->m_pState->m_pFile = pFile; pZip->m_archive_size = file_size; pZip->m_pState->m_file_archive_start_ofs = file_start_ofs; if (!mz_zip_reader_read_central_dir(pZip, flags)) { mz_zip_reader_end_internal(pZip, MZ_FALSE); return MZ_FALSE; } return MZ_TRUE; } mz_bool mz_zip_reader_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint64 archive_size, mz_uint flags) { mz_uint64 cur_file_ofs; if ((!pZip) || (!pFile)) return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); cur_file_ofs = MZ_FTELL64(pFile); if (!archive_size) { if (MZ_FSEEK64(pFile, 0, SEEK_END)) return mz_zip_set_error(pZip, MZ_ZIP_FILE_SEEK_FAILED); archive_size = MZ_FTELL64(pFile) - cur_file_ofs; if (archive_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE); } if (!mz_zip_reader_init_internal(pZip, flags)) return MZ_FALSE; pZip->m_zip_type = MZ_ZIP_TYPE_CFILE; pZip->m_pRead = mz_zip_file_read_func; pZip->m_pIO_opaque = pZip; pZip->m_pState->m_pFile = pFile; pZip->m_archive_size = archive_size; pZip->m_pState->m_file_archive_start_ofs = cur_file_ofs; if (!mz_zip_reader_read_central_dir(pZip, flags)) { mz_zip_reader_end_internal(pZip, MZ_FALSE); return MZ_FALSE; } return MZ_TRUE; } #endif /* #ifndef MINIZ_NO_STDIO */ static MZ_FORCEINLINE const mz_uint8 *mz_zip_get_cdh(mz_zip_archive *pZip, mz_uint file_index) { if ((!pZip) || (!pZip->m_pState) || (file_index >= pZip->m_total_files)) return NULL; return &MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, file_index)); } mz_bool mz_zip_reader_is_file_encrypted(mz_zip_archive *pZip, mz_uint file_index) { mz_uint m_bit_flag; const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index); if (!p) { mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); return MZ_FALSE; } m_bit_flag = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS); return (m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION)) != 0; } mz_bool mz_zip_reader_is_file_supported(mz_zip_archive *pZip, mz_uint file_index) { mz_uint bit_flag; mz_uint method; const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index); if (!p) { mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); return MZ_FALSE; } method = MZ_READ_LE16(p + MZ_ZIP_CDH_METHOD_OFS); bit_flag = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS); if ((method != 0) && (method != MZ_DEFLATED)) { mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD); return MZ_FALSE; } if (bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION)) { mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); return MZ_FALSE; } if (bit_flag & MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG) { mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE); return MZ_FALSE; } return MZ_TRUE; } mz_bool mz_zip_reader_is_file_a_directory(mz_zip_archive *pZip, mz_uint file_index) { mz_uint filename_len, attribute_mapping_id, external_attr; const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index); if (!p) { mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); return MZ_FALSE; } filename_len = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); if (filename_len) { if (*(p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_len - 1) == '/') return MZ_TRUE; } /* Bugfix: This code was also checking if the internal attribute was non-zero, which wasn't correct. */ /* Most/all zip writers (hopefully) set DOS file/directory attributes in the low 16-bits, so check for the DOS directory flag and ignore the source OS ID in the created by field. */ /* FIXME: Remove this check? Is it necessary - we already check the filename. */ attribute_mapping_id = MZ_READ_LE16(p + MZ_ZIP_CDH_VERSION_MADE_BY_OFS) >> 8; (void)attribute_mapping_id; external_attr = MZ_READ_LE32(p + MZ_ZIP_CDH_EXTERNAL_ATTR_OFS); if ((external_attr & MZ_ZIP_DOS_DIR_ATTRIBUTE_BITFLAG) != 0) { return MZ_TRUE; } return MZ_FALSE; } static mz_bool mz_zip_file_stat_internal(mz_zip_archive *pZip, mz_uint file_index, const mz_uint8 *pCentral_dir_header, mz_zip_archive_file_stat *pStat, mz_bool *pFound_zip64_extra_data) { mz_uint n; const mz_uint8 *p = pCentral_dir_header; if (pFound_zip64_extra_data) *pFound_zip64_extra_data = MZ_FALSE; if ((!p) || (!pStat)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); /* Extract fields from the central directory record. */ pStat->m_file_index = file_index; pStat->m_central_dir_ofs = MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, file_index); pStat->m_version_made_by = MZ_READ_LE16(p + MZ_ZIP_CDH_VERSION_MADE_BY_OFS); pStat->m_version_needed = MZ_READ_LE16(p + MZ_ZIP_CDH_VERSION_NEEDED_OFS); pStat->m_bit_flag = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS); pStat->m_method = MZ_READ_LE16(p + MZ_ZIP_CDH_METHOD_OFS); #ifndef MINIZ_NO_TIME pStat->m_time = mz_zip_dos_to_time_t(MZ_READ_LE16(p + MZ_ZIP_CDH_FILE_TIME_OFS), MZ_READ_LE16(p + MZ_ZIP_CDH_FILE_DATE_OFS)); #endif pStat->m_crc32 = MZ_READ_LE32(p + MZ_ZIP_CDH_CRC32_OFS); pStat->m_comp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS); pStat->m_uncomp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS); pStat->m_internal_attr = MZ_READ_LE16(p + MZ_ZIP_CDH_INTERNAL_ATTR_OFS); pStat->m_external_attr = MZ_READ_LE32(p + MZ_ZIP_CDH_EXTERNAL_ATTR_OFS); pStat->m_local_header_ofs = MZ_READ_LE32(p + MZ_ZIP_CDH_LOCAL_HEADER_OFS); /* Copy as much of the filename and comment as possible. */ n = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); n = MZ_MIN(n, MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE - 1); memcpy(pStat->m_filename, p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, n); pStat->m_filename[n] = '\0'; n = MZ_READ_LE16(p + MZ_ZIP_CDH_COMMENT_LEN_OFS); n = MZ_MIN(n, MZ_ZIP_MAX_ARCHIVE_FILE_COMMENT_SIZE - 1); pStat->m_comment_size = n; memcpy(pStat->m_comment, p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS) + MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS), n); pStat->m_comment[n] = '\0'; /* Set some flags for convienance */ pStat->m_is_directory = mz_zip_reader_is_file_a_directory(pZip, file_index); pStat->m_is_encrypted = mz_zip_reader_is_file_encrypted(pZip, file_index); pStat->m_is_supported = mz_zip_reader_is_file_supported(pZip, file_index); /* See if we need to read any zip64 extended information fields. */ /* Confusingly, these zip64 fields can be present even on non-zip64 archives (Debian zip on a huge files from stdin piped to stdout creates them). */ if (MZ_MAX(MZ_MAX(pStat->m_comp_size, pStat->m_uncomp_size), pStat->m_local_header_ofs) == MZ_UINT32_MAX) { /* Attempt to find zip64 extended information field in the entry's extra data */ mz_uint32 extra_size_remaining = MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS); if (extra_size_remaining) { const mz_uint8 *pExtra_data = p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); do { mz_uint32 field_id; mz_uint32 field_data_size; if (extra_size_remaining < (sizeof(mz_uint16) * 2)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); field_id = MZ_READ_LE16(pExtra_data); field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); if ((field_data_size + sizeof(mz_uint16) * 2) > extra_size_remaining) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) { const mz_uint8 *pField_data = pExtra_data + sizeof(mz_uint16) * 2; mz_uint32 field_data_remaining = field_data_size; if (pFound_zip64_extra_data) *pFound_zip64_extra_data = MZ_TRUE; if (pStat->m_uncomp_size == MZ_UINT32_MAX) { if (field_data_remaining < sizeof(mz_uint64)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); pStat->m_uncomp_size = MZ_READ_LE64(pField_data); pField_data += sizeof(mz_uint64); field_data_remaining -= sizeof(mz_uint64); } if (pStat->m_comp_size == MZ_UINT32_MAX) { if (field_data_remaining < sizeof(mz_uint64)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); pStat->m_comp_size = MZ_READ_LE64(pField_data); pField_data += sizeof(mz_uint64); field_data_remaining -= sizeof(mz_uint64); } if (pStat->m_local_header_ofs == MZ_UINT32_MAX) { if (field_data_remaining < sizeof(mz_uint64)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); pStat->m_local_header_ofs = MZ_READ_LE64(pField_data); pField_data += sizeof(mz_uint64); field_data_remaining -= sizeof(mz_uint64); } break; } pExtra_data += sizeof(mz_uint16) * 2 + field_data_size; extra_size_remaining = extra_size_remaining - sizeof(mz_uint16) * 2 - field_data_size; } while (extra_size_remaining); } } return MZ_TRUE; } static MZ_FORCEINLINE mz_bool mz_zip_string_equal(const char *pA, const char *pB, mz_uint len, mz_uint flags) { mz_uint i; if (flags & MZ_ZIP_FLAG_CASE_SENSITIVE) return 0 == memcmp(pA, pB, len); for (i = 0; i < len; ++i) if (MZ_TOLOWER(pA[i]) != MZ_TOLOWER(pB[i])) return MZ_FALSE; return MZ_TRUE; } static MZ_FORCEINLINE int mz_zip_filename_compare(const mz_zip_array *pCentral_dir_array, const mz_zip_array *pCentral_dir_offsets, mz_uint l_index, const char *pR, mz_uint r_len) { const mz_uint8 *pL = &MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_array, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_offsets, mz_uint32, l_index)), *pE; mz_uint l_len = MZ_READ_LE16(pL + MZ_ZIP_CDH_FILENAME_LEN_OFS); mz_uint8 l = 0, r = 0; pL += MZ_ZIP_CENTRAL_DIR_HEADER_SIZE; pE = pL + MZ_MIN(l_len, r_len); while (pL < pE) { if ((l = MZ_TOLOWER(*pL)) != (r = MZ_TOLOWER(*pR))) break; pL++; pR++; } return (pL == pE) ? (int)(l_len - r_len) : (l - r); } static mz_bool mz_zip_locate_file_binary_search(mz_zip_archive *pZip, const char *pFilename, mz_uint32 *pIndex) { mz_zip_internal_state *pState = pZip->m_pState; const mz_zip_array *pCentral_dir_offsets = &pState->m_central_dir_offsets; const mz_zip_array *pCentral_dir = &pState->m_central_dir; mz_uint32 *pIndices = &MZ_ZIP_ARRAY_ELEMENT(&pState->m_sorted_central_dir_offsets, mz_uint32, 0); const mz_uint32 size = pZip->m_total_files; const mz_uint filename_len = (mz_uint)strlen(pFilename); if (pIndex) *pIndex = 0; if (size) { /* yes I could use uint32_t's, but then we would have to add some special case checks in the loop, argh, and */ /* honestly the major expense here on 32-bit CPU's will still be the filename compare */ mz_int64 l = 0, h = (mz_int64)size - 1; while (l <= h) { mz_int64 m = l + ((h - l) >> 1); mz_uint32 file_index = pIndices[(mz_uint32)m]; int comp = mz_zip_filename_compare(pCentral_dir, pCentral_dir_offsets, file_index, pFilename, filename_len); if (!comp) { if (pIndex) *pIndex = file_index; return MZ_TRUE; } else if (comp < 0) l = m + 1; else h = m - 1; } } return mz_zip_set_error(pZip, MZ_ZIP_FILE_NOT_FOUND); } int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags) { mz_uint32 index; if (!mz_zip_reader_locate_file_v2(pZip, pName, pComment, flags, &index)) return -1; else return (int)index; } mz_bool mz_zip_reader_locate_file_v2(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags, mz_uint32 *pIndex) { mz_uint file_index; size_t name_len, comment_len; if (pIndex) *pIndex = 0; if ((!pZip) || (!pZip->m_pState) || (!pName)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); /* See if we can use a binary search */ if (((pZip->m_pState->m_init_flags & MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY) == 0) && (pZip->m_zip_mode == MZ_ZIP_MODE_READING) && ((flags & (MZ_ZIP_FLAG_IGNORE_PATH | MZ_ZIP_FLAG_CASE_SENSITIVE)) == 0) && (!pComment) && (pZip->m_pState->m_sorted_central_dir_offsets.m_size)) { return mz_zip_locate_file_binary_search(pZip, pName, pIndex); } /* Locate the entry by scanning the entire central directory */ name_len = strlen(pName); if (name_len > MZ_UINT16_MAX) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); comment_len = pComment ? strlen(pComment) : 0; if (comment_len > MZ_UINT16_MAX) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); for (file_index = 0; file_index < pZip->m_total_files; file_index++) { const mz_uint8 *pHeader = &MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, file_index)); mz_uint filename_len = MZ_READ_LE16(pHeader + MZ_ZIP_CDH_FILENAME_LEN_OFS); const char *pFilename = (const char *)pHeader + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE; if (filename_len < name_len) continue; if (comment_len) { mz_uint file_extra_len = MZ_READ_LE16(pHeader + MZ_ZIP_CDH_EXTRA_LEN_OFS), file_comment_len = MZ_READ_LE16(pHeader + MZ_ZIP_CDH_COMMENT_LEN_OFS); const char *pFile_comment = pFilename + filename_len + file_extra_len; if ((file_comment_len != comment_len) || (!mz_zip_string_equal(pComment, pFile_comment, file_comment_len, flags))) continue; } if ((flags & MZ_ZIP_FLAG_IGNORE_PATH) && (filename_len)) { int ofs = filename_len - 1; do { if ((pFilename[ofs] == '/') || (pFilename[ofs] == '\\') || (pFilename[ofs] == ':')) break; } while (--ofs >= 0); ofs++; pFilename += ofs; filename_len -= ofs; } if ((filename_len == name_len) && (mz_zip_string_equal(pName, pFilename, filename_len, flags))) { if (pIndex) *pIndex = file_index; return MZ_TRUE; } } return mz_zip_set_error(pZip, MZ_ZIP_FILE_NOT_FOUND); } static mz_bool mz_zip_reader_extract_to_mem_no_alloc1(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size, const mz_zip_archive_file_stat *st) { int status = TINFL_STATUS_DONE; mz_uint64 needed_size, cur_file_ofs, comp_remaining, out_buf_ofs = 0, read_buf_size, read_buf_ofs = 0, read_buf_avail; mz_zip_archive_file_stat file_stat; void *pRead_buf; mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32; tinfl_decompressor inflator; if ((!pZip) || (!pZip->m_pState) || ((buf_size) && (!pBuf)) || ((user_read_buf_size) && (!pUser_read_buf)) || (!pZip->m_pRead)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); if (st) { file_stat = *st; } else if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat)) return MZ_FALSE; /* A directory or zero length file */ if ((file_stat.m_is_directory) || (!file_stat.m_comp_size)) return MZ_TRUE; /* Encryption and patch files are not supported. */ if (file_stat.m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG)) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); /* This function only supports decompressing stored and deflate. */ if ((!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (file_stat.m_method != 0) && (file_stat.m_method != MZ_DEFLATED)) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD); /* Ensure supplied output buffer is large enough. */ needed_size = (flags & MZ_ZIP_FLAG_COMPRESSED_DATA) ? file_stat.m_comp_size : file_stat.m_uncomp_size; if (buf_size < needed_size) return mz_zip_set_error(pZip, MZ_ZIP_BUF_TOO_SMALL); /* Read and parse the local directory entry. */ cur_file_ofs = file_stat.m_local_header_ofs; if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); cur_file_ofs += MZ_ZIP_LOCAL_DIR_HEADER_SIZE + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS); if ((cur_file_ofs + file_stat.m_comp_size) > pZip->m_archive_size) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); if ((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!file_stat.m_method)) { /* The file is stored or the caller has requested the compressed data. */ if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pBuf, (size_t)needed_size) != needed_size) return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); #ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS if ((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) == 0) { if (mz_crc32(MZ_CRC32_INIT, (const mz_uint8 *)pBuf, (size_t)file_stat.m_uncomp_size) != file_stat.m_crc32) return mz_zip_set_error(pZip, MZ_ZIP_CRC_CHECK_FAILED); } #endif return MZ_TRUE; } /* Decompress the file either directly from memory or from a file input buffer. */ tinfl_init(&inflator); if (pZip->m_pState->m_pMem) { /* Read directly from the archive in memory. */ pRead_buf = (mz_uint8 *)pZip->m_pState->m_pMem + cur_file_ofs; read_buf_size = read_buf_avail = file_stat.m_comp_size; comp_remaining = 0; } else if (pUser_read_buf) { /* Use a user provided read buffer. */ if (!user_read_buf_size) return MZ_FALSE; pRead_buf = (mz_uint8 *)pUser_read_buf; read_buf_size = user_read_buf_size; read_buf_avail = 0; comp_remaining = file_stat.m_comp_size; } else { /* Temporarily allocate a read buffer. */ read_buf_size = MZ_MIN(file_stat.m_comp_size, (mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE); if (((sizeof(size_t) == sizeof(mz_uint32))) && (read_buf_size > 0x7FFFFFFF)) return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); if (NULL == (pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)read_buf_size))) return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); read_buf_avail = 0; comp_remaining = file_stat.m_comp_size; } do { /* The size_t cast here should be OK because we've verified that the output buffer is >= file_stat.m_uncomp_size above */ size_t in_buf_size, out_buf_size = (size_t)(file_stat.m_uncomp_size - out_buf_ofs); if ((!read_buf_avail) && (!pZip->m_pState->m_pMem)) { read_buf_avail = MZ_MIN(read_buf_size, comp_remaining); if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail) { status = TINFL_STATUS_FAILED; mz_zip_set_error(pZip, MZ_ZIP_DECOMPRESSION_FAILED); break; } cur_file_ofs += read_buf_avail; comp_remaining -= read_buf_avail; read_buf_ofs = 0; } in_buf_size = (size_t)read_buf_avail; status = tinfl_decompress(&inflator, (mz_uint8 *)pRead_buf + read_buf_ofs, &in_buf_size, (mz_uint8 *)pBuf, (mz_uint8 *)pBuf + out_buf_ofs, &out_buf_size, TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF | (comp_remaining ? TINFL_FLAG_HAS_MORE_INPUT : 0)); read_buf_avail -= in_buf_size; read_buf_ofs += in_buf_size; out_buf_ofs += out_buf_size; } while (status == TINFL_STATUS_NEEDS_MORE_INPUT); if (status == TINFL_STATUS_DONE) { /* Make sure the entire file was decompressed, and check its CRC. */ if (out_buf_ofs != file_stat.m_uncomp_size) { mz_zip_set_error(pZip, MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE); status = TINFL_STATUS_FAILED; } #ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS else if (mz_crc32(MZ_CRC32_INIT, (const mz_uint8 *)pBuf, (size_t)file_stat.m_uncomp_size) != file_stat.m_crc32) { mz_zip_set_error(pZip, MZ_ZIP_CRC_CHECK_FAILED); status = TINFL_STATUS_FAILED; } #endif } if ((!pZip->m_pState->m_pMem) && (!pUser_read_buf)) pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); return status == TINFL_STATUS_DONE; } mz_bool mz_zip_reader_extract_to_mem_no_alloc(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size) { return mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, buf_size, flags, pUser_read_buf, user_read_buf_size, NULL); } mz_bool mz_zip_reader_extract_file_to_mem_no_alloc(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size) { mz_uint32 file_index; if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index)) return MZ_FALSE; return mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, buf_size, flags, pUser_read_buf, user_read_buf_size, NULL); } mz_bool mz_zip_reader_extract_to_mem(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags) { return mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, buf_size, flags, NULL, 0, NULL); } mz_bool mz_zip_reader_extract_file_to_mem(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags) { return mz_zip_reader_extract_file_to_mem_no_alloc(pZip, pFilename, pBuf, buf_size, flags, NULL, 0); } void *mz_zip_reader_extract_to_heap(mz_zip_archive *pZip, mz_uint file_index, size_t *pSize, mz_uint flags) { mz_zip_archive_file_stat file_stat; mz_uint64 alloc_size; void *pBuf; if (pSize) *pSize = 0; if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat)) return NULL; alloc_size = (flags & MZ_ZIP_FLAG_COMPRESSED_DATA) ? file_stat.m_comp_size : file_stat.m_uncomp_size; if (((sizeof(size_t) == sizeof(mz_uint32))) && (alloc_size > 0x7FFFFFFF)) { mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); return NULL; } if (NULL == (pBuf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)alloc_size))) { mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); return NULL; } if (!mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, (size_t)alloc_size, flags, NULL, 0, &file_stat)) { pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); return NULL; } if (pSize) *pSize = (size_t)alloc_size; return pBuf; } void *mz_zip_reader_extract_file_to_heap(mz_zip_archive *pZip, const char *pFilename, size_t *pSize, mz_uint flags) { mz_uint32 file_index; if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index)) { if (pSize) *pSize = 0; return MZ_FALSE; } return mz_zip_reader_extract_to_heap(pZip, file_index, pSize, flags); } mz_bool mz_zip_reader_extract_to_callback(mz_zip_archive *pZip, mz_uint file_index, mz_file_write_func pCallback, void *pOpaque, mz_uint flags) { int status = TINFL_STATUS_DONE; #ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS mz_uint file_crc32 = MZ_CRC32_INIT; #endif mz_uint64 read_buf_size, read_buf_ofs = 0, read_buf_avail, comp_remaining, out_buf_ofs = 0, cur_file_ofs; mz_zip_archive_file_stat file_stat; void *pRead_buf = NULL; void *pWrite_buf = NULL; mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32; if ((!pZip) || (!pZip->m_pState) || (!pCallback) || (!pZip->m_pRead)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat)) return MZ_FALSE; /* A directory or zero length file */ if ((file_stat.m_is_directory) || (!file_stat.m_comp_size)) return MZ_TRUE; /* Encryption and patch files are not supported. */ if (file_stat.m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG)) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); /* This function only supports decompressing stored and deflate. */ if ((!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (file_stat.m_method != 0) && (file_stat.m_method != MZ_DEFLATED)) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD); /* Read and do some minimal validation of the local directory entry (this doesn't crack the zip64 stuff, which we already have from the central dir) */ cur_file_ofs = file_stat.m_local_header_ofs; if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); cur_file_ofs += MZ_ZIP_LOCAL_DIR_HEADER_SIZE + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS); if ((cur_file_ofs + file_stat.m_comp_size) > pZip->m_archive_size) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); /* Decompress the file either directly from memory or from a file input buffer. */ if (pZip->m_pState->m_pMem) { pRead_buf = (mz_uint8 *)pZip->m_pState->m_pMem + cur_file_ofs; read_buf_size = read_buf_avail = file_stat.m_comp_size; comp_remaining = 0; } else { read_buf_size = MZ_MIN(file_stat.m_comp_size, (mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE); if (NULL == (pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)read_buf_size))) return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); read_buf_avail = 0; comp_remaining = file_stat.m_comp_size; } if ((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!file_stat.m_method)) { /* The file is stored or the caller has requested the compressed data. */ if (pZip->m_pState->m_pMem) { if (((sizeof(size_t) == sizeof(mz_uint32))) && (file_stat.m_comp_size > MZ_UINT32_MAX)) return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); if (pCallback(pOpaque, out_buf_ofs, pRead_buf, (size_t)file_stat.m_comp_size) != file_stat.m_comp_size) { mz_zip_set_error(pZip, MZ_ZIP_WRITE_CALLBACK_FAILED); status = TINFL_STATUS_FAILED; } else if (!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) { #ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS file_crc32 = (mz_uint32)mz_crc32(file_crc32, (const mz_uint8 *)pRead_buf, (size_t)file_stat.m_comp_size); #endif } cur_file_ofs += file_stat.m_comp_size; out_buf_ofs += file_stat.m_comp_size; comp_remaining = 0; } else { while (comp_remaining) { read_buf_avail = MZ_MIN(read_buf_size, comp_remaining); if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail) { mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); status = TINFL_STATUS_FAILED; break; } #ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS if (!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) { file_crc32 = (mz_uint32)mz_crc32(file_crc32, (const mz_uint8 *)pRead_buf, (size_t)read_buf_avail); } #endif if (pCallback(pOpaque, out_buf_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail) { mz_zip_set_error(pZip, MZ_ZIP_WRITE_CALLBACK_FAILED); status = TINFL_STATUS_FAILED; break; } cur_file_ofs += read_buf_avail; out_buf_ofs += read_buf_avail; comp_remaining -= read_buf_avail; } } } else { tinfl_decompressor inflator; tinfl_init(&inflator); if (NULL == (pWrite_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, TINFL_LZ_DICT_SIZE))) { mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); status = TINFL_STATUS_FAILED; } else { do { mz_uint8 *pWrite_buf_cur = (mz_uint8 *)pWrite_buf + (out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1)); size_t in_buf_size, out_buf_size = TINFL_LZ_DICT_SIZE - (out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1)); if ((!read_buf_avail) && (!pZip->m_pState->m_pMem)) { read_buf_avail = MZ_MIN(read_buf_size, comp_remaining); if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail) { mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); status = TINFL_STATUS_FAILED; break; } cur_file_ofs += read_buf_avail; comp_remaining -= read_buf_avail; read_buf_ofs = 0; } in_buf_size = (size_t)read_buf_avail; status = tinfl_decompress(&inflator, (const mz_uint8 *)pRead_buf + read_buf_ofs, &in_buf_size, (mz_uint8 *)pWrite_buf, pWrite_buf_cur, &out_buf_size, comp_remaining ? TINFL_FLAG_HAS_MORE_INPUT : 0); read_buf_avail -= in_buf_size; read_buf_ofs += in_buf_size; if (out_buf_size) { if (pCallback(pOpaque, out_buf_ofs, pWrite_buf_cur, out_buf_size) != out_buf_size) { mz_zip_set_error(pZip, MZ_ZIP_WRITE_CALLBACK_FAILED); status = TINFL_STATUS_FAILED; break; } #ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS file_crc32 = (mz_uint32)mz_crc32(file_crc32, pWrite_buf_cur, out_buf_size); #endif if ((out_buf_ofs += out_buf_size) > file_stat.m_uncomp_size) { mz_zip_set_error(pZip, MZ_ZIP_DECOMPRESSION_FAILED); status = TINFL_STATUS_FAILED; break; } } } while ((status == TINFL_STATUS_NEEDS_MORE_INPUT) || (status == TINFL_STATUS_HAS_MORE_OUTPUT)); } } if ((status == TINFL_STATUS_DONE) && (!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA))) { /* Make sure the entire file was decompressed, and check its CRC. */ if (out_buf_ofs != file_stat.m_uncomp_size) { mz_zip_set_error(pZip, MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE); status = TINFL_STATUS_FAILED; } #ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS else if (file_crc32 != file_stat.m_crc32) { mz_zip_set_error(pZip, MZ_ZIP_DECOMPRESSION_FAILED); status = TINFL_STATUS_FAILED; } #endif } if (!pZip->m_pState->m_pMem) pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); if (pWrite_buf) pZip->m_pFree(pZip->m_pAlloc_opaque, pWrite_buf); return status == TINFL_STATUS_DONE; } mz_bool mz_zip_reader_extract_file_to_callback(mz_zip_archive *pZip, const char *pFilename, mz_file_write_func pCallback, void *pOpaque, mz_uint flags) { mz_uint32 file_index; if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index)) return MZ_FALSE; return mz_zip_reader_extract_to_callback(pZip, file_index, pCallback, pOpaque, flags); } mz_zip_reader_extract_iter_state* mz_zip_reader_extract_iter_new(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags) { mz_zip_reader_extract_iter_state *pState; mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32; /* Argument sanity check */ if ((!pZip) || (!pZip->m_pState)) return NULL; /* Allocate an iterator status structure */ pState = (mz_zip_reader_extract_iter_state*)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(mz_zip_reader_extract_iter_state)); if (!pState) { mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); return NULL; } /* Fetch file details */ if (!mz_zip_reader_file_stat(pZip, file_index, &pState->file_stat)) { pZip->m_pFree(pZip->m_pAlloc_opaque, pState); return NULL; } /* Encryption and patch files are not supported. */ if (pState->file_stat.m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG)) { mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); pZip->m_pFree(pZip->m_pAlloc_opaque, pState); return NULL; } /* This function only supports decompressing stored and deflate. */ if ((!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (pState->file_stat.m_method != 0) && (pState->file_stat.m_method != MZ_DEFLATED)) { mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD); pZip->m_pFree(pZip->m_pAlloc_opaque, pState); return NULL; } /* Init state - save args */ pState->pZip = pZip; pState->flags = flags; /* Init state - reset variables to defaults */ pState->status = TINFL_STATUS_DONE; #ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS pState->file_crc32 = MZ_CRC32_INIT; #endif pState->read_buf_ofs = 0; pState->out_buf_ofs = 0; pState->pRead_buf = NULL; pState->pWrite_buf = NULL; pState->out_blk_remain = 0; /* Read and parse the local directory entry. */ pState->cur_file_ofs = pState->file_stat.m_local_header_ofs; if (pZip->m_pRead(pZip->m_pIO_opaque, pState->cur_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) { mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); pZip->m_pFree(pZip->m_pAlloc_opaque, pState); return NULL; } if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG) { mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); pZip->m_pFree(pZip->m_pAlloc_opaque, pState); return NULL; } pState->cur_file_ofs += MZ_ZIP_LOCAL_DIR_HEADER_SIZE + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS); if ((pState->cur_file_ofs + pState->file_stat.m_comp_size) > pZip->m_archive_size) { mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); pZip->m_pFree(pZip->m_pAlloc_opaque, pState); return NULL; } /* Decompress the file either directly from memory or from a file input buffer. */ if (pZip->m_pState->m_pMem) { pState->pRead_buf = (mz_uint8 *)pZip->m_pState->m_pMem + pState->cur_file_ofs; pState->read_buf_size = pState->read_buf_avail = pState->file_stat.m_comp_size; pState->comp_remaining = pState->file_stat.m_comp_size; } else { if (!((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!pState->file_stat.m_method))) { /* Decompression required, therefore intermediate read buffer required */ pState->read_buf_size = MZ_MIN(pState->file_stat.m_comp_size, (mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE); if (NULL == (pState->pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)pState->read_buf_size))) { mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); pZip->m_pFree(pZip->m_pAlloc_opaque, pState); return NULL; } } else { /* Decompression not required - we will be reading directly into user buffer, no temp buf required */ pState->read_buf_size = 0; } pState->read_buf_avail = 0; pState->comp_remaining = pState->file_stat.m_comp_size; } if (!((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!pState->file_stat.m_method))) { /* Decompression required, init decompressor */ tinfl_init( &pState->inflator ); /* Allocate write buffer */ if (NULL == (pState->pWrite_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, TINFL_LZ_DICT_SIZE))) { mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); if (pState->pRead_buf) pZip->m_pFree(pZip->m_pAlloc_opaque, pState->pRead_buf); pZip->m_pFree(pZip->m_pAlloc_opaque, pState); return NULL; } } return pState; } mz_zip_reader_extract_iter_state* mz_zip_reader_extract_file_iter_new(mz_zip_archive *pZip, const char *pFilename, mz_uint flags) { mz_uint32 file_index; /* Locate file index by name */ if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index)) return NULL; /* Construct iterator */ return mz_zip_reader_extract_iter_new(pZip, file_index, flags); } size_t mz_zip_reader_extract_iter_read(mz_zip_reader_extract_iter_state* pState, void* pvBuf, size_t buf_size) { size_t copied_to_caller = 0; /* Argument sanity check */ if ((!pState) || (!pState->pZip) || (!pState->pZip->m_pState) || (!pvBuf)) return 0; if ((pState->flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!pState->file_stat.m_method)) { /* The file is stored or the caller has requested the compressed data, calc amount to return. */ copied_to_caller = (size_t)MZ_MIN( buf_size, pState->comp_remaining ); /* Zip is in memory....or requires reading from a file? */ if (pState->pZip->m_pState->m_pMem) { /* Copy data to caller's buffer */ memcpy( pvBuf, pState->pRead_buf, copied_to_caller ); pState->pRead_buf = ((mz_uint8*)pState->pRead_buf) + copied_to_caller; } else { /* Read directly into caller's buffer */ if (pState->pZip->m_pRead(pState->pZip->m_pIO_opaque, pState->cur_file_ofs, pvBuf, copied_to_caller) != copied_to_caller) { /* Failed to read all that was asked for, flag failure and alert user */ mz_zip_set_error(pState->pZip, MZ_ZIP_FILE_READ_FAILED); pState->status = TINFL_STATUS_FAILED; copied_to_caller = 0; } } #ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS /* Compute CRC if not returning compressed data only */ if (!(pState->flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) pState->file_crc32 = (mz_uint32)mz_crc32(pState->file_crc32, (const mz_uint8 *)pvBuf, copied_to_caller); #endif /* Advance offsets, dec counters */ pState->cur_file_ofs += copied_to_caller; pState->out_buf_ofs += copied_to_caller; pState->comp_remaining -= copied_to_caller; } else { do { /* Calc ptr to write buffer - given current output pos and block size */ mz_uint8 *pWrite_buf_cur = (mz_uint8 *)pState->pWrite_buf + (pState->out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1)); /* Calc max output size - given current output pos and block size */ size_t in_buf_size, out_buf_size = TINFL_LZ_DICT_SIZE - (pState->out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1)); if (!pState->out_blk_remain) { /* Read more data from file if none available (and reading from file) */ if ((!pState->read_buf_avail) && (!pState->pZip->m_pState->m_pMem)) { /* Calc read size */ pState->read_buf_avail = MZ_MIN(pState->read_buf_size, pState->comp_remaining); if (pState->pZip->m_pRead(pState->pZip->m_pIO_opaque, pState->cur_file_ofs, pState->pRead_buf, (size_t)pState->read_buf_avail) != pState->read_buf_avail) { mz_zip_set_error(pState->pZip, MZ_ZIP_FILE_READ_FAILED); pState->status = TINFL_STATUS_FAILED; break; } /* Advance offsets, dec counters */ pState->cur_file_ofs += pState->read_buf_avail; pState->comp_remaining -= pState->read_buf_avail; pState->read_buf_ofs = 0; } /* Perform decompression */ in_buf_size = (size_t)pState->read_buf_avail; pState->status = tinfl_decompress(&pState->inflator, (const mz_uint8 *)pState->pRead_buf + pState->read_buf_ofs, &in_buf_size, (mz_uint8 *)pState->pWrite_buf, pWrite_buf_cur, &out_buf_size, pState->comp_remaining ? TINFL_FLAG_HAS_MORE_INPUT : 0); pState->read_buf_avail -= in_buf_size; pState->read_buf_ofs += in_buf_size; /* Update current output block size remaining */ pState->out_blk_remain = out_buf_size; } if (pState->out_blk_remain) { /* Calc amount to return. */ size_t to_copy = MZ_MIN( (buf_size - copied_to_caller), pState->out_blk_remain ); /* Copy data to caller's buffer */ memcpy( (mz_uint8*)pvBuf + copied_to_caller, pWrite_buf_cur, to_copy ); #ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS /* Perform CRC */ pState->file_crc32 = (mz_uint32)mz_crc32(pState->file_crc32, pWrite_buf_cur, to_copy); #endif /* Decrement data consumed from block */ pState->out_blk_remain -= to_copy; /* Inc output offset, while performing sanity check */ if ((pState->out_buf_ofs += to_copy) > pState->file_stat.m_uncomp_size) { mz_zip_set_error(pState->pZip, MZ_ZIP_DECOMPRESSION_FAILED); pState->status = TINFL_STATUS_FAILED; break; } /* Increment counter of data copied to caller */ copied_to_caller += to_copy; } } while ( (copied_to_caller < buf_size) && ((pState->status == TINFL_STATUS_NEEDS_MORE_INPUT) || (pState->status == TINFL_STATUS_HAS_MORE_OUTPUT)) ); } /* Return how many bytes were copied into user buffer */ return copied_to_caller; } mz_bool mz_zip_reader_extract_iter_free(mz_zip_reader_extract_iter_state* pState) { int status; /* Argument sanity check */ if ((!pState) || (!pState->pZip) || (!pState->pZip->m_pState)) return MZ_FALSE; /* Was decompression completed and requested? */ if ((pState->status == TINFL_STATUS_DONE) && (!(pState->flags & MZ_ZIP_FLAG_COMPRESSED_DATA))) { /* Make sure the entire file was decompressed, and check its CRC. */ if (pState->out_buf_ofs != pState->file_stat.m_uncomp_size) { mz_zip_set_error(pState->pZip, MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE); pState->status = TINFL_STATUS_FAILED; } #ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS else if (pState->file_crc32 != pState->file_stat.m_crc32) { mz_zip_set_error(pState->pZip, MZ_ZIP_DECOMPRESSION_FAILED); pState->status = TINFL_STATUS_FAILED; } #endif } /* Free buffers */ if (!pState->pZip->m_pState->m_pMem) pState->pZip->m_pFree(pState->pZip->m_pAlloc_opaque, pState->pRead_buf); if (pState->pWrite_buf) pState->pZip->m_pFree(pState->pZip->m_pAlloc_opaque, pState->pWrite_buf); /* Save status */ status = pState->status; /* Free context */ pState->pZip->m_pFree(pState->pZip->m_pAlloc_opaque, pState); return status == TINFL_STATUS_DONE; } #ifndef MINIZ_NO_STDIO static size_t mz_zip_file_write_callback(void *pOpaque, mz_uint64 ofs, const void *pBuf, size_t n) { (void)ofs; return MZ_FWRITE(pBuf, 1, n, (MZ_FILE *)pOpaque); } mz_bool mz_zip_reader_extract_to_file(mz_zip_archive *pZip, mz_uint file_index, const char *pDst_filename, mz_uint flags) { mz_bool status; mz_zip_archive_file_stat file_stat; MZ_FILE *pFile; if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat)) return MZ_FALSE; if ((file_stat.m_is_directory) || (!file_stat.m_is_supported)) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE); pFile = MZ_FOPEN(pDst_filename, "wb"); if (!pFile) return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); status = mz_zip_reader_extract_to_callback(pZip, file_index, mz_zip_file_write_callback, pFile, flags); if (MZ_FCLOSE(pFile) == EOF) { if (status) mz_zip_set_error(pZip, MZ_ZIP_FILE_CLOSE_FAILED); status = MZ_FALSE; } #if !defined(MINIZ_NO_TIME) && !defined(MINIZ_NO_STDIO) if (status) mz_zip_set_file_times(pDst_filename, file_stat.m_time, file_stat.m_time); #endif return status; } mz_bool mz_zip_reader_extract_file_to_file(mz_zip_archive *pZip, const char *pArchive_filename, const char *pDst_filename, mz_uint flags) { mz_uint32 file_index; if (!mz_zip_reader_locate_file_v2(pZip, pArchive_filename, NULL, flags, &file_index)) return MZ_FALSE; return mz_zip_reader_extract_to_file(pZip, file_index, pDst_filename, flags); } mz_bool mz_zip_reader_extract_to_cfile(mz_zip_archive *pZip, mz_uint file_index, MZ_FILE *pFile, mz_uint flags) { mz_zip_archive_file_stat file_stat; if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat)) return MZ_FALSE; if ((file_stat.m_is_directory) || (!file_stat.m_is_supported)) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE); return mz_zip_reader_extract_to_callback(pZip, file_index, mz_zip_file_write_callback, pFile, flags); } mz_bool mz_zip_reader_extract_file_to_cfile(mz_zip_archive *pZip, const char *pArchive_filename, MZ_FILE *pFile, mz_uint flags) { mz_uint32 file_index; if (!mz_zip_reader_locate_file_v2(pZip, pArchive_filename, NULL, flags, &file_index)) return MZ_FALSE; return mz_zip_reader_extract_to_cfile(pZip, file_index, pFile, flags); } #endif /* #ifndef MINIZ_NO_STDIO */ static size_t mz_zip_compute_crc32_callback(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n) { mz_uint32 *p = (mz_uint32 *)pOpaque; (void)file_ofs; *p = (mz_uint32)mz_crc32(*p, (const mz_uint8 *)pBuf, n); return n; } mz_bool mz_zip_validate_file(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags) { mz_zip_archive_file_stat file_stat; mz_zip_internal_state *pState; const mz_uint8 *pCentral_dir_header; mz_bool found_zip64_ext_data_in_cdir = MZ_FALSE; mz_bool found_zip64_ext_data_in_ldir = MZ_FALSE; mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32; mz_uint64 local_header_ofs = 0; mz_uint32 local_header_filename_len, local_header_extra_len, local_header_crc32; mz_uint64 local_header_comp_size, local_header_uncomp_size; mz_uint32 uncomp_crc32 = MZ_CRC32_INIT; mz_bool has_data_descriptor; mz_uint32 local_header_bit_flags; mz_zip_array file_data_array; mz_zip_array_init(&file_data_array, 1); if ((!pZip) || (!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || (!pZip->m_pRead)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); if (file_index > pZip->m_total_files) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); pState = pZip->m_pState; pCentral_dir_header = mz_zip_get_cdh(pZip, file_index); if (!mz_zip_file_stat_internal(pZip, file_index, pCentral_dir_header, &file_stat, &found_zip64_ext_data_in_cdir)) return MZ_FALSE; /* A directory or zero length file */ if ((file_stat.m_is_directory) || (!file_stat.m_uncomp_size)) return MZ_TRUE; /* Encryption and patch files are not supported. */ if (file_stat.m_is_encrypted) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION); /* This function only supports stored and deflate. */ if ((file_stat.m_method != 0) && (file_stat.m_method != MZ_DEFLATED)) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD); if (!file_stat.m_is_supported) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE); /* Read and parse the local directory entry. */ local_header_ofs = file_stat.m_local_header_ofs; if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); local_header_filename_len = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS); local_header_extra_len = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS); local_header_comp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_COMPRESSED_SIZE_OFS); local_header_uncomp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS); local_header_crc32 = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_CRC32_OFS); local_header_bit_flags = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_BIT_FLAG_OFS); has_data_descriptor = (local_header_bit_flags & 8) != 0; if (local_header_filename_len != strlen(file_stat.m_filename)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); if ((local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_len + local_header_extra_len + file_stat.m_comp_size) > pZip->m_archive_size) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); if (!mz_zip_array_resize(pZip, &file_data_array, MZ_MAX(local_header_filename_len, local_header_extra_len), MZ_FALSE)) { mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); goto handle_failure; } if (local_header_filename_len) { if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE, file_data_array.m_p, local_header_filename_len) != local_header_filename_len) { mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); goto handle_failure; } /* I've seen 1 archive that had the same pathname, but used backslashes in the local dir and forward slashes in the central dir. Do we care about this? For now, this case will fail validation. */ if (memcmp(file_stat.m_filename, file_data_array.m_p, local_header_filename_len) != 0) { mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED); goto handle_failure; } } if ((local_header_extra_len) && ((local_header_comp_size == MZ_UINT32_MAX) || (local_header_uncomp_size == MZ_UINT32_MAX))) { mz_uint32 extra_size_remaining = local_header_extra_len; const mz_uint8 *pExtra_data = (const mz_uint8 *)file_data_array.m_p; if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_len, file_data_array.m_p, local_header_extra_len) != local_header_extra_len) { mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); goto handle_failure; } do { mz_uint32 field_id, field_data_size, field_total_size; if (extra_size_remaining < (sizeof(mz_uint16) * 2)) { mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); goto handle_failure; } field_id = MZ_READ_LE16(pExtra_data); field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); field_total_size = field_data_size + sizeof(mz_uint16) * 2; if (field_total_size > extra_size_remaining) { mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); goto handle_failure; } if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) { const mz_uint8 *pSrc_field_data = pExtra_data + sizeof(mz_uint32); if (field_data_size < sizeof(mz_uint64) * 2) { mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); goto handle_failure; } local_header_uncomp_size = MZ_READ_LE64(pSrc_field_data); local_header_comp_size = MZ_READ_LE64(pSrc_field_data + sizeof(mz_uint64)); found_zip64_ext_data_in_ldir = MZ_TRUE; break; } pExtra_data += field_total_size; extra_size_remaining -= field_total_size; } while (extra_size_remaining); } /* TODO: parse local header extra data when local_header_comp_size is 0xFFFFFFFF! (big_descriptor.zip) */ /* I've seen zips in the wild with the data descriptor bit set, but proper local header values and bogus data descriptors */ if ((has_data_descriptor) && (!local_header_comp_size) && (!local_header_crc32)) { mz_uint8 descriptor_buf[32]; mz_bool has_id; const mz_uint8 *pSrc; mz_uint32 file_crc32; mz_uint64 comp_size = 0, uncomp_size = 0; mz_uint32 num_descriptor_uint32s = ((pState->m_zip64) || (found_zip64_ext_data_in_ldir)) ? 6 : 4; if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_len + local_header_extra_len + file_stat.m_comp_size, descriptor_buf, sizeof(mz_uint32) * num_descriptor_uint32s) != (sizeof(mz_uint32) * num_descriptor_uint32s)) { mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); goto handle_failure; } has_id = (MZ_READ_LE32(descriptor_buf) == MZ_ZIP_DATA_DESCRIPTOR_ID); pSrc = has_id ? (descriptor_buf + sizeof(mz_uint32)) : descriptor_buf; file_crc32 = MZ_READ_LE32(pSrc); if ((pState->m_zip64) || (found_zip64_ext_data_in_ldir)) { comp_size = MZ_READ_LE64(pSrc + sizeof(mz_uint32)); uncomp_size = MZ_READ_LE64(pSrc + sizeof(mz_uint32) + sizeof(mz_uint64)); } else { comp_size = MZ_READ_LE32(pSrc + sizeof(mz_uint32)); uncomp_size = MZ_READ_LE32(pSrc + sizeof(mz_uint32) + sizeof(mz_uint32)); } if ((file_crc32 != file_stat.m_crc32) || (comp_size != file_stat.m_comp_size) || (uncomp_size != file_stat.m_uncomp_size)) { mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED); goto handle_failure; } } else { if ((local_header_crc32 != file_stat.m_crc32) || (local_header_comp_size != file_stat.m_comp_size) || (local_header_uncomp_size != file_stat.m_uncomp_size)) { mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED); goto handle_failure; } } mz_zip_array_clear(pZip, &file_data_array); if ((flags & MZ_ZIP_FLAG_VALIDATE_HEADERS_ONLY) == 0) { if (!mz_zip_reader_extract_to_callback(pZip, file_index, mz_zip_compute_crc32_callback, &uncomp_crc32, 0)) return MZ_FALSE; /* 1 more check to be sure, although the extract checks too. */ if (uncomp_crc32 != file_stat.m_crc32) { mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED); return MZ_FALSE; } } return MZ_TRUE; handle_failure: mz_zip_array_clear(pZip, &file_data_array); return MZ_FALSE; } mz_bool mz_zip_validate_archive(mz_zip_archive *pZip, mz_uint flags) { mz_zip_internal_state *pState; mz_uint32 i; if ((!pZip) || (!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || (!pZip->m_pRead)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); pState = pZip->m_pState; /* Basic sanity checks */ if (!pState->m_zip64) { if (pZip->m_total_files > MZ_UINT16_MAX) return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); if (pZip->m_archive_size > MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); } else { if (pState->m_central_dir.m_size >= MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); } for (i = 0; i < pZip->m_total_files; i++) { if (MZ_ZIP_FLAG_VALIDATE_LOCATE_FILE_FLAG & flags) { mz_uint32 found_index; mz_zip_archive_file_stat stat; if (!mz_zip_reader_file_stat(pZip, i, &stat)) return MZ_FALSE; if (!mz_zip_reader_locate_file_v2(pZip, stat.m_filename, NULL, 0, &found_index)) return MZ_FALSE; /* This check can fail if there are duplicate filenames in the archive (which we don't check for when writing - that's up to the user) */ if (found_index != i) return mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED); } if (!mz_zip_validate_file(pZip, i, flags)) return MZ_FALSE; } return MZ_TRUE; } mz_bool mz_zip_validate_mem_archive(const void *pMem, size_t size, mz_uint flags, mz_zip_error *pErr) { mz_bool success = MZ_TRUE; mz_zip_archive zip; mz_zip_error actual_err = MZ_ZIP_NO_ERROR; if ((!pMem) || (!size)) { if (pErr) *pErr = MZ_ZIP_INVALID_PARAMETER; return MZ_FALSE; } mz_zip_zero_struct(&zip); if (!mz_zip_reader_init_mem(&zip, pMem, size, flags)) { if (pErr) *pErr = zip.m_last_error; return MZ_FALSE; } if (!mz_zip_validate_archive(&zip, flags)) { actual_err = zip.m_last_error; success = MZ_FALSE; } if (!mz_zip_reader_end_internal(&zip, success)) { if (!actual_err) actual_err = zip.m_last_error; success = MZ_FALSE; } if (pErr) *pErr = actual_err; return success; } #ifndef MINIZ_NO_STDIO mz_bool mz_zip_validate_file_archive(const char *pFilename, mz_uint flags, mz_zip_error *pErr) { mz_bool success = MZ_TRUE; mz_zip_archive zip; mz_zip_error actual_err = MZ_ZIP_NO_ERROR; if (!pFilename) { if (pErr) *pErr = MZ_ZIP_INVALID_PARAMETER; return MZ_FALSE; } mz_zip_zero_struct(&zip); if (!mz_zip_reader_init_file_v2(&zip, pFilename, flags, 0, 0)) { if (pErr) *pErr = zip.m_last_error; return MZ_FALSE; } if (!mz_zip_validate_archive(&zip, flags)) { actual_err = zip.m_last_error; success = MZ_FALSE; } if (!mz_zip_reader_end_internal(&zip, success)) { if (!actual_err) actual_err = zip.m_last_error; success = MZ_FALSE; } if (pErr) *pErr = actual_err; return success; } #endif /* #ifndef MINIZ_NO_STDIO */ /* ------------------- .ZIP archive writing */ #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS static MZ_FORCEINLINE void mz_write_le16(mz_uint8 *p, mz_uint16 v) { p[0] = (mz_uint8)v; p[1] = (mz_uint8)(v >> 8); } static MZ_FORCEINLINE void mz_write_le32(mz_uint8 *p, mz_uint32 v) { p[0] = (mz_uint8)v; p[1] = (mz_uint8)(v >> 8); p[2] = (mz_uint8)(v >> 16); p[3] = (mz_uint8)(v >> 24); } static MZ_FORCEINLINE void mz_write_le64(mz_uint8 *p, mz_uint64 v) { mz_write_le32(p, (mz_uint32)v); mz_write_le32(p + sizeof(mz_uint32), (mz_uint32)(v >> 32)); } #define MZ_WRITE_LE16(p, v) mz_write_le16((mz_uint8 *)(p), (mz_uint16)(v)) #define MZ_WRITE_LE32(p, v) mz_write_le32((mz_uint8 *)(p), (mz_uint32)(v)) #define MZ_WRITE_LE64(p, v) mz_write_le64((mz_uint8 *)(p), (mz_uint64)(v)) static size_t mz_zip_heap_write_func(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n) { mz_zip_archive *pZip = (mz_zip_archive *)pOpaque; mz_zip_internal_state *pState = pZip->m_pState; mz_uint64 new_size = MZ_MAX(file_ofs + n, pState->m_mem_size); if (!n) return 0; /* An allocation this big is likely to just fail on 32-bit systems, so don't even go there. */ if ((sizeof(size_t) == sizeof(mz_uint32)) && (new_size > 0x7FFFFFFF)) { mz_zip_set_error(pZip, MZ_ZIP_FILE_TOO_LARGE); return 0; } if (new_size > pState->m_mem_capacity) { void *pNew_block; size_t new_capacity = MZ_MAX(64, pState->m_mem_capacity); while (new_capacity < new_size) new_capacity *= 2; if (NULL == (pNew_block = pZip->m_pRealloc(pZip->m_pAlloc_opaque, pState->m_pMem, 1, new_capacity))) { mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); return 0; } pState->m_pMem = pNew_block; pState->m_mem_capacity = new_capacity; } memcpy((mz_uint8 *)pState->m_pMem + file_ofs, pBuf, n); pState->m_mem_size = (size_t)new_size; return n; } static mz_bool mz_zip_writer_end_internal(mz_zip_archive *pZip, mz_bool set_last_error) { mz_zip_internal_state *pState; mz_bool status = MZ_TRUE; if ((!pZip) || (!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || ((pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) && (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED))) { if (set_last_error) mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); return MZ_FALSE; } pState = pZip->m_pState; pZip->m_pState = NULL; mz_zip_array_clear(pZip, &pState->m_central_dir); mz_zip_array_clear(pZip, &pState->m_central_dir_offsets); mz_zip_array_clear(pZip, &pState->m_sorted_central_dir_offsets); #ifndef MINIZ_NO_STDIO if (pState->m_pFile) { if (pZip->m_zip_type == MZ_ZIP_TYPE_FILE) { if (MZ_FCLOSE(pState->m_pFile) == EOF) { if (set_last_error) mz_zip_set_error(pZip, MZ_ZIP_FILE_CLOSE_FAILED); status = MZ_FALSE; } } pState->m_pFile = NULL; } #endif /* #ifndef MINIZ_NO_STDIO */ if ((pZip->m_pWrite == mz_zip_heap_write_func) && (pState->m_pMem)) { pZip->m_pFree(pZip->m_pAlloc_opaque, pState->m_pMem); pState->m_pMem = NULL; } pZip->m_pFree(pZip->m_pAlloc_opaque, pState); pZip->m_zip_mode = MZ_ZIP_MODE_INVALID; return status; } mz_bool mz_zip_writer_init_v2(mz_zip_archive *pZip, mz_uint64 existing_size, mz_uint flags) { mz_bool zip64 = (flags & MZ_ZIP_FLAG_WRITE_ZIP64) != 0; if ((!pZip) || (pZip->m_pState) || (!pZip->m_pWrite) || (pZip->m_zip_mode != MZ_ZIP_MODE_INVALID)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) { if (!pZip->m_pRead) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); } if (pZip->m_file_offset_alignment) { /* Ensure user specified file offset alignment is a power of 2. */ if (pZip->m_file_offset_alignment & (pZip->m_file_offset_alignment - 1)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); } if (!pZip->m_pAlloc) pZip->m_pAlloc = miniz_def_alloc_func; if (!pZip->m_pFree) pZip->m_pFree = miniz_def_free_func; if (!pZip->m_pRealloc) pZip->m_pRealloc = miniz_def_realloc_func; pZip->m_archive_size = existing_size; pZip->m_central_directory_file_ofs = 0; pZip->m_total_files = 0; if (NULL == (pZip->m_pState = (mz_zip_internal_state *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(mz_zip_internal_state)))) return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); memset(pZip->m_pState, 0, sizeof(mz_zip_internal_state)); MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir, sizeof(mz_uint8)); MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir_offsets, sizeof(mz_uint32)); MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_sorted_central_dir_offsets, sizeof(mz_uint32)); pZip->m_pState->m_zip64 = zip64; pZip->m_pState->m_zip64_has_extended_info_fields = zip64; pZip->m_zip_type = MZ_ZIP_TYPE_USER; pZip->m_zip_mode = MZ_ZIP_MODE_WRITING; return MZ_TRUE; } mz_bool mz_zip_writer_init(mz_zip_archive *pZip, mz_uint64 existing_size) { return mz_zip_writer_init_v2(pZip, existing_size, 0); } mz_bool mz_zip_writer_init_heap_v2(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size, mz_uint flags) { pZip->m_pWrite = mz_zip_heap_write_func; pZip->m_pNeeds_keepalive = NULL; if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) pZip->m_pRead = mz_zip_mem_read_func; pZip->m_pIO_opaque = pZip; if (!mz_zip_writer_init_v2(pZip, size_to_reserve_at_beginning, flags)) return MZ_FALSE; pZip->m_zip_type = MZ_ZIP_TYPE_HEAP; if (0 != (initial_allocation_size = MZ_MAX(initial_allocation_size, size_to_reserve_at_beginning))) { if (NULL == (pZip->m_pState->m_pMem = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, initial_allocation_size))) { mz_zip_writer_end_internal(pZip, MZ_FALSE); return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } pZip->m_pState->m_mem_capacity = initial_allocation_size; } return MZ_TRUE; } mz_bool mz_zip_writer_init_heap(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size) { return mz_zip_writer_init_heap_v2(pZip, size_to_reserve_at_beginning, initial_allocation_size, 0); } #ifndef MINIZ_NO_STDIO static size_t mz_zip_file_write_func(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n) { mz_zip_archive *pZip = (mz_zip_archive *)pOpaque; mz_int64 cur_ofs = MZ_FTELL64(pZip->m_pState->m_pFile); file_ofs += pZip->m_pState->m_file_archive_start_ofs; if (((mz_int64)file_ofs < 0) || (((cur_ofs != (mz_int64)file_ofs)) && (MZ_FSEEK64(pZip->m_pState->m_pFile, (mz_int64)file_ofs, SEEK_SET)))) { mz_zip_set_error(pZip, MZ_ZIP_FILE_SEEK_FAILED); return 0; } return MZ_FWRITE(pBuf, 1, n, pZip->m_pState->m_pFile); } mz_bool mz_zip_writer_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning) { return mz_zip_writer_init_file_v2(pZip, pFilename, size_to_reserve_at_beginning, 0); } mz_bool mz_zip_writer_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning, mz_uint flags) { MZ_FILE *pFile; pZip->m_pWrite = mz_zip_file_write_func; pZip->m_pNeeds_keepalive = NULL; if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) pZip->m_pRead = mz_zip_file_read_func; pZip->m_pIO_opaque = pZip; if (!mz_zip_writer_init_v2(pZip, size_to_reserve_at_beginning, flags)) return MZ_FALSE; if (NULL == (pFile = MZ_FOPEN(pFilename, (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) ? "w+b" : "wb"))) { mz_zip_writer_end(pZip); return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); } pZip->m_pState->m_pFile = pFile; pZip->m_zip_type = MZ_ZIP_TYPE_FILE; if (size_to_reserve_at_beginning) { mz_uint64 cur_ofs = 0; char buf[4096]; MZ_CLEAR_ARR(buf); do { size_t n = (size_t)MZ_MIN(sizeof(buf), size_to_reserve_at_beginning); if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_ofs, buf, n) != n) { mz_zip_writer_end(pZip); return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); } cur_ofs += n; size_to_reserve_at_beginning -= n; } while (size_to_reserve_at_beginning); } return MZ_TRUE; } mz_bool mz_zip_writer_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint flags) { pZip->m_pWrite = mz_zip_file_write_func; pZip->m_pNeeds_keepalive = NULL; if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) pZip->m_pRead = mz_zip_file_read_func; pZip->m_pIO_opaque = pZip; if (!mz_zip_writer_init_v2(pZip, 0, flags)) return MZ_FALSE; pZip->m_pState->m_pFile = pFile; pZip->m_pState->m_file_archive_start_ofs = MZ_FTELL64(pZip->m_pState->m_pFile); pZip->m_zip_type = MZ_ZIP_TYPE_CFILE; return MZ_TRUE; } #endif /* #ifndef MINIZ_NO_STDIO */ mz_bool mz_zip_writer_init_from_reader_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags) { mz_zip_internal_state *pState; if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_READING)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); if (flags & MZ_ZIP_FLAG_WRITE_ZIP64) { /* We don't support converting a non-zip64 file to zip64 - this seems like more trouble than it's worth. (What about the existing 32-bit data descriptors that could follow the compressed data?) */ if (!pZip->m_pState->m_zip64) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); } /* No sense in trying to write to an archive that's already at the support max size */ if (pZip->m_pState->m_zip64) { if (pZip->m_total_files == MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); } else { if (pZip->m_total_files == MZ_UINT16_MAX) return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); if ((pZip->m_archive_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_ZIP_LOCAL_DIR_HEADER_SIZE) > MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_FILE_TOO_LARGE); } pState = pZip->m_pState; if (pState->m_pFile) { #ifdef MINIZ_NO_STDIO (void)pFilename; return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); #else if (pZip->m_pIO_opaque != pZip) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); if (pZip->m_zip_type == MZ_ZIP_TYPE_FILE) { if (!pFilename) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); /* Archive is being read from stdio and was originally opened only for reading. Try to reopen as writable. */ if (NULL == (pState->m_pFile = MZ_FREOPEN(pFilename, "r+b", pState->m_pFile))) { /* The mz_zip_archive is now in a bogus state because pState->m_pFile is NULL, so just close it. */ mz_zip_reader_end_internal(pZip, MZ_FALSE); return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); } } pZip->m_pWrite = mz_zip_file_write_func; pZip->m_pNeeds_keepalive = NULL; #endif /* #ifdef MINIZ_NO_STDIO */ } else if (pState->m_pMem) { /* Archive lives in a memory block. Assume it's from the heap that we can resize using the realloc callback. */ if (pZip->m_pIO_opaque != pZip) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); pState->m_mem_capacity = pState->m_mem_size; pZip->m_pWrite = mz_zip_heap_write_func; pZip->m_pNeeds_keepalive = NULL; } /* Archive is being read via a user provided read function - make sure the user has specified a write function too. */ else if (!pZip->m_pWrite) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); /* Start writing new files at the archive's current central directory location. */ /* TODO: We could add a flag that lets the user start writing immediately AFTER the existing central dir - this would be safer. */ pZip->m_archive_size = pZip->m_central_directory_file_ofs; pZip->m_central_directory_file_ofs = 0; /* Clear the sorted central dir offsets, they aren't useful or maintained now. */ /* Even though we're now in write mode, files can still be extracted and verified, but file locates will be slow. */ /* TODO: We could easily maintain the sorted central directory offsets. */ mz_zip_array_clear(pZip, &pZip->m_pState->m_sorted_central_dir_offsets); pZip->m_zip_mode = MZ_ZIP_MODE_WRITING; return MZ_TRUE; } mz_bool mz_zip_writer_init_from_reader(mz_zip_archive *pZip, const char *pFilename) { return mz_zip_writer_init_from_reader_v2(pZip, pFilename, 0); } /* TODO: pArchive_name is a terrible name here! */ mz_bool mz_zip_writer_add_mem(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, mz_uint level_and_flags) { return mz_zip_writer_add_mem_ex(pZip, pArchive_name, pBuf, buf_size, NULL, 0, level_and_flags, 0, 0); } typedef struct { mz_zip_archive *m_pZip; mz_uint64 m_cur_archive_file_ofs; mz_uint64 m_comp_size; } mz_zip_writer_add_state; static mz_bool mz_zip_writer_add_put_buf_callback(const void *pBuf, int len, void *pUser) { mz_zip_writer_add_state *pState = (mz_zip_writer_add_state *)pUser; if ((int)pState->m_pZip->m_pWrite(pState->m_pZip->m_pIO_opaque, pState->m_cur_archive_file_ofs, pBuf, len) != len) return MZ_FALSE; pState->m_cur_archive_file_ofs += len; pState->m_comp_size += len; return MZ_TRUE; } #define MZ_ZIP64_MAX_LOCAL_EXTRA_FIELD_SIZE (sizeof(mz_uint16) * 2 + sizeof(mz_uint64) * 2) #define MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE (sizeof(mz_uint16) * 2 + sizeof(mz_uint64) * 3) static mz_uint32 mz_zip_writer_create_zip64_extra_data(mz_uint8 *pBuf, mz_uint64 *pUncomp_size, mz_uint64 *pComp_size, mz_uint64 *pLocal_header_ofs) { mz_uint8 *pDst = pBuf; mz_uint32 field_size = 0; MZ_WRITE_LE16(pDst + 0, MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID); MZ_WRITE_LE16(pDst + 2, 0); pDst += sizeof(mz_uint16) * 2; if (pUncomp_size) { MZ_WRITE_LE64(pDst, *pUncomp_size); pDst += sizeof(mz_uint64); field_size += sizeof(mz_uint64); } if (pComp_size) { MZ_WRITE_LE64(pDst, *pComp_size); pDst += sizeof(mz_uint64); field_size += sizeof(mz_uint64); } if (pLocal_header_ofs) { MZ_WRITE_LE64(pDst, *pLocal_header_ofs); pDst += sizeof(mz_uint64); field_size += sizeof(mz_uint64); } MZ_WRITE_LE16(pBuf + 2, field_size); return (mz_uint32)(pDst - pBuf); } static mz_bool mz_zip_writer_create_local_dir_header(mz_zip_archive *pZip, mz_uint8 *pDst, mz_uint16 filename_size, mz_uint16 extra_size, mz_uint64 uncomp_size, mz_uint64 comp_size, mz_uint32 uncomp_crc32, mz_uint16 method, mz_uint16 bit_flags, mz_uint16 dos_time, mz_uint16 dos_date) { (void)pZip; memset(pDst, 0, MZ_ZIP_LOCAL_DIR_HEADER_SIZE); MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_SIG_OFS, MZ_ZIP_LOCAL_DIR_HEADER_SIG); MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_VERSION_NEEDED_OFS, method ? 20 : 0); MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_BIT_FLAG_OFS, bit_flags); MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_METHOD_OFS, method); MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_FILE_TIME_OFS, dos_time); MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_FILE_DATE_OFS, dos_date); MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_CRC32_OFS, uncomp_crc32); MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_COMPRESSED_SIZE_OFS, MZ_MIN(comp_size, MZ_UINT32_MAX)); MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS, MZ_MIN(uncomp_size, MZ_UINT32_MAX)); MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_FILENAME_LEN_OFS, filename_size); MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_EXTRA_LEN_OFS, extra_size); return MZ_TRUE; } static mz_bool mz_zip_writer_create_central_dir_header(mz_zip_archive *pZip, mz_uint8 *pDst, mz_uint16 filename_size, mz_uint16 extra_size, mz_uint16 comment_size, mz_uint64 uncomp_size, mz_uint64 comp_size, mz_uint32 uncomp_crc32, mz_uint16 method, mz_uint16 bit_flags, mz_uint16 dos_time, mz_uint16 dos_date, mz_uint64 local_header_ofs, mz_uint32 ext_attributes) { (void)pZip; memset(pDst, 0, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE); MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_SIG_OFS, MZ_ZIP_CENTRAL_DIR_HEADER_SIG); MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_VERSION_NEEDED_OFS, method ? 20 : 0); MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_BIT_FLAG_OFS, bit_flags); MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_METHOD_OFS, method); MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_FILE_TIME_OFS, dos_time); MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_FILE_DATE_OFS, dos_date); MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_CRC32_OFS, uncomp_crc32); MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS, MZ_MIN(comp_size, MZ_UINT32_MAX)); MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS, MZ_MIN(uncomp_size, MZ_UINT32_MAX)); MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_FILENAME_LEN_OFS, filename_size); MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_EXTRA_LEN_OFS, extra_size); MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_COMMENT_LEN_OFS, comment_size); MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_EXTERNAL_ATTR_OFS, ext_attributes); MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_LOCAL_HEADER_OFS, MZ_MIN(local_header_ofs, MZ_UINT32_MAX)); return MZ_TRUE; } static mz_bool mz_zip_writer_add_to_central_dir(mz_zip_archive *pZip, const char *pFilename, mz_uint16 filename_size, const void *pExtra, mz_uint16 extra_size, const void *pComment, mz_uint16 comment_size, mz_uint64 uncomp_size, mz_uint64 comp_size, mz_uint32 uncomp_crc32, mz_uint16 method, mz_uint16 bit_flags, mz_uint16 dos_time, mz_uint16 dos_date, mz_uint64 local_header_ofs, mz_uint32 ext_attributes, const char *user_extra_data, mz_uint user_extra_data_len) { mz_zip_internal_state *pState = pZip->m_pState; mz_uint32 central_dir_ofs = (mz_uint32)pState->m_central_dir.m_size; size_t orig_central_dir_size = pState->m_central_dir.m_size; mz_uint8 central_dir_header[MZ_ZIP_CENTRAL_DIR_HEADER_SIZE]; if (!pZip->m_pState->m_zip64) { if (local_header_ofs > 0xFFFFFFFF) return mz_zip_set_error(pZip, MZ_ZIP_FILE_TOO_LARGE); } /* miniz doesn't support central dirs >= MZ_UINT32_MAX bytes yet */ if (((mz_uint64)pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size + extra_size + user_extra_data_len + comment_size) >= MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); if (!mz_zip_writer_create_central_dir_header(pZip, central_dir_header, filename_size, (mz_uint16)(extra_size + user_extra_data_len), comment_size, uncomp_size, comp_size, uncomp_crc32, method, bit_flags, dos_time, dos_date, local_header_ofs, ext_attributes)) return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); if ((!mz_zip_array_push_back(pZip, &pState->m_central_dir, central_dir_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE)) || (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pFilename, filename_size)) || (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pExtra, extra_size)) || (!mz_zip_array_push_back(pZip, &pState->m_central_dir, user_extra_data, user_extra_data_len)) || (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pComment, comment_size)) || (!mz_zip_array_push_back(pZip, &pState->m_central_dir_offsets, ¢ral_dir_ofs, 1))) { /* Try to resize the central directory array back into its original state. */ mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } return MZ_TRUE; } static mz_bool mz_zip_writer_validate_archive_name(const char *pArchive_name) { /* Basic ZIP archive filename validity checks: Valid filenames cannot start with a forward slash, cannot contain a drive letter, and cannot use DOS-style backward slashes. */ if (*pArchive_name == '/') return MZ_FALSE; /* Making sure the name does not contain drive letters or DOS style backward slashes is the responsibility of the program using miniz*/ return MZ_TRUE; } static mz_uint mz_zip_writer_compute_padding_needed_for_file_alignment(mz_zip_archive *pZip) { mz_uint32 n; if (!pZip->m_file_offset_alignment) return 0; n = (mz_uint32)(pZip->m_archive_size & (pZip->m_file_offset_alignment - 1)); return (mz_uint)((pZip->m_file_offset_alignment - n) & (pZip->m_file_offset_alignment - 1)); } static mz_bool mz_zip_writer_write_zeros(mz_zip_archive *pZip, mz_uint64 cur_file_ofs, mz_uint32 n) { char buf[4096]; memset(buf, 0, MZ_MIN(sizeof(buf), n)); while (n) { mz_uint32 s = MZ_MIN(sizeof(buf), n); if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_file_ofs, buf, s) != s) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); cur_file_ofs += s; n -= s; } return MZ_TRUE; } mz_bool mz_zip_writer_add_mem_ex(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, mz_uint64 uncomp_size, mz_uint32 uncomp_crc32) { return mz_zip_writer_add_mem_ex_v2(pZip, pArchive_name, pBuf, buf_size, pComment, comment_size, level_and_flags, uncomp_size, uncomp_crc32, NULL, NULL, 0, NULL, 0); } mz_bool mz_zip_writer_add_mem_ex_v2(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, mz_uint64 uncomp_size, mz_uint32 uncomp_crc32, MZ_TIME_T *last_modified, const char *user_extra_data, mz_uint user_extra_data_len, const char *user_extra_data_central, mz_uint user_extra_data_central_len) { mz_uint16 method = 0, dos_time = 0, dos_date = 0; mz_uint level, ext_attributes = 0, num_alignment_padding_bytes; mz_uint64 local_dir_header_ofs = pZip->m_archive_size, cur_archive_file_ofs = pZip->m_archive_size, comp_size = 0; size_t archive_name_size; mz_uint8 local_dir_header[MZ_ZIP_LOCAL_DIR_HEADER_SIZE]; tdefl_compressor *pComp = NULL; mz_bool store_data_uncompressed; mz_zip_internal_state *pState; mz_uint8 *pExtra_data = NULL; mz_uint32 extra_size = 0; mz_uint8 extra_data[MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE]; mz_uint16 bit_flags = 0; if ((int)level_and_flags < 0) level_and_flags = MZ_DEFAULT_LEVEL; if (uncomp_size || (buf_size && !(level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA))) bit_flags |= MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR; if (!(level_and_flags & MZ_ZIP_FLAG_ASCII_FILENAME)) bit_flags |= MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_UTF8; level = level_and_flags & 0xF; store_data_uncompressed = ((!level) || (level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA)); if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) || ((buf_size) && (!pBuf)) || (!pArchive_name) || ((comment_size) && (!pComment)) || (level > MZ_UBER_COMPRESSION)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); pState = pZip->m_pState; if (pState->m_zip64) { if (pZip->m_total_files == MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); } else { if (pZip->m_total_files == MZ_UINT16_MAX) { pState->m_zip64 = MZ_TRUE; /*return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); */ } if (((mz_uint64)buf_size > 0xFFFFFFFF) || (uncomp_size > 0xFFFFFFFF)) { pState->m_zip64 = MZ_TRUE; /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */ } } if ((!(level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (uncomp_size)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); if (!mz_zip_writer_validate_archive_name(pArchive_name)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME); #ifndef MINIZ_NO_TIME if (last_modified != NULL) { mz_zip_time_t_to_dos_time(*last_modified, &dos_time, &dos_date); } else { MZ_TIME_T cur_time; time(&cur_time); mz_zip_time_t_to_dos_time(cur_time, &dos_time, &dos_date); } #endif /* #ifndef MINIZ_NO_TIME */ if (!(level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) { uncomp_crc32 = (mz_uint32)mz_crc32(MZ_CRC32_INIT, (const mz_uint8 *)pBuf, buf_size); uncomp_size = buf_size; if (uncomp_size <= 3) { level = 0; store_data_uncompressed = MZ_TRUE; } } archive_name_size = strlen(pArchive_name); if (archive_name_size > MZ_UINT16_MAX) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME); num_alignment_padding_bytes = mz_zip_writer_compute_padding_needed_for_file_alignment(pZip); /* miniz doesn't support central dirs >= MZ_UINT32_MAX bytes yet */ if (((mz_uint64)pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE + comment_size) >= MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); if (!pState->m_zip64) { /* Bail early if the archive would obviously become too large */ if ((pZip->m_archive_size + num_alignment_padding_bytes + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + archive_name_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + comment_size + user_extra_data_len + pState->m_central_dir.m_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE + user_extra_data_central_len + MZ_ZIP_DATA_DESCRIPTER_SIZE32) > 0xFFFFFFFF) { pState->m_zip64 = MZ_TRUE; /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */ } } if ((archive_name_size) && (pArchive_name[archive_name_size - 1] == '/')) { /* Set DOS Subdirectory attribute bit. */ ext_attributes |= MZ_ZIP_DOS_DIR_ATTRIBUTE_BITFLAG; /* Subdirectories cannot contain data. */ if ((buf_size) || (uncomp_size)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); } /* Try to do any allocations before writing to the archive, so if an allocation fails the file remains unmodified. (A good idea if we're doing an in-place modification.) */ if ((!mz_zip_array_ensure_room(pZip, &pState->m_central_dir, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + comment_size + (pState->m_zip64 ? MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE : 0))) || (!mz_zip_array_ensure_room(pZip, &pState->m_central_dir_offsets, 1))) return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); if ((!store_data_uncompressed) && (buf_size)) { if (NULL == (pComp = (tdefl_compressor *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(tdefl_compressor)))) return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } if (!mz_zip_writer_write_zeros(pZip, cur_archive_file_ofs, num_alignment_padding_bytes)) { pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); return MZ_FALSE; } local_dir_header_ofs += num_alignment_padding_bytes; if (pZip->m_file_offset_alignment) { MZ_ASSERT((local_dir_header_ofs & (pZip->m_file_offset_alignment - 1)) == 0); } cur_archive_file_ofs += num_alignment_padding_bytes; MZ_CLEAR_ARR(local_dir_header); if (!store_data_uncompressed || (level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) { method = MZ_DEFLATED; } if (pState->m_zip64) { if (uncomp_size >= MZ_UINT32_MAX || local_dir_header_ofs >= MZ_UINT32_MAX) { pExtra_data = extra_data; extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (uncomp_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL, (uncomp_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); } if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)(extra_size + user_extra_data_len), 0, 0, 0, method, bit_flags, dos_time, dos_date)) return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); if (pZip->m_pWrite(pZip->m_pIO_opaque, local_dir_header_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header)) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); cur_archive_file_ofs += sizeof(local_dir_header); if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size) { pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); } cur_archive_file_ofs += archive_name_size; if (pExtra_data != NULL) { if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, extra_data, extra_size) != extra_size) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); cur_archive_file_ofs += extra_size; } } else { if ((comp_size > MZ_UINT32_MAX) || (cur_archive_file_ofs > MZ_UINT32_MAX)) return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)user_extra_data_len, 0, 0, 0, method, bit_flags, dos_time, dos_date)) return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); if (pZip->m_pWrite(pZip->m_pIO_opaque, local_dir_header_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header)) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); cur_archive_file_ofs += sizeof(local_dir_header); if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size) { pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); } cur_archive_file_ofs += archive_name_size; } if (user_extra_data_len > 0) { if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, user_extra_data, user_extra_data_len) != user_extra_data_len) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); cur_archive_file_ofs += user_extra_data_len; } if (store_data_uncompressed) { if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pBuf, buf_size) != buf_size) { pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); } cur_archive_file_ofs += buf_size; comp_size = buf_size; } else if (buf_size) { mz_zip_writer_add_state state; state.m_pZip = pZip; state.m_cur_archive_file_ofs = cur_archive_file_ofs; state.m_comp_size = 0; if ((tdefl_init(pComp, mz_zip_writer_add_put_buf_callback, &state, tdefl_create_comp_flags_from_zip_params(level, -15, MZ_DEFAULT_STRATEGY)) != TDEFL_STATUS_OKAY) || (tdefl_compress_buffer(pComp, pBuf, buf_size, TDEFL_FINISH) != TDEFL_STATUS_DONE)) { pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); return mz_zip_set_error(pZip, MZ_ZIP_COMPRESSION_FAILED); } comp_size = state.m_comp_size; cur_archive_file_ofs = state.m_cur_archive_file_ofs; } pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); pComp = NULL; if (uncomp_size) { mz_uint8 local_dir_footer[MZ_ZIP_DATA_DESCRIPTER_SIZE64]; mz_uint32 local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE32; MZ_ASSERT(bit_flags & MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR); MZ_WRITE_LE32(local_dir_footer + 0, MZ_ZIP_DATA_DESCRIPTOR_ID); MZ_WRITE_LE32(local_dir_footer + 4, uncomp_crc32); if (pExtra_data == NULL) { if (comp_size > MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); MZ_WRITE_LE32(local_dir_footer + 8, comp_size); MZ_WRITE_LE32(local_dir_footer + 12, uncomp_size); } else { MZ_WRITE_LE64(local_dir_footer + 8, comp_size); MZ_WRITE_LE64(local_dir_footer + 16, uncomp_size); local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE64; } if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_footer, local_dir_footer_size) != local_dir_footer_size) return MZ_FALSE; cur_archive_file_ofs += local_dir_footer_size; } if (pExtra_data != NULL) { extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (uncomp_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL, (uncomp_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); } if (!mz_zip_writer_add_to_central_dir(pZip, pArchive_name, (mz_uint16)archive_name_size, pExtra_data, (mz_uint16)extra_size, pComment, comment_size, uncomp_size, comp_size, uncomp_crc32, method, bit_flags, dos_time, dos_date, local_dir_header_ofs, ext_attributes, user_extra_data_central, user_extra_data_central_len)) return MZ_FALSE; pZip->m_total_files++; pZip->m_archive_size = cur_archive_file_ofs; return MZ_TRUE; } mz_bool mz_zip_writer_add_read_buf_callback(mz_zip_archive *pZip, const char *pArchive_name, mz_file_read_func read_callback, void* callback_opaque, mz_uint64 max_size, const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, const char *user_extra_data, mz_uint user_extra_data_len, const char *user_extra_data_central, mz_uint user_extra_data_central_len) { mz_uint16 gen_flags; mz_uint uncomp_crc32 = MZ_CRC32_INIT, level, num_alignment_padding_bytes; mz_uint16 method = 0, dos_time = 0, dos_date = 0, ext_attributes = 0; mz_uint64 local_dir_header_ofs, cur_archive_file_ofs = pZip->m_archive_size, uncomp_size = 0, comp_size = 0; size_t archive_name_size; mz_uint8 local_dir_header[MZ_ZIP_LOCAL_DIR_HEADER_SIZE]; mz_uint8 *pExtra_data = NULL; mz_uint32 extra_size = 0; mz_uint8 extra_data[MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE]; mz_zip_internal_state *pState; mz_uint64 file_ofs = 0, cur_archive_header_file_ofs; if ((int)level_and_flags < 0) level_and_flags = MZ_DEFAULT_LEVEL; level = level_and_flags & 0xF; gen_flags = (level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE) ? 0 : MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR; if (!(level_and_flags & MZ_ZIP_FLAG_ASCII_FILENAME)) gen_flags |= MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_UTF8; /* Sanity checks */ if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) || (!pArchive_name) || ((comment_size) && (!pComment)) || (level > MZ_UBER_COMPRESSION)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); pState = pZip->m_pState; if ((!pState->m_zip64) && (max_size > MZ_UINT32_MAX)) { /* Source file is too large for non-zip64 */ /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */ pState->m_zip64 = MZ_TRUE; } /* We could support this, but why? */ if (level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); if (!mz_zip_writer_validate_archive_name(pArchive_name)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME); if (pState->m_zip64) { if (pZip->m_total_files == MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); } else { if (pZip->m_total_files == MZ_UINT16_MAX) { pState->m_zip64 = MZ_TRUE; /*return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); */ } } archive_name_size = strlen(pArchive_name); if (archive_name_size > MZ_UINT16_MAX) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME); num_alignment_padding_bytes = mz_zip_writer_compute_padding_needed_for_file_alignment(pZip); /* miniz doesn't support central dirs >= MZ_UINT32_MAX bytes yet */ if (((mz_uint64)pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE + comment_size) >= MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); if (!pState->m_zip64) { /* Bail early if the archive would obviously become too large */ if ((pZip->m_archive_size + num_alignment_padding_bytes + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + archive_name_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + comment_size + user_extra_data_len + pState->m_central_dir.m_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE + 1024 + MZ_ZIP_DATA_DESCRIPTER_SIZE32 + user_extra_data_central_len) > 0xFFFFFFFF) { pState->m_zip64 = MZ_TRUE; /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */ } } #ifndef MINIZ_NO_TIME if (pFile_time) { mz_zip_time_t_to_dos_time(*pFile_time, &dos_time, &dos_date); } #endif if (max_size <= 3) level = 0; if (!mz_zip_writer_write_zeros(pZip, cur_archive_file_ofs, num_alignment_padding_bytes)) { return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); } cur_archive_file_ofs += num_alignment_padding_bytes; local_dir_header_ofs = cur_archive_file_ofs; if (pZip->m_file_offset_alignment) { MZ_ASSERT((cur_archive_file_ofs & (pZip->m_file_offset_alignment - 1)) == 0); } if (max_size && level) { method = MZ_DEFLATED; } MZ_CLEAR_ARR(local_dir_header); if (pState->m_zip64) { if (max_size >= MZ_UINT32_MAX || local_dir_header_ofs >= MZ_UINT32_MAX) { pExtra_data = extra_data; if (level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE) extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (max_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL, (max_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); else extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, NULL, NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); } if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)(extra_size + user_extra_data_len), 0, 0, 0, method, gen_flags, dos_time, dos_date)) return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header)) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); cur_archive_file_ofs += sizeof(local_dir_header); if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size) { return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); } cur_archive_file_ofs += archive_name_size; if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, extra_data, extra_size) != extra_size) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); cur_archive_file_ofs += extra_size; } else { if ((comp_size > MZ_UINT32_MAX) || (cur_archive_file_ofs > MZ_UINT32_MAX)) return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)user_extra_data_len, 0, 0, 0, method, gen_flags, dos_time, dos_date)) return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header)) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); cur_archive_file_ofs += sizeof(local_dir_header); if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size) { return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); } cur_archive_file_ofs += archive_name_size; } if (user_extra_data_len > 0) { if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, user_extra_data, user_extra_data_len) != user_extra_data_len) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); cur_archive_file_ofs += user_extra_data_len; } if (max_size) { void *pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, MZ_ZIP_MAX_IO_BUF_SIZE); if (!pRead_buf) { return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } if (!level) { while (1) { size_t n = read_callback(callback_opaque, file_ofs, pRead_buf, MZ_ZIP_MAX_IO_BUF_SIZE); if (n == 0) break; if ((n > MZ_ZIP_MAX_IO_BUF_SIZE) || (file_ofs + n > max_size)) { pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); } if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pRead_buf, n) != n) { pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); } file_ofs += n; uncomp_crc32 = (mz_uint32)mz_crc32(uncomp_crc32, (const mz_uint8 *)pRead_buf, n); cur_archive_file_ofs += n; } uncomp_size = file_ofs; comp_size = uncomp_size; } else { mz_bool result = MZ_FALSE; mz_zip_writer_add_state state; tdefl_compressor *pComp = (tdefl_compressor *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(tdefl_compressor)); if (!pComp) { pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } state.m_pZip = pZip; state.m_cur_archive_file_ofs = cur_archive_file_ofs; state.m_comp_size = 0; if (tdefl_init(pComp, mz_zip_writer_add_put_buf_callback, &state, tdefl_create_comp_flags_from_zip_params(level, -15, MZ_DEFAULT_STRATEGY)) != TDEFL_STATUS_OKAY) { pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); } for (;;) { tdefl_status status; tdefl_flush flush = TDEFL_NO_FLUSH; size_t n = read_callback(callback_opaque, file_ofs, pRead_buf, MZ_ZIP_MAX_IO_BUF_SIZE); if ((n > MZ_ZIP_MAX_IO_BUF_SIZE) || (file_ofs + n > max_size)) { mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); break; } file_ofs += n; uncomp_crc32 = (mz_uint32)mz_crc32(uncomp_crc32, (const mz_uint8 *)pRead_buf, n); if (pZip->m_pNeeds_keepalive != NULL && pZip->m_pNeeds_keepalive(pZip->m_pIO_opaque)) flush = TDEFL_FULL_FLUSH; if (n == 0) flush = TDEFL_FINISH; status = tdefl_compress_buffer(pComp, pRead_buf, n, flush); if (status == TDEFL_STATUS_DONE) { result = MZ_TRUE; break; } else if (status != TDEFL_STATUS_OKAY) { mz_zip_set_error(pZip, MZ_ZIP_COMPRESSION_FAILED); break; } } pZip->m_pFree(pZip->m_pAlloc_opaque, pComp); if (!result) { pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); return MZ_FALSE; } uncomp_size = file_ofs; comp_size = state.m_comp_size; cur_archive_file_ofs = state.m_cur_archive_file_ofs; } pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf); } if (!(level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE)) { mz_uint8 local_dir_footer[MZ_ZIP_DATA_DESCRIPTER_SIZE64]; mz_uint32 local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE32; MZ_WRITE_LE32(local_dir_footer + 0, MZ_ZIP_DATA_DESCRIPTOR_ID); MZ_WRITE_LE32(local_dir_footer + 4, uncomp_crc32); if (pExtra_data == NULL) { if (comp_size > MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); MZ_WRITE_LE32(local_dir_footer + 8, comp_size); MZ_WRITE_LE32(local_dir_footer + 12, uncomp_size); } else { MZ_WRITE_LE64(local_dir_footer + 8, comp_size); MZ_WRITE_LE64(local_dir_footer + 16, uncomp_size); local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE64; } if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_footer, local_dir_footer_size) != local_dir_footer_size) return MZ_FALSE; cur_archive_file_ofs += local_dir_footer_size; } if (level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE) { if (pExtra_data != NULL) { extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (max_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL, (max_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); } if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)(extra_size + user_extra_data_len), (max_size >= MZ_UINT32_MAX) ? MZ_UINT32_MAX : uncomp_size, (max_size >= MZ_UINT32_MAX) ? MZ_UINT32_MAX : comp_size, uncomp_crc32, method, gen_flags, dos_time, dos_date)) return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR); cur_archive_header_file_ofs = local_dir_header_ofs; if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_header_file_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header)) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); if (pExtra_data != NULL) { cur_archive_header_file_ofs += sizeof(local_dir_header); if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_header_file_ofs, pArchive_name, archive_name_size) != archive_name_size) { return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); } cur_archive_header_file_ofs += archive_name_size; if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_header_file_ofs, extra_data, extra_size) != extra_size) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); cur_archive_header_file_ofs += extra_size; } } if (pExtra_data != NULL) { extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (uncomp_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL, (uncomp_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL); } if (!mz_zip_writer_add_to_central_dir(pZip, pArchive_name, (mz_uint16)archive_name_size, pExtra_data, (mz_uint16)extra_size, pComment, comment_size, uncomp_size, comp_size, uncomp_crc32, method, gen_flags, dos_time, dos_date, local_dir_header_ofs, ext_attributes, user_extra_data_central, user_extra_data_central_len)) return MZ_FALSE; pZip->m_total_files++; pZip->m_archive_size = cur_archive_file_ofs; return MZ_TRUE; } #ifndef MINIZ_NO_STDIO static size_t mz_file_read_func_stdio(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n) { MZ_FILE *pSrc_file = (MZ_FILE *)pOpaque; mz_int64 cur_ofs = MZ_FTELL64(pSrc_file); if (((mz_int64)file_ofs < 0) || (((cur_ofs != (mz_int64)file_ofs)) && (MZ_FSEEK64(pSrc_file, (mz_int64)file_ofs, SEEK_SET)))) return 0; return MZ_FREAD(pBuf, 1, n, pSrc_file); } mz_bool mz_zip_writer_add_cfile(mz_zip_archive *pZip, const char *pArchive_name, MZ_FILE *pSrc_file, mz_uint64 max_size, const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, const char *user_extra_data, mz_uint user_extra_data_len, const char *user_extra_data_central, mz_uint user_extra_data_central_len) { return mz_zip_writer_add_read_buf_callback(pZip, pArchive_name, mz_file_read_func_stdio, pSrc_file, max_size, pFile_time, pComment, comment_size, level_and_flags, user_extra_data, user_extra_data_len, user_extra_data_central, user_extra_data_central_len); } mz_bool mz_zip_writer_add_file(mz_zip_archive *pZip, const char *pArchive_name, const char *pSrc_filename, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags) { MZ_FILE *pSrc_file = NULL; mz_uint64 uncomp_size = 0; MZ_TIME_T file_modified_time; MZ_TIME_T *pFile_time = NULL; mz_bool status; memset(&file_modified_time, 0, sizeof(file_modified_time)); #if !defined(MINIZ_NO_TIME) && !defined(MINIZ_NO_STDIO) pFile_time = &file_modified_time; if (!mz_zip_get_file_modified_time(pSrc_filename, &file_modified_time)) return mz_zip_set_error(pZip, MZ_ZIP_FILE_STAT_FAILED); #endif pSrc_file = MZ_FOPEN(pSrc_filename, "rb"); if (!pSrc_file) return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED); MZ_FSEEK64(pSrc_file, 0, SEEK_END); uncomp_size = MZ_FTELL64(pSrc_file); MZ_FSEEK64(pSrc_file, 0, SEEK_SET); status = mz_zip_writer_add_cfile(pZip, pArchive_name, pSrc_file, uncomp_size, pFile_time, pComment, comment_size, level_and_flags, NULL, 0, NULL, 0); MZ_FCLOSE(pSrc_file); return status; } #endif /* #ifndef MINIZ_NO_STDIO */ static mz_bool mz_zip_writer_update_zip64_extension_block(mz_zip_array *pNew_ext, mz_zip_archive *pZip, const mz_uint8 *pExt, mz_uint32 ext_len, mz_uint64 *pComp_size, mz_uint64 *pUncomp_size, mz_uint64 *pLocal_header_ofs, mz_uint32 *pDisk_start) { /* + 64 should be enough for any new zip64 data */ if (!mz_zip_array_reserve(pZip, pNew_ext, ext_len + 64, MZ_FALSE)) return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); mz_zip_array_resize(pZip, pNew_ext, 0, MZ_FALSE); if ((pUncomp_size) || (pComp_size) || (pLocal_header_ofs) || (pDisk_start)) { mz_uint8 new_ext_block[64]; mz_uint8 *pDst = new_ext_block; mz_write_le16(pDst, MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID); mz_write_le16(pDst + sizeof(mz_uint16), 0); pDst += sizeof(mz_uint16) * 2; if (pUncomp_size) { mz_write_le64(pDst, *pUncomp_size); pDst += sizeof(mz_uint64); } if (pComp_size) { mz_write_le64(pDst, *pComp_size); pDst += sizeof(mz_uint64); } if (pLocal_header_ofs) { mz_write_le64(pDst, *pLocal_header_ofs); pDst += sizeof(mz_uint64); } if (pDisk_start) { mz_write_le32(pDst, *pDisk_start); pDst += sizeof(mz_uint32); } mz_write_le16(new_ext_block + sizeof(mz_uint16), (mz_uint16)((pDst - new_ext_block) - sizeof(mz_uint16) * 2)); if (!mz_zip_array_push_back(pZip, pNew_ext, new_ext_block, pDst - new_ext_block)) return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } if ((pExt) && (ext_len)) { mz_uint32 extra_size_remaining = ext_len; const mz_uint8 *pExtra_data = pExt; do { mz_uint32 field_id, field_data_size, field_total_size; if (extra_size_remaining < (sizeof(mz_uint16) * 2)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); field_id = MZ_READ_LE16(pExtra_data); field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); field_total_size = field_data_size + sizeof(mz_uint16) * 2; if (field_total_size > extra_size_remaining) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); if (field_id != MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) { if (!mz_zip_array_push_back(pZip, pNew_ext, pExtra_data, field_total_size)) return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } pExtra_data += field_total_size; extra_size_remaining -= field_total_size; } while (extra_size_remaining); } return MZ_TRUE; } /* TODO: This func is now pretty freakin complex due to zip64, split it up? */ mz_bool mz_zip_writer_add_from_zip_reader(mz_zip_archive *pZip, mz_zip_archive *pSource_zip, mz_uint src_file_index) { mz_uint n, bit_flags, num_alignment_padding_bytes, src_central_dir_following_data_size; mz_uint64 src_archive_bytes_remaining, local_dir_header_ofs; mz_uint64 cur_src_file_ofs, cur_dst_file_ofs; mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)]; mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32; mz_uint8 new_central_header[MZ_ZIP_CENTRAL_DIR_HEADER_SIZE]; size_t orig_central_dir_size; mz_zip_internal_state *pState; void *pBuf; const mz_uint8 *pSrc_central_header; mz_zip_archive_file_stat src_file_stat; mz_uint32 src_filename_len, src_comment_len, src_ext_len; mz_uint32 local_header_filename_size, local_header_extra_len; mz_uint64 local_header_comp_size, local_header_uncomp_size; mz_bool found_zip64_ext_data_in_ldir = MZ_FALSE; /* Sanity checks */ if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) || (!pSource_zip->m_pRead)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); pState = pZip->m_pState; /* Don't support copying files from zip64 archives to non-zip64, even though in some cases this is possible */ if ((pSource_zip->m_pState->m_zip64) && (!pZip->m_pState->m_zip64)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); /* Get pointer to the source central dir header and crack it */ if (NULL == (pSrc_central_header = mz_zip_get_cdh(pSource_zip, src_file_index))) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); if (MZ_READ_LE32(pSrc_central_header + MZ_ZIP_CDH_SIG_OFS) != MZ_ZIP_CENTRAL_DIR_HEADER_SIG) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); src_filename_len = MZ_READ_LE16(pSrc_central_header + MZ_ZIP_CDH_FILENAME_LEN_OFS); src_comment_len = MZ_READ_LE16(pSrc_central_header + MZ_ZIP_CDH_COMMENT_LEN_OFS); src_ext_len = MZ_READ_LE16(pSrc_central_header + MZ_ZIP_CDH_EXTRA_LEN_OFS); src_central_dir_following_data_size = src_filename_len + src_ext_len + src_comment_len; /* TODO: We don't support central dir's >= MZ_UINT32_MAX bytes right now (+32 fudge factor in case we need to add more extra data) */ if ((pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_central_dir_following_data_size + 32) >= MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); num_alignment_padding_bytes = mz_zip_writer_compute_padding_needed_for_file_alignment(pZip); if (!pState->m_zip64) { if (pZip->m_total_files == MZ_UINT16_MAX) return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); } else { /* TODO: Our zip64 support still has some 32-bit limits that may not be worth fixing. */ if (pZip->m_total_files == MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); } if (!mz_zip_file_stat_internal(pSource_zip, src_file_index, pSrc_central_header, &src_file_stat, NULL)) return MZ_FALSE; cur_src_file_ofs = src_file_stat.m_local_header_ofs; cur_dst_file_ofs = pZip->m_archive_size; /* Read the source archive's local dir header */ if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); cur_src_file_ofs += MZ_ZIP_LOCAL_DIR_HEADER_SIZE; /* Compute the total size we need to copy (filename+extra data+compressed data) */ local_header_filename_size = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS); local_header_extra_len = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS); local_header_comp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_COMPRESSED_SIZE_OFS); local_header_uncomp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS); src_archive_bytes_remaining = local_header_filename_size + local_header_extra_len + src_file_stat.m_comp_size; /* Try to find a zip64 extended information field */ if ((local_header_extra_len) && ((local_header_comp_size == MZ_UINT32_MAX) || (local_header_uncomp_size == MZ_UINT32_MAX))) { mz_zip_array file_data_array; const mz_uint8 *pExtra_data; mz_uint32 extra_size_remaining = local_header_extra_len; mz_zip_array_init(&file_data_array, 1); if (!mz_zip_array_resize(pZip, &file_data_array, local_header_extra_len, MZ_FALSE)) { return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, src_file_stat.m_local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_size, file_data_array.m_p, local_header_extra_len) != local_header_extra_len) { mz_zip_array_clear(pZip, &file_data_array); return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); } pExtra_data = (const mz_uint8 *)file_data_array.m_p; do { mz_uint32 field_id, field_data_size, field_total_size; if (extra_size_remaining < (sizeof(mz_uint16) * 2)) { mz_zip_array_clear(pZip, &file_data_array); return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); } field_id = MZ_READ_LE16(pExtra_data); field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16)); field_total_size = field_data_size + sizeof(mz_uint16) * 2; if (field_total_size > extra_size_remaining) { mz_zip_array_clear(pZip, &file_data_array); return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); } if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID) { const mz_uint8 *pSrc_field_data = pExtra_data + sizeof(mz_uint32); if (field_data_size < sizeof(mz_uint64) * 2) { mz_zip_array_clear(pZip, &file_data_array); return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED); } local_header_uncomp_size = MZ_READ_LE64(pSrc_field_data); local_header_comp_size = MZ_READ_LE64(pSrc_field_data + sizeof(mz_uint64)); /* may be 0 if there's a descriptor */ found_zip64_ext_data_in_ldir = MZ_TRUE; break; } pExtra_data += field_total_size; extra_size_remaining -= field_total_size; } while (extra_size_remaining); mz_zip_array_clear(pZip, &file_data_array); } if (!pState->m_zip64) { /* Try to detect if the new archive will most likely wind up too big and bail early (+(sizeof(mz_uint32) * 4) is for the optional descriptor which could be present, +64 is a fudge factor). */ /* We also check when the archive is finalized so this doesn't need to be perfect. */ mz_uint64 approx_new_archive_size = cur_dst_file_ofs + num_alignment_padding_bytes + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + src_archive_bytes_remaining + (sizeof(mz_uint32) * 4) + pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_central_dir_following_data_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE + 64; if (approx_new_archive_size >= MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); } /* Write dest archive padding */ if (!mz_zip_writer_write_zeros(pZip, cur_dst_file_ofs, num_alignment_padding_bytes)) return MZ_FALSE; cur_dst_file_ofs += num_alignment_padding_bytes; local_dir_header_ofs = cur_dst_file_ofs; if (pZip->m_file_offset_alignment) { MZ_ASSERT((local_dir_header_ofs & (pZip->m_file_offset_alignment - 1)) == 0); } /* The original zip's local header+ext block doesn't change, even with zip64, so we can just copy it over to the dest zip */ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_dst_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); cur_dst_file_ofs += MZ_ZIP_LOCAL_DIR_HEADER_SIZE; /* Copy over the source archive bytes to the dest archive, also ensure we have enough buf space to handle optional data descriptor */ if (NULL == (pBuf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)MZ_MAX(32U, MZ_MIN((mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE, src_archive_bytes_remaining))))) return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); while (src_archive_bytes_remaining) { n = (mz_uint)MZ_MIN((mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE, src_archive_bytes_remaining); if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pBuf, n) != n) { pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); } cur_src_file_ofs += n; if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_dst_file_ofs, pBuf, n) != n) { pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); } cur_dst_file_ofs += n; src_archive_bytes_remaining -= n; } /* Now deal with the optional data descriptor */ bit_flags = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_BIT_FLAG_OFS); if (bit_flags & 8) { /* Copy data descriptor */ if ((pSource_zip->m_pState->m_zip64) || (found_zip64_ext_data_in_ldir)) { /* src is zip64, dest must be zip64 */ /* name uint32_t's */ /* id 1 (optional in zip64?) */ /* crc 1 */ /* comp_size 2 */ /* uncomp_size 2 */ if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pBuf, (sizeof(mz_uint32) * 6)) != (sizeof(mz_uint32) * 6)) { pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); } n = sizeof(mz_uint32) * ((MZ_READ_LE32(pBuf) == MZ_ZIP_DATA_DESCRIPTOR_ID) ? 6 : 5); } else { /* src is NOT zip64 */ mz_bool has_id; if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pBuf, sizeof(mz_uint32) * 4) != sizeof(mz_uint32) * 4) { pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED); } has_id = (MZ_READ_LE32(pBuf) == MZ_ZIP_DATA_DESCRIPTOR_ID); if (pZip->m_pState->m_zip64) { /* dest is zip64, so upgrade the data descriptor */ const mz_uint8 *pSrc_descriptor = (const mz_uint8 *)pBuf + (has_id ? sizeof(mz_uint32) : 0); const mz_uint32 src_crc32 = MZ_READ_LE32(pSrc_descriptor); const mz_uint64 src_comp_size = MZ_READ_LE32(pSrc_descriptor + sizeof(mz_uint32)); const mz_uint64 src_uncomp_size = MZ_READ_LE32(pSrc_descriptor + 2*sizeof(mz_uint32)); mz_write_le32((mz_uint8 *)pBuf, MZ_ZIP_DATA_DESCRIPTOR_ID); mz_write_le32((mz_uint8 *)pBuf + sizeof(mz_uint32) * 1, src_crc32); mz_write_le64((mz_uint8 *)pBuf + sizeof(mz_uint32) * 2, src_comp_size); mz_write_le64((mz_uint8 *)pBuf + sizeof(mz_uint32) * 4, src_uncomp_size); n = sizeof(mz_uint32) * 6; } else { /* dest is NOT zip64, just copy it as-is */ n = sizeof(mz_uint32) * (has_id ? 4 : 3); } } if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_dst_file_ofs, pBuf, n) != n) { pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); } cur_src_file_ofs += n; cur_dst_file_ofs += n; } pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf); /* Finally, add the new central dir header */ orig_central_dir_size = pState->m_central_dir.m_size; memcpy(new_central_header, pSrc_central_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE); if (pState->m_zip64) { /* This is the painful part: We need to write a new central dir header + ext block with updated zip64 fields, and ensure the old fields (if any) are not included. */ const mz_uint8 *pSrc_ext = pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_filename_len; mz_zip_array new_ext_block; mz_zip_array_init(&new_ext_block, sizeof(mz_uint8)); MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS, MZ_UINT32_MAX); MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS, MZ_UINT32_MAX); MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_LOCAL_HEADER_OFS, MZ_UINT32_MAX); if (!mz_zip_writer_update_zip64_extension_block(&new_ext_block, pZip, pSrc_ext, src_ext_len, &src_file_stat.m_comp_size, &src_file_stat.m_uncomp_size, &local_dir_header_ofs, NULL)) { mz_zip_array_clear(pZip, &new_ext_block); return MZ_FALSE; } MZ_WRITE_LE16(new_central_header + MZ_ZIP_CDH_EXTRA_LEN_OFS, new_ext_block.m_size); if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, new_central_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE)) { mz_zip_array_clear(pZip, &new_ext_block); return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, src_filename_len)) { mz_zip_array_clear(pZip, &new_ext_block); mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, new_ext_block.m_p, new_ext_block.m_size)) { mz_zip_array_clear(pZip, &new_ext_block); mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_filename_len + src_ext_len, src_comment_len)) { mz_zip_array_clear(pZip, &new_ext_block); mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } mz_zip_array_clear(pZip, &new_ext_block); } else { /* sanity checks */ if (cur_dst_file_ofs > MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); if (local_dir_header_ofs >= MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_LOCAL_HEADER_OFS, local_dir_header_ofs); if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, new_central_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE)) return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, src_central_dir_following_data_size)) { mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } } /* This shouldn't trigger unless we screwed up during the initial sanity checks */ if (pState->m_central_dir.m_size >= MZ_UINT32_MAX) { /* TODO: Support central dirs >= 32-bits in size */ mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE); } n = (mz_uint32)orig_central_dir_size; if (!mz_zip_array_push_back(pZip, &pState->m_central_dir_offsets, &n, 1)) { mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE); return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED); } pZip->m_total_files++; pZip->m_archive_size = cur_dst_file_ofs; return MZ_TRUE; } mz_bool mz_zip_writer_finalize_archive(mz_zip_archive *pZip) { mz_zip_internal_state *pState; mz_uint64 central_dir_ofs, central_dir_size; mz_uint8 hdr[256]; if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); pState = pZip->m_pState; if (pState->m_zip64) { if ((mz_uint64)pState->m_central_dir.m_size >= MZ_UINT32_MAX) return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); } else { if ((pZip->m_total_files > MZ_UINT16_MAX) || ((pZip->m_archive_size + pState->m_central_dir.m_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) > MZ_UINT32_MAX)) return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); } central_dir_ofs = 0; central_dir_size = 0; if (pZip->m_total_files) { /* Write central directory */ central_dir_ofs = pZip->m_archive_size; central_dir_size = pState->m_central_dir.m_size; pZip->m_central_directory_file_ofs = central_dir_ofs; if (pZip->m_pWrite(pZip->m_pIO_opaque, central_dir_ofs, pState->m_central_dir.m_p, (size_t)central_dir_size) != central_dir_size) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); pZip->m_archive_size += central_dir_size; } if (pState->m_zip64) { /* Write zip64 end of central directory header */ mz_uint64 rel_ofs_to_zip64_ecdr = pZip->m_archive_size; MZ_CLEAR_ARR(hdr); MZ_WRITE_LE32(hdr + MZ_ZIP64_ECDH_SIG_OFS, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIG); MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_SIZE_OF_RECORD_OFS, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE - sizeof(mz_uint32) - sizeof(mz_uint64)); MZ_WRITE_LE16(hdr + MZ_ZIP64_ECDH_VERSION_MADE_BY_OFS, 0x031E); /* TODO: always Unix */ MZ_WRITE_LE16(hdr + MZ_ZIP64_ECDH_VERSION_NEEDED_OFS, 0x002D); MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS, pZip->m_total_files); MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_TOTAL_ENTRIES_OFS, pZip->m_total_files); MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_SIZE_OFS, central_dir_size); MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_OFS_OFS, central_dir_ofs); if (pZip->m_pWrite(pZip->m_pIO_opaque, pZip->m_archive_size, hdr, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) != MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); pZip->m_archive_size += MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE; /* Write zip64 end of central directory locator */ MZ_CLEAR_ARR(hdr); MZ_WRITE_LE32(hdr + MZ_ZIP64_ECDL_SIG_OFS, MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG); MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDL_REL_OFS_TO_ZIP64_ECDR_OFS, rel_ofs_to_zip64_ecdr); MZ_WRITE_LE32(hdr + MZ_ZIP64_ECDL_TOTAL_NUMBER_OF_DISKS_OFS, 1); if (pZip->m_pWrite(pZip->m_pIO_opaque, pZip->m_archive_size, hdr, MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) != MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); pZip->m_archive_size += MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE; } /* Write end of central directory record */ MZ_CLEAR_ARR(hdr); MZ_WRITE_LE32(hdr + MZ_ZIP_ECDH_SIG_OFS, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG); MZ_WRITE_LE16(hdr + MZ_ZIP_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS, MZ_MIN(MZ_UINT16_MAX, pZip->m_total_files)); MZ_WRITE_LE16(hdr + MZ_ZIP_ECDH_CDIR_TOTAL_ENTRIES_OFS, MZ_MIN(MZ_UINT16_MAX, pZip->m_total_files)); MZ_WRITE_LE32(hdr + MZ_ZIP_ECDH_CDIR_SIZE_OFS, MZ_MIN(MZ_UINT32_MAX, central_dir_size)); MZ_WRITE_LE32(hdr + MZ_ZIP_ECDH_CDIR_OFS_OFS, MZ_MIN(MZ_UINT32_MAX, central_dir_ofs)); if (pZip->m_pWrite(pZip->m_pIO_opaque, pZip->m_archive_size, hdr, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) != MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED); #ifndef MINIZ_NO_STDIO if ((pState->m_pFile) && (MZ_FFLUSH(pState->m_pFile) == EOF)) return mz_zip_set_error(pZip, MZ_ZIP_FILE_CLOSE_FAILED); #endif /* #ifndef MINIZ_NO_STDIO */ pZip->m_archive_size += MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE; pZip->m_zip_mode = MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED; return MZ_TRUE; } mz_bool mz_zip_writer_finalize_heap_archive(mz_zip_archive *pZip, void **ppBuf, size_t *pSize) { if ((!ppBuf) || (!pSize)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); *ppBuf = NULL; *pSize = 0; if ((!pZip) || (!pZip->m_pState)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); if (pZip->m_pWrite != mz_zip_heap_write_func) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); if (!mz_zip_writer_finalize_archive(pZip)) return MZ_FALSE; *ppBuf = pZip->m_pState->m_pMem; *pSize = pZip->m_pState->m_mem_size; pZip->m_pState->m_pMem = NULL; pZip->m_pState->m_mem_size = pZip->m_pState->m_mem_capacity = 0; return MZ_TRUE; } mz_bool mz_zip_writer_end(mz_zip_archive *pZip) { return mz_zip_writer_end_internal(pZip, MZ_TRUE); } #ifndef MINIZ_NO_STDIO mz_bool mz_zip_add_mem_to_archive_file_in_place(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags) { return mz_zip_add_mem_to_archive_file_in_place_v2(pZip_filename, pArchive_name, pBuf, buf_size, pComment, comment_size, level_and_flags, NULL); } mz_bool mz_zip_add_mem_to_archive_file_in_place_v2(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, mz_zip_error *pErr) { mz_bool status, created_new_archive = MZ_FALSE; mz_zip_archive zip_archive; struct MZ_FILE_STAT_STRUCT file_stat; mz_zip_error actual_err = MZ_ZIP_NO_ERROR; mz_zip_zero_struct(&zip_archive); if ((int)level_and_flags < 0) level_and_flags = MZ_DEFAULT_LEVEL; if ((!pZip_filename) || (!pArchive_name) || ((buf_size) && (!pBuf)) || ((comment_size) && (!pComment)) || ((level_and_flags & 0xF) > MZ_UBER_COMPRESSION)) { if (pErr) *pErr = MZ_ZIP_INVALID_PARAMETER; return MZ_FALSE; } if (!mz_zip_writer_validate_archive_name(pArchive_name)) { if (pErr) *pErr = MZ_ZIP_INVALID_FILENAME; return MZ_FALSE; } /* Important: The regular non-64 bit version of stat() can fail here if the file is very large, which could cause the archive to be overwritten. */ /* So be sure to compile with _LARGEFILE64_SOURCE 1 */ if (MZ_FILE_STAT(pZip_filename, &file_stat) != 0) { /* Create a new archive. */ if (!mz_zip_writer_init_file_v2(&zip_archive, pZip_filename, 0, level_and_flags)) { if (pErr) *pErr = zip_archive.m_last_error; return MZ_FALSE; } created_new_archive = MZ_TRUE; } else { /* Append to an existing archive. */ if (!mz_zip_reader_init_file_v2(&zip_archive, pZip_filename, level_and_flags | MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY, 0, 0)) { if (pErr) *pErr = zip_archive.m_last_error; return MZ_FALSE; } if (!mz_zip_writer_init_from_reader_v2(&zip_archive, pZip_filename, level_and_flags)) { if (pErr) *pErr = zip_archive.m_last_error; mz_zip_reader_end_internal(&zip_archive, MZ_FALSE); return MZ_FALSE; } } status = mz_zip_writer_add_mem_ex(&zip_archive, pArchive_name, pBuf, buf_size, pComment, comment_size, level_and_flags, 0, 0); actual_err = zip_archive.m_last_error; /* Always finalize, even if adding failed for some reason, so we have a valid central directory. (This may not always succeed, but we can try.) */ if (!mz_zip_writer_finalize_archive(&zip_archive)) { if (!actual_err) actual_err = zip_archive.m_last_error; status = MZ_FALSE; } if (!mz_zip_writer_end_internal(&zip_archive, status)) { if (!actual_err) actual_err = zip_archive.m_last_error; status = MZ_FALSE; } if ((!status) && (created_new_archive)) { /* It's a new archive and something went wrong, so just delete it. */ int ignoredStatus = MZ_DELETE_FILE(pZip_filename); (void)ignoredStatus; } if (pErr) *pErr = actual_err; return status; } void *mz_zip_extract_archive_file_to_heap_v2(const char *pZip_filename, const char *pArchive_name, const char *pComment, size_t *pSize, mz_uint flags, mz_zip_error *pErr) { mz_uint32 file_index; mz_zip_archive zip_archive; void *p = NULL; if (pSize) *pSize = 0; if ((!pZip_filename) || (!pArchive_name)) { if (pErr) *pErr = MZ_ZIP_INVALID_PARAMETER; return NULL; } mz_zip_zero_struct(&zip_archive); if (!mz_zip_reader_init_file_v2(&zip_archive, pZip_filename, flags | MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY, 0, 0)) { if (pErr) *pErr = zip_archive.m_last_error; return NULL; } if (mz_zip_reader_locate_file_v2(&zip_archive, pArchive_name, pComment, flags, &file_index)) { p = mz_zip_reader_extract_to_heap(&zip_archive, file_index, pSize, flags); } mz_zip_reader_end_internal(&zip_archive, p != NULL); if (pErr) *pErr = zip_archive.m_last_error; return p; } void *mz_zip_extract_archive_file_to_heap(const char *pZip_filename, const char *pArchive_name, size_t *pSize, mz_uint flags) { return mz_zip_extract_archive_file_to_heap_v2(pZip_filename, pArchive_name, NULL, pSize, flags, NULL); } #endif /* #ifndef MINIZ_NO_STDIO */ #endif /* #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS */ /* ------------------- Misc utils */ mz_zip_mode mz_zip_get_mode(mz_zip_archive *pZip) { return pZip ? pZip->m_zip_mode : MZ_ZIP_MODE_INVALID; } mz_zip_type mz_zip_get_type(mz_zip_archive *pZip) { return pZip ? pZip->m_zip_type : MZ_ZIP_TYPE_INVALID; } mz_zip_error mz_zip_set_last_error(mz_zip_archive *pZip, mz_zip_error err_num) { mz_zip_error prev_err; if (!pZip) return MZ_ZIP_INVALID_PARAMETER; prev_err = pZip->m_last_error; pZip->m_last_error = err_num; return prev_err; } mz_zip_error mz_zip_peek_last_error(mz_zip_archive *pZip) { if (!pZip) return MZ_ZIP_INVALID_PARAMETER; return pZip->m_last_error; } mz_zip_error mz_zip_clear_last_error(mz_zip_archive *pZip) { return mz_zip_set_last_error(pZip, MZ_ZIP_NO_ERROR); } mz_zip_error mz_zip_get_last_error(mz_zip_archive *pZip) { mz_zip_error prev_err; if (!pZip) return MZ_ZIP_INVALID_PARAMETER; prev_err = pZip->m_last_error; pZip->m_last_error = MZ_ZIP_NO_ERROR; return prev_err; } const char *mz_zip_get_error_string(mz_zip_error mz_err) { switch (mz_err) { case MZ_ZIP_NO_ERROR: return "no error"; case MZ_ZIP_UNDEFINED_ERROR: return "undefined error"; case MZ_ZIP_TOO_MANY_FILES: return "too many files"; case MZ_ZIP_FILE_TOO_LARGE: return "file too large"; case MZ_ZIP_UNSUPPORTED_METHOD: return "unsupported method"; case MZ_ZIP_UNSUPPORTED_ENCRYPTION: return "unsupported encryption"; case MZ_ZIP_UNSUPPORTED_FEATURE: return "unsupported feature"; case MZ_ZIP_FAILED_FINDING_CENTRAL_DIR: return "failed finding central directory"; case MZ_ZIP_NOT_AN_ARCHIVE: return "not a ZIP archive"; case MZ_ZIP_INVALID_HEADER_OR_CORRUPTED: return "invalid header or archive is corrupted"; case MZ_ZIP_UNSUPPORTED_MULTIDISK: return "unsupported multidisk archive"; case MZ_ZIP_DECOMPRESSION_FAILED: return "decompression failed or archive is corrupted"; case MZ_ZIP_COMPRESSION_FAILED: return "compression failed"; case MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE: return "unexpected decompressed size"; case MZ_ZIP_CRC_CHECK_FAILED: return "CRC-32 check failed"; case MZ_ZIP_UNSUPPORTED_CDIR_SIZE: return "unsupported central directory size"; case MZ_ZIP_ALLOC_FAILED: return "allocation failed"; case MZ_ZIP_FILE_OPEN_FAILED: return "file open failed"; case MZ_ZIP_FILE_CREATE_FAILED: return "file create failed"; case MZ_ZIP_FILE_WRITE_FAILED: return "file write failed"; case MZ_ZIP_FILE_READ_FAILED: return "file read failed"; case MZ_ZIP_FILE_CLOSE_FAILED: return "file close failed"; case MZ_ZIP_FILE_SEEK_FAILED: return "file seek failed"; case MZ_ZIP_FILE_STAT_FAILED: return "file stat failed"; case MZ_ZIP_INVALID_PARAMETER: return "invalid parameter"; case MZ_ZIP_INVALID_FILENAME: return "invalid filename"; case MZ_ZIP_BUF_TOO_SMALL: return "buffer too small"; case MZ_ZIP_INTERNAL_ERROR: return "internal error"; case MZ_ZIP_FILE_NOT_FOUND: return "file not found"; case MZ_ZIP_ARCHIVE_TOO_LARGE: return "archive is too large"; case MZ_ZIP_VALIDATION_FAILED: return "validation failed"; case MZ_ZIP_WRITE_CALLBACK_FAILED: return "write callback failed"; case MZ_ZIP_TOTAL_ERRORS: return "total errors"; default: break; } return "unknown error"; } /* Note: Just because the archive is not zip64 doesn't necessarily mean it doesn't have Zip64 extended information extra field, argh. */ mz_bool mz_zip_is_zip64(mz_zip_archive *pZip) { if ((!pZip) || (!pZip->m_pState)) return MZ_FALSE; return pZip->m_pState->m_zip64; } size_t mz_zip_get_central_dir_size(mz_zip_archive *pZip) { if ((!pZip) || (!pZip->m_pState)) return 0; return pZip->m_pState->m_central_dir.m_size; } mz_uint mz_zip_reader_get_num_files(mz_zip_archive *pZip) { return pZip ? pZip->m_total_files : 0; } mz_uint64 mz_zip_get_archive_size(mz_zip_archive *pZip) { if (!pZip) return 0; return pZip->m_archive_size; } mz_uint64 mz_zip_get_archive_file_start_offset(mz_zip_archive *pZip) { if ((!pZip) || (!pZip->m_pState)) return 0; return pZip->m_pState->m_file_archive_start_ofs; } MZ_FILE *mz_zip_get_cfile(mz_zip_archive *pZip) { if ((!pZip) || (!pZip->m_pState)) return 0; return pZip->m_pState->m_pFile; } size_t mz_zip_read_archive_data(mz_zip_archive *pZip, mz_uint64 file_ofs, void *pBuf, size_t n) { if ((!pZip) || (!pZip->m_pState) || (!pBuf) || (!pZip->m_pRead)) return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); return pZip->m_pRead(pZip->m_pIO_opaque, file_ofs, pBuf, n); } mz_uint mz_zip_reader_get_filename(mz_zip_archive *pZip, mz_uint file_index, char *pFilename, mz_uint filename_buf_size) { mz_uint n; const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index); if (!p) { if (filename_buf_size) pFilename[0] = '\0'; mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER); return 0; } n = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS); if (filename_buf_size) { n = MZ_MIN(n, filename_buf_size - 1); memcpy(pFilename, p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, n); pFilename[n] = '\0'; } return n + 1; } mz_bool mz_zip_reader_file_stat(mz_zip_archive *pZip, mz_uint file_index, mz_zip_archive_file_stat *pStat) { return mz_zip_file_stat_internal(pZip, file_index, mz_zip_get_cdh(pZip, file_index), pStat, NULL); } mz_bool mz_zip_end(mz_zip_archive *pZip) { if (!pZip) return MZ_FALSE; if (pZip->m_zip_mode == MZ_ZIP_MODE_READING) return mz_zip_reader_end(pZip); #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS else if ((pZip->m_zip_mode == MZ_ZIP_MODE_WRITING) || (pZip->m_zip_mode == MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED)) return mz_zip_writer_end(pZip); #endif return MZ_FALSE; } #ifdef __cplusplus } #endif #endif /*#ifndef MINIZ_NO_ARCHIVE_APIS*/ pr0m1th3as-datatypes-9450b29/src/ods/miniz.h000066400000000000000000002127201527370255000205550ustar00rootroot00000000000000#ifndef MINIZ_EXPORT #define MINIZ_EXPORT #endif /* miniz.c 3.0.0 - public domain deflate/inflate, zlib-subset, ZIP reading/writing/appending, PNG writing See "unlicense" statement at the end of this file. Rich Geldreich , last updated Oct. 13, 2013 Implements RFC 1950: http://www.ietf.org/rfc/rfc1950.txt and RFC 1951: http://www.ietf.org/rfc/rfc1951.txt Most API's defined in miniz.c are optional. For example, to disable the archive related functions just define MINIZ_NO_ARCHIVE_APIS, or to get rid of all stdio usage define MINIZ_NO_STDIO (see the list below for more macros). * Low-level Deflate/Inflate implementation notes: Compression: Use the "tdefl" API's. The compressor supports raw, static, and dynamic blocks, lazy or greedy parsing, match length filtering, RLE-only, and Huffman-only streams. It performs and compresses approximately as well as zlib. Decompression: Use the "tinfl" API's. The entire decompressor is implemented as a single function coroutine: see tinfl_decompress(). It supports decompression into a 32KB (or larger power of 2) wrapping buffer, or into a memory block large enough to hold the entire file. The low-level tdefl/tinfl API's do not make any use of dynamic memory allocation. * zlib-style API notes: miniz.c implements a fairly large subset of zlib. There's enough functionality present for it to be a drop-in zlib replacement in many apps: The z_stream struct, optional memory allocation callbacks deflateInit/deflateInit2/deflate/deflateReset/deflateEnd/deflateBound inflateInit/inflateInit2/inflate/inflateReset/inflateEnd compress, compress2, compressBound, uncompress CRC-32, Adler-32 - Using modern, minimal code size, CPU cache friendly routines. Supports raw deflate streams or standard zlib streams with adler-32 checking. Limitations: The callback API's are not implemented yet. No support for gzip headers or zlib static dictionaries. I've tried to closely emulate zlib's various flavors of stream flushing and return status codes, but there are no guarantees that miniz.c pulls this off perfectly. * PNG writing: See the tdefl_write_image_to_png_file_in_memory() function, originally written by Alex Evans. Supports 1-4 bytes/pixel images. * ZIP archive API notes: The ZIP archive API's where designed with simplicity and efficiency in mind, with just enough abstraction to get the job done with minimal fuss. There are simple API's to retrieve file information, read files from existing archives, create new archives, append new files to existing archives, or clone archive data from one archive to another. It supports archives located in memory or the heap, on disk (using stdio.h), or you can specify custom file read/write callbacks. - Archive reading: Just call this function to read a single file from a disk archive: void *mz_zip_extract_archive_file_to_heap(const char *pZip_filename, const char *pArchive_name, size_t *pSize, mz_uint zip_flags); For more complex cases, use the "mz_zip_reader" functions. Upon opening an archive, the entire central directory is located and read as-is into memory, and subsequent file access only occurs when reading individual files. - Archives file scanning: The simple way is to use this function to scan a loaded archive for a specific file: int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags); The locate operation can optionally check file comments too, which (as one example) can be used to identify multiple versions of the same file in an archive. This function uses a simple linear search through the central directory, so it's not very fast. Alternately, you can iterate through all the files in an archive (using mz_zip_reader_get_num_files()) and retrieve detailed info on each file by calling mz_zip_reader_file_stat(). - Archive creation: Use the "mz_zip_writer" functions. The ZIP writer immediately writes compressed file data to disk and builds an exact image of the central directory in memory. The central directory image is written all at once at the end of the archive file when the archive is finalized. The archive writer can optionally align each file's local header and file data to any power of 2 alignment, which can be useful when the archive will be read from optical media. Also, the writer supports placing arbitrary data blobs at the very beginning of ZIP archives. Archives written using either feature are still readable by any ZIP tool. - Archive appending: The simple way to add a single file to an archive is to call this function: mz_bool mz_zip_add_mem_to_archive_file_in_place(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags); The archive will be created if it doesn't already exist, otherwise it'll be appended to. Note the appending is done in-place and is not an atomic operation, so if something goes wrong during the operation it's possible the archive could be left without a central directory (although the local file headers and file data will be fine, so the archive will be recoverable). For more complex archive modification scenarios: 1. The safest way is to use a mz_zip_reader to read the existing archive, cloning only those bits you want to preserve into a new archive using using the mz_zip_writer_add_from_zip_reader() function (which compiles the compressed file data as-is). When you're done, delete the old archive and rename the newly written archive, and you're done. This is safe but requires a bunch of temporary disk space or heap memory. 2. Or, you can convert an mz_zip_reader in-place to an mz_zip_writer using mz_zip_writer_init_from_reader(), append new files as needed, then finalize the archive which will write an updated central directory to the original archive. (This is basically what mz_zip_add_mem_to_archive_file_in_place() does.) There's a possibility that the archive's central directory could be lost with this method if anything goes wrong, though. - ZIP archive support limitations: No spanning support. Extraction functions can only handle unencrypted, stored or deflated files. Requires streams capable of seeking. * This is a header file library, like stb_image.c. To get only a header file, either cut and paste the below header, or create miniz.h, #define MINIZ_HEADER_FILE_ONLY, and then include miniz.c from it. * Important: For best perf. be sure to customize the below macros for your target platform: #define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 1 #define MINIZ_LITTLE_ENDIAN 1 #define MINIZ_HAS_64BIT_REGISTERS 1 * On platforms using glibc, Be sure to "#define _LARGEFILE64_SOURCE 1" before including miniz.c to ensure miniz uses the 64-bit variants: fopen64(), stat64(), etc. Otherwise you won't be able to process large files (i.e. 32-bit stat() fails for me on files > 0x7FFFFFFF bytes). */ #pragma once /* Defines to completely disable specific portions of miniz.c: If all macros here are defined the only functionality remaining will be CRC-32 and adler-32. */ /* Define MINIZ_NO_STDIO to disable all usage and any functions which rely on stdio for file I/O. */ /*#define MINIZ_NO_STDIO */ /* If MINIZ_NO_TIME is specified then the ZIP archive functions will not be able to get the current time, or */ /* get/set file times, and the C run-time funcs that get/set times won't be called. */ /* The current downside is the times written to your archives will be from 1979. */ /*#define MINIZ_NO_TIME */ /* Define MINIZ_NO_DEFLATE_APIS to disable all compression API's. */ /*#define MINIZ_NO_DEFLATE_APIS */ /* Define MINIZ_NO_INFLATE_APIS to disable all decompression API's. */ /*#define MINIZ_NO_INFLATE_APIS */ /* Define MINIZ_NO_ARCHIVE_APIS to disable all ZIP archive API's. */ /*#define MINIZ_NO_ARCHIVE_APIS */ /* Define MINIZ_NO_ARCHIVE_WRITING_APIS to disable all writing related ZIP archive API's. */ /*#define MINIZ_NO_ARCHIVE_WRITING_APIS */ /* Define MINIZ_NO_ZLIB_APIS to remove all ZLIB-style compression/decompression API's. */ /*#define MINIZ_NO_ZLIB_APIS */ /* Define MINIZ_NO_ZLIB_COMPATIBLE_NAME to disable zlib names, to prevent conflicts against stock zlib. */ /*#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES */ /* Define MINIZ_NO_MALLOC to disable all calls to malloc, free, and realloc. Note if MINIZ_NO_MALLOC is defined then the user must always provide custom user alloc/free/realloc callbacks to the zlib and archive API's, and a few stand-alone helper API's which don't provide custom user functions (such as tdefl_compress_mem_to_heap() and tinfl_decompress_mem_to_heap()) won't work. */ /*#define MINIZ_NO_MALLOC */ #ifdef MINIZ_NO_INFLATE_APIS #define MINIZ_NO_ARCHIVE_APIS #endif #ifdef MINIZ_NO_DEFLATE_APIS #define MINIZ_NO_ARCHIVE_WRITING_APIS #endif #if defined(__TINYC__) && (defined(__linux) || defined(__linux__)) /* TODO: Work around "error: include file 'sys\utime.h' when compiling with tcc on Linux */ #define MINIZ_NO_TIME #endif #include #if !defined(MINIZ_NO_TIME) && !defined(MINIZ_NO_ARCHIVE_APIS) #include #endif #if defined(_M_IX86) || defined(_M_X64) || defined(__i386__) || defined(__i386) || defined(__i486__) || defined(__i486) || defined(i386) || defined(__ia64__) || defined(__x86_64__) /* MINIZ_X86_OR_X64_CPU is only used to help set the below macros. */ #define MINIZ_X86_OR_X64_CPU 1 #else #define MINIZ_X86_OR_X64_CPU 0 #endif /* Set MINIZ_LITTLE_ENDIAN only if not set */ #if !defined(MINIZ_LITTLE_ENDIAN) #if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) #if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) /* Set MINIZ_LITTLE_ENDIAN to 1 if the processor is little endian. */ #define MINIZ_LITTLE_ENDIAN 1 #else #define MINIZ_LITTLE_ENDIAN 0 #endif #else #if MINIZ_X86_OR_X64_CPU #define MINIZ_LITTLE_ENDIAN 1 #else #define MINIZ_LITTLE_ENDIAN 0 #endif #endif #endif /* Using unaligned loads and stores causes errors when using UBSan */ #if defined(__has_feature) #if __has_feature(undefined_behavior_sanitizer) #define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 0 #endif #endif /* Set MINIZ_USE_UNALIGNED_LOADS_AND_STORES only if not set */ #if !defined(MINIZ_USE_UNALIGNED_LOADS_AND_STORES) #if MINIZ_X86_OR_X64_CPU /* Set MINIZ_USE_UNALIGNED_LOADS_AND_STORES to 1 on CPU's that permit efficient integer loads and stores from unaligned addresses. */ #define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 0 #define MINIZ_UNALIGNED_USE_MEMCPY #else #define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 0 #endif #endif #if defined(_M_X64) || defined(_WIN64) || defined(__MINGW64__) || defined(_LP64) || defined(__LP64__) || defined(__ia64__) || defined(__x86_64__) /* Set MINIZ_HAS_64BIT_REGISTERS to 1 if operations on 64-bit integers are reasonably fast (and don't involve compiler generated calls to helper functions). */ #define MINIZ_HAS_64BIT_REGISTERS 1 #else #define MINIZ_HAS_64BIT_REGISTERS 0 #endif #ifdef __cplusplus extern "C" { #endif /* ------------------- zlib-style API Definitions. */ /* For more compatibility with zlib, miniz.c uses unsigned long for some parameters/struct members. Beware: mz_ulong can be either 32 or 64-bits! */ typedef unsigned long mz_ulong; /* mz_free() internally uses the MZ_FREE() macro (which by default calls free() unless you've modified the MZ_MALLOC macro) to release a block allocated from the heap. */ MINIZ_EXPORT void mz_free(void *p); #define MZ_ADLER32_INIT (1) /* mz_adler32() returns the initial adler-32 value to use when called with ptr==NULL. */ MINIZ_EXPORT mz_ulong mz_adler32(mz_ulong adler, const unsigned char *ptr, size_t buf_len); #define MZ_CRC32_INIT (0) /* mz_crc32() returns the initial CRC-32 value to use when called with ptr==NULL. */ MINIZ_EXPORT mz_ulong mz_crc32(mz_ulong crc, const unsigned char *ptr, size_t buf_len); /* Compression strategies. */ enum { MZ_DEFAULT_STRATEGY = 0, MZ_FILTERED = 1, MZ_HUFFMAN_ONLY = 2, MZ_RLE = 3, MZ_FIXED = 4 }; /* Method */ #define MZ_DEFLATED 8 /* Heap allocation callbacks. Note that mz_alloc_func parameter types purposely differ from zlib's: items/size is size_t, not unsigned long. */ typedef void *(*mz_alloc_func)(void *opaque, size_t items, size_t size); typedef void (*mz_free_func)(void *opaque, void *address); typedef void *(*mz_realloc_func)(void *opaque, void *address, size_t items, size_t size); /* Compression levels: 0-9 are the standard zlib-style levels, 10 is best possible compression (not zlib compatible, and may be very slow), MZ_DEFAULT_COMPRESSION=MZ_DEFAULT_LEVEL. */ enum { MZ_NO_COMPRESSION = 0, MZ_BEST_SPEED = 1, MZ_BEST_COMPRESSION = 9, MZ_UBER_COMPRESSION = 10, MZ_DEFAULT_LEVEL = 6, MZ_DEFAULT_COMPRESSION = -1 }; #define MZ_VERSION "11.0.2" #define MZ_VERNUM 0xB002 #define MZ_VER_MAJOR 11 #define MZ_VER_MINOR 2 #define MZ_VER_REVISION 0 #define MZ_VER_SUBREVISION 0 #ifndef MINIZ_NO_ZLIB_APIS /* Flush values. For typical usage you only need MZ_NO_FLUSH and MZ_FINISH. The other values are for advanced use (refer to the zlib docs). */ enum { MZ_NO_FLUSH = 0, MZ_PARTIAL_FLUSH = 1, MZ_SYNC_FLUSH = 2, MZ_FULL_FLUSH = 3, MZ_FINISH = 4, MZ_BLOCK = 5 }; /* Return status codes. MZ_PARAM_ERROR is non-standard. */ enum { MZ_OK = 0, MZ_STREAM_END = 1, MZ_NEED_DICT = 2, MZ_ERRNO = -1, MZ_STREAM_ERROR = -2, MZ_DATA_ERROR = -3, MZ_MEM_ERROR = -4, MZ_BUF_ERROR = -5, MZ_VERSION_ERROR = -6, MZ_PARAM_ERROR = -10000 }; /* Window bits */ #define MZ_DEFAULT_WINDOW_BITS 15 struct mz_internal_state; /* Compression/decompression stream struct. */ typedef struct mz_stream_s { const unsigned char *next_in; /* pointer to next byte to read */ unsigned int avail_in; /* number of bytes available at next_in */ mz_ulong total_in; /* total number of bytes consumed so far */ unsigned char *next_out; /* pointer to next byte to write */ unsigned int avail_out; /* number of bytes that can be written to next_out */ mz_ulong total_out; /* total number of bytes produced so far */ char *msg; /* error msg (unused) */ struct mz_internal_state *state; /* internal state, allocated by zalloc/zfree */ mz_alloc_func zalloc; /* optional heap allocation function (defaults to malloc) */ mz_free_func zfree; /* optional heap free function (defaults to free) */ void *opaque; /* heap alloc function user pointer */ int data_type; /* data_type (unused) */ mz_ulong adler; /* adler32 of the source or uncompressed data */ mz_ulong reserved; /* not used */ } mz_stream; typedef mz_stream *mz_streamp; /* Returns the version string of miniz.c. */ MINIZ_EXPORT const char *mz_version(void); #ifndef MINIZ_NO_DEFLATE_APIS /* mz_deflateInit() initializes a compressor with default options: */ /* Parameters: */ /* pStream must point to an initialized mz_stream struct. */ /* level must be between [MZ_NO_COMPRESSION, MZ_BEST_COMPRESSION]. */ /* level 1 enables a specially optimized compression function that's been optimized purely for performance, not ratio. */ /* (This special func. is currently only enabled when MINIZ_USE_UNALIGNED_LOADS_AND_STORES and MINIZ_LITTLE_ENDIAN are defined.) */ /* Return values: */ /* MZ_OK on success. */ /* MZ_STREAM_ERROR if the stream is bogus. */ /* MZ_PARAM_ERROR if the input parameters are bogus. */ /* MZ_MEM_ERROR on out of memory. */ MINIZ_EXPORT int mz_deflateInit(mz_streamp pStream, int level); /* mz_deflateInit2() is like mz_deflate(), except with more control: */ /* Additional parameters: */ /* method must be MZ_DEFLATED */ /* window_bits must be MZ_DEFAULT_WINDOW_BITS (to wrap the deflate stream with zlib header/adler-32 footer) or -MZ_DEFAULT_WINDOW_BITS (raw deflate/no header or footer) */ /* mem_level must be between [1, 9] (it's checked but ignored by miniz.c) */ MINIZ_EXPORT int mz_deflateInit2(mz_streamp pStream, int level, int method, int window_bits, int mem_level, int strategy); /* Quickly resets a compressor without having to reallocate anything. Same as calling mz_deflateEnd() followed by mz_deflateInit()/mz_deflateInit2(). */ MINIZ_EXPORT int mz_deflateReset(mz_streamp pStream); /* mz_deflate() compresses the input to output, consuming as much of the input and producing as much output as possible. */ /* Parameters: */ /* pStream is the stream to read from and write to. You must initialize/update the next_in, avail_in, next_out, and avail_out members. */ /* flush may be MZ_NO_FLUSH, MZ_PARTIAL_FLUSH/MZ_SYNC_FLUSH, MZ_FULL_FLUSH, or MZ_FINISH. */ /* Return values: */ /* MZ_OK on success (when flushing, or if more input is needed but not available, and/or there's more output to be written but the output buffer is full). */ /* MZ_STREAM_END if all input has been consumed and all output bytes have been written. Don't call mz_deflate() on the stream anymore. */ /* MZ_STREAM_ERROR if the stream is bogus. */ /* MZ_PARAM_ERROR if one of the parameters is invalid. */ /* MZ_BUF_ERROR if no forward progress is possible because the input and/or output buffers are empty. (Fill up the input buffer or free up some output space and try again.) */ MINIZ_EXPORT int mz_deflate(mz_streamp pStream, int flush); /* mz_deflateEnd() deinitializes a compressor: */ /* Return values: */ /* MZ_OK on success. */ /* MZ_STREAM_ERROR if the stream is bogus. */ MINIZ_EXPORT int mz_deflateEnd(mz_streamp pStream); /* mz_deflateBound() returns a (very) conservative upper bound on the amount of data that could be generated by deflate(), assuming flush is set to only MZ_NO_FLUSH or MZ_FINISH. */ MINIZ_EXPORT mz_ulong mz_deflateBound(mz_streamp pStream, mz_ulong source_len); /* Single-call compression functions mz_compress() and mz_compress2(): */ /* Returns MZ_OK on success, or one of the error codes from mz_deflate() on failure. */ MINIZ_EXPORT int mz_compress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len); MINIZ_EXPORT int mz_compress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len, int level); /* mz_compressBound() returns a (very) conservative upper bound on the amount of data that could be generated by calling mz_compress(). */ MINIZ_EXPORT mz_ulong mz_compressBound(mz_ulong source_len); #endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/ #ifndef MINIZ_NO_INFLATE_APIS /* Initializes a decompressor. */ MINIZ_EXPORT int mz_inflateInit(mz_streamp pStream); /* mz_inflateInit2() is like mz_inflateInit() with an additional option that controls the window size and whether or not the stream has been wrapped with a zlib header/footer: */ /* window_bits must be MZ_DEFAULT_WINDOW_BITS (to parse zlib header/footer) or -MZ_DEFAULT_WINDOW_BITS (raw deflate). */ MINIZ_EXPORT int mz_inflateInit2(mz_streamp pStream, int window_bits); /* Quickly resets a compressor without having to reallocate anything. Same as calling mz_inflateEnd() followed by mz_inflateInit()/mz_inflateInit2(). */ MINIZ_EXPORT int mz_inflateReset(mz_streamp pStream); /* Decompresses the input stream to the output, consuming only as much of the input as needed, and writing as much to the output as possible. */ /* Parameters: */ /* pStream is the stream to read from and write to. You must initialize/update the next_in, avail_in, next_out, and avail_out members. */ /* flush may be MZ_NO_FLUSH, MZ_SYNC_FLUSH, or MZ_FINISH. */ /* On the first call, if flush is MZ_FINISH it's assumed the input and output buffers are both sized large enough to decompress the entire stream in a single call (this is slightly faster). */ /* MZ_FINISH implies that there are no more source bytes available beside what's already in the input buffer, and that the output buffer is large enough to hold the rest of the decompressed data. */ /* Return values: */ /* MZ_OK on success. Either more input is needed but not available, and/or there's more output to be written but the output buffer is full. */ /* MZ_STREAM_END if all needed input has been consumed and all output bytes have been written. For zlib streams, the adler-32 of the decompressed data has also been verified. */ /* MZ_STREAM_ERROR if the stream is bogus. */ /* MZ_DATA_ERROR if the deflate stream is invalid. */ /* MZ_PARAM_ERROR if one of the parameters is invalid. */ /* MZ_BUF_ERROR if no forward progress is possible because the input buffer is empty but the inflater needs more input to continue, or if the output buffer is not large enough. Call mz_inflate() again */ /* with more input data, or with more room in the output buffer (except when using single call decompression, described above). */ MINIZ_EXPORT int mz_inflate(mz_streamp pStream, int flush); /* Deinitializes a decompressor. */ MINIZ_EXPORT int mz_inflateEnd(mz_streamp pStream); /* Single-call decompression. */ /* Returns MZ_OK on success, or one of the error codes from mz_inflate() on failure. */ MINIZ_EXPORT int mz_uncompress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len); MINIZ_EXPORT int mz_uncompress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong *pSource_len); #endif /*#ifndef MINIZ_NO_INFLATE_APIS*/ /* Returns a string description of the specified error code, or NULL if the error code is invalid. */ MINIZ_EXPORT const char *mz_error(int err); /* Redefine zlib-compatible names to miniz equivalents, so miniz.c can be used as a drop-in replacement for the subset of zlib that miniz.c supports. */ /* Define MINIZ_NO_ZLIB_COMPATIBLE_NAMES to disable zlib-compatibility if you use zlib in the same project. */ #ifndef MINIZ_NO_ZLIB_COMPATIBLE_NAMES typedef unsigned char Byte; typedef unsigned int uInt; typedef mz_ulong uLong; typedef Byte Bytef; typedef uInt uIntf; typedef char charf; typedef int intf; typedef void *voidpf; typedef uLong uLongf; typedef void *voidp; typedef void *const voidpc; #define Z_NULL 0 #define Z_NO_FLUSH MZ_NO_FLUSH #define Z_PARTIAL_FLUSH MZ_PARTIAL_FLUSH #define Z_SYNC_FLUSH MZ_SYNC_FLUSH #define Z_FULL_FLUSH MZ_FULL_FLUSH #define Z_FINISH MZ_FINISH #define Z_BLOCK MZ_BLOCK #define Z_OK MZ_OK #define Z_STREAM_END MZ_STREAM_END #define Z_NEED_DICT MZ_NEED_DICT #define Z_ERRNO MZ_ERRNO #define Z_STREAM_ERROR MZ_STREAM_ERROR #define Z_DATA_ERROR MZ_DATA_ERROR #define Z_MEM_ERROR MZ_MEM_ERROR #define Z_BUF_ERROR MZ_BUF_ERROR #define Z_VERSION_ERROR MZ_VERSION_ERROR #define Z_PARAM_ERROR MZ_PARAM_ERROR #define Z_NO_COMPRESSION MZ_NO_COMPRESSION #define Z_BEST_SPEED MZ_BEST_SPEED #define Z_BEST_COMPRESSION MZ_BEST_COMPRESSION #define Z_DEFAULT_COMPRESSION MZ_DEFAULT_COMPRESSION #define Z_DEFAULT_STRATEGY MZ_DEFAULT_STRATEGY #define Z_FILTERED MZ_FILTERED #define Z_HUFFMAN_ONLY MZ_HUFFMAN_ONLY #define Z_RLE MZ_RLE #define Z_FIXED MZ_FIXED #define Z_DEFLATED MZ_DEFLATED #define Z_DEFAULT_WINDOW_BITS MZ_DEFAULT_WINDOW_BITS #define alloc_func mz_alloc_func #define free_func mz_free_func #define internal_state mz_internal_state #define z_stream mz_stream #ifndef MINIZ_NO_DEFLATE_APIS #define deflateInit mz_deflateInit #define deflateInit2 mz_deflateInit2 #define deflateReset mz_deflateReset #define deflate mz_deflate #define deflateEnd mz_deflateEnd #define deflateBound mz_deflateBound #define compress mz_compress #define compress2 mz_compress2 #define compressBound mz_compressBound #endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/ #ifndef MINIZ_NO_INFLATE_APIS #define inflateInit mz_inflateInit #define inflateInit2 mz_inflateInit2 #define inflateReset mz_inflateReset #define inflate mz_inflate #define inflateEnd mz_inflateEnd #define uncompress mz_uncompress #define uncompress2 mz_uncompress2 #endif /*#ifndef MINIZ_NO_INFLATE_APIS*/ #define crc32 mz_crc32 #define adler32 mz_adler32 #define MAX_WBITS 15 #define MAX_MEM_LEVEL 9 #define zError mz_error #define ZLIB_VERSION MZ_VERSION #define ZLIB_VERNUM MZ_VERNUM #define ZLIB_VER_MAJOR MZ_VER_MAJOR #define ZLIB_VER_MINOR MZ_VER_MINOR #define ZLIB_VER_REVISION MZ_VER_REVISION #define ZLIB_VER_SUBREVISION MZ_VER_SUBREVISION #define zlibVersion mz_version #define zlib_version mz_version() #endif /* #ifndef MINIZ_NO_ZLIB_COMPATIBLE_NAMES */ #endif /* MINIZ_NO_ZLIB_APIS */ #ifdef __cplusplus } #endif #pragma once #include #include #include #include /* ------------------- Types and macros */ typedef unsigned char mz_uint8; typedef signed short mz_int16; typedef unsigned short mz_uint16; typedef unsigned int mz_uint32; typedef unsigned int mz_uint; typedef int64_t mz_int64; typedef uint64_t mz_uint64; typedef int mz_bool; #define MZ_FALSE (0) #define MZ_TRUE (1) /* Works around MSVC's spammy "warning C4127: conditional expression is constant" message. */ #ifdef _MSC_VER #define MZ_MACRO_END while (0, 0) #else #define MZ_MACRO_END while (0) #endif #ifdef MINIZ_NO_STDIO #define MZ_FILE void * #else #include #define MZ_FILE FILE #endif /* #ifdef MINIZ_NO_STDIO */ #ifdef MINIZ_NO_TIME typedef struct mz_dummy_time_t_tag { mz_uint32 m_dummy1; mz_uint32 m_dummy2; } mz_dummy_time_t; #define MZ_TIME_T mz_dummy_time_t #else #define MZ_TIME_T time_t #endif #define MZ_ASSERT(x) assert(x) #ifdef MINIZ_NO_MALLOC #define MZ_MALLOC(x) NULL #define MZ_FREE(x) (void)x, ((void)0) #define MZ_REALLOC(p, x) NULL #else #define MZ_MALLOC(x) malloc(x) #define MZ_FREE(x) free(x) #define MZ_REALLOC(p, x) realloc(p, x) #endif #define MZ_MAX(a, b) (((a) > (b)) ? (a) : (b)) #define MZ_MIN(a, b) (((a) < (b)) ? (a) : (b)) #define MZ_CLEAR_OBJ(obj) memset(&(obj), 0, sizeof(obj)) #define MZ_CLEAR_ARR(obj) memset((obj), 0, sizeof(obj)) #define MZ_CLEAR_PTR(obj) memset((obj), 0, sizeof(*obj)) #if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN #define MZ_READ_LE16(p) *((const mz_uint16 *)(p)) #define MZ_READ_LE32(p) *((const mz_uint32 *)(p)) #else #define MZ_READ_LE16(p) ((mz_uint32)(((const mz_uint8 *)(p))[0]) | ((mz_uint32)(((const mz_uint8 *)(p))[1]) << 8U)) #define MZ_READ_LE32(p) ((mz_uint32)(((const mz_uint8 *)(p))[0]) | ((mz_uint32)(((const mz_uint8 *)(p))[1]) << 8U) | ((mz_uint32)(((const mz_uint8 *)(p))[2]) << 16U) | ((mz_uint32)(((const mz_uint8 *)(p))[3]) << 24U)) #endif #define MZ_READ_LE64(p) (((mz_uint64)MZ_READ_LE32(p)) | (((mz_uint64)MZ_READ_LE32((const mz_uint8 *)(p) + sizeof(mz_uint32))) << 32U)) #ifdef _MSC_VER #define MZ_FORCEINLINE __forceinline #elif defined(__GNUC__) #define MZ_FORCEINLINE __inline__ __attribute__((__always_inline__)) #else #define MZ_FORCEINLINE inline #endif #ifdef __cplusplus extern "C" { #endif extern MINIZ_EXPORT void *miniz_def_alloc_func(void *opaque, size_t items, size_t size); extern MINIZ_EXPORT void miniz_def_free_func(void *opaque, void *address); extern MINIZ_EXPORT void *miniz_def_realloc_func(void *opaque, void *address, size_t items, size_t size); #define MZ_UINT16_MAX (0xFFFFU) #define MZ_UINT32_MAX (0xFFFFFFFFU) #ifdef __cplusplus } #endif #pragma once #ifndef MINIZ_NO_DEFLATE_APIS #ifdef __cplusplus extern "C" { #endif /* ------------------- Low-level Compression API Definitions */ /* Set TDEFL_LESS_MEMORY to 1 to use less memory (compression will be slightly slower, and raw/dynamic blocks will be output more frequently). */ #define TDEFL_LESS_MEMORY 0 /* tdefl_init() compression flags logically OR'd together (low 12 bits contain the max. number of probes per dictionary search): */ /* TDEFL_DEFAULT_MAX_PROBES: The compressor defaults to 128 dictionary probes per dictionary search. 0=Huffman only, 1=Huffman+LZ (fastest/crap compression), 4095=Huffman+LZ (slowest/best compression). */ enum { TDEFL_HUFFMAN_ONLY = 0, TDEFL_DEFAULT_MAX_PROBES = 128, TDEFL_MAX_PROBES_MASK = 0xFFF }; /* TDEFL_WRITE_ZLIB_HEADER: If set, the compressor outputs a zlib header before the deflate data, and the Adler-32 of the source data at the end. Otherwise, you'll get raw deflate data. */ /* TDEFL_COMPUTE_ADLER32: Always compute the adler-32 of the input data (even when not writing zlib headers). */ /* TDEFL_GREEDY_PARSING_FLAG: Set to use faster greedy parsing, instead of more efficient lazy parsing. */ /* TDEFL_NONDETERMINISTIC_PARSING_FLAG: Enable to decrease the compressor's initialization time to the minimum, but the output may vary from run to run given the same input (depending on the contents of memory). */ /* TDEFL_RLE_MATCHES: Only look for RLE matches (matches with a distance of 1) */ /* TDEFL_FILTER_MATCHES: Discards matches <= 5 chars if enabled. */ /* TDEFL_FORCE_ALL_STATIC_BLOCKS: Disable usage of optimized Huffman tables. */ /* TDEFL_FORCE_ALL_RAW_BLOCKS: Only use raw (uncompressed) deflate blocks. */ /* The low 12 bits are reserved to control the max # of hash probes per dictionary lookup (see TDEFL_MAX_PROBES_MASK). */ enum { TDEFL_WRITE_ZLIB_HEADER = 0x01000, TDEFL_COMPUTE_ADLER32 = 0x02000, TDEFL_GREEDY_PARSING_FLAG = 0x04000, TDEFL_NONDETERMINISTIC_PARSING_FLAG = 0x08000, TDEFL_RLE_MATCHES = 0x10000, TDEFL_FILTER_MATCHES = 0x20000, TDEFL_FORCE_ALL_STATIC_BLOCKS = 0x40000, TDEFL_FORCE_ALL_RAW_BLOCKS = 0x80000 }; /* High level compression functions: */ /* tdefl_compress_mem_to_heap() compresses a block in memory to a heap block allocated via malloc(). */ /* On entry: */ /* pSrc_buf, src_buf_len: Pointer and size of source block to compress. */ /* flags: The max match finder probes (default is 128) logically OR'd against the above flags. Higher probes are slower but improve compression. */ /* On return: */ /* Function returns a pointer to the compressed data, or NULL on failure. */ /* *pOut_len will be set to the compressed data's size, which could be larger than src_buf_len on uncompressible data. */ /* The caller must free() the returned block when it's no longer needed. */ MINIZ_EXPORT void *tdefl_compress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags); /* tdefl_compress_mem_to_mem() compresses a block in memory to another block in memory. */ /* Returns 0 on failure. */ MINIZ_EXPORT size_t tdefl_compress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags); /* Compresses an image to a compressed PNG file in memory. */ /* On entry: */ /* pImage, w, h, and num_chans describe the image to compress. num_chans may be 1, 2, 3, or 4. */ /* The image pitch in bytes per scanline will be w*num_chans. The leftmost pixel on the top scanline is stored first in memory. */ /* level may range from [0,10], use MZ_NO_COMPRESSION, MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc. or a decent default is MZ_DEFAULT_LEVEL */ /* If flip is true, the image will be flipped on the Y axis (useful for OpenGL apps). */ /* On return: */ /* Function returns a pointer to the compressed data, or NULL on failure. */ /* *pLen_out will be set to the size of the PNG image file. */ /* The caller must mz_free() the returned heap block (which will typically be larger than *pLen_out) when it's no longer needed. */ MINIZ_EXPORT void *tdefl_write_image_to_png_file_in_memory_ex(const void *pImage, int w, int h, int num_chans, size_t *pLen_out, mz_uint level, mz_bool flip); MINIZ_EXPORT void *tdefl_write_image_to_png_file_in_memory(const void *pImage, int w, int h, int num_chans, size_t *pLen_out); /* Output stream interface. The compressor uses this interface to write compressed data. It'll typically be called TDEFL_OUT_BUF_SIZE at a time. */ typedef mz_bool (*tdefl_put_buf_func_ptr)(const void *pBuf, int len, void *pUser); /* tdefl_compress_mem_to_output() compresses a block to an output stream. The above helpers use this function internally. */ MINIZ_EXPORT mz_bool tdefl_compress_mem_to_output(const void *pBuf, size_t buf_len, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags); enum { TDEFL_MAX_HUFF_TABLES = 3, TDEFL_MAX_HUFF_SYMBOLS_0 = 288, TDEFL_MAX_HUFF_SYMBOLS_1 = 32, TDEFL_MAX_HUFF_SYMBOLS_2 = 19, TDEFL_LZ_DICT_SIZE = 32768, TDEFL_LZ_DICT_SIZE_MASK = TDEFL_LZ_DICT_SIZE - 1, TDEFL_MIN_MATCH_LEN = 3, TDEFL_MAX_MATCH_LEN = 258 }; /* TDEFL_OUT_BUF_SIZE MUST be large enough to hold a single entire compressed output block (using static/fixed Huffman codes). */ #if TDEFL_LESS_MEMORY enum { TDEFL_LZ_CODE_BUF_SIZE = 24 * 1024, TDEFL_OUT_BUF_SIZE = (TDEFL_LZ_CODE_BUF_SIZE * 13) / 10, TDEFL_MAX_HUFF_SYMBOLS = 288, TDEFL_LZ_HASH_BITS = 12, TDEFL_LEVEL1_HASH_SIZE_MASK = 4095, TDEFL_LZ_HASH_SHIFT = (TDEFL_LZ_HASH_BITS + 2) / 3, TDEFL_LZ_HASH_SIZE = 1 << TDEFL_LZ_HASH_BITS }; #else enum { TDEFL_LZ_CODE_BUF_SIZE = 64 * 1024, TDEFL_OUT_BUF_SIZE = (TDEFL_LZ_CODE_BUF_SIZE * 13) / 10, TDEFL_MAX_HUFF_SYMBOLS = 288, TDEFL_LZ_HASH_BITS = 15, TDEFL_LEVEL1_HASH_SIZE_MASK = 4095, TDEFL_LZ_HASH_SHIFT = (TDEFL_LZ_HASH_BITS + 2) / 3, TDEFL_LZ_HASH_SIZE = 1 << TDEFL_LZ_HASH_BITS }; #endif /* The low-level tdefl functions below may be used directly if the above helper functions aren't flexible enough. The low-level functions don't make any heap allocations, unlike the above helper functions. */ typedef enum { TDEFL_STATUS_BAD_PARAM = -2, TDEFL_STATUS_PUT_BUF_FAILED = -1, TDEFL_STATUS_OKAY = 0, TDEFL_STATUS_DONE = 1 } tdefl_status; /* Must map to MZ_NO_FLUSH, MZ_SYNC_FLUSH, etc. enums */ typedef enum { TDEFL_NO_FLUSH = 0, TDEFL_SYNC_FLUSH = 2, TDEFL_FULL_FLUSH = 3, TDEFL_FINISH = 4 } tdefl_flush; /* tdefl's compression state structure. */ typedef struct { tdefl_put_buf_func_ptr m_pPut_buf_func; void *m_pPut_buf_user; mz_uint m_flags, m_max_probes[2]; int m_greedy_parsing; mz_uint m_adler32, m_lookahead_pos, m_lookahead_size, m_dict_size; mz_uint8 *m_pLZ_code_buf, *m_pLZ_flags, *m_pOutput_buf, *m_pOutput_buf_end; mz_uint m_num_flags_left, m_total_lz_bytes, m_lz_code_buf_dict_pos, m_bits_in, m_bit_buffer; mz_uint m_saved_match_dist, m_saved_match_len, m_saved_lit, m_output_flush_ofs, m_output_flush_remaining, m_finished, m_block_index, m_wants_to_finish; tdefl_status m_prev_return_status; const void *m_pIn_buf; void *m_pOut_buf; size_t *m_pIn_buf_size, *m_pOut_buf_size; tdefl_flush m_flush; const mz_uint8 *m_pSrc; size_t m_src_buf_left, m_out_buf_ofs; mz_uint8 m_dict[TDEFL_LZ_DICT_SIZE + TDEFL_MAX_MATCH_LEN - 1]; mz_uint16 m_huff_count[TDEFL_MAX_HUFF_TABLES][TDEFL_MAX_HUFF_SYMBOLS]; mz_uint16 m_huff_codes[TDEFL_MAX_HUFF_TABLES][TDEFL_MAX_HUFF_SYMBOLS]; mz_uint8 m_huff_code_sizes[TDEFL_MAX_HUFF_TABLES][TDEFL_MAX_HUFF_SYMBOLS]; mz_uint8 m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE]; mz_uint16 m_next[TDEFL_LZ_DICT_SIZE]; mz_uint16 m_hash[TDEFL_LZ_HASH_SIZE]; mz_uint8 m_output_buf[TDEFL_OUT_BUF_SIZE]; } tdefl_compressor; /* Initializes the compressor. */ /* There is no corresponding deinit() function because the tdefl API's do not dynamically allocate memory. */ /* pBut_buf_func: If NULL, output data will be supplied to the specified callback. In this case, the user should call the tdefl_compress_buffer() API for compression. */ /* If pBut_buf_func is NULL the user should always call the tdefl_compress() API. */ /* flags: See the above enums (TDEFL_HUFFMAN_ONLY, TDEFL_WRITE_ZLIB_HEADER, etc.) */ MINIZ_EXPORT tdefl_status tdefl_init(tdefl_compressor *d, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags); /* Compresses a block of data, consuming as much of the specified input buffer as possible, and writing as much compressed data to the specified output buffer as possible. */ MINIZ_EXPORT tdefl_status tdefl_compress(tdefl_compressor *d, const void *pIn_buf, size_t *pIn_buf_size, void *pOut_buf, size_t *pOut_buf_size, tdefl_flush flush); /* tdefl_compress_buffer() is only usable when the tdefl_init() is called with a non-NULL tdefl_put_buf_func_ptr. */ /* tdefl_compress_buffer() always consumes the entire input buffer. */ MINIZ_EXPORT tdefl_status tdefl_compress_buffer(tdefl_compressor *d, const void *pIn_buf, size_t in_buf_size, tdefl_flush flush); MINIZ_EXPORT tdefl_status tdefl_get_prev_return_status(tdefl_compressor *d); MINIZ_EXPORT mz_uint32 tdefl_get_adler32(tdefl_compressor *d); /* Create tdefl_compress() flags given zlib-style compression parameters. */ /* level may range from [0,10] (where 10 is absolute max compression, but may be much slower on some files) */ /* window_bits may be -15 (raw deflate) or 15 (zlib) */ /* strategy may be either MZ_DEFAULT_STRATEGY, MZ_FILTERED, MZ_HUFFMAN_ONLY, MZ_RLE, or MZ_FIXED */ MINIZ_EXPORT mz_uint tdefl_create_comp_flags_from_zip_params(int level, int window_bits, int strategy); #ifndef MINIZ_NO_MALLOC /* Allocate the tdefl_compressor structure in C so that */ /* non-C language bindings to tdefl_ API don't need to worry about */ /* structure size and allocation mechanism. */ MINIZ_EXPORT tdefl_compressor *tdefl_compressor_alloc(void); MINIZ_EXPORT void tdefl_compressor_free(tdefl_compressor *pComp); #endif #ifdef __cplusplus } #endif #endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/ #pragma once /* ------------------- Low-level Decompression API Definitions */ #ifndef MINIZ_NO_INFLATE_APIS #ifdef __cplusplus extern "C" { #endif /* Decompression flags used by tinfl_decompress(). */ /* TINFL_FLAG_PARSE_ZLIB_HEADER: If set, the input has a valid zlib header and ends with an adler32 checksum (it's a valid zlib stream). Otherwise, the input is a raw deflate stream. */ /* TINFL_FLAG_HAS_MORE_INPUT: If set, there are more input bytes available beyond the end of the supplied input buffer. If clear, the input buffer contains all remaining input. */ /* TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF: If set, the output buffer is large enough to hold the entire decompressed stream. If clear, the output buffer is at least the size of the dictionary (typically 32KB). */ /* TINFL_FLAG_COMPUTE_ADLER32: Force adler-32 checksum computation of the decompressed bytes. */ enum { TINFL_FLAG_PARSE_ZLIB_HEADER = 1, TINFL_FLAG_HAS_MORE_INPUT = 2, TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF = 4, TINFL_FLAG_COMPUTE_ADLER32 = 8 }; /* High level decompression functions: */ /* tinfl_decompress_mem_to_heap() decompresses a block in memory to a heap block allocated via malloc(). */ /* On entry: */ /* pSrc_buf, src_buf_len: Pointer and size of the Deflate or zlib source data to decompress. */ /* On return: */ /* Function returns a pointer to the decompressed data, or NULL on failure. */ /* *pOut_len will be set to the decompressed data's size, which could be larger than src_buf_len on uncompressible data. */ /* The caller must call mz_free() on the returned block when it's no longer needed. */ MINIZ_EXPORT void *tinfl_decompress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags); /* tinfl_decompress_mem_to_mem() decompresses a block in memory to another block in memory. */ /* Returns TINFL_DECOMPRESS_MEM_TO_MEM_FAILED on failure, or the number of bytes written on success. */ #define TINFL_DECOMPRESS_MEM_TO_MEM_FAILED ((size_t)(-1)) MINIZ_EXPORT size_t tinfl_decompress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags); /* tinfl_decompress_mem_to_callback() decompresses a block in memory to an internal 32KB buffer, and a user provided callback function will be called to flush the buffer. */ /* Returns 1 on success or 0 on failure. */ typedef int (*tinfl_put_buf_func_ptr)(const void *pBuf, int len, void *pUser); MINIZ_EXPORT int tinfl_decompress_mem_to_callback(const void *pIn_buf, size_t *pIn_buf_size, tinfl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags); struct tinfl_decompressor_tag; typedef struct tinfl_decompressor_tag tinfl_decompressor; #ifndef MINIZ_NO_MALLOC /* Allocate the tinfl_decompressor structure in C so that */ /* non-C language bindings to tinfl_ API don't need to worry about */ /* structure size and allocation mechanism. */ MINIZ_EXPORT tinfl_decompressor *tinfl_decompressor_alloc(void); MINIZ_EXPORT void tinfl_decompressor_free(tinfl_decompressor *pDecomp); #endif /* Max size of LZ dictionary. */ #define TINFL_LZ_DICT_SIZE 32768 /* Return status. */ typedef enum { /* This flags indicates the inflator needs 1 or more input bytes to make forward progress, but the caller is indicating that no more are available. The compressed data */ /* is probably corrupted. If you call the inflator again with more bytes it'll try to continue processing the input but this is a BAD sign (either the data is corrupted or you called it incorrectly). */ /* If you call it again with no input you'll just get TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS again. */ TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS = -4, /* This flag indicates that one or more of the input parameters was obviously bogus. (You can try calling it again, but if you get this error the calling code is wrong.) */ TINFL_STATUS_BAD_PARAM = -3, /* This flags indicate the inflator is finished but the adler32 check of the uncompressed data didn't match. If you call it again it'll return TINFL_STATUS_DONE. */ TINFL_STATUS_ADLER32_MISMATCH = -2, /* This flags indicate the inflator has somehow failed (bad code, corrupted input, etc.). If you call it again without resetting via tinfl_init() it it'll just keep on returning the same status failure code. */ TINFL_STATUS_FAILED = -1, /* Any status code less than TINFL_STATUS_DONE must indicate a failure. */ /* This flag indicates the inflator has returned every byte of uncompressed data that it can, has consumed every byte that it needed, has successfully reached the end of the deflate stream, and */ /* if zlib headers and adler32 checking enabled that it has successfully checked the uncompressed data's adler32. If you call it again you'll just get TINFL_STATUS_DONE over and over again. */ TINFL_STATUS_DONE = 0, /* This flag indicates the inflator MUST have more input data (even 1 byte) before it can make any more forward progress, or you need to clear the TINFL_FLAG_HAS_MORE_INPUT */ /* flag on the next call if you don't have any more source data. If the source data was somehow corrupted it's also possible (but unlikely) for the inflator to keep on demanding input to */ /* proceed, so be sure to properly set the TINFL_FLAG_HAS_MORE_INPUT flag. */ TINFL_STATUS_NEEDS_MORE_INPUT = 1, /* This flag indicates the inflator definitely has 1 or more bytes of uncompressed data available, but it cannot write this data into the output buffer. */ /* Note if the source compressed data was corrupted it's possible for the inflator to return a lot of uncompressed data to the caller. I've been assuming you know how much uncompressed data to expect */ /* (either exact or worst case) and will stop calling the inflator and fail after receiving too much. In pure streaming scenarios where you have no idea how many bytes to expect this may not be possible */ /* so I may need to add some code to address this. */ TINFL_STATUS_HAS_MORE_OUTPUT = 2 } tinfl_status; /* Initializes the decompressor to its initial state. */ #define tinfl_init(r) \ do \ { \ (r)->m_state = 0; \ } \ MZ_MACRO_END #define tinfl_get_adler32(r) (r)->m_check_adler32 /* Main low-level decompressor coroutine function. This is the only function actually needed for decompression. All the other functions are just high-level helpers for improved usability. */ /* This is a universal API, i.e. it can be used as a building block to build any desired higher level decompression API. In the limit case, it can be called once per every byte input or output. */ MINIZ_EXPORT tinfl_status tinfl_decompress(tinfl_decompressor *r, const mz_uint8 *pIn_buf_next, size_t *pIn_buf_size, mz_uint8 *pOut_buf_start, mz_uint8 *pOut_buf_next, size_t *pOut_buf_size, const mz_uint32 decomp_flags); /* Internal/private bits follow. */ enum { TINFL_MAX_HUFF_TABLES = 3, TINFL_MAX_HUFF_SYMBOLS_0 = 288, TINFL_MAX_HUFF_SYMBOLS_1 = 32, TINFL_MAX_HUFF_SYMBOLS_2 = 19, TINFL_FAST_LOOKUP_BITS = 10, TINFL_FAST_LOOKUP_SIZE = 1 << TINFL_FAST_LOOKUP_BITS }; #if MINIZ_HAS_64BIT_REGISTERS #define TINFL_USE_64BIT_BITBUF 1 #else #define TINFL_USE_64BIT_BITBUF 0 #endif #if TINFL_USE_64BIT_BITBUF typedef mz_uint64 tinfl_bit_buf_t; #define TINFL_BITBUF_SIZE (64) #else typedef mz_uint32 tinfl_bit_buf_t; #define TINFL_BITBUF_SIZE (32) #endif struct tinfl_decompressor_tag { mz_uint32 m_state, m_num_bits, m_zhdr0, m_zhdr1, m_z_adler32, m_final, m_type, m_check_adler32, m_dist, m_counter, m_num_extra, m_table_sizes[TINFL_MAX_HUFF_TABLES]; tinfl_bit_buf_t m_bit_buf; size_t m_dist_from_out_buf_start; mz_int16 m_look_up[TINFL_MAX_HUFF_TABLES][TINFL_FAST_LOOKUP_SIZE]; mz_int16 m_tree_0[TINFL_MAX_HUFF_SYMBOLS_0 * 2]; mz_int16 m_tree_1[TINFL_MAX_HUFF_SYMBOLS_1 * 2]; mz_int16 m_tree_2[TINFL_MAX_HUFF_SYMBOLS_2 * 2]; mz_uint8 m_code_size_0[TINFL_MAX_HUFF_SYMBOLS_0]; mz_uint8 m_code_size_1[TINFL_MAX_HUFF_SYMBOLS_1]; mz_uint8 m_code_size_2[TINFL_MAX_HUFF_SYMBOLS_2]; mz_uint8 m_raw_header[4], m_len_codes[TINFL_MAX_HUFF_SYMBOLS_0 + TINFL_MAX_HUFF_SYMBOLS_1 + 137]; }; #ifdef __cplusplus } #endif #endif /*#ifndef MINIZ_NO_INFLATE_APIS*/ #pragma once /* ------------------- ZIP archive reading/writing */ #ifndef MINIZ_NO_ARCHIVE_APIS #ifdef __cplusplus extern "C" { #endif enum { /* Note: These enums can be reduced as needed to save memory or stack space - they are pretty conservative. */ MZ_ZIP_MAX_IO_BUF_SIZE = 64 * 1024, MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE = 512, MZ_ZIP_MAX_ARCHIVE_FILE_COMMENT_SIZE = 512 }; typedef struct { /* Central directory file index. */ mz_uint32 m_file_index; /* Byte offset of this entry in the archive's central directory. Note we currently only support up to UINT_MAX or less bytes in the central dir. */ mz_uint64 m_central_dir_ofs; /* These fields are copied directly from the zip's central dir. */ mz_uint16 m_version_made_by; mz_uint16 m_version_needed; mz_uint16 m_bit_flag; mz_uint16 m_method; /* CRC-32 of uncompressed data. */ mz_uint32 m_crc32; /* File's compressed size. */ mz_uint64 m_comp_size; /* File's uncompressed size. Note, I've seen some old archives where directory entries had 512 bytes for their uncompressed sizes, but when you try to unpack them you actually get 0 bytes. */ mz_uint64 m_uncomp_size; /* Zip internal and external file attributes. */ mz_uint16 m_internal_attr; mz_uint32 m_external_attr; /* Entry's local header file offset in bytes. */ mz_uint64 m_local_header_ofs; /* Size of comment in bytes. */ mz_uint32 m_comment_size; /* MZ_TRUE if the entry appears to be a directory. */ mz_bool m_is_directory; /* MZ_TRUE if the entry uses encryption/strong encryption (which miniz_zip doesn't support) */ mz_bool m_is_encrypted; /* MZ_TRUE if the file is not encrypted, a patch file, and if it uses a compression method we support. */ mz_bool m_is_supported; /* Filename. If string ends in '/' it's a subdirectory entry. */ /* Guaranteed to be zero terminated, may be truncated to fit. */ char m_filename[MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE]; /* Comment field. */ /* Guaranteed to be zero terminated, may be truncated to fit. */ char m_comment[MZ_ZIP_MAX_ARCHIVE_FILE_COMMENT_SIZE]; #ifdef MINIZ_NO_TIME MZ_TIME_T m_padding; #else MZ_TIME_T m_time; #endif } mz_zip_archive_file_stat; typedef size_t (*mz_file_read_func)(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n); typedef size_t (*mz_file_write_func)(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n); typedef mz_bool (*mz_file_needs_keepalive)(void *pOpaque); struct mz_zip_internal_state_tag; typedef struct mz_zip_internal_state_tag mz_zip_internal_state; typedef enum { MZ_ZIP_MODE_INVALID = 0, MZ_ZIP_MODE_READING = 1, MZ_ZIP_MODE_WRITING = 2, MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED = 3 } mz_zip_mode; typedef enum { MZ_ZIP_FLAG_CASE_SENSITIVE = 0x0100, MZ_ZIP_FLAG_IGNORE_PATH = 0x0200, MZ_ZIP_FLAG_COMPRESSED_DATA = 0x0400, MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY = 0x0800, MZ_ZIP_FLAG_VALIDATE_LOCATE_FILE_FLAG = 0x1000, /* if enabled, mz_zip_reader_locate_file() will be called on each file as its validated to ensure the func finds the file in the central dir (intended for testing) */ MZ_ZIP_FLAG_VALIDATE_HEADERS_ONLY = 0x2000, /* validate the local headers, but don't decompress the entire file and check the crc32 */ MZ_ZIP_FLAG_WRITE_ZIP64 = 0x4000, /* always use the zip64 file format, instead of the original zip file format with automatic switch to zip64. Use as flags parameter with mz_zip_writer_init*_v2 */ MZ_ZIP_FLAG_WRITE_ALLOW_READING = 0x8000, MZ_ZIP_FLAG_ASCII_FILENAME = 0x10000, /*After adding a compressed file, seek back to local file header and set the correct sizes*/ MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE = 0x20000 } mz_zip_flags; typedef enum { MZ_ZIP_TYPE_INVALID = 0, MZ_ZIP_TYPE_USER, MZ_ZIP_TYPE_MEMORY, MZ_ZIP_TYPE_HEAP, MZ_ZIP_TYPE_FILE, MZ_ZIP_TYPE_CFILE, MZ_ZIP_TOTAL_TYPES } mz_zip_type; /* miniz error codes. Be sure to update mz_zip_get_error_string() if you add or modify this enum. */ typedef enum { MZ_ZIP_NO_ERROR = 0, MZ_ZIP_UNDEFINED_ERROR, MZ_ZIP_TOO_MANY_FILES, MZ_ZIP_FILE_TOO_LARGE, MZ_ZIP_UNSUPPORTED_METHOD, MZ_ZIP_UNSUPPORTED_ENCRYPTION, MZ_ZIP_UNSUPPORTED_FEATURE, MZ_ZIP_FAILED_FINDING_CENTRAL_DIR, MZ_ZIP_NOT_AN_ARCHIVE, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED, MZ_ZIP_UNSUPPORTED_MULTIDISK, MZ_ZIP_DECOMPRESSION_FAILED, MZ_ZIP_COMPRESSION_FAILED, MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE, MZ_ZIP_CRC_CHECK_FAILED, MZ_ZIP_UNSUPPORTED_CDIR_SIZE, MZ_ZIP_ALLOC_FAILED, MZ_ZIP_FILE_OPEN_FAILED, MZ_ZIP_FILE_CREATE_FAILED, MZ_ZIP_FILE_WRITE_FAILED, MZ_ZIP_FILE_READ_FAILED, MZ_ZIP_FILE_CLOSE_FAILED, MZ_ZIP_FILE_SEEK_FAILED, MZ_ZIP_FILE_STAT_FAILED, MZ_ZIP_INVALID_PARAMETER, MZ_ZIP_INVALID_FILENAME, MZ_ZIP_BUF_TOO_SMALL, MZ_ZIP_INTERNAL_ERROR, MZ_ZIP_FILE_NOT_FOUND, MZ_ZIP_ARCHIVE_TOO_LARGE, MZ_ZIP_VALIDATION_FAILED, MZ_ZIP_WRITE_CALLBACK_FAILED, MZ_ZIP_TOTAL_ERRORS } mz_zip_error; typedef struct { mz_uint64 m_archive_size; mz_uint64 m_central_directory_file_ofs; /* We only support up to UINT32_MAX files in zip64 mode. */ mz_uint32 m_total_files; mz_zip_mode m_zip_mode; mz_zip_type m_zip_type; mz_zip_error m_last_error; mz_uint64 m_file_offset_alignment; mz_alloc_func m_pAlloc; mz_free_func m_pFree; mz_realloc_func m_pRealloc; void *m_pAlloc_opaque; mz_file_read_func m_pRead; mz_file_write_func m_pWrite; mz_file_needs_keepalive m_pNeeds_keepalive; void *m_pIO_opaque; mz_zip_internal_state *m_pState; } mz_zip_archive; typedef struct { mz_zip_archive *pZip; mz_uint flags; int status; mz_uint64 read_buf_size, read_buf_ofs, read_buf_avail, comp_remaining, out_buf_ofs, cur_file_ofs; mz_zip_archive_file_stat file_stat; void *pRead_buf; void *pWrite_buf; size_t out_blk_remain; tinfl_decompressor inflator; #ifdef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS mz_uint padding; #else mz_uint file_crc32; #endif } mz_zip_reader_extract_iter_state; /* -------- ZIP reading */ /* Inits a ZIP archive reader. */ /* These functions read and validate the archive's central directory. */ MINIZ_EXPORT mz_bool mz_zip_reader_init(mz_zip_archive *pZip, mz_uint64 size, mz_uint flags); MINIZ_EXPORT mz_bool mz_zip_reader_init_mem(mz_zip_archive *pZip, const void *pMem, size_t size, mz_uint flags); #ifndef MINIZ_NO_STDIO /* Read a archive from a disk file. */ /* file_start_ofs is the file offset where the archive actually begins, or 0. */ /* actual_archive_size is the true total size of the archive, which may be smaller than the file's actual size on disk. If zero the entire file is treated as the archive. */ MINIZ_EXPORT mz_bool mz_zip_reader_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint32 flags); MINIZ_EXPORT mz_bool mz_zip_reader_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags, mz_uint64 file_start_ofs, mz_uint64 archive_size); /* Read an archive from an already opened FILE, beginning at the current file position. */ /* The archive is assumed to be archive_size bytes long. If archive_size is 0, then the entire rest of the file is assumed to contain the archive. */ /* The FILE will NOT be closed when mz_zip_reader_end() is called. */ MINIZ_EXPORT mz_bool mz_zip_reader_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint64 archive_size, mz_uint flags); #endif /* Ends archive reading, freeing all allocations, and closing the input archive file if mz_zip_reader_init_file() was used. */ MINIZ_EXPORT mz_bool mz_zip_reader_end(mz_zip_archive *pZip); /* -------- ZIP reading or writing */ /* Clears a mz_zip_archive struct to all zeros. */ /* Important: This must be done before passing the struct to any mz_zip functions. */ MINIZ_EXPORT void mz_zip_zero_struct(mz_zip_archive *pZip); MINIZ_EXPORT mz_zip_mode mz_zip_get_mode(mz_zip_archive *pZip); MINIZ_EXPORT mz_zip_type mz_zip_get_type(mz_zip_archive *pZip); /* Returns the total number of files in the archive. */ MINIZ_EXPORT mz_uint mz_zip_reader_get_num_files(mz_zip_archive *pZip); MINIZ_EXPORT mz_uint64 mz_zip_get_archive_size(mz_zip_archive *pZip); MINIZ_EXPORT mz_uint64 mz_zip_get_archive_file_start_offset(mz_zip_archive *pZip); MINIZ_EXPORT MZ_FILE *mz_zip_get_cfile(mz_zip_archive *pZip); /* Reads n bytes of raw archive data, starting at file offset file_ofs, to pBuf. */ MINIZ_EXPORT size_t mz_zip_read_archive_data(mz_zip_archive *pZip, mz_uint64 file_ofs, void *pBuf, size_t n); /* All mz_zip funcs set the m_last_error field in the mz_zip_archive struct. These functions retrieve/manipulate this field. */ /* Note that the m_last_error functionality is not thread safe. */ MINIZ_EXPORT mz_zip_error mz_zip_set_last_error(mz_zip_archive *pZip, mz_zip_error err_num); MINIZ_EXPORT mz_zip_error mz_zip_peek_last_error(mz_zip_archive *pZip); MINIZ_EXPORT mz_zip_error mz_zip_clear_last_error(mz_zip_archive *pZip); MINIZ_EXPORT mz_zip_error mz_zip_get_last_error(mz_zip_archive *pZip); MINIZ_EXPORT const char *mz_zip_get_error_string(mz_zip_error mz_err); /* MZ_TRUE if the archive file entry is a directory entry. */ MINIZ_EXPORT mz_bool mz_zip_reader_is_file_a_directory(mz_zip_archive *pZip, mz_uint file_index); /* MZ_TRUE if the file is encrypted/strong encrypted. */ MINIZ_EXPORT mz_bool mz_zip_reader_is_file_encrypted(mz_zip_archive *pZip, mz_uint file_index); /* MZ_TRUE if the compression method is supported, and the file is not encrypted, and the file is not a compressed patch file. */ MINIZ_EXPORT mz_bool mz_zip_reader_is_file_supported(mz_zip_archive *pZip, mz_uint file_index); /* Retrieves the filename of an archive file entry. */ /* Returns the number of bytes written to pFilename, or if filename_buf_size is 0 this function returns the number of bytes needed to fully store the filename. */ MINIZ_EXPORT mz_uint mz_zip_reader_get_filename(mz_zip_archive *pZip, mz_uint file_index, char *pFilename, mz_uint filename_buf_size); /* Attempts to locates a file in the archive's central directory. */ /* Valid flags: MZ_ZIP_FLAG_CASE_SENSITIVE, MZ_ZIP_FLAG_IGNORE_PATH */ /* Returns -1 if the file cannot be found. */ MINIZ_EXPORT int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags); MINIZ_EXPORT mz_bool mz_zip_reader_locate_file_v2(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags, mz_uint32 *file_index); /* Returns detailed information about an archive file entry. */ MINIZ_EXPORT mz_bool mz_zip_reader_file_stat(mz_zip_archive *pZip, mz_uint file_index, mz_zip_archive_file_stat *pStat); /* MZ_TRUE if the file is in zip64 format. */ /* A file is considered zip64 if it contained a zip64 end of central directory marker, or if it contained any zip64 extended file information fields in the central directory. */ MINIZ_EXPORT mz_bool mz_zip_is_zip64(mz_zip_archive *pZip); /* Returns the total central directory size in bytes. */ /* The current max supported size is <= MZ_UINT32_MAX. */ MINIZ_EXPORT size_t mz_zip_get_central_dir_size(mz_zip_archive *pZip); /* Extracts a archive file to a memory buffer using no memory allocation. */ /* There must be at least enough room on the stack to store the inflator's state (~34KB or so). */ MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_mem_no_alloc(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size); MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_mem_no_alloc(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size); /* Extracts a archive file to a memory buffer. */ MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_mem(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags); MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_mem(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags); /* Extracts a archive file to a dynamically allocated heap buffer. */ /* The memory will be allocated via the mz_zip_archive's alloc/realloc functions. */ /* Returns NULL and sets the last error on failure. */ MINIZ_EXPORT void *mz_zip_reader_extract_to_heap(mz_zip_archive *pZip, mz_uint file_index, size_t *pSize, mz_uint flags); MINIZ_EXPORT void *mz_zip_reader_extract_file_to_heap(mz_zip_archive *pZip, const char *pFilename, size_t *pSize, mz_uint flags); /* Extracts a archive file using a callback function to output the file's data. */ MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_callback(mz_zip_archive *pZip, mz_uint file_index, mz_file_write_func pCallback, void *pOpaque, mz_uint flags); MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_callback(mz_zip_archive *pZip, const char *pFilename, mz_file_write_func pCallback, void *pOpaque, mz_uint flags); /* Extract a file iteratively */ MINIZ_EXPORT mz_zip_reader_extract_iter_state* mz_zip_reader_extract_iter_new(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags); MINIZ_EXPORT mz_zip_reader_extract_iter_state* mz_zip_reader_extract_file_iter_new(mz_zip_archive *pZip, const char *pFilename, mz_uint flags); MINIZ_EXPORT size_t mz_zip_reader_extract_iter_read(mz_zip_reader_extract_iter_state* pState, void* pvBuf, size_t buf_size); MINIZ_EXPORT mz_bool mz_zip_reader_extract_iter_free(mz_zip_reader_extract_iter_state* pState); #ifndef MINIZ_NO_STDIO /* Extracts a archive file to a disk file and sets its last accessed and modified times. */ /* This function only extracts files, not archive directory records. */ MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_file(mz_zip_archive *pZip, mz_uint file_index, const char *pDst_filename, mz_uint flags); MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_file(mz_zip_archive *pZip, const char *pArchive_filename, const char *pDst_filename, mz_uint flags); /* Extracts a archive file starting at the current position in the destination FILE stream. */ MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_cfile(mz_zip_archive *pZip, mz_uint file_index, MZ_FILE *File, mz_uint flags); MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_cfile(mz_zip_archive *pZip, const char *pArchive_filename, MZ_FILE *pFile, mz_uint flags); #endif #if 0 /* TODO */ typedef void *mz_zip_streaming_extract_state_ptr; mz_zip_streaming_extract_state_ptr mz_zip_streaming_extract_begin(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags); mz_uint64 mz_zip_streaming_extract_get_size(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState); mz_uint64 mz_zip_streaming_extract_get_cur_ofs(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState); mz_bool mz_zip_streaming_extract_seek(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState, mz_uint64 new_ofs); size_t mz_zip_streaming_extract_read(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState, void *pBuf, size_t buf_size); mz_bool mz_zip_streaming_extract_end(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState); #endif /* This function compares the archive's local headers, the optional local zip64 extended information block, and the optional descriptor following the compressed data vs. the data in the central directory. */ /* It also validates that each file can be successfully uncompressed unless the MZ_ZIP_FLAG_VALIDATE_HEADERS_ONLY is specified. */ MINIZ_EXPORT mz_bool mz_zip_validate_file(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags); /* Validates an entire archive by calling mz_zip_validate_file() on each file. */ MINIZ_EXPORT mz_bool mz_zip_validate_archive(mz_zip_archive *pZip, mz_uint flags); /* Misc utils/helpers, valid for ZIP reading or writing */ MINIZ_EXPORT mz_bool mz_zip_validate_mem_archive(const void *pMem, size_t size, mz_uint flags, mz_zip_error *pErr); #ifndef MINIZ_NO_STDIO MINIZ_EXPORT mz_bool mz_zip_validate_file_archive(const char *pFilename, mz_uint flags, mz_zip_error *pErr); #endif /* Universal end function - calls either mz_zip_reader_end() or mz_zip_writer_end(). */ MINIZ_EXPORT mz_bool mz_zip_end(mz_zip_archive *pZip); /* -------- ZIP writing */ #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS /* Inits a ZIP archive writer. */ /*Set pZip->m_pWrite (and pZip->m_pIO_opaque) before calling mz_zip_writer_init or mz_zip_writer_init_v2*/ /*The output is streamable, i.e. file_ofs in mz_file_write_func always increases only by n*/ MINIZ_EXPORT mz_bool mz_zip_writer_init(mz_zip_archive *pZip, mz_uint64 existing_size); MINIZ_EXPORT mz_bool mz_zip_writer_init_v2(mz_zip_archive *pZip, mz_uint64 existing_size, mz_uint flags); MINIZ_EXPORT mz_bool mz_zip_writer_init_heap(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size); MINIZ_EXPORT mz_bool mz_zip_writer_init_heap_v2(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size, mz_uint flags); #ifndef MINIZ_NO_STDIO MINIZ_EXPORT mz_bool mz_zip_writer_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning); MINIZ_EXPORT mz_bool mz_zip_writer_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning, mz_uint flags); MINIZ_EXPORT mz_bool mz_zip_writer_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint flags); #endif /* Converts a ZIP archive reader object into a writer object, to allow efficient in-place file appends to occur on an existing archive. */ /* For archives opened using mz_zip_reader_init_file, pFilename must be the archive's filename so it can be reopened for writing. If the file can't be reopened, mz_zip_reader_end() will be called. */ /* For archives opened using mz_zip_reader_init_mem, the memory block must be growable using the realloc callback (which defaults to realloc unless you've overridden it). */ /* Finally, for archives opened using mz_zip_reader_init, the mz_zip_archive's user provided m_pWrite function cannot be NULL. */ /* Note: In-place archive modification is not recommended unless you know what you're doing, because if execution stops or something goes wrong before */ /* the archive is finalized the file's central directory will be hosed. */ MINIZ_EXPORT mz_bool mz_zip_writer_init_from_reader(mz_zip_archive *pZip, const char *pFilename); MINIZ_EXPORT mz_bool mz_zip_writer_init_from_reader_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags); /* Adds the contents of a memory buffer to an archive. These functions record the current local time into the archive. */ /* To add a directory entry, call this method with an archive name ending in a forwardslash with an empty buffer. */ /* level_and_flags - compression level (0-10, see MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc.) logically OR'd with zero or more mz_zip_flags, or just set to MZ_DEFAULT_COMPRESSION. */ MINIZ_EXPORT mz_bool mz_zip_writer_add_mem(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, mz_uint level_and_flags); /* Like mz_zip_writer_add_mem(), except you can specify a file comment field, and optionally supply the function with already compressed data. */ /* uncomp_size/uncomp_crc32 are only used if the MZ_ZIP_FLAG_COMPRESSED_DATA flag is specified. */ MINIZ_EXPORT mz_bool mz_zip_writer_add_mem_ex(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, mz_uint64 uncomp_size, mz_uint32 uncomp_crc32); MINIZ_EXPORT mz_bool mz_zip_writer_add_mem_ex_v2(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, mz_uint64 uncomp_size, mz_uint32 uncomp_crc32, MZ_TIME_T *last_modified, const char *user_extra_data_local, mz_uint user_extra_data_local_len, const char *user_extra_data_central, mz_uint user_extra_data_central_len); /* Adds the contents of a file to an archive. This function also records the disk file's modified time into the archive. */ /* File data is supplied via a read callback function. User mz_zip_writer_add_(c)file to add a file directly.*/ MINIZ_EXPORT mz_bool mz_zip_writer_add_read_buf_callback(mz_zip_archive *pZip, const char *pArchive_name, mz_file_read_func read_callback, void* callback_opaque, mz_uint64 max_size, const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, const char *user_extra_data_local, mz_uint user_extra_data_local_len, const char *user_extra_data_central, mz_uint user_extra_data_central_len); #ifndef MINIZ_NO_STDIO /* Adds the contents of a disk file to an archive. This function also records the disk file's modified time into the archive. */ /* level_and_flags - compression level (0-10, see MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc.) logically OR'd with zero or more mz_zip_flags, or just set to MZ_DEFAULT_COMPRESSION. */ MINIZ_EXPORT mz_bool mz_zip_writer_add_file(mz_zip_archive *pZip, const char *pArchive_name, const char *pSrc_filename, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags); /* Like mz_zip_writer_add_file(), except the file data is read from the specified FILE stream. */ MINIZ_EXPORT mz_bool mz_zip_writer_add_cfile(mz_zip_archive *pZip, const char *pArchive_name, MZ_FILE *pSrc_file, mz_uint64 max_size, const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, const char *user_extra_data_local, mz_uint user_extra_data_local_len, const char *user_extra_data_central, mz_uint user_extra_data_central_len); #endif /* Adds a file to an archive by fully cloning the data from another archive. */ /* This function fully clones the source file's compressed data (no recompression), along with its full filename, extra data (it may add or modify the zip64 local header extra data field), and the optional descriptor following the compressed data. */ MINIZ_EXPORT mz_bool mz_zip_writer_add_from_zip_reader(mz_zip_archive *pZip, mz_zip_archive *pSource_zip, mz_uint src_file_index); /* Finalizes the archive by writing the central directory records followed by the end of central directory record. */ /* After an archive is finalized, the only valid call on the mz_zip_archive struct is mz_zip_writer_end(). */ /* An archive must be manually finalized by calling this function for it to be valid. */ MINIZ_EXPORT mz_bool mz_zip_writer_finalize_archive(mz_zip_archive *pZip); /* Finalizes a heap archive, returning a pointer to the heap block and its size. */ /* The heap block will be allocated using the mz_zip_archive's alloc/realloc callbacks. */ MINIZ_EXPORT mz_bool mz_zip_writer_finalize_heap_archive(mz_zip_archive *pZip, void **ppBuf, size_t *pSize); /* Ends archive writing, freeing all allocations, and closing the output file if mz_zip_writer_init_file() was used. */ /* Note for the archive to be valid, it *must* have been finalized before ending (this function will not do it for you). */ MINIZ_EXPORT mz_bool mz_zip_writer_end(mz_zip_archive *pZip); /* -------- Misc. high-level helper functions: */ /* mz_zip_add_mem_to_archive_file_in_place() efficiently (but not atomically) appends a memory blob to a ZIP archive. */ /* Note this is NOT a fully safe operation. If it crashes or dies in some way your archive can be left in a screwed up state (without a central directory). */ /* level_and_flags - compression level (0-10, see MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc.) logically OR'd with zero or more mz_zip_flags, or just set to MZ_DEFAULT_COMPRESSION. */ /* TODO: Perhaps add an option to leave the existing central dir in place in case the add dies? We could then truncate the file (so the old central dir would be at the end) if something goes wrong. */ MINIZ_EXPORT mz_bool mz_zip_add_mem_to_archive_file_in_place(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags); MINIZ_EXPORT mz_bool mz_zip_add_mem_to_archive_file_in_place_v2(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, mz_zip_error *pErr); #ifndef MINIZ_NO_STDIO /* Reads a single file from an archive into a heap block. */ /* If pComment is not NULL, only the file with the specified comment will be extracted. */ /* Returns NULL on failure. */ MINIZ_EXPORT void *mz_zip_extract_archive_file_to_heap(const char *pZip_filename, const char *pArchive_name, size_t *pSize, mz_uint flags); MINIZ_EXPORT void *mz_zip_extract_archive_file_to_heap_v2(const char *pZip_filename, const char *pArchive_name, const char *pComment, size_t *pSize, mz_uint flags, mz_zip_error *pErr); #endif #endif /* #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS */ #ifdef __cplusplus } #endif #endif /* MINIZ_NO_ARCHIVE_APIS */ pr0m1th3as-datatypes-9450b29/src/ods/miniz_LICENSE000066400000000000000000000022401527370255000214630ustar00rootroot00000000000000Copyright 2013-2014 RAD Game Tools and Valve Software Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pr0m1th3as-datatypes-9450b29/src/ods/pugiconfig.hpp000066400000000000000000000055201527370255000221170ustar00rootroot00000000000000/** * pugixml parser - version 1.14 * -------------------------------------------------------- * Copyright (C) 2006-2023, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) * Report bugs and download new versions at https://pugixml.org/ * * This library is distributed under the MIT License. See notice at the end * of this file. * * This work is based on the pugxml parser, which is: * Copyright (C) 2003, by Kristen Wegner (kristen@tima.net) */ #ifndef HEADER_PUGICONFIG_HPP #define HEADER_PUGICONFIG_HPP // Uncomment this to enable wchar_t mode // #define PUGIXML_WCHAR_MODE // Uncomment this to enable compact mode // #define PUGIXML_COMPACT // Uncomment this to disable XPath // #define PUGIXML_NO_XPATH // Uncomment this to disable STL // #define PUGIXML_NO_STL // Uncomment this to disable exceptions // #define PUGIXML_NO_EXCEPTIONS // Set this to control attributes for public classes/functions, i.e.: // #define PUGIXML_API __declspec(dllexport) // to export all public symbols from DLL // #define PUGIXML_CLASS __declspec(dllimport) // to import all classes from DLL // #define PUGIXML_FUNCTION __fastcall // to set calling conventions to all public functions to fastcall // In absence of PUGIXML_CLASS/PUGIXML_FUNCTION definitions PUGIXML_API is used instead // Tune these constants to adjust memory-related behavior // #define PUGIXML_MEMORY_PAGE_SIZE 32768 // #define PUGIXML_MEMORY_OUTPUT_STACK 10240 // #define PUGIXML_MEMORY_XPATH_PAGE_SIZE 4096 // Tune this constant to adjust max nesting for XPath queries // #define PUGIXML_XPATH_DEPTH_LIMIT 1024 // Uncomment this to switch to header-only version // #define PUGIXML_HEADER_ONLY // Uncomment this to enable long long support // #define PUGIXML_HAS_LONG_LONG #endif /** * Copyright (c) 2006-2023 Arseny Kapoulkine * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ pr0m1th3as-datatypes-9450b29/src/ods/pugixml.cpp000066400000000000000000012453131527370255000214540ustar00rootroot00000000000000/** * pugixml parser - version 1.14 * -------------------------------------------------------- * Copyright (C) 2006-2023, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) * Report bugs and download new versions at https://pugixml.org/ * * This library is distributed under the MIT License. See notice at the end * of this file. * * This work is based on the pugxml parser, which is: * Copyright (C) 2003, by Kristen Wegner (kristen@tima.net) */ #ifndef SOURCE_PUGIXML_CPP #define SOURCE_PUGIXML_CPP #include "pugixml.hpp" #include #include #include #include #include #ifdef PUGIXML_WCHAR_MODE # include #endif #ifndef PUGIXML_NO_XPATH # include # include #endif #ifndef PUGIXML_NO_STL # include # include # include #endif // For placement new #include // For load_file #if defined(__linux__) || defined(__APPLE__) #include #endif #ifdef _MSC_VER # pragma warning(push) # pragma warning(disable: 4127) // conditional expression is constant # pragma warning(disable: 4324) // structure was padded due to __declspec(align()) # pragma warning(disable: 4702) // unreachable code # pragma warning(disable: 4996) // this function or variable may be unsafe #endif #if defined(_MSC_VER) && defined(__c2__) # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wdeprecated" // this function or variable may be unsafe #endif #ifdef __INTEL_COMPILER # pragma warning(disable: 177) // function was declared but never referenced # pragma warning(disable: 279) // controlling expression is constant # pragma warning(disable: 1478 1786) // function was declared "deprecated" # pragma warning(disable: 1684) // conversion from pointer to same-sized integral type #endif #if defined(__BORLANDC__) && defined(PUGIXML_HEADER_ONLY) # pragma warn -8080 // symbol is declared but never used; disabling this inside push/pop bracket does not make the warning go away #endif #ifdef __BORLANDC__ # pragma option push # pragma warn -8008 // condition is always false # pragma warn -8066 // unreachable code #endif #ifdef __SNC__ // Using diag_push/diag_pop does not disable the warnings inside templates due to a compiler bug # pragma diag_suppress=178 // function was declared but never referenced # pragma diag_suppress=237 // controlling expression is constant #endif #ifdef __TI_COMPILER_VERSION__ # pragma diag_suppress 179 // function was declared but never referenced #endif // Inlining controls #if defined(_MSC_VER) && _MSC_VER >= 1300 # define PUGI_IMPL_NO_INLINE __declspec(noinline) #elif defined(__GNUC__) # define PUGI_IMPL_NO_INLINE __attribute__((noinline)) #else # define PUGI_IMPL_NO_INLINE #endif // Branch weight controls #if defined(__GNUC__) && !defined(__c2__) # define PUGI_IMPL_UNLIKELY(cond) __builtin_expect(cond, 0) #else # define PUGI_IMPL_UNLIKELY(cond) (cond) #endif // Simple static assertion #define PUGI_IMPL_STATIC_ASSERT(cond) { static const char condition_failed[(cond) ? 1 : -1] = {0}; (void)condition_failed[0]; } // Digital Mars C++ bug workaround for passing char loaded from memory via stack #ifdef __DMC__ # define PUGI_IMPL_DMC_VOLATILE volatile #else # define PUGI_IMPL_DMC_VOLATILE #endif // Integer sanitizer workaround; we only apply this for clang since gcc8 has no_sanitize but not unsigned-integer-overflow and produces "attribute directive ignored" warnings #if defined(__clang__) && defined(__has_attribute) # if __has_attribute(no_sanitize) # define PUGI_IMPL_UNSIGNED_OVERFLOW __attribute__((no_sanitize("unsigned-integer-overflow"))) # else # define PUGI_IMPL_UNSIGNED_OVERFLOW # endif #else # define PUGI_IMPL_UNSIGNED_OVERFLOW #endif // Borland C++ bug workaround for not defining ::memcpy depending on header include order (can't always use std::memcpy because some compilers don't have it at all) #if defined(__BORLANDC__) && !defined(__MEM_H_USING_LIST) using std::memcpy; using std::memmove; using std::memset; #endif // Old versions of GCC do not define ::malloc and ::free depending on header include order #if defined(__GNUC__) && (__GNUC__ < 3 || (__GNUC__ == 3 && __GNUC_MINOR__ < 4)) using std::malloc; using std::free; #endif // Some MinGW/GCC versions have headers that erroneously omit LLONG_MIN/LLONG_MAX/ULLONG_MAX definitions from limits.h in some configurations #if defined(PUGIXML_HAS_LONG_LONG) && defined(__GNUC__) && !defined(LLONG_MAX) && !defined(LLONG_MIN) && !defined(ULLONG_MAX) # define LLONG_MIN (-LLONG_MAX - 1LL) # define LLONG_MAX __LONG_LONG_MAX__ # define ULLONG_MAX (LLONG_MAX * 2ULL + 1ULL) #endif // In some environments MSVC is a compiler but the CRT lacks certain MSVC-specific features #if defined(_MSC_VER) && !defined(__S3E__) && !defined(_WIN32_WCE) # define PUGI_IMPL_MSVC_CRT_VERSION _MSC_VER #elif defined(_WIN32_WCE) # define PUGI_IMPL_MSVC_CRT_VERSION 1310 // MSVC7.1 #endif // Not all platforms have snprintf; we define a wrapper that uses snprintf if possible. This only works with buffers with a known size. #if __cplusplus >= 201103 # define PUGI_IMPL_SNPRINTF(buf, ...) snprintf(buf, sizeof(buf), __VA_ARGS__) #elif defined(PUGI_IMPL_MSVC_CRT_VERSION) && PUGI_IMPL_MSVC_CRT_VERSION >= 1400 # define PUGI_IMPL_SNPRINTF(buf, ...) _snprintf_s(buf, _countof(buf), _TRUNCATE, __VA_ARGS__) #elif defined(__APPLE__) && __clang_major__ >= 14 // Xcode 14 marks sprintf as deprecated while still using C++98 by default # define PUGI_IMPL_SNPRINTF(buf, fmt, arg1, arg2) snprintf(buf, sizeof(buf), fmt, arg1, arg2) #else # define PUGI_IMPL_SNPRINTF sprintf #endif // We put implementation details into an anonymous namespace in source mode, but have to keep it in non-anonymous namespace in header-only mode to prevent binary bloat. #ifdef PUGIXML_HEADER_ONLY # define PUGI_IMPL_NS_BEGIN namespace pugi { namespace impl { # define PUGI_IMPL_NS_END } } # define PUGI_IMPL_FN inline # define PUGI_IMPL_FN_NO_INLINE inline #else # if defined(_MSC_VER) && _MSC_VER < 1300 // MSVC6 seems to have an amusing bug with anonymous namespaces inside namespaces # define PUGI_IMPL_NS_BEGIN namespace pugi { namespace impl { # define PUGI_IMPL_NS_END } } # else # define PUGI_IMPL_NS_BEGIN namespace pugi { namespace impl { namespace { # define PUGI_IMPL_NS_END } } } # endif # define PUGI_IMPL_FN # define PUGI_IMPL_FN_NO_INLINE PUGI_IMPL_NO_INLINE #endif // uintptr_t #if (defined(_MSC_VER) && _MSC_VER < 1600) || (defined(__BORLANDC__) && __BORLANDC__ < 0x561) namespace pugi { # ifndef _UINTPTR_T_DEFINED typedef size_t uintptr_t; # endif typedef unsigned __int8 uint8_t; typedef unsigned __int16 uint16_t; typedef unsigned __int32 uint32_t; } #else # include #endif // Memory allocation PUGI_IMPL_NS_BEGIN PUGI_IMPL_FN void* default_allocate(size_t size) { return malloc(size); } PUGI_IMPL_FN void default_deallocate(void* ptr) { free(ptr); } template struct xml_memory_management_function_storage { static allocation_function allocate; static deallocation_function deallocate; }; // Global allocation functions are stored in class statics so that in header mode linker deduplicates them // Without a template<> we'll get multiple definitions of the same static template allocation_function xml_memory_management_function_storage::allocate = default_allocate; template deallocation_function xml_memory_management_function_storage::deallocate = default_deallocate; typedef xml_memory_management_function_storage xml_memory; PUGI_IMPL_NS_END // String utilities PUGI_IMPL_NS_BEGIN // Get string length PUGI_IMPL_FN size_t strlength(const char_t* s) { assert(s); #ifdef PUGIXML_WCHAR_MODE return wcslen(s); #else return strlen(s); #endif } // Compare two strings PUGI_IMPL_FN bool strequal(const char_t* src, const char_t* dst) { assert(src && dst); #ifdef PUGIXML_WCHAR_MODE return wcscmp(src, dst) == 0; #else return strcmp(src, dst) == 0; #endif } // Compare lhs with [rhs_begin, rhs_end) PUGI_IMPL_FN bool strequalrange(const char_t* lhs, const char_t* rhs, size_t count) { for (size_t i = 0; i < count; ++i) if (lhs[i] != rhs[i]) return false; return lhs[count] == 0; } // Get length of wide string, even if CRT lacks wide character support PUGI_IMPL_FN size_t strlength_wide(const wchar_t* s) { assert(s); #ifdef PUGIXML_WCHAR_MODE return wcslen(s); #else const wchar_t* end = s; while (*end) end++; return static_cast(end - s); #endif } PUGI_IMPL_NS_END // auto_ptr-like object for exception recovery PUGI_IMPL_NS_BEGIN template struct auto_deleter { typedef void (*D)(T*); T* data; D deleter; auto_deleter(T* data_, D deleter_): data(data_), deleter(deleter_) { } ~auto_deleter() { if (data) deleter(data); } T* release() { T* result = data; data = 0; return result; } }; PUGI_IMPL_NS_END #ifdef PUGIXML_COMPACT PUGI_IMPL_NS_BEGIN class compact_hash_table { public: compact_hash_table(): _items(0), _capacity(0), _count(0) { } void clear() { if (_items) { xml_memory::deallocate(_items); _items = 0; _capacity = 0; _count = 0; } } void* find(const void* key) { if (_capacity == 0) return 0; item_t* item = get_item(key); assert(item); assert(item->key == key || (item->key == 0 && item->value == 0)); return item->value; } void insert(const void* key, void* value) { assert(_capacity != 0 && _count < _capacity - _capacity / 4); item_t* item = get_item(key); assert(item); if (item->key == 0) { _count++; item->key = key; } item->value = value; } bool reserve(size_t extra = 16) { if (_count + extra >= _capacity - _capacity / 4) return rehash(_count + extra); return true; } private: struct item_t { const void* key; void* value; }; item_t* _items; size_t _capacity; size_t _count; bool rehash(size_t count); item_t* get_item(const void* key) { assert(key); assert(_capacity > 0); size_t hashmod = _capacity - 1; size_t bucket = hash(key) & hashmod; for (size_t probe = 0; probe <= hashmod; ++probe) { item_t& probe_item = _items[bucket]; if (probe_item.key == key || probe_item.key == 0) return &probe_item; // hash collision, quadratic probing bucket = (bucket + probe + 1) & hashmod; } assert(false && "Hash table is full"); // unreachable return 0; } static PUGI_IMPL_UNSIGNED_OVERFLOW unsigned int hash(const void* key) { unsigned int h = static_cast(reinterpret_cast(key) & 0xffffffff); // MurmurHash3 32-bit finalizer h ^= h >> 16; h *= 0x85ebca6bu; h ^= h >> 13; h *= 0xc2b2ae35u; h ^= h >> 16; return h; } }; PUGI_IMPL_FN_NO_INLINE bool compact_hash_table::rehash(size_t count) { size_t capacity = 32; while (count >= capacity - capacity / 4) capacity *= 2; compact_hash_table rt; rt._capacity = capacity; rt._items = static_cast(xml_memory::allocate(sizeof(item_t) * capacity)); if (!rt._items) return false; memset(rt._items, 0, sizeof(item_t) * capacity); for (size_t i = 0; i < _capacity; ++i) if (_items[i].key) rt.insert(_items[i].key, _items[i].value); if (_items) xml_memory::deallocate(_items); _capacity = capacity; _items = rt._items; assert(_count == rt._count); return true; } PUGI_IMPL_NS_END #endif PUGI_IMPL_NS_BEGIN #ifdef PUGIXML_COMPACT static const uintptr_t xml_memory_block_alignment = 4; #else static const uintptr_t xml_memory_block_alignment = sizeof(void*); #endif // extra metadata bits static const uintptr_t xml_memory_page_contents_shared_mask = 64; static const uintptr_t xml_memory_page_name_allocated_mask = 32; static const uintptr_t xml_memory_page_value_allocated_mask = 16; static const uintptr_t xml_memory_page_type_mask = 15; // combined masks for string uniqueness static const uintptr_t xml_memory_page_name_allocated_or_shared_mask = xml_memory_page_name_allocated_mask | xml_memory_page_contents_shared_mask; static const uintptr_t xml_memory_page_value_allocated_or_shared_mask = xml_memory_page_value_allocated_mask | xml_memory_page_contents_shared_mask; #ifdef PUGIXML_COMPACT #define PUGI_IMPL_GETHEADER_IMPL(object, page, flags) // unused #define PUGI_IMPL_GETPAGE_IMPL(header) (header).get_page() #else #define PUGI_IMPL_GETHEADER_IMPL(object, page, flags) (((reinterpret_cast(object) - reinterpret_cast(page)) << 8) | (flags)) // this macro casts pointers through void* to avoid 'cast increases required alignment of target type' warnings #define PUGI_IMPL_GETPAGE_IMPL(header) static_cast(const_cast(static_cast(reinterpret_cast(&header) - (header >> 8)))) #endif #define PUGI_IMPL_GETPAGE(n) PUGI_IMPL_GETPAGE_IMPL((n)->header) #define PUGI_IMPL_NODETYPE(n) static_cast((n)->header & impl::xml_memory_page_type_mask) struct xml_allocator; struct xml_memory_page { static xml_memory_page* construct(void* memory) { xml_memory_page* result = static_cast(memory); result->allocator = 0; result->prev = 0; result->next = 0; result->busy_size = 0; result->freed_size = 0; #ifdef PUGIXML_COMPACT result->compact_string_base = 0; result->compact_shared_parent = 0; result->compact_page_marker = 0; #endif return result; } xml_allocator* allocator; xml_memory_page* prev; xml_memory_page* next; size_t busy_size; size_t freed_size; #ifdef PUGIXML_COMPACT char_t* compact_string_base; void* compact_shared_parent; uint32_t* compact_page_marker; #endif }; static const size_t xml_memory_page_size = #ifdef PUGIXML_MEMORY_PAGE_SIZE (PUGIXML_MEMORY_PAGE_SIZE) #else 32768 #endif - sizeof(xml_memory_page); struct xml_memory_string_header { uint16_t page_offset; // offset from page->data uint16_t full_size; // 0 if string occupies whole page }; struct xml_allocator { xml_allocator(xml_memory_page* root): _root(root), _busy_size(root->busy_size) { #ifdef PUGIXML_COMPACT _hash = 0; #endif } xml_memory_page* allocate_page(size_t data_size) { size_t size = sizeof(xml_memory_page) + data_size; // allocate block with some alignment, leaving memory for worst-case padding void* memory = xml_memory::allocate(size); if (!memory) return 0; // prepare page structure xml_memory_page* page = xml_memory_page::construct(memory); assert(page); assert(this == _root->allocator); page->allocator = this; return page; } static void deallocate_page(xml_memory_page* page) { xml_memory::deallocate(page); } void* allocate_memory_oob(size_t size, xml_memory_page*& out_page); void* allocate_memory(size_t size, xml_memory_page*& out_page) { if (PUGI_IMPL_UNLIKELY(_busy_size + size > xml_memory_page_size)) return allocate_memory_oob(size, out_page); void* buf = reinterpret_cast(_root) + sizeof(xml_memory_page) + _busy_size; _busy_size += size; out_page = _root; return buf; } #ifdef PUGIXML_COMPACT void* allocate_object(size_t size, xml_memory_page*& out_page) { void* result = allocate_memory(size + sizeof(uint32_t), out_page); if (!result) return 0; // adjust for marker ptrdiff_t offset = static_cast(result) - reinterpret_cast(out_page->compact_page_marker); if (PUGI_IMPL_UNLIKELY(static_cast(offset) >= 256 * xml_memory_block_alignment)) { // insert new marker uint32_t* marker = static_cast(result); *marker = static_cast(reinterpret_cast(marker) - reinterpret_cast(out_page)); out_page->compact_page_marker = marker; // since we don't reuse the page space until we reallocate it, we can just pretend that we freed the marker block // this will make sure deallocate_memory correctly tracks the size out_page->freed_size += sizeof(uint32_t); return marker + 1; } else { // roll back uint32_t part _busy_size -= sizeof(uint32_t); return result; } } #else void* allocate_object(size_t size, xml_memory_page*& out_page) { return allocate_memory(size, out_page); } #endif void deallocate_memory(void* ptr, size_t size, xml_memory_page* page) { if (page == _root) page->busy_size = _busy_size; assert(ptr >= reinterpret_cast(page) + sizeof(xml_memory_page) && ptr < reinterpret_cast(page) + sizeof(xml_memory_page) + page->busy_size); (void)!ptr; page->freed_size += size; assert(page->freed_size <= page->busy_size); if (page->freed_size == page->busy_size) { if (page->next == 0) { assert(_root == page); // top page freed, just reset sizes page->busy_size = 0; page->freed_size = 0; #ifdef PUGIXML_COMPACT // reset compact state to maximize efficiency page->compact_string_base = 0; page->compact_shared_parent = 0; page->compact_page_marker = 0; #endif _busy_size = 0; } else { assert(_root != page); assert(page->prev); // remove from the list page->prev->next = page->next; page->next->prev = page->prev; // deallocate deallocate_page(page); } } } char_t* allocate_string(size_t length) { static const size_t max_encoded_offset = (1 << 16) * xml_memory_block_alignment; PUGI_IMPL_STATIC_ASSERT(xml_memory_page_size <= max_encoded_offset); // allocate memory for string and header block size_t size = sizeof(xml_memory_string_header) + length * sizeof(char_t); // round size up to block alignment boundary size_t full_size = (size + (xml_memory_block_alignment - 1)) & ~(xml_memory_block_alignment - 1); xml_memory_page* page; xml_memory_string_header* header = static_cast(allocate_memory(full_size, page)); if (!header) return 0; // setup header ptrdiff_t page_offset = reinterpret_cast(header) - reinterpret_cast(page) - sizeof(xml_memory_page); assert(page_offset % xml_memory_block_alignment == 0); assert(page_offset >= 0 && static_cast(page_offset) < max_encoded_offset); header->page_offset = static_cast(static_cast(page_offset) / xml_memory_block_alignment); // full_size == 0 for large strings that occupy the whole page assert(full_size % xml_memory_block_alignment == 0); assert(full_size < max_encoded_offset || (page->busy_size == full_size && page_offset == 0)); header->full_size = static_cast(full_size < max_encoded_offset ? full_size / xml_memory_block_alignment : 0); // round-trip through void* to avoid 'cast increases required alignment of target type' warning // header is guaranteed a pointer-sized alignment, which should be enough for char_t return static_cast(static_cast(header + 1)); } void deallocate_string(char_t* string) { // this function casts pointers through void* to avoid 'cast increases required alignment of target type' warnings // we're guaranteed the proper (pointer-sized) alignment on the input string if it was allocated via allocate_string // get header xml_memory_string_header* header = static_cast(static_cast(string)) - 1; assert(header); // deallocate size_t page_offset = sizeof(xml_memory_page) + header->page_offset * xml_memory_block_alignment; xml_memory_page* page = reinterpret_cast(static_cast(reinterpret_cast(header) - page_offset)); // if full_size == 0 then this string occupies the whole page size_t full_size = header->full_size == 0 ? page->busy_size : header->full_size * xml_memory_block_alignment; deallocate_memory(header, full_size, page); } bool reserve() { #ifdef PUGIXML_COMPACT return _hash->reserve(); #else return true; #endif } xml_memory_page* _root; size_t _busy_size; #ifdef PUGIXML_COMPACT compact_hash_table* _hash; #endif }; PUGI_IMPL_FN_NO_INLINE void* xml_allocator::allocate_memory_oob(size_t size, xml_memory_page*& out_page) { const size_t large_allocation_threshold = xml_memory_page_size / 4; xml_memory_page* page = allocate_page(size <= large_allocation_threshold ? xml_memory_page_size : size); out_page = page; if (!page) return 0; if (size <= large_allocation_threshold) { _root->busy_size = _busy_size; // insert page at the end of linked list page->prev = _root; _root->next = page; _root = page; _busy_size = size; } else { // insert page before the end of linked list, so that it is deleted as soon as possible // the last page is not deleted even if it's empty (see deallocate_memory) assert(_root->prev); page->prev = _root->prev; page->next = _root; _root->prev->next = page; _root->prev = page; page->busy_size = size; } return reinterpret_cast(page) + sizeof(xml_memory_page); } PUGI_IMPL_NS_END #ifdef PUGIXML_COMPACT PUGI_IMPL_NS_BEGIN static const uintptr_t compact_alignment_log2 = 2; static const uintptr_t compact_alignment = 1 << compact_alignment_log2; class compact_header { public: compact_header(xml_memory_page* page, unsigned int flags) { PUGI_IMPL_STATIC_ASSERT(xml_memory_block_alignment == compact_alignment); ptrdiff_t offset = (reinterpret_cast(this) - reinterpret_cast(page->compact_page_marker)); assert(offset % compact_alignment == 0 && static_cast(offset) < 256 * compact_alignment); _page = static_cast(offset >> compact_alignment_log2); _flags = static_cast(flags); } void operator&=(uintptr_t mod) { _flags &= static_cast(mod); } void operator|=(uintptr_t mod) { _flags |= static_cast(mod); } uintptr_t operator&(uintptr_t mod) const { return _flags & mod; } xml_memory_page* get_page() const { // round-trip through void* to silence 'cast increases required alignment of target type' warnings const char* page_marker = reinterpret_cast(this) - (_page << compact_alignment_log2); const char* page = page_marker - *reinterpret_cast(static_cast(page_marker)); return const_cast(reinterpret_cast(static_cast(page))); } private: unsigned char _page; unsigned char _flags; }; PUGI_IMPL_FN xml_memory_page* compact_get_page(const void* object, int header_offset) { const compact_header* header = reinterpret_cast(static_cast(object) - header_offset); return header->get_page(); } template PUGI_IMPL_FN_NO_INLINE T* compact_get_value(const void* object) { return static_cast(compact_get_page(object, header_offset)->allocator->_hash->find(object)); } template PUGI_IMPL_FN_NO_INLINE void compact_set_value(const void* object, T* value) { compact_get_page(object, header_offset)->allocator->_hash->insert(object, value); } template class compact_pointer { public: compact_pointer(): _data(0) { } void operator=(const compact_pointer& rhs) { *this = rhs + 0; } void operator=(T* value) { if (value) { // value is guaranteed to be compact-aligned; 'this' is not // our decoding is based on 'this' aligned to compact alignment downwards (see operator T*) // so for negative offsets (e.g. -3) we need to adjust the diff by compact_alignment - 1 to // compensate for arithmetic shift rounding for negative values ptrdiff_t diff = reinterpret_cast(value) - reinterpret_cast(this); ptrdiff_t offset = ((diff + int(compact_alignment - 1)) >> compact_alignment_log2) - start; if (static_cast(offset) <= 253) _data = static_cast(offset + 1); else { compact_set_value(this, value); _data = 255; } } else _data = 0; } operator T*() const { if (_data) { if (_data < 255) { uintptr_t base = reinterpret_cast(this) & ~(compact_alignment - 1); return reinterpret_cast(base + (_data - 1 + start) * compact_alignment); } else return compact_get_value(this); } else return 0; } T* operator->() const { return *this; } private: unsigned char _data; }; template class compact_pointer_parent { public: compact_pointer_parent(): _data(0) { } void operator=(const compact_pointer_parent& rhs) { *this = rhs + 0; } void operator=(T* value) { if (value) { // value is guaranteed to be compact-aligned; 'this' is not // our decoding is based on 'this' aligned to compact alignment downwards (see operator T*) // so for negative offsets (e.g. -3) we need to adjust the diff by compact_alignment - 1 to // compensate for arithmetic shift behavior for negative values ptrdiff_t diff = reinterpret_cast(value) - reinterpret_cast(this); ptrdiff_t offset = ((diff + int(compact_alignment - 1)) >> compact_alignment_log2) + 65533; if (static_cast(offset) <= 65533) { _data = static_cast(offset + 1); } else { xml_memory_page* page = compact_get_page(this, header_offset); if (PUGI_IMPL_UNLIKELY(page->compact_shared_parent == 0)) page->compact_shared_parent = value; if (page->compact_shared_parent == value) { _data = 65534; } else { compact_set_value(this, value); _data = 65535; } } } else { _data = 0; } } operator T*() const { if (_data) { if (_data < 65534) { uintptr_t base = reinterpret_cast(this) & ~(compact_alignment - 1); return reinterpret_cast(base + (_data - 1 - 65533) * compact_alignment); } else if (_data == 65534) return static_cast(compact_get_page(this, header_offset)->compact_shared_parent); else return compact_get_value(this); } else return 0; } T* operator->() const { return *this; } private: uint16_t _data; }; template class compact_string { public: compact_string(): _data(0) { } void operator=(const compact_string& rhs) { *this = rhs + 0; } void operator=(char_t* value) { if (value) { xml_memory_page* page = compact_get_page(this, header_offset); if (PUGI_IMPL_UNLIKELY(page->compact_string_base == 0)) page->compact_string_base = value; ptrdiff_t offset = value - page->compact_string_base; if (static_cast(offset) < (65535 << 7)) { // round-trip through void* to silence 'cast increases required alignment of target type' warnings uint16_t* base = reinterpret_cast(static_cast(reinterpret_cast(this) - base_offset)); if (*base == 0) { *base = static_cast((offset >> 7) + 1); _data = static_cast((offset & 127) + 1); } else { ptrdiff_t remainder = offset - ((*base - 1) << 7); if (static_cast(remainder) <= 253) { _data = static_cast(remainder + 1); } else { compact_set_value(this, value); _data = 255; } } } else { compact_set_value(this, value); _data = 255; } } else { _data = 0; } } operator char_t*() const { if (_data) { if (_data < 255) { xml_memory_page* page = compact_get_page(this, header_offset); // round-trip through void* to silence 'cast increases required alignment of target type' warnings const uint16_t* base = reinterpret_cast(static_cast(reinterpret_cast(this) - base_offset)); assert(*base); ptrdiff_t offset = ((*base - 1) << 7) + (_data - 1); return page->compact_string_base + offset; } else { return compact_get_value(this); } } else return 0; } private: unsigned char _data; }; PUGI_IMPL_NS_END #endif #ifdef PUGIXML_COMPACT namespace pugi { struct xml_attribute_struct { xml_attribute_struct(impl::xml_memory_page* page): header(page, 0), namevalue_base(0) { PUGI_IMPL_STATIC_ASSERT(sizeof(xml_attribute_struct) == 8); } impl::compact_header header; uint16_t namevalue_base; impl::compact_string<4, 2> name; impl::compact_string<5, 3> value; impl::compact_pointer prev_attribute_c; impl::compact_pointer next_attribute; }; struct xml_node_struct { xml_node_struct(impl::xml_memory_page* page, xml_node_type type): header(page, type), namevalue_base(0) { PUGI_IMPL_STATIC_ASSERT(sizeof(xml_node_struct) == 12); } impl::compact_header header; uint16_t namevalue_base; impl::compact_string<4, 2> name; impl::compact_string<5, 3> value; impl::compact_pointer_parent parent; impl::compact_pointer first_child; impl::compact_pointer prev_sibling_c; impl::compact_pointer next_sibling; impl::compact_pointer first_attribute; }; } #else namespace pugi { struct xml_attribute_struct { xml_attribute_struct(impl::xml_memory_page* page): name(0), value(0), prev_attribute_c(0), next_attribute(0) { header = PUGI_IMPL_GETHEADER_IMPL(this, page, 0); } uintptr_t header; char_t* name; char_t* value; xml_attribute_struct* prev_attribute_c; xml_attribute_struct* next_attribute; }; struct xml_node_struct { xml_node_struct(impl::xml_memory_page* page, xml_node_type type): name(0), value(0), parent(0), first_child(0), prev_sibling_c(0), next_sibling(0), first_attribute(0) { header = PUGI_IMPL_GETHEADER_IMPL(this, page, type); } uintptr_t header; char_t* name; char_t* value; xml_node_struct* parent; xml_node_struct* first_child; xml_node_struct* prev_sibling_c; xml_node_struct* next_sibling; xml_attribute_struct* first_attribute; }; } #endif PUGI_IMPL_NS_BEGIN struct xml_extra_buffer { char_t* buffer; xml_extra_buffer* next; }; struct xml_document_struct: public xml_node_struct, public xml_allocator { xml_document_struct(xml_memory_page* page): xml_node_struct(page, node_document), xml_allocator(page), buffer(0), extra_buffers(0) { } const char_t* buffer; xml_extra_buffer* extra_buffers; #ifdef PUGIXML_COMPACT compact_hash_table hash; #endif }; template inline xml_allocator& get_allocator(const Object* object) { assert(object); return *PUGI_IMPL_GETPAGE(object)->allocator; } template inline xml_document_struct& get_document(const Object* object) { assert(object); return *static_cast(PUGI_IMPL_GETPAGE(object)->allocator); } PUGI_IMPL_NS_END // Low-level DOM operations PUGI_IMPL_NS_BEGIN inline xml_attribute_struct* allocate_attribute(xml_allocator& alloc) { xml_memory_page* page; void* memory = alloc.allocate_object(sizeof(xml_attribute_struct), page); if (!memory) return 0; return new (memory) xml_attribute_struct(page); } inline xml_node_struct* allocate_node(xml_allocator& alloc, xml_node_type type) { xml_memory_page* page; void* memory = alloc.allocate_object(sizeof(xml_node_struct), page); if (!memory) return 0; return new (memory) xml_node_struct(page, type); } inline void destroy_attribute(xml_attribute_struct* a, xml_allocator& alloc) { if (a->header & impl::xml_memory_page_name_allocated_mask) alloc.deallocate_string(a->name); if (a->header & impl::xml_memory_page_value_allocated_mask) alloc.deallocate_string(a->value); alloc.deallocate_memory(a, sizeof(xml_attribute_struct), PUGI_IMPL_GETPAGE(a)); } inline void destroy_node(xml_node_struct* n, xml_allocator& alloc) { if (n->header & impl::xml_memory_page_name_allocated_mask) alloc.deallocate_string(n->name); if (n->header & impl::xml_memory_page_value_allocated_mask) alloc.deallocate_string(n->value); for (xml_attribute_struct* attr = n->first_attribute; attr; ) { xml_attribute_struct* next = attr->next_attribute; destroy_attribute(attr, alloc); attr = next; } for (xml_node_struct* child = n->first_child; child; ) { xml_node_struct* next = child->next_sibling; destroy_node(child, alloc); child = next; } alloc.deallocate_memory(n, sizeof(xml_node_struct), PUGI_IMPL_GETPAGE(n)); } inline void append_node(xml_node_struct* child, xml_node_struct* node) { child->parent = node; xml_node_struct* head = node->first_child; if (head) { xml_node_struct* tail = head->prev_sibling_c; tail->next_sibling = child; child->prev_sibling_c = tail; head->prev_sibling_c = child; } else { node->first_child = child; child->prev_sibling_c = child; } } inline void prepend_node(xml_node_struct* child, xml_node_struct* node) { child->parent = node; xml_node_struct* head = node->first_child; if (head) { child->prev_sibling_c = head->prev_sibling_c; head->prev_sibling_c = child; } else child->prev_sibling_c = child; child->next_sibling = head; node->first_child = child; } inline void insert_node_after(xml_node_struct* child, xml_node_struct* node) { xml_node_struct* parent = node->parent; child->parent = parent; xml_node_struct* next = node->next_sibling; if (next) next->prev_sibling_c = child; else parent->first_child->prev_sibling_c = child; child->next_sibling = next; child->prev_sibling_c = node; node->next_sibling = child; } inline void insert_node_before(xml_node_struct* child, xml_node_struct* node) { xml_node_struct* parent = node->parent; child->parent = parent; xml_node_struct* prev = node->prev_sibling_c; if (prev->next_sibling) prev->next_sibling = child; else parent->first_child = child; child->prev_sibling_c = prev; child->next_sibling = node; node->prev_sibling_c = child; } inline void remove_node(xml_node_struct* node) { xml_node_struct* parent = node->parent; xml_node_struct* next = node->next_sibling; xml_node_struct* prev = node->prev_sibling_c; if (next) next->prev_sibling_c = prev; else parent->first_child->prev_sibling_c = prev; if (prev->next_sibling) prev->next_sibling = next; else parent->first_child = next; node->parent = 0; node->prev_sibling_c = 0; node->next_sibling = 0; } inline void append_attribute(xml_attribute_struct* attr, xml_node_struct* node) { xml_attribute_struct* head = node->first_attribute; if (head) { xml_attribute_struct* tail = head->prev_attribute_c; tail->next_attribute = attr; attr->prev_attribute_c = tail; head->prev_attribute_c = attr; } else { node->first_attribute = attr; attr->prev_attribute_c = attr; } } inline void prepend_attribute(xml_attribute_struct* attr, xml_node_struct* node) { xml_attribute_struct* head = node->first_attribute; if (head) { attr->prev_attribute_c = head->prev_attribute_c; head->prev_attribute_c = attr; } else attr->prev_attribute_c = attr; attr->next_attribute = head; node->first_attribute = attr; } inline void insert_attribute_after(xml_attribute_struct* attr, xml_attribute_struct* place, xml_node_struct* node) { xml_attribute_struct* next = place->next_attribute; if (next) next->prev_attribute_c = attr; else node->first_attribute->prev_attribute_c = attr; attr->next_attribute = next; attr->prev_attribute_c = place; place->next_attribute = attr; } inline void insert_attribute_before(xml_attribute_struct* attr, xml_attribute_struct* place, xml_node_struct* node) { xml_attribute_struct* prev = place->prev_attribute_c; if (prev->next_attribute) prev->next_attribute = attr; else node->first_attribute = attr; attr->prev_attribute_c = prev; attr->next_attribute = place; place->prev_attribute_c = attr; } inline void remove_attribute(xml_attribute_struct* attr, xml_node_struct* node) { xml_attribute_struct* next = attr->next_attribute; xml_attribute_struct* prev = attr->prev_attribute_c; if (next) next->prev_attribute_c = prev; else node->first_attribute->prev_attribute_c = prev; if (prev->next_attribute) prev->next_attribute = next; else node->first_attribute = next; attr->prev_attribute_c = 0; attr->next_attribute = 0; } PUGI_IMPL_FN_NO_INLINE xml_node_struct* append_new_node(xml_node_struct* node, xml_allocator& alloc, xml_node_type type = node_element) { if (!alloc.reserve()) return 0; xml_node_struct* child = allocate_node(alloc, type); if (!child) return 0; append_node(child, node); return child; } PUGI_IMPL_FN_NO_INLINE xml_attribute_struct* append_new_attribute(xml_node_struct* node, xml_allocator& alloc) { if (!alloc.reserve()) return 0; xml_attribute_struct* attr = allocate_attribute(alloc); if (!attr) return 0; append_attribute(attr, node); return attr; } PUGI_IMPL_NS_END // Helper classes for code generation PUGI_IMPL_NS_BEGIN struct opt_false { enum { value = 0 }; }; struct opt_true { enum { value = 1 }; }; PUGI_IMPL_NS_END // Unicode utilities PUGI_IMPL_NS_BEGIN inline uint16_t endian_swap(uint16_t value) { return static_cast(((value & 0xff) << 8) | (value >> 8)); } inline uint32_t endian_swap(uint32_t value) { return ((value & 0xff) << 24) | ((value & 0xff00) << 8) | ((value & 0xff0000) >> 8) | (value >> 24); } struct utf8_counter { typedef size_t value_type; static value_type low(value_type result, uint32_t ch) { // U+0000..U+007F if (ch < 0x80) return result + 1; // U+0080..U+07FF else if (ch < 0x800) return result + 2; // U+0800..U+FFFF else return result + 3; } static value_type high(value_type result, uint32_t) { // U+10000..U+10FFFF return result + 4; } }; struct utf8_writer { typedef uint8_t* value_type; static value_type low(value_type result, uint32_t ch) { // U+0000..U+007F if (ch < 0x80) { *result = static_cast(ch); return result + 1; } // U+0080..U+07FF else if (ch < 0x800) { result[0] = static_cast(0xC0 | (ch >> 6)); result[1] = static_cast(0x80 | (ch & 0x3F)); return result + 2; } // U+0800..U+FFFF else { result[0] = static_cast(0xE0 | (ch >> 12)); result[1] = static_cast(0x80 | ((ch >> 6) & 0x3F)); result[2] = static_cast(0x80 | (ch & 0x3F)); return result + 3; } } static value_type high(value_type result, uint32_t ch) { // U+10000..U+10FFFF result[0] = static_cast(0xF0 | (ch >> 18)); result[1] = static_cast(0x80 | ((ch >> 12) & 0x3F)); result[2] = static_cast(0x80 | ((ch >> 6) & 0x3F)); result[3] = static_cast(0x80 | (ch & 0x3F)); return result + 4; } static value_type any(value_type result, uint32_t ch) { return (ch < 0x10000) ? low(result, ch) : high(result, ch); } }; struct utf16_counter { typedef size_t value_type; static value_type low(value_type result, uint32_t) { return result + 1; } static value_type high(value_type result, uint32_t) { return result + 2; } }; struct utf16_writer { typedef uint16_t* value_type; static value_type low(value_type result, uint32_t ch) { *result = static_cast(ch); return result + 1; } static value_type high(value_type result, uint32_t ch) { uint32_t msh = static_cast(ch - 0x10000) >> 10; uint32_t lsh = static_cast(ch - 0x10000) & 0x3ff; result[0] = static_cast(0xD800 + msh); result[1] = static_cast(0xDC00 + lsh); return result + 2; } static value_type any(value_type result, uint32_t ch) { return (ch < 0x10000) ? low(result, ch) : high(result, ch); } }; struct utf32_counter { typedef size_t value_type; static value_type low(value_type result, uint32_t) { return result + 1; } static value_type high(value_type result, uint32_t) { return result + 1; } }; struct utf32_writer { typedef uint32_t* value_type; static value_type low(value_type result, uint32_t ch) { *result = ch; return result + 1; } static value_type high(value_type result, uint32_t ch) { *result = ch; return result + 1; } static value_type any(value_type result, uint32_t ch) { *result = ch; return result + 1; } }; struct latin1_writer { typedef uint8_t* value_type; static value_type low(value_type result, uint32_t ch) { *result = static_cast(ch > 255 ? '?' : ch); return result + 1; } static value_type high(value_type result, uint32_t ch) { (void)ch; *result = '?'; return result + 1; } }; struct utf8_decoder { typedef uint8_t type; template static inline typename Traits::value_type process(const uint8_t* data, size_t size, typename Traits::value_type result, Traits) { const uint8_t utf8_byte_mask = 0x3f; while (size) { uint8_t lead = *data; // 0xxxxxxx -> U+0000..U+007F if (lead < 0x80) { result = Traits::low(result, lead); data += 1; size -= 1; // process aligned single-byte (ascii) blocks if ((reinterpret_cast(data) & 3) == 0) { // round-trip through void* to silence 'cast increases required alignment of target type' warnings while (size >= 4 && (*static_cast(static_cast(data)) & 0x80808080) == 0) { result = Traits::low(result, data[0]); result = Traits::low(result, data[1]); result = Traits::low(result, data[2]); result = Traits::low(result, data[3]); data += 4; size -= 4; } } } // 110xxxxx -> U+0080..U+07FF else if (static_cast(lead - 0xC0) < 0x20 && size >= 2 && (data[1] & 0xc0) == 0x80) { result = Traits::low(result, ((lead & ~0xC0) << 6) | (data[1] & utf8_byte_mask)); data += 2; size -= 2; } // 1110xxxx -> U+0800-U+FFFF else if (static_cast(lead - 0xE0) < 0x10 && size >= 3 && (data[1] & 0xc0) == 0x80 && (data[2] & 0xc0) == 0x80) { result = Traits::low(result, ((lead & ~0xE0) << 12) | ((data[1] & utf8_byte_mask) << 6) | (data[2] & utf8_byte_mask)); data += 3; size -= 3; } // 11110xxx -> U+10000..U+10FFFF else if (static_cast(lead - 0xF0) < 0x08 && size >= 4 && (data[1] & 0xc0) == 0x80 && (data[2] & 0xc0) == 0x80 && (data[3] & 0xc0) == 0x80) { result = Traits::high(result, ((lead & ~0xF0) << 18) | ((data[1] & utf8_byte_mask) << 12) | ((data[2] & utf8_byte_mask) << 6) | (data[3] & utf8_byte_mask)); data += 4; size -= 4; } // 10xxxxxx or 11111xxx -> invalid else { data += 1; size -= 1; } } return result; } }; template struct utf16_decoder { typedef uint16_t type; template static inline typename Traits::value_type process(const uint16_t* data, size_t size, typename Traits::value_type result, Traits) { while (size) { uint16_t lead = opt_swap::value ? endian_swap(*data) : *data; // U+0000..U+D7FF if (lead < 0xD800) { result = Traits::low(result, lead); data += 1; size -= 1; } // U+E000..U+FFFF else if (static_cast(lead - 0xE000) < 0x2000) { result = Traits::low(result, lead); data += 1; size -= 1; } // surrogate pair lead else if (static_cast(lead - 0xD800) < 0x400 && size >= 2) { uint16_t next = opt_swap::value ? endian_swap(data[1]) : data[1]; if (static_cast(next - 0xDC00) < 0x400) { result = Traits::high(result, 0x10000 + ((lead & 0x3ff) << 10) + (next & 0x3ff)); data += 2; size -= 2; } else { data += 1; size -= 1; } } else { data += 1; size -= 1; } } return result; } }; template struct utf32_decoder { typedef uint32_t type; template static inline typename Traits::value_type process(const uint32_t* data, size_t size, typename Traits::value_type result, Traits) { while (size) { uint32_t lead = opt_swap::value ? endian_swap(*data) : *data; // U+0000..U+FFFF if (lead < 0x10000) { result = Traits::low(result, lead); data += 1; size -= 1; } // U+10000..U+10FFFF else { result = Traits::high(result, lead); data += 1; size -= 1; } } return result; } }; struct latin1_decoder { typedef uint8_t type; template static inline typename Traits::value_type process(const uint8_t* data, size_t size, typename Traits::value_type result, Traits) { while (size) { result = Traits::low(result, *data); data += 1; size -= 1; } return result; } }; template struct wchar_selector; template <> struct wchar_selector<2> { typedef uint16_t type; typedef utf16_counter counter; typedef utf16_writer writer; typedef utf16_decoder decoder; }; template <> struct wchar_selector<4> { typedef uint32_t type; typedef utf32_counter counter; typedef utf32_writer writer; typedef utf32_decoder decoder; }; typedef wchar_selector::counter wchar_counter; typedef wchar_selector::writer wchar_writer; struct wchar_decoder { typedef wchar_t type; template static inline typename Traits::value_type process(const wchar_t* data, size_t size, typename Traits::value_type result, Traits traits) { typedef wchar_selector::decoder decoder; return decoder::process(reinterpret_cast(data), size, result, traits); } }; #ifdef PUGIXML_WCHAR_MODE PUGI_IMPL_FN void convert_wchar_endian_swap(wchar_t* result, const wchar_t* data, size_t length) { for (size_t i = 0; i < length; ++i) result[i] = static_cast(endian_swap(static_cast::type>(data[i]))); } #endif PUGI_IMPL_NS_END PUGI_IMPL_NS_BEGIN enum chartype_t { ct_parse_pcdata = 1, // \0, &, \r, < ct_parse_attr = 2, // \0, &, \r, ', " ct_parse_attr_ws = 4, // \0, &, \r, ', ", \n, tab ct_space = 8, // \r, \n, space, tab ct_parse_cdata = 16, // \0, ], >, \r ct_parse_comment = 32, // \0, -, >, \r ct_symbol = 64, // Any symbol > 127, a-z, A-Z, 0-9, _, :, -, . ct_start_symbol = 128 // Any symbol > 127, a-z, A-Z, _, : }; static const unsigned char chartype_table[256] = { 55, 0, 0, 0, 0, 0, 0, 0, 0, 12, 12, 0, 0, 63, 0, 0, // 0-15 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31 8, 0, 6, 0, 0, 0, 7, 6, 0, 0, 0, 0, 0, 96, 64, 0, // 32-47 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 192, 0, 1, 0, 48, 0, // 48-63 0, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, // 64-79 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 0, 0, 16, 0, 192, // 80-95 0, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, // 96-111 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 0, 0, 0, 0, 0, // 112-127 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, // 128+ 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192 }; enum chartypex_t { ctx_special_pcdata = 1, // Any symbol >= 0 and < 32 (except \t, \r, \n), &, <, > ctx_special_attr = 2, // Any symbol >= 0 and < 32, &, <, ", ' ctx_start_symbol = 4, // Any symbol > 127, a-z, A-Z, _ ctx_digit = 8, // 0-9 ctx_symbol = 16 // Any symbol > 127, a-z, A-Z, 0-9, _, -, . }; static const unsigned char chartypex_table[256] = { 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 3, 3, 2, 3, 3, // 0-15 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // 16-31 0, 0, 2, 0, 0, 0, 3, 2, 0, 0, 0, 0, 0, 16, 16, 0, // 32-47 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 0, 0, 3, 0, 1, 0, // 48-63 0, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, // 64-79 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 0, 0, 0, 0, 20, // 80-95 0, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, // 96-111 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 0, 0, 0, 0, 0, // 112-127 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, // 128+ 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20 }; #ifdef PUGIXML_WCHAR_MODE #define PUGI_IMPL_IS_CHARTYPE_IMPL(c, ct, table) ((static_cast(c) < 128 ? table[static_cast(c)] : table[128]) & (ct)) #else #define PUGI_IMPL_IS_CHARTYPE_IMPL(c, ct, table) (table[static_cast(c)] & (ct)) #endif #define PUGI_IMPL_IS_CHARTYPE(c, ct) PUGI_IMPL_IS_CHARTYPE_IMPL(c, ct, chartype_table) #define PUGI_IMPL_IS_CHARTYPEX(c, ct) PUGI_IMPL_IS_CHARTYPE_IMPL(c, ct, chartypex_table) PUGI_IMPL_FN bool is_little_endian() { unsigned int ui = 1; return *reinterpret_cast(&ui) == 1; } PUGI_IMPL_FN xml_encoding get_wchar_encoding() { PUGI_IMPL_STATIC_ASSERT(sizeof(wchar_t) == 2 || sizeof(wchar_t) == 4); if (sizeof(wchar_t) == 2) return is_little_endian() ? encoding_utf16_le : encoding_utf16_be; else return is_little_endian() ? encoding_utf32_le : encoding_utf32_be; } PUGI_IMPL_FN bool parse_declaration_encoding(const uint8_t* data, size_t size, const uint8_t*& out_encoding, size_t& out_length) { #define PUGI_IMPL_SCANCHAR(ch) { if (offset >= size || data[offset] != ch) return false; offset++; } #define PUGI_IMPL_SCANCHARTYPE(ct) { while (offset < size && PUGI_IMPL_IS_CHARTYPE(data[offset], ct)) offset++; } // check if we have a non-empty XML declaration if (size < 6 || !((data[0] == '<') & (data[1] == '?') & (data[2] == 'x') & (data[3] == 'm') & (data[4] == 'l') && PUGI_IMPL_IS_CHARTYPE(data[5], ct_space))) return false; // scan XML declaration until the encoding field for (size_t i = 6; i + 1 < size; ++i) { // declaration can not contain ? in quoted values if (data[i] == '?') return false; if (data[i] == 'e' && data[i + 1] == 'n') { size_t offset = i; // encoding follows the version field which can't contain 'en' so this has to be the encoding if XML is well formed PUGI_IMPL_SCANCHAR('e'); PUGI_IMPL_SCANCHAR('n'); PUGI_IMPL_SCANCHAR('c'); PUGI_IMPL_SCANCHAR('o'); PUGI_IMPL_SCANCHAR('d'); PUGI_IMPL_SCANCHAR('i'); PUGI_IMPL_SCANCHAR('n'); PUGI_IMPL_SCANCHAR('g'); // S? = S? PUGI_IMPL_SCANCHARTYPE(ct_space); PUGI_IMPL_SCANCHAR('='); PUGI_IMPL_SCANCHARTYPE(ct_space); // the only two valid delimiters are ' and " uint8_t delimiter = (offset < size && data[offset] == '"') ? '"' : '\''; PUGI_IMPL_SCANCHAR(delimiter); size_t start = offset; out_encoding = data + offset; PUGI_IMPL_SCANCHARTYPE(ct_symbol); out_length = offset - start; PUGI_IMPL_SCANCHAR(delimiter); return true; } } return false; #undef PUGI_IMPL_SCANCHAR #undef PUGI_IMPL_SCANCHARTYPE } PUGI_IMPL_FN xml_encoding guess_buffer_encoding(const uint8_t* data, size_t size) { // skip encoding autodetection if input buffer is too small if (size < 4) return encoding_utf8; uint8_t d0 = data[0], d1 = data[1], d2 = data[2], d3 = data[3]; // look for BOM in first few bytes if (d0 == 0 && d1 == 0 && d2 == 0xfe && d3 == 0xff) return encoding_utf32_be; if (d0 == 0xff && d1 == 0xfe && d2 == 0 && d3 == 0) return encoding_utf32_le; if (d0 == 0xfe && d1 == 0xff) return encoding_utf16_be; if (d0 == 0xff && d1 == 0xfe) return encoding_utf16_le; if (d0 == 0xef && d1 == 0xbb && d2 == 0xbf) return encoding_utf8; // look for <, (contents); return guess_buffer_encoding(data, size); } PUGI_IMPL_FN bool get_mutable_buffer(char_t*& out_buffer, size_t& out_length, const void* contents, size_t size, bool is_mutable) { size_t length = size / sizeof(char_t); if (is_mutable) { out_buffer = static_cast(const_cast(contents)); out_length = length; } else { char_t* buffer = static_cast(xml_memory::allocate((length + 1) * sizeof(char_t))); if (!buffer) return false; if (contents) memcpy(buffer, contents, length * sizeof(char_t)); else assert(length == 0); buffer[length] = 0; out_buffer = buffer; out_length = length + 1; } return true; } #ifdef PUGIXML_WCHAR_MODE PUGI_IMPL_FN bool need_endian_swap_utf(xml_encoding le, xml_encoding re) { return (le == encoding_utf16_be && re == encoding_utf16_le) || (le == encoding_utf16_le && re == encoding_utf16_be) || (le == encoding_utf32_be && re == encoding_utf32_le) || (le == encoding_utf32_le && re == encoding_utf32_be); } PUGI_IMPL_FN bool convert_buffer_endian_swap(char_t*& out_buffer, size_t& out_length, const void* contents, size_t size, bool is_mutable) { const char_t* data = static_cast(contents); size_t length = size / sizeof(char_t); if (is_mutable) { char_t* buffer = const_cast(data); convert_wchar_endian_swap(buffer, data, length); out_buffer = buffer; out_length = length; } else { char_t* buffer = static_cast(xml_memory::allocate((length + 1) * sizeof(char_t))); if (!buffer) return false; convert_wchar_endian_swap(buffer, data, length); buffer[length] = 0; out_buffer = buffer; out_length = length + 1; } return true; } template PUGI_IMPL_FN bool convert_buffer_generic(char_t*& out_buffer, size_t& out_length, const void* contents, size_t size, D) { const typename D::type* data = static_cast(contents); size_t data_length = size / sizeof(typename D::type); // first pass: get length in wchar_t units size_t length = D::process(data, data_length, 0, wchar_counter()); // allocate buffer of suitable length char_t* buffer = static_cast(xml_memory::allocate((length + 1) * sizeof(char_t))); if (!buffer) return false; // second pass: convert utf16 input to wchar_t wchar_writer::value_type obegin = reinterpret_cast(buffer); wchar_writer::value_type oend = D::process(data, data_length, obegin, wchar_writer()); assert(oend == obegin + length); *oend = 0; out_buffer = buffer; out_length = length + 1; return true; } PUGI_IMPL_FN bool convert_buffer(char_t*& out_buffer, size_t& out_length, xml_encoding encoding, const void* contents, size_t size, bool is_mutable) { // get native encoding xml_encoding wchar_encoding = get_wchar_encoding(); // fast path: no conversion required if (encoding == wchar_encoding) return get_mutable_buffer(out_buffer, out_length, contents, size, is_mutable); // only endian-swapping is required if (need_endian_swap_utf(encoding, wchar_encoding)) return convert_buffer_endian_swap(out_buffer, out_length, contents, size, is_mutable); // source encoding is utf8 if (encoding == encoding_utf8) return convert_buffer_generic(out_buffer, out_length, contents, size, utf8_decoder()); // source encoding is utf16 if (encoding == encoding_utf16_be || encoding == encoding_utf16_le) { xml_encoding native_encoding = is_little_endian() ? encoding_utf16_le : encoding_utf16_be; return (native_encoding == encoding) ? convert_buffer_generic(out_buffer, out_length, contents, size, utf16_decoder()) : convert_buffer_generic(out_buffer, out_length, contents, size, utf16_decoder()); } // source encoding is utf32 if (encoding == encoding_utf32_be || encoding == encoding_utf32_le) { xml_encoding native_encoding = is_little_endian() ? encoding_utf32_le : encoding_utf32_be; return (native_encoding == encoding) ? convert_buffer_generic(out_buffer, out_length, contents, size, utf32_decoder()) : convert_buffer_generic(out_buffer, out_length, contents, size, utf32_decoder()); } // source encoding is latin1 if (encoding == encoding_latin1) return convert_buffer_generic(out_buffer, out_length, contents, size, latin1_decoder()); assert(false && "Invalid encoding"); // unreachable return false; } #else template PUGI_IMPL_FN bool convert_buffer_generic(char_t*& out_buffer, size_t& out_length, const void* contents, size_t size, D) { const typename D::type* data = static_cast(contents); size_t data_length = size / sizeof(typename D::type); // first pass: get length in utf8 units size_t length = D::process(data, data_length, 0, utf8_counter()); // allocate buffer of suitable length char_t* buffer = static_cast(xml_memory::allocate((length + 1) * sizeof(char_t))); if (!buffer) return false; // second pass: convert utf16 input to utf8 uint8_t* obegin = reinterpret_cast(buffer); uint8_t* oend = D::process(data, data_length, obegin, utf8_writer()); assert(oend == obegin + length); *oend = 0; out_buffer = buffer; out_length = length + 1; return true; } PUGI_IMPL_FN size_t get_latin1_7bit_prefix_length(const uint8_t* data, size_t size) { for (size_t i = 0; i < size; ++i) if (data[i] > 127) return i; return size; } PUGI_IMPL_FN bool convert_buffer_latin1(char_t*& out_buffer, size_t& out_length, const void* contents, size_t size, bool is_mutable) { const uint8_t* data = static_cast(contents); size_t data_length = size; // get size of prefix that does not need utf8 conversion size_t prefix_length = get_latin1_7bit_prefix_length(data, data_length); assert(prefix_length <= data_length); const uint8_t* postfix = data + prefix_length; size_t postfix_length = data_length - prefix_length; // if no conversion is needed, just return the original buffer if (postfix_length == 0) return get_mutable_buffer(out_buffer, out_length, contents, size, is_mutable); // first pass: get length in utf8 units size_t length = prefix_length + latin1_decoder::process(postfix, postfix_length, 0, utf8_counter()); // allocate buffer of suitable length char_t* buffer = static_cast(xml_memory::allocate((length + 1) * sizeof(char_t))); if (!buffer) return false; // second pass: convert latin1 input to utf8 memcpy(buffer, data, prefix_length); uint8_t* obegin = reinterpret_cast(buffer); uint8_t* oend = latin1_decoder::process(postfix, postfix_length, obegin + prefix_length, utf8_writer()); assert(oend == obegin + length); *oend = 0; out_buffer = buffer; out_length = length + 1; return true; } PUGI_IMPL_FN bool convert_buffer(char_t*& out_buffer, size_t& out_length, xml_encoding encoding, const void* contents, size_t size, bool is_mutable) { // fast path: no conversion required if (encoding == encoding_utf8) return get_mutable_buffer(out_buffer, out_length, contents, size, is_mutable); // source encoding is utf16 if (encoding == encoding_utf16_be || encoding == encoding_utf16_le) { xml_encoding native_encoding = is_little_endian() ? encoding_utf16_le : encoding_utf16_be; return (native_encoding == encoding) ? convert_buffer_generic(out_buffer, out_length, contents, size, utf16_decoder()) : convert_buffer_generic(out_buffer, out_length, contents, size, utf16_decoder()); } // source encoding is utf32 if (encoding == encoding_utf32_be || encoding == encoding_utf32_le) { xml_encoding native_encoding = is_little_endian() ? encoding_utf32_le : encoding_utf32_be; return (native_encoding == encoding) ? convert_buffer_generic(out_buffer, out_length, contents, size, utf32_decoder()) : convert_buffer_generic(out_buffer, out_length, contents, size, utf32_decoder()); } // source encoding is latin1 if (encoding == encoding_latin1) return convert_buffer_latin1(out_buffer, out_length, contents, size, is_mutable); assert(false && "Invalid encoding"); // unreachable return false; } #endif PUGI_IMPL_FN size_t as_utf8_begin(const wchar_t* str, size_t length) { // get length in utf8 characters return wchar_decoder::process(str, length, 0, utf8_counter()); } PUGI_IMPL_FN void as_utf8_end(char* buffer, size_t size, const wchar_t* str, size_t length) { // convert to utf8 uint8_t* begin = reinterpret_cast(buffer); uint8_t* end = wchar_decoder::process(str, length, begin, utf8_writer()); assert(begin + size == end); (void)!end; (void)!size; } #ifndef PUGIXML_NO_STL PUGI_IMPL_FN std::string as_utf8_impl(const wchar_t* str, size_t length) { // first pass: get length in utf8 characters size_t size = as_utf8_begin(str, length); // allocate resulting string std::string result; result.resize(size); // second pass: convert to utf8 if (size > 0) as_utf8_end(&result[0], size, str, length); return result; } PUGI_IMPL_FN std::basic_string as_wide_impl(const char* str, size_t size) { const uint8_t* data = reinterpret_cast(str); // first pass: get length in wchar_t units size_t length = utf8_decoder::process(data, size, 0, wchar_counter()); // allocate resulting string std::basic_string result; result.resize(length); // second pass: convert to wchar_t if (length > 0) { wchar_writer::value_type begin = reinterpret_cast(&result[0]); wchar_writer::value_type end = utf8_decoder::process(data, size, begin, wchar_writer()); assert(begin + length == end); (void)!end; } return result; } #endif template inline bool strcpy_insitu_allow(size_t length, const Header& header, uintptr_t header_mask, char_t* target) { // never reuse shared memory if (header & xml_memory_page_contents_shared_mask) return false; size_t target_length = strlength(target); // always reuse document buffer memory if possible if ((header & header_mask) == 0) return target_length >= length; // reuse heap memory if waste is not too great const size_t reuse_threshold = 32; return target_length >= length && (target_length < reuse_threshold || target_length - length < target_length / 2); } template PUGI_IMPL_FN bool strcpy_insitu(String& dest, Header& header, uintptr_t header_mask, const char_t* source, size_t source_length) { assert((header & header_mask) == 0 || dest); // header bit indicates whether dest was previously allocated if (source_length == 0) { // empty string and null pointer are equivalent, so just deallocate old memory xml_allocator* alloc = PUGI_IMPL_GETPAGE_IMPL(header)->allocator; if (header & header_mask) alloc->deallocate_string(dest); // mark the string as not allocated dest = 0; header &= ~header_mask; return true; } else if (dest && strcpy_insitu_allow(source_length, header, header_mask, dest)) { // we can reuse old buffer, so just copy the new data (including zero terminator) memcpy(dest, source, source_length * sizeof(char_t)); dest[source_length] = 0; return true; } else { xml_allocator* alloc = PUGI_IMPL_GETPAGE_IMPL(header)->allocator; if (!alloc->reserve()) return false; // allocate new buffer char_t* buf = alloc->allocate_string(source_length + 1); if (!buf) return false; // copy the string (including zero terminator) memcpy(buf, source, source_length * sizeof(char_t)); buf[source_length] = 0; // deallocate old buffer (*after* the above to protect against overlapping memory and/or allocation failures) if (header & header_mask) alloc->deallocate_string(dest); // the string is now allocated, so set the flag dest = buf; header |= header_mask; return true; } } struct gap { char_t* end; size_t size; gap(): end(0), size(0) { } // Push new gap, move s count bytes further (skipping the gap). // Collapse previous gap. void push(char_t*& s, size_t count) { if (end) // there was a gap already; collapse it { // Move [old_gap_end, new_gap_start) to [old_gap_start, ...) assert(s >= end); memmove(end - size, end, reinterpret_cast(s) - reinterpret_cast(end)); } s += count; // end of current gap // "merge" two gaps end = s; size += count; } // Collapse all gaps, return past-the-end pointer char_t* flush(char_t* s) { if (end) { // Move [old_gap_end, current_pos) to [old_gap_start, ...) assert(s >= end); memmove(end - size, end, reinterpret_cast(s) - reinterpret_cast(end)); return s - size; } else return s; } }; PUGI_IMPL_FN char_t* strconv_escape(char_t* s, gap& g) { char_t* stre = s + 1; switch (*stre) { case '#': // &#... { unsigned int ucsc = 0; if (stre[1] == 'x') // &#x... (hex code) { stre += 2; char_t ch = *stre; if (ch == ';') return stre; for (;;) { if (static_cast(ch - '0') <= 9) ucsc = 16 * ucsc + (ch - '0'); else if (static_cast((ch | ' ') - 'a') <= 5) ucsc = 16 * ucsc + ((ch | ' ') - 'a' + 10); else if (ch == ';') break; else // cancel return stre; ch = *++stre; } ++stre; } else // &#... (dec code) { char_t ch = *++stre; if (ch == ';') return stre; for (;;) { if (static_cast(ch - '0') <= 9) ucsc = 10 * ucsc + (ch - '0'); else if (ch == ';') break; else // cancel return stre; ch = *++stre; } ++stre; } #ifdef PUGIXML_WCHAR_MODE s = reinterpret_cast(wchar_writer::any(reinterpret_cast(s), ucsc)); #else s = reinterpret_cast(utf8_writer::any(reinterpret_cast(s), ucsc)); #endif g.push(s, stre - s); return stre; } case 'a': // &a { ++stre; if (*stre == 'm') // &am { if (*++stre == 'p' && *++stre == ';') // & { *s++ = '&'; ++stre; g.push(s, stre - s); return stre; } } else if (*stre == 'p') // &ap { if (*++stre == 'o' && *++stre == 's' && *++stre == ';') // ' { *s++ = '\''; ++stre; g.push(s, stre - s); return stre; } } break; } case 'g': // &g { if (*++stre == 't' && *++stre == ';') // > { *s++ = '>'; ++stre; g.push(s, stre - s); return stre; } break; } case 'l': // &l { if (*++stre == 't' && *++stre == ';') // < { *s++ = '<'; ++stre; g.push(s, stre - s); return stre; } break; } case 'q': // &q { if (*++stre == 'u' && *++stre == 'o' && *++stre == 't' && *++stre == ';') // " { *s++ = '"'; ++stre; g.push(s, stre - s); return stre; } break; } default: break; } return stre; } // Parser utilities #define PUGI_IMPL_ENDSWITH(c, e) ((c) == (e) || ((c) == 0 && endch == (e))) #define PUGI_IMPL_SKIPWS() { while (PUGI_IMPL_IS_CHARTYPE(*s, ct_space)) ++s; } #define PUGI_IMPL_OPTSET(OPT) ( optmsk & (OPT) ) #define PUGI_IMPL_PUSHNODE(TYPE) { cursor = append_new_node(cursor, *alloc, TYPE); if (!cursor) PUGI_IMPL_THROW_ERROR(status_out_of_memory, s); } #define PUGI_IMPL_POPNODE() { cursor = cursor->parent; } #define PUGI_IMPL_SCANFOR(X) { while (*s != 0 && !(X)) ++s; } #define PUGI_IMPL_SCANWHILE(X) { while (X) ++s; } #define PUGI_IMPL_SCANWHILE_UNROLL(X) { for (;;) { char_t ss = s[0]; if (PUGI_IMPL_UNLIKELY(!(X))) { break; } ss = s[1]; if (PUGI_IMPL_UNLIKELY(!(X))) { s += 1; break; } ss = s[2]; if (PUGI_IMPL_UNLIKELY(!(X))) { s += 2; break; } ss = s[3]; if (PUGI_IMPL_UNLIKELY(!(X))) { s += 3; break; } s += 4; } } #define PUGI_IMPL_ENDSEG() { ch = *s; *s = 0; ++s; } #define PUGI_IMPL_THROW_ERROR(err, m) return error_offset = m, error_status = err, static_cast(0) #define PUGI_IMPL_CHECK_ERROR(err, m) { if (*s == 0) PUGI_IMPL_THROW_ERROR(err, m); } PUGI_IMPL_FN char_t* strconv_comment(char_t* s, char_t endch) { gap g; while (true) { PUGI_IMPL_SCANWHILE_UNROLL(!PUGI_IMPL_IS_CHARTYPE(ss, ct_parse_comment)); if (*s == '\r') // Either a single 0x0d or 0x0d 0x0a pair { *s++ = '\n'; // replace first one with 0x0a if (*s == '\n') g.push(s, 1); } else if (s[0] == '-' && s[1] == '-' && PUGI_IMPL_ENDSWITH(s[2], '>')) // comment ends here { *g.flush(s) = 0; return s + (s[2] == '>' ? 3 : 2); } else if (*s == 0) { return 0; } else ++s; } } PUGI_IMPL_FN char_t* strconv_cdata(char_t* s, char_t endch) { gap g; while (true) { PUGI_IMPL_SCANWHILE_UNROLL(!PUGI_IMPL_IS_CHARTYPE(ss, ct_parse_cdata)); if (*s == '\r') // Either a single 0x0d or 0x0d 0x0a pair { *s++ = '\n'; // replace first one with 0x0a if (*s == '\n') g.push(s, 1); } else if (s[0] == ']' && s[1] == ']' && PUGI_IMPL_ENDSWITH(s[2], '>')) // CDATA ends here { *g.flush(s) = 0; return s + 1; } else if (*s == 0) { return 0; } else ++s; } } typedef char_t* (*strconv_pcdata_t)(char_t*); template struct strconv_pcdata_impl { static char_t* parse(char_t* s) { gap g; char_t* begin = s; while (true) { PUGI_IMPL_SCANWHILE_UNROLL(!PUGI_IMPL_IS_CHARTYPE(ss, ct_parse_pcdata)); if (*s == '<') // PCDATA ends here { char_t* end = g.flush(s); if (opt_trim::value) while (end > begin && PUGI_IMPL_IS_CHARTYPE(end[-1], ct_space)) --end; *end = 0; return s + 1; } else if (opt_eol::value && *s == '\r') // Either a single 0x0d or 0x0d 0x0a pair { *s++ = '\n'; // replace first one with 0x0a if (*s == '\n') g.push(s, 1); } else if (opt_escape::value && *s == '&') { s = strconv_escape(s, g); } else if (*s == 0) { char_t* end = g.flush(s); if (opt_trim::value) while (end > begin && PUGI_IMPL_IS_CHARTYPE(end[-1], ct_space)) --end; *end = 0; return s; } else ++s; } } }; PUGI_IMPL_FN strconv_pcdata_t get_strconv_pcdata(unsigned int optmask) { PUGI_IMPL_STATIC_ASSERT(parse_escapes == 0x10 && parse_eol == 0x20 && parse_trim_pcdata == 0x0800); switch (((optmask >> 4) & 3) | ((optmask >> 9) & 4)) // get bitmask for flags (trim eol escapes); this simultaneously checks 3 options from assertion above { case 0: return strconv_pcdata_impl::parse; case 1: return strconv_pcdata_impl::parse; case 2: return strconv_pcdata_impl::parse; case 3: return strconv_pcdata_impl::parse; case 4: return strconv_pcdata_impl::parse; case 5: return strconv_pcdata_impl::parse; case 6: return strconv_pcdata_impl::parse; case 7: return strconv_pcdata_impl::parse; default: assert(false); return 0; // unreachable } } typedef char_t* (*strconv_attribute_t)(char_t*, char_t); template struct strconv_attribute_impl { static char_t* parse_wnorm(char_t* s, char_t end_quote) { gap g; // trim leading whitespaces if (PUGI_IMPL_IS_CHARTYPE(*s, ct_space)) { char_t* str = s; do ++str; while (PUGI_IMPL_IS_CHARTYPE(*str, ct_space)); g.push(s, str - s); } while (true) { PUGI_IMPL_SCANWHILE_UNROLL(!PUGI_IMPL_IS_CHARTYPE(ss, ct_parse_attr_ws | ct_space)); if (*s == end_quote) { char_t* str = g.flush(s); do *str-- = 0; while (PUGI_IMPL_IS_CHARTYPE(*str, ct_space)); return s + 1; } else if (PUGI_IMPL_IS_CHARTYPE(*s, ct_space)) { *s++ = ' '; if (PUGI_IMPL_IS_CHARTYPE(*s, ct_space)) { char_t* str = s + 1; while (PUGI_IMPL_IS_CHARTYPE(*str, ct_space)) ++str; g.push(s, str - s); } } else if (opt_escape::value && *s == '&') { s = strconv_escape(s, g); } else if (!*s) { return 0; } else ++s; } } static char_t* parse_wconv(char_t* s, char_t end_quote) { gap g; while (true) { PUGI_IMPL_SCANWHILE_UNROLL(!PUGI_IMPL_IS_CHARTYPE(ss, ct_parse_attr_ws)); if (*s == end_quote) { *g.flush(s) = 0; return s + 1; } else if (PUGI_IMPL_IS_CHARTYPE(*s, ct_space)) { if (*s == '\r') { *s++ = ' '; if (*s == '\n') g.push(s, 1); } else *s++ = ' '; } else if (opt_escape::value && *s == '&') { s = strconv_escape(s, g); } else if (!*s) { return 0; } else ++s; } } static char_t* parse_eol(char_t* s, char_t end_quote) { gap g; while (true) { PUGI_IMPL_SCANWHILE_UNROLL(!PUGI_IMPL_IS_CHARTYPE(ss, ct_parse_attr)); if (*s == end_quote) { *g.flush(s) = 0; return s + 1; } else if (*s == '\r') { *s++ = '\n'; if (*s == '\n') g.push(s, 1); } else if (opt_escape::value && *s == '&') { s = strconv_escape(s, g); } else if (!*s) { return 0; } else ++s; } } static char_t* parse_simple(char_t* s, char_t end_quote) { gap g; while (true) { PUGI_IMPL_SCANWHILE_UNROLL(!PUGI_IMPL_IS_CHARTYPE(ss, ct_parse_attr)); if (*s == end_quote) { *g.flush(s) = 0; return s + 1; } else if (opt_escape::value && *s == '&') { s = strconv_escape(s, g); } else if (!*s) { return 0; } else ++s; } } }; PUGI_IMPL_FN strconv_attribute_t get_strconv_attribute(unsigned int optmask) { PUGI_IMPL_STATIC_ASSERT(parse_escapes == 0x10 && parse_eol == 0x20 && parse_wconv_attribute == 0x40 && parse_wnorm_attribute == 0x80); switch ((optmask >> 4) & 15) // get bitmask for flags (wnorm wconv eol escapes); this simultaneously checks 4 options from assertion above { case 0: return strconv_attribute_impl::parse_simple; case 1: return strconv_attribute_impl::parse_simple; case 2: return strconv_attribute_impl::parse_eol; case 3: return strconv_attribute_impl::parse_eol; case 4: return strconv_attribute_impl::parse_wconv; case 5: return strconv_attribute_impl::parse_wconv; case 6: return strconv_attribute_impl::parse_wconv; case 7: return strconv_attribute_impl::parse_wconv; case 8: return strconv_attribute_impl::parse_wnorm; case 9: return strconv_attribute_impl::parse_wnorm; case 10: return strconv_attribute_impl::parse_wnorm; case 11: return strconv_attribute_impl::parse_wnorm; case 12: return strconv_attribute_impl::parse_wnorm; case 13: return strconv_attribute_impl::parse_wnorm; case 14: return strconv_attribute_impl::parse_wnorm; case 15: return strconv_attribute_impl::parse_wnorm; default: assert(false); return 0; // unreachable } } inline xml_parse_result make_parse_result(xml_parse_status status, ptrdiff_t offset = 0) { xml_parse_result result; result.status = status; result.offset = offset; return result; } struct xml_parser { xml_allocator* alloc; char_t* error_offset; xml_parse_status error_status; xml_parser(xml_allocator* alloc_): alloc(alloc_), error_offset(0), error_status(status_ok) { } // DOCTYPE consists of nested sections of the following possible types: // , , "...", '...' // // // First group can not contain nested groups // Second group can contain nested groups of the same type // Third group can contain all other groups char_t* parse_doctype_primitive(char_t* s) { if (*s == '"' || *s == '\'') { // quoted string char_t ch = *s++; PUGI_IMPL_SCANFOR(*s == ch); if (!*s) PUGI_IMPL_THROW_ERROR(status_bad_doctype, s); s++; } else if (s[0] == '<' && s[1] == '?') { // s += 2; PUGI_IMPL_SCANFOR(s[0] == '?' && s[1] == '>'); // no need for ENDSWITH because ?> can't terminate proper doctype if (!*s) PUGI_IMPL_THROW_ERROR(status_bad_doctype, s); s += 2; } else if (s[0] == '<' && s[1] == '!' && s[2] == '-' && s[3] == '-') { s += 4; PUGI_IMPL_SCANFOR(s[0] == '-' && s[1] == '-' && s[2] == '>'); // no need for ENDSWITH because --> can't terminate proper doctype if (!*s) PUGI_IMPL_THROW_ERROR(status_bad_doctype, s); s += 3; } else PUGI_IMPL_THROW_ERROR(status_bad_doctype, s); return s; } char_t* parse_doctype_ignore(char_t* s) { size_t depth = 0; assert(s[0] == '<' && s[1] == '!' && s[2] == '['); s += 3; while (*s) { if (s[0] == '<' && s[1] == '!' && s[2] == '[') { // nested ignore section s += 3; depth++; } else if (s[0] == ']' && s[1] == ']' && s[2] == '>') { // ignore section end s += 3; if (depth == 0) return s; depth--; } else s++; } PUGI_IMPL_THROW_ERROR(status_bad_doctype, s); } char_t* parse_doctype_group(char_t* s, char_t endch) { size_t depth = 0; assert((s[0] == '<' || s[0] == 0) && s[1] == '!'); s += 2; while (*s) { if (s[0] == '<' && s[1] == '!' && s[2] != '-') { if (s[2] == '[') { // ignore s = parse_doctype_ignore(s); if (!s) return s; } else { // some control group s += 2; depth++; } } else if (s[0] == '<' || s[0] == '"' || s[0] == '\'') { // unknown tag (forbidden), or some primitive group s = parse_doctype_primitive(s); if (!s) return s; } else if (*s == '>') { if (depth == 0) return s; depth--; s++; } else s++; } if (depth != 0 || endch != '>') PUGI_IMPL_THROW_ERROR(status_bad_doctype, s); return s; } char_t* parse_exclamation(char_t* s, xml_node_struct* cursor, unsigned int optmsk, char_t endch) { // parse node contents, starting with exclamation mark ++s; if (*s == '-') // 'value = s; // Save the offset. } if (PUGI_IMPL_OPTSET(parse_eol) && PUGI_IMPL_OPTSET(parse_comments)) { s = strconv_comment(s, endch); if (!s) PUGI_IMPL_THROW_ERROR(status_bad_comment, cursor->value); } else { // Scan for terminating '-->'. PUGI_IMPL_SCANFOR(s[0] == '-' && s[1] == '-' && PUGI_IMPL_ENDSWITH(s[2], '>')); PUGI_IMPL_CHECK_ERROR(status_bad_comment, s); if (PUGI_IMPL_OPTSET(parse_comments)) *s = 0; // Zero-terminate this segment at the first terminating '-'. s += (s[2] == '>' ? 3 : 2); // Step over the '\0->'. } } else PUGI_IMPL_THROW_ERROR(status_bad_comment, s); } else if (*s == '[') { // 'value = s; // Save the offset. if (PUGI_IMPL_OPTSET(parse_eol)) { s = strconv_cdata(s, endch); if (!s) PUGI_IMPL_THROW_ERROR(status_bad_cdata, cursor->value); } else { // Scan for terminating ']]>'. PUGI_IMPL_SCANFOR(s[0] == ']' && s[1] == ']' && PUGI_IMPL_ENDSWITH(s[2], '>')); PUGI_IMPL_CHECK_ERROR(status_bad_cdata, s); *s++ = 0; // Zero-terminate this segment. } } else // Flagged for discard, but we still have to scan for the terminator. { // Scan for terminating ']]>'. PUGI_IMPL_SCANFOR(s[0] == ']' && s[1] == ']' && PUGI_IMPL_ENDSWITH(s[2], '>')); PUGI_IMPL_CHECK_ERROR(status_bad_cdata, s); ++s; } s += (s[1] == '>' ? 2 : 1); // Step over the last ']>'. } else PUGI_IMPL_THROW_ERROR(status_bad_cdata, s); } else if (s[0] == 'D' && s[1] == 'O' && s[2] == 'C' && s[3] == 'T' && s[4] == 'Y' && s[5] == 'P' && PUGI_IMPL_ENDSWITH(s[6], 'E')) { s -= 2; if (cursor->parent) PUGI_IMPL_THROW_ERROR(status_bad_doctype, s); char_t* mark = s + 9; s = parse_doctype_group(s, endch); if (!s) return s; assert((*s == 0 && endch == '>') || *s == '>'); if (*s) *s++ = 0; if (PUGI_IMPL_OPTSET(parse_doctype)) { while (PUGI_IMPL_IS_CHARTYPE(*mark, ct_space)) ++mark; PUGI_IMPL_PUSHNODE(node_doctype); cursor->value = mark; } } else if (*s == 0 && endch == '-') PUGI_IMPL_THROW_ERROR(status_bad_comment, s); else if (*s == 0 && endch == '[') PUGI_IMPL_THROW_ERROR(status_bad_cdata, s); else PUGI_IMPL_THROW_ERROR(status_unrecognized_tag, s); return s; } char_t* parse_question(char_t* s, xml_node_struct*& ref_cursor, unsigned int optmsk, char_t endch) { // load into registers xml_node_struct* cursor = ref_cursor; char_t ch = 0; // parse node contents, starting with question mark ++s; // read PI target char_t* target = s; if (!PUGI_IMPL_IS_CHARTYPE(*s, ct_start_symbol)) PUGI_IMPL_THROW_ERROR(status_bad_pi, s); PUGI_IMPL_SCANWHILE(PUGI_IMPL_IS_CHARTYPE(*s, ct_symbol)); PUGI_IMPL_CHECK_ERROR(status_bad_pi, s); // determine node type; stricmp / strcasecmp is not portable bool declaration = (target[0] | ' ') == 'x' && (target[1] | ' ') == 'm' && (target[2] | ' ') == 'l' && target + 3 == s; if (declaration ? PUGI_IMPL_OPTSET(parse_declaration) : PUGI_IMPL_OPTSET(parse_pi)) { if (declaration) { // disallow non top-level declarations if (cursor->parent) PUGI_IMPL_THROW_ERROR(status_bad_pi, s); PUGI_IMPL_PUSHNODE(node_declaration); } else { PUGI_IMPL_PUSHNODE(node_pi); } cursor->name = target; PUGI_IMPL_ENDSEG(); // parse value/attributes if (ch == '?') { // empty node if (!PUGI_IMPL_ENDSWITH(*s, '>')) PUGI_IMPL_THROW_ERROR(status_bad_pi, s); s += (*s == '>'); PUGI_IMPL_POPNODE(); } else if (PUGI_IMPL_IS_CHARTYPE(ch, ct_space)) { PUGI_IMPL_SKIPWS(); // scan for tag end char_t* value = s; PUGI_IMPL_SCANFOR(s[0] == '?' && PUGI_IMPL_ENDSWITH(s[1], '>')); PUGI_IMPL_CHECK_ERROR(status_bad_pi, s); if (declaration) { // replace ending ? with / so that 'element' terminates properly *s = '/'; // we exit from this function with cursor at node_declaration, which is a signal to parse() to go to LOC_ATTRIBUTES s = value; } else { // store value and step over > cursor->value = value; PUGI_IMPL_POPNODE(); PUGI_IMPL_ENDSEG(); s += (*s == '>'); } } else PUGI_IMPL_THROW_ERROR(status_bad_pi, s); } else { // scan for tag end PUGI_IMPL_SCANFOR(s[0] == '?' && PUGI_IMPL_ENDSWITH(s[1], '>')); PUGI_IMPL_CHECK_ERROR(status_bad_pi, s); s += (s[1] == '>' ? 2 : 1); } // store from registers ref_cursor = cursor; return s; } char_t* parse_tree(char_t* s, xml_node_struct* root, unsigned int optmsk, char_t endch) { strconv_attribute_t strconv_attribute = get_strconv_attribute(optmsk); strconv_pcdata_t strconv_pcdata = get_strconv_pcdata(optmsk); char_t ch = 0; xml_node_struct* cursor = root; char_t* mark = s; char_t* merged_pcdata = s; while (*s != 0) { if (*s == '<') { ++s; LOC_TAG: if (PUGI_IMPL_IS_CHARTYPE(*s, ct_start_symbol)) // '<#...' { PUGI_IMPL_PUSHNODE(node_element); // Append a new node to the tree. cursor->name = s; PUGI_IMPL_SCANWHILE_UNROLL(PUGI_IMPL_IS_CHARTYPE(ss, ct_symbol)); // Scan for a terminator. PUGI_IMPL_ENDSEG(); // Save char in 'ch', terminate & step over. if (ch == '>') { // end of tag } else if (PUGI_IMPL_IS_CHARTYPE(ch, ct_space)) { LOC_ATTRIBUTES: while (true) { PUGI_IMPL_SKIPWS(); // Eat any whitespace. if (PUGI_IMPL_IS_CHARTYPE(*s, ct_start_symbol)) // <... #... { xml_attribute_struct* a = append_new_attribute(cursor, *alloc); // Make space for this attribute. if (!a) PUGI_IMPL_THROW_ERROR(status_out_of_memory, s); a->name = s; // Save the offset. PUGI_IMPL_SCANWHILE_UNROLL(PUGI_IMPL_IS_CHARTYPE(ss, ct_symbol)); // Scan for a terminator. PUGI_IMPL_ENDSEG(); // Save char in 'ch', terminate & step over. if (PUGI_IMPL_IS_CHARTYPE(ch, ct_space)) { PUGI_IMPL_SKIPWS(); // Eat any whitespace. ch = *s; ++s; } if (ch == '=') // '<... #=...' { PUGI_IMPL_SKIPWS(); // Eat any whitespace. if (*s == '"' || *s == '\'') // '<... #="...' { ch = *s; // Save quote char to avoid breaking on "''" -or- '""'. ++s; // Step over the quote. a->value = s; // Save the offset. s = strconv_attribute(s, ch); if (!s) PUGI_IMPL_THROW_ERROR(status_bad_attribute, a->value); // After this line the loop continues from the start; // Whitespaces, / and > are ok, symbols and EOF are wrong, // everything else will be detected if (PUGI_IMPL_IS_CHARTYPE(*s, ct_start_symbol)) PUGI_IMPL_THROW_ERROR(status_bad_attribute, s); } else PUGI_IMPL_THROW_ERROR(status_bad_attribute, s); } else PUGI_IMPL_THROW_ERROR(status_bad_attribute, s); } else if (*s == '/') { ++s; if (*s == '>') { PUGI_IMPL_POPNODE(); s++; break; } else if (*s == 0 && endch == '>') { PUGI_IMPL_POPNODE(); break; } else PUGI_IMPL_THROW_ERROR(status_bad_start_element, s); } else if (*s == '>') { ++s; break; } else if (*s == 0 && endch == '>') { break; } else PUGI_IMPL_THROW_ERROR(status_bad_start_element, s); } // !!! } else if (ch == '/') // '<#.../' { if (!PUGI_IMPL_ENDSWITH(*s, '>')) PUGI_IMPL_THROW_ERROR(status_bad_start_element, s); PUGI_IMPL_POPNODE(); // Pop. s += (*s == '>'); } else if (ch == 0) { // we stepped over null terminator, backtrack & handle closing tag --s; if (endch != '>') PUGI_IMPL_THROW_ERROR(status_bad_start_element, s); } else PUGI_IMPL_THROW_ERROR(status_bad_start_element, s); } else if (*s == '/') { ++s; mark = s; char_t* name = cursor->name; if (!name) PUGI_IMPL_THROW_ERROR(status_end_element_mismatch, mark); while (PUGI_IMPL_IS_CHARTYPE(*s, ct_symbol)) { if (*s++ != *name++) PUGI_IMPL_THROW_ERROR(status_end_element_mismatch, mark); } if (*name) { if (*s == 0 && name[0] == endch && name[1] == 0) PUGI_IMPL_THROW_ERROR(status_bad_end_element, s); else PUGI_IMPL_THROW_ERROR(status_end_element_mismatch, mark); } PUGI_IMPL_POPNODE(); // Pop. PUGI_IMPL_SKIPWS(); if (*s == 0) { if (endch != '>') PUGI_IMPL_THROW_ERROR(status_bad_end_element, s); } else { if (*s != '>') PUGI_IMPL_THROW_ERROR(status_bad_end_element, s); ++s; } } else if (*s == '?') // 'first_child) continue; } } if (!PUGI_IMPL_OPTSET(parse_trim_pcdata)) s = mark; if (cursor->parent || PUGI_IMPL_OPTSET(parse_fragment)) { char_t* parsed_pcdata = s; s = strconv_pcdata(s); if (PUGI_IMPL_OPTSET(parse_embed_pcdata) && cursor->parent && !cursor->first_child && !cursor->value) { cursor->value = parsed_pcdata; // Save the offset. } else if (PUGI_IMPL_OPTSET(parse_merge_pcdata) && cursor->first_child && PUGI_IMPL_NODETYPE(cursor->first_child->prev_sibling_c) == node_pcdata) { assert(merged_pcdata >= cursor->first_child->prev_sibling_c->value); // Catch up to the end of last parsed value; only needed for the first fragment. merged_pcdata += strlength(merged_pcdata); size_t length = strlength(parsed_pcdata); // Must use memmove instead of memcpy as this move may overlap memmove(merged_pcdata, parsed_pcdata, (length + 1) * sizeof(char_t)); merged_pcdata += length; } else { xml_node_struct* prev_cursor = cursor; PUGI_IMPL_PUSHNODE(node_pcdata); // Append a new node on the tree. cursor->value = parsed_pcdata; // Save the offset. merged_pcdata = parsed_pcdata; // Used for parse_merge_pcdata above, cheaper to save unconditionally cursor = prev_cursor; // Pop since this is a standalone. } if (!*s) break; } else { PUGI_IMPL_SCANFOR(*s == '<'); // '...<' if (!*s) break; ++s; } // We're after '<' goto LOC_TAG; } } // check that last tag is closed if (cursor != root) PUGI_IMPL_THROW_ERROR(status_end_element_mismatch, s); return s; } #ifdef PUGIXML_WCHAR_MODE static char_t* parse_skip_bom(char_t* s) { unsigned int bom = 0xfeff; return (s[0] == static_cast(bom)) ? s + 1 : s; } #else static char_t* parse_skip_bom(char_t* s) { return (s[0] == '\xef' && s[1] == '\xbb' && s[2] == '\xbf') ? s + 3 : s; } #endif static bool has_element_node_siblings(xml_node_struct* node) { while (node) { if (PUGI_IMPL_NODETYPE(node) == node_element) return true; node = node->next_sibling; } return false; } static xml_parse_result parse(char_t* buffer, size_t length, xml_document_struct* xmldoc, xml_node_struct* root, unsigned int optmsk) { // early-out for empty documents if (length == 0) return make_parse_result(PUGI_IMPL_OPTSET(parse_fragment) ? status_ok : status_no_document_element); // get last child of the root before parsing xml_node_struct* last_root_child = root->first_child ? root->first_child->prev_sibling_c + 0 : 0; // create parser on stack xml_parser parser(static_cast(xmldoc)); // save last character and make buffer zero-terminated (speeds up parsing) char_t endch = buffer[length - 1]; buffer[length - 1] = 0; // skip BOM to make sure it does not end up as part of parse output char_t* buffer_data = parse_skip_bom(buffer); // perform actual parsing parser.parse_tree(buffer_data, root, optmsk, endch); xml_parse_result result = make_parse_result(parser.error_status, parser.error_offset ? parser.error_offset - buffer : 0); assert(result.offset >= 0 && static_cast(result.offset) <= length); if (result) { // since we removed last character, we have to handle the only possible false positive (stray <) if (endch == '<') return make_parse_result(status_unrecognized_tag, length - 1); // check if there are any element nodes parsed xml_node_struct* first_root_child_parsed = last_root_child ? last_root_child->next_sibling + 0 : root->first_child + 0; if (!PUGI_IMPL_OPTSET(parse_fragment) && !has_element_node_siblings(first_root_child_parsed)) return make_parse_result(status_no_document_element, length - 1); } else { // roll back offset if it occurs on a null terminator in the source buffer if (result.offset > 0 && static_cast(result.offset) == length - 1 && endch == 0) result.offset--; } return result; } }; // Output facilities PUGI_IMPL_FN xml_encoding get_write_native_encoding() { #ifdef PUGIXML_WCHAR_MODE return get_wchar_encoding(); #else return encoding_utf8; #endif } PUGI_IMPL_FN xml_encoding get_write_encoding(xml_encoding encoding) { // replace wchar encoding with utf implementation if (encoding == encoding_wchar) return get_wchar_encoding(); // replace utf16 encoding with utf16 with specific endianness if (encoding == encoding_utf16) return is_little_endian() ? encoding_utf16_le : encoding_utf16_be; // replace utf32 encoding with utf32 with specific endianness if (encoding == encoding_utf32) return is_little_endian() ? encoding_utf32_le : encoding_utf32_be; // only do autodetection if no explicit encoding is requested if (encoding != encoding_auto) return encoding; // assume utf8 encoding return encoding_utf8; } template PUGI_IMPL_FN size_t convert_buffer_output_generic(typename T::value_type dest, const char_t* data, size_t length, D, T) { PUGI_IMPL_STATIC_ASSERT(sizeof(char_t) == sizeof(typename D::type)); typename T::value_type end = D::process(reinterpret_cast(data), length, dest, T()); return static_cast(end - dest) * sizeof(*dest); } template PUGI_IMPL_FN size_t convert_buffer_output_generic(typename T::value_type dest, const char_t* data, size_t length, D, T, bool opt_swap) { PUGI_IMPL_STATIC_ASSERT(sizeof(char_t) == sizeof(typename D::type)); typename T::value_type end = D::process(reinterpret_cast(data), length, dest, T()); if (opt_swap) { for (typename T::value_type i = dest; i != end; ++i) *i = endian_swap(*i); } return static_cast(end - dest) * sizeof(*dest); } #ifdef PUGIXML_WCHAR_MODE PUGI_IMPL_FN size_t get_valid_length(const char_t* data, size_t length) { if (length < 1) return 0; // discard last character if it's the lead of a surrogate pair return (sizeof(wchar_t) == 2 && static_cast(static_cast(data[length - 1]) - 0xD800) < 0x400) ? length - 1 : length; } PUGI_IMPL_FN size_t convert_buffer_output(char_t* r_char, uint8_t* r_u8, uint16_t* r_u16, uint32_t* r_u32, const char_t* data, size_t length, xml_encoding encoding) { // only endian-swapping is required if (need_endian_swap_utf(encoding, get_wchar_encoding())) { convert_wchar_endian_swap(r_char, data, length); return length * sizeof(char_t); } // convert to utf8 if (encoding == encoding_utf8) return convert_buffer_output_generic(r_u8, data, length, wchar_decoder(), utf8_writer()); // convert to utf16 if (encoding == encoding_utf16_be || encoding == encoding_utf16_le) { xml_encoding native_encoding = is_little_endian() ? encoding_utf16_le : encoding_utf16_be; return convert_buffer_output_generic(r_u16, data, length, wchar_decoder(), utf16_writer(), native_encoding != encoding); } // convert to utf32 if (encoding == encoding_utf32_be || encoding == encoding_utf32_le) { xml_encoding native_encoding = is_little_endian() ? encoding_utf32_le : encoding_utf32_be; return convert_buffer_output_generic(r_u32, data, length, wchar_decoder(), utf32_writer(), native_encoding != encoding); } // convert to latin1 if (encoding == encoding_latin1) return convert_buffer_output_generic(r_u8, data, length, wchar_decoder(), latin1_writer()); assert(false && "Invalid encoding"); // unreachable return 0; } #else PUGI_IMPL_FN size_t get_valid_length(const char_t* data, size_t length) { if (length < 5) return 0; for (size_t i = 1; i <= 4; ++i) { uint8_t ch = static_cast(data[length - i]); // either a standalone character or a leading one if ((ch & 0xc0) != 0x80) return length - i; } // there are four non-leading characters at the end, sequence tail is broken so might as well process the whole chunk return length; } PUGI_IMPL_FN size_t convert_buffer_output(char_t* /* r_char */, uint8_t* r_u8, uint16_t* r_u16, uint32_t* r_u32, const char_t* data, size_t length, xml_encoding encoding) { if (encoding == encoding_utf16_be || encoding == encoding_utf16_le) { xml_encoding native_encoding = is_little_endian() ? encoding_utf16_le : encoding_utf16_be; return convert_buffer_output_generic(r_u16, data, length, utf8_decoder(), utf16_writer(), native_encoding != encoding); } if (encoding == encoding_utf32_be || encoding == encoding_utf32_le) { xml_encoding native_encoding = is_little_endian() ? encoding_utf32_le : encoding_utf32_be; return convert_buffer_output_generic(r_u32, data, length, utf8_decoder(), utf32_writer(), native_encoding != encoding); } if (encoding == encoding_latin1) return convert_buffer_output_generic(r_u8, data, length, utf8_decoder(), latin1_writer()); assert(false && "Invalid encoding"); // unreachable return 0; } #endif class xml_buffered_writer { xml_buffered_writer(const xml_buffered_writer&); xml_buffered_writer& operator=(const xml_buffered_writer&); public: xml_buffered_writer(xml_writer& writer_, xml_encoding user_encoding): writer(writer_), bufsize(0), encoding(get_write_encoding(user_encoding)) { PUGI_IMPL_STATIC_ASSERT(bufcapacity >= 8); } size_t flush() { flush(buffer, bufsize); bufsize = 0; return 0; } void flush(const char_t* data, size_t size) { if (size == 0) return; // fast path, just write data if (encoding == get_write_native_encoding()) writer.write(data, size * sizeof(char_t)); else { // convert chunk size_t result = convert_buffer_output(scratch.data_char, scratch.data_u8, scratch.data_u16, scratch.data_u32, data, size, encoding); assert(result <= sizeof(scratch)); // write data writer.write(scratch.data_u8, result); } } void write_direct(const char_t* data, size_t length) { // flush the remaining buffer contents flush(); // handle large chunks if (length > bufcapacity) { if (encoding == get_write_native_encoding()) { // fast path, can just write data chunk writer.write(data, length * sizeof(char_t)); return; } // need to convert in suitable chunks while (length > bufcapacity) { // get chunk size by selecting such number of characters that are guaranteed to fit into scratch buffer // and form a complete codepoint sequence (i.e. discard start of last codepoint if necessary) size_t chunk_size = get_valid_length(data, bufcapacity); assert(chunk_size); // convert chunk and write flush(data, chunk_size); // iterate data += chunk_size; length -= chunk_size; } // small tail is copied below bufsize = 0; } memcpy(buffer + bufsize, data, length * sizeof(char_t)); bufsize += length; } void write_buffer(const char_t* data, size_t length) { size_t offset = bufsize; if (offset + length <= bufcapacity) { memcpy(buffer + offset, data, length * sizeof(char_t)); bufsize = offset + length; } else { write_direct(data, length); } } void write_string(const char_t* data) { // write the part of the string that fits in the buffer size_t offset = bufsize; while (*data && offset < bufcapacity) buffer[offset++] = *data++; // write the rest if (offset < bufcapacity) { bufsize = offset; } else { // backtrack a bit if we have split the codepoint size_t length = offset - bufsize; size_t extra = length - get_valid_length(data - length, length); bufsize = offset - extra; write_direct(data - extra, strlength(data) + extra); } } void write(char_t d0) { size_t offset = bufsize; if (offset > bufcapacity - 1) offset = flush(); buffer[offset + 0] = d0; bufsize = offset + 1; } void write(char_t d0, char_t d1) { size_t offset = bufsize; if (offset > bufcapacity - 2) offset = flush(); buffer[offset + 0] = d0; buffer[offset + 1] = d1; bufsize = offset + 2; } void write(char_t d0, char_t d1, char_t d2) { size_t offset = bufsize; if (offset > bufcapacity - 3) offset = flush(); buffer[offset + 0] = d0; buffer[offset + 1] = d1; buffer[offset + 2] = d2; bufsize = offset + 3; } void write(char_t d0, char_t d1, char_t d2, char_t d3) { size_t offset = bufsize; if (offset > bufcapacity - 4) offset = flush(); buffer[offset + 0] = d0; buffer[offset + 1] = d1; buffer[offset + 2] = d2; buffer[offset + 3] = d3; bufsize = offset + 4; } void write(char_t d0, char_t d1, char_t d2, char_t d3, char_t d4) { size_t offset = bufsize; if (offset > bufcapacity - 5) offset = flush(); buffer[offset + 0] = d0; buffer[offset + 1] = d1; buffer[offset + 2] = d2; buffer[offset + 3] = d3; buffer[offset + 4] = d4; bufsize = offset + 5; } void write(char_t d0, char_t d1, char_t d2, char_t d3, char_t d4, char_t d5) { size_t offset = bufsize; if (offset > bufcapacity - 6) offset = flush(); buffer[offset + 0] = d0; buffer[offset + 1] = d1; buffer[offset + 2] = d2; buffer[offset + 3] = d3; buffer[offset + 4] = d4; buffer[offset + 5] = d5; bufsize = offset + 6; } // utf8 maximum expansion: x4 (-> utf32) // utf16 maximum expansion: x2 (-> utf32) // utf32 maximum expansion: x1 enum { bufcapacitybytes = #ifdef PUGIXML_MEMORY_OUTPUT_STACK PUGIXML_MEMORY_OUTPUT_STACK #else 10240 #endif , bufcapacity = bufcapacitybytes / (sizeof(char_t) + 4) }; char_t buffer[bufcapacity]; union { uint8_t data_u8[4 * bufcapacity]; uint16_t data_u16[2 * bufcapacity]; uint32_t data_u32[bufcapacity]; char_t data_char[bufcapacity]; } scratch; xml_writer& writer; size_t bufsize; xml_encoding encoding; }; PUGI_IMPL_FN void text_output_escaped(xml_buffered_writer& writer, const char_t* s, chartypex_t type, unsigned int flags) { while (*s) { const char_t* prev = s; // While *s is a usual symbol PUGI_IMPL_SCANWHILE_UNROLL(!PUGI_IMPL_IS_CHARTYPEX(ss, type)); writer.write_buffer(prev, static_cast(s - prev)); switch (*s) { case 0: break; case '&': writer.write('&', 'a', 'm', 'p', ';'); ++s; break; case '<': writer.write('&', 'l', 't', ';'); ++s; break; case '>': writer.write('&', 'g', 't', ';'); ++s; break; case '"': if (flags & format_attribute_single_quote) writer.write('"'); else writer.write('&', 'q', 'u', 'o', 't', ';'); ++s; break; case '\'': if (flags & format_attribute_single_quote) writer.write('&', 'a', 'p', 'o', 's', ';'); else writer.write('\''); ++s; break; default: // s is not a usual symbol { unsigned int ch = static_cast(*s++); assert(ch < 32); if (!(flags & format_skip_control_chars)) writer.write('&', '#', static_cast((ch / 10) + '0'), static_cast((ch % 10) + '0'), ';'); } } } } PUGI_IMPL_FN void text_output(xml_buffered_writer& writer, const char_t* s, chartypex_t type, unsigned int flags) { if (flags & format_no_escapes) writer.write_string(s); else text_output_escaped(writer, s, type, flags); } PUGI_IMPL_FN void text_output_cdata(xml_buffered_writer& writer, const char_t* s) { do { writer.write('<', '!', '[', 'C', 'D'); writer.write('A', 'T', 'A', '['); const char_t* prev = s; // look for ]]> sequence - we can't output it as is since it terminates CDATA while (*s && !(s[0] == ']' && s[1] == ']' && s[2] == '>')) ++s; // skip ]] if we stopped at ]]>, > will go to the next CDATA section if (*s) s += 2; writer.write_buffer(prev, static_cast(s - prev)); writer.write(']', ']', '>'); } while (*s); } PUGI_IMPL_FN void text_output_indent(xml_buffered_writer& writer, const char_t* indent, size_t indent_length, unsigned int depth) { switch (indent_length) { case 1: { for (unsigned int i = 0; i < depth; ++i) writer.write(indent[0]); break; } case 2: { for (unsigned int i = 0; i < depth; ++i) writer.write(indent[0], indent[1]); break; } case 3: { for (unsigned int i = 0; i < depth; ++i) writer.write(indent[0], indent[1], indent[2]); break; } case 4: { for (unsigned int i = 0; i < depth; ++i) writer.write(indent[0], indent[1], indent[2], indent[3]); break; } default: { for (unsigned int i = 0; i < depth; ++i) writer.write_buffer(indent, indent_length); } } } PUGI_IMPL_FN void node_output_comment(xml_buffered_writer& writer, const char_t* s) { writer.write('<', '!', '-', '-'); while (*s) { const char_t* prev = s; // look for -\0 or -- sequence - we can't output it since -- is illegal in comment body while (*s && !(s[0] == '-' && (s[1] == '-' || s[1] == 0))) ++s; writer.write_buffer(prev, static_cast(s - prev)); if (*s) { assert(*s == '-'); writer.write('-', ' '); ++s; } } writer.write('-', '-', '>'); } PUGI_IMPL_FN void node_output_pi_value(xml_buffered_writer& writer, const char_t* s) { while (*s) { const char_t* prev = s; // look for ?> sequence - we can't output it since ?> terminates PI while (*s && !(s[0] == '?' && s[1] == '>')) ++s; writer.write_buffer(prev, static_cast(s - prev)); if (*s) { assert(s[0] == '?' && s[1] == '>'); writer.write('?', ' ', '>'); s += 2; } } } PUGI_IMPL_FN void node_output_attributes(xml_buffered_writer& writer, xml_node_struct* node, const char_t* indent, size_t indent_length, unsigned int flags, unsigned int depth) { const char_t* default_name = PUGIXML_TEXT(":anonymous"); const char_t enquotation_char = (flags & format_attribute_single_quote) ? '\'' : '"'; for (xml_attribute_struct* a = node->first_attribute; a; a = a->next_attribute) { if ((flags & (format_indent_attributes | format_raw)) == format_indent_attributes) { writer.write('\n'); text_output_indent(writer, indent, indent_length, depth + 1); } else { writer.write(' '); } writer.write_string(a->name ? a->name + 0 : default_name); writer.write('=', enquotation_char); if (a->value) text_output(writer, a->value, ctx_special_attr, flags); writer.write(enquotation_char); } } PUGI_IMPL_FN bool node_output_start(xml_buffered_writer& writer, xml_node_struct* node, const char_t* indent, size_t indent_length, unsigned int flags, unsigned int depth) { const char_t* default_name = PUGIXML_TEXT(":anonymous"); const char_t* name = node->name ? node->name + 0 : default_name; writer.write('<'); writer.write_string(name); if (node->first_attribute) node_output_attributes(writer, node, indent, indent_length, flags, depth); // element nodes can have value if parse_embed_pcdata was used if (!node->value) { if (!node->first_child) { if (flags & format_no_empty_element_tags) { writer.write('>', '<', '/'); writer.write_string(name); writer.write('>'); return false; } else { if ((flags & format_raw) == 0) writer.write(' '); writer.write('/', '>'); return false; } } else { writer.write('>'); return true; } } else { writer.write('>'); text_output(writer, node->value, ctx_special_pcdata, flags); if (!node->first_child) { writer.write('<', '/'); writer.write_string(name); writer.write('>'); return false; } else { return true; } } } PUGI_IMPL_FN void node_output_end(xml_buffered_writer& writer, xml_node_struct* node) { const char_t* default_name = PUGIXML_TEXT(":anonymous"); const char_t* name = node->name ? node->name + 0 : default_name; writer.write('<', '/'); writer.write_string(name); writer.write('>'); } PUGI_IMPL_FN void node_output_simple(xml_buffered_writer& writer, xml_node_struct* node, unsigned int flags) { const char_t* default_name = PUGIXML_TEXT(":anonymous"); switch (PUGI_IMPL_NODETYPE(node)) { case node_pcdata: text_output(writer, node->value ? node->value + 0 : PUGIXML_TEXT(""), ctx_special_pcdata, flags); break; case node_cdata: text_output_cdata(writer, node->value ? node->value + 0 : PUGIXML_TEXT("")); break; case node_comment: node_output_comment(writer, node->value ? node->value + 0 : PUGIXML_TEXT("")); break; case node_pi: writer.write('<', '?'); writer.write_string(node->name ? node->name + 0 : default_name); if (node->value) { writer.write(' '); node_output_pi_value(writer, node->value); } writer.write('?', '>'); break; case node_declaration: writer.write('<', '?'); writer.write_string(node->name ? node->name + 0 : default_name); node_output_attributes(writer, node, PUGIXML_TEXT(""), 0, flags | format_raw, 0); writer.write('?', '>'); break; case node_doctype: writer.write('<', '!', 'D', 'O', 'C'); writer.write('T', 'Y', 'P', 'E'); if (node->value) { writer.write(' '); writer.write_string(node->value); } writer.write('>'); break; default: assert(false && "Invalid node type"); // unreachable } } enum indent_flags_t { indent_newline = 1, indent_indent = 2 }; PUGI_IMPL_FN void node_output(xml_buffered_writer& writer, xml_node_struct* root, const char_t* indent, unsigned int flags, unsigned int depth) { size_t indent_length = ((flags & (format_indent | format_indent_attributes)) && (flags & format_raw) == 0) ? strlength(indent) : 0; unsigned int indent_flags = indent_indent; xml_node_struct* node = root; do { assert(node); // begin writing current node if (PUGI_IMPL_NODETYPE(node) == node_pcdata || PUGI_IMPL_NODETYPE(node) == node_cdata) { node_output_simple(writer, node, flags); indent_flags = 0; } else { if ((indent_flags & indent_newline) && (flags & format_raw) == 0) writer.write('\n'); if ((indent_flags & indent_indent) && indent_length) text_output_indent(writer, indent, indent_length, depth); if (PUGI_IMPL_NODETYPE(node) == node_element) { indent_flags = indent_newline | indent_indent; if (node_output_start(writer, node, indent, indent_length, flags, depth)) { // element nodes can have value if parse_embed_pcdata was used if (node->value) indent_flags = 0; node = node->first_child; depth++; continue; } } else if (PUGI_IMPL_NODETYPE(node) == node_document) { indent_flags = indent_indent; if (node->first_child) { node = node->first_child; continue; } } else { node_output_simple(writer, node, flags); indent_flags = indent_newline | indent_indent; } } // continue to the next node while (node != root) { if (node->next_sibling) { node = node->next_sibling; break; } node = node->parent; // write closing node if (PUGI_IMPL_NODETYPE(node) == node_element) { depth--; if ((indent_flags & indent_newline) && (flags & format_raw) == 0) writer.write('\n'); if ((indent_flags & indent_indent) && indent_length) text_output_indent(writer, indent, indent_length, depth); node_output_end(writer, node); indent_flags = indent_newline | indent_indent; } } } while (node != root); if ((indent_flags & indent_newline) && (flags & format_raw) == 0) writer.write('\n'); } PUGI_IMPL_FN bool has_declaration(xml_node_struct* node) { for (xml_node_struct* child = node->first_child; child; child = child->next_sibling) { xml_node_type type = PUGI_IMPL_NODETYPE(child); if (type == node_declaration) return true; if (type == node_element) return false; } return false; } PUGI_IMPL_FN bool is_attribute_of(xml_attribute_struct* attr, xml_node_struct* node) { for (xml_attribute_struct* a = node->first_attribute; a; a = a->next_attribute) if (a == attr) return true; return false; } PUGI_IMPL_FN bool allow_insert_attribute(xml_node_type parent) { return parent == node_element || parent == node_declaration; } PUGI_IMPL_FN bool allow_insert_child(xml_node_type parent, xml_node_type child) { if (parent != node_document && parent != node_element) return false; if (child == node_document || child == node_null) return false; if (parent != node_document && (child == node_declaration || child == node_doctype)) return false; return true; } PUGI_IMPL_FN bool allow_move(xml_node parent, xml_node child) { // check that child can be a child of parent if (!allow_insert_child(parent.type(), child.type())) return false; // check that node is not moved between documents if (parent.root() != child.root()) return false; // check that new parent is not in the child subtree xml_node cur = parent; while (cur) { if (cur == child) return false; cur = cur.parent(); } return true; } template PUGI_IMPL_FN void node_copy_string(String& dest, Header& header, uintptr_t header_mask, char_t* source, Header& source_header, xml_allocator* alloc) { assert(!dest && (header & header_mask) == 0); // copies are performed into fresh nodes if (source) { if (alloc && (source_header & header_mask) == 0) { dest = source; // since strcpy_insitu can reuse document buffer memory we need to mark both source and dest as shared header |= xml_memory_page_contents_shared_mask; source_header |= xml_memory_page_contents_shared_mask; } else strcpy_insitu(dest, header, header_mask, source, strlength(source)); } } PUGI_IMPL_FN void node_copy_contents(xml_node_struct* dn, xml_node_struct* sn, xml_allocator* shared_alloc) { node_copy_string(dn->name, dn->header, xml_memory_page_name_allocated_mask, sn->name, sn->header, shared_alloc); node_copy_string(dn->value, dn->header, xml_memory_page_value_allocated_mask, sn->value, sn->header, shared_alloc); for (xml_attribute_struct* sa = sn->first_attribute; sa; sa = sa->next_attribute) { xml_attribute_struct* da = append_new_attribute(dn, get_allocator(dn)); if (da) { node_copy_string(da->name, da->header, xml_memory_page_name_allocated_mask, sa->name, sa->header, shared_alloc); node_copy_string(da->value, da->header, xml_memory_page_value_allocated_mask, sa->value, sa->header, shared_alloc); } } } PUGI_IMPL_FN void node_copy_tree(xml_node_struct* dn, xml_node_struct* sn) { xml_allocator& alloc = get_allocator(dn); xml_allocator* shared_alloc = (&alloc == &get_allocator(sn)) ? &alloc : 0; node_copy_contents(dn, sn, shared_alloc); xml_node_struct* dit = dn; xml_node_struct* sit = sn->first_child; while (sit && sit != sn) { // loop invariant: dit is inside the subtree rooted at dn assert(dit); // when a tree is copied into one of the descendants, we need to skip that subtree to avoid an infinite loop if (sit != dn) { xml_node_struct* copy = append_new_node(dit, alloc, PUGI_IMPL_NODETYPE(sit)); if (copy) { node_copy_contents(copy, sit, shared_alloc); if (sit->first_child) { dit = copy; sit = sit->first_child; continue; } } } // continue to the next node do { if (sit->next_sibling) { sit = sit->next_sibling; break; } sit = sit->parent; dit = dit->parent; // loop invariant: dit is inside the subtree rooted at dn while sit is inside sn assert(sit == sn || dit); } while (sit != sn); } assert(!sit || dit == dn->parent); } PUGI_IMPL_FN void node_copy_attribute(xml_attribute_struct* da, xml_attribute_struct* sa) { xml_allocator& alloc = get_allocator(da); xml_allocator* shared_alloc = (&alloc == &get_allocator(sa)) ? &alloc : 0; node_copy_string(da->name, da->header, xml_memory_page_name_allocated_mask, sa->name, sa->header, shared_alloc); node_copy_string(da->value, da->header, xml_memory_page_value_allocated_mask, sa->value, sa->header, shared_alloc); } inline bool is_text_node(xml_node_struct* node) { xml_node_type type = PUGI_IMPL_NODETYPE(node); return type == node_pcdata || type == node_cdata; } // get value with conversion functions template PUGI_IMPL_FN PUGI_IMPL_UNSIGNED_OVERFLOW U string_to_integer(const char_t* value, U minv, U maxv) { U result = 0; const char_t* s = value; while (PUGI_IMPL_IS_CHARTYPE(*s, ct_space)) s++; bool negative = (*s == '-'); s += (*s == '+' || *s == '-'); bool overflow = false; if (s[0] == '0' && (s[1] | ' ') == 'x') { s += 2; // since overflow detection relies on length of the sequence skip leading zeros while (*s == '0') s++; const char_t* start = s; for (;;) { if (static_cast(*s - '0') < 10) result = result * 16 + (*s - '0'); else if (static_cast((*s | ' ') - 'a') < 6) result = result * 16 + ((*s | ' ') - 'a' + 10); else break; s++; } size_t digits = static_cast(s - start); overflow = digits > sizeof(U) * 2; } else { // since overflow detection relies on length of the sequence skip leading zeros while (*s == '0') s++; const char_t* start = s; for (;;) { if (static_cast(*s - '0') < 10) result = result * 10 + (*s - '0'); else break; s++; } size_t digits = static_cast(s - start); PUGI_IMPL_STATIC_ASSERT(sizeof(U) == 8 || sizeof(U) == 4 || sizeof(U) == 2); const size_t max_digits10 = sizeof(U) == 8 ? 20 : sizeof(U) == 4 ? 10 : 5; const char_t max_lead = sizeof(U) == 8 ? '1' : sizeof(U) == 4 ? '4' : '6'; const size_t high_bit = sizeof(U) * 8 - 1; overflow = digits >= max_digits10 && !(digits == max_digits10 && (*start < max_lead || (*start == max_lead && result >> high_bit))); } if (negative) { // Workaround for crayc++ CC-3059: Expected no overflow in routine. #ifdef _CRAYC return (overflow || result > ~minv + 1) ? minv : ~result + 1; #else return (overflow || result > 0 - minv) ? minv : 0 - result; #endif } else return (overflow || result > maxv) ? maxv : result; } PUGI_IMPL_FN int get_value_int(const char_t* value) { return string_to_integer(value, static_cast(INT_MIN), INT_MAX); } PUGI_IMPL_FN unsigned int get_value_uint(const char_t* value) { return string_to_integer(value, 0, UINT_MAX); } PUGI_IMPL_FN double get_value_double(const char_t* value) { #ifdef PUGIXML_WCHAR_MODE return wcstod(value, 0); #else return strtod(value, 0); #endif } PUGI_IMPL_FN float get_value_float(const char_t* value) { #ifdef PUGIXML_WCHAR_MODE return static_cast(wcstod(value, 0)); #else return static_cast(strtod(value, 0)); #endif } PUGI_IMPL_FN bool get_value_bool(const char_t* value) { // only look at first char char_t first = *value; // 1*, t* (true), T* (True), y* (yes), Y* (YES) return (first == '1' || first == 't' || first == 'T' || first == 'y' || first == 'Y'); } #ifdef PUGIXML_HAS_LONG_LONG PUGI_IMPL_FN long long get_value_llong(const char_t* value) { return string_to_integer(value, static_cast(LLONG_MIN), LLONG_MAX); } PUGI_IMPL_FN unsigned long long get_value_ullong(const char_t* value) { return string_to_integer(value, 0, ULLONG_MAX); } #endif template PUGI_IMPL_FN PUGI_IMPL_UNSIGNED_OVERFLOW char_t* integer_to_string(char_t* begin, char_t* end, U value, bool negative) { char_t* result = end - 1; U rest = negative ? 0 - value : value; do { *result-- = static_cast('0' + (rest % 10)); rest /= 10; } while (rest); assert(result >= begin); (void)begin; *result = '-'; return result + !negative; } // set value with conversion functions template PUGI_IMPL_FN bool set_value_ascii(String& dest, Header& header, uintptr_t header_mask, char* buf) { #ifdef PUGIXML_WCHAR_MODE char_t wbuf[128]; assert(strlen(buf) < sizeof(wbuf) / sizeof(wbuf[0])); size_t offset = 0; for (; buf[offset]; ++offset) wbuf[offset] = buf[offset]; return strcpy_insitu(dest, header, header_mask, wbuf, offset); #else return strcpy_insitu(dest, header, header_mask, buf, strlen(buf)); #endif } template PUGI_IMPL_FN bool set_value_integer(String& dest, Header& header, uintptr_t header_mask, U value, bool negative) { char_t buf[64]; char_t* end = buf + sizeof(buf) / sizeof(buf[0]); char_t* begin = integer_to_string(buf, end, value, negative); return strcpy_insitu(dest, header, header_mask, begin, end - begin); } template PUGI_IMPL_FN bool set_value_convert(String& dest, Header& header, uintptr_t header_mask, float value, int precision) { char buf[128]; PUGI_IMPL_SNPRINTF(buf, "%.*g", precision, double(value)); return set_value_ascii(dest, header, header_mask, buf); } template PUGI_IMPL_FN bool set_value_convert(String& dest, Header& header, uintptr_t header_mask, double value, int precision) { char buf[128]; PUGI_IMPL_SNPRINTF(buf, "%.*g", precision, value); return set_value_ascii(dest, header, header_mask, buf); } template PUGI_IMPL_FN bool set_value_bool(String& dest, Header& header, uintptr_t header_mask, bool value) { return strcpy_insitu(dest, header, header_mask, value ? PUGIXML_TEXT("true") : PUGIXML_TEXT("false"), value ? 4 : 5); } PUGI_IMPL_FN xml_parse_result load_buffer_impl(xml_document_struct* doc, xml_node_struct* root, void* contents, size_t size, unsigned int options, xml_encoding encoding, bool is_mutable, bool own, char_t** out_buffer) { // check input buffer if (!contents && size) return make_parse_result(status_io_error); // get actual encoding xml_encoding buffer_encoding = impl::get_buffer_encoding(encoding, contents, size); // if convert_buffer below throws bad_alloc, we still need to deallocate contents if we own it auto_deleter contents_guard(own ? contents : 0, xml_memory::deallocate); // get private buffer char_t* buffer = 0; size_t length = 0; // coverity[var_deref_model] if (!impl::convert_buffer(buffer, length, buffer_encoding, contents, size, is_mutable)) return impl::make_parse_result(status_out_of_memory); // after this we either deallocate contents (below) or hold on to it via doc->buffer, so we don't need to guard it contents_guard.release(); // delete original buffer if we performed a conversion if (own && buffer != contents && contents) impl::xml_memory::deallocate(contents); // grab onto buffer if it's our buffer, user is responsible for deallocating contents himself if (own || buffer != contents) *out_buffer = buffer; // store buffer for offset_debug doc->buffer = buffer; // parse xml_parse_result res = impl::xml_parser::parse(buffer, length, doc, root, options); // remember encoding res.encoding = buffer_encoding; return res; } // we need to get length of entire file to load it in memory; the only (relatively) sane way to do it is via seek/tell trick PUGI_IMPL_FN xml_parse_status get_file_size(FILE* file, size_t& out_result) { #if defined(__linux__) || defined(__APPLE__) // this simultaneously retrieves the file size and file mode (to guard against loading non-files) struct stat st; if (fstat(fileno(file), &st) != 0) return status_io_error; // anything that's not a regular file doesn't have a coherent length if (!S_ISREG(st.st_mode)) return status_io_error; typedef off_t length_type; length_type length = st.st_size; #elif defined(PUGI_IMPL_MSVC_CRT_VERSION) && PUGI_IMPL_MSVC_CRT_VERSION >= 1400 // there are 64-bit versions of fseek/ftell, let's use them typedef __int64 length_type; _fseeki64(file, 0, SEEK_END); length_type length = _ftelli64(file); _fseeki64(file, 0, SEEK_SET); #elif defined(__MINGW32__) && !defined(__NO_MINGW_LFS) && (!defined(__STRICT_ANSI__) || defined(__MINGW64_VERSION_MAJOR)) // there are 64-bit versions of fseek/ftell, let's use them typedef off64_t length_type; fseeko64(file, 0, SEEK_END); length_type length = ftello64(file); fseeko64(file, 0, SEEK_SET); #else // if this is a 32-bit OS, long is enough; if this is a unix system, long is 64-bit, which is enough; otherwise we can't do anything anyway. typedef long length_type; fseek(file, 0, SEEK_END); length_type length = ftell(file); fseek(file, 0, SEEK_SET); #endif // check for I/O errors if (length < 0) return status_io_error; // check for overflow size_t result = static_cast(length); if (static_cast(result) != length) return status_out_of_memory; // finalize out_result = result; return status_ok; } // This function assumes that buffer has extra sizeof(char_t) writable bytes after size PUGI_IMPL_FN size_t zero_terminate_buffer(void* buffer, size_t size, xml_encoding encoding) { // We only need to zero-terminate if encoding conversion does not do it for us #ifdef PUGIXML_WCHAR_MODE xml_encoding wchar_encoding = get_wchar_encoding(); if (encoding == wchar_encoding || need_endian_swap_utf(encoding, wchar_encoding)) { size_t length = size / sizeof(char_t); static_cast(buffer)[length] = 0; return (length + 1) * sizeof(char_t); } #else if (encoding == encoding_utf8) { static_cast(buffer)[size] = 0; return size + 1; } #endif return size; } PUGI_IMPL_FN xml_parse_result load_file_impl(xml_document_struct* doc, FILE* file, unsigned int options, xml_encoding encoding, char_t** out_buffer) { if (!file) return make_parse_result(status_file_not_found); // get file size (can result in I/O errors) size_t size = 0; xml_parse_status size_status = get_file_size(file, size); if (size_status != status_ok) return make_parse_result(size_status); size_t max_suffix_size = sizeof(char_t); // allocate buffer for the whole file char* contents = static_cast(xml_memory::allocate(size + max_suffix_size)); if (!contents) return make_parse_result(status_out_of_memory); // read file in memory size_t read_size = fread(contents, 1, size, file); if (read_size != size) { xml_memory::deallocate(contents); return make_parse_result(status_io_error); } xml_encoding real_encoding = get_buffer_encoding(encoding, contents, size); return load_buffer_impl(doc, doc, contents, zero_terminate_buffer(contents, size, real_encoding), options, real_encoding, true, true, out_buffer); } PUGI_IMPL_FN void close_file(FILE* file) { fclose(file); } #ifndef PUGIXML_NO_STL template struct xml_stream_chunk { static xml_stream_chunk* create() { void* memory = xml_memory::allocate(sizeof(xml_stream_chunk)); if (!memory) return 0; return new (memory) xml_stream_chunk(); } static void destroy(xml_stream_chunk* chunk) { // free chunk chain while (chunk) { xml_stream_chunk* next_ = chunk->next; xml_memory::deallocate(chunk); chunk = next_; } } xml_stream_chunk(): next(0), size(0) { } xml_stream_chunk* next; size_t size; T data[xml_memory_page_size / sizeof(T)]; }; template PUGI_IMPL_FN xml_parse_status load_stream_data_noseek(std::basic_istream& stream, void** out_buffer, size_t* out_size) { auto_deleter > chunks(0, xml_stream_chunk::destroy); // read file to a chunk list size_t total = 0; xml_stream_chunk* last = 0; while (!stream.eof()) { // allocate new chunk xml_stream_chunk* chunk = xml_stream_chunk::create(); if (!chunk) return status_out_of_memory; // append chunk to list if (last) last = last->next = chunk; else chunks.data = last = chunk; // read data to chunk stream.read(chunk->data, static_cast(sizeof(chunk->data) / sizeof(T))); chunk->size = static_cast(stream.gcount()) * sizeof(T); // read may set failbit | eofbit in case gcount() is less than read length, so check for other I/O errors if (stream.bad() || (!stream.eof() && stream.fail())) return status_io_error; // guard against huge files (chunk size is small enough to make this overflow check work) if (total + chunk->size < total) return status_out_of_memory; total += chunk->size; } size_t max_suffix_size = sizeof(char_t); // copy chunk list to a contiguous buffer char* buffer = static_cast(xml_memory::allocate(total + max_suffix_size)); if (!buffer) return status_out_of_memory; char* write = buffer; for (xml_stream_chunk* chunk = chunks.data; chunk; chunk = chunk->next) { assert(write + chunk->size <= buffer + total); memcpy(write, chunk->data, chunk->size); write += chunk->size; } assert(write == buffer + total); // return buffer *out_buffer = buffer; *out_size = total; return status_ok; } template PUGI_IMPL_FN xml_parse_status load_stream_data_seek(std::basic_istream& stream, void** out_buffer, size_t* out_size) { // get length of remaining data in stream typename std::basic_istream::pos_type pos = stream.tellg(); stream.seekg(0, std::ios::end); std::streamoff length = stream.tellg() - pos; stream.seekg(pos); if (stream.fail() || pos < 0) return status_io_error; // guard against huge files size_t read_length = static_cast(length); if (static_cast(read_length) != length || length < 0) return status_out_of_memory; size_t max_suffix_size = sizeof(char_t); // read stream data into memory (guard against stream exceptions with buffer holder) auto_deleter buffer(xml_memory::allocate(read_length * sizeof(T) + max_suffix_size), xml_memory::deallocate); if (!buffer.data) return status_out_of_memory; stream.read(static_cast(buffer.data), static_cast(read_length)); // read may set failbit | eofbit in case gcount() is less than read_length (i.e. line ending conversion), so check for other I/O errors if (stream.bad() || (!stream.eof() && stream.fail())) return status_io_error; // return buffer size_t actual_length = static_cast(stream.gcount()); assert(actual_length <= read_length); *out_buffer = buffer.release(); *out_size = actual_length * sizeof(T); return status_ok; } template PUGI_IMPL_FN xml_parse_result load_stream_impl(xml_document_struct* doc, std::basic_istream& stream, unsigned int options, xml_encoding encoding, char_t** out_buffer) { void* buffer = 0; size_t size = 0; xml_parse_status status = status_ok; // if stream has an error bit set, bail out (otherwise tellg() can fail and we'll clear error bits) if (stream.fail()) return make_parse_result(status_io_error); // load stream to memory (using seek-based implementation if possible, since it's faster and takes less memory) if (stream.tellg() < 0) { stream.clear(); // clear error flags that could be set by a failing tellg status = load_stream_data_noseek(stream, &buffer, &size); } else status = load_stream_data_seek(stream, &buffer, &size); if (status != status_ok) return make_parse_result(status); xml_encoding real_encoding = get_buffer_encoding(encoding, buffer, size); return load_buffer_impl(doc, doc, buffer, zero_terminate_buffer(buffer, size, real_encoding), options, real_encoding, true, true, out_buffer); } #endif #if defined(PUGI_IMPL_MSVC_CRT_VERSION) || defined(__BORLANDC__) || (defined(__MINGW32__) && (!defined(__STRICT_ANSI__) || defined(__MINGW64_VERSION_MAJOR))) PUGI_IMPL_FN FILE* open_file_wide(const wchar_t* path, const wchar_t* mode) { #if defined(PUGI_IMPL_MSVC_CRT_VERSION) && PUGI_IMPL_MSVC_CRT_VERSION >= 1400 FILE* file = 0; return _wfopen_s(&file, path, mode) == 0 ? file : 0; #else return _wfopen(path, mode); #endif } #else PUGI_IMPL_FN char* convert_path_heap(const wchar_t* str) { assert(str); // first pass: get length in utf8 characters size_t length = strlength_wide(str); size_t size = as_utf8_begin(str, length); // allocate resulting string char* result = static_cast(xml_memory::allocate(size + 1)); if (!result) return 0; // second pass: convert to utf8 as_utf8_end(result, size, str, length); // zero-terminate result[size] = 0; return result; } PUGI_IMPL_FN FILE* open_file_wide(const wchar_t* path, const wchar_t* mode) { // there is no standard function to open wide paths, so our best bet is to try utf8 path char* path_utf8 = convert_path_heap(path); if (!path_utf8) return 0; // convert mode to ASCII (we mirror _wfopen interface) char mode_ascii[4] = {0}; for (size_t i = 0; mode[i]; ++i) mode_ascii[i] = static_cast(mode[i]); // try to open the utf8 path FILE* result = fopen(path_utf8, mode_ascii); // free dummy buffer xml_memory::deallocate(path_utf8); return result; } #endif PUGI_IMPL_FN FILE* open_file(const char* path, const char* mode) { #if defined(PUGI_IMPL_MSVC_CRT_VERSION) && PUGI_IMPL_MSVC_CRT_VERSION >= 1400 FILE* file = 0; return fopen_s(&file, path, mode) == 0 ? file : 0; #else return fopen(path, mode); #endif } PUGI_IMPL_FN bool save_file_impl(const xml_document& doc, FILE* file, const char_t* indent, unsigned int flags, xml_encoding encoding) { if (!file) return false; xml_writer_file writer(file); doc.save(writer, indent, flags, encoding); return fflush(file) == 0 && ferror(file) == 0; } struct name_null_sentry { xml_node_struct* node; char_t* name; name_null_sentry(xml_node_struct* node_): node(node_), name(node_->name) { node->name = 0; } ~name_null_sentry() { node->name = name; } }; PUGI_IMPL_NS_END namespace pugi { PUGI_IMPL_FN xml_writer::~xml_writer() { } PUGI_IMPL_FN xml_writer_file::xml_writer_file(void* file_): file(file_) { } PUGI_IMPL_FN void xml_writer_file::write(const void* data, size_t size) { size_t result = fwrite(data, 1, size, static_cast(file)); (void)!result; // unfortunately we can't do proper error handling here } #ifndef PUGIXML_NO_STL PUGI_IMPL_FN xml_writer_stream::xml_writer_stream(std::basic_ostream >& stream): narrow_stream(&stream), wide_stream(0) { } PUGI_IMPL_FN xml_writer_stream::xml_writer_stream(std::basic_ostream >& stream): narrow_stream(0), wide_stream(&stream) { } PUGI_IMPL_FN void xml_writer_stream::write(const void* data, size_t size) { if (narrow_stream) { assert(!wide_stream); narrow_stream->write(reinterpret_cast(data), static_cast(size)); } else { assert(wide_stream); assert(size % sizeof(wchar_t) == 0); wide_stream->write(reinterpret_cast(data), static_cast(size / sizeof(wchar_t))); } } #endif PUGI_IMPL_FN xml_tree_walker::xml_tree_walker(): _depth(0) { } PUGI_IMPL_FN xml_tree_walker::~xml_tree_walker() { } PUGI_IMPL_FN int xml_tree_walker::depth() const { return _depth; } PUGI_IMPL_FN bool xml_tree_walker::begin(xml_node&) { return true; } PUGI_IMPL_FN bool xml_tree_walker::end(xml_node&) { return true; } PUGI_IMPL_FN xml_attribute::xml_attribute(): _attr(0) { } PUGI_IMPL_FN xml_attribute::xml_attribute(xml_attribute_struct* attr): _attr(attr) { } PUGI_IMPL_FN static void unspecified_bool_xml_attribute(xml_attribute***) { } PUGI_IMPL_FN xml_attribute::operator xml_attribute::unspecified_bool_type() const { return _attr ? unspecified_bool_xml_attribute : 0; } PUGI_IMPL_FN bool xml_attribute::operator!() const { return !_attr; } PUGI_IMPL_FN bool xml_attribute::operator==(const xml_attribute& r) const { return (_attr == r._attr); } PUGI_IMPL_FN bool xml_attribute::operator!=(const xml_attribute& r) const { return (_attr != r._attr); } PUGI_IMPL_FN bool xml_attribute::operator<(const xml_attribute& r) const { return (_attr < r._attr); } PUGI_IMPL_FN bool xml_attribute::operator>(const xml_attribute& r) const { return (_attr > r._attr); } PUGI_IMPL_FN bool xml_attribute::operator<=(const xml_attribute& r) const { return (_attr <= r._attr); } PUGI_IMPL_FN bool xml_attribute::operator>=(const xml_attribute& r) const { return (_attr >= r._attr); } PUGI_IMPL_FN xml_attribute xml_attribute::next_attribute() const { if (!_attr) return xml_attribute(); return xml_attribute(_attr->next_attribute); } PUGI_IMPL_FN xml_attribute xml_attribute::previous_attribute() const { if (!_attr) return xml_attribute(); xml_attribute_struct* prev = _attr->prev_attribute_c; return prev->next_attribute ? xml_attribute(prev) : xml_attribute(); } PUGI_IMPL_FN const char_t* xml_attribute::as_string(const char_t* def) const { if (!_attr) return def; const char_t* value = _attr->value; return value ? value : def; } PUGI_IMPL_FN int xml_attribute::as_int(int def) const { if (!_attr) return def; const char_t* value = _attr->value; return value ? impl::get_value_int(value) : def; } PUGI_IMPL_FN unsigned int xml_attribute::as_uint(unsigned int def) const { if (!_attr) return def; const char_t* value = _attr->value; return value ? impl::get_value_uint(value) : def; } PUGI_IMPL_FN double xml_attribute::as_double(double def) const { if (!_attr) return def; const char_t* value = _attr->value; return value ? impl::get_value_double(value) : def; } PUGI_IMPL_FN float xml_attribute::as_float(float def) const { if (!_attr) return def; const char_t* value = _attr->value; return value ? impl::get_value_float(value) : def; } PUGI_IMPL_FN bool xml_attribute::as_bool(bool def) const { if (!_attr) return def; const char_t* value = _attr->value; return value ? impl::get_value_bool(value) : def; } #ifdef PUGIXML_HAS_LONG_LONG PUGI_IMPL_FN long long xml_attribute::as_llong(long long def) const { if (!_attr) return def; const char_t* value = _attr->value; return value ? impl::get_value_llong(value) : def; } PUGI_IMPL_FN unsigned long long xml_attribute::as_ullong(unsigned long long def) const { if (!_attr) return def; const char_t* value = _attr->value; return value ? impl::get_value_ullong(value) : def; } #endif PUGI_IMPL_FN bool xml_attribute::empty() const { return !_attr; } PUGI_IMPL_FN const char_t* xml_attribute::name() const { if (!_attr) return PUGIXML_TEXT(""); const char_t* name = _attr->name; return name ? name : PUGIXML_TEXT(""); } PUGI_IMPL_FN const char_t* xml_attribute::value() const { if (!_attr) return PUGIXML_TEXT(""); const char_t* value = _attr->value; return value ? value : PUGIXML_TEXT(""); } PUGI_IMPL_FN size_t xml_attribute::hash_value() const { return static_cast(reinterpret_cast(_attr) / sizeof(xml_attribute_struct)); } PUGI_IMPL_FN xml_attribute_struct* xml_attribute::internal_object() const { return _attr; } PUGI_IMPL_FN xml_attribute& xml_attribute::operator=(const char_t* rhs) { set_value(rhs); return *this; } PUGI_IMPL_FN xml_attribute& xml_attribute::operator=(int rhs) { set_value(rhs); return *this; } PUGI_IMPL_FN xml_attribute& xml_attribute::operator=(unsigned int rhs) { set_value(rhs); return *this; } PUGI_IMPL_FN xml_attribute& xml_attribute::operator=(long rhs) { set_value(rhs); return *this; } PUGI_IMPL_FN xml_attribute& xml_attribute::operator=(unsigned long rhs) { set_value(rhs); return *this; } PUGI_IMPL_FN xml_attribute& xml_attribute::operator=(double rhs) { set_value(rhs); return *this; } PUGI_IMPL_FN xml_attribute& xml_attribute::operator=(float rhs) { set_value(rhs); return *this; } PUGI_IMPL_FN xml_attribute& xml_attribute::operator=(bool rhs) { set_value(rhs); return *this; } #ifdef PUGIXML_HAS_LONG_LONG PUGI_IMPL_FN xml_attribute& xml_attribute::operator=(long long rhs) { set_value(rhs); return *this; } PUGI_IMPL_FN xml_attribute& xml_attribute::operator=(unsigned long long rhs) { set_value(rhs); return *this; } #endif PUGI_IMPL_FN bool xml_attribute::set_name(const char_t* rhs) { if (!_attr) return false; return impl::strcpy_insitu(_attr->name, _attr->header, impl::xml_memory_page_name_allocated_mask, rhs, impl::strlength(rhs)); } PUGI_IMPL_FN bool xml_attribute::set_name(const char_t* rhs, size_t size) { if (!_attr) return false; return impl::strcpy_insitu(_attr->name, _attr->header, impl::xml_memory_page_name_allocated_mask, rhs, size); } PUGI_IMPL_FN bool xml_attribute::set_value(const char_t* rhs) { if (!_attr) return false; return impl::strcpy_insitu(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, impl::strlength(rhs)); } PUGI_IMPL_FN bool xml_attribute::set_value(const char_t* rhs, size_t size) { if (!_attr) return false; return impl::strcpy_insitu(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, size); } PUGI_IMPL_FN bool xml_attribute::set_value(int rhs) { if (!_attr) return false; return impl::set_value_integer(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, rhs < 0); } PUGI_IMPL_FN bool xml_attribute::set_value(unsigned int rhs) { if (!_attr) return false; return impl::set_value_integer(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, false); } PUGI_IMPL_FN bool xml_attribute::set_value(long rhs) { if (!_attr) return false; return impl::set_value_integer(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, rhs < 0); } PUGI_IMPL_FN bool xml_attribute::set_value(unsigned long rhs) { if (!_attr) return false; return impl::set_value_integer(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, false); } PUGI_IMPL_FN bool xml_attribute::set_value(double rhs) { if (!_attr) return false; return impl::set_value_convert(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, default_double_precision); } PUGI_IMPL_FN bool xml_attribute::set_value(double rhs, int precision) { if (!_attr) return false; return impl::set_value_convert(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, precision); } PUGI_IMPL_FN bool xml_attribute::set_value(float rhs) { if (!_attr) return false; return impl::set_value_convert(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, default_float_precision); } PUGI_IMPL_FN bool xml_attribute::set_value(float rhs, int precision) { if (!_attr) return false; return impl::set_value_convert(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, precision); } PUGI_IMPL_FN bool xml_attribute::set_value(bool rhs) { if (!_attr) return false; return impl::set_value_bool(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs); } #ifdef PUGIXML_HAS_LONG_LONG PUGI_IMPL_FN bool xml_attribute::set_value(long long rhs) { if (!_attr) return false; return impl::set_value_integer(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, rhs < 0); } PUGI_IMPL_FN bool xml_attribute::set_value(unsigned long long rhs) { if (!_attr) return false; return impl::set_value_integer(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, false); } #endif #ifdef __BORLANDC__ PUGI_IMPL_FN bool operator&&(const xml_attribute& lhs, bool rhs) { return (bool)lhs && rhs; } PUGI_IMPL_FN bool operator||(const xml_attribute& lhs, bool rhs) { return (bool)lhs || rhs; } #endif PUGI_IMPL_FN xml_node::xml_node(): _root(0) { } PUGI_IMPL_FN xml_node::xml_node(xml_node_struct* p): _root(p) { } PUGI_IMPL_FN static void unspecified_bool_xml_node(xml_node***) { } PUGI_IMPL_FN xml_node::operator xml_node::unspecified_bool_type() const { return _root ? unspecified_bool_xml_node : 0; } PUGI_IMPL_FN bool xml_node::operator!() const { return !_root; } PUGI_IMPL_FN xml_node::iterator xml_node::begin() const { return iterator(_root ? _root->first_child + 0 : 0, _root); } PUGI_IMPL_FN xml_node::iterator xml_node::end() const { return iterator(0, _root); } PUGI_IMPL_FN xml_node::attribute_iterator xml_node::attributes_begin() const { return attribute_iterator(_root ? _root->first_attribute + 0 : 0, _root); } PUGI_IMPL_FN xml_node::attribute_iterator xml_node::attributes_end() const { return attribute_iterator(0, _root); } PUGI_IMPL_FN xml_object_range xml_node::children() const { return xml_object_range(begin(), end()); } PUGI_IMPL_FN xml_object_range xml_node::children(const char_t* name_) const { return xml_object_range(xml_named_node_iterator(child(name_)._root, _root, name_), xml_named_node_iterator(0, _root, name_)); } PUGI_IMPL_FN xml_object_range xml_node::attributes() const { return xml_object_range(attributes_begin(), attributes_end()); } PUGI_IMPL_FN bool xml_node::operator==(const xml_node& r) const { return (_root == r._root); } PUGI_IMPL_FN bool xml_node::operator!=(const xml_node& r) const { return (_root != r._root); } PUGI_IMPL_FN bool xml_node::operator<(const xml_node& r) const { return (_root < r._root); } PUGI_IMPL_FN bool xml_node::operator>(const xml_node& r) const { return (_root > r._root); } PUGI_IMPL_FN bool xml_node::operator<=(const xml_node& r) const { return (_root <= r._root); } PUGI_IMPL_FN bool xml_node::operator>=(const xml_node& r) const { return (_root >= r._root); } PUGI_IMPL_FN bool xml_node::empty() const { return !_root; } PUGI_IMPL_FN const char_t* xml_node::name() const { if (!_root) return PUGIXML_TEXT(""); const char_t* name = _root->name; return name ? name : PUGIXML_TEXT(""); } PUGI_IMPL_FN xml_node_type xml_node::type() const { return _root ? PUGI_IMPL_NODETYPE(_root) : node_null; } PUGI_IMPL_FN const char_t* xml_node::value() const { if (!_root) return PUGIXML_TEXT(""); const char_t* value = _root->value; return value ? value : PUGIXML_TEXT(""); } PUGI_IMPL_FN xml_node xml_node::child(const char_t* name_) const { if (!_root) return xml_node(); for (xml_node_struct* i = _root->first_child; i; i = i->next_sibling) { const char_t* iname = i->name; if (iname && impl::strequal(name_, iname)) return xml_node(i); } return xml_node(); } PUGI_IMPL_FN xml_attribute xml_node::attribute(const char_t* name_) const { if (!_root) return xml_attribute(); for (xml_attribute_struct* i = _root->first_attribute; i; i = i->next_attribute) { const char_t* iname = i->name; if (iname && impl::strequal(name_, iname)) return xml_attribute(i); } return xml_attribute(); } PUGI_IMPL_FN xml_node xml_node::next_sibling(const char_t* name_) const { if (!_root) return xml_node(); for (xml_node_struct* i = _root->next_sibling; i; i = i->next_sibling) { const char_t* iname = i->name; if (iname && impl::strequal(name_, iname)) return xml_node(i); } return xml_node(); } PUGI_IMPL_FN xml_node xml_node::next_sibling() const { return _root ? xml_node(_root->next_sibling) : xml_node(); } PUGI_IMPL_FN xml_node xml_node::previous_sibling(const char_t* name_) const { if (!_root) return xml_node(); for (xml_node_struct* i = _root->prev_sibling_c; i->next_sibling; i = i->prev_sibling_c) { const char_t* iname = i->name; if (iname && impl::strequal(name_, iname)) return xml_node(i); } return xml_node(); } PUGI_IMPL_FN xml_attribute xml_node::attribute(const char_t* name_, xml_attribute& hint_) const { xml_attribute_struct* hint = hint_._attr; // if hint is not an attribute of node, behavior is not defined assert(!hint || (_root && impl::is_attribute_of(hint, _root))); if (!_root) return xml_attribute(); // optimistically search from hint up until the end for (xml_attribute_struct* i = hint; i; i = i->next_attribute) { const char_t* iname = i->name; if (iname && impl::strequal(name_, iname)) { // update hint to maximize efficiency of searching for consecutive attributes hint_._attr = i->next_attribute; return xml_attribute(i); } } // wrap around and search from the first attribute until the hint // 'j' null pointer check is technically redundant, but it prevents a crash in case the assertion above fails for (xml_attribute_struct* j = _root->first_attribute; j && j != hint; j = j->next_attribute) { const char_t* jname = j->name; if (jname && impl::strequal(name_, jname)) { // update hint to maximize efficiency of searching for consecutive attributes hint_._attr = j->next_attribute; return xml_attribute(j); } } return xml_attribute(); } PUGI_IMPL_FN xml_node xml_node::previous_sibling() const { if (!_root) return xml_node(); xml_node_struct* prev = _root->prev_sibling_c; return prev->next_sibling ? xml_node(prev) : xml_node(); } PUGI_IMPL_FN xml_node xml_node::parent() const { return _root ? xml_node(_root->parent) : xml_node(); } PUGI_IMPL_FN xml_node xml_node::root() const { return _root ? xml_node(&impl::get_document(_root)) : xml_node(); } PUGI_IMPL_FN xml_text xml_node::text() const { return xml_text(_root); } PUGI_IMPL_FN const char_t* xml_node::child_value() const { if (!_root) return PUGIXML_TEXT(""); // element nodes can have value if parse_embed_pcdata was used if (PUGI_IMPL_NODETYPE(_root) == node_element && _root->value) return _root->value; for (xml_node_struct* i = _root->first_child; i; i = i->next_sibling) { const char_t* ivalue = i->value; if (impl::is_text_node(i) && ivalue) return ivalue; } return PUGIXML_TEXT(""); } PUGI_IMPL_FN const char_t* xml_node::child_value(const char_t* name_) const { return child(name_).child_value(); } PUGI_IMPL_FN xml_attribute xml_node::first_attribute() const { if (!_root) return xml_attribute(); return xml_attribute(_root->first_attribute); } PUGI_IMPL_FN xml_attribute xml_node::last_attribute() const { if (!_root) return xml_attribute(); xml_attribute_struct* first = _root->first_attribute; return first ? xml_attribute(first->prev_attribute_c) : xml_attribute(); } PUGI_IMPL_FN xml_node xml_node::first_child() const { if (!_root) return xml_node(); return xml_node(_root->first_child); } PUGI_IMPL_FN xml_node xml_node::last_child() const { if (!_root) return xml_node(); xml_node_struct* first = _root->first_child; return first ? xml_node(first->prev_sibling_c) : xml_node(); } PUGI_IMPL_FN bool xml_node::set_name(const char_t* rhs) { xml_node_type type_ = _root ? PUGI_IMPL_NODETYPE(_root) : node_null; if (type_ != node_element && type_ != node_pi && type_ != node_declaration) return false; return impl::strcpy_insitu(_root->name, _root->header, impl::xml_memory_page_name_allocated_mask, rhs, impl::strlength(rhs)); } PUGI_IMPL_FN bool xml_node::set_name(const char_t* rhs, size_t size) { xml_node_type type_ = _root ? PUGI_IMPL_NODETYPE(_root) : node_null; if (type_ != node_element && type_ != node_pi && type_ != node_declaration) return false; return impl::strcpy_insitu(_root->name, _root->header, impl::xml_memory_page_name_allocated_mask, rhs, size); } PUGI_IMPL_FN bool xml_node::set_value(const char_t* rhs) { xml_node_type type_ = _root ? PUGI_IMPL_NODETYPE(_root) : node_null; if (type_ != node_pcdata && type_ != node_cdata && type_ != node_comment && type_ != node_pi && type_ != node_doctype) return false; return impl::strcpy_insitu(_root->value, _root->header, impl::xml_memory_page_value_allocated_mask, rhs, impl::strlength(rhs)); } PUGI_IMPL_FN bool xml_node::set_value(const char_t* rhs, size_t size) { xml_node_type type_ = _root ? PUGI_IMPL_NODETYPE(_root) : node_null; if (type_ != node_pcdata && type_ != node_cdata && type_ != node_comment && type_ != node_pi && type_ != node_doctype) return false; return impl::strcpy_insitu(_root->value, _root->header, impl::xml_memory_page_value_allocated_mask, rhs, size); } PUGI_IMPL_FN xml_attribute xml_node::append_attribute(const char_t* name_) { if (!impl::allow_insert_attribute(type())) return xml_attribute(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_attribute(); xml_attribute a(impl::allocate_attribute(alloc)); if (!a) return xml_attribute(); impl::append_attribute(a._attr, _root); a.set_name(name_); return a; } PUGI_IMPL_FN xml_attribute xml_node::prepend_attribute(const char_t* name_) { if (!impl::allow_insert_attribute(type())) return xml_attribute(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_attribute(); xml_attribute a(impl::allocate_attribute(alloc)); if (!a) return xml_attribute(); impl::prepend_attribute(a._attr, _root); a.set_name(name_); return a; } PUGI_IMPL_FN xml_attribute xml_node::insert_attribute_after(const char_t* name_, const xml_attribute& attr) { if (!impl::allow_insert_attribute(type())) return xml_attribute(); if (!attr || !impl::is_attribute_of(attr._attr, _root)) return xml_attribute(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_attribute(); xml_attribute a(impl::allocate_attribute(alloc)); if (!a) return xml_attribute(); impl::insert_attribute_after(a._attr, attr._attr, _root); a.set_name(name_); return a; } PUGI_IMPL_FN xml_attribute xml_node::insert_attribute_before(const char_t* name_, const xml_attribute& attr) { if (!impl::allow_insert_attribute(type())) return xml_attribute(); if (!attr || !impl::is_attribute_of(attr._attr, _root)) return xml_attribute(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_attribute(); xml_attribute a(impl::allocate_attribute(alloc)); if (!a) return xml_attribute(); impl::insert_attribute_before(a._attr, attr._attr, _root); a.set_name(name_); return a; } PUGI_IMPL_FN xml_attribute xml_node::append_copy(const xml_attribute& proto) { if (!proto) return xml_attribute(); if (!impl::allow_insert_attribute(type())) return xml_attribute(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_attribute(); xml_attribute a(impl::allocate_attribute(alloc)); if (!a) return xml_attribute(); impl::append_attribute(a._attr, _root); impl::node_copy_attribute(a._attr, proto._attr); return a; } PUGI_IMPL_FN xml_attribute xml_node::prepend_copy(const xml_attribute& proto) { if (!proto) return xml_attribute(); if (!impl::allow_insert_attribute(type())) return xml_attribute(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_attribute(); xml_attribute a(impl::allocate_attribute(alloc)); if (!a) return xml_attribute(); impl::prepend_attribute(a._attr, _root); impl::node_copy_attribute(a._attr, proto._attr); return a; } PUGI_IMPL_FN xml_attribute xml_node::insert_copy_after(const xml_attribute& proto, const xml_attribute& attr) { if (!proto) return xml_attribute(); if (!impl::allow_insert_attribute(type())) return xml_attribute(); if (!attr || !impl::is_attribute_of(attr._attr, _root)) return xml_attribute(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_attribute(); xml_attribute a(impl::allocate_attribute(alloc)); if (!a) return xml_attribute(); impl::insert_attribute_after(a._attr, attr._attr, _root); impl::node_copy_attribute(a._attr, proto._attr); return a; } PUGI_IMPL_FN xml_attribute xml_node::insert_copy_before(const xml_attribute& proto, const xml_attribute& attr) { if (!proto) return xml_attribute(); if (!impl::allow_insert_attribute(type())) return xml_attribute(); if (!attr || !impl::is_attribute_of(attr._attr, _root)) return xml_attribute(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_attribute(); xml_attribute a(impl::allocate_attribute(alloc)); if (!a) return xml_attribute(); impl::insert_attribute_before(a._attr, attr._attr, _root); impl::node_copy_attribute(a._attr, proto._attr); return a; } PUGI_IMPL_FN xml_node xml_node::append_child(xml_node_type type_) { if (!impl::allow_insert_child(type(), type_)) return xml_node(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_node(); xml_node n(impl::allocate_node(alloc, type_)); if (!n) return xml_node(); impl::append_node(n._root, _root); if (type_ == node_declaration) n.set_name(PUGIXML_TEXT("xml")); return n; } PUGI_IMPL_FN xml_node xml_node::prepend_child(xml_node_type type_) { if (!impl::allow_insert_child(type(), type_)) return xml_node(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_node(); xml_node n(impl::allocate_node(alloc, type_)); if (!n) return xml_node(); impl::prepend_node(n._root, _root); if (type_ == node_declaration) n.set_name(PUGIXML_TEXT("xml")); return n; } PUGI_IMPL_FN xml_node xml_node::insert_child_before(xml_node_type type_, const xml_node& node) { if (!impl::allow_insert_child(type(), type_)) return xml_node(); if (!node._root || node._root->parent != _root) return xml_node(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_node(); xml_node n(impl::allocate_node(alloc, type_)); if (!n) return xml_node(); impl::insert_node_before(n._root, node._root); if (type_ == node_declaration) n.set_name(PUGIXML_TEXT("xml")); return n; } PUGI_IMPL_FN xml_node xml_node::insert_child_after(xml_node_type type_, const xml_node& node) { if (!impl::allow_insert_child(type(), type_)) return xml_node(); if (!node._root || node._root->parent != _root) return xml_node(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_node(); xml_node n(impl::allocate_node(alloc, type_)); if (!n) return xml_node(); impl::insert_node_after(n._root, node._root); if (type_ == node_declaration) n.set_name(PUGIXML_TEXT("xml")); return n; } PUGI_IMPL_FN xml_node xml_node::append_child(const char_t* name_) { xml_node result = append_child(node_element); result.set_name(name_); return result; } PUGI_IMPL_FN xml_node xml_node::prepend_child(const char_t* name_) { xml_node result = prepend_child(node_element); result.set_name(name_); return result; } PUGI_IMPL_FN xml_node xml_node::insert_child_after(const char_t* name_, const xml_node& node) { xml_node result = insert_child_after(node_element, node); result.set_name(name_); return result; } PUGI_IMPL_FN xml_node xml_node::insert_child_before(const char_t* name_, const xml_node& node) { xml_node result = insert_child_before(node_element, node); result.set_name(name_); return result; } PUGI_IMPL_FN xml_node xml_node::append_copy(const xml_node& proto) { xml_node_type type_ = proto.type(); if (!impl::allow_insert_child(type(), type_)) return xml_node(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_node(); xml_node n(impl::allocate_node(alloc, type_)); if (!n) return xml_node(); impl::append_node(n._root, _root); impl::node_copy_tree(n._root, proto._root); return n; } PUGI_IMPL_FN xml_node xml_node::prepend_copy(const xml_node& proto) { xml_node_type type_ = proto.type(); if (!impl::allow_insert_child(type(), type_)) return xml_node(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_node(); xml_node n(impl::allocate_node(alloc, type_)); if (!n) return xml_node(); impl::prepend_node(n._root, _root); impl::node_copy_tree(n._root, proto._root); return n; } PUGI_IMPL_FN xml_node xml_node::insert_copy_after(const xml_node& proto, const xml_node& node) { xml_node_type type_ = proto.type(); if (!impl::allow_insert_child(type(), type_)) return xml_node(); if (!node._root || node._root->parent != _root) return xml_node(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_node(); xml_node n(impl::allocate_node(alloc, type_)); if (!n) return xml_node(); impl::insert_node_after(n._root, node._root); impl::node_copy_tree(n._root, proto._root); return n; } PUGI_IMPL_FN xml_node xml_node::insert_copy_before(const xml_node& proto, const xml_node& node) { xml_node_type type_ = proto.type(); if (!impl::allow_insert_child(type(), type_)) return xml_node(); if (!node._root || node._root->parent != _root) return xml_node(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_node(); xml_node n(impl::allocate_node(alloc, type_)); if (!n) return xml_node(); impl::insert_node_before(n._root, node._root); impl::node_copy_tree(n._root, proto._root); return n; } PUGI_IMPL_FN xml_node xml_node::append_move(const xml_node& moved) { if (!impl::allow_move(*this, moved)) return xml_node(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_node(); // disable document_buffer_order optimization since moving nodes around changes document order without changing buffer pointers impl::get_document(_root).header |= impl::xml_memory_page_contents_shared_mask; impl::remove_node(moved._root); impl::append_node(moved._root, _root); return moved; } PUGI_IMPL_FN xml_node xml_node::prepend_move(const xml_node& moved) { if (!impl::allow_move(*this, moved)) return xml_node(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_node(); // disable document_buffer_order optimization since moving nodes around changes document order without changing buffer pointers impl::get_document(_root).header |= impl::xml_memory_page_contents_shared_mask; impl::remove_node(moved._root); impl::prepend_node(moved._root, _root); return moved; } PUGI_IMPL_FN xml_node xml_node::insert_move_after(const xml_node& moved, const xml_node& node) { if (!impl::allow_move(*this, moved)) return xml_node(); if (!node._root || node._root->parent != _root) return xml_node(); if (moved._root == node._root) return xml_node(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_node(); // disable document_buffer_order optimization since moving nodes around changes document order without changing buffer pointers impl::get_document(_root).header |= impl::xml_memory_page_contents_shared_mask; impl::remove_node(moved._root); impl::insert_node_after(moved._root, node._root); return moved; } PUGI_IMPL_FN xml_node xml_node::insert_move_before(const xml_node& moved, const xml_node& node) { if (!impl::allow_move(*this, moved)) return xml_node(); if (!node._root || node._root->parent != _root) return xml_node(); if (moved._root == node._root) return xml_node(); impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return xml_node(); // disable document_buffer_order optimization since moving nodes around changes document order without changing buffer pointers impl::get_document(_root).header |= impl::xml_memory_page_contents_shared_mask; impl::remove_node(moved._root); impl::insert_node_before(moved._root, node._root); return moved; } PUGI_IMPL_FN bool xml_node::remove_attribute(const char_t* name_) { return remove_attribute(attribute(name_)); } PUGI_IMPL_FN bool xml_node::remove_attribute(const xml_attribute& a) { if (!_root || !a._attr) return false; if (!impl::is_attribute_of(a._attr, _root)) return false; impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return false; impl::remove_attribute(a._attr, _root); impl::destroy_attribute(a._attr, alloc); return true; } PUGI_IMPL_FN bool xml_node::remove_attributes() { if (!_root) return false; impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return false; for (xml_attribute_struct* attr = _root->first_attribute; attr; ) { xml_attribute_struct* next = attr->next_attribute; impl::destroy_attribute(attr, alloc); attr = next; } _root->first_attribute = 0; return true; } PUGI_IMPL_FN bool xml_node::remove_child(const char_t* name_) { return remove_child(child(name_)); } PUGI_IMPL_FN bool xml_node::remove_child(const xml_node& n) { if (!_root || !n._root || n._root->parent != _root) return false; impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return false; impl::remove_node(n._root); impl::destroy_node(n._root, alloc); return true; } PUGI_IMPL_FN bool xml_node::remove_children() { if (!_root) return false; impl::xml_allocator& alloc = impl::get_allocator(_root); if (!alloc.reserve()) return false; for (xml_node_struct* cur = _root->first_child; cur; ) { xml_node_struct* next = cur->next_sibling; impl::destroy_node(cur, alloc); cur = next; } _root->first_child = 0; return true; } PUGI_IMPL_FN xml_parse_result xml_node::append_buffer(const void* contents, size_t size, unsigned int options, xml_encoding encoding) { // append_buffer is only valid for elements/documents if (!impl::allow_insert_child(type(), node_element)) return impl::make_parse_result(status_append_invalid_root); // append buffer can not merge PCDATA into existing PCDATA nodes if ((options & parse_merge_pcdata) != 0 && last_child().type() == node_pcdata) return impl::make_parse_result(status_append_invalid_root); // get document node impl::xml_document_struct* doc = &impl::get_document(_root); // disable document_buffer_order optimization since in a document with multiple buffers comparing buffer pointers does not make sense doc->header |= impl::xml_memory_page_contents_shared_mask; // get extra buffer element (we'll store the document fragment buffer there so that we can deallocate it later) impl::xml_memory_page* page = 0; impl::xml_extra_buffer* extra = static_cast(doc->allocate_memory(sizeof(impl::xml_extra_buffer) + sizeof(void*), page)); (void)page; if (!extra) return impl::make_parse_result(status_out_of_memory); #ifdef PUGIXML_COMPACT // align the memory block to a pointer boundary; this is required for compact mode where memory allocations are only 4b aligned // note that this requires up to sizeof(void*)-1 additional memory, which the allocation above takes into account extra = reinterpret_cast((reinterpret_cast(extra) + (sizeof(void*) - 1)) & ~(sizeof(void*) - 1)); #endif // add extra buffer to the list extra->buffer = 0; extra->next = doc->extra_buffers; doc->extra_buffers = extra; // name of the root has to be NULL before parsing - otherwise closing node mismatches will not be detected at the top level impl::name_null_sentry sentry(_root); return impl::load_buffer_impl(doc, _root, const_cast(contents), size, options, encoding, false, false, &extra->buffer); } PUGI_IMPL_FN xml_node xml_node::find_child_by_attribute(const char_t* name_, const char_t* attr_name, const char_t* attr_value) const { if (!_root) return xml_node(); for (xml_node_struct* i = _root->first_child; i; i = i->next_sibling) { const char_t* iname = i->name; if (iname && impl::strequal(name_, iname)) { for (xml_attribute_struct* a = i->first_attribute; a; a = a->next_attribute) { const char_t* aname = a->name; if (aname && impl::strequal(attr_name, aname)) { const char_t* avalue = a->value; if (impl::strequal(attr_value, avalue ? avalue : PUGIXML_TEXT(""))) return xml_node(i); } } } } return xml_node(); } PUGI_IMPL_FN xml_node xml_node::find_child_by_attribute(const char_t* attr_name, const char_t* attr_value) const { if (!_root) return xml_node(); for (xml_node_struct* i = _root->first_child; i; i = i->next_sibling) for (xml_attribute_struct* a = i->first_attribute; a; a = a->next_attribute) { const char_t* aname = a->name; if (aname && impl::strequal(attr_name, aname)) { const char_t* avalue = a->value; if (impl::strequal(attr_value, avalue ? avalue : PUGIXML_TEXT(""))) return xml_node(i); } } return xml_node(); } #ifndef PUGIXML_NO_STL PUGI_IMPL_FN string_t xml_node::path(char_t delimiter) const { if (!_root) return string_t(); size_t offset = 0; for (xml_node_struct* i = _root; i; i = i->parent) { const char_t* iname = i->name; offset += (i != _root); offset += iname ? impl::strlength(iname) : 0; } string_t result; result.resize(offset); for (xml_node_struct* j = _root; j; j = j->parent) { if (j != _root) result[--offset] = delimiter; const char_t* jname = j->name; if (jname) { size_t length = impl::strlength(jname); offset -= length; memcpy(&result[offset], jname, length * sizeof(char_t)); } } assert(offset == 0); return result; } #endif PUGI_IMPL_FN xml_node xml_node::first_element_by_path(const char_t* path_, char_t delimiter) const { xml_node context = path_[0] == delimiter ? root() : *this; if (!context._root) return xml_node(); const char_t* path_segment = path_; while (*path_segment == delimiter) ++path_segment; const char_t* path_segment_end = path_segment; while (*path_segment_end && *path_segment_end != delimiter) ++path_segment_end; if (path_segment == path_segment_end) return context; const char_t* next_segment = path_segment_end; while (*next_segment == delimiter) ++next_segment; if (*path_segment == '.' && path_segment + 1 == path_segment_end) return context.first_element_by_path(next_segment, delimiter); else if (*path_segment == '.' && *(path_segment+1) == '.' && path_segment + 2 == path_segment_end) return context.parent().first_element_by_path(next_segment, delimiter); else { for (xml_node_struct* j = context._root->first_child; j; j = j->next_sibling) { const char_t* jname = j->name; if (jname && impl::strequalrange(jname, path_segment, static_cast(path_segment_end - path_segment))) { xml_node subsearch = xml_node(j).first_element_by_path(next_segment, delimiter); if (subsearch) return subsearch; } } return xml_node(); } } PUGI_IMPL_FN bool xml_node::traverse(xml_tree_walker& walker) { walker._depth = -1; xml_node arg_begin(_root); if (!walker.begin(arg_begin)) return false; xml_node_struct* cur = _root ? _root->first_child + 0 : 0; if (cur) { ++walker._depth; do { xml_node arg_for_each(cur); if (!walker.for_each(arg_for_each)) return false; if (cur->first_child) { ++walker._depth; cur = cur->first_child; } else if (cur->next_sibling) cur = cur->next_sibling; else { while (!cur->next_sibling && cur != _root && cur->parent) { --walker._depth; cur = cur->parent; } if (cur != _root) cur = cur->next_sibling; } } while (cur && cur != _root); } assert(walker._depth == -1); xml_node arg_end(_root); return walker.end(arg_end); } PUGI_IMPL_FN size_t xml_node::hash_value() const { return static_cast(reinterpret_cast(_root) / sizeof(xml_node_struct)); } PUGI_IMPL_FN xml_node_struct* xml_node::internal_object() const { return _root; } PUGI_IMPL_FN void xml_node::print(xml_writer& writer, const char_t* indent, unsigned int flags, xml_encoding encoding, unsigned int depth) const { if (!_root) return; impl::xml_buffered_writer buffered_writer(writer, encoding); impl::node_output(buffered_writer, _root, indent, flags, depth); buffered_writer.flush(); } #ifndef PUGIXML_NO_STL PUGI_IMPL_FN void xml_node::print(std::basic_ostream >& stream, const char_t* indent, unsigned int flags, xml_encoding encoding, unsigned int depth) const { xml_writer_stream writer(stream); print(writer, indent, flags, encoding, depth); } PUGI_IMPL_FN void xml_node::print(std::basic_ostream >& stream, const char_t* indent, unsigned int flags, unsigned int depth) const { xml_writer_stream writer(stream); print(writer, indent, flags, encoding_wchar, depth); } #endif PUGI_IMPL_FN ptrdiff_t xml_node::offset_debug() const { if (!_root) return -1; impl::xml_document_struct& doc = impl::get_document(_root); // we can determine the offset reliably only if there is exactly once parse buffer if (!doc.buffer || doc.extra_buffers) return -1; switch (type()) { case node_document: return 0; case node_element: case node_declaration: case node_pi: return _root->name && (_root->header & impl::xml_memory_page_name_allocated_or_shared_mask) == 0 ? _root->name - doc.buffer : -1; case node_pcdata: case node_cdata: case node_comment: case node_doctype: return _root->value && (_root->header & impl::xml_memory_page_value_allocated_or_shared_mask) == 0 ? _root->value - doc.buffer : -1; default: assert(false && "Invalid node type"); // unreachable return -1; } } #ifdef __BORLANDC__ PUGI_IMPL_FN bool operator&&(const xml_node& lhs, bool rhs) { return (bool)lhs && rhs; } PUGI_IMPL_FN bool operator||(const xml_node& lhs, bool rhs) { return (bool)lhs || rhs; } #endif PUGI_IMPL_FN xml_text::xml_text(xml_node_struct* root): _root(root) { } PUGI_IMPL_FN xml_node_struct* xml_text::_data() const { if (!_root || impl::is_text_node(_root)) return _root; // element nodes can have value if parse_embed_pcdata was used if (PUGI_IMPL_NODETYPE(_root) == node_element && _root->value) return _root; for (xml_node_struct* node = _root->first_child; node; node = node->next_sibling) if (impl::is_text_node(node)) return node; return 0; } PUGI_IMPL_FN xml_node_struct* xml_text::_data_new() { xml_node_struct* d = _data(); if (d) return d; return xml_node(_root).append_child(node_pcdata).internal_object(); } PUGI_IMPL_FN xml_text::xml_text(): _root(0) { } PUGI_IMPL_FN static void unspecified_bool_xml_text(xml_text***) { } PUGI_IMPL_FN xml_text::operator xml_text::unspecified_bool_type() const { return _data() ? unspecified_bool_xml_text : 0; } PUGI_IMPL_FN bool xml_text::operator!() const { return !_data(); } PUGI_IMPL_FN bool xml_text::empty() const { return _data() == 0; } PUGI_IMPL_FN const char_t* xml_text::get() const { xml_node_struct* d = _data(); if (!d) return PUGIXML_TEXT(""); const char_t* value = d->value; return value ? value : PUGIXML_TEXT(""); } PUGI_IMPL_FN const char_t* xml_text::as_string(const char_t* def) const { xml_node_struct* d = _data(); if (!d) return def; const char_t* value = d->value; return value ? value : def; } PUGI_IMPL_FN int xml_text::as_int(int def) const { xml_node_struct* d = _data(); if (!d) return def; const char_t* value = d->value; return value ? impl::get_value_int(value) : def; } PUGI_IMPL_FN unsigned int xml_text::as_uint(unsigned int def) const { xml_node_struct* d = _data(); if (!d) return def; const char_t* value = d->value; return value ? impl::get_value_uint(value) : def; } PUGI_IMPL_FN double xml_text::as_double(double def) const { xml_node_struct* d = _data(); if (!d) return def; const char_t* value = d->value; return value ? impl::get_value_double(value) : def; } PUGI_IMPL_FN float xml_text::as_float(float def) const { xml_node_struct* d = _data(); if (!d) return def; const char_t* value = d->value; return value ? impl::get_value_float(value) : def; } PUGI_IMPL_FN bool xml_text::as_bool(bool def) const { xml_node_struct* d = _data(); if (!d) return def; const char_t* value = d->value; return value ? impl::get_value_bool(value) : def; } #ifdef PUGIXML_HAS_LONG_LONG PUGI_IMPL_FN long long xml_text::as_llong(long long def) const { xml_node_struct* d = _data(); if (!d) return def; const char_t* value = d->value; return value ? impl::get_value_llong(value) : def; } PUGI_IMPL_FN unsigned long long xml_text::as_ullong(unsigned long long def) const { xml_node_struct* d = _data(); if (!d) return def; const char_t* value = d->value; return value ? impl::get_value_ullong(value) : def; } #endif PUGI_IMPL_FN bool xml_text::set(const char_t* rhs) { xml_node_struct* dn = _data_new(); return dn ? impl::strcpy_insitu(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, impl::strlength(rhs)) : false; } PUGI_IMPL_FN bool xml_text::set(const char_t* rhs, size_t size) { xml_node_struct* dn = _data_new(); return dn ? impl::strcpy_insitu(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, size) : false; } PUGI_IMPL_FN bool xml_text::set(int rhs) { xml_node_struct* dn = _data_new(); return dn ? impl::set_value_integer(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, rhs < 0) : false; } PUGI_IMPL_FN bool xml_text::set(unsigned int rhs) { xml_node_struct* dn = _data_new(); return dn ? impl::set_value_integer(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, false) : false; } PUGI_IMPL_FN bool xml_text::set(long rhs) { xml_node_struct* dn = _data_new(); return dn ? impl::set_value_integer(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, rhs < 0) : false; } PUGI_IMPL_FN bool xml_text::set(unsigned long rhs) { xml_node_struct* dn = _data_new(); return dn ? impl::set_value_integer(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, false) : false; } PUGI_IMPL_FN bool xml_text::set(float rhs) { xml_node_struct* dn = _data_new(); return dn ? impl::set_value_convert(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, default_float_precision) : false; } PUGI_IMPL_FN bool xml_text::set(float rhs, int precision) { xml_node_struct* dn = _data_new(); return dn ? impl::set_value_convert(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, precision) : false; } PUGI_IMPL_FN bool xml_text::set(double rhs) { xml_node_struct* dn = _data_new(); return dn ? impl::set_value_convert(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, default_double_precision) : false; } PUGI_IMPL_FN bool xml_text::set(double rhs, int precision) { xml_node_struct* dn = _data_new(); return dn ? impl::set_value_convert(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, precision) : false; } PUGI_IMPL_FN bool xml_text::set(bool rhs) { xml_node_struct* dn = _data_new(); return dn ? impl::set_value_bool(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs) : false; } #ifdef PUGIXML_HAS_LONG_LONG PUGI_IMPL_FN bool xml_text::set(long long rhs) { xml_node_struct* dn = _data_new(); return dn ? impl::set_value_integer(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, rhs < 0) : false; } PUGI_IMPL_FN bool xml_text::set(unsigned long long rhs) { xml_node_struct* dn = _data_new(); return dn ? impl::set_value_integer(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, false) : false; } #endif PUGI_IMPL_FN xml_text& xml_text::operator=(const char_t* rhs) { set(rhs); return *this; } PUGI_IMPL_FN xml_text& xml_text::operator=(int rhs) { set(rhs); return *this; } PUGI_IMPL_FN xml_text& xml_text::operator=(unsigned int rhs) { set(rhs); return *this; } PUGI_IMPL_FN xml_text& xml_text::operator=(long rhs) { set(rhs); return *this; } PUGI_IMPL_FN xml_text& xml_text::operator=(unsigned long rhs) { set(rhs); return *this; } PUGI_IMPL_FN xml_text& xml_text::operator=(double rhs) { set(rhs); return *this; } PUGI_IMPL_FN xml_text& xml_text::operator=(float rhs) { set(rhs); return *this; } PUGI_IMPL_FN xml_text& xml_text::operator=(bool rhs) { set(rhs); return *this; } #ifdef PUGIXML_HAS_LONG_LONG PUGI_IMPL_FN xml_text& xml_text::operator=(long long rhs) { set(rhs); return *this; } PUGI_IMPL_FN xml_text& xml_text::operator=(unsigned long long rhs) { set(rhs); return *this; } #endif PUGI_IMPL_FN xml_node xml_text::data() const { return xml_node(_data()); } #ifdef __BORLANDC__ PUGI_IMPL_FN bool operator&&(const xml_text& lhs, bool rhs) { return (bool)lhs && rhs; } PUGI_IMPL_FN bool operator||(const xml_text& lhs, bool rhs) { return (bool)lhs || rhs; } #endif PUGI_IMPL_FN xml_node_iterator::xml_node_iterator() { } PUGI_IMPL_FN xml_node_iterator::xml_node_iterator(const xml_node& node): _wrap(node), _parent(node.parent()) { } PUGI_IMPL_FN xml_node_iterator::xml_node_iterator(xml_node_struct* ref, xml_node_struct* parent): _wrap(ref), _parent(parent) { } PUGI_IMPL_FN bool xml_node_iterator::operator==(const xml_node_iterator& rhs) const { return _wrap._root == rhs._wrap._root && _parent._root == rhs._parent._root; } PUGI_IMPL_FN bool xml_node_iterator::operator!=(const xml_node_iterator& rhs) const { return _wrap._root != rhs._wrap._root || _parent._root != rhs._parent._root; } PUGI_IMPL_FN xml_node& xml_node_iterator::operator*() const { assert(_wrap._root); return _wrap; } PUGI_IMPL_FN xml_node* xml_node_iterator::operator->() const { assert(_wrap._root); return const_cast(&_wrap); // BCC5 workaround } PUGI_IMPL_FN xml_node_iterator& xml_node_iterator::operator++() { assert(_wrap._root); _wrap._root = _wrap._root->next_sibling; return *this; } PUGI_IMPL_FN xml_node_iterator xml_node_iterator::operator++(int) { xml_node_iterator temp = *this; ++*this; return temp; } PUGI_IMPL_FN xml_node_iterator& xml_node_iterator::operator--() { _wrap = _wrap._root ? _wrap.previous_sibling() : _parent.last_child(); return *this; } PUGI_IMPL_FN xml_node_iterator xml_node_iterator::operator--(int) { xml_node_iterator temp = *this; --*this; return temp; } PUGI_IMPL_FN xml_attribute_iterator::xml_attribute_iterator() { } PUGI_IMPL_FN xml_attribute_iterator::xml_attribute_iterator(const xml_attribute& attr, const xml_node& parent): _wrap(attr), _parent(parent) { } PUGI_IMPL_FN xml_attribute_iterator::xml_attribute_iterator(xml_attribute_struct* ref, xml_node_struct* parent): _wrap(ref), _parent(parent) { } PUGI_IMPL_FN bool xml_attribute_iterator::operator==(const xml_attribute_iterator& rhs) const { return _wrap._attr == rhs._wrap._attr && _parent._root == rhs._parent._root; } PUGI_IMPL_FN bool xml_attribute_iterator::operator!=(const xml_attribute_iterator& rhs) const { return _wrap._attr != rhs._wrap._attr || _parent._root != rhs._parent._root; } PUGI_IMPL_FN xml_attribute& xml_attribute_iterator::operator*() const { assert(_wrap._attr); return _wrap; } PUGI_IMPL_FN xml_attribute* xml_attribute_iterator::operator->() const { assert(_wrap._attr); return const_cast(&_wrap); // BCC5 workaround } PUGI_IMPL_FN xml_attribute_iterator& xml_attribute_iterator::operator++() { assert(_wrap._attr); _wrap._attr = _wrap._attr->next_attribute; return *this; } PUGI_IMPL_FN xml_attribute_iterator xml_attribute_iterator::operator++(int) { xml_attribute_iterator temp = *this; ++*this; return temp; } PUGI_IMPL_FN xml_attribute_iterator& xml_attribute_iterator::operator--() { _wrap = _wrap._attr ? _wrap.previous_attribute() : _parent.last_attribute(); return *this; } PUGI_IMPL_FN xml_attribute_iterator xml_attribute_iterator::operator--(int) { xml_attribute_iterator temp = *this; --*this; return temp; } PUGI_IMPL_FN xml_named_node_iterator::xml_named_node_iterator(): _name(0) { } PUGI_IMPL_FN xml_named_node_iterator::xml_named_node_iterator(const xml_node& node, const char_t* name): _wrap(node), _parent(node.parent()), _name(name) { } PUGI_IMPL_FN xml_named_node_iterator::xml_named_node_iterator(xml_node_struct* ref, xml_node_struct* parent, const char_t* name): _wrap(ref), _parent(parent), _name(name) { } PUGI_IMPL_FN bool xml_named_node_iterator::operator==(const xml_named_node_iterator& rhs) const { return _wrap._root == rhs._wrap._root && _parent._root == rhs._parent._root; } PUGI_IMPL_FN bool xml_named_node_iterator::operator!=(const xml_named_node_iterator& rhs) const { return _wrap._root != rhs._wrap._root || _parent._root != rhs._parent._root; } PUGI_IMPL_FN xml_node& xml_named_node_iterator::operator*() const { assert(_wrap._root); return _wrap; } PUGI_IMPL_FN xml_node* xml_named_node_iterator::operator->() const { assert(_wrap._root); return const_cast(&_wrap); // BCC5 workaround } PUGI_IMPL_FN xml_named_node_iterator& xml_named_node_iterator::operator++() { assert(_wrap._root); _wrap = _wrap.next_sibling(_name); return *this; } PUGI_IMPL_FN xml_named_node_iterator xml_named_node_iterator::operator++(int) { xml_named_node_iterator temp = *this; ++*this; return temp; } PUGI_IMPL_FN xml_named_node_iterator& xml_named_node_iterator::operator--() { if (_wrap._root) _wrap = _wrap.previous_sibling(_name); else { _wrap = _parent.last_child(); if (!impl::strequal(_wrap.name(), _name)) _wrap = _wrap.previous_sibling(_name); } return *this; } PUGI_IMPL_FN xml_named_node_iterator xml_named_node_iterator::operator--(int) { xml_named_node_iterator temp = *this; --*this; return temp; } PUGI_IMPL_FN xml_parse_result::xml_parse_result(): status(status_internal_error), offset(0), encoding(encoding_auto) { } PUGI_IMPL_FN xml_parse_result::operator bool() const { return status == status_ok; } PUGI_IMPL_FN const char* xml_parse_result::description() const { switch (status) { case status_ok: return "No error"; case status_file_not_found: return "File was not found"; case status_io_error: return "Error reading from file/stream"; case status_out_of_memory: return "Could not allocate memory"; case status_internal_error: return "Internal error occurred"; case status_unrecognized_tag: return "Could not determine tag type"; case status_bad_pi: return "Error parsing document declaration/processing instruction"; case status_bad_comment: return "Error parsing comment"; case status_bad_cdata: return "Error parsing CDATA section"; case status_bad_doctype: return "Error parsing document type declaration"; case status_bad_pcdata: return "Error parsing PCDATA section"; case status_bad_start_element: return "Error parsing start element tag"; case status_bad_attribute: return "Error parsing element attribute"; case status_bad_end_element: return "Error parsing end element tag"; case status_end_element_mismatch: return "Start-end tags mismatch"; case status_append_invalid_root: return "Unable to append nodes: root is not an element or document"; case status_no_document_element: return "No document element found"; default: return "Unknown error"; } } PUGI_IMPL_FN xml_document::xml_document(): _buffer(0) { _create(); } PUGI_IMPL_FN xml_document::~xml_document() { _destroy(); } #ifdef PUGIXML_HAS_MOVE PUGI_IMPL_FN xml_document::xml_document(xml_document&& rhs) PUGIXML_NOEXCEPT_IF_NOT_COMPACT: _buffer(0) { _create(); _move(rhs); } PUGI_IMPL_FN xml_document& xml_document::operator=(xml_document&& rhs) PUGIXML_NOEXCEPT_IF_NOT_COMPACT { if (this == &rhs) return *this; _destroy(); _create(); _move(rhs); return *this; } #endif PUGI_IMPL_FN void xml_document::reset() { _destroy(); _create(); } PUGI_IMPL_FN void xml_document::reset(const xml_document& proto) { reset(); impl::node_copy_tree(_root, proto._root); } PUGI_IMPL_FN void xml_document::_create() { assert(!_root); #ifdef PUGIXML_COMPACT // space for page marker for the first page (uint32_t), rounded up to pointer size; assumes pointers are at least 32-bit const size_t page_offset = sizeof(void*); #else const size_t page_offset = 0; #endif // initialize sentinel page PUGI_IMPL_STATIC_ASSERT(sizeof(impl::xml_memory_page) + sizeof(impl::xml_document_struct) + page_offset <= sizeof(_memory)); // prepare page structure impl::xml_memory_page* page = impl::xml_memory_page::construct(_memory); assert(page); page->busy_size = impl::xml_memory_page_size; // setup first page marker #ifdef PUGIXML_COMPACT // round-trip through void* to avoid 'cast increases required alignment of target type' warning page->compact_page_marker = reinterpret_cast(static_cast(reinterpret_cast(page) + sizeof(impl::xml_memory_page))); *page->compact_page_marker = sizeof(impl::xml_memory_page); #endif // allocate new root _root = new (reinterpret_cast(page) + sizeof(impl::xml_memory_page) + page_offset) impl::xml_document_struct(page); _root->prev_sibling_c = _root; // setup sentinel page page->allocator = static_cast(_root); // setup hash table pointer in allocator #ifdef PUGIXML_COMPACT page->allocator->_hash = &static_cast(_root)->hash; #endif // verify the document allocation assert(reinterpret_cast(_root) + sizeof(impl::xml_document_struct) <= _memory + sizeof(_memory)); } PUGI_IMPL_FN void xml_document::_destroy() { assert(_root); // destroy static storage if (_buffer) { impl::xml_memory::deallocate(_buffer); _buffer = 0; } // destroy extra buffers (note: no need to destroy linked list nodes, they're allocated using document allocator) for (impl::xml_extra_buffer* extra = static_cast(_root)->extra_buffers; extra; extra = extra->next) { if (extra->buffer) impl::xml_memory::deallocate(extra->buffer); } // destroy dynamic storage, leave sentinel page (it's in static memory) impl::xml_memory_page* root_page = PUGI_IMPL_GETPAGE(_root); assert(root_page && !root_page->prev); assert(reinterpret_cast(root_page) >= _memory && reinterpret_cast(root_page) < _memory + sizeof(_memory)); for (impl::xml_memory_page* page = root_page->next; page; ) { impl::xml_memory_page* next = page->next; impl::xml_allocator::deallocate_page(page); page = next; } #ifdef PUGIXML_COMPACT // destroy hash table static_cast(_root)->hash.clear(); #endif _root = 0; } #ifdef PUGIXML_HAS_MOVE PUGI_IMPL_FN void xml_document::_move(xml_document& rhs) PUGIXML_NOEXCEPT_IF_NOT_COMPACT { impl::xml_document_struct* doc = static_cast(_root); impl::xml_document_struct* other = static_cast(rhs._root); // save first child pointer for later; this needs hash access xml_node_struct* other_first_child = other->first_child; #ifdef PUGIXML_COMPACT // reserve space for the hash table up front; this is the only operation that can fail // if it does, we have no choice but to throw (if we have exceptions) if (other_first_child) { size_t other_children = 0; for (xml_node_struct* node = other_first_child; node; node = node->next_sibling) other_children++; // in compact mode, each pointer assignment could result in a hash table request // during move, we have to relocate document first_child and parents of all children // normally there's just one child and its parent has a pointerless encoding but // we assume the worst here if (!other->_hash->reserve(other_children + 1)) { #ifdef PUGIXML_NO_EXCEPTIONS return; #else throw std::bad_alloc(); #endif } } #endif // move allocation state // note that other->_root may point to the embedded document page, in which case we should keep original (empty) state if (other->_root != PUGI_IMPL_GETPAGE(other)) { doc->_root = other->_root; doc->_busy_size = other->_busy_size; } // move buffer state doc->buffer = other->buffer; doc->extra_buffers = other->extra_buffers; _buffer = rhs._buffer; #ifdef PUGIXML_COMPACT // move compact hash; note that the hash table can have pointers to other but they will be "inactive", similarly to nodes removed with remove_child doc->hash = other->hash; doc->_hash = &doc->hash; // make sure we don't access other hash up until the end when we reinitialize other document other->_hash = 0; #endif // move page structure impl::xml_memory_page* doc_page = PUGI_IMPL_GETPAGE(doc); assert(doc_page && !doc_page->prev && !doc_page->next); impl::xml_memory_page* other_page = PUGI_IMPL_GETPAGE(other); assert(other_page && !other_page->prev); // relink pages since root page is embedded into xml_document if (impl::xml_memory_page* page = other_page->next) { assert(page->prev == other_page); page->prev = doc_page; doc_page->next = page; other_page->next = 0; } // make sure pages point to the correct document state for (impl::xml_memory_page* page = doc_page->next; page; page = page->next) { assert(page->allocator == other); page->allocator = doc; #ifdef PUGIXML_COMPACT // this automatically migrates most children between documents and prevents ->parent assignment from allocating if (page->compact_shared_parent == other) page->compact_shared_parent = doc; #endif } // move tree structure assert(!doc->first_child); doc->first_child = other_first_child; for (xml_node_struct* node = other_first_child; node; node = node->next_sibling) { #ifdef PUGIXML_COMPACT // most children will have migrated when we reassigned compact_shared_parent assert(node->parent == other || node->parent == doc); node->parent = doc; #else assert(node->parent == other); node->parent = doc; #endif } // reset other document new (other) impl::xml_document_struct(PUGI_IMPL_GETPAGE(other)); rhs._buffer = 0; } #endif #ifndef PUGIXML_NO_STL PUGI_IMPL_FN xml_parse_result xml_document::load(std::basic_istream >& stream, unsigned int options, xml_encoding encoding) { reset(); return impl::load_stream_impl(static_cast(_root), stream, options, encoding, &_buffer); } PUGI_IMPL_FN xml_parse_result xml_document::load(std::basic_istream >& stream, unsigned int options) { reset(); return impl::load_stream_impl(static_cast(_root), stream, options, encoding_wchar, &_buffer); } #endif PUGI_IMPL_FN xml_parse_result xml_document::load_string(const char_t* contents, unsigned int options) { // Force native encoding (skip autodetection) #ifdef PUGIXML_WCHAR_MODE xml_encoding encoding = encoding_wchar; #else xml_encoding encoding = encoding_utf8; #endif return load_buffer(contents, impl::strlength(contents) * sizeof(char_t), options, encoding); } PUGI_IMPL_FN xml_parse_result xml_document::load(const char_t* contents, unsigned int options) { return load_string(contents, options); } PUGI_IMPL_FN xml_parse_result xml_document::load_file(const char* path_, unsigned int options, xml_encoding encoding) { reset(); using impl::auto_deleter; // MSVC7 workaround auto_deleter file(impl::open_file(path_, "rb"), impl::close_file); return impl::load_file_impl(static_cast(_root), file.data, options, encoding, &_buffer); } PUGI_IMPL_FN xml_parse_result xml_document::load_file(const wchar_t* path_, unsigned int options, xml_encoding encoding) { reset(); using impl::auto_deleter; // MSVC7 workaround auto_deleter file(impl::open_file_wide(path_, L"rb"), impl::close_file); return impl::load_file_impl(static_cast(_root), file.data, options, encoding, &_buffer); } PUGI_IMPL_FN xml_parse_result xml_document::load_buffer(const void* contents, size_t size, unsigned int options, xml_encoding encoding) { reset(); return impl::load_buffer_impl(static_cast(_root), _root, const_cast(contents), size, options, encoding, false, false, &_buffer); } PUGI_IMPL_FN xml_parse_result xml_document::load_buffer_inplace(void* contents, size_t size, unsigned int options, xml_encoding encoding) { reset(); return impl::load_buffer_impl(static_cast(_root), _root, contents, size, options, encoding, true, false, &_buffer); } PUGI_IMPL_FN xml_parse_result xml_document::load_buffer_inplace_own(void* contents, size_t size, unsigned int options, xml_encoding encoding) { reset(); return impl::load_buffer_impl(static_cast(_root), _root, contents, size, options, encoding, true, true, &_buffer); } PUGI_IMPL_FN void xml_document::save(xml_writer& writer, const char_t* indent, unsigned int flags, xml_encoding encoding) const { impl::xml_buffered_writer buffered_writer(writer, encoding); if ((flags & format_write_bom) && encoding != encoding_latin1) { // BOM always represents the codepoint U+FEFF, so just write it in native encoding #ifdef PUGIXML_WCHAR_MODE unsigned int bom = 0xfeff; buffered_writer.write(static_cast(bom)); #else buffered_writer.write('\xef', '\xbb', '\xbf'); #endif } if (!(flags & format_no_declaration) && !impl::has_declaration(_root)) { buffered_writer.write_string(PUGIXML_TEXT("'); if (!(flags & format_raw)) buffered_writer.write('\n'); } impl::node_output(buffered_writer, _root, indent, flags, 0); buffered_writer.flush(); } #ifndef PUGIXML_NO_STL PUGI_IMPL_FN void xml_document::save(std::basic_ostream >& stream, const char_t* indent, unsigned int flags, xml_encoding encoding) const { xml_writer_stream writer(stream); save(writer, indent, flags, encoding); } PUGI_IMPL_FN void xml_document::save(std::basic_ostream >& stream, const char_t* indent, unsigned int flags) const { xml_writer_stream writer(stream); save(writer, indent, flags, encoding_wchar); } #endif PUGI_IMPL_FN bool xml_document::save_file(const char* path_, const char_t* indent, unsigned int flags, xml_encoding encoding) const { using impl::auto_deleter; // MSVC7 workaround auto_deleter file(impl::open_file(path_, (flags & format_save_file_text) ? "w" : "wb"), impl::close_file); return impl::save_file_impl(*this, file.data, indent, flags, encoding) && fclose(file.release()) == 0; } PUGI_IMPL_FN bool xml_document::save_file(const wchar_t* path_, const char_t* indent, unsigned int flags, xml_encoding encoding) const { using impl::auto_deleter; // MSVC7 workaround auto_deleter file(impl::open_file_wide(path_, (flags & format_save_file_text) ? L"w" : L"wb"), impl::close_file); return impl::save_file_impl(*this, file.data, indent, flags, encoding) && fclose(file.release()) == 0; } PUGI_IMPL_FN xml_node xml_document::document_element() const { assert(_root); for (xml_node_struct* i = _root->first_child; i; i = i->next_sibling) if (PUGI_IMPL_NODETYPE(i) == node_element) return xml_node(i); return xml_node(); } #ifndef PUGIXML_NO_STL PUGI_IMPL_FN std::string PUGIXML_FUNCTION as_utf8(const wchar_t* str) { assert(str); return impl::as_utf8_impl(str, impl::strlength_wide(str)); } PUGI_IMPL_FN std::string PUGIXML_FUNCTION as_utf8(const std::basic_string& str) { return impl::as_utf8_impl(str.c_str(), str.size()); } PUGI_IMPL_FN std::basic_string PUGIXML_FUNCTION as_wide(const char* str) { assert(str); return impl::as_wide_impl(str, strlen(str)); } PUGI_IMPL_FN std::basic_string PUGIXML_FUNCTION as_wide(const std::string& str) { return impl::as_wide_impl(str.c_str(), str.size()); } #endif PUGI_IMPL_FN void PUGIXML_FUNCTION set_memory_management_functions(allocation_function allocate, deallocation_function deallocate) { impl::xml_memory::allocate = allocate; impl::xml_memory::deallocate = deallocate; } PUGI_IMPL_FN allocation_function PUGIXML_FUNCTION get_memory_allocation_function() { return impl::xml_memory::allocate; } PUGI_IMPL_FN deallocation_function PUGIXML_FUNCTION get_memory_deallocation_function() { return impl::xml_memory::deallocate; } } #if !defined(PUGIXML_NO_STL) && (defined(_MSC_VER) || defined(__ICC)) namespace std { // Workarounds for (non-standard) iterator category detection for older versions (MSVC7/IC8 and earlier) PUGI_IMPL_FN std::bidirectional_iterator_tag _Iter_cat(const pugi::xml_node_iterator&) { return std::bidirectional_iterator_tag(); } PUGI_IMPL_FN std::bidirectional_iterator_tag _Iter_cat(const pugi::xml_attribute_iterator&) { return std::bidirectional_iterator_tag(); } PUGI_IMPL_FN std::bidirectional_iterator_tag _Iter_cat(const pugi::xml_named_node_iterator&) { return std::bidirectional_iterator_tag(); } } #endif #if !defined(PUGIXML_NO_STL) && defined(__SUNPRO_CC) namespace std { // Workarounds for (non-standard) iterator category detection PUGI_IMPL_FN std::bidirectional_iterator_tag __iterator_category(const pugi::xml_node_iterator&) { return std::bidirectional_iterator_tag(); } PUGI_IMPL_FN std::bidirectional_iterator_tag __iterator_category(const pugi::xml_attribute_iterator&) { return std::bidirectional_iterator_tag(); } PUGI_IMPL_FN std::bidirectional_iterator_tag __iterator_category(const pugi::xml_named_node_iterator&) { return std::bidirectional_iterator_tag(); } } #endif #ifndef PUGIXML_NO_XPATH // STL replacements PUGI_IMPL_NS_BEGIN struct equal_to { template bool operator()(const T& lhs, const T& rhs) const { return lhs == rhs; } }; struct not_equal_to { template bool operator()(const T& lhs, const T& rhs) const { return lhs != rhs; } }; struct less { template bool operator()(const T& lhs, const T& rhs) const { return lhs < rhs; } }; struct less_equal { template bool operator()(const T& lhs, const T& rhs) const { return lhs <= rhs; } }; template inline void swap(T& lhs, T& rhs) { T temp = lhs; lhs = rhs; rhs = temp; } template PUGI_IMPL_FN I min_element(I begin, I end, const Pred& pred) { I result = begin; for (I it = begin + 1; it != end; ++it) if (pred(*it, *result)) result = it; return result; } template PUGI_IMPL_FN void reverse(I begin, I end) { while (end - begin > 1) swap(*begin++, *--end); } template PUGI_IMPL_FN I unique(I begin, I end) { // fast skip head while (end - begin > 1 && *begin != *(begin + 1)) begin++; if (begin == end) return begin; // last written element I write = begin++; // merge unique elements while (begin != end) { if (*begin != *write) *++write = *begin++; else begin++; } // past-the-end (write points to live element) return write + 1; } template PUGI_IMPL_FN void insertion_sort(T* begin, T* end, const Pred& pred) { if (begin == end) return; for (T* it = begin + 1; it != end; ++it) { T val = *it; T* hole = it; // move hole backwards while (hole > begin && pred(val, *(hole - 1))) { *hole = *(hole - 1); hole--; } // fill hole with element *hole = val; } } template inline I median3(I first, I middle, I last, const Pred& pred) { if (pred(*middle, *first)) swap(middle, first); if (pred(*last, *middle)) swap(last, middle); if (pred(*middle, *first)) swap(middle, first); return middle; } template PUGI_IMPL_FN void partition3(T* begin, T* end, T pivot, const Pred& pred, T** out_eqbeg, T** out_eqend) { // invariant: array is split into 4 groups: = < ? > (each variable denotes the boundary between the groups) T* eq = begin; T* lt = begin; T* gt = end; while (lt < gt) { if (pred(*lt, pivot)) lt++; else if (*lt == pivot) swap(*eq++, *lt++); else swap(*lt, *--gt); } // we now have just 4 groups: = < >; move equal elements to the middle T* eqbeg = gt; for (T* it = begin; it != eq; ++it) swap(*it, *--eqbeg); *out_eqbeg = eqbeg; *out_eqend = gt; } template PUGI_IMPL_FN void sort(I begin, I end, const Pred& pred) { // sort large chunks while (end - begin > 16) { // find median element I middle = begin + (end - begin) / 2; I median = median3(begin, middle, end - 1, pred); // partition in three chunks (< = >) I eqbeg, eqend; partition3(begin, end, *median, pred, &eqbeg, &eqend); // loop on larger half if (eqbeg - begin > end - eqend) { sort(eqend, end, pred); end = eqbeg; } else { sort(begin, eqbeg, pred); begin = eqend; } } // insertion sort small chunk insertion_sort(begin, end, pred); } PUGI_IMPL_FN bool hash_insert(const void** table, size_t size, const void* key) { assert(key); unsigned int h = static_cast(reinterpret_cast(key)); // MurmurHash3 32-bit finalizer h ^= h >> 16; h *= 0x85ebca6bu; h ^= h >> 13; h *= 0xc2b2ae35u; h ^= h >> 16; size_t hashmod = size - 1; size_t bucket = h & hashmod; for (size_t probe = 0; probe <= hashmod; ++probe) { if (table[bucket] == 0) { table[bucket] = key; return true; } if (table[bucket] == key) return false; // hash collision, quadratic probing bucket = (bucket + probe + 1) & hashmod; } assert(false && "Hash table is full"); // unreachable return false; } PUGI_IMPL_NS_END // Allocator used for AST and evaluation stacks PUGI_IMPL_NS_BEGIN static const size_t xpath_memory_page_size = #ifdef PUGIXML_MEMORY_XPATH_PAGE_SIZE PUGIXML_MEMORY_XPATH_PAGE_SIZE #else 4096 #endif ; static const uintptr_t xpath_memory_block_alignment = sizeof(double) > sizeof(void*) ? sizeof(double) : sizeof(void*); struct xpath_memory_block { xpath_memory_block* next; size_t capacity; union { char data[xpath_memory_page_size]; double alignment; }; }; struct xpath_allocator { xpath_memory_block* _root; size_t _root_size; bool* _error; xpath_allocator(xpath_memory_block* root, bool* error = 0): _root(root), _root_size(0), _error(error) { } void* allocate(size_t size) { // round size up to block alignment boundary size = (size + xpath_memory_block_alignment - 1) & ~(xpath_memory_block_alignment - 1); if (_root_size + size <= _root->capacity) { void* buf = &_root->data[0] + _root_size; _root_size += size; return buf; } else { // make sure we have at least 1/4th of the page free after allocation to satisfy subsequent allocation requests size_t block_capacity_base = sizeof(_root->data); size_t block_capacity_req = size + block_capacity_base / 4; size_t block_capacity = (block_capacity_base > block_capacity_req) ? block_capacity_base : block_capacity_req; size_t block_size = block_capacity + offsetof(xpath_memory_block, data); xpath_memory_block* block = static_cast(xml_memory::allocate(block_size)); if (!block) { if (_error) *_error = true; return 0; } block->next = _root; block->capacity = block_capacity; _root = block; _root_size = size; return block->data; } } void* reallocate(void* ptr, size_t old_size, size_t new_size) { // round size up to block alignment boundary old_size = (old_size + xpath_memory_block_alignment - 1) & ~(xpath_memory_block_alignment - 1); new_size = (new_size + xpath_memory_block_alignment - 1) & ~(xpath_memory_block_alignment - 1); // we can only reallocate the last object assert(ptr == 0 || static_cast(ptr) + old_size == &_root->data[0] + _root_size); // try to reallocate the object inplace if (ptr && _root_size - old_size + new_size <= _root->capacity) { _root_size = _root_size - old_size + new_size; return ptr; } // allocate a new block void* result = allocate(new_size); if (!result) return 0; // we have a new block if (ptr) { // copy old data (we only support growing) assert(new_size >= old_size); memcpy(result, ptr, old_size); // free the previous page if it had no other objects assert(_root->data == result); assert(_root->next); if (_root->next->data == ptr) { // deallocate the whole page, unless it was the first one xpath_memory_block* next = _root->next->next; if (next) { xml_memory::deallocate(_root->next); _root->next = next; } } } return result; } void revert(const xpath_allocator& state) { // free all new pages xpath_memory_block* cur = _root; while (cur != state._root) { xpath_memory_block* next = cur->next; xml_memory::deallocate(cur); cur = next; } // restore state _root = state._root; _root_size = state._root_size; } void release() { xpath_memory_block* cur = _root; assert(cur); while (cur->next) { xpath_memory_block* next = cur->next; xml_memory::deallocate(cur); cur = next; } } }; struct xpath_allocator_capture { xpath_allocator_capture(xpath_allocator* alloc): _target(alloc), _state(*alloc) { } ~xpath_allocator_capture() { _target->revert(_state); } xpath_allocator* _target; xpath_allocator _state; }; struct xpath_stack { xpath_allocator* result; xpath_allocator* temp; }; struct xpath_stack_data { xpath_memory_block blocks[2]; xpath_allocator result; xpath_allocator temp; xpath_stack stack; bool oom; xpath_stack_data(): result(blocks + 0, &oom), temp(blocks + 1, &oom), oom(false) { blocks[0].next = blocks[1].next = 0; blocks[0].capacity = blocks[1].capacity = sizeof(blocks[0].data); stack.result = &result; stack.temp = &temp; } ~xpath_stack_data() { result.release(); temp.release(); } }; PUGI_IMPL_NS_END // String class PUGI_IMPL_NS_BEGIN class xpath_string { const char_t* _buffer; bool _uses_heap; size_t _length_heap; static char_t* duplicate_string(const char_t* string, size_t length, xpath_allocator* alloc) { char_t* result = static_cast(alloc->allocate((length + 1) * sizeof(char_t))); if (!result) return 0; memcpy(result, string, length * sizeof(char_t)); result[length] = 0; return result; } xpath_string(const char_t* buffer, bool uses_heap_, size_t length_heap): _buffer(buffer), _uses_heap(uses_heap_), _length_heap(length_heap) { } public: static xpath_string from_const(const char_t* str) { return xpath_string(str, false, 0); } static xpath_string from_heap_preallocated(const char_t* begin, const char_t* end) { assert(begin <= end && *end == 0); return xpath_string(begin, true, static_cast(end - begin)); } static xpath_string from_heap(const char_t* begin, const char_t* end, xpath_allocator* alloc) { assert(begin <= end); if (begin == end) return xpath_string(); size_t length = static_cast(end - begin); const char_t* data = duplicate_string(begin, length, alloc); return data ? xpath_string(data, true, length) : xpath_string(); } xpath_string(): _buffer(PUGIXML_TEXT("")), _uses_heap(false), _length_heap(0) { } void append(const xpath_string& o, xpath_allocator* alloc) { // skip empty sources if (!*o._buffer) return; // fast append for constant empty target and constant source if (!*_buffer && !_uses_heap && !o._uses_heap) { _buffer = o._buffer; } else { // need to make heap copy size_t target_length = length(); size_t source_length = o.length(); size_t result_length = target_length + source_length; // allocate new buffer char_t* result = static_cast(alloc->reallocate(_uses_heap ? const_cast(_buffer) : 0, (target_length + 1) * sizeof(char_t), (result_length + 1) * sizeof(char_t))); if (!result) return; // append first string to the new buffer in case there was no reallocation if (!_uses_heap) memcpy(result, _buffer, target_length * sizeof(char_t)); // append second string to the new buffer memcpy(result + target_length, o._buffer, source_length * sizeof(char_t)); result[result_length] = 0; // finalize _buffer = result; _uses_heap = true; _length_heap = result_length; } } const char_t* c_str() const { return _buffer; } size_t length() const { return _uses_heap ? _length_heap : strlength(_buffer); } char_t* data(xpath_allocator* alloc) { // make private heap copy if (!_uses_heap) { size_t length_ = strlength(_buffer); const char_t* data_ = duplicate_string(_buffer, length_, alloc); if (!data_) return 0; _buffer = data_; _uses_heap = true; _length_heap = length_; } return const_cast(_buffer); } bool empty() const { return *_buffer == 0; } bool operator==(const xpath_string& o) const { return strequal(_buffer, o._buffer); } bool operator!=(const xpath_string& o) const { return !strequal(_buffer, o._buffer); } bool uses_heap() const { return _uses_heap; } }; PUGI_IMPL_NS_END PUGI_IMPL_NS_BEGIN PUGI_IMPL_FN bool starts_with(const char_t* string, const char_t* pattern) { while (*pattern && *string == *pattern) { string++; pattern++; } return *pattern == 0; } PUGI_IMPL_FN const char_t* find_char(const char_t* s, char_t c) { #ifdef PUGIXML_WCHAR_MODE return wcschr(s, c); #else return strchr(s, c); #endif } PUGI_IMPL_FN const char_t* find_substring(const char_t* s, const char_t* p) { #ifdef PUGIXML_WCHAR_MODE // MSVC6 wcsstr bug workaround (if s is empty it always returns 0) return (*p == 0) ? s : wcsstr(s, p); #else return strstr(s, p); #endif } // Converts symbol to lower case, if it is an ASCII one PUGI_IMPL_FN char_t tolower_ascii(char_t ch) { return static_cast(ch - 'A') < 26 ? static_cast(ch | ' ') : ch; } PUGI_IMPL_FN xpath_string string_value(const xpath_node& na, xpath_allocator* alloc) { if (na.attribute()) return xpath_string::from_const(na.attribute().value()); else { xml_node n = na.node(); switch (n.type()) { case node_pcdata: case node_cdata: case node_comment: case node_pi: return xpath_string::from_const(n.value()); case node_document: case node_element: { xpath_string result; // element nodes can have value if parse_embed_pcdata was used if (n.value()[0]) result.append(xpath_string::from_const(n.value()), alloc); xml_node cur = n.first_child(); while (cur && cur != n) { if (cur.type() == node_pcdata || cur.type() == node_cdata) result.append(xpath_string::from_const(cur.value()), alloc); if (cur.first_child()) cur = cur.first_child(); else if (cur.next_sibling()) cur = cur.next_sibling(); else { while (!cur.next_sibling() && cur != n) cur = cur.parent(); if (cur != n) cur = cur.next_sibling(); } } return result; } default: return xpath_string(); } } } PUGI_IMPL_FN bool node_is_before_sibling(xml_node_struct* ln, xml_node_struct* rn) { assert(ln->parent == rn->parent); // there is no common ancestor (the shared parent is null), nodes are from different documents if (!ln->parent) return ln < rn; // determine sibling order xml_node_struct* ls = ln; xml_node_struct* rs = rn; while (ls && rs) { if (ls == rn) return true; if (rs == ln) return false; ls = ls->next_sibling; rs = rs->next_sibling; } // if rn sibling chain ended ln must be before rn return !rs; } PUGI_IMPL_FN bool node_is_before(xml_node_struct* ln, xml_node_struct* rn) { // find common ancestor at the same depth, if any xml_node_struct* lp = ln; xml_node_struct* rp = rn; while (lp && rp && lp->parent != rp->parent) { lp = lp->parent; rp = rp->parent; } // parents are the same! if (lp && rp) return node_is_before_sibling(lp, rp); // nodes are at different depths, need to normalize heights bool left_higher = !lp; while (lp) { lp = lp->parent; ln = ln->parent; } while (rp) { rp = rp->parent; rn = rn->parent; } // one node is the ancestor of the other if (ln == rn) return left_higher; // find common ancestor... again while (ln->parent != rn->parent) { ln = ln->parent; rn = rn->parent; } return node_is_before_sibling(ln, rn); } PUGI_IMPL_FN bool node_is_ancestor(xml_node_struct* parent, xml_node_struct* node) { while (node && node != parent) node = node->parent; return parent && node == parent; } PUGI_IMPL_FN const void* document_buffer_order(const xpath_node& xnode) { xml_node_struct* node = xnode.node().internal_object(); if (node) { if ((get_document(node).header & xml_memory_page_contents_shared_mask) == 0) { if (node->name && (node->header & impl::xml_memory_page_name_allocated_or_shared_mask) == 0) return node->name; if (node->value && (node->header & impl::xml_memory_page_value_allocated_or_shared_mask) == 0) return node->value; } return 0; } xml_attribute_struct* attr = xnode.attribute().internal_object(); if (attr) { if ((get_document(attr).header & xml_memory_page_contents_shared_mask) == 0) { if ((attr->header & impl::xml_memory_page_name_allocated_or_shared_mask) == 0) return attr->name; if ((attr->header & impl::xml_memory_page_value_allocated_or_shared_mask) == 0) return attr->value; } return 0; } return 0; } struct document_order_comparator { bool operator()(const xpath_node& lhs, const xpath_node& rhs) const { // optimized document order based check const void* lo = document_buffer_order(lhs); const void* ro = document_buffer_order(rhs); if (lo && ro) return lo < ro; // slow comparison xml_node ln = lhs.node(), rn = rhs.node(); // compare attributes if (lhs.attribute() && rhs.attribute()) { // shared parent if (lhs.parent() == rhs.parent()) { // determine sibling order for (xml_attribute a = lhs.attribute(); a; a = a.next_attribute()) if (a == rhs.attribute()) return true; return false; } // compare attribute parents ln = lhs.parent(); rn = rhs.parent(); } else if (lhs.attribute()) { // attributes go after the parent element if (lhs.parent() == rhs.node()) return false; ln = lhs.parent(); } else if (rhs.attribute()) { // attributes go after the parent element if (rhs.parent() == lhs.node()) return true; rn = rhs.parent(); } if (ln == rn) return false; if (!ln || !rn) return ln < rn; return node_is_before(ln.internal_object(), rn.internal_object()); } }; PUGI_IMPL_FN double gen_nan() { #if defined(__STDC_IEC_559__) || ((FLT_RADIX - 0 == 2) && (FLT_MAX_EXP - 0 == 128) && (FLT_MANT_DIG - 0 == 24)) PUGI_IMPL_STATIC_ASSERT(sizeof(float) == sizeof(uint32_t)); typedef uint32_t UI; // BCC5 workaround union { float f; UI i; } u; u.i = 0x7fc00000; return double(u.f); #else // fallback const volatile double zero = 0.0; return zero / zero; #endif } PUGI_IMPL_FN bool is_nan(double value) { #if defined(PUGI_IMPL_MSVC_CRT_VERSION) || defined(__BORLANDC__) return !!_isnan(value); #elif defined(fpclassify) && defined(FP_NAN) return fpclassify(value) == FP_NAN; #else // fallback const volatile double v = value; return v != v; #endif } PUGI_IMPL_FN const char_t* convert_number_to_string_special(double value) { #if defined(PUGI_IMPL_MSVC_CRT_VERSION) || defined(__BORLANDC__) if (_finite(value)) return (value == 0) ? PUGIXML_TEXT("0") : 0; if (_isnan(value)) return PUGIXML_TEXT("NaN"); return value > 0 ? PUGIXML_TEXT("Infinity") : PUGIXML_TEXT("-Infinity"); #elif defined(fpclassify) && defined(FP_NAN) && defined(FP_INFINITE) && defined(FP_ZERO) switch (fpclassify(value)) { case FP_NAN: return PUGIXML_TEXT("NaN"); case FP_INFINITE: return value > 0 ? PUGIXML_TEXT("Infinity") : PUGIXML_TEXT("-Infinity"); case FP_ZERO: return PUGIXML_TEXT("0"); default: return 0; } #else // fallback const volatile double v = value; if (v == 0) return PUGIXML_TEXT("0"); if (v != v) return PUGIXML_TEXT("NaN"); if (v * 2 == v) return value > 0 ? PUGIXML_TEXT("Infinity") : PUGIXML_TEXT("-Infinity"); return 0; #endif } PUGI_IMPL_FN bool convert_number_to_boolean(double value) { return (value != 0 && !is_nan(value)); } PUGI_IMPL_FN void truncate_zeros(char* begin, char* end) { while (begin != end && end[-1] == '0') end--; *end = 0; } // gets mantissa digits in the form of 0.xxxxx with 0. implied and the exponent #if defined(PUGI_IMPL_MSVC_CRT_VERSION) && PUGI_IMPL_MSVC_CRT_VERSION >= 1400 PUGI_IMPL_FN void convert_number_to_mantissa_exponent(double value, char (&buffer)[32], char** out_mantissa, int* out_exponent) { // get base values int sign, exponent; _ecvt_s(buffer, sizeof(buffer), value, DBL_DIG + 1, &exponent, &sign); // truncate redundant zeros truncate_zeros(buffer, buffer + strlen(buffer)); // fill results *out_mantissa = buffer; *out_exponent = exponent; } #else PUGI_IMPL_FN void convert_number_to_mantissa_exponent(double value, char (&buffer)[32], char** out_mantissa, int* out_exponent) { // get a scientific notation value with IEEE DBL_DIG decimals PUGI_IMPL_SNPRINTF(buffer, "%.*e", DBL_DIG, value); // get the exponent (possibly negative) char* exponent_string = strchr(buffer, 'e'); assert(exponent_string); int exponent = atoi(exponent_string + 1); // extract mantissa string: skip sign char* mantissa = buffer[0] == '-' ? buffer + 1 : buffer; assert(mantissa[0] != '0' && (mantissa[1] == '.' || mantissa[1] == ',')); // divide mantissa by 10 to eliminate integer part mantissa[1] = mantissa[0]; mantissa++; exponent++; // remove extra mantissa digits and zero-terminate mantissa truncate_zeros(mantissa, exponent_string); // fill results *out_mantissa = mantissa; *out_exponent = exponent; } #endif PUGI_IMPL_FN xpath_string convert_number_to_string(double value, xpath_allocator* alloc) { // try special number conversion const char_t* special = convert_number_to_string_special(value); if (special) return xpath_string::from_const(special); // get mantissa + exponent form char mantissa_buffer[32]; char* mantissa; int exponent; convert_number_to_mantissa_exponent(value, mantissa_buffer, &mantissa, &exponent); // allocate a buffer of suitable length for the number size_t result_size = strlen(mantissa_buffer) + (exponent > 0 ? exponent : -exponent) + 4; char_t* result = static_cast(alloc->allocate(sizeof(char_t) * result_size)); if (!result) return xpath_string(); // make the number! char_t* s = result; // sign if (value < 0) *s++ = '-'; // integer part if (exponent <= 0) { *s++ = '0'; } else { while (exponent > 0) { assert(*mantissa == 0 || static_cast(*mantissa - '0') <= 9); *s++ = *mantissa ? *mantissa++ : '0'; exponent--; } } // fractional part if (*mantissa) { // decimal point *s++ = '.'; // extra zeroes from negative exponent while (exponent < 0) { *s++ = '0'; exponent++; } // extra mantissa digits while (*mantissa) { assert(static_cast(*mantissa - '0') <= 9); *s++ = *mantissa++; } } // zero-terminate assert(s < result + result_size); *s = 0; return xpath_string::from_heap_preallocated(result, s); } PUGI_IMPL_FN bool check_string_to_number_format(const char_t* string) { // parse leading whitespace while (PUGI_IMPL_IS_CHARTYPE(*string, ct_space)) ++string; // parse sign if (*string == '-') ++string; if (!*string) return false; // if there is no integer part, there should be a decimal part with at least one digit if (!PUGI_IMPL_IS_CHARTYPEX(string[0], ctx_digit) && (string[0] != '.' || !PUGI_IMPL_IS_CHARTYPEX(string[1], ctx_digit))) return false; // parse integer part while (PUGI_IMPL_IS_CHARTYPEX(*string, ctx_digit)) ++string; // parse decimal part if (*string == '.') { ++string; while (PUGI_IMPL_IS_CHARTYPEX(*string, ctx_digit)) ++string; } // parse trailing whitespace while (PUGI_IMPL_IS_CHARTYPE(*string, ct_space)) ++string; return *string == 0; } PUGI_IMPL_FN double convert_string_to_number(const char_t* string) { // check string format if (!check_string_to_number_format(string)) return gen_nan(); // parse string #ifdef PUGIXML_WCHAR_MODE return wcstod(string, 0); #else return strtod(string, 0); #endif } PUGI_IMPL_FN bool convert_string_to_number_scratch(char_t (&buffer)[32], const char_t* begin, const char_t* end, double* out_result) { size_t length = static_cast(end - begin); char_t* scratch = buffer; if (length >= sizeof(buffer) / sizeof(buffer[0])) { // need to make dummy on-heap copy scratch = static_cast(xml_memory::allocate((length + 1) * sizeof(char_t))); if (!scratch) return false; } // copy string to zero-terminated buffer and perform conversion memcpy(scratch, begin, length * sizeof(char_t)); scratch[length] = 0; *out_result = convert_string_to_number(scratch); // free dummy buffer if (scratch != buffer) xml_memory::deallocate(scratch); return true; } PUGI_IMPL_FN double round_nearest(double value) { return floor(value + 0.5); } PUGI_IMPL_FN double round_nearest_nzero(double value) { // same as round_nearest, but returns -0 for [-0.5, -0] // ceil is used to differentiate between +0 and -0 (we return -0 for [-0.5, -0] and +0 for +0) return (value >= -0.5 && value <= 0) ? ceil(value) : floor(value + 0.5); } PUGI_IMPL_FN const char_t* qualified_name(const xpath_node& node) { return node.attribute() ? node.attribute().name() : node.node().name(); } PUGI_IMPL_FN const char_t* local_name(const xpath_node& node) { const char_t* name = qualified_name(node); const char_t* p = find_char(name, ':'); return p ? p + 1 : name; } struct namespace_uri_predicate { const char_t* prefix; size_t prefix_length; namespace_uri_predicate(const char_t* name) { const char_t* pos = find_char(name, ':'); prefix = pos ? name : 0; prefix_length = pos ? static_cast(pos - name) : 0; } bool operator()(xml_attribute a) const { const char_t* name = a.name(); if (!starts_with(name, PUGIXML_TEXT("xmlns"))) return false; return prefix ? name[5] == ':' && strequalrange(name + 6, prefix, prefix_length) : name[5] == 0; } }; PUGI_IMPL_FN const char_t* namespace_uri(xml_node node) { namespace_uri_predicate pred = node.name(); xml_node p = node; while (p) { xml_attribute a = p.find_attribute(pred); if (a) return a.value(); p = p.parent(); } return PUGIXML_TEXT(""); } PUGI_IMPL_FN const char_t* namespace_uri(xml_attribute attr, xml_node parent) { namespace_uri_predicate pred = attr.name(); // Default namespace does not apply to attributes if (!pred.prefix) return PUGIXML_TEXT(""); xml_node p = parent; while (p) { xml_attribute a = p.find_attribute(pred); if (a) return a.value(); p = p.parent(); } return PUGIXML_TEXT(""); } PUGI_IMPL_FN const char_t* namespace_uri(const xpath_node& node) { return node.attribute() ? namespace_uri(node.attribute(), node.parent()) : namespace_uri(node.node()); } PUGI_IMPL_FN char_t* normalize_space(char_t* buffer) { char_t* write = buffer; for (char_t* it = buffer; *it; ) { char_t ch = *it++; if (PUGI_IMPL_IS_CHARTYPE(ch, ct_space)) { // replace whitespace sequence with single space while (PUGI_IMPL_IS_CHARTYPE(*it, ct_space)) it++; // avoid leading spaces if (write != buffer) *write++ = ' '; } else *write++ = ch; } // remove trailing space if (write != buffer && PUGI_IMPL_IS_CHARTYPE(write[-1], ct_space)) write--; // zero-terminate *write = 0; return write; } PUGI_IMPL_FN char_t* translate(char_t* buffer, const char_t* from, const char_t* to, size_t to_length) { char_t* write = buffer; while (*buffer) { PUGI_IMPL_DMC_VOLATILE char_t ch = *buffer++; const char_t* pos = find_char(from, ch); if (!pos) *write++ = ch; // do not process else if (static_cast(pos - from) < to_length) *write++ = to[pos - from]; // replace } // zero-terminate *write = 0; return write; } PUGI_IMPL_FN unsigned char* translate_table_generate(xpath_allocator* alloc, const char_t* from, const char_t* to) { unsigned char table[128] = {0}; while (*from) { unsigned int fc = static_cast(*from); unsigned int tc = static_cast(*to); if (fc >= 128 || tc >= 128) return 0; // code=128 means "skip character" if (!table[fc]) table[fc] = static_cast(tc ? tc : 128); from++; if (tc) to++; } for (int i = 0; i < 128; ++i) if (!table[i]) table[i] = static_cast(i); void* result = alloc->allocate(sizeof(table)); if (!result) return 0; memcpy(result, table, sizeof(table)); return static_cast(result); } PUGI_IMPL_FN char_t* translate_table(char_t* buffer, const unsigned char* table) { char_t* write = buffer; while (*buffer) { char_t ch = *buffer++; unsigned int index = static_cast(ch); if (index < 128) { unsigned char code = table[index]; // code=128 means "skip character" (table size is 128 so 128 can be a special value) // this code skips these characters without extra branches *write = static_cast(code); write += 1 - (code >> 7); } else { *write++ = ch; } } // zero-terminate *write = 0; return write; } inline bool is_xpath_attribute(const char_t* name) { return !(starts_with(name, PUGIXML_TEXT("xmlns")) && (name[5] == 0 || name[5] == ':')); } struct xpath_variable_boolean: xpath_variable { xpath_variable_boolean(): xpath_variable(xpath_type_boolean), value(false) { } bool value; char_t name[1]; }; struct xpath_variable_number: xpath_variable { xpath_variable_number(): xpath_variable(xpath_type_number), value(0) { } double value; char_t name[1]; }; struct xpath_variable_string: xpath_variable { xpath_variable_string(): xpath_variable(xpath_type_string), value(0) { } ~xpath_variable_string() { if (value) xml_memory::deallocate(value); } char_t* value; char_t name[1]; }; struct xpath_variable_node_set: xpath_variable { xpath_variable_node_set(): xpath_variable(xpath_type_node_set) { } xpath_node_set value; char_t name[1]; }; static const xpath_node_set dummy_node_set; PUGI_IMPL_FN PUGI_IMPL_UNSIGNED_OVERFLOW unsigned int hash_string(const char_t* str) { // Jenkins one-at-a-time hash (http://en.wikipedia.org/wiki/Jenkins_hash_function#one-at-a-time) unsigned int result = 0; while (*str) { result += static_cast(*str++); result += result << 10; result ^= result >> 6; } result += result << 3; result ^= result >> 11; result += result << 15; return result; } template PUGI_IMPL_FN T* new_xpath_variable(const char_t* name) { size_t length = strlength(name); if (length == 0) return 0; // empty variable names are invalid // $$ we can't use offsetof(T, name) because T is non-POD, so we just allocate additional length characters void* memory = xml_memory::allocate(sizeof(T) + length * sizeof(char_t)); if (!memory) return 0; T* result = new (memory) T(); memcpy(result->name, name, (length + 1) * sizeof(char_t)); return result; } PUGI_IMPL_FN xpath_variable* new_xpath_variable(xpath_value_type type, const char_t* name) { switch (type) { case xpath_type_node_set: return new_xpath_variable(name); case xpath_type_number: return new_xpath_variable(name); case xpath_type_string: return new_xpath_variable(name); case xpath_type_boolean: return new_xpath_variable(name); default: return 0; } } template PUGI_IMPL_FN void delete_xpath_variable(T* var) { var->~T(); xml_memory::deallocate(var); } PUGI_IMPL_FN void delete_xpath_variable(xpath_value_type type, xpath_variable* var) { switch (type) { case xpath_type_node_set: delete_xpath_variable(static_cast(var)); break; case xpath_type_number: delete_xpath_variable(static_cast(var)); break; case xpath_type_string: delete_xpath_variable(static_cast(var)); break; case xpath_type_boolean: delete_xpath_variable(static_cast(var)); break; default: assert(false && "Invalid variable type"); // unreachable } } PUGI_IMPL_FN bool copy_xpath_variable(xpath_variable* lhs, const xpath_variable* rhs) { switch (rhs->type()) { case xpath_type_node_set: return lhs->set(static_cast(rhs)->value); case xpath_type_number: return lhs->set(static_cast(rhs)->value); case xpath_type_string: return lhs->set(static_cast(rhs)->value); case xpath_type_boolean: return lhs->set(static_cast(rhs)->value); default: assert(false && "Invalid variable type"); // unreachable return false; } } PUGI_IMPL_FN bool get_variable_scratch(char_t (&buffer)[32], xpath_variable_set* set, const char_t* begin, const char_t* end, xpath_variable** out_result) { size_t length = static_cast(end - begin); char_t* scratch = buffer; if (length >= sizeof(buffer) / sizeof(buffer[0])) { // need to make dummy on-heap copy scratch = static_cast(xml_memory::allocate((length + 1) * sizeof(char_t))); if (!scratch) return false; } // copy string to zero-terminated buffer and perform lookup memcpy(scratch, begin, length * sizeof(char_t)); scratch[length] = 0; *out_result = set->get(scratch); // free dummy buffer if (scratch != buffer) xml_memory::deallocate(scratch); return true; } PUGI_IMPL_NS_END // Internal node set class PUGI_IMPL_NS_BEGIN PUGI_IMPL_FN xpath_node_set::type_t xpath_get_order(const xpath_node* begin, const xpath_node* end) { if (end - begin < 2) return xpath_node_set::type_sorted; document_order_comparator cmp; bool first = cmp(begin[0], begin[1]); for (const xpath_node* it = begin + 1; it + 1 < end; ++it) if (cmp(it[0], it[1]) != first) return xpath_node_set::type_unsorted; return first ? xpath_node_set::type_sorted : xpath_node_set::type_sorted_reverse; } PUGI_IMPL_FN xpath_node_set::type_t xpath_sort(xpath_node* begin, xpath_node* end, xpath_node_set::type_t type, bool rev) { xpath_node_set::type_t order = rev ? xpath_node_set::type_sorted_reverse : xpath_node_set::type_sorted; if (type == xpath_node_set::type_unsorted) { xpath_node_set::type_t sorted = xpath_get_order(begin, end); if (sorted == xpath_node_set::type_unsorted) { sort(begin, end, document_order_comparator()); type = xpath_node_set::type_sorted; } else type = sorted; } if (type != order) reverse(begin, end); return order; } PUGI_IMPL_FN xpath_node xpath_first(const xpath_node* begin, const xpath_node* end, xpath_node_set::type_t type) { if (begin == end) return xpath_node(); switch (type) { case xpath_node_set::type_sorted: return *begin; case xpath_node_set::type_sorted_reverse: return *(end - 1); case xpath_node_set::type_unsorted: return *min_element(begin, end, document_order_comparator()); default: assert(false && "Invalid node set type"); // unreachable return xpath_node(); } } class xpath_node_set_raw { xpath_node_set::type_t _type; xpath_node* _begin; xpath_node* _end; xpath_node* _eos; public: xpath_node_set_raw(): _type(xpath_node_set::type_unsorted), _begin(0), _end(0), _eos(0) { } xpath_node* begin() const { return _begin; } xpath_node* end() const { return _end; } bool empty() const { return _begin == _end; } size_t size() const { return static_cast(_end - _begin); } xpath_node first() const { return xpath_first(_begin, _end, _type); } void push_back_grow(const xpath_node& node, xpath_allocator* alloc); void push_back(const xpath_node& node, xpath_allocator* alloc) { if (_end != _eos) *_end++ = node; else push_back_grow(node, alloc); } void append(const xpath_node* begin_, const xpath_node* end_, xpath_allocator* alloc) { if (begin_ == end_) return; size_t size_ = static_cast(_end - _begin); size_t capacity = static_cast(_eos - _begin); size_t count = static_cast(end_ - begin_); if (size_ + count > capacity) { // reallocate the old array or allocate a new one xpath_node* data = static_cast(alloc->reallocate(_begin, capacity * sizeof(xpath_node), (size_ + count) * sizeof(xpath_node))); if (!data) return; // finalize _begin = data; _end = data + size_; _eos = data + size_ + count; } memcpy(_end, begin_, count * sizeof(xpath_node)); _end += count; } void sort_do() { _type = xpath_sort(_begin, _end, _type, false); } void truncate(xpath_node* pos) { assert(_begin <= pos && pos <= _end); _end = pos; } void remove_duplicates(xpath_allocator* alloc) { if (_type == xpath_node_set::type_unsorted && _end - _begin > 2) { xpath_allocator_capture cr(alloc); size_t size_ = static_cast(_end - _begin); size_t hash_size = 1; while (hash_size < size_ + size_ / 2) hash_size *= 2; const void** hash_data = static_cast(alloc->allocate(hash_size * sizeof(void**))); if (!hash_data) return; memset(hash_data, 0, hash_size * sizeof(const void**)); xpath_node* write = _begin; for (xpath_node* it = _begin; it != _end; ++it) { const void* attr = it->attribute().internal_object(); const void* node = it->node().internal_object(); const void* key = attr ? attr : node; if (key && hash_insert(hash_data, hash_size, key)) { *write++ = *it; } } _end = write; } else { _end = unique(_begin, _end); } } xpath_node_set::type_t type() const { return _type; } void set_type(xpath_node_set::type_t value) { _type = value; } }; PUGI_IMPL_FN_NO_INLINE void xpath_node_set_raw::push_back_grow(const xpath_node& node, xpath_allocator* alloc) { size_t capacity = static_cast(_eos - _begin); // get new capacity (1.5x rule) size_t new_capacity = capacity + capacity / 2 + 1; // reallocate the old array or allocate a new one xpath_node* data = static_cast(alloc->reallocate(_begin, capacity * sizeof(xpath_node), new_capacity * sizeof(xpath_node))); if (!data) return; // finalize _begin = data; _end = data + capacity; _eos = data + new_capacity; // push *_end++ = node; } PUGI_IMPL_NS_END PUGI_IMPL_NS_BEGIN struct xpath_context { xpath_node n; size_t position, size; xpath_context(const xpath_node& n_, size_t position_, size_t size_): n(n_), position(position_), size(size_) { } }; enum lexeme_t { lex_none = 0, lex_equal, lex_not_equal, lex_less, lex_greater, lex_less_or_equal, lex_greater_or_equal, lex_plus, lex_minus, lex_multiply, lex_union, lex_var_ref, lex_open_brace, lex_close_brace, lex_quoted_string, lex_number, lex_slash, lex_double_slash, lex_open_square_brace, lex_close_square_brace, lex_string, lex_comma, lex_axis_attribute, lex_dot, lex_double_dot, lex_double_colon, lex_eof }; struct xpath_lexer_string { const char_t* begin; const char_t* end; xpath_lexer_string(): begin(0), end(0) { } bool operator==(const char_t* other) const { size_t length = static_cast(end - begin); return strequalrange(other, begin, length); } }; class xpath_lexer { const char_t* _cur; const char_t* _cur_lexeme_pos; xpath_lexer_string _cur_lexeme_contents; lexeme_t _cur_lexeme; public: explicit xpath_lexer(const char_t* query): _cur(query) { next(); } const char_t* state() const { return _cur; } void next() { const char_t* cur = _cur; while (PUGI_IMPL_IS_CHARTYPE(*cur, ct_space)) ++cur; // save lexeme position for error reporting _cur_lexeme_pos = cur; switch (*cur) { case 0: _cur_lexeme = lex_eof; break; case '>': if (*(cur+1) == '=') { cur += 2; _cur_lexeme = lex_greater_or_equal; } else { cur += 1; _cur_lexeme = lex_greater; } break; case '<': if (*(cur+1) == '=') { cur += 2; _cur_lexeme = lex_less_or_equal; } else { cur += 1; _cur_lexeme = lex_less; } break; case '!': if (*(cur+1) == '=') { cur += 2; _cur_lexeme = lex_not_equal; } else { _cur_lexeme = lex_none; } break; case '=': cur += 1; _cur_lexeme = lex_equal; break; case '+': cur += 1; _cur_lexeme = lex_plus; break; case '-': cur += 1; _cur_lexeme = lex_minus; break; case '*': cur += 1; _cur_lexeme = lex_multiply; break; case '|': cur += 1; _cur_lexeme = lex_union; break; case '$': cur += 1; if (PUGI_IMPL_IS_CHARTYPEX(*cur, ctx_start_symbol)) { _cur_lexeme_contents.begin = cur; while (PUGI_IMPL_IS_CHARTYPEX(*cur, ctx_symbol)) cur++; if (cur[0] == ':' && PUGI_IMPL_IS_CHARTYPEX(cur[1], ctx_symbol)) // qname { cur++; // : while (PUGI_IMPL_IS_CHARTYPEX(*cur, ctx_symbol)) cur++; } _cur_lexeme_contents.end = cur; _cur_lexeme = lex_var_ref; } else { _cur_lexeme = lex_none; } break; case '(': cur += 1; _cur_lexeme = lex_open_brace; break; case ')': cur += 1; _cur_lexeme = lex_close_brace; break; case '[': cur += 1; _cur_lexeme = lex_open_square_brace; break; case ']': cur += 1; _cur_lexeme = lex_close_square_brace; break; case ',': cur += 1; _cur_lexeme = lex_comma; break; case '/': if (*(cur+1) == '/') { cur += 2; _cur_lexeme = lex_double_slash; } else { cur += 1; _cur_lexeme = lex_slash; } break; case '.': if (*(cur+1) == '.') { cur += 2; _cur_lexeme = lex_double_dot; } else if (PUGI_IMPL_IS_CHARTYPEX(*(cur+1), ctx_digit)) { _cur_lexeme_contents.begin = cur; // . ++cur; while (PUGI_IMPL_IS_CHARTYPEX(*cur, ctx_digit)) cur++; _cur_lexeme_contents.end = cur; _cur_lexeme = lex_number; } else { cur += 1; _cur_lexeme = lex_dot; } break; case '@': cur += 1; _cur_lexeme = lex_axis_attribute; break; case '"': case '\'': { char_t terminator = *cur; ++cur; _cur_lexeme_contents.begin = cur; while (*cur && *cur != terminator) cur++; _cur_lexeme_contents.end = cur; if (!*cur) _cur_lexeme = lex_none; else { cur += 1; _cur_lexeme = lex_quoted_string; } break; } case ':': if (*(cur+1) == ':') { cur += 2; _cur_lexeme = lex_double_colon; } else { _cur_lexeme = lex_none; } break; default: if (PUGI_IMPL_IS_CHARTYPEX(*cur, ctx_digit)) { _cur_lexeme_contents.begin = cur; while (PUGI_IMPL_IS_CHARTYPEX(*cur, ctx_digit)) cur++; if (*cur == '.') { cur++; while (PUGI_IMPL_IS_CHARTYPEX(*cur, ctx_digit)) cur++; } _cur_lexeme_contents.end = cur; _cur_lexeme = lex_number; } else if (PUGI_IMPL_IS_CHARTYPEX(*cur, ctx_start_symbol)) { _cur_lexeme_contents.begin = cur; while (PUGI_IMPL_IS_CHARTYPEX(*cur, ctx_symbol)) cur++; if (cur[0] == ':') { if (cur[1] == '*') // namespace test ncname:* { cur += 2; // :* } else if (PUGI_IMPL_IS_CHARTYPEX(cur[1], ctx_symbol)) // namespace test qname { cur++; // : while (PUGI_IMPL_IS_CHARTYPEX(*cur, ctx_symbol)) cur++; } } _cur_lexeme_contents.end = cur; _cur_lexeme = lex_string; } else { _cur_lexeme = lex_none; } } _cur = cur; } lexeme_t current() const { return _cur_lexeme; } const char_t* current_pos() const { return _cur_lexeme_pos; } const xpath_lexer_string& contents() const { assert(_cur_lexeme == lex_var_ref || _cur_lexeme == lex_number || _cur_lexeme == lex_string || _cur_lexeme == lex_quoted_string); return _cur_lexeme_contents; } }; enum ast_type_t { ast_unknown, ast_op_or, // left or right ast_op_and, // left and right ast_op_equal, // left = right ast_op_not_equal, // left != right ast_op_less, // left < right ast_op_greater, // left > right ast_op_less_or_equal, // left <= right ast_op_greater_or_equal, // left >= right ast_op_add, // left + right ast_op_subtract, // left - right ast_op_multiply, // left * right ast_op_divide, // left / right ast_op_mod, // left % right ast_op_negate, // left - right ast_op_union, // left | right ast_predicate, // apply predicate to set; next points to next predicate ast_filter, // select * from left where right ast_string_constant, // string constant ast_number_constant, // number constant ast_variable, // variable ast_func_last, // last() ast_func_position, // position() ast_func_count, // count(left) ast_func_id, // id(left) ast_func_local_name_0, // local-name() ast_func_local_name_1, // local-name(left) ast_func_namespace_uri_0, // namespace-uri() ast_func_namespace_uri_1, // namespace-uri(left) ast_func_name_0, // name() ast_func_name_1, // name(left) ast_func_string_0, // string() ast_func_string_1, // string(left) ast_func_concat, // concat(left, right, siblings) ast_func_starts_with, // starts_with(left, right) ast_func_contains, // contains(left, right) ast_func_substring_before, // substring-before(left, right) ast_func_substring_after, // substring-after(left, right) ast_func_substring_2, // substring(left, right) ast_func_substring_3, // substring(left, right, third) ast_func_string_length_0, // string-length() ast_func_string_length_1, // string-length(left) ast_func_normalize_space_0, // normalize-space() ast_func_normalize_space_1, // normalize-space(left) ast_func_translate, // translate(left, right, third) ast_func_boolean, // boolean(left) ast_func_not, // not(left) ast_func_true, // true() ast_func_false, // false() ast_func_lang, // lang(left) ast_func_number_0, // number() ast_func_number_1, // number(left) ast_func_sum, // sum(left) ast_func_floor, // floor(left) ast_func_ceiling, // ceiling(left) ast_func_round, // round(left) ast_step, // process set left with step ast_step_root, // select root node ast_opt_translate_table, // translate(left, right, third) where right/third are constants ast_opt_compare_attribute // @name = 'string' }; enum axis_t { axis_ancestor, axis_ancestor_or_self, axis_attribute, axis_child, axis_descendant, axis_descendant_or_self, axis_following, axis_following_sibling, axis_namespace, axis_parent, axis_preceding, axis_preceding_sibling, axis_self }; enum nodetest_t { nodetest_none, nodetest_name, nodetest_type_node, nodetest_type_comment, nodetest_type_pi, nodetest_type_text, nodetest_pi, nodetest_all, nodetest_all_in_namespace }; enum predicate_t { predicate_default, predicate_posinv, predicate_constant, predicate_constant_one }; enum nodeset_eval_t { nodeset_eval_all, nodeset_eval_any, nodeset_eval_first }; template struct axis_to_type { static const axis_t axis; }; template const axis_t axis_to_type::axis = N; class xpath_ast_node { private: // node type char _type; char _rettype; // for ast_step char _axis; // for ast_step/ast_predicate/ast_filter char _test; // tree node structure xpath_ast_node* _left; xpath_ast_node* _right; xpath_ast_node* _next; union { // value for ast_string_constant const char_t* string; // value for ast_number_constant double number; // variable for ast_variable xpath_variable* variable; // node test for ast_step (node name/namespace/node type/pi target) const char_t* nodetest; // table for ast_opt_translate_table const unsigned char* table; } _data; xpath_ast_node(const xpath_ast_node&); xpath_ast_node& operator=(const xpath_ast_node&); template static bool compare_eq(xpath_ast_node* lhs, xpath_ast_node* rhs, const xpath_context& c, const xpath_stack& stack, const Comp& comp) { xpath_value_type lt = lhs->rettype(), rt = rhs->rettype(); if (lt != xpath_type_node_set && rt != xpath_type_node_set) { if (lt == xpath_type_boolean || rt == xpath_type_boolean) return comp(lhs->eval_boolean(c, stack), rhs->eval_boolean(c, stack)); else if (lt == xpath_type_number || rt == xpath_type_number) return comp(lhs->eval_number(c, stack), rhs->eval_number(c, stack)); else if (lt == xpath_type_string || rt == xpath_type_string) { xpath_allocator_capture cr(stack.result); xpath_string ls = lhs->eval_string(c, stack); xpath_string rs = rhs->eval_string(c, stack); return comp(ls, rs); } } else if (lt == xpath_type_node_set && rt == xpath_type_node_set) { xpath_allocator_capture cr(stack.result); xpath_node_set_raw ls = lhs->eval_node_set(c, stack, nodeset_eval_all); xpath_node_set_raw rs = rhs->eval_node_set(c, stack, nodeset_eval_all); for (const xpath_node* li = ls.begin(); li != ls.end(); ++li) for (const xpath_node* ri = rs.begin(); ri != rs.end(); ++ri) { xpath_allocator_capture cri(stack.result); if (comp(string_value(*li, stack.result), string_value(*ri, stack.result))) return true; } return false; } else { if (lt == xpath_type_node_set) { swap(lhs, rhs); swap(lt, rt); } if (lt == xpath_type_boolean) return comp(lhs->eval_boolean(c, stack), rhs->eval_boolean(c, stack)); else if (lt == xpath_type_number) { xpath_allocator_capture cr(stack.result); double l = lhs->eval_number(c, stack); xpath_node_set_raw rs = rhs->eval_node_set(c, stack, nodeset_eval_all); for (const xpath_node* ri = rs.begin(); ri != rs.end(); ++ri) { xpath_allocator_capture cri(stack.result); if (comp(l, convert_string_to_number(string_value(*ri, stack.result).c_str()))) return true; } return false; } else if (lt == xpath_type_string) { xpath_allocator_capture cr(stack.result); xpath_string l = lhs->eval_string(c, stack); xpath_node_set_raw rs = rhs->eval_node_set(c, stack, nodeset_eval_all); for (const xpath_node* ri = rs.begin(); ri != rs.end(); ++ri) { xpath_allocator_capture cri(stack.result); if (comp(l, string_value(*ri, stack.result))) return true; } return false; } } assert(false && "Wrong types"); // unreachable return false; } static bool eval_once(xpath_node_set::type_t type, nodeset_eval_t eval) { return type == xpath_node_set::type_sorted ? eval != nodeset_eval_all : eval == nodeset_eval_any; } template static bool compare_rel(xpath_ast_node* lhs, xpath_ast_node* rhs, const xpath_context& c, const xpath_stack& stack, const Comp& comp) { xpath_value_type lt = lhs->rettype(), rt = rhs->rettype(); if (lt != xpath_type_node_set && rt != xpath_type_node_set) return comp(lhs->eval_number(c, stack), rhs->eval_number(c, stack)); else if (lt == xpath_type_node_set && rt == xpath_type_node_set) { xpath_allocator_capture cr(stack.result); xpath_node_set_raw ls = lhs->eval_node_set(c, stack, nodeset_eval_all); xpath_node_set_raw rs = rhs->eval_node_set(c, stack, nodeset_eval_all); for (const xpath_node* li = ls.begin(); li != ls.end(); ++li) { xpath_allocator_capture cri(stack.result); double l = convert_string_to_number(string_value(*li, stack.result).c_str()); for (const xpath_node* ri = rs.begin(); ri != rs.end(); ++ri) { xpath_allocator_capture crii(stack.result); if (comp(l, convert_string_to_number(string_value(*ri, stack.result).c_str()))) return true; } } return false; } else if (lt != xpath_type_node_set && rt == xpath_type_node_set) { xpath_allocator_capture cr(stack.result); double l = lhs->eval_number(c, stack); xpath_node_set_raw rs = rhs->eval_node_set(c, stack, nodeset_eval_all); for (const xpath_node* ri = rs.begin(); ri != rs.end(); ++ri) { xpath_allocator_capture cri(stack.result); if (comp(l, convert_string_to_number(string_value(*ri, stack.result).c_str()))) return true; } return false; } else if (lt == xpath_type_node_set && rt != xpath_type_node_set) { xpath_allocator_capture cr(stack.result); xpath_node_set_raw ls = lhs->eval_node_set(c, stack, nodeset_eval_all); double r = rhs->eval_number(c, stack); for (const xpath_node* li = ls.begin(); li != ls.end(); ++li) { xpath_allocator_capture cri(stack.result); if (comp(convert_string_to_number(string_value(*li, stack.result).c_str()), r)) return true; } return false; } else { assert(false && "Wrong types"); // unreachable return false; } } static void apply_predicate_boolean(xpath_node_set_raw& ns, size_t first, xpath_ast_node* expr, const xpath_stack& stack, bool once) { assert(ns.size() >= first); assert(expr->rettype() != xpath_type_number); size_t i = 1; size_t size = ns.size() - first; xpath_node* last = ns.begin() + first; // remove_if... or well, sort of for (xpath_node* it = last; it != ns.end(); ++it, ++i) { xpath_context c(*it, i, size); if (expr->eval_boolean(c, stack)) { *last++ = *it; if (once) break; } } ns.truncate(last); } static void apply_predicate_number(xpath_node_set_raw& ns, size_t first, xpath_ast_node* expr, const xpath_stack& stack, bool once) { assert(ns.size() >= first); assert(expr->rettype() == xpath_type_number); size_t i = 1; size_t size = ns.size() - first; xpath_node* last = ns.begin() + first; // remove_if... or well, sort of for (xpath_node* it = last; it != ns.end(); ++it, ++i) { xpath_context c(*it, i, size); if (expr->eval_number(c, stack) == static_cast(i)) { *last++ = *it; if (once) break; } } ns.truncate(last); } static void apply_predicate_number_const(xpath_node_set_raw& ns, size_t first, xpath_ast_node* expr, const xpath_stack& stack) { assert(ns.size() >= first); assert(expr->rettype() == xpath_type_number); size_t size = ns.size() - first; xpath_node* last = ns.begin() + first; xpath_node cn; xpath_context c(cn, 1, size); double er = expr->eval_number(c, stack); if (er >= 1.0 && er <= static_cast(size)) { size_t eri = static_cast(er); if (er == static_cast(eri)) { xpath_node r = last[eri - 1]; *last++ = r; } } ns.truncate(last); } void apply_predicate(xpath_node_set_raw& ns, size_t first, const xpath_stack& stack, bool once) { if (ns.size() == first) return; assert(_type == ast_filter || _type == ast_predicate); if (_test == predicate_constant || _test == predicate_constant_one) apply_predicate_number_const(ns, first, _right, stack); else if (_right->rettype() == xpath_type_number) apply_predicate_number(ns, first, _right, stack, once); else apply_predicate_boolean(ns, first, _right, stack, once); } void apply_predicates(xpath_node_set_raw& ns, size_t first, const xpath_stack& stack, nodeset_eval_t eval) { if (ns.size() == first) return; bool last_once = eval_once(ns.type(), eval); for (xpath_ast_node* pred = _right; pred; pred = pred->_next) pred->apply_predicate(ns, first, stack, !pred->_next && last_once); } bool step_push(xpath_node_set_raw& ns, xml_attribute_struct* a, xml_node_struct* parent, xpath_allocator* alloc) { assert(a); const char_t* name = a->name ? a->name + 0 : PUGIXML_TEXT(""); switch (_test) { case nodetest_name: if (strequal(name, _data.nodetest) && is_xpath_attribute(name)) { ns.push_back(xpath_node(xml_attribute(a), xml_node(parent)), alloc); return true; } break; case nodetest_type_node: case nodetest_all: if (is_xpath_attribute(name)) { ns.push_back(xpath_node(xml_attribute(a), xml_node(parent)), alloc); return true; } break; case nodetest_all_in_namespace: if (starts_with(name, _data.nodetest) && is_xpath_attribute(name)) { ns.push_back(xpath_node(xml_attribute(a), xml_node(parent)), alloc); return true; } break; default: ; } return false; } bool step_push(xpath_node_set_raw& ns, xml_node_struct* n, xpath_allocator* alloc) { assert(n); xml_node_type type = PUGI_IMPL_NODETYPE(n); switch (_test) { case nodetest_name: if (type == node_element && n->name && strequal(n->name, _data.nodetest)) { ns.push_back(xml_node(n), alloc); return true; } break; case nodetest_type_node: ns.push_back(xml_node(n), alloc); return true; case nodetest_type_comment: if (type == node_comment) { ns.push_back(xml_node(n), alloc); return true; } break; case nodetest_type_text: if (type == node_pcdata || type == node_cdata) { ns.push_back(xml_node(n), alloc); return true; } break; case nodetest_type_pi: if (type == node_pi) { ns.push_back(xml_node(n), alloc); return true; } break; case nodetest_pi: if (type == node_pi && n->name && strequal(n->name, _data.nodetest)) { ns.push_back(xml_node(n), alloc); return true; } break; case nodetest_all: if (type == node_element) { ns.push_back(xml_node(n), alloc); return true; } break; case nodetest_all_in_namespace: if (type == node_element && n->name && starts_with(n->name, _data.nodetest)) { ns.push_back(xml_node(n), alloc); return true; } break; default: assert(false && "Unknown axis"); // unreachable } return false; } template void step_fill(xpath_node_set_raw& ns, xml_node_struct* n, xpath_allocator* alloc, bool once, T) { const axis_t axis = T::axis; switch (axis) { case axis_attribute: { for (xml_attribute_struct* a = n->first_attribute; a; a = a->next_attribute) if (step_push(ns, a, n, alloc) & once) return; break; } case axis_child: { for (xml_node_struct* c = n->first_child; c; c = c->next_sibling) if (step_push(ns, c, alloc) & once) return; break; } case axis_descendant: case axis_descendant_or_self: { if (axis == axis_descendant_or_self) if (step_push(ns, n, alloc) & once) return; xml_node_struct* cur = n->first_child; while (cur) { if (step_push(ns, cur, alloc) & once) return; if (cur->first_child) cur = cur->first_child; else { while (!cur->next_sibling) { cur = cur->parent; if (cur == n) return; } cur = cur->next_sibling; } } break; } case axis_following_sibling: { for (xml_node_struct* c = n->next_sibling; c; c = c->next_sibling) if (step_push(ns, c, alloc) & once) return; break; } case axis_preceding_sibling: { for (xml_node_struct* c = n->prev_sibling_c; c->next_sibling; c = c->prev_sibling_c) if (step_push(ns, c, alloc) & once) return; break; } case axis_following: { xml_node_struct* cur = n; // exit from this node so that we don't include descendants while (!cur->next_sibling) { cur = cur->parent; if (!cur) return; } cur = cur->next_sibling; while (cur) { if (step_push(ns, cur, alloc) & once) return; if (cur->first_child) cur = cur->first_child; else { while (!cur->next_sibling) { cur = cur->parent; if (!cur) return; } cur = cur->next_sibling; } } break; } case axis_preceding: { xml_node_struct* cur = n; // exit from this node so that we don't include descendants while (!cur->prev_sibling_c->next_sibling) { cur = cur->parent; if (!cur) return; } cur = cur->prev_sibling_c; while (cur) { if (cur->first_child) cur = cur->first_child->prev_sibling_c; else { // leaf node, can't be ancestor if (step_push(ns, cur, alloc) & once) return; while (!cur->prev_sibling_c->next_sibling) { cur = cur->parent; if (!cur) return; if (!node_is_ancestor(cur, n)) if (step_push(ns, cur, alloc) & once) return; } cur = cur->prev_sibling_c; } } break; } case axis_ancestor: case axis_ancestor_or_self: { if (axis == axis_ancestor_or_self) if (step_push(ns, n, alloc) & once) return; xml_node_struct* cur = n->parent; while (cur) { if (step_push(ns, cur, alloc) & once) return; cur = cur->parent; } break; } case axis_self: { step_push(ns, n, alloc); break; } case axis_parent: { if (n->parent) step_push(ns, n->parent, alloc); break; } default: assert(false && "Unimplemented axis"); // unreachable } } template void step_fill(xpath_node_set_raw& ns, xml_attribute_struct* a, xml_node_struct* p, xpath_allocator* alloc, bool once, T v) { const axis_t axis = T::axis; switch (axis) { case axis_ancestor: case axis_ancestor_or_self: { if (axis == axis_ancestor_or_self && _test == nodetest_type_node) // reject attributes based on principal node type test if (step_push(ns, a, p, alloc) & once) return; xml_node_struct* cur = p; while (cur) { if (step_push(ns, cur, alloc) & once) return; cur = cur->parent; } break; } case axis_descendant_or_self: case axis_self: { if (_test == nodetest_type_node) // reject attributes based on principal node type test step_push(ns, a, p, alloc); break; } case axis_following: { xml_node_struct* cur = p; while (cur) { if (cur->first_child) cur = cur->first_child; else { while (!cur->next_sibling) { cur = cur->parent; if (!cur) return; } cur = cur->next_sibling; } if (step_push(ns, cur, alloc) & once) return; } break; } case axis_parent: { step_push(ns, p, alloc); break; } case axis_preceding: { // preceding:: axis does not include attribute nodes and attribute ancestors (they are the same as parent's ancestors), so we can reuse node preceding step_fill(ns, p, alloc, once, v); break; } default: assert(false && "Unimplemented axis"); // unreachable } } template void step_fill(xpath_node_set_raw& ns, const xpath_node& xn, xpath_allocator* alloc, bool once, T v) { const axis_t axis = T::axis; const bool axis_has_attributes = (axis == axis_ancestor || axis == axis_ancestor_or_self || axis == axis_descendant_or_self || axis == axis_following || axis == axis_parent || axis == axis_preceding || axis == axis_self); if (xn.node()) step_fill(ns, xn.node().internal_object(), alloc, once, v); else if (axis_has_attributes && xn.attribute() && xn.parent()) step_fill(ns, xn.attribute().internal_object(), xn.parent().internal_object(), alloc, once, v); } template xpath_node_set_raw step_do(const xpath_context& c, const xpath_stack& stack, nodeset_eval_t eval, T v) { const axis_t axis = T::axis; const bool axis_reverse = (axis == axis_ancestor || axis == axis_ancestor_or_self || axis == axis_preceding || axis == axis_preceding_sibling); const xpath_node_set::type_t axis_type = axis_reverse ? xpath_node_set::type_sorted_reverse : xpath_node_set::type_sorted; bool once = (axis == axis_attribute && _test == nodetest_name) || (!_right && eval_once(axis_type, eval)) || // coverity[mixed_enums] (_right && !_right->_next && _right->_test == predicate_constant_one); xpath_node_set_raw ns; ns.set_type(axis_type); if (_left) { xpath_node_set_raw s = _left->eval_node_set(c, stack, nodeset_eval_all); // self axis preserves the original order if (axis == axis_self) ns.set_type(s.type()); for (const xpath_node* it = s.begin(); it != s.end(); ++it) { size_t size = ns.size(); // in general, all axes generate elements in a particular order, but there is no order guarantee if axis is applied to two nodes if (axis != axis_self && size != 0) ns.set_type(xpath_node_set::type_unsorted); step_fill(ns, *it, stack.result, once, v); if (_right) apply_predicates(ns, size, stack, eval); } } else { step_fill(ns, c.n, stack.result, once, v); if (_right) apply_predicates(ns, 0, stack, eval); } // child, attribute and self axes always generate unique set of nodes // for other axis, if the set stayed sorted, it stayed unique because the traversal algorithms do not visit the same node twice if (axis != axis_child && axis != axis_attribute && axis != axis_self && ns.type() == xpath_node_set::type_unsorted) ns.remove_duplicates(stack.temp); return ns; } public: xpath_ast_node(ast_type_t type, xpath_value_type rettype_, const char_t* value): _type(static_cast(type)), _rettype(static_cast(rettype_)), _axis(0), _test(0), _left(0), _right(0), _next(0) { assert(type == ast_string_constant); _data.string = value; } xpath_ast_node(ast_type_t type, xpath_value_type rettype_, double value): _type(static_cast(type)), _rettype(static_cast(rettype_)), _axis(0), _test(0), _left(0), _right(0), _next(0) { assert(type == ast_number_constant); _data.number = value; } xpath_ast_node(ast_type_t type, xpath_value_type rettype_, xpath_variable* value): _type(static_cast(type)), _rettype(static_cast(rettype_)), _axis(0), _test(0), _left(0), _right(0), _next(0) { assert(type == ast_variable); _data.variable = value; } xpath_ast_node(ast_type_t type, xpath_value_type rettype_, xpath_ast_node* left = 0, xpath_ast_node* right = 0): _type(static_cast(type)), _rettype(static_cast(rettype_)), _axis(0), _test(0), _left(left), _right(right), _next(0) { } xpath_ast_node(ast_type_t type, xpath_ast_node* left, axis_t axis, nodetest_t test, const char_t* contents): _type(static_cast(type)), _rettype(xpath_type_node_set), _axis(static_cast(axis)), _test(static_cast(test)), _left(left), _right(0), _next(0) { assert(type == ast_step); _data.nodetest = contents; } xpath_ast_node(ast_type_t type, xpath_ast_node* left, xpath_ast_node* right, predicate_t test): _type(static_cast(type)), _rettype(xpath_type_node_set), _axis(0), _test(static_cast(test)), _left(left), _right(right), _next(0) { assert(type == ast_filter || type == ast_predicate); } void set_next(xpath_ast_node* value) { _next = value; } void set_right(xpath_ast_node* value) { _right = value; } bool eval_boolean(const xpath_context& c, const xpath_stack& stack) { switch (_type) { case ast_op_or: return _left->eval_boolean(c, stack) || _right->eval_boolean(c, stack); case ast_op_and: return _left->eval_boolean(c, stack) && _right->eval_boolean(c, stack); case ast_op_equal: return compare_eq(_left, _right, c, stack, equal_to()); case ast_op_not_equal: return compare_eq(_left, _right, c, stack, not_equal_to()); case ast_op_less: return compare_rel(_left, _right, c, stack, less()); case ast_op_greater: return compare_rel(_right, _left, c, stack, less()); case ast_op_less_or_equal: return compare_rel(_left, _right, c, stack, less_equal()); case ast_op_greater_or_equal: return compare_rel(_right, _left, c, stack, less_equal()); case ast_func_starts_with: { xpath_allocator_capture cr(stack.result); xpath_string lr = _left->eval_string(c, stack); xpath_string rr = _right->eval_string(c, stack); return starts_with(lr.c_str(), rr.c_str()); } case ast_func_contains: { xpath_allocator_capture cr(stack.result); xpath_string lr = _left->eval_string(c, stack); xpath_string rr = _right->eval_string(c, stack); return find_substring(lr.c_str(), rr.c_str()) != 0; } case ast_func_boolean: return _left->eval_boolean(c, stack); case ast_func_not: return !_left->eval_boolean(c, stack); case ast_func_true: return true; case ast_func_false: return false; case ast_func_lang: { if (c.n.attribute()) return false; xpath_allocator_capture cr(stack.result); xpath_string lang = _left->eval_string(c, stack); for (xml_node n = c.n.node(); n; n = n.parent()) { xml_attribute a = n.attribute(PUGIXML_TEXT("xml:lang")); if (a) { const char_t* value = a.value(); // strnicmp / strncasecmp is not portable for (const char_t* lit = lang.c_str(); *lit; ++lit) { if (tolower_ascii(*lit) != tolower_ascii(*value)) return false; ++value; } return *value == 0 || *value == '-'; } } return false; } case ast_opt_compare_attribute: { const char_t* value = (_right->_type == ast_string_constant) ? _right->_data.string : _right->_data.variable->get_string(); xml_attribute attr = c.n.node().attribute(_left->_data.nodetest); return attr && strequal(attr.value(), value) && is_xpath_attribute(attr.name()); } case ast_variable: { assert(_rettype == _data.variable->type()); if (_rettype == xpath_type_boolean) return _data.variable->get_boolean(); // variable needs to be converted to the correct type, this is handled by the fallthrough block below break; } default: ; } // none of the ast types that return the value directly matched, we need to perform type conversion switch (_rettype) { case xpath_type_number: return convert_number_to_boolean(eval_number(c, stack)); case xpath_type_string: { xpath_allocator_capture cr(stack.result); return !eval_string(c, stack).empty(); } case xpath_type_node_set: { xpath_allocator_capture cr(stack.result); return !eval_node_set(c, stack, nodeset_eval_any).empty(); } default: assert(false && "Wrong expression for return type boolean"); // unreachable return false; } } double eval_number(const xpath_context& c, const xpath_stack& stack) { switch (_type) { case ast_op_add: return _left->eval_number(c, stack) + _right->eval_number(c, stack); case ast_op_subtract: return _left->eval_number(c, stack) - _right->eval_number(c, stack); case ast_op_multiply: return _left->eval_number(c, stack) * _right->eval_number(c, stack); case ast_op_divide: return _left->eval_number(c, stack) / _right->eval_number(c, stack); case ast_op_mod: return fmod(_left->eval_number(c, stack), _right->eval_number(c, stack)); case ast_op_negate: return -_left->eval_number(c, stack); case ast_number_constant: return _data.number; case ast_func_last: return static_cast(c.size); case ast_func_position: return static_cast(c.position); case ast_func_count: { xpath_allocator_capture cr(stack.result); return static_cast(_left->eval_node_set(c, stack, nodeset_eval_all).size()); } case ast_func_string_length_0: { xpath_allocator_capture cr(stack.result); return static_cast(string_value(c.n, stack.result).length()); } case ast_func_string_length_1: { xpath_allocator_capture cr(stack.result); return static_cast(_left->eval_string(c, stack).length()); } case ast_func_number_0: { xpath_allocator_capture cr(stack.result); return convert_string_to_number(string_value(c.n, stack.result).c_str()); } case ast_func_number_1: return _left->eval_number(c, stack); case ast_func_sum: { xpath_allocator_capture cr(stack.result); double r = 0; xpath_node_set_raw ns = _left->eval_node_set(c, stack, nodeset_eval_all); for (const xpath_node* it = ns.begin(); it != ns.end(); ++it) { xpath_allocator_capture cri(stack.result); r += convert_string_to_number(string_value(*it, stack.result).c_str()); } return r; } case ast_func_floor: { double r = _left->eval_number(c, stack); return r == r ? floor(r) : r; } case ast_func_ceiling: { double r = _left->eval_number(c, stack); return r == r ? ceil(r) : r; } case ast_func_round: return round_nearest_nzero(_left->eval_number(c, stack)); case ast_variable: { assert(_rettype == _data.variable->type()); if (_rettype == xpath_type_number) return _data.variable->get_number(); // variable needs to be converted to the correct type, this is handled by the fallthrough block below break; } default: ; } // none of the ast types that return the value directly matched, we need to perform type conversion switch (_rettype) { case xpath_type_boolean: return eval_boolean(c, stack) ? 1 : 0; case xpath_type_string: { xpath_allocator_capture cr(stack.result); return convert_string_to_number(eval_string(c, stack).c_str()); } case xpath_type_node_set: { xpath_allocator_capture cr(stack.result); return convert_string_to_number(eval_string(c, stack).c_str()); } default: assert(false && "Wrong expression for return type number"); // unreachable return 0; } } xpath_string eval_string_concat(const xpath_context& c, const xpath_stack& stack) { assert(_type == ast_func_concat); xpath_allocator_capture ct(stack.temp); // count the string number size_t count = 1; for (xpath_ast_node* nc = _right; nc; nc = nc->_next) count++; // allocate a buffer for temporary string objects xpath_string* buffer = static_cast(stack.temp->allocate(count * sizeof(xpath_string))); if (!buffer) return xpath_string(); // evaluate all strings to temporary stack xpath_stack swapped_stack = {stack.temp, stack.result}; buffer[0] = _left->eval_string(c, swapped_stack); size_t pos = 1; for (xpath_ast_node* n = _right; n; n = n->_next, ++pos) buffer[pos] = n->eval_string(c, swapped_stack); assert(pos == count); // get total length size_t length = 0; for (size_t i = 0; i < count; ++i) length += buffer[i].length(); // create final string char_t* result = static_cast(stack.result->allocate((length + 1) * sizeof(char_t))); if (!result) return xpath_string(); char_t* ri = result; for (size_t j = 0; j < count; ++j) for (const char_t* bi = buffer[j].c_str(); *bi; ++bi) *ri++ = *bi; *ri = 0; return xpath_string::from_heap_preallocated(result, ri); } xpath_string eval_string(const xpath_context& c, const xpath_stack& stack) { switch (_type) { case ast_string_constant: return xpath_string::from_const(_data.string); case ast_func_local_name_0: { xpath_node na = c.n; return xpath_string::from_const(local_name(na)); } case ast_func_local_name_1: { xpath_allocator_capture cr(stack.result); xpath_node_set_raw ns = _left->eval_node_set(c, stack, nodeset_eval_first); xpath_node na = ns.first(); return xpath_string::from_const(local_name(na)); } case ast_func_name_0: { xpath_node na = c.n; return xpath_string::from_const(qualified_name(na)); } case ast_func_name_1: { xpath_allocator_capture cr(stack.result); xpath_node_set_raw ns = _left->eval_node_set(c, stack, nodeset_eval_first); xpath_node na = ns.first(); return xpath_string::from_const(qualified_name(na)); } case ast_func_namespace_uri_0: { xpath_node na = c.n; return xpath_string::from_const(namespace_uri(na)); } case ast_func_namespace_uri_1: { xpath_allocator_capture cr(stack.result); xpath_node_set_raw ns = _left->eval_node_set(c, stack, nodeset_eval_first); xpath_node na = ns.first(); return xpath_string::from_const(namespace_uri(na)); } case ast_func_string_0: return string_value(c.n, stack.result); case ast_func_string_1: return _left->eval_string(c, stack); case ast_func_concat: return eval_string_concat(c, stack); case ast_func_substring_before: { xpath_allocator_capture cr(stack.temp); xpath_stack swapped_stack = {stack.temp, stack.result}; xpath_string s = _left->eval_string(c, swapped_stack); xpath_string p = _right->eval_string(c, swapped_stack); const char_t* pos = find_substring(s.c_str(), p.c_str()); return pos ? xpath_string::from_heap(s.c_str(), pos, stack.result) : xpath_string(); } case ast_func_substring_after: { xpath_allocator_capture cr(stack.temp); xpath_stack swapped_stack = {stack.temp, stack.result}; xpath_string s = _left->eval_string(c, swapped_stack); xpath_string p = _right->eval_string(c, swapped_stack); const char_t* pos = find_substring(s.c_str(), p.c_str()); if (!pos) return xpath_string(); const char_t* rbegin = pos + p.length(); const char_t* rend = s.c_str() + s.length(); return s.uses_heap() ? xpath_string::from_heap(rbegin, rend, stack.result) : xpath_string::from_const(rbegin); } case ast_func_substring_2: { xpath_allocator_capture cr(stack.temp); xpath_stack swapped_stack = {stack.temp, stack.result}; xpath_string s = _left->eval_string(c, swapped_stack); size_t s_length = s.length(); double first = round_nearest(_right->eval_number(c, stack)); if (is_nan(first)) return xpath_string(); // NaN else if (first >= static_cast(s_length + 1)) return xpath_string(); size_t pos = first < 1 ? 1 : static_cast(first); assert(1 <= pos && pos <= s_length + 1); const char_t* rbegin = s.c_str() + (pos - 1); const char_t* rend = s.c_str() + s.length(); return s.uses_heap() ? xpath_string::from_heap(rbegin, rend, stack.result) : xpath_string::from_const(rbegin); } case ast_func_substring_3: { xpath_allocator_capture cr(stack.temp); xpath_stack swapped_stack = {stack.temp, stack.result}; xpath_string s = _left->eval_string(c, swapped_stack); size_t s_length = s.length(); double first = round_nearest(_right->eval_number(c, stack)); double last = first + round_nearest(_right->_next->eval_number(c, stack)); if (is_nan(first) || is_nan(last)) return xpath_string(); else if (first >= static_cast(s_length + 1)) return xpath_string(); else if (first >= last) return xpath_string(); else if (last < 1) return xpath_string(); size_t pos = first < 1 ? 1 : static_cast(first); size_t end = last >= static_cast(s_length + 1) ? s_length + 1 : static_cast(last); assert(1 <= pos && pos <= end && end <= s_length + 1); const char_t* rbegin = s.c_str() + (pos - 1); const char_t* rend = s.c_str() + (end - 1); return (end == s_length + 1 && !s.uses_heap()) ? xpath_string::from_const(rbegin) : xpath_string::from_heap(rbegin, rend, stack.result); } case ast_func_normalize_space_0: { xpath_string s = string_value(c.n, stack.result); char_t* begin = s.data(stack.result); if (!begin) return xpath_string(); char_t* end = normalize_space(begin); return xpath_string::from_heap_preallocated(begin, end); } case ast_func_normalize_space_1: { xpath_string s = _left->eval_string(c, stack); char_t* begin = s.data(stack.result); if (!begin) return xpath_string(); char_t* end = normalize_space(begin); return xpath_string::from_heap_preallocated(begin, end); } case ast_func_translate: { xpath_allocator_capture cr(stack.temp); xpath_stack swapped_stack = {stack.temp, stack.result}; xpath_string s = _left->eval_string(c, stack); xpath_string from = _right->eval_string(c, swapped_stack); xpath_string to = _right->_next->eval_string(c, swapped_stack); char_t* begin = s.data(stack.result); if (!begin) return xpath_string(); char_t* end = translate(begin, from.c_str(), to.c_str(), to.length()); return xpath_string::from_heap_preallocated(begin, end); } case ast_opt_translate_table: { xpath_string s = _left->eval_string(c, stack); char_t* begin = s.data(stack.result); if (!begin) return xpath_string(); char_t* end = translate_table(begin, _data.table); return xpath_string::from_heap_preallocated(begin, end); } case ast_variable: { assert(_rettype == _data.variable->type()); if (_rettype == xpath_type_string) return xpath_string::from_const(_data.variable->get_string()); // variable needs to be converted to the correct type, this is handled by the fallthrough block below break; } default: ; } // none of the ast types that return the value directly matched, we need to perform type conversion switch (_rettype) { case xpath_type_boolean: return xpath_string::from_const(eval_boolean(c, stack) ? PUGIXML_TEXT("true") : PUGIXML_TEXT("false")); case xpath_type_number: return convert_number_to_string(eval_number(c, stack), stack.result); case xpath_type_node_set: { xpath_allocator_capture cr(stack.temp); xpath_stack swapped_stack = {stack.temp, stack.result}; xpath_node_set_raw ns = eval_node_set(c, swapped_stack, nodeset_eval_first); return ns.empty() ? xpath_string() : string_value(ns.first(), stack.result); } default: assert(false && "Wrong expression for return type string"); // unreachable return xpath_string(); } } xpath_node_set_raw eval_node_set(const xpath_context& c, const xpath_stack& stack, nodeset_eval_t eval) { switch (_type) { case ast_op_union: { xpath_allocator_capture cr(stack.temp); xpath_stack swapped_stack = {stack.temp, stack.result}; xpath_node_set_raw ls = _left->eval_node_set(c, stack, eval); xpath_node_set_raw rs = _right->eval_node_set(c, swapped_stack, eval); // we can optimize merging two sorted sets, but this is a very rare operation, so don't bother ls.set_type(xpath_node_set::type_unsorted); ls.append(rs.begin(), rs.end(), stack.result); ls.remove_duplicates(stack.temp); return ls; } case ast_filter: { xpath_node_set_raw set = _left->eval_node_set(c, stack, _test == predicate_constant_one ? nodeset_eval_first : nodeset_eval_all); // either expression is a number or it contains position() call; sort by document order if (_test != predicate_posinv) set.sort_do(); bool once = eval_once(set.type(), eval); apply_predicate(set, 0, stack, once); return set; } case ast_func_id: return xpath_node_set_raw(); case ast_step: { switch (_axis) { case axis_ancestor: return step_do(c, stack, eval, axis_to_type()); case axis_ancestor_or_self: return step_do(c, stack, eval, axis_to_type()); case axis_attribute: return step_do(c, stack, eval, axis_to_type()); case axis_child: return step_do(c, stack, eval, axis_to_type()); case axis_descendant: return step_do(c, stack, eval, axis_to_type()); case axis_descendant_or_self: return step_do(c, stack, eval, axis_to_type()); case axis_following: return step_do(c, stack, eval, axis_to_type()); case axis_following_sibling: return step_do(c, stack, eval, axis_to_type()); case axis_namespace: // namespaced axis is not supported return xpath_node_set_raw(); case axis_parent: return step_do(c, stack, eval, axis_to_type()); case axis_preceding: return step_do(c, stack, eval, axis_to_type()); case axis_preceding_sibling: return step_do(c, stack, eval, axis_to_type()); case axis_self: return step_do(c, stack, eval, axis_to_type()); default: assert(false && "Unknown axis"); // unreachable return xpath_node_set_raw(); } } case ast_step_root: { assert(!_right); // root step can't have any predicates xpath_node_set_raw ns; ns.set_type(xpath_node_set::type_sorted); if (c.n.node()) ns.push_back(c.n.node().root(), stack.result); else if (c.n.attribute()) ns.push_back(c.n.parent().root(), stack.result); return ns; } case ast_variable: { assert(_rettype == _data.variable->type()); if (_rettype == xpath_type_node_set) { const xpath_node_set& s = _data.variable->get_node_set(); xpath_node_set_raw ns; ns.set_type(s.type()); ns.append(s.begin(), s.end(), stack.result); return ns; } // variable needs to be converted to the correct type, this is handled by the fallthrough block below break; } default: ; } // none of the ast types that return the value directly matched, but conversions to node set are invalid assert(false && "Wrong expression for return type node set"); // unreachable return xpath_node_set_raw(); } void optimize(xpath_allocator* alloc) { if (_left) _left->optimize(alloc); if (_right) _right->optimize(alloc); if (_next) _next->optimize(alloc); // coverity[var_deref_model] optimize_self(alloc); } void optimize_self(xpath_allocator* alloc) { // Rewrite [position()=expr] with [expr] // Note that this step has to go before classification to recognize [position()=1] if ((_type == ast_filter || _type == ast_predicate) && _right && // workaround for clang static analyzer (_right is never null for ast_filter/ast_predicate) _right->_type == ast_op_equal && _right->_left->_type == ast_func_position && _right->_right->_rettype == xpath_type_number) { _right = _right->_right; } // Classify filter/predicate ops to perform various optimizations during evaluation if ((_type == ast_filter || _type == ast_predicate) && _right) // workaround for clang static analyzer (_right is never null for ast_filter/ast_predicate) { assert(_test == predicate_default); if (_right->_type == ast_number_constant && _right->_data.number == 1.0) _test = predicate_constant_one; else if (_right->_rettype == xpath_type_number && (_right->_type == ast_number_constant || _right->_type == ast_variable || _right->_type == ast_func_last)) _test = predicate_constant; else if (_right->_rettype != xpath_type_number && _right->is_posinv_expr()) _test = predicate_posinv; } // Rewrite descendant-or-self::node()/child::foo with descendant::foo // The former is a full form of //foo, the latter is much faster since it executes the node test immediately // Do a similar kind of rewrite for self/descendant/descendant-or-self axes // Note that we only rewrite positionally invariant steps (//foo[1] != /descendant::foo[1]) if (_type == ast_step && (_axis == axis_child || _axis == axis_self || _axis == axis_descendant || _axis == axis_descendant_or_self) && _left && _left->_type == ast_step && _left->_axis == axis_descendant_or_self && _left->_test == nodetest_type_node && !_left->_right && is_posinv_step()) { if (_axis == axis_child || _axis == axis_descendant) _axis = axis_descendant; else _axis = axis_descendant_or_self; _left = _left->_left; } // Use optimized lookup table implementation for translate() with constant arguments if (_type == ast_func_translate && _right && // workaround for clang static analyzer (_right is never null for ast_func_translate) _right->_type == ast_string_constant && _right->_next->_type == ast_string_constant) { unsigned char* table = translate_table_generate(alloc, _right->_data.string, _right->_next->_data.string); if (table) { _type = ast_opt_translate_table; _data.table = table; } } // Use optimized path for @attr = 'value' or @attr = $value if (_type == ast_op_equal && _left && _right && // workaround for clang static analyzer and Coverity (_left and _right are never null for ast_op_equal) // coverity[mixed_enums] _left->_type == ast_step && _left->_axis == axis_attribute && _left->_test == nodetest_name && !_left->_left && !_left->_right && (_right->_type == ast_string_constant || (_right->_type == ast_variable && _right->_rettype == xpath_type_string))) { _type = ast_opt_compare_attribute; } } bool is_posinv_expr() const { switch (_type) { case ast_func_position: case ast_func_last: return false; case ast_string_constant: case ast_number_constant: case ast_variable: return true; case ast_step: case ast_step_root: return true; case ast_predicate: case ast_filter: return true; default: if (_left && !_left->is_posinv_expr()) return false; for (xpath_ast_node* n = _right; n; n = n->_next) if (!n->is_posinv_expr()) return false; return true; } } bool is_posinv_step() const { assert(_type == ast_step); for (xpath_ast_node* n = _right; n; n = n->_next) { assert(n->_type == ast_predicate); if (n->_test != predicate_posinv) return false; } return true; } xpath_value_type rettype() const { return static_cast(_rettype); } }; static const size_t xpath_ast_depth_limit = #ifdef PUGIXML_XPATH_DEPTH_LIMIT PUGIXML_XPATH_DEPTH_LIMIT #else 1024 #endif ; struct xpath_parser { xpath_allocator* _alloc; xpath_lexer _lexer; const char_t* _query; xpath_variable_set* _variables; xpath_parse_result* _result; char_t _scratch[32]; size_t _depth; xpath_ast_node* error(const char* message) { _result->error = message; _result->offset = _lexer.current_pos() - _query; return 0; } xpath_ast_node* error_oom() { assert(_alloc->_error); *_alloc->_error = true; return 0; } xpath_ast_node* error_rec() { return error("Exceeded maximum allowed query depth"); } void* alloc_node() { return _alloc->allocate(sizeof(xpath_ast_node)); } xpath_ast_node* alloc_node(ast_type_t type, xpath_value_type rettype, const char_t* value) { void* memory = alloc_node(); return memory ? new (memory) xpath_ast_node(type, rettype, value) : 0; } xpath_ast_node* alloc_node(ast_type_t type, xpath_value_type rettype, double value) { void* memory = alloc_node(); return memory ? new (memory) xpath_ast_node(type, rettype, value) : 0; } xpath_ast_node* alloc_node(ast_type_t type, xpath_value_type rettype, xpath_variable* value) { void* memory = alloc_node(); return memory ? new (memory) xpath_ast_node(type, rettype, value) : 0; } xpath_ast_node* alloc_node(ast_type_t type, xpath_value_type rettype, xpath_ast_node* left = 0, xpath_ast_node* right = 0) { void* memory = alloc_node(); return memory ? new (memory) xpath_ast_node(type, rettype, left, right) : 0; } xpath_ast_node* alloc_node(ast_type_t type, xpath_ast_node* left, axis_t axis, nodetest_t test, const char_t* contents) { void* memory = alloc_node(); return memory ? new (memory) xpath_ast_node(type, left, axis, test, contents) : 0; } xpath_ast_node* alloc_node(ast_type_t type, xpath_ast_node* left, xpath_ast_node* right, predicate_t test) { void* memory = alloc_node(); return memory ? new (memory) xpath_ast_node(type, left, right, test) : 0; } const char_t* alloc_string(const xpath_lexer_string& value) { if (!value.begin) return PUGIXML_TEXT(""); size_t length = static_cast(value.end - value.begin); char_t* c = static_cast(_alloc->allocate((length + 1) * sizeof(char_t))); if (!c) return 0; memcpy(c, value.begin, length * sizeof(char_t)); c[length] = 0; return c; } xpath_ast_node* parse_function(const xpath_lexer_string& name, size_t argc, xpath_ast_node* args[2]) { switch (name.begin[0]) { case 'b': if (name == PUGIXML_TEXT("boolean") && argc == 1) return alloc_node(ast_func_boolean, xpath_type_boolean, args[0]); break; case 'c': if (name == PUGIXML_TEXT("count") && argc == 1) { if (args[0]->rettype() != xpath_type_node_set) return error("Function has to be applied to node set"); return alloc_node(ast_func_count, xpath_type_number, args[0]); } else if (name == PUGIXML_TEXT("contains") && argc == 2) return alloc_node(ast_func_contains, xpath_type_boolean, args[0], args[1]); else if (name == PUGIXML_TEXT("concat") && argc >= 2) return alloc_node(ast_func_concat, xpath_type_string, args[0], args[1]); else if (name == PUGIXML_TEXT("ceiling") && argc == 1) return alloc_node(ast_func_ceiling, xpath_type_number, args[0]); break; case 'f': if (name == PUGIXML_TEXT("false") && argc == 0) return alloc_node(ast_func_false, xpath_type_boolean); else if (name == PUGIXML_TEXT("floor") && argc == 1) return alloc_node(ast_func_floor, xpath_type_number, args[0]); break; case 'i': if (name == PUGIXML_TEXT("id") && argc == 1) return alloc_node(ast_func_id, xpath_type_node_set, args[0]); break; case 'l': if (name == PUGIXML_TEXT("last") && argc == 0) return alloc_node(ast_func_last, xpath_type_number); else if (name == PUGIXML_TEXT("lang") && argc == 1) return alloc_node(ast_func_lang, xpath_type_boolean, args[0]); else if (name == PUGIXML_TEXT("local-name") && argc <= 1) { if (argc == 1 && args[0]->rettype() != xpath_type_node_set) return error("Function has to be applied to node set"); return alloc_node(argc == 0 ? ast_func_local_name_0 : ast_func_local_name_1, xpath_type_string, args[0]); } break; case 'n': if (name == PUGIXML_TEXT("name") && argc <= 1) { if (argc == 1 && args[0]->rettype() != xpath_type_node_set) return error("Function has to be applied to node set"); return alloc_node(argc == 0 ? ast_func_name_0 : ast_func_name_1, xpath_type_string, args[0]); } else if (name == PUGIXML_TEXT("namespace-uri") && argc <= 1) { if (argc == 1 && args[0]->rettype() != xpath_type_node_set) return error("Function has to be applied to node set"); return alloc_node(argc == 0 ? ast_func_namespace_uri_0 : ast_func_namespace_uri_1, xpath_type_string, args[0]); } else if (name == PUGIXML_TEXT("normalize-space") && argc <= 1) return alloc_node(argc == 0 ? ast_func_normalize_space_0 : ast_func_normalize_space_1, xpath_type_string, args[0], args[1]); else if (name == PUGIXML_TEXT("not") && argc == 1) return alloc_node(ast_func_not, xpath_type_boolean, args[0]); else if (name == PUGIXML_TEXT("number") && argc <= 1) return alloc_node(argc == 0 ? ast_func_number_0 : ast_func_number_1, xpath_type_number, args[0]); break; case 'p': if (name == PUGIXML_TEXT("position") && argc == 0) return alloc_node(ast_func_position, xpath_type_number); break; case 'r': if (name == PUGIXML_TEXT("round") && argc == 1) return alloc_node(ast_func_round, xpath_type_number, args[0]); break; case 's': if (name == PUGIXML_TEXT("string") && argc <= 1) return alloc_node(argc == 0 ? ast_func_string_0 : ast_func_string_1, xpath_type_string, args[0]); else if (name == PUGIXML_TEXT("string-length") && argc <= 1) return alloc_node(argc == 0 ? ast_func_string_length_0 : ast_func_string_length_1, xpath_type_number, args[0]); else if (name == PUGIXML_TEXT("starts-with") && argc == 2) return alloc_node(ast_func_starts_with, xpath_type_boolean, args[0], args[1]); else if (name == PUGIXML_TEXT("substring-before") && argc == 2) return alloc_node(ast_func_substring_before, xpath_type_string, args[0], args[1]); else if (name == PUGIXML_TEXT("substring-after") && argc == 2) return alloc_node(ast_func_substring_after, xpath_type_string, args[0], args[1]); else if (name == PUGIXML_TEXT("substring") && (argc == 2 || argc == 3)) return alloc_node(argc == 2 ? ast_func_substring_2 : ast_func_substring_3, xpath_type_string, args[0], args[1]); else if (name == PUGIXML_TEXT("sum") && argc == 1) { if (args[0]->rettype() != xpath_type_node_set) return error("Function has to be applied to node set"); return alloc_node(ast_func_sum, xpath_type_number, args[0]); } break; case 't': if (name == PUGIXML_TEXT("translate") && argc == 3) return alloc_node(ast_func_translate, xpath_type_string, args[0], args[1]); else if (name == PUGIXML_TEXT("true") && argc == 0) return alloc_node(ast_func_true, xpath_type_boolean); break; default: break; } return error("Unrecognized function or wrong parameter count"); } axis_t parse_axis_name(const xpath_lexer_string& name, bool& specified) { specified = true; switch (name.begin[0]) { case 'a': if (name == PUGIXML_TEXT("ancestor")) return axis_ancestor; else if (name == PUGIXML_TEXT("ancestor-or-self")) return axis_ancestor_or_self; else if (name == PUGIXML_TEXT("attribute")) return axis_attribute; break; case 'c': if (name == PUGIXML_TEXT("child")) return axis_child; break; case 'd': if (name == PUGIXML_TEXT("descendant")) return axis_descendant; else if (name == PUGIXML_TEXT("descendant-or-self")) return axis_descendant_or_self; break; case 'f': if (name == PUGIXML_TEXT("following")) return axis_following; else if (name == PUGIXML_TEXT("following-sibling")) return axis_following_sibling; break; case 'n': if (name == PUGIXML_TEXT("namespace")) return axis_namespace; break; case 'p': if (name == PUGIXML_TEXT("parent")) return axis_parent; else if (name == PUGIXML_TEXT("preceding")) return axis_preceding; else if (name == PUGIXML_TEXT("preceding-sibling")) return axis_preceding_sibling; break; case 's': if (name == PUGIXML_TEXT("self")) return axis_self; break; default: break; } specified = false; return axis_child; } nodetest_t parse_node_test_type(const xpath_lexer_string& name) { switch (name.begin[0]) { case 'c': if (name == PUGIXML_TEXT("comment")) return nodetest_type_comment; break; case 'n': if (name == PUGIXML_TEXT("node")) return nodetest_type_node; break; case 'p': if (name == PUGIXML_TEXT("processing-instruction")) return nodetest_type_pi; break; case 't': if (name == PUGIXML_TEXT("text")) return nodetest_type_text; break; default: break; } return nodetest_none; } // PrimaryExpr ::= VariableReference | '(' Expr ')' | Literal | Number | FunctionCall xpath_ast_node* parse_primary_expression() { switch (_lexer.current()) { case lex_var_ref: { xpath_lexer_string name = _lexer.contents(); if (!_variables) return error("Unknown variable: variable set is not provided"); xpath_variable* var = 0; if (!get_variable_scratch(_scratch, _variables, name.begin, name.end, &var)) return error_oom(); if (!var) return error("Unknown variable: variable set does not contain the given name"); _lexer.next(); return alloc_node(ast_variable, var->type(), var); } case lex_open_brace: { _lexer.next(); xpath_ast_node* n = parse_expression(); if (!n) return 0; if (_lexer.current() != lex_close_brace) return error("Expected ')' to match an opening '('"); _lexer.next(); return n; } case lex_quoted_string: { const char_t* value = alloc_string(_lexer.contents()); if (!value) return 0; _lexer.next(); return alloc_node(ast_string_constant, xpath_type_string, value); } case lex_number: { double value = 0; if (!convert_string_to_number_scratch(_scratch, _lexer.contents().begin, _lexer.contents().end, &value)) return error_oom(); _lexer.next(); return alloc_node(ast_number_constant, xpath_type_number, value); } case lex_string: { xpath_ast_node* args[2] = {0}; size_t argc = 0; xpath_lexer_string function = _lexer.contents(); _lexer.next(); xpath_ast_node* last_arg = 0; if (_lexer.current() != lex_open_brace) return error("Unrecognized function call"); _lexer.next(); size_t old_depth = _depth; while (_lexer.current() != lex_close_brace) { if (argc > 0) { if (_lexer.current() != lex_comma) return error("No comma between function arguments"); _lexer.next(); } if (++_depth > xpath_ast_depth_limit) return error_rec(); xpath_ast_node* n = parse_expression(); if (!n) return 0; if (argc < 2) args[argc] = n; else last_arg->set_next(n); argc++; last_arg = n; } _lexer.next(); _depth = old_depth; return parse_function(function, argc, args); } default: return error("Unrecognizable primary expression"); } } // FilterExpr ::= PrimaryExpr | FilterExpr Predicate // Predicate ::= '[' PredicateExpr ']' // PredicateExpr ::= Expr xpath_ast_node* parse_filter_expression() { xpath_ast_node* n = parse_primary_expression(); if (!n) return 0; size_t old_depth = _depth; while (_lexer.current() == lex_open_square_brace) { _lexer.next(); if (++_depth > xpath_ast_depth_limit) return error_rec(); if (n->rettype() != xpath_type_node_set) return error("Predicate has to be applied to node set"); xpath_ast_node* expr = parse_expression(); if (!expr) return 0; n = alloc_node(ast_filter, n, expr, predicate_default); if (!n) return 0; if (_lexer.current() != lex_close_square_brace) return error("Expected ']' to match an opening '['"); _lexer.next(); } _depth = old_depth; return n; } // Step ::= AxisSpecifier NodeTest Predicate* | AbbreviatedStep // AxisSpecifier ::= AxisName '::' | '@'? // NodeTest ::= NameTest | NodeType '(' ')' | 'processing-instruction' '(' Literal ')' // NameTest ::= '*' | NCName ':' '*' | QName // AbbreviatedStep ::= '.' | '..' xpath_ast_node* parse_step(xpath_ast_node* set) { if (set && set->rettype() != xpath_type_node_set) return error("Step has to be applied to node set"); bool axis_specified = false; axis_t axis = axis_child; // implied child axis if (_lexer.current() == lex_axis_attribute) { axis = axis_attribute; axis_specified = true; _lexer.next(); } else if (_lexer.current() == lex_dot) { _lexer.next(); if (_lexer.current() == lex_open_square_brace) return error("Predicates are not allowed after an abbreviated step"); return alloc_node(ast_step, set, axis_self, nodetest_type_node, 0); } else if (_lexer.current() == lex_double_dot) { _lexer.next(); if (_lexer.current() == lex_open_square_brace) return error("Predicates are not allowed after an abbreviated step"); return alloc_node(ast_step, set, axis_parent, nodetest_type_node, 0); } nodetest_t nt_type = nodetest_none; xpath_lexer_string nt_name; if (_lexer.current() == lex_string) { // node name test nt_name = _lexer.contents(); _lexer.next(); // was it an axis name? if (_lexer.current() == lex_double_colon) { // parse axis name if (axis_specified) return error("Two axis specifiers in one step"); axis = parse_axis_name(nt_name, axis_specified); if (!axis_specified) return error("Unknown axis"); // read actual node test _lexer.next(); if (_lexer.current() == lex_multiply) { nt_type = nodetest_all; nt_name = xpath_lexer_string(); _lexer.next(); } else if (_lexer.current() == lex_string) { nt_name = _lexer.contents(); _lexer.next(); } else { return error("Unrecognized node test"); } } if (nt_type == nodetest_none) { // node type test or processing-instruction if (_lexer.current() == lex_open_brace) { _lexer.next(); if (_lexer.current() == lex_close_brace) { _lexer.next(); nt_type = parse_node_test_type(nt_name); if (nt_type == nodetest_none) return error("Unrecognized node type"); nt_name = xpath_lexer_string(); } else if (nt_name == PUGIXML_TEXT("processing-instruction")) { if (_lexer.current() != lex_quoted_string) return error("Only literals are allowed as arguments to processing-instruction()"); nt_type = nodetest_pi; nt_name = _lexer.contents(); _lexer.next(); if (_lexer.current() != lex_close_brace) return error("Unmatched brace near processing-instruction()"); _lexer.next(); } else { return error("Unmatched brace near node type test"); } } // QName or NCName:* else { if (nt_name.end - nt_name.begin > 2 && nt_name.end[-2] == ':' && nt_name.end[-1] == '*') // NCName:* { nt_name.end--; // erase * nt_type = nodetest_all_in_namespace; } else { nt_type = nodetest_name; } } } } else if (_lexer.current() == lex_multiply) { nt_type = nodetest_all; _lexer.next(); } else { return error("Unrecognized node test"); } const char_t* nt_name_copy = alloc_string(nt_name); if (!nt_name_copy) return 0; xpath_ast_node* n = alloc_node(ast_step, set, axis, nt_type, nt_name_copy); if (!n) return 0; size_t old_depth = _depth; xpath_ast_node* last = 0; while (_lexer.current() == lex_open_square_brace) { _lexer.next(); if (++_depth > xpath_ast_depth_limit) return error_rec(); xpath_ast_node* expr = parse_expression(); if (!expr) return 0; xpath_ast_node* pred = alloc_node(ast_predicate, 0, expr, predicate_default); if (!pred) return 0; if (_lexer.current() != lex_close_square_brace) return error("Expected ']' to match an opening '['"); _lexer.next(); if (last) last->set_next(pred); else n->set_right(pred); last = pred; } _depth = old_depth; return n; } // RelativeLocationPath ::= Step | RelativeLocationPath '/' Step | RelativeLocationPath '//' Step xpath_ast_node* parse_relative_location_path(xpath_ast_node* set) { xpath_ast_node* n = parse_step(set); if (!n) return 0; size_t old_depth = _depth; while (_lexer.current() == lex_slash || _lexer.current() == lex_double_slash) { lexeme_t l = _lexer.current(); _lexer.next(); if (l == lex_double_slash) { n = alloc_node(ast_step, n, axis_descendant_or_self, nodetest_type_node, 0); if (!n) return 0; ++_depth; } if (++_depth > xpath_ast_depth_limit) return error_rec(); n = parse_step(n); if (!n) return 0; } _depth = old_depth; return n; } // LocationPath ::= RelativeLocationPath | AbsoluteLocationPath // AbsoluteLocationPath ::= '/' RelativeLocationPath? | '//' RelativeLocationPath xpath_ast_node* parse_location_path() { if (_lexer.current() == lex_slash) { _lexer.next(); xpath_ast_node* n = alloc_node(ast_step_root, xpath_type_node_set); if (!n) return 0; // relative location path can start from axis_attribute, dot, double_dot, multiply and string lexemes; any other lexeme means standalone root path lexeme_t l = _lexer.current(); if (l == lex_string || l == lex_axis_attribute || l == lex_dot || l == lex_double_dot || l == lex_multiply) return parse_relative_location_path(n); else return n; } else if (_lexer.current() == lex_double_slash) { _lexer.next(); xpath_ast_node* n = alloc_node(ast_step_root, xpath_type_node_set); if (!n) return 0; n = alloc_node(ast_step, n, axis_descendant_or_self, nodetest_type_node, 0); if (!n) return 0; return parse_relative_location_path(n); } // else clause moved outside of if because of bogus warning 'control may reach end of non-void function being inlined' in gcc 4.0.1 return parse_relative_location_path(0); } // PathExpr ::= LocationPath // | FilterExpr // | FilterExpr '/' RelativeLocationPath // | FilterExpr '//' RelativeLocationPath // UnionExpr ::= PathExpr | UnionExpr '|' PathExpr // UnaryExpr ::= UnionExpr | '-' UnaryExpr xpath_ast_node* parse_path_or_unary_expression() { // Clarification. // PathExpr begins with either LocationPath or FilterExpr. // FilterExpr begins with PrimaryExpr // PrimaryExpr begins with '$' in case of it being a variable reference, // '(' in case of it being an expression, string literal, number constant or // function call. if (_lexer.current() == lex_var_ref || _lexer.current() == lex_open_brace || _lexer.current() == lex_quoted_string || _lexer.current() == lex_number || _lexer.current() == lex_string) { if (_lexer.current() == lex_string) { // This is either a function call, or not - if not, we shall proceed with location path const char_t* state = _lexer.state(); while (PUGI_IMPL_IS_CHARTYPE(*state, ct_space)) ++state; if (*state != '(') return parse_location_path(); // This looks like a function call; however this still can be a node-test. Check it. if (parse_node_test_type(_lexer.contents()) != nodetest_none) return parse_location_path(); } xpath_ast_node* n = parse_filter_expression(); if (!n) return 0; if (_lexer.current() == lex_slash || _lexer.current() == lex_double_slash) { lexeme_t l = _lexer.current(); _lexer.next(); if (l == lex_double_slash) { if (n->rettype() != xpath_type_node_set) return error("Step has to be applied to node set"); n = alloc_node(ast_step, n, axis_descendant_or_self, nodetest_type_node, 0); if (!n) return 0; } // select from location path return parse_relative_location_path(n); } return n; } else if (_lexer.current() == lex_minus) { _lexer.next(); // precedence 7+ - only parses union expressions xpath_ast_node* n = parse_expression(7); if (!n) return 0; return alloc_node(ast_op_negate, xpath_type_number, n); } else { return parse_location_path(); } } struct binary_op_t { ast_type_t asttype; xpath_value_type rettype; int precedence; binary_op_t(): asttype(ast_unknown), rettype(xpath_type_none), precedence(0) { } binary_op_t(ast_type_t asttype_, xpath_value_type rettype_, int precedence_): asttype(asttype_), rettype(rettype_), precedence(precedence_) { } static binary_op_t parse(xpath_lexer& lexer) { switch (lexer.current()) { case lex_string: if (lexer.contents() == PUGIXML_TEXT("or")) return binary_op_t(ast_op_or, xpath_type_boolean, 1); else if (lexer.contents() == PUGIXML_TEXT("and")) return binary_op_t(ast_op_and, xpath_type_boolean, 2); else if (lexer.contents() == PUGIXML_TEXT("div")) return binary_op_t(ast_op_divide, xpath_type_number, 6); else if (lexer.contents() == PUGIXML_TEXT("mod")) return binary_op_t(ast_op_mod, xpath_type_number, 6); else return binary_op_t(); case lex_equal: return binary_op_t(ast_op_equal, xpath_type_boolean, 3); case lex_not_equal: return binary_op_t(ast_op_not_equal, xpath_type_boolean, 3); case lex_less: return binary_op_t(ast_op_less, xpath_type_boolean, 4); case lex_greater: return binary_op_t(ast_op_greater, xpath_type_boolean, 4); case lex_less_or_equal: return binary_op_t(ast_op_less_or_equal, xpath_type_boolean, 4); case lex_greater_or_equal: return binary_op_t(ast_op_greater_or_equal, xpath_type_boolean, 4); case lex_plus: return binary_op_t(ast_op_add, xpath_type_number, 5); case lex_minus: return binary_op_t(ast_op_subtract, xpath_type_number, 5); case lex_multiply: return binary_op_t(ast_op_multiply, xpath_type_number, 6); case lex_union: return binary_op_t(ast_op_union, xpath_type_node_set, 7); default: return binary_op_t(); } } }; xpath_ast_node* parse_expression_rec(xpath_ast_node* lhs, int limit) { binary_op_t op = binary_op_t::parse(_lexer); while (op.asttype != ast_unknown && op.precedence >= limit) { _lexer.next(); if (++_depth > xpath_ast_depth_limit) return error_rec(); xpath_ast_node* rhs = parse_path_or_unary_expression(); if (!rhs) return 0; binary_op_t nextop = binary_op_t::parse(_lexer); while (nextop.asttype != ast_unknown && nextop.precedence > op.precedence) { rhs = parse_expression_rec(rhs, nextop.precedence); if (!rhs) return 0; nextop = binary_op_t::parse(_lexer); } if (op.asttype == ast_op_union && (lhs->rettype() != xpath_type_node_set || rhs->rettype() != xpath_type_node_set)) return error("Union operator has to be applied to node sets"); lhs = alloc_node(op.asttype, op.rettype, lhs, rhs); if (!lhs) return 0; op = binary_op_t::parse(_lexer); } return lhs; } // Expr ::= OrExpr // OrExpr ::= AndExpr | OrExpr 'or' AndExpr // AndExpr ::= EqualityExpr | AndExpr 'and' EqualityExpr // EqualityExpr ::= RelationalExpr // | EqualityExpr '=' RelationalExpr // | EqualityExpr '!=' RelationalExpr // RelationalExpr ::= AdditiveExpr // | RelationalExpr '<' AdditiveExpr // | RelationalExpr '>' AdditiveExpr // | RelationalExpr '<=' AdditiveExpr // | RelationalExpr '>=' AdditiveExpr // AdditiveExpr ::= MultiplicativeExpr // | AdditiveExpr '+' MultiplicativeExpr // | AdditiveExpr '-' MultiplicativeExpr // MultiplicativeExpr ::= UnaryExpr // | MultiplicativeExpr '*' UnaryExpr // | MultiplicativeExpr 'div' UnaryExpr // | MultiplicativeExpr 'mod' UnaryExpr xpath_ast_node* parse_expression(int limit = 0) { size_t old_depth = _depth; if (++_depth > xpath_ast_depth_limit) return error_rec(); xpath_ast_node* n = parse_path_or_unary_expression(); if (!n) return 0; n = parse_expression_rec(n, limit); _depth = old_depth; return n; } xpath_parser(const char_t* query, xpath_variable_set* variables, xpath_allocator* alloc, xpath_parse_result* result): _alloc(alloc), _lexer(query), _query(query), _variables(variables), _result(result), _depth(0) { } xpath_ast_node* parse() { xpath_ast_node* n = parse_expression(); if (!n) return 0; assert(_depth == 0); // check if there are unparsed tokens left if (_lexer.current() != lex_eof) return error("Incorrect query"); return n; } static xpath_ast_node* parse(const char_t* query, xpath_variable_set* variables, xpath_allocator* alloc, xpath_parse_result* result) { xpath_parser parser(query, variables, alloc, result); return parser.parse(); } }; struct xpath_query_impl { static xpath_query_impl* create() { void* memory = xml_memory::allocate(sizeof(xpath_query_impl)); if (!memory) return 0; return new (memory) xpath_query_impl(); } static void destroy(xpath_query_impl* impl) { // free all allocated pages impl->alloc.release(); // free allocator memory (with the first page) xml_memory::deallocate(impl); } xpath_query_impl(): root(0), alloc(&block, &oom), oom(false) { block.next = 0; block.capacity = sizeof(block.data); } xpath_ast_node* root; xpath_allocator alloc; xpath_memory_block block; bool oom; }; PUGI_IMPL_FN impl::xpath_ast_node* evaluate_node_set_prepare(xpath_query_impl* impl) { if (!impl) return 0; if (impl->root->rettype() != xpath_type_node_set) { #ifdef PUGIXML_NO_EXCEPTIONS return 0; #else xpath_parse_result res; res.error = "Expression does not evaluate to node set"; throw xpath_exception(res); #endif } return impl->root; } PUGI_IMPL_NS_END namespace pugi { #ifndef PUGIXML_NO_EXCEPTIONS PUGI_IMPL_FN xpath_exception::xpath_exception(const xpath_parse_result& result_): _result(result_) { assert(_result.error); } PUGI_IMPL_FN const char* xpath_exception::what() const throw() { return _result.error; } PUGI_IMPL_FN const xpath_parse_result& xpath_exception::result() const { return _result; } #endif PUGI_IMPL_FN xpath_node::xpath_node() { } PUGI_IMPL_FN xpath_node::xpath_node(const xml_node& node_): _node(node_) { } PUGI_IMPL_FN xpath_node::xpath_node(const xml_attribute& attribute_, const xml_node& parent_): _node(attribute_ ? parent_ : xml_node()), _attribute(attribute_) { } PUGI_IMPL_FN xml_node xpath_node::node() const { return _attribute ? xml_node() : _node; } PUGI_IMPL_FN xml_attribute xpath_node::attribute() const { return _attribute; } PUGI_IMPL_FN xml_node xpath_node::parent() const { return _attribute ? _node : _node.parent(); } PUGI_IMPL_FN static void unspecified_bool_xpath_node(xpath_node***) { } PUGI_IMPL_FN xpath_node::operator xpath_node::unspecified_bool_type() const { return (_node || _attribute) ? unspecified_bool_xpath_node : 0; } PUGI_IMPL_FN bool xpath_node::operator!() const { return !(_node || _attribute); } PUGI_IMPL_FN bool xpath_node::operator==(const xpath_node& n) const { return _node == n._node && _attribute == n._attribute; } PUGI_IMPL_FN bool xpath_node::operator!=(const xpath_node& n) const { return _node != n._node || _attribute != n._attribute; } #ifdef __BORLANDC__ PUGI_IMPL_FN bool operator&&(const xpath_node& lhs, bool rhs) { return (bool)lhs && rhs; } PUGI_IMPL_FN bool operator||(const xpath_node& lhs, bool rhs) { return (bool)lhs || rhs; } #endif PUGI_IMPL_FN void xpath_node_set::_assign(const_iterator begin_, const_iterator end_, type_t type_) { assert(begin_ <= end_); size_t size_ = static_cast(end_ - begin_); // use internal buffer for 0 or 1 elements, heap buffer otherwise xpath_node* storage = (size_ <= 1) ? _storage : static_cast(impl::xml_memory::allocate(size_ * sizeof(xpath_node))); if (!storage) { #ifdef PUGIXML_NO_EXCEPTIONS return; #else throw std::bad_alloc(); #endif } // deallocate old buffer if (_begin != _storage) impl::xml_memory::deallocate(_begin); // size check is necessary because for begin_ = end_ = nullptr, memcpy is UB if (size_) memcpy(storage, begin_, size_ * sizeof(xpath_node)); _begin = storage; _end = storage + size_; _type = type_; } #ifdef PUGIXML_HAS_MOVE PUGI_IMPL_FN void xpath_node_set::_move(xpath_node_set& rhs) PUGIXML_NOEXCEPT { _type = rhs._type; _storage[0] = rhs._storage[0]; _begin = (rhs._begin == rhs._storage) ? _storage : rhs._begin; _end = _begin + (rhs._end - rhs._begin); rhs._type = type_unsorted; rhs._begin = rhs._storage; rhs._end = rhs._storage; } #endif PUGI_IMPL_FN xpath_node_set::xpath_node_set(): _type(type_unsorted), _begin(_storage), _end(_storage) { } PUGI_IMPL_FN xpath_node_set::xpath_node_set(const_iterator begin_, const_iterator end_, type_t type_): _type(type_unsorted), _begin(_storage), _end(_storage) { _assign(begin_, end_, type_); } PUGI_IMPL_FN xpath_node_set::~xpath_node_set() { if (_begin != _storage) impl::xml_memory::deallocate(_begin); } PUGI_IMPL_FN xpath_node_set::xpath_node_set(const xpath_node_set& ns): _type(type_unsorted), _begin(_storage), _end(_storage) { _assign(ns._begin, ns._end, ns._type); } PUGI_IMPL_FN xpath_node_set& xpath_node_set::operator=(const xpath_node_set& ns) { if (this == &ns) return *this; _assign(ns._begin, ns._end, ns._type); return *this; } #ifdef PUGIXML_HAS_MOVE PUGI_IMPL_FN xpath_node_set::xpath_node_set(xpath_node_set&& rhs) PUGIXML_NOEXCEPT: _type(type_unsorted), _begin(_storage), _end(_storage) { _move(rhs); } PUGI_IMPL_FN xpath_node_set& xpath_node_set::operator=(xpath_node_set&& rhs) PUGIXML_NOEXCEPT { if (this == &rhs) return *this; if (_begin != _storage) impl::xml_memory::deallocate(_begin); _move(rhs); return *this; } #endif PUGI_IMPL_FN xpath_node_set::type_t xpath_node_set::type() const { return _type; } PUGI_IMPL_FN size_t xpath_node_set::size() const { return _end - _begin; } PUGI_IMPL_FN bool xpath_node_set::empty() const { return _begin == _end; } PUGI_IMPL_FN const xpath_node& xpath_node_set::operator[](size_t index) const { assert(index < size()); return _begin[index]; } PUGI_IMPL_FN xpath_node_set::const_iterator xpath_node_set::begin() const { return _begin; } PUGI_IMPL_FN xpath_node_set::const_iterator xpath_node_set::end() const { return _end; } PUGI_IMPL_FN void xpath_node_set::sort(bool reverse) { _type = impl::xpath_sort(_begin, _end, _type, reverse); } PUGI_IMPL_FN xpath_node xpath_node_set::first() const { return impl::xpath_first(_begin, _end, _type); } PUGI_IMPL_FN xpath_parse_result::xpath_parse_result(): error("Internal error"), offset(0) { } PUGI_IMPL_FN xpath_parse_result::operator bool() const { return error == 0; } PUGI_IMPL_FN const char* xpath_parse_result::description() const { return error ? error : "No error"; } PUGI_IMPL_FN xpath_variable::xpath_variable(xpath_value_type type_): _type(type_), _next(0) { } PUGI_IMPL_FN const char_t* xpath_variable::name() const { switch (_type) { case xpath_type_node_set: return static_cast(this)->name; case xpath_type_number: return static_cast(this)->name; case xpath_type_string: return static_cast(this)->name; case xpath_type_boolean: return static_cast(this)->name; default: assert(false && "Invalid variable type"); // unreachable return 0; } } PUGI_IMPL_FN xpath_value_type xpath_variable::type() const { return _type; } PUGI_IMPL_FN bool xpath_variable::get_boolean() const { return (_type == xpath_type_boolean) ? static_cast(this)->value : false; } PUGI_IMPL_FN double xpath_variable::get_number() const { return (_type == xpath_type_number) ? static_cast(this)->value : impl::gen_nan(); } PUGI_IMPL_FN const char_t* xpath_variable::get_string() const { const char_t* value = (_type == xpath_type_string) ? static_cast(this)->value : 0; return value ? value : PUGIXML_TEXT(""); } PUGI_IMPL_FN const xpath_node_set& xpath_variable::get_node_set() const { return (_type == xpath_type_node_set) ? static_cast(this)->value : impl::dummy_node_set; } PUGI_IMPL_FN bool xpath_variable::set(bool value) { if (_type != xpath_type_boolean) return false; static_cast(this)->value = value; return true; } PUGI_IMPL_FN bool xpath_variable::set(double value) { if (_type != xpath_type_number) return false; static_cast(this)->value = value; return true; } PUGI_IMPL_FN bool xpath_variable::set(const char_t* value) { if (_type != xpath_type_string) return false; impl::xpath_variable_string* var = static_cast(this); // duplicate string size_t size = (impl::strlength(value) + 1) * sizeof(char_t); char_t* copy = static_cast(impl::xml_memory::allocate(size)); if (!copy) return false; memcpy(copy, value, size); // replace old string if (var->value) impl::xml_memory::deallocate(var->value); var->value = copy; return true; } PUGI_IMPL_FN bool xpath_variable::set(const xpath_node_set& value) { if (_type != xpath_type_node_set) return false; static_cast(this)->value = value; return true; } PUGI_IMPL_FN xpath_variable_set::xpath_variable_set() { for (size_t i = 0; i < sizeof(_data) / sizeof(_data[0]); ++i) _data[i] = 0; } PUGI_IMPL_FN xpath_variable_set::~xpath_variable_set() { for (size_t i = 0; i < sizeof(_data) / sizeof(_data[0]); ++i) _destroy(_data[i]); } PUGI_IMPL_FN xpath_variable_set::xpath_variable_set(const xpath_variable_set& rhs) { for (size_t i = 0; i < sizeof(_data) / sizeof(_data[0]); ++i) _data[i] = 0; _assign(rhs); } PUGI_IMPL_FN xpath_variable_set& xpath_variable_set::operator=(const xpath_variable_set& rhs) { if (this == &rhs) return *this; _assign(rhs); return *this; } #ifdef PUGIXML_HAS_MOVE PUGI_IMPL_FN xpath_variable_set::xpath_variable_set(xpath_variable_set&& rhs) PUGIXML_NOEXCEPT { for (size_t i = 0; i < sizeof(_data) / sizeof(_data[0]); ++i) { _data[i] = rhs._data[i]; rhs._data[i] = 0; } } PUGI_IMPL_FN xpath_variable_set& xpath_variable_set::operator=(xpath_variable_set&& rhs) PUGIXML_NOEXCEPT { for (size_t i = 0; i < sizeof(_data) / sizeof(_data[0]); ++i) { _destroy(_data[i]); _data[i] = rhs._data[i]; rhs._data[i] = 0; } return *this; } #endif PUGI_IMPL_FN void xpath_variable_set::_assign(const xpath_variable_set& rhs) { xpath_variable_set temp; for (size_t i = 0; i < sizeof(_data) / sizeof(_data[0]); ++i) if (rhs._data[i] && !_clone(rhs._data[i], &temp._data[i])) return; _swap(temp); } PUGI_IMPL_FN void xpath_variable_set::_swap(xpath_variable_set& rhs) { for (size_t i = 0; i < sizeof(_data) / sizeof(_data[0]); ++i) { xpath_variable* chain = _data[i]; _data[i] = rhs._data[i]; rhs._data[i] = chain; } } PUGI_IMPL_FN xpath_variable* xpath_variable_set::_find(const char_t* name) const { const size_t hash_size = sizeof(_data) / sizeof(_data[0]); size_t hash = impl::hash_string(name) % hash_size; // look for existing variable for (xpath_variable* var = _data[hash]; var; var = var->_next) if (impl::strequal(var->name(), name)) return var; return 0; } PUGI_IMPL_FN bool xpath_variable_set::_clone(xpath_variable* var, xpath_variable** out_result) { xpath_variable* last = 0; while (var) { // allocate storage for new variable xpath_variable* nvar = impl::new_xpath_variable(var->_type, var->name()); if (!nvar) return false; // link the variable to the result immediately to handle failures gracefully if (last) last->_next = nvar; else *out_result = nvar; last = nvar; // copy the value; this can fail due to out-of-memory conditions if (!impl::copy_xpath_variable(nvar, var)) return false; var = var->_next; } return true; } PUGI_IMPL_FN void xpath_variable_set::_destroy(xpath_variable* var) { while (var) { xpath_variable* next = var->_next; impl::delete_xpath_variable(var->_type, var); var = next; } } PUGI_IMPL_FN xpath_variable* xpath_variable_set::add(const char_t* name, xpath_value_type type) { const size_t hash_size = sizeof(_data) / sizeof(_data[0]); size_t hash = impl::hash_string(name) % hash_size; // look for existing variable for (xpath_variable* var = _data[hash]; var; var = var->_next) if (impl::strequal(var->name(), name)) return var->type() == type ? var : 0; // add new variable xpath_variable* result = impl::new_xpath_variable(type, name); if (result) { result->_next = _data[hash]; _data[hash] = result; } return result; } PUGI_IMPL_FN bool xpath_variable_set::set(const char_t* name, bool value) { xpath_variable* var = add(name, xpath_type_boolean); return var ? var->set(value) : false; } PUGI_IMPL_FN bool xpath_variable_set::set(const char_t* name, double value) { xpath_variable* var = add(name, xpath_type_number); return var ? var->set(value) : false; } PUGI_IMPL_FN bool xpath_variable_set::set(const char_t* name, const char_t* value) { xpath_variable* var = add(name, xpath_type_string); return var ? var->set(value) : false; } PUGI_IMPL_FN bool xpath_variable_set::set(const char_t* name, const xpath_node_set& value) { xpath_variable* var = add(name, xpath_type_node_set); return var ? var->set(value) : false; } PUGI_IMPL_FN xpath_variable* xpath_variable_set::get(const char_t* name) { return _find(name); } PUGI_IMPL_FN const xpath_variable* xpath_variable_set::get(const char_t* name) const { return _find(name); } PUGI_IMPL_FN xpath_query::xpath_query(const char_t* query, xpath_variable_set* variables): _impl(0) { impl::xpath_query_impl* qimpl = impl::xpath_query_impl::create(); if (!qimpl) { #ifdef PUGIXML_NO_EXCEPTIONS _result.error = "Out of memory"; #else throw std::bad_alloc(); #endif } else { using impl::auto_deleter; // MSVC7 workaround auto_deleter impl(qimpl, impl::xpath_query_impl::destroy); qimpl->root = impl::xpath_parser::parse(query, variables, &qimpl->alloc, &_result); if (qimpl->root) { qimpl->root->optimize(&qimpl->alloc); _impl = impl.release(); _result.error = 0; } else { #ifdef PUGIXML_NO_EXCEPTIONS if (qimpl->oom) _result.error = "Out of memory"; #else if (qimpl->oom) throw std::bad_alloc(); throw xpath_exception(_result); #endif } } } PUGI_IMPL_FN xpath_query::xpath_query(): _impl(0) { } PUGI_IMPL_FN xpath_query::~xpath_query() { if (_impl) impl::xpath_query_impl::destroy(static_cast(_impl)); } #ifdef PUGIXML_HAS_MOVE PUGI_IMPL_FN xpath_query::xpath_query(xpath_query&& rhs) PUGIXML_NOEXCEPT { _impl = rhs._impl; _result = rhs._result; rhs._impl = 0; rhs._result = xpath_parse_result(); } PUGI_IMPL_FN xpath_query& xpath_query::operator=(xpath_query&& rhs) PUGIXML_NOEXCEPT { if (this == &rhs) return *this; if (_impl) impl::xpath_query_impl::destroy(static_cast(_impl)); _impl = rhs._impl; _result = rhs._result; rhs._impl = 0; rhs._result = xpath_parse_result(); return *this; } #endif PUGI_IMPL_FN xpath_value_type xpath_query::return_type() const { if (!_impl) return xpath_type_none; return static_cast(_impl)->root->rettype(); } PUGI_IMPL_FN bool xpath_query::evaluate_boolean(const xpath_node& n) const { if (!_impl) return false; impl::xpath_context c(n, 1, 1); impl::xpath_stack_data sd; bool r = static_cast(_impl)->root->eval_boolean(c, sd.stack); if (sd.oom) { #ifdef PUGIXML_NO_EXCEPTIONS return false; #else throw std::bad_alloc(); #endif } return r; } PUGI_IMPL_FN double xpath_query::evaluate_number(const xpath_node& n) const { if (!_impl) return impl::gen_nan(); impl::xpath_context c(n, 1, 1); impl::xpath_stack_data sd; double r = static_cast(_impl)->root->eval_number(c, sd.stack); if (sd.oom) { #ifdef PUGIXML_NO_EXCEPTIONS return impl::gen_nan(); #else throw std::bad_alloc(); #endif } return r; } #ifndef PUGIXML_NO_STL PUGI_IMPL_FN string_t xpath_query::evaluate_string(const xpath_node& n) const { if (!_impl) return string_t(); impl::xpath_context c(n, 1, 1); impl::xpath_stack_data sd; impl::xpath_string r = static_cast(_impl)->root->eval_string(c, sd.stack); if (sd.oom) { #ifdef PUGIXML_NO_EXCEPTIONS return string_t(); #else throw std::bad_alloc(); #endif } return string_t(r.c_str(), r.length()); } #endif PUGI_IMPL_FN size_t xpath_query::evaluate_string(char_t* buffer, size_t capacity, const xpath_node& n) const { impl::xpath_context c(n, 1, 1); impl::xpath_stack_data sd; impl::xpath_string r = _impl ? static_cast(_impl)->root->eval_string(c, sd.stack) : impl::xpath_string(); if (sd.oom) { #ifdef PUGIXML_NO_EXCEPTIONS r = impl::xpath_string(); #else throw std::bad_alloc(); #endif } size_t full_size = r.length() + 1; if (capacity > 0) { size_t size = (full_size < capacity) ? full_size : capacity; assert(size > 0); memcpy(buffer, r.c_str(), (size - 1) * sizeof(char_t)); buffer[size - 1] = 0; } return full_size; } PUGI_IMPL_FN xpath_node_set xpath_query::evaluate_node_set(const xpath_node& n) const { impl::xpath_ast_node* root = impl::evaluate_node_set_prepare(static_cast(_impl)); if (!root) return xpath_node_set(); impl::xpath_context c(n, 1, 1); impl::xpath_stack_data sd; impl::xpath_node_set_raw r = root->eval_node_set(c, sd.stack, impl::nodeset_eval_all); if (sd.oom) { #ifdef PUGIXML_NO_EXCEPTIONS return xpath_node_set(); #else throw std::bad_alloc(); #endif } return xpath_node_set(r.begin(), r.end(), r.type()); } PUGI_IMPL_FN xpath_node xpath_query::evaluate_node(const xpath_node& n) const { impl::xpath_ast_node* root = impl::evaluate_node_set_prepare(static_cast(_impl)); if (!root) return xpath_node(); impl::xpath_context c(n, 1, 1); impl::xpath_stack_data sd; impl::xpath_node_set_raw r = root->eval_node_set(c, sd.stack, impl::nodeset_eval_first); if (sd.oom) { #ifdef PUGIXML_NO_EXCEPTIONS return xpath_node(); #else throw std::bad_alloc(); #endif } return r.first(); } PUGI_IMPL_FN const xpath_parse_result& xpath_query::result() const { return _result; } PUGI_IMPL_FN static void unspecified_bool_xpath_query(xpath_query***) { } PUGI_IMPL_FN xpath_query::operator xpath_query::unspecified_bool_type() const { return _impl ? unspecified_bool_xpath_query : 0; } PUGI_IMPL_FN bool xpath_query::operator!() const { return !_impl; } PUGI_IMPL_FN xpath_node xml_node::select_node(const char_t* query, xpath_variable_set* variables) const { xpath_query q(query, variables); return q.evaluate_node(*this); } PUGI_IMPL_FN xpath_node xml_node::select_node(const xpath_query& query) const { return query.evaluate_node(*this); } PUGI_IMPL_FN xpath_node_set xml_node::select_nodes(const char_t* query, xpath_variable_set* variables) const { xpath_query q(query, variables); return q.evaluate_node_set(*this); } PUGI_IMPL_FN xpath_node_set xml_node::select_nodes(const xpath_query& query) const { return query.evaluate_node_set(*this); } PUGI_IMPL_FN xpath_node xml_node::select_single_node(const char_t* query, xpath_variable_set* variables) const { xpath_query q(query, variables); return q.evaluate_node(*this); } PUGI_IMPL_FN xpath_node xml_node::select_single_node(const xpath_query& query) const { return query.evaluate_node(*this); } } #endif #ifdef __BORLANDC__ # pragma option pop #endif // Intel C++ does not properly keep warning state for function templates, // so popping warning state at the end of translation unit leads to warnings in the middle. #if defined(_MSC_VER) && !defined(__INTEL_COMPILER) # pragma warning(pop) #endif #if defined(_MSC_VER) && defined(__c2__) # pragma clang diagnostic pop #endif // Undefine all local macros (makes sure we're not leaking macros in header-only mode) #undef PUGI_IMPL_NO_INLINE #undef PUGI_IMPL_UNLIKELY #undef PUGI_IMPL_STATIC_ASSERT #undef PUGI_IMPL_DMC_VOLATILE #undef PUGI_IMPL_UNSIGNED_OVERFLOW #undef PUGI_IMPL_MSVC_CRT_VERSION #undef PUGI_IMPL_SNPRINTF #undef PUGI_IMPL_NS_BEGIN #undef PUGI_IMPL_NS_END #undef PUGI_IMPL_FN #undef PUGI_IMPL_FN_NO_INLINE #undef PUGI_IMPL_GETHEADER_IMPL #undef PUGI_IMPL_GETPAGE_IMPL #undef PUGI_IMPL_GETPAGE #undef PUGI_IMPL_NODETYPE #undef PUGI_IMPL_IS_CHARTYPE_IMPL #undef PUGI_IMPL_IS_CHARTYPE #undef PUGI_IMPL_IS_CHARTYPEX #undef PUGI_IMPL_ENDSWITH #undef PUGI_IMPL_SKIPWS #undef PUGI_IMPL_OPTSET #undef PUGI_IMPL_PUSHNODE #undef PUGI_IMPL_POPNODE #undef PUGI_IMPL_SCANFOR #undef PUGI_IMPL_SCANWHILE #undef PUGI_IMPL_SCANWHILE_UNROLL #undef PUGI_IMPL_ENDSEG #undef PUGI_IMPL_THROW_ERROR #undef PUGI_IMPL_CHECK_ERROR #endif /** * Copyright (c) 2006-2023 Arseny Kapoulkine * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ pr0m1th3as-datatypes-9450b29/src/ods/pugixml.hpp000066400000000000000000001520311527370255000214520ustar00rootroot00000000000000/** * pugixml parser - version 1.14 * -------------------------------------------------------- * Copyright (C) 2006-2023, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) * Report bugs and download new versions at https://pugixml.org/ * * This library is distributed under the MIT License. See notice at the end * of this file. * * This work is based on the pugxml parser, which is: * Copyright (C) 2003, by Kristen Wegner (kristen@tima.net) */ // Define version macro; evaluates to major * 1000 + minor * 10 + patch so that it's safe to use in less-than comparisons // Note: pugixml used major * 100 + minor * 10 + patch format up until 1.9 (which had version identifier 190); starting from pugixml 1.10, the minor version number is two digits #ifndef PUGIXML_VERSION # define PUGIXML_VERSION 1140 // 1.14 #endif // Include user configuration file (this can define various configuration macros) #include "pugiconfig.hpp" #ifndef HEADER_PUGIXML_HPP #define HEADER_PUGIXML_HPP // Include stddef.h for size_t and ptrdiff_t #include // Include exception header for XPath #if !defined(PUGIXML_NO_XPATH) && !defined(PUGIXML_NO_EXCEPTIONS) # include #endif // Include STL headers #ifndef PUGIXML_NO_STL # include # include # include #endif // Macro for deprecated features #ifndef PUGIXML_DEPRECATED # if defined(__GNUC__) # define PUGIXML_DEPRECATED __attribute__((deprecated)) # elif defined(_MSC_VER) && _MSC_VER >= 1300 # define PUGIXML_DEPRECATED __declspec(deprecated) # else # define PUGIXML_DEPRECATED # endif #endif // If no API is defined, assume default #ifndef PUGIXML_API # define PUGIXML_API #endif // If no API for classes is defined, assume default #ifndef PUGIXML_CLASS # define PUGIXML_CLASS PUGIXML_API #endif // If no API for functions is defined, assume default #ifndef PUGIXML_FUNCTION # define PUGIXML_FUNCTION PUGIXML_API #endif // If the platform is known to have long long support, enable long long functions #ifndef PUGIXML_HAS_LONG_LONG # if __cplusplus >= 201103 # define PUGIXML_HAS_LONG_LONG # elif defined(_MSC_VER) && _MSC_VER >= 1400 # define PUGIXML_HAS_LONG_LONG # endif #endif // If the platform is known to have move semantics support, compile move ctor/operator implementation #ifndef PUGIXML_HAS_MOVE # if __cplusplus >= 201103 # define PUGIXML_HAS_MOVE # elif defined(_MSC_VER) && _MSC_VER >= 1600 # define PUGIXML_HAS_MOVE # endif #endif // If C++ is 2011 or higher, add 'noexcept' specifiers #ifndef PUGIXML_NOEXCEPT # if __cplusplus >= 201103 # define PUGIXML_NOEXCEPT noexcept # elif defined(_MSC_VER) && _MSC_VER >= 1900 # define PUGIXML_NOEXCEPT noexcept # else # define PUGIXML_NOEXCEPT # endif #endif // Some functions can not be noexcept in compact mode #ifdef PUGIXML_COMPACT # define PUGIXML_NOEXCEPT_IF_NOT_COMPACT #else # define PUGIXML_NOEXCEPT_IF_NOT_COMPACT PUGIXML_NOEXCEPT #endif // If C++ is 2011 or higher, add 'override' qualifiers #ifndef PUGIXML_OVERRIDE # if __cplusplus >= 201103 # define PUGIXML_OVERRIDE override # elif defined(_MSC_VER) && _MSC_VER >= 1700 # define PUGIXML_OVERRIDE override # else # define PUGIXML_OVERRIDE # endif #endif // If C++ is 2011 or higher, use 'nullptr' #ifndef PUGIXML_NULL # if __cplusplus >= 201103 # define PUGIXML_NULL nullptr # elif defined(_MSC_VER) && _MSC_VER >= 1600 # define PUGIXML_NULL nullptr # else # define PUGIXML_NULL 0 # endif #endif // Character interface macros #ifdef PUGIXML_WCHAR_MODE # define PUGIXML_TEXT(t) L ## t # define PUGIXML_CHAR wchar_t #else # define PUGIXML_TEXT(t) t # define PUGIXML_CHAR char #endif namespace pugi { // Character type used for all internal storage and operations; depends on PUGIXML_WCHAR_MODE typedef PUGIXML_CHAR char_t; #ifndef PUGIXML_NO_STL // String type used for operations that work with STL string; depends on PUGIXML_WCHAR_MODE typedef std::basic_string, std::allocator > string_t; #endif } // The PugiXML namespace namespace pugi { // Tree node types enum xml_node_type { node_null, // Empty (null) node handle node_document, // A document tree's absolute root node_element, // Element tag, i.e. '' node_pcdata, // Plain character data, i.e. 'text' node_cdata, // Character data, i.e. '' node_comment, // Comment tag, i.e. '' node_pi, // Processing instruction, i.e. '' node_declaration, // Document declaration, i.e. '' node_doctype // Document type declaration, i.e. '' }; // Parsing options // Minimal parsing mode (equivalent to turning all other flags off). // Only elements and PCDATA sections are added to the DOM tree, no text conversions are performed. const unsigned int parse_minimal = 0x0000; // This flag determines if processing instructions (node_pi) are added to the DOM tree. This flag is off by default. const unsigned int parse_pi = 0x0001; // This flag determines if comments (node_comment) are added to the DOM tree. This flag is off by default. const unsigned int parse_comments = 0x0002; // This flag determines if CDATA sections (node_cdata) are added to the DOM tree. This flag is on by default. const unsigned int parse_cdata = 0x0004; // This flag determines if plain character data (node_pcdata) that consist only of whitespace are added to the DOM tree. // This flag is off by default; turning it on usually results in slower parsing and more memory consumption. const unsigned int parse_ws_pcdata = 0x0008; // This flag determines if character and entity references are expanded during parsing. This flag is on by default. const unsigned int parse_escapes = 0x0010; // This flag determines if EOL characters are normalized (converted to #xA) during parsing. This flag is on by default. const unsigned int parse_eol = 0x0020; // This flag determines if attribute values are normalized using CDATA normalization rules during parsing. This flag is on by default. const unsigned int parse_wconv_attribute = 0x0040; // This flag determines if attribute values are normalized using NMTOKENS normalization rules during parsing. This flag is off by default. const unsigned int parse_wnorm_attribute = 0x0080; // This flag determines if document declaration (node_declaration) is added to the DOM tree. This flag is off by default. const unsigned int parse_declaration = 0x0100; // This flag determines if document type declaration (node_doctype) is added to the DOM tree. This flag is off by default. const unsigned int parse_doctype = 0x0200; // This flag determines if plain character data (node_pcdata) that is the only child of the parent node and that consists only // of whitespace is added to the DOM tree. // This flag is off by default; turning it on may result in slower parsing and more memory consumption. const unsigned int parse_ws_pcdata_single = 0x0400; // This flag determines if leading and trailing whitespace is to be removed from plain character data. This flag is off by default. const unsigned int parse_trim_pcdata = 0x0800; // This flag determines if plain character data that does not have a parent node is added to the DOM tree, and if an empty document // is a valid document. This flag is off by default. const unsigned int parse_fragment = 0x1000; // This flag determines if plain character data is be stored in the parent element's value. This significantly changes the structure of // the document; this flag is only recommended for parsing documents with many PCDATA nodes in memory-constrained environments. // This flag is off by default. const unsigned int parse_embed_pcdata = 0x2000; // This flag determines whether determines whether the the two pcdata should be merged or not, if no intermediatory data are parsed in the document. // This flag is off by default. const unsigned int parse_merge_pcdata = 0x4000; // The default parsing mode. // Elements, PCDATA and CDATA sections are added to the DOM tree, character/reference entities are expanded, // End-of-Line characters are normalized, attribute values are normalized using CDATA normalization rules. const unsigned int parse_default = parse_cdata | parse_escapes | parse_wconv_attribute | parse_eol; // The full parsing mode. // Nodes of all types are added to the DOM tree, character/reference entities are expanded, // End-of-Line characters are normalized, attribute values are normalized using CDATA normalization rules. const unsigned int parse_full = parse_default | parse_pi | parse_comments | parse_declaration | parse_doctype; // These flags determine the encoding of input data for XML document enum xml_encoding { encoding_auto, // Auto-detect input encoding using BOM or < / class xml_object_range { public: typedef It const_iterator; typedef It iterator; xml_object_range(It b, It e): _begin(b), _end(e) { } It begin() const { return _begin; } It end() const { return _end; } bool empty() const { return _begin == _end; } private: It _begin, _end; }; // Writer interface for node printing (see xml_node::print) class PUGIXML_CLASS xml_writer { public: virtual ~xml_writer(); // Write memory chunk into stream/file/whatever virtual void write(const void* data, size_t size) = 0; }; // xml_writer implementation for FILE* class PUGIXML_CLASS xml_writer_file: public xml_writer { public: // Construct writer from a FILE* object; void* is used to avoid header dependencies on stdio xml_writer_file(void* file); virtual void write(const void* data, size_t size) PUGIXML_OVERRIDE; private: void* file; }; #ifndef PUGIXML_NO_STL // xml_writer implementation for streams class PUGIXML_CLASS xml_writer_stream: public xml_writer { public: // Construct writer from an output stream object xml_writer_stream(std::basic_ostream >& stream); xml_writer_stream(std::basic_ostream >& stream); virtual void write(const void* data, size_t size) PUGIXML_OVERRIDE; private: std::basic_ostream >* narrow_stream; std::basic_ostream >* wide_stream; }; #endif // A light-weight handle for manipulating attributes in DOM tree class PUGIXML_CLASS xml_attribute { friend class xml_attribute_iterator; friend class xml_node; private: xml_attribute_struct* _attr; typedef void (*unspecified_bool_type)(xml_attribute***); public: // Default constructor. Constructs an empty attribute. xml_attribute(); // Constructs attribute from internal pointer explicit xml_attribute(xml_attribute_struct* attr); // Safe bool conversion operator operator unspecified_bool_type() const; // Borland C++ workaround bool operator!() const; // Comparison operators (compares wrapped attribute pointers) bool operator==(const xml_attribute& r) const; bool operator!=(const xml_attribute& r) const; bool operator<(const xml_attribute& r) const; bool operator>(const xml_attribute& r) const; bool operator<=(const xml_attribute& r) const; bool operator>=(const xml_attribute& r) const; // Check if attribute is empty bool empty() const; // Get attribute name/value, or "" if attribute is empty const char_t* name() const; const char_t* value() const; // Get attribute value, or the default value if attribute is empty const char_t* as_string(const char_t* def = PUGIXML_TEXT("")) const; // Get attribute value as a number, or the default value if conversion did not succeed or attribute is empty int as_int(int def = 0) const; unsigned int as_uint(unsigned int def = 0) const; double as_double(double def = 0) const; float as_float(float def = 0) const; #ifdef PUGIXML_HAS_LONG_LONG long long as_llong(long long def = 0) const; unsigned long long as_ullong(unsigned long long def = 0) const; #endif // Get attribute value as bool (returns true if first character is in '1tTyY' set), or the default value if attribute is empty bool as_bool(bool def = false) const; // Set attribute name/value (returns false if attribute is empty or there is not enough memory) bool set_name(const char_t* rhs); bool set_name(const char_t* rhs, size_t size); bool set_value(const char_t* rhs); bool set_value(const char_t* rhs, size_t size); // Set attribute value with type conversion (numbers are converted to strings, boolean is converted to "true"/"false") bool set_value(int rhs); bool set_value(unsigned int rhs); bool set_value(long rhs); bool set_value(unsigned long rhs); bool set_value(double rhs); bool set_value(double rhs, int precision); bool set_value(float rhs); bool set_value(float rhs, int precision); bool set_value(bool rhs); #ifdef PUGIXML_HAS_LONG_LONG bool set_value(long long rhs); bool set_value(unsigned long long rhs); #endif // Set attribute value (equivalent to set_value without error checking) xml_attribute& operator=(const char_t* rhs); xml_attribute& operator=(int rhs); xml_attribute& operator=(unsigned int rhs); xml_attribute& operator=(long rhs); xml_attribute& operator=(unsigned long rhs); xml_attribute& operator=(double rhs); xml_attribute& operator=(float rhs); xml_attribute& operator=(bool rhs); #ifdef PUGIXML_HAS_LONG_LONG xml_attribute& operator=(long long rhs); xml_attribute& operator=(unsigned long long rhs); #endif // Get next/previous attribute in the attribute list of the parent node xml_attribute next_attribute() const; xml_attribute previous_attribute() const; // Get hash value (unique for handles to the same object) size_t hash_value() const; // Get internal pointer xml_attribute_struct* internal_object() const; }; #ifdef __BORLANDC__ // Borland C++ workaround bool PUGIXML_FUNCTION operator&&(const xml_attribute& lhs, bool rhs); bool PUGIXML_FUNCTION operator||(const xml_attribute& lhs, bool rhs); #endif // A light-weight handle for manipulating nodes in DOM tree class PUGIXML_CLASS xml_node { friend class xml_attribute_iterator; friend class xml_node_iterator; friend class xml_named_node_iterator; protected: xml_node_struct* _root; typedef void (*unspecified_bool_type)(xml_node***); public: // Default constructor. Constructs an empty node. xml_node(); // Constructs node from internal pointer explicit xml_node(xml_node_struct* p); // Safe bool conversion operator operator unspecified_bool_type() const; // Borland C++ workaround bool operator!() const; // Comparison operators (compares wrapped node pointers) bool operator==(const xml_node& r) const; bool operator!=(const xml_node& r) const; bool operator<(const xml_node& r) const; bool operator>(const xml_node& r) const; bool operator<=(const xml_node& r) const; bool operator>=(const xml_node& r) const; // Check if node is empty. bool empty() const; // Get node type xml_node_type type() const; // Get node name, or "" if node is empty or it has no name const char_t* name() const; // Get node value, or "" if node is empty or it has no value // Note: For text node.value() does not return "text"! Use child_value() or text() methods to access text inside nodes. const char_t* value() const; // Get attribute list xml_attribute first_attribute() const; xml_attribute last_attribute() const; // Get children list xml_node first_child() const; xml_node last_child() const; // Get next/previous sibling in the children list of the parent node xml_node next_sibling() const; xml_node previous_sibling() const; // Get parent node xml_node parent() const; // Get root of DOM tree this node belongs to xml_node root() const; // Get text object for the current node xml_text text() const; // Get child, attribute or next/previous sibling with the specified name xml_node child(const char_t* name) const; xml_attribute attribute(const char_t* name) const; xml_node next_sibling(const char_t* name) const; xml_node previous_sibling(const char_t* name) const; // Get attribute, starting the search from a hint (and updating hint so that searching for a sequence of attributes is fast) xml_attribute attribute(const char_t* name, xml_attribute& hint) const; // Get child value of current node; that is, value of the first child node of type PCDATA/CDATA const char_t* child_value() const; // Get child value of child with specified name. Equivalent to child(name).child_value(). const char_t* child_value(const char_t* name) const; // Set node name/value (returns false if node is empty, there is not enough memory, or node can not have name/value) bool set_name(const char_t* rhs); bool set_name(const char_t* rhs, size_t size); bool set_value(const char_t* rhs); bool set_value(const char_t* rhs, size_t size); // Add attribute with specified name. Returns added attribute, or empty attribute on errors. xml_attribute append_attribute(const char_t* name); xml_attribute prepend_attribute(const char_t* name); xml_attribute insert_attribute_after(const char_t* name, const xml_attribute& attr); xml_attribute insert_attribute_before(const char_t* name, const xml_attribute& attr); // Add a copy of the specified attribute. Returns added attribute, or empty attribute on errors. xml_attribute append_copy(const xml_attribute& proto); xml_attribute prepend_copy(const xml_attribute& proto); xml_attribute insert_copy_after(const xml_attribute& proto, const xml_attribute& attr); xml_attribute insert_copy_before(const xml_attribute& proto, const xml_attribute& attr); // Add child node with specified type. Returns added node, or empty node on errors. xml_node append_child(xml_node_type type = node_element); xml_node prepend_child(xml_node_type type = node_element); xml_node insert_child_after(xml_node_type type, const xml_node& node); xml_node insert_child_before(xml_node_type type, const xml_node& node); // Add child element with specified name. Returns added node, or empty node on errors. xml_node append_child(const char_t* name); xml_node prepend_child(const char_t* name); xml_node insert_child_after(const char_t* name, const xml_node& node); xml_node insert_child_before(const char_t* name, const xml_node& node); // Add a copy of the specified node as a child. Returns added node, or empty node on errors. xml_node append_copy(const xml_node& proto); xml_node prepend_copy(const xml_node& proto); xml_node insert_copy_after(const xml_node& proto, const xml_node& node); xml_node insert_copy_before(const xml_node& proto, const xml_node& node); // Move the specified node to become a child of this node. Returns moved node, or empty node on errors. xml_node append_move(const xml_node& moved); xml_node prepend_move(const xml_node& moved); xml_node insert_move_after(const xml_node& moved, const xml_node& node); xml_node insert_move_before(const xml_node& moved, const xml_node& node); // Remove specified attribute bool remove_attribute(const xml_attribute& a); bool remove_attribute(const char_t* name); // Remove all attributes bool remove_attributes(); // Remove specified child bool remove_child(const xml_node& n); bool remove_child(const char_t* name); // Remove all children bool remove_children(); // Parses buffer as an XML document fragment and appends all nodes as children of the current node. // Copies/converts the buffer, so it may be deleted or changed after the function returns. // Note: append_buffer allocates memory that has the lifetime of the owning document; removing the appended nodes does not immediately reclaim that memory. xml_parse_result append_buffer(const void* contents, size_t size, unsigned int options = parse_default, xml_encoding encoding = encoding_auto); // Find attribute using predicate. Returns first attribute for which predicate returned true. template xml_attribute find_attribute(Predicate pred) const { if (!_root) return xml_attribute(); for (xml_attribute attrib = first_attribute(); attrib; attrib = attrib.next_attribute()) if (pred(attrib)) return attrib; return xml_attribute(); } // Find child node using predicate. Returns first child for which predicate returned true. template xml_node find_child(Predicate pred) const { if (!_root) return xml_node(); for (xml_node node = first_child(); node; node = node.next_sibling()) if (pred(node)) return node; return xml_node(); } // Find node from subtree using predicate. Returns first node from subtree (depth-first), for which predicate returned true. template xml_node find_node(Predicate pred) const { if (!_root) return xml_node(); xml_node cur = first_child(); while (cur._root && cur._root != _root) { if (pred(cur)) return cur; if (cur.first_child()) cur = cur.first_child(); else if (cur.next_sibling()) cur = cur.next_sibling(); else { while (!cur.next_sibling() && cur._root != _root) cur = cur.parent(); if (cur._root != _root) cur = cur.next_sibling(); } } return xml_node(); } // Find child node by attribute name/value xml_node find_child_by_attribute(const char_t* name, const char_t* attr_name, const char_t* attr_value) const; xml_node find_child_by_attribute(const char_t* attr_name, const char_t* attr_value) const; #ifndef PUGIXML_NO_STL // Get the absolute node path from root as a text string. string_t path(char_t delimiter = '/') const; #endif // Search for a node by path consisting of node names and . or .. elements. xml_node first_element_by_path(const char_t* path, char_t delimiter = '/') const; // Recursively traverse subtree with xml_tree_walker bool traverse(xml_tree_walker& walker); #ifndef PUGIXML_NO_XPATH // Select single node by evaluating XPath query. Returns first node from the resulting node set. xpath_node select_node(const char_t* query, xpath_variable_set* variables = PUGIXML_NULL) const; xpath_node select_node(const xpath_query& query) const; // Select node set by evaluating XPath query xpath_node_set select_nodes(const char_t* query, xpath_variable_set* variables = PUGIXML_NULL) const; xpath_node_set select_nodes(const xpath_query& query) const; // (deprecated: use select_node instead) Select single node by evaluating XPath query. PUGIXML_DEPRECATED xpath_node select_single_node(const char_t* query, xpath_variable_set* variables = PUGIXML_NULL) const; PUGIXML_DEPRECATED xpath_node select_single_node(const xpath_query& query) const; #endif // Print subtree using a writer object void print(xml_writer& writer, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default, xml_encoding encoding = encoding_auto, unsigned int depth = 0) const; #ifndef PUGIXML_NO_STL // Print subtree to stream void print(std::basic_ostream >& os, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default, xml_encoding encoding = encoding_auto, unsigned int depth = 0) const; void print(std::basic_ostream >& os, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default, unsigned int depth = 0) const; #endif // Child nodes iterators typedef xml_node_iterator iterator; iterator begin() const; iterator end() const; // Attribute iterators typedef xml_attribute_iterator attribute_iterator; attribute_iterator attributes_begin() const; attribute_iterator attributes_end() const; // Range-based for support xml_object_range children() const; xml_object_range attributes() const; // Range-based for support for all children with the specified name // Note: name pointer must have a longer lifetime than the returned object; be careful with passing temporaries! xml_object_range children(const char_t* name) const; // Get node offset in parsed file/string (in char_t units) for debugging purposes ptrdiff_t offset_debug() const; // Get hash value (unique for handles to the same object) size_t hash_value() const; // Get internal pointer xml_node_struct* internal_object() const; }; #ifdef __BORLANDC__ // Borland C++ workaround bool PUGIXML_FUNCTION operator&&(const xml_node& lhs, bool rhs); bool PUGIXML_FUNCTION operator||(const xml_node& lhs, bool rhs); #endif // A helper for working with text inside PCDATA nodes class PUGIXML_CLASS xml_text { friend class xml_node; xml_node_struct* _root; typedef void (*unspecified_bool_type)(xml_text***); explicit xml_text(xml_node_struct* root); xml_node_struct* _data_new(); xml_node_struct* _data() const; public: // Default constructor. Constructs an empty object. xml_text(); // Safe bool conversion operator operator unspecified_bool_type() const; // Borland C++ workaround bool operator!() const; // Check if text object is empty bool empty() const; // Get text, or "" if object is empty const char_t* get() const; // Get text, or the default value if object is empty const char_t* as_string(const char_t* def = PUGIXML_TEXT("")) const; // Get text as a number, or the default value if conversion did not succeed or object is empty int as_int(int def = 0) const; unsigned int as_uint(unsigned int def = 0) const; double as_double(double def = 0) const; float as_float(float def = 0) const; #ifdef PUGIXML_HAS_LONG_LONG long long as_llong(long long def = 0) const; unsigned long long as_ullong(unsigned long long def = 0) const; #endif // Get text as bool (returns true if first character is in '1tTyY' set), or the default value if object is empty bool as_bool(bool def = false) const; // Set text (returns false if object is empty or there is not enough memory) bool set(const char_t* rhs); bool set(const char_t* rhs, size_t size); // Set text with type conversion (numbers are converted to strings, boolean is converted to "true"/"false") bool set(int rhs); bool set(unsigned int rhs); bool set(long rhs); bool set(unsigned long rhs); bool set(double rhs); bool set(double rhs, int precision); bool set(float rhs); bool set(float rhs, int precision); bool set(bool rhs); #ifdef PUGIXML_HAS_LONG_LONG bool set(long long rhs); bool set(unsigned long long rhs); #endif // Set text (equivalent to set without error checking) xml_text& operator=(const char_t* rhs); xml_text& operator=(int rhs); xml_text& operator=(unsigned int rhs); xml_text& operator=(long rhs); xml_text& operator=(unsigned long rhs); xml_text& operator=(double rhs); xml_text& operator=(float rhs); xml_text& operator=(bool rhs); #ifdef PUGIXML_HAS_LONG_LONG xml_text& operator=(long long rhs); xml_text& operator=(unsigned long long rhs); #endif // Get the data node (node_pcdata or node_cdata) for this object xml_node data() const; }; #ifdef __BORLANDC__ // Borland C++ workaround bool PUGIXML_FUNCTION operator&&(const xml_text& lhs, bool rhs); bool PUGIXML_FUNCTION operator||(const xml_text& lhs, bool rhs); #endif // Child node iterator (a bidirectional iterator over a collection of xml_node) class PUGIXML_CLASS xml_node_iterator { friend class xml_node; private: mutable xml_node _wrap; xml_node _parent; xml_node_iterator(xml_node_struct* ref, xml_node_struct* parent); public: // Iterator traits typedef ptrdiff_t difference_type; typedef xml_node value_type; typedef xml_node* pointer; typedef xml_node& reference; #ifndef PUGIXML_NO_STL typedef std::bidirectional_iterator_tag iterator_category; #endif // Default constructor xml_node_iterator(); // Construct an iterator which points to the specified node xml_node_iterator(const xml_node& node); // Iterator operators bool operator==(const xml_node_iterator& rhs) const; bool operator!=(const xml_node_iterator& rhs) const; xml_node& operator*() const; xml_node* operator->() const; xml_node_iterator& operator++(); xml_node_iterator operator++(int); xml_node_iterator& operator--(); xml_node_iterator operator--(int); }; // Attribute iterator (a bidirectional iterator over a collection of xml_attribute) class PUGIXML_CLASS xml_attribute_iterator { friend class xml_node; private: mutable xml_attribute _wrap; xml_node _parent; xml_attribute_iterator(xml_attribute_struct* ref, xml_node_struct* parent); public: // Iterator traits typedef ptrdiff_t difference_type; typedef xml_attribute value_type; typedef xml_attribute* pointer; typedef xml_attribute& reference; #ifndef PUGIXML_NO_STL typedef std::bidirectional_iterator_tag iterator_category; #endif // Default constructor xml_attribute_iterator(); // Construct an iterator which points to the specified attribute xml_attribute_iterator(const xml_attribute& attr, const xml_node& parent); // Iterator operators bool operator==(const xml_attribute_iterator& rhs) const; bool operator!=(const xml_attribute_iterator& rhs) const; xml_attribute& operator*() const; xml_attribute* operator->() const; xml_attribute_iterator& operator++(); xml_attribute_iterator operator++(int); xml_attribute_iterator& operator--(); xml_attribute_iterator operator--(int); }; // Named node range helper class PUGIXML_CLASS xml_named_node_iterator { friend class xml_node; public: // Iterator traits typedef ptrdiff_t difference_type; typedef xml_node value_type; typedef xml_node* pointer; typedef xml_node& reference; #ifndef PUGIXML_NO_STL typedef std::bidirectional_iterator_tag iterator_category; #endif // Default constructor xml_named_node_iterator(); // Construct an iterator which points to the specified node // Note: name pointer is stored in the iterator and must have a longer lifetime than iterator itself xml_named_node_iterator(const xml_node& node, const char_t* name); // Iterator operators bool operator==(const xml_named_node_iterator& rhs) const; bool operator!=(const xml_named_node_iterator& rhs) const; xml_node& operator*() const; xml_node* operator->() const; xml_named_node_iterator& operator++(); xml_named_node_iterator operator++(int); xml_named_node_iterator& operator--(); xml_named_node_iterator operator--(int); private: mutable xml_node _wrap; xml_node _parent; const char_t* _name; xml_named_node_iterator(xml_node_struct* ref, xml_node_struct* parent, const char_t* name); }; // Abstract tree walker class (see xml_node::traverse) class PUGIXML_CLASS xml_tree_walker { friend class xml_node; private: int _depth; protected: // Get current traversal depth int depth() const; public: xml_tree_walker(); virtual ~xml_tree_walker(); // Callback that is called when traversal begins virtual bool begin(xml_node& node); // Callback that is called for each node traversed virtual bool for_each(xml_node& node) = 0; // Callback that is called when traversal ends virtual bool end(xml_node& node); }; // Parsing status, returned as part of xml_parse_result object enum xml_parse_status { status_ok = 0, // No error status_file_not_found, // File was not found during load_file() status_io_error, // Error reading from file/stream status_out_of_memory, // Could not allocate memory status_internal_error, // Internal error occurred status_unrecognized_tag, // Parser could not determine tag type status_bad_pi, // Parsing error occurred while parsing document declaration/processing instruction status_bad_comment, // Parsing error occurred while parsing comment status_bad_cdata, // Parsing error occurred while parsing CDATA section status_bad_doctype, // Parsing error occurred while parsing document type declaration status_bad_pcdata, // Parsing error occurred while parsing PCDATA section status_bad_start_element, // Parsing error occurred while parsing start element tag status_bad_attribute, // Parsing error occurred while parsing element attribute status_bad_end_element, // Parsing error occurred while parsing end element tag status_end_element_mismatch,// There was a mismatch of start-end tags (closing tag had incorrect name, some tag was not closed or there was an excessive closing tag) status_append_invalid_root, // Unable to append nodes since root type is not node_element or node_document (exclusive to xml_node::append_buffer) status_no_document_element // Parsing resulted in a document without element nodes }; // Parsing result struct PUGIXML_CLASS xml_parse_result { // Parsing status (see xml_parse_status) xml_parse_status status; // Last parsed offset (in char_t units from start of input data) ptrdiff_t offset; // Source document encoding xml_encoding encoding; // Default constructor, initializes object to failed state xml_parse_result(); // Cast to bool operator operator bool() const; // Get error description const char* description() const; }; // Document class (DOM tree root) class PUGIXML_CLASS xml_document: public xml_node { private: char_t* _buffer; char _memory[192]; // Non-copyable semantics xml_document(const xml_document&); xml_document& operator=(const xml_document&); void _create(); void _destroy(); void _move(xml_document& rhs) PUGIXML_NOEXCEPT_IF_NOT_COMPACT; public: // Default constructor, makes empty document xml_document(); // Destructor, invalidates all node/attribute handles to this document ~xml_document(); #ifdef PUGIXML_HAS_MOVE // Move semantics support xml_document(xml_document&& rhs) PUGIXML_NOEXCEPT_IF_NOT_COMPACT; xml_document& operator=(xml_document&& rhs) PUGIXML_NOEXCEPT_IF_NOT_COMPACT; #endif // Removes all nodes, leaving the empty document void reset(); // Removes all nodes, then copies the entire contents of the specified document void reset(const xml_document& proto); #ifndef PUGIXML_NO_STL // Load document from stream. xml_parse_result load(std::basic_istream >& stream, unsigned int options = parse_default, xml_encoding encoding = encoding_auto); xml_parse_result load(std::basic_istream >& stream, unsigned int options = parse_default); #endif // (deprecated: use load_string instead) Load document from zero-terminated string. No encoding conversions are applied. PUGIXML_DEPRECATED xml_parse_result load(const char_t* contents, unsigned int options = parse_default); // Load document from zero-terminated string. No encoding conversions are applied. xml_parse_result load_string(const char_t* contents, unsigned int options = parse_default); // Load document from file xml_parse_result load_file(const char* path, unsigned int options = parse_default, xml_encoding encoding = encoding_auto); xml_parse_result load_file(const wchar_t* path, unsigned int options = parse_default, xml_encoding encoding = encoding_auto); // Load document from buffer. Copies/converts the buffer, so it may be deleted or changed after the function returns. xml_parse_result load_buffer(const void* contents, size_t size, unsigned int options = parse_default, xml_encoding encoding = encoding_auto); // Load document from buffer, using the buffer for in-place parsing (the buffer is modified and used for storage of document data). // You should ensure that buffer data will persist throughout the document's lifetime, and free the buffer memory manually once document is destroyed. xml_parse_result load_buffer_inplace(void* contents, size_t size, unsigned int options = parse_default, xml_encoding encoding = encoding_auto); // Load document from buffer, using the buffer for in-place parsing (the buffer is modified and used for storage of document data). // You should allocate the buffer with pugixml allocation function; document will free the buffer when it is no longer needed (you can't use it anymore). xml_parse_result load_buffer_inplace_own(void* contents, size_t size, unsigned int options = parse_default, xml_encoding encoding = encoding_auto); // Save XML document to writer (semantics is slightly different from xml_node::print, see documentation for details). void save(xml_writer& writer, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default, xml_encoding encoding = encoding_auto) const; #ifndef PUGIXML_NO_STL // Save XML document to stream (semantics is slightly different from xml_node::print, see documentation for details). void save(std::basic_ostream >& stream, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default, xml_encoding encoding = encoding_auto) const; void save(std::basic_ostream >& stream, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default) const; #endif // Save XML to file bool save_file(const char* path, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default, xml_encoding encoding = encoding_auto) const; bool save_file(const wchar_t* path, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default, xml_encoding encoding = encoding_auto) const; // Get document element xml_node document_element() const; }; #ifndef PUGIXML_NO_XPATH // XPath query return type enum xpath_value_type { xpath_type_none, // Unknown type (query failed to compile) xpath_type_node_set, // Node set (xpath_node_set) xpath_type_number, // Number xpath_type_string, // String xpath_type_boolean // Boolean }; // XPath parsing result struct PUGIXML_CLASS xpath_parse_result { // Error message (0 if no error) const char* error; // Last parsed offset (in char_t units from string start) ptrdiff_t offset; // Default constructor, initializes object to failed state xpath_parse_result(); // Cast to bool operator operator bool() const; // Get error description const char* description() const; }; // A single XPath variable class PUGIXML_CLASS xpath_variable { friend class xpath_variable_set; protected: xpath_value_type _type; xpath_variable* _next; xpath_variable(xpath_value_type type); // Non-copyable semantics xpath_variable(const xpath_variable&); xpath_variable& operator=(const xpath_variable&); public: // Get variable name const char_t* name() const; // Get variable type xpath_value_type type() const; // Get variable value; no type conversion is performed, default value (false, NaN, empty string, empty node set) is returned on type mismatch error bool get_boolean() const; double get_number() const; const char_t* get_string() const; const xpath_node_set& get_node_set() const; // Set variable value; no type conversion is performed, false is returned on type mismatch error bool set(bool value); bool set(double value); bool set(const char_t* value); bool set(const xpath_node_set& value); }; // A set of XPath variables class PUGIXML_CLASS xpath_variable_set { private: xpath_variable* _data[64]; void _assign(const xpath_variable_set& rhs); void _swap(xpath_variable_set& rhs); xpath_variable* _find(const char_t* name) const; static bool _clone(xpath_variable* var, xpath_variable** out_result); static void _destroy(xpath_variable* var); public: // Default constructor/destructor xpath_variable_set(); ~xpath_variable_set(); // Copy constructor/assignment operator xpath_variable_set(const xpath_variable_set& rhs); xpath_variable_set& operator=(const xpath_variable_set& rhs); #ifdef PUGIXML_HAS_MOVE // Move semantics support xpath_variable_set(xpath_variable_set&& rhs) PUGIXML_NOEXCEPT; xpath_variable_set& operator=(xpath_variable_set&& rhs) PUGIXML_NOEXCEPT; #endif // Add a new variable or get the existing one, if the types match xpath_variable* add(const char_t* name, xpath_value_type type); // Set value of an existing variable; no type conversion is performed, false is returned if there is no such variable or if types mismatch bool set(const char_t* name, bool value); bool set(const char_t* name, double value); bool set(const char_t* name, const char_t* value); bool set(const char_t* name, const xpath_node_set& value); // Get existing variable by name xpath_variable* get(const char_t* name); const xpath_variable* get(const char_t* name) const; }; // A compiled XPath query object class PUGIXML_CLASS xpath_query { private: void* _impl; xpath_parse_result _result; typedef void (*unspecified_bool_type)(xpath_query***); // Non-copyable semantics xpath_query(const xpath_query&); xpath_query& operator=(const xpath_query&); public: // Construct a compiled object from XPath expression. // If PUGIXML_NO_EXCEPTIONS is not defined, throws xpath_exception on compilation errors. explicit xpath_query(const char_t* query, xpath_variable_set* variables = PUGIXML_NULL); // Constructor xpath_query(); // Destructor ~xpath_query(); #ifdef PUGIXML_HAS_MOVE // Move semantics support xpath_query(xpath_query&& rhs) PUGIXML_NOEXCEPT; xpath_query& operator=(xpath_query&& rhs) PUGIXML_NOEXCEPT; #endif // Get query expression return type xpath_value_type return_type() const; // Evaluate expression as boolean value in the specified context; performs type conversion if necessary. // If PUGIXML_NO_EXCEPTIONS is not defined, throws std::bad_alloc on out of memory errors. bool evaluate_boolean(const xpath_node& n) const; // Evaluate expression as double value in the specified context; performs type conversion if necessary. // If PUGIXML_NO_EXCEPTIONS is not defined, throws std::bad_alloc on out of memory errors. double evaluate_number(const xpath_node& n) const; #ifndef PUGIXML_NO_STL // Evaluate expression as string value in the specified context; performs type conversion if necessary. // If PUGIXML_NO_EXCEPTIONS is not defined, throws std::bad_alloc on out of memory errors. string_t evaluate_string(const xpath_node& n) const; #endif // Evaluate expression as string value in the specified context; performs type conversion if necessary. // At most capacity characters are written to the destination buffer, full result size is returned (includes terminating zero). // If PUGIXML_NO_EXCEPTIONS is not defined, throws std::bad_alloc on out of memory errors. // If PUGIXML_NO_EXCEPTIONS is defined, returns empty set instead. size_t evaluate_string(char_t* buffer, size_t capacity, const xpath_node& n) const; // Evaluate expression as node set in the specified context. // If PUGIXML_NO_EXCEPTIONS is not defined, throws xpath_exception on type mismatch and std::bad_alloc on out of memory errors. // If PUGIXML_NO_EXCEPTIONS is defined, returns empty node set instead. xpath_node_set evaluate_node_set(const xpath_node& n) const; // Evaluate expression as node set in the specified context. // Return first node in document order, or empty node if node set is empty. // If PUGIXML_NO_EXCEPTIONS is not defined, throws xpath_exception on type mismatch and std::bad_alloc on out of memory errors. // If PUGIXML_NO_EXCEPTIONS is defined, returns empty node instead. xpath_node evaluate_node(const xpath_node& n) const; // Get parsing result (used to get compilation errors in PUGIXML_NO_EXCEPTIONS mode) const xpath_parse_result& result() const; // Safe bool conversion operator operator unspecified_bool_type() const; // Borland C++ workaround bool operator!() const; }; #ifndef PUGIXML_NO_EXCEPTIONS #if defined(_MSC_VER) // C4275 can be ignored in Visual C++ if you are deriving // from a type in the Standard C++ Library #pragma warning(push) #pragma warning(disable: 4275) #endif // XPath exception class class PUGIXML_CLASS xpath_exception: public std::exception { private: xpath_parse_result _result; public: // Construct exception from parse result explicit xpath_exception(const xpath_parse_result& result); // Get error message virtual const char* what() const throw() PUGIXML_OVERRIDE; // Get parse result const xpath_parse_result& result() const; }; #if defined(_MSC_VER) #pragma warning(pop) #endif #endif // XPath node class (either xml_node or xml_attribute) class PUGIXML_CLASS xpath_node { private: xml_node _node; xml_attribute _attribute; typedef void (*unspecified_bool_type)(xpath_node***); public: // Default constructor; constructs empty XPath node xpath_node(); // Construct XPath node from XML node/attribute xpath_node(const xml_node& node); xpath_node(const xml_attribute& attribute, const xml_node& parent); // Get node/attribute, if any xml_node node() const; xml_attribute attribute() const; // Get parent of contained node/attribute xml_node parent() const; // Safe bool conversion operator operator unspecified_bool_type() const; // Borland C++ workaround bool operator!() const; // Comparison operators bool operator==(const xpath_node& n) const; bool operator!=(const xpath_node& n) const; }; #ifdef __BORLANDC__ // Borland C++ workaround bool PUGIXML_FUNCTION operator&&(const xpath_node& lhs, bool rhs); bool PUGIXML_FUNCTION operator||(const xpath_node& lhs, bool rhs); #endif // A fixed-size collection of XPath nodes class PUGIXML_CLASS xpath_node_set { public: // Collection type enum type_t { type_unsorted, // Not ordered type_sorted, // Sorted by document order (ascending) type_sorted_reverse // Sorted by document order (descending) }; // Constant iterator type typedef const xpath_node* const_iterator; // We define non-constant iterator to be the same as constant iterator so that various generic algorithms (i.e. boost foreach) work typedef const xpath_node* iterator; // Default constructor. Constructs empty set. xpath_node_set(); // Constructs a set from iterator range; data is not checked for duplicates and is not sorted according to provided type, so be careful xpath_node_set(const_iterator begin, const_iterator end, type_t type = type_unsorted); // Destructor ~xpath_node_set(); // Copy constructor/assignment operator xpath_node_set(const xpath_node_set& ns); xpath_node_set& operator=(const xpath_node_set& ns); #ifdef PUGIXML_HAS_MOVE // Move semantics support xpath_node_set(xpath_node_set&& rhs) PUGIXML_NOEXCEPT; xpath_node_set& operator=(xpath_node_set&& rhs) PUGIXML_NOEXCEPT; #endif // Get collection type type_t type() const; // Get collection size size_t size() const; // Indexing operator const xpath_node& operator[](size_t index) const; // Collection iterators const_iterator begin() const; const_iterator end() const; // Sort the collection in ascending/descending order by document order void sort(bool reverse = false); // Get first node in the collection by document order xpath_node first() const; // Check if collection is empty bool empty() const; private: type_t _type; xpath_node _storage[1]; xpath_node* _begin; xpath_node* _end; void _assign(const_iterator begin, const_iterator end, type_t type); void _move(xpath_node_set& rhs) PUGIXML_NOEXCEPT; }; #endif #ifndef PUGIXML_NO_STL // Convert wide string to UTF8 std::basic_string, std::allocator > PUGIXML_FUNCTION as_utf8(const wchar_t* str); std::basic_string, std::allocator > PUGIXML_FUNCTION as_utf8(const std::basic_string, std::allocator >& str); // Convert UTF8 to wide string std::basic_string, std::allocator > PUGIXML_FUNCTION as_wide(const char* str); std::basic_string, std::allocator > PUGIXML_FUNCTION as_wide(const std::basic_string, std::allocator >& str); #endif // Memory allocation function interface; returns pointer to allocated memory or NULL on failure typedef void* (*allocation_function)(size_t size); // Memory deallocation function interface typedef void (*deallocation_function)(void* ptr); // Override default memory management functions. All subsequent allocations/deallocations will be performed via supplied functions. void PUGIXML_FUNCTION set_memory_management_functions(allocation_function allocate, deallocation_function deallocate); // Get current memory management functions allocation_function PUGIXML_FUNCTION get_memory_allocation_function(); deallocation_function PUGIXML_FUNCTION get_memory_deallocation_function(); } #if !defined(PUGIXML_NO_STL) && (defined(_MSC_VER) || defined(__ICC)) namespace std { // Workarounds for (non-standard) iterator category detection for older versions (MSVC7/IC8 and earlier) std::bidirectional_iterator_tag PUGIXML_FUNCTION _Iter_cat(const pugi::xml_node_iterator&); std::bidirectional_iterator_tag PUGIXML_FUNCTION _Iter_cat(const pugi::xml_attribute_iterator&); std::bidirectional_iterator_tag PUGIXML_FUNCTION _Iter_cat(const pugi::xml_named_node_iterator&); } #endif #if !defined(PUGIXML_NO_STL) && defined(__SUNPRO_CC) namespace std { // Workarounds for (non-standard) iterator category detection std::bidirectional_iterator_tag PUGIXML_FUNCTION __iterator_category(const pugi::xml_node_iterator&); std::bidirectional_iterator_tag PUGIXML_FUNCTION __iterator_category(const pugi::xml_attribute_iterator&); std::bidirectional_iterator_tag PUGIXML_FUNCTION __iterator_category(const pugi::xml_named_node_iterator&); } #endif #endif // Make sure implementation is included in header-only mode // Use macro expansion in #include to work around QMake (QTBUG-11923) #if defined(PUGIXML_HEADER_ONLY) && !defined(PUGIXML_SOURCE) # define PUGIXML_SOURCE "pugixml.cpp" # include PUGIXML_SOURCE #endif /** * Copyright (c) 2006-2023 Arseny Kapoulkine * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ pr0m1th3as-datatypes-9450b29/src/tz.cpp000066400000000000000000004023401527370255000176310ustar00rootroot00000000000000// The MIT License (MIT) // // Copyright (c) 2015, 2016, 2017 Howard Hinnant // Copyright (c) 2015 Ville Voutilainen // Copyright (c) 2016 Alexander Kormanovsky // Copyright (c) 2016, 2017 Jiangang Zhuang // Copyright (c) 2017 Nicolas Veloz Savino // Copyright (c) 2017 Florian Dang // Copyright (c) 2017 Aaron Bishop // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // Our apologies. When the previous paragraph was written, lowercase had not yet // been invented (that would involve another several millennia of evolution). // We did not mean to shout. #ifdef _WIN32 // windows.h will be included directly and indirectly (e.g. by curl). // We need to define these macros to prevent windows.h bringing in // more than we need and do it early so windows.h doesn't get included // without these macros having been defined. // min/max macros interfere with the C++ versions. # ifndef NOMINMAX # define NOMINMAX # endif // We don't need all that Windows has to offer. # ifndef WIN32_LEAN_AND_MEAN # define WIN32_LEAN_AND_MEAN # endif // for wcstombs # ifndef _CRT_SECURE_NO_WARNINGS # define _CRT_SECURE_NO_WARNINGS # endif // None of this happens with the MS SDK (at least VS14 which I tested), but: // Compiling with mingw, we get "error: 'KF_FLAG_DEFAULT' was not declared in this scope." // and error: 'SHGetKnownFolderPath' was not declared in this scope.". // It seems when using mingw NTDDI_VERSION is undefined and that // causes KNOWN_FOLDER_FLAG and the KF_ flags to not get defined. // So we must define NTDDI_VERSION to get those flags on mingw. // The docs say though here: // https://msdn.microsoft.com/en-nz/library/windows/desktop/aa383745(v=vs.85).aspx // that "If you define NTDDI_VERSION, you must also define _WIN32_WINNT." // So we declare we require Vista or greater. # ifdef __MINGW32__ # ifndef NTDDI_VERSION # define NTDDI_VERSION 0x06000000 # define _WIN32_WINNT _WIN32_WINNT_VISTA # elif NTDDI_VERSION < 0x06000000 # warning "If this fails to compile NTDDI_VERSION may be to low. See comments above." # endif // But once we define the values above we then get this linker error: // "tz.cpp:(.rdata$.refptr.FOLDERID_Downloads[.refptr.FOLDERID_Downloads]+0x0): " // "undefined reference to `FOLDERID_Downloads'" // which #include cures see: // https://support.microsoft.com/en-us/kb/130869 # include // But with included, the error moves on to: // error: 'FOLDERID_Downloads' was not declared in this scope // Which #include cures. # include # endif // __MINGW32__ # include # if !defined(S_ISDIR) && defined(S_IFMT) && defined(_S_IFDIR) # define S_ISDIR(m) (((m) & S_IFMT) == _S_IFDIR) # endif #endif // _WIN32 #include "date/tz_private.h" #ifdef __APPLE__ # include "date/ios.h" #else # define TARGET_OS_IPHONE 0 # define TARGET_OS_SIMULATOR 0 #endif #if defined(ANDROID) || defined(__ANDROID__) # include # if USE_OS_TZDB # define MISSING_LEAP_SECONDS 1 // from https://android.googlesource.com/platform/bionic/+/master/libc/tzcode/bionic.cpp static constexpr size_t ANDROID_TIMEZONE_NAME_LENGTH = 40; struct bionic_tzdata_header_t { char tzdata_version[12]; std::int32_t index_offset; std::int32_t data_offset; std::int32_t final_offset; }; struct index_entry_t { char buf[ANDROID_TIMEZONE_NAME_LENGTH]; std::int32_t start; std::int32_t length; std::int32_t unused; // Was raw GMT offset; always 0 since tzdata2014f (L). }; # endif // USE_OS_TZDB #endif // defined(ANDROID) || defined(__ANDROID__) #if USE_OS_TZDB # include #endif #include #include #include #include #include #include #include #include #include #include #if USE_OS_TZDB # include #endif #include #include #include #include #include // unistd.h is used on some platforms as part of the the means to get // the current time zone. On Win32 windows.h provides a means to do it. // gcc/mingw supports unistd.h on Win32 but MSVC does not. #ifdef __ANDROID__ # define INSTALL . #endif #ifdef _WIN32 # ifdef WINAPI_FAMILY # include # if WINAPI_FAMILY != WINAPI_FAMILY_DESKTOP_APP # define WINRT # define INSTALL . # endif # endif # include // _unlink etc. # if defined(__clang__) struct IUnknown; // fix for issue with static_cast<> in objbase.h // (see https://github.com/philsquared/Catch/issues/690) # endif # include // CoTaskFree, ShGetKnownFolderPath etc. # if HAS_REMOTE_API # include // _mkdir # include // ShFileOperation etc. # endif // HAS_REMOTE_API #else // !_WIN32 # include # if !USE_OS_TZDB && !defined(INSTALL) # include # endif # include # include # if !USE_SHELL_API # include # include # include # include # include # include # endif //!USE_SHELL_API #endif // !_WIN32 #if HAS_REMOTE_API // Note curl includes windows.h so we must include curl AFTER definitions of things // that affect windows.h such as NOMINMAX. #if defined(_MSC_VER) && defined(SHORTENED_CURL_INCLUDE) // For rmt_curl nuget package # include #else # include #endif #endif #ifdef _WIN32 static CONSTDATA char folder_delimiter = '\\'; #elif !defined(ANDROID) && !defined(__ANDROID__) static CONSTDATA char folder_delimiter = '/'; #endif // !defined(WIN32) && !defined(ANDROID) && !defined(__ANDROID__) #if defined(__GNUC__) && __GNUC__ < 5 // GCC 4.9 Bug 61489 Wrong warning with -Wmissing-field-initializers # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wmissing-field-initializers" #endif // defined(__GNUC__) && __GNUC__ < 5 #if !USE_OS_TZDB # ifdef _WIN32 static std::wstring convert_utf8_to_utf16(const std::string& s) { std::wstring out; const int size = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, NULL, 0); if (size == 0) { std::string msg = "Failed to determine required size when converting \""; msg += s; msg += "\" to UTF-16."; throw std::runtime_error(msg); } out.resize(size); const int check = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, &out[0], size); if (size != check) { std::string msg = "Failed to convert \""; msg += s; msg += "\" to UTF-16."; throw std::runtime_error(msg); } return out; } # ifndef WINRT namespace { struct task_mem_deleter { void operator()(wchar_t buf[]) { if (buf != nullptr) CoTaskMemFree(buf); } }; using co_task_mem_ptr = std::unique_ptr; } // We might need to know certain locations even if not using the remote API, // so keep these routines out of that block for now. static std::string get_known_folder(const GUID& folderid) { std::string folder; PWSTR pfolder = nullptr; HRESULT hr = SHGetKnownFolderPath(folderid, KF_FLAG_DEFAULT, nullptr, &pfolder); if (SUCCEEDED(hr)) { co_task_mem_ptr folder_ptr(pfolder); const wchar_t* fptr = folder_ptr.get(); auto state = std::mbstate_t(); const auto required = std::wcsrtombs(nullptr, &fptr, 0, &state); if (required != 0 && required != std::size_t(-1)) { folder.resize(required); std::wcsrtombs(&folder[0], &fptr, folder.size(), &state); } } return folder; } # ifndef INSTALL // Usually something like "c:\Users\username\Downloads". static std::string get_download_folder() { return get_known_folder(FOLDERID_Downloads); } # endif // !INSTALL # endif // WINRT # else // !_WIN32 # if !defined(INSTALL) static std::string expand_path(std::string path) { # if TARGET_OS_IPHONE return date::iOSUtils::get_tzdata_path(); # else // !TARGET_OS_IPHONE ::wordexp_t w{}; std::unique_ptr<::wordexp_t, void(*)(::wordexp_t*)> hold{&w, ::wordfree}; ::wordexp(path.c_str(), &w, 0); if (w.we_wordc != 1) throw std::runtime_error("Cannot expand path: " + path); path = w.we_wordv[0]; return path; # endif // !TARGET_OS_IPHONE } static std::string get_download_folder() { return expand_path("~/Downloads"); } # endif // !defined(INSTALL) # endif // !_WIN32 /* * This class is provided to mimic the following usage of `ifstream`: * * std::ifstream is(filename); * * file_streambuf ibuf(filename); * std::istream is(&ibuf); * * This is required because `ifstream` does not support opening files * containing wide characters on Windows. On Windows, `file_streambuf` uses * `file_open()` to convert the file name to UTF-16 before opening it with * `_wfopen()`. * * Note that this is not an exact re-implementation of `ifstream`, * but is enough for usage here. * * It is partially based on these two implementations: * - fdinbuf from http://www.josuttis.com/cppcode/fdstream.html * - stdiobuf https://stackoverflow.com/questions/12342542/convert-file-to-ifstream-c-android-ndk * * Apparently MSVC provides non-standard overloads of `ifstream` that support * a `const wchar_t*` file name, but MinGW does not https://stackoverflow.com/a/822032 */ class file_streambuf : public std::streambuf { private: FILE* file_; static const int buffer_size_ = 1024; char buffer_[buffer_size_]; public: ~file_streambuf() { if (file_) { ::fclose(file_); } } file_streambuf(const file_streambuf&) = delete; file_streambuf& operator=(const file_streambuf&) = delete; file_streambuf(const std::string& filename) : file_(file_open(filename)) { } protected: virtual int_type underflow() { if (gptr() == egptr() && file_) { const size_t size = ::fread(buffer_, 1, buffer_size_, file_); setg(buffer_, buffer_, buffer_ + size); } return (gptr() == egptr()) ? traits_type::eof() : traits_type::to_int_type(*gptr()); } private: FILE* file_open(const std::string& filename) { # ifdef _WIN32 std::wstring wfilename = convert_utf8_to_utf16(filename); FILE* file = ::_wfopen(wfilename.c_str(), L"r"); # else // !_WIN32 FILE* file = ::fopen(filename.c_str(), "rb"); # endif // _WIN32 if (file == NULL) { std::string msg = "Error opening file \""; msg += filename; msg += "\"."; throw std::runtime_error(msg); } return file; } }; #endif // !USE_OS_TZDB namespace date { // +---------------------+ // | Begin Configuration | // +---------------------+ using namespace detail; #if !USE_OS_TZDB static std::string& access_install() { static std::string install #ifndef INSTALL = get_download_folder() + folder_delimiter + "tzdata"; #else // !INSTALL # define STRINGIZEIMP(x) #x # define STRINGIZE(x) STRINGIZEIMP(x) = STRINGIZE(INSTALL) + std::string(1, folder_delimiter) + "tzdata"; #undef STRINGIZEIMP #undef STRINGIZE #endif // !INSTALL { static char* tz_local_env = getenv("TZDATA"); if (tz_local_env != nullptr) { static std::string tz_local_env_s = tz_local_env; return tz_local_env_s; } } return install; } void set_install(const std::string& install) { access_install() = install; } static const std::string& get_install() { static const std::string& ref = access_install(); return ref; } #if HAS_REMOTE_API static std::string get_download_gz_file(const std::string& version) { auto file = get_install() + version + ".tar.gz"; return file; } #endif // HAS_REMOTE_API #endif // !USE_OS_TZDB // These can be used to reduce the range of the database to save memory CONSTDATA auto min_year = date::year::min(); CONSTDATA auto max_year = date::year::max(); CONSTDATA auto min_day = date::January/1; CONSTDATA auto max_day = date::December/31; #if USE_OS_TZDB CONSTCD14 const sys_seconds min_seconds = sys_days(min_year/min_day); #endif // USE_OS_TZDB #ifndef _WIN32 static std::string discover_tz_dir() { struct stat sb; using namespace std; # if defined(ANDROID) || defined(__ANDROID__) CONSTDATA auto tz_dir_default = "/apex/com.android.tzdata/etc/tz"; CONSTDATA auto tz_dir_fallback = "/system/usr/share/zoneinfo"; // Check updatable path first if(stat(tz_dir_default, &sb) == 0 && S_ISDIR(sb.st_mode)) return tz_dir_default; else if(stat(tz_dir_fallback, &sb) == 0 && S_ISDIR(sb.st_mode)) return tz_dir_fallback; else throw runtime_error("discover_tz_dir failed to find zoneinfo\n"); # elif !defined(__APPLE__) CONSTDATA auto tz_dir_default = "/usr/share/zoneinfo"; CONSTDATA auto tz_dir_buildroot = "/usr/share/zoneinfo/uclibc"; { static char* tz_local_env = getenv("TZDATA"); if (tz_local_env != nullptr) { static std::string tz_local_env_s = tz_local_env; return tz_local_env_s; } } // Check special path which is valid for buildroot with uclibc builds if(stat(tz_dir_buildroot, &sb) == 0 && S_ISDIR(sb.st_mode)) return tz_dir_buildroot; else if(stat(tz_dir_default, &sb) == 0 && S_ISDIR(sb.st_mode)) return tz_dir_default; else throw runtime_error("discover_tz_dir failed to find zoneinfo\n"); # else // __APPLE__ # if TARGET_OS_IPHONE # if TARGET_OS_SIMULATOR return "/usr/share/zoneinfo"; # else return "/var/db/timezone/zoneinfo"; # endif # else CONSTDATA auto timezone = "/etc/localtime"; if (!(lstat(timezone, &sb) == 0 && S_ISLNK(sb.st_mode) && sb.st_size > 0)) throw runtime_error("discover_tz_dir failed\n"); string result; unique_ptr rp(new char[sb.st_size]); const auto rp_length = readlink(timezone, rp.get(), sb.st_size); if (rp_length > 0) result = string(rp.get(), rp_length); // readlink doesn't null-terminate else throw system_error(errno, system_category(), "readlink() failed"); auto i = result.find("zoneinfo"); if (i == string::npos) throw runtime_error("discover_tz_dir failed to find zoneinfo\n"); i = result.find('/', i); if (i == string::npos) throw runtime_error("discover_tz_dir failed to find '/'\n"); return result.substr(0, i); # endif # endif // __APPLE__ } static const std::string& get_tz_dir() { static const std::string tz_dir = discover_tz_dir(); return tz_dir; } #endif // +-------------------+ // | End Configuration | // +-------------------+ #ifndef _MSC_VER static_assert(min_year <= max_year, "Configuration error"); #endif #if !defined(ANDROID) && !defined(__ANDROID__) static std::unique_ptr init_tzdb(); #endif // !defined(ANDROID) && !defined(__ANDROID__) tzdb_list::~tzdb_list() { const tzdb* ptr = head_; head_ = nullptr; while (ptr != nullptr) { auto next = ptr->next; delete ptr; ptr = next; } } tzdb_list::tzdb_list(tzdb_list&& x) NOEXCEPT : head_{x.head_.exchange(nullptr)} { } void tzdb_list::push_front(tzdb* tzdb) NOEXCEPT { tzdb->next = head_; head_ = tzdb; } tzdb_list::const_iterator tzdb_list::erase_after(const_iterator p) NOEXCEPT { auto t = p.p_->next; p.p_->next = p.p_->next->next; delete t; return ++p; } struct tzdb_list::undocumented_helper { static void push_front(tzdb_list& db_list, tzdb* tzdb) NOEXCEPT { db_list.push_front(tzdb); } }; static tzdb_list create_tzdb() { tzdb_list tz_db; tzdb_list::undocumented_helper::push_front(tz_db, init_tzdb().release()); return tz_db; } tzdb_list& get_tzdb_list() { static tzdb_list tz_db = create_tzdb(); return tz_db; } #if !defined(ANDROID) && !defined(__ANDROID__) inline static char tolower(char c) { return static_cast(std::tolower(c)); } inline static void tolower(std::string& s) { for (auto& c : s) c = tolower(c); } inline static std::string get_alpha_word(std::istream& in) { ws(in); std::string s; while (!in.eof() && std::isalpha(in.peek())) s.push_back(static_cast(in.get())); return s; } #endif // !defined(ANDROID) && !defined(__ANDROID__) inline static bool is_prefix_of(std::string const& key, std::string const& value) { const size_t size = std::min(key.size(), value.size()); return key.compare(0, size, value, 0, size) == 0; } #if !defined(ANDROID) && !defined(__ANDROID__) static unsigned parse_month(std::istream& in) { static std::string const month_names[] = {"january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"}; auto s = get_alpha_word(in); tolower(s); auto m = std::find_if(std::begin(month_names), std::end(month_names), [&s](std::string const& m) { return is_prefix_of(s, m); }) - month_names; if (m >= std::end(month_names) - std::begin(month_names)) throw std::runtime_error("oops: bad month name: " + s); return static_cast(++m); } #endif // !defined(ANDROID) && !defined(__ANDROID__) #if !USE_OS_TZDB #ifdef _WIN32 static void sort_zone_mappings(std::vector& mappings) { std::sort(mappings.begin(), mappings.end(), [](const date::detail::timezone_mapping& lhs, const date::detail::timezone_mapping& rhs)->bool { auto other_result = lhs.other.compare(rhs.other); if (other_result < 0) return true; else if (other_result == 0) { auto territory_result = lhs.territory.compare(rhs.territory); if (territory_result < 0) return true; else if (territory_result == 0) { if (lhs.type < rhs.type) return true; } } return false; }); } static bool native_to_standard_timezone_name(const std::string& native_tz_name, std::string& standard_tz_name) { // TOOD! Need be a case insensitive compare? if (native_tz_name == "UTC") { standard_tz_name = "Etc/UTC"; return true; } standard_tz_name.clear(); // TODO! we can improve on linear search. const auto& mappings = date::get_tzdb().mappings; for (const auto& tzm : mappings) { if (tzm.other == native_tz_name) { standard_tz_name = tzm.type; return true; } } return false; } // Parse this XML file: // https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml // The parsing method is designed to be simple and quick. It is not overly // forgiving of change but it should diagnose basic format issues. // See timezone_mapping structure for more info. static std::vector load_timezone_mappings_from_xml_file(const std::string& input_path) { std::size_t line_num = 0; std::vector mappings; std::string line; file_streambuf ibuf(input_path); std::istream is(&ibuf); auto error = [&input_path, &line_num](const char* info) { std::string msg = "Error loading time zone mapping file \""; msg += input_path; msg += "\" at line "; msg += std::to_string(line_num); msg += ": "; msg += info; throw std::runtime_error(msg); }; // [optional space]a="b" auto read_attribute = [&line, &error] (const char* name, std::string& value, std::size_t startPos) ->std::size_t { value.clear(); // Skip leading space before attribute name. std::size_t spos = line.find_first_not_of(' ', startPos); if (spos == std::string::npos) spos = startPos; // Assume everything up to next = is the attribute name // and that an = will always delimit that. std::size_t epos = line.find('=', spos); if (epos == std::string::npos) error("Expected \'=\' right after attribute name."); std::size_t name_len = epos - spos; // Expect the name we find matches the name we expect. if (line.compare(spos, name_len, name) != 0) { std::string msg; msg = "Expected attribute name \'"; msg += name; msg += "\' around position "; msg += std::to_string(spos); msg += " but found something else."; error(msg.c_str()); } ++epos; // Skip the '=' that is after the attribute name. spos = epos; if (spos < line.length() && line[spos] == '\"') ++spos; // Skip the quote that is before the attribute value. else { std::string msg = "Expected '\"' to begin value of attribute \'"; msg += name; msg += "\'."; error(msg.c_str()); } epos = line.find('\"', spos); if (epos == std::string::npos) { std::string msg = "Expected '\"' to end value of attribute \'"; msg += name; msg += "\'."; error(msg.c_str()); } // Extract everything in between the quotes. Note no escaping is done. std::size_t value_len = epos - spos; value.assign(line, spos, value_len); ++epos; // Skip the quote that is after the attribute value; return epos; }; // Quick but not overly forgiving XML mapping file processing. bool mapTimezonesOpenTagFound = false; bool mapTimezonesCloseTagFound = false; std::size_t mapZonePos = std::string::npos; std::size_t mapTimezonesPos = std::string::npos; CONSTDATA char mapTimeZonesOpeningTag[] = { ""); mapTimezonesCloseTagFound = (mapTimezonesPos != std::string::npos); if (!mapTimezonesCloseTagFound) { std::size_t commentPos = line.find(" " << x.target_; } // leap_second leap_second::leap_second(const std::string& s, detail::undocumented) { using namespace date; std::istringstream in(s); in.exceptions(std::ios::failbit | std::ios::badbit); std::string word; int y; MonthDayTime date; in >> word >> y >> date; date_ = date.to_time_point(year(y)); } static bool file_exists(const std::string& filename) { #ifdef _WIN32 std::wstring wfilename = convert_utf8_to_utf16(filename); return ::_waccess(wfilename.c_str(), 0) == 0; #else return ::access(filename.c_str(), F_OK) == 0; #endif } #if HAS_REMOTE_API // CURL tools namespace { struct curl_global_init_and_cleanup { ~curl_global_init_and_cleanup() { ::curl_global_cleanup(); } curl_global_init_and_cleanup() { if (::curl_global_init(CURL_GLOBAL_DEFAULT) != 0) throw std::runtime_error("CURL global initialization failed"); } curl_global_init_and_cleanup(curl_global_init_and_cleanup const&) = delete; curl_global_init_and_cleanup& operator=(curl_global_init_and_cleanup const&) = delete; }; struct curl_deleter { void operator()(CURL* p) const { ::curl_easy_cleanup(p); } }; } // unnamed namespace static std::unique_ptr curl_init() { static const curl_global_init_and_cleanup _{}; return std::unique_ptr{::curl_easy_init()}; } static bool download_to_string(const std::string& url, std::string& str) { str.clear(); auto curl = curl_init(); if (!curl) return false; std::string version; curl_easy_setopt(curl.get(), CURLOPT_USERAGENT, "curl"); curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); curl_write_callback write_cb = [](char* contents, std::size_t size, std::size_t nmemb, void* userp) -> std::size_t { auto& userstr = *static_cast(userp); auto realsize = size * nmemb; userstr.append(contents, realsize); return realsize; }; curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &str); curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, false); auto res = curl_easy_perform(curl.get()); return (res == CURLE_OK); } namespace { enum class download_file_options { binary, text }; } static bool download_to_file(const std::string& url, const std::string& local_filename, download_file_options opts, char* error_buffer) { auto curl = curl_init(); if (!curl) return false; curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, false); if (error_buffer) curl_easy_setopt(curl.get(), CURLOPT_ERRORBUFFER, error_buffer); curl_write_callback write_cb = [](char* contents, std::size_t size, std::size_t nmemb, void* userp) -> std::size_t { auto& of = *static_cast(userp); auto realsize = size * nmemb; of.write(contents, static_cast(realsize)); return realsize; }; curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, write_cb); decltype(curl_easy_perform(curl.get())) res; { std::ofstream of(local_filename, opts == download_file_options::binary ? std::ofstream::out | std::ofstream::binary : std::ofstream::out); of.exceptions(std::ios::badbit); curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &of); res = curl_easy_perform(curl.get()); } return res == CURLE_OK; } std::string remote_version() { std::string version; std::string str; if (download_to_string("https://www.iana.org/time-zones", str)) { CONSTDATA char db[] = "/time-zones/releases/tzdata"; CONSTDATA auto db_size = sizeof(db) - 1; auto p = str.find(db, 0, db_size); const int ver_str_len = 5; if (p != std::string::npos && p + (db_size + ver_str_len) <= str.size()) version = str.substr(p + db_size, ver_str_len); } return version; } // TODO! Using system() create a process and a console window. // This is useful to see what errors may occur but is slow and distracting. // Consider implementing this functionality more directly, such as // using _mkdir and CreateProcess etc. // But use the current means now as matches Unix implementations and while // in proof of concept / testing phase. // TODO! Use eventually. static bool remove_folder_and_subfolders(const std::string& folder) { # ifdef _WIN32 # if USE_SHELL_API // Delete the folder contents by deleting the folder. std::string cmd = "rd /s /q \""; cmd += folder; cmd += '\"'; return std::system(cmd.c_str()) == EXIT_SUCCESS; # else // !USE_SHELL_API // Create a buffer containing the path to delete. It must be terminated // by two nuls. Who designs these API's... std::vector from; from.assign(folder.begin(), folder.end()); from.push_back('\0'); from.push_back('\0'); SHFILEOPSTRUCT fo{}; // Zero initialize. fo.wFunc = FO_DELETE; fo.pFrom = from.data(); fo.fFlags = FOF_NO_UI; int ret = SHFileOperation(&fo); if (ret == 0 && !fo.fAnyOperationsAborted) return true; return false; # endif // !USE_SHELL_API # else // !_WIN32 # if USE_SHELL_API return std::system(("rm -R " + folder).c_str()) == EXIT_SUCCESS; # else // !USE_SHELL_API struct dir_deleter { dir_deleter() {} void operator()(DIR* d) const { if (d != nullptr) { int result = closedir(d); assert(result == 0); } } }; using closedir_ptr = std::unique_ptr; std::string filename; struct stat statbuf; std::size_t folder_len = folder.length(); struct dirent* p = nullptr; closedir_ptr d(opendir(folder.c_str())); bool r = d.get() != nullptr; while (r && (p=readdir(d.get())) != nullptr) { if (strcmp(p->d_name, ".") == 0 || strcmp(p->d_name, "..") == 0) continue; // + 2 for path delimiter and nul terminator. std::size_t buf_len = folder_len + strlen(p->d_name) + 2; filename.resize(buf_len); std::size_t path_len = static_cast( snprintf(&filename[0], buf_len, "%s/%s", folder.c_str(), p->d_name)); assert(path_len == buf_len - 1); filename.resize(path_len); if (stat(filename.c_str(), &statbuf) == 0) r = S_ISDIR(statbuf.st_mode) ? remove_folder_and_subfolders(filename) : unlink(filename.c_str()) == 0; } d.reset(); if (r) r = rmdir(folder.c_str()) == 0; return r; # endif // !USE_SHELL_API # endif // !_WIN32 } static bool make_directory(const std::string& folder) { # ifdef _WIN32 # if USE_SHELL_API // Re-create the folder. std::string cmd = "mkdir \""; cmd += folder; cmd += '\"'; return std::system(cmd.c_str()) == EXIT_SUCCESS; # else // !USE_SHELL_API return _mkdir(folder.c_str()) == 0; # endif // !USE_SHELL_API # else // !_WIN32 # if USE_SHELL_API return std::system(("mkdir -p " + folder).c_str()) == EXIT_SUCCESS; # else // !USE_SHELL_API return mkdir(folder.c_str(), 0777) == 0; # endif // !USE_SHELL_API # endif // !_WIN32 } static bool delete_file(const std::string& file) { # ifdef _WIN32 # if USE_SHELL_API std::string cmd = "del \""; cmd += file; cmd += '\"'; return std::system(cmd.c_str()) == 0; # else // !USE_SHELL_API return _unlink(file.c_str()) == 0; # endif // !USE_SHELL_API # else // !_WIN32 # if USE_SHELL_API return std::system(("rm " + file).c_str()) == EXIT_SUCCESS; # else // !USE_SHELL_API return unlink(file.c_str()) == 0; # endif // !USE_SHELL_API # endif // !_WIN32 } # ifdef _WIN32 static bool move_file(const std::string& from, const std::string& to) { # if USE_SHELL_API std::string cmd = "move \""; cmd += from; cmd += "\" \""; cmd += to; cmd += '\"'; return std::system(cmd.c_str()) == EXIT_SUCCESS; # else // !USE_SHELL_API return !!::MoveFile(from.c_str(), to.c_str()); # endif // !USE_SHELL_API } // Usually something like "c:\Program Files". static std::string get_program_folder() { return get_known_folder(FOLDERID_ProgramFiles); } // Note folder can and usually does contain spaces. static std::string get_unzip_program() { std::string path; // 7-Zip appears to note its location in the registry. // If that doesn't work, fall through and take a guess, but it will likely be wrong. HKEY hKey = nullptr; if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\7-Zip", 0, KEY_READ, &hKey) == ERROR_SUCCESS) { char value_buffer[MAX_PATH + 1]; // fyi 260 at time of writing. // in/out parameter. Documentation say that size is a count of bytes not chars. DWORD size = sizeof(value_buffer) - sizeof(value_buffer[0]); DWORD tzi_type = REG_SZ; // Testing shows Path key value is "C:\Program Files\7-Zip\" i.e. always with trailing \. bool got_value = (RegQueryValueExA(hKey, "Path", nullptr, &tzi_type, reinterpret_cast(value_buffer), &size) == ERROR_SUCCESS); RegCloseKey(hKey); // Close now incase of throw later. if (got_value) { // Function does not guarantee to null terminate. value_buffer[size / sizeof(value_buffer[0])] = '\0'; path = value_buffer; if (!path.empty()) { path += "7z.exe"; return path; } } } path += get_program_folder(); path += folder_delimiter; path += "7-Zip\\7z.exe"; return path; } # if !USE_SHELL_API static int run_program(const std::string& command) { STARTUPINFO si{}; si.cb = sizeof(si); PROCESS_INFORMATION pi{}; // Allegedly CreateProcess overwrites the command line. Ugh. std::string mutable_command(command); if (CreateProcess(nullptr, &mutable_command[0], nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) { WaitForSingleObject(pi.hProcess, INFINITE); DWORD exit_code; bool got_exit_code = !!GetExitCodeProcess(pi.hProcess, &exit_code); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); // Not 100% sure about this still active thing is correct, // but I'm going with it because I *think* WaitForSingleObject might // return in some cases without INFINITE-ly waiting. // But why/wouldn't GetExitCodeProcess return false in that case? if (got_exit_code && exit_code != STILL_ACTIVE) return static_cast(exit_code); } return EXIT_FAILURE; } # endif // !USE_SHELL_API static std::string get_download_tar_file(const std::string& version) { auto file = get_install(); file += folder_delimiter; file += "tzdata"; file += version; file += ".tar"; return file; } static bool extract_gz_file(const std::string& version, const std::string& gz_file, const std::string& dest_folder) { auto unzip_prog = get_unzip_program(); bool unzip_result = false; // Use the unzip program to extract the tar file from the archive. // Aim to create a string like: // "C:\Program Files\7-Zip\7z.exe" x "C:\Users\SomeUser\Downloads\tzdata2016d.tar.gz" // -o"C:\Users\SomeUser\Downloads\tzdata" std::string cmd; cmd = '\"'; cmd += unzip_prog; cmd += "\" x \""; cmd += gz_file; cmd += "\" -o\""; cmd += dest_folder; cmd += '\"'; # if USE_SHELL_API // When using shelling out with std::system() extra quotes are required around the // whole command. It's weird but necessary it seems, see: // http://stackoverflow.com/q/27975969/576911 cmd = "\"" + cmd + "\""; if (std::system(cmd.c_str()) == EXIT_SUCCESS) unzip_result = true; # else // !USE_SHELL_API if (run_program(cmd) == EXIT_SUCCESS) unzip_result = true; # endif // !USE_SHELL_API if (unzip_result) delete_file(gz_file); // Use the unzip program extract the data from the tar file that was // just extracted from the archive. auto tar_file = get_download_tar_file(version); cmd = '\"'; cmd += unzip_prog; cmd += "\" x \""; cmd += tar_file; cmd += "\" -o\""; cmd += get_install(); cmd += '\"'; # if USE_SHELL_API cmd = "\"" + cmd + "\""; if (std::system(cmd.c_str()) == EXIT_SUCCESS) unzip_result = true; # else // !USE_SHELL_API if (run_program(cmd) == EXIT_SUCCESS) unzip_result = true; # endif // !USE_SHELL_API if (unzip_result) delete_file(tar_file); return unzip_result; } static std::string get_download_mapping_file(const std::string& version) { auto file = get_install() + version + "windowsZones.xml"; return file; } # else // !_WIN32 # if !USE_SHELL_API static int run_program(const char* prog, const char*const args[]) { pid_t pid = fork(); if (pid == -1) // Child failed to start. return EXIT_FAILURE; if (pid != 0) { // We are in the parent. Child started. Wait for it. pid_t ret; int status; while ((ret = waitpid(pid, &status, 0)) == -1) { if (errno != EINTR) break; } if (ret != -1) { if (WIFEXITED(status)) return WEXITSTATUS(status); } printf("Child issues!\n"); return EXIT_FAILURE; // Not sure what status of child is. } else // We are in the child process. Start the program the parent wants to run. { if (execv(prog, const_cast(args)) == -1) // Does not return. { perror("unreachable 0\n"); _Exit(127); } printf("unreachable 2\n"); } printf("unreachable 2\n"); // Unreachable. assert(false); exit(EXIT_FAILURE); return EXIT_FAILURE; } # endif // !USE_SHELL_API static bool extract_gz_file(const std::string&, const std::string& gz_file, const std::string&) { # if USE_SHELL_API bool unzipped = std::system(("tar -xzf " + gz_file + " -C " + get_install()).c_str()) == EXIT_SUCCESS; # else // !USE_SHELL_API const char prog[] = {"/usr/bin/tar"}; const char*const args[] = { prog, "-xzf", gz_file.c_str(), "-C", get_install().c_str(), nullptr }; bool unzipped = (run_program(prog, args) == EXIT_SUCCESS); # endif // !USE_SHELL_API if (unzipped) { delete_file(gz_file); return true; } return false; } # endif // !_WIN32 bool remote_download(const std::string& version, char* error_buffer) { assert(!version.empty()); # ifdef _WIN32 // Download folder should be always available for Windows # else // !_WIN32 // Create download folder if it does not exist on UNIX system auto download_folder = get_install(); if (!file_exists(download_folder)) { if (!make_directory(download_folder)) return false; } # endif // _WIN32 auto url = "https://data.iana.org/time-zones/releases/tzdata" + version + ".tar.gz"; bool result = download_to_file(url, get_download_gz_file(version), download_file_options::binary, error_buffer); # ifdef _WIN32 if (result) { auto mapping_file = get_download_mapping_file(version); result = download_to_file( "https://raw.githubusercontent.com/unicode-org/cldr/master/" "common/supplemental/windowsZones.xml", mapping_file, download_file_options::text, error_buffer); } # endif // _WIN32 return result; } bool remote_install(const std::string& version) { auto success = false; assert(!version.empty()); std::string install = get_install(); auto gz_file = get_download_gz_file(version); if (file_exists(gz_file)) { if (file_exists(install)) remove_folder_and_subfolders(install); if (make_directory(install)) { if (extract_gz_file(version, gz_file, install)) success = true; # ifdef _WIN32 auto mapping_file_source = get_download_mapping_file(version); auto mapping_file_dest = get_install(); mapping_file_dest += folder_delimiter; mapping_file_dest += "windowsZones.xml"; if (!move_file(mapping_file_source, mapping_file_dest)) success = false; # endif // _WIN32 } } return success; } #endif // HAS_REMOTE_API static std::string get_version(const std::string& path) { std::string version; std::string path_version = path + "version"; if (file_exists(path_version)) { file_streambuf inbuf(path_version); std::istream infile(&inbuf); infile >> version; if (!infile.fail()) return version; } std::string path_news = path + "NEWS"; if (file_exists(path_news)) { file_streambuf inbuf(path_news); std::istream infile(&inbuf); while (infile) { infile >> version; if (version == "Release") { infile >> version; return version; } } } throw std::runtime_error("Unable to get Timezone database version from " + path); } static std::unique_ptr init_tzdb() { using namespace date; const std::string install = get_install(); const std::string path = install + folder_delimiter; std::string line; bool continue_zone = false; std::unique_ptr db(new tzdb); #if AUTO_DOWNLOAD if (!file_exists(install)) { auto rv = remote_version(); if (!rv.empty() && remote_download(rv)) { if (!remote_install(rv)) { std::string msg = "Timezone database version \""; msg += rv; msg += "\" did not install correctly to \""; msg += install; msg += "\""; throw std::runtime_error(msg); } } if (!file_exists(install)) { std::string msg = "Timezone database not found at \""; msg += install; msg += "\""; throw std::runtime_error(msg); } db->version = get_version(path); } else { db->version = get_version(path); auto rv = remote_version(); if (!rv.empty() && db->version != rv) { if (remote_download(rv)) { remote_install(rv); db->version = get_version(path); } } } #else // !AUTO_DOWNLOAD if (!file_exists(install)) { std::string msg = "Timezone database not found at \""; msg += install; msg += "\""; throw std::runtime_error(msg); } db->version = get_version(path); #endif // !AUTO_DOWNLOAD CONSTDATA char*const files[] = { "africa", "antarctica", "asia", "australasia", "backward", "etcetera", "europe", "pacificnew", "northamerica", "southamerica", "systemv", "leapseconds" }; for (const auto& filename : files) { std::string file_path = path + filename; if (!file_exists(file_path)) { continue; } file_streambuf inbuf(file_path); std::istream infile(&inbuf); while (infile) { std::getline(infile, line); if (!line.empty() && line[0] != '#') { std::istringstream in(line); std::string word; in >> word; tolower(word); if (is_prefix_of(word, "rule")) { db->rules.push_back(Rule(line)); continue_zone = false; } else if (is_prefix_of(word, "link")) { db->links.push_back(time_zone_link(line)); continue_zone = false; } else if (is_prefix_of(word, "leap")) { db->leap_seconds.push_back(leap_second(line, detail::undocumented{})); continue_zone = false; } else if (is_prefix_of(word, "zone")) { db->zones.push_back(time_zone(line, detail::undocumented{})); continue_zone = true; } else if (line[0] == '\t' && continue_zone) { db->zones.back().add(line); } else if (word.size() > 0 && word[0] == '#') { continue; } else { std::cerr << line << '\n'; } } } } std::sort(db->rules.begin(), db->rules.end()); Rule::split_overlaps(db->rules); std::sort(db->zones.begin(), db->zones.end()); db->zones.shrink_to_fit(); std::sort(db->links.begin(), db->links.end()); db->links.shrink_to_fit(); std::sort(db->leap_seconds.begin(), db->leap_seconds.end()); db->leap_seconds.shrink_to_fit(); #ifdef _WIN32 std::string mapping_file = get_install() + folder_delimiter + "windowsZones.xml"; db->mappings = load_timezone_mappings_from_xml_file(mapping_file); sort_zone_mappings(db->mappings); #endif // _WIN32 return db; } const tzdb& reload_tzdb() { #if AUTO_DOWNLOAD auto const& v = get_tzdb_list().front().version; if (!v.empty() && v == remote_version()) return get_tzdb_list().front(); #endif // AUTO_DOWNLOAD tzdb_list::undocumented_helper::push_front(get_tzdb_list(), init_tzdb().release()); return get_tzdb_list().front(); } #endif // !USE_OS_TZDB const tzdb& get_tzdb() { return get_tzdb_list().front(); } namespace { class recursion_limiter { unsigned depth_ = 0; unsigned limit_; class restore_recursion_depth; public: recursion_limiter(recursion_limiter const&) = delete; recursion_limiter& operator=(recursion_limiter const&) = delete; explicit constexpr recursion_limiter(unsigned limit) noexcept; restore_recursion_depth count(); }; class recursion_limiter::restore_recursion_depth { recursion_limiter* rc_; public: ~restore_recursion_depth(); restore_recursion_depth(restore_recursion_depth&&) = default; explicit restore_recursion_depth(recursion_limiter* rc) noexcept; }; inline recursion_limiter::restore_recursion_depth::~restore_recursion_depth() { --(rc_->depth_); } inline recursion_limiter::restore_recursion_depth::restore_recursion_depth(recursion_limiter* rc) noexcept : rc_{rc} {} inline constexpr recursion_limiter::recursion_limiter(unsigned limit) noexcept : limit_{limit} { } inline recursion_limiter::restore_recursion_depth recursion_limiter::count() { ++depth_; if (depth_ > limit_) throw std::runtime_error("recursion limit of " + std::to_string(limit_) + " exceeded"); return restore_recursion_depth{this}; } } // unnamed namespace const time_zone* #if HAS_STRING_VIEW tzdb::locate_zone(std::string_view tz_name) const #else tzdb::locate_zone(const std::string& tz_name) const #endif { // If a link-to-link chain exceeds this limit, give up thread_local recursion_limiter rc{10}; auto restore_count = rc.count(); auto zi = std::lower_bound(zones.begin(), zones.end(), tz_name, #if HAS_STRING_VIEW [](const time_zone& z, const std::string_view& nm) #else [](const time_zone& z, const std::string& nm) #endif { return z.name() < nm; }); if (zi == zones.end() || zi->name() != tz_name) { #if !USE_OS_TZDB auto li = std::lower_bound(links.begin(), links.end(), tz_name, #if HAS_STRING_VIEW [](const time_zone_link& z, const std::string_view& nm) #else [](const time_zone_link& z, const std::string& nm) #endif { return z.name() < nm; }); if (li != links.end() && li->name() == tz_name) { return locate_zone(li->target()); } #endif // !USE_OS_TZDB throw std::runtime_error(std::string(tz_name) + " not found in timezone database"); } return &*zi; } const time_zone* #if HAS_STRING_VIEW locate_zone(std::string_view tz_name) #else locate_zone(const std::string& tz_name) #endif { return get_tzdb().locate_zone(tz_name); } #if USE_OS_TZDB std::ostream& operator<<(std::ostream& os, const tzdb& db) { os << "Version: " << db.version << "\n\n"; for (const auto& x : db.zones) os << x << '\n'; os << '\n'; for (const auto& x : db.leap_seconds) os << x << '\n'; return os; } #else // !USE_OS_TZDB std::ostream& operator<<(std::ostream& os, const tzdb& db) { os << "Version: " << db.version << '\n'; std::string title("--------------------------------------------" "--------------------------------------------\n" "Name ""Start Y ""End Y " "Beginning ""Offset " "Designator\n" "--------------------------------------------" "--------------------------------------------\n"); int count = 0; for (const auto& x : db.rules) { if (count++ % 50 == 0) os << title; os << x << '\n'; } os << '\n'; title = std::string("---------------------------------------------------------" "--------------------------------------------------------\n" "Name ""Offset " "Rule ""Abrev ""Until\n" "---------------------------------------------------------" "--------------------------------------------------------\n"); count = 0; for (const auto& x : db.zones) { if (count++ % 10 == 0) os << title; os << x << '\n'; } os << '\n'; title = std::string("---------------------------------------------------------" "--------------------------------------------------------\n" "Alias ""To\n" "---------------------------------------------------------" "--------------------------------------------------------\n"); count = 0; for (const auto& x : db.links) { if (count++ % 45 == 0) os << title; os << x << '\n'; } os << '\n'; title = std::string("---------------------------------------------------------" "--------------------------------------------------------\n" "Leap second on\n" "---------------------------------------------------------" "--------------------------------------------------------\n"); os << title; for (const auto& x : db.leap_seconds) os << x << '\n'; return os; } #endif // !USE_OS_TZDB // ----------------------- #ifdef _WIN32 static std::string getTimeZoneKeyName() { DYNAMIC_TIME_ZONE_INFORMATION dtzi{}; auto result = GetDynamicTimeZoneInformation(&dtzi); if (result == TIME_ZONE_ID_INVALID) throw std::runtime_error("current_zone(): GetDynamicTimeZoneInformation()" " reported TIME_ZONE_ID_INVALID."); auto wlen = wcslen(dtzi.TimeZoneKeyName); char buf[128] = {}; assert(sizeof(buf) >= wlen+1); wcstombs(buf, dtzi.TimeZoneKeyName, wlen); if (strcmp(buf, "Coordinated Universal Time") == 0) return "UTC"; return buf; } const time_zone* tzdb::current_zone() const { std::string win_tzid = getTimeZoneKeyName(); std::string standard_tzid; if (!native_to_standard_timezone_name(win_tzid, standard_tzid)) { std::string msg; msg = "current_zone() failed: A mapping from the Windows Time Zone id \""; msg += win_tzid; msg += "\" was not found in the time zone mapping database."; throw std::runtime_error(msg); } return locate_zone(standard_tzid); } #else // !_WIN32 #if HAS_STRING_VIEW static std::string_view extract_tz_name(char const* rp) { using namespace std; string_view result = rp; CONSTDATA string_view zoneinfo = "zoneinfo"; size_t pos = result.rfind(zoneinfo); if (pos == result.npos) throw runtime_error( "current_zone() failed to find \"zoneinfo\" in " + string(result)); pos = result.find('/', pos); result.remove_prefix(pos + 1); return result; } #else // !HAS_STRING_VIEW static std::string extract_tz_name(char const* rp) { using namespace std; string result = rp; CONSTDATA char zoneinfo[] = "zoneinfo"; size_t pos = result.rfind(zoneinfo); if (pos == result.npos) throw runtime_error( "current_zone() failed to find \"zoneinfo\" in " + result); pos = result.find('/', pos); result.erase(0, pos + 1); return result; } #endif // HAS_STRING_VIEW static bool sniff_realpath(const char* timezone) { using namespace std; unique_ptr rp(realpath(timezone, nullptr), free); if (rp.get() == nullptr) throw system_error(errno, system_category(), "realpath() failed"); auto result = extract_tz_name(rp.get()); if (result.find("posix") == 0) return false; return result != "posixrules"; } const time_zone* tzdb::current_zone() const { // On some OS's a file called /etc/localtime may // exist and it may be either a real file // containing time zone details or a symlink to such a file. // On MacOS and BSD Unix if this file is a symlink it // might resolve to a path like this: // "/usr/share/zoneinfo/America/Los_Angeles" // If it does, we try to determine the current // timezone from the remainder of the path by removing the prefix // and hoping the rest resolves to a valid timezone. // It may not always work though. If it doesn't then an // exception will be thrown by local_timezone. // The path may also take a relative form: // "../usr/share/zoneinfo/America/Los_Angeles". { struct stat sb; CONSTDATA auto timezone = "/etc/localtime"; if (lstat(timezone, &sb) == 0 && S_ISLNK(sb.st_mode) && sb.st_size > 0) { using namespace std; static const bool use_realpath = sniff_realpath(timezone); if (use_realpath) { unique_ptr rp(realpath(timezone, nullptr), free); if (rp.get() == nullptr) throw system_error(errno, system_category(), "realpath() failed"); return locate_zone(extract_tz_name(rp.get())); } else { // +1 because st_size doesn't include the '\0' terminator const auto rp_size = sb.st_size + 1; unique_ptr rp(new char[rp_size]); const auto rp_length = readlink(timezone, rp.get(), rp_size); if (rp_length <= 0) throw system_error(errno, system_category(), "readlink() failed"); rp.get()[rp_length] = '\0'; // readlink doesn't null-terminate return locate_zone(extract_tz_name(rp.get())); } } } // On embedded systems e.g. buildroot with uclibc the timezone is linked // into /etc/TZ which is a symlink to path like this: // "/usr/share/zoneinfo/uclibc/America/Los_Angeles" // If it does, we try to determine the current // timezone from the remainder of the path by removing the prefix // and hoping the rest resolves to valid timezone. // It may not always work though. If it doesn't then an // exception will be thrown by local_timezone. // The path may also take a relative form: // "../usr/share/zoneinfo/uclibc/America/Los_Angeles". { struct stat sb; CONSTDATA auto timezone = "/etc/TZ"; if (lstat(timezone, &sb) == 0 && S_ISLNK(sb.st_mode) && sb.st_size > 0) { using namespace std; string result; unique_ptr rp(new char[sb.st_size]); const auto rp_length = readlink(timezone, rp.get(), sb.st_size); if (rp_length > 0) result = string(rp.get(), rp_length); // readlink doesn't null-terminate else throw system_error(errno, system_category(), "readlink() failed"); const size_t pos = result.find(get_tz_dir()); if (pos != result.npos) result.erase(0, get_tz_dir().size() + 1 + pos); return locate_zone(result); } } { // On some versions of some linux distro's (e.g. Ubuntu), // the current timezone might be in the first line of // the /etc/timezone file. std::ifstream timezone_file("/etc/timezone"); if (timezone_file.is_open()) { std::string result; std::getline(timezone_file, result); if (!result.empty()) return locate_zone(result); } // Fall through to try other means. } { // On some versions of some bsd distro's (e.g. FreeBSD), // the current timezone might be in the first line of // the /var/db/zoneinfo file. std::ifstream timezone_file("/var/db/zoneinfo"); if (timezone_file.is_open()) { std::string result; std::getline(timezone_file, result); if (!result.empty()) return locate_zone(result); } // Fall through to try other means. } { // On some versions of some bsd distro's (e.g. iOS), // it is not possible to use file based approach, // we switch to system API, calling functions in // CoreFoundation framework. #if TARGET_OS_IPHONE std::string result = date::iOSUtils::get_current_timezone(); if (!result.empty()) return locate_zone(result); #endif // Fall through to try other means. } { // On Android, it is not possible to use file based approach either, // we have to ask the value of `persist.sys.timezone` system property #if defined(ANDROID) || defined(__ANDROID__) char sys_timezone[PROP_VALUE_MAX]; if (__system_property_get("persist.sys.timezone", sys_timezone) > 0) { return locate_zone(sys_timezone); } #endif // defined(ANDROID) || defined(__ANDROID__) // Fall through to try other means. } { // On some versions of some linux distro's (e.g. Red Hat), // the current timezone might be in the first line of // the /etc/sysconfig/clock file as: // ZONE="US/Eastern" std::ifstream timezone_file("/etc/sysconfig/clock"); std::string result; while (timezone_file) { std::getline(timezone_file, result); auto p = result.find("ZONE=\""); if (p != std::string::npos) { result.erase(0, p+6); result.erase(result.rfind('"')); return locate_zone(result); } } // Fall through to try other means. } // On OpenWRT we need to check /etc/config/system // It will have a line with the following structure // ... // option zoneName 'Europe/Berlin' // ... { std::ifstream timezone_file("/etc/config/system"); if (timezone_file.is_open()) { for(std::string result; std::getline(timezone_file, result);) { std::string findStr = "option zoneName '"; size_t startPos = result.find(findStr); if (startPos != std::string::npos) { size_t endPos = result.find("'", startPos + findStr.size()); return locate_zone(result.substr(startPos + findStr.size(), endPos - startPos - findStr.size())); } } } } throw std::runtime_error("Could not get current timezone"); } #endif // !_WIN32 const time_zone* current_zone() { return get_tzdb().current_zone(); } } // namespace date #if defined(__GNUC__) && __GNUC__ < 5 # pragma GCC diagnostic pop #endif